From 580e5068d95f56cf55a5ba25acfc1bc3d37caac8 Mon Sep 17 00:00:00 2001 From: Feng Ren Date: Thu, 1 Aug 2024 16:35:05 +0800 Subject: [PATCH] JYCache V1 --- CMakeLists.txt | 24 + COPYING | 339 + README.md | 151 + build.sh | 13 + doc/cache_framework_design.md | 40 + doc/conf_spec/newcache.conf_spec | 38 + doc/frequently_asked_questions.md | 7 + doc/image/HybridCache_architecture.PNG | Bin 0 -> 247174 bytes doc/image/JYCache_architecture.PNG | Bin 0 -> 200526 bytes doc/image/page_structure.jpg | Bin 0 -> 23339 bytes doc/image/system_positioning.png | Bin 0 -> 100397 bytes doc/image/system_purpose.png | Bin 0 -> 78446 bytes global_cache/CMakeLists.txt | 46 + global_cache/Common.cpp | 122 + global_cache/Common.h | 130 + .../ErasureCodingWriteCacheClient.cpp | 333 + global_cache/ErasureCodingWriteCacheClient.h | 61 + global_cache/EtcdClient.h | 101 + global_cache/FileSystemDataAdaptor.h | 323 + global_cache/GarbageCollectorMain.cpp | 50 + global_cache/GlobalCacheClient.cpp | 368 + global_cache/GlobalCacheClient.h | 62 + global_cache/GlobalCacheServer.cpp | 107 + global_cache/GlobalCacheServer.h | 74 + global_cache/GlobalCacheServerMain.cpp | 41 + global_cache/GlobalDataAdaptor.cpp | 674 ++ global_cache/GlobalDataAdaptor.h | 143 + global_cache/Placement.h | 15 + global_cache/ReadCache.cpp | 215 + global_cache/ReadCache.h | 53 + global_cache/ReadCacheClient.cpp | 245 + global_cache/ReadCacheClient.h | 60 + global_cache/ReplicationWriteCacheClient.cpp | 248 + global_cache/ReplicationWriteCacheClient.h | 57 + global_cache/S3DataAdaptor.cpp | 188 + global_cache/S3DataAdaptor.h | 47 + global_cache/WriteCache.cpp | 404 ++ global_cache/WriteCache.h | 53 + global_cache/WriteCacheClient.h | 42 + global_cache/gcache.proto | 72 + install.sh | 16 + intercept/CMakeLists.txt | 34 + intercept/client.cpp | 138 + intercept/common/CMakeLists.txt | 8 + intercept/common/common.cpp | 175 + intercept/common/common.h | 143 + intercept/discovery/CMakeLists.txt | 22 + intercept/discovery/discovery.h | 66 + intercept/discovery/iceoryx_discovery.cpp | 125 + intercept/discovery/iceoryx_discovery.h | 41 + intercept/filesystem/CMakeLists.txt | 28 + intercept/filesystem/abstract_filesystem.h | 57 + intercept/filesystem/curve_filesystem.cpp | 166 + intercept/filesystem/curve_filesystem.h | 47 + intercept/filesystem/dummy_filesystem.cpp | 186 + intercept/filesystem/dummy_filesystem.h | 50 + intercept/filesystem/libcurvefs_external.cpp | 121 + intercept/filesystem/libcurvefs_external.h | 142 + intercept/filesystem/s3fs_filesystem.cpp | 222 + intercept/filesystem/s3fs_filesystem.h | 49 + intercept/filesystem/s3fs_lib.h | 63 + intercept/internal/CMakeLists.txt | 12 + intercept/internal/metainfo.h | 112 + intercept/internal/posix_op_req_res.cpp | 1014 +++ intercept/internal/posix_op_req_res.h | 650 ++ intercept/middleware/CMakeLists.txt | 45 + intercept/middleware/iceoryx_wrapper.cpp | 645 ++ intercept/middleware/iceoryx_wrapper.h | 76 + .../middleware/req_res_middleware_wrapper.cpp | 49 + .../middleware/req_res_middleware_wrapper.h | 80 + intercept/posix/CMakeLists.txt | 13 + .../posix/libsyscall_intercept_hook_point.h | 102 + intercept/posix/posix_helper.h | 37 + intercept/posix/posix_op.cpp | 657 ++ intercept/posix/posix_op.h | 493 ++ intercept/posix/syscall_client.h | 44 + intercept/registry/CMakeLists.txt | 40 + intercept/registry/client_server_registry.cpp | 169 + intercept/registry/client_server_registry.h | 78 + intercept/server.cpp | 32 + local_cache/CMakeLists.txt | 5 + local_cache/accessor.h | 52 + local_cache/common.cpp | 18 + local_cache/common.h | 40 + local_cache/config.cpp | 187 + local_cache/config.h | 93 + local_cache/data_adaptor.h | 89 + local_cache/errorcode.h | 21 + local_cache/page_cache.cpp | 440 ++ local_cache/page_cache.h | 161 + local_cache/read_cache.cpp | 257 + local_cache/read_cache.h | 57 + local_cache/write_cache.cpp | 286 + local_cache/write_cache.h | 74 + s3fs/CMakeLists.txt | 16 + s3fs/addhead.cpp | 248 + s3fs/addhead.h | 98 + s3fs/autolock.cpp | 78 + s3fs/autolock.h | 63 + s3fs/cache.cpp | 933 +++ s3fs/cache.h | 214 + s3fs/common.h | 68 + s3fs/common_auth.cpp | 71 + s3fs/config.h | 92 + s3fs/curl.cpp | 4576 ++++++++++++ s3fs/curl.h | 418 ++ s3fs/curl_handlerpool.cpp | 137 + s3fs/curl_handlerpool.h | 70 + s3fs/curl_multi.cpp | 394 ++ s3fs/curl_multi.h | 90 + s3fs/curl_util.cpp | 334 + s3fs/curl_util.h | 56 + s3fs/fdcache.cpp | 1157 +++ s3fs/fdcache.h | 118 + s3fs/fdcache_auto.cpp | 126 + s3fs/fdcache_auto.h | 74 + s3fs/fdcache_entity.cpp | 2907 ++++++++ s3fs/fdcache_entity.h | 197 + s3fs/fdcache_fdinfo.cpp | 1049 +++ s3fs/fdcache_fdinfo.h | 133 + s3fs/fdcache_page.cpp | 1035 +++ s3fs/fdcache_page.h | 136 + s3fs/fdcache_pseudofd.cpp | 133 + s3fs/fdcache_pseudofd.h | 71 + s3fs/fdcache_stat.cpp | 282 + s3fs/fdcache_stat.h | 66 + s3fs/fdcache_untreated.cpp | 277 + s3fs/fdcache_untreated.h | 76 + s3fs/hybridcache_accessor_4_s3fs.cpp | 554 ++ s3fs/hybridcache_accessor_4_s3fs.h | 65 + s3fs/hybridcache_disk_data_adaptor.cpp | 89 + s3fs/hybridcache_disk_data_adaptor.h | 40 + s3fs/hybridcache_s3_data_adaptor.cpp | 136 + s3fs/hybridcache_s3_data_adaptor.h | 33 + s3fs/metaheader.cpp | 341 + s3fs/metaheader.h | 71 + s3fs/mpu_util.cpp | 159 + s3fs/mpu_util.h | 64 + s3fs/openssl_auth.cpp | 444 ++ s3fs/psemaphore.h | 111 + s3fs/s3fs.cpp | 6181 +++++++++++++++++ s3fs/s3fs.h | 92 + s3fs/s3fs_auth.h | 66 + s3fs/s3fs_cred.cpp | 1628 +++++ s3fs/s3fs_cred.h | 187 + s3fs/s3fs_extcred.h | 144 + s3fs/s3fs_global.cpp | 50 + s3fs/s3fs_help.cpp | 657 ++ s3fs/s3fs_help.h | 41 + s3fs/s3fs_lib.cpp | 2992 ++++++++ s3fs/s3fs_lib.h | 69 + s3fs/s3fs_logger.cpp | 306 + s3fs/s3fs_logger.h | 270 + s3fs/s3fs_util.cpp | 592 ++ s3fs/s3fs_util.h | 123 + s3fs/s3fs_version.md | 2 + s3fs/s3fs_xml.cpp | 531 ++ s3fs/s3fs_xml.h | 62 + s3fs/s3objlist.cpp | 282 + s3fs/s3objlist.h | 85 + s3fs/sighandlers.cpp | 267 + s3fs/sighandlers.h | 79 + s3fs/string_util.cpp | 669 ++ s3fs/string_util.h | 136 + s3fs/threadpoolman.cpp | 264 + s3fs/threadpoolman.h | 109 + s3fs/types.h | 365 + test/CMakeLists.txt | 25 + test/hybridcache.conf | 39 + test/test_config.cpp | 26 + test/test_future.cpp | 72 + test/test_global_read_cache.cpp | 176 + test/test_global_read_cache_perf.cpp | 86 + test/test_global_write_cache_perf.cpp | 86 + test/test_page_cache.cpp | 174 + test/test_read_cache.cpp | 134 + test/test_write_cache.cpp | 199 + 177 files changed, 48139 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 COPYING create mode 100755 build.sh create mode 100644 doc/cache_framework_design.md create mode 100644 doc/conf_spec/newcache.conf_spec create mode 100644 doc/frequently_asked_questions.md create mode 100644 doc/image/HybridCache_architecture.PNG create mode 100644 doc/image/JYCache_architecture.PNG create mode 100644 doc/image/page_structure.jpg create mode 100644 doc/image/system_positioning.png create mode 100644 doc/image/system_purpose.png create mode 100644 global_cache/CMakeLists.txt create mode 100644 global_cache/Common.cpp create mode 100644 global_cache/Common.h create mode 100644 global_cache/ErasureCodingWriteCacheClient.cpp create mode 100644 global_cache/ErasureCodingWriteCacheClient.h create mode 100644 global_cache/EtcdClient.h create mode 100644 global_cache/FileSystemDataAdaptor.h create mode 100644 global_cache/GarbageCollectorMain.cpp create mode 100644 global_cache/GlobalCacheClient.cpp create mode 100644 global_cache/GlobalCacheClient.h create mode 100644 global_cache/GlobalCacheServer.cpp create mode 100644 global_cache/GlobalCacheServer.h create mode 100644 global_cache/GlobalCacheServerMain.cpp create mode 100644 global_cache/GlobalDataAdaptor.cpp create mode 100644 global_cache/GlobalDataAdaptor.h create mode 100644 global_cache/Placement.h create mode 100644 global_cache/ReadCache.cpp create mode 100644 global_cache/ReadCache.h create mode 100644 global_cache/ReadCacheClient.cpp create mode 100644 global_cache/ReadCacheClient.h create mode 100644 global_cache/ReplicationWriteCacheClient.cpp create mode 100644 global_cache/ReplicationWriteCacheClient.h create mode 100644 global_cache/S3DataAdaptor.cpp create mode 100644 global_cache/S3DataAdaptor.h create mode 100644 global_cache/WriteCache.cpp create mode 100644 global_cache/WriteCache.h create mode 100644 global_cache/WriteCacheClient.h create mode 100644 global_cache/gcache.proto create mode 100755 install.sh create mode 100644 intercept/CMakeLists.txt create mode 100644 intercept/client.cpp create mode 100644 intercept/common/CMakeLists.txt create mode 100644 intercept/common/common.cpp create mode 100644 intercept/common/common.h create mode 100644 intercept/discovery/CMakeLists.txt create mode 100644 intercept/discovery/discovery.h create mode 100644 intercept/discovery/iceoryx_discovery.cpp create mode 100644 intercept/discovery/iceoryx_discovery.h create mode 100644 intercept/filesystem/CMakeLists.txt create mode 100644 intercept/filesystem/abstract_filesystem.h create mode 100644 intercept/filesystem/curve_filesystem.cpp create mode 100644 intercept/filesystem/curve_filesystem.h create mode 100644 intercept/filesystem/dummy_filesystem.cpp create mode 100644 intercept/filesystem/dummy_filesystem.h create mode 100644 intercept/filesystem/libcurvefs_external.cpp create mode 100644 intercept/filesystem/libcurvefs_external.h create mode 100644 intercept/filesystem/s3fs_filesystem.cpp create mode 100644 intercept/filesystem/s3fs_filesystem.h create mode 100644 intercept/filesystem/s3fs_lib.h create mode 100644 intercept/internal/CMakeLists.txt create mode 100644 intercept/internal/metainfo.h create mode 100644 intercept/internal/posix_op_req_res.cpp create mode 100644 intercept/internal/posix_op_req_res.h create mode 100644 intercept/middleware/CMakeLists.txt create mode 100644 intercept/middleware/iceoryx_wrapper.cpp create mode 100644 intercept/middleware/iceoryx_wrapper.h create mode 100644 intercept/middleware/req_res_middleware_wrapper.cpp create mode 100644 intercept/middleware/req_res_middleware_wrapper.h create mode 100644 intercept/posix/CMakeLists.txt create mode 100644 intercept/posix/libsyscall_intercept_hook_point.h create mode 100644 intercept/posix/posix_helper.h create mode 100644 intercept/posix/posix_op.cpp create mode 100644 intercept/posix/posix_op.h create mode 100644 intercept/posix/syscall_client.h create mode 100644 intercept/registry/CMakeLists.txt create mode 100644 intercept/registry/client_server_registry.cpp create mode 100644 intercept/registry/client_server_registry.h create mode 100644 intercept/server.cpp create mode 100644 local_cache/CMakeLists.txt create mode 100644 local_cache/accessor.h create mode 100644 local_cache/common.cpp create mode 100644 local_cache/common.h create mode 100644 local_cache/config.cpp create mode 100644 local_cache/config.h create mode 100644 local_cache/data_adaptor.h create mode 100644 local_cache/errorcode.h create mode 100644 local_cache/page_cache.cpp create mode 100644 local_cache/page_cache.h create mode 100644 local_cache/read_cache.cpp create mode 100644 local_cache/read_cache.h create mode 100644 local_cache/write_cache.cpp create mode 100644 local_cache/write_cache.h create mode 100644 s3fs/CMakeLists.txt create mode 100644 s3fs/addhead.cpp create mode 100644 s3fs/addhead.h create mode 100644 s3fs/autolock.cpp create mode 100644 s3fs/autolock.h create mode 100644 s3fs/cache.cpp create mode 100644 s3fs/cache.h create mode 100644 s3fs/common.h create mode 100644 s3fs/common_auth.cpp create mode 100644 s3fs/config.h create mode 100644 s3fs/curl.cpp create mode 100644 s3fs/curl.h create mode 100644 s3fs/curl_handlerpool.cpp create mode 100644 s3fs/curl_handlerpool.h create mode 100644 s3fs/curl_multi.cpp create mode 100644 s3fs/curl_multi.h create mode 100644 s3fs/curl_util.cpp create mode 100644 s3fs/curl_util.h create mode 100644 s3fs/fdcache.cpp create mode 100644 s3fs/fdcache.h create mode 100644 s3fs/fdcache_auto.cpp create mode 100644 s3fs/fdcache_auto.h create mode 100644 s3fs/fdcache_entity.cpp create mode 100644 s3fs/fdcache_entity.h create mode 100644 s3fs/fdcache_fdinfo.cpp create mode 100644 s3fs/fdcache_fdinfo.h create mode 100644 s3fs/fdcache_page.cpp create mode 100644 s3fs/fdcache_page.h create mode 100644 s3fs/fdcache_pseudofd.cpp create mode 100644 s3fs/fdcache_pseudofd.h create mode 100644 s3fs/fdcache_stat.cpp create mode 100644 s3fs/fdcache_stat.h create mode 100644 s3fs/fdcache_untreated.cpp create mode 100644 s3fs/fdcache_untreated.h create mode 100644 s3fs/hybridcache_accessor_4_s3fs.cpp create mode 100644 s3fs/hybridcache_accessor_4_s3fs.h create mode 100644 s3fs/hybridcache_disk_data_adaptor.cpp create mode 100644 s3fs/hybridcache_disk_data_adaptor.h create mode 100644 s3fs/hybridcache_s3_data_adaptor.cpp create mode 100644 s3fs/hybridcache_s3_data_adaptor.h create mode 100644 s3fs/metaheader.cpp create mode 100644 s3fs/metaheader.h create mode 100644 s3fs/mpu_util.cpp create mode 100644 s3fs/mpu_util.h create mode 100644 s3fs/openssl_auth.cpp create mode 100644 s3fs/psemaphore.h create mode 100644 s3fs/s3fs.cpp create mode 100644 s3fs/s3fs.h create mode 100644 s3fs/s3fs_auth.h create mode 100644 s3fs/s3fs_cred.cpp create mode 100644 s3fs/s3fs_cred.h create mode 100644 s3fs/s3fs_extcred.h create mode 100644 s3fs/s3fs_global.cpp create mode 100644 s3fs/s3fs_help.cpp create mode 100644 s3fs/s3fs_help.h create mode 100644 s3fs/s3fs_lib.cpp create mode 100644 s3fs/s3fs_lib.h create mode 100644 s3fs/s3fs_logger.cpp create mode 100644 s3fs/s3fs_logger.h create mode 100644 s3fs/s3fs_util.cpp create mode 100644 s3fs/s3fs_util.h create mode 100644 s3fs/s3fs_version.md create mode 100644 s3fs/s3fs_xml.cpp create mode 100644 s3fs/s3fs_xml.h create mode 100644 s3fs/s3objlist.cpp create mode 100644 s3fs/s3objlist.h create mode 100644 s3fs/sighandlers.cpp create mode 100644 s3fs/sighandlers.h create mode 100644 s3fs/string_util.cpp create mode 100644 s3fs/string_util.h create mode 100644 s3fs/threadpoolman.cpp create mode 100644 s3fs/threadpoolman.h create mode 100644 s3fs/types.h create mode 100644 test/CMakeLists.txt create mode 100644 test/hybridcache.conf create mode 100644 test/test_config.cpp create mode 100644 test/test_future.cpp create mode 100644 test/test_global_read_cache.cpp create mode 100644 test/test_global_read_cache_perf.cpp create mode 100644 test/test_global_write_cache_perf.cpp create mode 100644 test/test_page_cache.cpp create mode 100644 test/test_read_cache.cpp create mode 100644 test/test_write_cache.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..370b848 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,24 @@ +project(hybridcache) + +cmake_minimum_required(VERSION 3.7) +cmake_policy(SET CMP0079 NEW) +set(CMAKE_CXX_STANDARD 17) + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-PIE") +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fno-PIE") + +list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/thirdparties) +list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/thirdparties/CmakeFiles) +include(ThirdPartyConfig) + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DNDEBUG -O3 -g -D__const__=__unused__ -pipe -W -Wno-deprecated -Wno-sign-compare -Wno-unused-parameter -fPIC") + +include_directories(AFTER ${CMAKE_SOURCE_DIR}/local_cache ${CMAKE_SOURCE_DIR}/global_cache) +include_directories(AFTER ${CMAKE_BINARY_DIR}/local_cache ${CMAKE_BINARY_DIR}/global_cache) + +# subdirectory +add_subdirectory(local_cache) +add_subdirectory(global_cache) +add_subdirectory(s3fs) +add_subdirectory(intercept) +add_subdirectory(test) diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..d511905 --- /dev/null +++ b/COPYING @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/README.md b/README.md index e53ce33..5532956 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,153 @@ # JYCache +**九源缓存存储系统(简称:JYCache)** 是一款面向个人使用、大模型训练推理等多种场景,适配大容量对象存储等多种底层存储形态,高性能、易扩展的分布式缓存存储系统。通过层次化架构、接入层优化、I/O优化等多种组合优化,JYCache 不仅支持文件顺序/随机读写,其读写性能也领先国际主流产品 Alluxio。JYCache 现支持在 X86(Intel、AMD、海光等)及 ARM(鲲鹏、飞腾等)平台下运行。 + +缓存存储系统面向个人使用及集群使用等场景,可为用户提供以下两种运行模式: +1. **单机对象加速**:将 S3 对象存储通过 POSIX 接口挂载到本地,像本地磁盘一样进行读写访问。S3 上的一个完整对象对应本地的一个文件,通过对象名构造目录树结构。进一步地,热点数据可缓存于本地的 DRAM/SSD,通过减少与 S3 的数据交互操作,可提升文件系统性能。 +2. **分布式对象加速**:将 S3 对象存储通过 POSIX 接口挂载到本地,像本地磁盘一样进行读写访问。S3 上的一个完整对象对应本地的一个文件,通过对象名构造目录树结构。热点数据呈现两级缓存结构,除各个客户端内部 DRAM/SSD 缓存外,还提供一层共享的 DRAM/SSD 缓存,进一步提高缓存命中率,提升并发读等场景下的 IO 性能。 + +## 主要特性 + + - **兼容 POSIX 接口**。通过 FUSE 或动态库劫持技术,应用程序无需重新编译即可立即实现缓存存储加速。 + - **高可用缓存写**。数据写入缓存层即可视为持久化,通过多副本、纠删码机制实现缓存层内数据高可用,降低下层存储系统压力,提高 I/O 性能。 + - **支持用户态零拷贝 I/O**。动态库劫持技术(Syscall intercept)实现全用户态 I/O,降低上下文切换和拷贝,实现极限性能。 + - **层次化缓存存储**。本地缓存与计算任务同机部署,使用高速共享缓存可为用户进程提供高达 45GB/s 的缓存带宽;为进一步提高分布式系统缓存效率,可额外部署全局缓存服务,通过与多个本地缓存相关联,进一步提高缓存命中率。 + - **易于扩展和集成**。本地缓存与全局缓存采用模块化设计,可依据业务需要实现多样的组合。 + - **兼容多种平台**。支持在 X86(Intel、AMD、海光等)及 ARM(鲲鹏、飞腾等)平台下运行。 + +## 系统架构 + +![](doc/image/JYCache_architecture.PNG) +在单机对象加速部署模式下,对象存储可通过 FUSE(基于S3FS(V1.94)实现) 或系统调用劫持等方式挂载到本地,用户可像本地磁盘一样进行读写访问。对象存储系统上的一个完整对象对应本地的一个文件,通过对象名构造目录树结构。热点数据可缓存于本地的 DRAM/SSD,通过减少与对象存储系统的数据交互操作,可提升文件系统性能。 + +在分布式对象加速模式下,热点数据呈现两级缓存结构,除各个客户端内部 DRAM/SSD 缓存外,还提供一层共享的 DRAM/SSD 缓存,进一步提高缓存命中率,提升并发读等场景下的 IO 性能。 + +缓存存储系统的两个核心部件是客户端缓存模块及全局缓存模块。客户端缓存模块内部包含写缓存、读缓存。客户端缓存模块按需向全局缓存服务器发出 RPC 通信请求,实现数据的传递。全局缓存服务器包含写缓存和读缓存,其中写缓存提供多副本等高可用模式。当用户发出下刷(fsync)请求时,写数据会落入此处,可容忍少量全局缓存服务器故障时不丢失写入的数据。无论是读缓存还是写缓存,都会按需调用数据源访问组件访问对象存储等底层存储资源,从而轻松适配其他类型的底层存储。 + +此外,在intercept模式的缓存系统中,我们采用了client-server+中间件架构,利用系统调用拦截技术捕获POSIX请求,将posix请求封装后发送至服务器处理,处理完成后返回至客户端。通过绕过FUSE内核模块和采用零拷贝中间件,最大限度地减少了数据拷贝和系统开销,不仅确保了与常见posix接口的兼容,还显著提升了系统性能,尤其在读写密集的场景中,避免了数据的重复拷贝,性能优势明显。 + +## 系统性能 + +顺序读性能使用 FIO 测试工具,带宽数据如下表所示: + +| BS | 优化前 | JYCache(FUSE) | JYCache(intercept) | +| ------------ | ------------ | ------------ | ------------ | +| 4K | 761MiB/s | 933MiB/s | 3576MiB/s | +| 16K | 706MiB/s | 3643MiB/s | 11.6GiB/s | +| 128K | 2268MiB/s | 22.6GiB/s | 38GiB/s | + +顺序写性能使用 FIO 测试工具,带宽数据如下表所示: + +| BS | 优化前 | JYCache(FUSE) | JYCache(intercept) | +| ------------ | ------------ | ------------ | ------------ | +| 4K | 624MiB/s | 1226MiB/s | 2571MiB/s | +| 16K | 2153MiB/s | 5705MiB/s | 9711MiB/s | +| 128K | 7498MiB/s | 23.5GiB/s | 31.2GiB/s | + +## 系统构建 +**环境要求** + +- GCC 9.3.0 +- GLIBC 2.31 +- CMake 3.7 +- C++ 17 +- FUSE >= 2.6 + +**从源码构建** + +直接在根目录下运行build.sh脚本 +```bash +sh build.sh +``` +*在build.sh脚本中,会自动下载第三方依赖。* + +**系统安装** + +编译完成后,在根目录下运行install.sh脚本 +```bash +sh install.sh +``` + +## 快速使用 + +执行install.sh脚本后会在当前目录下构建JYCache运行环境,其目录为JYCache_Env。下述使用方法均以JYCache_Env为根目录。 + +**一、JYCache普通模式(不启用全局缓存)** + +修改conf/newcache.conf配置文件中的`UseGlobalCache=0` +```bash +# 1.启动minio +cd ./minio && sh start.sh && cd .. +# 2.启动s3fs +sh start_s3fs.sh +``` +启动完成后,在挂载目录 ./mnt 下的文件操作均为JYCache控制。 + +*注:需要在此模式下,在挂载目录 ./mnt 创建文件夹testdir,此为intercept模式所需。* + +**关闭服务** +```bash +sh stop_s3fs.sh +cd ./minio && sh stop.sh && cd .. +``` + +**二、JYCache普通模式(启用全局缓存)** + +修改conf/newcache.conf配置文件中的`UseGlobalCache=1` +```bash +# 1.启动minio +cd ./minio && sh start.sh && cd .. +# 2.启动etcd +sh start_etcd.sh +# 3.启动全局缓存 +sh start_global.sh +# 4.启动s3fs +sh start_s3fs.sh +``` +启动完成后,在挂载目录 ./mnt 下的文件操作均为JYCache控制 + +**关闭服务** +```bash +sh stop_s3fs.sh +sh stop_global.sh +sh stop_etcd.sh +cd ./minio && sh stop.sh && cd .. +``` + +**三、JYCache intercept模式** + +此模式也支持全局缓存,方法与二同。下述以不开全局缓存为例: +```bash +# 1.启动minio +cd ./minio && sh start.sh && cd .. +# 2.启动intercept_server +sh start_intercept_server.sh +``` +启动完成后,在JYCache_Env根目录下执行 +```bash +LD_LIBRARY_PATH=./libs/:$LD_LIBRARY_PATH LD_PRELOAD=./libintercept_client.so ${cmd} +``` +其中`${cmd}`为用户实际文件操作的命令。例如: +```bash +LD_LIBRARY_PATH=./libs/:$LD_LIBRARY_PATH LD_PRELOAD=./libintercept_client.so ll /testdir/ +``` +需要在testdir目录下进行文件操作,才为JYCache intercept模式控制。 +*且使用intercept模式前需要先通过普通模式在挂载目录下创建文件夹testdir。* + +**关闭服务** +```bash +sh stop_intercept_server.sh +cd ./minio && sh stop.sh && cd .. +``` + +## 常见问题 + +[常见问题](doc/frequently_asked_questions.md) + +## 许可 + +本项目使用了以下遵循GPLv2许可的代码: +- S3FS (https://github.com/s3fs-fuse/s3fs-fuse) + +This software is licensed under the GNU GPL version 2. + diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..2302716 --- /dev/null +++ b/build.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +if [ ! -d "./thirdparties" ]; then + wget https://madstorage.s3.cn-north-1.jdcloud-oss.com/JYCache_Dendepency_x64.tgz + md5=`md5sum JYCache_Dendepency_x64.tgz | awk {'print $1'}` + if [ "$md5" != "48f67dd9b7bcb1b2bdd6be9f2283b714" ]; then +   echo 'JYCache_Dendepency version inconsistency!' + exit 1 + fi + tar -zxvf JYCache_Dendepency_x64.tgz +fi + +mkdir -p build && cd build +cmake .. && cmake --build . -j 16 diff --git a/doc/cache_framework_design.md b/doc/cache_framework_design.md new file mode 100644 index 0000000..e994c3e --- /dev/null +++ b/doc/cache_framework_design.md @@ -0,0 +1,40 @@ +# 缓存系统设计 + +### 设计背景 + +在用户和数据服务器之间构建一套缓存系统,该缓存系统可以让用户以本地文件的形式透明且高效地访问数据服务器中的数据。其中,数据服务器的类型有对象存储、自建全局缓存等。以数据服务器为对象存储为例,用户可以通过fuse以本地文件形式访问存储在远端的对象,且远端的对象索引是用户可懂的。 +![](image/system_purpose.png) + +### 系统定位 +该缓存系统支持多种数据源,包括S3对象存储、自建全局缓存等,故称为HybridCache。同时借助S3FS对fuse的支持,以及其在元数据管理方面的能力,实现fuse模式下的文件管理操作。HybridCache的定位如下图所示: +![](image/system_positioning.png) + +### 系统架构 +HybridCache架构如下图所示: +![](image/HybridCache_architecture.PNG) + +1.写缓存模块 + +写缓存模块的定位是本地写缓存,写缓存中的key是文件的path,不理解远端数据源(对象存储和全局缓存等),从write->flush的过程由上层去做。 + +2.读缓存模块 + +读缓存模块的定位是文件(以远端数据源为对象存储为例)的只读缓存,读缓存中的key是对象的key。读缓存需要用到本地缓存,以及远端缓存(对象存储和全局缓存等)。 + +3.数据源访问组件 + +数据源访问组件负责和远端数据源进行交互,涉及数据的上传下载等。以Adaptor的形式支持多种数据源,包括对象存储和全局缓存等。 + +4.缓存管理组件 + +内存管理组件管理本地缓存,写缓存模块和读缓存模块中实际的本地缓存就是用的该组件。 +在本地缓存中,我们直接将文件切分为固定大小的page(page大小可配置,下文以64KB为例),并使用CacheLib来维护这些page。page在CacheLib中以KV形式进行存储,其存储结构如下: +- key为 cacheKey_pageid。读写模块各自维护自己的本地缓存,cacheKey在写缓存模块中就是文件的path,在读缓存模块中就是S3上对象的key。pageid即为页号,通过offset/64KB计算得来。 +- value的数据结构如下: +![](image/page_structure.jpg) + +通过 cacheKey+offset+size 即可接操作指定文件中的特定page。page并发操作的安全性是通过CacheLib自身的机制以及page内的lock和新旧版号位来保证。 + +5.HybridCache访问组件 + +HybridCache访问组件定位在胶水层,要根据上层调用方的特性定制化实现,其内需要理解到上层调用方的逻辑。 diff --git a/doc/conf_spec/newcache.conf_spec b/doc/conf_spec/newcache.conf_spec new file mode 100644 index 0000000..be7a951 --- /dev/null +++ b/doc/conf_spec/newcache.conf_spec @@ -0,0 +1,38 @@ +# ReadCache +ReadCacheConfig.CacheConfig.CacheName # 读缓存名称 +ReadCacheConfig.CacheConfig.MaxCacheSize # 读缓存内存容量限制 +ReadCacheConfig.CacheConfig.PageBodySize # 读缓存page大小 +ReadCacheConfig.CacheConfig.PageMetaSize # 读缓存page元数据大小 +ReadCacheConfig.CacheConfig.EnableCAS # 读缓存是否启用CAS +ReadCacheConfig.CacheConfig.CacheLibConfig.EnableNvmCache # 读缓存是否开启nvm缓存 +ReadCacheConfig.CacheConfig.CacheLibConfig.RaidPath # nvm缓存文件目录 +ReadCacheConfig.CacheConfig.CacheLibConfig.RaidFileNum # nvm缓存文件数量限制 +ReadCacheConfig.CacheConfig.CacheLibConfig.RaidFileSize # nvm单个缓存文件大小限制 +ReadCacheConfig.CacheConfig.CacheLibConfig.DataChecksum # nvm缓存是否进行数据校验 +ReadCacheConfig.DownloadNormalFlowLimit # 读缓存内存未命中从远端下载时的平峰流控 +ReadCacheConfig.DownloadBurstFlowLimit # 读缓存内存未命中从远端下载时的顶峰流控 + +# WriteCache +WriteCacheConfig.CacheConfig.CacheName # 写缓存名称 +WriteCacheConfig.CacheConfig.MaxCacheSize # 写缓存内存容量限制 +WriteCacheConfig.CacheConfig.PageBodySize # 写缓存page大小 +WriteCacheConfig.CacheConfig.PageMetaSize # 写缓存page元数据大小 +WriteCacheConfig.CacheConfig.EnableCAS # 写缓存是否启用CAS +WriteCacheConfig.CacheSafeRatio # 写缓存安全容量阈值(百分比), 缓存达到阈值时阻塞待异步flush释放空间 + +# GlobalCache +UseGlobalCache # 全局缓存开关 +GlobalCacheConfig.EnableWriteCache # 全局缓存是否启用写缓存 +GlobalCacheConfig.EtcdAddress # etcd地址,例如 http://127.0.0.1:2379 +GlobalCacheConfig.GlobalServers # 全局缓存服务端地址,例如 127.0.0.1:8000 +GlobalCacheConfig.GflagFile # 全局缓存gflag文件形式输入 + +ThreadNum=48 # 线程数 +BackFlushCacheRatio # 写缓存异步flush阈值(百分比) +UploadNormalFlowLimit # 上传平峰流控 +UploadBurstFlowLimit # 上传顶峰流控 +LogPath # 日志文件路径 +LogLevel # 日志级别,INFO=0, WARNING=1, ERROR=2, FATAL=3 +EnableLog # 是否启用日志打印 +FlushToRead # 文件flush完成后是否写入读缓存 +CleanCacheByOpen # 文件open时是否清理读缓存 diff --git a/doc/frequently_asked_questions.md b/doc/frequently_asked_questions.md new file mode 100644 index 0000000..cc36869 --- /dev/null +++ b/doc/frequently_asked_questions.md @@ -0,0 +1,7 @@ +**1. 如何切换挂载目录?** + +在start_s3fs.sh中 +```bash +LD_LIBRARY_PATH=./libs/:$LD_LIBRARY_PATH nohup ./s3fs -o passwd_file=./conf/passwd -o use_path_request_style -o endpoint=us-east-1 -o url=http://127.0.0.1:9000 -o bucket=test ./mnt -o dbglevel=err -o use_cache=./diskcache -o del_cache -o newcache_conf=./conf/newcache.conf -f >> ./log/s3fs.log 2>&1 & +``` +更换其中的 `./mnt` 即可 \ No newline at end of file diff --git a/doc/image/HybridCache_architecture.PNG b/doc/image/HybridCache_architecture.PNG new file mode 100644 index 0000000000000000000000000000000000000000..7dd1657790d8b002f21a62b85ac9cc5d39ec8c33 GIT binary patch literal 247174 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

& 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

Fhgq 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

=em8#<{4m3e7iQZg8)?* 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 TV|0z~3jv3VgR4EkE+F+Eoyz~w literal 0 HcmV?d00001 diff --git a/doc/image/JYCache_architecture.PNG b/doc/image/JYCache_architecture.PNG new file mode 100644 index 0000000000000000000000000000000000000000..c12f5781431f3c71555c12d89c35ae477ab53ca4 GIT binary patch literal 200526 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 literal 0 HcmV?d00001 diff --git a/doc/image/page_structure.jpg b/doc/image/page_structure.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0d2bf5d85ea5ce5f9daa4fc849cec099b8e84e07 GIT binary patch literal 23339 zcmeIa2Ut_f)-W7V@PL8`5RhUEO@e@wfHYA^=p8~2NGL*R8k%$wkuK7aDxrlWfRun# z3%wH{bWo69rAz0jEv@0H;oVfaA$igGzF8W}4a>@=B`E-)=MmPT1<`0AM{A0H8Jm0O-2`04n3Z+&ek^gtluZx7beT zayt1v0N4Yp0M`IY07rlYfd2#&0^9-!0z{8T0CIrSWZ&WY@$?Ctp*Zs$exaZsr=a|W zii+|VN=mBp7tT|iqd7-ONli;lbK%!tX@8|UPe)JpEBy)n>-R!VeSh-wnO{yG{Pi5= zxs%j?5{};kXn#5N3%SwhQ$PS2?WxnWr;h6ZmrrEvI{;3f`fGE4IZHu)hU_%u_uHz! z0#1>UpQ0e6p!nqs`RUW-C*4DK`V2Y6S=wLdE*Zr3&|kkOD1Ohqgna z7ql7eldTIl@-i7Lx2a_8V>CS-P}Zm%rcudP8G!=d>*mxJVQQ1kw-+sUchz#;fNg+} zey@?4epo#C=<2|c9Tn!Et?{30&xM)s&MIkZ$#T#G3APu5;*4R8{Z-d>EIL(W6TB9b z=dK8Z(OwA+hnuc)*(6oy_hX$*e&>d$%;CO+!W!AxZ3ocne-XcaLcL7dfGFQEe;|Q8 z;mPOaqG)vQN|kuBtnC@ci z$})uvc&(geBM|RpH0a>CvVdNk!Oi%|D(M5g6a=rsG^8;skTdPEkJNs*Jh81rYT1#XgIy2nW7ZhS1giXjf?}(K%xx>f6M3;fS zY+uwXiycD`KaD=Wvf#6)id+X!W|DjsVYRFIYNSP&r-Yh%X9q;TA(DZ31v?8m229ZR z1qX+quM_Bx-uums&>sUd>UJZ)+w_0HkBT)=SGnScY5%X8D1R7v<}kW~AE&gx<{^U( z9s_ok30LcXxjIye4bita27D^(^ZWIhpLGxRk6g`Z#CCuC5oDJ93?u;0|IckAjh z`c$0#*`tUenau;4#PuiKhfj_H#4_%q^q`F^f3EOPypBV0mpaoP$P^y*|3TN!TOX(k zIDOqep}?RDrgncovGdCp|z;&bjLnxm#u*@8-&vwbwC9jyj!z*_4ocbfYfI_J3^+sh*K6H*P%6(q@g^eWdko3wtr zifs3K=}cIDZs|Cp&{)>5Zkiaq)l8d%EgGX?pGl@+k%WDPVVc00X=G-3oU|hAO917c z<%JU-c?|F!(K@*6f0(R{0mzut*HbFea^+;!1yG#3Ovj=4LY@!-A(>!n6@$}|*hjt0 zRp*sO!*{EztnN89Q5Rv_=-%Dv%ae0osx&i5{{q~POG5h(2W%<4N{CPJ9&La8@U<+W z-dn$drIA1nLt%G;wyWYJay%9{1FksgeQYjkXk)qOZk^EsTHkn)0C(vD%6i46`8C~@ zc<52;6+QMAF4aozo2ug~WV)MiN`GjmdQ zrkfTnTHBe{ip$!zqpOb&C0N#UM z|2gYFb#*$n?P_IP+n#UcVW}KWT(xK)U;Qq3L1qMU8Z>y}kQL!K|IJTu8lbU%>KKq| zuW=}IVMglD75+GGv_MlDHZ$zs{LFUoCv?%JwR#k*4-Gy2-0D$q3`h;RE?>oeNryqA zbkq=o_ovBZWV^(t&zrVP?;8Gm%%{1J41Z?J(V;v7TTTuPa~%}t{}}T&hnYt%A70k3 z>?4}Zd<>Yb3%cqt&YE60q?kvoyHs+8SjNSao^4D}c4$J4G4w;iRLk;1jj>=QjOH=G z5hRPuG^DQFty!`2OUF~I(x>X!;>>s^@Hi@>73smaTU@!MpfseQxjwt!_JvES!oA3f z>UUPK$}$eI3b2N0?vRtixw}mUcC^KduAcK%t47j7)Dlr@@*O1XqcQ#_=!-l?cKSz^ zI5aeP6S7l)dwmfg^NWUyqH6Fr^=GqS27_s}nZmDd1|8iaWsL9rpQ{|Q2Tx{7HlORJ z?PRiy!YE;_mFzn63>zCf0Rq0X0jxHZqZ5J;vlMm8qK%4fEOO@#Z3c!GRr?$RJo#5< zW;e!nP6-UFy@%O)z^2&F+ad$`hF@Pz-mF_xL*cO>+##I#H)b zVBL1zDijY~bCd13nz^UlQaZ&$e1KtU-?VWfS-W;dr$k3%A*dciqhsYqq8P2d9{+;f zl*6`Z(<>RaW@uA97ir)!J<~U0DzB5uNHGnjw@$l6-mfme=o-bLw9=L`PC3lNWKOv#2dHbdN@mLbqW0Ae#b`Yv1;qq=048*cV4Q z>)mB$&F1xfmpzz`OP-gbcF4_^eu7c+#+`F%^Fai*N-jJSv{>o#@D}F)WSA>68Glg1 z_)4MTersJt{^Lyp9nA&)_^006}x}#p>;evh#yRpD^&p3xRN)Nm;dRNNct_G)6cAJ{Usz=8H5Vhv-Ln z!liz3%$QUupWV~A&1H&g$lAe$EKYXiZ&g(zY3vMD!^ouhf=Xo2xxhEG3jwD`1~nG1 z_I$ca98=!Y=svi>3SV85y1cV;G_bT0-*%&`?a#Qg)iJHEvD7yW@R4o@lHW z1l(JqR1~~Qqs-CwfSsj@m!&{~|AnHb$AX*tojaXzl4%VUMH+M8q8jwc^Wo7Eo714B z*K&4w$#X*$2Hn|cK?^x=PWK`BG6Ss^gEES)<|tOom+!tH4@yb@+?}o z*!e5ltX$I51{Vk-XB}N+GNlWHZtZ%DjK!hUx=@-(uX_$Wdd;O?MxK||6CerYRyT^* zxH58HPIgM1PMME$#A3z_#=JMT&_S&3rVq=I?aT}227}sSt|@Itv5kep5jqKomUcq^ zP6d1}Ys8{E+-{WU`z=H9z>)Vd4u z-u1J@iW+wfF5Xs$N9BE8a?wr66w;^A>8QdtqYw||}!|TWFh^uQ_EX}sm zh5FLVYP3w6;Ma)$7ky>ZZSF>8Fgs>TNzv@bWf_|mj-C)64i_cSBI0#%I^nbz&R45D zSn^Teahtr1LAbc*2tf#z9?HdmbS5lz#6(TAa?`M8w6?VtU1pT}Sh1{EN9sWEYPA-i zxYE8#zSP*!8#Y)Ea)#ws>0h0~KWRfrX4?@Y?+p9M%O)`A;U=IF)qP&W>7|28v^wKy zC1cfPZ|dS58FQBbd-WZ9ZJ5qhB_UKgC3a3d^76G+sHMJ~fh~1wEl^+t3I1dS6FF@` z;T|F^m1Cxy;^`(?yWFDem9Zy9YJ(bigJN$4W1D+MI`ySPLkfrOOpJN+_$$n+FNC#Cp^n&~aKPpS1q zg8eEX&B z`9!xVxP)gJh9t!*MXuj}|D+MG7-dpx9GsLBR*-aq#pc#IIr}!?J0GpJ<+1Q$Ux_@E zN2z>rVIIbI?0h=m!3AGmH&`l0DB!g8UH4xc8DXz)T2dDk0rAUGu#$BsECU9~} z3n*o1YkMLvEwRCv=yprB>90L3cm;@}z2nB6o&}1P^Ihkb$q^4OotOx2dKJ)u5#lQ{ zf|JR`9Yq*Vp(nD7bAv`P`b7d_Ad zoBw`R=vhL3<$^s+6%nkb(x6ZidMI@uhP!$UP_Mn~1ZY|x?_=cWuAC~`=fi#6s+zT` zkz6>;P;ixZJCjpY8^tV^)U3HhqBY%w^|oBBUn}eVb>%X;#Nms2TD6^Mel9|5!3-8x zbiG5dqs}le>FE%2P|-{4+K=o}Fl;4vQcfs&A`K`ZVJWosx_=7igVFd>&#V1sUTh+z zi))#JD|0HHfIxIkB{B)=X|S*z+rr36b@Vcm#G_q)<{9W2r9>8&Tyg*nyH>kmzQuTd z6%JJ>regl$Hl+Zz(2b|6TYi-ImRCMeA0R=7ur?gTuxV0)t%AGX=+Z#p?eJ2 z9--|f!MtNybdS>5)CvRFTvIJ|1qn2ru__3hpcq(EHQ8IKS(ug!Ey3aHso+e~>T*tS zhxZLYAR5;M3wvmd%D&jAKI%;s=M~m|+pnxW5JbJ~t%Qx4xsESxLn8}N{4bzRd9?a{ zj|aDJs`xn>9YpNh8iXnL3!-IT-?7qSw0Ivz-}~yUnQ=6b(24YzM;Aur7_hR~w}h== z#0W7VJSf;Jeco&?c7?=bB~CN%(%WlSTKtXydHE8fgr{r#E?I3v6d{V;EFmVi$O;H? zQ52>tG&6qP^lYZ#84iuaqwLV9;%B7-!WX(rxMa{H%aI<$qV_%#qJ&JDs_cBJqE__z zEW=ncdOuy~9=e?1u$onFc-p+XUVFlG-nNPtGXd_JLJj$9WyxlV(UzP9t^*P8loT=U z7*2GNF?HwqcyJ$TuT8#l=*zh;i&XjHOCwbBaZdUf&;Z?p07ii@i9x)cw1p~fzlZ`a zOvj^3pSwbHlV3_OykOKc5%Gm!u3^{_?XTe=%297Q8bHoE!H&r6;WZg+oZIx2W&jUp zO5XP_#oA}Hyy8)Oso28DNgn-GTg1K5_X|sef+q)Cq%aVX^6&|FJ(Lu-2Kn zB+|(X*!VUJ>k#Ru-jvhJogw$U_~lH-M}s^H;kdc&ieIR|`f%Ek>1yCPJxJo>b;yrZSMvH}xanL@r%#t>*b4whsb zdb7?2!FIFt>ua}`!?gnxJ#*HD5TKfH#pGIFR)(%)Ko<2VGuw8bK@QCsyaTIZ3PvyA zh^^Exq*+M2d1%h**`}odjX!hu4!dAmX5yA(lHcH-AXj(k`7XVrID)<8f_5YgtRz~W zK`*qp6q^FpRkN}X-zWO`s#FGJk~?5(405`FQ=lck{q5n%$uk$0hEM!yMaO{E5u{A; zuQDYk+lPP&r<6MTFC*j4MFl*dWQ$L|Mf0UCbD9(QEOE@kc+kD@X;0|_FQerSmMIt# zjXgDXI_Mp0xgxRv2WrdHS%}%PTgjzMRtwO0+MLD|L`g(6g%^6cUn|PrKsoUjAc6e3 zr@WT~&5hh6)Emm|r9z@WrDwr$eZ~0$y03e#a%%66rP>?1t z@&x$vvdZ^xgBWmnL41q|)cM$AQBT$hX=cXLujO5&o7+)yGt$O6>Dzb0l}h@!fdO{v;iPJ8)YF7zYbiOrEsi^~%_o7L!_)n2U| zM!w&jHsF&$z~xl(CxJj7r^Jt}cSPzk1$)}~i#HsD zXK}<(5<(y$9j85pC)(^>=5h>ps67qR9s0(fkQAHzbnuNu9m-}#`UO-0m|LOQWS<%; zYTV>*S+i`5pt2;GDgzs%!>(9cJ6=37%4ZS+uJBIW)!n%%DyH#}dHY`Di)!I^k_y?f zfUvnW$f`smp|Dv^i&>fnmcyPl|2WEOhBj|sJvj862CCc(3NbU)@f`Pfx>V<-HPKc^ zEdCUgkMlJ`WIP-a>I$SNrx4)=2?@y@Xee3!qOJfosbr6$r4{L~Xd4mvJi2;C*&Spt z1ZrOfV_THA6(V`WPlYX7SaW`&0f}|^c5bKHO?f3YU9eySJ685JidsqdtA^29$OrN_ zIL`nD^4HMPur-s~{Mm}O+-gdeK#Q5-&JI+$CA`Lg$HbOp!0E**$vb9xdP@4L>sMGu zG6|MX3Jp#kX~{|2y^1ZgBQDRe(i&~)rQ33~q$S{O7H zPGrMY0(q+1?Rkq$Nd1egcj(agjTs-ouXG}HmRDKf-U^CY8c=<^S?ouV#RMjU1RrGI zTSuzXTo0B)n!37yLQ)2N(J$syC{4nj&k*J(DvT>PopnZ&cKzcVvK{*_hh?E^YWU~k zmUIl`s-||Bt9RZ%S?0bD^gJ3Z$>a(-sPWB={{Xu!J`1+&Kx2f}k~4ery6wa6m@^bS zpDAWhRcm^lcMK4D9F8~!94PBDP*O_ZpLGaj^0$N-HVgP)q3_kJf4-?#fjfv}(42kr zg8$7M(ku4JN7+?WAx^#V?Acip4IvDd3O#EU3WH~*PZj|M*lDs$gJtiBx=;yWR&Cx#U%UWs>_2EL_bh;v<6ffPnMVgx>fEBp&>2>OB99U67MXAHSS640IuZlamST<_cpm*F# zM1o_SQN21s>Zy=*4MioJHzR`6(?>wbwzfW3!YEUSBQHgA7?M#coL4(am`99VIm$)p z2Hlz!I?I3`dDodi%(TxfX%4rqSc$pZYt#?ER1dM!fk1YyA3gbt!SS=zD%tP555L(Z zQs<5V$AEjI=?9S67LV0*X<1qAyz9cM>4B4NY9G9RX&={HmGgoXsOdn}wDdceG`TJZ zIxaZ~yva_`3xH+c%_KT}d$U+*5NYW@X;KiU6gATGi18x{?x_4vUUw$X$9WkKU0E;5=p60G4e!C!FHY6tZCDEx2n7a$ zm!b?_sJR2#lMCPO*n2x!eR&KE7KO9U&FEuG7%HE7LJY=5mRzA>Oa}^*?r{30a{1hf zUn%R%m27RiZjLzw2~da7%~zEZ@sRNzH%aefKx8z~Z|K*yTiZGjh$W8_=|acB{)ReF zQ=N1LV_`92mt1Y@#3IuY4Nouv+<~;|uhaF4bAif^7CnLmMmX5wK&`J2FUzMUrKCpk z&1+>kH6{hmMUB0qUGa1hUxCxQsCh^II`g3H{(F+DlKKZ%RcCfdOKp~jmpQ6UkqlNa z2H0g9$+V)RO?}Db%$;fyxSLYnCTFJARO@}TI8J16i>G2KNIi_l&N%+d)aIx{=8T?{ zM{rR}|0=tW8WLBtIA!a`XVe>W$ib}qO7~T4plN#14H>8?Z&DOO)*|y}o%!NS?OPBg zZR$uq;@)7LwTg8rpGF2EPaGPXrwq&J6!moIJ&L=zJV)d51&yy8CLII(i40*gp(Lo# zUVLZ&jLFCaUTB-aN9%lB_2KwM9s=_#o3GdKqudH>bViO>7Rq?FT52Y0*n(GOI8Tq2 ze`(P>1}LhvNh2XaEYwVwTYC!(dXDVvzs$1j+6c0xsnA}}OmNlLMIHl$(rz!A1q

2ILR(Xm#Cu)+kp1fA(q&Sb=|e#f`NHj#&_fVxp`j z=j6vAnJWl{n^ZM;UGJWEyJ8qQHqH3Wj*bQ&pzGnY`WjaI(Je?N`vHv0U3u3Kv8y!QlkpmB?Wx78+cwfk=QTShD8q|4Da)r45))QC}|^g z*Y(*MS>eY32c|xUS6&ai1=6l7mhc*AyEd0Lpjy-(6SH%s%ir0)nYkt%WklK`Tt@GA z@8a#xKEI`l%pVDRl)?hpREF>{2a99}7bYjf8$?~Nv*(t1M1*zujp!=Wm?U?a@sWu; z^|jvslIt z3GvScZ)Fffngw2INj0G{v5hS^8ny_QxWA>YDmv_v~kL59l=odl_GtUiBLXy*#^*UIlji2-D?^ z$4+CYPDN>U4iPhybc8qf=*EVl%%zt@jI0?@gmfg-E-4QS63D+uDMJn~P0e+S$a{bl zsTZ$8HV~$k_vnYQc;(mwQ8kSU>DC!x;sk%1O;xj{B>SMG!ho$fS(N26OsNQ~i<=>A zF4qW~x01L%t3qBmk6ht+9COnvnKpC)466is01E=^!iug)eC!`pk-Ic<%f=F7}db@K`jtnud@(6?L=-~ zR5~9WOCypyehG@Qon9bQs#unAtthG`3HQ93)W4xlnzd%{=jL!|(&pp@soLLoG*O3y zc;61K%GZ~mTUk8lt1haprd0K?Yi*OYzkSQ5N7aIL!A|=$?XPP&Z}iw-HP9P~m+{@v zZcL3eys>FZy?a~zDK>O!{OL>$0}QOs9@ScltbJgs@iI2MSw)BA`eW^$mxg5L#NtNY z4EEXSz)1QK$Of$M!fU{pKd+PM3Yv}q?;J2k*X|y5oPcd}kEScrD|^On3F?h2DePbtpc4knNDL6%-;ZT6PAuW8>*#pa2E zmE*koYg5Z}?8S|;Mk#}1_`NLZBk8Onoe_dYGuU#71_!MB?ISn#CIh395E~0tYigP( zMmOnu>)l_Vp<#l|BgCsU44v*tdogg@zW|L4k9}lG&V#pfOU-W^l{1XKj!<#R8aZ0a zBBVsNgoLNjat0lc^|9arHL04rN@CKv>gA$h!#SQhQp(em!_`amp7m1^Zxr)>CH z?9KmupukU+?f?FO{3;%iV-fOwo0MORoZmt5)IO@ASNe-TiyY&5 zT=(xrhx^0+ez5cFA0xKW(3~adxVc`PlD>#Cp_}0y%G?o>EJg_$&vp|QrpJS4iWnAg zDV0?)Qrm&<=09xszlg0AKgPGQ8;(5@x-7qyYiT=Hx~^*%AH~g*&aUl}k{dy03PRUE zC-thaVUl()pZ&cje%kbZFJu3EmHxvz;SbKF9IU)&rpff|Ef!fB_5gv~6k{SZqVgov zbsGHz7_<;_$oZTXx@z7}ivJ}8;a?;VKN)8KQ+nx7IrC%0S-|i5P2KlDr;P3z{#!NB zf3Jb^&uP#9q5Ws`%6~7;{AlUj|Kk+}-@KX#VUq*AE%S2a`m^@ZcCmXA6e8grsksNqQ3MeTj&vKt~^GZELRN_fSb+T}x3@H%K|^KadBLXTgL zHsvf%RKRp};joL;&A-)oLbjT=Cz7=X2lvJ1ED4Ak@&X6E#e!~K~)3W$``#S z?jS9eix^JTqQvczv|)tb8T)1S5IUnc+ZGdN9Wgv8Z9iV5-UifoM{5tn{hUNJNF(g4 zEZhmRlW-7=K)EKo*v=^_YFCm96lY10FBpt*ZlgG}ab1G_e*4`N$D>F)eFcIGR6}Np z;nHs{xusg3B=n^iU-rw;dQaKx%JZH(sW?5y;I$#;?AA)|AWCeD%bG=l7lD|shXw+% zySu)Z0>q~Er}M_rx1SR7D_zH1#rmmWdV`7KdsaqJAjgf+!AHk{h5$O&(LTKjldzhp zclBbQp?X&=4zCU6Sg+zIUtLbKau_Bb(ktgQe)uuJ*ma)!)|}!_*4bJXEP=y~4a-tc-rF_qi^11POHHFdSPbr&hmv&iK0^_ zHaG1%5T>1}IF-KFt*byt#D$*WH=o{y)l@CQFP=Mdm4Pvz+bmO2-?q~^8>HQx^I5!u zsK+MHo`g-CCTi~A{jGsN9#0=QC-vh}Fu@^M?EcGG?y}{K(Pa6-OUAzv``_Iy)Z?~w zttz;QcgpO!NvTNGGg21fHD(kxX~ccnPK+SYGU)3C-aqdwGb5SKU@{mV;iT9zbImLP ze-4#3Kd5!(n|4=11cMp5!FDFXNfwEmLfPQ9edgC4$V*M^A4sY;KJFa1nvN@Yri(Ui z_sqKeYi~<$Ng?zYKw+3hKw)`HiSHUtJDzDP6*U`Hp&J^^2|^09^cC~%qCS*!Tj*MH zPiUD#Likufm8{pBT-*qksp&mTwscAEI&k{Ls^XPT2B4wzR>Pe359OT6_bjaK=(b&! zjZCvC^gZY}Y<%NM&kyV2%b$$qUK?Edh9THW9J0t(sXM3gXvYVNo$7Q9eKRS@M$+Bk z4-Fdrk!yb!XX{sfH+Kp9{;iO7FT>Y)(dj#2c?W}}8bfMqRldfFKOUkniBYE;>T;m; zy1KPxSoe0T>}p0fyuY+Y-Dyz-ge><#d~5v#_0|o_JmBjafT}HfzvR7pze3JtzLibt zvQd-fyekWrK_y4tqs&g(;Mp=+g!WAr@5@aDR(d@!7$r{MA+nP@RLV$vRFtwT%=|^t zBOzX|l2$v!NpRuiXE_aS_0Afm17>SaQY)sQSN<4qk@_t?fA^4|?M;;jh@{Jyc029n z^655;(bNd7m+67fKst_68|<8Y&}HIqpNojvv@=NuHg=*p5i4y~KKF6femvI6!W5(N z4&)K9@G3*S1Y6HoX84q1RK}jM9_tRRd%2Gt+W&sQB$|}9xM@qZEZ*sot!#90^sIO# zs4lqP8KyH@3A$?YcNzMR6Rn2oLRRIo9}evmtnEx5@@{>Zr|#&Wulp)}p$qO_Uj8mV zZQU0=lV6i18bo*)IKlY@S%{I{)2oher|MJeOVWX(8$-XX+TV5xt}LU6u@$%PODHPZnDe6#)4;B*TFc6BvZY}Kg`GX~CL+FWC*Hvt$#}GLuJ0h+O!$AHt z-!$=I*($F|wvb0K2U`&5S~$8d&$ZIn`mz|;@H6}chJqkkF}81lEO?Y}TNHzqzHT4& z*GoPbr8Q*f#e}?4M=OjDCLcG_12Y^%X^w}7fUnSrb55I!cY2M^@-H%1&kC$<<*f}G z>d3&>pwq8}I1}y>IEK`cutsG|2DG2F0|aQ!rBiR+NE4me--H@y4mKe9LznMj{E z9=4}aoFiRU96^&>B5AY3Cu*9{$1AwG$^4Fjmv-5r)Om1VuF``yifKG&wZzR4W4$kw zty#gRfzy{J7$%kDHp^5We|@dqhGmbx2&eaDjWU{ogVkKj^j_Ot`@NUAcp1V3_GH(S z)R9qKMO*LaRJ_ax;Kk*QB!?hNc! zFhR^dzk9#ptlkw2`sAIw%>_+ zl!h3-DxfsW>XUHhoTnPF`?_Z4NN-Hr=VDLOGbR1!-91(+5M1kE-_~6GK=DeO;fux2 z67eo-2k>4AM#H$LY-zN-ufnJ+g5tbQk=DottyVq>;TaYD?m>U$TzZawF{9kuf)Tg* zVPo5*e*HjOjWV2Kme6Q)hR%>g*ZP;nK)&jtlrCfUlq7+55+L{PUs&}}JXv(`F{p2k zJb)tFXcBGo!X;D`EsPgJUe5Nf%F;y2?A>#N|t1vV-%{5DF^y#Ni|JTtf1 zJEk7?nycFcjgp*@+^Ec@MIJA6AMDCatt4I{_vvY0Gz ztDtZgJ!ux>_**aB0d*iF9JOcc+^LO}6VziKgb0dDUVeG)-UFP+s%NHAn(Yi$)!5R! zx;&1LC2r`!jG^K-OyU(yuHgL+oupw0U$!$Y4~oIErtDnqz;gGc%=oKAj`_MU?U<@% z3Gy8~7`_UdO5+=8920h3nvZ@P@gbOGH~em?w%A@R0+nU_=xvg_U0W?bujQGjJ@Qf| zF4$#Y2wGVJpFW9~9%I42Brbg7TL!dmzpPxS7`bSl>uy0+Rdgu3SUq2IF^`b@F>UV( zd)|hGEg9F#$xIR5lTAb&x#i9(afjfjdy)EErtgDKIZ%f?kHoffE+$V-B z$7rk>17^wzw++*x9XHA-olT9KMX4pc$Tv}nGXHYvTVtVsZbqkEh8(tx>Rx;Z8JkD| zBk!e34mDS|?Me^yVqRmPoANMfKc~aUFw${&xz4t4=(BEKsTX4cBoche&ygwnBpf3G z<-s2^6Z+aIQT-@$S_=ALO2s#R!AjzDrgE2xlBAkd;+$=kfB+xsK2L51&EUENuU>$K z@kN~IKyI;o6eK3_I_J$U-5a-tBP_fReHGIl;D1vyKOWD1o5?&;olvM61Jk&QB(yhg zGc~KsAg3qGYHFHuMY;Fd`-EqM4<`sbTGdJYXdw5xY_9P6ZrPFZjS>P7*i%yx3-NeQid=2bxAro+8KW7F5T{h6IdL z+uSlIsvr!FUMYv6(EPZJw#i|urlNSFQ+%g88*iD0=r`)1mt0oH6gLU35yZa1lb67T zuNw?`@_~C&P9M^sY!H+ZsNrsfO~(nT}G%r#y0NCc@|-&2mCaEZz`8V!Hf1JhjXF&J+^VL zZWjwbEHBmncrxyz7^GFuy8>^MMvCI)^SU|L)aD6rqG|m)K8SD2$_9nn=+)L7SW;H) z8#=maV8m{;d}-k11+RjmQxaqAulS>i)f?%`M6#ZaE1|TH!h&E5qVTBn>mqOZg@EW4 zLIG5FYuCospzPJ7C96{7>5|kYs^yrD+SBG!l&|zYgz>>GTwFzY-)(27FFqa9qSd1R zHm^8sa+W(#`pO zF1-LgqxdIj97;AZC@pWj%&6MJAs9r#1Nl7RrJH)2{x(me-ZP)95pK^f678V+?1?R< zmpKhfdDzrWh?P`uceJW;MU-xyVsFU>vv1~gh?=%zz%y;1LErH_J$vOcqS!MgK2jq0C)4n`;HsX! zdYI)ioada9I2w5Y!z>ArOoSZviUsHdVLNz zUHi!lzhmEKt=Cq1BmP+oaeY(SrREMRF*&&-kQ%SxxKL?VRgi>X%aM&Qm^RkVb(lz* z;I-+`;_lJ&EL4q&#Wlou(Hlf}1=7M^PV#TKjuV$P^pX}-A1fi_X@rJZAB2@Dt2@q@ zWaWb5c;jJfsauh~QTviFr|%dZMrH2E zTY&F}CZ!~XoVrgqVimj8{BXEZh~5PP={0-&YlnZh=(C)q!{^&2m0Uan5$uvHnE{frwsVxaEvI)ZOR)xyHF*L|{WEcM^NL7nHpM2n6h5%ns*C~MYa7LIBsX4-C(0{zF55k!}L z-x|5IL+NLhqaNB%M1tZS$5iaQXO}}Rl`aTbC|mY4-*sWcUGr3#ROT~^Nk9*T5Mu6F&Cm#0+ZkYK!G5wK?ebx0#Bx6fc@sG+}a@!b*6lGoK0=)(^ao+Cis{XfdtkzKIBH}=YfK%=F)S z@{fnx-{fG{^XW{-tddURGQxbs_TD#N^g1DQz*Fq?o_p9afaDi{@=XKhkH-LkrBx^2 z5tj{;l7f?XurMq?X?c*Ac8e!v)}hZ_C3pFbks&+&amWfWgf?masK*w=@bGUc`}ZeY z5S=Qwarb$rP2fjbQbhoZco^k9wIyAH1W(x2{Bkt?gAq1;Mae}~q0F@H)`-JrP)AYR zTKx@dG2<8sOec?%Pk=xy@%WkbI&ZG|=}GP(2t1E|GrpcVTCElEa++?n~zWHHum85!H4f(BfC>O?FI8?^S%FQ zYln9j_w~!hNjOB@PKCPft9C1#iZ;)-}NEB5cNGdhHd_*%W~Ys|60vc6Sa@?FT8ll(W!v=<`8(YK!qr z7*PvTYm?J$x%+!+J}b6bn>pw6#Ch~w6&6c6d4Kfu zSnI0+nh6$fi8O=eOV!Gc>lYsP+?Ai*8VC1r+=3f(8-r6ewP56-(>~+gs$*+;ref0 z^wqHK4NRj&d}-5i zAu3v#m1IW8K3s)k;fmD5q8Bq^H@g4*n&_XXfy^m&q=b1-%{RgBOLek4Y1uPtOsG(` zY`HHzk}G<3)TJI~Z$|wx1%s`3?qQF@=3Qu}x=U|AK0b&-498dDjm@roDlLTCk7*+Y z$|lvyLN0We#I()|UWXuoxD0e95a{GMhrwm*y9L$jtpKAtVZ3EbB&w8Z%24mo8v-G8 zk1T9Ie+(eU6kTOlsKV5%ExWF-Yu;W?Ednn_3gx&}I==daZgX*RH2{XL54zPHcEzw5 zkF4oHE_9XST-?HNFqd8^eCngUTH%sP4rf4A(U^AatXxL+ykl)L1%4BXs!Irs z^paBRgMA&xd*&W+|FWD@cuCT!bJ8Gjk+Z`XwRwZ?p&_5b)8dCS2(`Jr%(2PLejQhq z`cMoubik$_3e&{j^B*_17!uv;&?nd*SjDWXlBxvfTru15()nGaw~OOO1|)o^&H@c) z09%5+V&EbJ&BP)K^P-Zt7y;ieyWci0&JoeA4MD>EyB;Bi!RljSWFp*X@hFy6&XYX@ z2c34B!GEIL{>!o^x-sXr+>LABcxh!`EH$HB@OX{3_K}z#i3GMox`gNTAueMh zbNxUkTgl6{eoZa3GE2d#__)fe#nw#>%CKcV8y(96zLw;tZYeFhMwadJ3YK`AZlH9y zmpjQQxf23K@z*MBzG!IBV+0d&@HkMwrYnmZ66GdUo-H5ojs?k%R&YB|sF<&wt#E-7 z!RrvYiOkOj8jyyEx5BfduCp}8#~I#mF`V|Ex=LtYJv?6DQaKO#4EshnxR>-|24swPx9nl2x&Sk z+*F>xydaR{OmCZ_SFNdOCc1Xpr=A(KU0y6b{Xj0YB43}diA~{Y2{GsvGmW$}d4muH z7O^F*KorEx9$uoj*=*PZ)>)}aKhsb@n0~cYe?gl+F)gDZ4JZG~pVm-N^2){A-JBER z3naJ5aa2y|^qqIa8VSNpyO*=^3jrv+ccifw?pMxa0U96IAiSHLW4?t+txYgCNg%oq z-{VLfDr#8iWQz|pt&0u8_8nLzrkb&sN0&Tu9q<*&L~vB=gLbheJDG*}Zv0LC`(MuL z^?xikCk{RAW6YDGIRCid>292bL17SOr`|oaez2H%rt$R2Bi> zUVx1S-U*$QO9B5uvsae7b0)9z>hhU0G-qV)imN*7txe#*AsvWE9D7RCc%Schz93IO zaX0#Tl(ihaDHV3^jhpA$X#zUwWr=r_@O`!|u(&Pw=(_|z5;@+xmhc)vV zF(#}v#v667uPz4-DH)Y#NPR%NW`O@@=kNrkJyx3yThAI*D_tq7-xU&KskP7*v9xL_ z_SCc-*xB9PJx_p+P2+Xu&o3P4ycm!iA`C*YGiZN&;Y7!P=!IW@`j_Wm)6if**wFv@ zh=2Y;5j-d1&Gl!0eU4bcd-A^-gF*O(UaVj)AmGm5R|b~#=N0{*IsdmifAPZq^PRt8 z#{cck|Lx9yLhFBr%4)=H>!YQRGOMwVuju}^jc?Dtq7+oOu(XuO=3FK<5r9^$=2g$V z^`{E;r}ntLye)S6my|^VM<-{CcN#U{>ag)p;i&lR6CTv0UCw=l%{*y`;PeZ@)~IE^ zi%Js7pA#cC4WUm%hEmF0v*?K}hjH6XSfznXSVIJ>#zWh@%fx^A!_>QsVohTMkU(*NZVG@og@`}*i=Yiol<785<|mY6Ly zUAc(a_}5jdc6uQuqb25cE1BsJ%_lG8{^c?BVg^Hby1fGf4CRxq**TiU55glo-QtAm9q%<|bGb=UsrE+MpN6O|5~{EH-kOO5BK z!)-jaV>78uR-VGIa6fFJ3l<7Z#I99Y{zzdXcl`dwnZMbR-vF!Ij!zbh>J&r>X;#{q zxSrcl>QbR*8|d#Rkw>qNP_AdZ@|Vn!NmhW+BImUTKiYuRLyjwyDgW_zKLTn!p7UFF8M-bKTdD-31*pbS@?haIcag0zFv4Ze|Xv1dZCdOwQed>@s+{MeA_x=i>hJ!Pt?ZRaoFffO`k` zHy@)3KzHYoKIzw>J5XGW9(_{vXl1SC6dB`q${{bG&hM1C^_7GJfAH)oZRc*c2co`h zXPMjhen;ZnImKkpDu#8`!90BS=S{!J+|862LU76>otpMg>@kBRv;EerCH$Gc0rUCd``zdMd@|Cn8gk1yB3Sc}+%$`WK0o{XjM7|z zTFz&{;dH;ivdcfnbHSn@6-{NFpiz08+rTpMFUzF;!g*?!t>ZWlQCW8(J2#?JUzpu- zH|1gvpMWKC-2kJ^>VK5 zI2(wKjba|>J3qX3Us=3BrshdL0Tc$_WiN0W;)r(oxZFFjD_57vu3aW=WMtGYAC&`^ zeaUI@-d~=s=N&`N|E2lqT?-4lQ^n)K?1lE;d01QjGWX0nSuQFi`(f}vEn*(ais%eI z)YhtF72R*OlMudc?T30cIHAHm%HJTU_cNR$8LU?7L;NP-YIV$e$(Q`FAqx?P= zaygE^(rNJMPCCBk?!F-KMG1ld`Pkad*P`V7r{E{dZtxa$zmp2gACyV5#UPYUZD6v% zmkvB?b)LeGd=*B_#_%fcw{S%d{rs-(DO}Ow;Ab{zFtOS_ zuBg7Fgc!;ZH7Z|z(T{S<|3FhQbPN!BH_khtpf+p2S_ahvA9EHkB*!`iq1FTnwnbcT)_Sn%4kHq z(-T}}6$F-3zY2s*N1l)55~{p_hlcS4SI2XLH@QA1z0}H^O~>Hm^scB$8_Dw8+9#wD z2m&tiUizt7nOkN^Avlt+7|%^H8U9z0*xu zlmqKuC5?f?XI9TmNteA5Yaa%QIShzjk~tO{hBDI)=w5qLjL7WqLJ-63+&sm2g>8yl zf)KxcMrNkWV7`80fnF~qMlUN!z>}3Ji@&&wm@-((r%sHxi8J0i*uYWvG}fRv zU7<0AEpcGG7Sh7eXn(QgBgk2wLW$X;chh)*lLVuj#9s1;lYrg&B+vXW|9AzqHkyG6 z%C#zQiA@ti1wQm;;l)pvz<@-@Dr5c1{ahxafpS3<3XBJ12MNDz)--CfoFn_6i7f!L zx%lA!@}CU|!^gKs5jX4*lcR}zR*Bzz5qvJh0A(m6NI~Cy_m7530Yf`uB7vdqsCe$U^1mAz z1PqPiWe0|CVdm9ukoV{h_KlW_UE(m~p4?P8ABvp3l3=|Jp}DntSCes zOT5oncmFq5Hw-MRVe6r#qMmm>1~yY29Ie7O@=VbOTUJ546CtL1d_6Otn!!{qmR2QQ zf$SJTFN*SB{GZ+nYCuun3%?dF&vn7>u92iNL$zuTsLX4(5O?1(PywGw6l>4fp3fSj zroUwXOth9KVv{t5bYQ2Vs=r;8mNUOpbl$@#k5dpQ_hG;bO-r<`awrsRU7Jbb#kfXz zSzVj;rFDhO!?HMP@%3qS{(qvO?lJ4`eF_+xix`)t@$APyE|;yjCt>pP@=<0^U=x>@O&Uh+e^pt3i-9T6T~-1)L3 zFe!qV4U_cftGm-TU^P|%hTE&OdHrHI+AKJehpk~(*_IXH=B9CaTU7AMZH_RIt*DO$ zM%c)Kn|ft?d3aP;m$#;MGMTKW5Gens&zhNCW`$^s)8Yj_lX<9tlS_NQ(#ucI z)w8DvUQVKx+&trWvaVDkV6#ah@@l3qXUS0gYn0l>eZv`nF{LcacIaMX8 zf6;Yi$42(B_B65ujR?R8I|u+xWn%PbVj5`McsLi$FG;YW;tW8WtyC-sIm>cX_n?Y~ z24{S1`HPLmpTz+H;VgAB9IJhGu=WsmT}=o)UfcA4e;of-P!PVkwRI=o?ZRaq&Ek#o z_LPs!GRqfv&4p|odK63bM@3O?S1;E0RprHoPK?{KNzszuhX9JpAWl{8{O{OBr)W`7 zvNoulx|_8hfLrsa;{rQvh?Vz1wt8C)gyR!|GD5CT7b4?^LL8$1262?1G8`&tj^`R% z%LAxk#HgxHc%62B_C?HS>7oR&e3aF@6H`UYF?7v}qz>o$G`Z+&@$vEdPJG{bF0RSo z8x-KoRKltVTi$@!{>T_U{cCLB1W+C=0nFyv7Bp?`8WIWhhdR8I=R)?jcQm5V?JmcC zx35gtkypddM_$wSI3XYYo;Xgw_kxr94B_|Aph_T{gz&z)?My$XVyKQ|annR3OLxInFhPG48Z&sLHqyUO z-xxXq7qi5^Oe#II7zc2S0TeN)lpW#;#)p)#n#5^L7#bYv)_kf2beIO zJn_d56M;qROw-Q1>jv%ormdsA)Ke6ZJSBb~!{GMYefO#*3+6t_7Z^J*SMl8|n6tin z9>WKm0cOvv?k@pw28|S0TaAw$iRC&Je3cfUE|(yYi1rvb_(Yp$`cFo3A;8U8oAPhJ zC(pM&e7lrik@%5V3S!e`X$wnvbeJ|o(ipn!0Gi9Slsn&gkH3YQoe%th&GDALgjcF) zRrOt1^!xo!vE16~rP$E_0;N_|X+t%jX8IvBYq~3{@|KOlAH_iC^L;09wY42x(_=DTk z`2Sk?8Gw2tS{s*=lCpPvJg(v~O($pEx>al)ENy&^42$3OLIRaP*-+T~r1qkMbNuVb zNlumYuIDdV$gs1bc9-z8=+}JuPTp6h%^bzK>0WqhQhy<9d0R`U#0~@UR>@BVl>fJb z`hC2@&NN=MJo~RQIs;gAUmSrTX2UCCIY_QG%yF zS(2aW05V0Q{}!1hrc_rJU)FwkntXuJkrc8xvRkNUFu>W@`*;a_#}Y#wsW$3I6MYgRYy)eo(JyainrBW z-P{fiHMCh>k94joLhUaN#Ato*V_jlohaNJNVL&=9Fdz{*8Q4>_&=-VJSww_Hz*4l| zTKYd28nlYL=wkn{ePyqDo33B*)zLF@^6-+lLyi3g_*FHAw=!=8vwY-?E(`jM`-C=~ zs9kg#hNQ zPa#jEvUG^*Q?IAMZy>+-8&BXj?rQ@)hB`eP(YkITJHDC~FPqz6l4F>Co?c|GI-BK# zJT946y|u(ac6g|F(C>jb=5jo^z$s~OvR2&{$x$!yuv1`a`_Pc|f-7gFyL{DdcAiJL zgYaRIc42bj5ViX=zzz!{-6Cob*@Zq$V7u(rpji%Tk~>t5%Ag>5*&qcVnho<;*z`^U zZ;E3c*D4n56lxfn%_8*@lm#CzbXMnFztCgUg9E#3-tGaL9j<>hL^rvU!55|&;^yK| zt}-i=VvkhNRMgdV?-ZB7b9W#MEHk7uNv}^gT02;8PiW495oJc~F6ppU*%r#A>1a!e z8@z)qj~5NtQ_JRNMzb%HVj7$4smNT2Tnpny)tBVkhmWu$g*@dxk}nMdzivn9b;M7^;x7V zOb8%p3W6%52)88Tr+c$&M}CjReVT5>`B3RXhdtMW`|{Y`1hobgKbs{I*H^z|DZK`@ zi=_Mi+P{7T>D&^J+jV%gY-CVC9OPUhANaF`+(5W^?v z5;;!|nD5GE-AQ@|T&*4bw-AMLwOzLnSaUe85S!rybFb?FY2}21Z?N`#Rg0les@=5a z(&cR}N4Xd_a8@gQ*atRkS4a<$GagltU$H;IC4W2$&ixtn)jKRdlWuk|BwP{))x-12^K@5=*PsNE$-=nL1~ zeZcIsGF|>a^aW5obHuM^e(7k=pr1u0Lk=7thSv4up4o|{;p#1Z9c zxsp$S;j=?7-xe0%xQ$a)bu_tYOrTNX&Nrp(&Q@wK(;hMAOR^J`lBDjj;mlI{QhtoD zG>v7XkgVU|pj9)(fO%;JXCIUSA>x_%Xt5|<`Tb!d5HJS^X?uJ76JOc7FUZLlWN2Fb z*aObJOUZN-{qmlb>nw%1P{GI&_d{FPd354KYv%gtr^x8b&|VR~d8g@N z*ykmM0uYuEiP_r*-5(OmBIgg@1$H51`hk*lnDk#Cf zc2XR?>4W3ynP`wX)kf7HeyBWQWANGW)|%kGHeboT33h9#CYSZ6QFfBzf$fwszKz~~ zvI2~FFdrfEkG%4?8K^lbeI-473TTdt>M4xnD}7D$4$Y@@7ff|yp(-G7xMd3F93v=L zqza6&V;*LygWAR-J;D3eS?F6(SNPsr)&d29=>XZ_Yy`r%c_o4DqE;N77^+?YTDfYOqMtcIP!wN|u^( z8Q7iOn$3jSRS(_cofI8Syc3=?pE`me)a!YWsHy40j0R=*&FbRZ$MWXx^FjO<-UR@+ zdu_n{iuBS0N(>0?17%Wl2H@n-OTRs@1GiJa-JR&&xIa2a*10z!YTX<%0`p!{dbEx_ ziR~W<&Vt^9q00--?j*a#jyvUk9U)S&ZGObr`K?HfKb^2cikZ_M3awB|xDMS9F&*jE zz6+z(3?8#)udmMW+39c{LTpeU#`GpV?&$I)S{fG99YQe&OJ8%Ft>pLPr;^keMHlrB z+kzPKl~3AH;;ZQT+%`{9NFAV7FVYWykuFd+_5N$>hK=HVRlBDS2dLQgCkEcW6`u`g zUVzO&N6Bw2caqe1ZA?_ZtUDS)PB>MVMQ1oEM67yFE6}npmZf}{Y#txBu!O%g%+XKN z+27c(lAMk`Nr-1J5#Va62)pUwF>bV3Ln(2sAkRL9oSuC<@Zcy%8FM}SSMoR z&%f@(=5kze<-hX9%w!9KNS4Z{%^o!|OlH)+qljAlG=)T^Jv>q5;#ae(M@scDMsw`+ zX>Y7MN3IDrI=QB^}rd5ARc15{ov%vpk z+hi?A(m_apD(%&L+RsXWSE3{N@GIGZaTUbpCypRs%$mM+^?Fm`L#eZw1M95k)^u>V z+^SS$V4uUQwOg~M5Vn@$2XKTsxd8BjTn*jVZla&0f*bk-6CBdtj2RKQjt4!n&+b}Z zjKIPQ%LT0|V`|7+e+u2jznJe{jM2 z(Wmex&R)lJa*L6<3240IJ%XM*JUxO|pqLb@wj-U=yics7vfYOIaQ z)appxJJ_L~KDMh|47ui888~eix%vI%#@LhUq4dy{<(fg=ES=*PqJ6r#3T#v@kx%}r$w-3jjYI)+(E z-aHeA>YrRtG&pnuZ3Pe*Ud`Fq&Y->A^1Aykp`mvXDEA(cN%NkA>{W;EnDKHpcSpz5 zE)M!^T-)Nt$9o92PYQ;ILXN~CBTj9Qb@%BOcnJFp*j_Z(rR|tW97jYp&zbswFH}SI z%ES`?d#}7@fgib6&7EB5#LC@g;{^OR!VP$gDnMSv%L9yFszK% z#Ey)*@nU1>RF}r1`SXzvQqrech-jc$p#cxy{Ne^;&%jcMW=69%mX<;m78Z^WEAeax zlFsA70i1EXY^Z5nvVJO=-x=HB37bO;6JapWYmZ4hZ5k4m*BdQHJc#CBebQrewr^+Y zwYLENs%F&@evj?5{erY3^O&!3hpRmez26!{T&+L^a-?uBmTi;_j5c=RPAI25`-?Sa z(0}By`WQe$SyLk{S~;$Kz`x>lCgc&CP3_BTcTMD7ipymI47d@o?Q26`M}eFW_XaBq zkmZ?eWM|$<{nBpV0q2K<)?-A90ij0hZAwReeSiU(OhdKkVUaH$$eY)K%K2A@dpn3! zSq4-LH1D#&?2S!kMLekl6b`JvZ%9y$ED@WFCi@z6E1AA2h`~ldof!lmrw@atCG%D< z2IMtypI8K-N!9kz4Zi-eS}|;}_p~$z88aXWh{(n=Tg>2^x?v{?LU%MqsVMzViW06n zMGY^`u9Q)y1u_p!HN{_NbYd$^YvUv)PPDJbji7{he5i@aUtV41_SB9*ow#PXb7bNn@&j0`L8t+vR%;!QjXU1ky`n2ob8d5%1(HI zDApn;SL}?+{AeKaln?JS4vb}_$6-D#7`!F8`8Zgt)Rb;)tC)P(H#mW2aW6-sLJSI$wK}m-?Y8`EhLjB54_TdS!gJxu1;jTT+O%N z6BT)xSZL=^{0Y_{SP3TR3X;Wf=#^-z0el@TlnOQkWFxJkrNMLP#H)qqOsnEc5fK80`S$z%-VE>Ol`H_mnPZzQ zjBt9WYy*9_uN=T16Ia(OSATiD!Cl~L*!Fo~C}4~UA%9a6=jPO7YdvVuec!wX@1@B? z&i2bo(!ham3S0+d&1)?@S|7{6CQGN>>Vf_w_rO2~P3v27HEy70v(3u{gOj~3mG6^* zaq2ee()ZuyzQBy(XWj9Cn{)CvDi`@MSS}2S925ZHK@Ip)^Q=`lXnH7b??}zg4~%WcT%b^o)rPq0YfNwOW#Y_zp)!}{us zP><$K1yCAz)X3*BuLlQoRVLb6YC5s)lWyzPrRC@6ce!mEM}RFEe9|4eh8q3aC|n`> zaWWyiXvdx_dWSGiR#CSn?t^|Goe3Y4UUqp;)H}K-VIFOtX*qHm9B1?ZYGh66XbD6S zw5}vNZ-Rx-U>9#xtSHl!;%xU&id#gKGQHph{UW53)fXtu4oQgqk$frq$AG#eyundJ zpO){WJyb3*ppoW&Ktl@=UFxwZLEG@@t-8=5I!`cWwwp-?BCk)bQWIJvv;gPJHR+&R z3&s($4+@N_8x2M{pIUrE6w^Yt$|J9F0=W?XO+3{~K=wv(p79I*0nGHOV!^eChqz$s zLuC}Dd?)?6^#__SnGyuAwKc|>`?^jPF62hpgo4RzI9TFtP!&L3n^s22Skj`!f66~IFMe&#&I|FZa{Rmrv!|di-Wc{%4fO52MtJvgps?HAx!6-9v4$P6 z7ZLK>o~%IG<)ccyQVl3|yDD|{+{RGW3z(=$FMtUjxHK8`4H)zKX3FUPlU^UF{SQM# zFO&3rx~LQtM4`tj8goPM=<7(~vK}b)I5}YI zh;fjM0F+#nDbTTg)$3%kd)%?zhS- z4@2?&*&n~lFPA;U<-DX8n@YC{XvT!itzDw;nv5kd54#4H?Jxy0N?sL7DHW9ZvNs@Q z?C1GmVLBm389>q^30!Nl)eqHd4sQKycI6`a35G|#XZ;hm4uKxQH#v*>^&*niZU%w% zFRrlwe@SkG zZyk$nzv*f()xVNBy4h`u(R8U>7eaixSWl`p_AS-2tff5}k}8;s&6xo;HA~v8BnBb& z^^o@l%Ll7@M8_t=-zgL?y4_QO#!%1VKcqFVuDauafUU18r>_y6;~%l21F*e{oh2Zn^iP*cT2?Z%14+9N2zA1JFI)_nUH&3+MpCE16BiejcX< zX4;cVrIXqH#jWG|Sxfy0&QtwFg>~np+vrNzpFEDpwsyu4bJ-wl?(pwBQUUpyZM@tJ zS!aMmvIKUj4oyI!y%U>cc}9g1vw*h6J(^umG-oE>`5z6E@jZh!Ps{LA=O6=SZsInv zYoO72@r>6o;vyT|x^3wOdNgPP>=N#wWUm3mM*oO+e`h8b{|Y)fj3a`#!M+e%pH61tpjI{lGRAP-D!cDpJjOIG`qcukaXDc;^$YR{4qf z0SX|FDGKZp?su8z|CbeBsL>~Hd+eXX2I!7%XI@}-0L5^(jEW|0qcTf+pu#_cX#g4h zY!AST94VzdYA7hn6lDFgguMa^^tR8bT&dl_#5+v#Cca>c<8BEA0_7KWB-oeY=jS7p zxz!wa;9&Ea<8N05{s462`(U|G!`edbfVjqq=ZPQ%yKXnS#DkvsJ z0e6^Ly7e>Di339~D6mN2fRdNlp|@Xc_`BH8L-CP-JSZWr?b)VI<1P-rUOI!$OeXzq z@yYi?fT#aXbdZDUgE+7BVuMAX^o>_h4JP>yM?8Zr0~|xPO|Cnb9>gJoi_@o_K!es- z+_^Mf4PpRQ7uCc;7p{2W48a~l2*WoX6ZhhyLE6*X3Pc{!6Uhq^ytUQkt>5n^gX zx7rwxgAWi=mH3|?@hgI!4={11!6IxE6Jrk${wn8YpenYWoe<3eBcN$$qlEN~D0BaF zh_c5dTt6=#&~O6}h>n)Uy7(4Y@T`J4OF+7yzq(k1YDJ4x|6sh8YxB+K+dzR0&bU~{ zSEwrP4u#?{srmh0fLMkatJNZS5Uoz(F{B%#KD6p6=U=*q*0v8i1A+ph?;ppiOs3V{)vQDk+^rZJvq&-wDfH$T&R z;iu)7^5xF_1-&a$)0ljzSQu@fUMm#&A961gv{2#U(Mi-YpbnAUT6?pq4R7qV(eu1# z?P8~?B(>`2sgQRv`s9k;mk_bD&$n?G<>M7!lqK$%c2qGL0~JT16ZdlUV-nExvUEzp z>X$hxQboT{+;0qa+y>~qKKcoZz6HLlEUJqvHwIGiHv+$AX zvzaMRTmcetcxh2Z)b81mFyvr~QWz+-u9(8MtjhKH>z71Qz_9gIseVpo?~$!XXDtV| z48DijCaCi-@$L!KepyyWYer(czxt7M@_I$ zf&u#UFw|JfQ|SBT{btUa6UMN?#5>P_Orh&ZksBOto&tQL>?n=-EtvW{avY1>X2NMO z*oS?7+&~l6;rr zDqii3>mX=eP+e4=%Y-l~5}zfTJ6j_++$Boq4Pq>r!aB4vN{@<6ZR4`ggzZCNTSOAe zGc%!S=<@pdvOc1;G#Lhqc139_DCrtQHJFh=PO)(>Tr9}~fN+-6%9YzckrSo+eJ_8W zQWhjDWppM6KMqX1$Q+KTx_yq3os~#5x~~ssdG;U9I_T5A1e5y;*y9S?5#5<_d3bXN zI~Y&awc8?aSW1eg%z29EYoNi)*}_M}`y0`dq;q9yGvjZ`h8D*=Qon#`^eAbIV)&B) zFB9l&WY_z9Y@Y%T#402nRsAMb_1}XdC3J(^By=sYUOCMa&dw)qk~ynC2d4bv{Z_&e z8%;f4vJz2%{_h>@3meN#mP#BL7+T!0jN#v|8Q&nR@7f*Gr8{VM{Z80Vm&e?18(Yli z)xBM{ZQhp_6!+S7xv%@itwnhAai#`?qT!nq$K_3XTcJH(-tPjMy6R+0i`|uK$yGqz z@AkUBgQ|d`4K256Jfhk&qJ|JGQc?eWPQ+Zndv#tK(nZe52?o&LL=(cY)vedG zAI_okU+yPeuQ0#N8n0dE>byF8Z6!rvMQx+!^P}Y=!6*KVhM4+wSZc`y?(%9E$1{{x z2Q=n_&DaK*@FMzKZMrY3e^Coi(Mhxyb!UKXrKr`h{Yl6B@8r;(mW&Hhc6p+*w#Bri(Z6*-KbF(+-RjkenS-WEAP0CdtOUfGKqWNe zV!dwoayNM}MKBx%Ba@Q}XuJEhOqrX{jJ9{pJ||B{Et`I){~@LE7Ns-9C|usj#F zkz6QRMJyG3vVAao2$lIfQ<`fxkD3*LFnKh5W7%a^xNBe~rU$tmJQmYybh`6ZG+z;@ zHn*AyR|F%dESE}!vvqV5x9yJUp<9A4C)xSfb!i>QWR)jVHY`^-I)vuOxHgB+GmHhz;+h^teRZu@1k(!VGV_=l z95BU@T{G$A<^=N$W8qOhEH2;1fJkkiE$v#zm_VDYe9MKo*b&X1J%;Fv`a*2HRA^1k zrWx$VPJZyIc4sU3Qq)8hJuI;wOzF1k5sM0$N!wTaRJti2?sa(&ff1}iCe8Mh_sl_E zmD--?BgdbyoC0=c-l--)Yym_8m*|HJgg+z`(t)T#B<)4eM*yQFbRIr2d&F$DUppcD z3CNQnrxBPz;@XKpdCjSR?bgYui|A=F+2l!uPUTy@<5JeU2)!av+b7eH2!xK-9vAvw zu1ZO@-yRH=DmNeQ=9vypo`85bvHRK*sCLhr67+og%)FDJc||`EW9)BqfJAA-3m)h=S1x>w z0G98#;i#V|Egy(ba(DlvVX{zRL*UFA#2Lq08z%Mc>_lyfR6ZEQl5L+w57R{H0ie5Ug0EOeRwNVL;b-As2o{REe-tJS^2uF=m-!0of?vFMHl9e} z3ohNyq!#JjoLHRl(9Cl`y40NLX*&n4e9|k_oVbclFWlR?nRx>$+Z)7CPy1{`xyqv_ z_(k1O`I8;9YP&{%;j2ZHeS%?^CJLZDLd;0ji)&xCpe8sP7auKK7Yt_Sx15tz-_;93 zE9rWSfmvUCvpB8a>MHb%8p(E)+pQx^vFUq71_)yv9$g=uzDc%9Xswqv&3!u86$V{hjj8Hd|QY&_({k&$JT8 z&V8AcPEJDV`={c?(;Is_ls2MYy=u0TkdR?IY6EP%s~$(^xAq4yD`Py$m#9s;w!3`q zTb6(so#ZOT!*yZ7@^<0+je3M>^WLh>HA-9BRBT7*SF% z^|^`Jz^)pSVSdEiJ5kF%B`PY_gQ*+k5iDPfNbT9H#gN_eh{Sm_jvhkOOo8Rti(Hio zBGhF)SWGp zi0GOm;z3|_hpOTltlskV^7Ib z&C59EpHmdO4X!U8>OEy(Hx;JCr>3RzJBhyfbrucW@gtZIui{+rROfq=3X-byM!n6{ zlc-0oj*!;}?~OjTOA>QDjyvC{e|xpZ$kirfw_;+x58TsBe(QUDo)y{~4-p=N-98vb zFmKr>Ds<0J*>!3bId#zOTeOwt=8-pOPQTbyhBk%{YHV<@9ZqPdq{lO_BX`f0D2aNE zCBRyJN!L}2f>Xw=EApo5)|~qvz@@~oYWM_!B_a%A}IQ^R+D^G!IM zu#?+?ni5$qQJ*3ws$q*S!*Mf@Hpp@8^(YjtuI~rYyPZk%j>i%Q3?$2n@o|71D zMhJf;V0g`|O77Z17%jRyQxukjC4n#SNGuxMDtgA}g2`*;YEnF2XJVo>Q57a5!C=W3 zrt%KxzV?c>-<@FAj1@ZS*{<($=z91Y72IzXmeM;^%8SDibghZqt?(77!j*%uFV8xW z#3i1PQeiG#x-kyvv2boe`RK zH+M%6nE%_WC?GV!=q*m59L$hdmNzoW)hEJmLvX0^Y2bj@BC2GO=+ z-55H_x6|k?I!E4bG|%GzrX}fAhh#zV*yIpA4SPeznJQmf8IWU%2%RF}^E+i!aWoUR0Fh@VSnxJL-`%88w)a|P_zZ9Dgn=~zooz8heX_RaFg?#*16nUfl0F$FKLYyG6u~} z9D9dZekS$tP|%>eN;ZUIq>@3nmu-ivtXhvn_Ah`g`Pw zz!2Tul!7c(p3Vmt2x}e>m^gX^##zrYLBh7EU*-^PrGNF-QT^5>lt|rC( zio-Br&%wY4#2cOlfs#Ct(-Ue}H`$F4JDkXpiKq8oWn{Q#TOFr&`m2mZ-0Ap1!Riwf zZO?pw>4nY)9rze&>`@Lgl|Ry9tUcYWTH2sIOpz*KKUO$B5?qJkkgs95dv1OlOZE8L zv!6F~IwF0=^-IXO=c>D;6qUzaxC+sjo8dXg;T|FkA=gG#}?II8+)q_5#T z5?X1YtAf(FEe=q9EG=UX@x>jjsiV@W{K5a%eZR(dCtEZKR1-7z)8#RgQLIjAKyvI4 z@=%Nvo1PyG-tRjJmFZ1d)HmDS>M|HvFzS#?f_DHVt#x6GZEd~`Z?;Bi2>EGufE>1-_Uh;jXZ*S)R%4<#rID?w|OGr z#}^+-p)s8@>w+kBtP}}%D+zi{*2TP4(6%a6v;Fz3WS`_P(A~=RbM1ZUo;-TY| z9#Q7rU)~uyEuW6RG!xW3I6qoOO#p3e9dtm0(U2X5CR1E9TQYxb$%ZX9h zTO2%8!i0#rK9Z?BKDnQ9T1SibY)50f+Mgx*T@vEi6(F4Gj+d}$l$-O)zztQFTTA8J6b^#)e1?Cy#&nQgVa~Q#s(bL=> zFSFO(sX`}$>vH}*h2rg7WO4vtnZmjjAeZuut=B9?UU`rVPL~x!+V7Rxx0$n{4p#Vl zWR2`TCm4fFz2YOxAV)Dh(H2-Qelk6JG_8%HGXtJBbx&3x>2pnJcs6lVc$av_yPI)b zcBXKjGT`y&oS7l0t=(#Nce59Klp`iN=ImlP=Jh!926j)C=v38x2Uzf~neJ(20WUI9 z&j;RAw<355TMIcWIN5>xfODrrAMOJ;8oks83aILs3U-lnkE=X0Ejk${!ZxM(7xBzx zg|+;t+D)%$8C?-F&bX>I*{^nruUopfS4Rq?dP?zsASCzYGu*w*Jo1`NOeUwqNd zuE-HFRa2Lnkr&YXD(WSp&gW_ma134F6{CohF`&cD5HyQnj0+vJBH2r+PN+(7|FDbA zRbTzUXHTVh+3bF949izXlu=Aw|bc8&6VSn)w>8YS482iq~KE= z35xXlU3&LmPe?ZsdJ}_dzFr*h7~4v&TE`l-nFxHi&ZW|2H0lvTkcI(z##yLJ+!AuVHRnHx{Lsz$AfBKI2T1Ftw>P87p?Vv3ETd z5h|Z=&1~@nl1RZC6@RNso9Pi;&SN^qFu>z&Ua+5lu+7fY;r!l`-~oe!;r02wps_B~ zvN$I4NN3?|c}rA@AHyWihGitsRBJk%3)a%C_Vd|Ofh^p#*<1nGchGg4hgZ zGJd2T>~d}OyMaal^*q%OyYU%@$|T9Ngm}Jd!F@NKweoK8u&9-*MUl!ad#>V>O)NhU z29R~4S4k-ZN)0UjL2D*AL3KCZS!Ob}2#R3VuTq#fhvBjjoZ~$^)sm-B5-;<8BQ3Ip z;Cg#6`o9V&3m-wcJuJ%H|y}c$a6_1U=q5}hc70^n2)r-Xg{`A1SKB0zcQl0;cC#MdwW`er~-FYU_{i zC)O3-HW_W?RK*81_9c!Xpmr;YhQo#kiRrGLjB>I?{f*l>c8#H>64Lrk)!c2P5vqBd zh8U3fg>;<;wcD4{8@gUPW;V3BcHxjXMvY_-ks_}39hQ`M&R@knL@<428L$C;8W#2- z_ng6m)ee6>HKrnve+u37Rlg24@BUEA*8QQFC!RbyH4j&sUPv(Hw)6wtc|+rBkHy>R zb2-;v6*OCt$zkE^9nPiebc6rVPLp8EKi<;ntTSqEr&QNlEsNsb<(zm>n01s3p|QFam!xYo-t+fc!krh1(O+h%22Lr=<5g# znn!0F`mldGMLY|@QQ7pe2`DP>@90T&8<;mIP;R|T)Mo`mCk^>+;3fOYd^M*WMY7Z? zfaa{fYg3Q(Kq-gd6Sq$ELS~nboXVDlYyAp)D^0SeSRSxX@tlMw$15wXZ*aM<=?!Nv z4uxlSxe(F;O||O>#P^>=`Hw4yU_N-4lFem8ZI&bzT0s67pIMk~X%CH+Q z6NRa}n$-qvm{+Vp^te7ro3G}Yp-{2YMUvY|x|1Z#f6e5K53LrI4W{oGYdsl0RC0Dm z8iTGmy5RZ6`M`;$%p$u=KDDW3!yZJq;pgUetv_OcJ$V~Nke_H^KIQG6CpGM1-Uqqj z!@wy!9dVwV&0G->NrI7#RNQ)i<-PWJQO0d7`WMG7J!K>4pg74IM;w2qrp2_tG2fW^us=my%O^#RC$YkLT0KrF>?)UVya@=? z5n5t;Xwk}$NR{u+d7oRWrtqNW+tq{vyGt)m8L1gFQwFoqCP6uCS9UPmyl%?2Cjv$v zIE($O&gD~OcbuG@oY_T^;)V=l5cjH1He=^j^7V;EHetT;;2N5N!NsG62-RPuV-E@N~4$tPLiU_-Qxb6g{@rs^GE_nwChxdGuM<8VS`o_lS$lq%! zl1yK=Y@2#&qv0g#}))NZ5Owv z>l7IK3Md%H41It)^F{c{AcVepp%Pp2P&{ZAfxbQS*nCQ4h}N@vTZ`jK&d%mVHqE!a zb!VwtLZ0GtU*sTSQxiyusMP@Cpkt1>ww+LZ&^KMori8|Z$?jG2N=kcU%`A`)WBqK;^8@4oATu92Rb zD4g3pz9Bkd-Pjvci)bYnI+Es5WqHC8^H_YNr8tAi2cBVkZgA}T;Gg>LrimLv%FRt* zZdxy7G!TN$;k>nry?!>2DcDogfh_5k6oer^kUD`%lm>)6^|qU4KgKcwm4 zohhvsH9H>zDNMHJTKAn|Vq%Ps4|ZRdL(XEWz66C)=_j7qpX$R>p*H~ z-eb-oU1hprXM+asT^yBF`N`J)*5Hbp9gJytmSNm>)xUH$GhDMW0s*&jpJIlx=$sqf zLtN60rPfeQybV-!Wwpx#>~@C>)mRR*t41fv4}%UBmL@>%Q*mx;9aPBv9jWB>YSh zKC`%=gAqxd{HF}}_I=L%7M&9IdRaN?B+%0kPX%VH6fm%SHI`qmdCPNotR5J1Bz>NJ z>9iKj*CEm377M7%r1OEM0SO~A8eOw@DxzBAPuCmj&JO5Mpf%>-cAK0}alSVE0syVh zVfK!kc-5xCo@v>V%*-Hv^x3E`qq;_AA?pWY`DKGPNn7a&pTE@^_iE`C&UaSr(-QIM zf+kCH4n6RZl{N(qtEE8%@$^dPfgkRBJd}5tn$T-}6YZfpjRBIonxnSx?JP5DA#>WHh(b$JV@c1DmCv6|nRwD1u3lL^X`90e<6tF9MQOkUzoBI6}*RBZz3T zvX1a;(F=keS_&Q840^(RhGmzXwis{f;h1gs%g2a#Ph>I!1$A>e=Uq3H8m#f9fag zSf-jZi|H4~f6s15PH2;DGUEb*+HuX%SK0ZCM9tEarEJP?!rU3Kqc^8c>tS9Oxj&vk zZG()v*6+R3hyqSd9-MD+6SQKeDEp_xn4qpdq6snIj=%hs`EU|wk>C3@Py;-~hx)9( zyp$J_dh@1ZjJr+;96<8V{_#tTDxl@zRbYRXnF9mkL!oc zD@hc4TU1Ivo@9_ld+L|4Rx|NQE$Jcl=dnQA?WK+-w|n|?-bd~i2h#2kS}kM`e$T$ zfC8fGS{x4+gc;|XI&c#>ch)C*|J%$jVbuN{5>shxc@t3N#*yd^3>E`NS82tg8j;`B zfn)h5ZBV!HUr`;G9!Ztqd??x*(Q+vDzoejJ5(zTF|6ig$!0fYG-JANH)!=!MUwy6r zhuYP7I69z21gYo*q^-jg4}m8kN(3>KJ+&r#L?8MkdYW?BZ{`w=9@=i4)r1Qiz%!|F)XZUurUJ$T@W2i1Q@v62kW11quN^%FANh#`Nwf6-D9wV*FV ztemQ=QBdym8~T*#9o-yiCEy7boV-ly0vu%$29w|7w%7-!prfX~WA=op8=Z4nrlrIG z28x~>9FkT|PR+1UOxeEwXaRtPNzCNYV~mtawUPk4P744Zn|-+?HxcY`|2r+rk{&mRP^{Z_sdqmIWVj4+(RfP$7fN}w z?l#UR!Cg(=rZc-ihJLRDR#Df=8v_c+Nhr`5NB}~BEI#s1F2T(QK(tCf1s3m05AWIx z%qNrzhy$MiQQzq8|0rw%fFkxLXNy*%E#;FFhWci^hRpWR;IezfmD-W$wZ8iORoM<~ zAgiF8JPH$?Y~ttEkq3!buifT|L1+NWLxh#++@Hy$t3|pk1j(*br@DbblZ)7~0KL75d0-xJOBUqP;>6W+ftMh!6Dccjkuj#Grm0s0qT`oT6K8Aw16jepYboX zm24x8rsbxLY-w$hrcVT}MMZK2Dd2?ec!UM_I*z=9p&WgAUzAfPQUvKTX%{FDJV-q^ zHG~zcozqJVuWfZOj+9Ym9@Q)ms-!o#-%am;!9n%U6Jpe=chRB@=r!1o01(G7qMWdT zjl*UB%e)o5#!!0yw={=<{uDuid0;H43g!Nt4f1o@^jeHWZlyUkDTY&wHTKQc0ht*N zz=$!4x$!Umj872iEuM4JX)Tmg_Vrkcg9=CvI=|;C&bGhQOIPYFAI3GWz=a(FrrTnC>P4* zoy~qoNq3`xHSA^5jkS zo*g%-*{Ro~&g1QQtOrnGBOiPl>iib>j>PU;aNWn<>9}_)KrWRfsNCCJ@m#u=SDYFR z`jv+LjfPo-#-ffqi92j8X#5{p&bL-3!)(KAEVvrp`Tv~I5DTP^GB4oYsYt?}Aj;9G z3I1zM!WQ(SDpcM1R$^4#M%U{(Uc9e3ee+-!t~ zFd0zR;y@AaEz3ms;Qxco^`DVfL7K3zohrkcv3zJbsQs4LhXXl_Ju_}%m;=;6y#7sw zf$=|{n;WDsV=({uWdSRR48%AiT2r#t1-M4eVK2&ZA;|7?&`AEjSw#L5!O0Sf35<;j zcmA16Ep7vSgk4g*KsE#ef^6*LH9-lm6efh<8Z(N1E{Ho&l6<_!=L`N!N3aw=0(PaP zpy}5tKtykuTpo|Icj7o~=Faqjj4=EsZ zzmq3~Mf3yg8Dbb}DI3gBJ{4qTv)w2JSK9||v4|XiQu&qRO^3O9^TX}QL1$-7$Gc$I zYjAy{R5ztCnmsq9(o*w0-M3og+FM2JFd@}tNPfxq%U7UYXHRi>7a{o!$fdxGFDIL7 zkoxa2W^!4ets*fG5x+?Uod*A~QAq7eJ@z~S@&M8(6Fu{Yp5`Ref>GS#@9|zw3LWzH z`O&f;5aN%!DwL8HaaV!NRZH-Z0a3OOKajltH??~dSOplt&3ZI}?^e@9yzG=2wv6Rv z*T}dw{Vv7E4*q5=qrv~S_>J{Vy|Q^22u*={^7k!wpu>o$75%2(-l}PI<6Y2d!MY_0 zvS$BR(3=*x7e-SizE72dLhe~UWOYQbVlOilVK$cS=~0c9WzJ_2D- z1=fbHk3jxlLJ#`(n=taO|CPc@^uG0heKQ+()5+V$>#K98?pp4i!Hi3rF0Tx~GfU6? zK7ob!jEfwfrKcH*Jf$a>^;Z-FD3p-TjK zeK$UlTY-C+|A)8xsd(bQf|up~dm-|{>rM9!w3({c^7Gm6+e?aXVed)ejyLN;F0G%Q zx3nm{>HDOmY43f`u3PO=(ActknKdJ^Rr`10lXbD{y=2}}y=I#No0CQEyN%~vZ4zw$ z{v%pX{`OLtb?)@wtNf6d^2lyHT}qKz1e5^F_-{P(Yxl7V_vqe_&f&c&?gjRQhBp97 zQ?hN_|19r;)BQ8^h>Tg9Z{0U7V9npYHDWov($`%XexsIq2Y*|-Rlp{5v5x!SZxx85 zK(-Oi`N99;7?{D-F3pkpAkLu;v)@5c{LkjaMOKKXcqoQt5AEfC1=E`zHPN4<5JO1XBf`ilu&a0iQZuSK9|_VkG8!w zdRTALC7RMD=@-jlpTJBUxUu2s?`m;s58#F~#BBtRA~t5r)nUnER>YfSzs{>=2Gzlv za|I--juFdQl^!IvoMeff+qlyKGHRU9J##z8;z54q(IdY({cn!}U4YdMd9>@?AB9>$ z{wFK!88ry4`Y%agfD~}QSQ&Jq9-VAl@J|rE+Ish(IBBGC!r9_zcIooipS!~R`dDA? zU~Irod$h>=D@5md=6dF|->B!{=|F-m=L{!q`hoeVG829%|E1M_Kseh42;M15Nt#qA z^H?3=I~WQm(7aBz1O=cKgbK&e7wvTaiwGVA#jJa4@{EcK{FJksF+dUl9_{`(kW*}( z7^3OBdT*EQJnV`m!WH83mhQRku@uKr5dazB{W{V;S z&;bD&A*i<{;-95w{N%H}YC{nOLC~jW?}?C#-`*=r-NMp@{82;f@zx0&pFd*=UH95&d5DVr^GUSDUUH7yCY{O=z?ANlOMy_(MtB??{iv_jW z8)c&E&J5N^-Xf0XR+ny@<5cLl$F7=K7(_cVt+*xyIsJ8sECH!EJ_HCTzDnEO0RyGE;5^ zLuXbPV5`&GOT59QuL*Pp3`-Ix`)wb#P^dK#jJ&rqi^RAOwZLM4VdIP4xq~r56cYkH z($;J8id1Px_gbUWL?GmR&mIjndhYvz>>7u_guK5W_3!Xwqh-`lCNdj4S zRD^l5Se=TW?Z}rA!=}WAKn7R=qbxSjEewo*7TFWqWcBE+=tv3W<$k<>gC9>W+?;y9 z?e=ZVo_FjFL=;$Y5(s!+celQC-&E;GuR}lH73bqb)776f*C?uMm+K9PBiqj6+fp(? z>4RI~t)<<(wO$68<%~*hbwKHB(dqXoIt3e(%Pxm#6ZRI5U^`^Sv4~vJiif=Mmx%@O z_f|e7yLUc!W|9Pen4gRLG*~uLk`y4^n&A{PVV`9;x&%G9cG79&e|qYfkd!0~vNGe= zyeL#mY<;H$~ku>8iyxAzs~E-PAmb z+}@F~$}XeSm|{Lw52_9-c=6h_YRzK`1ALM~#<9kXkYq*4!qP`pQ357x-2PAd(w=rg znX4>_Wv56Q)bt|+0WlSm?rj%5+>}?EbMNlzhRpGn#}L)oMvCkgX9nFo{P0t>0%A5!vz`%@fF%% zWb{i0Kn-ZPmn+{wxZ9qIEq+Uo7<`rO#qSLuCh_uE>*dt_-n`1kK3U75CYdFPMg z<#q<+JGCNsEWNk+-v;Pn?J1KD;?dhTUD8?pi6KZAG#z#Lban`ZIkGgjqw+t+sv|_h z;Gb0Ci4S3x=5InNE{+=wo6g)GWAg_DaA6W5eN2N&bFASXe?!pfvo`j~jO>cg>Db2j z53s?$293W*EjeM-VG%ZubPCVkmcHh5{BRfZh$&MGL7f`+tbi@LKslIOUc_x<q{qdrk@z)w!pbBo zEe4+n#CBE?QY32^`OPTltG_!sFe_KrU&hP-`W2>E#yGKkFHb2$P3FT;SUbfCoXapL z*yH%_A0mPQ3ixd%8_9sC#CHpJ4tPPw3o3Vi8cF=93Uqyb6%6H%`HnKnDE?ez|(;PH9QWpqG*sm#E@4#U6yGebFpOS`XZ!M5Lr=aZ;$fJ@*yw`@GO ziM%KA`QSSxEzw&Eri^6iq;9k}Sb?)7s_*Jd;fNn5CGr&Qc+q4-h;*DP{kWHOWlG6=px%I?4=G%B$Ii5MPx|EAM(tQ~r)&Lu>@G{hu9XB;tBfCDd&ji@$n zXh)6W&p%RQ`@&3z7%30K#>P%aP2I3U3snpL)-H`8JWPiC@!jecdxA!qfhhHvqBlJpG(aY2@+k@S}CdfdtmD zQ9i#L`n>Uj5^tKYU_yn)C^&3kvr|tnun9Adx;c?B@&vj$vwbndm9i0dBk$;9j`9R%a+>8m!loaH zK;X^pWWmru_Y|2!a4y{SZK@1=f84B0UQ=vkPa_$jH@HjYKEnZt1c^$lmnmrAXIYk9I$2K0Pdk+f9R>mO$GIbFPFdyjes zr4i*cb=Mf6n7)iL4p%5oBuc6*N}{y_KW}4iy+Z`y!ca2_FQO5!gV~P7aZ`!DQM?mL zCzktu_abD;X%$+-(wHzu*ch&!+UnQ8F;<*+(r9qRp24tBvDW(3=zV(L>e~jA`?Oe+ zt+<$}GscVE* zR(_F@K3MN)l9{A7n&u0C*`!jVKcbwuruB?{))dW}D(7J607HXpZF5n^Ge zPaGE#5~6-$#HH6_%EqMPVckP`r~C63LR9_z^9x$1t<*7fI;cJ9FXl$qM@nRa?&hh! z=-AiFZSzg#;?hoA0-OgchxKR8=(hXL;_6fhTbh z`!g(a(@monyZ-*59~Dx8)N~CD#6MHtCAHK)3Fosn5*F=T;;Dx+E2xz1)=u6HeKw1( z%dGTU#o1yvb^%DN5c0#EID-=0VYVmqReLUUA7e-N(V_}Fms`mAzAH|4mH(J(Gs9S1 zhO7^98zpDAD3;qkLa)V+)9eGC6ch?I6spMQ<=v#Bj^BNmVMy8B4lO*w>%hQhAkNom z;OD&PQK!CWCu;bZ%LX!nb~gswu?>}`EvNGHIo}Qr3A;5qRFeg~dTgH+ms$~e{f^^1 zG#d@{^)K(A8nDLrE$*~!6jeI@VlJ5rT5L?bm~v{nb3E|IDcRZHBs}rqkq?jNXqgxH zpH1hFYWp>R&uQr3FN*9&{21BU*}o2GoBi}*fQ1K%R#|^C6G|NZI-d$x62{jleD~W{ z62K^Y3Jm4|$rv09@Q7(%#Z~&8I_brxZF&tU3t=~ZrHzl3Dqt(Hd-~UHW4F?LfUag{ zKj}l|fy*k+(=n#*-;@uHd>uk>aPSAJrxtVQkT`+2u|~OX8=v2&1DjeBa}PUoJjs`M zE_C4L;y%@*y+u1uQUq&sg!Ss)4lmj?Tuh{*BB|lo>SGe2l3OfgOQphA&QFN49dzy< z`Ez-4I;KPY&1PpcspEN&g8J#^r~4nSqu;cg3L9{7Q_@kAOEHk)RN4%OT{YXqJ2XEj zq`JRa)}A%=L1`bwrno9WF&~%o9pL*z0!sP;4bAxh>_VLq$MseXeQF8u_@N=pp7{G= zw-)T(yw0`uwc|Q9U)1c7#Vl-pnSO?1A-vDvd$o6b3R;+xX7VB&lP&=^4_NpXW5k*i zg5o8(}uad?%@EDTLU)tyCnPTvl z`EYX`H-%wbcI7L>8%zN2Ljo+s8Tm}Y2R>68(Jb#N&c{6UJwI~AyUi|P_8`BM*L98T zl}R|BQF@N}k#kLj#`A?MWv*lGcU^Gr@glmKjH`Kj3)62D z5XBdqxMz;{0+IL+hT7#@@(ur?@>3%)*^n`6eV7Y1W@L3zg zME*3VsN~U%G7<81F_e;9eH49K`Q+R{*m1sg;Bfh|=f<~oiFD88QGn3T-E6YoKM=4u z@25p>KfDLQd_!t6l7$VeY>9vMfHy0_WZ>)`xn(u2-noDRLJu7BXU5krYUZ-7UMQr) ziIVJ0150yULq8d1h`B7W?{#>+e>4r-Axk>h@5-1?-PT~;6~J3Gr=%!Q*Pws;dMG3$ z)Uuz(RtS0F>o@m%w5aE^PDix6SWNbT-FUTa1KdUZ%5SFm_K;5kopRud0@WiF1@jnD z^247vpdXBYm~_2)K!c7udz*T-;R@Qo=S{DOtlF!op5u?_Vwt?Y$EE;^0V~_^Z46Kk zZ@wVd;D*J2+VUaI1WL+u=Y19!o6&v}uMJP7Ih)U8g(ixUKvgJZ$*-6wv zJGw*n(Q(jZ9?1XNNJck|9$UMBzfg#6NjZjO%$>04qYZ(GD) z=O~jSU!MzIlSC+9p%=>XD_uM7QZ^WqF)$fsBLmdXag)bcn`2t-P(NGEXx=dRb1aiC zx^3yJ#+|i74gJrKW9(v>*m$fJ%e`)57wF5&?<+Xk@SD<9vtx!;I%;fywmOl5{SVy*;* zw&VgDSFZzdJ1}4bi_T-#ufE|gt;drIs~!Fagv)6+R8R6VhWpF3Mw9neW-=6d6i_S% z*VV5r6DJ*f23wP!)2snPgwc{O8cG+$t#x9l$2Fb4wI~dLY=YtOa^$1s;Q1buz)Y%+ z87&28ch=XhAw%1j&=P(#4E&9R@6i5l?bnA|T*zr&hvzQXfJ@-T$PR&454=Q(Lg0xU zcTKuhXulSA zp-Xz(>U?)9T(74+e=0 zk$GV$FR$*f zw>WiCKfaHRWh0qs67QklQ!8)@T`yukh1wX?DLxJi@R{DcbP@4&Kgz*j%7hb@vIPFf zKO5#?`*gkbnwoO+q?9Tq;r_oXWZjblzf+3) zd~E-$@3uWHGuD#?pIaAf$2*?mu;~R5dQN%5Uj9E?09@~ea)kSpA1Pf(c@DL6CKo!FD(;Nf$1%F*N{p-}gLf5Q288#^d2 z;Q>@=lH->~Q zMLJ`SVdg7J|LVz7$LxpR-+l-^7@-99@XUhUE-`pqMnhj*D_Y;AL= zRubm@Ft)~sk{((k%E#S!1gOx&WkDaFr_6snzv#5kH3_@EHx)%9n<|3&eG=dg#PwJj z`$1Y5K0eLYtIBGJ(0mBk|AH75fc!}KjxwD|3(h}i`$A4ert0KWZjHam*eo)K7K(`+ zNQXI_4H(o}udy3g#iYu*l;cC-NnWk&9=JbQ1!Rh$r3(PVsf;W=N! zv|^2_Fx(zXssD6Mr%Tx^_vk`fBK&u>OjMu)gNI8lR>OXfk?58*pMf_Mu&;e(g{AE( zrB^npxU?5OvWOBwWXnqneDgbshH1gD>K*+p4q%fCV;H4?Cdag zRaz}N{TdKy4$w{6e@LDoEE5unS0LRWk3H$s#qC1T10Ab?cYpN6y*g@JSj%w+){AQ z+?X2z0{^BqZRF`Bvsow{*=T_BKRTKlP!$#T2U;6q*(5l3os&lQ^hj@cX{nQPcU)psD|veV z%WErAD`Lv!!>1{eQJu6xPQe_Lj+vM={uDlT3y`Eg_t&yWoP)tpNBIn)?j5RkAq+Nu zYlNF}PRimkUr`wGY19EkPc5=Ry`q1>x|}?*=%KN!NjSHz_mN+)GTmjg4kYsBjI0_D zXAV6X5rhv@M?qPmtHwyZ>mzrQa?$>!n5A{#_66zZ4knr<=WE~Nq2ES+bFN=_lqNJ3 zD~<%W(I!m6b2KCU4WsUpGB5FP6)BU@L`^6S!;BPZ4xL6IEhy5Wb+;J*#p|HT{_k$8 z_zaJd^YiD`f&A(lo*ZyvprX?HL*S!jM>OH|OUApVy~)c4n0F-P(1ac*92_H6Lv8e2>zWT&-NhqFjx}U5406wtJNGGH4FtjO%-){U7j!Krve&sld z0yDvtK__-xzsja$EkbfCV5)r^5B)DvrYQSSSYpNG1qnAdB=_ifr!`3+%OqqB@4tD~txAdTC?gGVHOT2M--d4>m>=RV+ z0f}6g8)*quJDhqM4cdI}B;HJso_;WVCGQs`#5!+U;8fZ68U8#xJeAk8Iz;!&S|nRB zu-5MrAyH@`7#vSRFL_}ym-Yy#I#^v~*|we%`lVVA7yG@gh%j|g3E|7tkD?K0ac z{(HOQ^#{~P=d1f?JCCJ_AQ+h6$dFM+g7Q!K`;u6dS0(si?WhOjRkk(O8y<5ulycg5 z@QG?$0hyIqRrl0m`R55HDqQ^-T{3;~a)x=u1|rs9xa*tKvq)-4KL(j!r+ZdEUxr%c zk*%aR_un71a2cED`gh2XYOeG?Gd!;Y7%T3cMF^j$jm<2FtJdR2oY!Mw7Jz zhn10v&;%7Zr3AHpcLxQ9L?XY9L+4eef|uft%u*zpPMHsta7A?*^4w#c`mLOII%FwE zPpSaG4V76DQ6T`2T8r~+ST~Ifo!ln1SFYY0BtDE4@>2Vi#z;a46ZTj`?7^ zks!rSX6bq7S6QZ*r>s{p3gp+%`|bu3OSUdyy^kjQ$wIH_P~o&5H8ot<)5Kpxv*gJ% zySaDpxzk*2GA;VneMqoaUq004f@X#}k zV5siFPA_e{A%9*(1mEokz!Q_mUoIOfp|-R$*LE=ZgE;Qi_iBgY{9kzy1+}%wZkuNb zg23p!w7MCemZs#qti1$Nke5dxIbY(Ai6*aieAg=IIW0BxF@cd&P$-l*o;2^0GS@cd zW0E6#Nw$NTY1dHUVE(gk0sR8|;O>`d32iKmZx%i>E9 z+BGaIPqVeSii?doadX1(s z@VHHl$G+3#Q~c!Y&)L?tkXzf5o{IQ~IOTGsNY2?WZ?eCiIOggKDJycmqTgHC!gm zvNl@uGO?};eZOY$O2lQymSbPPu6*xO{GW+^z3g#J=kiWKNGRd2gH?Zr$YmwO5Jz)C zkqPUP@mqjCMjpZnnwVgIwCFhZSU9{($02M^nTxPO|E3?qb08WIEvlQU}Tum z31*2l(=`LjTsFuzxs=9xLHnq*#)ie1frV)29wB+s&+sefod8nB&`3J0PXe}bi#l9& zei5Y^e;65Nm1pN`WLd8_&8Pf4%MO?y>&gX4yA|t~eb9c>QYgIED#aFOKJ@dzTyx#4 zuytN5$=h!MnQb|j1+72-Wrjr^cngB1$nXbEqGIem=%-gHmQs8bV_%@$C--UG9|q+O zBlPfdTL(mmulDOXG9pom9WO%~`_sO=&I5pk zl6)}p9lGC)MLM<~CiB%UJUk)!B=ZqbL7%jx#PI-n!h5?Y7l<#s(^`i5qxo(2R>|96 z>0EjZ+FeQ_NXFI0IiwJKeS~OrWAC1wQLy`&#BRVw<4afnNUP`kqm#;Gv)y9@D-+!1 zyoYLqcA`~2J6bn25FD%&NI!9tP(Gr^xDdhMTP#&Z*KZazES$c{6iQPJv?;1Qq+L#X z`kPog1wKn(;WgX%l+rPAdW=b)Bwib)aj0xHB`ihaCpej=z5Qq)u!5pDJNzSegI}NT z_Vs`c#Qphn?dGWjr39J4c65R zm9F{zy-JlYz?sx>g+CUZF0}5%zE5U1Q9Y$3$D@EC!CCHfT^lK6v8fRiKrv|PX;Z~T z?ZcEZ#DTq67e>9ez@6jZ9i*Xd(QzTt>K(s4obG792tNRn^*PGQ_%cd zJp6TkU++_9u?&z4=weZ~1q4W8ISg76{a|pvZNl~gFJO*V>eh0aYfq)PK&W=HN787? z%R*sRZx+YehQJ^Vt@ZcXD^5BhH*8_yS+INw$@M=~Eh4}GRJD%zp~UsY;ewCeARmSJ z(4I3GYF?BZ#!C9*LtOm1hNB~oV{P3?;qO{kGgN7g8Nmj|cRKlQA}<)(_o^^w&j3fk z)`J6=8avr28fBouK<+U&9bZxRp(FLuS^b?=-}UZOu&ef5r0G%8fw>9+9h`q+1nd^> zAE!+IdGNeF-V#Jldx0iLed3ba&f{N`1o*uyfDH5LxQ~JL z;ZZ<@Hk0L@5^m7Pm`2B@NrSgtXZ?LzUKejB>#)u`q%mI5o&-;Ur>yK9b;m31@oUk! z(dFIv<$LswT>djsOjJxWn^1`%pEW@9y0<~iq9`T+~2|# zNlF;j!7XEnQ1E&-+x7Pu0(avQeE0$q9x&|cntS$}kD^haM_fS?Q@B|L=Gf;ZcBm7@WA>Rz@*_#@plLms@kntHsM`&8??IJvB;e!$%)l$b#C$;AtqmD0dsL^0P+ zr!cc|{nXcA!yspWBf8?f4Bv)i-*!_P7UFfU!&gv|Xmn_u-JS%hHd$zin=NyE#q1nf zLOWK=#Qzz0EduPetC?|98YFnl)12J&zhGCOTY5nfhfrOcTr@0HOz1EX5hA0T_~Dt@ zAu7>7w99iJJim@usgGb~`Ig&04F5IW{EOR6p}VBJje7#SVQaAoKTWIO3~u<$Jz*EBiezNOL@SI~4S&`SZwWn) zqyIpJ#GEj-z{k`kg)ByBM~%_`&O)gS%3WbHP`iCws~UVl7YyaBR29(23EuNGU;0!o z$+WDjzOOt_2hQ<5IMe8X=OtXb+o;-I4vfe48UJGVa12L3>;JVWL1N$(zLOC=JqBtVb%`5*3eGx% ztjKa_P9dZZ)%R)Xe~9p-MJiOKY;n@3GiL=QrGi&U>YD2BTK|G9LuF+GGjnctPth{g z4pimWVY64E1hlMTzW1SQ?d-()M9Al?r^Tk7$3mh=%$FMeMc@#8R1IS%-}W_N-AD_= z@p7gwE7dydQ;vNIDK1}Vf7o5Igb zM5V!!f<~2K5dS*DgovmVKhQyZyOIkNPmV)}E50#anMPu%M#!7xhGy7Pwo3c?VqG<# z5EHOr;y8jbh<`!V>?f&7<)TAdQq&_%Q#>5^OxQf&P=D^SaZHsGx_*uGHNt7pU%3OB z!wH%t%#M%-Kr#-0mX-yhpiC=%Zz<6K^%c>DR>w<^;?sUws-UMRJ3G4Mfr9BMpFLN^ z3ynPGMD&7VEOr?@32r&CWdM|l%U(+S0ZnNg=E|98hgp`Vdy4?CWwARO#}?Sn__be~ z%_$$f-{aRMHVJPkIoc@bIr7xWn|IA^b53{sR4<3jjX_~`L#32w8$nSk+qG(OqiP31w#rI}61ILj! zx9_6l(&eRpWvCTJ;DXg;$P5k82!^IlY-L?mT6pstG|i<~)W0m`;{9SyEi>{p`)(l0 z7QmMG6Sj{VAaI-Eo3ph)Rd<1^nu|!dTVEmlvNUrK%vu}YzhE>iZ(@?Ymn0hfXMeTN z(_a4VU>I>5VDOpj3>>&nJf`U-PbeZZV2{gmey#r65=wo(#~qFf9}p8jFDD>2J_*=2 z|45K7j-m~Ls{9yP71oN#FHHqp&y6PQXEAzcb-<92bvvd-_xJm#JY;x@m0tLez1z<* z>8&yqK3C&sixkI~QH-YBWLu}_d)q62qHr)mTlV>ki|TiW_oQs~Ln7TBWFT+?L(yb9 zB-V#UP~JlLybkV{h@J*rBRNlptN{e{6?d(EC9|lJ(Jc(SQ`Dmi=bWF{R~gt%j-b%O zy}TeBOR?{dzuN4fs3SC62_vh{HiIWMHE+yUzueo;X)kk{%R3kv3SGTod_9~i%_^EL zmzAN)Y`ow}sgU~Tzaro`a5y){KeMDX=3+2b!30Ck}JXXT!ZrminvS z^cZsOF*O9WU?#WijP`8#6Zf_^^QDEX{fwW4Oab-WEN+vxi*wIR9hffmIJrZR^o4GZfa@9elWN|(3)D(L_I`S}j5U75g1}q%Z9Fd1(FfF)iWHN_sye)LhntZ8J&b=h@_he;%`< zSK`TCKr=A7Ks@}Cnh;*U+~R{uUr)wKBA!H{hBXHJ!(A#a8oxEwyLUd^CGqOb3ZtIi z`b|Sp0tRpOhHgWe6T`yMcGGE}T&Rn+`!)})p+*YTDF$DDe^Dv&Znism9JL0g{JO{rhwysf?ynJy9Gy@LbhmIDvxFb~3<@L=>!p~o+S#3>@sl(PP54yS6Isg7O zNLOqIAD8LxVwHjuUT=v-xlO@FDIhQYZ71iuv1~^t*NymVqj6T6hdP_={I}FIcKpuM z9B>RZew30-3y57#JF__Y`gueo9ALy+5f7FzXE76UNU$&l3og8oS;tNvwVJ}-v|>)6 zJ|JP5t;h4~CHL_0R>mSd~ zZfEg|%>A0(*Cnp0|#EAfS!A&El<&ZCW9T zz`f)8VU-v2MTWcbr~@CZ^Tj;?hJ zFtxb$Gy71VmXzz9d;)?S$^>Y;GE^DDuNgd=D5^ zXIy1)%3lYEvLe6vJ>ge_%dZBnvm-j4LSV0UgJg(`pon?(OQdhXU|P$tRk7yvdZQ8E zqV_8(Hs)eVzO|i7YZ{7;=F3++Q}urG78~_STiLnC9s^LuVW}`JpvZrP_kJ~46eF8k z?-ED2e3{VusbjAdMUtotE$Vmtvb6$Fm{=khM=U%gs8T}@B^1O%2biNUn!OIKO8&Ba z&jW|vT^c6+fn4x7hf(1=(9dE>%rVoq<# z!fIa*8PX?YJbsl53OigIlkP6e;hzq`xLiG0eZ}_N=#235=~sPu2wZ!p1ECBxKJYY` zCAh%$y*#4i*X}aX&{EUg0uY7#{8hBs;XpHS64A- zt6|~I-1!y!9?j3BYI_ccF*Kp1%~bOP6Fu89VPIG|AeB4MVEM9eHtE09Qte|ejTmYH z1q{rGJL963uJx(}h*g->2J_73HGwij!Bx?ST+n6aqqM!o_adh$-xKQ}&bB_w+_%ej z0rITZY*ZQ%=PRB`@|W#&Q_h4`r89m}n-Gr}whHN6Cqkm%6z~iZJv8o+u^ynCDyaNg z`Q9QSbu#09L_|0?ldk06J$ja9ojnQ#6f$Z`YTxCq+>LB|e*~mh3fX&~I@7~L}iy0(ck&o7@$ii3XOw4 z!AcCczgmLF2^5#_v=QS~Ab+-gD|o{ol>vYjGtsPSA62$&ocZ*?85ow?cG6AwD5o8c z^Ul-PDW>v%%vDT(jY)iMKyd$YK(*b}+Lwfk3>DwAgEA1$au=u#iImh#wdnE;GeVVtm(=L#W53`nwgExHGG*Da?kKQLJ@FS zMV3z$2~bg0k!8_TW)I2}JZO#uRYXCfj|#Y6XW;;7y}rJBHSzpSNLQq9$b|QLRB{g&rk+{3a42Zl!uhH z*~TB|g%X{OkSGrbL=@SscoZoFMB*vuM84p% zgHkghg}O4QUpb{rHn=N=gD+z3f7T-&Av(=W_RM(sMh?`DG2B&} z!@uJ2U{hWiG?iiJ9EtVSsoAn9VU2Z$A-LP(M90Gx9=eedzXFA#~vLhiou1#Y6P+8B=s5&BN7 zu=69j^BlJ35XgIU|44s@PlxF&g?fukw1TaDf-g5B(=Zz4+1rH>c8j>I1}Q>?clOjM znUm{?vA{(l{p6xOwUMfO5fYP?lQC^nDJHU=*Rtc3=uccD--gKCy}}x(?95P+KOkCx z^2pSM(-mJV*rlUVHBxV}5f=u!s3SwNT3VCzHnG`vvenO|VIyDsGo4e)GLMm z0kEOXzrB~I+0cv$qEnm!!^ysPUJ}9i%5T>ZXKmqtI7^S1@GUjM+jQt#1$!@Bj8ff0 z+xX7zh3?gYno9`3I6`s*2oQj=D+k*W91?&4VKbDEOYmX-V^w`;1_T9txm1!R!W3#G zixk6Q8MxXIhLeIAlP@IAaTt-?8;_S>a3ktTn1b8&xlnxvQK!K&qEsk;)zq)z%>q(T z#T&|5&@T!)!sFgL`8@Q_K08Cv+J(<`A})Pe-VCD{P58+ZNfrv%Aj_WW^TzD#1v%{T z9i>#6b)tHvw0S^SGb^X~j46unRTXnnSV4?AM!H4x9HqJ6bD(uW;Q zqi7h$=0BLyET?z6!P-he(3oK~zdJB%@mHXX3!;7B-;lt^oS~vytol4A|BM37NY?b+ zUTZ4Z=fYs7wUl!68V`vO@~cvcdS2k>-3gA#BQd)d^zHg`L_<+I97HCej(Pl9e}-=U zdT5_nzE{3su^S(*wVoE2m6a`XZ}B+Kk09YKu6ga6gInlwBD=k$90G*1UBbziYh_6ga&kaXQ}C z_5<(|J8%7xW=uIm4d4(ev`~vcboei$%6qX62Aoo4S0Z*mZ+c;{*A_vW5Bop*LR zga{3OIoTd#z=7f?>ws~h_5;8y;_r#AlhTY#j){>2vYd*w1wh5{B?OSo zL{@XqTYAwuns$!bqi3vaQm)u%&msEbCmJYgOo{9iR;niR)%hDEqn9ailHA1_Eu0y@ z&w0nC1lCTZ9Jr@R6SLMqO9PKy-~s$7N|M{~OpTRU?;X&4z07!7qEVV3Nha7hz#u62 z8Yg{7G@NUR9Wb%pP5#&{Hn{L0+NsOhwZGBy5 z8l-<&haS^0Z@-W1 zljpcm#>I7O!7!Junz29D|8Ng%JTbAW&u2fqoPPh>WADoZL4V$HNcbsA@VDjp@n)}| zlCVXU3YxmZxgP=hQ zDJuBeFgPHbEAfC<+8`+h)H38p8r2OdTHliaz}>6%rK1<^6<#hm;45iyc8BLaxIQ2L zp>0Q);6zMNnqBhVed!f=r->SVD~rA0`;Wf?B{&)=nOvm?5b!+iz)i91ZoQ7*U-Nrj zun#0Zhq#9x^SHT-GR!{f(Qh0(U(`BF0(3@&0!2-C#>yTwIXP6*H$nzoi1@Yj`{0JX z+-4F!e0K-v|pMt6bJ zEiPES8ki9ZrhT~=or}Q{buo_*0X^Bt@<2CB7vkPNSxlnHLgA^dIQr2xM@FlUkXu|% zi6s$WenN-o4`CA@7c7&~ULg8=4O{-?A{`z0qjUPz?D!+*>rX5ytnlN=u2nFF9`4&U z6$dr>dgQ~r2^1Ug5TSgK@8J-xkbypEqqfOh)Y)oWisF@@237(hW!8r+FqTM;Bz&+5 zq}8&IxG3ad6~xH+=)jSq)6uAfntjRK&jo%&@(sA+Cwj?=40C;#9i~o2uBl6-ks*b$ zr4$(o<5VQX3w5J{W7N#(a{DqFp~VqciJ)EY#rxHp2^n!Ed;Ejjv~xI&V6o%Kbj;oR z5I|o|G?yS^c%~u2JKb-X1D}6*Yd$e4Ux*s{SzUncRw(Vvt_JWQ9Q-3}-wG#>O4Qb? zK>P!(7wIXtA^LJxPe8BCWK$0RFHf>W@oqM*$>>X#$*1R7SWm%w1mbLK$%7fMeJ894 z7%45-$&*rRhL&Y5tHde{sW=^z(`oO9+oBUBQc*oyT1v)>_hfE^Z9)Sr-6Li{{aSIL zrSibjKuYwDCn)`zUL-8t<3=#&T)z=X*w|#_)f;ltVz&7s$m%OMa?WeH_TA%spcX9q zA8?|%Pvc6j-H6T(>%j+rl`qz1)>ey2WX*a3Ng46^oVAP-Nc*UTLwrsR%$r)!+o zi*`iI8OoRieV_11{6wNDk4-1j3AY~okpK96LEJzF`QJb^8>oRnK`ID#biAOU@jf$X z>TpsN>T2l{k3WyVA?_rpg6$fA>~uny90?w4SgIXSNMcIyFV4n$*QO&#$ZiN{Gv7Eq z&MYdLd3CatQ$zB4JhNvDY-dQMKC-zBaKzE~Q zf*M~}hD$cbzRwq>4r&U3s?_iD{J;f~4jG(qMh5)``}lmj6iq4J55+_bZmdL4SwS{3 z?#afa2H%*|pXv%Z4h~e(C|9FF{t#gAQ9jl*D_tdqF5{Lo-?Q_$6u zz>BsR?QnyVl&TP7!+ZbUq)oX>44_h<-v%5S=6CYf<9aDq+{drfQ1wc92rjFw-j(BX zc^Z;zIpfbu50o)7ho0qG9AT7Lce?e6k6g{wfJou6Kr^W7_a{dDaURH<{h_4`<4v65 zk-d9hkz4PSlU;izC60#0o&;o30e;zndazMu2i+K`b+G7G7%LyCPB#o~&g9`bm5$x2 zWVtvo73;Ot(9k^`l?au%P@iiob{nZN@h2!g009p+C2Q1hkFnKbbNzb8tp92Ld~j^&Y|j|6M9_c zpcm&qR5@y@rs3Ux;BQ~ZD-u?X&(eW|#zn=S^g{?Y-hX%n7kZV(8r(s`QOJpx!R8T{ zo!r)1&FQS-)v`h6L1W6|2|)@=fb|#$dKKq)!PR20gZWaXGn}lFSMve92P2ltG&G< zPu$*sxx?*#KvEJRG2b(FoHzk!18<=RQUK_)`HDRQZ?Wpdow%nXk&5B-N*a(ma02s` z+|T)-tXK(YO)gp1ZqAJ$j`Nkp*F5A;_`FssFaua=&lnhb)yts=3)S{3(&H{mT3IKl z=`at9<&U1yVK1|Vw9Sp}KErxuh20m~tbHs=r{OpWt{8s;D9$Vj(1Y>?S+cMYlxFp{ zV;rNNA1lcJ=CF(y>sJ22mPN-*#Gpb{L#}a7?Y;nHvD)7SivX00N$SkTX^#D@FAt0h zD6ufWUc1y^gPE2n40ub*XOj@sJ^|6@Jr3DJ5N%Z8e*YZf0;e-v0xb&U+L;2vyi%#brozKv6BU6bhq({16VtpGicmy!68(=+xI;^S;J#a% zVOjp^raRWn@mdR(800rbLtfaJD-GeRq>2eA0M(`&vuQZ-i!_;DO(Q_VbmRp<(Uso9 z%OqO_7F5jvT#}`(fGd&ELdBv?ArS}h69A6Tui&kT9^t|Afz2qc2QA=7QoGUfXH^iW z0t^+yi~FGknUIC~aQ8&NR^T>!yf_|RQcFd@;|k4yRwZiZW}RR!ge_1QvXJroWMMs4 zf)mm4T~n*Yl|WlM7je()Q%YS1FlN@j?-pUT+hx;>`HcF5H1=wB4-gW z>eh5+?aD=^ZFD4uKYGVw7Ptz20W;V*ug9si5CIP@(q)RF)GtG zMGL0AbF=emgL%E6V6&b%P3=p0_mw)m+{B~vjI25hpEc;sqR4b)%)XsmTBhAtT1zCK ziPT`zUu@u5{G&zlK*j>bCFi;&8r}NssJIc|Z|B1>GOC6XF(pVz z@Ca&LAd!&ve>(3JADNc0d&DxP_X(~;)^MiHN0ddY!dD@U5BnL9>a5T89uXt6&@OR~ zY;68mmI-~ENkV-52$275t`CrmeYSA49ILmFzj}nRdz`}V0Vri6Y;NEaveb|T<^}KD zzD(XC(?y~X)RQ({tILIGP4D~y>E&eP=2o4R?GndjrhGrUF`8RavD07hk;{At>7 zwf%JETKQWo1D>+P?t|E9lh@;uAD>D*+!ULt*tvB5QU-{sS?&f^OrdAJK!0)9lB zLF4Wk{bIr0=_e-ZCquq$+A@#jKF#edVtKGKpw+pVSQBhH#Ll*!-uZ0B1}rudS3JTs z;BxGp_fAQ0cSQ=pM%~k)A}CGk6nWZO{EKAf%`Sh_3jI(JnD!M`De8EmbrPcf#a&;96P%;I(54AI^Bck&WB^@#lZfXgW92Bg*1Q5?@xzpa5jTR5qoCK-{ z5La`fgu~(; z5cxBrWFsVqvIa-Fl&*`81~-iPt}ib(b*rh|(K=B=*2dei+z}%`KRqL_knX%_@P7Fa zUG4A|y@nO$ic_pzz%g!<0w_!_s-<>sgMul%lQ2m64A-$D_dM>u-pAC@hdu@S*P;R# zAMwR}W3w+TcAizxlXzeVU=A=5BE(Ql51bM=+7Iv3p9Spct;PIgSH1EM$Te zdjIT%?x3B>X1;RPN}r%06Y7Tw+ER9{gV_4}DBlEr20*#vH1y@Qu9kNoPQR@d`4^_b zMpSp#y@iD%J*TOIV^(-tJCnECT4!nBU-nnh;B+YFvWZv`M73pRQQjNYHC_B6+WmF{_k^xhw~Qwd=^)h?Rg;CsG*x$V7IWHFJf>Q&S!6YGBpDKYPQ zDlnq;Li+Rs8S(!}2S&Yo3F__w2K8xjLwh*~<-i59oHF}+(iv?0<;_w8WPO9A?t*Wc z<2>aPf@-Aqf-g)Dy)L0A13n2#ZoP?kPwO*4Q;Rz%Md_8Nf%>6B1w?+l>dP0vT02xz zQOPdSE7V-~e1t~8b}ArWU{I}7<3eQ=Glh@A#(T>4X#NLIZ+a*C&A4jo`hoaVY;Xu= zmrWc&o{#rEQ#rv`jmp{;zHEZh)}zQNdxb2^QP7T@{6&#+=h&}mcoxupOsNNY{_2$^TmR7Xhg1MbiCr?6(fuQJo^BREEOV)i$_)bgiH|_ z5)|kR$D=fQ#e*sANV3GiPcWpeLC02(Ag;b*=&JS5eJ%LF^7xU6U}AH#XNn*+vcFU2 z`Xo=2lO8QuS4T%~cz9TS+kvnwoP)Y#)#Z?$(ViYFA+SOfY~EVV>vK#w#qW+&h12~} zFPE~%`hMxN=w{`P2}Q4>sRw>5kyG!NDj_A8v$6TzDBx$;cKbes+bZ&S{}F;8GNkW| zn7q&g6?3Hw&c>S~bx`uSp_emI5!z=`ol16r&bCxvUlu4f@o`0ybqDRzWBWO9@MIu%KID;t)a!sIOpG%;D_$dg{fR1BTB{Ng%Sfz zA*BKDZOQab4}1)yV3sNaOT7hm!FP>L4g36+gKWXWc1x5bjr`n2)8+s5Uzk@?kg zF3^D~e+sqX<|ljrTlJ?e0n{@LYqXvuo^iTZVN7qnJYH0d-ITU%3$DN;w5sNb1e#BZm0t*GXEoyf2w36Rmm?T{JZnP=~#LEh%$yYBgeW>_k4Hg`%OwpSvxc zI%?kn;a?o;SFH#TF)%-L=w&{N@cP;<_7%?kJ?VCZuR@^xjnA(ccKSlh!j3nc~b1kBr=kplxKN$t}03@a`CQ( zy9D?jXW$BpjSmWXhvY${iut)sj7=8`y4BfqF{axpOq@b|0wtoXkhPIa0jkoLh8{;fxq3w%I`@v^&e4u&F{Hr9T242}vQ z42x$#=A_GiX&QW+L1y_R!2~9@58ZqFF2D=gPywP*RgGsBf1#Pz@HMux7SY8#R1X^4-z)>F_OD`2exe%ca99D`LR)fb|VMVR+r>$1;uwo4t45u<)pt z3#w5s;ld^@JNOI2O7KKC4B0_zBa?$jBTYjT_iHOwJ04b9sE&pSKy-f|Z z(zkJ}jEjr);*)Y=h0E*epvR`7NkJ7+?aFHJvw@a4oVRR4rPnm>S1( zJIJ2}jJfha7`C+8uWQw?mo49jNvPp;^5@xNRx;`>glKfbgWicMAuaRm!c!?qfliY;>-;-9(nY zzv_9@){n1k=C-9>XzQTD7P#DUf>D}}`4)&las85`)LZ;C<@{3!vGe0C)oR>~{|ZD!rLD$NHPTNtCr8;46P@_^2avW*?LU{( zI!fHs_dZHzS^nr^;JJ4jGnm@tyfgM`t(;Dd?Gm*&uI>DBOU)HiB}z^L34AF*X}j5D z_%!G~={PtPWAWmVGuoecBJq{KC(46EucD=r@hwVmj-fi*bVy0W<5C7cWlZQ>pa7;g zt?&j}nU=`vM{Ey`UU1NWP4gYLY4&gub=#zr1#*V9tQe%xOu^9*cXLh` z1iFmwQnEnWbmk~qB6(PLo*1_pFPIQEAh30`oCJ+#V%L}a_;JM5@>!ef!ht4Ha43B>%(_4y*%f~Agia-Lj2xc|5SvjFd^&0&=dJt-M4RzxA5kXPN{DgUgg z#d#GI&jE^qS!P%&rJsq)V2u?Ycf&WuarGWh)iPe%%n;nZKH*eCg=8$pbCPC=Mlo>U zENMfAq^w+HIBh5Rzzw!)xSnI1Y=h02DV-X0X=iI+zb|nY07Kwn5^%HehWzRT73yV` zEQG?cVLN56DrC+jPIK`MQuWZl&B(2PJ}`~(^DR=0{%8FY588L^B)WiD2xSjY0E^eosh zf_f68JW+bHM}NPt+01Gp9{HM+xVc|@nkSHv*x;$$o0w?e=_PAfa`M(#nc;a$0F99S zVKe)B*z#6eV=4?emzQ5n%J$9M&+Hw=v*|uMvsSq0ckg{7tr-5wbqsYin_CE(NDz_+ zU2xmR7LF5UcxXM?Q|mNuZ+t+@>KXZXd)Oc8)N&suzByg$Mlet8+lKaC#+PCe-(&RN zhDD+4-y6NX&g0;qKU1m=e(2<;aATltj)R>*-q@7ol;N;+obU~)Z!bBTZI}@T%*>b_ zO_ddhV$_~Q4lokhFWBGF;wN3SEy(Qz@>IVY##uF}nx6Sa3W_E(*nu8sBx`#Y^VT{= zQPVa~71W+5xJ(0G*6z@k8xRn+)48Lu`Est8x^XpdC;Di$rSmdjnIcV&n~DT2*?D`^!l~s|4!xEn zx89EYW3JiTvlzOXz1@aV`uh|1v!t8V(`?5L{>xvbo>n9|Z={-Hyi zdzbzit-0|JY42Lf)5tTHaVAXRvQAL<4>D9#QFC=E6`lS=%ZKMp zosO5DD?&1IZ1Te#@fmtYZvB|>A@4cYcofhBdTL4BU<>QbmdR&7x1+H&eyuEPXq<)e zH>dtX1Qr&~wI@RIed#Qra(zd-QD#MxWb-vnL5-A#z$72?-cXVr2^L#Om5?FC+S`X3 z#!rfEGP^pc9}*+50p}dc8H>O@P!Pcz9?-5AYws#M5sqKzTTbvxgjCXC?5Bwl*Sr{^ z!ayZj_LXxF%zM#$JC||8y&My-3=0tKU;%|tJ&k! zxsQlFTJYY!2)X`MaO(A>m2fR=D|p{>^=*LlqdL8s%iJOQqpp`hZ&_fC?%0o&U*xXW zR)9#WXPjZMNe&NmO6M>=E*@`BcRayYL>gFtct%#3m%8BF*9)<9y)p{jGdxU_Snkcb zdovri@&OexH)-cHJh*1ccz(L&(;7<-_gf?GsbV@QQq;!V6F2$|13*KIl9OxRz#vVz zdpwYKiez6X^tZByw*f%jVkNKcEX9jr_|Y}5hDFAN4f1WSi9ySJHW~6J6{xESc(^!| zVW<-h5YE0I0b*h5cB#`}N3oLO;@R<xk6+QXbp;&F8T9z;6rn@^T*h!M%X_Sf zbRq=eetKkpEvW*wWarYGi>i1wK!AK>ngIrii|U>Y3CMt#i<=1<=n;uwVqTQ=ybx`|Dsl?S(e1Uc0G_Rx{} zIg+TN<>1aL_+?NKKZ)*b>niU(&W8Rn=^u|LkbalJ`-Xl&tzI_})scQZcu+PXU+yx$ z)v!N=G$RaXf$U)ObQCHLi^Gsx0J@RvgZ|^7{!Nub9(0zFYFDPpdI!%QL|8hWB@`WV zDK5-S+XXYMP5eYK@66ViH$$y#(m2XnrOm79oWDmMkF+SSNjBNp!A@|kO2cc)U8Qo~ zH`Zk~Lxx1oOS8|3!+0V(d=GojyEe#zhnAJJrJtlhrK2T9GZ5q4GTzJ2u9-&V7L<&!Wfaodv_-EDrajWzc=go*7 zgPg(?Wu%BeTd{UgSh#uWLivlR!}~weNO_WfwHmkW-&(E5K`@*YIF%x$(AVBa+Um9C zW<6B?%XF`#Cw_3Kf%;H6^eYlEez|Oc83W*jWpQ(Q2nU5%xY4$B#1M^kNR<$&4Vx8W zvtMU(kGW1ei|`%nHgXQuhB*qg9L}O)8sewU6L@Vkp^q(eckFkUF>X3YS^Yw9jj z8Mepak6_~n3h-Lmb=+E7ataFhNgC)ylaL3r_MuXFp`u>NI=tqlU|P+v3kJ}r*DQu; zaDZ$08cgN=t@+u84%2ZFtrpAZwwG{1>SFK@<&F{ke}0OQqEnx9P~>AU{(xUjD@<;V zJ#GnDI|Lt+aB!S~z^jwf7{^Gmy*O-wU7R?s2rvy$&puL|@V1%Lul-!%bRS&s?Kqf* zPZ-}I1KWu$@9v0AK8Y`0hrYV<0r}LQPf<$^7y&c14a&-|iGOznQKi(;hIOA3Jx~U} zY&QpPStzsZuqJDkWy+gc`o@pM? z3E|!UDp8}Zs$8#iv3>U!3zzjv4(meRh6Oc-*J)D&Vh5kI$@cljqtfVMin z?zd9S!Il%lg7+iU2dz0G^NuoJTZ;{|ilZZQCXqeZV1jls+^az}XAFqMH#NS%cOCX{wF&*!-p0<1#J>Vz) z;jZW3HB=@1v`Z1hba>E`1)oC<&3l`kNl!FcP zJvK7nmezCB8n=QV@QL)wMp?-%qSEY~Ale~<%gYr==AZ`Tc$f4fSIcB~v5Z?>fZ z!wWe0zeC7uR|+uN(9X(tgDq+gzVHsc(qC7fTe3~-P*RA%*W_{Q{sfk|xVY8r)<4so z9G-G2<=GQtEOZ^rPg5>YB^X(gf(2j}yR77qrNc+F(5rV3;q7l573$(?*-(~@5GdaJ za6SktF^VTtHGWbNT5eChhk3ERQ#QmwzIciSh$zhA_TUMKdN|WOmMfsLm|W3Vae6JX z4YO@opc#uoMB~DDP-}CWL>K833NxOIXWGyczsvhL?T`da+QfXpKYoa;Wa3x~B&K#z zN6HPf;t$DF_t9TF$hYRI%hvOh--0% zF<+t6BDe?~00lY(_qoAQvX2GuvrOUNL0>gM%|j1qfK77#^mnU#K~o{&uJ)L0=4@QS zy{zZU*iPg=d_-6`fRSt(1giVIg1_$B&i5uR!wMyuM;Bd zPV?Na1jV!rIr}8++c}pT%+{bnJi?%=W1C@(c5J#I5wP9V@ zgwl%DIPf#omLgu^OZL?p5(MXL0kY%h*t{GO8j+X$#xv?#X2|IkBA7!$DUPYmbFHAXwmaa4WlhK31ZKnfVD!NoQMUUii#jmX(xu z4@YdC_UM13^7UPUt$HwMw!uM!-CQMw-=2hMEXAz^Ck8vRkS#{bKB?<_sIlV`|5wvA zZi2XTNQjGGQ_yQjqVc}s%{M7OS;}9*zItprmF6>`O*+zzR`V^^cmChQ-x%2Cff0*N zy*Y|KIo+R#feZ@`pY=?EG>v7(QVzMoi3OJ!aJP=sr#49~+fUVp8sg?< zzYrY^#`fdM&>%n~_VaJSl=TWkO6QW-|G4+?mv-{N*h2)J&ps>U?cH$%b}y#Wy0SL^yinI z8-NftlI~TS94RB_ACxQ}4Nk$1hC1*oOQ@{pvV-8KYXY>?mHcIqy&I9T3SpBH8~x@+fF;6(fs3FZ zbij8H`e3EHl|Oa>&z+0Tgjqf*_wJJWRRYl{xG@9yDQq9TekI(2^BTI={UqgbXtHRt zgQ1bFl__E;)+l|LuvZyT^5IjTju>-_NQ4I_HnP5=KF3kIbP6f2ROvJa*vx7VA0R#c zRQkHE@jY-=E)em$N)rBDbSt5S(_SK@CATRORxHJHGV5{KMeTI;W%40&y&lX)({kd{ zP2NU4@2E(zVED8uHYIHo!)a;S_k?sqvk?P0)`>QR(UM;k2YpHoO2tA@B+&6Zp z2}TnIZWGr7ZCs8MBwX*_StgY-=A(hJejhlH+I5ZYptlcP|DQB1jL!e#QGa5)7|fFvTiFnG-cN<0l}FAyV0O#OS0}Aco4O!dz9|T%R(dRB%JIMySoXn#>+caW^Qo zZ~rWD_y_kbj9K=t$pA|W#Yh&nW)@_^EyUorlvp$fS?`94X`E__@jGHl6CQL=#fd#j zhu)Y@8r3RW!cUQ8!0LQ^9I@U=xA}7GiE~$ShKD(M7gwCfpkkbt< z+CQ|rOco$@3R>7Yd^3nCzy?TnX@Df3&Sw*>*m3t`9bO(BFV zr{Qg(t8VS^<3YZJO$H~Fz!^6f;RSS}t(H8?!{{~)KpF54=DZ^Y;dY`d3Aj3hqDh@h z_j<+6xP7dN?J%#U{RPp*;ULV8L8cw!h!Oik7hw$FRVid!1#{y!dU?ReTS-MF#;Sdx z#bXrQnuN{^)@|vfvvZ*goPSRY>sXoeZ!lh>&Naz94%=Wh21g&LO#^J?E1@u z!sDD|xnH`y&H@Gw^295@5BqIR$OJT&ukICWjCJoLi}liVc%!n**}SAnL=3WeMMC6K z-epz2fF%XQn2=wUH0@wq07rvXyxA)Y?#ihM@&iK!(P||45|qGgsgiNh0AcdLJi~Ih zZYYAElIS1U0$d(`r}}`-FR;XL?r``j4f}wUVEJqjo(BRyt+Jn=eOh18v!*%8uY=yk zRzt=Rn6p2#q7NBHpi^>P=1gyLjfnJ7m8NfP5f364kI8^%%#On1USJ%<%LcqEHBLWR zarsGH3&#-bD!7=3>fp%0O`9Nt|KoX22qhM>pHiDSdSFEC44^BHSbm)_#70g$wlzI# zg6(rghKAd?iWq*-RkV6!tP7s56uhqmExwl5^FUe*Ki=a^yPxAEn)BgS^OPBMHz%ec z5I>gWtzy%T^1xj*%0E2;Rp*|xlGBq*$07NEyQXIQ=QG$g@_2ER0-pBQrBv6L0%-0lR3Wq=$+5m z!lUNm?Y?V+zGwcD6-v>RvIeEH=CnU!rj%n75!G;_C1Sy?gGYJGgjsqncjj)(<)cv3ehn4f8^zC4;;V=t!I&aaCX8|G25F6yxY=H#o$R-s zn=eP4E6t4a7Bb;5HDJ3!&z0S6W}gbU+*Gf9Piy+^sX*X4sr{@j73nNn@a{kz>y>yl z`!hlWEXY@-W06P3vc9yPAV@rvpiICMJJaguGq&gqA8k~QnY^Q zp*yWNLi=9izNyi%n&m@Rf`!|MydNG0pU5xv6kng=$OZ?uPuc`;4dk`iqj}$SigtWS z>dM(ZXJhel;i##paT{RYi5P@thHQ?+053!%9yB)|9{Va4vPH(bhf4{5|>$X_tQ1hzt4X$pl4#y zPhzDfQ6}3E7Gsz3@9C#rx5W_eXg0d$E^wtS%rs+rhJ+j#EZ@7Xw|RJ6Xf^jhGIFu0 z9lxT%#+q?BzFI0DQd@=TU3QaZK3AkXMxfyGl4UAB8!j3(GY$MA6`mn1Fj~E+53$#D zsgY`ZIidshXvZXG2rYaq7Ko;JkNvb%K!8VK3eD>w_wmOUx%y^DZ!pIH-ZuRPHS(M4 zy^f(28qP11EX%%wEIY;fijwl1F6WaSgGGz4*3l}I2>qF_WglWL91wRe9&q-wi6y!m z;F>dvJc94R{e5;@c2czFbnkUxifeMrTUMT{{SfoA#b@hqgx*5zh_a{c`7Q2A>@B-xa;L|I z*zM8+L&^Z0>Bl!mp> z3ntqAqmshk6Twdt{mrd!_f4wb9-ECo87hUgC!3hM&bOrPHJwo0Wv~`c$Br&#udWa6 zHru0PXGAjva5Ko%+g{eE(XH!7G$s1GMzOC6`6)QPXKntK=0I9+BFpb^k2Wzeq4M@^ zzLk=@yMVf;CS(Z5SXN-<6M{JN!yuMiS&f;>&LE)}(O`|Z_{MQA!>Pwt7TbDH$3uH5 z4b2b)c}3({Jm#W&Q;zRha+9Nk-fUE5i$Lub56WbTD)quhDmuv!p>qS{Sxa0~E$ z;-dM~8?d@>U6iGnav$g)LGN)ETRMGSurHw_fVzPIJ|yp5^eOoys_|Ap0L5_3Or9-D zm2OG`N07VTj4BHjNqMdDt%y5xQkmh&$?ih3t=ztB0Im;SwIq&wAe4(t*gM6A=Z?*5 z%2RiHpN=$we&%8Lu{4~q?1G0M87!9p{RjggLS-&U5;z*Tkeh($7Xe5ki87cCrhRR_ zLy5C1NzYCXkTt?dVUckwARHp`&(-sU|JjLqFF!P=-roHP=IH25${q0j^bxa$sEaYv&q7Hny||K00f=2thOIH&kZQ>2rs{VypO%y@Lg%U$z&9` zn||-R68^wlUob_WM{>huX>{+=<`lh^{NeU-n9ODj`CSy+)%Gv^QG2#9`J9Z5-wG`(a z>ao-8GRv=8o;BUKd)2FJ>z{H}62k|;xNYd=VRG9c^O7Uu7g zBZ-O7%yhiuZ=7=ZUBVC-34yu7?1_*h39p)4#GpUOCZbDsdF&N3FR?=KVX#G@#b%ZV z+7wT-FqXZ3LCSjlzQ zi-Ee4T;4^|>^(yP=k?E9)s%^^sKfeDupmb>7?m{NkF-X^PSmYhewkB+&>E>3kv)6O zT&cEwd+d`1DSwG#wV;7w`=~Pa%T@WkdjyZIdzi?bYu%Yl!BAS>LF-E75qI(ee#xTLUb5XXpfe2VDy0QzqhZeP4%&G51 zwK*Z-RzHl8gENtw%?(GUVy3R^J1JruILIo&bKAzUWGEh=osLq!s8wy-IsFRZ5ABdy zl>gC^;+^c;AdHGm)&F7OdTWnN=G9Q3E@#A#Vcd)RQQ|1C3~mpMCj-f)&FkR0m*SEV z$I{oTsuDy*M198}8^F`QN})4(=-3m}OPlRoAENca{iQdY81vE|h|T|g{5EMmEw)vk zOm~ef3Z*tYUR?SL?#%Q2tHokVhi-USzFMdTu}>2^dUy=qgyi{#h?4{OK`b7U_w&Mt z=f68&fYYhOr?`nCQp4vzvCJwz#o0vw4Y4`G1&{(m3T&}snR{ayECVj)-d_{27> zSIAqA7k9LGn2vW%b@Z%v^lG)%wEG4z3gqxF+KK+4w&5~ki~a7)v7@yY=o*t@jY>!{Mn{}kD{PQS>Yht)4FJei&MN%mhGsSam|8xwrN(#~Gq z-P06bHe+QOZ=N70P-wV1!iK|IU;&jH(%Z_{FMdWg7drSeh#*Z>UwB_j1-SM5gDNR5 zIeCjXE(54f$H(#eN1y`qB8v(>i&hbfV%6wev7u-3Ecszg{2n{ivNZYcC76>tp2P>7 z@7}#Ed3}Z%m5oF90f0oE?v8fcEoSy7pBn6H#q^9gEbmxByGJyHRfXWk5P}e_^Lp!@Rmq}Y z#EC~#eS2ISo~rZqp_@wPl-4#0$qv-J!nk)=ApgT}&icIz`xN!x9|2OhxTpS2_9{!{ z(91*R_3KooSMz5`H^z`cB@PNiJXjrroEoyrn2@9*^-agU#NO(Wp+YpIyv6Zj@wQHo zK?o}WG`pkeT6Hd7FKC0$>l58Z#&uF;O=y3EPpT^!p}=hy)@Mv(&?RbT2aw_A5~=^n z?+%%|bLZHiohTpd)`!`YoSiCoT>ODJ9!}Tvlh!xu?~o}up8N@7VnpzIQ*%D|G%1{> zjNCjtBiT}UV?TfX>U+uqS~cq%qd$Kb&16<8>D0e_s;;iy*xl(Amep*(Vm^}F>+N`s zpGi+cM>p~#`C(iNutxLg2=pjwX-Q8>N%8ESud#}nHDiut)F{tRNhuwh*}e;4#5ZhK z0C7Nz=on)iR7BS;KA~$cNcku|_>e${51FEl*NxFzmd*TA5;DRE6fx$ahqh*wow?0q zVH=2*PStyl2RzqriYT&&0@JLXu{0EH{w^e%gq05HpZvNhquI$Vs7d^xb)(k)T z8{Kym#0VhNjt#o+&;LBJ|wutW;cuC>XhZmgT?&Yc4Y7^6U>pwSjs)dZw{G8XlsC^76jg=}rVGkk z1_iOggZn`&Z4z-so0}{~^9RVp4i)l-b7U-hL|lIxrnLRym^_3wD<~^tk55c|EI662 zGzsQ0Tk61(7u&idB4FQl;QUb4T)8smLPvD`kV8>0swSk+v4U8WEbG>(?_EiJKQo&N>wKs|`xCxIJBu#?c= z!*l-r8k{^8-~V^U{=x)6@L4FSbGboDzmn4To9y`E(t-p z5d`Uw6b@Y;kP=V{0g*U#s7Qk#971VOKm`xoc>qCLLP_aP>HPKqoM&d9cjkM)|N5`R zde+QZ55paMU;9_rb=TkK68V_QhK1T(X<#C30Kuw(!Htt{eBDP6uE3Ue?<2I0kHs62 zhBLQvvs}X=(p3m6NALyu{DJ_R=AQLK=SAZ!Wjzx`DR`$a_)M0EX;ucu^mzXU-inxL^NpAdYEvoCrU?%{~}5h_0nUzI{bm@g%UNg8Lz{r zIzTOv$UMZzbmDQMC}7pbj{wX^$T1N`X9?0zTUCt4Q)qT`T;KR@BgQeq# zYBxFqVBA5YH3FO`*M&42wHDU>eQ+P`2VOy^rw_V-jAD9)exDvifUjvG+@z|iCIi=U zg!5G?V{UaVMY@Juqm-*EH+gJF6zYPyQMXSoz?&kDl&%Gl2sq7vRyO8sE~Unx^E!@; z{e>9fX)%Hfnk)*HXwQAG@PtE=9BHe&e~ za|@~I0Q@JQeV4%~X_f>fw0f zwYkjKRl=_##pd>SkK%tr!0La3fPt9k_>312Q=eZd^8nqHU*pBuCke5|(8$b`Vr4u} zwCq-BC2d5V<_JLvw@&^_2oVxX|^wgDetvxU!g8I4g%T?u_BsmrDt&L_llwmYsZ zuO(MaHV#kdxZrKWeWcs5m+`>Mc4Y?OVx=*V1<~u85wASz9#wAK>;LY7N@~U^&Vn>v z?&hEvR(4LV?h^scGaSY!e(ho>iRIoQ(~(1CbG`6q8zst^Wh@yk|2%&H&dfu;(3^UI zLQ`yiABqQHC8sgQY50l|`yyJ5S&PbkY9O=Zid}cT|6rop;hI`DGD8ma8ISF47uy|D zDLo>{TRgB8U?@Eopza04AcEBsy~0E)=wx2VMY6 zq^PS#VnBcS84>i;^w*0z8#Aw+Hioow`&dU^2{MhmWV?R!`jjUcREyuKSrWRFkl{Wx z2J1#lzW0Bs92$}ePGh{V#*}ajW#eIpD)+LCCAcf>*Nm3{pvw#Uzuy)6_3M5czwrEX z?x?CdQ?t60CV8uqs9yufb2^M#QTAp)nc1FLzB>5q2f+jiEN`R0vU8?S`5Z&Si6#5G zk@9{6zqMXnL#_{@8z$j>uube}#&2KCK>Fl!r|z=-;x zLMulN-N3+1*-xLG?rlHeKK}BS=Clmj=o@j(idiAaW;>aKW$+z4xKbFxa5$tm| zRL1Xws7p#`RH*s&hssD>Z|;;yIQ{%WAeM93*s2tfDHdQz*7`IKBKOdR`{)?%TE2C+ zs*bxA+bQj)(>UgA^xZHqcr@cqjj63I_k)KI%P(cVf3NNAT-2B^CrWQ>0+#0Xa$<1X zb-0d36V=(>^5bL?LYj=^YzA`EGcuUIF$V@Rn{#h%t!k;XJ)rGJgg|>pW)ET?N{WY7 zAMe}bwL!TMrW2EE1_s*|O^*R7f31dMUmqbr-m|q&ljq{oHF?SotHmyNG!4J8I{?dj zKLE3T5>~^w=Tgq-ma*p9>GxfNM4nL5`sCCcB}EVZ zEm~=Ee!1ip7-u}Xn(@;}SzOI$H>E+z6OhCMUw61Aj=?Tk zI5>cp8}|Coj2Wv`@hr1nHZF#`=UUU*j&T>KgS-!RiIQbXyb(kgUmZI;9c-8u8&+hL zI6M#oJsefUO5w`f*k(!`Y8YolOhZ~)u8FdFH;T{C1w951?@ACPGmsY%DHHHHeum$N z@Ez>_+_0?1eb1DlE#lt z`${};p;(B`_4#KEV0M27XT&pRis4F25{QaYEab4Wz|Jdff@k#+Zj#Dx#K}0mbz(c= zObUB&cYK|4F4;G};*(ujwa^u{ea!%06!kD3;k{&y1<-udfDSl(kf9;^V3!Ce@p7Wzz_qC+`<3SZKbMuiL z`^S$nPT^8h1$?6^?AvktiQyR9gW;=<=1-qG8m*VujmW5~sz$$hCEecMK4@4G#7v7l zmJJTo`^h`J^OL(t%%d$Y01~N9K%vKNW~xSF63P87K2f)s46AIUmIRJ`t_o2vqy;1@|Xv>YMY+597gy4!he}^D0gXBW4tD0uOF6zuR|FvP+8l4>-Bls4d z&aUVaw8v(wv^uKk_*h7CjJVnZWi^mi%0tls3_i05@x0XvgEM~y5%qx@tyj52mrH1M zcTAeZzLGWT)x+>Q57XYF&9BI>&ke9%dy+u&i{m8^!6!)_8ya&UvucBO1$9AEX(mj@ z_}!#DUBr_zTrJOWz+Evj`1wv^OLK|jW@_O*gGL3D)SVFlQbOV0LFG>Y_*R^m@7}o^ zbqlkwvqzelb*%F9OIF^?LLih57){JK*J<)n?_<%5x@ru`N5{q8@yaU?23)5Kmw8gu zb80l4*2m$EkigAv+^ij$`T4L#FH%Q^qN$4%Rk3BA& z>n2SeG2+k3I+7rV6xRTKwn34_fmqw2Voev9Vw<|c&Fh7LBRJ!Hcgk#HY}eW7f1SJZ z+MQKnoSOhkx1I5%IwYKS$6GyxkDjh%8+q~KbAyYo4@qtm*r>34^{%SW*stZBdpBi_ zGBNpO8R1Nz*hNVb6aCxkz2KL{T04xNM8 z$R{FUSFc4Fsf?r3SQrLo($~4gU;4MVB2CYMvqq~-Ep+&qv*q#&w2POl`Gb6@lTYeF z538vWxRnL>i-@Vjt-bUW2MlA=-7SaN$LiAm7Hl{=JSVGCN*_Iij8M7~=D=JxNyH@!=bs>kl65>5&65q|B_Z zw^u_!;brvqNL+LTDf z%y7BY?Rl6yMZym6#oj7P&C;l=EfjX*o)q8Kd{$&jGPYlu;tD;E*aa};B0hptvN-TE zdND9+EDp$D7F`I;{5&E5iu;uQ z6uHr5q#Cz+e6Qx`teSM=LAuY#nn#D&mO|(kgujJ96nAq(ZDn>WX^~4PQRTxQ#D(cpBwU&9Wh#P_RNPN7>4$vt;P3;pweG35sS$|(@p^GFAPsn|GQyg^9X5ul{;zv6?gQclaC@{8><2tXq1kIO= zWk|+AYW=(x_$4#HA}KAGB82WP3kTfW2O>VQ|6$bcH8SuO&fCKot|A(dt4?pLl8dYy zMLPT#S%V_P#GP5zXbm2cBRiOH7Ru1!AxK4KCD8x1hN2$p4Zw8`42qGqPM{PN-)kH@ zQhqkhcNycctS>X?YW-whfb>p?d^y_^5X6DG9A3GHEj~G-eApQVaxdOv+xRe^`2sw9 zx40qL zv3V4szkqgp+kHRm`G||vWy%|`Zap3}YniOUP_8E=#E!q>25ctnMJdxeH436t|X6_{mDHCFZZ_FQ{nbmxU{;A6p`&)@N8AE(!R3rMCR5MAX- z6zi_^xhf4ee{23RI(W*7w*HIOw)U!sJk)1N!6Da$>s zE(r3099ZieUMir(H=&@-Encd|o;P9k-x6LrYu)ao<_nnMi69AQ=YS{imKzJRTpA3J z29(DADDk<>)9!n^NA9pxx1B4W)$4AxlLC2T5-h5Jls8ZhLT|CUTPWRkM)wW`E<6C@ujM+42Y*H?_zVtpqo42OeS zH?Xj10?>dj`wW;-Gb!O;zMip@2V)bquSAehl`H@an+M&Y|0xU(uW*~}-e>gij0w3@ ztNq;00UzFVdi#w-!|)|#1&j_Jlyw*C~1dvtGp0=F|t2o7bNb{ZAq6eazRm!Yb0(sWaVp_fwMJ~R|bRQ*6RBb7!6;M=qQnPDLVel;4 z7_?84meBO|^>yyvoy9$Ovhrg!sepRTZpU>AeH(;WEb_dwJIKwH1RXwtfn-2+|NXNK z3QZnk_=Pln8k6?&xK}ToLjX zgr6nALmMi&@evbLWisAps5xza{PhE@B5=e>g7D3h%Xw0f1hub=E1w*v!=Xu;s3AYx!5o@lOH`Ty_;c0&f+{bqYe(di_fCn$%qwGfq zr%NzJ-O(v1UHrgNMD!f%y#~;6G%7hhZ~%9La|`r!48I*tF$2v(f7~V%rK81^6O|d# z!xnNaIJL_xs?T>OFLKJ-Obtwr5vaEv>l%lEbEuLXwz@$1BWw3p&9z zj9?;q7pie?1>we=SwZ|VjF-TlAiB*pF1QBR4$i7#2KLf-#B&xknmlfwlcU!tXn#AC z-}_La&~aAn++6!fo(^n54jvwsXSoi)r=Zh zj(S~WculWYgy9GLdl}xUA``Y)z~4A%22B2!*CK2MhXmkX%W#-%(gYGl&!V*S8ANo% z__%-3(NWh_0Q*I(uWpSRSbRgU&N*Pqxh;O+)*n)#^L{G+bD~p{KMsT?*umlbgavK1 zNMI`{vA%KKi%5Z4fY1zRV_o!tK-8qfK<`!p%Ubk+$3PGn#l{}`T2(p*8kHzCaaQDC zDAeXi-5~hH(RK-~4E$|p5Cbad^h>+Vg~>pg)M?@|ORw8kbm_A3ja(__#%2sqP&=5{ z4ErPps}Hg?{0KtJ;V4VJ3wjwBfNAy{jNL?pkhF{&_!w!gKt&+EH6qtS2w^e3pQVEy z78WMpzRAWtbF`zL#;*PD5}D7r;4T3C2_U&&&!R*rP@n*%g!}6HnC6CG&_hQ%I3-~f z|4qW=)|JB!hO(XdMtw1!_L$FiT)&HXdVTc!xi|dYXnTD7KEB@JpIXWf&S1cg{)Z#y zr36$~_Ia^_nb|X!kM}$d?&|33%7ana-N#-Jz0tP9!j@kt>gqDD|u*Lcw^cbqywM_j7clscH!4OJ#1`%sln zD|Rb_ih5^%1Tm#&pykntnbvTnX2#h5r+wvRo{ay2OzH zw?U}}i2@D|4$$^=hICOSXzJOceu87mU*zT^N}T>;cC+lCmW@s8>loM|<#wM^s^Hv-+AYXFCd zM-k$0q0!I@S|iR6l4RC=EBeIsQ_h4)t+gfe_7_lGIqc-;(xkxT51yF8iw0q(3oSCT zPD)X6aal^#Q+>`fO+fPXsjO@nGSP<(qIVJPLT%RExIQ z9oBFO>s~fh9=A7N{epvMyY_Q9xM$+{%(GKcy0|5qe{SvBF?m-8WquO8XDF5cwR8J$ ze`}$y&{8v-oRJiP{N+ZWoBUnUcmUDG>8o=l9-;o1-VB8o(@wSx9~I%52~wbS8;RD` zFelj^>Dp&bf!mu5kA{j4y1aZS`75$Z%-5sr#SJ%F3-$iJ|<)B>c zg17$Y!Gq48jp?t2BSO428#x0N(YvE*L-_$VeQrZVQ+bcmyVk|MJ1GaoG@tJCIaP)W zA)E$`YWk+)Yp7i9QpDhLE?f>Y#_!#K&ETVV$Sf;z{DHXW2{pmmwqBRz%AU40Y)`J_ z3lBj$xDrA}$3pG|F?T3T&B*ok4Xe}GawB^Xhemj9-`nOFnNO28f0FhU_*J8@xL?20Qsb%TQjt!-^iSCrM&-!Cj!hdc;!k_L$`dic_i z7+HMt&horVXWj90MTbs5+_l@e!3KFVZLnA_1klsq;JQ!SVKyYyAX{)yD zC8v4@G5a+CbhzPT)>692{%Xw+O1#Y9^atts?(eI6A|(nCu@DgEx$QgqMmUw7dPfm@+47=n zary|9ehn%*8qQ@}_nXtx(`8NTx$i-RCRx>*2$-~Zh!4)OMRp*#Qc=ILoItJ*K1!Nk z<+Kr|l^j;iM{0Q;Rd}yTJcHtXH@^G_N&0*h?ZdGT3MKJ^%3?5@^dVjZ-~Jq*Db*7~ z2qDVG0wKJpkTFYR#?#rRABd<_fsDda{j+wLQ4BMdx*0@S*Uturk46! zeGMXqj@AsY@KZxbCV6lE$xY?sJ@pZ@6)D$flGsJO@WM?(>%-xM^3R_i!!?}MybGvV1pp4!(agch#m z=Np3!-CD#@9MATE6#vdUtmFAEe5bxEy4S9su#Z$rZPXnrVu~ic#E#0!VH6B8fX5&A zH_);9tjC^mY5PqB8E=&`qr!}`2^N|xD?JB1w4RBTHFl=cgub#=^YUK#82Iu@U``H4 zQCw0*LZ}uSA77+iF4qo6aVXzPjSsPEJbxdzq7mgfU9M3@cvMuBXs?F8u99O&NTJk@ z^Lr9x)RRR%&V5=VqU0@ctHPMT`QYM zU|T7@W~peoS{-8bBR>;t!k;Y8p)dhNH28KnEGQKOwhEoBt^y=jX1(EJXnYouNDTv@ z;7N_YiDzh2usK1i+C({ujM5BW@Az=|41L0L_r6=vW2< zw=af(cd)1;(a{1>3lJ*XpwW>uLZ`VO#%>q6m&8o2;`&j(HWWJ17WPyr?*)wLYNp!h z0af+mlg6Zk(xIP&S_z~=)BFB)ZZ7sz5|iXLcLHqy$pyR*t9plrmq_TpzH4nADC8SO z5~~ZW+}R)x%!Dc{=dd8?;UBkh8K?n+dPHa`=509CCvI9RZPh$wu!S|#F-Mv61Q$gp z0q*f=poa(Q=`5$iYR1@mr=!$#;$lhp$~M2;teAriS*23Y^GUwMZQi1w3c1}uf8#;7 znyB;s<65(Qqi+A1qp!ZclQ&WrfjGEUMfTU!L24Ab_{k($1w7hRs0y2opjmx|7N)hb zQvpNA98Z*j#7aWMVCBhII<#1XcGOzi+vlI?(1xUf4=93fn0_@J*JpcxPpyo(hdJmv z8q?d`duMOY-#8+BGRV_JTUVD1)ZT^Q*O8G*Zf=uof`WZy4JHzAPwM@d?V^GoP&R-0 z`ZXOuB+IQFUEqVsVPg8zKG`*Iswfb?C3U=jjigLQ2{67SIG=TtYuNpFC7VijlQn>g z)a5&M`+BT(A6;C4Atk!6G%9GUH8gb=2X54Rv)9HWYW+$iG8%Hmw2be|{Kip0v9L5j zN0ln6++Apv@2TiT-J^*OZ>A5-G|dWX2j|-43r%{$7_ev3@|WL*|#kYVZpfL zn=JQmxU>d`h6J-2_ty}PhnrzxWQjfZX__r1oWAsykCL0WfDtZpTOIFGUN^{{vEw`p zxMW4#Lo)HD{f{0I*9ynON@U^SY;hew!x6pPJcErrSYakTH}gvAZc`|gShNO52;E6ZVY+=Z*)#t?;HC`A&*fM(i!aQ9nXm3Wzd%pOheC1PiXc2VxK$C})4Fvq zT&t+e6aOQ=9tZZH6a4ic1=a_4W{uNCp*@8G$&NkFePXKKvp6sb6{jQN3N%VdTjt|A zaFRai{snGDC_d{$8V0|#`N)e;kya> zk~G;Eh-Zm8Di8av?chz?2eB-2j9ye={ws~yytuvn)1VX2`Y#{YZB2PIRoze;-^uYP zYo!0d){4xpm4-x7hL20aO-n>UofJm)&HUW)HvtkV{u>f{Sw!Et+*tV?Och9Lm>W%Ar{qo7vJe=k<%^oe)}DwVN$id z&eVhO1|yY2P1JA_@Qf$_<{97T{W3)GiaxeJoEDXoLjCwYz=6v1tQ&cN9GT_YO9W~} z)fIVZW<@T|^iseTmn|3S3RX2Jew@w8-Mzni+_0q^lk4`HMYY(>>PyQhXSfAwWnzi$ zRFBOnO-4@nubr-7Ns_0zsfANXz-{fggwdwlG&V-8UUHmMa)qNn5g3je`xt-45%+ns zfT=WbK!6g}iu-|4t5fX69NviMf%W<9IsrO}@FknlI**!PQG5KD%XY9(gr1F@t>gg# zEbJa4%@BGA^YZbL@Ah_HR(5uLx$~j|I1AFgWrR@+uLYQcwr;MJ%F0S#L>QObTCR}? zdPm&_)b2e1?wa$IEhg{+-xLV!lts*g!ZuEUu63I-#`aP|*}b3;_wC5KQhSIQ3}_;C ze~a!hVFx%tu zjHtb*%ZQqTD@BE+Cv+^;g%wY>4HLv4BxJ-8UOM;wq-%LF7}#T-{Jt&s*r75x0uS4u z9BgcxAuG8R-VhM#$mn#)2SJF-Ov6;v)K;5wns&peQ-$oyd#v7ps!3(*hIic2a7m^K zB^kh;>j2PU6B8RMqTfj#yC^=31HH|I8HH!Wvm!_0nD@?v#W=iJB=7>ApAd>w^v)O5 z>vI1`XukYED`iGzE!E+TlgzFI#CY%c%va&13^IR^0%)Ri##9s!90f!HNe-STm)%;m z+X;eDuzBqxwH`mF8!U5V718Hx zEkcN2_PyIY5@+aU@VY)|QHJfjFXgPb#DnY%1btb(y-$zBrNxhNJaMtUisU1ARw<%6;{!x?A zW=R5H81^*wpj1FqOnrD*1*NH6X{VVXE9KGH9ld@eEDm6|OfcpZDbHB#hYwj$@!f)K z`9?>9?#AY>>DiOd&s)b5A(aY_j;|Mm%cG-pKtd5`p|Lxn{@dbZrxTq)RUxf|AeRIo zAGCIEo}T{8Oz^gu#l=x}#(Og{K(onu7(UR|m9i{JvK(^lyCYBD;-}=GARNhqw$(Rt zgQM27QXjIjyZ76Vz$bdQ?g=5Ix@_)$z0F#)7j96EeJDF`RNlVOdFoyeZbh{O+`SRlY#=P)?a_+Zg7S9 z;!%Vy^6J_tu{XWE{Wol8tn?e1O`q6ye)*!26wrow9d6Mh$3^%Ar6UFi3Ot+U0@=-N zsVUH+C|U5+NX1h{7I>n!ns60e2QKIYnA3g1^daXAS4XhokR7B*KPT+RW(}JyX zbG_)fF|rxibY$`sKgjhy)06OyZcHb?HC!W}c{z0zS~;T%C*mZMCFyE=ltt^b(57#e zt>e1WS*iTlbV8O3Q+7E(1hDGQTJtl#qXI6wd&yxxal$~UvphU(e+k8uhc#Xp@*-FbndzA>~tNp1?Ih zw`QM{yYl&)+{w7Nj12mxdpjIgry;<=3aEri&sD&7=PILnNrRV3AXl!46*6w_jvKN3 zn&o}cF31UY>TX1Hy<$7t_JFT9V;PJ}oyfOz+naXwhTU;vvHxh>9A47}0IVmjkQa<> zRZrAuE#(%R&Rz^I{%d*?!~9^Ito80}Cn8KNy%csRAb87a$6~#K26ZUlOLW8Du48%n z=$L6SFte~|YHO!|U_FfI)wf(?&l7W+YagV#S5m#4(7CFp(za(F*9p~T?RsHD&} zlQ~uEtG@^~)m&%n-hS;_>Z+67+?-&hbzjMQpJu`uad^N3`n{YiYQ7^xv9su(tk860 zPKW{5r|bE(QqJ4*5APdY1f6u1slm*nVZ~E1>d*EiLm1a&787SdA4{2dSU0e+n#m(? z*KPyI_jpVOLQyLgcgrp-0~o-CH3woWojv~aBEKOkS~kX6GsTi?MCcqrTNU#pJKSk} zQk7lPYI%3K-YS$NgiqJ5G=M!oG^ zYCaE=fS}r2XYVO;P=XCVB})XU#ItRm=>(7=9OjDIimUh&*l_!^iF22YT-PSbx#GN{goox>H7#$=3~d`X_kkFx(8Hw>o#M)2o9;BJ8#}jtXmeei_SL)wp7>SF zFkS>w?B3=| z#jTMD)|=u`N7u~B$qiaz;zL?U(E4lyI;mUhU9R^vCr5Y#T|p$QFP)luI=vaHi|=?p zY}$;d>1|sJ3_aR@n_xc7pk){sk|gd7SJqoyURx1U;^1QdEwaYZ8pxhusnyhrCU5x@ zeV06nRagm`Nq|TyNG0|cTaXm<55(mwjl@~wjT-hIYDZSYhDAtaBPZp2OjkZht0R!+ z;t`dTQs4Xo9Gb&<5u}!nLXrYBZ@I37T-uvS!a0Sg!^B|B6pUbi}Cy`~ny^s5>*O}pmh~Cv#8>EmAck|xK zhbUj%u7jo{Owx)J4?~0lG3@sHc<1tbMkW*Xjt^G9IS&|7$T6u90bOk>F^V^PDtdA? zR9BWO$TM^I8c8s8!hG$xbi!O4N16k)i1wHQin>;&`nUWE>6z(zx=e_e2|wygWIPCA z_8JIw7{Cr8PnRBd_6lN7Hze|>%7NzO{)2W?1kKXS3_)QiRm&HW0 zj3M7V=z&(_f-V&=`IFIW>fqscrL%2{=kWBx7`g`2R^!QbN2@cmhRKYA zRtx6`GhYv~_`3;$w{;xs zJ)A*-o-`Y&^pzY!^Xnb8eG1T!QiN{VzI*qsYV&94)hV~tFft_Z*U&P@5W|Y7UA6aZ zZK)FThL3({0iqM!B^tMvAu)M13SVP+zqwQAKsUtkH7HaSf{5>M`A6kt83GjJ8r#>C z3CRY$vgt)c5kyI~+m)GxNz21SO!cj>@j=^Aq;pd`X0p3G4lt1Wn)0(io3p?tg3%QX z{?QSpegBuB3}xKtKy^;R47yojO5FpQZ<_GYHoxS63xk2;9@*KMBlZj^HleOUEoa2* z0RMhD%-i)55KJdgorHf1PXFchW$1Ol?NhIsP5IE>Fr{~1?Uv2DX0JHDE@y<%_Eujy zOx456OIH`Go1Ot4x+8$Yym>?)nZ5>%vUv1B6ToG&)aYcBJK5RUc3&}wC90opFh2^d z4_l4gsI>nw=AJskDa+L!$7eK@iLB^kQIA)4b91x9M*mbN2u?a)K_MZMv9Or9=&W{o z>&HF&wc>hksa0Y%_challum-^*m&4iYjcFTlC2^=-1{eOm{!n-04VOVjoM2nN+))= zXi-GLqr4W>w3c^a<#3e7ZIB;l8~i_-)aJ(?-K>TkL1HvwG-y0eGCf=E{C*rF&aks*m zz0izG*VjE3`>Pg;bEWu5Pv2WClz)ZkSHC(>#<@+9Ab#*JuT$}7Uroix=0M1L;*AO$ zr9zi)O8TZCTvW~?&2^h&edrSEPI!pTf+CP_4u+e7QZ8kw(yxwcc`modh6Vwi_5LVR zs^DQ~YMMwv%(4Z#QXW zn)vqiFa=ze_70v2y8ybx&CU63<*1uZ-z;cgqJbKExOU_i5DW9@P|6lONr(VnQGP6o2ayZq^C5Ea2Z z3Ibmm-Xnnw5`<7uXP3H$FJ8NLd~95jK`VuZba(dEdu0$-25EB+EDTvVJS zRUH8%oerybGeb^cb;1MT+WX@%fP+I%b12*N%#`)h5c4(A6fKhyWthBJ zG(GC@o?LOW+il-~H`+6-XxIE28w_qj_Tg@Y3;xzT3FN%g+3pa2P4AZtsNB|#y(g|% zu<|j8{&tNfE&7ZUZCYd1amNQhPE3RJX;^XxUO6(PqC;@bNt*^5t zATO^V>bB1%pRO}eLT%P^Fd&SKDyZ3U1M(ERr^LUmliCXA-GHHe607VCn%bRbwXrWY zVhKY0rGJGsPsyj1QBtIpSUf+Bjq$&Xd;;h_hMM2-ee zQCMC^YSGJdue%k7fPrbD2hW=GTQ?tS}-w_ zg_k|* zp`QR+zneW8QVVI)9*7zOmWTPbNdBLJz+;eHl)6#Et--Vs{L~+0BxGBQ+DPVA%x$GfgD>n6ac#hs^F)8T31aN+e^oRgM)*HWT;LE5L&oD8T?8V zTaEw?^kod#0PykgQzbEA%OieJ+_2=Bhq9mrocgb3>~-8f%If()_ywYjuAyhA#wNX9 z0T`>0*<}FQ$Ec(@Q7W5>(BDJ~(%;#qKQuO?(-0IUDlK%+0mW0w|9|=Q&Xx0&lseJ> zA|+*k|66kUeSLbT(}zg@>#{ll#~Yvp(U?Cs8vGeNqHgS`=^sgaGBCT$zo#cauF?!_ zA8wQ_I28P$4;3S&^Sa;I`#4g2FohF^hPa=~N6mouA3r&5FYwo{n>5r&sXzZb!IqI{wa%7-hx-I_;13kYmfS~MhgL>%UDz)_*z%a}Ly$VcuBmcU(A5Kfj zd2BB|oj!4$e=R`o_R4ng_5}FII@lxQDb_V{ptXGQDnF37HDtk#y7uU=5f1s=f4ppZ z(|e~ua9ckH2Swt~V5fu8OgrTugB%7vk!ysvL3_`0fc)Ik0A?1dHvc6N_U9+@nHz+o zYKM!ZBTE_2;uuOJiQVi?o!9K#5Rp&*)+ z!3FoTYW!`_A^)HE94d28%_zTs&t!U9KAm}~QTb3P4v3NN15v4dfbp-59fX+Yjw{M^ zH3tD8teJ4MP#J0ZP}Kh$djK+bzSB;d%0ocaz%?=dmn{~K(l6rl(q6Hl0?x(rsO4-f z2v*YK>95-VfBDq|!nxmDi}L?*YpJ0+;|s&nk}ScZ%P1Z?-Cg&K9(;vcTWy{-d%V&{ zh0_KHsgzYzG5~oD4l3iCisMIr{ON}bMHgnPV-7GJzM7e_-E_>F=XYHh@7J@c;!C{1 zQi>WOup{#~-~6xZ<#%#d8A4|^L-T!68HE&>$%}xV>6fJ1{ZmUztH2Dalo&zB zhiD*CxW^UA9Gx?wf*RgcyfkjW1Cr8(b|W8@yMFOxV?Ao?>NluxNo4Uw*+EK5K{7>H z#1MBQz7aFa(Pw)C0*-M}%Xd~G`ekbg!YEBR3^ZCTH_zhfX&}JSHhEGiW8%q5nlIgpf92hv>1}Gh7YvfFWcPsnpdhk+3B((UNZ#o z;ovkG2wx4EKFE8z`sCoTg&I%TwXJ*0aaEBbB88^M>8WP*hZn#ro2p{?=c0J3i_zTq zc^*&wkk9AllLGp5xwY8=_0@a)M?tG{qP}IT8GAu*K{}nKYjG$z{5& z*;Tvr)*NLp$twdGvvrd`w~pP^)OgyHYow{ohjq!GI54QPtlP19qa4Ehrlw$(hB!iS zuJ~W*H0Su|CHREtba#Mpd^Gyh!RTl{7_RSoKGp|VwJ)4Jq1F@j;8{1(a|7kk867n( zqdR6Zy-TinanfaZkWri&is=3R{SJYou+flagSvtO3pez0ZJrMQ%H#5rVnEaFL6?=muW!&)ZV$G(k@s-dP=5UPtX zV)S*==MD^n|DGCSTbw7Bs1UXv1|Rxa_dL7u5HpW+uiRW|Y^zZa;Chf`cgRfWq^e4oo2(2Wmf@WA~$ z3+tiEnd;^)3E61BAR~}uQFHWi(^u;?1Nh#>C*B=>%FdBADaz9k`x8g;N|W>A;@B34 zcO3Irs*c9bdtcm0Q1W`edu>GZ5Ic<^J&TnVjv2eSJ?Im3EoI=q7JdexDdS)u!=IpO zB*yQdXJdotv{*BH4VOAi<5o|E1=k!;?#C?bu~{=QGQDgxPg5GXw?7c za{D7om@iLYQ|Q$XEx1>kkyjh>LtcSNBW~@n5A;$L$21!!55t7Q@#ML#!mgkwcVFL| ziy$igKDeH!~Cn28i@X%Cri(P{n*eXo7OQOP~;N# z>u`GbTP`^CTsF% zfucQ((5>R&J?L{>wk*lde`K|{@nCRR8r>3%NGzKCkivvvDh*4>X!MA81s#iw)ZEP+g^iV;6b}{)61N8Eavk zg-c_3N-V>XFT|ogx+@ap+tVKeSw@$pStTYLCnS` zq0GER_^u8Y83l!=u5RWnXV8v?`qEv=l13vjUP{FZmHekqp9V$CEkV7-W!2nnazG#u zo4wj0Ui~R}0+Tc=0g*B5H@$Y?d7bSumc=^{ClwD|Q2_RW%Rke;Gf00r&zS**LGD=y z9vRb)rfzJ03#nR(Yw5U_8x#!&6dIJQ=th3d<>N<2Odu{BVc&o?U4 zSdnLDez0pXXB(g|{gaX&HL``6G;M^`Z4+ zai6zuzi5M^YHeL?Evvuq`TMWp$yV0@(ul9TAQc8s%#6d8w*#mlGu+v9*D2?!*^ksX zlfLwPt~_+BQCtt|eZf};=nrmE;;agIz<Sr%$0mLY&=;9q zws$xpo%FP&RC@-TJMU8C8w~Hep=k8F-E|XnF!ywThk6p9+W0kW72_c8hhO^Y(u7#h z?z+W&79cD<8J8Kb`yj2=%(mk?5@#3Njhep4T-CytSSE!)YT1m}YjMAC3WM(df6`>m;659Q1CxuOq?NP}h&eVRsPf{6oI-YmGE|Gxmb=qi2Z}H$KcbJ& zjkd&rdxf8Siy9P9JeR(U15Gf0O^rIdpx}Oh$<>*%;@K+Y!?GVEYVImUwTE!1~EB<*T##Ite%KS3)r6lQ9|LaPE7`kXX3Yb?Dp_1-Vvk z@`!PhI5g0R{F^F?Mib*+TOe~dUx_QepnyQB4Cn>eaz9LRSgw1n4Z_@lKM$Uxr2vUH5f=rnYsTwAI8~u{XKK%h0oGT;tPX!Bojb z?RdP_cvr4`=iQ!mpWt7_p4`2Eq=Y-6ihIxJxGuK4?_?exN$T8y%+OHX=2L-uo8tcq z_ovgk!VkBV%**$gv`h_|=fJsFUj-CxG%}tjaG#PLHBq8A`ZgC+&D@pdqYXtvrsWz& zrN^$>637RvuX%Px)IG`&uqBpT-)j#h+D-30pjOS@)JXhn-x?=3!9Togn-brK;dg$O z95ojnmYJ*_?q4&x!0+_X|M#5=H_w$%&=mcZ0deT??;|?Ol4hmK7C5xU(nh++*wdr1b`XvaVC$;vL-t8b z>QB^)s^^~;@Gj=&o+K``rbn&T;^O(S5 z49(gl==e`}dD^q^zQyD2MZ~dKm&>KYXq0tV+bcGhl(;zcXymYY*<_p*#YkXqW3HlC z5H?y_Hz$u5!dZVtb1#Im)#xo-JzO>aH0472CNMa@ZGpI|{K8`*qmid_C#j+M~=K;mmFGK z?Fdg;(R$)W)PDQuu2*DC2u z(P7-p?gu7`{ry-iCT<&@dfv)jkOg*QHM5E2bB!ETRj_ z=6-YRF5yh`wGVOM2GDxD`YZ=~4(JI?{eKDDdi)V;w07glGk$d<-U z;D3F)Z6X4Lkc>1lFR)TenVevQaTzEry?<6Wjh)BTm~Gb7)n&V$Shf6A6p`7u27eq- zk%*>G02q5=m!EsF<)w=m6kIV=ZUiXK&xgxOdyqewfd$@`&nfRh7*{)T)jH!)}kmhvBFXuB|<3*rY7Ufnb`yR{rtv^HhSOLXa-zcq@fKat(> z@(>ZBJ!&4|K88!y!&vhS*<2~iv??5B!UFbi6YKCA3-@pp@Vg3hH`IsrLEf(%G zo)WUNp&xFH)`zO8@{s&TXHgWhVWl-I!Qs=_Dr}vaToh&QE4DbckXj=HYU18PZ?-sA zAbdXdi4O0P7_Dy#;+KIV*tA+|dR4cc`}lO6vm_`WZWmk5oHA*qTZ54D~Ikd=4nCL)zB)-jDSeZ z!zW!g24Nra>j@qPQ=q!Ju`nW&d3gW1E6t1ct4C#IPob+UYl0p-JtnG-^gnei$|^+E z=pk!A^;j5(a^2-?^eVg>@Yl(>g~#eidq{QJnJ{PtZq6ouo;SBq`c5Nk zq|UP=y6p7h8og|f&t+M8Vv&3N7Td_Yh~UBoJ*bYlKWTd;-A>}CGskSwH_%(ce2A7w z7oZd|A`$o?Y#~9puz(4#xDsL4C)CZu3P*vL_w0srDr5#>eJfEJGf!cX&TKLTivzNF zBms^g1ys~)VVWdYZemC54nWj|i#(^gitIc5^(mU6s`kW2cMAQbF|xN}>?rAQ6n(tc z?o8yp$G7ZTV$MWRuN^406s3gs6}uZ-lQVE;ftDHVIp&PZ!2M4+z_>GxY~(pEY!dV8 zQ0E~KM|K!k^3~`?yi0GCGqhxb8HUGQSdW&JePDnnV?aMkxn|~a1`eeH9+Kj#E9DNI za)u$!&5z$Q&KQ=RynoreqW6_%WX5NV z^9|tA=(!Q=b`Y-m!z54KS&!>lxZ9Q)RfmSE{847*9`-4(Q}!Kdbw#Rtf?T1_l4k(0 zT;TDo`yfjMy~}LJv_WWhy^wi<724BPX|W-9mVYZNnxG=clk8+>u1uY=AwSIr;ddAO%uvjcKAWUTW!-NgVvArrY3vs#82 zN)|Unt&AWmcTvGw6we?J8x$c^KU&bdLj#x%iCR)BD~tLa98laeT8aruydTb8vL+{XAdW@^wtlPriiWP_D zlMme@68ZI*ZyVI6y3#_r^Rg^%ynTAqzTvT8sIRq7p<2kk&Y{O>qZ1~RX{j&SwW0t| zzusct4S4$M9n}Tj$+E`UoeB^MAWVW=P$;Dl{)Yt@-z*SR5=Z!>3$`%9(duBUOlHs! z!BVu(jnZqkclatq$bGE&vba&)6K%!0H=6j9REH0K>yqwdE8s=Cx=y}9qGxB!;cMme zM)8g-vcv5dsCjYBHlDR(@G2`c9mXj``v7+fsSBk1&G)f;n+W4DgjsR2X8xY;2uy~v zWSUTuoA>re--0O_cQJmB$jo#Kn79n@!c4H9Gd1PHs+CU&oAhnnYJmYVD8BM@TuY@(hd{1L|`FWtq zV^NE95n1xNR}fgi)B0AJ!nzV z-m@21Dx}evo?>^qw#v;M`-+Z=CK@Yebeu8tTac5oB`8t-y1k0TrP22vrc7$(!28Aq zCV^)rjVcpFO(qSSX>623K;0{!?kU#<(A&WN2CDBp;EL)iahif-HGU~{>&yfu1m~V9 z<%k@49kp#~E-Y_y8D@vSgB|#=Y(u%npQ~do)O*T?hG#B}_cum}Gbr52;^91BRKbJo zEZGHkv2w^kMAgy*#LPSV{6N><1iE$_{AI0jSDr=VPrzH-#~%0NV`hho?DKv(7Fi#{ z>5_Gj>(q>|y>Xw%aA|7ugUO-#NA> z9g_cH~)mVxi?DPv0tJkUV`F2pKnVr*+;HviA_6`W!KcD(GHY3FY(Rlm0ilq@yT0Y zx=6Mg#uomDbjk`8aR&>?^HRCBl%#&KzSV@vVideLxCb|CSk&6bp34^19jvc{!#Zx=0sFmcRMdkv5 zxBIbPzQN9-A=JpjiI*@Hg`Cwg@cRIOBpy|O&@jXWLc{;$l=SLQ*`ce=&C#+haIEiR z--iAU@w}?`Q@ed<%Sd7oJdFKa zi#Qn%xSa(f6Od&Mf4DhZr6gfdUZnd`mm^Gjm*Vmzf6l8sfBygX_!_RCOionwQOvbC z@Wu)KzBNxq8Yjjg=CP^Y&4%*ErZ<(dyWp6rwOuMV;)FJ|ra{Zj=9m`BNFaug`}A{V zCDFC~V=r1_@rA~fD<<2arJ~yrWp0M_EXznuooOgdCqYiVxJH6Se}Ub;vc}! z8C4sA>+N!m0nq5MKqJNTlW{#zZ9X8+m@aGw!S3COwfx$hu%^iPaFDUYQHBG&M#(*T7iPp7P4A$njq$891^yu*yV8t3Ty~7Kw>h{|MIg(mbsVPSbLbJhm5igbf z8LJF_rzmMV?Y3OCkg&_{q-J}jzsUfS6hHB}-^Hc5p8oEBWLmMK%9-7$-`1-Wfv&p& z%4IbH`Db@tyj?xcaSchkwhl>_Y-xMBOnByJcUWPth-jt_-xyK|MU>(Xu%Q?EkqP}>R#nt7{}o%I<~ zJd;Vtt$Wv28pZQYQw$o9d-T<&3)Seo*H5tN90>DTVLVfzJ@Z&-Db?E6z2)qV;A87l zAi^Rg7ZZ>~m}WivC9sWmH-yvq5#Yx{!&89;;IGh?-IJ`DaNzYBd3an8qJyy$D0WnZ zn3vkl#FW6s)UFnTxA}oOsJGYj1xQM%Tc@0TT`&bUwj^#6eDC78C&-6OIN!Nhr&u%@ zXJ5)Piq~iGrT_kAa%0xPPy4gz!Ah2%gH>C@`N-1#R*B}ZlN9h*fzWwDR%-W-?eOj1 zm&7Mlwz_vO($0=2A?<5Ea+J^3gcHIXOK%RwB>0TX6*-bx)#cpBTCZ^I6G(V$Z|0C^ zcwb~Xcxv>4<35PVNo$Nru*+1W#6%e>G2W}fc7S~|D{uTIP>Ku8n8H<-fbUcXGX^5Y z7FiiEmR_;w>K>3))}z@yzlfXl8#Zjb0G!jQ!1ncyW%g+dIvy_V=sm+((x5l;ermgb%n4#2Y>~FS&n$}9W|b^{vo?scN=Q} zsx!bfzHh&ya5mUVp#gW`L!RA`D+9kz?q)H}pTh+c2)-UB2@)!EC-B5D@WcS?qd#)~ zpF;wIh(%3sx35vt4`6bIU;Ia@`0XKqeXL~kDF?_gjQBTf92F5ZnH)CR_G6Orbx7>* zB?0=v{1}uO`-ygJ==Lx(9}nLTH5!wSw_{jnc)iZyKx!tA#kYXsC$q zfh_6ko3b6#0*S!&O_>URc@|^@EP=1=QKRSI<#1Tla&cW?i2Nup{&H3FdZ8j{cpB|g zxi>Qu*drnafH6=FaA%kG>qcuR_=|Dq{kJ<<7dtFiwXxl-B#!0a6c&^Qdj22o7!}D! z&^>)yvImQ0dk(^ipLyrr0*5vL>D7fk`wph>1R#Q+6Ib==DJs4l%U+k7)=OAl9iQYC zxHk10sU5#Mg_oK1pDt)>7IPk3C=0UUjOSe)+a)9+*FPU0Jj8Mw45emgAh@kc^SG*|ADE&QeuZynUL_({kFGFDDv7Vq6aQE@ z>^pQmB;DDmiC<&jRd8Fhd|HCA+k`9!fspJ2-)Y}5tbBiN435`%@`> zThQMw?i~Y;I8Yf39C0r2yR!O>gvHBWFZUH#YorW;^K0N_-9HkbZ!7WJ#g5AW-$#5p zxrI+YWx0$q)7Xx!DTY8NkjNf+Hfon;OXshrNgeu*4EF~Q6dSy)I{jt*}g!Wad^Kun*4{LQQ-3?%o4 zPe=eaSMO$gepB`I{jZbuZ5l0KZ7j6K4m0}$ z1f2QbYlGFs#na=`<(&V(c4KzFj=ZuU;FfMD)csn;rOgsQok$ za(j9zQQq`0#>z45V1;A$(lDqXasFnJf7)vLCj&p($a*$njY4hDHZ4>yi|Avmb~Cnn za^|A)CrmPJqOy#EUnh-P!3My0Bk%EM^J;jmQxV{zd4ITYv1d`k#ZTX?Y?5a};=VvR z*>YSIWW^Gn7ZO)aE78X(9-ws7UeE8MsUe|Q-ZpGHQ97a0e4v%vr;IBI%T=+$1{0AH z{@D;uZCs0_!0>~Wc9(Jdxp*0iBFeMSlALRv@bZz#wYHKR-W*!f;6?EJdEzZqJ6Y1^A->>4Q%p+#17ILAUt+YNKSthEc`%IzgNQWXEo)C=*{T7J?hX|V~h`cjve*5xm6QRKYx&Q6TDWm}~B2?aa(ZTqcGu%yY=jLkD&PnRw(ptymGvIYnlBD-BV=29%6DJ zI=QD~xAecE>(|ZB_|eBj){GPopJsJb`e|#}aCk>a4%~0oY@K&f>;{>oE_Zy{aH`Ps zj5cphnefV|<2q^7Dv}bA`oxLbn|AVL5U$s>U*f!PY5(Tl1^0g-e;pOaE(+VqtvoOC z7&>%cpnc9JhcyBItO;mkTz{|P#WME1?Y}trPj9~i%oW;EU;ltZ)0-oc`x=-xl)!=gwqlSU}bjTUC~89u({S1eag%5a_RK zIXh4`!HZv3OPN|@64v5|7E@(I&(viPR(O*Sb{}Lerm0*374-l`EDcZZ%&RX}=)3aG zP2XK;J*Xz2DTUytV%ZFsI8>jze00kEjKtM`(^6` ztmM@teOoAJmW6bV_w*gC3Zqa3##;TgA2YLZHAxAmSuKleI^RA28~g8$Dyc>=>kuy# z$hbn+m50X@qGX)(^d@(~kBA zvnOZ1sqXJC7*VPVH?iZZEwaOLt@2aZ1sFbtrKn70aA+=?)`XDRA`4V=SrCI}H`EC>Z#-#0Q?{VtakHnMx?8@ir z`So{1e%U7a%gDdRb@E1FPdUk2h|v=+;rG1ta&*of|L6ynqVnyyZat>OkpQ3}V5B(r z)+F*?;`+|r4}=aDKO?_-sT+~l1>zm1`o(lkxPpvSjh>c-!s5FNX<8EEVQ@$oZy{zR zK=9v9i{Acb2XPRW&vT?U)tvRtqaSFYvn5e&XHF`g1O+PknVlC%}wNOA4TRPm!*`ZYQ9vzD2TsT-56bmyDX0+%zr!5X8|Q3l_uNlxp2R zotyavvuj}i%u%ao9N3Z7s9Tf&cOXmeJ^SJIrgrmTdY4sM#f4o;9;tPqbxVwk`KR}Z zBtQ9g=W(&>PRqa8AN;9jSP!%`0D3p&6nEdB8%{p~1D`7p>koGQKK74X^B-2Ks{wu! z6G{GW=JEf0D1qC!mIDw2(X(aLDi2Du-HYBryYBWZA?D|~=Lw~PNS_A!D!pC2y^x@U zku+V)SRA8mO}jBAbnt8`)T!Af(YNvP#G<~=EG(LMUOAsBQWt=pY3)A{url8SCEGAt zP*oADpQ|@@Fz@V|>{ZDPE#Nm0>Pvp~db!x4pEqyCE#{1)(Z{wt#tX8n`u&aOgVHT& zBq3j#Oftr3LH#;&%L?-fhwT<#Po$+I%4F$RgT7S;`R}EpIevtP{I)ku z)#lC>N`kJ-*C(dD{XH8qXlmCDYYJNxMdEKUT`hD}CdWtHLlnHRc>}jB8Wx7vl6R#w z91J+MGALM@+FMA~5xR!9ox>+qOwQ*f8=O8Ilh6;V30*$7s4O2 z>*cPr*T2u))y>Qut__=-zs>xCiTi@vl7<8}`S%JaY* zS_@SxOMX78)EAyrV*hRw)34jTU|c-BB2QQtGIJ|W>}E_Z4COHjH06S@Vw!*Mt^Iug zq(gp3oC=uDB+Q<1KaM|If-K)rRISN}Dt$w=wUKu=q7frJ7*N zp_=2}*TQGZygT66%_2@F#|NZYiek(?r{|G__4$wInpP&?Q^vgNwGueGb9J^76iz5D zSC{(EcsY?s`!#aJm>;cCuZu*&Ox~3!L91RBvfJ2^@c}U^0q~}U`5ky8m27LKTcxK` zKv~tqy^NeV&|K8vbu5Kcxilj;NSL~e9&%(@PUt-W0!{tA!Vl=+5#BwFTkC0D2>zXP zTxF}!ge$w0aM<=TB5wq;7iXe9`^>Rx zuT9bR+L);(X!|I+N!paYAEMSPcQ&}!u&v9@GQ1zatKkK{Y_2Kv*ck8o4$cdiY_PTj zF^x_s*0}M9g&*eU`V~3a?Yz{#v{YT@-cBRMOaE@OD{FboJrj44jKvv|#!&g4gh@X9 zx|zBpkJ|S}+Jm-tkwa9Y*ja#o6|%H{B*U}PG6V6n@yj;q->3Rg0rXPy({ULXmftY# zc8Z_VPQA=#qu?pC%JgQU+PIr!PebKipgr8}$0h+ao&&~%YI z=jfF=N(+&%O+_>WZf%|6JMTM0jZ6UJt)kXOYl=(!oT_Ow>xv4)o0XbH-I8Hq{`APtOS~=I8o}k}u zpO#<%xi1TjsH)%S#9f4Ux5~utPOfi_uL#D6_F5EdH4s3W=5~_e3yq&3qjRQRDbYfb zkrYSoW|ifnNdIgFlT1gZFS1R*nlVdF(1Yyi#swJqM3NVb3g>+V$>$P;s3?IJRD8tL zBgV`uV6-vNwH01jo>`Oy>~?MvPQ;6?%#!31sFawF72J?9*y!Mz$>zp7p0 zIcIqH^xVc3qOm-*&KN$FtI5A07hh$e67S|)b_`nB=|n5JW{VutE+tmE_0n*8gGh61 z>TgSf)S+%Kidp<oiBL6Y@{fwPwwuY|6L>O;#y?2}_T-v#+jJ&6{;1+C{mF>>V zEXqI-5E12cq;?*oRc$bV8Xy#C^o3stjF;x7L7<@-4N3&xh0b?|!dun3=KTPT)X`{l z?npc~%KozsL9x3S2e$B@B>2zL-rHvsFoW%)`v!wUydUq>`Kc9`IGKnNMWx8*sB+q!4&d4N}hHm94+X?vQ*D3PqH> zcTZuARoDC1Y7M+tbzx)WD#kmJ-|X(PyAA?W42(M_baIpM%Okl(?%lbsp?)KONSl^# zzB)timlR-`TXo|x>ci{eDgJxiXnSj^g{d{%M_ApkVRVGC=K#YWUNlqCka$T$pvp3D z-BmQx=-HPKJs%_D2?%@x)*P8YT@8SfIS8myhQm7*(5`D{Li_t_WQoVXgPR+h>WDUk z5v(iZ5N}h7+F2fG_2_Tj5MFOOLN&++#UY73bo;iIvPwyqmgT@4`w+sQIPDaMhY= zLLI2cASor?Te4E1F9s6r_La~cygU9nk&n?y%5ivPh>ku$n(3ju3;#Vk)=sjbQO}1< z@3c_CT}w8ft%Q)H6bfVz6UJVkt+MnHLt z3i&*LcQnii^2B(jGzzJ`1?iks&~(lu%TiP&v5hPeb{6N4r0l zm_d>WfE7ODy8T^bx{sS2ps=SW{(z?=SK*S98-t<(c>*tGiNM1HW?Ie7x11~RZopzJ zdc7t}eUb&Fr5X>lBeIznb-M%INiSFtC}Lx(iLYd&Vo3C9yTqlB^=P%Zr&Y-4^2|Gf zqU_re!*ZAx+wwZJ%M4I*?z_pYmy`X*lGf^!)Q=SBY4SUe>A1EyiuD z*B>F|JRD_PrDCVqLk5K%Tu!xMu%FoX5-No{jmIS zOih-aw;beP*RrRhouc>(dbPZ>mz;Rp*#~(8CvRduF;e8dwb3l9xS<40V!IT z|L~oN?JeSHc39jKW%svgbpd0TS)$GM{xkWYJ#0^Xp2hAihZ1;|-RtcBl}^tOTYb($ zYubT|Fkp*tO?=bbaCXG~s4^lZOqLL43SU#c;|lI63tjPBJ^XuEZ}J_*7?nIFa*o{} zl&J+@?nLUjp+v*ReGo%kw;#%BN$P=a`FlD`IDA$?beFKrdDEtN0m}@XiTcbDYdd*8CyDTYI!u6@`*@jT=I(T?HH(^9vmQf z+0M<&Q|9f@FtCcbMAaNQlg1TMf>AI-xREn9%5|RxTF%@C8t(2VV+deHMtf(cin zKYc`J?*`iiDeu|1`bwGYfY*0qDT+Ypd(M%E2a%4x3(QJ}$*9J~rp(MOFk(S-BB$Jq z%$IM5PdplCg{eXdTlp>`ftSE=b&_K&A(b8y7+U0-XgJg)CL?L;hr+r4z7wFWKl*q} zXE2_D=vKb0^yQ>DCdW*PEot>*N8oXx_C}D8~M&}WxH2^)5q0}-xE`v%l7nz^; zZ!zKuS6Y}}sN0dF$diFH)|HgqYR2{8V*cJ_t-P9ZXxHcq)#;3_VM+!DggKto%(6!bvT8$-tUYC-TZg)Hz<|9GNt5YAS{CFwsq+2fwE!ZBxm zq2yzWhO@&V}G#ML(I(3ts0QOhA>f#<`n+(i1~ z`246Soy!FT-K^*fa|=G(1e=^95RD}qk@GYzw;W}1T%_k1x?V33fuAZ7l`b@EQGCGr z8rfTQ!}{)JuG7t*PHgpCTUbQ}&s!m@jp6>qIsR#u8HVQbtCw?Z6V*=aK4$>&=DV)3 zGsf+Yz1G};W;77+cnD_5+uy65lN_S++?|(nXB+@ICN(X1NGOZ_&!y;#ZO!xnY%=3kEPAb;4C;b`$ z`(92&rK&<}^k{0{NF#m;TL{y2t%F&Qh>B5wwSYP z;N!-5Psrh{v zz50yhNTiXr`)#gsmmwjPo>3hcGv##Nk2IolxKQB}t5gjL|6VAEHLU>WXP$LSlqyT+ z*aOYBCcKEEy_b9u?5_NZ*=(hLh0V6mj;vx+R-r9jIgdWof-)u^t?c{l_{=>Og5(X! zJVnNFih|fyL)vD~3a_!1@XP_XoJVe|yKAmOhP#{-bLVe{xmnDO-~`5q(V{E9$#Cj& zF{(`<(0o9_6$kWL{mqhPGW+$>AqUTkNxUER1_E3?%wE213WOHeyh zEz3c$wrPKk)Eye6qQwW@$*Jm&j4~Z9WS4=o2E=9$tSBBm`8HH2q87_uYxTAm7oY<{ z@}d;Mk$DAw`qPlqYZ#6Ps~10qBPKCZ&5Wa9nGQrspBfo3D*Vmu{^bR-q*tPJ2+mhzHj<0mW3^Z|SjnXlBBRd7MO0a6b{D0L zIiOvK9Avb5D>1y2wS0Inn01|2LRgMsUKG|~r?rC8h-*MY)iuN``g|j``4{C=bhBUC z4B{j23)C+cPkl&Aza(F8-P;kkP#wSvGyX4mUMLOApEiI&hBc#Js3@ae`Ft3f{Lw19 z%!Hne!)WilGhVx{MwBlijf`l0Zstv0gF)D9>O(>q$hh*Bi`)8f1pNwA^&EPSq-vwJ z0PMw}KcQ4?D^yowVMjg?SP{G}ZC#2}X&q=iS9r5Ya8gO*bP0>;n}GH=)|zw_(wGhDcEt!zR5EN!7>4sG>za&U8}CfJG-)XhoNJ<#D(CHboia7NbXM%Ja)_ zR!|=T-46sLV%VmO1mJ_C-I;_t=+&61=8p~#C{0!tT}C|V0jM4w74W%6V$&_)C54J^ z!qjyoGS5QT@7o2rAcm@L+rP{GRFkv=K^*N|Fe_Gqwi(&f*J&|^a79K%jCKOtv{^5h z_d{B|uiM;kTV#}{#|%csXgj8_SUd=mUa0?WFG{D5+uc>`xPcgSt=ri&TiY3_6QSL$ zE-0mNVq^aNkVyfr0h?JooJqpWmT?;P_lTKnCtO&adZMN)rJvH@v~bNX;&y68n{d60 z2{a}abENs6w0~-!#jx|u`4yuC`wnLnEcl@WFGq$jkd3YK33#`7V&xz5O0dC%(3|}Z z1Ze`J#>piIu9A0u3FnQHIW|EG!d^ab#sB-#%CYlF0(^CkVvL&XBJ6T5ro~cwAM*A| z!BQ<=J++fRLr8is#j2u?m@k3TbNU_gk4pThA0;M07J!T5^kB|4ykO-41&f1cx}CIS zhv}Wq2Na*B%H94$deifjxs4@?9cdPK4&Qi4tbbm`o;}jX%4bmV2c2Hgy88L0v4``K zi>O^!qG@}i%jJL5BjzXRzv{@%Att4Eu3jQejSP?#7cZ5M^-XoOT2vfRnLO=y7&?QY z+5{&_My_y#fx51@K!;KC`B|6TLYCXk0hOK9kT)1I<`>4hwDx&!aWU-R)YKy^AKph4 zK2(^||1_e*cO32AmLG&Y7l!D4(Mp1L$KL64mcCG-#6NgmfxviQ5deyhwZ21k^V5&} zi-WMP{8BUDQ|>W1D?hOl*<<3rT820Gz3JsUJ$D=&%0ZWzY?pXE=7QHaIbQ>kRMc7= z2<8*n(u5mqN9S4?QG$CmXbBKqhB2GMmQAPA-02y|jpv=w1#!^eUR73pXLYQ-!3h2Q zhCZZnFX{xbI^FOW-5~`r`mI$YNy}7OjLx@HZubMXg(p-Cdh=1HZDh}@pK>eJCY4IY>?;$beGzaPqdi>4*K`ZS_5(^+rCP$Oan4I}w( zE{#;+@OlHg2H^>Ii`q4MgQFNmF&*a~JXWV{O++0lm8(zD6gDEpjcq+{%UM^swlcHJ z*f%xRUC(0u!xHy^7_t-Vd*`H6?o3cmu?n$6dDB?#wLM`p% z)M4h|sqsL606@iyK68U)Wj)CKuE`t40orUA5jYCnqsgh%d(mO%i^e9Uy5Djl9UTlU z;YQFk2t1)dD{J{#ZHf=qO5iU4BZA0JA6eBsWt%ar3e2LE^z~Y-@g9FDVMimBv&rdU z?`bZ$T6_R2+=e~ZDIM@d(PuC0k_~!wmfFFKFn~yRE5;tb@7N;!WP~howVW};2!5Hy zFIHDfBf3S{u&1o>=2{rD-Ee*5)9|E0=6*n172d3(iip;oD`aJsnhfJJAfG@TCge_1 z<`qXv`Mb*-ppiTM#-i=`M-f#n^YbCDWRLCgv1A4~f{W{J*P4wL6GD5k!92qdizq$< zf)*VtW3a6cAP|}N(yM6#Ib^qrRN78JMc8=`5I`RVgWW;pJB9)W2#J=WujAQaxn;9W z;8+8@Myv8dtsv%M2qs)gTEzA^(dNBXr1o8fncS9Sjb=KHZc*xKAusXSTD?h{plHmd z02oZK-{w;c)vR$722ozR-E6-7DluCBBTpgwV@0=`gwK-?YN-x(Y^GAu;#*JY8i(Tn zFw&xkS0T0wZ<;erc1pZXnST^?VhU87K!s%_{lHFeq{_5r6YN}f2z2)5UHzG$id(+p zA2r$ssRP~V()A**^WCoUj{4TnNc)Jy{WoVQA&oWgp}Y)55%I}>T1Br5SBV-@&ZLQY z0Xb30oX1Vx=~m1IemXenM{e-<{lvx5HQy+vWEQ1h$_yyRtn`+dVJO{6+LCB{Gk zN;;u7>fv7M!gMb~R=bICZc#yqRR(trsC)Fs80xg`g~+wl_xjon&G2j#a}vF3g`+6y zqikjC{TC)+l9{Jb(klEdsn-)KQ*epQ<+=K^K12GGB+^oTB0zENs1HD~fX0>B?@|14 z-wAwALhC{0(cAh7YR~%n$yH+*92c9~?_CbVFMKlir862Cyl3#sp(Ryo7lbZzZ8!9< ziDN8AUfAU#*PpF_3sW0+LL?}pLtCkA1~xNa$xT{q0mW(lE%i8eBwm>TO zi_4@)GzRat<9crpJ$EYJuCzPJMcz}3)H~h1+_}Pxpxz2b$!;9fM&(8*jlvmO9Noq7 z*AI|nhPNhAm4a5v5%*{~9a8UsZbvrQsJz<`yZBn!)m6J8bsy?DI_(ntOZXbV_R2N+ zmG{rYT7J)z|2%hI{xOTHaYtGiV;~)CBGxsB8XYo-<2hu|$=h7^+UK%hpaJcHy!P4E zo|2iy8GCqFp2hLkQX7BIWL&Qc=?3D%S<`h+Pd3gf6@~2y<_=LbN?((CE$baAOzQ<~u7Q;7whvUCd4676UKw?3=-gj32mDc{PU-P?hf6dq| zS^e*$(nez^0k;278) zCz#lS=DEK={crh#Wjd?db3W_K>C=DS{^h2V%HR`o`^6=`*8+cS$oB;U`# se?amBX#WF}|A6FM|Nj3yldJ|zM4D;E8!8<61^jneP5WZ*dCQ>x0)O5*i~s-t literal 0 HcmV?d00001 diff --git a/doc/image/system_purpose.png b/doc/image/system_purpose.png new file mode 100644 index 0000000000000000000000000000000000000000..ef854bc00e7b8831208d5c645c79942f8f6cc590 GIT binary patch literal 78446 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", "S3 - server address (URL)"); +DEFINE_string(s3_access_key, "", "S3 - AccessKey"); +DEFINE_string(s3_secret_access_key, "", "S3 - SecretAccessKey"); +DEFINE_string(s3_bucket, "madfs", "S3 - bucket name"); +DEFINE_int32(s3_bg_threads, 4, "S3 - number of background threads"); + +DEFINE_uint64(read_normal_flow_limit, 1024, "Read cache normal flow limit"); +DEFINE_uint64(read_burst_flow_limit, 10 * 1024, "Read cache burst flow limit"); +DEFINE_uint64(read_capacity_mb, 4096, "Read cache capacity in MB"); +DEFINE_uint64(read_page_body_size, 64 * 1024, "Read cache page body size"); +DEFINE_uint64(read_page_meta_size, 1024, "Read cache page meta size"); +DEFINE_bool(read_cas, true, "Read cache enable CAS"); +DEFINE_bool(read_nvm_cache, false, "Read cache enable NVM cache"); + +DEFINE_bool(use_meta_cache, true, "Enable meta cache"); +DEFINE_uint64(meta_cache_max_size, 1024 * 1024, "Max size of meta cache"); +DEFINE_uint64(meta_cache_clear_size, 512 * 1024, "Read cache burst flow limit"); + +DEFINE_uint64(write_chunk_size, 16 * 1024 * 1024, "Granularity of global write cache"); +DEFINE_uint64(max_inflight_payload_size, 256 * 1024 * 1024, "Max inflight payload size in bytes"); + +DEFINE_string(etcd_prefix, "/madfs/", "Etcd directory prefix"); + +DEFINE_bool(verbose, false, "Print debug logging"); + +namespace brpc { + DECLARE_int64(socket_max_unwritten_bytes); +}; + +static GlobalConfig g_cfg; +std::once_flag g_cfg_once; + +#define SAFE_ASSIGN(conf, flag, min_val, max_val) { \ + const static auto flag##_min = (min_val); \ + const static auto flag##_max = (max_val); \ + if (flag < (min_val) || flag > (max_val)) { \ + LOG(WARNING) << "Invalid " #flag ", reset to " << (max_val); \ + flag = (max_val); \ + } \ + conf = flag; \ +} + +void InitGlobalConfig() { + SAFE_ASSIGN(g_cfg.rpc_timeout, FLAGS_rpc_timeout, 0, 60000); + SAFE_ASSIGN(g_cfg.rpc_threads, FLAGS_rpc_threads, 0, 256); + SAFE_ASSIGN(g_cfg.rpc_connections, FLAGS_rpc_connections, 0, 64); + SAFE_ASSIGN(g_cfg.folly_threads, FLAGS_folly_threads, 0, 256); + g_cfg.use_rdma = FLAGS_use_rdma; + g_cfg.write_chunk_size = FLAGS_write_chunk_size; + + g_cfg.default_policy.read_chunk_size = FLAGS_read_chunk_size; + g_cfg.default_policy.read_replication_factor = FLAGS_read_replication_factor; + + g_cfg.default_policy.read_chunk_size = FLAGS_read_chunk_size; + g_cfg.default_policy.read_replication_factor = FLAGS_read_replication_factor; + + g_cfg.use_meta_cache = FLAGS_use_meta_cache; + g_cfg.meta_cache_max_size = size_t(FLAGS_meta_cache_max_size); + g_cfg.meta_cache_clear_size = size_t(FLAGS_meta_cache_clear_size); + + g_cfg.read_cache_dir = FLAGS_read_cache_dir; + g_cfg.write_cache_dir = FLAGS_write_cache_dir; + + g_cfg.etcd_prefix = FLAGS_etcd_prefix; + g_cfg.max_inflight_payload_size = FLAGS_max_inflight_payload_size; + + if (FLAGS_write_cache_type == "nocache") { + g_cfg.default_policy.write_cache_type = NOCACHE; + } else if (FLAGS_write_cache_type == "replication") { + g_cfg.default_policy.write_cache_type = REPLICATION; + g_cfg.default_policy.write_replication_factor = FLAGS_write_replication_factor; + } else if (FLAGS_write_cache_type == "reed-solomon") { + g_cfg.default_policy.write_cache_type = REED_SOLOMON; + g_cfg.default_policy.write_data_blocks = FLAGS_write_data_blocks; + g_cfg.default_policy.write_parity_blocks = FLAGS_write_parity_blocks; + } else { + LOG(ERROR) << "The program will be terminated because of unsupported write cache type: " << FLAGS_write_cache_type; + exit(EXIT_FAILURE); + } + + g_cfg.s3_config.address = FLAGS_s3_address; + g_cfg.s3_config.access_key = FLAGS_s3_access_key; + g_cfg.s3_config.secret_access_key = FLAGS_s3_secret_access_key; + g_cfg.s3_config.bucket = FLAGS_s3_bucket; + g_cfg.s3_config.bg_threads = FLAGS_s3_bg_threads; + + HybridCache::ReadCacheConfig &read_cache = g_cfg.read_cache; + read_cache.DownloadNormalFlowLimit = FLAGS_read_normal_flow_limit; + read_cache.DownloadBurstFlowLimit = FLAGS_read_burst_flow_limit; + read_cache.CacheCfg.CacheName = "Read"; + read_cache.CacheCfg.MaxCacheSize = FLAGS_read_capacity_mb * 1024 * 1024;; + read_cache.CacheCfg.PageBodySize = FLAGS_read_page_body_size; + read_cache.CacheCfg.PageMetaSize = FLAGS_read_page_meta_size; + read_cache.CacheCfg.EnableCAS = FLAGS_read_cas; + read_cache.CacheCfg.CacheLibCfg.EnableNvmCache = FLAGS_read_nvm_cache; + + brpc::FLAGS_socket_max_unwritten_bytes = FLAGS_max_inflight_payload_size * 2; +} + +GlobalConfig &GetGlobalConfig() { + std::call_once(g_cfg_once, InitGlobalConfig); + return g_cfg; +} diff --git a/global_cache/Common.h b/global_cache/Common.h new file mode 100644 index 0000000..246a10a --- /dev/null +++ b/global_cache/Common.h @@ -0,0 +1,130 @@ +#ifndef MADFS_COMMON_H +#define MADFS_COMMON_H + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "config.h" + +using folly::Future; +using folly::Promise; + +#define RED "\033[1;31m" +#define GREEN "\033[1;32m" +#define YELLOW "\033[1;33m" +#define WHITE "\033[0m" + +DECLARE_bool(verbose); + +const static int OK = 0; +const static int RPC_FAILED = -2; +const static int NOT_FOUND = -3; +const static int CACHE_ENTRY_NOT_FOUND = -3; // deprecated +const static int INVALID_ARGUMENT = -4; +const static int S3_INTERNAL_ERROR = -5; +const static int FOLLY_ERROR = -6; +const static int NO_ENOUGH_REPLICAS = -7; +const static int METADATA_ERROR = -8; +const static int IO_ERROR = -9; +const static int END_OF_FILE = -10; +const static int NO_ENOUGH_DISKSPACE = -11; +const static int UNSUPPORTED_TYPE = -12; +const static int UNSUPPORTED_OPERATION = -13; +const static int UNIMPLEMENTED = -128; + +struct GetOutput { + int status; + butil::IOBuf buf; +}; + +struct PutOutput { + int status; + std::string internal_key; +}; + +struct QueryTsOutput { + int status; + uint64_t timestamp; +}; + +enum WriteCacheType { + NOCACHE, REPLICATION, REED_SOLOMON +}; + +struct S3Config { + std::string address; + std::string access_key; + std::string secret_access_key; + std::string bucket; + int bg_threads; +}; + +struct CachePolicy { + size_t read_chunk_size; + size_t read_replication_factor; + + WriteCacheType write_cache_type; + size_t write_replication_factor; // if write_cache_type == REPLICATION + size_t write_data_blocks; + size_t write_parity_blocks; // if write_cache_type == REED_SOLOMON +}; + +struct GlobalConfig { + int rpc_timeout; + int rpc_threads; + int rpc_connections; + int folly_threads; + bool use_rdma; + + bool use_meta_cache; + size_t meta_cache_max_size; + size_t meta_cache_clear_size; + + size_t write_chunk_size; + + size_t max_inflight_payload_size; + + CachePolicy default_policy; + S3Config s3_config; + + HybridCache::ReadCacheConfig read_cache; + HybridCache::WriteCacheConfig write_cache; + + std::string read_cache_dir; + std::string write_cache_dir; + + std::string etcd_prefix; +}; + +GlobalConfig &GetGlobalConfig(); + +static inline std::string PathJoin(const std::string &left, const std::string &right) { + if (left.empty()) { + return right; + } else if (left[left.length() - 1] == '/') { + return left + right; + } else { + return left + "/" + right; + } +} + +static inline int CreateParentDirectories(const std::string &path) { + auto pos = path.rfind('/'); + if (pos == path.npos) { + return 0; + } + auto parent = path.substr(0, pos); + boost::filesystem::create_directories(parent); + return 0; +} + +#endif // MADFS_COMMON_H \ No newline at end of file diff --git a/global_cache/ErasureCodingWriteCacheClient.cpp b/global_cache/ErasureCodingWriteCacheClient.cpp new file mode 100644 index 0000000..e01e5b3 --- /dev/null +++ b/global_cache/ErasureCodingWriteCacheClient.cpp @@ -0,0 +1,333 @@ +#include "ErasureCodingWriteCacheClient.h" +#include "GlobalDataAdaptor.h" + +// #define CONFIG_JERASURE + +#ifdef CONFIG_JERASURE +#include +#include + +static int _roundup(int a, int b) { + if (a % b == 0) return a; + return a + b - (a % b); +} + +folly::Future ErasureCodingWriteCacheClient::Put(const std::string &key, + size_t size, + const ByteBuffer &buffer, + const std::map &headers, + size_t off) { + std::vector > future_list; + Json::Value root; + Json::Value json_replica(Json::arrayValue), json_headers; + + const std::vector replicas = GetReplica(key); + for (auto server_id: replicas) { + json_replica.append(server_id); + } + + auto &policy = parent_->GetCachePolicy(key); + const int k = policy.write_data_blocks; + const int m = policy.write_parity_blocks; + const int w = 32; + auto matrix = reed_sol_vandermonde_coding_matrix(k, m, w); + std::vector data_buf_list; + auto rpc_client = parent_->GetRpcClient(); + auto write_chunk_size = GetGlobalConfig().write_chunk_size; + for (uint64_t offset = 0; offset < size; offset += write_chunk_size) { + const auto unit_size = _roundup((write_chunk_size + k - 1) / k, w); + const auto region_size = std::min(write_chunk_size, size - offset); + char *data_buf = new char[(k + m) * unit_size]; + data_buf_list.push_back(data_buf); + memcpy(data_buf, &buffer.data[offset], region_size); + memset(data_buf + region_size, 0, k * unit_size - region_size); + char *data_ptrs[k] = { nullptr }, *coding_ptrs[m] = { nullptr }; + for (int i = 0; i < k + m; ++i) { + if (i < k) { + data_ptrs[i] = &data_buf[i * unit_size]; + } else { + coding_ptrs[i - k] = &data_buf[i * unit_size]; + } + } + jerasure_matrix_encode(k, m, w, matrix, data_ptrs, coding_ptrs, unit_size); + auto cur_data_buf = data_buf; + for (auto server_id: replicas) { + ByteBuffer region_buffer(cur_data_buf, unit_size); + cur_data_buf += unit_size; + std::string partial_key = key + + "-" + std::to_string(offset / write_chunk_size) + + "-" + std::to_string(write_chunk_size); + future_list.emplace_back(rpc_client->PutEntryFromWriteCache(server_id, partial_key, region_buffer, unit_size)); + } + } + for (auto iter = headers.begin(); iter != headers.end(); ++iter) { + json_headers[iter->first] = iter->second; + } + + root["type"] = "reed-solomon"; + root["size"] = size; + root["replica"] = json_replica; + root["headers"] = json_headers; + + return folly::collectAll(future_list).via(parent_->executor_.get()).thenValue( + [this, root, data_buf_list, matrix](std::vector > output) -> PutResult { + free(matrix); + for (auto &entry : data_buf_list) { + delete []entry; + } + Json::Value res_root; + Json::Value json_path(Json::arrayValue); + for (auto &entry: output) { + if (!entry.hasValue()) + return PutResult { FOLLY_ERROR, res_root }; + if (entry.value().status != OK) + return PutResult { entry.value().status, res_root }; + json_path.append(entry.value().internal_key); + } + res_root = root; + res_root["path"] = json_path; + return PutResult { OK, res_root }; + }); +} + +folly::Future ErasureCodingWriteCacheClient::Get(const std::string &key, + size_t start, + size_t size, + ByteBuffer &buffer, + Json::Value &root) { + std::vector replicas; + for (auto &entry : root["replica"]) { + replicas.push_back(entry.asInt()); + } + + std::vector internal_keys; + for (auto &entry : root["path"]) { + internal_keys.push_back(entry.asString()); + } + + std::vector > future_list; + std::vector requests; + auto write_chunk_size = GetGlobalConfig().write_chunk_size; + GenerateGetChunkRequestsV2(key, start, size, buffer, requests, write_chunk_size); + if (requests.empty()) + return folly::makeFuture(OK); + + for (auto &entry: requests) { + auto &policy = parent_->GetCachePolicy(key); + const int k = policy.write_data_blocks; + const int m = policy.write_parity_blocks; + const int w = 32; + const auto unit_size = _roundup((write_chunk_size + k - 1) / k, w); + const auto start_replica_id = entry.chunk_start / unit_size; + const auto end_replica_id = (entry.chunk_start + entry.chunk_len + unit_size - 1) / unit_size; + size_t dest_buf_pos = 0; + for (auto replica_id = start_replica_id; replica_id < end_replica_id; ++replica_id) { + auto start_off = (replica_id == start_replica_id) ? entry.chunk_start % unit_size : 0; + auto end_off = (replica_id + 1 == end_replica_id) ? (entry.chunk_start + entry.chunk_len) - replica_id * unit_size : unit_size; + int server_id = replicas[replica_id]; + std::string internal_key = internal_keys[entry.chunk_id * replicas.size() + replica_id]; + auto cur_dest_buf_pos = dest_buf_pos; + dest_buf_pos += (end_off - start_off); + future_list.emplace_back(parent_->GetRpcClient()->GetEntryFromWriteCache(server_id, internal_key, start_off, end_off - start_off) + .then([this, server_id, entry, start_off, end_off, cur_dest_buf_pos](folly::Try &&output) -> folly::Future { + if (!output.hasValue()) { + return folly::makeFuture(FOLLY_ERROR); + } + auto &value = output.value(); + if (value.status == OK) { + value.buf.copy_to(entry.buffer.data + cur_dest_buf_pos, end_off - start_off); + return folly::makeFuture(OK); + } else { + return folly::makeFuture(value.status); + } + })); + } + } + + return folly::collectAll(future_list).via(parent_->executor_.get()).thenValue( + [=](std::vector > output) -> int { + for (auto &entry: output) + if (entry.value_or(FOLLY_ERROR) != OK) { + LOG(ERROR) << "Failed to get data from write cache, key: " << key + << ", start: " << start + << ", size: " << size + << ", buf: " << (void *) buffer.data << " " << buffer.len + << ", error code: " << entry.hasValue() << " " << entry.value_or(FOLLY_ERROR); + return entry.value_or(FOLLY_ERROR); + } + return OK; + }); +} + + +folly::Future ErasureCodingWriteCacheClient::GetDecode(const std::string &key, + size_t start, + size_t size, + ByteBuffer &buffer, + Json::Value &root) { + std::vector replicas; + for (auto &entry : root["replica"]) { + replicas.push_back(entry.asInt()); + } + + std::vector internal_keys; + for (auto &entry : root["path"]) { + internal_keys.push_back(entry.asString()); + } + + std::vector requests; + auto write_chunk_size = GetGlobalConfig().write_chunk_size; + GenerateGetChunkRequestsV2(key, start, size, buffer, requests, write_chunk_size); + if (requests.empty()) + return folly::makeFuture(OK); + + std::vector > future_list; + + for (auto &entry: requests) { + auto &policy = parent_->GetCachePolicy(key); + const int k = policy.write_data_blocks; + const int m = policy.write_parity_blocks; + const int w = 32; + auto matrix = reed_sol_vandermonde_coding_matrix(k, m, w); + const auto unit_size = _roundup((write_chunk_size + k - 1) / k, w); + const auto start_replica_id = entry.chunk_start / unit_size; + const auto end_replica_id = (entry.chunk_start + entry.chunk_len + unit_size - 1) / unit_size; + int erasures[k + m + 1] = { 0 }; + int erasures_idx = 0; + + char *data_buf = new char[(k + m) * unit_size]; + char *data_ptrs[k] = { nullptr }, *coding_ptrs[m] = { nullptr }; + for (int i = 0; i < k + m; ++i) { + if (i < k) { + data_ptrs[i] = &data_buf[i * unit_size]; + } else { + coding_ptrs[i - k] = &data_buf[i * unit_size]; + } + } + + // rarely occurred, can be synchronized + for (auto replica_id = 0; replica_id < k + m; ++replica_id) { + int server_id = replicas[replica_id]; + std::string internal_key = internal_keys[entry.chunk_id * replicas.size() + replica_id]; + auto output = parent_->GetRpcClient()->GetEntryFromWriteCache(server_id, internal_key, 0, unit_size).get(); + if (output.status == OK) { + if (replica_id < k) { + output.buf.copy_to(data_ptrs[replica_id], unit_size); + } else { + output.buf.copy_to(coding_ptrs[replica_id - k], unit_size); + } + } else { + erasures[erasures_idx++] = replica_id; + } + } + + erasures[erasures_idx] = -1; + + int rc = jerasure_matrix_decode(k, m, w, matrix, 1, erasures, data_ptrs, coding_ptrs, unit_size); + if (rc == -1) { + LOG(FATAL) << "Unable to decode RS matrix"; + return IO_ERROR; + } + + auto cur_pos = 0; + for (auto replica_id = start_replica_id; replica_id < end_replica_id; ++replica_id) { + auto start_pos = (replica_id == start_replica_id) ? entry.chunk_start % unit_size : 0; + auto end_pos = (replica_id + 1 == end_replica_id) ? (entry.chunk_start + entry.chunk_len) - replica_id * unit_size : unit_size; + memcpy(entry.buffer.data + cur_pos, data_ptrs[replica_id] + start_pos, end_pos - start_pos); + cur_pos += end_pos - start_pos; + } + + delete []data_buf; + free(matrix); + } + + return OK; +} + +std::vector ErasureCodingWriteCacheClient::GetReplica(const std::string &key) { + const int num_available = parent_->server_list_.size(); + auto &policy = parent_->GetCachePolicy(key); + const int num_choose = policy.write_data_blocks + policy.write_parity_blocks; + uint64_t seed = std::hash < std::string > {}(key); + std::vector output; + // for (int i = 0; i < std::min(num_available, num_choose); ++i) + for (int i = 0; i < num_choose; ++i) + output.push_back((seed + i) % num_available); + return output; +} + +void ErasureCodingWriteCacheClient::GenerateGetChunkRequestsV2(const std::string &key, + size_t start, + size_t size, + ByteBuffer &buffer, + std::vector &requests, + size_t chunk_size) { + const size_t end = start + size; + + const size_t begin_chunk_id = start / chunk_size; + const size_t end_chunk_id = (end + chunk_size - 1) / chunk_size; + + if (buffer.len < size) { + LOG(WARNING) << "Buffer capacity may be not enough, expect " << size << ", actual " << buffer.len; + } + + size_t buffer_offset = 0; + for (size_t chunk_id = begin_chunk_id; chunk_id < end_chunk_id; ++chunk_id) { + size_t chunk_start = std::max(chunk_id * chunk_size, start); + size_t chunk_stop = std::min((chunk_id + 1) * chunk_size, end); + if (chunk_stop <= chunk_start) + return; + GetChunkRequestV2 item; + item.user_key = key; + item.chunk_id = chunk_id; + item.chunk_start = chunk_start % chunk_size; + item.chunk_len = chunk_stop - chunk_start; + item.chunk_granularity = chunk_size; + item.buffer.data = buffer.data + buffer_offset; + item.buffer.len = item.chunk_len; + buffer_offset += item.chunk_len; + requests.emplace_back(item); + } + LOG_ASSERT(buffer_offset == size); +} +#else +folly::Future ErasureCodingWriteCacheClient::Put(const std::string &key, + size_t size, + const ByteBuffer &buffer, + const std::map &headers, + size_t off) { + PutResult res; + res.status = UNSUPPORTED_OPERATION; + return res; +} + +folly::Future ErasureCodingWriteCacheClient::Get(const std::string &key, + size_t start, + size_t size, + ByteBuffer &buffer, + Json::Value &root) { + return UNSUPPORTED_OPERATION; +} + + +folly::Future ErasureCodingWriteCacheClient::GetDecode(const std::string &key, + size_t start, + size_t size, + ByteBuffer &buffer, + Json::Value &root) { + return UNSUPPORTED_OPERATION; +} + +std::vector ErasureCodingWriteCacheClient::GetReplica(const std::string &key) { + return std::vector{}; +} + +void ErasureCodingWriteCacheClient::GenerateGetChunkRequestsV2(const std::string &key, + size_t start, + size_t size, + ByteBuffer &buffer, + std::vector &requests, + size_t chunk_size) { + +} +#endif \ No newline at end of file diff --git a/global_cache/ErasureCodingWriteCacheClient.h b/global_cache/ErasureCodingWriteCacheClient.h new file mode 100644 index 0000000..ba5263a --- /dev/null +++ b/global_cache/ErasureCodingWriteCacheClient.h @@ -0,0 +1,61 @@ +#ifndef MADFS_EC_WRITE_CACHE_CLIENT_H +#define MADFS_EC_WRITE_CACHE_CLIENT_H + +#include "WriteCacheClient.h" + +using HybridCache::ByteBuffer; + +class GlobalDataAdaptor; + +using PutResult = WriteCacheClient::PutResult; + +class ErasureCodingWriteCacheClient : public WriteCacheClient { + friend class GetChunkContext; + +public: + ErasureCodingWriteCacheClient(GlobalDataAdaptor *parent) : parent_(parent) {} + + ~ErasureCodingWriteCacheClient() {} + + virtual folly::Future Put(const std::string &key, + size_t size, + const ByteBuffer &buffer, + const std::map &headers, + size_t off = 0); + + virtual folly::Future Get(const std::string &key, + size_t start, + size_t size, + ByteBuffer &buffer, + Json::Value &root); + + virtual folly::Future GetDecode(const std::string &key, + size_t start, + size_t size, + ByteBuffer &buffer, + Json::Value &root); + +public: + std::vector GetReplica(const std::string &key); + + struct GetChunkRequestV2 { + std::string user_key; + size_t chunk_id; + size_t chunk_start; + size_t chunk_len; + size_t chunk_granularity; + ByteBuffer buffer; + }; + + static void GenerateGetChunkRequestsV2(const std::string &key, + size_t start, + size_t size, + ByteBuffer &buffer, + std::vector &requests, + size_t chunk_size); + +private: + GlobalDataAdaptor *parent_; +}; + +#endif // MADFS_EC_WRITE_CACHE_CLIENT_H \ No newline at end of file diff --git a/global_cache/EtcdClient.h b/global_cache/EtcdClient.h new file mode 100644 index 0000000..321683d --- /dev/null +++ b/global_cache/EtcdClient.h @@ -0,0 +1,101 @@ +#ifndef ETCD_CLIENT_H +#define ETCD_CLIENT_H + +#include +#include +#include + +#include "WriteCacheClient.h" + +class EtcdClient { +public: + EtcdClient(const std::string &etcd_url) : client_(etcd_url) {}; + + ~EtcdClient() {} + + struct GetResult { + int status; + Json::Value root; + }; + + folly::Future GetJson(const std::string &key) { + std::lock_guard lock(mutex_); + Json::Reader reader; + Json::Value root; + auto resp = client_.get(PathJoin(GetGlobalConfig().etcd_prefix, key)); + if (!resp.is_ok()) { + if (resp.error_code() != 100) { + LOG(ERROR) << "Error from etcd client: " << resp.error_code() + << ", message: " << resp.error_message(); + return folly::makeFuture(GetResult{ METADATA_ERROR, root }); + } else { + LOG(WARNING) << "Record not found in the etcd storage: key " << key; + return folly::makeFuture(GetResult{ NOT_FOUND, root }); + } + } + if (!reader.parse(resp.value().as_string(), root)) { + LOG(ERROR) << "Error from etcd client: failed to parse record: " << resp.value().as_string(); + return folly::makeFuture(GetResult{ METADATA_ERROR, root }); + } + LOG(INFO) << "Record get: " << key; + return folly::makeFuture(GetResult{ OK, root }); + } + + folly::Future PutJson(const std::string &key, const Json::Value &root) { + std::lock_guard lock(mutex_); + Json::FastWriter writer; + const std::string json_file = writer.write(root); + auto resp = client_.put(PathJoin(GetGlobalConfig().etcd_prefix, key), json_file); + if (!resp.is_ok()) { + LOG(ERROR) << "Error from etcd client: " << resp.error_code() + << ", message: " << resp.error_message(); + return folly::makeFuture(METADATA_ERROR); + } + LOG(INFO) << "Record put: " << key; + return folly::makeFuture(OK); + } + + folly::Future DeleteJson(const std::string &key) { + std::lock_guard lock(mutex_); + auto resp = client_.rm(PathJoin(GetGlobalConfig().etcd_prefix, key)); + if (!resp.is_ok()) { + if (resp.error_code() != 100) { + LOG(ERROR) << "Error from etcd client: " << resp.error_code() + << ", message: " << resp.error_message(); + return folly::makeFuture(METADATA_ERROR); + } else { + LOG(WARNING) << "Record not found in the etcd storage: key " << key; + return folly::makeFuture(NOT_FOUND); + } + return folly::makeFuture(METADATA_ERROR); + } + return folly::makeFuture(OK); + } + + folly::Future ListJson(const std::string &key_prefix, std::vector &key_list) { + std::lock_guard lock(mutex_); + const std::string etcd_prefix = GetGlobalConfig().etcd_prefix; + auto resp = client_.keys(PathJoin(etcd_prefix, key_prefix)); + if (!resp.is_ok()) { + if (resp.error_code() != 100) { + LOG(ERROR) << "Error from etcd client: " << resp.error_code() + << ", message: " << resp.error_message(); + return folly::makeFuture(METADATA_ERROR); + } else { + LOG(WARNING) << "Record not found in the etcd storage: key " << key_prefix; + return folly::makeFuture(NOT_FOUND); + } + return folly::makeFuture(METADATA_ERROR); + } + for (auto &entry : resp.keys()) { + key_list.push_back(entry.substr(etcd_prefix.length())); + } + return folly::makeFuture(OK); + } + +private: + std::mutex mutex_; + etcd::SyncClient client_; +}; + +#endif // ETCD_CLIENT_H \ No newline at end of file diff --git a/global_cache/FileSystemDataAdaptor.h b/global_cache/FileSystemDataAdaptor.h new file mode 100644 index 0000000..4edf98f --- /dev/null +++ b/global_cache/FileSystemDataAdaptor.h @@ -0,0 +1,323 @@ +#ifndef MADFS_FILE_SYSTEM_DATA_ADAPTOR_H +#define MADFS_FILE_SYSTEM_DATA_ADAPTOR_H + +#include +#include +#include +#include +#include +#include + +#include "Common.h" +#include "data_adaptor.h" + +#include +#include +#include +#include + +#include + +using HybridCache::ByteBuffer; +using HybridCache::DataAdaptor; + +static inline ssize_t fully_pread(int fd, void* buf, size_t n, size_t offset) { + ssize_t total_read = 0; + ssize_t bytes_read; + while (total_read < n) { + bytes_read = pread(fd, buf + total_read, n - total_read, offset); + if (bytes_read < 0) { + if (errno == EAGAIN) continue; + return -1; + } else if (bytes_read == 0) { + break; + } + total_read += bytes_read; + offset += bytes_read; + } + return total_read; +} + +static inline ssize_t fully_pwrite(int fd, void* buf, size_t n, size_t offset) { + ssize_t total_written = 0; + ssize_t bytes_written; + while (total_written < n) { + bytes_written = pwrite(fd, buf + total_written, n - total_written, offset); + if (bytes_written < 0) { + if (errno == EAGAIN) continue; + return -1; + } else if (bytes_written == 0) { + break; + } + total_written += bytes_written; + offset += bytes_written; + } + return total_written; +} + +class FileSystemDataAdaptor : public DataAdaptor { + const std::string prefix_; + std::shared_ptr base_adaptor_; + bool use_optimized_path_; + std::shared_ptr executor_; + bool fsync_required_; + +public: + FileSystemDataAdaptor(const std::string &prefix = "", + std::shared_ptr base_adaptor = nullptr, + bool use_optimized_path = false, + std::shared_ptr executor = nullptr, + bool fsync_required = true) + : prefix_(prefix), + base_adaptor_(base_adaptor), + use_optimized_path_(use_optimized_path), + executor_(executor), + fsync_required_(fsync_required) {} + + ~FileSystemDataAdaptor() {} + + virtual folly::Future DownLoad(const std::string &key, + size_t start, + size_t size, + ByteBuffer &buffer) { + LOG_IF(INFO, FLAGS_verbose) << "Download key: " << key << ", start: " << start << ", size: " << size; + + if (!buffer.data || buffer.len < size) { + LOG(ERROR) << "Buffer capacity is not enough, expected " << size + << ", actual " << buffer.len; + return folly::makeFuture(INVALID_ARGUMENT); + } + + auto path = BuildPath(prefix_, key); + if (access(path.c_str(), F_OK)) { + if (base_adaptor_) { + #if 1 + size_t full_size; + std::map headers; + if (base_adaptor_->Head(key, full_size, headers).get()) { + LOG(ERROR) << "Fail to retrive metadata of key: " << key; + return folly::makeFuture(IO_ERROR); + } + ByteBuffer tmp_buffer(new char[full_size], full_size); + return base_adaptor_->DownLoad(key, 0, full_size, tmp_buffer).thenValue([buffer, tmp_buffer, start, size, key](int rc) -> int { + if (rc) { + LOG(ERROR) << "Fail to retrive data of key: " << key; + return IO_ERROR; + } + memcpy(buffer.data, tmp_buffer.data + start, size); + delete []tmp_buffer.data; + return OK; + }); + #else + return base_adaptor_->DownLoad(key, start, size, buffer); + #endif + } else if (errno == ENOENT) { + LOG_IF(ERROR, FLAGS_verbose) << "File not found: " << path; + return folly::makeFuture(NOT_FOUND); + } else { + PLOG(ERROR) << "Fail inaccessible: " << path; + return folly::makeFuture(IO_ERROR); + } + } + + butil::Timer t; + t.start(); + + const bool kUseDirectIO = false; // ((uint64_t) buffer.data & 4095) == 0 && (size & 4095) == 0; + int flags = O_RDONLY; + flags |= kUseDirectIO ? O_DIRECT : 0; + int fd = open(path.c_str(), flags); + if (fd < 0) { + PLOG(ERROR) << "Fail to open file: " << path; + return folly::makeFuture(IO_ERROR); + } + +#ifdef ASYNC_IO + if (kUseDirectIO) { + thread_local folly::SimpleAsyncIO aio(folly::SimpleAsyncIO::Config().setCompletionExecutor(executor_.get())); + auto promise = std::make_shared>(); + aio.pread(fd, buffer.data, size, start, [key, size, promise, fd](int rc) { + if (rc != size) { + PLOG(ERROR) << "Fail to read file: " << key + << ", expected read " << size + << ", actual read " << rc; + close(fd); + promise->setValue(IO_ERROR); + } else { + close(fd); + promise->setValue(OK); + } + }); + return promise->getFuture(); + } +#endif + + ssize_t nbytes = fully_pread(fd, buffer.data, size, start); + if (nbytes != size) { + PLOG(ERROR) << "Fail to read file: " << key + << ", expected read " << size + << ", actual read " << nbytes; + close(fd); + return folly::makeFuture(IO_ERROR); + } + + t.stop(); + // LOG_EVERY_N(INFO, 1) << t.u_elapsed() << " " << size; + + close(fd); + return folly::makeFuture(OK); + } + + virtual folly::Future UpLoad(const std::string &key, + size_t size, + const ByteBuffer &buffer, + const std::map &headers) { + butil::Timer t; + t.start(); + LOG_IF(INFO, FLAGS_verbose) << "Upload key: " << key << ", size: " << size; + if (!buffer.data || buffer.len < size) { + LOG(ERROR) << "Buffer capacity is not enough, expected " << size + << ", actual " << buffer.len; + return folly::makeFuture(INVALID_ARGUMENT); + } + + auto path = BuildPath(prefix_, key); + if (CreateParentDirectories(path)) { + return folly::makeFuture(IO_ERROR); + } + + t.stop(); + //LOG(INFO) << "Upload P0: " << key << " " << t.u_elapsed() << " " << size; + const bool kUseDirectIO = false; // ((uint64_t) buffer.data & 4095) == 0 && (size & 4095) == 0; + int flags = O_WRONLY | O_CREAT; + flags |= kUseDirectIO ? O_DIRECT : 0; + int fd = open(path.c_str(), flags, 0644); + if (fd < 0) { + PLOG(ERROR) << "Fail to open file: " << path; + return folly::makeFuture(IO_ERROR); + } + +#ifdef ASYNC_IO + if (kUseDirectIO) { + thread_local folly::SimpleAsyncIO aio(folly::SimpleAsyncIO::Config().setCompletionExecutor(executor_.get())); + auto promise = std::make_shared>(); + aio.pwrite(fd, buffer.data, size, 0, [key, size, promise, fd](int rc) { + if (rc != size) { + PLOG(ERROR) << "Fail to write file: " << key + << ", expected " << size + << ", actual " << rc; + close(fd); + promise->setValue(IO_ERROR); + } + + if (ftruncate64(fd, size) < 0) { + PLOG(ERROR) << "Fail to truncate file: " << key; + close(fd); + return folly::makeFuture(IO_ERROR); + } + + if (fsync_required_ && fsync(fd) < 0) { + PLOG(ERROR) << "Fail to sync file: " << key; + close(fd); + return folly::makeFuture(IO_ERROR); + } + + close(fd); + promise->setValue(OK); + + }); + return promise->getFuture(); + } +#endif + + ssize_t nbytes = fully_pwrite(fd, buffer.data, size, 0); + if (nbytes != size) { + PLOG(ERROR) << "Fail to write file: " << key + << ", expected read " << size + << ", actual read " << nbytes; + close(fd); + return folly::makeFuture(IO_ERROR); + } + + t.stop(); + //LOG(INFO) << "Upload P2: " << key << " " << t.u_elapsed() << " " << size; + if (ftruncate64(fd, size) < 0) { + PLOG(ERROR) << "Fail to truncate file: " << key; + close(fd); + return folly::makeFuture(IO_ERROR); + } + + t.stop(); + //LOG(INFO) << "Upload P3: " << key << " " << t.u_elapsed() << " " << size; + if (fsync_required_ && fsync(fd) < 0) { + PLOG(ERROR) << "Fail to sync file: " << key; + close(fd); + return folly::makeFuture(IO_ERROR); + } + + close(fd); + + if (base_adaptor_) { + return base_adaptor_->UpLoad(key, size, buffer, headers); + } + t.stop(); + // LOG(INFO) << "Upload P4: " << key << " " << t.u_elapsed() << " " << size; + return folly::makeFuture(OK); + } + + virtual folly::Future Delete(const std::string &key) { + LOG_IF(INFO, FLAGS_verbose) << "Delete key: " << key; + auto path = BuildPath(prefix_, key); + if (remove(path.c_str())) { + if (errno == ENOENT) { + LOG_IF(ERROR, FLAGS_verbose) << "File not found: " << path; + return folly::makeFuture(NOT_FOUND); + } else { + PLOG(ERROR) << "Failed to remove file: " << path; + return folly::makeFuture(IO_ERROR); + } + } + if (base_adaptor_) { + return base_adaptor_->Delete(key); + } + return folly::makeFuture(OK); + } + + virtual folly::Future Head(const std::string &key, + size_t &size, + std::map &headers) { + LOG_IF(INFO, FLAGS_verbose) << "Head key: " << key; + if (base_adaptor_) { + return base_adaptor_->Head(key, size, headers); + } + auto path = BuildPath(prefix_, key); + struct stat st; + if (access(path.c_str(), F_OK)) { + if (errno == ENOENT) { + LOG_IF(ERROR, FLAGS_verbose) << "File not found: " << path; + return folly::makeFuture(NOT_FOUND); + } else { + PLOG(ERROR) << "Failed to access file: " << path; + return folly::makeFuture(IO_ERROR); + } + } + if (stat(path.c_str(), &st)) { + PLOG(ERROR) << "Fail to state file: " << path; + return folly::makeFuture(IO_ERROR); + } + size = st.st_size; + return folly::makeFuture(OK); + } + + std::string BuildPath(const std::string &prefix, const std::string &key) { + if (use_optimized_path_) { + std::size_t h1 = std::hash{}(key); + std::string suffix = std::to_string(h1 % 256) + '/' + std::to_string(h1 % 65536) + '/' + key; + return PathJoin(prefix, suffix); + } else { + return PathJoin(prefix, key); + } + } +}; + +#endif // MADFS_FILE_SYSTEM_DATA_ADAPTOR_H \ No newline at end of file diff --git a/global_cache/GarbageCollectorMain.cpp b/global_cache/GarbageCollectorMain.cpp new file mode 100644 index 0000000..fe52134 --- /dev/null +++ b/global_cache/GarbageCollectorMain.cpp @@ -0,0 +1,50 @@ +#include +#include +#include +#include +#include + +#include "S3DataAdaptor.h" +#include "FileSystemDataAdaptor.h" +#include "GlobalDataAdaptor.h" +#include "ReadCacheClient.h" + +#include "GlobalCacheServer.h" +#include "S3DataAdaptor.h" + +DEFINE_string(data_server, "0.0.0.0:8000", "IP address of global data servers"); +DEFINE_string(etcd_server, "http://127.0.0.1:2379", "Location of etcd server"); +DEFINE_string(prefix, "", "Key prefix for garbage collection"); +DEFINE_bool(use_s3, false, "Use S3 storage"); + +std::vector SplitString(const std::string &input) { + std::vector result; + std::stringstream ss(input); + std::string item; + while (std::getline(ss, item, ',')) { + result.push_back(item); + } + return result; +} + +int main(int argc, char *argv[]) { + std::cerr << YELLOW << "MADFS GC TOOL" << WHITE << std::endl; + + gflags::ParseCommandLineFlags(&argc, &argv, true); + auto etcd_client = std::make_shared(FLAGS_etcd_server); + std::shared_ptr base_adaptor; + if (FLAGS_use_s3) { + base_adaptor = std::make_shared(); + } else { + base_adaptor = std::make_shared(); + } + + auto global_adaptor = std::make_shared(base_adaptor, SplitString(FLAGS_data_server), etcd_client); + if (global_adaptor->PerformGarbageCollection(FLAGS_prefix)) { + std::cerr << RED << "Garbage collection failed!" << WHITE << std::endl; + exit(EXIT_FAILURE); + } else { + std::cerr << GREEN << "Garbage collection successfully" << WHITE << std::endl; + exit(EXIT_SUCCESS); + } +} diff --git a/global_cache/GlobalCacheClient.cpp b/global_cache/GlobalCacheClient.cpp new file mode 100644 index 0000000..972fa5a --- /dev/null +++ b/global_cache/GlobalCacheClient.cpp @@ -0,0 +1,368 @@ +#include +#include +#include +#include + +#include "gcache.pb.h" +#include "GlobalCacheClient.h" + +GlobalCacheClient::GlobalCacheClient(const std::string &group) : group_(group), inflight_payload_size_(0) {} + +GlobalCacheClient::~GlobalCacheClient() { + for (auto &entry: server_map_) { + delete entry.second; + } + server_map_.clear(); +} + +int GlobalCacheClient::RegisterServer(int server_id, const char *hostname) { + std::lock_guard lock(mutex_); + if (server_map_.count(server_id)) { + LOG(WARNING) << "Server has been registered, previous regitration will be override" + << ", group: " << group_ + << ", server_id: " << server_id + << ", hostname: " << hostname; + } + + brpc::ChannelOptions options; + options.use_rdma = GetGlobalConfig().use_rdma; + options.timeout_ms = GetGlobalConfig().rpc_timeout; + options.connection_group = group_; + + int32_t fixed_backoff_time_ms = 100; // 固定时间间隔(毫秒) + int32_t no_backoff_remaining_rpc_time_ms = 150; // 无需重试退避的剩余rpc时间阈值(毫秒) + bool retry_backoff_in_pthread = false; + static brpc::RpcRetryPolicyWithFixedBackoff g_retry_policy_with_fixed_backoff( + fixed_backoff_time_ms, no_backoff_remaining_rpc_time_ms, retry_backoff_in_pthread); + options.retry_policy = &g_retry_policy_with_fixed_backoff; + options.max_retry = 5; + + + auto channel = new brpc::Channel(); + if (channel->Init(hostname, &options)) { + PLOG(ERROR) << "Unable to initialize channel object" + << ", group: " << group_ + << ", server_id: " << server_id + << ", hostname: " << hostname; + delete channel; + return RPC_FAILED; + } + + // Sending sync register RPC + gcache::GlobalCacheService_Stub stub(channel); + brpc::Controller cntl; + gcache::RegisterRequest request; + gcache::RegisterResponse response; + stub.Register(&cntl, &request, &response, nullptr); + if (cntl.Failed() || response.status_code() != OK) { + LOG(ERROR) << "Failed to register server, reason: " << cntl.ErrorText() + << ", group: " << group_ + << ", server_id: " << server_id + << ", hostname: " << hostname; + delete channel; + return RPC_FAILED; + } + + LOG_IF(INFO, FLAGS_verbose) << "Register server successfully" + << ", group: " << group_ + << ", server_id: " << server_id + << ", hostname: " << hostname; + + server_map_[server_id] = channel; + return OK; +} + +brpc::Channel *GlobalCacheClient::GetChannelByServerId(int server_id) { + std::lock_guard lock(mutex_); + if (!server_map_.count(server_id)) { + LOG_EVERY_SECOND(ERROR) << "Server not registered. server_id: " << server_id; + return nullptr; + } + return server_map_[server_id]; +} + +Future GlobalCacheClient::GetEntry(int server_id, + const std::string &key, + uint64_t start, + uint64_t length, + bool is_read_cache) { + // while (inflight_payload_size_.load() >= GetGlobalConfig().max_inflight_payload_size) { + // LOG_EVERY_SECOND(INFO) << "Overcroweded " << inflight_payload_size_.load(); + // sched_yield(); + // } + inflight_payload_size_.fetch_add(length); + + auto channel = GetChannelByServerId(server_id); + if (!channel) { + GetOutput output; + output.status = RPC_FAILED; + return folly::makeFuture(output); + } + + gcache::GlobalCacheService_Stub stub(channel); + gcache::GetEntryRequest request; + request.set_key(key); + request.set_start(start); + request.set_length(length); + + struct OnRPCDone : public google::protobuf::Closure { + virtual void Run() { + GetOutput output; + if (cntl.Failed()) { + LOG(WARNING) << "RPC error: " << cntl.ErrorText() + << ", server id: " << server_id + << ", key: " << key + << ", start: " << start + << ", length: " << length; + output.status = RPC_FAILED; + } else { + output.status = response.status_code(); + output.buf = cntl.response_attachment(); + if (output.status == OK && output.buf.length() != length) { + LOG(WARNING) << "Received truncated attachment, expected " << length + << " bytes, actual " << output.buf.length() << " bytes" + << ", server id: " << server_id + << ", key: " << key + << ", start: " << start + << ", length: " << length; + output.status = RPC_FAILED; + } + } + promise.setValue(output); + parent->inflight_payload_size_.fetch_sub(length); + t.stop(); + LOG_EVERY_N(INFO, 1000) << t.u_elapsed(); + delete this; + } + + brpc::Controller cntl; + gcache::GetEntryResponse response; + Promise promise; + + int server_id; + std::string key; + uint64_t start; + uint64_t length; + GlobalCacheClient *parent; + butil::Timer t; + }; + + auto done = new OnRPCDone(); + done->t.start(); + done->parent = this; + done->server_id = server_id; + done->key = key; + done->start = start; + done->length = length; + + auto future = done->promise.getFuture(); + if (is_read_cache) + stub.GetEntryFromReadCache(&done->cntl, &request, &done->response, done); + else + stub.GetEntryFromWriteCache(&done->cntl, &request, &done->response, done); + return std::move(future); +} + +Future GlobalCacheClient::PutEntry(int server_id, + const std::string &key, + const ByteBuffer &buf, + uint64_t length, + bool is_read_cache) { + // while (inflight_payload_size_.load() >= GetGlobalConfig().max_inflight_payload_size) { + // LOG_EVERY_SECOND(INFO) << "Overcroweded " << inflight_payload_size_.load(); + // sched_yield(); + // } + inflight_payload_size_.fetch_add(length); + + auto channel = GetChannelByServerId(server_id); + if (!channel) { + PutOutput output; + output.status = RPC_FAILED; + return folly::makeFuture(output); + } + + gcache::GlobalCacheService_Stub stub(channel); + gcache::PutEntryRequest request; + request.set_key(key); + request.set_length(length); + + struct OnRPCDone : public google::protobuf::Closure { + virtual void Run() { + PutOutput output; + if (cntl.Failed()) { + LOG(WARNING) << "RPC error: " << cntl.ErrorText() + << ", server id: " << server_id + << ", key: " << key + << ", length: " << length; + output.status = RPC_FAILED; + } else { + output.status = response.status_code(); + output.internal_key = response.internal_key(); + } + promise.setValue(output); + parent->inflight_payload_size_.fetch_sub(length); + delete this; + } + + brpc::Controller cntl; + gcache::PutEntryResponse response; + Promise promise; + + int server_id; + std::string key; + uint64_t length; + GlobalCacheClient *parent; + }; + + auto done = new OnRPCDone(); + done->parent = this; + done->server_id = server_id; + done->key = key; + done->length = length; + + done->cntl.request_attachment().append(buf.data, length); + auto future = done->promise.getFuture(); + if (is_read_cache) + stub.PutEntryFromReadCache(&done->cntl, &request, &done->response, done); + else + stub.PutEntryFromWriteCache(&done->cntl, &request, &done->response, done); + return std::move(future); +} + +Future GlobalCacheClient::DeleteEntryFromReadCache(int server_id, + const std::string &key, + uint64_t chunk_size, + uint64_t max_chunk_id) { + auto channel = GetChannelByServerId(server_id); + if (!channel) { + LOG(ERROR) << "Cannot find channel for server " << server_id; + return folly::makeFuture(RPC_FAILED); + } + + gcache::GlobalCacheService_Stub stub(channel); + gcache::DeleteEntryRequest request; + request.set_key(key); + request.set_chunk_size(chunk_size); + request.set_max_chunk_id(max_chunk_id); + + struct OnRPCDone : public google::protobuf::Closure { + virtual void Run() { + int status; + if (cntl.Failed()) { + LOG(WARNING) << "RPC error: " << cntl.ErrorText() + << ", server id: " << server_id + << ", key: " << key; + status = RPC_FAILED; + } else { + status = response.status_code(); + } + promise.setValue(status); + delete this; + } + + brpc::Controller cntl; + gcache::DeleteEntryResponse response; + Promise promise; + + int server_id; + std::string key; + }; + + auto done = new OnRPCDone(); + done->server_id = server_id; + done->key = key; + + auto future = done->promise.getFuture(); + stub.DeleteEntryFromReadCache(&done->cntl, &request, &done->response, done); + return std::move(future); +} + +Future GlobalCacheClient::QueryTsFromWriteCache(int server_id) { + auto channel = GetChannelByServerId(server_id); + if (!channel) { + QueryTsOutput output; + output.status = RPC_FAILED; + return folly::makeFuture(output); + } + + gcache::GlobalCacheService_Stub stub(channel); + gcache::QueryTsRequest request; + + struct OnRPCDone : public google::protobuf::Closure { + virtual void Run() { + QueryTsOutput output; + if (cntl.Failed()) { + LOG(WARNING) << "RPC error: " << cntl.ErrorText() + << ", server id: " << server_id; + output.status = RPC_FAILED; + } else { + output.status = response.status_code(); + output.timestamp = response.timestamp(); + } + promise.setValue(output); + delete this; + } + + brpc::Controller cntl; + gcache::QueryTsResponse response; + Promise promise; + + int server_id; + }; + + auto done = new OnRPCDone(); + done->server_id = server_id; + + auto future = done->promise.getFuture(); + stub.QueryTsFromWriteCache(&done->cntl, &request, &done->response, done); + return std::move(future); +} + +Future GlobalCacheClient::DeleteEntryFromWriteCache(int server_id, + const std::string &key_prefix, + uint64_t max_ts, + std::vector &except_keys) { + auto channel = GetChannelByServerId(server_id); + if (!channel) { + LOG(ERROR) << "Cannot find channel for server " << server_id; + return folly::makeFuture(RPC_FAILED); + } + + gcache::GlobalCacheService_Stub stub(channel); + gcache::DeleteEntryRequestForWriteCache request; + request.set_key_prefix(key_prefix); + request.set_max_ts(max_ts); + for (auto &entry : except_keys) + request.add_except_keys(entry); + + struct OnRPCDone : public google::protobuf::Closure { + virtual void Run() { + int status; + if (cntl.Failed()) { + LOG(WARNING) << "RPC error: " << cntl.ErrorText() + << ", server id: " << server_id + << ", key: " << key; + status = RPC_FAILED; + } else { + status = response.status_code(); + } + promise.setValue(status); + delete this; + } + + brpc::Controller cntl; + gcache::DeleteEntryResponse response; + Promise promise; + + int server_id; + std::string key; + }; + + auto done = new OnRPCDone(); + done->server_id = server_id; + done->key = key_prefix; + + auto future = done->promise.getFuture(); + stub.DeleteEntryFromWriteCache(&done->cntl, &request, &done->response, done); + return std::move(future); +} \ No newline at end of file diff --git a/global_cache/GlobalCacheClient.h b/global_cache/GlobalCacheClient.h new file mode 100644 index 0000000..297bbee --- /dev/null +++ b/global_cache/GlobalCacheClient.h @@ -0,0 +1,62 @@ +#ifndef MADFS_GLOBAL_CACHE_CLIENT_H +#define MADFS_GLOBAL_CACHE_CLIENT_H + +#include +#include +#include +#include +#include + +#include "Common.h" +#include "common.h" + +using HybridCache::ByteBuffer; + +class GlobalCacheClient { +public: + GlobalCacheClient(const std::string &group = ""); + + ~GlobalCacheClient(); + + int RegisterServer(int server_id, const char *hostname); + + Future GetEntryFromReadCache(int server_id, const std::string &key, uint64_t start, uint64_t length) { + return GetEntry(server_id, key, start, length, true); + } + + Future PutEntryFromReadCache(int server_id, const std::string &key, const ByteBuffer &buf, uint64_t length) { + return PutEntry(server_id, key, buf, length, true); + } + + Future DeleteEntryFromReadCache(int server_id, const std::string &key, uint64_t chunk_size, uint64_t max_chunk_id); + + Future GetEntryFromWriteCache(int server_id, const std::string &key, uint64_t start, uint64_t length){ + return GetEntry(server_id, key, start, length, false); + } + + Future PutEntryFromWriteCache(int server_id, const std::string &key, const ByteBuffer &buf, uint64_t length){ + return PutEntry(server_id, key, buf, length, false); + } + + Future QueryTsFromWriteCache(int server_id); + + Future DeleteEntryFromWriteCache(int server_id, + const std::string &key_prefix, + uint64_t max_ts, + std::vector &except_keys); + +private: + brpc::Channel *GetChannelByServerId(int server_id); + + Future GetEntry(int server_id, const std::string &key, uint64_t start, uint64_t length, bool is_read_cache); + + Future PutEntry(int server_id, const std::string &key, const ByteBuffer &buf, uint64_t length, bool is_read_cache); + +private: + std::mutex mutex_; + const std::string group_; + std::map server_map_; + std::atomic inflight_payload_size_; +}; + +#endif // MADFS_GLOBAL_CACHE_CLIENT_H \ No newline at end of file diff --git a/global_cache/GlobalCacheServer.cpp b/global_cache/GlobalCacheServer.cpp new file mode 100644 index 0000000..bb59b13 --- /dev/null +++ b/global_cache/GlobalCacheServer.cpp @@ -0,0 +1,107 @@ +#include "GlobalCacheServer.h" + +namespace gcache { + GlobalCacheServiceImpl::GlobalCacheServiceImpl(std::shared_ptr executor, + std::shared_ptr base_adaptor) + : executor_(executor) { + read_cache_ = std::make_shared(executor_, base_adaptor); + write_cache_ = std::make_shared(executor_); + } + + void GlobalCacheServiceImpl::GetEntryFromReadCache(google::protobuf::RpcController *cntl_base, + const GetEntryRequest *request, + GetEntryResponse *response, + google::protobuf::Closure *done) { + brpc::Controller *cntl = static_cast(cntl_base); + read_cache_->Get(request->key(), request->start(), request->length()) + .thenValue([this, cntl, request, done, response](GetOutput output) { + response->set_status_code(output.status); + butil::Timer t; + t.start(); + cntl->response_attachment().append(output.buf); + t.stop(); + // LOG_EVERY_N(INFO, 1000) << t.u_elapsed(); + done->Run(); + }); + } + + void GlobalCacheServiceImpl:: PutEntryFromReadCache(google::protobuf::RpcController *cntl_base, + const PutEntryRequest *request, + PutEntryResponse *response, + google::protobuf::Closure *done) { + brpc::Controller *cntl = static_cast(cntl_base); + auto output = read_cache_->Put(request->key(), request->length(), cntl->request_attachment()); + response->set_status_code(output); + done->Run(); + } + + void GlobalCacheServiceImpl::DeleteEntryFromReadCache(google::protobuf::RpcController *cntl_base, + const DeleteEntryRequest *request, + DeleteEntryResponse *response, + google::protobuf::Closure *done) { + brpc::Controller *cntl = static_cast(cntl_base); + if (request->has_chunk_size() && request->has_max_chunk_id()) { + response->set_status_code(read_cache_->Delete(request->key(), + request->chunk_size(), + request->max_chunk_id())); + } else { + response->set_status_code(read_cache_->Delete(request->key())); + } + done->Run(); + } + + void GlobalCacheServiceImpl::GetEntryFromWriteCache(google::protobuf::RpcController *cntl_base, + const GetEntryRequest *request, + GetEntryResponse *response, + google::protobuf::Closure *done) { + brpc::Controller *cntl = static_cast(cntl_base); + auto output = write_cache_->Get(request->key(), request->start(), request->length()); + response->set_status_code(output.status); + cntl->response_attachment().append(output.buf); + done->Run(); + } + + void GlobalCacheServiceImpl::PutEntryFromWriteCache(google::protobuf::RpcController *cntl_base, + const PutEntryRequest *request, + PutEntryResponse *response, + google::protobuf::Closure *done) { + brpc::Controller *cntl = static_cast(cntl_base); + auto output = write_cache_->Put(request->key(), request->length(), cntl->request_attachment()); + response->set_status_code(output.status); + response->set_internal_key(output.internal_key); + done->Run(); + } + + void GlobalCacheServiceImpl::DeleteEntryFromWriteCache(google::protobuf::RpcController *cntl_base, + const DeleteEntryRequestForWriteCache *request, + DeleteEntryResponse *response, + google::protobuf::Closure *done) { + brpc::Controller *cntl = static_cast(cntl_base); + std::unordered_set except_keys; + for (auto &entry : request->except_keys()) { + except_keys.insert(entry); + } + auto output = write_cache_->Delete(request->key_prefix(), request->max_ts(), except_keys); + response->set_status_code(output); + done->Run(); + } + + void GlobalCacheServiceImpl::QueryTsFromWriteCache(google::protobuf::RpcController *cntl_base, + const QueryTsRequest *request, + QueryTsResponse *response, + google::protobuf::Closure *done) { + brpc::Controller *cntl = static_cast(cntl_base); + response->set_timestamp(write_cache_->QueryTS()); + response->set_status_code(OK); + done->Run(); + } + + void GlobalCacheServiceImpl::Register(google::protobuf::RpcController *cntl_base, + const RegisterRequest *request, + RegisterResponse *response, + google::protobuf::Closure *done) { + brpc::Controller *cntl = static_cast(cntl_base); + response->set_status_code(OK); + done->Run(); + } +} \ No newline at end of file diff --git a/global_cache/GlobalCacheServer.h b/global_cache/GlobalCacheServer.h new file mode 100644 index 0000000..58a9c62 --- /dev/null +++ b/global_cache/GlobalCacheServer.h @@ -0,0 +1,74 @@ +#ifndef MADFS_GLOBAL_CACHE_SERVER_H +#define MADFS_GLOBAL_CACHE_SERVER_H + +#include +#include +#include +#include +#include +#include + +#include "butil/time.h" +#include "bvar/bvar.h" + +#include "gcache.pb.h" +#include "ReadCache.h" +#include "WriteCache.h" +#include "data_adaptor.h" + +namespace gcache { + class GlobalCacheServiceImpl : public GlobalCacheService { + public: + GlobalCacheServiceImpl(std::shared_ptr executor, + std::shared_ptr base_adaptor); + + virtual ~GlobalCacheServiceImpl() {} + + virtual void GetEntryFromReadCache(google::protobuf::RpcController *cntl_base, + const GetEntryRequest *request, + GetEntryResponse *response, + google::protobuf::Closure *done); + + virtual void PutEntryFromReadCache(google::protobuf::RpcController *cntl_base, + const PutEntryRequest *request, + PutEntryResponse *response, + google::protobuf::Closure *done); + + virtual void DeleteEntryFromReadCache(google::protobuf::RpcController *cntl_base, + const DeleteEntryRequest *request, + DeleteEntryResponse *response, + google::protobuf::Closure *done); + + virtual void GetEntryFromWriteCache(google::protobuf::RpcController *cntl_base, + const GetEntryRequest *request, + GetEntryResponse *response, + google::protobuf::Closure *done); + + virtual void PutEntryFromWriteCache(google::protobuf::RpcController *cntl_base, + const PutEntryRequest *request, + PutEntryResponse *response, + google::protobuf::Closure *done); + + virtual void DeleteEntryFromWriteCache(google::protobuf::RpcController *cntl_base, + const DeleteEntryRequestForWriteCache *request, + DeleteEntryResponse *response, + google::protobuf::Closure *done); + + virtual void QueryTsFromWriteCache(google::protobuf::RpcController *cntl_base, + const QueryTsRequest *request, + QueryTsResponse *response, + google::protobuf::Closure *done); + + virtual void Register(google::protobuf::RpcController *cntl_base, + const RegisterRequest *request, + RegisterResponse *response, + google::protobuf::Closure *done); + + private: + std::shared_ptr executor_; + std::shared_ptr read_cache_; + std::shared_ptr write_cache_; + }; +} + +#endif // MADFS_GLOBAL_CACHE_SERVER_H \ No newline at end of file diff --git a/global_cache/GlobalCacheServerMain.cpp b/global_cache/GlobalCacheServerMain.cpp new file mode 100644 index 0000000..5ca94fa --- /dev/null +++ b/global_cache/GlobalCacheServerMain.cpp @@ -0,0 +1,41 @@ +#include "GlobalCacheServer.h" +#include "S3DataAdaptor.h" + +#include + +DEFINE_int32(port, 8000, "TCP Port of global cache server"); +DEFINE_bool(fetch_s3_if_miss, false, "Allow fetch data from S3 if cache miss"); + +int main(int argc, char *argv[]) { + LOG(INFO) << "MADFS Global Cache Server"; + gflags::ParseCommandLineFlags(&argc, &argv, true); + brpc::Server server; + + folly::SingletonVault::singleton()->registrationComplete(); + + brpc::ServerOptions options; + options.num_threads = GetGlobalConfig().rpc_threads; + options.use_rdma = GetGlobalConfig().use_rdma; + + std::shared_ptr base_adaptor = nullptr; + if (FLAGS_fetch_s3_if_miss) { + base_adaptor = std::make_shared(); + } + + auto executor = std::make_shared(GetGlobalConfig().folly_threads); + auto gcache_service = std::make_shared(executor, base_adaptor); + + if (server.AddService(gcache_service.get(), brpc::SERVER_DOESNT_OWN_SERVICE)) { + PLOG(ERROR) << "Failed to register global cache service"; + return -1; + } + + butil::EndPoint point = butil::EndPoint(butil::IP_ANY, FLAGS_port); + if (server.Start(point, &options) != 0) { + PLOG(ERROR) << "Failed to start global cache server"; + return -1; + } + + server.RunUntilAskedToQuit(); + return 0; +} diff --git a/global_cache/GlobalDataAdaptor.cpp b/global_cache/GlobalDataAdaptor.cpp new file mode 100644 index 0000000..6d3c973 --- /dev/null +++ b/global_cache/GlobalDataAdaptor.cpp @@ -0,0 +1,674 @@ +#include "GlobalDataAdaptor.h" +#include "ReadCacheClient.h" +#include "ReplicationWriteCacheClient.h" +#include "ErasureCodingWriteCacheClient.h" + +using HybridCache::ByteBuffer; + +#define CONFIG_GC_ON_EXCEEDING_DISKSPACE + +DEFINE_uint32(bg_execution_period, 10, "Background execution period in seconds"); + +GlobalDataAdaptor::GlobalDataAdaptor(std::shared_ptr base_adaptor, + const std::vector &server_list, + std::shared_ptr etcd_client, + std::shared_ptr executor) + : base_adaptor_(base_adaptor), + executor_(executor), + server_list_(server_list), + etcd_client_(etcd_client), + meta_cache_(GetGlobalConfig().meta_cache_max_size, GetGlobalConfig().meta_cache_clear_size) { + if (!executor_) { + executor_ = std::make_shared(GetGlobalConfig().folly_threads); + } + + read_cache_ = std::make_shared(this); + write_caches_[WC_TYPE_REPLICATION] = std::make_shared(this); + write_caches_[WC_TYPE_REEDSOLOMON] = std::make_shared(this); + + for (int conn_id = 0; conn_id < GetGlobalConfig().rpc_connections; conn_id++) { + auto client = std::make_shared(std::to_string(conn_id)); + int server_id = 0; + for (auto &entry: server_list_) { + if (client->RegisterServer(server_id, entry.c_str())) { + // TODO 周期性尝试重连 + LOG(WARNING) << "Failed to connect with server id: " << server_id + << ", address: " << entry; + bg_mutex_.lock(); + bg_tasks_.push_back([client,server_id, entry]() -> int { + return client->RegisterServer(server_id, entry.c_str()); + }); + bg_mutex_.unlock(); + } + server_id++; + } + rpc_client_.push_back(client); + } + srand48(time(nullptr)); + bg_running_ = true; + bg_thread_ = std::thread(std::bind(&GlobalDataAdaptor::BackgroundWorker, this)); +} + +GlobalDataAdaptor::~GlobalDataAdaptor() { + bg_running_ = false; + bg_cv_.notify_all(); + bg_thread_.join(); +} + +void GlobalDataAdaptor::BackgroundWorker() { + while (bg_running_) { + std::unique_lock lock(bg_mutex_); + std::vector> bg_tasks_next; + for (auto &entry : bg_tasks_) { + if (entry()) { + bg_tasks_next.push_back(entry); + } + } + bg_tasks_ = bg_tasks_next; + bg_cv_.wait_for(lock, std::chrono::seconds(FLAGS_bg_execution_period)); + } +} + +struct DownloadArgs { + DownloadArgs(const std::string &key, size_t start, size_t size, ByteBuffer &buffer) + : key(key), start(start), size(size), buffer(buffer) {} + + std::string key; + size_t start; + size_t size; + ByteBuffer &buffer; +}; + +folly::Future GlobalDataAdaptor::DownLoad(const std::string &key, + size_t start, + size_t size, + ByteBuffer &buffer) { + return DownLoadFromGlobalCache(key, start, size, buffer).then( + [this, key, start, size, &buffer](folly::Try &&output) -> folly::Future { + if (output.value_or(FOLLY_ERROR) == RPC_FAILED) { + return base_adaptor_->DownLoad(key, start, size, buffer); + } + return output.value_or(FOLLY_ERROR); + }); +} + +folly::Future GlobalDataAdaptor::DownLoadFromGlobalCache(const std::string &key, + size_t start, + size_t size, + ByteBuffer &buffer) { + auto &policy = GetCachePolicy(key); + auto meta_cache_entry = GetMetaCacheEntry(key); + if (meta_cache_entry->present) { + if (!meta_cache_entry->existed) { + LOG(ERROR) << "Request for potential deleted file: " << key; + return folly::makeFuture(NOT_FOUND); + } + if (start + size > meta_cache_entry->size) { + LOG(ERROR) << "Request out of file range, key: " << key + << ", start: " << start + << ", size: " << size + << ", file length: " << meta_cache_entry->size; + return folly::makeFuture(END_OF_FILE); + } + } + + if (policy.write_cache_type != NOCACHE) { + auto args = std::make_shared(key, start, size, buffer); + if (meta_cache_entry->present) { + if (meta_cache_entry->write_cached) { + auto &root = meta_cache_entry->root; + if (root["type"] == "replication") { + return write_caches_[WC_TYPE_REPLICATION]->Get(args->key, args->start, args->size, args->buffer, root); + } else if (root["type"] == "reed-solomon") { + return write_caches_[WC_TYPE_REEDSOLOMON]->Get(args->key, args->start, args->size, args->buffer, root); + } + LOG(ERROR) << "Failed to download data, reason: unsuppported type, key: " << args->key + << ", start: " << args->start + << ", size: " << args->size + << ", type: " << root["type"]; + return folly::makeFuture(UNSUPPORTED_TYPE); + } else { + return read_cache_->Get(key, start, size, buffer); + } + } else { + return etcd_client_->GetJson(key).then( + [this, args, meta_cache_entry](folly::Try &&output) -> folly::Future { + if (!output.hasValue()) { // 当 GetJson 函数抛出异常时执行这部分代码 + LOG(ERROR) << "Failed to download data, reason: internal error, key: " << args->key + << ", start: " << args->start + << ", size: " << args->size; + return folly::makeFuture(FOLLY_ERROR); + } + + auto &status = output.value().status; + if (status == NOT_FOUND) { + if (GetGlobalConfig().use_meta_cache) { + return base_adaptor_->Head(args->key, meta_cache_entry->size, meta_cache_entry->headers).then( + [this, meta_cache_entry, args](folly::Try &&output) -> folly::Future { + int res = output.value_or(FOLLY_ERROR); + if (res == OK || res == NOT_FOUND) { + meta_cache_entry->present = true; + meta_cache_entry->existed = (res == OK); + meta_cache_entry->write_cached = false; + } + if (res == OK) { + return read_cache_->Get(args->key, args->start, args->size, args->buffer); + } + return res; + }); + } else { + return read_cache_->Get(args->key, args->start, args->size, args->buffer); + } + } else if (status != OK) { + return folly::makeFuture(status); + } + + auto &root = output.value().root; + if (GetGlobalConfig().use_meta_cache) { + meta_cache_entry->present = true; + meta_cache_entry->existed = true; + meta_cache_entry->write_cached = true; + meta_cache_entry->size = root["size"].asInt64(); + for (auto iter = root["headers"].begin(); iter != root["headers"].end(); iter++) { + meta_cache_entry->headers[iter.key().asString()] = (*iter).asString(); + } + meta_cache_entry->root = root; + } + + if (root["type"] == "replication") { + return write_caches_[WC_TYPE_REPLICATION]->Get(args->key, args->start, args->size, args->buffer, root); + } else if (root["type"] == "reed-solomon") { + return write_caches_[WC_TYPE_REEDSOLOMON]->Get(args->key, args->start, args->size, args->buffer, root); + } + + LOG(ERROR) << "Failed to download data, reason: unsuppported type, key: " << args->key + << ", start: " << args->start + << ", size: " << args->size + << ", type: " << root["type"]; + + return folly::makeFuture(UNSUPPORTED_TYPE); + }); + } + } else { + return read_cache_->Get(key, start, size, buffer); + } +} + +folly::Future GlobalDataAdaptor::UpLoad(const std::string &key, + size_t size, + const ByteBuffer &buffer, + const std::map &headers) { +#ifdef CONFIG_GC_ON_EXCEEDING_DISKSPACE + return DoUpLoad(key, size, buffer, headers).thenValue([this, key, size, &buffer, &headers](int &&res) -> int { + if (res != NO_ENOUGH_DISKSPACE) { + return res; + } + LOG(INFO) << "Disk limit exceeded - perform GC immediately"; + res = PerformGarbageCollection(); + if (res) { + LOG(WARNING) << "GC failed"; + return res; + } + LOG(INFO) << "Disk limit exceeded - GC completed, now retry"; + return DoUpLoad(key, size, buffer, headers).get(); + }); +#else + return DoUpLoad(key, size, buffer, headers); +#endif +} + +folly::Future GlobalDataAdaptor::DoUpLoad(const std::string &key, + size_t size, + const ByteBuffer &buffer, + const std::map &headers) { + butil::Timer *t = new butil::Timer(); + t->start(); + auto &policy = GetCachePolicy(key); + auto meta_cache_entry = GetMetaCacheEntry(key); + meta_cache_entry->present = false; + meta_cache_entry->existed = true; + meta_cache_entry->size = size; + meta_cache_entry->headers = headers; + auto pre_op = read_cache_->Invalidate(key, size); + if (policy.write_cache_type == REPLICATION || policy.write_cache_type == REED_SOLOMON) { + auto write_cache = policy.write_cache_type == REPLICATION + ? write_caches_[WC_TYPE_REPLICATION] + : write_caches_[WC_TYPE_REEDSOLOMON]; + return std::move(pre_op) + .then(std::bind(&WriteCacheClient::Put, write_cache.get(), key, size, buffer, headers, 0)) + .then([this, key, meta_cache_entry, t] (folly::Try output) -> folly::Future { + int status = output.hasValue() ? output.value().status : FOLLY_ERROR; + if (status == OK) { + status = etcd_client_->PutJson(key, output.value().root).get(); + if (status == OK && GetGlobalConfig().use_meta_cache) { + meta_cache_entry->root = output.value().root; + meta_cache_entry->write_cached = true; + meta_cache_entry->present = true; + } + t->stop(); + LOG(INFO) << "JSON: " << t->u_elapsed(); + delete t; + } + return folly::makeFuture(status); + }); + } else if (policy.write_cache_type == NOCACHE) { + return std::move(pre_op) + .then(std::bind(&DataAdaptor::UpLoad, base_adaptor_.get(), key, size, buffer, headers)) + .thenValue([meta_cache_entry](int &&res) -> int { + if (res == OK && GetGlobalConfig().use_meta_cache) { + meta_cache_entry->write_cached = false; + meta_cache_entry->present = true; + } + return res; + }); + } else { + LOG(ERROR) << "Failed to upload data, reason: unsuppported type, key: " << key + << ", size: " << size + << ", type: " << policy.write_cache_type; + return folly::makeFuture(UNSUPPORTED_TYPE); + } +} + +folly::Future GlobalDataAdaptor::Delete(const std::string &key) { + auto &policy = GetCachePolicy(key); + if (policy.write_cache_type == NOCACHE) { + InvalidateMetaCacheEntry(key); + return base_adaptor_->Delete(key); + } else { + auto meta_cache_entry = GetMetaCacheEntry(key); + auto size = meta_cache_entry->size; + bool present = meta_cache_entry->present; + bool has_write_cache = false; + + if (!present) { + auto result = etcd_client_->GetJson(key).get(); + if (result.status == OK) { + size = result.root["size"].asInt64(); + has_write_cache = true; + } else if (result.status == NOT_FOUND) { // 只在 S3 里存储 + std::map headers; + int ret = base_adaptor_->Head(key, size, headers).get(); + if (ret) return ret; + } else { + return folly::makeFuture(result.status); + } + } + + InvalidateMetaCacheEntry(key); + + if (has_write_cache) { + return base_adaptor_->Delete(key) + .then(std::bind(&ReadCacheClient::Invalidate, read_cache_.get(), key, size)) + .then(std::bind(&EtcdClient::DeleteJson, etcd_client_.get(), key)); + } else { + return base_adaptor_->Delete(key) + .then(std::bind(&ReadCacheClient::Invalidate, read_cache_.get(), key, size)); + } + } +} + +struct DeepFlushArgs { + DeepFlushArgs(const std::string &key) : key(key) {} + ~DeepFlushArgs() { if (buffer.data) delete []buffer.data; } + + std::string key; + std::map headers; + ByteBuffer buffer; +}; + +folly::Future GlobalDataAdaptor::DeepFlush(const std::string &key) { + butil::Timer *t = new butil::Timer(); + t->start(); + auto &policy = GetCachePolicy(key); + if (policy.write_cache_type == REPLICATION || policy.write_cache_type == REED_SOLOMON) { + auto args = std::make_shared(key); + return etcd_client_->GetJson(key).then([this, t, args](folly::Try &&output) -> folly::Future { + if (!output.hasValue()) { + return folly::makeFuture(FOLLY_ERROR); + } + if (output.value().status != OK) { + return folly::makeFuture(output.value().status); + } + auto &root = output.value().root; + args->buffer.len = root["size"].asInt64(); + args->buffer.data = new char[args->buffer.len]; + for (auto iter = root["headers"].begin(); iter != root["headers"].end(); iter++) { + args->headers[iter.key().asString()] = (*iter).asString(); + } + t->stop(); + LOG(INFO) << "DeepFlush phase 1: " << t->u_elapsed(); + + return DownLoad(args->key, 0, args->buffer.len, args->buffer); + }).then([this, t, args](folly::Try &&output) -> folly::Future { + int res = output.value_or(FOLLY_ERROR); + t->stop(); + LOG(INFO) << "DeepFlush phase 2: " << t->u_elapsed(); + if (res != OK) { + return folly::makeFuture(res); + } else { + return base_adaptor_->UpLoad(args->key, args->buffer.len, args->buffer, args->headers); + } + }).then([this, t, key, args](folly::Try &&output) -> folly::Future { + t->stop(); + LOG(INFO) << "DeepFlush phase 3: " << t->u_elapsed(); + int res = output.value_or(FOLLY_ERROR); + if (res != OK) { + return folly::makeFuture(res); + } else { + InvalidateMetaCacheEntry(key); + return etcd_client_->DeleteJson(key); + } + }); + } else { + t->stop(); + LOG(INFO) << "DeepFlush phase 4: " << t->u_elapsed(); + return folly::makeFuture(OK); + } +} + +struct HeadArgs { + HeadArgs(const std::string &key, size_t &size, std::map &headers) + : key(key), size(size), headers(headers) {} + + std::string key; + size_t &size; + std::map &headers; +}; + +folly::Future GlobalDataAdaptor::Head(const std::string &key, + size_t &size, + std::map &headers) { + auto &policy = GetCachePolicy(key); + auto meta_cache_entry = GetMetaCacheEntry(key); + if (meta_cache_entry->present) { + if (!meta_cache_entry->existed) { + LOG(ERROR) << "Request for potential deleted file: " << key; + return folly::makeFuture(NOT_FOUND); + } + size = meta_cache_entry->size; + headers = meta_cache_entry->headers; + return folly::makeFuture(OK); + } + + if (policy.write_cache_type == REPLICATION || policy.write_cache_type == REED_SOLOMON) { + auto args = std::make_shared(key, size, headers); + return etcd_client_->GetJson(key).then([this, args, meta_cache_entry](folly::Try &&output) -> folly::Future { + if (!output.hasValue()) { + return folly::makeFuture(FOLLY_ERROR); + } + if (output.value().status != OK) { + return folly::makeFuture(output.value().status); + } + auto &root = output.value().root; + args->size = root["size"].asInt64(); + for (auto iter = root["headers"].begin(); iter != root["headers"].end(); iter++) { + args->headers[iter.key().asString()] = (*iter).asString(); + } + + if (GetGlobalConfig().use_meta_cache) { + meta_cache_entry->present = true; + meta_cache_entry->existed = true; + meta_cache_entry->write_cached = true; + meta_cache_entry->size = args->size; + meta_cache_entry->headers = args->headers; + meta_cache_entry->root = output.value().root; + } + + return folly::makeFuture(OK); + }).then([this, args, meta_cache_entry](folly::Try &&output) -> folly::Future { + int res = output.value_or(FOLLY_ERROR); + if (res != NOT_FOUND) { + return folly::makeFuture(res); + } else { + return base_adaptor_->Head(args->key, args->size, args->headers).thenValue([args, meta_cache_entry](int &&res) -> int { + if (GetGlobalConfig().use_meta_cache && (res == OK || res == NOT_FOUND)) { + meta_cache_entry->present = true; + meta_cache_entry->existed = (res == OK); + meta_cache_entry->write_cached = false; + meta_cache_entry->size = args->size; + meta_cache_entry->headers = args->headers; + } + return res; + }); + } + }); + } else { + return base_adaptor_->Head(key, size, headers).thenValue([meta_cache_entry, &size, &headers](int &&res) -> int { + if (GetGlobalConfig().use_meta_cache && (res == OK || res == NOT_FOUND)) { + meta_cache_entry->present = true; + meta_cache_entry->existed = (res == OK); + meta_cache_entry->write_cached = false; + meta_cache_entry->size = size; + meta_cache_entry->headers = headers; + } + return res; + }); + } +} + +void GlobalDataAdaptor::InvalidateMetaCache() { + std::lock_guard lock(meta_cache_mutex_); + meta_cache_.clear(); +} + +void GlobalDataAdaptor::InvalidateMetaCacheEntry(const std::string &key) { + std::lock_guard lock(meta_cache_mutex_); + meta_cache_.erase(key); +} + +std::shared_ptr GlobalDataAdaptor::GetMetaCacheEntry(const std::string &key) { + std::lock_guard lock(meta_cache_mutex_); + auto iter = meta_cache_.find(key); + if (iter == meta_cache_.end()) { + auto entry = std::make_shared(key); + meta_cache_.insert(key, entry); + return entry; + } else { + return iter->second; + } +} + + +void GlobalDataAdaptor::SetCachePolicy(const std::string &key, CachePolicy &policy) { + // ... +} + +const CachePolicy &GlobalDataAdaptor::GetCachePolicy(const std::string &key) const { + return GetGlobalConfig().default_policy; +} + +std::shared_ptr GlobalDataAdaptor::GetRpcClient() const { + return rpc_client_[lrand48() % rpc_client_.size()]; +} + +int GlobalDataAdaptor::PerformGarbageCollection(const std::string &prefix) { + LOG(INFO) << "==================GC START==================="; + butil::Timer t; + t.start(); + + std::vector write_cache_ts; + std::set skipped_server_id_list; + for (int server_id = 0; server_id < server_list_.size(); ++server_id) { + auto res = GetRpcClient()->QueryTsFromWriteCache(server_id).get(); + if (res.status != OK) { + std::cerr << RED << "Skip recycling write cache data in server " << server_id << WHITE << std::endl; + skipped_server_id_list.insert(server_id); + } + write_cache_ts.push_back(res.timestamp); + LOG(INFO) << "TS for server " << server_id << ": " << res.timestamp; + } + + t.stop(); + LOG(INFO) << "Flush stage 1: " << t.u_elapsed(); + + if (server_list_.size() == skipped_server_id_list.size()) { + std::cerr << RED << "All servers are not available." << WHITE << std::endl; + return RPC_FAILED; + } + + std::vector key_list; + int rc = etcd_client_->ListJson(prefix, key_list).get(); + if (rc) { + std::cerr << RED << "Failed to list metadata in write cache. " + << "Check the availability of etcd server." << WHITE << std::endl; + return rc; + } + + for (auto &key : key_list) { + LOG(INFO) << "Found entry: " << key; + } + + t.stop(); + LOG(INFO) << "Flush stage 2: " << t.u_elapsed(); + + std::vector> future_list; + for (auto &key : key_list) { + future_list.emplace_back(DeepFlush(key)); + } + + auto output = folly::collectAll(future_list).get(); + for (auto &entry: output) + if (entry.value_or(FOLLY_ERROR) != OK) { + LOG(ERROR) << "Cannot flush data to S3 storage"; + } + + t.stop(); + LOG(INFO) << "Flush stage 3: " << t.u_elapsed(); + + // Recheck the JSON metadata from etcd server + rc = etcd_client_->ListJson(prefix, key_list).get(); + if (rc != 0 && rc != NOT_FOUND) { + return rc; + } + + t.stop(); + LOG(INFO) << "Flush stage 4: " << t.u_elapsed(); + + std::unordered_map> preserve_chunk_keys_map; + for (auto &key : key_list) { + auto resp = etcd_client_->GetJson(key).get(); + if (resp.status) { + continue; + } + + std::vector replicas; + for (auto &entry : resp.root["replica"]) { + replicas.push_back(entry.asInt()); + } + + std::vector internal_keys; + for (auto &entry : resp.root["path"]) { + internal_keys.push_back(entry.asString()); + } + + assert(!replicas.empty() && !internal_keys.empty()); + for (int i = 0; i < internal_keys.size(); ++i) { + preserve_chunk_keys_map[replicas[i % replicas.size()]].push_back(internal_keys[i]); + } + } + + for (int server_id = 0; server_id < server_list_.size(); ++server_id) { + if (skipped_server_id_list.count(server_id)) { + continue; + } + + std::vector except_keys; + if (preserve_chunk_keys_map.count(server_id)) { + except_keys = preserve_chunk_keys_map[server_id]; + } + + int rc = GetRpcClient()->DeleteEntryFromWriteCache(server_id, + prefix, + write_cache_ts[server_id], + except_keys).get(); + if (rc) { + LOG(WARNING) << "Cannot delete unused entries from write cache. Server id: " << server_id; + } + } + + t.stop(); + LOG(INFO) << "Flush stage 5: " << t.u_elapsed(); + + LOG(INFO) << "==================GC END==================="; + return 0; +} + +folly::Future GlobalDataAdaptor::UpLoadPart(const std::string &key, + size_t off, + size_t size, + const ByteBuffer &buffer, + const std::map &headers, + Json::Value& root) { +#ifdef CONFIG_GC_ON_EXCEEDING_DISKSPACE + return DoUpLoadPart(key, off, size, buffer, headers, root) + .thenValue([this, key, off, size, &buffer, &headers, &root](int &&res) -> int { + if (res != NO_ENOUGH_DISKSPACE) { + return res; + } + LOG(INFO) << "Disk limit exceeded - perform GC immediately"; + res = PerformGarbageCollection(); + if (res) { + LOG(WARNING) << "GC failed"; + return res; + } + LOG(INFO) << "Disk limit exceeded - GC completed, now retry"; + return DoUpLoadPart(key, off, size, buffer, headers, root).get(); + }); +#else + return DoUpLoadPart(key, off, size, buffer, headers, root); +#endif +} + +folly::Future GlobalDataAdaptor::DoUpLoadPart(const std::string &key, + size_t off, + size_t size, + const ByteBuffer &buffer, + const std::map &headers, + Json::Value& root) { + butil::Timer *t = new butil::Timer(); + t->start(); + auto &policy = GetCachePolicy(key); + auto pre_op = read_cache_->Invalidate(key, off + size); + if (policy.write_cache_type == REPLICATION || policy.write_cache_type == REED_SOLOMON) { + auto write_cache = policy.write_cache_type == REPLICATION + ? write_caches_[WC_TYPE_REPLICATION] + : write_caches_[WC_TYPE_REEDSOLOMON]; + return std::move(pre_op) + .then(std::bind(&WriteCacheClient::Put, write_cache.get(), key, size, buffer, headers, off)) + .then([this, t, &root] (folly::Try output) -> folly::Future { + int status = output.hasValue() ? output.value().status : FOLLY_ERROR; + if (status == OK) { + root = std::move(output.value().root); + t->stop(); + delete t; + } + return folly::makeFuture(status); + }); + } else { + LOG(ERROR) << "Failed to upload data, reason: unsuppported type, key: " << key + << ", size: " << size + << ", type: " << policy.write_cache_type; + return folly::makeFuture(UNSUPPORTED_TYPE); + } +} + +folly::Future GlobalDataAdaptor::Completed(const std::string &key, + const std::vector &roots, + size_t size) { + if (!roots.empty()) { + auto meta_cache_entry = GetMetaCacheEntry(key); + meta_cache_entry->present = false; + + Json::Value json_path(Json::arrayValue); + for (int i=0; iPutJson(key, new_root); + } + return folly::makeFuture(OK); +} diff --git a/global_cache/GlobalDataAdaptor.h b/global_cache/GlobalDataAdaptor.h new file mode 100644 index 0000000..183b7cb --- /dev/null +++ b/global_cache/GlobalDataAdaptor.h @@ -0,0 +1,143 @@ +#ifndef MADFS_GLOBAL_DATA_ADAPTOR_H +#define MADFS_GLOBAL_DATA_ADAPTOR_H + +#include +#include +#include + +#include "data_adaptor.h" +#include "EtcdClient.h" +#include "ReadCacheClient.h" +#include "WriteCacheClient.h" +#include "GlobalCacheClient.h" + +#define NUM_WC_TYPES 2 +#define WC_TYPE_REPLICATION 0 +#define WC_TYPE_REEDSOLOMON 1 + +using HybridCache::ByteBuffer; +using HybridCache::DataAdaptor; + +class GlobalDataAdaptor : public DataAdaptor { + friend class ReadCacheClient; + + friend class ReplicationWriteCacheClient; + friend class ErasureCodingWriteCacheClient; + +public: + GlobalDataAdaptor(std::shared_ptr base_adaptor, + const std::vector &server_list, + std::shared_ptr etcd_client = nullptr, + std::shared_ptr executor = nullptr); + + ~GlobalDataAdaptor(); + + // 从数据服务器加载数据 + virtual folly::Future DownLoad(const std::string &key, + size_t start, + size_t size, + ByteBuffer &buffer); + + folly::Future DownLoadFromGlobalCache(const std::string &key, + size_t start, + size_t size, + ByteBuffer &buffer); + + // 上传数据到数据服务器 + virtual folly::Future UpLoad(const std::string &key, + size_t size, + const ByteBuffer &buffer, + const std::map &headers); + + virtual folly::Future DoUpLoad(const std::string &key, + size_t size, + const ByteBuffer &buffer, + const std::map &headers); + + virtual folly::Future UpLoadPart(const std::string &key, + size_t off, + size_t size, + const ByteBuffer &buffer, + const std::map &headers, + Json::Value& root); + + virtual folly::Future DoUpLoadPart(const std::string &key, + size_t off, + size_t size, + const ByteBuffer &buffer, + const std::map &headers, + Json::Value& root); + + virtual folly::Future Completed(const std::string &key, + const std::vector &roots, + size_t size); + + // 删除数据服务器的数据 + virtual folly::Future Delete(const std::string &key); + + // 数据源flush到S3(全局缓存用) + virtual folly::Future DeepFlush(const std::string &key); + + // 获取数据的元数据 + virtual folly::Future Head(const std::string &key, + size_t &size, + std::map &headers); + + int PerformGarbageCollection(const std::string &prefix = ""); + + void SetCachePolicy(const std::string &key, CachePolicy &policy); + +public: + struct MetaCacheEntry { + MetaCacheEntry(const std::string &key) : key(key), present(false) {} + + const std::string key; + bool present; // 只有设为 true,这个缓存才有效 + bool existed; // key 目前是存在的 + bool write_cached; // key 的数据位于全局写缓存 + size_t size; + std::map headers; + Json::Value root; + }; + + void InvalidateMetaCache(); + + void InvalidateMetaCacheEntry(const std::string &key); + + std::shared_ptr GetMetaCacheEntry(const std::string &key); + + const CachePolicy &GetCachePolicy(const std::string &key) const; + + std::shared_ptr GetRpcClient() const; + + const std::string GetServerHostname(int server_id) const { + if (server_id >= 0 && server_id < server_list_.size()) + return server_list_[server_id]; + return ""; + }; + + void BackgroundWorker(); + +private: + std::shared_ptr executor_; + + std::shared_ptr read_cache_; + std::shared_ptr write_caches_[NUM_WC_TYPES]; + + std::shared_ptr base_adaptor_; + + std::vector> rpc_client_; + std::shared_ptr etcd_client_; + std::vector server_list_; + + std::mutex meta_cache_mutex_; + folly::EvictingCacheMap> meta_cache_; + + std::atomic bg_running_; + std::thread bg_thread_; + std::mutex bg_mutex_; + std::condition_variable bg_cv_; + std::vector> bg_tasks_; +}; + +#endif // MADFS_GLOBAL_DATA_ADAPTOR_H \ No newline at end of file diff --git a/global_cache/Placement.h b/global_cache/Placement.h new file mode 100644 index 0000000..b8940a5 --- /dev/null +++ b/global_cache/Placement.h @@ -0,0 +1,15 @@ +#ifndef MADFS_PLACEMENT_H +#define MADFS_PLACEMENT_H + +#include +#include "Common.h" + +inline static std::vector Placement(const std::string &key, int num_available, int num_choose) { + uint64_t seed = std::hash < std::string > {}(key); + std::vector output; + for (int i = 0; i < std::min(num_available, num_choose); ++i) + output.push_back((seed + i) % num_available); + return output; +} + +#endif // MADFS_PLACEMENT_H \ No newline at end of file diff --git a/global_cache/ReadCache.cpp b/global_cache/ReadCache.cpp new file mode 100644 index 0000000..4fc67ac --- /dev/null +++ b/global_cache/ReadCache.cpp @@ -0,0 +1,215 @@ +#include +#include +#include +#include + +#define BRPC_WITH_RDMA 1 +#include + +#include "ReadCache.h" +#include "FileSystemDataAdaptor.h" + +bvar::LatencyRecorder g_latency_readcache4cachelib_get("readcache4cachelib_get"); + +class ReadCache4Cachelib : public ReadCacheImpl { +public: + explicit ReadCache4Cachelib(std::shared_ptr executor, + std::shared_ptr base_adaptor = nullptr); + + ~ReadCache4Cachelib() {} + + virtual Future Get(const std::string &key, uint64_t start, uint64_t length); + + virtual int Put(const std::string &key, uint64_t length, const butil::IOBuf &buf); + + virtual int Delete(const std::string &key); + + virtual int Delete(const std::string &key, uint64_t chunk_size, uint64_t max_chunk_id); + +private: + std::shared_ptr executor_; + std::shared_ptr base_adaptor_; + std::shared_ptr impl_; +}; + +ReadCache4Cachelib::ReadCache4Cachelib(std::shared_ptr executor, + std::shared_ptr base_adaptor) + : executor_(executor), base_adaptor_(base_adaptor) { + HybridCache::EnableLogging = false; + impl_ = std::make_shared(GetGlobalConfig().read_cache, + base_adaptor_, + executor); +} + +Future ReadCache4Cachelib::Get(const std::string &key, uint64_t start, uint64_t length) { + butil::Timer *t = new butil::Timer(); + t->start(); +#ifndef BRPC_WITH_RDMA + auto wrap = HybridCache::ByteBuffer(new char[length], length); +#else + auto wrap = HybridCache::ByteBuffer((char *) brpc::rdma::AllocBlock(length), length); +#endif + return impl_->Get(key, start, length, wrap).thenValue([wrap, key, start, length, t](int res) -> GetOutput { + t->stop(); + g_latency_readcache4cachelib_get << t->u_elapsed(); + delete t; + GetOutput output; + output.status = res; +#ifndef BRPC_WITH_RDMA + if (res == OK) { + output.buf.append(wrap.data, wrap.len); + } + delete []wrap.data; +#else + if (res == OK) { + output.buf.append_user_data(wrap.data, wrap.len, brpc::rdma::DeallocBlock); + } else { + brpc::rdma::DeallocBlock(wrap.data); + } +#endif + LOG_IF(INFO, FLAGS_verbose) << "Get key: " << key + << ", start: " << start + << ", length: " << length + << ", status: " << res; + return output; + }); +} + +int ReadCache4Cachelib::Put(const std::string &key, uint64_t length, const butil::IOBuf &buf) { + auto data_len = buf.length(); + auto aux_buffer = malloc(data_len); + auto data = buf.fetch(aux_buffer, data_len); + auto wrap = HybridCache::ByteBuffer((char *) data, data_len); + int res = impl_->Put(key, 0, length, wrap); + free(aux_buffer); + LOG_IF(INFO, FLAGS_verbose) << "Put key: " << key + << ", length: " << length + << ", status: " << res; + return res; +} + +int ReadCache4Cachelib::Delete(const std::string &key) { + LOG_IF(INFO, FLAGS_verbose) << "Delete key: " << key; + return impl_->Delete(key); +} + +int ReadCache4Cachelib::Delete(const std::string &key, uint64_t chunk_size, uint64_t max_chunk_id) { + LOG_IF(INFO, FLAGS_verbose) << "Delete key: " << key; + for (uint64_t chunk_id = 0; chunk_id < max_chunk_id; chunk_id++) { + auto internal_key = key + "-" + std::to_string(chunk_id) + "-" + std::to_string(chunk_size); + int ret = impl_->Delete(internal_key); + if (ret) { + return ret; + } + } + return OK; +} + +bvar::LatencyRecorder g_latency_readcache4disk_get("readcache4disk_get"); + +// ---------------------------------------------------------------------------- +class ReadCache4Disk : public ReadCacheImpl { +public: + explicit ReadCache4Disk(std::shared_ptr executor, + std::shared_ptr base_adaptor = nullptr); + + ~ReadCache4Disk() {} + + virtual Future Get(const std::string &key, uint64_t start, uint64_t length); + + virtual int Put(const std::string &key, uint64_t length, const butil::IOBuf &buf); + + virtual int Delete(const std::string &key); + + virtual int Delete(const std::string &key, uint64_t chunk_size, uint64_t max_chunk_id); + +private: + std::shared_ptr executor_; + std::shared_ptr base_adaptor_; + std::shared_ptr cache_fs_adaptor_; +}; + +ReadCache4Disk::ReadCache4Disk(std::shared_ptr executor, + std::shared_ptr base_adaptor) + : executor_(executor), base_adaptor_(base_adaptor) { + cache_fs_adaptor_ = std::make_shared(GetGlobalConfig().read_cache_dir, base_adaptor_, true, executor_); +} + +Future ReadCache4Disk::Get(const std::string &key, uint64_t start, uint64_t length) { + butil::Timer *t = new butil::Timer(); + t->start(); +#ifndef BRPC_WITH_RDMA + auto wrap = HybridCache::ByteBuffer(new char[length], length); +#else + auto wrap = HybridCache::ByteBuffer((char *) brpc::rdma::AllocBlock(length), length); +#endif + return cache_fs_adaptor_->DownLoad(key, start, length, wrap).thenValue([wrap, key, start, length, t](int res) -> GetOutput { + GetOutput output; + output.status = res; +#ifndef BRPC_WITH_RDMA + if (res == OK) { + output.buf.append(wrap.data, wrap.len); + } + delete []wrap.data; +#else + if (res == OK) { + output.buf.append_user_data(wrap.data, wrap.len, brpc::rdma::DeallocBlock); + } else { + brpc::rdma::DeallocBlock(wrap.data); + } +#endif + t->stop(); + g_latency_readcache4disk_get << t->u_elapsed(); + delete t; + LOG_IF(INFO, FLAGS_verbose) << "Get key: " << key + << ", start: " << start + << ", length: " << length + << ", status: " << res; + return output; + }); +} + +int ReadCache4Disk::Put(const std::string &key, uint64_t length, const butil::IOBuf &buf) { + auto data_len = buf.length(); + auto aux_buffer = malloc(data_len); + auto data = buf.fetch(aux_buffer, data_len); + auto wrap = HybridCache::ByteBuffer((char *) data, data_len); + std::map headers; + int res = cache_fs_adaptor_->UpLoad(key, length, wrap, headers).get(); + free(aux_buffer); + LOG_IF(INFO, FLAGS_verbose) << "Put key: " << key + << ", length: " << length + << ", status: " << res; + return res; +} + +int ReadCache4Disk::Delete(const std::string &key) { + LOG_IF(INFO, FLAGS_verbose) << "Delete key: " << key; + return cache_fs_adaptor_->Delete(key).get(); +} + +int ReadCache4Disk::Delete(const std::string &key, uint64_t chunk_size, uint64_t max_chunk_id) { + LOG_IF(INFO, FLAGS_verbose) << "Delete key: " << key; + for (uint64_t chunk_id = 0; chunk_id < max_chunk_id; chunk_id++) { + auto internal_key = key + "-" + std::to_string(chunk_id) + "-" + std::to_string(chunk_size); + int ret = cache_fs_adaptor_->Delete(internal_key).get(); + if (ret) { + return ret; + } + } + return OK; +} + +DEFINE_string(read_cache_engine, "cachelib", "Read cache engine: cachelib | disk"); + +ReadCache::ReadCache(std::shared_ptr executor, + std::shared_ptr base_adaptor) { + if (FLAGS_read_cache_engine == "cachelib") + impl_ = new ReadCache4Cachelib(executor, base_adaptor); + else if (FLAGS_read_cache_engine == "disk") + impl_ = new ReadCache4Disk(executor, base_adaptor); + else { + LOG(FATAL) << "unsupported read cache engine"; + exit(EXIT_FAILURE); + } +} \ No newline at end of file diff --git a/global_cache/ReadCache.h b/global_cache/ReadCache.h new file mode 100644 index 0000000..5f1a686 --- /dev/null +++ b/global_cache/ReadCache.h @@ -0,0 +1,53 @@ +#ifndef MADFS_READ_CACHE_H +#define MADFS_READ_CACHE_H + +#include +#include +#include + +#include + +#include "Common.h" +#include "data_adaptor.h" +#include "read_cache.h" + +using HybridCache::DataAdaptor; + +class ReadCacheImpl { +public: + virtual Future Get(const std::string &key, uint64_t start, uint64_t length) = 0; + virtual int Put(const std::string &key, uint64_t length, const butil::IOBuf &buf) = 0; + virtual int Delete(const std::string &key) = 0; + virtual int Delete(const std::string &key, uint64_t chunk_size, uint64_t max_chunk_id) = 0; +}; + +class ReadCache { +public: + explicit ReadCache(std::shared_ptr executor, + std::shared_ptr base_adaptor = nullptr); + + ~ReadCache() { + delete impl_; + } + + Future Get(const std::string &key, uint64_t start, uint64_t length) { + return impl_->Get(key, start, length); + } + + int Put(const std::string &key, uint64_t length, const butil::IOBuf &buf) { + return impl_->Put(key, length, buf); + } + + int Delete(const std::string &key) { + return impl_->Delete(key); + } + + int Delete(const std::string &key, uint64_t chunk_size, uint64_t max_chunk_id) { + return impl_->Delete(key, chunk_size, max_chunk_id); + } + +private: + ReadCacheImpl *impl_; +}; + +#endif // MADFS_READ_CACHE_H \ No newline at end of file diff --git a/global_cache/ReadCacheClient.cpp b/global_cache/ReadCacheClient.cpp new file mode 100644 index 0000000..3ffc4f7 --- /dev/null +++ b/global_cache/ReadCacheClient.cpp @@ -0,0 +1,245 @@ +#include "ReadCacheClient.h" +#include "GlobalDataAdaptor.h" + +#define AWS_BUFFER_PADDING 64 + +ReadCacheClient::ReadCacheClient(GlobalDataAdaptor *parent) + : parent_(parent) {} + +ReadCacheClient::~ReadCacheClient() {} + +folly::Future ReadCacheClient::Get(const std::string &key, size_t start, size_t size, ByteBuffer &buffer) { + butil::Timer t; + t.start(); + LOG_IF(INFO, FLAGS_verbose) << "Get key=" << key << ", start=" << start << ", size=" << size; + std::vector> future_list; + std::vector requests; + auto &policy = parent_->GetCachePolicy(key); + const int num_choose = policy.read_replication_factor; + GenerateGetChunkRequestsV2(key, start, size, buffer, requests, policy.read_chunk_size); + if (requests.empty()) + return folly::makeFuture(OK); + + auto DoGetChunkAsync = [this, num_choose](GetChunkRequestV2 &entry) -> folly::Future { + auto replicas = GetReplica(entry.internal_key, num_choose); + int primary_server_id = replicas[lrand48() % replicas.size()]; + return GetChunkAsync(primary_server_id, entry).thenValue([this, replicas, entry, primary_server_id](int res) -> int { + if (res != RPC_FAILED) { + return res; + } + LOG_EVERY_SECOND(WARNING) << "Unable to connect primary replicas. server_id " << primary_server_id + << ", hostname: " << parent_->GetServerHostname(primary_server_id); + for (auto &server_id : replicas) { + if (server_id == primary_server_id) { + continue; + } + res = GetChunkAsync(server_id, entry).get(); + if (res != RPC_FAILED) { + return res; + } + LOG_EVERY_SECOND(WARNING) << "Unable to connect secondary replicas. server_id " << server_id + << ", hostname: " << parent_->GetServerHostname(server_id); + } + LOG_EVERY_SECOND(ERROR) << "Unable to connect all target replicas"; + return RPC_FAILED; + }); + }; + + if (requests.size() == 1) { + return DoGetChunkAsync(requests[0]); + } + + size_t aggregated_size = 0; + for (auto &entry: requests) { + aggregated_size += entry.chunk_len; + future_list.emplace_back(DoGetChunkAsync(entry)); + if (aggregated_size >= GetGlobalConfig().max_inflight_payload_size) { + auto output = folly::collectAll(future_list).get(); + for (auto &entry: output) + if (entry.value_or(FOLLY_ERROR) != OK) { + LOG(ERROR) << "Failed to get data from read cache, key: " << key + << ", start: " << start + << ", size: " << size + << ", buf: " << (void *) buffer.data << " " << buffer.len + << ", error code: " << entry.value_or(FOLLY_ERROR); + return entry.value_or(FOLLY_ERROR); + } + future_list.clear(); + } + } + + if (future_list.empty()) return OK; + + return folly::collectAll(future_list).via(parent_->executor_.get()).thenValue( + [=](std::vector > output) -> int { + for (auto &entry: output) + if (entry.value_or(FOLLY_ERROR) != OK) { + LOG(ERROR) << "Failed to get data from read cache, key: " << key + << ", start: " << start + << ", size: " << size + << ", buf: " << (void *) buffer.data << " " << buffer.len + << ", error code: " << entry.value_or(FOLLY_ERROR); + return entry.value_or(FOLLY_ERROR); + } + return OK; + }); +} + +folly::Future ReadCacheClient::GetChunkAsync(int server_id, GetChunkRequestV2 request) { + LOG_IF(INFO, FLAGS_verbose) << "GetChunkAsync server_id=" << server_id + << ", internal_key=" << request.internal_key + << ", chunk_id=" << request.chunk_id + << ", chunk_start=" << request.chunk_start + << ", chunk_len=" << request.chunk_len + << ", buffer=" << (void *) request.buffer.data; + return parent_->GetRpcClient()->GetEntryFromReadCache(server_id, request.internal_key, request.chunk_start, request.chunk_len) + .then([this, server_id, request](folly::Try &&output) -> folly::Future { + if (!output.hasValue()) { + return folly::makeFuture(FOLLY_ERROR); + } + auto &value = output.value(); + if (value.status == OK) { + value.buf.copy_to(request.buffer.data, request.buffer.len); + return folly::makeFuture(OK); + } else if (value.status == CACHE_ENTRY_NOT_FOUND) { + return GetChunkFromGlobalCache(server_id, request); + } else { + return folly::makeFuture(value.status); + } + }); +} + +folly::Future ReadCacheClient::GetChunkFromGlobalCache(int server_id, GetChunkRequestV2 request) { + struct Args { + size_t size; + std::map headers; + ByteBuffer data_buf; + + ~Args() { + if (data_buf.data) { + delete []data_buf.data; + } + } + }; + auto args = std::make_shared(); + // auto f = parent_->base_adaptor_->Head(request.user_key, args->size, args->headers) + auto f = parent_->Head(request.user_key, args->size, args->headers) + .then([this, args, request] (folly::Try &&output) -> folly::Future { + if (output.value_or(FOLLY_ERROR) != OK) { + return folly::makeFuture(output.value_or(FOLLY_ERROR)); + } + + const size_t align_chunk_start = request.chunk_id * request.chunk_granularity; + const size_t align_chunk_stop = std::min(align_chunk_start + request.chunk_granularity, args->size); + + if (align_chunk_start + request.chunk_start + request.chunk_len > args->size) { + LOG(WARNING) << "Requested data range exceeds object size, key: " << request.user_key + << " request offset: " << align_chunk_start + request.chunk_start + request.chunk_len + << ", size: " << args->size; + return folly::makeFuture(END_OF_FILE); + } else if (align_chunk_start == align_chunk_stop) { + return folly::makeFuture(OK); + } else if (align_chunk_start > align_chunk_stop) { + LOG(WARNING) << "Unexpected request range, key: " << request.user_key + << " start offset: " << align_chunk_start + << ", end offset: " << align_chunk_stop; + return folly::makeFuture(INVALID_ARGUMENT); + } + + args->data_buf.len = align_chunk_stop - align_chunk_start + AWS_BUFFER_PADDING; + args->data_buf.data = new char[args->data_buf.len]; + return parent_->base_adaptor_->DownLoad(request.user_key, + align_chunk_start, + align_chunk_stop - align_chunk_start, + args->data_buf); + }).then([this, args, request] (folly::Try &&output) -> folly::Future { + if (output.value_or(FOLLY_ERROR) != OK) { + return folly::makeFuture(output.value_or(FOLLY_ERROR)); + } + + memcpy(request.buffer.data, args->data_buf.data + request.chunk_start, request.chunk_len); + args->data_buf.len -= AWS_BUFFER_PADDING; + auto &policy = parent_->GetCachePolicy(request.user_key); + auto replicas = GetReplica(request.internal_key, policy.read_replication_factor); + std::vector > future_list; + for (auto server_id: replicas) + future_list.emplace_back(parent_->GetRpcClient()->PutEntryFromReadCache(server_id, + request.internal_key, + args->data_buf, + args->data_buf.len)); + return folly::collectAll(std::move(future_list)).via(parent_->executor_.get()).thenValue( + [](std::vector > &&output) -> int { + for (auto &entry: output) { + if (!entry.hasValue()) + return FOLLY_ERROR; + if (entry.value().status != OK) + return entry.value().status; + } + return OK; + }); + }); + return f; +} + +folly::Future ReadCacheClient::Invalidate(const std::string &key, size_t size) { + // LOG(INFO) << "Invalidate key=" << key; + std::vector > future_list; + auto &policy = parent_->GetCachePolicy(key); + const size_t chunk_size = policy.read_chunk_size; + const size_t end_chunk_id = (size + chunk_size - 1) / chunk_size; + for (int server_id = 0; server_id < parent_->server_list_.size(); server_id++) { + future_list.emplace_back(parent_->GetRpcClient()->DeleteEntryFromReadCache(server_id, key, chunk_size, end_chunk_id)); + } + return folly::collectAll(future_list).via(parent_->executor_.get()).thenValue( + [](std::vector > output) -> int { + for (auto &entry: output) + if (entry.value_or(FOLLY_ERROR) != OK) + return entry.value_or(FOLLY_ERROR); + return OK; + }); +} + +void ReadCacheClient::GenerateGetChunkRequestsV2(const std::string &key, + size_t start, + size_t size, + ByteBuffer &buffer, + std::vector &requests, + size_t chunk_size) { + const size_t end = start + size; + + const size_t begin_chunk_id = start / chunk_size; + const size_t end_chunk_id = (end + chunk_size - 1) / chunk_size; + + if (buffer.len < size) { + LOG(WARNING) << "Buffer capacity may be not enough, expect " << size << ", actual " << buffer.len; + } + + size_t buffer_offset = 0; + for (size_t chunk_id = begin_chunk_id; chunk_id < end_chunk_id; ++chunk_id) { + size_t chunk_start = std::max(chunk_id * chunk_size, start); + size_t chunk_stop = std::min((chunk_id + 1) * chunk_size, end); + if (chunk_stop <= chunk_start) + return; + GetChunkRequestV2 item; + item.user_key = key; + item.internal_key = key + "-" + std::to_string(chunk_id) + "-" + std::to_string(chunk_size); + item.chunk_id = chunk_id; + item.chunk_start = chunk_start % chunk_size; + item.chunk_len = chunk_stop - chunk_start; + item.chunk_granularity = chunk_size; + item.buffer.data = buffer.data + buffer_offset; + item.buffer.len = item.chunk_len; + buffer_offset += item.chunk_len; + requests.emplace_back(item); + } + LOG_ASSERT(buffer_offset == size); +} + +std::vector ReadCacheClient::GetReplica(const std::string &key, int num_choose) { + const int num_available = parent_->server_list_.size(); + uint64_t seed = std::hash < std::string > {}(key); + std::vector output; + for (int i = 0; i < std::min(num_available, num_choose); ++i) + output.push_back((seed + i) % num_available); + return output; +} \ No newline at end of file diff --git a/global_cache/ReadCacheClient.h b/global_cache/ReadCacheClient.h new file mode 100644 index 0000000..389437c --- /dev/null +++ b/global_cache/ReadCacheClient.h @@ -0,0 +1,60 @@ +#ifndef MADFS_READ_CACHE_CLIENT_H +#define MADFS_READ_CACHE_CLIENT_H + +#include +#include +#include + +#include "Common.h" +#include "Placement.h" +#include "data_adaptor.h" + +using HybridCache::ByteBuffer; + +class GlobalDataAdaptor; + +class ReadCacheClient { + friend class GetChunkContext; + +public: + ReadCacheClient(GlobalDataAdaptor *parent); + + ~ReadCacheClient(); + + virtual folly::Future Get(const std::string &key, + size_t start, + size_t size, + ByteBuffer &buffer); + + virtual folly::Future Invalidate(const std::string &key, size_t size); + + // for testing only +public: + struct GetChunkRequestV2 { + std::string user_key; + std::string internal_key; + size_t chunk_id; + size_t chunk_start; + size_t chunk_len; + size_t chunk_granularity; + ByteBuffer buffer; + }; + + static void GenerateGetChunkRequestsV2(const std::string &key, + size_t start, + size_t size, + ByteBuffer &buffer, + std::vector &requests, + size_t chunk_size); + + folly::Future GetChunkAsync(int server_id, GetChunkRequestV2 context); + + folly::Future GetChunkFromGlobalCache(int server_id, GetChunkRequestV2 context); + + std::vector GetReplica(const std::string &key, int num_choose); + +private: + GlobalDataAdaptor *parent_; +}; + +#endif // MADFS_READ_CACHE_CLIENT_H \ No newline at end of file diff --git a/global_cache/ReplicationWriteCacheClient.cpp b/global_cache/ReplicationWriteCacheClient.cpp new file mode 100644 index 0000000..a2cb1be --- /dev/null +++ b/global_cache/ReplicationWriteCacheClient.cpp @@ -0,0 +1,248 @@ +#include "ReplicationWriteCacheClient.h" +#include "GlobalDataAdaptor.h" + +folly::Future ReplicationWriteCacheClient::Put(const std::string &key, + size_t size, + const ByteBuffer &buffer, + const std::map &headers, + size_t off) { + std::vector > future_list; + Json::Value root, dummy_root; + Json::Value json_replica(Json::arrayValue), json_path(Json::arrayValue), json_headers; + + butil::Timer *t = new butil::Timer(); + t->start(); + + const std::vector replicas = GetReplica(key); + for (auto server_id: replicas) { + json_replica.append(server_id); + } + + auto rpc_client = parent_->GetRpcClient(); + auto write_chunk_size = GetGlobalConfig().write_chunk_size; + + for (auto iter = headers.begin(); iter != headers.end(); ++iter) { + json_headers[iter->first] = iter->second; + } + + size_t aggregated_size = 0; + for (uint64_t offset = 0; offset < size; offset += write_chunk_size) { + for (auto server_id: replicas) { + auto region_size = std::min(size - offset, write_chunk_size); + ByteBuffer region_buffer(buffer.data + offset, region_size); + std::string partial_key = key + + "-" + std::to_string((off + offset) / write_chunk_size) + + "-" + std::to_string(write_chunk_size); + auto PutRPC = folly::via(parent_->executor_.get(), [this, server_id, partial_key, region_buffer, region_size]() -> PutOutput { + return parent_->GetRpcClient()->PutEntryFromWriteCache(server_id, partial_key, region_buffer, region_size).get(); + }); + future_list.emplace_back(std::move(PutRPC)); + } + } + + t->stop(); + LOG(INFO) << "Phase 1: " << t->u_elapsed(); + + root["type"] = "replication"; + root["size"] = size; + root["replica"] = json_replica; + root["headers"] = json_headers; + + return folly::collectAll(future_list).via(parent_->executor_.get()).thenValue([root, t](std::vector> &&output) -> folly::Future { + Json::Value dummy_root; + Json::Value json_path(Json::arrayValue); + for (auto &entry: output) { + if (!entry.hasValue()) + return PutResult { FOLLY_ERROR, dummy_root }; + if (entry.value().status != OK) { + LOG(INFO) << "Found error"; + return PutResult { entry.value().status, dummy_root }; + } + json_path.append(entry.value().internal_key); + } + Json::Value new_root = root; + new_root["path"] = json_path; + t->stop(); + LOG(INFO) << "Duration: " << t->u_elapsed(); + delete t; + return PutResult { OK, new_root }; + }); +} + +folly::Future ReplicationWriteCacheClient::Get(const std::string &key, + size_t start, + size_t size, + ByteBuffer &buffer, + Json::Value &root) { + std::vector replicas; + for (auto &entry : root["replica"]) { + replicas.push_back(entry.asInt()); + } + + std::vector internal_keys; + for (auto &entry : root["path"]) { + internal_keys.push_back(entry.asString()); + } + + std::vector > future_list; + std::vector requests; + auto write_chunk_size = GetGlobalConfig().write_chunk_size; + GenerateGetChunkRequestsV2(key, start, size, buffer, requests, write_chunk_size); + if (requests.empty()) + return folly::makeFuture(OK); + + size_t aggregated_size = 0; + for (auto &entry: requests) { + int primary_replica_id = lrand48() % replicas.size(); + int primary_server_id = replicas[primary_replica_id]; + std::string internal_key = internal_keys[entry.chunk_id * replicas.size() + primary_replica_id]; + future_list.emplace_back(GetChunkAsync(primary_server_id, entry, internal_key) + .thenValue([this, replicas, entry, primary_server_id, internal_keys](int res) -> int { + if (res != RPC_FAILED) { + return res; + } + LOG_EVERY_SECOND(WARNING) << "Unable to connect primary replicas. server_id " << primary_server_id + << ", hostname: " << parent_->GetServerHostname(primary_server_id); + for (auto &server_id : replicas) { + if (server_id == primary_server_id) { + continue; + } + auto internal_key = internal_keys[entry.chunk_id * replicas.size() + server_id]; + res = GetChunkAsync(server_id, entry, internal_key).get(); + if (res != RPC_FAILED) { + return res; + } + LOG_EVERY_SECOND(WARNING) << "Unable to connect secondary replicas. server_id " << server_id + << ", hostname: " << parent_->GetServerHostname(server_id); + } + LOG_EVERY_SECOND(ERROR) << "Unable to connect all target replicas"; + return RPC_FAILED; + })); + + aggregated_size += entry.chunk_len; + if (aggregated_size >= GetGlobalConfig().max_inflight_payload_size) { + auto output = folly::collectAll(future_list).get(); + for (auto &entry: output) + if (entry.value_or(FOLLY_ERROR) != OK) { + LOG(ERROR) << "Failed to get data from write cache, key: " << key + << ", start: " << start + << ", size: " << size + << ", buf: " << (void *) buffer.data << " " << buffer.len + << ", error code: " << entry.hasValue() << " " << entry.value_or(FOLLY_ERROR); + return entry.value_or(FOLLY_ERROR); + } + aggregated_size = 0; + future_list.clear(); + } + } + + return folly::collectAll(future_list).via(parent_->executor_.get()).thenValue( + [=](std::vector > output) -> int { + for (auto &entry: output) + if (entry.value_or(FOLLY_ERROR) != OK) { + LOG(ERROR) << "Failed to get data from write cache, key: " << key + << ", start: " << start + << ", size: " << size + << ", buf: " << (void *) buffer.data << " " << buffer.len + << ", error code: " << entry.hasValue() << " " << entry.value_or(FOLLY_ERROR); + return entry.value_or(FOLLY_ERROR); + } + return OK; + }); + + // return parent_->GetRpcClient()->GetEntryFromWriteCache(replica[primary_index], internal_keys[primary_index], start, size).thenValue( + // [this, &buffer, start, size, replica, internal_keys, primary_index](GetOutput &&output) -> int { + // if (output.status == OK) { + // output.buf.copy_to(buffer.data, size); + // } + // if (output.status == RPC_FAILED) { + // for (int index = 0; index < replica.size(); ++index) { + // if (index == primary_index) { + // continue; + // } + // auto res = parent_->GetRpcClient()->GetEntryFromWriteCache(replica[index], internal_keys[index], start, size).get(); + // if (res.status == OK) { + // output.buf.copy_to(buffer.data, size); + // } + // if (res.status != RPC_FAILED) { + // return res.status; + // } + // } + // LOG(ERROR) << "All target replicas are crashed"; + // return RPC_FAILED; + // } + // return output.status; + // } + // ); +} + +folly::Future ReplicationWriteCacheClient::GetChunkAsync(int server_id, GetChunkRequestV2 request, std::string &internal_key) { + LOG_IF(INFO, FLAGS_verbose) << "GetChunkAsync server_id=" << server_id + << ", internal_key=" << internal_key + << ", chunk_id=" << request.chunk_id + << ", chunk_start=" << request.chunk_start + << ", chunk_len=" << request.chunk_len + << ", buffer=" << (void *) request.buffer.data; + auto f = parent_->GetRpcClient()->GetEntryFromWriteCache(server_id, internal_key, request.chunk_start, request.chunk_len) + .then([this, server_id, request](folly::Try &&output) -> folly::Future { + if (!output.hasValue()) { + return folly::makeFuture(FOLLY_ERROR); + } + auto &value = output.value(); + if (value.status == OK) { + value.buf.copy_to(request.buffer.data, request.buffer.len); + return folly::makeFuture(OK); + } else { + return folly::makeFuture(value.status); + } + }).via(parent_->executor_.get()); + return f; + // memset(request.buffer.data, 'x', request.buffer.len); + // return folly::makeFuture(OK); +} + +std::vector ReplicationWriteCacheClient::GetReplica(const std::string &key) { + const int num_available = parent_->server_list_.size(); + auto &policy = parent_->GetCachePolicy(key); + const int num_choose = policy.write_replication_factor; + uint64_t seed = std::hash < std::string > {}(key); + std::vector output; + for (int i = 0; i < num_choose; ++i) + output.push_back((seed + i) % num_available); + return output; +} + +void ReplicationWriteCacheClient::GenerateGetChunkRequestsV2(const std::string &key, + size_t start, + size_t size, + ByteBuffer &buffer, + std::vector &requests, + size_t chunk_size) { + const size_t end = start + size; + + const size_t begin_chunk_id = start / chunk_size; + const size_t end_chunk_id = (end + chunk_size - 1) / chunk_size; + + if (buffer.len < size) { + LOG(WARNING) << "Buffer capacity may be not enough, expect " << size << ", actual " << buffer.len; + } + + size_t buffer_offset = 0; + for (size_t chunk_id = begin_chunk_id; chunk_id < end_chunk_id; ++chunk_id) { + size_t chunk_start = std::max(chunk_id * chunk_size, start); + size_t chunk_stop = std::min((chunk_id + 1) * chunk_size, end); + if (chunk_stop <= chunk_start) + return; + GetChunkRequestV2 item; + item.user_key = key; + item.chunk_id = chunk_id; + item.chunk_start = chunk_start % chunk_size; + item.chunk_len = chunk_stop - chunk_start; + item.chunk_granularity = chunk_size; + item.buffer.data = buffer.data + buffer_offset; + item.buffer.len = item.chunk_len; + buffer_offset += item.chunk_len; + requests.emplace_back(item); + } + LOG_ASSERT(buffer_offset == size); +} diff --git a/global_cache/ReplicationWriteCacheClient.h b/global_cache/ReplicationWriteCacheClient.h new file mode 100644 index 0000000..1bb8dbf --- /dev/null +++ b/global_cache/ReplicationWriteCacheClient.h @@ -0,0 +1,57 @@ +#ifndef MADFS_REPLICATION_WRITE_CACHE_CLIENT_H +#define MADFS_REPLICATION_WRITE_CACHE_CLIENT_H + +#include "WriteCacheClient.h" + +using HybridCache::ByteBuffer; + +class GlobalDataAdaptor; + +using PutResult = WriteCacheClient::PutResult; + +class ReplicationWriteCacheClient : public WriteCacheClient { + friend class GetChunkContext; + +public: + ReplicationWriteCacheClient(GlobalDataAdaptor *parent) : parent_(parent) {} + + ~ReplicationWriteCacheClient() {} + + virtual folly::Future Put(const std::string &key, + size_t size, + const ByteBuffer &buffer, + const std::map &headers, + size_t off = 0); + + virtual folly::Future Get(const std::string &key, + size_t start, + size_t size, + ByteBuffer &buffer, + Json::Value &root); + +public: + std::vector GetReplica(const std::string &key); + + struct GetChunkRequestV2 { + std::string user_key; + size_t chunk_id; + size_t chunk_start; + size_t chunk_len; + size_t chunk_granularity; + ByteBuffer buffer; + }; + + static void GenerateGetChunkRequestsV2(const std::string &key, + size_t start, + size_t size, + ByteBuffer &buffer, + std::vector &requests, + size_t chunk_size); + + folly::Future GetChunkAsync(int server_id, GetChunkRequestV2 context, std::string &internal_key); + +private: + GlobalDataAdaptor *parent_; +}; + +#endif // MADFS_REPLICATION_WRITE_CACHE_CLIENT_H \ No newline at end of file diff --git a/global_cache/S3DataAdaptor.cpp b/global_cache/S3DataAdaptor.cpp new file mode 100644 index 0000000..ba0e6a2 --- /dev/null +++ b/global_cache/S3DataAdaptor.cpp @@ -0,0 +1,188 @@ +#include "S3DataAdaptor.h" + +#include +#include +#include +#include +#include +#include + +#define STRINGIFY_HELPER(val) #val +#define STRINGIFY(val) STRINGIFY_HELPER(val) +#define AWS_ALLOCATE_TAG __FILE__ ":" STRINGIFY(__LINE__) + +std::once_flag S3INIT_FLAG; +std::once_flag S3SHUTDOWN_FLAG; +Aws::SDKOptions AWS_SDK_OPTIONS; + +// https://github.com/aws/aws-sdk-cpp/issues/1430 +class PreallocatedIOStream : public Aws::IOStream { +public: + PreallocatedIOStream(char *buf, size_t size) + : Aws::IOStream(new Aws::Utils::Stream::PreallocatedStreamBuf( + reinterpret_cast(buf), size)) {} + + PreallocatedIOStream(const char *buf, size_t size) + : PreallocatedIOStream(const_cast(buf), size) {} + + ~PreallocatedIOStream() { + // corresponding new in constructor + delete rdbuf(); + } +}; + +Aws::String GetObjectRequestRange(uint64_t offset, uint64_t len) { + auto range = + "bytes=" + std::to_string(offset) + "-" + std::to_string(offset + len); + return {range.data(), range.size()}; +} + +S3DataAdaptor::S3DataAdaptor() { + auto initSDK = [&]() { + Aws::InitAPI(AWS_SDK_OPTIONS); + }; + std::call_once(S3INIT_FLAG, initSDK); + auto &s3_config = GetGlobalConfig().s3_config; + setenv("AWS_EC2_METADATA_DISABLED", "true", 1); + clientCfg_ = Aws::New(AWS_ALLOCATE_TAG, true); + clientCfg_->scheme = Aws::Http::Scheme::HTTP; + clientCfg_->verifySSL = false; + clientCfg_->maxConnections = 10; + clientCfg_->endpointOverride = s3_config.address; + clientCfg_->executor = Aws::MakeShared("S3Adapter.S3Client", s3_config.bg_threads); + + s3Client_ = Aws::New(AWS_ALLOCATE_TAG, + Aws::Auth::AWSCredentials(s3_config.access_key, s3_config.secret_access_key), + *clientCfg_, + Aws::Client::AWSAuthV4Signer::PayloadSigningPolicy::Never, + false); +} + +S3DataAdaptor::~S3DataAdaptor() { + if (clientCfg_ != nullptr) { + Aws::Delete(clientCfg_); + clientCfg_ = nullptr; + } + if (s3Client_ != nullptr) { + Aws::Delete(s3Client_); + s3Client_ = nullptr; + } + auto shutdownSDK = [&]() { + Aws::ShutdownAPI(AWS_SDK_OPTIONS); + }; + std::call_once(S3SHUTDOWN_FLAG, shutdownSDK); +} + +folly::Future S3DataAdaptor::DownLoad(const std::string &key, + size_t start, + size_t size, + ByteBuffer &buffer) { + Aws::S3::Model::GetObjectRequest request; + request.SetBucket(GetGlobalConfig().s3_config.bucket); + request.SetKey(Aws::String{key.c_str(), key.size()}); + request.SetRange(GetObjectRequestRange(start, size)); + request.SetResponseStreamFactory( + [&buffer]() { return Aws::New(AWS_ALLOCATE_TAG, buffer.data, buffer.len); }); + auto promise = std::make_shared < folly::Promise < int >> (); + Aws::S3::GetObjectResponseReceivedHandler handler = + [&buffer, size, promise]( + const Aws::S3::S3Client */*client*/, + const Aws::S3::Model::GetObjectRequest &/*request*/, + const Aws::S3::Model::GetObjectOutcome &response, + const std::shared_ptr &awsCtx) { + if (response.IsSuccess()) { + promise->setValue(OK); + } else if (response.GetError().GetErrorType() == Aws::S3::S3Errors::NO_SUCH_KEY) { + promise->setValue(NOT_FOUND); + } else { + LOG(ERROR) << "GetObjectAsync error: " + << response.GetError().GetExceptionName() + << "message: " << response.GetError().GetMessage(); + promise->setValue(S3_INTERNAL_ERROR); + } + }; + s3Client_->GetObjectAsync(request, handler, nullptr); + return promise->getFuture(); +} + +folly::Future S3DataAdaptor::UpLoad(const std::string &key, + size_t size, + const ByteBuffer &buffer, + const std::map &headers) { + Aws::S3::Model::PutObjectRequest request; + request.SetBucket(GetGlobalConfig().s3_config.bucket); + request.SetKey(key); + request.SetMetadata(headers); + request.SetBody(Aws::MakeShared(AWS_ALLOCATE_TAG, buffer.data, buffer.len)); + auto promise = std::make_shared < folly::Promise < int >> (); + Aws::S3::PutObjectResponseReceivedHandler handler = + [promise]( + const Aws::S3::S3Client */*client*/, + const Aws::S3::Model::PutObjectRequest &/*request*/, + const Aws::S3::Model::PutObjectOutcome &response, + const std::shared_ptr &awsCtx) { + LOG_IF(ERROR, !response.IsSuccess()) + << "PutObjectAsync error: " + << response.GetError().GetExceptionName() + << "message: " << response.GetError().GetMessage(); + promise->setValue(response.IsSuccess() ? OK : S3_INTERNAL_ERROR); + }; + s3Client_->PutObjectAsync(request, handler, nullptr); + return promise->getFuture(); +} + +folly::Future S3DataAdaptor::Delete(const std::string &key) { + Aws::S3::Model::DeleteObjectRequest request; + request.SetBucket(GetGlobalConfig().s3_config.bucket); + request.SetKey(key); + auto promise = std::make_shared < folly::Promise < int >> (); + Aws::S3::DeleteObjectResponseReceivedHandler handler = + [promise]( + const Aws::S3::S3Client */*client*/, + const Aws::S3::Model::DeleteObjectRequest &/*request*/, + const Aws::S3::Model::DeleteObjectOutcome &response, + const std::shared_ptr &awsCtx) { + if (response.IsSuccess()) { + promise->setValue(OK); + } else if (response.GetError().GetErrorType() == Aws::S3::S3Errors::NO_SUCH_KEY) { + promise->setValue(NOT_FOUND); + } else { + LOG(ERROR) << "DeleteObjectAsync error: " + << response.GetError().GetExceptionName() + << "message: " << response.GetError().GetMessage(); + promise->setValue(S3_INTERNAL_ERROR); + } + }; + s3Client_->DeleteObjectAsync(request, handler, nullptr); + return promise->getFuture(); +} + +folly::Future S3DataAdaptor::Head(const std::string &key, + size_t &size, + std::map &headers) { + Aws::S3::Model::HeadObjectRequest request; + request.SetBucket(GetGlobalConfig().s3_config.bucket); + request.SetKey(key); + auto promise = std::make_shared < folly::Promise < int >> (); + Aws::S3::HeadObjectResponseReceivedHandler handler = + [promise, &size, &headers]( + const Aws::S3::S3Client */*client*/, + const Aws::S3::Model::HeadObjectRequest &/*request*/, + const Aws::S3::Model::HeadObjectOutcome &response, + const std::shared_ptr &awsCtx) { + if (response.IsSuccess()) { + headers = response.GetResult().GetMetadata(); + size = response.GetResult().GetContentLength(); + promise->setValue(OK); + } else if (response.GetError().GetErrorType() == Aws::S3::S3Errors::NO_SUCH_KEY) { + promise->setValue(NOT_FOUND); + } else { + LOG(ERROR) << "HeadObjectAsync error: " + << response.GetError().GetExceptionName() + << "message: " << response.GetError().GetMessage(); + promise->setValue(S3_INTERNAL_ERROR); + } + }; + s3Client_->HeadObjectAsync(request, handler, nullptr); + return promise->getFuture(); +} diff --git a/global_cache/S3DataAdaptor.h b/global_cache/S3DataAdaptor.h new file mode 100644 index 0000000..677e1c8 --- /dev/null +++ b/global_cache/S3DataAdaptor.h @@ -0,0 +1,47 @@ +#ifndef MADFS_S3_DATA_ADAPTOR_H +#define MADFS_S3_DATA_ADAPTOR_H + +#include +#include +#include +#include + +#include "data_adaptor.h" + +#include "Common.h" + +using HybridCache::ByteBuffer; +using HybridCache::DataAdaptor; + +class S3DataAdaptor : public DataAdaptor { +public: + S3DataAdaptor(); + + ~S3DataAdaptor(); + + // 从数据服务器加载数据 + virtual folly::Future DownLoad(const std::string &key, + size_t start, + size_t size, + ByteBuffer &buffer); + + // 上传数据到数据服务器 + virtual folly::Future UpLoad(const std::string &key, + size_t size, + const ByteBuffer &buffer, + const std::map &headers); + + // 删除数据服务器的数据 + virtual folly::Future Delete(const std::string &key); + + // 获取数据的元数据 + virtual folly::Future Head(const std::string &key, + size_t &size, + std::map &headers); + +private: + Aws::Client::ClientConfiguration *clientCfg_; + Aws::S3::S3Client *s3Client_; +}; + +#endif // MADFS_S3_DATA_ADAPTOR_H \ No newline at end of file diff --git a/global_cache/WriteCache.cpp b/global_cache/WriteCache.cpp new file mode 100644 index 0000000..2f27582 --- /dev/null +++ b/global_cache/WriteCache.cpp @@ -0,0 +1,404 @@ +#include + +#include "WriteCache.h" +#include "FileSystemDataAdaptor.h" +#include +#include "write_cache.h" + + +//#define BRPC_WITH_RDMA 1 +//#include + +class WriteCache4RocksDB : public WriteCacheImpl { +public: + explicit WriteCache4RocksDB(std::shared_ptr executor); + + ~WriteCache4RocksDB(); + + virtual GetOutput Get(const std::string &internal_key, uint64_t start, uint64_t length); + + virtual PutOutput Put(const std::string &key, uint64_t length, const butil::IOBuf &buf); + + virtual int Delete(const std::string &key_prefix, uint64_t ts, const std::unordered_set &except_keys); + +private: + std::string rocksdb_path_; + rocksdb::DB *db_; +}; + +WriteCache4RocksDB::WriteCache4RocksDB(std::shared_ptr executor) + : WriteCacheImpl(executor) { + rocksdb::Options options; + options.create_if_missing = true; + rocksdb_path_ = PathJoin(GetGlobalConfig().write_cache_dir, ".write_cache.db"); + if (CreateParentDirectories(rocksdb_path_)) { + LOG(WARNING) << "Failed to create directory: " << rocksdb_path_; + abort(); + } + auto status = rocksdb::DB::Open(options, rocksdb_path_, &db_); + if (!status.ok()) { + LOG(WARNING) << "Failed to open RocksDB: " << status.ToString(); + abort(); + } +} + +WriteCache4RocksDB::~WriteCache4RocksDB() { + if (db_) { + db_->Close(); + } +} + +GetOutput WriteCache4RocksDB::Get(const std::string &internal_key, uint64_t start, uint64_t length) { + rocksdb::ReadOptions options; + std::string value; + auto status = db_->Get(options, internal_key, &value); + GetOutput output; + if (status.IsNotFound()) { + output.status = CACHE_ENTRY_NOT_FOUND; + return output; + } else if (!status.ok()) { + LOG(WARNING) << "Failed to get key " << internal_key << " from RocksDB: " << status.ToString(); + output.status = IO_ERROR; + return output; + } + if (length == 0 || start + length > value.size()) { + output.status = INVALID_ARGUMENT; + return output; + } + output.status = OK; + output.buf.append(&value[start], length); + LOG_IF(INFO, FLAGS_verbose) << "GetWriteCache internal_key: " << internal_key << ", size: " << length; + return output; +} + +PutOutput WriteCache4RocksDB::Put(const std::string &key, uint64_t length, const butil::IOBuf &buf) { + auto oid = next_object_id_.fetch_add(1); + auto internal_key = key + "-" + std::to_string(oid); + rocksdb::WriteOptions options; + std::string value = buf.to_string(); + auto status = db_->Put(options, internal_key, value); + if (!status.ok()) { + LOG(WARNING) << "Failed to put key " << internal_key << " from RocksDB: " << status.ToString(); + return {IO_ERROR, ""}; + } + LOG_IF(INFO, FLAGS_verbose) << "PutWriteCache key: " << key << ", internal_key: " << internal_key << ", size: " << length; + return {OK, internal_key}; +} + +static bool HasPrefix(const std::string &key, const std::string &key_prefix) { + return key.substr(0, key_prefix.size()) == key_prefix; +} + +static uint64_t ParseTS(const std::string &key) { + size_t pos = key.rfind('-'); + if (pos != std::string::npos) { + std::string lastSubStr = key.substr(pos + 1); + uint64_t number; + std::istringstream(lastSubStr) >> number; + if (!std::cin.fail()) { + return number; + } else { + return UINT64_MAX; + } + } else { + return UINT64_MAX; + } +} + +// Delete all entries that: match the prefix, < ts, and not in except_keys +int WriteCache4RocksDB::Delete(const std::string &key_prefix, uint64_t ts, const std::unordered_set &except_keys) { + LOG(INFO) << "Request key_prefix = " << key_prefix << ", ts = " << ts; + rocksdb::ReadOptions read_options; + rocksdb::WriteOptions write_options; + auto iter = db_->NewIterator(read_options); + iter->Seek(key_prefix); + for (; iter->Valid(); iter->Next()) { + std::string key = iter->key().ToString(); + LOG(INFO) << "Processing key " << key; + if (!HasPrefix(key, key_prefix)) { + break; + } + if (ParseTS(key) >= ts || except_keys.count(key)) { + continue; + } + auto status = db_->Delete(write_options, key); + if (!status.ok() && !status.IsNotFound()) { + LOG(WARNING) << "Failed to delete key " << key << " from RocksDB: " << status.ToString(); + iter->Reset(); + return IO_ERROR; + } + LOG(INFO) << "Deleted key " << key; + } + iter->Reset(); + return OK; +} + +// ---------------------------------------------------------------------------- + +class WriteCache4Disk : public WriteCacheImpl { +public: + explicit WriteCache4Disk(std::shared_ptr executor); + + ~WriteCache4Disk(); + + virtual GetOutput Get(const std::string &internal_key, uint64_t start, uint64_t length); + + virtual PutOutput Put(const std::string &key, uint64_t length, const butil::IOBuf &buf); + + virtual int Delete(const std::string &key_prefix, uint64_t ts, const std::unordered_set &except_keys); + +private: + std::shared_ptr cache_fs_adaptor_; +}; + +WriteCache4Disk::WriteCache4Disk(std::shared_ptr executor) + : WriteCacheImpl(executor) { + cache_fs_adaptor_ = std::make_shared(GetGlobalConfig().write_cache_dir, nullptr, false, nullptr, false); +} + +WriteCache4Disk::~WriteCache4Disk() {} + +GetOutput WriteCache4Disk::Get(const std::string &internal_key, uint64_t start, uint64_t length) { + butil::Timer t; + t.start(); +#ifndef BRPC_WITH_RDMA + auto wrap = HybridCache::ByteBuffer(new char[length], length); +#else + auto wrap = HybridCache::ByteBuffer((char *) brpc::rdma::AllocBlock(length), length); +#endif + int res = cache_fs_adaptor_->DownLoad(internal_key, start, length, wrap).get(); + GetOutput output; + output.status = res; +#ifndef BRPC_WITH_RDMA + if (res == OK) { + output.buf.append(wrap.data, wrap.len); + } + delete []wrap.data; +#else + if (res == OK) { + output.buf.append_user_data(wrap.data, wrap.len, brpc::rdma::DeallocBlock); + } else { + brpc::rdma::DeallocBlock(wrap.data); + } +#endif + t.stop(); + LOG_IF(INFO, FLAGS_verbose) << "Get key: " << internal_key + << ", start: " << start + << ", length: " << length + << ", status: " << res + << ", latency: " << t.u_elapsed(); + return output; +} + +uint64_t ReportAvailableDiskSpace(std::string &path) { + struct statvfs stat; + if (statvfs(path.c_str(), &stat)) { + PLOG(ERROR) << "Failed to statvfs"; + return 0; + } + return stat.f_bavail * stat.f_bsize; +} + +const static size_t kMinDiskFreeSpace = 1024 * 1024 * 512; + +PutOutput WriteCache4Disk::Put(const std::string &key, uint64_t length, const butil::IOBuf &buf) { + butil::Timer t; + t.start(); + auto oid = next_object_id_.fetch_add(1); + auto internal_key = key + "-" + std::to_string(oid); + + if (ReportAvailableDiskSpace(GetGlobalConfig().write_cache_dir) < std::max(length, kMinDiskFreeSpace)) { + // LOG(WARNING) << "No enough space to persist data, please perform one GC immediately"; + return {NO_ENOUGH_DISKSPACE, ""}; + } + + t.stop(); + // LOG_IF(INFO, FLAGS_verbose) << "duration: " << t.u_elapsed(); + + auto data_len = buf.length(); + + thread_local void *aux_buffer = nullptr; + if (!aux_buffer) + posix_memalign(&aux_buffer, 4096, GetGlobalConfig().write_chunk_size); + + auto data = buf.fetch(aux_buffer, data_len); + auto wrap = HybridCache::ByteBuffer((char *) data, data_len); + std::map headers; + + t.stop(); + // LOG_IF(INFO, FLAGS_verbose) << "duration: " << t.u_elapsed(); + + int res = cache_fs_adaptor_->UpLoad(internal_key, length, wrap, headers).get(); + // free(aux_buffer); + if (res) { + LOG(WARNING) << "Failed to put key " << internal_key << " to disk"; + return {IO_ERROR, ""}; + } + t.stop(); + LOG_IF(INFO, FLAGS_verbose) << "PutWriteCache key: " << key << ", internal_key: " << internal_key << ", size: " << length << ", duration: " << t.u_elapsed(); + return {OK, internal_key}; +} + + +void listFilesRecursively(const std::string &directoryPath, + std::vector &to_remove, + const std::string &key_prefix, + uint64_t ts, + const std::unordered_set &except_keys) { + DIR* dir = opendir(directoryPath.c_str()); + if (dir == nullptr) { + std::cerr << "Error opening directory: " << directoryPath << std::endl; + return; + } + + struct dirent* entry; + while ((entry = readdir(dir)) != nullptr) { + // Skip "." and ".." entries + if (std::string(entry->d_name) == "." || std::string(entry->d_name) == "..") { + continue; + } + + std::string fullPath = PathJoin(directoryPath, entry->d_name); + std::string rootPath = GetGlobalConfig().write_cache_dir; + struct stat statbuf; + if (stat(fullPath.c_str(), &statbuf) == 0) { + if (S_ISDIR(statbuf.st_mode)) { + // It's a directory, recurse into it + listFilesRecursively(fullPath, to_remove, key_prefix, ts, except_keys); + } else if (S_ISREG(statbuf.st_mode)) { + std::string key = fullPath.substr(rootPath.length()); + if (!key.empty() && key[0] == '/') { + key = key.substr(1); + } + if (!HasPrefix(key, key_prefix) || except_keys.count(key) || ParseTS(key) >= ts) { + continue; + } + to_remove.push_back(fullPath); + // LOG(INFO) << "Deleted key " << key << ", location " << fullPath; + } + } + } + closedir(dir); +} + + +// Delete all entries that: match the prefix, < ts, and not in except_keys +int WriteCache4Disk::Delete(const std::string &key_prefix, uint64_t ts, const std::unordered_set &except_keys) { + LOG(INFO) << "Request key_prefix = " << key_prefix << ", ts = " << ts; + std::vector to_remove; + listFilesRecursively(GetGlobalConfig().write_cache_dir, + to_remove, + key_prefix, + ts, + except_keys); + for (auto &entry : to_remove) { + if (remove(entry.c_str())) { + LOG(WARNING) << "Failed to remove file: " << entry; + return IO_ERROR; + } + } + return OK; +} + + +class WriteCache4Fake : public WriteCacheImpl { +public: + explicit WriteCache4Fake(std::shared_ptr executor) : WriteCacheImpl(executor) {} + + virtual ~WriteCache4Fake() {} + + virtual GetOutput Get(const std::string &internal_key, uint64_t start, uint64_t length) { + LOG_IF(INFO, FLAGS_verbose) << "Get internal_key " << internal_key << " start " << start << " length " << length; + GetOutput ret; + ret.status = OK; + ret.buf.resize(length, 'x'); + return ret; + } + + virtual PutOutput Put(const std::string &key, uint64_t length, const butil::IOBuf &buf) { + LOG_IF(INFO, FLAGS_verbose) << "Put key " << key << " length " << length; + PutOutput ret; + ret.status = OK; + ret.internal_key = key; + return ret; + } + + virtual int Delete(const std::string &key_prefix, uint64_t ts, const std::unordered_set &except_keys) { + return OK; + } +}; + +class WriteCache4Cachelib : public WriteCacheImpl { +public: + explicit WriteCache4Cachelib(std::shared_ptr executor) : WriteCacheImpl(executor) { + HybridCache::EnableLogging = false; + impl_ = std::make_shared(GetGlobalConfig().write_cache); + } + + virtual ~WriteCache4Cachelib() {} + + virtual GetOutput Get(const std::string &internal_key, uint64_t start, uint64_t length) { + butil::Timer t; + t.start(); + std::vector> dataBoundary; +#ifndef BRPC_WITH_RDMA + auto wrap = HybridCache::ByteBuffer(new char[length], length); +#else + auto wrap = HybridCache::ByteBuffer((char *) brpc::rdma::AllocBlock(length), length); +#endif + int res = impl_->Get(internal_key, start, length, wrap, dataBoundary); + GetOutput output; + output.status = res; +#ifndef BRPC_WITH_RDMA + if (res == OK) { + output.buf.append(wrap.data, wrap.len); + } + delete []wrap.data; +#else + if (res == OK) { + output.buf.append_user_data(wrap.data, wrap.len, brpc::rdma::DeallocBlock); + } else { + brpc::rdma::DeallocBlock(wrap.data); + } +#endif + t.stop(); + LOG_IF(INFO, FLAGS_verbose) << "Get key: " << internal_key + << ", start: " << start + << ", length: " << length + << ", status: " << res + << ", latency: " << t.u_elapsed(); + return output; + } + + virtual PutOutput Put(const std::string &key, uint64_t length, const butil::IOBuf &buf) { + LOG_IF(INFO, FLAGS_verbose) << "Put key " << key << " length " << length; + PutOutput ret; + ret.status = OK; + ret.internal_key = key; + return ret; + } + + virtual int Delete(const std::string &key_prefix, uint64_t ts, const std::unordered_set &except_keys) { + return OK; + } + +private: + std::shared_ptr impl_; + +}; + + +DEFINE_string(write_cache_engine, "disk", "Write cache engine: rocksdb | disk"); + +WriteCache::WriteCache(std::shared_ptr executor) { + if (FLAGS_write_cache_engine == "rocksdb") + impl_ = new WriteCache4RocksDB(executor); + else if (FLAGS_write_cache_engine == "disk") + impl_ = new WriteCache4Disk(executor); + else if (FLAGS_write_cache_engine == "fake") + impl_ = new WriteCache4Fake(executor); + else { + LOG(WARNING) << "unsupported write cache engine"; + exit(EXIT_FAILURE); + } +} diff --git a/global_cache/WriteCache.h b/global_cache/WriteCache.h new file mode 100644 index 0000000..52e1028 --- /dev/null +++ b/global_cache/WriteCache.h @@ -0,0 +1,53 @@ +#ifndef MADFS_WRITE_CACHE_H +#define MADFS_WRITE_CACHE_H + +#include +#include +#include +#include +#include + +#include +#include + +#include "Common.h" + +class WriteCacheImpl { +public: + WriteCacheImpl(std::shared_ptr executor) : executor_(executor), next_object_id_(0) {} + virtual GetOutput Get(const std::string &internal_key, uint64_t start, uint64_t length) = 0; + virtual PutOutput Put(const std::string &key, uint64_t length, const butil::IOBuf &buf) = 0; + virtual uint64_t QueryTS() { return next_object_id_.load(); } + virtual int Delete(const std::string &key_prefix, uint64_t ts, const std::unordered_set &except_keys) = 0; + + std::shared_ptr executor_; + std::atomic next_object_id_; +}; + +class WriteCache { +public: + explicit WriteCache(std::shared_ptr executor); + + ~WriteCache() { + delete impl_; + } + + GetOutput Get(const std::string &internal_key, uint64_t start, uint64_t length) { + return impl_->Get(internal_key, start, length); + } + + PutOutput Put(const std::string &key, uint64_t length, const butil::IOBuf &buf) { + return impl_->Put(key, length, buf); + } + + uint64_t QueryTS() { return impl_->QueryTS(); } + + int Delete(const std::string &key_prefix, uint64_t ts, const std::unordered_set &except_keys) { + return impl_->Delete(key_prefix, ts, except_keys); + } + +private: + WriteCacheImpl *impl_; +}; + +#endif // MADFS_WRITE_CACHE_H \ No newline at end of file diff --git a/global_cache/WriteCacheClient.h b/global_cache/WriteCacheClient.h new file mode 100644 index 0000000..c0112a0 --- /dev/null +++ b/global_cache/WriteCacheClient.h @@ -0,0 +1,42 @@ +#ifndef MADFS_WRITE_CACHE_CLIENT_H +#define MADFS_WRITE_CACHE_CLIENT_H + +#include +#include +#include + +#include "Common.h" +#include "Placement.h" +#include "data_adaptor.h" +#include "EtcdClient.h" + +using HybridCache::ByteBuffer; + +class GlobalDataAdaptor; + +class WriteCacheClient { +public: + struct PutResult { + int status; + Json::Value root; + }; + +public: + WriteCacheClient() {} + + ~WriteCacheClient() {} + + virtual folly::Future Put(const std::string &key, + size_t size, + const ByteBuffer &buffer, + const std::map &headers, + size_t off = 0) = 0; + + virtual folly::Future Get(const std::string &key, + size_t start, + size_t size, + ByteBuffer &buffer, + Json::Value &root) = 0; +}; + +#endif // MADFS_WRITE_CACHE_CLIENT_H \ No newline at end of file diff --git a/global_cache/gcache.proto b/global_cache/gcache.proto new file mode 100644 index 0000000..9d350ba --- /dev/null +++ b/global_cache/gcache.proto @@ -0,0 +1,72 @@ +syntax="proto2"; +package gcache; + +option cc_generic_services = true; + +message GetEntryRequest { + required string key = 1; + required uint64 start = 2; + required uint64 length = 3; +}; + +message GetEntryResponse { + required int32 status_code = 1; + optional bytes data = 2; +}; + +message PutEntryRequest { + required string key = 1; + required uint64 length = 2; + optional bytes data = 3; +}; + +message PutEntryResponse { + required int32 status_code = 1; + optional string internal_key = 2; // for write cache +}; + +message DeleteEntryRequest { + required string key = 1; // actually 'prefix' + optional uint64 chunk_size = 2; + optional uint64 max_chunk_id = 3; +}; + +message DeleteEntryRequestForWriteCache { + required string key_prefix = 1; + required uint64 max_ts = 2; + repeated string except_keys = 3; +}; + +message DeleteEntryResponse { + required int32 status_code = 1; +}; + +message RegisterRequest { + // nothing +}; + +message QueryTsRequest { + // nothing +}; + +message QueryTsResponse { + required int32 status_code = 1; + required uint64 timestamp = 2; +}; + +message RegisterResponse { + required int32 status_code = 1; +}; + +service GlobalCacheService { + rpc GetEntryFromReadCache(GetEntryRequest) returns (GetEntryResponse); + rpc PutEntryFromReadCache(PutEntryRequest) returns (PutEntryResponse); + rpc DeleteEntryFromReadCache(DeleteEntryRequest) returns (DeleteEntryResponse); + + rpc GetEntryFromWriteCache(GetEntryRequest) returns (GetEntryResponse); + rpc PutEntryFromWriteCache(PutEntryRequest) returns (PutEntryResponse); + rpc DeleteEntryFromWriteCache(DeleteEntryRequestForWriteCache) returns (DeleteEntryResponse); + rpc QueryTsFromWriteCache(QueryTsRequest) returns (QueryTsResponse); + + rpc Register(RegisterRequest) returns (RegisterResponse); +}; diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..cde4575 --- /dev/null +++ b/install.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +if [ ! -d "./JYCache_Env" ]; then + wget https://madstorage.s3.cn-north-1.jdcloud-oss.com/JYCache_Env_x64.tgz + md5=`md5sum JYCache_Env_x64.tgz | awk {'print $1'}` + if [ "$md5" != "cd27e0db8b1fc33b88bf1c467ed012b8" ]; then +   echo 'JYCache_Env version inconsistency!' + exit 1 + fi + tar -zxvf JYCache_Env_x64.tgz +fi + +cp ./build/intercept/intercept_server JYCache_Env/ +cp ./build/intercept/libintercept_client.so JYCache_Env/ +cp ./build/global_cache/madfs_gc JYCache_Env/ +cp ./build/global_cache/madfs_global_server JYCache_Env/ +cp ./build/bin/s3fs JYCache_Env/ diff --git a/intercept/CMakeLists.txt b/intercept/CMakeLists.txt new file mode 100644 index 0000000..170886d --- /dev/null +++ b/intercept/CMakeLists.txt @@ -0,0 +1,34 @@ +link_libraries(-lrt) + +find_library(ICEORYX_POSH_LIB iceoryx_posh PATHS ../thirdparties/iceoryx/lib) +find_library(ICEORYX_HOOFS_LIB iceoryx_hoofs PATHS ../thirdparties/iceoryx/lib) +find_library(ICEORYX_PLATFORM_LIB iceoryx_platform PATHS ../thirdparties/iceoryx/lib) + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mavx2") + +include_directories(${CMAKE_CURRENT_SOURCE_DIR}) +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../thirdparties/spdlog/include) + +add_subdirectory(common) +add_subdirectory(internal) +add_subdirectory(discovery) +add_subdirectory(filesystem) +add_subdirectory(registry) +add_subdirectory(middleware) +add_subdirectory(posix) + +add_executable(intercept_server server.cpp) +target_link_libraries(intercept_server PUBLIC intercept_discovery intercept_internal common_lib intercept_filesystem intercept_middleware intercept_registry hybridcache_local madfs_global s3fs_lib ${THIRD_PARTY_LIBRARIES} ${ICEORYX_POSH_LIB} ${ICEORYX_HOOFS_LIB} ${ICEORYX_PLATFORM_LIB} -pthread -lcurl -lxml2 -lcrypto -ldl -laio -lrt -lacl) + +add_library(intercept_client SHARED client.cpp) +target_link_libraries(intercept_client PUBLIC + intercept_posix_interface_client + -lsyscall_intercept + -pthread + ${ICEORYX_POSH_LIB} + ${ICEORYX_HOOFS_LIB} + ${ICEORYX_PLATFORM_LIB} + -lrt + -L${CMAKE_CURRENT_SOURCE_DIR}/../thirdparties/intercept/lib +) +target_compile_options(intercept_client PUBLIC -DCLIENT_BUILD -mavx2) diff --git a/intercept/client.cpp b/intercept/client.cpp new file mode 100644 index 0000000..04c14dc --- /dev/null +++ b/intercept/client.cpp @@ -0,0 +1,138 @@ + + + +// int main2(int argc, char *argv[]) { +// InitSyscall(); +// GlobalInit(); +// long args[6]; +// const char* pathname = "/curvefs/test_mount/testfile"; +// args[0] = (long)(pathname); +// args[1] = O_CREAT | O_WRONLY | O_TRUNC; +// args[2] = S_IRUSR | S_IWUSR; +// long result = 0; +// PosixOpOpen(args, &result); +// PosixOpAccess(args, &result); +// return 0; +// } + +#include "registry/client_server_registry.h" +// ! 暂时注释,使用时不能注释 +#include "posix/posix_helper.h" +using intercept::middleware::ReqResMiddlewareWrapper; +int main() { + constexpr char APP_NAME[] = "iox-intercept-client"; + iox::runtime::PoshRuntime::initRuntime(APP_NAME); + + intercept::internal::ServiceMetaInfo info; + info.service = SERVICE_FLAG; + info.instance = INTERCEPT_INSTANCE_FLAG; + intercept::registry::ClientServerRegistry registry(ICEORYX, info); + auto dummyserver = registry.CreateDummyServer(); + sleep(2); + + info = dummyserver->GetServiceMetaInfo(); + info.service = SERVICE_FLAG; + info.instance = INTERCEPT_INSTANCE_FLAG; + + std::shared_ptr wrapper = registry.CreateClient(info); + + intercept::internal::OpenOpReqRes req("/testdir/hellofile1", O_CREAT|O_RDWR, S_IRUSR | S_IWUSR); + wrapper->OnRequest(req); + const auto& openRes = static_cast (req.GetResponse()); + + + char* writebuf = (char*)malloc(sizeof(char) * 1024); + char str[] = "hello world"; + memcpy(writebuf, str, sizeof(str)); + intercept::internal::WriteOpReqRes writeReq(openRes.fd, writebuf, strlen(writebuf)); + wrapper->OnRequest(writeReq); + + // open and read + intercept::internal::OpenOpReqRes req2("/testdir/hellofile1", O_RDWR, S_IRUSR | S_IWUSR); + wrapper->OnRequest(req2); + const auto& openRes2 = static_cast (req2.GetResponse()); + char* buf2 = (char*)malloc(sizeof(char) * 1024); + + intercept::internal::ReadOpReqRes readReq2(openRes2.fd, buf2, 8); + wrapper->OnRequest(readReq2); + free((void*)buf2); + + dummyserver->StopServer(); + std::cout << "stop dummyserver in main" << std::endl; + //sleep(5); + return 0; +} + +int mainposix() { + char filename[256]; + + // 循环执行流程 + while (true) { + std::cout << "Enter filename (or 'exit' to quit): "; + std::cin >> filename; + + if (strcmp(filename, "exit") == 0) { + std::cout << "Exiting program..." << std::endl; + break; + } + + std::cout << "Enter 'write' to write to file, 'read' to read from file: "; + std::string operation; + std::cin >> operation; + + if (operation == "write") { + // 打开文件进行写入 + int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR); + if (fd == -1) { + std::cerr << "Error: Failed to open file for writing." << std::endl; + continue; + } + + std::string content; + std::cout << "Enter content to write to file (end with 'EOF'): " << std::endl; + std::cin.ignore(); // 忽略前一个输入的换行符 + while (true) { + std::string line; + std::getline(std::cin, line); + if (line == "EOF") { + break; + } + content += line + "\n"; + } + + ssize_t bytes_written = write(fd, content.c_str(), content.size()); + std::cout << "the write byte: " << bytes_written << std::endl; + close(fd); + } else if (operation == "read") { + // 打开文件进行读取 + int fd = open(filename, O_RDONLY); + if (fd == -1) { + std::cerr << "Error: Failed to open file for reading." << std::endl; + continue; + } + + char buffer[4096]; + ssize_t bytesRead; + std::cout << "Content read from file:" << std::endl; + while ((bytesRead = read(fd, buffer, sizeof(buffer))) > 0) { + std::cout.write(buffer, bytesRead); + } + std::cout << std::endl; + + // 获取文件的状态信息 + struct stat fileStat; + if (fstat(fd, &fileStat) == 0) { + std::cout << "File size: " << fileStat.st_size << " bytes" << std::endl; + std::cout << "File permissions: " << (fileStat.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO)) << std::endl; + } else { + std::cerr << "Error: Failed to get file status." << std::endl; + } + + close(fd); + } else { + std::cerr << "Error: Invalid operation. Please enter 'write' or 'read'." << std::endl; + } + } + + return 0; +} diff --git a/intercept/common/CMakeLists.txt b/intercept/common/CMakeLists.txt new file mode 100644 index 0000000..84081d6 --- /dev/null +++ b/intercept/common/CMakeLists.txt @@ -0,0 +1,8 @@ +file(GLOB COMMON_SOURCES *.cpp) + +add_library(common_lib ${COMMON_SOURCES}) +target_include_directories(common_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +add_library(common_lib_client ${COMMON_SOURCES}) +target_compile_options(common_lib_client PUBLIC -fPIC -DCLIENT_BUILD) +target_include_directories(common_lib_client PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/intercept/common/common.cpp b/intercept/common/common.cpp new file mode 100644 index 0000000..6ee255d --- /dev/null +++ b/intercept/common/common.cpp @@ -0,0 +1,175 @@ +#include + +#include "common.h" +#include "spdlog/sinks/basic_file_sink.h" +#include "spdlog/sinks/stdout_color_sinks.h" + + + +namespace intercept{ +namespace common { +void dummy() { + +} + + +// Constructor starts the timer +Timer::Timer() : m_startTimePoint(std::chrono::high_resolution_clock::now()) {} + +Timer::Timer(const std::string& message) : m_message(message), + m_startTimePoint(std::chrono::high_resolution_clock::now()) {} + +// Destructor prints the elapsed time if the timer hasn't been stopped manually +Timer::~Timer() { + if (!m_stopped) { + Stop(); + } + // std::cout << m_message << " Elapsed time: " << m_elapsedTime << " ms" << std::endl; +} + +// Method to stop the timer and return the elapsed time in milliseconds +void Timer::Stop() { + if (!m_stopped) { + auto endTimePoint = std::chrono::high_resolution_clock::now(); + auto start = std::chrono::time_point_cast(m_startTimePoint).time_since_epoch().count(); + auto end = std::chrono::time_point_cast(endTimePoint).time_since_epoch().count(); + + m_elapsedTime = end - start; + m_stopped = true; + if (m_elapsedTime > 0) { + // std::cout << m_message << ", Elapsed time: " << m_elapsedTime << " us" << std::endl; + spdlog::warn("{}, Elapsed time: {} us ", m_message, m_elapsedTime); + } + } +} + +// Method to get the elapsed time in microseconds +long long Timer::ElapsedMicroseconds() const { + return m_elapsedTime; +} + +// Method to restart the timer +void Timer::Restart() { + m_startTimePoint = std::chrono::high_resolution_clock::now(); + m_stopped = false; +} + +ThreadPool::ThreadPool(size_t numThreads) : stop(false) { + for (size_t i = 0; i < numThreads; ++i) { + workers.emplace_back([this] { + for (;;) { + std::function task; + { + std::unique_lock lock(this->queueMutex); + this->condition.wait(lock, [this] { return this->stop || !this->tasks.empty(); }); + if (this->stop && this->tasks.empty()) + return; + task = std::move(this->tasks.front()); + this->tasks.pop(); + } + task(); + } + }); + } +} + +ThreadPool::~ThreadPool() { + { + std::unique_lock lock(queueMutex); + stop = true; + } + condition.notify_all(); + for (std::thread &worker : workers) + worker.join(); +} + +void ThreadPool::enqueue(std::function task) { + { + std::unique_lock lock(queueMutex); + if (stop) + throw std::runtime_error("enqueue on stopped ThreadPool"); + tasks.emplace(task); + } + condition.notify_one(); +} + + +std::string generateRandomSuffix() { + // 使用当前时间作为随机数生成器的种子,以获得更好的随机性 + unsigned seed = std::chrono::system_clock::now().time_since_epoch().count(); + std::default_random_engine generator(seed); + std::uniform_int_distribution distribution(0, 25); // 生成0-25之间的整数,对应字母'a'到'z' + + std::string suffix; + suffix.reserve(5); // 假设我们想要生成5个随机字符的后缀 + for (size_t i = 0; i < 5; ++i) { + suffix += static_cast('a' + distribution(generator)); + } + return suffix; +} + +std::atomic running(true); +void UpdateLogLevelPeriodically(int intervalSeconds) { + auto& config = Configure::getInstance(); + while (running) { + std::this_thread::sleep_for(std::chrono::seconds(intervalSeconds)); + // std::cout << "reload the config: " << CONFIG_FILE << std::endl; + config.loadConfig(CONFIG_FILE); // Assuming this reloads the configuration + std::string loglevel = config.getConfig("loglevel"); + if (loglevel == "debug") { + spdlog::set_level(spdlog::level::debug); + } else if (loglevel == "warning") { + spdlog::set_level(spdlog::level::warn); + } else if (loglevel == "info") { + spdlog::set_level(spdlog::level::info); + } else if (loglevel == "error") { + spdlog::set_level(spdlog::level::err); + } else { + std::cerr << "Invalid log level specified in the config file" << std::endl; + } + } +} +void InitLog() { + const auto& config = Configure::getInstance(); + std::string pid = std::to_string((long)getpid()); + std::string logpath = config.getConfig("logpath") + "." + pid; + std::string loglevel = config.getConfig("loglevel"); + try + { + std::shared_ptr logger; + std::string printtype = config.getConfig("logprinttype"); + if (printtype == "console") { + logger = spdlog::stdout_color_mt("console"); + } else { + logger = spdlog::basic_logger_mt("basic_logger", logpath); + } + spdlog::set_default_logger(logger); + if (loglevel == "debug") { + spdlog::set_level(spdlog::level::debug); + } + else if (loglevel == "warning") { + spdlog::set_level(spdlog::level::warn); + } + else if (loglevel == "info") { + spdlog::set_level(spdlog::level::info); + } + else if (loglevel == "error") { + spdlog::set_level(spdlog::level::err); + } + else { + std::cerr << "Invalid log level specified in the config file" << std::endl; + } + //spdlog::set_pattern("[%H:%M:%S %z] [%n] [%^---%L---%$] [thread %t] %v"); + spdlog::set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%n] [%l] [pid %P tid %t] %v"); + spdlog::flush_every(std::chrono::seconds(5)); + // Start the periodic log level updater thread + std::thread updateThread(UpdateLogLevelPeriodically, 5); // Check every 60 seconds + updateThread.detach(); // Detach the thread so it runs independently + } + catch (const spdlog::spdlog_ex &ex) { + std::cout << "Log init failed: " << ex.what() << std::endl; + } +} + +} // namespace common +} // namespace intercept diff --git a/intercept/common/common.h b/intercept/common/common.h new file mode 100644 index 0000000..00ebff2 --- /dev/null +++ b/intercept/common/common.h @@ -0,0 +1,143 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "spdlog/spdlog.h" +#include "spdlog/fmt/fmt.h" + +namespace intercept { +namespace common { + +#ifndef CLIENT_BUILD +const std::string CONFIG_FILE = "conf/server.conf"; +#else +const std::string CONFIG_FILE = "conf/client.conf"; +#endif + +using Ino = uint64_t; +struct DirStream { + Ino ino; + uint64_t fh; + uint64_t offset; +}; + +class Timer { +public: + // Constructor starts the timer + Timer(); + Timer(const std::string& message); + + // Destructor prints the elapsed time if the timer hasn't been stopped manually + ~Timer(); + + // Method to stop the timer and return the elapsed time in milliseconds + void Stop(); + + // Method to get the elapsed time in microseconds + long long ElapsedMicroseconds() const; + + // Method to restart the timer + void Restart(); + +private: + std::chrono::time_point m_startTimePoint; + long long m_elapsedTime = 0; + bool m_stopped = false; + std::string m_message; +}; + + +class Configure { +public: + // 获取单例实例的静态方法 + static Configure& getInstance() { + static Configure instance; + return instance; + } + + // 加载配置文件的方法 + bool loadConfig(const std::string& filePath) { + std::ifstream file(filePath); + if (!file.is_open()) { + std::cerr << "Failed to open config file: " << filePath << std::endl; + return false; + } + + std::string line; + while (std::getline(file, line)) { + // Ignore comments and empty lines + if (line.empty() || line[0] == '#') { + continue; + } + + std::istringstream iss(line); + std::string key, value; + + // Split line into key and value + if (std::getline(iss, key, '=') && std::getline(iss, value)) { + // Remove whitespace from the key and value + key.erase(key.find_last_not_of(" \t\n\r\f\v") + 1); + key.erase(0, key.find_first_not_of(" \t\n\r\f\v")); + value.erase(value.find_last_not_of(" \t\n\r\f\v") + 1); + value.erase(0, value.find_first_not_of(" \t\n\r\f\v")); + + configMap[key] = value; + } + } + + file.close(); + return true; + } + + // 获取配置值的方法 + std::string getConfig(const std::string& key) const { + auto it = configMap.find(key); + if (it != configMap.end()) { + return it->second; + } + return ""; + } + +private: + std::map configMap; // 存储配置键值对 + Configure() {} // 私有构造函数,防止外部直接实例化 + Configure(const Configure&) = delete; // 禁止拷贝构造 + Configure& operator=(const Configure&) = delete; // 禁止赋值操作 +}; + +class ThreadPool { +public: + ThreadPool(size_t numThreads = 30); + ~ThreadPool(); + void enqueue(std::function task); + +private: + std::vector workers; + std::queue> tasks; + std::mutex queueMutex; + std::condition_variable condition; + bool stop; +}; + + + + +std::string generateRandomSuffix(); +void InitLog(); + +} // namespace common +} // namespace intercept \ No newline at end of file diff --git a/intercept/discovery/CMakeLists.txt b/intercept/discovery/CMakeLists.txt new file mode 100644 index 0000000..e48f08a --- /dev/null +++ b/intercept/discovery/CMakeLists.txt @@ -0,0 +1,22 @@ +# discovery/CMakeLists.txt + +file(GLOB DISCOVERY_SOURCES *.cpp) + +find_library(ICEORYX_POSH_LIB NAMES iceoryx_posh PATHS ../../thirdparties/iceoryx/lib) +find_library(ICEORYX_HOOFS_LIB NAMES iceoryx_hoofs PATHS ../../thirdparties/iceoryx/lib) +find_library(ICEORYX_PLATFORM_LIB NAMES iceoryx_platform PATHS ../../thirdparties/iceoryx/lib) + +add_library(intercept_discovery ${DISCOVERY_SOURCES}) +target_include_directories(intercept_discovery PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../../thirdparties/iceoryx/include +) +target_link_libraries(intercept_discovery PUBLIC intercept_internal ${ICEORYX_POSH_LIB}) + +add_library(intercept_discovery_client ${DISCOVERY_SOURCES}) +target_include_directories(intercept_discovery_client PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../../thirdparties/iceoryx/include +) +target_compile_options(intercept_discovery_client PUBLIC -fPIC ) +target_link_libraries(intercept_discovery_client PUBLIC -lrt intercept_internal_client ${ICEORYX_POSH_LIB} ${ICEORYX_HOOFS_LIB} ${ICEORYX_PLATFORM_LIB} ) \ No newline at end of file diff --git a/intercept/discovery/discovery.h b/intercept/discovery/discovery.h new file mode 100644 index 0000000..59a8a91 --- /dev/null +++ b/intercept/discovery/discovery.h @@ -0,0 +1,66 @@ +#pragma once +#include +#include +#include +#include + +#include "internal/metainfo.h" + +namespace intercept { +namespace discovery { + +using intercept::internal::ServiceMetaInfo; + + + +// Discovery : use to discover the existing servers +// and the servers to be deleted +class Discovery { +public: + // Constructor + Discovery() { + // Initialization code + } + + // Initialize the discovery + virtual void Init() = 0; + + // Start the discovery loop + virtual void Start() = 0; + + // Stop the discovery loop + virtual void Stop() = 0; + + // Get the existing servers + virtual std::vector GetServers() const { + // Return the existing servers + return std::vector(); + } + + // Get the servers to be deleted + virtual std::set GetServersToDelete() const { + // Return the servers to be deleted + return std::set(); + } + + virtual std::vector FindServices(const ServiceMetaInfo& info) = 0; + + // Create a new server + virtual void CreateServer(const ServiceMetaInfo& serverInfo) { + // Create a new server using the serverInfo + } + + // Delete a server + virtual void DeleteServer(const ServiceMetaInfo& serverInfo) { + // Delete a server using the serverInfo + } + +protected: + std::vector existingServers; + std::set serversToDelete; + bool DISCOVERY_RUNNING; +}; + +} +} + diff --git a/intercept/discovery/iceoryx_discovery.cpp b/intercept/discovery/iceoryx_discovery.cpp new file mode 100644 index 0000000..de06b3c --- /dev/null +++ b/intercept/discovery/iceoryx_discovery.cpp @@ -0,0 +1,125 @@ +#include +#include + +#include "iceoryx_discovery.h" + + +#include "iox/signal_watcher.hpp" +#include "iceoryx_posh/runtime/posh_runtime.hpp" +#include "iceoryx_posh/runtime/service_discovery.hpp" + +namespace intercept { +namespace discovery { + +// TODO: Add your own discovery service implementation here +#define DISCOVERY_SERVICE_NAME "IceoryxDiscoveryService" +#define DISCOVERY_SERVICE_VERSION "1.0.0" +#define DISCOVERY_SERVICE_DESCRIPTION "IceoryxDiscoveryServiceDescription" +#define DISCOVERY_SERVICE_PROVIDER "IceoryxDiscoveryServiceProvider" +// constexpr char APP_NAME[] = "iox-discovery-service"; + +IceoryxDiscovery::IceoryxDiscovery() { + +} + +IceoryxDiscovery::~IceoryxDiscovery() { + // TODO: Clean up your discovery service implementation here +} + +void IceoryxDiscovery::Init() { + // TODO: Initialize your discovery service implementation here +} + +void IceoryxDiscovery::Start() { + // TODO: Start your discovery service implementation here + while (!iox::hasTerminationRequested()) { + // TODO: Implement discovery service logic here + const auto& servers = GetServers(); + const auto& newservers = GetNewServers(existingServers, servers); + for (auto& server : newservers) { + // TODO: Implement logic to handle new servers here + CreateServer(server); + } + + const auto& removedServers = GetRemovedServers(existingServers, servers); + for (auto& server : removedServers) { + // TODO: Implement logic to handle deleted servers here + DeleteServer(server); + } + existingServers = servers; + + } +} + +void IceoryxDiscovery::Stop() { + // TODO: Stop your discovery service implementation here +} + +std::vector IceoryxDiscovery::GetServers() const { + return {}; +} + +std::vector IceoryxDiscovery::GetNewServers(const std::vector& existingServers, const std::vector& newServers) { + std::vector newServersList; + return newServersList; +} + +std::set IceoryxDiscovery::GetRemovedServers(const std::vector& existingServers, const std::vector& newServers) { + std::set removedServersList; + return removedServersList; +} + +std::vector IceoryxDiscovery::FindServices(const ServiceMetaInfo& info) { + iox::capro::IdString_t serviceStr(iox::TruncateToCapacity, info.service.c_str()); + iox::capro::IdString_t instanceStr(iox::TruncateToCapacity, info.instance.c_str()); + iox::capro::IdString_t eventStr(iox::TruncateToCapacity, info.instance.c_str()); + + iox::optional service = serviceStr; + iox::optional instance = instanceStr; + iox::optional event = eventStr; + + if (info.service == "") { + //service = iox::capro::Wildcard; + service = iox::optional(iox::capro::Wildcard); + + } + if (info.instance == "") { + //instance = iox::capro::Wildcard; + instance = iox::optional(iox::capro::Wildcard); + + } + if (info.event == "") { + //event = iox::capro::Wildcard; + event = iox::optional(iox::capro::Wildcard); + } + + std::vector results; + serviceDiscovery_.findService(service, instance, event, + [&results](const iox::capro::ServiceDescription& serviceDescription) { + results.push_back(serviceDescription); + }, + iox::popo::MessagingPattern::REQ_RES + ); + std::vector metainfos; + for (const iox::capro::ServiceDescription& result : results) { + ServiceMetaInfo metaInfo; + metaInfo.service = result.getServiceIDString().c_str(); + metaInfo.instance = result.getInstanceIDString().c_str(); + metaInfo.event = result.getEventIDString().c_str(); + metainfos.push_back(metaInfo); + // std::cout << "Found service: " << metaInfo.service + // << " instance: " << metaInfo.instance << " event: " << metaInfo.event << std::endl; + } + return metainfos; +} + +void IceoryxDiscovery::CreateServer(const ServiceMetaInfo& server) { + // TODO: Implement logic to handle new servers here +} + +void IceoryxDiscovery::DeleteServer(const ServiceMetaInfo& server) { + +} + +} // namespace discovery +} // namespace intercept \ No newline at end of file diff --git a/intercept/discovery/iceoryx_discovery.h b/intercept/discovery/iceoryx_discovery.h new file mode 100644 index 0000000..96be187 --- /dev/null +++ b/intercept/discovery/iceoryx_discovery.h @@ -0,0 +1,41 @@ +#pragma once +#include "discovery.h" +#include "iceoryx_posh/runtime/service_discovery.hpp" + +namespace intercept { +namespace discovery { + +class IceoryxDiscovery : public Discovery +{ +public: + IceoryxDiscovery(); + + virtual ~IceoryxDiscovery(); + + virtual void Init(); + + virtual void Start(); + + virtual void Stop(); + + virtual std::vector GetServers() const; + + virtual std::vector GetNewServers(const std::vector& oldservers, + const std::vector& newservers); + + virtual std::set GetRemovedServers( + const std::vector& oldservers, const std::vector& newservers); + + virtual std::vector FindServices(const ServiceMetaInfo& info); + + virtual void CreateServer(const ServiceMetaInfo& serverInfo); + + virtual void DeleteServer(const ServiceMetaInfo& serverInfo); + +private: + iox::runtime::ServiceDiscovery serviceDiscovery_; +}; + +} // namespace discovery +} // namespace intercept + diff --git a/intercept/filesystem/CMakeLists.txt b/intercept/filesystem/CMakeLists.txt new file mode 100644 index 0000000..aeaeffe --- /dev/null +++ b/intercept/filesystem/CMakeLists.txt @@ -0,0 +1,28 @@ +find_library(ICEORYX_POSH_LIB iceoryx_posh PATHS ../thirdparties/iceoryx/lib) +find_library(ICEORYX_HOOFS_LIB iceoryx_hoofs PATHS ../thirdparties/iceoryx/lib) +find_library(ICEORYX_PLATFORM_LIB iceoryx_platform PATHS ../thirdparties/iceoryx/lib) + +file(GLOB FILESYSTEM_SOURCES *.cpp) +file(GLOB FILESYSTEM_HEADERS *.h) + +add_library(intercept_filesystem ${FILESYSTEM_SOURCES}) +target_include_directories(intercept_filesystem PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries(intercept_filesystem PUBLIC + ${ICEORYX_POSH_LIB} ${ICEORYX_HOOFS_LIB} ${ICEORYX_PLATFORM_LIB} + hybridcache_local madfs_global s3fs_lib ${THIRD_PARTY_LIBRARIES} common_lib + -pthread + -lcurl + -lxml2 + -lcrypto + -ldl + -laio + -lrt +) + +add_library(intercept_filesystem_client INTERFACE) +target_include_directories(intercept_filesystem_client INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries(intercept_filesystem_client INTERFACE + common_lib_client + -lrt +) +target_compile_options(intercept_filesystem_client INTERFACE -DCLIENT_BUILD) \ No newline at end of file diff --git a/intercept/filesystem/abstract_filesystem.h b/intercept/filesystem/abstract_filesystem.h new file mode 100644 index 0000000..7dc3c9b --- /dev/null +++ b/intercept/filesystem/abstract_filesystem.h @@ -0,0 +1,57 @@ +#ifndef ABSTRACT_FILESYSTEM_H +#define ABSTRACT_FILESYSTEM_H + +#include +#include +#include +#include +#include + +#include "common/common.h" + +namespace intercept { +namespace filesystem { + +using intercept::common::DirStream; +class AbstractFileSystem { +public: + virtual ~AbstractFileSystem() {} + virtual void Init() = 0; + virtual void Shutdown() = 0; + virtual int Open(const char* path, int flags, int mode) = 0; + virtual ssize_t Read(int fd, void* buf, size_t count) = 0; + virtual ssize_t Write(int fd, const void* buf, size_t count) = 0; + virtual int Close(int fd) = 0; + virtual off_t Lseek(int fd, off_t offset, int whence) = 0; + virtual int Stat(const char* path, struct stat* st) = 0; + virtual int Fstat(int fd, struct stat* st) = 0; + virtual int Fsync(int fd) = 0; + virtual int Truncate(const char* path, off_t length) = 0; + virtual int Ftruncate(int fd, off_t length) = 0; + virtual int Unlink(const char* path) = 0; + virtual int Mkdir(const char* path, mode_t mode) = 0; + virtual int Opendir(const char* path, DirStream* dirstream) = 0; + virtual int Getdents(DirStream* dirstream, char* contents, size_t maxread, ssize_t* realbytes) = 0; + virtual int Closedir(DirStream* dirstream) = 0; + virtual int Rmdir(const char* path) = 0; + virtual int Chmod(const char* path, mode_t mode) = 0; + virtual int Chown(const char* path, uid_t owner, gid_t group) = 0; + virtual int Rename(const char* oldpath, const char* newpath) = 0; + virtual int Link(const char* oldpath, const char* newpath) = 0; + virtual int Symlink(const char* oldpath, const char* newpath) = 0; + virtual int Readlink(const char* path, char* buf, size_t bufsize) = 0; + virtual int Utime(const char* path, const struct utimbuf* times) = 0; + + virtual ssize_t MultiRead(int fd, void* buf, size_t count) {} + virtual ssize_t MultiWrite(int fd, const void* buf, size_t count) {} + +protected: + virtual std::string NormalizePath(const std::string& path) = 0; +}; + +} // namespace filesystem +} // namespace intercept + + + +#endif // ABSTRACT_FILESYSTEM_H diff --git a/intercept/filesystem/curve_filesystem.cpp b/intercept/filesystem/curve_filesystem.cpp new file mode 100644 index 0000000..67aea1b --- /dev/null +++ b/intercept/filesystem/curve_filesystem.cpp @@ -0,0 +1,166 @@ +#include +#include + +#include "curve_filesystem.h" +#include "libcurvefs_external.h" + +namespace intercept { +namespace filesystem { + +#define POSIX_SET_ATTR_SIZE (1 << 3) +CurveFileSystem::CurveFileSystem() {} +CurveFileSystem::~CurveFileSystem() { + curvefs_release(instance_); +} + +void CurveFileSystem::Init() { + instance_ = curvefs_create(); + curvefs_load_config(instance_, "./curve_posix_client.conf"); + //curvefs_mount(instance_, "s3cy1", "/tmp/curvefs"); + curvefs_mount(instance_, "s3cy1", "/"); + std::cout << "finish curvefs create" << std::endl; +} + +void CurveFileSystem::Shutdown() { + +} + +int CurveFileSystem::Open(const char* path, int flags, int mode) { + std::cout << "open, the path: " << path << std::endl; + int ret = curvefs_open(instance_, path, flags, mode); + // 注意,EEXIST为17, 那么当fd ret也是17时 是不是就会判断错误。 + if (ret == EEXIST) { // 不去创建 + ret = curvefs_open(instance_, path, flags & ~O_CREAT, mode); + } + //std::cout << "the path: " << path << " , the stat: " << tmp.st_size << " , the time: " << tmp.st_mtime << std::endl; + return ret; +} + +ssize_t CurveFileSystem::Read(int fd, void* buf, size_t count) { + int ret = curvefs_read(instance_, fd, (char*)buf, count); + //int ret = count; + //std::cout << "read, the fd: " << fd << " the buf: " << (char*)buf << ", the count: " << count << ", the ret: " << ret << std::endl; + return ret; +} + +ssize_t CurveFileSystem::Write(int fd, const void* buf, size_t count) { + int ret = curvefs_write(instance_, fd, (char*)buf, count); + //int ret = count; + //std::cout << "write, the fd: " << fd << " the buf: " << (char*)buf << ", the count: " << count << ", the ret: " << ret << std::endl; + return ret; +} + +int CurveFileSystem::Close(int fd) { + int ret = curvefs_close(instance_, fd); + std::cout << "curve close, the fd: " << fd << std::endl; + return ret; +} + +off_t CurveFileSystem::Lseek(int fd, off_t offset, int whence) { + int ret = curvefs_lseek(instance_, fd, offset, whence); + std::cout << "curve lseek, the fd: " << fd << ", the offset: " << offset << ", the whence: " << whence << ", the ret: " << ret << std::endl; + return ret; +} + +int CurveFileSystem::Stat(const char* path, struct stat* st) { + int ret = curvefs_lstat(instance_, path, st); + return ret; +} + +int CurveFileSystem::Fstat(int fd, struct stat* st) { + int ret = curvefs_fstat(instance_, fd, st); + return ret; +} + +int CurveFileSystem::Fsync(int fd) { + int ret = curvefs_fsync(instance_, fd); + return ret; +} + +int CurveFileSystem::Ftruncate(int fd, off_t length) { + throw std::runtime_error("未实现"); +} + +int CurveFileSystem::Unlink(const char* path) { + int ret = curvefs_unlink(instance_, path); + std::cout << "unlink, the path: " << path << ", the ret: " << ret << std::endl; + return ret; +} + +int CurveFileSystem::Mkdir(const char* path, mode_t mode) { + int ret = curvefs_mkdir(instance_, path, mode); + std::cout << "mkdir, the path: " << path << ", the mode: " << mode << ", the ret: " << ret << std::endl; + return ret; +} + +int CurveFileSystem::Opendir(const char* path, DirStream* dirstream) { + int ret = curvefs_opendir(instance_, path, (dir_stream_t*)dirstream); + std::cout << "opendir, the path: " << path << ", the dirstream ino: " << dirstream->ino << ", the ret: " << ret << std::endl; + return ret; +} + +int CurveFileSystem::Getdents(DirStream* dirstream, char* contents, size_t maxread, ssize_t* realbytes) { + int ret = curvefs_getdents(instance_, (dir_stream_t*)dirstream, contents, maxread, realbytes); + std::cout << "getdents, the dirstream ino: " << dirstream->ino << ", the maxread: " << maxread << ", the realbytes: " << realbytes << ", the ret: " << ret << std::endl; + return ret; +} + +int CurveFileSystem::Closedir(DirStream* dirstream) { + int ret = curvefs_closedir(instance_, (dir_stream_t*)dirstream);; + std::cout << "closedir, the fd: " << dirstream->fh << " ino:" << dirstream->ino << std::endl; + return ret; +} + +int CurveFileSystem::Rmdir(const char* path) { + int ret = curvefs_rmdir(instance_, path); + std::cout << "rmdir, the path: " << path << ", the ret: " << ret << std::endl; + return ret; +} + +int CurveFileSystem::Rename(const char* oldpath, const char* newpath) { + int ret = curvefs_rename(instance_, oldpath, newpath); + std::cout << "rename, the oldpath: " << oldpath << ", the newpath: " << newpath << ", the ret: " << ret << std::endl; + return ret; + +} +int CurveFileSystem::Link(const char* oldpath, const char* newpath) { + throw std::runtime_error("未实现"); +} + +int CurveFileSystem::Symlink(const char* oldpath, const char* newpath) { + throw std::runtime_error("未实现"); +} + +int CurveFileSystem::Readlink(const char* path, char* buf, size_t bufsize) { + throw std::runtime_error("未实现"); +} + +int CurveFileSystem::Chmod(const char* path, mode_t mode) { + throw std::runtime_error("未实现"); +} + +int CurveFileSystem::Chown(const char* path, uid_t uid, gid_t gid) { + throw std::runtime_error("未实现"); +} + +int CurveFileSystem::Truncate(const char* path, off_t length) { + struct stat attr; + attr.st_size = length; + int set = POSIX_SET_ATTR_SIZE ; + int ret = curvefs_setattr(instance_, path, &attr, set); + return ret; +} + +int CurveFileSystem::Utime(const char* path, const struct utimbuf* ubuf) { + throw std::runtime_error("未实现"); +} + + + +std::string CurveFileSystem::NormalizePath(const std::string& path) { + throw std::runtime_error("未实现"); +} + + +} // namespace filesystem +} // namespace intercept diff --git a/intercept/filesystem/curve_filesystem.h b/intercept/filesystem/curve_filesystem.h new file mode 100644 index 0000000..306b802 --- /dev/null +++ b/intercept/filesystem/curve_filesystem.h @@ -0,0 +1,47 @@ +#ifndef CURVE_FILESYSTEM_H +#define CURVE_FILESYSTEM_H + +#include "abstract_filesystem.h" +namespace intercept { +namespace filesystem { +class CurveFileSystem : public AbstractFileSystem { +public: + CurveFileSystem(); + ~CurveFileSystem() override; + void Init() override; + void Shutdown() override; + int Open(const char* path, int flags, int mode) override; + ssize_t Read(int fd, void* buf, size_t count) override; + ssize_t Write(int fd, const void* buf, size_t count) override; + int Close(int fd) override; + off_t Lseek(int fd, off_t offset, int whence) override; + int Stat(const char* path, struct stat* st) override; + int Fstat(int fd, struct stat* st) override; + int Fsync(int fd) override; + int Ftruncate(int fd, off_t length) override; + int Unlink(const char* path) override; + int Mkdir(const char* path, mode_t mode) override; + int Opendir(const char* path, DirStream* dirstream); + int Getdents(DirStream* dirstream, char* contents, size_t maxread, ssize_t* realbytes); + int Closedir(DirStream* dirstream); + int Rmdir(const char* path) override; + int Rename(const char* from, const char* to) override; + int Link(const char* from, const char* to) override; + int Symlink(const char* from, const char* to) override; + int Readlink(const char* path, char* buf, size_t bufsize) override; + int Chmod(const char* path, mode_t mode) override; + int Chown(const char* path, uid_t uid, gid_t gid) override; + int Truncate(const char* path, off_t length) override; + int Utime(const char* path, const struct utimbuf* times) override; + + +protected: + std::string NormalizePath(const std::string& path) override; + uintptr_t instance_; +}; + +} // namespace filesystem +} // namespace intercept + + +#endif // CURVE_FILESYSTEM_H diff --git a/intercept/filesystem/dummy_filesystem.cpp b/intercept/filesystem/dummy_filesystem.cpp new file mode 100644 index 0000000..79825bf --- /dev/null +++ b/intercept/filesystem/dummy_filesystem.cpp @@ -0,0 +1,186 @@ +#include +#include + +#include "dummy_filesystem.h" + +namespace intercept { +namespace filesystem{ + +std::size_t g_size = 10240000000; +char* DummyFileSystem::memory_ = nullptr; + +DummyFileSystem::DummyFileSystem() +{ + if (memory_ == nullptr) { + memory_ = new char[g_size]; + //memset(memory_, 'j', g_size); + std::cout << "Memory allocated for shared_memory" << std::endl; + } + std::cout << "DummyFileSystem created" << std::endl; +} + +DummyFileSystem::~DummyFileSystem() +{ + std::cout << "DummyFileSystem destroyed, copy num: " << copynum_ << std::endl; + if (memory_ != nullptr) { + delete[] memory_; + memory_ = nullptr; + std::cout << "Memory deallocated for shared_memory" << std::endl; + } +} + +void DummyFileSystem::Init() { + std::cout << "DummyFileSystem Init" << std::endl; +} + +void DummyFileSystem::Shutdown() { + std::cout << "DummyFileSystem Shutdown" << std::endl; +} + +int DummyFileSystem::Open(const char* path, int flags, int mode) { + fd_.fetch_add(1); + std::cout << "DummyFileSystem Open: " << path << " ret: " << fd_.load() << std::endl; + + return fd_.load(); +} + +char buffer[] = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +ssize_t DummyFileSystem::Read(int fd, void* buf, size_t count) { + // std::cout << "DummyFileSystem Read: " << fd << std::endl; + offset_ += count; + if (offset_ > g_size - count) { + // std::cout << "begin offset_: " << offset_ << " g_size: "<< g_size << ", count: " << count << std::endl; + offset_ = offset_ % (g_size - 10000000); + // std::cout << "after offset_: " << offset_ << std::endl; + } + if (offset_ < (g_size - 10000000)) { + memcpy((char*)buf, memory_ + offset_, count); + // memcpy((char*)buf, buffer, count); + } else { + memcpy((char*)buf, memory_ + 128, count); + // memcpy((char*)buf, buffer, count); + } + copynum_++; + return count; +} + +ssize_t DummyFileSystem::Write(int fd, const void* buf, size_t count) { + std::cout << "DummyFileSystem Write: " << fd << ", count: " << count << std::endl; + //memcpy(memory_ + offset_, buf, count); + return count; +} + +int DummyFileSystem::Close(int fd) { + std::cout << "DummyFileSystem Close: " << fd << " ,copynum_ :" << copynum_ << std::endl; + return 0; +} + + +off_t DummyFileSystem::Lseek(int fd, off_t offset, int whence) { + std::cout << "DummyFileSystem Lseek: " << fd << std::endl; + + if (offset_ > g_size - 10000000) { + offset_ = offset_ % (g_size-10000000); + } else { + offset_ = offset; + } + return 0; +} + +int DummyFileSystem::Stat(const char* path, struct stat* buf) { + buf->st_ino = 111111; + std::cout << "DummyFileSystem Stat: " << path << std::endl; + return 0; +} + +int DummyFileSystem::Fstat(int fd, struct stat* buf) { + std::cout << "DummyFileSystem Fstat: " << fd << std::endl; + return 0; +} + +int DummyFileSystem::Fsync(int fd) { + std::cout << "DummyFileSystem Fsync: " << fd << std::endl; + return 0; +} + +int DummyFileSystem::Ftruncate(int fd, off_t length) { + std::cout << "DummyFileSystem Ftruncate: " << fd << std::endl; + return 0; +} + + +int DummyFileSystem::Unlink(const char* path) { + std::cout << "DummyFileSystem Unlink: " << path << std::endl; + return 0; +} + + +int DummyFileSystem::Mkdir(const char* path, mode_t mode) { + std::cout << "DummyFileSystem Mkdir: " << path << std::endl; + return 0; +} + +int DummyFileSystem::Opendir(const char* path, DirStream* dirstream) { + std::cout << "DummyFileSystem Opendir: " << path << std::endl; + return 0; +} + +int DummyFileSystem::Getdents(DirStream* dirstream, char* contents, size_t maxread, ssize_t* realbytes) { + std::cout << "DummyFileSystem getdentes: " << std::endl; + return 0; +} + + +int DummyFileSystem::Closedir(DirStream* dirstream) { + std::cout << "DummyFileSystem Closedir: " << std::endl; + return 0; +} + +int DummyFileSystem::Rmdir(const char* path) { + std::cout << "DummyFileSystem Rmdir: " << path << std::endl; + return 0; +} + +int DummyFileSystem::Rename(const char* oldpath, const char* newpath) { + std::cout << "DummyFileSystem Rename: " << oldpath << " to " << newpath << std::endl; + return 0; +} + +int DummyFileSystem::Link(const char* oldpath, const char* newpath) { + std::cout << "DummyFileSystem Link: " << oldpath << " to " << newpath << std::endl; + return 0; +} + +int DummyFileSystem::Symlink(const char* oldpath, const char* newpath) { + std::cout << "DummyFileSystem Symlink: " << oldpath << std::endl; + return 0; +} + +int DummyFileSystem::Readlink(const char* path, char* buf, size_t bufsize) { + throw std::runtime_error("未实现"); +} + +int DummyFileSystem::Chmod(const char* path, mode_t mode) { + throw std::runtime_error("未实现"); +} + +int DummyFileSystem::Chown(const char* path, uid_t uid, gid_t gid) { + throw std::runtime_error("未实现"); +} + +int DummyFileSystem::Truncate(const char* path, off_t length) { + return 0; +} + +int DummyFileSystem::Utime(const char* path, const struct utimbuf* ubuf) { + throw std::runtime_error("未实现"); +} + + + +std::string DummyFileSystem::NormalizePath(const std::string& path) { + throw std::runtime_error("未实现"); +} + +} // namespace intercept +} // namespace filesystem diff --git a/intercept/filesystem/dummy_filesystem.h b/intercept/filesystem/dummy_filesystem.h new file mode 100644 index 0000000..aa0b02b --- /dev/null +++ b/intercept/filesystem/dummy_filesystem.h @@ -0,0 +1,50 @@ +#ifndef DUMMY_FILESYSTEM_H +#define DUMMY_FILESYSTEM_H +#include + +#include "abstract_filesystem.h" +namespace intercept { +namespace filesystem { +class DummyFileSystem : public AbstractFileSystem { +public: + DummyFileSystem(); + ~DummyFileSystem() override; + void Init() override; + void Shutdown() override; + int Open(const char* path, int flags, int mode) override; + ssize_t Read(int fd, void* buf, size_t count) override; + ssize_t Write(int fd, const void* buf, size_t count) override; + int Close(int fd) override; + off_t Lseek(int fd, off_t offset, int whence) override; + int Stat(const char* path, struct stat* st) override; + int Fstat(int fd, struct stat* st) override; + int Fsync(int fd) override; + int Ftruncate(int fd, off_t length) override; + int Unlink(const char* path) override; + int Mkdir(const char* path, mode_t mode) override; + int Opendir(const char* path, DirStream* dirstream); + int Getdents(DirStream* dirstream, char* contents, size_t maxread, ssize_t* realbytes); + int Closedir(DirStream* dirstream); + int Rmdir(const char* path) override; + int Rename(const char* from, const char* to) override; + int Link(const char* from, const char* to) override; + int Symlink(const char* from, const char* to) override; + int Readlink(const char* path, char* buf, size_t bufsize) override; + int Chmod(const char* path, mode_t mode) override; + int Chown(const char* path, uid_t uid, gid_t gid) override; + int Truncate(const char* path, off_t length) override; + int Utime(const char* path, const struct utimbuf* times) override; + + +protected: + std::string NormalizePath(const std::string& path) override; + uintptr_t instance_; + std::atomic fd_ = 0; + off_t offset_ = 0; + long copynum_ = 0; + static char* memory_; +}; + +} // namespace filesystem +} // namespace intercept +#endif // DUMMY_FILESYSTEM_H \ No newline at end of file diff --git a/intercept/filesystem/libcurvefs_external.cpp b/intercept/filesystem/libcurvefs_external.cpp new file mode 100644 index 0000000..5cb3078 --- /dev/null +++ b/intercept/filesystem/libcurvefs_external.cpp @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2023 NetEase Inc. + * + * 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. + */ + +/* + * Project: Curve + * Created Date: 2023-07-12 + * Author: Jingli Chen (Wine93) + */ + + +#include +#include +#include +#include + +#include "libcurvefs_external.h" + + +uintptr_t curvefs_create() {return 0;} + +void curvefs_load_config(uintptr_t instance_ptr, + const char* config_file) {} + +void curvefs_release(uintptr_t instance_ptr) {} + +// NOTE: instance_ptr is the pointer of curvefs_mount_t instance. +void curvefs_conf_set(uintptr_t instance_ptr, + const char* key, + const char* value) {} + +int curvefs_mount(uintptr_t instance_ptr, + const char* fsname, + const char* mountpoint) {return 0;} + +int curvefs_umonut(uintptr_t instance_ptr, + const char* fsname, + const char* mountpoint) {return 0;} + +// directory +int curvefs_mkdir(uintptr_t instance_ptr, const char* path, uint16_t mode) {return 0;} + +int curvefs_mkdirs(uintptr_t instance_ptr, const char* path, uint16_t mode) {return 0;} + +int curvefs_rmdir(uintptr_t instance_ptr, const char* path) {return 0;} + +int curvefs_opendir(uintptr_t instance_ptr, + const char* path, + dir_stream_t* dir_stream) {return 0;} + +ssize_t curvefs_readdir(uintptr_t instance_ptr, + dir_stream_t* dir_stream, + dirent_t* dirent) {return 0;} + +int curvefs_getdents(uintptr_t instance_ptr, + dir_stream_t* dir_stream, + char* data, size_t maxread, ssize_t* realbytes) {return 0;} + +int curvefs_closedir(uintptr_t instance_ptr, dir_stream_t* dir_stream) {return 0;} + +// file +int curvefs_open(uintptr_t instance_ptr, + const char* path, + uint32_t flags, + uint16_t mode) {return 0;} + +int curvefs_lseek(uintptr_t instance_ptr, + int fd, + uint64_t offset, + int whence){return 0;} + +ssize_t curvefs_read(uintptr_t instance_ptr, + int fd, + char* buffer, + size_t count) {return 0;} + +ssize_t curvefs_write(uintptr_t instance_ptr, + int fd, + char* buffer, + size_t count) {return 0;} + +int curvefs_fsync(uintptr_t instance_ptr, int fd) {return 0;} + +int curvefs_close(uintptr_t instance_ptr, int fd) {return 0;} + +int curvefs_unlink(uintptr_t instance_ptr, const char* path) {return 0;} + +// others +int curvefs_statfs(uintptr_t instance_ptr, struct statvfs* statvfs) {return 0;} + +int curvefs_lstat(uintptr_t instance_ptr, const char* path, struct stat* stat) {return 0;} + +int curvefs_fstat(uintptr_t instance_ptr, int fd, struct stat* stat) {return 0;} + +int curvefs_setattr(uintptr_t instance_ptr, + const char* path, + struct stat* stat, + int to_set) {return 0;} + +int curvefs_chmod(uintptr_t instance_ptr, const char* path, uint16_t mode) {return 0;} + +int curvefs_chown(uintptr_t instance_ptr, + const char* path, + uint32_t uid, + uint32_t gid) {return 0;} + +int curvefs_rename(uintptr_t instance_ptr, + const char* oldpath, + const char* newpath) {return 0;} diff --git a/intercept/filesystem/libcurvefs_external.h b/intercept/filesystem/libcurvefs_external.h new file mode 100644 index 0000000..7909596 --- /dev/null +++ b/intercept/filesystem/libcurvefs_external.h @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2023 NetEase Inc. + * + * 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. + */ + +/* + * Project: Curve + * Created Date: 2023-07-12 + * Author: Jingli Chen (Wine93) + */ + +#ifndef CURVEFS_SDK_LIBCURVEFS_LIBCURVEFS_H_ +#define CURVEFS_SDK_LIBCURVEFS_LIBCURVEFS_H_ + +#include +#include +#include +#include + + +// Must be synchronized with DirStream if changed +typedef struct { + uint64_t ino; + uint64_t fh; + uint64_t offset; +} dir_stream_t; + +typedef struct { + struct stat stat; + char name[256]; +} dirent_t; + +#ifdef __cplusplus +extern "C" { +#endif + +uintptr_t curvefs_create(); + +void curvefs_load_config(uintptr_t instance_ptr, + const char* config_file); + +void curvefs_release(uintptr_t instance_ptr); + +// NOTE: instance_ptr is the pointer of curvefs_mount_t instance. +void curvefs_conf_set(uintptr_t instance_ptr, + const char* key, + const char* value); + +int curvefs_mount(uintptr_t instance_ptr, + const char* fsname, + const char* mountpoint); + +int curvefs_umonut(uintptr_t instance_ptr, + const char* fsname, + const char* mountpoint); + +// directory +int curvefs_mkdir(uintptr_t instance_ptr, const char* path, uint16_t mode); + +int curvefs_mkdirs(uintptr_t instance_ptr, const char* path, uint16_t mode); + +int curvefs_rmdir(uintptr_t instance_ptr, const char* path); + +int curvefs_opendir(uintptr_t instance_ptr, + const char* path, + dir_stream_t* dir_stream); + +ssize_t curvefs_readdir(uintptr_t instance_ptr, + dir_stream_t* dir_stream, + dirent_t* dirent); + +int curvefs_getdents(uintptr_t instance_ptr, + dir_stream_t* dir_stream, + char* data, size_t maxread, ssize_t* realbytes); + +int curvefs_closedir(uintptr_t instance_ptr, dir_stream_t* dir_stream); + +// file +int curvefs_open(uintptr_t instance_ptr, + const char* path, + uint32_t flags, + uint16_t mode); + +int curvefs_lseek(uintptr_t instance_ptr, + int fd, + uint64_t offset, + int whence); + +ssize_t curvefs_read(uintptr_t instance_ptr, + int fd, + char* buffer, + size_t count); + +ssize_t curvefs_write(uintptr_t instance_ptr, + int fd, + char* buffer, + size_t count); + +int curvefs_fsync(uintptr_t instance_ptr, int fd); + +int curvefs_close(uintptr_t instance_ptr, int fd); + +int curvefs_unlink(uintptr_t instance_ptr, const char* path); + +// others +int curvefs_statfs(uintptr_t instance_ptr, struct statvfs* statvfs); + +int curvefs_lstat(uintptr_t instance_ptr, const char* path, struct stat* stat); + +int curvefs_fstat(uintptr_t instance_ptr, int fd, struct stat* stat); + +int curvefs_setattr(uintptr_t instance_ptr, + const char* path, + struct stat* stat, + int to_set); + +int curvefs_chmod(uintptr_t instance_ptr, const char* path, uint16_t mode); + +int curvefs_chown(uintptr_t instance_ptr, + const char* path, + uint32_t uid, + uint32_t gid); + +int curvefs_rename(uintptr_t instance_ptr, + const char* oldpath, + const char* newpath); +#ifdef __cplusplus +} +#endif + +#endif // CURVEFS_SDK_LIBCURVEFS_LIBCURVEFS_H_ diff --git a/intercept/filesystem/s3fs_filesystem.cpp b/intercept/filesystem/s3fs_filesystem.cpp new file mode 100644 index 0000000..9dabc1a --- /dev/null +++ b/intercept/filesystem/s3fs_filesystem.cpp @@ -0,0 +1,222 @@ +#include +#include +#include "spdlog/spdlog.h" + +#include "s3fs_filesystem.h" +#include "s3fs_lib.h" + +namespace intercept { +namespace filesystem { + +S3fsFileSystem::S3fsFileSystem() { + +} + +S3fsFileSystem::~S3fsFileSystem() { + s3fs_global_uninit(); +} + + +void S3fsFileSystem::Init() { + s3fs_global_init(); +} + +void S3fsFileSystem::Shutdown() { + std::cout << "S3fsFileSystem::Shutdown" << std::endl; +} + +int S3fsFileSystem::Open(const char* path, int flags, int mode) { + // std::cout << "S3fsFileSystem::Open: " << path << std::endl; + spdlog::info("S3fsFileSystem::Open:{}", path); + return posix_s3fs_open(path, flags, mode); +} + +ssize_t S3fsFileSystem::MultiRead(int fd, void* buf, size_t count) { + intercept::common::Timer timer("server S3fsFileSystem::MultiRead"); + int numThreads = intercept::common::Configure::getInstance().getConfig("opThreadnum") == "" ? + 1 : atoi(intercept::common::Configure::getInstance().getConfig("opThreadnum").c_str()); + size_t partSize = count / numThreads; // Part size for each thread + size_t remaining = count % numThreads; // Remaining part + + std::vector threads; + char* charBuf = static_cast(buf); + + std::atomic totalBytesRead(0); // Atomic variable to accumulate bytes read + std::mutex readMutex; // Mutex to protect shared variable + + for (size_t i = 0; i < numThreads; ++i) { + size_t offset = i * partSize; + size_t size = (i == numThreads - 1) ? (partSize + remaining) : partSize; + threads.emplace_back([=, &totalBytesRead, &readMutex]() { + ssize_t bytesRead = posix_s3fs_multiread(fd, charBuf + offset, size, offset); + spdlog::debug("S3fsFileSystem::MultiRead, fd: {}, offset: {}, size: {}, bytesRead: {}", fd, offset, size, bytesRead); + std::lock_guard lock(readMutex); + totalBytesRead += bytesRead; + }); + } + for (auto& th : threads) { + th.join(); + } + posix_s3fs_lseek(fd, totalBytesRead.load(), SEEK_CUR); + spdlog::info("S3fsFileSystem::MultiRead, read bytes: {}", totalBytesRead.load()); + return totalBytesRead.load(); // Return the total bytes read +} + +ssize_t S3fsFileSystem::Read(int fd, void* buf, size_t count) { + // std::cout << "S3fsFileSystem::Read: " << fd << std::endl; + spdlog::debug("S3fsFileSystem::Read, fd: {}, count: {}", fd, count); + return posix_s3fs_read(fd, buf, count); +} + +ssize_t S3fsFileSystem::MultiWrite(int fd, const void* buf, size_t count) { + intercept::common::Timer timer("server S3fsFileSystem::MultiWrite"); + int numThreads = intercept::common::Configure::getInstance().getConfig("opThreadnum") == "" ? + 1 : atoi(intercept::common::Configure::getInstance().getConfig("opThreadnum").c_str()); + size_t partSize = count / numThreads; // Part size for each thread + size_t remaining = count % numThreads; // Remaining part + + std::vector threads; + const char* charBuf = static_cast(buf); + + std::atomic totalBytesWrite(0); // Atomic variable to accumulate bytes write + std::mutex writeMutex; // Mutex to protect shared variable + + for (size_t i = 0; i < numThreads; ++i) { + size_t offset = i * partSize; + size_t size = (i == numThreads - 1) ? (partSize + remaining) : partSize; + threads.emplace_back([=, &totalBytesWrite, &writeMutex]() { + ssize_t bytesWrite = posix_s3fs_multiwrite(fd, charBuf + offset, size, offset); + spdlog::debug("finish S3fsFileSystem::Multiwrite, fd: {}, offset: {}, size: {}, bytesRead: {}", fd, offset, size, bytesWrite); + std::lock_guard lock(writeMutex); + totalBytesWrite += bytesWrite; + }); + } + for (auto& th : threads) { + th.join(); + } + posix_s3fs_lseek(fd, totalBytesWrite.load(), SEEK_CUR); + spdlog::debug("S3fsFileSystem::Multiwrite, multiwrite bytes: {}", totalBytesWrite.load()); + return totalBytesWrite.load(); // Return the total bytes write +} + +ssize_t S3fsFileSystem::Write(int fd, const void* buf, size_t count) { + // std::cout << "S3fsFileSystem::Write: " << fd << std::endl; + spdlog::debug("S3fsFileSystem::Write, fd: {}, count: {}", fd, count); + return posix_s3fs_write(fd, buf, count); +} +int S3fsFileSystem::Close(int fd) { + //std::cout << "S3fsFileSystem::Close: " << fd << std::endl; + spdlog::info("S3fsFileSystem::Close, fd: {}", fd); + return posix_s3fs_close(fd); +} + +off_t S3fsFileSystem::Lseek(int fd, off_t offset, int whence) { + //std::cout << "S3fsFileSystem::Lseek: " << fd << std::endl; + spdlog::debug("S3fsFileSystem::Lseek, fd: {}, offset: {}, whence: {}", fd, offset, whence); + return posix_s3fs_lseek(fd, offset, whence); +} + +int S3fsFileSystem::Stat(const char* path, struct stat* statbuf) { + // std::cout << "S3fsFileSystem::Stat: " << path << std::endl; + spdlog::info("S3fsFileSystem::Stat, path: {}", path); + int ret = posix_s3fs_stat(path, statbuf); + return ret; +} + +int S3fsFileSystem::Fstat(int fd, struct stat* statbuf) { + // std::cout << "S3fsFileSystem::Fstat: " << fd << std::endl; + spdlog::info("S3fsFileSystem::Stat, fd: {}", fd); + int ret = posix_s3fs_fstat(fd, statbuf); + return ret; +} + +int S3fsFileSystem::Fsync(int fd) { + // std::cout << "S3fsFileSystem::Fsync: " << fd << std::endl; + spdlog::info("S3fsFileSystem::Fsync, fd: {} no implement....", fd); + return 0; +} + +int S3fsFileSystem::Ftruncate(int fd, off_t length) { + // std::cout << "S3fsFileSystem::Ftruncate: " << fd << " " << length << std::endl; + spdlog::info("S3fsFileSystem::Ftruncate, fd: {} length: {} no implement...", fd, length); + return 0; +} + +int S3fsFileSystem::Unlink(const char* path) { + // std::cout << "S3fsFileSystem::Unlink: " << path << std::endl; + spdlog::info("S3fsFileSystem::Unlink, path: {}", path); + return posix_s3fs_unlink(path); +} +int S3fsFileSystem::Mkdir(const char* path, mode_t mode) { + // std::cout << "S3fsFileSystem::Mkdir: " << path << " " << mode << std::endl; + spdlog::info("S3fsFileSystem::Mkdir, path: {} mode: {}", path, mode); + return posix_s3fs_mkdir(path, mode); +} + +int S3fsFileSystem::Opendir(const char* path, DirStream* dirstream) { + int ret = posix_s3fs_opendir(path, (S3DirStream*)dirstream); + // std::cout << "S3fsFileSystem::Opendir: " << path << std::endl; + spdlog::info("S3fsFileSystem::Opendir path: {} ret {}", path, ret); + return 0; +} + +int S3fsFileSystem::Getdents(DirStream* dirstream, char* contents, size_t maxread, ssize_t* realbytes) { + //std::cout << "S3fsFileSystem::Getdents: " << dirstream << " " << maxread << " " << realbytes << std::endl; + int ret = posix_s3fs_getdents((S3DirStream*)dirstream, contents, maxread, realbytes); + spdlog::info("S3fsFileSystem::Getdents, maxread: {}, realbytes: {}", maxread, *realbytes); + return ret; +} + +int S3fsFileSystem::Closedir(DirStream* dirstream) { + // std::cout << "S3fsFileSystem::Closedir: " << dirstream << std::endl; + int ret = posix_s3fs_closedir((S3DirStream*)dirstream); + spdlog::info("S3fsFileSystem::Closedir, ret: {}", ret); + return ret; +} + +int S3fsFileSystem::Rmdir(const char* path) { + std::cout << "S3fsFileSystem::Rmdir: " << path << std::endl; + return 0; +} + +int S3fsFileSystem::Rename(const char* from, const char* to) { + std::cout << "S3fsFileSystem::Rename: " << from << " to " << to << std::endl; + return 0; +} + +int S3fsFileSystem::Link(const char* oldpath, const char* newpath) { + throw std::runtime_error("未实现"); +} + +int S3fsFileSystem::Symlink(const char* oldpath, const char* newpath) { + throw std::runtime_error("未实现"); +} + +int S3fsFileSystem::Readlink(const char* path, char* buf, size_t bufsize) { + throw std::runtime_error("未实现"); +} + +int S3fsFileSystem::Chmod(const char* path, mode_t mode) { + throw std::runtime_error("未实现"); +} + +int S3fsFileSystem::Chown(const char* path, uid_t uid, gid_t gid) { + throw std::runtime_error("未实现"); +} + +int S3fsFileSystem::Truncate(const char* path, off_t length) { + std::cout << "S3fsFileSystem::Truncate" << std::endl; + return 0; +} + +int S3fsFileSystem::Utime(const char* path, const struct utimbuf* ubuf) { + throw std::runtime_error("未实现"); +} + + +std::string S3fsFileSystem::NormalizePath(const std::string& path) { + throw std::runtime_error("未实现"); +} + +} // namespace filesystem +} // namespace intercept diff --git a/intercept/filesystem/s3fs_filesystem.h b/intercept/filesystem/s3fs_filesystem.h new file mode 100644 index 0000000..33b84c7 --- /dev/null +++ b/intercept/filesystem/s3fs_filesystem.h @@ -0,0 +1,49 @@ +#ifndef S3FS_FILESYSTEM_H +#define S3FS_FILESYSTEM_H + +#include "abstract_filesystem.h" +namespace intercept { +namespace filesystem { + +class S3fsFileSystem : public AbstractFileSystem { +public: + S3fsFileSystem(); + ~S3fsFileSystem() override; + void Init() override; + void Shutdown() override; + int Open(const char* path, int flags, int mode) override; + ssize_t Read(int fd, void* buf, size_t count) override; + ssize_t Write(int fd, const void* buf, size_t count) override; + int Close(int fd) override; + off_t Lseek(int fd, off_t offset, int whence) override; + int Stat(const char* path, struct stat* st) override; + int Fstat(int fd, struct stat* st) override; + int Fsync(int fd) override; + int Ftruncate(int fd, off_t length) override; + int Unlink(const char* path) override; + int Mkdir(const char* path, mode_t mode) override; + int Opendir(const char* path, DirStream* dirstream); + int Getdents(DirStream* dirstream, char* contents, size_t maxread, ssize_t* realbytes); + int Closedir(DirStream* dirstream); + int Rmdir(const char* path) override; + int Rename(const char* from, const char* to) override; + int Link(const char* from, const char* to) override; + int Symlink(const char* from, const char* to) override; + int Readlink(const char* path, char* buf, size_t bufsize) override; + int Chmod(const char* path, mode_t mode) override; + int Chown(const char* path, uid_t uid, gid_t gid) override; + int Truncate(const char* path, off_t length) override; + int Utime(const char* path, const struct utimbuf* times) override; + + ssize_t MultiRead(int fd, void* buf, size_t count) override; + ssize_t MultiWrite(int fd, const void* buf, size_t count) override; + +protected: + std::string NormalizePath(const std::string& path) override; + +}; + +} // namespace filesystem +} // namespace intercept + +#endif \ No newline at end of file diff --git a/intercept/filesystem/s3fs_lib.h b/intercept/filesystem/s3fs_lib.h new file mode 100644 index 0000000..69a5980 --- /dev/null +++ b/intercept/filesystem/s3fs_lib.h @@ -0,0 +1,63 @@ +#ifndef S3FS_S3FS_LIB_H_ +#define S3FS_S3FS_LIB_H_ + +#ifdef S3FS_MALLOC_TRIM +#ifdef HAVE_MALLOC_TRIM +#include +#define S3FS_MALLOCTRIM(pad) malloc_trim(pad) +#else // HAVE_MALLOC_TRIM +#define S3FS_MALLOCTRIM(pad) +#endif // HAVE_MALLOC_TRIM +#else // S3FS_MALLOC_TRIM +#define S3FS_MALLOCTRIM(pad) +#endif // S3FS_MALLOC_TRIM + + +//------------------------------------------------------------------- +// posix interface functions +//------------------------------------------------------------------- +#ifdef __cplusplus +extern "C" { +#endif + +struct S3DirStream; + +void s3fs_global_init(); + +void s3fs_global_uninit(); + +int posix_s3fs_create(const char* _path, int flags, mode_t mode); + +int posix_s3fs_open(const char* _path, int flags, mode_t mode); + +int posix_s3fs_multiread(int fd, void* buf, size_t size, off_t file_offset); + +int posix_s3fs_read(int fd, void* buf, size_t size); + +int posix_s3fs_multiwrite(int fd, const void* buf, size_t size, off_t file_offset); + +int posix_s3fs_write(int fd, const void* buf, size_t size); + +off_t posix_s3fs_lseek(int fd, off_t offset, int whence); + +int posix_s3fs_close(int fd); + +int posix_s3fs_stat(const char* _path, struct stat* stbuf); + +int posix_s3fs_fstat(int fd, struct stat* stbuf) ; + +int posix_s3fs_mkdir(const char* _path, mode_t mode); + +int posix_s3fs_opendir(const char* _path, S3DirStream* dirstream); + +int posix_s3fs_getdents(S3DirStream* dirstream, char* contents, size_t maxread, ssize_t* realbytes); + +int posix_s3fs_closedir(S3DirStream* dirstream); + +int posix_s3fs_unlink(const char* _path); + +#ifdef __cplusplus +} +#endif + +#endif // S3FS_S3FS_LIB_H_ \ No newline at end of file diff --git a/intercept/internal/CMakeLists.txt b/intercept/internal/CMakeLists.txt new file mode 100644 index 0000000..0ca488e --- /dev/null +++ b/intercept/internal/CMakeLists.txt @@ -0,0 +1,12 @@ +# internal/CMakeLists.txt + +file(GLOB INTERNAL_SOURCES *.cpp) + +add_library(intercept_internal ${INTERNAL_SOURCES}) +target_include_directories(intercept_internal PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries(intercept_internal PUBLIC common_lib) + +add_library(intercept_internal_client ${INTERNAL_SOURCES}) +target_include_directories(intercept_internal_client PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_compile_options(intercept_internal_client PUBLIC -fPIC -mavx2) +target_link_libraries(intercept_internal_client PUBLIC common_lib_client) diff --git a/intercept/internal/metainfo.h b/intercept/internal/metainfo.h new file mode 100644 index 0000000..078eca5 --- /dev/null +++ b/intercept/internal/metainfo.h @@ -0,0 +1,112 @@ +// Copyright (c) 2022 by Apex.AI Inc. All rights reserved. +// +// 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. +// +// SPDX-License-Identifier: Apache-2.0 + +#ifndef IOX_EXAMPLES_REQUEST_AND_RESPONSE_TYPES_HPP +#define IOX_EXAMPLES_REQUEST_AND_RESPONSE_TYPES_HPP + +#include +#include +#include +#include + +#include + + + +#define SERVICE_FLAG "interceptservice" +#define DUMMY_INSTANCE_FLAG "dummyserver" +#define INTERCEPT_INSTANCE_FLAG "interceptserver" + +#define ICEORYX "ICEORYX" + +namespace intercept { +namespace internal { +//! [request] +struct AddRequest +{ + uint64_t augend{0}; + uint64_t addend{0}; +}; +//! [request] + +//! [response] +struct AddResponse +{ + uint64_t sum{0}; +}; +//! [response] + +struct UserRequest +{ + uint64_t pid{0}; + uint64_t threadid{0}; +}; + +struct UserResponse +{ + uint64_t pid{0}; + uint64_t threadid{0}; +}; + + +struct Metainfo { + int type = 0; + int fd = 0; + size_t count = 0; +}; + + + +struct ServiceMetaInfo { + std::string service = ""; + std::string instance = ""; + std::string event = ""; + std::string serverType = ""; // server类型 : normal dummy +}; + +} // namespace internal +} // namespace intercept + + +#define MAX_LENGTH 2000000 +// 生成随机字符,不包括 '\0' +// char randomChar() { +// const char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; +// return charset[rand() % (sizeof(charset) - 1)]; +// } + +// // 生成随机字符串 +// char* generateRandomString(size_t length) { +// if (length > MAX_LENGTH) { +// fprintf(stderr, "String length is too long.\n"); +// } + +// char *str = (char*)malloc((length + 1) * sizeof(char)); // +1 为字符串的终止符 '\0' 预留空间 +// if (str == NULL) { +// perror("malloc"); +// } + +// for (size_t i = 0; i < length; ++i) { +// str[i] = randomChar(); +// } +// str[length] = '\0'; // 确保字符串以空字符结尾 + +// return str; +// } + + + +#endif // IOX_EXAMPLES_REQUEST_AND_RESPONSE_TYPES_HPP diff --git a/intercept/internal/posix_op_req_res.cpp b/intercept/internal/posix_op_req_res.cpp new file mode 100644 index 0000000..209903b --- /dev/null +++ b/intercept/internal/posix_op_req_res.cpp @@ -0,0 +1,1014 @@ +#include +#include +#include +#include +#include +#include + +#include "posix_op_req_res.h" + +namespace intercept { +namespace internal { +std::string TypeToStr(PosixOpType opType) { + switch(opType) { + case PosixOpType::OPEN: return "OPEN"; + case PosixOpType::WRITE: return "WRITE"; + case PosixOpType::READ: return "READ"; + case PosixOpType::ACCESS: return "ACCESS"; + case PosixOpType::CLOSE: return "CLOSE"; + case PosixOpType::FSYNC: return "FSYNC"; + case PosixOpType::TRUNCATE: return "TRUNCATE"; + case PosixOpType::FTRUNCATE: return "FTRUNCATE"; + case PosixOpType::FUTIMES: return "FUTIMES"; + case PosixOpType::LSEEK: return "LSEEK"; + case PosixOpType::MKDIR: return "MKDIR"; + case PosixOpType::MKNOD: return "MKNOD"; + case PosixOpType::OPENDIR: return "OPENDIR"; + case PosixOpType::READDIR: return "READDIR"; + case PosixOpType::GETDENTS: return "GETDENTS"; + case PosixOpType::CLOSEDIR: return "CLOSEDIR"; + case PosixOpType::RENAME: return "RENAME"; + case PosixOpType::STAT: return "STAT"; + case PosixOpType::FSTAT: return "FSTAT"; + case PosixOpType::UNLINK: return "UNLINK"; + case PosixOpType::UTIMES: return "UTIMES"; + case PosixOpType::TERMINAL: return "TERMINAL"; + // ... 其他操作类型 + default: return "UNKNOWN"; + } +} + + +// PosixOpReqRes 类的实现 +PosixOpReqRes::PosixOpReqRes(PosixOpType opType) : opType_(opType) {} + +PosixOpReqRes::PosixOpReqRes(const long* args, long* result) {} + +void PosixOpReqRes::SetOpType(PosixOpType type) { opType_ = type; } + +PosixOpType PosixOpReqRes::GetOpType() const { return opType_; } + + +// ------------------------------open--------------------------- +OpenOpReqRes::OpenOpReqRes(const char* path, int flags, mode_t mode) + : PosixOpReqRes(PosixOpType::OPEN) { + strcpy(requestData_.path, path); + requestData_.flags = flags; + requestData_.mode = mode; + requestData_.opType = opType_; +} + +OpenOpReqRes::OpenOpReqRes(const long *args, long *result) : PosixOpReqRes(PosixOpType::OPEN) { + strcpy(requestData_.path, reinterpret_cast(args[0])); + requestData_.flags = static_cast(args[1]); + requestData_.mode = static_cast(args[2]); + requestData_.opType = opType_; +} + +OpenOpReqRes::~OpenOpReqRes() { + +} + +void OpenOpReqRes::CopyRequestDataToBuf(void* buf) { + // 将请求数据复制到缓冲区 + memcpy(buf, &requestData_.opType, sizeof(requestData_.opType)); + + memcpy(buf + sizeof(requestData_.opType), requestData_.path, sizeof(requestData_.path)); + + memcpy(buf + sizeof(requestData_.opType) + sizeof(requestData_.path), &requestData_.flags, sizeof(requestData_.flags)); + + memcpy(buf + sizeof(requestData_.opType) + sizeof(requestData_.path) + sizeof(requestData_.flags), &requestData_.mode, sizeof(requestData_.mode)); + return; +} + +int OpenOpReqRes::GetRequestSize() { + return sizeof(OpenRequestData); +} + +int OpenOpReqRes::GetRequestAlignSize() { + return alignof(OpenRequestData); +} + +int OpenOpReqRes::GetResponseSize() { + return sizeof(OpenResponseData); +} + +int OpenOpReqRes::GetResponseAlignSize() { + return alignof(OpenResponseData); +} + +// 将response转化为Response +PosixOpResponse& OpenOpReqRes::GetResponse() { + return responseData_; +} + +void OpenOpReqRes::SetResponse(void* response) { + OpenResponseData + *responseData = reinterpret_cast(response); + responseData_.opType = responseData->opType; + responseData_.fd = responseData->fd; + spdlog::info("OpenOpReqRes::SetResponse: fd ={}",responseData_.fd); +} + + +// ------------------------------read---------------------------- +ReadOpReqRes::ReadOpReqRes(int fd, void* buf, size_t count) + : PosixOpReqRes(PosixOpType::READ) { + // for request + requestData_.fd = fd; + requestData_.count = count; + requestData_.opType = opType_; + // for response + responseData_.buf = buf; +} + +ReadOpReqRes::ReadOpReqRes(const long *args, long *result): PosixOpReqRes(PosixOpType::READ) { + requestData_.opType = opType_; + // for reqeust + requestData_.fd = static_cast(args[0]); + requestData_.count = static_cast(args[2]); + // for response + responseData_.buf = reinterpret_cast(args[1]); +} + +ReadOpReqRes::~ReadOpReqRes() { + // 析构函数 +} + +void ReadOpReqRes::CopyRequestDataToBuf(void* buf) { + memcpy(buf, &requestData_, sizeof(requestData_)); +} + +int ReadOpReqRes::GetRequestSize() { + return sizeof(requestData_); +} + +int ReadOpReqRes::GetRequestAlignSize() { + return alignof(ReadRequestData); +} + +int ReadOpReqRes::GetResponseSize() { + // 响应数据大小 结构体大小+需要的长度 + return sizeof(responseData_) + requestData_.count; +} + +int ReadOpReqRes::GetResponseAlignSize() { + return alignof(ReadResponseData); +} + +PosixOpResponse& ReadOpReqRes::GetResponse() { + return responseData_; +} + +void ReadOpReqRes::SetResponse(void* response) { + ReadResponseData* responseData = static_cast(response); + responseData_.opType = responseData->opType; + responseData_.ret = responseData->ret; + responseData_.length = responseData->length; + if (intercept::common::Configure::getInstance().getConfig("multiop") == "true" + && responseData_.length >= atol(intercept::common::Configure::getInstance().getConfig("blocksize").c_str())) { + SetResponseMultithreads(response); + } else { + if (responseData_.length > 0 && responseData_.buf != nullptr) { + intercept::common::Timer timer("client ReadOpReqRes::SetResponse time "); + memcpy(responseData_.buf, responseData->content, responseData->length); + //std::cout << "the read response, the length: " << responseData->length << " , the buf: " << (char*)responseData_.buf << std::endl; + } else { + spdlog::debug("the length: {}, the buf maybe nullptr", responseData_.length); + } + } + +} + +void initialize_memory(char* ptr, size_t size) { + // 通过访问每个页面确保内存已分配 + for (size_t i = 0; i < size; i += sysconf(_SC_PAGESIZE)) { + ptr[i] = 0; + } + ptr[size - 1] = 0; // 访问最后一个字节确保全部内存已分配 +} +void ReadOpReqRes::SetResponseMultithreads(void* response) { + ReadResponseData* responseData = static_cast(response); + responseData_.opType = responseData->opType; + responseData_.ret = responseData->ret; + responseData_.length = responseData->length; + + if (responseData_.length > 0 && responseData_.buf != nullptr) { + intercept::common::Timer timer("client ReadOpReqRes::SetResponseMultithreads time "); + // Determine the number of threads to use (for example, 4) + int numThreads = intercept::common::Configure::getInstance().getConfig("opThreadnum") == "" ? + 1 : atoi(intercept::common::Configure::getInstance().getConfig("opThreadnum").c_str()); + size_t chunkSize = responseData_.length / numThreads; + size_t remainder = responseData_.length % numThreads; + auto copyChunk = [](char* dest, const char* src, size_t len) { + + // initialize_memory(dest, len); + // mlock(dest, len); + // memmove(dest, src, len); + memcpy(dest, src, len); + + // size_t i = 0; + // // 处理前面未对齐的部分 + // while (i < len && reinterpret_cast(dest + i) % 32 != 0) { + // dest[i] = src[i]; + // i++; + // } + + // // 处理对齐的中间部分 + // for (; i + 31 < len; i += 32) { + // __m256i data = _mm256_loadu_si256(reinterpret_cast(src + i)); + // _mm256_storeu_si256(reinterpret_cast<__m256i*>(dest + i), data); + // } + + // // 处理末尾未对齐的部分 + // for (; i < len; ++i) { + // dest[i] = src[i]; + // } + // munlock(dest, len); + }; + + std::vector threads; + std::vector numaNode1Cores = {1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61, 63, 65, 67, 69, 71, 73, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93, 95}; + + for (int i = 0; i < numThreads; ++i) { + size_t startIdx = i * chunkSize; + size_t len = (i == numThreads - 1) ? (chunkSize + remainder) : chunkSize; + + // threads.emplace_back([&, startIdx, len, i]() { + // cpu_set_t cpuset; + // CPU_ZERO(&cpuset); + // CPU_SET(numaNode1Cores[i % numaNode1Cores.size()], &cpuset); + // sched_setaffinity(0, sizeof(cpu_set_t), &cpuset); + + // copyChunk(static_cast(responseData_.buf) + startIdx, responseData->content + startIdx, len); + // }); + threads.emplace_back(copyChunk, static_cast(responseData_.buf) + startIdx, responseData->content + startIdx, len); + } + for (auto& t : threads) { + if (t.joinable()) { + t.join(); + } + } + spdlog::debug("the read response, the length: {}" ,responseData_.length); + } else { + spdlog::debug("the length: {}, the buf maybe nullptr", responseData_.length); + } +} + +// void ReadOpReqRes::SetResponse(void* response) { +// ReadResponseData* responseData = static_cast(response); +// responseData_.opType = responseData->opType; +// responseData_.ret = responseData->ret; +// responseData_.length = responseData->length; + +// if (responseData_.length > 0 && responseData_.buf != nullptr) { + +// int numThreads = intercept::common::Configure::getInstance().getConfig("opThreadnum").empty() ? +// 1 : std::stoi(intercept::common::Configure::getInstance().getConfig("opThreadnum")); +// size_t chunkSize = responseData_.length / numThreads; +// size_t remainder = responseData_.length % numThreads; + +// if (intercept::common::Configure::getInstance().getConfig("multiop") == "true") { +// intercept::common::Timer timer("client ReadOpReqRes::SetResponse time "); +// auto copyChunk = [](char* dest, const char* src, size_t len) { +// memcpy(dest, src, len); +// }; +// std::atomic tasksRemaining(numThreads); +// auto tasksMutex = std::make_shared(); +// auto tasksCondition = std::make_shared(); + +// for (int i = 0; i < numThreads; ++i) { +// size_t startIdx = i * chunkSize; +// size_t len = (i == numThreads - 1) ? (chunkSize + remainder) : chunkSize; +// threadPool_.enqueue([=, &tasksRemaining, tasksMutex, tasksCondition]() { +// copyChunk(static_cast(responseData_.buf) + startIdx, responseData->content + startIdx, len); +// if (--tasksRemaining == 0) { +// std::unique_lock lock(*tasksMutex); +// tasksCondition->notify_all(); +// } +// }); +// } + +// { +// std::unique_lock lock(*tasksMutex); +// tasksCondition->wait(lock, [&tasksRemaining] { return tasksRemaining.load() == 0; }); +// } + +// } else { +// memcpy(responseData_.buf, responseData->content, responseData->length); +// } + +// spdlog::debug("The read response, length: {}", responseData_.length); +// } else { +// spdlog::debug("The length: {}, the buffer may be nullptr", responseData_.length); +// } +// } + + +// -----------------------------write------------------------- +WriteOpReqRes::WriteOpReqRes(int fd, void* buf, size_t count) + : PosixOpReqRes(PosixOpType::WRITE) { + requestData_.opType = opType_; + requestData_.fd = fd; + requestData_.buf = buf; + requestData_.count = count; +} + +WriteOpReqRes::WriteOpReqRes(const long *args, long *result) + : PosixOpReqRes(PosixOpType::WRITE) { + // 从参数中初始化 + requestData_.opType = opType_; + requestData_.fd = static_cast(args[0]); + requestData_.buf = reinterpret_cast(args[1]); + requestData_.count = static_cast(args[2]); +} + +WriteOpReqRes::~WriteOpReqRes() { + // 析构函数 +} + +void WriteOpReqRes::CopyRequestDataToBuf(void* buf) { + // 元信息 + memcpy(buf, &requestData_, sizeof(requestData_)); + // 数据 + if (intercept::common::Configure::getInstance().getConfig("multiop") == "true" && + requestData_.count >= atoi(intercept::common::Configure::getInstance().getConfig("blocksize").c_str()) ) { + int numThreads = intercept::common::Configure::getInstance().getConfig("opThreadnum") == "" ? + 1 : atoi(intercept::common::Configure::getInstance().getConfig("opThreadnum").c_str()); + CopyRequestDataToBufMultithread((char*)buf + sizeof(requestData_), requestData_.buf, requestData_.count, numThreads); + } else { + memcpy((char*)buf + sizeof(requestData_), requestData_.buf, requestData_.count); + } +} + +void WriteOpReqRes::CopyRequestDataToBufMultithread(void* dest, const void* src, size_t count, int numThreads) { + size_t chunkSize = count / numThreads; + size_t remainder = count % numThreads; + intercept::common::Timer timer("client WriteOpReqRes::CopyRequestDataToBufMultithread time:"); + auto copyChunk = [](char* dest, const char* src, size_t len) { + memcpy(dest, src, len); + }; + spdlog::info("copy request with multithread for writing, chunksize: {}, remainder: {}", chunkSize, remainder); + std::vector threads; + for (int i = 0; i < numThreads; ++i) { + size_t startIdx = i * chunkSize; + size_t len = (i == numThreads - 1) ? (chunkSize + remainder) : chunkSize; + threads.emplace_back(copyChunk, static_cast(dest + startIdx), static_cast(src + startIdx), len); + } + + for (auto& t : threads) { + if (t.joinable()) { + t.join(); + } + } +} + +int WriteOpReqRes::GetRequestSize() { + return sizeof(requestData_) + requestData_.count; +} + +int WriteOpReqRes::GetRequestAlignSize() { + return alignof(WriteRequestData); +} + +int WriteOpReqRes::GetResponseSize() { + return sizeof(WriteResponseData); +} + +int WriteOpReqRes::GetResponseAlignSize() { + return alignof(WriteResponseData); +} + +PosixOpResponse& WriteOpReqRes::GetResponse() { + return responseData_; +} + +void WriteOpReqRes::SetResponse(void* response) { + WriteResponseData* responseData = static_cast(response); + responseData_.opType = responseData->opType; + responseData_.ret = responseData->ret; + responseData_.length = responseData->length; + // std::cout << "write response, optype: " << (int)responseData_.opType << " , ret: " << responseData_.ret << " , length: " << responseData_.length << std::endl; +} + +// -----------------------close------------------------------- +CloseOpReqRes::CloseOpReqRes(int fd) : PosixOpReqRes(PosixOpType::CLOSE) { + requestData_.opType = PosixOpType::CLOSE; + requestData_.fd = fd; +} + +CloseOpReqRes::CloseOpReqRes(const long* args, long* result) : PosixOpReqRes(PosixOpType::CLOSE) { + requestData_.opType = PosixOpType::CLOSE; + requestData_.fd = static_cast(args[0]); +} + +CloseOpReqRes::~CloseOpReqRes() { + // 析构函数 +} + +void CloseOpReqRes::CopyRequestDataToBuf(void* buf) { + memcpy(buf, &requestData_, sizeof(requestData_)); +} + +int CloseOpReqRes::GetRequestSize() { + return sizeof(requestData_); +} + +int CloseOpReqRes::GetRequestAlignSize() { + return alignof(CloseRequestData); +} + +int CloseOpReqRes::GetResponseSize() { + return sizeof(CloseResponseData); +} + +int CloseOpReqRes::GetResponseAlignSize() { + return alignof(CloseResponseData); +} + +PosixOpResponse& CloseOpReqRes::GetResponse() { + return responseData_; +} + +void CloseOpReqRes::SetResponse(void* response) { + CloseResponseData* responseData = static_cast(response); + responseData_.opType = responseData->opType; + responseData_.ret = responseData->ret; +} + +// ------------------------fysnc------------------------------------- +FsyncOpReqRes::FsyncOpReqRes(int fd) { + requestData_.opType = PosixOpType::FSYNC; + requestData_.fd = fd; +} + +FsyncOpReqRes::FsyncOpReqRes(const long* args, long* result) { + requestData_.opType = PosixOpType::FSYNC; + requestData_.fd = static_cast(args[0]); +} + +FsyncOpReqRes::~FsyncOpReqRes() { + +} +void FsyncOpReqRes::CopyRequestDataToBuf(void* buf) { + memcpy(buf, &requestData_, sizeof(requestData_)); +} + +int FsyncOpReqRes::GetRequestSize() { + return sizeof(requestData_); +} + +int FsyncOpReqRes::GetRequestAlignSize() { + return alignof(FsyncRequestData); +} + +int FsyncOpReqRes::GetResponseSize() { + return sizeof(FsyncResponseData); +} + +int FsyncOpReqRes::GetResponseAlignSize() { + return alignof(FsyncResponseData); +} + +PosixOpResponse& FsyncOpReqRes::GetResponse() { + return responseData_; +} + +void FsyncOpReqRes::SetResponse(void* response) { + FsyncResponseData* responseData = static_cast(response); + responseData_.opType = responseData->opType; + responseData_.ret = responseData->ret; +} + +// -------------------stat--------------------------- +StatOpReqRes::StatOpReqRes(const char* path, struct stat* st) + : PosixOpReqRes(PosixOpType::STAT){ + requestData_.opType = opType_; + strncpy(requestData_.path, path, strlen(path)); + requestData_.path[strlen(path)] = '\0'; + responseData_.st = st; + spdlog::debug("StatOpReqRes, the type: {}, the path: {}", TypeToStr(requestData_.opType), requestData_.path); +} + +StatOpReqRes::StatOpReqRes(const long* args, long* result) + : PosixOpReqRes(PosixOpType::STAT){ + requestData_.opType = opType_; + strncpy(requestData_.path, (const char*)args[1], strlen((const char*)args[1])); + requestData_.path[strlen((const char*)args[1])] = '\0'; + responseData_.st = (struct stat*)(args[1]); + spdlog::debug("StatOpReqRes, the type: {}, the path: {}", TypeToStr(requestData_.opType), requestData_.path ); +} +StatOpReqRes::~StatOpReqRes() { +} + +void StatOpReqRes::CopyRequestDataToBuf(void* buf) { + memcpy(buf, &requestData_, sizeof(requestData_)); +} + +int StatOpReqRes::GetRequestSize() { + return sizeof(requestData_); +} + +int StatOpReqRes::GetRequestAlignSize() { + return alignof(StatRequestData); +} + +int StatOpReqRes::GetResponseSize() { + return sizeof(StatResponseData); +} + +int StatOpReqRes::GetResponseAlignSize() { + return alignof(StatResponseData); +} + +PosixOpResponse& StatOpReqRes::GetResponse() { + return responseData_; +} + +void StatOpReqRes::SetResponse(void* response) { + StatResponseData* responseData = static_cast(response); + responseData_.opType = responseData->opType; + responseData_.ret = responseData->ret; + memcpy(responseData_.st, &responseData->fileStat, sizeof(struct stat)); +} + +// --------------------------------fstat-------------------------------------- + +FstatOpReqRes::FstatOpReqRes(int fd, struct stat* st) + : PosixOpReqRes(PosixOpType::FSTAT){ + requestData_.opType = opType_; + requestData_.fd = fd; + responseData_.st = st; + spdlog::debug("FstatOpReqRes, the type: {}, the fd: {}", TypeToStr(requestData_.opType), requestData_.fd ); + +} + +FstatOpReqRes::FstatOpReqRes(const long* args, long* result) + : PosixOpReqRes(PosixOpType::FSTAT){ + requestData_.opType = opType_; + requestData_.fd = (int)args[0]; + responseData_.st = (struct stat*)(args[1]); + spdlog::debug("FstatOpReqRes, the type: {}, the fd: {}", TypeToStr(requestData_.opType), requestData_.fd); + +} +FstatOpReqRes::~FstatOpReqRes() { +} + +void FstatOpReqRes::CopyRequestDataToBuf(void* buf) { + memcpy(buf, &requestData_, sizeof(requestData_)); +} + +int FstatOpReqRes::GetRequestSize() { + return sizeof(requestData_); +} + +int FstatOpReqRes::GetRequestAlignSize() { + return alignof(FstatRequestData); +} + +int FstatOpReqRes::GetResponseSize() { + return sizeof(FstatResponseData); +} + +int FstatOpReqRes::GetResponseAlignSize() { + return alignof(FstatResponseData); +} + +PosixOpResponse& FstatOpReqRes::GetResponse() { + return responseData_; +} + +void FstatOpReqRes::SetResponse(void* response) { + FstatResponseData* responseData = static_cast(response); + responseData_.opType = responseData->opType; + responseData_.ret = responseData->ret; + memcpy(responseData_.st, &responseData->fileStat, sizeof(struct stat)); +} + +// --------------------------------lseek--------------------------------------- +LseekOpReqRes::LseekOpReqRes(int fd, uint64_t offset, int whence) + : PosixOpReqRes(PosixOpType::LSEEK){ + requestData_.opType = opType_; + requestData_.fd = fd; + requestData_.offset = offset; + requestData_.whence = whence; +} + +LseekOpReqRes::LseekOpReqRes(const long* args, long* result) + : PosixOpReqRes(PosixOpType::LSEEK){ + requestData_.opType = opType_; + requestData_.fd = (int)args[0]; + requestData_.offset = (off_t)args[1]; + requestData_.whence = (int)args[2]; +} + +LseekOpReqRes::~LseekOpReqRes() { +} + +void LseekOpReqRes::CopyRequestDataToBuf(void* buf) { + memcpy(buf, &requestData_, sizeof(requestData_)); +} + +int LseekOpReqRes::GetRequestSize() { + return sizeof(requestData_); +} + +int LseekOpReqRes::GetRequestAlignSize() { + return alignof(LseekRequestData); +} + +int LseekOpReqRes::GetResponseSize() { + return sizeof(LseekResponseData); +} + +int LseekOpReqRes::GetResponseAlignSize() { + return alignof(LseekResponseData); +} + +PosixOpResponse& LseekOpReqRes::GetResponse() { + return responseData_; +} + +void LseekOpReqRes::SetResponse(void* response) { + LseekResponseData* responseData = static_cast(response); + responseData_.opType = responseData->opType; + responseData_.ret = responseData->ret; +} + +// -------------------------------mkdir---------------------------------------- +MkdirOpReqRes::MkdirOpReqRes(const char* path, mode_t mode) + : PosixOpReqRes(PosixOpType::MKDIR){ + requestData_.opType = opType_; + strncpy(requestData_.path, path, strlen(path)); + requestData_.path[strlen(path)] = '\0'; + requestData_.mode = mode; +} + +MkdirOpReqRes::MkdirOpReqRes(const long* args, long* result) + : PosixOpReqRes(PosixOpType::MKDIR){ + requestData_.opType = opType_; + strncpy(requestData_.path, (const char*)args[0], strlen((const char*)args[0])); + requestData_.path[strlen((const char*)args[0])] = '\0'; + requestData_.mode = (mode_t)args[1]; +} + +MkdirOpReqRes::~MkdirOpReqRes() { +} + +void MkdirOpReqRes::CopyRequestDataToBuf(void* buf) { + memcpy(buf, &requestData_, sizeof(requestData_)); +} + +int MkdirOpReqRes::GetRequestSize() { + return sizeof(requestData_); +} + +int MkdirOpReqRes::GetRequestAlignSize() { + return alignof(MkdirRequestData); +} + +int MkdirOpReqRes::GetResponseSize() { + return sizeof(MkdirResponseData); +} + +int MkdirOpReqRes::GetResponseAlignSize() { + return alignof(MkdirResponseData); +} + +PosixOpResponse& MkdirOpReqRes::GetResponse() { + return responseData_; +} +void MkdirOpReqRes::SetResponse(void* response) { + MkdirResponseData* responseData = static_cast(response); + responseData_.opType = responseData->opType; + responseData_.ret = responseData->ret; +} + +// -------------------------------opendir--------------------------------------- +OpendirOpReqRes::OpendirOpReqRes(const char* path) + : PosixOpReqRes(PosixOpType::OPENDIR){ + requestData_.opType = opType_; + strncpy(requestData_.path, path, strlen(path)); + requestData_.path[strlen(path)] = '\0'; +} + +OpendirOpReqRes::OpendirOpReqRes(const long* args, long* result) { + requestData_.opType = opType_; + strncpy(requestData_.path, (const char*)args[0], strlen((const char*)args[0])); + requestData_.path[strlen((const char*)args[0])] = '\0'; +} + +OpendirOpReqRes::~OpendirOpReqRes() { +} + +void OpendirOpReqRes::CopyRequestDataToBuf(void* buf) { + memcpy(buf, &requestData_, sizeof(requestData_)); +} + +int OpendirOpReqRes::GetRequestSize() { + return sizeof(requestData_); +} + +int OpendirOpReqRes::GetRequestAlignSize() { + return alignof(OpendirRequestData); +} + +int OpendirOpReqRes::GetResponseSize() { + return sizeof(OpendirResponseData); +} + +int OpendirOpReqRes::GetResponseAlignSize() { + return alignof(OpendirResponseData); +} + +PosixOpResponse& OpendirOpReqRes::GetResponse() { + return responseData_; +} + +void OpendirOpReqRes::SetResponse(void* response) { + OpendirResponseData* responseData = static_cast(response); + responseData_.opType = responseData->opType; + responseData_.ret = responseData->ret; + responseData_.dirStream = responseData->dirStream; +} + +//------------------------------getdents--------------------------- +GetdentsOpReqRes::GetdentsOpReqRes(DirStream dirinfo, char* data, size_t maxread) + : PosixOpReqRes(PosixOpType::GETDENTS){ + requestData_.opType = opType_; + requestData_.dirinfo = dirinfo; + requestData_.maxread = maxread; + + responseData_.data = data; +} + +GetdentsOpReqRes::GetdentsOpReqRes(const long* args, long* result) { + requestData_.opType = opType_; + // TODO +} + +GetdentsOpReqRes::~GetdentsOpReqRes() { + +} + +void GetdentsOpReqRes::CopyRequestDataToBuf(void* buf) { + memcpy(buf, &requestData_, sizeof(requestData_)); +} + +int GetdentsOpReqRes::GetRequestSize() { + return sizeof(requestData_); +} + +int GetdentsOpReqRes::GetRequestAlignSize() { + return alignof(GetdentsRequestData); +} + +int GetdentsOpReqRes::GetResponseSize() { + return sizeof(GetdentsResponseData); +} + +int GetdentsOpReqRes::GetResponseAlignSize() { + return alignof(GetdentsResponseData); +} + +PosixOpResponse& GetdentsOpReqRes::GetResponse() { + return responseData_; +} + +void GetdentsOpReqRes::SetResponse(void* response) { + GetdentsResponseData* responseData = static_cast(response); + responseData_.opType = responseData->opType; + responseData_.dirinfo = responseData->dirinfo; + responseData_.realbytes = responseData->realbytes;; + responseData_.ret = responseData->ret; + memcpy(responseData_.data, responseData->contents, responseData->realbytes); +} + +// ------------------------------closedir------------------------------- +ClosedirOpReqRes::ClosedirOpReqRes(const DirStream& dirstream) + : PosixOpReqRes(PosixOpType::CLOSEDIR){ + requestData_.opType = opType_; + requestData_.dirstream = dirstream; +} + +ClosedirOpReqRes::ClosedirOpReqRes(const long* args, long* result) { + requestData_.opType = opType_; + // TODO +} + +ClosedirOpReqRes::~ClosedirOpReqRes() { +} + +void ClosedirOpReqRes::CopyRequestDataToBuf(void* buf) { + memcpy(buf, &requestData_, sizeof(requestData_)); +} + +int ClosedirOpReqRes::GetRequestSize() { + return sizeof(requestData_); +} + +int ClosedirOpReqRes::GetRequestAlignSize() { + return alignof(ClosedirRequestData); +} + +int ClosedirOpReqRes::GetResponseSize() { + return sizeof(ClosedirResponseData); +} + +int ClosedirOpReqRes::GetResponseAlignSize() { + return alignof(ClosedirResponseData); +} + +PosixOpResponse& ClosedirOpReqRes::GetResponse() { + return responseData_; +} + +void ClosedirOpReqRes::SetResponse(void* response) { + ClosedirResponseData* responseData = static_cast(response); + responseData_.opType = responseData->opType; + responseData_.ret = responseData->ret; +} + +// -------------------------------unlink------------------------------- +UnlinkOpReqRes::UnlinkOpReqRes(const char* path) + : PosixOpReqRes(PosixOpType::UNLINK){ + requestData_.opType = opType_; + strncpy(requestData_.path, path, strlen(path)); + requestData_.path[strlen(path)] = '\0'; +} + +UnlinkOpReqRes::UnlinkOpReqRes(const long* args, long* result) { + requestData_.opType = opType_; + strncpy(requestData_.path, (const char*)args[0], strlen((const char*)args[0])); + requestData_.path[strlen((const char*)args[0])] = '\0'; +} + +UnlinkOpReqRes::~UnlinkOpReqRes() { +} + +void UnlinkOpReqRes::CopyRequestDataToBuf(void* buf) { + memcpy(buf, &requestData_, sizeof(requestData_)); +} + +int UnlinkOpReqRes::GetRequestSize() { + return sizeof(requestData_); +} + +int UnlinkOpReqRes::GetRequestAlignSize() { + return alignof(UnlinkRequestData); +} + +int UnlinkOpReqRes::GetResponseSize() { + return sizeof(UnlinkResponseData); +} + +int UnlinkOpReqRes::GetResponseAlignSize() { + return alignof(UnlinkResponseData); +} + +PosixOpResponse& UnlinkOpReqRes::GetResponse() { + return responseData_; +} +void UnlinkOpReqRes::SetResponse(void* response) { + UnlinkResponseData* responseData = static_cast(response); + responseData_.opType = responseData->opType; + responseData_.ret = responseData->ret; +} + +// ------------------------------rename------------------------------------- +RenameOpReqRes::RenameOpReqRes(const char* oldpath, const char* newpath) + : PosixOpReqRes(PosixOpType::RENAME){ + requestData_.opType = opType_; + strncpy(requestData_.oldpath, oldpath, strlen(oldpath)); + requestData_.oldpath[strlen(oldpath)] = '\0'; + strncpy(requestData_.newpath, newpath, strlen(newpath)); + requestData_.newpath[strlen(newpath)] = '\0'; +} + +RenameOpReqRes::RenameOpReqRes(const long* args, long* result) { + requestData_.opType = opType_; + strncpy(requestData_.oldpath, (const char*)args[0], strlen((const char*)args[0])); + requestData_.oldpath[strlen((const char*)args[0])] = '\0'; + strncpy(requestData_.newpath, (const char*)args[1], strlen((const char*)args[1])); + requestData_.newpath[strlen((const char*)args[1])] = '\0'; +} + +RenameOpReqRes::~RenameOpReqRes() { +} + +void RenameOpReqRes::CopyRequestDataToBuf(void* buf) { + memcpy(buf, &requestData_, sizeof(requestData_)); +} + +int RenameOpReqRes::GetRequestSize() { + return sizeof(requestData_); +} + +int RenameOpReqRes::GetRequestAlignSize() { + return alignof(RenameRequestData); +} + +int RenameOpReqRes::GetResponseSize() { + return sizeof(RenameResponseData); +} + +int RenameOpReqRes::GetResponseAlignSize() { + return alignof(RenameResponseData); +} + +PosixOpResponse& RenameOpReqRes::GetResponse() { + return responseData_; +} +void RenameOpReqRes::SetResponse(void* response) { + RenameResponseData* responseData = static_cast(response); + responseData_.ret = responseData->ret; + responseData_.opType = responseData->opType; +} + +// -------------------------truncate--------------------------------- +TruncateOpReqRes::TruncateOpReqRes(const char* path, off_t length) + : PosixOpReqRes(PosixOpType::TRUNCATE){ + requestData_.opType = opType_; + strncpy(requestData_.path, path, strlen(path)); + requestData_.path[strlen(path)] = '\0'; + requestData_.length = length; +} + +TruncateOpReqRes::TruncateOpReqRes(const long* args, long* result) { + requestData_.opType = opType_; + strncpy(requestData_.path, (const char*)args[0], strlen((const char*)args[0])); + requestData_.path[strlen((const char*)args[0])] = '\0'; + requestData_.length = (off_t)args[1]; +} + +TruncateOpReqRes::~TruncateOpReqRes() { +} + +void TruncateOpReqRes::CopyRequestDataToBuf(void* buf) { + memcpy(buf, &requestData_, sizeof(requestData_)); +} + +int TruncateOpReqRes::GetRequestSize() { + return sizeof(requestData_); +} + +int TruncateOpReqRes::GetRequestAlignSize() { + return alignof(TruncateRequestData); +} + +int TruncateOpReqRes::GetResponseSize() { + return sizeof(TruncateResponseData); +} + +int TruncateOpReqRes::GetResponseAlignSize() { + return alignof(TruncateResponseData); +} + +PosixOpResponse& TruncateOpReqRes::GetResponse() { + return responseData_; +} + +void TruncateOpReqRes::SetResponse(void* response) { + TruncateResponseData* responseData = static_cast(response); + responseData_.opType = responseData->opType; + responseData_.ret = responseData->ret; +} + +// -------------------------terminal--------------------------------- +TerminalOpReqRes::TerminalOpReqRes() + : PosixOpReqRes(PosixOpType::TERMINAL){ + requestData_.opType = opType_; +} + +void TerminalOpReqRes::CopyRequestDataToBuf(void* buf) { + memcpy(buf, &requestData_, sizeof(requestData_)); +} + +int TerminalOpReqRes::GetRequestSize() { + return sizeof(requestData_); +} + +int TerminalOpReqRes::GetRequestAlignSize() { + return alignof(TerminalRequestData); +} + +int TerminalOpReqRes::GetResponseSize() { + return sizeof(TerminalResponseData); +} + +int TerminalOpReqRes::GetResponseAlignSize() { + return alignof(TerminalResponseData); +} + +PosixOpResponse& TerminalOpReqRes::GetResponse() { + return responseData_; +} + +void TerminalOpReqRes::SetResponse(void* response) { + TerminalResponseData* responseData = static_cast(response); + responseData_.opType = responseData->opType; + responseData_.ret = responseData->ret; +} + +} // namespace internal +} // namespace intercept diff --git a/intercept/internal/posix_op_req_res.h b/intercept/internal/posix_op_req_res.h new file mode 100644 index 0000000..6be289f --- /dev/null +++ b/intercept/internal/posix_op_req_res.h @@ -0,0 +1,650 @@ +#pragma once +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "common/common.h" + +namespace intercept { +namespace internal { +using intercept::common::DirStream; +// 操作类型枚举 +enum class FileType { + FILE = 0, + DIR = 1, +}; + +enum class PosixOpType { + OPEN = 0, + WRITE, + READ, + ACCESS, + CLOSE, + FSYNC, + TRUNCATE, + FTRUNCATE, + FUTIMES, + LSEEK, + MKDIR, + MKNOD, + OPENDIR, + READDIR, + GETDENTS, + CLOSEDIR, + RENAME, + STAT, + FSTAT, + UNLINK, + UTIMES, + TERMINAL, // 程序退出时的操作 + // ... 其他操作类型 +}; + +std::string TypeToStr(PosixOpType opType); + +// 请求数据结构体 +struct PosixOpRequest { + PosixOpType opType; + //virtual ~PosixOpRequest() = default; // 添加虚析构函数使类变为多态 +}; + +// 响应数据结构体 +struct PosixOpResponse{ + PosixOpType opType; + //virtual ~PosixOpResponse() = default; // 添加虚析构函数使类变为多态 +}; + +// 请求/响应类 +class PosixOpReqRes { +public: + PosixOpReqRes() = default; + + PosixOpReqRes(PosixOpType opType); + + PosixOpReqRes(const long* args, long* result); + + virtual ~PosixOpReqRes() = default; // 添加虚析构函数使类变为多态 + + void SetOpType(PosixOpType type); + + PosixOpType GetOpType() const; + + // virtual void Init() = 0; + + // virtual void Shutdown() = 0; + + // 设置和获取请求数据 + // virtual const PosixOpRequest& GetRequestData() const = 0; + // virtual void SetRequestData(const PosixOpRequest& requestData) = 0; + // virtual void SetRequestData(const long* args, long* result) = 0; + + // 复制请求数据到缓冲区 + virtual void CopyRequestDataToBuf(void* buf) = 0; + + // 获取请求大小 + virtual int GetRequestSize() = 0; + virtual int GetRequestAlignSize() = 0; + virtual int GetResponseSize() = 0; + virtual int GetResponseAlignSize() = 0; + + // 设置和获取响应数据 + virtual PosixOpResponse& GetResponse() = 0; + + virtual void SetResponse(void* response) = 0; + +protected: + PosixOpType opType_; +}; + +// ---------------------------------open------------------------------------------------ +struct OpenRequestData : PosixOpRequest { + char path[200]; + int flags; + mode_t mode; +}; + +struct OpenResponseData : PosixOpResponse { + int fd; +}; +class OpenOpReqRes : public PosixOpReqRes { +public: + OpenOpReqRes(const char* path, int flags, mode_t mode); + + OpenOpReqRes(const long *args, long *result); + + ~OpenOpReqRes() override; + + // 复制请求数据到缓冲区 + virtual void CopyRequestDataToBuf(void* buf); + + // 获取请求大小 + int GetRequestSize() override; + int GetRequestAlignSize() override; + + int GetResponseSize() override; + int GetResponseAlignSize() override; + + // 获取和设置响应数据 + PosixOpResponse& GetResponse() override; + void SetResponse(void* request) override; + +private: + OpenRequestData requestData_; + OpenResponseData responseData_; +}; + + +// --------------------------------------read---------------------------------------- +struct ReadRequestData : PosixOpRequest { + int fd; + size_t count; + // void* buf; +}; + +struct ReadResponseData : PosixOpResponse { + int ret; // 返回值 + ssize_t length; // 返回长度 + void* buf; // 为上游保存数据的指针 + char content[0]; // server返回数据 +}; + +class ReadOpReqRes : public PosixOpReqRes { +public: + ReadOpReqRes(int fd, void* buf, size_t count); + ReadOpReqRes(const long *args, long *result); + virtual ~ReadOpReqRes() override; + + virtual void CopyRequestDataToBuf(void* buf); + + virtual int GetRequestSize() override; + virtual int GetRequestAlignSize() override; + virtual int GetResponseSize() override; + virtual int GetResponseAlignSize() override; + + virtual PosixOpResponse& GetResponse() override; + void SetResponse(void* response) override; + void SetResponseMultithreads(void* response); + +private: + ReadRequestData requestData_; + ReadResponseData responseData_; + // intercept::common::ThreadPool threadPool_; +}; + +// ---------------------------------write------------------------------------------- +struct WriteRequestData : PosixOpRequest { + int fd; + size_t count; // 要求长度 + void* buf; + char content[0]; // 传输时保存数据 +}; + +struct WriteResponseData : PosixOpResponse { + int ret; // 返回值 + ssize_t length; // 返回长度 +}; + +class WriteOpReqRes : public PosixOpReqRes { +public: + WriteOpReqRes() + : PosixOpReqRes(PosixOpType::WRITE) {} + WriteOpReqRes(int fd, void* buf, size_t count); + WriteOpReqRes(const long *args, long *result); + ~WriteOpReqRes() override; + + virtual void CopyRequestDataToBuf(void* buf); + void CopyRequestDataToBufMultithread(void* dest, const void* src, size_t count, int numThreads); + + virtual int GetRequestSize() override; + virtual int GetRequestAlignSize() override; + virtual int GetResponseSize() override; + virtual int GetResponseAlignSize() override; + + virtual PosixOpResponse& GetResponse() override; + void SetResponse(void* response) override; + + +private: + WriteRequestData requestData_; + WriteResponseData responseData_; +}; + +//-------------------------------------close--------------------------------------- +struct CloseRequestData : PosixOpRequest { + int fd; +}; + +struct CloseResponseData : PosixOpResponse { + int ret; // 返回值 +}; + +class CloseOpReqRes : public PosixOpReqRes { +public: + CloseOpReqRes() + : PosixOpReqRes(PosixOpType::CLOSE) {} + CloseOpReqRes(int fd); + CloseOpReqRes(const long *args, long *result); + ~CloseOpReqRes() override; + + virtual void CopyRequestDataToBuf(void* buf); + + virtual int GetRequestSize() override; + virtual int GetRequestAlignSize() override; + virtual int GetResponseSize() override; + virtual int GetResponseAlignSize() override; + + virtual PosixOpResponse& GetResponse() override; + void SetResponse(void* response) override; + +private: + CloseRequestData requestData_; + CloseResponseData responseData_; +}; + +// ----------------------------------------fsync------------------------------- +struct FsyncRequestData : PosixOpRequest { + int fd; +}; + +struct FsyncResponseData : PosixOpResponse { + int ret; // 返回值 +}; + +class FsyncOpReqRes : public PosixOpReqRes { +public: + FsyncOpReqRes() + : PosixOpReqRes(PosixOpType::CLOSE) {} + FsyncOpReqRes(int fd); + FsyncOpReqRes(const long *args, long *result); + ~FsyncOpReqRes() override; + + virtual void CopyRequestDataToBuf(void* buf); + + virtual int GetRequestSize() override; + virtual int GetRequestAlignSize() override; + virtual int GetResponseSize() override; + virtual int GetResponseAlignSize() override; + + virtual PosixOpResponse& GetResponse() override; + void SetResponse(void* response) override; + +private: + FsyncRequestData requestData_; + FsyncResponseData responseData_; +}; +// -----------------------------------stat---------------------------------------- +struct StatRequestData : PosixOpRequest { + char path[200]; +}; + +struct StatResponseData : PosixOpResponse { + int ret; // 返回值 + void* st; // 为上游保存数据的指针 + struct stat fileStat; // server返回数据 +}; + +class StatOpReqRes : public PosixOpReqRes { +public: + StatOpReqRes() + : PosixOpReqRes(PosixOpType::STAT) {} + StatOpReqRes(const char *path, struct stat *st); + StatOpReqRes(const long *args, long *result); + ~StatOpReqRes() override; + + virtual void CopyRequestDataToBuf(void* buf); + + virtual int GetRequestSize() override; + virtual int GetRequestAlignSize() override; + virtual int GetResponseSize() override; + virtual int GetResponseAlignSize() override; + + virtual PosixOpResponse& GetResponse() override; + void SetResponse(void* response) override; +private: + StatRequestData requestData_; + StatResponseData responseData_; +}; + +// ----------------------------------fstat------------------------------------------ +struct FstatRequestData : PosixOpRequest { + int fd; +}; + +struct FstatResponseData : PosixOpResponse { + int ret; // 返回值 + void* st; // 为上游保存数据的指针 + struct stat fileStat; // server返回数据 +}; + +class FstatOpReqRes : public PosixOpReqRes { +public: + FstatOpReqRes() + : PosixOpReqRes(PosixOpType::FSTAT) {} + FstatOpReqRes(int fd, struct stat *st); + FstatOpReqRes(const long *args, long *result); + ~FstatOpReqRes() override; + + virtual void CopyRequestDataToBuf(void* buf); + + virtual int GetRequestSize() override; + virtual int GetRequestAlignSize() override; + virtual int GetResponseSize() override; + virtual int GetResponseAlignSize() override; + + virtual PosixOpResponse& GetResponse() override; + void SetResponse(void* response) override; +private: + FstatRequestData requestData_; + FstatResponseData responseData_; +}; + +// -----------------------------------lseek------------------------------------------ +struct LseekRequestData : PosixOpRequest { + int fd; + uint64_t offset; + int whence; +}; + +struct LseekResponseData : PosixOpResponse { + off_t ret; // 返回值 +}; + +class LseekOpReqRes : public PosixOpReqRes { +public: + LseekOpReqRes() + : PosixOpReqRes(PosixOpType::LSEEK) {} + + LseekOpReqRes(int fd, uint64_t offset, int whence); + LseekOpReqRes(const long *args, long *result); + ~LseekOpReqRes() override; + + virtual void CopyRequestDataToBuf(void* buf); + + virtual int GetRequestSize() override; + virtual int GetRequestAlignSize() override; + virtual int GetResponseSize() override; + virtual int GetResponseAlignSize() override; + + virtual PosixOpResponse& GetResponse() override; + void SetResponse(void* response) override; +private: + LseekRequestData requestData_; + LseekResponseData responseData_; +}; + +// ----------------------------------mkdir----------------------------------------------- +struct MkdirRequestData : PosixOpRequest { + char path[200]; + mode_t mode; +}; + +struct MkdirResponseData : PosixOpResponse { + int ret; // 返回值 +}; + +class MkdirOpReqRes : public PosixOpReqRes { +public: + MkdirOpReqRes() + : PosixOpReqRes(PosixOpType::MKDIR) {} + MkdirOpReqRes(const char *path, mode_t mode); + MkdirOpReqRes(const long *args, long *result); + + ~MkdirOpReqRes() override; + + virtual void CopyRequestDataToBuf(void* buf); + + virtual int GetRequestSize() override; + virtual int GetRequestAlignSize() override; + virtual int GetResponseSize() override; + virtual int GetResponseAlignSize() override; + + virtual PosixOpResponse& GetResponse() override; + void SetResponse(void* response) override; +private: + MkdirRequestData requestData_; + MkdirResponseData responseData_; +}; + +// ----------------------------------opendir------------------------------------ + + +struct OpendirRequestData : PosixOpRequest { + char path[200]; +}; + +struct OpendirResponseData : PosixOpResponse { + int ret; // 返回值 + DIR* dir; // 上游保存dir的指针 + DirStream dirStream; // 保存server获取的结果 +}; + +class OpendirOpReqRes : public PosixOpReqRes { +public: + OpendirOpReqRes() + : PosixOpReqRes(PosixOpType::OPENDIR) {} + OpendirOpReqRes(const char *path); + OpendirOpReqRes(const long *args, long *result); + + ~OpendirOpReqRes() override; + + virtual void CopyRequestDataToBuf(void* buf); + + virtual int GetRequestSize() override; + virtual int GetRequestAlignSize() override; + virtual int GetResponseSize() override; + virtual int GetResponseAlignSize() override; + + virtual PosixOpResponse& GetResponse() override; + void SetResponse(void* response) override; +private: + OpendirRequestData requestData_; + OpendirResponseData responseData_; +}; + +// ----------------------------------getdents------------------------ + +struct GetdentsRequestData : PosixOpRequest { + DirStream dirinfo; + size_t maxread; +}; + +struct GetdentsResponseData : PosixOpResponse { + int ret; // 返回值 + DirStream dirinfo; + ssize_t realbytes; + char* data; // 上游数据指针 + char contents[0]; // 保存server获取的结果 +}; + +class GetdentsOpReqRes : public PosixOpReqRes { +public: + GetdentsOpReqRes() + : PosixOpReqRes(PosixOpType::GETDENTS) {} + GetdentsOpReqRes(DirStream dirinfo, char* data, size_t maxread); + GetdentsOpReqRes(const long *args, long *result); + + ~GetdentsOpReqRes() override; + + virtual void CopyRequestDataToBuf(void* buf); + + virtual int GetRequestSize() override; + virtual int GetRequestAlignSize() override; + virtual int GetResponseSize() override; + virtual int GetResponseAlignSize() override; + + virtual PosixOpResponse& GetResponse() override; + void SetResponse(void* response) override; +private: + GetdentsRequestData requestData_; + GetdentsResponseData responseData_; +}; + +// ----------------------------------closedir------------------------------------ +struct ClosedirRequestData : PosixOpRequest { + DirStream dirstream; +}; + +struct ClosedirResponseData : PosixOpResponse { + int ret; // 返回值 +}; + +class ClosedirOpReqRes : public PosixOpReqRes { +public: + ClosedirOpReqRes() + : PosixOpReqRes(PosixOpType::CLOSEDIR) {} + ClosedirOpReqRes(const DirStream& dirstream); + ClosedirOpReqRes(const long *args, long *result); + + ~ClosedirOpReqRes() override; + + virtual void CopyRequestDataToBuf(void* buf); + + virtual int GetRequestSize() override; + virtual int GetRequestAlignSize() override; + virtual int GetResponseSize() override; + virtual int GetResponseAlignSize() override; + virtual PosixOpResponse& GetResponse() override; + virtual void SetResponse(void* response) override; + +private: + ClosedirRequestData requestData_; + ClosedirResponseData responseData_; +}; + +// -------------------------unlink----------------------------------------- +struct UnlinkRequestData : PosixOpRequest { + char path[200]; +}; + +struct UnlinkResponseData : PosixOpResponse { + int ret; // 返回值 +}; + +class UnlinkOpReqRes : public PosixOpReqRes { +public: + UnlinkOpReqRes() + : PosixOpReqRes(PosixOpType::UNLINK) {} + UnlinkOpReqRes(const char *path); + UnlinkOpReqRes(const long *args, long *result); + ~UnlinkOpReqRes() override; + + virtual void CopyRequestDataToBuf(void* buf); + + virtual int GetRequestSize() override; + virtual int GetRequestAlignSize() override; + virtual int GetResponseSize() override; + virtual int GetResponseAlignSize() override; + + virtual PosixOpResponse& GetResponse() override; + void SetResponse(void* response) override; +private: + UnlinkRequestData requestData_; + UnlinkResponseData responseData_; +}; + +struct RenameRequestData : PosixOpRequest { + char oldpath[200]; + char newpath[200]; +}; + +struct RenameResponseData : PosixOpResponse { + int ret; // 返回值 +}; + +class RenameOpReqRes : public PosixOpReqRes { +public: + RenameOpReqRes() + : PosixOpReqRes(PosixOpType::RENAME) {} + RenameOpReqRes(const char *oldpath, const char *newpath); + RenameOpReqRes(const long *args, long *result); + + ~RenameOpReqRes() override; + + virtual void CopyRequestDataToBuf(void* buf); + + virtual int GetRequestSize() override; + virtual int GetRequestAlignSize() override; + virtual int GetResponseSize() override; + virtual int GetResponseAlignSize() override; + + virtual PosixOpResponse& GetResponse() override; + void SetResponse(void* response) override; +private: + RenameRequestData requestData_; + RenameResponseData responseData_; +}; + +// ----------------------truncate----------------------------------------- +class TruncateRequestData : public PosixOpRequest { +public: + char path[200]; + off_t length; +}; + +class TruncateResponseData : public PosixOpResponse { +public: + int ret; // 返回值 +}; + +class TruncateOpReqRes : public PosixOpReqRes { +public: + TruncateOpReqRes() + : PosixOpReqRes(PosixOpType::TRUNCATE) {} + TruncateOpReqRes(const char *path, off_t length); + TruncateOpReqRes(const long *args, long *result); + + ~TruncateOpReqRes() override; + + virtual void CopyRequestDataToBuf(void* buf); + + virtual int GetRequestSize() override; + virtual int GetRequestAlignSize() override; + virtual int GetResponseSize() override; + virtual int GetResponseAlignSize() override; + + virtual PosixOpResponse& GetResponse() override; + void SetResponse(void* response) override; +private: + TruncateRequestData requestData_; + TruncateResponseData responseData_; +}; + +// ----------------------TERMINAL------------------- +class TerminalRequestData : public PosixOpRequest { +public: + +}; + +class TerminalResponseData : public PosixOpResponse { +public: + int ret; // 返回值 +}; + +class TerminalOpReqRes : public PosixOpReqRes { +public: + TerminalOpReqRes(); + ~TerminalOpReqRes() override {}; + + virtual void CopyRequestDataToBuf(void* buf); + + virtual int GetRequestSize() override; + virtual int GetRequestAlignSize() override; + virtual int GetResponseSize() override ; + virtual int GetResponseAlignSize() override; + + virtual PosixOpResponse& GetResponse() override; + void SetResponse(void* response) override;; +private: + TerminalRequestData requestData_; + TerminalResponseData responseData_; +}; + +} // namespace internal +} // namespace intercept + diff --git a/intercept/middleware/CMakeLists.txt b/intercept/middleware/CMakeLists.txt new file mode 100644 index 0000000..d506dcb --- /dev/null +++ b/intercept/middleware/CMakeLists.txt @@ -0,0 +1,45 @@ +# src/middleware/CMakeLists.txt + +find_library(ICEORYX_POSH_LIB NAMES iceoryx_posh PATHS ../../thirdparties/iceoryx/lib) +find_library(ICEORYX_HOOFS_LIB NAMES iceoryx_hoofs PATHS ../../thirdparties/iceoryx/lib) + +file(GLOB MIDDLEWARE_SOURCES *.cpp) +file(GLOB MIDDLEWARE_HEADERS *.h) + +add_library(intercept_middleware ${MIDDLEWARE_SOURCES}) +target_include_directories(intercept_middleware PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../../thirdparties/iceoryx/include +) +target_link_libraries(intercept_middleware PUBLIC + intercept_internal + intercept_filesystem + ${ICEORYX_HOOFS_LIB} + ${ICEORYX_POSH_LIB} + +) + + +set(CMAKE_FIND_LIBRARY_SUFFIXES ".so" ".a") + +find_library(ICEORYX_POSH_LIB NAMES iceoryx_posh PATHS ../../thirdparties/iceoryx/lib) +find_library(ICEORYX_HOOFS_LIB NAMES iceoryx_hoofs PATHS ../../thirdparties/iceoryx/lib) +find_library(ICEORYX_PLATFORM_LIB NAMES iceoryx_hoofs PATHS ../../thirdparties/iceoryx/lib) + +file(GLOB CLIENT_MIDDLEWARE_SOURCES *.cpp) +file(GLOB CLIENT_MIDDLEWARE_HEADERS *.h) + +add_library(intercept_middleware_client ${CLIENT_MIDDLEWARE_SOURCES}) +target_include_directories(intercept_middleware_client PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../../thirdparties/iceoryx/include +) +target_link_libraries(intercept_middleware_client PUBLIC + -lrt + intercept_internal_client + intercept_filesystem_client + ${ICEORYX_POSH_LIB} + ${ICEORYX_HOOFS_LIB} + ${ICEORYX_PLATFORM_LIB} +) +target_compile_options(intercept_middleware_client PUBLIC -DCLIENT_BUILD -fPIC ) \ No newline at end of file diff --git a/intercept/middleware/iceoryx_wrapper.cpp b/intercept/middleware/iceoryx_wrapper.cpp new file mode 100644 index 0000000..82ad99a --- /dev/null +++ b/intercept/middleware/iceoryx_wrapper.cpp @@ -0,0 +1,645 @@ +#include "filesystem/abstract_filesystem.h" +#ifndef CLIENT_BUILD +#include "filesystem/curve_filesystem.h" +#endif +#include "iox/signal_watcher.hpp" +#include "iceoryx_wrapper.h" + +#include "iceoryx_posh/mepoo/chunk_header.hpp" + +namespace intercept { +namespace middleware { + +using intercept::internal::PosixOpReqRes; +using intercept::internal::PosixOpRequest; +using intercept::internal::PosixOpResponse; +using intercept::internal::PosixOpType; + +using intercept::internal::OpenRequestData; +using intercept::internal::OpenResponseData; +using intercept::internal::ReadRequestData; +using intercept::internal::ReadResponseData; +using intercept::internal::WriteRequestData; +using intercept::internal::WriteResponseData; +using intercept::internal::CloseRequestData; +using intercept::internal::CloseResponseData; +using intercept::internal::StatRequestData; +using intercept::internal::StatResponseData; +using intercept::internal::FstatRequestData; +using intercept::internal::FstatResponseData; +using intercept::internal::FsyncRequestData; +using intercept::internal::FsyncResponseData; +using intercept::internal::LseekRequestData; +using intercept::internal::LseekResponseData; +using intercept::internal::MkdirRequestData; +using intercept::internal::MkdirResponseData; +using intercept::internal::OpendirRequestData; +using intercept::internal::OpendirResponseData; +using intercept::internal::GetdentsRequestData; +using intercept::internal::GetdentsResponseData; +using intercept::internal::ClosedirRequestData; +using intercept::internal::ClosedirResponseData; +using intercept::internal::UnlinkRequestData; +using intercept::internal::UnlinkResponseData; +using intercept::internal::RenameRequestData; +using intercept::internal::RenameResponseData; +using intercept::internal::TruncateRequestData; +using intercept::internal::TruncateResponseData; +using intercept::internal::TerminalRequestData; +using intercept::internal::TerminalResponseData; + +std::shared_ptr ReqResMiddlewareWrapper::fileSystem_ = nullptr; + +IceoryxWrapper::IceoryxWrapper(const ServiceMetaInfo& info) : + ReqResMiddlewareWrapper(info){ +} + +IceoryxWrapper::~IceoryxWrapper() { + Shutdown(); +} + +void IceoryxWrapper::Init() { + +} +void IceoryxWrapper::InitClient() { + // 创建client + iox::capro::IdString_t service(iox::TruncateToCapacity, + info_.service.c_str(), info_.service.length()); + iox::capro::IdString_t instance(iox::TruncateToCapacity, + info_.instance.c_str(), info_.instance.length()); + iox::capro::IdString_t event(iox::TruncateToCapacity, + info_.event.c_str(), info_.event.length()); + + client_.reset(new iox::popo::UntypedClient({service, instance, event})); + spdlog::info("client init, service: {}, instance: {}, event: {}", + info_.service, info_.instance, info_.event); +} + +void IceoryxWrapper::InitServer() { + // 创建server + ReqResMiddlewareWrapper::InitServer(); + iox::capro::IdString_t service(iox::TruncateToCapacity, + info_.service.c_str(), info_.service.length()); + iox::capro::IdString_t instance(iox::TruncateToCapacity, + info_.instance.c_str(), info_.instance.length()); + iox::capro::IdString_t event(iox::TruncateToCapacity, + info_.event.c_str(), info_.event.length()); + server_.reset(new iox::popo::UntypedServer({service, instance, event})); + // std::cout << "server init, service: " << info_.service << ", instance: " << info_.instance << ", event: " << info_.event << std::endl; + spdlog::info("IceoryxWrapper::InitServer, server: {}, instance: {}, event: {} ", info_.service, info_.instance, info_.event); +} + +void IceoryxWrapper::InitDummyServer() { + iox::capro::IdString_t service(iox::TruncateToCapacity, + info_.service.c_str(), info_.service.length()); + iox::capro::IdString_t instance(iox::TruncateToCapacity, + info_.instance.c_str(), info_.instance.length()); + iox::capro::IdString_t event(iox::TruncateToCapacity, + info_.event.c_str(), info_.event.length()); + server_.reset(new iox::popo::UntypedServer({service, instance, event})); + // std::cout << "server init, service: " << info_.service << ", instance: " << info_.instance << ", event: " << info_.event << std::endl; + spdlog::info("IceoryxWrapper::InitDummyServer, server: {}, instance: {}, event: {} ", info_.service, info_.instance, info_.event); +} + +void IceoryxWrapper::Shutdown() { + spdlog::info("shutdown IceoryxWrapper"); + if (servicetype_ == ServiceType::SERVER) { + spdlog::info("stop the server...."); + // StopServer(); + } else if (servicetype_ == ServiceType::CLIENT) { + StopClient(); + spdlog::info("stop the client...."); + } else if (servicetype_ == ServiceType::DUMMYSERVER) { + spdlog::info("stop the dummyserver, do nothing"); + } else { + spdlog::info("unknown service type : {}", (int)servicetype_); + } +} + +void IceoryxWrapper::StartServer() { + // 启动server + if (server_.get() == nullptr) { + std::cerr << "server is nullptr" << std::endl; + return; + } + spdlog::info("enter IceoryxWrapper::StartServer, bgein OnResponse"); + running_ = true; + OnResponse(); + spdlog::info("enter IceoryxWrapper::StartServer, end OnResponse"); +} + +// 暂时没有调用 +void IceoryxWrapper::StartClient() { + // 启动client + InitClient(); +} + +void IceoryxWrapper::StopServer() { + kill(getpid(), SIGINT); + running_ = false; +} + +void IceoryxWrapper::StopClient() { + intercept::internal::TerminalOpReqRes terminal; + spdlog::info("wait stop client, service: {}, instance: {}, event: {}, client count: {}", + info_.service, info_.instance, info_.event, client_.use_count()); + OnRequest(terminal); +} + +// client: 这里组织请求并处理返回的响应 +void IceoryxWrapper::OnRequest(PosixOpReqRes& reqRes) { + // 上游用户侧需要调用 + // 假设我们直接将请求的响应数据复制回响应对象 + int reqsize = reqRes.GetRequestSize(); + int alignsize = reqRes.GetRequestAlignSize(); + int64_t expectedResponseSequenceId = requestSequenceId_; + + { + // intercept::common::Timer timer("client request"); + client_->loan(reqsize, alignsize) + .and_then([&](auto& requestPayload) { + + auto requestHeader = iox::popo::RequestHeader::fromPayload(requestPayload); + requestHeader->setSequenceId(requestSequenceId_); + expectedResponseSequenceId = requestSequenceId_; + requestSequenceId_ += 1; + char* request = static_cast(requestPayload); + + const iox::mepoo::ChunkHeader * chunkHeader = iox::mepoo::ChunkHeader::fromUserPayload(requestPayload); + spdlog::info("to loan chunk in client, head info, chunksize: {}", chunkHeader->chunkSize()); + + reqRes.CopyRequestDataToBuf((void*)request); + client_->send(request).or_else( + [&](auto& error) { std::cout << "Could not send Request! Error: " << error << std::endl; }); + }) + .or_else([](auto& error) { std::cout << "Could not allocate Request! Error: " << error << std::endl; }); + + } + //! [take response] + { + // intercept::common::Timer timer("client response"); + bool hasReceivedResponse{false}; + do{ + client_->take().and_then([&](const auto& responsePayload) { + auto responseHeader = iox::popo::ResponseHeader::fromPayload(responsePayload); + if (responseHeader->getSequenceId() == expectedResponseSequenceId) + { + const iox::mepoo::ChunkHeader * chunkHeader = iox::mepoo::ChunkHeader::fromUserPayload(responsePayload); + spdlog::info("to release chunk in client, head info, type: {} typestr: {} , chunksize: {}", int(reqRes.GetOpType()), TypeToStr(reqRes.GetOpType()), chunkHeader->chunkSize()); + + reqRes.SetResponse((void*)responsePayload); + + client_->releaseResponse(responsePayload); + // sleep(10); + const iox::mepoo::ChunkHeader* nowheader = iox::mepoo::ChunkHeader::fromUserPayload(responsePayload); + if (nowheader == nullptr) { + spdlog::error("the chunkheader is nullptr!!!!"); + } + spdlog::info("chunkheader info, chunksize {}", nowheader->chunkSize()); + // std::cout << "Got Response with expected sequence ID! -> continue" << std::endl; + } + else + { + spdlog::error("Got Response with outdated sequence ID! Expected = {}; Actual = {} ! -> skip", + expectedResponseSequenceId, responseHeader->getSequenceId()); + } + hasReceivedResponse = true; + }); + } while (!hasReceivedResponse); + } + +} + +// server: 这里获取、处理请求并返回响应结果 +void IceoryxWrapper::OnResponse() { + auto lastRequestTime = std::chrono::steady_clock::now(); // 初始化上一次处理请求的时间戳 + int intervalSeconds = intercept::common::Configure::getInstance().getConfig("waitRequestMaxSeconds") == "" ? 5 : std::stoi(intercept::common::Configure::getInstance().getConfig("waitRequestMaxSeconds")); + int trynumber = 0; + int getnum = 0; + int missnum = 0; + + std::chrono::steady_clock::duration totalDuration = std::chrono::steady_clock::duration::zero(); // 总耗时 + while (!iox::hasTerminationRequested() && running_) { + trynumber++; + if(trynumber > 2000000) { + // ! 注意的判断可能会导致某些连接过早被中断,使得client无法正常响应 + auto now = std::chrono::steady_clock::now(); // 获取当前时间 + if (now - lastRequestTime > std::chrono::seconds(intervalSeconds)) { // 检查是否超过n秒无请求处理 + spdlog::info("No request handled in the last {} seconds. Exiting loop.", intervalSeconds); + break; + } + } + server_->take().and_then([&](auto& requestPayload) { + auto begintime = std::chrono::steady_clock::now(); + auto request = static_cast(requestPayload); + // std::cout << "request type: " << (int)request->opType << std::endl; + switch (request->opType) { + case PosixOpType::OPEN: + HandleOpenRequest(requestPayload); + break; + case PosixOpType::READ: + HandleReadRequest(requestPayload); + break; + case PosixOpType::WRITE: + HandleWriteRequest(requestPayload); + break; + case PosixOpType::CLOSE: + HandleCloseRequest(requestPayload); + break; + case PosixOpType::STAT: + HandleStatRequest(requestPayload); + break; + case PosixOpType::FSTAT: + HandleFstatRequest(requestPayload); + break; + case PosixOpType::FSYNC: + HandleFsyncRequest(requestPayload); + break; + case PosixOpType::LSEEK: + HandleLseekRequest(requestPayload); + break; + case PosixOpType::MKDIR: + HandleMkdirRequest(requestPayload); + break; + case PosixOpType::UNLINK: + HandleUnlinkRequest(requestPayload); + break; + case PosixOpType::OPENDIR: + HandleOpendirRequest(requestPayload); + break; + case PosixOpType::GETDENTS: + HandleGetdentsRequest(requestPayload); + break; + case PosixOpType::CLOSEDIR: + HandleClosedirRequest(requestPayload); + break; + case PosixOpType::RENAME: + HandleRenameRequest(requestPayload); + break; + case PosixOpType::TRUNCATE: + HandleTruncateRequest(requestPayload); + break; + case PosixOpType::TERMINAL: + HandleTerminalRequest(requestPayload); + break; + default: + spdlog::error("Unsupported request type: {}", (int)request->opType); + break; + } + + // 更新最后处理请求的时间戳 + lastRequestTime = std::chrono::steady_clock::now(); + trynumber = 0; // 归零 + getnum++; + totalDuration += (lastRequestTime - begintime); + } + ); + // TODO: 如果不sleep 获取不到数据 待排查 + // sleep(1); + } + std::cout << "exit Server OnResponse... " << info_.service << " " << info_.instance << " " << info_.event << std::endl; + + // if (getnum > 0) { + // std::cout << "total request time: " << totalDuration.count() << " , average time : " << totalDuration.count()/ getnum << std::endl; + // } +} + +void IceoryxWrapper::HandleOpenRequest(const auto& requestPayload) { + auto request = static_cast(requestPayload); + spdlog::info("Open file request, path: {}, flags: {}, mode: {}", request->path, request->flags, request->mode); + // 这里可以调用posix open函数 + auto requestHeader = iox::popo::RequestHeader::fromPayload(requestPayload); + server_->loan(requestHeader, sizeof(OpenResponseData), alignof(OpenResponseData)) + .and_then([&](auto& responsePayload) { + const iox::mepoo::ChunkHeader * chunkHeader = iox::mepoo::ChunkHeader::fromUserPayload(requestPayload); + spdlog::info("to loan chunk in server open , head info, chunksize: {}", chunkHeader->chunkSize()); + + auto response = static_cast(responsePayload); + response->opType = request->opType; + response->fd = fileSystem_->Open(request->path, request->flags, request->mode); + server_->send(responsePayload).or_else( + [&](auto& error) { std::cout << "Could not send Response! Error: " << error << std::endl; }); + spdlog::info("open response info, the type: {}, the fd: {}", intercept::internal::TypeToStr(response->opType), response->fd ); + }) + .or_else( + [&](auto& error) { std::cout << "Could not allocate Open Response! Error: " << error << std::endl; }); + + const iox::mepoo::ChunkHeader * chunkHeader = iox::mepoo::ChunkHeader::fromUserPayload(requestPayload); + spdlog::info("to release chunk in server open , head info, chunksize: {}", chunkHeader->chunkSize()); + server_->releaseRequest(request); +} + +void IceoryxWrapper::HandleReadRequest(const auto& requestPayload) { + auto request = static_cast(requestPayload); + auto requestHeader = iox::popo::RequestHeader::fromPayload(requestPayload); + server_->loan(requestHeader, sizeof(ReadResponseData) + request->count, alignof(ReadResponseData)) + .and_then([&](auto& responsePayload) { + auto response = static_cast(responsePayload); + response->opType = request->opType; + char* buf = (char*) response + sizeof(ReadResponseData); + + const iox::mepoo::ChunkHeader * chunkHeader = iox::mepoo::ChunkHeader::fromUserPayload(requestPayload); + // spdlog::info("to loan chunk in server read , head info, chunksize: {} real size: {}", chunkHeader->chunkSize(), sizeof(ReadResponseData) + request->count); + + if (intercept::common::Configure::getInstance().getConfig("multiop") == "true" + && request->count >= atol(intercept::common::Configure::getInstance().getConfig("blocksize").c_str())) { + response->length = fileSystem_->MultiRead(request->fd, buf, request->count); + } else { + response->length = fileSystem_->Read(request->fd, buf, request->count); + } + server_->send(responsePayload).or_else([&](auto& error){ std::cout << "Could not send Response for Read! Error: " << error << std::endl;}); + spdlog::debug("read response, fd: {}, count: {}, read response info, the type: {}, the length: {}", + request->fd, request->count, intercept::internal::TypeToStr(response->opType), response->length); + }).or_else( + [&](auto& error) { std::cout << "Could not allocate Read Response! Error: " << error << std::endl; }); + + const iox::mepoo::ChunkHeader * chunkHeader = iox::mepoo::ChunkHeader::fromUserPayload(requestPayload); + // spdlog::info("to release chunk in server read , head info, chunksize: {}", chunkHeader->chunkSize()); + + server_->releaseRequest(request); + +} + +void IceoryxWrapper::HandleWriteRequest(const auto& requestPayload) { + spdlog::debug("handle one write request"); + auto request = static_cast(requestPayload); + auto requestHeader = iox::popo::RequestHeader::fromPayload(requestPayload); + server_->loan(requestHeader, sizeof(WriteResponseData), alignof(WriteResponseData)) + .and_then([&](auto& responsePayload) { + auto response = static_cast(responsePayload); + response->opType = request->opType; + if (intercept::common::Configure::getInstance().getConfig("multiop") == "true" + && request->count >= atol(intercept::common::Configure::getInstance().getConfig("blocksize").c_str())) { + response->length = fileSystem_->MultiWrite(request->fd, request->content, request->count); + } else { + response->length = fileSystem_->Write(request->fd, request->content, request->count); + } + + server_->send(responsePayload).or_else([&](auto& error){ std::cout << "Could not send Response for Write! Error: " << error << std::endl;}); + spdlog::debug("write response, fd: {}, count: {}, write response info, the type: {}, the length: {}", + request->fd, request->count, intercept::internal::TypeToStr(response->opType), response->length); + }).or_else( + [&](auto& error) { std::cout << "Could not allocate Write Response! Error: " << error << std::endl; }); + server_->releaseRequest(request); +} + +void IceoryxWrapper::HandleCloseRequest(const auto& requestPayload) { + auto request = static_cast(requestPayload); + spdlog::info("close request, fd: {}", request->fd); + auto requestHeader = iox::popo::RequestHeader::fromPayload(requestPayload); + server_->loan(requestHeader, sizeof(CloseResponseData), alignof(CloseResponseData)) + .and_then([&](auto& responsePayload) { + + const iox::mepoo::ChunkHeader * chunkHeader = iox::mepoo::ChunkHeader::fromUserPayload(requestPayload); + spdlog::info("to loan chunk in server close , head info, chunksize: {}", chunkHeader->chunkSize()); + + auto response = static_cast(responsePayload); + response->opType = request->opType; + response->ret = fileSystem_->Close(request->fd); + spdlog::info("finish close, fd: {}", request->fd); + server_->send(responsePayload).or_else([&](auto& error){ std::cout << "Could not send Response for Close! Error: " << error << std::endl;}); + + spdlog::info("close response info, the type: {}, the ret: {}", intercept::internal::TypeToStr(response->opType), response->ret); + }).or_else( + [&](auto& error) { std::cout << "Could not allocate Write Response! Error: " << error << std::endl; }); + + const iox::mepoo::ChunkHeader * chunkHeader = iox::mepoo::ChunkHeader::fromUserPayload(requestPayload); + spdlog::info("to release chunk in server close , head info, chunksize: {}", chunkHeader->chunkSize()); + + server_->releaseRequest(request); +} + +void IceoryxWrapper::HandleFsyncRequest(const auto& requestPayload) { + auto request = static_cast(requestPayload); + spdlog::info("fsync reqeust, fd: {}", request->fd); + auto requestHeader = iox::popo::RequestHeader::fromPayload(requestPayload); + server_->loan(requestHeader, sizeof(FsyncResponseData), alignof(FsyncResponseData)) + .and_then([&](auto& responsePayload) { + auto response = static_cast(responsePayload); + response->opType = request->opType; + response->ret = fileSystem_->Fsync(request->fd); + server_->send(responsePayload).or_else([&](auto& error){ std::cout << "Could not send Response for Stat! Error: " << error << std::endl;}); + spdlog::info("fsync response info, ret: {}", response->ret); + }).or_else( + [&](auto& error) { std::cout << "Could not allocate Write Response! Error: " << error << std::endl; }); + server_->releaseRequest(request); +} + +void IceoryxWrapper::HandleStatRequest(const auto& requestPayload) { + auto request = static_cast(requestPayload); + spdlog::info("stat request, pathname: {}", request->path); + auto requestHeader = iox::popo::RequestHeader::fromPayload(requestPayload); + server_->loan(requestHeader, sizeof(StatResponseData), alignof(StatResponseData)) + .and_then([&](auto& responsePayload) { + auto response = static_cast(responsePayload); + response->opType = request->opType; + response->ret = fileSystem_->Stat(request->path, &(response->fileStat)); + server_->send(responsePayload).or_else([&](auto& error){ std::cout << "Could not send Response for Stat! Error: " << error << std::endl;}); + spdlog::info("stat response info, the ino: {}, size: {}, the ret: {}", + (int)response->fileStat.st_ino, response->fileStat.st_size, response->ret); + }).or_else( + [&](auto& error) { std::cout << "Could not allocate Write Response! Error: " << error << std::endl; }); + server_->releaseRequest(request); +} + +void IceoryxWrapper::HandleFstatRequest(const auto& requestPayload) { + auto request = static_cast(requestPayload); + spdlog::info("fstat request, fd: {}", request->fd); + auto requestHeader = iox::popo::RequestHeader::fromPayload(requestPayload); + server_->loan(requestHeader, sizeof(FstatResponseData), alignof(FstatResponseData)) + .and_then([&](auto& responsePayload) { + auto response = static_cast(responsePayload); + response->opType = request->opType; + response->ret = fileSystem_->Fstat(request->fd, &(response->fileStat)); + server_->send(responsePayload).or_else([&](auto& error){ std::cout << "Could not send Response for Stat! Error: " << error << std::endl;}); + spdlog::info("fstat response info, the ino: {}, size: {}, the ret: {}", + (int)response->fileStat.st_ino, response->fileStat.st_size, response->ret); + + }).or_else( + [&](auto& error) { std::cout << "Could not allocate Write Response! Error: " << error << std::endl; }); + server_->releaseRequest(request); +} + +void IceoryxWrapper::HandleLseekRequest(const auto& requestPayload) { + auto request = static_cast(requestPayload); + spdlog::debug("lseek request, fd: {}, offset: {}", request->fd, request->offset); + auto requestHeader = iox::popo::RequestHeader::fromPayload(requestPayload); + server_->loan(requestHeader, sizeof(LseekResponseData), alignof(LseekResponseData)) + .and_then([&](auto& responsePayload) { + auto response = static_cast(responsePayload); + response->opType = request->opType; + response->ret = fileSystem_->Lseek(request->fd, request->offset, request->whence); + server_->send(responsePayload).or_else([&](auto& error){ std::cout << "Could not send Response for Stat! Error: " << error << std::endl;}); + spdlog::debug("lseek response, ret: {}", response->ret); + }).or_else( + [&](auto& error) { std::cout << "Could not allocate Write Response! Error: " << error << std::endl; }); + server_->releaseRequest(request); +} + +void IceoryxWrapper::HandleMkdirRequest(const auto& requestPayload) { + auto request = static_cast(requestPayload); + spdlog::info("mkdir request, pathname: {}, mode: {}", request->path, request->mode); + auto requestHeader = iox::popo::RequestHeader::fromPayload(requestPayload); + server_->loan(requestHeader, sizeof(MkdirResponseData), alignof(MkdirResponseData)) + .and_then([&](auto& responsePayload) { + auto response = static_cast(responsePayload); + response->opType = request->opType; + response->ret = fileSystem_->Mkdir(request->path, request->mode); + server_->send(responsePayload).or_else([&](auto& error){ std::cout << "Could not send Response for Stat! Error: " << error << std::endl;}); + spdlog::info("mkdir resposne, ret: {}", response->ret); + }).or_else( + [&](auto& error) { std::cout << "Could not allocate Write Response! Error: " << error << std::endl; }); + server_->releaseRequest(request); +} + +void IceoryxWrapper::HandleGetdentsRequest(const auto& requestPayload) { + auto request = static_cast(requestPayload); + int maxread = request->maxread; + maxread = 200; // 暂时读取目录下的200个文件,否则分配会失败 + spdlog::info("getdents request, fd: {}, the info: {}", request->dirinfo.fh, request->dirinfo.ino); + auto requestHeader = iox::popo::RequestHeader::fromPayload(requestPayload); + server_->loan(requestHeader, sizeof(GetdentsResponseData) + maxread * sizeof(dirent64), alignof(GetdentsResponseData)) + .and_then([&](auto& responsePayload) { + auto response = static_cast(responsePayload); + response->opType = request->opType; + auto req = const_cast(request); + response->ret = fileSystem_->Getdents(&req->dirinfo, response->contents, maxread, &response->realbytes); + response->dirinfo = req->dirinfo; + server_->send(responsePayload).or_else([&](auto& error){ std::cout << "Could not send Response for Stat! Error: " << error << std::endl;}); + spdlog::info("getdents response, ret: {}, thre realbytes: {}, the offset: {}", + response->ret, response->realbytes, response->dirinfo.offset); + }).or_else( + [&](auto& error) { std::cout << "Could not allocate Write Response! Error: " << error << std::endl; }); + server_->releaseRequest(request); +} + +void IceoryxWrapper::HandleOpendirRequest(const auto&requestPayload) { + auto request = static_cast(requestPayload); + spdlog::info("opendir request, path: {}", request->path); + auto requestHeader = iox::popo::RequestHeader::fromPayload(requestPayload); + server_->loan(requestHeader, sizeof(OpendirResponseData), alignof(OpendirResponseData)) + .and_then([&](auto& responsePayload) { + auto response = static_cast(responsePayload); + response->opType = request->opType; + response->ret = fileSystem_->Opendir(request->path, &response->dirStream); + server_->send(responsePayload).or_else( + [&](auto& error) { std::cout << "Could not send Response! Error: " << error << std::endl; }); + spdlog::info("opendir response, the type: {}, the fd: {}", TypeToStr(response->opType), response->dirStream.fh); + }) + .or_else( + [&](auto& error) { std::cout << "Could not allocate Open Response! Error: " << error << std::endl; }); + server_->releaseRequest(request); +} + +void IceoryxWrapper::HandleClosedirRequest(const auto& requestPayload) { + auto request = static_cast(requestPayload); + spdlog::info("closedir requset, fd: {}", request->dirstream.fh); + auto requestHeader = iox::popo::RequestHeader::fromPayload(requestPayload); + server_->loan(requestHeader, sizeof(ClosedirResponseData), alignof(ClosedirResponseData)) + .and_then([&](auto& responsePayload) { + auto response = static_cast(responsePayload); + response->opType = request->opType; + response->ret = fileSystem_->Closedir(const_cast(&request->dirstream)); + server_->send(responsePayload).or_else([&](auto& error){ std::cout << "Could not send Response for Closedir! Error: " << error << std::endl;}); + spdlog::info("closedir response, the type: {}, the ret: {}", TypeToStr(response->opType), response->ret ); + }).or_else( + [&](auto& error) { std::cout << "Could not allocate Write Response! Error: " << error << std::endl; }); + server_->releaseRequest(request); +} + + +void IceoryxWrapper::HandleUnlinkRequest(const auto& requestPayload) { + auto request = static_cast(requestPayload); + spdlog::info("unlink reqeust, pathname: {}", request->path); + auto requestHeader = iox::popo::RequestHeader::fromPayload(requestPayload); + server_->loan(requestHeader, sizeof(UnlinkResponseData), alignof(UnlinkResponseData)) + .and_then([&](auto& responsePayload) { + auto response = static_cast(responsePayload); + response->opType = request->opType; + response->ret = fileSystem_->Unlink(request->path); + server_->send(responsePayload).or_else([&](auto& error){ std::cout << "Could not send Response for Stat! Error: " << error << std::endl;}); + spdlog::info("unlink response, ret: ", response->ret); + }).or_else( + [&](auto& error) { std::cout << "Could not allocate Write Response! Error: " << error << std::endl; }); + server_->releaseRequest(request); +} + +void IceoryxWrapper::HandleRenameRequest(const auto& requestPayload) { + auto request = static_cast(requestPayload); + spdlog::info("rename request, oldpath: {}, newpath: {}", request->oldpath, request->newpath); + auto requestHeader = iox::popo::RequestHeader::fromPayload(requestPayload); + server_->loan(requestHeader, sizeof(RenameResponseData), alignof(RenameResponseData)) + .and_then([&](auto& responsePayload) { + auto response = static_cast(responsePayload); + response->opType = request->opType; + response->ret = fileSystem_->Rename(request->oldpath, request->newpath); + server_->send(responsePayload).or_else([&](auto& error){ std::cout << "Could not send Response for Stat! Error: " << error << std::endl;}); + spdlog::info("rename response, ret: {}", response->ret); + }).or_else( + [&](auto& error) { std::cout << "Could not allocate Write Response! Error: " << error << std::endl; }); + server_->releaseRequest(request); +} + +void IceoryxWrapper::HandleTruncateRequest(const auto& requestPayload) { + + auto request = static_cast(requestPayload); + spdlog::info("truncate request, path: {}, length: {}", request->path, request->length); + auto requestHeader = iox::popo::RequestHeader::fromPayload(requestPayload); + server_->loan(requestHeader, sizeof(TruncateResponseData), alignof(TruncateResponseData)) + .and_then([&](auto& responsePayload) { + auto response = static_cast(responsePayload); + + response->opType = request->opType; + response->ret = fileSystem_->Truncate(request->path, request->length); + server_->send(responsePayload).or_else([&](auto& error){ std::cout << "Could not send Response for Stat! Error: " << error << std::endl;}); + spdlog::info("truncate response, ret: {}", response->ret); + }).or_else( + [&](auto& error) { std::cout << "Could not allocate Write Response! Error: " << error << std::endl; }); + server_->releaseRequest(request); +} + +void IceoryxWrapper::HandleTerminalRequest(const auto& requestPayload) { + + auto request = static_cast(requestPayload); + spdlog::info("terminal request."); + auto requestHeader = iox::popo::RequestHeader::fromPayload(requestPayload); + server_->loan(requestHeader, sizeof(TerminalResponseData), alignof(TerminalResponseData)) + .and_then([&](auto& responsePayload) { + + auto response = static_cast(responsePayload); + + response->opType = request->opType; + response->ret = 0; + running_ = false; // 终结退出 + server_->send(responsePayload).or_else([&](auto& error){ std::cout << "Could not send Response for Terminal! Error: " << error << std::endl;}); + + const iox::mepoo::ChunkHeader * chunkHeader = iox::mepoo::ChunkHeader::fromUserPayload(responsePayload); + spdlog::info("terminal response, ret: {}, pid: {}, tid: {}, loan chunk chunksize: {}", + response->ret, (unsigned int) getpid(), (unsigned int) pthread_self(), chunkHeader->chunkSize()); + sleep(0.1); + }).or_else( + [&](auto& error) { std::cout << "Could not allocate Terminal Response! Error: " << error << std::endl; }); + + const iox::mepoo::ChunkHeader * chunkHeader = iox::mepoo::ChunkHeader::fromUserPayload(requestPayload); + spdlog::info("to release chunk in server terminal , head info, chunksize: {}", chunkHeader->chunkSize()); + server_->releaseRequest(request); +} + +} // namespace middleware +} // namespace intercept + + +int test() { + // std::string servicename = "MyService"; + // std::unique_ptr middleware = std::make_unique(servicename); + // AddClientService(servicename); + // WriteOpReqRes writeReqRes(1, "data".data(), 4, 0); + // int ret = middleware->OnRequest(writeReqRes); + // const auto& response = middleware->GetResponse(writeRequest); + // if (response.result >= 0) { + // std::cout << "Write operation successful!" << std::endl; + // } else { + // std::cout << "Write operation failed with error code: " << response.result << std::endl; + // } + return 0; +} diff --git a/intercept/middleware/iceoryx_wrapper.h b/intercept/middleware/iceoryx_wrapper.h new file mode 100644 index 0000000..eabc093 --- /dev/null +++ b/intercept/middleware/iceoryx_wrapper.h @@ -0,0 +1,76 @@ +#pragma once + +#include "req_res_middleware_wrapper.h" + +#include "iceoryx_posh/popo/untyped_server.hpp" +#include "iceoryx_posh/popo/untyped_client.hpp" + +namespace intercept { +namespace filesystem { + class AbstractFileSystem; // Forward declaration +} +} + +namespace intercept { +namespace middleware { + +class IceoryxWrapper : public ReqResMiddlewareWrapper { +public: + explicit IceoryxWrapper(const ServiceMetaInfo& info); + + ~IceoryxWrapper() override; + + virtual void Init() override; + + virtual void InitClient() override; + + virtual void InitServer() override; + + virtual void InitDummyServer() override; + + virtual void StartServer(); + + virtual void StartClient(); + + virtual void StopServer() override; + + virtual void StopClient() override; + + virtual void OnRequest(PosixOpReqRes& reqRes) override; + + virtual void OnResponse() override; + + virtual void Shutdown() override; + + virtual ServiceMetaInfo GetServiceMetaInfo() override {return info_;} + +private: + void HandleOpenRequest(const auto& requestPayload); + void HandleReadRequest(const auto& requestPayload); + void HandleWriteRequest(const auto& requestPayload); + void HandleCloseRequest(const auto& requestPayload); + void HandleLseekRequest(const auto& requestPayload); + void HandleFsyncRequest(const auto& requestPayload); + void HandleStatRequest(const auto& requestPayload); + void HandleFstatRequest(const auto& requestPayload); + void HandleMkdirRequest(const auto& requestPayload); + void HandleOpendirRequest(const auto& requestPayload); + void HandleGetdentsRequest(const auto& requestPayload); + void HandleClosedirRequest(const auto& requestPayload); + void HandleUnlinkRequest(const auto& requestPayload); + void HandleRenameRequest(const auto& requestPayload); + void HandleTruncateRequest(const auto& requestPayload); + void HandleTerminalRequest(const auto& requestPayload); + +private: + std::shared_ptr server_; + + std::shared_ptr client_; + + int64_t requestSequenceId_ = 0; + bool running_ = false; +}; + + +} // namespace middleware +} // namespace intercept diff --git a/intercept/middleware/req_res_middleware_wrapper.cpp b/intercept/middleware/req_res_middleware_wrapper.cpp new file mode 100644 index 0000000..ffbea21 --- /dev/null +++ b/intercept/middleware/req_res_middleware_wrapper.cpp @@ -0,0 +1,49 @@ +#include + +#include "middleware/req_res_middleware_wrapper.h" +#ifndef CLIENT_BUILD +#include "filesystem/curve_filesystem.h" +#include "filesystem/s3fs_filesystem.h" +#include "filesystem/dummy_filesystem.h" +#endif +#include "filesystem/abstract_filesystem.h" + + +namespace intercept { +namespace middleware { +using intercept::common::Configure; +void ReqResMiddlewareWrapper::Init() { + +} + +void ReqResMiddlewareWrapper::InitServer() { + if (info_.serverType == "dummy") { + spdlog::info("dont create fileSystem in ReqResMiddlewareWrapper::InitServer"); + return; + } + if (!fileSystem_) { + #ifndef CLIENT_BUILD + if (Configure::getInstance().getConfig("backendFilesystem") == "s3fs") { + fileSystem_.reset(new intercept::filesystem::S3fsFileSystem); + } else if (Configure::getInstance().getConfig("backendFilesystem") == "curvefs") { + fileSystem_.reset(new intercept::filesystem::CurveFileSystem); + } else if (Configure::getInstance().getConfig("backendFilesystem") == "dummyfs") { + fileSystem_.reset(new intercept::filesystem::DummyFileSystem); + } else { + spdlog::error("dont create fileSystem in ReqResMiddlewareWrapper::InitServer"); + return; + } + fileSystem_->Init(); + spdlog::info("Initserver, filesystem: {}", Configure::getInstance().getConfig("backendFilesystem")); + #endif + } else { + spdlog::info("ReqResMiddlewareWrapper::InitServer, have inited, donot need to init again"); + } +} + +void ReqResMiddlewareWrapper::InitClient() { + +} + +} // namespace middleware +} // namespace intercept \ No newline at end of file diff --git a/intercept/middleware/req_res_middleware_wrapper.h b/intercept/middleware/req_res_middleware_wrapper.h new file mode 100644 index 0000000..99157eb --- /dev/null +++ b/intercept/middleware/req_res_middleware_wrapper.h @@ -0,0 +1,80 @@ +#pragma once +#include + +#include "internal/posix_op_req_res.h" +#include "internal/metainfo.h" + +namespace intercept { +namespace filesystem { + class AbstractFileSystem; // Forward declaration +} +} + +namespace intercept +{ +namespace middleware +{ +using intercept::internal::ServiceMetaInfo; +using intercept::internal::PosixOpReqRes; + +enum class ServiceType { + CLIENT = 0, + SERVER = 1, + DUMMYSERVER = 2, +}; + +class ReqResMiddlewareWrapper { +public: + ReqResMiddlewareWrapper() { + spdlog::info("construct ReqResMiddlewareWrapper"); + } + + ReqResMiddlewareWrapper(ServiceMetaInfo info) : info_(info) { + spdlog::info("construct ReqResMiddlewareWrapper"); + + } + + virtual ~ReqResMiddlewareWrapper() { + spdlog::info("deconstruct ReqResMiddlewareWrapper"); + + } + + virtual void Init(); + + virtual void InitClient(); + + virtual void InitServer(); + + virtual void SetServiceType(ServiceType type) { + servicetype_ = type; + } + + virtual void InitDummyServer() {} + + virtual void StartServer() = 0; + + virtual void StartClient() = 0; + + virtual void StopServer() = 0; + + virtual void StopClient() = 0; + + // 对外request接口 + virtual void OnRequest(PosixOpReqRes& reqRes) = 0; + + // 对外response接口 + virtual void OnResponse() = 0; + + virtual void Shutdown() = 0; + + virtual ServiceMetaInfo GetServiceMetaInfo() = 0; + +protected: + static std::shared_ptr fileSystem_; + ServiceMetaInfo info_; + ServiceType servicetype_; +}; + +} // namespace middleware +} // namespace intercept + diff --git a/intercept/posix/CMakeLists.txt b/intercept/posix/CMakeLists.txt new file mode 100644 index 0000000..eb35a58 --- /dev/null +++ b/intercept/posix/CMakeLists.txt @@ -0,0 +1,13 @@ +# src/posix/CMakeLists.txt + +file(GLOB POSIX_SOURCES *.cpp) +file(GLOB POSIX_HEADERS *.h) + +add_library(intercept_posix_interface_client ${POSIX_SOURCES}) +target_include_directories(intercept_posix_interface_client PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +) +target_link_libraries(intercept_posix_interface_client PUBLIC + intercept_registry_client +) +target_compile_options(intercept_posix_interface_client PUBLIC -DCLIENT_BUILD -fPIC) diff --git a/intercept/posix/libsyscall_intercept_hook_point.h b/intercept/posix/libsyscall_intercept_hook_point.h new file mode 100644 index 0000000..2fe7d57 --- /dev/null +++ b/intercept/posix/libsyscall_intercept_hook_point.h @@ -0,0 +1,102 @@ +/* + * Copyright 2016-2017, Intel Corporation + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef LIBSYSCALL_INTERCEPT_HOOK_POINT_H +#define LIBSYSCALL_INTERCEPT_HOOK_POINT_H + +/* + * The inteface for using the intercepting library. + * This callback function should be implemented by + * the code using the library. + * + * The syscall_number, and the six args describe the syscall + * currently being intercepted. + * A non-zero return value means libsyscall_intercept + * should execute the original syscall, use its result. A zero return value + * means libsyscall_intercept should not execute the syscall, and + * use the integer stored to *result as the result of the syscall + * to be returned in RAX to libc. + */ + +#ifdef __cplusplus +extern "C" { +#endif + +extern int (*intercept_hook_point)(long syscall_number, + long arg0, long arg1, + long arg2, long arg3, + long arg4, long arg5, + long *result); + +extern void (*intercept_hook_point_clone_child)(void); +extern void (*intercept_hook_point_clone_parent)(long pid); + +/* + * syscall_no_intercept - syscall without interception + * + * Call syscall_no_intercept to make syscalls + * from the interceptor library, once glibc is already patched. + * Don't use the syscall function from glibc, that + * would just result in an infinite recursion. + */ +long syscall_no_intercept(long syscall_number, ...); + +/* + * syscall_error_code - examines a return value from + * syscall_no_intercept, and returns an error code if said + * return value indicates an error. + */ +static inline int +syscall_error_code(long result) +{ + if (result < 0 && result >= -0x1000) + return (int)-result; + + return 0; +} + +/* + * The syscall intercepting library checks for the + * INTERCEPT_HOOK_CMDLINE_FILTER environment variable, with which one can + * control in which processes interception should actually happen. + * If the library is loaded in this process, but syscall interception + * is not allowed, the syscall_hook_in_process_allowed function returns zero, + * otherwise, it returns one. The user of the library can use it to notice + * such situations, where the code is loaded, but no syscall will be hooked. + */ +int syscall_hook_in_process_allowed(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/intercept/posix/posix_helper.h b/intercept/posix/posix_helper.h new file mode 100644 index 0000000..0a06226 --- /dev/null +++ b/intercept/posix/posix_helper.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021 NetEase Inc. + * + * 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. + */ + + +/* + * Project: curve + * Created Date: Thur May 27 2021 + * Author: xuchaojie + */ + +#include +#include +#include + +#include +#include + +#include "posix_op.h" +#include "syscall_client.h" + +// 仅用于联编 +int help(int argc, char *argv[]) { + return 0; +} diff --git a/intercept/posix/posix_op.cpp b/intercept/posix/posix_op.cpp new file mode 100644 index 0000000..979b04b --- /dev/null +++ b/intercept/posix/posix_op.cpp @@ -0,0 +1,657 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/common.h" +#include "posix_op.h" +#include "middleware/iceoryx_wrapper.h" +#include "registry/client_server_registry.h" + +using intercept::internal::FileType; +struct PosixInfo { + std::string fileName; + FileType fileType; + uint64_t fd; + intercept::internal::DirStream dirinfo; +}; + +// key : 返回给上游的fd, value: 存储文件信息 +std::unordered_map g_fdtofile(10000); +// 以BEGIN_COUNTER为起始值在map中保存,避免fd从0开始与系统内部fd冲突 +constexpr uint32_t BEGIN_COUNTER = 10000; +std::atomic g_fdCounter(BEGIN_COUNTER); + +std::chrono::steady_clock::duration totalDuration = std::chrono::steady_clock::duration::zero(); // 总耗时 +int readnum = 0; + +unsigned long g_processid = -1; +thread_local std::shared_ptr g_wrapper; +thread_local bool g_initflag = false; +std::mutex global_mutex; + +thread_local struct ThreadCleanup { + ThreadCleanup() { + std::cout << "Thread cleanup object created\n"; + } + + ~ThreadCleanup() { + std::cout << "Thread cleanup object destroyed\n"; + } +} cleanup; + +struct syscall_desc table[1000] = { + {0, 0, {argNone, argNone, argNone, argNone, argNone, argNone}}}; + +#define FUNC_NAME(name) PosixOp##name +#define REGISTER_CALL(sysname, funcname, ...) \ + table[SYS_##sysname] = syscall_desc { \ + #sysname, (FUNC_NAME(funcname)), { __VA_ARGS__, } \ + } + + +// ---------------------------init and unint---------------------------------- + + + +int ThreadInit() { + // std::lock_guard lock(global_mutex); + if (g_initflag == true) { + return 0; + } + std::stringstream ss; + auto myid = std::this_thread::get_id(); + ss << myid; + std::string threadid = ss.str(); + pthread_t tid = pthread_self(); + pid_t pid = getpid(); + if (g_processid == -1) { + // 进程级初始化 + g_processid = (unsigned long)pid; + GlobalInit(); + } + spdlog::warn("thread init, processid: {}, threadid: {}, flag id: {}", + (unsigned long) pid, (unsigned long)tid, g_initflag); + // sleep(10); + + intercept::internal::ServiceMetaInfo info; + info.service = SERVICE_FLAG; + info.instance = INTERCEPT_INSTANCE_FLAG; + intercept::registry::ClientServerRegistry registry(ICEORYX, info); + auto dummyserver = registry.CreateDummyServer(); + std::cout << "wait dummy server for client...." << std::endl; + sleep(5); + + info = dummyserver->GetServiceMetaInfo(); + info.service = SERVICE_FLAG; + info.instance = INTERCEPT_INSTANCE_FLAG; + g_wrapper = registry.CreateClient(info); + g_initflag = true; + return 0; +} +int GlobalInit() { + if (intercept::common::Configure::getInstance().loadConfig(intercept::common::CONFIG_FILE)) { + std::cout << "Config file loaded : " << intercept::common::CONFIG_FILE << std::endl; + } else { + std::cout << "Config file not loaded:" << intercept::common::CONFIG_FILE << std::endl; + return 0; + } + intercept::common::InitLog(); + + constexpr char BASE_APP_NAME[] = "iox-intercept-client"; + std::string appNameWithRandom = BASE_APP_NAME + intercept::common::generateRandomSuffix(); + iox::string appname(iox::TruncateToCapacity, appNameWithRandom.c_str(), appNameWithRandom.length()); + spdlog::info("create app name: {}", appNameWithRandom); + iox::runtime::PoshRuntime::initRuntime(appname); + return 0; +} + +void UnInitPosixClient() { +} + +// 初始化函数 +static __attribute__((constructor)) void Init(void) { + printf("Library loaded: PID %d TID: %lu\n", getpid(), (unsigned long)pthread_self()); + //GlobalInit(); +} + +// 退出函数 +static __attribute__((destructor)) void Clean(void) { + // std::cout << "readnum: " << readnum << " , total time : " << totalDuration.count() << " , average time : " << totalDuration.count() / readnum << std::endl; + pthread_t tid = pthread_self(); + pid_t pid = getpid(); + std::cout << "exit and kill, pid:" << (unsigned long)pid + << " threadid:" << (unsigned long) tid << std::endl; + //kill(getpid(), SIGINT); + //sleep(5); +} + +// ---------------------------posix func---------------------------------------------- + +// 判断字符串是否以指定挂载点开头 +bool StartsWithMountPath(const char *str) { + // 指定路径 + const std::string mountpath = "/testdir"; + //"/home/caiyi/shared_memory_code/iceoryx/iceoryx_examples/intercept/testdir"; + size_t prefixLen = mountpath.length(); + return strncmp(str, mountpath.c_str(), prefixLen) == 0; +} + +std::string GetPath(const char* path) { + return ""; +} + +// 获取相对路径 +std::string GetRelativeFilePath(const std::string& fullPath) { + size_t found = fullPath.find_last_of("/\\"); + return fullPath.substr(found+1); +} + +// 判断路径是否有效 +bool IsValidPath(arg_type type, long arg0, long arg1) { + int fd = -1; + switch (type) { + case argFd: + fd = (int)arg0; + if (fd >= BEGIN_COUNTER && + (g_fdtofile.empty() == false && g_fdtofile.count(fd)) > 0) { + return true; + } else { + return false; + } + case argCstr: + if (StartsWithMountPath(reinterpret_cast(arg0))) { + return true; + } else { + // printf("cstr, not right filepath: %s\n", reinterpret_cast(arg0)); + return false; + } + case argAtfd: + if (StartsWithMountPath(reinterpret_cast(arg1)) || + (g_fdtofile.empty() == false && g_fdtofile.count((int)arg0)) > 0) { + return true; + } else { + // printf("atfd, not right filepath: %s\n", reinterpret_cast(arg1)); + return false; + } + case arg_: + return true; + default: + return false; + } +} + +// 判断系统调用是否需要拦截 +bool ShouldInterceptSyscall(const struct syscall_desc *desc, const long *args) { + return IsValidPath(desc->args[0], args[0], args[1]); +} + +const struct syscall_desc *GetSyscallDesc(long syscallNumber, + const long args[6]) { + //char buffer[1024]; + if (syscallNumber < 0 || + static_cast(syscallNumber) >= + sizeof(table) / sizeof(table[0]) || + table[syscallNumber].name == NULL || + ShouldInterceptSyscall(&table[syscallNumber], args) == false) { + return nullptr; + } + //sprintf(buffer, "right number:%ld, name:%s\n", syscallNumber, table[syscallNumber].name); + //printSyscall(buffer); + return table + syscallNumber; +} + +uint32_t GetNextFileDescriptor() { return g_fdCounter.fetch_add(1); } + +void InitSyscall() { + #ifdef __aarch64__ + //REGISTER_CALL(access, Access, argCstr, argMode); + REGISTER_CALL(faccessat, Faccessat, argAtfd, argCstr, argMode); + //REGISTER_CALL(open, Open, argCstr, argOpenFlags, argMode); + REGISTER_CALL(close, Close, argFd); + REGISTER_CALL(openat, Openat, argAtfd, argCstr, argOpenFlags, argMode); + //REGISTER_CALL(creat, Creat, argCstr, argMode); + REGISTER_CALL(write, Write, argFd); + REGISTER_CALL(read, Read, argFd); + REGISTER_CALL(fsync, Fsync, argFd); + REGISTER_CALL(lseek, Lseek, argFd); + //REGISTER_CALL(stat, Stat, argCstr); + // for fstatat + REGISTER_CALL(newfstatat, Newfstatat, argAtfd, argCstr); + REGISTER_CALL(fstat, Fstat, argFd); + REGISTER_CALL(statx, Statx, argAtfd, argCstr); + //REGISTER_CALL(lstat, Lstat, argCstr); + //REGISTER_CALL(mkdir, MkDir, argCstr, argMode); + REGISTER_CALL(mkdirat, MkDirat, argAtfd, argCstr, argMode); + REGISTER_CALL(getdents64, Getdents64, argFd, argCstr, arg_); + //REGISTER_CALL(unlink, Unlink, argCstr); + REGISTER_CALL(unlinkat, Unlinkat, argAtfd, argCstr, argMode); + //REGISTER_CALL(rmdir, Rmdir, argCstr); + REGISTER_CALL(chdir, Chdir, argCstr); + REGISTER_CALL(utimensat, Utimensat, argAtfd, argCstr); + REGISTER_CALL(statfs, Statfs, argCstr); + REGISTER_CALL(fstatfs, Fstatfs, argFd); + + REGISTER_CALL(truncate, Truncate, argCstr); + REGISTER_CALL(ftruncate, Ftruncate, argFd); + REGISTER_CALL(renameat, Renameat, argAtfd, argCstr); + #else + REGISTER_CALL(access, Access, argCstr, argMode); + REGISTER_CALL(faccessat, Faccessat, argAtfd, argCstr, argMode); + REGISTER_CALL(open, Open, argCstr, argOpenFlags, argMode); + REGISTER_CALL(close, Close, argFd); + REGISTER_CALL(openat, Openat, argAtfd, argCstr, argOpenFlags, argMode); + REGISTER_CALL(creat, Creat, argCstr, argMode); + REGISTER_CALL(write, Write, argFd); + REGISTER_CALL(read, Read, argFd); + REGISTER_CALL(fsync, Fsync, argFd); + REGISTER_CALL(lseek, Lseek, argFd); + REGISTER_CALL(stat, Stat, argCstr); + // for fstatat + REGISTER_CALL(newfstatat, Newfstatat, argAtfd, argCstr); + REGISTER_CALL(fstat, Fstat, argFd); + REGISTER_CALL(lstat, Lstat, argCstr); + REGISTER_CALL(mkdir, MkDir, argCstr, argMode); + REGISTER_CALL(getdents64, Getdents64, argFd, argCstr, arg_); + REGISTER_CALL(unlink, Unlink, argCstr); + REGISTER_CALL(unlinkat, Unlinkat, argAtfd, argCstr, argMode); + REGISTER_CALL(rmdir, Rmdir, argCstr); + REGISTER_CALL(chdir, Chdir, argCstr); + REGISTER_CALL(utimensat, Utimensat, argAtfd, argCstr); + REGISTER_CALL(statfs, Statfs, argCstr); + REGISTER_CALL(fstatfs, Fstatfs, argFd); + + REGISTER_CALL(truncate, Truncate, argCstr); + REGISTER_CALL(ftruncate, Ftruncate, argFd); + REGISTER_CALL(rename, Rename, argCstr, argCstr); + #endif +} + +int PosixOpAccess(const long *args, long *result) { + return 0; +} + +int PosixOpFaccessat(const long *args, long *result) { + return PosixOpAccess(args + 1, result); +} + +int PosixOpOpen(const long *args, long *result) { + ThreadInit(); + const char* path = (const char*)args[0]; + int flags = args[1]; + mode_t mode = args[2]; + + if (flags & O_DIRECTORY) { + intercept::internal::OpendirOpReqRes req(path); + g_wrapper->OnRequest(req); + const auto& openRes = static_cast (req.GetResponse()); + // 向上游返回的fd + *result = openRes.dirStream.fh + BEGIN_COUNTER; + // 记录打开的fd + PosixInfo info; + info.fd = *result; + info.dirinfo = openRes.dirStream; + info.fileType = FileType::DIR; + g_fdtofile[*result] = info; + std::cout << "the opendir result fd is: " << *result << std::endl; + } else { + intercept::internal::OpenOpReqRes req(path, flags, mode); + g_wrapper->OnRequest(req); + const auto& openRes = static_cast (req.GetResponse()); + // 向上游返回的fd + *result = openRes.fd + BEGIN_COUNTER; + // 记录打开的fd + PosixInfo info; + info.fd = *result; + info.fileType = FileType::FILE; + info.fileName = path; + g_fdtofile[*result] = info; + spdlog::info("the open result fd: {}, path: {}", *result, path); + } + return 0; +} + +int PosixOpOpenat(const long *args, long *result) { + return PosixOpOpen(args + 1, result); // args[0] is dir fd, jump +} + +int PosixOpCreat(const long *args, long *result) { + return 0; +} + +int PosixOpRead(const long *args, long *result) { + ThreadInit(); + int fd = args[0] - BEGIN_COUNTER; + char* buf = (char*)args[1]; + int count = args[2]; + const auto& info = g_fdtofile[fd]; + std::string timeinfo = "client read, count: " + std::to_string(count) + " filename: " + info.fileName; + intercept::common::Timer timer(timeinfo); + + intercept::internal::ReadOpReqRes readReq(fd, buf, count); + //intercept::common::Timer timer("client OnRequest"); + g_wrapper->OnRequest(readReq); + + const auto& readRes = static_cast (readReq.GetResponse()); + *result = readRes.length; + spdlog::debug("read fd: {}, length: {}", fd, readRes.length); + return 0; +} + +int PosixOpWrite(const long *args, long *result) { + spdlog::debug("get write request..."); + ThreadInit(); + int fd = args[0] - BEGIN_COUNTER; + char* writebuf = (char*)args[1]; + int count = args[2]; + std::string timeinfo = "client write, count: " + std::to_string(count); + intercept::common::Timer timer(timeinfo); + intercept::internal::WriteOpReqRes writeReq(fd, writebuf, count); + g_wrapper->OnRequest(writeReq); + const auto& writeRes = static_cast (writeReq.GetResponse()); + *result = writeRes.length; + spdlog::debug("write fd: {}, length: {}", fd, writeRes.length); + return 0; +} +int PosixOpFsync(const long *args, long *result) { + ThreadInit(); + int fd = args[0] - BEGIN_COUNTER; + spdlog::info("begin fsync, fd: {}", fd); + intercept::internal::FsyncOpReqRes fsyncReq(fd); + g_wrapper->OnRequest(fsyncReq); + const auto& fsyncRes = static_cast (fsyncReq.GetResponse()); + *result = fsyncRes.ret; + spdlog::info("the fysnc result is: {}", *result); + return 0; +} + +int PosixOpLseek(const long *args, long *result) { + ThreadInit(); + int fd = args[0] - BEGIN_COUNTER; + long offset = args[1]; + int whence = args[2]; + intercept::internal::LseekOpReqRes lseekReq(fd, offset, whence); + g_wrapper->OnRequest(lseekReq); + const auto& lseekRes = static_cast (lseekReq.GetResponse()); + *result = lseekRes.ret; + // std::cout << "the lseek result is: " << *result << " , the offset: "<< offset << std::endl; + spdlog::debug("lseek, fd: {}, offset: {}, whence: {}, result: {}", fd, offset, whence, *result); + return 0; +} + +int PosixOpStat(const long *args, long *result) { + ThreadInit(); + spdlog::debug("it is opstat..."); + const char* filename = (const char*) args[0]; + struct stat* statbuf = (struct stat*) args[1]; + intercept::internal::StatOpReqRes statReq(filename, statbuf); + g_wrapper->OnRequest(statReq); + const auto& statRes = static_cast (statReq.GetResponse()); + // 向上游返回的fd + *result = statRes.ret; + spdlog::debug("the stat result fd: {}", *result); + return 0; +} +int PosixOpNewfstatat(const long *args, long *result) { + std::cout << "newfstatat" << std::endl; + // TODO: 以args[0]为起点,找到args[1]路径 + int ret = 0; + if (strlen((char*)args[1]) == 0) { + // 空目录 + long newargs[4]; + newargs[0] = args[0]; + newargs[1] = args[2]; + return PosixOpFstat(newargs, result); + } + return PosixOpStat(args + 1, result); +} + +int PosixOpLstat(const long *args, long *result) { + std::cout << "call PosixOpLstat" << std::endl; + return PosixOpStat(args, result); +} + +int PosixOpFstat(const long *args, long *result) { + ThreadInit(); + spdlog::debug("it is opfstat..."); + int fd = args[0] - BEGIN_COUNTER; + struct stat* statbuf = (struct stat*) args[1]; + intercept::internal::FstatOpReqRes statReq(fd, statbuf); + g_wrapper->OnRequest(statReq); + const auto& statRes = static_cast (statReq.GetResponse()); + // 向上游返回的fd + *result = statRes.ret; + spdlog::debug("the fstat result fd: {}, the stat ino: {}, size: {}", + fd, statbuf->st_ino, statbuf->st_size); + return 0; +} + +int PosixOpFstat64(const long *args, long *result) { + std::cout << "it is opfstat64" << std::endl; + return 0; +} + +int PosixOpStatx(const long *args, long *result) { + ThreadInit(); + std::cout << "it is opstatx" << std::endl; + const char* filename = (const char*) args[1]; + struct statx* fileStat = (struct statx*) args[4]; + struct stat tmpStat; + intercept::internal::StatOpReqRes statReq(filename, &tmpStat); + g_wrapper->OnRequest(statReq); + const auto& statRes = static_cast (statReq.GetResponse()); + if (statRes.ret != 0 ) { + std::cout << "get stat failed.." << std::endl; + } + + *result = statRes.ret; + // inode number + fileStat->stx_ino = (uint64_t)tmpStat.st_ino; + + // total size, in bytes + fileStat->stx_size = (uint64_t)tmpStat.st_size; + + // protection + fileStat->stx_mode = (uint32_t)tmpStat.st_mode; + + // number of hard links + fileStat->stx_nlink = (uint32_t)tmpStat.st_nlink; + + // user ID of owner + fileStat->stx_uid = (uint32_t)tmpStat.st_uid; + + // group ID of owner + fileStat->stx_gid = (uint32_t)tmpStat.st_gid; + + // last access time + fileStat->stx_atime.tv_sec = tmpStat.st_atim.tv_sec; + fileStat->stx_atime.tv_nsec = tmpStat.st_atim.tv_nsec; + + // last modification time + fileStat->stx_mtime.tv_sec = tmpStat.st_mtim.tv_sec; + fileStat->stx_mtime.tv_nsec = tmpStat.st_mtim.tv_nsec; + + // last status change time + fileStat->stx_ctime.tv_sec = tmpStat.st_ctim.tv_sec; + fileStat->stx_ctime.tv_nsec = tmpStat.st_ctim.tv_nsec; + + // 示意性地为stx_attributes设置一个默认值,实际上这需要更具体的场景考虑 + fileStat->stx_attributes = 0; // 假设没有额外的属性 + + // stx_attributes_mask通常和stx_attributes一起使用,表示希望查询或设置哪些属性 + fileStat->stx_attributes_mask = 0; // 示意性地设置,可能需要根据场景具体调整 + return 0; +} + +int PosixOpClose(const long *args, long *result) { + if (g_fdtofile.find((int)args[0]) == g_fdtofile.end()) { + std::cout << "fd not found: " << args[0] << std::endl; + } + const auto& info = g_fdtofile[(int)args[0]]; + if (info.fileType == FileType::FILE) { + int fd = args[0] - BEGIN_COUNTER; + intercept::internal::CloseOpReqRes req(fd); + spdlog::info("begin close, fd: {}", fd); + g_wrapper->OnRequest(req); + const auto& closeRes = static_cast (req.GetResponse()); + // 向上游返回的fd + *result = closeRes.ret; + spdlog::info("the close result, fd: {}", fd); + } else if (info.fileType == FileType::DIR) { + int fd = args[0] - BEGIN_COUNTER; + intercept::internal::ClosedirOpReqRes req(info.dirinfo); + g_wrapper->OnRequest(req); + const auto& closeRes = static_cast (req.GetResponse()); + // 向上游返回的fd + *result = closeRes.ret; + std::cout << "the closedir result fd is: " << fd << std::endl; + } else { + std::cout << "unknown file type for close" << std::endl; + } + g_fdtofile.erase((int)args[0]); + return 0; +} + +int PosixOpMkDir(const long *args, long *result) { + ThreadInit(); + const char* path = (const char*) args[0]; + mode_t mode = args[1]; + intercept::internal::MkdirOpReqRes req(path, mode); + g_wrapper->OnRequest(req); + const auto& mkdirRes = static_cast (req.GetResponse()); + // 向上游返回的fd + *result = mkdirRes.ret; + std::cout << "the mkdir result fd is: " << *result << std::endl; + return 0; +} + +int PosixOpMkDirat(const long *args, long *result) { + // 直接按照绝对路径处理 + return PosixOpMkDir(args + 1, result); +} + +int PosixOpOpenDir(const long *args, long *result) { + std::cout << "open dir....." << std::endl; + return 0; +} + +int PosixOpGetdents64(const long *args, long *result) { + int fd = args[0] - BEGIN_COUNTER; + char* data = (char*)args[1]; + size_t maxread = args[2]; + if (g_fdtofile.find(args[0]) == g_fdtofile.end()) { + std::cout << "fd not found" << std::endl; + *result = 0; + return 0; + } + std::cout << "getdents request, fd: " << fd << " maxread: " << maxread << std::endl; + PosixInfo& posixinfo = g_fdtofile[args[0]]; + intercept::internal::GetdentsOpReqRes req(posixinfo.dirinfo, data, maxread); + g_wrapper->OnRequest(req); + const auto& getdentsRes = static_cast (req.GetResponse()); + posixinfo.dirinfo.offset = getdentsRes.dirinfo.offset; + *result = getdentsRes.realbytes; + std::cout << "the getdents result bytes:" << getdentsRes.realbytes << ", offset is: " << getdentsRes.dirinfo.offset << std::endl; + return 0; +} + +int PosixOpRmdir(const long *args, long *result) { + std::cout << "rmdir, call thePosixOpUnlink " << std::endl; + PosixOpUnlink(args, result); + return 0; +} + +int PosixOpChdir(const long *args, long *result) { + return 0; +} + +int PosixOpUnlink(const long *args, long *result) { + const char* path = (const char*) args[0]; + intercept::internal::UnlinkOpReqRes req(path); + g_wrapper->OnRequest(req); + const auto& unlinkRes = static_cast (req.GetResponse()); + // 向上游返回的fd + *result = unlinkRes.ret; + std::cout << "the unlink path: " << path << " ,result fd is: " << *result << std::endl; + return 0; +} + +int PosixOpUnlinkat(const long *args, long *result) { + const char *filename = (const char *)args[1]; + int flags = args[2]; + if (flags & AT_REMOVEDIR) { + // 删除目录 + std::cout << "unlinkat remove dir..." << std::endl; + PosixOpRmdir(args + 1, result); + return 0; + } + int ret = 0; + // 暂不支持从指定位置开始删除 + ret = PosixOpUnlink(args + 1, result); + std::cout << "unlinkat... ret: " << ret << std::endl; + return ret; +} + +int PosixOpUtimensat(const long* args, long *result) { + int dirfd = args[0]; + return 0; +} + +int PosixOpExitgroup(const long* args, long *result) { + return 0; +} + +int PosixOpStatfs(const long* args, long *result) { + return 0; +} + +int PosixOpFstatfs(const long* args, long *result) { + return 0; +} + +int PosixOpTruncate(const long* args, long *result) { + const char* path = (const char*) args[0]; + off_t length = args[1]; + intercept::internal::TruncateOpReqRes req(path, length); + g_wrapper->OnRequest(req); + const auto& truncateRes = static_cast (req.GetResponse()); + // 向上游返回的fd + *result = truncateRes.ret; + std::cout << "the truncate path: " << path << " ,result fd is: " << *result << std::endl; + return 0; +} + +int PosixOpFtruncate(const long* args, long *result) { + return 0; +} + +int PosixOpRename(const long *args, long *result) { + return 0; +} + +int PosixOpRenameat(const long *args, long *result) { + // 假设都从根目录开始 + const char *oldpath = (const char *)args[1]; + const char* newpath = (const char*)args[3]; + intercept::internal::RenameOpReqRes req(oldpath, newpath); + g_wrapper->OnRequest(req); + const auto& renameRes = static_cast (req.GetResponse()); + // 向上游返回的fd + *result = renameRes.ret; + std::cout << "the rename path: " << oldpath << " ,result fd is: " << *result << std::endl; + return 0; +} + + diff --git a/intercept/posix/posix_op.h b/intercept/posix/posix_op.h new file mode 100644 index 0000000..cba3267 --- /dev/null +++ b/intercept/posix/posix_op.h @@ -0,0 +1,493 @@ + +#ifndef CURVEFS_SRC_CLIENT_CURVE_POSIX_OP_H_ +#define CURVEFS_SRC_CLIENT_CURVE_POSIX_OP_H_ + +#include +#include + +// #include "curvefs/src/client/filesystem/meta.h" +// using ::curvefs::client::filesystem::PosixFile; + +// extern std::unordered_map g_fdtofile; + +typedef int (*syscallFunction_t)(const long *args, long *result); + +enum arg_type { + argNone, + argFd, + argAtfd, + argCstr, + argOpenFlags, + argMode, + arg_ /* no special formatting implemented yet, print as hex number */ +}; + +struct syscall_desc { + const char *name; + syscallFunction_t syscallFunction; + enum arg_type args[6]; +}; + +extern struct syscall_desc table[1000]; + + +bool ShouldInterceptSyscall(const struct syscall_desc* desc, const long* args); + +void InitSyscall(); + +const struct syscall_desc* GetSyscallDesc(long syscallNumber, const long args[6]); + +bool StartsWithMountPath(const char* str); + +int GlobalInit(); + +void UnInitPosixClient(); + +#ifdef __cplusplus +extern "C" { +#endif + + +/** + * The access() function is used to check the permissions of a file or directory. + * + * @param args[0] const char* path The path name of the file or directory to be checked. + * @param args[1] int: mode The mode specifies the desired permissions to be verified, and can be a combination of the following constants using bitwise OR: + * - R_OK: Check if the file or directory is readable. + * - W_OK: Check if the file or directory is writable. + * - X_OK: Check if the file or directory is executable. + * - F_OK: Check if the file or directory exists. + * @return If the file or directory has the specified permissions (or exists), it returns 0. Otherwise, it returns -1 (with an errno error code set). + */ +int PosixOpAccess(const long *args, long *result); + +/** + * The faccessat() function is used to check the permissions of a file or directory relative to a specified directory file descriptor. + * + * @param args[0] int: dirfd The file descriptor of the directory from which the path is relative. + * @param args[1] const char* pathname The relative path name of the file or directory to be checked. + * @param args[2] int The mode specifies the desired permissions to be verified, and can be a combination of the following constants using bitwise OR: + * - R_OK: Check if the file or directory is readable. + * - W_OK: Check if the file or directory is writable. + * - X_OK: Check if the file or directory is executable. + * - F_OK: Check if the file or directory exists. + * @param args[3] int Flags for controlling how the function operates, such as AT_SYMLINK_NOFOLLOW to not follow symbolic links. + * @return If the file or directory has the specified permissions (or exists), it returns 0. Otherwise, it returns -1 (with an errno error code set). + */ +int PosixOpFaccessat(const long *args, long *result); + +/** + * Open a file + * + * Opens the file specified by 'path' with the given 'flags'. + * The 'flags' parameter provides information about the access mode + * (read, write, read-write) and other options for opening the file. + * + * args[0]: path The path of the file to be opened + * args[1]: flags The flags controlling the file open operation + * args[2]: mode The mode for accessing file, only be used for creating new file + * result: The file descriptor on success, or -1 on failure with errno set + */ +int PosixOpOpen(const long *args, long *result); + +int PosixOpOpenat(const long *args, long *result); + +/** + * Creates a new file or truncates an existing file. + * + * args[0] pathname The path to the file to be created. + * args[1] mode The permissions to be set for the newly created file. + * + * result: On success, the file descriptor for the newly created file is returned. + * On error, -1 is returned, and errno is set appropriately. + */ +int PosixOpCreat(const long *args, long *result); + + +/** + * Read data from a file + * + * Reads up to 'count' bytes from the file associated with the file + * descriptor 'fd' into the buffer pointed to by 'buf', + * The actual number of bytes read is returned. + * + * args[0]: int fd: The file descriptor of the file to read from + * args[1]: void* buf: The buffer to store the read data + * args[2]: size_t count: The maximum number of bytes to read + * result: The number of bytes read on success, or -1 on failure with errno set + */ +int PosixOpRead(const long *args, long *result); + + +/** + * Read data from a file + * + * Reads up to 'count' bytes from the file associated with the file + * descriptor 'fd' into the buffer pointed to by 'buf', starting at + * the specified 'offset'. The actual number of bytes read is returned. + * + * args[0] int fd: The file descriptor of the file to read from + * args[1] void* buf: The buffer to store the read data + * args[2] size_t count: The maximum number of bytes to read + * args[3] off_t offset: The offset within the file to start reading from + * result: The number of bytes read on success, or -1 on failure with errno set + */ +int PosixOpPread(const long *args, long *result); + + +/** + * Write data to a file + * + * Writes up to 'count' bytes from the buffer pointed to by 'buf' + * to the file associated with the file descriptor 'fd'. + * The actual number of bytes written is returned. + * + * args[0] int fd: The file descriptor of the file to write to + * args[1] const void* buf: The buffer containing the data to be written + * args[2] size_t count: The number of bytes to write + * result: The number of bytes written on success, or -1 on failure with errno set + */ +int PosixOpWrite(const long *args, long *result); + +/** + * Write data to a file + * + * Writes up to 'count' bytes from the buffer pointed to by 'buf' + * to the file associated with the file descriptor 'fd', starting at + * the specified 'offset'. The actual number of bytes written is returned. + * + * args[0] int fd: The file descriptor of the file to write to + * args[1] const void* buf: The buffer containing the data to be written + * args[2] size_t count: The number of bytes to write + * args[3] off_t offset: The offset within the file to start writing to + * result: The number of bytes written on success, or -1 on failure with errno set + */ +int PosixOpPwrite(const long *args, long *result); + + +/** + * Sets the current read/write position of a file descriptor. + * + * args[0] int fd: The file descriptor representing the file. + * args[1] off_t offset: The offset relative to the 'whence' position. + * args[2] int whence: The reference position for calculating the offset: + * - SEEK_SET: Calculates from the beginning of the file. + * - SEEK_CUR: Calculates from the current position. + * - SEEK_END: Calculates from the end of the file. + * + * result The new offset of the file, or -1 if an error occurs. + */ +int PosixOpLseek(const long *args, long *result); + +/** + * Close a file + * + * args[0] int fd: The file descriptor of the file to close + * result: 0 on success, or -1 on failure with errno set + */ +int PosixOpClose(const long *args, long *result); + +/** + * Create a directory. + * + * args[0] const char* name: Name of the directory to create + * args[1] mode_t mode: Mode with which to create the new directory + * result: 0 on success, -1 on failure + */ +int PosixOpMkDir(const long *args, long *result); + +/** + * mkdirat - create a new directory relative to a directory file descriptor + * @dirfd: the file descriptor of the base directory + * @pathname: the pathname of the new directory to be created + * @mode: the permissions to be set for the new directory + * + * Returns: 0 on success, or -1 on failure + */ +int PosixOpMkDirat(const long *args, long *result); + +/** + * Open a directory + * + * @args[0] const char* name: dirname The path to the directory you want to open. + * + * @result: If successful, returns a pointer to a DIR structure that can be + * used for subsequent directory operations. If there's an error, + * it returns NULL, and you can use the errno variable to check the + * specific error. + */ +int PosixOpOpenDir(const long *args, long *result); + +/** + * Read directory entries from a directory file descriptor. + * + * @args[0]: fd File descriptor of the directory to read. + * @args[1]: dirp Pointer to a buffer where the directory entries will be stored. + * @args[2]: count The size of the buffer `dirp` in bytes. + * + * @result: realbytes, On success, returns the number of bytes read into the buffer `dirp`. + * On error, returns -1 and sets the appropriate errno. + */ +//ssize_t PosixOpGetdents64(int fd, struct linux_dirent64 *dirp, size_t count); +int PosixOpGetdents64(const long *args, long *result); + +/** + * Deletes a directory, which must be empty. + * + * + * args[0] const char* name: Name of the directory to remove + * result: 0 on success, -1 on failure + */ +int PosixOpRmdir(const long *args, long *result); + +/** + A function to change the current working directory of the calling process. + @param args - A pointer to a null-terminated string specifying the path to the new working directory + @param result - A pointer to an integer where the result of the operation will be stored. + On successful completion, 0 will be returned. + In case of failure, a non-zero value is returned. + @return - On successful completion, the function should return 0. + If the function encounters an error, it will return -1 and set errno accordingly. +*/ +int PosixOpChdir(const long *args, long *result); + +/** + * Rename a file + * + * Renames the file specified by 'oldpath' to 'newpath'. + * If 'newpath' already exists, it should be replaced atomically. + * If the target's inode's lookup count is non-zero, the file system + * is expected to postpone any removal of the inode until the lookup + * count reaches zero.s + * + * args[0] const char* oldpath: The path of the file to be renamed + * args[1] const char* newpath: The new path of the file + * result: 0 on success, or -1 on failure with errno set + */ +int PosixOpRename(const long *args, long *result); + +/* + * Renameat renames a file, moving it between directories if required. + * + * args[0] int olddirfd: The file descriptor of the directory containing the file to be renamed + * args[1] const char* oldpath: The path of the file to be renamed + * args[2] int newdirfd: The file descriptor of the directory containing the new path of the file + * args[3] const char* newpath: The new path of the file + * result: 0 on success, or -1 on failure with errno set + * +*/ +int PosixOpRenameat(const long *args, long *result); + + +/** + * Get pathname attributes. + * + * args[0] const char* pathname: The path name + * args[1] struct stat* attr: Pointer to struct stat to store the file attributes + + * result: 0 on success, -1 on failure + */ +int PosixOpStat(const long *args, long *result); + +/** + * Get file attributes. + * + * args[0] int fd: file descriptor + * args[1] struct stat* attr: Pointer to struct stat to store the file attributes + + * result: 0 on success, -1 on failure + */ +int PosixOpFstat(const long *args, long *result); + +/** + * Get file status relative to a directory file descriptor + * args[0] int dirfd + * args[1] pathname + * args[2] struct stat* buf + * args[3] flags :can either be 0, or include one or more of the following flags ORed: + * AT_EMPTY_PATH AT_NO_AUTOMOUNT AT_SYMLINK_NOFOLLOW +*/ +int PosixOpNewfstatat(const long *args, long *result); + +/** + * Get file status information for a symbolic link or file. + * + * args[0] const char* pathname The path to the symbolic link or file. + * args[1] struct stat* statbuf A pointer to a struct stat object where the file status + * information will be stored. + * + * result: On success, 0 is returned. On error, -1 is returned, and errno is + * set appropriately. If the symbolic link is encountered and the + * 'pathname' argument refers to a symbolic link, then the 'statbuf' + * parameter will contain information about the link itself rather + * than the file it refers to. + */ +int PosixOpLstat(const long *args, long *result); + +/* + Obtain file status information. + + Parameters: + - args[0] dirfd: A file descriptor referring to the directory in which the file resides. + Use AT_FDCWD to refer to the current working directory. + - args[1] pathname: The path to the file whose status information is to be retrieved. + - args[2] flags: Flags controlling the behavior of the call. + - args[3] mask: Mask specifying which fields in the returned 'statx' structure should be populated. + - args[4] statxbuf: Pointer to the 'statx' structure where the retrieved status information is stored. + + Return Value: + - On success, returns 0. The 'statxbuf' structure contains the requested file status information. + - On failure, returns -1 and sets errno to indicate the error. +*/ +int PosixOpStatx(const long *args, long *result); + +/** + * Creates a symbolic link. + * + * args[0] const char* target: The target file or directory that the symbolic link should point to. + * args[1] const cahr* linkpath: The path and name of the symbolic link to be created. + * + * result: On success, 0 is returned. On error, -1 is returned, and errno is + * set appropriately. + */ +int PosixOpSymlink(const long *args, long *result); + + +/** + * Create a hard link + * + * Creates a hard link between the file specified by 'oldpath' + * and the 'newpath'. + * + * args[0] const char* oldpath: The path of the existing file + * args[1] const char* newpath: The path of the new link to be created + * result: 0 on success, or -1 on failure with errno set + */ +void PosixOpLink(const long *args, long *result); + +/** + * Deletes a file by removing its directory entry. + * + * args[0] const char* pathname: The path to the file to be deleted. + * + * result: On success, 0 is returned. On error, -1 is returned, and errno is + * set appropriately. + */ +int PosixOpUnlink(const long *args, long *result); + +/* + * Deletes a specified file or directory at a given path + * + * args[0] dirfd: A file descriptor representing the directory in which to perform the unlinkat operation. + * Typically, you can use AT_FDCWD to indicate the current working directory. + * This parameter specifies the base directory for the operation. + * args[1] pathname: The path to the file to be removed. It can be either a relative or absolute path, + * depending on the setting of dirfd. + * args[2] flags: An integer value used to control the behavior of the unlinkat operation. + * You can use flags to influence the operation. Common flags include 0 (default behavior) + * and AT_REMOVEDIR (to remove a directory instead of a file). + + * result: On success, returns 0, indicating the successful removal of the file or directory. + * On failure, returns -1 and sets the global variable errno to indicate the type of error. + */ +int PosixOpUnlinkat(const long *args, long *result); + + +/** + * Synchronize the file data and metadata to disk. + * + * arg[0] int fd The file descriptor associated with the file. + * + * result: On success, the function should return 0. On error, it should + * return a negative value, + */ +int PosixOpFsync(const long* args, long *result); + +/* + * int utimensat(int dirfd, const char *pathname, const struct timespec *times, int flags); + * + * args[0] dirfd:The file descriptor of the directory containing the file or directory to be modified. + * If dirfd is AT_FDCWD, then the current working directory is used. + * + * args[1] pathname: The path to the file or directory to be modified. + * + * args[2] times: A pointer to a structure containing the new access and modification times for the file or directory. + * If times is NULL, then the current time is used for both times. + * + * args[3] flags: A bitwise OR of flags that modify the behavior of the call. + * See the `man utimensat` page for a list of supported flags. + * + * result: 0 on success; -1 on error, with errno set to the error number. + */ +int PosixOpUtimensat(const long* args, long *result); + + +/** + * Terminate all threads in a process and exit. + * + * This system call terminates all threads in the calling process and + * causes the process to exit. The exit status of the process is + * specified by the parameter "status". + * + * args[0] status The exit status of the process. + */ +int PosixOpExitgroup(const long* args, long *result); + + +/** + * statfs() - Get filesystem statistics + * + * @param args[0] path The path to the filesystem to query. + * @param args[1] buf A pointer to a statfs structure to store the results. + * + * @return 0 on success, or a negative error code on failure. + * + */ +int PosixOpStatfs(const long* args, long *result); + +/** + * fstatfs() - Get filesystem statistics for a file descriptor + * + * @param args[0] fd The file descriptor of the filesystem to query. + * @param args[1] buf A pointer to a statfs structure to store the results. + * + * @return 0 on success, or a negative error code on failure. + */ +int PosixOpFstatfs(const long* args, long *result); + +/** + * @brief Truncate a file to the specified length. + * + * This function truncates the file specified by the given path to the specified + * length. If the file is larger than the specified length, it is truncated to + * the specified size; if it is smaller, it is extended and filled with zeros. + * + * @param args[0] path: The path to the file to be truncated. + * @param args[1] length:The desired length to which the file should be truncated. + * + * @return On success, returns 0. On failure, returns -1, and sets errno to indicate + * the error type. + */ +int PosixOpTruncate(const long* args, long *result); + +/** + * @brief Truncate a file opened with the specified file descriptor to the specified length. + * + * This function truncates the file associated with the given file descriptor to the + * specified length. If the file is larger than the specified length, it is truncated; + * if it is smaller, it is extended and filled with zeros. + * + * @param args[0] :fd The file descriptor of the file to be truncated. + * @param args[1]: length The desired length to which the file should be truncated. + * + * @return On success, returns 0. On failure, returns -1, and sets errno to indicate + * the error type. + */ +int PosixOpFtruncate(const long* args, long *result); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // CURVEFS_SRC_CLIENT_CURVE_POSIX_OP_H_ + diff --git a/intercept/posix/syscall_client.h b/intercept/posix/syscall_client.h new file mode 100644 index 0000000..3cd5cf8 --- /dev/null +++ b/intercept/posix/syscall_client.h @@ -0,0 +1,44 @@ +#ifndef CURVEFS_SRC_CLIENT_SYSCALL_CLIENT_ +#define CURVEFS_SRC_CLIENT_SYSCALL_CLIENT_ + +#include +#include +#include +#include + +#include "posix/libsyscall_intercept_hook_point.h" +//#include "syscall_interception.h" +#include "posix/posix_op.h" +#include +#include +#include + +// 拦截函数 + +static int hook(long syscallNumber, + long arg0, long arg1, + long arg2, long arg3, + long arg4, long arg5, + long* result) { + + long args[6] = {arg0, arg1, arg2, arg3, arg4, arg5}; + const struct syscall_desc* desc = GetSyscallDesc(syscallNumber, args); + if (desc != nullptr) { + int ret = desc->syscallFunction(args, result); + //return 0; // 接管 + return ret; + } + + return 1; // 如果不需要拦截,返回1 +} + +// 初始化函数 +static __attribute__((constructor)) void start(void) { + InitSyscall(); + intercept_hook_point = &hook; +} + +#endif + + + diff --git a/intercept/registry/CMakeLists.txt b/intercept/registry/CMakeLists.txt new file mode 100644 index 0000000..34d412f --- /dev/null +++ b/intercept/registry/CMakeLists.txt @@ -0,0 +1,40 @@ +# src/registry/CMakeLists.txt + +find_library(ICEORYX_POSH_LIB NAMES iceoryx_posh PATHS ../../thirdparties/iceoryx/lib) +find_library(ICEORYX_HOOFS_LIB iceoryx_hoofs PATHS ../thirdparties/iceoryx/lib) +find_library(ICEORYX_PLATFORM_LIB iceoryx_platform PATHS ../thirdparties/iceoryx/lib) + +file(GLOB REGISTRY_SOURCES *.cpp) +file(GLOB REGISTRY_HEADERS *.h) + +add_library(intercept_registry ${REGISTRY_SOURCES}) +target_include_directories(intercept_registry PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../../thirdparties/iceoryx/include +) +target_link_libraries(intercept_registry PUBLIC + intercept_middleware + intercept_discovery + ${ICEORYX_HOOFS_LIB} + ${ICEORYX_PLATFORM_LIB} + ${ICEORYX_POSH_LIB} +) + + +file(GLOB CLIENT_REGISTRY_SOURCES *.cpp) +file(GLOB CLIENT_REGISTRY_HEADERS *.h) + +add_library(intercept_registry_client ${CLIENT_REGISTRY_SOURCES}) +target_include_directories(intercept_registry_client PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../../thirdparties/iceoryx/include +) +target_link_libraries(intercept_registry_client PUBLIC + intercept_middleware_client + intercept_discovery_client + ${ICEORYX_POSH_LIB} + ${ICEORYX_HOOFS_LIB} + ${ICEORYX_PLATFORM_LIB} + -lrt +) +target_compile_options(intercept_registry_client PUBLIC -DCLIENT_BUILD ) \ No newline at end of file diff --git a/intercept/registry/client_server_registry.cpp b/intercept/registry/client_server_registry.cpp new file mode 100644 index 0000000..7a87bf0 --- /dev/null +++ b/intercept/registry/client_server_registry.cpp @@ -0,0 +1,169 @@ +#include +#include +#include + +#include "middleware/iceoryx_wrapper.h" +#include "client_server_registry.h" + +namespace intercept { +namespace registry { + +using intercept::discovery::IceoryxDiscovery; +using intercept::middleware::IceoryxWrapper; +std::string generateRandomString(int length) { + std::string result; + const char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + srand(time(0)); // 初始化随机数生成器 + for (int i = 0; i < length; i++) { + int randomIndex = rand() % strlen(charset); + result += charset[randomIndex]; + } + return result; +} + +ClientServerRegistry::ClientServerRegistry(const std::string& middlewareType, const ServiceMetaInfo& info) { + // 根据middlewareType创建对应的ServiceDiscovery + discovery_ = std::make_shared(); + serviceInfo_ = info; + middlewareType_ = middlewareType; + spdlog::info("ClientServerRegistry init"); +} + +ClientServerRegistry::~ClientServerRegistry() { + spdlog::info("ClientServerRegistry destory"); +} + +// 在用户侧,创建dummpserver + +std::shared_ptr ClientServerRegistry::CreateDummyServer() { + std::string dummpyserver = "dummy_server"; + ServiceMetaInfo info; + info.service = SERVICE_FLAG; + info.instance = DUMMY_INSTANCE_FLAG; + pid_t pid = getpid(); + auto myid = std::this_thread::get_id(); + std::stringstream ss; + ss << myid; + std::string threadid = ss.str(); + info.event = generateRandomString(10) + std::to_string((long)pid) + threadid; + info.serverType = "dummy"; + + spdlog::info("ClientServerRegistry try to create dummy server, the service: {}, instance: {}, event: {}", + info.service, info.instance, info.event); + + std::shared_ptr wrapper; + if (middlewareType_ == ICEORYX) { + wrapper = std::make_shared(info); + wrapper->SetServiceType(intercept::middleware::ServiceType::DUMMYSERVER); + } + wrapper->InitDummyServer(); + spdlog::info("ClientServerRegistry finish creating dummy server, server: {}, instance: {}, event: {}", + info.service, info.instance, info.event); + return wrapper; +} + +void ClientServerRegistry::DestroyDummyServer() { + std::string dummpyserver = "dummy_server"; +} + +std::shared_ptr + ClientServerRegistry::CreateClient(const ServiceMetaInfo& info) { + // 1. 获取客户端创建client的请求 + // 2. 创建对应的client + // 3. 返回对应的client + if (middlewareType_ == ICEORYX) { + spdlog::info("ClientServerRegistry begin creating client, service: {}, instance: {}, event: {}", + info.service, info.instance, info.event); + std::shared_ptr wrapper = std::make_shared(info); + wrapper->SetServiceType(intercept::middleware::ServiceType::CLIENT); + wrapper->InitClient(); + return wrapper; + } + return nullptr; +} + +std::shared_ptr + ClientServerRegistry::CreateServer(const ServiceMetaInfo& info) { + // 1. 获取客户端创建server的请求 + // 2. 创建对应的server + // 3. 返回对应的server + if (middlewareType_ == ICEORYX) { + std::shared_ptr wrapper = std::make_shared(info); + wrapper->SetServiceType(intercept::middleware::ServiceType::SERVER); + // wrapper->InitServer(); + return wrapper; + } + return nullptr; +} + + +// 作用于服务端 +void ClientServerRegistry::CreateServers() { + // 1. 获取客户端创建server的请求 + std::vector results = discovery_->FindServices(serviceInfo_); + std::vector neededServers; + + // 通过dummy请求获取创建server的需求 + for (auto& result : results) { + if (result.instance == DUMMY_INSTANCE_FLAG && + serverMap_.find(result.event) == serverMap_.end()){ + // 根据dummy 创建一个serveiceinfo + ServiceMetaInfo info; + info.service = result.service; + info.instance = INTERCEPT_INSTANCE_FLAG; + info.event = result.event; + neededServers.push_back(info); + + spdlog::info("ClientServerRegistry create server, service: {}, instance: {}, event: {}", + info.service, info.instance, info.event); + } + } + + // 2. 创建对应的server + for (const auto& result : neededServers) { + // 启动一个线程,创建ReqResMiddlewareWrapper 并调用它的StartServer函数 + // 2.1 是否已经创建对应server + // 2.2 如果没有创建, 创建server,并添加到serverMap_中 + // 2.3 如果已经创建,跳过 + if (middlewareType_ == ICEORYX) { + std::thread t([this, result]() { + // 创建server + auto wrapper = std::make_shared(result); + wrapper->SetServiceType(intercept::middleware::ServiceType::SERVER); + this->serverMap_[result.event] = wrapper; + // 启动server + wrapper->InitServer(); + wrapper->StartServer(); + // 添加到serverMap_中 + }); + threads_.push_back(std::move(t)); + } + sleep(0.1); + } + +} + +void ClientServerRegistry::DestroyServers() { + // 1. 获取客户端销毁server的请求 + // 2. 销毁对应的server +} + +void ClientServerRegistry::MonitorServers() { + spdlog::info("ClientServerRegistry monitor servers"); + while (1) { + // create: + CreateServers(); + // destroy: + DestroyServers(); + // TODO: 这个等待很重要 + sleep(1); + } + for (auto& t : threads_) { + t.join(); + } +} + +} // namespace internal +} // namespace intercecpt + + diff --git a/intercept/registry/client_server_registry.h b/intercept/registry/client_server_registry.h new file mode 100644 index 0000000..0cb7cc1 --- /dev/null +++ b/intercept/registry/client_server_registry.h @@ -0,0 +1,78 @@ +#pragma once + +#include "middleware/req_res_middleware_wrapper.h" +#include "discovery/iceoryx_discovery.h" +#include "discovery/discovery.h" + +#define CREATE_FLAG "create" +#define DESTROY_FLAG "destroy" +#define SERVER_FLAG "server" + +namespace intercept { +namespace registry { + +using intercept::middleware::ReqResMiddlewareWrapper; +using intercept::discovery::Discovery; +using intercept::internal::OpenOpReqRes; +using intercept::internal::ServiceMetaInfo; + + +class ClientServerRegistry { + +public: + // ... + ClientServerRegistry(const std::string& middlewareType, const ServiceMetaInfo& info); + ~ClientServerRegistry(); + // 创建临时的server,主要用于通过server创建数据交换的server + std::shared_ptr CreateDummyServer(); + void DestroyDummyServer(); + + // 返回一个已经初始化的middleWrapper_; + std::shared_ptr CreateClient(const ServiceMetaInfo& info); + std::shared_ptr CreateServer(const ServiceMetaInfo& info); + + // 在daemon端更新server + void MonitorServers(); + +private: + // 根据client传递的信息 + void CreateServers(); // 创建服务 + void DestroyServers(); // 销毁服务 + +private: + // ... + std::string middlewareType_; + ServiceMetaInfo serviceInfo_; // 这里一个service由:service instance构成 + std::shared_ptr discovery_; + + std::vector> clientWrapper_; + std::vector> serverWrapper_; + + std::set dummyevent_; + std::unordered_map> serverMap_; + + // 存放创建的线程 + std::vector threads_; + +}; + +/// +// int client() { +// ServiceMetaInfo info = {"Service", "Instance", "Event"}; +// ClientServerRegistry registry("ICE", info); +// registry.CreateDummyServer(); +// auto client = registry.CreateClient(ServiceMetaInfo{"Service", "Instance", "Event"}); +// OpenOpReqRes reqres("test", 1, 1); +// client->OnRequest(reqres); +// // 全局使用这一个client去操作请求 + +// registry.DestroyDummyServer(); +// return 0; +// } + +} +} + + + + diff --git a/intercept/server.cpp b/intercept/server.cpp new file mode 100644 index 0000000..9b46596 --- /dev/null +++ b/intercept/server.cpp @@ -0,0 +1,32 @@ + +#include +#include +#include +#include +#include + +#include "registry/client_server_registry.h" + +using namespace intercept::internal; +using namespace intercept::registry; +std::mutex mtx; +std::condition_variable cv; +std::atomic discovery_thread_running{false}; + +int main() { + constexpr char APP_NAME[] = "iox-intercept-server"; + if (intercept::common::Configure::getInstance().loadConfig(intercept::common::CONFIG_FILE)) { + std::cout << "Config file loaded" << std::endl; + } else { + std::cout << "Config file not loaded: server.conf" << std::endl; + return 0; + } + intercept::common::InitLog(); + iox::runtime::PoshRuntime::initRuntime(APP_NAME); + ServiceMetaInfo info = {SERVICE_FLAG, "", ""}; + std::string type = ICEORYX; + ClientServerRegistry registry(type, info); + spdlog::info("begin to monitor servers"); + registry.MonitorServers(); + return 0; +} \ No newline at end of file diff --git a/local_cache/CMakeLists.txt b/local_cache/CMakeLists.txt new file mode 100644 index 0000000..f3e3e61 --- /dev/null +++ b/local_cache/CMakeLists.txt @@ -0,0 +1,5 @@ +SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin) + +file (GLOB_RECURSE LOCAL_CACHE_SOURCES CONFIGURE_DEPENDS "*.cpp") +add_library(hybridcache_local STATIC ${LOCAL_CACHE_SOURCES}) +target_link_libraries(hybridcache_local PUBLIC ${THIRD_PARTY_LIBRARIES} -laio) diff --git a/local_cache/accessor.h b/local_cache/accessor.h new file mode 100644 index 0000000..2733e37 --- /dev/null +++ b/local_cache/accessor.h @@ -0,0 +1,52 @@ +/* + * Project: HybridCache + * Created Date: 24-3-25 + * Author: lshb + */ +#ifndef HYBRIDCACHE_ACCESSOR_H_ +#define HYBRIDCACHE_ACCESSOR_H_ + +#include "read_cache.h" +#include "write_cache.h" + +namespace HybridCache { + +class HybridCacheAccessor { + public: + HybridCacheAccessor(const HybridCacheConfig& cfg) : cfg_(cfg) {} + ~HybridCacheAccessor() {} + + // Put in write cache. + // If the write cache is full, block waiting for asynchronous flush to release the write cache space + virtual int Put(const std::string &key, size_t start, size_t len, const char* buf) = 0; + + // 1.Read from write cache. 2.Read from read cache. + virtual int Get(const std::string &key, size_t start, size_t len, char* buf) = 0; + + // Get4ReadHandle(); + + // File flush. Need to handle flush/write concurrency. + virtual int Flush(const std::string &key) = 0; + + // Flush to the final data source, such as global cache to s3. + virtual int DeepFlush(const std::string &key) = 0; + + virtual int Delete(const std::string &key) = 0; + + // Invalidated the local read cache. + // Delete read cache when open the file. That is a configuration item. + virtual int Invalidate(const std::string &key) = 0; + + // Background asynchronous flush all files and releases write cache space. + virtual int FsSync() = 0; + + protected: + HybridCacheConfig cfg_; + std::shared_ptr writeCache_; + std::shared_ptr readCache_; + std::shared_ptr dataAdaptor_; +}; + +} // namespace HybridCache + +#endif // HYBRIDCACHE_ACCESSOR_H_ diff --git a/local_cache/common.cpp b/local_cache/common.cpp new file mode 100644 index 0000000..b77a980 --- /dev/null +++ b/local_cache/common.cpp @@ -0,0 +1,18 @@ +#include "common.h" + +namespace HybridCache { + +bool EnableLogging = true; + +void split(const std::string& str, const char delim, + std::vector& items) { + std::istringstream iss(str); + std::string tmp; + while (std::getline(iss, tmp, delim)) { + if (!tmp.empty()) { + items.emplace_back(std::move(tmp)); + } + } +} + +} // namespace HybridCache diff --git a/local_cache/common.h b/local_cache/common.h new file mode 100644 index 0000000..bb33bfb --- /dev/null +++ b/local_cache/common.h @@ -0,0 +1,40 @@ +/* + * Project: HybridCache + * Created Date: 24-2-21 + * Author: lshb + */ +#ifndef HYBRIDCACHE_COMMON_H_ +#define HYBRIDCACHE_COMMON_H_ + +#include +#include +#include +#include + +#include "folly/executors/CPUThreadPoolExecutor.h" + +namespace HybridCache { + +typedef folly::CPUThreadPoolExecutor ThreadPool; + +static const char PAGE_SEPARATOR = 26; + +static const uint32_t BYTE_LEN = 8; + +// ConcurrentSkipList height +static const int SKIP_LIST_HEIGHT = 2; + +extern bool EnableLogging; + +struct ByteBuffer { + char* data; + size_t len; + ByteBuffer(char* buf = nullptr, size_t bufLen = 0) : data(buf), len(bufLen) {} +}; + +void split(const std::string& str, const char delim, + std::vector& items); + +} // namespace HybridCache + +#endif // HYBRIDCACHE_COMMON_H_ diff --git a/local_cache/config.cpp b/local_cache/config.cpp new file mode 100644 index 0000000..db5ce4e --- /dev/null +++ b/local_cache/config.cpp @@ -0,0 +1,187 @@ +#include +#include +#include +#include + +#include "gflags/gflags.h" +#include "glog/logging.h" + +#include "common.h" +#include "config.h" + +namespace HybridCache { + +std::vector SplitString(const std::string &input) { + std::vector result; + std::stringstream ss(input); + std::string item; + while (std::getline(ss, item, ',')) { + result.push_back(item); + } + return result; +} + +bool GetHybridCacheConfig(const std::string& file, HybridCacheConfig& cfg) { + Configuration conf; + if (!conf.LoadConfig(file)) return false; + + // ReadCache + conf.GetValueFatalIfFail("ReadCacheConfig.CacheConfig.CacheName", + cfg.ReadCacheCfg.CacheCfg.CacheName); + conf.GetValueFatalIfFail("ReadCacheConfig.CacheConfig.MaxCacheSize", + cfg.ReadCacheCfg.CacheCfg.MaxCacheSize); + conf.GetValueFatalIfFail("ReadCacheConfig.CacheConfig.PageBodySize", + cfg.ReadCacheCfg.CacheCfg.PageBodySize); + conf.GetValueFatalIfFail("ReadCacheConfig.CacheConfig.PageMetaSize", + cfg.ReadCacheCfg.CacheCfg.PageMetaSize); + conf.GetValueFatalIfFail("ReadCacheConfig.CacheConfig.EnableCAS", + cfg.ReadCacheCfg.CacheCfg.EnableCAS); + conf.GetValueFatalIfFail("ReadCacheConfig.CacheConfig.CacheLibConfig.EnableNvmCache", + cfg.ReadCacheCfg.CacheCfg.CacheLibCfg.EnableNvmCache); + if (cfg.ReadCacheCfg.CacheCfg.CacheLibCfg.EnableNvmCache) { + conf.GetValueFatalIfFail("ReadCacheConfig.CacheConfig.CacheLibConfig.RaidPath", + cfg.ReadCacheCfg.CacheCfg.CacheLibCfg.RaidPath); + conf.GetValueFatalIfFail("ReadCacheConfig.CacheConfig.CacheLibConfig.RaidFileNum", + cfg.ReadCacheCfg.CacheCfg.CacheLibCfg.RaidFileNum); + conf.GetValueFatalIfFail("ReadCacheConfig.CacheConfig.CacheLibConfig.RaidFileSize", + cfg.ReadCacheCfg.CacheCfg.CacheLibCfg.RaidFileSize); + conf.GetValueFatalIfFail("ReadCacheConfig.CacheConfig.CacheLibConfig.DataChecksum", + cfg.ReadCacheCfg.CacheCfg.CacheLibCfg.DataChecksum); + } + conf.GetValueFatalIfFail("ReadCacheConfig.DownloadNormalFlowLimit", + cfg.ReadCacheCfg.DownloadNormalFlowLimit); + conf.GetValueFatalIfFail("ReadCacheConfig.DownloadBurstFlowLimit", + cfg.ReadCacheCfg.DownloadBurstFlowLimit); + + // WriteCache + conf.GetValueFatalIfFail("WriteCacheConfig.CacheConfig.CacheName", + cfg.WriteCacheCfg.CacheCfg.CacheName); + conf.GetValueFatalIfFail("WriteCacheConfig.CacheConfig.MaxCacheSize", + cfg.WriteCacheCfg.CacheCfg.MaxCacheSize); + conf.GetValueFatalIfFail("WriteCacheConfig.CacheConfig.PageBodySize", + cfg.WriteCacheCfg.CacheCfg.PageBodySize); + conf.GetValueFatalIfFail("WriteCacheConfig.CacheConfig.PageMetaSize", + cfg.WriteCacheCfg.CacheCfg.PageMetaSize); + conf.GetValueFatalIfFail("WriteCacheConfig.CacheConfig.EnableCAS", + cfg.WriteCacheCfg.CacheCfg.EnableCAS); + conf.GetValueFatalIfFail("WriteCacheConfig.CacheSafeRatio", + cfg.WriteCacheCfg.CacheSafeRatio); + + // GlobalCache + conf.GetValueFatalIfFail("UseGlobalCache", cfg.UseGlobalCache); + if (cfg.UseGlobalCache) { + conf.GetValueFatalIfFail("GlobalCacheConfig.EnableWriteCache", + cfg.GlobalCacheCfg.EnableWriteCache); + conf.GetValueFatalIfFail("GlobalCacheConfig.EtcdAddress", + cfg.GlobalCacheCfg.EtcdAddress); + std::string servers; + conf.GetValueFatalIfFail("GlobalCacheConfig.GlobalServers", + servers); + cfg.GlobalCacheCfg.GlobalServers = std::move(SplitString(servers)); + conf.GetValueFatalIfFail("GlobalCacheConfig.GflagFile", + cfg.GlobalCacheCfg.GflagFile); + } + + conf.GetValueFatalIfFail("ThreadNum", cfg.ThreadNum); + conf.GetValueFatalIfFail("BackFlushCacheRatio", cfg.BackFlushCacheRatio); + conf.GetValueFatalIfFail("UploadNormalFlowLimit", cfg.UploadNormalFlowLimit); + conf.GetValueFatalIfFail("UploadBurstFlowLimit", cfg.UploadBurstFlowLimit); + conf.GetValueFatalIfFail("LogPath", cfg.LogPath); + conf.GetValueFatalIfFail("LogLevel", cfg.LogLevel); + conf.GetValueFatalIfFail("EnableLog", cfg.EnableLog); + conf.GetValueFatalIfFail("FlushToRead", cfg.FlushToRead); + conf.GetValueFatalIfFail("CleanCacheByOpen", cfg.CleanCacheByOpen); + + conf.PrintConfig(); + return CheckConfig(cfg); +} + +bool CheckConfig(const HybridCacheConfig& cfg) { + if (cfg.WriteCacheCfg.CacheCfg.CacheLibCfg.EnableNvmCache) { + LOG(FATAL) << "Config error. Write Cache not support nvm cache!"; + return false; + } + + if (cfg.ReadCacheCfg.CacheCfg.PageBodySize % BYTE_LEN || + cfg.WriteCacheCfg.CacheCfg.PageBodySize % BYTE_LEN) { + LOG(FATAL) << "Config error. Page body size must be a multiple of " << BYTE_LEN; + return false; + } + + return true; +} + +bool ParseFlagFromFile(const std::string& file) { + std::ifstream config_file(file); + if (config_file.is_open()) { + std::string line; + std::vector args; + args.push_back("hybridcache"); + while (std::getline(config_file, line)) { + args.push_back(line); + } + char* dummy_argv[args.size()]; + for (size_t i = 0; i < args.size(); ++i) { + dummy_argv[i] = const_cast(args[i].c_str()); + } + int size = args.size(); + char** tmp = const_cast(dummy_argv); + google::ParseCommandLineFlags(&size, &tmp, true); + config_file.close(); + } else { + LOG(ERROR) << "Unable to open gflag file '" << file << "' failed: " + << strerror(errno); + return false; + } + return true; +} + +bool Configuration::LoadConfig(const std::string& file) { + confFile_ = file; + std::ifstream cFile(confFile_); + + if (cFile.is_open()) { + std::string line; + while (getline(cFile, line)) { + // FIXME: may not remove middle spaces + line.erase(std::remove_if(line.begin(), line.end(), isspace), + line.end()); + if (line[0] == '#' || line.empty()) + continue; + + int delimiterPos = line.find("="); + std::string key = line.substr(0, delimiterPos); + int commentPos = line.find("#"); + std::string value = line.substr(delimiterPos + 1, + commentPos - delimiterPos - 1); + config_[key] = value; + } + } else { + LOG(ERROR) << "Open config file '" << confFile_ << "' failed: " + << strerror(errno); + return false; + } + + return true; +} + +void Configuration::PrintConfig() { + LOG(INFO) << std::string(30, '=') << "BEGIN" << std::string(30, '='); + for (auto &item : config_) { + LOG(INFO) << item.first << std::string(60 - item.first.size(), ' ') + << ": " << item.second; + } + LOG(INFO) << std::string(31, '=') << "END" << std::string(31, '='); +} + +template +void Configuration::GetValueFatalIfFail(const std::string& key, T& value) { + if (config_.find(key) != config_.end()) { + std::stringstream sstream(config_[key]); + sstream >> value; + return; + } + LOG(FATAL) << "Get " << key << " from " << confFile_ << " fail"; +} + +} // namespace HybridCache diff --git a/local_cache/config.h b/local_cache/config.h new file mode 100644 index 0000000..7026527 --- /dev/null +++ b/local_cache/config.h @@ -0,0 +1,93 @@ +/* + * Project: HybridCache + * Created Date: 24-2-21 + * Author: lshb + */ +#ifndef HYBRIDCACHE_CONFIG_H_ +#define HYBRIDCACHE_CONFIG_H_ + +#include +#include + +namespace HybridCache { + +struct CacheLibConfig { + bool EnableNvmCache = false; + std::string RaidPath; + uint64_t RaidFileNum; + size_t RaidFileSize; + bool DataChecksum = false; +}; + +struct CacheConfig { + std::string CacheName; + size_t MaxCacheSize; + uint32_t PageBodySize; + uint32_t PageMetaSize; + bool EnableCAS; + CacheLibConfig CacheLibCfg; +}; + +struct ReadCacheConfig { + CacheConfig CacheCfg; + uint64_t DownloadNormalFlowLimit; + uint64_t DownloadBurstFlowLimit; +}; + +struct WriteCacheConfig { + CacheConfig CacheCfg; + uint32_t CacheSafeRatio; // cache safety concern threshold (percent) +}; + +struct GlobalCacheConfig { + bool EnableWriteCache; + std::string EtcdAddress; + std::vector GlobalServers; + std::string GflagFile; +}; + +struct HybridCacheConfig { + ReadCacheConfig ReadCacheCfg; + WriteCacheConfig WriteCacheCfg; + GlobalCacheConfig GlobalCacheCfg; + uint32_t ThreadNum; + uint32_t BackFlushCacheRatio; + uint64_t UploadNormalFlowLimit; + uint64_t UploadBurstFlowLimit; + std::string LogPath; + uint32_t LogLevel; + bool EnableLog = true; + bool UseGlobalCache = false; + bool FlushToRead = false; // write to read cache after flush + bool CleanCacheByOpen = false; // clean read cache when open file +}; + +bool GetHybridCacheConfig(const std::string& file, HybridCacheConfig& cfg); +bool CheckConfig(const HybridCacheConfig& cfg); +bool ParseFlagFromFile(const std::string& file); + +class Configuration { + public: + bool LoadConfig(const std::string& file); + void PrintConfig(); + + /* + * @brief GetValueFatalIfFail Get the value of the specified config item + * log it if get error + * + * @param[in] key config name + * @param[out] value config value + * + * @return + */ + template + void GetValueFatalIfFail(const std::string& key, T& value); + + private: + std::string confFile_; + std::map config_; +}; + +} // namespace HybridCache + +#endif // HYBRIDCACHE_CONFIG_H_ diff --git a/local_cache/data_adaptor.h b/local_cache/data_adaptor.h new file mode 100644 index 0000000..0694c15 --- /dev/null +++ b/local_cache/data_adaptor.h @@ -0,0 +1,89 @@ +/* + * Project: HybridCache + * Created Date: 24-2-26 + * Author: lshb + */ +#ifndef HYBRIDCACHE_DATA_ADAPTOR_H_ +#define HYBRIDCACHE_DATA_ADAPTOR_H_ + +#include + +#include "folly/futures/Future.h" +#include "glog/logging.h" + +#include "common.h" +#include "errorcode.h" + +namespace HybridCache { + +class DataAdaptor { + public: + virtual folly::Future DownLoad(const std::string &key, + size_t start, + size_t size, + ByteBuffer &buffer) = 0; + + virtual folly::Future UpLoad(const std::string &key, + size_t size, + const ByteBuffer &buffer, + const std::map& headers) = 0; + + virtual folly::Future Delete(const std::string &key) = 0; + + // for global cache + virtual folly::Future DeepFlush(const std::string &key) { + return folly::makeFuture(0); + } + + virtual folly::Future Head(const std::string &key, + size_t& size, + std::map& headers) = 0; + + void SetExecutor(std::shared_ptr executor) { + executor_ = executor; + } + + protected: + std::shared_ptr executor_; +}; + +class DataAdaptor4Test : public DataAdaptor { + public: + folly::Future DownLoad(const std::string &key, + size_t start, + size_t size, + ByteBuffer &buffer) { + assert(executor_); + return folly::via(executor_.get(), [key, start, size, buffer]() -> int { + LOG(INFO) << "[DataAdaptor]DownLoad start, key:" << key + << ", start:" << start << ", size:" << size; + std::this_thread::sleep_for(std::chrono::seconds(3)); + LOG(INFO) << "[DataAdaptor]DownLoad error, key:" << key + << ", start:" << start << ", size:" << size; + return REMOTE_FILE_NOT_FOUND; + }); + } + + folly::Future UpLoad(const std::string &key, + size_t size, + const ByteBuffer &buffer, + const std::map& headers) { + return folly::makeFuture(REMOTE_FILE_NOT_FOUND); + } + + folly::Future Delete(const std::string &key) { + return folly::makeFuture(REMOTE_FILE_NOT_FOUND); + } + + folly::Future Head(const std::string &key, + size_t& size, + std::map& headers) { + return folly::makeFuture(REMOTE_FILE_NOT_FOUND); + } +}; + +} // namespace HybridCache + +#endif // HYBRIDCACHE_DATA_ADAPTOR_H_ diff --git a/local_cache/errorcode.h b/local_cache/errorcode.h new file mode 100644 index 0000000..addd210 --- /dev/null +++ b/local_cache/errorcode.h @@ -0,0 +1,21 @@ +/* + * Project: HybridCache + * Created Date: 24-3-18 + * Author: lshb + */ +#ifndef HYBRIDCACHE_ERRORCODE_H_ +#define HYBRIDCACHE_ERRORCODE_H_ + +namespace HybridCache { + +enum ErrCode { + SUCCESS = 0, + PAGE_NOT_FOUND = -1, + PAGE_DEL_FAIL = -2, + ADAPTOR_NOT_FOUND = -3, + REMOTE_FILE_NOT_FOUND = -4, +}; + +} // namespace HybridCache + +#endif // HYBRIDCACHE_ERRORCODE_H_ diff --git a/local_cache/page_cache.cpp b/local_cache/page_cache.cpp new file mode 100644 index 0000000..b6a023c --- /dev/null +++ b/local_cache/page_cache.cpp @@ -0,0 +1,440 @@ +#include "glog/logging.h" + +#include "common.h" +#include "errorcode.h" +#include "page_cache.h" + +namespace HybridCache { + +bool PageCache::Lock(char* pageMemory) { + if (!cfg_.EnableCAS) return true; + uint8_t* lock = reinterpret_cast(pageMemory + int(MetaPos::LOCK)); + uint8_t lockExpected = 0; + return __atomic_compare_exchange_n(lock, &lockExpected, 1, true, + __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST); +} + +void PageCache::UnLock(char* pageMemory) { + if (!cfg_.EnableCAS) return; + uint8_t* lock = reinterpret_cast(pageMemory + int(MetaPos::LOCK)); + __atomic_store_n(lock, 0, __ATOMIC_SEQ_CST); +} + +uint8_t PageCache::AddNewVer(char* pageMemory) { + if (!cfg_.EnableCAS) return 0; + uint8_t* newVer = reinterpret_cast(pageMemory + int(MetaPos::NEWVER)); + return __atomic_add_fetch(newVer, 1, __ATOMIC_SEQ_CST); +} + +void PageCache::SetLastVer(char* pageMemory, uint8_t newVer) { + if (!cfg_.EnableCAS) return; + uint8_t* lastVer = reinterpret_cast(pageMemory + int(MetaPos::LASTVER)); + __atomic_store_n(lastVer, newVer, __ATOMIC_SEQ_CST); +} + +uint8_t PageCache::GetLastVer(const char* pageMemory) { + if (!cfg_.EnableCAS) return 0; + const uint8_t* lastVer = reinterpret_cast(pageMemory + int(MetaPos::LASTVER)); + return __atomic_load_n(lastVer, __ATOMIC_SEQ_CST); +} + +uint8_t PageCache::GetNewVer(const char* pageMemory) { + if (!cfg_.EnableCAS) return 0; + const uint8_t* newVer = reinterpret_cast(pageMemory + int(MetaPos::NEWVER)); + return __atomic_load_n(newVer, __ATOMIC_SEQ_CST); +} + +void PageCache::SetFastBitmap(char* pageMemory, bool valid) { + uint8_t* fastBitmap = reinterpret_cast(pageMemory + int(MetaPos::FAST_BITMAP)); + if (valid) *fastBitmap = 1; + else *fastBitmap = 0; +} + +bool PageCache::GetFastBitmap(const char* pageMemory) { + const uint8_t* fastBitmap = reinterpret_cast(pageMemory + int(MetaPos::FAST_BITMAP)); + return *fastBitmap == 1; +} + +void PageCache::SetBitMap(char* pageMemory, int pos, int len, bool valid) { + if (len == cfg_.PageBodySize && valid) + SetFastBitmap(pageMemory, valid); + if (!valid) + SetFastBitmap(pageMemory, valid); + + char* x = pageMemory + cfg_.PageMetaSize; + uint32_t startByte = pos / BYTE_LEN; + // head byte + if (pos % BYTE_LEN > 0) { + int headByteSetLen = BYTE_LEN - pos % BYTE_LEN; + headByteSetLen = headByteSetLen > len ? len : headByteSetLen; + len -= headByteSetLen; + while (headByteSetLen) { + if (valid) + SetBit(x+startByte, pos%BYTE_LEN+(--headByteSetLen)); + else + ClearBit(x+startByte, pos%BYTE_LEN+(--headByteSetLen)); + } + ++startByte; + } + // mid bytes + int midLen = len / BYTE_LEN; + if (midLen > 0) { + if (valid) + memset(x+startByte, UINT8_MAX, midLen); + else + memset(x+startByte, 0, midLen); + len -= BYTE_LEN * midLen; + startByte += midLen; + } + // tail byte + while (len > 0) { + if (valid) + SetBit(x+startByte, --len); + else + ClearBit(x+startByte, --len); + } +} + +int PageCacheImpl::Init() { + const uint64_t REDUNDANT_SIZE = 1024 * 1024 * 1024; + const unsigned bucketsPower = 25; + const unsigned locksPower = 15; + + Cache::Config config; + config + .setCacheSize(cfg_.MaxCacheSize + REDUNDANT_SIZE) + .setCacheName(cfg_.CacheName) + .setAccessConfig({bucketsPower, locksPower}) + .validate(); + if (cfg_.CacheLibCfg.EnableNvmCache) { + Cache::NvmCacheConfig nvmConfig; + std::vector raidPaths; + for (int i=0; i(config); + pool_ = cache_->addPool(cfg_.CacheName + "_pool", cfg_.MaxCacheSize); + + LOG(WARNING) << "[PageCache]Init, name:" << config.getCacheName() + << ", size:" << config.getCacheSize() + << ", dir:" << config.getCacheDir(); + return SUCCESS; +} + +int PageCacheImpl::Close() { + if (cache_) + cache_.reset(); + LOG(WARNING) << "[PageCache]Close, name:" << cfg_.CacheName; + return SUCCESS; +} + +int PageCacheImpl::Write(const std::string &key, + uint32_t pagePos, + uint32_t length, + const char *buf) { + assert(cfg_.PageBodySize >= pagePos + length); + assert(cache_); + + Cache::WriteHandle writeHandle = nullptr; + char* pageValue = nullptr; + while (true) { + writeHandle = std::move(FindOrCreateWriteHandle(key)); + pageValue = reinterpret_cast(writeHandle->getMemory()); + if (Lock(pageValue)) break; + } + + uint64_t realOffset = cfg_.PageMetaSize + bitmapSize_ + pagePos; + uint8_t newVer = AddNewVer(pageValue); + std::memcpy(pageValue + realOffset, buf, length); + SetBitMap(pageValue, pagePos, length, true); + SetLastVer(pageValue, newVer); + UnLock(pageValue); + return SUCCESS; +} + +int PageCacheImpl::Read(const std::string &key, + uint32_t pagePos, + uint32_t length, + char *buf, + std::vector>& dataBoundary) { + assert(cfg_.PageBodySize >= pagePos + length); + assert(cache_); + + int res = SUCCESS; + while (true) { + auto readHandle = cache_->find(key); + if (!readHandle) { + res = PAGE_NOT_FOUND; + break; + } + while (!readHandle.isReady()); + + const char* pageValue = reinterpret_cast( + readHandle->getMemory()); + uint8_t lastVer = GetLastVer(pageValue); + uint8_t newVer = GetNewVer(pageValue); + if (lastVer != newVer) continue; + + dataBoundary.clear(); + uint32_t cur = pagePos; + if (GetFastBitmap(pageValue)) { + uint32_t pageOff = cfg_.PageMetaSize + bitmapSize_ + pagePos; + std::memcpy(buf, pageValue + pageOff, length); + dataBoundary.push_back(std::make_pair(0, length)); + cur += length; + } + + bool continuousDataValid = false; // continuous Data valid or invalid + uint32_t continuousLen = 0; + while (cur < pagePos+length) { + const char *byte = pageValue + cfg_.PageMetaSize + cur / BYTE_LEN; + + // fast to judge full byte of bitmap + uint16_t batLen = 0; + bool batByteValid = false, isBatFuncValid = false; + + batLen = 64; + if (cur % batLen == 0 && (pagePos+length-cur) >= batLen) { + uint64_t byteValue = *reinterpret_cast(byte); + if (byteValue == UINT64_MAX) { + batByteValid = true; + isBatFuncValid = true; + } else if (byteValue == 0) { + isBatFuncValid = true; + } + } + + if (isBatFuncValid && (continuousLen == 0 || + continuousDataValid == batByteValid)) { + continuousDataValid = batByteValid; + continuousLen += batLen; + cur += batLen; + continue; + } + + bool curByteValid = GetBit(byte, cur % BYTE_LEN); + if (continuousLen == 0 || continuousDataValid == curByteValid) { + continuousDataValid = curByteValid; + ++continuousLen; + ++cur; + continue; + } + + if (continuousDataValid) { + uint32_t bufOff = cur - continuousLen - pagePos; + uint32_t pageOff = cfg_.PageMetaSize + bitmapSize_ + + cur - continuousLen; + std::memcpy(buf + bufOff, pageValue + pageOff, continuousLen); + dataBoundary.push_back(std::make_pair(bufOff, continuousLen)); + } + + continuousDataValid = curByteValid; + continuousLen = 1; + ++cur; + } + if (continuousDataValid) { + uint32_t bufOff = cur - continuousLen - pagePos; + uint32_t pageOff = cfg_.PageMetaSize + bitmapSize_ + + cur - continuousLen; + std::memcpy(buf + bufOff, pageValue + pageOff, continuousLen); + dataBoundary.push_back(std::make_pair(bufOff, continuousLen)); + } + + newVer = GetNewVer(pageValue); + if (lastVer == newVer) break; + } + return res; +} + +int PageCacheImpl::GetAllCache(const std::string &key, + std::vector>& dataSegments) { + assert(cache_); + uint32_t pageSize = cfg_.PageBodySize; + + int res = SUCCESS; + while (true) { + auto readHandle = cache_->find(key); + if (!readHandle) { + res = PAGE_NOT_FOUND; + break; + } + while (!readHandle.isReady()); + + const char* pageValue = reinterpret_cast( + readHandle->getMemory()); + uint8_t lastVer = GetLastVer(pageValue); + uint8_t newVer = GetNewVer(pageValue); + if (lastVer != newVer) continue; + + dataSegments.clear(); + uint32_t cur = 0; + if (GetFastBitmap(pageValue)) { + uint32_t pageOff = cfg_.PageMetaSize + bitmapSize_; + dataSegments.push_back(std::make_pair( + ByteBuffer(const_cast(pageValue + pageOff), pageSize), 0)); + cur += pageSize; + } + + bool continuousDataValid = false; // continuous Data valid or invalid + uint32_t continuousLen = 0; + while (cur < pageSize) { + const char *byte = pageValue + cfg_.PageMetaSize + cur / BYTE_LEN; + + // fast to judge full byte of bitmap + uint16_t batLen = 0; + bool batByteValid = false, isBatFuncValid = false; + + batLen = 64; + if (cur % batLen == 0 && (pageSize-cur) >= batLen) { + uint64_t byteValue = *reinterpret_cast(byte); + if (byteValue == UINT64_MAX) { + batByteValid = true; + isBatFuncValid = true; + } else if (byteValue == 0) { + isBatFuncValid = true; + } + } + + if (isBatFuncValid && (continuousLen == 0 || + continuousDataValid == batByteValid)) { + continuousDataValid = batByteValid; + continuousLen += batLen; + cur += batLen; + continue; + } + + bool curByteValid = GetBit(byte, cur % BYTE_LEN); + if (continuousLen == 0 || continuousDataValid == curByteValid) { + continuousDataValid = curByteValid; + ++continuousLen; + ++cur; + continue; + } + + if (continuousDataValid) { + uint32_t pageOff = cfg_.PageMetaSize + bitmapSize_ + + cur - continuousLen; + dataSegments.push_back(std::make_pair( + ByteBuffer(const_cast(pageValue + pageOff), continuousLen), + cur - continuousLen)); + } + + continuousDataValid = curByteValid; + continuousLen = 1; + ++cur; + } + if (continuousDataValid) { + uint32_t pageOff = cfg_.PageMetaSize + bitmapSize_ + + cur - continuousLen; + dataSegments.push_back(std::make_pair( + ByteBuffer(const_cast(pageValue + pageOff), continuousLen), + cur - continuousLen)); + } + + newVer = GetNewVer(pageValue); + if (lastVer == newVer) break; + } + return res; +} + +int PageCacheImpl::DeletePart(const std::string &key, + uint32_t pagePos, + uint32_t length) { + assert(cfg_.PageBodySize >= pagePos + length); + assert(cache_); + + int res = SUCCESS; + Cache::WriteHandle writeHandle = nullptr; + char* pageValue = nullptr; + while (true) { + writeHandle = cache_->findToWrite(key); + if (!writeHandle) { + res = PAGE_NOT_FOUND; + break; + } + pageValue = reinterpret_cast(writeHandle->getMemory()); + if (Lock(pageValue)) break; + } + + if (SUCCESS == res) { + uint8_t newVer = AddNewVer(pageValue); + SetBitMap(pageValue, pagePos, length, false); + + bool isEmpty = true; + uint32_t pos = 0; + while (pos < bitmapSize_) { + if (*(pageValue + cfg_.PageMetaSize + pos) != 0) { + isEmpty = false; + break; + } + ++pos; + } + + bool isDel = false; + if (isEmpty) { + if (cache_->remove(writeHandle) == Cache::RemoveRes::kSuccess) { + pageNum_.fetch_sub(1); + pagesList_.erase(key); + isDel = true; + } else { + res = PAGE_DEL_FAIL; + } + } + + if (!isDel) { + SetLastVer(pageValue, newVer); + UnLock(pageValue); + } + } + return res; +} + +int PageCacheImpl::Delete(const std::string &key) { + assert(cache_); + int res = cache_->remove(key) == Cache::RemoveRes::kSuccess ? SUCCESS : PAGE_NOT_FOUND; + if (SUCCESS == res) { + pageNum_.fetch_sub(1); + pagesList_.erase(key); + } + return res; +} + +Cache::WriteHandle PageCacheImpl::FindOrCreateWriteHandle(const std::string &key) { + auto writeHandle = cache_->findToWrite(key); + if (!writeHandle) { + writeHandle = cache_->allocate(pool_, key, GetRealPageSize()); + assert(writeHandle); + assert(writeHandle->getMemory()); + // need init + memset(writeHandle->getMemory(), 0, cfg_.PageMetaSize + bitmapSize_); + + if (cfg_.CacheLibCfg.EnableNvmCache) { + // insertOrReplace will insert or replace existing item for the key, + // and return the handle of the replaced old item + // Note: write cache nonsupport NVM, because it will be replaced + if (!cache_->insertOrReplace(writeHandle)) { + pageNum_.fetch_add(1); + pagesList_.insert(key); + } + } else { + if (cache_->insert(writeHandle)) { + pageNum_.fetch_add(1); + pagesList_.insert(key); + } else { + writeHandle = cache_->findToWrite(key); + } + } + } + return writeHandle; +} + +} // namespace HybridCache diff --git a/local_cache/page_cache.h b/local_cache/page_cache.h new file mode 100644 index 0000000..dbfb72a --- /dev/null +++ b/local_cache/page_cache.h @@ -0,0 +1,161 @@ +/* + * Project: HybridCache + * Created Date: 24-2-21 + * Author: lshb + */ +#ifndef HYBRIDCACHE_PAGE_CACHE_H_ +#define HYBRIDCACHE_PAGE_CACHE_H_ + +#include +#include + +#include "folly/ConcurrentSkipList.h" +#include "cachelib/allocator/CacheAllocator.h" + +#include "common.h" +#include "config.h" + +namespace HybridCache { + +typedef folly::ConcurrentSkipList StringSkipList; +using facebook::cachelib::PoolId; +using Cache = facebook::cachelib::LruAllocator; + +enum class MetaPos { + LOCK = 0, + LASTVER, + NEWVER, + FAST_BITMAP +}; + +class PageCache { + public: + PageCache(const CacheConfig& cfg): cfg_(cfg) {} + virtual ~PageCache() {} + + virtual int Init() = 0; + virtual int Close() = 0; + + virtual int Write(const std::string &key, // page key + uint32_t pagePos, + uint32_t length, + const char *buf // user buf + ) = 0; + + virtual int Read(const std::string &key, + uint32_t pagePos, + uint32_t length, + char *buf, // user buf + std::vector>& dataBoundary // valid data segment boundar + ) = 0; + + // upper layer need to guarantee that the page will not be delete + virtual int GetAllCache(const std::string &key, + std::vector>& dataSegments // + ) = 0; + + // delete part data from page + // if the whole page is empty then delete that page + virtual int DeletePart(const std::string &key, + uint32_t pagePos, + uint32_t length + ) = 0; + + virtual int Delete(const std::string &key) = 0; + + virtual size_t GetCacheSize() = 0; + virtual size_t GetCacheMaxSize() = 0; + + const folly::ConcurrentSkipList::Accessor& GetPageList() { + return this->pagesList_; + } + + protected: + // CAS operate + bool Lock(char* pageMemory); + void UnLock(char* pageMemory); + uint8_t AddNewVer(char* pageMemory); + void SetLastVer(char* pageMemory, uint8_t newVer); + uint8_t GetLastVer(const char* pageMemory); + uint8_t GetNewVer(const char* pageMemory); + + // bitmap operate + void SetFastBitmap(char* pageMemory, bool valid); + bool GetFastBitmap(const char* pageMemory); + void SetBitMap(char* pageMemory, int pos, int len, bool valid); + void SetBit(char *x, int n) { *x |= (1 << n); } + void ClearBit(char *x, int n) { *x &= ~ (1 << n); } + bool GetBit(const char *x, int n) { return *x & (1 << n); } + + protected: + StringSkipList::Accessor pagesList_ = StringSkipList::create(SKIP_LIST_HEIGHT); + CacheConfig cfg_; +}; + +class PageCacheImpl : public PageCache { + public: + PageCacheImpl(const CacheConfig& cfg): PageCache(cfg) { + bitmapSize_ = cfg_.PageBodySize / BYTE_LEN; + } + ~PageCacheImpl() {} + + int Init(); + + int Close(); + + int Write(const std::string &key, + uint32_t pagePos, + uint32_t length, + const char *buf + ); + + int Read(const std::string &key, + uint32_t pagePos, + uint32_t length, + char *buf, + std::vector>& dataBoundary + ); + + int GetAllCache(const std::string &key, + std::vector>& dataSegments + ); + + int DeletePart(const std::string &key, + uint32_t pagePos, + uint32_t length + ); + + int Delete(const std::string &key); + + size_t GetCacheSize() { + return GetPageNum() * GetRealPageSize(); + } + size_t GetCacheMaxSize() { + if (!cfg_.CacheLibCfg.EnableNvmCache) + return cfg_.MaxCacheSize; + size_t nvmMaxSize = cfg_.CacheLibCfg.RaidFileNum * + cfg_.CacheLibCfg.RaidFileSize; + return cfg_.MaxCacheSize + nvmMaxSize; + } + + private: + uint64_t GetPageNum() { + return pageNum_.load(); + } + + uint32_t GetRealPageSize() { + return cfg_.PageMetaSize + bitmapSize_ + cfg_.PageBodySize; + } + + Cache::WriteHandle FindOrCreateWriteHandle(const std::string &key); + + private: + std::shared_ptr cache_; + PoolId pool_; + std::atomic pageNum_{0}; + uint32_t bitmapSize_; +}; + +} // namespace HybridCache + +#endif // HYBRIDCACHE_PAGE_CACHE_H_ diff --git a/local_cache/read_cache.cpp b/local_cache/read_cache.cpp new file mode 100644 index 0000000..31aed1a --- /dev/null +++ b/local_cache/read_cache.cpp @@ -0,0 +1,257 @@ +#include "errorcode.h" +#include "read_cache.h" + +namespace HybridCache { + +ReadCache::ReadCache(const ReadCacheConfig& cfg, + std::shared_ptr dataAdaptor, + std::shared_ptr executor) : + cfg_(cfg), dataAdaptor_(dataAdaptor), executor_(executor) { + Init(); +} + +folly::Future ReadCache::Get(const std::string &key, size_t start, + size_t len, ByteBuffer &buffer) { + std::chrono::steady_clock::time_point startTime; + if (EnableLogging) startTime = std::chrono::steady_clock::now(); + + int res = SUCCESS; + uint32_t pageSize = cfg_.CacheCfg.PageBodySize; + size_t index = start / pageSize; + uint32_t pagePos = start % pageSize; + size_t readLen = 0; + size_t realReadLen = 0; + size_t bufOffset = 0; + size_t remainLen = len; + uint64_t readPageCnt = 0; + std::vector> dataBoundary; + + while (remainLen > 0) { + readLen = pagePos + remainLen > pageSize ? pageSize - pagePos : remainLen; + std::string pageKey = std::move(GetPageKey(key, index)); + std::vector> stepDataBoundary; + int tmpRes = pageCache_->Read(pageKey, pagePos, readLen, + (buffer.data + bufOffset), stepDataBoundary); + if (SUCCESS == tmpRes) { + ++readPageCnt; + } else if (PAGE_NOT_FOUND != tmpRes) { + res = tmpRes; + break; + } + + for (auto& it : stepDataBoundary) { + dataBoundary.push_back(std::make_pair(it.first + bufOffset, it.second)); + realReadLen += it.second; + } + remainLen -= readLen; + ++index; + bufOffset += readLen; + pagePos = (pagePos + readLen) % pageSize; + } + + remainLen = len - realReadLen; + if (remainLen > 0 && !dataAdaptor_) { + res = ADAPTOR_NOT_FOUND; + } + + // handle cache misses + readLen = 0; + size_t stepStart = 0; + size_t fileStartOff = 0; + std::vector> fs; + auto it = dataBoundary.begin(); + while (remainLen > 0 && SUCCESS == res) { + ByteBuffer stepBuffer(buffer.data + stepStart); + fileStartOff = start + stepStart; + if (it != dataBoundary.end()) { + readLen = it->first - stepStart; + if (!readLen) { + stepStart = it->first + it->second; + ++it; + continue; + } + stepStart = it->first + it->second; + ++it; + } else { + readLen = remainLen; + } + stepBuffer.len = readLen; + remainLen -= readLen; + + auto download = folly::via(executor_.get(), [this, readLen]() { + // download flow control + while(!this->tokenBucket_->consume(readLen)); + return SUCCESS; + }).thenValue([this, key, fileStartOff, readLen, stepBuffer](int i) { + // LOG(INFO) << "Extra download: " << key << " " << readLen; + ByteBuffer tmpBuffer(stepBuffer.data, readLen); + return this->dataAdaptor_->DownLoad(key, fileStartOff, readLen, tmpBuffer).get(); + }).thenValue([this, key, fileStartOff, readLen, stepBuffer](int downRes) { + if (EnableLogging && SUCCESS != downRes) { + LOG(ERROR) << "[ReadCache]DownLoad failed, file:" << key + << ", start:" << fileStartOff << ", len:" << readLen + << ", res:" << downRes; + return downRes; + } + return this->Put(key, fileStartOff, readLen, stepBuffer); + }); + + fs.emplace_back(std::move(download)); + } + + if (!fs.empty()) { + return collectAll(fs).via(executor_.get()) + .thenValue([key, start, len, readPageCnt, startTime]( + std::vector, std::allocator>>&& tups) { + int finalRes = SUCCESS; + for (const auto& t : tups) { + if (SUCCESS != t.value()) finalRes = t.value(); + } + if (EnableLogging) { + double totalTime = std::chrono::duration( + std::chrono::steady_clock::now() - startTime).count(); + LOG(INFO) << "[ReadCache]Get, key:" << key << ", start:" << start + << ", len:" << len << ", res:" << finalRes + << ", readPageCnt:" << readPageCnt + << ", time:" << totalTime << "ms"; + } + return finalRes; + }); + // auto tups = collectAll(fs).get(); + // int finalRes = SUCCESS; + // for (const auto& t : tups) { + // if (SUCCESS != t.value()) finalRes = t.value(); + // } + // if (EnableLogging) { + // double totalTime = std::chrono::duration( + // std::chrono::steady_clock::now() - startTime).count(); + // LOG(INFO) << "[ReadCache]Get, key:" << key << ", start:" << start + // << ", len:" << len << ", res:" << finalRes + // << ", readPageCnt:" << readPageCnt + // << ", time:" << totalTime << "ms"; + // } + // return finalRes; + } + + if (EnableLogging) { + double totalTime = std::chrono::duration( + std::chrono::steady_clock::now() - startTime).count(); + LOG(INFO) << "[ReadCache]Get, key:" << key << ", start:" << start + << ", len:" << len << ", res:" << res + << ", readPageCnt:" << readPageCnt + << ", time:" << totalTime << "ms"; + } + return folly::makeFuture(res); +} + +int ReadCache::Put(const std::string &key, size_t start, size_t len, + const ByteBuffer &buffer) { + std::chrono::steady_clock::time_point startTime; + if (EnableLogging) startTime = std::chrono::steady_clock::now(); + + int res = SUCCESS; + uint32_t pageSize = cfg_.CacheCfg.PageBodySize; + uint64_t index = start / pageSize; + uint64_t pagePos = start % pageSize; + uint64_t writeLen = 0; + uint64_t writeOffset = 0; + uint64_t writePageCnt = 0; + size_t remainLen = len; + + while (remainLen > 0) { + writeLen = pagePos + remainLen > pageSize ? pageSize - pagePos : remainLen; + std::string pageKey = std::move(GetPageKey(key, index)); + res = pageCache_->Write(pageKey, pagePos, writeLen, + (buffer.data + writeOffset)); + if (SUCCESS != res) break; + ++writePageCnt; + remainLen -= writeLen; + ++index; + writeOffset += writeLen; + pagePos = (pagePos + writeLen) % pageSize; + } + + if (EnableLogging) { + double totalTime = std::chrono::duration( + std::chrono::steady_clock::now() - startTime).count(); + LOG(INFO) << "[ReadCache]Put, key:" << key << ", start:" << start + << ", len:" << len << ", res:" << res + << ", writePageCnt:" << writePageCnt + << ", time:" << totalTime << "ms"; + } + return res; +} + +int ReadCache::Delete(const std::string &key) { + std::chrono::steady_clock::time_point startTime; + if (EnableLogging) startTime = std::chrono::steady_clock::now(); + + int res = SUCCESS; + size_t delPageNum = 0; + std::string firstPage = std::move(GetPageKey(key, 0)); + auto pageKey = pageCache_->GetPageList().lower_bound(firstPage); + while (pageKey != pageCache_->GetPageList().end()) { + std::vector tokens; + split(*pageKey, PAGE_SEPARATOR, tokens); + if (key != tokens[0]) break; + int tmpRes = pageCache_->Delete(*pageKey); + if (SUCCESS == tmpRes) { + ++delPageNum; + } else if (PAGE_NOT_FOUND != tmpRes) { + res = tmpRes; + break; + } + ++pageKey; + } + + if (EnableLogging) { + double totalTime = std::chrono::duration( + std::chrono::steady_clock::now() - startTime).count(); + LOG(INFO) << "[ReadCache]Delete, key:" << key << ", res:" << res + << ", delPageCnt:" << delPageNum + << ", time:" << totalTime << "ms"; + } + return res; +} + +int ReadCache::GetAllKeys(std::set& keys) { + std::chrono::steady_clock::time_point startTime; + if (EnableLogging) startTime = std::chrono::steady_clock::now(); + + auto pageKey = pageCache_->GetPageList().begin(); + while (pageKey != pageCache_->GetPageList().end()) { + std::vector tokens; + split(*pageKey, PAGE_SEPARATOR, tokens); + keys.insert(tokens[0]); + ++pageKey; + } + if (EnableLogging) { + double totalTime = std::chrono::duration( + std::chrono::steady_clock::now() - startTime).count(); + LOG(INFO) << "[ReadCache]Get all keys, keyCnt:" << keys.size() + << ", time:" << totalTime << "ms"; + } + return SUCCESS; +} + +void ReadCache::Close() { + pageCache_->Close(); + LOG(WARNING) << "[ReadCache]Close"; +} + +int ReadCache::Init() { + pageCache_ = std::make_shared(cfg_.CacheCfg); + tokenBucket_ = std::make_shared( + cfg_.DownloadNormalFlowLimit, cfg_.DownloadBurstFlowLimit); + int res = pageCache_->Init(); + LOG(WARNING) << "[ReadCache]Init, res:" << res; + return res; +} + +std::string ReadCache::GetPageKey(const std::string &key, size_t pageIndex) { + std::string pageKey(key); + pageKey.append(std::string(1, PAGE_SEPARATOR)).append(std::to_string(pageIndex)); + return pageKey; +} + +} // namespace HybridCache diff --git a/local_cache/read_cache.h b/local_cache/read_cache.h new file mode 100644 index 0000000..f3f5fe5 --- /dev/null +++ b/local_cache/read_cache.h @@ -0,0 +1,57 @@ +/* + * Project: HybridCache + * Created Date: 24-2-29 + * Author: lshb + */ +#ifndef HYBRIDCACHE_READ_CACHE_H_ +#define HYBRIDCACHE_READ_CACHE_H_ + +#include "folly/TokenBucket.h" + +#include "page_cache.h" +#include "data_adaptor.h" + +namespace HybridCache { + +class ReadCache { + public: + ReadCache(const ReadCacheConfig& cfg, + std::shared_ptr dataAdaptor, + std::shared_ptr executor); + ReadCache() = default; + ~ReadCache() { Close(); } + + // Read the local page cache first, and get it from the DataAdaptor if it misses + folly::Future Get(const std::string &key, + size_t start, + size_t len, + ByteBuffer &buffer // user buf + ); + + int Put(const std::string &key, + size_t start, + size_t len, + const ByteBuffer &buffer); + + int Delete(const std::string &key); + + int GetAllKeys(std::set& keys); + + void Close(); + + private: + int Init(); + + std::string GetPageKey(const std::string &key, size_t pageIndex); + + private: + ReadCacheConfig cfg_; + std::shared_ptr pageCache_; + std::shared_ptr dataAdaptor_; + std::shared_ptr executor_; + std::shared_ptr tokenBucket_; // download flow limit +}; + +} // namespace HybridCache + +#endif // HYBRIDCACHE_READ_CACHE_H_ diff --git a/local_cache/write_cache.cpp b/local_cache/write_cache.cpp new file mode 100644 index 0000000..364a0df --- /dev/null +++ b/local_cache/write_cache.cpp @@ -0,0 +1,286 @@ +#include "glog/logging.h" + +#include "errorcode.h" +#include "write_cache.h" + +namespace HybridCache { + +int WriteCache::Put(const std::string &key, size_t start, size_t len, + const ByteBuffer &buffer) { + std::chrono::steady_clock::time_point startTime; + if (EnableLogging) startTime = std::chrono::steady_clock::now(); + + int res = SUCCESS; + uint32_t pageSize = cfg_.CacheCfg.PageBodySize; + uint64_t index = start / pageSize; + uint64_t pagePos = start % pageSize; + uint64_t writeLen = 0; + uint64_t writeOffset = 0; + uint64_t writePageCnt = 0; + size_t remainLen = len; + + while (remainLen > 0) { + writeLen = pagePos + remainLen > pageSize ? pageSize - pagePos : remainLen; + std::string pageKey = std::move(GetPageKey(key, index)); + res = pageCache_->Write(pageKey, pagePos, writeLen, + (buffer.data + writeOffset)); + if (SUCCESS != res) break; + ++writePageCnt; + remainLen -= writeLen; + ++index; + writeOffset += writeLen; + pagePos = (pagePos + writeLen) % pageSize; + } + if (0 < writePageCnt) + keys_.insert(key, time(nullptr)); + + if (EnableLogging) { + double totalTime = std::chrono::duration( + std::chrono::steady_clock::now() - startTime).count(); + LOG(INFO) << "[WriteCache]Put, key:" << key << ", start:" << start + << ", len:" << len << ", res:" << res + << ", writePageCnt:" << writePageCnt + << ", time:" << totalTime << "ms"; + } + return res; +} + +int WriteCache::Get(const std::string &key, size_t start, size_t len, + ByteBuffer &buffer, + std::vector>& dataBoundary) { + std::chrono::steady_clock::time_point startTime; + if (EnableLogging) startTime = std::chrono::steady_clock::now(); + + int res = SUCCESS; + uint32_t pageSize = cfg_.CacheCfg.PageBodySize; + size_t index = start / pageSize; + uint32_t pagePos = start % pageSize; + size_t readLen = 0; + size_t bufOffset = 0; + size_t remainLen = len; + uint64_t readPageCnt = 0; + + while (remainLen > 0) { + readLen = pagePos + remainLen > pageSize ? pageSize - pagePos : remainLen; + std::string pageKey = std::move(GetPageKey(key, index)); + std::vector> stepDataBoundary; + int tmpRes = pageCache_->Read(pageKey, pagePos, readLen, + (buffer.data + bufOffset), stepDataBoundary); + if (SUCCESS == tmpRes) { + ++readPageCnt; + } else if (PAGE_NOT_FOUND != tmpRes) { + res = tmpRes; + break; + } + + for (auto& it : stepDataBoundary) { + size_t realStart = it.first + bufOffset; + auto last = dataBoundary.rbegin(); + if (last != dataBoundary.rend() && (last->first + last->second) == realStart) { + last->second += it.second; + } else { + dataBoundary.push_back(std::make_pair(realStart, it.second)); + } + } + remainLen -= readLen; + ++index; + bufOffset += readLen; + pagePos = (pagePos + readLen) % pageSize; + } + + if (EnableLogging) { + double totalTime = std::chrono::duration( + std::chrono::steady_clock::now() - startTime).count(); + LOG(INFO) << "[WriteCache]Get, key:" << key << ", start:" << start + << ", len:" << len << ", res:" << res + << ", boundaryVecSize:" << dataBoundary.size() + << ", readPageCnt:" << readPageCnt + << ", time:" << totalTime << "ms"; + } + return res; +} + +int WriteCache::GetAllCacheWithLock(const std::string &key, + std::vector>& dataSegments) { + std::chrono::steady_clock::time_point startTime; + if (EnableLogging) startTime = std::chrono::steady_clock::now(); + + int res = SUCCESS; + Lock(key); + + std::string firstPage = std::move(GetPageKey(key, 0)); + auto pageKey = pageCache_->GetPageList().lower_bound(firstPage); + while (pageKey != pageCache_->GetPageList().end()) { + std::vector tokens; + split(*pageKey, PAGE_SEPARATOR, tokens); + if (key != tokens[0]) break; + + size_t pageIdx = 0; + std::stringstream sstream(tokens[1]); + sstream >> pageIdx; + size_t wholeValueOff = pageIdx * cfg_.CacheCfg.PageBodySize; + + std::vector> stepDataSegments; + res = pageCache_->GetAllCache(*pageKey, stepDataSegments); + if (SUCCESS != res) break; + for (auto& it : stepDataSegments) { + dataSegments.push_back(std::make_pair(it.first, + it.second + wholeValueOff)); + } + ++pageKey; + } + + if (EnableLogging) { + double totalTime = std::chrono::duration( + std::chrono::steady_clock::now() - startTime).count(); + LOG(INFO) << "[WriteCache]Get all cache with lock, key:" << key + << ", res:" << res << ", dataVecSize:" << dataSegments.size() + << ", time:" << totalTime << "ms"; + } + return res; +} + +int WriteCache::Delete(const std::string &key, LockType type) { + std::chrono::steady_clock::time_point startTime; + if (EnableLogging) startTime = std::chrono::steady_clock::now(); + + int res = SUCCESS; + if (LockType::ALREADY_LOCKED != type) { + Lock(key); + } + + keys_.erase(key); + size_t delPageNum = 0; + std::string firstPage = std::move(GetPageKey(key, 0)); + auto pageKey = pageCache_->GetPageList().lower_bound(firstPage); + while (pageKey != pageCache_->GetPageList().end()) { + std::vector tokens; + split(*pageKey, PAGE_SEPARATOR, tokens); + if (key != tokens[0]) break; + int tmpRes = pageCache_->Delete(*pageKey); + if (SUCCESS == tmpRes) { + ++delPageNum; + } else if (PAGE_NOT_FOUND != tmpRes) { + res = tmpRes; + break; + } + ++pageKey; + } + + UnLock(key); + + if (EnableLogging) { + double totalTime = std::chrono::duration( + std::chrono::steady_clock::now() - startTime).count(); + LOG(INFO) << "[WriteCache]Delete, key:" << key << ", res:" << res + << ", delPageCnt:" << delPageNum + << ", time:" << totalTime << "ms"; + } + return res; +} + +int WriteCache::Truncate(const std::string &key, size_t len) { + std::chrono::steady_clock::time_point startTime; + if (EnableLogging) startTime = std::chrono::steady_clock::now(); + + int res = SUCCESS; + uint32_t pageSize = cfg_.CacheCfg.PageBodySize; + uint64_t index = len / pageSize; + uint64_t pagePos = len % pageSize; + + if (0 != pagePos) { + uint32_t TruncateLen = pageSize - pagePos; + std::string TruncatePage = std::move(GetPageKey(key, index)); + int tmpRes = pageCache_->DeletePart(TruncatePage, pagePos, TruncateLen); + if (SUCCESS != tmpRes && PAGE_NOT_FOUND != tmpRes) { + res = tmpRes; + } + ++index; + } + + size_t delPageNum = 0; + if (SUCCESS == res) { + Lock(key); + std::string firstPage = std::move(GetPageKey(key, index)); + auto pageKey = pageCache_->GetPageList().lower_bound(firstPage); + while (pageKey != pageCache_->GetPageList().end()) { + std::vector tokens; + split(*pageKey, PAGE_SEPARATOR, tokens); + if (key != tokens[0]) break; + int tmpRes = pageCache_->Delete(*pageKey); + if (SUCCESS == tmpRes) { + ++delPageNum; + } else if (PAGE_NOT_FOUND != tmpRes) { + res = tmpRes; + break; + } + ++pageKey; + } + UnLock(key); + } + + if (EnableLogging) { + double totalTime = std::chrono::duration( + std::chrono::steady_clock::now() - startTime).count(); + LOG(INFO) << "[WriteCache]Truncate, key:" << key << ", len:" << len + << ", res:" << res << ", delPageCnt:" << delPageNum + << ", time:" << totalTime << "ms"; + } + return res; +} + +void WriteCache::UnLock(const std::string &key) { + keyLocks_.erase(key); + if (EnableLogging) { + LOG(INFO) << "[WriteCache]UnLock, key:" << key; + } +} + +int WriteCache::GetAllKeys(std::map& keys) { + std::chrono::steady_clock::time_point startTime; + if (EnableLogging) startTime = std::chrono::steady_clock::now(); + + for (auto& it : keys_) { + keys[it.first] = it.second; + } + if (EnableLogging) { + double totalTime = std::chrono::duration( + std::chrono::steady_clock::now() - startTime).count(); + LOG(INFO) << "[WriteCache]Get all keys, keyCnt:" << keys.size() + << ", time:" << totalTime << "ms"; + } + return SUCCESS; +} + +void WriteCache::Close() { + pageCache_->Close(); + keys_.clear(); + LOG(WARNING) << "[WriteCache]Close"; +} + +size_t WriteCache::GetCacheSize() { + return pageCache_->GetCacheSize(); +} + +size_t WriteCache::GetCacheMaxSize() { + return pageCache_->GetCacheMaxSize(); +} + +int WriteCache::Init() { + pageCache_ = std::make_shared(cfg_.CacheCfg); + int res = pageCache_->Init(); + LOG(WARNING) << "[WriteCache]Init, res:" << res; + return res; +} + +void WriteCache::Lock(const std::string &key) { + while(!keyLocks_.add(key)); +} + +std::string WriteCache::GetPageKey(const std::string &key, size_t pageIndex) { + std::string pageKey(key); + pageKey.append(std::string(1, PAGE_SEPARATOR)).append(std::to_string(pageIndex)); + return pageKey; +} + +} // namespace HybridCache diff --git a/local_cache/write_cache.h b/local_cache/write_cache.h new file mode 100644 index 0000000..82e005e --- /dev/null +++ b/local_cache/write_cache.h @@ -0,0 +1,74 @@ +/* + * Project: HybridCache + * Created Date: 24-3-18 + * Author: lshb + */ +#ifndef HYBRIDCACHE_WRITE_CACHE_H_ +#define HYBRIDCACHE_WRITE_CACHE_H_ + +#include "folly/concurrency/ConcurrentHashMap.h" + +#include "page_cache.h" + +namespace HybridCache { + +class WriteCache { + public: + WriteCache(const WriteCacheConfig& cfg) : cfg_(cfg) { Init(); } + WriteCache() = default; + ~WriteCache() { Close(); } + + enum class LockType { + NONE = 0, + ALREADY_LOCKED = -1, + }; + + int Put(const std::string &key, + size_t start, + size_t len, + const ByteBuffer &buffer + ); + + int Get(const std::string &key, + size_t start, + size_t len, + ByteBuffer &buffer, + std::vector>& dataBoundary // valid data segment boundar + ); + + // lock to ensure the availability of the returned buf + // After being locked, it can be read and written, but cannot be deleted + int GetAllCacheWithLock(const std::string &key, + std::vector>& dataSegments // ByteBuffer + off of key value(file) + ); + + int Delete(const std::string &key, LockType type = LockType::NONE); + + int Truncate(const std::string &key, size_t len); + + void UnLock(const std::string &key); + + int GetAllKeys(std::map& keys); + + void Close(); + + size_t GetCacheSize(); + size_t GetCacheMaxSize(); + + private: + int Init(); + + void Lock(const std::string &key); + + std::string GetPageKey(const std::string &key, size_t pageIndex); + + private: + WriteCacheConfig cfg_; + std::shared_ptr pageCache_; + folly::ConcurrentHashMap keys_; // + StringSkipList::Accessor keyLocks_ = StringSkipList::create(SKIP_LIST_HEIGHT); // presence key indicates lock +}; + +} // namespace HybridCache + +#endif // HYBRIDCACHE_WRITE_CACHE_H_ diff --git a/s3fs/CMakeLists.txt b/s3fs/CMakeLists.txt new file mode 100644 index 0000000..fb1985b --- /dev/null +++ b/s3fs/CMakeLists.txt @@ -0,0 +1,16 @@ +SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin) + +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -O3 -D_FILE_OFFSET_BITS=64 -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -O3 -D_FILE_OFFSET_BITS=64 -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3") + +file(GLOB_RECURSE ALL_SOURCES CONFIGURE_DEPENDS "*.cpp") +list(REMOVE_ITEM ALL_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/s3fs_lib.cpp") +add_executable(s3fs ${ALL_SOURCES}) +target_include_directories(s3fs PRIVATE /usr/include/fuse /usr/include/libxml2) +target_link_libraries(s3fs PUBLIC hybridcache_local madfs_global -lfuse -pthread -lcurl -lxml2 -lcrypto -ldl) + +file(GLOB_RECURSE LIB_SOURCES CONFIGURE_DEPENDS "*.cpp") +list(REMOVE_ITEM LIB_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/s3fs.cpp") +add_library(s3fs_lib STATIC ${LIB_SOURCES}) +target_include_directories(s3fs_lib PRIVATE /usr/include/fuse /usr/include/libxml2) +target_link_libraries(s3fs_lib PUBLIC hybridcache_local madfs_global -pthread -lcurl -lxml2 -lcrypto -ldl) diff --git a/s3fs/addhead.cpp b/s3fs/addhead.cpp new file mode 100644 index 0000000..bfcd5da --- /dev/null +++ b/s3fs/addhead.cpp @@ -0,0 +1,248 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "s3fs.h" +#include "addhead.h" +#include "curl_util.h" +#include "s3fs_logger.h" + +//------------------------------------------------------------------- +// Symbols +//------------------------------------------------------------------- +static constexpr char ADD_HEAD_REGEX[] = "reg:"; + +//------------------------------------------------------------------- +// Class AdditionalHeader +//------------------------------------------------------------------- +AdditionalHeader AdditionalHeader::singleton; + +//------------------------------------------------------------------- +// Class AdditionalHeader method +//------------------------------------------------------------------- +AdditionalHeader::AdditionalHeader() +{ + if(this == AdditionalHeader::get()){ + is_enable = false; + }else{ + abort(); + } +} + +AdditionalHeader::~AdditionalHeader() +{ + if(this == AdditionalHeader::get()){ + Unload(); + }else{ + abort(); + } +} + +bool AdditionalHeader::Load(const char* file) +{ + if(!file){ + S3FS_PRN_WARN("file is nullptr."); + return false; + } + Unload(); + + std::ifstream AH(file); + if(!AH.good()){ + S3FS_PRN_WARN("Could not open file(%s).", file); + return false; + } + + // read file + std::string line; + while(getline(AH, line)){ + if(line.empty()){ + continue; + } + if('#' == line[0]){ + continue; + } + // load a line + std::istringstream ss(line); + std::string key; // suffix(key) + std::string head; // additional HTTP header + std::string value; // header value + if(0 == isblank(line[0])){ + ss >> key; + } + if(ss){ + ss >> head; + if(ss && static_cast(ss.tellg()) < line.size()){ + value = line.substr(static_cast(ss.tellg()) + 1); + } + } + + // check it + if(head.empty()){ + if(key.empty()){ + continue; + } + S3FS_PRN_ERR("file format error: %s key(suffix) is no HTTP header value.", key.c_str()); + Unload(); + return false; + } + + if(0 == strncasecmp(key.c_str(), ADD_HEAD_REGEX, strlen(ADD_HEAD_REGEX))){ + // regex + if(key.size() <= strlen(ADD_HEAD_REGEX)){ + S3FS_PRN_ERR("file format error: %s key(suffix) does not have key std::string.", key.c_str()); + continue; + } + key.erase(0, strlen(ADD_HEAD_REGEX)); + + // compile + std::unique_ptr preg(new regex_t); + int result; + if(0 != (result = regcomp(preg.get(), key.c_str(), REG_EXTENDED | REG_NOSUB))){ // we do not need matching info + char errbuf[256]; + regerror(result, preg.get(), errbuf, sizeof(errbuf)); + S3FS_PRN_ERR("failed to compile regex from %s key by %s.", key.c_str(), errbuf); + continue; + } + + addheadlist.emplace_back(std::move(preg), key, head, value); + }else{ + // not regex, directly comparing + addheadlist.emplace_back(nullptr, key, head, value); + } + + // set flag + is_enable = true; + } + return true; +} + +void AdditionalHeader::Unload() +{ + is_enable = false; + + addheadlist.clear(); +} + +bool AdditionalHeader::AddHeader(headers_t& meta, const char* path) const +{ + if(!is_enable){ + return true; + } + if(!path){ + S3FS_PRN_WARN("path is nullptr."); + return false; + } + + size_t pathlength = strlen(path); + + // loop + // + // [NOTE] + // Because to allow duplicate key, and then scanning the entire table. + // + for(addheadlist_t::const_iterator iter = addheadlist.begin(); iter != addheadlist.end(); ++iter){ + const add_header *paddhead = &*iter; + + if(paddhead->pregex){ + // regex + regmatch_t match; // not use + if(0 == regexec(paddhead->pregex.get(), path, 1, &match, 0)){ + // match -> adding header + meta[paddhead->headkey] = paddhead->headvalue; + } + }else{ + // directly comparing + if(paddhead->basestring.length() < pathlength){ + if(paddhead->basestring.empty() || paddhead->basestring == &path[pathlength - paddhead->basestring.length()]){ + // match -> adding header + meta[paddhead->headkey] = paddhead->headvalue; + } + } + } + } + return true; +} + +struct curl_slist* AdditionalHeader::AddHeader(struct curl_slist* list, const char* path) const +{ + headers_t meta; + + if(!AddHeader(meta, path)){ + return list; + } + for(headers_t::iterator iter = meta.begin(); iter != meta.end(); ++iter){ + // Adding header + list = curl_slist_sort_insert(list, iter->first.c_str(), iter->second.c_str()); + } + meta.clear(); + S3FS_MALLOCTRIM(0); + return list; +} + +bool AdditionalHeader::Dump() const +{ + if(!S3fsLog::IsS3fsLogDbg()){ + return true; + } + + std::ostringstream ssdbg; + int cnt = 1; + + ssdbg << "Additional Header list[" << addheadlist.size() << "] = {" << std::endl; + + for(addheadlist_t::const_iterator iter = addheadlist.begin(); iter != addheadlist.end(); ++iter, ++cnt){ + const add_header *paddhead = &*iter; + + ssdbg << " [" << cnt << "] = {" << std::endl; + + if(paddhead->pregex){ + ssdbg << " type\t\t--->\tregex" << std::endl; + }else{ + ssdbg << " type\t\t--->\tsuffix matching" << std::endl; + } + ssdbg << " base std::string\t--->\t" << paddhead->basestring << std::endl; + ssdbg << " add header\t--->\t" << paddhead->headkey << ": " << paddhead->headvalue << std::endl; + ssdbg << " }" << std::endl; + } + + + ssdbg << "}" << std::endl; + + // print all + S3FS_PRN_DBG("%s", ssdbg.str().c_str()); + + return true; +} + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/addhead.h b/s3fs/addhead.h new file mode 100644 index 0000000..adb77a1 --- /dev/null +++ b/s3fs/addhead.h @@ -0,0 +1,98 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef S3FS_ADDHEAD_H_ +#define S3FS_ADDHEAD_H_ + +#include +#include +#include + +#include "metaheader.h" + +//---------------------------------------------- +// Structure / Typedef +//---------------------------------------------- +struct add_header{ + add_header(std::unique_ptr pregex, std::string basestring, std::string headkey, std::string headvalue) + : pregex(std::move(pregex)) + , basestring(std::move(basestring)) + , headkey(std::move(headkey)) + , headvalue(std::move(headvalue)) + {} + ~add_header() { + if(pregex){ + regfree(pregex.get()); + } + } + + add_header(const add_header&) = delete; + add_header(add_header&& val) = default; + add_header& operator=(const add_header&) = delete; + add_header& operator=(add_header&&) = delete; + + std::unique_ptr pregex; // not nullptr means using regex, nullptr means comparing suffix directly. + std::string basestring; + std::string headkey; + std::string headvalue; +}; + +typedef std::vector addheadlist_t; + +//---------------------------------------------- +// Class AdditionalHeader +//---------------------------------------------- +class AdditionalHeader +{ + private: + static AdditionalHeader singleton; + bool is_enable; + addheadlist_t addheadlist; + + protected: + AdditionalHeader(); + ~AdditionalHeader(); + AdditionalHeader(const AdditionalHeader&) = delete; + AdditionalHeader(AdditionalHeader&&) = delete; + AdditionalHeader& operator=(const AdditionalHeader&) = delete; + AdditionalHeader& operator=(AdditionalHeader&&) = delete; + + public: + // Reference singleton + static AdditionalHeader* get() { return &singleton; } + + bool Load(const char* file); + void Unload(); + + bool AddHeader(headers_t& meta, const char* path) const; + struct curl_slist* AddHeader(struct curl_slist* list, const char* path) const; + bool Dump() const; +}; + +#endif // S3FS_ADDHEAD_H_ + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/autolock.cpp b/s3fs/autolock.cpp new file mode 100644 index 0000000..b8c4371 --- /dev/null +++ b/s3fs/autolock.cpp @@ -0,0 +1,78 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Takeshi Nakatani + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include + +#include "autolock.h" +#include "s3fs_logger.h" + +//------------------------------------------------------------------- +// Class AutoLock +//------------------------------------------------------------------- +AutoLock::AutoLock(pthread_mutex_t* pmutex, Type type) : auto_mutex(pmutex) +{ + if (type == ALREADY_LOCKED) { + is_lock_acquired = false; + } else if (type == NO_WAIT) { + int result = pthread_mutex_trylock(auto_mutex); + if(result == 0){ + is_lock_acquired = true; + }else if(result == EBUSY){ + is_lock_acquired = false; + }else{ + S3FS_PRN_CRIT("pthread_mutex_trylock returned: %d", result); + abort(); + } + } else { + int result = pthread_mutex_lock(auto_mutex); + if(result == 0){ + is_lock_acquired = true; + }else{ + S3FS_PRN_CRIT("pthread_mutex_lock returned: %d", result); + abort(); + } + } +} + +bool AutoLock::isLockAcquired() const +{ + return is_lock_acquired; +} + +AutoLock::~AutoLock() +{ + if (is_lock_acquired) { + int result = pthread_mutex_unlock(auto_mutex); + if(result != 0){ + S3FS_PRN_CRIT("pthread_mutex_unlock returned: %d", result); + abort(); + } + } +} + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/autolock.h b/s3fs/autolock.h new file mode 100644 index 0000000..2202cfd --- /dev/null +++ b/s3fs/autolock.h @@ -0,0 +1,63 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef S3FS_AUTOLOCK_H_ +#define S3FS_AUTOLOCK_H_ + +#include + +//------------------------------------------------------------------- +// AutoLock Class +//------------------------------------------------------------------- +class AutoLock +{ + public: + enum Type { + NO_WAIT = 1, + ALREADY_LOCKED = 2, + NONE = 0 + }; + + private: + pthread_mutex_t* const auto_mutex; + bool is_lock_acquired; + + private: + AutoLock(const AutoLock&) = delete; + AutoLock(AutoLock&&) = delete; + AutoLock& operator=(const AutoLock&) = delete; + AutoLock& operator=(AutoLock&&) = delete; + + public: + explicit AutoLock(pthread_mutex_t* pmutex, Type type = NONE); + ~AutoLock(); + bool isLockAcquired() const; +}; + +#endif // S3FS_AUTOLOCK_H_ + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/cache.cpp b/s3fs/cache.cpp new file mode 100644 index 0000000..76e3720 --- /dev/null +++ b/s3fs/cache.cpp @@ -0,0 +1,933 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include +#include +#include + +#include "s3fs.h" +#include "s3fs_logger.h" +#include "s3fs_util.h" +#include "cache.h" +#include "autolock.h" +#include "string_util.h" + +//------------------------------------------------------------------- +// Utility +//------------------------------------------------------------------- +inline void SetStatCacheTime(struct timespec& ts) +{ + if(-1 == clock_gettime(static_cast(CLOCK_MONOTONIC_COARSE), &ts)){ + S3FS_PRN_CRIT("clock_gettime failed: %d", errno); + abort(); + } +} + +inline void InitStatCacheTime(struct timespec& ts) +{ + ts.tv_sec = 0; + ts.tv_nsec = 0; +} + +inline int CompareStatCacheTime(const struct timespec& ts1, const struct timespec& ts2) +{ + // return -1: ts1 < ts2 + // 0: ts1 == ts2 + // 1: ts1 > ts2 + if(ts1.tv_sec < ts2.tv_sec){ + return -1; + }else if(ts1.tv_sec > ts2.tv_sec){ + return 1; + }else{ + if(ts1.tv_nsec < ts2.tv_nsec){ + return -1; + }else if(ts1.tv_nsec > ts2.tv_nsec){ + return 1; + } + } + return 0; +} + +inline bool IsExpireStatCacheTime(const struct timespec& ts, const time_t& expire) +{ + struct timespec nowts; + SetStatCacheTime(nowts); + nowts.tv_sec -= expire; + + return (0 < CompareStatCacheTime(nowts, ts)); +} + +// +// For stats cache out +// +typedef std::vector statiterlist_t; + +struct sort_statiterlist{ + // ascending order + bool operator()(const stat_cache_t::iterator& src1, const stat_cache_t::iterator& src2) const + { + int result = CompareStatCacheTime(src1->second.cache_date, src2->second.cache_date); + if(0 == result){ + if(src1->second.hit_count < src2->second.hit_count){ + result = -1; + } + } + return (result < 0); + } +}; + +// +// For symbolic link cache out +// +typedef std::vector symlinkiterlist_t; + +struct sort_symlinkiterlist{ + // ascending order + bool operator()(const symlink_cache_t::iterator& src1, const symlink_cache_t::iterator& src2) const + { + int result = CompareStatCacheTime(src1->second.cache_date, src2->second.cache_date); // use the same as Stats + if(0 == result){ + if(src1->second.hit_count < src2->second.hit_count){ + result = -1; + } + } + return (result < 0); + } +}; + +//------------------------------------------------------------------- +// Static +//------------------------------------------------------------------- +StatCache StatCache::singleton; +pthread_mutex_t StatCache::stat_cache_lock; + +//------------------------------------------------------------------- +// Constructor/Destructor +//------------------------------------------------------------------- +StatCache::StatCache() : IsExpireTime(true), IsExpireIntervalType(false), ExpireTime(15 * 60), CacheSize(100000), IsCacheNoObject(true) +{ + if(this == StatCache::getStatCacheData()){ + stat_cache.clear(); + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); +#if S3FS_PTHREAD_ERRORCHECK + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK); +#endif + int result; + if(0 != (result = pthread_mutex_init(&StatCache::stat_cache_lock, &attr))){ + S3FS_PRN_CRIT("failed to init stat_cache_lock: %d", result); + abort(); + } + }else{ + abort(); + } +} + +StatCache::~StatCache() +{ + if(this == StatCache::getStatCacheData()){ + Clear(); + int result = pthread_mutex_destroy(&StatCache::stat_cache_lock); + if(result != 0){ + S3FS_PRN_CRIT("failed to destroy stat_cache_lock: %d", result); + abort(); + } + }else{ + abort(); + } +} + +//------------------------------------------------------------------- +// Methods +//------------------------------------------------------------------- +unsigned long StatCache::GetCacheSize() const +{ + return CacheSize; +} + +unsigned long StatCache::SetCacheSize(unsigned long size) +{ + unsigned long old = CacheSize; + CacheSize = size; + return old; +} + +time_t StatCache::GetExpireTime() const +{ + return (IsExpireTime ? ExpireTime : (-1)); +} + +time_t StatCache::SetExpireTime(time_t expire, bool is_interval) +{ + time_t old = ExpireTime; + ExpireTime = expire; + IsExpireTime = true; + IsExpireIntervalType = is_interval; + return old; +} + +time_t StatCache::UnsetExpireTime() +{ + time_t old = IsExpireTime ? ExpireTime : (-1); + ExpireTime = 0; + IsExpireTime = false; + IsExpireIntervalType = false; + return old; +} + +bool StatCache::SetCacheNoObject(bool flag) +{ + bool old = IsCacheNoObject; + IsCacheNoObject = flag; + return old; +} + +void StatCache::Clear() +{ + AutoLock lock(&StatCache::stat_cache_lock); + + stat_cache.clear(); + S3FS_MALLOCTRIM(0); +} + +bool StatCache::GetStat(const std::string& key, struct stat* pst, headers_t* meta, bool overcheck, const char* petag, bool* pisforce) +{ + bool is_delete_cache = false; + std::string strpath = key; + + AutoLock lock(&StatCache::stat_cache_lock); + + stat_cache_t::iterator iter = stat_cache.end(); + if(overcheck && '/' != *strpath.rbegin()){ + strpath += "/"; + iter = stat_cache.find(strpath); + } + if(iter == stat_cache.end()){ + strpath = key; + iter = stat_cache.find(strpath); + } + + if(iter != stat_cache.end()){ + stat_cache_entry* ent = &iter->second; + if(0 < ent->notruncate || !IsExpireTime || !IsExpireStatCacheTime(ent->cache_date, ExpireTime)){ + if(ent->noobjcache){ + if(!IsCacheNoObject){ + // need to delete this cache. + DelStat(strpath, AutoLock::ALREADY_LOCKED); + }else{ + // noobjcache = true means no object. + } + return false; + } + // hit without checking etag + std::string stretag; + if(petag){ + // find & check ETag + for(headers_t::iterator hiter = ent->meta.begin(); hiter != ent->meta.end(); ++hiter){ + std::string tag = lower(hiter->first); + if(tag == "etag"){ + stretag = hiter->second; + if('\0' != petag[0] && petag != stretag){ + is_delete_cache = true; + } + break; + } + } + } + if(is_delete_cache){ + // not hit by different ETag + S3FS_PRN_DBG("stat cache not hit by ETag[path=%s][time=%lld.%09ld][hit count=%lu][ETag(%s)!=(%s)]", + strpath.c_str(), static_cast(ent->cache_date.tv_sec), ent->cache_date.tv_nsec, ent->hit_count, petag ? petag : "null", stretag.c_str()); + }else{ + // hit + S3FS_PRN_DBG("stat cache hit [path=%s][time=%lld.%09ld][hit count=%lu]", + strpath.c_str(), static_cast(ent->cache_date.tv_sec), ent->cache_date.tv_nsec, ent->hit_count); + + if(pst!= nullptr){ + *pst= ent->stbuf; + } + if(meta != nullptr){ + *meta = ent->meta; + } + if(pisforce != nullptr){ + (*pisforce) = ent->isforce; + } + ent->hit_count++; + + if(IsExpireIntervalType){ + SetStatCacheTime(ent->cache_date); + } + return true; + } + + }else{ + // timeout + is_delete_cache = true; + } + } + + if(is_delete_cache){ + DelStat(strpath, AutoLock::ALREADY_LOCKED); + } + return false; +} + +bool StatCache::IsNoObjectCache(const std::string& key, bool overcheck) +{ + bool is_delete_cache = false; + std::string strpath = key; + + if(!IsCacheNoObject){ + return false; + } + + AutoLock lock(&StatCache::stat_cache_lock); + + stat_cache_t::iterator iter = stat_cache.end(); + if(overcheck && '/' != *strpath.rbegin()){ + strpath += "/"; + iter = stat_cache.find(strpath); + } + if(iter == stat_cache.end()){ + strpath = key; + iter = stat_cache.find(strpath); + } + + if(iter != stat_cache.end()) { + const stat_cache_entry* ent = &iter->second; + if(0 < ent->notruncate || !IsExpireTime || !IsExpireStatCacheTime(iter->second.cache_date, ExpireTime)){ + if(iter->second.noobjcache){ + // noobjcache = true means no object. + SetStatCacheTime((*iter).second.cache_date); + return true; + } + }else{ + // timeout + is_delete_cache = true; + } + } + + if(is_delete_cache){ + DelStat(strpath, AutoLock::ALREADY_LOCKED); + } + return false; +} + +bool StatCache::AddStat(const std::string& key, const headers_t& meta, bool forcedir, bool no_truncate) +{ + if(!no_truncate && CacheSize< 1){ + return true; + } + S3FS_PRN_INFO3("add stat cache entry[path=%s]", key.c_str()); + + AutoLock lock(&StatCache::stat_cache_lock); + + if(stat_cache.end() != stat_cache.find(key)){ + // found cache + DelStat(key.c_str(), AutoLock::ALREADY_LOCKED); + }else{ + // check: need to truncate cache + if(stat_cache.size() > CacheSize){ + // cppcheck-suppress unmatchedSuppression + // cppcheck-suppress knownConditionTrueFalse + if(!TruncateCache(AutoLock::ALREADY_LOCKED)){ + return false; + } + } + } + + // make new + stat_cache_entry ent; + if(!convert_header_to_stat(key.c_str(), meta, &ent.stbuf, forcedir)){ + return false; + } + ent.hit_count = 0; + ent.isforce = forcedir; + ent.noobjcache = false; + ent.notruncate = (no_truncate ? 1L : 0L); + ent.meta.clear(); + SetStatCacheTime(ent.cache_date); // Set time. + //copy only some keys + for(headers_t::const_iterator iter = meta.begin(); iter != meta.end(); ++iter){ + std::string tag = lower(iter->first); + std::string value = iter->second; + if(tag == "content-type"){ + ent.meta[iter->first] = value; + }else if(tag == "content-length"){ + ent.meta[iter->first] = value; + }else if(tag == "etag"){ + ent.meta[iter->first] = value; + }else if(tag == "last-modified"){ + ent.meta[iter->first] = value; + }else if(is_prefix(tag.c_str(), "x-amz")){ + ent.meta[tag] = value; // key is lower case for "x-amz" + } + } + + const auto& value = stat_cache[key] = std::move(ent); + + // check symbolic link cache + if(!S_ISLNK(value.stbuf.st_mode)){ + if(symlink_cache.end() != symlink_cache.find(key)){ + // if symbolic link cache has key, thus remove it. + DelSymlink(key.c_str(), AutoLock::ALREADY_LOCKED); + } + } + + // If no_truncate flag is set, set file name to notruncate_file_cache + // + if(no_truncate){ + AddNotruncateCache(key); + } + + return true; +} + +// [NOTE] +// Updates only meta data if cached data exists. +// And when these are updated, it also updates the cache time. +// +// Since the file mode may change while the file is open, it is +// updated as well. +// +bool StatCache::UpdateMetaStats(const std::string& key, const headers_t& meta) +{ + if(CacheSize < 1){ + return true; + } + S3FS_PRN_INFO3("update stat cache entry[path=%s]", key.c_str()); + + AutoLock lock(&StatCache::stat_cache_lock); + stat_cache_t::iterator iter = stat_cache.find(key); + if(stat_cache.end() == iter){ + return true; + } + stat_cache_entry* ent = &iter->second; + + // update only meta keys + for(headers_t::const_iterator metaiter = meta.begin(); metaiter != meta.end(); ++metaiter){ + std::string tag = lower(metaiter->first); + std::string value = metaiter->second; + if(tag == "content-type"){ + ent->meta[metaiter->first] = value; + }else if(tag == "content-length"){ + ent->meta[metaiter->first] = value; + }else if(tag == "etag"){ + ent->meta[metaiter->first] = value; + }else if(tag == "last-modified"){ + ent->meta[metaiter->first] = value; + }else if(is_prefix(tag.c_str(), "x-amz")){ + ent->meta[tag] = value; // key is lower case for "x-amz" + } + } + + // Update time. + SetStatCacheTime(ent->cache_date); + + // Update only mode + ent->stbuf.st_mode = get_mode(meta, key); + + return true; +} + +bool StatCache::AddNoObjectCache(const std::string& key) +{ + if(!IsCacheNoObject){ + return true; // pretend successful + } + if(CacheSize < 1){ + return true; + } + S3FS_PRN_INFO3("add no object cache entry[path=%s]", key.c_str()); + + AutoLock lock(&StatCache::stat_cache_lock); + + if(stat_cache.end() != stat_cache.find(key)){ + // found + DelStat(key.c_str(), AutoLock::ALREADY_LOCKED); + }else{ + // check: need to truncate cache + if(stat_cache.size() > CacheSize){ + // cppcheck-suppress unmatchedSuppression + // cppcheck-suppress knownConditionTrueFalse + if(!TruncateCache(AutoLock::ALREADY_LOCKED)){ + return false; + } + } + } + + // make new + stat_cache_entry ent; + memset(&ent.stbuf, 0, sizeof(struct stat)); + ent.hit_count = 0; + ent.isforce = false; + ent.noobjcache = true; + ent.notruncate = 0L; + ent.meta.clear(); + SetStatCacheTime(ent.cache_date); // Set time. + + stat_cache[key] = std::move(ent); + + // check symbolic link cache + if(symlink_cache.end() != symlink_cache.find(key)){ + // if symbolic link cache has key, thus remove it. + DelSymlink(key.c_str(), AutoLock::ALREADY_LOCKED); + } + return true; +} + +void StatCache::ChangeNoTruncateFlag(const std::string& key, bool no_truncate) +{ + AutoLock lock(&StatCache::stat_cache_lock); + stat_cache_t::iterator iter = stat_cache.find(key); + + if(stat_cache.end() != iter){ + stat_cache_entry* ent = &iter->second; + if(no_truncate){ + if(0L == ent->notruncate){ + // need to add no truncate cache. + AddNotruncateCache(key); + } + ++(ent->notruncate); + }else{ + if(0L < ent->notruncate){ + --(ent->notruncate); + if(0L == ent->notruncate){ + // need to delete from no truncate cache. + DelNotruncateCache(key); + } + } + } + } +} + +bool StatCache::TruncateCache(AutoLock::Type locktype) +{ + AutoLock lock(&StatCache::stat_cache_lock, locktype); + + if(stat_cache.empty()){ + return true; + } + + // 1) erase over expire time + if(IsExpireTime){ + for(stat_cache_t::iterator iter = stat_cache.begin(); iter != stat_cache.end(); ){ + const stat_cache_entry* entry = &iter->second; + if(0L == entry->notruncate && IsExpireStatCacheTime(entry->cache_date, ExpireTime)){ + iter = stat_cache.erase(iter); + }else{ + ++iter; + } + } + } + + // 2) check stat cache count + if(stat_cache.size() < CacheSize){ + return true; + } + + // 3) erase from the old cache in order + size_t erase_count= stat_cache.size() - CacheSize + 1; + statiterlist_t erase_iters; + for(stat_cache_t::iterator iter = stat_cache.begin(); iter != stat_cache.end() && 0 < erase_count; ++iter){ + // check no truncate + const stat_cache_entry* ent = &iter->second; + if(0L < ent->notruncate){ + // skip for no truncate entry and keep extra counts for this entity. + if(0 < erase_count){ + --erase_count; // decrement + } + }else{ + // iter is not have notruncate flag + erase_iters.push_back(iter); + } + if(erase_count < erase_iters.size()){ + std::sort(erase_iters.begin(), erase_iters.end(), sort_statiterlist()); + while(erase_count < erase_iters.size()){ + erase_iters.pop_back(); + } + } + } + for(statiterlist_t::iterator iiter = erase_iters.begin(); iiter != erase_iters.end(); ++iiter){ + stat_cache_t::iterator siter = *iiter; + + S3FS_PRN_DBG("truncate stat cache[path=%s]", siter->first.c_str()); + stat_cache.erase(siter); + } + S3FS_MALLOCTRIM(0); + + return true; +} + +bool StatCache::DelStat(const char* key, AutoLock::Type locktype) +{ + if(!key){ + return false; + } + S3FS_PRN_INFO3("delete stat cache entry[path=%s]", key); + + AutoLock lock(&StatCache::stat_cache_lock, locktype); + + stat_cache_t::iterator iter; + if(stat_cache.end() != (iter = stat_cache.find(key))){ + stat_cache.erase(iter); + DelNotruncateCache(key); + } + if(0 < strlen(key) && 0 != strcmp(key, "/")){ + std::string strpath = key; + if('/' == *strpath.rbegin()){ + // If there is "path" cache, delete it. + strpath.erase(strpath.length() - 1); + }else{ + // If there is "path/" cache, delete it. + strpath += "/"; + } + if(stat_cache.end() != (iter = stat_cache.find(strpath))){ + stat_cache.erase(iter); + DelNotruncateCache(strpath); + } + } + S3FS_MALLOCTRIM(0); + + return true; +} + +bool StatCache::GetSymlink(const std::string& key, std::string& value) +{ + bool is_delete_cache = false; + const std::string& strpath = key; + + AutoLock lock(&StatCache::stat_cache_lock); + + symlink_cache_t::iterator iter = symlink_cache.find(strpath); + if(iter != symlink_cache.end()){ + symlink_cache_entry* ent = &iter->second; + if(!IsExpireTime || !IsExpireStatCacheTime(ent->cache_date, ExpireTime)){ // use the same as Stats + // found + S3FS_PRN_DBG("symbolic link cache hit [path=%s][time=%lld.%09ld][hit count=%lu]", + strpath.c_str(), static_cast(ent->cache_date.tv_sec), ent->cache_date.tv_nsec, ent->hit_count); + + value = ent->link; + + ent->hit_count++; + if(IsExpireIntervalType){ + SetStatCacheTime(ent->cache_date); + } + return true; + }else{ + // timeout + is_delete_cache = true; + } + } + + if(is_delete_cache){ + DelSymlink(strpath.c_str(), AutoLock::ALREADY_LOCKED); + } + return false; +} + +bool StatCache::AddSymlink(const std::string& key, const std::string& value) +{ + if(CacheSize< 1){ + return true; + } + S3FS_PRN_INFO3("add symbolic link cache entry[path=%s, value=%s]", key.c_str(), value.c_str()); + + AutoLock lock(&StatCache::stat_cache_lock); + + if(symlink_cache.end() != symlink_cache.find(key)){ + // found + DelSymlink(key.c_str(), AutoLock::ALREADY_LOCKED); + }else{ + // check: need to truncate cache + if(symlink_cache.size() > CacheSize){ + // cppcheck-suppress unmatchedSuppression + // cppcheck-suppress knownConditionTrueFalse + if(!TruncateSymlink(AutoLock::ALREADY_LOCKED)){ + return false; + } + } + } + + // make new + symlink_cache_entry ent; + ent.link = value; + ent.hit_count = 0; + SetStatCacheTime(ent.cache_date); // Set time(use the same as Stats). + + symlink_cache[key] = std::move(ent); + + return true; +} + +bool StatCache::TruncateSymlink(AutoLock::Type locktype) +{ + AutoLock lock(&StatCache::stat_cache_lock, locktype); + + if(symlink_cache.empty()){ + return true; + } + + // 1) erase over expire time + if(IsExpireTime){ + for(symlink_cache_t::iterator iter = symlink_cache.begin(); iter != symlink_cache.end(); ){ + const symlink_cache_entry* entry = &iter->second; + if(IsExpireStatCacheTime(entry->cache_date, ExpireTime)){ // use the same as Stats + iter = symlink_cache.erase(iter); + }else{ + ++iter; + } + } + } + + // 2) check stat cache count + if(symlink_cache.size() < CacheSize){ + return true; + } + + // 3) erase from the old cache in order + size_t erase_count= symlink_cache.size() - CacheSize + 1; + symlinkiterlist_t erase_iters; + for(symlink_cache_t::iterator iter = symlink_cache.begin(); iter != symlink_cache.end(); ++iter){ + erase_iters.push_back(iter); + sort(erase_iters.begin(), erase_iters.end(), sort_symlinkiterlist()); + if(erase_count < erase_iters.size()){ + erase_iters.pop_back(); + } + } + for(symlinkiterlist_t::iterator iiter = erase_iters.begin(); iiter != erase_iters.end(); ++iiter){ + symlink_cache_t::iterator siter = *iiter; + + S3FS_PRN_DBG("truncate symbolic link cache[path=%s]", siter->first.c_str()); + symlink_cache.erase(siter); + } + S3FS_MALLOCTRIM(0); + + return true; +} + +bool StatCache::DelSymlink(const char* key, AutoLock::Type locktype) +{ + if(!key){ + return false; + } + S3FS_PRN_INFO3("delete symbolic link cache entry[path=%s]", key); + + AutoLock lock(&StatCache::stat_cache_lock, locktype); + + symlink_cache_t::iterator iter; + if(symlink_cache.end() != (iter = symlink_cache.find(key))){ + symlink_cache.erase(iter); + } + S3FS_MALLOCTRIM(0); + + return true; +} + +// [NOTE] +// Need to lock StatCache::stat_cache_lock before calling this method. +// +bool StatCache::AddNotruncateCache(const std::string& key) +{ + if(key.empty() || '/' == *key.rbegin()){ + return false; + } + + std::string parentdir = mydirname(key); + std::string filename = mybasename(key); + if(parentdir.empty() || filename.empty()){ + return false; + } + parentdir += '/'; // directory path must be '/' termination. + + notruncate_dir_map_t::iterator iter = notruncate_file_cache.find(parentdir); + if(iter == notruncate_file_cache.end()){ + // add new list + notruncate_filelist_t list; + list.push_back(filename); + notruncate_file_cache[parentdir] = list; + }else{ + // add filename to existed list + notruncate_filelist_t& filelist = iter->second; + notruncate_filelist_t::const_iterator fiter = std::find(filelist.begin(), filelist.end(), filename); + if(fiter == filelist.end()){ + filelist.push_back(filename); + } + } + return true; +} + +// [NOTE] +// Need to lock StatCache::stat_cache_lock before calling this method. +// +bool StatCache::DelNotruncateCache(const std::string& key) +{ + if(key.empty() || '/' == *key.rbegin()){ + return false; + } + + std::string parentdir = mydirname(key); + std::string filename = mybasename(key); + if(parentdir.empty() || filename.empty()){ + return false; + } + parentdir += '/'; // directory path must be '/' termination. + + notruncate_dir_map_t::iterator iter = notruncate_file_cache.find(parentdir); + if(iter != notruncate_file_cache.end()){ + // found directory in map + notruncate_filelist_t& filelist = iter->second; + notruncate_filelist_t::iterator fiter = std::find(filelist.begin(), filelist.end(), filename); + if(fiter != filelist.end()){ + // found filename in directory file list + filelist.erase(fiter); + if(filelist.empty()){ + notruncate_file_cache.erase(parentdir); + } + } + } + return true; +} + +// [Background] +// When s3fs creates a new file, the file does not exist until the file contents +// are uploaded.(because it doesn't create a 0 byte file) +// From the time this file is created(opened) until it is uploaded(flush), it +// will have a Stat cache with the No truncate flag added. +// This avoids file not existing errors in operations such as chmod and utimens +// that occur in the short period before file upload. +// Besides this, we also need to support readdir(list_bucket), this method is +// called to maintain the cache for readdir and return its value. +// +// [NOTE] +// Add the file names under parentdir to the list. +// However, if the same file name exists in the list, it will not be added. +// parentdir must be terminated with a '/'. +// +bool StatCache::GetNotruncateCache(const std::string& parentdir, notruncate_filelist_t& list) +{ + if(parentdir.empty()){ + return false; + } + + std::string dirpath = parentdir; + if('/' != *dirpath.rbegin()){ + dirpath += '/'; + } + + AutoLock lock(&StatCache::stat_cache_lock); + + notruncate_dir_map_t::iterator iter = notruncate_file_cache.find(dirpath); + if(iter == notruncate_file_cache.end()){ + // not found directory map + return true; + } + + // found directory in map + const notruncate_filelist_t& filelist = iter->second; + for(notruncate_filelist_t::const_iterator fiter = filelist.begin(); fiter != filelist.end(); ++fiter){ + if(list.end() == std::find(list.begin(), list.end(), *fiter)){ + // found notuncate file that does not exist in the list, so add it. + list.push_back(*fiter); + } + } + return true; +} + +//------------------------------------------------------------------- +// Functions +//------------------------------------------------------------------- +bool convert_header_to_stat(const char* path, const headers_t& meta, struct stat* pst, bool forcedir) +{ + if(!path || !pst){ + return false; + } + memset(pst, 0, sizeof(struct stat)); + + pst->st_nlink = 1; // see fuse FAQ + + // mode + pst->st_mode = get_mode(meta, path, true, forcedir); + + // blocks + if(S_ISREG(pst->st_mode)){ + pst->st_blocks = get_blocks(pst->st_size); + } + pst->st_blksize = 4096; + + // mtime + struct timespec mtime = get_mtime(meta); + if(pst->st_mtime < 0){ + pst->st_mtime = 0L; + }else{ + if(mtime.tv_sec < 0){ + mtime.tv_sec = 0; + mtime.tv_nsec = 0; + } + set_timespec_to_stat(*pst, stat_time_type::MTIME, mtime); + } + + // ctime + struct timespec ctime = get_ctime(meta); + if(pst->st_ctime < 0){ + pst->st_ctime = 0L; + }else{ + if(ctime.tv_sec < 0){ + ctime.tv_sec = 0; + ctime.tv_nsec = 0; + } + set_timespec_to_stat(*pst, stat_time_type::CTIME, ctime); + } + + // atime + struct timespec atime = get_atime(meta); + if(pst->st_atime < 0){ + pst->st_atime = 0L; + }else{ + if(atime.tv_sec < 0){ + atime.tv_sec = 0; + atime.tv_nsec = 0; + } + set_timespec_to_stat(*pst, stat_time_type::ATIME, atime); + } + + // size + if(S_ISDIR(pst->st_mode)){ + pst->st_size = 4096; + }else{ + pst->st_size = get_size(meta); + } + + // uid/gid + pst->st_uid = get_uid(meta); + pst->st_gid = get_gid(meta); + + return true; +} + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/cache.h b/s3fs/cache.h new file mode 100644 index 0000000..5157f74 --- /dev/null +++ b/s3fs/cache.h @@ -0,0 +1,214 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef S3FS_CACHE_H_ +#define S3FS_CACHE_H_ + +#include + +#include "autolock.h" +#include "metaheader.h" + +//------------------------------------------------------------------- +// Structure +//------------------------------------------------------------------- +// +// Struct for stats cache +// +struct stat_cache_entry { + struct stat stbuf; + unsigned long hit_count; + struct timespec cache_date; + headers_t meta; + bool isforce; + bool noobjcache; // Flag: cache is no object for no listing. + unsigned long notruncate; // 0<: not remove automatically at checking truncate + + stat_cache_entry() : hit_count(0), isforce(false), noobjcache(false), notruncate(0L) + { + memset(&stbuf, 0, sizeof(struct stat)); + cache_date.tv_sec = 0; + cache_date.tv_nsec = 0; + meta.clear(); + } +}; + +typedef std::map stat_cache_t; // key=path + +// +// Struct for symbolic link cache +// +struct symlink_cache_entry { + std::string link; + unsigned long hit_count; + struct timespec cache_date; // The function that operates timespec uses the same as Stats + + symlink_cache_entry() : link(""), hit_count(0) + { + cache_date.tv_sec = 0; + cache_date.tv_nsec = 0; + } +}; + +typedef std::map symlink_cache_t; + +// +// Typedefs for No truncate file name cache +// +typedef std::vector notruncate_filelist_t; // untruncated file name list in dir +typedef std::map notruncate_dir_map_t; // key is parent dir path + +//------------------------------------------------------------------- +// Class StatCache +//------------------------------------------------------------------- +// [NOTE] About Symbolic link cache +// The Stats cache class now also has a symbolic link cache. +// It is possible to take out the Symbolic link cache in another class, +// but the cache out etc. should be synchronized with the Stats cache +// and implemented in this class. +// Symbolic link cache size and timeout use the same settings as Stats +// cache. This simplifies user configuration, and from a user perspective, +// the symbolic link cache appears to be included in the Stats cache. +// +class StatCache +{ + private: + static StatCache singleton; + static pthread_mutex_t stat_cache_lock; + stat_cache_t stat_cache; + bool IsExpireTime; + bool IsExpireIntervalType; // if this flag is true, cache data is updated at last access time. + time_t ExpireTime; + unsigned long CacheSize; + bool IsCacheNoObject; + symlink_cache_t symlink_cache; + notruncate_dir_map_t notruncate_file_cache; + + private: + StatCache(); + ~StatCache(); + + void Clear(); + bool GetStat(const std::string& key, struct stat* pst, headers_t* meta, bool overcheck, const char* petag, bool* pisforce); + // Truncate stat cache + bool TruncateCache(AutoLock::Type locktype = AutoLock::NONE); + // Truncate symbolic link cache + bool TruncateSymlink(AutoLock::Type locktype = AutoLock::NONE); + + bool AddNotruncateCache(const std::string& key); + bool DelNotruncateCache(const std::string& key); + + public: + // Reference singleton + static StatCache* getStatCacheData() + { + return &singleton; + } + + // Attribute + unsigned long GetCacheSize() const; + unsigned long SetCacheSize(unsigned long size); + time_t GetExpireTime() const; + time_t SetExpireTime(time_t expire, bool is_interval = false); + time_t UnsetExpireTime(); + bool SetCacheNoObject(bool flag); + bool EnableCacheNoObject() + { + return SetCacheNoObject(true); + } + bool DisableCacheNoObject() + { + return SetCacheNoObject(false); + } + bool GetCacheNoObject() const + { + return IsCacheNoObject; + } + + // Get stat cache + bool GetStat(const std::string& key, struct stat* pst, headers_t* meta, bool overcheck = true, bool* pisforce = nullptr) + { + return GetStat(key, pst, meta, overcheck, nullptr, pisforce); + } + bool GetStat(const std::string& key, struct stat* pst, bool overcheck = true) + { + return GetStat(key, pst, nullptr, overcheck, nullptr, nullptr); + } + bool GetStat(const std::string& key, headers_t* meta, bool overcheck = true) + { + return GetStat(key, nullptr, meta, overcheck, nullptr, nullptr); + } + bool HasStat(const std::string& key, bool overcheck = true) + { + return GetStat(key, nullptr, nullptr, overcheck, nullptr, nullptr); + } + bool HasStat(const std::string& key, const char* etag, bool overcheck = true) + { + return GetStat(key, nullptr, nullptr, overcheck, etag, nullptr); + } + bool HasStat(const std::string& key, struct stat* pst, const char* etag) + { + return GetStat(key, pst, nullptr, true, etag, nullptr); + } + + // Cache For no object + bool IsNoObjectCache(const std::string& key, bool overcheck = true); + bool AddNoObjectCache(const std::string& key); + + // Add stat cache + bool AddStat(const std::string& key, const headers_t& meta, bool forcedir = false, bool no_truncate = false); + + // Update meta stats + bool UpdateMetaStats(const std::string& key, const headers_t& meta); + + // Change no truncate flag + void ChangeNoTruncateFlag(const std::string& key, bool no_truncate); + + // Delete stat cache + bool DelStat(const char* key, AutoLock::Type locktype = AutoLock::NONE); + bool DelStat(const std::string& key, AutoLock::Type locktype = AutoLock::NONE) + { + return DelStat(key.c_str(), locktype); + } + + // Cache for symbolic link + bool GetSymlink(const std::string& key, std::string& value); + bool AddSymlink(const std::string& key, const std::string& value); + bool DelSymlink(const char* key, AutoLock::Type locktype = AutoLock::NONE); + + // Cache for Notruncate file + bool GetNotruncateCache(const std::string& parentdir, notruncate_filelist_t& list); +}; + +//------------------------------------------------------------------- +// Functions +//------------------------------------------------------------------- +bool convert_header_to_stat(const char* path, const headers_t& meta, struct stat* pst, bool forcedir = false); + +#endif // S3FS_CACHE_H_ + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/common.h b/s3fs/common.h new file mode 100644 index 0000000..6f49c98 --- /dev/null +++ b/s3fs/common.h @@ -0,0 +1,68 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef S3FS_COMMON_H_ +#define S3FS_COMMON_H_ + +#include + +#include "config.h" +#include "types.h" +#include "hybridcache_accessor_4_s3fs.h" + +//------------------------------------------------------------------- +// Global variables +//------------------------------------------------------------------- +// TODO: namespace these +static constexpr int64_t FIVE_GB = 5LL * 1024LL * 1024LL * 1024LL; +static constexpr off_t MIN_MULTIPART_SIZE = 5 * 1024 * 1024; +static constexpr int NEW_CACHE_FAKE_FD = -2; + +extern bool foreground; +extern bool nomultipart; +extern bool pathrequeststyle; +extern bool complement_stat; +extern bool noxmlns; +extern bool use_newcache; +extern std::string program_name; +extern std::string service_path; +extern std::string s3host; +extern std::string mount_prefix; +extern std::string endpoint; +extern std::string cipher_suites; +extern std::string instance_name; + +extern std::shared_ptr accessor; + +//------------------------------------------------------------------- +// For weak attribute +//------------------------------------------------------------------- +#define S3FS_FUNCATTR_WEAK __attribute__ ((weak,unused)) + +#endif // S3FS_COMMON_H_ + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/common_auth.cpp b/s3fs/common_auth.cpp new file mode 100644 index 0000000..b42a39f --- /dev/null +++ b/s3fs/common_auth.cpp @@ -0,0 +1,71 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include + +#include "s3fs_auth.h" +#include "string_util.h" + +//------------------------------------------------------------------- +// Utility Function +//------------------------------------------------------------------- +std::string s3fs_get_content_md5(int fd) +{ + md5_t md5; + if(!s3fs_md5_fd(fd, 0, -1, &md5)){ + // TODO: better return value? + return ""; + } + return s3fs_base64(md5.data(), md5.size()); +} + +std::string s3fs_sha256_hex_fd(int fd, off_t start, off_t size) +{ + sha256_t sha256; + + if(!s3fs_sha256_fd(fd, start, size, &sha256)){ + // TODO: better return value? + return ""; + } + + std::string sha256hex = s3fs_hex_lower(sha256.data(), sha256.size()); + + return sha256hex; +} + + +std::string s3fs_get_content_md5(off_t fsize, char* buf) { + md5_t md5; + if(!s3fs_md5(reinterpret_cast(buf), fsize, &md5)){ + // TODO: better return value? + return ""; + } + return s3fs_base64(md5.data(), md5.size()); +} + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/config.h b/s3fs/config.h new file mode 100644 index 0000000..808fe47 --- /dev/null +++ b/s3fs/config.h @@ -0,0 +1,92 @@ +/* config.h. Generated from config.h.in by configure. */ +/* config.h.in. Generated from configure.ac by autoheader. */ + +/* short commit hash value on github */ +#define COMMIT_HASH_VAL "70a30d6" + +/* Define to 1 if you have the header file. */ +#define HAVE_ATTR_XATTR_H 1 + +/* Define to 1 if you have the `clock_gettime' function. */ +#define HAVE_CLOCK_GETTIME 1 + +/* Define to 1 if libcurl has CURLOPT_KEEP_SENDING_ON_ERROR CURLoption */ +#define HAVE_CURLOPT_KEEP_SENDING_ON_ERROR 1 + +/* Define to 1 if libcurl has CURLOPT_SSL_ENABLE_ALPN CURLoption */ +#define HAVE_CURLOPT_SSL_ENABLE_ALPN 1 + +/* Define to 1 if libcurl has CURLOPT_TCP_KEEPALIVE CURLoption */ +#define HAVE_CURLOPT_TCP_KEEPALIVE 1 + +/* Define to 1 if you have the `fallocate' function. */ +#define HAVE_FALLOCATE 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_INTTYPES_H 1 + +/* Define to 1 if you have the `dl' library (-ldl). */ +#define HAVE_LIBDL 1 + +/* Define to 1 if you have the `malloc_trim' function. */ +#define HAVE_MALLOC_TRIM 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_MEMORY_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_STDINT_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_STDLIB_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_STRINGS_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_STRING_H 1 + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_SYS_EXTATTR_H */ + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_STAT_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_TYPES_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_XATTR_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_UNISTD_H 1 + +/* Name of package */ +#define PACKAGE "s3fs" + +/* Define to the address where bug reports for this package should be sent. */ +#define PACKAGE_BUGREPORT "" + +/* Define to the full name of this package. */ +#define PACKAGE_NAME "s3fs" + +/* Define to the full name and version of this package. */ +#define PACKAGE_STRING "s3fs 1.94" + +/* Define to the one symbol short name of this package. */ +#define PACKAGE_TARNAME "s3fs" + +/* Define to the home page for this package. */ +#define PACKAGE_URL "" + +/* Define to the version of this package. */ +#define PACKAGE_VERSION "1.94" + +/* Define if you have PTHREAD_MUTEX_RECURSIVE_NP */ +#define S3FS_MUTEX_RECURSIVE PTHREAD_MUTEX_RECURSIVE + +/* Define to 1 if you have the ANSI C header files. */ +#define STDC_HEADERS 1 + +/* Version number of package */ +#define VERSION "1.94" diff --git a/s3fs/curl.cpp b/s3fs/curl.cpp new file mode 100644 index 0000000..6f76185 --- /dev/null +++ b/s3fs/curl.cpp @@ -0,0 +1,4576 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.h" +#include "s3fs.h" +#include "s3fs_logger.h" +#include "curl.h" +#include "curl_multi.h" +#include "curl_util.h" +#include "s3fs_auth.h" +#include "autolock.h" +#include "curl_handlerpool.h" +#include "s3fs_cred.h" +#include "s3fs_util.h" +#include "string_util.h" +#include "addhead.h" + +//------------------------------------------------------------------- +// Symbols +//------------------------------------------------------------------- +static constexpr char EMPTY_PAYLOAD_HASH[] = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; +static constexpr char EMPTY_MD5_BASE64_HASH[] = "1B2M2Y8AsgTpgAmY7PhCfg=="; + +//------------------------------------------------------------------- +// Class S3fsCurl +//------------------------------------------------------------------- +static constexpr int MULTIPART_SIZE = 10 * 1024 * 1024; +static constexpr int GET_OBJECT_RESPONSE_LIMIT = 1024; + +// [NOTE] about default mime.types file +// If no mime.types file is specified in the mime option, s3fs +// will look for /etc/mime.types on all operating systems and +// load mime information. +// However, in the case of macOS, when this file does not exist, +// it tries to detect the /etc/apache2/mime.types file. +// The reason for this is that apache2 is preinstalled on macOS, +// and the mime.types file is expected to exist in this path. +// If the mime.types file is not found, s3fs will exit with an +// error. +// +static constexpr char DEFAULT_MIME_FILE[] = "/etc/mime.types"; +static constexpr char SPECIAL_DARWIN_MIME_FILE[] = "/etc/apache2/mime.types"; + +// [NOTICE] +// This symbol is for libcurl under 7.23.0 +#ifndef CURLSHE_NOT_BUILT_IN +#define CURLSHE_NOT_BUILT_IN 5 +#endif + +#if LIBCURL_VERSION_NUM >= 0x073100 +#define S3FS_CURLOPT_XFERINFOFUNCTION CURLOPT_XFERINFOFUNCTION +#else +#define S3FS_CURLOPT_XFERINFOFUNCTION CURLOPT_PROGRESSFUNCTION +#endif + +//------------------------------------------------------------------- +// Class S3fsCurl +//------------------------------------------------------------------- +pthread_mutex_t S3fsCurl::curl_warnings_lock; +pthread_mutex_t S3fsCurl::curl_handles_lock; +S3fsCurl::callback_locks_t S3fsCurl::callback_locks; +bool S3fsCurl::is_initglobal_done = false; +CurlHandlerPool* S3fsCurl::sCurlPool = nullptr; +int S3fsCurl::sCurlPoolSize = 32; +CURLSH* S3fsCurl::hCurlShare = nullptr; +bool S3fsCurl::is_cert_check = true; // default +bool S3fsCurl::is_dns_cache = true; // default +bool S3fsCurl::is_ssl_session_cache= true; // default +long S3fsCurl::connect_timeout = 300; // default +time_t S3fsCurl::readwrite_timeout = 120; // default +int S3fsCurl::retries = 5; // default +bool S3fsCurl::is_public_bucket = false; +acl_t S3fsCurl::default_acl = acl_t::PRIVATE; +std::string S3fsCurl::storage_class = "STANDARD"; +sseckeylist_t S3fsCurl::sseckeys; +std::string S3fsCurl::ssekmsid; +sse_type_t S3fsCurl::ssetype = sse_type_t::SSE_DISABLE; +bool S3fsCurl::is_content_md5 = false; +bool S3fsCurl::is_verbose = false; +bool S3fsCurl::is_dump_body = false; +S3fsCred* S3fsCurl::ps3fscred = nullptr; +long S3fsCurl::ssl_verify_hostname = 1; // default(original code...) + +// protected by curl_warnings_lock +bool S3fsCurl::curl_warnings_once = false; + +// protected by curl_handles_lock +curltime_t S3fsCurl::curl_times; +curlprogress_t S3fsCurl::curl_progress; + +std::string S3fsCurl::curl_ca_bundle; +mimes_t S3fsCurl::mimeTypes; +std::string S3fsCurl::userAgent; +int S3fsCurl::max_parallel_cnt = 5; // default +int S3fsCurl::max_multireq = 20; // default +off_t S3fsCurl::multipart_size = MULTIPART_SIZE; // default +off_t S3fsCurl::multipart_copy_size = 512 * 1024 * 1024; // default +signature_type_t S3fsCurl::signature_type = signature_type_t::V2_OR_V4; // default +bool S3fsCurl::is_unsigned_payload = false; // default +bool S3fsCurl::is_ua = true; // default +bool S3fsCurl::listobjectsv2 = false; // default +bool S3fsCurl::requester_pays = false; // default +std::string S3fsCurl::proxy_url; +bool S3fsCurl::proxy_http = false; +std::string S3fsCurl::proxy_userpwd; + +//------------------------------------------------------------------- +// Class methods for S3fsCurl +//------------------------------------------------------------------- +bool S3fsCurl::InitS3fsCurl() +{ + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); +#if S3FS_PTHREAD_ERRORCHECK + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK); +#endif + if(0 != pthread_mutex_init(&S3fsCurl::curl_warnings_lock, &attr)){ + return false; + } + if(0 != pthread_mutex_init(&S3fsCurl::curl_handles_lock, &attr)){ + return false; + } + if(0 != pthread_mutex_init(&S3fsCurl::callback_locks.dns, &attr)){ + return false; + } + if(0 != pthread_mutex_init(&S3fsCurl::callback_locks.ssl_session, &attr)){ + return false; + } + if(!S3fsCurl::InitGlobalCurl()){ + return false; + } + if(!S3fsCurl::InitShareCurl()){ + return false; + } + if(!S3fsCurl::InitCryptMutex()){ + return false; + } + // [NOTE] + // sCurlPoolSize must be over parallel(or multireq) count. + // + if(sCurlPoolSize < std::max(GetMaxParallelCount(), GetMaxMultiRequest())){ + sCurlPoolSize = std::max(GetMaxParallelCount(), GetMaxMultiRequest()); + } + sCurlPool = new CurlHandlerPool(sCurlPoolSize); + if (!sCurlPool->Init()) { + return false; + } + return true; +} + +bool S3fsCurl::DestroyS3fsCurl() +{ + bool result = true; + + if(!S3fsCurl::DestroyCryptMutex()){ + result = false; + } + if(!sCurlPool->Destroy()){ + result = false; + } + delete sCurlPool; + sCurlPool = nullptr; + if(!S3fsCurl::DestroyShareCurl()){ + result = false; + } + if(!S3fsCurl::DestroyGlobalCurl()){ + result = false; + } + if(0 != pthread_mutex_destroy(&S3fsCurl::callback_locks.dns)){ + result = false; + } + if(0 != pthread_mutex_destroy(&S3fsCurl::callback_locks.ssl_session)){ + result = false; + } + if(0 != pthread_mutex_destroy(&S3fsCurl::curl_handles_lock)){ + result = false; + } + if(0 != pthread_mutex_destroy(&S3fsCurl::curl_warnings_lock)){ + result = false; + } + return result; +} + +bool S3fsCurl::InitGlobalCurl() +{ + if(S3fsCurl::is_initglobal_done){ + return false; + } + if(CURLE_OK != curl_global_init(CURL_GLOBAL_ALL)){ + S3FS_PRN_ERR("init_curl_global_all returns error."); + return false; + } + S3fsCurl::is_initglobal_done = true; + return true; +} + +bool S3fsCurl::DestroyGlobalCurl() +{ + if(!S3fsCurl::is_initglobal_done){ + return false; + } + curl_global_cleanup(); + S3fsCurl::is_initglobal_done = false; + return true; +} + +bool S3fsCurl::InitShareCurl() +{ + CURLSHcode nSHCode; + + if(!S3fsCurl::is_dns_cache && !S3fsCurl::is_ssl_session_cache){ + S3FS_PRN_INFO("Curl does not share DNS data."); + return true; + } + if(S3fsCurl::hCurlShare){ + S3FS_PRN_WARN("already initiated."); + return false; + } + if(nullptr == (S3fsCurl::hCurlShare = curl_share_init())){ + S3FS_PRN_ERR("curl_share_init failed"); + return false; + } + if(CURLSHE_OK != (nSHCode = curl_share_setopt(S3fsCurl::hCurlShare, CURLSHOPT_LOCKFUNC, S3fsCurl::LockCurlShare))){ + S3FS_PRN_ERR("curl_share_setopt(LOCKFUNC) returns %d(%s)", nSHCode, curl_share_strerror(nSHCode)); + return false; + } + if(CURLSHE_OK != (nSHCode = curl_share_setopt(S3fsCurl::hCurlShare, CURLSHOPT_UNLOCKFUNC, S3fsCurl::UnlockCurlShare))){ + S3FS_PRN_ERR("curl_share_setopt(UNLOCKFUNC) returns %d(%s)", nSHCode, curl_share_strerror(nSHCode)); + return false; + } + if(S3fsCurl::is_dns_cache){ + nSHCode = curl_share_setopt(S3fsCurl::hCurlShare, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS); + if(CURLSHE_OK != nSHCode && CURLSHE_BAD_OPTION != nSHCode && CURLSHE_NOT_BUILT_IN != nSHCode){ + S3FS_PRN_ERR("curl_share_setopt(DNS) returns %d(%s)", nSHCode, curl_share_strerror(nSHCode)); + return false; + }else if(CURLSHE_BAD_OPTION == nSHCode || CURLSHE_NOT_BUILT_IN == nSHCode){ + S3FS_PRN_WARN("curl_share_setopt(DNS) returns %d(%s), but continue without shared dns data.", nSHCode, curl_share_strerror(nSHCode)); + } + } + if(S3fsCurl::is_ssl_session_cache){ + nSHCode = curl_share_setopt(S3fsCurl::hCurlShare, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION); + if(CURLSHE_OK != nSHCode && CURLSHE_BAD_OPTION != nSHCode && CURLSHE_NOT_BUILT_IN != nSHCode){ + S3FS_PRN_ERR("curl_share_setopt(SSL SESSION) returns %d(%s)", nSHCode, curl_share_strerror(nSHCode)); + return false; + }else if(CURLSHE_BAD_OPTION == nSHCode || CURLSHE_NOT_BUILT_IN == nSHCode){ + S3FS_PRN_WARN("curl_share_setopt(SSL SESSION) returns %d(%s), but continue without shared ssl session data.", nSHCode, curl_share_strerror(nSHCode)); + } + } + if(CURLSHE_OK != (nSHCode = curl_share_setopt(S3fsCurl::hCurlShare, CURLSHOPT_USERDATA, &S3fsCurl::callback_locks))){ + S3FS_PRN_ERR("curl_share_setopt(USERDATA) returns %d(%s)", nSHCode, curl_share_strerror(nSHCode)); + return false; + } + return true; +} + +bool S3fsCurl::DestroyShareCurl() +{ + if(!S3fsCurl::hCurlShare){ + if(!S3fsCurl::is_dns_cache && !S3fsCurl::is_ssl_session_cache){ + return true; + } + S3FS_PRN_WARN("already destroy share curl."); + return false; + } + if(CURLSHE_OK != curl_share_cleanup(S3fsCurl::hCurlShare)){ + return false; + } + S3fsCurl::hCurlShare = nullptr; + return true; +} + +void S3fsCurl::LockCurlShare(CURL* handle, curl_lock_data nLockData, curl_lock_access laccess, void* useptr) +{ + if(!hCurlShare){ + return; + } + S3fsCurl::callback_locks_t* locks = static_cast(useptr); + int result; + if(CURL_LOCK_DATA_DNS == nLockData){ + if(0 != (result = pthread_mutex_lock(&locks->dns))){ + S3FS_PRN_CRIT("pthread_mutex_lock returned: %d", result); + abort(); + } + }else if(CURL_LOCK_DATA_SSL_SESSION == nLockData){ + if(0 != (result = pthread_mutex_lock(&locks->ssl_session))){ + S3FS_PRN_CRIT("pthread_mutex_lock returned: %d", result); + abort(); + } + } +} + +void S3fsCurl::UnlockCurlShare(CURL* handle, curl_lock_data nLockData, void* useptr) +{ + if(!hCurlShare){ + return; + } + S3fsCurl::callback_locks_t* locks = static_cast(useptr); + int result; + if(CURL_LOCK_DATA_DNS == nLockData){ + if(0 != (result = pthread_mutex_unlock(&locks->dns))){ + S3FS_PRN_CRIT("pthread_mutex_unlock returned: %d", result); + abort(); + } + }else if(CURL_LOCK_DATA_SSL_SESSION == nLockData){ + if(0 != (result = pthread_mutex_unlock(&locks->ssl_session))){ + S3FS_PRN_CRIT("pthread_mutex_unlock returned: %d", result); + abort(); + } + } +} + +bool S3fsCurl::InitCryptMutex() +{ + return s3fs_init_crypt_mutex(); +} + +bool S3fsCurl::DestroyCryptMutex() +{ + return s3fs_destroy_crypt_mutex(); +} + +// homegrown timeout mechanism +int S3fsCurl::CurlProgress(void *clientp, double dltotal, double dlnow, double ultotal, double ulnow) +{ + CURL* curl = static_cast(clientp); + time_t now = time(nullptr); + progress_t p(dlnow, ulnow); + + AutoLock lock(&S3fsCurl::curl_handles_lock); + + // any progress? + if(p != S3fsCurl::curl_progress[curl]){ + // yes! + S3fsCurl::curl_times[curl] = now; + S3fsCurl::curl_progress[curl] = p; + }else{ + // timeout? + if(now - S3fsCurl::curl_times[curl] > readwrite_timeout){ + S3FS_PRN_ERR("timeout now: %lld, curl_times[curl]: %lld, readwrite_timeout: %lld", + static_cast(now), static_cast((S3fsCurl::curl_times[curl])), static_cast(readwrite_timeout)); + return CURLE_ABORTED_BY_CALLBACK; + } + } + return 0; +} + +bool S3fsCurl::InitCredentialObject(S3fsCred* pcredobj) +{ + // Set the only Credential object + if(!pcredobj || S3fsCurl::ps3fscred){ + S3FS_PRN_ERR("Unable to set the only Credential object."); + return false; + } + S3fsCurl::ps3fscred = pcredobj; + + return true; +} + +bool S3fsCurl::InitMimeType(const std::string& strFile) +{ + std::string MimeFile; + if(!strFile.empty()){ + MimeFile = strFile; + }else{ + // search default mime.types + std::string errPaths = DEFAULT_MIME_FILE; + struct stat st; + if(0 == stat(DEFAULT_MIME_FILE, &st)){ + MimeFile = DEFAULT_MIME_FILE; + }else if(compare_sysname("Darwin")){ + // for macOS, search another default file. + if(0 == stat(SPECIAL_DARWIN_MIME_FILE, &st)){ + MimeFile = SPECIAL_DARWIN_MIME_FILE; + }else{ + errPaths += " and "; + errPaths += SPECIAL_DARWIN_MIME_FILE; + } + } + if(MimeFile.empty()){ + S3FS_PRN_WARN("Could not find mime.types files, you have to create file(%s) or specify mime option for existing mime.types file.", errPaths.c_str()); + return false; + } + } + S3FS_PRN_DBG("Try to load mime types from %s file.", MimeFile.c_str()); + + std::ifstream MT(MimeFile.c_str()); + if(MT.good()){ + S3FS_PRN_DBG("The old mime types are cleared to load new mime types."); + S3fsCurl::mimeTypes.clear(); + std::string line; + + while(getline(MT, line)){ + if(line.empty()){ + continue; + } + if(line[0]=='#'){ + continue; + } + + std::istringstream tmp(line); + std::string mimeType; + tmp >> mimeType; + std::string ext; + while(tmp >> ext){ + S3fsCurl::mimeTypes[ext] = mimeType; + } + } + S3FS_PRN_INIT_INFO("Loaded mime information from %s", MimeFile.c_str()); + }else{ + S3FS_PRN_WARN("Could not load mime types from %s, please check the existence and permissions of this file.", MimeFile.c_str()); + return false; + } + return true; +} + +void S3fsCurl::InitUserAgent() +{ + if(S3fsCurl::userAgent.empty()){ + S3fsCurl::userAgent = "s3fs/"; + S3fsCurl::userAgent += VERSION; + S3fsCurl::userAgent += " (commit hash "; + S3fsCurl::userAgent += COMMIT_HASH_VAL; + S3fsCurl::userAgent += "; "; + S3fsCurl::userAgent += s3fs_crypt_lib_name(); + S3fsCurl::userAgent += ")"; + S3fsCurl::userAgent += instance_name; + } +} + +// +// @param s e.g., "index.html" +// @return e.g., "text/html" +// +std::string S3fsCurl::LookupMimeType(const std::string& name) +{ + if(!name.empty() && name[name.size() - 1] == '/'){ + return "application/x-directory"; + } + + std::string result("application/octet-stream"); + std::string::size_type last_pos = name.find_last_of('.'); + std::string::size_type first_pos = name.find_first_of('.'); + std::string prefix, ext, ext2; + + // No dots in name, just return + if(last_pos == std::string::npos){ + return result; + } + // extract the last extension + ext = name.substr(1+last_pos, std::string::npos); + + if (last_pos != std::string::npos) { + // one dot was found, now look for another + if (first_pos != std::string::npos && first_pos < last_pos) { + prefix = name.substr(0, last_pos); + // Now get the second to last file extension + std::string::size_type next_pos = prefix.find_last_of('.'); + if (next_pos != std::string::npos) { + ext2 = prefix.substr(1+next_pos, std::string::npos); + } + } + } + + // if we get here, then we have an extension (ext) + mimes_t::const_iterator iter = S3fsCurl::mimeTypes.find(ext); + // if the last extension matches a mimeType, then return + // that mime type + if (iter != S3fsCurl::mimeTypes.end()) { + result = (*iter).second; + return result; + } + + // return with the default result if there isn't a second extension + if(first_pos == last_pos){ + return result; + } + + // Didn't find a mime-type for the first extension + // Look for second extension in mimeTypes, return if found + iter = S3fsCurl::mimeTypes.find(ext2); + if (iter != S3fsCurl::mimeTypes.end()) { + result = (*iter).second; + return result; + } + + // neither the last extension nor the second-to-last extension + // matched a mimeType, return the default mime type + return result; +} + +bool S3fsCurl::LocateBundle() +{ + // See if environment variable CURL_CA_BUNDLE is set + // if so, check it, if it is a good path, then set the + // curl_ca_bundle variable to it + if(S3fsCurl::curl_ca_bundle.empty()){ + char* CURL_CA_BUNDLE = getenv("CURL_CA_BUNDLE"); + if(CURL_CA_BUNDLE != nullptr) { + // check for existence and readability of the file + std::ifstream BF(CURL_CA_BUNDLE); + if(!BF.good()){ + S3FS_PRN_ERR("%s: file specified by CURL_CA_BUNDLE environment variable is not readable", program_name.c_str()); + return false; + } + BF.close(); + S3fsCurl::curl_ca_bundle = CURL_CA_BUNDLE; + return true; + } + }else{ + // Already set ca bundle variable + return true; + } + + // not set via environment variable, look in likely locations + + /////////////////////////////////////////// + // following comment from curl's (7.21.2) acinclude.m4 file + /////////////////////////////////////////// + // dnl CURL_CHECK_CA_BUNDLE + // dnl ------------------------------------------------- + // dnl Check if a default ca-bundle should be used + // dnl + // dnl regarding the paths this will scan: + // dnl /etc/ssl/certs/ca-certificates.crt Debian systems + // dnl /etc/pki/tls/certs/ca-bundle.crt Redhat and Mandriva + // dnl /usr/share/ssl/certs/ca-bundle.crt old(er) Redhat + // dnl /usr/local/share/certs/ca-root.crt FreeBSD + // dnl /etc/ssl/cert.pem OpenBSD + // dnl /etc/ssl/certs/ (ca path) SUSE + /////////////////////////////////////////// + // Within CURL the above path should have been checked + // according to the OS. Thus, although we do not need + // to check files here, we will only examine some files. + // + std::ifstream BF("/etc/pki/tls/certs/ca-bundle.crt"); + if(BF.good()){ + BF.close(); + S3fsCurl::curl_ca_bundle = "/etc/pki/tls/certs/ca-bundle.crt"; + }else{ + BF.open("/etc/ssl/certs/ca-certificates.crt"); + if(BF.good()){ + BF.close(); + S3fsCurl::curl_ca_bundle = "/etc/ssl/certs/ca-certificates.crt"; + }else{ + BF.open("/usr/share/ssl/certs/ca-bundle.crt"); + if(BF.good()){ + BF.close(); + S3fsCurl::curl_ca_bundle = "/usr/share/ssl/certs/ca-bundle.crt"; + }else{ + BF.open("/usr/local/share/certs/ca-root.crt"); + if(BF.good()){ + BF.close(); + S3fsCurl::curl_ca_bundle = "/usr/share/ssl/certs/ca-bundle.crt"; + }else{ + S3FS_PRN_ERR("%s: /.../ca-bundle.crt is not readable", program_name.c_str()); + return false; + } + } + } + } + return true; +} + +size_t S3fsCurl::WriteMemoryCallback(void* ptr, size_t blockSize, size_t numBlocks, void* data) +{ + std::string* body = static_cast(data); + body->append(static_cast(ptr), blockSize * numBlocks); + return (blockSize * numBlocks); +} + +size_t S3fsCurl::ReadCallback(void* ptr, size_t size, size_t nmemb, void* userp) +{ + S3fsCurl* pCurl = static_cast(userp); + + if(1 > (size * nmemb)){ + return 0; + } + if(0 >= pCurl->postdata_remaining){ + return 0; + } + size_t copysize = std::min(static_cast(size * nmemb), pCurl->postdata_remaining); + memcpy(ptr, pCurl->postdata, copysize); + + pCurl->postdata_remaining = (pCurl->postdata_remaining > static_cast(copysize) ? (pCurl->postdata_remaining - copysize) : 0); + pCurl->postdata += static_cast(copysize); + + return copysize; +} + +size_t S3fsCurl::UploadReadCallbackByMemory(void *ptr, size_t size, size_t nmemb, void *stream) +{ + drp_upload_ctx *ctx = static_cast(stream); + S3FS_PRN_INFO("Upload [path=%s][size=%zu][pos=%zu]", ctx->path.c_str(), ctx->len, ctx->pos); + + if(1 > (size * nmemb)){ + return 0; + } + if(ctx->pos >= ctx->len){ + return 0; + } + + size_t len = std::min(size * nmemb, ctx->len - ctx->pos); + memcpy(ptr, ctx->data + ctx->pos, len); + + if(len < ctx->len){ + S3FS_PRN_WARN("Upload send data copy [path=%s][size=%zu][pos=%zu][sendlen=%zu]", + ctx->path.c_str(), ctx->len, ctx->pos, len); + } + + ctx->pos += len; + return len; +} + +size_t S3fsCurl::HeaderCallback(void* data, size_t blockSize, size_t numBlocks, void* userPtr) +{ + headers_t* headers = static_cast(userPtr); + std::string header(static_cast(data), blockSize * numBlocks); + std::string key; + std::istringstream ss(header); + + if(getline(ss, key, ':')){ + // Force to lower, only "x-amz" + std::string lkey = key; + transform(lkey.begin(), lkey.end(), lkey.begin(), static_cast(std::tolower)); + if(is_prefix(lkey.c_str(), "x-amz")){ + key = lkey; + } + std::string value; + getline(ss, value); + (*headers)[key] = trim(value); + } + return blockSize * numBlocks; +} + +size_t S3fsCurl::UploadReadCallback(void* ptr, size_t size, size_t nmemb, void* userp) +{ + S3fsCurl* pCurl = static_cast(userp); + + if(1 > (size * nmemb)){ + return 0; + } + if(-1 == pCurl->partdata.fd || 0 >= pCurl->partdata.size){ + return 0; + } + // read size + ssize_t copysize = (size * nmemb) < (size_t)pCurl->partdata.size ? (size * nmemb) : (size_t)pCurl->partdata.size; + ssize_t readbytes; + ssize_t totalread; + // read and set + if(use_newcache){ + std::memcpy(static_cast(ptr), pCurl->partdata.buf, copysize); + readbytes = copysize; + totalread = copysize; + pCurl->partdata.buf += copysize; + if(copysize < pCurl->partdata.size){ + std::string uploadPath = pCurl->path + "@" + std::to_string(pCurl->partdata.get_part_number()); + S3FS_PRN_WARN("Upload send data copy [path=%s][startpos:%zu][partsize=%zu][uploadlen=%zu]", + uploadPath.c_str(), pCurl->partdata.startpos, pCurl->partdata.size, copysize); + } + }else{ + for(totalread = 0, readbytes = 0; totalread < copysize; totalread += readbytes){ + readbytes = pread(pCurl->partdata.fd, &(static_cast(ptr))[totalread], (copysize - totalread), pCurl->partdata.startpos + totalread); + if(0 == readbytes){ + // eof + break; + }else if(-1 == readbytes){ + // error + S3FS_PRN_ERR("read file error(%d).", errno); + return 0; + } + } + } + pCurl->partdata.startpos += totalread; + pCurl->partdata.size -= totalread; + + return totalread; +} + +size_t S3fsCurl::DownloadWriteCallback(void* ptr, size_t size, size_t nmemb, void* userp) +{ + S3fsCurl* pCurl = static_cast(userp); + + if(1 > (size * nmemb)){ + return 0; + } + if(-1 == pCurl->partdata.fd || 0 >= pCurl->partdata.size){ + return 0; + } + + // Buffer initial bytes in case it is an XML error response. + if(pCurl->bodydata.size() < GET_OBJECT_RESPONSE_LIMIT){ + pCurl->bodydata.append(static_cast(ptr), std::min(size * nmemb, GET_OBJECT_RESPONSE_LIMIT - pCurl->bodydata.size())); + } + + // write size + ssize_t copysize = (size * nmemb) < (size_t)pCurl->partdata.size ? (size * nmemb) : (size_t)pCurl->partdata.size; + ssize_t writebytes; + ssize_t totalwrite; + + // write + if(use_newcache && nullptr != pCurl->partdata.buf){ + std::memcpy(pCurl->partdata.buf, static_cast(ptr), copysize); + writebytes = copysize; + totalwrite = copysize; + pCurl->partdata.buf += copysize; + if(copysize < pCurl->partdata.size){ + S3FS_PRN_WARN("Download recv data copy [path=%s][startpos:%zu][partsize=%zu][downlen=%zu]", + pCurl->path.c_str(), pCurl->partdata.startpos, pCurl->partdata.size, copysize); + } + }else{ + for(totalwrite = 0, writebytes = 0; totalwrite < copysize; totalwrite += writebytes){ + writebytes = pwrite(pCurl->partdata.fd, &(static_cast(ptr))[totalwrite], (copysize - totalwrite), pCurl->partdata.startpos + totalwrite); + if(0 == writebytes){ + // eof? + break; + }else if(-1 == writebytes){ + // error + S3FS_PRN_ERR("write file error(%d).", errno); + return 0; + } + } + } + pCurl->partdata.startpos += totalwrite; + pCurl->partdata.size -= totalwrite; + + return totalwrite; +} + +bool S3fsCurl::SetCheckCertificate(bool isCertCheck) +{ + bool old = S3fsCurl::is_cert_check; + S3fsCurl::is_cert_check = isCertCheck; + return old; +} + +bool S3fsCurl::SetDnsCache(bool isCache) +{ + bool old = S3fsCurl::is_dns_cache; + S3fsCurl::is_dns_cache = isCache; + return old; +} + +void S3fsCurl::ResetOffset(S3fsCurl* pCurl) +{ + pCurl->partdata.startpos = pCurl->b_partdata_startpos; + pCurl->partdata.size = pCurl->b_partdata_size; +} + +bool S3fsCurl::SetSslSessionCache(bool isCache) +{ + bool old = S3fsCurl::is_ssl_session_cache; + S3fsCurl::is_ssl_session_cache = isCache; + return old; +} + +long S3fsCurl::SetConnectTimeout(long timeout) +{ + long old = S3fsCurl::connect_timeout; + S3fsCurl::connect_timeout = timeout; + return old; +} + +time_t S3fsCurl::SetReadwriteTimeout(time_t timeout) +{ + time_t old = S3fsCurl::readwrite_timeout; + S3fsCurl::readwrite_timeout = timeout; + return old; +} + +int S3fsCurl::SetRetries(int count) +{ + int old = S3fsCurl::retries; + S3fsCurl::retries = count; + return old; +} + +bool S3fsCurl::SetPublicBucket(bool flag) +{ + bool old = S3fsCurl::is_public_bucket; + S3fsCurl::is_public_bucket = flag; + return old; +} + +acl_t S3fsCurl::SetDefaultAcl(acl_t acl) +{ + acl_t old = S3fsCurl::default_acl; + S3fsCurl::default_acl = acl; + return old; +} + +acl_t S3fsCurl::GetDefaultAcl() +{ + return S3fsCurl::default_acl; +} + +std::string S3fsCurl::SetStorageClass(const std::string& storage_class) +{ + std::string old = S3fsCurl::storage_class; + S3fsCurl::storage_class = storage_class; + // AWS requires uppercase storage class values + transform(S3fsCurl::storage_class.begin(), S3fsCurl::storage_class.end(), S3fsCurl::storage_class.begin(), ::toupper); + return old; +} + +bool S3fsCurl::PushbackSseKeys(const std::string& input) +{ + std::string onekey = trim(input); + if(onekey.empty()){ + return false; + } + if('#' == onekey[0]){ + return false; + } + // make base64 if the key is short enough, otherwise assume it is already so + std::string base64_key; + std::string raw_key; + if(onekey.length() > 256 / 8){ + std::string p_key(s3fs_decode64(onekey.c_str(), onekey.size())); + raw_key = p_key; + base64_key = onekey; + } else { + base64_key = s3fs_base64(reinterpret_cast(onekey.c_str()), onekey.length()); + raw_key = onekey; + } + + // make MD5 + std::string strMd5; + if(!make_md5_from_binary(raw_key.c_str(), raw_key.length(), strMd5)){ + S3FS_PRN_ERR("Could not make MD5 from SSE-C keys(%s).", raw_key.c_str()); + return false; + } + // mapped MD5 = SSE Key + sseckeymap_t md5map; + md5map.clear(); + md5map[strMd5] = base64_key; + S3fsCurl::sseckeys.push_back(md5map); + + return true; +} + +sse_type_t S3fsCurl::SetSseType(sse_type_t type) +{ + sse_type_t old = S3fsCurl::ssetype; + S3fsCurl::ssetype = type; + return old; +} + +bool S3fsCurl::SetSseCKeys(const char* filepath) +{ + if(!filepath){ + S3FS_PRN_ERR("SSE-C keys filepath is empty."); + return false; + } + struct stat st; + if(0 != stat(filepath, &st)){ + S3FS_PRN_ERR("could not open use_sse keys file(%s).", filepath); + return false; + } + if(st.st_mode & (S_IXUSR | S_IRWXG | S_IRWXO)){ + S3FS_PRN_ERR("use_sse keys file %s should be 0600 permissions.", filepath); + return false; + } + + S3fsCurl::sseckeys.clear(); + + std::ifstream ssefs(filepath); + if(!ssefs.good()){ + S3FS_PRN_ERR("Could not open SSE-C keys file(%s).", filepath); + return false; + } + + std::string line; + while(getline(ssefs, line)){ + S3fsCurl::PushbackSseKeys(line); + } + if(S3fsCurl::sseckeys.empty()){ + S3FS_PRN_ERR("There is no SSE Key in file(%s).", filepath); + return false; + } + return true; +} + +bool S3fsCurl::SetSseKmsid(const char* kmsid) +{ + if(!kmsid || '\0' == kmsid[0]){ + S3FS_PRN_ERR("SSE-KMS kms id is empty."); + return false; + } + S3fsCurl::ssekmsid = kmsid; + return true; +} + +// [NOTE] +// Because SSE is set by some options and environment, +// this function check the integrity of the SSE data finally. +bool S3fsCurl::FinalCheckSse() +{ + switch(S3fsCurl::ssetype){ + case sse_type_t::SSE_DISABLE: + S3fsCurl::ssekmsid.erase(); + return true; + case sse_type_t::SSE_S3: + S3fsCurl::ssekmsid.erase(); + return true; + case sse_type_t::SSE_C: + if(S3fsCurl::sseckeys.empty()){ + S3FS_PRN_ERR("sse type is SSE-C, but there is no custom key."); + return false; + } + S3fsCurl::ssekmsid.erase(); + return true; + case sse_type_t::SSE_KMS: + if(S3fsCurl::ssekmsid.empty()){ + S3FS_PRN_ERR("sse type is SSE-KMS, but there is no specified kms id."); + return false; + } + if(S3fsCurl::GetSignatureType() == signature_type_t::V2_ONLY){ + S3FS_PRN_ERR("sse type is SSE-KMS, but signature type is not v4. SSE-KMS require signature v4."); + return false; + } + + // SSL/TLS is required for KMS + // + if(!is_prefix(s3host.c_str(), "https://")){ + S3FS_PRN_ERR("The sse type is SSE-KMS, but it is not configured to use SSL/TLS. SSE-KMS requires SSL/TLS communication."); + return false; + } + return true; + } + S3FS_PRN_ERR("sse type is unknown(%d).", static_cast(S3fsCurl::ssetype)); + + return false; +} + +bool S3fsCurl::LoadEnvSseCKeys() +{ + char* envkeys = getenv("AWSSSECKEYS"); + if(nullptr == envkeys){ + // nothing to do + return true; + } + S3fsCurl::sseckeys.clear(); + + std::istringstream fullkeys(envkeys); + std::string onekey; + while(getline(fullkeys, onekey, ':')){ + S3fsCurl::PushbackSseKeys(onekey); + } + + // cppcheck-suppress unmatchedSuppression + // cppcheck-suppress knownConditionTrueFalse + if(S3fsCurl::sseckeys.empty()){ + S3FS_PRN_ERR("There is no SSE Key in environment(AWSSSECKEYS=%s).", envkeys); + return false; + } + return true; +} + +bool S3fsCurl::LoadEnvSseKmsid() +{ + const char* envkmsid = getenv("AWSSSEKMSID"); + if(nullptr == envkmsid){ + // nothing to do + return true; + } + return S3fsCurl::SetSseKmsid(envkmsid); +} + +// +// If md5 is empty, returns first(current) sse key. +// +bool S3fsCurl::GetSseKey(std::string& md5, std::string& ssekey) +{ + for(sseckeylist_t::const_iterator iter = S3fsCurl::sseckeys.begin(); iter != S3fsCurl::sseckeys.end(); ++iter){ + if(md5.empty() || md5 == (*iter).begin()->first){ + md5 = iter->begin()->first; + ssekey = iter->begin()->second; + return true; + } + } + return false; +} + +bool S3fsCurl::GetSseKeyMd5(size_t pos, std::string& md5) +{ + if(S3fsCurl::sseckeys.size() <= static_cast(pos)){ + return false; + } + size_t cnt = 0; + for(sseckeylist_t::const_iterator iter = S3fsCurl::sseckeys.begin(); iter != S3fsCurl::sseckeys.end(); ++iter, ++cnt){ + if(pos == cnt){ + md5 = iter->begin()->first; + return true; + } + } + return false; +} + +size_t S3fsCurl::GetSseKeyCount() +{ + return S3fsCurl::sseckeys.size(); +} + +bool S3fsCurl::SetContentMd5(bool flag) +{ + bool old = S3fsCurl::is_content_md5; + S3fsCurl::is_content_md5 = flag; + return old; +} + +bool S3fsCurl::SetVerbose(bool flag) +{ + bool old = S3fsCurl::is_verbose; + S3fsCurl::is_verbose = flag; + return old; +} + +bool S3fsCurl::SetDumpBody(bool flag) +{ + bool old = S3fsCurl::is_dump_body; + S3fsCurl::is_dump_body = flag; + return old; +} + +long S3fsCurl::SetSslVerifyHostname(long value) +{ + if(0 != value && 1 != value){ + return -1; + } + long old = S3fsCurl::ssl_verify_hostname; + S3fsCurl::ssl_verify_hostname = value; + return old; +} + +bool S3fsCurl::SetMultipartSize(off_t size) +{ + size = size * 1024 * 1024; + if(size < MIN_MULTIPART_SIZE){ + return false; + } + S3fsCurl::multipart_size = size; + return true; +} + +bool S3fsCurl::SetMultipartCopySize(off_t size) +{ + size = size * 1024 * 1024; + if(size < MIN_MULTIPART_SIZE){ + return false; + } + S3fsCurl::multipart_copy_size = size; + return true; +} + +int S3fsCurl::SetMaxParallelCount(int value) +{ + int old = S3fsCurl::max_parallel_cnt; + S3fsCurl::max_parallel_cnt = value; + return old; +} + +int S3fsCurl::SetMaxMultiRequest(int max) +{ + int old = S3fsCurl::max_multireq; + S3fsCurl::max_multireq = max; + return old; +} + +// [NOTE] +// This proxy setting is as same as the "--proxy" option of the curl command, +// and equivalent to the "CURLOPT_PROXY" option of the curl_easy_setopt() +// function. +// However, currently s3fs does not provide another option to set the schema +// and port, so you need to specify these it in this function. (Other than +// this function, there is no means of specifying the schema and port.) +// Therefore, it should be specified "url" as "[://][:]". +// s3fs passes this string to curl_easy_setopt() function with "CURLOPT_PROXY". +// If no "schema" is specified, "http" will be used as default, and if no port +// is specified, "443" will be used for "HTTPS" and "1080" otherwise. +// (See the description of "CURLOPT_PROXY" in libcurl document.) +// +bool S3fsCurl::SetProxy(const char* url) +{ + if(!url || '\0' == url[0]){ + return false; + } + std::string tmpurl = url; + + // check schema + bool is_http = true; + size_t pos = 0; + if(std::string::npos != (pos = tmpurl.find("://", pos))){ + if(0 == pos){ + // no schema string before "://" + return false; + } + pos += strlen("://"); + + // Check if it is other than "http://" + if(0 != tmpurl.find("http://", 0)){ + is_http = false; + } + }else{ + // not have schema string + pos = 0; + } + // check fqdn and port number string + if(std::string::npos != (pos = tmpurl.find(':', pos))){ + // specify port + if(0 == pos){ + // no fqdn(hostname) string before ":" + return false; + } + pos += strlen(":"); + if(std::string::npos != tmpurl.find(':', pos)){ + // found wrong separator + return false; + } + } + + S3fsCurl::proxy_url = tmpurl; + S3fsCurl::proxy_http = is_http; + return true; +} + +// [NOTE] +// This function loads proxy credentials(username and passphrase) +// from a file. +// The loaded values is set to "CURLOPT_PROXYUSERPWD" in the +// curl_easy_setopt() function. (But only used if the proxy is HTTP +// schema.) +// +// The file is expected to contain only one valid line: +// ------------------------ +// # comment line +// : +// ------------------------ +// Lines starting with a '#' character are treated as comments. +// Lines with only space characters and blank lines are ignored. +// If the user name contains spaces, it must be url encoded(ex. %20). +// +bool S3fsCurl::SetProxyUserPwd(const char* file) +{ + if(!file || '\0' == file[0]){ + return false; + } + if(!S3fsCurl::proxy_userpwd.empty()){ + S3FS_PRN_WARN("Already set username and passphrase for proxy."); + return false; + } + + std::ifstream credFileStream(file); + if(!credFileStream.good()){ + S3FS_PRN_WARN("Could not load username and passphrase for proxy from %s.", file); + return false; + } + + std::string userpwd; + std::string line; + while(getline(credFileStream, line)){ + line = trim(line); + if(line.empty()){ + continue; + } + if(line[0]=='#'){ + continue; + } + if(!userpwd.empty()){ + S3FS_PRN_WARN("Multiple valid username and passphrase found in %s file. Should specify only one pair.", file); + return false; + } + // check separator for username and passphrase + size_t pos = 0; + if(std::string::npos == (pos = line.find(':', pos))){ + S3FS_PRN_WARN("Found string for username and passphrase in %s file does not have separator ':'.", file); + return false; + } + if(0 == pos || (pos + 1) == line.length()){ + S3FS_PRN_WARN("Found string for username or passphrase in %s file is empty.", file); + return false; + } + if(std::string::npos != line.find(':', ++pos)){ + S3FS_PRN_WARN("Found string for username and passphrase in %s file has multiple separator ':'.", file); + return false; + } + userpwd = line; + } + if(userpwd.empty()){ + S3FS_PRN_WARN("No valid username and passphrase found in %s.", file); + return false; + } + + S3fsCurl::proxy_userpwd = userpwd; + return true; +} + +// cppcheck-suppress unmatchedSuppression +// cppcheck-suppress constParameter +// cppcheck-suppress constParameterCallback +bool S3fsCurl::UploadMultipartPostCallback(S3fsCurl* s3fscurl, void* param) +{ + if(!s3fscurl || param){ // this callback does not need a parameter + return false; + } + + return s3fscurl->UploadMultipartPostComplete(); +} + +// cppcheck-suppress unmatchedSuppression +// cppcheck-suppress constParameter +// cppcheck-suppress constParameterCallback +bool S3fsCurl::MixMultipartPostCallback(S3fsCurl* s3fscurl, void* param) +{ + if(!s3fscurl || param){ // this callback does not need a parameter + return false; + } + + return s3fscurl->MixMultipartPostComplete(); +} + +std::unique_ptr S3fsCurl::UploadMultipartPostRetryCallback(S3fsCurl* s3fscurl) +{ + if(!s3fscurl){ + return nullptr; + } + // parse and get part_num, upload_id. + std::string upload_id; + std::string part_num_str; + int part_num; + off_t tmp_part_num = 0; + if(!get_keyword_value(s3fscurl->url, "uploadId", upload_id)){ + return nullptr; + } + upload_id = urlDecode(upload_id); // decode + if(!get_keyword_value(s3fscurl->url, "partNumber", part_num_str)){ + return nullptr; + } + if(!s3fs_strtoofft(&tmp_part_num, part_num_str.c_str(), /*base=*/ 10)){ + return nullptr; + } + part_num = static_cast(tmp_part_num); + + if(s3fscurl->retry_count >= S3fsCurl::retries){ + S3FS_PRN_ERR("Over retry count(%d) limit(%s:%d).", s3fscurl->retry_count, s3fscurl->path.c_str(), part_num); + return nullptr; + } + + // duplicate request + std::unique_ptr newcurl(new S3fsCurl(s3fscurl->IsUseAhbe())); + newcurl->partdata.petag = s3fscurl->partdata.petag; + newcurl->partdata.fd = s3fscurl->partdata.fd; + newcurl->partdata.startpos = s3fscurl->b_partdata_startpos; + newcurl->partdata.size = s3fscurl->b_partdata_size; + newcurl->partdata.buf = s3fscurl->b_partdata_buf; + newcurl->b_partdata_startpos = s3fscurl->b_partdata_startpos; + newcurl->b_partdata_size = s3fscurl->b_partdata_size; + newcurl->b_partdata_buf = s3fscurl->b_partdata_buf; + newcurl->retry_count = s3fscurl->retry_count + 1; + newcurl->op = s3fscurl->op; + newcurl->type = s3fscurl->type; + + // setup new curl object + if(0 != newcurl->UploadMultipartPostSetup(s3fscurl->path.c_str(), part_num, upload_id)){ + S3FS_PRN_ERR("Could not duplicate curl object(%s:%d).", s3fscurl->path.c_str(), part_num); + return nullptr; + } + return newcurl; +} + +std::unique_ptr S3fsCurl::CopyMultipartPostRetryCallback(S3fsCurl* s3fscurl) +{ + if(!s3fscurl){ + return nullptr; + } + // parse and get part_num, upload_id. + std::string upload_id; + std::string part_num_str; + int part_num; + off_t tmp_part_num = 0; + if(!get_keyword_value(s3fscurl->url, "uploadId", upload_id)){ + return nullptr; + } + upload_id = urlDecode(upload_id); // decode + if(!get_keyword_value(s3fscurl->url, "partNumber", part_num_str)){ + return nullptr; + } + if(!s3fs_strtoofft(&tmp_part_num, part_num_str.c_str(), /*base=*/ 10)){ + return nullptr; + } + part_num = static_cast(tmp_part_num); + + if(s3fscurl->retry_count >= S3fsCurl::retries){ + S3FS_PRN_ERR("Over retry count(%d) limit(%s:%d).", s3fscurl->retry_count, s3fscurl->path.c_str(), part_num); + return nullptr; + } + + // duplicate request + std::unique_ptr newcurl(new S3fsCurl(s3fscurl->IsUseAhbe())); + newcurl->partdata.petag = s3fscurl->partdata.petag; + newcurl->b_from = s3fscurl->b_from; + newcurl->b_meta = s3fscurl->b_meta; + newcurl->retry_count = s3fscurl->retry_count + 1; + newcurl->op = s3fscurl->op; + newcurl->type = s3fscurl->type; + + // setup new curl object + if(0 != newcurl->CopyMultipartPostSetup(s3fscurl->b_from.c_str(), s3fscurl->path.c_str(), part_num, upload_id, s3fscurl->b_meta)){ + S3FS_PRN_ERR("Could not duplicate curl object(%s:%d).", s3fscurl->path.c_str(), part_num); + return nullptr; + } + return newcurl; +} + +std::unique_ptr S3fsCurl::MixMultipartPostRetryCallback(S3fsCurl* s3fscurl) +{ + if(!s3fscurl){ + return nullptr; + } + + if(-1 == s3fscurl->partdata.fd){ + return S3fsCurl::CopyMultipartPostRetryCallback(s3fscurl); + }else{ + return S3fsCurl::UploadMultipartPostRetryCallback(s3fscurl); + } +} + +int S3fsCurl::MapPutErrorResponse(int result) +{ + if(result != 0){ + return result; + } + // PUT returns 200 status code with something error, thus + // we need to check body. + // + // example error body: + // + // + // AccessDenied + // Access Denied + // E4CA6F6767D6685C + // BHzLOATeDuvN8Es1wI8IcERq4kl4dc2A9tOB8Yqr39Ys6fl7N4EJ8sjGiVvu6wLP + // + // + const char* pstrbody = bodydata.c_str(); + std::string code; + if(simple_parse_xml(pstrbody, bodydata.size(), "Code", code)){ + S3FS_PRN_ERR("Put request get 200 status response, but it included error body(or nullptr). The request failed during copying the object in S3. Code: %s", code.c_str()); + // TODO: parse more specific error from + result = -EIO; + } + return result; +} + +// [NOTE] +// It is a factory method as utility because it requires an S3fsCurl object +// initialized for multipart upload from outside this class. +// +std::unique_ptr S3fsCurl::CreateParallelS3fsCurl(const char* tpath, int fd, off_t start, off_t size, int part_num, bool is_copy, etagpair* petag, const std::string& upload_id, int& result) +{ + // duplicate fd + if(!tpath || -1 == fd || start < 0 || size <= 0 || !petag){ + S3FS_PRN_ERR("Parameters are wrong: tpath(%s), fd(%d), start(%lld), size(%lld), petag(%s)", SAFESTRPTR(tpath), fd, static_cast(start), static_cast(size), (petag ? "not null" : "null")); + result = -EIO; + return nullptr; + } + result = 0; + + std::unique_ptr s3fscurl(new S3fsCurl(true)); + + if(!is_copy){ + s3fscurl->partdata.fd = fd; + s3fscurl->partdata.startpos = start; + s3fscurl->partdata.size = size; + s3fscurl->partdata.is_copy = is_copy; + s3fscurl->partdata.petag = petag; // [NOTE] be careful, the value is set directly + s3fscurl->b_partdata_startpos = s3fscurl->partdata.startpos; + s3fscurl->b_partdata_size = s3fscurl->partdata.size; + + S3FS_PRN_INFO3("Upload Part [tpath=%s][start=%lld][size=%lld][part=%d]", SAFESTRPTR(tpath), static_cast(start), static_cast(size), part_num); + + if(0 != (result = s3fscurl->UploadMultipartPostSetup(tpath, part_num, upload_id))){ + S3FS_PRN_ERR("failed uploading part setup(%d)", result); + return nullptr; + } + }else{ + headers_t meta; + std::string srcresource; + std::string srcurl; + MakeUrlResource(get_realpath(tpath).c_str(), srcresource, srcurl); + meta["x-amz-copy-source"] = srcresource; + + std::ostringstream strrange; + strrange << "bytes=" << start << "-" << (start + size - 1); + meta["x-amz-copy-source-range"] = strrange.str(); + + s3fscurl->b_from = SAFESTRPTR(tpath); + s3fscurl->b_meta = meta; + s3fscurl->partdata.petag = petag; // [NOTE] be careful, the value is set directly + + S3FS_PRN_INFO3("Copy Part [tpath=%s][start=%lld][size=%lld][part=%d]", SAFESTRPTR(tpath), static_cast(start), static_cast(size), part_num); + + if(0 != (result = s3fscurl->CopyMultipartPostSetup(tpath, tpath, part_num, upload_id, meta))){ + S3FS_PRN_ERR("failed uploading part setup(%d)", result); + return nullptr; + } + } + + // Call lazy function + if(!s3fscurl->fpLazySetup || !s3fscurl->fpLazySetup(s3fscurl.get())){ + S3FS_PRN_ERR("failed lazy function setup for uploading part"); + result = -EIO; + return nullptr; + } + return s3fscurl; +} + +int S3fsCurl::ParallelMultipartUploadRequest(const char* tpath, headers_t& meta, int fd, off_t fsize, char* buf) +{ + int result; + std::string upload_id; + struct stat st; + etaglist_t list; + off_t remaining_bytes; + S3fsCurl s3fscurl(true); + + S3FS_PRN_INFO3("[tpath=%s][fd=%d]", SAFESTRPTR(tpath), fd); + + if(!use_newcache && -1 == fstat(fd, &st)){ + S3FS_PRN_ERR("Invalid file descriptor(errno=%d)", errno); + return -errno; + } + + if(0 != (result = s3fscurl.PreMultipartPostRequest(tpath, meta, upload_id, false))){ + return result; + } + s3fscurl.DestroyCurlHandle(); + + // Initialize S3fsMultiCurl + S3fsMultiCurl curlmulti(GetMaxParallelCount()); + curlmulti.SetSuccessCallback(S3fsCurl::UploadMultipartPostCallback); + curlmulti.SetRetryCallback(S3fsCurl::UploadMultipartPostRetryCallback); + off_t real_size = 0; + if (use_newcache) { + real_size = fsize; + } else { + real_size = st.st_size; + } + + // cycle through open fd, pulling off 10MB chunks at a time + for(remaining_bytes = real_size; 0 < remaining_bytes; ){ + off_t chunk = remaining_bytes > S3fsCurl::multipart_size ? S3fsCurl::multipart_size : remaining_bytes; + + // s3fscurl sub object + std::unique_ptr s3fscurl_para(new S3fsCurl(true)); + s3fscurl_para->partdata.fd = fd; + s3fscurl_para->partdata.startpos = real_size - remaining_bytes; + s3fscurl_para->partdata.size = chunk; + s3fscurl_para->partdata.buf = buf + real_size - remaining_bytes; + s3fscurl_para->b_partdata_startpos = s3fscurl_para->partdata.startpos; + s3fscurl_para->b_partdata_size = s3fscurl_para->partdata.size; + s3fscurl_para->b_partdata_buf = s3fscurl_para->partdata.buf; + + s3fscurl_para->partdata.add_etag_list(list); + + // initiate upload part for parallel + if(0 != (result = s3fscurl_para->UploadMultipartPostSetup(tpath, s3fscurl_para->partdata.get_part_number(), upload_id))){ + S3FS_PRN_ERR("failed uploading part setup(%d)", result); + return result; + } + + // set into parallel object + if(!curlmulti.SetS3fsCurlObject(std::move(s3fscurl_para))){ + S3FS_PRN_ERR("Could not make curl object into multi curl(%s).", tpath); + return -EIO; + } + remaining_bytes -= chunk; + } + + // Multi request + if(0 != (result = curlmulti.Request())){ + S3FS_PRN_ERR("error occurred in multi request(errno=%d).", result); + + S3fsCurl s3fscurl_abort(true); + int result2 = s3fscurl_abort.AbortMultipartUpload(tpath, upload_id); + s3fscurl_abort.DestroyCurlHandle(); + if(result2 != 0){ + S3FS_PRN_ERR("error aborting multipart upload(errno=%d).", result2); + } + + return result; + } + + if(0 != (result = s3fscurl.CompleteMultipartPostRequest(tpath, upload_id, list))){ + return result; + } + return 0; +} + +int S3fsCurl::ParallelMixMultipartUploadRequest(const char* tpath, headers_t& meta, int fd, const fdpage_list_t& mixuppages) +{ + int result; + std::string upload_id; + struct stat st; + etaglist_t list; + S3fsCurl s3fscurl(true); + + S3FS_PRN_INFO3("[tpath=%s][fd=%d]", SAFESTRPTR(tpath), fd); + + if(-1 == fstat(fd, &st)){ + S3FS_PRN_ERR("Invalid file descriptor(errno=%d)", errno); + return -errno; + } + + if(0 != (result = s3fscurl.PreMultipartPostRequest(tpath, meta, upload_id, true))){ + return result; + } + s3fscurl.DestroyCurlHandle(); + + // for copy multipart + std::string srcresource; + std::string srcurl; + MakeUrlResource(get_realpath(tpath).c_str(), srcresource, srcurl); + meta["Content-Type"] = S3fsCurl::LookupMimeType(tpath); + meta["x-amz-copy-source"] = srcresource; + + // Initialize S3fsMultiCurl + S3fsMultiCurl curlmulti(GetMaxParallelCount()); + curlmulti.SetSuccessCallback(S3fsCurl::MixMultipartPostCallback); + curlmulti.SetRetryCallback(S3fsCurl::MixMultipartPostRetryCallback); + + for(fdpage_list_t::const_iterator iter = mixuppages.begin(); iter != mixuppages.end(); ++iter){ + if(iter->modified){ + // Multipart upload + std::unique_ptr s3fscurl_para(new S3fsCurl(true)); + s3fscurl_para->partdata.fd = fd; + s3fscurl_para->partdata.startpos = iter->offset; + s3fscurl_para->partdata.size = iter->bytes; + s3fscurl_para->b_partdata_startpos = s3fscurl_para->partdata.startpos; + s3fscurl_para->b_partdata_size = s3fscurl_para->partdata.size; + s3fscurl_para->partdata.add_etag_list(list); + + S3FS_PRN_INFO3("Upload Part [tpath=%s][start=%lld][size=%lld][part=%d]", SAFESTRPTR(tpath), static_cast(iter->offset), static_cast(iter->bytes), s3fscurl_para->partdata.get_part_number()); + + // initiate upload part for parallel + if(0 != (result = s3fscurl_para->UploadMultipartPostSetup(tpath, s3fscurl_para->partdata.get_part_number(), upload_id))){ + S3FS_PRN_ERR("failed uploading part setup(%d)", result); + return result; + } + + // set into parallel object + if(!curlmulti.SetS3fsCurlObject(std::move(s3fscurl_para))){ + S3FS_PRN_ERR("Could not make curl object into multi curl(%s).", tpath); + return -EIO; + } + }else{ + // Multipart copy + for(off_t i = 0, bytes = 0; i < iter->bytes; i += bytes){ + std::unique_ptr s3fscurl_para(new S3fsCurl(true)); + + bytes = std::min(static_cast(GetMultipartCopySize()), iter->bytes - i); + /* every part should be larger than MIN_MULTIPART_SIZE and smaller than FIVE_GB */ + off_t remain_bytes = iter->bytes - i - bytes; + + if ((MIN_MULTIPART_SIZE > remain_bytes) && (0 < remain_bytes)){ + if(FIVE_GB < (bytes + remain_bytes)){ + bytes = (bytes + remain_bytes)/2; + } else{ + bytes += remain_bytes; + } + } + + std::ostringstream strrange; + strrange << "bytes=" << (iter->offset + i) << "-" << (iter->offset + i + bytes - 1); + meta["x-amz-copy-source-range"] = strrange.str(); + + s3fscurl_para->b_from = SAFESTRPTR(tpath); + s3fscurl_para->b_meta = meta; + s3fscurl_para->partdata.add_etag_list(list); + + S3FS_PRN_INFO3("Copy Part [tpath=%s][start=%lld][size=%lld][part=%d]", SAFESTRPTR(tpath), static_cast(iter->offset + i), static_cast(bytes), s3fscurl_para->partdata.get_part_number()); + + // initiate upload part for parallel + if(0 != (result = s3fscurl_para->CopyMultipartPostSetup(tpath, tpath, s3fscurl_para->partdata.get_part_number(), upload_id, meta))){ + S3FS_PRN_ERR("failed uploading part setup(%d)", result); + return result; + } + + // set into parallel object + if(!curlmulti.SetS3fsCurlObject(std::move(s3fscurl_para))){ + S3FS_PRN_ERR("Could not make curl object into multi curl(%s).", tpath); + return -EIO; + } + } + } + } + + // Multi request + if(0 != (result = curlmulti.Request())){ + S3FS_PRN_ERR("error occurred in multi request(errno=%d).", result); + + S3fsCurl s3fscurl_abort(true); + int result2 = s3fscurl_abort.AbortMultipartUpload(tpath, upload_id); + s3fscurl_abort.DestroyCurlHandle(); + if(result2 != 0){ + S3FS_PRN_ERR("error aborting multipart upload(errno=%d).", result2); + } + return result; + } + + if(0 != (result = s3fscurl.CompleteMultipartPostRequest(tpath, upload_id, list))){ + return result; + } + return 0; +} + +std::unique_ptr S3fsCurl::ParallelGetObjectRetryCallback(S3fsCurl* s3fscurl) +{ + int result; + + if(!s3fscurl){ + return nullptr; + } + if(s3fscurl->retry_count >= S3fsCurl::retries){ + S3FS_PRN_ERR("Over retry count(%d) limit(%s).", s3fscurl->retry_count, s3fscurl->path.c_str()); + return nullptr; + } + + // duplicate request(setup new curl object) + std::unique_ptr newcurl(new S3fsCurl(s3fscurl->IsUseAhbe())); + + if(0 != (result = newcurl->PreGetObjectRequest(s3fscurl->path.c_str(), s3fscurl->partdata.fd, s3fscurl->partdata.startpos, s3fscurl->partdata.size, s3fscurl->b_ssetype, s3fscurl->b_ssevalue, s3fscurl->partdata.buf))){ + S3FS_PRN_ERR("failed downloading part setup(%d)", result); + return nullptr; + } + newcurl->retry_count = s3fscurl->retry_count + 1; + + return newcurl; +} + +int S3fsCurl::ParallelGetObjectRequest(const char* tpath, int fd, off_t start, off_t size, char* buf) +{ + S3FS_PRN_INFO3("[tpath=%s][fd=%d]", SAFESTRPTR(tpath), fd); + + sse_type_t ssetype = sse_type_t::SSE_DISABLE; + std::string ssevalue; + if(!get_object_sse_type(tpath, ssetype, ssevalue)){ + S3FS_PRN_WARN("Failed to get SSE type for file(%s).", SAFESTRPTR(tpath)); + } + int result = 0; + off_t remaining_bytes; + + // cycle through open fd, pulling off 10MB chunks at a time + for(remaining_bytes = size; 0 < remaining_bytes; ){ + S3fsMultiCurl curlmulti(GetMaxParallelCount()); + int para_cnt; + off_t chunk; + + // Initialize S3fsMultiCurl + //curlmulti.SetSuccessCallback(nullptr); // not need to set success callback + curlmulti.SetRetryCallback(S3fsCurl::ParallelGetObjectRetryCallback); + + // Loop for setup parallel upload(multipart) request. + for(para_cnt = 0; para_cnt < S3fsCurl::max_parallel_cnt && 0 < remaining_bytes; para_cnt++, remaining_bytes -= chunk){ + // chunk size + chunk = remaining_bytes > S3fsCurl::multipart_size ? S3fsCurl::multipart_size : remaining_bytes; + + // s3fscurl sub object + std::unique_ptr s3fscurl_para(new S3fsCurl(true)); + char* realBuf = nullptr; + if (buf) realBuf = buf + size - remaining_bytes; + if(0 != (result = s3fscurl_para->PreGetObjectRequest(tpath, fd, (start + size - remaining_bytes), chunk, ssetype, ssevalue, realBuf))){ + S3FS_PRN_ERR("failed downloading part setup(%d)", result); + return result; + } + + // set into parallel object + if(!curlmulti.SetS3fsCurlObject(std::move(s3fscurl_para))){ + S3FS_PRN_ERR("Could not make curl object into multi curl(%s).", tpath); + return -EIO; + } + } + + // Multi request + if(0 != (result = curlmulti.Request())){ + S3FS_PRN_ERR("error occurred in multi request(errno=%d).", result); + break; + } + + // reinit for loop. + curlmulti.Clear(); + } + return result; +} + +bool S3fsCurl::UploadMultipartPostSetCurlOpts(S3fsCurl* s3fscurl) +{ + if(!s3fscurl){ + return false; + } + if(!s3fscurl->CreateCurlHandle()){ + return false; + } + if(CURLE_OK != curl_easy_setopt(s3fscurl->hCurl, CURLOPT_URL, s3fscurl->url.c_str())){ + return false; + } + if(CURLE_OK != curl_easy_setopt(s3fscurl->hCurl, CURLOPT_UPLOAD, true)){ // HTTP PUT + return false; + } + if(CURLE_OK != curl_easy_setopt(s3fscurl->hCurl, CURLOPT_WRITEDATA, reinterpret_cast(&s3fscurl->bodydata))){ + return false; + } + if(CURLE_OK != curl_easy_setopt(s3fscurl->hCurl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback)){ + return false; + } + if(CURLE_OK != curl_easy_setopt(s3fscurl->hCurl, CURLOPT_HEADERDATA, reinterpret_cast(&s3fscurl->responseHeaders))){ + return false; + } + if(CURLE_OK != curl_easy_setopt(s3fscurl->hCurl, CURLOPT_HEADERFUNCTION, HeaderCallback)){ + return false; + } + if(CURLE_OK != curl_easy_setopt(s3fscurl->hCurl, CURLOPT_INFILESIZE_LARGE, static_cast(s3fscurl->partdata.size))){ // Content-Length + return false; + } + if(CURLE_OK != curl_easy_setopt(s3fscurl->hCurl, CURLOPT_READFUNCTION, UploadReadCallback)){ + return false; + } + if(CURLE_OK != curl_easy_setopt(s3fscurl->hCurl, CURLOPT_READDATA, reinterpret_cast(s3fscurl))){ + return false; + } + if(!S3fsCurl::AddUserAgent(s3fscurl->hCurl)){ // put User-Agent + return false; + } + + return true; +} + +bool S3fsCurl::CopyMultipartPostSetCurlOpts(S3fsCurl* s3fscurl) +{ + if(!s3fscurl){ + return false; + } + if(!s3fscurl->CreateCurlHandle()){ + return false; + } + + if(CURLE_OK != curl_easy_setopt(s3fscurl->hCurl, CURLOPT_URL, s3fscurl->url.c_str())){ + return false; + } + if(CURLE_OK != curl_easy_setopt(s3fscurl->hCurl, CURLOPT_UPLOAD, true)){ // HTTP PUT + return false; + } + if(CURLE_OK != curl_easy_setopt(s3fscurl->hCurl, CURLOPT_WRITEDATA, reinterpret_cast(&s3fscurl->bodydata))){ + return false; + } + if(CURLE_OK != curl_easy_setopt(s3fscurl->hCurl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback)){ + return false; + } + if(CURLE_OK != curl_easy_setopt(s3fscurl->hCurl, CURLOPT_HEADERDATA, reinterpret_cast(&s3fscurl->headdata))){ + return false; + } + if(CURLE_OK != curl_easy_setopt(s3fscurl->hCurl, CURLOPT_HEADERFUNCTION, WriteMemoryCallback)){ + return false; + } + if(CURLE_OK != curl_easy_setopt(s3fscurl->hCurl, CURLOPT_INFILESIZE, 0)){ // Content-Length + return false; + } + if(!S3fsCurl::AddUserAgent(s3fscurl->hCurl)){ // put User-Agent + return false; + } + + return true; +} + +bool S3fsCurl::PreGetObjectRequestSetCurlOpts(S3fsCurl* s3fscurl) +{ + if(!s3fscurl){ + return false; + } + if(!s3fscurl->CreateCurlHandle()){ + return false; + } + + if(CURLE_OK != curl_easy_setopt(s3fscurl->hCurl, CURLOPT_URL, s3fscurl->url.c_str())){ + return false; + } + if(CURLE_OK != curl_easy_setopt(s3fscurl->hCurl, CURLOPT_WRITEFUNCTION, DownloadWriteCallback)){ + return false; + } + if(CURLE_OK != curl_easy_setopt(s3fscurl->hCurl, CURLOPT_WRITEDATA, reinterpret_cast(s3fscurl))){ + return false; + } + if(!S3fsCurl::AddUserAgent(s3fscurl->hCurl)){ // put User-Agent + return false; + } + + return true; +} + +bool S3fsCurl::PreHeadRequestSetCurlOpts(S3fsCurl* s3fscurl) +{ + if(!s3fscurl){ + return false; + } + if(!s3fscurl->CreateCurlHandle()){ + return false; + } + + if(CURLE_OK != curl_easy_setopt(s3fscurl->hCurl, CURLOPT_URL, s3fscurl->url.c_str())){ + return false; + } + if(CURLE_OK != curl_easy_setopt(s3fscurl->hCurl, CURLOPT_NOBODY, true)){ // HEAD + return false; + } + if(CURLE_OK != curl_easy_setopt(s3fscurl->hCurl, CURLOPT_FILETIME, true)){ // Last-Modified + return false; + } + + // responseHeaders + if(CURLE_OK != curl_easy_setopt(s3fscurl->hCurl, CURLOPT_HEADERDATA, reinterpret_cast(&s3fscurl->responseHeaders))){ + return false; + } + if(CURLE_OK != curl_easy_setopt(s3fscurl->hCurl, CURLOPT_HEADERFUNCTION, HeaderCallback)){ + return false; + } + if(!S3fsCurl::AddUserAgent(s3fscurl->hCurl)){ // put User-Agent + return false; + } + + return true; +} + +bool S3fsCurl::AddUserAgent(CURL* hCurl) +{ + if(!hCurl){ + return false; + } + if(S3fsCurl::IsUserAgentFlag()){ + curl_easy_setopt(hCurl, CURLOPT_USERAGENT, S3fsCurl::userAgent.c_str()); + } + return true; +} + +int S3fsCurl::CurlDebugFunc(const CURL* hcurl, curl_infotype type, char* data, size_t size, void* userptr) +{ + return S3fsCurl::RawCurlDebugFunc(hcurl, type, data, size, userptr, CURLINFO_END); +} + +int S3fsCurl::CurlDebugBodyInFunc(const CURL* hcurl, curl_infotype type, char* data, size_t size, void* userptr) +{ + return S3fsCurl::RawCurlDebugFunc(hcurl, type, data, size, userptr, CURLINFO_DATA_IN); +} + +int S3fsCurl::CurlDebugBodyOutFunc(const CURL* hcurl, curl_infotype type, char* data, size_t size, void* userptr) +{ + return S3fsCurl::RawCurlDebugFunc(hcurl, type, data, size, userptr, CURLINFO_DATA_OUT); +} + +int S3fsCurl::RawCurlDebugFunc(const CURL* hcurl, curl_infotype type, char* data, size_t size, void* userptr, curl_infotype datatype) +{ + if(!hcurl){ + // something wrong... + return 0; + } + + switch(type){ + case CURLINFO_TEXT: + // Swap tab indentation with spaces so it stays pretty in syslog + int indent; + indent = 0; + while (*data == '\t' && size > 0) { + indent += 4; + size--; + data++; + } + if(foreground && 0 < size && '\n' == data[size - 1]){ + size--; + } + S3FS_PRN_CURL("* %*s%.*s", indent, "", (int)size, data); + break; + + case CURLINFO_DATA_IN: + case CURLINFO_DATA_OUT: + if(type != datatype || !S3fsCurl::is_dump_body){ + // not put + break; + } + case CURLINFO_HEADER_IN: + case CURLINFO_HEADER_OUT: + size_t remaining; + char* p; + + // Print each line individually for tidy output + remaining = size; + p = data; + do { + char* eol = reinterpret_cast(memchr(p, '\n', remaining)); + int newline = 0; + if (eol == nullptr) { + eol = reinterpret_cast(memchr(p, '\r', remaining)); + } else { + if (eol > p && *(eol - 1) == '\r') { + newline++; + } + newline++; + eol++; + } + size_t length = eol - p; + S3FS_PRN_CURL("%s %.*s", getCurlDebugHead(type), (int)length - newline, p); + remaining -= length; + p = eol; + } while (p != nullptr && remaining > 0); + break; + + case CURLINFO_SSL_DATA_IN: + case CURLINFO_SSL_DATA_OUT: + // not put + break; + default: + // why + break; + } + return 0; +} + +//------------------------------------------------------------------- +// Methods for S3fsCurl +//------------------------------------------------------------------- +S3fsCurl::S3fsCurl(bool ahbe) : + hCurl(nullptr), type(REQTYPE::UNSET), requestHeaders(nullptr), + LastResponseCode(S3FSCURL_RESPONSECODE_NOTSET), postdata(nullptr), postdata_remaining(0), is_use_ahbe(ahbe), + retry_count(0), b_infile(nullptr), b_postdata(nullptr), b_postdata_remaining(0), b_partdata_startpos(0), b_partdata_size(0), + b_ssekey_pos(-1), b_ssetype(sse_type_t::SSE_DISABLE), + sem(nullptr), completed_tids_lock(nullptr), completed_tids(nullptr), fpLazySetup(nullptr), curlCode(CURLE_OK) +{ + if(!S3fsCurl::ps3fscred){ + S3FS_PRN_CRIT("The object of S3fs Credential class is not initialized."); + abort(); + } +} + +S3fsCurl::~S3fsCurl() +{ + DestroyCurlHandle(); +} + +bool S3fsCurl::ResetHandle(AutoLock::Type locktype) +{ + bool run_once; + { + AutoLock lock(&S3fsCurl::curl_warnings_lock); + run_once = curl_warnings_once; + curl_warnings_once = true; + } + + sCurlPool->ResetHandler(hCurl); + + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_NOSIGNAL, 1)){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_FOLLOWLOCATION, true)){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_CONNECTTIMEOUT, S3fsCurl::connect_timeout)){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_NOPROGRESS, 0)){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, S3FS_CURLOPT_XFERINFOFUNCTION, S3fsCurl::CurlProgress)){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_PROGRESSDATA, hCurl)){ + return false; + } + // curl_easy_setopt(hCurl, CURLOPT_FORBID_REUSE, 1); + if(CURLE_OK != curl_easy_setopt(hCurl, S3FS_CURLOPT_TCP_KEEPALIVE, 1) && !run_once){ + S3FS_PRN_WARN("The CURLOPT_TCP_KEEPALIVE option could not be set. For maximize performance you need to enable this option and you should use libcurl 7.25.0 or later."); + } + if(CURLE_OK != curl_easy_setopt(hCurl, S3FS_CURLOPT_SSL_ENABLE_ALPN, 0) && !run_once){ + S3FS_PRN_WARN("The CURLOPT_SSL_ENABLE_ALPN option could not be unset. S3 server does not support ALPN, then this option should be disabled to maximize performance. you need to use libcurl 7.36.0 or later."); + } + if(CURLE_OK != curl_easy_setopt(hCurl, S3FS_CURLOPT_KEEP_SENDING_ON_ERROR, 1) && !run_once){ + S3FS_PRN_WARN("The S3FS_CURLOPT_KEEP_SENDING_ON_ERROR option could not be set. For maximize performance you need to enable this option and you should use libcurl 7.51.0 or later."); + } + + if(type != REQTYPE::IAMCRED && type != REQTYPE::IAMROLE){ + // REQTYPE::IAMCRED and REQTYPE::IAMROLE are always HTTP + if(0 == S3fsCurl::ssl_verify_hostname){ + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_SSL_VERIFYHOST, 0)){ + return false; + } + } + if(!S3fsCurl::curl_ca_bundle.empty()){ + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_CAINFO, S3fsCurl::curl_ca_bundle.c_str())){ + return false; + } + } + } + if((S3fsCurl::is_dns_cache || S3fsCurl::is_ssl_session_cache) && S3fsCurl::hCurlShare){ + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_SHARE, S3fsCurl::hCurlShare)){ + return false; + } + } + if(!S3fsCurl::is_cert_check) { + S3FS_PRN_DBG("'no_check_certificate' option in effect."); + S3FS_PRN_DBG("The server certificate won't be checked against the available certificate authorities."); + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_SSL_VERIFYPEER, false)){ + return false; + } + } + if(S3fsCurl::is_verbose){ + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_VERBOSE, true)){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_DEBUGFUNCTION, S3fsCurl::CurlDebugFunc)){ + return false; + } + } + if(!cipher_suites.empty()) { + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_SSL_CIPHER_LIST, cipher_suites.c_str())){ + return false; + } + } + if(!S3fsCurl::proxy_url.empty()){ + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_PROXY, S3fsCurl::proxy_url.c_str())){ + return false; + } + if(S3fsCurl::proxy_http){ + if(!S3fsCurl::proxy_userpwd.empty()){ + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_PROXYUSERPWD, S3fsCurl::proxy_userpwd.c_str())){ + return false; + } + } + }else if(!S3fsCurl::proxy_userpwd.empty()){ + S3FS_PRN_DBG("Username and passphrase are specified even though proxy is not 'http' scheme, so skip to set those."); + } + } + + AutoLock lock(&S3fsCurl::curl_handles_lock, locktype); + S3fsCurl::curl_times[hCurl] = time(nullptr); + S3fsCurl::curl_progress[hCurl] = progress_t(-1, -1); + + return true; +} + +bool S3fsCurl::CreateCurlHandle(bool only_pool, bool remake) +{ + AutoLock lock(&S3fsCurl::curl_handles_lock); + + if(hCurl && remake){ + if(!DestroyCurlHandle(false, true, AutoLock::ALREADY_LOCKED)){ + S3FS_PRN_ERR("could not destroy handle."); + return false; + } + S3FS_PRN_INFO3("already has handle, so destroyed it or restored it to pool."); + } + + if(!hCurl){ + if(nullptr == (hCurl = sCurlPool->GetHandler(only_pool))){ + if(!only_pool){ + S3FS_PRN_ERR("Failed to create handle."); + return false; + }else{ + // [NOTE] + // Further initialization processing is left to lazy processing to be executed later. + // (Currently we do not use only_pool=true, but this code is remained for the future) + return true; + } + } + } + ResetHandle(AutoLock::ALREADY_LOCKED); + + return true; +} + +bool S3fsCurl::DestroyCurlHandle(bool restore_pool, bool clear_internal_data, AutoLock::Type locktype) +{ + // [NOTE] + // If type is REQTYPE::IAMCRED or REQTYPE::IAMROLE, do not clear type. + // Because that type only uses HTTP protocol, then the special + // logic in ResetHandle function. + // + if(type != REQTYPE::IAMCRED && type != REQTYPE::IAMROLE){ + type = REQTYPE::UNSET; + } + + AutoLock lock(&S3fsCurl::curl_handles_lock, locktype); + + if(clear_internal_data){ + ClearInternalData(); + } + + if(hCurl){ + S3fsCurl::curl_times.erase(hCurl); + S3fsCurl::curl_progress.erase(hCurl); + sCurlPool->ReturnHandler(hCurl, restore_pool); + hCurl = nullptr; + }else{ + return false; + } + return true; +} + +bool S3fsCurl::ClearInternalData() +{ + // Always clear internal data + // + type = REQTYPE::UNSET; + path = ""; + base_path = ""; + saved_path = ""; + url = ""; + op = ""; + query_string= ""; + if(requestHeaders){ + curl_slist_free_all(requestHeaders); + requestHeaders = nullptr; + } + responseHeaders.clear(); + bodydata.clear(); + headdata.clear(); + LastResponseCode = S3FSCURL_RESPONSECODE_NOTSET; + postdata = nullptr; + postdata_remaining = 0; + retry_count = 0; + b_infile = nullptr; + b_postdata = nullptr; + b_postdata_remaining = 0; + b_partdata_startpos = 0; + b_partdata_size = 0; + partdata.clear(); + + fpLazySetup = nullptr; + + S3FS_MALLOCTRIM(0); + + return true; +} + +bool S3fsCurl::SetUseAhbe(bool ahbe) +{ + bool old = is_use_ahbe; + is_use_ahbe = ahbe; + return old; +} + +bool S3fsCurl::GetResponseCode(long& responseCode, bool from_curl_handle) const +{ + responseCode = -1; + + if(!from_curl_handle){ + responseCode = LastResponseCode; + }else{ + if(!hCurl){ + return false; + } + if(CURLE_OK != curl_easy_getinfo(hCurl, CURLINFO_RESPONSE_CODE, &LastResponseCode)){ + return false; + } + responseCode = LastResponseCode; + } + return true; +} + +// +// Reset all options for retrying +// +bool S3fsCurl::RemakeHandle() +{ + S3FS_PRN_INFO3("Retry request. [type=%d][url=%s][path=%s]", static_cast(type), url.c_str(), path.c_str()); + + if(REQTYPE::UNSET == type){ + return false; + } + + // rewind file + struct stat st; + if(b_infile){ + if(-1 == fseek(b_infile, 0, SEEK_SET)){ + S3FS_PRN_WARN("Could not reset position(fd=%d)", fileno(b_infile)); + return false; + } + if(-1 == fstat(fileno(b_infile), &st)){ + S3FS_PRN_WARN("Could not get file stat(fd=%d)", fileno(b_infile)); + return false; + } + } + + // reinitialize internal data + requestHeaders = curl_slist_remove(requestHeaders, "Authorization"); + responseHeaders.clear(); + bodydata.clear(); + headdata.clear(); + LastResponseCode = S3FSCURL_RESPONSECODE_NOTSET; + + // count up(only use for multipart) + retry_count++; + + // set from backup + postdata = b_postdata; + postdata_remaining = b_postdata_remaining; + partdata.startpos = b_partdata_startpos; + partdata.size = b_partdata_size; + + // reset handle + ResetHandle(); + + // set options + switch(type){ + case REQTYPE::DELETE: + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_URL, url.c_str())){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_CUSTOMREQUEST, "DELETE")){ + return false; + } + break; + + case REQTYPE::HEAD: + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_URL, url.c_str())){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_NOBODY, true)){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_FILETIME, true)){ + return false; + } + // responseHeaders + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_HEADERDATA, reinterpret_cast(&responseHeaders))){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_HEADERFUNCTION, HeaderCallback)){ + return false; + } + break; + + case REQTYPE::PUTHEAD: + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_URL, url.c_str())){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_UPLOAD, true)){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEDATA, reinterpret_cast(&bodydata))){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback)){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_INFILESIZE, 0)){ + return false; + } + break; + + case REQTYPE::PUT: + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_URL, url.c_str())){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_UPLOAD, true)){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEDATA, reinterpret_cast(&bodydata))){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback)){ + return false; + } + if(b_infile){ + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_INFILESIZE_LARGE, static_cast(st.st_size))){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_INFILE, b_infile)){ + return false; + } + }else{ + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_INFILESIZE, 0)){ + return false; + } + } + break; + + case REQTYPE::GET: + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_URL, url.c_str())){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEFUNCTION, S3fsCurl::DownloadWriteCallback)){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEDATA, reinterpret_cast(this))){ + return false; + } + break; + + case REQTYPE::CHKBUCKET: + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_URL, url.c_str())){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEDATA, reinterpret_cast(&bodydata))){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback)){ + return false; + } + break; + + case REQTYPE::LISTBUCKET: + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_URL, url.c_str())){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEDATA, reinterpret_cast(&bodydata))){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback)){ + return false; + } + break; + + case REQTYPE::PREMULTIPOST: + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_URL, url.c_str())){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_POST, true)){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEDATA, reinterpret_cast(&bodydata))){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback)){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_POSTFIELDSIZE, 0)){ + return false; + } + break; + + case REQTYPE::COMPLETEMULTIPOST: + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_URL, url.c_str())){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_POST, true)){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEDATA, reinterpret_cast(&bodydata))){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback)){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_POSTFIELDSIZE, static_cast(postdata_remaining))){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_READDATA, reinterpret_cast(this))){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_READFUNCTION, S3fsCurl::ReadCallback)){ + return false; + } + break; + + case REQTYPE::UPLOADMULTIPOST: + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_URL, url.c_str())){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_UPLOAD, true)){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEDATA, reinterpret_cast(&bodydata))){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback)){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_HEADERDATA, reinterpret_cast(&responseHeaders))){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_HEADERFUNCTION, HeaderCallback)){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_INFILESIZE_LARGE, static_cast(partdata.size))){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_READFUNCTION, S3fsCurl::UploadReadCallback)){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_READDATA, reinterpret_cast(this))){ + return false; + } + break; + + case REQTYPE::COPYMULTIPOST: + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_URL, url.c_str())){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_UPLOAD, true)){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEDATA, reinterpret_cast(&bodydata))){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback)){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_HEADERDATA, reinterpret_cast(&headdata))){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_HEADERFUNCTION, WriteMemoryCallback)){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_INFILESIZE, 0)){ + return false; + } + break; + + case REQTYPE::MULTILIST: + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_URL, url.c_str())){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEDATA, reinterpret_cast(&bodydata))){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback)){ + return false; + } + break; + + case REQTYPE::IAMCRED: + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_URL, url.c_str())){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEDATA, reinterpret_cast(&bodydata))){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback)){ + return false; + } + if(S3fsCurl::ps3fscred->IsIBMIAMAuth()){ + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_POST, true)){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_POSTFIELDSIZE, static_cast(postdata_remaining))){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_READDATA, reinterpret_cast(this))){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_READFUNCTION, S3fsCurl::ReadCallback)){ + return false; + } + } + break; + + case REQTYPE::ABORTMULTIUPLOAD: + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_URL, url.c_str())){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_CUSTOMREQUEST, "DELETE")){ + return false; + } + break; + + case REQTYPE::IAMROLE: + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_URL, url.c_str())){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEDATA, reinterpret_cast(&bodydata))){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback)){ + return false; + } + break; + + default: + S3FS_PRN_ERR("request type is unknown(%d)", static_cast(type)); + return false; + } + if(!S3fsCurl::AddUserAgent(hCurl)){ // put User-Agent + return false; + } + + return true; +} + +// +// returns curl return code +// +int S3fsCurl::RequestPerform(bool dontAddAuthHeaders /*=false*/) +{ + if(S3fsLog::IsS3fsLogDbg()){ + char* ptr_url = nullptr; + curl_easy_getinfo(hCurl, CURLINFO_EFFECTIVE_URL , &ptr_url); + S3FS_PRN_DBG("connecting to URL %s", SAFESTRPTR(ptr_url)); + } + + LastResponseCode = S3FSCURL_RESPONSECODE_NOTSET; + long responseCode = S3FSCURL_RESPONSECODE_NOTSET; + int result = S3FSCURL_PERFORM_RESULT_NOTSET; + + // 1 attempt + retries... + for(int retrycnt = 0; S3FSCURL_PERFORM_RESULT_NOTSET == result && retrycnt < S3fsCurl::retries; ++retrycnt){ + // Reset response code + responseCode = S3FSCURL_RESPONSECODE_NOTSET; + + // Insert headers + if(!dontAddAuthHeaders) { + insertAuthHeaders(); + } + + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_HTTPHEADER, requestHeaders)){ + return false; + } + + // Requests + curlCode = curl_easy_perform(hCurl); + + // Check result + switch(curlCode){ + case CURLE_OK: + // Need to look at the HTTP response code + if(0 != curl_easy_getinfo(hCurl, CURLINFO_RESPONSE_CODE, &responseCode)){ + S3FS_PRN_ERR("curl_easy_getinfo failed while trying to retrieve HTTP response code"); + responseCode = S3FSCURL_RESPONSECODE_FATAL_ERROR; + result = -EIO; + break; + } + if(responseCode >= 200 && responseCode < 300){ + S3FS_PRN_INFO3("HTTP response code %ld", responseCode); + result = 0; + break; + } + + { + // Try to parse more specific AWS error code otherwise fall back to HTTP error code. + std::string value; + if(simple_parse_xml(bodydata.c_str(), bodydata.size(), "Code", value)){ + // TODO: other error codes + if(value == "EntityTooLarge"){ + result = -EFBIG; + break; + }else if(value == "InvalidObjectState"){ + result = -EREMOTE; + break; + }else if(value == "KeyTooLongError"){ + result = -ENAMETOOLONG; + break; + } + } + } + + // Service response codes which are >= 300 && < 500 + switch(responseCode){ + case 301: + case 307: + S3FS_PRN_ERR("HTTP response code 301(Moved Permanently: also happens when bucket's region is incorrect), returning EIO. Body Text: %s", bodydata.c_str()); + S3FS_PRN_ERR("The options of url and endpoint may be useful for solving, please try to use both options."); + result = -EIO; + break; + + case 400: + if(op == "HEAD"){ + if(path.size() > 1024){ + S3FS_PRN_ERR("HEAD HTTP response code %ld with path longer than 1024, returning ENAMETOOLONG.", responseCode); + return -ENAMETOOLONG; + } + S3FS_PRN_ERR("HEAD HTTP response code %ld, returning EPERM.", responseCode); + result = -EPERM; + }else{ + S3FS_PRN_ERR("HTTP response code %ld, returning EIO. Body Text: %s", responseCode, bodydata.c_str()); + result = -EIO; + } + break; + + case 403: + S3FS_PRN_ERR("HTTP response code %ld, returning EPERM. Body Text: %s", responseCode, bodydata.c_str()); + result = -EPERM; + break; + + case 404: + S3FS_PRN_INFO3("HTTP response code 404 was returned, returning ENOENT"); + S3FS_PRN_DBG("Body Text: %s", bodydata.c_str()); + result = -ENOENT; + break; + + case 416: // 416 Requested Range Not Satisfiable + if(use_newcache){ + S3FS_PRN_INFO3("HTTP response code 416 was returned, returning ENOENT"); + result = -ENOENT; + }else{ + S3FS_PRN_INFO3("HTTP response code 416 was returned, returning EIO"); + result = -EIO; + } + break; + + case 501: + S3FS_PRN_INFO3("HTTP response code 501 was returned, returning ENOTSUP"); + S3FS_PRN_DBG("Body Text: %s", bodydata.c_str()); + result = -ENOTSUP; + break; + + case 500: + case 503: { + S3FS_PRN_INFO3("HTTP response code %ld was returned, slowing down", responseCode); + S3FS_PRN_DBG("Body Text: %s", bodydata.c_str()); + // Add jitter to avoid thundering herd. + unsigned int sleep_time = 2 << retry_count; + sleep(sleep_time + static_cast(random()) % sleep_time); + break; + } + default: + S3FS_PRN_ERR("HTTP response code %ld, returning EIO. Body Text: %s", responseCode, bodydata.c_str()); + result = -EIO; + break; + } + break; + + case CURLE_WRITE_ERROR: + S3FS_PRN_ERR("### CURLE_WRITE_ERROR"); + sleep(2); + break; + + case CURLE_OPERATION_TIMEDOUT: + S3FS_PRN_ERR("### CURLE_OPERATION_TIMEDOUT"); + sleep(2); + break; + + case CURLE_COULDNT_RESOLVE_HOST: + S3FS_PRN_ERR("### CURLE_COULDNT_RESOLVE_HOST"); + sleep(2); + break; + + case CURLE_COULDNT_CONNECT: + S3FS_PRN_ERR("### CURLE_COULDNT_CONNECT"); + sleep(4); + break; + + case CURLE_GOT_NOTHING: + S3FS_PRN_ERR("### CURLE_GOT_NOTHING"); + sleep(4); + break; + + case CURLE_ABORTED_BY_CALLBACK: + S3FS_PRN_ERR("### CURLE_ABORTED_BY_CALLBACK"); + sleep(4); + { + AutoLock lock(&S3fsCurl::curl_handles_lock); + S3fsCurl::curl_times[hCurl] = time(nullptr); + } + break; + + case CURLE_PARTIAL_FILE: + S3FS_PRN_ERR("### CURLE_PARTIAL_FILE"); + sleep(4); + break; + + case CURLE_SEND_ERROR: + S3FS_PRN_ERR("### CURLE_SEND_ERROR"); + sleep(2); + break; + + case CURLE_RECV_ERROR: + S3FS_PRN_ERR("### CURLE_RECV_ERROR"); + sleep(2); + break; + + case CURLE_SSL_CONNECT_ERROR: + S3FS_PRN_ERR("### CURLE_SSL_CONNECT_ERROR"); + sleep(2); + break; + + case CURLE_SSL_CACERT: + S3FS_PRN_ERR("### CURLE_SSL_CACERT"); + + // try to locate cert, if successful, then set the + // option and continue + if(S3fsCurl::curl_ca_bundle.empty()){ + if(!S3fsCurl::LocateBundle()){ + S3FS_PRN_ERR("could not get CURL_CA_BUNDLE."); + result = -EIO; + } + // retry with CAINFO + }else{ + S3FS_PRN_ERR("curlCode: %d msg: %s", curlCode, curl_easy_strerror(curlCode)); + result = -EIO; + } + break; + +#ifdef CURLE_PEER_FAILED_VERIFICATION + case CURLE_PEER_FAILED_VERIFICATION: + S3FS_PRN_ERR("### CURLE_PEER_FAILED_VERIFICATION"); + + first_pos = S3fsCred::GetBucket().find_first_of('.'); + if(first_pos != std::string::npos){ + S3FS_PRN_INFO("curl returned a CURL_PEER_FAILED_VERIFICATION error"); + S3FS_PRN_INFO("security issue found: buckets with periods in their name are incompatible with http"); + S3FS_PRN_INFO("This check can be over-ridden by using the -o ssl_verify_hostname=0"); + S3FS_PRN_INFO("The certificate will still be checked but the hostname will not be verified."); + S3FS_PRN_INFO("A more secure method would be to use a bucket name without periods."); + }else{ + S3FS_PRN_INFO("my_curl_easy_perform: curlCode: %d -- %s", curlCode, curl_easy_strerror(curlCode)); + } + result = -EIO; + break; +#endif + + // This should be invalid since curl option HTTP FAILONERROR is now off + case CURLE_HTTP_RETURNED_ERROR: + S3FS_PRN_ERR("### CURLE_HTTP_RETURNED_ERROR"); + + if(0 != curl_easy_getinfo(hCurl, CURLINFO_RESPONSE_CODE, &responseCode)){ + result = -EIO; + }else{ + S3FS_PRN_INFO3("HTTP response code =%ld", responseCode); + + // Let's try to retrieve the + if(404 == responseCode){ + result = -ENOENT; + }else if(500 > responseCode){ + result = -EIO; + } + } + break; + + // Unknown CURL return code + default: + S3FS_PRN_ERR("###curlCode: %d msg: %s", curlCode, curl_easy_strerror(curlCode)); + result = -EIO; + break; + } // switch + + if(S3FSCURL_PERFORM_RESULT_NOTSET == result){ + S3FS_PRN_INFO("### retrying..."); + + if(!RemakeHandle()){ + S3FS_PRN_INFO("Failed to reset handle and internal data for retrying."); + result = -EIO; + break; + } + } + } // for + + // set last response code + if(S3FSCURL_RESPONSECODE_NOTSET == responseCode){ + LastResponseCode = S3FSCURL_RESPONSECODE_FATAL_ERROR; + }else{ + LastResponseCode = responseCode; + } + + if(S3FSCURL_PERFORM_RESULT_NOTSET == result){ + S3FS_PRN_ERR("### giving up"); + result = -EIO; + } + return result; +} + +// +// Returns the Amazon AWS signature for the given parameters. +// +// @param method e.g., "GET" +// @param content_type e.g., "application/x-directory" +// @param date e.g., get_date_rfc850() +// @param resource e.g., "/pub" +// +std::string S3fsCurl::CalcSignatureV2(const std::string& method, const std::string& strMD5, const std::string& content_type, const std::string& date, const std::string& resource, const std::string& secret_access_key, const std::string& access_token) +{ + std::string Signature; + std::string StringToSign; + + if(!access_token.empty()){ + requestHeaders = curl_slist_sort_insert(requestHeaders, "x-amz-security-token", access_token.c_str()); + } + + StringToSign += method + "\n"; + StringToSign += strMD5 + "\n"; // md5 + StringToSign += content_type + "\n"; + StringToSign += date + "\n"; + StringToSign += get_canonical_headers(requestHeaders, true); + StringToSign += resource; + + const void* key = secret_access_key.data(); + size_t key_len = secret_access_key.size(); + const unsigned char* sdata = reinterpret_cast(StringToSign.data()); + size_t sdata_len = StringToSign.size(); + unsigned int md_len = 0; + + std::unique_ptr md = s3fs_HMAC(key, key_len, sdata, sdata_len, &md_len); + + Signature = s3fs_base64(md.get(), md_len); + + return Signature; +} + +std::string S3fsCurl::CalcSignature(const std::string& method, const std::string& canonical_uri, const std::string& query_string, const std::string& strdate, const std::string& payload_hash, const std::string& date8601, const std::string& secret_access_key, const std::string& access_token) +{ + std::string StringCQ, StringToSign; + std::string uriencode; + + if(!access_token.empty()){ + requestHeaders = curl_slist_sort_insert(requestHeaders, "x-amz-security-token", access_token.c_str()); + } + + uriencode = urlEncodePath(canonical_uri); + StringCQ = method + "\n"; + if(method == "HEAD" || method == "PUT" || method == "DELETE"){ + StringCQ += uriencode + "\n"; + }else if(method == "GET" && uriencode.empty()){ + StringCQ +="/\n"; + }else if(method == "GET" && is_prefix(uriencode.c_str(), "/")){ + StringCQ += uriencode +"\n"; + }else if(method == "GET" && !is_prefix(uriencode.c_str(), "/")){ + StringCQ += "/\n" + urlEncodeQuery(canonical_uri) +"\n"; + }else if(method == "POST"){ + StringCQ += uriencode + "\n"; + } + StringCQ += urlEncodeQuery(query_string) + "\n"; + StringCQ += get_canonical_headers(requestHeaders) + "\n"; + StringCQ += get_sorted_header_keys(requestHeaders) + "\n"; + StringCQ += payload_hash; + + std::string kSecret = "AWS4" + secret_access_key; + unsigned int kDate_len,kRegion_len, kService_len, kSigning_len = 0; + + std::unique_ptr kDate = s3fs_HMAC256(kSecret.c_str(), kSecret.size(), reinterpret_cast(strdate.data()), strdate.size(), &kDate_len); + std::unique_ptr kRegion = s3fs_HMAC256(kDate.get(), kDate_len, reinterpret_cast(endpoint.c_str()), endpoint.size(), &kRegion_len); + std::unique_ptr kService = s3fs_HMAC256(kRegion.get(), kRegion_len, reinterpret_cast("s3"), sizeof("s3") - 1, &kService_len); + std::unique_ptr kSigning = s3fs_HMAC256(kService.get(), kService_len, reinterpret_cast("aws4_request"), sizeof("aws4_request") - 1, &kSigning_len); + + const unsigned char* cRequest = reinterpret_cast(StringCQ.c_str()); + size_t cRequest_len = StringCQ.size(); + sha256_t sRequest; + s3fs_sha256(cRequest, cRequest_len, &sRequest); + + StringToSign = "AWS4-HMAC-SHA256\n"; + StringToSign += date8601 + "\n"; + StringToSign += strdate + "/" + endpoint + "/s3/aws4_request\n"; + StringToSign += s3fs_hex_lower(sRequest.data(), sRequest.size()); + + const unsigned char* cscope = reinterpret_cast(StringToSign.c_str()); + size_t cscope_len = StringToSign.size(); + unsigned int md_len = 0; + + std::unique_ptr md = s3fs_HMAC256(kSigning.get(), kSigning_len, cscope, cscope_len, &md_len); + + return s3fs_hex_lower(md.get(), md_len); +} + +void S3fsCurl::insertV4Headers(const std::string& access_key_id, const std::string& secret_access_key, const std::string& access_token) +{ + std::string server_path = type == REQTYPE::LISTBUCKET ? "/" : path; + std::string payload_hash; + switch (type) { + case REQTYPE::PUT: + if(GetUnsignedPayload()){ + payload_hash = "UNSIGNED-PAYLOAD"; + }else{ + if(use_newcache && !sha256.empty()){ + payload_hash = sha256; + }else{ + payload_hash = s3fs_sha256_hex_fd(b_infile == nullptr ? -1 : fileno(b_infile), 0, -1); + } + } + break; + + case REQTYPE::COMPLETEMULTIPOST: + { + size_t cRequest_len = strlen(reinterpret_cast(b_postdata)); + sha256_t sRequest; + s3fs_sha256(b_postdata, cRequest_len, &sRequest); + payload_hash = s3fs_hex_lower(sRequest.data(), sRequest.size()); + break; + } + + case REQTYPE::UPLOADMULTIPOST: + if(GetUnsignedPayload()){ + payload_hash = "UNSIGNED-PAYLOAD"; + }else{ + if(use_newcache){ + sha256_t sRequest; + s3fs_sha256(reinterpret_cast(partdata.buf), partdata.size, &sRequest); + payload_hash = s3fs_hex_lower(sRequest.data(), sRequest.size()); + }else{ + payload_hash = s3fs_sha256_hex_fd(partdata.fd, partdata.startpos, partdata.size); + } + } + break; + default: + break; + } + + if(b_infile != nullptr && payload_hash.empty()){ + S3FS_PRN_ERR("Failed to make SHA256."); + // TODO: propagate error + } + + S3FS_PRN_INFO3("computing signature [%s] [%s] [%s] [%s]", op.c_str(), server_path.c_str(), query_string.c_str(), payload_hash.c_str()); + std::string strdate; + std::string date8601; + get_date_sigv3(strdate, date8601); + + std::string contentSHA256 = payload_hash.empty() ? EMPTY_PAYLOAD_HASH : payload_hash; + const std::string realpath = pathrequeststyle ? "/" + S3fsCred::GetBucket() + server_path : server_path; + + //string canonical_headers, signed_headers; + requestHeaders = curl_slist_sort_insert(requestHeaders, "host", get_bucket_host().c_str()); + requestHeaders = curl_slist_sort_insert(requestHeaders, "x-amz-content-sha256", contentSHA256.c_str()); + requestHeaders = curl_slist_sort_insert(requestHeaders, "x-amz-date", date8601.c_str()); + + if (S3fsCurl::IsRequesterPays()) { + requestHeaders = curl_slist_sort_insert(requestHeaders, "x-amz-request-payer", "requester"); + } + + if(!S3fsCurl::IsPublicBucket()){ + std::string Signature = CalcSignature(op, realpath, query_string + (type == REQTYPE::PREMULTIPOST || type == REQTYPE::MULTILIST ? "=" : ""), strdate, contentSHA256, date8601, secret_access_key, access_token); + std::string auth = "AWS4-HMAC-SHA256 Credential=" + access_key_id + "/" + strdate + "/" + endpoint + "/s3/aws4_request, SignedHeaders=" + get_sorted_header_keys(requestHeaders) + ", Signature=" + Signature; + requestHeaders = curl_slist_sort_insert(requestHeaders, "Authorization", auth.c_str()); + } +} + +void S3fsCurl::insertV2Headers(const std::string& access_key_id, const std::string& secret_access_key, const std::string& access_token) +{ + std::string resource; + std::string turl; + std::string server_path = type == REQTYPE::LISTBUCKET ? "/" : path; + MakeUrlResource(server_path.c_str(), resource, turl); + if(!query_string.empty() && type != REQTYPE::CHKBUCKET && type != REQTYPE::LISTBUCKET){ + resource += "?" + query_string; + } + + std::string date = get_date_rfc850(); + requestHeaders = curl_slist_sort_insert(requestHeaders, "Date", date.c_str()); + if(op != "PUT" && op != "POST"){ + requestHeaders = curl_slist_sort_insert(requestHeaders, "Content-Type", nullptr); + } + + if(!S3fsCurl::IsPublicBucket()){ + std::string Signature = CalcSignatureV2(op, get_header_value(requestHeaders, "Content-MD5"), get_header_value(requestHeaders, "Content-Type"), date, resource, secret_access_key, access_token); + requestHeaders = curl_slist_sort_insert(requestHeaders, "Authorization", ("AWS " + access_key_id + ":" + Signature).c_str()); + } +} + +void S3fsCurl::insertIBMIAMHeaders(const std::string& access_key_id, const std::string& access_token) +{ + requestHeaders = curl_slist_sort_insert(requestHeaders, "Authorization", ("Bearer " + access_token).c_str()); + + if(op == "PUT" && path == mount_prefix + "/"){ + // ibm-service-instance-id header is required for bucket creation requests + requestHeaders = curl_slist_sort_insert(requestHeaders, "ibm-service-instance-id", access_key_id.c_str()); + } +} + +void S3fsCurl::insertAuthHeaders() +{ + std::string access_key_id; + std::string secret_access_key; + std::string access_token; + + // check and get credential variables + if(!S3fsCurl::ps3fscred->CheckIAMCredentialUpdate(&access_key_id, &secret_access_key, &access_token)){ + S3FS_PRN_ERR("An error occurred in checking IAM credential."); + return; // do not insert auth headers on error + } + + if(S3fsCurl::ps3fscred->IsIBMIAMAuth()){ + insertIBMIAMHeaders(access_key_id, access_token); + }else if(S3fsCurl::signature_type == signature_type_t::V2_ONLY){ + insertV2Headers(access_key_id, secret_access_key, access_token); + }else{ + insertV4Headers(access_key_id, secret_access_key, access_token); + } +} + +int S3fsCurl::DeleteRequest(const char* tpath) +{ + S3FS_PRN_INFO3("[tpath=%s]", SAFESTRPTR(tpath)); + + if(!tpath){ + return -EINVAL; + } + if(!CreateCurlHandle()){ + return -EIO; + } + std::string resource; + std::string turl; + MakeUrlResource(get_realpath(tpath).c_str(), resource, turl); + + url = prepare_url(turl.c_str()); + path = get_realpath(tpath); + requestHeaders = nullptr; + responseHeaders.clear(); + + op = "DELETE"; + type = REQTYPE::DELETE; + + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_URL, url.c_str())){ + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_CUSTOMREQUEST, "DELETE")){ + return -EIO; + } + if(!S3fsCurl::AddUserAgent(hCurl)){ // put User-Agent + return -EIO; + } + + return RequestPerform(); +} + +int S3fsCurl::GetIAMv2ApiToken(const char* token_url, int token_ttl, const char* token_ttl_hdr, std::string& response) +{ + if(!token_url || !token_ttl_hdr){ + S3FS_PRN_ERR("IAMv2 token url(%s) or ttl_hdr(%s) parameter are wrong.", token_url ? token_url : "null", token_ttl_hdr ? token_ttl_hdr : "null"); + return -EIO; + } + response.erase(); + url = token_url; + if(!CreateCurlHandle()){ + return -EIO; + } + requestHeaders = nullptr; + responseHeaders.clear(); + bodydata.clear(); + + std::string ttlstr = std::to_string(token_ttl); + requestHeaders = curl_slist_sort_insert(requestHeaders, token_ttl_hdr, ttlstr.c_str()); + + // Curl appends an "Expect: 100-continue" header to the token request, + // and aws responds with a 417 Expectation Failed. This ensures the + // Expect header is empty before the request is sent. + requestHeaders = curl_slist_sort_insert(requestHeaders, "Expect", ""); + + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_UPLOAD, true)){ + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_URL, url.c_str())){ + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEDATA, reinterpret_cast(&bodydata))){ + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback)){ + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_INFILESIZE, 0)){ + return false; + } + if(!S3fsCurl::AddUserAgent(hCurl)){ // put User-Agent + return -EIO; + } + + // [NOTE] + // Be sure to give "dontAddAuthHeaders=true". + // If set to false(default), it will deadlock in S3fsCred. + // + int result = RequestPerform(true); + + if(0 == result){ + response.swap(bodydata); + }else{ + S3FS_PRN_ERR("Error(%d) occurred, could not get IAMv2 api token.", result); + } + bodydata.clear(); + + return result; +} + +// +// Get AccessKeyId/SecretAccessKey/AccessToken/Expiration by IAM role, +// and Set these value to class variable. +// +bool S3fsCurl::GetIAMCredentials(const char* cred_url, const char* iam_v2_token, const char* ibm_secret_access_key, std::string& response) +{ + if(!cred_url){ + S3FS_PRN_ERR("url is null."); + return false; + } + url = cred_url; + response.erase(); + + // at first set type for handle + type = REQTYPE::IAMCRED; + + if(!CreateCurlHandle()){ + return false; + } + requestHeaders = nullptr; + responseHeaders.clear(); + bodydata.clear(); + std::string postContent; + + if(ibm_secret_access_key){ + // make contents + postContent += "grant_type=urn:ibm:params:oauth:grant-type:apikey"; + postContent += "&response_type=cloud_iam"; + postContent += "&apikey="; + postContent += ibm_secret_access_key; + + // set postdata + postdata = reinterpret_cast(postContent.c_str()); + b_postdata = postdata; + postdata_remaining = postContent.size(); // without null + b_postdata_remaining = postdata_remaining; + + requestHeaders = curl_slist_sort_insert(requestHeaders, "Authorization", "Basic Yng6Yng="); + + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_POST, true)){ // POST + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_POSTFIELDSIZE, static_cast(postdata_remaining))){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_READDATA, reinterpret_cast(this))){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_READFUNCTION, S3fsCurl::ReadCallback)){ + return false; + } + } + + if(iam_v2_token){ + requestHeaders = curl_slist_sort_insert(requestHeaders, S3fsCred::IAMv2_token_hdr, iam_v2_token); + } + + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_URL, url.c_str())){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEDATA, reinterpret_cast(&bodydata))){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback)){ + return false; + } + if(!S3fsCurl::AddUserAgent(hCurl)){ // put User-Agent + return false; + } + + // [NOTE] + // Be sure to give "dontAddAuthHeaders=true". + // If set to false(default), it will deadlock in S3fsCred. + // + int result = RequestPerform(true); + + // analyzing response + if(0 == result){ + response.swap(bodydata); + }else{ + S3FS_PRN_ERR("Error(%d) occurred, could not get IAM role name.", result); + } + bodydata.clear(); + + return (0 == result); +} + +// +// Get IAM role name automatically. +// +bool S3fsCurl::GetIAMRoleFromMetaData(const char* cred_url, const char* iam_v2_token, std::string& token) +{ + if(!cred_url){ + S3FS_PRN_ERR("url is null."); + return false; + } + url = cred_url; + token.erase(); + + S3FS_PRN_INFO3("Get IAM Role name"); + + // at first set type for handle + type = REQTYPE::IAMROLE; + + if(!CreateCurlHandle()){ + return false; + } + requestHeaders = nullptr; + responseHeaders.clear(); + bodydata.clear(); + + if(iam_v2_token){ + requestHeaders = curl_slist_sort_insert(requestHeaders, S3fsCred::IAMv2_token_hdr, iam_v2_token); + } + + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_URL, url.c_str())){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEDATA, reinterpret_cast(&bodydata))){ + return false; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback)){ + return false; + } + if(!S3fsCurl::AddUserAgent(hCurl)){ // put User-Agent + return false; + } + + // [NOTE] + // Be sure to give "dontAddAuthHeaders=true". + // If set to false(default), it will deadlock in S3fsCred. + // + int result = RequestPerform(true); + + // analyzing response + if(0 == result){ + token.swap(bodydata); + }else{ + S3FS_PRN_ERR("Error(%d) occurred, could not get IAM role name from meta data.", result); + } + bodydata.clear(); + + return (0 == result); +} + +bool S3fsCurl::AddSseRequestHead(sse_type_t ssetype, const std::string& input, bool is_copy) +{ + std::string ssevalue = input; + switch(ssetype){ + case sse_type_t::SSE_DISABLE: + return true; + case sse_type_t::SSE_S3: + if(!is_copy){ + requestHeaders = curl_slist_sort_insert(requestHeaders, "x-amz-server-side-encryption", "AES256"); + } + return true; + case sse_type_t::SSE_C: + { + std::string sseckey; + if(S3fsCurl::GetSseKey(ssevalue, sseckey)){ + if(is_copy){ + requestHeaders = curl_slist_sort_insert(requestHeaders, "x-amz-copy-source-server-side-encryption-customer-algorithm", "AES256"); + requestHeaders = curl_slist_sort_insert(requestHeaders, "x-amz-copy-source-server-side-encryption-customer-key", sseckey.c_str()); + requestHeaders = curl_slist_sort_insert(requestHeaders, "x-amz-copy-source-server-side-encryption-customer-key-md5", ssevalue.c_str()); + }else{ + requestHeaders = curl_slist_sort_insert(requestHeaders, "x-amz-server-side-encryption-customer-algorithm", "AES256"); + requestHeaders = curl_slist_sort_insert(requestHeaders, "x-amz-server-side-encryption-customer-key", sseckey.c_str()); + requestHeaders = curl_slist_sort_insert(requestHeaders, "x-amz-server-side-encryption-customer-key-md5", ssevalue.c_str()); + } + }else{ + S3FS_PRN_WARN("Failed to insert SSE-C header."); + } + return true; + } + case sse_type_t::SSE_KMS: + if(!is_copy){ + if(ssevalue.empty()){ + ssevalue = S3fsCurl::GetSseKmsId(); + } + requestHeaders = curl_slist_sort_insert(requestHeaders, "x-amz-server-side-encryption", "aws:kms"); + requestHeaders = curl_slist_sort_insert(requestHeaders, "x-amz-server-side-encryption-aws-kms-key-id", ssevalue.c_str()); + } + return true; + } + S3FS_PRN_ERR("sse type is unknown(%d).", static_cast(S3fsCurl::ssetype)); + + return false; +} + +// +// tpath : target path for head request +// bpath : saved into base_path +// savedpath : saved into saved_path +// ssekey_pos : -1 means "not" SSE-C type +// 0 - X means SSE-C type and position for SSE-C key(0 is latest key) +// +bool S3fsCurl::PreHeadRequest(const char* tpath, const char* bpath, const char* savedpath, size_t ssekey_pos) +{ + S3FS_PRN_INFO3("[tpath=%s][bpath=%s][save=%s][sseckeypos=%zu]", SAFESTRPTR(tpath), SAFESTRPTR(bpath), SAFESTRPTR(savedpath), ssekey_pos); + + if(!tpath){ + return false; + } + std::string resource; + std::string turl; + MakeUrlResource(get_realpath(tpath).c_str(), resource, turl); + + // libcurl 7.17 does deep copy of url, deep copy "stable" url + url = prepare_url(turl.c_str()); + path = get_realpath(tpath); + base_path = SAFESTRPTR(bpath); + saved_path = SAFESTRPTR(savedpath); + requestHeaders = nullptr; + responseHeaders.clear(); + + // requestHeaders(SSE-C) + if(0 <= static_cast(ssekey_pos) && ssekey_pos < S3fsCurl::sseckeys.size()){ + std::string md5; + if(!S3fsCurl::GetSseKeyMd5(ssekey_pos, md5) || !AddSseRequestHead(sse_type_t::SSE_C, md5, false)){ + S3FS_PRN_ERR("Failed to set SSE-C headers for sse-c key pos(%zu)(=md5(%s)).", ssekey_pos, md5.c_str()); + return false; + } + } + b_ssekey_pos = ssekey_pos; + + op = "HEAD"; + type = REQTYPE::HEAD; + + // set lazy function + fpLazySetup = PreHeadRequestSetCurlOpts; + + return true; +} + +int S3fsCurl::HeadRequest(const char* tpath, headers_t& meta) +{ + int result = -1; + + S3FS_PRN_INFO3("[tpath=%s]", SAFESTRPTR(tpath)); + + // At first, try to get without SSE-C headers + if(!PreHeadRequest(tpath) || !fpLazySetup || !fpLazySetup(this) || 0 != (result = RequestPerform())){ + // If has SSE-C keys, try to get with all SSE-C keys. + for(size_t pos = 0; pos < S3fsCurl::sseckeys.size(); pos++){ + if(!DestroyCurlHandle()){ + break; + } + if(!PreHeadRequest(tpath, nullptr, nullptr, pos)){ + break; + } + if(!fpLazySetup || !fpLazySetup(this)){ + S3FS_PRN_ERR("Failed to lazy setup in single head request."); + break; + } + if(0 == (result = RequestPerform())){ + break; + } + } + if(0 != result){ + DestroyCurlHandle(); // not check result. + return result; + } + } + + // file exists in s3 + // fixme: clean this up. + meta.clear(); + for(headers_t::iterator iter = responseHeaders.begin(); iter != responseHeaders.end(); ++iter){ + std::string key = lower(iter->first); + std::string value = iter->second; + if(key == "content-type"){ + meta[iter->first] = value; + }else if(key == "content-length"){ + meta[iter->first] = value; + }else if(key == "etag"){ + meta[iter->first] = value; + }else if(key == "last-modified"){ + meta[iter->first] = value; + }else if(is_prefix(key.c_str(), "x-amz")){ + meta[key] = value; // key is lower case for "x-amz" + } + } + return 0; +} + +int S3fsCurl::PutHeadRequest(const char* tpath, headers_t& meta, bool is_copy) +{ + S3FS_PRN_INFO3("[tpath=%s]", SAFESTRPTR(tpath)); + + if(!tpath){ + return -EINVAL; + } + if(!CreateCurlHandle()){ + return -EIO; + } + std::string resource; + std::string turl; + MakeUrlResource(get_realpath(tpath).c_str(), resource, turl); + + url = prepare_url(turl.c_str()); + path = get_realpath(tpath); + requestHeaders = nullptr; + responseHeaders.clear(); + bodydata.clear(); + + std::string contype = S3fsCurl::LookupMimeType(tpath); + requestHeaders = curl_slist_sort_insert(requestHeaders, "Content-Type", contype.c_str()); + + // Make request headers + for(headers_t::iterator iter = meta.begin(); iter != meta.end(); ++iter){ + std::string key = lower(iter->first); + std::string value = iter->second; + if(is_prefix(key.c_str(), "x-amz-acl")){ + // not set value, but after set it. + }else if(is_prefix(key.c_str(), "x-amz-meta")){ + requestHeaders = curl_slist_sort_insert(requestHeaders, iter->first.c_str(), value.c_str()); + }else if(key == "x-amz-copy-source"){ + requestHeaders = curl_slist_sort_insert(requestHeaders, iter->first.c_str(), value.c_str()); + }else if(key == "x-amz-server-side-encryption" && value != "aws:kms"){ + // skip this header, because this header is specified after logic. + }else if(key == "x-amz-server-side-encryption-aws-kms-key-id"){ + // skip this header, because this header is specified after logic. + }else if(key == "x-amz-server-side-encryption-customer-key-md5"){ + // Only copy mode. + if(is_copy){ + if(!AddSseRequestHead(sse_type_t::SSE_C, value, true)){ + S3FS_PRN_WARN("Failed to insert SSE-C header."); + } + } + } + } + + // "x-amz-acl", storage class, sse + if(S3fsCurl::default_acl != acl_t::PRIVATE){ + requestHeaders = curl_slist_sort_insert(requestHeaders, "x-amz-acl", str(S3fsCurl::default_acl)); + } + if(strcasecmp(GetStorageClass().c_str(), "STANDARD") != 0){ + requestHeaders = curl_slist_sort_insert(requestHeaders, "x-amz-storage-class", GetStorageClass().c_str()); + } + // SSE + if(S3fsCurl::GetSseType() != sse_type_t::SSE_DISABLE){ + std::string ssevalue; + if(!AddSseRequestHead(S3fsCurl::GetSseType(), ssevalue, false)){ + S3FS_PRN_WARN("Failed to set SSE header, but continue..."); + } + } + if(is_use_ahbe){ + // set additional header by ahbe conf + requestHeaders = AdditionalHeader::get()->AddHeader(requestHeaders, tpath); + } + + op = "PUT"; + type = REQTYPE::PUTHEAD; + + // setopt + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_URL, url.c_str())){ + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_UPLOAD, true)){ // HTTP PUT + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEDATA, reinterpret_cast(&bodydata))){ + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback)){ + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_INFILESIZE, 0)){ // Content-Length + return -EIO; + } + if(!S3fsCurl::AddUserAgent(hCurl)){ // put User-Agent + return -EIO; + } + + S3FS_PRN_INFO3("copying... [path=%s]", tpath); + + int result = RequestPerform(); + result = MapPutErrorResponse(result); + bodydata.clear(); + + return result; +} + +int S3fsCurl::PutRequest(const char* tpath, headers_t& meta, int fd, off_t fsize, char* buf) +{ + struct stat st; + std::unique_ptr file(nullptr, &s3fs_fclose); + + S3FS_PRN_INFO3("[tpath=%s]", SAFESTRPTR(tpath)); + + if(!tpath){ + return -EINVAL; + } + if (use_newcache) { + }else if(-1 != fd){ + // duplicate fd + // + // [NOTE] + // This process requires FILE*, then it is linked to fd with fdopen. + // After processing, the FILE* is closed with fclose, and fd is closed together. + // The fd should not be closed here, so call dup here to duplicate it. + // + int fd2; + if(-1 == (fd2 = dup(fd)) || -1 == fstat(fd2, &st) || 0 != lseek(fd2, 0, SEEK_SET) || nullptr == (file = {fdopen(fd2, "rb"), &s3fs_fclose})){ + S3FS_PRN_ERR("Could not duplicate file descriptor(errno=%d)", errno); + if(-1 != fd2){ + close(fd2); + } + return -errno; + } + b_infile = file.get(); + }else{ + // This case is creating zero byte object.(calling by create_file_object()) + S3FS_PRN_INFO3("create zero byte file object."); + } + + if(!CreateCurlHandle()){ + return -EIO; + } + std::string resource; + std::string turl; + MakeUrlResource(get_realpath(tpath).c_str(), resource, turl); + + url = prepare_url(turl.c_str()); + path = get_realpath(tpath); + requestHeaders = nullptr; + responseHeaders.clear(); + bodydata.clear(); + + // Make request headers + if(S3fsCurl::is_content_md5){ + std::string strMD5; + if(use_newcache){ + strMD5 = s3fs_get_content_md5(fsize, buf); + if(0 == strMD5.length()){ + S3FS_PRN_ERR("Failed to make MD5."); + return -EIO; + } + }else if(-1 != fd){ + strMD5 = s3fs_get_content_md5(fd); + if(0 == strMD5.length()){ + S3FS_PRN_ERR("Failed to make MD5."); + return -EIO; + } + }else{ + strMD5 = EMPTY_MD5_BASE64_HASH; + } + requestHeaders = curl_slist_sort_insert(requestHeaders, "Content-MD5", strMD5.c_str()); + } + + std::string contype = S3fsCurl::LookupMimeType(tpath); + requestHeaders = curl_slist_sort_insert(requestHeaders, "Content-Type", contype.c_str()); + + for(headers_t::iterator iter = meta.begin(); iter != meta.end(); ++iter){ + std::string key = lower(iter->first); + std::string value = iter->second; + if(is_prefix(key.c_str(), "x-amz-acl")){ + // not set value, but after set it. + }else if(is_prefix(key.c_str(), "x-amz-meta")){ + requestHeaders = curl_slist_sort_insert(requestHeaders, iter->first.c_str(), value.c_str()); + }else if(key == "x-amz-server-side-encryption" && value != "aws:kms"){ + // skip this header, because this header is specified after logic. + }else if(key == "x-amz-server-side-encryption-aws-kms-key-id"){ + // skip this header, because this header is specified after logic. + }else if(key == "x-amz-server-side-encryption-customer-key-md5"){ + // skip this header, because this header is specified after logic. + } + } + // "x-amz-acl", storage class, sse + if(S3fsCurl::default_acl != acl_t::PRIVATE){ + requestHeaders = curl_slist_sort_insert(requestHeaders, "x-amz-acl", str(S3fsCurl::default_acl)); + } + if(strcasecmp(GetStorageClass().c_str(), "STANDARD") != 0){ + requestHeaders = curl_slist_sort_insert(requestHeaders, "x-amz-storage-class", GetStorageClass().c_str()); + } + // SSE + // do not add SSE for create bucket + if(0 != strcmp(tpath, "/")){ + std::string ssevalue; + if(!AddSseRequestHead(S3fsCurl::GetSseType(), ssevalue, false)){ + S3FS_PRN_WARN("Failed to set SSE header, but continue..."); + } + } + if(is_use_ahbe){ + // set additional header by ahbe conf + requestHeaders = AdditionalHeader::get()->AddHeader(requestHeaders, tpath); + } + + op = "PUT"; + type = REQTYPE::PUT; + + // setopt + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_URL, url.c_str())){ + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_UPLOAD, true)){ // HTTP PUT + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEDATA, reinterpret_cast(&bodydata))){ + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback)){ + return -EIO; + } + + if(use_newcache){ + // ps: Minio not support PostObject + if(0 < fsize){ + sha256_t sRequest; + s3fs_sha256(reinterpret_cast(buf), fsize, &sRequest); + sha256 = s3fs_hex_lower(sRequest.data(), sRequest.size()); + + drp_upload_ctx ctx(path, buf, 0, fsize); + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_READFUNCTION, UploadReadCallbackByMemory)){ + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_READDATA, ctx)){ // set memory data + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_INFILESIZE_LARGE, fsize)){ // Content-Length + return -EIO; + } + }else if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_INFILESIZE, 0)){ // Content-Length: 0 + return -EIO; + } + }else if(file){ + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_INFILESIZE_LARGE, static_cast(st.st_size))){ // Content-Length + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_INFILE, file.get())){ + return -EIO; + } + }else{ + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_INFILESIZE, 0)){ // Content-Length: 0 + return -EIO; + } + } + if(!S3fsCurl::AddUserAgent(hCurl)){ // put User-Agent + return -EIO; + } + + off_t size = 0; + if(use_newcache){ + size = fsize; + }else{ + size = -1 != fd ? st.st_size : 0; + } + S3FS_PRN_INFO3("uploading... [path=%s][fd=%d][size=%lld]", tpath, fd, static_cast(size)); + + int result = RequestPerform(); + result = MapPutErrorResponse(result); + bodydata.clear(); + return result; +} + +int S3fsCurl::PreGetObjectRequest(const char* tpath, int fd, off_t start, off_t size, sse_type_t ssetype, const std::string& ssevalue, char* buf) +{ + S3FS_PRN_INFO3("[tpath=%s][start=%lld][size=%lld]", SAFESTRPTR(tpath), static_cast(start), static_cast(size)); + + if(!tpath || -1 == fd || 0 > start || 0 > size){ + return -EINVAL; + } + + std::string resource; + std::string turl; + MakeUrlResource(get_realpath(tpath).c_str(), resource, turl); + + url = prepare_url(turl.c_str()); + path = get_realpath(tpath); + requestHeaders = nullptr; + responseHeaders.clear(); + + if(0 < size){ + std::string range = "bytes="; + range += std::to_string(start); + range += "-"; + range += std::to_string(start + size - 1); + requestHeaders = curl_slist_sort_insert(requestHeaders, "Range", range.c_str()); + } + // SSE-C + if(sse_type_t::SSE_C == ssetype){ + if(!AddSseRequestHead(ssetype, ssevalue, false)){ + S3FS_PRN_WARN("Failed to set SSE header, but continue..."); + } + } + + op = "GET"; + type = REQTYPE::GET; + + // set lazy function + fpLazySetup = PreGetObjectRequestSetCurlOpts; + + // set info for callback func. + // (use only fd, startpos and size, other member is not used.) + partdata.clear(); + partdata.fd = fd; + partdata.startpos = start; + partdata.size = size; + partdata.buf = buf; + b_partdata_startpos = start; + b_partdata_size = size; + b_partdata_buf = buf; + b_ssetype = ssetype; + b_ssevalue = ssevalue; + b_ssekey_pos = -1; // not use this value for get object. + + return 0; +} + +int S3fsCurl::GetObjectRequest(const char* tpath, int fd, off_t start, off_t size, char* buf) +{ + int result; + + S3FS_PRN_INFO3("[tpath=%s][start=%lld][size=%lld]", SAFESTRPTR(tpath), static_cast(start), static_cast(size)); + + if(!tpath){ + return -EINVAL; + } + sse_type_t local_ssetype = sse_type_t::SSE_DISABLE; + std::string ssevalue; + if(!get_object_sse_type(tpath, local_ssetype, ssevalue)){ + S3FS_PRN_WARN("Failed to get SSE type for file(%s).", SAFESTRPTR(tpath)); + } + + if(0 != (result = PreGetObjectRequest(tpath, fd, start, size, local_ssetype, ssevalue, buf))){ + return result; + } + if(!fpLazySetup || !fpLazySetup(this)){ + S3FS_PRN_ERR("Failed to lazy setup in single get object request."); + return -EIO; + } + + S3FS_PRN_INFO3("downloading... [path=%s][fd=%d]", tpath, fd); + + result = RequestPerform(); + partdata.clear(); + + return result; +} + +int S3fsCurl::CheckBucket(const char* check_path, bool compat_dir, bool force_no_sse) +{ + S3FS_PRN_INFO3("check a bucket path(%s)%s.", (check_path && 0 < strlen(check_path)) ? check_path : "", compat_dir ? " containing compatible directory paths" : ""); + + if(!check_path || 0 == strlen(check_path)){ + return -EIO; + } + if(!CreateCurlHandle()){ + return -EIO; + } + + std::string strCheckPath; + std::string urlargs; + if(S3fsCurl::IsListObjectsV2()){ + query_string = "list-type=2"; + }else{ + query_string.clear(); + } + if(!compat_dir){ + // do not check compatibility directories + strCheckPath = check_path; + + }else{ + // check path including compatibility directory + strCheckPath = "/"; + + if(1 < strlen(check_path)){ // for directory path ("/...") not root path("/") + if(!query_string.empty()){ + query_string += '&'; + } + query_string += "prefix="; + query_string += &check_path[1]; // skip first '/' charactor + } + } + if(!query_string.empty()){ + urlargs = "?" + query_string; + } + + std::string resource; + std::string turl; + MakeUrlResource(strCheckPath.c_str(), resource, turl); + + turl += urlargs; + url = prepare_url(turl.c_str()); + path = strCheckPath; + requestHeaders = nullptr; + responseHeaders.clear(); + bodydata.clear(); + + // SSE + if(!force_no_sse && S3fsCurl::GetSseType() != sse_type_t::SSE_DISABLE){ + std::string ssevalue; + if(!AddSseRequestHead(S3fsCurl::GetSseType(), ssevalue, false)){ + S3FS_PRN_WARN("Failed to set SSE header, but continue..."); + } + } + + op = "GET"; + type = REQTYPE::CHKBUCKET; + + // setopt + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_URL, url.c_str())){ + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEDATA, reinterpret_cast(&bodydata))){ + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback)){ + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_UNRESTRICTED_AUTH, 1L)){ + return -EIO; + } + if(!S3fsCurl::AddUserAgent(hCurl)){ // put User-Agent + return -EIO; + } + + int result = RequestPerform(); + if (result != 0) { + S3FS_PRN_ERR("Check bucket failed, S3 response: %s", bodydata.c_str()); + } + return result; +} + +int S3fsCurl::ListBucketRequest(const char* tpath, const char* query) +{ + S3FS_PRN_INFO3("[tpath=%s]", SAFESTRPTR(tpath)); + + if(!tpath){ + return -EINVAL; + } + if(!CreateCurlHandle()){ + return -EIO; + } + std::string resource; + std::string turl; + MakeUrlResource("", resource, turl); // NOTICE: path is "". + if(query){ + turl += "?"; + turl += query; + query_string = query; + } + + url = prepare_url(turl.c_str()); + path = get_realpath(tpath); + requestHeaders = nullptr; + responseHeaders.clear(); + bodydata.clear(); + + op = "GET"; + type = REQTYPE::LISTBUCKET; + + // setopt + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_URL, url.c_str())){ + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEDATA, reinterpret_cast(&bodydata))){ + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback)){ + return -EIO; + } + if(S3fsCurl::is_verbose){ + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_DEBUGFUNCTION, S3fsCurl::CurlDebugBodyInFunc)){ // replace debug function + return -EIO; + } + } + if(!S3fsCurl::AddUserAgent(hCurl)){ // put User-Agent + return -EIO; + } + + return RequestPerform(); +} + +// +// Initialize multipart upload +// +// Example : +// POST /example-object?uploads HTTP/1.1 +// Host: example-bucket.s3.amazonaws.com +// Date: Mon, 1 Nov 2010 20:34:56 GMT +// Authorization: AWS VGhpcyBtZXNzYWdlIHNpZ25lZCBieSBlbHZpbmc= +// +int S3fsCurl::PreMultipartPostRequest(const char* tpath, headers_t& meta, std::string& upload_id, bool is_copy) +{ + S3FS_PRN_INFO3("[tpath=%s]", SAFESTRPTR(tpath)); + + if(!tpath){ + return -EINVAL; + } + if(!CreateCurlHandle()){ + return -EIO; + } + std::string resource; + std::string turl; + MakeUrlResource(get_realpath(tpath).c_str(), resource, turl); + + query_string = "uploads"; + turl += "?" + query_string; + url = prepare_url(turl.c_str()); + path = get_realpath(tpath); + requestHeaders = nullptr; + bodydata.clear(); + responseHeaders.clear(); + + std::string contype = S3fsCurl::LookupMimeType(tpath); + + for(headers_t::iterator iter = meta.begin(); iter != meta.end(); ++iter){ + std::string key = lower(iter->first); + std::string value = iter->second; + if(is_prefix(key.c_str(), "x-amz-acl")){ + // not set value, but after set it. + }else if(is_prefix(key.c_str(), "x-amz-meta")){ + requestHeaders = curl_slist_sort_insert(requestHeaders, iter->first.c_str(), value.c_str()); + }else if(key == "x-amz-server-side-encryption" && value != "aws:kms"){ + // skip this header, because this header is specified after logic. + }else if(key == "x-amz-server-side-encryption-aws-kms-key-id"){ + // skip this header, because this header is specified after logic. + }else if(key == "x-amz-server-side-encryption-customer-key-md5"){ + // skip this header, because this header is specified after logic. + } + } + // "x-amz-acl", storage class, sse + if(S3fsCurl::default_acl != acl_t::PRIVATE){ + requestHeaders = curl_slist_sort_insert(requestHeaders, "x-amz-acl", str(S3fsCurl::default_acl)); + } + if(strcasecmp(GetStorageClass().c_str(), "STANDARD") != 0){ + requestHeaders = curl_slist_sort_insert(requestHeaders, "x-amz-storage-class", GetStorageClass().c_str()); + } + // SSE + if(S3fsCurl::GetSseType() != sse_type_t::SSE_DISABLE){ + std::string ssevalue; + if(!AddSseRequestHead(S3fsCurl::GetSseType(), ssevalue, false)){ + S3FS_PRN_WARN("Failed to set SSE header, but continue..."); + } + } + if(is_use_ahbe){ + // set additional header by ahbe conf + requestHeaders = AdditionalHeader::get()->AddHeader(requestHeaders, tpath); + } + + requestHeaders = curl_slist_sort_insert(requestHeaders, "Accept", nullptr); + requestHeaders = curl_slist_sort_insert(requestHeaders, "Content-Type", contype.c_str()); + + op = "POST"; + type = REQTYPE::PREMULTIPOST; + + // setopt + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_URL, url.c_str())){ + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_POST, true)){ // POST + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEDATA, reinterpret_cast(&bodydata))){ + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback)){ + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_POSTFIELDSIZE, 0)){ + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_INFILESIZE, 0)){ // Content-Length + return -EIO; + } + if(!S3fsCurl::AddUserAgent(hCurl)){ // put User-Agent + return -EIO; + } + + // request + int result; + if(0 != (result = RequestPerform())){ + bodydata.clear(); + return result; + } + + if(!simple_parse_xml(bodydata.c_str(), bodydata.size(), "UploadId", upload_id)){ + bodydata.clear(); + return -EIO; + } + + bodydata.clear(); + return 0; +} + +int S3fsCurl::CompleteMultipartPostRequest(const char* tpath, const std::string& upload_id, etaglist_t& parts) +{ + S3FS_PRN_INFO3("[tpath=%s][parts=%zu]", SAFESTRPTR(tpath), parts.size()); + + if(!tpath){ + return -EINVAL; + } + + // make contents + std::string postContent; + postContent += "\n"; + for(etaglist_t::iterator it = parts.begin(); it != parts.end(); ++it){ + if(it->etag.empty()){ + S3FS_PRN_ERR("%d file part is not finished uploading.", it->part_num); + return -EIO; + } + postContent += "\n"; + postContent += " " + std::to_string(it->part_num) + "\n"; + postContent += " " + it->etag + "\n"; + postContent += "\n"; + } + postContent += "\n"; + + // set postdata + postdata = reinterpret_cast(postContent.c_str()); + b_postdata = postdata; + postdata_remaining = postContent.size(); // without null + b_postdata_remaining = postdata_remaining; + + if(!CreateCurlHandle()){ + postdata = nullptr; + b_postdata = nullptr; + return -EIO; + } + std::string resource; + std::string turl; + MakeUrlResource(get_realpath(tpath).c_str(), resource, turl); + + // [NOTE] + // Encode the upload_id here. + // In compatible S3 servers(Cloudflare, etc), there are cases where characters that require URL encoding are included. + // + query_string = "uploadId=" + urlEncodeGeneral(upload_id); + turl += "?" + query_string; + url = prepare_url(turl.c_str()); + path = get_realpath(tpath); + requestHeaders = nullptr; + bodydata.clear(); + responseHeaders.clear(); + std::string contype = "application/xml"; + + requestHeaders = curl_slist_sort_insert(requestHeaders, "Accept", nullptr); + requestHeaders = curl_slist_sort_insert(requestHeaders, "Content-Type", contype.c_str()); + + if(sse_type_t::SSE_C == S3fsCurl::GetSseType()){ + std::string ssevalue; + if(!AddSseRequestHead(S3fsCurl::GetSseType(), ssevalue, false)){ + S3FS_PRN_WARN("Failed to set SSE header, but continue..."); + } + } + + op = "POST"; + type = REQTYPE::COMPLETEMULTIPOST; + + // setopt + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_URL, url.c_str())){ + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_POST, true)){ // POST + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEDATA, reinterpret_cast(&bodydata))){ + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback)){ + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_POSTFIELDSIZE, static_cast(postdata_remaining))){ + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_READDATA, reinterpret_cast(this))){ + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_READFUNCTION, S3fsCurl::ReadCallback)){ + return -EIO; + } + if(S3fsCurl::is_verbose){ + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_DEBUGFUNCTION, S3fsCurl::CurlDebugBodyOutFunc)){ // replace debug function + return -EIO; + } + } + if(!S3fsCurl::AddUserAgent(hCurl)){ // put User-Agent + return -EIO; + } + + // request + int result = RequestPerform(); + bodydata.clear(); + postdata = nullptr; + b_postdata = nullptr; + + return result; +} + +int S3fsCurl::MultipartListRequest(std::string& body) +{ + S3FS_PRN_INFO3("list request(multipart)"); + + if(!CreateCurlHandle()){ + return -EIO; + } + std::string resource; + std::string turl; + path = get_realpath("/"); + MakeUrlResource(path.c_str(), resource, turl); + + query_string = "uploads"; + turl += "?" + query_string; + url = prepare_url(turl.c_str()); + requestHeaders = nullptr; + responseHeaders.clear(); + bodydata.clear(); + + requestHeaders = curl_slist_sort_insert(requestHeaders, "Accept", nullptr); + + op = "GET"; + type = REQTYPE::MULTILIST; + + // setopt + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_URL, url.c_str())){ + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEDATA, reinterpret_cast(&bodydata))){ + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback)){ + return -EIO; + } + if(!S3fsCurl::AddUserAgent(hCurl)){ // put User-Agent + return -EIO; + } + + int result; + if(0 == (result = RequestPerform()) && !bodydata.empty()){ + body.swap(bodydata); + }else{ + body = ""; + } + bodydata.clear(); + + return result; +} + +int S3fsCurl::AbortMultipartUpload(const char* tpath, const std::string& upload_id) +{ + S3FS_PRN_INFO3("[tpath=%s]", SAFESTRPTR(tpath)); + + if(!tpath){ + return -EINVAL; + } + if(!CreateCurlHandle()){ + return -EIO; + } + std::string resource; + std::string turl; + MakeUrlResource(get_realpath(tpath).c_str(), resource, turl); + + // [NOTE] + // Encode the upload_id here. + // In compatible S3 servers(Cloudflare, etc), there are cases where characters that require URL encoding are included. + // + query_string = "uploadId=" + urlEncodeGeneral(upload_id); + turl += "?" + query_string; + url = prepare_url(turl.c_str()); + path = get_realpath(tpath); + requestHeaders = nullptr; + responseHeaders.clear(); + + op = "DELETE"; + type = REQTYPE::ABORTMULTIUPLOAD; + + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_URL, url.c_str())){ + return -EIO; + } + if(CURLE_OK != curl_easy_setopt(hCurl, CURLOPT_CUSTOMREQUEST, "DELETE")){ + return -EIO; + } + if(!S3fsCurl::AddUserAgent(hCurl)){ // put User-Agent + return -EIO; + } + + return RequestPerform(); +} + +// +// PUT /ObjectName?partNumber=PartNumber&uploadId=UploadId HTTP/1.1 +// Host: BucketName.s3.amazonaws.com +// Date: date +// Content-Length: Size +// Authorization: Signature +// +// PUT /my-movie.m2ts?partNumber=1&uploadId=VCVsb2FkIElEIGZvciBlbZZpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZR HTTP/1.1 +// Host: example-bucket.s3.amazonaws.com +// Date: Mon, 1 Nov 2010 20:34:56 GMT +// Content-Length: 10485760 +// Content-MD5: pUNXr/BjKK5G2UKvaRRrOA== +// Authorization: AWS VGhpcyBtZXNzYWdlIHNpZ25lZGGieSRlbHZpbmc= +// +int S3fsCurl::UploadMultipartPostSetup(const char* tpath, int part_num, const std::string& upload_id) +{ + S3FS_PRN_INFO3("[tpath=%s][start=%lld][size=%lld][part=%d]", SAFESTRPTR(tpath), static_cast(partdata.startpos), static_cast(partdata.size), part_num); + + if(-1 == partdata.fd || -1 == partdata.startpos || -1 == partdata.size){ + return -EINVAL; + } + + requestHeaders = nullptr; + + // make md5 and file pointer + if(S3fsCurl::is_content_md5){ + md5_t md5raw; + if(use_newcache){ + if(!s3fs_md5(reinterpret_cast(partdata.buf), partdata.size, &md5raw)){ + S3FS_PRN_ERR("Could not make md5 for file(part %d)", part_num); + return -EIO; + } + }else if(!s3fs_md5_fd(partdata.fd, partdata.startpos, partdata.size, &md5raw)){ + S3FS_PRN_ERR("Could not make md5 for file(part %d)", part_num); + return -EIO; + } + partdata.etag = s3fs_hex_lower(md5raw.data(), md5raw.size()); + std::string md5base64 = s3fs_base64(md5raw.data(), md5raw.size()); + requestHeaders = curl_slist_sort_insert(requestHeaders, "Content-MD5", md5base64.c_str()); + } + + // make request + // + // [NOTE] + // Encode the upload_id here. + // In compatible S3 servers(Cloudflare, etc), there are cases where characters that require URL encoding are included. + // + query_string = "partNumber=" + std::to_string(part_num) + "&uploadId=" + urlEncodeGeneral(upload_id); + std::string urlargs = "?" + query_string; + std::string resource; + std::string turl; + MakeUrlResource(get_realpath(tpath).c_str(), resource, turl); + + turl += urlargs; + url = prepare_url(turl.c_str()); + path = get_realpath(tpath); + bodydata.clear(); + headdata.clear(); + responseHeaders.clear(); + + // SSE-C + if(sse_type_t::SSE_C == S3fsCurl::GetSseType()){ + std::string ssevalue; + if(!AddSseRequestHead(S3fsCurl::GetSseType(), ssevalue, false)){ + S3FS_PRN_WARN("Failed to set SSE header, but continue..."); + } + } + + requestHeaders = curl_slist_sort_insert(requestHeaders, "Accept", nullptr); + + op = "PUT"; + type = REQTYPE::UPLOADMULTIPOST; + + // set lazy function + fpLazySetup = UploadMultipartPostSetCurlOpts; + + return 0; +} + +int S3fsCurl::UploadMultipartPostRequest(const char* tpath, int part_num, const std::string& upload_id) +{ + int result; + + S3FS_PRN_INFO3("[tpath=%s][start=%lld][size=%lld][part=%d]", SAFESTRPTR(tpath), static_cast(partdata.startpos), static_cast(partdata.size), part_num); + + // setup + if(0 != (result = S3fsCurl::UploadMultipartPostSetup(tpath, part_num, upload_id))){ + return result; + } + + if(!fpLazySetup || !fpLazySetup(this)){ + S3FS_PRN_ERR("Failed to lazy setup in multipart upload post request."); + return -EIO; + } + + // request + if(0 == (result = RequestPerform())){ + // UploadMultipartPostComplete returns true on success -> convert to 0 + result = !UploadMultipartPostComplete(); + } + + // closing + bodydata.clear(); + headdata.clear(); + + return result; +} + +int S3fsCurl::CopyMultipartPostSetup(const char* from, const char* to, int part_num, const std::string& upload_id, headers_t& meta) +{ + S3FS_PRN_INFO3("[from=%s][to=%s][part=%d]", SAFESTRPTR(from), SAFESTRPTR(to), part_num); + + if(!from || !to){ + return -EINVAL; + } + // [NOTE] + // Encode the upload_id here. + // In compatible S3 servers(Cloudflare, etc), there are cases where characters that require URL encoding are included. + // + query_string = "partNumber=" + std::to_string(part_num) + "&uploadId=" + urlEncodeGeneral(upload_id); + std::string urlargs = "?" + query_string; + std::string resource; + std::string turl; + MakeUrlResource(get_realpath(to).c_str(), resource, turl); + + turl += urlargs; + url = prepare_url(turl.c_str()); + path = get_realpath(to); + requestHeaders = nullptr; + responseHeaders.clear(); + bodydata.clear(); + headdata.clear(); + + std::string contype = S3fsCurl::LookupMimeType(to); + requestHeaders = curl_slist_sort_insert(requestHeaders, "Content-Type", contype.c_str()); + + // Make request headers + for(headers_t::iterator iter = meta.begin(); iter != meta.end(); ++iter){ + std::string key = lower(iter->first); + std::string value = iter->second; + if(key == "x-amz-copy-source"){ + requestHeaders = curl_slist_sort_insert(requestHeaders, iter->first.c_str(), value.c_str()); + }else if(key == "x-amz-copy-source-range"){ + requestHeaders = curl_slist_sort_insert(requestHeaders, iter->first.c_str(), value.c_str()); + }else if(key == "x-amz-server-side-encryption" && value != "aws:kms"){ + // skip this header + }else if(key == "x-amz-server-side-encryption-aws-kms-key-id"){ + // skip this header + }else if(key == "x-amz-server-side-encryption-customer-key-md5"){ + if(!AddSseRequestHead(sse_type_t::SSE_C, value, true)){ + S3FS_PRN_WARN("Failed to insert SSE-C header."); + } + } + } + // SSE-C + if(sse_type_t::SSE_C == S3fsCurl::GetSseType()){ + std::string ssevalue; + if(!AddSseRequestHead(S3fsCurl::GetSseType(), ssevalue, false)){ + S3FS_PRN_WARN("Failed to set SSE header, but continue..."); + } + } + + op = "PUT"; + type = REQTYPE::COPYMULTIPOST; + + // set lazy function + fpLazySetup = CopyMultipartPostSetCurlOpts; + + // request + S3FS_PRN_INFO3("copying... [from=%s][to=%s][part=%d]", from, to, part_num); + + return 0; +} + +bool S3fsCurl::UploadMultipartPostComplete() +{ + headers_t::iterator it = responseHeaders.find("ETag"); + if (it == responseHeaders.end()) { + return false; + } + std::string etag = peeloff(it->second); + + // check etag(md5); + // + // The ETAG when using SSE_C and SSE_KMS does not reflect the MD5 we sent + // SSE_C: https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPUT.html + // SSE_KMS is ignored in the above, but in the following it states the same in the highlights: + // https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingKMSEncryption.html + // + if(S3fsCurl::is_content_md5 && sse_type_t::SSE_C != S3fsCurl::GetSseType() && sse_type_t::SSE_KMS != S3fsCurl::GetSseType()){ + if(!etag_equals(etag, partdata.etag)){ + return false; + } + } + partdata.petag->etag = etag; + partdata.uploaded = true; + + return true; +} + +// cppcheck-suppress unmatchedSuppression +// cppcheck-suppress constParameter +// cppcheck-suppress constParameterCallback +bool S3fsCurl::CopyMultipartPostCallback(S3fsCurl* s3fscurl, void* param) +{ + if(!s3fscurl || param){ // this callback does not need a parameter + return false; + } + + // cppcheck-suppress unmatchedSuppression + // cppcheck-suppress knownConditionTrueFalse + return s3fscurl->CopyMultipartPostComplete(); +} + +bool S3fsCurl::CopyMultipartPostComplete() +{ + std::string etag; + partdata.uploaded = simple_parse_xml(bodydata.c_str(), bodydata.size(), "ETag", etag); + partdata.petag->etag = peeloff(etag); + + bodydata.clear(); + headdata.clear(); + + return true; +} + +bool S3fsCurl::MixMultipartPostComplete() +{ + bool result; + if(-1 == partdata.fd){ + result = CopyMultipartPostComplete(); + }else{ + result = UploadMultipartPostComplete(); + } + return result; +} + +int S3fsCurl::MultipartHeadRequest(const char* tpath, off_t size, headers_t& meta, bool is_copy) +{ + int result; + std::string upload_id; + off_t chunk; + off_t bytes_remaining; + etaglist_t list; + + S3FS_PRN_INFO3("[tpath=%s]", SAFESTRPTR(tpath)); + + if(0 != (result = PreMultipartPostRequest(tpath, meta, upload_id, is_copy))){ + return result; + } + DestroyCurlHandle(); + + // Initialize S3fsMultiCurl + S3fsMultiCurl curlmulti(GetMaxParallelCount()); + curlmulti.SetSuccessCallback(S3fsCurl::CopyMultipartPostCallback); + curlmulti.SetRetryCallback(S3fsCurl::CopyMultipartPostRetryCallback); + + for(bytes_remaining = size, chunk = 0; 0 < bytes_remaining; bytes_remaining -= chunk){ + chunk = bytes_remaining > GetMultipartCopySize() ? GetMultipartCopySize() : bytes_remaining; + + std::ostringstream strrange; + strrange << "bytes=" << (size - bytes_remaining) << "-" << (size - bytes_remaining + chunk - 1); + meta["x-amz-copy-source-range"] = strrange.str(); + + // s3fscurl sub object + std::unique_ptr s3fscurl_para(new S3fsCurl(true)); + s3fscurl_para->b_from = SAFESTRPTR(tpath); + s3fscurl_para->b_meta = meta; + s3fscurl_para->partdata.add_etag_list(list); + + // initiate upload part for parallel + if(0 != (result = s3fscurl_para->CopyMultipartPostSetup(tpath, tpath, s3fscurl_para->partdata.get_part_number(), upload_id, meta))){ + S3FS_PRN_ERR("failed uploading part setup(%d)", result); + return result; + } + + // set into parallel object + if(!curlmulti.SetS3fsCurlObject(std::move(s3fscurl_para))){ + S3FS_PRN_ERR("Could not make curl object into multi curl(%s).", tpath); + return -EIO; + } + } + + // Multi request + if(0 != (result = curlmulti.Request())){ + S3FS_PRN_ERR("error occurred in multi request(errno=%d).", result); + + S3fsCurl s3fscurl_abort(true); + int result2 = s3fscurl_abort.AbortMultipartUpload(tpath, upload_id); + s3fscurl_abort.DestroyCurlHandle(); + if(result2 != 0){ + S3FS_PRN_ERR("error aborting multipart upload(errno=%d).", result2); + } + return result; + } + + if(0 != (result = CompleteMultipartPostRequest(tpath, upload_id, list))){ + return result; + } + return 0; +} + +int S3fsCurl::MultipartUploadRequest(const std::string& upload_id, const char* tpath, int fd, off_t offset, off_t size, etagpair* petagpair) +{ + S3FS_PRN_INFO3("[upload_id=%s][tpath=%s][fd=%d][offset=%lld][size=%lld]", upload_id.c_str(), SAFESTRPTR(tpath), fd, static_cast(offset), static_cast(size)); + + // set + partdata.fd = fd; + partdata.startpos = offset; + partdata.size = size; + b_partdata_startpos = partdata.startpos; + b_partdata_size = partdata.size; + partdata.set_etag(petagpair); + + // upload part + int result; + if(0 != (result = UploadMultipartPostRequest(tpath, petagpair->part_num, upload_id))){ + S3FS_PRN_ERR("failed uploading %d part by error(%d)", petagpair->part_num, result); + return result; + } + DestroyCurlHandle(); + + return 0; +} + +int S3fsCurl::MultipartRenameRequest(const char* from, const char* to, headers_t& meta, off_t size) +{ + int result; + std::string upload_id; + off_t chunk; + off_t bytes_remaining; + etaglist_t list; + + S3FS_PRN_INFO3("[from=%s][to=%s]", SAFESTRPTR(from), SAFESTRPTR(to)); + + std::string srcresource; + std::string srcurl; + MakeUrlResource(get_realpath(from).c_str(), srcresource, srcurl); + + meta["Content-Type"] = S3fsCurl::LookupMimeType(to); + meta["x-amz-copy-source"] = srcresource; + + if(0 != (result = PreMultipartPostRequest(to, meta, upload_id, true))){ + return result; + } + DestroyCurlHandle(); + + // Initialize S3fsMultiCurl + S3fsMultiCurl curlmulti(GetMaxParallelCount()); + curlmulti.SetSuccessCallback(S3fsCurl::CopyMultipartPostCallback); + curlmulti.SetRetryCallback(S3fsCurl::CopyMultipartPostRetryCallback); + + for(bytes_remaining = size, chunk = 0; 0 < bytes_remaining; bytes_remaining -= chunk){ + chunk = bytes_remaining > GetMultipartCopySize() ? GetMultipartCopySize() : bytes_remaining; + + std::ostringstream strrange; + strrange << "bytes=" << (size - bytes_remaining) << "-" << (size - bytes_remaining + chunk - 1); + meta["x-amz-copy-source-range"] = strrange.str(); + + // s3fscurl sub object + std::unique_ptr s3fscurl_para(new S3fsCurl(true)); + s3fscurl_para->b_from = SAFESTRPTR(from); + s3fscurl_para->b_meta = meta; + s3fscurl_para->partdata.add_etag_list(list); + + // initiate upload part for parallel + if(0 != (result = s3fscurl_para->CopyMultipartPostSetup(from, to, s3fscurl_para->partdata.get_part_number(), upload_id, meta))){ + S3FS_PRN_ERR("failed uploading part setup(%d)", result); + return result; + } + + // set into parallel object + if(!curlmulti.SetS3fsCurlObject(std::move(s3fscurl_para))){ + S3FS_PRN_ERR("Could not make curl object into multi curl(%s).", to); + return -EIO; + } + } + + // Multi request + if(0 != (result = curlmulti.Request())){ + S3FS_PRN_ERR("error occurred in multi request(errno=%d).", result); + + S3fsCurl s3fscurl_abort(true); + int result2 = s3fscurl_abort.AbortMultipartUpload(to, upload_id); + s3fscurl_abort.DestroyCurlHandle(); + if(result2 != 0){ + S3FS_PRN_ERR("error aborting multipart upload(errno=%d).", result2); + } + return result; + } + + if(0 != (result = CompleteMultipartPostRequest(to, upload_id, list))){ + return result; + } + return 0; +} + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/curl.h b/s3fs/curl.h new file mode 100644 index 0000000..06d8c42 --- /dev/null +++ b/s3fs/curl.h @@ -0,0 +1,418 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef S3FS_CURL_H_ +#define S3FS_CURL_H_ + +#include +#include +#include +#include + +#include "autolock.h" +#include "metaheader.h" +#include "fdcache_page.h" + +//---------------------------------------------- +// Avoid dependency on libcurl version +//---------------------------------------------- +// [NOTE] +// The following symbols (enum) depend on the version of libcurl. +// CURLOPT_TCP_KEEPALIVE 7.25.0 and later +// CURLOPT_SSL_ENABLE_ALPN 7.36.0 and later +// CURLOPT_KEEP_SENDING_ON_ERROR 7.51.0 and later +// +// s3fs uses these, if you build s3fs with the old libcurl, +// substitute the following symbols to avoid errors. +// If the version of libcurl linked at runtime is old, +// curl_easy_setopt results in an error(CURLE_UNKNOWN_OPTION) and +// a message is output. +// +#if defined(HAVE_CURLOPT_TCP_KEEPALIVE) && (HAVE_CURLOPT_TCP_KEEPALIVE == 1) + #define S3FS_CURLOPT_TCP_KEEPALIVE CURLOPT_TCP_KEEPALIVE +#else + #define S3FS_CURLOPT_TCP_KEEPALIVE static_cast(213) +#endif + +#if defined(HAVE_CURLOPT_SSL_ENABLE_ALPN) && (HAVE_CURLOPT_SSL_ENABLE_ALPN == 1) + #define S3FS_CURLOPT_SSL_ENABLE_ALPN CURLOPT_SSL_ENABLE_ALPN +#else + #define S3FS_CURLOPT_SSL_ENABLE_ALPN static_cast(226) +#endif + +#if defined(HAVE_CURLOPT_KEEP_SENDING_ON_ERROR) && (HAVE_CURLOPT_KEEP_SENDING_ON_ERROR == 1) + #define S3FS_CURLOPT_KEEP_SENDING_ON_ERROR CURLOPT_KEEP_SENDING_ON_ERROR +#else + #define S3FS_CURLOPT_KEEP_SENDING_ON_ERROR static_cast(245) +#endif + +//---------------------------------------------- +// Structure / Typedefs +//---------------------------------------------- +typedef std::pair progress_t; +typedef std::map curltime_t; +typedef std::map curlprogress_t; + +//---------------------------------------------- +// class S3fsCurl +//---------------------------------------------- +class CurlHandlerPool; +class S3fsCred; +class S3fsCurl; +class Semaphore; + +// Prototype function for lazy setup options for curl handle +typedef bool (*s3fscurl_lazy_setup)(S3fsCurl* s3fscurl); + +typedef std::map sseckeymap_t; +typedef std::vector sseckeylist_t; + +// Class for lapping curl +// +class S3fsCurl +{ + friend class S3fsMultiCurl; + + private: + enum class REQTYPE { + UNSET = -1, + DELETE = 0, + HEAD, + PUTHEAD, + PUT, + GET, + CHKBUCKET, + LISTBUCKET, + PREMULTIPOST, + COMPLETEMULTIPOST, + UPLOADMULTIPOST, + COPYMULTIPOST, + MULTILIST, + IAMCRED, + ABORTMULTIUPLOAD, + IAMROLE + }; + + // class variables + static pthread_mutex_t curl_warnings_lock; + static bool curl_warnings_once; // emit older curl warnings only once + static pthread_mutex_t curl_handles_lock; + static struct callback_locks_t { + pthread_mutex_t dns; + pthread_mutex_t ssl_session; + } callback_locks; + static bool is_initglobal_done; + static CurlHandlerPool* sCurlPool; + static int sCurlPoolSize; + static CURLSH* hCurlShare; + static bool is_cert_check; + static bool is_dns_cache; + static bool is_ssl_session_cache; + static long connect_timeout; + static time_t readwrite_timeout; + static int retries; + static bool is_public_bucket; + static acl_t default_acl; + static std::string storage_class; + static sseckeylist_t sseckeys; + static std::string ssekmsid; + static sse_type_t ssetype; + static bool is_content_md5; + static bool is_verbose; + static bool is_dump_body; + static S3fsCred* ps3fscred; + static long ssl_verify_hostname; + static curltime_t curl_times; + static curlprogress_t curl_progress; + static std::string curl_ca_bundle; + static mimes_t mimeTypes; + static std::string userAgent; + static int max_parallel_cnt; + static int max_multireq; + static off_t multipart_size; + static off_t multipart_copy_size; + static signature_type_t signature_type; + static bool is_unsigned_payload; + static bool is_ua; // User-Agent + static bool listobjectsv2; + static bool requester_pays; + static std::string proxy_url; + static bool proxy_http; + static std::string proxy_userpwd; // load from file(:) + + // variables + CURL* hCurl; + REQTYPE type; // type of request + std::string path; // target object path + std::string base_path; // base path (for multi curl head request) + std::string saved_path; // saved path = cache key (for multi curl head request) + std::string url; // target object path(url) + struct curl_slist* requestHeaders; + headers_t responseHeaders; // header data by HeaderCallback + std::string bodydata; // body data by WriteMemoryCallback + std::string headdata; // header data by WriteMemoryCallback + long LastResponseCode; + const unsigned char* postdata; // use by post method and read callback function. + off_t postdata_remaining; // use by post method and read callback function. + filepart partdata; // use by multipart upload/get object callback + bool is_use_ahbe; // additional header by extension + int retry_count; // retry count for multipart + FILE* b_infile; // backup for retrying + const unsigned char* b_postdata; // backup for retrying + off_t b_postdata_remaining; // backup for retrying + off_t b_partdata_startpos; // backup for retrying + off_t b_partdata_size; // backup for retrying + char* b_partdata_buf; // backup for retrying + size_t b_ssekey_pos; // backup for retrying + std::string b_ssevalue; // backup for retrying + sse_type_t b_ssetype; // backup for retrying + std::string b_from; // backup for retrying(for copy request) + headers_t b_meta; // backup for retrying(for copy request) + std::string op; // the HTTP verb of the request ("PUT", "GET", etc.) + std::string query_string; // request query string + Semaphore *sem; + pthread_mutex_t *completed_tids_lock; + std::vector *completed_tids; + s3fscurl_lazy_setup fpLazySetup; // curl options for lazy setting function + CURLcode curlCode; // handle curl return + std::string sha256; // sha256 + + public: + static constexpr long S3FSCURL_RESPONSECODE_NOTSET = -1; + static constexpr long S3FSCURL_RESPONSECODE_FATAL_ERROR = -2; + static constexpr int S3FSCURL_PERFORM_RESULT_NOTSET = 1; + + public: + // constructor/destructor + explicit S3fsCurl(bool ahbe = false); + ~S3fsCurl(); + S3fsCurl(const S3fsCurl&) = delete; + S3fsCurl(S3fsCurl&&) = delete; + S3fsCurl& operator=(const S3fsCurl&) = delete; + S3fsCurl& operator=(S3fsCurl&&) = delete; + + private: + // class methods + static bool InitGlobalCurl(); + static bool DestroyGlobalCurl(); + static bool InitShareCurl(); + static bool DestroyShareCurl(); + static void LockCurlShare(CURL* handle, curl_lock_data nLockData, curl_lock_access laccess, void* useptr); + static void UnlockCurlShare(CURL* handle, curl_lock_data nLockData, void* useptr); + static bool InitCryptMutex(); + static bool DestroyCryptMutex(); + static int CurlProgress(void *clientp, double dltotal, double dlnow, double ultotal, double ulnow); + + static bool LocateBundle(); + static size_t HeaderCallback(void *data, size_t blockSize, size_t numBlocks, void *userPtr); + static size_t WriteMemoryCallback(void *ptr, size_t blockSize, size_t numBlocks, void *data); + static size_t ReadCallback(void *ptr, size_t size, size_t nmemb, void *userp); + static size_t UploadReadCallback(void *ptr, size_t size, size_t nmemb, void *userp); + static size_t DownloadWriteCallback(void* ptr, size_t size, size_t nmemb, void* userp); + + struct drp_upload_ctx { + std::string path; + char* data; + size_t pos; + size_t len; + drp_upload_ctx(std::string path_t, char* data_t, size_t pos_t, size_t len_t) : + path(path_t), data(data_t), pos(pos_t), len(len_t) {} + }; + static size_t UploadReadCallbackByMemory(void *ptr, size_t size, size_t nmemb, void *stream); + + static bool UploadMultipartPostCallback(S3fsCurl* s3fscurl, void* param); + static bool CopyMultipartPostCallback(S3fsCurl* s3fscurl, void* param); + static bool MixMultipartPostCallback(S3fsCurl* s3fscurl, void* param); + static std::unique_ptr UploadMultipartPostRetryCallback(S3fsCurl* s3fscurl); + static std::unique_ptr CopyMultipartPostRetryCallback(S3fsCurl* s3fscurl); + static std::unique_ptr MixMultipartPostRetryCallback(S3fsCurl* s3fscurl); + static std::unique_ptr ParallelGetObjectRetryCallback(S3fsCurl* s3fscurl); + + // lazy functions for set curl options + static bool CopyMultipartPostSetCurlOpts(S3fsCurl* s3fscurl); + static bool PreGetObjectRequestSetCurlOpts(S3fsCurl* s3fscurl); + static bool PreHeadRequestSetCurlOpts(S3fsCurl* s3fscurl); + + static bool LoadEnvSseCKeys(); + static bool LoadEnvSseKmsid(); + static bool PushbackSseKeys(const std::string& onekey); + static bool AddUserAgent(CURL* hCurl); + + static int CurlDebugFunc(const CURL* hcurl, curl_infotype type, char* data, size_t size, void* userptr); + static int CurlDebugBodyInFunc(const CURL* hcurl, curl_infotype type, char* data, size_t size, void* userptr); + static int CurlDebugBodyOutFunc(const CURL* hcurl, curl_infotype type, char* data, size_t size, void* userptr); + static int RawCurlDebugFunc(const CURL* hcurl, curl_infotype type, char* data, size_t size, void* userptr, curl_infotype datatype); + + // methods + bool ResetHandle(AutoLock::Type locktype = AutoLock::NONE); + bool RemakeHandle(); + bool ClearInternalData(); + void insertV4Headers(const std::string& access_key_id, const std::string& secret_access_key, const std::string& access_token); + void insertV2Headers(const std::string& access_key_id, const std::string& secret_access_key, const std::string& access_token); + void insertIBMIAMHeaders(const std::string& access_key_id, const std::string& access_token); + void insertAuthHeaders(); + bool AddSseRequestHead(sse_type_t ssetype, const std::string& ssevalue, bool is_copy); + std::string CalcSignatureV2(const std::string& method, const std::string& strMD5, const std::string& content_type, const std::string& date, const std::string& resource, const std::string& secret_access_key, const std::string& access_token); + std::string CalcSignature(const std::string& method, const std::string& canonical_uri, const std::string& query_string, const std::string& strdate, const std::string& payload_hash, const std::string& date8601, const std::string& secret_access_key, const std::string& access_token); + int UploadMultipartPostSetup(const char* tpath, int part_num, const std::string& upload_id); + int CopyMultipartPostSetup(const char* from, const char* to, int part_num, const std::string& upload_id, headers_t& meta); + bool UploadMultipartPostComplete(); + bool CopyMultipartPostComplete(); + int MapPutErrorResponse(int result); + + public: + // class methods + static bool InitS3fsCurl(); + static bool InitCredentialObject(S3fsCred* pcredobj); + static bool InitMimeType(const std::string& strFile); + static bool DestroyS3fsCurl(); + static std::unique_ptr CreateParallelS3fsCurl(const char* tpath, int fd, off_t start, off_t size, int part_num, bool is_copy, etagpair* petag, const std::string& upload_id, int& result); + static int ParallelMultipartUploadRequest(const char* tpath, headers_t& meta, int fd, off_t fsize = -1, char* buf = nullptr); + static int ParallelMixMultipartUploadRequest(const char* tpath, headers_t& meta, int fd, const fdpage_list_t& mixuppages); + static int ParallelGetObjectRequest(const char* tpath, int fd, off_t start, off_t size, char* buf = nullptr); + + // lazy functions for set curl options(public) + static bool UploadMultipartPostSetCurlOpts(S3fsCurl* s3fscurl); + + // class methods(variables) + static std::string LookupMimeType(const std::string& name); + static bool SetCheckCertificate(bool isCertCheck); + static bool SetDnsCache(bool isCache); + static bool SetSslSessionCache(bool isCache); + static long SetConnectTimeout(long timeout); + static time_t SetReadwriteTimeout(time_t timeout); + static time_t GetReadwriteTimeout() { return S3fsCurl::readwrite_timeout; } + static int SetRetries(int count); + static bool SetPublicBucket(bool flag); + static bool IsPublicBucket() { return S3fsCurl::is_public_bucket; } + static acl_t SetDefaultAcl(acl_t acl); + static acl_t GetDefaultAcl(); + static std::string SetStorageClass(const std::string& storage_class); + static std::string GetStorageClass() { return S3fsCurl::storage_class; } + static bool LoadEnvSse() { return (S3fsCurl::LoadEnvSseCKeys() && S3fsCurl::LoadEnvSseKmsid()); } + static sse_type_t SetSseType(sse_type_t type); + static sse_type_t GetSseType() { return S3fsCurl::ssetype; } + static bool IsSseDisable() { return (sse_type_t::SSE_DISABLE == S3fsCurl::ssetype); } + static bool IsSseS3Type() { return (sse_type_t::SSE_S3 == S3fsCurl::ssetype); } + static bool IsSseCType() { return (sse_type_t::SSE_C == S3fsCurl::ssetype); } + static bool IsSseKmsType() { return (sse_type_t::SSE_KMS == S3fsCurl::ssetype); } + static bool FinalCheckSse(); + static bool SetSseCKeys(const char* filepath); + static bool SetSseKmsid(const char* kmsid); + static bool IsSetSseKmsId() { return !S3fsCurl::ssekmsid.empty(); } + static const char* GetSseKmsId() { return S3fsCurl::ssekmsid.c_str(); } + static bool GetSseKey(std::string& md5, std::string& ssekey); + static bool GetSseKeyMd5(size_t pos, std::string& md5); + static size_t GetSseKeyCount(); + static bool SetContentMd5(bool flag); + static bool SetVerbose(bool flag); + static bool GetVerbose() { return S3fsCurl::is_verbose; } + static bool SetDumpBody(bool flag); + static bool IsDumpBody() { return S3fsCurl::is_dump_body; } + static long SetSslVerifyHostname(long value); + static long GetSslVerifyHostname() { return S3fsCurl::ssl_verify_hostname; } + static void ResetOffset(S3fsCurl* pCurl); + // maximum parallel GET and PUT requests + static int SetMaxParallelCount(int value); + static int GetMaxParallelCount() { return S3fsCurl::max_parallel_cnt; } + // maximum parallel HEAD requests + static int SetMaxMultiRequest(int max); + static int GetMaxMultiRequest() { return S3fsCurl::max_multireq; } + static bool SetMultipartSize(off_t size); + static off_t GetMultipartSize() { return S3fsCurl::multipart_size; } + static bool SetMultipartCopySize(off_t size); + static off_t GetMultipartCopySize() { return S3fsCurl::multipart_copy_size; } + static signature_type_t SetSignatureType(signature_type_t signature_type) { signature_type_t bresult = S3fsCurl::signature_type; S3fsCurl::signature_type = signature_type; return bresult; } + static signature_type_t GetSignatureType() { return S3fsCurl::signature_type; } + static bool SetUnsignedPayload(bool issset) { bool bresult = S3fsCurl::is_unsigned_payload; S3fsCurl::is_unsigned_payload = issset; return bresult; } + static bool GetUnsignedPayload() { return S3fsCurl::is_unsigned_payload; } + static bool SetUserAgentFlag(bool isset) { bool bresult = S3fsCurl::is_ua; S3fsCurl::is_ua = isset; return bresult; } + static bool IsUserAgentFlag() { return S3fsCurl::is_ua; } + static void InitUserAgent(); + static bool SetListObjectsV2(bool isset) { bool bresult = S3fsCurl::listobjectsv2; S3fsCurl::listobjectsv2 = isset; return bresult; } + static bool IsListObjectsV2() { return S3fsCurl::listobjectsv2; } + static bool SetRequesterPays(bool flag) { bool old_flag = S3fsCurl::requester_pays; S3fsCurl::requester_pays = flag; return old_flag; } + static bool IsRequesterPays() { return S3fsCurl::requester_pays; } + static bool SetProxy(const char* url); + static bool SetProxyUserPwd(const char* userpwd); + + // methods + bool CreateCurlHandle(bool only_pool = false, bool remake = false); + bool DestroyCurlHandle(bool restore_pool = true, bool clear_internal_data = true, AutoLock::Type locktype = AutoLock::NONE); + + bool GetIAMCredentials(const char* cred_url, const char* iam_v2_token, const char* ibm_secret_access_key, std::string& response); + bool GetIAMRoleFromMetaData(const char* cred_url, const char* iam_v2_token, std::string& token); + bool GetResponseCode(long& responseCode, bool from_curl_handle = true) const; + int RequestPerform(bool dontAddAuthHeaders=false); + int DeleteRequest(const char* tpath); + int GetIAMv2ApiToken(const char* token_url, int token_ttl, const char* token_ttl_hdr, std::string& response); + bool PreHeadRequest(const char* tpath, const char* bpath = nullptr, const char* savedpath = nullptr, size_t ssekey_pos = -1); + bool PreHeadRequest(const std::string& tpath, const std::string& bpath, const std::string& savedpath, size_t ssekey_pos = -1) { + return PreHeadRequest(tpath.c_str(), bpath.c_str(), savedpath.c_str(), ssekey_pos); + } + int HeadRequest(const char* tpath, headers_t& meta); + int PutHeadRequest(const char* tpath, headers_t& meta, bool is_copy); + int PutRequest(const char* tpath, headers_t& meta, int fd, off_t fsize = -1, char* buf = nullptr); + int PreGetObjectRequest(const char* tpath, int fd, off_t start, off_t size, sse_type_t ssetype, const std::string& ssevalue, char* buf = nullptr); + int GetObjectRequest(const char* tpath, int fd, off_t start = -1, off_t size = -1, char* buf = nullptr); + int CheckBucket(const char* check_path, bool compat_dir, bool force_no_sse); + int ListBucketRequest(const char* tpath, const char* query); + int PreMultipartPostRequest(const char* tpath, headers_t& meta, std::string& upload_id, bool is_copy); + int CompleteMultipartPostRequest(const char* tpath, const std::string& upload_id, etaglist_t& parts); + int UploadMultipartPostRequest(const char* tpath, int part_num, const std::string& upload_id); + bool MixMultipartPostComplete(); + int MultipartListRequest(std::string& body); + int AbortMultipartUpload(const char* tpath, const std::string& upload_id); + int MultipartHeadRequest(const char* tpath, off_t size, headers_t& meta, bool is_copy); + int MultipartUploadRequest(const std::string& upload_id, const char* tpath, int fd, off_t offset, off_t size, etagpair* petagpair); + int MultipartRenameRequest(const char* from, const char* to, headers_t& meta, off_t size); + + // methods(variables) + CURL* GetCurlHandle() const { return hCurl; } + std::string GetPath() const { return path; } + std::string GetBasePath() const { return base_path; } + std::string GetSpecialSavedPath() const { return saved_path; } + std::string GetUrl() const { return url; } + std::string GetOp() const { return op; } + const headers_t* GetResponseHeaders() const { return &responseHeaders; } + const std::string* GetBodyData() const { return &bodydata; } + const std::string* GetHeadData() const { return &headdata; } + CURLcode GetCurlCode() const { return curlCode; } + long GetLastResponseCode() const { return LastResponseCode; } + bool SetUseAhbe(bool ahbe); + bool EnableUseAhbe() { return SetUseAhbe(true); } + bool DisableUseAhbe() { return SetUseAhbe(false); } + bool IsUseAhbe() const { return is_use_ahbe; } + int GetMultipartRetryCount() const { return retry_count; } + void SetMultipartRetryCount(int retrycnt) { retry_count = retrycnt; } + bool IsOverMultipartRetryCount() const { return (retry_count >= S3fsCurl::retries); } + size_t GetLastPreHeadSeecKeyPos() const { return b_ssekey_pos; } +}; + +#endif // S3FS_CURL_H_ + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/curl_handlerpool.cpp b/s3fs/curl_handlerpool.cpp new file mode 100644 index 0000000..8646f2f --- /dev/null +++ b/s3fs/curl_handlerpool.cpp @@ -0,0 +1,137 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include + +#include "s3fs_logger.h" +#include "curl_handlerpool.h" +#include "autolock.h" + +//------------------------------------------------------------------- +// Class CurlHandlerPool +//------------------------------------------------------------------- +bool CurlHandlerPool::Init() +{ + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); +#if S3FS_PTHREAD_ERRORCHECK + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK); +#endif + if (0 != pthread_mutex_init(&mLock, &attr)) { + S3FS_PRN_ERR("Init curl handlers lock failed"); + return false; + } + + for(int cnt = 0; cnt < mMaxHandlers; ++cnt){ + CURL* hCurl = curl_easy_init(); + if(!hCurl){ + S3FS_PRN_ERR("Init curl handlers pool failed"); + Destroy(); + return false; + } + mPool.push_back(hCurl); + } + return true; +} + +bool CurlHandlerPool::Destroy() +{ + { + AutoLock lock(&mLock); + + while(!mPool.empty()){ + CURL* hCurl = mPool.back(); + mPool.pop_back(); + if(hCurl){ + curl_easy_cleanup(hCurl); + } + } + } + if (0 != pthread_mutex_destroy(&mLock)) { + S3FS_PRN_ERR("Destroy curl handlers lock failed"); + return false; + } + return true; +} + +CURL* CurlHandlerPool::GetHandler(bool only_pool) +{ + AutoLock lock(&mLock); + + CURL* hCurl = nullptr; + + if(!mPool.empty()){ + hCurl = mPool.back(); + mPool.pop_back(); + S3FS_PRN_DBG("Get handler from pool: rest = %d", static_cast(mPool.size())); + } + if(only_pool){ + return hCurl; + } + if(!hCurl){ + S3FS_PRN_INFO("Pool empty: force to create new handler"); + hCurl = curl_easy_init(); + } + return hCurl; +} + +void CurlHandlerPool::ReturnHandler(CURL* hCurl, bool restore_pool) +{ + if(!hCurl){ + return; + } + AutoLock lock(&mLock); + + if(restore_pool){ + S3FS_PRN_DBG("Return handler to pool"); + mPool.push_back(hCurl); + + while(mMaxHandlers < static_cast(mPool.size())){ + CURL* hOldCurl = mPool.front(); + mPool.pop_front(); + if(hOldCurl){ + S3FS_PRN_INFO("Pool full: destroy the oldest handler"); + curl_easy_cleanup(hOldCurl); + } + } + }else{ + S3FS_PRN_INFO("Pool full: destroy the handler"); + curl_easy_cleanup(hCurl); + } +} + +void CurlHandlerPool::ResetHandler(CURL* hCurl) +{ + if(!hCurl){ + return; + } + AutoLock lock(&mLock); + + curl_easy_reset(hCurl); +} + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/curl_handlerpool.h b/s3fs/curl_handlerpool.h new file mode 100644 index 0000000..a55c9b0 --- /dev/null +++ b/s3fs/curl_handlerpool.h @@ -0,0 +1,70 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef S3FS_CURL_HANDLERPOOL_H_ +#define S3FS_CURL_HANDLERPOOL_H_ + +#include +#include +#include + +//---------------------------------------------- +// Typedefs +//---------------------------------------------- +typedef std::list hcurllist_t; + +//---------------------------------------------- +// class CurlHandlerPool +//---------------------------------------------- +class CurlHandlerPool +{ + public: + explicit CurlHandlerPool(int maxHandlers) : mMaxHandlers(maxHandlers) + { + assert(maxHandlers > 0); + } + CurlHandlerPool(const CurlHandlerPool&) = delete; + CurlHandlerPool(CurlHandlerPool&&) = delete; + CurlHandlerPool& operator=(const CurlHandlerPool&) = delete; + CurlHandlerPool& operator=(CurlHandlerPool&&) = delete; + + bool Init(); + bool Destroy(); + + CURL* GetHandler(bool only_pool); + void ReturnHandler(CURL* hCurl, bool restore_pool); + void ResetHandler(CURL* hCurl); + + private: + int mMaxHandlers; + pthread_mutex_t mLock; + hcurllist_t mPool; +}; + +#endif // S3FS_CURL_HANDLERPOOL_H_ + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/curl_multi.cpp b/s3fs/curl_multi.cpp new file mode 100644 index 0000000..8a761f6 --- /dev/null +++ b/s3fs/curl_multi.cpp @@ -0,0 +1,394 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include +#include +#include + +#include "s3fs.h" +#include "s3fs_logger.h" +#include "curl_multi.h" +#include "curl.h" +#include "autolock.h" +#include "psemaphore.h" + +//------------------------------------------------------------------- +// Class S3fsMultiCurl +//------------------------------------------------------------------- +S3fsMultiCurl::S3fsMultiCurl(int maxParallelism, bool not_abort) : maxParallelism(maxParallelism), not_abort(not_abort), SuccessCallback(nullptr), NotFoundCallback(nullptr), RetryCallback(nullptr), pSuccessCallbackParam(nullptr), pNotFoundCallbackParam(nullptr) +{ + int result; + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); +#if S3FS_PTHREAD_ERRORCHECK + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK); +#endif + if (0 != (result = pthread_mutex_init(&completed_tids_lock, &attr))) { + S3FS_PRN_ERR("could not initialize completed_tids_lock: %i", result); + abort(); + } +} + +S3fsMultiCurl::~S3fsMultiCurl() +{ + Clear(); + int result; + if(0 != (result = pthread_mutex_destroy(&completed_tids_lock))){ + S3FS_PRN_ERR("could not destroy completed_tids_lock: %i", result); + } +} + +bool S3fsMultiCurl::ClearEx(bool is_all) +{ + s3fscurllist_t::iterator iter; + for(iter = clist_req.begin(); iter != clist_req.end(); ++iter){ + S3fsCurl* s3fscurl = iter->get(); + if(s3fscurl){ + s3fscurl->DestroyCurlHandle(); + } + } + clist_req.clear(); + + if(is_all){ + for(iter = clist_all.begin(); iter != clist_all.end(); ++iter){ + S3fsCurl* s3fscurl = iter->get(); + s3fscurl->DestroyCurlHandle(); + } + clist_all.clear(); + } + + S3FS_MALLOCTRIM(0); + + return true; +} + +S3fsMultiSuccessCallback S3fsMultiCurl::SetSuccessCallback(S3fsMultiSuccessCallback function) +{ + S3fsMultiSuccessCallback old = SuccessCallback; + SuccessCallback = function; + return old; +} + +S3fsMultiNotFoundCallback S3fsMultiCurl::SetNotFoundCallback(S3fsMultiNotFoundCallback function) +{ + S3fsMultiNotFoundCallback old = NotFoundCallback; + NotFoundCallback = function; + return old; +} + +S3fsMultiRetryCallback S3fsMultiCurl::SetRetryCallback(S3fsMultiRetryCallback function) +{ + S3fsMultiRetryCallback old = RetryCallback; + RetryCallback = function; + return old; +} + +void* S3fsMultiCurl::SetSuccessCallbackParam(void* param) +{ + void* old = pSuccessCallbackParam; + pSuccessCallbackParam = param; + return old; +} + +void* S3fsMultiCurl::SetNotFoundCallbackParam(void* param) +{ + void* old = pNotFoundCallbackParam; + pNotFoundCallbackParam = param; + return old; +} + +bool S3fsMultiCurl::SetS3fsCurlObject(std::unique_ptr s3fscurl) +{ + if(!s3fscurl){ + return false; + } + clist_all.push_back(std::move(s3fscurl)); + + return true; +} + +int S3fsMultiCurl::MultiPerform() +{ + std::vector threads; + bool success = true; + bool isMultiHead = false; + Semaphore sem(GetMaxParallelism()); + int rc; + + for(s3fscurllist_t::iterator iter = clist_req.begin(); iter != clist_req.end(); ++iter) { + pthread_t thread; + S3fsCurl* s3fscurl = iter->get(); + if(!s3fscurl){ + continue; + } + + sem.wait(); + + { + AutoLock lock(&completed_tids_lock); + for(std::vector::iterator it = completed_tids.begin(); it != completed_tids.end(); ++it){ + void* retval; + + rc = pthread_join(*it, &retval); + if (rc) { + success = false; + S3FS_PRN_ERR("failed pthread_join - rc(%d) %s", rc, strerror(rc)); + } else { + long int_retval = reinterpret_cast(retval); + if (int_retval && !(int_retval == -ENOENT && isMultiHead)) { + S3FS_PRN_WARN("thread terminated with non-zero return code: %ld", int_retval); + } + } + } + completed_tids.clear(); + } + s3fscurl->sem = &sem; + s3fscurl->completed_tids_lock = &completed_tids_lock; + s3fscurl->completed_tids = &completed_tids; + + isMultiHead |= s3fscurl->GetOp() == "HEAD"; + + rc = pthread_create(&thread, nullptr, S3fsMultiCurl::RequestPerformWrapper, static_cast(s3fscurl)); + if (rc != 0) { + success = false; + S3FS_PRN_ERR("failed pthread_create - rc(%d)", rc); + break; + } + threads.push_back(thread); + } + + for(int i = 0; i < sem.get_value(); ++i){ + sem.wait(); + } + + AutoLock lock(&completed_tids_lock); + for (std::vector::iterator titer = completed_tids.begin(); titer != completed_tids.end(); ++titer) { + void* retval; + + rc = pthread_join(*titer, &retval); + if (rc) { + success = false; + S3FS_PRN_ERR("failed pthread_join - rc(%d)", rc); + } else { + long int_retval = reinterpret_cast(retval); + if (int_retval && !(int_retval == -ENOENT && isMultiHead)) { + S3FS_PRN_WARN("thread terminated with non-zero return code: %ld", int_retval); + } + } + } + completed_tids.clear(); + + return success ? 0 : -EIO; +} + +int S3fsMultiCurl::MultiRead() +{ + int result = 0; + + for(s3fscurllist_t::iterator iter = clist_req.begin(); iter != clist_req.end(); ){ + std::unique_ptr s3fscurl(std::move(*iter)); + + bool isRetry = false; + bool isPostpone = false; + bool isNeedResetOffset = true; + long responseCode = S3fsCurl::S3FSCURL_RESPONSECODE_NOTSET; + CURLcode curlCode = s3fscurl->GetCurlCode(); + + if(s3fscurl->GetResponseCode(responseCode, false) && curlCode == CURLE_OK){ + if(S3fsCurl::S3FSCURL_RESPONSECODE_NOTSET == responseCode){ + // This is a case where the processing result has not yet been updated (should be very rare). + isPostpone = true; + }else if(400 > responseCode){ + // add into stat cache + // cppcheck-suppress unmatchedSuppression + // cppcheck-suppress knownPointerToBool + if(SuccessCallback && !SuccessCallback(s3fscurl.get(), pSuccessCallbackParam)){ + S3FS_PRN_WARN("error from success callback function(%s).", s3fscurl->url.c_str()); + } + }else if(400 == responseCode){ + // as possibly in multipart + S3FS_PRN_WARN("failed a request(%ld: %s)", responseCode, s3fscurl->url.c_str()); + isRetry = true; + }else if(404 == responseCode){ + // not found + // HEAD requests on readdir_multi_head can return 404 + if(s3fscurl->GetOp() != "HEAD"){ + S3FS_PRN_WARN("failed a request(%ld: %s)", responseCode, s3fscurl->url.c_str()); + } + // Call callback function + // cppcheck-suppress unmatchedSuppression + // cppcheck-suppress knownPointerToBool + if(NotFoundCallback && !NotFoundCallback(s3fscurl.get(), pNotFoundCallbackParam)){ + S3FS_PRN_WARN("error from not found callback function(%s).", s3fscurl->url.c_str()); + } + }else if(500 == responseCode){ + // case of all other result, do retry.(11/13/2013) + // because it was found that s3fs got 500 error from S3, but could success + // to retry it. + S3FS_PRN_WARN("failed a request(%ld: %s)", responseCode, s3fscurl->url.c_str()); + isRetry = true; + }else{ + // Retry in other case. + S3FS_PRN_WARN("failed a request(%ld: %s)", responseCode, s3fscurl->url.c_str()); + isRetry = true; + } + }else{ + S3FS_PRN_ERR("failed a request(Unknown response code: %s)", s3fscurl->url.c_str()); + // Reuse partical file + switch(curlCode){ + case CURLE_OPERATION_TIMEDOUT: + isRetry = true; + isNeedResetOffset = false; + break; + + case CURLE_PARTIAL_FILE: + isRetry = true; + isNeedResetOffset = false; + break; + + default: + S3FS_PRN_ERR("###curlCode: %d msg: %s", curlCode, curl_easy_strerror(curlCode)); + isRetry = true; + break; + } + } + + if(isPostpone){ + clist_req.erase(iter); + clist_req.push_back(std::move(s3fscurl)); // Re-evaluate at the end + iter = clist_req.begin(); + }else{ + if(!isRetry || (!not_abort && 0 != result)){ + // If an EIO error has already occurred, it will be terminated + // immediately even if retry processing is required. + s3fscurl->DestroyCurlHandle(); + }else{ + // Reset offset + if(isNeedResetOffset){ + S3fsCurl::ResetOffset(s3fscurl.get()); + } + + // For retry + std::unique_ptr retrycurl; + const S3fsCurl* retrycurl_ptr = retrycurl.get(); // save this due to std::move below + if(RetryCallback){ + retrycurl = RetryCallback(s3fscurl.get()); + if(nullptr != retrycurl){ + clist_all.push_back(std::move(retrycurl)); + }else{ + // set EIO and wait for other parts. + result = -EIO; + } + } + // cppcheck-suppress mismatchingContainers + if(s3fscurl.get() != retrycurl_ptr){ + s3fscurl->DestroyCurlHandle(); + } + } + iter = clist_req.erase(iter); + } + } + clist_req.clear(); + + if(!not_abort && 0 != result){ + // If an EIO error has already occurred, clear all retry objects. + for(s3fscurllist_t::iterator iter = clist_all.begin(); iter != clist_all.end(); ++iter){ + S3fsCurl* s3fscurl = iter->get(); + s3fscurl->DestroyCurlHandle(); + } + clist_all.clear(); + } + return result; +} + +int S3fsMultiCurl::Request() +{ + S3FS_PRN_INFO3("[count=%zu]", clist_all.size()); + + // Make request list. + // + // Send multi request loop( with retry ) + // (When many request is sends, sometimes gets "Couldn't connect to server") + // + while(!clist_all.empty()){ + // set curl handle to multi handle + int result; + s3fscurllist_t::iterator iter; + for(iter = clist_all.begin(); iter != clist_all.end(); ++iter){ + clist_req.push_back(std::move(*iter)); + } + clist_all.clear(); + + // Send multi request. + if(0 != (result = MultiPerform())){ + Clear(); + return result; + } + + // Read the result + if(0 != (result = MultiRead())){ + Clear(); + return result; + } + + // Cleanup curl handle in multi handle + ClearEx(false); + } + return 0; +} + +// +// thread function for performing an S3fsCurl request +// +void* S3fsMultiCurl::RequestPerformWrapper(void* arg) +{ + S3fsCurl* s3fscurl= static_cast(arg); + void* result = nullptr; + if(!s3fscurl){ + return reinterpret_cast(static_cast(-EIO)); + } + if(s3fscurl->fpLazySetup){ + if(!s3fscurl->fpLazySetup(s3fscurl)){ + S3FS_PRN_ERR("Failed to lazy setup, then respond EIO."); + result = reinterpret_cast(static_cast(-EIO)); + } + } + + if(!result){ + result = reinterpret_cast(static_cast(s3fscurl->RequestPerform())); + s3fscurl->DestroyCurlHandle(true, false); + } + + AutoLock lock(s3fscurl->completed_tids_lock); + s3fscurl->completed_tids->push_back(pthread_self()); + s3fscurl->sem->post(); + + return result; +} + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/curl_multi.h b/s3fs/curl_multi.h new file mode 100644 index 0000000..604f0b1 --- /dev/null +++ b/s3fs/curl_multi.h @@ -0,0 +1,90 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef S3FS_CURL_MULTI_H_ +#define S3FS_CURL_MULTI_H_ + +#include +#include + +//---------------------------------------------- +// Typedef +//---------------------------------------------- +class S3fsCurl; + +typedef std::vector> s3fscurllist_t; +typedef bool (*S3fsMultiSuccessCallback)(S3fsCurl* s3fscurl, void* param); // callback for succeed multi request +typedef bool (*S3fsMultiNotFoundCallback)(S3fsCurl* s3fscurl, void* param); // callback for succeed multi request +typedef std::unique_ptr (*S3fsMultiRetryCallback)(S3fsCurl* s3fscurl); // callback for failure and retrying + +//---------------------------------------------- +// class S3fsMultiCurl +//---------------------------------------------- +class S3fsMultiCurl +{ + private: + const int maxParallelism; + + s3fscurllist_t clist_all; // all of curl requests + s3fscurllist_t clist_req; // curl requests are sent + bool not_abort; // complete all requests without aborting on errors + + S3fsMultiSuccessCallback SuccessCallback; + S3fsMultiNotFoundCallback NotFoundCallback; + S3fsMultiRetryCallback RetryCallback; + void* pSuccessCallbackParam; + void* pNotFoundCallbackParam; + + pthread_mutex_t completed_tids_lock; + std::vector completed_tids; + + private: + bool ClearEx(bool is_all); + int MultiPerform(); + int MultiRead(); + + static void* RequestPerformWrapper(void* arg); + + public: + explicit S3fsMultiCurl(int maxParallelism, bool not_abort = false); + ~S3fsMultiCurl(); + + int GetMaxParallelism() const { return maxParallelism; } + + S3fsMultiSuccessCallback SetSuccessCallback(S3fsMultiSuccessCallback function); + S3fsMultiNotFoundCallback SetNotFoundCallback(S3fsMultiNotFoundCallback function); + S3fsMultiRetryCallback SetRetryCallback(S3fsMultiRetryCallback function); + void* SetSuccessCallbackParam(void* param); + void* SetNotFoundCallbackParam(void* param); + bool Clear() { return ClearEx(true); } + bool SetS3fsCurlObject(std::unique_ptr s3fscurl); + int Request(); +}; + +#endif // S3FS_CURL_MULTI_H_ + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/curl_util.cpp b/s3fs/curl_util.cpp new file mode 100644 index 0000000..bfd0244 --- /dev/null +++ b/s3fs/curl_util.cpp @@ -0,0 +1,334 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include +#include + +#include "common.h" +#include "s3fs_logger.h" +#include "curl_util.h" +#include "string_util.h" +#include "s3fs_auth.h" +#include "s3fs_cred.h" + +//------------------------------------------------------------------- +// Utility Functions +//------------------------------------------------------------------- +// +// curl_slist_sort_insert +// This function is like curl_slist_append function, but this adds data by a-sorting. +// Because AWS signature needs sorted header. +// +struct curl_slist* curl_slist_sort_insert(struct curl_slist* list, const char* key, const char* value) +{ + if(!key){ + return list; + } + + // key & value are trimmed and lower (only key) + std::string strkey = trim(key); + std::string strval = value ? trim(value) : ""; + std::string strnew = key + std::string(": ") + strval; + char* data; + if(nullptr == (data = strdup(strnew.c_str()))){ + return list; + } + + struct curl_slist **p = &list; + for(;*p; p = &(*p)->next){ + std::string strcur = (*p)->data; + size_t pos; + if(std::string::npos != (pos = strcur.find(':', 0))){ + strcur.erase(pos); + } + + int result = strcasecmp(strkey.c_str(), strcur.c_str()); + if(0 == result){ + free((*p)->data); + (*p)->data = data; + return list; + }else if(result < 0){ + break; + } + } + + struct curl_slist* new_item; + // Must use malloc since curl_slist_free_all calls free. + if(nullptr == (new_item = static_cast(malloc(sizeof(*new_item))))){ + free(data); + return list; + } + + struct curl_slist* before = *p; + *p = new_item; + new_item->data = data; + new_item->next = before; + + return list; +} + +struct curl_slist* curl_slist_remove(struct curl_slist* list, const char* key) +{ + if(!key){ + return list; + } + + std::string strkey = trim(key); + struct curl_slist **p = &list; + while(*p){ + std::string strcur = (*p)->data; + size_t pos; + if(std::string::npos != (pos = strcur.find(':', 0))){ + strcur.erase(pos); + } + + int result = strcasecmp(strkey.c_str(), strcur.c_str()); + if(0 == result){ + free((*p)->data); + struct curl_slist *tmp = *p; + *p = (*p)->next; + free(tmp); + }else{ + p = &(*p)->next; + } + } + + return list; +} + +std::string get_sorted_header_keys(const struct curl_slist* list) +{ + std::string sorted_headers; + + if(!list){ + return sorted_headers; + } + + for( ; list; list = list->next){ + std::string strkey = list->data; + size_t pos; + if(std::string::npos != (pos = strkey.find(':', 0))){ + if (trim(strkey.substr(pos + 1)).empty()) { + // skip empty-value headers (as they are discarded by libcurl) + continue; + } + strkey.erase(pos); + } + if(!sorted_headers.empty()){ + sorted_headers += ";"; + } + sorted_headers += lower(strkey); + } + + return sorted_headers; +} + +std::string get_header_value(const struct curl_slist* list, const std::string &key) +{ + if(!list){ + return ""; + } + + for( ; list; list = list->next){ + std::string strkey = list->data; + size_t pos; + if(std::string::npos != (pos = strkey.find(':', 0))){ + if(0 == strcasecmp(trim(strkey.substr(0, pos)).c_str(), key.c_str())){ + return trim(strkey.substr(pos+1)); + } + } + } + + return ""; +} + +std::string get_canonical_headers(const struct curl_slist* list, bool only_amz) +{ + std::string canonical_headers; + + if(!list){ + canonical_headers = "\n"; + return canonical_headers; + } + + for( ; list; list = list->next){ + std::string strhead = list->data; + size_t pos; + if(std::string::npos != (pos = strhead.find(':', 0))){ + std::string strkey = trim(lower(strhead.substr(0, pos))); + std::string strval = trim(strhead.substr(pos + 1)); + if (strval.empty()) { + // skip empty-value headers (as they are discarded by libcurl) + continue; + } + strhead = strkey; + strhead += ":"; + strhead += strval; + }else{ + strhead = trim(lower(strhead)); + } + if(only_amz && strhead.substr(0, 5) != "x-amz"){ + continue; + } + canonical_headers += strhead; + canonical_headers += "\n"; + } + return canonical_headers; +} + +// function for using global values +bool MakeUrlResource(const char* realpath, std::string& resourcepath, std::string& url) +{ + if(!realpath){ + return false; + } + resourcepath = urlEncodePath(service_path + S3fsCred::GetBucket() + realpath); + url = s3host + resourcepath; + return true; +} + +std::string prepare_url(const char* url) +{ + S3FS_PRN_INFO3("URL is %s", url); + + std::string uri; + std::string hostname; + std::string path; + std::string url_str = url; + std::string token = "/" + S3fsCred::GetBucket(); + size_t bucket_pos; + size_t bucket_length = token.size(); + size_t uri_length = 0; + + if(!strncasecmp(url_str.c_str(), "https://", 8)){ + uri_length = 8; + } else if(!strncasecmp(url_str.c_str(), "http://", 7)) { + uri_length = 7; + } + uri = url_str.substr(0, uri_length); + bucket_pos = url_str.find(token, uri_length); + + if(!pathrequeststyle){ + hostname = S3fsCred::GetBucket() + "." + url_str.substr(uri_length, bucket_pos - uri_length); + path = url_str.substr((bucket_pos + bucket_length)); + }else{ + hostname = url_str.substr(uri_length, bucket_pos - uri_length); + std::string part = url_str.substr((bucket_pos + bucket_length)); + if('/' != part[0]){ + part = "/" + part; + } + path = "/" + S3fsCred::GetBucket() + part; + } + + url_str = uri + hostname + path; + + S3FS_PRN_INFO3("URL changed is %s", url_str.c_str()); + + return url_str; +} + +bool make_md5_from_binary(const char* pstr, size_t length, std::string& md5) +{ + if(!pstr || '\0' == pstr[0]){ + S3FS_PRN_ERR("Parameter is wrong."); + return false; + } + md5_t binary; + if(!s3fs_md5(reinterpret_cast(pstr), length, &binary)){ + return false; + } + + md5 = s3fs_base64(binary.data(), binary.size()); + return true; +} + +std::string url_to_host(const std::string &url) +{ + S3FS_PRN_INFO3("url is %s", url.c_str()); + + static constexpr char HTTP[] = "http://"; + static constexpr char HTTPS[] = "https://"; + std::string hostname; + + if (is_prefix(url.c_str(), HTTP)) { + hostname = url.substr(sizeof(HTTP) - 1); + } else if (is_prefix(url.c_str(), HTTPS)) { + hostname = url.substr(sizeof(HTTPS) - 1); + } else { + S3FS_PRN_EXIT("url does not begin with http:// or https://"); + abort(); + } + + size_t idx; + if ((idx = hostname.find('/')) != std::string::npos) { + return hostname.substr(0, idx); + } else { + return hostname; + } +} + +std::string get_bucket_host() +{ + if(!pathrequeststyle){ + return S3fsCred::GetBucket() + "." + url_to_host(s3host); + } + return url_to_host(s3host); +} + +const char* getCurlDebugHead(curl_infotype type) +{ + const char* unknown = ""; + const char* dataIn = "BODY <"; + const char* dataOut = "BODY >"; + const char* headIn = "<"; + const char* headOut = ">"; + + switch(type){ + case CURLINFO_DATA_IN: + return dataIn; + case CURLINFO_DATA_OUT: + return dataOut; + case CURLINFO_HEADER_IN: + return headIn; + case CURLINFO_HEADER_OUT: + return headOut; + default: + break; + } + return unknown; +} + +// +// compare ETag ignoring quotes and case +// +bool etag_equals(const std::string& s1, const std::string& s2) +{ + return 0 == strcasecmp(peeloff(s1).c_str(), peeloff(s2).c_str()); +} + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/curl_util.h b/s3fs/curl_util.h new file mode 100644 index 0000000..596c6d4 --- /dev/null +++ b/s3fs/curl_util.h @@ -0,0 +1,56 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef S3FS_CURL_UTIL_H_ +#define S3FS_CURL_UTIL_H_ + +#include + +enum class sse_type_t; + +//---------------------------------------------- +// Functions +//---------------------------------------------- +struct curl_slist* curl_slist_sort_insert(struct curl_slist* list, const char* key, const char* value); +struct curl_slist* curl_slist_remove(struct curl_slist* list, const char* key); +std::string get_sorted_header_keys(const struct curl_slist* list); +std::string get_canonical_headers(const struct curl_slist* list, bool only_amz = false); +std::string get_header_value(const struct curl_slist* list, const std::string &key); +bool MakeUrlResource(const char* realpath, std::string& resourcepath, std::string& url); +std::string prepare_url(const char* url); +bool get_object_sse_type(const char* path, sse_type_t& ssetype, std::string& ssevalue); // implement in s3fs.cpp + +bool make_md5_from_binary(const char* pstr, size_t length, std::string& md5); +std::string url_to_host(const std::string &url); +std::string get_bucket_host(); +const char* getCurlDebugHead(curl_infotype type); + +bool etag_equals(const std::string& s1, const std::string& s2); + +#endif // S3FS_CURL_UTIL_H_ + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/fdcache.cpp b/s3fs/fdcache.cpp new file mode 100644 index 0000000..77630d6 --- /dev/null +++ b/s3fs/fdcache.cpp @@ -0,0 +1,1157 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Takeshi Nakatani + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "fdcache.h" +#include "fdcache_stat.h" +#include "s3fs_util.h" +#include "s3fs_logger.h" +#include "s3fs_cred.h" +#include "string_util.h" +#include "autolock.h" + +// +// The following symbols are used by FdManager::RawCheckAllCache(). +// +// These must be #defines due to string literal concatenation. +#define CACHEDBG_FMT_HEAD "---------------------------------------------------------------------------\n" \ + "Check cache file and its stats file consistency at %s\n" \ + "---------------------------------------------------------------------------" +#define CACHEDBG_FMT_FOOT "---------------------------------------------------------------------------\n" \ + "Summary - Total files: %d\n" \ + " Detected error files: %d\n" \ + " Detected error directories: %d\n" \ + "---------------------------------------------------------------------------" +#define CACHEDBG_FMT_FILE_OK "File: %s%s -> [OK] no problem" +#define CACHEDBG_FMT_FILE_PROB "File: %s%s" +#define CACHEDBG_FMT_DIR_PROB "Directory: %s" +#define CACHEDBG_FMT_ERR_HEAD " -> [E] there is a mark that data exists in stats, but there is no data in the cache file." +#define CACHEDBG_FMT_WARN_HEAD " -> [W] These show no data in stats, but there is evidence of data in the cache file(no problem)." +#define CACHEDBG_FMT_WARN_OPEN "\n -> [W] This file is currently open and may not provide accurate analysis results." +#define CACHEDBG_FMT_CRIT_HEAD " -> [C] %s" +#define CACHEDBG_FMT_CRIT_HEAD2 " -> [C] " +#define CACHEDBG_FMT_PROB_BLOCK " 0x%016zx(0x%016zx bytes)" + +// [NOTE] +// NOCACHE_PATH_PREFIX symbol needs for not using cache mode. +// Now s3fs I/F functions in s3fs.cpp has left the processing +// to FdManager and FdEntity class. FdManager class manages +// the list of local file stat and file descriptor in conjunction +// with the FdEntity class. +// When s3fs is not using local cache, it means FdManager must +// return new temporary file descriptor at each opening it. +// Then FdManager caches fd by key which is dummy file path +// instead of real file path. +// This process may not be complete, but it is easy way can +// be realized. +// +static constexpr char NOCACHE_PATH_PREFIX_FORM[] = " __S3FS_UNEXISTED_PATH_%lx__ / "; // important space words for simply + +//------------------------------------------------ +// FdManager class variable +//------------------------------------------------ +FdManager FdManager::singleton; +pthread_mutex_t FdManager::fd_manager_lock; +pthread_mutex_t FdManager::cache_cleanup_lock; +pthread_mutex_t FdManager::reserved_diskspace_lock; +bool FdManager::is_lock_init(false); +std::string FdManager::cache_dir; +bool FdManager::check_cache_dir_exist(false); +off_t FdManager::free_disk_space = 0; +off_t FdManager::fake_used_disk_space = 0; +std::string FdManager::check_cache_output; +bool FdManager::checked_lseek(false); +bool FdManager::have_lseek_hole(false); +std::string FdManager::tmp_dir = "/tmp"; + +//------------------------------------------------ +// FdManager class methods +//------------------------------------------------ +bool FdManager::SetCacheDir(const char* dir) +{ + if(!dir || '\0' == dir[0]){ + cache_dir = ""; + }else{ + cache_dir = dir; + } + return true; +} + +bool FdManager::SetCacheCheckOutput(const char* path) +{ + if(!path || '\0' == path[0]){ + check_cache_output.erase(); + }else{ + check_cache_output = path; + } + return true; +} + +bool FdManager::DeleteCacheDirectory() +{ + if(FdManager::cache_dir.empty()){ + return true; + } + + std::string cache_path; + if(!FdManager::MakeCachePath(nullptr, cache_path, false)){ + return false; + } + if(!delete_files_in_dir(cache_path.c_str(), true)){ + return false; + } + + std::string mirror_path = FdManager::cache_dir + "/." + S3fsCred::GetBucket() + ".mirror"; + if(!delete_files_in_dir(mirror_path.c_str(), true)){ + return false; + } + + return true; +} + +int FdManager::DeleteCacheFile(const char* path) +{ + S3FS_PRN_INFO3("[path=%s]", SAFESTRPTR(path)); + + if(!path){ + return -EIO; + } + if(FdManager::cache_dir.empty()){ + return 0; + } + std::string cache_path; + if(!FdManager::MakeCachePath(path, cache_path, false)){ + return 0; + } + int result = 0; + if(0 != unlink(cache_path.c_str())){ + if(ENOENT == errno){ + S3FS_PRN_DBG("failed to delete file(%s): errno=%d", path, errno); + }else{ + S3FS_PRN_ERR("failed to delete file(%s): errno=%d", path, errno); + } + return -errno; + } + if(0 != (result = CacheFileStat::DeleteCacheFileStat(path))){ + if(-ENOENT == result){ + S3FS_PRN_DBG("failed to delete stat file(%s): errno=%d", path, result); + }else{ + S3FS_PRN_ERR("failed to delete stat file(%s): errno=%d", path, result); + } + } + return result; +} + +bool FdManager::MakeCachePath(const char* path, std::string& cache_path, bool is_create_dir, bool is_mirror_path) +{ + if(FdManager::cache_dir.empty()){ + cache_path = ""; + return true; + } + + std::string resolved_path(FdManager::cache_dir); + if(!is_mirror_path){ + resolved_path += "/"; + resolved_path += S3fsCred::GetBucket(); + }else{ + resolved_path += "/."; + resolved_path += S3fsCred::GetBucket(); + resolved_path += ".mirror"; + } + + if(is_create_dir){ + int result; + if(0 != (result = mkdirp(resolved_path + mydirname(path), 0777))){ + S3FS_PRN_ERR("failed to create dir(%s) by errno(%d).", path, result); + return false; + } + } + if(!path || '\0' == path[0]){ + cache_path = resolved_path; + }else{ + cache_path = resolved_path + SAFESTRPTR(path); + } + return true; +} + +bool FdManager::CheckCacheTopDir() +{ + if(FdManager::cache_dir.empty()){ + return true; + } + std::string toppath(FdManager::cache_dir + "/" + S3fsCred::GetBucket()); + + return check_exist_dir_permission(toppath.c_str()); +} + +bool FdManager::MakeRandomTempPath(const char* path, std::string& tmppath) +{ + char szBuff[64]; + + snprintf(szBuff, sizeof(szBuff), NOCACHE_PATH_PREFIX_FORM, random()); // worry for performance, but maybe don't worry. + szBuff[sizeof(szBuff) - 1] = '\0'; // for safety + tmppath = szBuff; + tmppath += path ? path : ""; + return true; +} + +bool FdManager::SetCheckCacheDirExist(bool is_check) +{ + bool old = FdManager::check_cache_dir_exist; + FdManager::check_cache_dir_exist = is_check; + return old; +} + +bool FdManager::CheckCacheDirExist() +{ + if(!FdManager::check_cache_dir_exist){ + return true; + } + if(FdManager::cache_dir.empty()){ + return true; + } + return IsDir(&cache_dir); +} + +off_t FdManager::GetEnsureFreeDiskSpace() +{ + AutoLock auto_lock(&FdManager::reserved_diskspace_lock); + return FdManager::free_disk_space; +} + +off_t FdManager::SetEnsureFreeDiskSpace(off_t size) +{ + AutoLock auto_lock(&FdManager::reserved_diskspace_lock); + off_t old = FdManager::free_disk_space; + FdManager::free_disk_space = size; + return old; +} + +bool FdManager::InitFakeUsedDiskSize(off_t fake_freesize) +{ + FdManager::fake_used_disk_space = 0; // At first, clear this value because this value is used in GetFreeDiskSpace. + + off_t actual_freesize = FdManager::GetFreeDiskSpace(nullptr); + + if(fake_freesize < actual_freesize){ + FdManager::fake_used_disk_space = actual_freesize - fake_freesize; + }else{ + FdManager::fake_used_disk_space = 0; + } + return true; +} + +off_t FdManager::GetTotalDiskSpaceByRatio(int ratio) +{ + return FdManager::GetTotalDiskSpace(nullptr) * ratio / 100; +} + +off_t FdManager::GetTotalDiskSpace(const char* path) +{ + struct statvfs vfsbuf; + int result = FdManager::GetVfsStat(path, &vfsbuf); + if(result == -1){ + return 0; + } + + off_t actual_totalsize = vfsbuf.f_blocks * vfsbuf.f_frsize; + + return actual_totalsize; +} + +off_t FdManager::GetFreeDiskSpace(const char* path) +{ + struct statvfs vfsbuf; + int result = FdManager::GetVfsStat(path, &vfsbuf); + if(result == -1){ + return 0; + } + + off_t actual_freesize = vfsbuf.f_bavail * vfsbuf.f_frsize; + + return (FdManager::fake_used_disk_space < actual_freesize ? (actual_freesize - FdManager::fake_used_disk_space) : 0); +} + +int FdManager::GetVfsStat(const char* path, struct statvfs* vfsbuf){ + std::string ctoppath; + if(!FdManager::cache_dir.empty()){ + ctoppath = FdManager::cache_dir + "/"; + ctoppath = get_exist_directory_path(ctoppath); // existed directory + if(ctoppath != "/"){ + ctoppath += "/"; + } + }else{ + ctoppath = tmp_dir + "/"; + } + if(path && '\0' != *path){ + ctoppath += path; + }else{ + ctoppath += "."; + } + if(-1 == statvfs(ctoppath.c_str(), vfsbuf)){ + S3FS_PRN_ERR("could not get vfs stat by errno(%d)", errno); + return -1; + } + + return 0; +} + +bool FdManager::IsSafeDiskSpace(const char* path, off_t size) +{ + off_t fsize = FdManager::GetFreeDiskSpace(path); + return size + FdManager::GetEnsureFreeDiskSpace() <= fsize; +} + +bool FdManager::IsSafeDiskSpaceWithLog(const char* path, off_t size) +{ + off_t fsize = FdManager::GetFreeDiskSpace(path); + off_t needsize = size + FdManager::GetEnsureFreeDiskSpace(); + if(needsize <= fsize){ + return true; + } else { + S3FS_PRN_EXIT("There is no enough disk space for used as cache(or temporary) directory by s3fs. Requires %.3f MB, already has %.3f MB.", static_cast(needsize) / 1024 / 1024, static_cast(fsize) / 1024 / 1024); + return false; + } +} + +bool FdManager::HaveLseekHole() +{ + if(FdManager::checked_lseek){ + return FdManager::have_lseek_hole; + } + + // create temporary file + int fd; + std::unique_ptr ptmpfp(MakeTempFile(), &s3fs_fclose); + if(nullptr == ptmpfp || -1 == (fd = fileno(ptmpfp.get()))){ + S3FS_PRN_ERR("failed to open temporary file by errno(%d)", errno); + FdManager::checked_lseek = true; + FdManager::have_lseek_hole = false; + return false; + } + + // check SEEK_DATA/SEEK_HOLE options + bool result = true; + if(-1 == lseek(fd, 0, SEEK_DATA)){ + if(EINVAL == errno){ + S3FS_PRN_ERR("lseek does not support SEEK_DATA"); + result = false; + } + } + if(result && -1 == lseek(fd, 0, SEEK_HOLE)){ + if(EINVAL == errno){ + S3FS_PRN_ERR("lseek does not support SEEK_HOLE"); + result = false; + } + } + + FdManager::checked_lseek = true; + FdManager::have_lseek_hole = result; + return FdManager::have_lseek_hole; +} + +bool FdManager::SetTmpDir(const char *dir) +{ + if(!dir || '\0' == dir[0]){ + tmp_dir = "/tmp"; + }else{ + tmp_dir = dir; + } + return true; +} + +bool FdManager::IsDir(const std::string* dir) +{ + // check the directory + struct stat st; + if(0 != stat(dir->c_str(), &st)){ + S3FS_PRN_ERR("could not stat() directory %s by errno(%d).", dir->c_str(), errno); + return false; + } + if(!S_ISDIR(st.st_mode)){ + S3FS_PRN_ERR("the directory %s is not a directory.", dir->c_str()); + return false; + } + return true; +} + +bool FdManager::CheckTmpDirExist() +{ + if(FdManager::tmp_dir.empty()){ + return true; + } + return IsDir(&tmp_dir); +} + +FILE* FdManager::MakeTempFile() { + int fd; + char cfn[PATH_MAX]; + std::string fn = tmp_dir + "/s3fstmp.XXXXXX"; + strncpy(cfn, fn.c_str(), sizeof(cfn) - 1); + cfn[sizeof(cfn) - 1] = '\0'; + + fd = mkstemp(cfn); + if (-1 == fd) { + S3FS_PRN_ERR("failed to create tmp file. errno(%d)", errno); + return nullptr; + } + if (-1 == unlink(cfn)) { + S3FS_PRN_ERR("failed to delete tmp file. errno(%d)", errno); + return nullptr; + } + return fdopen(fd, "rb+"); +} + +bool FdManager::HasOpenEntityFd(const char* path) +{ + AutoLock auto_lock(&FdManager::fd_manager_lock); + + const FdEntity* ent; + int fd = -1; + if(nullptr == (ent = FdManager::singleton.GetFdEntity(path, fd, false, AutoLock::ALREADY_LOCKED))){ + return false; + } + return (0 < ent->GetOpenCount()); +} + +// [NOTE] +// Returns the number of open pseudo fd. +// +int FdManager::GetOpenFdCount(const char* path) +{ + AutoLock auto_lock(&FdManager::fd_manager_lock); + + return FdManager::singleton.GetPseudoFdCount(path); +} + +//------------------------------------------------ +// FdManager methods +//------------------------------------------------ +FdManager::FdManager() +{ + if(this == FdManager::get()){ + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); +#if S3FS_PTHREAD_ERRORCHECK + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK); +#endif + int result; + if(0 != (result = pthread_mutex_init(&FdManager::fd_manager_lock, &attr))){ + S3FS_PRN_CRIT("failed to init fd_manager_lock: %d", result); + abort(); + } + if(0 != (result = pthread_mutex_init(&FdManager::cache_cleanup_lock, &attr))){ + S3FS_PRN_CRIT("failed to init cache_cleanup_lock: %d", result); + abort(); + } + if(0 != (result = pthread_mutex_init(&FdManager::reserved_diskspace_lock, &attr))){ + S3FS_PRN_CRIT("failed to init reserved_diskspace_lock: %d", result); + abort(); + } + FdManager::is_lock_init = true; + }else{ + abort(); + } +} + +FdManager::~FdManager() +{ + if(this == FdManager::get()){ + for(fdent_map_t::iterator iter = fent.begin(); fent.end() != iter; ++iter){ + FdEntity* ent = (*iter).second.get(); + S3FS_PRN_WARN("To exit with the cache file opened: path=%s, refcnt=%d", ent->GetPath().c_str(), ent->GetOpenCount()); + } + fent.clear(); + + if(FdManager::is_lock_init){ + int result; + if(0 != (result = pthread_mutex_destroy(&FdManager::fd_manager_lock))){ + S3FS_PRN_CRIT("failed to destroy fd_manager_lock: %d", result); + abort(); + } + if(0 != (result = pthread_mutex_destroy(&FdManager::cache_cleanup_lock))){ + S3FS_PRN_CRIT("failed to destroy cache_cleanup_lock: %d", result); + abort(); + } + if(0 != (result = pthread_mutex_destroy(&FdManager::reserved_diskspace_lock))){ + S3FS_PRN_CRIT("failed to destroy reserved_diskspace_lock: %d", result); + abort(); + } + FdManager::is_lock_init = false; + } + }else{ + abort(); + } +} + +FdEntity* FdManager::GetFdEntity(const char* path, int& existfd, bool newfd, AutoLock::Type locktype) +{ + S3FS_PRN_INFO3("[path=%s][pseudo_fd=%d]", SAFESTRPTR(path), existfd); + + if(!path || '\0' == path[0]){ + return nullptr; + } + AutoLock auto_lock(&FdManager::fd_manager_lock, locktype); + + fdent_map_t::iterator iter = fent.find(path); + if(fent.end() != iter && iter->second){ + if(-1 == existfd){ + if(newfd){ + existfd = iter->second->OpenPseudoFd(O_RDWR); // [NOTE] O_RDWR flags + } + return iter->second.get(); + }else if(iter->second->FindPseudoFd(existfd)){ + if(newfd){ + existfd = iter->second->Dup(existfd); + } + return iter->second.get(); + } + } + + if(-1 != existfd){ + for(iter = fent.begin(); iter != fent.end(); ++iter){ + if(iter->second && iter->second->FindPseudoFd(existfd)){ + // found opened fd in map + if(iter->second->GetPath() == path){ + if(newfd){ + existfd = iter->second->Dup(existfd); + } + return iter->second.get(); + } + // found fd, but it is used another file(file descriptor is recycled) + // so returns nullptr. + break; + } + } + } + + // If the cache directory is not specified, s3fs opens a temporary file + // when the file is opened. + if(!FdManager::IsCacheDir()){ + for(iter = fent.begin(); iter != fent.end(); ++iter){ + if(iter->second && iter->second->IsOpen() && iter->second->GetPath() == path){ + return iter->second.get(); + } + } + } + return nullptr; +} + +FdEntity* FdManager::Open(int& fd, const char* path, const headers_t* pmeta, off_t size, const struct timespec& ts_mctime, int flags, bool force_tmpfile, bool is_create, bool ignore_modify, AutoLock::Type type) +{ + S3FS_PRN_DBG("[path=%s][size=%lld][ts_mctime=%s][flags=0x%x][force_tmpfile=%s][create=%s][ignore_modify=%s]", SAFESTRPTR(path), static_cast(size), str(ts_mctime).c_str(), flags, (force_tmpfile ? "yes" : "no"), (is_create ? "yes" : "no"), (ignore_modify ? "yes" : "no")); + + if(!path || '\0' == path[0]){ + return nullptr; + } + + AutoLock auto_lock(&FdManager::fd_manager_lock); + + // search in mapping by key(path) + fdent_map_t::iterator iter = fent.find(path); + if(fent.end() == iter && !force_tmpfile && !FdManager::IsCacheDir()){ + // If the cache directory is not specified, s3fs opens a temporary file + // when the file is opened. + // Then if it could not find a entity in map for the file, s3fs should + // search a entity in all which opened the temporary file. + // + for(iter = fent.begin(); iter != fent.end(); ++iter){ + if(iter->second && iter->second->IsOpen() && iter->second->GetPath() == path){ + break; // found opened fd in mapping + } + } + } + + if(fent.end() != iter){ + // found + FdEntity* ent = iter->second.get(); + + // [NOTE] + // If the file is being modified and ignore_modify flag is false, + // the file size will not be changed even if there is a request + // to reduce the size of the modified file. + // If you do, the "test_open_second_fd" test will fail. + // + if(!ignore_modify && ent->IsModified()){ + // If the file is being modified and it's size is larger than size parameter, it will not be resized. + off_t cur_size = 0; + if(ent->GetSize(cur_size) && size <= cur_size){ + size = -1; + } + } + + // (re)open + if(0 > (fd = ent->Open(pmeta, size, ts_mctime, flags, type))){ + S3FS_PRN_ERR("failed to (re)open and create new pseudo fd for path(%s).", path); + return nullptr; + } + + if(use_newcache){ + accessor->Invalidate(path); + } + + return ent; + }else if(is_create){ + // not found + std::string cache_path; + if(!force_tmpfile && !FdManager::MakeCachePath(path, cache_path, true)){ + S3FS_PRN_ERR("failed to make cache path for object(%s).", path); + return nullptr; + } + // make new obj + std::unique_ptr ent(new FdEntity(path, cache_path.c_str())); + // open + if(0 > (fd = ent->Open(pmeta, size, ts_mctime, flags, type))){ + S3FS_PRN_ERR("failed to open and create new pseudo fd for path(%s) errno:%d.", path, fd); + return nullptr; + } + + if(use_newcache){ + ent->UpdateRealsize(size); + } + + if(!cache_path.empty()){ + // using cache + return (fent[path] = std::move(ent)).get(); + }else{ + // not using cache, so the key of fdentity is set not really existing path. + // (but not strictly unexisting path.) + // + // [NOTE] + // The reason why this process here, please look at the definition of the + // comments of NOCACHE_PATH_PREFIX_FORM symbol. + // + std::string tmppath; + FdManager::MakeRandomTempPath(path, tmppath); + return (fent[tmppath] = std::move(ent)).get(); + } + }else{ + return nullptr; + } +} + +// [NOTE] +// This method does not create a new pseudo fd. +// It just finds existfd and returns the corresponding entity. +// +FdEntity* FdManager::GetExistFdEntity(const char* path, int existfd) +{ + S3FS_PRN_DBG("[path=%s][pseudo_fd=%d]", SAFESTRPTR(path), existfd); + + AutoLock auto_lock(&FdManager::fd_manager_lock); + + // search from all entity. + for(fdent_map_t::iterator iter = fent.begin(); iter != fent.end(); ++iter){ + if(iter->second && iter->second->FindPseudoFd(existfd)){ + // found existfd in entity + return iter->second.get(); + } + } + // not found entity + return nullptr; +} + +FdEntity* FdManager::OpenExistFdEntity(const char* path, int& fd, int flags) +{ + S3FS_PRN_DBG("[path=%s][flags=0x%x]", SAFESTRPTR(path), flags); + + // search entity by path, and create pseudo fd + FdEntity* ent = Open(fd, path, nullptr, -1, S3FS_OMIT_TS, flags, false, false, false, AutoLock::NONE); + if(!ent){ + // Not found entity + return nullptr; + } + return ent; +} + +// [NOTE] +// Returns the number of open pseudo fd. +// This method is called from GetOpenFdCount method which is already locked. +// +int FdManager::GetPseudoFdCount(const char* path) +{ + S3FS_PRN_DBG("[path=%s]", SAFESTRPTR(path)); + + if(!path || '\0' == path[0]){ + return 0; + } + + // search from all entity. + for(fdent_map_t::iterator iter = fent.begin(); iter != fent.end(); ++iter){ + if(iter->second && iter->second->GetPath() == path){ + // found the entity for the path + return iter->second->GetOpenCount(); + } + } + // not found entity + return 0; +} + +void FdManager::Rename(const std::string &from, const std::string &to) +{ + AutoLock auto_lock(&FdManager::fd_manager_lock); + + fdent_map_t::iterator iter = fent.find(from); + if(fent.end() == iter && !FdManager::IsCacheDir()){ + // If the cache directory is not specified, s3fs opens a temporary file + // when the file is opened. + // Then if it could not find a entity in map for the file, s3fs should + // search a entity in all which opened the temporary file. + // + for(iter = fent.begin(); iter != fent.end(); ++iter){ + if(iter->second && iter->second->IsOpen() && iter->second->GetPath() == from){ + break; // found opened fd in mapping + } + } + } + + if(fent.end() != iter){ + // found + S3FS_PRN_DBG("[from=%s][to=%s]", from.c_str(), to.c_str()); + + std::unique_ptr ent(std::move(iter->second)); + + // retrieve old fd entity from map + fent.erase(iter); + + // rename path and caches in fd entity + std::string fentmapkey; + if(!ent->RenamePath(to, fentmapkey)){ + S3FS_PRN_ERR("Failed to rename FdEntity object for %s to %s", from.c_str(), to.c_str()); + return; + } + + // set new fd entity to map + fent[fentmapkey] = std::move(ent); + } +} + +bool FdManager::Close(FdEntity* ent, int fd) +{ + S3FS_PRN_DBG("[ent->file=%s][pseudo_fd=%d]", ent ? ent->GetPath().c_str() : "", fd); + + if(!ent || -1 == fd){ + return true; // returns success + } + AutoLock auto_lock(&FdManager::fd_manager_lock); + + for(fdent_map_t::iterator iter = fent.begin(); iter != fent.end(); ++iter){ + if(iter->second.get() == ent){ + ent->Close(fd); + if(!ent->IsOpen()){ + // remove found entity from map. + iter = fent.erase(iter); + + // check another key name for entity value to be on the safe side + for(; iter != fent.end(); ){ + if(iter->second.get() == ent){ + iter = fent.erase(iter); + }else{ + ++iter; + } + } + } + return true; + } + } + return false; +} + +bool FdManager::ChangeEntityToTempPath(FdEntity* ent, const char* path) +{ + AutoLock auto_lock(&FdManager::fd_manager_lock); + + for(fdent_map_t::iterator iter = fent.begin(); iter != fent.end(); ){ + if(iter->second.get() == ent){ + std::string tmppath; + FdManager::MakeRandomTempPath(path, tmppath); + iter->second.reset(ent); + break; + }else{ + ++iter; + } + } + return false; +} + +void FdManager::CleanupCacheDir() +{ + //S3FS_PRN_DBG("cache cleanup requested"); + + if(!FdManager::IsCacheDir()){ + return; + } + + AutoLock auto_lock_no_wait(&FdManager::cache_cleanup_lock, AutoLock::NO_WAIT); + + if(auto_lock_no_wait.isLockAcquired()){ + //S3FS_PRN_DBG("cache cleanup started"); + CleanupCacheDirInternal(""); + //S3FS_PRN_DBG("cache cleanup ended"); + }else{ + // wait for other thread to finish cache cleanup + AutoLock auto_lock(&FdManager::cache_cleanup_lock); + } +} + +void FdManager::CleanupCacheDirInternal(const std::string &path) +{ + DIR* dp; + struct dirent* dent; + std::string abs_path = cache_dir + "/" + S3fsCred::GetBucket() + path; + + if(nullptr == (dp = opendir(abs_path.c_str()))){ + S3FS_PRN_ERR("could not open cache dir(%s) - errno(%d)", abs_path.c_str(), errno); + return; + } + + for(dent = readdir(dp); dent; dent = readdir(dp)){ + if(0 == strcmp(dent->d_name, "..") || 0 == strcmp(dent->d_name, ".")){ + continue; + } + std::string fullpath = abs_path; + fullpath += "/"; + fullpath += dent->d_name; + struct stat st; + if(0 != lstat(fullpath.c_str(), &st)){ + S3FS_PRN_ERR("could not get stats of file(%s) - errno(%d)", fullpath.c_str(), errno); + closedir(dp); + return; + } + std::string next_path = path + "/" + dent->d_name; + if(S_ISDIR(st.st_mode)){ + CleanupCacheDirInternal(next_path); + }else{ + AutoLock auto_lock(&FdManager::fd_manager_lock, AutoLock::NO_WAIT); + if (!auto_lock.isLockAcquired()) { + S3FS_PRN_INFO("could not get fd_manager_lock when clean up file(%s), then skip it.", next_path.c_str()); + continue; + } + fdent_map_t::iterator iter = fent.find(next_path); + if(fent.end() == iter) { + S3FS_PRN_DBG("cleaned up: %s", next_path.c_str()); + FdManager::DeleteCacheFile(next_path.c_str()); + } + } + } + closedir(dp); +} + +bool FdManager::ReserveDiskSpace(off_t size) +{ + if(IsSafeDiskSpace(nullptr, size)){ + AutoLock auto_lock(&FdManager::reserved_diskspace_lock); + free_disk_space += size; + return true; + } + return false; +} + +void FdManager::FreeReservedDiskSpace(off_t size) +{ + AutoLock auto_lock(&FdManager::reserved_diskspace_lock); + free_disk_space -= size; +} + +// +// Inspect all files for stats file for cache file +// +// [NOTE] +// The minimum sub_path parameter is "/". +// The sub_path is a directory path starting from "/" and ending with "/". +// +// This method produces the following output. +// +// * Header +// ------------------------------------------------------------ +// Check cache file and its stats file consistency +// ------------------------------------------------------------ +// * When the cache file and its stats information match +// File path: -> [OK] no problem +// +// * If there is a problem with the cache file and its stats information +// File path: +// -> [P] +// -> [E] there is a mark that data exists in stats, but there is no data in the cache file. +// (bytes) +// ... +// ... +// -> [W] These show no data in stats, but there is evidence of data in the cache file.(no problem.) +// (bytes) +// ... +// ... +// +bool FdManager::RawCheckAllCache(FILE* fp, const char* cache_stat_top_dir, const char* sub_path, int& total_file_cnt, int& err_file_cnt, int& err_dir_cnt) +{ + if(!cache_stat_top_dir || '\0' == cache_stat_top_dir[0] || !sub_path || '\0' == sub_path[0]){ + S3FS_PRN_ERR("Parameter cache_stat_top_dir is empty."); + return false; + } + + // open directory of cache file's stats + DIR* statsdir; + std::string target_dir = cache_stat_top_dir; + target_dir += sub_path; + if(nullptr == (statsdir = opendir(target_dir.c_str()))){ + S3FS_PRN_ERR("Could not open directory(%s) by errno(%d)", target_dir.c_str(), errno); + return false; + } + + // loop in directory of cache file's stats + const struct dirent* pdirent = nullptr; + while(nullptr != (pdirent = readdir(statsdir))){ + if(DT_DIR == pdirent->d_type){ + // found directory + if(0 == strcmp(pdirent->d_name, ".") || 0 == strcmp(pdirent->d_name, "..")){ + continue; + } + + // reentrant for sub directory + std::string subdir_path = sub_path; + subdir_path += pdirent->d_name; + subdir_path += '/'; + if(!RawCheckAllCache(fp, cache_stat_top_dir, subdir_path.c_str(), total_file_cnt, err_file_cnt, err_dir_cnt)){ + // put error message for this dir. + ++err_dir_cnt; + S3FS_PRN_CACHE(fp, CACHEDBG_FMT_DIR_PROB, subdir_path.c_str()); + S3FS_PRN_CACHE(fp, CACHEDBG_FMT_CRIT_HEAD, "Something error is occurred in checking this directory"); + } + + }else{ + ++total_file_cnt; + + // make cache file path + std::string strOpenedWarn; + std::string cache_path; + std::string object_file_path = sub_path; + object_file_path += pdirent->d_name; + if(!FdManager::MakeCachePath(object_file_path.c_str(), cache_path, false, false) || cache_path.empty()){ + ++err_file_cnt; + S3FS_PRN_CACHE(fp, CACHEDBG_FMT_FILE_PROB, object_file_path.c_str(), strOpenedWarn.c_str()); + S3FS_PRN_CACHE(fp, CACHEDBG_FMT_CRIT_HEAD, "Could not make cache file path"); + continue; + } + + // check if the target file is currently in operation. + { + AutoLock auto_lock(&FdManager::fd_manager_lock); + + fdent_map_t::iterator iter = fent.find(object_file_path); + if(fent.end() != iter){ + // This file is opened now, then we need to put warning message. + strOpenedWarn = CACHEDBG_FMT_WARN_OPEN; + } + } + + // open cache file + int cache_file_fd; + if(-1 == (cache_file_fd = open(cache_path.c_str(), O_RDONLY))){ + ++err_file_cnt; + S3FS_PRN_CACHE(fp, CACHEDBG_FMT_FILE_PROB, object_file_path.c_str(), strOpenedWarn.c_str()); + S3FS_PRN_CACHE(fp, CACHEDBG_FMT_CRIT_HEAD, "Could not open cache file"); + continue; + } + scope_guard guard([&]() { close(cache_file_fd); }); + + // get inode number for cache file + struct stat st; + if(0 != fstat(cache_file_fd, &st)){ + ++err_file_cnt; + S3FS_PRN_CACHE(fp, CACHEDBG_FMT_FILE_PROB, object_file_path.c_str(), strOpenedWarn.c_str()); + S3FS_PRN_CACHE(fp, CACHEDBG_FMT_CRIT_HEAD, "Could not get file inode number for cache file"); + + continue; + } + ino_t cache_file_inode = st.st_ino; + + // open cache stat file and load page info. + PageList pagelist; + CacheFileStat cfstat(object_file_path.c_str()); + if(!cfstat.ReadOnlyOpen() || !pagelist.Serialize(cfstat, false, cache_file_inode)){ + ++err_file_cnt; + S3FS_PRN_CACHE(fp, CACHEDBG_FMT_FILE_PROB, object_file_path.c_str(), strOpenedWarn.c_str()); + S3FS_PRN_CACHE(fp, CACHEDBG_FMT_CRIT_HEAD, "Could not load cache file stats information"); + + continue; + } + cfstat.Release(); + + // compare cache file size and stats information + if(st.st_size != pagelist.Size()){ + ++err_file_cnt; + S3FS_PRN_CACHE(fp, CACHEDBG_FMT_FILE_PROB, object_file_path.c_str(), strOpenedWarn.c_str()); + S3FS_PRN_CACHE(fp, CACHEDBG_FMT_CRIT_HEAD2 "The cache file size(%lld) and the value(%lld) from cache file stats are different", static_cast(st.st_size), static_cast(pagelist.Size())); + + continue; + } + + // compare cache file stats and cache file blocks + fdpage_list_t err_area_list; + fdpage_list_t warn_area_list; + if(!pagelist.CompareSparseFile(cache_file_fd, st.st_size, err_area_list, warn_area_list)){ + // Found some error or warning + S3FS_PRN_CACHE(fp, CACHEDBG_FMT_FILE_PROB, object_file_path.c_str(), strOpenedWarn.c_str()); + if(!warn_area_list.empty()){ + S3FS_PRN_CACHE(fp, CACHEDBG_FMT_WARN_HEAD); + for(fdpage_list_t::const_iterator witer = warn_area_list.begin(); witer != warn_area_list.end(); ++witer){ + S3FS_PRN_CACHE(fp, CACHEDBG_FMT_PROB_BLOCK, static_cast(witer->offset), static_cast(witer->bytes)); + } + } + if(!err_area_list.empty()){ + ++err_file_cnt; + S3FS_PRN_CACHE(fp, CACHEDBG_FMT_ERR_HEAD); + for(fdpage_list_t::const_iterator eiter = err_area_list.begin(); eiter != err_area_list.end(); ++eiter){ + S3FS_PRN_CACHE(fp, CACHEDBG_FMT_PROB_BLOCK, static_cast(eiter->offset), static_cast(eiter->bytes)); + } + } + }else{ + // There is no problem! + if(!strOpenedWarn.empty()){ + strOpenedWarn += "\n "; + } + S3FS_PRN_CACHE(fp, CACHEDBG_FMT_FILE_OK, object_file_path.c_str(), strOpenedWarn.c_str()); + } + err_area_list.clear(); + warn_area_list.clear(); + } + } + closedir(statsdir); + + return true; +} + +bool FdManager::CheckAllCache() +{ + if(!FdManager::HaveLseekHole()){ + S3FS_PRN_ERR("lseek does not support SEEK_DATA/SEEK_HOLE, then could not check cache."); + return false; + } + + FILE* fp; + if(FdManager::check_cache_output.empty()){ + fp = stdout; + }else{ + if(nullptr == (fp = fopen(FdManager::check_cache_output.c_str(), "a+"))){ + S3FS_PRN_ERR("Could not open(create) output file(%s) for checking all cache by errno(%d)", FdManager::check_cache_output.c_str(), errno); + return false; + } + } + + // print head message + S3FS_PRN_CACHE(fp, CACHEDBG_FMT_HEAD, S3fsLog::GetCurrentTime().c_str()); + + // Loop in directory of cache file's stats + std::string top_path = CacheFileStat::GetCacheFileStatTopDir(); + int total_file_cnt = 0; + int err_file_cnt = 0; + int err_dir_cnt = 0; + bool result = RawCheckAllCache(fp, top_path.c_str(), "/", total_file_cnt, err_file_cnt, err_dir_cnt); + if(!result){ + S3FS_PRN_ERR("Processing failed due to some problem."); + } + + // print foot message + S3FS_PRN_CACHE(fp, CACHEDBG_FMT_FOOT, total_file_cnt, err_file_cnt, err_dir_cnt); + + if(stdout != fp){ + fclose(fp); + } + + return result; +} + +void FdManager::ReleaseCache(const std::string &avoid_path, off_t size, const std::string &dir) +{ + DIR* dp; + struct dirent* dent; + std::string abs_path = cache_dir + "/" + S3fsCred::GetBucket() + dir; + + if(nullptr == (dp = opendir(abs_path.c_str()))){ + S3FS_PRN_ERR("could not open cache dir(%s) - errno(%d)", abs_path.c_str(), errno); + return; + } + + for(dent = readdir(dp); dent; dent = readdir(dp)){ + if(GetFreeDiskSpace(nullptr) >= size) + return; + + if(0 == strcmp(dent->d_name, "..") || 0 == strcmp(dent->d_name, ".")){ + continue; + } + std::string fullpath = abs_path; + fullpath += "/"; + fullpath += dent->d_name; + struct stat st; + if(0 != lstat(fullpath.c_str(), &st)){ + S3FS_PRN_ERR("could not get stats of file(%s) - errno(%d)", fullpath.c_str(), errno); + continue; + } + std::string next_path = dir + "/" + dent->d_name; + + if(next_path == avoid_path) + continue; + + if(S_ISDIR(st.st_mode)){ + ReleaseCache(avoid_path, size, next_path); + }else{ + AutoLock auto_lock(&FdManager::fd_manager_lock, AutoLock::NO_WAIT); + if (!auto_lock.isLockAcquired()) { + S3FS_PRN_INFO("could not get fd_manager_lock when clean up file(%s), then skip it.", next_path.c_str()); + continue; + } + fdent_map_t::iterator iter = fent.find(next_path); + if(fent.end() == iter) { + S3FS_PRN_DBG("cleaned up: %s", next_path.c_str()); + FdManager::DeleteCacheFile(next_path.c_str()); + }else{ + FdEntity* ent = (*iter).second.get(); + ent->ReleaseCache(); + } + } + } + closedir(dp); +} + +bool FdManager::EnsureDiskSpaceUsable(const std::string &avoid_path, off_t size) +{ + if(GetFreeDiskSpace(nullptr) >= size) + return true; + + ReleaseCache(avoid_path, size, ""); + + return GetFreeDiskSpace(nullptr) >= size; +} + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/fdcache.h b/s3fs/fdcache.h new file mode 100644 index 0000000..3ea8d7a --- /dev/null +++ b/s3fs/fdcache.h @@ -0,0 +1,118 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef S3FS_FDCACHE_H_ +#define S3FS_FDCACHE_H_ + +#include "fdcache_entity.h" + +//------------------------------------------------ +// class FdManager +//------------------------------------------------ +class FdManager +{ + private: + static FdManager singleton; + static pthread_mutex_t fd_manager_lock; + static pthread_mutex_t cache_cleanup_lock; + static pthread_mutex_t reserved_diskspace_lock; + static bool is_lock_init; + static std::string cache_dir; + static bool check_cache_dir_exist; + static off_t free_disk_space; // limit free disk space + static off_t fake_used_disk_space; // difference between fake free disk space and actual at startup(for test/debug) + static std::string check_cache_output; + static bool checked_lseek; + static bool have_lseek_hole; + static std::string tmp_dir; + + fdent_map_t fent; + + private: + static off_t GetFreeDiskSpace(const char* path); + static off_t GetTotalDiskSpace(const char* path); + static bool IsDir(const std::string* dir); + static int GetVfsStat(const char* path, struct statvfs* vfsbuf); + + int GetPseudoFdCount(const char* path); + void CleanupCacheDirInternal(const std::string &path = ""); + bool RawCheckAllCache(FILE* fp, const char* cache_stat_top_dir, const char* sub_path, int& total_file_cnt, int& err_file_cnt, int& err_dir_cnt); + + void ReleaseCache(const std::string &avoid_path, off_t size, const std::string &dir); + + public: + FdManager(); + ~FdManager(); + + // Reference singleton + static FdManager* get() { return &singleton; } + + static bool DeleteCacheDirectory(); + static int DeleteCacheFile(const char* path); + static bool SetCacheDir(const char* dir); + static bool IsCacheDir() { return !FdManager::cache_dir.empty(); } + static const char* GetCacheDir() { return FdManager::cache_dir.c_str(); } + static bool SetCacheCheckOutput(const char* path); + static const char* GetCacheCheckOutput() { return FdManager::check_cache_output.c_str(); } + static bool MakeCachePath(const char* path, std::string& cache_path, bool is_create_dir = true, bool is_mirror_path = false); + static bool CheckCacheTopDir(); + static bool MakeRandomTempPath(const char* path, std::string& tmppath); + static bool SetCheckCacheDirExist(bool is_check); + static bool CheckCacheDirExist(); + static bool HasOpenEntityFd(const char* path); + static int GetOpenFdCount(const char* path); + static off_t GetEnsureFreeDiskSpace(); + static off_t SetEnsureFreeDiskSpace(off_t size); + static bool InitFakeUsedDiskSize(off_t fake_freesize); + static bool IsSafeDiskSpace(const char* path, off_t size); + static bool IsSafeDiskSpaceWithLog(const char* path, off_t size); + static void FreeReservedDiskSpace(off_t size); + static bool ReserveDiskSpace(off_t size); + static bool HaveLseekHole(); + static bool SetTmpDir(const char* dir); + static bool CheckTmpDirExist(); + static FILE* MakeTempFile(); + static off_t GetTotalDiskSpaceByRatio(int ratio); + + // Return FdEntity associated with path, returning nullptr on error. This operation increments the reference count; callers must decrement via Close after use. + FdEntity* GetFdEntity(const char* path, int& existfd, bool newfd = true, AutoLock::Type locktype = AutoLock::NONE); + FdEntity* Open(int& fd, const char* path, const headers_t* pmeta, off_t size, const struct timespec& ts_mctime, int flags, bool force_tmpfile, bool is_create, bool ignore_modify, AutoLock::Type type); + FdEntity* GetExistFdEntity(const char* path, int existfd = -1); + FdEntity* OpenExistFdEntity(const char* path, int& fd, int flags = O_RDONLY); + void Rename(const std::string &from, const std::string &to); + bool Close(FdEntity* ent, int fd); + bool ChangeEntityToTempPath(FdEntity* ent, const char* path); + void CleanupCacheDir(); + + bool CheckAllCache(); + + bool EnsureDiskSpaceUsable(const std::string &avoid_path, off_t size); +}; + +#endif // S3FS_FDCACHE_H_ + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/fdcache_auto.cpp b/s3fs/fdcache_auto.cpp new file mode 100644 index 0000000..5a97073 --- /dev/null +++ b/s3fs/fdcache_auto.cpp @@ -0,0 +1,126 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Takeshi Nakatani + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include + +#include "s3fs_logger.h" +#include "fdcache_auto.h" +#include "fdcache.h" + +//------------------------------------------------ +// AutoFdEntity methods +//------------------------------------------------ +AutoFdEntity::AutoFdEntity() : pFdEntity(nullptr), pseudo_fd(-1) +{ +} + +AutoFdEntity::~AutoFdEntity() +{ + Close(); +} + +bool AutoFdEntity::Close() +{ + if(pFdEntity){ + if(!FdManager::get()->Close(pFdEntity, pseudo_fd)){ + S3FS_PRN_ERR("Failed to close fdentity."); + return false; + } + pFdEntity = nullptr; + pseudo_fd = -1; + } + return true; +} + +// [NOTE] +// This method touches the internal fdentity with. +// This is used to keep the file open. +// +int AutoFdEntity::Detach() +{ + if(!pFdEntity){ + S3FS_PRN_ERR("Does not have a associated FdEntity."); + return -1; + } + int fd = pseudo_fd; + pseudo_fd = -1; + pFdEntity = nullptr; + + return fd; +} + +FdEntity* AutoFdEntity::Attach(const char* path, int existfd) +{ + Close(); + + if(nullptr == (pFdEntity = FdManager::get()->GetFdEntity(path, existfd, false))){ + S3FS_PRN_DBG("Could not find fd entity object(file=%s, pseudo_fd=%d)", path, existfd); + return nullptr; + } + pseudo_fd = existfd; + return pFdEntity; +} + +FdEntity* AutoFdEntity::Open(const char* path, const headers_t* pmeta, off_t size, const struct timespec& ts_mctime, int flags, bool force_tmpfile, bool is_create, bool ignore_modify, AutoLock::Type type, int* error) +{ + Close(); + + if(nullptr == (pFdEntity = FdManager::get()->Open(pseudo_fd, path, pmeta, size, ts_mctime, flags, force_tmpfile, is_create, ignore_modify, type))){ + if(error){ + *error = pseudo_fd; + } + pseudo_fd = -1; + return nullptr; + } + return pFdEntity; +} + +// [NOTE] +// the fd obtained by this method is not a newly created pseudo fd. +// +FdEntity* AutoFdEntity::GetExistFdEntity(const char* path, int existfd) +{ + Close(); + + FdEntity* ent; + if(nullptr == (ent = FdManager::get()->GetExistFdEntity(path, existfd))){ + return nullptr; + } + return ent; +} + +FdEntity* AutoFdEntity::OpenExistFdEntity(const char* path, int flags) +{ + Close(); + + if(nullptr == (pFdEntity = FdManager::get()->OpenExistFdEntity(path, pseudo_fd, flags))){ + return nullptr; + } + return pFdEntity; +} + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/fdcache_auto.h b/s3fs/fdcache_auto.h new file mode 100644 index 0000000..c96ee41 --- /dev/null +++ b/s3fs/fdcache_auto.h @@ -0,0 +1,74 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef S3FS_FDCACHE_AUTO_H_ +#define S3FS_FDCACHE_AUTO_H_ + +#include + +#include "autolock.h" +#include "metaheader.h" + +class FdEntity; + +//------------------------------------------------ +// class AutoFdEntity +//------------------------------------------------ +// A class that opens fdentry and closes it automatically. +// This class object is used to prevent inconsistencies in +// the number of references in fdentry. +// The methods are wrappers to the method of the FdManager class. +// +class AutoFdEntity +{ + private: + FdEntity* pFdEntity; + int pseudo_fd; + + private: + AutoFdEntity(const AutoFdEntity&) = delete; + AutoFdEntity(AutoFdEntity&&) = delete; + AutoFdEntity& operator=(const AutoFdEntity&) = delete; + AutoFdEntity& operator=(AutoFdEntity&&) = delete; + + public: + AutoFdEntity(); + ~AutoFdEntity(); + + bool Close(); + int Detach(); + FdEntity* Attach(const char* path, int existfd); + int GetPseudoFd() const { return pseudo_fd; } + + FdEntity* Open(const char* path, const headers_t* pmeta, off_t size, const struct timespec& ts_mctime, int flags, bool force_tmpfile, bool is_create, bool ignore_modify, AutoLock::Type type, int* error = nullptr); + FdEntity* GetExistFdEntity(const char* path, int existfd = -1); + FdEntity* OpenExistFdEntity(const char* path, int flags = O_RDONLY); +}; + +#endif // S3FS_FDCACHE_AUTO_H_ + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/fdcache_entity.cpp b/s3fs/fdcache_entity.cpp new file mode 100644 index 0000000..d7d4a6a --- /dev/null +++ b/s3fs/fdcache_entity.cpp @@ -0,0 +1,2907 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Takeshi Nakatani + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "common.h" +#include "fdcache_entity.h" +#include "fdcache_stat.h" +#include "fdcache_untreated.h" +#include "fdcache.h" +#include "string_util.h" +#include "s3fs_logger.h" +#include "s3fs_util.h" +#include "autolock.h" +#include "curl.h" +#include "s3fs_cred.h" + +//------------------------------------------------ +// FdEntity class variables +//------------------------------------------------ +bool FdEntity::mixmultipart = true; +bool FdEntity::streamupload = false; + +//------------------------------------------------ +// FdEntity class methods +//------------------------------------------------ +bool FdEntity::SetNoMixMultipart() +{ + bool old = mixmultipart; + mixmultipart = false; + return old; +} + +bool FdEntity::SetStreamUpload(bool isstream) +{ + bool old = streamupload; + streamupload = isstream; + return old; +} + +int FdEntity::FillFile(int fd, unsigned char byte, off_t size, off_t start) +{ + unsigned char bytes[1024 * 32]; // 32kb + memset(bytes, byte, std::min(static_cast(sizeof(bytes)), size)); + + for(off_t total = 0, onewrote = 0; total < size; total += onewrote){ + if(-1 == (onewrote = pwrite(fd, bytes, std::min(static_cast(sizeof(bytes)), size - total), start + total))){ + S3FS_PRN_ERR("pwrite failed. errno(%d)", errno); + return -errno; + } + } + return 0; +} + +// [NOTE] +// If fd is wrong or something error is occurred, return 0. +// The ino_t is allowed zero, but inode 0 is not realistic. +// So this method returns 0 on error assuming the correct +// inode is never 0. +// The caller must have exclusive control. +// +ino_t FdEntity::GetInode(int fd) +{ + if(-1 == fd){ + S3FS_PRN_ERR("file descriptor is wrong."); + return 0; + } + + struct stat st; + if(0 != fstat(fd, &st)){ + S3FS_PRN_ERR("could not get stat for physical file descriptor(%d) by errno(%d).", fd, errno); + return 0; + } + return st.st_ino; +} + +//------------------------------------------------ +// FdEntity methods +//------------------------------------------------ +FdEntity::FdEntity(const char* tpath, const char* cpath) : + is_lock_init(false), path(SAFESTRPTR(tpath)), + physical_fd(-1), pfile(nullptr), inode(0), size_orgmeta(0), + cachepath(SAFESTRPTR(cpath)), pending_status(pending_status_t::NO_UPDATE_PENDING) +{ + holding_mtime.tv_sec = -1; + holding_mtime.tv_nsec = 0; + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); +#if S3FS_PTHREAD_ERRORCHECK + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK); +#endif + int result; + if(0 != (result = pthread_mutex_init(&fdent_lock, &attr))){ + S3FS_PRN_CRIT("failed to init fdent_lock: %d", result); + abort(); + } + if(0 != (result = pthread_mutex_init(&fdent_data_lock, &attr))){ + S3FS_PRN_CRIT("failed to init fdent_data_lock: %d", result); + abort(); + } + is_lock_init = true; +} + +FdEntity::~FdEntity() +{ + Clear(); + + if(is_lock_init){ + int result; + if(0 != (result = pthread_mutex_destroy(&fdent_data_lock))){ + S3FS_PRN_CRIT("failed to destroy fdent_data_lock: %d", result); + abort(); + } + if(0 != (result = pthread_mutex_destroy(&fdent_lock))){ + S3FS_PRN_CRIT("failed to destroy fdent_lock: %d", result); + abort(); + } + is_lock_init = false; + } +} + +void FdEntity::Clear() +{ + AutoLock auto_lock(&fdent_lock); + AutoLock auto_data_lock(&fdent_data_lock); + + pseudo_fd_map.clear(); + + if(-1 != physical_fd){ + if(!cachepath.empty()){ + // [NOTE] + // Compare the inode of the existing cache file with the inode of + // the cache file output by this object, and if they are the same, + // serialize the pagelist. + // + ino_t cur_inode = GetInode(); + if(0 != cur_inode && cur_inode == inode){ + CacheFileStat cfstat(path.c_str()); + if(!pagelist.Serialize(cfstat, true, inode)){ + S3FS_PRN_WARN("failed to save cache stat file(%s).", path.c_str()); + } + } + } + if(pfile){ + fclose(pfile); + pfile = nullptr; + } + physical_fd = -1; + inode = 0; + + if(!mirrorpath.empty()){ + if(-1 == unlink(mirrorpath.c_str())){ + S3FS_PRN_WARN("failed to remove mirror cache file(%s) by errno(%d).", mirrorpath.c_str(), errno); + } + mirrorpath.erase(); + } + } + pagelist.Init(0, false, false); + path = ""; + cachepath = ""; +} + +// [NOTE] +// This method returns the inode of the file in cachepath. +// The return value is the same as the class method GetInode(). +// The caller must have exclusive control. +// +ino_t FdEntity::GetInode() const +{ + if(cachepath.empty()){ + S3FS_PRN_INFO("cache file path is empty, then return inode as 0."); + return 0; + } + + struct stat st; + if(0 != stat(cachepath.c_str(), &st)){ + S3FS_PRN_INFO("could not get stat for file(%s) by errno(%d).", cachepath.c_str(), errno); + return 0; + } + return st.st_ino; +} + +void FdEntity::Close(int fd) +{ + AutoLock auto_lock(&fdent_lock); + + S3FS_PRN_DBG("[path=%s][pseudo_fd=%d][physical_fd=%d]", path.c_str(), fd, physical_fd); + + // search pseudo fd and close it. + fdinfo_map_t::iterator iter = pseudo_fd_map.find(fd); + if(pseudo_fd_map.end() != iter){ + pseudo_fd_map.erase(iter); + }else{ + S3FS_PRN_WARN("Not found pseudo_fd(%d) in entity object(%s)", fd, path.c_str()); + } + + // check pseudo fd count + if(-1 != physical_fd && 0 == GetOpenCount(AutoLock::ALREADY_LOCKED)){ + AutoLock auto_data_lock(&fdent_data_lock); + if(!cachepath.empty()){ + // [NOTE] + // Compare the inode of the existing cache file with the inode of + // the cache file output by this object, and if they are the same, + // serialize the pagelist. + // + ino_t cur_inode = GetInode(); + if(0 != cur_inode && cur_inode == inode){ + CacheFileStat cfstat(path.c_str()); + if(!pagelist.Serialize(cfstat, true, inode)){ + S3FS_PRN_WARN("failed to save cache stat file(%s).", path.c_str()); + } + } + } + if(pfile){ + fclose(pfile); + pfile = nullptr; + } + physical_fd = -1; + inode = 0; + + if(!mirrorpath.empty()){ + if(-1 == unlink(mirrorpath.c_str())){ + S3FS_PRN_WARN("failed to remove mirror cache file(%s) by errno(%d).", mirrorpath.c_str(), errno); + } + mirrorpath.erase(); + } + } +} + +int FdEntity::Dup(int fd, AutoLock::Type locktype) +{ + AutoLock auto_lock(&fdent_lock, locktype); + + S3FS_PRN_DBG("[path=%s][pseudo_fd=%d][physical_fd=%d][pseudo fd count=%zu]", path.c_str(), fd, physical_fd, pseudo_fd_map.size()); + + if(-1 == physical_fd){ + return -1; + } + fdinfo_map_t::iterator iter = pseudo_fd_map.find(fd); + if(pseudo_fd_map.end() == iter){ + S3FS_PRN_ERR("Not found pseudo_fd(%d) in entity object(%s) for physical_fd(%d)", fd, path.c_str(), physical_fd); + return -1; + } + const PseudoFdInfo* org_pseudoinfo = iter->second.get(); + std::unique_ptr ppseudoinfo(new PseudoFdInfo(physical_fd, (org_pseudoinfo ? org_pseudoinfo->GetFlags() : 0))); + int pseudo_fd = ppseudoinfo->GetPseudoFd(); + pseudo_fd_map[pseudo_fd] = std::move(ppseudoinfo); + + return pseudo_fd; +} + +int FdEntity::OpenPseudoFd(int flags, AutoLock::Type locktype) +{ + AutoLock auto_lock(&fdent_lock, locktype); + + S3FS_PRN_DBG("[path=%s][physical_fd=%d][pseudo fd count=%zu]", path.c_str(), physical_fd, pseudo_fd_map.size()); + + if(-1 == physical_fd){ + return -1; + } + std::unique_ptr ppseudoinfo(new PseudoFdInfo(physical_fd, flags)); + int pseudo_fd = ppseudoinfo->GetPseudoFd(); + pseudo_fd_map[pseudo_fd] = std::move(ppseudoinfo); + + return pseudo_fd; +} + +int FdEntity::GetOpenCount(AutoLock::Type locktype) const +{ + AutoLock auto_lock(&fdent_lock, locktype); + + return static_cast(pseudo_fd_map.size()); +} + +// +// Open mirror file which is linked cache file. +// +int FdEntity::OpenMirrorFile() +{ + if(cachepath.empty()){ + S3FS_PRN_ERR("cache path is empty, why come here"); + return -EIO; + } + + // make temporary directory + std::string bupdir; + if(!FdManager::MakeCachePath(nullptr, bupdir, true, true)){ + S3FS_PRN_ERR("could not make bup cache directory path or create it."); + return -EIO; + } + + // create seed generating mirror file name + unsigned int seed = static_cast(time(nullptr)); + int urandom_fd; + if(-1 != (urandom_fd = open("/dev/urandom", O_RDONLY))){ + unsigned int rand_data; + if(sizeof(rand_data) == read(urandom_fd, &rand_data, sizeof(rand_data))){ + seed ^= rand_data; + } + close(urandom_fd); + } + + // try to link mirror file + while(true){ + // make random(temp) file path + // (do not care for threading, because allowed any value returned.) + // + char szfile[NAME_MAX + 1]; + snprintf(szfile, sizeof(szfile), "%x.tmp", rand_r(&seed)); + szfile[NAME_MAX] = '\0'; // for safety + mirrorpath = bupdir + "/" + szfile; + + // link mirror file to cache file + if(0 == link(cachepath.c_str(), mirrorpath.c_str())){ + break; + } + if(EEXIST != errno){ + S3FS_PRN_ERR("could not link mirror file(%s) to cache file(%s) by errno(%d).", mirrorpath.c_str(), cachepath.c_str(), errno); + return -errno; + } + ++seed; + } + + // open mirror file + int mirrorfd; + if(-1 == (mirrorfd = open(mirrorpath.c_str(), O_RDWR))){ + S3FS_PRN_ERR("could not open mirror file(%s) by errno(%d).", mirrorpath.c_str(), errno); + return -errno; + } + return mirrorfd; +} + +bool FdEntity::FindPseudoFd(int fd, AutoLock::Type locktype) const +{ + AutoLock auto_lock(&fdent_lock, locktype); + + if(-1 == fd){ + return false; + } + if(pseudo_fd_map.end() == pseudo_fd_map.find(fd)){ + return false; + } + return true; +} + +PseudoFdInfo* FdEntity::CheckPseudoFdFlags(int fd, bool writable, AutoLock::Type locktype) +{ + AutoLock auto_lock(&fdent_lock, locktype); + + if(-1 == fd){ + return nullptr; + } + fdinfo_map_t::iterator iter = pseudo_fd_map.find(fd); + if(pseudo_fd_map.end() == iter || nullptr == iter->second){ + return nullptr; + } + if(writable){ + if(!iter->second->Writable()){ + return nullptr; + } + }else{ + if(!iter->second->Readable()){ + return nullptr; + } + } + return iter->second.get(); +} + +bool FdEntity::IsUploading(AutoLock::Type locktype) +{ + AutoLock auto_lock(&fdent_lock, locktype); + + for(fdinfo_map_t::const_iterator iter = pseudo_fd_map.begin(); iter != pseudo_fd_map.end(); ++iter){ + const PseudoFdInfo* ppseudoinfo = iter->second.get(); + if(ppseudoinfo && ppseudoinfo->IsUploading()){ + return true; + } + } + return false; +} + +// [NOTE] +// If the open is successful, returns pseudo fd. +// If it fails, it returns an error code with a negative value. +// +// ts_mctime argument is a variable for mtime/ctime. +// If you want to disable this variable, specify UTIME_OMIT for +// tv_nsec in timespec member(in this case tv_sec member is ignored). +// This is similar to utimens operation. +// You can use "S3FS_OMIT_TS" global variable for UTIME_OMIT. +// +int FdEntity::Open(const headers_t* pmeta, off_t size, const struct timespec& ts_mctime, int flags, AutoLock::Type type) +{ + AutoLock auto_lock(&fdent_lock, type); + + S3FS_PRN_DBG("[path=%s][physical_fd=%d][size=%lld][ts_mctime=%s][flags=0x%x]", path.c_str(), physical_fd, static_cast(size), str(ts_mctime).c_str(), flags); + + if (!auto_lock.isLockAcquired()) { + // had to wait for fd lock, return + S3FS_PRN_ERR("Could not get lock."); + return -EIO; + } + + AutoLock auto_data_lock(&fdent_data_lock); + + // [NOTE] + // When the file size is incremental by truncating, it must be keeped + // as an untreated area, and this area is set to these variables. + // + off_t truncated_start = 0; + off_t truncated_size = 0; + + if(-1 != physical_fd){ + // + // already open file + // + + // check only file size(do not need to save cfs and time. + if(0 <= size && pagelist.Size() != size){ + // truncate temporary file size + if(-1 == ftruncate(physical_fd, size) || -1 == fsync(physical_fd)){ + S3FS_PRN_ERR("failed to truncate temporary file(physical_fd=%d) by errno(%d).", physical_fd, errno); + return -errno; + } + // resize page list + if(!pagelist.Resize(size, false, true)){ // Areas with increased size are modified + S3FS_PRN_ERR("failed to truncate temporary file information(physical_fd=%d).", physical_fd); + return -EIO; + } + } + + // set untreated area + if(0 <= size && size_orgmeta < size){ + // set untreated area + truncated_start = size_orgmeta; + truncated_size = size - size_orgmeta; + } + + // set original headers and set size. + off_t new_size = (0 <= size ? size : size_orgmeta); + if(pmeta){ + orgmeta = *pmeta; + size_orgmeta = get_size(orgmeta); + } + if(new_size < size_orgmeta){ + size_orgmeta = new_size; + } + + }else{ + // + // file is not opened yet + // + bool need_save_csf = false; // need to save(reset) cache stat file + bool is_truncate = false; // need to truncate + + std::unique_ptr pcfstat; + + if(!cachepath.empty()){ + // using cache + struct stat st; + if(stat(cachepath.c_str(), &st) == 0){ + if(0 > compare_timespec(st, stat_time_type::MTIME, ts_mctime)){ + S3FS_PRN_DBG("cache file stale, removing: %s", cachepath.c_str()); + if(unlink(cachepath.c_str()) != 0){ + return (0 == errno ? -EIO : -errno); + } + } + } + + // open cache and cache stat file, load page info. + pcfstat.reset(new CacheFileStat(path.c_str())); + // try to open cache file + if( -1 != (physical_fd = open(cachepath.c_str(), O_RDWR)) && + 0 != (inode = FdEntity::GetInode(physical_fd)) && + pagelist.Serialize(*pcfstat, false, inode) ) + { + // succeed to open cache file and to load stats data + memset(&st, 0, sizeof(struct stat)); + if(-1 == fstat(physical_fd, &st)){ + S3FS_PRN_ERR("fstat is failed. errno(%d)", errno); + physical_fd = -1; + inode = 0; + return (0 == errno ? -EIO : -errno); + } + // check size, st_size, loading stat file + if(-1 == size){ + if(st.st_size != pagelist.Size()){ + pagelist.Resize(st.st_size, false, true); // Areas with increased size are modified + need_save_csf = true; // need to update page info + } + size = st.st_size; + }else{ + // First if the current cache file size and pagelist do not match, fix pagelist. + if(st.st_size != pagelist.Size()){ + pagelist.Resize(st.st_size, false, true); // Areas with increased size are modified + need_save_csf = true; // need to update page info + } + if(size != pagelist.Size()){ + pagelist.Resize(size, false, true); // Areas with increased size are modified + need_save_csf = true; // need to update page info + } + if(size != st.st_size){ + is_truncate = true; + } + } + + }else{ + if(-1 != physical_fd){ + close(physical_fd); + } + inode = 0; + // could not open cache file or could not load stats data, so initialize it. + if(-1 == (physical_fd = open(cachepath.c_str(), O_CREAT|O_RDWR|O_TRUNC, 0600))){ + S3FS_PRN_ERR("failed to open file(%s). errno(%d)", cachepath.c_str(), errno); + + // remove cache stat file if it is existed + int result; + if(0 != (result = CacheFileStat::DeleteCacheFileStat(path.c_str()))){ + if(-ENOENT != result){ + S3FS_PRN_WARN("failed to delete current cache stat file(%s) by errno(%d), but continue...", path.c_str(), result); + } + } + return result; + } + need_save_csf = true; // need to update page info + inode = FdEntity::GetInode(physical_fd); + if(-1 == size){ + size = 0; + pagelist.Init(0, false, false); + }else{ + // [NOTE] + // The modify flag must not be set when opening a file, + // if the ts_mctime parameter(mtime) is specified(tv_nsec != UTIME_OMIT) + // and the cache file does not exist. + // If mtime is specified for the file and the cache file + // mtime is older than it, the cache file is removed and + // the processing comes here. + // + pagelist.Resize(size, false, (UTIME_OMIT == ts_mctime.tv_nsec ? true : false)); + + is_truncate = true; + } + } + + // open mirror file + int mirrorfd; + if(0 >= (mirrorfd = OpenMirrorFile())){ + S3FS_PRN_ERR("failed to open mirror file linked cache file(%s).", cachepath.c_str()); + return (0 == mirrorfd ? -EIO : mirrorfd); + } + // switch fd + close(physical_fd); + physical_fd = mirrorfd; + + // make file pointer(for being same tmpfile) + if(nullptr == (pfile = fdopen(physical_fd, "wb"))){ + S3FS_PRN_ERR("failed to get fileno(%s). errno(%d)", cachepath.c_str(), errno); + close(physical_fd); + physical_fd = -1; + inode = 0; + return (0 == errno ? -EIO : -errno); + } + + }else{ + // not using cache + inode = 0; + + // open temporary file + if(nullptr == (pfile = FdManager::MakeTempFile()) || -1 ==(physical_fd = fileno(pfile))){ + S3FS_PRN_ERR("failed to open temporary file by errno(%d)", errno); + if(pfile){ + fclose(pfile); + pfile = nullptr; + } + return (0 == errno ? -EIO : -errno); + } + if(-1 == size){ + size = 0; + pagelist.Init(0, false, false); + }else{ + // [NOTE] + // The modify flag must not be set when opening a file, + // if the ts_mctime parameter(mtime) is specified(tv_nsec != UTIME_OMIT) + // and the cache file does not exist. + // If mtime is specified for the file and the cache file + // mtime is older than it, the cache file is removed and + // the processing comes here. + // + pagelist.Resize(size, false, (UTIME_OMIT == ts_mctime.tv_nsec ? true : false)); + + is_truncate = true; + } + } + + // truncate cache(tmp) file + if(is_truncate){ + if(0 != ftruncate(physical_fd, size) || 0 != fsync(physical_fd)){ + S3FS_PRN_ERR("ftruncate(%s) or fsync returned err(%d)", cachepath.c_str(), errno); + fclose(pfile); + pfile = nullptr; + physical_fd = -1; + inode = 0; + return (0 == errno ? -EIO : -errno); + } + } + + // reset cache stat file + if(need_save_csf && pcfstat.get()){ + if(!pagelist.Serialize(*pcfstat, true, inode)){ + S3FS_PRN_WARN("failed to save cache stat file(%s), but continue...", path.c_str()); + } + } + + // set original headers and size in it. + if(pmeta){ + orgmeta = *pmeta; + size_orgmeta = get_size(orgmeta); + }else{ + orgmeta.clear(); + size_orgmeta = 0; + } + + // set untreated area + if(0 <= size && size_orgmeta < size){ + truncated_start = size_orgmeta; + truncated_size = size - size_orgmeta; + } + + // set mtime and ctime(set "x-amz-meta-mtime" and "x-amz-meta-ctime" in orgmeta) + if(UTIME_OMIT != ts_mctime.tv_nsec){ + if(0 != SetMCtime(ts_mctime, ts_mctime, AutoLock::ALREADY_LOCKED)){ + S3FS_PRN_ERR("failed to set mtime/ctime. errno(%d)", errno); + fclose(pfile); + pfile = nullptr; + physical_fd = -1; + inode = 0; + return (0 == errno ? -EIO : -errno); + } + } + } + + // create new pseudo fd, and set it to map + std::unique_ptr ppseudoinfo(new PseudoFdInfo(physical_fd, flags)); + int pseudo_fd = ppseudoinfo->GetPseudoFd(); + pseudo_fd_map[pseudo_fd] = std::move(ppseudoinfo); + + // if there is untreated area, set it to pseudo object. + if(0 < truncated_size){ + if(!AddUntreated(truncated_start, truncated_size)){ + pseudo_fd_map.erase(pseudo_fd); + if(pfile){ + fclose(pfile); + pfile = nullptr; + } + } + } + + return pseudo_fd; +} + +// [NOTE] +// This method is called for only nocopyapi functions. +// So we do not check disk space for this option mode, if there is no enough +// disk space this method will be failed. +// +bool FdEntity::LoadAll(int fd, headers_t* pmeta, off_t* size, bool force_load) +{ + AutoLock auto_lock(&fdent_lock); + + S3FS_PRN_INFO3("[path=%s][pseudo_fd=%d][physical_fd=%d]", path.c_str(), fd, physical_fd); + + if(-1 == physical_fd || !FindPseudoFd(fd, AutoLock::ALREADY_LOCKED)){ + S3FS_PRN_ERR("pseudo_fd(%d) and physical_fd(%d) for path(%s) is not opened yet", fd, physical_fd, path.c_str()); + return false; + } + + AutoLock auto_data_lock(&fdent_data_lock); + + if(force_load){ + SetAllStatusUnloaded(); + } + // + // TODO: possibly do background for delay loading + // + int result; + if(0 != (result = Load(/*start=*/ 0, /*size=*/ 0, AutoLock::ALREADY_LOCKED))){ + S3FS_PRN_ERR("could not download, result(%d)", result); + return false; + } + if(size){ + *size = pagelist.Size(); + } + return true; +} + +// +// Rename file path. +// +// This method sets the FdManager::fent map registration key to fentmapkey. +// +// [NOTE] +// This method changes the file path of FdEntity. +// Old file is deleted after linking to the new file path, and this works +// without problem because the file descriptor is not affected even if the +// cache file is open. +// The mirror file descriptor is also the same. The mirror file path does +// not need to be changed and will remain as it is. +// +bool FdEntity::RenamePath(const std::string& newpath, std::string& fentmapkey) +{ + if(!cachepath.empty()){ + // has cache path + + // make new cache path + std::string newcachepath; + if(!FdManager::MakeCachePath(newpath.c_str(), newcachepath, true)){ + S3FS_PRN_ERR("failed to make cache path for object(%s).", newpath.c_str()); + return false; + } + + // rename cache file + if(-1 == rename(cachepath.c_str(), newcachepath.c_str())){ + S3FS_PRN_ERR("failed to rename old cache path(%s) to new cache path(%s) by errno(%d).", cachepath.c_str(), newcachepath.c_str(), errno); + return false; + } + + // link and unlink cache file stat + if(!CacheFileStat::RenameCacheFileStat(path.c_str(), newpath.c_str())){ + S3FS_PRN_ERR("failed to rename cache file stat(%s to %s).", path.c_str(), newpath.c_str()); + return false; + } + fentmapkey = newpath; + cachepath = newcachepath; + + }else{ + // does not have cache path + fentmapkey.erase(); + FdManager::MakeRandomTempPath(newpath.c_str(), fentmapkey); + } + // set new path + path = newpath; + + return true; +} + +bool FdEntity::IsModified() const +{ + if(use_newcache){ + return GetUpdateMark(); + } + + AutoLock auto_lock(&fdent_lock); + AutoLock auto_data_lock2(&fdent_data_lock); + return pagelist.IsModified(); +} + +bool FdEntity::GetStats(struct stat& st, AutoLock::Type locktype) const +{ + AutoLock auto_lock(&fdent_lock, locktype); + if(-1 == physical_fd){ + return false; + } + + memset(&st, 0, sizeof(struct stat)); + if(-1 == fstat(physical_fd, &st)){ + S3FS_PRN_ERR("fstat failed. errno(%d)", errno); + return false; + } + + if(use_newcache){ + st.st_size = GetRealsize(); + } + + return true; +} + +int FdEntity::SetCtime(struct timespec time, AutoLock::Type locktype) +{ + AutoLock auto_lock(&fdent_lock, locktype); + + S3FS_PRN_INFO3("[path=%s][physical_fd=%d][time=%s]", path.c_str(), physical_fd, str(time).c_str()); + + if(-1 == time.tv_sec){ + return 0; + } + orgmeta["x-amz-meta-ctime"] = str(time); + return 0; +} + +int FdEntity::SetAtime(struct timespec time, AutoLock::Type locktype) +{ + AutoLock auto_lock(&fdent_lock, locktype); + + S3FS_PRN_INFO3("[path=%s][physical_fd=%d][time=%s]", path.c_str(), physical_fd, str(time).c_str()); + + if(-1 == time.tv_sec){ + return 0; + } + orgmeta["x-amz-meta-atime"] = str(time); + return 0; +} + +// [NOTE] +// This method updates mtime as well as ctime. +// +int FdEntity::SetMCtime(struct timespec mtime, struct timespec ctime, AutoLock::Type locktype) +{ + AutoLock auto_lock(&fdent_lock, locktype); + + S3FS_PRN_INFO3("[path=%s][physical_fd=%d][mtime=%s][ctime=%s]", path.c_str(), physical_fd, str(mtime).c_str(), str(ctime).c_str()); + + if(mtime.tv_sec < 0 || ctime.tv_sec < 0){ + return 0; + } + + if(-1 != physical_fd){ + struct timespec ts[2]; + ts[0].tv_sec = mtime.tv_sec; + ts[0].tv_nsec = mtime.tv_nsec; + ts[1].tv_sec = ctime.tv_sec; + ts[1].tv_nsec = ctime.tv_nsec; + if(-1 == futimens(physical_fd, ts)){ + S3FS_PRN_ERR("futimens failed. errno(%d)", errno); + return -errno; + } + }else if(!cachepath.empty()){ + // not opened file yet. + struct timespec ts[2]; + ts[0].tv_sec = ctime.tv_sec; + ts[0].tv_nsec = ctime.tv_nsec; + ts[1].tv_sec = mtime.tv_sec; + ts[1].tv_nsec = mtime.tv_nsec; + if(-1 == utimensat(AT_FDCWD, cachepath.c_str(), ts, 0)){ + S3FS_PRN_ERR("utimensat failed. errno(%d)", errno); + return -errno; + } + } + + orgmeta["x-amz-meta-mtime"] = str(mtime); + orgmeta["x-amz-meta-ctime"] = str(ctime); + + return 0; +} + +bool FdEntity::UpdateCtime() +{ + AutoLock auto_lock(&fdent_lock); + struct stat st; + if(!GetStats(st, AutoLock::ALREADY_LOCKED)){ + return false; + } + + orgmeta["x-amz-meta-ctime"] = str_stat_time(st, stat_time_type::CTIME); + + return true; +} + +bool FdEntity::UpdateAtime() +{ + AutoLock auto_lock(&fdent_lock); + struct stat st; + if(!GetStats(st, AutoLock::ALREADY_LOCKED)){ + return false; + } + + orgmeta["x-amz-meta-atime"] = str_stat_time(st, stat_time_type::ATIME); + + return true; +} + +bool FdEntity::UpdateMtime(bool clear_holding_mtime) +{ + AutoLock auto_lock(&fdent_lock); + + if(0 <= holding_mtime.tv_sec){ + // [NOTE] + // This conditional statement is very special. + // If you copy a file with "cp -p" etc., utimens or chown will be + // called after opening the file, after that call to write, flush. + // If normally utimens are not called(cases like "cp" only), mtime + // should be updated at the file flush. + // Here, check the holding_mtime value to prevent mtime from being + // overwritten. + // + if(clear_holding_mtime){ + if(!ClearHoldingMtime(AutoLock::ALREADY_LOCKED)){ + return false; + } + // [NOTE] + // If come here after fdatasync has been processed, the file + // content update has already taken place. However, the metadata + // update is necessary and needs to be flagged in order to + // perform it with flush, + // + pending_status = pending_status_t::UPDATE_META_PENDING; + } + }else{ + struct stat st; + if(!GetStats(st, AutoLock::ALREADY_LOCKED)){ + return false; + } + orgmeta["x-amz-meta-mtime"] = str_stat_time(st, stat_time_type::MTIME); + } + return true; +} + +bool FdEntity::SetHoldingMtime(struct timespec mtime, AutoLock::Type locktype) +{ + AutoLock auto_lock(&fdent_lock, locktype); + + S3FS_PRN_INFO3("[path=%s][physical_fd=%d][mtime=%s]", path.c_str(), physical_fd, str(mtime).c_str()); + + if(mtime.tv_sec < 0){ + return false; + } + holding_mtime = mtime; + return true; +} + +bool FdEntity::ClearHoldingMtime(AutoLock::Type locktype) +{ + AutoLock auto_lock(&fdent_lock, locktype); + + if(holding_mtime.tv_sec < 0){ + return false; + } + struct stat st; + if(!GetStats(st, AutoLock::ALREADY_LOCKED)){ + return false; + } + if(-1 != physical_fd){ + struct timespec ts[2]; + struct timespec ts_ctime; + + ts[0].tv_sec = holding_mtime.tv_sec; + ts[0].tv_nsec = holding_mtime.tv_nsec; + + set_stat_to_timespec(st, stat_time_type::CTIME, ts_ctime); + ts[1].tv_sec = ts_ctime.tv_sec; + ts[1].tv_nsec = ts_ctime.tv_nsec; + + if(-1 == futimens(physical_fd, ts)){ + S3FS_PRN_ERR("futimens failed. errno(%d)", errno); + return false; + } + }else if(!cachepath.empty()){ + // not opened file yet. + struct timespec ts[2]; + struct timespec ts_ctime; + + set_stat_to_timespec(st, stat_time_type::CTIME, ts_ctime); + ts[0].tv_sec = ts_ctime.tv_sec; + ts[0].tv_nsec = ts_ctime.tv_nsec; + + ts[1].tv_sec = holding_mtime.tv_sec; + ts[1].tv_nsec = holding_mtime.tv_nsec; + if(-1 == utimensat(AT_FDCWD, cachepath.c_str(), ts, 0)){ + S3FS_PRN_ERR("utimensat failed. errno(%d)", errno); + return false; + } + } + holding_mtime.tv_sec = -1; + holding_mtime.tv_nsec = 0; + + return true; +} + +bool FdEntity::GetSize(off_t& size) const +{ + AutoLock auto_lock(&fdent_lock); + if(-1 == physical_fd){ + return false; + } + + if(use_newcache){ + size = GetRealsize(); + return true; + } + + AutoLock auto_data_lock(&fdent_data_lock); + size = pagelist.Size(); + + return true; +} + +bool FdEntity::GetXattr(std::string& xattr) const +{ + AutoLock auto_lock(&fdent_lock); + + headers_t::const_iterator iter = orgmeta.find("x-amz-meta-xattr"); + if(iter == orgmeta.end()){ + return false; + } + xattr = iter->second; + return true; +} + +bool FdEntity::SetXattr(const std::string& xattr) +{ + AutoLock auto_lock(&fdent_lock); + orgmeta["x-amz-meta-xattr"] = xattr; + return true; +} + +bool FdEntity::SetMode(mode_t mode) +{ + AutoLock auto_lock(&fdent_lock); + orgmeta["x-amz-meta-mode"] = std::to_string(mode); + return true; +} + +bool FdEntity::SetUId(uid_t uid) +{ + AutoLock auto_lock(&fdent_lock); + orgmeta["x-amz-meta-uid"] = std::to_string(uid); + return true; +} + +bool FdEntity::SetGId(gid_t gid) +{ + AutoLock auto_lock(&fdent_lock); + orgmeta["x-amz-meta-gid"] = std::to_string(gid); + return true; +} + +bool FdEntity::SetContentType(const char* path) +{ + if(!path){ + return false; + } + AutoLock auto_lock(&fdent_lock); + orgmeta["Content-Type"] = S3fsCurl::LookupMimeType(path); + return true; +} + +bool FdEntity::SetAllStatus(bool is_loaded) +{ + S3FS_PRN_INFO3("[path=%s][physical_fd=%d][%s]", path.c_str(), physical_fd, is_loaded ? "loaded" : "unloaded"); + + if(-1 == physical_fd){ + return false; + } + // [NOTE] + // this method is only internal use, and calling after locking. + // so do not lock now. + // + //AutoLock auto_lock(&fdent_lock); + + // get file size + struct stat st; + memset(&st, 0, sizeof(struct stat)); + if(-1 == fstat(physical_fd, &st)){ + S3FS_PRN_ERR("fstat is failed. errno(%d)", errno); + return false; + } + // Reinit + pagelist.Init(st.st_size, is_loaded, false); + + return true; +} + +int FdEntity::Load(off_t start, off_t size, AutoLock::Type type, bool is_modified_flag) +{ + AutoLock auto_lock(&fdent_lock, type); + + S3FS_PRN_DBG("[path=%s][physical_fd=%d][offset=%lld][size=%lld]", path.c_str(), physical_fd, static_cast(start), static_cast(size)); + + if(-1 == physical_fd){ + return -EBADF; + } + AutoLock auto_data_lock(&fdent_data_lock, type); + + int result = 0; + + // check loaded area & load + fdpage_list_t unloaded_list; + if(0 < pagelist.GetUnloadedPages(unloaded_list, start, size)){ + for(fdpage_list_t::iterator iter = unloaded_list.begin(); iter != unloaded_list.end(); ++iter){ + if(0 != size && start + size <= iter->offset){ + // reached end + break; + } + // check loading size + off_t need_load_size = 0; + if(iter->offset < size_orgmeta){ + // original file size(on S3) is smaller than request. + need_load_size = (iter->next() <= size_orgmeta ? iter->bytes : (size_orgmeta - iter->offset)); + } + + // download + if(S3fsCurl::GetMultipartSize() <= need_load_size && !nomultipart){ + // parallel request + result = S3fsCurl::ParallelGetObjectRequest(path.c_str(), physical_fd, iter->offset, need_load_size); + }else{ + // single request + if(0 < need_load_size){ + S3fsCurl s3fscurl; + result = s3fscurl.GetObjectRequest(path.c_str(), physical_fd, iter->offset, need_load_size); + }else{ + result = 0; + } + } + + if(0 != result){ + break; + } + // Set loaded flag + pagelist.SetPageLoadedStatus(iter->offset, iter->bytes, (is_modified_flag ? PageList::page_status::LOAD_MODIFIED : PageList::page_status::LOADED)); + } + PageList::FreeList(unloaded_list); + } + return result; +} + +int FdEntity::LoadByAdaptor(off_t start, off_t size, AutoLock::Type type, std::shared_ptr dataAdaptor, bool is_modified_flag) +{ + AutoLock auto_lock(&fdent_lock, type); + + S3FS_PRN_DBG("[path=%s][physical_fd=%d][offset=%lld][size=%lld]", path.c_str(), physical_fd, static_cast(start), static_cast(size)); + + if(-1 == physical_fd){ + return -EBADF; + } + AutoLock auto_data_lock(&fdent_data_lock, type); + + int result = 0; + + // check loaded area & load + fdpage_list_t unloaded_list; + if(0 < pagelist.GetUnloadedPages(unloaded_list, start, size)){ + for(fdpage_list_t::iterator iter = unloaded_list.begin(); iter != unloaded_list.end(); ++iter){ + if(0 != size && start + size <= iter->offset){ + // reached end + break; + } + // check loading size + off_t need_load_size = 0; + if(iter->offset < size_orgmeta){ + // original file size(on S3) is smaller than request. + need_load_size = (iter->next() <= size_orgmeta ? iter->bytes : (size_orgmeta - iter->offset)); + } + + if(0 < need_load_size){ + std::unique_ptr buf(new char[need_load_size]); + HybridCache::ByteBuffer buffer(buf.get(), need_load_size); + result = dataAdaptor->DownLoad(path, iter->offset, need_load_size, buffer).get(); + if(0 == result){ + WriteCache(buffer.data, iter->offset, need_load_size, type); + } + }else{ + result = 0; + } + if(0 != result){ + break; + } + // Set loaded flag + pagelist.SetPageLoadedStatus(iter->offset, iter->bytes, (is_modified_flag ? PageList::page_status::LOAD_MODIFIED : PageList::page_status::LOADED)); + } + PageList::FreeList(unloaded_list); + } + return result; +} + +// [NOTE] +// At no disk space for caching object. +// This method is downloading by dividing an object of the specified range +// and uploading by multipart after finishing downloading it. +// +// [NOTICE] +// Need to lock before calling this method. +// +int FdEntity::NoCacheLoadAndPost(PseudoFdInfo* pseudo_obj, off_t start, off_t size) +{ + int result = 0; + + S3FS_PRN_INFO3("[path=%s][physical_fd=%d][offset=%lld][size=%lld]", path.c_str(), physical_fd, static_cast(start), static_cast(size)); + + if(!pseudo_obj){ + S3FS_PRN_ERR("Pseudo object is nullptr."); + return -EIO; + } + + if(-1 == physical_fd){ + return -EBADF; + } + + // [NOTE] + // This method calling means that the cache file is never used no more. + // + if(!cachepath.empty()){ + // remove cache files(and cache stat file) + FdManager::DeleteCacheFile(path.c_str()); + // cache file path does not use no more. + cachepath.erase(); + mirrorpath.erase(); + } + + // Change entity key in manager mapping + FdManager::get()->ChangeEntityToTempPath(this, path.c_str()); + + // open temporary file + int tmpfd; + std::unique_ptr ptmpfp(FdManager::MakeTempFile(), &s3fs_fclose); + if(nullptr == ptmpfp || -1 == (tmpfd = fileno(ptmpfp.get()))){ + S3FS_PRN_ERR("failed to open temporary file by errno(%d)", errno); + return (0 == errno ? -EIO : -errno); + } + + // loop uploading by multipart + for(fdpage_list_t::iterator iter = pagelist.pages.begin(); iter != pagelist.pages.end(); ++iter){ + if(iter->end() < start){ + continue; + } + if(0 != size && start + size <= iter->offset){ + break; + } + // download each multipart size(default 10MB) in unit + for(off_t oneread = 0, totalread = (iter->offset < start ? start : 0); totalread < static_cast(iter->bytes); totalread += oneread){ + int upload_fd = physical_fd; + off_t offset = iter->offset + totalread; + oneread = std::min(static_cast(iter->bytes) - totalread, S3fsCurl::GetMultipartSize()); + + // check rest size is over minimum part size + // + // [NOTE] + // If the final part size is smaller than 5MB, it is not allowed by S3 API. + // For this case, if the previous part of the final part is not over 5GB, + // we incorporate the final part to the previous part. If the previous part + // is over 5GB, we want to even out the last part and the previous part. + // + if((iter->bytes - totalread - oneread) < MIN_MULTIPART_SIZE){ + if(FIVE_GB < iter->bytes - totalread){ + oneread = (iter->bytes - totalread) / 2; + }else{ + oneread = iter->bytes - totalread; + } + } + + if(!iter->loaded){ + // + // loading or initializing + // + upload_fd = tmpfd; + + // load offset & size + size_t need_load_size = 0; + if(size_orgmeta <= offset){ + // all area is over of original size + need_load_size = 0; + }else{ + if(size_orgmeta < (offset + oneread)){ + // original file size(on S3) is smaller than request. + need_load_size = size_orgmeta - offset; + }else{ + need_load_size = oneread; + } + } + size_t over_size = oneread - need_load_size; + + // [NOTE] + // truncate file to zero and set length to part offset + size + // after this, file length is (offset + size), but file does not use any disk space. + // + if(-1 == ftruncate(tmpfd, 0) || -1 == ftruncate(tmpfd, (offset + oneread))){ + S3FS_PRN_ERR("failed to truncate temporary file(physical_fd=%d).", tmpfd); + result = -EIO; + break; + } + + // single area get request + if(0 < need_load_size){ + S3fsCurl s3fscurl; + if(0 != (result = s3fscurl.GetObjectRequest(path.c_str(), tmpfd, offset, oneread))){ + S3FS_PRN_ERR("failed to get object(start=%lld, size=%lld) for file(physical_fd=%d).", static_cast(offset), static_cast(oneread), tmpfd); + break; + } + } + // initialize fd without loading + if(0 < over_size){ + if(0 != (result = FdEntity::FillFile(tmpfd, 0, over_size, offset + need_load_size))){ + S3FS_PRN_ERR("failed to fill rest bytes for physical_fd(%d). errno(%d)", tmpfd, result); + break; + } + } + }else{ + // already loaded area + } + // single area upload by multipart post + if(0 != (result = NoCacheMultipartPost(pseudo_obj, upload_fd, offset, oneread))){ + S3FS_PRN_ERR("failed to multipart post(start=%lld, size=%lld) for file(physical_fd=%d).", static_cast(offset), static_cast(oneread), upload_fd); + break; + } + } + if(0 != result){ + break; + } + + // set loaded flag + if(!iter->loaded){ + if(iter->offset < start){ + fdpage page(iter->offset, start - iter->offset, iter->loaded, false); + iter->bytes -= (start - iter->offset); + iter->offset = start; + pagelist.pages.insert(iter, page); + } + if(0 != size && start + size < iter->next()){ + fdpage page(iter->offset, start + size - iter->offset, true, false); + iter->bytes -= (start + size - iter->offset); + iter->offset = start + size; + pagelist.pages.insert(iter, page); + }else{ + iter->loaded = true; + iter->modified = false; + } + } + } + if(0 == result){ + // compress pagelist + pagelist.Compress(); + + // fd data do empty + if(-1 == ftruncate(physical_fd, 0)){ + S3FS_PRN_ERR("failed to truncate file(physical_fd=%d), but continue...", physical_fd); + } + } + + return result; +} + +// [NOTE] +// At no disk space for caching object. +// This method is starting multipart uploading. +// +int FdEntity::NoCachePreMultipartPost(PseudoFdInfo* pseudo_obj) +{ + if(!pseudo_obj){ + S3FS_PRN_ERR("Internal error, pseudo fd object pointer is null."); + return -EIO; + } + + // initialize multipart upload values + pseudo_obj->ClearUploadInfo(true); + + S3fsCurl s3fscurl(true); + std::string upload_id; + int result; + if(0 != (result = s3fscurl.PreMultipartPostRequest(path.c_str(), orgmeta, upload_id, false))){ + return result; + } + s3fscurl.DestroyCurlHandle(); + + // Clear the dirty flag, because the meta data is updated. + pending_status = pending_status_t::NO_UPDATE_PENDING; + + // reset upload_id + if(!pseudo_obj->InitialUploadInfo(upload_id)){ + return -EIO; + } + return 0; +} + +// [NOTE] +// At no disk space for caching object. +// This method is uploading one part of multipart. +// +int FdEntity::NoCacheMultipartPost(PseudoFdInfo* pseudo_obj, int tgfd, off_t start, off_t size) +{ + if(-1 == tgfd || !pseudo_obj || !pseudo_obj->IsUploading()){ + S3FS_PRN_ERR("Need to initialize for multipart post."); + return -EIO; + } + + // get upload id + std::string upload_id; + if(!pseudo_obj->GetUploadId(upload_id)){ + return -EIO; + } + + // append new part and get it's etag string pointer + etagpair* petagpair = nullptr; + if(!pseudo_obj->AppendUploadPart(start, size, false, &petagpair)){ + return -EIO; + } + + S3fsCurl s3fscurl(true); + return s3fscurl.MultipartUploadRequest(upload_id, path.c_str(), tgfd, start, size, petagpair); +} + +// [NOTE] +// At no disk space for caching object. +// This method is finishing multipart uploading. +// +int FdEntity::NoCacheCompleteMultipartPost(PseudoFdInfo* pseudo_obj) +{ + etaglist_t etaglist; + if(!pseudo_obj || !pseudo_obj->IsUploading() || !pseudo_obj->GetEtaglist(etaglist)){ + S3FS_PRN_ERR("There is no upload id or etag list."); + return -EIO; + } + + // get upload id + std::string upload_id; + if(!pseudo_obj->GetUploadId(upload_id)){ + return -EIO; + } + + S3fsCurl s3fscurl(true); + int result = s3fscurl.CompleteMultipartPostRequest(path.c_str(), upload_id, etaglist); + s3fscurl.DestroyCurlHandle(); + if(0 != result){ + S3fsCurl s3fscurl_abort(true); + int result2 = s3fscurl.AbortMultipartUpload(path.c_str(), upload_id); + s3fscurl_abort.DestroyCurlHandle(); + if(0 != result2){ + S3FS_PRN_ERR("failed to abort multipart upload by errno(%d)", result2); + } + return result; + } + + // clear multipart upload info + untreated_list.ClearAll(); + pseudo_obj->ClearUploadInfo(); + + return 0; +} + +off_t FdEntity::BytesModified() +{ + AutoLock auto_lock(&fdent_lock); + AutoLock auto_lock2(&fdent_data_lock); + return pagelist.BytesModified(); +} + +// [NOTE] +// There are conditions that allow you to perform multipart uploads. +// +// According to the AWS spec: +// - 1 to 10,000 parts are allowed +// - minimum size of parts is 5MB (except for the last part) +// +// For example, if you set the minimum part size to 5MB, you can upload +// a maximum (5 * 10,000)MB file. +// The part size can be changed in MB units, then the maximum file size +// that can be handled can be further increased. +// Files smaller than the minimum part size will not be multipart uploaded, +// but will be uploaded as single part(normally). +// +int FdEntity::RowFlush(int fd, const char* tpath, AutoLock::Type type, bool force_sync, bool force_tmpfile) +{ + AutoLock auto_lock(&fdent_lock, type); + + S3FS_PRN_INFO3("[tpath=%s][path=%s][pseudo_fd=%d][physical_fd=%d]", SAFESTRPTR(tpath), path.c_str(), fd, physical_fd); + + if(-1 == physical_fd){ + return -EBADF; + } + + // check pseudo fd and its flag + fdinfo_map_t::iterator miter = pseudo_fd_map.find(fd); + if(pseudo_fd_map.end() == miter || nullptr == miter->second){ + return -EBADF; + } + if(!miter->second->Writable() && !(miter->second->GetFlags() & O_CREAT)){ + // If the entity is opened read-only, it will end normally without updating. + return 0; + } + + if(use_newcache && !force_tmpfile){ + if(!force_sync && !GetUpdateMark() && !IsDirtyMetadata()){ + S3FS_PRN_WARN("Nothing to update[path=%s][pseudo_fd=%d][physical_fd=%d]", path.c_str(), fd, physical_fd); + return 0; + } + int res = accessor->Flush(path); + if (0 == res) { + SetUpdateMark(false); + pagelist.ClearAllModified(); + pending_status = pending_status_t::NO_UPDATE_PENDING; + } + return res; + } + + PseudoFdInfo* pseudo_obj = miter->second.get(); + + AutoLock auto_lock2(&fdent_data_lock); + + int result; + if(!force_sync && !pagelist.IsModified() && !IsDirtyMetadata()){ + // nothing to update. + return 0; + } + if(S3fsLog::IsS3fsLogDbg()){ + pagelist.Dump(); + } + + if(nomultipart){ + // No multipart upload + if(!force_sync && !pagelist.IsModified()){ + // for only push pending headers + result = UploadPending(-1, AutoLock::ALREADY_LOCKED); + }else{ + result = RowFlushNoMultipart(pseudo_obj, tpath); + } + }else if(FdEntity::streamupload){ + // Stream multipart upload + result = RowFlushStreamMultipart(pseudo_obj, tpath); + }else if(FdEntity::mixmultipart){ + // Mix multipart upload + result = RowFlushMixMultipart(pseudo_obj, tpath); + }else{ + // Normal multipart upload + result = RowFlushMultipart(pseudo_obj, tpath); + } + + // [NOTE] + // if something went wrong, so if you are using a cache file, + // the cache file may not be correct. So delete cache files. + // + if(0 != result && !cachepath.empty()){ + FdManager::DeleteCacheFile(tpath); + } + + return result; +} + +// [NOTE] +// Both fdent_lock and fdent_data_lock must be locked before calling. +// +int FdEntity::RowFlushNoMultipart(const PseudoFdInfo* pseudo_obj, const char* tpath) +{ + S3FS_PRN_INFO3("[tpath=%s][path=%s][pseudo_fd=%d][physical_fd=%d]", SAFESTRPTR(tpath), path.c_str(), (pseudo_obj ? pseudo_obj->GetPseudoFd() : -1), physical_fd); + + if(-1 == physical_fd || !pseudo_obj){ + return -EBADF; + } + + if(pseudo_obj->IsUploading()){ + S3FS_PRN_ERR("Why uploading now, even though s3fs is No Multipart uploading mode."); + return -EBADF; + } + + int result; + std::string tmppath = path; + headers_t tmporgmeta = orgmeta; + + // If there is no loading all of the area, loading all area. + off_t restsize = pagelist.GetTotalUnloadedPageSize(); + if(0 < restsize){ + // check disk space + if(!ReserveDiskSpace(restsize)){ + // no enough disk space + S3FS_PRN_WARN("Not enough local storage to flush: [path=%s][pseudo_fd=%d][physical_fd=%d]", path.c_str(), pseudo_obj->GetPseudoFd(), physical_fd); + return -ENOSPC; // No space left on device + } + } + FdManager::FreeReservedDiskSpace(restsize); + + // Always load all uninitialized area + if(0 != (result = Load(/*start=*/ 0, /*size=*/ 0, AutoLock::ALREADY_LOCKED))){ + S3FS_PRN_ERR("failed to upload all area(errno=%d)", result); + return result; + } + + // check size + if(pagelist.Size() > MAX_MULTIPART_CNT * S3fsCurl::GetMultipartSize()){ + S3FS_PRN_ERR("Part count exceeds %d. Increase multipart size and try again.", MAX_MULTIPART_CNT); + return -EFBIG; + } + + // backup upload file size + struct stat st; + memset(&st, 0, sizeof(struct stat)); + if(-1 == fstat(physical_fd, &st)){ + S3FS_PRN_ERR("fstat is failed by errno(%d), but continue...", errno); + } + + S3fsCurl s3fscurl(true); + result = s3fscurl.PutRequest(tpath ? tpath : tmppath.c_str(), tmporgmeta, physical_fd); + + // reset uploaded file size + size_orgmeta = st.st_size; + + untreated_list.ClearAll(); + + if(0 == result){ + pagelist.ClearAllModified(); + } + + return result; +} + +// [NOTE] +// Both fdent_lock and fdent_data_lock must be locked before calling. +// +int FdEntity::RowFlushMultipart(PseudoFdInfo* pseudo_obj, const char* tpath) +{ + S3FS_PRN_INFO3("[tpath=%s][path=%s][pseudo_fd=%d][physical_fd=%d]", SAFESTRPTR(tpath), path.c_str(), (pseudo_obj ? pseudo_obj->GetPseudoFd() : -1), physical_fd); + + if(-1 == physical_fd || !pseudo_obj){ + return -EBADF; + } + + int result = 0; + + if(!pseudo_obj->IsUploading()){ + // Start uploading + + // If there is no loading all of the area, loading all area. + off_t restsize = pagelist.GetTotalUnloadedPageSize(); + + // Check rest size and free disk space + if(0 < restsize && !ReserveDiskSpace(restsize)){ + // no enough disk space + if(0 != (result = NoCachePreMultipartPost(pseudo_obj))){ + S3FS_PRN_ERR("failed to switch multipart uploading with no cache(errno=%d)", result); + return result; + } + // upload all by multipart uploading + if(0 != (result = NoCacheLoadAndPost(pseudo_obj))){ + S3FS_PRN_ERR("failed to upload all area by multipart uploading(errno=%d)", result); + return result; + } + + }else{ + // enough disk space or no rest size + std::string tmppath = path; + headers_t tmporgmeta = orgmeta; + + FdManager::FreeReservedDiskSpace(restsize); + + // Load all uninitialized area(no mix multipart uploading) + if(0 != (result = Load(/*start=*/ 0, /*size=*/ 0, AutoLock::ALREADY_LOCKED))){ + S3FS_PRN_ERR("failed to upload all area(errno=%d)", result); + return result; + } + + // backup upload file size + struct stat st; + memset(&st, 0, sizeof(struct stat)); + if(-1 == fstat(physical_fd, &st)){ + S3FS_PRN_ERR("fstat is failed by errno(%d), but continue...", errno); + } + + if(pagelist.Size() > MAX_MULTIPART_CNT * S3fsCurl::GetMultipartSize()){ + S3FS_PRN_ERR("Part count exceeds %d. Increase multipart size and try again.", MAX_MULTIPART_CNT); + return -EFBIG; + + }else if(pagelist.Size() >= S3fsCurl::GetMultipartSize()){ + // multipart uploading + result = S3fsCurl::ParallelMultipartUploadRequest(tpath ? tpath : tmppath.c_str(), tmporgmeta, physical_fd); + + }else{ + // normal uploading (too small part size) + S3fsCurl s3fscurl(true); + result = s3fscurl.PutRequest(tpath ? tpath : tmppath.c_str(), tmporgmeta, physical_fd); + } + + // reset uploaded file size + size_orgmeta = st.st_size; + } + untreated_list.ClearAll(); + + }else{ + // Already start uploading + + // upload rest data + off_t untreated_start = 0; + off_t untreated_size = 0; + if(untreated_list.GetLastUpdatedPart(untreated_start, untreated_size, S3fsCurl::GetMultipartSize(), 0) && 0 < untreated_size){ + if(0 != (result = NoCacheMultipartPost(pseudo_obj, physical_fd, untreated_start, untreated_size))){ + S3FS_PRN_ERR("failed to multipart post(start=%lld, size=%lld) for file(physical_fd=%d).", static_cast(untreated_start), static_cast(untreated_size), physical_fd); + return result; + } + untreated_list.ClearParts(untreated_start, untreated_size); + } + // complete multipart uploading. + if(0 != (result = NoCacheCompleteMultipartPost(pseudo_obj))){ + S3FS_PRN_ERR("failed to complete(finish) multipart post for file(physical_fd=%d).", physical_fd); + return result; + } + // truncate file to zero + if(-1 == ftruncate(physical_fd, 0)){ + // So the file has already been removed, skip error. + S3FS_PRN_ERR("failed to truncate file(physical_fd=%d) to zero, but continue...", physical_fd); + } + // put pending headers or create new file + if(0 != (result = UploadPending(-1, AutoLock::ALREADY_LOCKED))){ + return result; + } + } + + if(0 == result){ + pagelist.ClearAllModified(); + pending_status = pending_status_t::NO_UPDATE_PENDING; + } + return result; +} + +// [NOTE] +// Both fdent_lock and fdent_data_lock must be locked before calling. +// +int FdEntity::RowFlushMixMultipart(PseudoFdInfo* pseudo_obj, const char* tpath) +{ + S3FS_PRN_INFO3("[tpath=%s][path=%s][pseudo_fd=%d][physical_fd=%d]", SAFESTRPTR(tpath), path.c_str(), (pseudo_obj ? pseudo_obj->GetPseudoFd() : -1), physical_fd); + + if(-1 == physical_fd || !pseudo_obj){ + return -EBADF; + } + + int result = 0; + + if(!pseudo_obj->IsUploading()){ + // Start uploading + + // If there is no loading all of the area, loading all area. + off_t restsize = pagelist.GetTotalUnloadedPageSize(/* start */ 0, /* size = all */ 0, MIN_MULTIPART_SIZE); + + // Check rest size and free disk space + if(0 < restsize && !ReserveDiskSpace(restsize)){ + // no enough disk space + if(0 != (result = NoCachePreMultipartPost(pseudo_obj))){ + S3FS_PRN_ERR("failed to switch multipart uploading with no cache(errno=%d)", result); + return result; + } + // upload all by multipart uploading + if(0 != (result = NoCacheLoadAndPost(pseudo_obj))){ + S3FS_PRN_ERR("failed to upload all area by multipart uploading(errno=%d)", result); + return result; + } + + }else{ + // enough disk space or no rest size + std::string tmppath = path; + headers_t tmporgmeta = orgmeta; + + FdManager::FreeReservedDiskSpace(restsize); + + // backup upload file size + struct stat st; + memset(&st, 0, sizeof(struct stat)); + if(-1 == fstat(physical_fd, &st)){ + S3FS_PRN_ERR("fstat is failed by errno(%d), but continue...", errno); + } + + if(pagelist.Size() > MAX_MULTIPART_CNT * S3fsCurl::GetMultipartSize()){ + S3FS_PRN_ERR("Part count exceeds %d. Increase multipart size and try again.", MAX_MULTIPART_CNT); + return -EFBIG; + + }else if(pagelist.Size() >= S3fsCurl::GetMultipartSize()){ + // mix multipart uploading + + // This is to ensure that each part is 5MB or more. + // If the part is less than 5MB, download it. + fdpage_list_t dlpages; + fdpage_list_t mixuppages; + if(!pagelist.GetPageListsForMultipartUpload(dlpages, mixuppages, S3fsCurl::GetMultipartSize())){ + S3FS_PRN_ERR("something error occurred during getting download pagelist."); + return -1; + } + + // [TODO] should use parallel downloading + // + for(fdpage_list_t::const_iterator iter = dlpages.begin(); iter != dlpages.end(); ++iter){ + if(0 != (result = Load(iter->offset, iter->bytes, AutoLock::ALREADY_LOCKED, /*is_modified_flag=*/ true))){ // set loaded and modified flag + S3FS_PRN_ERR("failed to get parts(start=%lld, size=%lld) before uploading.", static_cast(iter->offset), static_cast(iter->bytes)); + return result; + } + } + + // multipart uploading with copy api + result = S3fsCurl::ParallelMixMultipartUploadRequest(tpath ? tpath : tmppath.c_str(), tmporgmeta, physical_fd, mixuppages); + + }else{ + // normal uploading (too small part size) + + // If there are unloaded pages, they are loaded at here. + if(0 != (result = Load(/*start=*/ 0, /*size=*/ 0, AutoLock::ALREADY_LOCKED))){ + S3FS_PRN_ERR("failed to load parts before uploading object(%d)", result); + return result; + } + + S3fsCurl s3fscurl(true); + result = s3fscurl.PutRequest(tpath ? tpath : tmppath.c_str(), tmporgmeta, physical_fd); + } + + // reset uploaded file size + size_orgmeta = st.st_size; + } + untreated_list.ClearAll(); + + }else{ + // Already start uploading + + // upload rest data + off_t untreated_start = 0; + off_t untreated_size = 0; + if(untreated_list.GetLastUpdatedPart(untreated_start, untreated_size, S3fsCurl::GetMultipartSize(), 0) && 0 < untreated_size){ + if(0 != (result = NoCacheMultipartPost(pseudo_obj, physical_fd, untreated_start, untreated_size))){ + S3FS_PRN_ERR("failed to multipart post(start=%lld, size=%lld) for file(physical_fd=%d).", static_cast(untreated_start), static_cast(untreated_size), physical_fd); + return result; + } + untreated_list.ClearParts(untreated_start, untreated_size); + } + // complete multipart uploading. + if(0 != (result = NoCacheCompleteMultipartPost(pseudo_obj))){ + S3FS_PRN_ERR("failed to complete(finish) multipart post for file(physical_fd=%d).", physical_fd); + return result; + } + // truncate file to zero + if(-1 == ftruncate(physical_fd, 0)){ + // So the file has already been removed, skip error. + S3FS_PRN_ERR("failed to truncate file(physical_fd=%d) to zero, but continue...", physical_fd); + } + // put pending headers or create new file + if(0 != (result = UploadPending(-1, AutoLock::ALREADY_LOCKED))){ + return result; + } + } + + if(0 == result){ + pagelist.ClearAllModified(); + pending_status = pending_status_t::NO_UPDATE_PENDING; + } + return result; +} + +// [NOTE] +// Both fdent_lock and fdent_data_lock must be locked before calling. +// +int FdEntity::RowFlushStreamMultipart(PseudoFdInfo* pseudo_obj, const char* tpath) +{ + S3FS_PRN_INFO3("[tpath=%s][path=%s][pseudo_fd=%d][physical_fd=%d][mix_upload=%s]", SAFESTRPTR(tpath), path.c_str(), (pseudo_obj ? pseudo_obj->GetPseudoFd() : -1), physical_fd, (FdEntity::mixmultipart ? "true" : "false")); + + if(-1 == physical_fd || !pseudo_obj){ + return -EBADF; + } + int result = 0; + + if(pagelist.Size() <= S3fsCurl::GetMultipartSize()){ + // + // Use normal upload instead of multipart upload(too small part size) + // + + // backup upload file size + struct stat st; + memset(&st, 0, sizeof(struct stat)); + if(-1 == fstat(physical_fd, &st)){ + S3FS_PRN_ERR("fstat is failed by errno(%d), but continue...", errno); + } + + // If there are unloaded pages, they are loaded at here. + if(0 != (result = Load(/*start=*/ 0, /*size=*/ 0, AutoLock::ALREADY_LOCKED))){ + S3FS_PRN_ERR("failed to load parts before uploading object(%d)", result); + return result; + } + + headers_t tmporgmeta = orgmeta; + S3fsCurl s3fscurl(true); + result = s3fscurl.PutRequest(path.c_str(), tmporgmeta, physical_fd); + + // reset uploaded file size + size_orgmeta = st.st_size; + + untreated_list.ClearAll(); + + if(0 == result){ + pagelist.ClearAllModified(); + } + + }else{ + // + // Make upload/download/copy/cancel lists from file + // + mp_part_list_t to_upload_list; + mp_part_list_t to_copy_list; + mp_part_list_t to_download_list; + filepart_list_t cancel_uploaded_list; + bool wait_upload_complete = false; + if(!pseudo_obj->ExtractUploadPartsFromAllArea(untreated_list, to_upload_list, to_copy_list, to_download_list, cancel_uploaded_list, wait_upload_complete, S3fsCurl::GetMultipartSize(), pagelist.Size(), FdEntity::mixmultipart)){ + S3FS_PRN_ERR("Failed to extract various upload parts list from all area: errno(EIO)"); + return -EIO; + } + + // + // Check total size for downloading and Download + // + off_t total_download_size = total_mp_part_list(to_download_list); + if(0 < total_download_size){ + // + // Check if there is enough free disk space for the total download size + // + if(!ReserveDiskSpace(total_download_size)){ + // no enough disk space + // + // [NOTE] + // Because there is no left space size to download, we can't solve this anymore + // in this case which is uploading in sequence. + // + S3FS_PRN_WARN("Not enough local storage(%lld byte) to cache write request for whole of the file: [path=%s][physical_fd=%d]", static_cast(total_download_size), path.c_str(), physical_fd); + return -ENOSPC; // No space left on device + } + // enough disk space + + // + // Download all parts + // + // [TODO] + // Execute in parallel downloading with multiple thread. + // + for(mp_part_list_t::const_iterator download_iter = to_download_list.begin(); download_iter != to_download_list.end(); ++download_iter){ + if(0 != (result = Load(download_iter->start, download_iter->size, AutoLock::ALREADY_LOCKED))){ + break; + } + } + FdManager::FreeReservedDiskSpace(total_download_size); + if(0 != result){ + S3FS_PRN_ERR("failed to load uninitialized area before writing(errno=%d)", result); + return result; + } + } + + // + // Has multipart uploading already started? + // + if(!pseudo_obj->IsUploading()){ + // + // Multipart uploading hasn't started yet, so start it. + // + S3fsCurl s3fscurl(true); + std::string upload_id; + if(0 != (result = s3fscurl.PreMultipartPostRequest(path.c_str(), orgmeta, upload_id, true))){ + S3FS_PRN_ERR("failed to setup multipart upload(create upload id) by errno(%d)", result); + return result; + } + if(!pseudo_obj->InitialUploadInfo(upload_id)){ + S3FS_PRN_ERR("failed to setup multipart upload(set upload id to object)"); + return -EIO; + } + + // Clear the dirty flag, because the meta data is updated. + pending_status = pending_status_t::NO_UPDATE_PENDING; + } + + // + // Output debug level information + // + // When canceling(overwriting) a part that has already been uploaded, output it. + // + if(S3fsLog::IsS3fsLogDbg()){ + for(filepart_list_t::const_iterator cancel_iter = cancel_uploaded_list.begin(); cancel_iter != cancel_uploaded_list.end(); ++cancel_iter){ + S3FS_PRN_DBG("Cancel uploaded: start(%lld), size(%lld), part number(%d)", static_cast(cancel_iter->startpos), static_cast(cancel_iter->size), (cancel_iter->petag ? cancel_iter->petag->part_num : -1)); + } + } + + // [NOTE] + // If there is a part where has already been uploading, that part + // is re-updated after finishing uploading, so the part of the last + // uploded must be canceled. + // (These are cancel_uploaded_list, cancellation processing means + // re-uploading the same area.) + // + // In rare cases, the completion of the previous upload and the + // re-upload may be reversed, causing the ETag to be reversed, + // in which case the upload will fail. + // To prevent this, if the upload of the same area as the re-upload + // is incomplete, we must wait for it to complete here. + // + if(wait_upload_complete){ + if(0 != (result = pseudo_obj->WaitAllThreadsExit())){ + S3FS_PRN_ERR("Some cancel area uploads that were waiting to complete failed with %d.", result); + return result; + } + } + + // + // Upload multipart and copy parts and wait exiting them + // + if(!pseudo_obj->ParallelMultipartUploadAll(path.c_str(), to_upload_list, to_copy_list, result)){ + S3FS_PRN_ERR("Failed to upload multipart parts."); + untreated_list.ClearAll(); + pseudo_obj->ClearUploadInfo(); // clear multipart upload info + return -EIO; + } + if(0 != result){ + S3FS_PRN_ERR("An error(%d) occurred in some threads that were uploading parallel multiparts, but continue to clean up..", result); + untreated_list.ClearAll(); + pseudo_obj->ClearUploadInfo(); // clear multipart upload info + return result; + } + + // + // Complete uploading + // + std::string upload_id; + etaglist_t etaglist; + if(!pseudo_obj->GetUploadId(upload_id) || !pseudo_obj->GetEtaglist(etaglist)){ + S3FS_PRN_ERR("There is no upload id or etag list."); + untreated_list.ClearAll(); + pseudo_obj->ClearUploadInfo(); // clear multipart upload info + return -EIO; + }else{ + S3fsCurl s3fscurl(true); + result = s3fscurl.CompleteMultipartPostRequest(path.c_str(), upload_id, etaglist); + s3fscurl.DestroyCurlHandle(); + if(0 != result){ + S3FS_PRN_ERR("failed to complete multipart upload by errno(%d)", result); + untreated_list.ClearAll(); + pseudo_obj->ClearUploadInfo(); // clear multipart upload info + + S3fsCurl s3fscurl_abort(true); + int result2 = s3fscurl.AbortMultipartUpload(path.c_str(), upload_id); + s3fscurl_abort.DestroyCurlHandle(); + if(0 != result2){ + S3FS_PRN_ERR("failed to abort multipart upload by errno(%d)", result2); + } + return result; + } + } + untreated_list.ClearAll(); + pseudo_obj->ClearUploadInfo(); // clear multipart upload info + + // put pending headers or create new file + if(0 != (result = UploadPending(-1, AutoLock::ALREADY_LOCKED))){ + return result; + } + } + untreated_list.ClearAll(); + + if(0 == result){ + pagelist.ClearAllModified(); + } + + return result; +} + +// [NOTICE] +// Need to lock before calling this method. +bool FdEntity::ReserveDiskSpace(off_t size) +{ + if(FdManager::ReserveDiskSpace(size)){ + return true; + } + + if(!pagelist.IsModified()){ + // try to clear all cache for this fd. + pagelist.Init(pagelist.Size(), false, false); + if(-1 == ftruncate(physical_fd, 0) || -1 == ftruncate(physical_fd, pagelist.Size())){ + S3FS_PRN_ERR("failed to truncate temporary file(physical_fd=%d).", physical_fd); + return false; + } + + if(FdManager::ReserveDiskSpace(size)){ + return true; + } + } + + FdManager::get()->CleanupCacheDir(); + + return FdManager::ReserveDiskSpace(size); +} + +ssize_t FdEntity::Read(int fd, char* bytes, off_t start, size_t size, bool force_load) +{ + S3FS_PRN_DBG("[path=%s][pseudo_fd=%d][physical_fd=%d][offset=%lld][size=%zu]", path.c_str(), fd, physical_fd, static_cast(start), size); + + if(-1 == physical_fd || nullptr == CheckPseudoFdFlags(fd, false)){ + S3FS_PRN_DBG("pseudo_fd(%d) to physical_fd(%d) for path(%s) is not opened or not readable", fd, physical_fd, path.c_str()); + return -EBADF; + } + + if(use_newcache){ + size_t realSize = GetRealsize(); + if (start >= realSize) return 0; + size_t realReadSize = size; + if (start + size > realSize) { + realReadSize = realSize - start; + } + + int res = accessor->Get(path, start, realReadSize, bytes); + if (!res) { + return realReadSize; + } + return res; + } + + AutoLock auto_lock(&fdent_lock); + AutoLock auto_lock2(&fdent_data_lock); + + if(force_load){ + pagelist.SetPageLoadedStatus(start, size, PageList::page_status::NOT_LOAD_MODIFIED); + } + + ssize_t rsize; + + // check disk space + if(0 < pagelist.GetTotalUnloadedPageSize(start, size)){ + // load size(for prefetch) + size_t load_size = size; + if(start + static_cast(size) < pagelist.Size()){ + ssize_t prefetch_max_size = std::max(static_cast(size), S3fsCurl::GetMultipartSize() * S3fsCurl::GetMaxParallelCount()); + + if(start + prefetch_max_size < pagelist.Size()){ + load_size = prefetch_max_size; + }else{ + load_size = pagelist.Size() - start; + } + } + + if(!ReserveDiskSpace(load_size)){ + S3FS_PRN_WARN("could not reserve disk space for pre-fetch download"); + load_size = size; + if(!ReserveDiskSpace(load_size)){ + S3FS_PRN_ERR("could not reserve disk space for pre-fetch download"); + return -ENOSPC; + } + } + + // Loading + int result = 0; + if(0 < size){ + result = Load(start, load_size, AutoLock::ALREADY_LOCKED); + } + + FdManager::FreeReservedDiskSpace(load_size); + + if(0 != result){ + S3FS_PRN_ERR("could not download. start(%lld), size(%zu), errno(%d)", static_cast(start), size, result); + return result; + } + } + + // Reading + if(-1 == (rsize = pread(physical_fd, bytes, size, start))){ + S3FS_PRN_ERR("pread failed. errno(%d)", errno); + return -errno; + } + return rsize; +} + +ssize_t FdEntity::ReadByAdaptor(int fd, char* bytes, off_t start, size_t size, bool force_load, std::shared_ptr dataAdaptor) +{ + S3FS_PRN_DBG("[path=%s][pseudo_fd=%d][physical_fd=%d][offset=%lld][size=%zu]", path.c_str(), fd, physical_fd, static_cast(start), size); + + ssize_t rsize; + { + // AutoLock auto_lock(&fdent_lock); // TODO: If download occurs during flush, the fdent_lock will conflict + AutoLock auto_lock2(&fdent_data_lock); + + if(force_load){ + pagelist.SetPageLoadedStatus(start, size, PageList::page_status::NOT_LOAD_MODIFIED); + } + + // check disk space + if(0 < pagelist.GetTotalUnloadedPageSize(start, size)){ + // load size(for prefetch) + size_t load_size = size; + if(start + static_cast(size) < pagelist.Size()){ + ssize_t prefetch_max_size = std::max(static_cast(size), S3fsCurl::GetMultipartSize() * S3fsCurl::GetMaxParallelCount()); + + if(start + prefetch_max_size < pagelist.Size()){ + load_size = prefetch_max_size; + }else{ + load_size = pagelist.Size() - start; + } + } + + if(!ReserveDiskSpace(load_size)){ + S3FS_PRN_WARN("could not reserve disk space for pre-fetch download"); + load_size = size; + if(!ReserveDiskSpace(load_size)){ + S3FS_PRN_ERR("could not reserve disk space for pre-fetch download"); + return -ENOSPC; + } + } + + // Loading + int result = 0; + if(0 < size){ + result = LoadByAdaptor(start, load_size, AutoLock::ALREADY_LOCKED, dataAdaptor); + } + + FdManager::FreeReservedDiskSpace(load_size); + + if(0 != result){ + S3FS_PRN_ERR("could not download. start(%lld), size(%zu), errno(%d)", static_cast(start), size, result); + return result; + } + } + + // read/write/disk release may be concurrent + int lock_res = flock_set(physical_fd, F_RDLCK); + if(lock_res < 0){ + S3FS_PRN_ERR("cache file read lock failed. path(%s), physical_fd(%d)", path.c_str(), physical_fd); + return lock_res; + } + } + + // Reading + if(-1 == (rsize = pread(physical_fd, bytes, size, start))){ + flock_set(physical_fd, F_UNLCK); + S3FS_PRN_ERR("pread failed. errno(%d)", errno); + return -errno; + } + + flock_set(physical_fd, F_UNLCK); + return rsize; +} + +ssize_t FdEntity::Write(int fd, const char* bytes, off_t start, size_t size, bool force_tmpfile) +{ + S3FS_PRN_WARN("[path=%s][pseudo_fd=%d][physical_fd=%d][offset=%lld][size=%zu]", path.c_str(), fd, physical_fd, static_cast(start), size); + + PseudoFdInfo* pseudo_obj = nullptr; + if(-1 == physical_fd || nullptr == (pseudo_obj = CheckPseudoFdFlags(fd, false))){ + S3FS_PRN_ERR("pseudo_fd(%d) to physical_fd(%d) for path(%s) is not opened or not writable", fd, physical_fd, path.c_str()); + return -EBADF; + } + + if(use_newcache && !force_tmpfile){ + int res = accessor->Put(path, start, size, bytes); + if (!res) { + return size; + } + return res; + } + + // check if not enough disk space left BEFORE locking fd + if(FdManager::IsCacheDir() && !FdManager::IsSafeDiskSpace(nullptr, size)){ + FdManager::get()->CleanupCacheDir(); + } + AutoLock auto_lock(&fdent_lock); + AutoLock auto_lock2(&fdent_data_lock); + + // check file size + if(pagelist.Size() < start){ + // grow file size + if(-1 == ftruncate(physical_fd, start)){ + S3FS_PRN_ERR("failed to truncate temporary file(physical_fd=%d).", physical_fd); + return -errno; + } + // set untreated area + if(!AddUntreated(pagelist.Size(), (start - pagelist.Size()))){ + S3FS_PRN_ERR("failed to set untreated area by incremental."); + return -EIO; + } + + // add new area + pagelist.SetPageLoadedStatus(pagelist.Size(), start - pagelist.Size(), PageList::page_status::MODIFIED); + } + + ssize_t wsize; + if(nomultipart){ + // No multipart upload + wsize = WriteNoMultipart(pseudo_obj, bytes, start, size); + }else if(FdEntity::streamupload){ + // Stream upload + wsize = WriteStreamUpload(pseudo_obj, bytes, start, size); + }else if(FdEntity::mixmultipart){ + // Mix multipart upload + wsize = WriteMixMultipart(pseudo_obj, bytes, start, size); + }else{ + // Normal multipart upload + wsize = WriteMultipart(pseudo_obj, bytes, start, size); + } + + return wsize; +} + +// [NOTE] +// Both fdent_lock and fdent_data_lock must be locked before calling. +// +ssize_t FdEntity::WriteNoMultipart(const PseudoFdInfo* pseudo_obj, const char* bytes, off_t start, size_t size) +{ + S3FS_PRN_DBG("[path=%s][pseudo_fd=%d][physical_fd=%d][offset=%lld][size=%zu]", path.c_str(), (pseudo_obj ? pseudo_obj->GetPseudoFd() : -1), physical_fd, static_cast(start), size); + + if(-1 == physical_fd || !pseudo_obj){ + S3FS_PRN_ERR("pseudo_fd(%d) to physical_fd(%d) for path(%s) is not opened or not writable", (pseudo_obj ? pseudo_obj->GetPseudoFd() : -1), physical_fd, path.c_str()); + return -EBADF; + } + + int result = 0; + + if(pseudo_obj->IsUploading()){ + S3FS_PRN_ERR("Why uploading now, even though s3fs is No Multipart uploading mode."); + return -EBADF; + } + + // check disk space + off_t restsize = pagelist.GetTotalUnloadedPageSize(0, start) + size; + if(!ReserveDiskSpace(restsize)){ + // no enough disk space + S3FS_PRN_WARN("Not enough local storage to cache write request: [path=%s][physical_fd=%d][offset=%lld][size=%zu]", path.c_str(), physical_fd, static_cast(start), size); + return -ENOSPC; // No space left on device + } + + // Load uninitialized area which starts from 0 to (start + size) before writing. + if(0 < start){ + result = Load(0, start, AutoLock::ALREADY_LOCKED); + } + + FdManager::FreeReservedDiskSpace(restsize); + if(0 != result){ + S3FS_PRN_ERR("failed to load uninitialized area before writing(errno=%d)", result); + return result; + } + + // Writing + ssize_t wsize; + if(-1 == (wsize = pwrite(physical_fd, bytes, size, start))){ + S3FS_PRN_ERR("pwrite failed. errno(%d)", errno); + return -errno; + } + if(0 < wsize){ + pagelist.SetPageLoadedStatus(start, wsize, PageList::page_status::LOAD_MODIFIED); + AddUntreated(start, wsize); + } + + // Load uninitialized area which starts from (start + size) to EOF after writing. + if(pagelist.Size() > start + static_cast(size)){ + result = Load(start + size, pagelist.Size(), AutoLock::ALREADY_LOCKED); + if(0 != result){ + S3FS_PRN_ERR("failed to load uninitialized area after writing(errno=%d)", result); + return result; + } + } + + return wsize; +} + +// [NOTE] +// Both fdent_lock and fdent_data_lock must be locked before calling. +// +ssize_t FdEntity::WriteMultipart(PseudoFdInfo* pseudo_obj, const char* bytes, off_t start, size_t size) +{ + S3FS_PRN_DBG("[path=%s][pseudo_fd=%d][physical_fd=%d][offset=%lld][size=%zu]", path.c_str(), (pseudo_obj ? pseudo_obj->GetPseudoFd() : -1), physical_fd, static_cast(start), size); + + if(-1 == physical_fd || !pseudo_obj){ + S3FS_PRN_ERR("pseudo_fd(%d) to physical_fd(%d) for path(%s) is not opened or not writable", (pseudo_obj ? pseudo_obj->GetPseudoFd() : -1), physical_fd, path.c_str()); + return -EBADF; + } + + int result = 0; + + if(!pseudo_obj->IsUploading()){ + // check disk space + off_t restsize = pagelist.GetTotalUnloadedPageSize(0, start) + size; + if(ReserveDiskSpace(restsize)){ + // enough disk space + + // Load uninitialized area which starts from 0 to (start + size) before writing. + if(0 < start){ + result = Load(0, start, AutoLock::ALREADY_LOCKED); + } + + FdManager::FreeReservedDiskSpace(restsize); + if(0 != result){ + S3FS_PRN_ERR("failed to load uninitialized area before writing(errno=%d)", result); + return result; + } + }else{ + // no enough disk space + if((start + static_cast(size)) <= S3fsCurl::GetMultipartSize()){ + S3FS_PRN_WARN("Not enough local storage to cache write request till multipart upload can start: [path=%s][physical_fd=%d][offset=%lld][size=%zu]", path.c_str(), physical_fd, static_cast(start), size); + return -ENOSPC; // No space left on device + } + if(0 != (result = NoCachePreMultipartPost(pseudo_obj))){ + S3FS_PRN_ERR("failed to switch multipart uploading with no cache(errno=%d)", result); + return result; + } + // start multipart uploading + if(0 != (result = NoCacheLoadAndPost(pseudo_obj, 0, start))){ + S3FS_PRN_ERR("failed to load uninitialized area and multipart uploading it(errno=%d)", result); + return result; + } + untreated_list.ClearAll(); + } + }else{ + // already start multipart uploading + } + + // Writing + ssize_t wsize; + if(-1 == (wsize = pwrite(physical_fd, bytes, size, start))){ + S3FS_PRN_ERR("pwrite failed. errno(%d)", errno); + return -errno; + } + if(0 < wsize){ + pagelist.SetPageLoadedStatus(start, wsize, PageList::page_status::LOAD_MODIFIED); + AddUntreated(start, wsize); + } + + // Load uninitialized area which starts from (start + size) to EOF after writing. + if(pagelist.Size() > start + static_cast(size)){ + result = Load(start + size, pagelist.Size(), AutoLock::ALREADY_LOCKED); + if(0 != result){ + S3FS_PRN_ERR("failed to load uninitialized area after writing(errno=%d)", result); + return result; + } + } + + // check multipart uploading + if(pseudo_obj->IsUploading()){ + // get last untreated part(maximum size is multipart size) + off_t untreated_start = 0; + off_t untreated_size = 0; + if(untreated_list.GetLastUpdatedPart(untreated_start, untreated_size, S3fsCurl::GetMultipartSize())){ + // when multipart max size is reached + if(0 != (result = NoCacheMultipartPost(pseudo_obj, physical_fd, untreated_start, untreated_size))){ + S3FS_PRN_ERR("failed to multipart post(start=%lld, size=%lld) for file(physical_fd=%d).", static_cast(untreated_start), static_cast(untreated_size), physical_fd); + return result; + } + + // [NOTE] + // truncate file to zero and set length to part offset + size + // after this, file length is (offset + size), but file does not use any disk space. + // + if(-1 == ftruncate(physical_fd, 0) || -1 == ftruncate(physical_fd, (untreated_start + untreated_size))){ + S3FS_PRN_ERR("failed to truncate file(physical_fd=%d).", physical_fd); + return -errno; + } + untreated_list.ClearParts(untreated_start, untreated_size); + } + } + return wsize; +} + +// [NOTE] +// Both fdent_lock and fdent_data_lock must be locked before calling. +// +ssize_t FdEntity::WriteMixMultipart(PseudoFdInfo* pseudo_obj, const char* bytes, off_t start, size_t size) +{ + S3FS_PRN_DBG("[path=%s][pseudo_fd=%d][physical_fd=%d][offset=%lld][size=%zu]", path.c_str(), (pseudo_obj ? pseudo_obj->GetPseudoFd() : -1), physical_fd, static_cast(start), size); + + if(-1 == physical_fd || !pseudo_obj){ + S3FS_PRN_ERR("pseudo_fd(%d) to physical_fd(%d) for path(%s) is not opened or not writable", (pseudo_obj ? pseudo_obj->GetPseudoFd() : -1), physical_fd, path.c_str()); + return -EBADF; + } + + int result; + + if(!pseudo_obj->IsUploading()){ + // check disk space + off_t restsize = pagelist.GetTotalUnloadedPageSize(0, start, MIN_MULTIPART_SIZE) + size; + if(ReserveDiskSpace(restsize)){ + // enough disk space + FdManager::FreeReservedDiskSpace(restsize); + }else{ + // no enough disk space + if((start + static_cast(size)) <= S3fsCurl::GetMultipartSize()){ + S3FS_PRN_WARN("Not enough local storage to cache write request till multipart upload can start: [path=%s][physical_fd=%d][offset=%lld][size=%zu]", path.c_str(), physical_fd, static_cast(start), size); + return -ENOSPC; // No space left on device + } + if(0 != (result = NoCachePreMultipartPost(pseudo_obj))){ + S3FS_PRN_ERR("failed to switch multipart uploading with no cache(errno=%d)", result); + return result; + } + // start multipart uploading + if(0 != (result = NoCacheLoadAndPost(pseudo_obj, 0, start))){ + S3FS_PRN_ERR("failed to load uninitialized area and multipart uploading it(errno=%d)", result); + return result; + } + untreated_list.ClearAll(); + } + }else{ + // already start multipart uploading + } + + // Writing + ssize_t wsize; + if(-1 == (wsize = pwrite(physical_fd, bytes, size, start))){ + S3FS_PRN_ERR("pwrite failed. errno(%d)", errno); + return -errno; + } + if(0 < wsize){ + pagelist.SetPageLoadedStatus(start, wsize, PageList::page_status::LOAD_MODIFIED); + AddUntreated(start, wsize); + } + + // check multipart uploading + if(pseudo_obj->IsUploading()){ + // get last untreated part(maximum size is multipart size) + off_t untreated_start = 0; + off_t untreated_size = 0; + if(untreated_list.GetLastUpdatedPart(untreated_start, untreated_size, S3fsCurl::GetMultipartSize())){ + // when multipart max size is reached + if(0 != (result = NoCacheMultipartPost(pseudo_obj, physical_fd, untreated_start, untreated_size))){ + S3FS_PRN_ERR("failed to multipart post(start=%lld, size=%lld) for file(physical_fd=%d).", static_cast(untreated_start), static_cast(untreated_size), physical_fd); + return result; + } + + // [NOTE] + // truncate file to zero and set length to part offset + size + // after this, file length is (offset + size), but file does not use any disk space. + // + if(-1 == ftruncate(physical_fd, 0) || -1 == ftruncate(physical_fd, (untreated_start + untreated_size))){ + S3FS_PRN_ERR("failed to truncate file(physical_fd=%d).", physical_fd); + return -errno; + } + untreated_list.ClearParts(untreated_start, untreated_size); + } + } + return wsize; +} + +// +// On Stream upload, the uploading is executed in another thread when the +// written area exceeds the maximum size of multipart upload. +// +// [NOTE] +// Both fdent_lock and fdent_data_lock must be locked before calling. +// +ssize_t FdEntity::WriteStreamUpload(PseudoFdInfo* pseudo_obj, const char* bytes, off_t start, size_t size) +{ + S3FS_PRN_DBG("[path=%s][pseudo_fd=%d][physical_fd=%d][offset=%lld][size=%zu]", path.c_str(), (pseudo_obj ? pseudo_obj->GetPseudoFd() : -1), physical_fd, static_cast(start), size); + + if(-1 == physical_fd || !pseudo_obj){ + S3FS_PRN_ERR("pseudo_fd(%d) to physical_fd(%d) for path(%s) is not opened or not writable", (pseudo_obj ? pseudo_obj->GetPseudoFd() : -1), physical_fd, path.c_str()); + return -EBADF; + } + + // Writing + ssize_t wsize; + if(-1 == (wsize = pwrite(physical_fd, bytes, size, start))){ + S3FS_PRN_ERR("pwrite failed. errno(%d)", errno); + return -errno; + } + if(0 < wsize){ + pagelist.SetPageLoadedStatus(start, wsize, PageList::page_status::LOAD_MODIFIED); + AddUntreated(start, wsize); + } + + // Check and Upload + // + // If the last updated Untreated area exceeds the maximum upload size, + // upload processing is performed. + // + headers_t tmporgmeta = orgmeta; + bool isuploading = pseudo_obj->IsUploading(); + ssize_t result; + if(0 != (result = pseudo_obj->UploadBoundaryLastUntreatedArea(path.c_str(), tmporgmeta, this))){ + S3FS_PRN_ERR("Failed to upload the last untreated parts(area) : result=%zd", result); + return result; + } + + if(!isuploading && pseudo_obj->IsUploading()){ + // Clear the dirty flag, because the meta data is updated. + pending_status = pending_status_t::NO_UPDATE_PENDING; + } + + return wsize; +} + +// [NOTE] +// Returns true if merged to orgmeta. +// If true is returned, the caller can update the header. +// If it is false, do not update the header because multipart upload is in progress. +// In this case, the header is pending internally and is updated after the upload +// is complete(flush file). +// +bool FdEntity::MergeOrgMeta(headers_t& updatemeta) +{ + AutoLock auto_lock(&fdent_lock); + + merge_headers(orgmeta, updatemeta, true); // overwrite all keys + // [NOTE] + // this is special cases, we remove the key which has empty values. + for(headers_t::iterator hiter = orgmeta.begin(); hiter != orgmeta.end(); ){ + if(hiter->second.empty()){ + hiter = orgmeta.erase(hiter); + }else{ + ++hiter; + } + } + updatemeta = orgmeta; + orgmeta.erase("x-amz-copy-source"); + + // update ctime/mtime/atime + struct timespec mtime = get_mtime(updatemeta, false); // not overcheck + struct timespec ctime = get_ctime(updatemeta, false); // not overcheck + struct timespec atime = get_atime(updatemeta, false); // not overcheck + if(0 <= mtime.tv_sec){ + SetMCtime(mtime, (ctime.tv_sec < 0 ? mtime : ctime), AutoLock::ALREADY_LOCKED); + } + if(0 <= atime.tv_sec){ + SetAtime(atime, AutoLock::ALREADY_LOCKED); + } + + AutoLock auto_lock2(&fdent_data_lock); + if(pending_status_t::NO_UPDATE_PENDING == pending_status && (IsUploading(AutoLock::ALREADY_LOCKED) || pagelist.IsModified())){ + pending_status = pending_status_t::UPDATE_META_PENDING; + } + + return (pending_status_t::NO_UPDATE_PENDING != pending_status); +} + +// global function in s3fs.cpp +int put_headers(const char* path, headers_t& meta, bool is_copy, bool use_st_size = true); + +int FdEntity::UploadPending(int fd, AutoLock::Type type) +{ + AutoLock auto_lock(&fdent_lock, type); + int result; + + if(pending_status_t::NO_UPDATE_PENDING == pending_status){ + // nothing to do + result = 0; + + }else if(pending_status_t::UPDATE_META_PENDING == pending_status){ + headers_t updatemeta = orgmeta; + updatemeta["x-amz-copy-source"] = urlEncodePath(service_path + S3fsCred::GetBucket() + get_realpath(path.c_str())); + updatemeta["x-amz-metadata-directive"] = "REPLACE"; + + // put headers, no need to update mtime to avoid dead lock + result = put_headers(path.c_str(), updatemeta, true); + if(0 != result){ + S3FS_PRN_ERR("failed to put header after flushing file(%s) by(%d).", path.c_str(), result); + }else{ + pending_status = pending_status_t::NO_UPDATE_PENDING; + } + + }else{ // CREATE_FILE_PENDING == pending_status + if(-1 == fd){ + S3FS_PRN_ERR("could not create a new file(%s), because fd is not specified.", path.c_str()); + result = -EBADF; + }else{ + result = Flush(fd, AutoLock::ALREADY_LOCKED, true); + if(0 != result){ + S3FS_PRN_ERR("failed to flush for file(%s) by(%d).", path.c_str(), result); + }else{ + pending_status = pending_status_t::NO_UPDATE_PENDING; + } + } + } + return result; +} + +// [NOTE] +// For systems where the fallocate function cannot be detected, use a dummy function. +// ex. OSX +// +#ifndef HAVE_FALLOCATE +static int fallocate(int /*fd*/, int /*mode*/, off_t /*offset*/, off_t /*len*/) +{ + errno = ENOSYS; // This is a bad idea, but the caller can handle it simply. + return -1; +} +#endif // HAVE_FALLOCATE + +// [NOTE] +// If HAVE_FALLOCATE is undefined, or versions prior to 2.6.38(fallocate function exists), +// following flags are undefined. Then we need these symbols defined in fallocate, so we +// define them here. +// The definitions are copied from linux/falloc.h, but if HAVE_FALLOCATE is undefined, +// these values can be anything. +// +#ifndef FALLOC_FL_PUNCH_HOLE +#define FALLOC_FL_PUNCH_HOLE 0x02 /* de-allocates range */ +#endif +#ifndef FALLOC_FL_KEEP_SIZE +#define FALLOC_FL_KEEP_SIZE 0x01 +#endif + +// [NOTE] +// This method punches an area(on cache file) that has no data at the time it is called. +// This is called to prevent the cache file from growing. +// However, this method uses the non-portable(Linux specific) system call fallocate(). +// Also, depending on the file system, FALLOC_FL_PUNCH_HOLE mode may not work and HOLE +// will not open.(Filesystems for which this method works are ext4, btrfs, xfs, etc.) +// +bool FdEntity::PunchHole(off_t start, size_t size) +{ + S3FS_PRN_DBG("[path=%s][physical_fd=%d][offset=%lld][size=%zu]", path.c_str(), physical_fd, static_cast(start), size); + + AutoLock auto_lock(&fdent_lock); + AutoLock auto_lock2(&fdent_data_lock); + + if(-1 == physical_fd){ + return false; + } + + // get page list that have no data + fdpage_list_t nodata_pages; + if(!pagelist.GetNoDataPageLists(nodata_pages)){ + S3FS_PRN_ERR("failed to get page list that have no data."); + return false; + } + if(nodata_pages.empty()){ + S3FS_PRN_DBG("there is no page list that have no data, so nothing to do."); + return true; + } + + // try to punch hole to file + for(fdpage_list_t::const_iterator iter = nodata_pages.begin(); iter != nodata_pages.end(); ++iter){ + if(0 != fallocate(physical_fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, iter->offset, iter->bytes)){ + if(ENOSYS == errno || EOPNOTSUPP == errno){ + S3FS_PRN_ERR("failed to fallocate for punching hole to file with errno(%d), it maybe the fallocate function is not implemented in this kernel, or the file system does not support FALLOC_FL_PUNCH_HOLE.", errno); + }else{ + S3FS_PRN_ERR("failed to fallocate for punching hole to file with errno(%d)", errno); + } + return false; + } + if(!pagelist.SetPageLoadedStatus(iter->offset, iter->bytes, PageList::page_status::NOT_LOAD_MODIFIED)){ + S3FS_PRN_ERR("succeed to punch HOLEs in the cache file, but failed to update the cache stat."); + return false; + } + S3FS_PRN_DBG("made a hole at [%lld - %lld bytes](into a boundary) of the cache file.", static_cast(iter->offset), static_cast(iter->bytes)); + } + return true; +} + +// [NOTE] +// Indicate that a new file's is dirty. +// This ensures that both metadata and data are synced during flush. +// +void FdEntity::MarkDirtyNewFile() +{ + AutoLock auto_lock(&fdent_lock); + AutoLock auto_lock2(&fdent_data_lock); + + pagelist.Init(0, false, true); + pending_status = pending_status_t::CREATE_FILE_PENDING; +} + +bool FdEntity::IsDirtyNewFile() const +{ + AutoLock auto_lock(&fdent_lock); + + return (pending_status_t::CREATE_FILE_PENDING == pending_status); +} + +// [NOTE] +// The fdatasync call only uploads the content but does not update +// the meta data. In the flush call, if there is no update contents, +// need to upload only metadata, so use these functions. +// +void FdEntity::MarkDirtyMetadata() +{ + AutoLock auto_lock(&fdent_lock); + AutoLock auto_lock2(&fdent_data_lock); + + if(pending_status_t::NO_UPDATE_PENDING == pending_status){ + pending_status = pending_status_t::UPDATE_META_PENDING; + } +} + +bool FdEntity::IsDirtyMetadata() const +{ + // [NOTE] + // fdent_lock must be previously locked. + // + return (pending_status_t::UPDATE_META_PENDING == pending_status); +} + +bool FdEntity::AddUntreated(off_t start, off_t size) +{ + bool result = untreated_list.AddPart(start, size); + if(!result){ + S3FS_PRN_DBG("Failed adding untreated area part."); + }else if(S3fsLog::IsS3fsLogDbg()){ + untreated_list.Dump(); + } + + return result; +} + +bool FdEntity::GetLastUpdateUntreatedPart(off_t& start, off_t& size) const +{ + // Get last untreated area + if(!untreated_list.GetLastUpdatePart(start, size)){ + return false; + } + return true; +} + +bool FdEntity::ReplaceLastUpdateUntreatedPart(off_t front_start, off_t front_size, off_t behind_start, off_t behind_size) +{ + if(0 < front_size){ + if(!untreated_list.ReplaceLastUpdatePart(front_start, front_size)){ + return false; + } + }else{ + if(!untreated_list.RemoveLastUpdatePart()){ + return false; + } + } + if(0 < behind_size){ + if(!untreated_list.AddPart(behind_start, behind_size)){ + return false; + } + } + return true; +} + +size_t FdEntity::GetRealsize() const +{ + return static_cast(realsize.load()); +} + +void FdEntity::UpdateRealsize(off_t size) +{ + if (size < 0) return; + while(true){ + size_t curSize = GetRealsize(); + if(curSize >= size) break; + if(realsize.compare_exchange_weak(curSize, size)) break; + } + SetUpdateMark(true); +} + +void FdEntity::TruncateRealsize(off_t size) +{ + while(true){ + size_t curSize = GetRealsize(); + if(realsize.compare_exchange_weak(curSize, size)) break; + } + SetUpdateMark(true); +} + +void FdEntity::SetUpdateMark(bool is_update) +{ + update_mark.store(is_update); +} + +bool FdEntity::GetUpdateMark() const +{ + return update_mark.load(); +} + +const headers_t& FdEntity::GetOriginalHeaders() const +{ + return this->orgmeta; +} + +ssize_t FdEntity::WriteCache(const char* bytes, off_t start, size_t size, AutoLock::Type type) +{ + AutoLock auto_data_lock(&fdent_data_lock, type); + + if(!FdManager::get()->EnsureDiskSpaceUsable(path, size)) { + S3FS_PRN_ERR("disk space not enough. path:%s", path.c_str()); + return 0; + } + + if(flock_set(physical_fd, F_WRLCK) < 0){ + S3FS_PRN_ERR("cache file write lock failed. path(%s), physical_fd(%d)", path.c_str(), physical_fd); + return 0; + } + ssize_t wsize; + if(-1 == (wsize = pwrite(physical_fd, bytes, size, start))){ + flock_set(physical_fd, F_UNLCK); + S3FS_PRN_ERR("pwrite failed. path:%s, errno(%d)", path.c_str(), errno); + return 0; + } + flock_set(physical_fd, F_UNLCK); + + pagelist.SetPageLoadedStatus(start, wsize, PageList::page_status::LOADED); + return wsize; +} + +void FdEntity::ReleaseCache() +{ + AutoLock auto_data_lock(&fdent_data_lock); + pagelist.Init(0, false, false); + + if(flock_set(physical_fd, F_WRLCK) < 0){ + S3FS_PRN_ERR("cache file write lock failed. path(%s), physical_fd(%d)", path.c_str(), physical_fd); + return; + } + + if(-1 == ftruncate(physical_fd, 0)) { + flock_set(physical_fd, F_UNLCK); + S3FS_PRN_ERR("failed to truncate temporary file(physical_fd=%d).", physical_fd); + return; + } + lseek(physical_fd, 0, SEEK_SET); + flock_set(physical_fd, F_UNLCK); +} + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/fdcache_entity.h b/s3fs/fdcache_entity.h new file mode 100644 index 0000000..25b4764 --- /dev/null +++ b/s3fs/fdcache_entity.h @@ -0,0 +1,197 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef S3FS_FDCACHE_ENTITY_H_ +#define S3FS_FDCACHE_ENTITY_H_ + +#include +#include + +#include "autolock.h" +#include "fdcache_page.h" +#include "fdcache_fdinfo.h" +#include "fdcache_untreated.h" +#include "metaheader.h" + +//------------------------------------------------ +// Symbols +//------------------------------------------------ +static constexpr int MAX_MULTIPART_CNT = 10 * 1000; // S3 multipart max count + +//------------------------------------------------ +// class FdEntity +//------------------------------------------------ +class FdEntity +{ + private: + // [NOTE] + // Distinguish between meta pending and new file creation pending, + // because the processing(request) at these updates is different. + // Therefore, the pending state is expressed by this enum type. + // + enum class pending_status_t { + NO_UPDATE_PENDING = 0, + UPDATE_META_PENDING, // pending meta header + CREATE_FILE_PENDING // pending file creation and meta header + }; + + static bool mixmultipart; // whether multipart uploading can use copy api. + static bool streamupload; // whether stream uploading. + + mutable pthread_mutex_t fdent_lock; + bool is_lock_init; + std::string path; // object path + int physical_fd; // physical file(cache or temporary file) descriptor + UntreatedParts untreated_list; // list of untreated parts that have been written and not yet uploaded(for streamupload) + fdinfo_map_t pseudo_fd_map; // pseudo file descriptor information map + FILE* pfile; // file pointer(tmp file or cache file) + ino_t inode; // inode number for cache file + headers_t orgmeta; // original headers at opening + off_t size_orgmeta; // original file size in original headers + + mutable pthread_mutex_t fdent_data_lock;// protects the following members + PageList pagelist; + std::string cachepath; // local cache file path + // (if this is empty, does not load/save pagelist.) + std::string mirrorpath; // mirror file path to local cache file path + pending_status_t pending_status;// status for new file creation and meta update + struct timespec holding_mtime; // if mtime is updated while the file is open, it is set time_t value + + std::atomic realsize{0}; // real file size + std::atomic update_mark{false}; // file update mark + + private: + static int FillFile(int fd, unsigned char byte, off_t size, off_t start); + static ino_t GetInode(int fd); + + void Clear(); + ino_t GetInode() const; + int OpenMirrorFile(); + int NoCacheLoadAndPost(PseudoFdInfo* pseudo_obj, off_t start = 0, off_t size = 0); // size=0 means loading to end + PseudoFdInfo* CheckPseudoFdFlags(int fd, bool writable, AutoLock::Type locktype = AutoLock::NONE); + bool IsUploading(AutoLock::Type locktype = AutoLock::NONE); + bool SetAllStatus(bool is_loaded); // [NOTE] not locking + bool SetAllStatusUnloaded() { return SetAllStatus(false); } + int NoCachePreMultipartPost(PseudoFdInfo* pseudo_obj); + int NoCacheMultipartPost(PseudoFdInfo* pseudo_obj, int tgfd, off_t start, off_t size); + int NoCacheCompleteMultipartPost(PseudoFdInfo* pseudo_obj); + int RowFlushNoMultipart(const PseudoFdInfo* pseudo_obj, const char* tpath); + int RowFlushMultipart(PseudoFdInfo* pseudo_obj, const char* tpath); + int RowFlushMixMultipart(PseudoFdInfo* pseudo_obj, const char* tpath); + int RowFlushStreamMultipart(PseudoFdInfo* pseudo_obj, const char* tpath); + ssize_t WriteNoMultipart(const PseudoFdInfo* pseudo_obj, const char* bytes, off_t start, size_t size); + ssize_t WriteMultipart(PseudoFdInfo* pseudo_obj, const char* bytes, off_t start, size_t size); + ssize_t WriteMixMultipart(PseudoFdInfo* pseudo_obj, const char* bytes, off_t start, size_t size); + ssize_t WriteStreamUpload(PseudoFdInfo* pseudo_obj, const char* bytes, off_t start, size_t size); + + bool ReserveDiskSpace(off_t size); + + bool AddUntreated(off_t start, off_t size); + + bool IsDirtyMetadata() const; + + public: + static bool GetNoMixMultipart() { return mixmultipart; } + static bool SetNoMixMultipart(); + static bool GetStreamUpload() { return streamupload; } + static bool SetStreamUpload(bool isstream); + + explicit FdEntity(const char* tpath = nullptr, const char* cpath = nullptr); + ~FdEntity(); + FdEntity(const FdEntity&) = delete; + FdEntity(FdEntity&&) = delete; + FdEntity& operator=(const FdEntity&) = delete; + FdEntity& operator=(FdEntity&&) = delete; + + void Close(int fd); + bool IsOpen() const { return (-1 != physical_fd); } + bool FindPseudoFd(int fd, AutoLock::Type locktype = AutoLock::NONE) const; + int Open(const headers_t* pmeta, off_t size, const struct timespec& ts_mctime, int flags, AutoLock::Type type); + bool LoadAll(int fd, headers_t* pmeta = nullptr, off_t* size = nullptr, bool force_load = false); + int Dup(int fd, AutoLock::Type locktype = AutoLock::NONE); + int OpenPseudoFd(int flags = O_RDONLY, AutoLock::Type locktype = AutoLock::NONE); + int GetOpenCount(AutoLock::Type locktype = AutoLock::NONE) const; + const std::string& GetPath() const { return path; } + bool RenamePath(const std::string& newpath, std::string& fentmapkey); + int GetPhysicalFd() const { return physical_fd; } + bool IsModified() const; + bool MergeOrgMeta(headers_t& updatemeta); + int UploadPending(int fd, AutoLock::Type type); + + bool GetStats(struct stat& st, AutoLock::Type locktype = AutoLock::NONE) const; + int SetCtime(struct timespec time, AutoLock::Type locktype = AutoLock::NONE); + int SetAtime(struct timespec time, AutoLock::Type locktype = AutoLock::NONE); + int SetMCtime(struct timespec mtime, struct timespec ctime, AutoLock::Type locktype = AutoLock::NONE); + bool UpdateCtime(); + bool UpdateAtime(); + bool UpdateMtime(bool clear_holding_mtime = false); + bool UpdateMCtime(); + bool SetHoldingMtime(struct timespec mtime, AutoLock::Type locktype = AutoLock::NONE); + bool ClearHoldingMtime(AutoLock::Type locktype = AutoLock::NONE); + bool GetSize(off_t& size) const; + bool GetXattr(std::string& xattr) const; + bool SetXattr(const std::string& xattr); + bool SetMode(mode_t mode); + bool SetUId(uid_t uid); + bool SetGId(gid_t gid); + bool SetContentType(const char* path); + + int Load(off_t start, off_t size, AutoLock::Type type, bool is_modified_flag = false); // size=0 means loading to end + int LoadByAdaptor(off_t start, off_t size, AutoLock::Type type, std::shared_ptr dataAdaptor, bool is_modified_flag = false); + + off_t BytesModified(); + int RowFlush(int fd, const char* tpath, AutoLock::Type type, bool force_sync = false, bool force_tmpfile = false); + int Flush(int fd, AutoLock::Type type, bool force_sync = false, bool force_tmpfile = false) { return RowFlush(fd, nullptr, type, force_sync, force_tmpfile); } + + ssize_t Read(int fd, char* bytes, off_t start, size_t size, bool force_load = false); + ssize_t ReadByAdaptor(int fd, char* bytes, off_t start, size_t size, bool force_load, std::shared_ptr dataAdaptor); + ssize_t Write(int fd, const char* bytes, off_t start, size_t size, bool force_tmpfile = false); + + bool PunchHole(off_t start = 0, size_t size = 0); + + void MarkDirtyNewFile(); + bool IsDirtyNewFile() const; + void MarkDirtyMetadata(); + + bool GetLastUpdateUntreatedPart(off_t& start, off_t& size) const; + bool ReplaceLastUpdateUntreatedPart(off_t front_start, off_t front_size, off_t behind_start, off_t behind_size); + + size_t GetRealsize() const; + void UpdateRealsize(off_t size); + void TruncateRealsize(off_t size); + void SetUpdateMark(bool is_update); + bool GetUpdateMark() const; + const headers_t& GetOriginalHeaders() const; + ssize_t WriteCache(const char* bytes, off_t start, size_t size, AutoLock::Type locktype = AutoLock::NONE); + void ReleaseCache(); +}; + +typedef std::map> fdent_map_t; // key=path, value=FdEntity* + +#endif // S3FS_FDCACHE_ENTITY_H_ + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/fdcache_fdinfo.cpp b/s3fs/fdcache_fdinfo.cpp new file mode 100644 index 0000000..c71dbb7 --- /dev/null +++ b/s3fs/fdcache_fdinfo.cpp @@ -0,0 +1,1049 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Takeshi Nakatani + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "common.h" +#include "s3fs_logger.h" +#include "s3fs_util.h" +#include "fdcache_fdinfo.h" +#include "fdcache_pseudofd.h" +#include "fdcache_entity.h" +#include "curl.h" +#include "string_util.h" +#include "threadpoolman.h" + +//------------------------------------------------ +// PseudoFdInfo class variables +//------------------------------------------------ +int PseudoFdInfo::max_threads = -1; +int PseudoFdInfo::opt_max_threads = -1; + +//------------------------------------------------ +// PseudoFdInfo class methods +//------------------------------------------------ +// +// Worker function for uploading +// +void* PseudoFdInfo::MultipartUploadThreadWorker(void* arg) +{ + std::unique_ptr pthparam(static_cast(arg)); + if(!pthparam || !(pthparam->ppseudofdinfo)){ + return reinterpret_cast(-EIO); + } + S3FS_PRN_INFO3("Upload Part Thread [tpath=%s][start=%lld][size=%lld][part=%d]", pthparam->path.c_str(), static_cast(pthparam->start), static_cast(pthparam->size), pthparam->part_num); + + int result; + { + AutoLock auto_lock(&(pthparam->ppseudofdinfo->upload_list_lock)); + + if(0 != (result = pthparam->ppseudofdinfo->last_result)){ + S3FS_PRN_DBG("Already occurred error, thus this thread worker is exiting."); + + if(!pthparam->ppseudofdinfo->CompleteInstruction(result, AutoLock::ALREADY_LOCKED)){ // result will be overwritten with the same value. + result = -EIO; + } + return reinterpret_cast(result); + } + } + + // setup and make curl object + std::unique_ptr s3fscurl(S3fsCurl::CreateParallelS3fsCurl(pthparam->path.c_str(), pthparam->upload_fd, pthparam->start, pthparam->size, pthparam->part_num, pthparam->is_copy, pthparam->petag, pthparam->upload_id, result)); + if(nullptr == s3fscurl){ + S3FS_PRN_ERR("failed creating s3fs curl object for uploading [path=%s][start=%lld][size=%lld][part=%d]", pthparam->path.c_str(), static_cast(pthparam->start), static_cast(pthparam->size), pthparam->part_num); + + // set result for exiting + if(!pthparam->ppseudofdinfo->CompleteInstruction(result, AutoLock::NONE)){ + result = -EIO; + } + return reinterpret_cast(result); + } + + // Send request and get result + if(0 == (result = s3fscurl->RequestPerform())){ + S3FS_PRN_DBG("succeed uploading [path=%s][start=%lld][size=%lld][part=%d]", pthparam->path.c_str(), static_cast(pthparam->start), static_cast(pthparam->size), pthparam->part_num); + if(!s3fscurl->MixMultipartPostComplete()){ + S3FS_PRN_ERR("failed completion uploading [path=%s][start=%lld][size=%lld][part=%d]", pthparam->path.c_str(), static_cast(pthparam->start), static_cast(pthparam->size), pthparam->part_num); + result = -EIO; + } + }else{ + S3FS_PRN_ERR("failed uploading with error(%d) [path=%s][start=%lld][size=%lld][part=%d]", result, pthparam->path.c_str(), static_cast(pthparam->start), static_cast(pthparam->size), pthparam->part_num); + } + s3fscurl->DestroyCurlHandle(true, false); + + // set result + if(!pthparam->ppseudofdinfo->CompleteInstruction(result, AutoLock::NONE)){ + S3FS_PRN_WARN("This thread worker is about to end, so it doesn't return an EIO here and runs to the end."); + } + + return reinterpret_cast(result); +} + +//------------------------------------------------ +// PseudoFdInfo methods +//------------------------------------------------ +PseudoFdInfo::PseudoFdInfo(int fd, int open_flags) : pseudo_fd(-1), physical_fd(fd), flags(0), upload_fd(-1), uploaded_sem(0), instruct_count(0), completed_count(0), last_result(0) +{ + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); +#if S3FS_PTHREAD_ERRORCHECK + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK); +#endif + int result; + if(0 != (result = pthread_mutex_init(&upload_list_lock, &attr))){ + S3FS_PRN_CRIT("failed to init upload_list_lock: %d", result); + abort(); + } + is_lock_init = true; + + if(-1 != physical_fd){ + pseudo_fd = PseudoFdManager::Get(); + flags = open_flags; + } +} + +PseudoFdInfo::~PseudoFdInfo() +{ + Clear(); // call before destrying the mutex + + if(is_lock_init){ + int result; + if(0 != (result = pthread_mutex_destroy(&upload_list_lock))){ + S3FS_PRN_CRIT("failed to destroy upload_list_lock: %d", result); + abort(); + } + is_lock_init = false; + } +} + +bool PseudoFdInfo::Clear() +{ + // cppcheck-suppress unmatchedSuppression + // cppcheck-suppress knownConditionTrueFalse + if(!CancelAllThreads() || !ResetUploadInfo(AutoLock::NONE)){ + return false; + } + CloseUploadFd(); + + if(-1 != pseudo_fd){ + PseudoFdManager::Release(pseudo_fd); + } + pseudo_fd = -1; + physical_fd = -1; + + return true; +} + +void PseudoFdInfo::CloseUploadFd() +{ + AutoLock auto_lock(&upload_list_lock); + + if(-1 != upload_fd){ + close(upload_fd); + } +} + +bool PseudoFdInfo::OpenUploadFd(AutoLock::Type type) +{ + AutoLock auto_lock(&upload_list_lock, type); + + if(-1 != upload_fd){ + // already initialized + return true; + } + if(-1 == physical_fd){ + S3FS_PRN_ERR("physical_fd is not initialized yet."); + return false; + } + + // duplicate fd + int fd; + if(-1 == (fd = dup(physical_fd))){ + S3FS_PRN_ERR("Could not duplicate physical file descriptor(errno=%d)", errno); + return false; + } + scope_guard guard([&]() { close(fd); }); + + if(0 != lseek(fd, 0, SEEK_SET)){ + S3FS_PRN_ERR("Could not seek physical file descriptor(errno=%d)", errno); + return false; + } + struct stat st; + if(-1 == fstat(fd, &st)){ + S3FS_PRN_ERR("Invalid file descriptor for uploading(errno=%d)", errno); + return false; + } + + guard.dismiss(); + upload_fd = fd; + return true; +} + +bool PseudoFdInfo::Set(int fd, int open_flags) +{ + if(-1 == fd){ + return false; + } + Clear(); + physical_fd = fd; + pseudo_fd = PseudoFdManager::Get(); + flags = open_flags; + + return true; +} + +bool PseudoFdInfo::Writable() const +{ + if(-1 == pseudo_fd){ + return false; + } + if(0 == (flags & (O_WRONLY | O_RDWR))){ + return false; + } + return true; +} + +bool PseudoFdInfo::Readable() const +{ + if(-1 == pseudo_fd){ + return false; + } + // O_RDONLY is 0x00, it means any pattern is readable. + return true; +} + +bool PseudoFdInfo::ClearUploadInfo(bool is_cancel_mp) +{ + if(is_cancel_mp){ + // cppcheck-suppress unmatchedSuppression + // cppcheck-suppress knownConditionTrueFalse + if(!CancelAllThreads()){ + return false; + } + } + return ResetUploadInfo(AutoLock::NONE); +} + +bool PseudoFdInfo::ResetUploadInfo(AutoLock::Type type) +{ + AutoLock auto_lock(&upload_list_lock, type); + + upload_id.erase(); + upload_list.clear(); + instruct_count = 0; + completed_count = 0; + last_result = 0; + + return true; +} + +bool PseudoFdInfo::RowInitialUploadInfo(const std::string& id, bool is_cancel_mp, AutoLock::Type type) +{ + if(is_cancel_mp && AutoLock::ALREADY_LOCKED == type){ + S3FS_PRN_ERR("Internal Error: Could not call this with type=AutoLock::ALREADY_LOCKED and is_cancel_mp=true"); + return false; + } + + if(is_cancel_mp){ + // cppcheck-suppress unmatchedSuppression + // cppcheck-suppress knownConditionTrueFalse + if(!ClearUploadInfo(is_cancel_mp)){ + return false; + } + }else{ + // cppcheck-suppress unmatchedSuppression + // cppcheck-suppress knownConditionTrueFalse + if(!ResetUploadInfo(type)){ + return false; + } + } + + AutoLock auto_lock(&upload_list_lock, type); + upload_id = id; + return true; +} + +bool PseudoFdInfo::CompleteInstruction(int result, AutoLock::Type type) +{ + AutoLock auto_lock(&upload_list_lock, type); + + if(0 != result){ + last_result = result; + } + + if(0 >= instruct_count){ + S3FS_PRN_ERR("Internal error: instruct_count caused an underflow."); + return false; + } + --instruct_count; + ++completed_count; + + return true; +} + +bool PseudoFdInfo::GetUploadId(std::string& id) const +{ + if(!IsUploading()){ + S3FS_PRN_ERR("Multipart Upload has not started yet."); + return false; + } + id = upload_id; + return true; +} + +bool PseudoFdInfo::GetEtaglist(etaglist_t& list) const +{ + if(!IsUploading()){ + S3FS_PRN_ERR("Multipart Upload has not started yet."); + return false; + } + + AutoLock auto_lock(&upload_list_lock); + + list.clear(); + for(filepart_list_t::const_iterator iter = upload_list.begin(); iter != upload_list.end(); ++iter){ + if(iter->petag){ + list.push_back(*(iter->petag)); + }else{ + S3FS_PRN_ERR("The pointer to the etag string is null(internal error)."); + return false; + } + } + return !list.empty(); +} + +// [NOTE] +// This method adds a part for a multipart upload. +// The added new part must be an area that is exactly continuous with the +// immediately preceding part. +// An error will occur if it is discontinuous or if it overlaps with an +// existing area. +// +bool PseudoFdInfo::AppendUploadPart(off_t start, off_t size, bool is_copy, etagpair** ppetag) +{ + if(!IsUploading()){ + S3FS_PRN_ERR("Multipart Upload has not started yet."); + return false; + } + + AutoLock auto_lock(&upload_list_lock); + off_t next_start_pos = 0; + if(!upload_list.empty()){ + next_start_pos = upload_list.back().startpos + upload_list.back().size; + } + if(start != next_start_pos){ + S3FS_PRN_ERR("The expected starting position for the next part is %lld, but %lld was specified.", static_cast(next_start_pos), static_cast(start)); + return false; + } + + // make part number + int partnumber = static_cast(upload_list.size()) + 1; + + // add new part + etagpair* petag_entity = etag_entities.add(etagpair(nullptr, partnumber)); // [NOTE] Create the etag entity and register it in the list. + upload_list.emplace_back(false, physical_fd, start, size, is_copy, petag_entity); + + // set etag pointer + if(ppetag){ + *ppetag = petag_entity; + } + + return true; +} + +// +// Utility for sorting upload list +// +static bool filepart_partnum_compare(const filepart& src1, const filepart& src2) +{ + return src1.get_part_number() < src2.get_part_number(); +} + +bool PseudoFdInfo::InsertUploadPart(off_t start, off_t size, int part_num, bool is_copy, etagpair** ppetag, AutoLock::Type type) +{ + //S3FS_PRN_DBG("[start=%lld][size=%lld][part_num=%d][is_copy=%s]", static_cast(start), static_cast(size), part_num, (is_copy ? "true" : "false")); + + if(!IsUploading()){ + S3FS_PRN_ERR("Multipart Upload has not started yet."); + return false; + } + if(start < 0 || size <= 0 || part_num < 0 || !ppetag){ + S3FS_PRN_ERR("Parameters are wrong."); + return false; + } + + AutoLock auto_lock(&upload_list_lock, type); + + // insert new part + etagpair* petag_entity = etag_entities.add(etagpair(nullptr, part_num)); + upload_list.emplace_back(false, physical_fd, start, size, is_copy, petag_entity); + + // sort by part number + std::sort(upload_list.begin(), upload_list.end(), filepart_partnum_compare); + + // set etag pointer + *ppetag = petag_entity; + + return true; +} + +// [NOTE] +// This method only launches the upload thread. +// Check the maximum number of threads before calling. +// +bool PseudoFdInfo::ParallelMultipartUpload(const char* path, const mp_part_list_t& mplist, bool is_copy, AutoLock::Type type) +{ + //S3FS_PRN_DBG("[path=%s][mplist(%zu)]", SAFESTRPTR(path), mplist.size()); + + AutoLock auto_lock(&upload_list_lock, type); + + if(mplist.empty()){ + // nothing to do + return true; + } + if(!OpenUploadFd(AutoLock::ALREADY_LOCKED)){ + return false; + } + + for(mp_part_list_t::const_iterator iter = mplist.begin(); iter != mplist.end(); ++iter){ + // Insert upload part + etagpair* petag = nullptr; + if(!InsertUploadPart(iter->start, iter->size, iter->part_num, is_copy, &petag, AutoLock::ALREADY_LOCKED)){ + S3FS_PRN_ERR("Failed to insert insert upload part(path=%s, start=%lld, size=%lld, part=%d, copy=%s) to mplist", SAFESTRPTR(path), static_cast(iter->start), static_cast(iter->size), iter->part_num, (is_copy ? "true" : "false")); + return false; + } + + // make parameter for my thread + pseudofdinfo_thparam* thargs = new pseudofdinfo_thparam; + thargs->ppseudofdinfo = this; + thargs->path = SAFESTRPTR(path); + thargs->upload_id = upload_id; + thargs->upload_fd = upload_fd; + thargs->start = iter->start; + thargs->size = iter->size; + thargs->is_copy = is_copy; + thargs->part_num = iter->part_num; + thargs->petag = petag; + + // make parameter for thread pool + std::unique_ptr ppoolparam(new thpoolman_param); + ppoolparam->args = thargs; + ppoolparam->psem = &uploaded_sem; + ppoolparam->pfunc = PseudoFdInfo::MultipartUploadThreadWorker; + + // setup instruction + if(!ThreadPoolMan::Instruct(std::move(ppoolparam))){ + S3FS_PRN_ERR("failed setup instruction for uploading."); + delete thargs; + return false; + } + ++instruct_count; + } + return true; +} + +bool PseudoFdInfo::ParallelMultipartUploadAll(const char* path, const mp_part_list_t& to_upload_list, const mp_part_list_t& copy_list, int& result) +{ + S3FS_PRN_DBG("[path=%s][to_upload_list(%zu)][copy_list(%zu)]", SAFESTRPTR(path), to_upload_list.size(), copy_list.size()); + + result = 0; + + if(!OpenUploadFd(AutoLock::NONE)){ + return false; + } + + if(!ParallelMultipartUpload(path, to_upload_list, false, AutoLock::NONE) || !ParallelMultipartUpload(path, copy_list, true, AutoLock::NONE)){ + S3FS_PRN_ERR("Failed setup instruction for uploading(path=%s, to_upload_list=%zu, copy_list=%zu).", SAFESTRPTR(path), to_upload_list.size(), copy_list.size()); + return false; + } + + // Wait for all thread exiting + result = WaitAllThreadsExit(); + + return true; +} + +// +// Upload the last updated Untreated area +// +// [Overview] +// Uploads untreated areas with the maximum multipart upload size as the +// boundary. +// +// * The starting position of the untreated area is aligned with the maximum +// multipart upload size as the boundary. +// * If there is an uploaded area that overlaps with the aligned untreated +// area, that uploaded area is canceled and absorbed by the untreated area. +// * Upload only when the aligned untreated area exceeds the maximum multipart +// upload size. +// * When the start position of the untreated area is changed to boundary +// alignment(to backward), and if that gap area is remained, that area is +// rest to untreated area. +// +ssize_t PseudoFdInfo::UploadBoundaryLastUntreatedArea(const char* path, headers_t& meta, FdEntity* pfdent) +{ + S3FS_PRN_DBG("[path=%s][pseudo_fd=%d][physical_fd=%d]", SAFESTRPTR(path), pseudo_fd, physical_fd); + + if(!path || -1 == physical_fd || -1 == pseudo_fd || !pfdent){ + S3FS_PRN_ERR("pseudo_fd(%d) to physical_fd(%d) for path(%s) is not opened or not writable, or pfdent is nullptr.", pseudo_fd, physical_fd, path); + return -EBADF; + } + AutoLock auto_lock(&upload_list_lock); + + // + // Get last update untreated area + // + off_t last_untreated_start = 0; + off_t last_untreated_size = 0; + if(!pfdent->GetLastUpdateUntreatedPart(last_untreated_start, last_untreated_size) || last_untreated_start < 0 || last_untreated_size <= 0){ + S3FS_PRN_WARN("Not found last update untreated area or it is empty, thus return without any error."); + return 0; + } + + // + // Aligns the start position of the last updated raw area with the boundary + // + // * Align the last updated raw space with the maximum upload size boundary. + // * The remaining size of the part before the boundary is will not be uploaded. + // + off_t max_mp_size = S3fsCurl::GetMultipartSize(); + off_t aligned_start = ((last_untreated_start / max_mp_size) + (0 < (last_untreated_start % max_mp_size) ? 1 : 0)) * max_mp_size; + if((last_untreated_start + last_untreated_size) <= aligned_start){ + S3FS_PRN_INFO("After the untreated area(start=%lld, size=%lld) is aligned with the boundary, the aligned start(%lld) exceeds the untreated area, so there is nothing to do.", static_cast(last_untreated_start), static_cast(last_untreated_size), static_cast(aligned_start)); + return 0; + } + + off_t aligned_size = (((last_untreated_start + last_untreated_size) - aligned_start) / max_mp_size) * max_mp_size; + if(0 == aligned_size){ + S3FS_PRN_DBG("After the untreated area(start=%lld, size=%lld) is aligned with the boundary(start is %lld), the aligned size is empty, so nothing to do.", static_cast(last_untreated_start), static_cast(last_untreated_size), static_cast(aligned_start)); + return 0; + } + + off_t front_rem_start = last_untreated_start; // start of the remainder untreated area in front of the boundary + off_t front_rem_size = aligned_start - last_untreated_start; // size of the remainder untreated area in front of the boundary + + // + // Get the area for uploading, if last update treated area can be uploaded. + // + // [NOTE] + // * Create the updoad area list, if the untreated area aligned with the boundary + // exceeds the maximum upload size. + // * If it overlaps with an area that has already been uploaded(unloaded list), + // that area is added to the cancellation list and included in the untreated area. + // + mp_part_list_t to_upload_list; + filepart_list_t cancel_uploaded_list; + if(!ExtractUploadPartsFromUntreatedArea(aligned_start, aligned_size, to_upload_list, cancel_uploaded_list, S3fsCurl::GetMultipartSize())){ + S3FS_PRN_ERR("Failed to extract upload parts from last untreated area."); + return -EIO; + } + if(to_upload_list.empty()){ + S3FS_PRN_INFO("There is nothing to upload. In most cases, the untreated area does not meet the upload size."); + return 0; + } + + // + // Has multipart uploading already started? + // + if(!IsUploading()){ + // Multipart uploading hasn't started yet, so start it. + // + S3fsCurl s3fscurl(true); + std::string tmp_upload_id; + int result; + if(0 != (result = s3fscurl.PreMultipartPostRequest(path, meta, tmp_upload_id, true))){ + S3FS_PRN_ERR("failed to setup multipart upload(create upload id) by errno(%d)", result); + return result; + } + if(!RowInitialUploadInfo(tmp_upload_id, false/* not need to cancel */, AutoLock::ALREADY_LOCKED)){ + S3FS_PRN_ERR("failed to setup multipart upload(set upload id to object)"); + return result; + } + } + + // + // Output debug level information + // + // When canceling(overwriting) a part that has already been uploaded, output it. + // + if(S3fsLog::IsS3fsLogDbg()){ + for(filepart_list_t::const_iterator cancel_iter = cancel_uploaded_list.begin(); cancel_iter != cancel_uploaded_list.end(); ++cancel_iter){ + S3FS_PRN_DBG("Cancel uploaded: start(%lld), size(%lld), part number(%d)", static_cast(cancel_iter->startpos), static_cast(cancel_iter->size), (cancel_iter->petag ? cancel_iter->petag->part_num : -1)); + } + } + + // + // Upload Multipart parts + // + if(!ParallelMultipartUpload(path, to_upload_list, false, AutoLock::ALREADY_LOCKED)){ + S3FS_PRN_ERR("Failed to upload multipart parts."); + return -EIO; + } + + // + // Exclude the uploaded Untreated area and update the last Untreated area. + // + off_t behind_rem_start = aligned_start + aligned_size; + off_t behind_rem_size = (last_untreated_start + last_untreated_size) - behind_rem_start; + + if(!pfdent->ReplaceLastUpdateUntreatedPart(front_rem_start, front_rem_size, behind_rem_start, behind_rem_size)){ + S3FS_PRN_WARN("The last untreated area could not be detected and the uploaded area could not be excluded from it, but continue because it does not affect the overall processing."); + } + + return 0; +} + +int PseudoFdInfo::WaitAllThreadsExit() +{ + int result; + bool is_loop = true; + { + AutoLock auto_lock(&upload_list_lock); + if(0 == instruct_count && 0 == completed_count){ + result = last_result; + is_loop = false; + } + } + + while(is_loop){ + // need to wait the worker exiting + uploaded_sem.wait(); + { + AutoLock auto_lock(&upload_list_lock); + if(0 < completed_count){ + --completed_count; + } + if(0 == instruct_count && 0 == completed_count){ + // break loop + result = last_result; + is_loop = false; + } + } + } + + return result; +} + +bool PseudoFdInfo::CancelAllThreads() +{ + bool need_cancel = false; + { + AutoLock auto_lock(&upload_list_lock); + if(0 < instruct_count && 0 < completed_count){ + S3FS_PRN_INFO("The upload thread is running, so cancel them and wait for the end."); + need_cancel = true; + last_result = -ECANCELED; // to stop thread running + } + } + if(need_cancel){ + WaitAllThreadsExit(); + } + return true; +} + +// +// Extract the list for multipart upload from the Unteated Area +// +// The untreated_start parameter must be set aligning it with the boundaries +// of the maximum multipart upload size. This method expects it to be bounded. +// +// This method creates the upload area aligned from the untreated area by +// maximum size and creates the required list. +// If it overlaps with an area that has already been uploaded, the overlapped +// upload area will be canceled and absorbed by the untreated area. +// If the list creation process is complete and areas smaller than the maximum +// size remain, those area will be reset to untreated_start and untreated_size +// and returned to the caller. +// If the called untreated area is smaller than the maximum size of the +// multipart upload, no list will be created. +// +// [NOTE] +// Maximum multipart upload size must be uploading boundary. +// +bool PseudoFdInfo::ExtractUploadPartsFromUntreatedArea(const off_t& untreated_start, const off_t& untreated_size, mp_part_list_t& to_upload_list, filepart_list_t& cancel_upload_list, off_t max_mp_size) +{ + if(untreated_start < 0 || untreated_size <= 0){ + S3FS_PRN_ERR("Paramters are wrong(untreated_start=%lld, untreated_size=%lld).", static_cast(untreated_start), static_cast(untreated_size)); + return false; + } + + // Initiliaze lists + to_upload_list.clear(); + cancel_upload_list.clear(); + + // + // Align start position with maximum multipart upload boundaries + // + off_t aligned_start = (untreated_start / max_mp_size) * max_mp_size; + off_t aligned_size = untreated_size + (untreated_start - aligned_start); + + // + // Check aligned untreated size + // + if(aligned_size < max_mp_size){ + S3FS_PRN_INFO("untreated area(start=%lld, size=%lld) to aligned boundary(start=%lld, size=%lld) is smaller than max mp size(%lld), so nothing to do.", static_cast(untreated_start), static_cast(untreated_size), static_cast(aligned_start), static_cast(aligned_size), static_cast(max_mp_size)); + return true; // successful termination + } + + // + // Check each unloaded area in list + // + // [NOTE] + // The uploaded area must be to be aligned by boundary. + // Also, it is assumed that it must not be a copy area. + // So if the areas overlap, include uploaded area as an untreated area. + // + for(filepart_list_t::iterator cur_iter = upload_list.begin(); cur_iter != upload_list.end(); /* ++cur_iter */){ + // Check overlap + if((cur_iter->startpos + cur_iter->size - 1) < aligned_start || (aligned_start + aligned_size - 1) < cur_iter->startpos){ + // Areas do not overlap + ++cur_iter; + + }else{ + // The areas overlap + // + // Since the start position of the uploaded area is aligned with the boundary, + // it is not necessary to check the start position. + // If the uploaded area exceeds the untreated area, expand the untreated area. + // + if((aligned_start + aligned_size - 1) < (cur_iter->startpos + cur_iter->size - 1)){ + aligned_size += (cur_iter->startpos + cur_iter->size) - (aligned_start + aligned_size); + } + + // + // Add this to cancel list + // + cancel_upload_list.push_back(*cur_iter); // Copy and Push to cancel list + cur_iter = upload_list.erase(cur_iter); + } + } + + // + // Add upload area to the list + // + while(max_mp_size <= aligned_size){ + int part_num = static_cast((aligned_start / max_mp_size) + 1); + to_upload_list.emplace_back(aligned_start, max_mp_size, part_num); + + aligned_start += max_mp_size; + aligned_size -= max_mp_size; + } + + return true; +} + +// +// Extract the area lists to be uploaded/downloaded for the entire file. +// +// [Parameters] +// to_upload_list : A list of areas to upload in multipart upload. +// to_copy_list : A list of areas for copy upload in multipart upload. +// to_download_list : A list of areas that must be downloaded before multipart upload. +// cancel_upload_list : A list of areas that have already been uploaded and will be canceled(overwritten). +// wait_upload_complete : If cancellation areas exist, this flag is set to true when it is necessary to wait until the upload of those cancellation areas is complete. +// file_size : The size of the upload file. +// use_copy : Specify true if copy multipart upload is available. +// +// [NOTE] +// The untreated_list in fdentity does not change, but upload_list is changed. +// (If you want to restore it, you can use cancel_upload_list.) +// +bool PseudoFdInfo::ExtractUploadPartsFromAllArea(UntreatedParts& untreated_list, mp_part_list_t& to_upload_list, mp_part_list_t& to_copy_list, mp_part_list_t& to_download_list, filepart_list_t& cancel_upload_list, bool& wait_upload_complete, off_t max_mp_size, off_t file_size, bool use_copy) +{ + AutoLock auto_lock(&upload_list_lock); + + // Initiliaze lists + to_upload_list.clear(); + to_copy_list.clear(); + to_download_list.clear(); + cancel_upload_list.clear(); + wait_upload_complete = false; + + // Duplicate untreated list + untreated_list_t dup_untreated_list; + untreated_list.Duplicate(dup_untreated_list); + + // Initialize the iterator of each list first + untreated_list_t::iterator dup_untreated_iter = dup_untreated_list.begin(); + filepart_list_t::iterator uploaded_iter = upload_list.begin(); + + // + // Loop to extract areas to upload and download + // + // Check at the boundary of the maximum upload size from the beginning of the file + // + for(off_t cur_start = 0, cur_size = 0; cur_start < file_size; cur_start += cur_size){ + // + // Set part size + // (To avoid confusion, the area to be checked is called the "current area".) + // + cur_size = ((cur_start + max_mp_size) <= file_size ? max_mp_size : (file_size - cur_start)); + + // + // Extract the untreated erea that overlaps this current area. + // (The extracted area is deleted from dup_untreated_list.) + // + untreated_list_t cur_untreated_list; + for(cur_untreated_list.clear(); dup_untreated_iter != dup_untreated_list.end(); ){ + if((dup_untreated_iter->start < (cur_start + cur_size)) && (cur_start < (dup_untreated_iter->start + dup_untreated_iter->size))){ + // this untreated area is overlap + off_t tmp_untreated_start; + off_t tmp_untreated_size; + if(dup_untreated_iter->start < cur_start){ + // [NOTE] + // This untreated area overlaps with the current area, but starts + // in front of the target area. + // This state should not be possible, but if this state is detected, + // the part before the target area will be deleted. + // + tmp_untreated_start = cur_start; + tmp_untreated_size = dup_untreated_iter->size - (cur_start - dup_untreated_iter->start); + }else{ + tmp_untreated_start = dup_untreated_iter->start; + tmp_untreated_size = dup_untreated_iter->size; + } + + // + // Check the end of the overlapping untreated area. + // + if((tmp_untreated_start + tmp_untreated_size) <= (cur_start + cur_size)){ + // + // All of untreated areas are within the current area + // + // - Add this untreated area to cur_untreated_list + // - Delete this from dup_untreated_list + // + cur_untreated_list.emplace_back(tmp_untreated_start, tmp_untreated_size); + dup_untreated_iter = dup_untreated_list.erase(dup_untreated_iter); + }else{ + // + // The untreated area exceeds the end of the current area + // + + // Ajust untreated area + tmp_untreated_size = (cur_start + cur_size) - tmp_untreated_start; + + // Add ajusted untreated area to cur_untreated_list + cur_untreated_list.emplace_back(tmp_untreated_start, tmp_untreated_size); + + // Remove this ajusted untreated area from the area pointed + // to by dup_untreated_iter. + dup_untreated_iter->size = (dup_untreated_iter->start + dup_untreated_iter->size) - (cur_start + cur_size); + dup_untreated_iter->start = tmp_untreated_start + tmp_untreated_size; + } + + }else if((cur_start + cur_size - 1) < dup_untreated_iter->start){ + // this untreated area is over the current area, thus break loop. + break; + }else{ + ++dup_untreated_iter; + } + } + + // + // Check uploaded area + // + // [NOTE] + // The uploaded area should be aligned with the maximum upload size boundary. + // It also assumes that each size of uploaded area must be a maximum upload + // size. + // + filepart_list_t::iterator overlap_uploaded_iter = upload_list.end(); + for(; uploaded_iter != upload_list.end(); ++uploaded_iter){ + if((cur_start < (uploaded_iter->startpos + uploaded_iter->size)) && (uploaded_iter->startpos < (cur_start + cur_size))){ + if(overlap_uploaded_iter != upload_list.end()){ + // + // Something wrong in this unloaded area. + // + // This area is not aligned with the boundary, then this condition + // is unrecoverable and return failure. + // + S3FS_PRN_ERR("The uploaded list may not be the boundary for the maximum multipart upload size. No further processing is possible."); + return false; + } + // Set this iterator to overlap iter + overlap_uploaded_iter = uploaded_iter; + + }else if((cur_start + cur_size - 1) < uploaded_iter->startpos){ + break; + } + } + + // + // Create upload/download/cancel/copy list for this current area + // + int part_num = static_cast((cur_start / max_mp_size) + 1); + if(cur_untreated_list.empty()){ + // + // No untreated area was detected in this current area + // + if(overlap_uploaded_iter != upload_list.end()){ + // + // This current area already uploaded, then nothing to add to lists. + // + S3FS_PRN_DBG("Already uploaded: start=%lld, size=%lld", static_cast(cur_start), static_cast(cur_size)); + + }else{ + // + // This current area has not been uploaded + // (neither an uploaded area nor an untreated area.) + // + if(use_copy){ + // + // Copy multipart upload available + // + S3FS_PRN_DBG("To copy: start=%lld, size=%lld", static_cast(cur_start), static_cast(cur_size)); + to_copy_list.emplace_back(cur_start, cur_size, part_num); + }else{ + // + // This current area needs to be downloaded and uploaded + // + S3FS_PRN_DBG("To download and upload: start=%lld, size=%lld", static_cast(cur_start), static_cast(cur_size)); + to_download_list.emplace_back(cur_start, cur_size); + to_upload_list.emplace_back(cur_start, cur_size, part_num); + } + } + }else{ + // + // Found untreated area in this current area + // + if(overlap_uploaded_iter != upload_list.end()){ + // + // This current area is also the uploaded area + // + // [NOTE] + // The uploaded area is aligned with boundary, there are all data in + // this current area locally(which includes all data of untreated area). + // So this current area only needs to be uploaded again. + // + S3FS_PRN_DBG("Cancel upload: start=%lld, size=%lld", static_cast(overlap_uploaded_iter->startpos), static_cast(overlap_uploaded_iter->size)); + + if(!overlap_uploaded_iter->uploaded){ + S3FS_PRN_DBG("This cancel upload area is still uploading, so you must wait for it to complete before starting any Stream uploads."); + wait_upload_complete = true; + } + cancel_upload_list.push_back(*overlap_uploaded_iter); // add this uploaded area to cancel_upload_list + uploaded_iter = upload_list.erase(overlap_uploaded_iter); // remove it from upload_list + + S3FS_PRN_DBG("To upload: start=%lld, size=%lld", static_cast(cur_start), static_cast(cur_size)); + to_upload_list.emplace_back(cur_start, cur_size, part_num); // add new uploading area to list + + }else{ + // + // No uploaded area overlap this current area + // (Areas other than the untreated area must be downloaded.) + // + // [NOTE] + // Need to consider the case where there is a gap between the start + // of the current area and the untreated area. + // This gap is the area that should normally be downloaded. + // But it is the area that can be copied if we can use copy multipart + // upload. Then If we can use copy multipart upload and the previous + // area is used copy multipart upload, this gap will be absorbed by + // the previous area. + // Unifying the copy multipart upload area can reduce the number of + // upload requests. + // + off_t tmp_cur_start = cur_start; + off_t tmp_cur_size = cur_size; + off_t changed_start = cur_start; + off_t changed_size = cur_size; + bool first_area = true; + for(untreated_list_t::const_iterator tmp_cur_untreated_iter = cur_untreated_list.begin(); tmp_cur_untreated_iter != cur_untreated_list.end(); ++tmp_cur_untreated_iter, first_area = false){ + if(tmp_cur_start < tmp_cur_untreated_iter->start){ + // + // Detected a gap at the start of area + // + bool include_prev_copy_part = false; + if(first_area && use_copy && !to_copy_list.empty()){ + // + // Make sure that the area of the last item in to_copy_list + // is contiguous with this current area. + // + // [NOTE] + // Areas can be unified if the total size of the areas is + // within 5GB and the remaining area after unification is + // larger than the minimum multipart upload size. + // + mp_part_list_t::reverse_iterator copy_riter = to_copy_list.rbegin(); + + if( (copy_riter->start + copy_riter->size) == tmp_cur_start && + (copy_riter->size + (tmp_cur_untreated_iter->start - tmp_cur_start)) <= FIVE_GB && + ((tmp_cur_start + tmp_cur_size) - tmp_cur_untreated_iter->start) >= MIN_MULTIPART_SIZE ) + { + // + // Unify to this area to previouse copy area. + // + copy_riter->size += tmp_cur_untreated_iter->start - tmp_cur_start; + S3FS_PRN_DBG("Resize to copy: start=%lld, size=%lld", static_cast(copy_riter->start), static_cast(copy_riter->size)); + + changed_size -= (tmp_cur_untreated_iter->start - changed_start); + changed_start = tmp_cur_untreated_iter->start; + include_prev_copy_part = true; + } + } + if(!include_prev_copy_part){ + // + // If this area is not unified, need to download this area + // + S3FS_PRN_DBG("To download: start=%lld, size=%lld", static_cast(tmp_cur_start), static_cast(tmp_cur_untreated_iter->start - tmp_cur_start)); + to_download_list.emplace_back(tmp_cur_start, tmp_cur_untreated_iter->start - tmp_cur_start); + } + } + // + // Set next start position + // + tmp_cur_size = (tmp_cur_start + tmp_cur_size) - (tmp_cur_untreated_iter->start + tmp_cur_untreated_iter->size); + tmp_cur_start = tmp_cur_untreated_iter->start + tmp_cur_untreated_iter->size; + } + + // + // Add download area to list, if remaining size + // + if(0 < tmp_cur_size){ + S3FS_PRN_DBG("To download: start=%lld, size=%lld", static_cast(tmp_cur_start), static_cast(tmp_cur_size)); + to_download_list.emplace_back(tmp_cur_start, tmp_cur_size); + } + + // + // Set upload area(whole of area) to list + // + S3FS_PRN_DBG("To upload: start=%lld, size=%lld", static_cast(changed_start), static_cast(changed_size)); + to_upload_list.emplace_back(changed_start, changed_size, part_num); + } + } + } + return true; +} + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/fdcache_fdinfo.h b/s3fs/fdcache_fdinfo.h new file mode 100644 index 0000000..0f1bcc8 --- /dev/null +++ b/s3fs/fdcache_fdinfo.h @@ -0,0 +1,133 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef S3FS_FDCACHE_FDINFO_H_ +#define S3FS_FDCACHE_FDINFO_H_ + +#include + +#include "psemaphore.h" +#include "metaheader.h" +#include "autolock.h" +#include "types.h" + +class FdEntity; +class UntreatedParts; + +//------------------------------------------------ +// Structure of parameters to pass to thread +//------------------------------------------------ +class PseudoFdInfo; + +struct pseudofdinfo_thparam +{ + PseudoFdInfo* ppseudofdinfo; + std::string path; + std::string upload_id; + int upload_fd; + off_t start; + off_t size; + bool is_copy; + int part_num; + etagpair* petag; + + pseudofdinfo_thparam() : ppseudofdinfo(nullptr), path(""), upload_id(""), upload_fd(-1), start(0), size(0), is_copy(false), part_num(-1), petag(nullptr) {} +}; + +//------------------------------------------------ +// Class PseudoFdInfo +//------------------------------------------------ +class PseudoFdInfo +{ + private: + static int max_threads; + static int opt_max_threads; // for option value + + int pseudo_fd; + int physical_fd; + int flags; // flags at open + std::string upload_id; + int upload_fd; // duplicated fd for uploading + filepart_list_t upload_list; + petagpool etag_entities; // list of etag string and part number entities(to maintain the etag entity even if MPPART_INFO is destroyed) + bool is_lock_init; + mutable pthread_mutex_t upload_list_lock; // protects upload_id and upload_list + Semaphore uploaded_sem; // use a semaphore to trigger an upload completion like event flag + int instruct_count; // number of instructions for processing by threads + int completed_count; // number of completed processes by thread + int last_result; // the result of thread processing + + private: + static void* MultipartUploadThreadWorker(void* arg); + + bool Clear(); + void CloseUploadFd(); + bool OpenUploadFd(AutoLock::Type type = AutoLock::NONE); + bool ResetUploadInfo(AutoLock::Type type); + bool RowInitialUploadInfo(const std::string& id, bool is_cancel_mp, AutoLock::Type type); + bool CompleteInstruction(int result, AutoLock::Type type = AutoLock::NONE); + bool ParallelMultipartUpload(const char* path, const mp_part_list_t& mplist, bool is_copy, AutoLock::Type type = AutoLock::NONE); + bool InsertUploadPart(off_t start, off_t size, int part_num, bool is_copy, etagpair** ppetag, AutoLock::Type type = AutoLock::NONE); + bool CancelAllThreads(); + bool ExtractUploadPartsFromUntreatedArea(const off_t& untreated_start, const off_t& untreated_size, mp_part_list_t& to_upload_list, filepart_list_t& cancel_upload_list, off_t max_mp_size); + + public: + explicit PseudoFdInfo(int fd = -1, int open_flags = 0); + ~PseudoFdInfo(); + PseudoFdInfo(const PseudoFdInfo&) = delete; + PseudoFdInfo(PseudoFdInfo&&) = delete; + PseudoFdInfo& operator=(const PseudoFdInfo&) = delete; + PseudoFdInfo& operator=(PseudoFdInfo&&) = delete; + + int GetPhysicalFd() const { return physical_fd; } + int GetPseudoFd() const { return pseudo_fd; } + int GetFlags() const { return flags; } + bool Writable() const; + bool Readable() const; + + bool Set(int fd, int open_flags); + bool ClearUploadInfo(bool is_cancel_mp = false); + bool InitialUploadInfo(const std::string& id){ return RowInitialUploadInfo(id, true, AutoLock::NONE); } + + bool IsUploading() const { return !upload_id.empty(); } + bool GetUploadId(std::string& id) const; + bool GetEtaglist(etaglist_t& list) const; + + bool AppendUploadPart(off_t start, off_t size, bool is_copy = false, etagpair** ppetag = nullptr); + + bool ParallelMultipartUploadAll(const char* path, const mp_part_list_t& to_upload_list, const mp_part_list_t& copy_list, int& result); + + int WaitAllThreadsExit(); + ssize_t UploadBoundaryLastUntreatedArea(const char* path, headers_t& meta, FdEntity* pfdent); + bool ExtractUploadPartsFromAllArea(UntreatedParts& untreated_list, mp_part_list_t& to_upload_list, mp_part_list_t& to_copy_list, mp_part_list_t& to_download_list, filepart_list_t& cancel_upload_list, bool& wait_upload_complete, off_t max_mp_size, off_t file_size, bool use_copy); +}; + +typedef std::map> fdinfo_map_t; + +#endif // S3FS_FDCACHE_FDINFO_H_ + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/fdcache_page.cpp b/s3fs/fdcache_page.cpp new file mode 100644 index 0000000..f5b50ef --- /dev/null +++ b/s3fs/fdcache_page.cpp @@ -0,0 +1,1035 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Takeshi Nakatani + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include +#include +#include +#include +#include + +#include "common.h" +#include "s3fs_logger.h" +#include "fdcache_page.h" +#include "fdcache_stat.h" +#include "string_util.h" + +//------------------------------------------------ +// Symbols +//------------------------------------------------ +static constexpr int CHECK_CACHEFILE_PART_SIZE = 1024 * 16; // Buffer size in PageList::CheckZeroAreaInFile() + +//------------------------------------------------ +// fdpage_list_t utility +//------------------------------------------------ +// Inline function for repeated processing +inline void raw_add_compress_fdpage_list(fdpage_list_t& pagelist, const fdpage& orgpage, bool ignore_load, bool ignore_modify, bool default_load, bool default_modify) +{ + if(0 < orgpage.bytes){ + // [NOTE] + // The page variable is subject to change here. + // + fdpage page = orgpage; + + if(ignore_load){ + page.loaded = default_load; + } + if(ignore_modify){ + page.modified = default_modify; + } + pagelist.push_back(page); + } +} + +// Compress the page list +// +// ignore_load: Ignore the flag of loaded member and compress +// ignore_modify: Ignore the flag of modified member and compress +// default_load: loaded flag value in the list after compression when ignore_load=true +// default_modify: modified flag value in the list after compression when default_modify=true +// +// NOTE: ignore_modify and ignore_load cannot both be true. +// Zero size pages will be deleted. However, if the page information is the only one, +// it will be left behind. This is what you need to do to create a new empty file. +// +static void raw_compress_fdpage_list(const fdpage_list_t& pages, fdpage_list_t& compressed_pages, bool ignore_load, bool ignore_modify, bool default_load, bool default_modify) +{ + compressed_pages.clear(); + + fdpage* lastpage = nullptr; + fdpage_list_t::iterator add_iter; + for(fdpage_list_t::const_iterator iter = pages.begin(); iter != pages.end(); ++iter){ + if(0 == iter->bytes){ + continue; + } + if(!lastpage){ + // First item + raw_add_compress_fdpage_list(compressed_pages, (*iter), ignore_load, ignore_modify, default_load, default_modify); + lastpage = &(compressed_pages.back()); + }else{ + // check page continuity + if(lastpage->next() != iter->offset){ + // Non-consecutive with last page, so add a page filled with default values + if( (!ignore_load && (lastpage->loaded != false)) || + (!ignore_modify && (lastpage->modified != false)) ) + { + // add new page + fdpage tmppage(lastpage->next(), (iter->offset - lastpage->next()), false, false); + raw_add_compress_fdpage_list(compressed_pages, tmppage, ignore_load, ignore_modify, default_load, default_modify); + + add_iter = compressed_pages.end(); + --add_iter; + lastpage = &(*add_iter); + }else{ + // Expand last area + lastpage->bytes = iter->offset - lastpage->offset; + } + } + + // add current page + if( (!ignore_load && (lastpage->loaded != iter->loaded )) || + (!ignore_modify && (lastpage->modified != iter->modified)) ) + { + // Add new page + raw_add_compress_fdpage_list(compressed_pages, (*iter), ignore_load, ignore_modify, default_load, default_modify); + + add_iter = compressed_pages.end(); + --add_iter; + lastpage = &(*add_iter); + }else{ + // Expand last area + lastpage->bytes += iter->bytes; + } + } + } +} + +static void compress_fdpage_list_ignore_modify(const fdpage_list_t& pages, fdpage_list_t& compressed_pages, bool default_modify) +{ + raw_compress_fdpage_list(pages, compressed_pages, /* ignore_load= */ false, /* ignore_modify= */ true, /* default_load= */false, /* default_modify= */default_modify); +} + +static void compress_fdpage_list_ignore_load(const fdpage_list_t& pages, fdpage_list_t& compressed_pages, bool default_load) +{ + raw_compress_fdpage_list(pages, compressed_pages, /* ignore_load= */ true, /* ignore_modify= */ false, /* default_load= */default_load, /* default_modify= */false); +} + +static void compress_fdpage_list(const fdpage_list_t& pages, fdpage_list_t& compressed_pages) +{ + raw_compress_fdpage_list(pages, compressed_pages, /* ignore_load= */ false, /* ignore_modify= */ false, /* default_load= */false, /* default_modify= */false); +} + +static fdpage_list_t parse_partsize_fdpage_list(const fdpage_list_t& pages, off_t max_partsize) +{ + fdpage_list_t parsed_pages; + for(fdpage_list_t::const_iterator iter = pages.begin(); iter != pages.end(); ++iter){ + if(iter->modified){ + // modified page + fdpage tmppage = *iter; + for(off_t start = iter->offset, rest_bytes = iter->bytes; 0 < rest_bytes; ){ + if((max_partsize * 2) < rest_bytes){ + // do parse + tmppage.offset = start; + tmppage.bytes = max_partsize; + parsed_pages.push_back(tmppage); + + start += max_partsize; + rest_bytes -= max_partsize; + }else{ + // Since the number of remaining bytes is less than twice max_partsize, + // one of the divided areas will be smaller than max_partsize. + // Therefore, this area at the end should not be divided. + tmppage.offset = start; + tmppage.bytes = rest_bytes; + parsed_pages.push_back(tmppage); + + start += rest_bytes; + rest_bytes = 0; + } + } + }else{ + // not modified page is not parsed + parsed_pages.push_back(*iter); + } + } + return parsed_pages; +} + +//------------------------------------------------ +// PageList class methods +//------------------------------------------------ +// +// Examine and return the status of each block in the file. +// +// Assuming the file is a sparse file, check the HOLE and DATA areas +// and return it in fdpage_list_t. The loaded flag of each fdpage is +// set to false for HOLE blocks and true for DATA blocks. +// +bool PageList::GetSparseFilePages(int fd, size_t file_size, fdpage_list_t& sparse_list) +{ + // [NOTE] + // Express the status of the cache file using fdpage_list_t. + // There is a hole in the cache file(sparse file), and the + // state of this hole is expressed by the "loaded" member of + // struct fdpage. (the "modified" member is not used) + // + if(0 == file_size){ + // file is empty + return true; + } + + bool is_hole = false; + off_t hole_pos = lseek(fd, 0, SEEK_HOLE); + off_t data_pos = lseek(fd, 0, SEEK_DATA); + if(-1 == hole_pos && -1 == data_pos){ + S3FS_PRN_ERR("Could not find the first position both HOLE and DATA in the file(physical_fd=%d).", fd); + return false; + }else if(-1 == hole_pos){ + is_hole = false; + }else if(-1 == data_pos){ + is_hole = true; + }else if(hole_pos < data_pos){ + is_hole = true; + }else{ + is_hole = false; + } + + for(off_t cur_pos = 0, next_pos = 0; 0 <= cur_pos; cur_pos = next_pos, is_hole = !is_hole){ + fdpage page; + page.offset = cur_pos; + page.loaded = !is_hole; + page.modified = false; + + next_pos = lseek(fd, cur_pos, (is_hole ? SEEK_DATA : SEEK_HOLE)); + if(-1 == next_pos){ + page.bytes = static_cast(file_size - cur_pos); + }else{ + page.bytes = next_pos - cur_pos; + } + sparse_list.push_back(page); + } + return true; +} + +// +// Confirm that the specified area is ZERO +// +bool PageList::CheckZeroAreaInFile(int fd, off_t start, size_t bytes) +{ + std::unique_ptr readbuff(new char[CHECK_CACHEFILE_PART_SIZE]); + + for(size_t comp_bytes = 0, check_bytes = 0; comp_bytes < bytes; comp_bytes += check_bytes){ + if(CHECK_CACHEFILE_PART_SIZE < (bytes - comp_bytes)){ + check_bytes = CHECK_CACHEFILE_PART_SIZE; + }else{ + check_bytes = bytes - comp_bytes; + } + bool found_bad_data = false; + ssize_t read_bytes; + if(-1 == (read_bytes = pread(fd, readbuff.get(), check_bytes, (start + comp_bytes)))){ + S3FS_PRN_ERR("Something error is occurred in reading %zu bytes at %lld from file(physical_fd=%d).", check_bytes, static_cast(start + comp_bytes), fd); + found_bad_data = true; + }else{ + check_bytes = static_cast(read_bytes); + for(size_t tmppos = 0; tmppos < check_bytes; ++tmppos){ + if('\0' != readbuff[tmppos]){ + // found not ZERO data. + found_bad_data = true; + break; + } + } + } + if(found_bad_data){ + return false; + } + } + return true; +} + +// +// Checks that the specified area matches the state of the sparse file. +// +// [Parameters] +// checkpage: This is one state of the cache file, it is loaded from the stats file. +// sparse_list: This is a list of the results of directly checking the cache file status(HOLE/DATA). +// In the HOLE area, the "loaded" flag of fdpage is false. The DATA area has it set to true. +// fd: opened file discriptor to target cache file. +// +bool PageList::CheckAreaInSparseFile(const struct fdpage& checkpage, const fdpage_list_t& sparse_list, int fd, fdpage_list_t& err_area_list, fdpage_list_t& warn_area_list) +{ + // Check the block status of a part(Check Area: checkpage) of the target file. + // The elements of sparse_list have 5 patterns that overlap this block area. + // + // File |<---...--------------------------------------...--->| + // Check Area (offset)<-------------------->(offset + bytes - 1) + // Area case(0) <-------> + // Area case(1) <-------> + // Area case(2) <--------> + // Area case(3) <----------> + // Area case(4) <-----------> + // Area case(5) <-----------------------------> + // + bool result = true; + + for(fdpage_list_t::const_iterator iter = sparse_list.begin(); iter != sparse_list.end(); ++iter){ + off_t check_start = 0; + off_t check_bytes = 0; + if((iter->offset + iter->bytes) <= checkpage.offset){ + // case 0 + continue; // next + + }else if((checkpage.offset + checkpage.bytes) <= iter->offset){ + // case 1 + break; // finish + + }else if(iter->offset < checkpage.offset && (iter->offset + iter->bytes) < (checkpage.offset + checkpage.bytes)){ + // case 2 + check_start = checkpage.offset; + check_bytes = iter->bytes - (checkpage.offset - iter->offset); + + }else if((checkpage.offset + checkpage.bytes) < (iter->offset + iter->bytes)){ // here, already "iter->offset < (checkpage.offset + checkpage.bytes)" is true. + // case 3 + check_start = iter->offset; + check_bytes = checkpage.bytes - (iter->offset - checkpage.offset); + + }else if(checkpage.offset < iter->offset && (iter->offset + iter->bytes) < (checkpage.offset + checkpage.bytes)){ + // case 4 + check_start = iter->offset; + check_bytes = iter->bytes; + + }else{ // (iter->offset <= checkpage.offset && (checkpage.offset + checkpage.bytes) <= (iter->offset + iter->bytes)) + // case 5 + check_start = checkpage.offset; + check_bytes = checkpage.bytes; + } + + // check target area type + if(checkpage.loaded || checkpage.modified){ + // target area must be not HOLE(DATA) area. + if(!iter->loaded){ + // Found bad area, it is HOLE area. + fdpage page(check_start, check_bytes, false, false); + err_area_list.push_back(page); + result = false; + } + }else{ + // target area should be HOLE area.(If it is not a block boundary, it may be a DATA area.) + if(iter->loaded){ + // need to check this area's each data, it should be ZERO. + if(!PageList::CheckZeroAreaInFile(fd, check_start, static_cast(check_bytes))){ + // Discovered an area that has un-initial status data but it probably does not effect bad. + fdpage page(check_start, check_bytes, true, false); + warn_area_list.push_back(page); + result = false; + } + } + } + } + return result; +} + +//------------------------------------------------ +// PageList methods +//------------------------------------------------ +void PageList::FreeList(fdpage_list_t& list) +{ + list.clear(); +} + +PageList::PageList(off_t size, bool is_loaded, bool is_modified, bool shrinked) : is_shrink(shrinked) +{ + Init(size, is_loaded, is_modified); +} + +PageList::~PageList() +{ + Clear(); +} + +void PageList::Clear() +{ + PageList::FreeList(pages); + is_shrink = false; +} + +bool PageList::Init(off_t size, bool is_loaded, bool is_modified) +{ + Clear(); + if(0 <= size){ + fdpage page(0, size, is_loaded, is_modified); + pages.push_back(page); + } + return true; +} + +off_t PageList::Size() const +{ + if(pages.empty()){ + return 0; + } + fdpage_list_t::const_reverse_iterator riter = pages.rbegin(); + return riter->next(); +} + +bool PageList::Compress() +{ + fdpage* lastpage = nullptr; + for(fdpage_list_t::iterator iter = pages.begin(); iter != pages.end(); ){ + if(!lastpage){ + // First item + lastpage = &(*iter); + ++iter; + }else{ + // check page continuity + if(lastpage->next() != iter->offset){ + // Non-consecutive with last page, so add a page filled with default values + if(lastpage->loaded || lastpage->modified){ + // insert new page before current pos + fdpage tmppage(lastpage->next(), (iter->offset - lastpage->next()), false, false); + iter = pages.insert(iter, tmppage); + lastpage = &(*iter); + ++iter; + }else{ + // Expand last area + lastpage->bytes = iter->offset - lastpage->offset; + } + } + // check current page + if(lastpage->loaded == iter->loaded && lastpage->modified == iter->modified){ + // Expand last area and remove current pos + lastpage->bytes += iter->bytes; + iter = pages.erase(iter); + }else{ + lastpage = &(*iter); + ++iter; + } + } + } + return true; +} + +bool PageList::Parse(off_t new_pos) +{ + for(fdpage_list_t::iterator iter = pages.begin(); iter != pages.end(); ++iter){ + if(new_pos == iter->offset){ + // nothing to do + return true; + }else if(iter->offset < new_pos && new_pos < iter->next()){ + fdpage page(iter->offset, new_pos - iter->offset, iter->loaded, iter->modified); + iter->bytes -= (new_pos - iter->offset); + iter->offset = new_pos; + pages.insert(iter, page); + return true; + } + } + return false; +} + +bool PageList::Resize(off_t size, bool is_loaded, bool is_modified) +{ + off_t total = Size(); + + if(0 == total){ + // [NOTE] + // The is_shrink flag remains unchanged in this function. + // + bool backup_is_shrink = is_shrink; + + Init(size, is_loaded, is_modified); + is_shrink = backup_is_shrink; + + }else if(total < size){ + // add new area + fdpage page(total, (size - total), is_loaded, is_modified); + pages.push_back(page); + + }else if(size < total){ + // cut area + for(fdpage_list_t::iterator iter = pages.begin(); iter != pages.end(); ){ + if(iter->next() <= size){ + ++iter; + }else{ + if(size <= iter->offset){ + iter = pages.erase(iter); + }else{ + iter->bytes = size - iter->offset; + } + } + } + if(is_modified){ + is_shrink = true; + } + }else{ // total == size + // nothing to do + } + // compress area + return Compress(); +} + +bool PageList::IsPageLoaded(off_t start, off_t size) const +{ + for(fdpage_list_t::const_iterator iter = pages.begin(); iter != pages.end(); ++iter){ + if(iter->end() < start){ + continue; + } + if(!iter->loaded){ + return false; + } + if(0 != size && start + size <= iter->next()){ + break; + } + } + return true; +} + +bool PageList::SetPageLoadedStatus(off_t start, off_t size, PageList::page_status pstatus, bool is_compress) +{ + off_t now_size = Size(); + bool is_loaded = (page_status::LOAD_MODIFIED == pstatus || page_status::LOADED == pstatus); + bool is_modified = (page_status::LOAD_MODIFIED == pstatus || page_status::MODIFIED == pstatus); + + if(now_size <= start){ + if(now_size < start){ + // add + Resize(start, false, is_modified); // set modified flag from now end pos to specified start pos. + } + Resize(start + size, is_loaded, is_modified); + + }else if(now_size <= start + size){ + // cut + Resize(start, false, false); // not changed loaded/modified flags in existing area. + // add + Resize(start + size, is_loaded, is_modified); + + }else{ + // start-size are inner pages area + // parse "start", and "start + size" position + Parse(start); + Parse(start + size); + + // set loaded flag + for(fdpage_list_t::iterator iter = pages.begin(); iter != pages.end(); ++iter){ + if(iter->end() < start){ + continue; + }else if(start + size <= iter->offset){ + break; + }else{ + iter->loaded = is_loaded; + iter->modified = is_modified; + } + } + } + // compress area + return (is_compress ? Compress() : true); +} + +bool PageList::FindUnloadedPage(off_t start, off_t& resstart, off_t& ressize) const +{ + for(fdpage_list_t::const_iterator iter = pages.begin(); iter != pages.end(); ++iter){ + if(start <= iter->end()){ + if(!iter->loaded && !iter->modified){ // Do not load unloaded and modified areas + resstart = iter->offset; + ressize = iter->bytes; + return true; + } + } + } + return false; +} + +// [NOTE] +// Accumulates the range of unload that is smaller than the Limit size. +// If you want to integrate all unload ranges, set the limit size to 0. +// +off_t PageList::GetTotalUnloadedPageSize(off_t start, off_t size, off_t limit_size) const +{ + // If size is 0, it means loading to end. + if(0 == size){ + if(start < Size()){ + size = Size() - start; + } + } + off_t next = start + size; + off_t restsize = 0; + for(fdpage_list_t::const_iterator iter = pages.begin(); iter != pages.end(); ++iter){ + if(iter->next() <= start){ + continue; + } + if(next <= iter->offset){ + break; + } + if(iter->loaded || iter->modified){ + continue; + } + off_t tmpsize; + if(iter->offset <= start){ + if(iter->next() <= next){ + tmpsize = (iter->next() - start); + }else{ + tmpsize = next - start; // = size + } + }else{ + if(iter->next() <= next){ + tmpsize = iter->next() - iter->offset; // = iter->bytes + }else{ + tmpsize = next - iter->offset; + } + } + if(0 == limit_size || tmpsize < limit_size){ + restsize += tmpsize; + } + } + return restsize; +} + +size_t PageList::GetUnloadedPages(fdpage_list_t& unloaded_list, off_t start, off_t size) const +{ + // If size is 0, it means loading to end. + if(0 == size){ + if(start < Size()){ + size = Size() - start; + } + } + off_t next = start + size; + + for(fdpage_list_t::const_iterator iter = pages.begin(); iter != pages.end(); ++iter){ + if(iter->next() <= start){ + continue; + } + if(next <= iter->offset){ + break; + } + if(iter->loaded || iter->modified){ + continue; // already loaded or modified + } + + // page area + off_t page_start = std::max(iter->offset, start); + off_t page_next = std::min(iter->next(), next); + off_t page_size = page_next - page_start; + + // add list + fdpage_list_t::reverse_iterator riter = unloaded_list.rbegin(); + if(riter != unloaded_list.rend() && riter->next() == page_start){ + // merge to before page + riter->bytes += page_size; + }else{ + fdpage page(page_start, page_size, false, false); + unloaded_list.push_back(page); + } + } + return unloaded_list.size(); +} + +// [NOTE] +// This method is called in advance when mixing POST and COPY in multi-part upload. +// The minimum size of each part must be 5 MB, and the data area below this must be +// downloaded from S3. +// This method checks the current PageList status and returns the area that needs +// to be downloaded so that each part is at least 5 MB. +// +bool PageList::GetPageListsForMultipartUpload(fdpage_list_t& dlpages, fdpage_list_t& mixuppages, off_t max_partsize) +{ + // compress before this processing + Compress(); // always true + + // make a list by modified flag + fdpage_list_t modified_pages; + fdpage_list_t download_pages; // A non-contiguous page list showing the areas that need to be downloaded + fdpage_list_t mixupload_pages; // A continuous page list showing only modified flags for mixupload + compress_fdpage_list_ignore_load(pages, modified_pages, false); + + fdpage prev_page; + for(fdpage_list_t::const_iterator iter = modified_pages.begin(); iter != modified_pages.end(); ++iter){ + if(iter->modified){ + // current is modified area + if(!prev_page.modified){ + // previous is not modified area + if(prev_page.bytes < MIN_MULTIPART_SIZE){ + // previous(not modified) area is too small for one multipart size, + // then all of previous area is needed to download. + download_pages.push_back(prev_page); + + // previous(not modified) area is set upload area. + prev_page.modified = true; + mixupload_pages.push_back(prev_page); + }else{ + // previous(not modified) area is set copy area. + prev_page.modified = false; + mixupload_pages.push_back(prev_page); + } + // set current to previous + prev_page = *iter; + }else{ + // previous is modified area, too + prev_page.bytes += iter->bytes; + } + + }else{ + // current is not modified area + if(!prev_page.modified){ + // previous is not modified area, too + prev_page.bytes += iter->bytes; + + }else{ + // previous is modified area + if(prev_page.bytes < MIN_MULTIPART_SIZE){ + // previous(modified) area is too small for one multipart size, + // then part or all of current area is needed to download. + off_t missing_bytes = MIN_MULTIPART_SIZE - prev_page.bytes; + + if((missing_bytes + MIN_MULTIPART_SIZE) < iter-> bytes){ + // The current size is larger than the missing size, and the remainder + // after deducting the missing size is larger than the minimum size. + + fdpage missing_page(iter->offset, missing_bytes, false, false); + download_pages.push_back(missing_page); + + // previous(not modified) area is set upload area. + prev_page.bytes = MIN_MULTIPART_SIZE; + mixupload_pages.push_back(prev_page); + + // set current to previous + prev_page = *iter; + prev_page.offset += missing_bytes; + prev_page.bytes -= missing_bytes; + + }else{ + // The current size is less than the missing size, or the remaining + // size less the missing size is less than the minimum size. + download_pages.push_back(*iter); + + // add current to previous + prev_page.bytes += iter->bytes; + } + + }else{ + // previous(modified) area is enough size for one multipart size. + mixupload_pages.push_back(prev_page); + + // set current to previous + prev_page = *iter; + } + } + } + } + // last area + if(0 < prev_page.bytes){ + mixupload_pages.push_back(prev_page); + } + + // compress + compress_fdpage_list_ignore_modify(download_pages, dlpages, false); + compress_fdpage_list_ignore_load(mixupload_pages, mixuppages, false); + + // parse by max pagesize + dlpages = parse_partsize_fdpage_list(dlpages, max_partsize); + mixuppages = parse_partsize_fdpage_list(mixuppages, max_partsize); + + return true; +} + +bool PageList::GetNoDataPageLists(fdpage_list_t& nodata_pages, off_t start, size_t size) +{ + // compress before this processing + Compress(); // always true + + // extract areas without data + fdpage_list_t tmp_pagelist; + off_t stop_pos = (0L == size ? -1 : (start + size)); + for(fdpage_list_t::const_iterator iter = pages.begin(); iter != pages.end(); ++iter){ + if((iter->offset + iter->bytes) < start){ + continue; + } + if(-1 != stop_pos && stop_pos <= iter->offset){ + break; + } + if(iter->modified){ + continue; + } + + fdpage tmppage; + tmppage.offset = std::max(iter->offset, start); + tmppage.bytes = (-1 == stop_pos ? iter->bytes : std::min(iter->bytes, (stop_pos - tmppage.offset))); + tmppage.loaded = iter->loaded; + tmppage.modified = iter->modified; + + tmp_pagelist.push_back(tmppage); + } + + if(tmp_pagelist.empty()){ + nodata_pages.clear(); + }else{ + // compress + compress_fdpage_list(tmp_pagelist, nodata_pages); + } + return true; +} + +off_t PageList::BytesModified() const +{ + off_t total = 0; + for(fdpage_list_t::const_iterator iter = pages.begin(); iter != pages.end(); ++iter){ + if(iter->modified){ + total += iter->bytes; + } + } + return total; +} + +bool PageList::IsModified() const +{ + if(is_shrink){ + return true; + } + for(fdpage_list_t::const_iterator iter = pages.begin(); iter != pages.end(); ++iter){ + if(iter->modified){ + return true; + } + } + return false; +} + +bool PageList::ClearAllModified() +{ + is_shrink = false; + + for(fdpage_list_t::iterator iter = pages.begin(); iter != pages.end(); ++iter){ + if(iter->modified){ + iter->modified = false; + } + } + return Compress(); +} + +bool PageList::Serialize(CacheFileStat& file, bool is_output, ino_t inode) +{ + if(!file.Open()){ + return false; + } + if(is_output){ + // + // put to file + // + std::ostringstream ssall; + ssall << inode << ":" << Size(); + + for(fdpage_list_t::iterator iter = pages.begin(); iter != pages.end(); ++iter){ + ssall << "\n" << iter->offset << ":" << iter->bytes << ":" << (iter->loaded ? "1" : "0") << ":" << (iter->modified ? "1" : "0"); + } + + if(-1 == ftruncate(file.GetFd(), 0)){ + S3FS_PRN_ERR("failed to truncate file(to 0) for stats(%d)", errno); + return false; + } + std::string strall = ssall.str(); + if(0 >= pwrite(file.GetFd(), strall.c_str(), strall.length(), 0)){ + S3FS_PRN_ERR("failed to write stats(%d)", errno); + return false; + } + + }else{ + // + // loading from file + // + struct stat st; + memset(&st, 0, sizeof(struct stat)); + if(-1 == fstat(file.GetFd(), &st)){ + S3FS_PRN_ERR("fstat is failed. errno(%d)", errno); + return false; + } + if(0 >= st.st_size){ + // nothing + Init(0, false, false); + return true; + } + std::unique_ptr ptmp(new char[st.st_size + 1]); + ssize_t result; + // read from file + if(0 >= (result = pread(file.GetFd(), ptmp.get(), st.st_size, 0))){ + S3FS_PRN_ERR("failed to read stats(%d)", errno); + return false; + } + ptmp[result] = '\0'; + std::string oneline; + std::istringstream ssall(ptmp.get()); + + // loaded + Clear(); + + // load head line(for size and inode) + off_t total; + ino_t cache_inode; // if this value is 0, it means old format. + if(!getline(ssall, oneline, '\n')){ + S3FS_PRN_ERR("failed to parse stats."); + return false; + }else{ + std::istringstream sshead(oneline); + std::string strhead1; + std::string strhead2; + + // get first part in head line. + if(!getline(sshead, strhead1, ':')){ + S3FS_PRN_ERR("failed to parse stats."); + return false; + } + // get second part in head line. + if(!getline(sshead, strhead2, ':')){ + // old head format is "\n" + total = cvt_strtoofft(strhead1.c_str(), /* base= */10); + cache_inode = 0; + }else{ + // current head format is ":\n" + total = cvt_strtoofft(strhead2.c_str(), /* base= */10); + cache_inode = static_cast(cvt_strtoofft(strhead1.c_str(), /* base= */10)); + if(0 == cache_inode){ + S3FS_PRN_ERR("wrong inode number in parsed cache stats."); + return false; + } + } + } + // check inode number + if(0 != cache_inode && cache_inode != inode){ + S3FS_PRN_ERR("differ inode and inode number in parsed cache stats."); + return false; + } + + // load each part + bool is_err = false; + while(getline(ssall, oneline, '\n')){ + std::string part; + std::istringstream ssparts(oneline); + // offset + if(!getline(ssparts, part, ':')){ + is_err = true; + break; + } + off_t offset = cvt_strtoofft(part.c_str(), /* base= */10); + // size + if(!getline(ssparts, part, ':')){ + is_err = true; + break; + } + off_t size = cvt_strtoofft(part.c_str(), /* base= */10); + // loaded + if(!getline(ssparts, part, ':')){ + is_err = true; + break; + } + bool is_loaded = (1 == cvt_strtoofft(part.c_str(), /* base= */10) ? true : false); + bool is_modified; + if(!getline(ssparts, part, ':')){ + is_modified = false; // old version does not have this part. + }else{ + is_modified = (1 == cvt_strtoofft(part.c_str(), /* base= */10) ? true : false); + } + // add new area + PageList::page_status pstatus = PageList::page_status::NOT_LOAD_MODIFIED; + if(is_loaded){ + if(is_modified){ + pstatus = PageList::page_status::LOAD_MODIFIED; + }else{ + pstatus = PageList::page_status::LOADED; + } + }else{ + if(is_modified){ + pstatus = PageList::page_status::MODIFIED; + } + } + SetPageLoadedStatus(offset, size, pstatus); + } + if(is_err){ + S3FS_PRN_ERR("failed to parse stats."); + Clear(); + return false; + } + + // check size + if(total != Size()){ + S3FS_PRN_ERR("different size(%lld - %lld).", static_cast(total), static_cast(Size())); + Clear(); + return false; + } + } + return true; +} + +void PageList::Dump() const +{ + int cnt = 0; + + S3FS_PRN_DBG("pages (shrinked=%s) = {", (is_shrink ? "yes" : "no")); + for(fdpage_list_t::const_iterator iter = pages.begin(); iter != pages.end(); ++iter, ++cnt){ + S3FS_PRN_DBG(" [%08d] -> {%014lld - %014lld : %s / %s}", cnt, static_cast(iter->offset), static_cast(iter->bytes), iter->loaded ? "loaded" : "unloaded", iter->modified ? "modified" : "not modified"); + } + S3FS_PRN_DBG("}"); +} + +// +// Compare the fdpage_list_t pages of the object with the state of the file. +// +// The loaded=true or modified=true area of pages must be a DATA block +// (not a HOLE block) in the file. +// The other area is a HOLE block in the file or is a DATA block(but the +// data of the target area in that block should be ZERO). +// If it is a bad area in the previous case, it will be reported as an error. +// If the latter case does not match, it will be reported as a warning. +// +bool PageList::CompareSparseFile(int fd, size_t file_size, fdpage_list_t& err_area_list, fdpage_list_t& warn_area_list) +{ + err_area_list.clear(); + warn_area_list.clear(); + + // First, list the block disk allocation area of the cache file. + // The cache file has holes(sparse file) and no disk block areas + // are assigned to any holes. + fdpage_list_t sparse_list; + if(!PageList::GetSparseFilePages(fd, file_size, sparse_list)){ + S3FS_PRN_ERR("Something error is occurred in parsing hole/data of the cache file(physical_fd=%d).", fd); + + fdpage page(0, static_cast(file_size), false, false); + err_area_list.push_back(page); + + return false; + } + + if(sparse_list.empty() && pages.empty()){ + // both file and stats information are empty, it means cache file size is ZERO. + return true; + } + + // Compare each pages and sparse_list + bool result = true; + for(fdpage_list_t::const_iterator iter = pages.begin(); iter != pages.end(); ++iter){ + if(!PageList::CheckAreaInSparseFile(*iter, sparse_list, fd, err_area_list, warn_area_list)){ + result = false; + } + } + return result; +} + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/fdcache_page.h b/s3fs/fdcache_page.h new file mode 100644 index 0000000..f4ef8c2 --- /dev/null +++ b/s3fs/fdcache_page.h @@ -0,0 +1,136 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef S3FS_FDCACHE_PAGE_H_ +#define S3FS_FDCACHE_PAGE_H_ + +#include +#include + +//------------------------------------------------ +// Symbols +//------------------------------------------------ +// [NOTE] +// If the following symbols in lseek whence are undefined, define them. +// If it is not supported by lseek, s3fs judges by the processing result of lseek. +// +#ifndef SEEK_DATA +#define SEEK_DATA 3 +#endif +#ifndef SEEK_HOLE +#define SEEK_HOLE 4 +#endif + +//------------------------------------------------ +// Structure fdpage +//------------------------------------------------ +// page block information +struct fdpage +{ + off_t offset; + off_t bytes; + bool loaded; + bool modified; + + explicit fdpage(off_t start = 0, off_t size = 0, bool is_loaded = false, bool is_modified = false) : + offset(start), bytes(size), loaded(is_loaded), modified(is_modified) {} + + off_t next() const + { + return (offset + bytes); + } + off_t end() const + { + return (0 < bytes ? offset + bytes - 1 : 0); + } +}; +typedef std::vector fdpage_list_t; + +//------------------------------------------------ +// Class PageList +//------------------------------------------------ +class CacheFileStat; +class FdEntity; + +// cppcheck-suppress copyCtorAndEqOperator +class PageList +{ + friend class FdEntity; // only one method access directly pages. + + private: + fdpage_list_t pages; + bool is_shrink; // [NOTE] true if it has been shrinked even once + + public: + enum class page_status{ + NOT_LOAD_MODIFIED = 0, + LOADED, + MODIFIED, + LOAD_MODIFIED + }; + + private: + static bool GetSparseFilePages(int fd, size_t file_size, fdpage_list_t& sparse_list); + static bool CheckZeroAreaInFile(int fd, off_t start, size_t bytes); + static bool CheckAreaInSparseFile(const struct fdpage& checkpage, const fdpage_list_t& sparse_list, int fd, fdpage_list_t& err_area_list, fdpage_list_t& warn_area_list); + + void Clear(); + bool Parse(off_t new_pos); + + public: + static void FreeList(fdpage_list_t& list); + + explicit PageList(off_t size = 0, bool is_loaded = false, bool is_modified = false, bool shrinked = false); + PageList(const PageList&) = delete; + PageList& operator=(const PageList&) = delete; + ~PageList(); + + bool Init(off_t size, bool is_loaded, bool is_modified); + off_t Size() const; + bool Resize(off_t size, bool is_loaded, bool is_modified); + + bool IsPageLoaded(off_t start = 0, off_t size = 0) const; // size=0 is checking to end of list + bool SetPageLoadedStatus(off_t start, off_t size, PageList::page_status pstatus = page_status::LOADED, bool is_compress = true); + bool FindUnloadedPage(off_t start, off_t& resstart, off_t& ressize) const; + off_t GetTotalUnloadedPageSize(off_t start = 0, off_t size = 0, off_t limit_size = 0) const; // size=0 is checking to end of list + size_t GetUnloadedPages(fdpage_list_t& unloaded_list, off_t start = 0, off_t size = 0) const; // size=0 is checking to end of list + bool GetPageListsForMultipartUpload(fdpage_list_t& dlpages, fdpage_list_t& mixuppages, off_t max_partsize); + bool GetNoDataPageLists(fdpage_list_t& nodata_pages, off_t start = 0, size_t size = 0); + + off_t BytesModified() const; + bool IsModified() const; + bool ClearAllModified(); + + bool Compress(); + bool Serialize(CacheFileStat& file, bool is_output, ino_t inode); + void Dump() const; + bool CompareSparseFile(int fd, size_t file_size, fdpage_list_t& err_area_list, fdpage_list_t& warn_area_list); +}; + +#endif // S3FS_FDCACHE_PAGE_H_ + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/fdcache_pseudofd.cpp b/s3fs/fdcache_pseudofd.cpp new file mode 100644 index 0000000..711cada --- /dev/null +++ b/s3fs/fdcache_pseudofd.cpp @@ -0,0 +1,133 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Takeshi Nakatani + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include +#include + +#include "s3fs_logger.h" +#include "fdcache_pseudofd.h" +#include "autolock.h" + +//------------------------------------------------ +// Symbols +//------------------------------------------------ +// [NOTE] +// The minimum pseudo fd value starts 2. +// This is to avoid mistakes for 0(stdout) and 1(stderr), which are usually used. +// +static constexpr int MIN_PSEUDOFD_NUMBER = 2; + +//------------------------------------------------ +// PseudoFdManager class methods +//------------------------------------------------ +PseudoFdManager& PseudoFdManager::GetManager() +{ + static PseudoFdManager singleton; + return singleton; +} + +int PseudoFdManager::Get() +{ + return (PseudoFdManager::GetManager()).CreatePseudoFd(); +} + +bool PseudoFdManager::Release(int fd) +{ + return (PseudoFdManager::GetManager()).ReleasePseudoFd(fd); +} + +//------------------------------------------------ +// PseudoFdManager methods +//------------------------------------------------ +PseudoFdManager::PseudoFdManager() : is_lock_init(false) +{ + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); +#if S3FS_PTHREAD_ERRORCHECK + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK); +#endif + int result; + if(0 != (result = pthread_mutex_init(&pseudofd_list_lock, &attr))){ + S3FS_PRN_CRIT("failed to init pseudofd_list_lock: %d", result); + abort(); + } + is_lock_init = true; +} + +PseudoFdManager::~PseudoFdManager() +{ + if(is_lock_init){ + int result; + if(0 != (result = pthread_mutex_destroy(&pseudofd_list_lock))){ + S3FS_PRN_CRIT("failed to destroy pseudofd_list_lock: %d", result); + abort(); + } + is_lock_init = false; + } +} + +int PseudoFdManager::GetUnusedMinPseudoFd() const +{ + int min_fd = MIN_PSEUDOFD_NUMBER; + + // Look for the first discontinuous value. + for(pseudofd_list_t::const_iterator iter = pseudofd_list.begin(); iter != pseudofd_list.end(); ++iter){ + if(min_fd == (*iter)){ + ++min_fd; + }else if(min_fd < (*iter)){ + break; + } + } + return min_fd; +} + +int PseudoFdManager::CreatePseudoFd() +{ + AutoLock auto_lock(&pseudofd_list_lock); + + int new_fd = PseudoFdManager::GetUnusedMinPseudoFd(); + pseudofd_list.push_back(new_fd); + std::sort(pseudofd_list.begin(), pseudofd_list.end()); + + return new_fd; +} + +bool PseudoFdManager::ReleasePseudoFd(int fd) +{ + AutoLock auto_lock(&pseudofd_list_lock); + + for(pseudofd_list_t::iterator iter = pseudofd_list.begin(); iter != pseudofd_list.end(); ++iter){ + if(fd == (*iter)){ + pseudofd_list.erase(iter); + return true; + } + } + return false; +} + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/fdcache_pseudofd.h b/s3fs/fdcache_pseudofd.h new file mode 100644 index 0000000..1025264 --- /dev/null +++ b/s3fs/fdcache_pseudofd.h @@ -0,0 +1,71 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef S3FS_FDCACHE_PSEUDOFD_H_ +#define S3FS_FDCACHE_PSEUDOFD_H_ + +#include + +//------------------------------------------------ +// Typdefs +//------------------------------------------------ +// List of pseudo fd in use +// +typedef std::vector pseudofd_list_t; + +//------------------------------------------------ +// Class PseudoFdManager +//------------------------------------------------ +class PseudoFdManager +{ + private: + pseudofd_list_t pseudofd_list; + bool is_lock_init; + pthread_mutex_t pseudofd_list_lock; // protects pseudofd_list + + private: + static PseudoFdManager& GetManager(); + + PseudoFdManager(); + ~PseudoFdManager(); + PseudoFdManager(const PseudoFdManager&) = delete; + PseudoFdManager(PseudoFdManager&&) = delete; + PseudoFdManager& operator=(const PseudoFdManager&) = delete; + PseudoFdManager& operator=(PseudoFdManager&&) = delete; + + int GetUnusedMinPseudoFd() const; + int CreatePseudoFd(); + bool ReleasePseudoFd(int fd); + + public: + static int Get(); + static bool Release(int fd); +}; + +#endif // S3FS_FDCACHE_PSEUDOFD_H_ + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/fdcache_stat.cpp b/s3fs/fdcache_stat.cpp new file mode 100644 index 0000000..c337409 --- /dev/null +++ b/s3fs/fdcache_stat.cpp @@ -0,0 +1,282 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Takeshi Nakatani + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include +#include +#include + +#include "s3fs_logger.h" +#include "fdcache_stat.h" +#include "fdcache.h" +#include "s3fs_util.h" +#include "s3fs_cred.h" +#include "string_util.h" + +//------------------------------------------------ +// CacheFileStat class methods +//------------------------------------------------ +std::string CacheFileStat::GetCacheFileStatTopDir() +{ + std::string top_path; + if(!FdManager::IsCacheDir() || S3fsCred::GetBucket().empty()){ + return top_path; + } + + // stat top dir( "//..stat" ) + top_path += FdManager::GetCacheDir(); + top_path += "/."; + top_path += S3fsCred::GetBucket(); + top_path += ".stat"; + return top_path; +} + +int CacheFileStat::MakeCacheFileStatPath(const char* path, std::string& sfile_path, bool is_create_dir) +{ + std::string top_path = CacheFileStat::GetCacheFileStatTopDir(); + if(top_path.empty()){ + S3FS_PRN_ERR("The path to cache top dir is empty."); + return -EIO; + } + + if(is_create_dir){ + int result; + if(0 != (result = mkdirp(top_path + mydirname(path), 0777))){ + S3FS_PRN_ERR("failed to create dir(%s) by errno(%d).", path, result); + return result; + } + } + if(!path || '\0' == path[0]){ + sfile_path = top_path; + }else{ + sfile_path = top_path + SAFESTRPTR(path); + } + return 0; +} + +bool CacheFileStat::CheckCacheFileStatTopDir() +{ + std::string top_path = CacheFileStat::GetCacheFileStatTopDir(); + if(top_path.empty()){ + S3FS_PRN_INFO("The path to cache top dir is empty, thus not need to check permission."); + return true; + } + + return check_exist_dir_permission(top_path.c_str()); +} + +int CacheFileStat::DeleteCacheFileStat(const char* path) +{ + if(!path || '\0' == path[0]){ + return -EINVAL; + } + // stat path + std::string sfile_path; + int result; + if(0 != (result = CacheFileStat::MakeCacheFileStatPath(path, sfile_path, false))){ + S3FS_PRN_ERR("failed to create cache stat file path(%s)", path); + return result; + } + if(0 != unlink(sfile_path.c_str())){ + result = -errno; + if(-ENOENT == result){ + S3FS_PRN_DBG("failed to delete file(%s): errno=%d", path, result); + }else{ + S3FS_PRN_ERR("failed to delete file(%s): errno=%d", path, result); + } + return result; + } + return 0; +} + +// [NOTE] +// If remove stat file directory, it should do before removing +// file cache directory. +// +bool CacheFileStat::DeleteCacheFileStatDirectory() +{ + std::string top_path = CacheFileStat::GetCacheFileStatTopDir(); + if(top_path.empty()){ + S3FS_PRN_INFO("The path to cache top dir is empty, thus not need to remove it."); + return true; + } + return delete_files_in_dir(top_path.c_str(), true); +} + +bool CacheFileStat::RenameCacheFileStat(const char* oldpath, const char* newpath) +{ + if(!oldpath || '\0' == oldpath[0] || !newpath || '\0' == newpath[0]){ + return false; + } + + // stat path + std::string old_filestat; + std::string new_filestat; + if(0 != CacheFileStat::MakeCacheFileStatPath(oldpath, old_filestat, false) || 0 != CacheFileStat::MakeCacheFileStatPath(newpath, new_filestat, false)){ + return false; + } + + // check new stat path + struct stat st; + if(0 == stat(new_filestat.c_str(), &st)){ + // new stat path is existed, then unlink it. + if(-1 == unlink(new_filestat.c_str())){ + S3FS_PRN_ERR("failed to unlink new cache file stat path(%s) by errno(%d).", new_filestat.c_str(), errno); + return false; + } + } + + // check old stat path + if(0 != stat(old_filestat.c_str(), &st)){ + // old stat path is not existed, then nothing to do any more. + return true; + } + + // link and unlink + if(-1 == link(old_filestat.c_str(), new_filestat.c_str())){ + S3FS_PRN_ERR("failed to link old cache file stat path(%s) to new cache file stat path(%s) by errno(%d).", old_filestat.c_str(), new_filestat.c_str(), errno); + return false; + } + if(-1 == unlink(old_filestat.c_str())){ + S3FS_PRN_ERR("failed to unlink old cache file stat path(%s) by errno(%d).", old_filestat.c_str(), errno); + return false; + } + return true; +} + +//------------------------------------------------ +// CacheFileStat methods +//------------------------------------------------ +CacheFileStat::CacheFileStat(const char* tpath) : fd(-1) +{ + if(tpath && '\0' != tpath[0]){ + SetPath(tpath, true); + } +} + +CacheFileStat::~CacheFileStat() +{ + Release(); +} + +bool CacheFileStat::SetPath(const char* tpath, bool is_open) +{ + if(!tpath || '\0' == tpath[0]){ + return false; + } + if(!Release()){ + // could not close old stat file. + return false; + } + path = tpath; + if(!is_open){ + return true; + } + return Open(); +} + +bool CacheFileStat::RawOpen(bool readonly) +{ + if(path.empty()){ + return false; + } + if(-1 != fd){ + // already opened + return true; + } + // stat path + std::string sfile_path; + if(0 != CacheFileStat::MakeCacheFileStatPath(path.c_str(), sfile_path, true)){ + S3FS_PRN_ERR("failed to create cache stat file path(%s)", path.c_str()); + return false; + } + // open + int tmpfd; + if(readonly){ + if(-1 == (tmpfd = open(sfile_path.c_str(), O_RDONLY))){ + S3FS_PRN_ERR("failed to read only open cache stat file path(%s) - errno(%d)", path.c_str(), errno); + return false; + } + }else{ + if(-1 == (tmpfd = open(sfile_path.c_str(), O_CREAT|O_RDWR, 0600))){ + S3FS_PRN_ERR("failed to open cache stat file path(%s) - errno(%d)", path.c_str(), errno); + return false; + } + } + scope_guard guard([&]() { close(tmpfd); }); + + // lock + if(-1 == flock(tmpfd, LOCK_EX)){ + S3FS_PRN_ERR("failed to lock cache stat file(%s) - errno(%d)", path.c_str(), errno); + return false; + } + // seek top + if(0 != lseek(tmpfd, 0, SEEK_SET)){ + S3FS_PRN_ERR("failed to lseek cache stat file(%s) - errno(%d)", path.c_str(), errno); + flock(tmpfd, LOCK_UN); + return false; + } + S3FS_PRN_DBG("file locked(%s - %s)", path.c_str(), sfile_path.c_str()); + + guard.dismiss(); + fd = tmpfd; + return true; +} + +bool CacheFileStat::Open() +{ + return RawOpen(false); +} + +bool CacheFileStat::ReadOnlyOpen() +{ + return RawOpen(true); +} + +bool CacheFileStat::Release() +{ + if(-1 == fd){ + // already release + return true; + } + // unlock + if(-1 == flock(fd, LOCK_UN)){ + S3FS_PRN_ERR("failed to unlock cache stat file(%s) - errno(%d)", path.c_str(), errno); + return false; + } + S3FS_PRN_DBG("file unlocked(%s)", path.c_str()); + + if(-1 == close(fd)){ + S3FS_PRN_ERR("failed to close cache stat file(%s) - errno(%d)", path.c_str(), errno); + return false; + } + fd = -1; + + return true; +} + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/fdcache_stat.h b/s3fs/fdcache_stat.h new file mode 100644 index 0000000..3ad476b --- /dev/null +++ b/s3fs/fdcache_stat.h @@ -0,0 +1,66 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef S3FS_FDCACHE_STAT_H_ +#define S3FS_FDCACHE_STAT_H_ + +#include + +//------------------------------------------------ +// CacheFileStat +//------------------------------------------------ +class CacheFileStat +{ + private: + std::string path; + int fd; + + private: + static int MakeCacheFileStatPath(const char* path, std::string& sfile_path, bool is_create_dir = true); + + bool RawOpen(bool readonly); + + public: + static std::string GetCacheFileStatTopDir(); + static int DeleteCacheFileStat(const char* path); + static bool CheckCacheFileStatTopDir(); + static bool DeleteCacheFileStatDirectory(); + static bool RenameCacheFileStat(const char* oldpath, const char* newpath); + + explicit CacheFileStat(const char* tpath = nullptr); + ~CacheFileStat(); + + bool Open(); + bool ReadOnlyOpen(); + bool Release(); + bool SetPath(const char* tpath, bool is_open = true); + int GetFd() const { return fd; } +}; + +#endif // S3FS_FDCACHE_STAT_H_ + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/fdcache_untreated.cpp b/s3fs/fdcache_untreated.cpp new file mode 100644 index 0000000..dcba302 --- /dev/null +++ b/s3fs/fdcache_untreated.cpp @@ -0,0 +1,277 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Takeshi Nakatani + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include + +#include "s3fs_logger.h" +#include "fdcache_untreated.h" +#include "autolock.h" + +//------------------------------------------------ +// UntreatedParts methods +//------------------------------------------------ +UntreatedParts::UntreatedParts() : last_tag(0) //, is_lock_init(false) +{ + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); +#if S3FS_PTHREAD_ERRORCHECK + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK); +#endif + + int result; + if(0 != (result = pthread_mutex_init(&untreated_list_lock, &attr))){ + S3FS_PRN_CRIT("failed to init untreated_list_lock: %d", result); + abort(); + } + is_lock_init = true; +} + +UntreatedParts::~UntreatedParts() +{ + if(is_lock_init){ + int result; + if(0 != (result = pthread_mutex_destroy(&untreated_list_lock))){ + S3FS_PRN_CRIT("failed to destroy untreated_list_lock: %d", result); + abort(); + } + is_lock_init = false; + } +} + +bool UntreatedParts::empty() +{ + AutoLock auto_lock(&untreated_list_lock); + return untreated_list.empty(); +} + +bool UntreatedParts::AddPart(off_t start, off_t size) +{ + if(start < 0 || size <= 0){ + S3FS_PRN_ERR("Paramter are wrong(start=%lld, size=%lld).", static_cast(start), static_cast(size)); + return false; + } + AutoLock auto_lock(&untreated_list_lock); + + ++last_tag; + + // Check the overlap with the existing part and add the part. + for(untreated_list_t::iterator iter = untreated_list.begin(); iter != untreated_list.end(); ++iter){ + if(iter->stretch(start, size, last_tag)){ + // the part was stretched, thus check if it overlaps with next parts + untreated_list_t::iterator niter = iter; + for(++niter; niter != untreated_list.end(); ){ + if(!iter->stretch(niter->start, niter->size, last_tag)){ + // This next part does not overlap with the current part + break; + } + // Since the parts overlap and the current part is stretched, delete this next part. + niter = untreated_list.erase(niter); + } + // success to stretch and compress existed parts + return true; + + }else if((start + size) < iter->start){ + // The part to add should be inserted before the current part. + untreated_list.insert(iter, untreatedpart(start, size, last_tag)); + // success to stretch and compress existed parts + return true; + } + } + // There are no overlapping parts in the untreated_list, then add the part at end of list + untreated_list.emplace_back(start, size, last_tag); + return true; +} + +bool UntreatedParts::RowGetPart(off_t& start, off_t& size, off_t max_size, off_t min_size, bool lastpart) const +{ + if(max_size <= 0 || min_size < 0 || max_size < min_size){ + S3FS_PRN_ERR("Paramter are wrong(max_size=%lld, min_size=%lld).", static_cast(max_size), static_cast(min_size)); + return false; + } + AutoLock auto_lock(&untreated_list_lock); + + // Check the overlap with the existing part and add the part. + for(untreated_list_t::const_iterator iter = untreated_list.begin(); iter != untreated_list.end(); ++iter){ + if(!lastpart || iter->untreated_tag == last_tag){ + if(min_size <= iter->size){ + if(iter->size <= max_size){ + // whole part( min <= part size <= max ) + start = iter->start; + size = iter->size; + }else{ + // Partially take out part( max < part size ) + start = iter->start; + size = max_size; + } + return true; + }else{ + if(lastpart){ + return false; + } + } + } + } + return false; +} + +// [NOTE] +// If size is specified as 0, all areas(parts) after start will be deleted. +// +bool UntreatedParts::ClearParts(off_t start, off_t size) +{ + if(start < 0 || size < 0){ + S3FS_PRN_ERR("Paramter are wrong(start=%lld, size=%lld).", static_cast(start), static_cast(size)); + return false; + } + AutoLock auto_lock(&untreated_list_lock); + + if(untreated_list.empty()){ + return true; + } + + // Check the overlap with the existing part. + for(untreated_list_t::iterator iter = untreated_list.begin(); iter != untreated_list.end(); ){ + if(0 != size && (start + size) <= iter->start){ + // clear area is in front of iter area, no more to do. + break; + }else if(start <= iter->start){ + if(0 != size && (start + size) <= (iter->start + iter->size)){ + // clear area overlaps with iter area(on the start side) + iter->size = (iter->start + iter->size) - (start + size); + iter->start = start + size; + if(0 == iter->size){ + iter = untreated_list.erase(iter); + } + }else{ + // clear area overlaps with all of iter area + iter = untreated_list.erase(iter); + } + }else if(start < (iter->start + iter->size)){ + // clear area overlaps with iter area(on the end side) + if(0 == size || (iter->start + iter->size) <= (start + size)){ + // start to iter->end is clear + iter->size = start - iter->start; + }else{ + // parse current part + iter->size = start - iter->start; + + // add new part + off_t next_start = start + size; + off_t next_size = (iter->start + iter->size) - (start + size); + long next_tag = iter->untreated_tag; + ++iter; + iter = untreated_list.insert(iter, untreatedpart(next_start, next_size, next_tag)); + ++iter; + } + }else{ + // clear area is in behind of iter area + ++iter; + } + } + return true; +} + +// +// Update the last updated Untreated part +// +bool UntreatedParts::GetLastUpdatePart(off_t& start, off_t& size) const +{ + AutoLock auto_lock(&untreated_list_lock); + + for(untreated_list_t::const_iterator iter = untreated_list.begin(); iter != untreated_list.end(); ++iter){ + if(iter->untreated_tag == last_tag){ + start = iter->start; + size = iter->size; + return true; + } + } + return false; +} + +// +// Replaces the last updated Untreated part. +// +// [NOTE] +// If size <= 0, delete that part +// +bool UntreatedParts::ReplaceLastUpdatePart(off_t start, off_t size) +{ + AutoLock auto_lock(&untreated_list_lock); + + for(untreated_list_t::iterator iter = untreated_list.begin(); iter != untreated_list.end(); ++iter){ + if(iter->untreated_tag == last_tag){ + if(0 < size){ + iter->start = start; + iter->size = size; + }else{ + iter = untreated_list.erase(iter); + } + return true; + } + } + return false; +} + +// +// Remove the last updated Untreated part. +// +bool UntreatedParts::RemoveLastUpdatePart() +{ + AutoLock auto_lock(&untreated_list_lock); + + for(untreated_list_t::iterator iter = untreated_list.begin(); iter != untreated_list.end(); ++iter){ + if(iter->untreated_tag == last_tag){ + untreated_list.erase(iter); + return true; + } + } + return false; +} + +// +// Duplicate the internally untreated_list. +// +bool UntreatedParts::Duplicate(untreated_list_t& list) +{ + AutoLock auto_lock(&untreated_list_lock); + + list = untreated_list; + return true; +} + +void UntreatedParts::Dump() +{ + AutoLock auto_lock(&untreated_list_lock); + + S3FS_PRN_DBG("untreated list = ["); + for(untreated_list_t::const_iterator iter = untreated_list.begin(); iter != untreated_list.end(); ++iter){ + S3FS_PRN_DBG(" {%014lld - %014lld : tag=%ld}", static_cast(iter->start), static_cast(iter->size), iter->untreated_tag); + } + S3FS_PRN_DBG("]"); +} + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/fdcache_untreated.h b/s3fs/fdcache_untreated.h new file mode 100644 index 0000000..8e55afe --- /dev/null +++ b/s3fs/fdcache_untreated.h @@ -0,0 +1,76 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef S3FS_FDCACHE_UNTREATED_H_ +#define S3FS_FDCACHE_UNTREATED_H_ + +#include "common.h" +#include "types.h" + +//------------------------------------------------ +// Class UntreatedParts +//------------------------------------------------ +class UntreatedParts +{ + private: + mutable pthread_mutex_t untreated_list_lock; // protects untreated_list + bool is_lock_init; + + untreated_list_t untreated_list; + long last_tag; // [NOTE] Use this to identify the latest updated part. + + private: + bool RowGetPart(off_t& start, off_t& size, off_t max_size, off_t min_size, bool lastpart) const; + + public: + UntreatedParts(); + ~UntreatedParts(); + UntreatedParts(const UntreatedParts&) = delete; + UntreatedParts(UntreatedParts&&) = delete; + UntreatedParts& operator=(const UntreatedParts&) = delete; + UntreatedParts& operator=(UntreatedParts&&) = delete; + + bool empty(); + + bool AddPart(off_t start, off_t size); + bool GetLastUpdatedPart(off_t& start, off_t& size, off_t max_size, off_t min_size = MIN_MULTIPART_SIZE) const { return RowGetPart(start, size, max_size, min_size, true); } + + bool ClearParts(off_t start, off_t size); + bool ClearAll() { return ClearParts(0, 0); } + + bool GetLastUpdatePart(off_t& start, off_t& size) const; + bool ReplaceLastUpdatePart(off_t start, off_t size); + bool RemoveLastUpdatePart(); + + bool Duplicate(untreated_list_t& list); + + void Dump(); +}; + +#endif // S3FS_FDCACHE_UNTREATED_H_ + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/hybridcache_accessor_4_s3fs.cpp b/s3fs/hybridcache_accessor_4_s3fs.cpp new file mode 100644 index 0000000..d947c53 --- /dev/null +++ b/s3fs/hybridcache_accessor_4_s3fs.cpp @@ -0,0 +1,554 @@ +#include "fdcache_entity.h" +#include "fdcache.h" +#include "hybridcache_accessor_4_s3fs.h" +#include "hybridcache_disk_data_adaptor.h" +#include "hybridcache_s3_data_adaptor.h" +#include "s3fs_logger.h" +#include "time.h" + +#include "Common.h" +#include "FileSystemDataAdaptor.h" +#include "GlobalDataAdaptor.h" + +using HybridCache::ByteBuffer; +using HybridCache::WriteCache; +using HybridCache::ReadCache; +using HybridCache::ErrCode::SUCCESS; +using HybridCache::EnableLogging; + +HybridCacheAccessor4S3fs::HybridCacheAccessor4S3fs( + const HybridCache::HybridCacheConfig& cfg) : HybridCacheAccessor(cfg) { + Init(); +} + +HybridCacheAccessor4S3fs::~HybridCacheAccessor4S3fs() { + Stop(); +} + +void HybridCacheAccessor4S3fs::Init() { + InitLog(); + + if (cfg_.UseGlobalCache) { + std::shared_ptr etcd_client = nullptr; + if (cfg_.GlobalCacheCfg.EnableWriteCache) { + GetGlobalConfig().default_policy.write_cache_type = REPLICATION; + GetGlobalConfig().default_policy.write_replication_factor = 1; + etcd_client = std::make_shared(cfg_.GlobalCacheCfg.EtcdAddress); + } + if (!cfg_.GlobalCacheCfg.GflagFile.empty()) { + HybridCache::ParseFlagFromFile(cfg_.GlobalCacheCfg.GflagFile); + } + dataAdaptor_ = std::make_shared( + std::make_shared(std::make_shared()), + cfg_.GlobalCacheCfg.GlobalServers, etcd_client); + } else { + dataAdaptor_ = std::make_shared( + std::make_shared()); + } + + executor_ = std::make_shared(cfg_.ThreadNum); + dataAdaptor_->SetExecutor(executor_); + writeCache_ = std::make_shared(cfg_.WriteCacheCfg); + readCache_ = std::make_shared(cfg_.ReadCacheCfg, dataAdaptor_, + executor_); + tokenBucket_ = std::make_shared( + cfg_.UploadNormalFlowLimit, cfg_.UploadBurstFlowLimit); + toStop_.store(false, std::memory_order_release); + bgFlushThread_ = std::thread(&HybridCacheAccessor4S3fs::BackGroundFlush, this); + LOG(WARNING) << "[Accessor]Init, useGlobalCache:" << cfg_.UseGlobalCache; +} + +void HybridCacheAccessor4S3fs::Stop() { + toStop_.store(true, std::memory_order_release); + if (bgFlushThread_.joinable()) { + bgFlushThread_.join(); + } + executor_->stop(); + writeCache_.reset(); + readCache_.reset(); + LOG(WARNING) << "[Accessor]Stop"; +} + +int HybridCacheAccessor4S3fs::Put(const std::string &key, size_t start, + size_t len, const char* buf) { + std::chrono::steady_clock::time_point startTime; + if (EnableLogging) startTime = std::chrono::steady_clock::now(); + + // When the write cache is full, + // block waiting for asynchronous flush to release the write cache space. + while(IsWriteCacheFull(len)) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + // shared lock + auto fileLock = fileLock_.find(key); + while(true) { + if (fileLock_.end() != fileLock) break; + auto res = fileLock_.insert(key, std::make_shared>(0)); + if (res.second) { + fileLock = std::move(res.first); + break; + } + fileLock = fileLock_.find(key); + } + while(true) { + int lock = fileLock->second->load(); + if (lock >= 0 && fileLock->second->compare_exchange_weak(lock, lock + 1)) + break; + } + + int res = writeCache_->Put(key, start, len, ByteBuffer(const_cast(buf), len)); + + int fd = -1; + FdEntity* ent = nullptr; + if (SUCCESS == res && nullptr == (ent = FdManager::get()->GetFdEntity( + key.c_str(), fd, false, AutoLock::ALREADY_LOCKED))) { + res = -EIO; + LOG(ERROR) << "[Accessor]Put, can't find opened path, file:" << key; + } + if (SUCCESS == res) { + ent->UpdateRealsize(start + len); // TODO: size如何获取?并发情况下的一致性? + } + + fileLock->second->fetch_sub(1); // release shared lock + + if (EnableLogging) { + double totalTime = std::chrono::duration( + std::chrono::steady_clock::now() - startTime).count(); + LOG(INFO) << "[Accessor]Put, key:" << key << ", start:" << start + << ", len:" << len << ", res:" << res + << ", time:" << totalTime << "ms"; + } + return res; +} + +int HybridCacheAccessor4S3fs::Get(const std::string &key, size_t start, + size_t len, char* buf) { + std::chrono::steady_clock::time_point startTime; + if (EnableLogging) startTime = std::chrono::steady_clock::now(); + + int res = SUCCESS; + ByteBuffer buffer(buf, len); + std::vector> dataBoundary; + res = writeCache_->Get(key, start, len, buffer, dataBoundary); + + size_t remainLen = len; + for (auto it : dataBoundary) { + remainLen -= it.second; + } + + // handle cache misses + size_t readLen = 0; + size_t stepStart = 0; + size_t fileStartOff = 0; + std::vector> fs; + auto it = dataBoundary.begin(); + while (remainLen > 0 && SUCCESS == res) { + ByteBuffer buffer(buf + stepStart); + fileStartOff = start + stepStart; + if (it != dataBoundary.end()) { + readLen = it->first - stepStart; + if (!readLen) { + stepStart = it->first + it->second; + ++it; + continue; + } + stepStart = it->first + it->second; + ++it; + } else { + readLen = remainLen; + } + buffer.len = readLen; + remainLen -= readLen; + fs.emplace_back(std::move(readCache_->Get(key, fileStartOff, readLen, buffer))); + } + + if (!fs.empty()) { + auto collectRes = folly::collectAll(fs).get(); + for (auto& entry: collectRes) { + int tmpRes = entry.value(); + if (SUCCESS != tmpRes && -ENOENT != tmpRes) + res = tmpRes; + } + } + + if (EnableLogging) { + double totalTime = std::chrono::duration( + std::chrono::steady_clock::now() - startTime).count(); + LOG(INFO) << "[Accessor]Get, key:" << key << ", start:" << start + << ", len:" << len << ", res:" << res + << ", time:" << totalTime << "ms"; + } + return res; +} + +int HybridCacheAccessor4S3fs::Flush(const std::string &key) { + std::chrono::steady_clock::time_point startTime; + if (EnableLogging) { + startTime = std::chrono::steady_clock::now(); + LOG(INFO) << "[Accessor]Flush start, key:" << key; + } + + // exclusive lock + auto fileLock = fileLock_.find(key); + while(true) { + if (fileLock_.end() != fileLock) break; + auto res = fileLock_.insert(key, std::make_shared>(0)); + if (res.second) { + fileLock = std::move(res.first); + break; + } + fileLock = fileLock_.find(key); + } + while(true) { + int expected = 0; + if (fileLock->second->compare_exchange_weak(expected, -1)) + break; + } + + int res = SUCCESS; + int fd = -1; + FdEntity* ent = nullptr; + if (nullptr == (ent = FdManager::get()->GetFdEntity( + key.c_str(), fd, false, AutoLock::ALREADY_LOCKED))) { + res = -EIO; + LOG(ERROR) << "[Accessor]Flush, can't find opened path, file:" << key; + } + size_t realSize = 0; + std::map realHeaders; + if (SUCCESS == res) { + realSize = ent->GetRealsize(); + for (auto &it : ent->GetOriginalHeaders()) { + realHeaders[it.first] = it.second; + } + } + + if (SUCCESS == res && cfg_.UseGlobalCache) { + // first head S3,upload a empty file when the file does not exist + size_t size; + std::map headers; + S3DataAdaptor s3Adaptor; + res = s3Adaptor.Head(key, size, headers).get(); + if (-ENOENT == res) { + res = s3Adaptor.UpLoad(key, 0, ByteBuffer(nullptr, 0), realHeaders).get(); + if (SUCCESS != res) { + LOG(ERROR) << "[Accessor]Flush, upload empty file error, file:" << key + << ", res:" << res; + } + } else if (SUCCESS != res) { + LOG(ERROR) << "[Accessor]Flush, head error, file:" << key + << ", res:" << res; + } + } + + char *buf = nullptr; + while(0 != posix_memalign((void **) &buf, 4096, realSize)); + ByteBuffer buffer(buf, realSize); + if (SUCCESS == res) { + const size_t chunkSize = GetGlobalConfig().write_chunk_size * 2; + const uint64_t chunkNum = realSize / chunkSize + (realSize % chunkSize == 0 ? 0 : 1); + std::vector jsonRoots(chunkNum); + std::vector> fs; + uint64_t cur = 0; + for (size_t offset = 0; offset < realSize; offset += chunkSize) { + size_t len = std::min(chunkSize, realSize - offset); + fs.emplace_back(folly::via(executor_.get(), [this, key, offset, len, buf, &realHeaders, &jsonRoots, cur]() { + int getRes = Get(key, offset, len, buf + offset); + if (!cfg_.UseGlobalCache || SUCCESS != getRes) return getRes; + while(!tokenBucket_->consume(len)); // upload flow control + ByteBuffer buffer(buf + offset, len); + GlobalDataAdaptor* adaptor = dynamic_cast(dataAdaptor_.get()); + return adaptor->UpLoadPart(key, offset, len, buffer, realHeaders, jsonRoots[cur]).get(); + })); + ++cur; + } + auto collectRes = folly::collectAll(fs).get(); + for (auto& entry: collectRes) { + int tmpRes = entry.value(); + if (SUCCESS != tmpRes) res = tmpRes; + } + if (cfg_.UseGlobalCache && SUCCESS == res) { + GlobalDataAdaptor* adaptor = dynamic_cast(dataAdaptor_.get()); + res = adaptor->Completed(key, jsonRoots, realSize).get(); + } + } + + if (SUCCESS == res && !cfg_.UseGlobalCache) { // Get success + while(!tokenBucket_->consume(realSize)); // upload flow control + res = dataAdaptor_->UpLoad(key, realSize, buffer, realHeaders).get(); + if (SUCCESS != res){ + LOG(ERROR) << "[Accessor]Flush, upload error, file:" << key + << ", res:" << res; + } + } + + // folly via is not executed immediately, so use separate thread + std::thread t([this, key, res]() { + if (SUCCESS == res) // upload success + writeCache_->Delete(key); + auto fileLock = fileLock_.find(key); + if (fileLock_.end() != fileLock) { + fileLock->second->store(0); + fileLock_.erase(fileLock); // release exclusive lock + } + }); + t.detach(); + + if (SUCCESS == res && cfg_.FlushToRead) { // upload success + // TODO: 为提升性能,解锁可能会先于put readCache,可能导致并发flush时写脏数据 + std::vector> fs; + const size_t chunkSize = 32 * 1024 * 1024; + for (size_t offset = 0; offset < realSize; offset += chunkSize) { + size_t len = std::min(chunkSize, realSize - offset); + fs.emplace_back(folly::via(executor_.get(), [this, key, offset, len, buf]() { + return readCache_->Put(key, offset, len, ByteBuffer(buf + offset, len)); + })); + } + folly::collectAll(fs).via(executor_.get()).thenValue([this, buf]( + std::vector, std::allocator>>&& tups) { + if (buf) free(buf); + return 0; + }); + } else { + folly::via(executor_.get(), [this, buf]() { + if (buf) free(buf); + }); + } + + if (EnableLogging) { + double totalTime = std::chrono::duration( + std::chrono::steady_clock::now() - startTime).count(); + LOG(INFO) << "[Accessor]Flush end, key:" << key << ", size:" << realSize + << ", res:" << res << ", time:" << totalTime << "ms"; + } + return res; +} + +int HybridCacheAccessor4S3fs::DeepFlush(const std::string &key) { + std::chrono::steady_clock::time_point startTime; + if (EnableLogging) startTime = std::chrono::steady_clock::now(); + + int res = SUCCESS; + if (cfg_.UseGlobalCache) { + res = dataAdaptor_->DeepFlush(key).get(); + } + if (EnableLogging) { + double totalTime = std::chrono::duration( + std::chrono::steady_clock::now() - startTime).count(); + LOG(INFO) << "[Accessor]DeepFlush, key:" << key << ", res:" << res + << ", time:" << totalTime << "ms"; + } + return res; +} + +int HybridCacheAccessor4S3fs::Delete(const std::string &key) { + std::chrono::steady_clock::time_point startTime; + if (EnableLogging) startTime = std::chrono::steady_clock::now(); + + // exclusive lock + auto fileLock = fileLock_.find(key); + while(true) { + if (fileLock_.end() != fileLock) break; + auto res = fileLock_.insert(key, std::make_shared>(0)); + if (res.second) { + fileLock = std::move(res.first); + break; + } + fileLock = fileLock_.find(key); + } + while(true) { + int expected = 0; + if (fileLock->second->compare_exchange_weak(expected, -1)) + break; + } + + int res = writeCache_->Delete(key); + if (SUCCESS == res) { + res = readCache_->Delete(key); + } + if (SUCCESS == res) { + res = dataAdaptor_->Delete(key).get(); + } + + fileLock->second->store(0); + fileLock_.erase(fileLock); // release exclusive lock + if (EnableLogging) { + double totalTime = std::chrono::duration( + std::chrono::steady_clock::now() - startTime).count(); + LOG(INFO) << "[Accessor]Delete, key:" << key << ", res:" << res + << ", time:" << totalTime << "ms"; + } + return res; +} + +int HybridCacheAccessor4S3fs::Truncate(const std::string &key, size_t size) { + std::chrono::steady_clock::time_point startTime; + if (EnableLogging) startTime = std::chrono::steady_clock::now(); + + // exclusive lock + auto fileLock = fileLock_.find(key); + while(true) { + if (fileLock_.end() != fileLock) break; + auto res = fileLock_.insert(key, std::make_shared>(0)); + if (res.second) { + fileLock = std::move(res.first); + break; + } + fileLock = fileLock_.find(key); + } + while(true) { + int expected = 0; + if (fileLock->second->compare_exchange_weak(expected, -1)) + break; + } + + int res = SUCCESS; + int fd = -1; + FdEntity* ent = nullptr; + if (nullptr == (ent = FdManager::get()->GetFdEntity(key.c_str(), fd, + false, AutoLock::ALREADY_LOCKED))) { + res = -EIO; + LOG(ERROR) << "[Accessor]Flush, can't find opened path, file:" << key; + } + size_t realSize = 0; + if (SUCCESS == res) { + realSize = ent->GetRealsize(); + if (size < realSize) { + res = writeCache_->Truncate(key, size); + } else if (size > realSize) { + // fill write cache + size_t fillSize = size - realSize; + std::unique_ptr buf = std::make_unique(fillSize); + res = writeCache_->Put(key, realSize, fillSize, + ByteBuffer(buf.get(), fillSize)); + } + } + + if (SUCCESS == res && size != realSize) { + ent->TruncateRealsize(size); + } + + // release exclusive lock + fileLock->second->store(0); + + if (EnableLogging) { + double totalTime = std::chrono::duration( + std::chrono::steady_clock::now() - startTime).count(); + LOG(INFO) << "[Accessor]Truncate, key:" << key << ", size:" << size + << ", res:" << res << ", time:" << totalTime << "ms"; + } + return res; +} + +int HybridCacheAccessor4S3fs::Invalidate(const std::string &key) { + std::chrono::steady_clock::time_point startTime; + if (EnableLogging) startTime = std::chrono::steady_clock::now(); + int res = SUCCESS; + if (cfg_.CleanCacheByOpen) { + res = readCache_->Delete(key); + if (EnableLogging) { + double totalTime = std::chrono::duration( + std::chrono::steady_clock::now() - startTime).count(); + LOG(INFO) << "[Accessor]Invalidate, key:" << key + << ", res:" << res << ", time:" << totalTime << "ms"; + } + } + return res; +} + +int HybridCacheAccessor4S3fs::Head(const std::string &key, size_t& size, + std::map& headers) { + std::chrono::steady_clock::time_point startTime; + if (EnableLogging) startTime = std::chrono::steady_clock::now(); + int res = dataAdaptor_->Head(key, size, headers).get(); + if (EnableLogging) { + double totalTime = std::chrono::duration( + std::chrono::steady_clock::now() - startTime).count(); + LOG(INFO) << "[Accessor]Head, key:" << key << ", res:" << res + << ", size:" << size << ", headerCnt:" << headers.size() + << ", time:" << totalTime << "ms"; + } + return res; +} + +int HybridCacheAccessor4S3fs::FsSync() { + std::chrono::steady_clock::time_point startTime; + if (EnableLogging) startTime = std::chrono::steady_clock::now(); + if (EnableLogging) { + LOG(WARNING) << "[Accessor]FsSync start"; + } + while(true) { + bool expected = false; + if (backFlushRunning_.compare_exchange_weak(expected, true)) + break; + } + + std::map files; + writeCache_->GetAllKeys(files); + std::vector> filesVec(files.begin(), files.end()); + std::sort(filesVec.begin(), filesVec.end(), + [](std::pair lhs, std::pair rhs) { + return lhs.second < rhs.second; + }); + + std::vector> fs; + for (auto& file : filesVec) { + std::string key = file.first; + fs.emplace_back(folly::via(executor_.get(), [this, key]() { + int res = this->Flush(key); + if (res) { + LOG(ERROR) << "[Accessor]FsSync, flush error in FsSync, file:" << key + << ", res:" << res; + } + return res; + })); + } + if (fs.size()) { + collectAll(fs).get(); + } + backFlushRunning_.store(false); + if (EnableLogging) { + double totalTime = std::chrono::duration( + std::chrono::steady_clock::now() - startTime).count(); + LOG(WARNING) << "[Accessor]FsSync end, fileCnt:" << filesVec.size() + << ", time:" << totalTime << "ms"; + } + return SUCCESS; +} + +bool HybridCacheAccessor4S3fs::UseGlobalCache() { + return cfg_.UseGlobalCache; +} + +void HybridCacheAccessor4S3fs::BackGroundFlush() { + LOG(WARNING) << "[Accessor]BackGroundFlush start"; + while (!toStop_.load(std::memory_order_acquire)) { + if (WriteCacheRatio() < cfg_.BackFlushCacheRatio) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + continue; + } + LOG(WARNING) << "[Accessor]BackGroundFlush radically, write cache ratio:" + << WriteCacheRatio(); + FsSync(); + } + if (0 < writeCache_->GetCacheSize()) { + FsSync(); + } + LOG(WARNING) << "[Accessor]BackGroundFlush end"; +} + +void HybridCacheAccessor4S3fs::InitLog() { + FLAGS_log_dir = cfg_.LogPath; + FLAGS_minloglevel = cfg_.LogLevel; + EnableLogging = cfg_.EnableLog; + google::InitGoogleLogging("hybridcache"); +} + +uint32_t HybridCacheAccessor4S3fs::WriteCacheRatio() { + return writeCache_->GetCacheSize() * 100 / writeCache_->GetCacheMaxSize(); +} + +bool HybridCacheAccessor4S3fs::IsWriteCacheFull(size_t len) { + return writeCache_->GetCacheSize() + len >= + (writeCache_->GetCacheMaxSize() * cfg_.WriteCacheCfg.CacheSafeRatio / 100); +} diff --git a/s3fs/hybridcache_accessor_4_s3fs.h b/s3fs/hybridcache_accessor_4_s3fs.h new file mode 100644 index 0000000..6cdfe8f --- /dev/null +++ b/s3fs/hybridcache_accessor_4_s3fs.h @@ -0,0 +1,65 @@ +/* + * Project: HybridCache + * Created Date: 24-3-25 + * Author: lshb + */ + +#ifndef HYBRIDCACHE_ACCESSOR_4_S3FS_H_ +#define HYBRIDCACHE_ACCESSOR_4_S3FS_H_ + +#include + +#include "accessor.h" + +using atomic_ptr_t = std::shared_ptr>; + +class HybridCacheAccessor4S3fs : public HybridCache::HybridCacheAccessor { + public: + HybridCacheAccessor4S3fs(const HybridCache::HybridCacheConfig& cfg); + ~HybridCacheAccessor4S3fs(); + + void Init(); + void Stop(); + + int Put(const std::string &key, size_t start, size_t len, const char* buf); + + int Get(const std::string &key, size_t start, size_t len, char* buf); + + int Flush(const std::string &key); + + int DeepFlush(const std::string &key); + + int Delete(const std::string &key); + + int Truncate(const std::string &key, size_t size); + + int Invalidate(const std::string &key); + + int Head(const std::string &key, size_t& size, + std::map& headers); + + // async full files flush in background + int FsSync(); + + bool UseGlobalCache(); + + HybridCache::ThreadPool* GetExecutor() { + return executor_.get(); + } + + private: + void InitLog(); + bool IsWriteCacheFull(size_t len); + uint32_t WriteCacheRatio(); + void BackGroundFlush(); + + private: + folly::ConcurrentHashMap fileLock_; // rwlock. write and flush are exclusive + std::shared_ptr executor_; + std::shared_ptr tokenBucket_; // upload flow limit + std::atomic toStop_{false}; + std::atomic backFlushRunning_{false}; + std::thread bgFlushThread_; +}; + +#endif // HYBRIDCACHE_ACCESSOR_4_S3FS_H_ diff --git a/s3fs/hybridcache_disk_data_adaptor.cpp b/s3fs/hybridcache_disk_data_adaptor.cpp new file mode 100644 index 0000000..08de2b9 --- /dev/null +++ b/s3fs/hybridcache_disk_data_adaptor.cpp @@ -0,0 +1,89 @@ +#include "fdcache_entity.h" +#include "fdcache.h" +#include "hybridcache_disk_data_adaptor.h" + +using HybridCache::ErrCode::SUCCESS; +using HybridCache::EnableLogging; + +const size_t SINGLE_WRITE_SIZE = 1024 * 1024 * 1024; + +folly::Future DiskDataAdaptor::DownLoad(const std::string &key, + size_t start, + size_t size, + ByteBuffer &buffer) { + assert(executor_); + return folly::via(executor_.get(), [this, key, start, size, buffer]() -> int { + std::chrono::steady_clock::time_point startTime; + if (EnableLogging) startTime = std::chrono::steady_clock::now(); + + int res = SUCCESS; + int fd = -1; + FdEntity* ent = FdManager::get()->GetFdEntity( + key.c_str(), fd, false, AutoLock::ALREADY_LOCKED); + if (nullptr == ent) { + LOG(ERROR) << "[DataAdaptor]DownLoad, can't find opened path, file:" << key; + res = -EIO; + } + if (SUCCESS == res) { + res = ent->ReadByAdaptor(fd, buffer.data, start, size, false, dataAdaptor_); + } + if (EnableLogging) { + double totalTime = std::chrono::duration( + std::chrono::steady_clock::now() - startTime).count(); + LOG(INFO) << "[DataAdaptor]DownLoad, file:" << key + << ", start:" << start << ", size:" << size + << ", res:" << res << ", time:" << totalTime << "ms"; + } + return 0 < res ? SUCCESS : res; + }); +} + +folly::Future DiskDataAdaptor::UpLoad(const std::string &key, + size_t size, + const ByteBuffer &buffer, + const std::map& headers) { + return dataAdaptor_->UpLoad(key, size, buffer, headers).thenValue( + [this, key, buffer, size](int upRes) { + if (SUCCESS != upRes) + return upRes; + int fd = -1; + FdEntity* ent = FdManager::get()->GetFdEntity( + key.c_str(), fd, false, AutoLock::ALREADY_LOCKED); + if (nullptr == ent) { + LOG(ERROR) << "[DataAdaptor]UpLoad, can't find opened path, file:" << key; + return upRes; + } + size_t remainLen = size; + size_t totalWriteLen = 0; + while (0 < remainLen) { + size_t stepLen = SINGLE_WRITE_SIZE < remainLen ? SINGLE_WRITE_SIZE : remainLen; + totalWriteLen += ent->WriteCache(buffer.data + size - remainLen, + size - remainLen, stepLen); + remainLen -= stepLen; + } + if (EnableLogging) { + LOG(INFO) << "[DataAdaptor]UpLoad, write disk cache, file:" << key + << ", size:" << size << ", wsize:" << totalWriteLen; + } + return upRes; + }); +} + +folly::Future DiskDataAdaptor::Delete(const std::string &key) { + return dataAdaptor_->Delete(key).thenValue([this, key](int delRes) { + if (SUCCESS == delRes) { + int tmpRes = FdManager::DeleteCacheFile(key.c_str()); + if (EnableLogging) { + LOG(INFO) << "[DataAdaptor]Delete, delete disk cache, file:" << key + << ", res:" << tmpRes; + } + } + return delRes; + }); +} + +folly::Future DiskDataAdaptor::Head(const std::string &key, + size_t& size, + std::map& headers) { + return dataAdaptor_->Head(key, size, headers); +} diff --git a/s3fs/hybridcache_disk_data_adaptor.h b/s3fs/hybridcache_disk_data_adaptor.h new file mode 100644 index 0000000..41d7d44 --- /dev/null +++ b/s3fs/hybridcache_disk_data_adaptor.h @@ -0,0 +1,40 @@ +/* + * Project: HybridCache + * Created Date: 24-6-7 + * Author: lshb + */ + +#ifndef DISK_DATA_ADAPTOR_H_ +#define DISK_DATA_ADAPTOR_H_ + +#include "data_adaptor.h" + +using HybridCache::ByteBuffer; + +class DiskDataAdaptor : public HybridCache::DataAdaptor { + public: + DiskDataAdaptor(std::shared_ptr dataAdaptor) : dataAdaptor_(dataAdaptor) {} + DiskDataAdaptor() = default; + ~DiskDataAdaptor() {} + + folly::Future DownLoad(const std::string &key, + size_t start, + size_t size, + ByteBuffer &buffer); + + folly::Future UpLoad(const std::string &key, + size_t size, + const ByteBuffer &buffer, + const std::map& headers); + + folly::Future Delete(const std::string &key); + + folly::Future Head(const std::string &key, + size_t& size, + std::map& headers); + + private: + std::shared_ptr dataAdaptor_; +}; + +#endif // DISK_DATA_ADAPTOR_H_ diff --git a/s3fs/hybridcache_s3_data_adaptor.cpp b/s3fs/hybridcache_s3_data_adaptor.cpp new file mode 100644 index 0000000..1025e30 --- /dev/null +++ b/s3fs/hybridcache_s3_data_adaptor.cpp @@ -0,0 +1,136 @@ +#include "common.h" +#include "curl.h" +#include "curl_multi.h" +#include "fdcache_entity.h" +#include "hybridcache_s3_data_adaptor.h" +#include "s3fs_logger.h" +#include "string_util.h" + +using HybridCache::EnableLogging; + +folly::Future S3DataAdaptor::DownLoad(const std::string &key, + size_t start, + size_t size, + ByteBuffer &buffer) { + assert(executor_); + return folly::via(executor_.get(), [key, start, size, buffer]() -> int { + std::chrono::steady_clock::time_point startTime; + if (EnableLogging) startTime = std::chrono::steady_clock::now(); + + int res = 0; + // parallel request + if (S3fsCurl::GetMultipartSize() <= size && !nomultipart) { + res = S3fsCurl::ParallelGetObjectRequest(key.c_str(), + NEW_CACHE_FAKE_FD, start, size, buffer.data); + } else if (0 < size) { // single request + S3fsCurl s3fscurl; + res = s3fscurl.GetObjectRequest(key.c_str(), + NEW_CACHE_FAKE_FD, start, size, buffer.data); + } + + if (EnableLogging) { + double totalTime = std::chrono::duration( + std::chrono::steady_clock::now() - startTime).count(); + LOG(INFO) << "[DataAdaptor]DownLoad, file:" << key + << ", start:" << start << ", size:" << size + << ", res:" << res << ", time:" << totalTime << "ms"; + } + return res; + }); +} + +folly::Future S3DataAdaptor::UpLoad(const std::string &key, + size_t size, + const ByteBuffer &buffer, + const std::map& headers) { + assert(executor_); + + // check size + if (size > MAX_MULTIPART_CNT * S3fsCurl::GetMultipartSize()) { + int res = -EFBIG; + LOG(ERROR) << "[DataAdaptor]UpLoad, file size too large, " + << "increase multipart size and try again. Part count exceeds:" + << MAX_MULTIPART_CNT << ", file:" << key << ", size:" << size; + if (EnableLogging) { + LOG(INFO) << "[DataAdaptor]UpLoad, file:" << key << ", size:" << size + << ", headerSize:" << headers.size() << ", res:" << res; + } + return res; + } + + return folly::via(executor_.get(), [key, size, buffer, headers]() -> int { + std::chrono::steady_clock::time_point startTime; + if (EnableLogging) startTime = std::chrono::steady_clock::now(); + + int res = 0; + headers_t s3fsHeaders; + for (auto it : headers) { + s3fsHeaders[it.first] = it.second; + } + + if (nomultipart || size < S3fsCurl::GetMultipartSize()) { // normal uploading + S3fsCurl s3fscurl(true); + res = s3fscurl.PutRequest(key.c_str(), s3fsHeaders, + NEW_CACHE_FAKE_FD, size, buffer.data); + } else { // Multi part Upload + res = S3fsCurl::ParallelMultipartUploadRequest(key.c_str(), s3fsHeaders, + NEW_CACHE_FAKE_FD, size, buffer.data); + } + + if (EnableLogging) { + double totalTime = std::chrono::duration( + std::chrono::steady_clock::now() - startTime).count(); + LOG(INFO) << "[DataAdaptor]UpLoad, file:" << key << ", size:" << size + << ", headerSize:" << headers.size() << ", res:" << res + << ", time:" << totalTime << "ms"; + } + return res; + }); +} + +folly::Future S3DataAdaptor::Delete(const std::string &key) { + assert(executor_); + return folly::via(executor_.get(), [key]() -> int { + std::chrono::steady_clock::time_point startTime; + if (EnableLogging) startTime = std::chrono::steady_clock::now(); + + S3fsCurl s3fscurl; + int res = s3fscurl.DeleteRequest(key.c_str()); + if (EnableLogging) { + double totalTime = std::chrono::duration( + std::chrono::steady_clock::now() - startTime).count(); + LOG(INFO) << "[DataAdaptor]Delete, file:" << key << ", res:" << res + << ", time:" << totalTime << "ms"; + } + return res; + }); +} + +folly::Future S3DataAdaptor::Head(const std::string &key, + size_t& size, + std::map& headers) { + assert(executor_); + return folly::via(executor_.get(), [key, &size, &headers]() -> int { + std::chrono::steady_clock::time_point startTime; + if (EnableLogging) startTime = std::chrono::steady_clock::now(); + + headers_t s3fsHeaders; + S3fsCurl s3fscurl; + int res = s3fscurl.HeadRequest(key.c_str(), s3fsHeaders); + for (auto it : s3fsHeaders) { + headers[it.first] = it.second; + if (lower(it.first) == "content-length") { + std::stringstream sstream(it.second); + sstream >> size; + } + } + if (EnableLogging) { + double totalTime = std::chrono::duration( + std::chrono::steady_clock::now() - startTime).count(); + LOG(INFO) << "[DataAdaptor]Head, file:" << key << ", res:" << res + << ", size:" << size << ", headerSize:" << headers.size() + << ", time:" << totalTime << "ms"; + } + return res; + }); +} diff --git a/s3fs/hybridcache_s3_data_adaptor.h b/s3fs/hybridcache_s3_data_adaptor.h new file mode 100644 index 0000000..c0e62a3 --- /dev/null +++ b/s3fs/hybridcache_s3_data_adaptor.h @@ -0,0 +1,33 @@ +/* + * Project: HybridCache + * Created Date: 24-3-11 + * Author: lshb + */ + +#ifndef S3_DATA_ADAPTOR_H_ +#define S3_DATA_ADAPTOR_H_ + +#include "data_adaptor.h" + +using HybridCache::ByteBuffer; + +class S3DataAdaptor : public HybridCache::DataAdaptor { + public: + folly::Future DownLoad(const std::string &key, + size_t start, + size_t size, + ByteBuffer &buffer); + + folly::Future UpLoad(const std::string &key, + size_t size, + const ByteBuffer &buffer, + const std::map& headers); + + folly::Future Delete(const std::string &key); + + folly::Future Head(const std::string &key, + size_t& size, + std::map& headers); +}; + +#endif // S3_DATA_ADAPTOR_H_ diff --git a/s3fs/metaheader.cpp b/s3fs/metaheader.cpp new file mode 100644 index 0000000..69e97e8 --- /dev/null +++ b/s3fs/metaheader.cpp @@ -0,0 +1,341 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Takeshi Nakatani + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include +#include +#include + +#include "common.h" +#include "metaheader.h" +#include "string_util.h" + +static constexpr struct timespec DEFAULT_TIMESPEC = {-1, 0}; + +//------------------------------------------------------------------- +// Utility functions for convert +//------------------------------------------------------------------- +static struct timespec cvt_string_to_time(const char *str) +{ + // [NOTE] + // In rclone, there are cases where ns is set to x-amz-meta-mtime + // with floating point number. s3fs uses x-amz-meta-mtime by + // truncating the floating point or less (in seconds or less) to + // correspond to this. + // + std::string strmtime; + long nsec = 0; + if(str && '\0' != *str){ + strmtime = str; + std::string::size_type pos = strmtime.find('.', 0); + if(std::string::npos != pos){ + nsec = cvt_strtoofft(strmtime.substr(pos + 1).c_str(), /*base=*/ 10); + strmtime.erase(pos); + } + } + struct timespec ts = {static_cast(cvt_strtoofft(strmtime.c_str(), /*base=*/ 10)), nsec}; + return ts; +} + +static struct timespec get_time(const headers_t& meta, const char *header) +{ + headers_t::const_iterator iter; + if(meta.end() == (iter = meta.find(header))){ + return DEFAULT_TIMESPEC; + } + return cvt_string_to_time((*iter).second.c_str()); +} + +struct timespec get_mtime(const headers_t& meta, bool overcheck) +{ + struct timespec t = get_time(meta, "x-amz-meta-mtime"); + if(0 < t.tv_sec){ + return t; + } + t = get_time(meta, "x-amz-meta-goog-reserved-file-mtime"); + if(0 < t.tv_sec){ + return t; + } + if(overcheck){ + struct timespec ts = {get_lastmodified(meta), 0}; + return ts; + } + return DEFAULT_TIMESPEC; +} + +struct timespec get_ctime(const headers_t& meta, bool overcheck) +{ + struct timespec t = get_time(meta, "x-amz-meta-ctime"); + if(0 < t.tv_sec){ + return t; + } + if(overcheck){ + struct timespec ts = {get_lastmodified(meta), 0}; + return ts; + } + return DEFAULT_TIMESPEC; +} + +struct timespec get_atime(const headers_t& meta, bool overcheck) +{ + struct timespec t = get_time(meta, "x-amz-meta-atime"); + if(0 < t.tv_sec){ + return t; + } + if(overcheck){ + struct timespec ts = {get_lastmodified(meta), 0}; + return ts; + } + return DEFAULT_TIMESPEC; +} + +off_t get_size(const char *s) +{ + return cvt_strtoofft(s, /*base=*/ 10); +} + +off_t get_size(const headers_t& meta) +{ + headers_t::const_iterator iter = meta.find("Content-Length"); + if(meta.end() == iter){ + return 0; + } + return get_size((*iter).second.c_str()); +} + +mode_t get_mode(const char *s, int base) +{ + return static_cast(cvt_strtoofft(s, base)); +} + +mode_t get_mode(const headers_t& meta, const std::string& strpath, bool checkdir, bool forcedir) +{ + mode_t mode = 0; + bool isS3sync = false; + headers_t::const_iterator iter; + + if(meta.end() != (iter = meta.find("x-amz-meta-mode"))){ + mode = get_mode((*iter).second.c_str()); + }else if(meta.end() != (iter = meta.find("x-amz-meta-permissions"))){ // for s3sync + mode = get_mode((*iter).second.c_str()); + isS3sync = true; + }else if(meta.end() != (iter = meta.find("x-amz-meta-goog-reserved-posix-mode"))){ // for GCS + mode = get_mode((*iter).second.c_str(), 8); + }else{ + // If another tool creates an object without permissions, default to owner + // read-write and group readable. + mode = (!strpath.empty() && '/' == *strpath.rbegin()) ? 0750 : 0640; + } + + // Checking the bitmask, if the last 3 bits are all zero then process as a regular + // file type (S_IFDIR or S_IFREG), otherwise return mode unmodified so that S_IFIFO, + // S_IFSOCK, S_IFCHR, S_IFLNK and S_IFBLK devices can be processed properly by fuse. + if(!(mode & S_IFMT)){ + if(!isS3sync){ + if(checkdir){ + if(forcedir){ + mode |= S_IFDIR; + }else{ + if(meta.end() != (iter = meta.find("Content-Type"))){ + std::string strConType = (*iter).second; + // Leave just the mime type, remove any optional parameters (eg charset) + std::string::size_type pos = strConType.find(';'); + if(std::string::npos != pos){ + strConType.erase(pos); + } + if(strConType == "application/x-directory" || strConType == "httpd/unix-directory"){ + // Nextcloud uses this MIME type for directory objects when mounting bucket as external Storage + mode |= S_IFDIR; + }else if(!strpath.empty() && '/' == *strpath.rbegin()){ + if(strConType == "binary/octet-stream" || strConType == "application/octet-stream"){ + mode |= S_IFDIR; + }else{ + if(complement_stat){ + // If complement lack stat mode, when the object has '/' character at end of name + // and content type is text/plain and the object's size is 0 or 1, it should be + // directory. + off_t size = get_size(meta); + if(strConType == "text/plain" && (0 == size || 1 == size)){ + mode |= S_IFDIR; + }else{ + mode |= S_IFREG; + } + }else{ + mode |= S_IFREG; + } + } + }else{ + mode |= S_IFREG; + } + }else{ + mode |= S_IFREG; + } + } + } + // If complement lack stat mode, when it's mode is not set any permission, + // the object is added minimal mode only for read permission. + if(complement_stat && 0 == (mode & (S_IRWXU | S_IRWXG | S_IRWXO))){ + mode |= (S_IRUSR | (0 == (mode & S_IFDIR) ? 0 : S_IXUSR)); + } + }else{ + if(!checkdir){ + // cut dir/reg flag. + mode &= ~S_IFDIR; + mode &= ~S_IFREG; + } + } + } + return mode; +} + +uid_t get_uid(const char *s) +{ + return static_cast(cvt_strtoofft(s, /*base=*/ 0)); +} + +uid_t get_uid(const headers_t& meta) +{ + headers_t::const_iterator iter; + if(meta.end() != (iter = meta.find("x-amz-meta-uid"))){ + return get_uid((*iter).second.c_str()); + }else if(meta.end() != (iter = meta.find("x-amz-meta-owner"))){ // for s3sync + return get_uid((*iter).second.c_str()); + }else if(meta.end() != (iter = meta.find("x-amz-meta-goog-reserved-posix-uid"))){ // for GCS + return get_uid((*iter).second.c_str()); + }else{ + return geteuid(); + } +} + +gid_t get_gid(const char *s) +{ + return static_cast(cvt_strtoofft(s, /*base=*/ 0)); +} + +gid_t get_gid(const headers_t& meta) +{ + headers_t::const_iterator iter; + if(meta.end() != (iter = meta.find("x-amz-meta-gid"))){ + return get_gid((*iter).second.c_str()); + }else if(meta.end() != (iter = meta.find("x-amz-meta-group"))){ // for s3sync + return get_gid((*iter).second.c_str()); + }else if(meta.end() != (iter = meta.find("x-amz-meta-goog-reserved-posix-gid"))){ // for GCS + return get_gid((*iter).second.c_str()); + }else{ + return getegid(); + } +} + +blkcnt_t get_blocks(off_t size) +{ + return (size / 512) + (0 == (size % 512) ? 0 : 1); +} + +time_t cvtIAMExpireStringToTime(const char* s) +{ + struct tm tm; + if(!s){ + return 0L; + } + memset(&tm, 0, sizeof(struct tm)); + strptime(s, "%Y-%m-%dT%H:%M:%S", &tm); + return timegm(&tm); // GMT +} + +time_t get_lastmodified(const char* s) +{ + struct tm tm; + if(!s){ + return -1; + } + memset(&tm, 0, sizeof(struct tm)); + strptime(s, "%a, %d %b %Y %H:%M:%S %Z", &tm); + return timegm(&tm); // GMT +} + +time_t get_lastmodified(const headers_t& meta) +{ + headers_t::const_iterator iter = meta.find("Last-Modified"); + if(meta.end() == iter){ + return -1; + } + return get_lastmodified((*iter).second.c_str()); +} + +// +// Returns it whether it is an object with need checking in detail. +// If this function returns true, the object is possible to be directory +// and is needed checking detail(searching sub object). +// +bool is_need_check_obj_detail(const headers_t& meta) +{ + headers_t::const_iterator iter; + + // directory object is Content-Length as 0. + if(0 != get_size(meta)){ + return false; + } + // if the object has x-amz-meta information, checking is no more. + if(meta.end() != meta.find("x-amz-meta-mode") || + meta.end() != meta.find("x-amz-meta-mtime") || + meta.end() != meta.find("x-amz-meta-ctime") || + meta.end() != meta.find("x-amz-meta-atime") || + meta.end() != meta.find("x-amz-meta-uid") || + meta.end() != meta.find("x-amz-meta-gid") || + meta.end() != meta.find("x-amz-meta-owner") || + meta.end() != meta.find("x-amz-meta-group") || + meta.end() != meta.find("x-amz-meta-permissions") ) + { + return false; + } + // if there is not Content-Type, or Content-Type is "x-directory", + // checking is no more. + if(meta.end() == (iter = meta.find("Content-Type"))){ + return false; + } + if("application/x-directory" == (*iter).second){ + return false; + } + return true; +} + +// [NOTE] +// If add_noexist is false and the key does not exist, it will not be added. +// +bool merge_headers(headers_t& base, const headers_t& additional, bool add_noexist) +{ + bool added = false; + for(headers_t::const_iterator iter = additional.begin(); iter != additional.end(); ++iter){ + if(add_noexist || base.find(iter->first) != base.end()){ + base[iter->first] = iter->second; + added = true; + } + } + return added; +} + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/metaheader.h b/s3fs/metaheader.h new file mode 100644 index 0000000..44dd8a5 --- /dev/null +++ b/s3fs/metaheader.h @@ -0,0 +1,71 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef S3FS_METAHEADER_H_ +#define S3FS_METAHEADER_H_ + +#include +#include +#include + +//------------------------------------------------------------------- +// headers_t +//------------------------------------------------------------------- +struct header_nocase_cmp +{ + bool operator()(const std::string &strleft, const std::string &strright) const + { + return (strcasecmp(strleft.c_str(), strright.c_str()) < 0); + } +}; +typedef std::map headers_t; + +//------------------------------------------------------------------- +// Functions +//------------------------------------------------------------------- +struct timespec get_mtime(const headers_t& meta, bool overcheck = true); +struct timespec get_ctime(const headers_t& meta, bool overcheck = true); +struct timespec get_atime(const headers_t& meta, bool overcheck = true); +off_t get_size(const char *s); +off_t get_size(const headers_t& meta); +mode_t get_mode(const char *s, int base = 0); +mode_t get_mode(const headers_t& meta, const std::string& strpath, bool checkdir = false, bool forcedir = false); +uid_t get_uid(const char *s); +uid_t get_uid(const headers_t& meta); +gid_t get_gid(const char *s); +gid_t get_gid(const headers_t& meta); +blkcnt_t get_blocks(off_t size); +time_t cvtIAMExpireStringToTime(const char* s); +time_t get_lastmodified(const char* s); +time_t get_lastmodified(const headers_t& meta); +bool is_need_check_obj_detail(const headers_t& meta); +bool merge_headers(headers_t& base, const headers_t& additional, bool add_noexist); +bool simple_parse_xml(const char* data, size_t len, const char* key, std::string& value); + +#endif // S3FS_METAHEADER_H_ + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/mpu_util.cpp b/s3fs/mpu_util.cpp new file mode 100644 index 0000000..1421905 --- /dev/null +++ b/s3fs/mpu_util.cpp @@ -0,0 +1,159 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Takeshi Nakatani + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include + +#include "s3fs.h" +#include "s3fs_logger.h" +#include "mpu_util.h" +#include "curl.h" +#include "s3fs_xml.h" +#include "s3fs_auth.h" +#include "string_util.h" + +//------------------------------------------------------------------- +// Global variables +//------------------------------------------------------------------- +utility_incomp_type utility_mode = utility_incomp_type::NO_UTILITY_MODE; + +//------------------------------------------------------------------- +// Functions +//------------------------------------------------------------------- +static void print_incomp_mpu_list(const incomp_mpu_list_t& list) +{ + printf("\n"); + printf("Lists the parts that have been uploaded for a specific multipart upload.\n"); + printf("\n"); + + if(!list.empty()){ + printf("---------------------------------------------------------------\n"); + + int cnt = 0; + for(incomp_mpu_list_t::const_iterator iter = list.begin(); iter != list.end(); ++iter, ++cnt){ + printf(" Path : %s\n", (*iter).key.c_str()); + printf(" UploadId : %s\n", (*iter).id.c_str()); + printf(" Date : %s\n", (*iter).date.c_str()); + printf("\n"); + } + printf("---------------------------------------------------------------\n"); + + }else{ + printf("There is no list.\n"); + } +} + +static bool abort_incomp_mpu_list(const incomp_mpu_list_t& list, time_t abort_time) +{ + if(list.empty()){ + return true; + } + time_t now_time = time(nullptr); + + // do removing. + S3fsCurl s3fscurl; + bool result = true; + for(incomp_mpu_list_t::const_iterator iter = list.begin(); iter != list.end(); ++iter){ + const char* tpath = (*iter).key.c_str(); + std::string upload_id = (*iter).id; + + if(0 != abort_time){ // abort_time is 0, it means all. + time_t date = 0; + if(!get_unixtime_from_iso8601((*iter).date.c_str(), date)){ + S3FS_PRN_DBG("date format is not ISO 8601 for %s multipart uploading object, skip this.", tpath); + continue; + } + if(now_time <= (date + abort_time)){ + continue; + } + } + + if(0 != s3fscurl.AbortMultipartUpload(tpath, upload_id)){ + S3FS_PRN_EXIT("Failed to remove %s multipart uploading object.", tpath); + result = false; + }else{ + printf("Succeed to remove %s multipart uploading object.\n", tpath); + } + + // reset(initialize) curl object + s3fscurl.DestroyCurlHandle(); + } + return result; +} + +int s3fs_utility_processing(time_t abort_time) +{ + if(utility_incomp_type::NO_UTILITY_MODE == utility_mode){ + return EXIT_FAILURE; + } + printf("\n*** s3fs run as utility mode.\n\n"); + + S3fsCurl s3fscurl; + std::string body; + int result = EXIT_SUCCESS; + if(0 != s3fscurl.MultipartListRequest(body)){ + S3FS_PRN_EXIT("Could not get list multipart upload.\nThere is no incomplete multipart uploaded object in bucket.\n"); + result = EXIT_FAILURE; + }else{ + // parse result(incomplete multipart upload information) + S3FS_PRN_DBG("response body = {\n%s\n}", body.c_str()); + + xmlDocPtr doc; + if(nullptr == (doc = xmlReadMemory(body.c_str(), static_cast(body.size()), "", nullptr, 0))){ + S3FS_PRN_DBG("xmlReadMemory exited with error."); + result = EXIT_FAILURE; + + }else{ + // make incomplete uploads list + incomp_mpu_list_t list; + if(!get_incomp_mpu_list(doc, list)){ + S3FS_PRN_DBG("get_incomp_mpu_list exited with error."); + result = EXIT_FAILURE; + + }else{ + if(utility_incomp_type::INCOMP_TYPE_LIST == utility_mode){ + // print list + print_incomp_mpu_list(list); + }else if(utility_incomp_type::INCOMP_TYPE_ABORT == utility_mode){ + // remove + if(!abort_incomp_mpu_list(list, abort_time)){ + S3FS_PRN_DBG("an error occurred during removal process."); + result = EXIT_FAILURE; + } + } + } + S3FS_XMLFREEDOC(doc); + } + } + + // ssl + s3fs_destroy_global_ssl(); + + return result; +} + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/mpu_util.h b/s3fs/mpu_util.h new file mode 100644 index 0000000..ca60659 --- /dev/null +++ b/s3fs/mpu_util.h @@ -0,0 +1,64 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef S3FS_MPU_UTIL_H_ +#define S3FS_MPU_UTIL_H_ + +#include +#include + +//------------------------------------------------------------------- +// Structure / Typedef +//------------------------------------------------------------------- +typedef struct incomplete_multipart_upload_info +{ + std::string key; + std::string id; + std::string date; +}INCOMP_MPU_INFO; + +typedef std::vector incomp_mpu_list_t; + +//------------------------------------------------------------------- +// enum for utility process mode +//------------------------------------------------------------------- +enum class utility_incomp_type{ + NO_UTILITY_MODE = 0, // not utility mode + INCOMP_TYPE_LIST, // list of incomplete mpu + INCOMP_TYPE_ABORT // delete incomplete mpu +}; + +extern utility_incomp_type utility_mode; + +//------------------------------------------------------------------- +// Functions +//------------------------------------------------------------------- +int s3fs_utility_processing(time_t abort_time); + +#endif // S3FS_MPU_UTIL_H_ + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/openssl_auth.cpp b/s3fs/openssl_auth.cpp new file mode 100644 index 0000000..5340a97 --- /dev/null +++ b/s3fs/openssl_auth.cpp @@ -0,0 +1,444 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifdef __clang__ +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "s3fs_auth.h" +#include "s3fs_logger.h" + +//------------------------------------------------------------------- +// Utility Function for version +//------------------------------------------------------------------- +const char* s3fs_crypt_lib_name() +{ + static constexpr char version[] = "OpenSSL"; + + return version; +} + +//------------------------------------------------------------------- +// Utility Function for global init +//------------------------------------------------------------------- +bool s3fs_init_global_ssl() +{ + ERR_load_crypto_strings(); + + // [NOTE] + // OpenSSL 3.0 loads error strings automatically so these functions are not needed. + // + #ifndef USE_OPENSSL_30 + ERR_load_BIO_strings(); + #endif + + OpenSSL_add_all_algorithms(); + return true; +} + +bool s3fs_destroy_global_ssl() +{ + EVP_cleanup(); + ERR_free_strings(); + return true; +} + +//------------------------------------------------------------------- +// Utility Function for crypt lock +//------------------------------------------------------------------- +// internal use struct for openssl +struct CRYPTO_dynlock_value +{ + pthread_mutex_t dyn_mutex; +}; + +static pthread_mutex_t* s3fs_crypt_mutex = nullptr; + +static void s3fs_crypt_mutex_lock(int mode, int pos, const char* file, int line) __attribute__ ((unused)); +static void s3fs_crypt_mutex_lock(int mode, int pos, const char* file, int line) +{ + if(s3fs_crypt_mutex){ + int result; + if(mode & CRYPTO_LOCK){ + if(0 != (result = pthread_mutex_lock(&s3fs_crypt_mutex[pos]))){ + S3FS_PRN_CRIT("pthread_mutex_lock returned: %d", result); + abort(); + } + }else{ + if(0 != (result = pthread_mutex_unlock(&s3fs_crypt_mutex[pos]))){ + S3FS_PRN_CRIT("pthread_mutex_unlock returned: %d", result); + abort(); + } + } + } +} + +static unsigned long s3fs_crypt_get_threadid() __attribute__ ((unused)); +static unsigned long s3fs_crypt_get_threadid() +{ + // For FreeBSD etc, some system's pthread_t is structure pointer. + // Then we use cast like C style(not C++) instead of ifdef. + return (unsigned long)(pthread_self()); +} + +static struct CRYPTO_dynlock_value* s3fs_dyn_crypt_mutex(const char* file, int line) __attribute__ ((unused)); +static struct CRYPTO_dynlock_value* s3fs_dyn_crypt_mutex(const char* file, int line) +{ + struct CRYPTO_dynlock_value* dyndata = new CRYPTO_dynlock_value(); + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); +#if S3FS_PTHREAD_ERRORCHECK + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK); +#endif + int result; + if(0 != (result = pthread_mutex_init(&(dyndata->dyn_mutex), &attr))){ + S3FS_PRN_CRIT("pthread_mutex_init returned: %d", result); + return nullptr; + } + return dyndata; +} + +static void s3fs_dyn_crypt_mutex_lock(int mode, struct CRYPTO_dynlock_value* dyndata, const char* file, int line) __attribute__ ((unused)); +static void s3fs_dyn_crypt_mutex_lock(int mode, struct CRYPTO_dynlock_value* dyndata, const char* file, int line) +{ + if(dyndata){ + int result; + if(mode & CRYPTO_LOCK){ + if(0 != (result = pthread_mutex_lock(&(dyndata->dyn_mutex)))){ + S3FS_PRN_CRIT("pthread_mutex_lock returned: %d", result); + abort(); + } + }else{ + if(0 != (result = pthread_mutex_unlock(&(dyndata->dyn_mutex)))){ + S3FS_PRN_CRIT("pthread_mutex_unlock returned: %d", result); + abort(); + } + } + } +} + +static void s3fs_destroy_dyn_crypt_mutex(struct CRYPTO_dynlock_value* dyndata, const char* file, int line) __attribute__ ((unused)); +static void s3fs_destroy_dyn_crypt_mutex(struct CRYPTO_dynlock_value* dyndata, const char* file, int line) +{ + if(dyndata){ + int result = pthread_mutex_destroy(&(dyndata->dyn_mutex)); + if(result != 0){ + S3FS_PRN_CRIT("failed to destroy dyn_mutex"); + abort(); + } + delete dyndata; + } +} + +bool s3fs_init_crypt_mutex() +{ + if(s3fs_crypt_mutex){ + S3FS_PRN_DBG("s3fs_crypt_mutex is not nullptr, destroy it."); + + // cppcheck-suppress unmatchedSuppression + // cppcheck-suppress knownConditionTrueFalse + if(!s3fs_destroy_crypt_mutex()){ + S3FS_PRN_ERR("Failed to s3fs_crypt_mutex"); + return false; + } + } + s3fs_crypt_mutex = new pthread_mutex_t[CRYPTO_num_locks()]; + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); +#if S3FS_PTHREAD_ERRORCHECK + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK); +#endif + for(int cnt = 0; cnt < CRYPTO_num_locks(); cnt++){ + int result = pthread_mutex_init(&s3fs_crypt_mutex[cnt], &attr); + if(result != 0){ + S3FS_PRN_CRIT("pthread_mutex_init returned: %d", result); + return false; + } + } + // static lock + CRYPTO_set_locking_callback(s3fs_crypt_mutex_lock); + CRYPTO_set_id_callback(s3fs_crypt_get_threadid); + // dynamic lock + CRYPTO_set_dynlock_create_callback(s3fs_dyn_crypt_mutex); + CRYPTO_set_dynlock_lock_callback(s3fs_dyn_crypt_mutex_lock); + CRYPTO_set_dynlock_destroy_callback(s3fs_destroy_dyn_crypt_mutex); + + return true; +} + +bool s3fs_destroy_crypt_mutex() +{ + if(!s3fs_crypt_mutex){ + return true; + } + + CRYPTO_set_dynlock_destroy_callback(nullptr); + CRYPTO_set_dynlock_lock_callback(nullptr); + CRYPTO_set_dynlock_create_callback(nullptr); + CRYPTO_set_id_callback(nullptr); + CRYPTO_set_locking_callback(nullptr); + + for(int cnt = 0; cnt < CRYPTO_num_locks(); cnt++){ + int result = pthread_mutex_destroy(&s3fs_crypt_mutex[cnt]); + if(result != 0){ + S3FS_PRN_CRIT("failed to destroy s3fs_crypt_mutex[%d]", cnt); + abort(); + } + } + CRYPTO_cleanup_all_ex_data(); + delete[] s3fs_crypt_mutex; + s3fs_crypt_mutex = nullptr; + + return true; +} + +//------------------------------------------------------------------- +// Utility Function for HMAC +//------------------------------------------------------------------- +static std::unique_ptr s3fs_HMAC_RAW(const void* key, size_t keylen, const unsigned char* data, size_t datalen, unsigned int* digestlen, bool is_sha256) +{ + if(!key || !data || !digestlen){ + return nullptr; + } + (*digestlen) = EVP_MAX_MD_SIZE * sizeof(unsigned char); + std::unique_ptr digest(new unsigned char[*digestlen]); + if(is_sha256){ + HMAC(EVP_sha256(), key, static_cast(keylen), data, datalen, digest.get(), digestlen); + }else{ + HMAC(EVP_sha1(), key, static_cast(keylen), data, datalen, digest.get(), digestlen); + } + + return digest; +} + +std::unique_ptr s3fs_HMAC(const void* key, size_t keylen, const unsigned char* data, size_t datalen, unsigned int* digestlen) +{ + return s3fs_HMAC_RAW(key, keylen, data, datalen, digestlen, false); +} + +std::unique_ptr s3fs_HMAC256(const void* key, size_t keylen, const unsigned char* data, size_t datalen, unsigned int* digestlen) +{ + return s3fs_HMAC_RAW(key, keylen, data, datalen, digestlen, true); +} + +#ifdef USE_OPENSSL_30 +//------------------------------------------------------------------- +// Utility Function for MD5 (OpenSSL >= 3.0) +//------------------------------------------------------------------- +// [NOTE] +// OpenSSL 3.0 deprecated the MD5_*** low-level encryption functions, +// so we should use the high-level EVP API instead. +// + +bool s3fs_md5(const unsigned char* data, size_t datalen, md5_t* digest) +{ + unsigned int digestlen = static_cast(digest->size()); + + const EVP_MD* md = EVP_get_digestbyname("md5"); + EVP_MD_CTX* mdctx = EVP_MD_CTX_create(); + EVP_DigestInit_ex(mdctx, md, nullptr); + EVP_DigestUpdate(mdctx, data, datalen); + EVP_DigestFinal_ex(mdctx, digest->data(), &digestlen); + EVP_MD_CTX_destroy(mdctx); + + return true; +} + +bool s3fs_md5_fd(int fd, off_t start, off_t size, md5_t* result) +{ + EVP_MD_CTX* mdctx; + unsigned int md5_digest_len = static_cast(result->size()); + off_t bytes; + + if(-1 == size){ + struct stat st; + if(-1 == fstat(fd, &st)){ + return false; + } + size = st.st_size; + } + + // instead of MD5_Init + mdctx = EVP_MD_CTX_new(); + EVP_DigestInit_ex(mdctx, EVP_md5(), nullptr); + + for(off_t total = 0; total < size; total += bytes){ + const off_t len = 512; + char buf[len]; + bytes = len < (size - total) ? len : (size - total); + bytes = pread(fd, buf, bytes, start + total); + if(0 == bytes){ + // end of file + break; + }else if(-1 == bytes){ + // error + S3FS_PRN_ERR("file read error(%d)", errno); + EVP_MD_CTX_free(mdctx); + return false; + } + // instead of MD5_Update + EVP_DigestUpdate(mdctx, buf, bytes); + } + + // instead of MD5_Final + EVP_DigestFinal_ex(mdctx, result->data(), &md5_digest_len); + EVP_MD_CTX_free(mdctx); + + return true; +} + +#else +//------------------------------------------------------------------- +// Utility Function for MD5 (OpenSSL < 3.0) +//------------------------------------------------------------------- + +// TODO: Does this fail on OpenSSL < 3.0 and we need to use MD5_CTX functions? +bool s3fs_md5(const unsigned char* data, size_t datalen, md5_t* digest) +{ + unsigned int digestlen = digest->size(); + + const EVP_MD* md = EVP_get_digestbyname("md5"); + EVP_MD_CTX* mdctx = EVP_MD_CTX_create(); + EVP_DigestInit_ex(mdctx, md, nullptr); + EVP_DigestUpdate(mdctx, data, datalen); + EVP_DigestFinal_ex(mdctx, digest->data(), &digestlen); + EVP_MD_CTX_destroy(mdctx); + + return true; +} + +bool s3fs_md5_fd(int fd, off_t start, off_t size, md5_t* result) +{ + MD5_CTX md5ctx; + off_t bytes; + + if(-1 == size){ + struct stat st; + if(-1 == fstat(fd, &st)){ + return false; + } + size = st.st_size; + } + + MD5_Init(&md5ctx); + + for(off_t total = 0; total < size; total += bytes){ + const off_t len = 512; + char buf[len]; + bytes = len < (size - total) ? len : (size - total); + bytes = pread(fd, buf, bytes, start + total); + if(0 == bytes){ + // end of file + break; + }else if(-1 == bytes){ + // error + S3FS_PRN_ERR("file read error(%d)", errno); + return false; + } + MD5_Update(&md5ctx, buf, bytes); + } + + MD5_Final(result->data(), &md5ctx); + + return true; +} +#endif + +//------------------------------------------------------------------- +// Utility Function for SHA256 +//------------------------------------------------------------------- +bool s3fs_sha256(const unsigned char* data, size_t datalen, sha256_t* digest) +{ + const EVP_MD* md = EVP_get_digestbyname("sha256"); + EVP_MD_CTX* mdctx = EVP_MD_CTX_create(); + EVP_DigestInit_ex(mdctx, md, nullptr); + EVP_DigestUpdate(mdctx, data, datalen); + unsigned int digestlen = static_cast(digest->size()); + EVP_DigestFinal_ex(mdctx, digest->data(), &digestlen); + EVP_MD_CTX_destroy(mdctx); + + return true; +} + +bool s3fs_sha256_fd(int fd, off_t start, off_t size, sha256_t* result) +{ + const EVP_MD* md = EVP_get_digestbyname("sha256"); + EVP_MD_CTX* sha256ctx; + off_t bytes; + + if(-1 == fd){ + return false; + } + if(-1 == size){ + struct stat st; + if(-1 == fstat(fd, &st)){ + S3FS_PRN_ERR("fstat error(%d)", errno); + return false; + } + size = st.st_size; + } + + sha256ctx = EVP_MD_CTX_create(); + EVP_DigestInit_ex(sha256ctx, md, nullptr); + + for(off_t total = 0; total < size; total += bytes){ + const off_t len = 512; + char buf[len]; + bytes = len < (size - total) ? len : (size - total); + bytes = pread(fd, buf, bytes, start + total); + if(0 == bytes){ + // end of file + break; + }else if(-1 == bytes){ + // error + S3FS_PRN_ERR("file read error(%d)", errno); + EVP_MD_CTX_destroy(sha256ctx); + return false; + } + EVP_DigestUpdate(sha256ctx, buf, bytes); + } + EVP_DigestFinal_ex(sha256ctx, result->data(), nullptr); + EVP_MD_CTX_destroy(sha256ctx); + + return true; +} + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/psemaphore.h b/s3fs/psemaphore.h new file mode 100644 index 0000000..9de2596 --- /dev/null +++ b/s3fs/psemaphore.h @@ -0,0 +1,111 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef S3FS_SEMAPHORE_H_ +#define S3FS_SEMAPHORE_H_ + +//------------------------------------------------------------------- +// Class Semaphore +//------------------------------------------------------------------- +// portability wrapper for sem_t since macOS does not implement it +#ifdef __APPLE__ + +#include + +class Semaphore +{ + public: + explicit Semaphore(int value) : value(value), sem(dispatch_semaphore_create(value)) {} + ~Semaphore() + { + // macOS cannot destroy a semaphore with posts less than the initializer + for(int i = 0; i < get_value(); ++i){ + post(); + } + dispatch_release(sem); + } + Semaphore(const Semaphore&) = delete; + Semaphore(Semaphore&&) = delete; + Semaphore& operator=(const Semaphore&) = delete; + Semaphore& operator=(Semaphore&&) = delete; + + void wait() { dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); } + bool try_wait() + { + if(0 == dispatch_semaphore_wait(sem, DISPATCH_TIME_NOW)){ + return true; + }else{ + return false; + } + } + void post() { dispatch_semaphore_signal(sem); } + int get_value() const { return value; } + + private: + const int value; + dispatch_semaphore_t sem; +}; + +#else + +#include +#include + +class Semaphore +{ + public: + explicit Semaphore(int value) : value(value) { sem_init(&mutex, 0, value); } + ~Semaphore() { sem_destroy(&mutex); } + void wait() + { + int r; + do { + r = sem_wait(&mutex); + } while (r == -1 && errno == EINTR); + } + bool try_wait() + { + int result; + do{ + result = sem_trywait(&mutex); + }while(result == -1 && errno == EINTR); + + return (0 == result); + } + void post() { sem_post(&mutex); } + int get_value() const { return value; } + + private: + const int value; + sem_t mutex; +}; + +#endif + +#endif // S3FS_SEMAPHORE_H_ + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/s3fs.cpp b/s3fs/s3fs.cpp new file mode 100644 index 0000000..0cc895c --- /dev/null +++ b/s3fs/s3fs.cpp @@ -0,0 +1,6181 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.h" +#include "s3fs.h" +#include "s3fs_logger.h" +#include "metaheader.h" +#include "fdcache.h" +#include "fdcache_auto.h" +#include "fdcache_stat.h" +#include "curl.h" +#include "curl_multi.h" +#include "s3objlist.h" +#include "cache.h" +#include "addhead.h" +#include "sighandlers.h" +#include "s3fs_xml.h" +#include "string_util.h" +#include "s3fs_auth.h" +#include "s3fs_cred.h" +#include "s3fs_help.h" +#include "s3fs_util.h" +#include "mpu_util.h" +#include "threadpoolman.h" +#include "autolock.h" + +//------------------------------------------------------------------- +// Symbols +//------------------------------------------------------------------- +#if !defined(ENOATTR) +#define ENOATTR ENODATA +#endif + +enum class dirtype { + UNKNOWN = -1, + NEW = 0, + OLD = 1, + FOLDER = 2, + NOOBJ = 3, +}; + +//------------------------------------------------------------------- +// Static variables +//------------------------------------------------------------------- +static uid_t mp_uid = 0; // owner of mount point(only not specified uid opt) +static gid_t mp_gid = 0; // group of mount point(only not specified gid opt) +static mode_t mp_mode = 0; // mode of mount point +static mode_t mp_umask = 0; // umask for mount point +static bool is_mp_umask = false;// default does not set. +static std::string mountpoint; +static std::unique_ptr ps3fscred; // using only in this file +static std::string mimetype_file; +static bool nocopyapi = false; +static bool norenameapi = false; +static bool nonempty = false; +static bool allow_other = false; +static uid_t s3fs_uid = 0; +static gid_t s3fs_gid = 0; +static mode_t s3fs_umask = 0; +static bool is_s3fs_uid = false;// default does not set. +static bool is_s3fs_gid = false;// default does not set. +static bool is_s3fs_umask = false;// default does not set. +static bool is_remove_cache = false; +static bool is_use_xattr = false; +static off_t multipart_threshold = 25 * 1024 * 1024; +static int64_t singlepart_copy_limit = 512 * 1024 * 1024; +static bool is_specified_endpoint = false; +static int s3fs_init_deferred_exit_status = 0; +static bool support_compat_dir = false;// default does not support compatibility directory type +static int max_keys_list_object = 1000;// default is 1000 +static off_t max_dirty_data = 5LL * 1024LL * 1024LL * 1024LL; +static bool use_wtf8 = false; +static off_t fake_diskfree_size = -1; // default is not set(-1) +static int max_thread_count = 5; // default is 5 +static bool update_parent_dir_stat= false; // default not updating parent directory stats +static fsblkcnt_t bucket_block_count; // advertised block count of the bucket +static unsigned long s3fs_block_size = 16 * 1024 * 1024; // s3fs block size is 16MB +std::string newcache_conf; + +//------------------------------------------------------------------- +// Global functions : prototype +//------------------------------------------------------------------- +int put_headers(const char* path, headers_t& meta, bool is_copy, bool use_st_size = true); // [NOTE] global function because this is called from FdEntity class + +//------------------------------------------------------------------- +// Static functions : prototype +//------------------------------------------------------------------- +static bool is_special_name_folder_object(const char* path); +static int chk_dir_object_type(const char* path, std::string& newpath, std::string& nowpath, std::string& nowcache, headers_t* pmeta = nullptr, dirtype* pDirType = nullptr); +static int remove_old_type_dir(const std::string& path, dirtype type); +static int get_object_attribute(const char* path, struct stat* pstbuf, headers_t* pmeta = nullptr, bool overcheck = true, bool* pisforce = nullptr, bool add_no_truncate_cache = false); +static int check_object_access(const char* path, int mask, struct stat* pstbuf); +static int check_object_owner(const char* path, struct stat* pstbuf); +static int check_parent_object_access(const char* path, int mask); +static int get_local_fent(AutoFdEntity& autoent, FdEntity **entity, const char* path, int flags = O_RDONLY, bool is_load = false); +static bool multi_head_callback(S3fsCurl* s3fscurl, void* param); +static std::unique_ptr multi_head_retry_callback(S3fsCurl* s3fscurl); +static int readdir_multi_head(const char* path, const S3ObjList& head, void* buf, fuse_fill_dir_t filler); +static int readdir_multi_head_4_newcache(const char* path, const S3ObjList& head, void* buf, fuse_fill_dir_t filler); +static int list_bucket(const char* path, S3ObjList& head, const char* delimiter, bool check_content_only = false); +static int directory_empty(const char* path); +static int rename_large_object(const char* from, const char* to); +static int create_file_object(const char* path, mode_t mode, uid_t uid, gid_t gid); +static int create_directory_object(const char* path, mode_t mode, const struct timespec& ts_atime, const struct timespec& ts_mtime, const struct timespec& ts_ctime, uid_t uid, gid_t gid, const char* pxattrvalue); +static int rename_object(const char* from, const char* to, bool update_ctime); +static int rename_object_nocopy(const char* from, const char* to, bool update_ctime); +static int clone_directory_object(const char* from, const char* to, bool update_ctime, const char* pxattrvalue); +static int rename_directory(const char* from, const char* to); +static int update_mctime_parent_directory(const char* _path); +static int remote_mountpath_exists(const char* path, bool compat_dir); +static bool get_meta_xattr_value(const char* path, std::string& rawvalue); +static bool get_parent_meta_xattr_value(const char* path, std::string& rawvalue); +static bool get_xattr_posix_key_value(const char* path, std::string& xattrvalue, bool default_key); +static bool build_inherited_xattr_value(const char* path, std::string& xattrvalue); +static bool parse_xattr_keyval(const std::string& xattrpair, std::string& key, std::string* pval); +static size_t parse_xattrs(const std::string& strxattrs, xattrs_t& xattrs); +static std::string raw_build_xattrs(const xattrs_t& xattrs); +static std::string build_xattrs(const xattrs_t& xattrs); +static int s3fs_check_service(); +static bool set_mountpoint_attribute(struct stat& mpst); +static int set_bucket(const char* arg); +static int my_fuse_opt_proc(void* data, const char* arg, int key, struct fuse_args* outargs); +static fsblkcnt_t parse_bucket_size(char* value); +static bool is_cmd_exists(const std::string& command); +static int print_umount_message(const std::string& mp, bool force); + +//------------------------------------------------------------------- +// fuse interface functions +//------------------------------------------------------------------- +static int s3fs_getattr(const char* path, struct stat* stbuf); +static int s3fs_readlink(const char* path, char* buf, size_t size); +static int s3fs_mknod(const char* path, mode_t mode, dev_t rdev); +static int s3fs_mkdir(const char* path, mode_t mode); +static int s3fs_unlink(const char* path); +static int s3fs_rmdir(const char* path); +static int s3fs_symlink(const char* from, const char* to); +static int s3fs_rename(const char* from, const char* to); +static int s3fs_link(const char* from, const char* to); +static int s3fs_chmod(const char* path, mode_t mode); +static int s3fs_chmod_nocopy(const char* path, mode_t mode); +static int s3fs_chown(const char* path, uid_t uid, gid_t gid); +static int s3fs_chown_nocopy(const char* path, uid_t uid, gid_t gid); +static int s3fs_utimens(const char* path, const struct timespec ts[2]); +static int s3fs_utimens_nocopy(const char* path, const struct timespec ts[2]); +static int s3fs_truncate(const char* path, off_t size); +static int s3fs_create(const char* path, mode_t mode, struct fuse_file_info* fi); +static int s3fs_open(const char* path, struct fuse_file_info* fi); +static int s3fs_read(const char* path, char* buf, size_t size, off_t offset, struct fuse_file_info* fi); +static int s3fs_write(const char* path, const char* buf, size_t size, off_t offset, struct fuse_file_info* fi); +static int s3fs_statfs(const char* path, struct statvfs* stbuf); +static int s3fs_flush(const char* path, struct fuse_file_info* fi); +static int s3fs_fsync(const char* path, int datasync, struct fuse_file_info* fi); +static int s3fs_release(const char* path, struct fuse_file_info* fi); +static int s3fs_opendir(const char* path, struct fuse_file_info* fi); +static int s3fs_readdir(const char* path, void* buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info* fi); +static int s3fs_access(const char* path, int mask); +static void* s3fs_init(struct fuse_conn_info* conn); +static void s3fs_destroy(void*); +#if defined(__APPLE__) +static int s3fs_setxattr(const char* path, const char* name, const char* value, size_t size, int flags, uint32_t position); +static int s3fs_getxattr(const char* path, const char* name, char* value, size_t size, uint32_t position); +#else +static int s3fs_setxattr(const char* path, const char* name, const char* value, size_t size, int flags); +static int s3fs_getxattr(const char* path, const char* name, char* value, size_t size); +#endif +static int s3fs_listxattr(const char* path, char* list, size_t size); +static int s3fs_removexattr(const char* path, const char* name); + +//------------------------------------------------------------------- +// Classes +//------------------------------------------------------------------- +// +// A flag class indicating whether the mount point has a stat +// +// [NOTE] +// The flag is accessed from child threads, so This class is used for exclusive control of flags. +// This class will be reviewed when we organize the code in the future. +// +class MpStatFlag +{ + private: + std::atomic has_mp_stat; + + public: + MpStatFlag() = default; + MpStatFlag(const MpStatFlag&) = delete; + MpStatFlag(MpStatFlag&&) = delete; + ~MpStatFlag() = default; + MpStatFlag& operator=(const MpStatFlag&) = delete; + MpStatFlag& operator=(MpStatFlag&&) = delete; + + bool Get(); + bool Set(bool flag); +}; + +bool MpStatFlag::Get() +{ + return has_mp_stat; +} + +bool MpStatFlag::Set(bool flag) +{ + return has_mp_stat.exchange(flag); +} + +// whether the stat information file for mount point exists +static MpStatFlag* pHasMpStat = nullptr; + +// +// A synchronous class that calls the fuse_fill_dir_t function that processes the readdir data +// +class SyncFiller +{ + private: + mutable pthread_mutex_t filler_lock; + bool is_lock_init = false; + void* filler_buff; + fuse_fill_dir_t filler_func; + std::set filled; + + public: + explicit SyncFiller(void* buff = nullptr, fuse_fill_dir_t filler = nullptr); + SyncFiller(const SyncFiller&) = delete; + SyncFiller(SyncFiller&&) = delete; + ~SyncFiller(); + SyncFiller& operator=(const SyncFiller&) = delete; + SyncFiller& operator=(SyncFiller&&) = delete; + + int Fill(const char *name, const struct stat *stbuf, off_t off); + int SufficiencyFill(const std::vector& pathlist); +}; + +SyncFiller::SyncFiller(void* buff, fuse_fill_dir_t filler) : filler_buff(buff), filler_func(filler) +{ + if(!filler_buff || !filler_func){ + S3FS_PRN_CRIT("Internal error: SyncFiller constructor parameter is critical value."); + abort(); + } + + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); +#if S3FS_PTHREAD_ERRORCHECK + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK); +#endif + + int result; + if(0 != (result = pthread_mutex_init(&filler_lock, &attr))){ + S3FS_PRN_CRIT("failed to init filler_lock: %d", result); + abort(); + } + is_lock_init = true; +} + +SyncFiller::~SyncFiller() +{ + if(is_lock_init){ + int result; + if(0 != (result = pthread_mutex_destroy(&filler_lock))){ + S3FS_PRN_CRIT("failed to destroy filler_lock: %d", result); + abort(); + } + is_lock_init = false; + } +} + +// +// See. prototype fuse_fill_dir_t in fuse.h +// +int SyncFiller::Fill(const char *name, const struct stat *stbuf, off_t off) +{ + AutoLock auto_lock(&filler_lock); + + int result = 0; + if(filled.insert(name).second){ + result = filler_func(filler_buff, name, stbuf, off); + } + return result; +} + +int SyncFiller::SufficiencyFill(const std::vector& pathlist) +{ + AutoLock auto_lock(&filler_lock); + + int result = 0; + for(std::vector::const_iterator it = pathlist.begin(); it != pathlist.end(); ++it) { + if(filled.insert(*it).second){ + if(0 != filler_func(filler_buff, it->c_str(), nullptr, 0)){ + result = 1; + } + } + } + return result; +} + +//------------------------------------------------------------------- +// Functions +//------------------------------------------------------------------- +static bool IS_REPLACEDIR(dirtype type) +{ + return dirtype::OLD == type || dirtype::FOLDER == type || dirtype::NOOBJ == type; +} + +static bool IS_RMTYPEDIR(dirtype type) +{ + return dirtype::OLD == type || dirtype::FOLDER == type; +} + +static bool IS_CREATE_MP_STAT(const char* path) +{ + // [NOTE] + // pHasMpStat->Get() is set in get_object_attribute() + // + return (path && 0 == strcmp(path, "/") && !pHasMpStat->Get()); +} + +static bool is_special_name_folder_object(const char* path) +{ + if(!support_compat_dir){ + // s3fs does not support compatibility directory type("_$folder$" etc) now, + // thus always returns false. + return false; + } + + if(!path || '\0' == path[0]){ + return false; + } + if(0 == strcmp(path, "/") && mount_prefix.empty()){ + // the path is the mount point which is the bucket root + return false; + } + + std::string strpath = path; + headers_t header; + + if(std::string::npos == strpath.find("_$folder$", 0)){ + if('/' == *strpath.rbegin()){ + strpath.erase(strpath.length() - 1); + } + strpath += "_$folder$"; + } + S3fsCurl s3fscurl; + if(0 != s3fscurl.HeadRequest(strpath.c_str(), header)){ + return false; + } + header.clear(); + S3FS_MALLOCTRIM(0); + return true; +} + +// [Detail] +// This function is complicated for checking directory object type. +// Arguments is used for deleting cache/path, and remake directory object. +// Please see the codes which calls this function. +// +// path: target path +// newpath: should be object path for making/putting/getting after checking +// nowpath: now object name for deleting after checking +// nowcache: now cache path for deleting after checking +// pmeta: headers map +// pDirType: directory object type +// +static int chk_dir_object_type(const char* path, std::string& newpath, std::string& nowpath, std::string& nowcache, headers_t* pmeta, dirtype* pDirType) +{ + dirtype TypeTmp = dirtype::UNKNOWN; + int result = -1; + bool isforce = false; + dirtype* pType = pDirType ? pDirType : &TypeTmp; + + // Normalize new path. + newpath = path; + if('/' != *newpath.rbegin()){ + std::string::size_type Pos; + if(std::string::npos != (Pos = newpath.find("_$folder$", 0))){ + newpath.erase(Pos); + } + newpath += "/"; + } + + // Always check "dir/" at first. + if(0 == (result = get_object_attribute(newpath.c_str(), nullptr, pmeta, false, &isforce))){ + // Found "dir/" cache --> Check for "_$folder$", "no dir object" + nowcache = newpath; + if(is_special_name_folder_object(newpath.c_str())){ // check support_compat_dir in this function + // "_$folder$" type. + (*pType) = dirtype::FOLDER; + nowpath.erase(newpath.length() - 1); + nowpath += "_$folder$"; // cut and add + }else if(isforce){ + // "no dir object" type. + (*pType) = dirtype::NOOBJ; + nowpath = ""; + }else{ + nowpath = newpath; + if(!nowpath.empty() && '/' == *nowpath.rbegin()){ + // "dir/" type + (*pType) = dirtype::NEW; + }else{ + // "dir" type + (*pType) = dirtype::OLD; + } + } + }else if(support_compat_dir){ + // Check "dir" when support_compat_dir is enabled + nowpath.erase(newpath.length() - 1); + if(0 == (result = get_object_attribute(nowpath.c_str(), nullptr, pmeta, false, &isforce))){ + // Found "dir" cache --> this case is only "dir" type. + // Because, if object is "_$folder$" or "no dir object", the cache is "dir/" type. + // (But "no dir object" is checked here.) + nowcache = nowpath; + if(isforce){ + (*pType) = dirtype::NOOBJ; + nowpath = ""; + }else{ + (*pType) = dirtype::OLD; + } + }else{ + // Not found cache --> check for "_$folder$" and "no dir object". + // (come here is that support_compat_dir is enabled) + nowcache = ""; // This case is no cache. + nowpath += "_$folder$"; + if(is_special_name_folder_object(nowpath.c_str())){ + // "_$folder$" type. + (*pType) = dirtype::FOLDER; + result = 0; // result is OK. + }else if(-ENOTEMPTY == directory_empty(newpath.c_str())){ + // "no dir object" type. + (*pType) = dirtype::NOOBJ; + nowpath = ""; // now path. + result = 0; // result is OK. + }else{ + // Error: Unknown type. + (*pType) = dirtype::UNKNOWN; + newpath = ""; + nowpath = ""; + } + } + } + return result; +} + +static int remove_old_type_dir(const std::string& path, dirtype type) +{ + if(IS_RMTYPEDIR(type)){ + S3fsCurl s3fscurl; + int result = s3fscurl.DeleteRequest(path.c_str()); + if(0 != result && -ENOENT != result){ + return result; + } + // succeed removing or not found the directory + }else{ + // nothing to do + } + return 0; +} + +// +// Get object attributes with stat cache. +// This function is base for s3fs_getattr(). +// +// [NOTICE] +// Checking order is changed following list because of reducing the number of the requests. +// 1) "dir" +// 2) "dir/" +// 3) "dir_$folder$" +// +// Special two case of the mount point directory: +// [Case 1] the mount point is the root of the bucket: +// 1) "/" +// +// [Case 2] the mount point is a directory path(ex. foo) below the bucket: +// 1) "foo" +// 2) "foo/" +// 3) "foo_$folder$" +// +static int get_object_attribute(const char* path, struct stat* pstbuf, headers_t* pmeta, bool overcheck, bool* pisforce, bool add_no_truncate_cache) +{ + int result = -1; + struct stat tmpstbuf; + struct stat* pstat = pstbuf ? pstbuf : &tmpstbuf; + headers_t tmpHead; + headers_t* pheader = pmeta ? pmeta : &tmpHead; + std::string strpath; + S3fsCurl s3fscurl; + bool forcedir = false; + bool is_mountpoint = false; // path is the mount point + bool is_bucket_mountpoint = false; // path is the mount point which is the bucket root + std::string::size_type Pos; + + S3FS_PRN_DBG("[path=%s]", path); + + if(!path || '\0' == path[0]){ + return -ENOENT; + } + + memset(pstat, 0, sizeof(struct stat)); + + // check mount point + if(0 == strcmp(path, "/") || 0 == strcmp(path, ".")){ + is_mountpoint = true; + if(mount_prefix.empty()){ + is_bucket_mountpoint = true; + } + // default stat for mount point if the directory stat file is not existed. + pstat->st_mode = mp_mode; + pstat->st_uid = is_s3fs_uid ? s3fs_uid : mp_uid; + pstat->st_gid = is_s3fs_gid ? s3fs_gid : mp_gid; + } + + // Check cache. + pisforce = (nullptr != pisforce ? pisforce : &forcedir); + (*pisforce) = false; + strpath = path; + if(support_compat_dir && overcheck && std::string::npos != (Pos = strpath.find("_$folder$", 0))){ + strpath.erase(Pos); + strpath += "/"; + } + // [NOTE] + // For mount points("/"), the Stat cache key name is "/". + // + if(StatCache::getStatCacheData()->GetStat(strpath, pstat, pheader, overcheck, pisforce)){ + if(is_mountpoint){ + // if mount point, we need to set this. + pstat->st_nlink = 1; // see fuse faq + } + return 0; + } + if(StatCache::getStatCacheData()->IsNoObjectCache(strpath)){ + // there is the path in the cache for no object, it is no object. + return -ENOENT; + } + + // set query(head request) path + if(is_bucket_mountpoint){ + // [NOTE] + // This is a special process for mount point + // The path is "/" for mount points. + // If the bucket mounted at a mount point, we try to find "/" object under + // the bucket for mount point's stat. + // In this case, we will send the request "HEAD // HTTP /1.1" to S3 server. + // + // If the directory under the bucket is mounted, it will be sent + // "HEAD // HTTP/1.1", so we do not need to change path at + // here. + // + strpath = "//"; // strpath is "//" + }else{ + strpath = path; + } + + if(use_newcache && accessor->UseGlobalCache()){ + size_t realSize = 0; + std::map headers; + result = accessor->Head(strpath, realSize, headers); + if(0 == result){ + headers["Content-Length"] = std::to_string(realSize); + for(auto& it : headers) { + pheader->insert(std::make_pair(it.first, it.second)); + } + } + }else{ + result = s3fscurl.HeadRequest(strpath.c_str(), (*pheader)); + s3fscurl.DestroyCurlHandle(); + } + + // if not found target path object, do over checking + if(-EPERM == result){ + // [NOTE] + // In case of a permission error, it exists in directory + // file list but inaccessible. So there is a problem that + // it will send a HEAD request every time, because it is + // not registered in the Stats cache. + // Therefore, even if the file has a permission error, it + // should be registered in the Stats cache. However, if + // the response without modifying is registered in the + // cache, the file permission will be 0644(umask dependent) + // because the meta header does not exist. + // Thus, set the mode of 0000 here in the meta header so + // that s3fs can print a permission error when the file + // is actually accessed. + // It is better not to set meta header other than mode, + // so do not do it. + // + (*pheader)["x-amz-meta-mode"] = "0"; + + }else if(0 != result){ + if(overcheck && !is_bucket_mountpoint){ + // when support_compat_dir is disabled, strpath maybe have "_$folder$". + if('/' != *strpath.rbegin() && std::string::npos == strpath.find("_$folder$", 0)){ + // now path is "object", do check "object/" for over checking + strpath += "/"; + result = s3fscurl.HeadRequest(strpath.c_str(), (*pheader)); + s3fscurl.DestroyCurlHandle(); + } + if(support_compat_dir && 0 != result){ + // now path is "object/", do check "object_$folder$" for over checking + strpath.erase(strpath.length() - 1); + strpath += "_$folder$"; + result = s3fscurl.HeadRequest(strpath.c_str(), (*pheader)); + s3fscurl.DestroyCurlHandle(); + + if(0 != result){ + // cut "_$folder$" for over checking "no dir object" after here + if(std::string::npos != (Pos = strpath.find("_$folder$", 0))){ + strpath.erase(Pos); + } + } + } + } + if(0 != result && std::string::npos == strpath.find("_$folder$", 0)){ + // now path is "object" or "object/", do check "no dir object" which is not object but has only children. + // + // [NOTE] + // If the path is mount point and there is no Stat information file for it, we need this process. + // + if('/' == *strpath.rbegin()){ + strpath.erase(strpath.length() - 1); + } + if(-ENOTEMPTY == directory_empty(strpath.c_str())){ + // found "no dir object". + strpath += "/"; + *pisforce = true; + result = 0; + } + } + }else{ + if('/' != *strpath.rbegin() && std::string::npos == strpath.find("_$folder$", 0) && is_need_check_obj_detail(*pheader)){ + // check a case of that "object" does not have attribute and "object" is possible to be directory. + if(-ENOTEMPTY == directory_empty(strpath.c_str())){ + // found "no dir object". + strpath += "/"; + *pisforce = true; + result = 0; + } + } + } + + // set headers for mount point from default stat + if(is_mountpoint){ + if(0 != result || pheader->empty()){ + pHasMpStat->Set(false); + + // [NOTE] + // If mount point and no stat information file, create header + // information from the default stat. + // + (*pheader)["Content-Type"] = S3fsCurl::LookupMimeType(strpath); + (*pheader)["x-amz-meta-uid"] = std::to_string(pstat->st_uid); + (*pheader)["x-amz-meta-gid"] = std::to_string(pstat->st_gid); + (*pheader)["x-amz-meta-mode"] = std::to_string(pstat->st_mode); + (*pheader)["x-amz-meta-atime"] = std::to_string(pstat->st_atime); + (*pheader)["x-amz-meta-ctime"] = std::to_string(pstat->st_ctime); + (*pheader)["x-amz-meta-mtime"] = std::to_string(pstat->st_mtime); + + result = 0; + }else{ + pHasMpStat->Set(true); + } + } + + // [NOTE] + // If the file is listed but not allowed access, put it in + // the positive cache instead of the negative cache. + // + // When mount points, the following error does not occur. + // + if(0 != result && -EPERM != result){ + // finally, "path" object did not find. Add no object cache. + strpath = path; // reset original + StatCache::getStatCacheData()->AddNoObjectCache(strpath); + return result; + } + + // set cache key + if(is_bucket_mountpoint){ + strpath = "/"; + }else if(std::string::npos != (Pos = strpath.find("_$folder$", 0))){ + // if path has "_$folder$", need to cut it. + strpath.erase(Pos); + strpath += "/"; + } + + // Set into cache + // + // [NOTE] + // When add_no_truncate_cache is true, the stats is always cached. + // This cached stats is only removed by DelStat(). + // This is necessary for the case to access the attribute of opened file. + // (ex. getxattr() is called while writing to the opened file.) + // + if(add_no_truncate_cache || 0 != StatCache::getStatCacheData()->GetCacheSize()){ + // add into stat cache + if(!StatCache::getStatCacheData()->AddStat(strpath, (*pheader), forcedir, add_no_truncate_cache)){ + S3FS_PRN_ERR("failed adding stat cache [path=%s]", strpath.c_str()); + return -ENOENT; + } + if(!StatCache::getStatCacheData()->GetStat(strpath, pstat, pheader, overcheck, pisforce)){ + // There is not in cache.(why?) -> retry to convert. + if(!convert_header_to_stat(strpath.c_str(), (*pheader), pstat, forcedir)){ + S3FS_PRN_ERR("failed convert headers to stat[path=%s]", strpath.c_str()); + return -ENOENT; + } + } + }else{ + // cache size is Zero -> only convert. + if(!convert_header_to_stat(strpath.c_str(), (*pheader), pstat, forcedir)){ + S3FS_PRN_ERR("failed convert headers to stat[path=%s]", strpath.c_str()); + return -ENOENT; + } + } + + if(is_mountpoint){ + // if mount point, we need to set this. + pstat->st_nlink = 1; // see fuse faq + } + + return 0; +} + +// +// Check the object uid and gid for write/read/execute. +// The param "mask" is as same as access() function. +// If there is not a target file, this function returns -ENOENT. +// If the target file can be accessed, the result always is 0. +// +// path: the target object path +// mask: bit field(F_OK, R_OK, W_OK, X_OK) like access(). +// stat: nullptr or the pointer of struct stat. +// +static int check_object_access(const char* path, int mask, struct stat* pstbuf) +{ + int result; + struct stat st; + struct stat* pst = (pstbuf ? pstbuf : &st); + struct fuse_context* pcxt; + + S3FS_PRN_DBG("[path=%s]", path); + + if(nullptr == (pcxt = fuse_get_context())){ + return -EIO; + } + S3FS_PRN_DBG("[pid=%u,uid=%u,gid=%u]", (unsigned int)(pcxt->pid), (unsigned int)(pcxt->uid), (unsigned int)(pcxt->gid)); + + if(0 != (result = get_object_attribute(path, pst))){ + // If there is not the target file(object), result is -ENOENT. + return result; + } + if(0 == pcxt->uid){ + // root is allowed all accessing. + return 0; + } + if(is_s3fs_uid && s3fs_uid == pcxt->uid){ + // "uid" user is allowed all accessing. + return 0; + } + if(F_OK == mask){ + // if there is a file, always return allowed. + return 0; + } + + // for "uid", "gid" option + uid_t obj_uid = (is_s3fs_uid ? s3fs_uid : pst->st_uid); + gid_t obj_gid = (is_s3fs_gid ? s3fs_gid : pst->st_gid); + + // compare file mode and uid/gid + mask. + mode_t mode; + mode_t base_mask = S_IRWXO; + if(is_s3fs_umask){ + // If umask is set, all object attributes set ~umask. + mode = ((S_IRWXU | S_IRWXG | S_IRWXO) & ~s3fs_umask); + }else{ + mode = pst->st_mode; + } + if(pcxt->uid == obj_uid){ + base_mask |= S_IRWXU; + } + if(pcxt->gid == obj_gid){ + base_mask |= S_IRWXG; + } else if(1 == is_uid_include_group(pcxt->uid, obj_gid)){ + base_mask |= S_IRWXG; + } + mode &= base_mask; + + if(X_OK == (mask & X_OK)){ + if(0 == (mode & (S_IXUSR | S_IXGRP | S_IXOTH))){ + return -EACCES; + } + } + if(W_OK == (mask & W_OK)){ + if(0 == (mode & (S_IWUSR | S_IWGRP | S_IWOTH))){ + return -EACCES; + } + } + if(R_OK == (mask & R_OK)){ + if(0 == (mode & (S_IRUSR | S_IRGRP | S_IROTH))){ + return -EACCES; + } + } + if(0 == mode){ + return -EACCES; + } + return 0; +} + +static int check_object_owner(const char* path, struct stat* pstbuf) +{ + int result; + struct stat st; + struct stat* pst = (pstbuf ? pstbuf : &st); + const struct fuse_context* pcxt; + + S3FS_PRN_DBG("[path=%s]", path); + + if(nullptr == (pcxt = fuse_get_context())){ + return -EIO; + } + if(0 != (result = get_object_attribute(path, pst))){ + // If there is not the target file(object), result is -ENOENT. + return result; + } + // check owner + if(0 == pcxt->uid){ + // root is allowed all accessing. + return 0; + } + if(is_s3fs_uid && s3fs_uid == pcxt->uid){ + // "uid" user is allowed all accessing. + return 0; + } + if(pcxt->uid == pst->st_uid){ + return 0; + } + return -EPERM; +} + +// +// Check accessing the parent directories of the object by uid and gid. +// +static int check_parent_object_access(const char* path, int mask) +{ + std::string parent; + int result; + + S3FS_PRN_DBG("[path=%s]", path); + + if(0 == strcmp(path, "/") || 0 == strcmp(path, ".")){ + // path is mount point. + return 0; + } + if(X_OK == (mask & X_OK)){ + for(parent = mydirname(path); !parent.empty(); parent = mydirname(parent)){ + if(parent == "."){ + parent = "/"; + } + if(0 != (result = check_object_access(parent.c_str(), X_OK, nullptr))){ + return result; + } + if(parent == "/" || parent == "."){ + break; + } + } + } + mask = (mask & ~X_OK); + if(0 != mask){ + parent = mydirname(path); + if(parent == "."){ + parent = "/"; + } + if(0 != (result = check_object_access(parent.c_str(), mask, nullptr))){ + return result; + } + } + return 0; +} + +// +// ssevalue is MD5 for SSE-C type, or KMS id for SSE-KMS +// +bool get_object_sse_type(const char* path, sse_type_t& ssetype, std::string& ssevalue) +{ + if(!path){ + return false; + } + + headers_t meta; + if(0 != get_object_attribute(path, nullptr, &meta)){ + S3FS_PRN_ERR("Failed to get object(%s) headers", path); + return false; + } + + ssetype = sse_type_t::SSE_DISABLE; + ssevalue.erase(); + for(headers_t::iterator iter = meta.begin(); iter != meta.end(); ++iter){ + std::string key = (*iter).first; + if(0 == strcasecmp(key.c_str(), "x-amz-server-side-encryption") && 0 == strcasecmp((*iter).second.c_str(), "AES256")){ + ssetype = sse_type_t::SSE_S3; + }else if(0 == strcasecmp(key.c_str(), "x-amz-server-side-encryption-aws-kms-key-id")){ + ssetype = sse_type_t::SSE_KMS; + ssevalue = (*iter).second; + }else if(0 == strcasecmp(key.c_str(), "x-amz-server-side-encryption-customer-key-md5")){ + ssetype = sse_type_t::SSE_C; + ssevalue = (*iter).second; + } + } + return true; +} + +static int get_local_fent(AutoFdEntity& autoent, FdEntity **entity, const char* path, int flags, bool is_load) +{ + int result; + struct stat stobj; + FdEntity* ent; + headers_t meta; + + S3FS_PRN_INFO2("[path=%s]", path); + + if(0 != (result = get_object_attribute(path, &stobj, &meta))){ + return result; + } + + // open + struct timespec st_mctime; + if(!S_ISREG(stobj.st_mode) && !S_ISLNK(stobj.st_mode)){ + st_mctime = S3FS_OMIT_TS; + }else{ + set_stat_to_timespec(stobj, stat_time_type::MTIME, st_mctime); + } + bool force_tmpfile = S_ISREG(stobj.st_mode) ? false : true; + + if(nullptr == (ent = autoent.Open(path, &meta, stobj.st_size, st_mctime, flags, force_tmpfile, true, false, AutoLock::NONE))){ + S3FS_PRN_ERR("Could not open file. errno(%d)", errno); + return -EIO; + } + // load + if(is_load && !ent->LoadAll(autoent.GetPseudoFd(), &meta)){ + S3FS_PRN_ERR("Could not load file. errno(%d)", errno); + autoent.Close(); + return -EIO; + } + *entity = ent; + return 0; +} + +// +// create or update s3 meta +// @return fuse return code +// +int put_headers(const char* path, headers_t& meta, bool is_copy, bool use_st_size) +{ + int result; + S3fsCurl s3fscurl(true); + off_t size; + std::string strpath; + + S3FS_PRN_INFO2("[path=%s]", path); + + if(0 == strcmp(path, "/") && mount_prefix.empty()){ + strpath = "//"; // for the mount point that is bucket root, change "/" to "//". + }else{ + strpath = path; + } + + // files larger than 5GB must be modified via the multipart interface + // call use_st_size as false when the file does not exist(ex. rename object) + if(use_st_size && '/' != *strpath.rbegin()){ // directory object("dir/") is always 0(Content-Length = 0) + struct stat buf; + if(0 != (result = get_object_attribute(path, &buf))){ + return result; + } + size = buf.st_size; + }else{ + size = get_size(meta); + } + + if(!nocopyapi && !nomultipart && size >= multipart_threshold){ + if(0 != (result = s3fscurl.MultipartHeadRequest(strpath.c_str(), size, meta, is_copy))){ + return result; + } + }else{ + if(0 != (result = s3fscurl.PutHeadRequest(strpath.c_str(), meta, is_copy))){ + return result; + } + } + return 0; +} + +static int s3fs_getattr(const char* _path, struct stat* stbuf) +{ + WTF8_ENCODE(path) + int result; + +#if defined(__APPLE__) + FUSE_CTX_DBG("[path=%s]", path); +#else + FUSE_CTX_INFO("[path=%s]", path); +#endif + + // check parent directory attribute. + if(0 != (result = check_parent_object_access(path, X_OK))){ + return result; + } + if(0 != (result = check_object_access(path, F_OK, stbuf))){ + return result; + } + // If has already opened fd, the st_size should be instead. + // (See: Issue 241) + if(stbuf){ + AutoFdEntity autoent; + const FdEntity* ent; + if(nullptr != (ent = autoent.OpenExistFdEntity(path))){ + struct stat tmpstbuf; + if(ent->GetStats(tmpstbuf)){ + stbuf->st_size = tmpstbuf.st_size; + } + } + if(0 == strcmp(path, "/")){ + stbuf->st_size = 4096; + } + stbuf->st_blksize = 4096; + stbuf->st_blocks = get_blocks(stbuf->st_size); + + S3FS_PRN_DBG("[path=%s] uid=%u, gid=%u, mode=%04o", path, (unsigned int)(stbuf->st_uid), (unsigned int)(stbuf->st_gid), stbuf->st_mode); + } + S3FS_MALLOCTRIM(0); + + return result; +} + +static int s3fs_readlink(const char* _path, char* buf, size_t size) +{ + if(!_path || !buf || 0 == size){ + return 0; + } + WTF8_ENCODE(path) + std::string strValue; + FUSE_CTX_INFO("[path=%s]", path); + + // check symbolic link cache + if(!StatCache::getStatCacheData()->GetSymlink(path, strValue)){ + // not found in cache, then open the path + { // scope for AutoFdEntity + AutoFdEntity autoent; + FdEntity* ent; + int result; + if(0 != (result = get_local_fent(autoent, &ent, path, O_RDONLY))){ + S3FS_PRN_ERR("could not get fent(file=%s)", path); + return result; + } + // Get size + off_t readsize; + if(!ent->GetSize(readsize)){ + S3FS_PRN_ERR("could not get file size(file=%s)", path); + return -EIO; + } + if(static_cast(size) <= readsize){ + readsize = size - 1; + } + // Read + ssize_t ressize; + if(0 > (ressize = ent->Read(autoent.GetPseudoFd(), buf, 0, readsize))){ + S3FS_PRN_ERR("could not read file(file=%s, ressize=%zd)", path, ressize); + return static_cast(ressize); + } + buf[ressize] = '\0'; + } + + // check buf if it has space words. + strValue = trim(buf); + + // decode wtf8. This will always be shorter + if(use_wtf8){ + strValue = s3fs_wtf8_decode(strValue); + } + + // add symbolic link cache + if(!StatCache::getStatCacheData()->AddSymlink(path, strValue)){ + S3FS_PRN_ERR("failed to add symbolic link cache for %s", path); + } + } + // copy result + strncpy(buf, strValue.c_str(), size - 1); + buf[size - 1] = '\0'; + + S3FS_MALLOCTRIM(0); + + return 0; +} + +// common function for creation of a plain object +static int create_file_object(const char* path, mode_t mode, uid_t uid, gid_t gid) +{ + S3FS_PRN_INFO2("[path=%s][mode=%04o]", path, mode); + + std::string strnow = s3fs_str_realtime(); + headers_t meta; + meta["Content-Type"] = S3fsCurl::LookupMimeType(path); + meta["x-amz-meta-uid"] = std::to_string(uid); + meta["x-amz-meta-gid"] = std::to_string(gid); + meta["x-amz-meta-mode"] = std::to_string(mode); + meta["x-amz-meta-atime"] = strnow; + meta["x-amz-meta-ctime"] = strnow; + meta["x-amz-meta-mtime"] = strnow; + + S3fsCurl s3fscurl(true); + return s3fscurl.PutRequest(path, meta, -1); // fd=-1 means for creating zero byte object. +} + +static int s3fs_mknod(const char *_path, mode_t mode, dev_t rdev) +{ + WTF8_ENCODE(path) + int result; + struct fuse_context* pcxt; + + FUSE_CTX_INFO("[path=%s][mode=%04o][dev=%llu]", path, mode, (unsigned long long)rdev); + + if(nullptr == (pcxt = fuse_get_context())){ + return -EIO; + } + + if(0 != (result = create_file_object(path, mode, pcxt->uid, pcxt->gid))){ + S3FS_PRN_ERR("could not create object for special file(result=%d)", result); + return result; + } + StatCache::getStatCacheData()->DelStat(path); + + // update parent directory timestamp + int update_result; + if(0 != (update_result = update_mctime_parent_directory(path))){ + S3FS_PRN_ERR("succeed to mknod the file(%s), but could not update timestamp of its parent directory(result=%d).", path, update_result); + } + + S3FS_MALLOCTRIM(0); + + return result; +} + +static int s3fs_create(const char* _path, mode_t mode, struct fuse_file_info* fi) +{ + WTF8_ENCODE(path) + int result; + const struct fuse_context* pcxt; + + FUSE_CTX_INFO("[path=%s][mode=%04o][flags=0x%x]", path, mode, fi->flags); + + if(nullptr == (pcxt = fuse_get_context())){ + return -EIO; + } + + // check parent directory attribute. + if(0 != (result = check_parent_object_access(path, X_OK))){ + return result; + } + result = check_object_access(path, W_OK, nullptr); + if(-ENOENT == result){ + if(0 != (result = check_parent_object_access(path, W_OK))){ + return result; + } + }else if(0 != result){ + return result; + } + + std::string strnow = s3fs_str_realtime(); + headers_t meta; + meta["Content-Length"] = "0"; + meta["x-amz-meta-uid"] = std::to_string(pcxt->uid); + meta["x-amz-meta-gid"] = std::to_string(pcxt->gid); + meta["x-amz-meta-mode"] = std::to_string(mode); + meta["x-amz-meta-atime"] = strnow; + meta["x-amz-meta-mtime"] = strnow; + meta["x-amz-meta-ctime"] = strnow; + + std::string xattrvalue; + if(build_inherited_xattr_value(path, xattrvalue)){ + S3FS_PRN_DBG("Set xattrs = %s", urlDecode(xattrvalue).c_str()); + meta["x-amz-meta-xattr"] = xattrvalue; + } + + // [NOTE] set no_truncate flag + // At this point, the file has not been created(uploaded) and + // the data is only present in the Stats cache. + // The Stats cache should not be deleted automatically by + // timeout. If this stats is deleted, s3fs will try to get it + // from the server with a Head request and will get an + // unexpected error because the result object does not exist. + // + if(!StatCache::getStatCacheData()->AddStat(path, meta, false, true)){ + return -EIO; + } + + AutoFdEntity autoent; + FdEntity* ent; + int error = 0; + if(nullptr == (ent = autoent.Open(path, &meta, 0, S3FS_OMIT_TS, fi->flags, false, true, false, AutoLock::NONE, &error))){ + StatCache::getStatCacheData()->DelStat(path); + return error; + } + ent->MarkDirtyNewFile(); + fi->fh = autoent.Detach(); // KEEP fdentity open; + + S3FS_MALLOCTRIM(0); + + return 0; +} + +static int create_directory_object(const char* path, mode_t mode, const struct timespec& ts_atime, const struct timespec& ts_mtime, const struct timespec& ts_ctime, uid_t uid, gid_t gid, const char* pxattrvalue) +{ + S3FS_PRN_INFO1("[path=%s][mode=%04o][atime=%s][mtime=%s][ctime=%s][uid=%u][gid=%u]", path, mode, str(ts_atime).c_str(), str(ts_mtime).c_str(), str(ts_ctime).c_str(), (unsigned int)uid, (unsigned int)gid); + + if(!path || '\0' == path[0]){ + return -EINVAL; + } + std::string tpath = path; + if('/' != *tpath.rbegin()){ + tpath += "/"; + }else if("/" == tpath && mount_prefix.empty()){ + tpath = "//"; // for the mount point that is bucket root, change "/" to "//". + } + + headers_t meta; + meta["x-amz-meta-uid"] = std::to_string(uid); + meta["x-amz-meta-gid"] = std::to_string(gid); + meta["x-amz-meta-mode"] = std::to_string(mode); + meta["x-amz-meta-atime"] = str(ts_atime); + meta["x-amz-meta-mtime"] = str(ts_mtime); + meta["x-amz-meta-ctime"] = str(ts_ctime); + + if(pxattrvalue){ + S3FS_PRN_DBG("Set xattrs = %s", urlDecode(pxattrvalue).c_str()); + meta["x-amz-meta-xattr"] = pxattrvalue; + } + + S3fsCurl s3fscurl; + return s3fscurl.PutRequest(tpath.c_str(), meta, -1); // fd=-1 means for creating zero byte object. +} + +static int s3fs_mkdir(const char* _path, mode_t mode) +{ + WTF8_ENCODE(path) + int result; + struct fuse_context* pcxt; + + FUSE_CTX_INFO("[path=%s][mode=%04o]", path, mode); + + if(nullptr == (pcxt = fuse_get_context())){ + return -EIO; + } + + // check parent directory attribute. + if(0 != (result = check_parent_object_access(path, W_OK | X_OK))){ + return result; + } + if(-ENOENT != (result = check_object_access(path, F_OK, nullptr))){ + if(0 == result){ + result = -EEXIST; + } + return result; + } + + std::string xattrvalue; + const char* pxattrvalue; + if(get_parent_meta_xattr_value(path, xattrvalue)){ + pxattrvalue = xattrvalue.c_str(); + }else{ + pxattrvalue = nullptr; + } + + struct timespec now; + s3fs_realtime(now); + result = create_directory_object(path, mode, now, now, now, pcxt->uid, pcxt->gid, pxattrvalue); + + StatCache::getStatCacheData()->DelStat(path); + + // update parent directory timestamp + int update_result; + if(0 != (update_result = update_mctime_parent_directory(path))){ + S3FS_PRN_ERR("succeed to create the directory(%s), but could not update timestamp of its parent directory(result=%d).", path, update_result); + } + + S3FS_MALLOCTRIM(0); + + return result; +} + +static int s3fs_unlink(const char* _path) +{ + WTF8_ENCODE(path) + int result; + + FUSE_CTX_INFO("[path=%s]", path); + + if(0 != (result = check_parent_object_access(path, W_OK | X_OK))){ + return result; + } + + if(use_newcache){ + result = accessor->Delete(path); + }else{ + S3fsCurl s3fscurl; + result = s3fscurl.DeleteRequest(path); + FdManager::DeleteCacheFile(path); + } + + StatCache::getStatCacheData()->DelStat(path); + StatCache::getStatCacheData()->DelSymlink(path); + + // update parent directory timestamp + int update_result; + if(0 != (update_result = update_mctime_parent_directory(path))){ + S3FS_PRN_ERR("succeed to remove the file(%s), but could not update timestamp of its parent directory(result=%d).", path, update_result); + } + + S3FS_MALLOCTRIM(0); + + return result; +} + +static int directory_empty(const char* path) +{ + int result; + S3ObjList head; + + if((result = list_bucket(path, head, "/", true)) != 0){ + S3FS_PRN_ERR("list_bucket returns error."); + return result; + } + if(!head.IsEmpty()){ + return -ENOTEMPTY; + } + return 0; +} + +static int s3fs_rmdir(const char* _path) +{ + WTF8_ENCODE(path) + int result; + std::string strpath; + struct stat stbuf; + + FUSE_CTX_INFO("[path=%s]", path); + + if(0 != (result = check_parent_object_access(path, W_OK | X_OK))){ + return result; + } + + // directory must be empty + if(directory_empty(path) != 0){ + return -ENOTEMPTY; + } + + strpath = path; + if('/' != *strpath.rbegin()){ + strpath += "/"; + } + S3fsCurl s3fscurl; + result = s3fscurl.DeleteRequest(strpath.c_str()); + s3fscurl.DestroyCurlHandle(); + StatCache::getStatCacheData()->DelStat(strpath.c_str()); + + // double check for old version(before 1.63) + // The old version makes "dir" object, newer version makes "dir/". + // A case, there is only "dir", the first removing object is "dir/". + // Then "dir/" is not exists, but curl_delete returns 0. + // So need to check "dir" and should be removed it. + if('/' == *strpath.rbegin()){ + strpath.erase(strpath.length() - 1); + } + if(0 == get_object_attribute(strpath.c_str(), &stbuf, nullptr, false)){ + if(S_ISDIR(stbuf.st_mode)){ + // Found "dir" object. + result = s3fscurl.DeleteRequest(strpath.c_str()); + s3fscurl.DestroyCurlHandle(); + StatCache::getStatCacheData()->DelStat(strpath.c_str()); + } + } + // If there is no "dir" and "dir/" object(this case is made by s3cmd/s3sync), + // the cache key is "dir/". So we get error only once(delete "dir/"). + + // check for "_$folder$" object. + // This processing is necessary for other S3 clients compatibility. + if(is_special_name_folder_object(strpath.c_str())){ + strpath += "_$folder$"; + result = s3fscurl.DeleteRequest(strpath.c_str()); + } + + // update parent directory timestamp + int update_result; + if(0 != (update_result = update_mctime_parent_directory(path))){ + S3FS_PRN_ERR("succeed to remove the directory(%s), but could not update timestamp of its parent directory(result=%d).", path, update_result); + } + + S3FS_MALLOCTRIM(0); + + return result; +} + +static int s3fs_symlink(const char* _from, const char* _to) +{ + WTF8_ENCODE(from) + WTF8_ENCODE(to) + int result; + const struct fuse_context* pcxt; + + FUSE_CTX_INFO("[from=%s][to=%s]", from, to); + + if(nullptr == (pcxt = fuse_get_context())){ + return -EIO; + } + if(0 != (result = check_parent_object_access(to, W_OK | X_OK))){ + return result; + } + if(-ENOENT != (result = check_object_access(to, F_OK, nullptr))){ + if(0 == result){ + result = -EEXIST; + } + return result; + } + + std::string strnow = s3fs_str_realtime(); + headers_t headers; + headers["Content-Type"] = "application/octet-stream"; // Static + headers["x-amz-meta-mode"] = std::to_string(S_IFLNK | S_IRWXU | S_IRWXG | S_IRWXO); + headers["x-amz-meta-atime"] = strnow; + headers["x-amz-meta-ctime"] = strnow; + headers["x-amz-meta-mtime"] = strnow; + headers["x-amz-meta-uid"] = std::to_string(pcxt->uid); + headers["x-amz-meta-gid"] = std::to_string(pcxt->gid); + + // [NOTE] + // Symbolic links do not set xattrs. + + // open tmpfile + std::string strFrom; + { // scope for AutoFdEntity + AutoFdEntity autoent; + FdEntity* ent; + if(nullptr == (ent = autoent.Open(to, &headers, 0, S3FS_OMIT_TS, O_RDWR, true, true, false, AutoLock::NONE))){ + S3FS_PRN_ERR("could not open tmpfile(errno=%d)", errno); + return -errno; + } + // write(without space words) + strFrom = trim(from); + ssize_t from_size = static_cast(strFrom.length()); + ssize_t ressize; + if(from_size != (ressize = ent->Write(autoent.GetPseudoFd(), strFrom.c_str(), 0, from_size, true))){ + if(ressize < 0){ + S3FS_PRN_ERR("could not write tmpfile(errno=%d)", static_cast(ressize)); + return static_cast(ressize); + }else{ + S3FS_PRN_ERR("could not write tmpfile %zd byte(errno=%d)", ressize, errno); + return (0 == errno ? -EIO : -errno); + } + } + // upload + if(0 != (result = ent->Flush(autoent.GetPseudoFd(), AutoLock::NONE, true, true))){ + S3FS_PRN_WARN("could not upload tmpfile(result=%d)", result); + } + } + + StatCache::getStatCacheData()->DelStat(to); + if(!StatCache::getStatCacheData()->AddSymlink(to, strFrom)){ + S3FS_PRN_ERR("failed to add symbolic link cache for %s", to); + } + + // update parent directory timestamp + int update_result; + if(0 != (update_result = update_mctime_parent_directory(to))){ + S3FS_PRN_ERR("succeed to create symbolic link(%s), but could not update timestamp of its parent directory(result=%d).", to, update_result); + } + + S3FS_MALLOCTRIM(0); + + return result; +} + +static int rename_object(const char* from, const char* to, bool update_ctime) +{ + int result; + headers_t meta; + struct stat buf; + + S3FS_PRN_INFO1("[from=%s][to=%s]", from , to); + + if(0 != (result = check_parent_object_access(to, W_OK | X_OK))){ + // not permit writing "to" object parent dir. + return result; + } + if(0 != (result = check_parent_object_access(from, W_OK | X_OK))){ + // not permit removing "from" object parent dir. + return result; + } + if(0 != (result = get_object_attribute(from, &buf, &meta))){ + return result; + } + + std::string strSourcePath = (mount_prefix.empty() && 0 == strcmp("/", from)) ? "//" : from; + + if(update_ctime){ + meta["x-amz-meta-ctime"] = s3fs_str_realtime(); + } + meta["x-amz-copy-source"] = urlEncodePath(service_path + S3fsCred::GetBucket() + get_realpath(strSourcePath.c_str())); + meta["Content-Type"] = S3fsCurl::LookupMimeType(to); + meta["x-amz-metadata-directive"] = "REPLACE"; + + std::string xattrvalue; + if(get_meta_xattr_value(from, xattrvalue)){ + S3FS_PRN_DBG("Set xattrs = %s", urlDecode(xattrvalue).c_str()); + meta["x-amz-meta-xattr"] = xattrvalue; + } + + // [NOTE] + // If it has a cache, open it first and leave it open until rename. + // The cache is renamed after put_header, because it must be open + // at the time of renaming. + { + // update time + AutoFdEntity autoent; + FdEntity* ent; + if(nullptr == (ent = autoent.OpenExistFdEntity(from))){ + // no opened fd + + // get mtime/ctime/atime from meta + struct timespec mtime = get_mtime(meta); + struct timespec ctime = get_ctime(meta); + struct timespec atime = get_atime(meta); + if(mtime.tv_sec < 0){ + mtime.tv_sec = 0L; + mtime.tv_nsec = 0L; + } + if(ctime.tv_sec < 0){ + ctime.tv_sec = 0L; + ctime.tv_nsec = 0L; + } + if(atime.tv_sec < 0){ + atime.tv_sec = 0L; + atime.tv_nsec = 0L; + } + + if(FdManager::IsCacheDir()){ + // create cache file if be needed + // + // [NOTE] + // Do not specify "S3FS_OMIT_TS" for mctime parameter. + // This is because if the cache file does not exist, the pagelist for it + // will be recreated, but the entire file area indicated by this pagelist + // will be in the "modified" state. + // This does not affect the rename process, but the cache information in + // the "modified" state remains, making it impossible to read the file correctly. + // + ent = autoent.Open(from, &meta, buf.st_size, mtime, O_RDONLY, false, true, false, AutoLock::NONE); + } + if(ent){ + ent->SetMCtime(mtime, ctime); + ent->SetAtime(atime); + } + } + + // copy + if(0 != (result = put_headers(to, meta, true, /* use_st_size= */ false))){ + return result; + } + + // rename + FdManager::get()->Rename(from, to); + } + + // Remove file + result = s3fs_unlink(from); + + StatCache::getStatCacheData()->DelStat(to); + + return result; +} + +static int rename_object_nocopy(const char* from, const char* to, bool update_ctime) +{ + int result; + + FUSE_CTX_INFO1("[from=%s][to=%s]", from , to); + + if(0 != (result = check_parent_object_access(to, W_OK | X_OK))){ + // not permit writing "to" object parent dir. + return result; + } + if(0 != (result = check_parent_object_access(from, W_OK | X_OK))){ + // not permit removing "from" object parent dir. + return result; + } + + // open & load + { // scope for AutoFdEntity + AutoFdEntity autoent; + FdEntity* ent; + if(0 != (result = get_local_fent(autoent, &ent, from, O_RDWR, true))){ + S3FS_PRN_ERR("could not open and read file(%s)", from); + return result; + } + + // Set header + if(!ent->SetContentType(to)){ + S3FS_PRN_ERR("could not set content-type for %s", to); + return -EIO; + } + + // update ctime + if(update_ctime){ + struct timespec ts; + s3fs_realtime(ts); + ent->SetCtime(ts); + } + + // upload + if(0 != (result = ent->RowFlush(autoent.GetPseudoFd(), to, AutoLock::NONE, true))){ + S3FS_PRN_ERR("could not upload file(%s): result=%d", to, result); + return result; + } + FdManager::get()->Rename(from, to); + } + + // Remove file + result = s3fs_unlink(from); + + // Stats + StatCache::getStatCacheData()->DelStat(to); + + return result; +} + +static int rename_large_object(const char* from, const char* to) +{ + int result; + struct stat buf; + headers_t meta; + + S3FS_PRN_INFO1("[from=%s][to=%s]", from , to); + + if(0 != (result = check_parent_object_access(to, W_OK | X_OK))){ + // not permit writing "to" object parent dir. + return result; + } + if(0 != (result = check_parent_object_access(from, W_OK | X_OK))){ + // not permit removing "from" object parent dir. + return result; + } + if(0 != (result = get_object_attribute(from, &buf, &meta, false))){ + return result; + } + + S3fsCurl s3fscurl(true); + if(0 != (result = s3fscurl.MultipartRenameRequest(from, to, meta, buf.st_size))){ + return result; + } + s3fscurl.DestroyCurlHandle(); + + // Rename cache file + FdManager::get()->Rename(from, to); + + // Remove file + result = s3fs_unlink(from); + + // Stats + StatCache::getStatCacheData()->DelStat(to); + + return result; +} + +static int clone_directory_object(const char* from, const char* to, bool update_ctime, const char* pxattrvalue) +{ + int result = -1; + struct stat stbuf; + + S3FS_PRN_INFO1("[from=%s][to=%s]", from, to); + + // get target's attributes + if(0 != (result = get_object_attribute(from, &stbuf))){ + return result; + } + + struct timespec ts_atime; + struct timespec ts_mtime; + struct timespec ts_ctime; + set_stat_to_timespec(stbuf, stat_time_type::ATIME, ts_atime); + set_stat_to_timespec(stbuf, stat_time_type::MTIME, ts_mtime); + if(update_ctime){ + s3fs_realtime(ts_ctime); + }else{ + set_stat_to_timespec(stbuf, stat_time_type::CTIME, ts_ctime); + } + result = create_directory_object(to, stbuf.st_mode, ts_atime, ts_mtime, ts_ctime, stbuf.st_uid, stbuf.st_gid, pxattrvalue); + + StatCache::getStatCacheData()->DelStat(to); + + return result; +} + +static int rename_directory(const char* from, const char* to) +{ + S3ObjList head; + s3obj_list_t headlist; + std::string strfrom = from ? from : ""; // from is without "/". + std::string strto = to ? to : ""; // to is without "/" too. + std::string basepath = strfrom + "/"; + std::string newpath; // should be from name(not used) + std::string nowcache; // now cache path(not used) + dirtype DirType; + bool normdir; + std::vector mvnodes; + struct stat stbuf; + int result; + bool is_dir; + + S3FS_PRN_INFO1("[from=%s][to=%s]", from, to); + + // + // Initiate and Add base directory into mvnode struct. + // + strto += "/"; + if(0 == chk_dir_object_type(from, newpath, strfrom, nowcache, nullptr, &DirType) && dirtype::UNKNOWN != DirType){ + if(dirtype::NOOBJ != DirType){ + normdir = false; + }else{ + normdir = true; + strfrom = from; // from directory is not removed, but from directory attr is needed. + } + mvnodes.emplace_back(strfrom, strto, true, normdir); + }else{ + // Something wrong about "from" directory. + } + + // + // get a list of all the objects + // + // No delimiter is specified, the result(head) is all object keys. + // (CommonPrefixes is empty, but all object is listed in Key.) + if(0 != (result = list_bucket(basepath.c_str(), head, nullptr))){ + S3FS_PRN_ERR("list_bucket returns error."); + return result; + } + head.GetNameList(headlist); // get name without "/". + StatCache::getStatCacheData()->GetNotruncateCache(basepath, headlist); // Add notruncate file name from stat cache + S3ObjList::MakeHierarchizedList(headlist, false); // add hierarchized dir. + + s3obj_list_t::const_iterator liter; + for(liter = headlist.begin(); headlist.end() != liter; ++liter){ + // make "from" and "to" object name. + std::string from_name = basepath + (*liter); + std::string to_name = strto + (*liter); + std::string etag = head.GetETag((*liter).c_str()); + + // Check subdirectory. + StatCache::getStatCacheData()->HasStat(from_name, etag.c_str()); // Check ETag + if(0 != get_object_attribute(from_name.c_str(), &stbuf, nullptr)){ + S3FS_PRN_WARN("failed to get %s object attribute.", from_name.c_str()); + continue; + } + if(S_ISDIR(stbuf.st_mode)){ + is_dir = true; + if(0 != chk_dir_object_type(from_name.c_str(), newpath, from_name, nowcache, nullptr, &DirType) || dirtype::UNKNOWN == DirType){ + S3FS_PRN_WARN("failed to get %s%s object directory type.", basepath.c_str(), (*liter).c_str()); + continue; + } + if(dirtype::NOOBJ != DirType){ + normdir = false; + }else{ + normdir = true; + from_name = basepath + (*liter); // from directory is not removed, but from directory attr is needed. + } + }else{ + is_dir = false; + normdir = false; + } + + // push this one onto the stack + mvnodes.emplace_back(from_name, to_name, is_dir, normdir); + } + + std::sort(mvnodes.begin(), mvnodes.end(), [](const mvnode& a, const mvnode& b) { return a.old_path < b.old_path; }); + + // + // rename + // + // rename directory objects. + for(auto mn_cur = mvnodes.cbegin(); mn_cur != mvnodes.cend(); ++mn_cur){ + if(mn_cur->is_dir && !mn_cur->old_path.empty()){ + std::string xattrvalue; + const char* pxattrvalue; + if(get_meta_xattr_value(mn_cur->old_path.c_str(), xattrvalue)){ + pxattrvalue = xattrvalue.c_str(); + }else{ + pxattrvalue = nullptr; + } + + // [NOTE] + // The ctime is updated only for the top (from) directory. + // Other than that, it will not be updated. + // + if(0 != (result = clone_directory_object(mn_cur->old_path.c_str(), mn_cur->new_path.c_str(), (strfrom == mn_cur->old_path), pxattrvalue))){ + S3FS_PRN_ERR("clone_directory_object returned an error(%d)", result); + return result; + } + } + } + + // iterate over the list - copy the files with rename_object + // does a safe copy - copies first and then deletes old + for(auto mn_cur = mvnodes.begin(); mn_cur != mvnodes.end(); ++mn_cur){ + if(!mn_cur->is_dir){ + if(!nocopyapi && !norenameapi){ + result = rename_object(mn_cur->old_path.c_str(), mn_cur->new_path.c_str(), false); // keep ctime + }else{ + result = rename_object_nocopy(mn_cur->old_path.c_str(), mn_cur->new_path.c_str(), false); // keep ctime + } + if(0 != result){ + S3FS_PRN_ERR("rename_object returned an error(%d)", result); + return result; + } + } + } + + // Iterate over old the directories, bottoms up and remove + for(auto mn_cur = mvnodes.rbegin(); mn_cur != mvnodes.rend(); ++mn_cur){ + if(mn_cur->is_dir && !mn_cur->old_path.empty()){ + if(!(mn_cur->is_normdir)){ + if(0 != (result = s3fs_rmdir(mn_cur->old_path.c_str()))){ + S3FS_PRN_ERR("s3fs_rmdir returned an error(%d)", result); + return result; + } + }else{ + // cache clear. + StatCache::getStatCacheData()->DelStat(mn_cur->old_path); + } + } + } + + return 0; +} + +static int s3fs_rename(const char* _from, const char* _to) +{ + WTF8_ENCODE(from) + WTF8_ENCODE(to) + struct stat buf; + int result; + + FUSE_CTX_INFO("[from=%s][to=%s]", from, to); + + if(0 != (result = check_parent_object_access(to, W_OK | X_OK))){ + // not permit writing "to" object parent dir. + return result; + } + if(0 != (result = check_parent_object_access(from, W_OK | X_OK))){ + // not permit removing "from" object parent dir. + return result; + } + if(0 != (result = get_object_attribute(from, &buf, nullptr))){ + return result; + } + if(0 != (result = directory_empty(to))){ + return result; + } + + // flush pending writes if file is open + { // scope for AutoFdEntity + AutoFdEntity autoent; + FdEntity* ent; + if(nullptr != (ent = autoent.OpenExistFdEntity(from, O_RDWR))){ + if(0 != (result = ent->Flush(autoent.GetPseudoFd(), AutoLock::NONE, true))){ + S3FS_PRN_ERR("could not upload file(%s): result=%d", to, result); + return result; + } + StatCache::getStatCacheData()->DelStat(from); + } + } + + // files larger than 5GB must be modified via the multipart interface + if(S_ISDIR(buf.st_mode)){ + result = rename_directory(from, to); + }else if(!nomultipart && buf.st_size >= singlepart_copy_limit){ + result = rename_large_object(from, to); + }else{ + if(!nocopyapi && !norenameapi){ + result = rename_object(from, to, true); // update ctime + }else{ + result = rename_object_nocopy(from, to, true); // update ctime + } + } + + // update parent directory timestamp + // + // [NOTE] + // already updated timestamp for original path in above functions. + // + int update_result; + if(0 != (update_result = update_mctime_parent_directory(to))){ + S3FS_PRN_ERR("succeed to create the file/directory(%s), but could not update timestamp of its parent directory(result=%d).", to, update_result); + } + + S3FS_MALLOCTRIM(0); + + return result; +} + +static int s3fs_link(const char* _from, const char* _to) +{ + WTF8_ENCODE(from) + WTF8_ENCODE(to) + FUSE_CTX_INFO("[from=%s][to=%s]", from, to); + return -ENOTSUP; +} + +static int s3fs_chmod(const char* _path, mode_t mode) +{ + WTF8_ENCODE(path) + int result; + std::string strpath; + std::string newpath; + std::string nowcache; + headers_t meta; + struct stat stbuf; + dirtype nDirType = dirtype::UNKNOWN; + + FUSE_CTX_INFO("[path=%s][mode=%04o]", path, mode); + + if(0 != (result = check_parent_object_access(path, X_OK))){ + return result; + } + if(0 != (result = check_object_owner(path, &stbuf))){ + return result; + } + + if(S_ISDIR(stbuf.st_mode)){ + result = chk_dir_object_type(path, newpath, strpath, nowcache, &meta, &nDirType); + }else{ + strpath = path; + nowcache = strpath; + result = get_object_attribute(strpath.c_str(), nullptr, &meta); + } + if(0 != result){ + return result; + } + + if(S_ISDIR(stbuf.st_mode) && (IS_REPLACEDIR(nDirType) || IS_CREATE_MP_STAT(path))){ + std::string xattrvalue; + const char* pxattrvalue; + if(get_meta_xattr_value(path, xattrvalue)){ + pxattrvalue = xattrvalue.c_str(); + }else{ + pxattrvalue = nullptr; + } + if(IS_REPLACEDIR(nDirType)){ + // Should rebuild directory object(except new type) + // Need to remove old dir("dir" etc) and make new dir("dir/") + + // At first, remove directory old object + if(0 != (result = remove_old_type_dir(strpath, nDirType))){ + return result; + } + } + StatCache::getStatCacheData()->DelStat(nowcache); + + // Make new directory object("dir/") + struct timespec ts_atime; + struct timespec ts_mtime; + struct timespec ts_ctime; + set_stat_to_timespec(stbuf, stat_time_type::ATIME, ts_atime); + set_stat_to_timespec(stbuf, stat_time_type::MTIME, ts_mtime); + s3fs_realtime(ts_ctime); + + if(0 != (result = create_directory_object(newpath.c_str(), mode, ts_atime, ts_mtime, ts_ctime, stbuf.st_uid, stbuf.st_gid, pxattrvalue))){ + return result; + } + }else{ + // normal object or directory object of newer version + std::string strSourcePath = (mount_prefix.empty() && "/" == strpath) ? "//" : strpath; + headers_t updatemeta; + updatemeta["x-amz-meta-ctime"] = s3fs_str_realtime(); + updatemeta["x-amz-meta-mode"] = std::to_string(mode); + updatemeta["x-amz-copy-source"] = urlEncodePath(service_path + S3fsCred::GetBucket() + get_realpath(strSourcePath.c_str())); + updatemeta["x-amz-metadata-directive"] = "REPLACE"; + + // check opened file handle. + // + // If the file starts uploading by multipart when the disk capacity is insufficient, + // we need to put these header after finishing upload. + // Or if the file is only open, we must update to FdEntity's internal meta. + // + AutoFdEntity autoent; + FdEntity* ent; + bool need_put_header = true; + if(nullptr != (ent = autoent.OpenExistFdEntity(path))){ + if(ent->MergeOrgMeta(updatemeta)){ + // meta is changed, but now uploading. + // then the meta is pending and accumulated to be put after the upload is complete. + S3FS_PRN_INFO("meta pending until upload is complete"); + need_put_header = false; + + // If there is data in the Stats cache, update the Stats cache. + StatCache::getStatCacheData()->UpdateMetaStats(strpath, updatemeta); + + // [NOTE] + // There are cases where this function is called during the process of + // creating a new file (before uploading). + // In this case, a temporary cache exists in the Stat cache. + // So we need to update the cache, if it exists. (see. s3fs_create and s3fs_utimens) + // + if(!StatCache::getStatCacheData()->AddStat(strpath, updatemeta, false, true)){ + return -EIO; + } + } + } + if(need_put_header){ + // not found opened file. + merge_headers(meta, updatemeta, true); + + // upload meta directly. + if(0 != (result = put_headers(strpath.c_str(), meta, true))){ + return result; + } + StatCache::getStatCacheData()->DelStat(nowcache); + } + } + S3FS_MALLOCTRIM(0); + + return 0; +} + +static int s3fs_chmod_nocopy(const char* _path, mode_t mode) +{ + WTF8_ENCODE(path) + int result; + std::string strpath; + std::string newpath; + std::string nowcache; + struct stat stbuf; + dirtype nDirType = dirtype::UNKNOWN; + + FUSE_CTX_INFO1("[path=%s][mode=%04o]", path, mode); + + if(0 != (result = check_parent_object_access(path, X_OK))){ + return result; + } + if(0 != (result = check_object_owner(path, &stbuf))){ + return result; + } + + // Get attributes + if(S_ISDIR(stbuf.st_mode)){ + result = chk_dir_object_type(path, newpath, strpath, nowcache, nullptr, &nDirType); + }else{ + strpath = path; + nowcache = strpath; + result = get_object_attribute(strpath.c_str(), nullptr, nullptr); + } + if(0 != result){ + return result; + } + + if(S_ISDIR(stbuf.st_mode)){ + std::string xattrvalue; + const char* pxattrvalue; + if(get_meta_xattr_value(path, xattrvalue)){ + pxattrvalue = xattrvalue.c_str(); + }else{ + pxattrvalue = nullptr; + } + + if(IS_REPLACEDIR(nDirType)){ + // Should rebuild all directory object + // Need to remove old dir("dir" etc) and make new dir("dir/") + + // At first, remove directory old object + if(0 != (result = remove_old_type_dir(strpath, nDirType))){ + return result; + } + } + StatCache::getStatCacheData()->DelStat(nowcache); + + // Make new directory object("dir/") + struct timespec ts_atime; + struct timespec ts_mtime; + struct timespec ts_ctime; + set_stat_to_timespec(stbuf, stat_time_type::ATIME, ts_atime); + set_stat_to_timespec(stbuf, stat_time_type::MTIME, ts_mtime); + s3fs_realtime(ts_ctime); + + if(0 != (result = create_directory_object(newpath.c_str(), mode, ts_atime, ts_mtime, ts_ctime, stbuf.st_uid, stbuf.st_gid, pxattrvalue))){ + return result; + } + }else{ + // normal object or directory object of newer version + + // open & load + AutoFdEntity autoent; + FdEntity* ent; + if(0 != (result = get_local_fent(autoent, &ent, strpath.c_str(), O_RDWR, true))){ + S3FS_PRN_ERR("could not open and read file(%s)", strpath.c_str()); + return result; + } + + struct timespec ts; + s3fs_realtime(ts); + ent->SetCtime(ts); + + // Change file mode + ent->SetMode(mode); + + // upload + if(0 != (result = ent->Flush(autoent.GetPseudoFd(), AutoLock::NONE, true))){ + S3FS_PRN_ERR("could not upload file(%s): result=%d", strpath.c_str(), result); + return result; + } + StatCache::getStatCacheData()->DelStat(nowcache); + } + S3FS_MALLOCTRIM(0); + + return result; +} + +static int s3fs_chown(const char* _path, uid_t uid, gid_t gid) +{ + WTF8_ENCODE(path) + int result; + std::string strpath; + std::string newpath; + std::string nowcache; + headers_t meta; + struct stat stbuf; + dirtype nDirType = dirtype::UNKNOWN; + + FUSE_CTX_INFO("[path=%s][uid=%u][gid=%u]", path, (unsigned int)uid, (unsigned int)gid); + + if(0 != (result = check_parent_object_access(path, X_OK))){ + return result; + } + if(0 != (result = check_object_owner(path, &stbuf))){ + return result; + } + + if((uid_t)(-1) == uid){ + uid = stbuf.st_uid; + } + if((gid_t)(-1) == gid){ + gid = stbuf.st_gid; + } + if(S_ISDIR(stbuf.st_mode)){ + result = chk_dir_object_type(path, newpath, strpath, nowcache, &meta, &nDirType); + }else{ + strpath = path; + nowcache = strpath; + result = get_object_attribute(strpath.c_str(), nullptr, &meta); + } + if(0 != result){ + return result; + } + + if(S_ISDIR(stbuf.st_mode) && (IS_REPLACEDIR(nDirType) || IS_CREATE_MP_STAT(path))){ + std::string xattrvalue; + const char* pxattrvalue; + if(get_meta_xattr_value(path, xattrvalue)){ + pxattrvalue = xattrvalue.c_str(); + }else{ + pxattrvalue = nullptr; + } + + if(IS_REPLACEDIR(nDirType)){ + // Should rebuild directory object(except new type) + // Need to remove old dir("dir" etc) and make new dir("dir/") + + // At first, remove directory old object + if(0 != (result = remove_old_type_dir(strpath, nDirType))){ + return result; + } + } + StatCache::getStatCacheData()->DelStat(nowcache); + + // Make new directory object("dir/") + struct timespec ts_atime; + struct timespec ts_mtime; + struct timespec ts_ctime; + set_stat_to_timespec(stbuf, stat_time_type::ATIME, ts_atime); + set_stat_to_timespec(stbuf, stat_time_type::MTIME, ts_mtime); + s3fs_realtime(ts_ctime); + + if(0 != (result = create_directory_object(newpath.c_str(), stbuf.st_mode, ts_atime, ts_mtime, ts_ctime, uid, gid, pxattrvalue))){ + return result; + } + }else{ + std::string strSourcePath = (mount_prefix.empty() && "/" == strpath) ? "//" : strpath; + headers_t updatemeta; + updatemeta["x-amz-meta-ctime"] = s3fs_str_realtime(); + updatemeta["x-amz-meta-uid"] = std::to_string(uid); + updatemeta["x-amz-meta-gid"] = std::to_string(gid); + updatemeta["x-amz-copy-source"] = urlEncodePath(service_path + S3fsCred::GetBucket() + get_realpath(strSourcePath.c_str())); + updatemeta["x-amz-metadata-directive"] = "REPLACE"; + + // check opened file handle. + // + // If the file starts uploading by multipart when the disk capacity is insufficient, + // we need to put these header after finishing upload. + // Or if the file is only open, we must update to FdEntity's internal meta. + // + AutoFdEntity autoent; + FdEntity* ent; + bool need_put_header = true; + if(nullptr != (ent = autoent.OpenExistFdEntity(path))){ + if(ent->MergeOrgMeta(updatemeta)){ + // meta is changed, but now uploading. + // then the meta is pending and accumulated to be put after the upload is complete. + S3FS_PRN_INFO("meta pending until upload is complete"); + need_put_header = false; + + // If there is data in the Stats cache, update the Stats cache. + StatCache::getStatCacheData()->UpdateMetaStats(strpath, updatemeta); + + // [NOTE] + // There are cases where this function is called during the process of + // creating a new file (before uploading). + // In this case, a temporary cache exists in the Stat cache. + // So we need to update the cache, if it exists. (see. s3fs_create and s3fs_utimens) + // + if(!StatCache::getStatCacheData()->AddStat(strpath, updatemeta, false, true)){ + return -EIO; + } + } + } + if(need_put_header){ + // not found opened file. + merge_headers(meta, updatemeta, true); + + // upload meta directly. + if(0 != (result = put_headers(strpath.c_str(), meta, true))){ + return result; + } + StatCache::getStatCacheData()->DelStat(nowcache); + } + } + S3FS_MALLOCTRIM(0); + + return 0; +} + +static int s3fs_chown_nocopy(const char* _path, uid_t uid, gid_t gid) +{ + WTF8_ENCODE(path) + int result; + std::string strpath; + std::string newpath; + std::string nowcache; + struct stat stbuf; + dirtype nDirType = dirtype::UNKNOWN; + + FUSE_CTX_INFO1("[path=%s][uid=%u][gid=%u]", path, (unsigned int)uid, (unsigned int)gid); + + if(0 != (result = check_parent_object_access(path, X_OK))){ + return result; + } + if(0 != (result = check_object_owner(path, &stbuf))){ + return result; + } + + if((uid_t)(-1) == uid){ + uid = stbuf.st_uid; + } + if((gid_t)(-1) == gid){ + gid = stbuf.st_gid; + } + + // Get attributes + if(S_ISDIR(stbuf.st_mode)){ + result = chk_dir_object_type(path, newpath, strpath, nowcache, nullptr, &nDirType); + }else{ + strpath = path; + nowcache = strpath; + result = get_object_attribute(strpath.c_str(), nullptr, nullptr); + } + if(0 != result){ + return result; + } + + if(S_ISDIR(stbuf.st_mode)){ + std::string xattrvalue; + const char* pxattrvalue; + if(get_meta_xattr_value(path, xattrvalue)){ + pxattrvalue = xattrvalue.c_str(); + }else{ + pxattrvalue = nullptr; + } + + if(IS_REPLACEDIR(nDirType)){ + // Should rebuild all directory object + // Need to remove old dir("dir" etc) and make new dir("dir/") + + // At first, remove directory old object + if(0 != (result = remove_old_type_dir(strpath, nDirType))){ + return result; + } + } + StatCache::getStatCacheData()->DelStat(nowcache); + + // Make new directory object("dir/") + struct timespec ts_atime; + struct timespec ts_mtime; + struct timespec ts_ctime; + set_stat_to_timespec(stbuf, stat_time_type::ATIME, ts_atime); + set_stat_to_timespec(stbuf, stat_time_type::MTIME, ts_mtime); + s3fs_realtime(ts_ctime); + + if(0 != (result = create_directory_object(newpath.c_str(), stbuf.st_mode, ts_atime, ts_mtime, ts_ctime, uid, gid, pxattrvalue))){ + return result; + } + }else{ + // normal object or directory object of newer version + + // open & load + AutoFdEntity autoent; + FdEntity* ent; + if(0 != (result = get_local_fent(autoent, &ent, strpath.c_str(), O_RDWR, true))){ + S3FS_PRN_ERR("could not open and read file(%s)", strpath.c_str()); + return result; + } + + struct timespec ts; + s3fs_realtime(ts); + ent->SetCtime(ts); + + // Change owner + ent->SetUId(uid); + ent->SetGId(gid); + + // upload + if(0 != (result = ent->Flush(autoent.GetPseudoFd(), AutoLock::NONE, true))){ + S3FS_PRN_ERR("could not upload file(%s): result=%d", strpath.c_str(), result); + return result; + } + StatCache::getStatCacheData()->DelStat(nowcache); + } + S3FS_MALLOCTRIM(0); + + return result; +} + +static timespec handle_utimens_special_values(timespec ts, timespec now, timespec orig) +{ + if(ts.tv_nsec == UTIME_NOW){ + return now; + }else if(ts.tv_nsec == UTIME_OMIT){ + return orig; + }else{ + return ts; + } +} + +static int update_mctime_parent_directory(const char* _path) +{ + if(!update_parent_dir_stat){ + // Disable updating parent directory stat. + S3FS_PRN_DBG("Updating parent directory stats is disabled"); + return 0; + } + + WTF8_ENCODE(path) + int result; + std::string parentpath; // parent directory path + std::string nowpath; // now directory object path("dir" or "dir/" or "xxx_$folder$", etc) + std::string newpath; // directory path for the current version("dir/") + std::string nowcache; + headers_t meta; + struct stat stbuf; + struct timespec mctime; + struct timespec atime; + dirtype nDirType = dirtype::UNKNOWN; + + S3FS_PRN_INFO2("[path=%s]", path); + + // get parent directory path + parentpath = mydirname(path); + + // check & get directory type + if(0 != (result = chk_dir_object_type(parentpath.c_str(), newpath, nowpath, nowcache, &meta, &nDirType))){ + return result; + } + + // get directory stat + // + // [NOTE] + // It is assumed that this function is called after the operation on + // the file is completed, so there is no need to check the permissions + // on the parent directory. + // + if(0 != (result = get_object_attribute(parentpath.c_str(), &stbuf))){ + // If there is not the target file(object), result is -ENOENT. + return result; + } + if(!S_ISDIR(stbuf.st_mode)){ + S3FS_PRN_ERR("path(%s) is not parent directory.", parentpath.c_str()); + return -EIO; + } + + // make atime/mtime/ctime for updating + s3fs_realtime(mctime); + set_stat_to_timespec(stbuf, stat_time_type::ATIME, atime); + + if(0 == atime.tv_sec && 0 == atime.tv_nsec){ + atime = mctime; + } + + if(nocopyapi || IS_REPLACEDIR(nDirType) || IS_CREATE_MP_STAT(parentpath.c_str())){ + // Should rebuild directory object(except new type) + // Need to remove old dir("dir" etc) and make new dir("dir/") + std::string xattrvalue; + const char* pxattrvalue; + if(get_meta_xattr_value(path, xattrvalue)){ + pxattrvalue = xattrvalue.c_str(); + }else{ + pxattrvalue = nullptr; + } + + // At first, remove directory old object + if(!nowpath.empty()){ + if(0 != (result = remove_old_type_dir(nowpath, nDirType))){ + return result; + } + } + if(!nowcache.empty()){ + StatCache::getStatCacheData()->DelStat(nowcache); + } + + // Make new directory object("dir/") + if(0 != (result = create_directory_object(newpath.c_str(), stbuf.st_mode, atime, mctime, mctime, stbuf.st_uid, stbuf.st_gid, pxattrvalue))){ + return result; + } + }else{ + std::string strSourcePath = (mount_prefix.empty() && "/" == nowpath) ? "//" : nowpath; + headers_t updatemeta; + updatemeta["x-amz-meta-mtime"] = str(mctime); + updatemeta["x-amz-meta-ctime"] = str(mctime); + updatemeta["x-amz-meta-atime"] = str(atime); + updatemeta["x-amz-copy-source"] = urlEncodePath(service_path + S3fsCred::GetBucket() + get_realpath(strSourcePath.c_str())); + updatemeta["x-amz-metadata-directive"] = "REPLACE"; + + merge_headers(meta, updatemeta, true); + + // upload meta for parent directory. + if(0 != (result = put_headers(nowpath.c_str(), meta, true))){ + return result; + } + StatCache::getStatCacheData()->DelStat(nowcache); + } + S3FS_MALLOCTRIM(0); + + return 0; +} + +static int s3fs_utimens(const char* _path, const struct timespec ts[2]) +{ + WTF8_ENCODE(path) + int result; + std::string strpath; + std::string newpath; + std::string nowcache; + headers_t meta; + struct stat stbuf; + dirtype nDirType = dirtype::UNKNOWN; + + FUSE_CTX_INFO("[path=%s][mtime=%s][ctime/atime=%s]", path, str(ts[1]).c_str(), str(ts[0]).c_str()); + + if(0 != (result = check_parent_object_access(path, X_OK))){ + return result; + } + if(0 != (result = check_object_access(path, W_OK, &stbuf))){ + if(0 != check_object_owner(path, &stbuf)){ + return result; + } + } + + struct timespec now; + struct timespec ts_atime; + struct timespec ts_ctime; + struct timespec ts_mtime; + + s3fs_realtime(now); + set_stat_to_timespec(stbuf, stat_time_type::ATIME, ts_atime); + set_stat_to_timespec(stbuf, stat_time_type::CTIME, ts_ctime); + set_stat_to_timespec(stbuf, stat_time_type::MTIME, ts_mtime); + + struct timespec atime = handle_utimens_special_values(ts[0], now, ts_atime); + struct timespec ctime = handle_utimens_special_values(ts[0], now, ts_ctime); + struct timespec mtime = handle_utimens_special_values(ts[1], now, ts_mtime); + + if(S_ISDIR(stbuf.st_mode)){ + result = chk_dir_object_type(path, newpath, strpath, nowcache, &meta, &nDirType); + }else{ + strpath = path; + nowcache = strpath; + result = get_object_attribute(strpath.c_str(), nullptr, &meta); + } + if(0 != result){ + return result; + } + + if(S_ISDIR(stbuf.st_mode) && (IS_REPLACEDIR(nDirType) || IS_CREATE_MP_STAT(path))){ + std::string xattrvalue; + const char* pxattrvalue; + if(get_meta_xattr_value(path, xattrvalue)){ + pxattrvalue = xattrvalue.c_str(); + }else{ + pxattrvalue = nullptr; + } + + if(IS_REPLACEDIR(nDirType)){ + // Should rebuild directory object(except new type) + // Need to remove old dir("dir" etc) and make new dir("dir/") + + // At first, remove directory old object + if(0 != (result = remove_old_type_dir(strpath, nDirType))){ + return result; + } + } + StatCache::getStatCacheData()->DelStat(nowcache); + + // Make new directory object("dir/") + if(0 != (result = create_directory_object(newpath.c_str(), stbuf.st_mode, atime, mtime, ctime, stbuf.st_uid, stbuf.st_gid, pxattrvalue))){ + return result; + } + }else{ + std::string strSourcePath = (mount_prefix.empty() && "/" == strpath) ? "//" : strpath; + headers_t updatemeta; + updatemeta["x-amz-meta-mtime"] = str(mtime); + updatemeta["x-amz-meta-ctime"] = str(ctime); + updatemeta["x-amz-meta-atime"] = str(atime); + updatemeta["x-amz-copy-source"] = urlEncodePath(service_path + S3fsCred::GetBucket() + get_realpath(strSourcePath.c_str())); + updatemeta["x-amz-metadata-directive"] = "REPLACE"; + + // check opened file handle. + // + // If the file starts uploading by multipart when the disk capacity is insufficient, + // we need to put these header after finishing upload. + // Or if the file is only open, we must update to FdEntity's internal meta. + // + AutoFdEntity autoent; + FdEntity* ent; + bool need_put_header = true; + bool keep_mtime = false; + if(nullptr != (ent = autoent.OpenExistFdEntity(path))){ + if(ent->MergeOrgMeta(updatemeta)){ + // meta is changed, but now uploading. + // then the meta is pending and accumulated to be put after the upload is complete. + S3FS_PRN_INFO("meta pending until upload is complete"); + need_put_header = false; + ent->SetHoldingMtime(mtime); + + // If there is data in the Stats cache, update the Stats cache. + StatCache::getStatCacheData()->UpdateMetaStats(strpath, updatemeta); + + // [NOTE] + // There are cases where this function is called during the process of + // creating a new file (before uploading). + // In this case, a temporary cache exists in the Stat cache.(see s3fs_create) + // So we need to update the cache, if it exists. + // + // Previously, the process of creating a new file was to update the + // file content after first uploading the file, but now the file is + // not created until flushing. + // So we need to create a temporary Stat cache for it. + // + if(!StatCache::getStatCacheData()->AddStat(strpath, updatemeta, false, true)){ + return -EIO; + } + + }else{ + S3FS_PRN_INFO("meta is not pending, but need to keep current mtime."); + + // [NOTE] + // Depending on the order in which write/flush and utimens are called, + // the mtime updated here may be overwritten at the time of flush. + // To avoid that, set a special flag. + // + keep_mtime = true; + } + } + if(need_put_header){ + // not found opened file. + merge_headers(meta, updatemeta, true); + + // upload meta directly. + if(0 != (result = put_headers(strpath.c_str(), meta, true))){ + return result; + } + StatCache::getStatCacheData()->DelStat(nowcache); + + if(keep_mtime){ + ent->SetHoldingMtime(mtime); + } + } + } + S3FS_MALLOCTRIM(0); + + return 0; +} + +static int s3fs_utimens_nocopy(const char* _path, const struct timespec ts[2]) +{ + WTF8_ENCODE(path) + int result; + std::string strpath; + std::string newpath; + std::string nowcache; + struct stat stbuf; + dirtype nDirType = dirtype::UNKNOWN; + + FUSE_CTX_INFO1("[path=%s][mtime=%s][atime/ctime=%s]", path, str(ts[1]).c_str(), str(ts[0]).c_str()); + + if(0 != (result = check_parent_object_access(path, X_OK))){ + return result; + } + if(0 != (result = check_object_access(path, W_OK, &stbuf))){ + if(0 != check_object_owner(path, &stbuf)){ + return result; + } + } + + struct timespec now; + struct timespec ts_atime; + struct timespec ts_ctime; + struct timespec ts_mtime; + + s3fs_realtime(now); + set_stat_to_timespec(stbuf, stat_time_type::ATIME, ts_atime); + set_stat_to_timespec(stbuf, stat_time_type::CTIME, ts_ctime); + set_stat_to_timespec(stbuf, stat_time_type::MTIME, ts_mtime); + + struct timespec atime = handle_utimens_special_values(ts[0], now, ts_atime); + struct timespec ctime = handle_utimens_special_values(ts[0], now, ts_ctime); + struct timespec mtime = handle_utimens_special_values(ts[1], now, ts_mtime); + + // Get attributes + if(S_ISDIR(stbuf.st_mode)){ + result = chk_dir_object_type(path, newpath, strpath, nowcache, nullptr, &nDirType); + }else{ + strpath = path; + nowcache = strpath; + result = get_object_attribute(strpath.c_str(), nullptr, nullptr); + } + if(0 != result){ + return result; + } + + if(S_ISDIR(stbuf.st_mode)){ + std::string xattrvalue; + const char* pxattrvalue; + if(get_meta_xattr_value(path, xattrvalue)){ + pxattrvalue = xattrvalue.c_str(); + }else{ + pxattrvalue = nullptr; + } + + if(IS_REPLACEDIR(nDirType)){ + // Should rebuild all directory object + // Need to remove old dir("dir" etc) and make new dir("dir/") + + // At first, remove directory old object + if(0 != (result = remove_old_type_dir(strpath, nDirType))){ + return result; + } + } + StatCache::getStatCacheData()->DelStat(nowcache); + + // Make new directory object("dir/") + if(0 != (result = create_directory_object(newpath.c_str(), stbuf.st_mode, atime, mtime, ctime, stbuf.st_uid, stbuf.st_gid, pxattrvalue))){ + return result; + } + }else{ + // normal object or directory object of newer version + + // open & load + AutoFdEntity autoent; + FdEntity* ent; + if(0 != (result = get_local_fent(autoent, &ent, strpath.c_str(), O_RDWR, true))){ + S3FS_PRN_ERR("could not open and read file(%s)", strpath.c_str()); + return result; + } + + // set mtime/ctime + if(0 != (result = ent->SetMCtime(mtime, ctime))){ + S3FS_PRN_ERR("could not set mtime and ctime to file(%s): result=%d", strpath.c_str(), result); + return result; + } + + // set atime + if(0 != (result = ent->SetAtime(atime))){ + S3FS_PRN_ERR("could not set atime to file(%s): result=%d", strpath.c_str(), result); + return result; + } + + // upload + if(0 != (result = ent->Flush(autoent.GetPseudoFd(), AutoLock::NONE, true))){ + S3FS_PRN_ERR("could not upload file(%s): result=%d", strpath.c_str(), result); + return result; + } + StatCache::getStatCacheData()->DelStat(nowcache); + } + S3FS_MALLOCTRIM(0); + + return result; +} + +static int s3fs_truncate(const char* _path, off_t size) +{ + WTF8_ENCODE(path) + int result; + headers_t meta; + AutoFdEntity autoent; + FdEntity* ent = nullptr; + + FUSE_CTX_INFO("[path=%s][size=%lld]", path, static_cast(size)); + + if(size < 0){ + size = 0; + } + + if(0 != (result = check_parent_object_access(path, X_OK))){ + return result; + } + if(0 != (result = check_object_access(path, W_OK, nullptr))){ + return result; + } + + // Get file information + if(0 == (result = get_object_attribute(path, nullptr, &meta))){ + // File exists + + // [NOTE] + // If the file exists, the file has already been opened by FUSE before + // truncate is called. Then the call below will change the file size. + // (When an already open file is changed the file size, FUSE will not + // reopen it.) + // The Flush is called before this file is closed, so there is no need + // to do it here. + // + // [NOTICE] + // FdManager::Open() ignores changes that reduce the file size for the + // file you are editing. However, if user opens only onece, edit it, + // and then shrink the file, it should be done. + // When this function is called, the file is already open by FUSE or + // some other operation. Therefore, if the number of open files is 1, + // no edits other than that fd will be made, and the files will be + // shrunk using ignore_modify flag even while editing. + // See the comments when merging this code for FUSE2 limitations. + // (In FUSE3, it will be possible to handle it reliably using fuse_file_info.) + // + bool ignore_modify; + if(1 < FdManager::GetOpenFdCount(path)){ + ignore_modify = false; + }else{ + ignore_modify = true; + } + + if(nullptr == (ent = autoent.Open(path, &meta, size, S3FS_OMIT_TS, O_RDWR, false, true, ignore_modify, AutoLock::NONE))){ + S3FS_PRN_ERR("could not open file(%s): errno=%d", path, errno); + return -EIO; + } + if(use_newcache){ + int res = accessor->Truncate(path, size); + if(res) return res; + } + ent->UpdateCtime(); + +#if defined(__APPLE__) + // [NOTE] + // Only for macos, this truncate calls to "size=0" do not reflect size. + // The cause is unknown now, but it can be avoided by flushing the file. + // + if(0 == size){ + if(0 != (result = ent->Flush(autoent.GetPseudoFd(), AutoLock::NONE, true))){ + S3FS_PRN_ERR("could not upload file(%s): result=%d", path, result); + return result; + } + StatCache::getStatCacheData()->DelStat(path); + } +#endif + + }else{ + // Not found -> Make tmpfile(with size) + const struct fuse_context* pcxt; + if(nullptr == (pcxt = fuse_get_context())){ + return -EIO; + } + + std::string strnow = s3fs_str_realtime(); + meta["Content-Type"] = "application/octet-stream"; // Static + meta["x-amz-meta-mode"] = std::to_string(S_IFLNK | S_IRWXU | S_IRWXG | S_IRWXO); + meta["x-amz-meta-ctime"] = strnow; + meta["x-amz-meta-mtime"] = strnow; + meta["x-amz-meta-uid"] = std::to_string(pcxt->uid); + meta["x-amz-meta-gid"] = std::to_string(pcxt->gid); + + if(nullptr == (ent = autoent.Open(path, &meta, size, S3FS_OMIT_TS, O_RDWR, true, true, false, AutoLock::NONE))){ + S3FS_PRN_ERR("could not open file(%s): errno=%d", path, errno); + return -EIO; + } + if(use_newcache){ + int res = accessor->Truncate(path, size); + if(res) return res; + } + if(0 != (result = ent->Flush(autoent.GetPseudoFd(), AutoLock::NONE, true))){ + S3FS_PRN_ERR("could not upload file(%s): result=%d", path, result); + return result; + } + StatCache::getStatCacheData()->DelStat(path); + } + S3FS_MALLOCTRIM(0); + + return result; +} + +static int s3fs_open(const char* _path, struct fuse_file_info* fi) +{ + WTF8_ENCODE(path) + int result; + struct stat st; + bool needs_flush = false; + + FUSE_CTX_INFO("[path=%s][flags=0x%x]", path, fi->flags); + + if ((fi->flags & O_ACCMODE) == O_RDONLY && fi->flags & O_TRUNC) { + return -EACCES; + } + + // [NOTE] + // Delete the Stats cache only if the file is not open. + // If the file is open, the stats cache will not be deleted as + // there are cases where the object does not exist on the server + // and only the Stats cache exists. + // + if(StatCache::getStatCacheData()->HasStat(path)){ + if(!FdManager::HasOpenEntityFd(path)){ + StatCache::getStatCacheData()->DelStat(path); + } + } + + int mask = (O_RDONLY != (fi->flags & O_ACCMODE) ? W_OK : R_OK); + if(0 != (result = check_parent_object_access(path, X_OK))){ + return result; + } + + result = check_object_access(path, mask, &st); + if(-ENOENT == result){ + if(0 != (result = check_parent_object_access(path, W_OK))){ + return result; + } + }else if(0 != result){ + return result; + } + + AutoFdEntity autoent; + FdEntity* ent; + headers_t meta; + + if((unsigned int)fi->flags & O_TRUNC){ + if(0 != st.st_size){ + st.st_size = 0; + needs_flush = true; + } + }else{ + // [NOTE] + // If the file has already been opened and edited, the file size in + // the edited state is given priority. + // This prevents the file size from being reset to its original size + // if you keep the file open, shrink its size, and then read the file + // from another process while it has not yet been flushed. + // + if(nullptr != (ent = autoent.OpenExistFdEntity(path)) && ent->IsModified()){ + // sets the file size being edited. + ent->GetSize(st.st_size); + } + } + if(!S_ISREG(st.st_mode) || S_ISLNK(st.st_mode)){ + st.st_mtime = -1; + } + + if(0 != (result = get_object_attribute(path, nullptr, &meta, true, nullptr, true))){ // no truncate cache + return result; + } + + struct timespec st_mctime; + set_stat_to_timespec(st, stat_time_type::MTIME, st_mctime); + + if(nullptr == (ent = autoent.Open(path, &meta, st.st_size, st_mctime, fi->flags, false, true, false, AutoLock::NONE))){ + StatCache::getStatCacheData()->DelStat(path); + return -EIO; + } + + if (needs_flush){ + struct timespec ts; + s3fs_realtime(ts); + ent->SetMCtime(ts, ts); + + if(0 != (result = ent->RowFlush(autoent.GetPseudoFd(), path, AutoLock::NONE, true))){ + S3FS_PRN_ERR("could not upload file(%s): result=%d", path, result); + StatCache::getStatCacheData()->DelStat(path); + return result; + } + } + fi->fh = autoent.Detach(); // KEEP fdentity open; + + S3FS_MALLOCTRIM(0); + + return 0; +} + +static int s3fs_read(const char* _path, char* buf, size_t size, off_t offset, struct fuse_file_info* fi) +{ + WTF8_ENCODE(path) + ssize_t res; + + S3FS_PRN_WARN("[path=%s][size=%zu][offset=%lld][pseudo_fd=%llu]", path, size, static_cast(offset), (unsigned long long)(fi->fh)); + + AutoFdEntity autoent; + FdEntity* ent; + if(nullptr == (ent = autoent.GetExistFdEntity(path, static_cast(fi->fh)))){ + S3FS_PRN_ERR("could not find opened pseudo_fd(=%llu) for path(%s)", (unsigned long long)(fi->fh), path); + return -EIO; + } + + // check real file size + off_t realsize = 0; + if(!ent->GetSize(realsize) || 0 == realsize){ + S3FS_PRN_DBG("file size is 0, so break to read."); + return 0; + } + + if(0 > (res = ent->Read(static_cast(fi->fh), buf, offset, size, false))){ + S3FS_PRN_WARN("failed to read file(%s). result=%zd", path, res); + } + return static_cast(res); +} + +static int s3fs_write(const char* _path, const char* buf, size_t size, off_t offset, struct fuse_file_info* fi) +{ + WTF8_ENCODE(path) + ssize_t res; + + S3FS_PRN_WARN("[path=%s][size=%zu][offset=%lld][pseudo_fd=%llu]", path, size, static_cast(offset), (unsigned long long)(fi->fh)); + + AutoFdEntity autoent; + FdEntity* ent; + if(nullptr == (ent = autoent.GetExistFdEntity(path, static_cast(fi->fh)))){ + S3FS_PRN_ERR("could not find opened pseudo_fd(%llu) for path(%s)", (unsigned long long)(fi->fh), path); + return -EIO; + } + + if(0 > (res = ent->Write(static_cast(fi->fh), buf, offset, size))){ + S3FS_PRN_WARN("failed to write file(%s). result=%zd", path, res); + } + + if(max_dirty_data != -1 && ent->BytesModified() >= max_dirty_data && !use_newcache){ + int flushres; + if(0 != (flushres = ent->RowFlush(static_cast(fi->fh), path, AutoLock::NONE, true))){ + S3FS_PRN_ERR("could not upload file(%s): result=%d", path, flushres); + StatCache::getStatCacheData()->DelStat(path); + return flushres; + } + // Punch a hole in the file to recover disk space. + if(!ent->PunchHole()){ + S3FS_PRN_WARN("could not punching HOLEs to a cache file, but continue."); + } + } + + return static_cast(res); +} + +static int s3fs_statfs(const char* _path, struct statvfs* stbuf) +{ + // WTF8_ENCODE(path) + stbuf->f_bsize = s3fs_block_size; + stbuf->f_namemax = NAME_MAX; + +#if defined(__MSYS__) + // WinFsp resolves the free space from f_bfree * f_frsize, and the total space from f_blocks * f_frsize (in bytes). + stbuf->f_blocks = bucket_block_count; + stbuf->f_frsize = stbuf->f_bsize; + stbuf->f_bfree = stbuf->f_blocks; +#elif defined(__APPLE__) + stbuf->f_blocks = bucket_block_count; + stbuf->f_frsize = stbuf->f_bsize; + stbuf->f_bfree = stbuf->f_blocks; + stbuf->f_files = UINT32_MAX; + stbuf->f_ffree = UINT32_MAX; + stbuf->f_favail = UINT32_MAX; +#else + stbuf->f_frsize = stbuf->f_bsize; + stbuf->f_blocks = bucket_block_count; + stbuf->f_bfree = stbuf->f_blocks; +#endif + stbuf->f_bavail = stbuf->f_blocks; + + return 0; +} + +static int s3fs_flush(const char* _path, struct fuse_file_info* fi) +{ + WTF8_ENCODE(path) + int result; + + FUSE_CTX_INFO("[path=%s][pseudo_fd=%llu]", path, (unsigned long long)(fi->fh)); + + int mask = (O_RDONLY != (fi->flags & O_ACCMODE) ? W_OK : R_OK); + if(0 != (result = check_parent_object_access(path, X_OK))){ + return result; + } + result = check_object_access(path, mask, nullptr); + if(-ENOENT == result){ + if(0 != (result = check_parent_object_access(path, W_OK))){ + return result; + } + }else if(0 != result){ + return result; + } + + AutoFdEntity autoent; + FdEntity* ent; + if(nullptr != (ent = autoent.GetExistFdEntity(path, static_cast(fi->fh)))){ + bool is_new_file = ent->IsDirtyNewFile(); + + ent->UpdateMtime(true); // clear the flag not to update mtime. + ent->UpdateCtime(); + result = ent->Flush(static_cast(fi->fh), AutoLock::NONE, false); + StatCache::getStatCacheData()->DelStat(path); + + if(is_new_file){ + // update parent directory timestamp + int update_result; + if(0 != (update_result = update_mctime_parent_directory(path))){ + S3FS_PRN_ERR("succeed to create the file(%s), but could not update timestamp of its parent directory(result=%d).", path, update_result); + } + } + } + S3FS_MALLOCTRIM(0); + + return result; +} + +// [NOTICE] +// Assumption is a valid fd. +// +static int s3fs_fsync(const char* _path, int datasync, struct fuse_file_info* fi) +{ + WTF8_ENCODE(path) + int result = 0; + + FUSE_CTX_INFO("[path=%s][datasync=%d][pseudo_fd=%llu]", path, datasync, (unsigned long long)(fi->fh)); + + AutoFdEntity autoent; + FdEntity* ent; + if(nullptr != (ent = autoent.GetExistFdEntity(path, static_cast(fi->fh)))){ + bool is_new_file = ent->IsDirtyNewFile(); + + if(0 == datasync){ + ent->UpdateMtime(); + ent->UpdateCtime(); + } + result = ent->Flush(static_cast(fi->fh), AutoLock::NONE, false); + + if(0 != datasync){ + // [NOTE] + // The metadata are not updated when fdatasync is called. + // Instead of it, these metadata are pended and set the dirty flag here. + // Setting this flag allows metadata to be updated even if there is no + // content update between the fdatasync call and the flush call. + // + ent->MarkDirtyMetadata(); + } + + if(is_new_file){ + // update parent directory timestamp + int update_result; + if(0 != (update_result = update_mctime_parent_directory(path))){ + S3FS_PRN_ERR("succeed to create the file(%s), but could not update timestamp of its parent directory(result=%d).", path, update_result); + } + } + } + S3FS_MALLOCTRIM(0); + + // Issue 320: Delete stat cache entry because st_size may have changed. + StatCache::getStatCacheData()->DelStat(path); + + return result; +} + +static int s3fs_release(const char* _path, struct fuse_file_info* fi) +{ + WTF8_ENCODE(path) + FUSE_CTX_INFO("[path=%s][pseudo_fd=%llu]", path, (unsigned long long)(fi->fh)); + + { // scope for AutoFdEntity + AutoFdEntity autoent; + + // [NOTE] + // The pseudo fd stored in fi->fh is attached to AutoFdEntry so that it can be + // destroyed here. + // + FdEntity* ent; + if(nullptr == (ent = autoent.Attach(path, static_cast(fi->fh)))){ + S3FS_PRN_ERR("could not find pseudo_fd(%llu) for path(%s)", (unsigned long long)(fi->fh), path); + return -EIO; + } + + // [NOTE] + // There are cases when s3fs_flush is not called and s3fs_release is called. + // (There have been reported cases where it is not called when exported as NFS.) + // Therefore, Flush() is called here to try to upload the data. + // Flush() will only perform an upload if the file has been updated. + // + int result; + if(ent->IsModified()){ + if(0 != (result = ent->Flush(static_cast(fi->fh), AutoLock::NONE, false))){ + S3FS_PRN_ERR("failed to upload file contentsfor pseudo_fd(%llu) / path(%s) by result(%d)", (unsigned long long)(fi->fh), path, result); + return result; + } + } + + // [NOTE] + // All opened file's stats is cached with no truncate flag. + // Thus we unset it here. + StatCache::getStatCacheData()->ChangeNoTruncateFlag(path, false); + + // [NOTICE] + // At first, we remove stats cache. + // Because fuse does not wait for response from "release" function. :-( + // And fuse runs next command before this function returns. + // Thus we call deleting stats function ASAP. + // + if((fi->flags & O_RDWR) || (fi->flags & O_WRONLY)){ + StatCache::getStatCacheData()->DelStat(path); + } + + bool is_new_file = ent->IsDirtyNewFile(); + + if(0 != (result = ent->UploadPending(static_cast(fi->fh), AutoLock::NONE))){ + S3FS_PRN_ERR("could not upload pending data(meta, etc) for pseudo_fd(%llu) / path(%s)", (unsigned long long)(fi->fh), path); + return result; + } + + if(is_new_file){ + // update parent directory timestamp + int update_result; + if(0 != (update_result = update_mctime_parent_directory(path))){ + S3FS_PRN_ERR("succeed to create the file(%s), but could not update timestamp of its parent directory(result=%d).", path, update_result); + } + } + } + + // check - for debug + if(S3fsLog::IsS3fsLogDbg()){ + if(FdManager::HasOpenEntityFd(path)){ + S3FS_PRN_DBG("file(%s) is still opened(another pseudo fd is opend).", path); + } + } + S3FS_MALLOCTRIM(0); + + return 0; +} + +static int s3fs_opendir(const char* _path, struct fuse_file_info* fi) +{ + WTF8_ENCODE(path) + int result; + int mask = (O_RDONLY != (fi->flags & O_ACCMODE) ? W_OK : R_OK); + + FUSE_CTX_INFO("[path=%s][flags=0x%x]", path, fi->flags); + + if(0 == (result = check_object_access(path, mask, nullptr))){ + result = check_parent_object_access(path, X_OK); + } + S3FS_MALLOCTRIM(0); + + return result; +} + +// cppcheck-suppress unmatchedSuppression +// cppcheck-suppress constParameterCallback +static bool multi_head_callback(S3fsCurl* s3fscurl, void* param) +{ + if(!s3fscurl){ + return false; + } + + // Add stat cache + std::string saved_path = s3fscurl->GetSpecialSavedPath(); + if(!StatCache::getStatCacheData()->AddStat(saved_path, *(s3fscurl->GetResponseHeaders()))){ + S3FS_PRN_ERR("failed adding stat cache [path=%s]", saved_path.c_str()); + return false; + } + + // Get stats from stats cache(for converting from meta), and fill + std::string bpath = mybasename(saved_path); + if(use_wtf8){ + bpath = s3fs_wtf8_decode(bpath); + } + if(param){ + SyncFiller* pcbparam = reinterpret_cast(param); + struct stat st; + if(StatCache::getStatCacheData()->GetStat(saved_path, &st)){ + pcbparam->Fill(bpath.c_str(), &st, 0); + }else{ + S3FS_PRN_INFO2("Could not find %s file in stat cache.", saved_path.c_str()); + pcbparam->Fill(bpath.c_str(), nullptr, 0); + } + }else{ + S3FS_PRN_WARN("param(multi_head_callback_param*) is nullptr, then can not call filler."); + } + + return true; +} + +struct multi_head_notfound_callback_param +{ + pthread_mutex_t list_lock; + s3obj_list_t notfound_list; +}; + +static bool multi_head_notfound_callback(S3fsCurl* s3fscurl, void* param) +{ + if(!s3fscurl){ + return false; + } + S3FS_PRN_INFO("HEAD returned NotFound(404) for %s object, it maybe only the path exists and the object does not exist.", s3fscurl->GetPath().c_str()); + + if(!param){ + S3FS_PRN_WARN("param(multi_head_notfound_callback_param*) is nullptr, then can not call filler."); + return false; + } + + // set path to not found list + struct multi_head_notfound_callback_param* pcbparam = reinterpret_cast(param); + + AutoLock auto_lock(&(pcbparam->list_lock)); + pcbparam->notfound_list.push_back(s3fscurl->GetBasePath()); + + return true; +} + +static std::unique_ptr multi_head_retry_callback(S3fsCurl* s3fscurl) +{ + if(!s3fscurl){ + return nullptr; + } + size_t ssec_key_pos= s3fscurl->GetLastPreHeadSeecKeyPos(); + int retry_count = s3fscurl->GetMultipartRetryCount(); + + // retry next sse key. + // if end of sse key, set retry master count is up. + ssec_key_pos = (ssec_key_pos == static_cast(-1) ? 0 : ssec_key_pos + 1); + if(0 == S3fsCurl::GetSseKeyCount() || S3fsCurl::GetSseKeyCount() <= ssec_key_pos){ + if(s3fscurl->IsOverMultipartRetryCount()){ + S3FS_PRN_ERR("Over retry count(%d) limit(%s).", s3fscurl->GetMultipartRetryCount(), s3fscurl->GetSpecialSavedPath().c_str()); + return nullptr; + } + ssec_key_pos = -1; + retry_count++; + } + + std::unique_ptr newcurl(new S3fsCurl(s3fscurl->IsUseAhbe())); + std::string path = s3fscurl->GetBasePath(); + std::string base_path = s3fscurl->GetBasePath(); + std::string saved_path = s3fscurl->GetSpecialSavedPath(); + + if(!newcurl->PreHeadRequest(path, base_path, saved_path, ssec_key_pos)){ + S3FS_PRN_ERR("Could not duplicate curl object(%s).", saved_path.c_str()); + return nullptr; + } + newcurl->SetMultipartRetryCount(retry_count); + + return newcurl; +} + +static int readdir_multi_head(const char* path, const S3ObjList& head, void* buf, fuse_fill_dir_t filler) +{ + if(use_newcache && accessor->UseGlobalCache()){ + return readdir_multi_head_4_newcache(path, head, buf, filler); + } + + S3fsMultiCurl curlmulti(S3fsCurl::GetMaxMultiRequest(), true); // [NOTE] run all requests to completion even if some requests fail. + s3obj_list_t headlist; + int result = 0; + + S3FS_PRN_INFO1("[path=%s][list=%zu]", path, headlist.size()); + + // Make base path list. + head.GetNameList(headlist, true, false); // get name with "/". + StatCache::getStatCacheData()->GetNotruncateCache(std::string(path), headlist); // Add notruncate file name from stat cache + + // Initialize S3fsMultiCurl + curlmulti.SetSuccessCallback(multi_head_callback); + curlmulti.SetRetryCallback(multi_head_retry_callback); + + // Success Callback function parameter(SyncFiller object) + SyncFiller syncfiller(buf, filler); + curlmulti.SetSuccessCallbackParam(reinterpret_cast(&syncfiller)); + + // Not found Callback function parameter + struct multi_head_notfound_callback_param notfound_param; + if(support_compat_dir){ + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); + #if S3FS_PTHREAD_ERRORCHECK + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK); + #endif + + if(0 != (result = pthread_mutex_init(&(notfound_param.list_lock), &attr))){ + S3FS_PRN_CRIT("failed to init notfound_param.list_lock: %d", result); + abort(); + } + curlmulti.SetNotFoundCallback(multi_head_notfound_callback); + curlmulti.SetNotFoundCallbackParam(reinterpret_cast(¬found_param)); + } + + // Make single head request(with max). + for(s3obj_list_t::iterator iter = headlist.begin(); headlist.end() != iter; ++iter){ + std::string disppath = path + (*iter); + std::string etag = head.GetETag((*iter).c_str()); + struct stat st; + + // [NOTE] + // If there is a cache hit, file stat is filled by filler at here. + // + if(StatCache::getStatCacheData()->HasStat(disppath, &st, etag.c_str())){ + std::string bpath = mybasename(disppath); + if(use_wtf8){ + bpath = s3fs_wtf8_decode(bpath); + } + syncfiller.Fill(bpath.c_str(), &st, 0); + continue; + } + + // First check for directory, start checking "not SSE-C". + // If checking failed, retry to check with "SSE-C" by retry callback func when SSE-C mode. + std::unique_ptr s3fscurl(new S3fsCurl()); + if(!s3fscurl->PreHeadRequest(disppath, disppath, disppath)){ // target path = cache key path.(ex "dir/") + S3FS_PRN_WARN("Could not make curl object for head request(%s).", disppath.c_str()); + continue; + } + + if(!curlmulti.SetS3fsCurlObject(std::move(s3fscurl))){ + S3FS_PRN_WARN("Could not make curl object into multi curl(%s).", disppath.c_str()); + continue; + } + } + headlist.clear(); + + // Multi request + if(0 != (result = curlmulti.Request())){ + // If result is -EIO, it is something error occurred. + // This case includes that the object is encrypting(SSE) and s3fs does not have keys. + // So s3fs set result to 0 in order to continue the process. + if(-EIO == result){ + S3FS_PRN_WARN("error occurred in multi request(errno=%d), but continue...", result); + result = 0; + }else{ + S3FS_PRN_ERR("error occurred in multi request(errno=%d).", result); + return result; + } + } + + // [NOTE] + // Objects that could not be found by HEAD request may exist only + // as a path, so search for objects under that path.(a case of no dir object) + // + if(!support_compat_dir){ + syncfiller.SufficiencyFill(head.common_prefixes); + } + if(support_compat_dir && !notfound_param.notfound_list.empty()){ // [NOTE] not need to lock to access this here. + // dummy header + mode_t dirmask = umask(0); // macos does not have getumask() + umask(dirmask); + + headers_t dummy_header; + dummy_header["Content-Type"] = "application/x-directory"; // directory + dummy_header["x-amz-meta-uid"] = std::to_string(is_s3fs_uid ? s3fs_uid : geteuid()); + dummy_header["x-amz-meta-gid"] = std::to_string(is_s3fs_gid ? s3fs_gid : getegid()); + dummy_header["x-amz-meta-mode"] = std::to_string(S_IFDIR | (~dirmask & (S_IRWXU | S_IRWXG | S_IRWXO))); + dummy_header["x-amz-meta-atime"] = "0"; + dummy_header["x-amz-meta-ctime"] = "0"; + dummy_header["x-amz-meta-mtime"] = "0"; + + for(s3obj_list_t::iterator reiter = notfound_param.notfound_list.begin(); reiter != notfound_param.notfound_list.end(); ++reiter){ + int dir_result; + std::string dirpath = *reiter; + if(-ENOTEMPTY == (dir_result = directory_empty(dirpath.c_str()))){ + // Found objects under the path, so the path is directory. + + // Add stat cache + if(StatCache::getStatCacheData()->AddStat(dirpath, dummy_header, true)){ // set forcedir=true + // Get stats from stats cache(for converting from meta), and fill + std::string base_path = mybasename(dirpath); + if(use_wtf8){ + base_path = s3fs_wtf8_decode(base_path); + } + + struct stat st; + if(StatCache::getStatCacheData()->GetStat(dirpath, &st)){ + syncfiller.Fill(base_path.c_str(), &st, 0); + }else{ + S3FS_PRN_INFO2("Could not find %s directory(no dir object) in stat cache.", dirpath.c_str()); + syncfiller.Fill(base_path.c_str(), nullptr, 0); + } + }else{ + S3FS_PRN_ERR("failed adding stat cache [path=%s], but dontinue...", dirpath.c_str()); + } + }else{ + S3FS_PRN_WARN("%s object does not have any object under it(errno=%d),", reiter->c_str(), dir_result); + } + } + } + + return result; +} + +static int readdir_multi_head_4_newcache(const char* path, const S3ObjList& head, void* buf, fuse_fill_dir_t filler) +{ + s3obj_list_t headlist; + int result = 0; + + S3FS_PRN_INFO1("[path=%s][list=%zu]", path, headlist.size()); + + // Make base path list. + head.GetNameList(headlist, true, false); // get name with "/". + StatCache::getStatCacheData()->GetNotruncateCache(std::string(path), headlist); // Add notruncate file name from stat cache + + SyncFiller syncfiller(buf, filler); + + // Not found Callback function parameter + struct multi_head_notfound_callback_param notfound_param; + if(support_compat_dir){ + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); + #if S3FS_PTHREAD_ERRORCHECK + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK); + #endif + + if(0 != (result = pthread_mutex_init(&(notfound_param.list_lock), &attr))){ + S3FS_PRN_CRIT("failed to init notfound_param.list_lock: %d", result); + abort(); + } + } + + std::vector> fs; + for(s3obj_list_t::iterator iter = headlist.begin(); headlist.end() != iter; ++iter){ + std::string disppath = path + (*iter); + std::string etag = head.GetETag((*iter).c_str()); + struct stat st; + + // [NOTE] + // If there is a cache hit, file stat is filled by filler at here. + // + if(StatCache::getStatCacheData()->HasStat(disppath, &st, etag.c_str())){ + std::string bpath = mybasename(disppath); + if(use_wtf8){ + bpath = s3fs_wtf8_decode(bpath); + } + syncfiller.Fill(bpath.c_str(), &st, 0); + continue; + } + + fs.push_back(folly::via(accessor->GetExecutor(), [disppath, &syncfiller, ¬found_param]() { + size_t realSize = 0; + std::map headers; + headers_t meta; + int res = accessor->Head(get_realpath(disppath.c_str()), realSize, headers); + if(0 == res){ + headers["Content-Length"] = std::to_string(realSize); + for(auto& it : headers) { + meta.insert(std::make_pair(it.first, it.second)); + } + if(!StatCache::getStatCacheData()->AddStat(disppath, meta)){ + S3FS_PRN_ERR("failed adding stat cache [path=%s]", disppath.c_str()); + meta.clear(); + return -EIO; + } + meta.clear(); // ps: it must be released here. + + // Get stats from stats cache(for converting from meta), and fill + std::string bpath = mybasename(disppath); + if(use_wtf8){ + bpath = s3fs_wtf8_decode(bpath); + } + struct stat st; + if(StatCache::getStatCacheData()->GetStat(disppath, &st)){ + syncfiller.Fill(bpath.c_str(), &st, 0); + }else{ + S3FS_PRN_INFO2("Could not find %s file in stat cache.", disppath.c_str()); + syncfiller.Fill(bpath.c_str(), nullptr, 0); + } + }else if(-ENOENT == res) { // notfound + notfound_param.notfound_list.push_back(disppath); + res = 0; + }else{ + S3FS_PRN_ERR("File head failed [path=%s][errno=%d]", disppath.c_str(), res); + } + return res; + })); + } + + if(fs.size()){ + auto f = collectAll(fs.begin(), fs.end()).via(accessor->GetExecutor()).thenValue([]( + std::vector, std::allocator>>&& tups) { + for(const auto& t : tups){ + if (0 != t.value()) return t.value(); + } + return 0; + }); + f.wait(); + result = f.value(); + FUSE_CTX_INFO("multi request [path=%s][list=%zu][result=%d]", path, headlist.size(), result); + + // If result is -EIO, it is something error occurred. + // This case includes that the object is encrypting(SSE) and s3fs does not have keys. + // So s3fs set result to 0 in order to continue the process. + if(-EIO == result){ + S3FS_PRN_WARN("error occurred in multi request, but continue... [path=%s][list=%zu][errno=%d]", path, headlist.size(), result); + result = 0; + }else if(0 != result){ + S3FS_PRN_ERR("error occurred in multi request [path=%s][list=%zu][errno=%d]", path, headlist.size(), result); + return result; + } + } + headlist.clear(); + + // [NOTE] + // Objects that could not be found by HEAD request may exist only + // as a path, so search for objects under that path.(a case of no dir object) + // + if(!support_compat_dir){ + syncfiller.SufficiencyFill(head.common_prefixes); + } + if(support_compat_dir && !notfound_param.notfound_list.empty()){ // [NOTE] not need to lock to access this here. + // dummy header + mode_t dirmask = umask(0); // macos does not have getumask() + umask(dirmask); + + headers_t dummy_header; + dummy_header["Content-Type"] = "application/x-directory"; // directory + dummy_header["x-amz-meta-uid"] = std::to_string(is_s3fs_uid ? s3fs_uid : geteuid()); + dummy_header["x-amz-meta-gid"] = std::to_string(is_s3fs_gid ? s3fs_gid : getegid()); + dummy_header["x-amz-meta-mode"] = std::to_string(S_IFDIR | (~dirmask & (S_IRWXU | S_IRWXG | S_IRWXO))); + dummy_header["x-amz-meta-atime"] = "0"; + dummy_header["x-amz-meta-ctime"] = "0"; + dummy_header["x-amz-meta-mtime"] = "0"; + + for(s3obj_list_t::iterator reiter = notfound_param.notfound_list.begin(); reiter != notfound_param.notfound_list.end(); ++reiter){ + int dir_result; + std::string dirpath = *reiter; + if(-ENOTEMPTY == (dir_result = directory_empty(dirpath.c_str()))){ + // Found objects under the path, so the path is directory. + + // Add stat cache + if(StatCache::getStatCacheData()->AddStat(dirpath, dummy_header, true)){ // set forcedir=true + // Get stats from stats cache(for converting from meta), and fill + std::string base_path = mybasename(dirpath); + if(use_wtf8){ + base_path = s3fs_wtf8_decode(base_path); + } + + struct stat st; + if(StatCache::getStatCacheData()->GetStat(dirpath, &st)){ + syncfiller.Fill(base_path.c_str(), &st, 0); + }else{ + S3FS_PRN_INFO2("Could not find %s directory(no dir object) in stat cache.", dirpath.c_str()); + syncfiller.Fill(base_path.c_str(), nullptr, 0); + } + }else{ + S3FS_PRN_ERR("failed adding stat cache [path=%s], but dontinue...", dirpath.c_str()); + } + }else{ + S3FS_PRN_WARN("%s object does not have any object under it(errno=%d),", reiter->c_str(), dir_result); + } + } + } + + return result; +} + +static int s3fs_readdir(const char* _path, void* buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info* fi) +{ + WTF8_ENCODE(path) + S3ObjList head; + int result; + + FUSE_CTX_INFO("[path=%s]", path); + + if(0 != (result = check_object_access(path, R_OK, nullptr))){ + return result; + } + + // get a list of all the objects + if((result = list_bucket(path, head, "/")) != 0){ + S3FS_PRN_ERR("list_bucket returns error(%d).", result); + return result; + } + + // force to add "." and ".." name. + filler(buf, ".", nullptr, 0); + filler(buf, "..", nullptr, 0); + if(head.IsEmpty()){ + return 0; + } + + // Send multi head request for stats caching. + std::string strpath = path; + if(strcmp(path, "/") != 0){ + strpath += "/"; + } + if(0 != (result = readdir_multi_head(strpath.c_str(), head, buf, filler))){ + S3FS_PRN_ERR("readdir_multi_head returns error(%d).", result); + } + S3FS_MALLOCTRIM(0); + + return result; +} + +static int list_bucket(const char* path, S3ObjList& head, const char* delimiter, bool check_content_only) +{ + std::string s3_realpath; + std::string query_delimiter; + std::string query_prefix; + std::string query_maxkey; + std::string next_continuation_token; + std::string next_marker; + bool truncated = true; + S3fsCurl s3fscurl; + xmlDocPtr doc; + + S3FS_PRN_INFO1("[path=%s]", path); + + if(delimiter && 0 < strlen(delimiter)){ + query_delimiter += "delimiter="; + query_delimiter += delimiter; + query_delimiter += "&"; + } + + query_prefix += "&prefix="; + s3_realpath = get_realpath(path); + if(s3_realpath.empty() || '/' != *s3_realpath.rbegin()){ + // last word must be "/" + query_prefix += urlEncodePath(s3_realpath.substr(1) + "/"); + }else{ + query_prefix += urlEncodePath(s3_realpath.substr(1)); + } + if (check_content_only){ + // Just need to know if there are child objects in dir + // For dir with children, expect "dir/" and "dir/child" + query_maxkey += "max-keys=2"; + }else{ + query_maxkey += "max-keys=" + std::to_string(max_keys_list_object); + } + + while(truncated){ + // append parameters to query in alphabetical order + std::string each_query; + if(!next_continuation_token.empty()){ + each_query += "continuation-token=" + urlEncodePath(next_continuation_token) + "&"; + next_continuation_token = ""; + } + each_query += query_delimiter; + if(S3fsCurl::IsListObjectsV2()){ + each_query += "list-type=2&"; + } + if(!next_marker.empty()){ + each_query += "marker=" + urlEncodePath(next_marker) + "&"; + next_marker = ""; + } + each_query += query_maxkey; + each_query += query_prefix; + + // request + int result; + if(0 != (result = s3fscurl.ListBucketRequest(path, each_query.c_str()))){ + S3FS_PRN_ERR("ListBucketRequest returns with error."); + return result; + } + const std::string* body = s3fscurl.GetBodyData(); + + // [NOTE] + // CR code(\r) is replaced with LF(\n) by xmlReadMemory() function. + // To prevent that, only CR code is encoded by following function. + // The encoded CR code is decoded with append_objects_from_xml(_ex). + // + std::string encbody = get_encoded_cr_code(body->c_str()); + + // xmlDocPtr + if(nullptr == (doc = xmlReadMemory(encbody.c_str(), static_cast(encbody.size()), "", nullptr, 0))){ + S3FS_PRN_ERR("xmlReadMemory returns with error."); + return -EIO; + } + if(0 != append_objects_from_xml(path, doc, head)){ + S3FS_PRN_ERR("append_objects_from_xml returns with error."); + xmlFreeDoc(doc); + return -EIO; + } + if(true == (truncated = is_truncated(doc))){ + auto tmpch = get_next_continuation_token(doc); + if(nullptr != tmpch){ + next_continuation_token = reinterpret_cast(tmpch.get()); + }else if(nullptr != (tmpch = get_next_marker(doc))){ + next_marker = reinterpret_cast(tmpch.get()); + } + + if(next_continuation_token.empty() && next_marker.empty()){ + // If did not specify "delimiter", s3 did not return "NextMarker". + // On this case, can use last name for next marker. + // + std::string lastname; + if(!head.GetLastName(lastname)){ + S3FS_PRN_WARN("Could not find next marker, thus break loop."); + truncated = false; + }else{ + next_marker = s3_realpath.substr(1); + if(s3_realpath.empty() || '/' != *s3_realpath.rbegin()){ + next_marker += "/"; + } + next_marker += lastname; + } + } + } + S3FS_XMLFREEDOC(doc); + + // reset(initialize) curl object + s3fscurl.DestroyCurlHandle(); + + if(check_content_only){ + break; + } + } + S3FS_MALLOCTRIM(0); + + return 0; +} + +static int remote_mountpath_exists(const char* path, bool compat_dir) +{ + struct stat stbuf; + int result; + + S3FS_PRN_INFO1("[path=%s]", path); + + // getattr will prefix the path with the remote mountpoint + if(0 != (result = get_object_attribute(path, &stbuf, nullptr))){ + return result; + } + + // [NOTE] + // If there is no mount point(directory object) that s3fs can recognize, + // an error will occur. + // A mount point with a directory path(ex. "/...") + // requires that directory object. + // If the directory or object is created by a client other than s3fs, + // s3fs may not be able to recognize it. If you specify such a directory + // as a mount point, you can avoid the error by starting with "compat_dir" + // specified. + // + if(!compat_dir && !pHasMpStat->Get()){ + return -ENOENT; + } + return 0; +} + +static bool get_meta_xattr_value(const char* path, std::string& rawvalue) +{ + if(!path || '\0' == path[0]){ + S3FS_PRN_ERR("path is empty."); + return false; + } + S3FS_PRN_DBG("[path=%s]", path); + + rawvalue.erase(); + + headers_t meta; + if(0 != get_object_attribute(path, nullptr, &meta)){ + S3FS_PRN_ERR("Failed to get object(%s) headers", path); + return false; + } + + headers_t::const_iterator iter; + if(meta.end() == (iter = meta.find("x-amz-meta-xattr"))){ + return false; + } + rawvalue = iter->second; + return true; +} + +static bool get_parent_meta_xattr_value(const char* path, std::string& rawvalue) +{ + if(0 == strcmp(path, "/") || 0 == strcmp(path, ".")){ + // path is mount point, thus does not have parent. + return false; + } + + std::string parent = mydirname(path); + if(parent.empty()){ + S3FS_PRN_ERR("Could not get parent path for %s.", path); + return false; + } + return get_meta_xattr_value(parent.c_str(), rawvalue); +} + +static bool get_xattr_posix_key_value(const char* path, std::string& xattrvalue, bool default_key) +{ + xattrvalue.erase(); + + std::string rawvalue; + if(!get_meta_xattr_value(path, rawvalue)){ + return false; + } + + xattrs_t xattrs; + if(0 == parse_xattrs(rawvalue, xattrs)){ + return false; + } + + std::string targetkey; + if(default_key){ + targetkey = "system.posix_acl_default"; + }else{ + targetkey = "system.posix_acl_access"; + } + + xattrs_t::iterator iter; + if(xattrs.end() == (iter = xattrs.find(targetkey))){ + return false; + } + + // convert value by base64 + xattrvalue = s3fs_base64(reinterpret_cast(iter->second.c_str()), iter->second.length()); + + return true; +} + +// [NOTE] +// Converts and returns the POSIX ACL default(system.posix_acl_default) value of +// the parent directory as a POSIX ACL(system.posix_acl_access) value. +// Returns false if the parent directory has no POSIX ACL defaults. +// +static bool build_inherited_xattr_value(const char* path, std::string& xattrvalue) +{ + S3FS_PRN_DBG("[path=%s]", path); + + xattrvalue.erase(); + + if(0 == strcmp(path, "/") || 0 == strcmp(path, ".")){ + // path is mount point, thus does not have parent. + return false; + } + + std::string parent = mydirname(path); + if(parent.empty()){ + S3FS_PRN_ERR("Could not get parent path for %s.", path); + return false; + } + + // get parent's "system.posix_acl_default" value(base64'd). + std::string parent_default_value; + if(!get_xattr_posix_key_value(parent.c_str(), parent_default_value, true)){ + return false; + } + + // build "system.posix_acl_access" from parent's default value + std::string raw_xattr_value; + raw_xattr_value = "{\"system.posix_acl_access\":\""; + raw_xattr_value += parent_default_value; + raw_xattr_value += "\"}"; + + xattrvalue = urlEncodePath(raw_xattr_value); + return true; +} + +static bool parse_xattr_keyval(const std::string& xattrpair, std::string& key, std::string* pval) +{ + // parse key and value + size_t pos; + std::string tmpval; + if(std::string::npos == (pos = xattrpair.find_first_of(':'))){ + S3FS_PRN_ERR("one of xattr pair(%s) is wrong format.", xattrpair.c_str()); + return false; + } + key = xattrpair.substr(0, pos); + tmpval = xattrpair.substr(pos + 1); + + if(!takeout_str_dquart(key) || !takeout_str_dquart(tmpval)){ + S3FS_PRN_ERR("one of xattr pair(%s) is wrong format.", xattrpair.c_str()); + return false; + } + + *pval = s3fs_decode64(tmpval.c_str(), tmpval.size()); + + return true; +} + +static size_t parse_xattrs(const std::string& strxattrs, xattrs_t& xattrs) +{ + xattrs.clear(); + + // decode + std::string jsonxattrs = urlDecode(strxattrs); + + // get from "{" to "}" + std::string restxattrs; + { + size_t startpos; + size_t endpos = std::string::npos; + if(std::string::npos != (startpos = jsonxattrs.find_first_of('{'))){ + endpos = jsonxattrs.find_last_of('}'); + } + if(startpos == std::string::npos || endpos == std::string::npos || endpos <= startpos){ + S3FS_PRN_WARN("xattr header(%s) is not json format.", jsonxattrs.c_str()); + return 0; + } + restxattrs = jsonxattrs.substr(startpos + 1, endpos - (startpos + 1)); + } + + // parse each key:val + for(size_t pair_nextpos = restxattrs.find_first_of(','); !restxattrs.empty(); restxattrs = (pair_nextpos != std::string::npos ? restxattrs.substr(pair_nextpos + 1) : ""), pair_nextpos = restxattrs.find_first_of(',')){ + std::string pair = pair_nextpos != std::string::npos ? restxattrs.substr(0, pair_nextpos) : restxattrs; + std::string key; + std::string val; + if(!parse_xattr_keyval(pair, key, &val)){ + // something format error, so skip this. + continue; + } + xattrs[key] = val; + } + return xattrs.size(); +} + +static std::string raw_build_xattrs(const xattrs_t& xattrs) +{ + std::string strxattrs; + bool is_set = false; + for(xattrs_t::const_iterator iter = xattrs.begin(); iter != xattrs.end(); ++iter){ + if(is_set){ + strxattrs += ','; + }else{ + is_set = true; + strxattrs = "{"; + } + strxattrs += '\"'; + strxattrs += iter->first; + strxattrs += "\":\""; + strxattrs += s3fs_base64(reinterpret_cast(iter->second.c_str()), iter->second.length()); + strxattrs += '\"'; + } + if(is_set){ + strxattrs += "}"; + } + return strxattrs; +} + +static std::string build_xattrs(const xattrs_t& xattrs) +{ + std::string strxattrs = raw_build_xattrs(xattrs); + if(strxattrs.empty()){ + strxattrs = "{}"; + } + strxattrs = urlEncodePath(strxattrs); + + return strxattrs; +} + +static int set_xattrs_to_header(headers_t& meta, const char* name, const char* value, size_t size, int flags) +{ + std::string strxattrs; + xattrs_t xattrs; + + headers_t::iterator iter; + if(meta.end() == (iter = meta.find("x-amz-meta-xattr"))){ +#if defined(XATTR_REPLACE) + if(XATTR_REPLACE == (flags & XATTR_REPLACE)){ + // there is no xattr header but flags is replace, so failure. + return -ENOATTR; + } +#endif + }else{ +#if defined(XATTR_CREATE) + if(XATTR_CREATE == (flags & XATTR_CREATE)){ + // found xattr header but flags is only creating, so failure. + return -EEXIST; + } +#endif + strxattrs = iter->second; + } + + // get map as xattrs_t + parse_xattrs(strxattrs, xattrs); + + // add name(do not care overwrite and empty name/value) + xattrs[name] = std::string(value, size); + + // build new strxattrs(not encoded) and set it to headers_t + meta["x-amz-meta-xattr"] = build_xattrs(xattrs); + + S3FS_PRN_DBG("Set xattrs(after adding %s key) = %s", name, raw_build_xattrs(xattrs).c_str()); + + return 0; +} + +#if defined(__APPLE__) +static int s3fs_setxattr(const char* path, const char* name, const char* value, size_t size, int flags, uint32_t position) +#else +static int s3fs_setxattr(const char* path, const char* name, const char* value, size_t size, int flags) +#endif +{ + FUSE_CTX_INFO("[path=%s][name=%s][value=%p][size=%zu][flags=0x%x]", path, name, value, size, flags); + + if(!value && 0 < size){ + S3FS_PRN_ERR("Wrong parameter: value(%p), size(%zu)", value, size); + return 0; + } + +#if defined(__APPLE__) + if (position != 0) { + // No resource fork support + return -EINVAL; + } +#endif + + int result; + std::string strpath; + std::string newpath; + std::string nowcache; + headers_t meta; + struct stat stbuf; + dirtype nDirType = dirtype::UNKNOWN; + + if(0 != (result = check_parent_object_access(path, X_OK))){ + return result; + } + if(0 != (result = check_object_owner(path, &stbuf))){ + return result; + } + + if(S_ISDIR(stbuf.st_mode)){ + result = chk_dir_object_type(path, newpath, strpath, nowcache, &meta, &nDirType); + }else{ + strpath = path; + nowcache = strpath; + result = get_object_attribute(strpath.c_str(), nullptr, &meta); + } + if(0 != result){ + return result; + } + + if(S_ISDIR(stbuf.st_mode) && (IS_REPLACEDIR(nDirType) || IS_CREATE_MP_STAT(path))){ + if(IS_REPLACEDIR(nDirType)){ + // Should rebuild directory object(except new type) + // Need to remove old dir("dir" etc) and make new dir("dir/") + + // At first, remove directory old object + if(0 != (result = remove_old_type_dir(strpath, nDirType))){ + return result; + } + } + StatCache::getStatCacheData()->DelStat(nowcache); + + // Make new directory object("dir/") + struct timespec ts_atime; + struct timespec ts_mtime; + struct timespec ts_ctime; + set_stat_to_timespec(stbuf, stat_time_type::ATIME, ts_atime); + set_stat_to_timespec(stbuf, stat_time_type::MTIME, ts_mtime); + set_stat_to_timespec(stbuf, stat_time_type::CTIME, ts_ctime); + + if(0 != (result = create_directory_object(newpath.c_str(), stbuf.st_mode, ts_atime, ts_mtime, ts_ctime, stbuf.st_uid, stbuf.st_gid, nullptr))){ + return result; + } + + // need to set xattr header for directory. + strpath = newpath; + nowcache = strpath; + } + + // set xattr all object + std::string strSourcePath = (mount_prefix.empty() && "/" == strpath) ? "//" : strpath; + headers_t updatemeta; + updatemeta["x-amz-meta-ctime"] = s3fs_str_realtime(); + updatemeta["x-amz-copy-source"] = urlEncodePath(service_path + S3fsCred::GetBucket() + get_realpath(strSourcePath.c_str())); + updatemeta["x-amz-metadata-directive"] = "REPLACE"; + + // check opened file handle. + // + // If the file starts uploading by multipart when the disk capacity is insufficient, + // we need to put these header after finishing upload. + // Or if the file is only open, we must update to FdEntity's internal meta. + // + AutoFdEntity autoent; + FdEntity* ent; + bool need_put_header = true; + if(nullptr != (ent = autoent.OpenExistFdEntity(path))){ + // get xattr and make new xattr + std::string strxattr; + if(ent->GetXattr(strxattr)){ + updatemeta["x-amz-meta-xattr"] = strxattr; + }else{ + // [NOTE] + // Set an empty xattr. + // This requires the key to be present in order to add xattr. + ent->SetXattr(strxattr); + } + // cppcheck-suppress unmatchedSuppression + // cppcheck-suppress knownConditionTrueFalse + if(0 != (result = set_xattrs_to_header(updatemeta, name, value, size, flags))){ + return result; + } + + if(ent->MergeOrgMeta(updatemeta)){ + // meta is changed, but now uploading. + // then the meta is pending and accumulated to be put after the upload is complete. + S3FS_PRN_INFO("meta pending until upload is complete"); + need_put_header = false; + + // If there is data in the Stats cache, update the Stats cache. + StatCache::getStatCacheData()->UpdateMetaStats(strpath, updatemeta); + + // [NOTE] + // There are cases where this function is called during the process of + // creating a new file (before uploading). + // In this case, a temporary cache exists in the Stat cache. + // So we need to update the cache, if it exists. (see. s3fs_create and s3fs_utimens) + // + if(!StatCache::getStatCacheData()->AddStat(strpath, updatemeta, false, true)){ + return -EIO; + } + } + } + if(need_put_header){ + // not found opened file. + // cppcheck-suppress unmatchedSuppression + // cppcheck-suppress knownConditionTrueFalse + if(0 != (result = set_xattrs_to_header(meta, name, value, size, flags))){ + return result; + } + merge_headers(meta, updatemeta, true); + + // upload meta directly. + if(0 != (result = put_headers(strpath.c_str(), meta, true))){ + return result; + } + StatCache::getStatCacheData()->DelStat(nowcache); + } + + return 0; +} + +#if defined(__APPLE__) +static int s3fs_getxattr(const char* path, const char* name, char* value, size_t size, uint32_t position) +#else +static int s3fs_getxattr(const char* path, const char* name, char* value, size_t size) +#endif +{ +#if defined(__APPLE__) + FUSE_CTX_DBG("[path=%s][name=%s][value=%p][size=%zu]", path, name, value, size); +#else + FUSE_CTX_INFO("[path=%s][name=%s][value=%p][size=%zu]", path, name, value, size); +#endif + + if(!path || !name){ + return -EIO; + } + +#if defined(__APPLE__) + if (position != 0) { + // No resource fork support + return -EINVAL; + } +#endif + + int result; + headers_t meta; + xattrs_t xattrs; + + // check parent directory attribute. + if(0 != (result = check_parent_object_access(path, X_OK))){ + return result; + } + + // get headers + if(0 != (result = get_object_attribute(path, nullptr, &meta))){ + return result; + } + + // get xattrs + headers_t::iterator hiter = meta.find("x-amz-meta-xattr"); + if(meta.end() == hiter){ + // object does not have xattrs + return -ENOATTR; + } + std::string strxattrs = hiter->second; + + parse_xattrs(strxattrs, xattrs); + + S3FS_PRN_DBG("Get xattrs = %s", raw_build_xattrs(xattrs).c_str()); + + // search name + std::string strname = name; + xattrs_t::iterator xiter = xattrs.find(strname); + if(xattrs.end() == xiter){ + // not found name in xattrs + return -ENOATTR; + } + + // decode + size_t length = xiter->second.length(); + const char* pvalue = xiter->second.c_str(); + + if(0 < size){ + if(static_cast(size) < length){ + // over buffer size + return -ERANGE; + } + if(pvalue){ + memcpy(value, pvalue, length); + } + } + + return static_cast(length); +} + +static int s3fs_listxattr(const char* path, char* list, size_t size) +{ + S3FS_PRN_INFO("[path=%s][list=%p][size=%zu]", path, list, size); + + if(!path){ + return -EIO; + } + + int result; + headers_t meta; + xattrs_t xattrs; + + // check parent directory attribute. + if(0 != (result = check_parent_object_access(path, X_OK))){ + return result; + } + + // get headers + if(0 != (result = get_object_attribute(path, nullptr, &meta))){ + return result; + } + + // get xattrs + headers_t::iterator iter; + if(meta.end() == (iter = meta.find("x-amz-meta-xattr"))){ + // object does not have xattrs + return 0; + } + std::string strxattrs = iter->second; + + parse_xattrs(strxattrs, xattrs); + + S3FS_PRN_DBG("Get xattrs = %s", raw_build_xattrs(xattrs).c_str()); + + // calculate total name length + size_t total = 0; + for(xattrs_t::const_iterator xiter = xattrs.begin(); xiter != xattrs.end(); ++xiter){ + if(!xiter->first.empty()){ + total += xiter->first.length() + 1; + } + } + + if(0 == total){ + return 0; + } + + // check parameters + if(0 == size){ + return static_cast(total); + } + if(!list || size < total){ + return -ERANGE; + } + + // copy to list + char* setpos = list; + for(xattrs_t::const_iterator xiter = xattrs.begin(); xiter != xattrs.end(); ++xiter){ + if(!xiter->first.empty()){ + strcpy(setpos, xiter->first.c_str()); + setpos = &setpos[strlen(setpos) + 1]; + } + } + + return static_cast(total); +} + +static int s3fs_removexattr(const char* path, const char* name) +{ + FUSE_CTX_INFO("[path=%s][name=%s]", path, name); + + if(!path || !name){ + return -EIO; + } + + int result; + std::string strpath; + std::string newpath; + std::string nowcache; + headers_t meta; + xattrs_t xattrs; + struct stat stbuf; + dirtype nDirType = dirtype::UNKNOWN; + + if(0 == strcmp(path, "/")){ + S3FS_PRN_ERR("Could not change mode for mount point."); + return -EIO; + } + if(0 != (result = check_parent_object_access(path, X_OK))){ + return result; + } + if(0 != (result = check_object_owner(path, &stbuf))){ + return result; + } + + if(S_ISDIR(stbuf.st_mode)){ + result = chk_dir_object_type(path, newpath, strpath, nowcache, &meta, &nDirType); + }else{ + strpath = path; + nowcache = strpath; + result = get_object_attribute(strpath.c_str(), nullptr, &meta); + } + if(0 != result){ + return result; + } + + // get xattrs + headers_t::iterator hiter = meta.find("x-amz-meta-xattr"); + if(meta.end() == hiter){ + // object does not have xattrs + return -ENOATTR; + } + std::string strxattrs = hiter->second; + + parse_xattrs(strxattrs, xattrs); + + // check name xattrs + std::string strname = name; + xattrs_t::iterator xiter = xattrs.find(strname); + if(xattrs.end() == xiter){ + return -ENOATTR; + } + + // make new header_t after deleting name xattr + xattrs.erase(xiter); + + S3FS_PRN_DBG("Reset xattrs(after delete %s key) = %s", name, raw_build_xattrs(xattrs).c_str()); + + if(S_ISDIR(stbuf.st_mode) && IS_REPLACEDIR(nDirType)){ + // Should rebuild directory object(except new type) + // Need to remove old dir("dir" etc) and make new dir("dir/") + + // At first, remove directory old object + if(0 != (result = remove_old_type_dir(strpath, nDirType))){ + return result; + } + StatCache::getStatCacheData()->DelStat(nowcache); + + // Make new directory object("dir/") + struct timespec ts_atime; + struct timespec ts_mtime; + struct timespec ts_ctime; + set_stat_to_timespec(stbuf, stat_time_type::ATIME, ts_atime); + set_stat_to_timespec(stbuf, stat_time_type::MTIME, ts_mtime); + set_stat_to_timespec(stbuf, stat_time_type::CTIME, ts_ctime); + + if(0 != (result = create_directory_object(newpath.c_str(), stbuf.st_mode, ts_atime, ts_mtime, ts_ctime, stbuf.st_uid, stbuf.st_gid, nullptr))){ + return result; + } + + // need to set xattr header for directory. + strpath = newpath; + nowcache = strpath; + } + + // set xattr all object + std::string strSourcePath = (mount_prefix.empty() && "/" == strpath) ? "//" : strpath; + headers_t updatemeta; + updatemeta["x-amz-copy-source"] = urlEncodePath(service_path + S3fsCred::GetBucket() + get_realpath(strSourcePath.c_str())); + updatemeta["x-amz-metadata-directive"] = "REPLACE"; + if(!xattrs.empty()){ + updatemeta["x-amz-meta-xattr"] = build_xattrs(xattrs); + }else{ + updatemeta["x-amz-meta-xattr"] = ""; // This is a special case. If empty, this header will eventually be removed. + } + + // check opened file handle. + // + // If the file starts uploading by multipart when the disk capacity is insufficient, + // we need to put these header after finishing upload. + // Or if the file is only open, we must update to FdEntity's internal meta. + // + AutoFdEntity autoent; + FdEntity* ent; + bool need_put_header = true; + if(nullptr != (ent = autoent.OpenExistFdEntity(path))){ + if(ent->MergeOrgMeta(updatemeta)){ + // meta is changed, but now uploading. + // then the meta is pending and accumulated to be put after the upload is complete. + S3FS_PRN_INFO("meta pending until upload is complete"); + need_put_header = false; + + // If there is data in the Stats cache, update the Stats cache. + StatCache::getStatCacheData()->UpdateMetaStats(strpath, updatemeta); + } + } + if(need_put_header){ + // not found opened file. + if(updatemeta["x-amz-meta-xattr"].empty()){ + updatemeta.erase("x-amz-meta-xattr"); + } + + merge_headers(meta, updatemeta, true); + + // upload meta directly. + if(0 != (result = put_headers(strpath.c_str(), meta, true))){ + return result; + } + StatCache::getStatCacheData()->DelStat(nowcache); + } + + return 0; +} + +// s3fs_init calls this function to exit cleanly from the fuse event loop. +// +// There's no way to pass an exit status to the high-level event loop API, so +// this function stores the exit value in a global for main() +static void s3fs_exit_fuseloop(int exit_status) +{ + S3FS_PRN_ERR("Exiting FUSE event loop due to errors\n"); + s3fs_init_deferred_exit_status = exit_status; + struct fuse_context *ctx = fuse_get_context(); + if (nullptr != ctx) { + fuse_exit(ctx->fuse); + } +} + +static void* s3fs_init(struct fuse_conn_info* conn) +{ + S3FS_PRN_INIT_INFO("init v%s(commit:%s) with %s, credential-library(%s)", VERSION, COMMIT_HASH_VAL, s3fs_crypt_lib_name(), ps3fscred->GetCredFuncVersion(false)); + + // cache(remove cache dirs at first) + if(is_remove_cache && (!CacheFileStat::DeleteCacheFileStatDirectory() || !FdManager::DeleteCacheDirectory())){ + S3FS_PRN_DBG("Could not initialize cache directory."); + } + + // check loading IAM role name + if(!ps3fscred->LoadIAMRoleFromMetaData()){ + S3FS_PRN_CRIT("could not load IAM role name from meta data."); + s3fs_exit_fuseloop(EXIT_FAILURE); + return nullptr; + } + + // Check Bucket + { + int result; + if(EXIT_SUCCESS != (result = s3fs_check_service())){ + s3fs_exit_fuseloop(result); + return nullptr; + } + } + + // Investigate system capabilities + #ifndef __APPLE__ + if((unsigned int)conn->capable & FUSE_CAP_ATOMIC_O_TRUNC){ + conn->want |= FUSE_CAP_ATOMIC_O_TRUNC; + } + #endif + + if((unsigned int)conn->capable & FUSE_CAP_BIG_WRITES){ + conn->want |= FUSE_CAP_BIG_WRITES; + } + + if(!ThreadPoolMan::Initialize(max_thread_count)){ + S3FS_PRN_CRIT("Could not create thread pool(%d)", max_thread_count); + s3fs_exit_fuseloop(EXIT_FAILURE); + } + + // Signal object + if(!S3fsSignals::Initialize()){ + S3FS_PRN_ERR("Failed to initialize signal object, but continue..."); + } + + return nullptr; +} + +static void s3fs_destroy(void*) +{ + S3FS_PRN_INFO("destroy"); + + // Signal object + if(!S3fsSignals::Destroy()){ + S3FS_PRN_WARN("Failed to clean up signal object."); + } + + ThreadPoolMan::Destroy(); + + // cache(remove at last) + if(is_remove_cache && (!CacheFileStat::DeleteCacheFileStatDirectory() || !FdManager::DeleteCacheDirectory())){ + S3FS_PRN_WARN("Could not remove cache directory."); + } +} + +static int s3fs_access(const char* path, int mask) +{ + FUSE_CTX_INFO("[path=%s][mask=%s%s%s%s]", path, + ((mask & R_OK) == R_OK) ? "R_OK " : "", + ((mask & W_OK) == W_OK) ? "W_OK " : "", + ((mask & X_OK) == X_OK) ? "X_OK " : "", + (mask == F_OK) ? "F_OK" : ""); + + int result = check_object_access(path, mask, nullptr); + S3FS_MALLOCTRIM(0); + return result; +} + +// +// If calling with wrong region, s3fs gets following error body as 400 error code. +// " +// AuthorizationHeaderMalformed +// The authorization header is malformed; the region 'us-east-1' is wrong; expecting 'ap-northeast-1' +// ap-northeast-1 +// ... +// ... +// " +// +// So this is cheap code but s3fs should get correct region automatically. +// +static bool check_region_error(const char* pbody, size_t len, std::string& expectregion) +{ + if(!pbody){ + return false; + } + + std::string code; + if(!simple_parse_xml(pbody, len, "Code", code) || code != "AuthorizationHeaderMalformed"){ + return false; + } + + if(!simple_parse_xml(pbody, len, "Region", expectregion)){ + return false; + } + + return true; +} + +static bool check_endpoint_error(const char* pbody, size_t len, std::string& expectendpoint) +{ + if(!pbody){ + return false; + } + + std::string code; + if(!simple_parse_xml(pbody, len, "Code", code) || code != "PermanentRedirect"){ + return false; + } + + if(!simple_parse_xml(pbody, len, "Endpoint", expectendpoint)){ + return false; + } + + return true; +} + +static bool check_invalid_sse_arg_error(const char* pbody, size_t len) +{ + if(!pbody){ + return false; + } + + std::string code; + if(!simple_parse_xml(pbody, len, "Code", code) || code != "InvalidArgument"){ + return false; + } + std::string argname; + if(!simple_parse_xml(pbody, len, "ArgumentName", argname) || argname != "x-amz-server-side-encryption"){ + return false; + } + return true; +} + +static bool check_error_message(const char* pbody, size_t len, std::string& message) +{ + message.clear(); + if(!pbody){ + return false; + } + if(!simple_parse_xml(pbody, len, "Message", message)){ + return false; + } + return true; +} + +// [NOTE] +// This function checks if the bucket is accessible when s3fs starts. +// +// The following patterns for mount points are supported by s3fs: +// (1) Mount the bucket top +// (2) Mount to a directory(folder) under the bucket. In this case: +// (2A) Directories created by clients other than s3fs +// (2B) Directory created by s3fs +// +// Both case of (1) and (2) check access permissions to the mount point +// path(directory). +// In the case of (2A), if the directory(object) for the mount point does +// not exist, the check fails. However, launching s3fs with the "compat_dir" +// option avoids this error and the check succeeds. If you do not specify +// the "compat_dir" option in case (2A), please create a directory(object) +// for the mount point before launching s3fs. +// +static int s3fs_check_service() +{ + S3FS_PRN_INFO("check services."); + + // At first time for access S3, we check IAM role if it sets. + if(!ps3fscred->CheckIAMCredentialUpdate()){ + S3FS_PRN_CRIT("Failed to initialize IAM credential."); + return EXIT_FAILURE; + } + + S3fsCurl s3fscurl; + int res; + bool force_no_sse = false; + + while(0 > (res = s3fscurl.CheckBucket(get_realpath("/").c_str(), support_compat_dir, force_no_sse))){ + // get response code + bool do_retry = false; + long responseCode = s3fscurl.GetLastResponseCode(); + + // check wrong endpoint, and automatically switch endpoint + if(300 <= responseCode && responseCode < 500){ + + // check region error(for putting message or retrying) + const std::string* body = s3fscurl.GetBodyData(); + std::string expectregion; + std::string expectendpoint; + + // Check if any case can be retried + if(check_region_error(body->c_str(), body->size(), expectregion)){ + // [NOTE] + // If endpoint is not specified(using us-east-1 region) and + // an error is encountered accessing a different region, we + // will retry the check on the expected region. + // see) https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingBucket.html#access-bucket-intro + // + if(s3host != "http://s3.amazonaws.com" && s3host != "https://s3.amazonaws.com"){ + // specified endpoint for specified url is wrong. + if(is_specified_endpoint){ + S3FS_PRN_CRIT("The bucket region is not '%s'(specified) for specified url(%s), it is correctly '%s'. You should specify url(http(s)://s3-%s.amazonaws.com) and endpoint(%s) option.", endpoint.c_str(), s3host.c_str(), expectregion.c_str(), expectregion.c_str(), expectregion.c_str()); + }else{ + S3FS_PRN_CRIT("The bucket region is not '%s'(default) for specified url(%s), it is correctly '%s'. You should specify url(http(s)://s3-%s.amazonaws.com) and endpoint(%s) option.", endpoint.c_str(), s3host.c_str(), expectregion.c_str(), expectregion.c_str(), expectregion.c_str()); + } + + }else if(is_specified_endpoint){ + // specified endpoint is wrong. + S3FS_PRN_CRIT("The bucket region is not '%s'(specified), it is correctly '%s'. You should specify endpoint(%s) option.", endpoint.c_str(), expectregion.c_str(), expectregion.c_str()); + + }else if(S3fsCurl::GetSignatureType() == signature_type_t::V4_ONLY || S3fsCurl::GetSignatureType() == signature_type_t::V2_OR_V4){ + // current endpoint and url are default value, so try to connect to expected region. + S3FS_PRN_CRIT("Failed to connect region '%s'(default), so retry to connect region '%s' for url(http(s)://s3-%s.amazonaws.com).", endpoint.c_str(), expectregion.c_str(), expectregion.c_str()); + + // change endpoint + endpoint = expectregion; + + // change url + if(s3host == "http://s3.amazonaws.com"){ + s3host = "http://s3-" + endpoint + ".amazonaws.com"; + }else if(s3host == "https://s3.amazonaws.com"){ + s3host = "https://s3-" + endpoint + ".amazonaws.com"; + } + + // Retry with changed host + s3fscurl.DestroyCurlHandle(); + do_retry = true; + + }else{ + S3FS_PRN_CRIT("The bucket region is not '%s'(default), it is correctly '%s'. You should specify endpoint(%s) option.", endpoint.c_str(), expectregion.c_str(), expectregion.c_str()); + } + + }else if(check_endpoint_error(body->c_str(), body->size(), expectendpoint)){ + // redirect error + if(pathrequeststyle){ + S3FS_PRN_CRIT("S3 service returned PermanentRedirect (current is url(%s) and endpoint(%s)). You need to specify correct url(http(s)://s3-.amazonaws.com) and endpoint option with use_path_request_style option.", s3host.c_str(), endpoint.c_str()); + }else{ + S3FS_PRN_CRIT("S3 service returned PermanentRedirect with %s (current is url(%s) and endpoint(%s)). You need to specify correct endpoint option.", expectendpoint.c_str(), s3host.c_str(), endpoint.c_str()); + } + return EXIT_FAILURE; + + }else if(check_invalid_sse_arg_error(body->c_str(), body->size())){ + // SSE argument error, so retry it without SSE + S3FS_PRN_CRIT("S3 service returned InvalidArgument(x-amz-server-side-encryption), so retry without adding x-amz-server-side-encryption."); + + // Retry without sse parameters + s3fscurl.DestroyCurlHandle(); + do_retry = true; + force_no_sse = true; + } + } + + // Try changing signature from v4 to v2 + // + // [NOTE] + // If there is no case to retry with the previous checks, and there + // is a chance to retry with signature v2, prepare to retry with v2. + // + if(!do_retry && (responseCode == 400 || responseCode == 403) && S3fsCurl::GetSignatureType() == signature_type_t::V2_OR_V4){ + // switch sigv2 + S3FS_PRN_CRIT("Failed to connect by sigv4, so retry to connect by signature version 2. But you should to review url and endpoint option."); + + // retry to check with sigv2 + s3fscurl.DestroyCurlHandle(); + do_retry = true; + S3fsCurl::SetSignatureType(signature_type_t::V2_ONLY); + } + + // check errors(after retrying) + if(!do_retry && responseCode != 200 && responseCode != 301){ + // parse error message if existed + std::string errMessage; + const std::string* body = s3fscurl.GetBodyData(); + check_error_message(body->c_str(), body->size(), errMessage); + + if(responseCode == 400){ + S3FS_PRN_CRIT("Failed to check bucket and directory for mount point : Bad Request(host=%s, message=%s)", s3host.c_str(), errMessage.c_str()); + }else if(responseCode == 403){ + S3FS_PRN_CRIT("Failed to check bucket and directory for mount point : Invalid Credentials(host=%s, message=%s)", s3host.c_str(), errMessage.c_str()); + }else if(responseCode == 404){ + if(mount_prefix.empty()){ + S3FS_PRN_CRIT("Failed to check bucket and directory for mount point : Bucket or directory not found(host=%s, message=%s)", s3host.c_str(), errMessage.c_str()); + }else{ + S3FS_PRN_CRIT("Failed to check bucket and directory for mount point : Bucket or directory(%s) not found(host=%s, message=%s) - You may need to specify the compat_dir option.", mount_prefix.c_str(), s3host.c_str(), errMessage.c_str()); + } + }else{ + S3FS_PRN_CRIT("Failed to check bucket and directory for mount point : Unable to connect(host=%s, message=%s)", s3host.c_str(), errMessage.c_str()); + } + return EXIT_FAILURE; + } + } + s3fscurl.DestroyCurlHandle(); + + // make sure remote mountpath exists and is a directory + if(!mount_prefix.empty()){ + if(remote_mountpath_exists("/", support_compat_dir) != 0){ + S3FS_PRN_CRIT("Remote mountpath %s not found, this may be resolved with the compat_dir option.", mount_prefix.c_str()); + return EXIT_FAILURE; + } + } + S3FS_MALLOCTRIM(0); + + return EXIT_SUCCESS; +} + +// +// Check & Set attributes for mount point. +// +static bool set_mountpoint_attribute(struct stat& mpst) +{ + mp_uid = geteuid(); + mp_gid = getegid(); + mp_mode = S_IFDIR | (allow_other ? (is_mp_umask ? (~mp_umask & (S_IRWXU | S_IRWXG | S_IRWXO)) : (S_IRWXU | S_IRWXG | S_IRWXO)) : S_IRWXU); + +// In MSYS2 environment with WinFsp, it is not supported to change mode of mount point. +// Doing that forcely will occurs permission problem, so disabling it. +#ifdef __MSYS__ + return true; +#else + S3FS_PRN_INFO2("PROC(uid=%u, gid=%u) - MountPoint(uid=%u, gid=%u, mode=%04o)", + (unsigned int)mp_uid, (unsigned int)mp_gid, (unsigned int)(mpst.st_uid), (unsigned int)(mpst.st_gid), mpst.st_mode); + + // check owner + if(0 == mp_uid || mpst.st_uid == mp_uid){ + return true; + } + // check group permission + if(mpst.st_gid == mp_gid || 1 == is_uid_include_group(mp_uid, mpst.st_gid)){ + if(S_IRWXG == (mpst.st_mode & S_IRWXG)){ + return true; + } + } + // check other permission + if(S_IRWXO == (mpst.st_mode & S_IRWXO)){ + return true; + } + return false; +#endif +} + +// +// Set bucket and mount_prefix based on passed bucket name. +// +static int set_bucket(const char* arg) +{ + // TODO: Mutates input. Consider some other tokenization. + char *bucket_name = const_cast(arg); + if(strstr(arg, ":")){ + if(strstr(arg, "://")){ + S3FS_PRN_EXIT("bucket name and path(\"%s\") is wrong, it must be \"bucket[:/path]\".", arg); + return -1; + } + if(!S3fsCred::SetBucket(strtok(bucket_name, ":"))){ + S3FS_PRN_EXIT("bucket name and path(\"%s\") is wrong, it must be \"bucket[:/path]\".", arg); + return -1; + } + char* pmount_prefix = strtok(nullptr, ""); + if(pmount_prefix){ + if(0 == strlen(pmount_prefix) || '/' != pmount_prefix[0]){ + S3FS_PRN_EXIT("path(%s) must be prefix \"/\".", pmount_prefix); + return -1; + } + mount_prefix = pmount_prefix; + // Trim the last consecutive '/' + mount_prefix = trim_right(mount_prefix, "/"); + } + }else{ + if(!S3fsCred::SetBucket(arg)){ + S3FS_PRN_EXIT("bucket name and path(\"%s\") is wrong, it must be \"bucket[:/path]\".", arg); + return -1; + } + } + return 0; +} + +// +// Utility function for parse "--bucket_size" option +// +// max_size: A string like 20000000, 30GiB, 20TB etc +// return: An integer of type fsblkcnt_t corresponding to the number +// of blocks with max_size calculated with the s3fs block size, +// or 0 on error +// +static fsblkcnt_t parse_bucket_size(char* max_size) +{ + const unsigned long long ten00 = 1000L; + const unsigned long long ten24 = 1024L; + unsigned long long scale = 1; + unsigned long long n_bytes = 0; + char *ptr; + + if(nullptr != (ptr = strstr(max_size, "GB"))){ + scale = ten00 * ten00 * ten00; + if(2 < strlen(ptr)){ + return 0; // no trailing garbage + } + *ptr = '\0'; + }else if(nullptr != (ptr = strstr(max_size, "GiB"))){ + scale = ten24 * ten24 * ten24; + if(3 < strlen(ptr)){ + return 0; // no trailing garbage + } + *ptr = '\0'; + }else if(nullptr != (ptr = strstr(max_size, "TB"))){ + scale = ten00 * ten00 * ten00 * ten00; + if(2 < strlen(ptr)){ + return 0; // no trailing garbage + } + *ptr = '\0'; + }else if(nullptr != (ptr = strstr(max_size, "TiB"))){ + scale = ten24 * ten24 * ten24 * ten24; + if(3 < strlen(ptr)){ + return 0; // no trailing garbage + } + *ptr = '\0'; + }else if(nullptr != (ptr = strstr(max_size, "PB"))){ + scale = ten00 * ten00 * ten00 * ten00 * ten00; + if(2 < strlen(ptr)){ + return 0; // no trailing garbage + } + *ptr = '\0'; + }else if(nullptr != (ptr = strstr(max_size, "PiB"))){ + scale = ten24 * ten24 * ten24 * ten24 * ten24; + if(3 < strlen(ptr)){ + return 0; // no trailing garbage + } + *ptr = '\0'; + }else if(nullptr != (ptr = strstr(max_size, "EB"))){ + scale = ten00 * ten00 * ten00 * ten00 * ten00 * ten00; + if(2 < strlen(ptr)){ + return 0; // no trailing garbage + } + *ptr = '\0'; + }else if(nullptr != (ptr = strstr(max_size, "EiB"))){ + scale = ten24 * ten24 * ten24 * ten24 * ten24 * ten24; + if(3 < strlen(ptr)){ + return 0; // no trailing garbage + } + *ptr = '\0'; + } + + // extra check + for(ptr = max_size; *ptr != '\0'; ++ptr){ + if(!isdigit(*ptr)){ + return 0; // wrong number + } + n_bytes = static_cast(strtoull(max_size, nullptr, 10)); + if((INT64_MAX / scale) < n_bytes){ + return 0; // overflow + } + n_bytes *= scale; + } + + // [NOTE] + // To round a number by s3fs block size. + // And need to check the result value because fsblkcnt_t is 32bit in macos etc. + // + n_bytes /= s3fs_block_size; + + if(sizeof(fsblkcnt_t) <= 4){ + if(INT32_MAX < n_bytes){ + return 0; // overflow + } + } + return static_cast(n_bytes); // cast to fsblkcnt_t +} + +static bool is_cmd_exists(const std::string& command) +{ + // The `command -v` is a POSIX-compliant method for checking the existence of a program. + std::string cmd = "command -v " + command + " >/dev/null 2>&1"; + int result = system(cmd.c_str()); + return (result !=-1 && WIFEXITED(result) && WEXITSTATUS(result) == 0); +} + +static int print_umount_message(const std::string& mp, bool force) +{ + std::string cmd; + if (is_cmd_exists("fusermount")){ + if (force){ + cmd = "fusermount -uz " + mp; + } else { + cmd = "fusermount -u " + mp; + } + }else{ + if (force){ + cmd = "umount -l " + mp; + } else { + cmd = "umount " + mp; + } + } + + S3FS_PRN_EXIT("MOUNTPOINT %s is stale, you could use this command to fix: %s", mp.c_str(), cmd.c_str()); + + return 0; +} + +// This is repeatedly called by the fuse option parser +// if the key is equal to FUSE_OPT_KEY_OPT, it's an option passed in prefixed by +// '-' or '--' e.g.: -f -d -ousecache=/tmp +// +// if the key is equal to FUSE_OPT_KEY_NONOPT, it's either the bucket name +// or the mountpoint. The bucket name will always come before the mountpoint +// +static int my_fuse_opt_proc(void* data, const char* arg, int key, struct fuse_args* outargs) +{ + int ret; + if(key == FUSE_OPT_KEY_NONOPT){ + // the first NONOPT option is the bucket name + if(S3fsCred::GetBucket().empty()){ + if ((ret = set_bucket(arg))){ + return ret; + } + return 0; + }else if (!strcmp(arg, "s3fs")) { + return 0; + } + + // the second NONOPT option is the mountpoint(not utility mode) + if(mountpoint.empty() && utility_incomp_type::NO_UTILITY_MODE == utility_mode){ + // save the mountpoint and do some basic error checking + mountpoint = arg; + struct stat stbuf; + +// In MSYS2 environment with WinFsp, it is not needed to create the mount point before mounting. +// Also it causes a conflict with WinFsp's validation, so disabling it. +#ifdef __MSYS__ + memset(&stbuf, 0, sizeof stbuf); + set_mountpoint_attribute(stbuf); +#else + if(stat(arg, &stbuf) == -1){ + // check stale mountpoint + if(errno == ENOTCONN){ + print_umount_message(mountpoint, true); + } else { + S3FS_PRN_EXIT("unable to access MOUNTPOINT %s: %s", mountpoint.c_str(), strerror(errno)); + } + return -1; + } + if(!(S_ISDIR(stbuf.st_mode))){ + S3FS_PRN_EXIT("MOUNTPOINT: %s is not a directory.", mountpoint.c_str()); + return -1; + } + if(!set_mountpoint_attribute(stbuf)){ + S3FS_PRN_EXIT("MOUNTPOINT: %s permission denied.", mountpoint.c_str()); + return -1; + } + + if(!nonempty){ + const struct dirent *ent; + DIR *dp = opendir(mountpoint.c_str()); + if(dp == nullptr){ + S3FS_PRN_EXIT("failed to open MOUNTPOINT: %s: %s", mountpoint.c_str(), strerror(errno)); + return -1; + } + while((ent = readdir(dp)) != nullptr){ + if(strcmp(ent->d_name, ".") != 0 && strcmp(ent->d_name, "..") != 0){ + closedir(dp); + S3FS_PRN_EXIT("MOUNTPOINT directory %s is not empty. if you are sure this is safe, can use the 'nonempty' mount option.", mountpoint.c_str()); + return -1; + } + } + closedir(dp); + } +#endif + return 1; + } + + // Unknown option + if(utility_incomp_type::NO_UTILITY_MODE == utility_mode){ + S3FS_PRN_EXIT("specified unknown third option(%s).", arg); + }else{ + S3FS_PRN_EXIT("specified unknown second option(%s). you don't need to specify second option(mountpoint) for utility mode(-u).", arg); + } + return -1; + + }else if(key == FUSE_OPT_KEY_OPT){ + if(is_prefix(arg, "uid=")){ + s3fs_uid = get_uid(strchr(arg, '=') + sizeof(char)); + if(0 != geteuid() && 0 == s3fs_uid){ + S3FS_PRN_EXIT("root user can only specify uid=0."); + return -1; + } + is_s3fs_uid = true; + return 1; // continue for fuse option + } + else if(is_prefix(arg, "gid=")){ + s3fs_gid = get_gid(strchr(arg, '=') + sizeof(char)); + if(0 != getegid() && 0 == s3fs_gid){ + S3FS_PRN_EXIT("root user can only specify gid=0."); + return -1; + } + is_s3fs_gid = true; + return 1; // continue for fuse option + } + else if(is_prefix(arg, "bucket_size=")){ + bucket_block_count = parse_bucket_size(const_cast(strchr(arg, '=')) + sizeof(char)); + if(0 == bucket_block_count){ + S3FS_PRN_EXIT("invalid bucket_size option."); + return -1; + } + return 0; + } + else if(is_prefix(arg, "umask=")){ + off_t s3fs_umask_tmp = cvt_strtoofft(strchr(arg, '=') + sizeof(char), /*base=*/ 8); + s3fs_umask = s3fs_umask_tmp & (S_IRWXU | S_IRWXG | S_IRWXO); + is_s3fs_umask = true; + return 1; // continue for fuse option + } + else if(0 == strcmp(arg, "allow_other")){ + allow_other = true; + return 1; // continue for fuse option + } + else if(is_prefix(arg, "mp_umask=")){ + off_t mp_umask_tmp = cvt_strtoofft(strchr(arg, '=') + sizeof(char), /*base=*/ 8); + mp_umask = mp_umask_tmp & (S_IRWXU | S_IRWXG | S_IRWXO); + is_mp_umask = true; + return 0; + } + else if(is_prefix(arg, "default_acl=")){ + const char* acl_string = strchr(arg, '=') + sizeof(char); + acl_t acl = to_acl(acl_string); + if(acl == acl_t::UNKNOWN){ + S3FS_PRN_EXIT("unknown value for default_acl: %s", acl_string); + return -1; + } + S3fsCurl::SetDefaultAcl(acl); + return 0; + } + else if(is_prefix(arg, "retries=")){ + off_t retries = cvt_strtoofft(strchr(arg, '=') + sizeof(char), /*base=*/ 10); + if(retries == 0){ + S3FS_PRN_EXIT("retries must be greater than zero"); + return -1; + } + S3fsCurl::SetRetries(static_cast(retries)); + return 0; + } + else if(is_prefix(arg, "tmpdir=")){ + FdManager::SetTmpDir(strchr(arg, '=') + sizeof(char)); + return 0; + } + else if(is_prefix(arg, "use_cache=")){ + FdManager::SetCacheDir(strchr(arg, '=') + sizeof(char)); + return 0; + } + else if(0 == strcmp(arg, "check_cache_dir_exist")){ + FdManager::SetCheckCacheDirExist(true); + return 0; + } + else if(0 == strcmp(arg, "del_cache")){ + is_remove_cache = true; + return 0; + } + else if(is_prefix(arg, "multireq_max=")){ + int maxreq = static_cast(cvt_strtoofft(strchr(arg, '=') + sizeof(char), /*base=*/ 10)); + S3fsCurl::SetMaxMultiRequest(maxreq); + return 0; + } + else if(0 == strcmp(arg, "nonempty")){ + nonempty = true; + return 1; // need to continue for fuse. + } + else if(0 == strcmp(arg, "nomultipart")){ + nomultipart = true; + return 0; + } + // old format for storage_class + else if(0 == strcmp(arg, "use_rrs") || is_prefix(arg, "use_rrs=")){ + off_t rrs = 1; + // for an old format. + if(is_prefix(arg, "use_rrs=")){ + rrs = cvt_strtoofft(strchr(arg, '=') + sizeof(char), /*base=*/ 10); + } + if(0 == rrs){ + S3fsCurl::SetStorageClass("STANDARD"); + }else if(1 == rrs){ + S3fsCurl::SetStorageClass("REDUCED_REDUNDANCY"); + }else{ + S3FS_PRN_EXIT("poorly formed argument to option: use_rrs"); + return -1; + } + return 0; + } + else if(is_prefix(arg, "storage_class=")){ + const char *storage_class = strchr(arg, '=') + sizeof(char); + S3fsCurl::SetStorageClass(storage_class); + return 0; + } + // + // [NOTE] + // use_sse Set Server Side Encrypting type to SSE-S3 + // use_sse=1 + // use_sse=file Set Server Side Encrypting type to Custom key(SSE-C) and load custom keys + // use_sse=custom(c):file + // use_sse=custom(c) Set Server Side Encrypting type to Custom key(SSE-C) + // use_sse=kmsid(k):kms-key-id Set Server Side Encrypting type to AWS Key Management key id(SSE-KMS) and load KMS id + // use_sse=kmsid(k) Set Server Side Encrypting type to AWS Key Management key id(SSE-KMS) + // + // load_sse_c=file Load Server Side Encrypting custom keys + // + // AWSSSECKEYS Loading Environment for Server Side Encrypting custom keys + // AWSSSEKMSID Loading Environment for Server Side Encrypting Key id + // + else if(is_prefix(arg, "use_sse")){ + if(0 == strcmp(arg, "use_sse") || 0 == strcmp(arg, "use_sse=1")){ // use_sse=1 is old type parameter + // sse type is SSE_S3 + if(!S3fsCurl::IsSseDisable() && !S3fsCurl::IsSseS3Type()){ + S3FS_PRN_EXIT("already set SSE another type, so conflict use_sse option or environment."); + return -1; + } + S3fsCurl::SetSseType(sse_type_t::SSE_S3); + + }else if(0 == strcmp(arg, "use_sse=kmsid") || 0 == strcmp(arg, "use_sse=k")){ + // sse type is SSE_KMS with out kmsid(expecting id is loaded by environment) + if(!S3fsCurl::IsSseDisable() && !S3fsCurl::IsSseKmsType()){ + S3FS_PRN_EXIT("already set SSE another type, so conflict use_sse option or environment."); + return -1; + } + if(!S3fsCurl::IsSetSseKmsId()){ + S3FS_PRN_EXIT("use_sse=kms but not loaded kms id by environment."); + return -1; + } + S3fsCurl::SetSseType(sse_type_t::SSE_KMS); + + }else if(is_prefix(arg, "use_sse=kmsid:") || is_prefix(arg, "use_sse=k:")){ + // sse type is SSE_KMS with kmsid + if(!S3fsCurl::IsSseDisable() && !S3fsCurl::IsSseKmsType()){ + S3FS_PRN_EXIT("already set SSE another type, so conflict use_sse option or environment."); + return -1; + } + const char* kmsid; + if(is_prefix(arg, "use_sse=kmsid:")){ + kmsid = &arg[strlen("use_sse=kmsid:")]; + }else{ + kmsid = &arg[strlen("use_sse=k:")]; + } + if(!S3fsCurl::SetSseKmsid(kmsid)){ + S3FS_PRN_EXIT("failed to load use_sse kms id."); + return -1; + } + S3fsCurl::SetSseType(sse_type_t::SSE_KMS); + + }else if(0 == strcmp(arg, "use_sse=custom") || 0 == strcmp(arg, "use_sse=c")){ + // sse type is SSE_C with out custom keys(expecting keys are loaded by environment or load_sse_c option) + if(!S3fsCurl::IsSseDisable() && !S3fsCurl::IsSseCType()){ + S3FS_PRN_EXIT("already set SSE another type, so conflict use_sse option or environment."); + return -1; + } + // [NOTE] + // do not check ckeys exists here. + // + S3fsCurl::SetSseType(sse_type_t::SSE_C); + + }else if(is_prefix(arg, "use_sse=custom:") || is_prefix(arg, "use_sse=c:")){ + // sse type is SSE_C with custom keys + if(!S3fsCurl::IsSseDisable() && !S3fsCurl::IsSseCType()){ + S3FS_PRN_EXIT("already set SSE another type, so conflict use_sse option or environment."); + return -1; + } + const char* ssecfile; + if(is_prefix(arg, "use_sse=custom:")){ + ssecfile = &arg[strlen("use_sse=custom:")]; + }else{ + ssecfile = &arg[strlen("use_sse=c:")]; + } + if(!S3fsCurl::SetSseCKeys(ssecfile)){ + S3FS_PRN_EXIT("failed to load use_sse custom key file(%s).", ssecfile); + return -1; + } + S3fsCurl::SetSseType(sse_type_t::SSE_C); + + }else if(0 == strcmp(arg, "use_sse=")){ // this type is old style(parameter is custom key file path) + // SSE_C with custom keys. + const char* ssecfile = &arg[strlen("use_sse=")]; + if(!S3fsCurl::SetSseCKeys(ssecfile)){ + S3FS_PRN_EXIT("failed to load use_sse custom key file(%s).", ssecfile); + return -1; + } + S3fsCurl::SetSseType(sse_type_t::SSE_C); + + }else{ + // never come here. + S3FS_PRN_EXIT("something wrong use_sse option."); + return -1; + } + return 0; + } + // [NOTE] + // Do only load SSE custom keys, care for set without set sse type. + else if(is_prefix(arg, "load_sse_c=")){ + const char* ssecfile = &arg[strlen("load_sse_c=")]; + if(!S3fsCurl::SetSseCKeys(ssecfile)){ + S3FS_PRN_EXIT("failed to load use_sse custom key file(%s).", ssecfile); + return -1; + } + return 0; + } + else if(is_prefix(arg, "ssl_verify_hostname=")){ + long sslvh = static_cast(cvt_strtoofft(strchr(arg, '=') + sizeof(char), /*base=*/ 10)); + if(-1 == S3fsCurl::SetSslVerifyHostname(sslvh)){ + S3FS_PRN_EXIT("poorly formed argument to option: ssl_verify_hostname."); + return -1; + } + return 0; + } + // + // Detect options for credential + // + else if(0 >= (ret = ps3fscred->DetectParam(arg))){ + if(0 > ret){ + return -1; + } + return 0; + } + else if(is_prefix(arg, "public_bucket=")){ + off_t pubbucket = cvt_strtoofft(strchr(arg, '=') + sizeof(char), /*base=*/ 10); + if(1 == pubbucket){ + S3fsCurl::SetPublicBucket(true); + // [NOTE] + // if bucket is public(without credential), s3 do not allow copy api. + // so s3fs sets nocopyapi mode. + // + nocopyapi = true; + }else if(0 == pubbucket){ + S3fsCurl::SetPublicBucket(false); + }else{ + S3FS_PRN_EXIT("poorly formed argument to option: public_bucket."); + return -1; + } + return 0; + } + else if(is_prefix(arg, "bucket=")){ + std::string bname = strchr(arg, '=') + sizeof(char); + if ((ret = set_bucket(bname.c_str()))){ + return ret; + } + return 0; + } + else if(0 == strcmp(arg, "no_check_certificate")){ + S3fsCurl::SetCheckCertificate(false); + return 0; + } + else if(is_prefix(arg, "connect_timeout=")){ + long contimeout = static_cast(cvt_strtoofft(strchr(arg, '=') + sizeof(char), /*base=*/ 10)); + S3fsCurl::SetConnectTimeout(contimeout); + return 0; + } + else if(is_prefix(arg, "readwrite_timeout=")){ + time_t rwtimeout = static_cast(cvt_strtoofft(strchr(arg, '=') + sizeof(char), /*base=*/ 10)); + S3fsCurl::SetReadwriteTimeout(rwtimeout); + return 0; + } + else if(is_prefix(arg, "list_object_max_keys=")){ + int max_keys = static_cast(cvt_strtoofft(strchr(arg, '=') + sizeof(char), /*base=*/ 10)); + if(max_keys < 1000){ + S3FS_PRN_EXIT("argument should be over 1000: list_object_max_keys"); + return -1; + } + max_keys_list_object = max_keys; + return 0; + } + else if(is_prefix(arg, "max_stat_cache_size=")){ + unsigned long cache_size = static_cast(cvt_strtoofft(strchr(arg, '=') + sizeof(char), 10)); + StatCache::getStatCacheData()->SetCacheSize(cache_size); + return 0; + } + else if(is_prefix(arg, "stat_cache_expire=")){ + time_t expr_time = static_cast(cvt_strtoofft(strchr(arg, '=') + sizeof(char), 10)); + StatCache::getStatCacheData()->SetExpireTime(expr_time); + return 0; + } + // [NOTE] + // This option is for compatibility old version. + else if(is_prefix(arg, "stat_cache_interval_expire=")){ + time_t expr_time = static_cast(cvt_strtoofft(strchr(arg, '=') + sizeof(char), /*base=*/ 10)); + StatCache::getStatCacheData()->SetExpireTime(expr_time, true); + return 0; + } + else if(0 == strcmp(arg, "enable_noobj_cache")){ + S3FS_PRN_WARN("enable_noobj_cache is enabled by default and a future version will remove this option."); + StatCache::getStatCacheData()->EnableCacheNoObject(); + return 0; + } + else if(0 == strcmp(arg, "disable_noobj_cache")){ + StatCache::getStatCacheData()->DisableCacheNoObject(); + return 0; + } + else if(0 == strcmp(arg, "nodnscache")){ + S3fsCurl::SetDnsCache(false); + return 0; + } + else if(0 == strcmp(arg, "nosscache")){ + S3fsCurl::SetSslSessionCache(false); + return 0; + } + else if(is_prefix(arg, "parallel_count=") || is_prefix(arg, "parallel_upload=")){ + int maxpara = static_cast(cvt_strtoofft(strchr(arg, '=') + sizeof(char), /*base=*/ 10)); + if(0 >= maxpara){ + S3FS_PRN_EXIT("argument should be over 1: parallel_count"); + return -1; + } + S3fsCurl::SetMaxParallelCount(maxpara); + return 0; + } + else if(is_prefix(arg, "max_thread_count=")){ + int max_thcount = static_cast(cvt_strtoofft(strchr(arg, '=') + sizeof(char), /*base=*/ 10)); + if(0 >= max_thcount){ + S3FS_PRN_EXIT("argument should be over 1: max_thread_count"); + return -1; + } + max_thread_count = max_thcount; + S3FS_PRN_WARN("The max_thread_count option is not a formal option. Please note that it will change in the future."); + return 0; + } + else if(is_prefix(arg, "fd_page_size=")){ + S3FS_PRN_ERR("option fd_page_size is no longer supported, so skip this option."); + return 0; + } + else if(is_prefix(arg, "multipart_size=")){ + off_t size = static_cast(cvt_strtoofft(strchr(arg, '=') + sizeof(char), /*base=*/ 10)); + if(!S3fsCurl::SetMultipartSize(size)){ + S3FS_PRN_EXIT("multipart_size option must be at least 5 MB."); + return -1; + } + return 0; + } + else if(is_prefix(arg, "multipart_copy_size=")){ + off_t size = static_cast(cvt_strtoofft(strchr(arg, '=') + sizeof(char), /*base=*/ 10)); + if(!S3fsCurl::SetMultipartCopySize(size)){ + S3FS_PRN_EXIT("multipart_copy_size option must be at least 5 MB."); + return -1; + } + return 0; + } + else if(is_prefix(arg, "max_dirty_data=")){ + off_t size = static_cast(cvt_strtoofft(strchr(arg, '=') + sizeof(char), /*base=*/ 10)); + if(size >= 50){ + size *= 1024 * 1024; + }else if(size != -1){ + S3FS_PRN_EXIT("max_dirty_data option must be at least 50 MB."); + return -1; + } + max_dirty_data = size; + return 0; + } + if(is_prefix(arg, "free_space_ratio=")){ + int ratio = static_cast(cvt_strtoofft(strchr(arg, '=') + sizeof(char), /*base=*/ 10)); + + if(FdManager::GetEnsureFreeDiskSpace()!=0){ + S3FS_PRN_EXIT("option free_space_ratio conflicts with ensure_diskfree, please set only one of them."); + return -1; + } + + if(ratio < 0 || ratio > 100){ + S3FS_PRN_EXIT("option free_space_ratio must between 0 to 100, which is: %d", ratio); + return -1; + } + + off_t dfsize = FdManager::GetTotalDiskSpaceByRatio(ratio); + S3FS_PRN_INFO("Free space ratio set to %d %%, ensure the available disk space is greater than %.3f MB", ratio, static_cast(dfsize) / 1024 / 1024); + + if(dfsize < S3fsCurl::GetMultipartSize()){ + S3FS_PRN_WARN("specified size to ensure disk free space is smaller than multipart size, so set multipart size to it."); + dfsize = S3fsCurl::GetMultipartSize(); + } + FdManager::SetEnsureFreeDiskSpace(dfsize); + return 0; + } + else if(is_prefix(arg, "ensure_diskfree=")){ + off_t dfsize = cvt_strtoofft(strchr(arg, '=') + sizeof(char), /*base=*/ 10) * 1024 * 1024; + + if(FdManager::GetEnsureFreeDiskSpace()!=0){ + S3FS_PRN_EXIT("option free_space_ratio conflicts with ensure_diskfree, please set only one of them."); + return -1; + } + + S3FS_PRN_INFO("Set and ensure the available disk space is greater than %.3f MB.", static_cast(dfsize) / 1024 / 1024); + if(dfsize < S3fsCurl::GetMultipartSize()){ + S3FS_PRN_WARN("specified size to ensure disk free space is smaller than multipart size, so set multipart size to it."); + dfsize = S3fsCurl::GetMultipartSize(); + } + FdManager::SetEnsureFreeDiskSpace(dfsize); + return 0; + } + else if(is_prefix(arg, "fake_diskfree=")){ + S3FS_PRN_WARN("The fake_diskfree option was specified. Use this option for testing or debugging."); + + // [NOTE] This value is used for initializing to FdManager after parsing all options. + fake_diskfree_size = cvt_strtoofft(strchr(arg, '=') + sizeof(char), /*base=*/ 10) * 1024 * 1024; + return 0; + } + else if(is_prefix(arg, "multipart_threshold=")){ + multipart_threshold = static_cast(cvt_strtoofft(strchr(arg, '=') + sizeof(char), /*base=*/ 10)) * 1024 * 1024; + if(multipart_threshold <= MIN_MULTIPART_SIZE){ + S3FS_PRN_EXIT("multipart_threshold must be at least %lld, was: %lld", static_cast(MIN_MULTIPART_SIZE), static_cast(multipart_threshold)); + return -1; + } + return 0; + } + else if(is_prefix(arg, "singlepart_copy_limit=")){ + singlepart_copy_limit = static_cast(cvt_strtoofft(strchr(arg, '=') + sizeof(char), /*base=*/ 10)) * 1024 * 1024; + return 0; + } + else if(is_prefix(arg, "ahbe_conf=")){ + std::string ahbe_conf = strchr(arg, '=') + sizeof(char); + if(!AdditionalHeader::get()->Load(ahbe_conf.c_str())){ + S3FS_PRN_EXIT("failed to load ahbe_conf file(%s).", ahbe_conf.c_str()); + return -1; + } + AdditionalHeader::get()->Dump(); + return 0; + } + else if(0 == strcmp(arg, "noxmlns")){ + noxmlns = true; + return 0; + } + else if(0 == strcmp(arg, "nomixupload")){ + FdEntity::SetNoMixMultipart(); + return 0; + } + else if(0 == strcmp(arg, "nocopyapi")){ + nocopyapi = true; + return 0; + } + else if(0 == strcmp(arg, "streamupload")){ + FdEntity::SetStreamUpload(true); + S3FS_PRN_WARN("The streamupload option is not a formal option. Please note that it will change in the future."); + return 0; + } + else if(0 == strcmp(arg, "norenameapi")){ + norenameapi = true; + return 0; + } + else if(0 == strcmp(arg, "complement_stat")){ + complement_stat = true; + return 0; + } + else if(0 == strcmp(arg, "notsup_compat_dir")){ + S3FS_PRN_WARN("notsup_compat_dir is enabled by default and a future version will remove this option."); + support_compat_dir = false; + return 0; + } + else if(0 == strcmp(arg, "compat_dir")){ + support_compat_dir = true; + return 0; + } + else if(0 == strcmp(arg, "enable_content_md5")){ + S3fsCurl::SetContentMd5(true); + return 0; + } + else if(0 == strcmp(arg, "enable_unsigned_payload")){ + S3fsCurl::SetUnsignedPayload(true); + return 0; + } + else if(0 == strcmp(arg, "update_parent_dir_stat")){ + update_parent_dir_stat = true; + return 0; + } + else if(is_prefix(arg, "host=")){ + s3host = strchr(arg, '=') + sizeof(char); + return 0; + } + else if(is_prefix(arg, "servicepath=")){ + service_path = strchr(arg, '=') + sizeof(char); + return 0; + } + else if(is_prefix(arg, "url=")){ + s3host = strchr(arg, '=') + sizeof(char); + // strip the trailing '/', if any, off the end of the host + // std::string + size_t found, length; + found = s3host.find_last_of('/'); + length = s3host.length(); + while(found == (length - 1) && length > 0){ + s3host.erase(found); + found = s3host.find_last_of('/'); + length = s3host.length(); + } + // Check url for http / https protocol std::string + if(!is_prefix(s3host.c_str(), "https://") && !is_prefix(s3host.c_str(), "http://")){ + S3FS_PRN_EXIT("option url has invalid format, missing http / https protocol"); + return -1; + } + return 0; + } + else if(0 == strcmp(arg, "sigv2")){ + S3fsCurl::SetSignatureType(signature_type_t::V2_ONLY); + return 0; + } + else if(0 == strcmp(arg, "sigv4")){ + S3fsCurl::SetSignatureType(signature_type_t::V4_ONLY); + return 0; + } + else if(is_prefix(arg, "endpoint=")){ + endpoint = strchr(arg, '=') + sizeof(char); + is_specified_endpoint = true; + return 0; + } + else if(0 == strcmp(arg, "use_path_request_style")){ + pathrequeststyle = true; + return 0; + } + else if(0 == strcmp(arg, "noua")){ + S3fsCurl::SetUserAgentFlag(false); + return 0; + } + else if(0 == strcmp(arg, "listobjectsv2")){ + S3fsCurl::SetListObjectsV2(true); + return 0; + } + else if(0 == strcmp(arg, "use_xattr")){ + is_use_xattr = true; + return 0; + }else if(is_prefix(arg, "use_xattr=")){ + const char* strflag = strchr(arg, '=') + sizeof(char); + if(0 == strcmp(strflag, "1")){ + is_use_xattr = true; + }else if(0 == strcmp(strflag, "0")){ + is_use_xattr = false; + }else{ + S3FS_PRN_EXIT("option use_xattr has unknown parameter(%s).", strflag); + return -1; + } + return 0; + } + else if(is_prefix(arg, "cipher_suites=")){ + cipher_suites = strchr(arg, '=') + sizeof(char); + return 0; + } + else if(is_prefix(arg, "instance_name=")){ + instance_name = strchr(arg, '=') + sizeof(char); + instance_name = "[" + instance_name + "]"; + return 0; + } + else if(is_prefix(arg, "mime=")){ + mimetype_file = strchr(arg, '=') + sizeof(char); + return 0; + } + else if(is_prefix(arg, "proxy=")){ + const char* url = &arg[strlen("proxy=")]; + if(!S3fsCurl::SetProxy(url)){ + S3FS_PRN_EXIT("failed to set proxy(%s).", url); + return -1; + } + return 0; + } + else if(is_prefix(arg, "proxy_cred_file=")){ + const char* file = &arg[strlen("proxy_cred_file=")]; + if(!S3fsCurl::SetProxyUserPwd(file)){ + S3FS_PRN_EXIT("failed to set proxy user and passphrase from file(%s).", file); + return -1; + } + return 0; + } + // + // log file option + // + else if(is_prefix(arg, "logfile=")){ + const char* strlogfile = strchr(arg, '=') + sizeof(char); + if(!S3fsLog::SetLogfile(strlogfile)){ + S3FS_PRN_EXIT("The file(%s) specified by logfile option could not be opened.", strlogfile); + return -1; + } + return 0; + } + // + // debug level option + // + else if(is_prefix(arg, "dbglevel=")){ + const char* strlevel = strchr(arg, '=') + sizeof(char); + if(0 == strcasecmp(strlevel, "silent") || 0 == strcasecmp(strlevel, "critical") || 0 == strcasecmp(strlevel, "crit")){ + S3fsLog::SetLogLevel(S3fsLog::LEVEL_CRIT); + }else if(0 == strcasecmp(strlevel, "error") || 0 == strcasecmp(strlevel, "err")){ + S3fsLog::SetLogLevel(S3fsLog::LEVEL_ERR); + }else if(0 == strcasecmp(strlevel, "wan") || 0 == strcasecmp(strlevel, "warn") || 0 == strcasecmp(strlevel, "warning")){ + S3fsLog::SetLogLevel(S3fsLog::LEVEL_WARN); + }else if(0 == strcasecmp(strlevel, "inf") || 0 == strcasecmp(strlevel, "info") || 0 == strcasecmp(strlevel, "information")){ + S3fsLog::SetLogLevel(S3fsLog::LEVEL_INFO); + }else if(0 == strcasecmp(strlevel, "dbg") || 0 == strcasecmp(strlevel, "debug")){ + S3fsLog::SetLogLevel(S3fsLog::LEVEL_DBG); + }else{ + S3FS_PRN_EXIT("option dbglevel has unknown parameter(%s).", strlevel); + return -1; + } + return 0; + } + // + // debug option + // + // S3fsLog level is LEVEL_INFO, after second -d is passed to fuse. + // + else if(0 == strcmp(arg, "-d") || 0 == strcmp(arg, "--debug")){ + if(!S3fsLog::IsS3fsLogInfo() && !S3fsLog::IsS3fsLogDbg()){ + S3fsLog::SetLogLevel(S3fsLog::LEVEL_INFO); + return 0; + } + if(0 == strcmp(arg, "--debug")){ + // fuse doesn't understand "--debug", but it understands -d. + // but we can't pass -d back to fuse. + return 0; + } + } + // "f2" is not used no more. + // (set S3fsLog::LEVEL_DBG) + else if(0 == strcmp(arg, "f2")){ + S3fsLog::SetLogLevel(S3fsLog::LEVEL_DBG); + return 0; + } + else if(0 == strcmp(arg, "curldbg")){ + S3fsCurl::SetVerbose(true); + return 0; + }else if(is_prefix(arg, "curldbg=")){ + const char* strlevel = strchr(arg, '=') + sizeof(char); + if(0 == strcasecmp(strlevel, "normal")){ + S3fsCurl::SetVerbose(true); + }else if(0 == strcasecmp(strlevel, "body")){ + S3fsCurl::SetVerbose(true); + S3fsCurl::SetDumpBody(true); + }else{ + S3FS_PRN_EXIT("option curldbg has unknown parameter(%s).", strlevel); + return -1; + } + return 0; + } + // + // no time stamp in debug message + // + else if(0 == strcmp(arg, "no_time_stamp_msg")){ + S3fsLog::SetTimeStamp(false); + return 0; + } + // + // Check cache file, using SIGUSR1 + // + else if(0 == strcmp(arg, "set_check_cache_sigusr1")){ + if(!S3fsSignals::SetUsr1Handler(nullptr)){ + S3FS_PRN_EXIT("could not set sigusr1 for checking cache."); + return -1; + } + return 0; + }else if(is_prefix(arg, "set_check_cache_sigusr1=")){ + const char* strfilepath = strchr(arg, '=') + sizeof(char); + if(!S3fsSignals::SetUsr1Handler(strfilepath)){ + S3FS_PRN_EXIT("could not set sigusr1 for checking cache and output file(%s).", strfilepath); + return -1; + } + return 0; + } + else if(is_prefix(arg, "accessKeyId=")){ + S3FS_PRN_EXIT("option accessKeyId is no longer supported."); + return -1; + } + else if(is_prefix(arg, "secretAccessKey=")){ + S3FS_PRN_EXIT("option secretAccessKey is no longer supported."); + return -1; + } + else if(0 == strcmp(arg, "use_wtf8")){ + use_wtf8 = true; + return 0; + } + else if(0 == strcmp(arg, "requester_pays")){ + S3fsCurl::SetRequesterPays(true); + return 0; + } + // [NOTE] + // following option will be discarding, because these are not for fuse. + // (Referenced sshfs.c) + // + else if(0 == strcmp(arg, "auto") || + 0 == strcmp(arg, "noauto") || + 0 == strcmp(arg, "user") || + 0 == strcmp(arg, "nouser") || + 0 == strcmp(arg, "users") || + 0 == strcmp(arg, "_netdev")) + { + return 0; + } + else if(is_prefix(arg, "newcache_conf=")){ + newcache_conf = std::string(strchr(arg, '=') + sizeof(char)); + if(!newcache_conf.empty()) use_newcache = true; + return 0; + } + } + return 1; +} + +int main(int argc, char* argv[]) +{ + int ch; + int fuse_res; + int option_index = 0; + struct fuse_operations s3fs_oper; + time_t incomp_abort_time = (24 * 60 * 60); + S3fsLog singletonLog; + + static constexpr struct option long_opts[] = { + {"help", no_argument, nullptr, 'h'}, + {"version", no_argument, nullptr, 0}, + {"debug", no_argument, nullptr, 'd'}, + {"incomplete-mpu-list", no_argument, nullptr, 'u'}, + {"incomplete-mpu-abort", optional_argument, nullptr, 'a'}, // 'a' is only identifier and is not option. + {nullptr, 0, nullptr, 0} + }; + + // init bucket_block_size +#if defined(__MSYS__) + bucket_block_count = static_cast(INT32_MAX); +#elif defined(__APPLE__) + bucket_block_count = static_cast(INT32_MAX); +#else + bucket_block_count = ~0U; +#endif + + // init xml2 + xmlInitParser(); + LIBXML_TEST_VERSION + + init_sysconf_vars(); + + // get program name - emulate basename + program_name = argv[0]; + size_t found = program_name.find_last_of('/'); + if(found != std::string::npos){ + program_name.replace(0, found+1, ""); + } + + // set credential object + // + ps3fscred.reset(new S3fsCred()); + if(!S3fsCurl::InitCredentialObject(ps3fscred.get())){ + S3FS_PRN_EXIT("Failed to setup credential object to s3fs curl."); + exit(EXIT_FAILURE); + } + + while((ch = getopt_long(argc, argv, "dho:fsu", long_opts, &option_index)) != -1){ + switch(ch){ + case 0: + if(strcmp(long_opts[option_index].name, "version") == 0){ + show_version(); + exit(EXIT_SUCCESS); + } + break; + case 'h': + show_help(); + exit(EXIT_SUCCESS); + case 'o': + break; + case 'd': + break; + case 'f': + foreground = true; + break; + case 's': + break; + case 'u': // --incomplete-mpu-list + if(utility_incomp_type::NO_UTILITY_MODE != utility_mode){ + S3FS_PRN_EXIT("already utility mode option is specified."); + exit(EXIT_FAILURE); + } + utility_mode = utility_incomp_type::INCOMP_TYPE_LIST; + break; + case 'a': // --incomplete-mpu-abort + if(utility_incomp_type::NO_UTILITY_MODE != utility_mode){ + S3FS_PRN_EXIT("already utility mode option is specified."); + exit(EXIT_FAILURE); + } + utility_mode = utility_incomp_type::INCOMP_TYPE_ABORT; + + // check expire argument + if(nullptr != optarg && 0 == strcasecmp(optarg, "all")){ // all is 0s + incomp_abort_time = 0; + }else if(nullptr != optarg){ + if(!convert_unixtime_from_option_arg(optarg, incomp_abort_time)){ + S3FS_PRN_EXIT("--incomplete-mpu-abort option argument is wrong."); + exit(EXIT_FAILURE); + } + } + // if optarg is null, incomp_abort_time is 24H(default) + break; + default: + exit(EXIT_FAILURE); + } + } + // print launch message + print_launch_message(argc, argv); + + // Load SSE environment + if(!S3fsCurl::LoadEnvSse()){ + S3FS_PRN_EXIT("something wrong about SSE environment."); + exit(EXIT_FAILURE); + } + + // ssl init + if(!s3fs_init_global_ssl()){ + S3FS_PRN_EXIT("could not initialize for ssl libraries."); + exit(EXIT_FAILURE); + } + + // mutex for xml + if(!init_parser_xml_lock()){ + S3FS_PRN_EXIT("could not initialize mutex for xml parser."); + s3fs_destroy_global_ssl(); + exit(EXIT_FAILURE); + } + + // mutex for basename/dirname + if(!init_basename_lock()){ + S3FS_PRN_EXIT("could not initialize mutex for basename/dirname."); + s3fs_destroy_global_ssl(); + destroy_parser_xml_lock(); + exit(EXIT_FAILURE); + } + + // init curl (without mime types) + // + // [NOTE] + // The curl initialization here does not load mime types. + // The mime types file parameter are dynamic values according + // to the user's environment, and are analyzed by the my_fuse_opt_proc + // function. + // The my_fuse_opt_proc function is executed after this curl + // initialization. Because the curl method is used in the + // my_fuse_opt_proc function, then it must be called here to + // initialize. Fortunately, the processing using mime types + // is only PUT/POST processing, and it is not used until the + // call of my_fuse_opt_proc function is completed. Therefore, + // the mime type is loaded just after calling the my_fuse_opt_proc + // function. + // + if(!S3fsCurl::InitS3fsCurl()){ + S3FS_PRN_EXIT("Could not initiate curl library."); + s3fs_destroy_global_ssl(); + destroy_parser_xml_lock(); + destroy_basename_lock(); + exit(EXIT_FAILURE); + } + + // clear this structure + memset(&s3fs_oper, 0, sizeof(s3fs_oper)); + + // This is the fuse-style parser for the arguments + // after which the bucket name and mountpoint names + // should have been set + struct fuse_args custom_args = FUSE_ARGS_INIT(argc, argv); + if(0 != fuse_opt_parse(&custom_args, nullptr, nullptr, my_fuse_opt_proc)){ + S3fsCurl::DestroyS3fsCurl(); + s3fs_destroy_global_ssl(); + destroy_parser_xml_lock(); + destroy_basename_lock(); + exit(EXIT_FAILURE); + } + + // init mime types for curl + if(!S3fsCurl::InitMimeType(mimetype_file)){ + S3FS_PRN_WARN("Missing MIME types prevents setting Content-Type on uploaded objects."); + } + + // [NOTE] + // exclusive option check here. + // + if(strcasecmp(S3fsCurl::GetStorageClass().c_str(), "REDUCED_REDUNDANCY") == 0 && !S3fsCurl::IsSseDisable()){ + S3FS_PRN_EXIT("use_sse option could not be specified with storage class reduced_redundancy."); + S3fsCurl::DestroyS3fsCurl(); + s3fs_destroy_global_ssl(); + destroy_parser_xml_lock(); + destroy_basename_lock(); + exit(EXIT_FAILURE); + } + if(!S3fsCurl::FinalCheckSse()){ + S3FS_PRN_EXIT("something wrong about SSE options."); + S3fsCurl::DestroyS3fsCurl(); + s3fs_destroy_global_ssl(); + destroy_parser_xml_lock(); + destroy_basename_lock(); + exit(EXIT_FAILURE); + } + + if(S3fsCurl::GetSignatureType() == signature_type_t::V2_ONLY && S3fsCurl::GetUnsignedPayload()){ + S3FS_PRN_WARN("Ignoring enable_unsigned_payload with sigv2"); + } + + if(!FdEntity::GetNoMixMultipart() && max_dirty_data != -1){ + S3FS_PRN_WARN("Setting max_dirty_data to -1 when nomixupload is enabled"); + max_dirty_data = -1; + } + + // + // Check the combination of parameters for credential + // + if(!ps3fscred->CheckAllParams()){ + S3fsCurl::DestroyS3fsCurl(); + s3fs_destroy_global_ssl(); + destroy_parser_xml_lock(); + destroy_basename_lock(); + exit(EXIT_FAILURE); + } + + // The second plain argument is the mountpoint + // if the option was given, we all ready checked for a + // readable, non-empty directory, this checks determines + // if the mountpoint option was ever supplied + if(utility_incomp_type::NO_UTILITY_MODE == utility_mode){ + if(mountpoint.empty()){ + S3FS_PRN_EXIT("missing MOUNTPOINT argument."); + show_usage(); + S3fsCurl::DestroyS3fsCurl(); + s3fs_destroy_global_ssl(); + destroy_parser_xml_lock(); + destroy_basename_lock(); + exit(EXIT_FAILURE); + } + } + + // check tmp dir permission + if(!FdManager::CheckTmpDirExist()){ + S3FS_PRN_EXIT("temporary directory doesn't exists."); + S3fsCurl::DestroyS3fsCurl(); + s3fs_destroy_global_ssl(); + destroy_parser_xml_lock(); + destroy_basename_lock(); + exit(EXIT_FAILURE); + } + + // check cache dir permission + if(!FdManager::CheckCacheDirExist() || !FdManager::CheckCacheTopDir() || !CacheFileStat::CheckCacheFileStatTopDir()){ + S3FS_PRN_EXIT("could not allow cache directory permission, check permission of cache directories."); + S3fsCurl::DestroyS3fsCurl(); + s3fs_destroy_global_ssl(); + destroy_parser_xml_lock(); + destroy_basename_lock(); + exit(EXIT_FAILURE); + } + + // set fake free disk space + if(-1 != fake_diskfree_size){ + FdManager::InitFakeUsedDiskSize(fake_diskfree_size); + } + + // Set default value of free_space_ratio to 10% + if(FdManager::GetEnsureFreeDiskSpace()==0){ + int ratio = 10; + off_t dfsize = FdManager::GetTotalDiskSpaceByRatio(ratio); + S3FS_PRN_INFO("Free space ratio default to %d %%, ensure the available disk space is greater than %.3f MB", ratio, static_cast(dfsize) / 1024 / 1024); + + if(dfsize < S3fsCurl::GetMultipartSize()){ + S3FS_PRN_WARN("specified size to ensure disk free space is smaller than multipart size, so set multipart size to it."); + dfsize = S3fsCurl::GetMultipartSize(); + } + FdManager::SetEnsureFreeDiskSpace(dfsize); + } + + // set user agent + S3fsCurl::InitUserAgent(); + + // There's room for more command line error checking + + // Check to see if the bucket name contains periods and https (SSL) is + // being used. This is a known limitation: + // https://docs.amazonwebservices.com/AmazonS3/latest/dev/ + // The Developers Guide suggests that either use HTTP of for us to write + // our own certificate verification logic. + // For now, this will be unsupported unless we get a request for it to + // be supported. In that case, we have a couple of options: + // - implement a command line option that bypasses the verify host + // but doesn't bypass verifying the certificate + // - write our own host verification (this might be complex) + // See issue #128strncasecmp + /* + if(1 == S3fsCurl::GetSslVerifyHostname()){ + found = S3fsCred::GetBucket().find_first_of('.'); + if(found != std::string::npos){ + found = s3host.find("https:"); + if(found != std::string::npos){ + S3FS_PRN_EXIT("Using https and a bucket name with periods is unsupported."); + S3fsCurl::DestroyS3fsCurl(); + s3fs_destroy_global_ssl(); + destroy_parser_xml_lock(); + exit(EXIT_FAILURE); + } + } + } + */ + + if(utility_incomp_type::NO_UTILITY_MODE != utility_mode){ + int exitcode = s3fs_utility_processing(incomp_abort_time); + + S3fsCurl::DestroyS3fsCurl(); + s3fs_destroy_global_ssl(); + destroy_parser_xml_lock(); + destroy_basename_lock(); + exit(exitcode); + } + + // Check multipart / copy api for mix multipart uploading + if(nomultipart || nocopyapi || norenameapi){ + FdEntity::SetNoMixMultipart(); + max_dirty_data = -1; + } + + // check free disk space + if(!FdManager::IsSafeDiskSpace(nullptr, S3fsCurl::GetMultipartSize() * S3fsCurl::GetMaxParallelCount())){ + // clean cache dir and retry + S3FS_PRN_WARN("No enough disk space for s3fs, try to clean cache dir"); + FdManager::get()->CleanupCacheDir(); + + if(!FdManager::IsSafeDiskSpaceWithLog(nullptr, S3fsCurl::GetMultipartSize() * S3fsCurl::GetMaxParallelCount())){ + S3fsCurl::DestroyS3fsCurl(); + s3fs_destroy_global_ssl(); + destroy_parser_xml_lock(); + destroy_basename_lock(); + exit(EXIT_FAILURE); + } + } + + // set mp stat flag object + // + pHasMpStat = new MpStatFlag(); + + s3fs_oper.getattr = s3fs_getattr; // stat() + s3fs_oper.readlink = s3fs_readlink; + s3fs_oper.mknod = s3fs_mknod; + s3fs_oper.mkdir = s3fs_mkdir; + s3fs_oper.unlink = s3fs_unlink; + s3fs_oper.rmdir = s3fs_rmdir; + s3fs_oper.symlink = s3fs_symlink; + s3fs_oper.rename = s3fs_rename; + s3fs_oper.link = s3fs_link; + if(!nocopyapi){ + s3fs_oper.chmod = s3fs_chmod; + s3fs_oper.chown = s3fs_chown; + s3fs_oper.utimens = s3fs_utimens; + }else{ + s3fs_oper.chmod = s3fs_chmod_nocopy; + s3fs_oper.chown = s3fs_chown_nocopy; + s3fs_oper.utimens = s3fs_utimens_nocopy; + } + s3fs_oper.truncate = s3fs_truncate; + s3fs_oper.open = s3fs_open; + s3fs_oper.read = s3fs_read; + s3fs_oper.write = s3fs_write; + s3fs_oper.statfs = s3fs_statfs; + s3fs_oper.flush = s3fs_flush; + s3fs_oper.fsync = s3fs_fsync; + s3fs_oper.release = s3fs_release; + s3fs_oper.opendir = s3fs_opendir; + s3fs_oper.readdir = s3fs_readdir; // list + s3fs_oper.init = s3fs_init; + s3fs_oper.destroy = s3fs_destroy; + s3fs_oper.access = s3fs_access; + s3fs_oper.create = s3fs_create; + // extended attributes + if(is_use_xattr){ + s3fs_oper.setxattr = s3fs_setxattr; + s3fs_oper.getxattr = s3fs_getxattr; + s3fs_oper.listxattr = s3fs_listxattr; + s3fs_oper.removexattr = s3fs_removexattr; + } + s3fs_oper.flag_utime_omit_ok = true; + + if(use_newcache){ + HybridCache::HybridCacheConfig cfg; + HybridCache::GetHybridCacheConfig(newcache_conf, cfg); + accessor = std::make_shared(cfg); + } + + // now passing things off to fuse, fuse will finish evaluating the command line args + fuse_res = fuse_main(custom_args.argc, custom_args.argv, &s3fs_oper, nullptr); + if(fuse_res == 0){ + fuse_res = s3fs_init_deferred_exit_status; + } + fuse_opt_free_args(&custom_args); + + // Destroy curl + if(!S3fsCurl::DestroyS3fsCurl()){ + S3FS_PRN_WARN("Could not release curl library."); + } + s3fs_destroy_global_ssl(); + destroy_parser_xml_lock(); + destroy_basename_lock(); + delete pHasMpStat; + + // cleanup xml2 + xmlCleanupParser(); + S3FS_MALLOCTRIM(0); + + if(use_newcache){ + accessor.reset(); + } + + exit(fuse_res); +} + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/s3fs.h b/s3fs/s3fs.h new file mode 100644 index 0000000..29c84f4 --- /dev/null +++ b/s3fs/s3fs.h @@ -0,0 +1,92 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef S3FS_S3FS_H_ +#define S3FS_S3FS_H_ + +#define FUSE_USE_VERSION 26 + +#include + +#define S3FS_FUSE_EXIT() \ + do{ \ + struct fuse_context* pcxt = fuse_get_context(); \ + if(pcxt){ \ + fuse_exit(pcxt->fuse); \ + } \ + }while(0) + +// [NOTE] +// s3fs use many small allocated chunk in heap area for stats +// cache and parsing xml, etc. The OS may decide that giving +// this little memory back to the kernel will cause too much +// overhead and delay the operation. +// Address of gratitude, this workaround quotes a document of +// libxml2.( http://xmlsoft.org/xmlmem.html ) +// +// When valgrind is used to test memory leak of s3fs, a large +// amount of chunk may be reported. You can check the memory +// release accurately by defining the S3FS_MALLOC_TRIM flag +// and building it. Also, when executing s3fs, you can define +// the MMAP_THRESHOLD environment variable and check more +// accurate memory leak.( see, man 3 free ) +// +#ifdef S3FS_MALLOC_TRIM +#ifdef HAVE_MALLOC_TRIM +#include +#define S3FS_MALLOCTRIM(pad) malloc_trim(pad) +#else // HAVE_MALLOC_TRIM +#define S3FS_MALLOCTRIM(pad) +#endif // HAVE_MALLOC_TRIM +#else // S3FS_MALLOC_TRIM +#define S3FS_MALLOCTRIM(pad) +#endif // S3FS_MALLOC_TRIM + +#define S3FS_XMLFREEDOC(doc) \ + do{ \ + xmlFreeDoc(doc); \ + S3FS_MALLOCTRIM(0); \ + }while(0) +#define S3FS_XMLFREE(ptr) \ + do{ \ + xmlFree(ptr); \ + S3FS_MALLOCTRIM(0); \ + }while(0) +#define S3FS_XMLXPATHFREECONTEXT(ctx) \ + do{ \ + xmlXPathFreeContext(ctx); \ + S3FS_MALLOCTRIM(0); \ + }while(0) +#define S3FS_XMLXPATHFREEOBJECT(obj) \ + do{ \ + xmlXPathFreeObject(obj); \ + S3FS_MALLOCTRIM(0); \ + }while(0) + +#endif // S3FS_S3FS_H_ + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/s3fs_auth.h b/s3fs/s3fs_auth.h new file mode 100644 index 0000000..6b373fc --- /dev/null +++ b/s3fs/s3fs_auth.h @@ -0,0 +1,66 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef S3FS_AUTH_H_ +#define S3FS_AUTH_H_ + +#include +#include +#include +#include + +typedef std::array md5_t; +typedef std::array sha256_t; + +//------------------------------------------------------------------- +// Utility functions for Authentication +//------------------------------------------------------------------- +// +// in common_auth.cpp +// +std::string s3fs_get_content_md5(int fd); +std::string s3fs_sha256_hex_fd(int fd, off_t start, off_t size); +std::string s3fs_get_content_md5(off_t fsize, char* buf); + +// +// in xxxxxx_auth.cpp +// +const char* s3fs_crypt_lib_name(); +bool s3fs_init_global_ssl(); +bool s3fs_destroy_global_ssl(); +bool s3fs_init_crypt_mutex(); +bool s3fs_destroy_crypt_mutex(); +std::unique_ptr s3fs_HMAC(const void* key, size_t keylen, const unsigned char* data, size_t datalen, unsigned int* digestlen); +std::unique_ptr s3fs_HMAC256(const void* key, size_t keylen, const unsigned char* data, size_t datalen, unsigned int* digestlen); +bool s3fs_md5(const unsigned char* data, size_t datalen, md5_t* result); +bool s3fs_md5_fd(int fd, off_t start, off_t size, md5_t* result); +bool s3fs_sha256(const unsigned char* data, size_t datalen, sha256_t* digest); +bool s3fs_sha256_fd(int fd, off_t start, off_t size, sha256_t* result); + +#endif // S3FS_AUTH_H_ + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/s3fs_cred.cpp b/s3fs/s3fs_cred.cpp new file mode 100644 index 0000000..bce0c95 --- /dev/null +++ b/s3fs/s3fs_cred.cpp @@ -0,0 +1,1628 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.h" +#include "s3fs_cred.h" +#include "s3fs_help.h" +#include "s3fs_logger.h" +#include "curl.h" +#include "string_util.h" +#include "metaheader.h" + +//------------------------------------------------------------------- +// Symbols +//------------------------------------------------------------------- +static constexpr char DEFAULT_AWS_PROFILE_NAME[] = "default"; + +//------------------------------------------------------------------- +// External Credential dummy function +//------------------------------------------------------------------- +// [NOTE] +// This function expects the following values: +// +// detail=false ex. "Custom AWS Credential Library - v1.0.0" +// detail=true ex. "Custom AWS Credential Library - v1.0.0 +// s3fs-fuse credential I/F library for S3 compatible strage X. +// Copyright(C) 2022 Foo" +// +const char* VersionS3fsCredential(bool detail) +{ + static constexpr char version[] = "built-in"; + static constexpr char detail_version[] = + "s3fs-fuse built-in Credential I/F Function\n" + "Copyright(C) 2007 s3fs-fuse\n"; + + if(detail){ + return detail_version; + }else{ + return version; + } +} + +bool InitS3fsCredential(const char* popts, char** pperrstr) +{ + if(popts && 0 < strlen(popts)){ + S3FS_PRN_WARN("The external credential library does not have InitS3fsCredential function, but credlib_opts value is not empty(%s)", popts); + } + if(pperrstr){ + *pperrstr = strdup("The external credential library does not have InitS3fsCredential function, so built-in function was called."); + }else{ + S3FS_PRN_INFO("The external credential library does not have InitS3fsCredential function, so built-in function was called."); + } + return true; +} + +bool FreeS3fsCredential(char** pperrstr) +{ + if(pperrstr){ + *pperrstr = strdup("The external credential library does not have FreeS3fsCredential function, so built-in function was called."); + }else{ + S3FS_PRN_INFO("The external credential library does not have FreeS3fsCredential function, so built-in function was called."); + } + return true; +} + +bool UpdateS3fsCredential(char** ppaccess_key_id, char** ppserect_access_key, char** ppaccess_token, long long* ptoken_expire, char** pperrstr) +{ + S3FS_PRN_INFO("Parameters : ppaccess_key_id=%p, ppserect_access_key=%p, ppaccess_token=%p, ptoken_expire=%p", ppaccess_key_id, ppserect_access_key, ppaccess_token, ptoken_expire); + + if(pperrstr){ + *pperrstr = strdup("Check why built-in function was called, the external credential library must have UpdateS3fsCredential function."); + }else{ + S3FS_PRN_CRIT("Check why built-in function was called, the external credential library must have UpdateS3fsCredential function."); + } + + if(ppaccess_key_id){ + *ppaccess_key_id = nullptr; + } + if(ppserect_access_key){ + *ppserect_access_key = nullptr; + } + if(ppaccess_token){ + *ppaccess_token = nullptr; + } + return false; // always false +} + +//------------------------------------------------------------------- +// Class Variables +//------------------------------------------------------------------- +constexpr char S3fsCred::ALLBUCKET_FIELDS_TYPE[]; +constexpr char S3fsCred::KEYVAL_FIELDS_TYPE[]; +constexpr char S3fsCred::AWS_ACCESSKEYID[]; +constexpr char S3fsCred::AWS_SECRETKEY[]; + +constexpr char S3fsCred::ECS_IAM_ENV_VAR[]; +constexpr char S3fsCred::IAMCRED_ACCESSKEYID[]; +constexpr char S3fsCred::IAMCRED_SECRETACCESSKEY[]; +constexpr char S3fsCred::IAMCRED_ROLEARN[]; + +constexpr char S3fsCred::IAMv2_token_url[]; +constexpr char S3fsCred::IAMv2_token_ttl_hdr[]; +constexpr char S3fsCred::IAMv2_token_hdr[]; + +std::string S3fsCred::bucket_name; + +//------------------------------------------------------------------- +// Class Methods +//------------------------------------------------------------------- +bool S3fsCred::SetBucket(const char* bucket) +{ + if(!bucket || strlen(bucket) == 0){ + return false; + } + S3fsCred::bucket_name = bucket; + return true; +} + +const std::string& S3fsCred::GetBucket() +{ + return S3fsCred::bucket_name; +} + +bool S3fsCred::ParseIAMRoleFromMetaDataResponse(const char* response, std::string& rolename) +{ + if(!response){ + return false; + } + // [NOTE] + // expected following strings. + // + // myrolename + // + std::istringstream ssrole(response); + std::string oneline; + if (getline(ssrole, oneline, '\n')){ + rolename = oneline; + return !rolename.empty(); + } + return false; +} + +//------------------------------------------------------------------- +// Methods : Constructor / Destructor +//------------------------------------------------------------------- +S3fsCred::S3fsCred() : + is_lock_init(false), + aws_profile(DEFAULT_AWS_PROFILE_NAME), + load_iamrole(false), + AWSAccessTokenExpire(0), + is_ecs(false), + is_use_session_token(false), + is_ibm_iam_auth(false), + IAM_cred_url("http://169.254.169.254/latest/meta-data/iam/security-credentials/"), + IAM_api_version(2), + IAM_field_count(4), + IAM_token_field("Token"), + IAM_expiry_field("Expiration"), + set_builtin_cred_opts(false), + hExtCredLib(nullptr), + pFuncCredVersion(VersionS3fsCredential), + pFuncCredInit(InitS3fsCredential), + pFuncCredFree(FreeS3fsCredential), + pFuncCredUpdate(UpdateS3fsCredential) +{ + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); +#if S3FS_PTHREAD_ERRORCHECK + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK); +#endif + int result; + if(0 != (result = pthread_mutex_init(&token_lock, &attr))){ + S3FS_PRN_CRIT("failed to init token_lock: %d", result); + abort(); + } + is_lock_init = true; +} + +S3fsCred::~S3fsCred() +{ + UnloadExtCredLib(); + + if(is_lock_init){ + int result; + if(0 != (result = pthread_mutex_destroy(&token_lock))){ + S3FS_PRN_CRIT("failed to destroy token_lock: %d", result); + abort(); + } + is_lock_init = false; + } +} + +//------------------------------------------------------------------- +// Methods : Access member variables +//------------------------------------------------------------------- +bool S3fsCred::SetS3fsPasswdFile(const char* file) +{ + if(!file || strlen(file) == 0){ + return false; + } + passwd_file = file; + + return true; +} + +bool S3fsCred::IsSetPasswdFile() const +{ + return !passwd_file.empty(); +} + +bool S3fsCred::SetAwsProfileName(const char* name) +{ + if(!name || strlen(name) == 0){ + return false; + } + aws_profile = name; + + return true; +} + +bool S3fsCred::SetIAMRoleMetadataType(bool flag) +{ + bool old = load_iamrole; + load_iamrole = flag; + return old; +} + +bool S3fsCred::SetAccessKey(const char* AccessKeyId, const char* SecretAccessKey, AutoLock::Type type) +{ + AutoLock auto_lock(&token_lock, type); + + if((!is_ibm_iam_auth && (!AccessKeyId || '\0' == AccessKeyId[0])) || !SecretAccessKey || '\0' == SecretAccessKey[0]){ + return false; + } + AWSAccessKeyId = AccessKeyId; + AWSSecretAccessKey = SecretAccessKey; + + return true; +} + +bool S3fsCred::SetAccessKeyWithSessionToken(const char* AccessKeyId, const char* SecretAccessKey, const char * SessionToken, AutoLock::Type type) +{ + AutoLock auto_lock(&token_lock, type); + + bool access_key_is_empty = !AccessKeyId || '\0' == AccessKeyId[0]; + bool secret_access_key_is_empty = !SecretAccessKey || '\0' == SecretAccessKey[0]; + bool session_token_is_empty = !SessionToken || '\0' == SessionToken[0]; + + if((!is_ibm_iam_auth && access_key_is_empty) || secret_access_key_is_empty || session_token_is_empty){ + return false; + } + AWSAccessKeyId = AccessKeyId; + AWSSecretAccessKey = SecretAccessKey; + AWSAccessToken = SessionToken; + is_use_session_token= true; + + return true; +} + +bool S3fsCred::IsSetAccessKeys(AutoLock::Type type) const +{ + AutoLock auto_lock(&token_lock, type); + + return IsSetIAMRole(AutoLock::ALREADY_LOCKED) || ((!AWSAccessKeyId.empty() || is_ibm_iam_auth) && !AWSSecretAccessKey.empty()); +} + +bool S3fsCred::SetIsECS(bool flag) +{ + bool old = is_ecs; + is_ecs = flag; + return old; +} + +bool S3fsCred::SetIsUseSessionToken(bool flag) +{ + bool old = is_use_session_token; + is_use_session_token = flag; + return old; +} + +bool S3fsCred::SetIsIBMIAMAuth(bool flag) +{ + bool old = is_ibm_iam_auth; + is_ibm_iam_auth = flag; + return old; +} + +bool S3fsCred::SetIAMRole(const char* role, AutoLock::Type type) +{ + AutoLock auto_lock(&token_lock, type); + + IAM_role = role ? role : ""; + return true; +} + +std::string S3fsCred::GetIAMRole(AutoLock::Type type) const +{ + AutoLock auto_lock(&token_lock, type); + + return IAM_role; +} + +bool S3fsCred::IsSetIAMRole(AutoLock::Type type) const +{ + AutoLock auto_lock(&token_lock, type); + + return !IAM_role.empty(); +} + +size_t S3fsCred::SetIAMFieldCount(size_t field_count) +{ + size_t old = IAM_field_count; + IAM_field_count = field_count; + return old; +} + +std::string S3fsCred::SetIAMCredentialsURL(const char* url) +{ + std::string old = IAM_cred_url; + IAM_cred_url = url ? url : ""; + return old; +} + +std::string S3fsCred::SetIAMTokenField(const char* token_field) +{ + std::string old = IAM_token_field; + IAM_token_field = token_field ? token_field : ""; + return old; +} + +std::string S3fsCred::SetIAMExpiryField(const char* expiry_field) +{ + std::string old = IAM_expiry_field; + IAM_expiry_field = expiry_field ? expiry_field : ""; + return old; +} + +bool S3fsCred::GetIAMCredentialsURL(std::string& url, bool check_iam_role, AutoLock::Type type) +{ + // check + if(check_iam_role && !is_ecs && !IsIBMIAMAuth()){ + if(!IsSetIAMRole(type)) { + S3FS_PRN_ERR("IAM role name is empty."); + return false; + } + S3FS_PRN_INFO3("[IAM role=%s]", GetIAMRole(type).c_str()); + } + + if(is_ecs){ + const char *env = std::getenv(S3fsCred::ECS_IAM_ENV_VAR); + if(env == nullptr){ + S3FS_PRN_ERR("%s is not set.", S3fsCred::ECS_IAM_ENV_VAR); + return false; + } + url = IAM_cred_url + env; + + }else if(IsIBMIAMAuth()){ + url = IAM_cred_url; + + }else{ + // [NOTE] + // To avoid deadlocking, do not manipulate the S3fsCred object + // in the S3fsCurl::GetIAMv2ApiToken method (when retrying). + // + AutoLock auto_lock(&token_lock, type); // Lock for IAM_api_version, IAMv2_api_token + + if(GetIMDSVersion(AutoLock::ALREADY_LOCKED) > 1){ + S3fsCurl s3fscurl; + std::string token; + int result = s3fscurl.GetIAMv2ApiToken(S3fsCred::IAMv2_token_url, S3fsCred::IAMv2_token_ttl, S3fsCred::IAMv2_token_ttl_hdr, token); + if(-ENOENT == result){ + // If we get a 404 back when requesting the token service, + // then it's highly likely we're running in an environment + // that doesn't support the AWS IMDSv2 API, so we'll skip + // the token retrieval in the future. + SetIMDSVersion(1, AutoLock::ALREADY_LOCKED); + + }else if(result != 0){ + // If we get an unexpected error when retrieving the API + // token, log it but continue. Requirement for including + // an API token with the metadata request may or may not + // be required, so we should not abort here. + S3FS_PRN_ERR("AWS IMDSv2 token retrieval failed: %d", result); + + }else{ + // Set token + if(!SetIAMv2APIToken(token, AutoLock::ALREADY_LOCKED)){ + S3FS_PRN_ERR("Error storing IMDSv2 API token(%s).", token.c_str()); + } + } + } + if(check_iam_role){ + url = IAM_cred_url + GetIAMRole(AutoLock::ALREADY_LOCKED); + }else{ + url = IAM_cred_url; + } + } + return true; +} + +int S3fsCred::SetIMDSVersion(int version, AutoLock::Type type) +{ + AutoLock auto_lock(&token_lock, type); + + int old = IAM_api_version; + IAM_api_version = version; + return old; +} + +int S3fsCred::GetIMDSVersion(AutoLock::Type type) const +{ + AutoLock auto_lock(&token_lock, type); + + return IAM_api_version; +} + +bool S3fsCred::SetIAMv2APIToken(const std::string& token, AutoLock::Type type) +{ + S3FS_PRN_INFO3("Setting AWS IMDSv2 API token to %s", token.c_str()); + + AutoLock auto_lock(&token_lock, type); + + if(token.empty()){ + return false; + } + IAMv2_api_token = token; + return true; +} + +std::string S3fsCred::GetIAMv2APIToken(AutoLock::Type type) const +{ + AutoLock auto_lock(&token_lock, type); + + return IAMv2_api_token; +} + +// [NOTE] +// Currently, token_lock is always locked before calling this method, +// and this method calls the S3fsCurl::GetIAMCredentials method. +// Currently, when the request fails and retries in the process of +// S3fsCurl::GetIAMCredentials, does not use the S3fsCred object in +// retry logic. +// Be careful not to deadlock whenever you change this logic. +// +bool S3fsCred::LoadIAMCredentials(AutoLock::Type type) +{ + // url(check iam role) + std::string url; + + AutoLock auto_lock(&token_lock, type); + + if(!GetIAMCredentialsURL(url, true, AutoLock::ALREADY_LOCKED)){ + return false; + } + + const char* iam_v2_token = nullptr; + std::string str_iam_v2_token; + if(GetIMDSVersion(AutoLock::ALREADY_LOCKED) > 1){ + str_iam_v2_token = GetIAMv2APIToken(AutoLock::ALREADY_LOCKED); + iam_v2_token = str_iam_v2_token.c_str(); + } + + const char* ibm_secret_access_key = nullptr; + std::string str_ibm_secret_access_key; + if(IsIBMIAMAuth()){ + str_ibm_secret_access_key = AWSSecretAccessKey; + ibm_secret_access_key = str_ibm_secret_access_key.c_str(); + } + + S3fsCurl s3fscurl; + std::string response; + if(!s3fscurl.GetIAMCredentials(url.c_str(), iam_v2_token, ibm_secret_access_key, response)){ + return false; + } + + if(!SetIAMCredentials(response.c_str(), AutoLock::ALREADY_LOCKED)){ + S3FS_PRN_ERR("Something error occurred, could not set IAM role name."); + return false; + } + return true; +} + +// +// load IAM role name from http://169.254.169.254/latest/meta-data/iam/security-credentials +// +bool S3fsCred::LoadIAMRoleFromMetaData() +{ + AutoLock auto_lock(&token_lock); + + if(load_iamrole){ + // url(not check iam role) + std::string url; + + if(!GetIAMCredentialsURL(url, false, AutoLock::ALREADY_LOCKED)){ + return false; + } + + const char* iam_v2_token = nullptr; + std::string str_iam_v2_token; + if(GetIMDSVersion(AutoLock::ALREADY_LOCKED) > 1){ + str_iam_v2_token = GetIAMv2APIToken(AutoLock::ALREADY_LOCKED); + iam_v2_token = str_iam_v2_token.c_str(); + } + + S3fsCurl s3fscurl; + std::string token; + if(!s3fscurl.GetIAMRoleFromMetaData(url.c_str(), iam_v2_token, token)){ + return false; + } + + if(!SetIAMRoleFromMetaData(token.c_str(), AutoLock::ALREADY_LOCKED)){ + S3FS_PRN_ERR("Something error occurred, could not set IAM role name."); + return false; + } + S3FS_PRN_INFO("loaded IAM role name = %s", GetIAMRole(AutoLock::ALREADY_LOCKED).c_str()); + } + return true; +} + +bool S3fsCred::SetIAMCredentials(const char* response, AutoLock::Type type) +{ + S3FS_PRN_INFO3("IAM credential response = \"%s\"", response); + + iamcredmap_t keyval; + + if(!ParseIAMCredentialResponse(response, keyval)){ + return false; + } + + if(IAM_field_count != keyval.size()){ + return false; + } + + AutoLock auto_lock(&token_lock, type); + + AWSAccessToken = keyval[IAM_token_field]; + + if(is_ibm_iam_auth){ + off_t tmp_expire = 0; + if(!s3fs_strtoofft(&tmp_expire, keyval[IAM_expiry_field].c_str(), /*base=*/ 10)){ + return false; + } + AWSAccessTokenExpire = static_cast(tmp_expire); + }else{ + AWSAccessKeyId = keyval[S3fsCred::IAMCRED_ACCESSKEYID]; + AWSSecretAccessKey = keyval[S3fsCred::IAMCRED_SECRETACCESSKEY]; + AWSAccessTokenExpire = cvtIAMExpireStringToTime(keyval[IAM_expiry_field].c_str()); + } + return true; +} + +bool S3fsCred::SetIAMRoleFromMetaData(const char* response, AutoLock::Type type) +{ + S3FS_PRN_INFO3("IAM role name response = \"%s\"", response ? response : "(null)"); + + std::string rolename; + if(!S3fsCred::ParseIAMRoleFromMetaDataResponse(response, rolename)){ + return false; + } + + SetIAMRole(rolename.c_str(), type); + return true; +} + +//------------------------------------------------------------------- +// Methods : for Credentials +//------------------------------------------------------------------- +// +// Check passwd file readable +// +bool S3fsCred::IsReadableS3fsPasswdFile() const +{ + if(passwd_file.empty()){ + return false; + } + + std::ifstream PF(passwd_file.c_str()); + if(!PF.good()){ + return false; + } + PF.close(); + + return true; +} + +// +// S3fsCred::CheckS3fsPasswdFilePerms +// +// expect that global passwd_file variable contains +// a non-empty value and is readable by the current user +// +// Check for too permissive access to the file +// help save users from themselves via a security hole +// +// only two options: return or error out +// +bool S3fsCred::CheckS3fsPasswdFilePerms() +{ + struct stat info; + + // let's get the file info + if(stat(passwd_file.c_str(), &info) != 0){ + S3FS_PRN_EXIT("unexpected error from stat(%s): %s", passwd_file.c_str(), strerror(errno)); + return false; + } + + // Check readable + if(!IsReadableS3fsPasswdFile()){ + S3FS_PRN_EXIT("S3fs passwd file \"%s\" is not readable.", passwd_file.c_str()); + return false; + } + + // return error if any file has others permissions + if( (info.st_mode & S_IROTH) || + (info.st_mode & S_IWOTH) || + (info.st_mode & S_IXOTH)) { + S3FS_PRN_EXIT("credentials file %s should not have others permissions.", passwd_file.c_str()); + return false; + } + + // Any local file should not have any group permissions + // /etc/passwd-s3fs can have group permissions + if(passwd_file != "/etc/passwd-s3fs"){ + if( (info.st_mode & S_IRGRP) || + (info.st_mode & S_IWGRP) || + (info.st_mode & S_IXGRP)) { + S3FS_PRN_EXIT("credentials file %s should not have group permissions.", passwd_file.c_str()); + return false; + } + }else{ + // "/etc/passwd-s3fs" does not allow group write. + if((info.st_mode & S_IWGRP)){ + S3FS_PRN_EXIT("credentials file %s should not have group writable permissions.", passwd_file.c_str()); + return false; + } + } + if((info.st_mode & S_IXUSR) || (info.st_mode & S_IXGRP)){ + S3FS_PRN_EXIT("credentials file %s should not have executable permissions.", passwd_file.c_str()); + return false; + } + return true; +} + +// +// Read and Parse passwd file +// +// The line of the password file is one of the following formats: +// (1) "accesskey:secretkey" : AWS format for default(all) access key/secret key +// (2) "bucket:accesskey:secretkey" : AWS format for bucket's access key/secret key +// (3) "key=value" : Content-dependent KeyValue contents +// +// This function sets result into bucketkvmap_t, it bucket name and key&value mapping. +// If bucket name is empty(1 or 3 format), bucket name for mapping is set "\t" or "". +// +// Return: true - Succeed parsing +// false - Should shutdown immediately +// +bool S3fsCred::ParseS3fsPasswdFile(bucketkvmap_t& resmap) +{ + std::string line; + size_t first_pos; + readline_t linelist; + readline_t::iterator iter; + + // open passwd file + std::ifstream PF(passwd_file.c_str()); + if(!PF.good()){ + S3FS_PRN_EXIT("could not open passwd file : %s", passwd_file.c_str()); + return false; + } + + // read each line + while(getline(PF, line)){ + line = trim(line); + if(line.empty()){ + continue; + } + if('#' == line[0]){ + continue; + } + if(std::string::npos != line.find_first_of(" \t")){ + S3FS_PRN_EXIT("invalid line in passwd file, found whitespace character."); + return false; + } + if('[' == line[0]){ + S3FS_PRN_EXIT("invalid line in passwd file, found a bracket \"[\" character."); + return false; + } + linelist.push_back(line); + } + + // read '=' type + kvmap_t kv; + for(iter = linelist.begin(); iter != linelist.end(); ++iter){ + first_pos = iter->find_first_of('='); + if(first_pos == std::string::npos){ + continue; + } + // formatted by "key=val" + std::string key = trim(iter->substr(0, first_pos)); + std::string val = trim(iter->substr(first_pos + 1, std::string::npos)); + if(key.empty()){ + continue; + } + if(kv.end() != kv.find(key)){ + S3FS_PRN_WARN("same key name(%s) found in passwd file, skip this.", key.c_str()); + continue; + } + kv[key] = val; + } + // set special key name + resmap[S3fsCred::KEYVAL_FIELDS_TYPE] = kv; + + // read ':' type + for(iter = linelist.begin(); iter != linelist.end(); ++iter){ + first_pos = iter->find_first_of(':'); + size_t last_pos = iter->find_last_of(':'); + if(first_pos == std::string::npos){ + continue; + } + std::string bucketname; + std::string accesskey; + std::string secret; + if(first_pos != last_pos){ + // formatted by "bucket:accesskey:secretkey" + bucketname= trim(iter->substr(0, first_pos)); + accesskey = trim(iter->substr(first_pos + 1, last_pos - first_pos - 1)); + secret = trim(iter->substr(last_pos + 1, std::string::npos)); + }else{ + // formatted by "accesskey:secretkey" + bucketname= S3fsCred::ALLBUCKET_FIELDS_TYPE; + accesskey = trim(iter->substr(0, first_pos)); + secret = trim(iter->substr(first_pos + 1, std::string::npos)); + } + if(resmap.end() != resmap.find(bucketname)){ + S3FS_PRN_EXIT("there are multiple entries for the same bucket(%s) in the passwd file.", (bucketname.empty() ? "default" : bucketname.c_str())); + return false; + } + kv.clear(); + kv[S3fsCred::AWS_ACCESSKEYID] = accesskey; + kv[S3fsCred::AWS_SECRETKEY] = secret; + resmap[bucketname] = kv; + } + return true; +} + +// +// ReadS3fsPasswdFile +// +// Support for per bucket credentials +// +// Format for the credentials file: +// [bucket:]AccessKeyId:SecretAccessKey +// +// Lines beginning with # are considered comments +// and ignored, as are empty lines +// +// Uncommented lines without the ":" character are flagged as +// an error, so are lines with spaces or tabs +// +// only one default key pair is allowed, but not required +// +bool S3fsCred::ReadS3fsPasswdFile(AutoLock::Type type) +{ + bucketkvmap_t bucketmap; + kvmap_t keyval; + + // if you got here, the password file + // exists and is readable by the + // current user, check for permissions + if(!CheckS3fsPasswdFilePerms()){ + return false; + } + + // + // parse passwd file + // + if(!ParseS3fsPasswdFile(bucketmap)){ + return false; + } + + // + // check key=value type format. + // + bucketkvmap_t::iterator it = bucketmap.find(S3fsCred::KEYVAL_FIELDS_TYPE); + if(bucketmap.end() != it){ + // aws format + std::string access_key_id; + std::string secret_access_key; + int result = CheckS3fsCredentialAwsFormat(it->second, access_key_id, secret_access_key); + if(-1 == result){ + return false; + }else if(1 == result){ + // found ascess(secret) keys + if(!SetAccessKey(access_key_id.c_str(), secret_access_key.c_str(), type)){ + S3FS_PRN_EXIT("failed to set access key/secret key."); + return false; + } + return true; + } + } + + std::string bucket_key = S3fsCred::ALLBUCKET_FIELDS_TYPE; + if(!S3fsCred::bucket_name.empty() && bucketmap.end() != bucketmap.find(S3fsCred::bucket_name)){ + bucket_key = S3fsCred::bucket_name; + } + + it = bucketmap.find(bucket_key); + if(bucketmap.end() == it){ + S3FS_PRN_EXIT("Not found access key/secret key in passwd file."); + return false; + } + keyval = it->second; + kvmap_t::iterator aws_accesskeyid_it = keyval.find(S3fsCred::AWS_ACCESSKEYID); + kvmap_t::iterator aws_secretkey_it = keyval.find(S3fsCred::AWS_SECRETKEY); + if(keyval.end() == aws_accesskeyid_it || keyval.end() == aws_secretkey_it){ + S3FS_PRN_EXIT("Not found access key/secret key in passwd file."); + return false; + } + + if(!SetAccessKey(aws_accesskeyid_it->second.c_str(), aws_secretkey_it->second.c_str(), type)){ + S3FS_PRN_EXIT("failed to set internal data for access key/secret key from passwd file."); + return false; + } + return true; +} + +// +// Return: 1 - OK(could read and set accesskey etc.) +// 0 - NG(could not read) +// -1 - Should shutdown immediately +// +int S3fsCred::CheckS3fsCredentialAwsFormat(const kvmap_t& kvmap, std::string& access_key_id, std::string& secret_access_key) +{ + std::string str1(S3fsCred::AWS_ACCESSKEYID); + std::string str2(S3fsCred::AWS_SECRETKEY); + + if(kvmap.empty()){ + return 0; + } + kvmap_t::const_iterator str1_it = kvmap.find(str1); + kvmap_t::const_iterator str2_it = kvmap.find(str2); + if(kvmap.end() == str1_it && kvmap.end() == str2_it){ + return 0; + } + if(kvmap.end() == str1_it || kvmap.end() == str2_it){ + S3FS_PRN_EXIT("AWSAccesskey or AWSSecretkey is not specified."); + return -1; + } + access_key_id = str1_it->second; + secret_access_key = str2_it->second; + + return 1; +} + +// +// Read Aws Credential File +// +bool S3fsCred::ReadAwsCredentialFile(const std::string &filename, AutoLock::Type type) +{ + // open passwd file + std::ifstream PF(filename.c_str()); + if(!PF.good()){ + return false; + } + + std::string profile; + std::string accesskey; + std::string secret; + std::string session_token; + + // read each line + std::string line; + while(getline(PF, line)){ + line = trim(line); + if(line.empty()){ + continue; + } + if('#' == line[0]){ + continue; + } + + if(line.size() > 2 && line[0] == '[' && line[line.size() - 1] == ']') { + if(profile == aws_profile){ + break; + } + profile = line.substr(1, line.size() - 2); + accesskey.clear(); + secret.clear(); + session_token.clear(); + } + + size_t pos = line.find_first_of('='); + if(pos == std::string::npos){ + continue; + } + std::string key = trim(line.substr(0, pos)); + std::string value = trim(line.substr(pos + 1, std::string::npos)); + if(key == "aws_access_key_id"){ + accesskey = value; + }else if(key == "aws_secret_access_key"){ + secret = value; + }else if(key == "aws_session_token"){ + session_token = value; + } + } + + if(profile != aws_profile){ + return false; + } + if(session_token.empty()){ + if(is_use_session_token){ + S3FS_PRN_EXIT("AWS session token was expected but wasn't provided in aws/credentials file for profile: %s.", aws_profile.c_str()); + return false; + } + if(!SetAccessKey(accesskey.c_str(), secret.c_str(), type)){ + S3FS_PRN_EXIT("failed to set internal data for access key/secret key from aws credential file."); + return false; + } + }else{ + if(!SetAccessKeyWithSessionToken(accesskey.c_str(), secret.c_str(), session_token.c_str(), type)){ + S3FS_PRN_EXIT("session token is invalid."); + return false; + } + } + return true; +} + +// +// InitialS3fsCredentials +// +// called only when were are not mounting a +// public bucket +// +// Here is the order precedence for getting the +// keys: +// +// 1 - from the command line (security risk) +// 2 - from a password file specified on the command line +// 3 - from environment variables +// 3a - from the AWS_CREDENTIAL_FILE environment variable +// 3b - from ${HOME}/.aws/credentials +// 4 - from the users ~/.passwd-s3fs +// 5 - from /etc/passwd-s3fs +// +bool S3fsCred::InitialS3fsCredentials() +{ + // should be redundant + if(S3fsCurl::IsPublicBucket()){ + return true; + } + + // access key loading is deferred + if(load_iamrole || IsSetExtCredLib() || is_ecs){ + return true; + } + + // 1 - keys specified on the command line + if(IsSetAccessKeys(AutoLock::NONE)){ + return true; + } + + // 2 - was specified on the command line + if(IsSetPasswdFile()){ + if(!ReadS3fsPasswdFile(AutoLock::NONE)){ + return false; + } + return true; + } + + // 3 - environment variables + const char* AWSACCESSKEYID = getenv("AWS_ACCESS_KEY_ID") ? getenv("AWS_ACCESS_KEY_ID") : getenv("AWSACCESSKEYID"); + const char* AWSSECRETACCESSKEY = getenv("AWS_SECRET_ACCESS_KEY") ? getenv("AWS_SECRET_ACCESS_KEY") : getenv("AWSSECRETACCESSKEY"); + const char* AWSSESSIONTOKEN = getenv("AWS_SESSION_TOKEN") ? getenv("AWS_SESSION_TOKEN") : getenv("AWSSESSIONTOKEN"); + + if(AWSACCESSKEYID != nullptr || AWSSECRETACCESSKEY != nullptr){ + if( (AWSACCESSKEYID == nullptr && AWSSECRETACCESSKEY != nullptr) || + (AWSACCESSKEYID != nullptr && AWSSECRETACCESSKEY == nullptr) ){ + S3FS_PRN_EXIT("both environment variables AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY must be set together."); + return false; + } + S3FS_PRN_INFO2("access key from env variables"); + if(AWSSESSIONTOKEN != nullptr){ + S3FS_PRN_INFO2("session token is available"); + if(!SetAccessKeyWithSessionToken(AWSACCESSKEYID, AWSSECRETACCESSKEY, AWSSESSIONTOKEN, AutoLock::NONE)){ + S3FS_PRN_EXIT("session token is invalid."); + return false; + } + }else{ + S3FS_PRN_INFO2("session token is not available"); + if(is_use_session_token){ + S3FS_PRN_EXIT("environment variable AWS_SESSION_TOKEN is expected to be set."); + return false; + } + } + if(!SetAccessKey(AWSACCESSKEYID, AWSSECRETACCESSKEY, AutoLock::NONE)){ + S3FS_PRN_EXIT("if one access key is specified, both keys need to be specified."); + return false; + } + return true; + } + + // 3a - from the AWS_CREDENTIAL_FILE environment variable + char* AWS_CREDENTIAL_FILE = getenv("AWS_CREDENTIAL_FILE"); + if(AWS_CREDENTIAL_FILE != nullptr){ + passwd_file = AWS_CREDENTIAL_FILE; + if(IsSetPasswdFile()){ + if(!IsReadableS3fsPasswdFile()){ + S3FS_PRN_EXIT("AWS_CREDENTIAL_FILE: \"%s\" is not readable.", passwd_file.c_str()); + return false; + } + if(!ReadS3fsPasswdFile(AutoLock::NONE)){ + return false; + } + return true; + } + } + + // 3b - check ${HOME}/.aws/credentials + std::string aws_credentials = std::string(getpwuid(getuid())->pw_dir) + "/.aws/credentials"; + if(ReadAwsCredentialFile(aws_credentials, AutoLock::NONE)){ + return true; + }else if(aws_profile != DEFAULT_AWS_PROFILE_NAME){ + S3FS_PRN_EXIT("Could not find profile: %s in file: %s", aws_profile.c_str(), aws_credentials.c_str()); + return false; + } + + // 4 - from the default location in the users home directory + char* HOME = getenv("HOME"); + if(HOME != nullptr){ + passwd_file = HOME; + passwd_file += "/.passwd-s3fs"; + if(IsReadableS3fsPasswdFile()){ + if(!ReadS3fsPasswdFile(AutoLock::NONE)){ + return false; + } + + // It is possible that the user's file was there but + // contained no key pairs i.e. commented out + // in that case, go look in the final location + if(IsSetAccessKeys(AutoLock::NONE)){ + return true; + } + } + } + + // 5 - from the system default location + passwd_file = "/etc/passwd-s3fs"; + if(IsReadableS3fsPasswdFile()){ + if(!ReadS3fsPasswdFile(AutoLock::NONE)){ + return false; + } + return true; + } + + S3FS_PRN_EXIT("could not determine how to establish security credentials."); + return false; +} + +//------------------------------------------------------------------- +// Methods : for IAM +//------------------------------------------------------------------- +bool S3fsCred::ParseIAMCredentialResponse(const char* response, iamcredmap_t& keyval) +{ + if(!response){ + return false; + } + std::istringstream sscred(response); + std::string oneline; + keyval.clear(); + while(getline(sscred, oneline, ',')){ + std::string::size_type pos; + std::string key; + std::string val; + if(std::string::npos != (pos = oneline.find(S3fsCred::IAMCRED_ACCESSKEYID))){ + key = S3fsCred::IAMCRED_ACCESSKEYID; + }else if(std::string::npos != (pos = oneline.find(S3fsCred::IAMCRED_SECRETACCESSKEY))){ + key = S3fsCred::IAMCRED_SECRETACCESSKEY; + }else if(std::string::npos != (pos = oneline.find(IAM_token_field))){ + key = IAM_token_field; + }else if(std::string::npos != (pos = oneline.find(IAM_expiry_field))){ + key = IAM_expiry_field; + }else if(std::string::npos != (pos = oneline.find(S3fsCred::IAMCRED_ROLEARN))){ + key = S3fsCred::IAMCRED_ROLEARN; + }else{ + continue; + } + if(std::string::npos == (pos = oneline.find(':', pos + key.length()))){ + continue; + } + + if(is_ibm_iam_auth && key == IAM_expiry_field){ + // parse integer value + if(std::string::npos == (pos = oneline.find_first_of("0123456789", pos))){ + continue; + } + oneline.erase(0, pos); + if(std::string::npos == (pos = oneline.find_last_of("0123456789"))){ + continue; + } + val = oneline.substr(0, pos+1); + }else{ + // parse std::string value (starts and ends with quotes) + if(std::string::npos == (pos = oneline.find('\"', pos))){ + continue; + } + oneline.erase(0, pos+1); + if(std::string::npos == (pos = oneline.find('\"'))){ + continue; + } + val = oneline.substr(0, pos); + } + keyval[key] = val; + } + return true; +} + +bool S3fsCred::CheckIAMCredentialUpdate(std::string* access_key_id, std::string* secret_access_key, std::string* access_token) +{ + AutoLock auto_lock(&token_lock); + + if(IsIBMIAMAuth() || IsSetExtCredLib() || is_ecs || IsSetIAMRole(AutoLock::ALREADY_LOCKED)){ + if(AWSAccessTokenExpire < (time(nullptr) + S3fsCred::IAM_EXPIRE_MERGIN)){ + S3FS_PRN_INFO("IAM Access Token refreshing..."); + + // update + if(!IsSetExtCredLib()){ + if(!LoadIAMCredentials(AutoLock::ALREADY_LOCKED)){ + S3FS_PRN_ERR("Access Token refresh by built-in failed"); + return false; + } + }else{ + if(!UpdateExtCredentials(AutoLock::ALREADY_LOCKED)){ + S3FS_PRN_ERR("Access Token refresh by %s(external credential library) failed", credlib.c_str()); + return false; + } + } + S3FS_PRN_INFO("IAM Access Token refreshed"); + } + } + + // set + if(access_key_id){ + *access_key_id = AWSAccessKeyId; + } + if(secret_access_key){ + *secret_access_key = AWSSecretAccessKey; + } + if(access_token){ + if(IsIBMIAMAuth() || IsSetExtCredLib() || is_ecs || is_use_session_token || IsSetIAMRole(AutoLock::ALREADY_LOCKED)){ + *access_token = AWSAccessToken; + }else{ + access_token->erase(); + } + } + + return true; +} + +const char* S3fsCred::GetCredFuncVersion(bool detail) const +{ + static constexpr char errVersion[] = "unknown"; + + if(!pFuncCredVersion){ + return errVersion; + } + return (*pFuncCredVersion)(detail); +} + +//------------------------------------------------------------------- +// Methods : External Credential Library +//------------------------------------------------------------------- +bool S3fsCred::SetExtCredLib(const char* arg) +{ + if(!arg || strlen(arg) == 0){ + return false; + } + credlib = arg; + + return true; +} + +bool S3fsCred::IsSetExtCredLib() const +{ + return !credlib.empty(); +} + +bool S3fsCred::SetExtCredLibOpts(const char* args) +{ + if(!args || strlen(args) == 0){ + return false; + } + credlib_opts = args; + + return true; +} + +bool S3fsCred::IsSetExtCredLibOpts() const +{ + return !credlib_opts.empty(); +} + +bool S3fsCred::InitExtCredLib() +{ + if(!LoadExtCredLib()){ + return false; + } + // Initialize library + if(!pFuncCredInit){ + S3FS_PRN_CRIT("\"InitS3fsCredential\" function pointer is nullptr, why?"); + UnloadExtCredLib(); + return false; + } + + const char* popts = credlib_opts.empty() ? nullptr : credlib_opts.c_str(); + char* perrstr = nullptr; + if(!(*pFuncCredInit)(popts, &perrstr)){ + S3FS_PRN_ERR("Could not initialize %s(external credential library) by \"InitS3fsCredential\" function : %s", credlib.c_str(), perrstr ? perrstr : "unknown"); + // cppcheck-suppress unmatchedSuppression + // cppcheck-suppress knownConditionTrueFalse + if(perrstr){ + free(perrstr); + } + UnloadExtCredLib(); + return false; + } + // cppcheck-suppress unmatchedSuppression + // cppcheck-suppress knownConditionTrueFalse + if(perrstr){ + free(perrstr); + } + + return true; +} + +bool S3fsCred::LoadExtCredLib() +{ + if(credlib.empty()){ + return false; + } + UnloadExtCredLib(); + + S3FS_PRN_INFO("Load External Credential Library : %s", credlib.c_str()); + + // Open Library + // + // Search Library: (RPATH ->) LD_LIBRARY_PATH -> (RUNPATH ->) /etc/ld.so.cache -> /lib -> /usr/lib + // + if(nullptr == (hExtCredLib = dlopen(credlib.c_str(), RTLD_LAZY))){ + const char* preason = dlerror(); + S3FS_PRN_ERR("Could not load %s(external credential library) by error : %s", credlib.c_str(), preason ? preason : "unknown"); + return false; + } + + // Set function pointers + if(nullptr == (pFuncCredVersion = reinterpret_cast(dlsym(hExtCredLib, "VersionS3fsCredential")))){ + S3FS_PRN_ERR("%s(external credential library) does not have \"VersionS3fsCredential\" function which is required.", credlib.c_str()); + UnloadExtCredLib(); + return false; + } + if(nullptr == (pFuncCredUpdate = reinterpret_cast(dlsym(hExtCredLib, "UpdateS3fsCredential")))){ + S3FS_PRN_ERR("%s(external credential library) does not have \"UpdateS3fsCredential\" function which is required.", credlib.c_str()); + UnloadExtCredLib(); + return false; + } + if(nullptr == (pFuncCredInit = reinterpret_cast(dlsym(hExtCredLib, "InitS3fsCredential")))){ + S3FS_PRN_INFO("%s(external credential library) does not have \"InitS3fsCredential\" function which is optional.", credlib.c_str()); + pFuncCredInit = InitS3fsCredential; // set built-in function + } + if(nullptr == (pFuncCredFree = reinterpret_cast(dlsym(hExtCredLib, "FreeS3fsCredential")))){ + S3FS_PRN_INFO("%s(external credential library) does not have \"FreeS3fsCredential\" function which is optional.", credlib.c_str()); + pFuncCredFree = FreeS3fsCredential; // set built-in function + } + S3FS_PRN_INFO("Succeed loading External Credential Library : %s", credlib.c_str()); + + return true; +} + +bool S3fsCred::UnloadExtCredLib() +{ + if(hExtCredLib){ + S3FS_PRN_INFO("Unload External Credential Library : %s", credlib.c_str()); + + // Uninitialize library + if(!pFuncCredFree){ + S3FS_PRN_CRIT("\"FreeS3fsCredential\" function pointer is nullptr, why?"); + }else{ + char* perrstr = nullptr; + if(!(*pFuncCredFree)(&perrstr)){ + S3FS_PRN_ERR("Could not uninitialize by \"FreeS3fsCredential\" function : %s", perrstr ? perrstr : "unknown"); + } + // cppcheck-suppress unmatchedSuppression + // cppcheck-suppress knownConditionTrueFalse + if(perrstr){ + free(perrstr); + } + } + + // reset + pFuncCredVersion = VersionS3fsCredential; + pFuncCredInit = InitS3fsCredential; + pFuncCredFree = FreeS3fsCredential; + pFuncCredUpdate = UpdateS3fsCredential; + + // close + dlclose(hExtCredLib); + hExtCredLib = nullptr; + } + return true; +} + +bool S3fsCred::UpdateExtCredentials(AutoLock::Type type) +{ + if(!hExtCredLib){ + S3FS_PRN_CRIT("External Credential Library is not loaded, why?"); + return false; + } + + AutoLock auto_lock(&token_lock, type); + + char* paccess_key_id = nullptr; + char* pserect_access_key = nullptr; + char* paccess_token = nullptr; + char* perrstr = nullptr; + long long token_expire = 0; + + bool result = (*pFuncCredUpdate)(&paccess_key_id, &pserect_access_key, &paccess_token, &token_expire, &perrstr); + if(!result){ + // error occurred + S3FS_PRN_ERR("Could not update credential by \"UpdateS3fsCredential\" function : %s", perrstr ? perrstr : "unknown"); + + // cppcheck-suppress unmatchedSuppression + // cppcheck-suppress knownConditionTrueFalse + }else if(!paccess_key_id || !pserect_access_key || !paccess_token || token_expire <= 0){ + // some variables are wrong + S3FS_PRN_ERR("After updating credential by \"UpdateS3fsCredential\" function, but some variables are wrong : paccess_key_id=%p, pserect_access_key=%p, paccess_token=%p, token_expire=%lld", paccess_key_id, pserect_access_key, paccess_token, token_expire); + result = false; + }else{ + // succeed updating + AWSAccessKeyId = paccess_key_id; + AWSSecretAccessKey = pserect_access_key; + AWSAccessToken = paccess_token; + AWSAccessTokenExpire = token_expire; + } + + // clean + // cppcheck-suppress unmatchedSuppression + // cppcheck-suppress knownConditionTrueFalse + if(paccess_key_id){ + free(paccess_key_id); + } + // cppcheck-suppress unmatchedSuppression + // cppcheck-suppress knownConditionTrueFalse + if(pserect_access_key){ + free(pserect_access_key); + } + // cppcheck-suppress unmatchedSuppression + // cppcheck-suppress knownConditionTrueFalse + if(paccess_token){ + free(paccess_token); + } + // cppcheck-suppress unmatchedSuppression + // cppcheck-suppress knownConditionTrueFalse + if(perrstr){ + free(perrstr); + } + + return result; +} + +//------------------------------------------------------------------- +// Methods: Option detection +//------------------------------------------------------------------- +// return value: 1 = Not processed as it is not a option for this class +// 0 = The option was detected and processed appropriately +// -1 = Processing cannot be continued because a fatal error was detected +// +int S3fsCred::DetectParam(const char* arg) +{ + if(!arg){ + S3FS_PRN_EXIT("parameter arg is empty(null)"); + return -1; + } + + if(is_prefix(arg, "passwd_file=")){ + SetS3fsPasswdFile(strchr(arg, '=') + sizeof(char)); + set_builtin_cred_opts = true; + return 0; + } + + if(0 == strcmp(arg, "ibm_iam_auth")){ + SetIsIBMIAMAuth(true); + SetIAMCredentialsURL("https://iam.cloud.ibm.com/identity/token"); + SetIAMTokenField("\"access_token\""); + SetIAMExpiryField("\"expiration\""); + SetIAMFieldCount(2); + SetIMDSVersion(1, AutoLock::NONE); + set_builtin_cred_opts = true; + return 0; + } + + if(0 == strcmp(arg, "use_session_token")){ + SetIsUseSessionToken(true); + set_builtin_cred_opts = true; + return 0; + } + + if(is_prefix(arg, "ibm_iam_endpoint=")){ + std::string endpoint_url; + const char* iam_endpoint = strchr(arg, '=') + sizeof(char); + + // Check url for http / https protocol std::string + if(!is_prefix(iam_endpoint, "https://") && !is_prefix(iam_endpoint, "http://")){ + S3FS_PRN_EXIT("option ibm_iam_endpoint has invalid format, missing http / https protocol"); + return -1; + } + endpoint_url = std::string(iam_endpoint) + "/identity/token"; + SetIAMCredentialsURL(endpoint_url.c_str()); + set_builtin_cred_opts = true; + return 0; + } + + if(0 == strcmp(arg, "imdsv1only")){ + SetIMDSVersion(1, AutoLock::NONE); + set_builtin_cred_opts = true; + return 0; + } + + if(0 == strcmp(arg, "ecs")){ + if(IsIBMIAMAuth()){ + S3FS_PRN_EXIT("option ecs cannot be used in conjunction with ibm"); + return -1; + } + SetIsECS(true); + SetIMDSVersion(1, AutoLock::NONE); + SetIAMCredentialsURL("http://169.254.170.2"); + SetIAMFieldCount(5); + set_builtin_cred_opts = true; + return 0; + } + + if(is_prefix(arg, "iam_role")){ + if(is_ecs || IsIBMIAMAuth()){ + S3FS_PRN_EXIT("option iam_role cannot be used in conjunction with ecs or ibm"); + return -1; + } + if(0 == strcmp(arg, "iam_role") || 0 == strcmp(arg, "iam_role=auto")){ + // loading IAM role name in s3fs_init(), because we need to wait initializing curl. + // + SetIAMRoleMetadataType(true); + set_builtin_cred_opts = true; + return 0; + + }else if(is_prefix(arg, "iam_role=")){ + const char* role = strchr(arg, '=') + sizeof(char); + SetIAMRole(role, AutoLock::NONE); + SetIAMRoleMetadataType(false); + set_builtin_cred_opts = true; + return 0; + } + } + + if(is_prefix(arg, "profile=")){ + SetAwsProfileName(strchr(arg, '=') + sizeof(char)); + set_builtin_cred_opts = true; + return 0; + } + + if(is_prefix(arg, "credlib=")){ + if(!SetExtCredLib(strchr(arg, '=') + sizeof(char))){ + S3FS_PRN_EXIT("failed to set credlib option : %s", (strchr(arg, '=') + sizeof(char))); + return -1; + } + return 0; + } + + if(is_prefix(arg, "credlib_opts=")){ + if(!SetExtCredLibOpts(strchr(arg, '=') + sizeof(char))){ + S3FS_PRN_EXIT("failed to set credlib_opts option : %s", (strchr(arg, '=') + sizeof(char))); + return -1; + } + return 0; + } + + return 1; +} + +//------------------------------------------------------------------- +// Methods : check parameters +//------------------------------------------------------------------- +// +// Checking forbidden parameters for bucket +// +bool S3fsCred::CheckForbiddenBucketParams() +{ + // The first plain argument is the bucket + if(bucket_name.empty()){ + S3FS_PRN_EXIT("missing BUCKET argument."); + show_usage(); + return false; + } + + // bucket names cannot contain upper case characters in virtual-hosted style + if(!pathrequeststyle && (lower(bucket_name) != bucket_name)){ + S3FS_PRN_EXIT("BUCKET %s, name not compatible with virtual-hosted style.", bucket_name.c_str()); + return false; + } + + // check bucket name for illegal characters + size_t found = bucket_name.find_first_of("/:\\;!@#$%^&*?|+="); + if(found != std::string::npos){ + S3FS_PRN_EXIT("BUCKET %s -- bucket name contains an illegal character: '%c' at position %zu", bucket_name.c_str(), bucket_name[found], found); + return false; + } + + if(!pathrequeststyle && is_prefix(s3host.c_str(), "https://") && bucket_name.find_first_of('.') != std::string::npos) { + S3FS_PRN_EXIT("BUCKET %s -- cannot mount bucket with . while using HTTPS without use_path_request_style", bucket_name.c_str()); + return false; + } + return true; +} + +// +// Check the combination of parameters +// +bool S3fsCred::CheckAllParams() +{ + // + // Checking forbidden parameters for bucket + // + if(!CheckForbiddenBucketParams()){ + return false; + } + + // error checking of command line arguments for compatibility + if(S3fsCurl::IsPublicBucket() && IsSetAccessKeys(AutoLock::NONE)){ + S3FS_PRN_EXIT("specifying both public_bucket and the access keys options is invalid."); + return false; + } + + if(IsSetPasswdFile() && IsSetAccessKeys(AutoLock::NONE)){ + S3FS_PRN_EXIT("specifying both passwd_file and the access keys options is invalid."); + return false; + } + + if(!S3fsCurl::IsPublicBucket() && !load_iamrole && !is_ecs && !IsSetExtCredLib()){ + if(!InitialS3fsCredentials()){ + return false; + } + if(!IsSetAccessKeys(AutoLock::NONE)){ + S3FS_PRN_EXIT("could not establish security credentials, check documentation."); + return false; + } + // More error checking on the access key pair can be done + // like checking for appropriate lengths and characters + } + + // check IBM IAM requirements + if(IsIBMIAMAuth()){ + // check that default ACL is either public-read or private + acl_t defaultACL = S3fsCurl::GetDefaultAcl(); + if(defaultACL != acl_t::PRIVATE && defaultACL != acl_t::PUBLIC_READ){ + S3FS_PRN_EXIT("can only use 'public-read' or 'private' ACL while using ibm_iam_auth"); + return false; + } + } + + // check External Credential Library + // + // [NOTE] + // If credlib(_opts) option (for External Credential Library) is specified, + // no other Credential related options can be specified. It is exclusive. + // + if(set_builtin_cred_opts && (IsSetExtCredLib() || IsSetExtCredLibOpts())){ + S3FS_PRN_EXIT("The \"credlib\" or \"credlib_opts\" option and other credential-related options(passwd_file, iam_role, profile, use_session_token, ecs, imdsv1only, ibm_iam_auth, ibm_iam_endpoint, etc) cannot be specified together."); + return false; + } + + // Load and Initialize external credential library + if(IsSetExtCredLib() || IsSetExtCredLibOpts()){ + if(!IsSetExtCredLib()){ + S3FS_PRN_EXIT("The \"credlib_opts\"(%s) is specifyed but \"credlib\" option is not specified.", credlib_opts.c_str()); + return false; + } + + if(!InitExtCredLib()){ + S3FS_PRN_EXIT("failed to load the library specified by the option credlib(%s, %s).", credlib.c_str(), credlib_opts.c_str()); + return false; + } + S3FS_PRN_INFO("Loaded External Credential Library:\n%s", GetCredFuncVersion(true)); + } + + return true; +} + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/s3fs_cred.h b/s3fs/s3fs_cred.h new file mode 100644 index 0000000..845a1f3 --- /dev/null +++ b/s3fs/s3fs_cred.h @@ -0,0 +1,187 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef S3FS_CRED_H_ +#define S3FS_CRED_H_ + +#include "autolock.h" +#include "s3fs_extcred.h" + +//---------------------------------------------- +// Typedefs +//---------------------------------------------- +typedef std::map iamcredmap_t; + +//------------------------------------------------ +// class S3fsCred +//------------------------------------------------ +// This is a class for operating and managing Credentials(accesskey, +// secret key, tokens, etc.) used by S3fs. +// Operations related to Credentials are aggregated in this class. +// +// cppcheck-suppress ctuOneDefinitionRuleViolation ; for stub in test_curl_util.cpp +class S3fsCred +{ + private: + static constexpr char ALLBUCKET_FIELDS_TYPE[] = ""; // special key for mapping(This name is absolutely not used as a bucket name) + static constexpr char KEYVAL_FIELDS_TYPE[] = "\t"; // special key for mapping(This name is absolutely not used as a bucket name) + static constexpr char AWS_ACCESSKEYID[] = "AWSAccessKeyId"; + static constexpr char AWS_SECRETKEY[] = "AWSSecretKey"; + + static constexpr int IAM_EXPIRE_MERGIN = 20 * 60; // update timing + static constexpr char ECS_IAM_ENV_VAR[] = "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"; + static constexpr char IAMCRED_ACCESSKEYID[] = "AccessKeyId"; + static constexpr char IAMCRED_SECRETACCESSKEY[] = "SecretAccessKey"; + static constexpr char IAMCRED_ROLEARN[] = "RoleArn"; + + static std::string bucket_name; + + mutable pthread_mutex_t token_lock; + bool is_lock_init; + + std::string passwd_file; + std::string aws_profile; + + bool load_iamrole; + + std::string AWSAccessKeyId; // Protect exclusively + std::string AWSSecretAccessKey; // Protect exclusively + std::string AWSAccessToken; // Protect exclusively + time_t AWSAccessTokenExpire; // Protect exclusively + + bool is_ecs; + bool is_use_session_token; + bool is_ibm_iam_auth; + + std::string IAM_cred_url; + int IAM_api_version; // Protect exclusively + std::string IAMv2_api_token; // Protect exclusively + size_t IAM_field_count; + std::string IAM_token_field; + std::string IAM_expiry_field; + std::string IAM_role; // Protect exclusively + + bool set_builtin_cred_opts; // true if options other than "credlib" is set + std::string credlib; // credlib(name or path) + std::string credlib_opts; // options for credlib + + void* hExtCredLib; + fp_VersionS3fsCredential pFuncCredVersion; + fp_InitS3fsCredential pFuncCredInit; + fp_FreeS3fsCredential pFuncCredFree; + fp_UpdateS3fsCredential pFuncCredUpdate; + + public: + static constexpr char IAMv2_token_url[] = "http://169.254.169.254/latest/api/token"; + static constexpr int IAMv2_token_ttl = 21600; + static constexpr char IAMv2_token_ttl_hdr[] = "X-aws-ec2-metadata-token-ttl-seconds"; + static constexpr char IAMv2_token_hdr[] = "X-aws-ec2-metadata-token"; + + private: + static bool ParseIAMRoleFromMetaDataResponse(const char* response, std::string& rolename); + + bool SetS3fsPasswdFile(const char* file); + bool IsSetPasswdFile() const; + bool SetAwsProfileName(const char* profile_name); + bool SetIAMRoleMetadataType(bool flag); + + bool SetAccessKey(const char* AccessKeyId, const char* SecretAccessKey, AutoLock::Type type); + bool SetAccessKeyWithSessionToken(const char* AccessKeyId, const char* SecretAccessKey, const char * SessionToken, AutoLock::Type type); + bool IsSetAccessKeys(AutoLock::Type type) const; + + bool SetIsECS(bool flag); + bool SetIsUseSessionToken(bool flag); + + bool SetIsIBMIAMAuth(bool flag); + + int SetIMDSVersion(int version, AutoLock::Type type); + int GetIMDSVersion(AutoLock::Type type) const; + + bool SetIAMv2APIToken(const std::string& token, AutoLock::Type type); + std::string GetIAMv2APIToken(AutoLock::Type type) const; + + bool SetIAMRole(const char* role, AutoLock::Type type); + std::string GetIAMRole(AutoLock::Type type) const; + bool IsSetIAMRole(AutoLock::Type type) const; + size_t SetIAMFieldCount(size_t field_count); + std::string SetIAMCredentialsURL(const char* url); + std::string SetIAMTokenField(const char* token_field); + std::string SetIAMExpiryField(const char* expiry_field); + + bool IsReadableS3fsPasswdFile() const; + bool CheckS3fsPasswdFilePerms(); + bool ParseS3fsPasswdFile(bucketkvmap_t& resmap); + bool ReadS3fsPasswdFile(AutoLock::Type type); + + static int CheckS3fsCredentialAwsFormat(const kvmap_t& kvmap, std::string& access_key_id, std::string& secret_access_key); + bool ReadAwsCredentialFile(const std::string &filename, AutoLock::Type type); + + bool InitialS3fsCredentials(); + bool ParseIAMCredentialResponse(const char* response, iamcredmap_t& keyval); + + bool GetIAMCredentialsURL(std::string& url, bool check_iam_role, AutoLock::Type type); + bool LoadIAMCredentials(AutoLock::Type type); + bool SetIAMCredentials(const char* response, AutoLock::Type type); + bool SetIAMRoleFromMetaData(const char* response, AutoLock::Type type); + + bool SetExtCredLib(const char* arg); + bool IsSetExtCredLib() const; + bool SetExtCredLibOpts(const char* args); + bool IsSetExtCredLibOpts() const; + + bool InitExtCredLib(); + bool LoadExtCredLib(); + bool UnloadExtCredLib(); + bool UpdateExtCredentials(AutoLock::Type type); + + static bool CheckForbiddenBucketParams(); + + public: + static bool SetBucket(const char* bucket); + static const std::string& GetBucket(); + + S3fsCred(); + ~S3fsCred(); + S3fsCred(const S3fsCred&) = delete; + S3fsCred(S3fsCred&&) = delete; + S3fsCred& operator=(const S3fsCred&) = delete; + S3fsCred& operator=(S3fsCred&&) = delete; + + bool IsIBMIAMAuth() const { return is_ibm_iam_auth; } + + bool LoadIAMRoleFromMetaData(); + + bool CheckIAMCredentialUpdate(std::string* access_key_id = nullptr, std::string* secret_access_key = nullptr, std::string* access_token = nullptr); + const char* GetCredFuncVersion(bool detail) const; + + int DetectParam(const char* arg); + bool CheckAllParams(); +}; + +#endif // S3FS_CRED_H_ + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/s3fs_extcred.h b/s3fs/s3fs_extcred.h new file mode 100644 index 0000000..eb70e80 --- /dev/null +++ b/s3fs/s3fs_extcred.h @@ -0,0 +1,144 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Takeshi Nakatani + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef S3FS_EXTCRED_H_ +#define S3FS_EXTCRED_H_ + +//------------------------------------------------------------------- +// Attributes(weak) : use only in s3fs-fuse internally +//------------------------------------------------------------------- +// [NOTE] +// This macro is only used inside s3fs-fuse. +// External projects that utilize this header file substitute empty +//values as follows: +// +#ifndef S3FS_FUNCATTR_WEAK +#define S3FS_FUNCATTR_WEAK +#endif + +extern "C" { +//------------------------------------------------------------------- +// Prototype for External Credential 4 functions +//------------------------------------------------------------------- +// +// [Required] VersionS3fsCredential +// +// Returns the library name and version as a string. +// +extern const char* VersionS3fsCredential(bool detail) S3FS_FUNCATTR_WEAK; + +// +// [Optional] InitS3fsCredential +// +// A function that does the necessary initialization after the library is +// loaded. This function is called only once immediately after loading the +// library. +// If there is a required initialization inside the library, implement it. +// Implementation of this function is optional and not required. If not +// implemented, it will not be called. +// +// const char* popts : String passed with the credlib_opts option. If the +// credlib_opts option is not specified, nullptr will be +// passed. +// char** pperrstr : pperrstr is used to pass the error message to the +// caller when an error occurs. +// If this pointer is not nullptr, you can allocate memory +// and set an error message to it. The allocated memory +// area is freed by the caller. +// +extern bool InitS3fsCredential(const char* popts, char** pperrstr) S3FS_FUNCATTR_WEAK; + +// +// [Optional] FreeS3fsCredential +// +// A function that is called only once just before the library is unloaded. +// If there is a required discard process in the library, implement it. +// Implementation of this feature is optional and not required. +// If not implemented, it will not be called. +// +// char** pperrstr : pperrstr is used to pass the error message to the +// caller when an error occurs. +// If this pointer is not nullptr, you can allocate memory +// and set an error message to it. The allocated memory +// area is freed by the caller. +// +extern bool FreeS3fsCredential(char** pperrstr) S3FS_FUNCATTR_WEAK; + +// +// [Required] UpdateS3fsCredential +// +// A function that updates the token. +// +// char** ppaccess_key_id : Allocate and set "Access Key ID" string +// area to *ppaccess_key_id. +// char** ppserect_access_key : Allocate and set "Access Secret Key ID" +// string area to *ppserect_access_key. +// char** ppaccess_token : Allocate and set "Token" string area to +// *ppaccess_token. +// long long* ptoken_expire : Set token expire time(time_t) value to +// *ptoken_expire. +// This is essentially a time_t* variable. +// To avoid system differences about time_t +// size, long long* is used. +// When setting the value, cast from time_t +// to long long to set the value. +// char** pperrstr : pperrstr is used to pass the error message to the +// caller when an error occurs. +// +// For all argument of the character string pointer(char **) set the +// allocated string area. The allocated area is freed by the caller. +// +extern bool UpdateS3fsCredential(char** ppaccess_key_id, char** ppserect_access_key, char** ppaccess_token, long long* ptoken_expire, char** pperrstr) S3FS_FUNCATTR_WEAK; + +//--------------------------------------------------------- +// Typedef Prototype function +//--------------------------------------------------------- +// +// const char* VersionS3fsCredential() +// +typedef const char* (*fp_VersionS3fsCredential)(bool detail); + +// +// bool InitS3fsCredential(char** pperrstr) +// +typedef bool (*fp_InitS3fsCredential)(const char* popts, char** pperrstr); + +// +// bool FreeS3fsCredential(char** pperrstr) +// +typedef bool (*fp_FreeS3fsCredential)(char** pperrstr); + +// +// bool UpdateS3fsCredential(char** ppaccess_key_id, char** ppserect_access_key, char** ppaccess_token, long long* ptoken_expire, char** pperrstr) +// +typedef bool (*fp_UpdateS3fsCredential)(char** ppaccess_key_id, char** ppserect_access_key, char** ppaccess_token, long long* ptoken_expire, char** pperrstr); + +} // extern "C" + +#endif // S3FS_EXTCRED_H_ + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/s3fs_global.cpp b/s3fs/s3fs_global.cpp new file mode 100644 index 0000000..650f39e --- /dev/null +++ b/s3fs/s3fs_global.cpp @@ -0,0 +1,50 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Takeshi Nakatani + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include + +#include "hybridcache_accessor_4_s3fs.h" + +//------------------------------------------------------------------- +// Global variables +//------------------------------------------------------------------- +bool foreground = false; +bool nomultipart = false; +bool pathrequeststyle = false; +bool complement_stat = false; +bool noxmlns = false; +bool use_newcache = false; +std::string program_name; +std::string service_path = "/"; +std::string s3host = "https://s3.amazonaws.com"; +std::string endpoint = "us-east-1"; +std::string cipher_suites; +std::string instance_name; +std::shared_ptr accessor; + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/s3fs_help.cpp b/s3fs/s3fs_help.cpp new file mode 100644 index 0000000..469d408 --- /dev/null +++ b/s3fs/s3fs_help.cpp @@ -0,0 +1,657 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Takeshi Nakatani + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include + +#include + +#include "common.h" +#include "s3fs_help.h" +#include "s3fs_auth.h" + +//------------------------------------------------------------------- +// Contents +//------------------------------------------------------------------- +static constexpr char help_string[] = + "\n" + "Mount an Amazon S3 bucket as a file system.\n" + "\n" + "Usage:\n" + " mounting\n" + " s3fs bucket[:/path] mountpoint [options]\n" + " s3fs mountpoint [options (must specify bucket= option)]\n" + "\n" + " unmounting\n" + " umount mountpoint\n" + "\n" + " General forms for s3fs and FUSE/mount options:\n" + " -o opt[,opt...]\n" + " -o opt [-o opt] ...\n" + "\n" + " utility mode (remove interrupted multipart uploading objects)\n" + " s3fs --incomplete-mpu-list (-u) bucket\n" + " s3fs --incomplete-mpu-abort[=all | =] bucket\n" + "\n" + "s3fs Options:\n" + "\n" + " Most s3fs options are given in the form where \"opt\" is:\n" + "\n" + " =\n" + "\n" + " bucket\n" + " - if it is not specified bucket name (and path) in command line,\n" + " must specify this option after -o option for bucket name.\n" + "\n" + " default_acl (default=\"private\")\n" + " - the default canned acl to apply to all written s3 objects,\n" + " e.g., private, public-read. see\n" + " https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl\n" + " for the full list of canned ACLs\n" + "\n" + " retries (default=\"5\")\n" + " - number of times to retry a failed S3 transaction\n" + "\n" + " tmpdir (default=\"/tmp\")\n" + " - local folder for temporary files.\n" + "\n" + " use_cache (default=\"\" which means disabled)\n" + " - local folder to use for local file cache\n" + "\n" + " check_cache_dir_exist (default is disable)\n" + " - if use_cache is set, check if the cache directory exists.\n" + " If this option is not specified, it will be created at runtime\n" + " when the cache directory does not exist.\n" + "\n" + " del_cache (delete local file cache)\n" + " - delete local file cache when s3fs starts and exits.\n" + "\n" + " storage_class (default=\"standard\")\n" + " - store object with specified storage class. Possible values:\n" + " standard, standard_ia, onezone_ia, reduced_redundancy,\n" + " intelligent_tiering, glacier, glacier_ir, and deep_archive.\n" + "\n" + " use_rrs (default is disable)\n" + " - use Amazon's Reduced Redundancy Storage.\n" + " this option can not be specified with use_sse.\n" + " (can specify use_rrs=1 for old version)\n" + " this option has been replaced by new storage_class option.\n" + "\n" + " use_sse (default is disable)\n" + " - Specify three type Amazon's Server-Site Encryption: SSE-S3,\n" + " SSE-C or SSE-KMS. SSE-S3 uses Amazon S3-managed encryption\n" + " keys, SSE-C uses customer-provided encryption keys, and\n" + " SSE-KMS uses the master key which you manage in AWS KMS.\n" + " You can specify \"use_sse\" or \"use_sse=1\" enables SSE-S3\n" + " type (use_sse=1 is old type parameter).\n" + " Case of setting SSE-C, you can specify \"use_sse=custom\",\n" + " \"use_sse=custom:\" or\n" + " \"use_sse=\" (only \n" + " specified is old type parameter). You can use \"c\" for\n" + " short \"custom\".\n" + " The custom key file must be 600 permission. The file can\n" + " have some lines, each line is one SSE-C key. The first line\n" + " in file is used as Customer-Provided Encryption Keys for\n" + " uploading and changing headers etc. If there are some keys\n" + " after first line, those are used downloading object which\n" + " are encrypted by not first key. So that, you can keep all\n" + " SSE-C keys in file, that is SSE-C key history.\n" + " If you specify \"custom\" (\"c\") without file path, you\n" + " need to set custom key by load_sse_c option or AWSSSECKEYS\n" + " environment. (AWSSSECKEYS environment has some SSE-C keys\n" + " with \":\" separator.) This option is used to decide the\n" + " SSE type. So that if you do not want to encrypt a object\n" + " object at uploading, but you need to decrypt encrypted\n" + " object at downloading, you can use load_sse_c option instead\n" + " of this option.\n" + " For setting SSE-KMS, specify \"use_sse=kmsid\" or\n" + " \"use_sse=kmsid:\". You can use \"k\" for short \"kmsid\".\n" + " If you san specify SSE-KMS type with your in AWS\n" + " KMS, you can set it after \"kmsid:\" (or \"k:\"). If you\n" + " specify only \"kmsid\" (\"k\"), you need to set AWSSSEKMSID\n" + " environment which value is . You must be careful\n" + " about that you can not use the KMS id which is not same EC2\n" + " region.\n" + " Additionally, if you specify SSE-KMS, your endpoints must use\n" + " Secure Sockets Layer(SSL) or Transport Layer Security(TLS).\n" + "\n" + " load_sse_c - specify SSE-C keys\n" + " Specify the custom-provided encryption keys file path for decrypting\n" + " at downloading.\n" + " If you use the custom-provided encryption key at uploading, you\n" + " specify with \"use_sse=custom\". The file has many lines, one line\n" + " means one custom key. So that you can keep all SSE-C keys in file,\n" + " that is SSE-C key history. AWSSSECKEYS environment is as same as this\n" + " file contents.\n" + "\n" + " public_bucket (default=\"\" which means disabled)\n" + " - anonymously mount a public bucket when set to 1, ignores the \n" + " $HOME/.passwd-s3fs and /etc/passwd-s3fs files.\n" + " S3 does not allow copy object api for anonymous users, then\n" + " s3fs sets nocopyapi option automatically when public_bucket=1\n" + " option is specified.\n" + "\n" + " passwd_file (default=\"\")\n" + " - specify which s3fs password file to use\n" + "\n" + " ahbe_conf (default=\"\" which means disabled)\n" + " - This option specifies the configuration file path which\n" + " file is the additional HTTP header by file (object) extension.\n" + " The configuration file format is below:\n" + " -----------\n" + " line = [file suffix or regex] HTTP-header [HTTP-values]\n" + " file suffix = file (object) suffix, if this field is empty,\n" + " it means \"reg:(.*)\".(=all object).\n" + " regex = regular expression to match the file (object) path.\n" + " this type starts with \"reg:\" prefix.\n" + " HTTP-header = additional HTTP header name\n" + " HTTP-values = additional HTTP header value\n" + " -----------\n" + " Sample:\n" + " -----------\n" + " .gz Content-Encoding gzip\n" + " .Z Content-Encoding compress\n" + " reg:^/MYDIR/(.*)[.]t2$ Content-Encoding text2\n" + " -----------\n" + " A sample configuration file is uploaded in \"test\" directory.\n" + " If you specify this option for set \"Content-Encoding\" HTTP \n" + " header, please take care for RFC 2616.\n" + "\n" + " profile (default=\"default\")\n" + " - Choose a profile from ${HOME}/.aws/credentials to authenticate\n" + " against S3. Note that this format matches the AWS CLI format and\n" + " differs from the s3fs passwd format.\n" + "\n" + " connect_timeout (default=\"300\" seconds)\n" + " - time to wait for connection before giving up\n" + "\n" + " readwrite_timeout (default=\"120\" seconds)\n" + " - time to wait between read/write activity before giving up\n" + "\n" + " list_object_max_keys (default=\"1000\")\n" + " - specify the maximum number of keys returned by S3 list object\n" + " API. The default is 1000. you can set this value to 1000 or more.\n" + "\n" + " max_stat_cache_size (default=\"100,000\" entries (about 40MB))\n" + " - maximum number of entries in the stat cache, and this maximum is\n" + " also treated as the number of symbolic link cache.\n" + "\n" + " stat_cache_expire (default is 900))\n" + " - specify expire time (seconds) for entries in the stat cache.\n" + " This expire time indicates the time since stat cached. and this\n" + " is also set to the expire time of the symbolic link cache.\n" + "\n" + " stat_cache_interval_expire (default is 900)\n" + " - specify expire time (seconds) for entries in the stat cache(and\n" + " symbolic link cache).\n" + " This expire time is based on the time from the last access time\n" + " of the stat cache. This option is exclusive with stat_cache_expire,\n" + " and is left for compatibility with older versions.\n" + "\n" + " disable_noobj_cache (default is enable)\n" + " - By default s3fs memorizes when an object does not exist up until\n" + " the stat cache timeout. This caching can cause staleness for\n" + " applications. If disabled, s3fs will not memorize objects and may\n" + " cause extra HeadObject requests and reduce performance.\n" + "\n" + " no_check_certificate\n" + " - server certificate won't be checked against the available \n" + " certificate authorities.\n" + "\n" + " ssl_verify_hostname (default=\"2\")\n" + " - When 0, do not verify the SSL certificate against the hostname.\n" + "\n" + " nodnscache (disable DNS cache)\n" + " - s3fs is always using DNS cache, this option make DNS cache disable.\n" + "\n" + " nosscache (disable SSL session cache)\n" + " - s3fs is always using SSL session cache, this option make SSL \n" + " session cache disable.\n" + "\n" + " multireq_max (default=\"20\")\n" + " - maximum number of parallel request for listing objects.\n" + "\n" + " parallel_count (default=\"5\")\n" + " - number of parallel request for uploading big objects.\n" + " s3fs uploads large object (over 20MB) by multipart post request, \n" + " and sends parallel requests.\n" + " This option limits parallel request count which s3fs requests \n" + " at once. It is necessary to set this value depending on a CPU \n" + " and a network band.\n" + "\n" + " multipart_size (default=\"10\")\n" + " - part size, in MB, for each multipart request.\n" + " The minimum value is 5 MB and the maximum value is 5 GB.\n" + "\n" + " multipart_copy_size (default=\"512\")\n" + " - part size, in MB, for each multipart copy request, used for\n" + " renames and mixupload.\n" + " The minimum value is 5 MB and the maximum value is 5 GB.\n" + " Must be at least 512 MB to copy the maximum 5 TB object size\n" + " but lower values may improve performance.\n" + "\n" + " max_dirty_data (default=\"5120\")\n" + " - flush dirty data to S3 after a certain number of MB written.\n" + " The minimum value is 50 MB. -1 value means disable.\n" + " Cannot be used with nomixupload.\n" + "\n" + " bucket_size (default=maximum long unsigned integer value)\n" + " - The size of the bucket with which the corresponding\n" + " elements of the statvfs structure will be filled. The option\n" + " argument is an integer optionally followed by a\n" + " multiplicative suffix (GB, GiB, TB, TiB, PB, PiB,\n" + " EB, EiB) (no spaces in between). If no suffix is supplied,\n" + " bytes are assumed; eg: 20000000, 30GB, 45TiB. Note that\n" + " s3fs does not compute the actual volume size (too\n" + " expensive): by default it will assume the maximum possible\n" + " size; however, since this may confuse other software which\n" + " uses s3fs, the advertised bucket size can be set with this\n" + " option.\n" + "\n" + " ensure_diskfree (default 0)\n" + " - sets MB to ensure disk free space. This option means the\n" + " threshold of free space size on disk which is used for the\n" + " cache file by s3fs. s3fs makes file for\n" + " downloading, uploading and caching files. If the disk free\n" + " space is smaller than this value, s3fs do not use disk space\n" + " as possible in exchange for the performance.\n" + "\n" + " free_space_ratio (default=\"10\")\n" + " - sets min free space ratio of the disk.\n" + " The value of this option can be between 0 and 100. It will control\n" + " the size of the cache according to this ratio to ensure that the\n" + " idle ratio of the disk is greater than this value.\n" + " For example, when the disk space is 50GB, the default value will\n" + " ensure that the disk will reserve at least 50GB * 10%% = 5GB of\n" + " remaining space.\n" + "\n" + " multipart_threshold (default=\"25\")\n" + " - threshold, in MB, to use multipart upload instead of\n" + " single-part. Must be at least 5 MB.\n" + "\n" + " singlepart_copy_limit (default=\"512\")\n" + " - maximum size, in MB, of a single-part copy before trying \n" + " multipart copy.\n" + "\n" + " host (default=\"https://s3.amazonaws.com\")\n" + " - Set a non-Amazon host, e.g., https://example.com.\n" + "\n" + " servicepath (default=\"/\")\n" + " - Set a service path when the non-Amazon host requires a prefix.\n" + "\n" + " url (default=\"https://s3.amazonaws.com\")\n" + " - sets the url to use to access Amazon S3. If you want to use HTTP,\n" + " then you can set \"url=http://s3.amazonaws.com\".\n" + " If you do not use https, please specify the URL with the url\n" + " option.\n" + "\n" + " endpoint (default=\"us-east-1\")\n" + " - sets the endpoint to use on signature version 4\n" + " If this option is not specified, s3fs uses \"us-east-1\" region as\n" + " the default. If the s3fs could not connect to the region specified\n" + " by this option, s3fs could not run. But if you do not specify this\n" + " option, and if you can not connect with the default region, s3fs\n" + " will retry to automatically connect to the other region. So s3fs\n" + " can know the correct region name, because s3fs can find it in an\n" + " error from the S3 server.\n" + "\n" + " sigv2 (default is signature version 4 falling back to version 2)\n" + " - sets signing AWS requests by using only signature version 2\n" + "\n" + " sigv4 (default is signature version 4 falling back to version 2)\n" + " - sets signing AWS requests by using only signature version 4\n" + "\n" + " mp_umask (default is \"0000\")\n" + " - sets umask for the mount point directory.\n" + " If allow_other option is not set, s3fs allows access to the mount\n" + " point only to the owner. In the opposite case s3fs allows access\n" + " to all users as the default. But if you set the allow_other with\n" + " this option, you can control the permissions of the\n" + " mount point by this option like umask.\n" + "\n" + " umask (default is \"0000\")\n" + " - sets umask for files under the mountpoint. This can allow\n" + " users other than the mounting user to read and write to files\n" + " that they did not create.\n" + "\n" + " nomultipart (disable multipart uploads)\n" + "\n" + " streamupload (default is disable)\n" + " - Enable stream upload.\n" + " If this option is enabled, a sequential upload will be performed\n" + " in parallel with the write from the part that has been written\n" + " during a multipart upload.\n" + " This is expected to give better performance than other upload\n" + " functions.\n" + " Note that this option is still experimental and may change in the\n" + " future.\n" + "\n" + " max_thread_count (default is \"5\")\n" + " - Specifies the number of threads waiting for stream uploads.\n" + " Note that this option and Streamm Upload are still experimental\n" + " and subject to change in the future.\n" + " This option will be merged with \"parallel_count\" in the future.\n" + "\n" + " enable_content_md5 (default is disable)\n" + " - Allow S3 server to check data integrity of uploads via the\n" + " Content-MD5 header. This can add CPU overhead to transfers.\n" + "\n" + " enable_unsigned_payload (default is disable)\n" + " - Do not calculate Content-SHA256 for PutObject and UploadPart\n" + " payloads. This can reduce CPU overhead to transfers.\n" + "\n" + " ecs (default is disable)\n" + " - This option instructs s3fs to query the ECS container credential\n" + " metadata address instead of the instance metadata address.\n" + "\n" + " iam_role (default is no IAM role)\n" + " - This option requires the IAM role name or \"auto\". If you specify\n" + " \"auto\", s3fs will automatically use the IAM role names that are set\n" + " to an instance. If you specify this option without any argument, it\n" + " is the same as that you have specified the \"auto\".\n" + "\n" + " imdsv1only (default is to use IMDSv2 with fallback to v1)\n" + " - AWS instance metadata service, used with IAM role authentication,\n" + " supports the use of an API token. If you're using an IAM role\n" + " in an environment that does not support IMDSv2, setting this flag\n" + " will skip retrieval and usage of the API token when retrieving\n" + " IAM credentials.\n" + "\n" + " ibm_iam_auth (default is not using IBM IAM authentication)\n" + " - This option instructs s3fs to use IBM IAM authentication.\n" + " In this mode, the AWSAccessKey and AWSSecretKey will be used as\n" + " IBM's Service-Instance-ID and APIKey, respectively.\n" + "\n" + " ibm_iam_endpoint (default is https://iam.cloud.ibm.com)\n" + " - sets the URL to use for IBM IAM authentication.\n" + "\n" + " credlib (default=\"\" which means disabled)\n" + " - Specifies the shared library that handles the credentials\n" + " containing the authentication token.\n" + " If this option is specified, the specified credential and token\n" + " processing provided by the shared library ant will be performed\n" + " instead of the built-in credential processing.\n" + " This option cannot be specified with passwd_file, profile,\n" + " use_session_token, ecs, ibm_iam_auth, ibm_iam_endpoint, imdsv1only\n" + " and iam_role option.\n" + "\n" + " credlib_opts (default=\"\" which means disabled)\n" + " - Specifies the options to pass when the shared library specified\n" + " in credlib is loaded and then initialized.\n" + " For the string specified in this option, specify the string defined\n" + " by the shared library.\n" + "\n" + " use_xattr (default is not handling the extended attribute)\n" + " Enable to handle the extended attribute (xattrs).\n" + " If you set this option, you can use the extended attribute.\n" + " For example, encfs and ecryptfs need to support the extended attribute.\n" + " Notice: if s3fs handles the extended attribute, s3fs can not work to\n" + " copy command with preserve=mode.\n" + "\n" + " noxmlns (disable registering xml name space)\n" + " disable registering xml name space for response of \n" + " ListBucketResult and ListVersionsResult etc. Default name \n" + " space is looked up from \"http://s3.amazonaws.com/doc/2006-03-01\".\n" + " This option should not be specified now, because s3fs looks up\n" + " xmlns automatically after v1.66.\n" + "\n" + " nomixupload (disable copy in multipart uploads)\n" + " Disable to use PUT (copy api) when multipart uploading large size objects.\n" + " By default, when doing multipart upload, the range of unchanged data\n" + " will use PUT (copy api) whenever possible.\n" + " When nocopyapi or norenameapi is specified, use of PUT (copy api) is\n" + " invalidated even if this option is not specified.\n" + "\n" + " nocopyapi (for other incomplete compatibility object storage)\n" + " Enable compatibility with S3-like APIs which do not support\n" + " PUT (copy api).\n" + " If you set this option, s3fs do not use PUT with \n" + " \"x-amz-copy-source\" (copy api). Because traffic is increased\n" + " 2-3 times by this option, we do not recommend this.\n" + "\n" + " norenameapi (for other incomplete compatibility object storage)\n" + " Enable compatibility with S3-like APIs which do not support\n" + " PUT (copy api).\n" + " This option is a subset of nocopyapi option. The nocopyapi\n" + " option does not use copy-api for all command (ex. chmod, chown,\n" + " touch, mv, etc), but this option does not use copy-api for\n" + " only rename command (ex. mv). If this option is specified with\n" + " nocopyapi, then s3fs ignores it.\n" + "\n" + " use_path_request_style (use legacy API calling style)\n" + " Enable compatibility with S3-like APIs which do not support\n" + " the virtual-host request style, by using the older path request\n" + " style.\n" + "\n" + " listobjectsv2 (use ListObjectsV2)\n" + " Issue ListObjectsV2 instead of ListObjects, useful on object\n" + " stores without ListObjects support.\n" + "\n" + " noua (suppress User-Agent header)\n" + " Usually s3fs outputs of the User-Agent in \"s3fs/ (commit\n" + " hash ; )\" format.\n" + " If this option is specified, s3fs suppresses the output of the\n" + " User-Agent.\n" + "\n" + " cipher_suites\n" + " Customize the list of TLS cipher suites.\n" + " Expects a colon separated list of cipher suite names.\n" + " A list of available cipher suites, depending on your TLS engine,\n" + " can be found on the CURL library documentation:\n" + " https://curl.haxx.se/docs/ssl-ciphers.html\n" + "\n" + " instance_name - The instance name of the current s3fs mountpoint.\n" + " This name will be added to logging messages and user agent headers sent by s3fs.\n" + "\n" + " complement_stat (complement lack of file/directory mode)\n" + " s3fs complements lack of information about file/directory mode\n" + " if a file or a directory object does not have x-amz-meta-mode\n" + " header. As default, s3fs does not complements stat information\n" + " for a object, then the object will not be able to be allowed to\n" + " list/modify.\n" + "\n" + " compat_dir (enable support of alternative directory names)\n" + " s3fs supports two different naming schemas \"dir/\" and\n" + " \"dir\" to map directory names to S3 objects and\n" + " vice versa by default. As a third variant, directories can be\n" + " determined indirectly if there is a file object with a path (e.g.\n" + " \"/dir/file\") but without the parent directory.\n" + " This option enables a fourth variant, \"dir_$folder$\", created by\n" + " older applications.\n" + " \n" + " S3fs uses only the first schema \"dir/\" to create S3 objects for\n" + " directories." + " \n" + " The support for these different naming schemas causes an increased\n" + " communication effort.\n" + "\n" + " use_wtf8 - support arbitrary file system encoding.\n" + " S3 requires all object names to be valid UTF-8. But some\n" + " clients, notably Windows NFS clients, use their own encoding.\n" + " This option re-encodes invalid UTF-8 object names into valid\n" + " UTF-8 by mapping offending codes into a 'private' codepage of the\n" + " Unicode set.\n" + " Useful on clients not using UTF-8 as their file system encoding.\n" + "\n" + " use_session_token - indicate that session token should be provided.\n" + " If credentials are provided by environment variables this switch\n" + " forces presence check of AWSSESSIONTOKEN variable.\n" + " Otherwise an error is returned.\n" + "\n" + " requester_pays (default is disable)\n" + " This option instructs s3fs to enable requests involving\n" + " Requester Pays buckets.\n" + " It includes the 'x-amz-request-payer=requester' entry in the\n" + " request header.\n" + "\n" + " mime (default is \"/etc/mime.types\")\n" + " Specify the path of the mime.types file.\n" + " If this option is not specified, the existence of \"/etc/mime.types\"\n" + " is checked, and that file is loaded as mime information.\n" + " If this file does not exist on macOS, then \"/etc/apache2/mime.types\"\n" + " is checked as well.\n" + "\n" + " proxy (default=\"\")\n" + " This option specifies a proxy to S3 server.\n" + " Specify the proxy with '[]' formatted.\n" + " '://' can be omitted, and 'http://' is used when omitted.\n" + " Also, ':' can also be omitted. If omitted, port 443 is used for\n" + " HTTPS schema, and port 1080 is used otherwise.\n" + " This option is the same as the curl command's '--proxy(-x)' option and\n" + " libcurl's 'CURLOPT_PROXY' flag.\n" + " This option is equivalent to and takes precedence over the environment\n" + " variables 'http_proxy', 'all_proxy', etc.\n" + "\n" + " proxy_cred_file (default=\"\")\n" + " This option specifies the file that describes the username and\n" + " passphrase for authentication of the proxy when the HTTP schema\n" + " proxy is specified by the 'proxy' option.\n" + " Username and passphrase are valid only for HTTP schema. If the HTTP\n" + " proxy does not require authentication, this option is not required.\n" + " Separate the username and passphrase with a ':' character and\n" + " specify each as a URL-encoded string.\n" + "\n" + " logfile - specify the log output file.\n" + " s3fs outputs the log file to syslog. Alternatively, if s3fs is\n" + " started with the \"-f\" option specified, the log will be output\n" + " to the stdout/stderr.\n" + " You can use this option to specify the log file that s3fs outputs.\n" + " If you specify a log file with this option, it will reopen the log\n" + " file when s3fs receives a SIGHUP signal. You can use the SIGHUP\n" + " signal for log rotation.\n" + "\n" + " dbglevel (default=\"crit\")\n" + " Set the debug message level. set value as crit (critical), err\n" + " (error), warn (warning), info (information) to debug level.\n" + " default debug level is critical. If s3fs run with \"-d\" option,\n" + " the debug level is set information. When s3fs catch the signal\n" + " SIGUSR2, the debug level is bump up.\n" + "\n" + " curldbg - put curl debug message\n" + " Put the debug message from libcurl when this option is specified.\n" + " Specify \"normal\" or \"body\" for the parameter.\n" + " If the parameter is omitted, it is the same as \"normal\".\n" + " If \"body\" is specified, some API communication body data will be\n" + " output in addition to the debug message output as \"normal\".\n" + "\n" + " no_time_stamp_msg - no time stamp in debug message\n" + " The time stamp is output to the debug message by default.\n" + " If this option is specified, the time stamp will not be output\n" + " in the debug message.\n" + " It is the same even if the environment variable \"S3FS_MSGTIMESTAMP\"\n" + " is set to \"no\".\n" + "\n" + " set_check_cache_sigusr1 (default is stdout)\n" + " If the cache is enabled, you can check the integrity of the\n" + " cache file and the cache file's stats info file.\n" + " This option is specified and when sending the SIGUSR1 signal\n" + " to the s3fs process checks the cache status at that time.\n" + " This option can take a file path as parameter to output the\n" + " check result to that file. The file path parameter can be omitted.\n" + " If omitted, the result will be output to stdout or syslog.\n" + "\n" + " update_parent_dir_stat (default is disable)\n" + " The parent directory's mtime and ctime are updated when a file or\n" + " directory is created or deleted (when the parent directory's inode is\n" + " updated).\n" + " By default, parent directory statistics are not updated.\n" + "\n" + " newcache_conf (default=\"\" which means disabled)\n" + " - Enable the new cache.\n" + "\n" + "FUSE/mount Options:\n" + "\n" + " Most of the generic mount options described in 'man mount' are\n" + " supported (ro, rw, suid, nosuid, dev, nodev, exec, noexec, atime,\n" + " noatime, sync async, dirsync). Filesystems are mounted with\n" + " '-onodev,nosuid' by default, which can only be overridden by a\n" + " privileged user.\n" + " \n" + " There are many FUSE specific mount options that can be specified.\n" + " e.g. allow_other See the FUSE's README for the full set.\n" + "\n" + "Utility mode Options:\n" + "\n" + " -u, --incomplete-mpu-list\n" + " Lists multipart incomplete objects uploaded to the specified\n" + " bucket.\n" + " --incomplete-mpu-abort (=all or =)\n" + " Delete the multipart incomplete object uploaded to the specified\n" + " bucket.\n" + " If \"all\" is specified for this option, all multipart incomplete\n" + " objects will be deleted. If you specify no argument as an option,\n" + " objects older than 24 hours (24H) will be deleted (This is the\n" + " default value). You can specify an optional date format. It can\n" + " be specified as year, month, day, hour, minute, second, and it is\n" + " expressed as \"Y\", \"M\", \"D\", \"h\", \"m\", \"s\" respectively.\n" + " For example, \"1Y6M10D12h30m30s\".\n" + "\n" + "Miscellaneous Options:\n" + "\n" + " -h, --help Output this help.\n" + " --version Output version info.\n" + " -d --debug Turn on DEBUG messages to syslog. Specifying -d\n" + " twice turns on FUSE debug messages to STDOUT.\n" + " -f FUSE foreground option - do not run as daemon.\n" + " -s FUSE single-threaded option\n" + " disable multi-threaded operation\n" + "\n" + "\n" + "s3fs home page: \n" + ; + +//------------------------------------------------------------------- +// Functions +//------------------------------------------------------------------- +void show_usage() +{ + printf("Usage: %s BUCKET:[PATH] MOUNTPOINT [OPTION]...\n", program_name.c_str()); +} + +void show_help() +{ + show_usage(); + printf(help_string); +} + +void show_version() +{ + printf( + "Amazon Simple Storage Service File System V%s (commit:%s) with %s\n" + "Copyright (C) 2010 Randy Rizun \n" + "License GPL2: GNU GPL version 2 \n" + "This is free software: you are free to change and redistribute it.\n" + "There is NO WARRANTY, to the extent permitted by law.\n", + VERSION, COMMIT_HASH_VAL, s3fs_crypt_lib_name()); +} + +const char* short_version() +{ + static constexpr char short_ver[] = "s3fs version " VERSION "(" COMMIT_HASH_VAL ")"; + return short_ver; +} + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/s3fs_help.h b/s3fs/s3fs_help.h new file mode 100644 index 0000000..04ce416 --- /dev/null +++ b/s3fs/s3fs_help.h @@ -0,0 +1,41 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef S3FS_S3FS_HELP_H_ +#define S3FS_S3FS_HELP_H_ + +//------------------------------------------------------------------- +// Functions +//------------------------------------------------------------------- +void show_usage(); +void show_help(); +void show_version(); +const char* short_version(); + +#endif // S3FS_S3FS_HELP_H_ + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/s3fs_lib.cpp b/s3fs/s3fs_lib.cpp new file mode 100644 index 0000000..3dd7eb6 --- /dev/null +++ b/s3fs/s3fs_lib.cpp @@ -0,0 +1,2992 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.h" +//#include "s3fs.h" +#include "s3fs_lib.h" +#include "s3fs_logger.h" +#include "metaheader.h" +#include "fdcache.h" +#include "fdcache_auto.h" +#include "fdcache_stat.h" +#include "curl.h" +#include "curl_multi.h" +#include "s3objlist.h" +#include "cache.h" +#include "addhead.h" +#include "sighandlers.h" +#include "s3fs_xml.h" +#include "string_util.h" +#include "s3fs_auth.h" +#include "s3fs_cred.h" +#include "s3fs_help.h" +#include "s3fs_util.h" +#include "mpu_util.h" +#include "threadpoolman.h" +#include "autolock.h" + + +//------------------------------------------------------------------- +// Symbols +//------------------------------------------------------------------- +#if !defined(ENOATTR) +#define ENOATTR ENODATA +#endif + +enum class dirtype : int8_t { + UNKNOWN = -1, + NEW = 0, + OLD = 1, + FOLDER = 2, + NOOBJ = 3, +}; + +struct PosixContext { + uid_t uid; + gid_t gid; + pid_t pid; +}; + +using Ino = uint64_t; +struct S3DirStream { + Ino ino; + uint64_t fh; + uint64_t offset; +}; + +enum class FileType { + DIR = 1, + FILE = 2, +}; + +struct Fileinfo { + uint64_t fd; + int flags; + Ino ino; + // off64_t read_offset; + // off64_t write_offset; + off64_t offset; +}; + +struct PosixS3Info { + std::string filename; + FileType type; //0 is file, 1 is dir + Fileinfo fileinfo; + S3DirStream dirinfo; +}; + + +//------------------------------------------------------------------- +// Static variables +//------------------------------------------------------------------- +static uid_t mp_uid = 0; // owner of mount point(only not specified uid opt) +static gid_t mp_gid = 0; // group of mount point(only not specified gid opt) +static mode_t mp_mode = 0; // mode of mount point +static mode_t mp_umask = 0; // umask for mount point +static bool is_mp_umask = false;// default does not set. +static std::string mountpoint; +static std::unique_ptr ps3fscred; // using only in this file +static std::string mimetype_file; +static bool nocopyapi = false; +static bool norenameapi = false; +static bool nonempty = false; +static bool allow_other = false; +static uid_t s3fs_uid = 0; +static gid_t s3fs_gid = 0; +static mode_t s3fs_umask = 0; +static bool is_s3fs_uid = false;// default does not set. +static bool is_s3fs_gid = false;// default does not set. +static bool is_s3fs_umask = false;// default does not set. +static bool is_remove_cache = false; +static bool is_use_xattr = false; +static off_t multipart_threshold = 25 * 1024 * 1024; +static int64_t singlepart_copy_limit = 512 * 1024 * 1024; +static bool is_specified_endpoint = false; +static int s3fs_init_deferred_exit_status = 0; +static bool support_compat_dir = false;// default does not support compatibility directory type +static int max_keys_list_object = 1000;// default is 1000 +static off_t max_dirty_data = 5LL * 1024LL * 1024LL * 1024LL; +static bool use_wtf8 = false; +static off_t fake_diskfree_size = -1; // default is not set(-1) +static int max_thread_count = 5; // default is 5 +static bool update_parent_dir_stat= false; // default not updating parent directory stats +static fsblkcnt_t bucket_block_count; // advertised block count of the bucket +static unsigned long s3fs_block_size = 16 * 1024 * 1024; // s3fs block size is 16MB +std::string newcache_conf; + +static std::unordered_map fdtofile(1000); +static struct PosixContext posixcontext; +//------------------------------------------------------------------- +// Global functions : prototype +//------------------------------------------------------------------- +int put_headers(const char* path, headers_t& meta, bool is_copy, bool use_st_size = true); // [NOTE] global function because this is called from FdEntity class + + + +//------------------------------------------------------------------- +// Static functions : prototype +//------------------------------------------------------------------- +static int init_config(std::string configpath); + +static bool is_special_name_folder_object(const char* path); +static int chk_dir_object_type(const char* path, std::string& newpath, std::string& nowpath, std::string& nowcache, headers_t* pmeta = nullptr, dirtype* pDirType = nullptr); +static int remove_old_type_dir(const std::string& path, dirtype type); +static int get_object_attribute(const char* path, struct stat* pstbuf, headers_t* pmeta = nullptr, bool overcheck = true, bool* pisforce = nullptr, bool add_no_truncate_cache = false); +static int check_object_access(const char* path, int mask, struct stat* pstbuf); +static int check_object_owner(const char* path, struct stat* pstbuf); +static int check_parent_object_access(const char* path, int mask); +static int get_local_fent(AutoFdEntity& autoent, FdEntity **entity, const char* path, int flags = O_RDONLY, bool is_load = false); +static bool multi_head_callback(S3fsCurl* s3fscurl, void* param); +static std::unique_ptr multi_head_retry_callback(S3fsCurl* s3fscurl); +//static int readdir_multi_head(const char* path, const S3ObjList& head, void* buf, fuse_fill_dir_t filler); +static int readdir_multi_head(const char* path, const S3ObjList& head, char* data, int offset, int maxread, ssize_t* realbytes, int* realnum); +static int list_bucket(const char* path, S3ObjList& head, const char* delimiter, bool check_content_only = false); +static int directory_empty(const char* path); +static int rename_large_object(const char* from, const char* to); +static int create_file_object(const char* path, mode_t mode, uid_t uid, gid_t gid); +static int create_directory_object(const char* path, mode_t mode, const struct timespec& ts_atime, const struct timespec& ts_mtime, const struct timespec& ts_ctime, uid_t uid, gid_t gid, const char* pxattrvalue); +static int rename_object(const char* from, const char* to, bool update_ctime); +static int rename_object_nocopy(const char* from, const char* to, bool update_ctime); +static int clone_directory_object(const char* from, const char* to, bool update_ctime, const char* pxattrvalue); +static int rename_directory(const char* from, const char* to); +static int update_mctime_parent_directory(const char* _path); +static int remote_mountpath_exists(const char* path, bool compat_dir); +static bool get_meta_xattr_value(const char* path, std::string& rawvalue); +static bool get_parent_meta_xattr_value(const char* path, std::string& rawvalue); +static bool get_xattr_posix_key_value(const char* path, std::string& xattrvalue, bool default_key); +static bool build_inherited_xattr_value(const char* path, std::string& xattrvalue); +static bool parse_xattr_keyval(const std::string& xattrpair, std::string& key, std::string* pval); +static size_t parse_xattrs(const std::string& strxattrs, xattrs_t& xattrs); +static std::string raw_build_xattrs(const xattrs_t& xattrs); +static std::string build_xattrs(const xattrs_t& xattrs); +static int s3fs_check_service(); +static bool set_mountpoint_attribute(struct stat& mpst); +static int set_bucket(const char* arg); +static int my_fuse_opt_proc(void* data, const char* arg, int key, struct fuse_args* outargs); +static fsblkcnt_t parse_bucket_size(char* value); +static bool is_cmd_exists(const std::string& command); +static int print_umount_message(const std::string& mp, bool force); + + + + +//------------------------------------------------------------------- +// Classes +//------------------------------------------------------------------- +// +// A flag class indicating whether the mount point has a stat +// +// [NOTE] +// The flag is accessed from child threads, so This class is used for exclusive control of flags. +// This class will be reviewed when we organize the code in the future. +// +class MpStatFlag +{ + private: + std::atomic has_mp_stat; + + public: + MpStatFlag() = default; + MpStatFlag(const MpStatFlag&) = delete; + MpStatFlag(MpStatFlag&&) = delete; + ~MpStatFlag() = default; + MpStatFlag& operator=(const MpStatFlag&) = delete; + MpStatFlag& operator=(MpStatFlag&&) = delete; + + bool Get(); + bool Set(bool flag); +}; + +bool MpStatFlag::Get() +{ + return has_mp_stat; +} + +bool MpStatFlag::Set(bool flag) +{ + return has_mp_stat.exchange(flag); +} + +// whether the stat information file for mount point exists +static MpStatFlag* pHasMpStat = nullptr; + + + +// +// A synchronous class that calls the fuse_fill_dir_t function that processes the readdir data +// + +typedef int (*fill_dir_t) (void *buf, const char *name, + const struct stat *stbuf, off_t off); + +class SyncFiller +{ + private: + mutable pthread_mutex_t filler_lock; + bool is_lock_init = false; + void* filler_buff; + fill_dir_t filler_func; + std::set filled; + + public: + explicit SyncFiller(void* buff = nullptr, fill_dir_t filler = nullptr); + SyncFiller(const SyncFiller&) = delete; + SyncFiller(SyncFiller&&) = delete; + ~SyncFiller(); + SyncFiller& operator=(const SyncFiller&) = delete; + SyncFiller& operator=(SyncFiller&&) = delete; + + int Fill(const char *name, const struct stat *stbuf, off_t off); + int SufficiencyFill(const std::vector& pathlist); +}; + +SyncFiller::SyncFiller(void* buff, fill_dir_t filler) : filler_buff(buff), filler_func(filler) +{ + if(!filler_buff || !filler_func){ + S3FS_PRN_CRIT("Internal error: SyncFiller constructor parameter is critical value."); + abort(); + } + + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); +#if S3FS_PTHREAD_ERRORCHECK + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK); +#endif + + int result; + if(0 != (result = pthread_mutex_init(&filler_lock, &attr))){ + S3FS_PRN_CRIT("failed to init filler_lock: %d", result); + abort(); + } + is_lock_init = true; +} + +SyncFiller::~SyncFiller() +{ + if(is_lock_init){ + int result; + if(0 != (result = pthread_mutex_destroy(&filler_lock))){ + S3FS_PRN_CRIT("failed to destroy filler_lock: %d", result); + abort(); + } + is_lock_init = false; + } +} + +// +// See. prototype fuse_fill_dir_t in fuse.h +// +int SyncFiller::Fill(const char *name, const struct stat *stbuf, off_t off) +{ + AutoLock auto_lock(&filler_lock); + + int result = 0; + if(filled.insert(name).second){ + result = filler_func(filler_buff, name, stbuf, off); + } + return result; +} + +int SyncFiller::SufficiencyFill(const std::vector& pathlist) +{ + AutoLock auto_lock(&filler_lock); + + int result = 0; + for(std::vector::const_iterator it = pathlist.begin(); it != pathlist.end(); ++it) { + if(filled.insert(*it).second){ + if(0 != filler_func(filler_buff, it->c_str(), nullptr, 0)){ + result = 1; + } + } + } + return result; +} + + + +//------------------------------------------------------------------- +// Functions +//------------------------------------------------------------------- +static bool IS_REPLACEDIR(dirtype type) +{ + return dirtype::OLD == type || dirtype::FOLDER == type || dirtype::NOOBJ == type; +} + +static bool IS_RMTYPEDIR(dirtype type) +{ + return dirtype::OLD == type || dirtype::FOLDER == type; +} + +static bool IS_CREATE_MP_STAT(const char* path) +{ + // [NOTE] + // pHasMpStat->Get() is set in get_object_attribute() + // + return (path && 0 == strcmp(path, "/") && !pHasMpStat->Get()); +} + +int put_headers(const char* path, headers_t& meta, bool is_copy, bool use_st_size) +{ + int result; + S3fsCurl s3fscurl(true); + off_t size; + std::string strpath; + + S3FS_PRN_INFO2("[path=%s]", path); + + if(0 == strcmp(path, "/") && mount_prefix.empty()){ + strpath = "//"; // for the mount point that is bucket root, change "/" to "//". + }else{ + strpath = path; + } + + // files larger than 5GB must be modified via the multipart interface + // call use_st_size as false when the file does not exist(ex. rename object) + if(use_st_size && '/' != *strpath.rbegin()){ // directory object("dir/") is always 0(Content-Length = 0) + struct stat buf; + if(0 != (result = get_object_attribute(path, &buf))){ + return result; + } + size = buf.st_size; + }else{ + size = get_size(meta); + } + + if(!nocopyapi && !nomultipart && size >= multipart_threshold){ + if(0 != (result = s3fscurl.MultipartHeadRequest(strpath.c_str(), size, meta, is_copy))){ + return result; + } + }else{ + if(0 != (result = s3fscurl.PutHeadRequest(strpath.c_str(), meta, is_copy))){ + return result; + } + } + return 0; +} + + +static int directory_empty(const char* path) +{ + int result; + S3ObjList head; + + if((result = list_bucket(path, head, "/", true)) != 0){ + S3FS_PRN_ERR("list_bucket returns error."); + return result; + } + if(!head.IsEmpty()){ + return -ENOTEMPTY; + } + return 0; +} + +// +// Get object attributes with stat cache. +// This function is base for s3fs_getattr(). +// +// [NOTICE] +// Checking order is changed following list because of reducing the number of the requests. +// 1) "dir" +// 2) "dir/" +// 3) "dir_$folder$" +// +// Special two case of the mount point directory: +// [Case 1] the mount point is the root of the bucket: +// 1) "/" +// +// [Case 2] the mount point is a directory path(ex. foo) below the bucket: +// 1) "foo" +// 2) "foo/" +// 3) "foo_$folder$" +// +static int get_object_attribute(const char* path, struct stat* pstbuf, headers_t* pmeta, bool overcheck, bool* pisforce, bool add_no_truncate_cache) +{ + int result = -1; + struct stat tmpstbuf; + struct stat* pstat = pstbuf ? pstbuf : &tmpstbuf; + headers_t tmpHead; + headers_t* pheader = pmeta ? pmeta : &tmpHead; + std::string strpath; + S3fsCurl s3fscurl; + bool forcedir = false; + bool is_mountpoint = false; // path is the mount point + bool is_bucket_mountpoint = false; // path is the mount point which is the bucket root + std::string::size_type Pos; + + S3FS_PRN_DBG("[path=%s]", path); + + if(!path || '\0' == path[0]){ + return -ENOENT; + } + + memset(pstat, 0, sizeof(struct stat)); + + // check mount point + if(0 == strcmp(path, "/") || 0 == strcmp(path, ".")){ + is_mountpoint = true; + if(mount_prefix.empty()){ + is_bucket_mountpoint = true; + } + // default stat for mount point if the directory stat file is not existed. + pstat->st_mode = mp_mode; + pstat->st_uid = is_s3fs_uid ? s3fs_uid : mp_uid; + pstat->st_gid = is_s3fs_gid ? s3fs_gid : mp_gid; + } + + // Check cache. + pisforce = (nullptr != pisforce ? pisforce : &forcedir); + (*pisforce) = false; + strpath = path; + if(support_compat_dir && overcheck && std::string::npos != (Pos = strpath.find("_$folder$", 0))){ + strpath.erase(Pos); + strpath += "/"; + } + // [NOTE] + // For mount points("/"), the Stat cache key name is "/". + // + if(StatCache::getStatCacheData()->GetStat(strpath, pstat, pheader, overcheck, pisforce)){ + if(is_mountpoint){ + // if mount point, we need to set this. + pstat->st_nlink = 1; // see fuse faq + } + return 0; + } + if(StatCache::getStatCacheData()->IsNoObjectCache(strpath)){ + // there is the path in the cache for no object, it is no object. + return -ENOENT; + } + + // set query(head request) path + if(is_bucket_mountpoint){ + // [NOTE] + // This is a special process for mount point + // The path is "/" for mount points. + // If the bucket mounted at a mount point, we try to find "/" object under + // the bucket for mount point's stat. + // In this case, we will send the request "HEAD // HTTP /1.1" to S3 server. + // + // If the directory under the bucket is mounted, it will be sent + // "HEAD // HTTP/1.1", so we do not need to change path at + // here. + // + strpath = "//"; // strpath is "//" + }else{ + strpath = path; + } + + if(use_newcache && accessor->UseGlobalCache()){ + size_t realSize = 0; + std::map headers; + result = accessor->Head(strpath, realSize, headers); + if(0 == result){ + headers["Content-Length"] = std::to_string(realSize); + for(auto& it : headers) { + pheader->insert(std::make_pair(it.first, it.second)); + } + } + } else { + result = s3fscurl.HeadRequest(strpath.c_str(), (*pheader)); + s3fscurl.DestroyCurlHandle(); + } + + + // if not found target path object, do over checking + if(-EPERM == result){ + // [NOTE] + // In case of a permission error, it exists in directory + // file list but inaccessible. So there is a problem that + // it will send a HEAD request every time, because it is + // not registered in the Stats cache. + // Therefore, even if the file has a permission error, it + // should be registered in the Stats cache. However, if + // the response without modifying is registered in the + // cache, the file permission will be 0644(umask dependent) + // because the meta header does not exist. + // Thus, set the mode of 0000 here in the meta header so + // that s3fs can print a permission error when the file + // is actually accessed. + // It is better not to set meta header other than mode, + // so do not do it. + // + (*pheader)["x-amz-meta-mode"] = "0"; + + }else if(0 != result){ + if(overcheck && !is_bucket_mountpoint){ + // when support_compat_dir is disabled, strpath maybe have "_$folder$". + if('/' != *strpath.rbegin() && std::string::npos == strpath.find("_$folder$", 0)){ + // now path is "object", do check "object/" for over checking + strpath += "/"; + result = s3fscurl.HeadRequest(strpath.c_str(), (*pheader)); + s3fscurl.DestroyCurlHandle(); + } + if(support_compat_dir && 0 != result){ + // now path is "object/", do check "object_$folder$" for over checking + strpath.erase(strpath.length() - 1); + strpath += "_$folder$"; + result = s3fscurl.HeadRequest(strpath.c_str(), (*pheader)); + s3fscurl.DestroyCurlHandle(); + + if(0 != result){ + // cut "_$folder$" for over checking "no dir object" after here + if(std::string::npos != (Pos = strpath.find("_$folder$", 0))){ + strpath.erase(Pos); + } + } + } + } + if(0 != result && std::string::npos == strpath.find("_$folder$", 0)){ + // now path is "object" or "object/", do check "no dir object" which is not object but has only children. + // + // [NOTE] + // If the path is mount point and there is no Stat information file for it, we need this process. + // + if('/' == *strpath.rbegin()){ + strpath.erase(strpath.length() - 1); + } + if(-ENOTEMPTY == directory_empty(strpath.c_str())){ + // found "no dir object". + strpath += "/"; + *pisforce = true; + result = 0; + } + } + }else{ + if('/' != *strpath.rbegin() && std::string::npos == strpath.find("_$folder$", 0) && is_need_check_obj_detail(*pheader)){ + // check a case of that "object" does not have attribute and "object" is possible to be directory. + if(-ENOTEMPTY == directory_empty(strpath.c_str())){ + // found "no dir object". + strpath += "/"; + *pisforce = true; + result = 0; + } + } + } + + // set headers for mount point from default stat + if(is_mountpoint){ + if(0 != result || pheader->empty()){ + pHasMpStat->Set(false); + + // [NOTE] + // If mount point and no stat information file, create header + // information from the default stat. + // + (*pheader)["Content-Type"] = S3fsCurl::LookupMimeType(strpath); + (*pheader)["x-amz-meta-uid"] = std::to_string(pstat->st_uid); + (*pheader)["x-amz-meta-gid"] = std::to_string(pstat->st_gid); + (*pheader)["x-amz-meta-mode"] = std::to_string(pstat->st_mode); + (*pheader)["x-amz-meta-atime"] = std::to_string(pstat->st_atime); + (*pheader)["x-amz-meta-ctime"] = std::to_string(pstat->st_ctime); + (*pheader)["x-amz-meta-mtime"] = std::to_string(pstat->st_mtime); + + result = 0; + }else{ + pHasMpStat->Set(true); + } + } + + // [NOTE] + // If the file is listed but not allowed access, put it in + // the positive cache instead of the negative cache. + // + // When mount points, the following error does not occur. + // + if(0 != result && -EPERM != result){ + // finally, "path" object did not find. Add no object cache. + strpath = path; // reset original + StatCache::getStatCacheData()->AddNoObjectCache(strpath); + return result; + } + + // set cache key + if(is_bucket_mountpoint){ + strpath = "/"; + }else if(std::string::npos != (Pos = strpath.find("_$folder$", 0))){ + // if path has "_$folder$", need to cut it. + strpath.erase(Pos); + strpath += "/"; + } + + // Set into cache + // + // [NOTE] + // When add_no_truncate_cache is true, the stats is always cached. + // This cached stats is only removed by DelStat(). + // This is necessary for the case to access the attribute of opened file. + // (ex. getxattr() is called while writing to the opened file.) + // + if(add_no_truncate_cache || 0 != StatCache::getStatCacheData()->GetCacheSize()){ + // add into stat cache + if(!StatCache::getStatCacheData()->AddStat(strpath, (*pheader), forcedir, add_no_truncate_cache)){ + S3FS_PRN_ERR("failed adding stat cache [path=%s]", strpath.c_str()); + return -ENOENT; + } + if(!StatCache::getStatCacheData()->GetStat(strpath, pstat, pheader, overcheck, pisforce)){ + // There is not in cache.(why?) -> retry to convert. + if(!convert_header_to_stat(strpath.c_str(), (*pheader), pstat, forcedir)){ + S3FS_PRN_ERR("failed convert headers to stat[path=%s]", strpath.c_str()); + return -ENOENT; + } + } + }else{ + // cache size is Zero -> only convert. + if(!convert_header_to_stat(strpath.c_str(), (*pheader), pstat, forcedir)){ + S3FS_PRN_ERR("failed convert headers to stat[path=%s]", strpath.c_str()); + return -ENOENT; + } + } + + if(is_mountpoint){ + // if mount point, we need to set this. + pstat->st_nlink = 1; // see fuse faq + } + + return 0; +} + +bool get_object_sse_type(const char* path, sse_type_t& ssetype, std::string& ssevalue) +{ + if(!path){ + return false; + } + + headers_t meta; + if(0 != get_object_attribute(path, nullptr, &meta)){ + S3FS_PRN_ERR("Failed to get object(%s) headers", path); + return false; + } + + ssetype = sse_type_t::SSE_DISABLE; + ssevalue.clear(); + for(headers_t::iterator iter = meta.begin(); iter != meta.end(); ++iter){ + std::string key = (*iter).first; + if(0 == strcasecmp(key.c_str(), "x-amz-server-side-encryption") && 0 == strcasecmp((*iter).second.c_str(), "AES256")){ + ssetype = sse_type_t::SSE_S3; + }else if(0 == strcasecmp(key.c_str(), "x-amz-server-side-encryption-aws-kms-key-id")){ + ssetype = sse_type_t::SSE_KMS; + ssevalue = (*iter).second; + }else if(0 == strcasecmp(key.c_str(), "x-amz-server-side-encryption-customer-key-md5")){ + ssetype = sse_type_t::SSE_C; + ssevalue = (*iter).second; + } + } + return true; +} + + +// +// Check the object uid and gid for write/read/execute. +// The param "mask" is as same as access() function. +// If there is not a target file, this function returns -ENOENT. +// If the target file can be accessed, the result always is 0. +// +// path: the target object path +// mask: bit field(F_OK, R_OK, W_OK, X_OK) like access(). +// stat: nullptr or the pointer of struct stat. +// +static int check_object_access(const char* path, int mask, struct stat* pstbuf) +{ + //return 0; + int result; + struct stat st; + struct stat* pst = (pstbuf ? pstbuf : &st); + // struct fuse_context* pcxt; + + // S3FS_PRN_DBG("[path=%s]", path); + + // if(nullptr == (pcxt = fuse_get_context())){ + // return -EIO; + // } + // S3FS_PRN_DBG("[pid=%u,uid=%u,gid=%u]", (unsigned int)(pcxt->pid), (unsigned int)(pcxt->uid), (unsigned int)(pcxt->gid)); + + if(0 != (result = get_object_attribute(path, pst))){ + // If there is not the target file(object), result is -ENOENT. + return result; + } + // if(0 == pcxt->uid){ + // // root is allowed all accessing. + // return 0; + // } + // if(is_s3fs_uid && s3fs_uid == pcxt->uid){ + // // "uid" user is allowed all accessing. + // return 0; + // } + // if(F_OK == mask){ + // // if there is a file, always return allowed. + // return 0; + // } + + // // for "uid", "gid" option + // uid_t obj_uid = (is_s3fs_uid ? s3fs_uid : pst->st_uid); + // gid_t obj_gid = (is_s3fs_gid ? s3fs_gid : pst->st_gid); + + // // compare file mode and uid/gid + mask. + // mode_t mode; + // mode_t base_mask = S_IRWXO; + // if(is_s3fs_umask){ + // // If umask is set, all object attributes set ~umask. + // mode = ((S_IRWXU | S_IRWXG | S_IRWXO) & ~s3fs_umask); + // }else{ + // mode = pst->st_mode; + // } + // if(pcxt->uid == obj_uid){ + // base_mask |= S_IRWXU; + // } + // if(pcxt->gid == obj_gid){ + // base_mask |= S_IRWXG; + // } else if(1 == is_uid_include_group(pcxt->uid, obj_gid)){ + // base_mask |= S_IRWXG; + // } + // mode &= base_mask; + + // if(X_OK == (mask & X_OK)){ + // if(0 == (mode & (S_IXUSR | S_IXGRP | S_IXOTH))){ + // return -EACCES; + // } + // } + // if(W_OK == (mask & W_OK)){ + // if(0 == (mode & (S_IWUSR | S_IWGRP | S_IWOTH))){ + // return -EACCES; + // } + // } + // if(R_OK == (mask & R_OK)){ + // if(0 == (mode & (S_IRUSR | S_IRGRP | S_IROTH))){ + // return -EACCES; + // } + // } + // if(0 == mode){ + // return -EACCES; + // } + return 0; +} + +static bool check_region_error(const char* pbody, size_t len, std::string& expectregion) +{ + if(!pbody){ + return false; + } + + std::string code; + if(!simple_parse_xml(pbody, len, "Code", code) || code != "AuthorizationHeaderMalformed"){ + return false; + } + + if(!simple_parse_xml(pbody, len, "Region", expectregion)){ + return false; + } + + return true; +} + +static bool check_endpoint_error(const char* pbody, size_t len, std::string& expectendpoint) +{ + if(!pbody){ + return false; + } + + std::string code; + if(!simple_parse_xml(pbody, len, "Code", code) || code != "PermanentRedirect"){ + return false; + } + + if(!simple_parse_xml(pbody, len, "Endpoint", expectendpoint)){ + return false; + } + + return true; +} + +static bool check_invalid_sse_arg_error(const char* pbody, size_t len) +{ + if(!pbody){ + return false; + } + + std::string code; + if(!simple_parse_xml(pbody, len, "Code", code) || code != "InvalidArgument"){ + return false; + } + std::string argname; + if(!simple_parse_xml(pbody, len, "ArgumentName", argname) || argname != "x-amz-server-side-encryption"){ + return false; + } + return true; +} + +static bool check_error_message(const char* pbody, size_t len, std::string& message) +{ + message.clear(); + if(!pbody){ + return false; + } + if(!simple_parse_xml(pbody, len, "Message", message)){ + return false; + } + return true; +} + + + +// [NOTE] +// This function checks if the bucket is accessible when s3fs starts. +// +// The following patterns for mount points are supported by s3fs: +// (1) Mount the bucket top +// (2) Mount to a directory(folder) under the bucket. In this case: +// (2A) Directories created by clients other than s3fs +// (2B) Directory created by s3fs +// +// Both case of (1) and (2) check access permissions to the mount point +// path(directory). +// In the case of (2A), if the directory(object) for the mount point does +// not exist, the check fails. However, launching s3fs with the "compat_dir" +// option avoids this error and the check succeeds. If you do not specify +// the "compat_dir" option in case (2A), please create a directory(object) +// for the mount point before launching s3fs. +// +static int s3fs_check_service() +{ + S3FS_PRN_INFO("check services."); + + // At first time for access S3, we check IAM role if it sets. + if(!ps3fscred->CheckIAMCredentialUpdate()){ + S3FS_PRN_CRIT("Failed to initialize IAM credential."); + return EXIT_FAILURE; + } + + S3fsCurl s3fscurl; + int res; + bool force_no_sse = false; + + while(0 > (res = s3fscurl.CheckBucket(get_realpath("/").c_str(), support_compat_dir, force_no_sse))){ + // get response code + bool do_retry = false; + long responseCode = s3fscurl.GetLastResponseCode(); + + // check wrong endpoint, and automatically switch endpoint + if(300 <= responseCode && responseCode < 500){ + + // check region error(for putting message or retrying) + const std::string* body = s3fscurl.GetBodyData(); + std::string expectregion; + std::string expectendpoint; + + // Check if any case can be retried + if(check_region_error(body->c_str(), body->size(), expectregion)){ + // [NOTE] + // If endpoint is not specified(using us-east-1 region) and + // an error is encountered accessing a different region, we + // will retry the check on the expected region. + // see) https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingBucket.html#access-bucket-intro + // + if(s3host != "http://s3.amazonaws.com" && s3host != "https://s3.amazonaws.com"){ + // specified endpoint for specified url is wrong. + if(is_specified_endpoint){ + S3FS_PRN_CRIT("The bucket region is not '%s'(specified) for specified url(%s), it is correctly '%s'. You should specify url(http(s)://s3-%s.amazonaws.com) and endpoint(%s) option.", endpoint.c_str(), s3host.c_str(), expectregion.c_str(), expectregion.c_str(), expectregion.c_str()); + }else{ + S3FS_PRN_CRIT("The bucket region is not '%s'(default) for specified url(%s), it is correctly '%s'. You should specify url(http(s)://s3-%s.amazonaws.com) and endpoint(%s) option.", endpoint.c_str(), s3host.c_str(), expectregion.c_str(), expectregion.c_str(), expectregion.c_str()); + } + + }else if(is_specified_endpoint){ + // specified endpoint is wrong. + S3FS_PRN_CRIT("The bucket region is not '%s'(specified), it is correctly '%s'. You should specify endpoint(%s) option.", endpoint.c_str(), expectregion.c_str(), expectregion.c_str()); + + }else if(S3fsCurl::GetSignatureType() == signature_type_t::V4_ONLY || S3fsCurl::GetSignatureType() == signature_type_t::V2_OR_V4){ + // current endpoint and url are default value, so try to connect to expected region. + S3FS_PRN_CRIT("Failed to connect region '%s'(default), so retry to connect region '%s' for url(http(s)://s3-%s.amazonaws.com).", endpoint.c_str(), expectregion.c_str(), expectregion.c_str()); + + // change endpoint + endpoint = expectregion; + + // change url + if(s3host == "http://s3.amazonaws.com"){ + s3host = "http://s3-" + endpoint + ".amazonaws.com"; + }else if(s3host == "https://s3.amazonaws.com"){ + s3host = "https://s3-" + endpoint + ".amazonaws.com"; + } + + // Retry with changed host + s3fscurl.DestroyCurlHandle(); + do_retry = true; + + }else{ + S3FS_PRN_CRIT("The bucket region is not '%s'(default), it is correctly '%s'. You should specify endpoint(%s) option.", endpoint.c_str(), expectregion.c_str(), expectregion.c_str()); + } + + }else if(check_endpoint_error(body->c_str(), body->size(), expectendpoint)){ + // redirect error + if(pathrequeststyle){ + S3FS_PRN_CRIT("S3 service returned PermanentRedirect (current is url(%s) and endpoint(%s)). You need to specify correct url(http(s)://s3-.amazonaws.com) and endpoint option with use_path_request_style option.", s3host.c_str(), endpoint.c_str()); + }else{ + S3FS_PRN_CRIT("S3 service returned PermanentRedirect with %s (current is url(%s) and endpoint(%s)). You need to specify correct endpoint option.", expectendpoint.c_str(), s3host.c_str(), endpoint.c_str()); + } + return EXIT_FAILURE; + + }else if(check_invalid_sse_arg_error(body->c_str(), body->size())){ + // SSE argument error, so retry it without SSE + S3FS_PRN_CRIT("S3 service returned InvalidArgument(x-amz-server-side-encryption), so retry without adding x-amz-server-side-encryption."); + + // Retry without sse parameters + s3fscurl.DestroyCurlHandle(); + do_retry = true; + force_no_sse = true; + } + } + + // Try changing signature from v4 to v2 + // + // [NOTE] + // If there is no case to retry with the previous checks, and there + // is a chance to retry with signature v2, prepare to retry with v2. + // + if(!do_retry && (responseCode == 400 || responseCode == 403) && S3fsCurl::GetSignatureType() == signature_type_t::V2_OR_V4){ + // switch sigv2 + S3FS_PRN_CRIT("Failed to connect by sigv4, so retry to connect by signature version 2. But you should to review url and endpoint option."); + + // retry to check with sigv2 + s3fscurl.DestroyCurlHandle(); + do_retry = true; + S3fsCurl::SetSignatureType(signature_type_t::V2_ONLY); + } + + // check errors(after retrying) + if(!do_retry && responseCode != 200 && responseCode != 301){ + // parse error message if existed + std::string errMessage; + const std::string* body = s3fscurl.GetBodyData(); + check_error_message(body->c_str(), body->size(), errMessage); + + if(responseCode == 400){ + S3FS_PRN_CRIT("Failed to check bucket and directory for mount point : Bad Request(host=%s, message=%s)", s3host.c_str(), errMessage.c_str()); + }else if(responseCode == 403){ + S3FS_PRN_CRIT("Failed to check bucket and directory for mount point : Invalid Credentials(host=%s, message=%s)", s3host.c_str(), errMessage.c_str()); + }else if(responseCode == 404){ + if(mount_prefix.empty()){ + S3FS_PRN_CRIT("Failed to check bucket and directory for mount point : Bucket or directory not found(host=%s, message=%s)", s3host.c_str(), errMessage.c_str()); + }else{ + S3FS_PRN_CRIT("Failed to check bucket and directory for mount point : Bucket or directory(%s) not found(host=%s, message=%s) - You may need to specify the compat_dir option.", mount_prefix.c_str(), s3host.c_str(), errMessage.c_str()); + } + }else{ + S3FS_PRN_CRIT("Failed to check bucket and directory for mount point : Unable to connect(host=%s, message=%s)", s3host.c_str(), errMessage.c_str()); + } + return EXIT_FAILURE; + } + } + s3fscurl.DestroyCurlHandle(); + + // make sure remote mountpath exists and is a directory + if(!mount_prefix.empty()){ + if(remote_mountpath_exists("/", support_compat_dir) != 0){ + S3FS_PRN_CRIT("Remote mountpath %s not found, this may be resolved with the compat_dir option.", mount_prefix.c_str()); + return EXIT_FAILURE; + } + } + S3FS_MALLOCTRIM(0); + + return EXIT_SUCCESS; +} + +// +// Check accessing the parent directories of the object by uid and gid. +// +static int check_parent_object_access(const char* path, int mask) +{ + std::string parent; + int result; + + S3FS_PRN_DBG("[path=%s]", path); + + if(0 == strcmp(path, "/") || 0 == strcmp(path, ".")){ + // path is mount point. + return 0; + } + if(X_OK == (mask & X_OK)){ + for(parent = mydirname(path); !parent.empty(); parent = mydirname(parent)){ + if(parent == "."){ + parent = "/"; + } + if(0 != (result = check_object_access(parent.c_str(), X_OK, nullptr))){ + return result; + } + if(parent == "/" || parent == "."){ + break; + } + } + } + mask = (mask & ~X_OK); + if(0 != mask){ + parent = mydirname(path); + if(parent == "."){ + parent = "/"; + } + if(0 != (result = check_object_access(parent.c_str(), mask, nullptr))){ + return result; + } + } + return 0; +} + +static int list_bucket(const char* path, S3ObjList& head, const char* delimiter, bool check_content_only) +{ + std::string s3_realpath; + std::string query_delimiter; + std::string query_prefix; + std::string query_maxkey; + std::string next_continuation_token; + std::string next_marker; + bool truncated = true; + S3fsCurl s3fscurl; + + S3FS_PRN_INFO1("[path=%s]", path); + + if(delimiter && 0 < strlen(delimiter)){ + query_delimiter += "delimiter="; + query_delimiter += delimiter; + query_delimiter += "&"; + } + + query_prefix += "&prefix="; + s3_realpath = get_realpath(path); + if(s3_realpath.empty() || '/' != *s3_realpath.rbegin()){ + // last word must be "/" + query_prefix += urlEncodePath(s3_realpath.substr(1) + "/"); + }else{ + query_prefix += urlEncodePath(s3_realpath.substr(1)); + } + if (check_content_only){ + // Just need to know if there are child objects in dir + // For dir with children, expect "dir/" and "dir/child" + query_maxkey += "max-keys=2"; + }else{ + query_maxkey += "max-keys=" + std::to_string(max_keys_list_object); + } + + while(truncated){ + // append parameters to query in alphabetical order + std::string each_query; + if(!next_continuation_token.empty()){ + each_query += "continuation-token=" + urlEncodePath(next_continuation_token) + "&"; + next_continuation_token = ""; + } + each_query += query_delimiter; + if(S3fsCurl::IsListObjectsV2()){ + each_query += "list-type=2&"; + } + if(!next_marker.empty()){ + each_query += "marker=" + urlEncodePath(next_marker) + "&"; + next_marker = ""; + } + each_query += query_maxkey; + each_query += query_prefix; + + // request + int result; + if(0 != (result = s3fscurl.ListBucketRequest(path, each_query.c_str()))){ + S3FS_PRN_ERR("ListBucketRequest returns with error."); + return result; + } + const std::string* body = s3fscurl.GetBodyData(); + + // [NOTE] + // CR code(\r) is replaced with LF(\n) by xmlReadMemory() function. + // To prevent that, only CR code is encoded by following function. + // The encoded CR code is decoded with append_objects_from_xml(_ex). + // + std::string encbody = get_encoded_cr_code(body->c_str()); + + // xmlDocPtr + std::unique_ptr doc(xmlReadMemory(encbody.c_str(), static_cast(encbody.size()), "", nullptr, 0), xmlFreeDoc); + if(nullptr == doc){ + S3FS_PRN_ERR("xmlReadMemory returns with error."); + return -EIO; + } + if(0 != append_objects_from_xml(path, doc.get(), head)){ + S3FS_PRN_ERR("append_objects_from_xml returns with error."); + return -EIO; + } + if(true == (truncated = is_truncated(doc.get()))){ + auto tmpch = get_next_continuation_token(doc.get()); + if(nullptr != tmpch){ + next_continuation_token = reinterpret_cast(tmpch.get()); + }else if(nullptr != (tmpch = get_next_marker(doc.get()))){ + next_marker = reinterpret_cast(tmpch.get()); + } + + if(next_continuation_token.empty() && next_marker.empty()){ + // If did not specify "delimiter", s3 did not return "NextMarker". + // On this case, can use last name for next marker. + // + std::string lastname; + if(!head.GetLastName(lastname)){ + S3FS_PRN_WARN("Could not find next marker, thus break loop."); + truncated = false; + }else{ + next_marker = s3_realpath.substr(1); + if(s3_realpath.empty() || '/' != *s3_realpath.rbegin()){ + next_marker += "/"; + } + next_marker += lastname; + } + } + } + + // reset(initialize) curl object + s3fscurl.DestroyCurlHandle(); + + if(check_content_only){ + break; + } + } + S3FS_MALLOCTRIM(0); + + return 0; +} + +static int remote_mountpath_exists(const char* path, bool compat_dir) +{ + struct stat stbuf; + int result; + + S3FS_PRN_INFO1("[path=%s]", path); + + // getattr will prefix the path with the remote mountpoint + if(0 != (result = get_object_attribute(path, &stbuf, nullptr))){ + return result; + } + + // [NOTE] + // If there is no mount point(directory object) that s3fs can recognize, + // an error will occur. + // A mount point with a directory path(ex. "/...") + // requires that directory object. + // If the directory or object is created by a client other than s3fs, + // s3fs may not be able to recognize it. If you specify such a directory + // as a mount point, you can avoid the error by starting with "compat_dir" + // specified. + // + if(!compat_dir && !pHasMpStat->Get()){ + return -ENOENT; + } + return 0; +} + +// +// Check & Set attributes for mount point. +// +static bool set_mountpoint_attribute(struct stat& mpst) +{ + mp_uid = geteuid(); + mp_gid = getegid(); + mp_mode = S_IFDIR | (allow_other ? (is_mp_umask ? (~mp_umask & (S_IRWXU | S_IRWXG | S_IRWXO)) : (S_IRWXU | S_IRWXG | S_IRWXO)) : S_IRWXU); + +// In MSYS2 environment with WinFsp, it is not supported to change mode of mount point. +// Doing that forcely will occurs permission problem, so disabling it. +#ifdef __MSYS__ + return true; +#else + S3FS_PRN_INFO2("PROC(uid=%u, gid=%u) - MountPoint(uid=%u, gid=%u, mode=%04o)", + (unsigned int)mp_uid, (unsigned int)mp_gid, (unsigned int)(mpst.st_uid), (unsigned int)(mpst.st_gid), mpst.st_mode); + + // check owner + if(0 == mp_uid || mpst.st_uid == mp_uid){ + return true; + } + // check group permission + if(mpst.st_gid == mp_gid || 1 == is_uid_include_group(mp_uid, mpst.st_gid)){ + if(S_IRWXG == (mpst.st_mode & S_IRWXG)){ + return true; + } + } + // check other permission + if(S_IRWXO == (mpst.st_mode & S_IRWXO)){ + return true; + } + return false; +#endif +} + +// +// Set bucket and mount_prefix based on passed bucket name. +// +static int set_bucket(const char* arg) +{ + // TODO: Mutates input. Consider some other tokenization. + char *bucket_name = const_cast(arg); + if(strstr(arg, ":")){ + if(strstr(arg, "://")){ + S3FS_PRN_EXIT("bucket name and path(\"%s\") is wrong, it must be \"bucket[:/path]\".", arg); + return -1; + } + if(!S3fsCred::SetBucket(strtok(bucket_name, ":"))){ + S3FS_PRN_EXIT("bucket name and path(\"%s\") is wrong, it must be \"bucket[:/path]\".", arg); + return -1; + } + char* pmount_prefix = strtok(nullptr, ""); + if(pmount_prefix){ + if(0 == strlen(pmount_prefix) || '/' != pmount_prefix[0]){ + S3FS_PRN_EXIT("path(%s) must be prefix \"/\".", pmount_prefix); + return -1; + } + mount_prefix = pmount_prefix; + // Trim the last consecutive '/' + mount_prefix = trim_right(mount_prefix, "/"); + } + }else{ + if(!S3fsCred::SetBucket(arg)){ + S3FS_PRN_EXIT("bucket name and path(\"%s\") is wrong, it must be \"bucket[:/path]\".", arg); + return -1; + } + } + return 0; +} + +static int print_umount_message(const std::string& mp, bool force) +{ + std::string cmd; + if (is_cmd_exists("fusermount")){ + if (force){ + cmd = "fusermount -uz " + mp; + } else { + cmd = "fusermount -u " + mp; + } + }else{ + if (force){ + cmd = "umount -l " + mp; + } else { + cmd = "umount " + mp; + } + } + + S3FS_PRN_EXIT("MOUNTPOINT %s is stale, you could use this command to fix: %s", mp.c_str(), cmd.c_str()); + + return 0; +} + +static bool is_cmd_exists(const std::string& command) +{ + // The `command -v` is a POSIX-compliant method for checking the existence of a program. + std::string cmd = "command -v " + command + " >/dev/null 2>&1"; + int result = system(cmd.c_str()); + return (result !=-1 && WIFEXITED(result) && WEXITSTATUS(result) == 0); +} + +static int update_mctime_parent_directory(const char* _path) +{ + if(!update_parent_dir_stat){ + // Disable updating parent directory stat. + S3FS_PRN_DBG("Updating parent directory stats is disabled"); + return 0; + } + + WTF8_ENCODE(path) + int result; + std::string parentpath; // parent directory path + std::string nowpath; // now directory object path("dir" or "dir/" or "xxx_$folder$", etc) + std::string newpath; // directory path for the current version("dir/") + std::string nowcache; + headers_t meta; + struct stat stbuf; + struct timespec mctime; + struct timespec atime; + dirtype nDirType = dirtype::UNKNOWN; + + S3FS_PRN_INFO2("[path=%s]", path); + + // get parent directory path + parentpath = mydirname(path); + + // check & get directory type + if(0 != (result = chk_dir_object_type(parentpath.c_str(), newpath, nowpath, nowcache, &meta, &nDirType))){ + return result; + } + + // get directory stat + // + // [NOTE] + // It is assumed that this function is called after the operation on + // the file is completed, so there is no need to check the permissions + // on the parent directory. + // + if(0 != (result = get_object_attribute(parentpath.c_str(), &stbuf))){ + // If there is not the target file(object), result is -ENOENT. + return result; + } + if(!S_ISDIR(stbuf.st_mode)){ + S3FS_PRN_ERR("path(%s) is not parent directory.", parentpath.c_str()); + return -EIO; + } + + // make atime/mtime/ctime for updating + s3fs_realtime(mctime); + set_stat_to_timespec(stbuf, stat_time_type::ATIME, atime); + + if(0 == atime.tv_sec && 0 == atime.tv_nsec){ + atime = mctime; + } + + if(nocopyapi || IS_REPLACEDIR(nDirType) || IS_CREATE_MP_STAT(parentpath.c_str())){ + // Should rebuild directory object(except new type) + // Need to remove old dir("dir" etc) and make new dir("dir/") + std::string xattrvalue; + const char* pxattrvalue; + if(get_meta_xattr_value(path, xattrvalue)){ + pxattrvalue = xattrvalue.c_str(); + }else{ + pxattrvalue = nullptr; + } + + // At first, remove directory old object + if(!nowpath.empty()){ + if(0 != (result = remove_old_type_dir(nowpath, nDirType))){ + return result; + } + } + if(!nowcache.empty()){ + StatCache::getStatCacheData()->DelStat(nowcache); + } + + // Make new directory object("dir/") + if(0 != (result = create_directory_object(newpath.c_str(), stbuf.st_mode, atime, mctime, mctime, stbuf.st_uid, stbuf.st_gid, pxattrvalue))){ + return result; + } + }else{ + std::string strSourcePath = (mount_prefix.empty() && "/" == nowpath) ? "//" : nowpath; + headers_t updatemeta; + updatemeta["x-amz-meta-mtime"] = str(mctime); + updatemeta["x-amz-meta-ctime"] = str(mctime); + updatemeta["x-amz-meta-atime"] = str(atime); + updatemeta["x-amz-copy-source"] = urlEncodePath(service_path + S3fsCred::GetBucket() + get_realpath(strSourcePath.c_str())); + updatemeta["x-amz-metadata-directive"] = "REPLACE"; + + merge_headers(meta, updatemeta, true); + + // upload meta for parent directory. + if(0 != (result = put_headers(nowpath.c_str(), meta, true))){ + return result; + } + StatCache::getStatCacheData()->DelStat(nowcache); + } + S3FS_MALLOCTRIM(0); + + return 0; +} + +static int create_directory_object(const char* path, mode_t mode, const struct timespec& ts_atime, const struct timespec& ts_mtime, const struct timespec& ts_ctime, uid_t uid, gid_t gid, const char* pxattrvalue) +{ + S3FS_PRN_INFO1("[path=%s][mode=%04o][atime=%s][mtime=%s][ctime=%s][uid=%u][gid=%u]", path, mode, str(ts_atime).c_str(), str(ts_mtime).c_str(), str(ts_ctime).c_str(), (unsigned int)uid, (unsigned int)gid); + + if(!path || '\0' == path[0]){ + return -EINVAL; + } + std::string tpath = path; + if('/' != *tpath.rbegin()){ + tpath += "/"; + }else if("/" == tpath && mount_prefix.empty()){ + tpath = "//"; // for the mount point that is bucket root, change "/" to "//". + } + + headers_t meta; + meta["x-amz-meta-uid"] = std::to_string(uid); + meta["x-amz-meta-gid"] = std::to_string(gid); + meta["x-amz-meta-mode"] = std::to_string(mode); + meta["x-amz-meta-atime"] = str(ts_atime); + meta["x-amz-meta-mtime"] = str(ts_mtime); + meta["x-amz-meta-ctime"] = str(ts_ctime); + + if(pxattrvalue){ + S3FS_PRN_DBG("Set xattrs = %s", urlDecode(pxattrvalue).c_str()); + meta["x-amz-meta-xattr"] = pxattrvalue; + } + + S3fsCurl s3fscurl; + return s3fscurl.PutRequest(tpath.c_str(), meta, -1); // fd=-1 means for creating zero byte object. +} + +// [NOTE] +// Converts and returns the POSIX ACL default(system.posix_acl_default) value of +// the parent directory as a POSIX ACL(system.posix_acl_access) value. +// Returns false if the parent directory has no POSIX ACL defaults. +// +static bool build_inherited_xattr_value(const char* path, std::string& xattrvalue) +{ + S3FS_PRN_DBG("[path=%s]", path); + + xattrvalue.clear(); + + if(0 == strcmp(path, "/") || 0 == strcmp(path, ".")){ + // path is mount point, thus does not have parent. + return false; + } + + std::string parent = mydirname(path); + if(parent.empty()){ + S3FS_PRN_ERR("Could not get parent path for %s.", path); + return false; + } + + // get parent's "system.posix_acl_default" value(base64'd). + std::string parent_default_value; + if(!get_xattr_posix_key_value(parent.c_str(), parent_default_value, true)){ + return false; + } + + // build "system.posix_acl_access" from parent's default value + std::string raw_xattr_value; + raw_xattr_value = "{\"system.posix_acl_access\":\""; + raw_xattr_value += parent_default_value; + raw_xattr_value += "\"}"; + + xattrvalue = urlEncodePath(raw_xattr_value); + return true; +} + +static bool get_xattr_posix_key_value(const char* path, std::string& xattrvalue, bool default_key) +{ + xattrvalue.clear(); + + std::string rawvalue; + if(!get_meta_xattr_value(path, rawvalue)){ + return false; + } + + xattrs_t xattrs; + if(0 == parse_xattrs(rawvalue, xattrs)){ + return false; + } + + std::string targetkey; + if(default_key){ + targetkey = "system.posix_acl_default"; + }else{ + targetkey = "system.posix_acl_access"; + } + + xattrs_t::iterator iter; + if(xattrs.end() == (iter = xattrs.find(targetkey))){ + return false; + } + + // convert value by base64 + xattrvalue = s3fs_base64(reinterpret_cast(iter->second.c_str()), iter->second.length()); + + return true; +} + +static bool get_meta_xattr_value(const char* path, std::string& rawvalue) +{ + if(!path || '\0' == path[0]){ + S3FS_PRN_ERR("path is empty."); + return false; + } + S3FS_PRN_DBG("[path=%s]", path); + + rawvalue.clear(); + + headers_t meta; + if(0 != get_object_attribute(path, nullptr, &meta)){ + S3FS_PRN_ERR("Failed to get object(%s) headers", path); + return false; + } + + headers_t::const_iterator iter; + if(meta.end() == (iter = meta.find("x-amz-meta-xattr"))){ + return false; + } + rawvalue = iter->second; + return true; +} + +static size_t parse_xattrs(const std::string& strxattrs, xattrs_t& xattrs) +{ + xattrs.clear(); + + // decode + std::string jsonxattrs = urlDecode(strxattrs); + + // get from "{" to "}" + std::string restxattrs; + { + size_t startpos; + size_t endpos = std::string::npos; + if(std::string::npos != (startpos = jsonxattrs.find_first_of('{'))){ + endpos = jsonxattrs.find_last_of('}'); + } + if(startpos == std::string::npos || endpos == std::string::npos || endpos <= startpos){ + S3FS_PRN_WARN("xattr header(%s) is not json format.", jsonxattrs.c_str()); + return 0; + } + restxattrs = jsonxattrs.substr(startpos + 1, endpos - (startpos + 1)); + } + + // parse each key:val + for(size_t pair_nextpos = restxattrs.find_first_of(','); !restxattrs.empty(); restxattrs = (pair_nextpos != std::string::npos ? restxattrs.substr(pair_nextpos + 1) : ""), pair_nextpos = restxattrs.find_first_of(',')){ + std::string pair = pair_nextpos != std::string::npos ? restxattrs.substr(0, pair_nextpos) : restxattrs; + std::string key; + std::string val; + if(!parse_xattr_keyval(pair, key, &val)){ + // something format error, so skip this. + continue; + } + xattrs[key] = val; + } + return xattrs.size(); +} + + +static bool parse_xattr_keyval(const std::string& xattrpair, std::string& key, std::string* pval) +{ + // parse key and value + size_t pos; + std::string tmpval; + if(std::string::npos == (pos = xattrpair.find_first_of(':'))){ + S3FS_PRN_ERR("one of xattr pair(%s) is wrong format.", xattrpair.c_str()); + return false; + } + key = xattrpair.substr(0, pos); + tmpval = xattrpair.substr(pos + 1); + + if(!takeout_str_dquart(key) || !takeout_str_dquart(tmpval)){ + S3FS_PRN_ERR("one of xattr pair(%s) is wrong format.", xattrpair.c_str()); + return false; + } + + *pval = s3fs_decode64(tmpval.c_str(), tmpval.size()); + + return true; +} + +static bool get_parent_meta_xattr_value(const char* path, std::string& rawvalue) +{ + if(0 == strcmp(path, "/") || 0 == strcmp(path, ".")){ + // path is mount point, thus does not have parent. + return false; + } + + std::string parent = mydirname(path); + if(parent.empty()){ + S3FS_PRN_ERR("Could not get parent path for %s.", path); + return false; + } + return get_meta_xattr_value(parent.c_str(), rawvalue); +} + +struct multi_head_notfound_callback_param +{ + pthread_mutex_t list_lock; + s3obj_list_t notfound_list; +}; + +int posix_s3fs_create(const char* _path, int flags, mode_t mode) { + WTF8_ENCODE(path) + int result; + + S3FS_PRN_INFO("craete file [path=%s][mode=%04o][flags=0x%x]", path, mode, flags); + + // check parent directory attribute. + if(0 != (result = check_parent_object_access(path, X_OK))){ + return result; + } + struct stat statbuf; + memset(&statbuf, 0, sizeof(struct stat)); + result = check_object_access(path, W_OK, &statbuf); + if (statbuf.st_size > 0) { + // 文件已经存在,打开即可 + return -EEXIST; + } + if(-ENOENT == result){ + if(0 != (result = check_parent_object_access(path, W_OK))){ + return result; + } + }else if(0 != result){ + return result; + } + + std::string strnow = s3fs_str_realtime(); + headers_t meta; + meta["Content-Length"] = "0"; + meta["x-amz-meta-uid"] = std::to_string(posixcontext.uid); + meta["x-amz-meta-gid"] = std::to_string(posixcontext.gid); + meta["x-amz-meta-mode"] = std::to_string(mode); + meta["x-amz-meta-atime"] = strnow; + meta["x-amz-meta-mtime"] = strnow; + meta["x-amz-meta-ctime"] = strnow; + + std::string xattrvalue; + if(build_inherited_xattr_value(path, xattrvalue)){ + S3FS_PRN_DBG("Set xattrs = %s", urlDecode(xattrvalue).c_str()); + meta["x-amz-meta-xattr"] = xattrvalue; + } + + // [NOTE] set no_truncate flag + // At this point, the file has not been created(uploaded) and + // the data is only present in the Stats cache. + // The Stats cache should not be deleted automatically by + // timeout. If this stats is deleted, s3fs will try to get it + // from the server with a Head request and will get an + // unexpected error because the result object does not exist. + // + if(!StatCache::getStatCacheData()->AddStat(path, meta, false, true)){ + return -EIO; + } + + AutoFdEntity autoent; + FdEntity* ent; + int error = 0; + if(nullptr == (ent = autoent.Open(path, &meta, 0, S3FS_OMIT_TS, flags, false, true, false, AutoLock::NONE, &error))){ + StatCache::getStatCacheData()->DelStat(path); + return error; + } + ent->MarkDirtyNewFile(); + int fd = autoent.Detach(); // KEEP fdentity open; + + S3FS_MALLOCTRIM(0); + if (fd > 0) { + PosixS3Info info; + info.fileinfo.fd = fd; + info.fileinfo.flags = flags; + //info.fileinfo.read_offset = 0; + //info.fileinfo.write_offset = 0; + info.fileinfo.offset = 0; + info.filename = path; + fdtofile[fd] = info; + } + return fd; +} + +int posix_s3fs_open(const char* _path, int flags, mode_t mode) +{ + if (flags & O_CREAT) { + int ret = posix_s3fs_create(_path, flags, mode); + if (ret != -EEXIST) { + return ret; + } + } + WTF8_ENCODE(path) + int result; + struct stat st; + bool needs_flush = false; + + S3FS_PRN_INFO("[path=%s][flags=0x%x]", path, flags); + + if ((flags & O_ACCMODE) == O_RDONLY && flags & O_TRUNC) { + return -EACCES; + } + + // [NOTE] + // Delete the Stats cache only if the file is not open. + // If the file is open, the stats cache will not be deleted as + // there are cases where the object does not exist on the server + // and only the Stats cache exists. + // + if(StatCache::getStatCacheData()->HasStat(path)){ + if(!FdManager::HasOpenEntityFd(path)){ + StatCache::getStatCacheData()->DelStat(path); + } + } + + int mask = (O_RDONLY != (flags & O_ACCMODE) ? W_OK : R_OK); + if(0 != (result = check_parent_object_access(path, X_OK))){ + return result; + } + + result = check_object_access(path, mask, &st); + if(-ENOENT == result){ + if(0 != (result = check_parent_object_access(path, W_OK))){ + return result; + } + }else if(0 != result){ + return result; + } + + AutoFdEntity autoent; + FdEntity* ent; + headers_t meta; + + if((unsigned int)flags & O_TRUNC){ + if(0 != st.st_size){ + st.st_size = 0; + needs_flush = true; + } + }else{ + // [NOTE] + // If the file has already been opened and edited, the file size in + // the edited state is given priority. + // This prevents the file size from being reset to its original size + // if you keep the file open, shrink its size, and then read the file + // from another process while it has not yet been flushed. + // + if(nullptr != (ent = autoent.OpenExistFdEntity(path)) && ent->IsModified()){ + // sets the file size being edited. + ent->GetSize(st.st_size); + } + } + if(!S_ISREG(st.st_mode) || S_ISLNK(st.st_mode)){ + st.st_mtime = -1; + } + + if(0 != (result = get_object_attribute(path, nullptr, &meta, true, nullptr, true))){ // no truncate cache + return result; + } + + struct timespec st_mctime; + set_stat_to_timespec(st, stat_time_type::MTIME, st_mctime); + + if(nullptr == (ent = autoent.Open(path, &meta, st.st_size, st_mctime, flags, false, true, false, AutoLock::NONE))){ + StatCache::getStatCacheData()->DelStat(path); + return -EIO; + } + + if (needs_flush){ + struct timespec ts; + s3fs_realtime(ts); + ent->SetMCtime(ts, ts); + + if(0 != (result = ent->RowFlush(autoent.GetPseudoFd(), path, AutoLock::NONE, true))){ + S3FS_PRN_ERR("could not upload file(%s): result=%d", path, result); + StatCache::getStatCacheData()->DelStat(path); + return result; + } + } + int fd = autoent.Detach(); // KEEP fdentity open; + S3FS_MALLOCTRIM(0); + if (fd > 0) { + PosixS3Info info; + info.fileinfo.fd = fd; + info.fileinfo.flags = flags; + //info.fileinfo.read_offset = 0; + //info.fileinfo.write_offset = 0; + info.fileinfo.offset = 0; + info.filename = path; + info.type = FileType::FILE; + //info.fileinfo.ino = ent->GetInode(); + info.fileinfo.ino = fd;// 暂时赋值为fd + fdtofile[fd] = info; + } + return fd; +} + +int posix_s3fs_multiread(int fd, void* buf, size_t size, off_t file_offset) { + //WTF8_ENCODE(path) + S3FS_PRN_INFO("read [pseudo_fd=%llu]", (unsigned long long)fd); + if (fdtofile.find(fd) == fdtofile.end()) { + S3FS_PRN_ERR("readop could not find opened pseudo_fd(=%llu) ", (unsigned long long)(fd)); + return -EIO; + } + auto& info = fdtofile[fd]; + const char* path = info.filename.c_str(); + ssize_t res; + + // ! 注意这个偏移 + //off_t offset = info.fileinfo.read_offset; + off_t offset = info.fileinfo.offset + file_offset; + S3FS_PRN_INFO("[path=%s][size=%zu][offset=%lld][pseudo_fd=%llu]", path, size, (long long)offset, (unsigned long long)fd); + + AutoFdEntity autoent; + FdEntity* ent; + if(nullptr == (ent = autoent.GetExistFdEntity(path, fd))){ + S3FS_PRN_ERR("could not find opened pseudo_fd(=%llu) for path(%s)", (unsigned long long)(fd), path); + return -EIO; + } + + // check real file size + off_t realsize = 0; + if(!ent->GetSize(realsize) || 0 == realsize){ + S3FS_PRN_DBG("file size is 0, so break to read."); + return 0; + } + + if(0 > (res = ent->Read(fd, (char*)buf, offset, size, false))){ + S3FS_PRN_WARN("failed to read file(%s). result=%zd", path, res); + } + // 不更新offset 在调用层统一更新 + // if(0 < res){ + // info.fileinfo.offset += res; + // } + return static_cast(res); +} + +int posix_s3fs_read(int fd, void* buf, size_t size) +{ + //WTF8_ENCODE(path) + S3FS_PRN_INFO("read [pseudo_fd=%llu]", (unsigned long long)fd); + if (fdtofile.find(fd) == fdtofile.end()) { + S3FS_PRN_ERR("readop could not find opened pseudo_fd(=%llu) ", (unsigned long long)(fd)); + return -EIO; + } + auto& info = fdtofile[fd]; + const char* path = info.filename.c_str(); + ssize_t res; + + // ! 注意这个偏移 + //off_t offset = info.fileinfo.read_offset; + off_t offset = info.fileinfo.offset; + S3FS_PRN_INFO("[path=%s][size=%zu][offset=%lld][pseudo_fd=%llu]", path, size, (long long)offset, (unsigned long long)fd); + + AutoFdEntity autoent; + FdEntity* ent; + if(nullptr == (ent = autoent.GetExistFdEntity(path, fd))){ + S3FS_PRN_ERR("could not find opened pseudo_fd(=%llu) for path(%s)", (unsigned long long)(fd), path); + return -EIO; + } + + // check real file size + off_t realsize = 0; + if(!ent->GetSize(realsize) || 0 == realsize){ + S3FS_PRN_DBG("file size is 0, so break to read."); + return 0; + } + + if(0 > (res = ent->Read(fd, (char*)buf, offset, size, false))){ + S3FS_PRN_WARN("failed to read file(%s). result=%zd", path, res); + } + if(0 < res){ + //info.fileinfo.read_offset += res; + info.fileinfo.offset += res; + } + return static_cast(res); +} + +int posix_s3fs_multiwrite(int fd, const void* buf, size_t size, off_t file_offset) { + S3FS_PRN_INFO("multithread write [pseudo_fd=%llu]", (unsigned long long)fd); + if (fdtofile.find(fd) == fdtofile.end()) { + S3FS_PRN_ERR("writeop could not find opened pseudo_fd(=%llu) ", (unsigned long long)(fd)); + return -EIO; + } + auto& info = fdtofile[fd]; + const char* path = info.filename.c_str(); + //uint64_t offset = info.fileinfo.write_offset; + //uint64_t offset = info.fileinfo.offset; + uint64_t offset = info.fileinfo.offset + file_offset; + ssize_t res; + + S3FS_PRN_DBG("multiwrite [path=%s][size=%zu][offset=%lld][pseudo_fd=%llu]", path, size, static_cast(offset), (unsigned long long)(fd)); + + AutoFdEntity autoent; + FdEntity* ent; + if(nullptr == (ent = autoent.GetExistFdEntity(path, static_cast(fd)))){ + S3FS_PRN_ERR("could not find opened pseudo_fd(%llu) for path(%s)", (unsigned long long)(fd), path); + return -EIO; + } + + if(0 > (res = ent->Write(static_cast(fd), (const char*)buf, offset, size))){ + S3FS_PRN_WARN("failed to write file(%s). result=%zd", path, res); + } + + if(max_dirty_data != -1 && ent->BytesModified() >= max_dirty_data && !use_newcache){ + int flushres; + if(0 != (flushres = ent->RowFlush(static_cast(fd), path, AutoLock::NONE, true))){ + S3FS_PRN_ERR("could not upload file(%s): result=%d", path, flushres); + StatCache::getStatCacheData()->DelStat(path); + return flushres; + } + // Punch a hole in the file to recover disk space. + if(!ent->PunchHole()){ + S3FS_PRN_WARN("could not punching HOLEs to a cache file, but continue."); + } + } + // 不更新offset 在调用层统一更新 + // if (0 < res) { + // //info.fileinfo.write_offset += res; + // info.fileinfo.offset += res; + // } + return static_cast(res); +} + + +int posix_s3fs_write(int fd, const void* buf, size_t size) { + S3FS_PRN_INFO("write [pseudo_fd=%llu]", (unsigned long long)fd); + if (fdtofile.find(fd) == fdtofile.end()) { + S3FS_PRN_ERR("writeop could not find opened pseudo_fd(=%llu) ", (unsigned long long)(fd)); + return -EIO; + } + auto& info = fdtofile[fd]; + const char* path = info.filename.c_str(); + //uint64_t offset = info.fileinfo.write_offset; + uint64_t offset = info.fileinfo.offset; + ssize_t res; + + S3FS_PRN_DBG("[path=%s][size=%zu][offset=%lld][pseudo_fd=%llu]", path, size, static_cast(offset), (unsigned long long)(fd)); + + AutoFdEntity autoent; + FdEntity* ent; + if(nullptr == (ent = autoent.GetExistFdEntity(path, static_cast(fd)))){ + S3FS_PRN_ERR("could not find opened pseudo_fd(%llu) for path(%s)", (unsigned long long)(fd), path); + return -EIO; + } + + if(0 > (res = ent->Write(static_cast(fd), (const char*)buf, offset, size))){ + S3FS_PRN_WARN("failed to write file(%s). result=%zd", path, res); + } + + if(max_dirty_data != -1 && ent->BytesModified() >= max_dirty_data && !use_newcache){ + int flushres; + if(0 != (flushres = ent->RowFlush(static_cast(fd), path, AutoLock::NONE, true))){ + S3FS_PRN_ERR("could not upload file(%s): result=%d", path, flushres); + StatCache::getStatCacheData()->DelStat(path); + return flushres; + } + // Punch a hole in the file to recover disk space. + if(!ent->PunchHole()){ + S3FS_PRN_WARN("could not punching HOLEs to a cache file, but continue."); + } + } + if (0 < res) { + //info.fileinfo.write_offset += res; + info.fileinfo.offset += res; + } + return static_cast(res); +} + +off_t posix_s3fs_lseek(int fd, off_t offset, int whence) { + S3FS_PRN_INFO("lseek [pseudo_fd=%llu, offset=%llu, whence=%d]", (unsigned long long)fd, offset, whence); + if (fdtofile.find(fd) == fdtofile.end()) { + S3FS_PRN_ERR("lseekop could not find opened pseudo_fd(=%llu) ", (unsigned long long)(fd)); + return -EIO; + } + auto& info = fdtofile[fd]; + long new_pos = -1; + + FdEntity* ent = nullptr; + //ent = FdManager::get()->GetFdEntity(info.filename.c_str(), fd, false, AutoLock::ALREADY_LOCKED); + ent = FdManager::get()->GetFdEntity(info.filename.c_str(), fd, false, AutoLock::NONE); + if (ent == nullptr) { + S3FS_PRN_ERR("get stat failed in lseek...."); + return -1; + } + struct stat st; + ent->GetStats(st); + + switch (whence) { + case SEEK_SET: + new_pos = offset; + break; + case SEEK_CUR: + new_pos = info.fileinfo.offset + offset; + break; + case SEEK_END: + new_pos = st.st_size + offset; + break; + default: + errno = EINVAL; + return -1; + } + S3FS_PRN_INFO("lseek , filesize[%d], newpos[%d]", st.st_size, new_pos); + + // if (new_pos < 0 || new_pos > file->size) { + if (new_pos < 0) { + errno = EINVAL; + S3FS_PRN_ERR("lseek wrong new_pos, new_pos[%d]", new_pos); + return -1; + } + info.fileinfo.offset = new_pos; + return new_pos; +} + +int posix_s3fs_close(int fd) { + S3FS_PRN_INFO("close [pseudo_fd=%llu]", (unsigned long long)fd); + if (fdtofile.find(fd) == fdtofile.end()) { + S3FS_PRN_ERR("could not find opened pseudo_fd(=%llu) ", (unsigned long long)(fd)); + return -EIO; + } + const auto& info = fdtofile[fd]; + const char* path = info.filename.c_str(); + { // scope for AutoFdEntity + AutoFdEntity autoent; + + // [NOTE] + // The pseudo fd stored in fi->fh is attached to AutoFdEntry so that it can be + // destroyed here. + // + FdEntity* ent; + if(nullptr == (ent = autoent.Attach(path, static_cast(fd)))){ + S3FS_PRN_ERR("could not find pseudo_fd(%llu) for path(%s)", (unsigned long long)(fd), path); + return -EIO; + } + + // [NOTE] + // There are cases when s3fs_flush is not called and s3fs_release is called. + // (There have been reported cases where it is not called when exported as NFS.) + // Therefore, Flush() is called here to try to upload the data. + // Flush() will only perform an upload if the file has been updated. + // + int result; + if(ent->IsModified()){ + if(0 != (result = ent->Flush(static_cast(fd), AutoLock::NONE, false))){ + S3FS_PRN_ERR("failed to upload file contentsfor pseudo_fd(%llu) / path(%s) by result(%d)", (unsigned long long)(fd), path, result); + return result; + } + } + + // [NOTE] + // All opened file's stats is cached with no truncate flag. + // Thus we unset it here. + StatCache::getStatCacheData()->ChangeNoTruncateFlag(path, false); + + // [NOTICE] + // At first, we remove stats cache. + // Because fuse does not wait for response from "release" function. :-( + // And fuse runs next command before this function returns. + // Thus we call deleting stats function ASAP. + // + if((info.fileinfo.flags & O_RDWR) || (info.fileinfo.flags & O_WRONLY)){ + StatCache::getStatCacheData()->DelStat(path); + } + + bool is_new_file = ent->IsDirtyNewFile(); + + if(0 != (result = ent->UploadPending(static_cast(fd), AutoLock::NONE))){ + S3FS_PRN_ERR("could not upload pending data(meta, etc) for pseudo_fd(%llu) / path(%s)", (unsigned long long)(fd), path); + return result; + } + + if(is_new_file){ + // update parent directory timestamp + int update_result; + if(0 != (update_result = update_mctime_parent_directory(path))){ + S3FS_PRN_ERR("succeed to create the file(%s), but could not update timestamp of its parent directory(result=%d).", path, update_result); + } + } + } + + // check - for debug + if(S3fsLog::IsS3fsLogDbg()){ + if(FdManager::HasOpenEntityFd(path)){ + S3FS_PRN_DBG("file(%s) is still opened(another pseudo fd is opend).", path); + } + } + S3FS_MALLOCTRIM(0); + fdtofile.erase(fd); + return 0; +} + +int posix_s3fs_stat(const char* _path, struct stat* stbuf) { + WTF8_ENCODE(path) + int result; + +#if defined(__APPLE__) + S3FS_PRN_INFO("stat [path=%s]", path); +#else + S3FS_PRN_INFO("stat [path=%s]", path); +#endif + + // check parent directory attribute. + if(0 != (result = check_parent_object_access(path, X_OK))){ + return result; + } + if(0 != (result = check_object_access(path, F_OK, stbuf))){ + return result; + } + // If has already opened fd, the st_size should be instead. + // (See: Issue 241) + if(stbuf){ + AutoFdEntity autoent; + const FdEntity* ent; + if(nullptr != (ent = autoent.OpenExistFdEntity(path))){ + struct stat tmpstbuf; + if(ent->GetStats(tmpstbuf)){ + stbuf->st_size = tmpstbuf.st_size; + } + } + if(0 == strcmp(path, "/")){ + stbuf->st_size = 4096; + } + stbuf->st_blksize = 4096; + stbuf->st_blocks = get_blocks(stbuf->st_size); + + S3FS_PRN_DBG("stat [path=%s] uid=%u, gid=%u, mode=%04o", path, (unsigned int)(stbuf->st_uid), (unsigned int)(stbuf->st_gid), stbuf->st_mode); + } + S3FS_MALLOCTRIM(0); + + return result; +} + + +int posix_s3fs_fstat(int fd, struct stat* stbuf) { + // sleep(3); + const char* path = fdtofile[fd].filename.c_str(); + return posix_s3fs_stat(path, stbuf); +} + +int posix_s3fs_mkdir(const char* _path, mode_t mode) +{ + WTF8_ENCODE(path) + int result; + + S3FS_PRN_INFO("mkdir [path=%s][mode=%04o]", path, mode); + + // check parent directory attribute. + if(0 != (result = check_parent_object_access(path, W_OK | X_OK))){ + return result; + } + if(-ENOENT != (result = check_object_access(path, F_OK, nullptr))){ + if(0 == result){ + result = -EEXIST; + } + return result; + } + + std::string xattrvalue; + const char* pxattrvalue; + if(get_parent_meta_xattr_value(path, xattrvalue)){ + pxattrvalue = xattrvalue.c_str(); + }else{ + pxattrvalue = nullptr; + } + + struct timespec now; + s3fs_realtime(now); + result = create_directory_object(path, mode, now, now, now, posixcontext.uid, posixcontext.gid, pxattrvalue); + + StatCache::getStatCacheData()->DelStat(path); + + // update parent directory timestamp + int update_result; + if(0 != (update_result = update_mctime_parent_directory(path))){ + S3FS_PRN_ERR("succeed to create the directory(%s), but could not update timestamp of its parent directory(result=%d).", path, update_result); + } + + S3FS_MALLOCTRIM(0); + + return result; +} + +int posix_s3fs_opendir(const char* _path, S3DirStream* dirstream) { + int flags = O_DIRECTORY; + int mode = 0777; + int ret = posix_s3fs_open(_path, flags, mode); + fdtofile[ret].type = FileType::DIR; + fdtofile[ret].dirinfo.fh = ret; + fdtofile[ret].dirinfo.offset = 0; + dirstream->fh = ret; + dirstream->offset = 0; + dirstream->ino = fdtofile[ret].fileinfo.ino; + return ret; +} + +// cppcheck-suppress unmatchedSuppression +// cppcheck-suppress constParameterCallback +static bool multi_head_callback(S3fsCurl* s3fscurl, void* param) +{ + if(!s3fscurl){ + return false; + } + + // Add stat cache + std::string saved_path = s3fscurl->GetSpecialSavedPath(); + if(!StatCache::getStatCacheData()->AddStat(saved_path, *(s3fscurl->GetResponseHeaders()))){ + S3FS_PRN_ERR("failed adding stat cache [path=%s]", saved_path.c_str()); + return false; + } + + // Get stats from stats cache(for converting from meta), and fill + std::string bpath = mybasename(saved_path); + if(use_wtf8){ + bpath = s3fs_wtf8_decode(bpath); + } + if(param){ + SyncFiller* pcbparam = reinterpret_cast(param); + struct stat st; + if(StatCache::getStatCacheData()->GetStat(saved_path, &st)){ + pcbparam->Fill(bpath.c_str(), &st, 0); + }else{ + S3FS_PRN_INFO2("Could not find %s file in stat cache.", saved_path.c_str()); + pcbparam->Fill(bpath.c_str(), nullptr, 0); + } + }else{ + S3FS_PRN_WARN("param(multi_head_callback_param*) is nullptr, then can not call filler."); + } + + return true; +} + + +static bool multi_head_notfound_callback(S3fsCurl* s3fscurl, void* param) +{ + if(!s3fscurl){ + return false; + } + S3FS_PRN_INFO("HEAD returned NotFound(404) for %s object, it maybe only the path exists and the object does not exist.", s3fscurl->GetPath().c_str()); + + if(!param){ + S3FS_PRN_WARN("param(multi_head_notfound_callback_param*) is nullptr, then can not call filler."); + return false; + } + + // set path to not found list + struct multi_head_notfound_callback_param* pcbparam = reinterpret_cast(param); + + AutoLock auto_lock(&(pcbparam->list_lock)); + pcbparam->notfound_list.push_back(s3fscurl->GetBasePath()); + + return true; +} + +static std::unique_ptr multi_head_retry_callback(S3fsCurl* s3fscurl) +{ + if(!s3fscurl){ + return nullptr; + } + size_t ssec_key_pos= s3fscurl->GetLastPreHeadSeecKeyPos(); + int retry_count = s3fscurl->GetMultipartRetryCount(); + + // retry next sse key. + // if end of sse key, set retry master count is up. + ssec_key_pos = (ssec_key_pos == static_cast(-1) ? 0 : ssec_key_pos + 1); + if(0 == S3fsCurl::GetSseKeyCount() || S3fsCurl::GetSseKeyCount() <= ssec_key_pos){ + if(s3fscurl->IsOverMultipartRetryCount()){ + S3FS_PRN_ERR("Over retry count(%d) limit(%s).", s3fscurl->GetMultipartRetryCount(), s3fscurl->GetSpecialSavedPath().c_str()); + return nullptr; + } + ssec_key_pos = -1; + retry_count++; + } + + std::unique_ptr newcurl(new S3fsCurl(s3fscurl->IsUseAhbe())); + std::string path = s3fscurl->GetBasePath(); + std::string base_path = s3fscurl->GetBasePath(); + std::string saved_path = s3fscurl->GetSpecialSavedPath(); + + if(!newcurl->PreHeadRequest(path, base_path, saved_path, ssec_key_pos)){ + S3FS_PRN_ERR("Could not duplicate curl object(%s).", saved_path.c_str()); + return nullptr; + } + newcurl->SetMultipartRetryCount(retry_count); + + return newcurl; +} + + +static int readdir_multi_head(const char* path, const S3ObjList& head, char* data, int offset, int maxread, ssize_t* realbytes, int* realnum) +{ //TODO : for newcache + + S3fsMultiCurl curlmulti(S3fsCurl::GetMaxMultiRequest(), true); // [NOTE] run all requests to completion even if some requests fail. + s3obj_list_t headlist; + int result = 0; + *realnum = 0; + + S3FS_PRN_INFO1("readdir_multi_head [path=%s][list=%zu]", path, headlist.size()); + + // Make base path list. + head.GetNameList(headlist, true, false); // get name with "/". + StatCache::getStatCacheData()->GetNotruncateCache(std::string(path), headlist); // Add notruncate file name from stat cache + + // Initialize S3fsMultiCurl + curlmulti.SetSuccessCallback(multi_head_callback); + curlmulti.SetRetryCallback(multi_head_retry_callback); + + // Success Callback function parameter(SyncFiller object) + // SyncFiller syncfiller(buf, filler); + // curlmulti.SetSuccessCallbackParam(reinterpret_cast(&syncfiller)); + + // Not found Callback function parameter + struct multi_head_notfound_callback_param notfound_param; + if(support_compat_dir){ + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); + #if S3FS_PTHREAD_ERRORCHECK + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK); + #endif + + if(0 != (result = pthread_mutex_init(&(notfound_param.list_lock), &attr))){ + S3FS_PRN_CRIT("failed to init notfound_param.list_lock: %d", result); + abort(); + } + curlmulti.SetNotFoundCallback(multi_head_notfound_callback); + curlmulti.SetNotFoundCallbackParam(reinterpret_cast(¬found_param)); + } + + // Make single head request(with max). + int nowPos = 0; + for(s3obj_list_t::iterator iter = headlist.begin() + offset; headlist.end() != iter; ++iter){ + struct dirent64 * dirent = (struct dirent64*) (data + nowPos); + ssize_t entryLen = sizeof(dirent64); + + + std::string disppath = path + (*iter); + std::string etag = head.GetETag((*iter).c_str()); + struct stat st; + + strncpy(dirent->d_name, disppath.c_str(), sizeof(dirent->d_name)); + dirent->d_name[sizeof(dirent->d_name) - 1] = '\0'; + dirent->d_reclen = entryLen; + // TODO: stat的赋值处理 + dirent->d_ino = 999999; // 暂时将d_ino初始化为999999 + if (head.IsDir(disppath.c_str())) { + dirent->d_type = DT_DIR; + } else { + dirent->d_type = DT_REG; + + } + dirent->d_off = nowPos; + nowPos += dirent->d_reclen; + (*realnum)++; + + // [NOTE] + // If there is a cache hit, file stat is filled by filler at here. + // + if(StatCache::getStatCacheData()->HasStat(disppath, &st, etag.c_str())){ + std::string bpath = mybasename(disppath); + if(use_wtf8){ + bpath = s3fs_wtf8_decode(bpath); + } + + //syncfiller.Fill(bpath.c_str(), &st, 0); + //dirent->d_ino = st.st_ino; + continue; + } + + // First check for directory, start checking "not SSE-C". + // If checking failed, retry to check with "SSE-C" by retry callback func when SSE-C mode. + std::unique_ptr s3fscurl(new S3fsCurl()); + if(!s3fscurl->PreHeadRequest(disppath, disppath, disppath)){ // target path = cache key path.(ex "dir/") + S3FS_PRN_WARN("Could not make curl object for head request(%s).", disppath.c_str()); + continue; + } + + if(!curlmulti.SetS3fsCurlObject(std::move(s3fscurl))){ + S3FS_PRN_WARN("Could not make curl object into multi curl(%s).", disppath.c_str()); + continue; + } + } + *realbytes = nowPos; + headlist.clear(); + + // Multi request + if(0 != (result = curlmulti.Request())){ + // If result is -EIO, it is something error occurred. + // This case includes that the object is encrypting(SSE) and s3fs does not have keys. + // So s3fs set result to 0 in order to continue the process. + if(-EIO == result){ + S3FS_PRN_WARN("error occurred in multi request(errno=%d), but continue...", result); + result = 0; + }else{ + S3FS_PRN_ERR("error occurred in multi request(errno=%d).", result); + return result; + } + } + + // [NOTE] + // Objects that could not be found by HEAD request may exist only + // as a path, so search for objects under that path.(a case of no dir object) + // + if(!support_compat_dir){ + //TODO + //syncfiller.SufficiencyFill(head.common_prefixes); + } + if(support_compat_dir && !notfound_param.notfound_list.empty()){ // [NOTE] not need to lock to access this here. + // dummy header + mode_t dirmask = umask(0); // macos does not have getumask() + umask(dirmask); + + headers_t dummy_header; + dummy_header["Content-Type"] = "application/x-directory"; // directory + dummy_header["x-amz-meta-uid"] = std::to_string(is_s3fs_uid ? s3fs_uid : geteuid()); + dummy_header["x-amz-meta-gid"] = std::to_string(is_s3fs_gid ? s3fs_gid : getegid()); + dummy_header["x-amz-meta-mode"] = std::to_string(S_IFDIR | (~dirmask & (S_IRWXU | S_IRWXG | S_IRWXO))); + dummy_header["x-amz-meta-atime"] = "0"; + dummy_header["x-amz-meta-ctime"] = "0"; + dummy_header["x-amz-meta-mtime"] = "0"; + + for(s3obj_list_t::iterator reiter = notfound_param.notfound_list.begin(); reiter != notfound_param.notfound_list.end(); ++reiter){ + int dir_result; + std::string dirpath = *reiter; + if(-ENOTEMPTY == (dir_result = directory_empty(dirpath.c_str()))){ + // Found objects under the path, so the path is directory. + + // Add stat cache + if(StatCache::getStatCacheData()->AddStat(dirpath, dummy_header, true)){ // set forcedir=true + // Get stats from stats cache(for converting from meta), and fill + std::string base_path = mybasename(dirpath); + if(use_wtf8){ + base_path = s3fs_wtf8_decode(base_path); + } + + struct stat st; + if(StatCache::getStatCacheData()->GetStat(dirpath, &st)){ + // TODO + //syncfiller.Fill(base_path.c_str(), &st, 0); + }else{ + S3FS_PRN_INFO2("Could not find %s directory(no dir object) in stat cache.", dirpath.c_str()); + // TODO + //syncfiller.Fill(base_path.c_str(), nullptr, 0); + } + }else{ + S3FS_PRN_ERR("failed adding stat cache [path=%s], but dontinue...", dirpath.c_str()); + } + }else{ + S3FS_PRN_WARN("%s object does not have any object under it(errno=%d),", reiter->c_str(), dir_result); + } + } + } + + return result; +} + +int posix_s3fs_getdents(S3DirStream* dirstream, char* contents, size_t maxread, ssize_t* realbytes) { + + //WTF8_ENCODE(path) + const char* path = fdtofile[dirstream->fh].filename.c_str(); + S3ObjList head; + int result; + S3FS_PRN_INFO("getdents [path=%s]", path); + + if(0 != (result = check_object_access(path, R_OK, nullptr))){ + return result; + } + + // get a list of all the objects + if((result = list_bucket(path, head, "/")) != 0){ + S3FS_PRN_ERR("list_bucket returns error(%d).", result); + return result; + } + + if(head.IsEmpty()){ + return 0; + } + + // Send multi head request for stats caching. + std::string strpath = path; + if(strcmp(path, "/") != 0){ + strpath += "/"; + } + int readnum = 0; + if(0 != (result = readdir_multi_head(strpath.c_str(), head, contents, dirstream->offset, maxread, realbytes, &readnum))){ + S3FS_PRN_ERR("readdir_multi_head returns error(%d).", result); + } + dirstream->offset += readnum; + S3FS_PRN_DBG("the dirstream offset: %d, realbytes: %d", dirstream->offset, *realbytes); + S3FS_MALLOCTRIM(0); + + return result; +} + +int posix_s3fs_closedir(S3DirStream* dirstream) { + S3FS_PRN_INFO("closedir [pseudo_fd=%llu]", (unsigned long long)dirstream->fh); + return posix_s3fs_close(dirstream->fh); +} + +int posix_s3fs_unlink(const char* _path) +{ + WTF8_ENCODE(path) + int result; + + S3FS_PRN_INFO(" delete [path=%s]", path); + + if(0 != (result = check_parent_object_access(path, W_OK | X_OK))){ + return result; + } + if(use_newcache){ + result = accessor->Delete(path); + }else{ + S3fsCurl s3fscurl; + result = s3fscurl.DeleteRequest(path); + FdManager::DeleteCacheFile(path); + } + + StatCache::getStatCacheData()->DelStat(path); + StatCache::getStatCacheData()->DelSymlink(path); + + // update parent directory timestamp + int update_result; + if(0 != (update_result = update_mctime_parent_directory(path))){ + S3FS_PRN_ERR("succeed to remove the file(%s), but could not update timestamp of its parent directory(result=%d).", path, update_result); + } + S3FS_MALLOCTRIM(0); + return result; +} + +static void* s3fs_init() +{ + S3FS_PRN_INIT_INFO("init v%s(commit:%s) with %s, credential-library(%s)", VERSION, COMMIT_HASH_VAL, s3fs_crypt_lib_name(), ps3fscred->GetCredFuncVersion(false)); + + // cache(remove cache dirs at first) + if(is_remove_cache && (!CacheFileStat::DeleteCacheFileStatDirectory() || !FdManager::DeleteCacheDirectory())){ + S3FS_PRN_DBG("Could not initialize cache directory."); + } + + // check loading IAM role name + if(!ps3fscred->LoadIAMRoleFromMetaData()){ + S3FS_PRN_CRIT("could not load IAM role name from meta data."); + return nullptr; + } + + // Check Bucket + { + int result; + if(EXIT_SUCCESS != (result = s3fs_check_service())){ + return nullptr; + } + } + + if(!ThreadPoolMan::Initialize(max_thread_count)){ + S3FS_PRN_CRIT("Could not create thread pool(%d)", max_thread_count); + } + + // Signal object + if(!S3fsSignals::Initialize()){ + S3FS_PRN_ERR("Failed to initialize signal object, but continue..."); + } + + return nullptr; +} +static void s3fs_destroy() +{ + S3FS_PRN_INFO("destroy"); + + // Signal object + if(!S3fsSignals::Destroy()){ + S3FS_PRN_WARN("Failed to clean up signal object."); + } + + ThreadPoolMan::Destroy(); + + // cache(remove at last) + if(is_remove_cache && (!CacheFileStat::DeleteCacheFileStatDirectory() || !FdManager::DeleteCacheDirectory())){ + S3FS_PRN_WARN("Could not remove cache directory."); + } +} + +// 初始化配置 +static int init_config(std::string configpath) { + std::cout << "init_config: " << configpath << std::endl; + std::unordered_map config; + std::ifstream file(configpath); + std::string line = ""; + + if (!file.is_open()) { + std::cerr << "Could not open configuration file" << std::endl; + } + + while (std::getline(file, line)) { + // Ignore comments and empty lines + if (line.empty() || line[0] == '#') { + continue; + } + + std::istringstream iss(line); + std::string key, value; + + // Split line into key and value + if (std::getline(iss, key, '=') && std::getline(iss, value)) { + // Remove whitespace from the key and value + key.erase(key.find_last_not_of(" \t\n\r\f\v") + 1); + key.erase(0, key.find_first_not_of(" \t\n\r\f\v")); + value.erase(value.find_last_not_of(" \t\n\r\f\v") + 1); + value.erase(0, value.find_first_not_of(" \t\n\r\f\v")); + + config[key] = value; + } + } + + // log level + if (config.find("log_level") != config.end()) { + std::cout << "set loglevel: " << config["log_level"] << std::endl; + if(config["log_level"] == "debug") { + S3fsLog::SetLogLevel(S3fsLog::LEVEL_DBG); + } else if (config["log_level"] == "info") { + S3fsLog::SetLogLevel(S3fsLog::LEVEL_INFO); + } else if (config["log_level"] == "warning") { + S3fsLog::SetLogLevel(S3fsLog::LEVEL_WARN); + } else if (config["log_level"] == "error") { + S3fsLog::SetLogLevel(S3fsLog::LEVEL_ERR); + } else if (config["log_level"] == "critical") { + S3fsLog::SetLogLevel(S3fsLog::LEVEL_CRIT); + } + } + + // bucket config + if(S3fsCred::GetBucket().empty()) { + int ret = set_bucket(config["bucket"].c_str()); + std::cout << "set_bucket: " << ret << std::endl; + } + + // mountpoint config + // the second NONOPT option is the mountpoint(not utility mode) + if(mountpoint.empty() && utility_incomp_type::NO_UTILITY_MODE == utility_mode){ + // save the mountpoint and do some basic error checking + mountpoint = config["mountpoint"]; + struct stat stbuf; + +// In MSYS2 environment with WinFsp, it is not needed to create the mount point before mounting. +// Also it causes a conflict with WinFsp's validation, so disabling it. +#ifdef __MSYS__ + memset(&stbuf, 0, sizeof stbuf); + set_mountpoint_attribute(stbuf); +#else + if(stat(mountpoint.c_str(), &stbuf) == -1){ + // check stale mountpoint + if(errno == ENOTCONN){ + print_umount_message(mountpoint, true); + } else { + S3FS_PRN_EXIT("unable to access MOUNTPOINT %s: %s", mountpoint.c_str(), strerror(errno)); + } + return -1; + } + if(!(S_ISDIR(stbuf.st_mode))){ + S3FS_PRN_EXIT("MOUNTPOINT: %s is not a directory.", mountpoint.c_str()); + return -1; + } + if(!set_mountpoint_attribute(stbuf)){ + S3FS_PRN_EXIT("MOUNTPOINT: %s permission denied.", mountpoint.c_str()); + return -1; + } + + if(!nonempty){ + const struct dirent *ent; + DIR *dp = opendir(mountpoint.c_str()); + if(dp == nullptr){ + S3FS_PRN_EXIT("failed to open MOUNTPOINT: %s: %s", mountpoint.c_str(), strerror(errno)); + return -1; + } + while((ent = readdir(dp)) != nullptr){ + if(strcmp(ent->d_name, ".") != 0 && strcmp(ent->d_name, "..") != 0){ + closedir(dp); + S3FS_PRN_EXIT("MOUNTPOINT directory %s is not empty. if you are sure this is safe, can use the 'nonempty' mount option.", mountpoint.c_str()); + return -1; + } + } + closedir(dp); + } +#endif + } + + // passwd_file + std::string passwd_filename = config["passwd_file"]; + passwd_filename = "passwd_file=" + passwd_filename; + int ret = ps3fscred->DetectParam(passwd_filename.c_str()); + if (0 > ret) { + std::cerr << "Failed to parse passwd_file=" << passwd_filename << ": " << strerror(-ret); + return -1; + } + + // url + s3host = config["url"]; + // strip the trailing '/', if any, off the end of the host + // std::string + size_t found, length; + found = s3host.find_last_of('/'); + length = s3host.length(); + while(found == (length - 1) && length > 0){ + s3host.erase(found); + found = s3host.find_last_of('/'); + length = s3host.length(); + } + // Check url for http / https protocol std::string + if(!is_prefix(s3host.c_str(), "https://") && !is_prefix(s3host.c_str(), "http://")){ + S3FS_PRN_EXIT("option url has invalid format, missing http / https protocol"); + return -1; + } + + if (config.find("use_path_request_style") != config.end()) { + pathrequeststyle = true; + std::cout << "use path reqeust style" << std::endl; + } else { + std::cout << "use virtual host style" << std::endl; + } + + // newcache + if(config.find("newcache_conf") != config.end()) { + newcache_conf = config["newcache_conf"]; + if (!newcache_conf.empty()) { + use_newcache = true; + } + } + + return 0; +} + +S3fsLog singletonLog; +void s3fs_global_init() { +//static __attribute__((constructor)) void Init(void) { + static bool is_called = false; + if (is_called) { + std::cout << "global init has called"; + return; + } + int ch; + int option_index = 0; + time_t incomp_abort_time = (24 * 60 * 60); + + + S3fsLog::SetLogLevel(S3fsLog::LEVEL_DBG); + S3fsLog::SetLogfile("./log/posix_s3fs.log"); + //S3fsLog::debug_level = S3fsLog::LEVEL_DBG; + std::string configpath = "./conf/posix_s3fs.conf"; + + posixcontext.uid = geteuid(); + posixcontext.gid = getegid(); + S3FS_PRN_INFO("set the uid:%d , gid:%d", posixcontext.uid, posixcontext.gid); + + + // init bucket_block_size +#if defined(__MSYS__) + bucket_block_count = static_cast(INT32_MAX); +#elif defined(__APPLE__) + bucket_block_count = static_cast(INT32_MAX); +#else + bucket_block_count = ~0U; +#endif + + // init xml2 + xmlInitParser(); + LIBXML_TEST_VERSION + + init_sysconf_vars(); + + // get program name - emulate basename + program_name = "posixs3fs"; + + // set credential object + // + ps3fscred.reset(new S3fsCred()); + if(!S3fsCurl::InitCredentialObject(ps3fscred.get())){ + S3FS_PRN_EXIT("Failed to setup credential object to s3fs curl."); + exit(EXIT_FAILURE); + } + + // Load SSE environment + if(!S3fsCurl::LoadEnvSse()){ + S3FS_PRN_EXIT("something wrong about SSE environment."); + exit(EXIT_FAILURE); + } + + // ssl init + if(!s3fs_init_global_ssl()){ + S3FS_PRN_EXIT("could not initialize for ssl libraries."); + exit(EXIT_FAILURE); + } + + // mutex for xml + if(!init_parser_xml_lock()){ + S3FS_PRN_EXIT("could not initialize mutex for xml parser."); + s3fs_destroy_global_ssl(); + exit(EXIT_FAILURE); + } + + // mutex for basename/dirname + if(!init_basename_lock()){ + S3FS_PRN_EXIT("could not initialize mutex for basename/dirname."); + s3fs_destroy_global_ssl(); + destroy_parser_xml_lock(); + exit(EXIT_FAILURE); + } + + // init curl (without mime types) + // + // [NOTE] + // The curl initialization here does not load mime types. + // The mime types file parameter are dynamic values according + // to the user's environment, and are analyzed by the my_fuse_opt_proc + // function. + // The my_fuse_opt_proc function is executed after this curl + // initialization. Because the curl method is used in the + // my_fuse_opt_proc function, then it must be called here to + // initialize. Fortunately, the processing using mime types + // is only PUT/POST processing, and it is not used until the + // call of my_fuse_opt_proc function is completed. Therefore, + // the mime type is loaded just after calling the my_fuse_opt_proc + // function. + // + if(!S3fsCurl::InitS3fsCurl()){ + S3FS_PRN_EXIT("Could not initiate curl library."); + s3fs_destroy_global_ssl(); + destroy_parser_xml_lock(); + destroy_basename_lock(); + exit(EXIT_FAILURE); + } + + if(0 != init_config(configpath)){ + S3fsCurl::DestroyS3fsCurl(); + s3fs_destroy_global_ssl(); + destroy_parser_xml_lock(); + destroy_basename_lock(); + exit(EXIT_FAILURE); + } + + // init newcache + if(use_newcache){ + HybridCache::HybridCacheConfig cfg; + HybridCache::GetHybridCacheConfig(newcache_conf, cfg); + accessor = std::make_shared(cfg); + } + + // init mime types for curl + if(!S3fsCurl::InitMimeType(mimetype_file)){ + S3FS_PRN_WARN("Missing MIME types prevents setting Content-Type on uploaded objects."); + } + + // [NOTE] + // exclusive option check here. + // + if(strcasecmp(S3fsCurl::GetStorageClass().c_str(), "REDUCED_REDUNDANCY") == 0 && !S3fsCurl::IsSseDisable()){ + S3FS_PRN_EXIT("use_sse option could not be specified with storage class reduced_redundancy."); + S3fsCurl::DestroyS3fsCurl(); + s3fs_destroy_global_ssl(); + destroy_parser_xml_lock(); + destroy_basename_lock(); + exit(EXIT_FAILURE); + } + if(!S3fsCurl::FinalCheckSse()){ + S3FS_PRN_EXIT("something wrong about SSE options."); + S3fsCurl::DestroyS3fsCurl(); + s3fs_destroy_global_ssl(); + destroy_parser_xml_lock(); + destroy_basename_lock(); + exit(EXIT_FAILURE); + } + + if(S3fsCurl::GetSignatureType() == signature_type_t::V2_ONLY && S3fsCurl::GetUnsignedPayload()){ + S3FS_PRN_WARN("Ignoring enable_unsigned_payload with sigv2"); + } + + if(!FdEntity::GetNoMixMultipart() && max_dirty_data != -1){ + S3FS_PRN_WARN("Setting max_dirty_data to -1 when nomixupload is enabled"); + max_dirty_data = -1; + } + + // + // Check the combination of parameters for credential + // + if(!ps3fscred->CheckAllParams()){ + S3fsCurl::DestroyS3fsCurl(); + s3fs_destroy_global_ssl(); + destroy_parser_xml_lock(); + destroy_basename_lock(); + exit(EXIT_FAILURE); + } + + // The second plain argument is the mountpoint + // if the option was given, we all ready checked for a + // readable, non-empty directory, this checks determines + // if the mountpoint option was ever supplied + if(utility_incomp_type::NO_UTILITY_MODE == utility_mode){ + if(mountpoint.empty()){ + S3FS_PRN_EXIT("missing MOUNTPOINT argument."); + show_usage(); + S3fsCurl::DestroyS3fsCurl(); + s3fs_destroy_global_ssl(); + destroy_parser_xml_lock(); + destroy_basename_lock(); + exit(EXIT_FAILURE); + } + } + + // check tmp dir permission + if(!FdManager::CheckTmpDirExist()){ + S3FS_PRN_EXIT("temporary directory doesn't exists."); + S3fsCurl::DestroyS3fsCurl(); + s3fs_destroy_global_ssl(); + destroy_parser_xml_lock(); + destroy_basename_lock(); + exit(EXIT_FAILURE); + } + + // check cache dir permission + if(!FdManager::CheckCacheDirExist() || !FdManager::CheckCacheTopDir() || !CacheFileStat::CheckCacheFileStatTopDir()){ + S3FS_PRN_EXIT("could not allow cache directory permission, check permission of cache directories."); + S3fsCurl::DestroyS3fsCurl(); + s3fs_destroy_global_ssl(); + destroy_parser_xml_lock(); + destroy_basename_lock(); + exit(EXIT_FAILURE); + } + + // set fake free disk space + if(-1 != fake_diskfree_size){ + FdManager::InitFakeUsedDiskSize(fake_diskfree_size); + } + + // Set default value of free_space_ratio to 10% + if(FdManager::GetEnsureFreeDiskSpace()==0){ + //int ratio = 10; + int ratio = 5; + + off_t dfsize = FdManager::GetTotalDiskSpaceByRatio(ratio); + S3FS_PRN_INFO("Free space ratio default to %d %%, ensure the available disk space is greater than %.3f MB", ratio, static_cast(dfsize) / 1024 / 1024); + + if(dfsize < S3fsCurl::GetMultipartSize()){ + S3FS_PRN_WARN("specified size to ensure disk free space is smaller than multipart size, so set multipart size to it."); + dfsize = S3fsCurl::GetMultipartSize(); + } + FdManager::SetEnsureFreeDiskSpace(dfsize); + } + + // set user agent + S3fsCurl::InitUserAgent(); + + if(utility_incomp_type::NO_UTILITY_MODE != utility_mode){ + int exitcode = s3fs_utility_processing(incomp_abort_time); + + S3fsCurl::DestroyS3fsCurl(); + s3fs_destroy_global_ssl(); + destroy_parser_xml_lock(); + destroy_basename_lock(); + exit(exitcode); + } + + // Check multipart / copy api for mix multipart uploading + if(nomultipart || nocopyapi || norenameapi){ + FdEntity::SetNoMixMultipart(); + max_dirty_data = -1; + } + + // check free disk space + if(!FdManager::IsSafeDiskSpace(nullptr, S3fsCurl::GetMultipartSize() * S3fsCurl::GetMaxParallelCount())){ + // clean cache dir and retry + S3FS_PRN_WARN("No enough disk space for s3fs, try to clean cache dir"); + FdManager::get()->CleanupCacheDir(); + + if(!FdManager::IsSafeDiskSpaceWithLog(nullptr, S3fsCurl::GetMultipartSize() * S3fsCurl::GetMaxParallelCount())){ + S3fsCurl::DestroyS3fsCurl(); + s3fs_destroy_global_ssl(); + destroy_parser_xml_lock(); + destroy_basename_lock(); + exit(EXIT_FAILURE); + } + } + + // set mp stat flag object + + pHasMpStat = new MpStatFlag(); + s3fs_init(); + is_called = true; + std::cout << "finish s3fs global init" << std::endl; +} + +void s3fs_global_uninit() { +} + +static __attribute__((destructor)) void Clean(void) { + // Destroy curl + s3fs_destroy(); + + if(!S3fsCurl::DestroyS3fsCurl()){ + S3FS_PRN_WARN("Could not release curl library."); + } + s3fs_destroy_global_ssl(); + destroy_parser_xml_lock(); + destroy_basename_lock(); + delete pHasMpStat; + + // cleanup xml2 + xmlCleanupParser(); + S3FS_MALLOCTRIM(0); + + if(use_newcache){ + accessor.reset(); + } +} \ No newline at end of file diff --git a/s3fs/s3fs_lib.h b/s3fs/s3fs_lib.h new file mode 100644 index 0000000..93d777e --- /dev/null +++ b/s3fs/s3fs_lib.h @@ -0,0 +1,69 @@ +#ifndef S3FS_S3FS_LIB_H_ +#define S3FS_S3FS_LIB_H_ + + +#include +#include + + + +#ifdef S3FS_MALLOC_TRIM +#ifdef HAVE_MALLOC_TRIM +#include +#define S3FS_MALLOCTRIM(pad) malloc_trim(pad) +#else // HAVE_MALLOC_TRIM +#define S3FS_MALLOCTRIM(pad) +#endif // HAVE_MALLOC_TRIM +#else // S3FS_MALLOC_TRIM +#define S3FS_MALLOCTRIM(pad) +#endif // S3FS_MALLOC_TRIM + + +//------------------------------------------------------------------- +// posix interface functions +//------------------------------------------------------------------- +#ifdef __cplusplus +extern "C" { +#endif + +struct S3DirStream; + +void s3fs_global_init(); + +void s3fs_global_uninit(); + +int posix_s3fs_create(const char* _path, int flags, mode_t mode); + +int posix_s3fs_open(const char* _path, int flags, mode_t mode); + +int posix_s3fs_multiread(int fd, void* buf, size_t size, off_t file_offset); + +int posix_s3fs_read(int fd, void* buf, size_t size); + +int posix_s3fs_multiwrite(int fd, const void* buf, size_t size, off_t file_offset); + +int posix_s3fs_write(int fd, const void* buf, size_t size); + +off_t posix_s3fs_lseek(int fd, off_t offset, int whence); + +int posix_s3fs_close(int fd); + +int posix_s3fs_stat(const char* _path, struct stat* stbuf); + +int posix_s3fs_fstat(int fd, struct stat* stbuf) ; + +int posix_s3fs_mkdir(const char* _path, mode_t mode); + +int posix_s3fs_opendir(const char* _path, S3DirStream* dirstream); + +int posix_s3fs_getdents(S3DirStream* dirstream, char* contents, size_t maxread, ssize_t* realbytes); + +int posix_s3fs_closedir(S3DirStream* dirstream); + +int posix_s3fs_unlink(const char* _path); + +#ifdef __cplusplus +} +#endif + +#endif // S3FS_S3FS_LIB_H_ diff --git a/s3fs/s3fs_logger.cpp b/s3fs/s3fs_logger.cpp new file mode 100644 index 0000000..18e170e --- /dev/null +++ b/s3fs/s3fs_logger.cpp @@ -0,0 +1,306 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include +#include +#include +#include + +#include "common.h" +#include "s3fs_logger.h" + +//------------------------------------------------------------------- +// S3fsLog class : variables +//------------------------------------------------------------------- +constexpr char S3fsLog::LOGFILEENV[]; +constexpr const char* S3fsLog::nest_spaces[]; +constexpr char S3fsLog::MSGTIMESTAMP[]; +S3fsLog* S3fsLog::pSingleton = nullptr; +S3fsLog::s3fs_log_level S3fsLog::debug_level = S3fsLog::LEVEL_CRIT; +FILE* S3fsLog::logfp = nullptr; +std::string S3fsLog::logfile; +bool S3fsLog::time_stamp = true; + +//------------------------------------------------------------------- +// S3fsLog class : class methods +//------------------------------------------------------------------- +bool S3fsLog::IsS3fsLogLevel(s3fs_log_level level) +{ + return (level == (S3fsLog::debug_level & level)); +} + +std::string S3fsLog::GetCurrentTime() +{ + std::ostringstream current_time; + if(time_stamp){ + struct timeval now; + struct timespec tsnow; + struct tm res; + char tmp[32]; + if(-1 == clock_gettime(S3FS_CLOCK_MONOTONIC, &tsnow)){ + now.tv_sec = tsnow.tv_sec; + now.tv_usec = (tsnow.tv_nsec / 1000); + }else{ + gettimeofday(&now, nullptr); + } + strftime(tmp, sizeof(tmp), "%Y-%m-%dT%H:%M:%S", gmtime_r(&now.tv_sec, &res)); + current_time << tmp << "." << std::setfill('0') << std::setw(3) << (now.tv_usec / 1000) << "Z "; + } + return current_time.str(); +} + +bool S3fsLog::SetLogfile(const char* pfile) +{ + if(!S3fsLog::pSingleton){ + S3FS_PRN_CRIT("S3fsLog::pSingleton is nullptr."); + return false; + } + return S3fsLog::pSingleton->LowSetLogfile(pfile); +} + +bool S3fsLog::ReopenLogfile() +{ + if(!S3fsLog::pSingleton){ + S3FS_PRN_CRIT("S3fsLog::pSingleton is nullptr."); + return false; + } + if(!S3fsLog::logfp){ + S3FS_PRN_INFO("Currently the log file is output to stdout/stderr."); + return true; + } + if(!S3fsLog::logfile.empty()){ + S3FS_PRN_ERR("There is a problem with the path to the log file being empty."); + return false; + } + std::string tmp = S3fsLog::logfile; + return S3fsLog::pSingleton->LowSetLogfile(tmp.c_str()); +} + +S3fsLog::s3fs_log_level S3fsLog::SetLogLevel(s3fs_log_level level) +{ + if(!S3fsLog::pSingleton){ + S3FS_PRN_CRIT("S3fsLog::pSingleton is nullptr."); + return S3fsLog::debug_level; // Although it is an error, it returns the current value. + } + return S3fsLog::pSingleton->LowSetLogLevel(level); +} + +S3fsLog::s3fs_log_level S3fsLog::BumpupLogLevel() +{ + if(!S3fsLog::pSingleton){ + S3FS_PRN_CRIT("S3fsLog::pSingleton is nullptr."); + return S3fsLog::debug_level; // Although it is an error, it returns the current value. + } + return S3fsLog::pSingleton->LowBumpupLogLevel(); +} + +bool S3fsLog::SetTimeStamp(bool value) +{ + bool old = S3fsLog::time_stamp; + S3fsLog::time_stamp = value; + return old; +} + +//------------------------------------------------------------------- +// S3fsLog class : methods +//------------------------------------------------------------------- +S3fsLog::S3fsLog() +{ + if(!S3fsLog::pSingleton){ + S3fsLog::pSingleton = this; + + // init syslog(default CRIT) + openlog("s3fs", LOG_PID | LOG_ODELAY | LOG_NOWAIT, LOG_USER); + LowLoadEnv(); + }else{ + S3FS_PRN_ERR("Already set singleton object for S3fsLog."); + } +} + +S3fsLog::~S3fsLog() +{ + if(S3fsLog::pSingleton == this){ + FILE* oldfp = S3fsLog::logfp; + S3fsLog::logfp = nullptr; + if(oldfp && 0 != fclose(oldfp)){ + S3FS_PRN_ERR("Could not close old log file(%s), but continue...", (S3fsLog::logfile.empty() ? S3fsLog::logfile.c_str() : "null")); + } + S3fsLog::logfile.clear(); + S3fsLog::pSingleton = nullptr; + S3fsLog::debug_level = S3fsLog::LEVEL_CRIT; + + closelog(); + }else{ + S3FS_PRN_ERR("This object is not singleton S3fsLog object."); + } +} + +bool S3fsLog::LowLoadEnv() +{ + if(S3fsLog::pSingleton != this){ + S3FS_PRN_ERR("This object is not as same as S3fsLog::pSingleton."); + return false; + } + char* pEnvVal; + if(nullptr != (pEnvVal = getenv(S3fsLog::LOGFILEENV))){ + if(!SetLogfile(pEnvVal)){ + return false; + } + } + if(nullptr != (pEnvVal = getenv(S3fsLog::MSGTIMESTAMP))){ + if(0 == strcasecmp(pEnvVal, "true") || 0 == strcasecmp(pEnvVal, "yes") || 0 == strcasecmp(pEnvVal, "1")){ + S3fsLog::time_stamp = true; + }else if(0 == strcasecmp(pEnvVal, "false") || 0 == strcasecmp(pEnvVal, "no") || 0 == strcasecmp(pEnvVal, "0")){ + S3fsLog::time_stamp = false; + }else{ + S3FS_PRN_WARN("Unknown %s environment value(%s) is specified, skip to set time stamp mode.", S3fsLog::MSGTIMESTAMP, pEnvVal); + } + } + return true; +} + +bool S3fsLog::LowSetLogfile(const char* pfile) +{ + if(S3fsLog::pSingleton != this){ + S3FS_PRN_ERR("This object is not as same as S3fsLog::pSingleton."); + return false; + } + + if(!pfile){ + // close log file if it is opened + if(S3fsLog::logfp && 0 != fclose(S3fsLog::logfp)){ + S3FS_PRN_ERR("Could not close log file(%s).", (S3fsLog::logfile.empty() ? S3fsLog::logfile.c_str() : "null")); + return false; + } + S3fsLog::logfp = nullptr; + S3fsLog::logfile.clear(); + }else{ + // open new log file + // + // [NOTE] + // It will reopen even if it is the same file. + // + FILE* newfp; + if(nullptr == (newfp = fopen(pfile, "a+"))){ + S3FS_PRN_ERR("Could not open log file(%s).", pfile); + return false; + } + + // switch new log file and close old log file if it is opened + FILE* oldfp = S3fsLog::logfp; + if(oldfp && 0 != fclose(oldfp)){ + S3FS_PRN_ERR("Could not close old log file(%s).", (!S3fsLog::logfile.empty() ? S3fsLog::logfile.c_str() : "null")); + fclose(newfp); + return false; + } + S3fsLog::logfp = newfp; + S3fsLog::logfile = pfile; + } + return true; +} + +S3fsLog::s3fs_log_level S3fsLog::LowSetLogLevel(s3fs_log_level level) +{ + if(S3fsLog::pSingleton != this){ + S3FS_PRN_ERR("This object is not as same as S3fsLog::pSingleton."); + return S3fsLog::debug_level; // Although it is an error, it returns the current value. + } + if(level == S3fsLog::debug_level){ + return S3fsLog::debug_level; + } + s3fs_log_level old = S3fsLog::debug_level; + S3fsLog::debug_level = level; + setlogmask(LOG_UPTO(GetSyslogLevel(S3fsLog::debug_level))); + S3FS_PRN_CRIT("change debug level from %sto %s", GetLevelString(old), GetLevelString(S3fsLog::debug_level)); + return old; +} + +S3fsLog::s3fs_log_level S3fsLog::LowBumpupLogLevel() +{ + if(S3fsLog::pSingleton != this){ + S3FS_PRN_ERR("This object is not as same as S3fsLog::pSingleton."); + return S3fsLog::debug_level; // Although it is an error, it returns the current value. + } + s3fs_log_level old = S3fsLog::debug_level; + S3fsLog::debug_level = ( LEVEL_CRIT == S3fsLog::debug_level ? LEVEL_ERR : + LEVEL_ERR == S3fsLog::debug_level ? LEVEL_WARN : + LEVEL_WARN == S3fsLog::debug_level ? LEVEL_INFO : + LEVEL_INFO == S3fsLog::debug_level ? LEVEL_DBG : LEVEL_CRIT ); + setlogmask(LOG_UPTO(GetSyslogLevel(S3fsLog::debug_level))); + S3FS_PRN_CRIT("change debug level from %sto %s", GetLevelString(old), GetLevelString(S3fsLog::debug_level)); + return old; +} + +void s3fs_low_logprn(S3fsLog::s3fs_log_level level, const char* file, const char *func, int line, const char *fmt, ...) +{ + if(S3fsLog::IsS3fsLogLevel(level)){ + va_list va; + va_start(va, fmt); + size_t len = vsnprintf(nullptr, 0, fmt, va) + 1; + va_end(va); + + std::unique_ptr message(new char[len]); + va_start(va, fmt); + vsnprintf(message.get(), len, fmt, va); + va_end(va); + + if(foreground || S3fsLog::IsSetLogFile()){ + S3fsLog::SeekEnd(); + fprintf(S3fsLog::GetOutputLogFile(), "%s%s%s:%s(%d): %s\n", S3fsLog::GetCurrentTime().c_str(), S3fsLog::GetLevelString(level), file, func, line, message.get()); + S3fsLog::Flush(); + }else{ + // TODO: why does this differ from s3fs_low_logprn2? + syslog(S3fsLog::GetSyslogLevel(level), "%s%s:%s(%d): %s", instance_name.c_str(), file, func, line, message.get()); + } + } +} + +void s3fs_low_logprn2(S3fsLog::s3fs_log_level level, int nest, const char* file, const char *func, int line, const char *fmt, ...) +{ + if(S3fsLog::IsS3fsLogLevel(level)){ + va_list va; + va_start(va, fmt); + size_t len = vsnprintf(nullptr, 0, fmt, va) + 1; + va_end(va); + + std::unique_ptr message(new char[len]); + va_start(va, fmt); + vsnprintf(message.get(), len, fmt, va); + va_end(va); + + if(foreground || S3fsLog::IsSetLogFile()){ + S3fsLog::SeekEnd(); + fprintf(S3fsLog::GetOutputLogFile(), "%s%s%s%s:%s(%d): %s\n", S3fsLog::GetCurrentTime().c_str(), S3fsLog::GetLevelString(level), S3fsLog::GetS3fsLogNest(nest), file, func, line, message.get()); + S3fsLog::Flush(); + }else{ + syslog(S3fsLog::GetSyslogLevel(level), "%s%s%s", instance_name.c_str(), S3fsLog::GetS3fsLogNest(nest), message.get()); + } + } +} + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/s3fs_logger.h b/s3fs/s3fs_logger.h new file mode 100644 index 0000000..94d9d0c --- /dev/null +++ b/s3fs/s3fs_logger.h @@ -0,0 +1,270 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef S3FS_LOGGER_H_ +#define S3FS_LOGGER_H_ + +#include +#include +#include +#include +#include + +#include "common.h" + +#ifdef CLOCK_MONOTONIC_COARSE +#define S3FS_CLOCK_MONOTONIC CLOCK_MONOTONIC_COARSE +#else +// case of OSX +#define S3FS_CLOCK_MONOTONIC CLOCK_MONOTONIC +#endif + +//------------------------------------------------------------------- +// S3fsLog class +//------------------------------------------------------------------- +class S3fsLog +{ + public: + enum s3fs_log_level{ + LEVEL_CRIT = 0, // LEVEL_CRIT + LEVEL_ERR = 1, // LEVEL_ERR + LEVEL_WARN = 3, // LEVEL_WARNING + LEVEL_INFO = 7, // LEVEL_INFO + LEVEL_DBG = 15 // LEVEL_DEBUG + }; + + protected: + static constexpr int NEST_MAX = 4; + static constexpr const char* nest_spaces[NEST_MAX] = {"", " ", " ", " "}; + static constexpr char LOGFILEENV[] = "S3FS_LOGFILE"; + static constexpr char MSGTIMESTAMP[] = "S3FS_MSGTIMESTAMP"; + + static S3fsLog* pSingleton; + static s3fs_log_level debug_level; + static FILE* logfp; + static std::string logfile; + static bool time_stamp; + + protected: + bool LowLoadEnv(); + bool LowSetLogfile(const char* pfile); + s3fs_log_level LowSetLogLevel(s3fs_log_level level); + s3fs_log_level LowBumpupLogLevel(); + + public: + static bool IsS3fsLogLevel(s3fs_log_level level); + static bool IsS3fsLogCrit() { return IsS3fsLogLevel(LEVEL_CRIT); } + static bool IsS3fsLogErr() { return IsS3fsLogLevel(LEVEL_ERR); } + static bool IsS3fsLogWarn() { return IsS3fsLogLevel(LEVEL_WARN); } + static bool IsS3fsLogInfo() { return IsS3fsLogLevel(LEVEL_INFO); } + static bool IsS3fsLogDbg() { return IsS3fsLogLevel(LEVEL_DBG); } + + static constexpr int GetSyslogLevel(s3fs_log_level level) + { + return ( LEVEL_DBG == (level & LEVEL_DBG) ? LOG_DEBUG : + LEVEL_INFO == (level & LEVEL_DBG) ? LOG_INFO : + LEVEL_WARN == (level & LEVEL_DBG) ? LOG_WARNING : + LEVEL_ERR == (level & LEVEL_DBG) ? LOG_ERR : LOG_CRIT ); + } + + static std::string GetCurrentTime(); + + static constexpr const char* GetLevelString(s3fs_log_level level) + { + return ( LEVEL_DBG == (level & LEVEL_DBG) ? "[DBG] " : + LEVEL_INFO == (level & LEVEL_DBG) ? "[INF] " : + LEVEL_WARN == (level & LEVEL_DBG) ? "[WAN] " : + LEVEL_ERR == (level & LEVEL_DBG) ? "[ERR] " : "[CRT] " ); + } + + static constexpr const char* GetS3fsLogNest(int nest) + { + return nest_spaces[nest < NEST_MAX ? nest : NEST_MAX - 1]; + } + + static bool IsSetLogFile() + { + return (nullptr != logfp); + } + + static FILE* GetOutputLogFile() + { + return (logfp ? logfp : stdout); + } + + static FILE* GetErrorLogFile() + { + return (logfp ? logfp : stderr); + } + + static void SeekEnd() + { + if(logfp){ + fseek(logfp, 0, SEEK_END); + } + } + + static void Flush() + { + if(logfp){ + fflush(logfp); + } + } + + static bool SetLogfile(const char* pfile); + static bool ReopenLogfile(); + static s3fs_log_level SetLogLevel(s3fs_log_level level); + static s3fs_log_level BumpupLogLevel(); + static bool SetTimeStamp(bool value); + + explicit S3fsLog(); + ~S3fsLog(); + S3fsLog(const S3fsLog&) = delete; + S3fsLog(S3fsLog&&) = delete; + S3fsLog& operator=(const S3fsLog&) = delete; + S3fsLog& operator=(S3fsLog&&) = delete; +}; + +//------------------------------------------------------------------- +// Debug macros +//------------------------------------------------------------------- +void s3fs_low_logprn(S3fsLog::s3fs_log_level level, const char* file, const char *func, int line, const char *fmt, ...) __attribute__ ((format (printf, 5, 6))); +#define S3FS_LOW_LOGPRN(level, fmt, ...) \ + do{ \ + s3fs_low_logprn(level, __FILE__, __func__, __LINE__, fmt, ##__VA_ARGS__); \ + }while(0) + +void s3fs_low_logprn2(S3fsLog::s3fs_log_level level, int nest, const char* file, const char *func, int line, const char *fmt, ...) __attribute__ ((format (printf, 6, 7))); +#define S3FS_LOW_LOGPRN2(level, nest, fmt, ...) \ + do{ \ + s3fs_low_logprn2(level, nest, __FILE__, __func__, __LINE__, fmt, ##__VA_ARGS__); \ + }while(0) + +#define S3FS_LOW_CURLDBG(fmt, ...) \ + do{ \ + if(foreground || S3fsLog::IsSetLogFile()){ \ + S3fsLog::SeekEnd(); \ + fprintf(S3fsLog::GetOutputLogFile(), "%s[CURL DBG] " fmt "%s\n", S3fsLog::GetCurrentTime().c_str(), __VA_ARGS__); \ + S3fsLog::Flush(); \ + }else{ \ + syslog(S3fsLog::GetSyslogLevel(S3fsLog::LEVEL_CRIT), "%s" fmt "%s", instance_name.c_str(), __VA_ARGS__); \ + } \ + }while(0) + +#define S3FS_LOW_LOGPRN_EXIT(fmt, ...) \ + do{ \ + if(foreground || S3fsLog::IsSetLogFile()){ \ + S3fsLog::SeekEnd(); \ + fprintf(S3fsLog::GetErrorLogFile(), "s3fs: " fmt "%s\n", __VA_ARGS__); \ + S3fsLog::Flush(); \ + }else{ \ + fprintf(S3fsLog::GetErrorLogFile(), "s3fs: " fmt "%s\n", __VA_ARGS__); \ + syslog(S3fsLog::GetSyslogLevel(S3fsLog::LEVEL_CRIT), "%ss3fs: " fmt "%s", instance_name.c_str(), __VA_ARGS__); \ + } \ + }while(0) + +// Special macro for init message +#define S3FS_PRN_INIT_INFO(fmt, ...) \ + do{ \ + if(foreground || S3fsLog::IsSetLogFile()){ \ + S3fsLog::SeekEnd(); \ + fprintf(S3fsLog::GetOutputLogFile(), "%s%s%s%s:%s(%d): " fmt "%s\n", S3fsLog::GetCurrentTime().c_str(), S3fsLog::GetLevelString(S3fsLog::LEVEL_INFO), S3fsLog::GetS3fsLogNest(0), __FILE__, __func__, __LINE__, __VA_ARGS__, ""); \ + S3fsLog::Flush(); \ + }else{ \ + syslog(S3fsLog::GetSyslogLevel(S3fsLog::LEVEL_INFO), "%s%s" fmt "%s", instance_name.c_str(), S3fsLog::GetS3fsLogNest(0), __VA_ARGS__, ""); \ + } \ + }while(0) + +#define S3FS_PRN_LAUNCH_INFO(fmt, ...) \ + do{ \ + if(foreground || S3fsLog::IsSetLogFile()){ \ + S3fsLog::SeekEnd(); \ + fprintf(S3fsLog::GetOutputLogFile(), "%s%s" fmt "%s\n", S3fsLog::GetCurrentTime().c_str(), S3fsLog::GetLevelString(S3fsLog::LEVEL_INFO), __VA_ARGS__, ""); \ + S3fsLog::Flush(); \ + }else{ \ + syslog(S3fsLog::GetSyslogLevel(S3fsLog::LEVEL_INFO), "%s" fmt "%s", instance_name.c_str(), __VA_ARGS__, ""); \ + } \ + }while(0) + +// Special macro for checking cache files +#define S3FS_LOW_CACHE(fp, fmt, ...) \ + do{ \ + if(foreground || S3fsLog::IsSetLogFile()){ \ + S3fsLog::SeekEnd(); \ + fprintf(fp, fmt "%s\n", __VA_ARGS__); \ + S3fsLog::Flush(); \ + }else{ \ + syslog(S3fsLog::GetSyslogLevel(S3fsLog::LEVEL_INFO), "%s: " fmt "%s", instance_name.c_str(), __VA_ARGS__); \ + } \ + }while(0) + +// [NOTE] +// small trick for VA_ARGS +// +#define S3FS_PRN_EXIT(fmt, ...) S3FS_LOW_LOGPRN_EXIT(fmt, ##__VA_ARGS__, "") +#define S3FS_PRN_CRIT(fmt, ...) S3FS_LOW_LOGPRN(S3fsLog::LEVEL_CRIT, fmt, ##__VA_ARGS__) +#define S3FS_PRN_ERR(fmt, ...) S3FS_LOW_LOGPRN(S3fsLog::LEVEL_ERR, fmt, ##__VA_ARGS__) +#define S3FS_PRN_WARN(fmt, ...) S3FS_LOW_LOGPRN(S3fsLog::LEVEL_WARN, fmt, ##__VA_ARGS__) +#define S3FS_PRN_DBG(fmt, ...) S3FS_LOW_LOGPRN(S3fsLog::LEVEL_DBG, fmt, ##__VA_ARGS__) +#define S3FS_PRN_INFO(fmt, ...) S3FS_LOW_LOGPRN2(S3fsLog::LEVEL_INFO, 0, fmt, ##__VA_ARGS__) +#define S3FS_PRN_INFO1(fmt, ...) S3FS_LOW_LOGPRN2(S3fsLog::LEVEL_INFO, 1, fmt, ##__VA_ARGS__) +#define S3FS_PRN_INFO2(fmt, ...) S3FS_LOW_LOGPRN2(S3fsLog::LEVEL_INFO, 2, fmt, ##__VA_ARGS__) +#define S3FS_PRN_INFO3(fmt, ...) S3FS_LOW_LOGPRN2(S3fsLog::LEVEL_INFO, 3, fmt, ##__VA_ARGS__) +#define S3FS_PRN_CURL(fmt, ...) S3FS_LOW_CURLDBG(fmt, ##__VA_ARGS__, "") +#define S3FS_PRN_CACHE(fp, ...) S3FS_LOW_CACHE(fp, ##__VA_ARGS__, "") + +// Macros to print log with fuse context +#define PRINT_FUSE_CTX(level, indent, fmt, ...) do { \ + if(S3fsLog::IsS3fsLogLevel(level)){ \ + struct fuse_context *ctx = fuse_get_context(); \ + if(ctx == NULL){ \ + S3FS_LOW_LOGPRN2(level, indent, fmt, ##__VA_ARGS__); \ + }else{ \ + S3FS_LOW_LOGPRN2(level, indent, fmt"[pid=%u,uid=%u,gid=%u]",\ + ##__VA_ARGS__, \ + (unsigned int)(ctx->pid), \ + (unsigned int)(ctx->uid), \ + (unsigned int)(ctx->gid)); \ + } \ + } \ +} while (0) + +#define FUSE_CTX_INFO(fmt, ...) do { \ + PRINT_FUSE_CTX(S3fsLog::LEVEL_INFO, 0, fmt, ##__VA_ARGS__); \ +} while (0) + +#define FUSE_CTX_INFO1(fmt, ...) do { \ + PRINT_FUSE_CTX(S3fsLog::LEVEL_INFO, 1, fmt, ##__VA_ARGS__); \ +} while (0) + +#define FUSE_CTX_DBG(fmt, ...) do { \ + PRINT_FUSE_CTX(S3fsLog::LEVEL_DBG, 0, fmt, ##__VA_ARGS__); \ +} while (0) + +#endif // S3FS_LOGGER_H_ + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/s3fs_util.cpp b/s3fs/s3fs_util.cpp new file mode 100644 index 0000000..14a645e --- /dev/null +++ b/s3fs/s3fs_util.cpp @@ -0,0 +1,592 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Takeshi Nakatani + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "s3fs_logger.h" +#include "s3fs_util.h" +#include "string_util.h" +#include "s3fs_help.h" +#include "autolock.h" + +//------------------------------------------------------------------- +// Global variables +//------------------------------------------------------------------- +std::string mount_prefix; + +static size_t max_password_size; +static size_t max_group_name_length; + +//------------------------------------------------------------------- +// Utilities +//------------------------------------------------------------------- +std::string get_realpath(const char *path) +{ + std::string realpath = mount_prefix; + realpath += path; + + return realpath; +} + +void init_sysconf_vars() +{ + // SUSv4tc1 says the following about _SC_GETGR_R_SIZE_MAX and + // _SC_GETPW_R_SIZE_MAX: + // Note that sysconf(_SC_GETGR_R_SIZE_MAX) may return -1 if + // there is no hard limit on the size of the buffer needed to + // store all the groups returned. + + long res = sysconf(_SC_GETPW_R_SIZE_MAX); + if(0 > res){ + if (errno != 0){ + S3FS_PRN_WARN("could not get max pw length."); + abort(); + } + res = 1024; // default initial length + } + max_password_size = res; + + res = sysconf(_SC_GETGR_R_SIZE_MAX); + if(0 > res) { + if (errno != 0) { + S3FS_PRN_ERR("could not get max name length."); + abort(); + } + res = 1024; // default initial length + } + max_group_name_length = res; +} + +//------------------------------------------------------------------- +// Utility for UID/GID +//------------------------------------------------------------------- +// get user name from uid +std::string get_username(uid_t uid) +{ + size_t maxlen = max_password_size; + int result; + struct passwd pwinfo; + struct passwd* ppwinfo = nullptr; + + // make buffer + std::unique_ptr pbuf(new char[maxlen]); + // get pw information + while(ERANGE == (result = getpwuid_r(uid, &pwinfo, pbuf.get(), maxlen, &ppwinfo))){ + maxlen *= 2; + pbuf.reset(new char[maxlen]); + } + + if(0 != result){ + S3FS_PRN_ERR("could not get pw information(%d).", result); + return ""; + } + + // check pw + if(nullptr == ppwinfo){ + return ""; + } + std::string name = SAFESTRPTR(ppwinfo->pw_name); + return name; +} + +int is_uid_include_group(uid_t uid, gid_t gid) +{ + size_t maxlen = max_group_name_length; + int result; + struct group ginfo; + struct group* pginfo = nullptr; + + // make buffer + std::unique_ptr pbuf(new char[maxlen]); + // get group information + while(ERANGE == (result = getgrgid_r(gid, &ginfo, pbuf.get(), maxlen, &pginfo))){ + maxlen *= 2; + pbuf.reset(new char[maxlen]); + } + + if(0 != result){ + S3FS_PRN_ERR("could not get group information(%d).", result); + return -result; + } + + // check group + if(nullptr == pginfo){ + // there is not gid in group. + return -EINVAL; + } + + std::string username = get_username(uid); + + char** ppgr_mem; + for(ppgr_mem = pginfo->gr_mem; ppgr_mem && *ppgr_mem; ppgr_mem++){ + if(username == *ppgr_mem){ + // Found username in group. + return 1; + } + } + return 0; +} + +//------------------------------------------------------------------- +// Utility for file and directory +//------------------------------------------------------------------- +// [NOTE] +// basename/dirname returns a static variable pointer as the return value. +// Normally this shouldn't be a problem, but in macos10 we found a case +// where dirname didn't receive its return value correctly due to thread +// conflicts. +// To avoid this, exclusive control is performed by mutex. +// +static pthread_mutex_t* pbasename_lock = nullptr; + +bool init_basename_lock() +{ + if(pbasename_lock){ + S3FS_PRN_ERR("already initialized mutex for posix dirname/basename function."); + return false; + } + pbasename_lock = new pthread_mutex_t; + + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); + +#if S3FS_PTHREAD_ERRORCHECK + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK); +#endif + int result; + if(0 != (result = pthread_mutex_init(pbasename_lock, &attr))){ + S3FS_PRN_ERR("failed to init pbasename_lock: %d.", result); + delete pbasename_lock; + pbasename_lock = nullptr; + return false; + } + return true; +} + +bool destroy_basename_lock() +{ + if(!pbasename_lock){ + S3FS_PRN_ERR("the mutex for posix dirname/basename function is not initialized."); + return false; + } + int result; + if(0 != (result = pthread_mutex_destroy(pbasename_lock))){ + S3FS_PRN_ERR("failed to destroy pbasename_lock: %d", result); + return false; + } + delete pbasename_lock; + pbasename_lock = nullptr; + + return true; +} + +std::string mydirname(const std::string& path) +{ + AutoLock auto_lock(pbasename_lock); + + return mydirname(path.c_str()); +} + +// safe variant of dirname +// dirname clobbers path so let it operate on a tmp copy +std::string mydirname(const char* path) +{ + if(!path || '\0' == path[0]){ + return ""; + } + + char *buf = strdup(path); + std::string result = dirname(buf); + free(buf); + return result; +} + +std::string mybasename(const std::string& path) +{ + AutoLock auto_lock(pbasename_lock); + + return mybasename(path.c_str()); +} + +// safe variant of basename +// basename clobbers path so let it operate on a tmp copy +std::string mybasename(const char* path) +{ + if(!path || '\0' == path[0]){ + return ""; + } + + char *buf = strdup(path); + std::string result = basename(buf); + free(buf); + return result; +} + +// mkdir --parents +int mkdirp(const std::string& path, mode_t mode) +{ + std::string base; + std::string component; + std::istringstream ss(path); + while (getline(ss, component, '/')) { + base += component + "/"; + + struct stat st; + if(0 == stat(base.c_str(), &st)){ + if(!S_ISDIR(st.st_mode)){ + return EPERM; + } + }else{ + if(0 != mkdir(base.c_str(), mode) && errno != EEXIST){ + return errno; + } + } + } + return 0; +} + +// get existed directory path +std::string get_exist_directory_path(const std::string& path) +{ + std::string existed("/"); // "/" is existed. + std::string base; + std::string component; + std::istringstream ss(path); + while (getline(ss, component, '/')) { + if(base != "/"){ + base += "/"; + } + base += component; + struct stat st; + if(0 == stat(base.c_str(), &st) && S_ISDIR(st.st_mode)){ + existed = base; + }else{ + break; + } + } + return existed; +} + +bool check_exist_dir_permission(const char* dirpath) +{ + if(!dirpath || '\0' == dirpath[0]){ + return false; + } + + // exists + struct stat st; + if(0 != stat(dirpath, &st)){ + if(ENOENT == errno){ + // dir does not exist + return true; + } + if(EACCES == errno){ + // could not access directory + return false; + } + // something error occurred + return false; + } + + // check type + if(!S_ISDIR(st.st_mode)){ + // path is not directory + return false; + } + + // check permission + uid_t myuid = geteuid(); + if(myuid == st.st_uid){ + if(S_IRWXU != (st.st_mode & S_IRWXU)){ + return false; + } + }else{ + if(1 == is_uid_include_group(myuid, st.st_gid)){ + if(S_IRWXG != (st.st_mode & S_IRWXG)){ + return false; + } + }else{ + if(S_IRWXO != (st.st_mode & S_IRWXO)){ + return false; + } + } + } + return true; +} + +bool delete_files_in_dir(const char* dir, bool is_remove_own) +{ + DIR* dp; + struct dirent* dent; + + if(nullptr == (dp = opendir(dir))){ + S3FS_PRN_ERR("could not open dir(%s) - errno(%d)", dir, errno); + return false; + } + + for(dent = readdir(dp); dent; dent = readdir(dp)){ + if(0 == strcmp(dent->d_name, "..") || 0 == strcmp(dent->d_name, ".")){ + continue; + } + std::string fullpath = dir; + fullpath += "/"; + fullpath += dent->d_name; + struct stat st; + if(0 != lstat(fullpath.c_str(), &st)){ + S3FS_PRN_ERR("could not get stats of file(%s) - errno(%d)", fullpath.c_str(), errno); + closedir(dp); + return false; + } + if(S_ISDIR(st.st_mode)){ + // dir -> Reentrant + if(!delete_files_in_dir(fullpath.c_str(), true)){ + S3FS_PRN_ERR("could not remove sub dir(%s) - errno(%d)", fullpath.c_str(), errno); + closedir(dp); + return false; + } + }else{ + if(0 != unlink(fullpath.c_str())){ + S3FS_PRN_ERR("could not remove file(%s) - errno(%d)", fullpath.c_str(), errno); + closedir(dp); + return false; + } + } + } + closedir(dp); + + if(is_remove_own && 0 != rmdir(dir)){ + S3FS_PRN_ERR("could not remove dir(%s) - errno(%d)", dir, errno); + return false; + } + return true; +} + +//------------------------------------------------------------------- +// Utility for system information +//------------------------------------------------------------------- +bool compare_sysname(const char* target) +{ + // [NOTE] + // The buffer size of sysname member in struct utsname is + // OS dependent, but 512 bytes is sufficient for now. + // + static const char* psysname = nullptr; + static char sysname[512]; + if(!psysname){ + struct utsname sysinfo; + if(0 != uname(&sysinfo)){ + S3FS_PRN_ERR("could not initialize system name to internal buffer(errno:%d), thus use \"Linux\".", errno); + strcpy(sysname, "Linux"); + }else{ + S3FS_PRN_INFO("system name is %s", sysinfo.sysname); + sysname[sizeof(sysname) - 1] = '\0'; + strncpy(sysname, sysinfo.sysname, sizeof(sysname) - 1); + } + psysname = &sysname[0]; + } + + if(!target || 0 != strcmp(psysname, target)){ + return false; + } + return true; +} + +//------------------------------------------------------------------- +// Utility for print message at launching +//------------------------------------------------------------------- +void print_launch_message(int argc, char** argv) +{ + std::string message = short_version(); + + if(argv){ + message += " :"; + for(int cnt = 0; cnt < argc; ++cnt){ + if(argv[cnt]){ + message += " "; + if(0 == cnt){ + message += basename(argv[cnt]); + }else{ + message += argv[cnt]; + } + } + } + } + S3FS_PRN_LAUNCH_INFO("%s", message.c_str()); +} + +int flock_set(int fd, int type) +{ + struct flock lock; + lock.l_whence = SEEK_SET; + lock.l_start = 0; + lock.l_len = 0; + lock.l_type = type; + lock.l_pid = -1; + + return fcntl(fd, F_SETLKW, &lock); +} + +// +// result: -1 ts1 < ts2 +// 0 ts1 == ts2 +// 1 ts1 > ts2 +// +int compare_timespec(const struct timespec& ts1, const struct timespec& ts2) +{ + if(ts1.tv_sec < ts2.tv_sec){ + return -1; + }else if(ts1.tv_sec > ts2.tv_sec){ + return 1; + }else{ + if(ts1.tv_nsec < ts2.tv_nsec){ + return -1; + }else if(ts1.tv_nsec > ts2.tv_nsec){ + return 1; + } + } + return 0; +} + +// +// result: -1 st < ts +// 0 st == ts +// 1 st > ts +// +int compare_timespec(const struct stat& st, stat_time_type type, const struct timespec& ts) +{ + struct timespec st_ts; + set_stat_to_timespec(st, type, st_ts); + + return compare_timespec(st_ts, ts); +} + +void set_timespec_to_stat(struct stat& st, stat_time_type type, const struct timespec& ts) +{ + if(stat_time_type::ATIME == type){ + #if defined(__APPLE__) + st.st_atime = ts.tv_sec; + st.st_atimespec.tv_nsec = ts.tv_nsec; + #else + st.st_atim.tv_sec = ts.tv_sec; + st.st_atim.tv_nsec = ts.tv_nsec; + #endif + }else if(stat_time_type::MTIME == type){ + #if defined(__APPLE__) + st.st_mtime = ts.tv_sec; + st.st_mtimespec.tv_nsec = ts.tv_nsec; + #else + st.st_mtim.tv_sec = ts.tv_sec; + st.st_mtim.tv_nsec = ts.tv_nsec; + #endif + }else if(stat_time_type::CTIME == type){ + #if defined(__APPLE__) + st.st_ctime = ts.tv_sec; + st.st_ctimespec.tv_nsec = ts.tv_nsec; + #else + st.st_ctim.tv_sec = ts.tv_sec; + st.st_ctim.tv_nsec = ts.tv_nsec; + #endif + }else{ + S3FS_PRN_ERR("unknown type(%d), so skip to set value.", static_cast(type)); + } +} + +struct timespec* set_stat_to_timespec(const struct stat& st, stat_time_type type, struct timespec& ts) +{ + if(stat_time_type::ATIME == type){ + #if defined(__APPLE__) + ts.tv_sec = st.st_atime; + ts.tv_nsec = st.st_atimespec.tv_nsec; + #else + ts = st.st_atim; + #endif + }else if(stat_time_type::MTIME == type){ + #if defined(__APPLE__) + ts.tv_sec = st.st_mtime; + ts.tv_nsec = st.st_mtimespec.tv_nsec; + #else + ts = st.st_mtim; + #endif + }else if(stat_time_type::CTIME == type){ + #if defined(__APPLE__) + ts.tv_sec = st.st_ctime; + ts.tv_nsec = st.st_ctimespec.tv_nsec; + #else + ts = st.st_ctim; + #endif + }else{ + S3FS_PRN_ERR("unknown type(%d), so use 0 as timespec.", static_cast(type)); + ts.tv_sec = 0; + ts.tv_nsec = 0; + } + return &ts; +} + +std::string str_stat_time(const struct stat& st, stat_time_type type) +{ + struct timespec ts; + return str(*set_stat_to_timespec(st, type, ts)); +} + +struct timespec* s3fs_realtime(struct timespec& ts) +{ + if(-1 == clock_gettime(static_cast(CLOCK_REALTIME), &ts)){ + S3FS_PRN_WARN("failed to clock_gettime by errno(%d)", errno); + ts.tv_sec = time(nullptr); + ts.tv_nsec = 0; + } + return &ts; +} + +std::string s3fs_str_realtime() +{ + struct timespec ts; + return str(*s3fs_realtime(ts)); +} + +int s3fs_fclose(FILE* fp) +{ + if(fp == nullptr){ + return 0; + } + return fclose(fp); +} + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/s3fs_util.h b/s3fs/s3fs_util.h new file mode 100644 index 0000000..09173e0 --- /dev/null +++ b/s3fs/s3fs_util.h @@ -0,0 +1,123 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef S3FS_S3FS_UTIL_H_ +#define S3FS_S3FS_UTIL_H_ + +#include + +#ifndef CLOCK_REALTIME +#define CLOCK_REALTIME 0 +#endif +#ifndef CLOCK_MONOTONIC +#define CLOCK_MONOTONIC CLOCK_REALTIME +#endif +#ifndef CLOCK_MONOTONIC_COARSE +#define CLOCK_MONOTONIC_COARSE CLOCK_MONOTONIC +#endif + +//------------------------------------------------------------------- +// Functions +//------------------------------------------------------------------- +std::string get_realpath(const char *path); + +void init_sysconf_vars(); +std::string get_username(uid_t uid); +int is_uid_include_group(uid_t uid, gid_t gid); + +bool init_basename_lock(); +bool destroy_basename_lock(); +std::string mydirname(const char* path); +std::string mydirname(const std::string& path); +std::string mybasename(const char* path); +std::string mybasename(const std::string& path); + +int mkdirp(const std::string& path, mode_t mode); +std::string get_exist_directory_path(const std::string& path); +bool check_exist_dir_permission(const char* dirpath); +bool delete_files_in_dir(const char* dir, bool is_remove_own); + +bool compare_sysname(const char* target); + +void print_launch_message(int argc, char** argv); + +int flock_set(int fd, int type); + +// +// Utility for nanosecond time(timespec) +// +enum class stat_time_type{ + ATIME, + MTIME, + CTIME +}; + +//------------------------------------------------------------------- +// Utility for nanosecond time(timespec) +//------------------------------------------------------------------- +static constexpr struct timespec S3FS_OMIT_TS = {0, UTIME_OMIT}; + +int compare_timespec(const struct timespec& ts1, const struct timespec& ts2); +int compare_timespec(const struct stat& st, stat_time_type type, const struct timespec& ts); +void set_timespec_to_stat(struct stat& st, stat_time_type type, const struct timespec& ts); +struct timespec* set_stat_to_timespec(const struct stat& st, stat_time_type type, struct timespec& ts); +std::string str_stat_time(const struct stat& st, stat_time_type type); +struct timespec* s3fs_realtime(struct timespec& ts); +std::string s3fs_str_realtime(); + +// Wrap fclose since it is illegal to take the address of a stdlib function +int s3fs_fclose(FILE* fp); + +class scope_guard { +public: + template + explicit scope_guard(Callable&& undo_func) + : func(std::forward(undo_func)) + {} + + ~scope_guard() { + if(func != nullptr) { + func(); + } + } + + void dismiss() { + func = nullptr; + } + + scope_guard(const scope_guard&) = delete; + scope_guard(scope_guard&& other) = delete; + scope_guard& operator=(const scope_guard&) = delete; + scope_guard& operator=(scope_guard&&) = delete; + +private: + std::function func; +}; + +#endif // S3FS_S3FS_UTIL_H_ + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/s3fs_version.md b/s3fs/s3fs_version.md new file mode 100644 index 0000000..eec058c --- /dev/null +++ b/s3fs/s3fs_version.md @@ -0,0 +1,2 @@ +commit 70a30d6e26a5dfd07a00cf79ce1196079e5ab11a (tag: v1.94) +Date: Fri Feb 23 12:56:01 2024 +0900 diff --git a/s3fs/s3fs_xml.cpp b/s3fs/s3fs_xml.cpp new file mode 100644 index 0000000..1b9507a --- /dev/null +++ b/s3fs/s3fs_xml.cpp @@ -0,0 +1,531 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Takeshi Nakatani + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include +#include + +#include "common.h" +#include "s3fs.h" +#include "s3fs_logger.h" +#include "s3fs_xml.h" +#include "s3fs_util.h" +#include "s3objlist.h" +#include "autolock.h" +#include "string_util.h" + +//------------------------------------------------------------------- +// Variables +//------------------------------------------------------------------- +static constexpr char c_strErrorObjectName[] = "FILE or SUBDIR in DIR"; + +// [NOTE] +// mutex for static variables in GetXmlNsUrl +// +static pthread_mutex_t* pxml_parser_mutex = nullptr; + +//------------------------------------------------------------------- +// Functions +//------------------------------------------------------------------- +static bool GetXmlNsUrl(xmlDocPtr doc, std::string& nsurl) +{ + bool result = false; + + if(!pxml_parser_mutex || !doc){ + return result; + } + + std::string tmpNs; + { + static time_t tmLast = 0; // cache for 60 sec. + static std::string strNs; + + AutoLock lock(pxml_parser_mutex); + + if((tmLast + 60) < time(nullptr)){ + // refresh + tmLast = time(nullptr); + strNs = ""; + xmlNodePtr pRootNode = xmlDocGetRootElement(doc); + if(pRootNode){ + xmlNsPtr* nslist = xmlGetNsList(doc, pRootNode); + if(nslist){ + if(nslist[0] && nslist[0]->href){ + int len = xmlStrlen(nslist[0]->href); + if(0 < len){ + strNs = std::string(reinterpret_cast(nslist[0]->href), len); + } + } + S3FS_XMLFREE(nslist); + } + } + } + tmpNs = strNs; + } + if(!tmpNs.empty()){ + nsurl = tmpNs; + result = true; + } + return result; +} + +static unique_ptr_xmlChar get_base_exp(xmlDocPtr doc, const char* exp) +{ + std::string xmlnsurl; + std::string exp_string; + + if(!doc){ + return {nullptr, xmlFree}; + } + unique_ptr_xmlXPathContext ctx(xmlXPathNewContext(doc), xmlXPathFreeContext); + + if(!noxmlns && GetXmlNsUrl(doc, xmlnsurl)){ + xmlXPathRegisterNs(ctx.get(), reinterpret_cast("s3"), reinterpret_cast(xmlnsurl.c_str())); + exp_string = "/s3:ListBucketResult/s3:"; + } else { + exp_string = "/ListBucketResult/"; + } + + exp_string += exp; + + unique_ptr_xmlXPathObject marker_xp(xmlXPathEvalExpression(reinterpret_cast(exp_string.c_str()), ctx.get()), xmlXPathFreeObject); + if(nullptr == marker_xp){ + return {nullptr, xmlFree}; + } + if(xmlXPathNodeSetIsEmpty(marker_xp->nodesetval)){ + S3FS_PRN_INFO("marker_xp->nodesetval is empty."); + return {nullptr, xmlFree}; + } + xmlNodeSetPtr nodes = marker_xp->nodesetval; + + unique_ptr_xmlChar result(xmlNodeListGetString(doc, nodes->nodeTab[0]->xmlChildrenNode, 1), xmlFree); + return result; +} + +static unique_ptr_xmlChar get_prefix(xmlDocPtr doc) +{ + return get_base_exp(doc, "Prefix"); +} + +unique_ptr_xmlChar get_next_continuation_token(xmlDocPtr doc) +{ + return get_base_exp(doc, "NextContinuationToken"); +} + +unique_ptr_xmlChar get_next_marker(xmlDocPtr doc) +{ + return get_base_exp(doc, "NextMarker"); +} + +// return: the pointer to object name on allocated memory. +// the pointer to "c_strErrorObjectName".(not allocated) +// nullptr(a case of something error occurred) +static char* get_object_name(xmlDocPtr doc, xmlNodePtr node, const char* path) +{ + // Get full path + unique_ptr_xmlChar fullpath(xmlNodeListGetString(doc, node, 1), xmlFree); + if(!fullpath){ + S3FS_PRN_ERR("could not get object full path name.."); + return nullptr; + } + // basepath(path) is as same as fullpath. + if(0 == strcmp(reinterpret_cast(fullpath.get()), path)){ + return const_cast(c_strErrorObjectName); + } + + // Make dir path and filename + std::string strdirpath = mydirname(reinterpret_cast(fullpath.get())); + std::string strmybpath = mybasename(reinterpret_cast(fullpath.get())); + const char* dirpath = strdirpath.c_str(); + const char* mybname = strmybpath.c_str(); + const char* basepath= (path && '/' == path[0]) ? &path[1] : path; + + if('\0' == mybname[0]){ + return nullptr; + } + + // check subdir & file in subdir + if(0 < strlen(dirpath)){ + // case of "/" + if(0 == strcmp(mybname, "/") && 0 == strcmp(dirpath, "/")){ + return const_cast(c_strErrorObjectName); + } + // case of "." + if(0 == strcmp(mybname, ".") && 0 == strcmp(dirpath, ".")){ + return const_cast(c_strErrorObjectName); + } + // case of ".." + if(0 == strcmp(mybname, "..") && 0 == strcmp(dirpath, ".")){ + return const_cast(c_strErrorObjectName); + } + // case of "name" + if(0 == strcmp(dirpath, ".")){ + // OK + return strdup(mybname); + }else{ + if(basepath && 0 == strcmp(dirpath, basepath)){ + // OK + return strdup(mybname); + }else if(basepath && 0 < strlen(basepath) && '/' == basepath[strlen(basepath) - 1] && 0 == strncmp(dirpath, basepath, strlen(basepath) - 1)){ + std::string withdirname; + if(strlen(dirpath) > strlen(basepath)){ + withdirname = &dirpath[strlen(basepath)]; + } + // cppcheck-suppress unmatchedSuppression + // cppcheck-suppress knownConditionTrueFalse + if(!withdirname.empty() && '/' != *withdirname.rbegin()){ + withdirname += "/"; + } + withdirname += mybname; + return strdup(withdirname.c_str()); + } + } + } + // case of something wrong + return const_cast(c_strErrorObjectName); +} + +static unique_ptr_xmlChar get_exp_value_xml(xmlDocPtr doc, xmlXPathContextPtr ctx, const char* exp_key) +{ + if(!doc || !ctx || !exp_key){ + return {nullptr, xmlFree}; + } + + xmlNodeSetPtr exp_nodes; + + // search exp_key tag + unique_ptr_xmlXPathObject exp(xmlXPathEvalExpression(reinterpret_cast(exp_key), ctx), xmlXPathFreeObject); + if(nullptr == exp){ + S3FS_PRN_ERR("Could not find key(%s).", exp_key); + return {nullptr, xmlFree}; + } + if(xmlXPathNodeSetIsEmpty(exp->nodesetval)){ + S3FS_PRN_ERR("Key(%s) node is empty.", exp_key); + return {nullptr, xmlFree}; + } + // get exp_key value & set in struct + exp_nodes = exp->nodesetval; + unique_ptr_xmlChar exp_value(xmlNodeListGetString(doc, exp_nodes->nodeTab[0]->xmlChildrenNode, 1), xmlFree); + if(nullptr == exp_value){ + S3FS_PRN_ERR("Key(%s) value is empty.", exp_key); + return {nullptr, xmlFree}; + } + + return exp_value; +} + +bool get_incomp_mpu_list(xmlDocPtr doc, incomp_mpu_list_t& list) +{ + if(!doc){ + return false; + } + + unique_ptr_xmlXPathContext ctx(xmlXPathNewContext(doc), xmlXPathFreeContext); + + std::string xmlnsurl; + std::string ex_upload = "//"; + std::string ex_key; + std::string ex_id; + std::string ex_date; + + if(!noxmlns && GetXmlNsUrl(doc, xmlnsurl)){ + xmlXPathRegisterNs(ctx.get(), reinterpret_cast("s3"), reinterpret_cast(xmlnsurl.c_str())); + ex_upload += "s3:"; + ex_key += "s3:"; + ex_id += "s3:"; + ex_date += "s3:"; + } + ex_upload += "Upload"; + ex_key += "Key"; + ex_id += "UploadId"; + ex_date += "Initiated"; + + // get "Upload" Tags + unique_ptr_xmlXPathObject upload_xp(xmlXPathEvalExpression(reinterpret_cast(ex_upload.c_str()), ctx.get()), xmlXPathFreeObject); + if(nullptr == upload_xp){ + S3FS_PRN_ERR("xmlXPathEvalExpression returns null."); + return false; + } + if(xmlXPathNodeSetIsEmpty(upload_xp->nodesetval)){ + S3FS_PRN_INFO("upload_xp->nodesetval is empty."); + return true; + } + + // Make list + int cnt; + xmlNodeSetPtr upload_nodes; + list.clear(); + for(cnt = 0, upload_nodes = upload_xp->nodesetval; cnt < upload_nodes->nodeNr; cnt++){ + ctx->node = upload_nodes->nodeTab[cnt]; + + INCOMP_MPU_INFO part; + + // search "Key" tag + unique_ptr_xmlChar ex_value(get_exp_value_xml(doc, ctx.get(), ex_key.c_str())); + if(nullptr == ex_value){ + continue; + } + if('/' != *(reinterpret_cast(ex_value.get()))){ + part.key = "/"; + }else{ + part.key = ""; + } + part.key += reinterpret_cast(ex_value.get()); + + // search "UploadId" tag + if(nullptr == (ex_value = get_exp_value_xml(doc, ctx.get(), ex_id.c_str()))){ + continue; + } + part.id = reinterpret_cast(ex_value.get()); + + // search "Initiated" tag + if(nullptr == (ex_value = get_exp_value_xml(doc, ctx.get(), ex_date.c_str()))){ + continue; + } + part.date = reinterpret_cast(ex_value.get()); + + list.push_back(part); + } + + return true; +} + +bool is_truncated(xmlDocPtr doc) +{ + unique_ptr_xmlChar strTruncate(get_base_exp(doc, "IsTruncated")); + if(!strTruncate){ + return false; + } + return 0 == strcasecmp(reinterpret_cast(strTruncate.get()), "true"); +} + +int append_objects_from_xml_ex(const char* path, xmlDocPtr doc, xmlXPathContextPtr ctx, const char* ex_contents, const char* ex_key, const char* ex_etag, int isCPrefix, S3ObjList& head, bool prefix) +{ + xmlNodeSetPtr content_nodes; + + unique_ptr_xmlXPathObject contents_xp(xmlXPathEvalExpression(reinterpret_cast(ex_contents), ctx), xmlXPathFreeObject); + if(nullptr == contents_xp){ + S3FS_PRN_ERR("xmlXPathEvalExpression returns null."); + return -1; + } + if(xmlXPathNodeSetIsEmpty(contents_xp->nodesetval)){ + S3FS_PRN_DBG("contents_xp->nodesetval is empty."); + return 0; + } + content_nodes = contents_xp->nodesetval; + + bool is_dir; + std::string stretag; + int i; + for(i = 0; i < content_nodes->nodeNr; i++){ + ctx->node = content_nodes->nodeTab[i]; + + // object name + unique_ptr_xmlXPathObject key(xmlXPathEvalExpression(reinterpret_cast(ex_key), ctx), xmlXPathFreeObject); + if(nullptr == key){ + S3FS_PRN_WARN("key is null. but continue."); + continue; + } + if(xmlXPathNodeSetIsEmpty(key->nodesetval)){ + S3FS_PRN_WARN("node is empty. but continue."); + continue; + } + xmlNodeSetPtr key_nodes = key->nodesetval; + char* name = get_object_name(doc, key_nodes->nodeTab[0]->xmlChildrenNode, path); + + if(!name){ + S3FS_PRN_WARN("name is something wrong. but continue."); + + }else if(reinterpret_cast(name) != c_strErrorObjectName){ + is_dir = isCPrefix ? true : false; + stretag = ""; + + if(!isCPrefix && ex_etag){ + // Get ETag + unique_ptr_xmlXPathObject ETag(xmlXPathEvalExpression(reinterpret_cast(ex_etag), ctx), xmlXPathFreeObject); + if(nullptr != ETag){ + if(xmlXPathNodeSetIsEmpty(ETag->nodesetval)){ + S3FS_PRN_INFO("ETag->nodesetval is empty."); + }else{ + xmlNodeSetPtr etag_nodes = ETag->nodesetval; + unique_ptr_xmlChar petag(xmlNodeListGetString(doc, etag_nodes->nodeTab[0]->xmlChildrenNode, 1), xmlFree); + if(petag){ + stretag = reinterpret_cast(petag.get()); + } + } + } + } + + // [NOTE] + // The XML data passed to this function is CR code(\r) encoded. + // The function below decodes that encoded CR code. + // + std::string decname = get_decoded_cr_code(name); + free(name); + + if(prefix){ + head.common_prefixes.push_back(decname); + } + if(!head.insert(decname.c_str(), (!stretag.empty() ? stretag.c_str() : nullptr), is_dir)){ + S3FS_PRN_ERR("insert_object returns with error."); + return -1; + } + }else{ + S3FS_PRN_DBG("name is file or subdir in dir. but continue."); + } + } + + return 0; +} + +int append_objects_from_xml(const char* path, xmlDocPtr doc, S3ObjList& head) +{ + std::string xmlnsurl; + std::string ex_contents = "//"; + std::string ex_key; + std::string ex_cprefix = "//"; + std::string ex_prefix; + std::string ex_etag; + + if(!doc){ + return -1; + } + + // If there is not , use path instead of it. + auto pprefix = get_prefix(doc); + std::string prefix = (pprefix ? reinterpret_cast(pprefix.get()) : path ? path : ""); + + unique_ptr_xmlXPathContext ctx(xmlXPathNewContext(doc), xmlXPathFreeContext); + + if(!noxmlns && GetXmlNsUrl(doc, xmlnsurl)){ + xmlXPathRegisterNs(ctx.get(), reinterpret_cast("s3"), reinterpret_cast(xmlnsurl.c_str())); + ex_contents+= "s3:"; + ex_key += "s3:"; + ex_cprefix += "s3:"; + ex_prefix += "s3:"; + ex_etag += "s3:"; + } + ex_contents+= "Contents"; + ex_key += "Key"; + ex_cprefix += "CommonPrefixes"; + ex_prefix += "Prefix"; + ex_etag += "ETag"; + + if(-1 == append_objects_from_xml_ex(prefix.c_str(), doc, ctx.get(), ex_contents.c_str(), ex_key.c_str(), ex_etag.c_str(), 0, head, /*prefix=*/ false) || + -1 == append_objects_from_xml_ex(prefix.c_str(), doc, ctx.get(), ex_cprefix.c_str(), ex_prefix.c_str(), nullptr, 1, head, /*prefix=*/ true) ) + { + S3FS_PRN_ERR("append_objects_from_xml_ex returns with error."); + return -1; + } + + return 0; +} + +//------------------------------------------------------------------- +// Utility functions +//------------------------------------------------------------------- +bool simple_parse_xml(const char* data, size_t len, const char* key, std::string& value) +{ + bool result = false; + + if(!data || !key){ + return false; + } + value.clear(); + + std::unique_ptr doc(xmlReadMemory(data, static_cast(len), "", nullptr, 0), xmlFreeDoc); + if(nullptr == doc){ + return false; + } + + if(nullptr == doc->children){ + return false; + } + for(xmlNodePtr cur_node = doc->children->children; nullptr != cur_node; cur_node = cur_node->next){ + // For DEBUG + // std::string cur_node_name(reinterpret_cast(cur_node->name)); + // printf("cur_node_name: %s\n", cur_node_name.c_str()); + + if(XML_ELEMENT_NODE == cur_node->type){ + std::string elementName = reinterpret_cast(cur_node->name); + // For DEBUG + // printf("elementName: %s\n", elementName.c_str()); + + if(cur_node->children){ + if(XML_TEXT_NODE == cur_node->children->type){ + if(elementName == key) { + value = reinterpret_cast(cur_node->children->content); + result = true; + break; + } + } + } + } + } + + return result; +} + +//------------------------------------------------------------------- +// Utility for lock +//------------------------------------------------------------------- +bool init_parser_xml_lock() +{ + if(pxml_parser_mutex){ + return false; + } + pxml_parser_mutex = new pthread_mutex_t; + + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); +#if S3FS_PTHREAD_ERRORCHECK + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK); +#endif + + if(0 != pthread_mutex_init(pxml_parser_mutex, &attr)){ + delete pxml_parser_mutex; + pxml_parser_mutex = nullptr; + return false; + } + return true; +} + +bool destroy_parser_xml_lock() +{ + if(!pxml_parser_mutex){ + return false; + } + if(0 != pthread_mutex_destroy(pxml_parser_mutex)){ + return false; + } + delete pxml_parser_mutex; + pxml_parser_mutex = nullptr; + + return true; +} + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/s3fs_xml.h b/s3fs/s3fs_xml.h new file mode 100644 index 0000000..4f514df --- /dev/null +++ b/s3fs/s3fs_xml.h @@ -0,0 +1,62 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef S3FS_S3FS_XML_H_ +#define S3FS_S3FS_XML_H_ + +#include +#include // [NOTE] nessetially include this header in some environments +#include +#include + +#include "mpu_util.h" + +class S3ObjList; + +typedef std::unique_ptr unique_ptr_xmlChar; +typedef std::unique_ptr unique_ptr_xmlXPathObject; +typedef std::unique_ptr unique_ptr_xmlXPathContext; +typedef std::unique_ptr unique_ptr_xmlDoc; + +//------------------------------------------------------------------- +// Functions +//------------------------------------------------------------------- +bool is_truncated(xmlDocPtr doc); +int append_objects_from_xml_ex(const char* path, xmlDocPtr doc, xmlXPathContextPtr ctx, const char* ex_contents, const char* ex_key, const char* ex_etag, int isCPrefix, S3ObjList& head, bool prefix); +int append_objects_from_xml(const char* path, xmlDocPtr doc, S3ObjList& head); +unique_ptr_xmlChar get_next_continuation_token(xmlDocPtr doc); +unique_ptr_xmlChar get_next_marker(xmlDocPtr doc); +bool get_incomp_mpu_list(xmlDocPtr doc, incomp_mpu_list_t& list); + +bool simple_parse_xml(const char* data, size_t len, const char* key, std::string& value); + +bool init_parser_xml_lock(); +bool destroy_parser_xml_lock(); + +#endif // S3FS_S3FS_XML_H_ + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/s3objlist.cpp b/s3fs/s3objlist.cpp new file mode 100644 index 0000000..592c99d --- /dev/null +++ b/s3fs/s3objlist.cpp @@ -0,0 +1,282 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Takeshi Nakatani + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include + +#include "s3objlist.h" + +//------------------------------------------------------------------- +// Class S3ObjList +//------------------------------------------------------------------- +// New class S3ObjList is base on old s3_object struct. +// This class is for S3 compatible clients. +// +// If name is terminated by "/", it is forced dir type. +// If name is terminated by "_$folder$", it is forced dir type. +// If is_dir is true and name is not terminated by "/", the name is added "/". +// +bool S3ObjList::insert(const char* name, const char* etag, bool is_dir) +{ + if(!name || '\0' == name[0]){ + return false; + } + + s3obj_t::iterator iter; + std::string newname; + std::string orgname = name; + + // Normalization + std::string::size_type pos = orgname.find("_$folder$"); + if(std::string::npos != pos){ + newname = orgname.substr(0, pos); + is_dir = true; + }else{ + newname = orgname; + } + if(is_dir){ + if('/' != *newname.rbegin()){ + newname += "/"; + } + }else{ + if('/' == *newname.rbegin()){ + is_dir = true; + } + } + + // Check derived name object. + if(is_dir){ + std::string chkname = newname.substr(0, newname.length() - 1); + if(objects.end() != (iter = objects.find(chkname))){ + // found "dir" object --> remove it. + objects.erase(iter); + } + }else{ + std::string chkname = newname + "/"; + if(objects.end() != (iter = objects.find(chkname))){ + // found "dir/" object --> not add new object. + // and add normalization + return insert_normalized(orgname.c_str(), chkname.c_str(), true); + } + } + + // Add object + if(objects.end() != (iter = objects.find(newname))){ + // Found same object --> update information. + (*iter).second.normalname.erase(); + (*iter).second.orgname = orgname; + (*iter).second.is_dir = is_dir; + if(etag){ + (*iter).second.etag = etag; // over write + } + }else{ + // add new object + s3obj_entry newobject; + newobject.orgname = orgname; + newobject.is_dir = is_dir; + if(etag){ + newobject.etag = etag; + } + objects[newname] = newobject; + } + + // add normalization + return insert_normalized(orgname.c_str(), newname.c_str(), is_dir); +} + +bool S3ObjList::insert_normalized(const char* name, const char* normalized, bool is_dir) +{ + if(!name || '\0' == name[0] || !normalized || '\0' == normalized[0]){ + return false; + } + if(0 == strcmp(name, normalized)){ + return true; + } + + s3obj_t::iterator iter; + if(objects.end() != (iter = objects.find(name))){ + // found name --> over write + iter->second.orgname.erase(); + iter->second.etag.erase(); + iter->second.normalname = normalized; + iter->second.is_dir = is_dir; + }else{ + // not found --> add new object + s3obj_entry newobject; + newobject.normalname = normalized; + newobject.is_dir = is_dir; + objects[name] = newobject; + } + return true; +} + +const s3obj_entry* S3ObjList::GetS3Obj(const char* name) const +{ + s3obj_t::const_iterator iter; + + if(!name || '\0' == name[0]){ + return nullptr; + } + if(objects.end() == (iter = objects.find(name))){ + return nullptr; + } + return &((*iter).second); +} + +std::string S3ObjList::GetOrgName(const char* name) const +{ + const s3obj_entry* ps3obj; + + if(!name || '\0' == name[0]){ + return ""; + } + if(nullptr == (ps3obj = GetS3Obj(name))){ + return ""; + } + return ps3obj->orgname; +} + +std::string S3ObjList::GetNormalizedName(const char* name) const +{ + const s3obj_entry* ps3obj; + + if(!name || '\0' == name[0]){ + return ""; + } + if(nullptr == (ps3obj = GetS3Obj(name))){ + return ""; + } + if(ps3obj->normalname.empty()){ + return name; + } + return ps3obj->normalname; +} + +std::string S3ObjList::GetETag(const char* name) const +{ + const s3obj_entry* ps3obj; + + if(!name || '\0' == name[0]){ + return ""; + } + if(nullptr == (ps3obj = GetS3Obj(name))){ + return ""; + } + return ps3obj->etag; +} + +bool S3ObjList::IsDir(const char* name) const +{ + const s3obj_entry* ps3obj; + + if(nullptr == (ps3obj = GetS3Obj(name))){ + return false; + } + return ps3obj->is_dir; +} + +bool S3ObjList::GetLastName(std::string& lastname) const +{ + bool result = false; + lastname = ""; + for(s3obj_t::const_iterator iter = objects.begin(); iter != objects.end(); ++iter){ + if((*iter).second.orgname.length()){ + if(lastname.compare(iter->second.orgname) < 0){ + lastname = (*iter).second.orgname; + result = true; + } + }else{ + if(lastname.compare(iter->second.normalname) < 0){ + lastname = (*iter).second.normalname; + result = true; + } + } + } + return result; +} + +bool S3ObjList::GetNameList(s3obj_list_t& list, bool OnlyNormalized, bool CutSlash) const +{ + s3obj_t::const_iterator iter; + + for(iter = objects.begin(); objects.end() != iter; ++iter){ + if(OnlyNormalized && !iter->second.normalname.empty()){ + continue; + } + std::string name = (*iter).first; + if(CutSlash && 1 < name.length() && '/' == *name.rbegin()){ + // only "/" std::string is skipped this. + name.erase(name.length() - 1); + } + list.push_back(name); + } + return true; +} + +typedef std::map s3obj_h_t; + +bool S3ObjList::MakeHierarchizedList(s3obj_list_t& list, bool haveSlash) +{ + s3obj_h_t h_map; + s3obj_h_t::iterator hiter; + s3obj_list_t::const_iterator liter; + + for(liter = list.begin(); list.end() != liter; ++liter){ + std::string strtmp = (*liter); + if(1 < strtmp.length() && '/' == *strtmp.rbegin()){ + strtmp.erase(strtmp.length() - 1); + } + h_map[strtmp] = true; + + // check hierarchized directory + for(std::string::size_type pos = strtmp.find_last_of('/'); std::string::npos != pos; pos = strtmp.find_last_of('/')){ + strtmp.erase(pos); + if(strtmp.empty() || "/" == strtmp){ + break; + } + if(h_map.end() == h_map.find(strtmp)){ + // not found + h_map[strtmp] = false; + } + } + } + + // check map and add lost hierarchized directory. + for(hiter = h_map.begin(); hiter != h_map.end(); ++hiter){ + if(false == (*hiter).second){ + // add hierarchized directory. + std::string strtmp = (*hiter).first; + if(haveSlash){ + strtmp += "/"; + } + list.push_back(strtmp); + } + } + return true; +} + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/s3objlist.h b/s3fs/s3objlist.h new file mode 100644 index 0000000..ffd2d9b --- /dev/null +++ b/s3fs/s3objlist.h @@ -0,0 +1,85 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef S3FS_S3OBJLIST_H_ +#define S3FS_S3OBJLIST_H_ + +#include +#include +#include + +//------------------------------------------------------------------- +// Structure / Typedef +//------------------------------------------------------------------- +struct s3obj_entry{ + std::string normalname; // normalized name: if empty, object is normalized name. + std::string orgname; // original name: if empty, object is original name. + std::string etag; + bool is_dir; + + s3obj_entry() : is_dir(false) {} +}; + +typedef std::map s3obj_t; +typedef std::vector s3obj_list_t; + +//------------------------------------------------------------------- +// Class S3ObjList +//------------------------------------------------------------------- +class S3ObjList +{ + private: + s3obj_t objects; + public: + std::vector common_prefixes; + + private: + bool insert_normalized(const char* name, const char* normalized, bool is_dir); + const s3obj_entry* GetS3Obj(const char* name) const; + + s3obj_t::const_iterator begin() const { return objects.begin(); } + s3obj_t::const_iterator end() const { return objects.end(); } + + public: + S3ObjList() {} + ~S3ObjList() {} + + bool IsEmpty() const { return objects.empty(); } + bool insert(const char* name, const char* etag = nullptr, bool is_dir = false); + std::string GetOrgName(const char* name) const; + std::string GetNormalizedName(const char* name) const; + std::string GetETag(const char* name) const; + bool IsDir(const char* name) const; + bool GetNameList(s3obj_list_t& list, bool OnlyNormalized = true, bool CutSlash = true) const; + bool GetLastName(std::string& lastname) const; + + static bool MakeHierarchizedList(s3obj_list_t& list, bool haveSlash); +}; + +#endif // S3FS_S3OBJLIST_H_ + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/sighandlers.cpp b/s3fs/sighandlers.cpp new file mode 100644 index 0000000..81055fc --- /dev/null +++ b/s3fs/sighandlers.cpp @@ -0,0 +1,267 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include +#include + +#include "s3fs_logger.h" +#include "sighandlers.h" +#include "fdcache.h" + +//------------------------------------------------------------------- +// Class S3fsSignals +//------------------------------------------------------------------- +std::unique_ptr S3fsSignals::pSingleton; +bool S3fsSignals::enableUsr1 = false; + +//------------------------------------------------------------------- +// Class methods +//------------------------------------------------------------------- +bool S3fsSignals::Initialize() +{ + if(!S3fsSignals::pSingleton){ + S3fsSignals::pSingleton.reset(new S3fsSignals); + } + return true; +} + +bool S3fsSignals::Destroy() +{ + S3fsSignals::pSingleton.reset(); + return true; +} + +void S3fsSignals::HandlerUSR1(int sig) +{ + if(SIGUSR1 != sig){ + S3FS_PRN_ERR("The handler for SIGUSR1 received signal(%d)", sig); + return; + } + + S3fsSignals* pSigobj = S3fsSignals::get(); + if(!pSigobj){ + S3FS_PRN_ERR("S3fsSignals object is not initialized."); + return; + } + + if(!pSigobj->WakeupUsr1Thread()){ + S3FS_PRN_ERR("Failed to wakeup the thread for SIGUSR1."); + return; + } +} + +bool S3fsSignals::SetUsr1Handler(const char* path) +{ + if(!FdManager::HaveLseekHole()){ + S3FS_PRN_ERR("Could not set SIGUSR1 for checking cache, because this system does not support SEEK_DATA/SEEK_HOLE in lseek function."); + return false; + } + + // set output file + if(!FdManager::SetCacheCheckOutput(path)){ + S3FS_PRN_ERR("Could not set output file(%s) for checking cache.", path ? path : "null(stdout)"); + return false; + } + + S3fsSignals::enableUsr1 = true; + + return true; +} + +void* S3fsSignals::CheckCacheWorker(void* arg) +{ + Semaphore* pSem = static_cast(arg); + if(!pSem){ + pthread_exit(nullptr); + } + if(!S3fsSignals::enableUsr1){ + pthread_exit(nullptr); + } + + // wait and loop + while(S3fsSignals::enableUsr1){ + // wait + pSem->wait(); + + // cppcheck-suppress unmatchedSuppression + // cppcheck-suppress knownConditionTrueFalse + if(!S3fsSignals::enableUsr1){ + break; // assap + } + + // check all cache + if(!FdManager::get()->CheckAllCache()){ + S3FS_PRN_ERR("Processing failed due to some problem."); + } + + // do not allow request queuing + for(int value = pSem->get_value(); 0 < value; value = pSem->get_value()){ + pSem->wait(); + } + } + return nullptr; +} + +void S3fsSignals::HandlerUSR2(int sig) +{ + if(SIGUSR2 == sig){ + S3fsLog::BumpupLogLevel(); + }else{ + S3FS_PRN_ERR("The handler for SIGUSR2 received signal(%d)", sig); + } +} + +bool S3fsSignals::InitUsr2Handler() +{ + struct sigaction sa; + + memset(&sa, 0, sizeof(struct sigaction)); + sa.sa_handler = S3fsSignals::HandlerUSR2; + sa.sa_flags = SA_RESTART; + if(0 != sigaction(SIGUSR2, &sa, nullptr)){ + return false; + } + return true; +} + +void S3fsSignals::HandlerHUP(int sig) +{ + if(SIGHUP == sig){ + S3fsLog::ReopenLogfile(); + }else{ + S3FS_PRN_ERR("The handler for SIGHUP received signal(%d)", sig); + } +} + +bool S3fsSignals::InitHupHandler() +{ + struct sigaction sa; + + memset(&sa, 0, sizeof(struct sigaction)); + sa.sa_handler = S3fsSignals::HandlerHUP; + sa.sa_flags = SA_RESTART; + if(0 != sigaction(SIGHUP, &sa, nullptr)){ + return false; + } + return true; +} + +//------------------------------------------------------------------- +// Methods +//------------------------------------------------------------------- +S3fsSignals::S3fsSignals() +{ + if(S3fsSignals::enableUsr1){ + if(!InitUsr1Handler()){ + S3FS_PRN_ERR("failed creating thread for SIGUSR1 handler, but continue..."); + } + } + if(!S3fsSignals::InitUsr2Handler()){ + S3FS_PRN_ERR("failed to initialize SIGUSR2 handler for bumping log level, but continue..."); + } + if(!S3fsSignals::InitHupHandler()){ + S3FS_PRN_ERR("failed to initialize SIGHUP handler for reopen log file, but continue..."); + } +} + +S3fsSignals::~S3fsSignals() +{ + if(S3fsSignals::enableUsr1){ + if(!DestroyUsr1Handler()){ + S3FS_PRN_ERR("failed stopping thread for SIGUSR1 handler, but continue..."); + } + } +} + +bool S3fsSignals::InitUsr1Handler() +{ + if(pThreadUsr1 || pSemUsr1){ + S3FS_PRN_ERR("Already run thread for SIGUSR1"); + return false; + } + + // create thread + int result; + std::unique_ptr pSemUsr1_tmp(new Semaphore(0)); + std::unique_ptr pThreadUsr1_tmp(new pthread_t); + if(0 != (result = pthread_create(pThreadUsr1.get(), nullptr, S3fsSignals::CheckCacheWorker, static_cast(pSemUsr1_tmp.get())))){ + S3FS_PRN_ERR("Could not create thread for SIGUSR1 by %d", result); + return false; + } + pSemUsr1 = std::move(pSemUsr1_tmp); + pThreadUsr1 = std::move(pThreadUsr1_tmp); + + // set handler + struct sigaction sa; + memset(&sa, 0, sizeof(struct sigaction)); + sa.sa_handler = S3fsSignals::HandlerUSR1; + sa.sa_flags = SA_RESTART; + if(0 != sigaction(SIGUSR1, &sa, nullptr)){ + S3FS_PRN_ERR("Could not set signal handler for SIGUSR1"); + DestroyUsr1Handler(); + return false; + } + + return true; +} + +bool S3fsSignals::DestroyUsr1Handler() +{ + if(!pThreadUsr1 || !pSemUsr1){ + return false; + } + // for thread exit + S3fsSignals::enableUsr1 = false; + + // wakeup thread + pSemUsr1->post(); + + // wait for thread exiting + void* retval = nullptr; + int result; + if(0 != (result = pthread_join(*pThreadUsr1, &retval))){ + S3FS_PRN_ERR("Could not stop thread for SIGUSR1 by %d", result); + return false; + } + pSemUsr1.reset(); + pThreadUsr1.reset(); + + return true; +} + +bool S3fsSignals::WakeupUsr1Thread() +{ + if(!pThreadUsr1 || !pSemUsr1){ + S3FS_PRN_ERR("The thread for SIGUSR1 is not setup."); + return false; + } + pSemUsr1->post(); + return true; +} + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/sighandlers.h b/s3fs/sighandlers.h new file mode 100644 index 0000000..f4996e6 --- /dev/null +++ b/s3fs/sighandlers.h @@ -0,0 +1,79 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef S3FS_SIGHANDLERS_H_ +#define S3FS_SIGHANDLERS_H_ + +#include + +class Semaphore; + +//---------------------------------------------- +// class S3fsSignals +//---------------------------------------------- +class S3fsSignals +{ + private: + static std::unique_ptr pSingleton; + static bool enableUsr1; + + std::unique_ptr pThreadUsr1; + std::unique_ptr pSemUsr1; + + protected: + static S3fsSignals* get() { return pSingleton.get(); } + + static void HandlerUSR1(int sig); + static void* CheckCacheWorker(void* arg); + + static void HandlerUSR2(int sig); + static bool InitUsr2Handler(); + + static void HandlerHUP(int sig); + static bool InitHupHandler(); + + S3fsSignals(); + S3fsSignals(const S3fsSignals&) = delete; + S3fsSignals(S3fsSignals&&) = delete; + S3fsSignals& operator=(const S3fsSignals&) = delete; + S3fsSignals& operator=(S3fsSignals&&) = delete; + + bool InitUsr1Handler(); + bool DestroyUsr1Handler(); + bool WakeupUsr1Thread(); + + public: + ~S3fsSignals(); + static bool Initialize(); + static bool Destroy(); + + static bool SetUsr1Handler(const char* path); +}; + +#endif // S3FS_SIGHANDLERS_H_ + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/string_util.cpp b/s3fs/string_util.cpp new file mode 100644 index 0000000..7e2e8b0 --- /dev/null +++ b/s3fs/string_util.cpp @@ -0,0 +1,669 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include +#include +#include +#include + +#include + +#include "s3fs_logger.h" +#include "string_util.h" + +//------------------------------------------------------------------- +// Global variables +//------------------------------------------------------------------- + +//------------------------------------------------------------------- +// Functions +//------------------------------------------------------------------- + +std::string str(const struct timespec value) +{ + std::ostringstream s; + s << value.tv_sec; + if(value.tv_nsec != 0){ + s << "." << std::setfill('0') << std::setw(9) << value.tv_nsec; + } + return s.str(); +} + +#ifdef __MSYS__ +/* + * Polyfill for strptime function + * + * This source code is from https://gist.github.com/jeremyfromearth/5694aa3a66714254752179ecf3c95582 . + */ +char* strptime(const char* s, const char* f, struct tm* tm) +{ + std::istringstream input(s); + input.imbue(std::locale(setlocale(LC_ALL, nullptr))); + input >> std::get_time(tm, f); + if (input.fail()) { + return nullptr; + } + return (char*)(s + input.tellg()); +} +#endif + +bool s3fs_strtoofft(off_t* value, const char* str, int base) +{ + if(value == nullptr || str == nullptr){ + return false; + } + errno = 0; + char *temp; + long long result = strtoll(str, &temp, base); + + if(temp == str || *temp != '\0'){ + return false; + } + if((result == LLONG_MIN || result == LLONG_MAX) && errno == ERANGE){ + return false; + } + + *value = result; + return true; +} + +off_t cvt_strtoofft(const char* str, int base) +{ + off_t result = 0; + if(!s3fs_strtoofft(&result, str, base)){ + S3FS_PRN_WARN("something error is occurred in convert std::string(%s) to off_t, thus return 0 as default.", (str ? str : "null")); + return 0; + } + return result; +} + +std::string lower(std::string s) +{ + // change each character of the std::string to lower case + for(size_t i = 0; i < s.length(); i++){ + s[i] = tolower(s[i]); + } + return s; +} + +std::string trim_left(std::string d, const char *t /* = SPACES */) +{ + return d.erase(0, d.find_first_not_of(t)); +} + +std::string trim_right(std::string d, const char *t /* = SPACES */) +{ + std::string::size_type i(d.find_last_not_of(t)); + if(i == std::string::npos){ + return ""; + }else{ + return d.erase(d.find_last_not_of(t) + 1); + } +} + +std::string trim(std::string s, const char *t /* = SPACES */) +{ + return trim_left(trim_right(std::move(s), t), t); +} + +std::string peeloff(const std::string& s) +{ + if(s.size() < 2 || *s.begin() != '"' || *s.rbegin() != '"'){ + return s; + } + return s.substr(1, s.size() - 2); +} + +// +// Three url encode functions +// +// urlEncodeGeneral: A general URL encoding function. +// urlEncodePath : A function that URL encodes by excluding the path +// separator('/'). +// urlEncodeQuery : A function that does URL encoding by excluding +// some characters('=', '&' and '%'). +// This function can be used when the target string +// contains already URL encoded strings. It also +// excludes the character () used in query strings. +// Therefore, it is a function to use as URL encoding +// for use in query strings. +// +static constexpr char encode_general_except_chars[] = ".-_~"; // For general URL encode +static constexpr char encode_path_except_chars[] = ".-_~/"; // For fuse(included path) URL encode +static constexpr char encode_query_except_chars[] = ".-_~=&%"; // For query params(and encoded string) + +static std::string rawUrlEncode(const std::string &s, const char* except_chars) +{ + std::string result; + for (size_t i = 0; i < s.length(); ++i) { + unsigned char c = s[i]; + if((except_chars && nullptr != strchr(except_chars, c)) || + (c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') ) + { + result += c; + }else{ + result += "%"; + result += s3fs_hex_upper(&c, 1); + } + } + return result; +} + +std::string urlEncodeGeneral(const std::string &s) +{ + return rawUrlEncode(s, encode_general_except_chars); +} + +std::string urlEncodePath(const std::string &s) +{ + return rawUrlEncode(s, encode_path_except_chars); +} + +std::string urlEncodeQuery(const std::string &s) +{ + return rawUrlEncode(s, encode_query_except_chars); +} + +std::string urlDecode(const std::string& s) +{ + std::string result; + for(size_t i = 0; i < s.length(); ++i){ + if(s[i] != '%'){ + result += s[i]; + }else{ + int ch = 0; + if(s.length() <= ++i){ + break; // wrong format. + } + ch += ('0' <= s[i] && s[i] <= '9') ? (s[i] - '0') : ('A' <= s[i] && s[i] <= 'F') ? (s[i] - 'A' + 0x0a) : ('a' <= s[i] && s[i] <= 'f') ? (s[i] - 'a' + 0x0a) : 0x00; + if(s.length() <= ++i){ + break; // wrong format. + } + ch *= 16; + ch += ('0' <= s[i] && s[i] <= '9') ? (s[i] - '0') : ('A' <= s[i] && s[i] <= 'F') ? (s[i] - 'A' + 0x0a) : ('a' <= s[i] && s[i] <= 'f') ? (s[i] - 'a' + 0x0a) : 0x00; + result += static_cast(ch); + } + } + return result; +} + +bool takeout_str_dquart(std::string& str) +{ + size_t pos; + + // '"' for start + if(std::string::npos != (pos = str.find_first_of('\"'))){ + str.erase(0, pos + 1); + + // '"' for end + if(std::string::npos == (pos = str.find_last_of('\"'))){ + return false; + } + str.erase(pos); + if(std::string::npos != str.find_first_of('\"')){ + return false; + } + } + return true; +} + +// +// ex. target="http://......?keyword=value&..." +// +bool get_keyword_value(const std::string& target, const char* keyword, std::string& value) +{ + if(!keyword){ + return false; + } + size_t spos; + size_t epos; + if(std::string::npos == (spos = target.find(keyword))){ + return false; + } + spos += strlen(keyword); + if('=' != target[spos]){ + return false; + } + spos++; + if(std::string::npos == (epos = target.find('&', spos))){ + value = target.substr(spos); + }else{ + value = target.substr(spos, (epos - spos)); + } + return true; +} + +// +// Returns the current date +// in a format suitable for a HTTP request header. +// +std::string get_date_rfc850() +{ + char buf[100]; + time_t t = time(nullptr); + struct tm res; + strftime(buf, sizeof(buf), "%a, %d %b %Y %H:%M:%S GMT", gmtime_r(&t, &res)); + return buf; +} + +void get_date_sigv3(std::string& date, std::string& date8601) +{ + time_t tm = time(nullptr); + date = get_date_string(tm); + date8601 = get_date_iso8601(tm); +} + +std::string get_date_string(time_t tm) +{ + char buf[100]; + struct tm res; + strftime(buf, sizeof(buf), "%Y%m%d", gmtime_r(&tm, &res)); + return buf; +} + +std::string get_date_iso8601(time_t tm) +{ + char buf[100]; + struct tm res; + strftime(buf, sizeof(buf), "%Y%m%dT%H%M%SZ", gmtime_r(&tm, &res)); + return buf; +} + +bool get_unixtime_from_iso8601(const char* pdate, time_t& unixtime) +{ + if(!pdate){ + return false; + } + + struct tm tm; + const char* prest = strptime(pdate, "%Y-%m-%dT%T", &tm); + if(prest == pdate){ + // wrong format + return false; + } + unixtime = mktime(&tm); + return true; +} + +// +// Convert to unixtime from std::string which formatted by following: +// "12Y12M12D12h12m12s", "86400s", "9h30m", etc +// +bool convert_unixtime_from_option_arg(const char* argv, time_t& unixtime) +{ + if(!argv){ + return false; + } + unixtime = 0; + const char* ptmp; + int last_unit_type = 0; // unit flag. + bool is_last_number; + time_t tmptime; + for(ptmp = argv, is_last_number = true, tmptime = 0; ptmp && *ptmp; ++ptmp){ + if('0' <= *ptmp && *ptmp <= '9'){ + tmptime *= 10; + tmptime += static_cast(*ptmp - '0'); + is_last_number = true; + }else if(is_last_number){ + if('Y' == *ptmp && 1 > last_unit_type){ + unixtime += (tmptime * (60 * 60 * 24 * 365)); // average 365 day / year + last_unit_type = 1; + }else if('M' == *ptmp && 2 > last_unit_type){ + unixtime += (tmptime * (60 * 60 * 24 * 30)); // average 30 day / month + last_unit_type = 2; + }else if('D' == *ptmp && 3 > last_unit_type){ + unixtime += (tmptime * (60 * 60 * 24)); + last_unit_type = 3; + }else if('h' == *ptmp && 4 > last_unit_type){ + unixtime += (tmptime * (60 * 60)); + last_unit_type = 4; + }else if('m' == *ptmp && 5 > last_unit_type){ + unixtime += (tmptime * 60); + last_unit_type = 5; + }else if('s' == *ptmp && 6 > last_unit_type){ + unixtime += tmptime; + last_unit_type = 6; + }else{ + return false; + } + tmptime = 0; + is_last_number = false; + }else{ + return false; + } + } + if(is_last_number){ + return false; + } + return true; +} + +static std::string s3fs_hex(const unsigned char* input, size_t length, const char *hexAlphabet) +{ + std::string hex; + for(size_t pos = 0; pos < length; ++pos){ + hex += hexAlphabet[input[pos] / 16]; + hex += hexAlphabet[input[pos] % 16]; + } + return hex; +} + +std::string s3fs_hex_lower(const unsigned char* input, size_t length) +{ + return s3fs_hex(input, length, "0123456789abcdef"); +} + +std::string s3fs_hex_upper(const unsigned char* input, size_t length) +{ + return s3fs_hex(input, length, "0123456789ABCDEF"); +} + +std::string s3fs_base64(const unsigned char* input, size_t length) +{ + static constexpr char base[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + + std::string result; + result.reserve(((length + 3 - 1) / 3) * 4 + 1); + + unsigned char parts[4]; + size_t rpos; + for(rpos = 0; rpos < length; rpos += 3){ + parts[0] = (input[rpos] & 0xfc) >> 2; + parts[1] = ((input[rpos] & 0x03) << 4) | ((((rpos + 1) < length ? input[rpos + 1] : 0x00) & 0xf0) >> 4); + parts[2] = (rpos + 1) < length ? (((input[rpos + 1] & 0x0f) << 2) | ((((rpos + 2) < length ? input[rpos + 2] : 0x00) & 0xc0) >> 6)) : 0x40; + parts[3] = (rpos + 2) < length ? (input[rpos + 2] & 0x3f) : 0x40; + + result += base[parts[0]]; + result += base[parts[1]]; + result += base[parts[2]]; + result += base[parts[3]]; + } + + return result; +} + +inline unsigned char char_decode64(const char ch) +{ + unsigned char by; + if('A' <= ch && ch <= 'Z'){ // A - Z + by = static_cast(ch - 'A'); + }else if('a' <= ch && ch <= 'z'){ // a - z + by = static_cast(ch - 'a' + 26); + }else if('0' <= ch && ch <= '9'){ // 0 - 9 + by = static_cast(ch - '0' + 52); + }else if('+' == ch){ // + + by = 62; + }else if('/' == ch){ // / + by = 63; + }else if('=' == ch){ // = + by = 64; + }else{ // something wrong + by = UCHAR_MAX; + } + return by; +} + +std::string s3fs_decode64(const char* input, size_t input_len) +{ + std::string result; + result.reserve(input_len / 4 * 3); + unsigned char parts[4]; + size_t rpos; + for(rpos = 0; rpos < input_len; rpos += 4){ + parts[0] = char_decode64(input[rpos]); + parts[1] = (rpos + 1) < input_len ? char_decode64(input[rpos + 1]) : 64; + parts[2] = (rpos + 2) < input_len ? char_decode64(input[rpos + 2]) : 64; + parts[3] = (rpos + 3) < input_len ? char_decode64(input[rpos + 3]) : 64; + + result += static_cast(((parts[0] << 2) & 0xfc) | ((parts[1] >> 4) & 0x03)); + if(64 == parts[2]){ + break; + } + result += static_cast(((parts[1] << 4) & 0xf0) | ((parts[2] >> 2) & 0x0f)); + if(64 == parts[3]){ + break; + } + result += static_cast(((parts[2] << 6) & 0xc0) | (parts[3] & 0x3f)); + } + return result; +} + +// +// detect and rewrite invalid utf8. We take invalid bytes +// and encode them into a private region of the unicode +// space. This is sometimes known as wtf8, wobbly transformation format. +// it is necessary because S3 validates the utf8 used for identifiers for +// correctness, while some clients may provide invalid utf, notably +// windows using cp1252. +// + +// Base location for transform. The range 0xE000 - 0xF8ff +// is a private range, se use the start of this range. +static constexpr unsigned int escape_base = 0xe000; + +// encode bytes into wobbly utf8. +// 'result' can be null. returns true if transform was needed. +bool s3fs_wtf8_encode(const char *s, std::string *result) +{ + bool invalid = false; + + // Pass valid utf8 code through + for (; *s; s++) { + const unsigned char c = *s; + + // single byte encoding + if (c <= 0x7f) { + if (result) { + *result += c; + } + continue; + } + + // otherwise, it must be one of the valid start bytes + if ( c >= 0xc2 && c <= 0xf5 ) { + // two byte encoding + // don't need bounds check, std::string is zero terminated + if ((c & 0xe0) == 0xc0 && (s[1] & 0xc0) == 0x80) { + // all two byte encodings starting higher than c1 are valid + if (result) { + *result += c; + *result += *(++s); + } + continue; + } + // three byte encoding + if ((c & 0xf0) == 0xe0 && (s[1] & 0xc0) == 0x80 && (s[2] & 0xc0) == 0x80) { + const unsigned code = ((c & 0x0f) << 12) | ((s[1] & 0x3f) << 6) | (s[2] & 0x3f); + if (code >= 0x800 && ! (code >= 0xd800 && code <= 0xd8ff)) { + // not overlong and not a surrogate pair + if (result) { + *result += c; + *result += *(++s); + *result += *(++s); + } + continue; + } + } + // four byte encoding + if ((c & 0xf8) == 0xf0 && (s[1] & 0xc0) == 0x80 && (s[2] & 0xc0) == 0x80 && (s[3] & 0xc0) == 0x80) { + const unsigned code = ((c & 0x07) << 18) | ((s[1] & 0x3f) << 12) | ((s[2] & 0x3f) << 6) | (s[3] & 0x3f); + if (code >= 0x10000 && code <= 0x10ffff) { + // not overlong and in defined unicode space + if (result) { + *result += c; + *result += *(++s); + *result += *(++s); + *result += *(++s); + } + continue; + } + } + } + // printf("invalid %02x at %d\n", c, i); + // Invalid utf8 code. Convert it to a private two byte area of unicode + // e.g. the e000 - f8ff area. This will be a three byte encoding + invalid = true; + if (result) { + unsigned escape = escape_base + c; + *result += static_cast(0xe0 | ((escape >> 12) & 0x0f)); + *result += static_cast(0x80 | ((escape >> 06) & 0x3f)); + *result += static_cast(0x80 | ((escape >> 00) & 0x3f)); + } + } + return invalid; +} + +std::string s3fs_wtf8_encode(const std::string &s) +{ + std::string result; + s3fs_wtf8_encode(s.c_str(), &result); + return result; +} + +// The reverse operation, turn encoded bytes back into their original values +// The code assumes that we map to a three-byte code point. +bool s3fs_wtf8_decode(const char *s, std::string *result) +{ + bool encoded = false; + for (; *s; s++) { + unsigned char c = *s; + // look for a three byte tuple matching our encoding code + if ((c & 0xf0) == 0xe0 && (s[1] & 0xc0) == 0x80 && (s[2] & 0xc0) == 0x80) { + unsigned code = (c & 0x0f) << 12; + code |= (s[1] & 0x3f) << 6; + code |= (s[2] & 0x3f) << 0; + if (code >= escape_base && code <= escape_base + 0xff) { + // convert back + encoded = true; + if(result){ + *result += static_cast(code - escape_base); + } + s+=2; + continue; + } + } + if (result) { + *result += c; + } + } + return encoded; +} + +std::string s3fs_wtf8_decode(const std::string &s) +{ + std::string result; + s3fs_wtf8_decode(s.c_str(), &result); + return result; +} + +// +// Encode only CR('\r'=0x0D) and it also encodes the '%' character accordingly. +// +// The xmlReadMemory() function in libxml2 replaces CR code with LF code('\n'=0x0A) +// due to the XML specification. +// s3fs uses libxml2 to parse the S3 response, and this automatic substitution +// of libxml2 may change the object name(file/dir name). Therefore, before passing +// the response to the xmlReadMemory() function, we need the string encoded by +// this function. +// +// [NOTE] +// Normally the quotes included in the XML content data are HTML encoded("""). +// Encoding for CR can also be HTML encoded as binary code (ex, " "), but +// if the same string content(as file name) as this encoded string exists, the +// original string cannot be distinguished whichever encoded or not encoded. +// Therefore, CR is encoded in the same manner as URL encoding("%0A"). +// And it is assumed that there is no CR code in the S3 response tag etc.(actually +// it shouldn't exist) +// +std::string get_encoded_cr_code(const char* pbase) +{ + std::string result; + if(!pbase){ + return result; + } + std::string strbase(pbase); + size_t baselength = strbase.length(); + size_t startpos = 0; + size_t foundpos; + while(startpos < baselength && std::string::npos != (foundpos = strbase.find_first_of("%\r", startpos))){ + if(0 < (foundpos - startpos)){ + result += strbase.substr(startpos, foundpos - startpos); + } + if('%' == strbase[foundpos]){ + result += "%45"; + }else if('\r' == strbase[foundpos]){ + result += "%0D"; + } + startpos = foundpos + 1; + } + if(startpos < baselength){ + result += strbase.substr(startpos); + } + return result; +} + +// +// Decode a string encoded with get_encoded_cr_code(). +// +std::string get_decoded_cr_code(const char* pencode) +{ + std::string result; + if(!pencode){ + return result; + } + std::string strencode(pencode); + size_t encodelength = strencode.length(); + size_t startpos = 0; + size_t foundpos; + while(startpos < encodelength && std::string::npos != (foundpos = strencode.find('%', startpos))){ + if(0 < (foundpos - startpos)){ + result += strencode.substr(startpos, foundpos - startpos); + } + if((foundpos + 2) < encodelength && 0 == strencode.compare(foundpos, 3, "%45")){ + result += '%'; + startpos = foundpos + 3; + }else if((foundpos + 2) < encodelength && 0 == strencode.compare(foundpos, 3, "%0D")){ + result += '\r'; + startpos = foundpos + 3; + }else if((foundpos + 1) < encodelength && 0 == strencode.compare(foundpos, 2, "%%")){ + result += '%'; + startpos = foundpos + 2; + }else{ + result += '%'; + startpos = foundpos + 1; + } + } + if(startpos < encodelength){ + result += strencode.substr(startpos); + } + return result; +} + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/string_util.h b/s3fs/string_util.h new file mode 100644 index 0000000..077f5a6 --- /dev/null +++ b/s3fs/string_util.h @@ -0,0 +1,136 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef S3FS_STRING_UTIL_H_ +#define S3FS_STRING_UTIL_H_ + +#include +#include + +// +// A collection of string utilities for manipulating URLs and HTTP responses. +// +//------------------------------------------------------------------- +// Global variables +//------------------------------------------------------------------- +static constexpr char SPACES[] = " \t\r\n"; + +//------------------------------------------------------------------- +// Inline functions +//------------------------------------------------------------------- +static inline int is_prefix(const char *str, const char *prefix) { return strncmp(str, prefix, strlen(prefix)) == 0; } +static inline const char* SAFESTRPTR(const char *strptr) { return strptr ? strptr : ""; } + +//------------------------------------------------------------------- +// Macros(WTF8) +//------------------------------------------------------------------- +#define WTF8_ENCODE(ARG) \ + std::string ARG##_buf; \ + const char * ARG = _##ARG; \ + if (use_wtf8 && s3fs_wtf8_encode( _##ARG, 0 )) { \ + s3fs_wtf8_encode( _##ARG, &ARG##_buf); \ + ARG = ARG##_buf.c_str(); \ + } + +//------------------------------------------------------------------- +// Utilities +//------------------------------------------------------------------- +// TODO: rename to to_string? +std::string str(const struct timespec value); + +#ifdef __MSYS__ +// +// Polyfill for strptime function. +// +char* strptime(const char* s, const char* f, struct tm* tm); +#endif +// +// Convert string to off_t. Returns false on bad input. +// Replacement for C++11 std::stoll. +// +bool s3fs_strtoofft(off_t* value, const char* str, int base = 0); +// +// This function returns 0 if a value that cannot be converted is specified. +// Only call if 0 is considered an error and the operation can continue. +// +off_t cvt_strtoofft(const char* str, int base); + +// +// String Manipulation +// +std::string trim_left(std::string s, const char *t = SPACES); +std::string trim_right(std::string s, const char *t = SPACES); +std::string trim(std::string s, const char *t = SPACES); +std::string lower(std::string s); +std::string peeloff(const std::string& s); + +// +// Date string +// +std::string get_date_rfc850(); +void get_date_sigv3(std::string& date, std::string& date8601); +std::string get_date_string(time_t tm); +std::string get_date_iso8601(time_t tm); +bool get_unixtime_from_iso8601(const char* pdate, time_t& unixtime); +bool convert_unixtime_from_option_arg(const char* argv, time_t& unixtime); + +// +// For encoding +// +std::string urlEncodeGeneral(const std::string &s); +std::string urlEncodePath(const std::string &s); +std::string urlEncodeQuery(const std::string &s); +std::string urlDecode(const std::string& s); + +bool takeout_str_dquart(std::string& str); +bool get_keyword_value(const std::string& target, const char* keyword, std::string& value); + +// +// For binary string +// +std::string s3fs_hex_lower(const unsigned char* input, size_t length); +std::string s3fs_hex_upper(const unsigned char* input, size_t length); +std::string s3fs_base64(const unsigned char* input, size_t length); +std::string s3fs_decode64(const char* input, size_t input_len); + +// +// WTF8 +// +bool s3fs_wtf8_encode(const char *s, std::string *result); +std::string s3fs_wtf8_encode(const std::string &s); +bool s3fs_wtf8_decode(const char *s, std::string *result); +std::string s3fs_wtf8_decode(const std::string &s); + +// +// For CR in XML +// +std::string get_encoded_cr_code(const char* pbase); +std::string get_decoded_cr_code(const char* pencode); + +#endif // S3FS_STRING_UTIL_H_ + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/threadpoolman.cpp b/s3fs/threadpoolman.cpp new file mode 100644 index 0000000..682529d --- /dev/null +++ b/s3fs/threadpoolman.cpp @@ -0,0 +1,264 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Takeshi Nakatani + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include +#include +#include + +#include "s3fs_logger.h" +#include "threadpoolman.h" +#include "autolock.h" + +//------------------------------------------------ +// ThreadPoolMan class variables +//------------------------------------------------ +ThreadPoolMan* ThreadPoolMan::singleton = nullptr; + +//------------------------------------------------ +// ThreadPoolMan class methods +//------------------------------------------------ +bool ThreadPoolMan::Initialize(int count) +{ + if(ThreadPoolMan::singleton){ + S3FS_PRN_WARN("Already singleton for Thread Manager is existed, then re-create it."); + ThreadPoolMan::Destroy(); + } + ThreadPoolMan::singleton = new ThreadPoolMan(count); + return true; +} + +void ThreadPoolMan::Destroy() +{ + if(ThreadPoolMan::singleton){ + delete ThreadPoolMan::singleton; + ThreadPoolMan::singleton = nullptr; + } +} + +bool ThreadPoolMan::Instruct(std::unique_ptr pparam) +{ + if(!ThreadPoolMan::singleton){ + S3FS_PRN_WARN("The singleton object is not initialized yet."); + return false; + } + return ThreadPoolMan::singleton->SetInstruction(std::move(pparam)); +} + +// +// Thread worker +// +void* ThreadPoolMan::Worker(void* arg) +{ + ThreadPoolMan* psingleton = static_cast(arg); + + if(!psingleton){ + S3FS_PRN_ERR("The parameter for worker thread is invalid."); + return reinterpret_cast(-EIO); + } + S3FS_PRN_INFO3("Start worker thread in ThreadPoolMan."); + + while(!psingleton->IsExit()){ + // wait + psingleton->thpoolman_sem.wait(); + + if(psingleton->IsExit()){ + break; + } + + // get instruction + std::unique_ptr pparam; + { + AutoLock auto_lock(&(psingleton->thread_list_lock)); + + if(!psingleton->instruction_list.empty()){ + pparam = std::move(psingleton->instruction_list.front()); + psingleton->instruction_list.pop_front(); + if(!pparam){ + S3FS_PRN_WARN("Got a semaphore, but the instruction is empty."); + } + }else{ + S3FS_PRN_WARN("Got a semaphore, but there is no instruction."); + pparam = nullptr; + } + } + + if(pparam){ + void* retval = pparam->pfunc(pparam->args); + if(nullptr != retval){ + S3FS_PRN_WARN("The instruction function returned with somthign error code(%ld).", reinterpret_cast(retval)); + } + if(pparam->psem){ + pparam->psem->post(); + } + } + } + + return nullptr; +} + +//------------------------------------------------ +// ThreadPoolMan methods +//------------------------------------------------ +ThreadPoolMan::ThreadPoolMan(int count) : is_exit(false), thpoolman_sem(0), is_lock_init(false) +{ + if(count < 1){ + S3FS_PRN_CRIT("Failed to creating singleton for Thread Manager, because thread count(%d) is under 1.", count); + abort(); + } + if(ThreadPoolMan::singleton){ + S3FS_PRN_CRIT("Already singleton for Thread Manager is existed."); + abort(); + } + + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); +#if S3FS_PTHREAD_ERRORCHECK + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK); +#endif + + int result; + if(0 != (result = pthread_mutex_init(&thread_list_lock, &attr))){ + S3FS_PRN_CRIT("failed to init thread_list_lock: %d", result); + abort(); + } + is_lock_init = true; + + // create threads + if(!StartThreads(count)){ + S3FS_PRN_ERR("Failed starting threads at initializing."); + abort(); + } +} + +ThreadPoolMan::~ThreadPoolMan() +{ + StopThreads(); + + if(is_lock_init){ + int result; + if(0 != (result = pthread_mutex_destroy(&thread_list_lock))){ + S3FS_PRN_CRIT("failed to destroy thread_list_lock: %d", result); + abort(); + } + is_lock_init = false; + } +} + +bool ThreadPoolMan::IsExit() const +{ + return is_exit; +} + +void ThreadPoolMan::SetExitFlag(bool exit_flag) +{ + is_exit = exit_flag; +} + +bool ThreadPoolMan::StopThreads() +{ + if(thread_list.empty()){ + S3FS_PRN_INFO("Any threads are running now, then nothing to do."); + return true; + } + + // all threads to exit + SetExitFlag(true); + for(size_t waitcnt = thread_list.size(); 0 < waitcnt; --waitcnt){ + thpoolman_sem.post(); + } + + // wait for threads exiting + for(thread_list_t::const_iterator iter = thread_list.begin(); iter != thread_list.end(); ++iter){ + void* retval = nullptr; + int result = pthread_join(*iter, &retval); + if(result){ + S3FS_PRN_ERR("failed pthread_join - result(%d)", result); + }else{ + S3FS_PRN_DBG("succeed pthread_join - return code(%ld)", reinterpret_cast(retval)); + } + } + thread_list.clear(); + + // reset semaphore(to zero) + while(thpoolman_sem.try_wait()){ + } + + return true; +} + +bool ThreadPoolMan::StartThreads(int count) +{ + if(count < 1){ + S3FS_PRN_ERR("Failed to creating threads, because thread count(%d) is under 1.", count); + return false; + } + + // stop all thread if they are running. + // cppcheck-suppress unmatchedSuppression + // cppcheck-suppress knownConditionTrueFalse + if(!StopThreads()){ + S3FS_PRN_ERR("Failed to stop existed threads."); + return false; + } + + // create all threads + SetExitFlag(false); + for(int cnt = 0; cnt < count; ++cnt){ + // run thread + pthread_t thread; + int result; + if(0 != (result = pthread_create(&thread, nullptr, ThreadPoolMan::Worker, static_cast(this)))){ + S3FS_PRN_ERR("failed pthread_create with return code(%d)", result); + StopThreads(); // if possible, stop all threads + return false; + } + thread_list.push_back(thread); + } + return true; +} + +bool ThreadPoolMan::SetInstruction(std::unique_ptr pparam) +{ + if(!pparam){ + S3FS_PRN_ERR("The parameter value is nullptr."); + return false; + } + + // set parameter to list + { + AutoLock auto_lock(&thread_list_lock); + instruction_list.push_back(std::move(pparam)); + } + + // run thread + thpoolman_sem.post(); + + return true; +} + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/threadpoolman.h b/s3fs/threadpoolman.h new file mode 100644 index 0000000..675f374 --- /dev/null +++ b/s3fs/threadpoolman.h @@ -0,0 +1,109 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef S3FS_THREADPOOLMAN_H_ +#define S3FS_THREADPOOLMAN_H_ + +#include +#include +#include +#include + +#include "psemaphore.h" + +//------------------------------------------------ +// Typedefs for functions and structures +//------------------------------------------------ +// +// Prototype function +// +typedef void* (*thpoolman_worker)(void*); // same as start_routine for pthread_create function + +// +// Parameter structure +// +// [NOTE] +// The args member is a value that is an argument of the worker function. +// The psem member is allowed nullptr. If it is not nullptr, the post() method is +// called when finishing the function. +// +struct thpoolman_param +{ + void* args; + Semaphore* psem; + thpoolman_worker pfunc; + + thpoolman_param() : args(nullptr), psem(nullptr), pfunc(nullptr) {} +}; + +typedef std::list> thpoolman_params_t; + +typedef std::vector thread_list_t; + +//------------------------------------------------ +// Class ThreadPoolMan +//------------------------------------------------ +class ThreadPoolMan +{ + private: + static ThreadPoolMan* singleton; + + std::atomic is_exit; + Semaphore thpoolman_sem; + + bool is_lock_init; + pthread_mutex_t thread_list_lock; + thread_list_t thread_list; + + thpoolman_params_t instruction_list; + + private: + static void* Worker(void* arg); + + explicit ThreadPoolMan(int count = 1); + ~ThreadPoolMan(); + ThreadPoolMan(const ThreadPoolMan&) = delete; + ThreadPoolMan(ThreadPoolMan&&) = delete; + ThreadPoolMan& operator=(const ThreadPoolMan&) = delete; + ThreadPoolMan& operator=(ThreadPoolMan&&) = delete; + + bool IsExit() const; + void SetExitFlag(bool exit_flag); + + bool StopThreads(); + bool StartThreads(int count); + bool SetInstruction(std::unique_ptr pparam); + + public: + static bool Initialize(int count); + static void Destroy(); + static bool Instruct(std::unique_ptr pparam); +}; + +#endif // S3FS_THREADPOOLMAN_H_ + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/s3fs/types.h b/s3fs/types.h new file mode 100644 index 0000000..5d89e37 --- /dev/null +++ b/s3fs/types.h @@ -0,0 +1,365 @@ +/* + * s3fs - FUSE-based file system backed by Amazon S3 + * + * Copyright(C) 2007 Randy Rizun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef S3FS_TYPES_H_ +#define S3FS_TYPES_H_ + +#include +#include +#include +#include +#include +#include + +// +// For extended attribute +// (HAVE_XXX symbols are defined in config.h) +// +#ifdef HAVE_SYS_EXTATTR_H +#include +#elif HAVE_ATTR_XATTR_H +#include +#elif HAVE_SYS_XATTR_H +#include +#endif + +//------------------------------------------------------------------- +// xattrs_t +//------------------------------------------------------------------- +// +// Header "x-amz-meta-xattr" is for extended attributes. +// This header is url encoded string which is json formatted. +// x-amz-meta-xattr:urlencode({"xattr-1":"base64(value-1)","xattr-2":"base64(value-2)","xattr-3":"base64(value-3)"}) +// +typedef std::map xattrs_t; + +//------------------------------------------------------------------- +// acl_t +//------------------------------------------------------------------- +enum class acl_t{ + PRIVATE, + PUBLIC_READ, + PUBLIC_READ_WRITE, + AWS_EXEC_READ, + AUTHENTICATED_READ, + BUCKET_OWNER_READ, + BUCKET_OWNER_FULL_CONTROL, + LOG_DELIVERY_WRITE, + UNKNOWN +}; + +inline const char* str(acl_t value) +{ + switch(value){ + case acl_t::PRIVATE: + return "private"; + case acl_t::PUBLIC_READ: + return "public-read"; + case acl_t::PUBLIC_READ_WRITE: + return "public-read-write"; + case acl_t::AWS_EXEC_READ: + return "aws-exec-read"; + case acl_t::AUTHENTICATED_READ: + return "authenticated-read"; + case acl_t::BUCKET_OWNER_READ: + return "bucket-owner-read"; + case acl_t::BUCKET_OWNER_FULL_CONTROL: + return "bucket-owner-full-control"; + case acl_t::LOG_DELIVERY_WRITE: + return "log-delivery-write"; + case acl_t::UNKNOWN: + return nullptr; + } + abort(); +} + +inline acl_t to_acl(const char *acl) +{ + if(0 == strcmp(acl, "private")){ + return acl_t::PRIVATE; + }else if(0 == strcmp(acl, "public-read")){ + return acl_t::PUBLIC_READ; + }else if(0 == strcmp(acl, "public-read-write")){ + return acl_t::PUBLIC_READ_WRITE; + }else if(0 == strcmp(acl, "aws-exec-read")){ + return acl_t::AWS_EXEC_READ; + }else if(0 == strcmp(acl, "authenticated-read")){ + return acl_t::AUTHENTICATED_READ; + }else if(0 == strcmp(acl, "bucket-owner-read")){ + return acl_t::BUCKET_OWNER_READ; + }else if(0 == strcmp(acl, "bucket-owner-full-control")){ + return acl_t::BUCKET_OWNER_FULL_CONTROL; + }else if(0 == strcmp(acl, "log-delivery-write")){ + return acl_t::LOG_DELIVERY_WRITE; + }else{ + return acl_t::UNKNOWN; + } +} + +//------------------------------------------------------------------- +// sse_type_t +//------------------------------------------------------------------- +enum class sse_type_t{ + SSE_DISABLE = 0, // not use server side encrypting + SSE_S3, // server side encrypting by S3 key + SSE_C, // server side encrypting by custom key + SSE_KMS // server side encrypting by kms id +}; + +enum class signature_type_t { + V2_ONLY, + V4_ONLY, + V2_OR_V4 +}; + +//---------------------------------------------- +// etaglist_t / filepart / untreatedpart +//---------------------------------------------- +// +// Etag string and part number pair +// +struct etagpair +{ + std::string etag; // expected etag value + int part_num; // part number + + explicit etagpair(const char* petag = nullptr, int part = -1) : etag(petag ? petag : ""), part_num(part) {} + + ~etagpair() + { + clear(); + } + + void clear() + { + etag.erase(); + part_num = -1; + } +}; + +// Requires pointer stability and thus must be a list not a vector +typedef std::list etaglist_t; + +struct petagpool +{ + // Requires pointer stability and thus must be a list not a vector + std::list petaglist; + + ~petagpool() + { + clear(); + } + + void clear() + { + petaglist.clear(); + } + + etagpair* add(const etagpair& etag_entity) + { + petaglist.push_back(etag_entity); + return &petaglist.back(); + } +}; + +// +// Each part information for Multipart upload +// +struct filepart +{ + bool uploaded; // does finish uploading + std::string etag; // expected etag value + int fd; // base file(temporary full file) descriptor + off_t startpos; // seek fd point for uploading + off_t size; // uploading size + bool is_copy; // whether is copy multipart + etagpair* petag; // use only parallel upload + char* buf; // user buf. if this not null, it will not write to the file + + explicit filepart(bool is_uploaded = false, int _fd = -1, off_t part_start = 0, off_t part_size = -1, bool is_copy_part = false, etagpair* petagpair = nullptr, char* userBuf = nullptr) : uploaded(false), fd(_fd), startpos(part_start), size(part_size), is_copy(is_copy_part), petag(petagpair), buf(userBuf) {} + + ~filepart() + { + clear(); + } + + void clear() + { + uploaded = false; + etag = ""; + fd = -1; + startpos = 0; + size = -1; + is_copy = false; + petag = nullptr; + buf = nullptr; + } + + void add_etag_list(etaglist_t& list, int partnum = -1) + { + if(-1 == partnum){ + partnum = static_cast(list.size()) + 1; + } + list.push_back(etagpair(nullptr, partnum)); + petag = &list.back(); + } + + void set_etag(etagpair* petagobj) + { + petag = petagobj; + } + + int get_part_number() const + { + if(!petag){ + return -1; + } + return petag->part_num; + } +}; + +typedef std::vector filepart_list_t; + +// +// Each part information for Untreated parts +// +struct untreatedpart +{ + off_t start; // untreated start position + off_t size; // number of untreated bytes + long untreated_tag; // untreated part tag + + explicit untreatedpart(off_t part_start = 0, off_t part_size = 0, long part_untreated_tag = 0) : start(part_start), size(part_size), untreated_tag(part_untreated_tag) + { + if(part_start < 0 || part_size <= 0){ + clear(); // wrong parameter, so clear value. + } + } + + ~untreatedpart() + { + clear(); + } + + void clear() + { + start = 0; + size = 0; + untreated_tag = 0; + } + + // [NOTE] + // Check if the areas overlap + // However, even if the areas do not overlap, this method returns true if areas are adjacent. + // + bool check_overlap(off_t chk_start, off_t chk_size) + { + if(chk_start < 0 || chk_size <= 0 || start < 0 || size <= 0 || (chk_start + chk_size) < start || (start + size) < chk_start){ + return false; + } + return true; + } + + bool stretch(off_t add_start, off_t add_size, long tag) + { + if(!check_overlap(add_start, add_size)){ + return false; + } + off_t new_start = std::min(start, add_start); + off_t new_next_start = std::max((start + size), (add_start + add_size)); + + start = new_start; + size = new_next_start - new_start; + untreated_tag = tag; + + return true; + } +}; + +typedef std::vector untreated_list_t; + +// +// Information on each part of multipart upload +// +struct mp_part +{ + off_t start; + off_t size; + int part_num; // Set only for information to upload + + explicit mp_part(off_t set_start = 0, off_t set_size = 0, int part = 0) : start(set_start), size(set_size), part_num(part) {} +}; + +typedef std::vector mp_part_list_t; + +inline off_t total_mp_part_list(const mp_part_list_t& mplist) +{ + off_t size = 0; + for(mp_part_list_t::const_iterator iter = mplist.begin(); iter != mplist.end(); ++iter){ + size += iter->size; + } + return size; +} + +// +// Rename directory struct +// +struct mvnode +{ + mvnode(std::string old_path, std::string new_path, bool is_dir, bool is_normdir) + : old_path(std::move(old_path)) + , new_path(std::move(new_path)) + , is_dir(is_dir) + , is_normdir(is_normdir) + {} + std::string old_path; + std::string new_path; + bool is_dir; + bool is_normdir; +}; + +//------------------------------------------------------------------- +// mimes_t +//------------------------------------------------------------------- +struct case_insensitive_compare_func +{ + bool operator()(const std::string& a, const std::string& b) const { + return strcasecmp(a.c_str(), b.c_str()) < 0; + } +}; +typedef std::map mimes_t; + +//------------------------------------------------------------------- +// Typedefs specialized for use +//------------------------------------------------------------------- +typedef std::vector readline_t; +typedef std::map kvmap_t; +typedef std::map bucketkvmap_t; + +#endif // S3FS_TYPES_H_ + +/* +* Local variables: +* tab-width: 4 +* c-basic-offset: 4 +* End: +* vim600: expandtab sw=4 ts=4 fdm=marker +* vim<600: expandtab sw=4 ts=4 +*/ diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000..6824e4b --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,25 @@ +SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin) + +add_executable(test_page_cache test_page_cache.cpp) +target_link_libraries(test_page_cache PUBLIC hybridcache_local ${THIRD_PARTY_LIBRARIES}) + +add_executable(test_future test_future.cpp) +target_link_libraries(test_future PUBLIC ${THIRD_PARTY_LIBRARIES}) + +add_executable(test_read_cache test_read_cache.cpp) +target_link_libraries(test_read_cache PUBLIC hybridcache_local ${THIRD_PARTY_LIBRARIES}) + +add_executable(test_write_cache test_write_cache.cpp) +target_link_libraries(test_write_cache PUBLIC hybridcache_local ${THIRD_PARTY_LIBRARIES}) + +add_executable(test_config test_config.cpp) +target_link_libraries(test_config PUBLIC hybridcache_local ${THIRD_PARTY_LIBRARIES}) + +add_executable(test_global_read_cache test_global_read_cache.cpp) +target_link_libraries(test_global_read_cache PUBLIC madfs_global) + +add_executable(test_global_read_cache_perf test_global_read_cache_perf.cpp) +target_link_libraries(test_global_read_cache_perf PUBLIC madfs_global) + +add_executable(test_global_write_cache_perf test_global_write_cache_perf.cpp) +target_link_libraries(test_global_write_cache_perf PUBLIC madfs_global) diff --git a/test/hybridcache.conf b/test/hybridcache.conf new file mode 100644 index 0000000..9e00fa1 --- /dev/null +++ b/test/hybridcache.conf @@ -0,0 +1,39 @@ +# ReadCache +ReadCacheConfig.CacheConfig.CacheName=Read +ReadCacheConfig.CacheConfig.MaxCacheSize=1073741824 +ReadCacheConfig.CacheConfig.PageBodySize=65536 +ReadCacheConfig.CacheConfig.PageMetaSize=1024 +ReadCacheConfig.CacheConfig.EnableCAS=1 +ReadCacheConfig.CacheConfig.CacheLibConfig.EnableNvmCache=0 +ReadCacheConfig.CacheConfig.CacheLibConfig.RaidPath= +ReadCacheConfig.CacheConfig.CacheLibConfig.RaidFileNum= +ReadCacheConfig.CacheConfig.CacheLibConfig.RaidFileSize= +ReadCacheConfig.CacheConfig.CacheLibConfig.DataChecksum= +ReadCacheConfig.DownloadNormalFlowLimit=1048576 +ReadCacheConfig.DownloadBurstFlowLimit=10485760 + +# WriteCache +WriteCacheConfig.CacheConfig.CacheName=Write +WriteCacheConfig.CacheConfig.MaxCacheSize=104857600 +WriteCacheConfig.CacheConfig.PageBodySize=65536 +WriteCacheConfig.CacheConfig.PageMetaSize=1024 +WriteCacheConfig.CacheConfig.EnableCAS=1 +WriteCacheConfig.CacheSafeRatio=70 + +# GlobalCache +UseGlobalCache=1 +GlobalCacheConfig.EnableWriteCache=1 +GlobalCacheConfig.EtcdAddress=http://192.168.1.87:2379 +GlobalCacheConfig.GlobalServers=optane07:8000,optane08:8000 +GlobalCacheConfig.GflagFile= + +ThreadNum=16 +BackFlushCacheRatio=40 +UploadNormalFlowLimit=1048576 +UploadBurstFlowLimit=10485760 +LogPath=. +# LogLevel: GLOG_INFO=0, GLOG_WARNING=1, GLOG_ERROR=2, GLOG_FATAL=3 +LogLevel=1 +EnableLog=0 +FlushToRead=1 +CleanCacheByOpen=0 diff --git a/test/test_config.cpp b/test/test_config.cpp new file mode 100644 index 0000000..cdce38a --- /dev/null +++ b/test/test_config.cpp @@ -0,0 +1,26 @@ +#include + +#include "gtest/gtest.h" + +#include "config.h" + +using namespace std; +using namespace HybridCache; + +TEST(ConfigRead, Read) { + HybridCacheConfig cfg; + EXPECT_EQ(true, GetHybridCacheConfig("../../test/hybridcache.conf", cfg)); + EXPECT_EQ(1073741824, cfg.ReadCacheCfg.CacheCfg.MaxCacheSize); + EXPECT_EQ(16, cfg.ThreadNum); + + EXPECT_EQ(true, cfg.UseGlobalCache); + EXPECT_EQ("http://192.168.1.87:2379", cfg.GlobalCacheCfg.EtcdAddress); + EXPECT_EQ(2, cfg.GlobalCacheCfg.GlobalServers.size()); + EXPECT_EQ("optane08:8000", cfg.GlobalCacheCfg.GlobalServers[1]); +} + +int main(int argc, char **argv) { + printf("Running ConfigRead test from %s\n", __FILE__); + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/test/test_future.cpp b/test/test_future.cpp new file mode 100644 index 0000000..a88b301 --- /dev/null +++ b/test/test_future.cpp @@ -0,0 +1,72 @@ +#include +#include + +#include "folly/futures/Future.h" +#include "gtest/gtest.h" + +#include "common.h" + +using namespace folly; +using namespace std; + +std::shared_ptr executor; + +folly::Future test(int i) { + std::cout << i << " start" << endl; + return folly::via(executor.get(), [i]() -> int { + std::cout << i << " download ..." << endl; + std::this_thread::sleep_for(2000ms); + std::cout << i << " end" << endl; + return 0; + }); +} + +folly::Future testCombine() { + std::cout << "testCombine start" << endl; + + std::vector> fs; + for (int i = 0; i < 3; i++) { + fs.push_back(test(i)); + } + + std::cout << "testCombine mid" << endl; + + auto f = collectAll(fs).via(executor.get()) + .thenValue([](std::vector, std::allocator>>&& tups) { + int res = 0; + for (const auto& t : tups) { + if (t.value() == 0) ++res; + } + std::cout << "testCombine end" << endl; + return res; + }); + + return f; +} + +TEST(FollyFuture, combine) { + auto f = testCombine(); + std::cout << "testCombine running..." << endl; + f.wait(); + std::cout << "testCombine res:" << f.value() << endl; + EXPECT_EQ(3, f.value()); +} + +TEST(FollyFuture, chaining) { + std::cout << "test chaining..." << endl; + auto f = test(1); + auto f2 = move(f).thenValue([](int i){ return i + 100; }); + f2.wait(); + std::cout << "chaining res:" << f2.value() << endl; +} + +int main(int argc, char* argv[]) { + executor = std::make_shared(16); + + printf("Running folly::future test from %s\n", __FILE__); + testing::InitGoogleTest(&argc, argv); + int res = RUN_ALL_TESTS(); + + executor->stop(); + return res; +} diff --git a/test/test_global_read_cache.cpp b/test/test_global_read_cache.cpp new file mode 100644 index 0000000..4bbd186 --- /dev/null +++ b/test/test_global_read_cache.cpp @@ -0,0 +1,176 @@ +#include +#include +#include +#include +#include +#include + +#include "FileSystemDataAdaptor.h" +#include "GlobalDataAdaptor.h" +#include "ReadCacheClient.h" + +DEFINE_string(server, "0.0.0.0:8000", "IP Address of server"); +DEFINE_int32(bench_repeat, 1000, "Repeat count"); +DEFINE_int32(bench_size, 1024 * 16, "Request size in bytes"); +DEFINE_string(filename, "sample.dat", "Test file name"); + +std::string ReadDirectly(const std::string &path, size_t start, size_t length) { + int fd = open(path.c_str(), O_RDONLY); + if (fd < 0) { + PLOG(ERROR) << "Fail to open file: " << path; + return ""; + } + + if (lseek(fd, start, SEEK_SET) < 0) { + PLOG(ERROR) << "Fail to seek file: " << path << " at pos " << start; + close(fd); + return ""; + } + + std::string output; + output.resize(length); + ssize_t nbytes = read(fd, &output[0], length); + if (nbytes != length) { + PLOG(ERROR) << "Fail to read file: " << path + << ", expected read " << length + << ", actual read " << nbytes; + close(fd); + return ""; + } + close(fd); + return output; +} + +ssize_t GetSize(const std::string &path) { + struct stat st; + if (stat(path.c_str(), &st)) { + PLOG(ERROR) << "Fail to state file: " << path; + return -1; + } + return st.st_size; +} + +std::vector SplitString(const std::string &input) { + std::vector result; + std::stringstream ss(input); + std::string item; + while (std::getline(ss, item, ',')) { + result.push_back(item); + } + return result; +} + +TEST(read_cache, generate_get_chunk_request) +{ + const size_t chunk_size = GetGlobalConfig().default_policy.read_chunk_size; + ByteBuffer mock_buffer((char *) 0, 10 * chunk_size); + auto get_chunk_request = ReadCacheClient::GenerateGetChunkRequestsV2; + + // 0 ... CS+16========2CS + { + std::vector requests; + get_chunk_request("foo", chunk_size + 16, chunk_size - 16, mock_buffer, requests, chunk_size); + ASSERT_EQ(requests.size(), 1); + ASSERT_EQ(requests[0].chunk_id, 1); + ASSERT_EQ(requests[0].chunk_start, 16); + ASSERT_EQ(requests[0].chunk_len, chunk_size - 16); + ASSERT_EQ(requests[0].buffer.data, (char *) 0); + ASSERT_EQ(requests[0].buffer.len, chunk_size - 16); + ASSERT_EQ(requests[0].user_key, "foo"); + ASSERT_EQ(requests[0].internal_key, "foo-1-" + std::to_string(chunk_size)); + } + + // 0 ... CS+16========2CS===2CS+16 + { + std::vector requests; + get_chunk_request("foo", chunk_size + 16, chunk_size, mock_buffer, requests, chunk_size); + ASSERT_EQ(requests.size(), 2); + ASSERT_EQ(requests[0].chunk_id, 1); + ASSERT_EQ(requests[0].chunk_start, 16); + ASSERT_EQ(requests[0].chunk_len, chunk_size - 16); + ASSERT_EQ(requests[0].buffer.data, (char *) 0); + ASSERT_EQ(requests[0].buffer.len, chunk_size - 16); + ASSERT_EQ(requests[0].user_key, "foo"); + ASSERT_EQ(requests[0].internal_key, "foo-1-" + std::to_string(chunk_size)); + ASSERT_EQ(requests[1].chunk_id, 2); + ASSERT_EQ(requests[1].chunk_start, 0); + ASSERT_EQ(requests[1].chunk_len, 16); + ASSERT_EQ(requests[1].buffer.data, (char *) chunk_size - 16); + ASSERT_EQ(requests[1].buffer.len, 16); + ASSERT_EQ(requests[1].user_key, "foo"); + ASSERT_EQ(requests[1].internal_key, "foo-2-" + std::to_string(chunk_size)); + } + + // empty request + { + std::vector requests; + get_chunk_request("foo", chunk_size + 16, 0, mock_buffer, requests, chunk_size); + ASSERT_EQ(requests.size(), 0); + } +} + +TEST(read_cache, get_chunk) +{ + auto etcd_client = std::make_shared("http://127.0.0.1:2379"); + auto base = std::make_shared(); + auto global = std::make_shared(base, SplitString(FLAGS_server), etcd_client); + + const size_t chunk_size = GetGlobalConfig().default_policy.read_chunk_size; + ByteBuffer buffer(new char[size_t(FLAGS_bench_size)], size_t(FLAGS_bench_size)); + size_t file_size = GetSize("sample.dat"); + + for (int i = 0; i < FLAGS_bench_repeat; ++i) { + size_t start_pos = lrand48() % file_size; + size_t length = std::min(size_t(FLAGS_bench_size), file_size - start_pos); + if (length) length = lrand48() % length; + ASSERT_EQ(0, global->DownLoad("sample.dat", start_pos, length, buffer).get()); + buffer.data[length] = '\0'; + std::string buffer_cpp(buffer.data, length); + ASSERT_EQ(buffer_cpp, ReadDirectly("sample.dat", start_pos, length)); + } + + ASSERT_EQ(OK, global->DownLoad("sample.dat", file_size - 2, 0, buffer).get()); + ASSERT_EQ(OK, global->DownLoad("sample.dat", file_size - 2, 2, buffer).get()); + ASSERT_EQ(END_OF_FILE, global->DownLoad("sample.dat", file_size - 2, 5, buffer).get()); +} + +TEST(read_cache, mix_read_write) +{ + auto etcd_client = std::make_shared("http://127.0.0.1:2379"); + auto base = std::make_shared(); + auto global = std::make_shared(base, SplitString(FLAGS_server), etcd_client); + + const size_t chunk_size = GetGlobalConfig().default_policy.read_chunk_size; + ByteBuffer buffer(new char[10 * chunk_size], 10 * chunk_size); + std::map headers; + for (size_t i = 0; i < buffer.len; ++i) { + buffer.data[i] = lrand48() % 26 + 'a'; + } + std::string buffer_backup(buffer.data, buffer.len); + ASSERT_EQ(0, global->UpLoad("hello", buffer.len, buffer, headers).get()); + memset(buffer.data, 0, buffer.len); + ASSERT_EQ(0, global->DownLoad("hello", 0, buffer.len, buffer).get()); + ASSERT_EQ(std::string(buffer.data, buffer.len).substr(32), buffer_backup.substr(32)); + + strcpy(buffer.data, "Hello Madfs-----"); + ASSERT_EQ(0, global->UpLoad("hello", 17, buffer, headers).get()); + memset(buffer.data, 0, buffer.len); + ASSERT_EQ(0, global->DownLoad("hello", 6, 5, buffer).get()); + ASSERT_EQ(buffer.data, std::string("Madfs")); + + size_t fsize; + ASSERT_EQ(0, global->Head("hello", fsize, headers).get()); + ASSERT_EQ(fsize, 17); + + ASSERT_EQ(0, global->DeepFlush("hello").get()); + + ASSERT_EQ(0, global->Delete("hello").get()); + ASSERT_EQ(NOT_FOUND, global->Head("hello", fsize, headers).get()); +} + +int main(int argc, char **argv) +{ + gflags::ParseCommandLineFlags(&argc, &argv, true); + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/test/test_global_read_cache_perf.cpp b/test/test_global_read_cache_perf.cpp new file mode 100644 index 0000000..7f3f2f6 --- /dev/null +++ b/test/test_global_read_cache_perf.cpp @@ -0,0 +1,86 @@ +#include +#include +#include +#include +#include + +#include "S3DataAdaptor.h" +#include "FileSystemDataAdaptor.h" +#include "GlobalDataAdaptor.h" +#include "ReadCacheClient.h" + +DEFINE_string(server, "0.0.0.0:8000", "IP Address of server"); +DEFINE_int32(threads, 1, "Thread count in perf test"); +DEFINE_int32(duration, 5, "Test duration in seconds"); +DEFINE_int32(depth, 1, "IO depth"); +DEFINE_bool(use_s3, false, "Use S3 storage"); +DEFINE_string(filename, "sample.dat", "Test file name"); + +std::vector SplitString(const std::string &input) { + std::vector result; + std::stringstream ss(input); + std::string item; + while (std::getline(ss, item, ',')) { + result.push_back(item); + } + return result; +} + +TEST(global_cache_client, perf) +{ + auto etcd_client = std::make_shared("http://127.0.0.1:2379"); + + std::shared_ptr base_adaptor = std::make_shared(); + if (FLAGS_use_s3) { + base_adaptor = std::make_shared(); + } else { + base_adaptor = std::make_shared(); + } + auto global_adaptor = std::make_shared(base_adaptor, SplitString(FLAGS_server), etcd_client); + const size_t chunk_size = GetGlobalConfig().default_policy.read_chunk_size; + + struct stat st_buf; + if (stat(FLAGS_filename.c_str(), &st_buf)) { + PLOG(ERROR) << "Failed to stat file"; + exit(EXIT_FAILURE); + } + auto chunk_count = std::min(1024, (int) (st_buf.st_size / chunk_size)); + + std::vector workers; + std::atomic running(true); + std::atomic operations_total(0); + for (int i = 0; i < FLAGS_threads; ++i) { + workers.emplace_back([&] { + ByteBuffer buffer[FLAGS_depth]; + for (int j = 0; j < FLAGS_depth; ++j) { + buffer[j].data = new char[chunk_size]; + buffer[j].len = chunk_size; + } + uint64_t operations = 0; + std::vector > future_list; + while(running) { + future_list.clear(); + for (int j = 0; j < FLAGS_depth; ++j) { + future_list.emplace_back(global_adaptor->DownLoad(FLAGS_filename.c_str(), chunk_size * (lrand48() % chunk_count), chunk_size, buffer[j])); + } + folly::collectAll(future_list).wait(); + operations += FLAGS_depth; + } + operations_total.fetch_add(operations); + }); + } + sleep(FLAGS_duration); + running = false; + for (int i = 0; i < FLAGS_threads; ++i) { + workers[i].join(); + } + LOG(INFO) << "operation per second: " << operations_total.load() / double(FLAGS_duration) + << "data transfered (MB/s): " << chunk_size * operations_total.load() / double(FLAGS_duration) / 1024.0 / 1024.0; +} + +int main(int argc, char **argv) +{ + gflags::ParseCommandLineFlags(&argc, &argv, true); + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/test/test_global_write_cache_perf.cpp b/test/test_global_write_cache_perf.cpp new file mode 100644 index 0000000..267cfa6 --- /dev/null +++ b/test/test_global_write_cache_perf.cpp @@ -0,0 +1,86 @@ +#include +#include +#include +#include +#include + +#include "S3DataAdaptor.h" +#include "FileSystemDataAdaptor.h" +#include "GlobalDataAdaptor.h" +#include "ReadCacheClient.h" + +DEFINE_string(server, "0.0.0.0:8000", "IP Address of server"); +DEFINE_string(local_dir, "", "Local S3 dir"); +DEFINE_int32(threads, 1, "Thread count in perf test"); +DEFINE_int32(duration, 5, "Test duration in seconds"); +DEFINE_int64(size, 16, "File size in MB"); +DEFINE_int32(depth, 1, "IO depth"); +DEFINE_bool(use_s3, false, "Use S3 storage"); + +std::vector SplitString(const std::string &input) { + std::vector result; + std::stringstream ss(input); + std::string item; + while (std::getline(ss, item, ',')) { + result.push_back(item); + } + return result; +} + +TEST(global_cache_client, perf) +{ + auto etcd_client = std::make_shared("http://192.168.3.87:2379"); + + std::shared_ptr base_adaptor = std::make_shared(); + if (FLAGS_use_s3) { + base_adaptor = std::make_shared(); + } else { + base_adaptor = std::make_shared(FLAGS_local_dir); + } + auto global_adaptor = std::make_shared(base_adaptor, SplitString(FLAGS_server), etcd_client); + const size_t chunk_size = FLAGS_size * 1024 * 1024; + std::vector workers; + std::atomic running(true); + std::atomic operations_total(0); + butil::Timer t; + t.start(); + for (int i = 0; i < FLAGS_threads; ++i) { + workers.emplace_back([&] { + ByteBuffer buffer[FLAGS_depth]; + for (int j = 0; j < FLAGS_depth; ++j) { + int ret = posix_memalign((void **) &buffer[j].data, 4096, chunk_size); + // memset(buffer[j].data, 'x', chunk_size); + ASSERT(!ret); + buffer[j].len = chunk_size; + } + uint64_t operations = 0; + std::vector > future_list; + std::map header; + while(running) { + future_list.clear(); + for (int j = 0; j < FLAGS_depth; ++j) { + future_list.emplace_back(global_adaptor->UpLoad("foo/write-dummy-" + std::to_string(j), chunk_size, buffer[j], header)); + } + folly::collectAll(future_list).wait(); + operations += FLAGS_depth; + } + operations_total.fetch_add(operations); + }); + } + sleep(FLAGS_duration); + running = false; + for (int i = 0; i < FLAGS_threads; ++i) { + workers[i].join(); + } + t.stop(); + + LOG(INFO) << "operation per second: " << operations_total.load() / double(t.s_elapsed()) + << "data transfered (MB/s): " << chunk_size * operations_total.load() / double(t.s_elapsed()) / 1024.0 / 1024.0; +} + +int main(int argc, char **argv) +{ + gflags::ParseCommandLineFlags(&argc, &argv, true); + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/test/test_page_cache.cpp b/test/test_page_cache.cpp new file mode 100644 index 0000000..92fa9f7 --- /dev/null +++ b/test/test_page_cache.cpp @@ -0,0 +1,174 @@ +#include + +#include "gtest/gtest.h" + +#include "errorcode.h" +#include "page_cache.h" + +using namespace folly; + +using namespace std; +using namespace HybridCache; + +CacheConfig cfg; +std::shared_ptr page; + +const std::string key1 = "007"; +const std::string key2 = "009"; + +const size_t TEST_LEN = 64 * 1024; +std::unique_ptr bufIn(new char[TEST_LEN]); +std::unique_ptr bufOut(new char[TEST_LEN]); + +TEST(PageCache, Init) { + srand((unsigned)time(NULL)); + for (int i=0; i(cfg); + EXPECT_EQ(0, page->Init()); +} + +TEST(PageCache, Write) { + EXPECT_EQ(0, page->Write(key1, 0, 4, bufIn.get())); + EXPECT_EQ(0, page->Write(key1, 5, 4, bufIn.get())); + EXPECT_EQ(0, page->Write(key2, 0, TEST_LEN, bufIn.get())); +} + +TEST(PageCache, Read) { + std::vector> dataBoundary; + EXPECT_EQ(0, page->Read(key1, 0, 10, bufOut.get(), dataBoundary)); + + for (int i=0; i<4; ++i) { + EXPECT_EQ(bufIn[i], bufOut[i]); + } + for (int i=0; i<4; ++i) { + EXPECT_EQ(bufIn[i], bufOut[i+5]); + } + + EXPECT_EQ(2, dataBoundary.size()); + auto it = dataBoundary.begin(); + for (int i=0; i<2; ++i) { + if (i==0) { + EXPECT_EQ(0, it->first); + EXPECT_EQ(4, it->second); + } else { + EXPECT_EQ(5, it->first); + EXPECT_EQ(4, it->second); + } + ++it; + } + + dataBoundary.clear(); + EXPECT_EQ(0, page->Read(key1, 5, 4, bufOut.get(), dataBoundary)); + for (int i=0; i<4; ++i) { + EXPECT_EQ(bufIn[i], bufOut[i]); + } + EXPECT_EQ(1, dataBoundary.size()); + EXPECT_EQ(0, dataBoundary.begin()->first); + EXPECT_EQ(4, dataBoundary.begin()->second); + + dataBoundary.clear(); + EXPECT_EQ(0, page->Read(key2, 0, TEST_LEN, bufOut.get(), dataBoundary)); + for (int i=0; ifirst); + EXPECT_EQ(TEST_LEN, dataBoundary.begin()->second); + + dataBoundary.clear(); + EXPECT_EQ(0, page->Read(key2, 1, TEST_LEN-1, bufOut.get(), dataBoundary)); + for (int i=0; ifirst); + EXPECT_EQ(TEST_LEN-1, dataBoundary.begin()->second); +} + +TEST(PageCache, GetAllCache) { + std::vector> dataSegments; + page->GetAllCache(key1, dataSegments); + EXPECT_EQ(2, dataSegments.size()); + EXPECT_EQ(0, dataSegments[0].second); + EXPECT_EQ(5, dataSegments[1].second); + + for (auto& it : dataSegments) { + EXPECT_EQ(4, it.first.len); + for (int i=0; iGetAllCache(key2, dataSegments); + EXPECT_EQ(1, dataSegments.size()); + EXPECT_EQ(0, dataSegments[0].second); + EXPECT_EQ(TEST_LEN, dataSegments[0].first.len); + for (int i=0; iGetCacheSize()); +} + +TEST(PageCache, GetCacheMaxSize) { + EXPECT_EQ(100 * 1024 * 1024, page->GetCacheMaxSize()); +} + +TEST(PageCache, DeletePart) { + EXPECT_EQ(0, page->DeletePart(key1, 0, 4)); + std::vector> dataBoundary; + EXPECT_EQ(0, page->Read(key1, 0, 10, bufOut.get(), dataBoundary)); + for (int i=0; i<4; ++i) { + EXPECT_EQ(bufIn[i], bufOut[i+5]); + } + EXPECT_EQ(1, dataBoundary.size()); + auto it = dataBoundary.begin(); + EXPECT_EQ(5, it->first); + EXPECT_EQ(4, it->second); + + EXPECT_EQ(0, page->DeletePart(key2, 0, 1)); + dataBoundary.clear(); + EXPECT_EQ(0, page->Read(key2, 0, TEST_LEN, bufOut.get(), dataBoundary)); + for (int i=0; ifirst); + EXPECT_EQ(TEST_LEN-1, it->second); +} + +TEST(PageCache, Delete) { + EXPECT_EQ(0, page->Delete(key1)); + std::vector> dataBoundary; + EXPECT_EQ(ErrCode::PAGE_NOT_FOUND, + page->Read(key1, 0, 10, bufOut.get(), dataBoundary)); + + EXPECT_EQ(0, page->Delete(key2)); + dataBoundary.clear(); + EXPECT_EQ(ErrCode::PAGE_NOT_FOUND, + page->Read(key2, 0, TEST_LEN, bufOut.get(), dataBoundary)); +} + +int main(int argc, char **argv) { + printf("Running PageCache test from %s\n", __FILE__); + testing::InitGoogleTest(&argc, argv); + int res = RUN_ALL_TESTS(); + page->Close(); + page.reset(); + return res; +} diff --git a/test/test_read_cache.cpp b/test/test_read_cache.cpp new file mode 100644 index 0000000..ece0b27 --- /dev/null +++ b/test/test_read_cache.cpp @@ -0,0 +1,134 @@ +#include + +#include "gtest/gtest.h" + +#include "read_cache.h" + +using namespace folly; + +using namespace std; +using namespace HybridCache; + +ReadCacheConfig cfg; +std::shared_ptr executor; +std::shared_ptr readCache; + +const std::string file1 = "testfile1"; +const std::string file2 = "testfile2"; +const std::string file3 = "testfile3"; + +const size_t TEST_LEN = 100 * 1024; +std::unique_ptr bufIn(new char[TEST_LEN]); +std::unique_ptr bufOut(new char[TEST_LEN]); + +TEST(ReadCache, Init) { + srand((unsigned)time(NULL)); + for (int i=0; i(16); + auto dataAdaptor = std::make_shared(); + dataAdaptor->SetExecutor(executor); + readCache = std::make_shared(cfg, dataAdaptor, executor); +} + +TEST(ReadCache, Put) { + ByteBuffer stepBuffer(bufIn.get(), TEST_LEN); + EXPECT_EQ(0, readCache->Put(file1, 0, 4, stepBuffer)); + EXPECT_EQ(0, readCache->Put(file2, 5, 4, stepBuffer)); + EXPECT_EQ(0, readCache->Put(file3, 0, TEST_LEN, stepBuffer)); +} + +TEST(ReadCache, Get_From_Local) { + ByteBuffer stepBuffer(bufOut.get(), TEST_LEN); + + auto f = readCache->Get(file1, 0, 4, stepBuffer); + f.wait(); + EXPECT_EQ(0, f.value()); + for (int i=0; i<4; ++i) { + EXPECT_EQ(bufIn[i], bufOut[i]); + } + + stepBuffer.data = (bufOut.get() + 5); + f = readCache->Get(file2, 5, 4, stepBuffer); + f.wait(); + EXPECT_EQ(0, f.value()); + for (int i=0; i<4; ++i) { + EXPECT_EQ(bufIn[i], bufOut[i+5]); + } + + stepBuffer.data = (bufOut.get() + 3); + f = readCache->Get(file2, 6, 2, stepBuffer); + f.wait(); + EXPECT_EQ(0, f.value()); + for (int i=0; i<2; ++i) { + EXPECT_EQ(bufIn[i+1], bufOut[i+3]); + } + + stepBuffer.data = bufOut.get(); + f = readCache->Get(file3, 0, TEST_LEN, stepBuffer); + f.wait(); + EXPECT_EQ(0, f.value()); + for (int i=0; iGet(file2, 0, 10, stepBuffer); + cout << "wait download from s3 ..." << endl; + f.wait(); + EXPECT_EQ(REMOTE_FILE_NOT_FOUND, f.value()); + for (int i=0; i<4; ++i) { + EXPECT_EQ(bufIn[i], bufOut[i+5]); + } + + f = readCache->Get(file3, 1, TEST_LEN+1, stepBuffer); + cout << "wait download from s3 ..." << endl; + f.wait(); + EXPECT_EQ(REMOTE_FILE_NOT_FOUND, f.value()); + for (int i=0; i keys; + readCache->GetAllKeys(keys); + EXPECT_EQ(3, keys.size()); + EXPECT_EQ(1, keys.count(file1)); + EXPECT_EQ(1, keys.count(file2)); + EXPECT_EQ(1, keys.count(file3)); +} + +TEST(ReadCache, Delete) { + std::set keys; + readCache->Delete(file1); + readCache->GetAllKeys(keys); + EXPECT_EQ(2, keys.size()); + EXPECT_EQ(0, keys.count(file1)); + EXPECT_EQ(1, keys.count(file2)); + EXPECT_EQ(1, keys.count(file3)); +} + +int main(int argc, char **argv) { + printf("Running ReadCache test from %s\n", __FILE__); + testing::InitGoogleTest(&argc, argv); + int res = RUN_ALL_TESTS(); + executor->stop(); + readCache.reset(); + return res; +} diff --git a/test/test_write_cache.cpp b/test/test_write_cache.cpp new file mode 100644 index 0000000..1b03c59 --- /dev/null +++ b/test/test_write_cache.cpp @@ -0,0 +1,199 @@ +#include + +#include "gtest/gtest.h" + +#include "write_cache.h" + +using namespace folly; + +using namespace std; +using namespace HybridCache; + +WriteCacheConfig cfg; +std::shared_ptr writeCache; + +const std::string file1 = "testfile1"; +const std::string file2 = "testfile2"; +const std::string file3 = "testfile3"; + +const size_t TEST_LEN = 100 * 1024; +std::unique_ptr bufIn(new char[TEST_LEN]); +std::unique_ptr bufOut(new char[TEST_LEN]); + +TEST(WriteCache, Init) { + srand((unsigned)time(NULL)); + for (int i=0; i(cfg); +} + +TEST(WriteCache, Put) { + ByteBuffer stepBuffer(bufIn.get(), TEST_LEN); + EXPECT_EQ(0, writeCache->Put(file1, 0, 4, stepBuffer)); + EXPECT_EQ(0, writeCache->Put(file2, 5, 4, stepBuffer)); + EXPECT_EQ(0, writeCache->Put(file3, 0, TEST_LEN, stepBuffer)); +} + +TEST(WriteCache, Get) { + ByteBuffer stepBuffer(bufOut.get(), TEST_LEN); + std::vector> dataBoundary; + EXPECT_EQ(0, writeCache->Get(file1, 0, 10, stepBuffer, dataBoundary)); + for (int i=0; i<4; ++i) { + EXPECT_EQ(bufIn[i], bufOut[i]); + } + EXPECT_EQ(1, dataBoundary.size()); + EXPECT_EQ(0, dataBoundary.begin()->first); + EXPECT_EQ(4, dataBoundary.begin()->second); + + dataBoundary.clear(); + EXPECT_EQ(0, writeCache->Get(file2, 0, 10, stepBuffer, dataBoundary)); + for (int i=0; i<4; ++i) { + EXPECT_EQ(bufIn[i], bufOut[i+5]); + } + EXPECT_EQ(1, dataBoundary.size()); + EXPECT_EQ(5, dataBoundary.begin()->first); + EXPECT_EQ(4, dataBoundary.begin()->second); + + dataBoundary.clear(); + EXPECT_EQ(0, writeCache->Get(file3, 0, TEST_LEN, stepBuffer, dataBoundary)); + for (int i=0; ifirst); + EXPECT_EQ(TEST_LEN, it->second); +} + +TEST(WriteCache, GetAllCacheWithLock) { + std::vector> dataSegments; + EXPECT_EQ(0, writeCache->GetAllCacheWithLock(file1, dataSegments)); + EXPECT_EQ(1, dataSegments.size()); + EXPECT_EQ(0, dataSegments.begin()->second); + EXPECT_EQ(4, dataSegments.begin()->first.len); + for (int i=0; ifirst.len; ++i) { + EXPECT_EQ(bufIn[i], *(dataSegments.begin()->first.data+i)); + } + + dataSegments.clear(); + EXPECT_EQ(0, writeCache->GetAllCacheWithLock(file2, dataSegments)); + EXPECT_EQ(1, dataSegments.size()); + EXPECT_EQ(5, dataSegments.begin()->second); + EXPECT_EQ(4, dataSegments.begin()->first.len); + for (int i=0; ifirst.len; ++i) { + EXPECT_EQ(bufIn[i], *(dataSegments.begin()->first.data+i)); + } + + dataSegments.clear(); + EXPECT_EQ(0, writeCache->GetAllCacheWithLock(file3, dataSegments)); + EXPECT_EQ(2, dataSegments.size()); + auto it = dataSegments.begin(); + EXPECT_EQ(0, it->second); + EXPECT_EQ(64*1024, it->first.len); + for (int i=0; ifirst.len; ++i) { + EXPECT_EQ(bufIn[i], *(it->first.data+i)); + } + ++it; + EXPECT_EQ(64*1024, it->second); + EXPECT_EQ(TEST_LEN-64*1024, it->first.len); + for (int i=0; ifirst.len; ++i) { + EXPECT_EQ(bufIn[i+64*1024], *(it->first.data+i)); + } +} + +TEST(WriteCache, GetAllKeys) { + std::map keys; + EXPECT_EQ(0, writeCache->GetAllKeys(keys)); + EXPECT_EQ(3, keys.size()); + EXPECT_EQ(1, keys.count(file1)); + EXPECT_EQ(1, keys.count(file2)); + EXPECT_EQ(1, keys.count(file3)); + for (auto it : keys) { + cout << "key:" << it.first << " create_time:" << it.second << endl; + } +} + +TEST(WriteCache, GetSize) { + uint32_t realPageSize = cfg.CacheCfg.PageMetaSize + + cfg.CacheCfg.PageBodySize/8 + cfg.CacheCfg.PageBodySize; + cout << "CacheSize:" << writeCache->GetCacheSize() << endl; + cout << "CacheMaxSize:" << writeCache->GetCacheMaxSize() << endl; + EXPECT_EQ(realPageSize*4, writeCache->GetCacheSize()); + EXPECT_EQ(cfg.CacheCfg.MaxCacheSize, writeCache->GetCacheMaxSize()); +} + +TEST(WriteCache, UnLock) { + writeCache->UnLock(file1); + writeCache->UnLock(file2); + writeCache->UnLock(file3); +} + +TEST(WriteCache, Truncate) { + uint32_t pageSize = cfg.CacheCfg.PageBodySize; + EXPECT_EQ(0, writeCache->Truncate(file3, pageSize+1)); + + ByteBuffer stepBuffer(bufOut.get(), TEST_LEN); + std::vector> dataBoundary; + EXPECT_EQ(0, writeCache->Get(file3, 0, TEST_LEN, stepBuffer, dataBoundary)); + for (int i=0; ifirst); + EXPECT_EQ(pageSize+1, it->second); + + EXPECT_EQ(0, writeCache->Truncate(file3, pageSize)); + dataBoundary.clear(); + EXPECT_EQ(0, writeCache->Get(file3, 0, TEST_LEN, stepBuffer, dataBoundary)); + for (int i=0; ifirst); + EXPECT_EQ(pageSize, it->second); + + EXPECT_EQ(0, writeCache->Truncate(file3, pageSize-1)); + dataBoundary.clear(); + EXPECT_EQ(0, writeCache->Get(file3, 0, TEST_LEN, stepBuffer, dataBoundary)); + for (int i=0; ifirst); + EXPECT_EQ(pageSize-1, it->second); +} + +TEST(WriteCache, Delete) { + EXPECT_EQ(0, writeCache->Delete(file1)); + std::map keys; + EXPECT_EQ(0, writeCache->GetAllKeys(keys)); + EXPECT_EQ(2, keys.size()); + EXPECT_EQ(0, keys.count(file1)); + EXPECT_EQ(1, keys.count(file2)); + EXPECT_EQ(1, keys.count(file3)); + + ByteBuffer stepBuffer(bufOut.get(), TEST_LEN); + std::vector> dataBoundary; + EXPECT_EQ(0, writeCache->Get(file1, 0, 10, stepBuffer, dataBoundary)); + EXPECT_EQ(0, dataBoundary.size()); +} + +int main(int argc, char **argv) { + printf("Running WriteCache test from %s\n", __FILE__); + testing::InitGoogleTest(&argc, argv); + int res = RUN_ALL_TESTS(); + writeCache.reset(); + return res; +}