pax_global_header00006660000000000000000000000064151654315630014523gustar00rootroot0000000000000052 comment=f40c31c81d90289ef0156fd67b0ff6f38c148a2d MEGAcmd-2.5.2_Linux/000077500000000000000000000000001516543156300141055ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/.dockerignore000066400000000000000000000020301516543156300165540ustar00rootroot00000000000000# Compiled Object files *.slo *.lo *.o *.obj *.lo *.pyc # Compiled Dynamic libraries *.so *.dylib *.dll # Compiled Static libraries *.lai *.la *.a *.lib # Executables *.exe *.out *.app # Editors and IDEs *~ .project .cproject .settings *.pro.user* .vscode CMakeLists.txt.user # MEGAcmd-specific files mega-cmd mega-exec mega-cmd-server mega-cmd-updater mega-cmd-tests-integration mega-cmd-tests-unit megaclient_statecache* megacmdversion.h # Standard location of CMake builds /build/build-cmake-* # Linux packaging files and artifacts /build/megacmd/PKGBUILD /build/megacmd/megacmd.dsc /build/megacmd/megacmd.spec /build/megacmd/megacmd_*.tar.gz # Windows packaging files and artifacts /build/build-x64-windows-mega /build/build-x86-windows-mega /build/built64 /build/built32 /build/sign32 /build/sign64 mega-cmd-server_version.rc # macOS packaging files and artifacts Info.plist # Git *_BACKUP_* *_BASE_* *_LOCAL_* *_REMOTE_* # Backups *_bk # Docker-specific .git build-with-docker .cache /sdk/contrib/QtCreator /sdk/contrib/sdk_build MEGAcmd-2.5.2_Linux/.github/000077500000000000000000000000001516543156300154455ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/.github/ISSUE_TEMPLATE/000077500000000000000000000000001516543156300176305ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/.github/ISSUE_TEMPLATE/bug_report.yml000066400000000000000000000033431516543156300225260ustar00rootroot00000000000000name: Bug Report description: "Report an issue with MEGAcmd" labels: ["bug"] body: - type: markdown attributes: value: "Thank you for filing a bug report!" - type: dropdown id: version attributes: label: "MEGAcmd version" description: "Which version of MEGAcmd did you encountered this issue? (Can be looked up using the `version` command)" options: - 2.1.1 - 2.1.0 - 2.0.0 - 1.7.0 - 1.6.3 - 1.6.2 - 1.6.1 - 1.6.0 - 1.5.1 - 1.5.0 - 1.4.1 - 1.3.0 - 1.2.0 - 1.1.0 - 1.0.0 validations: required: true - type: dropdown id: platform attributes: label: "Operating System/Platform" description: "What platform are you using MEGAcmd on?" options: - Windows - Linux - MacOS - Synology - QNAP validations: required: true - type: input id: platform-version attributes: label: "Platform version. In the case of Linux, please enter the Linux distribution you are using." placeholder: "ex. Windows 11 Pro" validations: required: true - type: textarea id: steps attributes: label: "Steps" description: "What steps did you take you encounter this issue?" placeholder: | 1. 2. 3. 4. validations: required: true - type: textarea id: expected-behavior attributes: label: "Expected behavior" description: "What did you expect to happen?" - type: textarea id: actual-behavior attributes: label: "Actual behavior" description: "What actually happened?" validations: required: true MEGAcmd-2.5.2_Linux/.gitignore000066400000000000000000000022541516543156300161000ustar00rootroot00000000000000# Compiled Object files *.slo *.lo *.o *.obj *.lo *.pyc # Compiled Dynamic libraries *.so *.dylib *.dll # Compiled Static libraries *.lai *.la *.a *.lib # Executables *.exe *.out *.app # Editors and IDEs *~ .project .cproject .settings *.pro.user* .vscode CMakeLists.txt.user # MEGAcmd-specific files mega-cmd mega-exec mega-cmd-server mega-cmd-updater mega-cmd-tests-integration mega-cmd-tests-unit megaclient_statecache* megacmdversion.h megacmd_src_file_list.h # Standard location of CMake builds /build/build-cmake-* # Linux packaging files and artifacts /build/megacmd/PKGBUILD /build/megacmd/megacmd.dsc /build/megacmd/megacmd.spec /build/megacmd/megacmd_*.tar.gz # Windows packaging files and artifacts /build/build-x64-windows-mega /build/build-x86-windows-mega /build/built64 /build/built32 /build/sign32 /build/sign64 /build/installer/mega-cmd-server_version.rc /build/installer/mega-cmd-client_version.rc /build/installer/mega-cmd-shell_version.rc /build/installer/mega-cmd-updater_version.rc # macOS packaging files and artifacts Info.plist # Synology packaging files and artifacts /build/SynologyNAS/packages # Git *_BACKUP_* *_BASE_* *_LOCAL_* *_REMOTE_* # Backups *_bk MEGAcmd-2.5.2_Linux/.gitmodules000066400000000000000000000001071516543156300162600ustar00rootroot00000000000000[submodule "sdk"] path = sdk url = https://github.com/meganz/sdk.git MEGAcmd-2.5.2_Linux/CMakeLists.txt000066400000000000000000000474361516543156300166630ustar00rootroot00000000000000# CMakeLists.txt file to build the MEGAcmd cmake_minimum_required(VERSION 3.16) find_package(Git REQUIRED) execute_process( COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/sdk OUTPUT_VARIABLE SDK_COMMIT_HASH OUTPUT_STRIP_TRAILING_WHITESPACE ) set(MEGACMD_MAJOR_VERSION 2) set(MEGACMD_MINOR_VERSION 5) set(MEGACMD_MICRO_VERSION 2) ## Configure megacmdversion.h configure_file("${CMAKE_CURRENT_LIST_DIR}/src/megacmdversion.h.in" "${CMAKE_CURRENT_LIST_DIR}/src/megacmdversion.h" @ONLY) if (APPLE) configure_file("${CMAKE_CURRENT_LIST_DIR}/build/installer/Info.plist.in" "${CMAKE_CURRENT_LIST_DIR}/build/installer/Info.plist" @ONLY) endif() # Qt Creator configures VCPKG automatically. Disable it, we may want to use different tripplets, paths... set(QT_CREATOR_SKIP_VCPKG_SETUP TRUE CACHE BOOL "") ## Modules location list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}/build/cmake/modules) # Modules from MEGAcmd list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}/sdk/cmake/modules) # Modules from MEGAsdk set(VCPKG_ROOT "${CMAKE_CURRENT_LIST_DIR}/../vcpkg" CACHE PATH "If set, it will build and use the VCPKG packages defined in the manifest file") include(detect_host_architecture) if (WIN32) execute_process( COMMAND bash --version RESULT_VARIABLE BASH_VERSION_RESULT OUTPUT_QUIET ERROR_QUIET ) endif() if((NOT WIN32 OR BASH_VERSION_RESULT EQUAL 0) AND NOT EXISTS ${VCPKG_ROOT}) message(STATUS "vcpkg will be cloned into ${VCPKG_ROOT}") execute_process( #TODO: have the same for windows ... or at least check if bash is available COMMAND "bash" "-x" "${CMAKE_CURRENT_LIST_DIR}/build/clone_vcpkg_from_baseline.sh" "${VCPKG_ROOT}" WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}" ERROR_VARIABLE resulte OUTPUT_VARIABLE result RESULT_VARIABLE status ) if(NOT status EQUAL "0") message(FATAL_ERROR "Failed to run the clone_vcpkg_from_baseline. ${status} ${result} ${resulte} ") endif() message(STATUS "vcpkg cloned successfully: ${status}") endif() #TODO: Review the following, move to a separate module and add add_executable WIN32/MACOSX_BUNDLE properties # Set min OSX version if(CMAKE_HOST_APPLE) # Minimum deployment target differs if we are building for intel or arm64 targets # CMAKE_SYSTEM_PROCESSOR and CMAKE_HOST_SYSTEM_PROCESSOR are only available after project() execute_process( COMMAND uname -m OUTPUT_VARIABLE HOST_ARCHITECTURE OUTPUT_STRIP_TRAILING_WHITESPACE) # Setup CMAKE_OSX_DEPLOYMENT_TARGET before project() if(CMAKE_OSX_ARCHITECTURES STREQUAL "arm64" OR (NOT CMAKE_OSX_ARCHITECTURES AND HOST_ARCHITECTURE STREQUAL "arm64")) set(CMAKE_OSX_DEPLOYMENT_TARGET "11.1" CACHE STRING "Minimum OS X deployment version") else() set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "Minimum OS X deployment version") endif() message(STATUS "Minimum OS X deployment version is set to ${CMAKE_OSX_DEPLOYMENT_TARGET}") unset(HOST_ARCHITECTURE) endif() if(WIN32) add_definitions( -DUNICODE -D_UNICODE ) # needed for visual studio projects to use the unicode runtime libraries #supported windows version: 8 and beyond add_definitions( -D_WIN32_WINNT=0x0602 ) # 0602: windows 8. Note: NTDDI_VERSIO & WINVER shall be inferred from those by Win SDK add_definitions( -DNO_READLINE ) # This one is defined within SdkLib, but we need it too for MEGAcmd code. add_definitions( -DUSE_CPPTHREAD ) # For win32, we need this to be defined explicitly (used directly in different targets). TODO: remove in CMD-318 add_definitions( -DNOMINMAX ) # To fix usages of std::min / std::max endif() ## Configurable options ## include(megacmd_options) #Load first MEGAcmd's options (that we have prevalescence over SDK (e.g libuv) include(sdklib_options) #load default sdk's if (EXISTS ${CMAKE_CURRENT_LIST_DIR}/sdk/include/mega/config.h) file(RENAME ${CMAKE_CURRENT_LIST_DIR}/sdk/include/mega/config.h ${CMAKE_CURRENT_LIST_DIR}/sdk/include/mega/config.h_non_cmake_bk) endif() message(STATUS "Using VCPKG_ROOT = ${VCPKG_ROOT}") if(VCPKG_ROOT) if (ENABLE_MEGACMD_TESTS) list(APPEND VCPKG_MANIFEST_FEATURES "megacmd-enable-tests") endif() # Include VCPKG management tools. include(vcpkg_management) list(APPEND vcpkg_overlay ${CMAKE_CURRENT_LIST_DIR}/sdk/cmake) # MEGAsdk overlays process_vcpkg_libraries("${vcpkg_overlay}") # Choose and build libraries depending on the configured options. else() # For packages with no pkg-config in the system. list(APPEND CMAKE_MODULE_PATH sdk/cmake/modules/packages) message(STATUS "Using system dependencies") endif() project(MEGAcmd VERSION ${MEGACMD_MAJOR_VERSION}.${MEGACMD_MINOR_VERSION}.${MEGACMD_MICRO_VERSION} DESCRIPTION "MEGAcmd" ) # In-source build not allowed if(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR) message(FATAL_ERROR "In-source build is not allowed. Remove CMakeCache.txt and the CMakeFiles directory and set a new binary directory different than the source tree.") endif() message(STATUS "Building MEGAcmd v${PROJECT_VERSION}") #utilities/helper functions include(GNUInstallDirs) # Values for installation directories. All platforms include(CMakePackageConfigHelpers) # For the CMake package include(target_sources_conditional) # To add files to the project without building them include(target_platform_compile_options) # To add compile options depeding on the platform if(UNIX AND NOT APPLE) # Set rpath and location for dirs accordingly: # If CMAKE_INSTALL_PREFIX is set (not default), it will set rpath to to such prefix plus /opt/.... # If CMAKE_INSTALL_PREFIX is not set, it will set rpath to /opt/.... # Note: using cmake --install --prefix /some/prefix will not set rpath relative to that prefix # The above can be used for building packages: in which install dir is a path construction folder that will not be there in packages set(CMAKE_INSTALL_LIBDIR "opt/megacmd/lib") set(CMAKE_INSTALL_BINDIR "usr/bin") #override default "bin" if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) message(STATUS "Overriding default CMAKE_INSTALL_PREFIX to /") set(CMAKE_INSTALL_PREFIX "/" CACHE PATH "Default install path" FORCE) # override default /usr/local set(RPATH_FOR_DYNAMIC_LIBS "/${CMAKE_INSTALL_LIBDIR}") else() # If explicit cmake prefix at cmake call time, make rpath relative to install dir set(RPATH_FOR_DYNAMIC_LIBS "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}") endif() list( APPEND CMAKE_INSTALL_RPATH "${RPATH_FOR_DYNAMIC_LIBS}") endif() if(APPLE) set(CMAKE_MACOSX_RPATH 1) get_filename_component(ABSOLUTE_RPATH_LIBS ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR} ABSOLUTE) list( APPEND CMAKE_INSTALL_RPATH ${ABSOLUTE_RPATH_LIBS} ) #set(CMAKE_INSTALL_RPATH ${ABSOLUTE_RPATH_LIBS}) message(STATUS "Added CMAKE_INSTALL_LIBDIR=${ABSOLUTE_RPATH_LIBS} to rpath: ${CMAKE_INSTALL_RPATH}") endif() # Load SDK project to build sdk library add_subdirectory(sdk) #LOAD MEGACMD modules: include(megacmd_configuration) ## Load global CMake configuration for the project include(megacmd_libraries) # to load libraries dependencies by target include(megacmd_utility_functions) # utility functions #TODO: CONSIDER SPLITTING TARGET IN FILES and have add_subdirectory(src) , ... See MR !682 set(ProjectDir "${CMAKE_CURRENT_LIST_DIR}") # Populate list of MEGAcmd source files (used by logger): generate_src_file_list("${ProjectDir}/sdk" SDK_SRCS) generate_src_file_list("${ProjectDir}/src" MEGACMD_SRCS) foreach(CMDFILE ${MEGACMD_SRCS}) if (CMDFILE IN_LIST SDK_SRCS) message(FATAL_ERROR "MEGAcmd src filename ${CMDFILE} clashing with SDK's: please, pick a new one") endif() endforeach() string(JOIN ", " MEGACMD_SRC_FILE_LIST ${MEGACMD_SRCS}) configure_file("${ProjectDir}/src/megacmd_src_file_list.h.in" "${ProjectDir}/src/megacmd_src_file_list.h" @ONLY) add_library(LMEGAcmdCommonUtils STATIC) add_source_and_corresponding_header_to_target(LMEGAcmdCommonUtils PUBLIC "${ProjectDir}/src/megacmdcommonutils.cpp" "${ProjectDir}/src/megacmd_utf8.cpp" ) add_library(LMegacmdServer STATIC) add_source_and_corresponding_header_to_target(LMegacmdServer PUBLIC "${ProjectDir}/src/megacmd.cpp" "${ProjectDir}/src/megacmdexecuter.cpp" "${ProjectDir}/src/megacmd_events.cpp" "${ProjectDir}/src/megacmdlogger.cpp" "${ProjectDir}/src/megacmdsandbox.cpp" "${ProjectDir}/src/megacmdutils.cpp" "${ProjectDir}/src/comunicationsmanager.cpp" "${ProjectDir}/src/comunicationsmanagerfilesockets.cpp" "${ProjectDir}/src/comunicationsmanagernamedpipes.cpp" "${ProjectDir}/src/configurationmanager.cpp" "${ProjectDir}/src/listeners.cpp" "${ProjectDir}/src/sync_command.cpp" "${ProjectDir}/src/sync_issues.cpp" "${ProjectDir}/src/sync_ignore.cpp" "${ProjectDir}/src/megacmd_rotating_logger.cpp" "${ProjectDir}/src/megacmd_fuse.cpp" ) target_sources_conditional(LMegacmdServer FLAG APPLE PRIVATE "${ProjectDir}/src/megacmdplatform.h" "${ProjectDir}/src/megacmdplatform.mm" ) # Given we are no longer including the sdk, and some sources use certain defines without including SDK's config.h, # We need to explicitly pass the compiling options. target_compile_definitions(LMegacmdServer PUBLIC $<$:USE_PCRE> ) if (NOT WIN32) if (ENABLE_ASAN) add_compile_options("-fsanitize=address" "-fno-omit-frame-pointer" "-fno-common") link_libraries("-fsanitize=address") endif() if (ENABLE_UBSAN) add_compile_options("-fsanitize=undefined" "-fno-omit-frame-pointer") link_libraries("-fsanitize=undefined") endif() if (ENABLE_TSAN) add_compile_options("-fsanitize=thread" "-fno-omit-frame-pointer") link_libraries("-fsanitize=thread") endif() endif() if (APPLE) set(executablesType MACOSX_BUNDLE) endif() add_executable(mega-cmd-server ${executablesType}) if (WIN32) set(MEGACMD_RESOURCE_NAME MEGAcmdServer) configure_file("${CMAKE_CURRENT_LIST_DIR}/build/installer/winversion.rc.in" "${CMAKE_CURRENT_LIST_DIR}/build/installer/mega-cmd-server_version.rc" @ONLY) set(RESOURCE_FILES_MEGACMD_SERVER "${CMAKE_CURRENT_LIST_DIR}/build/installer/mega-cmd-server_version.rc") set(MEGACMD_RESOURCE_NAME MEGAclient) configure_file("${CMAKE_CURRENT_LIST_DIR}/build/installer/winversion.rc.in" "${CMAKE_CURRENT_LIST_DIR}/build/installer/mega-cmd-client_version.rc" @ONLY) set(RESOURCE_FILES_MEGACMD_CLIENT "${CMAKE_CURRENT_LIST_DIR}/build/installer/mega-cmd-client_version.rc") set(MEGACMD_RESOURCE_NAME MEGAcmdShell) configure_file("${CMAKE_CURRENT_LIST_DIR}/build/installer/winversion.rc.in" "${CMAKE_CURRENT_LIST_DIR}/build/installer/mega-cmd-shell_version.rc" @ONLY) set(RESOURCE_FILES_MEGACMD_SHELL "${CMAKE_CURRENT_LIST_DIR}/build/installer/mega-cmd-shell_version.rc") set(MEGACMD_RESOURCE_NAME MEGAcmdUpdater) configure_file("${CMAKE_CURRENT_LIST_DIR}/build/installer/winversion.rc.in" "${CMAKE_CURRENT_LIST_DIR}/build/installer/mega-cmd-updater_version.rc" @ONLY) set(RESOURCE_FILES_MEGACMD_UPDATER "${CMAKE_CURRENT_LIST_DIR}/build/installer/mega-cmd-updater_version.rc") elseif (APPLE) set(RESOURCE_FILES_MEGACMD_SERVER ${CMAKE_CURRENT_SOURCE_DIR}/build/installer/app.icns ) set_target_properties(mega-cmd-server PROPERTIES MACOSX_BUNDLE TRUE MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/build/installer/Info.plist RESOURCE "${RESOURCE_FILES_MEGACMD_SERVER}" ) endif() add_source_and_corresponding_header_to_target(mega-cmd-server PRIVATE "${ProjectDir}/src/megacmd_server_main.cpp" "${RESOURCE_FILES_MEGACMD_SERVER}" ) if (APPLE) set_target_properties(mega-cmd-server PROPERTIES OUTPUT_NAME "MEGAcmd") elseif (WIN32) set_target_properties(mega-cmd-server PROPERTIES OUTPUT_NAME "MEGAcmdServer") endif() add_library(LMegacmdClient STATIC) add_source_and_corresponding_header_to_target(LMegacmdClient PUBLIC "${ProjectDir}/src/client/megacmdclient.cpp" "${ProjectDir}/src/megacmdshell/megacmdshellcommunicationsnamedpipes.cpp" "${ProjectDir}/src/megacmdshell/megacmdshellcommunications.cpp" ) add_executable(mega-exec ${executablesType}) add_source_and_corresponding_header_to_target(mega-exec PRIVATE "${ProjectDir}/src/client/megacmd_client_main.cpp" "${RESOURCE_FILES_MEGACMD_CLIENT}" ) add_executable(mega-cmd ${executablesType}) add_source_and_corresponding_header_to_target(mega-cmd PRIVATE "${ProjectDir}/src/megacmdshell/megacmdshellcommunications.cpp" "${ProjectDir}/src/megacmdshell/megacmdshellcommunicationsnamedpipes.cpp" "${ProjectDir}/src/megacmdshell/megacmdshell.cpp" "${RESOURCE_FILES_MEGACMD_SHELL}" ) if (WIN32) add_executable(mega-cmd-updater WIN32) else() add_executable(mega-cmd-updater ${executablesType}) endif() add_source_and_corresponding_header_to_target(mega-cmd-updater PRIVATE "${ProjectDir}/src/updater/MegaUpdater.cpp" ) add_source_and_corresponding_header_to_target(mega-cmd-updater PRIVATE "${ProjectDir}/src/updater/UpdateTask.cpp" "${RESOURCE_FILES_MEGACMD_UPDATER}" ) target_sources_conditional(mega-cmd-updater FLAG APPLE PRIVATE "${ProjectDir}/src/updater/MacUtils.h" "${ProjectDir}/src/updater/MacUtils.mm" ) if (ENABLE_MEGACMD_TESTS) #Test Common: add_library(LMegacmdTestsCommon STATIC) add_source_and_corresponding_header_to_target(LMegacmdTestsCommon PRIVATE "${ProjectDir}/tests/common/Instruments.cpp" "${ProjectDir}/tests/common/TestUtils.cpp" ) #Integration tests: add_executable(mega-cmd-tests-integration ${executablesType}) add_source_and_corresponding_header_to_target(mega-cmd-tests-integration PRIVATE "${ProjectDir}/tests/integration/BasicTests.cpp" "${ProjectDir}/tests/integration/CatTests.cpp" "${ProjectDir}/tests/integration/ExportTests.cpp" "${ProjectDir}/tests/integration/PutTests.cpp" "${ProjectDir}/tests/integration/SyncIssuesTests.cpp" "${ProjectDir}/tests/integration/SyncIgnoreTests.cpp" "${ProjectDir}/tests/integration/FuseTests.cpp" "${ProjectDir}/tests/integration/MegaCmdTestingTools.cpp" "${ProjectDir}/tests/integration/main.cpp" ) target_include_directories(mega-cmd-tests-integration PUBLIC ${ProjectDir}/src ${ProjectDir}/tests/common) target_link_libraries(mega-cmd-tests-integration PUBLIC LMegacmdServer LMegacmdClient LMegacmdTestsCommon) if(APPLE) target_link_libraries(mega-cmd-tests-integration PRIVATE "-framework Security" ) endif() #Unit tests: add_executable(mega-cmd-tests-unit ${executablesType}) add_source_and_corresponding_header_to_target(mega-cmd-tests-unit PRIVATE "${ProjectDir}/tests/unit/ComunicationsManagerFileSocketsTests.cpp" "${ProjectDir}/tests/unit/OptionsFlagsUtilsTests.cpp" "${ProjectDir}/tests/unit/PlatformDirectoriesTests.cpp" "${ProjectDir}/tests/unit/StringUtilsTests.cpp" "${ProjectDir}/tests/unit/Utf8Tests.cpp" "${ProjectDir}/tests/unit/UtilsTests.cpp" "${ProjectDir}/tests/unit/main.cpp" ) if(APPLE) target_link_libraries(mega-cmd-tests-unit PRIVATE "-framework Security" ) endif() endif() if (WIN32) set_target_properties(mega-exec PROPERTIES OUTPUT_NAME MEGAclient) set_target_properties(mega-cmd PROPERTIES OUTPUT_NAME MEGAcmdShell) set_target_properties(mega-cmd-updater PROPERTIES OUTPUT_NAME MEGAcmdUpdater) set_target_properties(mega-cmd-server PROPERTIES OUTPUT_NAME MEGAcmdServer) set_target_properties(mega-cmd-server PROPERTIES LINK_FLAGS "/LARGEADDRESSAWARE /DEBUG" ) #TODO: if this is still required, these paths will need adjusting #set(3RDPARTY_RUNTIME_PATH_DEBUG "PATH=%PATH%" "${Mega3rdPartyDir}/vcpkg/installed/${VCPKG_TRIPLET}/debug/bin") #set(3RDPARTY_RUNTIME_PATH_RELEASE "PATH=%PATH%" "${Mega3rdPartyDir}/vcpkg/installed/${VCPKG_TRIPLET}/bin") #set_target_properties(mega-exec PROPERTIES VS_DEBUGGER_ENVIRONMENT "${3RDPARTY_RUNTIME_PATH_DEBUG}") #set_target_properties(mega-cmd PROPERTIES VS_DEBUGGER_ENVIRONMENT "${3RDPARTY_RUNTIME_PATH_DEBUG}") #set_target_properties(mega-cmd-updater PROPERTIES VS_DEBUGGER_ENVIRONMENT "${3RDPARTY_RUNTIME_PATH_DEBUG}") #set_target_properties(mega-cmd-server PROPERTIES VS_DEBUGGER_ENVIRONMENT "${3RDPARTY_RUNTIME_PATH_DEBUG}") endif() target_link_libraries(LMegacmdClient PUBLIC MEGA::SDKlib LMEGAcmdCommonUtils) target_link_libraries(mega-exec LMegacmdClient) target_link_libraries(mega-cmd PUBLIC MEGA::SDKlib LMEGAcmdCommonUtils) target_link_libraries(mega-cmd-updater PUBLIC MEGA::SDKlib LMEGAcmdCommonUtils) target_link_libraries(LMegacmdServer PUBLIC MEGA::SDKlib LMEGAcmdCommonUtils) if (ENABLE_MEGACMD_TESTS) target_link_libraries(LMegacmdServer PUBLIC LMegacmdTestsCommon) endif() if (WIN32) target_link_libraries(LMegacmdServer PUBLIC Lz32.lib Taskschd.lib) target_link_libraries(mega-cmd-updater PUBLIC Lz32.lib Urlmon.lib) endif() target_link_libraries(mega-cmd-server PUBLIC LMegacmdServer) if (ENABLE_MEGACMD_TESTS) target_include_directories(LMegacmdTestsCommon PUBLIC ${ProjectDir}/src ${ProjectDir}/tests/common) target_link_libraries(mega-cmd-tests-unit PUBLIC LMegacmdServer LMegacmdTestsCommon) endif() # Load 3rd parties #TODO: consider splitting by target? load_megacmdserver_libraries() #file(GET_RUNTIME_DEPENDENCIES ... _deps) #foreach(_dep IN LISTS _deps) # message( "es una dependencia") ## if("${_dep}" SAME_FILE "/some/file/built/by/cmake.so") # if(SAME_FILE) doesn't currently exist, we'd have to create it ## # Target install rules for this CMake-built target # #else() ## # Standard install rules for external runtime dependencies ## endif() #endforeach() list(APPEND all_targets mega-exec mega-cmd mega-cmd-server) if (APPLE) list(APPEND all_targets mega-cmd-updater) endif() if(ENABLE_MEGACMD_TESTS) list(APPEND all_targets mega-cmd-tests-unit mega-cmd-tests-integration) endif() # Install stuff if (APPLE) install(TARGETS ${all_targets} BUNDLE DESTINATION "./" ) elseif(NOT WIN32) set(PERMISSIONS755 OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE ) set(CMAKE_INSTALL_DEFAULT_DIRECTORY_PERMISSIONS ${PERMISSIONS755}) install(TARGETS ${all_targets} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} ) install(DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/src/client/" DESTINATION ${CMAKE_INSTALL_BINDIR} FILE_PERMISSIONS ${PERMISSIONS755} FILES_MATCHING PATTERN "mega-*" PATTERN "*.cpp" EXCLUDE PATTERN "*.h" EXCLUDE PATTERN "mega-exec" EXCLUDE PATTERN "megacmd_completion.sh" EXCLUDE PATTERN "python" EXCLUDE PATTERN "win" EXCLUDE) install(FILES "${CMAKE_CURRENT_LIST_DIR}/src/client/megacmd_completion.sh" DESTINATION "etc/bash_completion.d" ) # generate 100-megacmd-inotify-limit.conf file and have it installed execute_process(COMMAND echo "fs.inotify.max_user_watches = 524288" OUTPUT_FILE ${CMAKE_CURRENT_BINARY_DIR}/99-megacmd-inotify-limit.conf) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/99-megacmd-inotify-limit.conf DESTINATION "etc/sysctl.d" ) #Install vcpkg dynamic libraries in locations defined by GNUInstallDirs. if(CMAKE_BUILD_TYPE STREQUAL "Debug") SET(vcpkg_lib_folder "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/debug/lib/") else() SET(vcpkg_lib_folder "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/lib/") endif() install(DIRECTORY "${vcpkg_lib_folder}" DESTINATION ${CMAKE_INSTALL_LIBDIR} FILES_MATCHING PATTERN "lib*.so*" PATTERN "*dylib*" #macOS PATTERN "manual-link" EXCLUDE PATTERN "pkgconfig" EXCLUDE) endif() #not WIN32 MEGAcmd-2.5.2_Linux/LICENSE000066400000000000000000000032411516543156300151120ustar00rootroot00000000000000Copyright (c) 2013, Mega Limited All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. 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. 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 HOLDER 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. The previous applies to all sources and binaries with the exception of those explicitly distributed under the terms of the GNU General Public License (see http://www.gnu.org/copyleft/gpl.txt for details), and the interactive MEGAcmdShell executables (mega-cmd in linux, MEGAcmdShell in MAC and MEGAcmdShell.exe in Windows), distributed under the aforementioned GNU General Public License. MEGAcmd-2.5.2_Linux/README.md000066400000000000000000000310501516543156300153630ustar00rootroot00000000000000# MEGAcmd - Command Line Interactive and Scriptable Application MEGAcmd provides non UI access to MEGA services. It intends to offer all the functionality with your MEGA account via commands. It features **synchronization** , **backup** of local folders into your MEGA account and a **webdav/streaming** server. See [`Usage Examples`](#usage-examples). Available packages for MEGAcmd in all supported platforms should be found [here](https://mega.nz/cmd). If the package fails to install, read below for the requirements. **It supports 2 modes of interaction:** - **Interactive** - A shell to query your actions - **Scriptable** - A way to execute commands from a shell/a script/another program In order to provide those 2 modes, it features one server (**MEGAcmdServer**), an interactive shell (**MEGAcmdShell**) and several commands that will launch the non-interactive client (**MEGAcmdClient**). See [`Usage`](#usage) and [`Platform`](#platforms) to understand how to use it in your particular system. # Building MEGAcmd ## Requirements All dependencies are downloaded and installed automatically by vcpkg while building. The local path to the vcpkg repository can be set with the `-DVCPKG_ROOT` flag (defaults to `../vcpkg` if the flag is missing). ## Getting the source Ensure you obtain the repository recursively. ``` git clone https://github.com/meganz/MEGAcmd.git cd MEGAcmd && git submodule update --init --recursive ``` ## Building To build MEGAcmd, we first need to configure the project using CMake: ``` cmake -B build/build-cmake-Debug -DCMAKE_BUILD_TYPE=Debug ``` A different build type can be selected out of `Debug`, `Release`, `MinSizeRel`, or `RelWithDebInfo`. By convention, we use `build/build-cmake-[Debug|Release|...]` as build directory, but any other path can be selected with the `-B` option. To build unit and integration tests as well, we can add the `-DENABLE_MEGACMD_TESTS=ON` flag. If `ccache` is installed in our system, we can use it to speed up subsequent compilations by adding the `-DCMAKE_CXX_COMPILER_LAUNCHER=ccache` flag. After the project has been configured, we can build it with: ``` cmake --build build/build-cmake-Debug ``` To speed up compilation, we can use the `-j` option to specify a number of cores. ## System-wide installation (Linux/macOS) On Unix-based systems, such as Linux or macOS, MEGAcmd can be installed with (after building): ``` sudo cmake --install build/build-cmake-Release ``` This will install `mega-cmd`, `mega-cmd-server`, and all `mega-*` commands such as `mega-put`, `mega-cd`, etc. It is recommended to avoid installing a Debug build, since some paths might not be properly setup. We can specify the default directory to install from by adding the `-DCMAKE_INSTALL_PREFIX` flag while configuring. # Usage Before explaining the two ways of interaction, it's important to understand how MEGAcmd works. When you login with MEGAcmd, your session, the list of synced folders, cache databases, and extra configuration are stored in your local home folder. Closing it does not delete those, and restarting the MEGAcmd server will restore your previous session (similary to the MEGA Desktop App, which won't ask for user/password once restarted). You will need to `logout` properly in order to clean your data. Now let's get into details of the two usage modes. Both modes require that MEGAcmdServer is running. You can manually launch it. Fortunately, you can also open the interactive shell or execute any command and the server will start automatically. ## Interactively: Execute MEGAcmd shell. [`Platform`](#platforms) section explains how to do that in the different supported systems. You should be facing an interactive shell where you can start typing your commands, with their arguments and flags. You can list all the available commands with `help`. And obtain useful information about a command with `command --help` First you would like to log in into your account. Again, notice that doing this stores the session and other stuff in your home folder. A complete logout is required if you want to end you session permanently and clean any traces (see `logout --help` for further info). ## Non-interactively: When MEGAcmd server is running, it will be listening for client commands. Use the different `mega-*` commands available. `mega-help` will list all these commands (you will need to prepend "mega-" to the commands listed there). To obtain further info use `mega-command --help`. Those commands will have an output value != 0 in case of failure. See [megacmd.h](https://github.com/meganz/MEGAcmd/blob/master/src/megacmd.h) to view the existing error codes. Ideally, you would like to have these commands in your `PATH` variable (See [`Platform`](#platforms) for more info). For further info use `mega-help --non-interactive`. ## Usage examples Here are some examples of use (more info and usage examples are available at the [User Guide](UserGuide.md)). **Notice:** the commands listed here assume you are using the interactive interaction mode: they are supposed to be executed within MEGAcmdShell. * A **synchronization** can be established simply by typing: ``` sync /path/to/local/folder /folder/in/mega ``` This will synchronize the contents in your local and your mega folder both ways. * You can also set remote **backups** of a local folder to keep historical snapshots of your files. So simple as: ``` backup /path/mega/folder /remote/path --period="0 0 4 * * *" --num-backups=10 ``` This will configure a backup of "myfolder" into /remote/path that will be carried out at 4:00 A.M. (UTC) every day. It will store the last 10 copies. Further info on backups [here](contrib/docs/BACKUPS.md). * You **serve a location** in your MEGA account via WebDAV: ``` webdav /path/mega/folder ``` * Or **stream a file** in your MEGA account: ``` webdav /path/to/myfile.mp4 ``` Further info on webdav and streaming [here](contrib/docs/WEBDAV.md). * **Download the contents** of a shared link: ``` get https://mega.nz/#F!ABcD1E2F!gHiJ23k-LMno45PqrSTUvw /path/to/local/folder ``` Now let's do something more complicated with non-interactive usage using some GNU tools (similar stuff can be easily done in Windows as well): * We want to provide something crypto secured with only 10 minutes of access: ``` mega-put /path/to/my/temporary_resource /exportedstuff/ mega-export -a /exportedstuff/temporary_resource --expire=10M | awk '{print $4}' ``` * Or imagine we'd like to public the enterprise promotional videos of May 2015 that we have previously stored in MEGA: ``` for i in $(mega-find /enterprise/video/promotional2015/may --pattern="*mpeg"); do mega-export -a $i | awk '{print $4}'; done ``` # Platforms ## Linux If you have installed MEGAcmd using one of the available packages at [here](https://mega.nz/cmd). Or have it built without `--prefix`, both the server (`mega-cmd-server`), the shell (`mega-cmd`) and the different client commands (`mega-*`) will be in your `PATH` (on a fresh install, you might need to open your terminal again). If you are using bash, you should also have autocompletion for client commands working. If that is not you case, include the location for the binaries in your `PATH` variable. ## Windows You can have MEGAcmd installed using the installer from [here](https://mega.nz/cmd). If you are interested in installing MEGAcmd without human intervention, notice that this installer supports silent installation, you just need to execute in your command prompt: ``` MEGAcmdSetup.exe /S ``` Once you have MEGAcmd installed, you just need to execute it (via Desktop icon or Start Menu) to open the shell. This will open MEGAcmdServer in the background (a process named MEGAcmdServer.exe). For a better user experience we recommend executing MEGAcmd from PowerShell. **Open PowerShell and execute:** ``` $env:PATH += ";$env:LOCALAPPDATA\MEGAcmd" MEGAcmdShell ``` For *non-interactive* usage, there are several `mega-*.bat` client commands you can use writting their absolute paths, or including their location into your environment `PATH` and execute them normally (`mega-*`). If you use PowerShell and you have installed the official MEGAcmd, you can do that simply with: ``` $env:PATH += ";$env:LOCALAPPDATA\MEGAcmd" ``` Client commands completion requires bash, hence, it is not available for Windows. ### Caveats Although there have been several efforts in having non-ASCII unicode characters supported in Windows, there still may be some issues. Pay special attention if you are willing to use pipes or send the output of a command into a file from your client commands. See `help --unicode` for further info regarding that. ## macOS Install MEGAcmd from [here](https://mega.nz/cmd). For macOS, after installing the dmg, you can launch the server using MEGAcmd in Applications. If you wish to use the client commands from macOS Terminal, open the Terminal and include the installation folder in the `PATH`. **Typically:** ``` export PATH=/Applications/MEGAcmd.app/Contents/MacOS:$PATH ``` And for **bash completion**, source `megacmd_completion.sh`: ``` source /Applications/MEGAcmd.app/Contents/macOS/megacmd_completion.sh ``` *Note for macOS Catalina or above*: since Catalina, macOS uses `zsh` as default shell. If you want to have auto completion, we strongly recommend you to use `bash` shell (just execute `bash` in your terminal). ## NAS systems Currently we have build scripts for **Synology**, which can be found in the `build/SynologyNAS` folder along with instructions on how to set up the build. Typically this results in a 'package' which can then be manually installed in the NAS. To use MEGAcmd on those systems, ssh into the device and run the commands as normal (having first added their folder to your `PATH` variable). # Features: ## Autocompletion: MEGAcmd features autocompletion in both interactive and non-interactive (only for bash) mode. It will help completing both local and remote (Mega Cloud) files, flags for commands, values for flags/access levels, even contacts. ## Verbosity There are two different kinds of logging messages: - **SDK based**: those messages reported by the sdk and dependent libraries. - **MEGAcmd based**: those messages reported by MEGAcmd itself. You can adjust the level of logging for those kinds with `log` command. However, passing `-v` (`-vv`, `-vvv`, and so on for a more verbose output) to an specific command will use higher level of verbosity of MEGAcmd based messages. Further info on verbosity [here](contrib/docs/DEBUG.md). ## Regular Expressions If you have compiled MEGAcmd with PCRE (enabled by default), you can use PCRE compatible expressions in certain commands with the flag `--use-pcre`. Otherwise, if compiled with c++11, c++11 regular expressions will be used. If none of the above is the case, you can only use wildcards: "*" for any number of characters or "?" for a single unknown character. You can check the regular expressions compatibility with `find --help`: ``` find --help ... Options: --pattern=PATTERN Pattern to match (Perl Compatible Regular Expressions) ``` **Notice:** if you use MEGAcmd in non interactive mode, notice that shell pattern will take precedence. You will need to either escape symbols like `*` (`\*`) or surround them between quotes (e.g: "*.txt"). ## MEGAcmd Updates MEGAcmd updates automatically for Windows & macOS. For Linux, whenever there is a new update, it will be published in the corresponding repository and your system's updating tool will let you update it. ### Disable automatic updates You can type `update --auto=OFF` to disable automatic updates. `update --auto=ON` will re-enable them. If you want to see the state of automatic updates you can use `update --auto=query`. This will inform if automatic updates are enabled or not. Notice that MEGAcmdServer must be running in order to have automatic updates working. You can also update manually by typing `update` within MEGAcmd. This will check if there are updates available and proceed to update if affirmative. Whenever MEGAcmd is updated it will be restarted (all open instances of MEGAcmdShell will be restarted too). Alternatively you can also execute `MEGAcmdUpdater.exe` in Windows or `MEGAcmdUpdater` (located at /Applications/MEGAcmd.app/Contents/MacOS) in macOS. ## Known Bugs Currently there are certain discrepancies with **PATHS** when logging into a public folder. For instance, imagine a folder named `toshare` with a subfolder named `x`. If we login in to `toshare` and execute `find /x`, this will be the output: ``` /toshare/x ``` Whereas if we execute `find /toshare/x`, we receive an error, since folder absolute path refers to `/` as root path. ``` [err: 12:21:51] Couldn't find /toshare/x ``` It might better be referred as `/toshare/x`. MEGAcmd-2.5.2_Linux/UserGuide.md000066400000000000000000001170161516543156300163310ustar00rootroot00000000000000# MEGAcmd User Guide This document relates to MEGAcmd version 2.5.0. It contains introductory information and the [Command Summary](#command-summary), with links to detailed command descriptions. ### What is it A command line tool to work with your MEGA account and files. The intent is to offer all the MEGA account functionality via command line. You can run it in [interactive](#interactive) mode where it processes all commands directly, or you can run its [scriptable](#scriptable) commands from your favourite Linux or Mac shell such as bash, or you can even run its commands in a Windows command prompt. And of course you can write scripts using those scriptable commands. Here is an example of downloading a file using MEGAcmd. In this case we are downloading a file specified by a public link, which does not require being logged in:

``` mega-get https://mega.nz/#F!ABcD1E2F!gHiJ23k-LMno45PqrSTUvw /path/to/local/folder ``` Note:- If you get any error like this one ``` Event not found ``` In link put ```\``` in front of every ```!``` for example ``` mega-get https://mega.nz/#F\!ABcD1E2F\!gHiJ23k-LMno45PqrSTUvw /path/to/local/folder ``` And here is an example of uploading a file using MEGAcmd, and making a link available to share it, that will expire after 10 minutes.

``` mega-put /path/to/my/temporary_resource /exportedstuff/ mega-export -a /exportedstuff/temporary_resource --expire=10M | awk '{print $4}' ``` And here is an example of the power of using [scriptable](#scriptable) MEGAcmd commands in bash. In this case we are going to share some promotional videos previously uploaded to MEGA:

``` for i in $(mega-find /enterprise/video/promotional2015/may --pattern="*mpeg") do mega-export -a $i | awk '{print $4}'; done ``` In addition to running commands on request, MEGAcmd can also be configured to [synchronise](#synchronisation-configurations) folders between your local device and your MEGA account, or perform regular [backups](#backup-configurations) from your device to your MEGA account. In order to enable synchronisation and backup features, and for efficiency running commands, MEGAcmd runs a server process in the background which the MEGAcmd shell or the script commands forward requests to. The server keeps running in the background until it is told to close with the [`quit`](#misc) command. If you want it to keep running when you quit the interactive shell (to keep sync and backup running for example), use `quit --only-shell`. Working with your MEGA account requires signing in with your email and password using the [`login`](#login-logout) command, though you can download public links or upload to public folders without logging in. Logging in with your username and password starts a [Session](#session), and causes some of your account such as the folder structure to be downloaded to your [Local Cache](#local-cache). ### Where can you get it For Linux, Mac, or Windows: Download it from the MEGA.nz website: https://mega.nz/cmd

We are also building it for some NAS systems, please check your provider's App Store. ### What can you do with it The major features are * Move files around inside your MEGA account or between MEGA and your PC using command line tools. * Use those same commands in scripts to manage your files. * Set up synchronization or a backup schedule between a folder on your machine, and a folder on your MEGA account. (use the [`sync`](#moving-copying-files) or [`backup`](#moving-copying-files) commands). * Set up WebDAV access to files in your MEGA account (use the [`webdav`](#webdav) command). * [Linux only] Set up a FUSE mount point to seamlessly access files in your MEGA account (use the [`fuse-add`](#fuse-mount-your-cloud-folder-to-the-local-system) command). See our Help Centre pages for the basics of getting started, and friendly examples of common usages with plenty of pictures: https://mega.nz/help ## Terminology and Descriptions ### Interactive Interactive refers to running the MEGAcmd shell which only processes MEGA commands. You invoke commands by typing and pressing Enter. MEGAcmd shell provides a lot of feedback about what it's doing. You can start the MEGAcmd shell with `mega-cmd` (or `MEGAcmd` on Windows). You can then issue commands like `ls` directly:

`ls /my/account/folder`

or you can get a list of available commands with:

`help`

or you can get detailed information about any particular command by using the `--help` flag with that command:

`ls --help`

Autocompletion (pressing tab to fill in the remainder of a command) is available in interactive mode. ### Scriptable Scriptable refers to running the MEGAcmd commands from a shell such as bash or the windows powershell. If the PATH to the MEGAcmd commands are not yet on the PATH in that shell, you'll need to add it. You can then issue commands like `ls` by prefixing them with the `mega-` prefix:

`mega-ls /my/account/folder`

or you can get a list of available commands with:

`mega-help`

or you can get detailed information about any particular command by using the `--help` flag with that command:

`mega-ls --help`

Scriptable commands can of course be used in scripts to achieve a lot in a short space of time, using loops or preparing all the desired commands ahead of time. If you are using bash as your shell, the MEGAcmd commands support auto-completion. ### Contact A contact is someone (identified by their email address) that also has a MEGA account, who you can share files or folders with, and can chat with on MEGAchat. ### Remote Path This refers to a file or a folder stored in your MEGA account, or a publicly available file or folder in the MEGA cloud. Remote paths always use the '/' character as the separator between folder and file elements. Some MEGAcmd commands allow the use of regular expressions in remote paths. You can check if the command supports those by using the `--help` flag with the command. If you use these in the [scriptable](#scriptable) way, you need to escape characters that would otherwise be intercepted and interpreted by the shell. Paths to folders shared to you from another person start with their email and a : character, see the example at ([example](#shared-folders)) ### Local Path This refers to a file or folder on the PC or device that MEGAcmd is running in. ### Session When you log in with your email and MEGA account password, that creates a session. The session exists until you log out of it or kill it from another client. In MEGAcmd, use `whoami -l` to see all your open sessions across all devices, and use `killsession` to close them. You can use other MEGA clients such as the phone app, or webclient to close these also. Devices that were using a killed session will have their connection to MEGA closed immediately and will no longer have access to your account, until you log in on them again. Syncs, backups, and webdavs are specific to a session, so logging out will cause them to be cancelled. ### Local Cache Logging in with MEGAcmd creates your Local Cache, a subfolder of your home folder. MEGAcmd downloads and stores some data in your Local Cache relating to your account, such as folder structure and contacts, for performance reasons. The MEGAcmd background server keeps the local cache up to date when changes to your account occur from other clients. The cache does contain a way for MEGAcmd to access your MEGA account when it starts up again if you have not specifically logged out. The Local Cache also contains information from your Session, including sync, backup, and webdav configurations. Logging out cleans the Local Cache, but also closes your session and the sync, backup, and webdav configurations are wiped. ### Synchronisation configurations MEGAcmd can set up a synchronisation between a folder on your local machine and a folder in your MEGA account, using the [sync](#syncing) command. This is the same mechanism that MEGAsync uses. The synchronisation is two-way: the folders you nominate to be synced will mirror any action! Whatever you add or delete in your sync folder on your device gets added or deleted in your sync folder in your MEGA account. And additions or deletions in your synced folder in your MEGA account will similarly be applied to your local synced folder. Files that are removed from sync folders are moved to a hidden local folder (Rubbish/.debris inside your local sync folder, or SyncDebris folder in the Rubbish Bin of your MEGA account). Here is a very simple example of setting up a synchronisation with MEGAcmd:

``` sync /path/to/local/folder /folder/in/mega ``` You can set up more than one pair or folders to be synced, and you can also set a sync from another device to the same folder, to achieve folder synchronisations between different devices. The changes are sent via your MEGA account rather than directly between the devices in that case. Additional information about synchronising folders is available in our Help Centre: https://mega.nz/help/client/megasync/syncing ### Backup configurations MEGAcmd can set up a periodic copy of a local folder to your MEGA account using the [`backup`](#backups) command. Here is a simple example that will back up a folder immediately and then at 4am each day, keeping the 10 most recent backups:

``` backup /path/mega/folder /remote/path --period="0 0 4 * * *" --num-backups=10 ``` For further information on backups, please see the [`backup`](#backups) command and the [tutorial](BACKUPS.md). ### WebDAV configurations MEGAcmd can set up access to folders or files in your MEGA account as if they were local folders and files on your device using the [`webdav`](#webdav) command. For example making the folder appear like a local drive on your PC, or providing a hyperlink a browser can access, where the hyperlink is to your PC. For further information on WebDAV, please see the [`webdav`](#webdav) command and the [tutorial](WEBDAV.md ### FUSE mount point If you use Linux, MEGAcmd can set up access to folders or files in your MEGA account as if they were local folders and files on your device using Filesystem in User Space via [`fuse-add`](#fuse-mount-your-cloud-folder-to-the-local-system) command. For further information on FUSE, please see the [`fuse-add`](#fuse-mount-your-cloud-folder-to-the-local-system) command and the [tutorial](FUSE.md). ### Linux On Linux, MEGAcmd commands are installed at /usr/bin and so will already be on your PATH. The interactive shell is `mega-cmd` and the background server is `mega-cmd-server`, which will be automatically started on demand. The various scriptable commands are installed at the same location, and invoke `mega-exec` to send the command to `mega-cmd-server`. If you are using the scriptable commands in bash (or using the interactive commands in mega-cmd), the commands will auto-complete. ### Macintosh For MacOS, after installing the dmg, you can launch the server using **MEGAcmd** in Applications. If you wish to use the client commands from MacOS Terminal, open the Terminal and include the installation folder in the PATH.

Typically: ``` export PATH=/Applications/MEGAcmd.app/Contents/MacOS:$PATH ``` And for bash completion, source `megacmd_completion.sh` : ``` source /Applications/MEGAcmd.app/Contents/MacOS/megacmd_completion.sh ``` ### Windows Once you have MEGAcmd installed, you can start the [interactive](#interactive) shell from the Start Menu or desktop icon. On windows the interactive shell executable is called `MEGAcmdShell.exe` and the server is `MEGAcmdServer.exe`. You can also start MEGAcmd Shell from inside PowerShell. To do so, start powershell from the Start Menu and then execute these commands to start it: ``` $env:PATH += ";$env:LOCALAPPDATA\MEGAcmd" MEGAcmdShell ``` For [scriptable](#scriptable) usage, the MEGAcmd commands are provided via installed .bat files which pass the command to the MEGAcmdServer.exe. Provided you have set the PATH as above, you can use these like normal command line tools in PowerShell: ``` $env:PATH += ";$env:LOCALAPPDATA\MEGAcmd" mega-cd /my/favourite/folder mega-ls ``` Or in Command Prompt: ``` set PATH=%LOCALAPPDATA%\MEGAcmd;%PATH% mega-cd /my/favourite/folder mega-ls ``` And of course those can be invoked in your own .bat or .cmd files. Autocompletion is not available for the scriptable commands, but is in the interactive shell. Unicode is supported though it currently in the interactive shell it needs to be switched on, and to have a suitable font selected; please execute `help --unicode` for the latest information. There are plans to improve this. Please report any issues experienced to our support team. ### NAS Support We have released packages for QNAP and Synology, which you can download and install from the App Center in QNAP, and the Package Center in Synology. In QNAP, please make sure to turn on "Enable home folder for all users" from the control panel, and set HOME=/share/homes/ before starting any MEGA commands, and in Synology, 'Enable user home service', so that the `mega-cmd-server` creates the `.megaCmd` local cache folder there (as the default HOME location may be erased on restart). ## Command Summary These summaries use the usual conventions - `[]` indicates its content is optional, `|` indicates you should choose either the item on the left or the one on the right (but not both) Each command is described as it would be used in the [interactive](#interactive) MEGAcmd shell, and the corresponding [scriptable](#scriptable) command (which must be prefixed with `mega-`) works in the same way. Commands referring to a [remote path](#remote-path) are talking about a file in your MEGA account online, whereas a [local path](#local-path) refers to a file or folder on your local device where MEGAcmd is running. Verbosity: You can increase the amount of information given by any command by passing `-v` (`-vv`, `-vvv`, ...) ### Account / Contacts * [`signup`](contrib/docs/commands/signup.md)`email password [--name="Your Name"]` Register as user with a given email * [`confirm`](contrib/docs/commands/confirm.md)`link email password` Confirm an account using the link provided after the "signup" process. * [`invite`](contrib/docs/commands/invite.md)`[-d|-r] dstemail [--message="MESSAGE"]` Invites a contact / deletes an invitation * [`showpcr`](contrib/docs/commands/showpcr.md)`[--in | --out] [--time-format=FORMAT]` Shows incoming and outgoing contact requests. * [`ipc`](contrib/docs/commands/ipc.md)`email|handle -a|-d|-i` Manages contact incoming invitations. * [`users`](contrib/docs/commands/users.md)`[-s] [-h] [-n] [-d contact@email] [--time-format=FORMAT] [--verify|--unverify contact@email.com] [--help-verify [contact@email.com]]` List contacts * [`userattr`](contrib/docs/commands/userattr.md)`[-s attribute value|attribute|--list] [--user=user@email]` Lists/updates user attributes * [`passwd`](contrib/docs/commands/passwd.md)`[-f] [--auth-code=XXXX] newpassword` Modifies user password * [`masterkey`](contrib/docs/commands/masterkey.md)`pathtosave` Shows your master key. ### Login / Logout * [`login`](contrib/docs/commands/login.md)`[--auth-code=XXXX] email password | exportedfolderurl#key [--auth-key=XXXX] [--resume] | passwordprotectedlink [--password=PASSWORD] | session` Logs into a MEGA account, folder link or a previous session. You can only log into one entity at a time. * [`logout`](contrib/docs/commands/logout.md)`[--keep-session]` Logs out * [`whoami`](contrib/docs/commands/whoami.md)`[-l]` Prints info of the user * [`session`](contrib/docs/commands/session.md) Prints (secret) session ID * [`killsession`](contrib/docs/commands/killsession.md)`[-a | sessionid1 sessionid2 ... ]` Kills a session of current user. ### Browse * [`cd`](contrib/docs/commands/cd.md)`[remotepath]` Changes the current remote folder * [`lcd`](contrib/docs/commands/lcd.md)`[localpath]` Changes the current local folder for the interactive console * [`ls`](contrib/docs/commands/ls.md)`[-halRr] [--show-handles] [--tree] [--versions] [remotepath] [--use-pcre] [--show-creation-time] [--time-format=FORMAT]` Lists files in a remote path * [`pwd`](contrib/docs/commands/pwd.md) Prints the current remote folder * [`lpwd`](contrib/docs/commands/lpwd.md) Prints the current local folder for the interactive console * [`attr`](contrib/docs/commands/attr.md)`remotepath [--force-non-official] [-s attribute value|-d attribute [--print-only-value]` Lists/updates node attributes. * [`du`](contrib/docs/commands/du.md)`[-h] [--versions] [remotepath remotepath2 remotepath3 ... ] [--use-pcre]` Prints size used by files/folders * [`find`](contrib/docs/commands/find.md)`[remotepath] [-l] [--pattern=PATTERN] [--type=d|f] [--mtime=TIMECONSTRAIN] [--size=SIZECONSTRAIN] [--use-pcre] [--time-format=FORMAT] [--show-handles|--print-only-handles]` Find nodes matching a pattern * [`mount`](contrib/docs/commands/mount.md) Lists all the root nodes ### Moving / Copying files * [`mkdir`](contrib/docs/commands/mkdir.md)`[-p] remotepath` Creates a directory or a directories hierarchy * [`cp`](contrib/docs/commands/cp.md)`[--use-pcre] srcremotepath [srcremotepath2 srcremotepath3 ..] dstremotepath|dstemail` : Copies files/folders into a new location (all remotes) * [`put`](contrib/docs/commands/put.md)`[-c] [-q] [--print-tag-at-start] localfile [localfile2 localfile3 ...] [dstremotepath]` Uploads files/folders to a remote folder * [`get`](contrib/docs/commands/get.md)`[-m] [-q] [--ignore-quota-warn] [--use-pcre] [--password=PASSWORD] exportedlink|remotepath [localpath]` Downloads a remote file/folder or a public link * [`preview`](contrib/docs/commands/preview.md)`[-s] remotepath localpath` To download/upload the preview of a file. * [`thumbnail`](contrib/docs/commands/thumbnail.md)`[-s] remotepath localpath` To download/upload the thumbnail of a file. * [`mv`](contrib/docs/commands/mv.md)`srcremotepath [--use-pcre] [srcremotepath2 srcremotepath3 ..] dstremotepath` Moves file(s)/folder(s) into a new location (all remotes) * [`rm`](contrib/docs/commands/rm.md)`[-r] [-f] [--use-pcre] remotepath` Deletes a remote file/folder * [`transfers`](contrib/docs/commands/transfers.md)`[-c TAG|-a] | [-r TAG|-a] | [-p TAG|-a] [--only-downloads | --only-uploads] [SHOWOPTIONS]` List or operate with transfers * [`speedlimit`](contrib/docs/commands/speedlimit.md)`[-u|-d|--upload-connections|--download-connections] [-h] [NEWLIMIT]` Displays/modifies upload/download rate limits: either speed or max connections * [`sync`](contrib/docs/commands/sync.md)`[localpath dstremotepath| [-dpe] [ID|localpath]` Controls synchronizations. * [`sync-issues`](contrib/docs/commands/sync-issues.md)`[[--detail (ID|--all)] [--limit=rowcount] [--disable-path-collapse]] | [--enable-warning|--disable-warning]` Show all issues with current syncs * [`sync-ignore`](contrib/docs/commands/sync-ignore.md)`[--show|[--add|--add-exclusion|--remove|--remove-exclusion] filter1 filter2 ...] (ID|localpath|DEFAULT)` Manages ignore filters for syncs * [`sync-config`](contrib/docs/commands/sync-config.md)`[--delayed-uploads-wait-seconds | --delayed-uploads-max-attempts]` Controls sync configuration. * [`exclude`](contrib/docs/commands/exclude.md)`[(-a|-d) pattern1 pattern2 pattern3]` Manages default exclusion rules in syncs. * [`backup`](contrib/docs/commands/backup.md)`(localpath remotepath --period="PERIODSTRING" --num-backups=N | [-lhda] [TAG|localpath] [--period="PERIODSTRING"] [--num-backups=N]) [--time-format=FORMAT]` Controls backups ### Sharing (your own files, of course, without infringing any copyright) * [`export`](contrib/docs/commands/export.md)`[-d|-a [--writable] [--mega-hosted] [--password=PASSWORD] [--expire=TIMEDELAY] [-f]] [remotepath] [--use-pcre] [--time-format=FORMAT]` Prints/Modifies the status of current exports * [`import`](contrib/docs/commands/import.md)`exportedlink [--password=PASSWORD] [remotepath]` Imports the contents of a remote link into user's cloud * [`share`](contrib/docs/commands/share.md)`[-p] [-d|-a --with=user@email.com [--level=LEVEL]] [remotepath] [--use-pcre] [--time-format=FORMAT]` Prints/Modifies the status of current shares * [`webdav`](contrib/docs/commands/webdav.md)`[-d (--all | remotepath ) ] [ remotepath [--port=PORT] [--public] [--tls --certificate=/path/to/certificate.pem --key=/path/to/certificate.key]] [--use-pcre]` Configures a WEBDAV server to serve a location in MEGA ### FUSE (mount your cloud folder to the local system) * [`fuse-add`](contrib/docs/commands/fuse-add.md)`[--name=name] [--disabled] [--transient] [--read-only] localPath remotePath` Creates a new FUSE mount. * [`fuse-remove`](contrib/docs/commands/fuse-remove.md)`(name|localPath)` Deletes a specified FUSE mount. * [`fuse-enable`](contrib/docs/commands/fuse-enable.md)`[--temporarily] (name|localPath)` Enables a specified FUSE mount. * [`fuse-disable`](contrib/docs/commands/fuse-disable.md)`[--temporarily] (name|localPath)` Disables a specified FUSE mount. * [`fuse-show`](contrib/docs/commands/fuse-show.md)`[--only-enabled] [--disable-path-collapse] [[--limit=rowcount] | [name|localPath]]` Displays the list of FUSE mounts and their information. If a name or local path provided, displays information of that mount instead. * [`fuse-config`](contrib/docs/commands/fuse-config.md)`[--name=name] [--enable-at-startup=yes|no] [--persistent=yes|no] [--read-only=yes|no] (name|localPath)` Modifies the specified FUSE mount configuration. ### Misc. * [`autocomplete`](contrib/docs/commands/autocomplete.md)`[dos | unix]` Modifies how tab completion operates. * [`cancel`](contrib/docs/commands/cancel.md) Cancels your MEGA account * [`cat`](contrib/docs/commands/cat.md)`remotepath1 remotepath2 ...` Prints the contents of remote files * [`clear`](contrib/docs/commands/clear.md) Clear screen * [`codepage`](contrib/docs/commands/codepage.md)`[N [M]]` Switches the codepage used to decide which characters show on-screen. * [`configure`](contrib/docs/commands/configure.md)`[key [value]]` Shows and modifies global configurations. * [`confirmcancel`](contrib/docs/commands/confirmcancel.md)`link password` Confirms the cancellation of your MEGA account * [`debug`](contrib/docs/commands/debug.md) Enters debugging mode (HIGHLY VERBOSE) * [`deleteversions`](contrib/docs/commands/deleteversions.md)`[-f] (--all | remotepath1 remotepath2 ...) [--use-pcre]` Deletes previous versions. * [`df`](contrib/docs/commands/df.md)`[-h]` Shows storage info * [`errorcode`](contrib/docs/commands/errorcode.md)`number` Translate error code into string * [`exit`](contrib/docs/commands/exit.md)`[--only-shell]` Quits MEGAcmd * [`ftp`](contrib/docs/commands/ftp.md)`[-d ( --all | remotepath ) ] [ remotepath [--port=PORT] [--data-ports=BEGIN-END] [--public] [--tls --certificate=/path/to/certificate.pem --key=/path/to/certificate.key]] [--use-pcre]` Configures a FTP server to serve a location in MEGA * [`graphics`](contrib/docs/commands/graphics.md)`[on|off]` Shows if special features related to images and videos are enabled. * [`help`](contrib/docs/commands/help.md)`[-f|-ff|--non-interactive|--upgrade|--paths] [--show-all-options]` Prints list of commands * [`https`](contrib/docs/commands/https.md)`[on|off]` Shows if HTTPS is used for transfers. Use "https on" to enable it. * [`log`](contrib/docs/commands/log.md)`[-sc] level` Prints/Modifies the log level * [`mediainfo`](contrib/docs/commands/mediainfo.md)`remotepath1 remotepath2 ...` Prints media info of remote files * [`permissions`](contrib/docs/commands/permissions.md)`[(--files|--folders) [-s XXX]]` Shows/Establish default permissions for files and folders created by MEGAcmd. * [`proxy`](contrib/docs/commands/proxy.md)`[URL|--auto|--none] [--username=USERNAME --password=PASSWORD]` Show or sets proxy configuration * [`psa`](contrib/docs/commands/psa.md)`[--discard]` Shows the next available Public Service Announcement (PSA) * [`quit`](contrib/docs/commands/quit.md)`[--only-shell]` Quits MEGAcmd * [`reload`](contrib/docs/commands/reload.md) Forces a reload of the remote files of the user * [`tree`](contrib/docs/commands/tree.md)`[remotepath]` Lists files in a remote path in a nested tree decorated output * [`unicode`](contrib/docs/commands/unicode.md) Toggle unicode input enabled/disabled in interactive shell * [`update`](contrib/docs/commands/update.md)`[--auto=on|off|query]` Updates MEGAcmd * [`version`](contrib/docs/commands/version.md)`[-l][-c]` Prints MEGAcmd versioning and extra info ## Examples Some examples of typical MEGAcmd workflows and commands. **Note:** command output might differ. It could be slightly outdated, or it could've been manually re-formatted to better fit this markdown page. ### Account management #### Creating new accounts

MEGA CMD> signup eg.email_1@example.co.nz --name="test1"
New Password:
Retype New Password:
Account  created succesfully. You will receive a confirmation link. Use "confirm" with the provided link to confirm that account

MEGA CMD> confirm https://mega.nz/#confirmQFSfjtUkExc5M2Us6q5d-klx60RfxVbxjhk eg.email_1@example.co.nz
Password:
Account eg.email_1@example.co.nz confirmed succesfully. You can login with it now

MEGA CMD> signup eg.email_2@example.co.nz --name="test2"
New Password:
Retype New Password:
Account  created succesfully. You will receive a confirmation link. Use "confirm" with the provided link to confirm that account

MEGA CMD> confirm https://mega.nz/#confirmcz7Ss68ChhMKk8WEFTQCqLMHJg8esAEEpQE eg.email_2@example.co.nz
Password:
Account eg.email_2@example.co.nz confirmed succesfully. You can login with it now
#### Logging-in and contacts
MEGA CMD> login eg.email_1@example.co.nz
Password:
[SDK:info: 23:19:14] Fetching nodes ...
Fetching nodes ||########################################||(38/38 MB: 100.00 %)
[SDK:info: 23:19:17] Loading transfers from local cache
[SDK:info: 23:19:17] Login complete as eg.email_1@example.co.nz
#### Adding a contact and viewing
eg.email_1@example.co.nz:/$ invite eg.email_2@example.co.nz
Invitation to user: eg.email_2@example.co.nz sent

eg.email_1@example.co.nz:/$ showpcr
Outgoing PCRs:
 eg.email_2@example.co.nz  (id: 47Xhz6wvVTk, creation: Thu, 26 Apr 2018 11:20:09 +1200, modification: Thu, 26 Apr 2018 11:20:09 +1200)

eg.email_1@example.co.nz:/$ logout
Logging out...

MEGA CMD> login eg.email_2@example.co.nz
Password:
[SDK:info: 23:21:10] Fetching nodes ...
[SDK:info: 23:21:12] Loading transfers from local cache
[SDK:info: 23:21:12] Login complete as eg.email_2@example.co.nz

eg.email_2@example.co.nz:/$ showpcr
Incoming PCRs:
 eg.email_1@example.co.nz  (id: 47Xhz6wvVTk, creation: Thu, 26 Apr 2018 11:20:09 +1200, modification: Thu, 26 Apr 2018 11:20:09 +1200)

eg.email_2@example.co.nz:/$ ipc 47Xhz6wvVTk -a
Accepted invitation by eg.email_1@example.co.nz

eg.email_2@example.co.nz:/$ users
eg.email_1@example.co.nz, visible since Thu, 26 Apr 2018 11:22:02 +1200

eg.email_2@example.co.nz:/$ userattr --user=eg.email_1@example.co.nz
        firstname = test1
        ed25519 = 5Xl2-mUtsZkaATmSS88Ncepju5805uw66Hfdh_-SwpE
        cu25519 = ejoYtpaJIZvlpmPsYviIa6tNvPTdVjfkYf9G1k8PKgM
        rsa = AAAAAFrhDPPMS1AXAhJwScpJ_GKqFUJ42uIIcwxLp5RIalkWtsa5j87u2LFhoZlI_rHIzGXrdsbywgs7Msisw0CjodrtwtME
        cu255 = AAAAAFrhDPPWUOP2tNByV72zU4M3EKNoddyVCT13VkkouMldniR2UZtLrPjUjUeOZOLvOL7H1C0W0Q_b3QqYSvAKo775pUwD

eg.email_2@example.co.nz:/$ logout
Logging out...

MEGA CMD> login eg.email_1@example.co.nz
Password:
[SDK:info: 23:24:26] Fetching nodes ...
[SDK:info: 23:24:27] Loading transfers from local cache
[SDK:info: 23:24:27] Login complete as eg.email_1@example.co.nz

eg.email_1@example.co.nz:/$ users
eg.email_2@example.co.nz, visible

eg.email_1@example.co.nz:/$ userattr --user=eg.email_2@example.co.nz
        firstname = test2
        ed25519 = M7SLy2RajwUAvynxJQaVkhe6hxGpbwJmvve3dgl8B1o
        cu25519 = VaXluGS2c5xbo0xOHHJciqLRxwMaWZHVK8iuxtlCBTk
        rsa = AAAAAFrhDWemabQ4JAOtP7zcoy6m74PsFTFCbj04Zh4G8K_TZB5Sm9T5Xj9CXYzwWnpfRd1McPdDouKdsASQ6Er7i4Y4LpEA
        cu255 = AAAAAFrhDWcXE_7AHZmvxk5Hk0G7V65UnvFO42tb1gM9SYy3BpsMCas0X-pbqkYwf6_2eBG-ZLvkonGfXB3DWonWNvnVehIB
### Node operations #### Getting user info
MEGA CMD> login eg.email_1@example.co.nz
Password:
[SDK:info: 23:43:14] Fetching nodes ...
[SDK:info: 23:43:14] Loading transfers from local cache
[SDK:info: 23:43:14] Login complete as eg.email_1@example.co.nz

eg.email_1@example.co.nz:/$ whoami -l
Account e-mail: eg.email_1@example.co.nz
    Available storage: 50.00 GBytes
        In ROOT:      146... KBytes in     1 file(s) and     0 folder(s)
        In INBOX:       0.00  Bytes in     0 file(s) and     0 folder(s)
        In RUBBISH:     0.00  Bytes in     0 file(s) and     0 folder(s)
        Total size taken up by file versions:      0.00  Bytes
    Pro level: 0
    Subscription type:
    Account balance:
Current Active Sessions:
    * Current Session
    Session ID: m3a8eluyPdo
    Session start: 4/26/2018 11:43:12 AM
    Most recent activity: 4/26/2018 11:43:13 AM
    IP: 122.56.56.232
    Country: NZ
    User-Agent: MEGAcmd/0.9.9.0 (Windows 10.0.16299) MegaClient/3.3.5
    -----
1 active sessions opened

eg.email_1@example.co.nz:/$ mount
ROOT on /
INBOX on //in
RUBBISH on //bin
#### Downloading a file
eg.email_1@example.co.nz:/$ ls
Welcome to MEGA.pdf

eg.email_1@example.co.nz:/$ get "Welcome to MEGA.pdf"
TRANSFERING ||################################################################################||(1/1 MB: 100.00 %)
Download finished: Welcome to MEGA.pdf
TRANSFERING ||################################################################################||(1/1 MB: 100.00 %)
#### Uploading a file
eg.email_1@example.co.nz:/$ mkdir my-pictures

eg.email_1@example.co.nz:/$ cd my-pictures/

eg.email_1@example.co.nz:/my-pictures$ put C:\Users\MYWINDOWSUSER\Pictures
TRANSFERING ||################################################################################||(1/1 MB: 100.00 %)
Upload finished: C:\Users\MYWINDOWUSER\Pictures
TRANSFERING ||################################################################################||(1/1 MB: 100.00 %)
#### Creating and navigating directories
eg.email_1@example.co.nz:/my-pictures$ pwd
/my-pictures

eg.email_1@example.co.nz:/my-pictures$ ls
Pictures

eg.email_1@example.co.nz:/my-pictures$ cd Pictures/

eg.email_1@example.co.nz:/my-pictures/my-pictures$ ls
Camera Roll
Feedback
Saved Pictures
megacmdpkg.gif
megacmdpkg_80.gif
megacmdpkg_gray.gif

eg.email_1@example.co.nz:/my-pictures/my-pictures$ pwd
/my-pictures/Pictures

eg.email_1@example.co.nz:/my-pictures/my-pictures$ cd /

eg.email_1@example.co.nz:/$ du -h my-pictures/
FILENAME                                        SIZE
my-pictures:                                 1.31 MB
----------------------------------------------------------------
Total storage used:                          1.31 MB
#### Logging-out
eg.email_1@example.co.nz:/$ logout
Logging out...
MEGA CMD>
### Syncing
email_1@example.co.nz:/$ sync c:\Go go-backup/
Added sync: //?\c:\Go to /go-backup

email_1@example.co.nz:/$ sync
ID          LOCALPATH                   REMOTEPATH RUN_STATE STATUS  ERROR SIZE      FILES DIRS
WOOmFwZfQwM \\?\c:\Go                   /go-backup Running   Syncing NO    119.13 KB 10    97

email_1@example.co.nz:/$ sync
ID          LOCALPATH                   REMOTEPATH RUN_STATE STATUS  ERROR SIZE     FILES DIRS
WOOmFwZfQwM \\?\c:\Go                   /go-backup Running   Syncing NO    61.22 MB 1252  463

email_1@example.co.nz:/$ sync
ID          LOCALPATH                   REMOTEPATH RUN_STATE STATUS  ERROR SIZE      FILES DIRS
WOOmFwZfQwM \\?\c:\Go                   /go-backup Running   Syncing NO    232.94 MB 4942  773

email_1@example.co.nz:/$ sync
ID          LOCALPATH                   REMOTEPATH RUN_STATE STATUS ERROR SIZE      FILES DIRS
WOOmFwZfQwM \\?\c:\Go                   /go-backup Running   Synced NO    285.91 MB 7710  1003
Then, on a windows cmd prompt:
C:\Users\ME>rmdir /s c:\go\blog
c:\go\blog, Are you sure (Y/N)? Y
Back in MEGAcmd (the update has been applied to MEGA already):
email_1@example.co.nz:/$ sync
ID          LOCALPATH                   REMOTEPATH RUN_STATE STATUS ERROR SIZE      FILES DIRS
WOOmFwZfQwM \\?\c:\Go                   /go-backup Running   Synced NO    268.53 MB 7306  961
### Backups
eg.email@example.co.nz:/$ backup c:/cmake /cmake-backup --period="0 0 4 * * *" --num-backups=3
Backup established: c:/cmake into /cmake-backup period="0 0 4 * * *" Number-of-Backups=3

eg.email@example.co.nz:/$ backup
TAG   LOCALPATH                                               REMOTEPARENTPATH                                                STATUS
166   \\?\c:\cmake                                            /cmake-backup                                                  COMPLETE

eg.email@example.co.nz:/$ backup -h
TAG   LOCALPATH                                               REMOTEPARENTPATH                                                STATUS
166   \\?\c:\cmake                                            /cmake-backup                                                  COMPLETE
   -- HISTORY OF BACKUPS --
  NAME                                                    DATE                    STATUS   FILES FOLDERS
  cmake_bk_20180426133300                                 26Apr2018 13:33:00      COMPLETE     0      92
### WebDAV
eg.email@example.co.nz:/$ webdav myfile.tif --port=1024
Serving via webdav myfile.tif: http://127.0.0.1:1024/5mYHQT4B/myfile.tif

eg.email@example.co.nz:/$ webdav
WEBDAV SERVED LOCATIONS:
/myfile.tif: http://127.0.0.1:1024/5mYHQT4B/myfile.tif

eg.email@example.co.nz:/$ webdav -d myfile.tif
myfile.tif no longer served via webdav
### Exporting and importing
eg.email_1@example.co.nz:/$ export -a Pictures/
MEGA respects the copyrights of others and requires that users of the MEGA cloud service comply with the laws of copyright.
You are strictly prohibited from using the MEGA cloud service to infringe copyrights.
You may not upload, download, store, share, display, stream, distribute, email, link to, transmit or otherwise make available any files, data or content that infringes any copyright or other proprietary rights of any person or entity. Do you accept this terms? (Yes/No): Yes
Please enter [y]es/[n]o/[a]ll/none:yes
Exported /Pictures: https://mega.nz/#F!iaZlEBIL!mQD3rFuJhKov0sco-6s9xg

eg.email_1@example.co.nz:/$ export
Pictures (folder, shared as exported permanent folder link: https://mega.nz/#F!iaZlEBIL!mQD3rFuJhKov0sco-6s9xg)

eg.email_1@example.co.nz:/$ logout --keep-session
Logging out...
Session closed but not deleted. Warning: it will be restored the next time you execute the application. Execute "logout" to delete the session permanently.
You can also login with the session id: ARo7aiLAxK-jseOdVBYhj285Twb06ivWsFmT4XAnkTsiaDRRbm5oYS1zRm-V3I0FHHOvwj7P2RPvrSw_

MEGA CMD> login eg.email_2@example.co.nz
Password:
[SDK:info: 01:55:04] Fetching nodes ...
[SDK:info: 01:55:05] Loading transfers from local cache
[SDK:info: 01:55:05] Login complete as eg.email_2@example.co.nz

eg.email_2@example.co.nz:/$ ls
Welcome to MEGA.pdf

eg.email_2@example.co.nz:/$ import https://mega.nz/#F!iaZlEBIL!mQD3rFuJhKov0sco-6s9xg
Imported folder complete: /Pictures

eg.email_2@example.co.nz:/$ ls
Pictures
Welcome to MEGA.pdf

eg.email_2@example.co.nz:/$ ls Pictures/
Camera Roll
Feedback
Saved Pictures
megacmdpkg.gif
megacmdpkg_80.gif
megacmdpkg_gray.gif

eg.email_2@example.co.nz:/$ logout
Logging out...

MEGA CMD> login ARo7aiLAxK-jseOdVBYhj285Twb06ivWsFmT4XAnkTsiaDRRbm5oYS1zRm-V3I0FHHOvwj7P2RPvrSw_
eg.email_1@example.co.nz:/$ export
Pictures (folder, shared as exported permanent folder link: https://mega.nz/#F!iaZlEBIL!mQD3rFuJhKov0sco-6s9xg)

eg.email_1@example.co.nz:/$ export -d Pictures/
Disabled export: /Pictures

eg.email_1@example.co.nz:/$ export
Couldn't find anything exported below current folder. Use -a to export it
### Transfers
eg.email@example.co.nz:/tmp-test/Mega.dir$ transfers
DIR/SYNC TAG  SOURCEPATH                         DESTINYPATH                              PROGRESS           STATE
 U     17361 \\?\C:\Users\ME\...ebug\megaapi.obj /tmp-test/Mega.dir/Mega.dir/Debug    100.00% of 2016.62 KB  ACTIVE
 U     17362 \\?\C:\Users\ME\...megaapi_impl.obj /tmp-test/Mega.dir/Mega.dir/Debug     13.64% of   13.85 MB  ACTIVE
 U     17363 \\?\C:\Users\ME\...g\megaclient.obj /tmp-test/Mega.dir/Mega.dir/Debug      0.00% of   15.46 MB  QUEUED
 U     17364 \\?\C:\Users\ME\..._http_parser.obj /tmp-test/Mega.dir/Mega.dir/Debug      0.00% of   85.15 KB  QUEUED
 U     17365 \\?\C:\Users\ME\...ega_utf8proc.obj /tmp-test/Mega.dir/Mega.dir/Debug      0.00% of  312.44 KB  QUEUED
 U     17366 \\?\C:\Users\ME\...\mega_zxcvbn.obj /tmp-test/Mega.dir/Mega.dir/Debug      0.00% of  589.88 KB  QUEUED
 U     17367 \\?\C:\Users\ME\...ir\Debug\net.obj /tmp-test/Mega.dir/Mega.dir/Debug      0.00% of    3.20 MB  QUEUED
 U     17368 \\?\C:\Users\ME\...r\Debug\node.obj /tmp-test/Mega.dir/Mega.dir/Debug      0.00% of    3.73 MB  QUEUED
 U     17369 \\?\C:\Users\ME\...ntactrequest.obj /tmp-test/Mega.dir/Mega.dir/Debug      0.00% of  352.22 KB  QUEUED
 U     17370 \\?\C:\Users\ME\...\Debug\proxy.obj /tmp-test/Mega.dir/Mega.dir/Debug      0.00% of  203.57 KB  QUEUED
 ...  Showing first 10 transfers ...

eg.email@example.co.nz:/tmp-test/Mega.dir$ transfers -p 17367
Transfer 17367 paused successfully.

eg.email@example.co.nz:/tmp-test/Mega.dir$ transfers -c 17370
Transfer 17370 cancelled successfully.

eg.email@example.co.nz:/tmp-test/Mega.dir$ transfers
DIR/SYNC TAG  SOURCEPATH                         DESTINYPATH                              PROGRESS           STATE
 U     17362 \\?\C:\Users\ME\...megaapi_impl.obj /tmp-test/Mega.dir/Mega.dir/Debug     96.32% of   13.85 MB  ACTIVE
 U     17363 \\?\C:\Users\ME\...g\megaclient.obj /tmp-test/Mega.dir/Mega.dir/Debug      0.20% of   15.46 MB  ACTIVE
 U     17364 \\?\C:\Users\ME\..._http_parser.obj /tmp-test/Mega.dir/Mega.dir/Debug      0.00% of   85.15 KB  QUEUED
 U     17365 \\?\C:\Users\ME\...ega_utf8proc.obj /tmp-test/Mega.dir/Mega.dir/Debug      0.00% of  312.44 KB  QUEUED
 U     17366 \\?\C:\Users\ME\...\mega_zxcvbn.obj /tmp-test/Mega.dir/Mega.dir/Debug      0.00% of  589.88 KB  QUEUED
 U     17367 \\?\C:\Users\ME\...ir\Debug\net.obj /tmp-test/Mega.dir/Mega.dir/Debug      0.00% of    3.20 MB  PAUSED
 U     17368 \\?\C:\Users\ME\...r\Debug\node.obj /tmp-test/Mega.dir/Mega.dir/Debug      0.00% of    3.73 MB  QUEUED
 U     17369 \\?\C:\Users\ME\...ntactrequest.obj /tmp-test/Mega.dir/Mega.dir/Debug      0.00% of  352.22 KB  QUEUED
 U     17371 \\?\C:\Users\ME\...pubkeyaction.obj /tmp-test/Mega.dir/Mega.dir/Debug      0.00% of  355.75 KB  QUEUED
 U     17372 \\?\C:\Users\ME\...ebug\request.obj /tmp-test/Mega.dir/Mega.dir/Debug      0.00% of  933.14 KB  QUEUED
 ...  Showing first 10 transfers ...
### Shared folders
eg.email@example.co.nz:/$ mount
ROOT on /
INBOX on //in
RUBBISH on //bin
INSHARE on //from/family.member@example.co.nz:photos_Jan_1_2020 (read access)
INSHARE on //from/family.member@example.co.nz:other_folder (read access)

eg.email@example.co.nz:/$ ls family.member@example.co.nz:photos_Jan_1_2020
photo1.jpg
photo2.jpg

eg.email@example.co.nz:/$ get family.member@example.co.nz:photos_Jan_1_2020/photo1.jpg
TRANSFERRING ||###########################################################################################||(5/5 MB: 100.00 %)
Download finished: .\photo1.jpg

eg.email@example.co.nz:/$ share -a --with=family.member@example.co.nz --level=0  "/Camera Uploads/my_photos_from_that_day"
Shared /Camera Uploads/my_photos_from_that_day : family.member@example.co.nz accessLevel=0
MEGAcmd-2.5.2_Linux/build-with-docker/000077500000000000000000000000001516543156300174225ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/build-with-docker/Dockerfile.cmake000066400000000000000000000066211516543156300225000ustar00rootroot00000000000000# syntax=docker/dockerfile:1 FROM debian:12-slim as base RUN rm -f /etc/apt/apt.conf.d/docker-clean; \ echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ apt-get update \ && apt-get install -y --no-install-recommends \ cmake zip pkg-config curl python3 python3-pip autoconf-archive nasm git g++ uuid-runtime zstd fuse \ && python3 -m pip install unittest-xml-reporting pexpect --break-system-packages RUN if [ ! -e /etc/machine-id ]; then \ uuidgen > /etc/machine-id; \ fi FROM base as build-deps-cmake RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ apt-get update \ && apt-get install -y --no-install-recommends \ ccache jq \ cmake zip pkg-config curl python3 autoconf-archive nasm libgtest-dev libgmock-dev git g++ make unzip autoconf ca-certificates automake ninja-build \ libfuse-dev libtool COPY vcpkg.json ./vcpkg.json COPY build/clone_vcpkg_from_baseline.sh ./clone_vcpkg_from_baseline.sh RUN /clone_vcpkg_from_baseline.sh /vcpkg FROM scratch as src WORKDIR /usr/src/megacmd COPY sdk ./sdk COPY src ./src COPY build ./build COPY contrib ./contrib COPY tests/common ./tests/common COPY tests/integration ./tests/integration COPY tests/unit ./tests/unit COPY CMakeLists.txt ./CMakeLists.txt COPY vcpkg.json ./vcpkg.json FROM build-deps-cmake as build LABEL stage=autocleanable-intermediate-stage WORKDIR /usr/src/megacmd # setting ccache as env variables breaks latest vcpkg libsodium compilation #ENV CC "ccache gcc-12" #ENV CXX "ccache g++-12" ENV CCACHE_DIR=/tmp/ccache ENV VCPKG_DEFAULT_BINARY_CACHE=/tmp/vcpkgcache ENV VCPKG_BINARY_SOURCES="clear;files,/tmp/vcpkgcache,readwrite" ARG ENABLE_asan=OFF ARG ENABLE_ubsan=OFF ARG ENABLE_tsan=OFF ARG ENABLE_MEGACMD_TESTS=ON ARG BUILD_CORES COPY --from=src /usr/src/megacmd /usr/src/megacmd #We don't wont a potential config coming from host machine to meddle with the build: RUN rm ./sdk/include/mega/config.h || true RUN --mount=type=cache,target=/tmp/ccache \ --mount=type=cache,target=/tmp/vcpkgcache \ --mount=type=tmpfs,target=/tmp/build \ VCPKG_MAX_CONCURRENCY=${BUILD_CORES:-$(nproc)} \ flock -w 7200 /tmp/vcpkgcache/.vcpkg-install.lock \ cmake -B /tmp/build \ -DVCPKG_ROOT=/vcpkg \ -DCMAKE_CXX_COMPILER=g++ \ -DCMAKE_C_COMPILER=gcc \ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ -DCMAKE_C_COMPILER_LAUNCHER=ccache \ -DCMAKE_BUILD_TYPE=Debug \ -DCMAKE_VERBOSE_MAKEFILE=ON \ -DENABLE_ASAN=${ENABLE_asan} \ -DENABLE_UBSAN=${ENABLE_ubsan} \ -DENABLE_TSAN=${ENABLE_tsan} \ -DENABLE_MEGACMD_TESTS=${ENABLE_MEGACMD_TESTS} \ -DWITH_FUSE=ON \ && cmake --build /tmp/build -j${BUILD_CORES:-$(nproc)} --target mega-cmd mega-cmd-server mega-exec \ mega-cmd-updater mega-cmd-tests-integration mega-cmd-tests-unit \ && cmake --install /tmp/build #|| mkdir /inspectme && mv /tmp/build/* /vcpkg /inspectme FROM base as final COPY --from=build /usr/bin/mega* /usr/bin/ COPY --from=build /opt/megacmd /opt/megacmd COPY --chmod=555 tests/*.py /usr/local/bin/ #COPY --from=build /inspectme /inspectme # Integration tests that use fuse will requiere uid and gid 1001 RUN groupadd -g 1001 jenkins && useradd jenkins -u 1001 -g 1001 -m -s /bin/bash MEGAcmd-2.5.2_Linux/build/000077500000000000000000000000001516543156300152045ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/build/SynologyNAS/000077500000000000000000000000001516543156300173715ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/build/SynologyNAS/README.md000066400000000000000000000062011516543156300206470ustar00rootroot00000000000000# How to build MEGAcmd for Synology NAS drives. We use docker to cross-compile MEGAcmd for Synology. To build for a specific platform, run: ``` docker build -t megacmd-dms-build-env -f $PWD/build/SynologyNAS/synology-cross-build.dockerfile $PWD --build-arg PLATFORM= ``` The `-t` argument tags the container with a name, whereas `-f` specifies the dockerfile. Note that the working directory has to be the MEGAcmd repository. The possible platforms are: ``` alpine alpine4k apollolake armada37xx armada38x avoton broadwell broadwellnk broadwellnkv2 broadwellntbap bromolow braswell denverton epyc7002 geminilake grantley kvmx64 monaco purley r1000 rtd1296 rtd1619b v1000 ``` After building successfully, we need to run the container so we can extract the generated package. To do so, run: ``` docker run -d --name megacmd-dms megacmd-dms-build-env ``` Then, copy the package folder to the current directory with: ``` docker cp megacmd-dms:/image/ . ``` After copying, we can stop and remove the container if needed: ``` docker rm -f megacmd-dms ``` The script `build/SynologyNAS/generate_pkg.sh` automates all of these steps, creating the necessary docker container, extracting the package from it, and deleting it afterwards. In this case, the package will be extracted to `build/SynologyNAS/packages`. To install the package on your NAS device, login using the web interface. Navigate to the Package Manager, and click Manual Install. This will open up a menu which lets you choose a local file. Select your generated .pkg file, and install. To actually run MEGAcmd, you'll need to enable a telnet or SSH connection in the Synology NAS, and run a remote terminal on it. Executables are available at `/volume1/@appstore/megacmdpkg`. # Notes **Writable Home Directory** MEGAcmd requires a user's home directory to be writable. If you intend to use MEGAcmd as a non-root user on your Synology device, make sure you've correctly set up a home directory for your user. Generally this will require formatting a volume and configuring Synology's "User Home Service." You can learn more here: [Synology DSM - Advanced User Settings](https://www.synology.com/en-global/knowledgebase/DSM/help/DSM/AdminCenter/file_user_advanced) **Limit on Inotify Watches** MEGAcmd makes use of the Linux kernel's inotify functionality to monitor the filesystem for changes. Unfortunately, inotify watches are relatively expensive constructs and so the Linux kernel imposes a configurable limit to how many can be established. How large this limit is depends on the machine running Linux. Synology NAS, being embedded devices, have a somewhat small limit. If you find yourself having trouble synchronizing large directory trees, you can try increasing this limit using Linux's sysctl command. To view the current limit, you can issue the following command: `sysctl fs.inotify.max_user_watches` To temporarily alter the limit, you can issue this command as root: `sysctl fs.inotify.max_user_watches=131072` To permanently alter the limit, you will need to edit /etc/sysctl.conf. You can learn about sysctl.conf's format [here](https://man7.org/linux/man-pages/man5/sysctl.conf.5.html) MEGAcmd-2.5.2_Linux/build/SynologyNAS/dockerfile/000077500000000000000000000000001516543156300215005ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/build/SynologyNAS/dockerfile/synology-cross-build.dockerfile000066400000000000000000000050531516543156300276430ustar00rootroot00000000000000# Dockerfile for cross-compiling MEGAcmd for Synology in its different architectures. See README.md for more info. FROM debian:12-slim # Set default architecture ARG PLATFORM=alpine ENV PLATFORM=${PLATFORM} RUN apt-get --quiet=2 update && DEBCONF_NOWARNINGS=yes apt-get --quiet=2 install \ aria2 \ autoconf \ autoconf-archive \ build-essential \ ccache \ cmake \ curl \ fakeroot \ git \ nasm \ pkg-config \ python3 \ tar \ unzip \ wget \ xz-utils \ zip \ 1> /dev/null ENV CCACHE_DIR=/tmp/ccache ENV VCPKG_DEFAULT_BINARY_CACHE=/tmp/vcpkgcache # We don't wont a potential config coming from host machine to meddle with the build RUN rm sdk/include/mega/config.h || true WORKDIR /mega RUN chmod 777 /mega # Clone and checkout known pkgscripts baseline RUN git clone https://github.com/SynologyOpenSource/pkgscripts-ng.git pkgscripts \ && git -C ./pkgscripts checkout e1d9f52 COPY sdk/dockerfile/dms-toolchain.sh /mega/ COPY sdk/dockerfile/dms-toolchains.conf /mega/ COPY build/SynologyNAS/toolkit/source/MEGAcmd/SynoBuildConf/install /mega/spk_install.sh RUN chmod +x /mega/dms-toolchain.sh RUN chmod +x /mega/spk_install.sh COPY vcpkg.json ./vcpkg.json COPY build/clone_vcpkg_from_baseline.sh ./clone_vcpkg_from_baseline.sh RUN /mega/clone_vcpkg_from_baseline.sh /mega/vcpkg WORKDIR /mega/megacmd COPY sdk ./sdk COPY src ./src COPY build ./build COPY contrib ./contrib COPY tests/common ./tests/common COPY CMakeLists.txt ./CMakeLists.txt COPY vcpkg.json ./vcpkg.json RUN /mega/dms-toolchain.sh ${PLATFORM} # Remove the dynamic version of libatomic to avoid compiling with it, which causes # execution to fail in the NAS because the shared library object cannot be found RUN find /mega/toolchain/ \( -type f -o -type l \) -name libatomic.so* -delete RUN --mount=type=cache,target=/tmp/ccache \ --mount=type=cache,target=/tmp/vcpkgcache \ --mount=type=tmpfs,target=/tmp/build \ cmake -B buildDMS \ -DCMAKE_BUILD_TYPE=RelWithDebInfo \ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ -DCMAKE_C_COMPILER_LAUNCHER=ccache \ -DENABLE_MEGACMD_TESTS=OFF \ -DENABLE_ASAN=OFF \ -DENABLE_UBSAN=OFF \ -DENABLE_TSAN=OFF \ -DCMAKE_VERBOSE_MAKEFILE=ON \ -DWITH_FUSE=OFF \ -DVCPKG_CHAINLOAD_TOOLCHAIN_FILE=/mega/${PLATFORM}.toolchain.cmake \ -DVCPKG_OVERLAY_TRIPLETS=/mega \ -DVCPKG_ROOT=/mega/vcpkg \ -DVCPKG_TARGET_TRIPLET=${PLATFORM} && \ cmake --build buildDMS RUN export PLATFORM=${PLATFORM} && /mega/spk_install.sh MEGAcmd-2.5.2_Linux/build/SynologyNAS/generate_pkg.sh000077500000000000000000000016631516543156300223710ustar00rootroot00000000000000#!/bin/bash set -e SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) if [ "$#" -ne 1 ]; then echo "Usage: $0 " exit 1 fi PLATFORM="$1" CONTAINER="megacmd-${PLATFORM}-build-env" cleanup() { echo "Cleaning up container..." docker rm -f $CONTAINER > /dev/null 2>&1 || true } trap cleanup EXIT echo "Starting build for '${PLATFORM}'..." BUILDER_PARAMETER="${DOCKER_CUSTOM_BUILDER:+--builder $DOCKER_CUSTOM_BUILDER --load}" MEGACMD_FOLDER="${SOURCE_PATH:-${SCRIPT_DIR}/../..}" OUTPUT_FOLDER="${OUTPUT_PATH:-${SCRIPT_DIR}/packages}" docker buildx build $BUILDER_PARAMETER -t $CONTAINER \ -f "$SCRIPT_DIR/dockerfile/synology-cross-build.dockerfile" \ "${MEGACMD_FOLDER}" \ --build-arg PLATFORM="$PLATFORM" CONTAINER_ID=$(docker create $CONTAINER) mkdir -p "$OUTPUT_FOLDER" docker cp "${CONTAINER_ID}:/image/${PLATFORM}" "${OUTPUT_FOLDER}"/ echo "Extracted package for '${PLATFORM}'" MEGAcmd-2.5.2_Linux/build/SynologyNAS/toolkit/000077500000000000000000000000001516543156300210565ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/build/SynologyNAS/toolkit/source/000077500000000000000000000000001516543156300223565ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/build/SynologyNAS/toolkit/source/MEGAcmd/000077500000000000000000000000001516543156300235535ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/build/SynologyNAS/toolkit/source/MEGAcmd/INFO.sh000077500000000000000000000014561516543156300246530ustar00rootroot00000000000000#!/bin/bash source /mega/pkgscripts/include/pkg_util.sh package="megacmdpkg" version="2.2.0-0001" displayname="MEGAcmd" maintainer="Mega NZ" arch="$PLATFORM" description="MEGAcmd command line tool. Access your MEGA.nz secure cloud storage account and upload/download files, use its commands in scripts, automatically synchronise folders between your MEGA.nz account and your Synology NAS. Connect to your NAS via ssh or Putty to use the MEGAcmd commands. Run mega-help from the command line for documentation, or see our User Guide online." os_min_ver="7.0-40000" maintainer="Mega Ltd." maintaner_url="https://MEGA.nz" support_url="https://mega.nz/help" helpurl="https://github.com/meganz/MEGAcmd/blob/master/UserGuide.md" thirdparty="yes" ctl_stop="no" [ "$(caller)" != "0 NULL" ] && return 0 pkg_dump_info MEGAcmd-2.5.2_Linux/build/SynologyNAS/toolkit/source/MEGAcmd/PACKAGE_ICON.PNG000066400000000000000000000074551516543156300257770ustar00rootroot00000000000000PNG  IHDR@@iqgAMA a cHRMz&u0`:pQ<bKGD pHYsodtIME :!sgIDATx͛kuvIZi,J+,$$~HDJlebT, qX,b\r(I|XzBc0Di;}O>̫{G=sϹ%ƱpNfij GYl tSf@QՂvω ;`*}sR\q/r횽m3n續,Y`QUER!EU==g/]7Jk8w8Y ((1\տ)~j'}/=/)[̈F``|MʖY1_w/ \!"R2d&?VDzm|빅ɴ.+G`QʨeRT`AS9-<;S3ZMcyhsLfOFxJOW"%-S+OX ?n8@Ǣߎ _myP3ُNI9UAG~bOt̻ڮ}Ag>5taڧ{Er&6a#kae T]x_k>"9cL ZK'[eY?i2!Μc~A֧c?5QPи`5N5-̮`|ؒ rw1 ) Th@R7sndlOI~j{EN3`=QwacwFvgn.%#Ҹ|*`-{E "('+q+eZ TN=:V>pYOkf& x+CATab}0zjX:@XۍVQȓ&G'S֢\ 4n/rZ8?3d7)x*n[+b-A~=/r}X9[@0%3A6膃3EΚnrqW;Ȳ엃g&b ┾2.U8߇\1L~)\|o!ۿG" 8/c9h9<[իa t."^ÂkYMM`}+][v$%o,v{33#mOu#@X& B=C\ұc 4Xy]S٦M~HES ]IЈۛEi".jA#\`Vzkjµd6r|2⊜Rwlߜ͝-t cjMrmvM3uג}H|xn==RPH]^m\̒T_;-[E˓!W~f27^^Bk:9Q~B@<>΢+PsװS~ڞDN)K!)IV`A(}Ȭ>VƭU15ҠƁdۣc_Cuء2uiLLڷ=DfuQx@ZW@!:=& q֮Gx1uccއȿetOfGqPL cfZGj?&Gl|,k FILݿ{8o#n&׋MU}ʿ獯ŘO$)6j.1CA1zfQ&/d_0f|t8:]`W1uߥh:fQwׇa"ı_e(*%verc I DvZq8a xeJ>co^&6=?~c~b|uo/dc_PE^PT)v+*00aMxNBu@B@t|xU9دSQaclϾ(?Iaoo^j=r  Zzc?bUXlHtjj|`׺p;\* MEwk,DO: q+D+"6M+^[Cf)2ku{v>~®SY'8~HI~.n汰/+C>:Zt1y`39PP~ מ9z~e Xh>]!p9>COwX9;AOBV9 S;TJhyHvMUy G ) W`ze2<6q_tUZtY)^Iq[Z,bbK. +!!2)f|iSCr72p 8N建mzHQraZ/q\(}xnr*wA4e(MKȎb_ .-5&Vw)U3w Ci爝$$[SNKEqOvprz,5Sܝ&O*l*eWëj8 w]8 3Wܔzh7c?&i#$Zx܁ul8]K7Q>DUnk"5ZvЭKń,gwSP.жc|} {CLPB$IǾF_P0q8!gkbfc;y HG3!~8NGbAKxUXTXї&Ko?C7qdJ:Ev@Q!hs_(-pI6@(coG?G#m"6?JU+uMI'sjП<;`Pow|6à XWtª}G_Tޑ$>~'od~WE׋],ź՘j@,6G|5OLb-i:uIdǞ+9]tbE2A?a jqO5' _=䩾57<c\|Z%tEXtdate:create2020-10-13T03:53:24+00:00VUz%tEXtdate:modify2020-10-08T22:35:36+00:00x=IENDB`MEGAcmd-2.5.2_Linux/build/SynologyNAS/toolkit/source/MEGAcmd/PACKAGE_ICON_256.PNG000077500000000000000000000573001516543156300263700ustar00rootroot00000000000000PNG  IHDR\rfsRGBgAMA a pHYsod^UIDATx^ eEu뽞pdqIfM$&Il1cQQ0n$vw4@dgfTsΩ}ṷs~0K 5꘵a6!2G?}؆aE8kA?Șb9ƌ7L1f5.\(`30<nպjss'yA0l>A_k7ѐ 7j Â(otDHqC&;![zy{gdz{4 Ǐ_]O[wҦuipsU\B77{ ө/q7_vfǗzmoٱ4) Ky3 W>' 2EtCO2ޥ3qfaw܆% `_jrl.\}M<7[>a'd_zcĦԆNپMG摥 `y۴|/Nox9Oʱf7@45n:z(N*KSpVu/ޱKC6}MŁ_*8O%17^:@doׅyXm:_">HÒc&ݸ{ץo#K=91/p߄B xu`7@4"%”J\$:7^b2/莜f1ǃ\śvĬXf i߀j7K?qLwYX/y]莜%&%B <,:f*(n摿?\Υ̂ K6OwڅS4^iżn`7@4"%”J\$:7^b2/莜mw S|{~'-Q ckmHOCu.7qLwYX/y]莜%&%B <,:f*yEqM{~E#/}Nm#^btVi`Q/mw/o~fןs>r}HKdX2LBz]NK ﲰ-_9KMX]RX$O"oֳ:_R]wp{~[N40&mcΟ\a3=KM[T\#QD}0}ݒޓtzT`엾+~`No~d;xɍ{Z(-4[&N?srb! T<0\-'d˪[y!= % C2.{F<㤺\/>7ȵdHuŃMF񔑉_;7YL/,l8铫IgsK(k2:XU}S!zX/y]iSt—sz| ?X,TRu-'՝Oe% `r|K Gnh=z{IX, ^'?oMا.-y! SSPz;"oׅF:ENw *|`=wruBU(U}S,>r_sc{N'9#sO8C&i .$NL(iebp=X!֌Y+ԣ<TûDcqN$ZuX)8zYG_ `_OK W_RvLKiTNeT`̗dtGatjC=lc%5)U}S"x隢qCf8 ]|?=j|!GuppJBBX.yhDTXKuC +5?5ͱ?ed3-wimf(&MlYRDe`K;Q&eW?%6UH64\!QdSjcO3/< s% ]$ -7ap_ ^#!18 _»$"o=A (1{t?S0/î=ydE~D7-gir|e0)44Q",ch7Fاsfkjx%Ec'.o@Y `rl?7vJGG<<2_8@.Yv ck>sRc; 2Ϳz˺7G2\Ӊ'; cqH%#4R'z(G@%@+͝@ILxLc˺/x**;cSf .X6 ydt" O*kT%@k,:DkZ@.X!ʈye/}Y9\lկR:t$W=ZLyPX"{Y!f)pR]%"5ER$ѫ&ȵdHuͣlya^'x59"˝4Df zdb$záŐ&TS% ᚌN,P#42Zo%>oQp*Gr~ER'LJT<(v\Kd,,\W8yDz ꚇ ьpכּ/D5u.\y'!h'-]IJIHC!KMϧr-R+JӿY h},:U]Sۣ>ybc|̼L[֞<  ?ƔIɁ^&v ף%j͘A=ZO>Kzzi `r|ò ǎ_Yv2U=晃D'ywHAY wG c*=2(PI; !#Wp#=>^[co_r.!]V偝BX/r>H4nDguȻ+ %s=,,̸;xk2|.H=dUGM|fŜL~9jbC(k=V]H}/0Ig]zd3NvUS&v8ѪD'1$$:Qy=ƭ9.o|%bɉC5]l ! ԥGK.=2.$E&"?J]ꊇx<]xk# cmu't@͒2|??Ɍ>p7CK>JAN'$ ce2#ƈ<t# cCVpsx% =2ьog5\8qy<&>RKD*:8. DO<r-R]Po5'NA:)U-@:i qØ9*J `rlota3:2HHtTIJIHu#酄OZ2 M9题H:, Ze,,#o[@l<I2)9RI5r-DpCR^᤺%KDkHWMk,ꚇY ɕ0k7'oX\OdG&MBg<Z YjH~>k,\Qhꚇ \XYM 69 W= /%_ٳn1כbJr0aݳwoÏh) ["F!'RqMF/y{@wҗ" Bt}_/{/ލ֘Ɗ.)\6˴*C qSDwN~KUHvfv>3sK;Pkز?ГpСq XYeſa/> ރR׿o)*Se5U'Pj*KSB v@0TyX}0}20##fg,{SLch;?ov\9njۤr qקNY e_GzlmMŮ?%Ar\adSo3F:@"87wo7澇Hh;| ˋ.]#AHR/)`}Xz+~xZ3ی>GݎFϥMq{Hz 3nЩ/}Rp+[.LY*k?]7IOoid,ӟړ͎:l^uCЈ3ނ@5p6V-2^PȫZgy(1q:,k<>"0D•1KyGɯ5cM `ӊ?MWhb!pon0)#]i3dHWv_hǪǐԃr}8 ~u;!,ӟo$DFYh=FqF^8Vw7>-f)$`ȫubQŃO @/p?O$0)0 uܮQSK ꚇK2^?;7JXۧQS.cuiDv4lh?9nm Kyɮ `O tG{#5/gygR(H&VA,̆ Ŗ=V @9 4ʏn`rAϧRk,7uw_u+q WQyU1՝ךqqoP~kh`|ܡ&P~Mθ!f)pR]%"5WL( 8 Y4 {{pǝNq%084Q.'EKR'2^PȫZg6=.ڍS(*I>Wo,%5H/]"R]@+OF:Cfa;u5yW ;!!QOoZqs%}*גY չ]<?k։,zO#]![6F3bFGLKb6i9w$`&K=B=ZO>Kzh>c9x35T/ o467ߐ|p<0wo>?T1W{(O#R'uO4P '~qh" szAǽ \6 X`_}ȘgS]`n kHw>,NkΐY wϻ ±dbv5A vXa'tW4ʪOB7N W{(wO8 yig[|ڎiq-$㛀9S]h2Y'W8yDz ^|mq(̏vË7F~O+ejo~!Er{:XOZ2 :Wh<6\2h;ř&4}q>& ycIY]1q1KyG.y8l^u'l2 B3R$F ]LN =p=It,JP֋br=jx]ӄwڎiê>IƑqL@DW;ga`iTy@S3xyU>j#ӖtǐL/I8MZ,ɮ 4w :2/vyrTǔ5b.QA mvX_o1` ofqxq1O@]zd1K> YUH9P.w,qC~Lu}=h/q7 [lܑGFfҘ#o;1F].>${E^e1;QI'BODbV~8dlC8,1JE>>wgIXG7J2G@U(RX{L\6PH{ۏ8 X`+DŽ7>uc"(ceŋqFꚇC+R>PăUwC2.퇸7+?'pp'eO8 x= :*@0 @$$P8{/iN7 \nb[o3=DPȫZgy(1R05 ]h`ňyR'LJZo1~u?Gb?O+}yLܓ <_2{?o)LcMABiϾsCfٿ78}d^sL׌X/bkJ !pƬ77X1bԉ)##Mk?wFǤ{ʧO#!x:*)ŎDFl2t]B6=w&~yn(ӥm󦣱TGVW<<͝4=bf&lͧc |&p$_|oano9}:ۛNzdy-i.vXNʸGm,L{~~&wy^Sܱ?V3s@S꺇GܲGS04PpH;ѸYbCq`~&n?9gY&5Hg,GWOn`7Mc xꌕIsiI7a>k,{w,;tQx mhhI൦;HY|7ӑJN}objIu#KxT<#5+}hzn}o-7?>f^IbʵdHu(q@c})'b}ǜ.X-$($7?ۋj~}~ɼ÷ 1KѤ%KDk^4 9"Q$>k?8P,/%OgyF_j_" W8 z5}Dciݛ1!~_]'P-'vXu^brDΫJMѸy#wKټ um2kgyU;6mj8[C9 _琤~S,?/UvC2.{f~f"HKq7M{ ? ؛ONJ8'swubeU Jݣ`';%C'!хR&!Z^·>Y abAk'w?۟[ '' iyH7yU15%WfK8L@#I2)9RI5ZBRsbFz uv/^3cu ,cֆ4ʪ@G0[m&Mm}IHwIIJIէrDR+&M7up7wwh2:2tKxW8S]Pbye.\l/Ї +F̓:1eRrAkҵtX}% ^2 k?t|q?v6K߁+21z`T㻰 t4L,M?X}*G/չ"c; 촓F[ކ7}yXCɧxC%zkfV{6p}c^Zubʤ@I[/  3>>U.V/_˛}o?UxW 3uC[kF:,}S%UL?8|M{ˏP;G&:1ARcWm*rTǔ5bwvz: ~wGAW3k.+ɷ O#]![6y0W) խFq/A P+M?lQhlb5D/q1z9[-cCo]~_n hSLlooq+Y@gO_ 6#$cq1~3r|gGb|37LԵ[K7P&%G*iF%!&W i3dHuţ/O޶R%]̎5'NA:)U-'5IubeU JT?d9W_}3}- l<}_~#e~!dy#lR=w`;~SrR]`=yi_CC4vrڎib#$Vm5 bkJ L}xC?"S^d>UĻDΫJM=f%=(d цc5\Jk2!,Z,ɮ `Ě2'IyhqmkQS.ֈ Du:{?"ӯWB6@bh5]uVAGз:։%Z&͡p%C%w3CQ*#b8'G/޴>C ;\>;1ˣ ucL%"#9P<2z?p磐18ULxGzlG@Aaœwbe2#չ7ɷ-cmTjt@ƛD/qG6gW+wq GZDRE^eG@ƩtQx$d3B y(1LyoH'cbqH%#4RGlo\/y ^s$D^bI-P'VV=z׽ǴO,W{R 6@qdNBK\eGF-:(^UWo"5-y NŌGIeWx}ꚇC+bpJ ]~ nb<S&%G*iF%2^ loBW8y(:z ^5AF%@kZgfubeU Jݣi_Fq橛]iIHwIIJIH#SbY{Pk2:!bߔkI<_r2#@LuԈ!>M0Y:Nl+F̓:1eRrAkZ"ceOEj(!y(:z >O6;^sf߄%lo\+29%53Dpއ >)9d$ĕX6 ph1\P7~ʹj]=xhqPp'S5'gfw5%gU[n,Guej,+>WþjUno?Zl[& r]&%Jz5\2`ȫCqcM G_)v6>6پB^ᠮhh5L%"#9P<2zoWM{{LclԹQf?moGlXk;)ʹ/~oԺ7y!#ϿFkJS2;ϗԩ̀9(=,4d*%J|m1߁L`aJt yKTf@^#^"_ѸY]ROcrǛƲ&O`ua?4:lCUo* {{7Ž~9<4_:BIt]D`vpKH=d$]*җK&%`2Cz+~p.z}Âl`&%U?/UyX" {/* tw 4>|*J:R5{(LNBcz3 Nh")(~~H1@F(~ًAB GxSGzd\yD~>O&41隸>[ ɸrCw Le߰ m$]*z I{@wҗce^_:*=:.z}Âl`&%U?/UyX" {/*w/TO#X3u:~\L.% =2< Q[rD-h7[Įp WoR꺇'KeZ擄]Mo۵J؍Rq0EQy*<2Oŋ꺇'yIu# q(fk,\QhꚇWC A+cp=X"{Y!f)pR]%"5ER$ѫ&ȵdHuC,ͅ@Hg,:,}޳N `笇Lϴ^IӮ- rC>GtÒbRG*w\KfTGM=xhqPp'S5g8G4oyhDxÛ̧))=P')L9Od,Wk,z}]"PW6Zu#sH5A05}uVA]bIvUS&ǻWLЗ73{Ls:mD5Pyx3(񃘧 /GuLX#25E/!)Sqy.*kZ,*G>tlh2G{x?A3}njIR`wA?8rdϓLTX!։Fꚇ3@V*k,G _KDFr.=B#uqT=اs8&# cqzX5!ϝb)^zӺҘZFq>fpJ _bOTH64\!QD'3=UCd_N =ԩ̀y=,?i.tiStCٗ`s=a{C۴83} ^y0%BO:qA5'~e fud,cĨG@ƚG+n>״~׏%w4\u瘢R=QZqjz+^uw_t%$$P*b +GyIu TfM+{.3Ń$y,״}rd׎X7q0]•z1x"t(^U=T=,-@.S&%G*iF%@sCR^᤺ R]&3̜z}g&ˇG9rdsPvh'qzubeU JC3$$ꑉeGF'@k,^Vz+ ¼N;{c0m.KR'2^PȫZgy(1q(NLyPX"{Y!f)pR]%"5Ei}|/rPq÷^~o" S;KO zdb$záŐ&TS%^? 7k@3`vzlY? v8X"{Y@"jq  ȓ:1eRrAkZ"ceJIu#KxT<}|\3}6K2Jwwszs._IAV>} U5-VVCsahX6 gsԉ!:q@Wk]=xԳ?f(wl\~Mktds.#YSzO#8#@D][Ngw=A2)9P\" yN 4xP< )K꼪L67bZ/uy]wïpZ;xccr]UPtlhZ,*N5Ah (iZ,`yȘg<:uqĬ.=,sy̒VS?I8w7O=#GRo*F>OTg]zd3NvUS&vU=N6# c9HHtx{:[+8jz+^uw5)ϔ/83X`2Lw ָ*>Otsg=MwD.bГN_ ft2nG9zO@0P&`h›&T:(ּxG@ƚ#-/&!K\Azdyv=x'gW GΥ9m>}@jډJu3%KDkH/O#!x:*);:~L= Uaek|ahcw"l}PJtЇA0%s{XdUy` "7 A DIzVK_K]Rǻ7cf) #pAR>U.+Xܘ摉Dd$GFϠUr7br]UPphq۸ll#˟LdpҔ娎)k\ԃ%$e*52%ZuC%ZçxC ҩoSFןbIdϸ Di◽$$@zoB "OyduSu MW?+,׶=r̎9wņ p '=Eb@59{C첌QFbd>ۧsfkxDDEôL0iC:usΟS :c'  evJujB/b2B-_H.A/@nY sfj_>L]A`MSpg'Hau'՟n.lwB"=,|˗`bftCFItjsbK9 o2%mꉞP\u9giK¢aQ|ћ̣zxc# -'N2))yhqƃDW=2lR]D$y@Sy8N샛Mok)vXťS'-;~]1չR#W8NEԉ)#4]#Kd/ W:,Nk^»D聛kZ/}IX, 䆙w_7#{h 'ofYtm]IJIHC!KMϧr-R+Jl Kf3K_NSG&&>?Yu]^/$ !vYMg>‚[F!'RqMF/y{@wҗ" Bt}_/{/Xkf޹ b'grQLMcF`#LOT5cᗍ.$D%Il4-&HTMhD*:~娎)k\ݥ>`_&ӺPic}'O\>pa$^"T/p ,3A$:. =J:xk2|.H=d$=TjbKdu^_:43Y3s-5K@ħ&~C\ÖHx]JW?%6UH64\!QdSjcO|3WE|seipӚ .s]QDٟiD/m'RqCO"K&xT+WVEkf.sKp&:j[ k.4'Vtd.^װÔ<,Q0Kl<,e-_b9 ۦ-_"=9 }ezV3~Ưbi~FQ<.0:n兇[Fct@. % ݑDduDHa}%?X,T;9 {Kŵ03o%:4tɛ~60a}d.^%”J\$:7^b2/莜mw/ƞc?b;cf%j4̒/DŽ_2 W<1_:OdoׅyXm:_">HÒcR=V[|b^>Yᩍ{W4L J /R\#QD}ڜGF2/莜mw/8*5SRL_􅽹4 ;mdNlx Sv?u5K5RE'ѩyKl>-l2%ۄeTKim73'T\,M[م9ck_puOP #5|IBX/v+;Ha}O"oֳ:_RmNX~yMQ\n3u+-7&yU'|G~~cdy [yFޓtzT`엾fѼ]ţ_jZWo-fu%慥 `yuVqk[4LdG AM AJ D>Xa9m7Zy{6WyX&'/[Śxh㩍$s INFO cp -av INFO ${PKG_DIR} ### Create the final spk mkdir -p "/image/${PLATFORM}" pkg_make_spk ${PKG_DIR} "/image/${PLATFORM}" $(__get_spk_name get_platform_family INFO) popd } create_inner_tarball create_spk MEGAcmd-2.5.2_Linux/build/SynologyNAS/toolkit/source/MEGAcmd/conf/000077500000000000000000000000001516543156300245005ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/build/SynologyNAS/toolkit/source/MEGAcmd/conf/privilege000066400000000000000000000000711516543156300264070ustar00rootroot00000000000000{ "defaults": { "run-as": "package" } } MEGAcmd-2.5.2_Linux/build/SynologyNAS/toolkit/source/MEGAcmd/conf/resource000066400000000000000000000032261516543156300262550ustar00rootroot00000000000000{ "usr-local-linker": { "bin": [ "mega-attr", "mega-backup", "mega-cancel", "mega-cat", "mega-cd", "mega-cmd", "mega-cmd-server", "mega-confirm", "mega-confirmcancel", "mega-cp", "mega-debug", "mega-deleteversions", "mega-df", "mega-du", "mega-errorcode", "mega-exclude", "mega-exec", "mega-export", "mega-find", "mega-ftp", "mega-get", "mega-graphics", "mega-help", "mega-https", "mega-import", "mega-invite", "mega-ipc", "mega-killsession", "mega-lcd", "mega-log", "mega-login", "mega-logout", "mega-lpwd", "mega-ls", "mega-mediainfo", "mega-mkdir", "mega-mount", "mega-mv", "mega-passwd", "mega-permissions", "mega-preview", "mega-proxy", "mega-put", "mega-pwd", "mega-quit", "mega-reload", "mega-rm", "mega-session", "mega-share", "mega-showpcr", "mega-signup", "mega-speedlimit", "mega-sync", "mega-thumbnail", "mega-transfers", "mega-tree", "mega-userattr", "mega-users", "mega-version", "mega-webdav", "mega-whoami" ] } } MEGAcmd-2.5.2_Linux/build/SynologyNAS/toolkit/source/MEGAcmd/scripts/000077500000000000000000000000001516543156300252425ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/build/SynologyNAS/toolkit/source/MEGAcmd/scripts/postinst000077500000000000000000000000231516543156300270460ustar00rootroot00000000000000#!/bin/sh exit 0 MEGAcmd-2.5.2_Linux/build/SynologyNAS/toolkit/source/MEGAcmd/scripts/postuninst000077500000000000000000000000231516543156300274110ustar00rootroot00000000000000#!/bin/sh exit 0 MEGAcmd-2.5.2_Linux/build/SynologyNAS/toolkit/source/MEGAcmd/scripts/postupgrade000077500000000000000000000000231516543156300275200ustar00rootroot00000000000000#!/bin/sh exit 0 MEGAcmd-2.5.2_Linux/build/SynologyNAS/toolkit/source/MEGAcmd/scripts/preinst000077500000000000000000000000231516543156300266470ustar00rootroot00000000000000#!/bin/sh exit 0 MEGAcmd-2.5.2_Linux/build/SynologyNAS/toolkit/source/MEGAcmd/scripts/preuninst000077500000000000000000000000231516543156300272120ustar00rootroot00000000000000#!/bin/sh exit 0 MEGAcmd-2.5.2_Linux/build/SynologyNAS/toolkit/source/MEGAcmd/scripts/preupgrade000077500000000000000000000000231516543156300273210ustar00rootroot00000000000000#!/bin/sh exit 0 MEGAcmd-2.5.2_Linux/build/SynologyNAS/toolkit/source/MEGAcmd/scripts/start-stop-status000077500000000000000000000000231516543156300306240ustar00rootroot00000000000000#!/bin/sh exit 0 MEGAcmd-2.5.2_Linux/build/clone_vcpkg_from_baseline.sh000077500000000000000000000010501516543156300227160ustar00rootroot00000000000000#!/bin/bash set -e #If a dependency required an older commit (older than baseline), set it here: VCPKG_OLDEST_COMMIT=c6d3ab273572019ea0d6f6688e1163143190d326 if [ -z ${VCPKG_OLDEST_COMMIT+x} ]; then commit=$(grep '."builtin-baseline"' vcpkg.json | awk -F '"' '{print $(NF-1)}') else commit="${VCPKG_OLDEST_COMMIT}" fi commit_date=$(curl -s "https://api.github.com/repos/microsoft/vcpkg/commits/$commit" | grep '"date"' | head -n 1 | awk -F '"' '{print $(NF-1)}') git clone --shallow-since $commit_date https://github.com/microsoft/vcpkg $@ MEGAcmd-2.5.2_Linux/build/cmake/000077500000000000000000000000001516543156300162645ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/build/cmake/modules/000077500000000000000000000000001516543156300177345ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/build/cmake/modules/megacmd_configuration.cmake000066400000000000000000000014701516543156300252640ustar00rootroot00000000000000# Define debug symbols covering all platforms. add_compile_definitions( $<$:DEBUG> $<$:_DEBUG> ) # Build the project with C++17 set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) if (MSVC) # https://gitlab.kitware.com/cmake/cmake/-/issues/18837 add_compile_options(/Zc:__cplusplus) # Enable updated __cplusplus macro # Enable build with multiple processes. add_compile_options(/MP) # Create a separated PDB file with debug symbols. add_compile_options($<$:/Zi>) else() include(CheckIncludeFile) include(CheckFunctionExists) check_include_file(inttypes.h HAVE_INTTYPES_H) check_include_file(dirent.h HAVE_DIRENT_H) check_include_file(glob.h HAVE_GLOB_H) check_function_exists(aio_write, HAVE_AIO_RT) endif() MEGAcmd-2.5.2_Linux/build/cmake/modules/megacmd_libraries.cmake000066400000000000000000000014631516543156300243730ustar00rootroot00000000000000macro(load_megacmdserver_libraries) if(VCPKG_ROOT) if(USE_PCRE) find_package(unofficial-pcre REQUIRED) target_link_libraries(LMegacmdServer PRIVATE unofficial::pcre::pcrecpp) set(USE_PCRE 1) endif() if (ENABLE_MEGACMD_TESTS) find_package(GTest CONFIG REQUIRED) target_link_libraries(LMegacmdTestsCommon PUBLIC GTest::gtest GTest::gmock) endif() else() # No VCPKG usage. Use pkg-config find_package(PkgConfig REQUIRED) # For libraries loaded using pkg-config if(USE_PCRE) #TODO: UNTESTED! pkg_check_modules(pcre REQUIRED IMPORTED_TARGET libpcre) target_link_libraries(LMegacmdServer PRIVATE PkgConfig::pcre) set(USE_PCRE 1) endif() endif() endmacro() MEGAcmd-2.5.2_Linux/build/cmake/modules/megacmd_options.cmake000066400000000000000000000051451516543156300241130ustar00rootroot00000000000000option(USE_PCRE "Used to provide support for pcre" ON) option(FULL_REQS "Fail compilation when some requirement is not met" ON) if(USE_PCRE) add_compile_definitions(USE_PCRE) #this one is no longer added to target_compile_definitions(SDKlib, hence some cpps not using config.h will lack this, causing the full reqs to fail # TODO: remove once PCRE optionality is removed endif() if(FULL_REQS) #On by default in the SDK, but let's enforce anyway option(USE_PDFIUM "Used to create previews/thumbnails for PDF files" ON) option(USE_FFMPEG "Used to create previews/thumbnails for video files" ON) option(ENABLE_MEDIA_FILE_METADATA "Used to determine media properties and set those as node attributes" ON) option(USE_MEDIAINFO "[DEPRECATED] Used to determine media properties and set those as node attributes" ON) #make compilation fail in case those are not fully available: add_compile_definitions( REQUIRE_HAVE_FFMPEG REQUIRE_HAVE_PDFIUM REQUIRE_USE_MEDIAINFO REQUIRE_HAVE_LIBUV REQUIRE_USE_PCRE ) else() option(USE_PDFIUM "Used to create previews/thumbnails for PDF files" OFF) option(USE_FFMPEG "Used to create previews/thumbnails for video files" OFF) option(ENABLE_MEDIA_FILE_METADATA "Used to determine media properties and set those as node attributes" OFF) option(USE_MEDIAINFO "[DEPRECATED] Used to determine media properties and set those as node attributes" OFF) endif() if (CMAKE_BUILD_TYPE STREQUAL "Debug" ) option(ENABLE_MEGACMD_TESTS "Enable MEGAcmd testing code" ON) else() option(ENABLE_MEGACMD_TESTS "Enable MEGAcmd testing code" OFF) endif() if(ENABLE_MEGACMD_TESTS) add_compile_definitions(MEGACMD_TESTING_CODE=1) endif() #Override SDK's options: option(ENABLE_ISOLATED_GFX "Turns on isolated GFX processor" OFF) option(ENABLE_SDKLIB_WERROR "Enable warnings as errors" OFF) option(USE_LIBUV "Includes the library and turns on internal web and ftp server functionality" ON) if(WIN32) option(WITH_FUSE "Build with FUSE support." ON) elseif(UNIX AND NOT APPLE) execute_process( COMMAND uname -m OUTPUT_VARIABLE SYSTEM_ARCHITECTURE OUTPUT_STRIP_TRAILING_WHITESPACE ) message(STATUS "System Architecture: <${SYSTEM_ARCHITECTURE}>") if(SYSTEM_ARCHITECTURE MATCHES "^(i[3-6]86|x86|armhf|armv7l)$") set(IS_32_BIT ON) else() set(IS_32_BIT OFF) endif() if(NOT IS_32_BIT) message(STATUS "Configuring with FUSE support") option(WITH_FUSE "Build with FUSE support." ON) endif() endif() if(WITH_FUSE) add_compile_definitions(WITH_FUSE=1) endif() MEGAcmd-2.5.2_Linux/build/cmake/modules/megacmd_utility_functions.cmake000066400000000000000000000024461516543156300262140ustar00rootroot00000000000000function(header_from_source) set(SOURCE ${ARGV1}) string(REPLACE ".cpp" ".h" header_ ${SOURCE}) if(EXISTS ${header_}) set(${ARGV0} ${header_} PARENT_SCOPE) endif() endfunction() function(add_source_and_corresponding_header_to_target) set(options "") set(oneValueArgs "") set(multiValueArgs FLAG INTERFACE PUBLIC PRIVATE) set(TARGET ${ARGV0}) cmake_parse_arguments(PARSE_ARGV 1 "target_sources_conditional" "${options}" "${oneValueArgs}" "${multiValueArgs}" ) foreach(_source ${target_sources_conditional_PUBLIC}) header_from_source(header ${_source}) target_sources(${TARGET} PUBLIC ${_source} ${header}) endforeach() foreach(_source ${target_sources_conditional_PRIVATE}) header_from_source(header ${_source}) target_sources(${TARGET} PRIVATE ${_source} ${header}) endforeach() endfunction() function(generate_src_file_list base_folder OUTPUT_VAR) set(SRC_LIST "") file(GLOB_RECURSE ALL_FILES "${base_folder}/*.cpp" "${base_folder}/*.h" ) foreach(FILE_PATH ${ALL_FILES}) get_filename_component(FILENAME ${FILE_PATH} NAME) list(APPEND SRC_LIST "\"${FILENAME}\"") endforeach() set(${OUTPUT_VAR} ${SRC_LIST} PARENT_SCOPE) endfunction() MEGAcmd-2.5.2_Linux/build/create_tarball.sh000077500000000000000000000076711516543156300205220ustar00rootroot00000000000000#!/bin/bash -x ## # @file src/build/create_tarball.sh # @brief Generates megacmd tarballs and compilation scripts # # (c) 2013-2014 by Mega Limited, Auckland, New Zealand # # This file is part of the MEGAcmd. # # MEGAcmd 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. # # @copyright Simplified (2-clause) BSD License. # # You should have received a copy of the license along with this # program. ## set -euo pipefail IFS=$'\n\t' BASEPATH=$(pwd)/../ # get current version megacmd_VERSION=$(cat $BASEPATH/CMakeLists.txt | grep -Po "MEGACMD_.*_VERSION [0-9]*"| awk '{print $2}' | paste -sd '.') export megacmd_NAME=megacmd-$megacmd_VERSION rm -rf $megacmd_NAME.tar.gz rm -rf $megacmd_NAME echo "megacmd version: $megacmd_VERSION" # delete previously generated files rm -fr megacmd/megacmd*.dsc # fix version number in template files and copy to appropriate directories sed -e "s/megacmd_VERSION/$megacmd_VERSION/g" templates/megacmd/megacmd.spec | sed "s#^ *##g" > megacmd/megacmd.spec sed -e "s/megacmd_VERSION/$megacmd_VERSION/g" templates/megacmd/megacmd.dsc > megacmd/megacmd.dsc sed -e "s/megacmd_VERSION/$megacmd_VERSION/g" templates/megacmd/PKGBUILD > megacmd/PKGBUILD for dscFile in `find templates/megacmd/ -name megacmd-xUbuntu_* -o -name megacmd-Debian_* -o -name megacmd-Raspbian_*`; do sed -e "s/megacmd_VERSION/$megacmd_VERSION/g" "${dscFile}" > megacmd/`basename ${dscFile}` done # Adjustments to remove fuse dependency for 32 bits builds if pwd | grep -E "/(i[3-6]86|x86|armhf|armv7l)/" >/dev/null; then echo "Removing fuse dependency...." sed -i "/libfuse-dev/d" megacmd/debian.control sed -i "s#, fuse##g" megacmd/debian.control sed -i "/fuse/d" megacmd/megacmd.spec sed -i "s#, libfuse-dev##g" megacmd/megacmd.dsc sed -i "s# 'fuse2'##g" megacmd/PKGBUILD fi # read the last generated ChangeLog version version_file="version" if [ -s "$version_file" ]; then last_version=$(cat "$version_file") else last_version="none" fi if [ "$last_version" != "$megacmd_VERSION" ]; then # add RPM ChangeLog entry changelog="megacmd/megacmd.changes" changelogold="megacmd/megacmd.changes.old" if [ -f $changelog ]; then mv $changelog $changelogold fi ./generate_rpm_changelog_entry.sh $megacmd_VERSION $BASEPATH/src/megacmdversion.h > $changelog #TODO: read this from somewhere if [ -f $changelogold ]; then cat $changelogold >> $changelog rm $changelogold fi # add DEB ChangeLog entry changelog="megacmd/debian.changelog" changelogold="megacmd/debian.changelog.old" if [ -f $changelog ]; then mv $changelog $changelogold fi ./generate_deb_changelog_entry.sh $megacmd_VERSION $BASEPATH/src/megacmdversion.h > $changelog #TODO: read this from somewhere if [ -f $changelogold ]; then cat $changelogold >> $changelog rm $changelogold fi # update version file echo $megacmd_VERSION > $version_file fi # create archive mkdir $megacmd_NAME ln -s ../megacmd/megacmd.spec $megacmd_NAME/megacmd.spec ln -s ../megacmd/debian.postinst $megacmd_NAME/debian.postinst ln -s ../megacmd/debian.prerm $megacmd_NAME/debian.prerm ln -s ../megacmd/debian.postrm $megacmd_NAME/debian.postrm ln -s ../megacmd/debian.copyright $megacmd_NAME/debian.copyright for i in $BASEPATH/{src,sdk,tests,CMakeLists.txt,vcpkg.json}; do ln -s $i $megacmd_NAME/ done mkdir $megacmd_NAME/build ln -sf $BASEPATH/build/cmake $megacmd_NAME/build/ tar czfh $megacmd_NAME.tar.gz --exclude-vcs $megacmd_NAME rm -rf $megacmd_NAME # delete any previous archive rm -fr megacmd/megacmd_*.tar.gz # transform arch name, to satisfy Debian requirements mv $megacmd_NAME.tar.gz megacmd/megacmd_$megacmd_VERSION.tar.gz #get md5sum and replace in PKGBUILD MD5SUM=`md5sum megacmd/megacmd_$megacmd_VERSION.tar.gz | awk '{print $1}'` sed "s/MD5SUM/$MD5SUM/g" -i megacmd/PKGBUILD ###### ###### MEGAcmd-2.5.2_Linux/build/full_build_process.cmd000066400000000000000000000067641516543156300215650ustar00rootroot00000000000000@echo off IF "%1%" EQU "-help" ( goto Usage ) SET MEGA_ARCH=32/64 SET MEGA_SKIP_32_BIT_BUILD=false SET MEGA_SIGN=sign SET MEGA_CORES=0 SET MEGA_VERSION_SUFFIX= IF NOT "%1" == "" ( SET MEGA_ARCH=%1 SET MEGA_SIGN=%2 SET MEGA_CORES=%3 SET MEGA_VERSION_SUFFIX=%4 :: CHECK NUMBER OF ARGUMENTS IF "%3" == "" ( echo "Error: too few arguments" goto Usage ) IF NOT "%5" == "" ( echo "Error: too many arguments" goto Usage ) ) IF [%MEGA_WIN_KITVER%]==[] ( SET MEGA_WIN_KITVER=10.0.22621.0 ) :: CHECK ARCHITECTURE IF "%MEGA_ARCH%" EQU "64" ( echo "Info: Building x64 only" SET MEGA_SKIP_32_BIT_BUILD=true ) ELSE ( IF "%MEGA_ARCH%" EQU "32/64" ( echo "Info: Building both x64 and x86" ) ELSE ( echo "Please add the architecture as first parameter: 64 or 32/64" goto Usage ) ) :: CHECK SIGN IF "%MEGA_SIGN%" EQU "sign" ( echo "Info: Signed installer(s) will be generated" ) ELSE ( IF "%MEGA_SIGN%" EQU "nosign" ( echo "Info: Unsigned installer(s) will be generated" ) ELSE ( echo "Please add a correct sign argument: sign or nosign" goto Usage ) ) :: CHECK CORES SET "VALID_CORES=1" IF %MEGA_CORES% LSS 0 ( SET "VALID_CORES=0" ) IF %MEGA_CORES% GTR 16 ( SET "VALID_CORES=0" ) IF %MEGA_CORES% EQU 0 ( FOR /f "tokens=2 delims==" %%f IN ('wmic cpu get NumberOfLogicalProcessors /value ^| find "="') DO SET MEGA_CORES=%%f ) IF %VALID_CORES% EQU 0 ( echo "Please add a correct core argument: 1 to 16, or 0 for default value" goto Usage ) echo "Info: CORES SET to %MEGA_CORES%" REM Clean up any previous leftovers IF EXIST built32 ( rmdir /s /q built32 ) IF EXIST sign32 ( rmdir /s /q sign32 ) IF EXIST built64 ( rmdir /s /q built64 ) IF EXIST sign64 ( rmdir /s /q sign64 ) IF [%SKIP_BUILD_PRODUCTS%]==[] ( IF EXIST build-x64-windows-mega ( rmdir /s /q build-x64-windows-mega ) IF EXIST build-x86-windows-mega ( rmdir /s /q build-x86-windows-mega ) ) IF [%SKIP_BUILD_PRODUCTS%]==[] ( echo calling production_build.cmd call production_build.cmd || exit 1 /b ) echo calling gather_built_products.cmd call gather_built_products.cmd || exit 1 /b echo calling make_uninstallers.cmd call make_uninstallers.cmd || exit 1 /b IF "%MEGA_SIGN%" EQU "sign" ( echo time to sign the executables in built32/64 folders REM TODO: here in case of IF "%MEGA_SIGN%" EQU "sign" , the signing would need to take place, replacing the built .exes and .dlls with the signed ones echo gathering signed executables in the built folders call gather_signed_products.cmd || exit 1 /b ) echo calling make_installers.cmd call make_installers.cmd %MEGA_SIGN% || exit 1 /b REM TODO: Pending signing the installers themselves! exit /B :Usage echo "Usage: %~0 [-help] [64|32/64 sign|nosign []]" echo Script building, signing and creating the installer(s) echo It can take 0, 1, 3 or 4 arguments: echo - -help: this message echo - 0 arguments: use these settings: 32/64 sign 1 echo - Architecture : 64 or 32/64 to build either for 64 bit or both 32 and 64 bit echo - Sign: sign or nosign if the binaries must be signed or not echo - Cores: the number of cores to build the project, or 0 for default value (number of logical cores on the machine) echo - Suffix for installer: The installer will add this suffix to the version. [OPTIONAl] echo MEGA_VCPKGPATH environment variable may be set to the root of the vcpkg cloned (or to be cloned) folder. echo MEGA_WIN_KITVER environment variable can be used to set the Windows sdk to use. Value defaults to "10.0.22621.0". Set to "." to use the Universal Kit exit /B MEGAcmd-2.5.2_Linux/build/gather_built_products.cmd000066400000000000000000000043771516543156300223000ustar00rootroot00000000000000mkdir built64 mkdir sign64 SET BASEMEGACMDPATH=. copy %BASEMEGACMDPATH%\build-x64-windows-mega\RelWithDebInfo\*.dll built64 copy %BASEMEGACMDPATH%\build-x64-windows-mega\RelWithDebInfo\MEGAclient.exe built64 copy %BASEMEGACMDPATH%\build-x64-windows-mega\RelWithDebInfo\MEGAcmdServer.exe built64 copy %BASEMEGACMDPATH%\build-x64-windows-mega\RelWithDebInfo\MEGAcmdShell.exe built64 copy %BASEMEGACMDPATH%\build-x64-windows-mega\RelWithDebInfo\MEGAcmdUpdater.exe built64 copy %BASEMEGACMDPATH%\build-x64-windows-mega\RelWithDebInfo\*.pdb built64 copy %BASEMEGACMDPATH%\build-x64-windows-mega\RelWithDebInfo\*.dll sign64 copy %BASEMEGACMDPATH%\build-x64-windows-mega\RelWithDebInfo\MEGAclient.exe sign64 copy %BASEMEGACMDPATH%\build-x64-windows-mega\RelWithDebInfo\MEGAcmdServer.exe sign64 copy %BASEMEGACMDPATH%\build-x64-windows-mega\RelWithDebInfo\MEGAcmdShell.exe sign64 copy %BASEMEGACMDPATH%\build-x64-windows-mega\RelWithDebInfo\MEGAcmdUpdater.exe sign64 REM copy debug symbols of 3rd parties. dlls were already copied copy %BASEMEGACMDPATH%\build-x64-windows-mega\vcpkg_installed\x64-windows-mega\bin\*.pdb built64 IF "%MEGA_SKIP_32_BIT_BUILD%" == "true" ( GOTO :EOF ) mkdir built32 mkdir sign32 copy %BASEMEGACMDPATH%\build-x86-windows-mega\RelWithDebInfo\*.dll built32 copy %BASEMEGACMDPATH%\build-x86-windows-mega\RelWithDebInfo\MEGAclient.exe built32 copy %BASEMEGACMDPATH%\build-x86-windows-mega\RelWithDebInfo\MEGAcmdServer.exe built32 copy %BASEMEGACMDPATH%\build-x86-windows-mega\RelWithDebInfo\MEGAcmdShell.exe built32 copy %BASEMEGACMDPATH%\build-x86-windows-mega\RelWithDebInfo\MEGAcmdUpdater.exe built32 copy %BASEMEGACMDPATH%\build-x86-windows-mega\RelWithDebInfo\*.pdb built32 copy %BASEMEGACMDPATH%\build-x86-windows-mega\RelWithDebInfo\*.dll sign32 copy %BASEMEGACMDPATH%\build-x86-windows-mega\RelWithDebInfo\MEGAclient.exe sign32 copy %BASEMEGACMDPATH%\build-x86-windows-mega\RelWithDebInfo\MEGAcmdServer.exe sign32 copy %BASEMEGACMDPATH%\build-x86-windows-mega\RelWithDebInfo\MEGAcmdShell.exe sign32 copy %BASEMEGACMDPATH%\build-x86-windows-mega\RelWithDebInfo\MEGAcmdUpdater.exe sign32 REM copy debug symbols of 3rd parties. dlls were already copied copy %BASEMEGACMDPATH%\build-x86-windows-mega\vcpkg_installed\x86-windows-mega\bin\*.pdb built32 REM copy scritps?? MEGAcmd-2.5.2_Linux/build/gather_signed_products.cmd000066400000000000000000000023051516543156300224170ustar00rootroot00000000000000:: CHECK SIGN :Check echo Info: Checking if sources are signed "C:\Program Files (x86)\Windows Kits\10\bin\%MEGA_WIN_KITVER%\x64\signtool.exe" verify /pa sign64/*.exe >nul || goto SleepAndRetry "C:\Program Files (x86)\Windows Kits\10\bin\%MEGA_WIN_KITVER%\x64\signtool.exe" verify /pa sign64/*.dll >nul || goto SleepAndRetry IF NOT "%MEGA_SKIP_32_BIT_BUILD%" == "true" ( "C:\Program Files (x86)\Windows Kits\10\bin\%MEGA_WIN_KITVER%\x64\signtool.exe" verify /pa sign32/*.exe >nul || goto SleepAndRetry "C:\Program Files (x86)\Windows Kits\10\bin\%MEGA_WIN_KITVER%\x64\signtool.exe" verify /pa sign32/*.dll >nul || goto SleepAndRetry ) echo Signed executables found. Will copy them to builtXX folders REM Copying signed installers copy sign64\* built64 copy sign32\* built32 exit /b :SleepAndRetry IF [%MSYSTEM%]==[MINGW64] ( REM pause or set \p freezed mingw bash and there are no builtin mechanism for Batch to sleep, using python for it: echo "you need to place signed executables in sign folders. The signature verification will be retry in 30 seconds" python -c "import time; time.sleep(30)" ) else ( echo You need to place signed executables in sign folders. After that, pause ) goto Check exit 2 /b MEGAcmd-2.5.2_Linux/build/generate_deb_changelog_entry.sh000077500000000000000000000021051516543156300233750ustar00rootroot00000000000000#!/bin/bash ## # @file src/build/generate_changelog_entry.sh # @brief Processes the input file and prints DEB ChangeLog entry # # (c) 2013-2014 by Mega Limited, Auckland, New Zealand # # This file is part of the MEGAcmd. # # MEGAcmd 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. # # @copyright Simplified (2-clause) BSD License. # # You should have received a copy of the license along with this # program. ## if [ "$#" -ne 2 ]; then echo " $0 [version] [input file path]" exit 1 fi in_file="$2" out1=$(printf "#include using namespace std; #include \"$2\" int main() { cout << megacmd::megacmdchangelog << endl; }" | g++ -x c++ - -o /tmp/printChangeLogMcmd && /tmp/printChangeLogMcmd | awk '{print " * "$0}' && rm /tmp/printChangeLogMcmd ) # print ChangeLog entry NOW=$(LANG=C date -R) echo "megacmd ($1) stable; urgency=low" echo "" echo "$out1" | sed 's#\\"#"#g' echo "" echo " -- MEGA Team $NOW" echo "" MEGAcmd-2.5.2_Linux/build/generate_rpm_changelog_entry.sh000077500000000000000000000021561516543156300234470ustar00rootroot00000000000000#!/bin/bash ## # @file src/build/generate_changelog_entry.sh # @brief Processes the input file and prints RPM ChangeLog entry # # (c) 2013-2014 by Mega Limited, Auckland, New Zealand # # This file is part of the MEGAcmd. # # MEGAcmd 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. # # @copyright Simplified (2-clause) BSD License. # # You should have received a copy of the license along with this # program. ## if [ "$#" -ne 2 ]; then echo " $0 [version] [input file path]" exit 1 fi in_file="$2" out1=$(printf "#include using namespace std; #include \"$2\" int main() { cout << megacmd::megacmdchangelog << endl; }" | g++ -x c++ - -o /tmp/printChangeLogMcmd && /tmp/printChangeLogMcmd | awk '{print " * "$0}' && rm /tmp/printChangeLogMcmd ) # print ChangeLog entry NOW=$(LANG=en_us_8859_1;date) echo $NOW - linux@mega.co.nz echo "- Update to version $1:" echo "$out1" | sed 's#\\"#"#g' echo "" echo "-------------------------------------------------------------------" MEGAcmd-2.5.2_Linux/build/get_winfsp.cmd000066400000000000000000000013411516543156300200350ustar00rootroot00000000000000@echo off setlocal set "WINFSP_VERSION=2.1.25156" set "WINFSP_VERSION_SHORT=2.1" set "EXPECTED_HASH=073a70e00f77423e34bed98b86e600def93393ba5822204fac57a29324db9f7a" set "FILENAME=winfsp-installer.msi" set "URL=https://github.com/winfsp/winfsp/releases/download/v%WINFSP_VERSION_SHORT%/winfsp-%WINFSP_VERSION%.msi" echo Downloading: %URL% curl -L -o "%FILENAME%" "%URL%" if errorlevel 1 ( echo ERROR: Download failed exit /b 1 ) for /f %%H in ('sha256sum "%FILENAME%"') do set "ACTUAL_HASH=%%H" if /i "%ACTUAL_HASH%"=="%EXPECTED_HASH%" ( echo Downloaded %FILENAME% correctly ) else ( echo ERROR: Checksum mismatch! echo Expected: %EXPECTED_HASH% echo Actual : %ACTUAL_HASH% exit /b 1 ) endlocal exit /B MEGAcmd-2.5.2_Linux/build/installer/000077500000000000000000000000001516543156300172015ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/build/installer/Info.plist.in000066400000000000000000000020511516543156300215540ustar00rootroot00000000000000 NSPrincipalClass NSApplication CFBundleIconFile app.icns CFBundlePackageType APPL CFBundleShortVersionString @MEGACMD_MAJOR_VERSION@.@MEGACMD_MINOR_VERSION@.@MEGACMD_MICRO_VERSION@ CFBundleSignature ???? CFBundleExecutable MEGAcmd LSUIElement 1 CFBundleIdentifier megacmd.mac CFBundleShortVersionString NSHighResolutionCapable True LSMinimumSystemVersion 10.15 NSAppTransportSecurity NSAllowsArbitraryLoads MEGAcmd-2.5.2_Linux/build/installer/MEGAcmd.sh000077500000000000000000000002761516543156300207420ustar00rootroot00000000000000#!/bin/bash /Applications/MEGAcmd.app/Contents/MacOS/mega-cmd & sleep 2 osascript -e 'tell app "Terminal" to do script "/Applications/MEGAcmd.app/Contents/MacOS/MEGAcmdShell;exit" activate' MEGAcmd-2.5.2_Linux/build/installer/app.icns000066400000000000000000005040221516543156300206420ustar00rootroot00000000000000icnss8mk[zz[--[\zzzz\\ --\{{\is32C  [ց Ӂ΁!祁! -- ++ ## $$떁 t))//7755////)왁('  '      l8mk  SSZZZ[ QQ QQ [[ZZSS il32 K+e݀/ك܀׀փ׀؀ ֑ԋӀчσȇ߁쫇00,,,,,,++++*""**##*)),!!,ݽ  '$   & & '' 996666 ?66 5555 4--4b 4..43**35,,5"  """                     ic07'PNG  IHDR>aiCCPICC Profile8UoT?o\?US[IB*unS6mUo xB ISA$=t@hpS]Ƹ9w>5@WI`]5;V! A'@{N\..ƅG_!7suV$BlW=}i; F)A<.&Xax,38S(b׵*%31l #O-zQvaXOP5o6Zz&⻏^wkI/#&\%x/@{7S މjPh͔&mry>k7=ߪB#@fs_{덱п0-LZ~%Gpˈ{YXf^+_s-T>D@קƸ-9!r[2]3BcnCs?>*ԮeD|%4` :X2 pQSLPRaeyqĘ י5Fit )CdL$o$rpӶb>4+̹F_{Яki+x.+B.{L<۩ =UHcnf<>F ^e||pyv%b:iX'%8Iߔ? rw[vITVQN^dpYI"|#\cz[2M^S0[zIJ/HHȟ- IceU0AB<9g3=眙soɜ{zfQh$/ ")pREv#>?Ҍ)H<,@PtLEfP'kNN"T3aQqX@}9]u)IΉ B[xtdBBt1 Kɲa׵ g<%o0{=f~qCݲc^1?\&D 0!+M.Κ2<˞Ѐ@I=K}e׋NI)*1pcAtԊ)aFBt;n<௱eax{뚔YO)p:0ΠGC)CL܁`:ᛐK \9tbUSbY Bϻ|6Jeb50C2x;\>H4Zj8TdlcBǃ.kqb0ӔԇW^nIO~8ĈCB ?jgQĀXGlp^W{.6ǔ?Ú4_ex]. ;\ ,0Ejww]׀ 0{7"ìV5{1-De٥X|83N]v# ĉ`ヷa~)YsUcO" {~ `PFlj Ow1W-òb%>XTGC'V|1p =X3ך>W1%ZWK,S<<`DzzSyxi!dqd䱘1A*NkBf,9;oŭ+PՉIn;AA&[%NMIDpۑ>OƐKν@#Mz%{^M5{"]ʄ]ٽ=t H7͋Gzd+ZXg\ &9|v^dA2p,'78''}{dFP*Kgw@&Ǟ.NzFC ,D㏍˾/,O7 F+cX Z$_%~I˖@<|dY}JEzv|Bv}I5=8#S?KzL//~ԭJL<`fq)qK9t{7V>?m֯qvA[[k%ŞR3YU/$S|LXxi\@^|F >y.'*3dWhj2x|?K숡̲m2U2p࿗֝L0t4>+ߖNB/$> Op2UN2<R՞&#'e2Z(nZ>,\p<ʲֹX\SyPd=8dB(zױ_5g$x'ohaCx/H;9!sاFk3:h>e;X͸wqɅl! ƚz5TCЗ _D2bW>O LcǞy1nc$B^|*BHE.bG ;Ya&Pb7#C#)K~J` _us()`[AO >۫??)=;og7(Ctƹp!"A,_)n|_@o&]/>ۿk3p䆹Tg_9.秨&:9WqtYuQsAlh`>(z82n*S)!h]{'Z~V{'Ā6ؖ}oDVD2XE*ĐXS[Ю'w^3G^G}6^ x:>ESh$4Iۀ6|iުf+cmX{ ݐ`Q+YG XZߔ930"4h&ish̐_Mf%~16 sq+❊M X@b͒C+t E=uL;O9uDzWcb"O">l>{>2z:*si eBXBt`< 0Gw,^=:ՉR;<,T;jW-[4nIz]7lCk䵠Il#NNNN m>8}-igiAf#|5Xkƥl\`h9((VtB'FA nuߺ@ X[$WjϞu"FY0 xmKl+Aj8NEoĿ8콶TlAc_ݎře΍L* v0Ւ3 zqO>F@ft ]bP!zEL;=IwQ,?8N(@v##C1|L2l\iZ Võ`&v؞v*:|B/]vaĵ.(N@#Wܸ#;o7EsH!8'{dwcaK"fJWTR&){yW54TمeKh(H0p8Wlkf̧Wd++Od|xWe?д^'''bM>Bq]a }$oL {wtAV JmBB 's" ܛcy᠃O&>/OFw=ۿa8p _F>ȭ w\:Oi0. _(TteBLJGW=嬬*9, wKښׁ/ _-KF7mTț nW/bEm@8i_8DsXzܴ)OVl1'`!c( 3ѿ>ig2#*[;~-#6Br?!SaьvFAoEsSqu%#ʬޣ=(;E[!Û6h=5POHHD$h y=ȋ˛9. )Rz>@BiLlVӄ:h,f3j6"jtm*!5ݑASMZ(dzw(Me0gerٝyQo(,tc.5D퍕4 ۮr]0#eUpԞ]K* =y.zئGyT^Y+-WU#6qbW`%P$"pzh# .T 6_\>6p88i#{Z#@) +-'\]sV| ^Z(+\$A!UL*_ -(ؚ G 3' [u 6I8>i:zi s4yU@4OL -c|פs@GUSDW8Ѝ&CTHi>bS8 \c|fz啗O)兆뮻ջ,v vDY]:XYp8kSmGf:< 0D{>//ŵa|WUmh obCNVaԷdȑn6MxGdɑU[5 DQN͵Lm(/~.}!ۋo h{>蟶f?=V "ش.j0H,qw89]` nrmE'&E0r|UMdL d|h,?J;̭̬cRTSKrH0u _1 &uKU)YP5˨ Lӹ%rQN ]z7S8pVCv1mҏEDaCO,8]OtZmIyTBΆ1QV!Vr7v'K,xLW!TJ:@p!D`=BL7lfrMȚqQ̍7aR'Nhncz8{|>fZ)s<ծoZ!ͼ Lj/j>5ix=:[l <3YP)>] WJ.] |Z+?9΢Sc-U+V8J!d٭p8` ?20)McLuj`\MA8QU棬WXJO8瓐6|X& Ĝ{pl=ms(wg_uDY#y4Y)&Tah1-W2:]9S1탕@2 sph=Bg\le(~]SHͽCPrL|4M 7H^`AFJA=~>CtOzuJF `nF!\/3"uKl_9, _2,9lLd->Y-Tq 4{X<%3(˰03]*`ow_B |#O0,R52z/Ķ1%b~VKL+/*3NJwr4[2Z6{X3~?eۗ]A*[)$c(c3(G~Q l/r^.}ܧ.͗I,hUY/S7K]$_1e1Ga֍'@+ȶe|a%e'[~ydG|ԸB7Wi;?'CU 2leVC,Iļ?H ܡ|J+5[,#o9mA1Zo/TTJ;%phsbءa^isOF"+51so`eMJ^ @䙬jVLEXĈ:j ^tQܰ$V4lǣ;u2~Uu2kHkQnq`;)xQ=Ewc6ƆN\z l7-޷=Z߲ՖΦ!<. ގ?Ic0xX{1^!^O㠸-iVNanqn@FK[Fa:400`* >} d Izs^(4<<E1phFH4 T(*9 <Fo ˆel9D*g N)ۥܬ6ji 稕ͥ"/ P~#-ixLs b- c>gXhR`@R"xmE1_l2x$yQ7O!bLc"iWI0xu,cDc p?8@t[A> w x;:%T@4(bꦶ ^69GtX@X@  Rty$5?̿?*[,/1@)Fno3h8V.نy3}@2hu,Sh,[piA֑TAG85%EjB(;5/9%{IENDB`ic08K@PNG  IHDR\rfiCCPICC Profile8UoT?o\?US[IB*unS6mUo xB ISA$=t@hpS]Ƹ9w>5@WI`]5;V! A'@{N\..ƅG_!7suV$BlW=}i; F)A<.&Xax,38S(b׵*%31l #O-zQvaXOP5o6Zz&⻏^wkI/#&\%x/@{7S މjPh͔&mry>k7=ߪB#@fs_{덱п0-LZ~%Gpˈ{YXf^+_s-T>D@קƸ-9!r[2]3BcnCs?>*ԮeD|%4` :X2 pQSLPRaeyqĘ י5Fit )CdL$o$rpӶb>4+̹F_{Яki+x.+B.{L<۩ =UHcnf<>F ^e||pyv%b:iX'%8Iߔ? rw[vITVQN^dpYI"|#\cz[2M^S0[zIJ/HHȟ- IcB;>IEi(yiG dzp</ 5dHOBa|/E#T"NhR՗ps꫖;C$6qXq7 3/.5iؚ0fr\^8W>^SX%dGAC(O-Jp <zxT+R!./;nSs#Ȅ?k8a' ݒ}ЦWwFemJ/Aeʂ]g?НsCۜ(;<*!Ǵxnԛi2F)< ZQ(HHoo݁1p`$x`,ir{_lU8uYffBQκ3 L{4K@3"g8ݜ7T(/}3t5-C9)]a# cUq\n1p!߫6<}>ӼրgeC,7kC S{uA6^_=[tv|ވSzB m61;@H㡩g7gpv> 7B\WT! ơ裦K* %0f-RkmeMP,<xH-#FJ_gyI&/x߇YN†e7V4o!ÌhY@cLP`i $z nwI#"~R g W|N̙=Wjz7$8j% zbG To }*=!u6;hW}O6|M<~|'kWKﯣ=^tNm᫬Pp}m_˘1w^:[F_ϯ<{P QW?"=oBO+RN?WIN60Zr CWhtM1ɗ'[9@)H:־YyKkOw󣀥40Z?~Ǐ1𕲎х)z v;'hrT6\?oA m,{`kr>zGa >z5оϤ8 Wt|aY@kLJ at*PЖ}<=T|&X0עM2]/b?f:8$8" MB߼hMH'O[&{V88,/@fzV6 cp3Db"V&G/'x_<{iFcPAڲ1k[?SV})^+Ģ/`}k/}ȓG`Y , rE)}_M3vi׋'p>TG%|{2W2ȋ]sh;*'OgO>5 4}>e2BY2#]P Fʏ1}g(\\s<C@;16xK0]i厾ypYSnё"drUoȳaY2ա%m)sqEpazu^׻tHKZBG 7ix2d?"'Cp·o'ƚ&MȑN!CyN֯L#]@Wԁff]7n#~|ԅ@Df +L_G ^! yc9^d:d~ #sM_ ׍߶S?\ʴeNf(C%e2FYÏf^@GvP;"8.0f ^>xZFJd 29\!{̈́2rFH ξ`WV{-O#in{QJo:^N9Y-6&IlKF0#:WF_ AiWQz~~Hzd&` ΄v\r]؍@fu9=#kGGlq) /ޭ]zçh! CNFLc3c\‹]znE?Y'tL||~)t=3tt2au+ ʟ)2B IP|8_-w2vSoPB0O/Id-q AF`FscNv E ]ʟfqZesF-w;NӊLj nJ6\wb yOrEF jѴw r*S4-db~ DiRV) pjR@uzH? @j>y%@ݍI]4$8ٮJ N-ri;ï=-#PČi wJ@>dcJ 5MyI״B?,%t?&^dڏ<Ҭ _as[@sLlD X+X㾟;c:SP)˔ithR}KXk`mL/l  ( s'%eSɧO&̢`tXH-hi8g ]9dvRo=S"D֞^=_!k" 9XU1*sǚ } DOtN>41&lS)yhE \:fZIYCN #ЍcP)Ӑ}ҹSv$caN?N Ϸ!l98hwIn@.|po7}~F ,9|tJya,X[ еEo'>/@l ,t}r[  -;/Ap!uEoYL`WhW s\#iCv2Ԏ@*SXZ ;WlF헊P@3`OEC35މQo65a(BH@ `‡AM嘸_{ɺWNVWu{ZFP xDt j$J>%뿇 a\G#`C) /TKM„P~D-d L Е 3">T"$1=}Ɠ RTBWL>L#@eބ!,;ك-Ō.%A\Y)}mՄÔ?<E O/`tjn/B TIcH" /`̿_1~ɫ"cۏ1_RfjEҟ`{djOPwCDt(&]wP]龗p='#3``p u)}S9@(a1#Ikq=Y-S޾JrCC>@ *u˂,zBz!0V/ `H#OH|["84ֲj{_`St\e,D(at ʣd9q<Z!Yo%AlmDzG8/` *qgkk: k6:2kBhˈo%2Qiv.7*% SfJs[!Q+bMPW;Lm)͊ z@O70Ŵ DScr>i|^@zO}`@<: -KD:̠|\5!{,N+SxQOj&m{z1i3Xu dA0"+N9Z}`r̻!/=z.Rܷ_2}mR%*f6I^ ~#.OF ) =&/|C4O: ɟ5 4cva‹axOvUjɣY'2ߑ/ښn9t[eĿM](rki"e 'ebgdkeի.j 3} |S+M6xyQJxSӔ@5oHYE?gl}֛uN >%~S_v0)Xj$G.Ǭb)CN xP0`V`ЩBY)+?0\ I/jJ <)+#" ;< ,xq`(x)3As@=gؿ E?YH=E<. E$P>:Jv),D č n:AO$U dWFJI#szVPG٥R&RJ1!́E@L=h_R۱\2C01G:[% T^>IkE_mZa(ndldktUͮg% K𷬳KI%{rP7=򑐥%e=$6`TAO )Dյ_ HHi+S fjc4\s18qiIB#@^c0+8?<)'Vs#)b>e~γ,KF@BNC\ؖ{B{Mpb¹r"6OЄ9e t2SSV a",)K?g{! rǎyft`F! lDf#Ud=̲@oh9)˶w~Myf,82 ~(Bp֓%TQڲ Mٻ5hYp0M ʓ0T9D٤IQKbTZ tB*c U@1ɳ8gi!rK+]i@N% Y }ʟP%;[w%hpVY2yv|t"r3EQ#p6P:0 ؙ:B 5?Fo\wXuvE"  7񗗘P0 DŽm[@UZ*h@&`\@'S~eY ~-?۹p8rP_O ꄛ">#:\-`ܾ 3uCQKsȸ!BÜ"&xk~`|!ϼYeh떔+?=[wfIcCv.IvP=@Av> ӟ WsqVaju(OP81`%cbͿ(?7[w,VIb*uZb(DNaBW '8Ův48rD"Y. XjuO`QG^P;~)SN4QW$ IXECU0n_Bb HPi 4q_fy|R9PuoT (3'R9A0dfe|#0!kWvzC2~L-\,># U#<,ݧU \LY1=ú;d`ê9w{dtna;ʏza9Xi?a5VP!:tML!TdPwSMA2cqCA ;>7(nU+ #-oĢA*]b%j0PW[`1yF`W#F ʆ];Ŭ}iSNz9]^,ڮRVaaAW}jbDߟ|>܍sφ"a1O[uYy㪾4.ƻbffD i[sItQgtyw5H@kf 9H@۸ϋV~|mY~3SNQ1Vҭ]foP9p 551RWf*=;4#&KwxzBD# s8t͹zkd] s 8t<LJ!ƒrJ;h.,>zQǶm_H_7v#i0QB>8H^%&0"VmL`h\._=F5"2-53a0ji^& u32?#[QN]'/Un` MW?CQԔw>gF$aq#*W(FjTe/3΃9i9YmBez:Kݥ["=|G ?kϺ $j hZ˄lb==Py7#00gWyFVw#R37!+ø F gL}ňX 03!crGwmrM]%]p-j󰗢mG-tQZ#UxF e(%Ѩ5'{UYx>5EbtU~xG4ĈXİ н?pu93[5-^UdANT5LZFW7Ⱥ_~J$ㅉdm[e苟loLʿN?VSva1]AQMsPT~x*"CV ֪OOt1>Ҧw彄g3ޏoTL 1J< H~.u5 D+1aǠ19#.iEk ~ևcSn.u5 x0X=V+=ssсy\FhM,,Wi8$*k?uPc,j2ͮ qrF{Փ#jVE>>~]r6V˕ۖe' Ÿ (|(7XW&Ny U"7dz֤!#MSs?(LMG+d2q*~(`|/^ hi+HH˺d@"Y&U~AOhJX4 W8,XP nXfeU h Ŝbe`SGk 3T֞4{P9&RkkeӚʏ2(?\,F1c+ @ˋ[per{:'wY֏GO\;8B4rzc։38NxhewV*mz١*ϒ-Q$qq<I$z劔#ɢ *sU0nBtgh#a"J7@c "aaM4bT V/OlCY[ͯnQ(Mkf[ŌiZӦ7DM%̒}81Um7!c\u"d:X$3сmT-z!(>k`YVvfq%lH!z^IJ|BQgCtW@4#bQ ҕ16v,zK Bqg! |U~ecYX=X4I&Pg3,Ȝ./,V-=:3`%P$|#Kiw:&Q~)1_y,wȥҚ=RT./L' iu$ ]uBs\;sx{`BпLi"u|Lփ3t;V6Y W&7Dr#45 (n *_X 1 g=TcHKI9HEPV}ASC9E)5FxK#.p4hͦc,?8K^4TV~f82).&5)Yd\e|:Z' -燁15??1D.Bx4E.h;vqk J=3rsH X#pX KY뎷X[E6Vp5"jҳ$N 6r9:#݆aވF@ $)?b?άL.օVGR/9 )4+܁8`wQt.<؇Z tOQ@X̡=8\>t #7xQ~4ٽ~`3Z7R -7Co#1zKcM?hRy6j&X+=#gN1VԵtq0w}dǎA 5 k8ɇC}h3 SÏy$ܵ2^DKLZ!%*xY (؉ٟrtԢyD ~2=nEr#jqx)Q~+y2W)Q -l-|rPSЎ@tދiPsbR|cItZ*B23ZJΗ7@/V.7C6ŅE֪Rdg|״acKc2jzpPݭJ=@?]0ՈFӒ%9IYTH:90%y뵆IЃ]r=gø|MvQEi弜~IJ }7 -G֗R3)t9_|\%_exWN*f(vF,2́&cwq2|6އ&dy&,\{ T{1pzoӢ'Ó$r%SnnPT٥X0q<µ> t'ÓcTY' sςkypgd/~ϝr_g~NqJr=AU\»UhB.5ru>X:n'J.|E @pM'ݤ f%b &g5u1=:S2O* I,$#f"Dxzo,Vg.nNO|<_K'zyKHz)(4ECV%8iQ ,V|L|)' s(͏H\$ĐAe>s9J(P'^qgj~H])JQzr$I``|}OweނEE^.pIFiu^`"fL n[4~tYHu>程㹅@[y=!50/oby6q|nӓ_yϚ:H]Ć} VGE! b.ȷz:̀@Ţ^/x]laBݳM1$TtƑ Nh4jA_ƹ~3PfO RPZ%_cLe>ڽ+V5_Txi= Q[czC!b@\@&gC3麒&j#`1RV'?.b.M<|c.&|9e⧸ Ix;̀@g4"aĢ"贛d0#`ܬD\If+ZݻHrzOSF.|+Y9Y0;gɑ|[y_<71#vz;&Ai5W3)hB'\?vo Wh͟Yh_NrtTrl`` ^t F5a5[V6|LxZBOgR< Hz^,?zCm>Hʵ<6H_`F6sDcij4#Mo3csDyHXNs7#h{G#=,NRtÇew!1s kcν# ~wn NS"|Eخ`Ykabn@C]H)iSͶazuXD'>-@Y&v_TDXeGt?`k'^+$4yqӥ$K`AaYJcf$(12PU^ᇩ;!Ro4@]* fD.r)/o}`z9uO*B&1vh&/N G 8]1:]';Q @nI&B_SGG::Aݠl;mEH_qx17Ur'4KGxG`"`tyL}LJNg&qH]-HxϬm HIsاj X"m>gG.-+\ub?t"t!?*PqΌ@C:,=$+;(r!6ht:p2rH)6b[k_% |ItA<|t@[P^ m5$wa& m%dz:@H}~@nDm7$p;~Z;=s =/¯h7}~Bs\M&.P':Q_k2CRŶnwN\h2O' N: Xbg%`,Fs,9'@ 2FnC9H6w`lkLT/i\MSVtpvG e2MD'S#i `9Y_?Jwħq$O@ Ζw᚞t;X>'_n0>x2,eyB(ۤ: @ Qz%|htO3,H|IB\ @w%~洓J0r @vd`pCIw㋨'cJ9 cGU=X;'ŋ'۱BS*X$;20~YZL_F9ˇmZԿsy/][= ?fz;75\"H {EfpcJCVsRtLrNP DuX/} @ lRF)+Ү,sFw\YoiC)AʪYbVhycT|Pc-09~-RGzY29d6dD(6m[LHu P|k =W`wf5[8lg fAҺuуw4kφL n tzUs  | Y,aŗHeQ'm+A p1`ttc EP]XdKkDQ(sS"zeV@e4cpΗgS,16vC$@]|9_\9 e2DI&@pC V~.ٻ`"7>Tޤȧε {2_VK)֋S0) sLZdG? eE(?fzbitJ;8G.y=XYL9N ass=6^ft>d+t&$؎{ Jyҙio'R#!ywJW6^k&b#[-aAo  ߎ%䘟4MED h V]sL2dJ*Ϯ3 9SVM C07+NJHKQzss>/S݄GW`dq\/AP {FX["`eeBPv@(SW5?zH=e~Fn&`>wa I(9ٱ,$VւQw+E6Le; ux/I}h9=8ߊ̒d8²0CѷxBd 2Qx+CO#l2‮ ;Gd_>1xxxa ) KŞJ ÏNa!rwLEѥ, khҭ;`zp >>4 FB Y@\2}zt {1IDATpA?rL}LSD6D-NYqr `u(0ħ+GJuBj| u%)rd712¤L6V}ێA(dPba;rxC2ŕ=7w?04ϲj²7(}˨F ƽ?˸= a;>#%nӅmr/|>zL]Ҳw*TҷD(v>$wD cGG)RLCBʂT|N? {>d.ۜ3)+{NJ @">~HqCʶ$TgЩy`B?UcqX PSi ctAVI|P hLн+ 2d𞼄cB k6*}<[n>_=8 s7;a(S_wS/EoE p XLw uL=^8j.3r8ĺSbqPp }o B)=-Wxyi- >8jwy jڎ[op^1'T1 ^}ڑ ء^PzsM25v}h/M+; qcJr4] _2:5j3bב)r3Nh' y ~߶Uڄ1cLmlf'JCV  0X1p|qGt3P9F/zB,[nIsdr_`\Zi nPHmH#jxT֬ï_p)nv CFd qSYţ&?(h΃ր}!Ge=+wB"@gnQ`ćDH0%Bq⎣`t1`q%*/YD{}3 =P84\wsLd|t@&ǜT/˭6ucGXCq=(wRB9xH+}6E`@GXw[v۩ 0`Yh o~ eZ1*kQ4 fU̩~`=ǡjCmrmkfwD 'Ttvr{2݃x/=;Q]?h1}\q*r2:!},h˥ЧQ(nD'dP L9z)f( Ш!he/k\TFg ܑsxk94:|}YęBOIO ` -ɑg1Ԟɪ)nŸx"ã5`Z=;MjTf aGTCx TrBAxG{~*'bL";@åְkx$CZx<G#x<G#x<@f(%q5IENDB`ic09+PNG  IHDRxiCCPICC Profile8UoT?o\?US[IB*unS6mUo xB ISA$=t@hpS]Ƹ9w>5@WI`]5;V! A'@{N\..ƅG_!7suV$BlW=}i; F)A<.&Xax,38S(b׵*%31l #O-zQvaXOP5o6Zz&⻏^wkI/#&\%x/@{7S މjPh͔&mry>k7=ߪB#@fs_{덱п0-LZ~%Gpˈ{YXf^+_s-T>D@קƸ-9!r[2]3BcnCs?>*ԮeD|%4` :X2 pQSLPRaeyqĘ י5Fit )CdL$o$rpӶb>4+̹F_{Яki+x.+B.{L<۩ =UHcnf<>F ^e||pyv%b:iX'%8Iߔ? rw[vITVQN^dpYI"|#\cz[2M^S0[zIJ/HHȟ- Ic1! @l $1W>[N4v0>O ќk n| 숕N;}VNFvȽyw[0n\)F^3^DX<";1)!P;Wk2,Vu'YGYS6u{3@g@g}8 bό2"_~9~t)A|ܳEw/w?@NQ<6Iߒ!E-w<?&? S<}؏xϒNVy B,Bw!@c=e LZx63g}@lЕ;Ebߘgvtѓɯ8s a Nɓ#oo(Dg ]&S xYk\7j7߆K7NÐ%B@#0_LU I=gpvkУ?sb'EolA%H\9fЭ0#Dy! 7kpvΚ ;9aA@?| !PWkX]P "i'1~` )aFlC\< 88YJ8Xj8o4PQ\{x!k `|C5jj]߅uLb!p Š{jM4w*gй=c[Z}<9W΅Wx}T:3 N]Ѹ_Qn:_4Oo>cCD={4'Tg S(?aЀ/[!K.vV\Pd3NBJ1Ri*/?F)h١ &MZ0 (~'4|O@@ Cc.6|ҳcnLQdt*Ni@_ Je;Iq,p7EHEq]P#?frx&_/@>{&Q x0t,=jJ_c>ݻߍvCp hwz3M^S" !E0B(x,_b|kgBp$5QY.)jJZpu9~0-LuH_{uAaE.n( -\!p-tlW{g")1"1,jHh\ѓv''{^D/Ď0l{—w"C|qGWۧ`p~zn?iE"E#ؘgg:iѴƓ@O rN$C;BPtC6B>/F֟4wh؁lSQ覂(ha=ߑXMy0;5q_շ/"}9е1s ϽG0#'QG}nK3_%*F150? e7Bw?Ȣ!,j< V u'7\!sv>zKf~φ.~6ܘ'[P40q0lAG0Xa PgA?d^z_Hz \\ƽw6mWC;k? T>zs^nw@Y5W!x#r3\](K%gP2,M|c۟B\{"vaG/t4%,AALC Si h{}?kB +Ըf mlNwBJF8~5T\ژٗzϐ5?͓n`3K2Y.ďpO~7N=eĚcMY$qS: y'=eln''ܐH}ȣS>$+bm3ָl4x'G=)" 7<C`?jm9\$m/%a>ΐ֤( <6 `WGQW.3Q kK7/Rv?q1[r|_n5!0SBq{ O0ˆ6`7d_yϴm`De !' 40}VC+dt[BCtr>? מ L[UpOaKjG>6|wk y^LG'^1I!puݳ|˘| !pd,DC`7܍;!;k^ ,Wrg1)k}|'SLL$PA@@e2xv+w3vnԋH^ c! ^\xàm{w`Wm;m~+T!4 6'cc"%g`[>Wd_U7`'( ~̈́r/r$(唛qϡ3m}Hi\%ۜ1Q{ km\v%/^/&r;a+|v݌o{ dP `;X'h~GKI 1 1WyvMLNkD G $iԩ\KXY)B=<41Ćbgۤ.V@dP"Pb9Cv;.'<0f閨,%()-}3<(A;E(gI(IYVn?p9-.IV$%Gm{!ڠ&ߙmU%*[%(,T*;ja_?6QE(@UAlt=o~MYUxₗ"*؇bQnF\/?' El6b~aaUW!o('"?O?qZpwfw! @_IĵKhS50$p 뫀J|EXJzs)k} .gȿ壔G n*QxܐJ``̱-=lCV3Ja")~9?~s`K! ꌀ'O~^4<b@/*PA%I*alϙ9FA!P8|Ķm.alpͿP7ڊ"Om@[SoG,Lu!⯭j(Bty<@C94|[WLUP`(T_!~DH^W$kB &~gO522{Nj|ZpDPw!PLmX4(! ʎohc[6m} l˞2/s.5(:.fd/|Z",B@@L8-4pKFleC'y:PdBG3up]>>-K+XA!PY6^e@l6l 1`zWB_w|!@'>8hL?ɶPBWP9*ov?%1OÒ~W*! j@L>lk?@mf@922+2FX=5%cG$D#& zoWt:NE9%ay?5vLFdfB̡DG_I߆27%Z@F=Ίݲa>ž=Y(`FI*Z! @!0#ૣ6mi3VP52 'L3YQ !P9AbG)j3m(;RlS+dH !;AkR?eF`~g֚nӷxΙ"^AcG?8O"J 02/z)a3V鲚iz)ah' #;6ˁ.ts- IƒY|! @1٨XDBYX=NֿXg@28Bhp= XUH qk۩8e4]V t*(0NǘQ߶XE% 2c4g=2- )IS! @@מu۾/H(bqbpfvҬ?9?QE"Hho偵?m<4S)s\վMk}x̂3ZBRfB@ ʀLm=/ ,F?_3UCG 6`۾Mqv}V,Τu,ze@!ѡICИ q@#FwZ`_ \j"0 ;CT3Um wČXo ?];eE h?\g6@kW'voSsҸYB@pm]o=sЍ9ߞS?i_[YB@,@@oθ|r9a{}v9l[A濅u~#J ! "&APxܰjݑH[pN!a7 _rt}9_aVք+!4?;,VXk[Զ6LOr?ȟBcO[?鹢pԂPnmՃf[ګB+B@! ͝xwwmwOz$B@dCrD 6;K^ ~?咏jB@hi`[/kK+=wKm}־ 5ӳJ ! ʁ@eƆqF{rv)+F%~/{iDO! j@=a*\^1!b.aDZrĶc;o8JbRB@@L|_akF K&+iA_UJ)! J@+gq<ʻVnqc;ׁ`'dB@ c@kf}ƵJTqqS^Q.IbWqAS$qK޲Ppnßqrv`YN9+'B@, 13:Bk=hsLiw=y7cp/SBH K)%ҲW@qq^cA*! X r$SFᵵS{+a\s1Rɀ B@! B 搀Bn!ܮͲ]EZ*ܲöq,6L! J@L 7޵sN)3/ö1Vtݔ>orҩB@  (=M v_A! U@Fnc^v\~B pƒe6䄛4 B@!AFn;h[wyřӅxx(9kR!dKBŰna@_Q\U[9ߓN ~ C z(0nSE=AĬWJOmkCݞ)ӦF5o@m9mq>mZ$Qcx%V۟0f&&.!Pv@F [rL`'x_|4tma6ځ<-3 }e&,x 2 $bϛ=a gp >|,8 I'a7"-~Pfmֺ{־n 'hn)l {0iɀN@.6CztV͇gsoS`CV'L3 o܇?i?`.RP18.x:Rr$`_[՘08πӊ}!&$p)?lUeͻ-"jK7kzZ?fe]{-mHv*=R K@Q0~Vkl "|̣yC`ӯ8KG m<8l/4ѲD-9d>v`PmT&6O8R/y17 M"ȄD0rh8rT 79]6OƖF^e֧1Oѧ6dZ7 0faklG=-Ooat )0ccer`̟nF-IO4"=5o`cܵ߳/xّq$)# 5QmFk^+6޷cG{?Q1]{&/|Xπ 9E @ ܉vz<@jAv/@;|lqY*Cf؆0& F@^r9!Ƨٰ΃6?ُ߂Mg OX6] M'XxG6gۺ+bmutKLvl_>FtU 9#9al_ s%Irn(8 г #1>İ \nG g 0GQ @[~.ӟgVx;{=K0Ygapo ?@WIzi*}Fq*کXbO:}e)$u;|i`2)J!7&|k`вEPQO?uԑ?]6iqWQ;Ijx4y('sߚsB Xxm 8 C`9GܙfwCC:&wzGC}I ShƗ@}N'Y§!i\ O3/#eU 4a`#Iays]HkX1 1'I.`3qsͶR[@_oǾxc_3C IϷ0H% G꺗ǠACQF,$z7B1}Be`g9m l1(Kԩ=XH)U r Hndz MQ_UX0LE)rȑ ȿ%Wb)r"v<|x:O.6^fd /X"<2 RHEcϷuf{]+zv$_)`oHn(V_(NQ!>1f#cf2Bv;y=mDtZ3@TX+ :dF4# YzD|ߑ&+Rsrg`Pzx4B͢mpQ)#PցysF֦6q:#; e]*Dt~_D,s?e,$J#ɕ_tgzO^ ~dLz)&VC<V4 l OK`y+\%Bg9G;`UuMĽ9r&1:!3t/ax\dRK S/#S41 k5X7ffG9 O@!K Bx{ 4uuOI=Xrk\b c+2 q-3oC5B6:`A#`v֭'Aye䏞oN1Y5 Pr)rz"e/na8Tr&<[F1ոHXbHrv9Nj}<$Eq/^a$ 6Bߔ Wg鞘x(x !Plo֜b1Tșk?ɨGQ /&O ʀ@ 2ZRcR+}kOb rʺ3pH}-?EH](Z # PrÀs{_i@qgx.Q{nM3 ? 뱖rHWTS "7sJ.IXȾm_qYRUG`p<Ҏ+w1/pzEY4ۿpW ȲM7WL"w't$"0=u+YT1H@,$n%KQ3Z"ma_r "@Ѕ4H1pk1/΄@N,6+# '䏓LQ&"KJ.%Rxϱie$Ur,i>nnF"HKZ#@x<2UqeDɧ3`39{M+T Ğ VIeR!48 #`u~"ۿKU.P:nrr)ƒ'sH4nqjQECx # ZV?-˹\>DF<^j@le61şJE%F41p~K_{U1r(sk\%PwSm0&h x#dd[2[voKyz7 6 R3.ɾis̆@F!KlM1t\ Ѕ336R)*.R?Z:@ϟػQR:eάq@ lxȸ/NWS3 csֹSPX(|@*xe 5޴ k90 D@uz@EN%¥YOs>*0U,du: ߥ^p?Ի ǟj%}ycQ1FEs@ (KDcu)n9R8?z^\z uwN@T*wm5 ! +ݸ9\ r-_"xs2RnEA`@anǽnOq>h6q R}¹#7٨ܬ|w̌#MyO=2"l;!#e,kJEg1|OQ Oxkڡu`M&@Uf.+cAO @IDAT<7Zypr߽7AGY!^ӯrYmE(;6"]{ apHkQy!08m^Jgr}!C`ZL`WG5+-1]{޻6>$ƖqI A 6> O@_c?{nY6((V!Pwhsɽsq7tm<%Nm[0  GD^z!,0^zyLØ륎mD\r/\܍0]{bogM !&o~\yuo#_S)f!!9ܻqݵ)#1u _!1 #9# A/ϸX81釜 v,sj/um\0ܗ~DZm>u fNK8`I_1eRvʅ@sq7Yz>`O7B#+g%B \νhިfme1sܚOumǢܡm=Οjt+؟W[ZVĻ޶x.Fή $ZYTDL+A]l0\LҫzI  5̀["7#7EJDKb@ mt]< PߜຓAwbaMzFD[oIcgnNAc5u9ci7{x!|ĕmL/lv΂,[f,%sONQcԵ+tO^U^9KXG@Ct1q(t-E xzDY}u~exjX87k_s1&a zP1a;u9眀{fNȟ;?@\.#gߣ>!I_}}YGS0z (aCwuJ`$.J ^ 3EG!)Xh"#h@ Fc@j1c%؅/ųA0(Kmm8C==ɟՄ]Ԉx(u&j'|\j3UѕR%jĪӍA q7?C ޾hb`&"{ߑ?'URǪ:PU57`l븗~9nb߮ !/-q l'.=6 J8%vފ. =R%;]UBmvK1sCv `u~bg$cgOV3FA4tFE7^I'A9h̝14Ż?O0_QףҨ__4 08{z \pDVj!H`up'`3&JCC67ͫf^r;5NG+IZu}^t2DoI1{2M DOԩW_O>g(ֿEy:0pKD_W90d6~ 6/ỵl  5 ?gw΋[se ,UYsS?Q~n;Qtdlv;~4qK60Эh6m7+Q#`ӵ\1A+1] [8f,BmddD@=Y U( HãGݶ7s_LP'CB'{"Cj5Pz2) B#WJXOubYq?մN_:o`2$BO-.eDʸ69F1λkph[,}BޗanayYMk+Q%Oa NKw4uhVX.[4ʸY Z_ v􀵱ϻp_ Ù9;Cpmo\? yIK v->1|%F~{!O2)ɟe5&+#y&kr lэߕ!/D 6r:(rϗ$aHNc 4K%?=Q HAT!oɛxZN? >!Q]©@ո m+"Xs-4|T.,&|G3QnfڎWOOATY/^u_}}y1O' &9O ]~L? "9u2ЯoDO-˘ԵhXɥ+"lu,7=gOZ7ۉSQUbR傘Yg{{ ē6s.=!$2unx_z҃L{3p@k3̼d E&>9_E&]!@R]2~' D]G?9bQG\҈(ʼnW%E2?bΝD=]M#Z{RU!@27>V%b+i@}bus<'5 $0`~!%^hF=/6$8_:!Yx`8?%Q|éE/Sj ̙D+R87pG3VۑffQfR?"u:XB13/(WD7Xx1~@(Ojr7f)I d$@gpN.v^Yڙdz! K%-n.j2ȶ;O %夌E3V7r-v_*خ 5 5U[GN0-$7nN&X910i,NWBbnbz?Hp2]({ lb=̼x.&_]12oZrkɨY q54#2;b DN Le IcgYM~:=?\WNL!JE/0 Cy.^<]{Lq*Z3z %cJ.&2,\cFaqdpp@fA #PV42vcϟؒlN皁X:G0 Ĵ*Ծ&9>'1B3ؘ4Tn6٩=ȥGsBSXnKS?~Oɰ6,)׼'Sv#^Rmù70v+gvQ֝Rz#%0k)S'&?E{-Yý?ld 5]4Qܫ\z/q`<   ܑR#uxpoH=?{|ÔkuujފXv~t]ꎠ:1 EBwص*5Fل!TjjODV<@\ .Q[^pN_ t=H"@8?ʈg+lSWVo5e{2[3n~+dnໂtGN#2܎WՊ-ǜ!xd o g?c+?/ѵ^25)9UR鎠׿Vr]A@/@'\$6Z"JұEk FG5ýΈğ[]_^ Dw@z42VU?WGN7Orʸ)A\K΍I%S12I*Cb#/ H"["Os!716ȵ8'X4q?TB` o#3pf6, pC%Em)B ةшsspBd\Gll?3Э,zNTDF`F7vK8 "FĦnڕ2\H'ǒk K!=Гf{'qxIAO. BzpeJ$B4@ӶcƖel&kn%/,S\WR|q˥KKN%N@Z־)z% źbL^dҳ"DPnK"_= W"# (_"Q8 Wy‹+_ɲWP)kbGoYu8g$K oeG2Ͻ$s݅ΧIz)umC'͒]%pF篶pj͔., d䏼R?8#$fxN.F8; ,63rmKGW3~Yo FSX0 -"(,D_,+\oF$9+ٲ׋|1/:_dY%[\eg?<\L޳ ȑ0O!BПJ'('yXBW= ?RyMU= @N5Ĝ۳aPg@ hnƨ j@x/<2sWkt Q[1m s{'Ft9K:5sz^F@,C0`*/BI#@.%F^Ϭx5[OZ;z-=/F; (_m"HɩVr,#^B*GvmpQܨr|GA>DAe0#w_ Z"F\;u~ P@@8n"Ǧ)_*.N9,,)UcF-'<~[Ǟȿw|i*`-#U8JP"g4&ڐZ(nGj&R<:y(.=H€` ,w < `dYlڗ!So't d V/4ծ+:m z۵q܎kH'M{yhBx$ L+,Bĩ[IdMHv;tF(o* >?{M@zM*R{Wb'$Z@̡n `qnf\ 5g5n&0,XnD3#=n|&=&`WF=;9J>?:i_;'ƱHdƇjt2yϵiZqq C?FڇNH / I^%[4lJJ3kI-q\]5wU){fA no~\b)fRn5bc%H&]$H{'z1pF qNVq3 &?﫛?+`X)6K]5t^]5!40'8#ڇys̟n<0:#T9;D;G"#s 2h~):wNjR|o'.y# E盟n&#ij0k7XUHל Kwqϱiw'wEQ¥U`G,DFJF$! R0G\ϟc04#BMd9?6 SشMc;8|cVT?q "#ϐq'TpQB݈SvSW!Z/-`jx%׆@`T^1'AM|ྲྀ9~^9ڸ\y3|q0.@440H+B ȝPr)skijGܼ4,^6XL\ISJE7WAk4fSio_V_I'Z8]:~ZT[Zw> y]=8C-/JWrgyoי1 oߘ#M c KYr)BY!)w<%JM!e˶Ouo熣EMO[{ffsW?l3_ijF"̲_;: ~?6 )ZF,4L,w;Ga .)MWr'SdQEn6 "?]v >6YD?>s?Ѧ WlpNxP{]\r'zXߓyy_߂i{q %ɢR7 gc;"Lc*0. ՂUxY{5Y`s8ٚã֞Ӧ6oP|{`qݱ(oCwF.o!9 ƴW1, XW,|OHŧjN⳨Rt @bz X1'h,98*!bl;?)ÎښHЍU4l?lp6rL"kAՒ$ZӽB@@<s_M}f;#L؇|SKuB@69Ii̩g++,;y_ CtҸ8ƗoC9)h)-,bëM9?n\J_1<퐉B[e,~)z/@@cCv f)X}~m ?L/Qϥkfuwgf=7U0n;+~%_MK@ tPNϬb \I$w2ϥYusq?=ш%eř*2RS #:"XQ܇!/;{tfVx`^D 9lB4\ ?) X"e./t,sN cET_I.4S(Я q]>O,Bf ##3^ $ҚYLT"UWZYFE 7#ɕL^ݑebescRL3fjJYJ>*PBUt*a+o3̖y258/GWB@! <cgqa|pedQS! J9r$V8`G OCF^p /@S ! @VOnD{ءs'ϳ ܽ0L-U*^! (x\ .atv2jZ3pq`4 P6B@!:qO۟aY@.'6-ˁ%F#PBJ>VkUZ˯DsvsfֲbxNr l<@Y*GT"T%(g*V|]`|v'8rw\ wig:YW]A6H!P]8^~{cj+M9=ӭxI68mpy7k 7#$)BcP<̗ΊuAuP.FJ2)$_"(#JceݗWV~,s᷾ '*R[zن Q>RɅDI{wN=y#&5>=n! Q#?n` xr ,`I,\HN;rja&cw] |E0&B4bOc۴d ]%D]XT9}]QՕ")h_#'R0ϑy L\8k|2~7@^yU:D̙2Ut{E/"G,s:wq?ȮM˶RZ@ yXZ2R$cUCtbt9quDn`@lי= p 6:{31$VV,I+K?JY<qb{b] eMНӌ|V>Bn!#! tTu{vD18Gwʳ|C Ws6ȿ5 A#/EMf on BYw[pVT%WEGsԽX̔u{[,{3>2B\G#пRPA}1ձn >^2sŕEm ;HkزH+8!c08`0Zfh7"ԡ3 6uS?%jrm:r'αGȅRbYtZ~ qB-5l<}?Qk/ t ڀ_m5i2YՔFFs#PҠtu{꼚>A8/p~ ެ:sm1 ?a!k&DN~; opPhF\m37ѯ}nGᦓȉ b:v=kélъ8OQ;p{nH,-N;jζ_/} {"kX~xVQ4 [5Od'96kl_0r> B /tL9kgGlp}m7t5aXw(N6o'?aۜ;u#sľ7՞rLIR}7PGۆ1:!ie[V.`'а* R!i8c=^(G,YQ5e~@7w/;|=m(ic! | $~[Oml0h@N Όynf ?7K B W:SD|{ luscD,8mNC>}/ XLH;i>Mԥ"P?ِ` wlPB`h pcSdHub-}6S@q`%!> yt&eC=}Kr"SU!=ywayt&B+i6>@p~n6:il cB@0XQݰds*%?t=I03i+r! SFqH!Ba<D[F? [kNRz0 !B`a~Yvvqx6[ ^^ '+! (-LhHq„BDVU-kC@S ”B@""?n. 0]?f 8 @Bp ڡi9m 3bt\ⰵR #+ wҋ]H-&}p+. ! @hE~V?1+02XR3L%B@,Fg]S p> X]! @O*rKYNx}eM9+/@t]! @jo׊N )EV%&5ؑgIUyZ+&!VԺ)B@!t 'iB iA~6^V<Ph%B@T0={C"g J%P@gw;V+ ! @pw9 ,H) ]>6y +2-2kB@! B\oWU|Z^gOĄk->\ #(KJ! Xm1k^{j qҐ'ɟnXhwܷTB@!7툋"7Uy mz0޲±ymjĂ1!0-zN! Z@t ߜ"OZ_{p.)4(%̔΅BTwbvGDf  c.ß2^ wB@d9C"'L3Kgǖ&D4!(&)B!@i{A9Z?0dPMZa].9|/ՐF9B@"mnls2"RJ$ֺ >e$ (Jf! @9`CȞ!B)Kk7:&aD/G|XX&!Ў&Krnxw yy\/ns }}aXM Bgblbߤ78#MrPω!z %D=k|&yGXB@9qL匎C=PjBvKX p ; B'%r 9\C!q.@ /^ iB`TK"! *@Bn{h^%83!pΖ B'_x?cw'% wDȂB@ x߸ z(YS7- wƘO D`V(+^! ( {g) 6;sfp|Pf$L b-mfÂzڸ^ᐌH ! JO#pmøid"AkGz(tKyvZ%#({!  a/xILzyg`o_kޏ1gi>@=@Bv`v( õB$>o/:l59Ḇg϶Y\P;W?s?9`[aB/8>;r2' k xP`XܡQ\e! Mg϶>&Z'(x+&SN! *vͯTȌ e&Aûw6  %Uyǿwfwggf)B%b,`]E4`$Q$TL4T1eiTUJDF-rv, JE-;3Ͻ}0;Νݞ>t9d*_b&`B:+7n F.lEjKHj3S|_U%s0h" 2LD65&\&>+nmdJZzy:< }Nx ; @ SEL2t/KVKf7q]wKKLjRj;;005&\CX2YY2Zzѱs.#iDiǡ( Mg9/yrh00@JW_6ԛ8/n)B!LY-^酂LOh_(yR;k}V 2"}Nd&`&\Uɒ˽WJo E6ƀW \ L_c&`K#0gg7źQ/Up @Ab{,P.{JL\BS.eLN=e,L@ )*Z%s ,"Al,~jXijHL^Uz:Ͽ LKј|b֥!p|"+8 JM~%icL"%&`&0@RTq0/xFA+_)M[ t<΢5$s֜"f0b1tFȁggB&`&`s3,G.]#`!CBh5 XM ܫ'2e&;02 gKFk69{ZW.Ca0&MڛU?SPk2ӣ|[ Xo+W2@>'lg&`=AxC򍲲'`%m!ƌ֍o:X} daZQ&`-%d20$scMMlizV:kX[1=<}3p ?7waZQ&`! Y'''(/8ddkb՛Ou @ ]QJ|<@\x\F ď4X3Ⱥ>d]-1y=Oќs֒[֒vY)*R 9%+] MH0l;͝^2}۵B`: F<1%ʔMV2Nn~}Sߡy昼}՟ ϔWAPXy9A4&Vv. tɰY4$'lAZ?YlZ<( Q0X6$$$<"'Y'U'``Ց솣-~9a߭g +CLZDj/ƢgoL#*dd]#P2yqKuwU")1Z>[+4X27.YOxߒ9E@ĮYʷydCBg NvK4L?Md_5B7BB fNH)5ŅҒGONPdϧ`c0OMZFߟo~cda-(+EGNḌ-ots?ZkO7Llb Y-NX%2Y/f#2(#@v&`&j3w0,?eFÖEcYZ!0?nlVZuMz@#Ƞ}^%&k_ڝ]u1Z0.uPb:&a01h2M|'򜈣lWI[cUYTa_wkl@`fJ!?87ʤxGF;0+H@+Km֖ևgg&`'L@&S*4c?-O uM|5'`=W.OwbVM|]0u |70$>}Yopw]X.1t)^u CC-cog&`K" ŀPk P9 jr%uC.Ԗv]@Lo0hc`! 8B&礯L`\`FѢS)rŶ8t?^%9ZKxZo呿(pk@{0cHUC֟inaVF٢s'`ao@ N5,t6tK*1&uO(נYg,Qv6}{ݛ7C?/,}1v@SFR„3r5 eHz^]* H.v68qZE/-ʻh{2HUC t)u:5(a[L^QFtkUCqtw_65r#;}ـpP_ؙ tK~/F_ԵzEd1y^!AsKSGuŃ-*/'n~N'pO4e*Ֆޭw:+#q뚫dUke j`DU(T d$zϮIqHUU,Ll_B` C$ew b c, zM,fDx|@fvgPUW^2˿jy;Y流~cq,I@3v x:g,P6ؙX@WHϯ\Ln ſ}c'v+ @3#҂R߈X `AJ'@_m]J2| (#0~|jLީXu]FCv&:jʽn' dE h At )s)~\~Lp~mķV]m6(1%* Y"Ohx`4CfLW rB[eO'4}SŊWF@{brT^IOT%Kcyq2yg?]f҇X~Ѱfm&];|`=K1bPGjk {+ lS3f|٘~n܃2lctiTE0{cy`*Vo sp EUm? +#Kuk]2Tȳv&z (Ftl%3N LO E Pv-#PS2No~'_fۊWe&ZA|hgC 4~>b11|VACZ9ӵۙ@S TjSM3}?u?C7^ӄ3ly8#=fAӚ\E?uJb{ )}U KJ{y[ج(\V+|j@*EHKQ"~܊1@kj~n'I񫹾nX_U򸍼)' V]S383$`=ű:1l5LBw_ ?BN/"S#ci5[6E _fR SM_[@v.q|w&жv&Й8(A|R <ϥ^ a e00K[K]_e=/*_Zڧ}x?Ph᩼6B" :_鄥Sԍɞ}#6B2۠:f-^M=ǿu)_&?d=2kO*oU|7w>q8g^: NL5y1 `O J 8| َ!-ƀ0IkSmD2O?ev}#㺳үAC ؅VoP;3 @BʉeH5|[Rw+h]pLyf͹ln9G& (v&|!矕GYCd3 c  &A H[[@Oʾ—SjWUV߻n3u8x~uGv̺_ؙ@D+>6K>l@R(?O< f5@5PZG"np]Hٗix1%/'Mt?WZ%f6Yb֟LgPLd@Acd)tl圯vL3X f=t(7)ZA~"s8[79oҤ:G~U&Ǹc~3LjzB8M $2 k|{W1{}Ic-3 0`w7v}u4*wЏRVZ>wNL%VBبڗ 'v5.1lDlx, ?&3M{JSԗmu)0(XϕӮ8y 2-[wl<7_4^&brnw Trm~w9=.>=ٻϬǺ1n>7v5L`YTpLV()&F ,6-zi? ]?{N^ZsXS3ya{ibSY-ҵݪ':nܒǯܫ!e8~g9gv~q}31{5{O?L\..,j =38QR|w&`%JOfj_#l tn]RJ14}N (DmyS%*iN ma' r,us.ϽT1/4P_t\Nz8m1A'+sC}w?]RWbrGGk&u9lWq~3hʘ @;Ѧ(<ԝ% Cslh(ctd`AڗQ̤Th+^QĥGJ]M;KL` Z?3X "OAQZPʱC:-eR"+ȍ|(jCXWQ m$ 0TWK6պ o`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`&`-'a>??IENDB`ic10ZPNG  IHDR+iCCPICC Profile8UoT?o\?US[IB*unS6mUo xB ISA$=t@hpS]Ƹ9w>5@WI`]5;V! A'@{N\..ƅG_!7suV$BlW=}i; F)A<.&Xax,38S(b׵*%31l #O-zQvaXOP5o6Zz&⻏^wkI/#&\%x/@{7S މjPh͔&mry>k7=ߪB#@fs_{덱п0-LZ~%Gpˈ{YXf^+_s-T>D@קƸ-9!r[2]3BcnCs?>*ԮeD|%4` :X2 pQSLPRaeyqĘ י5Fit )CdL$o$rpӶb>4+̹F_{Яki+x.+B.{L<۩ =UHcnf<>F ^e||pyv%b:iX'%8Iߔ? rw[vITVQN^dpYI"|#\cz[2M^S0[zIJ/HHȟ- IcZ/,zfSYQ|w>;=<@:OOԞí<:nb-1LO L@``*"@: 쌉)ڎl|*׊Ȣ=(oJp77&G7+};{S`o֔|OzWS;;ڑJ-+%l @R$X)H_lHҳҧNloKYJ{Y1Nf禠0# ߔft6hݴ2nN HS@WhXaU0EoJ?/MOA~~}|9t|%{SKB b@O  }p<4UF_yu |Z!- }>]@}P~8_5*Z@ע#@|-}jza*)zhu~ dO*P;9)!t~~6O߉[HĞ C b*'pk_."x7%ؗX'[biUǧ))Zv"@g閁,Gvq(j* PӁ-,UDߊȯH4'{#it@vm':=!콲  P  . @h?'6_EҧߝL {a 8K3u=FL #@ HpP48lS'EEwݠV-%M3G?)SRMOKNh@]멻L q6ƭ)A`#@&H4aF ngg_Nul() Ne\ @[=#@7F?=g`?Jt)i:A.;X@ăiX1yl:o}O:)_cG|zu1~^%@HTew8/L{No  #~ܐ~׼/=j=bǿ` @$*,Uhl$]?/Mn~Rs<#~}).{]cq˧Sc @`7W#Ȏh?/wg_.PU6(@GEh;"WƖNMߺ2wAtìifձc/Hl P v tXl{tR~I+'T] ى)Ѹ7fT}H HT~u*  E/%iz_j'P&:ֈ[, X!*$ PT!p(5}铱E^ @`aiNo=f[E腢2 06<:vج_16uȋ[Kwpޱ6n]-t#~j+pCE{mguK8nxJ4Ҋs4& 0 t_Ey~,H Pb,: "6" V 4b:M_K%@@Oߩr(n˺ؿ?u(hX)=*- IY?M^*~YPI+O^;*=N t/ н#htѴ } T %"G=h*#@@$*>O`wg"mz'9G6nUf o(L@e$*3TJ0\;6Hܔ^''8w h@_ 8|L>o -@38yZHh켮\m _@`cD`WY#ߟGE%if @"R"_҅ԛ'c(!@TVPl9LHo}۟J-/+bKR#@@$ x+p,=WLm @[ѸcK/O @L Xc ht /YBQ%@ c;₈톆 @K .N@5fV_{eŷj@Z喴`G zz@/eE@Y$:2E@OĦ{M+P @@w",ƲźuN pB@ cZ?fm#@ 䝴N_:TQEqGM Uઈ֑|v]#K{V*/ @YQ ))W^̺  PA3*8hL7D>$$'>] @ _"b[/8I H  E |+b#yt~6}bs"qW F[q'м؁%(hs 쌉bb5if{{R3%~8u{/KV- oa 3yR` @Yt~D@x @`HCW- kGhL:j',~s_LD>T@%H,ϡW`Ol\6?+M @`I{b?ΦػI%9}b XذnM,{UR)S:/s ƸWFY@6!@`3:?0UGz)pk5`^*H,EϱDΊ?V/ B @4ί~#vGJ, !@v&Pk#F }KYd-G^ iF.v{_  0F @ّi/D PNbDbR rR@]GVTpl{J+ML/-i5 @ t'I跰 4\pL/)~4|}] punyi=J _E`l/"{y fK @it[F\6nNO @T |+bŖ[\X$_h d]$P-#tD @3Y?<?E P1Æ |+b\A  @= kZf^ @Vfj8ub1S#;w @=GcGcu|+A :>Ucoup|K*P\35Dq-1N^$@fTz4=Ė'FW @ _M~{ۃPN5Gc"F?  @9kVdW\[쉍kٛTF 8̧'Y:^!@t/Gy н#M BX7ew]ٕX@qf|ّ@)(hK;oNSΟo{ @.b+://4% 0h3->KH+-BDL @] yd;[*I: ] X877#._Q$@^볘zuSKW"0$CDzh&-"  @ZѺ,.Q; K aɫǚMbFd߻CF@G"ѵqW:ˮ-0x] @npyp7ٗ `pj"`bQC">,_0  @! ply Cl 8g2a m"K|̰ڠ^ @R:ߐ=bUUp bGGGbi.Q!@J -,:/Z;?Wi аr /h\5 @@oGmJ#@` ' SĖ?ɢS{V @X^\k?-L-#P_wlc(N?KSm @& 秧|g0`Xm,ڟ7ru8)]X\F'_}0* qC腱דūy @%p<ⷿzWC@$pvvL#姼K @;>ىV߄B@?[%ñ)ڧN @f:7Ng;,U@`'0@v$&)؛w* @ kfL0%C?X=TB`Ol\*ޜ{6 @#~ػ1G;$fw*E #:- @)) rY$F-qT #&s<-`#w2 @z!G) g dwĖzwoҋS @@kMqu(N KCD@7[`g굱\ڕ͖{ @@:W?0q{kR> Hudc}i# @ M%8^-ݛ9@OJϫ@@qU\bUM# Pԓ ?:4}fӀT  @5Xq-V\A!@; `ҍch)} @#-Ɋk,l>!Pk%PaA Xo#@,TiqZp^k@G_$p06޻K=lA؉ @`Yty*=ke@+V HTl4ww%yoZ`kV @@77wWOws} 4IMm}J`L^َwfg @Pk`l}PR iԊ;#U] @V"gq-E_RCM~_K.N@r5ݵ#o(53Z2dǖCn  @z"-w8@P{#p 6OE+?7%* @ Yx4g6h}8@Z8\  @DVL=my캩~# p­YC)࿆K @;Zݿ;/BMpM 'N @ |)wY8`\oCڑc=zG8] [.Ow! ЌqSŖES>e_ @4B kׂ5a#N8×*~ѿ!MzT @( kë5|z@]<.#s 1zAlӔ}ɜ;z @Qyt "6:H F{:}sʍi3s%@X@{c E\"PM jV/P`olX"7Mz @@rC$2M<9_7L Н@ >s,:vH{@5I+8["Nduy  @,ЊKydO]| ^S+^t`ܭN7,w @kZ\\ "P^3;6Z;b#LO,p @DSWBB.uIHKn,Oz= @KHkIˢK- PQІ% 쏉ǍDӣ,0 @8)8g]=/TT8;KpL>y$e+ @^ dkk⚳W%*z{"pGlyz:ߝ [ޓB @`v門筊m(#-<Y)Oz[#/ @L`yY "0` 7wsJLD @W`-EJ( PAѤh=%ʻ @HIۋkD误{,p0 -TǶ#@X@:smcH`hBfE. @@~,"աd蝥(+#ZoKU俏Ί&@Zh6(h\!0ڿ  @HIb{KB #$#|O:INGJ @HIrUl%V@CacɝhY4 @T@p+:^;m H4pЫ1v?ں F @w ]!Ble퉃t @'Hm~], Pѭ`G"[Wk2 @i(mSߵ*v He$#Ǧ1tRN @ Pu(ன8UC@X^c鄼w; @HIogq+c﷠@k P?z"k @ ׸Z⚷n}ӟ HToj⽱a]+V}(ZuLg @)P\ƪ-}0^c҂k8 @\Ck]ս Hxpܴ#w>>7 @<.Oy͗& 00jHQ3N_ @M(ω\\7X.O(x45bj?ވ$ @YR %}0o MPl5l2^#@h@qM\\7:|3?i1>o1Q @ t^*vq@=@TcMN4?= @#OME.c鵞 @X,=ie~Gؑ"$搅 綣tM,({ @h@Z`TL=z]캩Y=A Xp @ x%@Zf.kv H,n ܜbH{ @h@q\\C~ H[#.65-wY @ ]V\KK(ġf,^\x_z䟍 @E qUl"uYŋ=6B:VX@ @;y虀=TЁq+oISWN @Kt:vkm. P[tYפ/-X @ ;Y{s}TRS$:=m1qn;O'GUQ @ 4 `TL=z]캉X`)za];ZW  @#P\kD=ŵwjPjS$2}ghX, P"  @8)]P\{{e*)q @ P-|*]?cyj[k- oን'6g,F?N5]s  @S u4])~ n8!߸1bl,F&w2 @@qM_\R@/묘x]Z8 @ kn8!hEt @@^&֯v! jwOk}xvh:C @.Љ|DkcN+O ?t @ŵ~w-H@`ALimL1tQ}{g @k#l iih0 @h@31@ X4|s 6\اRW4zJ @"^:n\%`@}ޟ 8L hy}{ĪF+4 51i . @h@_xa~ &_߰n. @_:v=^Bm$j;ح|& { @$G~{'򇯋]_nRW4d2I @`8hƈyvvM$j2u1 @4Jҳbw- [chmDn@wu @xdOY;>av@冬5XYY @,Y MK.F(,qwwi8 @&CC4M[j:b9?ۊlMM[ @X =@'W3J0nBȿײ#@ Po"(b"wO; b5q5. @ g"(b>W!`𐈱Lue @ PkG:n\{ٰΙPrIJ\5fTpGjEM & @@;+ݍ6^*&~5 6]  @@_x7|TlGGӧi @!wnF\sQ_X6#  J  @8)FEr%_^@CtW'c6_ @ @`8Y4u8u1nX9[1 UI @lk/%`@LIo @ GU‡jk8;E0J @ZOH1͵! KSq8c+nb@m:N @ @=wb3T= 5a  @KYRs 018c Ŧ't;q:7 @(@Ow=ה=Zqw3.Ri],'/ɘh @s dY|-b\{yoxóɘYds'o @ @DE 1&S)t|?ˢY+ˈh @ ȏ1 ?ƞ0`]֑LOӴ @ @jٲ,FPV7%1Ԥ+J,!@ @@7Wws}//Wd&kّ @ȿs(v?q{)FPA_(/рh  @K< ph1bΏh}.72G @r tEtbArȣ[ f @ #Lӣ$ %[c9OMiJ!@ @@yXyӢD`ci~sP= @&Pk h[T@ު!@ @*ت7 h_* @8MjuZk,** @@g%^FKy'>Wx @TX {m1 wM09_oJ'@ @@_IptRbyYd E @ @ETPLI;!Ч6b$rW, @j'PPE,UC}cTb  @ PCX]+A_чArIJɘR=+ @#s"׶C꘩}W)O @ @+>c<uî51yS:j#M @w >;7qH;b ]G @@b?4=Dv$&G* @"߷;}uO6)࿇"@ @)Z?W57GŧZ*ncT  @-fzC2U0Pm A6 @(PlE6&Tj y)b @mCXؿ[ @ P ]1R4c`:19 @H`$Z.QsJaHƆf14` x @J%7ƭ,UJ38 @(@nf0`Gc[% @(@g߱}ɈK!5 YGbx @hJЁ7P`ve @ @@_Q6}nz؈rw{ٷC`Yq>?r>"@`V<=.-Ylw=1V^x}K?.P|?:{Hp-0ݏohowU,~_d5@?:qbYGUv ]#Wc?BkXWNJ__sPyKhM]w \/b bOߙ~OlO+_K"=ٺ{OXw8Wif @`aY,}v@J vGD~4TS㝘:'6kYMzGMt_EMZnϦs3eMzmjOwg\΄s* 4Y e~zݚ5MfXTߏSq^f\"@"_Dҭooёn? jbTmul#|vB`ͲU,a _9˗X I |~ULHzꙍ.x&ڑ5:3O蟀$@lL@u_ݱrUhE42n^*N@$7fZL@Ni_e52%6>+"T! $p2 0IW& ?7 \qcLy[#o"yB`: oK& @luQ.[n_],;ۃŵq3n5-Qȣ€ @)';F! ! P"KԤ51 $R2I-譬@񻫘4=Kh,$}YFjHTcf OgV\#;cCZ?+`#@@e$*3TJ@; @vLX,HD =#G@f€n(h  O hJXih?)&:&Ύԫ3 ԧ#;μw 0&|!Ԯʹ\= &vm{aԩ"?V_V4O.. m" /HhKH11SkȢ @`! QJZ= J c- [`: 0'@`N<$@W1dmZ'v˳hݫc4R`IFN(t~ ReZ'FU)e;H Y-.p2OH(P ,2SOL H8"@`Xu!kOr8gX'z  0$' 0kuhɣ%PĐ)sMkhGf- \h8~ LwLp @ u%kD< '> @p J4 <ƒL쉍k (׿#D@F PI{cÅlBG0ز1ن^]@zTC+ L'>N4s O f[Y@/0iE\XS$2 :'4YP.0g 0zŘiVC=ݱAYdGop6vvj|6 @`XÒW/ t#0'סH:P@@u&@uGP . ?wX@]b'n M[`:fFX+ t Py?,_J#7{AjH#QC . 0W TEdz6@bϑq [_'wpֳ\kӢamaً ~򡏇 P7ܫJjfh;*0ЋD"@gH?6@WU|!J'^gZe @@Lk?ΐp6(t~V'#@YdR * M٧TB`z&$@%J#) o5z Z W+hܐpE($@6iJ]")͙#(twy7g2 d*b2T6*?2_Ynɾ($2`?,' 0"]/J= V߃+z,p2/g#@ d~+QM# @@]ٝ@=T  PX "FZ˜9(p2 jKr8 hŴ/>@;EWĤ}W2pxIbU]M N|4- ( -Xt~7t$z%G,b^7r*hEf u @`I9pE *=jLZ@rQEz|)(0hD 5T]"@)&i&}wDM{tZ L^jjˍύ,m!PI,Z޴J]YkdKYj'E~۵g #Қ*71PuN0-%@Y#]?(ϒ{oHSIشu%RjяS&( PQѦ* SBFN֦\`*r h$' _ 0 MW@٣1$@N&֬@{h@~V[h_iӪV3)ς @`Z`: wE8t~Vg6&PĦEZv՞J%h]6WgG(ߘhQ!@@7i?}xO Htof1zI@}h?JT "TvJ' @I^V~eK?Wf4T+FLƈYĽDAHTd4Y(~&l PiVIlMKW,F @$H㍆2кI@#U$=*gv @`Iy][mVh@b %2 6J $GophVLƋ@P+ @`6$@0$l<^t_, @@VW"pK"=_$@}ROfUEU D`$FM٤X$"VzH  #J0ңh]\LE_N`y>TE`F`c}"qWF~`8s;^xȢU鹊x|G%(hڜ9yI 1kO,V @@N$k74Za\-ֱ(]4M fbrѹ8)=ZG ڏ;=kE{Mh8WMђ+-@}{SȺ2?F LHU`Q87 84D T!v-} e.) @ w]_< P kXnw#OZU @I]m&@Y9i_  0@1,oR'ΉMKNAiFSC Ps%j>?6 0#İ(uy%X2O@~cZ 6bK =-u }  @*Iv X9s8m @Yz}yR'=N @`#?0K sW?{6 0@a 5--yɨh4UD]}{ʠI>M p@W-P֘6=\۵M#rLk4G`[Y=m/[J4rg }wXg@sclks:J'B]yE25t\kO6OOm(Ƽ{]o  0xlt&|sXhD`HD:Ӱ:@c%Ңk9TI@J h^mYdݟ[ @E H,B6  @):Q@D^:  @@oN&RhtFVo Ptn0Ta(%0Hb0! 8*)0n 'I>XUIw(X%'nj%@u*V Ѕ@mW~=Vn␾Zh,;=VGHl{0S.b1 @a ݆S/UqSk 0,ϕ"=cg#@eH8@g= @ H {) X@q[f; 0I!wYekl w gw@bR8UA] L@V.?=Q_qȢcO 3x(0Hfp! /8h) pf>g~; @`?v&@ +--X/i0G?aP9,H [C:Jk5,Y`z&@ @dʮ 8?] 0 bͽQlu&0-g! @@))( 0б[e #2źI䑙УL1 0I8g&@uK3`][|iE _$@C8ԞCiC+mOH3C=< Xvi>. -t@nHZ. EU T |@+$RCNo @R L'@Ug$ @`euK||cj dI%,@ Pq2źY ?5F9j42`M5v zTtd~cP (S[3C̚vں?Fj!PX7+ b2EH r=1t39 ynYfHv 0 cإT) X37 @ӷO0`!loy^+; @@&ce)"0ғ; 0?LoR @ $-ExU=+ 09MAߴ_4S`*fȶڿ @3Iba@I+?!_HtЉِ2 6 @@}$[]"@ %-Ey)!?- ӳ 6}/?Q 00ļHtq`*"@CNpo( N ֦vJן!@(O<#X@^ ,B`a獽 @FMJ'g>+ PļH g? P'&$܎<dɒ%@n `p/SmTlLH%.Bn~ߔ@! PCs$@vh6b{`˖ttʞV$Y=fѣswgo$7Mǭ @RΛŇYr,nM#>%o"oۈ,E 7@D E!@ -*H[sx,[ e . @@T7`> [ӷf"Ի|3acB*][o[_ P 7gs器4W&XGgB,i; 0"f\k[ UqP7C+X@9`@7r bܷT&wC! $M9Iul 2:_ @`_M*Gs躀y_'@ ߏo}( vK'?!5{[z @wy@@w<噱q]O^ᄋ"@X vE{YVq{]HAuK_7  P74aŗ]'PdvENgz Oe{Zq[>rFai wuk/PQ}j(og6:ȩ0HLؑe.}v@o_K B@>լv,t7=ٳba_.GtS`ţ7N85f︽PygDKy ^{&uT'@e[R ~Ku)0(<3ZZGS^&U/;?g͛ﺢ  Qr )&@>I(H;{;4{B ^U!0(1`YUyͿO3; G_ԟfPmpu \sԅy"@JGy9俼B`.$@>}3(ĺ}:YBA77E@7u4;d!@ ԞPN *+ @ E !FQXl?֘F?6X~wckg!Pиesm33ȡp@cSX@yR&1sU|Hz"VDG%.3Ͽ"$@RQKj/$bhk &]wx7z3+Wʭ1#P{9\cl,q$0\p=9 (4b!@i7:&0HG .!P{ ]_1R$@" fuؙYDu;[xJYx"*]wm:z[UGA`p|Mcæ?bu PX lEf J, 7 -#, ~ @` @ @`WAfIwU5-;"PK6K{PH=sb-9d l2뤊^K |3?z@E pSu`QjV&Z -<&47[h O@>'@f+4^`g @`;4 BȠPLv^  P@ECF( !] @`E \ud^HY)O"+h@Y+Q[ @VZ[O^@ *p(h Oے  @`I Kb'о1գn H=zOr( @z1=="a Hi_^EL,1v @foT]L@_ (/j\{`}KX"2mN&2/g濦,2XYw|(H :*ONODn>Ϟc LnK^gV ǿ- h@nKR9alo  gL'4T(s @/ 7+X1V, gZ#@@E3uG=znXQ}C@`@~L&Af锿[`,zuGX=ҵI  P@̍7O==<\M`OOnjIs \DeU#H] 4 bȥ5@CA=i',м]!Z#C[{ !T P&< OqtƏ )s Ru:tCP]X?Ʊ @@r}k/"\ЬV,~*W P ܷ@#4kp)W];  |r cQL5(Py5e|Gf!@ ^Hr @sa @` @ 0AfHGkVwe~`kEmJBXIJ%܃"@sϊG>RBh@C'/}i㡪O,P}V}\[F;;&@Jfn%MUNH ._:-  ~k/ Қ#JG "@ F TH3(x#P7ϋA #@ (ݾami@v&i6%.}=.]ê7 P Eˊ /ZhtU@B'@@Y2( e3 S 4kګ <y(1WL2|HdXk}kf Xd-F&@@_͵VV4wzDFlZq~tD|?L>t.mY- T+0r".(?{h|B5]> fRq;t (FLv& f!@nĻtA!0(pGg$j%@n)UE$ ]%X`t9">'N!T.EΛ ,0*?4H (tP*x_, =3vt+Ļw֗ !E vG3O^, MޕCϳx{b @z"'ku5$KΛIwgN 0[oU]' ^M`,y(EH< P"@Z@Z- r,fgQeR qX @@'#/e (81`7 @"sGA3{/o渉i3v oɍ;?(澕 `h /Y7hԄJj6T"f" Ej= @13O+ ]xh]s Z%@ " aPxc6W;JݎlcHSa<, @`)YҍJnY m @@)8XCn 7$U )' @@Eƀ м$b&! @ =E`o؋ x,0{ǵ),̊$@n9nV3%YUG9E (T݆=6> @ rܬ 7 0 b11Բ ۈ{7> @j$iv@0.}]CZ:-  @ 'EFX$k]HmvngWHI > @@=" P~JQ @r Mhtc@En nߴNN @b"V(|%F,S@` |g @ 0zrfF\3H1oh{P 67cME3 @"@"@ Gw[$Z#@@eN[涹]Lx@nljx jEoY)PT'Pܰ-Ņr!+UXMOxRjE T(B6Ϟ+4G`)l,h 9h[X޳,E^d79ai¶"@4eQ lvl8ϝ0jL7 o%'-/O9m2eZ-qDAL -7sDM6 fYoi%Ь5Afq̦ P@9mUu v  P@5ASL`e OcV%Cu@9m'.1wj0,D`cCY Y:5 LӘY @@rz, e%f7b^#@JENUT}qPS-r+aog[e7L @@;A 4|Hd@lD1Poh$ @ e-CrNx2 W \6X|}^M/ @fj`6_h._LgXl  G@'@ `Sz5Ym(=yth*p-{]+3O0, @ Osج W9"@"#?$#' A ]{Yz a L@`d;w\X. @Ja.%9 @@)0`Y3 O b:ͺ!6}=o8F@`#N]oMY.3"\5!g 0@ɗ.8<"qP O @ _"6P4 G  @A3v՗kh@?F|h݈@wSl*vF`俚cN+ P@Y`)M ʊfLV(õh@o~ԃ45i"#?XG-ww~[ҴFia@^K\W tK _mB11~guM#ؗP&)"ڈ//Peڄ6 b$@ NLt _ȑahWsc )*N { ) /а4H`, aWjާ @PؙMIjcf\c[vYaH[7:D lUw{1oS8-b:ݗVvF [Tg{ $Pe:ݍ|7)+". @@Mm* Lh,T!r\_zeXm%ta~ @*c(jAsψ{t,%}__k׵wvߖLFX-H9j>n $ /0 Iwoԫ N娍*<9ܔ>%^ @f77 |E](nߖ6*y`s/ @2v~{f`ւiՀkjhbnڸl}@*({rL_xIE-.222V @s"f @Ǧ~:>@ʘ- @[MW$&/K3[ @@;~q13S?GB$lMNHOZ(4h @ ]۞#1}ѥPz>X@IDAT]PKD4Zf-~;U.~{ Oi=U|`[&ĺz~!|HvC`2'_j Rte%@\̸O_(7ݿA[>#5# @@3f@#g.zh4qxF-MΈ5N{A=[nY cOS_%_  @M&v폮r& _`.%_Xucg~~b!'<.M[1s1{ 1}UĖ/~56]ث @N/9i,F2^:#@ +1σ~bE}h:zkW^+&FS7yO:FCHh#-5+Ɍط@MnL5 @  Q#oXJB @F/Pl5/};i"hX @.r\tW]c %q/3͡) @4Ytot x?/v @hX4CF;0՜E @ D2,s&ƾ#FNc4O @#(sS:WFJ(=q  @,C e?Mb6Ve FH7T(BlM P@z6}  |܃3S&W/8 @'P\r|?ik/ة2&ub%@ @@҅9[R(,B%@ @@ڑsHL @#ؖkn>wMTVqAej"@ @Nfs(l'0 @ @9 'lMst @ 0ۚWa8><1Ư6ǷOA@wL, @` :*0qÓbt53If2ӖL? @ TQ|w E)7^#@ o=>#@`8h/T*жUkc|xG @ @uenYmX ?56  @ @@n1h{4mPeg^  @ @CƢTwU @@EѺGͷ#P @ @3eNYmp 4C @ښS:uFEDžf @ @em)[YHS5׸eǠ @ @\)GL-oe⣵j @+\1q'@ @ZڜK"k9b4J @2l\  x X @]Xu @XtD%cZP[ @*P/hs[]HWs@`}#@ @Pzgʮ2I >}dz  @v wl}BpG.8 @ @ 7cE5xZ_8-bp3a7G @9c;C{ʽުǢ\^ӪN Hp}YkFJGX@^gV2WKH9cPc(n€# @,\˜q[4wNi;L"'@ @Ql;1Q+HwtĔQB' @*Х\3'cѿ~ @ @UsLtKG{ @CZ؉8Bb+cvwy:^K.U :_(2Gj-ZS3[nNwx[t  @.ʒ_.sġ8vPCã2> F @*VDS+^3UP @ @Շluؼ1   @ @ cO=!8ֹ@؏_Fi @ @z_'sN=`Q?8(xM`O_T#mZolHw_gu3NGt]< @{|"Z'dٹNN @ @N٘l.a14+v @~ʸiNm^Wϕ78 @!W|+wPp:~OWZ? ,F`u)0weڜxvF!+:v0{,$P7T֖"f- h-9uFC,lDNw\HqE`%_%N1s5N muՎzki'w4#D@IڏFslkG߬67جEK%l(,L}iH+%{wA/|06GDaN.l.b|hPP*DzGg@7|<@QŭŦO5*i\ز cQ7я`QĦx2>hʿr  ii;:A/5߳:V؋JlXţobdD oy <0|3DUl7?10ӂԅ! Gͮ=!9]6v7Pܑ1U^Ǝ @ݦ-/+q=7 zm?/}!|~l'b;V{(uXA  M`bW/u'h2&>ѸeE[f!@`wt__~&ձm@o./@/ɓ.zWb/zLo(o9b!@{y{>;^m|⾏0blV@Q^kO;8WŁK(?˾%@`@t~׼B!`Ge=o~{lLk;9[U@/CbQGtR`1.MxOv?YsĦ\ {\럵2ze @/VǺkNzV~qя?,ZW1/g΋-׻o  ?NXu[zk>6ane[bW.}!?{~*)omuu0fx]0vUk^ @ tr=S)Gᑀx @MH9;g10VƁL?zF8X͏  @ @^K7[7`㣂o#k @ @n>F@`@ŦKnj~L @5 9[|cUP1Q>Vc @Mm[m4aX0]N[խB @ SG=)6oɆ6dnj @V}+,̩\mRW& @V(f=o.X0Uĕ蝺MJ @#(9$6<ݷlf,b@{"V* @Pşp۵"tAzĹĪ @ 0캵TXtћ؀ @,Pِw),rN7NdY @$>_c!3QXPs7 @ C }(#lG @H31ﵭ1,{,j @X[%K<*b}m×+ @ @<{C"6c/-P Bms @ @&_a71 t/ awvA @O͏=>b ?.⾥V 2.+Reej) @"0%,K3 Ft'+  @ hީ.zS+`,K{L4;[ @ @`W[]6`᪈X]G @ @ҵӝ ZJf,ja+OGyak[ @<*|I`m~{lT|? @]̥ʜjkR\T}lXHwc5?&@ @]_|Jl~./rHf rlQ[X @ @`aUrm (,kk1ۏ/x+ @ @)\ #KFF;qX^zB @D'ǦcϋYǏ!`2i:pB @N b @?£ u!醀' @ p@y'F!`(Tgwxɷ @ ]@T͡Pi&*hJ @h@m9Sn`  lLv*LWԜf @ bӿӀ@[@Exxl>o+jN3 @^̑ehKt kqCWFtCWج @ @El~wg\KZ~e٭w-Md @ P@*kNCߦi.Vܬ @ @ʉ+slH .a/u/\=O^ @/0O?nEPx#֝.xNMk @k? ՠ&X4B @EʅЕOF䰘j, @@ Ѱ6M<V:c6 @(s{cmic~3w'7SCZIc!@ @@z\LRonMk3#ؚC< @F%p)'ElUo3m45_4i#vN @ʜG_loW"(c?n 4 @ .OhW32_M$a @ @`L?W#cʈ_c_A @<zuhnni!@ @@(>?y;b32߹R! C @!PlM3y+q  5#bs@e_9 @2x˶gYCr @"d,}(, @&>|Dyn?Mfd1  )_^ P<^!@ @@nEыk$(oW{_v  @{e˾H@ /Q!Mky @99K]^C,bػm11~g"A @ @`2g)sy~䥌0xPz߈uF  @*Gb԰UXW]ks.+ @ @`O2GIY=q2^ A/97-k @CQ\xśն-1GvUmh @c PhXŏElGivMɦ @u ؿ_<)bK]hw 7uc 4N @@ʜN#4A"b]/-/d @-pc84b^f4p_:] N ?! @h@P俙#qc)w74|a @ @tlE| OnipN @@c[5&\>@@$yYxK3f"%@ Lr2if.~l~wC @-nSDI!  @ (sm70x!&G3)xY @<~k,6b7~ދ7 @h@Ѥźw3nӸ|3&47.p @ @[9Fv hf,. J*EE @@te*… PWkt~!  @ ʜB,3$eU1P|!D @@e.QyG)`)j kbR W @d!P'uDl!p1T3ʙ_tӎH @]!$3`vXVqr#$ @ Cc) 0rzxEmFu @(ny( (,W0폋 \*e @E(RG4*Cco/- @ @/zo=<&>k|,bӽu#@ @R#\tEl~Sj67/2V^*|u- @ @1>chF%`d3ѱT_&$ @*H7%57PT1^Pu#@ @ 4G{H$U(TYS1t+3 K8 @T rfck*hJ (d6 Us\doA @@.Ŗ4#g%"qT'PuV-G_UP!@ @`)|a1qHl< ۡ&{I5i @xq]k~3Z#3&^U^Tk'@ @`9}1`#Uf?F#XwX]Ћڑ7 @*Hd?iG+mXc ݐTPF~H @s}fȣL7tMлGސ @LxOy_YZ@ 61TVF @(sLQ6tЁUؗ/V^{بڰ_ @F-P=[nuK33VDZAEEO @ ^N߼u+F݀7OcWŪT  @t[ }Ļ `>cY @ @1<<6 `w/Pl/M7|E @@{:OJsR{2n5")4 @9{y^ 7x} ph#]C ` c @@Ey_(ݳ OP(d3rd,eo"#@ ]\6&i4k˅u_NM?5I @+Wnq_W:0`c43'N٩, @,Ϲ%Ձ%l*>ڏGLUج @t@Zk.*T܅& R?_ Hb!@ @`Eyrߝ=t^@Eqx{' @(ϭs pآǺ~ @K(Kv0`>-Kutvbc @(ϥw=B3F]]|uA  @,F Xxnb.%,Z|ÚSEol @(ϝsh.PtW(V?aU誁~ @X1 [U,`l |nid BC 9\xK"@ 9kS%,zj@н >84G{=&@@W N%PvˆźO8 @] }řqδ LWJEVfѕ>' @@MsR_{y@(7`:N vs^ۇ9u" cͣ/=N @tB ֹ{1NtX'0 Np-c{^鵞 @\ιܠ)4h1~L4uQml#@\<hyWuA .zlL^HB @911ym!@>c!ԗ#f"]/(  @F svNۨ rxDl9/F[  @(PLG]>/u6csQd*LwA  @"PLs XP7G'iM5 @&0H tD<I @@En*=2c;_ @#(gc#6f @Oϓ7z YEL^|== @stq? (6"i4` @H,s/`Kp%٤Kc  @,K ]GG利/-!Ѝq=.M/z+o  @Z,ϥGŤ-g]_@`~v@X{X>k;]]$@t^ %(^|LLy P cV'R @})88..h_d \8GZM]#@tY~ѱ.#;Op t^C03]`y  @ /,$XZl>cb=1܈wzD@ɓs 7]vuN?#&OIׇ:Oh@yNN.Z] 0 M8wSuӣmph@Q#ttLI;{- Y4 "pqyXKzBY?  @MHOGdƚcՊ @) E`lB;h@d*'\~ @(*o( *hcg' J9Zy! ( Ӯ+pR{1yjVR @HEyV5+rG=qj.8KOد: @L?_>:&_;!t (TN6\@+>n6G @MH݋ӎ-ՔI \FB0V?~e<3k\&@4P %W) .*Pg> Y挘H0 1Qzƺn{~7~"&@("z|T~ :T\@ 9ƚb?7'j @Hw{ّ1(4d cSb(  @y 7PT'3g,PqB#@d.?Lk27DwYMש m 4a @u ~?)=o@Tr4V?+?). @(ccS&"Ph8EƚG;>0FQL?}\LRc&zS[?:X@|N; @r(ϑ$x(`@GU$T'. FF`WS9  PꐕG}i @2(>3?u\L_I@ tbu2'lL<9% @@9І8^_6.`#P_i#@T.PlIOb*oZ jZp\zJ@e|r͡h 0*xScQ5`[%6 qiCvNA M{b%5kf[u \kN+b읽=8M@ڣc⟇/ @` C@ rc/Ӈ_"@T'пx&^p @O-7)"oQN5GX@QE%`) 0"X%0 bK1]؟} @F%01)̨ڰ_'`lM`GƖctGڐ @%D/Ǝ/ F.`ȉ5@`8{[wph/ @ iw.wO'@` 7 \KcoI |Xӂ/ X~eo!@k ]q%ű;bs=||K/Pdx_?V*dr#2bH'Ho OlYttV eצBk-Y'@`n '?H$ۇ @@7o&`(@{.U'WzB!>\q1}U7z,T@`R#AKci$z1v_  @@ߒNp, 2j h@yqGL=e.?Nlm^DL Pl-V%]o}$t3ngK(V?aETud-P|b.^wll.0G@ Y 4GXXW=9QK"%svtlvLoKFkZ(pLlkcto7[E]"@ o?aLt`E ǚY"z?. ~҄A#4T(zQ|h*ⷎ[ a P@ X1ԗ} @ ۏSdph@Lr(֜fr% @@S]_ֱ1uFSb'y @#:(OPO$O)~#llT%@wK˿LEZ"`@KR7&pICz4+`Un'PLEELh@GP2(V @`>"i?Mߚg!@hFj!pqyzzOR1=~[ ayGԗ:T$Pf&fpĊ?N7 < @∹7O?U&@z͵H@)%E(0 = @qX1 x @uZ"@~"|䜘8sL=r|E'P+ʿ ^ H/f.7Kn"P>wE=1"tI@ ytR =$־!}$k*vA  @%ŽD1gElnIt (4|OF4kS ~]!#Oh@qO1EPhޘ@gΏ8pUץB:q%@ w>#65B&@ d]$t_ԏץ{<?G ]W__K3zB mY"B/D?xm/7(pu ؞m*%o @:~>& 7 <(K>*Pܑ}17'E7&@ w& /EYkQx\+: @OSY)(C@`@~L@GXS9˿>9*EH(.-1ۜEJ (<+4WwYX۩ '6"'@ ]o>2~*Rd le9 \k~;]pJfmZ @~V|\L]MPhڈE \6TXL(~\L_ՑN&P2. \1_N}u*xr@}&@;K/zwxLlx@ (l@uHXiͧ<~J-Fb_N> IDAT / ޥbů7un a1WG$7Ѡ*Ցov \؏_ @@R)ubo6C:@e(,ϦK /kckWG|}E>+bsz_@`~ qg"kҥ??t [ӵM}z_Z @`W]5|M=.uߑN&_.}U/zǾ%@ X>Oxyg #0 cO7 L*dN2W칎  @:ϥwnwzֵDf (4sDM@_5ߝʱ(MUc(&@@ҏ?Ec0! < mtScbId Rؚ~\SM@ZHߜf?}p6>x|LmޕH#P9 ޅi4+.Jd,pGGS|<.O5 )P  0#Vv|L^4&04J;"@@_U;9|l:OY 0 TSD鯏$@ (  P\s?fC5B@43)xI1q{;k脀@'Y' EX}\=?'7K+Pܔ?۱غ=ڞPg,DB\?ӹ|q/p#K @`$ިVM!?SWspr$$N 4wtFwssUP7qO8U5^izcYWiyf 4 w_7\cq3[  @`8_C'&k*)^d}A:/Io'-@lrZFG`j5BbTH <v>u-3tL@c*lٚz&k꽚|?Is) rj;A5CK Bc  @`hk4[_wUgkH*rv:ulD@`+)Z\vp 'BXO/cr- ;#G4޾5]Q9?Qg]M  5F?i%:-pcV^ȩm1p= \ph5.&s_ םF6O@`}2#pjΚn)4K%PS\Sfy}jS+I-ՅZ&ƞ&j [Ǵ:#0JR`&I$3MgR0ؕL|,WvfLL0y~2e/ny-ϯwf%zo{w{7]r9YV&'q4w3q.W721IZخr-8k7K @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @SEOgIENDB`ic11ԉPNG  IHDR szziCCPICC Profile8UoT?o\?US[IB*unS6mUo xB ISA$=t@hpS]Ƹ9w>5@WI`]5;V! A'@{N\..ƅG_!7suV$BlW=}i; F)A<.&Xax,38S(b׵*%31l #O-zQvaXOP5o6Zz&⻏^wkI/#&\%x/@{7S މjPh͔&mry>k7=ߪB#@fs_{덱п0-LZ~%Gpˈ{YXf^+_s-T>D@קƸ-9!r[2]3BcnCs?>*ԮeD|%4` :X2 pQSLPRaeyqĘ י5Fit )CdL$o$rpӶb>4+̹F_{Яki+x.+B.{L<۩ =UHcnf<>F ^e||pyv%b:iX'%8Iߔ? rw[vITVQN^dpYI"|#\cz[2M^S0[zIJ/HHȟ- Icz6uIDl[yOkb *;6[MiydX#TWowQnp\Q?M>H%ƒ|qwQ|E0bQS_[UG,[&&0^Z:K`+ã'Diî+8}ߎ.%pߡ[ ?8[K /*ۈ+W3B\)Ūhm>O̴rѪlFLO~E)]Qn_,vdmxQ^՝ *'RCf-9' ȵ>[+A& 2Ll+nQpQ)T| O}ȗ|+~F(W`h VaHk>?ydQ̸]1Hmc[4Ш,MG*$=%s?t$2vѸ$O̜b F\܉6ï':ϩ$qӔ/sc4| a.K!.^bԧIENDB`ic12 PNG  IHDR@@iqiCCPICC Profile8UoT?o\?US[IB*unS6mUo xB ISA$=t@hpS]Ƹ9w>5@WI`]5;V! A'@{N\..ƅG_!7suV$BlW=}i; F)A<.&Xax,38S(b׵*%31l #O-zQvaXOP5o6Zz&⻏^wkI/#&\%x/@{7S މjPh͔&mry>k7=ߪB#@fs_{덱п0-LZ~%Gpˈ{YXf^+_s-T>D@קƸ-9!r[2]3BcnCs?>*ԮeD|%4` :X2 pQSLPRaeyqĘ י5Fit )CdL$o$rpӶb>4+̹F_{Яki+x.+B.{L<۩ =UHcnf<>F ^e||pyv%b:iX'%8Iߔ? rw[vITVQN^dpYI"|#\cz[2M^S0[zIJ/HHȟ- IcZPC궢EC4J-ww}3;ٙ9&guf忟eO'VZQ b!&jM+:IObz3[H踿{̣T)VF[$78T̫f+A*T|ĵq*k0e:>/ǫ i*@nߌPB p:P63>oZnat p -Bs@-^뷚޽+jP 5#hBՙ08CV<=XO/}.Y+1w1{({@in[aǝYBC y)T ysWlXM!rh;3lw^D+mC GQW~Ɲۀ*ކ>ۇϋ}h*+"yVԢ0EkbudjU(^b1HdɀTJ?rLWopG然뫍;(W!P*\=>:X:QZX>[uX44dʻ|M+]\ xhc92Zr!?γ!`yR۷_U^IG*U hD6>2Y%cjVFY#5R^ B1W:JWl $c꒽/a#*7 6+XaǓHlqg  tD9֞qAy)+O VC.ZbH&*alO8}Nn./rN0&4e]JQX]b*T17VRs_\ט'LdvHt^ǞfgxO}GD +ӫm|!Z\e  &SRf%КĿySpRvU;DCD3F2M  D$JEqJ8 ljؼp>-Ep>3QS.H%կ3郬 {MnJbxxC%?VoW[1=0FxN V84hVX#3q&\z0qyr^'Hh{(W Fnȫ||ԏSh]1{J(&f蚺Ѫ=!+{q,{N0w+}&̞+%?0cC6Xϵ<*m$7o؝CYbP6|)@Eᬝ[pZORR8щ^Kn*JWl+WKsQhD|7#ݤ>-6 kk2thU^Vt0%5`3O͍7JMt4wUd( ng < # C4%5@WI`]5;V! A'@{N\..ƅG_!7suV$BlW=}i; F)A<.&Xax,38S(b׵*%31l #O-zQvaXOP5o6Zz&⻏^wkI/#&\%x/@{7S މjPh͔&mry>k7=ߪB#@fs_{덱п0-LZ~%Gpˈ{YXf^+_s-T>D@קƸ-9!r[2]3BcnCs?>*ԮeD|%4` :X2 pQSLPRaeyqĘ י5Fit )CdL$o$rpӶb>4+̹F_{Яki+x.+B.{L<۩ =UHcnf<>F ^e||pyv%b:iX'%8Iߔ? rw[vITVQN^dpYI"|#\cz[2M^S0[zIJ/HHȟ- IclhC{۰/Ҩ0̴;?"(,{.:^ۏ0 ZObx8O0D:8M9 7W2<Q~4OCҗ3:-~#찷 7r0}Gs}59E9?ƮuN}e'Wô`c<|#ETMӢх$ԐF.)I0O8\8ťZ0lfg[ع}@C^UTKitoSc+vD i):"*OOs fz*byW.YK!B~LʯyZK\#-s} v202ucZ|GKrg jOL%QpCy_ q _3V)Xvս r).≯[ej[X:DעsѽocJ#.F4Kb?ed@jq߂,eȓy\yv|Zt/}@rD">s0*ngvrT\-RKo He|ӿAQ5dvf/RI!}rep*-(Eୂ'ST-vZbN&4R?⌯9RC ӑuUVd %v [e4*PgžtCoޣ{ 8E7$<m\s\qEt8𸏬^T&j A5~{k Lcp~Q.VDme,dwI0Yc?h /1z> qAz.l;H5.<g@oF?y*{nj?s+"ta)9}+p|Z"\Ʒx= W{U`_T=Qm8 n%>~U۹R0Ծ`BӜ,M@a26}Es t`\}k͋-(D tqВv % }d*5hvJ*8&kuPQvCţl"P&n TsCft23AvNA=)7)LN?(j;qC16r}|Bzg Զx!` `-;@(Rm#*T֫ycI+A!>6 Z3>؃)GTefuO6t bxTIRoY4~5ZE!@Hl?𒒪zh;%I)G&|Ɉ@O?L,6c11B>**c@L.x?u +MEܘ)%7r&1fϮRT%)>xkD+dAТ(~ U;>^ 5<mjmPk(z3 5h@%:vjO1ɛ}(!"XCi/(EbcbkNo9hy̝Xz"8F&$K&_e=ؚ؜$}KT$< yċpv#-C߱KtH[E4!:"Ezz) 3˩-}Ne17n2jo>[Z }.ɢ_y ?AYgkͬ[y6a| ~CFA;^a^ZN\Ykf~[Pq/9& fGeKBÑ?!D¶CS d4 {~n"08аt_X"h| x´CFI0_vh/fYGx{x=OqnnwN!s|ɩdtY;&6C:?1ѵ<XA/rxr.xTfouNP:|#kϷ&NS 6X*oXl/5:G  H:8ÿ`%Z__XcXlvbyl@l[ jgG'0A:Cv'VY $K/q"6(X*%; Rh]W0:4'PL㌟iq[,< )H"(bfA3ZKGGs3s*C0l#Ɵ Y#(@ &F{Zc,]mN5g&e^. MFB7~Ӻ ~*U3ݴ !n8,#Ӂ9qƯ ?=6z\9POYj܀fn tpqZ"#dqǵi`0o+Y'wq- 0%+N[uRW8v2z3b8&9:{t?z:wntV=Ơ}ه|i=C  6 CuӲmձJEFXUE:6?%xتc'|[X y'0bUx6oV9 ʰ.y̦z>8ՔR8ޓu\“D)⨄8a?Y/+G`:&ӝ'5T?aq tF,'Me{bcQ!6RIhpG\&;7d? f9>5XdDO8#Y'n G9YG w's_~]E%s@UoÝ)m YO'm':qX\Ut.6/#X]ٗls0~ǬM>fl;fi9N*;HhbZ'xꗝ2bl~S%"[uEx|s-K]/s@PN`h2#0;m@jg#u^e,P_n9]cRf@K1׀1~G%6:IT䘓\ϜCO14g^pĠg5~E'y5vr0%w ЎTϳ;0}8-abN`E@ f߻xs]I(}:5`?NCAtbbnˈ?h-v\FC1NN@? uV{b0xz6LN%:Б?Lyop4E&I3 @!:P-K9Q9?G4f$i޶21KKpG6-:m|sIuGJP_0'ynaM?b&i񄀭bjWog *_)_ ?Ux @oʈԁ),H~-x[-!؅ȋv#]ˁkFLc)[:xEn[}H$0ZH>dЋjLw8 1 d0`. CTCJy}#*7C7ɐimFmho1g,Eɮ *,V:?Lh_؆hۯ.`4<6LO^K^KnK/n/wD6Z3@n1^B+|oëW~dHBBһ8bbVl1"%9ޢʼn!Z 0 v-Dd;e-:Gq/S.ᝀTb~d2HfBpmHKxϯ_qi3OYE|¯`WQӉ#Au$uv F/nw6s_6T'3fepuRxoǩ^Ė]#zN@ %iŝ'MpٵlCK"GySft:4\@-G1=Isߘ+pn0m@:(!0E+kPK"CIo2#N^N>Ag2?EL߶/ |~_u nꖂ[FPF[`0'lAʱ}yK G @x#i. ;][@ $}8&VN kYH'۬n׺`9; P7\v2pc6r nedVh9 8Dz꥟6w{naG~2GΘ^y bvL:~X;l+ٵLDj/ڼj+40RzɧZ1~M_x*ot;>LzÎ;`ar_cϊ Q/yD ilj8=;wr9( 5'5~:$qLjQ!y.yw$g #;d3|9;~CjAחp[U Î'pW'' mTDG`5O:(Gbۜklɇl}A->Pİi/?V ZJ' -L^9vʟ͜V/[N'L%ҋ)8pEnm4"N67dw/Le&D$ mÑ6]=3ak4}"f:LAM@:7Yװ?zo7b٬#ЍNc,%s+Q㗑_o-n['$9c.L:!uC` HbG' $ȅjiY˴m}BjD³m)ưFfDɭ`K61Y1~%/t;smxbN/w~~)351A@ /*Rܹ/ؙŎUB81NjuL O]3X/#V; ש;C'\8x#ʥŕbΊ),`?WYqHk3̯`[;f\sx{?}s~Y/%bg/:q#huJXE1V} ?pX~sRGܰ82]-wӀ!X#ȿ]-2*so~[ho5(cFp5 1wKsFnax m|E,%QEF𿸚0n<<9='㤱⼊m5@._q_8Lzl @^щ)طbnv?(A 6]G'yļ1ijc-;? ]l*y8-INx{~ئ[H\GmjmCZ_P! 6)@F i}E@0@6YBC ̉E)bbN돭Wn=>0+@8-MҺ$h#J{tH$Liq9X\skYh$csz!ؙK\EJoR*i }yRE}R*9RuC@Fv X`:ɉN@LJc/HC0kDӎPpr׳Ckq`zw9ӉЉ؞ؠC V"WSy@ ;XC,# 1xmkۘn).Yu'=QSq#6aYA'歨16aT0CR0RzѭXtX\. &r/Cf4˛(bKPK-2ʈsR:^.uRO4N\5|48|"+2J(/@`z)2hH!^PΓj999o?OE@0[2'RV&_ۅ˹J$|!f%SJCP #~bu;oɱE@wuT}uL}O":;_p[ty8TB"kb[~C oͨSŶ 7A'ww+` JS('8oۍy~&aa(>߈~*@Ж3a‘eW*GuGL iC,nrM2kgL:6"W,n27b)@rOE@pc+\V6~ #}KhF@lFl4 /}l(}E 9d=_g<nyR(G%Q"1ڈ,9 zwL& (&`h~d;yځDXN ;SP !`}'(1mAFz9]""vt.XEԜ;"=eidZ@i |-K|ڢ"U2<4wFy^Bu̼%߈~*AZ%17ing4/DuKi_BD̅< E(!2,BsBtnc-@"bW)w@G\!LmE hC.jfWjW N+ID W–/@ EF#}Hg_0 /F#K}Pi[")X#}=ډ`,x3boz$9rZmE%ӓ@@H_D,r*V%69Q݀@x'ȃwE 8S4hA"#݀3H6]bpe_7*XY^%AUHw=O” <%矒H2l#S]W>MRRr( eUzԝ혱q# .z~/+`,@t/br4L[CO.͟FJTR4lRn #,o] n(7]\>E$.2/+CvP? .!,e⒬c+CyVtמɒ4i3_\ 2QxAk`! X YWP30ԍ|hv*k`o[-EO/hc=aV=M/g1zJGDw<%yV/8 rOwճQj|^G [|z0%΢ȋ:ZY 6bvk\jUpx7=+5\bȫ(A70 (?_0Y(KC|?7p؈m] " ,|MSj{F]AP<7 _^o7!}t~9S`@ N-D= Cc|V>~޺{ @@@P)47_@?̎dӱ9?@@ˁdtžE݇/8;/6;9^8j&?qp]Al&Vx|lQVv]sL>d3%Haz- ӧ NA,CD_E޷tf{9@w.kbҡZGYWͨ;N`nY 8əj@KrnA'1Lvy^A h '{4pMg}([4v2'x"<_%t$գX<]7OC';3ֻ>.d.>S?)"("("("("("("("6 kjIENDB`ic14\kPNG  IHDRxiCCPICC Profile8UoT?o\?US[IB*unS6mUo xB ISA$=t@hpS]Ƹ9w>5@WI`]5;V! A'@{N\..ƅG_!7suV$BlW=}i; F)A<.&Xax,38S(b׵*%31l #O-zQvaXOP5o6Zz&⻏^wkI/#&\%x/@{7S މjPh͔&mry>k7=ߪB#@fs_{덱п0-LZ~%Gpˈ{YXf^+_s-T>D@קƸ-9!r[2]3BcnCs?>*ԮeD|%4` :X2 pQSLPRaeyqĘ י5Fit )CdL$o$rpӶb>4+̹F_{Яki+x.+B.{L<۩ =UHcnf<>F ^e||pyv%b:iX'%8Iߔ? rw[vITVQN^dpYI"|#\cz[2M^S0[zIJ/HHȟ- Icܯit>Mgn5o]oψ̊]F%U~A HR2yz̎K~GuR;v{6ݡo HVojgO2KꑞZcC !*G q&E! ' ZOgG=(s Ȍ]Ej>T}%.\!1)W^^/k^vEjLtzmP9/ez!E=KɊ[hG'5z8 naG?Qq="/r$Q~qu?`DUY(тxc)^%țoY߫UxA p#%}Nx Ж|NV^O@lj&b[B#:A"|w_6StT@\ 5BVֲ?HҔ .)wlnn} TFEEG ^.mo(@`84MҶ:5?},UI'|' .iZE ,)L8dIc8T@^W\KzYZtaIvIMkd*_XI@߬0k |A~&@5R~m0^JgU<~|}GKZ5QL^!@[FZ(ugENk͑eLFV:;鲮}7!}FkÏ?Y($wl=IZX dI@W|Q?_wy 0@0.^@?c=E!:Y"2q6Ȕ-ҧM J'̛7KצHү̴E 7ksҸLC]翅 /kI7K#kh39Jۡdk? 0K2 GTHBpIl6ɤՕBGSKHq#y.婲aÈCIԙ|vNF2ۍBGZFH\HhK@黠^ּ\&Zܴ[YR%|{dn ο,t 00 H)VGfpUQ@^QO>^/$XH H}Ɠq%yfgY@HUPcSfnW+?UNMzDJO/h+"͖B'@:h3蒶ӳ1x]o %$A\X%S@BHݥjп;!ߑ_ER8JYtfڽFgX$@}9dD"踱7vԹO7$C"uO=  i,Ȉw67 32B!h=dE*eun<]:j!8g}g$N+De}~#[q!jU7&TDE#f~[G{;b4Ą@Jgy6gyVDmդni?qH5=eӹS}]18<Fꔶ}tc@@ /9YV>{ `m׈b#! 2. ;!@`)Fmbt_# <6KWU\Z-F(:fһ:8*"gYy"+JB!@ m3fR*@xW:l5J2,1x-/5oAU{G[Q[ˬ}},WR")%/u3$K41۽KfNiSE!{@g4}ݫ3 =32x=#unN@* f>*R& @Lo̘e3cBq0>YxIR9oohHk_8k `}y&M, R7[~sύ0[ `=]+d'^؄H!i"]`O(K &xFYq+N t"e&M ; 2?tIߩ3eMJ ;@VZ'SKݝ 9+ ={4Y1! Y@f SJ= !i@#<'Nh% 1 $Cu8, hiYhEcTwȞ4~޾!$ORC@>&K( Dt/?`$yg*-:L <+N R# ҌW? $@`lIwg@/54Ȥ۴2o@/YkY׬SV2). g>so0 N&4a k c$NEڮ4M]R 䞾~$*% "^l\bwKۿ+e ϚgqI2 Z*0{Z\% 8EMG' 4pN7}{*Cˉ|EV. 'dJP]e!ޯ4qB@=;Q>~VȁD;ns}D,N  蘀9>EV/0[gb` 3/̝8`q  gyVgvL"8-2MnNp*!@ fY[X(hbCYfL1i {d/>nqB0Ni's @B%m`hֈDt4):d3]&yɟ=YV`HsQ0ꡦQ !ХkϹ#r4 2eBх~p[/  4gFiaL0yTLVEFO@.0_GZ.a<:e֥x?l?!@ !|,uV`aqП0"El%`YeՍJ\J]fSF( DK@[6?E,6grK>\p" gygi^?5!/ TJVzuRKu%3?I1&z@@O}@'ԎhgI5)$\ RtYvN*[^HZ5&?i%} N@ǀe5>Ksf*Y'  xgɬzwL]@f^nEr@0xg[MRKymnY$ ޳d(34uxgq#o@v0.5g԰Y , pʀ@EHEQfꛠ2O+NF_r_0EVȞnjxW) )%s @VJë.k?[3C@9AEV]Z5IvH"F `Ǘ܂$.odo$!`*ٟ}C};2'ib[tYΆ l!mٓ_ 6Ȭ9:U)h@ rHW^39D6md?CR@)?L :d9esbN  vn9gKkk{06 _zw*6$EDWR&z@V16T,iX+5HTPSy#tYҌ/D8ohd _A"g@'y|#!k$@%! OhQrT'e*2:\ @ Cw{tl{p Y& $9-2k~^)Ϻn 9A,qI:69 @EMnalQfwlPH 0eigq'Dj  @ -KOrQ['tȬt\ , v'9蜋Z\d! G Usu.zK;P#N]*e @ f&t`4Ϛ$ ˳Mv2B@ ,n'\QԩFi2ߕCiG4v@c,S.FV@H }?O ;3uV](U@ &e$uU.D i-YWs&`q@AmׂN9K+8MOz <賂O9o SvӾjA @ nX:U& A `[j6 & @ ~Hz8W-Ȥ@Qj=s-'?k[VJS[46B,!'3e;3B k[&I_G؊? Ŷle ju2M_V[! @`"y7JλȆ q+[u. @AȈ2U>tAg] :{'P4 @qЅyGּ{/[m] :6!@UƧ&uNm P d.0*Wwh& @ .ƷWkUoPфd @}v8k;yR*_F#?gsQ韛:ñ$iXtk]>m(4EՈ.ߍ]6$Y~y?iÝdS)jt@_D_A*+Ɠ"eƟoC(С [9EZ?~4rdM E@[VϿ(]X:_E&@ $ N̽Ti:R6SPȭ['ݏ=%~qlipKlv0S_5;(/ wTl҅Xy;ɻo>tj ۦOol^1a@ F ˊ ǿNn=xտ8j.4Ag~Tꖿ%S lդƵA?UVTzcE^fܧVGV65[0Nc ˌ9-N佅tD}[$tx oqOfӑhL=,yɜ=WgOӶY$ 9 Lmdm*qwyfFeq`pt8ad OJ}i+yV\P' uqO0AWțIYwp֐n ^p.)k`[&qjk#w u&`@ AϥSh "WI`_srzt bb V̹jăcјLE YVV׀s'6rO0NrJ.%O_]^;]s~dZJ1TD?_Zas>1YI@'?(k*hwA@0LCTF.5D6A>t;e:(7 oqںoqڎ]OE)J-:lҵfN^ХXw < t;'W€k#V)@G/lgG$PpA@ _CΟfjQr}fүHS`\^Bsm (ZO-: H'FJ(}d6$Zְ `=֚8[V?[X!Gkeڞ##EL@:wF[&`æl֢"?Zf 5#*pzTJ!PSC v 0&PR;Qț(M久`i B%F8-#l@2-isO*yC@[C@PePS&, `|QI$u':բ(yLL `bF)8c> ,^^NBHڻE y@TA*R%^͙ZOg-@˰P?!5 ejm2_7^M_ 5V7ο'Ȼ2u5řeqH.LaC8Jӄ]m 0?QU,8 hvߐ~Ԉw\H,!A.T 襱oUl,?Xǂz%F 46$4@Te#TdLpURC0!*O(8ih!d*P7[^?%9V , 55OD6P#òFC`08!^U.8qulxG%VhKyOXBn8m4&>*n1`_kfnnssֽz` p{d `MSi Ѡj8-LBKJH* s%,$@`Q+}>4BUO`O>R%hi{m!> C_[g[`DƧ&J 6&&`%5' `bZQpK%ey CP iB"&jɞt4A@E53ο:\m?  CP6h% h0k*Xم`8r[ԧjUҴ'wi!P! Uv٠7ο2\So Z-ZH҃P8ᒴ&/J(V 3Hh 2ddW˫?O{ VG ģ+k͐ &hi!r@M1O% H 9篌 N t)՘ &ܪ-g'%Zh@e+4 SmxEEfG!` 8.*#05WƐCOQ@j $ T`@и$t|]]L=H% p `5RCP _jey$^65pSA0 0M $#!$6KKχ1vh?ejk F2k@ !0VSݿZ,pmՉH. #8ÝaE 󮷤eXG:?S5秋@!N?GPW ,8 Lսs:*a䓕k nN\+5)o r'У_`2 *Z<Wex~q/FW8W֥%]YǹχqXe;@ Vr0m%3/Y Hx A@5ߦ;f'2F#vvPZ Y"(HLٺ}1Gb`Wl@2{X9ce~@`l @:&]8k @]`xBs @҃!篁rLLky[&Ϙ$Hf牳 @`< dz: P N\5x:aCԛ1&8<cDͿG_@xD# H"b8pذAPkO3&4   |@@Z<g[R@P08kOa@B@U$Ũ BX'ίk]1òGU^nk3W[p[73=>[r @Pj X? `5=TutFVjZ @@@|n />NBBk EY(8k%([u"Uf ڐw̸3j^+Olc`ǶA}HA6##e.ѹ8ޅ"=H^u nH$R\ O~-٣Aod2AVK~V4  `"8%':$;3z&0XZ"]>V]:ȊLG}r@ ۮlA@ksq.h+X:YɭR s@A N@wվ@N@ { ;BUUO̩vMfݬ¬5̈́ `m+%W j4KB[|ǙhJMmYuPu /(]e΄"0y; mfฒ!:` @5&ڠJt A7HPA@k@/ SV_vC [u Jf%@6F .o|?"o369l8 `k@A_ e˔7Ĕ96Kz[BZ@;8`+PtL9@XPk_vʇd#O=&kZbhoK^gc32r&% {hhx\<Mr0xgXIu7H`fi}3ei2;T{M,nGi@ %@0 :88mU2%d_U 1%^ Fw;` GH@@`WBg+g~Z6j;`$+S`̾ k_+{Go˯.K]=bTgW:~9#+ !H-7t />w͑f^pZfZbU dT(4rYS3/]o\t\tq y:=2eƧϗL+F o2chXetFtrAvdI ;V@>uޖ> uO>Y+>/d!gDA.}$a6M~w|3*'0c*ؕ[/}qVhqUl7!@zէUW+v0@ bAުu&;N pV&Xpڃ<}IIZI!P,}%aG䍀 (.N0fV~_j-q[&`i>/[%SLsqd#{I987<E[]UxmN=LKqK8_>``HRcAOERAު[FbJKKHb\7nS\r:OS6I.@~!+:wP($smUh3-ـ-$%)gBu^ p@ #`@@u吵AުmPj/Gh΅KF=詔ua&$ Is e@-9o_HN>-0t-2 DF gCĺ4&#XD c^?l[2[? $@<Db$VV+f@VL0EkZ8ØI AުSunFn9RAo-+BE #:X(ң~+Ql:)$0O " ) 9N@[UO4xV̔j y'9Y$kvS'/<:~:,A:-sN+DB [[WbފJ:$PKZ;}ԋLkd%H{ ?^P-ÌfqE:E B@TT 6 8?Dn[l1_^ `ۯG9$/?&`af C}28 OAH H4Be#{OHI4+e1?oDe7# /~ i@6Y5P2ο4fN:5Jw @F@#iJNANe/ 'v.lRnԇS>.C EB ? 0.͒==cv+1!BvaL=z",p/ (IǚO@v,7'B-/T&z ]W˺` <ɼ2N>H{` +@ 2-+ᴒN ,Kr$@@`D𣩡OQ4AڠR5=ZB0vIZ~ɺ-Szœ>*{y4qX;xf:2`A<n,:ɎNp}3AaG^羿M{%80RXOrKUKڿV @R @^hqQ&ml@eʮ3-ۊɞh^RXM|T:cXDnTM }y!"%.(F x1(Hi$`"Xh 8#8u: ا+x F.I )Ai0&Y;)tsAh7l (8mVf{\z) @A*f c*Mj=AY/oo9߷^)x]sD6pH+u`AoF3/;EL4dW tU]-gm/_@8M\Qw-+Wz0u.ݿ)4dQ3 iW0[`Dkql '0` Ý? /|3[[KqES/ɧNStZ=(|ZMs^iԷ޽EVF  0:L^gj\I(яV7yd!h)(80!!0@%x’& 0`‰XP-Ýɛ @^P^JIK·9@ 7Ko05$%O U=AFڷcҫ~@\c^^-!nEu-SR.&fZ^J B"p)&`qlH?a?I:eJ`dU~W,TIS4~C/ވUZIˤYW) Ce~_%ZO?,'BhԔfI^V]ycsIäed'BL2!_Zr: =2rL#*꺉enVt/}ESs7.IvA2z}[6-{[ߪl@<@7%Eh8M²BDf:NqJN()9 k7'/&A _~]8H_, D@ə``N!A2P}h@N:2@@wv<thb(+ !@qP<0eu \QE j(FYI(sY ruLYg to`׾V!@I"+n^V>IA@@6>'~![ـCB;%1F-[ WG@mW]Y)ܔ5W7spo=]C( (#wi =Xp.U+Ik=9^"4xtte @@hg#N=|u .(}ed.i] 9 @ |+~." I狺Ҩ#@[W̑F!mڸ1* 7~(K^ 7E)l<^s_RA N@_W8J9# bY  '@1@$k-  IfW*+5W6>LZ^!c6F%Zm*2>pdS/ZZ kQ_  Wǡw-FIP<' w~;(fLs^kZ3>1ci]:^VeCa XDHFKx~d@@0@";  @ 2_Y[e#\\T&߮(@,3cٕ(5-sn'N_sn 'pVgq)N0u=`+LYb@$LY3eε@5ɓ+:.qV ӟqG~u=<#J&0eεGlW}`|, .'~ro#; `ʚ)smo\}b]e|`" (M L䎳 ݨ?VvlztcxYG q2LY3eΔ=WM˖ɪEwsXciXQmՅt*ܻ- ~wd&XoJY|R~4Y-)%@*3uȝRfzvYri} iZS׹ƭ -Q ȿH< <}P,>DRkt>kq(ɂӔ=hfas/{o2?A+Z.{h'm(Pl hq!i{OahYoKe5~'St5LB:P!l13OX(}|!R3mjh}z +oKZ5Y8XM2"s;B"0tJQa:b%`f wg:ZSRuJiO>pdMK^{}ҠcC~+fiݥ _gMxUvꓚW ʼnZ 'Z3G6~n`"黎E @lq5Fk @I#` `/G|C\'}Oh6L)"IDATaU @C!  @ZZ~i}u3k3M"hEI Է]Gi]phKf,xV 8D f֍0t}t-0h@ iȆ2.TZ:]#  H2QO2 lA$o%[68XVhC.@~t[;@,'a@U,}fP[ cu0_z^ױle4Bt_fV\dVApgKlm}l|M]e#gݴz8` e# ; Dv(qHv/i~RgA ?tx#?#-JiS4!@R={KÕ`uw œ/TπA |C@ DՖ)S{I:A@ hʜdW6O Ĵ _ B8IPMT `1VN7 @r{Jg|JZk? J_>s\k5 ͯzl''SlKJp  @@ sxDZe+@}|C&Qނǒ?-`Ɛ侘4 K48C--{^&߮O @`8;uSu$KE nJs^|! C.kR 4rt%Scrn΅ N:{J;i?U1}k^?L&50<qtQ{II J%5w4pݞ=tǓx#RA@*z>R*JT~+ JSו7 :Ti H[ۊ !@ 3<^j>8PW2ҩ%SD@pnO:DX bEBl$Yo6JL0g۞d. @mwTNr TB n P%0h "'iV @N_=w -`{HLN#`1t#ؖ/KAa g'tgLPA4~%#os @RⷯΗK,/v73|d8 @6f_ic*0tf`F?4B-?:cZƁ3xh4Ϭ @K}|C}ԩ-9|\mtvIDP=^;=}fp DH@+i:_ȠҠƩp3px h,2N 叙/ 9$2Li sIA 3!@ <u":埭Rvݓ"|p @| )Nm # @y< D]dG%<+qј &^*g  <#ͳ)6Ή~Hnl&Gr }=Cq1('Ex@Xtk2l @3MmꀿUfDe ,\cY#flv2%[.[uj8i>Kh~94 )C@g^S S@cV&sҴo^ua @fe?szf!.c,/OZ.*AP}晅0o#9D:.u w!0Ϩ$J)Ԧ )sqj1H(׮O ܲWA%2i'57,!%~O6/KZPQ3?)X#M?itTĆ'ʼt]6v\TOٗH54: CZ:|  k=Dj͊k#/ :unI,Ѩ8܈u/yЅ NyKĂ=LK Yތr!e@ M_hi;0 C}5RW A>Kg>88x5-6Z%xFx-M$I*@z]TghxuJ52:|?!M G|l~es8:\ g9 @e' |iLڭF2?$sN~@PZ$' ޥIZa炖o^\\\#Eʣ* 0*gـQbwXӎR?V?ֳLB &po>{`Qm cI'5^[ZtFO@_/ʦa^KDZBWn!2@俳R}ei> (W^" `AE9Djlz$6m$;̃>P_y>(֤C 9̽lisoc 4  LI<#+ojnR@ׅ'ځA@"@RvҸ Ԃޔp?h$:ć   {"8][sC!6@V2ɥ/YH{%,3⤞'ͽiQCh('xRdJF/ DMo(1mn/VKLy|Ek@.Va"H|\*Q60LoBN~׵?PI/ 4@4ס뺊ߛFI'@t Gߓ2i'ٯh 1U :~%W#!@ teb^4` P.Pߧ5򒿄Ǖ( xLwJKZ>]eDHmѹ?IC: I9ijEI43XP_<Nߠe3{Ha/!@ GR)EZjI'JS gt]vH{*F n pHp@@/A ??42K'@\ɷ@ i8"+#p&3(i `Fk9$Nh';:].Ȉ]p +{yNMo%^a&J{`i~>,?5:h2"0m}}'?z\:o?au\0?!v.h 0+ɺ[W:dI}A vLVS|/p`qD[M̢=گx\$5@]Y>e *hMI Leŋy\f'Hc2i׌dϫ\g5Qʡt޲>KCd2E8A` 0vR{PVjR?-ݒi] 5Ts 2###@09I ]Ipk0 {8B[A@Rsr/}7$}K ! @Kݞyɞҝôu@k 0:u:DtjFrOz_LB} J$LI:J4՝JrZ Su]:6_j}>q  #@bOeAWV1|4A3젭+ ޡپyS46xҵC*%@@ (PfR`` ( (@V#ޖ(  +ڐڐ,  ܔ         ܘ   ޼           ޾               ,      ,    |y    |y        }  }            ,,             !!!!!*!!!!!!!*!!!!(0 @ 4>>4Y٣  ٣Y?ڽ !   ! ڽ> d !     ! d d            d>  "!$"  $""!  > ߽ "       "  Z!!)&ML !!!! ML)&!! Y # FE}} !! !! }}FE  #   "!!IG  IG!!"   4!"!!IG||||IG!!"! 4 >#!""IGyxyxIG""!# > >$!!#"JIJI"!#!$ >4"# ""KI!!KI" "#""4 "$!##LJ!##!LJ#!#$" #&"##IG"#"$"$#"IG#!#&#"Y$$#,("##$$$#$#",(#$$"X*#&#$##$!%$$$$!%$##$#&#* >$&$,(+&%%%%%%%%+&,($&$ >$c%&$$%%%%%%%%$$&$$c#d% '&%%%%%%%%& '%#d%>% &"'"' &%% &"'"' &%%>*%X!%%!%"'"'!%%!%%X* "4%>%>"4 ( @ LڒڒLہہ        X                 X              ܖ                   ܖ            ޭ                     ޭ          ߔ                       ߔ        W                         X                                           N                       N                                                                                   LMWW        X                 X       !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!L!!!!!!!!!L!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!(0` -)Ldr||rdL) @ُ !! ُ@ a   aY  Zڧ               ڨB !                       !CW                        ZZ                            ZB                                C  !                                 !   ߨ "   " ߨ ]             ]  "     "  a"    2031%#  %#3120  " a " !! !! &#wu! !!! !!! !!! !!wu&# ! !!! " ; ! !!!! HF%# !! !!!!! !! !%#HF !!!! ! ; "  !!!!!ED%"! !!  ! !!%"ED!!!!!  "  # !!!!"!FD%" !"  " !%"FD!"!!!! # )" !!!!"!FE$""  "$"FE!"!!!! ")!L!"!"""""!FF.+  .+FF!"""""!"!!L!d"!!"""""!HF "" HF!"""""!!"!d u$!"""""""HGHF"""""""!$!r %!"""""""IGHG"""""""!%!{ %!"""""#"IGIG"#"""""!% ~"u%""""""!#"IHIG"!#""""""%"u!d#""####!#"JH""JH"!#####""#!d!L"#"####!#"JH "####" JH"!#####"#"!L)"$"####"##JH "%$##%$" JH#"#####"$")"%"####"$#KI "%$####%$" KI#"$####"%""$##$$$#$#KI "&$#$$$$#&$" KI##$$$$##$"";#$#$$$#$#MK "&$$$$$$$$$&$" MK##$$$$#$#";$$"%#$$$$$/+(#"&$$$$$$$$$$$&$")$/+$$$$$#%"$$"a&#$$$$$#":773"##%$$$$$$$$$$$$#%#"73:7"#$$$$$#%"a#&#$$$$$$$$$"$$$$$$$$$$$$$$$"$$$$$$$$$$#&##]%$#$$$$!$#%#%$$$$$$$$$$$$$$$$$$#%#%!$$$$$#$%#]$&$%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%$&$ $&$%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%$&$ #A$&$%%%%%%%%%%%%%%%%%%%%%%%%%%%%$&$#A#V$&$%%%%%%%%%%%%%%%%%%%%%%%%%%$&$#W$U$ '$$%%%%%%%%%%%%%%%%%%%%%%$$ '%"Y#A$ '&$%%%%%%%%%%%%%%%%%%%%$& '$"B $ &"'%%%% & & & & & & & & & & & &%%%%"' &$ %Y%"'"' &%%%%% & & & &%%%%% &"'"'%%Y%`%!%"'#'"'"&%%%%%% &"'#'"'!%%$a $?!%%%%"'#'$'$'#'"'%%% % $? %)"%K#d%u$~$~%u#d"%K%)(@ PH|٩٩|Htt?ڴڴ?@Aۥۦ U                                   U  ܔ                                     ܕ   ܾ                                                                                                                                    ޿                                               ߖ                                               ߖ V                                                 V                                                                                                         C                                                     D                                                      >            >        p    p                                                              lhnj                     I                                         I |                                       }                                                                                                                                                                             !!  ""  "" ##{##}I$$I$$$$p$$p%%%%>>ACUV  U                                   U                                    @                             @ !!!!>!!!!!!!!!!!!!!!!!!!!!!!!!?!!!!!!!!!!s!!!!!!!!!!!!!!!!!!!s!!!!!!!!!!!!!!!!H!|!!!!!!!!!!!|!H!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!(`     ":\ف؟׺׺؟ف\:"Lفٯ        ٯـL Iٜ !  !ٜI %r  r%*؋   ؋+؊   ؊b  c-ٰ     ڰ- b                                             c ٖ                                                 ۖ  ھ                                                     ۽ 3 !                                                     !3:  !                                                         ! :9 !                                                           !:3  !                                                             ! 3 !                                                               ! ۽ !                                                                   !۽  ޖ !                                                                     ! ޖ c                                                                     c,  !   ! - ݰ "   " ߰ b !     ! b  !   !   !     ! ߉*  !   ! * "  !$!"!"!"!"!$!! !$!"!"!"!"!$!! " %!  !  ! !%r"  !RP_]!  !_]SQ! "r  " ! ! ! ! ! ! ! ! ! ! ! ! !! QNQM ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !! QMOL ! ! ! ! ! ! ! ! ! ! ! ! ! ! "   I ! !!!!!!!!!!!! !$!yw!!!!!!!!!!!!!!!!!!!!!!!!!!yw$! !!!!!!!!!!!!! ! I " !!!!!!!!!!!!!!$" yv!!!!!!!!!!!!!!!!!!!!!!!!!!yv $"!!!!!!!!!!!!!! "  " !!!!!!!!!!!!! !$" yv!!!!!!!!!!!!!!!!!!!!!!!!!!yv $" !!!!!!!!!!!!!! " M ! !!!!!!!!!!!!!!$" yv!!!!!!!!!!!"!!!"!!!!!!!!!!!yv $"!!!!!!!!!!!!!! ! M# !!!!!!!!!!!!!!!%" zw"!!!!!!!!!!!!!!!!!!!!"zw %"!!!!!!!!!!!!!!! # # !!!!!!!!!!!!!!!%" zw "!!!!!!!" ! !"!!!!!!!" zw %"!!!!!!!!!!!!!!! #" " !!!!!!!!!!!!!!!%" zw "!!!!!!" !!"!!!!!!" zx %"!!!!!!!!!!!!!!! " ":"! !!!!!!!!!!!!!!!%" {w "!!!!!"!!"!!!!!" {w %"!!!!!!!!!!!!!!! !":\# !!!!!!!!!!!!!!!!%" {x "!!!!"!!"!!!!" {x %"!!!!!!!!!!!!!!!! #\!#!"""""""""""""""!&# {x "!"!"!!"!"!" {x &#!"""""""""""""""!#!#!""""""""""""""""%# {x """""""""" {x &#""""""""""""""""!#! #!""""""""""""""""&#!|y """""""" |y!&#""""""""""""""""!#! !#!""""""""""""""""&$!|y """""" |y!&$""""""""""""""""!#!  !#!""""""""""""""""&#!|y ##""## |z!&#""""""""""""""""!#! !#!""""""""""""""""&$!}z!""!}z!&$""""""""""""""""!#!!#!""""""""""""""""&$!|  |!($""""""""""""""""!#!!#!""""""""""""""""($!}{}{!($""""""""""""""""!#!!#!""""""""""""""""(%!soC?C?so!(%""""""""""""""""!#! !$!""""""""""""""""($!IF!!JG!(%""""""""""""""""!$!  !$"""""""""""""""""(%!LI!&#&#!LJ!(%"""""""""""""""""$!  $"################(%"KG!(%""(%!KH")%################"$ !$"################(%"KG!(%"##"(%!KH"(%################"$"!$"################)&"NK!(%######)%!NK")&################"$!![%"################)&"LI!)&########)&!MI")&################"%![:##"###############)&"LI")&##########)&"MI")&###############"##:"!$"###############)&"QM")&############)&"QM")&###############"$!"$$"%"###############)&"NJ")&##############)&"NJ"+&###############"%"$$#&"###############+&"NK")&################)&"NK"+&###############"&!!M"$"##############+'"RP"+&##################+&"RP"+'##############"$"!M#%###############+'"PM"+&####################+&"PM"+'###############%#"%#$$$$$$$$$$$$$$+'"PM"+'$$$$$$$$$$$$$$$$$$$$$$+'"PM"+'$$$$$$$$$$$$$$#%#"I#$#$$$$$$$$$$$$$,("TQ"+'$$$$$$$$$$$$$$$$$$$$$$$$+'"SQ",($$$$$$$$$$$$$#$#"I #%#$$$$$$$$$$$$$#$#YWWV"+'$$$$$$$$$$$$$$$$$$$$$$$$$$+'"WVWT##$$$$$$$$$$$$$$#%# #r&#$$$$$$$$$$$$$$"$"YW""(%$$$$$$$$$$$$$$$$$$$$$$$$$$$$(%""YW""$$$$$$$$$$$$$$$#&#r"%"%#$$$$$$$$$$$$$$"$######$%$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$%$$######"$$$$$$$$$$$$$$$#%""%#&#$$$$$$$$$$$$$$$%$,(,(,(,(+&!$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$!$+&,(,(,(,(%$$$$$$$$$$$$$$$$#&#$*#%#$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$#%#$*#&##$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$##&#"&#$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$#&"$b&##$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$##&!b"&#$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$#&"",$&$%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%$&$",$b%%$%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%$%%$b#&$%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%$&# $&$%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%$&$ "$ '$%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%$ '$"#3$&$%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%$&$#3#9$&$%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%$&$#:#9$ '$%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%$ '$#9#2$ '$$%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%$$ '$#2 % '&$%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%$& '%  %& '$%&%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%$ '&% $b%"'%%% & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & &%%%"'%$b",% ' '%% & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & &%% ' '%",$a%"' '%% & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & &%% '"'%$a%%"' '%%% & & & & & & & & & & & & & & & & & & & & & & & & & & & & & &%%% '"'%%$*$%"' ' &%%% & & & & & & & & & & & & & & & & & & & & & & & &%%% & '"'%$$*""%!$q$%"'"' &%%%% & & & & & & & & & & & & & & & &%%%% &"'"'%$!$q""% #H%!%%#'#'"' &%%%%%%%%%%%%%%%% &"'#'#'%!%%#H !$L%%!% &#'"'"'"'"'"'"'"'"'"'"'"'"'"'"'#' &!%%%"%K$$%%"#:$[%%%%!$!$!%!%!$!$%%%%$[#:%%"$$*  $$  *( @?dل٤پپ٤لd?:sٰٰs:dڬڬdeھھe8ڟڟ7RRRS@@۫۬mm              "                                                                     "                          \                                                                       ^                        ܛ                                                                         ܜ                                                                                                                    1                                                                               1                H                                                                                 H              R                                                                                   S            Z                                                                                     Z          R                                                                                       S        I                                                                                         I      -                                                                                           -                                                                                                                                                                                                   ߜ                                                                                               ߝ ]                                                                                                 ] !                                                                                                   "                                                                                                       l                                                                                                     m                                                                                                                                                                                                                 =                                                                                                         >                                                                                                             R                                                                                                           R                                                                                                               O                                                                                                             O8                      8 b]c^  jeje f yvyv f zvzv  zvzv d zvzv d                          zw                                      zw                                                     {w                                    {w                            9                          {w                C>C>                {w                           : t                           {x                                {x                            t                            {x                              {x                                                        |x                            |x                                                         |x                          |x                              >                            |y                        |y                             > c                            }y                      }y                             d                             }y                    }y                                                          }y                  }y                                                          ~z                ~z                                                          ~z              ~z                                                          ~z            ~z                                                          ~{          ~{                                                          {        {                                                          {      {                                                          |    |                              |UQUQ|  ~JF  JF~  JF  JF  JF  JF  KFLG  LGLG  LGLG c LGLG f>MHMH>MHNININININItPKOJt9PKPK:QLPKQMQMdQMQMdRNQMRNRNfTPSOfTPTPmiD?D?mi88OORR==ll !\\--IIRSZZQRGG00             \                                                                       \                                                                                               !                             l                                                                 l                                                                                                                                  @                                                           @                                       R                                                       R                                           R                                                   R                      !!!!!!!!!!!!!!!!!!!!!!!!!6!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!6!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!d!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!e!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!d!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!d!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!:!s!!!!!!!!!!!!!!!!!!!!!!!!!!!s!:!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!>!b!!!!!!!!!!!!!!!!!b!>!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!PNG  IHDR\rfgAMA[^\&x#[IDATx] EGe'$!+AP cęqٷq;8:8 sE #;l#$쁐7ޛ^]{꪿zzzd`rV[b (3Pbb@@m@ _U jb 웸_8 e @LV)恽%)10L |x`09À@lǏ.`'^6U> eZ@1 {$p p4pt`TAD!2Sq4o pmΆ Qn|c/UHb-R`aqt끢]"Ae@s Iȏ z"pp&p<*e*%< x HlPa+״CQ:| `4 F| l$Q z4hf"N !p6l_^jBZPy +*J^m/eyT#,`!p  -rM=m@[-[iv]< p$_hڂ6m8IbȀQHv%&p qڄY \fx  Hf6@] mE%(4 /ح0t+!mIJ1Џ:_?f i˅m+c q@o^n.ABTqVZ{IڭQC#~-R@xVik< yx-eo!K{ݔisڞmR0`Z_&J'e |0'wK0FL *kHiN e Se2.*c6`)$t^~ 8Tbdd$p=p[Z>.*N)9J(!GB >3.p\!JQyRPR;`!>$B_keؖ^BIWw](+2.00 JmF%b ض ΊOV06Ƕxok+Hy`[LQ>oVFb lԊ5xFi1@<0E/OD>*{D x(K,=o$qZ楘aX>s> |ߗU>=627Ru_w}!Vz7^>0C2LuC-J:f0PqPmmYq9 /Y%1#Aiae'%Un8ɚ ]懵&=vT}>5&R)` ~8w?@ `tUA 30 ?h:ĵW;K@KkIo.NE@.;MJvD 2\-UeV$Õ!cD`17<;qyЅS|h@"(Tvm?Wq2 'F\ ρ.͡Zy6ojLh=ȋ[_ AQE@)`؇!?T1"1@/'Z.yAM:J"@/|&"\yTVe!FKW[-iU 'lk3?ڪʩ13療OJVEZ*U`>Ҳu1 |ς%edrJKɭ DN(5Ug1q̇bg}HOd#kMiN1Ї-u8~?[oē/i=Bom  20{h"δb FOKMC3S32}o]gKi} #b ;cT$0i?(10 1Z*8ԽZ%w/Rke"f6'*x!$b@ d@jP9E(ֆ<)ɎO*>aŰ 4Lx!Pc3YܰO6nO=%OGܽn]ǃͷo b42g`Io g_ώg 6xSϢvp7$8|%y3N & JN/~t56gDʶ<0ͯXٽ;XsEW8t48*y;pOEKkΞ [@BF]H"Ӷǟ^:}<;|D8?VH#9| ǞNZ0??.SH':?8L"mM@`$ (%;t ͘DuNSs~ҴhBo`!@I[X%ϵݻ;/p\ ,3gXitɨ }i} k8)h-Izg4ƚӍRl1#j8Pc闚W)ۣfZx>H_%IJ,A8q9nؿgtoRϻbb!1-bC_̼;/ V@? s_< $!0~ k<, F6l6pj[z!~7o] QK,s;j[ ·~N_6wf7iRx/jA`RN t~DIbM+Ǿ`Y JS `O&?%Gコkl_dpMJۜ:BY&k~7־i^=qtJoѼ};"'AιAbu pYZR9oU@Ƨz Æi; P9=?Wg~6#}tli6+AÁ>Ώ@'ɔn0erMҲL[ [*AwϺwú"Dq=6z_#{& ѷLr!Gcb"Ԝ%c<8Ȣ#X0e6ɪA`˨A',9- <&Oɚ8(ȓI`d ;;'lќ\B67!47!gNHߍ Tମ>ŏ0[SaqQG  +jWjN;O_Mvy+֕Thɐ&=aQh4c{†p\?mV Vz}?nɕ$"GzzLD*IVgj~_ٍ p­>*  O͹Ou̙rC`iMzgF*gG&@,6zeyT"٣@sG^{sJ[&8>rC92_2ɜGiU{-ϳ<~eӜy5珼wdeP@e:ef=݋jX -8o)t>/X}n]fv_VI‰A8le#$n$cO;?^&Ձ6={X'GUƵ9@/ymKOR̻F81x n0BG]Y_5QzmYoOFE0ۊLjJ06•VxE te9ۤۺ=Jc!@d&Qh4a`OBG)X9*_} D`<co}aĹtØn䙿); Q<6De1't̿#sʉu9[b7F'IT!xtߦ ^+sߓ"-΅:p:p`6Nlm ]z*R:=:Pn,ٵ1u:Uߦb w(q-\uWA t~l4۟#%(:D~߂?`u r,gkyՆAV]ym8s$;UW]Ι[VW|ITFPl|%fQ\  p+񈿚-΄_$QҰI}~~_`YT&z+ :~rWk 5(T:Ja`U&&`-2r` Q߁G<߲e{շ\:}-7=TpCVww6SѤW+:r,C3߁&_749 ȃXl '2mji? FA`*4b-1I] PfSK$#deiH &\ |$3odj|dxɩ!U8' 3?Ξ37܃a3@_OZKFJZ`]b T|:oIj ҚUϺ o{|&X;4^r dJ@V_;,}ިD5`fnڎyeb@ ;▔| $n͎A[;_`= א3p.M' G{,t$ p,`ƏQRyZ=w*'M>Mzꩢ乊n @D()8w­<2Mf e;ٚ?l6^_υɷ6zoQ!} %_tčs/ y,17yٔ;rH.i mƴulB- '<-UC[~$glŬ.|[؅YEnuh,Wǥ#ew6_ o WCG1`HpxPQa*cY%b [ZJ(1 ,HV-!Qr1 X3NUSs!`|I@_)Ā_@ F&I81 Re`5r U(.jb@ }idv:0\P7*oJ 9q9"={+pR"@2Á&:tT] | Gs;0ˀuن_V=o1 BCL]d2#x?͇RHĀc> ɽiMK̢̲WӚi΢,|((KJb@ 9?dG!A߄Ff g:Y6S%,PA#!b)O`I@3%d=P%+rɥj!`>B_t4ڪ@]|1 (0JKUCjexS}1Pc`1:#I:H< h&VR1 z`A\`pݜߡb 'hxl([^`# ϛrIz-YQG 9N>Өn^!ץQ !Ǽ3WUș[Q>XqF?ƀ 06m>E)5{Jօ3"@ |ٳ;!+*E$࣏/JbCnNRjON Cb M_Z]\ \1x52t.}b;U!36̶6$q3) $m8ӃN 8Ѕm= yѳ>@^ƶ3'.5/zS, OD y,M. STF"06ʶx4%0>,L@=!qMߺ\#| ԟA@s,my3n?OIRWR^ hR8z/03swz@US%>11+?{=#I|y8.WB߽f'"A/OC"Z=(PAo@`sD oY-BkTC"Rgw(j/ aRݠO1lSl[_J)?')Z |`J gy"U()bon$b `;}SO ]Ht +kD 2 n3=tEۂ|=pF}M`[yPhgRy ҾgF&1 5W /dt .poKLxs`r,?A4qgRDw&r>T1lr(pŃ$}h{dukIe Ur//VJ ִ9m6PZ){ B |$dikڼC@v K7pP䏇*ik H|`WL ߆D>09@ieByG-l'eC!:2` P;- Sb9#UmVC=ڊ|ڮ|p0( HM; \ <Hb6Fm&`@ ~#>ȫG@C" m@[&tR^Gc@,KwӀN0m$a\srO'd@W, k| $}]O?{rLz yhkk̇pz6$U>ޢdY=ܙ7\D3Љ$ 9πdU,C% H0_M恄(=!@|${ge=PIa$\;~ p,2 I@>t8t%.>5QASc{I (HYq(8^A^b89^1 I `<ƑW @Uxf.}6Um//V2`- ɑWb oryŀ@.=gJG2P@JbSN UU (2tb (Ш0elÌ"9%IENDB`MEGAcmd-2.5.2_Linux/build/installer/left_banner.bmp000066400000000000000000004556601516543156300222000ustar00rootroot00000000000000BM[6(:z[wtZVE?2-!  !3,D>[Wxu闔~zHCID|鵳B<  B=ᇄ{x+$*$}y>8>9᠝~z{wokqmuq  xt       ݘ:4              <5zw                          ~{83                                       94                                                     ܣlh                                                                          njA<                                                                              B=+&                                                                                ,&                                                                                                                                                                                                                                                                                                                                                                                                                                              +&                                                                                            ,&D?                                                                                              D@mh                                                                                                qm                                                                                                  ߤ                                                                                                  :5                                                                                                    <6|y                                                                                                      ~                                                                                                       =6                                                                                                        >9                                                                                                          ល                                                                                                            xu                                                                                                            }y                                                                                                            {                                                                                         tp                                                                                                               ⃀"                 easpspspspsptpspspa\  a\sptpspsptpspspspea㤡?9䝛䐍E?{儀1+         2,                                                                                       捉E?                                                                               HC                                                                                                                                             芆                                             缻~{                                            ("("                                            燃LG                                           )#)#                                           PJ                                           *$*$                                          !                                          ,&+%                                                                                   -&,&                                         込                                        .'.'                                        韜{w                                       /)/)                                       |_Z                                      /)/)                                      c]LG                                     1+1+                                     OK<6                                    3-3-                                    >8*#                                   5.4.                                   *$                                   5/60                                  "                                 7161                                                                  8282                                                      :3:3                                    ;5XSXS:3                  "         =7KDKE:5     "+%JEJE,&>9IDID@9OIICHCRLb]HBHBe`}yF@F@E?D?D?D>D>D>)#C=C=+$PLC<C<UPB;A;@9@:>9?9KF>8>8OJ>7=8<7=794<6<6:4;5;5;4;4LG9292OJ"vqb]b]up#+%,&##xt|x#$|.'/(ICMG !ICKE& ' yu|xUPWR<5>70)0*+%,%*#+$-&-'2,1,@:@:SN     UPzv                         |y("                              *"KF                                       LG$                                              $MH                                                       OI2+                              !         !                   1+("                 !              !          !            )#~y(!!      !  !  !       !!  !   !!   !     !!  !       ' {2+!  ! !!    !   !  !  !! ! !!   ! ! !! ! ! !!!!!!1,RM!!  ! ! ! ! !  ! !!  ! ! !  !! !!! ! !! !!! RL@; !  !!!!! !!  ! ! ! ! !! !  !   !!! !!A20Z]3|x}r(D.hhCJDD3DJEћPD4J4\8wxqҬU=#;4OYk|4jQHU:t*T5ŰeE]Ս:U EEݪtk+FY (WE) gfh1/ö2-sdnyn얅vG12mK N7;55q7 :6,{3wmig[O6ϡ$yIʱCEg,j s_H;[SK1xSzK  BV^O'ˮKzX7.֗&rk^l.2c;MkT^.E^+4! ^ѳ72P-Kb:sӕ({:7? qb*0˞5s 3k_SV5w0ն5^{ _B@| <"Ϟ&OOH\=y9JtakSݰ^~fkM%D*u&cɇpMSd>ߟʊ5$̾ͅU x6 gve+&S9AW5rBJ:ڿA9W}d:j8O@8L~+Z\ -<r z[)_"k&W|mUz½#| Nzڄ>>p&/]R Gsu/nכd[~Gz؍h;w_.e-^;G߇D9~bL'ʢU21"2Q3#R X-  vhB)ށM,>kiE 1[*knLG?/f_fL3aj</y[B q hx 'CBn?ʁo8nn)xB9,6XWa=qc]S behHK^ 0@ڌ՗ʢXg<`GUiphsX.}3(7Fl}l5L}$`B$i ު0lP ZZ!"I+ěC,mDcw'k}Rt7Bo]OKo [>tKw#N|U E?PHwYnsv96f7D$|V[:~| Xv+aӅ`oۖ|Mzldf\7}g)Ήn=R]!dy@(q LۉMC\$23,H:C3ӔFq@?*^b;NԬ<@m;_pN.0 ɧKT% ]7D pzȔVHQgI(s%^{V/A,\ՃJ\w]t1ge5MR#.mfF'] utJ)Sl; v@euxWQeI.z}S~a@O šys&Ox_ˉמx@)vZ V _+b I$N.ݩ{A?z1:ev 9K2 {v6]r"zNҀE@芝="O*RpS&) aϝk0M/&Gh$t±ZQDϷ>ۺK ˟%oT)=d[|7MGǻ$7?T][q ODʖphA$^q0y t%u7 r+VhXMKFc77VumTB?N 2~Q# -vvy:ƙ(3.Xiڎi *|>' HXTbgM]{RigNάk ѳkt-c,u<+k/rX=I&Gkb1Rwjk+Ea]~ǒ3]' [zj=kP0Cx Ә|N o;U] ΤL;Y)=SsA T; #Θ;]ѡ^bHO.T]vGT{dUb:^>Qԫ;{E8j=':>&^V;6p찼g_v@BJ; o(3_"\}.¬g)y.dd8vZ>E#_ڋ?Mw:3x6|C6{v^?+YXcُU5YMsMMj5Njcz!;2xlOyy~sUΚvUIݜz@:t'U|Bs8+,Di^FKێaar_F,v.QWv%s24 ?!}:2ClŢ9yp.gvZTuLW\[yA$Z-nI9(yCl98k(V߬VG2&V`d;ewځA>!u.oGJG{9y&*]\Ԋd_ u_p/.!C~wɷi_.qHe㔷--UYZ=dN3VB]wyw;+xOnN Gk?xiO'Vz|7cRdY#eN6#?HO[?wgio*w ֶx-cw?^x IQ *z! %.>l`q{#uɝiבV'TNu羅6#k~s/T8Gϩ;?͠X$e\D帧C=}#3Z|aUMgɟ0 ?yvj_Q/)59`>ZƧֆDKe~("¬ǚ 28Z}\H7yo8ŇzsD,;jh:jͯjH?~Q:qMGq㺓ɼvt "; M*DZR~1 U Ѝn_11l'kخWv7s(]3Ne{VϚa {޿uU9?;֨Hc>~? 20e\]#Ư]Y v>Q6AmǢK_^'6Af(xa_biqa} r*;kŤ`1c9:H%_ij_dmXfRXVkmD$_5(9W:GE tRqćUΞc|#GtX8O<9R>+iS Yf?4ͽ bUg9s/cezo45툵#k>)ӪFHcGA_S_'%s6~ v0=жlst+b/}!+_ 'q!ߍu0ylZUޓ幇a3C:ۇIUQ֣FGC+g5}vq\m'`v -wۼ(t^(u:Ǿ:vpxuTo>pyC2n0YYxYE8K3) ?xy? `G㢻_7hmTHW6{X!ljz'uWOI2/,ɇM!h=Xd~|VsJzpbr5%`/+ê}Y5:iuUC}}Կ2KE>|ڛ|mV;Jb rg)̧=+b,P@ yTwкvŅ17fX722qycUy.4,`">xvy~@j)?sm78|Az&vsM?ZևnD >t+Y 7# VM+rJwdpʶ"}6jɓX?Ne~H..ߠx>e#L{x;i|BgӤ ?|Z`@Yt#ge|*EGi[~6ALE70nwyWO !wj%S=y`]w2C[38j':wYaBsMb+5bHEt͎pTޤۏDT*ml "4F%@.y ֫][-a[!3]uKly+8ct_g͗t{gѴcE2/x>W஺{ht'=8咷ϓ_D;<ҕ棌aa\%~um:ު'cºX |HgLYSmW=20ohs=H+G? -D,W_!FC̑nK(^ÉZ\4ųр{٥)3}@ 0rF0vJv)(ex]^iβ:562&Ag(3-!zʐĝiP:bD#YΔ5HigIȷzQlFfi,PJ]+Z8X8-GDl~KN޻RX_q¿; 1оYg8S7]=~r#WKJyR>iʑ.?<oJO#Lˈc#ud@H}2f 87;y_JTEc}*S2j~_>ʖ7x=4^8,+ E|yG^.cm'XlQ#a2عXQLݱxbg/mX+|=Unض:)~?3?4Ŋnܹ\8lIȂ@("ÛL4Med _Pl'W[cÜJ0$S6<f㴘+{_!&!|0a~b[ĨedkðGi{w!\ 8*4Y`Ti'N{ S f) iCZݙx 28 |YX.LOo7)qc%|WR5%Fg~)DKr<^ z;KB:*C^3U;)蒍O;64u~J$+6)y?)aʾ!CD]U 5g qtoW)U~^Xξy[98a!+[//oӔpG{5UOG$?;xg |TU$[軋l>on񡩖qOyn!+G.xHaj%`/2l;!gݮV=eɈe9/Xل<ջf.-m%V;6>nEl)U~[V ])s{={CE"q[ U5 znyޔc6z`1o. !?J%5׭xل 0y* W~0uod&}TL׿OW`Z³.<2ˬ>V< ;~AYַnr9_Pw\s8[0>Jو#$7>۩osW>Tf%7W4?zFƌ5SOq:{KQ#$kR;pXfcz re*kWcT|^WzC5.t‚C.hԍAv{򹵯ٞ\Ч;0/9nF)_G"'уz:(WJ˅|-V,v7 ;XVƭ?wgkM4~>/_+6<#GL~ ':ݑ}_7CļI_]8Aw wu/$aoT>#Ǜ]aA0B~z֐邏r/uRVrU3yXfذ27``]^?Ys xJn{Ns^o|Fxk<76|G(|e'Q qH,l^0ߣ f;ldwpǼ)@vTt%jmAhs4AȺ~, Sg#D d:"CfKXI}ϳޣ.2˧|!h4X`l>z9۷( ݁kv풝mi%pFCGRdNkӾ4rI0YR3)6wy0Y] fmήAXK_m^+^B(zi/ >sPA$dKgˤk4RowRG.X$*OP`-&훏g|L'( l7|y?lh[ U{mI'w`SwG"Q^ yNFpt߮||r[o'I?$ݦ?b =*5cAa[y6 $3ܥ个}mFb|mׇ`Cr|PBQ+dN6f̂KONIbiVqބoUk8?a/aRĭv<2Idm3Ks aZ2|$R6k|e#5c;FO]R\ 2o۴ݍߔ);WlfdHٕPR+TS:{J ePFB'ItGBL;Ixi`9W(ΜݿG g^U!.Er>*cc6 2ot\>;ٓt^gc~*\WI9@<3)UpE">02Lo! +3'ެ}<n{6j;RUf J}7}9b 15gXE3Yl(oRIGnӑ>UMWw@hEu|ߎS5==gC?%9S0n\POZ LRM QPMYwam֟zұpҗw9 x^~txʀ:it]<86ו',u9#[/鵜@3C=&Sggٙd/2E>j.[v| Z_->f{ƙ;uѺ-BQ?׏N&FaīM#@=)13j;]07 dS+֝baUg"m(Yu%X㈛Ht֠7z X,^:LL_u" > +lg;c~uLT~_4hl1.TS?;TtǸaIߖW^ _ 6a*޵3|:+1i(ǽLK*PWRc1BͫIg!׺ p83`u6~^گ|L]ߔeé<١я9hXo HW 2K.;dzϵ͹}˓D'#%\=9?` ͌J%v" hh[RR^f-b"\.V!YILϒr3te-eQm'7UVX\.kiɺicR͠/X"OѢכ:X?{ue_6dt"eQP[==aΔ T[۝ipRJ_?~KMOIWNe}7_CO#n:eKoz%qPsc_H43,,Mo&oyLL07u h cs3^4eOf-CrPR%%CDါв Ŵ$Ob}Q}ZdQw*k]0O$6_Xu\h\nוs%NiOiD"Zsu_jڝl@3yx$ȶKtf7e PS?;H5~ uHfqH/dzh:Bu켁r^Z=1zN-~ky|.t,dw5k>5.F<%|RnOIލ|FYsua;dC"}Ʊ3w7(J^,@cgRxO "#IBCmTnx[؏ cSFi9/# N^RC)7GDU!e K#^u 'rs6 9e9AWl|y:Ǽ4 Uw$Z1ί? ẁem%,O=M;% ;\Vcyѱ@E϶7x(s$|xAw>Ev9;܆fgf 4KcZPn4u 8eL|)-؇ t6_gݙ-y']ɆGc4;@-\_H}#IJ-mr$MRͺUPKvv87vщq{3,9[nuCߪeVs]eCuӊ{~jx`H+vx^h`FW6UUb};f]gcD7lsӬ%4`:HS\pbN#M^9ς͐յyr3A${ &>=.5WG q/.C-Q隲 MޥL})$bhM]G&`3b7Ύ.Áuys1.wA{oA%*8㘜5J$佝h\;? ȳ طi&p`3@ƞ'^y>!sG, 2 - 2@$i"^U˯pum ٍ68@6v[$u 3u}Ņ*=/Ӎݣ/fծ/~듾/|sgwh7:[sY;3çnܺqrٿ ,az}NYroV~l^OO[ݤ3fr*x!)*YhpP(S51VH-D+y炉祝=vL}L?ƖmT4jnSpxv2w}Om3LBXHK}wF"]|HN뙮l?7|8*F<>+|itP}GiÏ}lR\[-|q^ւ݌>}]Uw'ψ|JKCr0]-MәxD"B#ȩ}5un+Oi˿:53dc}ѯTaEݝaHesG27hѿ1[w/~LiG33IBg/z`f@3̨+ކ|Ϣퟤql366cÕ,HA_ݦ ߸֯;pho$N+:<6 M/P}qm/9dfk|%y@|1Ҷ'?7\q)Fq;dI'}1S@Ϫ}2o_ԡ|Ǩ(!ʣոL P6)E8sYȫH"2WJ\I\-Gy+yS4ڝ@ SZbFƈoQ=!iq`kAծh”dxx{$2a>t ?cU(][%SI)jkGT?uEߟv;PSEZ"0Y}v:ırD >Jȇl$ѱ57s[]cP[ ݤTd ]N3̝;5?)X#e'җ*.Q G$Uu6Nr^n:e'|ɑp?¹K;8W0w~0_F9]vU_fÈ en(l]Nml>f\cԟs=n[ՙ=AJe\u&~p:tR;DtW$Wdkk'EB_q5ɓntt?ԑGE0?;C_v8_1Tѱ-xY? -푏9-j:_wpw@#$-՛_qfv5eOHt# =; |G}3dYeqwxOE5TVROh.ݒ ?eht-gi~!N_Ñlj1YٽjT^n2X-"`tU}7+X'@#0 K'vVq`cIΙ.1P$G9~]YL9DK; 0:ApV-wg|U4Pja hed82# G䄝DV ߵ Pʤ r$*b|~!+ vUz+e|&~!j[wY߮|Ǟ:Ѵue:^(V‘& *~ųJ_!Nl>'~/1]?>֘lߌ?!a㘶N3ɤ`L8D Te Mʲb'z3G hģr7e+xG Pl7P٩fJ-5=ֹ8v/7(O1(k)xTd}idj{FUEx)yq1^;;s"6@ݓꈦؼ_d(MV>ohC WFc6˗_F x w!mA:UuT*60wN@gz =wW%1N~%nɪ\6 n)w"26H%. b] Nތ$/NF0RB Krㆿ,gV?]z cpn>Ә9۞{PC<1_|͝,:Ô4hh"-whWק lzASUߑmol_naAdez> OlVGn#A%|_?MBZygY=뿵utI^ W2 }?J472,;=S coE<2PŻp=W-J!ZA7cC cgGAI*[dI = ̷nUlEzRZk#kd%PU'W,.U~q2;!wyũDfg9k<uVX *>U"jc9XN{`?W "w4sAv!Z1&|ڻ ~;ΰޅ˜2Ms~kyt.ѹД_i5-u ү<'|_cpۿ(n ;ag7 2+Dc96}k;Gk -J~=:Cw-gHӲ(ӑٳ.cT2!y6d2. YPz=uݷ_sSe!_ÉQ.!56mwgq*bX 5 iqCsI-Kt쭘warYK'mu)Zoi m{Ý&Y3^CG'H犯v.#*:v-}ߺe'}O=y2ӿ)ޣ*b ,h{`c:"ooв\p8tFa+$}_`;3ORǂ8$xf~EZ!;Fnf^v$zQ'\@ ߠh7i\@И댸Hxf-]7Aj<װМ7>NQS|Lt_>ɦ~V 5B=0i\E/2/2 KH:+'X~b=^\;T^O*AZФkԻ-*獮)d.$߯ ;g-ݏCC ^(*ثf|iPi?"Z j׮ Cdog+ӅHv4~އFJh8_zg㎶;#g(Tp11ʋ1,c:hc@?I;cը HF~z,uq˗ V~#MpO5>zbkoyxW玎rWȒϞYת&rZǽ/<MݿIx9کJw۾oNO/M5\og2C=]mkS lv0>s*}cP},t40|7]ZfYJWKV3=!ҞArz],,nm95[&DZYՄPhxɥ DG}d|?0#*&mevYbjsZ*/ АjCoR^sLv ߣ!73F_ѷ-w;I#{#^h"q=M:w*rPwƓbǃ3Xj(O:'&7_Nó Eӆap'\wC[8۲k>w?Ч^*732Mٿ&fn6Vf?Xy& u;}g̯l?CCMgy-8ڋpFSy v3V Iw,]o Ϝ.y1?N@3>Lp_BL+rͻ/[t{Sfz|StFy{lͧye6s ko:9mr_9p3/=ox2i;HC2G&u= RPq]<~l{>oq2`w%}7 L͘[H߅\P Sbn|<Xk6Z\HKGѩ8EɛsXupϖ.W=32 4c; _hEF?F}ݓNŊ3q XB9{~߬>QAUPdC9FZ\fvLR sd#N9iڼ6Y"S;i$JSn*G|p1E*@I'+:>w6at4Td@d7vGqARGlp]po%27m\9w:V~ Xwdm}W],uҼKkJ́ LS̟ͫJwN>XkPkxԍj!W?./T(%'X/q4^迉 =<>;Jur B+օ!eIBZEdC2IrDW=A3wbKԏڔ:QOƬL(f{3,u~?gIiNF2X =.cDv-,@_v]]w];fr$=zEZ(bŨbIHK;)'T'Wl WNubq 'Ws>!鱝ML8{ʿvKkYwG1:&\ 7X9rxH>gG$J4QWo\oB#1;g1E>ȴ!84_W=WÔqo>skFXɦ a}N8yۡ(f8(oOBէ߲:^fGʙ!,= w1ݷ] x%s9_nq!j wcLM1@{_^hN;CVe#2{%mi~ag9Ҫ])a3^:@?IG;V.+;fy# b\w~ʆ#&#Z1 ɐg}gvL_J&o3w / JuF#!3X-IR\;{DG/T U4V;Sk陂l;pMD} SWX;|I{ֳ nMQ;pC\Āv7 ܤ34?2ԗtSY>@pߦ$r)\n趩vk:oHΖ'pF5뾔 o48+Gާx\}{cp<&>#Dse4pDr$6?|WsW?GICfxnX3|T-H!HTuI=MYwD+v2]GE "-w6.6T42\qyhwa\iYb][隯ipi>\_ :P.cAAC 2xNi,ȶ_mg?qƕb/`:1m<)"24N#$4ҤSȹQK9#zS+-;oy'cĽDtv}< c񻂓cy>[MTቍPEWVɠơ#ǡge/f7NagqY܍KNXTpn''ҧV1kW+NͮDw["bU;v7a>҈oŷX@γUwԽ I,v%< $|42Tⰿ.NDZ9z䀗%S> Gw=SߦAN( hG ͫߜ%$p(N( Lgj<,v2<WeISꤼa|hLGax;{Bi%K8Wۀ4Zq'Y'tuu*dbw8[ZtBmWLY5AͿ}^Z)n ʉ3߷2D+:9;+<(Y[n'ҙG_*Yp3ڦ /Ug벯}TWmW5fm#ޝW2o[[|ضPE8oFT3>gd;Z$;$LBlYGJO(%BMkHS i=ԭtkd'؁USJR%vYe䞝7ir~og*].HR ehAT g.ͫOߓ*$ook]àr겠Wgnq+nNj8.Kr~ĕ[mūTf ]_#2haO`0Qm׳mC mG΋؍SCwhu뫺\+vV<q'sgQW {v7FoLIE \;KrP}}5;_z{Mݙ\˷#ڎG6s?;_i)G<>?=K<8NTs;Gq}+QSL.wW2&heQɓq]d'7~4]7Y,{WMBs˛`d|X<}ӡ3;: .K3HѾKs{)\8'╝_i_zn>@Co6NFFvc˓61g;.z[v/|t:RJt$*|ȷzbd\;GKy.?rqǺ /;]:~[;3A;Fe;>xS(mf cu3Mwr몐ߜI v)iv.fq3"+޾u S!<\vuIcB f#9^~֛_ i;Bwo լߦexU=*Ŀuזu4d n,i ߜ8"ݛ3Y (7h)HvrD_ O8"ӍX!&|޲Kvlq<'4@+\˦~_zv>KLZmLѝ[_`5_9wq$ cAsY(]|IJ9?R("ޠMp(L Z=,1/%8hIsyǬr0݀}POU3m{u\wY؞(_ 9Аfgp;8 _"r(1Ao,i큐pglm?"Oギ|; ʋq܉$cADqx=oCD멨PtVKI:&dc /P$I T.ۄgVp]iFgm9hnrwicW'N0FѾ6涎V/l~Y7SzogIWz!NLpT ի"_+Pر\ҧOֵ鮻:9HU2i>TOtԋ خs#yu`:fc;KFq`fLsw'8I?פ3t4ݷwrSjp?m&_}}'ظ%b~ky=1tqS@P|R:)2.DE29%f_ŵŵbHG|?Ɣb*>^}47Bp}y̡*LG.10TZ?Yf%lptn's%Js-np5\)#$+(GOJDIZ>8\#ZLG Fu}<3ԌôY{fæXpuzK۾ظBF'նq3L"܋LExW>\R -mھ1Fz-ie:v$.ĕE޿^v* '?inv`Գ55 L-S[&~O/wIlWZQ&glW4lMpǺ'%\\}1påGՙW)۽_۬2sOH[#,snhڪ{ؙ_6- cKynׇ'Ə/yc2pnSL [pr"mz_oCdzqov;xlي)>|s0hއ^[wCfUx@TÛ-8s3c1-FO7O]D7\X3>E(7( Ka+5Y2]G*2˼J[ kmЕe"O܏JIh5Xf9iJ&Cf^D'4|n Qljյv -IoFX†ˣ>!j2ەUtu3 7 g+9dGvzddiy BF.T۩ ww`)Ё> `^6mߦ|E;e^$@—VlK\!1<W?Ӈ:n4"G%n(-o1z{U$1 Z~@_;#NC9-(#RYmܿ&\mnO+R5 s|ڶs:YnÄ':ٮdvhg}r}k@f^7N0=MB1w@Y Z+YXNGitfΆiz^veߐP'~!u_D|4ڣdiJ1+-u>)~ȗ{co[l#m/޳tَ %8<xR4~zWQ_2dΞWz!}v~*QC);+ƝevEO` .dIxA 5~ّM=WCT#|34CfLS?zcz;Z|Ǯ>Er7yV2.!*AMOًD aPs&a,=d9 IIFU$CNjsP+ݙjOPAs^7^veߪ2DT%.`vܞ"!F a*#L>oQ;dDṾZav O{V֬g;(3T3&ě<դϫ{֫"pYw5ge[+'\b -h*! RIݗ=M@zkN:IOWdžWwZrnBr_Ug\wpnI&YKS&Y&m::ߋ|,|ktyx?m'US7WS%1mߠ_td1JU&Yt~]=DzT>$pM>#~%7/[B9Fvo~(.0}H-plz  K"~p/Ds{JE|%j}w7i1A!«,nMG]X.Q(¾yYґ*xԼq# ()HTф(;Lui>_%9-#麟}RF7!hzR'ә.O:G?PW'>L-PtN$D׿i(׎E#Hx>ĖuER'٣?[Qhb}Y)0qJ?v`< ryٱ '*W`cNyM5Fy{)sAIsEZ9ڝBw;$JZ E{qۙH&oY.`rv bs`/ip4_+*s\Ǧ<30zkY9WfA(`/V<˓[;yDs h/ϞDh<^rc|},hY;åxwٛf<(CxF{;V?Nʏ|mG%'۾j>#񥂋!agwDMU?Y>ºYl~T>@7_i|;7<+EܩW J -CDZcikCsɛnEQ6 n- ӣBfKq^ׂّI194w(|1U9,\{wqg_}Mwlgv[{}cX ~{y+D kwIǍIQwcגCXI}~Yx T>?+n_UuGe舞ߏ%t~k8v+3|hiRݳ0sǧ_z/6:L*i>Soq?A'Sߌ|ٷtά|YtrwnXjg`;A֘&1uI2&o bn.ܝND%%a"*a`Rg;(7f;\NX3B e%a{Ko*w-T.Rߎ/oDb~dQv|ҁÛyW*`%:{gc}g?.^psLS; ^c؛ ,,@*/n eҁUl MzǍ cv )y2L$T[[4OiD``u,pc3(C_+lyvss_<@ \0KhUDG-&Mw)*e5yw=\dTwٟ]eZ%0/vNf Ԯ|gt^>2 Ge 0~=QHS#& Oh;;-į|ky|.Ȳ>]{8_4Q GozIG7wzv]+1дCe#l%'q%qrUQZƑ־o[ue]vh73 2RšO{=x%Gl+mL@,ϝjtV@ v0Hgh}8DjΌ{d:% 1Ǻk$@ 3Yg D)h& 9؆nJh'gT@''̬y՗ @>p :at*^3NSK)~[ > *o82R8ֿ}(d pvH"@xP9>!|^+) q=lA jq剹+z>zȿ~4赬eX'M>hSltWmteo:{`zQ2&0|խPWu.ڍQkQVℤd8NI N~0wV+/U׎1`on>b(nHakrn}gQEy c2%[^v]ɕhKo<ȇ+Y~v>x5gb}}ڿGcu3Iϧ7Z*./O 6a|P(RZg'#Ng26)i!Z1ǚxoOF6apZyvI?v$qe;Ɉ]&Ln= b2וNjx̳m %O2C(ޛdžY~#m5-8qfOm1]UizOo :77gi9wG}fV8cئ-]}5_pACJwVm`pܤCFm xϐ}wb> & 7x[RʽT,׌8Ϯ*?ثu~5Vl?3̻ p1I_?XR̯hUH ($h'[xs˫ox4*EM/JeٓWcmoa ]zRg܇;Yt_t=}VO%.T*؝nݵ:cU&Mw3%Q$cgqv(v!N]Z|t;w;)FR{s9G??bĽ…}}Aw5b;t.MnMl졬U @)_^raw3 g?LYo(UH"i;+ARIeTYpWۻ_ 4Yk$;)rߤNhvzQm|=D[c+|bnRhƫ-~`}~Z;aSpF](t~;Vn?{vmg+PlG'rgIhI9R?IƼ?ة'Nj~@0_˻L}_J $ڽtA ^Rֺu9/VNۉV9L-MjE{&{ҙI7 φcʷ?lO掝Q5a40%<~OA%ƣPQ8NEG+~3'ψr@⅖GB*GH;>g%{џ%QJ݂.0u>nu(8N4|e=YUcI~6wtlx*ͻ`Iȏ.2;wQ{~p:~Cn=xé$MlO3t?%ڰTT+LZ ٦D{s<#cDym^^w;0ݎ[9SgZ¥)W马XB\7f= W`PHwaW7nqbɎS8Kt;4QSmWG.}v](ZSVMBNc0x\x3 6.&z8{ 3S)acpV/vi7xAtП8Dy<<$gy@\9>@ 0؄}$sօG5;'WFs֦/Ļjkvu6^5=Ƹ{ms/]};|րp6x'19iM׼|LW|Վ+ 9qrLui*].X/~s vJER9d/Ъ2=˯^똦 vo *fiQٜ€H/ySNY vL~=JEhjm>S!i|O8ݏHFT[PcrrZDod4>}V}j89p%#xg;Iq?JGNCe  EFK7)eG> q?7Ocr'cs.o3vǏT#m}' ҴdNqO;m[`^aLedvvPGs3cowup!T}a$@Y;Z7+/ 3k3=0!Xt8R=<+|X!;7r5i9*Nu2`;f8F:Sm|81S ?|AH:9t떒Mc_7b*L=KRT|2vQ_:~OcQK^uw>Hfp%Fhy]Pev*De +H;~^?/Y@@]$ Ŕfwb!C]tXTM zc7*f XղhqJ?d#'*$מ:w*-Eġ0DַxO?v`]2G1e %®+3rD8%eH";(,V#RсOFeWOv'Y9CJ>A>NLXdCq$B&32׎/{ZЏ f:K>0}mg ul\L'l_K]"M}GO&a RN'J@E2XG"un݇uYSm/ }ٯBPgIwD cNQ>ߡtsYNegCl:~#A =l;8#A'4A ?:IA}J:]g0=kAegJ۸`{ B yR{ZA9 c.58S(݌1 nw}ǜ8$k+A>:7q0:N}bX<` Z*pQ)c 8Wiw5~gK[M8B՘ |%^1ϸ;+Y&0v5u'|ܲEH|vv;QxIG _8NEx >'pfg @V_ѮT(CmD?73Ϥh9ճ}u-,Y`睠U+E&p̠ǁN+41 X);5~{pڵ箿ΐ9*HК=vFs*#/ʔ<դHuҬ jtx;ಯhs@#E47QS_>sv6ҳlKה2xZ|O:ˢcD/;x_T >Cx'ݐ{rp;w{z+z h_})ww|i.NMפ-~W cg*vNydAʰ?qvN /D~(I \ЙD4QB:]ıO<ը 3]UnrVc1UA"/ 8?o\q 40i 4JqcUm6ANʊёwjHҗMƎfӽ`=ŵ/nzﮞ?*y<'Ŕne@ "t7|c1 &lq爮'u Ql, o*v/#[+ceFEݔ%)b|l-\yoqX9v @h?ؔ9vճ<.ʓ]KG++I@9qy%{^X*a2ǢkKQFP3fkf(sXDe =69Do3_C;g`R."WS#~Zq8s,ʀ1C8/^׎o~"$lw{~ xq0/V{\WG1j?3n9>'O/gZv=j+FE++<#3)u6_OX'ڤ`y@zy6Df-_m?NmKiqGAߡ}_sT~y0^6~W vdg7oE7k5q)2%jbOgWҝz/)y NDIɀa/Mt[6](T뇑D!ko}$&6[}sv5\6iE6<rUu(,p5š4i5!^Δ2Y¯6|Jr$@ܝ:*:䅀߶o3L>VHj1* 1IeZWl'DN4ƃ1Dž$CM}˶ ܱi ;sՂFn_zsJ!k*x(uYL{tּfLO$rB sbum.= ;6+1ܶZTmNٽmS c>IPӌqi>YW(EIZz 幽Td $TE9WNY GWns ~y_nxnmsO3/;!&h] V{J_>mAi+ !ZM*NT G^_ ߌVv>o;3==t|wζvKo_Y\lW} Ͼ V`9Pel &ўged 3y0ZG{󂟑Xy.8^-5˧Bַ򺅥IaNŕU.-gyƉa_újs*W=6k~*wsBį_7~wTwgƬZlxmv=j=mSŁFuiת6د%Aksgvꖚcutx(W?w@63MwQ.\~nze8 kgHZv3 >&1oOc"yL77;Egn8[vF(eu5>3 Ka#6d|Tw4c)2wΎnUo_&=p@%Ȳ'id?٘8?~QBq-yvM=}GRlt2eݗn>w`ğQk+Kރs7_D[h~W%;!Y1h#YYI7O\Ze5*~G`ф\qZ; U״WzB`6԰?ѡxFo9?)c+^e#EAeR#Zz, $=SD2퐦MnQ0 &>$uSV @ui|kB"<j1G~F2b-uʊ4͝kyڡ?i9hә M+>W3$s6G/4yqpt Y:d6Gxe\$8"è?[ 'ďedU^9q̝?IA_# ;zBw(kD5YZV6@h c:.G5׫St}F{yÔwH4M3|W)̱-c%//^88t1MCd_\ ѤV F>֯_cVHy-S@@Dt&w89>xO#cS@-枙<)^# ҖH7J}WE&:Gx?*iiAt{׹dyX^䡧YT1|N=~ؽ p 2tQ&?NOU% 񹉻-M[[ckTˆziΔU|j_Tq]BT(Å#ԥ +ϝ#N%\r54Z$xY/ |ʹD^ Du'vuw&.}S',!0'8*?jd{R{uMlYXkE8*y J=9YA~0,ɾw*Kp/~&)FDP8]N+z_듌Az[ʴ|aRe'_;_z\j?ɳgT~> -]ZGU> "c)߿&-%}򠹒-"]lvv!@WuX{;Xlh Nk{PU~ySVк)Rlc~igfFė9_#|ݺQS֌?RJp6 viLa-O b>ZzY{Ϣr_^wcᒏC;Þvqg_$fi% q+B9n_Lūc9Ӓ8rY\`3J˗%[ma]ZO;d-MS^1nxW*raNO]q{7vsz+[%P֝+Al,ݦYubvC |163[Db7 O0rݏy|ȩp>8<0{OmxXx/ JϡrǓ@ڝ^t|-l%M'MQ_f7x4!a9y`Əx@sW 9g v|Y+=Lw; 0SuV;SPFyۅO147AR GD<۳ )IP޼ef~Kfhvb!l`sMggΖ~2G^/w耻C`4');\?I]]B 7dSL*™up[q՜'3~]gWZ Zm0I(yT>Usk9v(NL`[;p9򂼐5Njks ~<1J(Lxx& jhS=/dj4t塿ƼJ}H{Me:wxZ0R'SP,~|72]w8bJ:?ihUKo˪ʷ 4ys@-!{?wA87Oz;yA#J'I be<:Ha-^_`.2gH8.Xǘ& m+iikȱ-T^&4TP"؁Ƙ)}܆cMzNC@WlW\Y%[\'V4:Ҵ7]Ru2@n~K**ĵj[Er\YNRIHIF!gh6r?5k S *o}Jk8ξ9_/|uo+ѥv;Ksŀ8/~v=>/w'\_7?;8͔_9Z2';G _zvטu5 iK;liٮ})(x1xl; u\8ׯq6,#NMYWf5$v= }ixc{:(@Gs^|NNB,OY`_w*k/Գzl+µ||/+iBg rczկ`-_ͲY3D;c$,UsݪԫDvo.G}|Myq$%]~gMZ;nh }P0.C ,#3]L>a~E~KaξMu8 by3dzEn}?U29A=f$OlBDhK_R1 ﳝ"ZG(ӵu2ʞ_n_Vqv> u?Jq] 3߉R+P7,FE{v4w ?5x?UY|zׯ⿇lciTtMm m"_ԠIGBrkcלpe"9"& _=FC;"CJΫ>8~>o'rqN̑{Vٓ(83و7,O{=+QY/)Va8̴T:΋\#x!լ/$2@>s"<V/ۭ:&? _]3*{yE$뜹C9s!?APNWyHNǓ5|c}oK=]`ticIh ^儣rf0336ىZd*jwGfı}`JA9 mD.,?%?Q@/miǝFәLH&<D!. xI~dA{u:]VnHp&/2_ ֮Qq|߿R˔ f1`Uc[(q13ngcw+h|?Exk  ;??iľ,ɻ/4la߾MN^ 8E~ֳkUu%nlϷq_r@5m61't(R5%A7^%*ϫ;rX<80?Y0T}h̲Y|+]:qy9_v[Z6|/u:iÐTg ?qvʥ%Y7eY/tDKx1?|+䗟I,TmGKR8]D;`)e@:.jfjhW2@[U4M^XSi-:.js>sd?Ў̯if~zIx6F euݸaYWgiSxz/^!cXK ^FE5T5PH;  n6)ʳ0) +^ŠT7h WhddE:.brkp #temB 'FI"p6h >24s/[RS9;y3GKPT?նQV[egMYXGoʺӛV42/ ?{YVOfQ,v~=،j#YU.Exl1\2/u\uc׿ ]R]STuRNνyyI>+n%[9iBoVtT͎=G16^_ς@*)H䧬!\::rjP :K*8ʦ;wm Ku~ <% NͷU)3ڦW`tT0\$wkǯq.[V+E&TO3?_l= P;/Uo\IrpC㫶lX*M4z wK/d"vɇ&tK`@r`hxOT,f8|LV4i NBD+=~'60qXx\kvk7kpvwQN#۟'nr,׃D %w J#Ɲc4ﳌ:NnXە/zjOGJ+I<3aŮ(θj/ _5A;+ ޜ-)6z~V =X86%S9dTl쯫*LjP G@|@'*XWr=J\(_-D .ϯj|įVJ:~v1$ sʜ /HxOx}]3a?{m`W_bCqTBK*_| :U} R>!CCh9̳p=f3awl~<JϚDѼ:s..pϯZG W;yU'B׼f^o\t*O }_zsSTa_,y5A ЊzFvOwKp-_ȪVE."AĉDκB0lvYt@|[=3(ao۠bAx]|ۗZBH#v+ZBϾ".m:O }¶QF]Gw{A%]n \F›kl|?xzi 8wuc&L7)ŵ쓉~Ƈ9n퇗%mbK{^_ԢylOԗ3bvJyaΰ2. /}wwc^XJ:hX䴅' p vVz٪tD [/aa]tjVb99"Ia/]B N(x"6X@'7PG_$F!^*ҏp 2l0"\#&:,Y,L2@9l7d3iK)}#;xBs=ygS\lAl3]-I.+ >+PVzg`S͠`L֥Jy%Ve٩/p?_8T:DyC9;6X&) ºSC4?=`[ S|٘)؏ӑaIs1^!izs:2n)n.=?Ί{@i(Slm &$)v,\!b]hN# :"Ӧgu +mWeVO>E z ktgm~ׅ>NC;|LzhQJe\~=n!ɋRu/IouK9(Sr}U#|7 uT_}E~[vM7Mr{;8Ez"ᰡ_Ln3& + O&DI -O/O펇p'iݳ+p=)NDވ8r/]rDIԎADؤ=ʦH'~+_%>g ?>D`$*C>e]_t_JeM,}#Q}gqXYB:R#v80;#OYd_,eFǣKv+3F2{#\JFO׺DcQ M [#^lS7-$f>^ޙ{ƿ3Y )×%}d#ւ?.>{/^cʁg؞zn7xI4?8=5m+ikLЇ**ߣs0m@ދGڦrqHfUiG@tNdnL4H9% NB 2%1w?7Zw7J(Z" F,P?H!or$H]diNb[e@GBaM*g?s3?JƧʼn֔y^ :6 aHc Fϰ'Bba{3{(z0b3^hOLfGd8Yi À D_L6).U!7shzN;zӳJ]g0mPuQZpslJlOa!z׏V9^oؿIo.EXUm/|k9^Rէ'<wu?c\,k,@ 5^$-/П?\Lc;@ "H @9N9^|q|0 IsSمBt[0BwN&mIkL^ ڊss8|֐/sw.#lbĵkd% k?zWt5f7XySz_?_vԶ`/&; XpbG^k]'WƮaGEة OU GsS_VGWU/OV&i㧱ǫ,7Ju5si鳜2jnUIp,}{^]OM~U<>hJ :rIoUJ&>o@*%^DxcGl\A!, /C{>y쭟<ߢ6t_u(qOw N34?Fݥ+PuH@}GS*76>["mLmԕ_,P/UZƠ iΩ#,,88q+㝢_NeMè2r}JϪ~M"JV_..mtfW pF+:vz83DI 7佤]+^ʺV?/!!w8d=)p8f p!D- ߟ$I]`fy h3B,ݥ;U/5T8Um!\o/^/r3+;b!m[E3u}hL'11(mj[i>_fo?;.֭+H|*x9`( i-ys\fV,}-FTh(w/y?iʬ=mjy4)qI %ᮎו~8HռEL Y"\H#/zWgICnpLӾZ7sof[M0Mx#NσouoD,z)/H7/]3w9AycC{Ӝ3Fl {}օ7:E^}cv^c; rt"GY}?Y Jʹ^7|FۏFAR*άv,2kLBe{zd1a @F4scioĹej?r÷>%Ә9taBD,NmAERlMG9˧2KK=z`Jz񜑇ֱ /WY |g=Pޔ8xAS`wozjv˦dD"gy>s"ŗEtZ=gĝVXS>YZ]Q/{dE^ZVW!^e]c{d ^y+uї(p<3ϩ^oMnᯂ~L.ԑv%>Yx]!΃T{n DSpv&ϝ,[z7 1_U☶ql>T-ӁymGRGozA΍io|c%L:[7gkzn@(#jrj?[۷k^}k{{ek!V4?Sqs%n\f/CX=|)!sa{Y{5~h?I \-P/u=`F\*#Yoٞp֜^==F-ev 17}#O|'R@JVL~8w{D*Y#W:HG0?c\Jux-/U(C9 s}&_=Տ.:62|O{ JTHnR|y[z=a6/Byu`JLD˴HG0|iMw⸄ƹBSL<k8A!?\C+y"cb~;G]7EC9OXI}٢pN_ YK'')"4⬕ 9] h9|34'"ɟ4cye X'8wֱ;2[{c꺆ĒU@׌֫eBE e]DTET̥cޮoYE.4>'f% {xOTNrsb2!,1",<n)*K}[1OIX|ux=ڙmU+Dw:w+'Vq'" ``9t7"LO>]}B++:ӇY".шI\?Ǚ&\Y }X|fX{y5E Wub蛽_ y@!JבRUqU*/xb@G1|}d0ִ'j7]{>6eGrg식jWBo[G|&/([6On-֊3NælOLOx).K9k)9%Q-ٳ1ĵZV=UUc>v.5\B=9V`x r3pB.s' -0~*",H$̏Hr֧SAcOtKn UW= +{lI,SNY=iIi_m_ܡ]hj c(A-MR1=/rS|:t?V/6 -u4V=lO#~Vcv?x^IHU\fV_xt86hjN65rfL 0]u~@Sy/CQȫ$Q__0u/|1t~Z45qR"!oGNectr=8N/ҍPxWy$vU\P,8Cg8 L$FقҔ@e %ؗ+g\S(w, JYI"a9Z~,4Q9V ?T[L|Rï/#Xߪ u㗖=sd1h-ÿYs=I |~*ճdZW @ iX,0 YkZ+wG8^'p6i>,nw |oD(FP?^tM>x;OGK}^DPb2vS16`.g5ה<]&~q[;1GOISc$z8,2ji8XK;i;Pϳ5Tg;"@GD{vk ^#,N+qv˾;fr~r?e;aw~Zm/V;[^;.]{$PԞkm-6v}Z. T \mAH Qg;%r_#["*g-{*0+8>]ˏ+[3x]t#Zhv&n=OX.;:NG)e1ٞq%8g);7]]yM8:"H+@ 4|nfuu?ч,£t'E2씷N k+}xՈFĝe1Ja,Iy޸=Ӿ7~_5ϳlqOS|ޑ`kRm2 6TDblK1Ƚ,/j ~Q6{9T7\לba>xC(N&k% T^asB:Ow#Nc{93SMmZA64]^.qh4@ } ?sj22—VVkI` H<^O,MK(Pkc2A e Xk"'pPX z*8)O;,E֊m~F蓀5Sԅ9ZqS*zOߎM[nG%D`N/M"Bҗ${6l̞DUcƽ+[SZ@gZ]tWsw˫'5煝Ei\uwG+З vx3QYGn?KZ4;|b?.D? ̶b-[@c^EK].|q+p'XV͟Q4)<nMf^K9)aP=Ҥc.\ [:QྫྷSNBOo%%_P^.Ц\FR]K! jRԡʇve3T< k(rח'e2 w_D*PdՆթAi xLM|MSݱ'ƂOYfGxӧKI3JAZYP[?|^ tr׸wtm~P{1eYʽ|Gec8qpyY12uBmR$`NtyDOqiX|G=h [1hy[$Рgo12jȩt׿~S|8o_"8qOiLOA${p2 ^{_TesZ|tA:thq›Tj~C$tY0`/뤣E]RB3p<^}7E>kyԧSD\|!;1ܟ_m߁߯ br_ WE?HYїeZt0\ERPj=K3 ;Z4ʝaskhtREwTIy)=i zI6ށ%ݱX #V H[hSDg %٩E_Яu&nOxmFH{z5{4yfp˧&bMl]S_7~j|HfnL$5?)[2aPU ?F<X/ka 9 z߶baXhm*t(&IAt"fVedˤiA/^p}eXz|GW$En ^v7v&Zj{`K@g/2)}m/9[[o[D^ی @7&٩w$@|E?Eyz)ܗiuWw|UL >/pJC䦵K !2#aF#QNy_VST[z}fG313.Nݛ3?m."ϣ`/.t)[^wez[fYEp%i41?“MFK%eӞoO@Nac~^3`_M4%Ӷt&r7)@pcI+Ư̷8 5^2vGtv4]W4FwxUx~/%Pn8p P=x i}Ф1EυϱBFCÐ:} D).-uޚW;Qq̢_GL&-&97mupJ?sβ"6~0 ^BOYIB6͂[U]4fůC)7,mi$|1Iҽz):?J0~al|©quQeoj8O( urYo;S]~,?Hw8ȯ%- Ð4)7LFֽIoUrf(k{h;pD]LqBF>ZɮUJBPdA)ΟBWT eW;s5bN՞lu[wi=f[E;gnv[$q>!#%jП7a@M"v<үqo{\^kg.0i_f8MNLB0ɏR/˶Hl,b4~WL_/r`y؜־DYqrbBi8v$&ቬ4W_5mSrhڏb'3oWLzo‡ [J{M=l{ȢUqR)8*`Z^9hOlRo_bx^~-8zTO5 8C;<Eց9cG~f}da?|KNŧy6"|#E>=6tce/_"B͊pnh WǕsZD{sfR0"˃?Jő.ʟ>HɷwW59 Rh6h1gx)FP^$XP5VW[@(f:9ߣ*L@'u~iqXO1V>OcjGZ!.f򰨉K?zs/50y^{`߭h40wM⺗|53cE2"7*Lb2>OI/dwrݾOF޽gQ|!RM|h-{vmvm/`%?5|^߾hcs˿g^=0Pyɟv(r,.DJ ;/@xz"A2ZX-i͒،)I?Pt)7fjCp'$HOr.յ#78āgf=@b楼J(b04r(?aӶ:2?j@d%C-h,M0ijڈilc3S:y#;Y|C"]~*k³-̴E:eIϰ^v#p%#E允ݢJ拉N2# 1̣(Q-4bF=Y~7G4bKEd_/TϺ ׼(J> ޶Qt0\jw0rqr7M7"ڜ ^RpzRB)=&@yefO/Tܞ%_pAL gL ʤȸ3b}FF'5@(Omz\A$\:_ՁY/CqRjeAM0m[sJ?U+KlҦW}n'wDlBSBqڙy&PWֺ^YerOmkD‰[7oxTg歆"j\eiB/}ֽ7̩PZv}̫{5e$h[4<&E^:{;DꁝB̠AYt u#%I_h_kF_\N* D֞]f_mGe0CD"Db֒Aqe'RH#^.t˨4;~MS86hd鑀cAGpiNݼLTh]Y:dDTYd!ާUH14gvM?EvXla{YĘE6|/.02 >Yˆ@kӎպCZN >l;rtR cDScʒCN@)?g ҔpH,w={]-yDDž)"n.?78yѫZTbr[$[aQL-~>GL gN)lYD}Ilu Z?a)cNFwH]@:>u 4 {d%{9s#]OǻR}7gdAiGAgSD> &DԑQ )euG BxY]-*X'"9؁Hymn:Չ1k%kSktU/876H$Cן+>G,fRD<p-pDr }6KN4 ,a7Po~hNy=ͮ_-ga'ġ}Y~vMܺ([,MW'ϣL/yC,pNvdC:VXAh,d>C䨧O{S~]&9aebx CK6)\ByoK,`4`yVkBP M})4exg'>,dzd)iƙ8/i1Ə\ Ꭹ[݂@Ǣ[767ت,"&w}ؕƁ[B8f w̪gl|1j!kO(Ӡ;5QM{Ҥ1K("ђyUg#MNHePuV̌ =#M^A<9gPS ,dgP)58r̎%z46yF2@*TWjwr0w<)p??7"+"ruJo=jG4QEJYXbokX|jf_NId=G2E _k x _{~AdwitBD'7:{;"aϱk ͟1.~+2ƹ]d H=+4Y529nx;=l/- ${;u'-eEXJ:QVUSrK=GՖ"ۂc"<"NSB@yrO%RL6I"'B F1nJ-=;9Ʒ[1𴚜4y6[<H ~X4E6P yw/Mّ.} #FCo IR·z`(To%ޒx.gq(7:Ur"eWNqC}=b?|0{PwimOH4#If+Q#l_ m<NLx.(#O3m?Ul|d%@@8aZeXtLagGd$Bn/(J7,rqXhE_\胺 ?6pl/5ogz)N[X=m|Hvv|uQ.enͺa 3i9Jw$Q'T_ H Wjܱl˹3 tP0RitQyJ> v۵s虓$p;P3슶&:5)f!2*fœT)MRq(V5kqsloP{$'2DѿK\1H[҂􇰍}8PѱҠ9 $[deR>@0ឩO "oXp, V }ٕCZFgn{8!񹣵":er[MUW:׃7m;_}*\0SĹB|c$I#:2>eS 3mWtctJ:E+Ogh11 [=b^@?0y w7d}3ʱ٘@)]% J!KԣR)-5P[5Mک)RH_7ELGi6+!~.m TkF@7Zw/\gx@Y;M3O?ge%ۣz<+ydYޗp_"ߋdczc{#em`%z5AS9"XΗ:)^8I3~ߣ[ʝh96cIZѾkDkt鋣3lr5D/ ,[724Gwzj㓂N݌쐣KzB]:{x ˬrpB׸2#_+8!q_;J.k}4Zȩsji34Nomk(eмp]Yc#48,UyLӘtʲm{f>x \ 0~*Xp/ټX:z %:Ǵl'x<iB;}MCE6.L8WW@4%%^`M{{- 巂1#V6=Cen/ڷBu9x_*—Ac4,QۉUJ wnrTwm-ԧ8O N{`_-I^Zsb12fJ+O3ۤ:= q'HOGQ4'# _OM \de\s*NA@1,׸k0:,2bizmu:ǢF../_WTߪ_, tHFWVm?zA'-$i9~wc>(e>P#w@>5YGfVqK( ;dB=%)v :6 'ʏPÄ#}[mpQVIG7I?C7c#F❳S%ዸf6|t#/fH B }]%ǔcBiM,鷃Gߦ9 U cCT)6)Fog{i)|/= 1e`;}7΃bV< F%]ܕg7-a=Fl;өsSNp)_е{L#qzUKR|o"(q5?J3oǷTlfサ|a!{yo-I"/Zys~ylɯGDUYTK_?+~tdF^j$O5yuox}ԣ%B`;[;V:T ;9,_W)/vwp kĵ6x)X&O,;h_$yR/mR$H^jWMMJ=ĥ&ںv4{?Y@/VJ c\T)m[~[ hHg]2#i_= fD@dFa,Ky q8/8[tE0ףeeB e9qRmF%^TJH]a+`kʖ/v8*u/;V!mXݺgta-Zs3HoNȂYu:W!R>\חgee{zU3jčPX/~k/"h$=+ֳqm3Q:{=n-?e^گD>c/NEʗurk^WXgQcǹ LT m uѴ2x@_^yE댎:Uywrʹh|rv QUSE95m//g]x[wUtaV'Z. {d;,+c:׫yvJ򵺶ֻE6ƿJy~U0|iNBQasႢlEg[j](;ޯF?闌x_ [P^dY=l[ ag*X6U&e+gN4qF @Mo/7TkZx8t͛۰OR7?HUTVNQ7f6}]?etپuH 'oeuZ?_s+VIbbeuϳn0]i__d{ܥ(ylE`s_l2x:qjE<8YiY~iagv9Hg~42镚>t#n{.`XFԵHcs 6&ve21ot ' xw Ina[ʭ9 o+sɋ5<_zprZ߻~n֯MCw>)QO@3Oψ8|)K~wN$_uЇD017靶u;gk! cn,2޻%К=;لP܆ʋ6yڧsߪ_c$W; ɡu$T_ŗL7"O7[qBآ'[if7)L]-sԵh9ȳU E|+_fFHWFYhs]Gލ ;l^etx?~|A<^#|%HN4 BN<L>߃/mܐ4oi?9<x/#`,siPW]}/^uCu1Vr.L3V[9qΐ(-B׿"xX)<8ĩÀ9㢐m@_ȗ!/i׭mUi5r󵞮^vKr{HŸŢo Hlmv4uZ_{( ՙNפD˦OOI -,E?P08r.ڔ>țy|CB BTI%W>?\1;)ts6+><l2,"ƣFv2}Tk?"-ԥ mL1ΰY?$U8 #规{"$8uwև(~ֺf= Tz=js~Vqb- Ɯ>$IסIIZk&*j|3 Fߙam1f*ĕҽ4H?e9bt,FLI:~}@|Rfs=nb;x^v=Ss '֐nڞ[ iJ$i ۼ.LGxu#~z7bGSN~~ou2KOp/P>?6{eN[ﺼ~hٷKU;i"1?eF4&%"GIXh x])'Ov7^刺r3~s2asW}?4鞄'mxޔWB QO*:y)#A]u$!E ؋#ȦRiꔹ:=eP"5<ֈS8 S5l=`skMنfͱhY_8jp}0/|<nO-ԸEjߡaMFMX"–#;qOh+{PxEB@*-umZpQ&vq#,H𕏅$l鞯-7r;h7Y|ĉ.OXسTxgynn ]0\"UHqW *p_q/ͳPCʴ=,gI9=]KBHvyCáH;t.J!J+kG`,_l+mqBVaK-}vM_v B싦ܱʔ^px԰t+S|߰W>VҾL.@aڽPmW[vhk|Qڸ,pyZvB2!}qe+;u)¸N' 7<)Vٍ֚}DYV&IQemZ_C+S×(yvwBQ8I>{Up`{7-wOFStUht>̅J<>\<5j͓0A~C vrɿ1CƘg?F=uJGPg}o[Wf,zq:>Z>Ȉ6ꈙ+^FF cT(kM[Wc֪R1EDrԈ"*|uv$5]%i䱊~{[+70So.>)/ME.Q?V]*(:.ϊeS?ЦeVXx>P bʬ4{S u$Xe*}Fѿ}Wt,8x~Wx-~ fM WjS#KCR^Ŀ G,kbl Aɮ *~ Uy |Snb.)|"^/}*&4.xz\Wx?ԉg&c3˿]WC)Oh6$FN8(5acTbw|]Ҥ[=&,,%1=R݂sMᷢa>ݔ ,wI4a}eWx ? hއc"R}">Np"FD(=y=LPQS%zsB(%r[_Nȯp;+ u%z>#ߜ$f]]{9;6@}TP)hH,z4-{ԁ knNysZs*o/|5V&@CyerB-G}pf}0e|Cj*/5B31Nh^g}-\zI 6˭6kلمXׁjyG;5N=;w (@mG S.\ MEPz>3E`en렋b}%:"2YN6*#jX|MRJ!e؃EENmy[alg"]\s;6f[8?c۝ q?O|6V~p/S2-/ٯK]QMقڿgPcc՟J)@kqjxZZ4h d{tf|jMþd0~T]5H } MHJGȹv"/X)`nPk! o'+FຶeׯPAҹ7)? Ӗ·O;%9ɐhZ4?Ff$;XQϖr4K(QYgOY5yXOFf厯3Nb;.yv`\qiZߣjV-Ig:82&QZpō<*[N:W&pq"ԮҺ&=͚BܑacщϤ̚?EfcדƏj'8Apn횋r mgTM,ߌI.-k䆨}Wx].KEV!CxrkbʋU?\/WXU=Yotm`6?B>y@?jZNï3e 8dS됷q,OecEu2D:ҌVqD;HNe@n+M`Wƹ[]S#;@lGflt?M]8Evۏj<~83ZK)p;pgi m}גo  .6U &~-E٧lZͳ.i #z«@fsDC85Ə b* ȹל\8a.q){SmVC)E٩ߴ4|`+AcBO4 ðw= N[^"ɂxi"H'_1"-Ֆ;/^7E'"]][٫/|o驾7oR:^EKj' 2$"4q GK$1},s"tMXIySTdBE{-+]XǃEXe%&\h%:lLCi⾢n ĺ z3D:e 7bүwx}?\@9M`H*㰛vMߙ]8&)Ie;:6cLE [E_U S,1t܃"j/>#>%YQz_G9"_|7^ޭIdZ慏^/~l^"+.^4z|3<ŵ)sF)+>μX_U@ҵKM8*@SYE܎o{Xʯ.MvO<c~Fߕ4-v3[F'3M^ 21WMXݬV&:a/FtuZp6Ɔhh0/Xf=M[SUuOaL!WǬna/æN%ս͜mq˳~iǙ郹?oh2YWvpƽ{6 z*aˍ2~~8^ *ݟU֋rfl c~N/~8R=䄆s D͇D\&`[ KѸ 1s}z>=go:ćSlE?Jn鷈>| 籠EܝX,,tLmo-lᔺ: ̹߸KOG}eJ#g{F?{LB,.Evm2ީ^{3Y{lncsWj ?N();O*dx˂\x># 9-Um];J&[l#~DLyԔmT®lkf-gzv"NAwigh;<ں8loˆqND!՜r~h|Qiu>kϬ6vPQOh^C!qAmoxoE*xtN&[ q*8[%m' *gZ^݉b몠 8k^b{a.oW~.ENz?>}~ Ht#TM)"VYp愧ٗu(=ߘ)GBYn'XXS: Ӷkhѝ$`Kݨ9"PP2!`jDnӧt=qա[Nel-USE {(oѽ2iS*E*0")_iР^ U+Ew_3vȬN\_zJJ-,r*jw{:t;B p$q3K1_Er^X[H)`AW&tӚ%"N58xrr3컲wu#nW\nsׯ;޹nN*^8\h4gj l rd 0"PH0 `Y*ݓ4*C%8y C: 4_XH]wi2f 8nD< ^l~Hoa**|Uvu 0O]]~WG6@]BctL*QA -UUK@Ҁtxi'OES->|b,Ј!$uHf8&U:_ m#-B p:uB"p1yhÛ֕څ<͒h_瞭ϯִ=T5vWy|R}nM0nNǧu)7H)Q%hu[¹"'X(W|C]NG>I#ݷFfY$hSf?N~x> Pr6=?>'Tiv4#0%Y$g78V4K<dqԯu8l|poI{Gu[ٹ09 D N6u U%'p^T΁DF_`88Ҟ[(]8fhA~wV%Rjcmv4B'[hS؄5EM9FrUڵ'vԂ2PB2qןc{NRQ5.x<#Zd -"}tO&;o<fʐsu5A~M12MS}kF=vRd\ h(tF4+N(?hNI溔61EFϹ1ԧ}Г*wŭM@n7k:3t?11SE IwyysP[ئF"_8t쿫B,4*yDոBR{9H!a,oZtȳ2_`(i4|ͻVW^p,J 2>iEq1U9KݢP.[-pVp;|3ڷ]VgYڤn6G=Y̾!Wz2XTQlKi42B~Hµ)`VJ~S m(fǍ$->D+{Rhhs ':!ІV:2$ W!2%5~UR8 I Qp&;x}[?QZӳm={BF9WR,ՓS:K{8N^51~WR^T8XRFgS?pn yX6Mc s_N*=cdkj#WRZTrC;3E[~ϰl op=S\kNHL\&i4 O*;E{E}Lvb,r,w02 Xn_?UL'<^d\e6|R%xdK2AjyDx㟟׹C6E8@9_V܂&x^lW-I+b0E̛4!1[h ud'Yss@&7'Y&H .D0]%CCΈ:Y5C2 ;ЮMrzް 7ksHY*ĻOhc{h&>_o^&xj(ɕSLqf99L5\K@.q0Xao26%lpvU5եj'W\/Xȫ"*GG> }S3)uZLVV헋#m㢾@ASSP,G;>QGĀ AK`WW=kgR(GOS/(ŴфYdUv4.kVig(5Ԡ \,fH'iS L;rX\46RJ*vI!/.xmM)҈h%" nt'nh"+ %ڷvj >$Pb¹Oz!7ϗF,ԡ~y|I lT'v7̉>Ha>~'SgQmq>^z3XDA,o~弓̬QSֈWl7ާwF];'D`Ud2כX{D2" ҈-d``M=&Ngo5~}Weh?zM!6V9ɎӴ(i["{G87]=, yU nD~:6 2Zhe6zop7iFP8'Eb_xZCoH=tquuL;97^-OͰ$!B%+|\穮(:2G÷ HS_5ϬkBYS'/:Ȭgms|s8A+ k=չP7F?.]bT|eofO8"FGLUZgWJr ѹ[ t˧x#v|MF /C?ED1cUMRcC/)l%C@" _/; t6W$#]ytB3@PT46ͩ%aM3?}\7?u*C-㻷=JOK?_>!Z[+Pک9+ZEf'z+NMlN* ^N(Ȗ%9ߺg"njo: ݏF\vba_+7)mT'TY)H`yTsW;/I?_F[2fd_䉂pjj&;rSGIJ:&k=ö?BS`݁eM%zQ"dI.MCoVhް\8RYplhC_EtƸ]{}jӮп9>Eo8YeڰnŎU4v.E jⵐJ--je = §b}B\I=Qt/.뛦n.@CC>DU)/ 䈛p*#mvBD٥K.8^quyZ4d{tTߦƳU@w䍰Bu0:͙mغ*XTF=da=%dNj= 94SP#V}!b W鵽Q#Ǐ?BMIBiww>2[j~vNx\f)>D1+q02&,D|Yi@yP| rn^}bTZEaEv8Tfe2Oc"E[0ft~Ό}X!,`ʼn'%#Rn|4p#R(/ O;EpyoQfZD$-jY9mZ"(1sGhym[4#n_4!e.59z13YQCBtѩp1YFKZD@.a/p**ؘx9qNP֏#a\]W?_U}XkbџުW_CoQrC"; 1XL?V: e2 WrIIΉ+g H,^*i=A]a;D^ +Fل4M$Yqسnt9D]o|L7 4ݗRL5݄vl$EsR3/_ "{ebɺͳ G~v. HAS4UR,~[9p uGγM6o>ވWpjEG3*$͇dlcF52}s&ye@roSӋ5Ehi~cD8c1 O=3Pxg-X$+6IT^3=e.ƲFU?eOT:ܴW1 uYCfScQG}]Dth w[ٮ@g-ϻZ<Ye7y"n} ?8NVx-j/vI0^]'){%ě= Td +L8Y*uDС駭ҼP+h\>Dm2Du4biPqqQI_OlfHorc$LO10-_oĠM9fHٯf^ޏ@:8H /վzymApCPidcȓg JEWEj=>+C++=9I1*jgOX  ~Uv 3uDi:'/5/&jR2<-31VPNG*GeMm+kX2da~CNXu; *u5?{E:B1J.|oFFzB"X~D٩~>8XqL{8+_6doA^3FoP|rMD\?T a M c 1`i;NʫKƏT,{ ;])y]~5xhE.2r뉎*oGsԩT,8@%,pEf .WBe?i7Q_M.^-ov1s] `kh񭒕;zLݤ [=QS?*y:e"EȢ6ӯ> Eqν$ 'Ø϶s}-.k3Es \]"UU#:#z3z&ǒ)yo)ɧxRg9gՈ9S>ؖ-2)D eH]s^t[*yDyvDZ|{8Vw8J%,6R;~)!~w.wm\קl8zCsV_Ǡ̏vl}B5eN *vr^Ix ^UbqٷHH.+MDO<6Γ<>+ )F% ZpiշbܾxasPNnȔI2w B2 u;iʠ}u_ = ?.k+ OfA_tIZF!qe|)R{);1/$4֍8}:O%SZ?ȳSx,ȗhh}l]\^OI|4ł}nXgW\G\xi5\:Zd $"M5=zg,z랜µw͌8Q3)6oF]=T:hgS,o{W˷x\~!tQ@mXD!{]@觬-{AIF Hݴ>LVn\yV76ޤ*:~ol+k"C4q b\Ɵ}\ĞgGK&$%:xV%kou<Ŋn"5e0WƪlZrSOgbЪ.w(ޢ"q⟍c OV^]G*i0*dIŠD7G"me@x5H,":( `l5V%#+̧9)DQME?Ŀ䲴o'6+)m)7Oaw{3﹈$usUZp\?)2^^ao(|VʼRKi#i5y Jz̖ie%3(*%\ѯ?JcJY9~qi,^pɾ7&lZu]tѴ8I s+M~8yG00_ژk̛A0f"Z}NLk1ILrKzd/qjl`Q%)͜e&LI{IO>]p8#7uʧTj;=U;֥O"1EU^vJ#R_7MtmxFB\N'*f>f"MVz=Uw4 8L [ES( K-*d^G_U/]icO/Tt+bSSÊͧ>WF}C'0sk2=+翀/ǁ_#,==(39V7YۼX/So#yEKx*``vL^IoX]kKYMNTg=ܽ W];&m6#b VߑmJ{n B{;K%K՟O=^'qz)A'0KyӅHErK%ZI%(2h~Lk;H/MC"P.nkCW_%@_iđ>V d&Aa y1NR]zyZ_'ʤIlm}iБރ.ϥTM2jHF"-؝Qm%-4zmo>)p|$ ctiԏAWR=I>Mr3TG^Tk7׫P$/*JpҬm9ԬFqЯSon7wZH}X_!sAk8GwV>XOs1ڧ-R8= 1\*f{Jic9b&9spYMVjqݎGX߷c(9qGfM}-q~#gaGs{QyPE>ͅtߟYJ!uy㷌b-NOXN4$Z?AG#hZPP|Ӱv04{ 1W;E9U3F+Nڒ'֨F_{!NI,ȯr*{co# AT΍d6d9i꿿SN2ym{C;t>d{t'7O|Y)?_g*Ea;EK|V%ҎGCV5Y\n*:nB  jK$-)f^M '(4yg+M%Cc')GVklE UJ&y<56SNF=<6~1 /kl^S˩2Jt=ݵ?GѫL3Fa% %EXUrkDwF2R۔ش-JWNK{&<* GWp5ZZ#rcFan4giu~:BM@r~Wo?$!}rpRPȠ5(y|se!?'x:,l.˧gy^ֲ<q%Q]F΀ؓjOhZMm)!"!'=Ǽӗ+YexS*i`;}TjlUs'Y3E+\9OM;YA7Rg2p,r*@׼v3[hp[tkF{Fnᗺh/]Ǩxƞ_RM$u'?m83/55oW"ilc+s:89\?3)͈YiIL5uѸ s{KXeGnd'4XVm>I-?yGړ@A3Cb~L&f WNi+ HRv:}*X3g` ű /بYgxqq%cV7H̯ Q!5n> _LKO) tXS/y^fPIJOFв&S1'11Դy)>opDz.*$~hz7Upw jpOýУd4cCa!时X:cD_# "[$Hc+=^OβY#d6c0HB۔%ux+ĈWm׸uNВ' %58>HAw\g}BGrhpj_йuq#?{$}2͂N1(d]NxkɾҡyCnщ, C!IYmUvKۦq;M#OvDwvNuޞz#Ew[k^,zFQpKX~xA?MLK=E!_mOGn zd8w}1ٔdo @ ߋũH݌H#AiڍWTK*RUhb{q}!Ũ%;jqƽD˨(K"$Tk`T i P>@!Ë]YDmCҷE/oUڪ-ps/<:!4i_e &s$X觛\sR{*Lӽ6(]^6Z#Fٗ(6:l/S3oɿKKEۓX嚁wG/Zԁdiq꺅'M( mm/˞4_"*pO~f/ۿu>v__ÖVEўh\|}rSñ!k:%addZ/ G~}SWT OvvՆ" L ۙv J|7$@C903F_d4g =@}1IUt"'%q:XJ9zy.1 ("q+ݤ!#@L)NGr7SrpA^[?4sSUmE!&&$=G?S8 Wx@+6}1\e m)l:@`L νA2<,'b ! *s(.]0y$-h"YӒK,~aOhLf{cV :4_ldzI0tunjqAsY&Cz^HԯJ 4̺5v${/+d<͊> ^bѮ8Lٝ-X~WEËCd {71k0yƠJ~rɹ/DBUhZ'ls3T~<]M~2ot/g}ᾌݲ)_[s_T?Qhф+==Et> :Yҧ'?NQJӱ;e?Xeg8{:>nq2 aE12]橉1j0҅"|j:mK#Ylo2 }__p&z: uk._}\S͘F^{UxB5eb^mi,L\-בQC3˯q^yO?>I?o}a!ߣwyoKݓϝM 5#ήzk$4k_`M⃗Ӿ)/,cwv0E#m?iܱ_X j4ͮ4Xtrjvr)KSOc@oj7Ck{=5o)Ffmz$D~h74YDQDD\^?-ZbPHLoE` .8%(O1KW?gX҇?&$բ9Xgi|Hv"PޒCQcQ:"<0MRf-bm!Z%a;5XxLjf3G :M~K{u̾4_˟ZcϢ~tj?4W姀^ )}T3<./CQ7Fr:~s.2샭qӢf!\zd5(5j H⟢0㩮V\CU1>tM\҉ሆSC[:H-誋3#'Oh)F I Z3K1:ض>B2S"Ҧ:-]gK3-#?AΩC/ h #eq>ƃ)9u<|I;ɹ4Yϵ29I`Omr2y'I`|B4k`R{a6/K=/eM~+ALclRVtnӹRؾ1d "" Duf87tKLyMF;F' ~^x(+`>`ϩ'__gCו$NYlkx# !0:P"1mOSivQ`_XNx+' e)Lgo?h "+:.=ia_?HOd#d=+G iW:Ӣ! dqD,|j9]򏆴CQ'u~~zVOc`+; oM_Euv B RѸz( :;ez> {*F5pc,..mjK|,Tc ܷgpzLb\A>/.Nڬ)²&[TmUtbZ||3?bySx7؅81 o_e8hm5K±Kw|\mY6+)Igv+8L$\ ׈5ƾoRZAk*: WPKKȇN]Zm!8.91 ~_*K%]C dx*%+Hpqa[k&9Z9!Z3.ejhJ>}N"]6kr:1xy>OM>8$xKe׳{ ͹ͭ\]\ |34i[ sdZ{|^^<؀) Yh]9R#%/+9LON۫6[Bz[]´dγ^]o* qB!w.~k) 9ƮG?&FFgۣnpbכko?+2Ȟm>pvm`¼Xq w23z*^پ`k_ IM1z᷽~{"&v9c*oOO6mjz2g6 RqJo _;"nH5V)6|/am<%盬 2֬'s AE.1؞??[oE/= mH:.&v'zb 7)3AYOR<X,rU>-׈*ΧW^Rh .By-Sަ<_BwL3?ڛ0 >$/ɘfd~U}W9t:pHCGF*\_zC~__ڃ7IO_3qux]r3{+C{Z2?*CjBBj(2xT$6ON3<6/uHCY]|T:z53CYsZem9" ', AZί|2?,{on‚Kg΁qD;DmJE|Lgd`B+ -px`Dr2[;+iT+O a oޒtPmgƧ]Y;9Jcv8 Ϩpgi9>(GI'Vha9y{=$/.#Js/R瓄pL5S |Ylut o֣T'ZȞ0/Kh?o6q{WuSrigKnXQ_LF1s9FLb +Ο0i/G0"f/T2]}i}xJߜ)FRGobqIEswyJBObf{_5g`/{UH:WY$)d{022 d)^۷$ 81V5>me=P?2TL]uu%N$XOc,wt2_92~zܨjReol"N&4A6vg)%G> ;eE MD\'p/2m7DTU%ۃEGc/;VGRq2:$%i4 Ohkp|k8/ `vh"gVĬy]ZѢ C溷7la`[Pyhȑ*W{l远> ZeMmOڪS ' jGkݬ_R_tPӆ=x)fRy&-N7t~s]]/688NolUB;c4OӇ&pDȚm,t2p`#կ+ƙhR}҇偸T)aXA+"`/g(zy>$ջpOcGd4<=mFa :_xij<{;'4둑3K#B48N}w")SˊuNM_t6bj>=O; /N1+`vo__0EV/Tz:Go=*O&r/NKV4Jl_aγwI yNG̈#j|TȮm)eϚ{12N=+wSd9HLֹHMN5ȕI:!s?H:tdP,2׵N _?_`=1k6|]g0_]䩒hH&$\.q fҶӯe[W[롑_~;7Ƶ?Rҷ EM6 `y1m7P: b.i[͑5-JQA2]R?ѭ}CwrZiSNz?(DH7?'Ѧ} H/CPN1jZ eFS{BeAt Lc_s؇hfdL@BA'|}]ΰ .u/S 㸅Y&؁ ;dN!&nҀΕ/iohht'e|L$'mqD?c帓d'; 5.*W&vf'}ݺ.XШv_`m_^#ՑdP,62k~3/bKT6KpMh758HuH4^C4(pYyZ7gs2;UCΣ[[`P@hAUa F˔jۻ@wHGٯ2"75_bfZseE':z[3 ]Iw%l9XW {Z7! ͸Vd~Mb;rVhA4ƮǯQaKێdžPZoE ;rx=8 އ6ԁHQrrKRfj^o״ FӕèӫA}sb7EXs.vq+&c }P\59ᛟ yIъQ< 4!ӛNX+ZwEo'{򸏦Շu^#^:,M!ۥu*^,{8SW u>c1ʠ~lePO(O7g mrrU.o&"|7mH!ۖcFJ[$`ʹ/,DAORԠ7-mSBߢIvg`7?~J=43=XeU1?wI}G=%::H[KF#A (t_5DGq®|{^3\cyHi_7EFgpROFrk͸d:'zUH@URuĜNc_ݖ3{ʼn>޼MO)L͢V\K,JI<\kGEuo=)tɩxtw!V :SSsҵHRk1]gD!tWX=ՏQpU6j2w7dZc;<gը?lpE}uzL0`#3 q͇*jKէ7{Tm/sLھ4>CSv ĬFVbnm |7_ʖO503Ù{CoGS,EN+[2KOM`~j7.ߍVzc]+ޛ-WSޔPD\M9q#?@6U09$-F9B%"`~O$FS {r $RCׅEEW׶_PJ9u(OF8bD }R*<UzdH %B8(7E:1ׄfsJ~w&(9\BTb5RȿϠVtx"iQgY:Q\=c[x=)  7fo^`%βCn]_,Ipei0Cf= 4ʿ!VmxTNՅP|v+x%}hK`.&"L3_ /8TmYI %]״c8Gn^?(E<@!4ɿtqWNȋHJ w/PmyƊBnt7F>lKҶ1bBjLomfSIeUM'Ռf.b\curz Qv%9&W+oWƏCNl y!"Fn(}{:sL۩h-3CoP#wާqX?5bc,"rYĸLxao >zqM[>Uod[F~v4Etb(7T>ocLqgvu1""];qP#PnB\;5!Wsz>GN,L9WXjy gKdLV/ݏqu||YwG49^MH͉)i֓UT)ݨֱ!#7}QzC0ܗ ˬT;N4Wg Yh|v[qz~jto8ٴ,J.e_p~8?H!ztԈJneZoMi_/h2nLI/tv;\C )x<"#LgՂh2/"6i]McshqjMǎ\_}.=LQHt`+zV UMg>sE߮M<~Pi+ݖ+sj||S$h.{D)-3771/OnmpEwwh$QPu;XS3:Pϣ+)@ܢ=eϩF+~ԗz~wDƸxwh︼9s=ԫFe r5"pzkNHe]OnE"f՟в${s;@Jo ("#k"c`-G$6LC)n牧po\D&d^k׭L (Y^ln_*R(0ϑF>Hy"+iE7Y12U$|ʟI`oO˻8)s6/8pV >s)R\V *0a0ނnaLh7HՊz7.&x'P\HF6<\L7;9(JU=Up:w5WK7U4ۻrB/4%y>?5FL}vT\?TB@C*8?bA_~bs{y[%OWa?/ t,&Ty0u"5`1 rÐ"C:v1v6jPS.Ebʋ]p] Zg{䚜r8҂O :tC?Fgj~AY֔.dS=Z@;N ">" C"x+(_9Q*= MgۯV zZ:{M g9,_ƥZWU~fFU -Iɗ,+3T@FRyKڬzHE7e;'pp`"%6"b0EE& O8}i=t;ַ:FBJ?-׻Wd'g)0glu}Of)Ib8{Pu[wK}G*HsFu wzc.-׾O;,"kܦwϜl* ɜrDaweʹi+'37q9ܸJDhcwh|;|fr4ǙDB-g]#vSUB CT `scnZЂ>xD[E_ѽ[[#|x]ӱmG$u*B+YxT_7q]Ur_U9~X#q,[p9|wݾ+G?Mֆ!y֮rsp9@otNO0T?7®0 jsUN1 z_z~Rw{]Ba,"Q'GvM+z#Ypibz940[I$C7"xY+sfA7Qݰ^q IϦ8O;rx"&8{o-Z}2cCDtSˍN9Cb((&5'o NҜw1a% S{q ;B z\:X⶿&;.wx>H>:kot_3B #2;b6DצBu}2EZ=0`/xBB{>PLVikdzU]>csېΠl|T,k-qVZp*Js%ո4V!@uoYJo}(yP?%:2!qM idBVٵ2 #/| zt/~꯼RaNs!Mԟ:8Aм%K eaK8|Sq)Jp~?qgqxêMSR Ϛ_[0ae.l+"U\e>`=utѴp9Ċ"1Q#40duz@T[cs_c='jHB24F%i~hKƟ1.x^LSLe^L5~}-ϠS3H=! FDpBֳT  4y<1^c®N*!tU:ق 0re^[4Pjw4-"tٟGzyd17` 4@} >Oũ$?-s$ HANC}:$j?W >u|'/5&L!a"WslMzcn&kEjSTf"=~N84#26͕!{N/#RM72޳v-RMm"xCa\|JCߟ4ݢ ˜w)4-"L<%+#I]*0.6g=e] 캈t܌U -}7=ަVWl1?/G4=HbY{ -h8~|:nΚV- /[`џz[&>D+Mӱfw,TY:]x{Wpڶي{}GRsd$Wduogdum ~Ӝ`iFjϙOpEe)b *^;<9ۮK 2/ M;RhX\}F^ ˳$m+uN( n_<_*A}0y":.(G22 e-q4,hEz-\!p)sU!tBq+1$Z[A2r6Zw]A߰"2_ZuM={ "Q&T]vavS񄯫$Uo/zNL1 e%*9vwo?bwhf1}Gqqc׈& 5YQxorW,.f`Z:9xuNm]8Q|aY%gh\_K;ͱ ]꼨?oisacu9 i]7˫ CۍaVv*aSD|sR=8Ì:Fade% J^m7ħ,x'D4>Ox)L:č(-i_g;i ogr T2P<_j9Trn2: C7q^g/nzTU [Ń[C_͈\HШX*  ]=į{1٨h)SzW}!1_Of%d:weLfH )`,:Ju_Be5^,/M:Yhu]7J]B3%k<^D Дx 5eC ~ .,^@0tUIe2paנ{Xl7}߳AWD̷[݉o EdNM |\8H|jtc<̳z}\SLq$t@nh+hpaiy,X7}=9Y+QH(:xstV+\Q r2q O֐_L(VNB.9MrgM;ɚ3]"'*!slr2*qz5\`P1#cBZgl֓_ 79 vw,hg`N:rzt<xv~O葾.yf5(:O睛^{rA N 0dl6ƣbA]x%h뜿;SN9 Ird:p>7>ydZ4C.s]"dL7^cw}ո*C#T8+,"n?ehU)Zɶv' 橊7sEN}U NRyB}SPf_+ \-"F_'^p}snI/ /Ɲ߂WG_5"pN Wᵷk&$& uVIz'燂w}F ^>a4vۊzyu 9Ƈrgx >^CY;xgPyt24;#ٱ$?f6k*ҳ 5B.%Ƕm'ZߓQ2ߟ}9K 8mIMynD#GETݛ9#MK!W9J5F?ݷ!S0qgA'-!'B#" ;9A i7er3uz 00hԘk3eR_x>LWɽ&~O@e$N (_dӠ_;t[ +c9]qzyᏨhSt4)@E; _mb"԰糛NὓE:Ƅm8$ fTeY2cZG\E2_hYvտXw9oɠٯ WgwĹq_XEݍHiЗhĠɹ?[dM&%""K:n:ءɓֱ`IvMهK ~i͹q2)oFf kˇ+Aׯ8E;Nrލ3E 5 i4ODN|̩1Z9.4k L*VqӦXȬ0 # r}2o}ρ|F}2jreHXU?㼾Gky'A|FOB~~_8Kwjh-Ua5FGJm8_UпW+~: =XofWeuO%<͹zz怋 Xԅ21z^P-uPq8r7=o0ǸPhX\Kb}<ͤ V_v{ֈKEqgxZq''(>rΥNU߈Xw[맦2!,9y1) i < Bz_St֑2O|}N S<q)ÑCѐ{9s',PK{ s?&u-B,]P-XP[mR,, mEB=.l4Zx]_#:?[Ru3psNi#7rg]޺sFҶz+eKyrA$H]{\ma+4{E~G]rđ7_ϔR.._{vT Xx.[yg(0`,Lߴdoo"<2rBn:1֊Loq$H1ppj&vg%sD ȺklowkM^yKX^)7W`㚤Asd?T#"\mYdq'qUc|w?aݶ}6l/7!T̎ysҋd X9Nu9eܿG#-CZuE!0RMC奥-ږNk2q EҢq]#f^PU9AUޒ֙&o_Ԍ'͛N]jn\Ӫ|6\ǿCoޞc^v՗[3PIlojVSƅ06`fjx] m{?RG|;Sº*|SGO\㜧ʺLoK$jѼ6aݯ\k-gd]`r+mDh Y> ƴD- % 7Fc~־ptÝCp6 ^OStqp2o>]0{U8|LE|Eȗ2%aa 2=21?"h+(euϒ(tjzM$Y1Exļw~ˊ=>^U7]f6UB[YDhfWPxϤP̿vjGJ%tUqm%j@\ϙ(^<ڻq1THd`>u9W h&s. H[?`){H@JN84\͓RU £˂}Ee] /.p)78Ic^13xeb2!̧<$sȪ,I:_hՆ+םss%|rXR}ɯ=Cu=1?>:̹ h(d X8 {쓽B'zjeVG= 1 @d"g͕yf;l1yGIĭtWɖ ^ter5<ӊWu/RC}S'3=bI1apB'ՋGV?/M_ vkz"Z2?(by >O TS\|9C7L{RC,eDٮG$P .Zn#Wm ֣=4OWǎ}kdQ!?O͹iMRNs`$tq,,Z|ޑ.l*c 1}I&1)~HїΤ"oP{+rq>>_y}K_7}jhq}IU<FS#'/ދDwc޾my,j_oK|]5c!>oGTwţ\FOz/pp" B?v_2`y_5VwgdOؚuyt?SՍ *qkzӪ#7R)g}[k[D]E܍s5.o۠Ǿgs3\P'.vY#Ty/pB:y)/#8Kv71mkvjVS?ʖ#:ޤ=+x{\sn % ?om[V6Mk G[.-CN29fy̮7m )$ _m@/~:<]g9 r=~"+ ^kPsUW\;J9:͂N:W͏,!&'TH֣!\p찉u4S!Y\x+3N×}DMB~YE  &L1g_Z0J}Z (CWs iQ; B@\JkN& zc1Ys->\t9[rѳN 2蜾y !0;o|+u[bngqp +IP\~EvAxmGP-}_O*|qM*Wy|5?+ۦtXCAlx2dU'>~KiëPc%\׉BTPj[knqcV2I4}se'htYZ:@pj(>UY].W8LEYnD #w&sZi"~S2@ 0Qӛ\8aLc6)fy'-|uEm8:V߱5MӓtB:,>B }TxV[8"[*}= ~ Zx)cd` FgNUզ~F|oHtN,ԓn\s ZhC+2&J1NPmMfX)桞 5:Mp!]biOg\Rb&u9؝vr"0gH̕q(}mDFF4"4r&C$qu~>s7}ˍ:69f-rJ)>Y3}{t`fxM?QaK(q9_Ƴ:FzyZN7!H7]tU>H)`b:JwO%`I\@9^z" {q~g;owl-"%lu 8437Pwݭ\uQ.(N9cЋlWZ*IrSDmm@?R,'W']8Z\䶻Z]>-|#ɚ#Y ʑTߔ1QVy*< /ELRʂF 'Ť$b~~/WŨdlܧ VnDMƱux1 9g Б)p9~u/ R#:'1=Ƨu^Écok1 # "/x65g UJe*Wpr03Lą*h"EQlJO2!p2ׂϮ4W8I LFdYrҎʝKjZcu)CwLxBxVh7?VJ 18UM$F'P'To)]{n\$W}\ٌF|bqyt2Z.]Ŷ5UzPcgWjεf pFֲGSֶq`X~B#4<bhb%6Ñ|yi6n5J0"~8sFKH,d͏sqi^L3YL*OV3jj_^MoP堛 "vWKhbd oOVS}kf.usޙRZj.y]|_-/L޷+Hw <}HƱp<} @9w$U&23ڂk+Jc'˛G]Ҍ$ǿghjog\(bz_(˿4 AMԧAZYA hHp󜡺0uo$1ՂnmJzkw yVu2. tro4áNԽBx r H.ϗHՂy>inxMhG`V`MauF$>"41L- %>oucup FƘꈤaϱ|9 D`vQû5ޛ ,FY[Xllaw#hB%"H}ӜmZ6̯ץjtݫ-'v;[l3ѲBt - FCq9*fjLCC9Cv-dYv}XNߍ]vH}ad-מӱj[إǻڦ'ޘ! 9g[q:.p.Ӷ{uZvm\=Y++t‹LZwSIaE֤ ~h+zǝ3dy\nU(6mhޞ^᣿G%g.qT}Yhw lH0W_ ko}W_P"PvljU {6` 3]@[-52of`w Bx?ty2r+JWUg ^ji~1F gt" cwWKA3*8;/Oy禸1ƿX,B9c?X\L/2C#-5qeBsK*ř-8-}EYpG+g.3K֟Dݍ-'葙5S^0r0^Ո0jcj&(W30?z4qYfݕhq]|-G[cm8vU=j:LǮ9b=3 D:`t>*Le|`齛o=0zZQC\sK3aWY=Aov"/xٗ!ʿ =1 sFFr6˧SBjN/.}~o A#rwd8ϥk%<F8GԟV_쳒lWeю hq( jҝi y~U|_6< >&?`im^BY1q],[/!\iJ>FGJ]F|9:M?8nL`w#{8֫qcZkioEiw%}ZoD&1Z]`p?xUc ?FJ]Ӆ.arutu z-T#o#E"Ҋ'n.U 5 "`@)(oF!"/Gfr ;q'rdaMt.8N9ᨡkDѻMݲޘ|(Ż(f^lZ^48+Pzfɛ`5Yv;wt#VܟtY8Ľ;HvXc sYN~N~ʗǔ6uy &n&х91N\,o Zx [qh}aԍ 7Ӏg=+Tf11H9\H)žqMy*LQ~nwϽysWDiBw[Ɋh=WA2n.l wUukm¦v": 'e앴O{;!EGuziMwo~A"4zD#=e 1r a")n(ܘRSPw^M2* !qgEKC0B٦^S.= k}b36Ayx,X?XmiY_HR`uHdb<^"/ ssB9d  r JuMi};2 |y&+p_znB%9݃׾h֢L\Ц_o^p>u!,ZzjuBk٢'U|gӐh%u\֫9nu;D>ɕns.X^e3xGO/pzbZrKp<\Ji湭7y[)Z bLEe/e0tV2I2Z`G\`||FZkǁQX5b)?f>L3_a`Ljq%/i1pתr<@.wbb+sf7s3whwX]ݬ'+弮B'.&s#ШCߠ/'M3"=f lx ^,pm d_5EU?G)maDG"y] JrY;f^tu>WmxLzcYV6ӋuycAiW49JMЙZL|/y/.#WEprYfTmz7!/Ǿ 썈ER>B7RJ!xoux]A?_xc_{=7Q1 uDW_aiE|ŽpSz]HgQ6o9,W}D1o:<6.V0  OL$g{E n'yMǯ4d9[4ֵyd#l FEmwFqx8vBdmTRQkn񷣢~׉=4zSq||0x(g@G)>(~uKzHIVO4v#wian݆%f>M)HcIpn9}-TʇLjTTQ,rKnO;euve5|M28a6{JKHU'c=U)}rV/u?hWNys\h85^L?˳⻻<c?IasV|hݧEdjD~J_GN/6(+=ظ  m2y4*ڶ_YtB?`pJ㝇΁SuWhaok?7Uw[@3zQ Y.M;<3%^ڋ.{>3~;&/@ByjLp8ɀeqHN μ H@[`-l[_ٮ/HcjWt7W^0GEouݞe^csLp?9Ww\Q aӈ]sv*G:/xK7kDS nU'3"cT,隸7i+]?n WM-Umw꼸?74TqW@Bn')ж[I~Xr80~z+\V?>1dۥ6,T`T[;l`d^l^DAɁ΃9B#x*lۅ2X}]FUۖ~7ұ[D{Jkإ5MC^rrmj{|<ַ5ǔ#OaQ{͝萳urjӥ`dݬ,F'UBwMobM@qF dG&,64 Lk;;mzsB:@x*ř6R㿑 toCD[ӸWV֩ȫ\wZ;GAʥʒfg 0cmPNI.d*Tڶ* 8c t[ٴntYgSe0s2vL'9>e%}1h(:H\mj;'UTƮBu1 xrR84ڃ(fƟW@9َ)fVړxئl;!w;Cs{fU ӻGЮG{ۚfW:zhd GQwˁ72L]c-6oW9-adx5*Ogɢg;Q;S|nnBd{P aH&5a{{EJMLWαLt}vJZXH qY+`/p;P}n?y=  = "j<[72=Éw$ҕ+)Jga>@q_f[+Us3yC}OkhY( _μ[?GMD v:b);#*Ư`ɤ'.T?Vrɵ8O_lYl%i3{29<160F% vg.}K"tMJ;p|x?$JA|y'D 9:S4//Z]7X."I-z)܏ Kfut4^zA#亄jWF)XZO~g_qk:'sUށÏ'rk\kVO>;N8=QH<8nrbz(8&Q"Ҟs`f?Zv-bjm׺Z XԳ'Mbh8usPv:y,G(P WcSa{nFJ(NV> "J$9źy|>iYB^5 E5~rYVݍJ5R "- f3k[YMC5g<QƓYM˗fVHsKc .ظۥ̋XG=/ .pt8]{u'zbӛ.58-;νP39zUnK!;Y>ϋDӮXn{s{۵"PtOw _D'8Z+15+h@FS6d* YVf8PJ``)Ӕ^8ߑ9UA$;?^<-nBvGQ⿱}ʸv,'}|b'vv:PخMڣFp8ҫ uUv;8hzj ( N=7%AO)kEW$Xi.rݗ rPMg~ F-U`;MR7w1PA)o{|Zr7P6+msk]&J aeKQ#zFO'hT 7;F v]j@ -/?ws# TjEcrN({-{44Lim\J`rKp8_WqVOQ}=7uK0p##nq^q_'sv.%|(sO LsD[#BGβ />ƎV㝂`􎬭:*2SNK $%i_1P4$֥gbRu|(8!abЛbŽu.x),>C;*Gt>V}O$4/砿=Qo᪓/$J̏׮:s>@uŞƊnQFAJ/} ˥w>e*ϊf}{C1ewl}$N^1P}a{_ ^8-8X)M9tl o_IBQaNʺuz{ s[D . y%3Y:iu^lx(eȖ3s{j@.'vłSI-DQL װvUz~!I/7'?zu[=҈#M<,_jTXE^#ف͎I>@YND=x5(r~>"NGPNK#7S$*AT4q3o1iGM}=Wʿ5ۖ]װC}MG(:UlQ)go gǧ"lfY Fpʇ|yA&|%c 8)䆚Bţ&WT=nsuErfjh8hǴ sѯgʡ"}Wx̂s2-ܳ^o~hU:q̊&VL"B I|Z\x!+8 6PxF'$'S.DiGIԋшd 2cfVρD(ϭ==wd͗Hώv/|Rl!볉mR4?;}j}MbGqgsKi8tx ӿ(y K-)1&G\6['`/FdU-]~9\z}\sXB8][s/l৴#]A(̺9"ycf?_ %{rz(Uٗor纤л+>3 ,h]284jzes br͔fadtGE0`aLηuZo4C\k9@7e EcBsnaj&j6HG;.vШh4xo{$܌7[pK(4[[@5<{ԬW抵a*Eå/s2AkLmpBvqI5p2Fϊ2fmdi(U"]_ۊ1na? {W8H8@&K󘦅Y.x/zޏkۖ_Q93a7^\$ rqKWp3m M%['n^z]DY_:?s_z,sL ڮ>0u㊫ry_@_c/Ӣi4Lɑ򬇷级-؂nIn4\]AZEM ND4֕LsDAb<tEPQ.;UOfI6D5<Ր?YV}i0Mxv2R"āY'ٮŞ6W\kѧׅ('/*Lk\wN3zqބpKn?ȫc!eں{]??(o\;Gc>>#=LC.!1#^Yv)(;p'07 #|&'27 3kw̯m#&Q9xC45] ]ըng? gjNnCDkCn`9Nca+ݻ{]mͻișx)ϻQyÈ+p}6suoz YN32xt.qy䇏pWVnp\#/6"#籫`o \ԗ-dOp@|ye>uWv^Uw_07W&7Ln g^u\WVjZZbן-ǵ/FD)"_x տuK/ߠ؋]ְ 6K+'Z};(㷇;U/;[}L?pX_|=]?v˻wc=fGB={7]5oZvaR)M|OOtt]qq T%D|3_n5U̘B.!﮵jW!%ov QU&p~&vY /Z,bdE"Knaf{7Vl_Zl[ /FFz%]Ϳ#T`"-gh{Wb~Ӣ1MEpzKI7pl#sE)jݚ'P莜w<}B>cUɀ9buw10#𩡮77 xv[Qtn(Guʂ)k&n狏+Ҁ_}8h,(,{[ޯa{wqo=hnQ&I4IsH׈NT0]PF8Ԕ3p|7'I`΁qq4cRV\h9i}\-04_If3]:`eB 6N5Xe 2$вg ``Sֵ,YI˵wJmSنWeт_Y]pJpwm>ʚ |ѨA'ޗ_ -yO4v\gc vh1dM#Lc\yhD}O0q} o\~=ZH$cmyWfHȩ9r #70"_Sizj;"X9H4:9 wiiǮTmOFYb7Y*JJyVA#0(;sSɧyaǩq#uRgoR47"}]rsD"vO#z.8_4.78ę fF7~u{1l;= c M">,'~=f3ucE-+q;yg=0ias@ H_Ww6ʯx(kbΪXSHC_s99j 5VM#wW},hi%Ϸ{Wwy{ m` q|un;.?E 1RRO-[+RsEײ**wmn߫m% zm h3v@t\G}j?"L Y2L00D=(K%W{vo۶uT3U~X~dT"z>f/},'Qd Ճl<|Sirevmrqm1{#76(@[8jqvq@(e_$C]Ka޿+r_8U䗌vڵBD.:a4i2`+Vgt rx]z:] 9ƾ첊bl0۠IGt?|A|)y-?0%TyPI6QiEO'__l1ϑZtrg ICr=u4tRs/r/c`"s,8t¾s':kt|S2F Ipk&1}<QJ"%Q-jwU;X`ro.ۄr\|U9ͼs"TE*|?ݗ.="r)KS!$8s=웸|ݞsXR&>(X>7\S&& 8>c‡:SL>.)y8p:h/֍YBҫS\d*L\q~@ag^JA ϨCe?'LXgW 0^3Fz4A'yyoqv2Yѕ "r}=xWD໼<.5wTb. XPSR?J@e\]#"z@q&"M`U_龻BTq~Cs$/ޏduP82ø1,51Em˫f*DY"?}M#j}dMJk@ᲷW_48p:eRʧjncHN 93 3'sTѷIV17\wIGDhL_S֐Xw?mע GiĈoy2 N|{, +Z{߿(..|<\ٟFy%KirKO>k}ÛF( "q~\ 9["ףћs99֜Y݋:ܞKI(kn7OGe(rfݝEz9d]0,6>6emخ~9vsJ̝.Js`3Έm5g7O)(/ѯ\$ʳ*>Ώ^i;"\d Y^{ OIZy& !Gg`I/LyD'ײ]%Dz/mA^W`kc(L̯h 13Eŏkeڹ@{*؟1k`MފooJp?c?_i InsN~ш@gs,m3M~,`Q߂(]D!J{jv"`pVI3>No/h꧊"/~SG'sE|+};hjN6?c947Br2|:H.l1+#\ .9T hUs HݲMKVNn" nλg&K:O]P~y#}埒@9x`ѵFA2R#Eqow۽*By,~f6[UZZwrKFrmzY+\AO~Ÿ6YtUX >ǀرȅі~#/pB6a3*eei vmzx(_kx5{VK䃨.}f*3^½1Uݧ  s?]R?}њnU TiP̴^6ޫǵECK@1"'d>/CQ~k^Ky׼e,ǭʹ̵Pk)xpz=7W&,6}+78ldv!5\6;kU:xCyGZ ?-Ck1boǥ+ 8zևSG *:|RF-ZWKn84{rNʅaX֗,QP璋"N8ؠM'DVyٟIDDΡ-]}2i[W#,m~ѥv`M`Ҷ۸foiFm6nҜGv}Us 8QRmӹrL",dr) nNl|?r\݇V#瞍;)'9daYV62\i?ƃڅGν[.HƘ([5;p/MT5*)Xѿ)'߶rYdf׊;iL䛈tc=%+L/4gPp[o گ"Obst=޳ЖR"=yf}%w+U/_AFO/#^wg2״9kfa>H{'lMH7[+;3~)U1 as-U6[\2V,oem!?x<{OEeE5IF"r'[-=3'oәERDnB#p氿[)ڭ;{Kc`h;n޺fؾ#cP A'M]ιe0M+¹5WykTnWfOo,]ի+=S%ŕ%nY+H՗顅& |,K}hÍeyf<|hH'_}w{}=>T{$7n))K~eDž~7C偏ƨiX<<ր[n/F_]ՇjyH Xh\kӈ (:{PVLa&׶xVb$Kӡ?B٭w#Fo,:#eׅ.]x% `*d3&=XxBgדpfXWseA{\ DMOyix v -J?^&seIVu$^FW&^˂<%&aqfV*.J; Pf +W-Qn͗oH(`tͦ}s7U&7Ka@F?w+e\]%:sd9/ެEͭ0sw ~3wq|w |n_+|*ojz&mq뼱[HfjERȯ含f?P.ؠࣺËΤO??/KOj30^b0Rh=C$ _ȀQXqj?iT>3`?K|=rFZ o7܉AK+I?o%yҏ(b/tK:Iw\uiQϹW8@z-YG@յ}U~K#꒿͜FJoyH>~jo Bc*n"QtX>iޖXFh&} EgL3D܍,;ha?e̒ˏm[$VI|jn-@E'iwc\6"ؖΟk;ا@}c6$~Vgx9&F.c 63o}`mq7 J0^~!OHdoYy6bEAW& I]3j w4 Ydjowrձ< '.G,Y[e*}퍾pV"|Apn rݵ\ ģHFA˵%-.@'l7x]3s9pR^vmBdE+QC@ih;qkUMf|U!X`kro0}1u%NZu/bDR"_%ϙ>5=S84y{!>6V(Sq)B+\zh1VfOIo>?~ 8r>*=@$lUm]?f|j˽()B< =̳|7d`( cIu,WE{sؓ[ -R>pxhm vsk^.niƉ&gwcBk=zJr/Ay㵽ˇ9'nj?lwoynO'C;µլŗyqV,&7axe*7(2Gk(%c`IvgŅܑeշvde z@WD4AE5 Wk8k+Qa B1BSh໦4B(>nJi!0l0s'.8$ìp-g:AN ~j! ?O(x[>S Jq 9ǮT'g>gN |XfGnZn}b̼&SY,nOH ?yL;0 TV:"WRtf-h,#/oSoƟE<3^#bW K#;eixls*c|H8lp=bҮn<ĔgNő ͸]D$XS81gY"lGي `zlYr0o#'%??,g=~u׈+b9LFMV#B{ÛfšE a' B!dZ",i-:%)+K=fs]vZW|ՕiDvh/2F 1v VvvߴZ`~kd,*'x\Э5:MÍK.Jh.oiN4{wzu^ãBgojyz^G5]6s Q2=w/,tT5 g4N~.e ,SOȐv0"Ձ<\ڄoᝢ=Gw#GܮQlW_lPU̵V LϞaLQvr~XO=Fy)=1q[gMqeqX*39sNs4n^z@HWq_ٗۉZ]7(EԊO~7ov\BtlD3Nv˵*@9Fsw[9WOJCfdL!ЬY*G0l<]Z#i+6RN ޷B/9"b+At3g C2E% io˯MFģi$R~cqQO94c lvq}nbl s-vhD'93U`No My9~7řB.gyN&KEwF:Α>ԑn;XNYO|F%0 XuFɂ<"fsʼ-\FD;:ЕJ"dzJ-4[gVf [d4Q)7 .9jǢD8s~w}2>W4u+9+]STggiq]G,:r݊ *~ j<Ŷ[SE$!3lwHT5{Xy5n8G@z.9#4on~~\-QD DM=ݕwUyԏYpunJG@ /2=XC $cئMk&Bض0oI<'Nm&ҋ劣kY`Nzmruڈҏq%j@ tKn%|@`XC1Jpn.ך E ~Gj}O^>4yCDb',)W1kM#$QWrOή66v=l~hP"\r431uxylǦ5.uiޡH.9nwimk=}~'O#+-A(އ"KJQ}O_ݔ6Mo}숐zʦ/,grYW69?'^Ϳ]#MfgfpYAVdT^ˌN*JaGkHK"cDSp儛l(Q+Q`}<t1Uyԁǔ>KA=h+;qM)~X%?:S֔ږT/a0β Y"Z\ #hY1Y4sn̏sK3\(Nwƨ>.ϦL2qQ JcMt.=^?6zWOuD_~%6 aa8?P|EbD]*㜫k$Kľn6_#zu?U*/ xR``.?D{kZ2?|& b' h4 vrqo]0UXŞTA //U&0鎔6Fp[<(J΢s 1gꝳCq }hOꔚ$0P1ɉr1=>K_Fȑw> rjR2v_?o:|> #%8 E7c\y啶dvV>8FDQFq4MI9)yS^eTpZ}՛[sq!o4s-V7[m97 |ܝgqN*dʹY=\.g #EKRJT1];QV_m3sޞ1/n-Bp۷1SVO?76΢E.m&λ3o!\\\:?^^N; /5'TʆZz?F*;= .Ѳ">Mc/#L5F 3膟 +Pª|0E?مan+WyiҞ.WJZDKͳ#I!9_"??j-\co>ߦW/|ӜI^V|$11άߑN`C.T<$WIM0MS>'P0Skn,K%{Cqq]y">߇dOA2jcPyuGNc~7KmV浜]&O7cc4|@J h8|0GUB9]~wێ9&eAQxU?]tc5M)hrŇCXDeW- 4DurII1<џkGkGԇOo;,Tؓn*4H(٥B0)(B`olC@yBTB5*ɯjя+i!pτAX_(e8Y_(bo[o]FC`7> T=vf4lJXB/\aDdR' ,pKaq*oSt5kw+աZA*l#Ml9x@pyK74+G]PͱR~6ط76[m/mtFJ꼲]0wL[4/|ɩHR[.6-u\|X%eaO\qw(2= O̒% ௰fyA 8pG*~v:WCd]ۓ^iڰ g(m6?_F>ō̰ǑhF9k2+U\Pu'xX+.9MLMJ< < kErr꾊L׈ \g严9s@fKEA(KCu]*9%ʫlU04 Kƈu7rs2jJ X~+߫2*ԋyY;W(:@fj-켂o[:u9CbHiΝz|0XzkղDoԥ_gz- Dk~h?ߎf˿FsJJ&P /EgFnZo c="k*rX=nݵC{4R鞟Iw>qчIYid/Ջi;N7USsT>MTƮ佒ko<-,VbY'w}Ј"+9l9#qfԑ`psԶw:>n`h0N'5 mTBO9s䮻Fn)‘+4~戺3rS!dȠؼnL)YQo/_9=bXsj6m荑fe* bI!G\DŽd7^]S z*xKbJ<2"TxwJ)Ep}yN执.1Niڦ@%I/1ä]d/K28,ufWuLuxmx+.0Kf\\+EHzlgRt{YaџW}~ jݜ{Ϸ0mT{##4қ*+rZfCO hMVpA?.ˬ@)ynv-)^Y7GQT-*<Ȼ~ c)>-іU/ť>ϋ}[|*/"Sb}Twkxȸnm``tQ/t/~ŀ n07*{b(6)p\e}Yww }L_i?K;_m[SAx6;)COz& hn:}Fv&EN]틶ͽ"gf#?t>>lt9IO8k`_j i;҈ϛ=|v鯹Tt"paj9{m ;)fSU=%d>6mqCj v=n=gS}< ߇ iPR&veMN_h$H7Fn:okoxry1SVvm'ZR">A&ygO? a!5ڇ7@; X|F&T*knK隔@A^[HP8>G-akeJ~95>cN#2n/B=w">7T iᘀ. b8y[TV6&i"ku-%QEu_Ē'-}t|;˧FS5㡄m){]A^09k@Q9.nNt7@}hSukn.ԧ}{s*iB`Saͱ.AпsDpi*ݗ_pg,G+Z?oGfN#Cw{:_SkL^>i/{\T."ewJP' J\99)`kw@} c6 X v/.(;])2&Ӄ!5yƔd.jz}|=ו&g^}>]#| sC҆=^W+}c\52GJk|Má6yy~Rfnx2j _` _/NkL&>U 'xA_k$JwRgw$ģ5|(ayh*jal.p2ts4^lKLi6B/̒WFһ陏m]X6`JM)|+R8FT9i䮻;?1Fj}v96%ɚvC QWi23*U@V]M r;~|2kA6VrKtJAcp ]siS6<ٍ|Aw!, V) َt'):\iغ{_0hΩDaȑwgw ?5NϺO5?:iB(aO5p&)B^᧱!j<>^M@8`b\8RaN}d8\yHFIsͯ+g DC^9kMq_|~H:oo3xXXap7q"x0y3ux^~ 62ڙIŮ8TYw cYri/ow^*/aÐOvՂICg ?sGcԱ T^? o32yIvVKf ٦Y_Ь;\%ѱ i~ZYp_}fq$Lh@MS?!?Oײ]{b˯|mt>2 گ]lޤ%);4b Tޡw2Nx^ G+=gP#ڇҪhwOmN n+*^Dȑ6re%IƤk1ʐv3N4n~Yr,U.ًSθ[AK̑X<B 2?/d!N9׫aۨ#˷̧aY ^*Z N7~.˗ȅ~ ӆ4aOԌguSK-=\,櫓kS]20 f:ȸI\^^G4)3H6pVnl.G9"?@eވ3ݩȟ}Gι&x{vL`^N߷H!X%PSzƇAqkc\;ap],|SS .jZ]L>r`S|1'5לE+ <27txQ#FJ7752N/#RH#-o僚>5](C6g{HqG#7[W AI9\rmWU)&݂r蛿sMy<\?{El~AF筋d@K.{%߳6/h5Xv nz8%؁Z"޾1?~f}SCO؇\ =ɾT7C+<)Ņ=%):#-n wq4TUf~c-+,ֻ?Y?[֭{`!z"n%Qm-[]l9P 񪩰-㶟lOS}#kGrwK-q%{éPr9v*3/#/B}F30[!􀪺d+Y~Eߟ&.U W!n.> JMeVB4.4N:@SCX_Pj`8=Mt9Ln C^%!ll2 ) HSZL`*G/h<+ũ|&zmVffg8(0dǃv$p4Gԋ+RUۈXd&\t+㣉MceλG,r,03.i8/d e-9L ">ow)6] \^ e.-񙍿*G ?!UZjU,<8UƕW,_b[:#9jޔrRF;o9EOu*+精~^o$74T:CMbtHynXkE4wAJ%| eTCYj&1- Z,M]Zݞ#BJ}doMCzwZhGuh:p{&[k6 F#)lŽ IDhQ@&JnJ`ݎ6"-+/{ГA OSs0|=>Kjp'V\Ƌǚa56o-GǑ3k)oVP{+ޚu^='Q!v]e LMܞFz xr{"VՅψM6˵ĞJWLe|+;|LV1GcR˲', ~wH:.q#1x:g98ޛHѫYN<)Neb@O~,js\L`sBo? FLU%/L?vvk_L 2qtM I)xֲ;C]vQvҥD~J E1 Y -aRC"O;m{gE 0uVeXm\Ԇ4|gɤ&uH}24x7'г յ3*X!Io GUݖaX1cI@ԧ~ iNZd(SQDa!rO5, ui>A{7ڿQ?8Ef) OU~ژRf@p72˒^»ӿϒ>7-xu~5fHÞ CW5NQo佌Z _ >IsH ;/߾?{ϼX/'6;k/ 34@/Uw^I6{XKh!>K 8c-Q_PњdAER[0D)O.0v2z6rAI%rO1UOSF75p/V-h$#iV0-Gj<pRXHNB-SN/C#. @1n8U5N}y5WCwM_3֕0DUR]<$W@(6γ.W'z)guEP ~7Mxo@`A˕ǥOۋ<..z}yn yrﳦLLnE>W! Obűұ^8]6v X} BдQlu73L&U.PS=2<=VޝV:̽ǻ3ڸf9@]OOK ddcYݩ>s kԷ {&qQo7D(1i)m:aYF+S;lVSGZD; Bv6c)'eiz̦q|B?c> 積]@R# u$[?(x I1d03l*൞.uXTcȆlZ[z@KLe @;'ؖu3Q0W9q YOSklV|uX 7Sr' XH{ׅ|50T)6QQ$N po1D'9`-E727L4gYe=浞٣nNy?"mxO}=v\R>cQ g!Pm4iPjmRcn78h*p5xb4whGzV?FŐ*=yNCW"x}GRk8ow|A^¿wO}Ԙ*5u^ 5*=?gV>%$ӑ&o?u F3-8pws,鬣T|Y >z3~;Mb|؂&3ؗxa (kDM  KTՊr艞Vzx9Պ8'PX8P:YPvҢqz'O\J(J4. ~9-d؆ kB4s|BVbM03lk2l84("g7aTbߊ;;⃊?@v,[;6ollW`CcCCJuq\mڇ6ix񿺱oyAn\B$=_q#zI>2|5'&lCXS -ӝs+L#7 yM[YulNeQmez\ԩ&9w:taxlKdzT84Nqy_2-pՇsƎTZ ]_BƉcEj.ޗ l˞zN6.2V1'3QpdA,bfݦM6pX ^~:zJMo>trgFzuP$1ƘPw=Vk?OJ -&ڮgPC%ɰhyeS1Qc݉lo`Z#1Jzu|.ݏpf^]ʠSwIRBT+zTNmړS>׶;r+FQ~m aځ9A~, 0#9~be T?sq w+to*ݶz!srx*p2"Kn6I"k2縛G=Tj-a+&6D#॔*t|=w4.=Oey ;ju8 t=ƿ//?17b@:!CAoR,5Γ\rl$3 ؓ 3Dm4=RrHN I:UxX BRklþ c >rx`W^7yrڣ֏)7'=N~AP띹cЛ)4õ/ʇZHy<4oPre<֤ ɝߜ4GyH`Ϟ1`{^ig*r^1=Ñ>7#Jj#ۈ\wE^T//gv4YE^Jsb fԝoNM9ʷua#B{cDƱfgTk @ckq3ץz ܜa`ssjF*:÷61+BXᮞԥ.kͱϡ8Kކ L찦eI-Gc~~'}ͱ z\+Y<9Wy 6>vTJqRi)J $h(Kx͹JPVDSHr;" EH#%X1lgaYX6{nBϟgB Gj@%{s1Gk$(bǎ漢P{yjY+s&]Y>ҤKx45xe.6|o T0Jĥ&*cXc'dqwl赮l}Gc|4$[+zo=aNXڨ"D~^,XEmƹc:2M/erdѺö!8O:uҨxRx4c(ŭ}{*N޶bh)G.ǵyRl }46H&Zΰiut7"C #8Ъ~.rT{h=8.g;)!jo:Hd/—_a3>P_gq :s1#p:foO˫_#MbFvCXyjO>{z::51*"[\WLue6'^/MSLyM<j?{i,+i>th;F4c%y^.^rofӕ @#TX6x9qsp#ʞ@uGD:x ?UWk+'LH:#h0ECMh9 TDi1Bu߯ӡ2jScyƃ_~̇Yd4 v{.Ӿ yĀC-))+v0VRu_?hIE̳i3UԪtb`GeW"_6q\Nԟ=/ A).|2>pH#,N{ um*F5! MNm׏~3YhK]Ѣ8ݙו=$w|c(&!fjՠ· 3@4/y0y/ ۯr Rc\ߵ]$>o\".}ħTcs,:fYIX\lQknH M{kЙ0Ql?A~j7,h[心{%)yمؕ9 {ui\z'EueSmY1 vBEw8ko+n?/Ҹ-3 bJڳ-@B-@N-s24$n2a8̵V.|}d/ɲrz~]`qUlCsw  kmܼX9!SD}DB~,pR0 z؈L#S>Ć\G_W>[;`Uίj @coa?/B;&XYy^PVa7KThjsX~ÊOFӟ }N?&GFjW32 w0crBoiE ;#W}Wy EXe͊򗽤s?~faK}~Z@J3fNP*5UUe9J'2 Y.Z_)i>N\ppMR29/*rbvs|6VM5YzFV,I[?T ɕ@2ߜtJxW՘|OĸtT޹]gǪbx%r7NJӫq:t3A%{=Qw+HǥyP1>t&[Nސ<@D~dz"Y5Z -}Jp]yj,9|2cW/mFЗ UͽWmNiKihK@mN$|o7pai_DXsҤ +18OK^*v.vQuDWA MgJڶ&/؉GW1*t#® 9ޠ;y񭣡[E'3NS~gxd6quu٧o0'Vk*d3yԴ8~Bd˓,o2I+4os:&?C5Wz !LZ,nR}? J<ϴ$v_A܎1RўjqbwE:J_^˓ߗo07jIP bKswl$YO} B&.dgzNϯUs9ݡ y?2 `+r*.1`};^"_B{ZkY T=v$;w݄;h4yS=24܀&n( =!(l0`!Sg" /ďKo>y|'@Qzזq3Omrk+i:mVtl_8롉#'Cmp|}W1vA'\Z/Ee~K(:l=AlLm7}sC>8AVm%n6۩Xvт6v{b"̹̑]vވ*G1i m,k ܕU [6'Xqt{2닅N-C_Xd~BGEPٶ.ˑ2] ;;V)N?xܤ nu`\c[ ?ʪĦZ%^Rvy.[3sO>PTP bN׳i6i;.kXmNq6 ֲC9\A-b*/nk)@3(A=B jtuoKK$؎ w9=R Y bGi9- O ֙&D֋5yJiY$1M]yb$zB> x͵b+pW00.6m@p>Uht39XiJ`®uf3PqYvQ6|kN,ʜsbkiKH-Xg{ˮ kC{LwK Xo8盎]x`SBE՗' D%MwÏ(5l^Z([J~3.OT1rlD Q\K]8*ڹ{0,%Z{޹lw4(2B0fߕB9 ck+{]=KyxeX}vϐ5*,{_$_QR{d^vJ80.g H w.>4y̌QO /-on<3f0΅SP %y ! y;?cʻ fD/ȫR֩V9\Gyf)sG1r{Gi X6^g8Xao75l{G ?w8 Xk<\$# / *MpQOM8E+4zu͊UP/ɓ]~Wφ0W 5>]8Z>=rMz S._jWGq\ywg#+^U]o-|j\9kl(ЋqjnĞ|2%kvLqi .=0[ܚN65a|yD3%CJ\8'ya ^$F$$;XhǩxA-6"SZoe+zP7,g`u|ߥ|CYSj[{Ń|5fַ%JZ}ΞL7}^ba\q)/)goX_%o~˟qZc .ۘrp]0PUvU~I4ȦΏw7A8{iiJQϰHX]jBgrJCbI7+zplUb%襦!dEbYd cQk~dph#0Mt _cO! UcXs ),^7D|$N2P'|^ʘfjSMKQc 2e6 No)tnlgjr[+dwUb2^<ٹoޮ1 ')$1g)Ib@ot_YW1 #Ӫe|/^L(7ugVh[t`i_շ%KQ]ΜÿU._bʷp)18es_Z1 ]ܗ,$f~QBQ"}ѝޡŃ"үБ׊4jVTqL6-Gx `,;3F#X9qNLB[ș!r.J JuB(Hx?[BBKAonS8J9qD,(I8x6-.|ws!t2;h4hnా&IjlQ00v~GFn8ίW=>GGaB?%C_w']} w0( GN^|w9ʹQ~cCa81hא/@`9Cj?V*F'*$3N~\e(M/g$C]:]iCݻ_/oxMJOICrL%ޫ>I_d9*oqI//4, W8%QwӉY{?eJ(g]TZyްM2dr**a-3ľ`Ѝ)qX1LGv5o4T9 ii"e'ES`y?IlR*Bl#|(Ch%_a{Ue݇JN??US5tT)$u<1SVʛ}++^zZ: }X<ʋ5`xEJaU)wh]rOσC>hm7RmwlҢdEwzV&78gXaY8(3 Z!ChTBimsƩ mdPBt9v'Nn72N\Rf%oԳMk z%'w]m+U=v3\4Drɡ5:P24N_jNDLMOTc~5>E)Qx$ZSz/Xy"R\w%N);,"E Etx>݁4PoD {:`i#'`ٯy>M/"C`5re=}Dz %F}PZh#xA5@Ccaee?[STqڅv2"le~J[V:&gyCV7^v*^-DPceZnEJ̣ 5L8yj5Cxh%Ee}w>z+!/Isڒ5kZX@GC=&qҨjs"3ggIS"|g8li#Tmv}t/o\ M*Ek^#Ո^G'7UҍNՓ-OVov푺tr^SDq SY tdǔruE㨺a* iv^լ<}ї@ g-'H=0϶A¿o/Sq> o?;;G!O'Mbǃ6tAzC`o.<3Q{YQu Y\W]g pβQ=H~/ЯSoV`@d/OI{oeCw5 !vD'g ܛsypBz %O:}]о.JNqO n+zO̻j4 x ev~y.-NXв2)^"w28`l0MtPA>W1QwB!f:A g JYnbKja^^7E+n'<[)ٗ#ab/Lo~9;|M̻^sz; g:ڼ0V~o=_4Mld|W-{=Fƿ[yYMu*^ۘ6SDr\cN4|ghK@@(n+C;g}%E_&7yuxc3۔>ݲޣM~#hgp'aA=Q6~%y Vq G/zr l4Cd;#D;*tFiQjh\ޝJѫC ӽR>钮|˻&3>2GG8F&{1wg}pȸhAUM߮\d\ i$UMO뵬Kճv\*eɒs;֠VL[^-%O,Mʦ%92*8ם_k eqJ#?"CTSv)$>?PxTD2;qHv1 O <2ʅP=sP[-8=bRÍ䝩/pHE-"$^D"^I/Ŋ;J;Y=\Sc$#(;՘ege# k+;[]WtAVJi=[U9)v.mGbJt HhKB/6#yklsN 'Hrr=q˴xEJRr q՗?o]uKyg0G)ƟY$}An9eF/p 캮&Y*T=ՓF~,[s]bpCAc(|z(J͹|+qgs$JϘ~_&|xSFفn`|3|AO>j?vmh4X 7K5&B"fa-oͥᥛ~_gp> p!VB,O(_F9G?R^ ŏץ1[.I1`J߮N??\ Rl· ǟ//ǻO6ү|s|X՞0|[/{\_.?M_-^_;" |H6<͔&n[ s@s9\F8:{lh :V O09D&8.5`Zy7~{[saU Z/#zr#Ѥqb!)ҵSK)Fu]sryU{q>h|4֟RtT>w}$~ @=39{u_ۦK'$ϓqc@Fp#K-~^e yfbSV ")C 1,xio=k,qo9 ד7zAVشE8W␈̓IROzxpQthmGG++@|ݍ{=:t^t6ŔǗ~[(ǖXQ󜤼 >!5Vy<ϥIzP~e`;mu㜸N.v#=.0Nipiḇ.v^%23 seRV Ρ=t+xAt\3Գrd!uq 6+0l[6fB18N/ӐyVx]u/| I"J;YkEO []'BxL`ILyou:YcahʺKz2>xW]*%$< S^;VWgjιf4KDƋե2lpG2tv_I,N}Lu<Vb|;7#$[cj64~ѼWJc-Ms&C-S1(^ R?g:1Qv$MLN/{!x7Csu#6K޳'w{#|= D` l=Ry PI>}c_MVBmtۍXpK;oZZ݀YE, uI1\h%7F])ʸR^?4t;ئ5Y3 R?`CG\ rύ7(ں0XuԱ>+va p;ė{+Ƭ/}ӛ6w^ZDE'j \{q"{V"~OS|1w"vAc 9JSzU )NNb-}fq3&4|;*kr/0 &. _Jl?EW-7tԻ¢Hj)/1z X 9*摦.c98Dmh9/WPcF(M`mz$SGկ tTy n@guaVLyܑ]֧DR1WiJ~\uTY]C-_hJǍblBqXR"ȕOT t\m2Ksa"B'6~%& =2d[yHϿK)z>B&Q#^> Oso3B8V*5@%q0+}E'F׌[J ~4}d:ZPA\ Ka@0iS2,D'AXn4},$(-ꄎ.!( -]wO3_<?<\nmhށ覷@TmZU` $|]GEuyɽ;xd4.v)%v'EKWpE+G~6ΗGԵ^Oo5ѝ ^#=Þ  KCި+ e6o.}m)~,Je!=-XC9|E;~޻"`9E[U\d[ES9|U%ʄ;7'=ߌW`dsg{+ޞA-h?( 4C`CDǻ϶h!qj:C)kZMa+4jΞdE*½c(_ؙ3ICߛ|$D蹓rh2T-u|џ{u4aRju%Udw<μtm|n-Lp }%V=j G>MџF|7x:ܞ@M J9SGv 1j s&:3&0V@wr}78g~Wᕒx#{Dt3h|TM]^-:ډՃMbm$ G.'5AR<ߤ p( ]2YެDgD%M W"EɃ=Bcפ~JSUouWjqAQN#-unX'ʼ7׭}i8S|7OӦ5 4뵽Wiu#Ӛ3.56EKO{sxz%ǘe.SC}]⍅+%`S%3cƞR'?ǚ54}=_RqӁS_3ްcigjތσ؝_Il=vɽCz^xY,î/bOw?L$8!S68^jԨSՑ\NBbg "y:y~f:֣cx~1!c *xܔXm|=a>"WNOY63^d:NAddbh@ax]~qp(kLnhin*Id!,粟%³uτ=5J_Ng;仗Sy(:@>wKit?~oZ{Y( 4--z[9ҀXHceuPza4ע^a\tߊT+UJMtsUMO"2;P#2}ˈ>–z]_= Fڥ!d[fYjbJ?@Nn"{5u kA 2B:lXէeeKbmo1X$FN|uwy U㈞^ϡ=IybhL4Rjx"qrԸ j.x>N<|"[VxDzIc3b֠(fQֆLSYfZ* O -F1U 4k؎JآvJeRm|GuyH1b-'<2 ?]ۘ<ƭeUAcԂv<9b.e v~تQ=fe`;N7IzF^2,!UEBK=F@Ѳ\]0<WwSb^HTcW^I=?^5YM=KB#>V3$k0,aʿ Ҏ.;8z e_&+XWսGgFRxKX;\7E~x7r:+dH󭥌ix_,gwZ?׿u-poی7q-7|-$()jwYB +RĬd@O MzX+M US$Icdb)uPwPL^0 n0ON58!|#&Xmw,^r4_fI;OAhs8Q$mX5:T]m7ԘK,zY#@"m2$YQ3.ŝLah2A Y/RT!k ?{׻,NdwDU$kR~RĜlm16r|>֪֪ Ռnrz[e@%iQB ֋(4T3X⦊UWPw !LUVK|V0]3kфw OJEwާqaY 3l(S`k+ާ‹Y?(o9)! ph <ǝnef]wXlOY5Qcξlh=R+Y{|y앳snBL-Ip4<=R\V/ԌdDt@(xr}B3 \V?MSpk&4`Π_\[U|~j_>\3~GOyU|ƻF y2 e>b'?ĻOC5k ~F |gEmwO難}!kCO~,-4:Gtr?gW);R$WAn@OwRR(MmXbHm VFY[ w>׏񅂋gS!BMg=v:Tu:[>j°8vSe6*<~ܔh8~!cyTXӀub-漌c) 4]駭LFx#{j\ PI|Nq[a+{yZpJ"EgC7ŝOr4&ɠm-i5XRb_ڎ@;D#҇z7jZ_3o5)kpp=[RjkNczQ:iY^+xwp[\P9z \8',7ż⡈Kr?,b1TZcn2TAʉ*1)«FS‰OgYSO-~:z[V1!.wğ PSel{/J"v1mxDX;Eai Fw>0DR1i~!c} w>`i0!px{w(8~ĉ`o!e_F@0Jw`> 7#PlEsEiV4궕nw`ti\ǶG`ٱO{')Cly4ׯ8Y?kǼ9TT=F`Hc*;M,@`d cK>Bg=7,w؀։Qb%NIya}ͼ?)㚽]$x.8V0i:9XߑSحI|%})U"rFplm=B;.<Ն'?%̵ЋE E4_J AIOgUp[ c3 ͻ7yáFHduh=5ƾ'^]c!? ]q4#}Je.<½ p4KXIN|Rj砿&`u\ 13%۝5HW_{`w;Qࡎ|n8{H%?̺a οRՇA4G%kXfl_v7hp&Qa5;w~|7f0T_zszOsk)r׿uu#=g\߀w[Uy6hf3p ~VXf-ct$g^% 7 _Z++6`H;V_v}6xk8huGMi:9: }RG*,!)_-ʸ+yk;Ԏ 4G9ߞ?Sc't7l2(j#jX WdB?jmQ#u^FГ-Ų5ǰI#Pr~n0 D9tuy;nGxސW.QjvRV F5z ؓKo-xRЮs=]ܹƌ!-2>Ψ[,2xd>q]vN 5ydiY%c(Pzӭg ﹊*>aĎ]Ϯv ݔk)A*@ACP輛xBDkہR\Ji"JQ *|_Tzl'7C7Ɨ)S7Lxť҇k6AbzV>5F*|Z? __6TOZ,\1Oo4hoZu0ʻo{x,?}{ܙzPD"J,';_^vzNWjl.-ʌ뱇Se r]e۸ 3`"$8ZcOc5O^Y<-،=7go5tlC ӣt[꟨$i3R V~|%Pc4K-y).;~ۙXYAORmiԋOIkNj1|Q_M="swީ'yH9S[ktL]`ޮ˧y`VB_TLtԆƣK0Q;og\NI]"CQ۲J;~d;--((Cvѫ;]}K>qI7uܞN2n&t;iNTq,u=kZwR@OEΞ$6 5Q 1ЍYkӪ`kR%TƘS`0Ny+r"'+h'Cgzv5{_UT^=wn nRTWa'6bPCY(k gL|2:h+p~ z0߸ YGΐ#1f.~*xAm@jM$sj۠oF+zX0 _'F-N^I9F;ڀMbIa7#!] lK?R{^vf*/?V%Gɧ04!6<$q*ij۝3̴wzIG>3q/9xOKr>P+b}y[6|ԛ%,BΤGKb4VԠ;WfOUEd%}q S|M~U֚Q|Yؿ zj(asA,~Q ;I/F~tR~y._d=Y*TLό_ y:^)loSjy kh}LpIͻp DZ`#ϡX1'Y#laGWD7FiTNxzyX'<-:"}1 Q>=];srj"QIՕ= q,|WpεQ-rS ;8[  iXm>Yl˞+̉O爏 1^׵k/L%%z1z: ·S5j3xb]QQv,F3-վrAn,$/{՞d-y#iQj{~ȞS=h`%0wo3-79{3Զ޿i<H%f7>*:b9煽}3o[ebcppKᴟ7]y-jye4X= *@M{|L%S;8(/R{51>ËM{U1~֍;}wǻHyGa ާ\' ^Un{Gj`vSc#8aM_ዕuѥáX`x;P;_tl{(C|m[&t>tNl{Yew rlos{n8Ǚܲ$uTYxv 5e&Lիj+ eLmE(_vbL;ꅷ*jV+IwB0BPy<ώȵɣwyCSW<W$.PͶc] N s7W~S,^ʫ,瓝^h&9=S,}h߿ kei_vqȀWNo6ڿxRE,ү$1 Z*WalTo5Ha3(ZBu>ݠ"yD>xlXVRw_rZ,3AxbYtH2} CLO*/ ,"@*)ugIXO|,P|*eWmɭF}} N EőaꡣecqGڢz5MJ":6HcyH# =5տ x/Q9 %D򹂦.tڊ!կ&ܗY<-۱y:Stv{{q;(6S}y=zx9 &< (gZ dXzٻԨq=2o_f29&ivl}Ǝu7‡CnyʹsQuyy`o:tBYCuI*,2~ nׯ;}uG=- |yi\3M{7M8=l~ݵn-c6m}ҹՐdɍ)7yf\I m >׏yl>VJ?`k9|*ԶV٥oC8.͌2hh"6_\F\'J=KJz೴ɂ3/ oqg*Dm9%+ix3L%&zޔCxVs+Gt*GYqNڌ}5{ K;=\4$WXf{z8WO]RTS X f1vZ m%AJYVg%V$eyWXn뒖<-m;zd,^[Voiw('Uv%敀Ҧ[zxz: w|.xjp gP8noȂHy۝o*Mk7c^5&2 iB,pk+4'"ك4;EPW~ڵ_tαer.U9j|(G78e݀.}9R: 0O]G`T}%$ (tvBh[-q F&tu5ȢjL=#)wY A:@)Uٮx|Zz8#mrm}x<*;&1ymt1(i2k}(x,OyX~߁^I +3/eK(4WWUr<=`x]rL$RRU5֙ӑr%o?/78L,{;?4XE]0oC C^xaC sMVu` \+jZ`z#eқ%V$Y8-삆zq|};2mrºŞ4 _4SeHiWߓzLS{~^f4ղP54fzNa@V*[0cuxcfF1Nh.nITcx2=6UdX**d D:=0Mwg>hb5ı&X;󲷕s j@i%t1ۀMaS>c b?;t q?mofQRtb$ 0f*Gqvq{r؇>cXJ}pj ?h+y5i@gj@,M)(]/oбVڮã˼!J5LO X#TX j쩼Hr-tR=LHte5N̸_WxDs5Z6٫v &v2QR./GH{#åY2 4 H WẶw7θzkAK/8P3T^эT.~xTбG Џzƺ]ޠZq^A^?Эc醖>rwt{J/\L Y݌wԛIԕ]>~}َ~yU@ltz3 try=ΣǏf;xǜy'F_oO6[ÔVT=J+h  IG,t1J\f]onaK+t:C[I%](* cw ƛ[#l+=َ Yuv`~5m3e%YqW%f[j<,{>iPyJp#oǕz>O78_OHJ,yHl2_K:CzdŐ2y%PGTfX=*"^/-\0&}TZ^uo?|,OqÔ=.m(x,AmkZwTV_gԳǦ‘?>jCPT.&YC bïP_tNjXK$dv Ij:ƒ8.g/gO:֖Z|]{Av#-N:Ng+Ea#Kkx@;IJc**u<K9Nϔ|1|Ќ ?! 3mN2n(cHGVc|ٗ{`ON#㿌/lha\/`_8!(/<3ˍG|-|m9?zT8̳~m M MJK2ki;ht1`|C]0*S4AХNʣ#WWCIoNW-KsbHw.Me WK>XH1 B ǎt޶i@*W_TdI7S7KΕf'2/cx8; [joytfc`ѩu؅  U)jFmp"&]z*i{,qC,=ݧO /f?Kۗya;̼t2Bsgl8\(YtF'4%'Wiax'j6S~=W]v.u ʣZ0{ǎyх9ߞQqo/rԠ~=GNYKlݗr-5'Q@%j1ydw4~GB;q"AJ r5Hhbջ֕bM;;t3~k)͎i-=(^ Ogܕ1WSȟxM7yhd@򺘾ȟ?4?p+> 3Uv]äÝF\ BI6U)HSɱ͛<Cm1Ʈ'n* NvI?whIa'x%HC#_ >I"n8I/`,P+#|=7`.ګ= ';Oވlǝ"c]07.Mt A= UK\\i 꾜Xȉ +#(dID9X]=}B{6;Z?Mqb .p6G+DZȼ(쬪 1$`F{+e5Ɠ\./ÒT蒮q7=Ez8k[8bفacJR~@2vId;n>=C: |RbfztIx,r\<KF >qlr$&11.mn0^ ‘ˠ˲rt#@˥{Nu^t7bLO66ab.᪻L˯Щ2F-%W6vwp'LPp[삛^GiD*%:7cMximYUu֤h 'QF>MײE,Xl|w"oU# p]] ~\Ly.(^r34:m,kc'HV6oc$suTcnN|[:ׄ`utqA 06^x JdVeXi.G#Sr瘉2)fPR;}+s",.A]N {bD/\BEy_ I¬ߘ4g31a`'2՗D~<^,:2" P<٦U/Uw]/ E %lc_`ύU\1G$U%]5t]>EXS$ yi::P4EJ-^Gj_xWu\%z[Nq6=uP:6bTTvp Ae~B)\ݘF* 6-godL-sXfk:^̊OѲ-cs-3 NXjvh0)Cu纈Y$fظ,|]Q;o4xM;}`:3yJJ YF7vകUDC%.4y= f;:'@#z"g?KqP^}Ƨ{-WqX~tX;Sh .k;wNT< ~1 h2l_%8g'J){gPO8:O"? _vX$=|EGo/?vȍ?U_I } Y ~;ᕒ̋nvyj5w c2ۦU9vE2[wW 84 AtUTp튲z;'A@P'1J< |f!Lv1ve׃h^TkINJ>G4G" 3kBCMߵwc?.\tl 53JvuS/7ȥE(%QD76wa' ;69*ss#i4`aӘϵk#)#']Ex>z =j724Y#Z&Pŕ|ξ}p<ΒgZԫ |mn)Ggl 4!N&HgO؁ f\ysބlxLpi?JU@xDwCS]N {G  'dexjgqz69Nh}aS/4?pDڏuozX[0KI?%{ o}7PU!>~NIӓ%4lp.g=KIx_G 1}P{1g?4CïߵVD ,4c+"ҍȱNLK1'S 뢄a.iV~βq0vqjNOO9գꖄEoҚ´nNFgPRMߡ Cy To֘gnB/4W"|ktS|xEc8lARW[*^8;S|t:ێYpsrGDaOS#D| 6<ٯ΋XP؎3BLan dkxUyd: <Mw,QvevB(jG_Ğ2, ^&oCs$?Ws qكU[ڝw0g{+|8ٴӲu0 w߄. _T t ~ Pex$'z$ xB]PQ,&kēTa{͵R|1``SI˰}j")h)ȂOjHM.Z^?=\My|_/=JqDk4.pc UtgWdH i6^RNmBNɹ13ppJ(Q|M8@ic!#TKx}>C0*3IF~┹E`f*VT܁Hz܄6&]7l6 A o/'EnڣgHSL ;@n4Ex`z O4d!H9|W߀gM|G2=a7J 6ټ_WU~g~Q8{JHlvaH Ch62U݈+`xa4t)= k4nnDf!rUt* t[!+f:M=K^(+f:87Y=]A^_Utɛǩf<TG"8h!:"k0k0)y=ǶC[q7Bɽ ] 82\ $BJ0_a$s>CIF;j@r /P J;J+VR=mi'06AHPڽc wui:y $: t@ŞmeOp(G(]D}hKMzu gjp @|mn_ RqLTK6è۵k1-{LcfԟO= Ui^!k^'V&:o[;?q^~ueLm<(tŦ=ȫ˲3R%2.jaX˩,d Nanq^VznYyNwɞOGLQ'.yPDp[H?tr6{mpj:6yEN[;Nvz$ $V*}1m9M9Pb.<Sob m "Xk$TKbE*tc=(tKC&uziF J,EŽ[b=́}a6wK0X[qB bvW ʤnxxi%yo^%Aը*T bֽx7_ vyeWv $8wUZ Dq['N[ VOld (Q|6OKj*\ؒang{z%i/7އ)+ 9p@1qp Q֝L+9gO_7U8f-nQ̥Nbi@SEjgzf*Bnf9T/-XV*jZ)KRJ`W֕>є(-?tvH7],xc;V)PoOVHh8@IQwZlu=WÞ( ژP H$3/eU^_^ZPh1#xҏ>JxhRS1Hex?l5 :~XĤ6d*-d 'PLz#e8~~j[cƃӏDZ[2s&Dpvx4T"C8 ᣝ7:vjz3Iu*qmhx{aPi?p|}Jn˅F;.7 %ޅ@}ν (v( 6HsnH1H^W{2h]Hh-ż2K.=|[0Ӡ/\OS%묛T,zN `WOI;)h 8})5XzϧVظJ:wUgS)woS_|^k 6yȜn,S@Y@B0JL8 MI~ZȄF+kꟓ*#|5ѻXbTS($떺SI{wMKd2($rbb75(af ZpzJ,1՛=+ z L&(6i-؂5;HLiA.ͩyImҌf u8T$ 8TU['\Jt'7[cLQb>Q^8UUtQl-vj*.}ck+Kƨ"O(u=JN_Ս^ )^dK̨*a~$Ů[\ 5}wbgJ?ߦ}d|lz@揀@,*<c>qڔ҆46WWjD5F*Lnvh7~i/hzg}jbx ԡZhISDcۑZxcQM*\S'𬱻S3h{Vv[Dϵ'wmF&Kegfy4 btwpR󰖱 Җ[hZz[pɡa>ɦP*ROBsNXiJVhW*_Vkm<56Vk_Y.!M$~%0cbV{rT(KS uUU3ZsF#Ln <d:oQtߏn e<ɞqVK+ȓZ rn"gc$₭QS2oO#:7<5wE|R \HCm{\J^6xGc犵FҢm &r!V('`)5[ ܔ](~f+5j1i*5cBTqBSF \N׽*P {^7)Dn8#[q[ '+3>q|Ic8:utK$^7 a{j1^&܅[s݋^8R&jD \]Ӡ-0wϼmߑBawow>cv۩DOo+[+7[S}xn ?ҜcσBErczxuS|^s7=WLQC8Ց۵eChg< g2T\,wrЏ|M=g6! \~}bo [AOLMFOXC(a.' e+QA7%9<独 $oi8mezIV,6p|xS0Nk[,oA4nj&-݊ؓ'&4C0oԥciN*q㪂ZkԈbҕ6LB<G!d_`JpݪE-gӳ ]8l1}+z"yY۸m*椅?wݓ{T;dsBw Zx NWUX4eV 2 7d=J\SN,P>G,^m~*AvU݅V<lxK=9[3˂st:*mJ~`Y*~, pb_N?suq[(ޝ)^28yON@ 2l RA Xx~tL+hj+n^ϮÝx,0}RUy7'EeۨxU q)FwUSSl?Ae|$5~6X~ +U0K2 }$r\ *k8F%ߒݲb1*vCuSwZ;~'w'(5ALبgtg쿜_-on|J謌 1hy:qE/1qɾsM? vRh$B' fl*88,dV|O03M (==E bZ;x 04I UvBimy#X}Z q0Rx*L˵kWب|ߪR+$04F½{}Va Vti|Oh9PMwe0F}j8 s V (\aǘ:һ!k-D l^3_WcMeoz]wS~3Œ~F'NQOyd9i3rͣ[ЏOJpyr(9S;SS#+p\fQ}M+ =_E茚krs}e@z.Br.:m*Q(uu2kS}2>&JFv?c*DUvg-w+R]d8.RߘEHfAǞ3ɕ==Ľ"| .t_ |NyWJLl}= WN0/o,s7| \ uLqXb^|v;4̾CW,JL]6Oʘy'XPܓ?uUVN.n;_/i>5|[Ef輪x ZYrV`C>ʶcv~oSdEV#gZ}۩/mx}1 mݶ(Wxsܞg] iݚ&I->b@Be9[<5)io } m&7@E.8;9lvM&ٽt"ޕHmo;Nn7mmɮzV{C0nlorHU=ܖé2uW\ik=wi\v+}z:]}fXw.$;F9=U)u/88ܖX3#{>nuuQs!8){]pu[iAJ&Y=7jJrm6˴^( 7J+]oOM;js.(*Op{ 72fc4V<6?>U_Lv$?v`oGd^@</Ǿ1;y?mVhz:(:1zZOluo]ոMt2UM4Z`7ک0p XY2I}2a_ >6=m.rb"_ӰB'ՁZդ˭#oRSXcf-y=lVX1,3 Mi V)#[Vd7Y(1ԓxR|.Y yQiQK+nZC ǩ?U)k :ZXL[eRnseY[ϕ|7!uPtd 1?JE@[|lϥwR+}3QhN+ Pk*'^)ںG<5-WtYL^]bv(ף׳^͘D<&U9Ɯ/g vt/2΋:8zIO 5!i"W˷XhP3 @[[WuTOmBxVwse$E_op]5\W</AkU3' a3.<%NPeoA0ARA6G`)/uK@lֱmc?k315@p϶!/{!W6Y'vkԉ$ ] ӑ*Cu%SL(|lIa;9AM0n3wxMk w_"褠e7~y7.£@SHuto1{^8*g?XC><ߟ[--Fmܿ_Nt֛CD`;Ewb(߯J&i_rb/@PD1l/zԧc/`t_@f lYx8 6|U>=M;!᷂ֆ3\!rg戼<s2=^1N/x[ OHX~2(?@΂7ѹ@C1Ѡ؆_Nh4LGRZjv{ayI{2( -g^o L7AZ9 F! @C{)0cmdxdAZ:2+\cf# 6tT!4HE*:uwZ]JDtt:<Ч_&ZECkK[H7ݐh,ًegiyӔW9§n1?$m` Rp&[ c!R6Y< <ņ ?lxsVQ1&9e!̤lʌV>7zX de7 b ŮX,FEm7/ߓ% /a9%Zc&ث"w_YN^ ]N< %V(Fò/x}^+Ubxp>T%HF¤;;l}k @Q6<v:~i=rH fڮlmk'ZEt:EO&|h| (=Ḿ­A(%!mɽDw}[nbV/Z^ E8v3']zuRv ,xS>~]O,aNZОPŘaOuqӯNEqm@}xWsL&_bkrkKLPi澾MUljeC0S⣷W}|=6RYO|m0DK= Ncv7|Y 1i#aT<0|ɯvM_~Μc t6TD~s̀hESm|<2<:)BU}Kui Yo:YEL2FD^_=e5^Rk$0]bmu@N5 ^&SG0mm=%w]bEKP赣U;.1?m~8@ϓ)6I=uғ}/u{oM6Y`vכ6+wN  }༱=uY%݂7׆K97߾egϹe-W~xM֫gbm'Ӱy5/r?{n{rnMhKTZ ҾM,Wh.t:Q%jM23gv9;!et^e68127Β"68[>i[j'*r{"4ku6_ט8~>6ZWr"ƪ FbGkӡGmFJϑ#c6Z^O? ^,߇?A;e'|<:Pa:H*^^S(X,fD G.0ᩩ?Ʒ PS䯯RkO˿׽ſ/?jC^?vpM 񰽒3 >>unL Fmn \F_""@oRv>Rn#fWݽth?9r9=0;^tUޠ"˾n5ޔh0oEb ].:*4mZQ>kg U9K.EG/rsZqj? %t#5|GaRV]v,a0C+Z@E70Yz0#ڥ${O`Awn]/KeUI#= T5f̀//-"D.[Aζu䟕 ތU- {逽z L"qy9OOk++w5>YWNIbY;388`XO픂XPnύ!+^'ǛOF["=î1h[4]C?ՈnW1lӓZvYN3"=_V!6gv5| ¤p\rcXnuEIϜ=U꽪d*t*ޝgjBr=M&|;0/O)B[T豺]o;K.o]5 bz=ד 7RC4Vh,Vnu~X۠[hZHHQBTa[ƻ-8Mk,pYًFc,)< Uhۣ*ݖEsb> y|ʩ'j"n M^4ʧ$wXM1zKRFc'f>ŐzaJ'_VdLǖ$Fڵa{bUa58 #"#n+Iw^5%ƛ%YT2l8G55F#Ԋl[ 3~?ρe( TÓғ՘[Eak5:LL8Gp,D,>"+JWçڻyQj#_ L9|vh1FҜ#S=W~[o'_ov??}wd]/5_~1,'%KL2g?W3qIC zo;aq.Ɨu:)E[=)qB-վLoc^AoH"YAoڦfʱ4J ȱ,?иK+%[[*+ V yl冏Jk@;TwO7dLk+W]M"M.K1Z/fD͋_Lѱc[)ӭœy3NW4޶v/~Tf]-.7qQ9'2 O:H}=&lw%rN:Og3iH&?ҜmO[LL|Y!ó#p[t h:o=J/tq-ɹ)xnSb$?jCby|]IKz~icv5t#Bk;nȃ3_]5y["YMٖ'@D[NL!2Đ2`}7xfIyo2UX|q rP&GEp@Liڧj >o@W_[zvXW~1 ?s>SO~ՃErG\_߆σQ>G| r6_F~zN j _o>h?"K=0Oz7"Dٯ!K? YkX%Op B(Rݎ oȫ )BdZlodRf n`P |pߢ1I66q!S DF]/"'hymn¥CG%E2f M#m1 ݽƯۍe7NkuZ^lͣX8ir"LT};}񘮽2⵷QYm}o.71?wTl{JE5>)kƟ:8@ǹw| nk_ɫYJĮ+o2~ |/;=ĎP:܌t{Ef-8J"GB[ȧ▘ [2խſ&Gco`e5v-4oY, {cv/'-χ;}l/DhXLo[r'D,jX}g~~=W?UwUOG7sOE*zǿTs>33>ᐧSS)_ /m[KLNl~g4aB˨HUsm}B]EU% Q?ږ6=bM2aLI&6PU"&lolV?8io3qZ hx/[=]lޮyXh a\]3uA/~ؤ7-ϼ-W!6"o:ԨAc3ʰW[Teۉ1 baDvofGx5&'G HLCj9Vw܀ @G%NjfIun])%&'Ee'x: ߿#^Eζ$JmursE==YR"^/:X7/Ib6YKD*Ͻ$h~TFSxnPyE$_W6ǎb1MWDh~]M6zְmbxJү71o%&9x'YWXiN65-J7"?"IzJ:Z4)կ>#vHѢ=suXOHF7~PТ]/}"&ㅭN9v '49^bICI~+tVr?5zeh٨cwR&L2HKW#M Qk'5%.9h+kyAY&A#YxĤYW{[_K*'ۂ46P?m}앖É ,d;gsޛۍv+ޞewhwr[`(sG+xغ1R`R_V˸L*E Ju{8>i/ft;4iO^+_ud'3 |˾0ڒNɺ1܌9,-ɐcj܆-#Z̿JaȭC1 l-jg9aluc`mku#zaޞڗm:$RN׌a1{2>y|Z룐`]\aYbPg"@/r** r|ZT<vXUg DCrA"ds$yi^s@tYr휄sF=tDUINs D1pӢjz%Te9í9m&۸&Ƕf sMj @Zۙ,=iE<&Or*ҦQ5ʂI$F0 %"T= |]-Pv+bpgc ܫtWӜ ѐӽgxOom4s+ޯ-h-'R7̀PD͞6wgt3DoZΙ0.,2=$jzv[SO@ѣW T2.$ҨmN<4LCȱE8ۦ m20ՉB|VWIu^Esu4^;%,ɳ⯛6oP$51. ϧtILQv4 Q<\%z6o`e8Q#q_Dgz_W.B2$neV;̃G29}>h(`jԗ+iSВJΞz BBRz4 `,zگIoe9FXOYNO nN^O9zY\~Ө=7lOalʷϞXFF>ԑ8K7w<ɜ7AE*Dz@[縂k5=҅ H4}&E&1o6{aXI|y1M0c,LԘwP} W`[`^iWXE uAG5}@_iqr`R2Pt;(4 ,DzF6nNUq9r=l/4Դ/cs}?S }h:QP'CGTf_~xeȡ2#ϲ(5"MwtN4<AG#JY՗3xڣ6ǵK8V mQB̛rW8>,c(OH2oC>? ~>[fP>M.?h*?Jl'Mtvʞ ܣBh<~ǿu6}&2FCӥDC'-ovڋw(hZ Ci9g5J"P! "jG?;wh&/F+jB}ڍD0F/R%:6]')8ty]a >Z:NEݳi8ק+_0,C=ݣ{ޣ1H^$^L1^zE/*Hޓ7@,<=<`3Sm-%8&AWUo~upa<_GzCݡf ~O2td r /@]Rb;g%4^S=m;!r'Z]`{tB:T {3֣Q,xѡ*N!jPz:W< :ԸmM`֑7뉵y!)0Vj=vzmE:שY\ьz $>ĕ9$OjKPO!ˉ<,^u& n8u[O׼vN.$9-&r[ɾȧnyq>4]߄xJi (;-;ve'B^y2]tr9zzlCh <5d=|#sS"g n./>KljB#XOWV .8y W+<\#dEDyuuGSEC ,4׺"Xr;lm]bqY_PO3SUXbAs9y٬?2ԫ`ڇRv[=c3riSau% pg+y~>yM"_xxsJ_6.p竜ƍ痷Q3l2*uWC/ͅgQxN7yHN-(rRSDGmxatM` X@Y~}_H'G%!6mMA5xTNnBYSѼXi Itګ>`cw?L\pW/eHA$ԜlOmJk_fHYiY:M7gڂmǠq3bWHe L$)qβ ;Kagp9UNEjuRx5ZݛJۏS ^[d 3CSۢ yObۉ'ۼ ݚ )h8,5TQ{0s<1`jC֧o29Zz\)b 6f "/  d@ blEVܞ^n.=>|DSNHh3{jd-U"`lKPN#at.BS״{ʉ ΔX+l\۸5W6PN*S fdrDbոzn&ԓ-|j,\j?*mO C*D&}ֆ`ԩm[6j;F5IfvT:Tx] /lgq :=߾> 5c C޼R͝jFhC+Sԑ }k]҆B…~( QZ<jBmN -W-mP.Uu8Z@USN"Vq"tHn7Q6.D4ymp/K 6 ̫Q'L=vdFUN;σEF m!DA"5V6.\tyg| r! ؁`W<,k ޫă JGMtDk>ȶ="=N\]/DA ,}w}ϛEpTb ?W*i~JP螳f>]3[VO67};rl` JD]ho^,)f͖RX:8zNSY' c){/Bù򘢃eǃbߞ%?h $x1l[]'Br:ĨW|frJ_YĿ~c6E]u_cTc-qv6goE F8_ }23͔AOUÿhjޡ`W\JיG>uf'=`^L~Tk;~= SD(f\v9_ jQ~i*s_]L5|VliP amWt9XVk|[L-'xSkK'VN\Rg32T!/ya-E2 ü'-kEAp)nnCguN4mGX=׶8(^| pv|\./1ݔw4E\gE>-h`;=uW<|sj=̷2SYնҴWd'"i:XpT|,qGuDx-"ىu>@,{[msF[&Tvk͉7Ӳś_FG |,|F@zh)Yhǒ.|B31e<%ϯ|3O?\ƺhTdk˵+kfvKSygDu4՘TT/*o0\2j L|꧆bS$lSvՈ;z(^V_.vYqNXeRloqa/TiuEOX\-nd|s ޓFHdz#WOB|Y|1QyA_H!n?˅1G5|Y_g;JζRU<ķ.yN3r U':u31rz EIu9Y_R>hc=c?jC40STc>ui]h!K"}S,G'Sm MUS=1a.P(^>5JT=0kۙ vħX[\dfK9Z QNlPmX$٨ڣeBBԗ4ѳu%qtoӟ ea -b0bukZB[tU,ٔtv8WyR`/0d֌ Y'T̼,<|`{/᰹br\ HX 'CenrP?o=-'yT=yɽ/^e-8->-sE,[l I88fNܡ|d'|~]O-7|?sq8(ʴUdb7wSqZ/0*6б=Ӊ@Pϭe|L w8Sy=M'4Kgj)%~ѽm ˟}G>`-ؘ)Rq?@]Df)XXxJ8P!4FOn/^}mA38+^LnoHO&^k3fT (AĂ^|h^uAϲ (zV~w1MJ:fw/Q -`0edA [\9wpnPwkſ8bp5bn. ]ab;Bxq~HjIeY6 4o룪te}_`G1Z߇V Ҵi^m&ݐy4Z耾cA (,zrH`h./_>~.|G?㌞V/_ Sc*vTkDr#Hgeo"W{vbۤGj#ݫʳD'q"oэ6~H$;P'CIY?fWOj7a4Au_}5l8T֎o-p SzbT4a F'}q@o څ!WE/P(ܣ>O9?6}ڠF{qF*v=s/ͷ/bl?U۽z7) }fVM`^?KD_THdk뭦 7 &e@%!cdcO B]au]"1Į3yXm̧NECrKiG%MXnuiÅGoNw-v &/j+nx%ԅh>YH6ʤ~qtT_l V0RNA.5<˕:׿\[Tqk)6Y v".SW3;zmuSB81Fƞ- .ˮ-޶lq`hi Kvn:XB 6'ꑳv4NWli8{&b%_rq)1yƖSf+Ov)>a^εd$J<̝} +$ݸ7c3i6nD]q{u|g{oXlSj;T[]k#oᩚyF lmyU[ni"Bioroଽ{6b&{H_RNAdJ},d:>ˠX=6~jF1{`N`Bue&'[\+-\δ%)}Pi)jDVy" +̧e8'YFCA˥vҵuVoOZQJ?(^%Zmnrk㼑sЉu閧/FnJ ֫>,2az3)OW!M_d^;GL|=_cxˍD<\9UjRkZq ?Q E[|ЎA0u0C~]CG O*Vk׊G;}nu̴& x k~f;3kFnx{SJ 5a3/Y>PM*| <'x|Dy+5ޯNjv13#2 m*!GCX]}g rA/}΂qcj<4D#}i:}țWi$:5_G7}\[WiFZx}I]d6ٯ m3^Nxs.aཕކO}_}?KU(]Bk/g9[lVOVÖͨ(:5rS:"/7imh3Xgځe_P~դL#uQ<_i7 6ފIeN 85G2"lz`+QMٝlzxE~Ε=MwW !A#_m?@!Q&p%Xٴ:F7`0Frmc#gc"J(2JL3hxd ϟ,6DXS%_sa*: {4m9~[,*j|p/WdI 5+jza.Ҝ˃`&,Dna` EBs+?潃Vzzbxx;k5y\&.=eUmyf\l?˺[&0i'9齀?Ú 9Sȑ֦ 7\=mdYl. ԃ\=*G5mL¸D=8FQ PI<#<]IX.s'\w 6Kv0 k[I:W^X[On,=MUB8ļ$B|?{ $Vy|N 8kx:9rדgٍ;]7PʅooӜ%' .Ge;!ۆ#SCrnz:\, 7`5Qw+7N:bo,Uڨ3 켝Dw܂immȟbiW_cKΡYCSmWXGmt'H|hR/3<MNF㌡ 2S5TydWy8B?b5jco3=rW}E]]o d Nxo  >яg݈ݼ~qaEx}3Լn Ó!Nh4#"?7F[| q=o_%btcz OrD 6Q{&d<%mPl^XYbc; ,*A(9WU"q fLĺ&UKl7Bag|(nIoYkaǼ(D ]xꉮ5¼e[ɪPu1_읒X{f"23āD'?WK:_a8 =PsA W>@&dW'Vt\eVJ꽆? UߟE.R䙏^6*vluD 5U~u-2Qr2JAl.5|ÒpP *tEИ8'UĞT.]/фx<1OFq?7xb(OǙ_lʟj˲Ef|I4]>Wl/\$EfVxdɌ`y/jYqVW)kꍡ&ܒf ׫vhb.OR&`l0%!8aztނR31~hPK< $˘DǮ܇v]?.("0%9gfܑZm[yU4sۘՠL"Խݶ`p7f'YzqI#:dseUҝG#ݩ[q1J ۳My<*׊xi?P~A88|L5&ak23#av'w#rVvg@_w /90thgS0WSݾuG{~F& O#T(U9՗G;Fϙ2@7t-ݳlFRV?$P0"c*-t)reO?^D>FU8h5Țv? qSVǾk Εd돮<)h8gtokI=9x>  Ŷx=ٰy|nAB)=7OSeo8Vˣx~#?o?X5C\ȑcF7IUK?F >r( 9?,tsoH ?' ,>h%l` 49% o#uV'sG5 `@{ UZY@ L~kYMטT<,-Q Y6Ӥp7v(ls{G䑒>t= &%" מF Q0?4z9C2N`wI[v |'B#"z 6 |ir8{>L]S̚W:ץ7uWx/$9X뫍|ɩ1m#d@ G` dm "Kq׷:1D|p>8!h|;s=sx |v( H:BeEx{3u6̶cw|9?yQ}ȇu _Ar"Aƃz׎??UERE*~oyX<޾7vwdgh]=~W4OU*E?=OPzp8Fo uH2_iT?|̗|/6WAX;Gv({Kuúyс+v}lX~ow?u6"DI*ǎ<<ӆ+[2BNqጩ#m!{ǻ ?_͉0Kvbo2k } 4ơd:rZUy8>s?!ؽ'ٱ8vb=8Q?-+eݿ6pvz#Gr5`]c>gA#ļ#8z x"`OekӠ~rϜY?򖘀~a<NApwи`%fYO0BjD/Ac OwFzGȿ7\XN2-Y'.3?aa*P,ŃS^^UOMcx"X-268I~UEmD`lP7̀G0% UL0rulYjJeDHsaSfP;s g_IpهnBOU9:[#CvXKoє^-`@I=ʎ -+c!]GR?x}?B$(_4E3l9X`nWߔwjeqMЋ^Y\ܬ yz|cy5ќK bT*xvpii^hݨYwN ē{D7~?G'?ٓ?4]aԯG}[@cZSh!Z(A^F@/s{9u,H16m_7ĕӎ|//-])oNO=k˩?՛5OIU׵2JqgH]h*ʡof8M?+и3~?} ?ASY5-RTOy~͊G ?~q{=?}χ_܉rc|pgM+6&!x[gУ"]?eJTB}u]=~4PmQu 0FѮk\@2}`8≸Λ7-47l;ߩZ̟z=+1JZk!t9$Q[Z=n4>unWQv9z`橖Q2r2=/4uʥqum}ToE4m|i_3{2zOWsh% +2S[*a/OkާoFp:!7Z/iX8? lAu,C$ҧJ(e5G@BcX r9ũZTSĿ4bX1|p e (.;K+Fn2M ]ZY#QYpɎ\½iIF#ӛݔE7z@hU*9=/.JwB(ҵqO;-Ȭ@SA8jU!«s[RUgc 0w(@LE/8 ?|.x:A[ltX'޸-ZHT&R\8 ȷH6ƕ:lFl ;r&w%dG$`u׷cPCTqu0»k&rb/[Ƚjc (M-%A $H^|!R,~xv OBrFXdá1 [tuxSo3"+XP>y] ot)L(g1C rf!#庪B[d[ҩ4jk?΍1;D-,1N!%B}>  o HN5/7d"6 dkgs?1mqgZ;mze7|F &Ÿǭ_:A\$٬ >ct ĒvMNF.ڹP9t֓(VoV~IJR$ט@sfCl"4fto "4$!3Hl't|*EP.=ƹ(]7od`/Ž[m ^h>i=  ",VUZfhm*%&H!OZX%@ ߍ~ZEԵ>J"tWnB/]\}24#cO*%N/ɮ4Z0KaW۽%зm#?/\8O#E;pz2U*͊ojxs9ㇻ2I8t WЮ+#Utkh.RY7D>cG í`GM u>S|iGjјJ~\4E; f>QzW oru&t ^4(PK㬻 o;09;DO;mƉR+K3gL:<| A?TZݎY&0)3wtQZhcD81N{O瘬5 d’cǸ]P+wwfwO{!2YqOdZXhzt0ռ:`)-!x#%)QV|EQ NpwH1Si}'ОIߑHlN}y,F$B xQkFHI^W6[4보Kצj؉mf_TUȧ~T>i{S3=BSZSےg幪|g.fzE NuSɇWygt˧v!I<.;A@Xż5m=P= )+kT1Īɋ;1@c~\1CWpS{o~$_]< (I=@'>, wH# |4$Gfr tKmO lHJ=e1lȡNOrrL)DO[sz[;%Տ}[̛J87C0a}շ[{O3|߼@W"?vBoW⯇RJU8=mCq\6OWݲ,1a }% s(x@-/1!0聖GcJ-Y@iGѺMf_lU>uweǷ?Y_Ѐ f {g@K=x{"Vooc]?ڃ |)˼_tI'dUk%oEӿ|i?LŃ=LZ Fg tQpC;r Fۻ~L__?VhMg+u2jOE@s$vEG<9Z G>&1 Yqh֔4]y!E_츱{r8ꋎ+\{?P UҠIi}ՓiC2rcgf&Ӈ_?*Јee^?PspLyyL#4tU4_B`)׎УIe0ckrT)X4(k-R+)H/G(1(iؚD0n]:ha;-J<]|-nf=ٳ%n1~? 6}0L=T͓ )lTRFCfwbDȽLkw`h%S* ޥ^9}xV| ݓP=V$ (p)x݌Ϙsbl#e"1f=#y4‡Ih! FBO{13u2H0|I5fK0[g0Uf/q i$*2hr -!qErX :]Kl}/qkzϛC{8"wJ]x^DbOah%Ą1^R N8Q%aeOk[grl&<iC~T|*GĂ ̌X(5&cUx,ĝW_9μnd7ybs&h`E;HmAou:wWQc=4>FIhk,f#rֺp/xYuIWY]Br{~:5]/^fOپسI@t-tWturz񘌃N'Ğخ4u/'G3'˄N7Agz:q0c\:t=]T+0b (3te!KLn} YTsD.C ,WKֳ ? {M&6vC؝ k^,]13}%)1݀ѐ#={G#>8`OZChSz׀^LjħLN׺4 rk+hYG}; z=6[1S3Z#bۅQa/nAxNXD;^{jO0i30b;b K~qZ5.Oe^V]#{my:-(=҆ wd &G`y%-6&` 5 Hr: d{л/g~y( x\:]䴇EX`hՃ`JvڢݧÅjUS?NФ{L-bz[OdYd rJw;r~PG}SKɦ1}XcAgꎈr_ЎYV204xo~ŜTf[j"l :!RcRԌ9jBGd[ d%6?>՜՝ tr7BAmdfRFoC_\/HH/z;ޑ{R#;1'ϼO^^@ʣ:Ёvt 9F^8A.0g*J}zx!kUr !NZb :ȟٍq'>Vy+#}";vyD;gnX_bjz]I͉Ѕ!ՏmV= eDc5HӺ}-껌z3;_.$o$5B4 .%ݘ`0SP{w=K|t9sµuDnOG8N_-,_{cqD* F(NPtgJb3N|mU$-?FU2L%$F~hZ?t۸UVOa4/BK9VjBqRD = C,/sDIC5Z<9GyB_CѪQhh9%-kuGK;MHs&Wsuu6yI7R<8|)l/]=uۆpgM)!ǎh1Aݜ>CrwƦk'LW2~S(zL jy3ϲz ǸZL38-2x%mp⅊- 7y!4.*(rz= }\aCnVk{"wT ^ģr g4p'Y3IRh7M=s/9˴uJ"2L'ja2 Ĝ12LfW'6x?' Vg|Z0 &ڷI&}g@ {`C;/Qg|0\"Cݠ IzLj @pL?v n'i\cOVWz\ؒpnc`R "A03)O1z{w3$x 6quWTSm*L-y!OvDGnU;}ic.oQlP!æHOygC*،n8}^NŔ}$r|>92Ϡ{A fLgb/#}Uзd8V=3o]Yjx/EI#g[zU‡ OO:UDɞ,1 syaODmnVA=ť[ Or{z`b#P b2NUO='G[qh$m.,na:M҄xy0.O1g)M_ܢi0"xFy\=͘>24Sɛ|f?ģs<Dg;]Z͝TWB[Ͷо=9|Pi--ґ'/P:&U~765,|^D?`JlTl'h>=}*Mb>]b3X#"} v7eS8WB\cv$v&6`vڙ_>T\]JF;YVҘ]ͩUu9Kpe2I+jo#Q{A,LFT߱Hm*n2ye$vYECA;N,+֊`8"1E)\Ʊoi~a{"tyN׏p8ϓȫK>u]EƉ6G'wF 5ȩի{hYNENi|=V?+,E*O dž 'X.riv5uE_b%3S0Ő߭!Σ{*!Xbxc'섄Iwv=u!9B(2/sa;'/z0ʴ.jx'S5"o*bzF/gbriR̋Y`(N]e1M_3nz `YYσ)Zr}ÿ{ܟ)|;ܗK܍;Jzsg5&]П@/Qe{F>]xfAOEE,yVKxmz8AH&'<&G6%yQeїrOYM ̊j _vjdk7I0Q=}tr _윶cMžrq;QɔC}Jf77 txY(S_E6!~,49w&N)[n!.u.z,NG(j/,b򬩋YE#_CY, D i|7xh[Sok53r?[)ӆNΑBixTg&.r>Z%>+އ19$D\bv OMyy_]T$o|I"w稆|؉6|ȀNF j~jY呣[{q;K4s#s\Z$2̖vCios8md, v8Us>o/{ b;׆bLq-RGӇ=I({6GN+Fȵ59^NmcL yw-;,uQ ;Ԕd iJ8^Av.ojKnf9Ɛ`, +9DL)% "JR^zC;Z1[08|<aߑW!?z+y&tM![(r?h=Pul^^9|8k@win~*p[c83(^B#zv UB&>C |V1G3࿽rWލ+͐R̢6bzzvteHY24h UZz4uC+B;zY'ODŽN.$BO{uu2,ߐ+|DG8x MDXlNؐQ#c"z~5'u/=Teu 64(tC c"pٯѐF sܛ=%u!bx=:_|u)4ĞPrabI~t<Ѐu]= 7eUuDG:C\v,`Xu{(]z,Iv|d8hYgV:3j-7RcO{!%&v LːTpiϓpAFSl~.uDw))S7;8 {KG]O {q"_\`Èy&HKWKԬ^l#2Ϡʈd#͵8RCOس9(HsER+_'#84}9 pbKJ COSQ?EV/Mgk<52/7Y\%fG!+]ĞdA@Xgub~_(rW1Hqsuo\:=o,D%Wʱ,Ӷ9.kd|lc&"EwAnWo\W =]s ZlL5/yvȗ:h17`Y[.zc;"FG%C8yz/ z9UON_Icȟb~e˓ļ\c2-bupp\E_dwx۵Lj⪫f ,j=7?ɼT FWS s[CW-@I &Їn` ]K]V7Tgxrj JRfaKFfp J7}~7h9Z[}+` ֞ Yt4v0ΈG~1QWŸn M*sr0(>gmO^ h&{NM p3Ҙ" a!n3ܛp4˙%NJ:H|ЄRyhܐi} N%D~~tv}@IY]>8cq9~_ ]cMwNi- m ?! tZfC0·#6jr0n"o,nѸ+=P'v?,3̐1G e{rhpڶ?8T({3,wu1up$?'?MUHbM^8XF')drn iTFN梗DTXIKÏ|bNq?q?g7ИnOG"zD(rbFχ|0˲8Odҥypd̊ >lS1T_H)a5K eȎxt">wN~|qVM.|[o7f(f0U/2!Qy(V13S{ƄXLWJR_9ո&ݱ~C 7ﭣA%誧_8\8J0uxK9+3c16΂$VE>閏-*pXѷFe>,p]|p~yk߾޴exZSL=Pfhc<  rmy!ZāQq5M6K<Mb)N1ȱ4ot?0 bqWU3 OJUN.nϹ<^rʥHwOm'v*\ϒ+WyU͠~< X]s 5Ob>66Q!(HCupL_SIc{^`F6*HHӶi? ٗ2Jyql>C)jgg4莉OxF&c[i*t$/.xcøx9Q s"Ozz43<- cЖ`0:& :g^rMh/8 il׋5&N}p7sѡxh`;Q.[\ [cn`Kn0Ə=\~w;;W~İ8T"'Ƽ 0˜!`؋nbpmbn_<\U3T(9+ZwbcIl@_d|yaj[V',59Q_pܟS)7]506ծC+L}(W͌)o#0؄ ѭqAb(n $o|,M*}>lXkTfu)OD %SoB!jdyhsiPe {Ն_6,Nގ63-ssCn)ip](wi"%$(:M+v/b{vSu~k |6`P.wHHRdN1Q]\;Z*ayg1KG3 OV{'l]gքuڔ?:2qx\4PT#H: GsȱW]Y t>yWRt,x^ 4txdsvi/r|? vUgq\BDS4i~7;ܑqUzهē0>+ٻ{ZC^eewvnD1{GM`(1qt޻w P[RHr^N?}29^irj$)X&8"ulvGvB˧ƏdZ fܸtx«zL&]Pl]ArudTvea>s=[ 㬧ǯNJcKatG;i%I6<1۪dqtLK q!HPD롸œ֭A/8֨Iq#J*G4bߓ(}ҼPR9m<L+(+w*KxQ^E&~}篗IohU/r C/S|]ʃ!WGA{F-b=j7źaUqrThЅъxV>|%$Sw=KL-QU[ GZ©ѩy"ĪsǭW@0D7 < a`!4ZzMuJFGX?Gn0JBXxIHc}H=NgF#="2zU1K9ǣ{ ѐdS>1^Bp;8:6Gi>x2zDDH gőE0&.kt1ڧcMDwD<Vziz[ŞR[3Ɗ{.N,| P:Jm|WrGMKOm_Ǥڍ-ކZ / / I 2@ T=`j0_]n.F];y0*=*S;m+1uַO5I/xsȍL dұX,Fk=8sE2(w|/fnѽӑtzNo6Z~p4ߧUaĖ58 m'Ќ%ǀM]Yz1iUxreG8I<#WDouFOYR y}|t>k>;b~'iQDAuc^6`JԱr4PDc0L*Qϙi(G|aZcKc8jQHSoz:hC9ר.j}wu(XOAk;SM}%haB4$pG3hCۜ^T~K4ZW8B~&  ͫ|ją8~ ŗֵ+̓4|l;#vvwъP=/UN}G=mzl|hY;Ckܞtg xH ڀc \nO#3,S}<y|]5h6|b4/B/ߢvj /ٳ0=!×w^MpC~<1$'tGZ7>)|Y_M(ɤUF*@>KmAj,BMHiM=n_9sLm5b?)-cN}~sYĞUό874݌O= ?w qGe)Ý]xr$Oz.lC>Rf<v0B7dV"O_F $l[9iygl%sTP=ԣpƇp<|c̯bx42 ]Җ|g2Q<^DMǏ&yL2YW6ׂjXš 9[p">+BGt7hk1YyucAFH $F "``p2Ci?mKGp8U5p~*nOjgf3N yW|Y~ڜc)6%n2Ѹn(ʼ 5d;hӉWc|v3jfLo4V䘢Tkj[>p;.mk؃6ǧkx얖a=Q|d^GOGBO%$+FFA >'&>E^oqoPN.چK43YayqF T+R{\|ګ1&^jW6G& ҤDzbkԃhe_Y%ySfoe@]!4jl.?[@[Bwgf?< <30nJ j5nq$wQ7ecq>s't2/'nCqS4 .^18=(a9ԇ/\zVv8b^+hEN i,)@.ﲆBٚo1@<*SHޣK,-:$};n ,Ea }WyOhTkLG!B7lBEc8֓!A&-i30-LǺߒB\m)Vf^'WY_oت)Љɓ1upFkrkhj<IE*#rTGbd/x 8y8B9 Ƨ>aE8m"4EëJu~pS=!slTW.vˬY'dͪAc9i6,0bjw7h'W1j%895&dr0? G[&xScMģt\Wʟ3%/ ܙPg]ufYEKҿ2)ղb- ED|b^Ǟu] YL 1y`v ,,E@X?.{35_Bl21J,tam?%#vao(#j4ޫhϙ0&ĊAV@n tr;cM0sh]5c^ /LvGHLl ZcC8hK"295y${|9tc2F0<~8! А~:JA` tĮoُf9a:Ԕ*][OϮFGq;?,}HטFLˇE::># `&9|?hT x8Rn@ұ\lTh+O}̀PzFe(G0}J=8a' "dVuuip>q܏nT8OyRW۰;^UdlW7`^Ѹ.|8{KH quajTS *m _T]a 4@h2}{ F A?(5o0;!Ԧ=,; 1O#2:-2ykDx 6؏& l` AlQA1TCqfkے$::{LCV4șАpHSAx^ySqmg8$vw?;#z3ȓeq]7 Әa0`, vō#>Ba+)7+NHoSc"{AH /VOijh{*в@߭c|q<>1iĞMœ=4x;|c>3{|xL V= ,v ^}XN3"TE\4VUjvkCDŹw\>Fc0![E mʩe~~nXf_3[\Pz4c!O1w$N4fS6uo=b%VtzՁ@!mO}h;l᡾k,~,Ғs 䮲7o^lp;.p% 6 z6җꑖ<ԼDg3(ON7ک7և/FY?dzͪ7/amc;5ӊ#鳾G+bD]3!:hhܶw*N |i1棪 ?IjEt A }ZR=>ݓr%4N]ߗv3#ybibghQ94kLr zR|A?,pD]lVO$e<R.W$["-޷846fMV[ˬ\s#6[kAzB]nzH?b<uJw/u46ٮs_`7j,R X_˅aR>S3d'UХ'|rXo: ^]o@LقeK$rH 6G8k{bV䴢͒oZ]RɦvE/~XߜػB-*^85,]9HݻDV]Sz23>.?I??l =9۸!(7OY:2$)9_I6O`v9:ݹH?5cZ(cmF@B#ᝄx aS(ĶvLA}hl\256 .D)ܽNUe9콒!9rK1Da(-Am/SZ?J[L2[FCSFco30cs]. tbUL+mFHmk]p@v?L :/;\zMeriG S*ij/1@/1m#vz y1^_VȫIri1*F(h:L;^GiDA>j~\Xw"xbIYfq۝v# {H mz:': o:&*1=]D0OzJ3ZNԲI+HjJCkڭIL΀.F-U@c4 Zӎ)niV[ KYtXCojﵔ>׭3+ML(X,5)-HzL:~~!P "ȍdv}EqӜL|x\"0n;1*t $N{/;PBcaNC!=03e0%3X㳜֨p L;Qآ~م1vکggcz05-mxtK}Jƫ,hԱ&w-t:5vvG|!p9|(WUky}RqZ!ýr{N;NN#eztfikC,Q v:Z@c+\hS:ބ;o1 ~dyp㺎6]|Z2w%$sl ie>V½mK$ : A$5MVG#7>>w2t"OZ/іpjzE*MԿP\;lc=W?ᶌ7zy52f{hawY(C$.,YDM+iH|CR*?xG)ÙrⳬaQ^o+sRp4t[-2+N!!+To(sM7MI>tM0@ wO~Ln95b׼%pw]%A&$V zÑV>?cIBz-hU}3<>zY/jYvT[@Rkߨf^l+]l%|'UIlvhnn!71Q}}ӓ Q4c?1]aV.P5 >WSVat]vG 4\/sS9,D0ԇ!2'ui%y$]V f,z]) eD}^ѹcָg`;jne\TՇِЫt!iP\jWlAFr;60%#fT.hvDNŽC]~)0t:)@hYĉ{d_xm9Y z T'5-(Ϩ;Pǵ%p_-Mgq`y Ηqg~Fa W8&3FS-*k&^̅5A)cA \NcKi"s{(1`ᢽt/=kA?5S}+Rt)pmZ܉ D ?{$q0AI%+m{BkqHj7c;hop Oz }Rt@"W eja<#GG߬lw+ޥn._E [\Iuc HDzmxV4~jr6xQ,Ikditvy~~?Oi 9BnWvVaB˒,>[;E\N1^g+;5T}k./"Ԧzo9dro[D陓tnQ7'FFsE nmodu5~Fj&=~)1/jN[wt(,}Z=a)Pt>n%$~. Yg qPwTmw <ٱXiCnh )+Â%&|I hkNl\$EA})d w-Oa 5<,wasyw*0.~pyj>-EBd{*>87Ed\CXpl9)_G]\FzSxgey+׌*bt[n?g08U^׵~+OO#RvQ?x%\o  : bV4~Aӿ3}DAc_g:Li/ sOv~IQv090;>e+Z-6k7;UٻEN90Q朣r=e8`xv"gu=Pl"~tF}}eۛ8s iD0MܵG!6XSU)` Ļя-3CƩ.=N6Nw[􇽧=/Q~шB7e USc?^D0]`Wo|)Xd={ My*Z^KZ-ʄ-esl4vꏥ"~sh < k7PB.ښ$|CS?P4rVniȷ(wMǒ2p Nv|yDT^hۋctSa"|Q9+ahn8_']S/vt3P|OOwoZT(AFӿKOےFʱl(p=ME/&ؖ*Z!X$=rxyf\ル zobRf 2wcW$sScVw$Ӆ]#P)Ϙd@y;#թ bc!yaȳsNJӌW,/ ( pxq L8m9'z/Ioy92#a_|;RγnGNrA+Y29SPI",0H:|5S3b(hH >&LM}A$Oy*{@erd9 d6{DUc~&31q^$GWT oIŲ۹׽@(Лu)^fxŞhW}&qf,UIそ5.w ~ eVXPVdRSubXo[PN 9ܐߩ:74Ȉt+%$<7{b^Nz ?׽[Q@ -ߨDʅR_SDj폹{ ?N,r^u!lؠcxFD~f[35Kä́JbnC+Jà@m}+;CE/_VNIWu7bn\&f;,|NЗX$J苟/L>Y)ƥ .zW }Gf]a^>Z6ؑ fRj Ou:W+Om"ĎԺlK5ol/O6el$-o<,Ն:>5{l60<W|Ǟ[순~ٍ/z6j*nچޱfhDO2,wGnQ M"Og{,Xf|lmK_EXO+RzNF=Zd\<@koHEʊ;L9d[##OɎ\{GS`Ո>v+ڻаwz%򅳩l߹W0 (尴yZ3-NW q}K^xД끛FzS"_Aj2o9Ezml8?52OX52:&mʁzMgy'sUᩖr=/UsQɮǀyR&o]YLoZ\g;+&A̵ʴvQBAn҃Z3߾w$}"iosdlj `G޴k=bS]ѐ2ha^7G͏d*u?tR/DZQeC. vsK/xѴr z'\3;axr-X=k;2Rq`Oiڻ8l|(Nz4 kߌLFlrq6< 3^sg\<'3Y@~PvH rJ(TOE aPF̆[$c<=άʈϝZeig9LgiJ,oz3ӛcb{FjH Smxt3}d#%wL]*t%xsD ]q`]kXNF˳B ti(3]?@oܯ ŇԘG+O?4 Jhjx/8Ŋ#Soc[+A.rͦ Щ?1s=aڣ9 y+3)u8}׺GQ.83xh2`[z*ŴUcCzUʇ{Xi']D.3 ch͘u\j]k˨b}f{Eįѧ󖟼7#E`,WR=N`N9m>WՓS=uJ,挣it"Y_D بS.VjvH^/M A3g G会l^U8L&02/kKs’]XeMp܅ }L0F2OdbxJ_}|k`׉B{)|]Bߡ,hUjs9cYe!yc&yqq~ 9gmЙbeAs<}VU1h'Dsկ_Kbd-^b t6{xtC'1IHr=ssIl8l:0E7DI$ L>/T3z-HBoЕ:zl?Tۙ7'*Ͻ7xS r3sh TIY;z?+22ȧFn! D P"0FA듻)4|y*c˹ ngSrZԝ?[Cz*aaismy:[YWVZR?%ŐS -R?n̅x|ef^s̹ӝ{9t "h>9xym )z 9*0o8hdp*2M-9}SmګVqb$Y 51υE2~^ ;bzs3rST~y"Bon8趷~S-zXn(H-iz) ^y[3;Ue0 @vET~5ɌPΡM=18 >w.欣iPHU=p- Ϣ!qfDs)y>kTr 5ron)'^E{{WS:L;,CN*>r9v=w]t!K"sk=uWv?nt"%&sԐћ榄7Xp53 n|^CҼLmWmv(Nڠ&r0g Ԏ}b_ַ)ݵjttTD#S{6qE)q߫eϙ{dw=gѿ,=3=9LO},751@䢘\5'Yњ1Ɵȱ$b=6s2ߥKAq#AkjTM< G1Q:k[8_rߏ: FY44b ;;89fqSIu`[w=msu^kwU4?f%>ZP'Ճb}igkPnz"S#<Υ\4rq3| òx7 \9}MPMc=#Jrk?4Z$ٽs/Q TDsڮum5ޚnXny0y6_k8gE>1͡*UJ`;Ӭ<(+D١ى#O/5qzx;n<> WzxX.JM1}{|}7 >k?9Tߛa&I=>"{9"PWg̅&PlJ|7<ʠ|0/,$)OyF*FÑ~-c>];Il3ՎKH %i S+vVm^ͽ#U0p-Z}D猱KK@/)~U$ c>Q^7͎xᧄП-O@[QTEӥ VFj?G^E =U ke.[}Ѯx%vaGQzK4#$ w/'֑8uD'c E}Tܚ3HNE{/ĀcOtD:"W_X.:OKiK_SU-AUlchteסe(ν lBaQ8K&0Q)eAƈ׿U>I|+Hf"`z woHwu6\8iIG` :``k"`-I/ 4#8I`C1$ǎ:fpԋLow; J7 2yeSb4odqE% 6Єo2fO k.lPH "|vB4hɨx6%B; Эp.ON*\9Ζ#`4ΗtDɠ@w,}dqvac5~[z{Z7Êd(_珺׏u3Nv N)rĎ;E#k 1=Á,&[n:;Nz9NDBsvڞOa9ږcWZo\e ّ#n*y\|mliLlN=x^5نW %8֎vK {.9O=dQg[P`YGs3-ͦ-993 %(ɑpzo;D b8ztPF&pj:wEFSÓO᱃XUseH3rFoB_pq_s!~7aG$'kS"F89`Ug-sd!d1>z4֍,ELJΞ619 ?q[OڼhPNdc] `rmyejvOy='Q57F= Jb4=Jge҆ 'eE>L\LZngoX:?R>۵_gд9yinI(OQ~C(¶r?{:7-Wbi7yHp]|N|˦K ?L>f\ȦU#{-NnMSZ]t@QU`/P tUҍ=GO(K}C&Cg@PGGrg6~n}ί#*)J4p @  ! O  ˛sBVȕ ~wE+]cDҦ~)' UfWљ^:.uj8"Fh\y5ٴ x7vH)Tҥ1Oe#Ygsm4*zA|}rutm6J M n` | 'w2d]((]Os4ڬ'bR#RD9k?3=N(8,Gȿz+HSv8ww҄8[廊*Hi.r&;:+%Ox4fD;GTT`ם$f-4EqD.a7nog㼌X>YLW.4z`MSKIXՅaY3do#X\bep1J}?j*eSbo8+4-:1vi6+L9pSf+Mׯ%O1dj%y&EaDzuE+?Cڌֺ@5H7A*kJ|V{zH0x9+O6)(|l7aV0㻞=nSy+_G]v@ >y^d"3[/]1GQ/GLW&׏uy#c܅P^g\PNz?Oޘ  H;O[Q"Eq%{NԿ3 ufұZ, Dw=*L >-a*=pB*rZeTl:wc>/o>,=|<Ĭ0tȨ/Z,\;%MQS'0ZKKAkWu2hr^5'{8,}5$C5;(Wr_Ѣ̄O uA U/ vL1d='Ay sEUӢ/z6`Nprw 3^#濠E8. vx)j+kF-FH?AeI5h u AL&~q3x!WT޺`hzu^(J?܄ghx9ܑTɳHJ&hϸ DaYyI%oRJ 1sz}8]/]AeKI 'KKq[mjyч]Z~ƶT]ՓT7+P?O6Mg-z.-g.5n3g]uEw ib=r$*Ex} Ώ_|Zn5K -Dć\Sq;-wKNsN[ݷHψ?uqt%u O#w6 yӟYK/'52x-*%8io> WEW-ߧ9#vLB"ߕ_> _گODZ|b>?? ~d\!8wzl#5*O &"XQސCm<{{cP?N9IzKSk>aC±~ KMvᅰ4o>hY$u1KEMC@K*-Ԑʐ(!;[#j2;O:>{^.uˈ;7r)-yNV/oԎLr=nz}S]Q]lm{ `\vʵn[;$]C~[/<|v꽘%ftg2ñH(7!z@cWYHAƤP]?:NPVPկ."tGE@K-p <8.<L /~|-I԰GYNC >[U|ֹꖣ1YJ eYUy lϑ5mDaI*+U91'}8 d0t\á^R?xTm~+pCxɰd"^gXqPu~ QY7:d&Gpu% E~ ~!#z:u#]R}y[m _qU@E\QRj;cDԤn঻e<>):cbz : Ww> ƎA`b*j;$ Z;_LmL"_E>oQuyrk~5oR.'a \QEhm-8lg`E4Ce!D*G5dc}/} .͍;:P}u%a- G5x}4.4a 0m|>% ]Kq^X's-I `~nJa e չـSYUՑ Jbze +b ߹h5Jj`sߠt#*a,ȇ6Fy/'{|c~]-Dd 7c"X[N?Θ[$O3WT\vF FZ:hJ^;EmvXNn3I#o/wx^wXl[zUsKRAgjm BEmԗo-ӹ:Ӏ#+϶ķ-Ъ~G.2ї<սh Ǯ!nj 6bs"/ܪ>iSǫ9Z.8]~Tlɞu. *<ӧ#^.'b8sғ+3.7>-4}nٿѭWx&cFWɾاzs]I:^!E'*uUdr5զϴ؍Gpi5=W ŦGM\V~-Rz{B1(=,ZiL>IiOǧn8&'I?un@k5RHq[y ХZ~kU8 ?2*\ "K&yG9taQ Zaw [$O SO=n~Rnt}5F?gA=_tρw8l7P-ZFYd|UBNq6 ȹ<<4g914Z [3;M#nRjQy~~^HSZj3"l-Ej$b;oݿZ#Wi4~""ZAy;hnlgK$Sq4:?$~Q:%ufν54cT_!)E,#^ܭq3¦qr`AAaމ[9EEPR2=sܩcm7Bh9 h˖DO%?V6E] f/tvׂ?k2*UYVG<PMv^/bzt؟iyn)y{1]Y{'`.>Fh2쩡"}i~޺q9|oJO#=[㜉 A3 NYpwFc kCz 1[)y,t1Ӱ1׸\3zJAd{p8]0i 'wA`$GM=X0t- a{HA[l qY vwaph]K$[]MUu=(Nù9RAawwqz 6Ia7)VrW_GktCr'K\(RTZ Dv[YahKK;O/ `'0fxP}}p|wv *!; sS r<G.8zRy嚷y2E!Tw"U SC4g)W`K"\NlgyE eNPL(SGnOE$ޘlץ=U[xG/odmFuPœt^dkwU'Q=zcqJH#@i:L'n 'yI&Ȝ<=71/R2];ճB ݤI\NݒG!ů>6d>ԯ|Mװ^j>?9)$ (s$Rj=zR`*ĺm`>I#v Q&Ԙδ9 ؅8jFo%I;.6cS:IwFtKF>_{1D͡_Q妧,?Tyi0#l`ɧ)*F즪v SK+wQ>eSbiAvDr%G"Z;ltʷ}XD鿲4jdio{و~6ls2 >t hn_  qԧǽIz#XJO`ה #}.}h!_N(<xY$'tO Q\%H#l"*M cjNZa LJ&YR5dRD3ML$nqԥdԡn,y]_Tz4q> ]{S\`YO>"6{t{?Hw ]1Xvj6 rb.uE/]?Sd:U+xv'D=5/vݜD#d[G9W(Qc>)@?Oq}UrTqD3o~hp"˒lAƔ}ʕs6o[[]Lt mۈh=$w-7&oon̝X09LwKAv?t \^F*n7m_d<L"E(P&7|Tcg"[$vTܬ#NS?/s 6ùQ7Ծ>].:Yn7nE .ΒK4?q7Db,rc h Zh:EYykziQ^;^`m !E6ϟ&~[E#2/YDvEǭ_*<mSWa}4Cdk>%vD5o~%Ouߒ;tx-fZO78cW8;W8uUVgl0iUNדt]hؓWjQH/LTޖ~ǣ uҬ r;BпKh 0V(5|ߚG'_$oS"vI;d2_/Ч4Jw, T2 N˕q ?jNopEB-qal$ ``. `휀29L7hdkw>j/4ev" WȦ+֎C+?7@VEM1kjw-E/[oc2% mJm6|]LL$~ E'Lfh{  L.9l> #2˙7u oMV $d͍z@VAd]?0 xH=߭]B.L(]Sߌh^꒧- 㹡J4C;6 E zn(mI9$KW.rR?ͩ{PG9r >SD"K?:##<쟰~^OͱHO"So"# ջ18F 試|T]9饏ga]-Ǥ%7a?}щH6lkPr;khc;|U["AK ׎H6U~l+%,v ?b*â{z:ico6M"'5"{ΘjVjO`㭼| hG B׎\}8_レTru<&EB.`/`Xru90!ϊcTƥwI D_9BLy$T;Z'&|&"0x;>JcD@Ys ~j[dv>Gu٧VIy̱ЖqUv[O:LȉPj!%El{/P=LI5ʼnUO!mYsm]Gjh+bh?|P7}0Xsz$+I|Wx3)]h+90w }]_k.U$6%6ߢMk܂a]Sc,`rE&8ۺ!(vxS݈Q;1ߜYBb^.2 oV$rF$,N%]#gtʗ\C E;mh|]#h[7Ә,?m.>]!Fb5Cj):"iw?oMA]drR5k8.1XFLISsK W^> MCL<`"H>-]{?%0ʗN;i8YBf^?\{hup]注3Yz8E*+`H`m,r r֢:vƒ,Dr78#@;^ >+Z![QZ81J|B~JczHAH\A'c7 $/]2= B?T0C:T~hnD]58,qV^˂迉Rx^{):)m'T\п~{g.DR!`_uFߠ>\mj"FdxS8a\L1WCD**$Ny X˦ RpL+>3qFz8=*/~gpW|E\i%MIo"H'8:/^k^j0G-2ϗ@'Wʕ.0o5TWi+/\in?C⽪`֥ύ|uwN= "$i%0 H^*~.)KsˈJLiiiV?kTL|XHZ/JAwCqK^Q\n]7+cc[,¿Oi =Gn޹4bdWE(rDq0xq^5טks5y+\T^8Ht*򩝫uG;vX~ #˕'|>wrT$]|5Gn <#reƧS qOs5}S,]Pt# 3z~]qk#xS 㿵leEq?X%" =e2YnMFuG&?<~!7.o۶`'Yo+ȿ-"4=5ķ?G-2ےXT..[*vg>6v?*G㎰ʟF(^ҽ-`Svz[jND[_tp/-r= x>kh~|ҭ`YzDg(9S\ܑ{nN @q `fœ֬ץTɚd}3n;6\iWjPY#O;~2'Μ'7̵5>S:>f.*,sʥL;Ey#7:& f ڄ[Kָ[.XO|4~fa2Nh{ͶhQXNi~d${iǒӔ\S΅󙇩7~"VWO]vJɀ>Y.`fKæopC E-t v%6ň)|i0LT=5򮿠t\So .oP^L~svg9Gl\L)I12Xh3Ե>x> favȰ7{5{4M}ٻN$δL94~q@D_Ɨy^fzI9SLK6+/3H{H'Ws-6{%7Èbi_BZJኆ6$؆pE a;oǶW0П)h9oc7*8pw|pmE2c*F?S D4Jp3ɿ-M4.ӨK$+ SCm[ vs5"T"x :#USѫ؝_뼊ޯv:tH So+oFSG e}n:skx#. 4a<\(Z6L+vO \0*G6÷hh8ܛ)EK=U(3 ZRϽ`C\Ra08IIHcs N0Gz(X̉&//ͧĺX?%RK@h߉&L+y/5]un|'/wy<vl]\swP3˨en iZI'Js)G<h%ֱlLu,ߏ\c×nydF;k}GNJ18tjoFP|(^KO˙Z; j:/pҏ'p~jA8H7*82[xmB\wUXģF̜I *=)+QL ֵx#Z"2#rb_+:>THN/2DZd^|n`k,Ss9":y9尅s"t| hQ4̣mEYsd3mڎ9Lo)i YPU~`-D E_Cݏi[e乎ܗmJ ;SUYR"ۢ9ɻgy"ʉfXrlC]#]ŋTF'vq^18sxK;MZ 5O"W9ͰڥĚV:0ua~Ffؿ_ӚuU.jLPj N9yոӛa\L{A=Dj$y)"k&P>e>,MAv.٧9aeSs+ $%Ӓӂ~ x9l0 g06+Ok&&Wzmc_t2h{*LsMH3IH$2I%efU"0W\4 Fz" g~u1?6O(>j\56A?6|8O]ENI# 7#Qv65<A/x 8Q j5x'C7ՈO6v dRYXhax}oP#;H嚨oJ`qPd.)  $+q|ϪDcv֊EQ […Α*X=mɜjϓAbD栊 H e_VC<)")|ҹ(q)|.4X`1V6Q-L�̀ -89} ok.6&"ɺ!c!0OHOfz=?E$.0Y%jHՆr:j;ɐ`b: G=?Z[TNf"?_ +E>+uNW̕u=?>̟߭__Q7~Yi$*\vjF֭b8M)CR\sb~lxk$ X~`(|23}) Q^^S(D1gO]#Ykׯ1IqZ<3+O=خshV=mxuCƘ7 3d".okY,'#y5zu'r?13uSO-LVrSPY❎lcaUjz:Gh?9BW],.)LGʼn,)4vߊ!^[f?x^C|jLz_FSn;*:dX'ghuH:cnY|I6 xhmi\Y.땱OvM/$,@y_>Ac V W\x)SG}4`fMQ@Js|i ]_<ٖ!27OFTdt3vx-aGN&.[.=,\uCj9?n9`:Z.\tJ']s?4wk@#@;V*t(2n2 y韨VrGw\&- SgsB]Uy%>zjuh?Nzy^Fg.%|{,lru^]/>+VJ4jUtto'H4 \?:oco=7t[!ڙӍ! @O':voBޑq^SZmXj̻nhD lK&3JP;ӕI"rz !po'ެ.3]~33ezL_9B+Q1bT)eEf>pGpJ:98α_I$(66 D AZ{T|$5Ρ`ԝd6%Y3金Ҏz= j8-[s e7fYv(IO%RrTQYN׀BFzkgJC;~=G념ϫh39}F_LסKT#-:$ܦX\c%mOG=\Uo?lAoWYp-!^Q7_ZVЧؕ8¶j^$IǴʶIԗu lZW@CyϑIpGlm9ӯxM&m'[N,P?[Agj'I{_4XjB?LQck/yiĩE)9EAG5:q. '{8Nֈ8;fsiU&?)*t>wKHrf5R9 Su-9SPg(n֭+"tr0,BUv e=Do-88`ƾ^>5Ex/wTNPTD'-Qhm⪑arT~tw.vg*PM@hŝ8AEEuANI #U|`Qs堨o3 9o廏49j.>KBLDww[sxGDc{9{8լKgL%Ȉc.xy$_)Ə{FH);B/"TR \}Ej㠦I5H܊%=R1:0;l.w怒L[h|W2O/ 㖃1gsU4~Fr}RQ29Zpt:ixNTg{|<M}nǏ|nn9~h_BxXΚ29~zq=22ۃDũ Oy}󲢽XZ5Sa5gu82zhƛy^U"gتӄē^`)d+U<̊kF ȝ~W ΝCObvg]'mU7h^KAi%@{BAsÁwyɤԉ}R.]P/ r\?AҦ n/Pm}\W?B/>(5.08=;V-4Il2"HQ|y0tJ"9z'rMF]w`[\/6&LZu.)@^8ݑaj).ʓk^3XYf?BNDLj7ޟ3g_D&h?&{ɩ5;(dwIlvqEJ9QFs?T) L!m۵ýx ~D K+G;#l\u0BG(%uRb[B4v8 =th69뛧wgoL\e)_^0o|ؒ ׏gi~߬W:tQV0T-ˋ7+)(VXs6Y,wxrSrmiWyA[$.˰2nt?n9jHf[o!7\uBCm{9evunܞ/wiZikkwH#nhaUa4(.LH: ?Ni+N7 2`b Au/b ?NͿEXmJ[<猪S*[[3/'uCikv/.e:$^hUQrj#[.gOg^k=wMEk|{ط"մ55{Nt\:k|i'ZNyhr ػG,x _-*8C I瓺gw+hwQkxtuC:60XCdLUjNCPqdQpjE!՛z ZbVDWUqZj/tPᦌFQ .]i[xNW3K~CM\u`:]{q#8YQm\Ѝ.mKhʜ1MSh Nc~E GBq:99J: Ne$`3%5M?rz OE"}P'qXn T|yX@(&a2ؗv8,'5:sPJ2b^@nQ  ߡ/W'#0Dp F埝7FtDaQ"E%ě@u:O{* ykr5&_w]L.Z WcǁA!@ z75]fhℾmNq.D25Sw븽Q]Gg@ʍ$LoLWhg|F:T0Wa";=DAa߉F/;¶k}U5D"m|W_/176[2AFk_(]ik^a{Gl~p#Rbfc@Bv#1D6X_ÂU&X4; e`S~LAhNJG,2;I+ vfKIy.$EᒣaJWL.Ϋ6M9ͤ7ݐ^EtFQ_4-Jw{tr58)w.Ǖ::ˆxZ.f/:Yx9q`6"Q5) ƪ7,#۽!ŤP"2hn/i'\CާW|a#!^ vòhnЁkIKYʴi$`v\"X_"կOYטdN!{.l10~cYd '_ e@7S--Χ-@# ~Z)[Yoj[crH{הGU0Z9r) f~  PNnsiS:)ͱ30'5>p{-Rsj~ Z|vZO:٪a s3kJˌ V?\Ԗ붽c$=tk/~@ 0>@j\N^i-#xfhEbHTBk/I1F Z4c&L~à=ɸ<uGt5s&9Wb>B种2=h9q.m?n+oWLhR :me>u.:>A ^mct!)BRI|!{q (͜<+ W**ɵ ~FI?K'7BQ򃠭@o|FIN{d6LX!)? ٷ-s9bTaGj*\Pд,:A!TGp?Ub_n.4^㶅 L2"E:'ՑQ*qFDɱ  :re`GH, h d8{Z_܁Iz{!ٺ8w\>_2Agw5GJJ~dKl3GvT76w?t^NZO7veNX\ t/|++ΐ%',<KPdE) Cl#Vmrs i{}"zdn'e&kW@_J?O]jH YxwⷞqO rc|mT#-ƻFd;wS8;?u)İ&\XOS ԑm jD4ԇتӃϑr-r9tF<-sse{*1Z=OWFfc]<%–D>Xi~#FH&A'%o(W㵭ݱd-ȑlpFз:#Gg2NyfHQ2y8{tcbi;pAV@(MMV։S=p&4;~~K)8Ge~<|g3s֎ZLsFG9kߌ>r}[.-S\(KwJ7t~5"pSF7HTj^cSOiLx-%DjXYS|iAWx#Uq,ظk$"DΣj#D;݂ |ݬQwK1:&5.\l3g`WXrR>wV.qiܫ%{wUȡ!-bz{1UR[^OI=89IY7I AV~NNQVa <)2m^1EE?>"Hv'v *v2qj;Ү陟`nkH2;ua<5,?)A*maTsGT2`#t?<]j,|)Cͬ|#F)Dq1s-tl#a~C=G5lߐi>f$({Hs\ޥm'G5\Cbyıq9nr? 3Hp{bߏ E79o.|P_kI'{~&w"Lpx(}+NM|? z* }, tA/fxƔoJ{߷p8X[8l.0`g*mVxrSs~.`N\Jos'8'ߠ}^5|w\ =4nIWus *v 0b`$M' yDĉ{]ȧj. ^I}@ыWZN(w4j.땖#.5/NK7ɉj[0ȹ ląx!'g\FeYE1k*Ufǀ5p:)^З# W?9&yLo |=2QOumTWq4 lXl̕)6xG~*HKkIɿ>iT쉟\O qFJj8 FOiQ/+^䁶P7o`Mn]:+u"ľl%7rY-y~|ꩀ>GZ<4rNv_"-iK)+k,r:i>ħF+dՈz 8ȑT/.N0]G0ilVc'|=Nu?ﳗFBFVY[vWÕQulÖCFjhu|^ޅAe}Bݨ<4ƵߗTSs:O$m{;{vQ98ҝbO?b|~v{?-nՅէMQE̿ĨjNTDَ8ߵ,pcIEyFSͯm_9CXIHv?<)ҦmxE0YγSV?szO D,z_YAiϳME#wSSaml/tI{DmX #+'~jsGlO3ܝY$٩Ћcf}rBMڼS@>odKgp-oZNɮ-GbD'bw<ϫ+Ju~Z[٩[@r8_?"ڦ?,{i9j%F{3Iߋj<l7~~sEjӵYyW|-5=[$ohPM|X[CO_X|`{Ӧ97E SoTs|W.z<|WgG)y|,{$n x3}n⺇󑂻?8]|yFuV ?&tθhT C}/ho,]4sb2FGZWo(h?M}_WhwϷP{/] =$o(۷̫ϱ 3G k.T> =ҎBNOu &+܃SkpAzb6wqt3, O^xR:UYfW^Th(.C7z[BzvurcsLSɹ\'Ni9|gd| oB|m9dh@ U]"Q71AFՃJ.V}"C&7,+)< \D S% q_q4Z `w+cS Rˢowx뚎ϕi;T*pjE*"q&_f(2,L~sZYuU0Swot u?b6CJ7}@E`{2`Z*m%XBP2>?mQǗ>v*>#OdDxj{zf45䊎{G=S:(I >i|[7@gZCq~)ݖfx{;hcn_JߏJIz 5עhE|:?7w!e_g O#?i{ aȘޔŝ7"ΤhDy JR2شϿS=FyFsmzF)[`Ց'*""Mx\202wZٍO,zYg۳"衑 ,@) QF-dz{7Ekd'מN 3c]4B@8:zz";mync]vyzs{ۅ~Y'6A4y~h÷Y+Utkq.|Oc_HT2k9@mwmOp8u կtlf򕢱H :m\0S67@E#`@MzhRd9WH(v)Io?»Ii$yQa}FX"CNqvK˝+~[d^CWG!+6 <%-k~7p: :Q\POSU׵i+f< >`w5|R(CCaB4a.8AOtjL޹C/O)[$cY8(qn~SV;U;EpXd<魋"[5Su=S$a[]{ov5hnfeu4l]ڸ,"% mLS'[C6)gA7}C&-{ǰf\LsWq_vjO\%sr*9O#7sh4Ks' hzMiXv귽hy&mmuT?t)Yiwv}p|{O4yP{`@OvGRHxj}hgom^ァ@>S[$>n{\k,[R:%#?]Dh\j"D3j[bhD \@ Fձ6؍֜'{Dդa~`Ä8~n5Eo7oRb fukڏ)Gzx<͂Lޡ-,VpZ$Ûvd Ʌ';S2kǒaKHϻWBg*Ln0S?%jވB:J&#$Jdֶ'dqMDpNYK}eW$ᄰIc'-"㠩dD>v<n>E+bO%( 7% o٩xU8)ŧx+OW O0RVN~} yx e-㔅8ޏv|"/aw^G)"R%J%1@D:Rl*2&MRT*,˂JTH8U$F<|QqYE2p)+}Kf _j>@cQ!\Jђ)cb%mxS2:SYVܹ~ʰؔI-qsjLƫuMř@ tz/0-$BQ+?DD9G9LD1}^dhFBGNĞP{+w &}d6B1ZiזsW"'KD˿`~i~5(SN-?Ňz:E4vڪ%0{=p|H×x?"!:ءoajHGХ\Qxtf9 n)=GmG-Rq+*e!+rnvr)nhuE<,_ 74dӓGBPazESn }-|x{+{DYB>'Od4Bx\>h+R<0}$n:Y;P*,{SCJΔL'^5Wfz>a#~pwy\Z}W}W c3iEֻ:8 ^>~[3 6**zF|ѺXiv/O d.>;咜 ZdYOVgyJ~\/}gU=3"Us}ǕR>h'QJ^\H{\vtBNUD ^4(?sg.$dczt3ϵ2`25z7反hi]Fҁ>/zZ_ڔ Y}[u@\z,i|/XJ"F_ ҀjD=?3Qؖy wMJamթ#ˈ(fjmoK9nm,۝jT? 48HndZmXť2Qy.H >8̤FBn9%qgŷ#8!*K`fCv 'Su XΑ{S-&::5J8??ZSL6CTdU5x5(41k/(X.ЏI՗5[.Y{\)Gu%9k[ʅnI9OwWfLΜ()glޖ2o}>9PK>t<6˼Xj>s b=>S'i& d'b<# Ne6*vpJǖ7JPA=u"à uSu3 I8>/D}ЍI?yijѱdvNsD 8ޛw51މ:I9O(]C#981ا),8B4-^Y\p*/Yێ?$U@3 4h QRH GՁPh@U?QzH֖ýf!{饐jh,t:-b؝w!Iւ'+l2J]:g|>(CUbNn~(jd;ܜ  -־'{UA9g;\UE! cMx̺C@Ä*:x=WL@%I4+udB+ǀ(YE׽E72&vC)Çω:ᝣֳ?ځ9\x?;B ]_O`C3Q"*dN`[8\q,C K@ѧ$_J0 0*f>%a6S~)Ř~ sv*E!}4%s)\88D/E^2bP7>PϰT'43(Hf!]3˽Hޑ{Iuݤ^~eߗ}oQh}evɸPJctaiv[l4/;-NQ8wU:h.:ѕ`;< d0GPߪ~ K?u^Tr~vE6<NDؚ?RnV=N©=gυ ݉-&F돆MVUsl=5'UDXTv$x]َ{&^{>OEA>㶝:ywBc.ٲ ~w3t~]'ǧy<X8:)>nB)$vwN(ȍu̔srƗ-Wi7=5ٌnάu%ܵl>5f6L--X|(N;槡{/  sdm ]ݚI?bpx;%z.@#w:T:6#rdpHY'\p4MPEO4 rx{ZEV=;.lU@QlKٖm&E \؏F$hnA%iKqz0ʓ#J#wZ\?ۭu#pBkY8^QD?e;"2ZNJbv9x-oFlӮ4^>w0[/-w 5.N  (h%GvMܥ̪~1{w f! ()zxNB /pyJn@:;756]|oH=٨tZuo&^},pZ53zj=}XtQm4XUKÓ4wW5I,r rW?3qvky(akh陇-Jg4Ob:qq埇vSd5eUOMvzhH{}<:=-GA6'9(ȟVk0,=A K l/:]r;#«/5} nzUys!ƕO dmd֘yu8?9.y6<|uWd1ZTw tvc iյٸЇ4C;_.%) |Kɟ_?ZLĖAklE/cmzb200vI}eJ箞K 7)?JG­T9I UU^Gv]Y̼i9GX\;ׁkBsŒ {Ԫ.{kxAcgFhd<»QH4Y7`Ts%;9>i >W6yf'8^oe&yzVy&UORi8G^P& Έњ7.&c\&o }CA.Pq1G2L+;`F-jW2YRq X;hz^7ߟ^r*@Ξ<J2v1v'0>Q۩_wmD)eIɠ=U:p9I/ρBIyB݀%?2 lT@;R@W^t"0- %KB}~Z ;0^kh "]/4 E}!cP|&~3W7kHBipJ"X`(Z0ѨNq45+џLȓi("΃%ՀcnX ٹ}snj)lҤi*\:sU "7&p@>%/p1g#bk+րhֿE$_}Pz+qBZ{ u `R&d={ =j JFskr-JHVb(p,! SOcH},ug> دa ƶE_G F[>994fI2@vҪ-@T!aP'?aXRLgK1AZX~C4zeXpgU`n#^)LZ1u򕟪j嫧kgO<6%kn3SHҘt|ox =wX W7d6#uUO?-'Ù2iu˻\uGU=QX' Z8 <=bO<_/!7oWس ֢j [{3>t1}Rͳ gKH˪zTdl!r a EdX_&*I_yUݣ?'\brxAPрE¼ZDeq`jfu~c9u葡4w~1s:_&a|nM d+~ڡ<2ދ~*řf"*;y16sPE'GyCڳΠz{=MtAf4E/r'?s^(|i> 2 i%g QOoU~L:͓ig\վ)ަ֟S۫ZlpIoX`?;V/Ճb]|ea]W-դ4ީ5*$20o4,.GHVi\ѢOh8^mf7duO7P\øt)LT-Ō&B򷶨w;2Eo Ngnz*QZo~8xZ{y R8] ?9pw{Hk_eN2mG?mU/ r+r _WOEcl'rk׷8˨(ɨj5ӗ{4=(_XᛞaNYdoa\[Ma%\Te >) 04qUz= <]€e1G:kNw:_ TEI'b1( dfZ/;]GEyb?[]8^za Xhdo尲hȌ`Y;#8ڂܷ UÕ&̟%\$iG-_SuMfTɹI'ƺ`^!%/i/blXS`;g ^LFڅ?bR2{2|޿M-*"G-B/2XH3>-nP~Js]~ ѣA DhH;\JцoMT(-EQhm6v3QXC4vv_HDZnxM+9pVF3x~Kb?Dx O؃@2#;nrU+UPj _zXa Gb@}c |8S7`9UjQu`=jFҡma5{ߠ"}#02רݺpX=h <̨uo摿D~*k/ctĐ)5>+9IP^&mCRstU .C0ΖxCov/ʹsJv{o:yBGJqdsy"Ő`&PzBD)3v;n7ZcޓvJk;Ib?-p|RzjcPϼb X ?B}j̬byw\dckj>[tz΃V56xo6֟R;P{\b:ro0bգ0xAoVVzNy'ֽچ7\'[螣.nAw;E=] /5a0LuH&+@q^qtxֶdOqAϬd26 =dZZLBA 'kt==:XBVnb13K֫954Wp&k9&!н6bwC#kqJ0?J} "R~TK%Ac/͎ l0Ұ{p/#MQiP2=>bĔO|@ۡLᯍ (NT4T'Icw>’_o)BmFL}*l 66gʱ@tNq(.H6mcT%<˴å ু'MRo'I:"/i\\]w/􉌯)q,ǂ7 %NHհ3 A#2OY]'ϱ1[v5SL0MWGA ATVoJiM|OYMҜ`O R]e0Ҥ<%$wBYZ9*} NZm(=S)048>e- -OYF⒇O_ܭ֋q hQi'|8XI~ms#[,n,D4F\dP[ 8`VO=9weŖϯ|}PvxVfyQvN3рt?CQj $wmGjx1ܫ_=bGq;ŗ=T9:O"a!ֱ-Ȉri?=$IR<`3O-^bq[ I?˙|*R7eF^s;kXi=F$ 0ݾ VȼᯍMD֥.*Ps.ff*gK'CKotnbٜhy}z|Jit:oVi}[bU]ٶ} ޖ MfGYѱᭉ"mOyrY N0=mզ12lz"Ͻy:E箞S3m.h?\Tmv\w>{hj %͓OoqOo+ГiC=@8?JWLݖ{{mV4њh zPe:~]>c^>=6U9[Mۼz1qFmt>spom]4`c[%u<=ZYZڈ/|װVma1kquKho$n₱-j9*y2M{ -z[CIg>/Ԙig#=`g/+/<:_WvkmMc,>, :寧AqyW^'9#\_N|P}C32ZL;/c@\ĢYSԐqFT#oYOѤm%APx揹g~$2uȸ&][IŹ^$°HE5Y&I xyy.6;ɽACW/P-CYu`y\^bx'zP1Bπcڏƕlϊ uy&)f fȟ6NNy+V]b4 /Iz5,zYww̎/3 < 7 N>N(v:L]V5=P5.=6WxP8R[W vu8Bu2EݣCzCEj~ g/1T#dXDޱOY_]2[ 0Q=\1XHYM%o(ٰyo^ir@j7)1^x/m+vܠ4^՗na{}lI#<-D81'iء[F:ˁGm,6Ҋ)CSqOƖC}+wRʬK,Ώ&h2 F<\gΑLZ s(T ͕Ȱi clf|G{q06d&eHp( *cvf*r#ؙ3Ưg `E ;h&X<4F8%VoF_XLc bZ/D;W*[Q.\Z}3jٖyF.< z ~#HiknFGENdk/?jݱej{^,yH8ŧKS[/awwf3eX[L'c'\R7g&Gb;<eU=Ls$Y蟞&P xL[2kǠ3gӼ?hhpүF9|=mɦRV\gkG[(,~ɰhlU4]v!Ӣճyx< 7smqfљX"\?¸@E?(98岟uzv]tU }1Ϳ_jB r-a5M /qVv>XZ @~4u?9 Z=?oGy>H1f d~x&A qyjR K(Cƥ쐝jWF$YbwЬ`F(}Uٕ|KF;)4\5쑫ً:ƞ٪{8H:d.&2l % N'h26L0e,qubh4JRfIJ(ً_Ð }HfJ(I% ĝxr.ȓ[ݣ'@[mS:ŬN^g?IՀR_>4dOLNl@Bה0=5n@yN~%H$H D&v)O.,hyj`4aeKx(Qi_*4(rĒ9YggKSele5:XS49=zR^H.%MAÎ(E?$ ?q}z/DL]bf绊x҂ZFc` 8&[4 '~L|]G%[m,f3ĎcH(_`9?[瘗f!uq$Jc{UD6X;> jz|%˙{Q3{cNvG:@L_zu:8>yCvgߝH0n$ ].rJV*Eɜ`9)8g'SgO)MRO?y`Iys#}rM]m@Ek?kۇ^h{S`l;z)q6Dƪ{HrESڗUm 1c̠\h$;<~RFuZǠH>d}9b,)BuD(o燧IaޮO34t|,v{r k*o,<% QK!dI~}3T@鍊ZO?C S桱/ 4v*sQX 9;\Hhsr¹1S{hcM^y\Y?nauƄՑu'{dž~yq0?]tum:OFiw}?utyMclZMa8uʧz?~Q{QZdg>Q0Xh竁Ւo %HN D.CL,ee,ubZ8{BCÝexgᔀf^+!w5F!%3!o[lRU33AZS&S~T Ïywq['ƒ|"[w4 YNϒ$ \Q8qϓW/e!y7L>.1M TBrbIJ"/M.iqֈ0KH 2$IC+rT/;G ~m㨸`ieY r `,LePZ~зM @O 6>xN?Q] B}ڇ^ ́$qwo cwڌ=RUuV0D4^+k_V_g=ld$5cߤ!dlUI.Ȗ.;`㨿GJ}|HX({Jvys k1'CBOm, |}&Ӹk?a#='/z>'eƩrlgGE> ez%vtT$ Lf=/fF%Z^IJsК{^2'K?_``sx#vQFh)Ms﷼ߗSϬo N\68ؽ~k[~D爍v\R/5ogeYgZM:Rvj,;~Lrvm~2zXsRct1hx g1rbГ{%L)|\]FF'W7{]?&~[SGEOb?3])>(US=V( Xƶkp/Y݂VNc>^@F菣}="Gۅ@QiLNQ}UF uvq|ө??kƅq:S_zfD ӫO|?B'OyzY$򉶜a\A؀/_qe=\ӯLt/0y~ZosK]%t1{fcUX}0Vbidy15P3r,h^^\>RԺ+C,.b Q N}"A؂+?iOeY2HAG[:#Jz!U=>>pvOw;~vzj|.4i-V#Wm,,v+ma oꡰؠ`6Τv' ?z;IHo7IpVސ=~dSݸe(7}"0}._v<^tT'b;/b85:ßwc|O˩yMt{QX,_y9Ƥub1 (<8|_/LUFcᬧQb}پ<zVMzHOOcP[-oK!}sQUK=C7=4?;-zE[俷|r1ʹMaD`].{ei_^"" ,v?p=<[[& LV# T9z{ QL^ʹk5CNaf2z!Qikcm9uy D]8!ogL8!o$keM $d*e|2?X|W4"] \). l鐊3K=b4_x&L\ 0(叜%]ޑ#yExXVo_| /=eui()wg($9 K_mx0+Bj4VlK`Gd'>3#b/ B7EӴMOxRD"ŅgOSӞP.xvd7~2l/S;bAB|pE4`+i;YJZOBƙ>6+z |ۂF?ltW <}9qOzdf?I:oT+:>dg&+G˪]3ϭ\SM=AV)'~eY7!,9[i1ԓ<֖|YƪDanx^iʠz8"d>vO=g,N~hyu'=qrө]NRPQXgd-ֿڵCc]X Nڔ-x8̍IrGX=kc#t1ϠZJC*|Cvk!G]OAmm/*~|rC` OjT7O??őaǍC;8t& G{8;rcY%|qGyoS^j㞆{@vYm1T4/=ҽ2}4[axȱRhĘ`x{׸mrV\샳Nig]J7LpYxp}48=N~؄k]ZL#%-V"%KJw!ZbJ.Hҋ'#4: (P0|-:I,!lU_0+V>Sx~L>\v- 4CmQz:x}6TS%ŅVOP\7 H JPʊyRmUO/}ע~&'.Gdq4Ƌ@iU)gV7e!BњoQ1"T`?M'0IB|g}(BS|q`H&Cg(WWF=aշTB53ԉȉ7tu^rIB E2YAAc;E^5qs\(ϡacU1 ?d3ܳ˶Sxd,ߨdo42W/ bb46tCȡ{syHSeٲɾ(y@@KJC|d7Xgi,M:'zE3MLtOf1JbdU~x"Eگ+8=l/0&A^ jt~ 0KfѿA.X%:tI{$LR~U8a_D?#؟4 B{F  iyS(&{^4q=)xP5 WhWj@kod`0ibٓ>`zE#.U:岔<gՋZ.Ӓ^RjgLنĕaG葈q^?(Bz퓊-0|bњׯ>!rGQjF54nz~mT\VO>#7XQ=~? Ww!tοQH_ _a0JT<'hbOh$=՝b_J`U&J||iv̮ duMyBn rOQNXG<_<‚O][ d r[y1LYkiSxssZ̃F &t B8>Z4IU Y;ץ,V7WsǭV=MxEvol#&D;#oZUG0f?|eFc]Acʟ1~9-3X:vׂiٛ:b>Ζgw%⫨sWOnQ;c1y3RӴ*~Sia,姍[Z©M)ܓUo mv&wi'hQyl틌f#<̬.lt3Ͽ||;E==f^q =|3'-ԣˏ5*@*gzlW_=@]G|m{[[S92Ko.NHsU-!?ǣMWI{ළ6C8C = )&O{x߸/,yIOk[Hۭ={|6\7uS\$VSx}%7 g|;6r/ /tZGm\qqNi;Řɾ'"/ǴXc{0`؍$8<6qрH.ĆYkfmw j9OA [x@e 1(V (O14ΈrJ&䃂PCzY&ԻKǝVO+;nlw( H3i ;^"*>y=ޛ+O*w̸2&ocMJfE1=cTV^EB #.|@K[g[iH1Rd }~3|#퐗p`=.VWQ}C;^PgoG\kveOF{#=.3v;|]; ".骒hϥg("r^[L3֘-E-fsWE9D'~vQƪZݲv85wNF}=UNON!xDd/W_%d6'? v3Ʃ(ԘG1ag m^2.Mʳt|1t09oޝuu]yy?{ni֌Ҿ?t*\$v_٢Ӿ@y?&(mWyJ*;=`|>"RMo]iy]A%l֫1Z CoqqU xDJP?M#"l>4L|n`S8g݃ki;/biˇdt֓-h1 i{11.!F"i-Վ/KeN;y02ҫOF8n9Ib~2{Ե}4:![C;Szk_gh+ Ez꒽P=/oskFC3":VF5?*«w4,Y`{^qNlJ<fw&E&䩭4Vy`uǾ' OAqmbw{ ~oo?ݭf=3domw}˫^q۴?RG2y}h{v51/˵=Kq:U|Saa̗ =z/6-{壦 pkۦ:\ k"偧_n:V0(? MCb&!5L醞gTU 6^RT@@?'.M/^-&?ˀ"Ce.!l| tP]u o{ +K#Odm"T۸NIJ&sъtC 2e[Mn/ĈDl&wWLo#b()^dLQ>a JaUbپX}2)(gr;s5 }{j1Q$V)L<΅Qʖ\"y6_{ósr}6Ѯ?0t ϐ$oΓFfT&p?ߘ{.gij Yj_CYZn+q"i(m,FyruBXs mKX =zznՓvz0.hh\3f2 q6*hY;J y8A;iF_r5xYtShlMPǗGsܛgp"ls3> k|ʗ[%u-Tk[OU2\b\tcQ럙pI⣧D[֫}K%9<,iTє{JY͇gſ>{Yoެ3Lr t2S2 quGv*S5yR;'ԷlQx;DE*1ߠ\ɴ'z62 ;}؂mޮ_U4n2<:xPѿϠaq䷞P;Tڨ?>i64W,۾J irYa~ګnޫ'U\E8=X+,ͅ,SNM>?TQtWKƾƐ*v*8=Y ɣe("_d4T#.ܥ.oZ={" ;6n_ZH>:0,!FrQ_Kg_ز9>bm;~Qj1 ՛!vT?t]5zfq~s8»ؔ+{y cpٲhC%u׶Le%Wծ1f_8L2&ma1!F͈( _% n@c 0JwRr7dzo-r5 Ze]x/ڕ N>r&Tcǽi7HNAw>4%PpI"ĝAst|+_9rڲ)'gϨzF_9Qp5%.dgnolf+ )|pDҫl4P!:Fq+>A3u[Bz* %B9yEBÑ[u@^%6UI@f-Idϕ%1U|EˇLA7%*O(<mvk DKH 8ݢE9cRUÓYh+~7SyltΊR]2+^l2\l t2o8O{;7h*91 $&8H|YOngߋq'`0h*zK,a!A@\؞Υ6^OQ[RQ>JIIu5-ϽA)6'*Ocq&U,$1Y8}Li'f׳AO[G((AyQfxoepOʇeI:?3~sihwNgzMAI+:{|Z@;?򌷹x,hz<- !GdO=mǥ#`Ӣ,E=]%]DžMnu3=)_;FQ %rّhJtq Lϭ;Ckom=SM Iʦ19ޯ*G|=</ϕ:%w&l= 9NeDUUP:,~ݤ?reumU+ӽbSR$J4\8Dd\nJ^7lBh '$ # 9G3 udʲeJӯ,&72 #S\ƕA:V cF==U~>O)(.5OZq`!C 3/+ 7 H_kCHZq34ʤO羐~ЄI^haڞuNYRSd aŰH Q?O @70\FgH*MskA;8A^V h$z…LVZa[gRb FV{6P_rFf]|W+ oZ}vbK}\& b5﵇FU}JIEI^Y4cwVh+<;Tk+P:w]L Kn_Xy 6m]lOK;nwyA/1:JRWI`^>r$5q)q]07='LѹhѯbTo#;ó FzJ?_2}y\w^E3dHC0Jm>DA'dqV!EѶ 9+>=_..B[=㤎[b_{\5>;,*vz!KX$==m@P;Majm^뢧ZɊZUOQk_ּD4V#n:˚1E4ńIv,G(.hW+7~YG;N5-YѩMO^?>Eol-<֕= oug/j:ٹ˧[7G6hX))9ƧNbFјAO='y< LИa*3;eex1Vw:ǀ]cd[~Wh(*z N1yjW\$;dJ9呀ĄV5[YSOSaY`MlvGhO>ugW3Nvi/8u9ڲlgjM>s4e'83O&N2 "zZ>81U4W˗<xޠ|Vfnyb.o=~^Uo5%y o<0.'㵍l|觜+\iiFG{at/}TۯksxSÅugO>W7B,ulk} fuI^J7x\NG–g鸮oW?qG{_Fi#m/(w+/]Up6GIpr^o4#3b^_hO><儣Hi$`QT ;cAfNJ Rf_R=h ֥;VAg`wF" u f ;Vw4*娡% &5D8/s* TGz7!1Lr&4 u[?ҰeerdG23*<;uJ6 u&JlTY.@."薠ᾚIO9Y:Yy>]?3{M_w#(bڲZ 6~XjkHy>ŔcG_Xl[;N-U!vjP6O BK{~Ob'v;˺GȾ+b)?-f~h4|SC_w> FA!bݾ&pI9?=8;a#GeDHsozooWv:?iq|Kw~*_W_扤1X5G_Y߾Z.^-6鎝툙o7>tݵzEjֺcQQ`ӱ<,#y@OPVHhrKc |T+>m1q@tgVHF/80MQ{Ho#:KjEF,gl!p3Ws2mrm5?N+rFDE_cgC/<4Ű%5MLo2cG祴pءג磖U[lL.2ZNfZL§f^rRnorAY Ov^=@6x$kL3`6u<=]wOu{,5cW#x4Xvq;VhQ%=\ˆ6ձABsbrL\9^Ndɶ]dؾ :={F6鼇kL~阕=v?t{G=v6>ܹ8f3Ot_m:l6KGhHbKAyT\XU>W]]$x~Adyq{5F0iז1$yFAK ,QفOff(608ŒӇhx's8&Z`׾ɪԛKtozv%W0vIw$*Y9]=×SG}С H}*zh1?{•·؎gZ6sOcXMcm*ẁ~PkzzP5XjM~YYjHd.ijZfUy1CMRtc2rթGޒ' ;7=lqDb ?Pc=D!0j5MNܟ~|͙Y_F܇"LN]\]@] ̃ao>r#8BN$}G`񒡐^{=\x#0|^DZ@ m7GGyy,D3Y9u^XPeSgBrPUʝ:_"<8+b7~ ژNˊd%beilMmyx5),RB8f9"3ODa#ir3SBl^.@><@@ er3*Aje8\$塽>G_|rW%i>UgC9H{)&B/";ʭͯ h21v^Ҽ+\ՓG5 㬭fmp,#Yk:^?j?~$$4V*|ʖcg/(w+o\e<,B8LBK8?kuhN<;*;T\1uXv֐2qa Zɝ1< KZ,2tKcTI #'2.lB_1*`E] (1]̚R-U0T#?=;P%A@IhU@j/rV[kgx*(g쐣>۩E<7=u^$iI0ղlͽMovd ,&_$N2}k *ѝ{q? $;Ǘu'R20qn@3Hdm VP|)_O: fưz1Og|GmH XOT|#rjܬ#p6?I Վͺv GO/m4>=V[} KяО2g,rOk,z~ xB#mDҭhN!SϭC]bZ~%2Otw3p~`=G=k;R?DQ`]Xjk6>H+#w]~SO&yvlO` `@Izb=gZ+9o`s{SU0O\ ߉m!ImT\wȚWH $򤁃םx# @{y HbR>kJr#7·S2qg.4>~Sjq~7 ՕnʚJn+3֣]‚iшW%!ybFRy n1X[dC蹶S\,СBa@b3(lWpy)zfٟ(Ouל ˔sEDpGQx{6,GI))~ { wj|)#M|Of-cu(KЩʣFP=?, e &c*76~MOq~%qԄ^ < &HFRyT=(ԐKw,wO\v9HAHK6  a2R8pD z?QZ̿yQC$ejYuYEJ]G;:R(՞Wi߯@b$3CJ:>Rp;$'CK);7US?O]ބ ;2u5C!WዓwPƣ*y/dgXoFx:Q Iz³L ijcKuAjTG=UD}XbSMatT^F*qrl1nD%%W8aSjX"[]gN OHF {R]tt_Prũ*ePw:=[c-y(iDQ(鈑n?t>Q SnlNumG_SxјJ{xT\Ҡ#k MI퐾 $=[nԽ4sḌ`?-wԎ^;.mbQ n;0*7L̓o#N 6WNT:$Kv:b :IԳSC@-:試lAiCb]j'Sںϟj+|A aoJsGݗC;b.Z ~ۃ_Su\Wj [>ՁqZy~'3uS?Nu-8%n]6A~GM6EbKӤsOZCO^Z'/Ae{/H!G]a*`۪ zkgbڂL͗I:7BH3E*Bjp{ڡp~m]Y1A_$Չ/'\dQ $E_ \f;f%""`:5x"A3x'2` e{bXIIf\$!)@' G|NXDIAVL J3AIpF^wp=v_EY:nA;*v3,@](/M|؍'ё|T a$(.R$ lE( i [K_9A˟? = \˧x91/QP0iLA :>7stK{4]u35䚶.nXP19RcI /lM&vr:V;n1Z9։s몤@k9KKpq@y--h3b2m9;eqH˰QûV#'btO,O`n ^>>8bH%E'a, Em̴o͞);"x]&M!Y+Ѩ;D};%EsW)wĤ؎&*0{%'BP]溾S&Oمl&Q\ iaf>hP>.p_3U?a_ ],&s["'%{U\^y_:Cw=o< CWWnZ~l1l* їI_ <~mjGUu;+svzG56޲gqWu~_UO f&_+Gnm$ ^\NI.^NyѦ #$mAmбJ<ǗSlFIKܪP ;E~Jb|4(DTzeBlo%d_A)G^UBD\QPzܲE꿕$O2apjw˿1w$ ׬B731ōg꟥ȇxDGp?T:P-<w5~ybu@_G;#Ceʍ'2Ǎ\"|[Æ0+ *p@ Ͽ_v@;l7Â:S]LW2Q*QXN2GQf U+L*>  -;:I߲5~"򢰙%eNٳ%Ү l?ɧ%̽2y>ON~I- PU ?4Ls.!>ruEwj7 bBͬ86ʚCEN\ Wr2' +Է0p.W wFY'D ʗwH,LßGg`#S 1]&vhI^<5@ zZ{2rE}մŚy)hx\.7VCN>{&{ȰT?yNֶŀz$BVٛQO%Ů}iWb"?0KN/rrXJʈz,O>mJpL<|Vow;3݀$ѰTʆ涉)`2ȅyo'䠏»Tb-A!e ?j/G؏tط~SvîSO 1[')[Ƙf_ݮjuT֏k[Pxeҥjlu;xaopNS(mWsSv'yѿ1rl0tLi[:xa60O "M2*.c'I>s8-@񮞱c/b&v:NMr(z9{$+?K.COJk#{($utODB-K9Hfj?Efs9\N.}VIuF;5Ûxa㣱f ʝ#E֦oyۚ^A~6zTPUY]n-cN\NﬞϢe3BWOhPziH4:ٻGDOP.eN#W;=i 6kH:|.+K]]Y6}iUj\pܵL3*`-}㏵;$>е{p.{ݶ➤TWcvgO֢4bݻ|hĥ驿qg:lޫ{`cugUZf|{Q\N_& r1wNo-&X}x>yI'r*]68O 1An_Iگ]XR3z4[TcCr뻭=p^%QY;zFOvRh{,CCdM+c r. ei4:=i;4ڟFQaDz#[C W ~8^DK3KEgUMlJ12%DOtApW殔+Sr(3Hտ,3veɠ1DDZ?:z^( my2?!teʬ,t| :-^X?Jb[=C/ " `CaqGy@~pʟzj <(&Xgt]DŽ0(ٯw5)9BXiDo˹y9Oʠ,Nw *5ӊȤ(B0#jnx@ƙӿGx=<i'<)1hs0Ș=gFs~-j]<15G,P7C-lu6%սʘYBS\aD({?8=G9x_Ryz9 xgZ&yʷ (g;z#qOi{6^GdIf!W> [8dz"$%5 x4Z=s)Q*QI=?ūÖP;C7#K |.L{ҕuꋹ'묞BJg`GS}X?XPmz .x X̹E@Գ`DP#e5ѻzlWjyP 1꡵)_b$/=fkScT:㪶c5V=*/Ah;b#9kCw|ѵcaJ+Bq߿>.kG<+jMH; ceq]IXC}g絯[ n5Ml1Xݠx3P=_¤gP mԟyGE|lz\(iTwU 혮Kv1IY[{̯^[?we$ˌgWw5~cݸnٸ~ -yY WӴ;vGbZDD \O}>CciHK 1_Ş=ߓڗ F/G:01rWBiM <6nկŊ o:>"<b)yBˣ~_wrA-x].>-O4|?I=Fjz|kkC^(<2x sۥM c":'Yl=1~7zfQi,j>Tw|[TX>5f*ȿLdTQ^O9fa=uZAb.H|;WmC @ĘPoyU@K,h+宇\=*0:ʘXΒhz%#|C^6i Xp=bzoϭ/}_=E?il{ڞg!Ӓc^|b} I3oCKX̡I=0+{k^O(Zq[0aܬF>Cu|קnͲt 7C7 hW=QVᓅ8 Pb6$trƕN})bO1mwW(G\HR˿aSdB K]Qߧ%u[v= wTE'0ϳ@XNޔ<vljdpSY.Fz^E]D$Tf&|K0*'ħQ,z&jc |~6ˌ:?+ų.lF\X)*C3JD^CAYW4 ÅH:䕜x&)|n;EY7hkC}6}LBO_DIǍ eE* ` Qf8Anm uS7rj9[zJ9I?' (pjiCT{s:F e{ ,2mfz~~M KE-/4,yHY?'-PFyS4 v/P'0A aԌԽux$Ob{'qv'_ZkNO~cu)vwoG3g/+--a#/5/E{}o;aSy~6Rgn NbP5Gq*PϴTm)J:YaX2۹31Rm%j=X?4]Oa ZG'Vz9>?h[e2O~C4*.޸z3\Ȫ1_D>X >Tsm)mkuW4ؚGC:+TYa+ê<- 06; ?SqQ:>x狹bH:b7`֘ Jo%NtivDlcPˍBqa.)V:UOV{n:O:!s!.&*z[kiAٳm9g^z''q㲞bS#DzK6EneyQᣟ`f=$ڃ*-gސ`z64kU({!GV3t>0W/vR}gJ%}O1dڄ0ܶt=ϣr-n/SEKh=ㄋ냟R8zU u͞s8_]U?ݓ k4 ?Es}v(έ#r^EGB#i4^6}@ ܤqAh9ǹI^:/yU,N^^RXGhXY͞7Jb$K:&ǎw_TOUl|ዣLl14_T` ~TN(S̀"(m6EBP۾ Nz[Е"/PoJǥݝù[ )8.gr4@Ge>9ntS%_Wn5ϵ?xDumli1'k֜v|ZE[ytٖ=lv/&7}Qpî=8s)+l[xu$X{|=yHC\  ,{'B@P-ώ( (Mqs_**Bqw =CeگPd]0mɠ' =cC(*ca% zhPuWؿteE;㩻Xg2%V2i=CRMi%U˼pԙׁ/6<Dn=uz:T .W Ю9<^yŴ2dJ3f>ߟd3ރ##V6f@*gיifǚG" }S=#=,&/^AHޑp4bóYj^VA0o%u_=ט Щƭ~I\WyjN 753h}q[/'uѝE1cϸy;CC^/bf5U=vyy6v]K SZ*SԘy!qtem\rxr {6;g[ik[Q~ۍM!LAOpSq qA&0PLh^dxR6#򉵄q`-D1H[A$`d܆LTe5qvԘw\ LH|m&ֺz<Cʕbn1RX8ж&9+2XG3̒l䥳.vi7\Ң acӗ9fz#L_07U ІX@:UUÎt^PY\x|u BYqz$' gX4gBJd ߃T uzsHBq q ;&NXzP .`h SkE#ELLު/"4k"">UkKPv17HH+%[،,޾j;jo{R*쩃H!!~[j@]"~(2ܶ!OR 1$kޚAzm ]ӘuG\&V}f?慧uĆظ`@ ԁxβ@6 (ƱcGA^hR{;m"Fئƀ.ǿp5DhnDwaS6k|uxdD3)gGWy" [3Hɻʐϒm ᠢ?5ū #4$^7LOQ3pX ۧeT1~EɃ w0H M|8@0{ǖ׉f:o]Py9<BðmaKSJٮvR%-c]cDn84VYۚi'|a%7`w_gGr-&׶AnjRau&SNNi[#NF=EG Nv$_B ҃7ԣR9==\ENX['r/ζLO,:=T_.Q|eDUѼZcҙlw.6{kOf0 N=!6Ov {`pAϐ`=9m|x8ᡑWΔM=)]0z<7%yd^Ճtn}G"ʹ!_-)Ώ7UO?T=lSlR\h@7Hl-ꣽZ|kp Oǥzb_~ugCR0Kynͬ16SqHiim=MJztk?aO L~*$_NMWU_mCؼ>²da0uU:l_ ٖJ<' "ݣM:K'XT[ҽ 90~tYF明.YE>m`ϞVO'wi9wD,,C'HWn_}t ػ]مMVfv=t{h]?XšN*71K+O}hwv7=uΧG֦7~YK,t&}-9nzXLԓji|q?[>Pi>OK_tr [?͇o{Y7n;^̹'LW#_J"SbϰX#qzrZ@>͎uCRh*93c,i̠=b e-%vWߎ?(Yi<J|g6y,b梭tYEK٘;D%Zv#*x;/ T ,, (ެy##C$U@S\xՎj~B;Pw kMg|FWp}.ן>ZņF]Ќ9ߠw1]p3$z.l>Sto@_W(uF Ɋ,:XkNf_:C+G zX`|#J @5N.+/''2ЉMS`CMٖPPPFS2*$D6+p1>tLC-YUڡ`L2Y@7M<`cϑ,_29ȿ݈{]-NP=hAvDOҳa7;Hex $y9p:>00 Fs4y/W?g. G0T N&+UizǔhS8շZ;b9{HAxN4+NQ퀽2T!]_ OKNU hP*ϱ;(ܩ jNqnS~YE)9ݟWSnnnA%hnj+g)!nOoP q>s''Ʒ#rO?}fvfk.*/<Өj5}>N=NA$J39>{5r-@] ?:V"۩txOv1%hS@ƺ}Ǻx\. 'vGtOib}/fS+lK;G7/mY}Fv #Om;Q3*q7Wj㖻|{CN"*-#츒~xϕ\Bt.c1 H1.v|V!y!`6J)WD4s>5=g^jb|IG "efOo1?-v^x. 2?2`bV9Fq.OoXߙ<Az)3?E2ߑ`u ohlg.SsWx9ZT~͛r=8Ui{Mqk'#ROߕ϶-rﰡxSP4GLaWOZ_XK0M3bڕ%{@'zni>~ 7:-qOpXnS䑛'Mhb.OubTxYzyσ~%Ǡ*c eӽ[h b[ͮDN27^XAë\Tj#rـ˱RSzРwfW_ml&zCbWm0G\uvSi{rEÜb W]hE1_~[=#viWL/>C?OSo\6WW1YiQd:ii 9ܕ3@/l{s<.`o4 V2nJ\ [Qh|m}3 gnvu(Ųm{ vIG[خ>D{ !(~Q \:^'_xs-O4@_:סơBpaH_keAU j|^w忑ޫ/#Ldݗ%˘(zu s\Us>ֶT$ EVEalp;|XαFa/8)&rZf겦s7BmOGceEQT~꒤HOIY6K7␢fw@A&! Sb?"sǟ3墈:]3ڮHȡY{_).uy?IDٓwԒtWJ2^;,˙a^h^@0PB|%JZ'ј̤oQ~TIu+늻 V}_vgsyVeB qwJ 2@?~NLǀxfԌ#yGž/$YwxHoeb~+\<吀:;*c)g1 M-ԣQw6ՅM j=t_+&.)"95f?!l2^8:ҥX{ǒN p0γ?+.t3 J,JE}A983J00&@i͕^Qgv|Z L$2TJ՛>dDƵrAGm&g>zf[%S%VJd#D]YAU6Y,~c|=!2p(Q ^T͸PI;(]eUSg^,EcqORC._m%kK}qũpxU`kQm'8p{ /Y2u&[l3Wht.Vn Q"aNY|'Zluf*\!}J4fɘx~cs~51~{?=fՄԏyƈVp!# >}?Mv|֊QN^"H7uBr.a,?*~L[˱َϳDe?-j<3"TFyN!bӸ3Llش:~!7^\rY^JⲤ})MOJ}w ' {ǻCw$+ i^DrE䪞f׵'?pH~: pۙsP>,=tΥDg9!2sxW,g`H}-e赶da%}y\[L2JH ϊW'Ȧ6#z𵐪}J3i,ܡS8i~5'uPtѐ{_IVjI<*+Hn ԍE*;C_[]yrG.#%QN~}c>"i-aN|N/#Jfs*B=l^d+>f}>W*!pvӘDG!rotFdHGտ-6#eO3It></w~S{j]ZJI"]r,r{4o9y$V gvgwfǕǥs\>46=h5Cϥ^tPh_>vg<\E*N"O1/?85*XN}ӠX:!$[m -S\Wkrz"b1J<ұː%n}V>f@8~a=B)Y2t7fJxcɰfvM6J}O(f*JZa N;(CZ3;:a${i/YJt(i,.+ם5Q)1atпAdH2-#H{;z&yqq*%$ l̔ɴriǍŹJ:I2 ގ:[+-U+OJCjc'&(>13ф+˕m{<7 ][_J7U\`d3>EyX5p0ElL=s( sȊ3䔤=ݳc:~Y4{1'W[HVc(ᮓK=:pzFGWyz Ap=9,3}dUh/Y$O'P`g(OԿ)0&ôO-S2nѬ^zifA,=|b~DžtP^z&UNQ6ƃΈ dY lN H[i|՚o#x$6:NMX(0fI;S#$E'~Yxfsx/&NcN 8FzE:Tߥϡ> W/?I֪XLs$??MM| nse *a;goSk+8%!Bēi*ovEϟtՌ(?|J`EYrR\%zM%gxz$-:`PKҐn-iĄ6:[S*sga(BVV S_N%>85mOXiWZP$LS7#IbKn- ?0OR 33\ac:F}7c\hϋ Z ?ލ(ʡ.i}[CHPSW~s5\ڵoB/,iN e>rGb }9dcVi-mx|o˒a[8 Vln~l.iøB}]S{o՞$8O)s!".HF?:aa;jTIbz$vj]4è}ɐ;Mx]ER !2 S#Wm]ȹz©tL!ɟF;_^[D]ާ1.hz /L>?`b$T?"=P",X,K <)6|ov90Ч"mI0>G ;׺8яK6 Oг. y?S+=W^=ƾ}ꅔ]2F I:s!g֪^/o7D>`CN+'w)=za x@0 < rTqQߗ~\tm}%L(wo̪g`衽HҨ}KeIjб/zgej8kY~Ҍ?RzZ,U>!9^v'p)PZ%>vLm{ LZ$ہx̏Fvl|O}ZU-Tšƻ|F}q^F3cMZvFfhX|N 8 "b}}L"@,~W7̇ *n`ь.+{Bs<<9 \b eQ#SdsA*_ϟ܇J]bVɩ/){& V%ʥ@EdҲLD#,O9T&#|NQ]!x6-B-E?L%8VaK ,ޭh*EF J23ebz{9c sFY0*&<)ѢCF'hd{ kRf2FxGܤuߓKeF^K}LBOP,5ؗj| kD|k=ЦBյ381>y.G y,2PŬx&yo~^Ѓ}-syNG ϱ8HWmgʌBU"67`n& km]g3k2PZ;V俍FQsyRp?">1BwUfr4u;Xiu>?e^o렁[{u1罃2Y9JD{_3v1&ST0j rwIz"b#قvV՚"xSR"&gƛ`E->VJ4 OiOmLV|}>[-=!5f]YN _8w$DZaAaE_8_E%Hj;k,ȝ=> Nr`,mߗ vZrR;}Wh~pL8d@H}>n-Bmtm b 皜EؾZ?c B`~s47&Q-֔aWpcs` [z? `yoSOq/ӷaJZ5Xk Ь"wоC,5+0ρwjXm`8L!>)OE=24o$]S0 r>~ndgvfD"9qXq:OPO>Xߏ>k^Ip>r-=q <1G8:z4$/&ڤo~t'fs0v d qgxAn9+c; b ,W!\LiDk?=5hMWq D? >bc\]={'.挙 ĐG9ibyFeİtQVa4 ?D"epͲvUx>`' L?Elنo짳>Wىp k1N %"elu'uniDx'Axa]Z#pJS[1nQN;&Z\1 E6'R=41ޛɱ\&~4鑃r\&WQYR[<Ө0-w[3@N2PևgT|k?lv/ kl}e1:/WD^p0ywWQ P^)|[>4Qory9?>x˵[J{[/N_?]Z oFoI(KuFV OwיWe|^{䦨HZ&e:BkpϜN:8D ;[U+';G8 єiJÐZnW(A3m&j5@-9/z}%]|2op[V;3 1R+!Do60Ykb,a 98PpR*AWqJ,0YmkKkd9RĢ23^-4tDFSg \lNIt|yڙEE1(@ XؕԁL&1ϫ҇JtL0z# f3=b~>uS4:\ Y2"iN6I `rO_`"P@u?EVϣ OZ|i=>_FŅԬIJ1((s)`Xœ D?̟_4z#iLoddx8ІGzRT:*"Ĩ5yMROZko4fa|%ixn$Y,"U2^/FpRo"Is ,%,TQ7 2dE5zX qߛt! b\vN6!4zmK :eJ(?˘3]p4Zڎ"BxגX {DƆs4rZC$"V8G@lg*`V+<砨d) [MDJێ+rV#Vux#gI\뒗K`ϭΛiI+œY~VzĢ:E{?3Az vv"' ǯ3wCJQ΋v0L^rnkuIuN{vLO&m3, qILޱ]KYsnL2!`\w}a-/\C5j{UeizP]d뫙ЇN:5 Vϝ7 65"wLĴ3dфe?^?[UNс34߅o4`3 9/N'犩M 1jiK&>I#yvn|7M'~b҄>b|7?t%651)x49B9jkQO g w_MKph,j\"<=qqnၜ8}V5!)"y\:<$wD"2B {.ynG`?[(2uuo#=.*,NN9s!+{N%76XBQ- r+I;MvW5o4\ސ#5E{ӗ WI:uϡ.vȧj2ΩuZb6|^ DJdF}9 p ySt]z=j #˲tikut:gddSHO湤!&LvQٞQ]7~_=o"D nР?#sc#|_bD?7ϴ!mʼ3N;d uoF@>p Dx jK >ƭ;".<1?f||5hE~}xqʗ:YWݕ[X{nXeW+yTI]ϰwg4\a8VnZN%o1viͺ!K/4nVhyD2$3 Ǡs-7Ay}bP,2] Qv`_K6Sv]\]/ۊ\62K\NQBϑlHv?#bW=bX)'rqp~z\_q@p;nW]RL>$q7±]w/)pΈv:v}=ƵWGx>\?xNxѢnyW&Ͼ|87ĻrVk.ڇq=~g^I,oe~7$ -!QFc.8m8r}بMQQ /ϋ3֏Zޅ֞ 巼~(2i54d+}C@#ԝIuՄf/(mh#W  ׾f70VݨiWߡ*&Lv[dn5 CZua'+·w&c<7m1g"Ip);]Fm;--閆+[ ?i_uaOqE}:c6?Mn/ e)$kd-rF"3z,+2`,ј_v3FjpQ>*C\c<*A=ox@;=PȬ7zJkXkb=-h8&o.fzo˿-a}oPPK.hge}^k=_[}A,+vx5Ij}m?3z{H$cJO3#`!\t;'{\/rސ&vp< |AR[9y$))0\!Uh =7ENQy=9_^s\kUϳU깣vx>?]$bNCR~"bBqv#NlSEiAnG?u1hWbCߏv=# h]܏&9ôEZO}46zh o,ZhtrOn->o!G穝k t"A|Uʾ~"Y^@U/YXOol>9jqDjC'AFzU4`eP;O=oliz~+NmozUvISKZ|oɊ ;q<dHI9nȾ%AI"NSƑiⷔyBvAEn#Ɍj6 -'2h疁)cgv2r6;BI.ryy,~F:u!֙^&QiϦsF*ޠQc,1Kw騧'~mdk;s[=H׏+W*@pGNHPt[^^rb yT ~ǭԵylkQaoinE\o W`7!?Mf/ p]8sb 6G"h!uY1gboy*/@m_\#gcN6A.%R?Q}zY"W (Mf~[ARnA..AWH4//`X 3sGbޯMY1QfmҮǴ(3zA_`r8M.'#+GG'^\fY%N;}=N6MpnukI :-SW^w)PH&O}&3mdžݳ]a{z}>R'sx[$6ucsČAy%@97+ RitdU]f>UY?%T) Z_h3/ez55iBt󇝠Ǭ2h3(Ji8/b=`4cLVZr#g*ֱK']7'|+-twyDaIIYyq$ t-tn9gZae|Y젃UyxrӾS xl)䅎.KHp#_;[-iDhWS[sx2H  ~өLW?wbN9!EV_:πpYH܇[]43 l|inخ1[5OU\(OWD.\g~tEKeJ3)=hG|&9xn}-ClZ Bt 9w=I}@:L3kWaȧ?ϥsB%-_hA'pV~~oͱ8#텦o#^@n)cgQr~|t~h5sΛ{xӑu8b1bѥ&ZGz4Be6.;Oeύzj2lrhQ ?xA r6ִ.=o=. ,];|oV"ߨCR ,,c@Ýpp.N}gs%V7i~0@O}6߂bHJB v8 7#=7-FJxmDui毙%hzӣh.cף/biX\aS!Xa`⳾$*?Gε}nY=]sc}T`^M$*UrA Ludd%ziawJU ZbM2c"S>>GZG'x=‰>h2&j}jv#cg;cl0&(eoL7eo 8ρ4y` } ɶGF<۫fk7ß`-VZ ɍVN3IiG&Ut&a }aftS!gVu,6VJߩYgֶE_vLq'k ̞H3+uRI!sCO@LJQI@cų?y&(7djŔ{V6|0: &RuV;Կd^p_w@ˍo&E +w5i?:H1(Xk^c\RU|>>H/rr!hXw<[f_K8hy7Ҭ3·cZ7 ХKng#UDVVUXt;y?l62xd&qiK;-')"d6J:=o=Zs9^~ !t$RE?Oȋd w>X XL}jx[g_a("Lv*ȍX4~AdPpod#Bcs#7HHNXݗp*s^9@,띟t2Y+-fɥ>wDij?K2:i\hm&i$5-.+ܷtȽ0:A_qvS>yȑ(iشɩȎvJ-GyiùyiΈD#[@~:y6No峍*[$"¹rH}J#4ZЗ8 [V_Ne.Wl7Dc<,%ayוLu "Vr~TDD/?&qCmt/܊rl9٦d;`@ocnW0/"*OH)J9n3Tr]_|-w~y4K_UKʳ6m㵺A{$Q@/]r(ZC.4A)ӽ XY{l>XKXgÑ_u^[7;ƿ4<,yE4>Y j~N߼z]ۜ ˲+x~ܕSflQM">v@ '4 NEa?U=4Yۼla Z78d8}׶S 牛2l`}bl7-is1G859Ha zl84~?xY9?ZnkQ:T[EJp7D"%CEօ/AgrBgK2dij5fTd\<%r#Eto_OL^7,}UbL)hGu^6$zrEjN! qbKQwڊ~_kӓF8~l"><p./ڮ=. -4Im+{F`1"% Υ_/[ϤK!R'8mvӇ=Ҿw ޯ02#p-vOj\DrC!G_Jv_j^]hܒ5Mg \N^lͬ,駿DyVVb =5rc#7~StM$;]ò|@ /sY~ϛt܎8<34"}M.s쳸r (:q weiN wmsHnC? Z IdGFOL@""Wz#/;%+ {/Hh&F3tTڷs ~ox}/?xdfg-)`4r~/݌leh/dhPV閥Ve):5E1̦:.S)&D4{דbD >Gv+3:WSy7ӤrVJ+n XY-z\݉Ri$eJMeZq穩u=agR ,55zϧL]Xatr?'2Ab#}vKeq2QTvGBdܕ7Ռ,͆Gm[3e m bȴjF|;p8kEP#-xB3"I8]Q1HLGjB`E%4ҷLD:y,Ӊ ~w1P输Z >Ԛ#??k?\fC3){MZw62U-Tu^f<]0j]ܪǷ-:PћW:d ;l8S/XRZg=~2L3RM~)$Z lwm~=O~pwxqN>*6 A&WLw ":ش͈Ww)w|q !N"w~)׏_!آv-%9'A@!Jwok"mM# v %chsNe\[D~q{GLǴ63&su,r_Ɋ9x 9~)X=,=/ط"r@WkzLzj{6h}4Sf$}!oS*֦ K=9I:ݛb$qfLwi (wח2^,zC");,ewddsLz;?{ &<_df@`I_!_l jM-gchoSzb_\=&"G㺜0 D>zDbg '[DzGn}zߠE,i>B_kCé=ck9\/+r/-kl4|p!'rl-®N^\?ӟ|YR=K; r.ڹNpB=zA\#w^#[Ұ4'VpAȒp*SH&CeFD`տڨf:͆cMOOn~*."i?D) w Љx=RA#8ӢlM'S tNݧ^/w&?ݚ\/MNnp*En0?H͇.6oWaЌ+NH`כ&Z^] Ixvw{ͬ6?Rl9Knf <%Ǘ o0[Y0Pq}A!|dnS Nl&R};c?iuǩgDF~_siES*a-"rJ2V{s#ڋ.ZHڵϷf? 7=`wsBh4ەxs}>W{=W]/x+ny>FΤ:#xcӪ2< vƲ}Ya%@}~qzȟ3+q?<5sgrK&(ꨐtO[*NB҃Κ7]7|yIѺm 'CU״|F_0~Ĩ&^1yC|63pJR { ;򃹎H=heJvĤo^F[CQ7+ևG3KbLIH(Cq R>PWr̺>÷L9yGzVu5%Rn5\*@,!1>ÛyyG_7_J}%u/fq]7L,p95gQ?H^'TdںNKcoŋ"b ŊVY 3b*x_`" k-Wk赝Q';q6ԄXіCic2u)ٷH.z;u#Ӄdc^t9"`,:+YBs=q0܆Lezf~smN2H9Xٷ ES}'O[S;@ZDݾ:~oyF+"âxwb`K7֙cҕ|-z'">9Mns7D\NY<:9=$3ו\"iG;IIda=y)=ȡ9RAdT ׋#UvQYy\(U5Rcx%yr/ˤbq$' S;cn gkJruԤ$ϵ%<>ZÅ .r~ TAm&q0_>?G:HƳ3=o2y7q"8ipv? a\ &_nMp4c)4:^,(YwޗR9}s@Mo?7'@eίC%9c4P}JìL&3x`lpIv5+$}GwHF:)` RAU9\S#J JuT)Rz¿HHFS ].̏ ~Fv Ƞi5yPeS uM-oq"+S Tn"3$MGzgNfjOuFUK(ǖ2E 6#HGoKqY{pq9.sP.*ȐH}9l"g0 !`~#w?^$-й\n{/Dz]l 9U1ʼj"yí+U"C0K?%#}A?Myi>.+ R"k RNu:p#[0(j=Ni<v8Mxx9_m!DaqKNj*EEyI}>OBM{ frz4'{u~BS֛+O 3)Rج # uC2X;}tV??MxBGrz1'}tt5>5NBH-pw3ֳhٍ .gt:>nZL&p! -pJ¶Կ! zae{6poYs"KcS?::>VwkAjr<)r67˒M{3{-n/цsʃݷE O%S7Qj}zC;*n3Ul>@7'Ƣu߈,MdXDIz̴i4`|6Ur8^io艹Q}pOhxg>efX .meEy3CةfrZAb{tx5!8eOF3E՛+ *ø^76O :t<bmU9+M*qu\q22tcvZK8{U2`MeqzK~%sI ^j&_y;b acS=Ő,IL~V4rgSj6VVˌhd`'`xO`jJO&7cIz^b ?iv93[b9d-7,^ S,Hf~aVg!J˿'B,.<'5zSsN)`U xAw?X٤Γו̍5٨>ʾ*Mp-Ӈ&ncz0G]Twap=ςun}SZ]M:B K WQA$y\y'{*L,#`-x':O,9A 1R gs*.V65qs?~d([:ON %Ϧ6}G{Hq :Ԟ++2;-5+N_R;v5Wa3Cjj8#v" 27N}Ks߭tRDK@{4>hl8^|\}\<] -6{Ya6ƺ4a;uq(.sp?Mwf+Z9fEGlG+}Ce==9񰕯[v9xL`mww29 Hu;W[\@w9ܯ|={Uut}JHI<0OF}9w&<P`1G$IwͰ=*5do9%\qMO&u&*C)Mm&n G#?O4!0 rp3>4&yʍΈWuhb,i?PƝ w!?yHS,3z0O?؈ş]͌Le sܞk0qôϧ l{܏&b8 3S7GrƂ2΅=< Jl%fN&vg6]/'n2oyO{*-SժDU'{51GD϶)9"oqMdohu#?̀;*e/е o%tG5 x3XE8b3s3e7#ZPQKAkkzU gg|T:e~ϟhCuׁ~}9_?&*i"Aַцo!w_II(?[D#6{鞾cZ ~z@Gn [MQ<.ȋӀ;&K{י[[iXu0[7$o=1":r'w"#77m{.7b /*Zr4p Η;lcNP3"-TվWM%+,.xDv\y2G(5>V벶Ӣ-Brħ.iWBix/+}B뇍I.}g)֤.&9(wENdfqy䧇(&8O݇hiҧI{Sd$v?QɲGИ-rlk;:5ٍ ZD"yZxɞF=tQd{s} :UNX`!K N224/Q[sG>-Pǩ"TQGtạ;o=Ro,J/?M:O?G=xGM1h8@ ]mga8%="{[zj)s[ Tt{Hz 2ۮK>F\EW@xX vakw8Ve938y4yIlplϋb1 [?-#Q1CY 榇XF'" KQS7=~~nZpOi~vjONɟ\_;|4;8trb6ykĜ-oid8ZNJy[:n׋rD+vc?m1*,߀qYsNfsz3mF2 P4::O[\ -^rXXb4P?~<6wT)ڨzbL`i]X#- R)Ɍ> ڛ62xU,k)+x_ebw<ю;nV>t}}U5ԹmF0}#!]|mKA2&əxG0, fӖIHse/Ŋ}jX$1Ge"P>h6 x׹T (ᆷ& ſc`Z=c <^:vy*0OgSjyCNʕ{d8csjF&VD+-~PX,?r7/XUQ/NJMϴEu  OjE5̝~CEnR_ϊo&O`03y7{)S+硍̋Puag6wp'8G-UiF}#|9M@0~ƧN73>ƥ1@QFnoс/aeY>#B,J̓cWbgf)G3 ؞liӹ/45bҦ*_f\-R5NG->_Mѿg: \ ?Zw];rBfo#'{iLSGtE ch8]T)͛帨{At_3NFI6v&|VWG8y\ Jluψ|"q6yqN?qSi=Jbe8-r-[oϹ(6Ff#[ÆUKH߹{XC<'r"1S775w-xxneK1C*ƊHA)Kwƍ9ur8[\A?}ĸ6ssºSҝ#Ve`8,]7!P_.jtY(o˟Dyqgz$;~!2᯻réťԼFl]}Dc@\wT?Q Ot^`!3Ùm/r+ѩ"fJ1jatrsG,ũ !O>{ح}O!ht@߅|#eG$n<E_^h=-oxAlfV6խYWӲ" ٓ!S9į>>f)z6Nݻ\@"}7_0ެxۓ/Oś%?z7؊V|!~>Cc!%;ܙS`y0K2nl,kQ_x/fMÚz SEL eTxшbnX Y_K;'ܤ/R~j3"0(~EtӻSgD޹Ts =&&qz55-"sO v4c|~oF)F)p"!2|B޿ꩮRk7B5+6홁254}W 2cDN][kqI":6os G7L 0}/ H _HX̟4Uߑ sIRBrsmg2]uui';2jV\&ן^lXVqPIsG9zp?)}UY^ E؀*/!?/EeB}+]_R`hO`ݱv0TBΟ ʺ+r4?6v}ޥP=CqR֏+=Go>3M~2lɳS4޾;G]:ֻY}Y&vѰ 76 ۻg{|?i1%06,lMuor$i# @ zrHQfqP;0h6< +:\;K3Zjs{9ZwHxLOt=zxY66lC>',/KV(Hh Sm5Ií _CB8gvQ]+j⚖W&tyS5IzHU ĿΟ@l/:9<* ܑ_wcfjISʹ|RF$3>ɿ FMm 2/!+o(dZ&XYR2D gי216)z/ajC˓,_ج~oTon>рE_qHXGoRdr?;bJQK?Γȇ=~;=xJ.?=#%LE`3ӫvCxs]˜vԵu=)O<hrm1 #G$k~rC`k$uri7⛂S ~\ENKHG|W-W7r Ӽ*!{\^+uzmfP[AidϲD`!%Ww,2t}| p^s) ($ŏmdŵU ]ruS>;y3 Z{p4cOL#PF; Dbt9+aRԬ/?eCU D"Y5SGn?w$_&p:#M9,L>>x q)~+B];p|<Y <Gz\[8fOA$w)RP/cIwQ,$͞*t|y|yoZ'K!wAīOO4eJ|ߘN~Z|n;pqʌ[t.>{ݯvc~@-ϟuWDD-TwS-9rmW3 A91Q-Mwu˱:<#b2} x+v*;n!'r-C] 7w;e{t>/כt{(JQpwDᶋ{e#I 8x.aC=N^#R&0dП\O |HD>~O!gy\s{.m܌1' Z{Ws}Y.PhKil~wś?oWW^ވc}2hh?mn\ KAywBUs}p_)h_=![$m[N&zN`oțEtPZ.I~k\{%|3>^ O^&hC4PܦO-q=>#|geC?:·^usv6~s1TM~¬ -Q==A AӤH5(ywfn.Jba7?dZ6`T| ~% {kQwGhGh?y`i^A})Oiy\/mz,㕣b_IoWΙQyS4ң'}D t-P9c-V{*YӍNjV\*O !RQW S۴=ͨcqJ=+L z)Y(#X?,,Tf8NcǗ`V>'3ǛRP=6=O"/Zk3 k蝸:Gv|x(WmvG9^mnGxwSL綮PntxFБެ>>kr邏^%c3ZczIz^yF!TvFK6~i]mOvx" hn!KKes~R#S0Zxh$ 9kɈ{D$M՗IQ ǖ6"yx|:*R]|"*Nً2Nm|̻X6r; .z`#gʎz8wxed_UvX1|+{rt`g<ȴdS;}~{w ?=K/D L8~n>r.(d8WC~~l{ ߗN;ކӾ<[0F E :E  ɂ'3(4[ZˋK}wwZ>3uCVHzklrO/ش;citA(zȝ7>D"x|"RpQ☳z-ⱝܯs08uY.00/w? ώaD*';{"ZzNe2Z>B&6/7œGB[;2ˋG>9Br1G >D$8VW-v6!bu@-(W6OƟrayp`t<Ұ]ovH]s"yzwp/#j"r۱"G^Հ~…-p˩7w0IRŷy G)4&|Xu2 ,jd2h5#Lz:*ZS _[(k橔o6 cq̵~L[Ż !@zp/\8ⅰ?Z$ףEdg`/υ9&Ȭ Шo{sǗrn10~*/Ĵexl9WT퉌faw4.?dBd8a7D8Rq(ӡ pz~c)7@n4WBֻnGbjB ,DG5rTu ]=k pӗ>O0xCr\5Ƅ1೩D(.w_Nq-cSt te n: DNP=fxr {Ti> xB9w/] nQua1)70 +K֮啕cE:LDҋN.JbY=^CNO`g-0c:iуk:D qzZE1:fc}6~x?^9)i{xC9-Vʨ1f=_'yɲa3Éhz͞E |dM'(##4$'>זyw@?~ީ ϋd9az3l8X48C#>;.}N0"ho1^y=v ~X>4į16JL`.ī<:=jD.&NPL))"mefjZɎ%]4c\R*`^8sǗ60\D&rsC^R{iuڅ$ϻqkIC YNУX2MG> **yEgLAs &z4GMM~mγ>udZlͅ KgO{.ef\4E$?*}G@VڮW7)PrEUQR7= (O3N]?Ń.bMFu$ 9ϺU 8<3a}۟עc340)i z>#ωHGjqh(u+N֤ XϧCݯ(=C`j޾θ:fyw{.4pF;]S8umFGd|vf1$0k[W;SK~V8>0Ͻֱc oΙmX ○sT/ï?[?Uڤn-니$/!r$"K4v82;Y#hk(KNu9vm/TKPos_zň\FV< Hvs{<2pS 1xǩ_a\-\y<\׭*1ODhx(,r9vi "s܂sY!GN@zvM| |mnM/0 JĐv?P2#W6Cr9WY2g4ڊG*1~|v5=# |W#z|B`9 ZϷӞtep=abSܑ,DZJɧ@ yzgSbUލ\yo*ڝio6hIzpR~-CnmcOu8z/-s8qPR"ם)Ȁ<"ˏdRIz\ZG(x}7^;ueѲy-Gn0zz:Q~&ͭBc칦r?} RHi]W\_pzt8{2/g;P"8)>BYL\ufSh6]nؽtwQiyS?xՔoS='ڶy[74spGK ɤ&6g8i|=bfDږjRn[v\t }Fzc~_dk}=3ߟ  dLfm&y%\/4}A{eI;Kxs# ӟ^uoȞj Xeؽe8RE}OsY|[qE^޸<&K6'U/m]1fe='W^t Z2WonWMKo8d9jy+xHi ;C`dKeG-]qi×[5 ^0y%]ksz?L[ 2o^4קKK%|R}ux+ZU9*34k r?gp58cmk:bnǰg·QYS9L+瑠IUT?/$&_gg^3ƄJa~Y\,´btbɸ\Ziퟚ|*?Ϩ;3]밳PcJNbN6N%4p̷Iw]$t7k^_q䏢չc_Y̓}>|mVZx$]c/U9|E{Ԫ3pj,Uh/СH8<XN_!>שZf2n/u~/:'pD-=V#gktF?ykD6#,[ 紛¤%GWNq,v-鹂~ڣI 2u\Wg?sp$lW}FGWhO<{d 8W0y4^H._뒇x/;Z=KT^9X nL/ThƯea'!s)ANoȏ_R Q2:28V/`x; Y~#VڮZ5X=' NĎاiC7Ζ#B}sh(Ok7J ǯXF"-do@N rzގw6ʧY\3^zS1 vvF"c{4'»ysNGy_}!?x'7MwDRġޣOs!'yargd󐇝Vy74ܯ\" pޖԧoB+Э+7n׵Sy" 7wҐO0-F꽄8kMxVkG2"}pO<'P3^W.)M Ba+BbșvET6$Xa.zo/Aԗl5y/.$xwzo8HhPKq%/+49EpϋSPqaz7άϯ.,K }}g#J@llȽVRvnYΣ4Ȼd6ϫQKOajM#Z3=(MMFToMb8vAs% .%0 uj'%cZnЧWK2beHr4\ʆv>tiÙL?\;Ҟ{˞wQ3i >ʯZ?-;.yDٟQy U>կ~pSO`)} Cw?=:'Ğ\ci 1I;rf}\TCA:Cu4(v\m2'ɎZGluci&^?Ex/*{є鳀yyMrկXɕ0)G5 ٦:I X+P'qMo0]h*ƢLcJ3=3R->7Æ4O1Vw5?v P":ܫry:_8яI:!OE·x97">|hK#!c[Mi`;N9\t>N4ߊO@L3?CZ>]c o)\`k"_v9-Ę&aTZYadX+W=eN9W<{[ot4lUz=r9 OԳ:@jhTӣ\&r3)%`Iv>jk|jL3YP߮ VtnCpo5p^냛M4)vz}ӧy--jpOg-s7\vBI~E[;v֯4iÈu'Wם.Rxwzo?79<"ٓ%~1U~:jav˷<55PoN997Sx:8-0Ԗ;쯆%3=`P)թZ2Wp[w*E^aJ3uDx=d?{k-w?lG8*i޲=0ouPlU)kvO'?Vz%3J xA08砐jfBmb??GYmWl(N{gs n&l6ɕs=/,ρ@/:O(r{D="85X(M \Z4] [ \~{.%5Vs^Slz`߲ӥboq/*(VQsOxqHOЗ(QǜCng/z˲M^O朏`,38~?F|hw){[=ꧢb ܌o-ɴ۾z z7W.K#SO;?qz+0l"ӊW;վF8l@+- f=&3mIξ!67{[գ p~No/rms6 }8yxy;rh&  q9Qn~W);z[kzot2jB9/ԭD )"|9!}31+_.;:>c;<Ǡ#T'jW:7PpxT![M%Y-w1֙M = HkŢ9ǀ`F:KT-u߉6O,r@!\|qWet8wĊN`zNE$>._lʃ2U;}NdX=cU'5XΰXg ~L r:ȡrO?sKDHc= ICχؼ >t?a8K~QR(S+D?4yv 0f|?aqqiJE|u bd]?oZOҌ:>hXZўJ'.80slDA^)9s9q=]e댆Wmbxl~ϯ Hm3-Fa`YNe۱[Z5VI DȻ>El~."wim |ǿl9{ټa]li[pi5o9J'"oWD2nS(^ Q[ہlt\.-Bde&N1v!0bE+uUVeB]~!9{ֈQU57܉z[rSϋw5׭#r;]Q<ن'N]l`pܐgԥ=!7]ױZwsreEn@;#IgX- _mv ]A7|?p#, 2]$vhחK=,!D!7Ax$ N|VrqS>;#FBl'wge;#Y{o\FrҩԷ{{s g*)K#CVi&v\fna.O|S?UAtqjꮱ5 ^"g~{PuuN\Zg4h{ˑ 7GqR}wu핇Lp!Di_Vv9 Nt,Q7$/pjy^ҩ-arD$*C/?|z豋&6#:1׽lꩮy I{,K:9 U"Fw>]6"p:x(zJ }9fI tnRD߹r+|+Tfv~X+zo[&6s&RM,nbÐc .$zs'wփ~GxO7M\y<7Vd.x[-=!e,/Zg#+OPfC|sS1=. :=םF{.2j'= I}z-ي-Rj#>Sw hQkʵo}i h7׻gwnj}@|:}R >vp[/YS9 lvE~.INfݽ_&)m3E5*_|6o_(\JT+/2`&Kt;)nL*R,Hc+zғ3jkS,8:o91Uoз?h.`7<|/ZH'VrhYft !YZ 21:[>2hPx(OnؚhRRVg9G(V4 ]1o{nr?9qa2ȅS5y}"r^Zr%/K @O[?`s <(BZӈy.͝E~TcAD_~'9IrZvq@nNE4F/u5."z {‚U̱ކ-TT壉bcZ 6΅T=I˷;.[¦-9Gn]|Jˡ"NwYzNH C 硹[RM}|ΙG>~oju6:(6.Π'rEPj_Rs /5Gё=zxoqګ7"+\ F#m{F_ӘOެunĽ-=#=bThh[y-$lL]F.+z OaUCt}"r%C߈/j 7K~@C;niQ /$kVFt--Z:qqtR! w_]&*y*b7-Z2?:]svAߔx[NM}&*kϑt+@yƴr/AOgOK8b$VI>Yq\,"}c 0hi_k)|zFMB\{ ~Yah,^o>nHD1O/n}{7ϕ^S((y栘~oh. _׻m,vw墩5S#ۭºm:?NOG\⁗oV`靃J! bm; [io4ӥ62ʼn*?PuHhPA^G:.31nqa=J`<f:X㚏(Nz GI[ˣ뻎5hV[ KI:xFF3t|'}g̔0+5,g,b33?Nü0ןrX2=Ve#.| h $=dh˾I6iʹ fǑf4Arו9rc`^::W&Mz:O᭹Eޮ:ґf|̡_L F9JC{m3sT1Cd=Eٴ "WD\ s}ˀS]ZO$DVCrŹeA]BzAfPVs[opIuWfm]סݴ>QQ^=WNj܈QS}]elIΓ<><q-6Xoh{tKxZđ_ !bSΞ;oYb{>3ZHcJղA~=r>1ct-V/y]?TL櫷@7]@_>>0nf/x}6Xfu^_$kufN*]*;oSJIyve[~ Ϲ{mADB`+]e. % I<bz'p+ {€_~KU7hOwl9.$뽤\9"rRƙZ㎢k>te;"ھ>`Y lB++}CV ! MO}ϋs\r~XR.w@Fm]͐>0#$ vTâr4Jl |*!3rQ ;"rO\#3ŏfDy,H2߶ -[jIB,K9S/?nL=K=^o'!܂3N =v0!Fl|gtٜf]o}j 9gQy 7D:2 z,B"T _1?kFrcSFrOIUnigAN& 9 sʆ#ioJ ^ސ3t>t5d'|e\Ni@Il9&;;{ X]y%;"W'DZYDd좞W4+wD^`'ncJ'fޑcqp<ЏE5HyA)!{!ti,~\q-r8S M3bcH~MmZ-qz;".ý~SSv(؄ӿs4Tm7y $O|jf,qF$rO!sVzș(7<_XuMvms4ȯ lAaq~v=M~T %cZU.;~mPjȵxs/m=#hv" ׉ڮȏLQWWXI#O1rK|9-M~iJwQW0؊He/WGXG$_T"/IyܬqBPԋPOaG9|!"C^|x{q,%[Db*һOZqEL Hs`d9r'JmByY+= }EOk at _|?9p&z y}h Qݔ2G{OJipr4n^q豸i@ӛތ9z8wmt C1p)IC+tAoZ$ o4Ñ](~'KUUMng[vqTy0U4w'Sx@-kOQ"ag/UZ %~tᦸjaq XVg K9pW]# m xQprdR>!ۯ_z}Sa F]rF~_-cc]}<>0w;]:&;+g飕dj6k?cYäI\ OގKY"~ieaO9 fw7~?i?i覇?±KOQ?b Qˁv#Zlt sҀ[LRT󠯢.Zu7S1ެegj^RB~.4pi>tG#jaR * %r*27FSsi@Bu(.J[c\CZkQfxD1z)U*sooyK~m8Aqݵ$YRx. =#aKtgO^ʪD-9[03'~֢\`_&ڎ@n_w?k1̂r*A;i7D}L lZSgj&ҍvM~˘+g hJ+?Y`AsϞðZz7QXي *rK\N8T|qTXw8T%DZΧL PO\^|܎^q">:SuNnzt]Թ"r^PYn%/ %'^,>!ҫ{NYF(X}nc68/VӫE^3 7u5E g{`w4۠'9?63/(VY Aܬn.%rR>49y 8I%=iK7 X^Z(6wJoɿ+^}Tpkk?vYSl{GXbfLԜې vhI%xY @/<.ڬRM3m[#g໑y)rGo}c oҒߥu <>*nӣyO23R1D5ў t Z2N׽wu$_c"uQu|kLAF&/1Ӻ5_1ڎ Nѥ|> 3D H3wG0(?qFNʵz8o^w׭;MSiŚ^Q~q5i]OKUQ?"6vF*fոt&+.xKN^߈"R2-w=m""r1M~)v,-'rӏzk:7=-0HAw~?4BHnw2Y"־( [,G/B6aO;*˺;c'劜wu% bݫSl-fMNa왬4lFE^n(nŽo"hvJ!/)" DJ)z~D7 sV8g=Dv}vs-8+NKN W3_HOT+g`͚po9ŕ]]Wn9 ۡ6TN:W-P/BB) w9>-kD\#b{[;Ev4%'>vգɒ/" G"gjPe&lC)}:*rjQu>;FQ8ЧrdbޕŀjqwD̶y۠Tg)نfs\k;iCt9񀲛^MNdn=ƥͪF6H!.fj.w*T jnţtYETXu^817 aHV6uSK"ӆ7bǬA95Cg" ˃esq3?͓BWƐ /n9'֭EV42t6C?-V*3' yZk?h.r:h[e8/KR뻝zù6E=֓ SKAa@RߞM_~6siMI_-AN7hꦯ=sEc];T;$нnjarz cg̡ed,LY8(c72w ] YUq|{7Ά+|b$21<*)+6İlݾ 3wYNA(qC]uX!iG=\B񝷗ˉs~#MNw*yV]sA?/?'`.mwO] H?H*y8, gwܷp<˜oPd2~v% z*+Ontێ{lomSSނ{|Fw ?YyKQ<zg^ymIBczRF:"ڥ"2t^}dE_ֻ#;Զd"E?95:"iq#?_&C_bIovD0q^G rF$~;/Au|r|>(tDf?}z1iK!*2X鞟: zZF;@ᰇ>Evw qB,wmRĢ}GN#Gj0)W|8kSi.]D}V4i?A$. oU;}Z!RόJTRL;sD6؎c ^3Q"WnT_ہ=!zϑj2d X:=)OIJoG8~0; p71%nɴia2BN#N{ R `37V@TIWVB#S"Ȗ1avo<ʴ{podةwQMu V'~#DLwYXJP#f2z7⛟<]}CAr-{,9NU^fg$(D-KzGqywK-=[^7{O/Ga=K}|Yvc('q﷫F?sĂ$b<;,ܽľwWFɠ5 /3? +=4e-Ŀ. yߞc0>O^Ȗ[^#*tۡ-m\{lh؞\Uhry+qS_^U2lS!9`P ] =װK|)} OTw? [dyRO!c\hǙUZs^Y{Of<q3+@TՙzO͍&+l^K=聱9r|WaKnM>^Z?c2#YeĹ9:Vދ=R̮~b^W:üFoIj^Oa9ԮA&){ccnok]Bo%@>H醈JFqhX;%/ѰnǕ;N(SgHX[vH"×6#(DQO=NmVFi;YG?Ub$HeK?׿)<-{'sMx|^(ےI<^=O>ǚx>|\i l;_v.]lH*zym)+N؎V6:oV>)9*w:A?qbz.;~[Z. |ῡw.K2I|C51s_Mn*rV2!ntpY.b?4M~ ty=^F׻H1rESEWα* ;M;g.ZN[\.DΫ;rZμ=~M)8CcK3ʆ).i/ބU)Ǭi<>4"7SuiII5?\}"Jqa6=K@/ht~'/0>s:x(UƲ\Xo Dc'ftbF>;qB?Rp+rS.Oe$8A4(i9^xJ !&34d,3rcܴ0r<6Se{߿UF@DH⺠vjV87FԈۜre.S]\ 8NKps2dxRW}6sY+?M!e[.D:ڔ /*,?tԝw_sǜt=֠Fh_8XQ)+;ND`OʐSH'yH\r~a>3U+e4SY#jJqvy jr~ rɤO~ӌiܪ-7oߎm䦽m6m&|#]È35v@v@) oKz$F#EqrCq I?&ϧ=px^weˀ~µǤr# +䪞B7tx~2'ޘnD")l7Qo ŬuRYaaⳭ [j>R}rT*lT)T3B Ew1c_^I.A(Dw1R(kfJ/n4x9Dڼ=KN:u[;?ɈKxx $-Eˌ`D`qeY.xFYpStQSx'ΤH eqjDxuGE;^ =~ v!`{Gn:nw97M<H?*9[pkXJo)hRXL*z`#;뵰OkEwՙQηx* déѱ|"yLTF(ˎt#?ZcX柔F&2.=T0A̐NwJd.vyl~tXj-YqEO~_gOşoT G q.f$RrK9ղ&ZI͏Y0+rN.$dD%t^4RNMo3MOqZ~.ҹ" n/# }84WʢL961v ;n:d( X& 2[NN4N_~RwH׿9Xv/2G:Xa)Hb8Bfe- \j޿>KʱSGxǎ<7#²\+ט"Έߝ)[=#pA|PO흻P0܍7K.:kd/#x8ܼߓN6_Q {U3W8ƋҴóTNB?U)@59;|j;+ v&%2"ҧ>टb!,O8\%6ev\Ho+X'QyvlZ.F˕I)2פ1ǝc~H%w&rU6N;"yՈeD]WJ>_򸒟xSdzv"_CUq9`݆fTK o[vɆ }:N8|]S~27;:y8N f {hB !,7K4.S%-el*G$W=Er4wRIcB ,4̛?r;%YJOPqDž ?kG~9 BEY7ip豉 P`Աuzs,//R:-0pC?o 3]]\c|,\j'&KRS/{Oz|>PaIG })l麨~x)eY:Fҏ$k8 3B7Uo{nN7sc%"'j) sC˙zzz}h窂Ʒk}J }Sfec(qy=`f6vɻ- "׆kӦW |>t>Ƥ~.JaFUy zGNceϿ6рcO{ yϾ~!n4(63y>~8.@},Dg܌} 6 *'TݥM Z9]=37(4쬌6P_w _`Bao_LD'-˩oⶏЮg2]YtvG$qk_:Iԍ)|02/ix*v-:x_n[z){2 2@Q<Fq ̷=?| (e!M}4$ΈtݮZ 4Vb:"SvvۥRNx5E/e鏞Vz۝YsH1X/{78׃cL GdKCݡ~O4mf"%Κ3c{v#tS@ Jç ʠiZ_z\4 ?xGv=vwkr X`x8^&###HoS_fp%߬jي&#DݍjZ}FOs|V!.fvs蓎SIt,#R#PiY.7z;S5YuglRHZ!tpïv5R>..r7;@GJa"כjԡ8X d4)yz@ov}{).D>B* ,$-TY!,#?9݌jy$kQS,g,2LxʶuAHkdF޷7P$ʠUrP+AN1Sb-zvFhEt'(nZrO[蜙'V+\JR(gN.ڜ8Z2;&^}|܀/yNtU)e}BЧѯ ;7eE&GA#Ɏ)j k 9~sI$Ϋ|ӦyҜyyNwo1_0+~c_ܐí<D<X0A+FħlcGkfa!}2r^z|?naɑ< Nwlh'cbdl˕]_@w&.|%k^tȿL>X+ $`:PC@4dwcKߣ* Z YF)'S*'wGn)tf4׏ӓ+K?@e>\]@H#O4[dC%IXBr$`̈$fZ\lg}#gv(L;jKEݾ{;G%I: GĸόeS;7?dG ^mbҖ;;FGe=a IXGf5 Up(r|.:``+1F(GWQ0OEߴzmǤ8-;Ep~ 'Ў{RT3JZd{Rl;s F$9"嶴9magf"/UW0qP{5S->F{h.*!KwE0_kn@N"ZT:/?kzSmGLh/C d)v|28)Qܘ[gk0=8je^zqD/MpX4&\Drx7J:1i>zz.+w;{6{D]*Yh<W ~gr]sP,XāEC~?]ܲ~7:c些fҺxY 37DйӷycD"zOZryr~lfS5[Y}.:h68 wtCm׫^]ٵ?X*r&W[Za:OnL#?+R Q2SN9ۘZ:c.J_$\YyQTi/W6N9v3%W >͋osߓU헩Kh)<]e7uv?L9޼ ےN|`KN]R&#NXAF$ WfSD_!>Ar`oKO@~Ov)5!D؂wK?pvWy'PcW!qT!_HL}_p.'cMֳSjNCz|%@ȋfqÇFlZWRoѕ4!rv,kˀL~l+z")k al ّG; NQy~Ӂ錔;3ͳtFv_w(GBUt^4J.GnIXlx8Q@[)Gs+l bҦY4H{qJ/E> y6,NU%G8vYd))ݷ]܄m5Fk3D:\d-efwם~3~RH ,tl3sbl)'XW) !odξez܁< P*:-4bs‘FKIɭB=HdY|,یSWzN1{" ;w#b&3 \wH(| }.܏yt?}Xa/okMix!@\tO|}]nsSS 3575( 7mDc=6[OGFU'aMuU}~Y*wXY[7"ogp&T{JSd.EXsTQ:_(!:1USXx;^^ܤTczܑ q'J9̶ȓ67>y2̟ -\Ez$CĐtym+\׌.S)^m;ր^F/N [?(.\x{yw{ :u'y`(UQ}:\$?N fKʏdmќL畸}P ,N|9֓>vKnI_,P0T];5}W+jw{`4g-a(#cq ט;p?I>32%풋(&Sg^{t]sM|xptHgۧH4o7<8\e KXsceINIcxs]'^=cy:LK5_v+rOς_['v[m۷.niTaV#逆ѪJ׈}f8y-#/T^8@P$}gr/^ IqA) PR `kQv4Hڵ(eV cEb+k1Ptx6Qy^)TEYTQ>*p+K@ :B _'}}>Uv fin4fDњ1O-g[|+"(rK85P~yWS+ "e'bdߒ,^r//'XE N4HtRz{g㊢zD|qn}>4 LF );N"츰rݔ5/Wίfd[ aN! ".wb9vs+NTAW)cz2> 'bbT2zD+xudSH Ћ{*x~cq5?M(+4J?E@Z 7& cydOpGv*6I$/[N>\&%Ս4Pdˬ0-^7;3sr(q.8"W ?OO9g3S`\Ms(>rZɏ~c,_DW?v6B _|' DJwzjU咂7tIŃuyX_Okn:Ъ9'dD.Q13rQ'`+rY?$ڡ7d:VBI؏Nﳜu "O'$oۑYI&i+$umCP3ØFAgz s0J?[SJb1-.`IǛ)˂s]l{"`$4:ZٽR=sߍH? )n/KCTڡ+yWz,ptb>"IL /`Xi,J'W"fB+&OtCRJe^=>l| b"Q^}t'ܐBSW`in7 ~!'";cn˷'^S("?pE*Z72_0Lm9Q'7]bg0!c77-^/G<ƱCj]oݡ6za r{\#=:OeeN=UTz3"dT vCOEN"7#ن"c" cgmNr?Yߌn"-V䘗 mgwIsEZ/oCD.I_7 f=r><MW9"zZ%UJE}/B;*BJEoI~^^uGmyN}x%C@O~Y;Wa{~Kf\/zv=wW{wlUe;q \<|4p_wШx2 [?w0_v/g N}艴+'ӟɡ߻>cW Ң]"fӮUx˟3eqM(mjMk+Kearb|g}q;IW n8F-ηBV\@O LmTamc*j%΂ (#|'%98"qK"p:a22҇vbNGF iDHEXˎH'"6>~~Z)#xFȰ- 0ӁKdHᗄL k93ыL۰lKy˶Crv"45* w)6" C5SGZp,@R8_1NռO+;g׵o˅%7N!Gt6}b \lcɟ([[Nj C})I@֤r\H+)\syFn~9#):;= L)Ǡ qj7T,E@5򩧐۩\LaDU}q\[ya6o3j9ECoa ؤ%)_vnr=_5"B8ۑFX39}KKMY_@ 8B3e,7}'A) 6wִ~ɧڼ=1hg*.VIgLO΢L8|&Ϳ+U#e_sTYOi8r?XtDS8|TmXf/fj(9I9=0 ázGnFw^uP_Z2GɎ^{o`>_M864{om6lx{m.Ұ뫢r>GiMDUf^Ar}D?S}IHXMJbO'7s}ߴG9Ԟ:oh4O]Pڝ\r|>*5[nE10l=̶@SgM$?K)!ʏu_R {~ȫm$m?avg砙7rm]3<6F<ƹh5ejJ=VCSP,g%E>NHFuN/q#bKR!DZ)9H)?3gN'f,S-+PYa|AYŲ3շ' r S.\_q7BX <&nUh2?.:{D}``Z ,%v1a9܄. ޡ ".}3.qc0"֧-tkx0uC}/_0וYzKoKsc݉D%vL{%>~٘@ld"4_)=g1Ur?gUs&uT\7`YIs sWϝ r>yы^Oѣƀ>dN׎M!8DqUr *cR?aL"/-YMIxrf c/=2zNqjnY*/%)Nt΅?7Qȃq)l'-=dv3SfArquGdUѨc$Ψ4c.׫PLf 7Zi>eey֢]?ܗȹlLU)OK8y`>.S~>)n` h[S3cY\Z칍B ׈+B&,`BǺ{ լ=nM9 Q9^/ HŮ:UPܮ9'/n }eF#Բ!ywI#Eg ]vZ9]Iy7T$$}yPD4og튥~DO,ap> ?5ir?)w=Ҙ#37g-UU_DyB㏩2_04/1^3?0U)8j~,F7oYDDqoCi3uE: FJ]7@5oeR=/It 3#0݇A:js߯9 r QZN>@ƒ.ƺR~Vѐ0/T$9/8픣z%qch{-[0nz9O"Bh Yiv)39X$;#9|&XPӲa7 f$Xf"m=wE'60C6qP[=3*nQ>Ց0~q}BxFݪKR '̷o0 N^B v\0bӗ/T<\kOL>J# <>=7ɾK_blGukEET2plP.kv$Ԩi95 3J- ϝ.٩.<`O.ob I_uy gS =8#/2R(wxnV{N| JZ|CL^i̭?-l;H{gQo]:i]& kj"@F "8-PфދeſgosZ(R+F׈j"]#\'O^00:#A>dfO8,6K⑫WHPzP6uw{J٣ qHÈs?lm郇<5kqD{r oƆFɯ0858; Ex@0pj{4.I1YӦǎMy;!nwяo|ovlpc籗uK~06&O)4\5iWps"(ϫ#GlTKN7s~dOY "r?z#|U(ӄa*EZdÁ*N/{}bdv,7$3QoN?Gdǘ *rg_ED H\ghRRφvۿ/yWHzWZn:Bxi9hSʭxܦͮ]jQa,z3{<@AXr| "l{?݇b/)[DCmКܨmQ[MCI'=Lv,`ڟ+ a;\hmKrd!T :g^sT{Þ^ԋ⭑|R A=p=z?W~z' %lS\4ajzɍEZrG'#ҐgO*߫ao*N0Ir?O(Ja=*!f〒ѹ _j oOtMDmOFAj|3~ErPgbV~V p% ^d9 wɬ/T ]Z>fgG86;^N>) ש&5>|wJ鷁Xʄ7}*.a΋C*5;oCNm !H"1+ۖ͞TLii[]$s vY[ *C!GVg`گ dRᆒĊ~P@}0{ȳ6R 1=>p⺳vFyE+ǢFIpj>|f9\Kc9XcNf!bĜz|b|ZwFGq2Gk*|>6_m40C܅NEnM#pgyGmmm2 -OMqay>_x"ј)z lx ?CG^{/^b_]^czC?˔s!RelU'R_4E8`7ơxѢhTg!Kqz J# UeQw:On0fkz+'؜ smyA5 WgB^쇋e3)6y`M|#)[Oн c))s<*< :\Eϳ+R[Gcr]n3%+?9X& f|駷 QԑT5AvYb",bu#:HK3=Mx6lsৈf.D9Տ:sǿHz^P.xj*qh1Uh]@7V|,P*"ɝ1:9EJ.]b>8AѮ;1`S@J_F KU:ѵߧ,Q d_֣\p@"Sܜ}WC,wB\OkQkeԺ/39(-F5"穤|FBKיc*DnN"ΥG|Wr;l9G9(?.䠣M#j?9!ޅ⼢jԂ_X.hz&a#b<$]߀̏)u̳F,?x>9_yO؟ǫwrC xl"Ɏ9tQ-R1D}谻g" oT4N (l%jmC|NogQmsUB|8]'n;rZ6i:؟_*KjI!ds_hP=n9S'ɓ"_C5] o$&}a Tmw&l;'cH@*-#e{=0vŤiHzhqY" ǖ~C=BPĐ[|l\蟉Znsa4/XRYb+~h}x¾94%þ>!͑F'?\ {2KmB}iuw]/mt8RmPM'a|l6(\s6%Uz7W،7`9F*Yw~ OV4pq/hӼb tJVaQ_Pj̋Wzoim̓{TpwʪJ4-fNa5ɯ]4BnkX:O*D# #jc[)zn-M|9 C4j"ۗbgmdH qP5EIREMɑ2Y=[Hk:F`' } ;8Æ\X9M(z6 gؠ PXV*)Ã~#lyш~S5|Nk;;ԧ2SMAUD,AuC'Fav&,LZR+$[ "VQdo9 W[ m5[/zAO-*0N{C̺ tx"[#:^e4pJ:ߛF@k& ~sI"I+wȁ1E!#B@r] S(7a,4;2vIO [#i8J \Y ) !Jn\L恇˿oP340:,ܝlKxv;\iftEkh[+D4˱#>4 Qd-C};ad/:l912#8OЩ8ͪZWÚʄ$Lľy[zt&ބ/Dzٻ#tՒ4G*EoӋ4c|RyL! ;S\%3w{S+n J|%>ę#2?%,"+TV$ۡ_-U0zV*ſ飞sUIiUd}&wD/ܩ:*\ruqKVY"nN{8SKm+*G?T^MtOSrg8_?~ݺiTӦ/ 4,cxdX9NOعK.3.xx!6 D_z7D|]8.iqwA\Bj-K jg9_]r=.H]rkj6ms7BG7K]:I6|mǚyyWY,"+͏Cv(`3`DTxw{bo{yԽv 䢮5=g^s||!o,OIcV@ϥh^cYU=S!>~~0Q@i)AA/?e#QNX-y<{(rj j/ Y É8 bj5b[c ̄ΎĿ7kNhk=hWeF]faG6=Z.)#)"5AlF/sYWԯv0qO-qjTTs|Tm {57n&){, 2-H40Pt\ש,r0 t-APkkfM+uԴ8Iה1?gJʴoُY]9y{zM}#N-{D _׊<ׅ/3KlȹbJZ~y0"D]'2%uexZ^I t  _[Yv-*@h\_p]X"=O<uKBN2@y'/rp̳9M<#GlI\iXD:L_گ/@Wq_uHۣ%=eD,8I0ߨo+MǴ}RTܹSj)E,) ".d/0`Ur}hV#gu~꒺[e YT Esj ;ww><٩]T"G? `N\_FW|`1\<s#̵."c誑aGn&E)[pՎv3s2\ݩgڏd-=m/S}G?Gno;]j30Hnc&)@@x(NeSD7S1vmR $ʀmGwS+c{{rCuLrDNQ}gtL#jCr`8q8#mTi`3wߋG.m|R#?NR*'[{Du8Y)ԖK=tw Jo ĦT'7>RQ{-%qJc+Ld^16PGI]^ӝUoG5v)(.r h?mb!to,PWrbu[a N(G6QE޹O*;~44RKJ7'urߵ劅H)R Jʞ.WLNqv:O U1墟+~7 b2MI,5{='_~TuFD޳Q-?'>4{Gw P|ks89W{k7<7lUt8,E[ud?Պǝ/VB -d\7f`IϏyF} 3eWwYO^u1p)^j>U'F~oRo>a`}+_/[~yQ`\%)WƒԹ5ߺD=?P%F $k"?k׽.̸b]5#Ice;$ssQ$|깄є'ji!].Xy@.h$GY9D/T#G>>PW:v̸Q[Ux͗eD  $t:. }kgc[7}]y!3/3Ҩj-(q%~"X3?߫t ܬBІN)x:.~20h@-@rxX]Znh2ߏ ?W6VNa[Bgx]HdqǦlq2_$lEV[6h?_۳yyK=}/Xϖ^/rdJTت /ɢPr;d%10}SCc0XK"G(5䡣~"nj-`݆W פ@˂ o C>ugƥ?3f]ca 8s;sM6eSzqCZ, x7ۂI,D>,r^eGUJԓ +"Jk %YHe=+rٍ}rVXaFV)?1Dd(.qZNmD@wy "nH8"q7q>)*hjK{ncyd䨩9'%]] *>~v#Q:TU L\-/ųy)hn rݟU v~G sW+sov}т*[ȷ= D˜.JHs~k p&Ydd~Q&ʑWrH#=N3^cH (xYy {ͨ/7;UҌn$c3M:WOed7:΄H|X$/>rFù 9e?ף5*;)kd=×]lR/ha/ P_jd׋7HrYa_7Et #Cm v[mCp({FZfv@ Y/+<B rqU]:ȳǫOnߋI_/\еPʢS6dͲ ]fa\]q t>hŞ^MWQի8HK[u: ѝm-3bRw|X5k*s}ЩhgW+(8kfG_#/89٤.\ip*Jm0ުu15.9<3֔m?=Y YqHvZy䨫_QcgÕ}<'W'/묬n8qAk㱃-<ɤc#j}dDOi|[] s(}.YBbҀ՟o.ɉY*DPWwRq9,;ZGXEx=w3~scJ7%utAj|QrV֊z="p0c FEEjr=JQ\S(2`I|m_m\ Z#mRCjQtDǚr]eIy=˦RDG=IQ^]KU=S[H-^GG\T|J9kŷ8lFb06(gΩߝq%F\i`,"w,A>?q9ig!i2ZLCr!p54t: w9OU\|{?09ӦI϶kk1>~xO7E Wځu#d ʟgLd7)ĎsJOE]nzJ|8ɩ iV\+cv!&& 935AUWؽ^b2`5]z4uB@ڶ|eE GaR[]$re}5QX6f`D5#}9ul>nc_} Xrd^"'zx4jNt^4{1170\r=Vxv\ "ٳ[u[qRGd؁{/7v׼e͗7a/$%rAn.܌"\k+p%Ely8u5"䏣5Y <_ȱg9vgOG#Xߚjgr@.ެǏO;E|JZ" |T=ALxeBHX}=O0\:`F a.{|{^ m?`>W+ۮczޮ~#xCxAf).7gp{կH yڿя_'p@A#}M`9,E?P}9]ݯ?'˱ x"GswS9/ ⳸5 {XܚEoi׻+J|dPq~U٨3?_8Ҭ^-mfr6sGUqn~za ui]>c({uиUYU`li,QSIwTt;5hn`쬠χXqwZ] @Ã.y*uk;%JA N;~uYg%v@,oLcڪJ4.]}(4On}yLI<6𿗃wɛУНNbIsfj8# ༅O_5ʑʹZ]bz93ៀ-B+_}%.I0wHNva:'t%Ud8HZ'}$cD]Ygi"Tv~Շ<{+G[#~9H &]Us y0Jh ;,n+44C4~6=˜n 7Sp̓/C JGx$|ԪtL̿Q]t7,Kpgر&}|z[oS圵g~T&@A n=t'y~g:;? K[_uy諒mTmj{``'cy/Y$frϏ.}FBSz7f:蟔woL]t^BY_a]H X?Oit #XgMW;<us2Y MJ޼#^4h-)v?j<|*;>6%i9 `EXV/zZK齒]OͥG[[O]jf\?ZnA,-Mo`qan} <4JU_P\3X+v œWkmraHֹ,pUո/Zor\"[HI1t-zȈT/Է_>iS,tZ-o~&lT]ݓ݅,)7y,Ӧ;ϫ*^*?r X#ɀđf{'rݖ6[n@**9 spzs\T/ӂZtArP\v\(<6̍_FFz rp8o"GQȹ1Aϣ=e>IvNbt#\X?E,s+)נC yǖ&zX8"!MO즔(tc$^)C2\nzz3'!vf99Ί2 | \54DdҠWߴug8ETڮ6l ~kD̡YO?A?΢~Po*>W9z\Վ2W4:>,6{5eL%'HOP&}rtvy:uֹӨ6-Ԩ}6\_B<>׌O*[6iʰ} mQq3sTv{]}:4qͦV.|o`7}I{+&cQYSn،mp q:Iv8MUd28-v=O.bs57*cf&@\)\/8ašE$ܕl{31B௟}s'ӾB-ۦK$ ->ޢ;xb؏ !e4sGT= 3Ȟm=P|\?7>p?x%^x33yн`qJq  ,yρ*?owI:+@?LzvcFf̈sƿZd!"4*>}feLʃo>zu'Yr֛{-0uPHNuFoew]o@+5 rw*דZ;g"kXdf;;O5ZE7WSG]UV@$L~|Xx('-T>L`%) 0鮆w?]$" G-ԛv{8幢(6BӬm-FG]/}4܎"5Q}s[~KyL ?wy=U*5}x_3C~K{f7KD PfK'\[Su&٫a*"{z:2e &Al:xA~r 3QSg=6d@{4FjҫCOQ7lt+T+Oik4o8)`HZY%ݳ3`2*~/{`jRP'vL.v_I|RT˵=Н(O ;w'S^R컵>6$eXL}-[?H*m6[ D2QqjZtJ*'O"S!L|gw+LyNc-:*ttp5όҒ6h⯗_tєsy>=4zPv̊ ΆMMYi&'QN"`h9E'م-g KkOllSFyZ\*~%x[jFɁQ}ӯ$JL_[9Cb끇b=VtKѲYT\N/1m?*8'DXESo?[\uḧ\z?rlaOԧ p9>Hf5 mXo4v֟Y}=MW#eAѱD;otNt,2r6YxxQ/ӏnj<۱劣w>N}>`^n?+1vN:1\ɷel!BS\VДh "gڥ |y.<mqCj!uV*0zT|^+LS8YTYw olvV\%GGXf9r%#L!|~6VS{An3!?.`f|FTl<`,^vЋI~q&,Gϋ~) hݩ{*t5C Q#Lr);k6{7u@k;|rL=`Yn($isSثr tʵ#pqmaůS1vk hdK,WeoXdvs?4Hn^ڹRfc%s =]#KLY*pч~!bB~򙧖܁>@MY6Y!+q?>W9"U"v}<~g^iS3xgӲqzK$ɡy@Em"ZwTj )ږ_,2z/&zz$4^yYL,27o6Slg v;|mB϶.5.5qu.pLt>Dn͔ %Nu8Ur">)t  gj½óD.Y#4,mxߕ_?iiz)_;BkR[R G0ktWڜ.2F}5>'Gλtt;F~>4sN~ a\ a.<&g)bz_<)Ј(~+[\Bit@n?W #O^\KƑ,NݖH_{WijgJSKCvARԠzf~ Imb͛} S? r_b:[]!s# ,; *l\+KY"VSӱyƯ9^*8˓sp 78Jo6ev$娺W^%1qZ-\HI^s臨*ÅFLFܿIPj=€D˱n$c3Y_~mM'4c#hh=^y5SЭ?yϩo+J ͅƦC>vS'ͮ].{1F^duE;v{VtkQKy%do݄*qu[ɨݠv'9GbD}wT;q:"~7Wuѥ*:;cѺBD!R%8 /zg֗`HȫTYR{¿WD8E Vph79X|sO$9H6颅y$>*?=ۚکLGpFUFw;NϜJNDϺ=Y-0?tE_{l:Nc9Wc(h(؉k.s r|~b;SxF=$!'W <A:`_Zϒk\Z[tlK;mȝL?\ֵdwMv(\ˋGf^NƧ"F,9߯dy43D"&FifŶżT{9^5 S I{"k;Hg#"/vuA$͢yNψ]u:>gC`m# ȿ}6!Xj%זfG -E1#yjCN尩`ĎE H8 zؓ+s\Ý^oE<1o\szmzIxjqu`gdTmH6%Z`R'*y o׀;"DL$ʼnüFB<#o-粀vxO?X$fXσ ɿl~VRZO\\ej+V ^@B!ץVZ$sϑn<4~+Ta--N1s-cxA0ٝ 22r.[YO?xY-9+ n7>l1m<'ԕ:oLΊ䚦v=X G;)oܔko\^Q}mD=yK+z {mFW\:|{j^ =?ګ ٩ȒhNJ b{ȬcL?T S|?edk SOu'Z%-fMO+"7[<"j*=]oq=r w;\-`[VZ3h23_wПkD^-C:!Frb='F8O~+~2v\Nu a`@)Dl~^o]7ݵc\~ni7.,E/BtB\BW?ƣ \Oq,N [k'];Ci?p; 7OQ~FpPO띨1i&8=?LZgWk85aT pE@GS2oim4ZZr1zSB13B JusrUzp!/.^~C*Hk2~ arğ:_VrNaˮPN;<tQf]KLJzjowF(svy;q]_:pf^5->7!Q}"[S ce缕G;#(\zbnlX++\?.7#r|=1ݘ2g+j5,k䰥-{ `]W7ݏ/Jrip!/ln鈟g'h 5 \YU t~ LR%᥆i[HDH>X,M]I& k..r,UD.Ww$3C6ﳟ Iy/RsM"ob-;?~@\r52xf+T߼L>!'49_\,'%ZBOm0;"ݦK?2Qm =mWwD^,)W>u:N7IgT\˥9+YxS\x9-hY1s4m㳒ZԘl]7=<"p>"q_""%{pu]Zߏrop>v:{iUֶĒ=(~QI:cA=,y-f܅]9{Fw/{ U˭?9`jwק%sXiVF*s{7͈؆#GީGgqqy<)|._z|˃7̋oGxEۣ\4k̵au }Snׇu˅(=ڛ 2ukI~u׌7Ό@')A.?x|׺́7m$E2E|ފjQoEI=3ř6FQ.vi=? JO6}{܂ⶃfύDN7"gRKC\{鳅ě@WA<97F{rqqnTld)RhcgAt#21DpP 1'Hm?g#.IRxI/ԕ4ui82t1+=pgx;=<,7bF'O^8% 6B~鱏H{NI*qcG?S`y*3%aK!ek/ﭓ DB'FMzV&©K=\1x^_ˍeklx}S0f0dIx [MQo9'8yYs!]aÿS0Tl*ժR%돁Jݍ;57S yz1t{]Va9 >`Ŀ~is]uFnXĐ{a2BE\7xW K.4Lb&~; |;q ov:-(ݒ8륹xgɥJo>jW\[]FlS|%P0˔p*~cȫ_ZM!V~%Fay?HֱvֱSVs|1L-G py _ىڮ%`TcK/™V#NᛗZ@'sxF.IVf=}ױ䆉jĽ:-a:9J4s%LUGݑOg[UNnU驱5XA_8mWmÈqLF!{:6AKwʵ-;׋"?'nw]:ӵ4R>.%PsH#f4z }'+l\SsBhlU.\>8zA8`Շel I1FU'(T{)5P;ǨoT?TG)s=!0Ri6jΚ-HҚ_ٔi.hCZy[)~1*)_FjR- rm'C_i`. uo-vpO|oRFoo T~L?"DJJz:}~87~TG=&9iW BY>B3޴<_.5B$ǭT<_! 2ֽS^S/cbի?"F0f-Zw Kg ~ukqk wP#b_4JHTpUqajA.z]PϾJEnS1D ދ3?p <ط xfnM1"g3NKmzz)\9Gv=_8JwNqKOr\ㆃk?|ݺR=Pʅuo:I u ֭J(m&ӥSЊ=6h K;WpuS/qk@ )GiM];aFE?#dE~wt]j=D6ߕ~wNG R'dGtTx [\bC#9Dk돟1V]EIk)%rTe,+V5Ч<`Ew\->;QfkW%3Y7]ʏq!/8b7 w9[,.N\]c(9/:컩 +Jͳ'(8qw@vM2*9U]W;.Տm>E)I%oO]lƕbw=-OsPݿ?us,S'Ks+Gtg-"wMpIǸF2Zw]tX84“Peِ$Ek7. 3+Z>kJk cJoQF^)Q;UP Go|cD$!G!qNĿFx\6 ~dt|- yA{3 )#|' &n2qt!*0ND% l.X6[vo%5sX|BU+acL+~ooq| FJWL>; 38YyCkn-[R&r~4=>Am9<|kF`nǐ 5 lJu'ܰE_Wk!x/\.-O_MŰAW,TX">V*PRO]s_#xZfؽ Uh^SwSӧT |1"WgK=es"h|;䮔ӪFϭg7A+<f 3.Z& ȱh=DŽ=lp৹b$fmiy5SPovՠsBD(s첶bq[/6gõَ}uL;b9Y3\k xaALEm>qHeÑ^(8 (1Fpp 24K1 "]efsEiuB],+lu|!?>*L^T|ӋNE34S_I_džAGk-ϴTBwU|3y26ñ|Ϸt-:WzSc={!InAWýG`H(*Sy[I`SP} r#5~y6Bjqdy6kc}_,L {-/zwͪDaG-Dz#9ㄬ ŸUsnHJȫYy9';I`vL@E'4J.!d>m޹W-90R2h~iJK> qFgavS2_X䤒_~W&638G( [iRewBzZ:Oga '77F, ѱOʲ|\#R%pPD [פ܋|m}n9g=WC 4$+Ticޮ}>7t!<_3~kj,-=j[ŧ, R_Z78zꯟnZL$7 %_ c7@5(c7Ee,%=Y@u,~8ZBzx2-t[?53"|ڋ쮮jĐէ^v<F4oŚԴ g [xoFppS[ZBND/6Zl44LIO:pv]r=ex7zP;WD&8%r$5MɰpjC$G[xxbak3NoOek"N1Hs ViaE! >6tZ"PC m/C=}nzC@̀4ΐZwÜ hN8 SvQô+<ۊ&) Z#6qCNf3׈UEx=diT5!'-҈Ƥ{~B4"o1cqpI#G+dl){BU$3INUQ#).zsE@Ov9L&i8QNCr+H)%ׄx>dHϨu"Ś;jSML"N;)K8v obC}i9#fZߖ yXaU<\r n?裏EPfe0 /N.9>ڭo]>{\ dQ=6g]hQԈH= %Y!a3w;D|ڋymc!7c&qBwYFfȶ/ N!w|-n`{FBN!Ǖ4 ^(pg&wwok ^>qxl98cGe((t(6\~{ZV&löDE$L  E:vQ> ]UsF#r]hW٫3n|(7GzH4rg[ko8nyL-5̕'#3eML +ׅK_vdʘ(47K+uwxf8,w$zqyf\ _#>w2eOMӚ$陔ҒU02iB׊}I@[H 7.4oC$ԔKd(q_($жm"Yg? ]UD9N?)lN[g"`g =N3v;# /vJҬds3UOW$Z93eݞjƺQvlE{Eb[_ "[̘; ,pkv|XDԭ n{&3U؜ܫ*勒D$fgo zݛ#W,uj7Ee*W GCDgCȫzyu$8E_7(gó"guE2DB~n=E\CF* ^1 Q(:gnNHC{w;8in\?k_W_ʼu:LF+.3DY͘ ), RGRkĥwl$k{f̽a˅ |+~;X9ǔ^L%ro8uxug鴤kADpE:휗ǷR}ߊ߂Nbz xD9ӥDSv@Xs\e,M/OVz_~%\+N҅j \9~.&D4Z ,yeG!~/v\dRr6s9ɍ 1wԹVa~{JnOw\A.7lE-9S$I& lOU1 uFkLhW `fגSnza/p4GJF^nYקOs{DfYM-i5Ji^*Hz\R"hSr`"j[]؟K#ْ||ÖmgӍ {0֪FA>`ϚV|@/M!CgkWf-ȿ寚Rȓ"rH6K$Om)o8o dR47k94KŀUǚz88\3]]n.vh%*on[fo5 wr4W՝lMI}u{}x޶puc-IHJ/P\O 9-zVXsWI_ǯ%ruTubBRP=_lL-fF9 ֈ79|]Oǖ4b×Z|.gJSծ}։Q1 ͯ)єѬ65\v@Jh^#/ة 8C*dJ-=T8 j 0Ydf(@8VȞk)Lh?rK]%م++"SSlw~ jNo} & &m sZ|#>DOcE̗WǵWQff6{yi3#K| V#*kJ7g!lQl9֘tYk<t) C2'⮃NF\kR)b)}Jۥ6F .)_Z= *\OZ?F,Էk:5}/wc]ev pVς+mIt8rDd54B݋2go@廦.@}~:\ص6Wm:&1?|iYޣ|+p>QNo7(w![YS(hPIs/@aX\sl[.?P@sY%ԯ{mH7DJ߂H7-os9|Kd3D#6š-Fi&`Va4%vCGfEb%yk9mӉjN-&['ku=!]yС&MrÂ+ԭٮTx_u c]13yB`df 'v"q$0N\ .#rǨ[{a*5'r c'Wz6z 8O:y™~LNTbH-zf;yVi>yNc~k/* }9= ?5¦KLZ^u !08U-Tq!/Lv6P0*NK3]pexm-L)!W?? C~Le||R=[oiz:wY6+!O?P? ,&:6[g壋S3`F8(>Ůn$:w ]l_%v{rMM8?(J_$*7TalR&p|?D碗Z&t]FY,nj+U nq&i*\08Uՠm ۪_7"6@96-5.Lj/W|N;8\"#S?$: 9:8,lUSwi{b[B5FjvYGɸ5 AxQ!n<;ڛYOa~! 0Rsw>Aζ :ܞ ܋ $^o=s:xr""%gO2af|k#uG$T r:qX>!紼<W>(M_ov^&%y-dG c<|S;S~bWI;gLdz҈vg'H&;=Id7Kk;mʽM6$$M_!]XgC*͎-  } CuLmݳN8^ycDTP2p+1 HTfTFhGs/v^-MZ$kS?va. *}c4Bsc}'N/0F:dnܒ0o"L8XzʋwE0fAg*r>JMQ?orդw^siFe*9؆bi^W4PlEt6X_5m5bzK 1rCμ룪.X@(;5_O%N~">ݶ>h$*BJΧ \o(d\~"1ڜtFb&kءOz3ϡ<"LqaN.\N4D-j7t>(]R-PjdNHfuZ͍5)@%rN%"q h |Ԟ $IPx.ݦ=I)ˢt8=]8YKq`6)8ánܓ A(O֭X~b$\T3~ ~\p;c5"p.!6-p\ڿS.,93I=4z?@LʪkVr"\ [N?k/v/0X*?ũce!4W͵Vt}:NcKk\hD"T 85ahӃSQ6vBeA ΞG0꓀\欧̏ BDF;, QZ弛;xF-|e}&%W` J=~WBVRDQzb-mj#>_l<'핞ڭfqq54-[OqCU<лuR_+Z}lQh -7J1u" 2WQ`Ǧ՛U3]]^3#MA+\j'\sOy{ўM hƱ]~W]7ԭ1|#0cu110"~ankS#}ܗ [yft}sVe ⪅wf/ Շ2Rlw8 S#>1O'b@'/;%Rr z7~}+[\Ų\"qGub@HT&k!z&w鯋7׭ nfUj%UF>+x#zXfh?7=۲,jEvQ!o -U(6 qѽ{}5d{"N~z}TOpCiD[uO?/>`Ǽ:5{>^l VǀCdժIa#127D;v.zUG`)n1xn3^qe}ۙH@~P&#} Xm3k8N .4(OZj,w$O=HHɚ+iu?u 563q M#>phKe Ǡ9V\|kK@@Z܎-'+PZ ]*QBa6}+T*#\Dz+mXNiے8- G>+QL3Nq'@]N纵-G}MjhB-o8ZO7D&kڽZvTd0;e.<'DNSɩ{ST ;6C6PT#Ҁb 횓iu@@ֺ_u^u0IgO\?7PM]2@E&h o޼Bls \ Na1y\ F.+"ᵶj+6WY*~=5w+5#".E„\Wc,.9+^}AU\Ǻ4=I t,glˀ,%1"+,=m!+Bڎ$#JOzD}|j-?G=|Iǀ܌z C2!Vab7>0oPυh%0 apA*o/-i_3t3vk)ތlYG[Cxy0o ~hmax>۲G v<_Ȱ 9A5&DΠuZsM"ԥfF ~٤U?5,6]˳Ps%~1=Um !E)fa S@/]7.ȅ[b2J-DG|@O]X{OO'fkVBht60x|e"Tbj/|Fd%.l].Afdu(!j^-gyUPPs͞GyRWc÷8Ke;|P;=lݕFP񓖭;zd͑ߢm8!V̘KYS ]jO}~C$Y/F>'\}<BH$eN ΩuPd)y&opܿ_˽R^Cs X8[Abٮ9W sO O/*WWyK@Y_p{MY^&~^"͢몮L1HmQQ3ma*|c D`eY`;ԜNp@pPOd²7.אEW]CP] u++qCdN8>ƽ>3Nd+X=Uz\yQER?X*Ꮃ'{n禡#qsC]{AݨncP[ˉ\1Ep*O=iYeWDb*]/usBd% b΃d]8r#rOEhAwZǁg*jM*πetx9 ˱ۡdyA*O푶=]҂KT,ɫSl|y)'!֜{i3T^zTG++,K+I*cl YFMڞɸULjiv=p}Z6͈8I{4wGO۫Xv9,E;vW *>iPeϖ֮=9x]Sw6CkO@{4G/4Xaak<.y w^EkǶkGsj"ؽ>|u؉&" dOٍ{khGk^ʏ8P}SyesÀ\ik?qEk{L- _ZFt$XHå,]-ӟT[HϠhyh.'֢)v.V:_u'4x#դH+vuƚB%Q}=5'cٺ4T]&ڍ8}&#I>|PQ˯e.N&>M@##C q~}]Ңȩ tX4+ij\:z:h5Wqz5 g0Dҍ̴|wBpZM /T6ADvj:s_ j;6zI@\߶Nᦢ=? NNIyGD,7b||e[ 9Dql~H NWo>^\u F"/2bo 8esU߯E]>KU="Wz7DV/zsi|}jNX~zc57]Vz!Z D wTeE} HiFp w3%xjg.Tmj7;۹vyn|x|(-"fk\v𸻁GM/Eo|iJN{L= KV384ol{0/(#t0 mh$-zzhxƷta4BEƐy%G/^`۬/M#XH@]Y|޺Xk5?C#-Cck'B̋c㉳wd[zV[s䪺#o:Rs՝V.\r;sUL,%슨N)0z#D;\Q4g?Ѝ9vbt1jK0ցZ[\oDM]uSsA/Q:\Mv mNDT%$IL%TY|Z{q O-5Hwua"WW1.T N]N8CǛ r7\xQn /S>>\kT_U6i|{`g `|ҾaE8Wk46515.}Rz^ ?ׇr*f@?cbdAMq1x8ڒd弄 .:Q__x[#>Ň7LI ;'FfG`qӭ.U70>ۅDNϚ{j༽qɆ<3{ HaE߿݋@|^_9^s$W@k> c"TiEO*;"O%=7vuQa(3^Lξ/ŀqFFhnkWuC꧶Ӂk+ mR-tŻmAU0#-wx]̢"?u2%{gs+1 ; #r Mtc!'r}C Le]rRsd+*m-,;WMF#T:%ҀzR8|-KZpgjhۧ[87Up{]RnѬh"'ӗT4-}|ē}h_ë&;q.QUƵz}%77Iz~0RV#=\O9BV7>zֽ%S} TBhk:[͗DՏ<ſOPǝ}Dzvec`@@Ts͟DHr_VU =oĭ|H{XrDYZ,XsiG˾RxwJt4ف͕ "?wv*I]CޑLBX>>IrsO%)߽Xzք(V|9Џ)2v֯m b8ʿi(Q%=\rk3E+ vZ$ !̡U0*1jymZGoA 'Z,ӱ-W`'9Kp(]q/] Q:CamϭF?I<.u Us³az| _+%lK9qӯ:"s%g0Ec&^"S+Qf! #;ӥk`I5Џs'#g0"7 tN\I+`+:Z8[aMKyH 6% )J!8:8+{jD+z WFvZ"61rF9xr+`\ 0#NS[HÕ{3 %w&/?RӿAnO\S97TS YCΨ^T9qY.`ȅvF$ k.0Dxj:Kd#/y]gk.=7/u\[iw0E8,m\@Q+t(u&-nU%rN4\Jh Ot{&Ή/葕D( -J'}>=0ОEs8:osMA\`Nz/~;GW C7.6{6f?94cSЫ@!D|@EP!Tn2ʕĴxd8h[ދ"<6"{AeN gDL39ðZsݟ tsȡXw%u'Nk(A,?.0.'DmxūPvthMӱ[1 عOyQ#3\E(Ql*ninDOSOOVw;rkGaH&)L{+3a}{5_Qd͠X:Ƒ]dT; .`Rӧ.Ҹ˯/{8 \ru>A0 ʩ'֘˺dBa %ʿ:-ߴ26~>E^5''`7>˖y$eع8UwP}6jvTIsZ'L:ϩ;xz]&{>4YL`G&{rxl`ʧk,65AUSG OvCc&b}Fp}kxY\#:VBSoא3+6y6ӓZ ɌNU~RD.}>t>f"ZnJm^>q@mv%x|2`-L?8Su_to#]aqE r;Zrl!oOe/2bG ~ QV S͢s"0}_`FҜ9?b)j|]@J-{BD߈Cp'r+ňԢlA%Y"1TkyJ$U]שK?to%ׂo;)+Zu5|Jǡ!l__4WGSF:-1dk.nV#Rfh.4IKdψPceSAw㚍;ّ۸e0r}48-R^z>'c.%*8pJi68\6ko?,?"wzV~_NE<4B9 Y~_J_qQQ}52g%ȥȿ4ڂςgꁜG_?vը~z*."󏜚[Ίc 0&o*4+=0%\+FDA{=L1| ',m/y6Z"[uD^5s@i6SoeǵK}b*?\OTc;mN1ǐާ [Ü!КpNEfz:VR60F^Dky:b]u'=;*FnY 3Qm<)6 g53o?.?HN?ZKZs4 70B#EonZLuex JFO?]v9'%wIld~\7V"'_D̋ZV?eܺZ׿r|.!iDV6]\ ~Ã邲<:H"eF]դjJ ޚ]mԎ.–VbP ȿ"G5h15אUE:TxfcR z5)O|^F~y_fu~3 MM}.$ЈS<5gO~k9k5Oȁ9Κ|2bs7}AƊVOuݠfҍAȋ挼\ʗGDBwq :~W31"iABΏ\4bִqŐҿjhGO]O[]|3BxL3rIOs5g|J}]1y}0/:yh[ DBwX <.75,|(#cAk qk uR[<<6}J_L;܏3'yO!:%ji)U֭;*ݬjs^Y։4sQ ]e:HG3GB[zR/!3=!MeYN !N^+nzl%^rٶHsfd?ڮBie׊ʤiSgxӁŽ"b#=𳮏CaJ'HTAS/iDs)G.?G,$O[N"~|D2 =Qo6Xß_T{mThlnv3n1ewm*2{I{͌:p&z|h&0bQ~´8Qq@琯:] BݧLֿ~Y7`}o2+8MygG"+vK!k6la&dpZE\ГvC-JbZ.0|39{~O_$FDT YjkE|+VǕҷ@d=i\|%e c 3EGi=K@ޮnx^rBm9ꖌG;Mo%p+5"%P~3O-um[n6YP5FNMOD G r˒0 Q#drdR<4 ȉӦt4?p+tEH9BWٯWQrwnn Z &mЋg:7luG'CC.K'M0ϟ!ԅl#nGN:>LE_#5IR**As|B oSgi$_ q7]i$B$rM4Ҏꤰ X]F?J88JfݜvGlP rV lgT*]_b(ѹrIa>YERT,Ѿ؎u2d  zrS [x'zZ6>ȻМcFp]خ_m#zjg!'w 9^ M"rZN\r#sZZ$i+v%efI̙jPH5ũDd iZrjp:(34Ř̞+ "D-phYbq fVv7-p r_-]/2An;W;;; T# 5u;V5јbޅ]YBMh87J)~XNaq׵̰+YGz]-f+Nn vBa.k%r଑g=-l~|yB#3rA‹d微!|H*_S.<2ǖLJ1Bu71}K%c Awy('?j]99Dc*pFY@Kwvx\qgEYPO1TY ੳ󰹖[dȒ$|>@qժ.F9Wqt&K03Q.Wsꊤ?d HISђpd v2D'hQHO+;9AO̧۲XNU! &mQ)xUU5c`;va%e2?w҆Bq|8(8f՗b+cub#HQjpԛ)Ż"r>Q]LդJ44n4KS\U|,z)HG~Pu{6%i*umﶃdYEɲ`2p%74*I<.l[=t#o eX2+E SMk|ssY;CKDelyn[v Hy9cwޓNn-u7!{HAz^<;3k^۷B~eZ!gD@&SoFQt1Ԩ\ir,\"SМeςy=4בg樹hs[wA.=JL#[un򳜚L R8Ztn?5ׁSRM9+Es /DvlN+Nk8x2]IF'cEp'|WTxz2F3rJф>ÕB!^㫑zkU\4x&$\[PJ{Ʉh7N/7Ӑ-VWDDM W9i>W! kڤCzM̸=_erМAy40Dl9+3ľ,&xz {VЭ A߇?~6JatmRsM xԋv:s,9#kX~_B#^kAS#&>,;&h,AOƜO87iNY <uJzzFY~BnT*cPUt[!υyB6nnvm}y)xxV+å$'Zx; rrfekg{ \vtM1W_ ц"ѧjO'ߓFlLY,g/%_yV<%Rr]2CuկbJp#}%m"8ݣ]/_l$)SeG6a#z)qh4Q~ 3q'9뺣lono-ۭožȩSo77*ʵu+3m)o;<\J8o[#z-\^L/ԧO٩b;vBƦKhh7{_ծ(܋"7|X\W"?U,F#Lk&Hu#wܬ]UW5cjw0y)/BM'fEXv''1:}߶vP ;γB@;4,JDZQǤ1ؽ w 7#L-bɱYln-9WlǪw'{m(9 fܴu<ȿk:Z׌V'3mn,^:۸dh4B=v&Β_tXe!SaSGVz/*&a;euǡZ]Uwdzԁ^J;[\4P3;!}˯^wuxj-d,w%Llw4j`g"m>"_as!t9鏿}MR84tO]c ,RS鱛ZX(LugG9q9W!vWӾ{QcejW^I_sh0?Y|]-z˯m՗F{vE%0Ne(߿Q4op%Md|:<þOJv7΀RPUv}IR/z !QlON|j}xo? Wњ+j{9vDbʙ W;tFKw'%2I _e(i d!F8Hlg*; Vy|MЭ\8,1 7=$cD $(鮹J\.r#XB(8!1g@>5&ũMOȕ7sQB(z=uZoΎ8oo#Sї;|˵ o [<p\u kҞ:6\To7z~1.o85+FVXө?M Ulz '8`D rT@!\urRJn< WK_Gj܆k*ϊK7 lߓ~ -,oH>_tV_&_Msw9U7ӎ%.kQC![HF$Qm9Z7x!rf+K&树i\DPhqTُ+77FŽYCBv#7pI~y=42Gr֝~Lx?6.E.S/Y5_Ԉfӂ4ǟN쐿dg,z>J[O=/eraQ'A0g X [yg~;GZеӳ=v`x/]O# (tikhxNOm=E+IŇR][{Y+7 2A È?+?*/B%:AyxkĀ;m$GDd72Ts< |Ŝ*As"M+,x>Tďf/hhҥ@S]COV4G%Aۿ(q:|^~F۴ aϯ00O0:s*cFpkģT [NC6+ DU eˊ_c}8o[VnrmBޜ4}զ1&X)h@gy68 xD0NeV+熹CeJ)e CG{t0s- ?S88!6՝$"OJ',/RǡF\P/{bZeP5knD~M DFrbUaMIԉWw婍'Pݳ^;y3ZW Ӧ4kW8}_龶]?lD22S8ln %y5֓=]lCfmWrdg^ɵ$f1Ӻ9N`T]$_X@ݵY G{ACW᳗ŜԁۂﺣmM߾jґ;Y"ά˱Ene2/V%Ȇw-ܓ39.ן8Ύ HepQXE#Ǖ;9; c^|%>JEQ=ipJ.?DEc V"nneǐդ{`j:*-~\˷Cg0(=ӂ-gם u+@# CTe81dxYiyA$D\q\=܌C^N&_]A{Z4w0=];&CS*jQ}w{]3խ@L88~VٳYjS#چ)CоTMs(zK7$r,goaV.4ܼܞ_z}f %Ҡ;?O\TF@H$j,zu"ŧul3g0Y*+w9ՎyfmPaOv6?~a_%r o1:ݭ_K%2pPζSE#"iӧ_Bi?A#?#Wv2XMȕ56rZќd]cTCSU ^Pa,S~wL50P|)48I.(f~ө3Y#Eq:Ca}̍cyeԡ|!=TԜt?#r!w*q+_4"\4]y]_ZɞW7=gu5rusp97yZ6!kd`Wɲn{ !Z~ xOgĥFJ76{24"Kp%貢‘2?\@O>|CVfן ࢼˡ 7'~RE=l؉= !AIbߥҺgk rlOsy^q".;R#n ?H?Sfo<|]u.T~('[DހY#$8gd\S{:mɌԥZoi'ՠPvaN\ >-O^HWaaez[[N 'L%wu,MRCk6jW(qt#wЩӍvJMw ?0x8q(>Aic]Y]@0ZQy͎3Xa :蘱9~Cil h9-'_B{N:Uͤo,5W9:Isvg;o:)L+;Uf,|V =$A cnUAK/b?z*bb~W}U7prc[T` f_[V!gͨ7y~w(/B2{ӷ<:ܐ}Y OO9l'q2uTب.JKaIޥXIڂNe҇X7ABry=Z cIjmř]X4ܞ*zeV}p yG 'H{.kbFXR{RіگRk9ۼ?uKrKL*W[dgĨ{Y19FUHTѝa}`"Tԥe!I힩_,B talU7thJTK\ d`&Qk$z+Czڱp@&GRcKn1P/[˄^{ȘneJZ$cQ{CzM# YDS᡿y}Q>+ubgi; Nc9i "Ȓ~Ϙxeu֪3t^a/V~MՑ@4rA;R;xBsM#QNVȭZتU P3 γ[G ~GIF#;N[]mx}t[; Wi.x\r6cF/EkF\Hr_rDCuGH2ȸ{7)~lrB=yW<8ngv|>Hw1~C2O]ypa2CqƁ!?O&NKDMklʑֆŽԱd܉j_{0)^ FwF(T_l* \4J׌n|E+B1MmށXZW u[I3 q&#p>$pEg,"b:v9)?f+ؠ8,{w"kS8бP 4Nbx^z?t`*n=xHV`@+0\'R#\4}ck)i]װxf3htUa(6PLnvv%O'^F+ozcS% _i\%o#bk=c:)cNI Q$*T^> ~y?NqgE90?NـAc:M5Wv|_Hhn)6CN5VBJӻD\U)* (qv DlC\𤎱5z\fB kQzU-lPX=fCW~йw<`ꛞ*۟][{hXYrSqq:7j.tY߰y̥?=L?-mV yVLuZVsR&ɴP晴j;[7Ժ^nfPL^;ITpNGmCi'Dr-}7_+~)ZX`#:NSi .{<-oi#5/%t'dL~ v* ;Ke2m\ӫ @b;89x959ӀS@ M sZgbRLg(~7m97%g])[x# "ugz/rI͍'8x%de)'FDzrv5!Prݵ$4[,)ϘR#/[$/m*򘾌'ZE[Qp\f0du'PZ永cj+6r>yUE흡X "yY13E0>*Ft1E[bP xRhGdy|w4 o we"W0皫 n a۝lfEgrDu)?EݑX==3JxH.*r"&)H cu'_wsgT22pшˣ6O劂949/ȁ<Y߮DD?NZĹ~o|BǟL~/9q wN0^سEb:xZ'U wuL-͚p/+ZNjNaޭx3kD57沛^V~G$L=U=2f{Ǧy~z067@EDBW}9i(]F5WZDzt\ƄLz)o5tt9~pA?k[ q鮹]{0|eDw=R7YA?P\o1 -㣡v!pi-aNbw!-R+Q`7o.CpDq>T=#ެ}+vQiK莑'@MP*+3}xzuF-J+i/LT_ޖxAȥ o7OHĿΘ( qҠ :;(2(MPAlF˳ꩩټyNnhjnK `i1"j딨L;4'} ˪oW@tjX]^/kbhԖ}}w5B^gc%/:1PSUo^K(p=~w2V a{jiʐ>k^.-kQrrλΝO,&Nwtt!#/FR*}digw٢z9o67ءJ>Ha-I&QLj.:Z'sQGt?`X߃N#tOx򣕣f$ȓg_·g:9WmsX;nx.w`ͩ%_Tn&(NirtT/v|1b^)xmы>LDF Cdyi"v ,mؑz5p|̈́Oyv2{ 5xQ ػ_7],x6\n"52d3Upr79כVz>R?fCKr6S窑@rG5+ngT[JKPUCeerWؘQ[~W0GmgpS+p^׶cG'{<d,=}ݡٱt~W}Ig!d쿳-a AqŲV>w+#pfrT[G#5c5 \:BGE:sk/F cu":%I?ǝr[dMS p[FK,ulu ͟*}8*Dܨv9T}A8&=#e+ZX=Sk{9A[>Ko'0qZ=Ԩוd z珒M?FkSeNP< V9 i_<_{)JFKgi$Ҝ^9ݎ'u_C`Isޑ\_xpDR >5C ܗ]w)z+KXK wƅ /eb\wN\#&ꋏX)cŨ DXOur>wxJ_Ϙq'I<9n[9-a4-C}C\"o^B`RWmVdr~LyVom3Ţ#l\xŦφ~źV*gP91p ҅+X0`yZQ癇Zݧ.g*QWG4Xzz8 vrE.6"WZ'p;k-7'jDHtvTE$,U-踇]oE2^c#ǰsdDEU1n_ō3-k䊅Cb_SQ,zwi.O ".+8zx3e% E@SzjYI:j/Of9upQM*# U+d_gI,Pc^`8mKd ^VBτ+sj4K=m!v S ?-KhBH2ߢ߹͎?Bi`6EFR:*;%}PCSÝyaL_ K<=X*nvܹJnlԹ`nʙľ4D]nOI(}JBkTEBTci9u3Ez%9\eh+b-IӬ7b g|1)v#Xh$@Nm=.#'jGW ZZ ~NzVHYrCRpFdzVXmxbmI=żZ2wk$'fT!^P8ԐŧHp i7V8I""q8e\q9t8uefH33j;ԩǤÀZ{dn恪nϔg͢U'ޒa3J8x[bب7~d.hѓ_;I[s ϣz!K9vusmztZrUKhhmv+ Zz`zg6| Cc6 Q8r{ۉ?Qk_Qܢ.ޅ>}O@ v\'Uf"7]ۖ+06'Vc7(KhFJVi]q:AJ?MړR~Ytͅv\^ڙ)m˂&0"R9zf\[8mM>K20FvJ]G0}nGɥ&ڈ2!wӊ{7TnOD|غ_#V|3^|#OIЭD;t %R4As ۵=fC^gL3[c=v,[70X%S̑~m9l |˞fY+-5\ OhJ9xМ^Tt /F"HyTCCb|/VNOC$8Z1&~tcO T|U1%&+ˏ"|=K n/XX¡9yj;qW >ϱ{`ʏ‚H642irJʍ@{.P7Y+Rr)È{DԪa>:|3րpx*QV.)=-}~r3Ƃ܅Hl.9_u\&y,RNAeM {)9E~.gr؈{܋ ǣZʪ+Hd`p?6YNt-3=< Ņ }g1o9O C?݉=㰺YלȭpZS*rqO Rie΢҆X.lov7LY3#xRvV^[F`UXIs],o3rc|U55x|1i 5C*>| tM׊EO!%̋;ԺyKԊuOfIs-fXmt RU=%r;xфTuaI.ƟA@9^s g{(돛+{.Ch~T3YݴBcl`d "(W󇡺窾99#~2lz6wޕzV҈Fpn[)NޔGtHXzFG<_  z*R"Fqw ?͡}e*jL[ ]ܨ1C/[U|H= n0Xsy2FC&KeBY}d6V6nKƣg3klaՉxڐW# x30_*]1z H(`r@3IjG~FF7ɾf}}n{ʍgɂTK'46cY_]sH>_b~wtءO"lY{vn1FW]4ȑIbx0W8#U5]Fʪcm{lݨ=/sc5NtUw·6?)I6[&I:J$-[COqH`񫮅x@p2Azj?.|O@+?]5So[_ 錏iVk=C9*D(ǠoUxVtcBzAd`ڍ%QKkck`X _ei2N]ʐ}$DǫL5o,xϯ `^yZ"P+o4fخӍHZFUaൢ$ZJ Qڏ8K2B_4B6K{`/iptЋd#~3Zn%q(#6[\V5}|~Y qiH py7A.r-! 77MZC[ YCӈ4tD(muk@T7ͦǴW5v0HqobmYZrcuX ǀ WTv;܄F6hg,W8rȜvNͱf#~K1& eXʈ{_<i\7B=ENzbgխεu䀭ކ8s{O!ɳ2,N&yfg1#B<&!T!5!,=60zphܝPѯ2 ThDeVbL1Osi2)W^2¯\%lA'19]/{Kx),ܐ|=@ہ"z2=C/ 5RJuNki䩘7[_b :>DܻHʤO#tae_ ?fgt Vä?hgse ?X~g.9֮.5bmav>UQ{hE6;S Ώ:/FUP4xb-7D^*j4wC.wzxhr/g-'xV3t^FN^R4R8Yf' y!M!52BoPQ~:fESk_߇v)r^zZ0Wm) 4'Ll#뽊*p:m/۩kg\6S9i\O^$JA?P {kU#Qu<̯UT ՛.`u9i9]E?cɋs4/}W" >,_\vQ14rJGq;-GRcRP;8(7u{m|lj0YrقvB~E|L1mǧ޳.V8#n5H=t@ULOQX܅vX=_݉Ԭl \EBh]׭ۚ# /IrvĜT$8ﴍ B]$|.nNWE@U݅o$ z"V/ji7O#X}cvs$ Vw[fl6588 |)f\ 7(퓻Lk Pb1#YfE)==RשY͈)"zYϸ q> 3@3z=`Bng16^ g}S_Nk[9s=r7CҊoM1E S df׋k$5'`h1\#K$ڸ$' [͛u>2WokUǿؐ("9=[{L'.\H ΉAm&^UobtkŦN~.wxXu#^Y `އQڧn%\i ĭ{T=j{I=ϊD]?%QJn $&Ð'v~6o%uُWS򇑥e^!)=aQtn7xk3&ɲ([qRddZޑ8cdt3e#α2b(rl\4(;`VtaYm { ^?!-wK|*9ɶ\Rvpو"8Խ"MO ~]FΩHנD"בimus ux]N5U^w^иh2#}Ռ0irAݶ^)[i5{=qU^5"ׂS< {.x "4]&IHӊ3hzqxo1:iq7ޝ%-Y-ئq, 0"lJSMgO[P "B4z|:5D\d{(;8*RKO\\RmƓ^1od:'#[1G7Q-:(Ip|θU.6zYdz syQw*Av pcCSu)|șʫ_( o[A6GīQR@2iDFglXJ&45'r鿗]i|XD\,#"qy8  %e,D$=F Ke0/9^Q>y.+,^N q1VZtXDϚ_VQ ~N5GuDV:&fQ1kD SkmǓd/Ri[U`H2'.~8p5Wp3z󹜚|QjE::Kck[d\1/[v=?/Ux;uo], CڸtF<ήB>2"^#Mfrō L uzB.G|~4GVӁ5rVN_Nc9/릟Ox0|_?&¢7 E3/|.{7{+LKR(Z r6Ykjٳyz q2n+n2^"ҐC޵VӤȝUQ1⫏}C*Ni)@.9sfD6 9F3k@п[JB3|R>/)knR_"9=;_Dj$ۚJx_~#0q:t,|~-5k}Bv% \/Z8!s\#SuLC0T:|῁<}|~wc6}vzto^9h)8[(ȅj_y+xe@x Y B@.FnM:lDviߵ%S&RZK1)}7^=.>@Dєy^t=d)AX k bx>GMŮA|J IxjnG"v8B^UA{ua37p\p > -9pr$[18g5  ;z1ygmPf UPb?'.H*Bf>.i{F#`< Yh=QV_2}hN{.{v0 :"mz(2F^ H-/JZGthͪZZX՝z=tB HvB!CTj^wINTYkyUNMk=mYs Ba߰O65`viG-oӝjXsi%e}"y<1IE! ~ ]ll eJ/H?]BdFDǵ@D2_x-.v}y{3\#NE0[_ķ7&s,P5I8H3-OA*M>]Y/҈%ɘ2YZvRkgu)f8%{9!r+ BFDe ){@l6зп|d72O_?n>sޓE72E-C=?_^LsCr=Žm>TXh o ]/Pz]Δ vU|e}/^d ~C3`?)&FГ䏜It5"wc. JV[ 6ױ"-ߤܲ3:& 9E^v4+kWCk LIYq܀7/4oBdX,ŞN=V<]0'׋ UXUo(b[rnԈ[xU%'D"w8W3[b殓^0S/vO oAS\s1३zwBN Su`rқ#gL1yQXԜWy'=T8#1PaW$[ն^8=)\akŭ |*uj}:dT~gf:rP9%ז`K:^>N\%W Wөab_xLKe0~޺`F_R->Icé=8[हӴESst Mï>.Ӄ{+EOAr i^mvmfDŸ𜸗)pS/*AJ<]v6+~ Er<Դh]nLmX{}qܺacŏUY,W3ZԮ C[:t{la4W)/K% ͉Y W;H9|}g^y{6;yb0knC]s@𖕅D=:Ѓ,t(z.j/j]ahfE9 vQMNן <pM%؆Qu <߽`\pJᰝ6krC$}O*' _=(_4"VyF҈Bn]iGjLϡwfXOesrtTN.xVRS9՘ ;7c+ vX4ES@,_1?/7j40Gœ\(S#bM0a{T$D}GiUis Y| =;> I;M2`b?FIJ E(տzU9ީOaT3jvZ}E^_v@MSz9+!!DEd` rZiH:` ZKG4w5B 5ݠu@ ^B(6wi1@O,?yxӒъ,APNlf5qj-TΈP$5w13徳ܩ#"tȏƆP{d yaj4~b3f8z|DXYʁ\s4GݓIsJEYO6pDqǸQy4>[랧2ӞczL{Ae!gc H`HLdaܣMOQE#ǥyض{ot Y4"Lܕ3rf)]1+y;ͩ9Zl\|o7jmQ1K\hȾ!Ss"aӻa6m ˑjk ʋr}V^,͟oEHgVmb]rK'כB :ٓ3zӀykoy!Xpı@"cXR`/q81Q[OO\Az+6{%Z`ķ[omV) QVC}5wAXf;NHx^jP:gCŠZAF9ȿ.Gy7C{9c䆿7:v2SK{7Q7|Kr^|t yr? 1R,K?`ܹz4}yb>>?ڲ#&͞pp ZLLl/|쏌:*f5'iߗyGz#8pf|+5v"Nw=e'.HDwÑ@ ׏ Z? ވIƈuGz:W #dFN/SEI~X籶 ~Z˥XbȮw1 TS<}ePoq=ڞNB}=Vn=P%Ol'OjE5QRE{ ou'P`__ 'Q"{ZnO\ud>5Ő`=1}z6g@ɧ@끞ҩ:ect6}Q'OuEO__=D~bEj7^~lA7MG*wCbc!yK~Z H}ɵQ \0ߠN7}WNRݘn"7M2($#[?ʩu[B3!FF°BC!/HqŁkI:Hd ldz&6,~ nDB4dceFIYwZsL V@ed *iq3~Im&:i5 ex󠱦*UYKxl_JӎS0et^\S;yuב},{[,F+ PFܵWWklն]o'9uURmJCΖ nǧBazƂ{4V j&5oW( <-4$<ڋ{TcPEܣ=-Ǫ%ň^}No9LɿKSM%>|\׀UжC̀H&z,O*lGHW4CS,TNJKj?ͽ$[}`t2A@{fD3NY!E {.ϪtjZH Mċ`n!G|hخɓ@fҽ6RϾnF ^GjrҬx)çg2sD/jnjf@$rj\TwV-и\rˡٳw߈+T:MaLtƪ|NkߗۖldաS||]:lU%^bVoz. #b1% <A#f-\'Vn.1s'˳2Y4sْY4Dۯ۵{;GĞ/3zi9Y%/O@a 0bQ =tC,6Q Ig!K}~qOjZ{G 5fER=tK\cj rq\1!fо3hIm9>Jc&-ߨ*+i0B|n\ܤ[YGcbع_cAXϮ sxUk 4dOJU"'Ά|)U:</ (+./3t3-cp)O4`-/S'yiʯ\RsmS#:t3 V ysYntuK3%%ۣ),]fxJ6zZ򌁺P|FiHռY۪h֕ ^d"xoil::~s+ϷEܡHv4`f`^( 0B#R>PWT£;\Ϯ=_%c-~)q3.8C]e7{irLo:Ide-7J 0:al I( rP$27֢wd3-F2m2IҎcH$F"/teӘU¶c%O6^khOT7/V6^SF~ ')8Zv/}k3P̈44K@"2^w칔(-m'uPgd:5#AyGD#l;z^b)9zwurz4Ӫ۟#܄j;8B/HMOED8Fl( 0R iEȻ뵮IL-# _/?o`^q^+_B^Cxz$BNDz x cg]^nXQ\$g4hhJcWg[F-">x8>ߎ[SWINˈKߡSʼݼŒU/VHy|70Y߁[[^goA_[O w/X2?mJ"ޖt`)þHFBJŦ&yeP) /2hlbOD[)ȿO끪ry[ޗ0^%[v CǍ`z/D=_~ VE=&ߍ1GjcQ_yϟ~ACt4yQ <8:{hPۺ*`do8t\3{ -[]K+4Nvb/`! vZŒߝܶ_"|7Nhn7mQ=TiIOޣ&5k$'Ko4)AYW˙:-ڪG+NwPOH(RϿʙ{:i$Nd+C[5o &%o~Ez(}I~zD8 t8Rvf4VHd*Ѳb;Һ'D;J_?I:] nFRo~$ANفpiٺL:13Pxvl/o|*r]KN)oehEFl&8N(zڷ&5x]OY~'E,Zr*'0Cų.vu!S)> h~q.`aF/1l Mu(j]Ҁ*'_ }^*+4Nkr\tsYk/C|p$<ECed|7l]p/[0,Qz)#jJ3JA? ЇL  <$?mu?8VOy:'Sb6v׻WZπuOR|ɘohstJ4:=|Q쯝L_ o׹A,A YZZʈo%W=szz6˫-f3bK+Mnc 3+{5ʑb6J^yhDIkA[C(mDɋ4xfS$p:_ H,-wg0*E̴LF1GG;vtC5rG(V,y4 <tvlktA0R$ő?Q}$V8q2zzR涇Oh?OwK2²?7#^,‘mVڮepˈO^Q*?$IwwL{ot:baFuh<փqI2c/&QG$0K2e`4Fp3!E0~7^0%G}!8•d^#9tI%uCq֖w~Fֳ{hE28kPuHof n+=95Q[ФtE6To܇/?yq;K^f5ur^.u0ͮu~)IuELcR)| |F$_ˋbdJa.l 18"SHE$:X.xF 3U]O5ԹvxhO8ᢞr)Ca Eyƕ@2QC]iII >/2xtx>+x'Շ]vqmaW̢J"0ghKtng)=~3Fba_Ϲ9e޸HuC4@! =ZJ|ϴj,ǽb(@qPWe$5eֆPʿ$$vj.T{ݪۥ]K'& p (TPF/i#>@dŕ/Sjdȓ[},m׎;'JC' oA W[bT p4zN <Hr$J;Ӌ \oȇ$l }܆zp됋 p-e7M=.Z4{u60l utlTuMmq Hi r㩴ݵXD]գ`ғzQAwЂWqGL4aIw|5\|kpХQ}@  Yxw_\,A[M潮vyetW yfJ_◮clߒ"R5'밼>!bCܬ %VH$%5}rX@ֻ){Xm{$~R ysLϰ$ Zs, ^!Lz)N xa@-5f, $1ʸFۋku+3>=]8B{b/~{^/Bj*ڜ9_n3T[C-uxF[J#X= 9 gUI7wXc9v@ϾXar8JT<E6muu~G<_w@BMUpRbu{/x%Ȉ _6}jvd*u+K8+'}~g]Omv-bQf #x1>mnyrZmnU iChq1lzs(oճK^긬:nY~g>*uy: m5Zǝct|sN?2;{O4e>`! ?[BkmBu@8+VF^"6bv] zcOF:-}A''ACS"A͌nF_vKzg'u:KYơۛMk->oϋt2a3⒍ plYqZF/̳" "LTP3Z'J $ 9Khd,"[<2ܞ$G hq9gܤK4jq;.7k,X^^gz*<#98qL)ɏA#kd'{Wqob$o3tg$S^2Mct<Ф7ܛdj|"q,*5>(݈2V^ _!-gJhEjIDa٭굑Pu@z{%Y*/k(I3H}ᙵ]E-1>K3=~jzE<˗MK\f<ӎ:ury%|!?Q羁:OH'$/fy!:MwKbRHV =bݕ[LtE;+֠83;YO'+=L~{w[ח{GP̠['?ѯRz+ӿ VS^?PbM3b&);h1j̻ƐZ"IO]- .<?#6/sͤuL4+}\NN/<@e-1R;xS Olu bꁃMBsqzcnS!zBy mكz_JA_tl5ƙxfk4z+x v%Y)ET_ _-'=E2\Nsho@\̓hmYS?GIj9ªL4z@G|xX\ŚoZ<Ѩ(ګF;Ęgrb4)2o%/s~!N]zm'k,e@^i/ (z B '7 Zy,[J޷hţq_C`|b5t&mEss8O<',֙җ6OIu!@.Em~ p* BeX'`d zx.I`s9k (\Oq#v:32Z~;#z MևٯƦfiMÕ<;J/j>N6`^1>ZNT1/-zV=?E}5綨y-,~.3Ia( <&|q}XA/\cz@H6äl_K׾vT0/xK߸qhC_f&.Mb˿";zTOU7RNodw#MdVvS[s&[ Ū xm^-Ky}96i?0;l낚:؏ 7xn5 P=uݜ[u0UWL:G>8Z$ZΥC9XIJy'3ߎ*!_mgþ-4X̢to|wIP~G/Mc\b5of턯r +3T;7W[~G1gpG Y3pOc1/xbPO]@j}j*KV~rO˳DLn7ݶk`dm`ֆ&[\d%yE )/q/qjf_V¿;qghn ]̯Gza+DΪb2vǎ$0SI4r/Hz&[LYާyp2C~֌D -% [_?'3\;M#k^ N)Wm՗^32FKIսV0Xc1&HūrE/C&gf j6ɮ]8e}$ g;VGz,[h;S~}cvfX(ӒCo&+~=ǽ5ܧo2߱FХ &睎7P FME,EJ}Om/Q:kceətR`_(2Fy0?Oʛ}nFZmQ? =v}$j& E9";s-GCNT#T}}] HmG1cQE?.οĿY¡;`Yy'LͿ!R}lQɔs6;\J"Jݬn"/t-gfawr83I] N"5/WSq&cx]ciuO7gDk/~(KG21BMu3_%T()lxNo,mnf2{Ae-.=|d֎k wڷgRV=tR(;w Ssxr qFmtcMI1-|.`Z N?o]wx ~v5w%ʁq EBg[ _9p../_{汇6xlZJwB=qj\V&wYVLhVdfGe4#Oz-|u,zikR ﭲ:yˣwHV;#pcv2xm6{pZl@~@N!g|sj*cRW(P+AuQ=v9.}[QZx/NgS ^COw ycLԟ e>KPIclފP(U}6֢J(e#'j&J~yhZ-. uf0u2AVFtiſgI2ؖ -M;R+b)ϔNvbE{!k+З -3lvMO]V; =c:4WG· #* YYtkhp,krD{ߧ W%U2cne=HVK[lIƠxpE=}Np[PpC&±nŌ}WaԛA/=}QLhn_ޠSSݫwa=u>5u"rNHb;GiǤv)Wl^oW+4sl< *΁'o^0< x'zi&0?o7JcU/@_Iу+e%eO]Ì7O"?cxՏ5Gr7Uh}^@Ay7~??!.3'ƥNko07$Mqz5A-vl& t;5*(-4jdC9%ӡ!Ov_-N:( W\-'8)>z}B\c/Sd8\iWLP7WFtGn}uMn 8j9̌F.0mOPVa:p/8AC/9iYc͡@6˫aSЄg\7bЀ~V~Gvo4Fwڿ6zWBB) K>󛽄vxY 7()UiڡXd[~ݩ.of3rDHu*J1$l;KTr`bqedC}Z/)oKÐwTz?Ae<7z9yù΁~ !~GIaw 4"߾Iǩ;&\v8U,B &EmZ"rl x}(S)1K5/qGH.|k'0=9 SidiB(rC S#Ch $WǑ5_<ꕊ?+mƾRU(#1hK£g㓙Rf]T|GmE6߶Oh4ޏ^Iz֢TP?N' WU;tx[$WsxQzIf9G!FִX1bL -ׯTlhW_/qxcD#MJAIEf:q$yDc T/(! v(ŅZj|PWY珲$/T_mҀRyFU֟{=>[[_X?#d_= P7~~T[trދO XBgxT#kY TD3 C“m\?vx%ao6{Y|8ӪV _O׏ĔA@bz؄tbywO8Ǭ-SNA]\a5܋of̂D~-wRj*Zli_ Ӭ1NcX;N3fNTq  '8=ƵS}kSMg@׆ztz?O7ac~JoUV . dNz@kө<2VxTP' XـxFۂڧW[ /\'dZs}POJ+QTLjl4lėxg@&ky&$7v_/zO-p-me#OUl0e.u'I_>)A,c[m\}SH]af!t* ?A[ +N[r .p]+-PTq*|9Q0WN]W1ODm@noN]nU=^yIX\l_V)z .9AIZv΁9\a[B+b=a?:Cm]_}\J{rcUϐˑ l:lzNSdDm:;c}{b{3ؑ*xC8-^^)k?4F&?ͯ-˾;ųvYyYVOyAF ?E)Yc͓iu5@S)7;]3G/bf rzXΪ'ю4lm|}hq#PqxgosAFux <ؚɊNԁ|[<><{x;u<FPLr*>ŶxϹ|䚌MtLL,N%Io}I.8|sCOcQ2/b]p:1K_!z u%Yr.Dg/{C(޵K=WgNa1P҄Ӽ c՞EӖh|ckL[s CU/_Ŭ~5A'pәNg=OV8%?%~\~`c'gUINaVn *MQ}??gL10Snㅥ|\N$\V! _X׀P2 Hp!Sפ#>CW7eёDNZo'"kS`ekE{ym_:kԏkax2+>B -.F7P] !>c`~8e[.RN p;gmӘE-=5'ƨnੴ n4 h_E]\_!U?f)ғd{@ (ſ5_9:Մ*< c}:8GD8~$ʨV.q]_-^g̿Pt[9ۏU=9#lSShʗ冧BUϛfs3|)}iK;1BZ43,1iWjmU7]PG97& IewvE'SqPGY)k;14rmt:osF_\5iòH`.Y~tiVt:>œ|(+L>Vmٮzo0cW 8Yʎ =^[VehjyJK3<);nii ৏WNsi;=.I=VtXK\Q_c2xV 钻( mq'uwy@³}4#ۛj3B:a]z,}.7W}rJh .m~7OwԓwźN~J;o:366jNC =9~x(S68JI7j̡ 4bZ=%d.sed=vzÓi㣧a'S)8ih=oē~).0^D"׎*HtYuZ_5s=w <%'o~߂ſRi=x[mzzcHm.pȅ;ҒPqGli W[Ju M@up!N<}m=yΠ{2|6IdZrc?ܛٳ7T,٩`f)n@H?i۟ FjOyڏk}0}ABddIjL;ꍐUK9deٕ'C4&{=I"^OzCd=6̐l|؋%x牴G;~Dc %-or ~4^6A1:aQ'6U$Hfw?SI'^cZ>);`Z{8 IpÃg΂5S, =;HTwl-g@#8xШg9u~lcUt/x+du_Z'b(0NiG,FgTAbfu{wȑ-uL?W?qrNƑ)ЕwVvğXx$U7G3y}h[MQV=#m8Pzz2vEԳxn>{K1ԶgFZ%vpOV#q]~n\| '|V]Wct^“ms@O xK,úo)S #已E[ Ž 0>=|vxc@CcTv*hPW9*na<%D ({RêF ԣp?xKpP=6 _N|CZO |=X.;.(7BMݙs[9NSrknN&7/nf&yZ_P^i pZ5 J@]ȽH'׹;>}%*bNeXS==Db(+L)q`Oy/ T3جjchv;r3<%Wf/Jy'Fi4{ѣWȝ j?SQz2`z*eo{<˟vhj"5~_5)4[#"3(SVo9e:7hN7.oFF 'r#<߶K5XWhY{)|Kځ bRSP*ҹK-)0h ocvq6PC?!֝Ϙi4il#Ǒʂj0V ~ϥ0Nk[+uA=cVrLMhlVFL+?.rSWƖ#Aoy=5#4m1D {_tѠtAB4?S9LҀ<ׯ1vOܥbG@ ?o,}ѬjBԍmdyLcco| 14v֪1VxN[^"#kMeܘ,yǧb:V;k14ɤ4*/kЮ "wjw/GIcރz|~s|>ZnT@rL[lF%4TOg~ޣj!:M4Lx|mELoVcjyH!dOy.ENz^pɷ+4.+N'&&#d^=Oﬓ۬z&%"8^[z) cTʲ~Noeُ^INsl]s ih?=S38U?_ pQD4F2=G^_p"<ſx/p($dP+:Q9a%.7vcI7K013y rsXrྜv'cSN[B;bk A6V vuh >́_h#&bkg{r Ɔ^} z4/LPi5K==i7ιOE^уl]}<._Nn x"?\(a}?/'uk⭰/hl;؋yUPiܸ?? =Ne)v:ekWr(NXu$ usœA;x: ohj<.Vͨo_TO3zRN`w.X8˒Ey=oPbN[EogOoSS \啽F 1:5'S~9(za]m54 1.'OW&rԆdAP^_ܫ''X'15V⧞[b0 { _fM!>0BJӫhkM!EcVF D[%pș11v~rпH_!*'FNf>tiZoS![^}1Hy(71űO*#lDW RNēi{``!@e[ <.u}t#;JK0wzˉk' ז0efx=̦'˺V3]5ei A_-2kpg݇6C qtc;btovݱ.;VUN+V~=h1i|f& :a@v\cX;ʞ*,4MB(7# `$X~3jſKg\ }sV s #Cd CYjfn#lv [c9 鎽~ \s JTAݕs_8?L}>O<$KV4Cqzm/%HeiͬՓ];Ox,zVK|O 0-Hl\jlS%auSe$s ~bS^L?l٬͎P A s;Sy(q~RrFKfڳIlDm UƘșBQOcCkmW ө3#_9:o:kvGB[!c0Qٕ157ݙ`54mcձOޣsxZ5MC??o0, )X:i|<<=ҀT^!pm;F?Os$ᱢ⡬~ctOy>.Пyyy?Mo)v=L̾,i]nٵ 2[j#35R7mTV.T|nz,|l((i;2c;s݂H[q۴GEE4$I޾^E:G,/%<:m3Tc>161j;jW?dRʛוbI/ʖ]ӄ鷨| s0.aLZW߳⦝ӤvLxr _Otg_Ƽxʉ4!V1~|Ό.mkdKrzBeRZ8YO#@GGZmӹKhOg#Ljrz_yv-=Hrrk^uu$Pf(k?dι4Dݴz6>6(Շ 'D ?0yeX>&NqEAճTdTu_"N5 R]ѶOB_Jd#XәKU'U)mVѯɯc:fj0?QuU; A}5 %u.+L2п}r*~ڃ'vm_Kr{iL*-|L.3Sp!j_cVZ<~ Sc7m e_:z^JBv˱P3ix^cE;Nkjϕ&˺e LuR'r:Ͻi&*uwB8y?w(7FLC˽UÅ^bgsle+@|-SNO ;=|q1b6Y$NAwlWx CUO:_J'fԬɛR{ 7{[1eеgzԌTnVWtQUrk˪Z@xP[؊) aG) >ai KSSGvTazՓ5HD`\Uެ OgAgh?6O:/Ą|oq-E3d7$Y=J*%w3PN8[nj(P^05N;U\N)oV8M罽YYV" FrG=*ط\Ճǧrs/AW@OqG8})Y<x.o8lIoSbO{_\gy{lxm#f- JNvz˨X(pU5y %Www/S{F'ƭqOE߰zkJY~s0WJGc 8 d:fv>[xЬ\Jbn #S.ƘiIqIb_$>N4ar!a;-7P3̌`uJWJOr} /mU=J˽z,mK6y_M>QmvV3<|kC%?jTWv*vco*K?CЮT<>Bw5q=̖>IEFOzgWҦΧ=jL7Ď?Z)Ϸ0W9=L6xhl?ʺvv. l9<{y::X˸̮pO?W/ڨ^[+R!c 4B ǫ^,l}vrLXGY{-'f5?NX+3hW־8WR.JcNzU/@8]y;UpK&!=NK"Z4uwb8D  c0Rk2pjsddEnn|fF/NuڷS͊8YT0IϪJk(`u׆jdh\K_|C>;܋^C3t(/=:R=mbk׌2=LUzM6DO#-WwSٲ;>6_г³^_Ǟnd̅2f `LPG_x!FTu\LcVYH8_Wիm߉i@~V~0>˙f{Z~FNW4~˽׹ѶȗĨ?>V4uX~N۳>11ĈU~6t+ *&.ɧG˿ݟO?q8-Ć;_̓I'+Wc|%h$ﹷ{-ٞz-%2Qet^x1*;ppw:Y;\EQwVy?21!(:hrE f~a>7 v +ʓv\zZxO5.~e_SqB;%8弲IN_wg.ܸgFJ ECsҺ7%>m^ޞr/op,K IgR1ty[ Vvh*>]~|4oOs{O!θ*1(׿S^^l[MGɮ/V6;A6V"Nn*dlBlV=>ԱV\p/g3rF"<:0 ~C9=pjlN)]ӉwqWq5^ Ii7BvFN|=ROQSosqesLIQ9i[jk[U~ޥWIc*nrt缞zy)?f,6CX(&ݡ7̼}AR=Khbz׆3maܯmp@mA'."zD[$ε r) X/uD¡;9|Sj֫Xׂ:3R,>0U|O_JLK4p@qz ϲ{8w:E>*CVmIw2t ko<ă"#`082 SS7<] Jop/ \"g [~I}˻4`Ze~0m'3sLYdYՐ"]UU,mδ}srɨX sg`#.?fI`ډұm+(d4Wj{^􆥷h3R̾!:P?n;{,6q#?&_[9MAgxN <:^Q(k޶#zӈ Tՙxal(AugP4 FޖZ^v-hvDIJ̿DB۱s|zy% " qSV2;kS%2i?kM< vP=-v&XO>Q^ìjL 7z_ZW,%e^$SݫzRG $D+KսXRwcEN?c+nHzqZ3 sV_(=.!?ŵ#WV璘B[[Q?k50;=Nqx+YZ&#V *,\<*7Fk, ou7Ƞ*Q[z"ƕzvL4ƕP)kQ {^-4rCLLncf\r@keh~'/a*'=XU4-\s1`X#@|Y 6OݳsO轌[d)%$7$V9KǔF*HP ]2vL"C=y'dM(SnDV^3p,bt]rN!>Ⱥu\O0vkaËᙻXl8]lisO}ˮ]=\O܇=+b&Ho=Ff{2FGyY1mT>qӌc)UgfVkwF#\98.Sp+kNq^*N_O*󒌕A\_skؐ%z©NITy!8 *w.=Z;bmq"ȭj,%==X=3k%ـzbn$_mg2T'%i+.[(o "wi>VAWԘed-\@75Fxs2 vL?zzVtM h>$AV)|C mAa.#~}j?ĵvO-%];O) ؾ>SMcЕgg_]SRgh6[ "^vuguZ'}-$6lyR^--f<{S3g$1E0Qe' N T=ht]46aH;پq4 yu.;!Vp9I| 6w QϞW; :՘F;}ymLirJqZQz>>jMQ zU#uX_V|t.tzK0D3WO+77|Azؚ1z5 u:)x\{-|~._ᒩx~m/$ؿ[[Sj˒(Z𪞙 ӳp4t{9*0[3^*P6(Vg^<@J=v2@aUʸ5 2i%0 _9Ź{b,!?- 860P1Z n]HQFz͑&S0@\Q-8t1JO|bضmIq pĔ& ]IOe*WMZCI#Wo'->&AkQ לe:7aUm z+)ArqH4߇CM4_N\(ۛ~׬E :]/E? +s:= e:zJlY+ _EëS.D>8V;i:[ǔ+`<}/z5WAQ^q{[^0㕂Tvu]QzEPY|;@/:o}:P3=]O*2׫.{QǞSGN3D޷q<穔qJAt5Lp@ۤWڊczD4ug,gFnat*sSfA}nt&?/sbF!-bEpdPsv1q1VTP#Uip(m h#i8L~,31zS,x`5ԫ_e6 +*j"Z >O. ]AhJJ1A蝼?%eL#GZֽh=mB)25:Zʵ9u Ә?,O4tq[8HݜKp P~1Oi˩t~)+Mƣi#;_=?ELg>oKl+|nӯ#s}ft/1Gam z{ >*Fs;<(m1AAgwڊ/&.%HY&@5mcz^ȮxE~-:^*[&4fH}g cx7N?lA _j|bNr%#5}|OscuJ\O3<XBݟf vڃ NcMƮ'C{$+\n-.|[\7[e3bĮ~v%?E| _͜{j[[Qfb@|'AAK2`m%C(jox1)SƺgEV8-jz؜f-*|bG1EGx*WŦ9K%zg{{!7A'3?@?F;w0y>IuRFC@7ӕaiXm=>^1b]8^oR44 bMgWaP=(D6dF/<{2y!iC,VP֓KOe=ط++bQ#;*|'G~k?k 6?/iv=jBϼy<ɡonSϺ{Z1--A ~qM5tP~]pv()nuQ~;yGt.AC`ܭ_}<4YI?2G|v4F|+cw|h"zr)1c;X{ѤQo8jj|GY,O=vG>@+ڞ遘|[ ԽOӐ קBD)gk$#"[,OwX9N_c070ݯWBHIn1L(Xk2x')7B,UCp穢-2mLC,ǣR1jҼǜeѽ֨Cc7lsхMy4hc_UqS K/o?iRYI\IME!`Ji,{j|J =N{p^߷jRC⨍?:le"K!&8796hp4sIsyώ[UYטoA?/3yz,Ph3 b]Lق2hCƺէKT1U@!{t-_a+\Z*|x 3&_4|}&]8nR (ż([a>/JU1Vvx^+\9iEӝir^%cKRUxLrk_}R~Ah?-uԳҝٖuɅw=SSǥ?(i,D'n{6R?(> K!)n^R['Ƚ1Q8uv0^τ-Þ,_IEn VzPt|{oenJgm;DN7t1p:&eguJP4B$?4W#JXkd&.H^*s\~z5[cXA,:y<@4n0E-j&֮4N] (VtO8XcH-#s?[gn7{Q'S%V) t<+VWROazATǧb8S(r7eS854( }+>3]63l 8+[Oe"3iLT"d<>$9 .!60%_U!_=~*:Naۿox07xV^j0+z*+^4fM] Tٹ=M"{k\ROA~x+zo1>&x>7n4^cL+VNY'gEhe%{/&Qƒ!Sя PG Ə-̏GܧBOOQa =Uh>Yy_FOɦzmK>=*7{s_>q0Fa7`m4K]ɧ}{/u8up8qR?kc+t}]?'}8l2.篲rF|{@0}C-"pq?y5xDn{;rg͓C9/ ߫R4@Qަ򷤫>OG1+-N/>:x3w\s&6P֦UUԌĬ+!- d¢KlvBX6bvtO?âel_tycj/VtP5+^r ^;3c.M;z 3ᚌA錼j_ώ+ .Q0wA*ب _mnprGA#1O8qSS rY\FSRŢgM)dMdʏTQb[` 9ofqS^f2Lxwbqג'5so"zj-Ml6wl78/|#ɳ%Drr$$UrWP#h8!QO.qv$7JKvA! F}NϿ;MN85lېKQhU܋ٹڏ!r %Xf&&yٝhzmnv*-EকR+aUVeߑ}wϒ'?t$xE< vMW1Mje){[95yGd^Q.q[R^]c)s> (Wiݺ XU⸳wՎ&lN8=@x~냬W9]yZ_8=)USzM۸mI|KX|ey]ųr;]8c1:fqwyqm.p8[c_;)f^1 %FpxO]t'{*XUSߴmٯ#<4~ii Z`/@/? 6ɁWgX|Ÿ 3{G 8_XU]\-FݎRT)b{l~yxEC$M,-E&9Y(`Ot >gqӺv߄")jmk[ÍNJ/Ϧ>PO18?jWh h8lޟؤC_0_vJPaB:uv[Ggع;\ix>:S Q/lBK):Kyu`^|{˰7}\ 6ꤢ+Xi6/.ֳx@;^yT0S&hX= :S-O~:%Kct4K|*@ׯ"<mi ^`u_nG2bqar>M~79ټN4ԃNd0z=uO~@&n221RJ֣^)um6 __$2PV =/UӾU/ma"C,MRs%v'mkс"鈆F],SHI+Gmk'v )ARZ,Du&7sT)޻> s6I? B|"+FVX5$&zh ~D`HS:8}$QmJę`2X<^T CLQgi{['Zvj!}9 t6Yת>! "@k^(9 2K7>h- u3* NgI*L󻬕A1d8,'?gifS"Bc嚷ny kU=:&|8&Yq eS/dOՄxPG(O:fÕr %%[B.VE/:9Q|dmeZcv:ġn|a f-xm FI|#.q2U1:IҀ1;Ds^xQ//!6NFM pG 2;KYNMnMV҉ة<?](*_[KnwLmˑV/t>ҦQ7׫[C3:kvArSweH{t/kN񙎮cnNlJrv_X-M~7kOwdrŞ +,>ϏbV>m["9 $^E~)Vhh%d9eQ[ְ|Ab NvhK143\ŀ\ޭkKDvMxBNgx\?^J9Pw;vV:僺Ÿ˃䵞)t?qU@A,[ \!7_x?Rxܣ5xR;VF@O~,cv?KlF~Apl|q#skaߥxNya Pժ_-6rWOqY 3~!g)1#bVnc)E=[^ٚߠϮ'r <AX&*6+ǤmO" xc# b mcTp8q_ .s 1j18?VZfCͽ$4XO \2`|R[\uc(4-ggɖ;> mh5!-o߃bmS/M`i1{Y"G͛mF~C5F=?|ѱ'tM/Ʃ.е d+ffuؿUcgVx~L!LI@t.%< ZF=%Ҫ2ؔ+>d5cGOuXQˢ|{d@G%/7ܪ>T^AIcʁ w'v*vĔ⁶P78Ϝ69xI7Pt*Yй܄;RUMG`8/]"hd3n8z`Z6*Qkx-+uFPlGtU%&QYPʗ?gaOIٺɠ@7՘{s|fErwuTL{Xz^BHSF-N88'_01 7ǀ}nP> V:.9d&ՠɏÉ*)dȣɔx9"bç-zgv_0eҠfez6žN4Ou׆}#\CYπ#vYջ7<)aCb+=<=>ȷSJ4 $WE w~1xյ##3Pb~yDA 7wTTZ8PAZ%JϜbnU_9Bc[ miD+1LcEGlXl+4'6#Ǝpz^04Yd}<}*#m32<ٴaE%{!k@Z<5nk{ks2"`׼vep JSQY-hj8[<6"TѰE,H}|?Ij)GS5{f.SHd0lyNɲF19?WR{ovx-9^'=UL{`ܷBe FG1I8@.)QzDŽq\>D3eLtө?o-* [B-ݔӪ<-CGCl #=:iSmИ]=6`k#=uR!$>O1%᭰ƾө_xLS[{ƩFy!0`"}Z=睎7x?}?Q#(N%rӂï[qCw)5nJ+3:!i(7ӗU07E4IHٟp5 Narv.T驂zeicjwYZx ĪZcBl.',|Y-]xǜ{!CsGo<6.7! :@KSt v0j̤-X!쬒{p6}qjɰv1tK=r[WmQz:I?z⩬X}o:Љ\w=lA{JܪqQ.eJxaB[gœ4~"1ܦ{HhT8ijP0g˹c:L"<ʡv`>gz2s &rkg fnuܽz<10Y=#֐oxtct}]>)snv[[˩x/[\e1̼@?s1i9C?$Vzu ]d3U`a7ݰOt!)$xӋv:>;;ɱ&̴X,e4C-C}?B ZA5gK%22z3ۑ^0A^N5<'=8U꡹B{-z2儘[lMFf0.'aCx lc#`rqL+yw_)ۓӅ+ӧ9ñZS-r] o{E[- Z1@qES*7ȹܛgL"7aV}G S[ @|ݢDek?.,D!CvϮ-<{(@ 8ңayםF\y}k̷Omѵ E|KͮRNJӆE)&jq&'V|KaNUz/4F Vc#S6G,,VY \Ʃ[ ]_M[Z8'f mgo4HeV@7>?"S |b2|-x 5f I>f孀Qa|PЀa-z4Ur`zEOKrLPU;}dTמVxM5{LGvtWӮ$U/멖R^k׀@2YY;Ybv12R' g Zdv 1!,g*~mOYrcUᚨczr3< ȐĎX9In\.2Nӹ|1s'}]?$;õǖ+u*Mn`t/1aogd=2N'r j]_JU'p@rjT"frx\2p7ElIR,c얌ݲ,>%YW-oOd)%\kU!ǣh?ps9أee>Nu!@g"$gʳ U҅v1,Q"n@M,<$bXΪ;-Rb،sc-Oٲ7 -rhCYX?yx+sT?KV1.A4>c{ ;tk(U-:B/4|{EZF <ˏ)v"qca=ȻΌI+B#PԗBfg&i*F9@3:S[$OT7Չ)`blgqhQ_"6֗q|<Ӿwxd;{Ͽc\~RXB!:4!@N;AOEUed֞Ŷ/Tv{mdo;ET;L<'9L/0@]9b HaKJ.I=KQ_iU* ;zAmj`JKm$ bs}N/iG8|#sTUfJB$R5vws) B`8<,͛n-,t뚽2Hx OFaS) fM,A$us ~qb^ FkσoB~>+s,?(nRf 6ͪ-˩ؑߊΫjR2/+^84Tg-절1UI Ƶ%kg 6<@ضɿ3]>DCLDoD@ C?u,#l}&:rzKp>V~f1b_ +Ae+<7f=>y&~@Ofn<~|}5It` Ű?8)~V~B?+8\쿮>^Iٰf8[\M ?"}r\zuϧw||6V# {}ݘAdƊ-`wTa&Q?X x蚝CG\i Ņrz[ES<%[v3bŌSY$ ۭs!Ko۝Ij@YW˿7XV;dO#tq.G8 x֍4VϳjvO+:xEzW#):~p}16?&!`Vѷs,.NIuzck8U̳6>V B'#R|UxVNuU괔1"+>Řt'"bэHGqg~dɕz񡝼Ut:-p=C:y(Z6:weP9Z6kmb q+֢ςIu~2 ~ƫ,{݅<1 1bxIÏ}>Yn2]}x"뀐eC<\_A΂,'G읊CisWOzƿ::OlpL^3Kk TӦ0a_e Lu547Nz^YK+{_Y~m&\wݒ?mWze2F-so˨vg< 夳SPû'7Y*kHttWewLu!/&R9roqB ^Yޛޛ^8szv0yg'X2˨ Tbbψz6Y㺅dZl)LӴed]Lx)?x?}+;9V4XFeXrP6<\{[ٯ \*4WJJx. m+yRsŲf_~|$@ RɟW0|?}Q yzxP$P 9̻0tJ=K on= ɍftsR%dѵj_|)Tך6MG{lLe$OM~#tuk^MZQґ-G8QT; V>/+pp;_jb\ +ՠ˝U~>4^>"(0׫k- ;n{,y/ <ha$Z41Ah6GѶ+uͿa\_m8Yo~egvv.6h\9?̈́fr)μ򻣷;5_>gKLC5`ѹ‡[7gZy]ܳY}TP#_zf3tk] r汾bڬ?z9} /QR=Y3d]tW}gH}؜x ?a:\*3'Y)n^sB"jDyf˓}b iCJiͿ ˠZ{շ  JK5hkz8-EchYb uѳ`NgEd-݂Ai:'qVhْB]=Hg5bYֲ"VlAx6zm=@5;D31Bv^}lQ@&҂히.n:B&AsƒW$Y? q4Ds(Y9rY’a A-dytxp/4A7,biR=&KsUbBrNlp'r喦l_ }ᵎfw& }iAE1cm5U5p7~ahևƜUH+e3Nj@Wc1#S"֝1u}LΎ' u<:ţzkiH#C镞mvʪvg} ]l zb-*+&ch\ࡴb3}7 xאY R=o/( qBNڃ'|G|Y`v/]fn~]/WoY2׽U0H_aܞpy1#nX"-̃iE,1$9/Y̦8*Sϫ"w? v1ʐ3VI7am08뵍ԁk~|uQ7cGJ 6Qboz+doV:~ǜu'B$ z\G+oU"m:'vmr~yϭg_!bEXuʄ{3za;c5X֬~sؕ>Z `:zD7\o /Y/!G Qo㿷>c\`sͲl1QؙaeY&W$o.zz]}W5^<> -GՓgӀߘG܄f/>4-dO؏ۋL<K0{z?.gk>{-4$Q/Bac9'fJ?Wæ~9/Exya QXw7QQ9o WN%CS9gUyWlsl>v; ynJ2M^o2..BnISVKo #p0C"T^=y^/erq.ssD*(uoA&#N<9k5A~K wp8YZѕ{r#V/W(orߖ^wZ>>EW1A4taPwfzJ@t?Sj nGTT 4Q|@/k2<twXgIqϱ9|bm)\0q]GL3;R)zlmqA!Pwe_Q{YUcC!W2tQ3XlA=U&]u\V1Ö0 pwou{&lWׯ)1w͆c5cHeEl:kZmj8GR,Gay`](P|,fޢG79o;e]l{GA5 ? ƀ~bYϫG aWtN΢z+j`Q_[\6@O\;+qů#8a4\?JnK:I l~yf.x%K.VKβNaRtXmnzkD b+ƦcEV>QEᢴZp-֩08 ~ܙ,հD\Yyp 7:|ΪcD ;j{,R|kb }wX/ˮqw"/~ì'y(}k'ŋZAH"X h_1:7!\M+yUqa:V'VѦΑ`XxCո5K בeY-&yd"TSհe9;*Ҷaٱ;3u aϖƅA߬'Q(י3{z$m}eom8W=P {(Wo¯1Ņ\UG_Lͫqua >y:O{#[F޼])k•&wnDEKmkG7@sK\'{˽epuK_UyQ }tmYv=$t>)x+bY7kZ ϘereJ]1#4y7[ף-Fa [A363sGf~Y>ܰɿO! o+xys嬓<bޓ2oc$C}^śG~?f̧b8#d'zPۖxqFhTo{^):x<ֽxse>aE{<"Ip.t[,um9Qʎ%q8M4abr>lv&7Z9fЮL'-kgj&) Ă#7^YU"LI|0ix;0$jhYv43>(:˽xYDb?O\??^[| dE=Y X)chAX넼c}|.f>:/-(lܼpVvM@EëYX{$9yfs_I%Ƨ /BxmSNμz2Kdׁ?u.߹ߩOr$BMzJ/f&YW%MY:_RSӳY&iISl>mɴh-Eÿ+z_KԃV$wQcLMTgt1pVJOȒӡSϱ6S)철p#JxU2-lWsqB}{}vY(5ݡs"Ш7jIqJSuD WCݧ6gKfݧB#ADwkŋWCa8sNULjMtNw^N?<> Zf)Vع+^^Nn}f/%yg#>B6];玂\jK&?r(L͸=xX]*<|ّBfur]oƼ,\Fgz^syCҡUEJ8Ƴ<Y l9r[Gar^uuz JܿܿZuܛG662Q@V=R}/89,GʾAAoH·;KmDz]=;wdWxl#'6Ja,mBwJ:BF[۸b Xz?uzbKFﬠ=tMQ%%E$O5-G4Ə)ZY"xlaمx$Ka$<"O1.A/z4U4r hܐ!6>i4[8u򁣚bGXpFL?U{gӏ5@K zCm- 1NcnA5i'"',׀O#)u+>5"b.ߨN2j; %i2Vǘ-W#)b,"8?lz <%Jx" ]-v1B?}J_|Tc: ;C<g<8ǬL ~6+9 ^¼醧z<9tȢǕ.<C*WC,+.Ѱ<{>t~G̜` ı0!d+Y6ı8=*<z@vc[?fӲh&ODT{=ah=z?2nG tVx_8/<=:|n9M ^2$\h=l.E[7:MҶ'8h{Il^|`.l[ͣ,s>QTWm<25?1O$wG˪؈j=ϝ"0t+ \leB?f#yͯ/3Eޱ}M]b7{?]׈ ?C|MrC-P.>{9QܐtIg>V:>TA=S y6W$[D>:[9Ay@־PzVxɒFiptQ--iy.,lyz(fE0iLC0xݙg˚U._+Kzjq;wJ<7CJ.?fQ=/NNgmrc<+ߖB\$bN.tq}9i7#dk|ճc'x9.㼮*1 oulqarJ6Ԟg};06swrR|U[} ] J^:t!)^c%?_!'(nù#ٜ-nOPv1#Mϕ'4O΃g'̀ŬG.l"Nn _Cc } dOz"O gbZcnד G=B <ƍ^v? t}xT.']рy8;[[Uec+bnGcߠD"xif!!b -˂\Mk abq;`f~6Mb@Fu\k龦1{).ʳg[}LjL%S"c1:\dD>5D' F@^L*x$B$.|t96E ZԧiK|wc^r={p6:Iٲ1Zs|,0iF/j8350yޮ޻|a5P+[Yg%]Ch0c1¦9i}xQmtr[iXtT/Ȣ{?Kwy> \Lی7P^oYʋG)̈́y7<*cUlKx-t颬 G: Y$I!#|})~ϊ'ȱS[B۫ɏ>A߮z6d }7u+Rz25olIFGT54p~5gɡ>{*[~bAo܃PGg٠/ڌ]@:Fp.r%K~Giڇz`νox1=r3gz^9! o7Vg`SΑc|2mq"rs?;}c. 3>a,V oK=;Swzs՗+9jz 2رѹG\x8mLoh,L\P0LNT7Fۏآ$Jw{˫D:rC_64z;urdg~^=9,-3S 3bư8q hc:ɞE^\exO WްxLfp蚍D&_lC=qsH5{xPg'w@h-g4仦-޶?pOzO֟ŏm ;ںj#4o1X>I[Q뺢w{|x1b b;eu<;]qYx3Ҥ!d:,X N2ֆ:SؔvX@:3-vvaI>p=W_ !V_v2`ܩKm =tTOCJN*xRyBLex3zA(g]_O= ᶉ||LcJ1)Bb:Xq`-+0![vcyێc\]sBWSٱ>ᡂST "V6}Ta͢]BP_Y ",#(>_v!WO@'+bJ+=8OOvqârƒQIbmwȓրb:0@{CAv^,ͫmʄ COWc :kXT^,fIoP#57蹔{^jALaD-Jci Oa,.XAfSߡ%wu+ww{ЪGP]Q$ _;<̓%nm"o+:r_Ԛl3MƹOr5pgxjȲLOwiPQgl%Pn?A'"ЋM[~in Bqlgtr@n,C֭G"wPXWPcV Dg?H_:wUb́Z|";y&E=s12f nO;ObrŦ HP`o5+y>C%g[M|AΈ j>_] YtF'o=^`w2,'" !9\Yb/ *)c|ZWoZuk+[a܈7@WSo7V;Ei=_oyZ>9>g6z#CiEJl8bX y ]vՊ!t>9;ݚTqꘞvIe}==#Hu/z>CNhz9>㭿\A2 j6Ri|%~8VbBEoB]v T;ڐiʩNm9Tܬ?pf GflC^RW_``iY*Pf?F́xH;{)du5>༖t_\ݜsZL2w Xr'5Q_`!zUȟNҟ'1,?U5E#$)ѽ/\vAɂ$:wf7QD=N=.z樀@3Ax;0LΔ'|};`Izu1mkK_:AQ^[9Й JqR®P[ۆWZWtp"^q݇u%oD֙zCɛgp[_rXLzb*ܫKPcITD#cto|X/H{v{~afԹ/30>fqz%9#ik>{_9ӫȑ|4fyzm[s+? -tYLÎkQt`׷Ǽ:7$AU&G@2CuxԞX~Gt/=?? ?Cn&`$NE؁e>~SE"/=b߯Ha%<OGf$ u#~oLo!o_vaGBؙ5`GD/6GGugMOr F:"hr&ϞCy }Xeshfl隠іgS<*"A9斍 ,XjG`HuP_IҺ1O;-kyX̻*0<aVH|,9r5뮱~[':ߧ 7wPI*7"cDhkx3K=Jz; R[˯s b_gȈqc47ٜmmp%ww}4gtz ;@, }شIYO-"V7(l +A,^'f)Ȓ9x`=21(v@u>lOh?۔jGI:YcOtRZ#Q6w3MR5 ϚS,V0S2\Z9Fp}!v SOF횋eHFBbcj[xc4="OhhY¸El_yb~slp='~N:0y AJdJ,|"%IbT'z"s*"ˋKMVh4|K]HLjlb1-P}XGW/ ֹbWmF۷kodYT10ii3Q|Oo.xCjXdg%$Jh FQ)Su~'O4\J6f׶ E0lr7a]hys$eO:8F)EbѽѲRI:!߻;}ǃuK*MrzS;YN>K}ͮh_ß] 4sm}ɂ0F k1q}ozOX׿O~|VF)y<x|)?9ўnϡ5m 8kNCY ;_8#k̮nc-nxXᘮE42N]tܢ.GfNU/pw]T>=X(nOJC Höj d1L?\ko7ặath̾Mui6`LIaщ'?S$]ذ ^6f_lGpp_X* |a=(\[M]uؕ'gX#t:“k2߿gmp鑧g_3SԑIuQEyCR)wh=),}y!hyQך?tC N:,vHcVGjguil%3"؉gj  ٽߧ_vOairFE5Y}(]5iQsjǾda7xY) G ~N l[Ö.%ĎCEg:(6ؐCE©J ٳnU`iDx=Mc%H; - ƣdeY}lE![Wq +"9CRRr牼үcdYcެacpfxjM9fƾW }=|^.yV960c@ c`7Da󀘜>NLX݄V^,Y}h$] McPZO*_ܳ[yȴPѤmFI]̃YSvգ\&ҩ2<\X"tKEN EPOG[%Bk\v.B1ea n32 i1d=B7*:{ Xq12>8J'~ڍB &u>qBvؤ:_peJFx)-qgǰqu PVŲr'67M jxMAI6_fAI`RnjZ4& =sb#;SYݲ>~ |灷,gⓝ/z#'qc2|2|3otu؋/crl>lYQf+ c) &ˢl c>,v\,)m97[C+ze,ٯ0&Szn~cwE"n:A |oBv qYX47ռ-?K\K>R 84lV2&|2 d oG05|]h(ڕ$HF_WN@]?:2GzZ_USIUG@5;*bZۘM I`SW\x N/i# kn{~ox0a|̕CmruթVz:p^T6s7qq8uUVgS0`FUc<)ND{d;.q={9(^ x5* w߾WPa-J'=[\YA^rU8 cr^tT‡ FuPQzn,y  mvU{@܀|~d"TtzuàS= ǟ',I埗GAuL;͑)YS6f8 ]1\EQ[ GTdHWtGʼTY[ǵCI4~縭=UAVx_%n}GtJs:޳ƐoAwoݬ@Fx"߻;kہEG,^ aVycVU3aVWq5_z?+=(R`HvO\Sm:dqg2VLQ12w&_.ovgT/pϧ!u'y}v~ ]Y+văzɀrdMD][83ٷtn㹖%0/E~O۩SFC'͊wQ4w0});ڢ?JXb,w0[ˆEVK޲y} C|V Zt;- a_z~GNDawb`ze"RK6X1]+B,XP;xUXH< (:n2 mU[k8|XG Nw1R =k1)pY6ȳ۵l׉㐏3Sy4BkkcӰ `+t$[d,;3gÄo0}ttD5*'e58z_晹zgIxp{ݷgI3}T@?J6;+af ^Ũ>cs|n74}~v@w G)RldYF\ۭh-I_5^z%ȃ ^ԂHqjgxq豾cvOp(@v#وO+6%#ݮ;{3irn?<&ЃεlT< ArKnIu)ӡ 8:5sr>|+%*xWZoՊ{?'&_NM.y]u0fW}1#'IϪL~oN +ADT 9NZ?}C7>QWۘQ h|44瞓fn3R$>ևfR$ޖ' al3Qi][Ig(Ug5i:o̻Cc5vtㅰ~$q^ndR]ϛvmz\G8<tjK$0ޣ䵸m_ c\^^>I(0GXb KN< ԍxj{)Ԧ<Hu"WaL;$D*?YvչjևLDg=C}0UfsϚ(\[+2Mޖ+5ծq֊=Ht'V8Q:lhҥ:_trW1M1!֝-ˮQ íٚ-Qm儣vzQ?,ϊq̶M^¶\.^sT;D ^9| IU?o2k@Ӛ4׋r^jWUT¶--9]ŗS {/\VG5|X}iKpW^_eK4ƶXx_TM_?|- ۨR#S6pa?yu\_:{$&v)g,m^OT +-U7mÇ-]x d>7 ȨPEW~=z㋋pifv|gHU\@7s<ȾfcL&lxM A_zaGLpU͏΃?R=GG򸶬zvRPG"vv(9m[NkO<1vυi[N]j6!"d1׆BWX}:++˂ MpaGyJQm1Tk V[,SC'ۏt2-ؙ8.o#y?q(a˕6C- qe<6:8lY[}饝/YwߊyAY [[yW1^p,'Cyv7]!HGb0NGb'9^@K >`Y:Iʨu?e\ViEy -ɼR*ȣedpP ޖ'FQ#6Szx&|t[{BHɊukp-Ѭ[]mf&-V%j@luuc}|C$pO]vr{S$ZzȝQ%8K,N!XDݔXwFoC7i!A Go<,&v]qǜgeEw}s/%Sju7@!4]aNx&'g,AL϶SGV˳g۽wZH7ƻtbkqhz iT$%-,7!M[?W^6=tAK`J#nFOܩV#n;# W_Gt+!:qR58dEaH_p4NJ?}O"w߃ɝ9wE,wa"e}jTJK g{% {c7f۫ '{ͽyEѐ\~.>~n/.oIԹG=놛A^`dW::UpyuLĎJ+zҭSZXWnf'flmml]?^Rg=zYOGWyKW2ׁU~wٞ&d079B%V(}~R7R۞_/TYz- DaWҎ'ónaǺSN N%oI}To׽zr .Ye,"{1!g[~ / 1u4NU{|(N hGon\rm1w/>^@mp"zŰNN9ݪ~g\# _Xj_?'xS3z7F1KeҘ~~> ~ Ki*v}pHRhos.dI|QCo2ϟ ,n o聩Ӹ85r7c',kxZ#L1i yoJ^(6@Ď`]1XL$n#Ɠv"ۙu,][ 5<0lr%`|ae}^Dvp+P'{x@3bUQ[\ڹrJ̶%1?qEP==o=AA„( r@Eb~Qew pDA)Pt\"ܦecA053,eo<$QPalzxgؚbGn2;575? dO [V1h լ򏐅α1xu14}і<͉֦pյIS؂Q/ ðqVg5KQk,jOVj*D͐8~[e4~ BQy23 {KS"y3f)ν=u Ͼ(eH3;cH-evD=,'Qy~pcR:2|djĈlbfHO 룂>~~Dz|\P|{,mw 2zm{ĭj%yr8O!{czx֣9>]gs8 KNj=^tC6.p?O[YKˋ uB6@ yoxյOx]jn* K0O{CmclOoPR8_գ}uuq= 6QomIG"3Bt@fg詶/V"qil%{Pt*i8v<| (kSJ2NNj[M4l$XV]sF W&`lj+>W7FnBH^?9,SSpvF.Vၭr^_'?I^O:.^\ >>~~An^ 2W]T17ׄJ 9Y|c4*7AOhf?!h.D6A <(#ƍNvhK_јnq\ӻ);%ϚxN,cmj{A_:ǺpgrReZDewTר2v w){y󞃫'F5מ]9ۈ(?3ӥe]=8bرHw;RnĔ55*31R;:7NlL=WVDE]} zz\O_D~iDRe]ܢmo n1q.%;6nWb2NYHōl-E9Kœ"QgYGAc0nvKͪ(v@a;_&z@1 ^վ0bhH x0-2vckj6ru<Èlglu\mP'VuT;YRdm(ʐqSVkEc.1H, 6u?,]*y9>3g\%:t~&n&"{da}Z{b(vWBm7A-E4,BgxC2v&ՊUQ-w~zk4+< SHl1no״X̾“beD8 eo6Ŝ?E3#zؚSyزUlkKp ,Qh64c,"PȀwƏc^/I J?zT| HӐCԹ煹W`&eɊP0ްp&פU4j8W(m9)ʇ(G;>^W9Kg6ģYQOe8YxKqUoG++dR.!b)=+!<8V3ƙB@CL{|O0Th[sÝL:x1is Fg:`a XCSԗԝ85KβFvn3RQMZy8ڜۦ^G8AYXkI[6Rw ^ jU9f 5DOӔmjEB^-/Sq]IV4`K E/ .6zZu L$> o.Ku*nFGr8,^ɩv6 [arQj_`p[or^䥮 ,ݓGQ&CՃt=UvkXNjgf'r,E30jgenS o&. Ym8rNM.=*X-sy(L`:g6Eڰ]?b]"`0uXN&$WV2Oz{]{4&/Bjmܲs1>:8b&xp*)=rk{<)y(07N#w]C2#1' #?) ,7b)VIZ:vT8-+w}'Xw@ 1*Kv?KwNL.|Unᔠdh5xZ#YpCl(y|p%C I$x9Xj¸}G\^.ma |}B|ّDKo:g{rzWFE\Pׇ쾸n< mkpo~y@–TlNe0;zsxaxnE,#m)ёctVj6/|н0{UFY<2XwئbsB= `B l*!tGRi+v$?{qÓPx%?5<-/'|4>B*T[ݵj8rKD*ײM5,+!>9 V!^̰[Ӱϕݑx_Z;9=վțWyY-=%/6̵GCj?֗*ۏQe)D6~|E!>,u p{]%ʃ |||| ٽ!PD5++q ?+v?_ +# ߰x}];_cA axJÃ.h87' yNE{wUE-ZwPܪ:6D: m0֡;{R#G]'wJK2^R5(~.\'Q]S #V Z}xuM5^}*QgjL~@('q+u633ǰk[)%ǿj TvXF3A6ރ#:9i3J_o릌;i<)8 ZU:s'yNU%JH;I\y)6_P*A괍()u.5f*)u\ccavrivM2A'ϋ<) .hͿEE9D XfƄDq8Q+\lQp߰W/t⑪&pZ\sOތr}Op@dE0qt׷[ۇJ%y ]^*ACKi<0Vz?WnXZSg?vp-mOlY=vԗ!2ُz pe;іM5kJ#nJki7Ȏ:u>zںE0:a두D1#A淺1(nm1`2Cu0z<؍|G@WbMUtxϿ+ ?>yKD[4.: ~"Iƪ6eG6ϫ,Uq.FjXVYx:MMoXU#E1y<5+Ԟ '?t(qa^ ?u,m.a(n^p;iX\ s]p"Xb>8vJA6^TBy3JP,E<׮54cs㥱ɶgE >Q,Ե(2ݳb;iӴzr.%6,aq:eUֹo2$ܲ-Ku&{Ur2MPُNwy,@O4$,8+63<(zkY3Qktɓl1tĂKSlԘD~1vmp}9!b$;N`w}dzbrC7]A*ݥI p俎ː~[K]*GSvu\7dV/Gx0נ3۪guIxΊSal!o@6QG5F]NIbY|z9r9GŀoN良wwB6 Tg{y?c0Y -W^^XFE:IKBt;חl7'x|`>zjYST4Ybj7[gfyL^j؟hfer)$l7%V.\ϖScqí 1^6,P)"O9šsHD1jnP<.z+gD0[~-x;(n]N} J%74F5͞\~C Zqk"Eнi ̐ tqbޙCz%OWb~NlC*DH/:J݊%B"emȥ[R# I|W:aLq{^h)q`5ZqJqÛ.?ur?)dmX:a>Vja%驨E!ȥfdD")5 ׿\5V}@TGڎ=me>JsK:}j/6f8T;[{;p_cW>%kXuCr,.,`J߹̽Fo  sıi,pmq}E쇍aH&׏}]47Ĥ.B!)*r9Qeׯ57)gpzN]nhp,YRAVPD­G%o%Ƚ1v҂D k߻'.4vɤ4w^p'&QYl;,V_Q ,w{Kke޷eybvT{R}x`9z8BN9#FpR[ 7Ɉ;Y5a𚕏UjNv87=1!,*0oX k_[^&I^!d) 5*'r<&j;-): 5r&#R?|ςGgW,Lǭ4]tZ8s"Y/o&#؅b*\'|tPa7:)1y.CQٕjꁈ<=37 Y?hU=br̛gtvЯ:E=t[g.Ol+vK_.^?3U^]>̍DNSVߒ.˼Fje6W8K[Ѵ &/s^Nޜa[l5w>z˻8G}ؘ;`Lb'3c@Κ)%=S,:&ջiTy,f%\s~U9<3^*$â[dà g1J{c# +2V|@'U4[V8_idx#ʲAW珥UyRO1LʋBAlEz '%S7<;OE1[s tqcJeݮ\C<!v`އ!2NF_IW kwʌF0X}CG o=,+;/~2AS,9 37npmzI%?Q.ͩfl*92}^r|MTo= bB:~Uqﵥ3*v*QB1 =zR0)Ã5^$ Q !Tuoq;Y4|oAF٬LܗVhBc! m YAehHfGH3q'N,ÁQ^>/ߠݝҵ x&̭6c{ 5sW$im+;8;7kO<|+^LwBŤw2Z׼ܤ9':W%ˮx#c: =]D8eHXDý(mP1Wk2Z+h.}jx4x?^?mttPgx^IC :Γв娗'B^$^mNXgP,5uZc=qx ]nңN]vލ6bw2ă \z U*2Vn 2>-Ͼߣ8u'C:٧M.Wބޤ0Mg!T~6<?DMAǤlRt?=,&\Xd^2[OzHuK]ہE0b*b/poKbbJ|𢘌k/|^?ǵ};2c0c% rmۏ@\Pgsv J[9Kw6֛lf$V][ZaKЏ'Iĵn@h?tůfju&R6*i)dņTc8%fe<Ҫ_|׭\9{T<]h^wdlvrlYҿHO5osY3[ȶ7}wl_h,rZ驁lRށ>#ߤA$APtDIW:Y;bl<( fxSx;o]Pz 6xvt@gn$3l/ U^)pd3xLx"$ƒv`xOF#cO6~l.K9:?.otœF)+1 :s/wv b)1:?evnts< 텚h񿒠jo,ވ‡OȾ{<L[֙q VؐʐU淳W#[N}*1M/W##)#/|uh,Fm[FwdE-N+ppl)KBRwn9>Ofq<w ҪK84#ǶY7f]0z|'Ղk ޿`as!c:ʂ ~H/3k?<ň}8"F1 OdA~IC}{n6p=o+ӳ%DO. 7ㅊH njo2#4{taY扷f.s+һɲOSK>+N dlO0*6#|Oeɞunq#3'خNDTAKeZG[i.iZ{ twr}qN{wXɺ<'p-I{enK戸yL ܷ4\yQ׮{࿣.\^ye[H:`hB'q}xU-.d'TZD)um@ ?k6qtд-6sXzГV9EVl&'Umik>UblTfI'2{/#9Zׄy>c*V]Cvs]t͐z4Ura:z/q"ڬNge) ϱkC ?%]"pJ+ʁ>kmX[~ʐȔa`ZJx(mIf]mxϗ '_k㙸g&EOQd)5zav$˂t߉OVխ[gRM[vwZSš"Ct#] udQ G n}eǶ+{ GG}Vb BѮ bWoo5|@oI,r$t_ضzkrhe1pI1\!,NȲNa,rVi1oal=E0sқ]B{xNݵ!Q'Ho ǿyF;#_6X\mce8i baZ6R c۵1Ϗ@'/ِt𶿡2—gUoU>`A7er\ןy~#<+v|XtO0 rj|ٳ2=l~ :_#YbY{3kg`ٿ$-P(1vòlHnFz+WwckսmcCqA*Jrf \_I ڲCNRۛ]oO56kCιgmi=FGF%5fg_U=@ >,7p\"a(h7#Wh'#T/0<&cDJB6/͹&. 6>A)k7OݳRH4')] zn.5GXMf򆘎|z%.Noޗ{XcBk7:yb0FŔz]ײ x 7 Qx%91k{r+s8r*R [2Q܋R{z <:/VS8jDm< 9 sA* gv?C|ݬRBAyly`C _߭Ln̓L}`ٛƄe1WxUi(1)NcFM;ҙ~c?-yr-C&߯4L b\Y,BMh1Co_M!K7O!dX45^xPm!ϳ'?&v[g%}sI\Eخ]-վաfOzq8*.=bsi\ɂX]F^&tGeAM+ P<nM84C~ZћKzy+Y9V/^O{WeM|$ Y&  f=|=m=v}xQ\3zK>/ d<3OnXVFIyS!Ygݗa]1q|Ojf Kz,nI1,QuE'xMCRJ =|CO`V0ayx^" }W/w]oB96ACo^: v/եpVP1fno<ְRd(%m֦Ы3n_'7I8hd3n<ͫvnXԒޣ9ͷђ;ΫO y%·=.O.U8\_ø %9dfnS]h{QEX_}C2pSTԅz)LER!'&^TXo߈cs̛8.Q!=RVȫ.ѣB;[&AwE:`{z{=iSHtKy|sBQp:oek+/ҍ̨cb(-VuXW U{E;Z6B"撴RpL/ 5x6)\x+tl!hs{4a^N#|,pUP1/b'&vyJUF OIV6 >`V3: zBo:>̎IOقe {|)KH )&L[1vҭ1/z;#_ߗQyinԹmCbmXf}[%q`MaC\,"؆-6*pB#W3~낆I8V)8`ub c]/ b}f7D76[L֖63u%=}B_?}_IyCӽt؛v#tTNo䜮!igˮsr9>adNml|s<,>N؎B[J^P:ۋ~+3BS &5Q킾1p{γŎ,ٴQ OF w߮;e;P͉='~"Å&L#SǎQAkM8h@:Urކrؾ]|/W?LwOtQl,ҥd `~0'RNwŬf1w<`9?΢S6<" Wt⸐$:icQݶ~ L=,Rm`v$< N&=CRqp3XDxoo$Q'mxPb rkQk(Y 'V8UsE9^j6 0bŝ~Jƺ<UË=, m猈еRVwWUXhzdV藴U*G@;晝 rxP*8#Ha<9_J0] w`ivjjs=6 *!tJk΍}gF/z9ldyg)%Vx=k N(0V(3|_kd,~xl-≘Y{}OqYgxS.pW9!6=ye d۝B%S}&رO`TEҸVfN5FpE{dmqwٰb Qe] +c94iD*~(uz!1`ճk|-eEƑ-؉l |$Od,[w.RgmIv\W)UctϽa#ֶ/=FU$$| TV/ݣ2S"AI$RiPwl=e_=zڔkGn(wQ"i3F@n׮.4$8vJ_ ~_C, #.ΎfΖCxTTqUH XcCt]>.zZjIW{d&azԇ#Tɳ{:6Afš H':&_(py{^P1{-=x'rN -ב>.w-of}_)gL#w]PzX"M?;dr6<\Q P\uUPl%3 v4Nxe:w2ߐͼO*4nZ<@ ڱI2FR3<ߧ ,bpAQݦߠ7>9c-ݶ B J/ ?*s /W(l{;pn/$xq7@ g_n`=)? >P`{Dvol5߰qpkA2OТ9߱AͰ$IaeH*J;}'LI2HKr?5l/>v\_~/XTX[t>OEA.0'r]r׷RVؒx/5Wr8>7A'9ܚORvv߼E4Aan('9Q/rL*x~@|rY(T^lW.q| m̾x`t_~iQo1D e(qZ¯u^-(բ-ICSIO o ~[*my-'폧CDY 0ix7xL<|i& \mn*H􇘕yN3Yuc`4X7 9Eϱ/2uCðY Б^$w(3DS2eڎt,>PpǸ&ãYp{7K-L2W)#S툗W?<"޳B皘Po[SCa;hٞw{LӳRICz8.^ʊTHSmX"Sy5GLYg9JZR?Qu&6˞*ܫE9[kθ'aⷈrmOD"oϽ˅"U+"-QX8ODŽt'Yһq}IǕsQ'}xJ9qܑ?Dn| wb׺x^)xv%\veN>4Ru 錒r1rۉhkeшA!xi^WNwEV!tǡ,zܜ2Cn8x,Oț\L00xv` ݨ5~eޛ~N'dr;%"@!}NiޢFtg9;4dritܜEs%JoէU ׳OM!?>!kW.rKt4fd5/$>5Tgu\ǩ6;]O?u5 {toy}i:[[(F4YV5us6(| MqGC|'!:'zAqbαSf)zM]y^jIzYhE~1*q \jhy""t~ iuձX DUrReVϧx=+liT֏(*<|cԱJ۲#}.n-V?R*\-WXA|?]Fq@8 jӕTݸFK.qt1S? &#Aa]S붂҇xQr ʦB?fNg4|t{B,$ΐi}V l?RP]sGSdP$4>c "75r:V블+{rvڨW ; "r i 8L![XRi\^:#ҩr7TߜMI6bF&7o7,raD;zWL4̟_]m ݘ<>#`6Nm/ e][h3d+3!4/{ :t2l%? (OXۼrDj5vC!*,N?Q#O*`\9HR-:.F,%jҾxHy9+Rt_=WAK [:9%3s{^ҚElD c Al}6)Nɖ'/# IpZ3EwJl}t<.yaL wU@f0*uP+zuWyhnA#zL"\97u& BʇZfYE &bUߙ.Bҁm8⪦w;﵍yYLg j7uW ! +9۷)bI2Ht~Zr'Er`7*]arTO+0j+<B4$-fnA盩M"᳃yKΘGĕ%J~f]L_կ-Lo)e} KC[:ijg2W,O3: V8,W:EZp'= L-Pw-L7c.sre{'>3 FYR2h2(}ӰlqMȝvQ: y=EG1D^cRU0E$eKD:ido9.7<Ì-ߓt]^JФ|V~#7oHUo8M""MS!a"AFԥ-2Q}`+LPNjZNH=a9(bI;0[ߋ vSkhow&*ħ2|d6dQ ?/}\H ]:nOs}2RHcQ=g,%u%wSu^TL5arۦgR˽7ar|*c'VΫMQf$?Θo|#<8i8!5p)7HSr<RG7iq} o\\?O~; `ӱ]Τ m-[܃Ɩ WPyV[w>qE;w r͌^2 ?sz}z LϷU$Iyr[A;z/ !3~ȇ磁YP^Ǩ(ddN<4[ۏn]mgöA̞tv]3X;3}O^`NL^$7WE߃ SZGbqňб1F %~厕*d/4 NWRI@8^drN~5js(3#. +%) w#o GRQE(bꕺL7!2=yx=&66_&GAڷxk_#{]G${_E(U([ ۥ37^d[KDUݜwOTTC[mK tXnLܿty83wp*p^x=jMi@6ڮ\9#a3Ld:v2uÃcixm;Xٯey>bĪ]"#[zqDjl'?/Hd<~^OuY< Ice/DW֋>0!s;Hp{]3F\33ɮQr]AW57Y*⮚:_V 9=9|]]kZsary]}j8rꜜo]- Ȟ^1W~xη&o6~q 3E6iX.9䞃os+qcY["NW$͚gM-=M__ %6XK'y]fK?q(=7t5-$cC(Fčo&\-qex{ݪaNz(4ג/7ЃMѲWHڷF\|<XZrˣ=E<p'O=,"r]KJn\_yY_͓2}ƱEqR7]F:^:A ӰEq?r=O徧OY߯Oj_o o,⌘bi|O:W&xǵȌ9ǘƵ' hT@)ϛ{musqzQAYl  xjP7~1{xREWQ7L4S rR8IYwAn;mEsri'fB;;q{3ƷEp /{:ef0ޒr˽ '4ϥC(xk\~FHو\.LS ']D|7-n E?O.m[}\ogo {2mKN^a;c`#>Uv5F@Է2|%h1>5d⨼$jo9KX*Í<[4C[+ NRp΂7S<}sk#kmXrYw 9`x-9-Bh/D]{j~c(ǷGDYH ISͤ?u|<џ`V*f>M.ϫ:~xvs61rBǿ ' {˹:" ^qߐ+Y_*Y#5%N dLWE{Oj1DK{yֺ["Zo"6xxcXK;NR9*otC(YFBJ. )98wO7hΗʗOw5o;M5J 1Bq7N`9W#_/i,-^W.JIh Qf;݌"&\N\G?v r)O<0ߖǤhk)9D퓉E^Uk3>LzJ_p Gq9t%ʂ?2-B]s.6buֶ OyJ9M'VY(;A۩Oz)8P}E< 驕['=YY˽ ZZm)\nL)C E^Ɣ"R&Zby4hSۗIMt>vu|t#%go 8ԱT]bOf4m c!^,pRĭeZ]/ɊQQ_}LwG Gy-|V脈*+։#%D8Oyחev3;M_w75"DM]gNueX ġiF"fPF=}ݩڀQwZu QUs~p+>,~R5ВNGkGx}&+r1޴i2~Ӷ3. WxGLm;m|"BlhiDgU*藯CM OK{ 4rg/Y':eK[:x~CFץ3fFT];͙Knj ̼;^uUX7:m{;ױ|m`˱i (TSqdg6e%R?g]o>d tyHt炴[܎N,&/4,ʿ2|dG'PL1wQŇ fTY,VT[-X0=]+iʸ);^E\3Ҫ;,hݙ>5p!"~I!۝ `T74*(c#iPĶ*gj|o@UpEفC}ɰ|v[gM$3Iʜ*"27ac[?^W f^'zRM52,ak88W Odch;r- Dp=ͶfPr}:~{{td7O/Lu)<߱X8A7wSy^_ E=,7[PF<;8~k},kOo2{x; |erU)c7F()qLvל/2l_׫ߧ?ڨk[s %78!nP=cM8b{䗣zvM8Eq#WD ηVQ8E㓛 gl=N^EߡLT`5psFr;D:z!%;N\QjLX.\X$gUy˧:鹈ȱ~r?.QlG' M]69v0K9DNe˗blOA?#Ax}jꍞ r!bbU kv96l8jƎzI%{_K.o0^y~ݚ!v0_)&C]a7vz\?ܰvcE}^e64D ?lmݞ~} P.?);:!raGs;&lc > '>vJ͆(?EµH1(ȐV^+*\Nn$ _ǁݸR`ktgˑ+8ŸB(JP'`j8,-T8pEkW6 G3szy61JZ=Kb'ZN,9ڹ{JDԓ3"f0}M?g˿1?(_O<._:`;%T6S}ivz"\x%ƌ\jȉHoپ/W"n6i~+O0lm?Hu$e[Ybb6E-Ꮁk:1)$3t4ErãF%.Py^POR|۩9 Q٭CXto~LGst5}3_U?|N B>%qC䜻՜r*Q1e RGO}YGCUT/^9>) u 3ý#` qh}͹hwc=2p;==NJ|^{W X!"YwzԗXg˦{!؋fsw o|NrDz=7˒wv`"V#bRmlwp/\spo('^qe.} *K%S/䊯9W;H{Y_ҰfZ$67,6/—2 ծѱ8-b8u$ ?ka0'RIJrɝlg$@DH3sz[thâ 2&>.ݶsR_N黗.ˠR8|CP#XD0t|\D"E{ H}~)۱'<,Ε^.:k?2LxLԠSCGxdP5ʼnf#$^rt$j<;X*mN ?̍g`3Z-_&_7gٿ`ءJ*XENh`Q$~STC>I?#ӠӰʱ3i-(nWOT&N7}~)1z-NĬR3[߬׷ȸQesg>~喝ӂљ|-}m96 |_wOCt3olIj9$BrmQa;r} ?u6_; na61JaࡠYs' }@d/7ÈXz5MB;~.u-IGE:|_۱me1Wu,Íayl%Vk8!2S\,]7=l"<鑼 r.9[?P\l9a O~H7F'(IǗGގq {҈&T1g[.}ȍq7D^^NVo>e7R)9vO42{ڂFxN%$#"sr9OjvrHI^.1ّlc"8GYt?96%pvd9Z_L7@-qIk|N{ۢQgv >]'[?&KLnuo~j.ƏSWՕR+D S-9syhnrj,:8Z8-EmCnݮ$F?pFD$yB2Qf.9WHA~(UT8Kã87D"bSb4P"rƊ>Oή{sN!1e{Q :^GaFm-uK=<@O{ aHCpdw=!9, P< [^ xжIѶnYහ-*SߢۏOa ߽Wt*-<} qK&JE"&ג$]`M0hoVU~%Ee5^T|./o>I%ǃ { ߥ 8hęU8:ࢃ*;ae3QRZ,h*-ֈԪwUֽ݂*tqS떰q,_GS-U@hT"ч8A z^΃ .̖oRt|_6>NUj$tyC^wŶ6(Ed;s~NSDI \ ꙻRA#Տ}ȏ%CYZ=HϔQ_s@unw}љsSx o#zEޮ.;;yJTWzSO*݈+'׿\aB@U)ɲ?JuW<8JSfzGa]:rN(}=_knҶN9"Qn03CQ}'Q`:%B{j?nG=*<^orHKx3؏kU2wth+v=3RUɟHWc6#;w Ga5pTlaȒy8z+g,omv:h70^+ȬOO ȧ?vʰ K̖[U(ᾟ4>tO% /kODG8pS~e h(?r,!uxoR3㨍@IwX__;Lt/%Sf q[O{?zin:* qzH>0ZXu7E}603[.LI aang|4^5]H[_= ;,n{"^S#7,ezzy9#RNBDʧEJG}zAqIyzWqݝ"&w(,xM[#zy\CMhv]r E#W_?4MNKD"m4 IJ3|x.Ƴ >[4f|x.b;"g:O+k2fr:|F*7!akmA"w oHײ㿆%c0EA%.@Fnn 4/qy[/g;82rvh_ r:q>܎>!X-˙A> hFDݲm^ܫ )o}' 1>I+<a:cZq2zƼ=S迾SH(=yƼ; 57^zvX)/Ijb2?zŏcP ךg(a>)I w8:^;ϡpM¡ Sjs0™ u6WHo_hF>:xԿt\o+F7Y0^ɠ9ߌCl4C5\3T;E{xrs^?t]s^m. `+'9gyID"1w\0%~4QKGKkPύ- Q08%A2\PT9F:`t#h Dœr@+oD9`硈`d$ŸN"B? ¥ru+=8>JF27.8Dg)/Sat=vX;a:nmKӛ&H<8DA W&eAнt^tUlЅ3/IGz.G3$Q+8=W/tPCM ,"Ngx8=:eܥ^=q qɮ #qz;}pk=Y0W+}]nU'2+.KԦt݇ddk[SC#c+un@:8悏D1{onF V\%W˲9f)uW+L8uaڞ`a)Q+:u U"R>LS\cMVb0g/K湹 z8r m^|/2Gvc-U4|vx_zM"sdKC п4~ u)/qM%w3:h wvtZQ"=v܏-FiNxkFF8d}}}کop_NsF5-/W4 CSĒ,h>_8oJ?~'=r66$4}䟚ӑMRɚ+9׭Qr9<rU5)>AKi͖ j!r5N:VHI^o*! :nY%;eVShPP ``1P[!Owst(v ĵ)P@4Q\RTs {gVb3) -Bnqa\FXCYnkЫ%"[.ݯy (`u+N>qĻ۝uID!燶{*R##bZ&ӺxRs >cU#[gݬQ"yN'd}! 7D:5 _O9wC^E7?S0Kl{1@Ӆ)O궈5mh}blӟׅ .nry`[,䕞|)/%6OvJ{Ǹ.y9o%Lo K~7M#\Nvl̟),J$ݠs+"?qQ}C'//kzozR 74XΙ-U~S}Uo3/u$f^¢`"Թg}o~^2,`N/~N115I+/>xEc+T$xaی*ѻ'%qE>|+q{-tѿ,-_2 -B~>dǃ=2zCt~=p{oO.z0*k!$DAWFȣ2I)xO]/? q*lp:&>zSr{qH3~qCξ҆Cj-S_RT9#MIlϡm hD7uKq0.}wcC{bcЎj]iAXEԀ=|}A8,`$ HM$yhī?Nw%1O@4`4H]Aňm2vL?9Q>aB|؟SY^J) 7Y O(b{EEy&a{׊Ɂn?F`B\kƩH9| ~S>M z }4kK ݡ_;!ou:+EہeW" r]~C^"G%n[VϚ6[2m $9teL Ӊ},$lmr;outC]^[7;IE1ADtXsNmbsFr5w&8͘׏4~3' KoYV,9 Ne|wjzz4eaUmϏKg _,FT;Y#ŭ8uxKfDŽ! L_B0!"lE3S=o܇&[v|PiXQ9JnsP9{jzZ$B;k~ acFe }\>ثr׌O5g~kh'ٷ&q:ȥsВSaTˢjNSQ9ߑSt)9"tc,]wLw+5"rWH:%χen [G.~jFO4sWs^ or ^K)٥fޮ\EϙM_fDCDDs6S e4]*:aL񸭧4;fOނH2s'{nP_(RF?~i|:5g异W[~@NOX0.o6Emc}nbߢ\<: f|?U~7z/EY.>p]:Fd?Y9s*q<!S׋|V@^ .<OoSnEƳUGCWw+dwL4&,98ei% JrH}dI$k ^Vύ&.csk4_}rP,I HQ; k[K=8fĀ~ajQ~ҹ4` k;5󮾟n*;S/ Im{ 0gBOֶgy'2Qt$+tMSσ끕ɖИ=󤲅8|On쀖zP+ >t`xJ|m4j`pO\:bP\656],'"_+V;?=sG*k9 VDX̴BdUOkO-w,\Ku]f3:cuIbC d\j0hl~|[~.IIWC#d m| T/9h@g;ܗw܀?0_oȷw`͒nIPtv?_ >G:Lz:vyn*C9^-+mO8e$/iē-H2X" >iwD@)Eo7E9vN}wjo4iiH驭-JQNN9e|L&:%OYx@ےjEuఋ\~Ӣo#Jmr7*N;ܪjDO=tj$B AvJѝ'n ~CӯDnӛbbḑڗ Yga=PĻ9J[w vWYU WW]kkʳE4tBE; S"<,L?$zinBJ:)8PD:ӬłSHh>w9^>{e +07^6=8 5s ӏjzBȑ6cvMAM;#B;}*@>0(҂VR_=p+)۞Ϫϵ!|WyؾN[9D˹Vz]ܪV6cS!/:nF@Y(Wkl }( U5;] XշKIy9Pd7# w^{U= hFNM>ܱ\~l!F 7)RS|~@GcQ-7־NWJp+B񞕏"fD-fT\'[цsB?uobfM^NS. `;5Gb,{,`]#o/m/oCG>-\W;x #l©Ogꂅ43r!wg _ WO'kDRObz[o>5tDwӓS ǟG_il[(DĵEv8 3oӨ}]kKGȲ>}~iZoo\Ob^Pflv va : ?r{~1{9Zqu@w`f\0 odj%%݂BXp _[:?v^ .J~_I܎jeh "X8Ǯ5%;+4IZ9}ia~"K{vT^_qKƒ@mqzސ1)D]^k O}=m# YWhgQ8GX ص<418-l1J!_S,'DGb5D0?ج/T4nhGs zCpq5Nި̻XJU%HV zks7y"u*KGMx}O3=fd``7emN܇CoWs• OZ8)¶s\"TcHFWv'[S윟E0v"3֓氛:^xIOqZ,"/V3j}UrLQZɸ C\;r n} W?9kA=N(* 9\ gcC=>Ѱ"R]BlVTOwX©-@_4ˣ܁EOaDwuArX9eZl;!枆K>wUv ?xvH%OUj.0бǹE`Vi{Ys霟D3hDow e}3jziPc3*S[;=?TN=j?vHW㖛5;~,VuO_4UT2RWy %'fܯ[))M~m%/OȲXBq-I\o|snD\~?:5F] ag"D0EEҋ(^h |4F'RKy~8!~E@CpwÀ1@[|n>k]1{6wQ<@xGF?D<.s[Ahn8ۚ0N=s4}o_Orjǻ |{˒_JK¢.yM"h+y8z?E*⸶>C4zw}\8 $b4|+(Hq3Ja}?OgvsN:"i{Z%^Gj[s׃=%U#;q­{1r^^#t'$SIՉ?r)q'19ܲ]l$1%no.B!@Nݭ*W^rifa ,xL=JbVnsCt MaεOrV? @E%ѫV+"q#U8}6{PH[Eɘv`y ) eC;=ysI'MFHNXt0҇"`>pnUSV>FֶSLxد. B5uՇEZe?؆t)>eUy' oF{!6N04 W ^*U9CWQuk<`r5bڶd p2|37:B`xgLuSpALFqܾr%p=F//w2L JxQ&\b]V\x ;=6y~(EF _oO"/fཡA?!a` ߶,O>q[l:*!8vVt}CnΆH,]+~)Zo,I$% $` /]5:4}8 ٕ(96ANN D)Mqrˍ)pGukſ4bs#U7?OO{-R#ۧo])x5F:!BDE$Zjɝ,w%1PhFD1F,قˁ©\ aM{h`6ߋEtȬ~/ĔZS !׈ٝ@D `S/x{9 C+ൗ^RSmJx߶\mEsVCeNH+ pD4Rf۪Oe%rQdbK%i{R/Ny!{ͧmyLmafM=8޲({NhtGS;HxhΪkW"ٵs0$5ιG[p2EN"r(=w!P"RQ;iI9nD#Ncpt;O;.+ݴF}h9p9O WU-JsB$c6;lo@n") 4|ƝПڡ9{C@;mqꄻפ7zSw»?/5uBNDŨ@; N$=ӖFC*'}aD|B D6uxzV#C|v0#7l/zDaS§T3/D5L<\OԌdg(>ϳ'.Z_l| ܙx4xH2 87,uspiϷ35xWΠ?~8%4Ŧz{C_G:}%;7`w{.BPrj+5 7waq O _56q?) ;Dz4cΩo9K@B=(t۪]⍽Xn&~8aʑ x‹ 7| Y}/sбXRg kPeT~`Lx|&DR>A7T喒0O`'rĦh^\wVrDCpLG>l }8)"/JД$<oK1r}JwQd]2@K8pesxV#,n^#>RDkNm/.1w+а4|d>8taڣvR bӦ;Itq驟:.A-s#|Z}Ŧ]/V"!l0 /;8Ռ 2_ߧcˈIlbbNJx\3w懾jgN^5t Z;Xa݁8Jmo2fV v9ϜR7NU؆CxYdaK[Ps/ i!9Ĝȧ? ˀ&z]:8WZ.zƈ49vH= /&[(DButdzap~Xcv?I@41fH0}^a7gJ~)&Eݵ ef]I,MNX6ֲlr:f\n㮵va0Gx3(atz2c?ӔwlWN #AH\=kla 85sGʒ Rk@ت^ ~SPutD'FS"}|}OOx-dž 'O\~%Y$9:[X}XĈni2viS O=Q~#wa RGrr;YA/m!("Q=rYMۂYd;Ca3+/ra\:FB(9g䈺+_+"A$Bzz->n-;ol[a)W>#T4m2[/m?W:=_5rr,iՀ-WrkI&Nr l[LsXߞAX =eA?C׿'7i_R@AkԼE0iģ<49lL}NHI_R..O*yjݢ9I'IQAS&Ekai̓ y5=iǻH]\Oﭸn$>ݕV 2 9TuM iwC"qZjr~"rQ"Y^*}Nb4ZrRu7v̴{NuOa"dɄ4BWxνر|ݲ QNeO XyZgj$|ơa8s&$vQ_ɩ;.Y2UB-$"y} 2a}u·Cdz*|}*]g&s`Fl*}öיG9 ^r>~gʲhgѕr|zjjE z@C9i$##}5ZE߸#k(G2jݽϙ*J#ymrq$'djcL]kRhGOyȂeѧI^֩AS1Qklpw3r=p,i9bn?N<3apMlZ˜c(7fjӇ^.{P)G1;J = \kj"hK̯+2[@G8o 5B$$;[퍓D{ ^op"=}g#8_qM=Ysibkec9( l8i$'AXpNe<~E脐%.H6^+z"ճǗ|g>3'լ?DOSٰt޹QrrѭP?1O|N;/ظ<3rng&z Y;)ˤ|AY3rD?c'ma>OdNJJfupM(},EN`/xHʋWEX#\_Y2,V~gEh,1; }$C9Ӈ77_~ I"G=)Nd}\oi5n=521M)4en(x|]`{Ec㮎Lbn.* .7a "ܛ?HKiw2_|#Njx TL;CGtWI+ } ~dE#D mEb? rml6׆!,9KF">S;D Mf j5d`d7AԓN}<86J߳yQ&:|*7VyOCZvlB^B2bW%& E9Xa'r<];}ZOQG@WLWH]QIϤ2mBO 1!>( ĩx&?:8aA"]/.V1f:#oItϕtx@c“c{+*?y[.661n |E8)@L|'v)'>GJcDj{nZ,j]Hnd-600MEDu俎븈w!Jӯ+ڎ!&UqhsǧS FS̮WaӷK#1EPwKji8sy]nȝ";Lޢ~Jv5N ӥp,g{EDն5 ǺEۯGrI53Su[y4 Bֹ#8#@\, f6|"KƋ{ClaC9i;]syr[ev@+g߾@]OHj#G`K"r>ȋj\;'SZg$'5ϱEnTAEJ?vշȆ"Ӈl[d:vqz'O2|"zeʧ#"Usl9|'5g>?%B+‘9;@8}zJ@g\r @J=sof8x%j<2K:<9-mseMm7+kyH QAi#E}|1`|`,|dÜFOX'4Ch0 ԧS-xl0hX9հU!2כ&q Z`Ee'Җ {nR;X<vKxiT->й%lg;[>cQ#~{S~y>_@ąǔq]?\^Z*S#b%A&8& _Vp6G.uR WHtRD?'lUh=Lڙ:[ "KUXw[wfߏPV-F(X ھk?zsfvN+ |#Aq0ÊQLᙧ|JQ {Kib J˸ʿ#܏(:|h0ʑnsM-WǼ"s'%:ltĥ2D:|\ß#1v,.Pii$*i[Z/ܫ|MbwM]髢 ?ű'x3XeGT@ԥ* w)\3Q[`Šh۱cl9Z<4rf;^H<"vxVeTs;e-gn֧-0AOMrx\eV 5!Bn?Ni':8>?qڤW"t_;;rz|n9b1n~ /E뭏:?gָX?\Df"׻rBLSM}-{\g$y@2L붅rӭC|"V@ Ib#J;helm"8.D0Olzш=Rn"AzKӑawSn Ґ\UNqj5QLn˩nVJް(!.Ydn{Ђ۶f)CMnKSg!i&$CY׹+܀;hn_$ey)6l-X8Zۮ9&@?H~+4V>#OKM^7HmZo V-7$S{e멯Zu^v+ⲅsaݚYh;KeԞƃPxh(f%ǘ~nMbݠwKeF"OGﯫFrgL kD6TN6v]c4 .::ƞ0+ڌe+U1ͽ"4ݟzlj"ErhŜL I38Ŝg=rFrd~{՝ (#A#-Ov Y*?f_Du8\KBmM8ԩxPjCK}{JO,,z94O7qڣ!Py/6s41?y:=#7wiKngt!(;\#x pzsF֍;02i颁apw=^+"K3?g<"A&"OP#heN9exN{bךWz;rm;&"u^>lYUpF<f}f">™),){ٳZ,!% ovO߷5cy_뾊i"\K_RY{ y sCQ~U{}GT9 9?hnNLṕrWDIMAwHK1BC}sl \eu$|!BpM:0KuieR76d%%xRC̍EsjC 4,Y9_ V1!O:?[;ʦω:P"-Ȍ>l7}j?u=N4i#cO,aT#:m<l#_O7J[g_a R"{"opʂHfq8.l~v/{ݍ[-# ⱘ:֜AoQ{7͈ԭrkS<PfePn|F}Iϳ<|X<K|Jr "d h6tuMr[.\lEԙEΈ(]C-lzVӓ,Zfx[RMς;e4": =a~˼- eoX^dE _m(+ rj;xP,m/{;]4ωrMNZ)"M}3?GkrYDLŏqkvB'h6h3^Y*2(rHemOS8'X{&@5?~Dq:#`/%x/hX+~Q=>KkK"lq)u"-h뱎EZ>yXH=)D_l3[GFY%v'o!ؘkb$4r97[<" ,%Yfl6{Mn0osoGw @2.W_fZζj{70pHJd,^DN x 9?c$(1~lh":TH0=;#& Z3'Ʊ3ߐ^Be:PsKt]u ʿz >˜+N7w Pn}MeNI.@T?bi+js3Kǂ9t{ = Y댕59xӊ3W{7z=q6wPs >yި߫8mxKj"8C }8<^I>utuRU!4ָB+ t<}SKeѓǁ I'U-{[q'?#Cj oT 2\."n/{y@WtŖHCots'%/ w9v AHF/MBVuArԙ4fU" ę )Ft@qNl7)^ oPu>;7}7UJ( GƐWtdj+ߛ&vzK3O]v0zFL|fDZl#tw@y=z}5mǜX,ȹp? Oq]fwub[CS5HowqϵN>,rMO}vPcz$K\Nw)FC X/8SY{ )JAtdkvwuYw^NkkUוFb9sn}SI/+rMܯ`IcQNIN[T:V|hwDK? R1S9S#DŎ]8Tk׋K1~]{Yw'"K4dtDliGЈ$+<V)G$Jc~N¢yprZ|i?{ri%|+ĈGv[;;ZDX"Kzxf*Sj_J!GȵW0}zcC(88^ *=5w"r]آH {_/Q;,G0"6}]YJ ؅ФCO:KhyND)")owkSꩬ>_ԋA|N9b `;&^,t+ '.O"DN):ww>+&/{G$QEЩ}y q;K ;s0tπs遡Y>cόQ"1ti)8xOGgguW ?N?K̝<{ /h^JFjM|ol3Ya>P?4Y9#L۽ "5nϛH-%EZ\WCk53eW(=rT<`ԴS'Wᑧ.azb=1w|Ox >7b#}̅(J8?xy /Pq5)4 Db"] 9R>Ew;2όܥ2#]yօ^}+N`?};taw?wrϧܓ-R#RBPV}fLqbS H5D("-!k *įl7v〆[ DGٞ[<}#tgD:בu?NmmO+Yp$·`$)8t4#ђޠmS*yON#X{U"Kku2&^^$74 }(e /뽬yYF4BMh+p(֯0Fy(t ICχ?e_\TovyT)hƖui&voa^`W8•c(u:9I`:lL (OU 9QoxV g~ſu̞JşbR:(u5m +9*=G1C=o` 0bZHP͝=Nhp%9]b5RCUOH#"(@?1q3_#2=}6$˚F~T8W081ߑԷts#ܾ:[k׮(\ǢeNߠV8?9nw$'q}Vq"S?gD|*qX9 :: Qu[<}hD Ks/[drI SB7vzB N$QBnsMg,'="lw'' ՛LxHrr 26#EtxzҐf܀۝VˈkOlI}aW:̐z ]=[0^r4 `ǙOnjnw 4aԳaO%zHnj?.v:$LWSFD}VsiO񣞟Ѵ3)=/?Ek'rqeN47柤,2ٱ.n9\fJoNNKQv>"2"ta59KT]Xu~ 9bg*Vw0!w>tcE>SoG{ %/t,L-iEO'OzoB/ȕtF,Q͑|*zT{;+"*<QsOJ&*u潐4<++k$}=<Іk.._֒ R&oK94=jnO6[+0m1E{CnEnZd͒U+O#tֈbs} ;xxpޗeϙZ;ԯ6AZ?RÍϹSwp0Tm|S}8?on6A-O>]<]\^ \ao)TM-~Cb^K1K}RKu𽴑Dհ "铎Kڀ}ЗC5M$iߢUPtcH<6V_u&mtоY'\I~cbc_3/hDdJ5ɿ44OҳoPj:?)~ӽSnqO-= bj<pJ"#"a3LT:j:}}`α}Wz5_PitdivvR .5 W~. E.$o4QdHrSHI N栤Os?eѾ'?DFZ 漢m '^'$:$vLqQѻ4~AT\ϛU}2֒S}4W/j`v>\qIĩٞ>r& ɶE`Ч^)Wu~Hs%L}) /--m/nqm ^k4_ LSqݩaUȿDliWbMbU<m[M]MFZi8nKJA7v;.J1ХegS~#yښ*xKUdPcxE\0ulAH.;8go=#=6~{})=1 i1-Fs*Y nS ۙhWծnºt ʭg^#b;ߵJh^s 49xi4+I񗡛$$ш2S1nz+cf;~ME6̚>pzf_xMЭew^S:< YMR~O'PFun7B r􊋼|FU+t{yA{#6|: C:l+߲Vͩi0O?.[Ӫd#6z#"Gqgm{DhOwY-biۂ$raٷ`Sⷆ5d@?/Ðo{SO""q A-'p~MxrR25$A򛲟|D8Kbgp|O˽e"VDnZDOܦs76Ŭ|NP|.zZzqbϒ_MC~y+<5 qpHl6r->gErBn/{Z!r2a\B~n:rZSNX_~hdrqv* @~UϰHa68tЇV\i|Q"rh.r>EҟdMR[Dky*>nSԷ?X/D6 Ƹ7;^8_H\&~:iN-2˳F;Ą̧hwuq]n-E{!)^R}^$G3=Lb/y})kR涟jiQ?90ثE| 7z'U|_esQ#(?,eha^Tb{ƑI#Ux_2KNJcgi7$+IsP߽Nh1uP;)T{?o׷/mm]yq҄~7aݍ2tq9ay{L$Wrݲ:KI ~/ /BgwJ%`7-m,tD^(%q`:Q#Nd!HYws+kԪکjYU+!vZbKE*M-⻶yeWLjv:d]MON=dt^[jA+ґx˛.^0洀!{T)_'o/:Hr#njظMD•#q.Ӑ9\qy^\%"<qslǵ01gڃ=ؼq3<9VT+؍3zt{!8l+Q߶^Gjxnov7P"o>;)s/ UY%r6A^vJ=`˵f 緋G/67.A#p)8Y;>^(ͣmCeh!S9 1n`7:{Z]k#rhR{'Is {ub{ F\]ؖ?2k373"Gchz 9琽N9ÅaC8^5"s(mEFaSim*  mUЈ-yU_JI!#: ?#G`n8-bb"/D8[NJɳ]hgq@qߟzuܛƬezsc>n2qS̩d]3L8&Ԁj_~n8`NJ)KҌ-RKRcIp4 G!Ϭ_~l9)WQ}t9 H `'Q[Wi~gHhHUQ_CH(OUWmEلvl{n$SߋFOIvUZ`>)^XO~\dl O7jN-]+U4pUF%r@Ů?flWW;/`}P⌜KKMgz^Os 0MIK~>(6W:=e?ͭ^xOvg _!6|*gH&6ihmT|]Sꐒ*x<+OS©HRfD,~T]6_eQDPJ7ҟT|fWiy'D,_6gaR`iy# 湺=Yʨ;&E[ک%wBtHAvg `78՘c lj߆cNN%}.*=4YO?}9es𜸯O,aD*Y-m'D0%D' b͍NpSj /Rs7MJ}K.>aOeKSBt=Wu^=IpNZv485Xa~$yy`ZÍwH2}6}-_ tu8lS8"^)`C,bXrկ,m͝ߺh)A](8sVOx∇Ӆ)M}ji]3Td [@"~ rcs8U͸C*s90w#Rɰ3љ zGʷoƵ8D!}yjںh0MS(8_Rݦ^&`ΉQ BY_y9q oEߍdy K{i[dxgwš__ͱ႓9 ;նDq3A͑4=DT`OqsBQlȋy,)pb޳cEz c:y9:stUhX P!vky_lm4W&bU(+9/q6IVoծ^HMVA1?4eV=yؓא6'ꛏQf.<`Ow೟tw_zuloc4O/ 8#EeKEF*Z:v+<^}N*>ϥ5YmPy']s܆Aɟ,\=QϖF)+ 5τI tS3 PsJ<1ף#v1%ARboW~7#溕&7J#zPQ]o$*h)~PB*}B0v2MV8KiY% A, טfsẉQ!ύ s,96?Yc{<א7tM\U4s=;";FS9m 'pP2!R>Zr:r)ˏJiv>{}}o$kl1kď,1o}9$[MbCD)EOeCau;j˭Fz~xr>q:]};cp[rM[m O(`$+-"Zxp?wƕdûkm9M鳟nc)޴j 8mz:{J KjP['! o{5$NțfnU?khdڳ" D?*.;.rv==x*Br.q"ͭ+d9Q\kq*"Ty?MoWrt:\G[o2:8?4=nӓ{j`giW8r5CT1<ÔXdwtU^^.佢D\)HsJHf7^rݙ)|:S\- 4|;4;9rDXδY"󭖭"ῤ26SXvN;]rQ0CM iGc$|VaӿSVՃ l|Vn&o\L"%t:W4Ο%Ǘ~ks ]#*\1?wfox ڤ>4xM@EM{PHwU% 'vmQ>W,ډWܙ{!Np愇zeSs;KUYwfۈJ~զtU S\5BsCo6(՝$g|C] ao zajZ9rthzίC< 'y qE4-H㬠Rp=M8́6pvѶ=rA]FSK,yy{$}{^0}]"MvS>LcKXg\ڕHnnD;Q+GDc[ґ1CH΅^`z9![""8E }uFJ ~\4` S[fcӷGTF@>(C:Bd s".# ˵8^ZTA$/IP;wXu[5Q̡N!|I1--ko亮lwZdfef*L^{. u:&ItWy5Qҕ|.e Bp`Yl$yh5.%UoκMLJn>FEhas(| Rbxʘ9%l2N(ˣxzίN+ xPk'!r_)~̵'͑PY6 "Or:OEl(N|١{OJXR+lͷwoQۮqcSᮻ1dE~@h3F#"xl ҂'rNDez.0ExX48ː^u5FlO!)y;W:nw[GI3r.dO[J(O3ݐ$P=4ƺ3k5z\~ץEa}o> >)D-o:VïH;cu4!upQpbwY/͋ƲC#'"@'^.\2uOp 9rLq|fZW|6;ig_i(7N~i^-GYpypW<> 㿍dCW-g-0ʎBD.眍UT뛶;)rwR`+rqߘ>ºޡڈkHF^t4@W~@JD,v߅ \S$€rSkvPVH-;dY!qJ6~rm6mB{>p lSC,BI1Ⱥv,O|bz=|- k$Ű""wIS7- {;+ϕzWd//=h4GXG] &~:Smkٶ|+xk |~r_v4ZD&`lu Ա܏Iة[!:`,ӬLw}b;-Ròѯ"1 t4flӪOeͦ[3FT7~O##!m:7puSoT{'%2 k==+Wmw =k=~ ;][s%9Ovm>xi]U/Ig3?#`ėc4ߒ>G8u=/^5}aF-۷vb{n˂ܓCjp?.qMc6:)nveMm9 1}9<6̰_޴o;ػ/x"O >9r_r wR*ߖ#t"H)yaHh,SQ5b." S)^X۸(= ',K1?X)BuYZGEZ{M? ~Xű- wiTHtr{_~:ľ%;Ohbw2υ⩛kfFPC(ѣ<` %?5Hqŝ%|σ?saF0$M:s6&ک \;ejCKtHX|S~pVr@utQnu6Tu"?#`w,Ie $G/QvtiG 0 *g1ezԗ98zP?OE}濧t 'C덞=4`ݏ۽c"8>䈬u~C2Tź24W݆J3Xr-kq[}5PdqW+j@=`0S_*t@%]һ&p.ic]ow}b Gz\\+^> Deh~Q|eqtW})WɚLgm~ax߯յ&˽@ܑ_[wjyUb<l+3H;\߽_yl$6f'?@~ b]cҢDyސeC"f~^iYW:׋~y?7Q}?T𪹺B)!gN C?AvCrMN B1i&WUE"^9ٮė%Jks=UTR!XhIBaX#z3rm963dϹڂf^z:G뉻3̟zmO/f˽F3Fv*ޝrKrzmErZ»B pFL۪ٲ ?VN"Ŏ-" =#w).T ]oh?DQv\hV7{r-ls,E: ##4+? " gzF"vG&,}]`ǖMLsms ?9g(LVR(MFp\JDfMͣVDfX;<-ԁri4'䎽^Cǘ'qEnWD"Dį:9EnXټ_:+oX[?k0$"pcD)]+ٲAsIo}ECsoD-GvvEx1t* z>lma~bj+ƥO߆i}Q+TUdA8t0\7^M-TZn_|}PuWsOu]o/vכ8^0N>-\1a8Z?2ȁbg] XӐE#MzbJrFJ[/Ϡu<ƜyѸd7_X7`??CC`nbDaG})A ص! KwT>mnQW4#ghmh/=_NVԝ![ pX zv=ܾ7*7ܲ?'( TiO; ?#8=䰐v@j&7 y\6G\Z¸ Mq9!U1TB4cN!BY8q zZFמ/>wvɩz'?g'%'CKnT+N3&i*jD'hBFjM7U6 }.@)YB|h|mF *# ){5Y_uս#oڧ]4o;:g:XQѹmߝ*b8]{}w<]<\5It TԨ wH~T|.(^9܄.9>vi)T<cSCwd[=zbKN)0yKoڽ!Gf#6<ԳqO8/,MV'rIó@9L=$Z7Uߙ|Nt4w)_ϸo9^fThk:k˓۱CJj{SaY%D3ą"ۼyS&7]4$-Isz֋t͝?Zk[O4tφHDFF$y#Q+ʯqxF{srYԛjgh@ecs\e:ұ}p%M {Ė&=Lk^S͚/} ߾T\d2|Y,{$Nۏ_wf<xN}u XHeir#:.gD['"$Y '7!G\b6~7\O9`eyyݯ* *vf$_|(ՇckCICyw}+>RKRAP }9|jv\+y61*-V{NN˔;/3F? Qm!v֔xrBIlsX Q*TnϝzJ_`F\P"zE3t8 G+Ǧ dל5ȇLѐD52[$uV<&KhTNk+"D eSs>@-`AXQO$%Uy'@N#>J!wS7A3r4r4,s&$ˤ[BU>x\n.R?xEdm9]v܈Y-13oLcw¤C˼;J2B\$?߈lW>DNGׯyЎ9Ls3Ժ/!J*9`eጚ?a߆?Q@o8!$W|Qq.x@Hوxُo=O^SWWd|$Mf Þ1OjμRa@KqcEF5g)]REfVj$^铩݊{AtB̓g󴳘KfIeȷedfR=Q8Mش۩@hip7B:ʛFpoF:!,g;7u`SW/i/A`3 5ɥZAuh\g~bYSw^6hONXށK6o,{Urw>l٠=2uG znkZrʐxBO,NN+]RJm/W@ͺd;X-2yI-}8-nv nҭiwg^ /{l&vdcڋecn{ܪjܫ[qrn:JWm~Z &@=\(k>cR vyj²!ZG%qb SQF~SdiLh(l A nK*i @m빻E|䜎Iwҳw悗X-I o52C* 6̃!nWP#}iv}VHっAncȋ7'~:>[cbŮ^ay.Ƚ #l.3_ UM%Nj߽~}ɸ{^. wMaEQh Xzq˝!MuG:zz;Ruw@7F܎Gp|#u~ pO|BL>+NfV-e1\Xbʲ}~D>Ҿ| UsDK>zc˺ )b&ma璜WyF dQ "@ɺ`.)]Văw:gBroDQ^fFx2i'y&ZЙku :~h ?nr6E?h`5##yO)~P1.ۣ/865z4SœqLjEr#A ??AvNXš#P1g|]+ [MȥkUV@0YOEH*P%^-NS9 9ȚS6 Zi2ƎxP yRm긬(p8`jR9@/@D2N\§tNGL廰^O֐|Jb]?À$o% /X&wNk NBe^ ~KRtkzB9l(e P\\-mFֹ';ᑐ=ֶaZjC:U9`N$(Dzz:v"Щwy܈Y~_(Z:p[fso~t,VA*g28;2޽=ӽWwmsų+ھ JeN7er֜fl! Ȅ$k;5rnho2Sc$ {9W{R)cA"~a*?x?NX?I84|>QZ +\H3 gI>#,Dg =.{azs`#H̓Ec8U+F,Ў"4pXi=JYsxt<#"["W -[Nu%[ ^q }]+_>hP^RB9 U{QG*?Ml߼K)$DN@ihѝw ^/qBh^?n|vq-V\Uls;SwZ^tÏ hl9'g\']DAq b@Dz=D0~*FG d4$bev.gɪ" air$?$g$coMQ[D>6lȑiƧ9R+qUeE$ȋ\*Y*tXK# dρFܩ.#S>g Wkfv5G%Is9Gsn:H9xOMMSr~NmHaM/8Et翮*o/_C*r8X9 3S}r닥鹢Vc/lnpVqce)p,2t^Dg/$Y~Uz=W8aYFhIHS^^$L{K}y$t~ڗaRe>aée7O؝&.Ci[Ϗbd^9hZsxRv ۛ"qj ^u U5 QH+6,w(1y@*nR\z{T:h8>,by=s;lI q>$M,P] |7<8LG(q3/(~MEN PE EutY#.qZU/okݿ{/Ɖwtt$y§z&W<#:X^X q5WIҵP=Gz|:3Sk6Q ½H2\ .~͸mYqo\Po$GsߣS5ʚ]E9p̋Cb>e. rQMm|F/?t.R#i=^4Xmfc?>:!Dz?uiP'&{δmR'3_۱^oD!$.~W4mOcмPo&SQアBm{3uzmPsD#q74p–{I֫M ؀=Rs[C@Ib,۞t-=)PYPCL' 1HZaϪv> lzmͫ *O/T`] lqʪj-b:/Wk^5snq[>qO͎umGbom!PN/>ߝ +SDw߇v4dW#XQWs\6x3A~D4cCY!8nK6jjnwrע/e}\?-3Ǭǃ%0mS}D[6H)?YĜhzQrNH8["3΃'Wi*@H p:0+ae9d9o L65"9P/LQbN6nU"_e]!c}9E)nOwOG}BZWR-F`䂏 M8>Wf-t`犈NENp.C39Z 8܊;Nɛn[Weg?SYs0c6{8%5E<1,kkW,1.d/x*7w1td~UlzJe JEj0xͯ_/zqB7)>VM0YHJj`h 7Tnp|4T/dVu~7[Zl,9p AΜk,6oXֽ^ W!ȻE@l;S\`4m,:9l{O( L+;xA#[ͿpzmUI_)ämvB4RiŚP* Uؒɖ3y=7:⃪<9'ۍeӃǸ[$i. 9:XW*4 bZsrO[K y;m;Re9DDnnp.g.xplFDԚt\8멯~.,]Ժ!#|o#hhi1?i[ ?U _OWߠ7O5rw_ozӅh|y1j+=ܭ3%7{qmF3|Gd ",_W,ɟ 55"vO*t?nK1O-1&y#UF܀Y~jU$ɜ! UB0ԑ}:;Q?~Æ(᳠ጕWK)Du J?n3L#Eb+K{]Qt_rqt3vԅ_hx&"℡t Q<$yxbfJ<KWWVl>P;5gM07Ӷ +|T~$ u'᳁ V/IʠIk>N2]ieh3Ul!$5ϽE\{IdDTy7^++nupRAvX<OOO@gki >T"ީJHq(0m'H2uizpiaLj;Q~K ՋohFsn:1-?E ͜p0+Lo"A^E@:èw; s-<;/Ƞ}$_U[2>jmnfCY5e3Qʬ?ojv.%{;Q(G=# P7.zNmD wBUٜ!l3ܨ" .g:^O-. QU[孤J &>Rn{?\qr.2w$v2~Ob,I`^FKE\{kɬAKj9_%Hd6Cb\}JnAk=F]9 ӁI )C{EI59qt7ѱ%VfVI}Aca֫~žwP@Z6~>7;{ c=gSh. [qD|:%arQa;5W" 4 lseI%W9 O.as;}ʹCY*QeY!itXFwwi|5s[yưCcAyu[ߎ[Yn>%v~,xc Bdn;A_ij}a=SVu>8IhSu6cVE'B ʠ9 ϧxoϖɢOBV rW ϗgPe `~@!Z8M "r r1B3N<^-.+ /i:-w2O|Wz?eG{ IL ABВ˓9CmqJr8A֤]|*N5M-m̈\ m{} 2 ,Vc;z9!x^%Dϋ)ޮ\o,\otx(|=eDveimf F}RZ]`ZVݎt{rPuGKe8|PilV!O<3Q*v`q&kk|?`7"Mԯ $,b|m_t>FN*:FN'? զb濠ʕ3}/E@ޏ_dTrܕ v6R07`σZc`&w=69,cK{z3tϡ zƀ{gߧQ`+ӳgDT8F$v{n.̀=w\O+z8$O'Ge)݌>ߟ?9+ݟmdڧ~"<`!.iUNa }+B)ZEnLG u֕PC+^1(uhJ6Ϳ:zE /qh[qcgOG)|`Ȉtf^aS,z-;AFxnۭW \))8&QGY6q9b.[&2࣡qIK`ĿG=uQ*ڊo$ '-]7 μcOYZMD 3jmZ_[{5"mD0s7c;I#(诬q1BMiCߴR[&AzפzL}#yf^WZMhx>X9@vZc<^$kםk8s]f-0']]&㙾\~8k##Qâ&?c^ڍĞRǹ^&5ڇq16P?)_8Sܭ#v$RN4oσP` hXYf^6}A C{A|YR˩^u3=u7|.7G%7nS_zXmKomnWOU6stn POZ#>Ί|~C\f@`25Op zG~%w[NaZDNIxD_y?޸h6`jП?lOnY(hgUdoOߐN=u'D,v**=2iΑNׯv$$͝/+^)\IOfiwH.[YO] ?z5ȥuuT#2ړw,nqx׏=)9J&.c?r@"8q/NG$WW׿!RRsyj=U#`BY~ ԫΆHSАyYƑFnrr|N}@n1D=>bb.Z8ʄSwHhĩxb,i;Fb1'&[__')sgNHM[!Y8F,3ѯs.RySJv( <( CDFAo~ !D˒e۷sˌ<7亝pz뺸'z;,Yc%S,c`u.khzP>5k+4W:/*wPF^,r|!I;ʈmB`s HU8ERܪsTqin2[N#EHxI2OP×߱oNcWpX4i76쌜_vfeH\~D\Ekr/{u>͉^3J3knVIr`gZ)*/.Brs Ԫ\3o;צiVSLnB2.J8|RVpg>7kypJv~z<$ +'}d^W'RE:܏vJQmN˿"K/'7ϤYd?ZWwED$T8U׾7H&)|)oο ?`uM rܾ02zۥwsdyhr.?F1Q%/4n?-W zۗ; y~7^^U?Il5X`tNN\jG07v\xuY~Rk{TlY[ٹxK,pM6^&xVon9ӷz,ٺGE#}=mo,1bΧ<`.兹`%"DɁ3GMpu%#Td\,5oVT<s Pwmx\ G^ؾ|{ĝ57_}%(̕0\?S:;{"ҟPݵ-+nQ$];PU ڟa[ 3R S6t,a_ سî]9d)6M_\pYVW6ݾU$b=@U?倿jGqc_<9p :(8j2%+$7~cjmwڱʙ?o2ރAzҽIPē=m(}{e9TBR+ӵ`46m.T5ǁҒ9:pp U&D|=rD|O nW,RHf[c@юi[[ăFo@%Έ j6ŎEdӄ>yb = eLzQ:7?"iT>.a^3dY۱RyUfm)~ .긿h$7<9?)"F%Oն^N;\juFk$܇]6RZr}MqR9^ܟՆzQ~Tc7q1(rA(}'77 }ޅa?rb]bnkJJw<0檓-"n[;3.ܻxk[Nƍp6AG}!'Nw.'ٌSOJ똂AT2;}]*mw%G@}7+.q) CsKm `fovx| ^&[}]COWl}ǡos|ف3@>-gs70J\syW2E}VHYҒ =Gf϶J蟁mӿx2Iz8q&(Vk\?]0]\L1Yxϣ;Zy0&:CP2Š]6^Òn 5%O,T=: ҸD>č?%:KՍ>Wȕ(W_#Wk4Egoʟw}cP15i^ey?%kk4 惶֥(E.\qܽ<;]r, ;nd Ïp#A~:nGGߴ77=[@ӑ}$F_JP^@#’ͿgLNCm$CX_.:yBO.]6x'T8U9K(o(_ ^nꐳj"y]egF)شxu1fa= EDev`uxۀlI?bh xF=j|KR՟V^%7XtMXsӍ8Gu8m܈î%ʙ|2l?(D}UHN[AO x7UF=jd|KSq^i@#|q-ԧ=(flO*"*Yz(l` +S"O֍[$ǽo6G&RT)r9èI3'Ɨ٫B[xR1D~=q]ׇ/Y)w~Xزr04e;NSS># m|1w m<*]v8g.cUNGٽ57;=-ZoH7K-ɫFA v\x;!]52 #HoyCi=9QaX4W6mL.q3 ?Po:D:unn6\Ke7[ +֍1Ap@>u$6u:A-re{:Q8cao[:(gEO﬽dFe~ȍݏ!`"–r68+v2ۄ4WiZ[:.8MvZ=D<ZV\ *GYVv Y1n %SY^ ?#ώ#-r3ʵ#2N\GS[.BN"s/qM;^#gaOHQq)@]GM*HTS3"sc!TS:]QH"[-י(oF" mt +ܺp:PFE"t"60}CaTN%@9j϶kS=}mGKf$`l͗tGi_Ďy xYhC>NJkM/O];oqXi(]JhE$&QT? C'7Pg"/{7gМF{CB?H3쀤HD&g8M/}_r W`MϭY? E@&gDFôs%]N-wB<鄍pѧNc>fj{vrox˼&eUz$:[Dí/ fxcپ1 q1>/L*9]cS_/zR1_}9A2YBN_G<:#L}6<'M3ިZ1/3rT3rhf+HFYjxv6#[2EW%X\\vӖ'6 a^?"v)0?So7sbdboJf }/0߅sfع?: \:6%Z>7{o42Ǚ|0jIJ}>axQ,* {z=%C\;}6p)BuDA?;#e1*Ai1Jlk*}6EZH58k>?p)+w0վZ%gA׎gh!}g2itz]R)4lcUۦ4/<zOhYSRFn;kK|>K] F6g ֤Zڬ)qt~wTռ}+ \=|0=fi5e8;QwڼGF #qۅI;4}X?xSEraO"$ߡcp (_k@AM~!@wmU?/wV|y#AZcn@ ,k#!vd߄#r4LkkYEBq^ѤMz ~4Oі5r(r;C"[麔X4zdz>ݼk*ix韶)kFjbi}<\KPԈ87K g_RSaH&DOJa]xJ$X3.W9_Kی;䃃](~{Hj3ZtHT>[$5I'K%\%R/$u7hPŐpgG \ /"{>SLCgDe"ep7|W$a aܢ{FmSL+-6ŅHs OutW7.C3B,@ia,9~Sn˅h/a;#7Dl~῾]1v FbXrS 7D`9Nm\VS~S28}΀)>+Nw~QB ݋gb N+uu*Zf֝j[YϘU4-5'$)'n}q>-͝,Vpjx ߫0@b<|'<+ r0B6d(ba7{ h8Uvz# Prs7hCyj %M{sV(SD@W䀻%ɌStVP C({~^#} kϞh=JuTr~ \n6xKurmrEr٧.zu1tb= :"BƱvkƫ>z^eFP!U?"$r>_| /¤fkzm9'zH=8wHoWIT8aN׵k-h[ DL#8փR",F(vq{Cd@+ƓTN3vg;v苈sZDb5;єSn!FA# 9ppO`UbnDxUgþ#E^ްA0;Ex5;Wr/mȿGVg,rN #Ҕ{83–`* ꠖo཮C+-_~c1W;ﮄ㬆y4~~B_ӛV~;Fcf9F&l w#A_O hv$%XTS^~lCoL#E#Nda$'VS9XE/ɦr]9hh'~ĝpDKis^q8*U`{Udzt,}]“~`t*~Xd*܇-S.op%tN{򟝮C ߾<~/K]pxAIccR7oiLuxOIY)V쳕 @v\x?b)8 q/'Gaa-]f<ձ=,TMtXBL&:\dsY#>OnJGJ%-ǜFѨ鴾$Um)\{ ѫ#jS-'')"(_)g:| "n-+}9'E|SvkK$cm:'v h)2-a9{y@q ,gD.8y\)B5 K"ػ{B7LIO|@7dZJ9EJVejS8FT[$on@."l.|UjB}^Ư4}sR-aϖtjẢh{ D:g-*n9d[vp;}scaOryEY>唐4M_%du"ّzQl< >;5vl>afŎisa4wv@YqSWWDDi{1fxLPysԷrĻn?"g0RZ2 tuZ47N+v^jLkvD#Y#FyKxKy&9mfc{?H1Oy%=Ft* #Sv7>vBNEKu8Z*/oEmzc—}ЭH}=hd:AH>)oj?ϳ}J?6+A7Og]Bێ"rMsX \|:+-&U#@$᧽Y {ov:%Pܖ_BQou|m ?Squ1Aa ( Ev7`u̖q\>)Sjn"%KSn*(}xMBEb/ADX=~[_T>}y̢$:(FDjGT4ųp-`vKʫ"v&v)0j`~}4]gyىM\ipx?+G2~=\6&u\5+NQ}/< u>eD|moAUb# QzD[\T$l~`74k+^j0K280)UJZXP= }rvۇԾ{偊(|J&JR<ž5|̯-X=Ue[A~vs|mtQOvtC Y~jDaS?\?b9Č8.U$όPf|K7LC.TE.)bb {@3eXnO"% ~y RCq4bN\[d"&Wt(}Gq8/w(JJ`e{کx&`9G+s3K8exv1&"rpA Ekv=?Xn|G҇c0>ҲHƖSφ|N'n-;m}@hDa2[.OK ]ڟ=kk6HvFa,sD#I?<ߑf)qjv!9~2vI TUOS䥟S="|QThc׎_Y߾LDK%ԭOs}d.aN,3IoEXn͜$k%hgp0\q4ǖo{t{*wg˅>dbQ1sjny?A:-y}l;fgk|n8m7>Vou%_O,}]._n?T#wj|;~T7fp=v )3&6Yd\,%9ߔnSݫqf~ζor;: R/`=RpqzIh!fk٤)LDaő:mґ%XĖ 8$qt(u?B;i0>q}!S{_lz$SunWbΓqUŖَXc684bm#~{׭ oGkSWƒ8]Ra]}<䚃-(L1Q& 5`OsDʿj_n]L:#kG7mgqLQt A˦ݪ1xʓ\<B[@~_b5nb' -gE: -o#dy K +]FG}G1˿8 ~LviPCo2 'TE`J#_@}#/9wa}niʽBwK8-7Uo wr7AĿj\!NuSN g(K&[M !`DE.pU" ""ݷܔH>)QWN." zܥO6\d{{ym*{onk߿~D j& g'=1~*+r ᗘJTG(mFCwlѐGp*fP=qi?buvh)!Guc.eSCq~EM% 6~K>T.rt@ s7/CǻF 4=VH#Jes,2`ǣSAWV&k,"EdB96 /w GƲ bH[L3z}?s Y؏-@qQ?:lNhʛ4X 2^y=>8T\HHm5r|]3|L\\= "H{7 #ZED촷g{m?e$B:Et i;9AIlpOeaZ"ِ6s?`RAe}Q~ѧk`tV͡ g "Id&Wæn0wtҧ.)2O%jJ3r)u@q*kA9=fW8u,C'>ȳ"q8GjRE&P$7qyhL(*r6ۦ-zLqLȝ~[\O.Hdoȼ5E#)G$@"%}b>Q NU[hF6͛z xWGN\_۬ .NgA(0=SbQmfK#'gCrrc Ok;JNNFIZ5ɭfEFFͅq1gL/ D)Ɔ@"E-*F~u2>*]jiS_6ܫl g{pn;gywɻrFsJɕjw"oz+*7<8%!G=NnW߷Nn` S:7>)dαa qQс6Dpew1~_W:J KG!*'Z$n#儣6*\4*]Z=Y~v /;fCI6J8Sb\ rA2鳲+ȞA #\"ȑ#rJac09i1HT\̛ķ+Rd,DA~*cl4A ]1=Mw 5׃2z[ˆ,YcYr__yMbSo`howƉ9 ?i{Е0Ǖ,&[GE:8kE=5UC1T$CN友NrR~zY?`2[ bV Sޥ[섏nJq4^jJm/41OB -PQRvosy=.ScHOoUㅢnv#wst :~9Y^(O*JR<،kP]Ԉ:Rd, d\:`+d}pFFsluΜ8PTcL߽Zq#!/B`S'G^٣H?={UrȱG2" O3h,܂TZ+V{)1t1*A#-V'۫?lxd[iD5m*|r ;MR %*Z=xϩ<,b!)WDTSDIäxkSX!S(h},YoO0dinjE-^ 9k6Z'[rIwg!Uqr?{z>r4-; X[G@8GEw7mT9Kk6X]n~G#Jr}c\q:pݹ "/,zHlj-0o۷O< wE%i댜jW֋?}X;9dIH0\O.)o%+;OR 5r.4唿-d3׳B#˹41+V*R_R;49Y٧pFu)AP] t]NJ1Gc=F&m:LY%4[,(C') "&_߆:s{Qg< 8EMe^̝Md%x5Fsr'/;r7ݬK Sx}(zr-ViTH%Hc)e`֠Ke&SnaT_ɣ[s-@ #Mѧ?|[U{!2xmnaG|[kgved8a-!R Q@2oA}- ȸUG=bj-# -u-w9>'C}4kL%G6b͟;x.vqb|<%n̦`QcI3"Ϧb:|#)K3n*h.bZ/r7^DT:\SsݖK=}㈥K w8 U7ר/_>l,]N~$3w4CߋF.Zt:~1+49[E.nYI_Eݷ6l/f|oO׿c|Ka"7 A[?s==WEF2S ltr YVZ:|ؙ,ߑɀLAcѦqV-) sx-LDcN4x2N_A:!Vyso>8~ #<wUƣ,N35$].}H&9c@鍾C&[rse2Ɣh ?v |Z|pt*%u[6 Iivs@l:|聤^5&j'mHȯ >~ڈ_40QW8x o0U= a.8\lI2v vcSO{aεp|InB^F}P:VDIgb9%`X>3NQoٲ[HTAN&e}GE#奏~, gCqXgD -Y_ 8-X-:)l@ 7vMf=kRC@̇@!G;%R 8+~˛2*#GDAIۤlseC #1ZmpM?v)Ugq tY8 fM8gV($S<喰t˾K" W~nxtr|q<Yd&8/ iRbӕSV5ZEnâ'k.Cb#!|]'mr[ MSJn!->!tS}gP~ix2>u) %.ZcAg֪~!]/۩)+_piq)Җd9E=~?Q~[Y^4Sɷ})xHYo־Mߠ~(ҭj[hʺ]:/e$P~DzŴX0vMOnbzxj\4$u) l 8 _W3Q|3_^@A#Ût^Kr4if_4j)p+ryIB5i׃9v9w0#0Yp:p *-lIx4rG[qE$"p: G`Uy2a7 )4e$Uwp"),x<;^S O|;GsZgwEB3)w`t{VsVa=Nc*9Pģȿw%,U:Ʒ\{.FW; tŗY1|2?rW2X f"^߉:.y8qkyOxvaqR{@Ҽ;r#K{9i۬6L^zmIy/otՏ9{c` m t_tU[J<ɡAm`3d׷7wh|q ̑AgEW:ٸ>1k\^<VS%yAoK~l#ZS?1ԓ7D&cZrt =h9g~W)|l4vgO#3~VM S$4)o?l_<>uDeiC "w?"9 o7dv||~oi1<;;zWD~>rOYIQb(Mv~DB  ó?ۜfzѻV,%i[>D>,ӯes0 q]ܬ-߉Z9m5 U]f萩-JvhyT4dXIz"ޯGMiLO2\険 &vUw/qLޱaI4 AP ճҨTz NmVRG'I3;>QgEEI.,uAU%`NptO"y$X[Rx1S/1["=#nv;haWxߠƒW\$A}&n\i-͸=&yBV"P{_Z2ÑDi1[R.em q{yHL\J!2jĻ1דCJf&f7gP*/1k[/GIu~ώZ#Ι~s&"Q{3*;I< A<" /i/im WEIA*JocűGѶz+)c~MÛmAv%x"쐔ٳ ӣ nO&DF.Jfcl;`'q '|q[qJףa2ҵj: A'd~n8>2#@d_خP&[EUg"Ϝ BG/2"a6;zQ+m«>AH^jl|H\tBk #jөI/]aRr`<ޒ̯ G鹏(Rbd‰OEb"|IK}tp۳}{$֧~@:u(-j`fcE鶝>}"~CKk<}_u>1rgN?n9S< ·Fj3N# m(^^#q0Я'Ao[+85 XT Ɉ'9ucGȭ-T"۱,Oo4*l-Cs._I"Zu!7/^U#2bI{F+`⮟qʚnLT.mX{o-J[ ⿻Rzp (:}ilNƃ]7¢1.Z%qvjv|rLg('T4j~;`&x4%:7֢nK驐f$I}HN=1TDsڑ{$GORtpe 6^:sS@\ҹ07<12; t?/^;J-b,I0 g fcgrpxdco̬9i[׾|~8&ckO:x]_J[p U[<zqCI%UgktoZs/Thz5pv?;"==p~9NdI QDG"p Xc*R'J-7MjV)'rl`J4ٞp1A,':Bb촦Y~\Iag}{h\")21SBzU8GeEa Uϒ[;ni7" " /r~xx$#Fj9K4Kyr֜j+7xh o6cdӘ  cl~2ҹ6wi#Nڝ=lvx˛셝p6k^g%LII8Brwe:dǁs593&%RWOAE R.+HΕ,u2Q-͂B~xizy! cUh) aU?2>5 ))6}>w nc3rIGWpdq â,ueFZK?xDIEUx{n\Ma&jd^`:Uy"XN[$ڟaàzyYwj3Pa.iDIZ#m7[m+ ߈S "nǵqM.ǞԹ`C_9y>Qս_emOWFvrOq1-ȰʇCՙ g3')2Hܰ/u&>$\,#bnN/CנCvF QH9`QG2Xbb^Eѻ1mt8?^5,d91h*uq7(Y1WrRT~$<'tM{ Љ2-(9Cf,r4An귻iHيE0)s_Iv:7rP"FNTҖSw9X&NHb+/e8~:gJv=yNjg]Oݻ@Utw ikSF3޵$7zU=mӚM]{<(}:DqDq`(ٵ.ۦڀcZ-WLv>@_jO\t3 "%髎 @GʫJ/ R`D`"Z'3r:VY; ސxA@٭۶+pו?YSNŐYA/WUEAOv"]ٟmr"|ϩrJm2^˥r;K&w$Q)uM}nzئ^;-G\rwLz+?c,N2ˋs{Ir]al止Hi>T=Y~L}j 0WPA{za󋸣qt C0EE!j/Jd2LuC=92և${k)HT"񡀋Iz%JY+bDTf8Pvкv 9J6"I8߱;QT} Zmӧo=orȿ7zT&FFzaI)SpPrsߴ%m 冭5crؖ>?Iс^S/‘聆ɣiY,Gt#&b4d'5mGNLD2k³'#oE0XRW|"}C>~DYIrr`PRj֗Dhfl9x\6z|:5\dF^/[ѧK+Od>qsUҶߋT5']iĐˋ6k{F.=t՞N_6J7u龈儳PK&okW>墄]jN{؍MDs^΀*.Ip;˴Peude;U ۩ͱwa4-iQc_S@b0b+T=&<,9_ (Oȗ|ZxǴDA8^F5rnA](ur?N#BܜaLh=kNƩj*\9 y99a\fuAbq:t)徨^?ܞ"&c.wj* K-l1>0 X1m'3` =rmNW.y~ȿ+"ֶdF$_&ӳ{kmUZSw|2`k\r\kzwzF$rǞj1:e@}y!#zW:xv,>נ1OU.+wy}ezAE]")qM\twHh>)ۼ#M)]Tx[~i*i F+ȑަZcu;#kF})\}Jk_{'s6/k7j?~~IWϏ+0CIOt+ =}K/eT'lζ]ZSġ`}j.3#X~S9 )xqeL|_.ß A}O5AdGbHtpZfД#/an Ju3mO8C٘)v$QUiUuE-nJKɟDlF;8{)OCF~Eo_@r8uǎČ\C3N}ґP,[#}uV(L<ǡWézdbWɠ<]GL2'Zm#/1oIl"`j"w.s(0MVk8]r)(j.Xx~d$3Gj 2Y:)-I5g܌满*j6vtL9}hSHf*QnBܚ2Wu'JXwD\&v0u}f@/HT}Xʌs rX7rHkSVr/p?q\Ɓq ^v:QB݃!zʬHS#]_?=6F){K Cs!?>lٓ C)n6 'vN,uG&qrF^^ BꀜMn(]y~>Mtw~J7ݨ[Qse k^.ɜx7'Q2շuk6slV'r^]M*:S#$~g?i FA_Y4^r,½z/%/;zPu0` @-_t'!rNIuloJ"i6˘+zn<MSu[ ,rnNK/iD/5N z"(lQ@NkDR1[9?0[t4qٯMUƵr ~:=wT#ME^ک *o+ó=?"[~q^GJ"O Yb%h-_,qH9^>_C`joAEIO9\r =C N50B-NjA8A*?md BSl"\=,r:c `3"nD^@.98G`d3[~w䞵\8#pSBӖ 2&Ӎܢ[#diM>vFl}̾F@tɑWC"[gnh 䄡F{HoX#rƧxDJrD 2jtU>Su{5|BeMwXκ))HiӦM!y>u[>MH4\OqeR{碝 %G?Rj&xs4ڞ+NFne+rdB,/H ;h\3>~Tz3 z܀1ON$͵5u/\\#By4zg-Pic/ugoz.` 3/cL.Ťa[?z?zG g7xm E+vx}^O[3I NiX6ˤdȢܼt67S{~ bKkZ&8ź"YUDNĒǸx!=dcd@`XnmoVȾQT^{U㇬-\?5uo/u >D|ϧkJ.ޙ'Ci#WO>VWCT 8H$ 5GL\Fy Cό"OnG{:A %EnHt脁?uh֧VGZ-RnGի\EPz4%mInE0 ^7 8Az.M_oJܭ#rԜsY=17;u [;L . =hsU $3g|8]˓Is{pԾ%Ȗd<7Jz驯 +>ZǔIښ>Z}sAR]O>AT?]z( 5ou@z}Pda*~ԟgxO>~\ Fr@:-(󫉂{rnzWEMJH;_ax_zON5unF@ g^hsNDd*⧱)JdķCt}^b@\n_b4/=AfryHR\-b̵}oU`Fk&#hvXR鴓\7q ѳ3r;} bH_֧% `}]iA,-2v$A>kg}SWAgKa`uPF*TUo`/kpw%FIOA^ozpp_!n>s| 4`'\J *!04*)/bnĺ(zE>>4_i@,gUpv`O~N\Or</ssl'`6;N0NB.bТäISJ~Ҏgż3rڋ`98ѓ8N{R+AӸF>D̞g NeDlhĂФIc7nZN!4 tXE5Jsmp'Fc<Q8~P)A2ҹu±յЇ5,H~ y]Ƅ~~G4ts*~¶=9reDBA>:1ms"m=x.]O*A\x "ƣd:/4jV@Fٴm.AN7\>ޯWnܧPؓŭ-WFN_4mx~dVE/{[6< }L E0-@].㤲\o:˙Y/}·6eÿ3r!ϕWAyծ1=]M 9M LV}zH_~r^`7&Dn"'qÝFy=Y>B ^mjG[LfW^*r+?U)vMƻ͓vjVLyyG5ߦ 4D':G8뱿O gf8 V3Zh]NɩE,E7G]Jߚ>ϓ&4Z| ]Ỷ 7M^~.n?o RhwŜ8>s0AdqĸQJNpg[u㰾~ 0Iߵ/77Q $T~IYe/GR)̙aj#U|/G=9 FOJ碌?_툚׵L%*e2!" ;&@͞JkE׀({nrZuAYڝkizx\pՖXl (`>Qy冎?("R_JBZ2xЏ9ښGIeْz[%@$d~QA"<}:1pr[׹# Q+OBAREZr{(Oi5bq=q^%aUrabu0aA[_m4P%.H㻕S*5(QA~JSд_/c=dj= l"@k_}Y" jPuyaˉ-$ {)!XO.ZӠ;(ax0^ތэsGlAѕtdo%z8a.pbEqㅏ]VݱN%e3l~kڥMPxf^O, qy*Md]EY~>Ӄ|E/G_$@?ɽrSCNR{`ȵ%9'N}< r$Z-6=ٞnoF W :([󱯝?z2q+NGӊc>,7*ya}<toˊb<𝪒J*6@p_׷ʽrwቕoJD5+rRCMAD+2~~ÿi3D"B,> H_r4#'_"W^ò~i+[ϮX-#.c-w\W s(C{Wk,I1"9WN!Vc"^X-l×z+1$÷a8l]|ݟߟs bOfIVia8[޸ʵ8XCm_.]ZڳX" [oASnf0/ZLwZn[odMD\ZP]5gRV^@p| ZRzӪ, ;-B }XFv,3So\nfDP@Y[:3SR-omkb~^k4 q;4ڮN8t`q'0jw_[ώGq>FPfD0]|ґN0,>H#9S7`qZ'62sHGpm˱6Mx;tv{JoO>+OmuI9ΰ 7D]`Q́Wy;ۨPg[19J Xwd99Wӫ1^Zg}bOu xS?R5Qa7m{ݪy^Tx3A&pŗr!O9}6S3kKb>KJu#~"׶ֽE? @]{?og^a>`(^v2^J'Ҙ yx|B?L-l_q\Gе'P< "U* }; RҖǤǾ۟+&F/ت}se~oU, UFҹVؤKu90a~ yރr`bG5]x6yRcq-{ zW|pYabMZW}F܆GbQ^NWpBU9rpUeZ˓5#7s{v'd\crW7NOl a68Nb_{ npZutDC۱C&=Rn!SLLg#5`K.sPrɓy?U!QQ8|91J,4F+<3qN6mk9R*ժa"nj99wvTrA[W$/|- %*vL^pmI^g}QHϡ4(/ g䜞.RO-O޺ұЙ ۱_3 sGDλ|6YRm{:Q CX=ц_ꑵ:zXwp.pb쯳k2P oC}hENxЏes`0±~le e:h-_ٛCpA"WxDZo&F›nY(ʊT#'i/ k=K? b\3A8!;f1mN]]eS#j{xb !5-fcefO1c{Y7ZáOo?Cr| \<b}֏zex}rQ#Q^ ;@aї0nZ37_է:ӤO+G$^btNc]bnEUbSkmymg1&o1~AOCVUOn/ a|->eb8ztw;(~a2Gu%'kذP֓TrEc՟q_oC_1OVy#b S]i/^oy0T[ kx@ zϛZ͂܈W0w/0ѳ"a$0~vg\ʮkQ?`{ةI>]rpqFn PަÀ?t e{E\i_[ꗈBG{+ܒqjKQ󁭏e&(Z%hKF'?MuPD(n:{4,| PbxڱSV-NڥhE*])bVՇA4H;łZׂ8*da#舍ʍ]Ki:=tt }.1Mݳ2$ӛ5$yBd< rNN^u7O;&8d56 '/G:(7cG.y.UHxĝtڧv;wv3$(0)e8[{dAP '*>^ Z"}L65p,&sK`nvH{(kSeUѦǩJIꘀ;s|2X ozC$AK[m}4d)N*- o1U7/xZ۰M*ξ}߃@3Xep4^NPك?2KsTxsy}~bHY-˜J_/UսzGiZ+&\Xnx <ϻ1S[RCI=vz !t2 ,^5[:#FE)ز_+lO.Z7c5R{]+3tR4)9l??wyCOC?.1Ec* ;sz79gzC1p }7xQɺXz_ǕXJNfp~|I N=(Kߎol #`c.7xݸU^U6FTϯ7_HqƗ&K Z?buرv:J.āGPT-G|:uxKlRNȡczZSO]C&bǻdJy_3N7^1^ZQv?U$u /a_=MQ)JP[[ooz/t _PPB4/1EMvSj_>No_㋋M7S,$yw̿+u%.ҍv^pg-M_`~9?00 o/c,' GWBqԌ|Ǒ<j)/2"`޷HÃ|._{5ޗ|Z;]> q,wGlșz^{6ȒTMMl[Ky5P 4y5p|K+(<{pהqo؃BK b#NRcg'옅Z@myaNG5Qx/>OO'-=.2_BN==5GN^whGiNV*%wy`Euˈ0ټۑĸpw%\VBj{bej:}<[t\?+|JY&!L:QgZNq\@Q Mskߛ=ϋshwR$z{sY/2,aUK]KrJ Qz#kHG^{_osߨe3$2"V[)b 7MuVx$/WA3.4V57}:bi3 hU _(,pgc}f*Z6vw(C*{,uU  >Ƀ 5 *AUK'˩FfOqWG(Ϲ߰(sUJSKH|ITV6a_Wn͝$-]PjPPH>rʎl<m픞~]K=剏uБ/Z܆-7/NFe7|+! u^5_òú,3b$p@s|ј/+ Cy F"x~C)xv7jG0Fox"o137y># t+7~(G\uld bY-C=bSӳÿuԒ<笏 4z<.v/kqzj,&Vޝj:b|ar cKV&7gp h:;mSc"@RҨ5 E8U%);+\B9CĪ{hNjymv^=/g^ɩJZgN5 ǫ|F,]8"/86h/OnM唸Ddi-JﷄESyΛIiOǽnێScڇ ;ðJyڱx~/D.DS+𤞹6.zW|4N.=wHPx~?d*#\VoKck<ΪJLVnՅvk98OGBi_T~x Oy폓XLef05zv<-bzc{hn,P?K:N(s6;e2Ëۈ< }v3&XyCE1<[wzاy^ ow&u\q=$g}J{|\Xm3z=Xf*3X3M?4mkLcZHmWvrbx16 zAEY|]52r3~JXq]u ).z3YV}K܌qhvU˾C+ԾOxX 8 {lx_ :X3Pg=< !.{,e<4rǗ"a3Զna LU6.WPSN?xy' hY˜%2XK4U-$ϯ_)~OKǡm}xtœ YV ^ܞG fFN Ud8q+9f]hp믋3U;lf3XI߉D,ّߔ<~ f4W]8Nu/oW }"dV-bvKzx0r1kyN[,'5W4)qq?OR/h)f*,ruuc16'zPպr./~JUX}?4c| =2:!(v uhZG[=Q;Q$l\@$}`ew"M7yS:i^*B EɔT%$0Q!³g2w]x0Kb8Έ/;:^ vLPp Md<I)owrjhA;.V5OBg=iUAa͓֡N&2rDI⒴0<dhQvf'jNng'MQtC҃K =z`a/Taz$Az j^Na/8 ۩!V8!7Iwwp֜qR]i)O h#p*m_ %"lt;| ^"6]K%SX[l~Έ[]kP?vQ[)Ts%\lnV0Ϲ0>Gqu?MzcOP"SB-kT;jWٔ:eسx5^>pڡ+UT}ݩ#ߩ`V˅Y@o` k{r]ip&9Jž[Dzt+#>[jl7OȿΟTu87~'{ ~+X߁rpN(Ɓ;}o~uxlcn2;eߑtošva_T-ł#:^5 =J&M_ *W블naUXnɯNܴpV0/4F3jݿ76uQ $Ll\ Fu Ԍ2hUL^;bnB$␃E?)⣨[=vPSwr 3ǔΡ1y~ t0:Փu~WR֫_A̘̓s_ v'~8T s$^;pZzVAOt_+%Qa{O6P6MW,6U+b^`=إ@C2qzmmHTyL0L^T3lxI =$˩cmXurhj>|]_/ O;4MNofb@5ukWCCkۉ a62Óm:Bo EϚk]mrІ"G[}/YðC&(c 1O::!{Տ\~5uQ-$u!Sv} pM)AЄΘ&X57 P>@=C <?&66Fpk Пw S_ EȮ={ &<ov:,*ߝAvzxmgn2RX=;|a ;kONbJ48OWgvYg,9LKN,^N5uJ'AD]҇us#>[B;p> ؋u~}>=HQ$񙯗ApEmY5W HnR[c%[SM;x3y?|yE 6NO}UI\KnIyc+/\.+",H{G&ᷧ3rԖZDzH}1ǝF; đjB _5az2Φރo"E^vˠ4Tcr:')u$E^8VIt؃TQ9PsЭ!u֑^ɪ>z0l/nk' r="Ƿ\]Ōx8=tu%~ ߴ{Hu} EwQ$5w9ztNsXE!eɛw/S&J{~OZ7Ww1c%t.y7{X!*Bg1Lֲ= !.db+l&u8p(F{l+q(h-NQkvmыMƙv{qj'Ⱦ<ïAf,Qi[6՝X޳ɷR]76W?c&KiĞp.w;#&`u!NKO AWl#eyZ &k &ȔhLC~y0i6.1+t\ +_/}z2%N~}d<|j'}c/H[Nzg>0'qgI l웞~*׶Ʃۋ+ky~wx>8;˧wUbW1Kn% @vIv+o|cҵE%eaq-<} ~x"mq{Ix`*cT@ųݬ1K;zH Ft1̵i?-*;ofӑ6֟eUu׌/ ~%9G 3p8`ƎW 6A0k<[j'niLIxD|ЄQp#6po1l8aWϐObѕ\w6ƐG25^,&ܜgW8'Oz׋ӷgO}PI{}PcmXcO%ϳ^jurGo>q WWArz.^nB ҋHh&_“cٱchd7MҦ/X0a>6fivZ,Rx^#Ay6>7٩rJ8adX6 abo\mXi"o.X/mjxc Z@6V!KiL&=`&FoQ&뽜`e߱JMʢ ߨg[ZDP<ޱl8b y{n.\LyKAX]|DD glrVcKܧ :Xǒjm 2>eKnuS7BZ 2_Pu<=[-o JJP ѡkl]0{~K.DG2(I) uZ;U7^I ωDdtOQ DO2<%ȉaޡ^:tծ}ĸET<31j=U->˺\!D~ f@H$$!RݝǸZJoR=Uyy[vз*,S5o~z#mjd,Tedt/?Uv8U#4o? Sv L8*|m m|Jh;^&hn=%[DeUƘ3=M'SXƿuG O0R>/L |'aqDW^#O-` ,LD[^t w]tQ>o{'qso+/W:|س$)]Ğ\-vviy-}db|s9vG2=(+G.oKihzgwL֩wϞaCH-5[(㧶gAYm7.Xoc=HLDVㇶSbm*qgϓ]'S?_.n;3: xh 86m6B||)wSu=r{˗w3#؉RM`wV[b{%֪<猪O?(h ԘHۯxp1vMD,UOUٵZ%aI֟@ϒȫDBz5oPbTjͨl9V &-yuAo>!h 59b!ņ( =EIylevkfK{m|KQO&'a]=pa{oo }9%*ٗvj,?pf>͖,RnҘ|WI/ƨd)_p ȫ6~ 7ոuv6a&SӨ1+I1 6xM78c[;^mfqF@<{});GgFTcxaE{~8tg~Swn[|p_821q9S^P=I1K'Vz̫E9By.8##3wĎ%e`r]C^Z 'dcm8%RO0"b0Nij<SZOs!}iؤ= Qsg߄!Hgu~E01g#/G9`G甅̢|]LqwHK1}dQ.kZ o\4h?{9`Ɖ?k[u,#Fۦ~zMkPfb!uv5MNnȼYwnTjb 9=>'؍1io[^1mmzC ưWJXr[w~*;8qҦL+%ظ0"pHֶ@}g>F#l"#Ctc۩yӪ?gl||:|VF{zeѵڏwQW^lvȖ7~l>=q>ioXCcogqqT j; 2t^W-yY}%q3OJ}x8Za?W/~~pnG7 ft[s"`ax|aͻg1$B7,j Pv.9v:s;ir…UZ:: /g%5ԁ鵺H?mv0t1G"F8&OÔEWN4zH5"b"-^ϙbd<V&(7d@Bve%6ϧ8~ ϾARAIҖO/ENj˓sIDiV敆lqY0;)4sm[ZKo0Uc׾dhR[$S5oiT;uN/#EVbx} e1S-֚yq*Cr3v8OF.鑌XoҸnNl񲋙izΤbz 돧vv kϻwrz6گ7<4>\b*[R8-Qp/GKgM;O(Jo(J z,D=CM -.?o~xm#oŋG,ƥPv`D;ĶXp2(i<(JpRXȲ{;kapa$-IG;\)F )xȯA ^\:kMBp9jYgt,7[I8x<'cO䕜^]]|xiQojr=y5='xoqS3x&j+㲗Y3xc5;dv}X=Vf5!8Nqގ dvun%OO#fG~~hVe39[0hlM/jtŶ(K qȨc桑v?$?sD1 'ewZN[so7,ԋS"nUVlqUе{, !lP_=u*}jvz @ ӍI)#52)Ϋ?R a ==ñ[`]:n7h!O[~}920g0|NCԛR> ~fRg;[, 1O!|e?uRv"asLb; }\}앩U~;i% oŮ?'޷zE:<\uLw!}>SiW+=ԙlT}JhFsJG$=% LA$ImU]\A_i|1:re(O(F̊ "a/4_OczzCf/a(@3Ŷ}'ꊢuǡ%&ב/ai^G.Ir?onN`ĘrJw1~7٩]CklE+8 's*[,~㌰PDžhB)tů5\ԣc7;-OTОSG|bvK=y`X̰<kz0\ N ٟ}^l4e \A}3.8?{Yz=N-Y2Q?/{i'N\5j1*.g񰣞/3z %ES~x~m FL4q16O/ rfJ=ݎR?wC:px:04'jZ<<OrD] bK'NY5O;~DZO9gUN?N-w2/l(I,G\r~sU}9Vd8~Q2Sq(r |FIҮ-Fl1b6ntO Rq.뮸Z?oGh%x՘p S {b@)Y؎?aVǔZ/q1bH\ஸyT--6C;.TɺSZgk<~[ʞhiBy<$!x6#`.\ƸaLy&A,gא&=+<Ɋ^ ڥ\v>A!vQ##zT|O)^%s](]z*¢aRM˺1؟zw9Cq!ݦ˹S9@Y D  ɾH.&5x9 pZW:/e6MQO iܙD[xt=>XXLgkWv\'5vhP.pbwiGwjboɹx93~$GSx\\,Qp*zm]>!kIΥzTuqxsFxM[1o_Z}0n_8؟'͞ѝNNx!w6r&pf+x^Gɧj鶺nCvnb1C4>v}HBByt.ĩt`s5 ^ظ$m| ̋'er}6.`؟5,m:Nԡ"BUw`}Ց BTYkQ.MfxϺ3B_@r$Ұ/5I&HaA^`}F)':; ֗wY+ C~[0]ўZ+Se%c8 WoKI;t7cWPTBuRveb810IW*NG!&'Gq5ǀRȋTfDڪ>cF# BFc3 S'>DKv7_{= DD ,w4XTe1fydʚ3 ># A^y}Jȴgn+&O" hI[}*#^zuecb};W&;=lo_ISAGJ+?9ܶvƹ2_?ܤzSb-5^~_1{=J? bx)RS;D< H)ϑ^7ج.5d3P\#pb ITNw4Ώhԃ=I$!cX8쏆vyb߇wDԴetԻ:L/IR6wM< BY>|eecQ=n?Ա32ѷN{=;ba 0Tg ۞_3gNP4éi}K0VT}q֮ #b\]1X9[LR ˣx7nh׈w~`u"㲭=sE>~ܞRkؗ~ {\<;6 v,gMg{;y8:b4ó@~A;ltK:O|po[wU]ꑺMsr;4Q40/3E2ˤpǥ+fxRtD؄1bjU`'ǽ-3q?%N>N>?& 㬟Oe 3<Ōb? 7{\=hbXNq}_mvf\a/NCE=M7Vr15I% ';. '0qz^%3~!eW皾0L@RB>?:;)wsvg븳iӜ`txfӉ?awNhf|s95)@L ^ [H>/&_N^T׍2~3gKesԗE3𦶃Cs4}AޔRh_EW|kG#Ծ?߶C:vr8p'o }b$0҅Ը|f+m(UТ_UDo߉^#+l7? ^j\)CA%͉H{IZ20~<V2SG*OjẢ)"=RG탉op+t"JtP`ik哮l#*@~[V:uIF,ug#-GxlT#81Iqjq']JrG $垥ؠ,H:n`ۏKe UcTfOg㨌Rm$sxqbX;#ŔX<[J.ǟ)5Le7EeU6 xQ#|8S=>SCLꌝP!,v:tZÏm2OG!9q{nJsH߬G]UVBB:nu=p@f;v(~a`wv.?_>ߟPm`~N>]68ǥ*&ͻH[^6 /P%}o`JنUqnRỴ^W?'Yi}=Q; raUYSTfp~bwZԓ} ~Gβک\8u{}ُ󃺋O7O؃\W'؀Soe;eyp֔IV@/яaR布g  Cqh9yi3z\?Zl o}E}k{o#Z'e*yYIucdn#Mvi2ۓ1zvxPwuI?3oZ//-< NU҄&G>?E'>>rT;onם8s4fq[ޙ(-8xo׸:ku.ջ vAPs޺|*#GJGv4mb]Kpǘ;ayxƄ邳Xw:bʞGg\ayVD/*Oq٩n?SͣrGgv)5Vqѵ8膇{A+b^>{d|@@/V]G'~i !y5j/_ jX*_~-k >H܇m EUR3q^^{<ZRNsiT_|B-]y3Lm=ϒM̅GiɔtSYVvr:gdh_iS9T|VN& ;)噯#AB~1rԭG='"cKuم*B3Mk&}?G?7OQ*O5xʕX"M?#9>DM vKjRS$b0*g܇(͊$@@gG̎OF?i ~$b1*?\2SWɷ<+,Me?l<*hu+sarߙg){|9 G*u2@[M8 (e:Rc'._zozJ…6==/X :BSL$Z?T,w7s74*/_)q;Ց>Nl 9V'cA):SZ ]ʥT^c7Z!~JyGZ~m Gu>t.T+DQ?}D+fgGl@P*/9+]X,ί ?[@ -sca|2:cq]:ٽk*&jjS?jZǡ3=y5̉U\$;X_V}<۱K^1_Pim q%ןUlRI78v0 Ry agHJrT #.f_b,I.p}_;a8ک9Ġ乊A+-hXci6v'>v?R1boɳc"ޞcoԹdJ#̝X/KX'Zbm~M[YĮbz֕j˩+{} (vJ4a | H{ -M'z1tʝkIjD4ۆp{_$;NM3>0,֗`|eAkLj=czpr Fg+Sv۱Q vgM  /sr ybڇ  %%t_w4>O>h{^գbr"Ħ s|"a!Q8C__Hh71d_Jp 2M1jwlV!Awy8×Nʊ4D~DŽ<&tZlb9_mSSxH鈪b؛l [+,|j@qkx&+у{E#{O=5g=\l9boKrx",VZ8ajF~őU\B.OR@C9.)yń.c Q(ӚxLP ".y""T5/!s62ZP-)aJc {5yӁ^}:w^KGg%}N\eX\9_v}CKN驞94HB\ΖPmM#H=F˗GAï=Z:vdCGEТPo:MwsdCuo;m$dWe렝A;AKLs_$9Kz 4 ̭nT}~[w /,"S=q8zW61l3^e;&mp#f[N5}zǃ&r4y+*]\e4&1='R7ʒkwOux]tŤҳ4{5xtlvc~BXB òV⭞'@d=S%_/ Mc2NHn|cGlO?3iK0oiϟ \vxع|©3v2¡`r 2;'قO_x(f`x0}7 *R ;˄`c>@$3bMI xǢ #xbphrH MtuI﫾P9SxV仂_5zvTpr>_ԓgr?aV+U[>8;^]u)y9WੇX\|ۈeˠSgy?Ua9T5YzHj<>U=bFӇ[\,Y“\NHqI+cq!٥g72OEi;k}v>L7* w K;x*åhTS}, bWx( %f.2Oc#÷.tk½O/jV-F̑ z& leyzxX&(Tl*v͡˭>p(zO$؇*~1 gM7Omp=\)D[!XLX?kZ"^'9&*rnj<$X^|jȌć R<76̾vd|>~< b󊃼GH 3w 1 @vK@;;3PZ34~{ /HJʈ7OIĐc|9|d"^.?YlP/7#<3ӋXl8f눉ep1̙bt,qa~^7 <4=V0&K:קuU+{mFoDתm';I snx"Ƭ~i)}x-b`'pr@ɟ6/g/uwF檅JAUAv6ǁfa/uS(ίo*_vO|4O~AYT>lu;cό|4<$}HNK}5xWnt*Jg81L9JBCbeRuMl}iN.$k^.+܄S\Bfvco9k5/xI H/mD\~z{jj2H G˙Uл>ܯkUdHjˀ7+-hz}T'TG;OW 0׶KǢV@4Ƶl&i?@Hxgϔ-ZkM”|EmC% abz uRKms(Pӻn4I3na<m!jY2O^AC!G:I?62I/~福.vשъ{F1;ȝj|j/]En_'Tz-'q*ޮ SuCBQ\! |Qu+}]3i/(Wyw}MIѢ؆y4?C9 ?.r= c虚pVZ@a.fv]r)|"尗\';:K7n6gZ4\^G#!I<=Z]V]sJlUsߪw[Kϧ)ĕ׾6rKaNX[8zژESf<2ەy>AL'AI=VM=S= F0C=AΥүx%lgޒ-v[GǟѶ_`<.8ԳN,%',pp9է大e扅O65:hY=U7/{S:v*M(Db8TS'b|zv%6zvoJdaxd;k-;;l^1x}׎3Kክ!m=m*Ix{ۑ] pwJ͒<L1y"u]yLQ[*yMڎ=: Jdھ^_o' ]){<'+-ɨk:b @)uјXuTxdnc&o9 K8] 0BBhD''3 ;=o{s" 1%˞mc~+N6{a;scBf7iE@ni T{w>;_Ƴ'\`biܕ|O4ٟ{.3F0\pX nUd@?Wyeې NrVj.N8Cy_StOQA)8\[5k,gG: ZXb751֬n#%WL}Gk@u{|Rҭ];]>=CYgѵ=0ZԪir@cW>ٺN17t9.ClzVѽO76Gl~}E^O6ӡMؔ\\ =}Y3YLB]6+w+Ntq9wf[sm8Mk*fF0]2_C^hK\Fr%7,ӭuk6ãG|GQVxMcb*T/tNYiӷ4Qe-"V#jF&wy-!I;y8zֳc\JN8Z;c2n%{zh rjK nf)z=lSH,GsRJ?p,Xg#H}"w#*5C\zu0*}*9XO(¹^#zׅF:A-jH|γ{Vܴ#T:Fڳ7q~ \p.[pZL\Cժ |wޢܣٞpS0-H䀝7U~tz,L0<ǍrM4ݡv[r{DeA~X>skGaZB*ў ^Yz%l$UpQ?z}L^!t$U}u?NCuWEw ;]1vͅXTNR?kr99vݓ3:n! M{u='W̃ʽ 8+ +*}sI󄪮@:qJ.]K͎75oAP<Qڕފ1luuN~Tc1h lKU}{00w\q:: iJPz~TÎ:e׃ J՝Tͬc"]sTiFx!\ v˼jK% StmqوRK1&,xZA3e(}HxbJ3n |p{e1=|g{mS(kdzpcEKڲ#p;Y84ylr`t7 Ѥ9OL~H~2[;pS=ewjO=Vx 8U` N}*St*F*k,S"]z'yco3ʶFvnp:%l.);qʈ'n~_Snգev=X+kF|]%sZ1}ԇi|vC'U$_l|Xaݛ<3LS6??;}{_v1Ï(8x2r ]L^V?DЃ:-vTNPwpxPvЭaqe[ww<^f3~#/E+o^2K!&SUj-o F;JHeq=ӳ$b{RO>C,KňacWU`XA-5iu<<i["3~W=E֪KqY`rUSêebB˯=~b|?S=孝pg1Yq!ymW Bd]?B؍G;{ub{rS:!jwzLS>0eCb04z FgxD}M0Lpq+ngĖNf ?5\ɸdd|fEV;yjeRdo#hzkXj< #q8p@t܄q`pne31S?#bHk֫$CGzfm̾wpMΧ#G~j_Xc<oSY jɀׅ^K] `Įmnj}9ǼÌ+9w$lKͦC?0=zZzG׉i?ڸfU[\K=!ƶxQOC}G2HYđ4|h t12܁8_=9Zbf_^Jrcut"]A# Y{oНmy[cclO4dpJ筈::;_8B9j`Or> 2lN2/8 J3B-W{с?LAIOڲ rT}PMSG|(Z__qsG.=TWCsPuwHlǓ`YޑMH ]zecm4%ZfR$I\yvݓ\! }LץT=Քk[ml sS'RJf2g\WԱ%5 8/KXێJ5V %W9gJ-Q|wI7b0yM# G@; u y@gI՘'?3=QG?sD2 Z|l~E',f䡣G>bA܅iqO upVIHV;ul oөq>Uَv5 =q1qo|=F.tkK Gyotw~Q5mN<8vެXU]SRoُęik)FDbVyCB';rBS3d&vwm/9RAM}{7-, [f8搌dz O{&ӄ{="G[7 x5xْvG CN=PUDu|nøzqhϼ2IA^0Ǥ t`#J-@ERp]dF;͓v"y-?mMc| 0S'C{w]dwx޴o'UCzq³y4x Kx h6G~3OpJNc,GӛvZ4U9rHUGVxm z~˧R=ͷ~0#M[\j/*;[j2 KӖFvƮ ϻl90%`S<9wmtrx̾8(}jrI[uؔaZ^O1,V 2!f"3HGyBG@hZr7ψkd+ɑ1dro_rdFd]siz\wsre/qQ5[*P c zwǂopNӬDnίzͳbF]QNtRˀi {wc)e%eh[5w%9y'|!˄S6ĤZ.F DT=m]F'W7xZyX_7,I~ޡVɾwf]'^g HrY~<I -w+"N-V^1،GuLЛÌݿpO,yEc}=YQ% ׫EZ`@Ƃtk!1@"1ʠ~>{zueaQ*sel/p+85; =)ri6Lg6 Q]ʐwaՙ7"}!+6G& $vh~a>.B8 CT 2~O6we'ߜRzc:Tu`O5;.TGgܿ_r壎u e^SiQ\ԗw{,gk)?VPDR3x~:%!p՗f#a}$UMV"3SBԩD6Nr;93khAyV{8u6tc_|@y+bP}lɝĮr 'AVXk3S3YD{F̰-٫{{fV0փPwxx$XtkEcwXwCv2OD&ӌ-+*g4=A!xs[пC4L W@O yJ 8񎶾棌 R**x8eooګx\{}p.U5V(%:Ypkt7z|(+Q_ݣlkT(u@D̦ـVN']֖Thv[C)LLUz*a+'<sn)4= 5|nb&xF7y6E/2r+01[a O_ mHDbs&hwXr*:v;]/@.qo:cF ^WyHN2 : ;?3KUtLM;Q5RcJ3tc i 7}y<}~i.^2ZyQڧs{'?Ұ?&VҷG6 `gx^:|>'7\s~PGXWO[gThܜ r3ę~Tƍ8LݺK]G]aXY[+tk]mB9QرOHi'?v@-A|"-8Rpq(I|'$MFG;At8Lu~H2g㭠K򂒓ox287Hu+F><[G)ɿQ2U&*30fg3d 5P-t^7Ȓony 8Paz$=ْf+"h9||elz# wvtDPVV6ZwSM+Q+JHvHrER-Iԕ3Ba5x7#W5?(݄LS]w(f-zmX*wr{YXxk5P7HIXq5LTcAU_*/sjH=UTgxXGdmBGS-_ߞSKCX+8C1݆#'U{ R8']ҘuSotHVA~a ߢ;]cGݕ.Mm.1&m詧b߯<^5֜y6o>jfxIWh#1Xz&t/vouZUr鰝lw}eY [$E$P3O'I1w8L~lk>8G~Smױ_-7Q(Np##tfmr |zqCQFOS[ϸ<ʳG;<"cl'BA1jxicl11[!}>//=vզm2cPyk\$˝BDE&;Trr *Ʃ+4?*J%[>j 1,\P‰3c鲛Gh^?G_rrs*.l%N%P{2M~#\9՛/;zQ*=M}v1TBA^u{F;۽_~1}nACi@ozg?~Bqm6ϝ<b( z{FOkϞwr ¢4*ߐѷioJPRS*|kcbKW<~)#1p:!%ca%{?6=P5ZjGY@to?.v8Ɓ!xY2}6_V;ݳ 0ep-a5{`^R&ϮS9O)LWʏO#JMmszxz-xv3ԋ'_C3 )-) _?W* CAf !i9|[8H/6ddƵ2Ygw)W+`\ό/^b_~⋇(}hWx~` ͙slر6Wg$'28(mUM_D}SiR@Wƈ.gWAF Og3f|3b)(4k* Q'3%ĒBN3ٛAoߜ:. ȝ`+YI@÷e_ۂ9~[Wo}[4x`a헇We'c<ЗR$MH GXʼnvz &{yԞ [rVķ$kB6DqjFPp{Wl'q??YMC{csPO^7_&4'JԧQy io z`~ĉ۸ZV=T|gPÏ!}@3&N=opn3E5le(K ISԷk%&,?8`U噽\IT?1=P𰷷IG5eX x>Gꛎ-VX Nnb P\&z"`8#K-9,?U߶,&ROLp]W>#s"xpϳy~,T{D/_pիl a'(3aqA\>MXNoIw9hh;{ ( mT:U^eGY0v6<6t)Q32r$NO4 7UXWʻ}buoF>[zSݓw@#~/$nˏ-v2S!&{lmv B ZẏS=A0צW"<7L'2d_fD_u)TM(7tl ai|`#&w,[8~h \sX:ov912/6}#`.r+Lvx(A9ػfps`oA{'jvfކEh_  :,W]Ni%oPeƮ6N0][Ub[ %ruA. /J=X䑘,qP#>:%éwY*gpqQi 7| VrVc=:6;m_QG=p=.|'q"Zg)==ʻmtK+߫/؅9{yga⌠>gkІ"U׉3{A+x Zۺ.Wc+sSl͆8 $SSӫ⣜LG.v2U _QAt8!.IsxtnL6x2ʕ"/-TGT!U06z,m5I̶+bLAŤ/fҿ w" $DO4k5j)YL=$R)p="c"gYa'm(yܖ#@%hd5_2I͍]c=aHS_2^$gUGGݬ^=+ax|c_6R|Tm04!Zzp]Tgt;;UJ=;Ḽ&b<_9ownOs5z|wZ[i*G nf;`b d 3d0Ê'SS2[R1F Cca#NUF)C%h.̓ 4Zu}׳;n;z}ƵcIJ~ 1՜e FjIkjtcQnOʻwpdhp2& /(,T#GBc;LXO|xKZ/Gtae"Վ}UFljKU;#(H. q J-LKy}7J1Y? HbvW.wmGjguW-#iad gxz zpawf 8?a2z8Y\ %@d|__6٭cI;6s0UpG t(;5-a4=V%MOcIW:ozW/8 4JHwgr ׻˦l} \**f[lMw E7T'舠~ӷG˹Izɣі,2 BN˸=?iӽ3|b7, z/+bMȹ׮xz<~pt_bUj.`1S*?ԛ fW i \kv2w"ZJ2{m?Q?W άWj y"t|G S+NLl@ҥ뻎 !n|6Eu}_Vmwk3+!!h8+/-{@qȵ/³6"v|e[~վZ`/.@r%mDl 、/li?;dl14@mhړi5*߹玵jjURaGeG󈩻Hl:#!˝)gH]}.H[4)"j=ںh$-9uAƛ-hRZx[IѢ[d]_ eWƅ"]'pPnԙPse/k=vUͿ AQ(?(aԞ U,-ڨ껎yH Y?"J ־G[I)N7y+1L;;ZԢW"Zy <}Kn>r3݈؃9yԿm> S^=fzwrNG^薞Sc}UF^STAGʹ,_F٤FqOnDV;jpœ5Q0?ORpcwGU֓"UO=c ~Od$?H){URKˤNg?cD>_JO -텾`-1;6{n -5ÓJ:|djQ.j\__S_>Di1$m,m9vJ-*ڣ9Qٗ:<<дÂ4%zֿ[*Kt8 Q4 xYn)7kSE1lySp*j7݂}j;N]?QeV^G ƴU8.:cm/wR O9݉; }7Y6Q*zk, QW =)tS=VWϐ_- xJfy 1NI^jt8*\ܵL{Z%xAiam";(9=d/6-]sʃCTBgP(L7e{,Dr [8ܛsʬ^i(3|j3+br||I_oɞʚ /bPkR3KB[i)8wy"=נr.~]L-.r,Ĥ%us}u>{/*g>1ye3UuX+)/[G ={/VJlǑeM ɣ#.(ok6 b4S&5h/Rލ 2xY#t3_-x&Mm~ :9wN4Fgqgң_iюr2LGuHiӔiP KG*TGhN屿dS,2`~t:|-j>x(e.(U+Q_Vx:J*#{vʚ*bxd֫2q2&k/ljIse%i_whǮSޏ^oFˮI޵A'ou4V=^'xp$Gi\jy{OHOߗx>Pk^hz Ayt~;%{ }H(ڴBӥ-B>6<ۅ MIƸGve_t Go?S\De+M=ݶǢM^gFX Ȅ츾)_vP{"K+JUlӖCFIICɫ[yCe%U=M)=iASxr#iðQ[NYѮY埂^g[SP靚`R;cۮ3W9/=t`8P="(O~Q9}GZK/I6]&i8e*QPAF2ܕ-ь*??/WT1kA _7p*vged1 L?M۰CRW޹q϶@9, U)˩1^/?-~]c7-Hm)1˟b/ {N@Y,i8 +`Ο:5~;m+-)wڒOr'kWkN+C=AAt=;mV/պ[W}y*kUɌsd^#꧂yVxڭjJ*bl-:ӭFjNM  Q6O,u}c4,z\:Ôplyy ,=(wFŃ@|zrR1nc|,̂7b.~@aNӀ`Fp1WxfةeE @=sr1\wajJDtאrqx* hkJ3<`k<؎[u9\vW=vhwaG:ދY,; .`9Q3YDP3GeFn\YSA=a7+~$R`i=2 =uw7fk&pI/a7[)S': OIȆtRoy mauKiGxv£׸ 1E/<~]8vAόj25OҒ?P8긂HWL ҫOP,9uэQ!ˎ2c)M uVVs*J'< [jOm1[+Ͽ6ێLrᱶL W'*LKݽz*ObpnMȒb}nV1o ^xмcw7s6v.Ui[֋/(C=ֿSr+^/6q K@fEb%`膝z8ܶfPȯMx9Y%/VNdlwrPC0>j1l,i{Jn ~F+ {.q N(~?X,n6D%ݶ!{ɸx]Oj0\{/[̻|0z4vfY(n=>Ex8t)5\]գRƗ[Nur߈ԓŪ3[|m@ Pѹ3z.Ч%Q$zi?\l>»*`TSQ?YX3w܆Doahb]κ.5,{!AfLdAD[V9p_?0<,v8aw4( PYq8u7 l4'{HE%m7I]"-;rgÖzk9cI2b[ðn0zB+kp7/ d1Fu<wcWH-$*BJIU9! [CqM(umgCCʄy߬ށs~n#;3{}8yk|3AnC-{sf퍡ֵ{]P$˃mÑhАtJ=[}b$<(酝ͭ_"eVكF }<̙O=m/;3?>k0!Fpc]S)]7% 0 A>(/K-hUD?Lq'^A0ݡFqrm=`H^Tо<ܕ5)X9dmPwh t ސ|`&ܫEܒ0 %r%oS^֑uiNwWcܽg$\_ݗ(  dt᪔别[=5SfL~Ӱĩd%SMQbZo༳4 \uG*/ř cA8siVN)0~iHykN5~1vޓJuw`e*h[t2BI).3sIBs޻^{|{ߕ^X7-'e^cw/Co㻏2<}^2ayY귽ԡׯȿf@//Lep0$vO*GvȮ joCJ1{m\zg׮wU.63kgV&,J- X[s}l92i'JnSɕ5jÏ>n/;Np5~|c^.;+ã"N(,+-yK`59#(bz"tޛnx*xʧx4ͦHP#/ǃ瞝>Fv{%a*#HLx}c[sWGԾYM͓ Yv‚IC܀b_~h,'EҌCSBUtvΠ pt2'wO2rl.<48`=1]}gO[iop/PY27o-fX礶oG]_.fc;b]< >n|/ <\ +'x( zE{5{#@%F2<6C{2"n1kc*rY<S5Kz$ӽe6ڲ~Ӂp$#7pk b80Ps7KP<2*${4 ]|0ur1R066V8mNx<\7=z rtI:NqPfM)UN#qXVj΀ͳ !OwbvW9gN;z8AW>_1p4+ %+]1IN5SaP`}!B Ku#9۶m6@q{9:gkai:w|z Y rz}!Q^Yv{^I+\?qMI+3.nw aMY HuKqnyl{P~ܐeMXоv`]ԨK[oQo*%+y,?!y/V5bIݠ]hkY*ܛ.)'|vޓ.<b5ɫaRݍE#Vз >{_6ZC~n?ic4y`6^P]*xs]v}xxϴ#Kl垦 z߅Fe (.☹:O(_tdub~ަ~ìs$7]@-ƪRGHvoQ{Nc"3=aO߰)3/=Kd6Naa!m']0/5CIN=FIq>!fI=,ԓpPyO>vbŮ}lc 3.V٬goʪzz`j)z KָWi7m~Jk:aQjb=fl띉ggkchRP⅐Exsþ kZu),1M,=y`C[ǥ)}X,W]`|#؎z/QkJ'DԹ*1;M)TYK} Nu7Cx4SoCk{iϬ '󀻼ӊ/ob7$l2]§dƤveF܁$mO76vb4hy}'!FgLSiUh_XkqPṪ;zt,*G?P[S*1;I>}zYhde=$  WP|Z.a_>g\29 MAD[ܳ;FP&/m D"/e{4SΙ<igF$Kw&],r|.jR &$:7ؿ-%ll=zx\JW.& 'HégOw~ߍ}[c٢E(f :*0nr3w`6rlC4go|H=+V"*%}e0 }H$#*z9z ;%}Hhuk^2\}&*.jtߪZLÁ#YlVQg嫌x&?퇉 3<_eu˼}TX0zt{`bx݉~ðbcC+ /> =2 1C'<,{&#Z&CMO7 k9E,A=<O0B)5]s>m>Exvz65~㘹e֘\l8T%Z%:' otʚ?n/z=@kRa?ʭXLSs, GϗOu%xtNѦgZto!b` Ӫ 4Hɶ"/ zY;c bwiVZE=3b̕47 ROrn3||PcoF5f{5kjm`G4|3]!ro<j%}އ&3jWdg#-l1Š. =IJ3dĢ<p9?!{i"cN N/;ţʉMQż]3OoZnHcߋ <%-tX]=0 uCWq  <=ߣו-;&D`X;z c֑IJ#z8WQBLgnvHofdnͳjnG=aIc/͖-ۦ'Szd^<q5Ckq4dzj3QRMiL`ǃ.;yv%0M_A{|sr Y>/7&c l1Xqx,n3 gdNJl ]5{+n/Ro5!Ne &+2̘y2̀*e9n͢68U5za9g.icͣ ձ+wP8>B-Ĥ˜ :\Y:Yr;<*e o4x¼b;؛17KX3^҇D`+#6"J~5Oj @O[m-ۯ}\ 쀘ǫs~ZH WFI_ ynQ/ ' m۷Li ]6P8"lij%z"9][.}HjѶ%tU-0$`Sԧ6 %~z<{u2.Z[q8yjeaeonFeU-ܽOvg>bVȾ{ ^86w6OζN^B|@ϋo<ڧ?婽+QVY~:(\E(wuN{')w_¦ufLn'maG=1ޝf׶E\iq#\yg fY5V5],&}s,=Y;u#L8wC/Nk&E3t&:xޮ|d855Gi Q;wHjxwz[kiJ~ܧWac0G^Δ QAT>7o;_iqc]"x/Ow2Q遤ANDؙg5rU^H (m..Cx967s3ae ynoh)BG5q5'Sޒ(" vɴ0.}VW%."5-zH`c#X TtoJryFP: XC?h;5FW=^/tS:;:sg\I.^|-DE욷m\R5#m}(h5+l _']޴җuI K%ЉBPA W O _)/Ywt1Тa<ƾ/uaa]$hV!p-m T̃mGG8U'4!V,j\M~cxk~L^msO<^3ZOg1y@ĠAwtn'lAhaoe'b"l-eY)У~:LQ~ :^JT8 b<!'1~mw\<,yQy:qEKS&7?qß5y.` @q:I*= ?(~_+򭮛4g{ON]}vvteƣķ}QKI\y*nE{s xC+WC\-^xJDr!"6wHO| 9 Mz)/ N*8+ϡ#Ig3q]jFȋ{F6$NW'.ZQ6z;p$5,[:R`8׾#G`s\ztP{ͪT2zE:G3T}P+_ungݡ+T+vߨ xӎ{bswv@bR-)z۠]u*Gj\͐o&y]$Yk6rwL]jh2uӞ7ҟmҒF'5%$CCڣ`ؕW%ؑε y)_ې<| JÑdcT!q9~ ŏoe=}d3SoרY{=GT7Ɏf9?"H.k}.~[?,Z^×aqĘ= cVnFܮJyr&[)BssÖO|WsDŽcQZ/4KK^|6ϮW_z4j`, N Au?)!GssG+>dr.!=4Y8 Ybf‚p%lpB=ٚlVu̲l8^Crx8D*?NINYm,V+@':QY7;Hݸ0'X[Yvh:UoNK1X", >8o<obB$U!`;s| 􌞋xMDžz06FTd|Տ y)Zp-'dE̻<[$_-y\@PdgH= Dc.<\+,éWWR.nb1f]GhF{ֆig< [bytDBW3z`drwrA^eٵP2+ f8j_SxR@bz~=~mEb|vjysم)P/1HNk9cb/Twjy`˚=ZsՓ5i/>zqsKezV`^{"hMz~>+ܗ˰QbyY:(-`wc+an'l;^9r"e_ R2b )W$ mԻ[$K<0lUlVp_M;?OiP&DeI\;߸ YnӔ6766ܺsx}ssq7?jejwIkWni j"Pw D:t=LDoм|ֿ+̃G[ ;s3ty# Orw;W SA$#P5$L^%޴{T~_+)ҿF! jzLMH[\IwCoA0{[Fn{JX8SW/ 8U+T QՓ"2e=϶V_;J3/J_𮠨 FC2:Sx?u3D&Bon񾏱8lf|]gB [ꆚJ|>QR=PxYEeJ^ʆdE[X0%y̒*R;vgYaB]|ۧ(56؁O]u`v,<~H,wn_;7m?ٖ[gpf~1}ik1JAeWSңg jӽOW>yijᛄD{կ-5]- f0[.=yGwE*OɿX,Au%w `~^;aZN.N'QwPBi}50I AdqdB=ϟ,/*wU\ jzy\A~VO'LRxR7͎f-NM}Ȏ*w~< 1f).3L=)E]@6_^Hb,W8?@aoRͳi rӊ-V⬞nd1#E=Oh?0o%Ѵ^ J'q 1>gbMrD$/QMJ rylVR<ި[I=`GO5v>`YirdZY ! ΠA=nI#ء5{,krXFƄ XPZ$ N?_c0[)uGb!/*kZ0%~!G3)+T_(-.rMO傓'(Fix"Jq4 mkTkg-wWYPiGk[fvW5go28G٘*oF{*nВ0N#pãVaR+׳Iu솵:=8!{mჴ\n<@?ko= Ge1z`83e\%)Œno݃K639"N=b#[Ɋ |ϧ%ȿ oP<zdOX,LhlLsTb2j4Mz fQb'>:4OZ󄬧 Ga`~#搐f ɬHjt7(X,Ga8YVV З'x̟c?rTn}Ʒ=ʸoSB,Cg7G3=A> }$AoNPncRnY75V{ xԾLiG_꫾wڛS\pD\{P]v bt>0O/e̺vFDț\Y5تZmȾ--ƳV413lA)[eOSıZbK͆A.x<\J iܶ+&WGL/<9 ƦC%hs- M -y9=r[n9 vVиC>E^۳!JIck&OSܬ-lpoN˂\K=|i4r8_~-wTY5kZYRl/F_ 7n J6xp vW{8U1=_LĮG )ҟt5N !0Y.z-7WwMzc]N0݉0Mce`ony :cωC1+'R@73.n/Vok{Pnʩ{ashvVs1wfVlۏ$e&MߒvK7BUtQGLznQczKFG&v9IׅK;%%*}zq-MK_^[o87B/Mxtǽ3b>ZcͩO*ش.x Z6B-7!lYzǻ̊,Im ]ipZ F+wYnI qH #%k++-yv~pl'r"{ړmCIu*奌&n"J|/*|T_)2W],;IAn*t۾/?ŽLjm22;B4³+'gŌ>?n.順EvDVף<\xFQ#n?HvASF+1쀼 5Yd,Rb7jlt?W! vA<~7R&I©d=$P? &'x!& NĞEzw1gn1 ~ #<ʉzՓn?j[c1fxt 3V zxOƷvȢv fjx,ˇ[ Vp5TϽqn' "_a5eV<,;l–2dUG |p tZ,+)"_iHWм2>УyZ=82۩X5Nbi,N~k~~y3<.O`@F3\.(yL[A>Y#bR" ;O3bpAx`bxa DVՕؗ1`lӞb13L\vxqRLIZgAb1(iLP?):P&lhz|nKzf9Ó{dNf=GJW@El 1@ Wzγ=XL2'Ĉ,k&gzD.bn,6[yH{9,M װ+6~ EVPǑo<SPvXj~̃0X]^SxI׽X5Bb@]NCỂCܨث cmfރ~] rߺNi=ܥۂyo ǓŦSxqa(KoA-XK[U38#<4, v\v?^ɦwԕ1]-ȇl>%,]|\Hg+^<~$T/9xP xQ~ʳ "VmHgh7U,O _|<~c7U;EAbnS `_UP]#I/ÌMWYYxjgbYq"27"j0 DŸawD+uV#E|ws#tڗ}!ݞ-ZN bGpmuhU_,ޖX+!cdz"y8僽9  o.P*3 (rѬ(|E/v5|AUx-]$umkgL)3 ׅv 3/}q ^o7^niCm0X kTwkNr JCT%_| -wpbLKx_8]c&GBA҇795]L;%-7Nw'ԒdtWK{cG0ɿ !t/RV*p.?i_S;?(c.mB;jNgdco/fA=g6s{mGR= x {"SS.V7TD?vỗzhD9"wOۺ>]n`SI*>-qèx;Ko@eՌ!ͽ%|{o3*rWwq?5ϞPژzrpء_7ꚉwx(Rwd: v33SabrWߗZJ&YΣYYoj)r}ȚS!w}Wiy^X=PHheZǪ!$ kW)/RЏ^ۉ?EDyM(/Չk(e|%G 2W~ <nuO?=K`sDw^{d7LG߹1ZPqluG%IR]<ʦI<:qglĚMW؂?ޮ-_#y(ӁpObEpCpzgĘ{PpT𠙭)77GD{]nsL\IJ|VoXǣ \ ‡4QY̵X5F O=02a!^7kĕs{tgG&[1># 'NdYd>4><dgxhhp).gR$+j>%驖h[p4O-b)K&xv n=bR*c8z pYoFGBL]JY*lԳy-6WUb?lYVAآ6Qϊ;`Eb 6yV.0v%թ${_ 7w F-[h' 3 7hgu:wJb@aiCp1{a4 nkk1rR[-/=?fQT_gGxrqxΘ m7:i~OBeT1#]w_/pA5a}s8r߇x(U&ĺyXOI>X3ML,ݷZ)g?X6?2o1X-iƽ^K!"==,:Wd ^YXIx8G1bJ ;R4;(;: cob=Bhr%7i3R/|Tw\toRqF\Z{NP$BܧtX!_kٝO4J|0=TXLNViaEt%~79 MTYM䐢~ s+W[nu]ϹR`^@MzRN##׻FWNcSkJLqlo C#_Ye vPTRS YGQؕ ªhBۏE= $QmܙR1_V`qHzK@nܡ@+ yޑK#aOwS֍~xeQGow)̢]Lmt%XK.TgCfPHu$N@u֋:垗eϲZK =um|H_2osr'ԓ}^e?zZi{1y'?SN" ^{[{jgo1[,F<<ސ!vm]^;jLWa ;]YvXxo]iX~E$d_Q\xx]{@:WvM=U7wuusgr823SaE$ZҼuƙ|}]>;پ kuya|\GjBIwBk7DIS$p(;{rRHפAӛ{ @~tl벧_ES q";zF ]b0_ꉰ |m. @y1̊ Yb|>KٝiwE[x(`,MJxl<;{ %om'Y݌!ގ kPP|$~V~N+Y.A^ >'m !˃z˦c Q\bNR a˘8y.Z+uҠ<|=Q޲؈SGH:2Oγn5ߗ?x,|:k]9YTgxHYZ%Ntrѩ$vI=)HE]rp֣6gd9" 5ƏHrufȎ!0,u\ 8 6&7?Qk23u͛Į+Yu-mJ6[&NڋaXz82Q]ر,* 2/wF K6"kXˀt>Gx^5\ ͗uʠK֩D;*3xӡ1p6vF->01Ī\{V9B Np72-+-jp:i^\{2ȱpC 'n./ܰ mz_mu]$^9mѡYs'JD"/q}b ;Kq {9mLhԞU/[ױZ N _E/pMTSCU#Z$Kzw UB2xL8wA:|.~}b @!DZO|' ⅽ4kU)%y)/s+FЗm"ƚ`C32MD 0,_=[}G7{ M&PnocJ1QS'~E&jggvhi b_>Ь[邫c1`òw6g PO?,~ь{E ; ^"5zgΰ#[C5ӫ0ꌡY[$)g{قw\e~'@vߒKG4{5lT[g[TkZF%P8켣&d={uC͕WrOLoׯ pj/|G"gӜ1'5X?EdUBupO`āY$,}[G[L}o[vrVT iDYy{%?ݗʬڶ-'=j*V|5 mxmn_w_nX5{+TZ2ݧ N.dda,)\08ضzk6.wxH =Iտݻ-t܏;j "v`(.8ANގ:<@ soA$f-`o Q+/s1*d֪ntՓϠ0hh٪Bf]~"b 2R <[j51OŚsEy:k5N".ubN*#I0'BZ;u8bW7JJyXR=M8G#OLLZsyPa9Hq{J33G6vy:R1#Z2&QW(l""ǶW4?3#m<)]kp-3b1N˦kcM_<AL@JJl>^gz,_,^P2rg"bNx!2ޟu>/j>7]/G K EWm qȺ{,/e{Hw.MRCeŌq{9B^@/b W^J?$ t;+~:6jz^}p3x4S}ߋ~{l ~żkW$J:-Kdp{M }q8"}*lrКT+ŜC\q*ߓĈ\^8]\#N+]5Z"l"EFnb?\y;t *6c(;SWEaF޼x/8Imj+b#[=n+ܖ8CG :ҙДhA211\4rZ-}&қ]>SxFcҷ-͆{Vj ˸\I1e:g[v <ޱ 4w#E/qjYm4xN(z ȣR܀$pǒ{xYUwOogLjuԶT( _/| Ҙ /ҵ uqxwHzNj`I2 aWטP[L|_'K>-+Zc}?}J}I5 |xT,/dߠK"!856|{ñSXXzrAID&w~ 7q'CSrlZAܠ>Rm^"ݑ9[ #V&ؖf؄6VC4m~VOƕ;z6Uw}='xG j1ukث%٫BIGMήr`~'iy>þ< R^rnǧtzU=Lk/Ѥ/`B[ɘ_c1˵d>rZGY۸^\~>+<\؃w+1W>^]oqbWɄdu@G<ǥ{xo0?(=G~NؗgRgw-*v-cM=/5?:\ jh,oq/]y7 ]z|FA=2ںG8zW׏~ALޛ-=f%ƁH}\IM̮oPTVVp'+GĿ^os|V(مWpguh^s|{ɔ,I^qIbRƐrƁvU*쌣UPEux;^,^H4jف 8%ouoo QbEaɧuk3yR.3%$nSd,5dO9M]/$=.q^pꦱN.{vhG˜()IN׍GiwcۜE?5=F .~;"tU:haP;8=p{מޚ.R])Vk>6;QV,#J7<#9EP3,G23/%pZi x omOt*(tg_,Cۛp}}- vω۰IyίE{PaIOv%եŠj`q%5+fc6JPWk?]Ux./^p9+c٨bUCn8(NxDZc `z]#<郎i.q nKH`=kK D|:|[ g:)Ikr pْn}zҭGfeЍcͪ$@+囁 ʵŧWdi'T So~^,m|8R$6g.Z `G31χ: xx?jaG `S(i SmǑ7=" Uyav׺6Ӹzv\O ُ3i3kʑ&1,X Ak@r6~,xO<4}c̓m:dGܲ/<-Ʈ龂84[;tIJ x=T:bB_(lh-s[ S r\c&uAkqrٽƮ\>!)t˦:Y̝":s,Y1:bፃ#kfĴ)6Ef XLWK%*c&a?+"d(gq`B0G]7:o8,pe|#D+J #7M Ȉ%wȢh*eKVbPMvGnvaq>[^1΋f؉b>K_Hk/0M;/0%3b ^   ҈%AT=-y$<β9rzLo߉O{x|ae8K\ct=4=yEzExUqAkz .ƶ |ˈ=z.v{;Ag|]l#4`:*@oUi_9f:`ky y 7x:yR}4 A1/Y<ᇐQ ۮ y&>ҚSo_P@qMN w~\LnNRC,rd*xDAݽ[U%ΓFE:ui@y$+&y 6{H,{/0;:}1NOsS qb2a;l-ܳ$8]s2^Fb*5 #L5?vAS{B>Mށ)%ٌ l:6|uU橠i;;U+^^YknaɖR!Sc8en7Rΰ037^jLOHSctC<6ʉ/Aƌ?FlЎ3͓~/<&d;ȺY{C=C|FلcNOsDPn;"8|TvЗX @/ 8/HYVHU-Z^ߤjcO)P6ɞ(>*&M9rWHHxzs럎Zj[x$jXӢo;AX<:2-DDQ6K'}ltQp .[ 1$[^VI Kl/ŲcQ˺f{{jbJo"mv)T <91 7e)wq}ޕ*S<# /=i(w3K~-by:_Z Vohrѫy4ӕ;Ȇ}V(mf";լñ^o-?r<}=JJpB7`~WDڥ#^|ML.8  K-i{ 7UvѮ$|ˏ4AU7-^L͜[?%_W_(ʲ!׏k˛'Q0̷_cai,߽.{πYU3__x/k?w 9~YY:N6,D]EiBQ6o_?[x~}&v`yx3O ,@1N 7Fe3G;֞B?M}s@k7C[hY.C%7IўIJ/G%@]4ՖUx,klkoehùi DOͯ4QϧzYjx}ŎtG,C#b= @^p? {bv)_a"Az6tت:I=i̲\iFTjsYc"1b Է2oYCOSIkhBK|1C &,z|| 7O[n^wbzl h깵^,86ACFK ۸_\bYGʃ旞x >TS u9!Lxؖn=|cL$xKJzB=?_౷26ͱ#8'/p m[zK<5mYG։aY.UN'[joAS'\?7rؖM)\5 Iy4)ҴώڶYnRyW{IY^+Nnoy|og#*bo }ĺ,:HOPy^= a:j[%m)k).==kozkϓeFl =y#/2Li :Ջ+]FK'"uOU0-c@1nb TgZ$1RNJohϼ,}ROe&yoJ!dUO#KHGq7 4z ^1{8_LN4vw:]oReyp"b!~-~ЮRZ&T^Ʊ7\qqAE=hD= ()) 7Kzqw+{s'wNIM"ՇB KۖAK.gnj%7ˋiΣ G[}}Pw7hV羵΄'d#r,žV'cNg^+ިۛ$r\x;tؔd(KnE[װK0{hpo?5 wY "[y[Ij7P2}hy]MH'= XJD`bF qie\m ; jqK:o 5s/1SBzKqx Y 2t p8wN;>na-A2ȏmW excn"=ո?5/dCO{|YL74oGށ+I YMoQ]?iP5 F&x3q)뵧 P(Y[Mnח@n5'}"ve۸|F-1_b3K(98.F2=PlDz8͟oꌓfq _N·|$5+u>L /r)>fhm8|4Frk[KstGev&,_j77ۉM7vGB $gk=}G eQs8|nߗL{Ϭ5QR`mY*;k&'8m?j bXl>?>~L6 eA? GO=<xlOn-gƈYl p/y<odYeyK!e13|U]"{8Z MnV}TA-8iršQۓ]Z3CkʆnCD;ڕ1[A')\=:D|8xofobκvw#< 3(ٛ0~X m_nJT?.N$n_i20u\GlQ_ AjYׇۺu\qX<+xM&wWx[gQayȘtCrZCv7/}i'+:.;Q͐A=Z`T]" ;+7:ZNvR/odb uڲܞixMr) LЀqg\3dY--{ʘ1loZ$yɵ+ G?ꡣ4iNT'ģ.7m7_,JAxV Gxr46ak 8 v._Lc4OpcݥlRF(y aTZOꯂ'Vm [?n;sAl:+HxKYcnp/N< &aNkvٲlк.ͣet26 ̓՚]z>Z<7cH~[uGzGԻe+^9{̟`0zY|r]25vt潬PP+G/xʪȵ}<{qѷ3cáj$rU0C]N3X>ܗV٥e{\$+G 0K[& jW4^)0nOM{}Rא;caWKiW$~mlqz+^k]+ Ѯm+{jGKT.@PS*ns[CU!k[J|PqMg}8XUVR)Ԋpz63s}2J0o QwQ|uqI ń~ K2QncJ%u(o;w8d:XT=Ivؕ)6%ޖ̬U]EqN=>RuK_?";߰Q›yܳGݥ#ߓRO)㝶]'TPL's4?rgW|t.?'bm."d?~Jr-_nW<ŴIЂl[G^c%~QĞ_T*fi1Z..0Rylf]qlDTO%[Qb.AXyuO{x:> ΟLmU·Q=zlUJA%>iI:bHcR䄜uA Dy<>3?RPczdzOso1Yc|gW89" YP7|2֎ĎRntxn< C͸I{g\p9UmưŞ>s05{9`&x4k0^+ dX(8rW%Y?xlSKJ/]) I j1'i6WP_^k"#۲'Z\480^\ntJr҅<r;ƈOcaHkӍ^ټtSmAL&{`晎Pz<PNoUF M1EWXeJo,O<V{>zuHAP3{P1gۥ0:{_ڥ {eyvx9˨ruz6ICg^T3}z3Я}rx>zX#ztRހm]+3.\@atvdǯzU)baogG9oYT@Gj6hxC Be]?,t<hM|ţ>) kL& Y[m?@\]4\N!X]12Vtzxfk"x̲ZF=ƩVBd% ){ݙ4m$:?\?~]igzo/—OPx8+O_;_M%ߨ'}:D7oxu˒ Gda}n{z,r:PTn[ğ|ԯm#a٪ұ0sl|JǭYmf w5DzXh08{_0_I#.<g417 nL9 T Ywh[К[ h`f;Waq9EA=hم(Ʀ9#Cz_3ba b܉ /qo2? M{D:-n;:^F:*ag|~m٭6AfcY6ƐDwBܪ`}h/S,~poe~d##ЯZ?TUSnZl#*Zi(mH4?[l(ed:lv-^v,2ktjKG>9,V9Yfx/,-o o>j{ͺ5 oeQӘ8mHz[^Zl9bkgZ$3[۟:עoF}@L2&24 </:mȲKXU}{O3uP&%1زٺt#iZ5X0Mf 1ZQ=~oY齵Ϟy>;yj3,kIHԉusmbuDᕎZc[w}/HY4}tYKm1&`0;ֿe.:df*D1@Xˑ덛!|x7`ڴ/gmU/m9텖 V uBxuA!@Ǒx`#s(Oy9(<Ԏ1+p~ϸ?x<ʤnx W:VXh+]ɕ%3WGg2r:#&b#7wxYڿ9cQ ,դ]^Yz oRqf%:﷫Zx>@0=lz~/'MCpzнe6aUNַ/J92+tz˛}|3`098/dQ񰘰WvDAE`7jr46jZ5߶~!!]r6յY #@v'{kxYn؅YnK%_P)m~IVc0kXyԣ^CHƇg_4~LB^ldzrxE-ќ=)9|UYc֫\V;2<c)d,Tz>Xo%^ӫ|ٺX,BЅŐ4`|dEۻnxilD69AЙko)+\ŲUh⒆Ww75.W8&FG*uʲJWIsS%9Hb﹍yҐs#SJU|3z/|{{j龌ySTYY}p2Jh#>ݶ1u:5vxP֬em^+L#G<+BMe`7F;S6^N^?Ϩ[-טn#)$0O(Nb~W˪cRm[`tmዘ77ݲ6]@SWV5ȁ#b>o:DmŴ|/ 'p3uzQOyvYu _4ӬzK\G8/b[_ w3B#4:8jo, @| G}!!(bjskq;.IbM`kV͓H+kw`4Ь(/Q[~>=)wҎ-|_ Cý+6ᄋ06MTP%}‡TqVMEn\1>ޛŤ1]^ esNb؅'N|ڔUFG.zqouf'sRڤÁ9qS:]t>43ߔLmeR,}ww/Qlv0QWO;wNp6^r痗7'-I]4}m9\=;{{9V^鳾>'g6$nE[Å}x-ɪ оVX4r{J>^'=dB/ .*?r3Xh-QY 4F~_/λm t#@T'͕V}]LE[DQúQdlzwr]|J3_oFם5Oooa>ؓ,.gZ͆F O #հ[@]3|XTǓ.&>#;/T6 mKk ̚ Ewi`JAQOnLiNHlZ0y%޺ 4C_>]{Jj2) }oZV2fiʆwdx@//3i<G!k̬~vDgx} Y~0q\aDs1iY:pQ@wKIn94{&Ɨ\:ԆVtp0Bl2L&7zDйkFB3S8j[+[g 7[6ҕeꗀ9Zs}o\)I-P*F[KJ3x-rrBP<#Fx EI"|ÃI?яwߧx {gFF4b~ş ?bvnð3CF;FC*>sg, xUtVU b7mq㵬:zCZ~f* n']W7'1s; xmH[LmCz: \w ~~?_/~o3rXytBY w~rzfb "IG?Gֿg}e^OP:U|:iLfuK&]{A;jkG䓱~~S~GbGmx1?~寧#l5c}X?<7? ~XK8%f>Do_`NbPkAeL[n/ =dasZ?b-V-~7X{<959,4>a2FVNW/N bw-ޖC:# <6~ma6~ K EN%6X_}&|j'|^g*;^|CnX^G61)q ~fųfl+p<"qoxY7J0Al/̤|k1?>اy7޺e<wkMo£t'O?2< ~F,s^@"_ VK.Exj~Q7;!ż^Foަ]Um@o۹YcJ; C.7JZq!+tncT118)`kM׆}  F+0zbx*ƽ^qxI vŰ߰yO8G'+\}Hҩg@?sҿ/+z'~Eu~M_R0܁g|/T_$ޟ=F"saͺKW`ǖnQ !FNFCbI2Il_GE>'h@Ʒrtb9u?OyS ꎋڡ}pv2W6E'S}@?}]ﭛ?`?\f<>}p(/Nj}dD(zmkq2_o+EXSm Գg*1_<=?oeB"s$SB69 fч/{i5P=`0妞_bi 1aј <5Y3mBVQ۲ec=X6Xih(3둯]L^^GfoL,&֩ v$k(7KQz],ϗosWộ56q4q+c<ƉLWz[vEe\ґ 8̄f'x4ƞn1Ǔ"~W乗~g]/hZ&O˂X6,xb0|v1zYRaMI?I##Vkm+>_U|D}{uW<-ҡ3?I$˳cu5Mzrickd'\N}O  QL}C2[O ~W6}YtM0T .v8nx#p[*H'kHA>&brGt)k?q'ULp=\bjy=eH3pzet\ ֿr3Fꖧ,%,c}2ouP\,|w/FoŠPЧXn50?+[>/u#[>8ܪ^6ƣ ]1^uϏ^`opq/THy 6u l3<>rL~7qȒւpIyIxz'/ ]6˫߄%dZˋ˕pnDh9^~C#h_Uq#g&/zarR.x:a]\6k5F:Əx\3` K_>>־#1Xja tF{x>@qM:*=bQ5 ~^"sZvJ9F+txo Y%x96TiRAb*~ZC خb{:af׏RZDzȎߋ7|ѱoBƜ)AtrAP"?G(p$-~3zTVS68O;[ۓkfttw}X#K 'iӅ`Nّm_2=qS·*$)=VKOwHvH"5/l}@'^7WAӟ/XDȉ~GeSD T'(u$;ͥA/{?QXgY]з%9Ydه|$dړuIf뫜/ON6VشL-(D]3 |}j6o-W8wPĽU>%G 0qX4{sw6w*[oO?O( {gKsT<~P[rd:T3ATMem@V­3QJeOmvQN& qɿ'ivWfns)8~o7w֕S#\?{2WǛ,3 6 xh,$}6v|{c%ٶ.O9<ϯjo#] _y z'j>U+bit"^}5i]׀;0K{&{4SphtZ?LJ/߯台uCأbˊуbw.?:W'|D1m9mr ɚ$fLdG<9<4,0xpMH4m8i|lϊ;MFkr,*ls >#1zyE@l_né>e[Kf`2/G)| |cJz31oErا=rzQV&= ztŲag[DԂqIvyn~Ck\tpp\O[AI%Quc8#<)g(1/s$u."{t_uy^^|"Gf^?%ɫQ' N1WZy}|ڲ&3vϒ>kj#tȋ uimGq Fst%An$FbƀU<]_Տz]v==-6HHH6AF=oEFDfDf}.{CdddddddD0h~"GqbaDKYT|2l5S\h/2j (g3oh]MOx >;On ó'۷ 'у E9}ѳ7G_?Vs_ڽpMW=*C>"hL>eXXEא}tN35liEGi蹁'ɢ<:u AzqG}~:I?<%ޓmjyc}41TE/h?(φyw+({ӛ&)ĵU_#]r| ; $Պ(ҒMdذZC!Ekop<úOG̈́ 8vlj%gQTx$M;'QgqTuu5۰Z6⁝XFpDLz.ee6k"'={#Gn4NudJ)nld<;lH{;}ϫ{{ai+p\n4x}\˝>8^G ׈c %\/Γ0I",Xd)bm؂r:4BN䆆 SX[-BG!GeMm\-$L}NЫh V 3x,8>,>Lα Pbg7zf^XڸqS;S6ЭȢ0!MMW(|`$GgIpdh\Tye@!F`1Oyb k;uP`8F!.ysG&v*a.$fR w80@4Së7Ke;]V; 79kP4}qEV^el2r= qʼn( 3O;?o ~7W?/y>W,(' o|SVn`u ETpa'u.ȿ]o7 8Tg{+rgݡ^T"QWKg;=(nmg?G,o ʹ{oJ?_9faʲ4+-ǎ+]5 e/ w "$]y.|gX;.8[ۦ+a9<.~rσs6`bxwM/y9B?v }=k_8s]K`gC>kh,xU/Ҭ`;&aoxgAiE ?F }}ׄ+_ppOg^~El_Nw? z5Nn?"ϰOO >~'?T>N.%_ yz7TYϰQ.<ס .X kgZ1>uCP8.~@_ s9peW³2;VEJø|؏ tgw=<̅8[υ/|E4w-~&nK.~ۿ>(Pnhk^}Csy@/B|.G}9< {᠌052˾|?|bًzIhG߉w|vx衇s9+)be~k} ,ww\ %W2?5wW|!܈p3@15)>p-|+w D,䪫^ :WrPaCX: ,/c $kx]WB~P[zM? 4 =jdleH\JTA Hټ5" /46sp-w ,WN=sRBK볥-eb4u, 2e6_q`v!݀maꉳzUSFhAy-AEq^2xVqte/Xl* rVI*:\H9>.C #K٘S5oT8'8ncy]Dᕱ[ 5;E47,;$zV݊jaB ͱRxP7e5g8޵7a@/4qgcW*g`3=BH$+\$ё|Vv<~)i ʷR-$z ЄloDfy xA|hzɄSm섏[ L8&->.:x`C'},)e4 |DCn}@ȇpNL4xNҵ;V |{ѧlMw"ǫ֦YEdGDggszx!X 쫓-4={!-U :]G\qS+ $1n沱(}O2>D{| ]O6}þ0OQ%:il -g.l&j+A_MOJQlgUH?[ D.Nsd97fݒ+=Co-(j{Zxxۃ?V{H'z,#Pad9 FTD Jeÿy3/,<:^!_pyA wϼ4>@r@ ȳHA:(n_KKR򗼔C<44tfhS,-|CAو}Q>ZdGnŧ *L^p!yTS7yPR@ L <G1ݡIi}x_>{g$WpQR 8$;{dhe9YOгp_/(ܰ ˧5Q?t]t`_k_)F2G4so} )~MXO< H!4Drl8(7BWOaD}*Я^ww'?XTKOmA͵:@s;jv<x[r |䃯ʿ{K_eX!;LΡ.$*:;:ߵYo*|0}cb]pXn kw_xۇ1:#H/]ctɴ&SO=; IrJқilLc-iڪTRÖقT%]>K1+o"lD|Q}0Qt t,SEcXv uqU,GAR,^>YZW>~㋣EfǦK><{%GQMձEj \b8@ [)Zޖv+ayPrn-iTi;h&͆)Gw͕r1n03}KrU[ XD`wy(]Ny,0G4wpGI-,L8aXm&#: J,DU#76&lQY+zٰr8ءr'6x:[u[,v=[IR'qoF)<[!<,H"ξ2B-!DeFώa|#ǁzf:n*y_Sյ؂_So3{H<ǽN-4(ϕ廀(m9)p ^dNSCಅbI/Rj䥽>Z}g:1tFc>2u| X]j:&Bzb (bic*=Օ2]%$S.',J&Hx* ޯ4i.R@ g6^($IE kCΎ5f !éT)|ޒӕ|$WWuk*8P,kka5j=>'XRd4Xז䋺l?~yTfWC=zݠ| KePA\t"H;{2 ppP_;O~(XýoPԓt>X%~v܋Gn\FM s~ƥpG>2g}@uqgM_-V(\F4 8wJSs|Я?A)O ϺRxG>,谏ObŜL%oѯi Y>O?#Za{Xe/zjP<2J?ˮ lOQ>.. u߾y=렔 Qn*=쳣w|(tw? $a;hHcs|߫N?[rg"((Ӟ{#v u7pC2|S.b.պX@"BqHW 8UgE*pDYI >g*F~^G~m λ?ɲI+P|7|g?T(Eqyk3~=0|↏=]Ζo}>^/Jf0܊c0:(0K}g;3(}"*0UWbPb_-NT]w0-~ڨ{k.`EATĽ'Q,i^CpX~̈́ψ%?L{ˮ AqJܿ wZIڠv6C]6TϞ+ ֨X:ޟyۿ Y5׾;ez6;LV,_͊"Iԇ̟ cQqų?C/*.Cood&gz=tڭɋFԩ2 .<6_ ڶ<Ͼ&WEcv15ʢ+jb8NæSow>l,GL_g|Nə.vGf'z+Cy@N鶡\} 86Y[5_ 1UO XL2<D5dNtMͳfbF9|ykgz{7٨_r]诀,8GuzS6k9:aC[Lu1'H7O7dhRȢCK"hmzZ >"7Rޞ񵡨nji&µ.q8?l < ,Y>%uHt]+jc«gO*c|1'|kK V;i`Cþ'Φ9pC(Z yr(>JC?rh߰+s윦H)y!1Ü.>sM-(Vy2O t9gwK>rCC(ED1lGE|ЧwvO#Y ktXl3sw%#Ss*}KA9ٻ&֏'v߆'OMۿ\Zo|`G#GgvDvG0/?w>'T|7ߐХ37ƣpY]F{%zAՀ\SAyr{ӏGm QS>Z˜t=?ko5}bc>(|+'lI+'"6=I}% *~(vT?־&WV r@c_Wy-}/zhh4o <1Bw}7~6*~'~zc ԧ//e ^O~^0vǰ-^'>pޓ _~: ~& dr7'nx44]3 OrxT>}sR<*. ?/!(3**~bF{pw~Cpo]:vχ_#Аz1~|}PI+Ï NSg~=DpCE7~ ^=(C Hĕ|EFTa7:}Pv`OWH08Fp>g˾u? A3΄N;~w~[ol[&NE b4jszX(9E_zz|3 $ νd_Lf˿[0#@Fwx8C6a}tqj܃xާM›X>dYui"[f% Z?{?ܱ& b$lEbO=Yd.[4lX!~W#0CB$љyb o_vjy>bc;> JTњla(4l7uۗ*r0oCcm34$Dha(#==i(˂RRZ({0."qsS͡| g9g3 M 5Q|E~3uӜ:/\H蔔tL^bp|N x}6ÝSUe#oʸ餁Spx_xm|`WIi+hjlszt;ݷ@>|̀SJ)]Nޢ Z*~{~w0( F.~!+u#2-Z&%OyJPtm(*:F>ΉJ`a-h_~tcW$OzSMm'Zaa?k[xU! Ua0;}(.@՟K//Ї>DI|=mceϻ F@LctPtr5_2;>CYDBWk/8F.\sU~Ii]wXk^h *p.eCQx4X:>WPZ}w L>`Wmtld*gև 䜬X@Ͻ/z0Wkmo)T M^+T8Jzͫ_kTCE.{bZxEE`;+1VTsa]t ]mƺHbٚח] v :TUa_- }Rm"7W[ğ/zɫ4 8ƗQCm"tѠ/q2 WΉqoM1(sԦYD1H[RTw B{Zc+\Wzk!ee Nh!uWs;׾g e[/zm$vl[hQJ'sg>cK #WYW]v q\kať|_Q[ E3xk߽mie}XFc3w9=Zs18 l>h1Ef;4HB 9Zێ|/N, t0|v~m1Soاg54z=\ntS9狾n:5Dq`305ӕwYՑh&s!`*(L l!׻Kgkv٬ԏ%[TK8߳ܰ!ּ5hmhrJ%G; GiFL샤YuEjMpUdnBmz^љX.I2OIQR2]>O   w ~]Dϫ`/fYcLY"29¾(Ro78 }-o}5hR:g<ubQK>N44>ɐuP!{8UNB/e>}hﮯ3_LaĀj~Sm" g=q(2l,BBeb5Tp|yO'?Izl 2d Qٷ+_b.}9ʭ(,8a ZQ!x1ꡐW^v%"ah钧\ Z:|֥?A>0'M|Ϝ<Y/bWe}ָ~WÒdLWp<z n*|Zbg 'PPQw3 + 9;94h'^w|^WFa+.M#[3^I_`A{OA"Fj1 L)j;(CUpP -чxoph˞}o쪗xUWcv._q9ʦcDE_ @|ԠTp*\V|&|q.A4C8\0MBp!Ohz({*[x+AzE>GQ97 31A&X `\|'~Ojhkp_};7}ol3-NB>`&m֧u.\jG -Iʯ>߄X-bDE^c 9[3;%|kBIpkI4؉"t ~L}ve'K!F^|U +|Z}+Uę)/OYv ړn-n6RO|rE#[1gM(hl1zf۰an,:}F6/⼌o,;T2%2p3VxYpv~> ) ,DiZ'_12+6`P-`z~ g9Xs,0'tK.i[4_˶2" ms?#߲/Nq=#|)܃⚱F)uPp/)sYZ;=\6Շ][M#5iy/N-NDP6>Hq;bX*_yQm=tw#ݞs;WEݕ bLIh}*&W:s U)y[H|SSp7} a**8:9PyV疊Z(Wz> mD3he!) #~+?{EC}!W_ACyPݝS- }_<71]xޓO(l{$Ax9y:#kYϸ *L;ƛεz_XyZ~|')];66d׬?<ؖN"-Ȅ @354B/x}>}-𽯠r.b "s|{,cdivdŌ4c Lp?䳷~vswPAJ/D?7%)QuX^+5yܓ]]9Ktc"'>uCAj+4^2|ɒb͆i!ْBaaW8cR.$_hw'[\DzN宊($ֆrt1Abz$(ϛCJ'5d9FHyDV.^XaDT#}^VfT󸓝Pkgux[4!Ygީ "3K G5W˨o 6W%mSirk\2u (WcpՆ&Wk/.^++i OyӿPQQzѦlzjWXm c9]x2{ܸLpeŚ`&q+dW0i$ (w-sxfJHT{\E6|l'?bG? cHoRwbB>_AeUrmxXR}3c QtgG@+'kF,p:ܦz-vA_{6%ZK, c=?Ap|*҆(]A4V@4^ 69eBozȉߩ'hN6N3%tA): 3* !%K | ERԳp,ޚ`"(-S?a9tvU͹d|!&GGG"=9KYsĶ !يD~#s#-N:Dg%_ w<]OM7|Cu* O, gΖQYd[@|"uk#=dpWv3c٘i͏FN?N-0tC PEtd7;l1'"pmx!? z3 o<1H ?(Yi>x }V,0jd~}az0ȠB_i8{ƥP&C=y0k dsįW,`"11u8} H;zmxN6]|5X׉'-W5筦 kMYYivVjhl} ;mW$G; Vis<\O"'=u]|qqW_)sW{=5H}^jfKPO-ٻM KRg nE_*4 @&8sگxpیM0HcB]vAu֜SNsmؒ{R#5Z 4Đ;=]ρC!*(__j/Xn6Qh'.羔(A<ٍQ(J-J?bnUr[ 7)tPMrDai/Z?cO|!@Ǜ6^P>cK^7Ge5|S}T&z9Qڨs;8X٢AIΞqT+qaZP0ZCdl |vfňuSX4K6IJ )[;K޶*ey^uG2_϶ sƟܝPS!|oM?qWNQs<-?^j*%/lҏbȆ5͂axjsy*~|ZO1yhfK΍̺  ~ɢ҆/0Mx>ݞ)Ұ]}S>miTq ;yC?==[-h&la).%nSwԎSg싉p4W6u\n>zcT[H"~w,%3gZN;Jp1(O=~L׉F:-A*-e\W]8d@DsFB1 atuIBzcT|.`S2gu{z˜>ުp7"#8`+`4p**sm&LeTyJ0s(ߥO'XUF,̗eLrxy:\=sP̅fbs)65|# $H^ .g= MݦX7kt$_%Z}S7~>ȣ €5\c5U"YjmF]by3^Y5ͳb_~b9Ug\˹JQ A1gQAQUWy9g{;~.7i3^;]p\͵)*(X$Y$DC/[7&u_ɭ%8:=pCJ՚4&fGx):D.q X^² j_[%sW˟V8naavYmhE&#d!?*ژTԐ>2lv aZ 4'uŏđ<.㟵:dsSc WF7ÿ%l߄y2ayc0@cZc XC jMR#˿)C(>N_LhA+` f[ZGn .ӷB&XO? nP; Щ8SAІp\tQw~)ԁB r.=>A@{`G  tV{*}/%܂t]_aXWnt ysCD+.}&\>(Y金|gYJx]ֳ#/mW۳(/t'>lzwhElxΠրh:dG\1 c:ƻS#ߙ[Gsl>wb͏#I,']Ad}v@)nhBe}0굠[>3,MMa^ZwTءr>4Fō<X[n4O?<0VBȚqFḺnBљCU1A/e)qsїNjyTX[?Sp&\ c _~H(8] m1F&vvMNۂ*ޞC_|(g.&Q#ٰ~OB;s9m^{emL;lF`P[ qٺHN5Ҹsf%R# >ZaBKMËY6R68qfo2!Dd^%_7 #ѭqH+-ːzXXh o06'.[oNO,yV0w! T?ҼG֦S_+̣\8Eϴ0e0Oha8>:Yyl?Ka#b_o$o9<_vWc]8>sgo:Eƾc!??С:0fl%姄ΰ2P=5,"*:n|.@ޏ4`j|e vv&\II.0X#D^ÓNx*5N-2Gz<JT v2ѹ'h3c>%0Uǜs۩1= Y,k?MN+z:.'(F4w@'ߒbY(iP-*jony"x˜']~oTU5gˇhmy_"=%D_'q _C q˳T)FWnP}u;󟃿:OûypY!ojۧeH>Xs- ?O61".pm+F ~^#2>yO>]m}{ O NO:1ք -'_ʵch C^`S'y _DA]Wq"}0+*sh"BO~nq}y_PΖudUӒɭqo9EF@<EME̡6eZ◬OlzP/[8|dbpf"1S͈╼FYզhYsf#[+BZɞ +bGLFэ- : :ZIs3ft#r8ܰuN)y+#Vا32_o,5מMn;\#d><[Mk qC|;=5uZzi T 6>Yx u=JrT*am}oi.~]fDXAg8ڄێ'+9 >Pq="#F#-iک%v-8jO/~nK _ qh#NRAALr3g6rv6 sO*&q3(K=Z%GNBDR8J<8nye8 BD[R+rnCt렴SVbAn,}kIw+F'~>Nr#IzTY3=%5iG5> 8|˛^Vj>.~Nu }+? ;BP[,>jvy"qݪl O O]Ov;luh u_v Q؀Oq"+%O9߳l5aoo7VÊ<|r-:3{w,t A٧.0<oyAͼfڧE'Jf޼Yφ>]_ x?q/ x8֎}ќqioC%akox/ocC3>Z;:OzEOB3]~آ[X}׵צ2.JMmQaYl}gZH)K`yo6<7DD|2ᕦMoN8D. Ϧ'H*gBm7M%3"~%owH>%>3 ͊E*NW=ӳ&.ms{xkopL>eܥt?|$iձ(eS7efQ['wR+f*}Q0$0NT$)+%z9_9Uu f0#TxӶ˾-sl|D/LRm'}Q2Pz9IP,)lfNSf龥S jh\=/V5!>-ɂAIQ[ vwrA-q|Ǿ@נe[c| `æXWuήaN qK;7C›e)J?@>~~ <|aPT䡯=| .9e_~Gм(1-h! gZ#Q2"8?᪮$THx?<"?L\tq }]Qu9Wi.՗˹K}?sG0>W`@|Zy0Bk_x\qe&D_~g!yA91_0f"|}/'<邭%_sQ_m %ۿzuBɟ&m߫_;*j+Z쪗O,R=G ={eτP??Ͽ>5a6y+ņ[pό]z'~&՗FF|}Ck|wՠ$}Lc:P/7> :loP@Ε#+U7peWfW]~H)p)%X XE/y%'~t&Kz"ώ$=ʓ(kdZceǜIQ-Lp*F̕OycɈUS0}?l>WmMg F( L4< 3xHA/i+ö !2{_Ӛ*,ABF[^2}wdZX?hwҤs-Yk%SRsQwz{(ʥ7c{sT6 Z]ι!,#T0v-5ES](oflKSN8b@z[65;$vmB6>EW'F̦%O)1'ҾP|Vc o4%?#I7BN+6:7}Zb ľ:GKѳґ+%B7܌ԜR$P8oPN,n}hl9zLgPf5$ѹWZ?+K>‡Da=G~M&.E;5x|tdtP_r5Y:cXÚ. f4Ջ I -BP UUDMIy9| _!:l7I܄&-0Q/!(1ˣ:>̞83c5@ce>컹buK3 Vۄ ITcX9$F \ԛM}r3Y8bŖ`ɇϗl%K_s ^<ɶi+3S/]c*U|г*YGCa?~Sۧ); V' ,E +9)Zi XAQ:b&dlؠ5yDޘ.&3kif;_.*M cv ~_Jdse!gτ'dTnMitz䁙Nju7[m[o~?> cI~1ӌ.B"$f(g/uO;1r1,o9oW4 ls,17|Ld`lL=Y IvGnSӒѱe?Szɻ _deuEpƷܛZ2Y6,2LB3H>JB6cMTU,PĞF!OI.6Wm XZ_F-'Lz.c38E'ٙ?_Pܸ[rTGWuK[kCYp7C"爝4 T"OQwS)|yzS~V hVr<2>\pҊp =j9bJ#ݩLq453EN{tæ>.oVS~:Zgrа4+qc}W@|az38j<|c9-j/Di|W_.ϟ)Jy>rS't;{LVY! /zJ}q)(%>?T]}E띂CaRkоWa^{IV7W"d܉J4tƇDu:寇ߑp<ڗY)2=RqO+s. <&7ԓR /=9"r vlsKNd1kw;cdj_2(oAzI^Ԍ[ 놷ϫ"c;W«՛5ˎd =,QW[0u^/`qզfTC{_(w%LU ,;ѯn4#gS# 'MVd9eXLc+Q5`)~[=!Q3=ΧBrf:"t_W <(^v+/l!uilڮTXjxⅴ)?4U4哢BGRw68}ͺ犟\u`3M JRi|dIG\UY Aћ?[#i4b'[Hٱ,T ?q>gg,OXMO-,C?#,[EJI~I/q,d1ѯR0(\?㒉@4}P*/yF&g7> 8CG%bIr-T&L?΂~|p ͇$,cق!b= :pmIH}9$@Ή4q!v(aYdȰ\G/ǹ 3۰#Ҿ +n<o4q6N;e%zze~]2ه'Zz1()lw*x̴oi0W)>J1[uP'M>uko'0xy~IM{sr֩|BMs3A2i"sN_|uT6>iDCp;ae_gp˿\߅kxA1Wd_t 1Ki?i>5ӮVK 얄G}6nQe['43,gF. &>KŧcϾ07eX"~z@Kby}KHq`ކ+Y ݜmWlAZ2߰u^"zVk8"-~޻;o_R8i!5 w3959DdMXP~`>3t-xh:YG,ɢ~M.j-6J8X i7+rt>:ه tQ )Զb鲍M{Im"{4Xވ>#&7xfu͖3wi'صBL ؽ41^o->VkSVP9IQwot̸O|n Ybn'R̈́MWzOq-~6_3jzHE>\ Ir Zit}҈iZU֤O|EdG= hs~LU`>! p(G v 5r2ːAKygl"P@Q|;YkD^sI_ŸчلAwrzgL>fP?'o=M0|qęvtQ^c}7h1^fdeFo QkS>q97|}"ZM#M2i- e )65T%?dF='Qq>5}Ů%YۈodyncvOG>Pn<_-ua7o/'a >upࢬ ~7Zk1]ߺ84}G9-߉4͏_/dRNXZ 9@ ;[sF]QM=_\s rCsı}VH e[>éUH+%$żmoDhkc|! Od'0ZTIۇPg"E}o,mt&YD)s'59sO+=BͰ{2[ZiՇ?.03CMPZ$!g_h>+ue&_/ e! <#k/Iץ9ȾoɗuzV>#2V`3֙!q(Ǣ |׵7y8X괻Xlxs^YO]VJg`pu_\BA;g @#E=c8-crzoW4b_UN! P͕Glm>GHO^S7c4qm5@mxF՗[CL룩(qbc-z,NF*##!+/pظRhMOĮ՗n9$dѩXK5$|XTq&HtjviMGRwez& eё-sF%g*R39Mb G⁉8$'>ͳC_uZG%+ .W*PBuJ@m14hpI?K`Nl,5zB7RAc+OP>kkİR(@`qVQ1oZy|`֯~Pc_j]o)6r&Nwgkd˄Mֶ:Qm85+Q_1\G>+ _5/ I;q1=;Gqk3܂%"7;z[|"'wr'1*$:6laۯhs9[-ZxgG6R_Jpb}[HRByCӥ+Rhʸ!+]zlpxS)2[.bI (䱐Y GuL3x",G`G|XLز0-)zRҮGyGQ~"kj'S^,l[,&K}oW!DĀ<+`z-4Ye`r z"(:fIpEpȑ=(^,Ӕ  [Y6",rCPAp6.HVT [Øct2Vfi^`Ow" GCd7sbaɾ9r뺬nE}gLj0 }]Ъ+[Cآ2D&M|d~eP|c`k;;)0ٱ_<.Ro 'ѭŨɄ18¾)W%;V#d*ΜfmJmv%|r}36l!z[-e꾷|>VȪWDɂxo.hf0u=@7`窒 "4dQutEU"g?їJYi;.p("|Mp'N2MA1z; [i#孫D;Gµ_ L*\_SɂKjhҖA`{R߮g>c+ab15$5T&=4botJ~Rڡ=<7&a(eK:X6zGCBAfjzb0jE]^{^7Pe)aڋ2UބHg;1u㠵pΣ% rh?I瓛Y^2/Ǘtb]m7 z~-z>{{|'S mhw x~aZ*r5Zr2>iâsXxuRɢO,|q ssHJǚW {Hj\y_+zJ>ȿc|p K嫼2M.n'R_S􌬕[HhpQC/v(g6WCRkݮ5>uߌ |fZpv'e/)EZ Ajg0 Yf ௶yӐ_$-, 5;P Ò,-HAw!+tma~-0S(Ns=O (QԓMn# pJ;+)2^%`K9up8Am`HʦV<9%Lq>Ee 1yK>?(Ev7]0pD1wZ缏tJ3!\컯huZEUGoK+˔)"(^õm8\1cb**3ݶ1j" Mƶ+ 5§|lSf^sEm-#6tٍrmD%_/3-lݚW\Ȫ#=lzE-&R慲>!DRkF >ލOC!튔 *CӪ/A؞\=Ϧ;j E+~d^lލ7ye[OAKBEk͆d4V}s;'GxcTu7n53@jXN>Y@6GS:1?UG8s *.mbj}т4 &%'S><A1\N.3d zެI8BC~c09r ܷd).Dw঴tvJ=}Cߊ/=?]A!09|v9E0nsv//06Z(QҜrp?1BX|4H-vC 0㕾w|R 3;?lH"Rn;t-F)6'd [.%J(o6:ƙ OطaY^UXq>"{lϻh\TO%[v%lrnϼnn|$GZ甝1=<~R9Ͼ|e(黳 ?ӳRIY9DS!*<%܎O:3!\%1we9h:>耀DqTBgY;XMb=-%|4mW)G\ ch{qi{teezk3. +Qcó\.(3Z' ˟#Kkt#XG9^_E7M[K7*Ht7iOTׂ(fj4)CF1سo&Z֚q1W˶ŭԄ6_ԵBӨ ҭ|9nnllQy۲2X:8e{$|V8>/ؒ*sY^r2ymQ%\) [|]:0;̇EI>ZZoBg]Q&*/cwIO}hC&<]=K98؈ْۥ jHyEљ=0?% |p&m4;4-(qo ġ]N"DӜŠd-pD-rJ=X^Jpwlyl{S6w}$ER#M/xBh YXj ~/"2p?=xZ?x32,`ank+k&j ᛳ/7貔;3Zg+k9w'p4##}~J5{0=[r9g,|iI>儾 rB̭Oņ -1 q'rP9)e>2]N͏H)㑎!cϦl+8~D4|qqvD^ΩVnht|IT9d-]b&%+}׿g^JosҖ5+*RNY%^FwzUFv*(/ʒ#KaRiI8dt8I^]St5hzu B('vΩ<,E$s\&ZfV)j]YɞhUqp}5wriB_Q`C-rɀKlg<Ӯ;UY!_z\e2~c@js8bq6P*#o C.2E\zǚ<)I,c0u ,$"Y|ƜTyMj>VGBBd.]1_kݏ6)ȩbcӱK7w"dRd?kKYy2O˿8;os$>sI=Qhi{C 6UJ9%jJxR)E<|mSYK T&^,2(F'4oKuʭc\"YCwW^QW5~dJ]Yީ:j0r⻆I Ռ d͚~j+Η@UK\H}Z liˏ?"ϳUr|>E]i΋z0)k@%(^mb{zL۲EDͼZ}kOYs"w_/3{,DGwU+옱`K"R|cX Oev'\`Kex%,մbB7ٮ"&1 3Y(U.Y^Y-`9ZHI|wG]ذqG}Mgud|Ob꼭/O0y Ϲ3g|DaSۮH8B Uevb{&Tla7#f}ߞoGFU:o-r5li!k_|zqb>*Na9Iy (ܵo浓yз2ر׸f5Q쫕X!j5 7b'YY+9zs .\7pz#\V+f$ 심GQPcNqTwW0a/z-PvD̋ ۪R) ]I[YW=&5 mĠdTn q)HFrEv3Nn7zSK$6,is9mj++#Tdž3L\i- RM΢Zd$b5@z`z]oIv9|p H> 76|,n%2Z .Mg|9^/9 #!7akN %@Ӊ/5վ74K-Z\W$X>jIw2c\k,yvALڍD,y~kkQH E-=k6{)Я]2*Qq]7דmvgIt2wUlP/Zl7l&8%ZQCqLbбd;'|VhaՍM9o֝0͉oD u2}Y^ٳæfF/kc+3\]ņѢݳDѧ)B6fEFn"%/Z,bI<`ˍ {Ǿּ0mXդ7x1^ײfp>z-}s)KfquύY@.pֹ)c>|tl:Ǵ`=REsmI9t4a˿nǾc#|#4Fs]j"E$=~-%#EUʸ=@%շUpr#LW ۨ9ޠ6-0X) N.E.B8Qi$R}sȬ3VB|ƍ*}tFlK_h\,~}U^\#Ru\ի ܪ4JM\Y| j}QI0) S4e<0-+f;_Kf^? n <,[S^DH/-|sb2~qًJ~kY+C#I5iVa/`;5 6G[+Y[*5&Gpj 3/ktCA(UiKc_An"I䓋e}/Vu|^YCR73(.V6^t zwk9ye3'k\*حZAӏYϏ/{Z*bؖ1y|da r)Nykްq'8 Uf3}5}O֡9ME yGw, Yx-DP&wgOaC'&(/lf@|a  V|e>l8,iD?~=N足)j¥w{\)jxVIv0b'zvB-<[3EJ41GY:3~IRZH>9ml?.ao ;G3*tUS7KcZNC&7_w!ŽZEz#W|n}:w'fX~\{H'L\dR7q9q[4%~&43ٿ:S׼?y&eڙ!fo9w"DY4 ;tBraF,Wp4o|7ms go=VwNxSҗWuDePJ *؜+'C-[X諰'x:(0)[Cƾhbn8oa4Y1p<^,`@o-'twRiE3wC5L6>aInKe>DK }y5/pzl:a8o.9u f >Wib~˳g^?z¥z'h_.4 ""73\6WO)K˒ZdLnMz7:Bc1ۯ'O:]g[Gϖmnc?31#cKKAn vD<%J*JikCde"e}t9Ep!rE{퍼h^OG|"lY*m,<.+SR7 7b;a#>6>dG=ਸ-_dӑhov]Fu\pLg-cj޶ oU0z5S\2/\r-,%PKNlEFdLnc*; 1幖ˢYҥBjRQ{&?6+<^W[֖<ÿ~n~l˲!.ݕvFf; %Z WOmdbƏi WS'DGQ]pt."#Nz=-Y; ɣdez K vUkvx#{^1y9?F .C#jp&} hټ]ٖ*p\zm[veJW VU7j69 #Iab+pYL=Q4/?4'+*kC(nM؆,ʲA>M&W(ZWןo4OFy`@¹AQkX*Pw8l6_j\?⤲Z|[Ioڣq`6h 6F2]R{.}W xPZH358}r{^ ]ş(9Zi&SeN 3*wqiok?!pP8ԼXxе/,c-ǵC iɧ$X #rTϬZAB]|)A/tb3gf;==ޕ\]>u0Nrn<^ScruѶR{oS)RgÏsye? Ȩ%,cl?|Ǖ+L?Џ$4Qe?ȫTo<ф,rvwD#е9”ϮWaQmXRՎ-힖z3;Ր B[آka%u]| Kk1s>zh&n/Q%Wԟ"}:;,J#َI6Q>Â)اU5'QWߌb݆,#M c|-豞qK; }㪜+X쏐h:rR ?B'NB%e :'S9Z0-W,#RArTʴBX8菞O }V8bɈ *)ҩQ`UW‡;v^{(TQæEM#і TS~vfՠ^43TQCx"dBt<6Y[LJ=Գ&O->⣑}6Š,agp b8o''7+I#}*r=M g݌rMwZ|^r>ZQ>9(&@ES^آ;F٬ ZB` t+gͬa(W-Ѽ 8o E-S 8,R0UF@-s  2zy85X(cI|{, lfV~ i ~m:]*v`7r4M Kу5Y7./ hC|>t<_Z7#qqi:)H)>Bj%ad->F9/}c܄- ŠB'[3I !ZxC$> pQS'^قDʂemmek b.!F\Z_ ;_ǖ.{/㊭+}"B> 9-_SþVM]O$6 WpV\?/ķwxwYL"U% .3(vǬcیAOU@%1fD' q8](u\⣲|ץǵoAW-n&d9٬rˍgjtV53W"kGt1j.A.vyVLZGxHk\ ]6<{\o٧e7Ѕ<0-势7⣖; O@!8a)d8=\sn. ܘ:gO9dN/)-ۛǑv?!$)s5cc' ~"|#O9_!uwy0CloZNXVk(73 0VLϻ)m/A.>3~~Pk!q*$"[ v ޛA@=tomkC2?֟GJizkeY{c3PHO\(jwjO4iW,,倏ᄍS'*_OgZߖQg-'J}w1W<@?ɟѠ s(O/j(mV^ͱm):KȪ(#wJXڊ3oineϐ50ս@ނ 1Xaxh99V/$/& =FUik0Swٗrz Z^6嘈9ZOj +*o{? N*(a]EQLы׌R!_G.ʠH5Z˦ϸmg࠺Zw.P\>N3mHGv %i<~M-b(ml˧7}#tI_@fcԗo3>:c4NuGډ1*QƉDUɣeTW|">[ Y_Yj3a˽ Y)l1܉E`2QHf8mk.cxH4/QQ ᄕ,Z8܀ż ҃O 쟠OeWS ftU|Mv|ߑo Ȣ9.'9ߣqG*sOgeJ3C_5g\qgsޯE!Nd3}CG i遜'2dO[qB73O 0I(v{~ÖG HI45߂e7xUP7. =i3[Нv3B~0іfjE|SGi(Z'q$|*~2>J^ְ~:1,Nqs:Y4rR}޸ ӂ8%\NN׳+'>N0WUG8*X]P_þ1nuOjy+SW}sܘz['MU⻧ec\}m)F@[/q=XⲅFz4.Ǧ^-)MAKf9)`Zg3V$Nd2F&jp`Ofbni w-\ulrtΓ2K6EDnP 9S$|ӮڜÙrZ| F m]Q[nN|07'^"CQ9s7SuĀj?'i}g9Sqs:!KˍDis'-w5|ϴ%j.φ'qj<>`A"l16d9"}0L7dɻ\m'sq3AgDφEO-G)[} FvǴMbyNhY LRK<:fQob5ʼAK6|1D14Ό-ZvεlKZ&3w9boMthiH:Z3f{3͋,ۮDm~&-b*ц<R8SG{ ly$0uS6lAV?f|@gO1\ũ z"fߦôY t`0 ~-8`لB|EEO4 Q+aculمIYN ƞbL"cx.\aUWzW]^>TKMy,-wXrΕ&/fLKi&ͤKDփݷ)Q92ž|1s)Gk8/ G OXh>ee3C^o_SEUIzeO\ JHje+)b9_>QViAYH4Ώ-]M62@ }'g >}ף_|eVJeq ИmQ_1əo#׶9_ZA++v,QV?E))V[NUj1X# 7_zjТ^`;^CSKsm2Wy_dž2*0[<ͧW^*"_!O=_[,2% \gh}5b&RC/'~m F Y%GOӔK5kKlAm/ȓڨ (qfMΊGc"#sI9^ڜ'9džf_A[,:ؘXmx؞|9relP83v&fL-drQ5bClG+M{qN[-PrG`X ?N#[|<05V7;q#Wg /7I$vx ,}k>_|C!qTh@<=\C\jķX[ruy4,,Z9PEgJ7dß,r,L|쎰Oմ>4櫒$UgG\# ~&V8RxKǯR̒Bnr.X`;lN/KzO\L [R@;z RW)δdIڟ Ӯ/X%هGI.7OU1s!x{] YÅ֜ޤd濦]-xJ]$PDz7tܴ<%)}jMu93q̵Nμ`*j]v=F@!|~}\R۾ 6Τ4\$kOgc70ِG b,&]ZVR@0x> GC.u#-Gwy|ݔL1ZXWPg5z+ۭ zQ(JZ?}4J|~;^tt1ux{So|OF>"XwibaPB g;]ȅZA ;aqj%.Ab =VJŗ|=Q>!rO13zkYTw #~2ZΗ12`ǺDߒ'i&Go[O%ݦ8K $=^<͝':>`yQ|l*\Abւ!">7b@j_lRלAtRg<]M̱O2lbk>o[pЇYp-սE9@ YV ~:i ~v/p-[;WtbS&=ϲd>[AW_;fF}l^*j^(S޲,JYͫ'-P g7)QU9_op1T7Ti'boΎxR-%YiX dPd*Gu(J >|0۶M%crؒ>,rPԯyۍc)W|I~(Ϗ5?ӜԕկQ壗PK)Er Q+1V\oLWԘ3g;ٲh? p p uɨ/\A=(:骜3y+e~;5|}*-C6 9}iS͵U[(c?\D|'jjL<)5S@b>2NٟN_ 3 uiq׮gTr4M<Fz:fi57« PoEh4Ӽ]5+md6m֫Hb, sM#X9͏WAN=$_Eh}OJ?95M$giS?^՛8g @-o|StL:TY,aG=?ա:mbz2$$ddR>'s[T&-i)X&b W%=e ;{oPԦ1SαD`Hj\B,FYqeqRku:_g [(πF)Z'7"&[վjG1k,d8Gj ݏd9K7=O8I Fּ̛[`uFv+Vdrf_cʼAK\FFTPLbܟJ !l2c_&! d_t jdz)}Nɤ+ߞUvYƎ E'=5; U,޴=M*2|"8]j:12|'p>s*xFGamX}ъ-QJjGrlIa_~i13qʳZP[|v8g J ZKTZLW fH`h=`#‘fD`a'ӞeBt ;NB BUtǴ% K 2/\YoPNėPJ86z5:X&(L}Ϲ +mi_NkL>[,bR6@$:v<ե5f\Evlq|fSk|-|LS=4/`7}oIplٯ(y7ev.O%9;Yh m'|C쫳 r>Cb:a:eF|u;! TR{QğSکkT 48J.H79_|zTVxCIK^'4j_DlWVX%g ݈\S9ee9x/nX]:fCM}U-%y`h2șD+#wXeUx b@NX/;Miǖ/`1kפV[%eWLyr|s\s3 ј1iBDϊ5>BOlq3Ϳ[>2wMrDzB ٰT GMPhpbI)3O\GY>[yxO8z\ mWk?m64{GF5>/ptbqޥņŲy~45TY~RY=X.E2p`a b0@PnMs%Yh9c,/x}/YMӌug'!;4;(w+jR\z} UˎƆIOtlsĸkuy t# 'F5dp u|b3^'=U7Afk^涎9[˗Øg5 * [Rꁠ`tjf}ګklg~z(r&a)*\'.x5)%p*fXeϳWsutTU wT,8KFVu8LX|Z?H>ct2nR>|P_;ߺ3awZ)LQ(}71-4 v4%q99q3ix՟J]þغc6\U]/%iARv'WԱLYTXjyb"Ϡ&8>AbFÌcQy[ [)#<8-衊݌#-kȕ*QHKԩQvVXdxkw䨏ιZC9J5'v~Ñp! 5ð6Gl, *@f 8 E)3Lp$1@<!KK aO$rb' UHmϋ%כڲb}G0ovgS!l'^tm"1`$Bq?lA|a~35 sxn'=%w,I?U⤅KX*#bsz@nw2'n8DiB[=u5GvHNֻIx+idгSgw>?cWs~Z ^GWN)!j_U]})itDo}yZ _-dɻO,6y;7my>Ju *Kkb(`R 5YLv2wUFqT<^`џxfG)msMCs=#xԡ9YF#-d&]2^LZQKbd=Ҽhg]t1G KJ 'S_]]pFEI%(ĶfÏzV,¾FiD{t3o{?5G^0;M|I^Yp ib$=G%j52]XP\΢veK|]5 *stn(!UAw,- y\ !ͫ+w)HIMEJ ϙ8SoƉǍ-ZX1)ּ/MƷ)VjQ[D 97sEx]S] X3ge D'!X|-{}%S˓Ūv"\w$$7wwϓ^cqeKhbάgÖ*:ޏ뻌fT]:ܲ2UH1>CjިfsQe0 'F,WleݎlS@qj{U钏S<96×Dus਷,JORp'DOjDߋt* #2դ:9w[ƵZ=76%@_+xVU=hh\6¢ػ1coȜ©12dO;"%M\k󜫫IlLZ 4Al$-cinL6|z9QT*|6lO.ޏ䗹KnL=)@ƢSF70Hn=EZ*j'te 1}u[5>ڌOټ)exveA(Z[IT/AITuu矵Wp2>[`.d39-~-h})~k|H-ѷT'otE:iY9v\!lŖn˙sslaa9׳8VVCנ_R]mcY^,mW|Ħ~%[d_slY6H!.d:cS=^_WLdzYy&Y#KiTYBR|_քSŗ/qt#+U؏V,J8M|e_tS5c] Ztx0 Ey[ƨQ`[%Z8wJ&>\XpRQU+ſ#=8GF'L_o,ܗ4]RWba;#S=ꅞz$v1usrYhhPojNHy&2{ݖ }]zļ`G.Ɏl GD̪ޙ5[۷lcl[w H<"Uy% K$zlե<eT=*h~zv$v}xIv2j4y66kG&v  Ղ+V?`tg+7Q5#8:=곒um'IW9Mb*JIW=QHM(+æa>uo`4E%;@eAr3ҙ 3ulc" VI7w8{iJ,SeUn]#,;%K I},woS'.ӗlAuv EɼU<.+$ gCF=a"U$l'wpˇu%#|fi:./վɎ88S7 j NA|;kg4:>]Y5?`8\Jo A8h$Ht? 2O꾬(4"TM|/ d@\~s%.vy<}c`Z^(K*^@m:^]H/{:x^Tcu-S|nr}D[i|;bϓ$quH(BSuu'8M>(g[nڻ2!TMి},"03NK=Ra83JwBk[>ƹzD z@NVFwUhF6s"1;h g6]uכn[>r>rs<*&Ѕ;,)kϱ|rzF #=ǡxA%gDn!X_@#rG<ͺ(I޹s7-qJN@ˍH7nv#Q@sK rBDٵқۣU#ɤj_l{=QSO{K~bΗśɄ3?V,x>.Ngl ;U+r(߮|D }? үD^,NOZз,tB!3"3?eZıf#[3o-Lb/V-'Vrȿ $xg(&- vMwz,SGf]~GdC*(a Ȋ6r9BHMrEQ_?)J+G~%:R._'?0Uaimfn@28.[i~!0[Ts<;~4~!yI `J?MEFǝ>I7Av2'r o҈#yRÈ yvgN{B46%;D!Mrw^pT&2֒J50ɐAfX36)2yYH= pDVxvbt5co.qsBoN0*ӆ G9XWAu8T =ϘG hu4H8+SV5}DŽ*SNU^&)e/fpk xt;N"Uye>NWGӳD3 7wGUqPGH0XWi*5DIa}'(%ԃ{<(ȘƢ먎t[C eV| DTw׫hExSmԩ& /3UJ4lqhTH!P7!/N#wR5ڐ$QHkwѱ]uEzIVt0 vh]dM)+OXAvKy81aB #\#Iƌ/7|߁̋Ĺ~{/0?L3m{%GrtFmfȖiOڼkryxTeCO:m'I|l=b2N)"_3:{}p߷%:36ުsܽz ΔCj8^-مKoKգ@}#1V9WY&ʟ4zA~ʿtk'>k)kӛ#Ⱦ#r-&Qb_23"Kޑq['f]|aߚcś'7SbD#ʋ|/i@Hv-^{r~ym ve&ֶ~Hn_i#(bAG3hs.W\-F*,SgW>Hg\c^%z8]u;ԫr˥,Ǿ=^DO ?+d恆 ^SsR-Ӗ=r{8\>n+˫0";ӟлKfFaܕ'KkB?v*6UN@7QgHb . oC\OJYѢWT@5 WN\3COc.?`kE!~.zxAɌtvO`A>& C h3r[V]!<qMF3(P48?S"(9.m Q-E Zz1[PO͌sR'%eY =1ps8Q|f; |2)f~RHnQ$3{ #N5ϐ'D2N6z}sk,azAq"|Ai }r~Y y#jq9bIEֈS.p/gNЇ+>>#!3cX ;~ӽ7{iv !!F)ɥ>XG,I_w%)T髧1 bx,T)&kHA8~j<@XdMBXfU8][Y8n66Tlڗ#jBV)B1}8ig9p}fMxھ)Crs{9F{<9i;]uq9xә 3y4q鬋#چ "R.RۆxVq }M Ѽ#/sHu9 sgr/,I703$Y`Gm{h[}ryzgIy vT=]9Xrs[]zz"47Zvmoyφ%kIp7Oe3CLm'!I%HЧ5ljÉ1ju08瀶B=.Yr QюU:%Mf7p#ˣ=)s1c)O(S̑sEiR]UEm1c1[ ",DܱUw/ជn̚u%`z& p6xL{.2T2S>]yd\{P[lHY Sm),zHGNMaʁ1'GJ*8YMq5%RRmYdw6ӳCGCG1 dj5J} 8,; e3?s3$ `)џ{hGti߉ phb>z 2@h۳KzxF{T^47r!bNǻ.؁>_)l;wEvnƉiW.8|WXeo#DhmV 9:n%;T֌IvN=éc{ 8Jw=BWɛ6@hr'5+vm -5l7r>%p-;=iOCTs54rsnR4BNq%f]7qT^p˝ 4NSP<Gn8 ~}T 99Xd{|(I~AJy`i[ 1VŠ^gg|P2#"nͰ5RrP2= +Nѝ_/&W?`N$ev oX`\wG| 4'Sw2\+/gEeOfzY5wb6drV9sD]^ٴW9sl/NէQXh8P?s >>cjbթmfGwn|#4%޵&n?k&[ڂO Kl6{qCdش('K:LtPt# kCzQA羠 =6?Ts{;s :~xX;:5w8 qw\lΘCbćc61o;9TrU#HylG oy~::pqց^kqԷ@G {?ڈ$EOyܤJC&WE|Q< ^;Fj; 9 S_[|n<wƏiLW!&ړTA|=¦ du-y+>`K3bbR ^<6O&7TLuQhr |9_dy ;X-\ *{=au\ʃ>^D@DdNX"FE6*؜M)ڸ jG8@FA WbCYCz|KυSì9k~@Kgu cFyt?X_ Mu#糶y^Nq1:9R~; әDxx.kr+Z [-򩄿lK@?~c 7 uQk[ݩv֞ KN(J8\^N5@zOf({~/=•Hw|)OڶK19)Y 9,"҄.Ζ ZDZ?-gT Mg3RV]b[C&&~ܓg)nSC>]lBU;N[RJ兠_LU\6?pJ<)Ux0]>MQ*)ʟ",r`k=ZsќSpY=[ݠX@fAȰ*WTnz:k}'F4g`TU>GܹӧT.IeB9 a:#yvz\ùnz]8vox29$E m;6PIx9'w=`Q,` c>r ʸnio}lc_ڝ1?4",u99,ȑ:Rj2%C`Nmr: 9)Psw-5aG\փб]f^ NF{&ȅ<{1aXz[[$?@yXHr86#K8^# |94=a"`FfUfD,?w9_[ ^ƷJ*b?.B l96ePES ٠싃rR-pb_y#ع0T#\oU jW1Oo9. {! xCc03B:,Bt}4N5tz]g;z/ ,JQ7X[d'P)ELMCCrmxXN%K<42 }Ҧ9e۔/{{/"nyB~qK/ڻ crt>p}\1xrMx9'~.ź[nN,ɡN mxXwv輍׻Yh̋`Ej:SuQM\yn,;!Ugxך%4^/~=okgWsiAGqBzܡEf>`_`9Ngh&w /?̉ yZԼ_\ˢ6^os?.CY0~/)y:ґo}F@>P?*~~F9uP9zyIW$TCӷbTJ4;5Nعf~?]܌wDC:ٜe;Fr}aJ+|A+tVT<Q)qC# ne>W%_I =Rk'Ovt*G;"0 yd7z}ju:ىf<:N/:~#۝H7i@_m;pV%ʴʉGuƟ}#<ڠ~,*b2㮹v{9'txצeWQ% ࣟ%T8hq}s9G~ǪCωԪ; aӔ_ưUۧz2 !jcuQCJ|GTLQ7J5!W5kؐOm VӀu8H.Fv6'ս+ i70wޗxG|Tl3}*#QN[7U;V7;l_CUL0y]x!Ǖe^~7&ϙ<&Le2`צvTr2 4r\"|^ZcQ(rI=?;=j*T%v:c?B#~sR;Gwv2yTΔSI;Uu*q\y wX°kl#ݸj<]ܳ|U76]7I@{,/O}ח/ʨU7 tZ%dרDۯ R0)1Ow;#r^dgy׺|zD[#_w`|` `)~̣߿#&v=e#Cn\ˢm#89ț 4q).}{9J;jyWUoXĵ@#^|BκEz7U+1?Sӧs9ׁm LŇk< ;doWmϿpq@s,ȩ9&"u/t԰>5BOs|$C+.z:m❟w1iNH n-b9|w32%"γ\ȮGa C ?xG,ǩLUQiFN}҉J9ȱ«,M#5 www>m_Ed~U#SگW(ϯP*ξn Ԗ덑O`7ٟrxu`Z..N'FP8꙳vj|Hl7{Z,UTUf".r{i;\{]-"?S(]㤹f;=$r [g鵠-}pBFؽ,qH } Y-NxSe *]CH2$w "<,hO3kr1OlЙv|=)'^w'?\]8|aw؛Xx0 v/\0]\u~r;(/?y^ c8&m>b};->Ǣ'i,$m=tҖw vW*絳??/n8`=8%C5m-ɝkᜱ|ܬmO+~W@<~:~C[Jx_t2A(`GxRYNZ@?1_f*VG>ՊH>Yx?LvXzXܣ?*Nӏl4[N}]$rLf‘f:6~r03O?3,jđ wg$ڏ w:mԧN(Fy0#&J=yz/c@Cx kGX s<%j2\6炂 TPI!I*&MQ~i+q= &506*ե,5[6=;.=N_56u"eی俁y1W}[QZi}"rro߼"h,n<6!4&Ϧ#aZ }x&6跫.~@!MGrkCW=/DشGUazOk!F~y0AZB[dMw,v(9EK#6]bIT2l/n4yQ0Eܦ@agMD]#sa 'W)[5ETT("'??7qOS@?Um(قe;"4qP _~)|</zڲb$/adgFgs{;r%]AbDZ!42$~sn&krf/;|G.Dyɶ?/9uC;ݳv5JEmͶ{@yyVkIs52hE?0?[c{-D`#M{SC7z^=X闠B|Sn9c9gSNS>Qcw~!m]BNdʤ!Cr>>[ݕ)8Exs0\@)\wsjn;IО+?w"r `$Y+a[8/ؽHkA*Q2ι\4Bu~-eukQZipNdlaVb Et>kβӀeٮ+DV/&:39竧])&8R+HFSmo:j{HqH#93W%E=aDFRgy$Zi4JєGQ,"ZV!8Mf[0 ;$XxC4|`\3bl>,0p3E_sG䦾!a:1m5ZOeGxF ]l-n*??˟}'n/a%UP|~쑳\#w_nF[Wo;Dʣf[kᱫR)O6+6V{BNN[}驷Y;zsᵼ "p7زx^0_lW~~G[>| 2b`b̂OY͍l\TF/I~]UӅyF<P_뾨]o%lN;a˵Dv*BƂPivriLl"9x%ό[R ⦰?n1Wh'4Q^')C:;v%grJmrZDk7zG_?5ϣ#yFojKOģݏ7pNsN[BTRWp X1't$kYwQhɱu0Hԕ#HhϬd ,r~#1qÃv99oF`8c&j]g򞯳J \$ꪜ|s`GB@d/_{xj&(E`e 2ⰼM|Uߟ6.7bԡ ֩4M 8I߾7P]hMwO0%:`̱o#S-4nzPD.l w;@Q_8rUzwm2 `jhCnSQ3u<`A 3ϫ!@EǮy܉IPy~Pnɮ-mkESxq:=& !Y]OO-nk$F:<*ȡT{[_4մuZzNw53㯼^}_WGycz+4d*#_zc1˕T r"i#NA;` !ӂGԔ~2t]9 jk2P|sk[!SzAnS{} {ceƸ@G3)vr^Nv[;܁Ya!\bJf?My[#> NE|ELh5#XOlĕC0[CZ.0t|iDWd*բ uv )v;6˭h|4ҫ̿w{Rɋn 'kanhE^ţh+\qΈ(rCS~*Yb"eIBC)uo#ȉݕ+t2?驠mC4t?,9N:!Ni>p<<6\IxBHr*m1jG!)nmH^,ו,x]s=^='g=DʙRF"KnG"ozA&Z֓)i:ؙ|j6vȇ$GV"#o`4N[DC+T'_evYaMZsKNzd Vx˵3W*.ߔe#Z*"m0@OIˌӜ~ӂ98SK9k\rLLȣno'Z4Ban'gG^]OZf9+B7]R0K"yIsm&DnFѿ "h5ϭ3cw-rtjs'>ZR "!GD13"2Ǿ#kG+$WDū=Y]|$=y=l~"\wqR#g^rS#"ɦy;s;0RV$_7[uGHy$ozޢ-;&LH Y,;̀1I/Xl/>﫛c?"4G_\`'vlY/(7ilV?3茔CuO`w۵<4 &HX=oL _(A]LoDƛX3St 7/|siv%_rVY=8 Hjjf_tS s־ӇfbNs/P~ÆߤV#NBZ}ndͰd=?;x)`<ͣ 6D 9940Z4cإvNφNmhgzh-{LDP;!aw^ RwTCiFOAx>9^";ߚ?1+=?|^Cs ! H_3UzsfUN]3)򅃼{rG8nIT- 4LO~<; | IwX/`qV-q0.+QF*#T\/bb?{~ߨѓ'0Oy( :܈[b%7]3_!b|=y "M'5-i=S7Bz!]4ےxW X࿑W7%}iZ';ۈ__Y[tF?mo("K/ԝA9 9%eoĶ[^/GbEq`ܚYV~4|v|3-E|">c5=-DhBE< y rM=S\!o/vA'9!sǀr,-Q.*jr NB4}yӨ 4XO.#v8#rqi''qf=MXwsW Yth94-+NNA9/Zj{l]"GvX~q1t6X'KB&)ownЛOBnș(nc kW{v֝yp-ߺG4]k$S0}h # 8HO܋Kն7,:;v u;s=FB |X [\'^tH;uc]&A'[~hD)oo߰njHb4SMznR}~vB䰬%Wyͻ|CTH0aȻ>Hη抜4TD#ZG/\pk0?=[<=ƟW/#:yv-Nզiz0۽0[=䨴7 ܬqmf4 [SvHvV`6k+>=_t~XRt4z@d̺Felg[ G5\55gTɪ %`\i<>lV'{NfuPЉo0oJmQ ^p K/a˅k7}gp,7v b >+Qy]"&)3Uw ;ͭ+jC~^|(_86O<iu~VROXηUGEcGhc|P7Llhsk,M_$rܢ@8y]/PDYë!y~qݞaOG~^Q~^}aΔ02#',5hay̬3;"=9hKs|x17-=9Pa'=Y*8jQ{W?Bs ?/ve FmS(6q0±A7.Yh8ZぞdGNH2 uq;iT"?^jknF=_ I~h.kdpy^;%M|_t\s\祧S : s5sG2A|3g9TV=Ƣ*7@PoԢ$y[nsxmDг<3v19JL_NSL V [Р!gREdUK5C%DAtNaC124?bglqߣ[g俈iRS!C4;Ɔ{pÖy8i0ʿ8w͓t6E{^6>Gs7YOp$ZQGH*W_fD N (\<ԎΥGӼdvtf)'/]-h#nȌiLKs8 BgZ;Iҟw "?nφhu$蓪DYfH͔+ujmHM_ó3[GXn@z+[Ա7(\>, @\rxT*6wvRk} 2F0{h#[yO/ @8 "ږ˦^{p9sP.^?N)Ϸ׽G@`\?^-26&o֏??b|5%q},9ɺlY^FkIަ&c);j-ϱar5ݯ`\ /ljk5!])3e΋ĦjNBxAO n\p[@;v;BiS~,; J$d -H+N6^-ueOR!-Ģ!-2r݀k}Ḵ'DHOSBjzڜh7<'˺0ض@g7~B!"h s56YdCiUdo֫=#`.n^`y8+?hy۽On\<G%CN߸4>.:+oeDIMϫyP˅UϏ%98}&ꎹ3&[YO4w^ev*) b}J&uT%GbNS6̪4@3[@ik[ϗ v'"azZrHO㇦ Κ#^ibQztŃ~k]4qc-X:iJs+o&ȉ[ܶ wϽf9.FG5d8DjZ잰Ey}]lCDe zZ9GH ZF4 or/4b_2aa NM ~] (b9;o2ԬY} L>笕Vc9g矈:斓N77f:)CmCX.?)NMnQq~G?kM@trr# @)sܧˬ9l;tC\p競I#uf VQku<=b1 A5u:_x흧1lp>T9t 4uyћŸݨ:,ށ |Ǫzζz32BBGKpk?3"?^߮z&76)9~ϣ'r~ƞ7m4>( Z:g2fcP.0aR9:|ō ioMu6屣Na{8%I&^!!:p w[vCfCO?GC+Hǃ`}QkPf%a3r~8vdGpÓ_캊tPpAsKH(U.ŸxkU)m 5˓εM]^vQaGfUĎr]vC2?/L|3sirs%zqÕ/G*iCQ_9Kc8?ޮUe4#ʵ+4vhtZۧ:_+haDLir@VǼPjtJp1m7Mw 鈓 1k䡹qeyr׵&s " <sr#obX&x8cC U\e-p67|,h;"R 6a=}䊪 HoJ}怢GxU%J&GDUiŇ'lPmt*$UlbQL."ĕ ڞ>W1w6Ų p9vk ?m,phLy;4iQ;O]k ))֕~›>r)}De˵*Vxvzszoj/ϴc8+vi5uaH[nZcʬfc^]=f.M?ZO)ti`zrMr y*Jȏ7c̰bhI/_t;`C>Y|A#S9C l2h;E_OБSOkgf(Mgr5Q-Swn $}娇mnBS1{=f|?A.d}=b&n@nM%Q>گ|EE5E$&s^~1WhBܓthqgլ}n t:_ vbKe_Юq==uEՖKֿZ!j[ -WB<Ű'@ ',U;x/CG_w~vYiPF_^T[Qxmrr!AxÌMT7t1_2NoX9x?g/fzʔ0dI0/vM?_ߕ~g" v>ǐC磦j?kgDqUߏЃI(G]Q80ySG&JϷF ӆ=P|])xz&DApcmₓ$qѢ zf!z&G; 6& D;]=)`|3sR=1quܪ|Y|D6t3Q#O5T_qO/_|Mtc7rƑKkk:@Ck9F7>^؀ qu:zzxF f:fͅ01bb,v =wk,dG柡:P: b4Lr6c_\ <ӪY簱3;^'xi;C;jN-GgM5.Tke$3E%+( C툿mf$-b1iX:Do_ jIcZWۍ~OQF tAI@s=N-FXsSGGXy?==roxX}sK5Rզ05W5"gߦ신=N8>&2,?v}-9yÅG+̮߯Xh"cqpwN+E<1V DX4;a]GMy3N-ENfzXB5p(~_)8se"_uLtL`$leJ3"l2YZ4WN''T'" Vn*A+u,רD"QXIvZ9# 6wGˢ_+|j-eRDb/|Â_#EH\nQɟ]m>d8Df OrL"5ԅ3rՅ7^ZquvO)é!B3r2ʂ\&_lQOsK4)brפ;>bXf7: P1I;To@r\i05ޔܷ X1NUCɗpiQ;aJxh&\K_/h $vjtw4bӽ- Z\=?,V. R_Kܾ)2p4vmg$| 8C{Y`=|q>HS49U>5].֎3rg&%^/Ohrefv:wARCtMaBڃyms&>gq:)1A|0"Ƴe% UOi[,S}q` "x}D=lID ' :6^WA"ClO=B|s}19YFc>¶w]?pD}?@U[K ̩mv=^s{Cz6Z#'o7W ;bJ偹r8Ḋq>Ա/e~ÿ=q`I3C._dj N%2_X ŴBq|6_?4wzkǷKpٞH6iD2躪rD 9Ҋ2EiT#|}I 3QzW- y:m>2wt c+a hJ<!"禶~Gr ]<,p2.bMU>N)_{mKh0wvpU+'8k=:J/_/|2}BQA'ӷb#?!cLJ6FC#" ն͝X2&wD3wfG}I j/Z|?]mtc:['Xle֗_?A%>|z@kiϜTe069Ȧ j[`Biu(}+`#[; Zwвo]?{s:*t<>Qp27;YT?;&nM䨟1 a >ȳ/*u䆓 !uN톿E: Mq665t}vQB⒚Hơҍi`Є$hZ8CiČ?s`80Y#q//FzYAݠץEAZnoCGuӍ_7ӶiaĔ_*Gw6b!pͺA:7˷FX|:Hx51/WsU;\j@YT5'4i%q1bgˉqjp!Ϳb;HVsЩ dB:)Z,ǢY7lߧ X=h虭q G}S9%_q=}-__'܎335̿CfIȼeҜ]OӞw'SW=n~-"Bշ8#v07[ư?7=c!c >J/N'Upk>O˓aȝISmebZVio&SӃ>rYjuj|Esu.cC{c, ^׊K1V0A{wԛӢix`OJJ}f r(s9oEq^|+T4ܷ6gc麁'ۇ@zDv X+Q5Z>cKz5uocy'LЌӡs/jDsV 0h{„/,Rޛ>o{ loɥZ<4AuFbPjuAn7y?vT^pNU^kN{r6:(%%E#°k*n,*0SsȇE5.N~}zqar]E턝r{Ҏl_1gHTg4EOdՅz3^6Z{Â0-g-}2Ӫu&{kWZr8Tm_YWv^)1U7zUP bB 4 ~,յn3^߰דt4,' K%PTqUgG=m,y,9C?{sm NVK{LZW~Ho} 2m]efmXQlsEvP$]IȎT C'QՈD_fB:|wL.ݺƿ~XiwVt $>po^Co#u?WN_ÒكC_ SBً! 2 77}x%=TZZ^Q%QPKhwd}/|8KLyЦEM3S1u2 8 s\q*sJ2)ݿaq)"9z1y1z;ZP_O5٭]N|#dl @B#0#\,~VGO|e +~~̈́4pB=m^FBw~? MwN- 8 ^ k֝8{V:G{mJ|^ٵҪKTSv8}\wzzat #RckUEG"i<[DNOrYM*ԣ3".v8*8ˎJsxs~qIT 7TU7[dJ"ro1L.ew~])Z[:61I"ͻ\nb=($/L3U6$ ։sXny}DКd0JzN|*SO~b%@n2!CdrUeC5t"#/ ǖf,1߯?r\h6_OV WtgRhXΙ?q8{•#=Dxު kaj;䚬Vt}Qg++Q`X}.읯M,pƟTkΧB.2?W1нFw9As-cj)\8tE#ez4xNfևtS "\({p~xT#g<_vZzz-ꅉ֎QU@$ɷ h9ߊ8S9!7HvV#IA rNuiїl5HkqcahfˡsƫR/SK|N⋀;rEvhFm[өHrUZ=)Z=~"a(2d[Š;rz_-;yNk#{VmD̈Sy0m N ZR9/ ܩ7cW463"jטcHNlj+V{.IꩤRs F99Q.10샕/l6rD#V}rFE5/v*ErPhVD_ڏ&W룄 ϙT^šٿHDA;aTFd㯢 ?A?aNv~W>;˝BepzǢ/w=Q(~D^.*vU""p8Z>?'Ehb~ -qzGҷZ_|=Z"Nݳ&gP+3DK].cPm:\t\}N9 Oxꮏ͚X@;xQӠjl6y0eD_/JJ60+QfbyL p'HsPO*k7&rcB̿aFZa3:C%Aq8Y6qf4C@\Z^[@yE;|¼.sPA~CR).+Zss_KO_I}./4edLW Y:қ&`=>}_vSNt:tA籅ys5Sy0 H/dRAt$ &tT@} =ҪU:]gĎ.y?yiɡ~U$9?ټN9pzngN{WW,QވWpV*njׇ3-}u / G$Ywrۙq;6/6{~%GH o6q,r"6yH+b7ݩq;-,9ːbNiK jJfzF /v|!vO^Js,`+Ysq,a)ChN'EڜTpk˅a9l:qJV0NNg Y0ZHrPpZY^b|[/]TI͇"8%' []}!/ieΟ/_֙w%er3ϑ5c]oDo8m{lp.-WVM t--E|#F㻷}{Ck{t^h%cUnmwx 'p;؉e;BTmR"O^rJo=JϋfAb6"g"> 4ԙ,zʺ>]&q[VN տ[,i t#Ȅ&ȍE@[|^+며_sш-k1ֆ yy'"|W9r,rZ.*ŻEXnvڵצdB[2vߖ|L5utVp*`&wtj͟Zi]b9|f M[1'NkNOO)jK_=Qvwo-"MΤC)5lwB ;˄{d`j'rMl9l{]H$Ic~|$[9򔞆_0hS[}<|dk4ʌuPUܤxe\DhЖ>9HBK rr"h: !K*pp-ez`X:*}55w7UYDNq#@y}b&zG^7q *;&WuQi^?y|X lPGYu.EvҜ3"K)鏢w;?ƣ6_P i 0@ xQCף(ūX=yLXOCvܻF"RwW|.`f#7L~UhE#E>lEH=?/~/ tG[1nSg1p?w%Y{;&z~ŅZ$Fɀ9E'oNK/R[Bh'lgrJ/k{/-q_Q[dMQ#SJNraY9,&=);s%.tumu: ܨ1=2h@i,"8vx8voǎ 'Nm0*F]w18[N㏺ıS[(T;oy7۞C)xJsGj%75Iݗ(udL8Nombx^~c,z-غ$uVx4_!-M' A6`O`9AΒS3lqOxd d<4ؓS|mn%QԤK3`QPo~1G~ oRsMOE{>_>j*e.?A.WCV"N:7ڕQ[!UQ?Fn EX%񪑕:B(F" UC|eo3}(m |E8e< HAv)9O/TxSĿww8bRX8iȖ4U1ZZ'ڟށ4\Co1!G>QF9>[?悰w^t RQ׿ 'tv]oX6;tv yDEϕZqfšion|wU9`D:}5yGgQvئc5EޡΚ#h2iN-t`Vc%>)6N;% }2ip;Dn=V䄈9?YDښ_nPi]o:\ ?;R]0; 7[o |U^wt\9&[,9wFd >ӫ72dS=a&MEZ.2n:)!:{Ӫ➝Z?d&ἕ}F%SSt ^ҹ rmq.eV<#a@v ]*fL"q٩jnSpLL?w )TղFeG85N۝~tf{lzUprU淗PKs]s{NN;ZzTA^2YW{r8FƬo:O:5ӛ#nVK/s;)\qNʉuޖ1)Rul s)𳀺gqª O糍lw!AM.N6䚶Sߡw\Aq}~;)r iНdrew8OzZZiE7ܿ}"t~&Zu -rxp}:=`R㭛rlߊ章 o;Z8'Sa"G* ʂ~ϤrOz+]p?ƒѻ_n,HGܵ뒤W-t40}lz~#CHG|3iq1j㽴 tvx\ovvDc^! p40(DI3}Q4a!/E*tf;k7BȄS/ t˗ _O-sPiMBd'زWm[{O&K$:R^ ̺i T%˦ 4>'ڂdEboГ^_pߑj886 r.Ըg$ȼAy|ƘPWxnS&{Ht'F~;z@QFtu6nE_;e%XN:B~0"y"}|3e$˶C-&7"d"܈?8W8vC_-Ix4)ak%GLʓ5FR쑎)޵ʣgd䀛;MNآ8'}x76{gPtGReH>S\\OrV /鬑yOJ{M 8L"g>/x]|EOv8D wI_v2Et |5x8;f(+f#L.yUOq5;iēԜ>1'_T@MHPZ$—}ٞZ&קG9nl [rq<|P6'tIz}os0ѠC RSsl;˃5y'~14|~`~ìr@ސU]u?* 4Zz#Gwg*L^CIZIvRYwKױ ,":fQit7~ӄiPW=nI3"=ܭLK%@?;>s:'ǥt-!"\^p;ܗ_3!NeG~@3=s{ ~EZU KyCCHm3%A0_3g9_ /| +R9M ::DD=oA:@}3ӲPӘhz<a/J5t!r<e-Q r9}׎~BfW~ r}o^r"ǷmLdZMs_o?8#E4ַG?='NDeQ[5mʝJ@M/tyC}~<\;a h~b^ߖ+"QQuSTXJ9'~ulD>dY zUfs{[v2uWˉ 1C@}B<6>\y1@S5R|)9+$4Ed`b|Q8!hrp򸃾IgYeXyy4_U3 \G*xK]ٳ\e?#:r*3|NsŸXϑqd@g.ouDa;11xI4O0h==?n ?F 88Ď/I~ν `L p\JgƏ5j8Zg~(Uݩ}dA&^uiFYUu'A z֬h;Ĕ$'Ȅu)"uB$u1Ԟw@ꍀ8n'QY)ـS0tֶ'8T,U[ 1#|j/ٱ R1'0XxUIi*cI84=Nɳi?_"|;CLnD4s]X/<"#^iW[r4ֶ^po sD &#r>a$aU䵉9=%K#,&_G?\c#>9uv a:Uӏ9'v_20 pv[+L&qM*yŠt=>#Wقc޷Ecv_Wk@Cˁ.4|3VЉY&|gK칄$Y_yꓰӕexْA`|ḫS|ɱ o:{wԻtStq,n< I|9veθǻMD%ge4ߩtFrARAH_ɣPM@dA6RlD' ;t'dZ3ӀqzvwGiFEʙx<򶓷'geIcad:8"2LVܨpۣ.-Y([.AvN`L@=}Vuvr;%|7D^`ߩ6媿^ D#墧9Ur}#_ӶA+X2P&~qsl@NuoowVX(*R~=кuU 0Oæ'9i-:Lъgx(_S3R0~0xD?ym~k.-MlN]#qS \lr"EpNo <1F3hm.K/6{]I)Tg&8ʹ̾WTv<-u9b+t*3|;#Loi$=?IPX4:Oe^+ &'g|"iR,-%M0rp,|+dO33j:41͵`[v^˲A?wWOd8ݸ#Sct"0%T9p_Hu=#s%] (fkYߋ3ƚ$uaT ANASJ<ARq&oq4`(kOߞAU&CL\E'T!Q+-OpxTyY5Uq~I IitSr2("űn@\bq2cX{f=u1"{$րiC.MBYawN9M,weDבoiX5X)=ɴ+/qJ%@hu;Tv͆kC#ې|ȑӦqNm%Dn&>GD}~>kZkQd%Dr7v93OS۝b"&":6 "-fDEHraK,-h43(6DN#"rЄī8ۇRSvU=|5ԑsyA]r3[1:3>A{!ew{h% '.ajT.Sr juFSyӳb_x^*" <)b:ҩ ~-$wOwnCw4r`pC谩nS365dm.-HEGjg>'M?߿b+d?fj @SUsclFR (*s rM^Bғ2,$ٶqk+a]:brod4w )У7{zBCw¼=;_UYw׷|0&"4){վd-aؑeym"Ӈsu, 7 _v>0h"DL*J^͹WKnePG ¤XW\38Ap44mqU͢Gqtm w=tqH^o={)2Hfxu .9痆d-h 9xό&m5rZG2J8'g];0>%KAɿe?f_UCZ|aliHC0~GrĹA[4qL˿~=5.6ASPqzȃ3Y`UB S)vˌ\W-/qH[`;G( L8eXΫDQbx㖻$_FM%8:MJ[yS֦7)K'$;G/%.nCsE ^vwЙ8J^C!ByD\z3lM&ln-2twppzMO4 e{Ke}]l/50EsYǽ495_~*膜J z06V,bN2Y#~ w v\2#w"@G2M %Z_lK{Cz"Cdȗyig5WyVvٲBzJu;%xuS@E8㴫\]eA?iKr˭vue(1 "nI:n׿9Sd2"P[N h9;<R4vt^ rLMcr'D4byk"kyS]p=ު8СiR~H1<aEbxjnI|۠1{dT;,,{Z'ӾC:ZVδ[ëȤԲH-6=ktABh^Nۡ`}\{.,,IH A$SpUUorڳi Xq}e35s׿#%ER?J-Hn+P oCkWT:=T!/h@\f luSrxsANcClErW)IW8{Η7I*Hd@}IwZ,"Q+O"Wd?حr&^gsCPȉj$摜uG$L@险 S`1P~r "yr Ur-{NCQ pv2"g^Vy.ז)rhA9ln9R0ۭ/`HE^w,r p_o_5׮/Q{],btTw'MK}-֒ܔ+`{M(c; k\g~PaySȣc9>~qo8&48[x.FzScUQ6=r==4h2C [)c|t;i=K7 84t :!N˙X>. Ekp:vrޠO.ɞ r2"+<^S^!rkuާ'v\g_Rd.,J;r_'x {Yud ,(D~ }~E$ݳ2ꊣ4a5tVD/FViT]r͞üwu. 7 %ya \g* \ل?/dT2x4.vu Ʃ\4 7֑W=wMp|4U摩Dps~#N" CX')N׎ 3ZP+d5<p_ %0"?A@2[6@CLGJ 1榱4Jspb"lG"q6:磻Sa@n>,'$2N87$!{oDYI硸uʟ+?w-į@x+lFkbzpYCwYkS?ԿY>vܧ zk`Gz9 9,E思)aιȓ-0_NБ1{f L:ljHsY?[n݉3}dS/:c5vb-D Iņ94Avd tw7Lk]/1=܏}![;;*l";'y`"N&50'd Ƞ+_OruvImܶnD= T {SkY)B #GشE(J$km3{~"O9NC'O bT/.عNTMO3.G W&_ln:v#|,وwN҈`hv\DF-p՟-|ĸ=Xvu?M%@$qt"+r>ƼQ?9#T| v2ɯG-Kի]>:q z c Ng?=>eDvڼd0y6nzmK>˥tF'T9/ܹg+WqG*3@݂^l_pH`D_8XSגS1rBrӾ!><ڟi V0/Fv䄈מ@]7l99΍;"^ XSGCO?uK ni׌fc'_6ܡk+3q@Bjo%%s]b?̵0^Z OC2ዯ"ݒv2w-gk@~7I ֒~XQvgYbz QNFpiRNm˾R9\=8c"աUnDxFԼ$Ã\y̝w}-ӆ=S­ڇ6[¸.bPB3ˠ7${"fs ]SR%eGQ3߂KquϿˬcYlEv49ݏp7F _LN*8'm;b]։htqUWKۓH20MG߇J$NҦ{1nuPvuEUځ:⟘Bi}_t8l`HƙMGoxW:edAmg"~yFD5ju_}S~#Kӿ#K<g@v_C47%3)ͪ[26} PS`r`Uñ5CnblnE^zh*O5]4z#ٷ|08P) I.ChN ؑ*U^Jgݸs}?V|ޒNqI,VN@65G!NS`gAPN}CtaQxt!OWNwzF ;˖ˇ'PU]"OBFEu(S^qj8-RˌS|w @4?LIz\x*z{{nlaѻc;q~h._^p:5BԹҌnL02a@d۩;S&rW0ߦ֦aEqCTC{\ԋ~=#H"N \ {DO9Siw/& ?4Q^kNyc~b9!B'Q9/Jx"w? 7{iu4zg&}ߍEJ SPowoDN7픁y 7!"  #iC6T`'su؃iKJc>5di"ͮE ~5E6ڇW ;= YgL1$`WƫJ$u+@ef-_Ǟ8]$$<HeS)A\ 982kLa3"_ܽ3E\/&{O-5FlP[hzXj83},)A=y 5D+ G\c4eA{U?Kg?OKfwvM&C=7pc^QA1mdˤzX6CDpQ3즬Rt{QޜI[пxi4'Mfg[}7;HD n3Ta,r^z{@Y"#?%5p3im- ) 1#.ɇJbۇF[$>Y n_FƲ!Sš$$Ѫ9Qz& 7P`Z"~W%PFEgps4Hclƾ^q\6l7ۚԘʞD0k'2rBck`,Y{r P\_4zǑZ22uu6zϭpG rSIT%#A3рKˉcc4Wj~|OgW~KdrZ?&xJCë9e`B=zBN/8??t?m"l{/ʝLtnUv-vOkd8uYμ-(ߍZcW <ʿe7F6w QZ3T]-wQobC bdWO`qĤ(BOszNPzԾ;}zf;؜99 Gv'S˹/sd_ssh(lZMHTdu4n)guV{ ngBRɩ7䃀yX FU/*^\ؤih"xۉ Qix/[DrFyj;r]Y.eoQ?Fo{^Ap跶'Y`_ s~8+">vGG~X.ّt)[ث5X~Gxv´poPR<;/H"ːi?x+YA:Gث\ZKCWel_&',FCڎ bL{;BɴHdIʹu SغPk6Cflؖ:-o:}(5jF>K8 gܡX V(z;F QkK3cQh+S K iGD0rro진;¸njͳ>_#NvhGDwUfD`[?kWyĈEaM4 -W`,9LDkjQeQ{ 0[AecZDG K>..YJtKmbgrAvtポvMj/ к(SB7-_~[\5Γq;BWzS\#Y6s (k$ S83cWm,=0^)ۖy34ĩ[5jwCXjnrQa- T2hB Por  ~ bb_- ji|K6l@Ur:[?GTͭ]KA_,2E>f9^j~jwurp(dӴqӷ+)Fzzziֳ@$ڌi͋咚ZMvF;0"NR+ogQxSDrM]ʄE.ސ|eVz.brGn]--2SB>PS'mrlÂ3].ܿ^~Lkn9dMk"^% ?8g6M8`Y`Na^+eR္M' BK4Ul+> hO ̖k!n|[V]1)JܜӁ%fO˽SSOgg1H4t:?$+ 8Uy~q?GFVz3r8'o3}vJS3NLN/oMqYXO@d<10ݗ+ /Kqu>fg:wc(]Q~5[O|@? Cc Mc=E6UI;gxfj}t!?6hTڐ5%'!Dq` Ε.v=fV+ff8huy8 .-dXRXYv+# 0{ґ0|[CkErݕ`eyW̬) @ J'3կ2{*"O.6,y񪃁4G\RqD<狎읷Xh{v{O^'#Ƕ2Dz̴=#$IHW}nO=\2^<:#b~?Ԝ9LZN1 }Wu}DCO(^.eGG =ׁ\Qbp/ /I F*/i4[ˑ?OA' ]SƷLYQ8[zJ{pf:5`΅}j@I$"CK\W-^MK q[xpM鄇073 x^?e8D@(^온/bQQ( 2e*(Btl;sG4M?Ws8s>;یM?$ʲ-qD?&⤟|y!,A%!Oy`nMaSsSU:8 9`3o^4W^P`lv(28^9HDoi4uݯݓ׮#Yqk;Trζ?:Kzʽ]78ٽ5O[||TgK<ʄ-5'، [;!傕łH[x==#BwjbrAکwtDuvFڔۛq&qFp D'MaZ)Gg|rU#bhn}׳n>=J%@?.x ޶E!]e(!CѦ{0ۖRe$h'D!^;hl'|ݽ)?Džo[׍w{A%M/Dyw8l9?s}"ED I+av.V{],~Y]yQiDj9{+@؇ rZ/"":tQDql4YOBqO/*d;̰nEݡ?!޲/i%-)xIsF.bo{nnfb=>++vx1饅Nc#daH"n+_zdmb9$M>.LKq׹˚;`[$CI*ط]T^76YZzAniuw+Dsn(ՋnSSwE\OeWlrMKAIJqS7w< _6]#YU^PTG.SnEwZ4LyRNpk.rjoaӤvj]zc5Bz ڏRmΏ/oC1]vIfwՎ?0Yq)ʏTk]ivSBXЪ#3۝]UZOgxS\p(AgD(jh+^H}Bhy>#f~rld' 5[?rK4M@)6rEy`_nv-r7JM5-?w՞k6Q[$vnCr5v{2;k~T?M+œo'4>s Ե&SptQ8cqa'AkʓVU1 жqѩ-Z =Ds D?E~=a<{+M) rzTWh^vС{|9~Ad[y&*ɭr}9v}|j7@k"3D"*;~(M"vKm"ҁƸS&wvTX{P!mXk5+~g;4vPK:AGk4ퟄC K]E}t̴;(E\z|* jtfr&B4"GNA-B'~T{z ,w7כh=q!b C6ކo1,gOWD]n4Lj$(in{% lG_hiNZD)$b(>J=S%#_B\Um]O@dxSlX(exޝ߳Q:Plwf8"Og h0ED>܋{S#='ˤgƒ;YhkQ_;LWv0`_<sWy>0?YʑiL84ӑF\lxwM>9{N牨ۖN"x]dKrVc;#]@ُh /z&֧:J6 इN} uxT'0@/?n}rh3u &2Ѱ 1a6tdi5ӽ^DFbaA#jŸjOC~>dù!,Ncm~NL H`k]px$.j\ne߁5A6\(c7/:ůSt-U׎sW6s:ݡ{RddN_'oЪbE t(FPw?0$^R8;v{$;g[;Ewf;*Ddo%ZNןULzݖ#O7-2e2if[(zJVyGnQ}@xy\޽5|Z4ґ5${#%~ ôUےYY2.UNmX;3B3/[46ui")k0\#v/A">fyhIfkEӳ,wZy}^?h]^sf,x1"dzffW˕HO_^ *s#cvr9atzɎ<+7z[E})(CzܚLJ jdu2sNl;3:P1 \.>ݢo"Ncf s[VMk-⪞hh[p} ٯ 'EN1^]iH>ӍsD%fֳ0\qi`|׽.үh%F9׷~Nu:qm`tE! XY'+N鳝|# Ֆ.r: 8~`G=i6Wor-#U_<4e:92;%z鄈eEdfyj򸛵Ucrϲw2S8nUVͰg{oׅƹ}ߢ+4a.BN!ٽQsH7t(HO.? VD#w^w` ǢW _~(꼟Q.#a>(1ѦD#HoL&ЀœV})jl!)p~VOzA4YclC{qP-2f/ Be<<"WDp-+L- vgGo6gjPLp lmY`CksAxK__e=ܘ,k$H^ۑMkN}rw`G/˕}"Һ݁>4O@N;)j*nqWhĤgV'_pZ$9J|Wpts>ZTnp@[l_͹/_V6|K *7 ={|"鵻^pT}'3kߏ`EXcC7Kz0z SO=~yHEiةNw7Pp*eφ/3yu)ܛ[qu#:٨n_eY~cp*O_/6 EsF"']_rK9sύRl6 ^os]tX_KWD0P֏f;,hG>yIu [nksyqL^5&Ѥej$f7 :5ӂ=BX $+;|Y^8Uo SO"]ȏBS? , B!n&oUB}sJ w{4OIJUcߔ^r-Dl'h@Fegvgb=}vtUY"j2W]6s=3ھ֙pr!~Z 0gw}ܤ ˭i{ڹz*h遆Yt[l V>J/;Lx?ٙ-rT ag`WNԹͤzZ7W-l볓-WYxrL?Sa8cKe׌bs? 8-ϬY\~| NB~eo Ӻ@ ),Poo,GQ"\RYy@痳.EsLfc>?4.' ^*oЇ["Nc${R#:%"(9| CNNҖ6z^!O?A~OnFd $ݯ_oɡ_DZ^J7A[(Mt{ߘF:G00K* V%\(!kN_2̝ot`ܻgp 90}*ecUu"ģ30=>5'F $"=#i.]5Z鿆[Tzl1f/&ާn|}^r0wHSD}PRO2IefQqU#{\*A${. =? mM9pƹj^JF˵&{DW݌MdjdeSffpZ{~xSM\Y<`W콧 HvjMmT1Tb \]5W wO^ /Qeynб'Zg"T晜2ws:9XhU0#/5R?9'N~ő烎[rxU}[qT%_@Ett ssi]TҕD]DmOqIHt_mF\Q|b-NgD/d}v!U-ZQ׳jK.cs~lXWm1-5{9Rҷ1.gVhv|Z}вmUTHGr8RPí1p De6ݓq^%.e,q;]S~#@i;ݒl׸f}ԉ5O%ƕ'AGUu|-<:c:^@9!7f@Y]4\2 ?Rg >?f9=ztbb{f뾢|ҁCj-E<gP <=<)p>|{ƛ0 NF>'w4Q=G<-K;[,8,)!qyc@50 T:8l#/QTuĻ' ,sbs?~mm?=]zc8" a<< _/.>|x7\NPΉ_>DZl%P]M%>}|*xx4z-*l[ʹa}>4sI,,98d囦޶5%}PF)Y; ^.ńu(-ֺح -X>xŗ/]x\dPo5d1az ~҉'ogUf3,q<{&kZ$.C1vTz 1Hg{(~Nn(@ziPH;mU[, C #aDvQ4"ؖñ +fA碴APٚlҔyok=f9 F5M q̖g! a$|%\q8a*Ɨ[m@P/3 >Ɉȶ^XM+-KfY _>ZH\ӜcLo^S5>[ 9̌-'\[ȘTu3"ZvX 3ܯ!;l?q|#{c ĉaܵx^1kv^U`%A\A91(#R!{ѹ7X 7"ASJ .PPch-uE{bEHNBa)o(!;~#2$6b\\5sF&q;OG}=+ʊyň,  ukAqC7ag-.|KbU`3 {.{|;NMQ( mYḷ˲_I.Wy߬^Zһ$`|ZKu81Hb{ݣp sVF? @q P(iYe^ 0HzD?K\>Rl4tzڈfvGR!%zEyVRuQ(iŽ"$]j#g4R7vqndU9i fR ;2m6*0du"6Lj Y@zXu7X9k3a$v/ZYc|x{SgXUȰ-z/8Q~qV >Ucհ8vWIМ=\<ڃӀ̰C?҈yyR=CqJ vWȤ<2aӣS FD=OK>YM0dB[L{>&2zLJ׃SY @a,9Bk!*kZee#Q\ip Dy V}xSMmsyjn`o'3׃Ldzi~}œ2nFLKeڳP5+3s;`sлpyl\;1$U2^/yP~r\K)q{9^4笝qr<_sG)e?"o_Vf](YmC] U򌝓{vv]O_IݨbR2mɖh@ 7#duBq2k b+A/'zᣚP"hcdy I q=bׇq1C:ݰ\Crz!0C(x.MX9:5 _\Xdk}] V;nҏ!#J&HwNk*^:(ϭ Z#5aFKOyv x8";Hz6uQH וAZW"Sî^>*GҬy !rdvw1/_$J]/F8x ph(=Fz#Ũ97jc6e4fKD?*nzvL^ݗ0CcuJ"ϛNZAfNti,bՆYx eYOP{*Mow2tF]:Dc݈mN~Lso )7"AOr^6A={eSKk~ÉO'SД$PpM|ӝ$T:ݰR=5`JۿtՎ% J7w e ȡrƉZZF8OW.Y#iq\`AЇUQ"od;4878 k.yv߇WRz5ёE3:v0ݏDZS.lH%7E5aBfŃ1iGa#zܴm`rgfCQDj-/xIźݤ z̜ր' 'I[WL$^F:/G9|Lb D}ɋulǥ." nƉlڱ ]x32A4/,0p bD+D=wx|m[s#E XU)[ =0Iw@, EErEXI!Hm@w=i(6bTFqd)k%iT>|g.TJpʤ\x==&x`3X4M\x6Nl^߾{yS-j+^͡Mao L `V¤QVyß_2o%e2Ƌ{Zi,M):ش 3؊~TN<L};{ .g;mltpL ~:to"Jx_op\G^dYpEV6BP`OJ>ZjV{@2Wi2OڲY22FM&~M0 ̉'lx`.tz}1q ҕiZά(ӪC쇜Bv\e{Df!&?Z%pҹ+OܣzdPbJ `'frbd'/T +}fCYn ^rPʫv0RgB<\=Gvc'((hl9B(Q -߅Ձ$;_)8!e<$#P!Tvoeg2kI^|*F*)) ؠRAC=_~M 2G01G@$gmThoR^EXlǰw0b1恼A,P71ztTXg7t3904~^q9{Cdz^LF>럗A tĵ|8|zSWs^ԟTS0(n*U\±Vn7J DdzIFuqAa;G3?ua ^bKGpn֫N ^VZ ''<6v0We8Mء<ݳ۳Y]4YQ;-MX(3]xx`fE~:~q91AexȱԚЩfED ?A vPK8O U-3~-9Nߝ{}>} 7<n_+H Ge(9>k&U=75*aHV7Cq81{Pm{U 4oTE֪"QS(0!^f>ܾlq|CM ,i@8p[8pЀ AOaPw\]iן:+rz~_mE& g6QdE!Fz*:* 3nL zL!귗c!%h\fC>|ҙ5_$#&k\z{52!pAQ_d|^<SߦTe"ɦ\ro4_f5rR1s=(nZSwu؞\_ʬ'_gxHA&qx‘—KR!q7e7l!~G8TY M͘q@}a<#O؞q7 gگa*wӇRAp^Xh̓f9QRO\f K>9d!n{P7EI1?<oyBT۰(s*(ѢM ej䉗jńmTL~*t}bi:u٣g ixh0ZJ7s$ѿl' 㥥`!/qsUғАCLirV)$<7P]`SkHz1s>{{e1Xj_ؕ_Xą!i=>~jΑփ܌ ,79ޘ.Iؕߞ.Ɖ{R: QV_ _A|Š_9 Gqj,3)k xu[[g׎C__X>(9H6pJa=N/"3oەqX6+@dbK.B>d_ ;Z:g~*vam2-Y$F$:FL3(d²f_W9v>BYI{:oA A+yhZ~Bסg.6!< Ѿ*RQ"$*z4[ZKtl={w?5P6k>g-(9ADJԻ4AUy;sb޼\?$eաB k/,`[r|^*NleI3xBOrA%!ѿYY;U28/_)ceH$MwuXJKh> hdKS>crTGXs}4SyZt/b#HcoGKgm}oܙB=!UҮQ elwvU0R uDYADɮ_H*aZU8Y[2$$>Ttũ?l"Cb_9?fSڣoTڤAnOG@#kffi+:MV\{|0)iP[JTʻӟCe8nWcW4 fW|ଃޝG~G[51Cuɡa }N T+՛9XKNz9!~ʼ߈'ľDCy+8^fcMҦo?8} E,z"\f6ݢ:9f:a 4WrBB¢1toۢ}5 u o[9,_?O\"'z%T̐H K]\tk"W@>2{&/sb4աYs>A9\;@t@UνDMy/+u "保xtV406PB縨/\u *mMBQˊ?=?X깦J7h1͹*WR~&pq:e>ap_9v]&-N|:2/ ;U^m{K _Ya+saԴlhZUi.2QyW^ vSG"]Vҏa^u61-j*/uksX;o#wNvm7Э'"e{g/M #w&UVw|?ɨu9|W0d*L0r]]  ~or%>}Y,-y- VӠC{_$tW 1Y-raSgI%m XRspH(+6xt[[hސp{|_7q|S-_Ȓ@kq_#b" lD7 \ǜCFj3hRTZ%m0g9^v |gRkhXeϖv0@$/bo#v%>d)YdZ@dǗ!].T?`z5aG^=R\v}L>]g.]` ;g xN,qy4/`aʱ,{θy~MuIXЇTWz-}b_E'?J):&G[;O^ Ӗ,f$2Cd_/!>)9h'w۱J'y+k.z71<%-K1,iwfmS|ru<9v˞JX+9mWq +1:R 1F`QKԛ`̞/Ҟ5&z̳=4$*9`~0.׵ p뵌B`fcDHXUA,ݳS^Sf:x"|=YMq?QWl= S*jGw0yi@۫! {8@̋Bij.WdF*%O);:əQePݺP"2?F$+hn"I^ŷ?+BF\T/8.+'9(!}h_֛NbLtҽțu 2v;=D+:0u*S^&>i7}vpiK?JX.^T15?IakJm*u L6lZ֞IR?c/NuKS1eYM~NOԺȔ2Jʎ#lJyjkA6<6"[UQl\>R?ŠyUՁ1AU$m'e0L>_b [|s%$|(qġj#TyH^P]Fהd}&{V.t[G 8H((p//@aeAuEckuDk}C & 2|$}㗊wq1k9oGAO;j00DD'dsS\L0Db5׺ HunrjKm*1,7VlyY, iCnWtbcjtH(AԼ'* yn}It!ľ*f=Kv7/THMd1wPO %ke(9HaSR'¿n=7b2c3w{f7%!ۉmV`dr!\L٣\/d)TZ|2, 8=2+YDk>h֮G3Gt7`jf{DO>[APEՑ5%ϲU7?EflIq'{WǼ1~魈6e$Bh$v'i%BhXn\*5bAgW¨gh qVy%r׿lyg\itob4ś ,xUc}3.H!{y۾5MGSh#KF=E1G(¹Z_J[?>:隸{sbRT0mt}y.8\3F@eş#=m1\/ x6 e_UB(5ʾͺvgbcp&﮳7< SHuԆ4)qQn%"ml~sĘc\;C)؋}Ƨ Un j*JKsu0oclKͱQBfg~=G}_鸿Z0SHǺrp,SR\3\ʆI-U$iֽӺ7FU8ԇoZ!pm飵 ϋ/~4 , MWxѝ<ڽ}fCB+7} Xܳ(h(}C9Lޫԩ͍+f]7tJ{:vH"/dg>hy0RWGU$TNaZcKk4×P<[ޣ2p02Fυ4H9ƌ(2?R֔Mne,?a]ڋp-dyIsB2FXCc~ݎx}wd( c4 tj GD,UނЕcUknzcŎ G jޱ?>=%Ef67٢韪F2"o`uF$=m.}{ %}I]91Gܮ@0b_Gb~yWʫ17tGJ=޻P^,Q截( 2Z7wĮ\-Rd텛n\hot$IܿM&#{'\[W} wmo9J~A"d%қ8A1eE(1kDJj NDÍqj0qۛ@BׯDeB@8fp!~Z|?IJsfhDc*+rm*;pW S&?YIV^CID/}fE[CdO{N)`XAg|yaL4 sP@~qǿDs!Kr(!re||Bdn_ 1zY{%S@*vv{芓:Σs0?Z0H:ϲ5`Ӗ$szzpȔlo 3ᚒnl ?:̧CPypg"fR 0/@0/>B|WHFCF:~*Qܜsn3=m~R*ѯz*xjnc֝E:bpXaz}6.vk]uGI X(QX8((ȩ欝ᾶ.y[cgrI[U]7Φoއ]2j&1F~P-e{uϒ 5L!ֳ yx_s,TTt;yy- =mhJ?ɑ>|)u^G2)R2Ŗ~G .)ײ5@dEy>Ĺ%p(Q:t ?8nC`3YXtUV n4=U+Mo07b !/7Ơ~="bEBg sA;6 b'+ 9.@cq0h12J;ElJY?c c6&'oʶ%%[[[VK75Tjċ[$?P(f\3A:"EEJ~05#2W%Ӏ K)qy:妪Aj|VA ڝMQΞy)A{rQKȵb f)CAt#|`^!rb/8-Qꈥc 0kSÊ,26 zC,#e4Pa6]YUy+C'C<3'xFFi6rɏ}02T5a.|+[_\99h1xrUH|h AÅ% \hP"0ne:~RZKj]A jF['d`*xhºW_pNz%nCÑyp*^P41p3pltY{E! ImIQvY"]>knWyU$bz<-g5a7^xü0L\NcyvEg"ICgtji:!$CfJ|I7Bllސ{)m]8д/C1^tG!.-] "pk(T2b؍x6ׄF_q#1IN_!!9Qg㑙6[0(lK:Pe(;#┏f iF` twJ/N\tG # x)Ywg;4o[cT ;r'C-"Qo첝";2nrԤJƞ$$s^Va<2f);n5388wҲsk($84 5:! iXpd!vW)όUn.sV`Chv PfP(&h}a@VR{{-=o}ost~=c0E+'Ǻ) _30irK<؃Z W`|V̝Tt_,j19*MW*|8Hj;F-P`1loii^wϐx_yѷ$Ū8Ɨl'KPvczSB96Ry^@Fu-3ALP#obDm@G"7a̫^o7/;0%ZB2LtG/re>(wY6+ ܓ6n@3YK(+ =Q'&忂jʅMoɠ+םXE#_=5ZnI6yS"?iN*Y1K3ॊi';'.ݵo?)uA=K],deτ?Qn^-=PkV$8yv34nۣ_ϟثBWDS+=xf^/i6!xRV+mxpntj"dJ߁9Ȃ7OW켛sbv?6=2*uio1pזj rN4<έ/e'J؇ T AyƒqE|yRqG>Csϡ b43H9D|}iDl9eϭ{O9X>hT)-~=@q~O8YۙJ%kƚ -5ɟa_v5 j?;W!;d껞a ՖqRkVڞ9fB?Qt\ ɗ($j^$sBdR,-| Tc?s|U'y{0=(A`<(gZ~Y<',e`D4s^{7eq oxNAn[z,)[p:EҪ'w+bfxYdPR~`ݲ^{hWt4[yO˱d^0t/A#`ӕ۪/  #_)B9Sq $H2L=ڦ { |Eϣ4 _Ky_ !8wDO3.Qrugmm({n4mjI  {ўOCd>/~fӓC|yԔ.OMZs9 /$Ft!-biA\Ԑxx^qYK/aw)﹜hpW?!6N\QD̼V+.FWƍ7 h+جd9" V)'t$\AjnxCQH,Q>WηjzG2Ȕˣ}e蔔w~lWCx/ِM$f_kn޺>[̤f\o[FkSK S~ UJgK]'Ua7-rֈot~|\g(pG iݻz ?̉/f(oyH.hGDu 4ݶ-9!2Nګyf7_77"3Zr  [eg:+Q3;Mʗ~&@N1úH.g]R~br/ݪԶ>&WTM>O xJ֝-ƴۣzT `.CE 죒79.ճajݺɁ&oɚ_@{BE%-qNQS?<#iEḢd ۜvdfkh$,eMT0iB!IG` ǞfXҽc;/Y H ͗ (l m,;1M trntIePF:Q*Hqq u=f=cb.UG5y$H='hO;y5csawtpxWA>ݗH_pD#dOyۍ.DG2Mn1)49VF^9ff  OT =N%r.V .\ %.^袭W |M)ׅpdͭýZFf~hE ފ&̚Z?UrYEw$CY !v֧vP&3_: MRH)k;)kJW q&_> VV@ۋtJkS0oW/||du9uȼkϮʲ+uX#OePmDl jݽc>~¢K>܎k~6kVwâ+ 1TۡuAuFn*}J!?'?Y_S'IKi=<{^Ͽ ϾY('17A3&%s]`kH6CV>Ų*Iʸn4\#M2?GjV'Bk+>#Als6.|J$ X)&iC.Hꣲ7'cO׫Eqk0՝gdnKn;v'Ɍ闢9>#VT&VuޘK+šyƬL?z y N - CZ2.b(11N( k[ͽgEYL JnA^e1U_A (p\F@`vm⤻pe>p$g_ [,> sf S):8HAZrGpA_@Ur s$ <+XVdQB(qJ߬8ǭY#)EY[V׻Qg2~. N&#EuK|ϫ6T 0( &?HSj0Tu?8 >:1}/]N(Oנ@e/ef:BX 9p%2b=W&+N<*OonG_x?4s1TLmS4=?k`dNc[洬CB zpݫdWYLjYLh*5tuW8< uQYw'`<4Y  9<~t/xk쮪k3uI'wN8WdYȠlKVK4+3%H 8 {rfߟoiυ@IӕDM*q"9kFÜO?F-rޓL^rǂiU ^V5D􍦺ts'݋>%O1&.w'w&25=1oZyeu$l0vڇאo|PȮITpz JQ5=CB'UcF)( [?X=6z/Oh<(x.eVOU֑(#\_+%t+](>(J!T{W||fj_ Y.2eƢ \6uv԰D)4t"xJV/J:(2M=leS"N&|Hs >*W8{Ypcv|U\AZqUb4lQrԱ1~F]\~vQX xU^pV̵Y-AY3VCHKA/bQzSնH^alcM$ Ddx:/{Eٕ8wNkq2g&FUq+leJJo2hGQITvӟP7"smV:.~/Mx&"kz6" Nܳ.\K9]?[!sٹ.:D~1k8~2.2p½*2-3†U/@Sna5É yBX Radɳ$)\TǙuIw;pn\"r}4MݮY}[H5G\/)OsbP% 5RZ3X]`X(QF@ӱ\![=Y[aᤡzQ^;!S;nec5'6bUG4bAoaxj5%l-L؂%}.>K΁PQ߫# })әޑ#q:gR.Tǀ L$y `{idnѶ7vawPwQC4b*Y}ʞ^*L M}jԎ˙)V QU.vtF`")<~5-9ʊd|CPF 9yTJT1Lԡ69#ht^M,g f%KaKPoi404 p/Hs>Z;u}+ ̑lڡ Q#.Ċ3~6O<\dPx׻ٕ 9(5lۻ`1p$vs jl$1OaϻA\ ̕1f%B:g&{pa~ "|za[P cù)7,/׆.B쪞?ɳݥ Bw\#u?6.5ʔ<<u!>@G$ z]dێW[>=jnt`_ˊS/^0Ӿe}cqۀT9ۄ$^"n4GI 槉[ [=4w.7;BV5M/ELNk'Vs9jTEH~A_5&2 YQ9/q^/*<_o~dAȣXfdc,xne!iϥUwp/5cG!2"14 5Hjʥ>VhYnHJ8cjF1.,lJ9#ω1}N܎ʉ`$Ӝj gQ o&XImu_mh50eom9@7 \"hb4SkBa0˙% 4{{DeU܌Uw8gRe7ޘJ>?Hf<;~H<{)))OQc\hkI;kTk7ob|ʷMܭFz~^?{\w<>Q-"x4atc PM_&iMwE3oYGIveJ& FcW&p\-.5&y lގ䤎R"kdH8'21=*^{{Rђ݊@h3.hv`zB}4O1Ҥ)i*֠IBfaK'0$YC,jCcݸ@N{ouY:"ݞunj&ddpSkHr#1˼Xg޽ʈ l  r ZE:.g8 KwyWZ _eB,_s|M3s{cxun*T;7+t`U s\nzYfSqA})HT-[ 48䌮Cy{h?w$thph[, ӊ|aet%I ͌6t~\<\"RR_S&JH(tcpd{g|(dEeZ d#YE4ǞNe;D ֣ɸEdq2;[:UVv줁~1\F;v߹n*gZ€,v&dZ4ՂQDs֑JuZ_XUIof Y-{*$Z r&I+ʎښOiXVNAoݠE&%|qҺ}qI}L-yG&J|u/"^Rh&*ģLV+Xr_*qdڣj5y1cҜiXy24Zv#nfzn\BG#d?p!Kedk'ҝ~tW 9 seNz javBk_K9upc6 fxIID-m:H ?A]Vh1:gu ,=h++A٭@%\8k;Ѽ{ω~Ř( XPi]ZwNATcR| <ҏ5ZG`"o@ ;>Ä[. `˥RrU?xb/}+>W{춟$.dr~z]';LtM{`f^2cE%OIoOQ: CV|0 }.d9v/ߦTUyķ`K~ h:o8 jG7EG/!tũ4^N1g; ^ ɋyr̚h`vȏp<ݘ-)OxPaޕ7 x"]aRE()U|~A^:%L^/@" ԎR; i5)IlM]Pu|Kyq [Mq\Ov%^z#v6$ wbB: t|~:]UmC?Oq~j  Z]Ȯ>h9]H ʟp3DH?%:wL%9e u/o2Z/Ŷ4[(pˊk^.5i$ŝ7uo F2YUb4R曜l=bJn]5 k\jh1IiB9M1 ;QA4)};b͐9^;9\'m|gzng;oT`I"l_0 HmQU"7,c!"W w492ÿmaY2N/MQ1*? л I*PÒ>|E(D%ʻ9$ ލOHE#:U)mKb}̊\7gMb\ۨG?o;J'>%YuXQşFLuk)>7C5_R 1KiesYA|S à-'0_%Hj f{:!65ZEEu}qJ3=,bg,g?sirĈŨp'\dG`y5 Eܡ#gHݽx b[ dCrPǢC3e nBH}KM@F-lr- FӘ!| DUĩ3ֺ4;.}m|˚U^яͣAykհuᒙ?Fc%`Ug*wT;~oۙ'оs6O˷{% 85^[B1mbZw \ȴ3IN9Gtl|[#qw|B5":p2:@L>k;KQl΄)S'*"+V@+' Ӟ|^+ nD~FC`^ iP1POpgw lG%'fs!Q&Ã2xZ“u&왫r [^ER>d_pbNt/[Lje7d._>U3f?!wzY/݃vlb/rx?mTBRR5hw C>rZ ~o;Yjwt"^t#bBB#֞6+; _sꌾB%eK:eZB:O,IZb'Gqլ̎@MOlxSvtr ͇q\̉4dLVTߨ:[]}j1oH@W.@43`}:JζN$"w' ɻ?׿{ a da1> X ,,imzy^+qN3ww<a'{|ߜa>hfH::N+Fq73pj_9ؼߎKePȞfW c+'JѣSk 9Etj/=\hI w@ `N=ғ'=ޝd>x=pAL+e73@aMM(UlzYylpL#ί>Ϳ%iBA~ ;-!:(濓w" \AEc,nǏa9d|~%rp-vI*Mb>1jo^CʋW\O+xz0" %'ь8z* &9ǽ@2Xl^4ᓹ,Z.]>-d[Ϯuޢ]Z`oCvk(zwndwHfTX&i;)Sf-W%E3 bsb+,oB ʜ\1oi[@Ige5=jrk\? <ܥ SzՅA%I*ɱt4;_D5<綌=}ha_ ^׻e ޼GR6D >&}MG6[4fjy[" No(u xV< C}/?ϴ=.-t(whK(!qS' H!o"_K~ջ쨂p?z6Mj1iducmi# Ȣٺw=`j-yNj'cWR" q}/eOj{ePcRxxd.d~,xYpc%rMS5 4 DzyJ7hW{o˜]U^OaJ\<LYa@^S&Km|W?0ee@ 1aC]ms/V7{f]%&9ŨY͐J1dwQ~F6+wlJUS#e+V,)0H/Ahߖx\XIW TtfhT&l^BIͥ=| 1nX*wjC$_sT&~yXnGӞkBHJɪ O~"[ ˭$5\?`؂COޓ2=op|Q\5ZVV/w/vH1k(s)h!ПhP.ȯ}ekWt𳥄{JSԋ$3q5ѳWkڙ1͡%G4anu ;DIn/TFd7_aw?YlTy70yYKm [ zh~Z8+xŗ&]2>wHr/ӼMN=K YԱnbRP99PMJ;vRnWeMbc{W,7:2M %w>w׎ӾB䏀GJf+ mѺ{.Ļs OE)bMǀd 'UnYqc[]yCT 2#i-rF*@ O5+|>ɫ} (n&ABF5dU}4QM仓F15tPvpYѱxۧ:a`~LR&qOhQc_Fuyf@NNu=Y3a"f"G v||av$cS&vBvoO"/:p.lPH@5n* D2K˗5t,}1S Zyg==|^1C}H_+"GiH7ZZ ?&O1yL^[zwyrPI  ta_|~EWQ\g'#!r6Um[ֹ^c;K[oZ'FSDyީx v |u@&I50 ,U?7ڇEleSC$?I0j|%Z[Kz{Ǟug#6eи9  ā&RpzaqTT!Nvּ)I!Q< ۏ" v^(եt|%iPmT_i{k3ğgZU$٭2*v`;bu?@U4բ*Em!؟ 2+WNQ6C6:$eiD#zrs)Vc9?w9?Pl-h=hӛkeLg7)X &֦XZ u) VFyfb&ȊKiX;HTJ9iJ`2!ק\wWބ&3lֱ➼>CkJ^v{ZD?BFӏKl S( no-r{ Lx2(]Lӷ+YYI׾ f'rjGJW- ȩJx??2sIPc%ۤXF1$ )rs)Sc> Gb|lzYXlU{7B CyjpE[lz%4eiߵU+JRl2zS705?'2y$LQm9r 9`BLC)or{eg<}1K+IIZpL*J($(>BBx}5N$%U%%Tͭl(%xbb9j:$ {$#,v$3|uj {[8h"E ZuF^U/]<2:CYj^oC\ɟ>Tybsq1٩Bc⌗ŸJMo@oze ;^]eIhRYA aR-ܝ2/ a4H`!Pg2NȟeY~ẳA Uf'ivf4٤2֞Ol|!f9毾{u|7m-1?Yk.}{w~? rx×_*m# NC2bۡ$;yDUs+nk:LycǴ4o=F`8cMW j6 nP;pDI<Ǜ T5]I~ MKre*lJ.CMϛc؅vN]FQN1QeS"Qxّ˱;d[5d.};T񒦩Pe:. l':1㥹 Qq ,r뇸 +lDXFM*} J7-8¿-B9.Uw/RҮ&gbgGXT&6pJg-x &.bA(|JXVNʂ[ۏJĦkZ 4>7vL.trhW,|!W.ӟ=Ŏ.o2A1u,\kRHBPNcLKk4U($}N7j~՞aSUc 5-*+Jj >o[~x>l?9q[MRe2a8JlMOHxn[iw+D1HNMDv1b̸|S̗6Wa*-*]Tӌ I5*HvIA$%c2˗HP^vDz@.Dƒ_RSCOd[yןMO)$|zڦ?!4.uhTq}h' nuApRlLz.VF"Dv1P\5ySkڀsW4ҹVW1 P#"CܗѨaVxQ=A7">ts?qHVyf^WM j>L1gomDn{G|9]OsK#6fķ E&w+p=;nr_ڌRU3 {]x5ڴiʛM%_sF=Lr?T3(\&'poovNT=D5\Evj]1鄂t̪oQӇGQ솿(]Ts39[֎ԺKNsUY"k=F:LVٌF> ^?Y):fWM9D7#6O뤳l b?ĉy꼪 9%έfæ0[-k<-&Z`I2Ip9;soGpf:ԂSX 6k{9ϪA8\ͧQ-.ݻH?>I|ZGIwkA/)"3W\5\NO;k$%\JH)#. avpB_ x6MV <-fDõmk2JOD7hڠFFH 3cvV5R;X X/^ ͅb zѢ;i].\(y e UFIŤ3! a!NosGG8<^2@rhj%P/Cfl4;An(kc  i'MW0P gfs4q}ٍƼxd8 J((N+ajy%2u(c; -=P%s<36w4YHrY2Jq1İZz,~Jъ*ܹ0*_ݱfö=[EfoEmBw`]Q䚎orEdC_>2AX%2; "}ch98Eo'E9%Ư@;@&Qu(!\BM]GDL"tUpT9YF XD$o_ncD (FBymiBqg< gܫ>|'^1IQvFz.}V츶}>Jl#B~ НXS:^OoC$7!wʉeMݤp#B气z298# لl#MVc } DHAfFh8y5,DpgPXQno`%xd%n ֱ8:ǵQ]}*)}aգ\>v~`۠~ `?=Y!2fsm.8o@dG[CMȰz+ 8t;>c嵮/o|Ȏ Iو3->Ѹ!>q֑DQ[|Dj^8kMy%%u^_/kx>.E9%sK#5~pqNk~LKZǙpRx7vob 1+ QrK%"57crU3$Q=xЇ& U*m26_U{gD)\e9w]U%He%}Ɓ\/)"Y䉚<ŕfN=jp/h?akTF*Ef~6.UFJaEm爴gY%~56Rd-a~ e) gU)kK.e}_H|[Ag GA3Ka!NB7VakB>[[=%=z͹ tY{ --. C`[6AO۠)_!i/?utP{Z?#Ĥ>n|-»&wC_Vү7 mJ^˗2-pTlHR1QCmlHKs29E&9M$[UtSdݧ ?iH-?ԴHicfwxIAXxaHH)(*sC㺘Wsţr˨TiSabş†u'muŎ) .Q1F$7>qNk1X֧S-A:E̐ΫIM+]s>ϩ ƈ S r/1ð*[nJ5nj{t"1oh:Jr/ɋA%t(ʘjmJ„faR =_2@1i;̂#oE~&94 _aV¦5usfv{" 6`J0f* Y<XaI.l |? Z v-y`)jV)DY4Ұ8LjCI~Eٛ33 x AT/FOQ2ۅhٗS7ejrIw-"Q+']X-`KHkAOB+IrHLJ+=bu!q2,i% Cx2BqXyh`39ꭽE5?wR8IF*ㇱxg6Q$';?E}* AcUt}zfHc=O؂oROsw ~@plpYr,=iTeiXIt ~"kHe!#Qe+*^]B Խccvc|Ɂ1u9RR],ݱ8:@\28Fw"Y?C ^$>}ԙwdҪ׫t P>|ǐz&#MSTQwRGVBYdo+<ȾINl&~2jr9c3lP4ZzTj,'6)OFZFzyʽ0Fö;r,6a5mwE@Sf (V3cj tAx!L#\}!PW@$[?}.C n=sBѧ& ca鱕ǷPޏs8)FxG]Xg_m[ XXNWwjX]@ ALL.W2SHv䇮+M]9`ͻ]Pg|/׏@Xk0͙ Vl /D\G흂 x@Rcnt}wakg$q|uХfQ؊,9knϭIyN~z9fSBcWEeK!}.z*A^PiEP{GA1QoiHMN`jZ&,I.an]+mˆ=dCiS>,JӤ ;E p=Ė+7LJa9Z!{V2g9j"FoSx]T3OH?2N1{yh-<l`$IL ["E ԰6.\0e=_eU6z,h4022p90B힥3="~1>ҶL"}@e咱e1$s3i-'( }й.Ѳ,p/2wprlO[h7pJ4-#_J8ݥ>94iʀisˈD"kQ"[%% ,Gs٨A #%%mG i3G(#EЕ%~Y{xvf#l yIVGDWRmQ_5km\E&~&R$[?VQ,:B~VWi%NvcUCDm6,7K7_Ϊ= ,Պ0 %IPwYESQ.K !+xNuf uuųN@0.4]qBi5哗ko%/z'l'Idٺf^HB'٭ ۖvmR8lN6Ӫו{8AScfD0LϏ3LAl.[&por^KE%w.9qОaL8Le)lPf{]!yW%ߡu8ɮ1NwhLSпZAM˒v gT>]Qs +2ߘڴf kd1.tw B~?䃇NMޭL!>NLUi6 @U#ԋ^v `EϙwM*AY6&/zWfՌY֩+BPɫ|>~i3{ USҨ?b8/g%c.Pۃ2 W{mcBC1RsP |FQJKҙ-C\՚oE;}i-sRx¿OלoӮݠ]wQL"l&;co|^KG㨯F5Vf79fLNUh~(s"X欚 |u~vxNͽdxO^ I08CV"b`Mulu|6=L6]KC^"U5hiՏ;}坓fUZow;4JWy ~c=+ TC8QFNy2"3%O|:S. S$6_X5,+'u;}$tz!ҞW{CWB*؇/?V«ӑ[aa@b"Fz 4J)g5=D% +hU!ln˲?~#@\6_oQ&_KaJY~u'R7#'np6K>g|רѹs 0 `iۘ"tM6R3^QR0*!7]#B@%-v锾%A*T]F|8Rxm;wwzi!l [th7\BFufN96GH$C~T;'m;4,c}h,OG-o+h"}.I-#NĞ_ ] R |Y5Z [33b5mAv($<*-kL^BؙV$tۡq*EFIG2%d=bp|tI 7.‰? RXF@@epzg eL mu[8ϟ%.GCNgj&z?OSD'œްyR(^\jP #n|.@]&5j+"Ob;38a}Os8O4o򩔑H2ΤAG4֜bJyYo6H N/oo+suBACg#\K;\e ;=($wU NC/[  ofL~zr#Mca*N>[Ѓ eZ9+ĎtQ›u}/nكIoZ'ovpU @-gDipzĝ||W&hSsL:7m 7:ώP!m#F2SPE' b]@a*{3*YJBYȖ\)-GewC.\K?ˬKE Ed\=PTz; y7Af^7Y| ޝQ<) 1I<]%^B9ɔ N)Bl z'䟕#d&Y 4L'G7!#}HoFR:-/jZ{ zt-=sŚqMG1BXzn}Br;7uz|z/TNO` l\0k_Ffyxr_S&_;a-{ uloxu`yXa>Tر ^EC4I.;lJ98( IQŊi*G"^m_PiË+_ބ+)lS c q.őİ[cCt .21P*ӅvD#m?-xl/8TRt0Ɖ@wWJakALS~r!h4;]7 ;@=Zia Т深KgHêY,a!M?Fspm~J, Ӛ{sRa{qǺ3zw='}(+a=)':._Ibqjl~7|$%4W%)|W>S!x[} نn&KV,.F3[߆0sH/+# '廨GE0JDXǖ<}/Ĥ*#gvѰ1d>biV((HSS;z9+VOV]LW"gpB_3p~6@[;AQt(icak!f\MUF T?Zy)"9#-XiZ~ 2$<|x`Jqج%c^r{ Qz.|៝\e*gP#ĕgwwc6WeƘdӄYa=kg]q~i#5r[8KMvGM\@u_EzGrH:d Y:GP%*IѬ@RRSA\=~n/caCOrAey,3b6̅xΒB*-ZBҏ\5$Eldl:MpVqeD")x`aTh  #BV@oo+- - ? !7 utlf|o+}d9T}`(|\(4v:Zg0zߢ`m}ɢIP%~0,b^Epmu`\{5e*{vv }vǬ,j%_<6\ 4-٧6ʑ%|q̚s(Bj`_q-rҲ DأX ȋlf[S[fDc3݉۠ [{ݲJtS틠RG7ڐ^&)%!;]Oni8}+sQ ?)(#%zI^zpB{ M@c̔'$$4`kKҳ! _܋ wQ[$"^a~ji;wAO.r=Eэ3N-SVk@~Q]eJ л}+>Q{d$#COT?Ӻ[Xɿyb;kex7z|@[3XtA'rܵ_8t|+n&*RAh,'-P?o EJIH p~ݦ릒"Ex~B=&lC{ztZC`XP1Q5o2< UY;X{A%;;pFA7< =$';#Kbp:\"2J(9a< ^p[@Jy8ƿr_4bTtbLŨZN`o315Mk^:; 7Α[t.IJѩNa4wa~S;uhFv,0rۏJQd ?J܃Ј2@//Ԓ$7~~AP1IUzSmLy&~/둎ʇdGcf Þ,8.B ޖ"׆&fD9DU YYOb&|cwM,IǝtZ-*W|>_!m/iak%:ʿ:>E&|"x~w~??X#[M\gy_WUCT@]?ܣ,R~ k|D6_!gƏ+s<ʄ#QWYG ?Pf^fO2Yޔ!Ke sߐU?G~|OШ-4c /,6}qaZ瀳rC#/B|${ϱ *8Yuj7鄭 m+ܳ Tߢ6)(sXbLs ݳQ}l.EzFDMDlIh,>RvTȮBQMll$c5z&N"y9ܓmuR~DŽ'Fw?^:T&p͚]8 Мj,s^y3sFr{xK?k6cto8>0oDʓsĬ~r kb{.C:N88 G5ׂ{r>uK$6MṇCR*[p0)zy)ˇ#z>#GzoZ׫V(k'[{ϼ/4"/,=vyB"[QVg6?΄VbDzuG>cr>I[_X)U 6fxs X{"L$?hL+/dK>bw $/d@KM 7 $? $ߋL)I [sd]xmϳA{z S?C?<ڷsGaCUp|!2MЊۣHcpfT,LZC5[gG2 G$iv\؀>H/w.2-B#4g9r H@hmI)ܷ$ߵ:/qD0ZF1w|ǕΞ{'ְFf׌եpTz˼:tztQP} H@BUqQm"ZN:)lpv.h`f~Q^ʋu)\n!h]Hea"%+ N+ocT7t޸z6wT:u8VtexN4wi„Uֈalի Mb.2OV J4bӚW⩿Js0qĐKT}q#(Jgc<⡡}B%uO|T[$> w{jOS4B E^{_xRв4SJ\WO@y3яaQdZ{{&Q ^4=K}tPV.XhRVW,}SN4.SD"VE,n֙pyb;Y?qHG7쁷6csrppM]`9yx)*b'16t` ݼqhqMA[7|M[B=$Vo[ka*hbW &rc[3ea뱑 <ֽF9}7a1_ǵ k~!U E ߅ as;. Kǫamo_)篛G4Ob$Onm~C!Eֽq\߸@v)~^ÑR[ә0 %oDW+]b{rvK¾|(*NX`7. i|qf(m qX8,  ﶿ`1<(DR%? 5 &MW>+N=|\x6:[|U:eP Pgc!;XH6?--'}S,47 X +CAc$W45-;#,|݃ӅM׫IR3J}ۼe?\{6℻y#R?O!$4ΛL5Юt>'Ӭ#AqČCT/,׏ZoA_k;FL .9'\K0ryޯcr$,Is2e"R;T>e#ڜhy[0SsUˀ\RWaL7 MƎ(o)hN.=ΔDy+L% %PhoH"Moj"JuTVHv̦wXXtZ8SӛyT=!DsR"$٤ۓ+$ p& >NZT.% (9ξuﲜ x8S] bz(H({%N3&g/Y۬o{w׫9.si| khca +HŸ%}|]]•.vAq*|=ܘ7~XϪDO:Z?U+,mL=dCYdo9%ŀKO?A#Euxl'_Qթ\qKu#Ḋp B_*/U-2>M99meI952.QT,+b8^5rY^T9!5[E|L[ ICbk1rϱ^Gkaďv_!ZXa8 CZհ%'8*]=&z;X37 Ăg4nۤF H:]jxS%VՆ[+4Vk?bAdD=MT#*hmefID{Ge?ķ;㒹-طbij# vEyߴ$~뎉tPoU1N1p5L|ruj B#Y)Mł#Rc bAojh~1k$xgiچ׃QV${s) /*0@Pl v5חX[b T7ƶe訍teGt4v:)(lH*{:Q e] T X_ $X*s>E:>s=&0t | 잷So9cUg%Q`XʜΡ >bЅ^ ?pbP^9VcoĻ }z3kCgP2ubxkߘ52q.d.wRJtI,9ff)KVftB+ʀQyBUHQ>pO{5atX_QrQ?r fv_ځ L@ Ju=]J]ANΫlRɒQB/BOxշh+l/Na{iX`m.h kfuo’_E:$qwzdNm.iF]P֋T\CU~%J *v 8Xʱv7YϰO+f}ƁyP:yD&19&q4E{XRG鈆iDXedB|uHvc!JLOt8)%{ݵN xךX(!a`QJah|\kK~6c! V,p<@#Dspc#= "#!Y"IJ9ᠸA)9 : ?~@?Aϯ2FJEFMfpa4ϝ9`\V>[Zg%V%Jix@  t FGうmn`2OЂ%ء:*];?"M`/$:݊t~߹&%_u楙~Ƅ6{ߣĒM" kԃ7 Ee~]~kaD+p[lU1G?5P π|zKS^g"0N3'""ߣg8%R܀_'2g΋/s.$AyUmRʾ%2~WR/h?Q.`@e`q`% HFh 뉧D;FflW  7I[&I,J#|]1MQ4Y;;Ch~szz[136X);6O!&36vS BjuF0da6zc=sr ³Ξ5*, vtX]Ԕ@am? vzi4`[ϵn Ɛy0>"7s¸o' B ZǯBțXrPE[Q:;N 4ihWQ|d\>}#N D/ ~ah|[Ej{rS DϢC gb̂N$eha@lcy;X ;J!sF^%R?/\La_ 5cV |; gxpFHUMB8_eߜтV=cke1ymʀ3;w  aݵ y$Ϗ%o9}@M'{hgԭfk_7[yPTY*E`G"AdÕ]"6<ƮW_06ņ+J eĭQӇn '\ x,($Nv%p&jssFT>ul  9  Q݂l#]˖)^//´!  .Bv$Ny>:Mj*x~Rir]w9tZKP1Z(Oovpon"| OCFfnTZc)#ӼM(^]P8N\VfÈL?Z-K=ѳ*n =ENZhs0VqBL*IDP|NCDNpEKx]CڏY+Lt (Q`ܡ1Aq7oEl6o%6})dw1(8txS9I2{ީK¯4:f+jGaZue }3 ?UElr"x=kHSF| _θ,*`ފˏRP!zx,xi=nb_鹔 k4XC[ Qy? `ۚB_ Pw-Sz1&U=:)ڜhl <  $wJ=c҂XFO^ân23a?KD΍ՊH^`y-O !yTe4 yb4gwPtr"Ѯ0Ȯm~񨫬  xVX s-'U֔94#_)nPiL,?I2˸X~iUvmI4&cbʛ4oSrBOA^f#^vgfb8AȴBy|COoVq\[%B· %?F&sr902"_^"AH9ݖV`;R"}I"{vZ\A܀&~sT^;]bC܎m74ȦzA Z&G+^+Rop}^g^Ơp`ðBUkoIRbǧc_ JmZ[JsnP2$mN_иcrbgy8Js_]"U/.BܨkC& cO*Bi4|Jem >aA&uwxg };1>^(0vGl_[9l`yToBV[W{Yz8äWLҁ| 4KWy[>ҳkVj=**q[ܜNCPuSNZ1͸L"$}fS{({)vU]|D#<*?_6 մ 8DM!UdꫳYT/P5#+BF[y1zB0G[Lf>'S]j>Rn1"H `#;9!8\ԗ5aJ/Fde [sґ4[JR9a'IG(T?7>A?Q Vwx4W4|)u]!5DH"Rn״蠄 p ې%R #prSE.uvRM h=EBK9Å_ԫTQSҗtqPs{Q= %Qy7 &ja#-CJf0J\nڢ8䘌#C&(&JjqĒ/>Jk.ZL[{^Ufg]1xok_@bxmOGɇb,p:]U;ي,BRca|eKՉ}C,DB ٖ\? $G7fDռ~Oi/R۳yC_̤,+bt-ck(Fi7$@=e ̋DA&$πv=zߔQG?`+Yل b)Vx:Zز@ G >N@A;bZ7 ^ 酠Θv{aEkHSnU<@egm_WϥKq'c/r:Mg;'`FdPi$ η( %HrJ}?XbШPq[[z&5brΉTbSX6.hC[ 2E^ ޗsA:SA1RcU"]#Vd֏b^eTy N+t)@$ pB@$T !*<E(ߊ \XZvcpZ( Ͻm(ӣD)o'˯އ(? 3[ԕ8>,x:1.t4q${GZMK)B"F ,P3;]z7F}NU+6z}md㭩O8Bb fdr'|pjȁeP>x]@3ޙ|>"0@1*~S\E k^KC/6CR {mV[ԧSS1U&*@,sRI̡DZWߘٰ bPXiNa=㨫tSLvV)1kݩyޟIƨ ۂvR'? $cCVӳ˫SQ{É e i Jcw6aLb)@_cD+gKy,(βP8.Cn~ 9 BT+pPcF?9+Om槦;0)3- ,6dɠ ز?ƔW3,=e; wp`st`&+`Gy$~b-'x@Aqr|4WEԞ.t\9}C&\?&-tj yY_ m*o1H:ҏ? dS%#= =ӕ/'ӏOv N{n)N[՟Յt{ Esbڿc*) `YXCKhV4rY}>瓄uqF]vq 4Z1@ Skxk ҳ»ڶOο!:D`:$a!u|ܜ ygtSüj0QsjVQ[>^_ӷkug@SzT0XY؂GumtO>}k {13i~ EC ׵t<?EfG@=)dm%{Sh/9p ,p!k{6Y7bNRvEN.wh687"OJ(4/4LʰwN$Q\̱90 {~q.R@3dLݺu.B%BdΠ:3DGͯ鷄U5N OxCR=߳diF xڣ0_к"vUVO҉0#ZNJ& %I2ui/gݒ*qHZxwFqepE jݎdR=3O9!a ͶQŎ?l>x?T_. D4g`{VqC.dr'tF < B|xg\fqG.HoyP}ONkw< "ɸ3~d%$ZE}6iد+TOlۮ{P-AzlzsZ @Bz*t$.#G5p#'תkꌊ<(kGWIb!M-V+bo?E#szwRR t:6f9HƠJ}~w0bƋ>ܪ̞^d4|Qk0o4Koԛ^f')Ktc}@S|, s^ޮa[C[iH}J<+%d0к~\Ug/\>N )\\ע& ڬ.®)f9x7:b4-w;1hkSܟG>NtBvA,?(@:uunm87^A 2QL/:'w#gjQC ~OPFgpO1HLwa0hs(giqJZs D`0IH :@@y4<̞Y|.I>&]9{nectzC<1ZeoJWQkEtCYXDmEVdq6lb&>&lFlvy}O g=ck..c؛Mb4O>F3|N `>Q 5+0 %oJ?(l Hx!yOfQel6ͼ3'@v(.ԉ22bz~JCku6J- EnqC8g##${})D?LWn²PKI`U߻?{z0cgf7.ӥyz0:BV_wcKtZ1v>#D!ɨtN?q&q%ȫeVH'X]?}0u9^8x 4Y[Ǫ֣^}[lJTU %+W-b&#xw*7j3OIAh vt2<#mcȱJ_cȡA RW 1K\>pI׫߹o0Y|X ";1vCk|JAy֕ꪽD_ݠIi# u#"#(W99[MSdʶO*6s& Zû*S GKKlBڤ?[oexk:˺iy !&'cQŋqpp;"p֕ fZOgO(:h2cr|U*sS|Yi16HI .^Ysdae?Sš/_- ww\u^3 JeX̒a7ȓU7y~E5 NMnCn}tb{Qk2a1kI]-Rkk#Ti?{gj˿ƌc2HC0`/IGoC+8 3ޝ#p&x+|C'}ˑCߨ;.O"x%Yg/+pcɃrvـ Pi|;J8 pQX@\f0puY6#:G*Г2 ҇ #wø?L>^Z\(_PACVAMP+-fLbG XAB01ᵋ<*ԜoE _dFi{h $ҽcSD?ُ,oFqC8ѡY=8K&ɧԱ 2*ZC׆vj77)}*򜜘;;*v]_"kv?%a@tW-h<ީ_ 5%"I lbSZn Փ9^@/rS4xU#:15&f8['~a_WWcr3Xe>RxiCL:/Qt.z 'ə^(|xoڬJCC֪ QIX-D5\O].Ĩ<Zcf6ջ]} u:o3QF~ L;WR7k T,ɿBc+q 44v9),: 8 cB&xP, Ѯ`hFDGxzD@eG"nX۞_ds? Xdu/4+sTay ήLn;n!@KIzyuLQӛPw(/Yvhs61th| ,~o픓?SԝARX!=JMzc#ƹ@ȣ@[ZD5NPr9C}>K( j ӇfYAՌp#`3/-OZa%!99HƦQ:' * Yc( aDeekV8ec=DNkl73)j>sXmu**KgomTHnYn`v ycW"^Q2C1C(N9Z'mz1g:LёtFQ4vR M 8:C 8bIxA5al)r.{s_; noB|6:xZ,W f'h_RޮV H(.)(MQPti w~s+a@l~r9 ӗ]zFE?yg t!+:v 3ƊPxz6v}a/~ya=¡wpOCR |fq}|@^bwhAPw7} ҉Հ#9|}48R2a5hȮN19SԒܱ iMR2WXrϣP^ 9Τ!=z{5;bx);t۴d裌pz~8`=jE9yV"q2 e?? % LrhwTN?gjQWN;i6~(t+G1wTLGu|iDs=eqד *f aAc,27c|% w ڵ*jB߲/sy/7D1Np)+za b.J( DVDBMb_m:;8yّ Ld;:É %@njC D|~^kwׄG+nD#Il:q.Fq7#-pwf> SW0 ~产WzJhN{nʼoN=$\lwԙ/?f|y¯ v-Aio]t΢n\@#:٠y9wD39F֛Ktɻ03Xi~nx"[F\[46WiO}72 U|jXYӨP7[~w@h 6A L3%f!ILLWKL);?TM::F{<YvgHfʼ8Y:0 Z=kCU^S3q i3;N^^=ޱr o88ּU_l]l+-fI3Rh=l !-ʅOJcXQ}y_ 7 bz{Q:$ 1q: A7c˽n.e!;;3bm4\y@/L띋+ʌTuυ^3q?@W)2xmN!ܹv#0| o0)B@VaM~o7Лl:Ѳ9qZ ;Vxk  |l~4T$L튳b,DSnӸC3#0r{q:p:7r濥@__a`fOo eW"e'&dރ|`5P%`x"; X6| ·J0Y#=nŸ/qqUS&!U6$<-Z#Ij~*[b6d^j3Dx>*l]1Zz_ee4+?T`"M+ Zć*rzo&){/s4hg|0w3qԻ`jWi8}ypU6rzB z1 `4$M{ʾ!zR /oJ*9ɯ}_+w ܵpLȔ 2?u\ IOŎ8KȠ 8]眾Τ~WV0cR/=@uA5n5% ҇mQ`~(-䫐͌24 (g5-l63KPro/}pY֜y_zЮLB7~vW q"hvUʊFn sIeS>$@Fؽ^TU_>]]J؍bg:l2zS+ό6H=AH5'3hzW-Key"/gBGs Ճ' Ea_dȝ|*tn' w? 읊m/@ykO̮J UZ&N_~Z6TpWۺQd qZ 8|vTXb"RUJ42#TΞmщ?Q{ҤxT ^RD?3ƺl!^š@z bDxcHɍ/WZߜrSEۜfOpgKf"A\s_Fqq5(}GGTjyM6rڕ{*̷"VJQ fн쯍ic*\,$<ZD"3T4ckMGC`Ն U.VVF\{&DOAPve@pI&(DZ!svcınVW!51 UVUQJKFy{%uXX Ô6bk:iH 77X7H&wݳ#yoDzJ(x#H;@ s 8t׍HSamhPr|sgL {(eD.1BĭXfYP0?5P,*M'ذx9󦰐=/E`GYtFX5 ɫPw: 拜f¹"QcgMsb}0f0ϊ4KZb}Qj$I x^N5hIb>ɟ%h"Y& |H1+`^-cEw^5qk8M S #y7on ȯ< PPފ$ii[2{' LQOmlL+FB܂kTw[Lp}J-:e|~o@(L02}ت!/dm?I<8 EӞ*D7:bY8WXKaI^<|q7A}[?H /Ta;qpF'Mc׳@3F{"Nϩ?rGZ2'O899/ ZGzY?ܻ-2ǩ*֮!LHFYIr#3:ۘeiYGj^#L`W׵[tuK _s,Iaxv2\h_hX3hΓ3}䎠qZ;9>@K@ p(9C(SIYedJHr/&gj}aaq["8a ޅ;ks#{ZJ_ݾu_賳s'fRʰK*RN<~I{ iJﭜ7?r10)n?M">VB']⡐k袦#B'`X3z1:e7q.^B(lpлw ݱ>7α.ȣ3F~6?Qǖ~y'CH)pc])Fnf]Οoޛ|c*[nASF|ox5m:^OnWi\櫿T_'Qn"f@Q*fxЮ (C 2* Y07d4˝k3z1 ^'inj\aT2zev8 rIu&׷F:S?=r,vt+IkjD's&G&pk$:~r7 K+u?~6Q][撜R7f!KԵYcpICβmVsOu)sn!է@hsj(+⹞o.Y+8Dmgp!ke*MVrںV_οoY `Cb^W>,9LאzTVss" B4xׄ l=$ K9㝵%OԺO9ri`Kh€L?nGӓkDyζKL}?Ԣzd֏ALbe%|>3ai;kZEĘK*?;kض7yHz,p}az fإX3iu5 `XXtt(l/ pDܥauеy4vǘVW|3εg),v -qāGQ֩ Sގ!,6/U+?-> `I3eaP)`#]qR@ iF 1Anr-c>ݧ}t~'`Gy?#;H?C*gNt.yȷ~+l!<`ԻyD)WJ^_FLw_ RLZ;ǧ:m50N!$}>я=ܸ'Fb]}F!y?$2>nGj\uߜ*n(7U ^]aكGt֌O f" ; h3q(w=E;jU#t<3V~"Ca.;/!7󒗟^ìʜǛ<3^ zNENn c&@JrȡCϮ \~ B=,"5rmR|f_T ݭG (IwRRbK teH mSG"CutPbρ9S7yxEì>Io`)_)ݸ ޅ);2 W &bxpkEx6dQ<䆔%$衯Cyς; m|lm#hY=!{WOpXh Pٚ}VwzPJK^uQ "9%w_| MCIM &YwIog{?I 4?TJ"p"j_A础A  P "<<4_RY߆ovG@T* pfvUN䠝gO obfѴmĻ6e 4uQJ}6f=ecl2Qq1go%O%V0,?"xt[aT}hFwkS)y~\q.ok//"b@_`*gm/]ΰ8iku2 = VCzˈ]ϓB_ӇHbY\BhV)(Q—B|骏[qtT?bs݃? laQJ?_ȸyoJONsWwxOonО u@L>m\<A|%~ߏHP^:fNQ(+34^tey1(3fj xS1˩ 1#`|})1Q/7$MYF?OPxsI>c1%|6J%,C Ro}~bWj<6yоnI&!Zf[dԓ;XG+΃CnY5%bkP/#zb_nyhEBOg.!ٽfF̥c fϿkFzmTƗ񣾕 r=hW6w^NSRjl/PyV'&'Jy}/}f旼%8A|w`W`lNj.lJ?ޠXr?<ܴ1#^zp[(,T&j5o8Qp ơdFbx&U˜l=6L;P_/^Z}=ΔA1@$Hn'ML. {[g$SLRGnl{ f*2Zˈq' ;dJޘ=_$Labj wRP=PŠN^J|QΚ3rm+D˧/TMKĪր|/ aӖKLgqҾ\ƆYoffG$_nߧ:J$yPww+º嚋GKӷE'TWNt"~UF:dԋGS=}ArmtC=e;.H$o>'#&4uR5O*aW*<>(WlEe8^Ӱ6G cC%%_Vc <ny:C΅ -zɡvﯨ^o2x@ (wl>(¯R|H΁ 8w%ߐF_O'm?,IvI$mnΛΡ7+OxwsEf Tv.,$Ϫg>:6SBs~n` tkix I;ߓ΢ J`>-/ BB]X&D%bscJSF^vmLJ}  ï{fQaC7sє@ڐGi7yFQs45}j{r 1j&rVþ7Jϸ&|.{g/Z['A}W̑H}m1ػ njuM+٢0w9q>¯ hPx)eև8Df^]Q!DURʉmh/)߽Ǯ 3zdX@qKonK?*G2g/dVg1,ssm\ؙwD{ \rS)!.['?|N?O/]ncH%W LW}mqnph-f47`hcR4ܓ`a&j#1Nzp4־yvu3&pve ʝ‘%q>4 gVY5*-}gJ6h6k9" FY.|CHfU6u(Wbf5[iW*5'p֡$:9~Ѭ2gݷn~:7~)/Va`Wv/7o&>''lFɮӉGK6vp#@u ;om$AWY"7^tD^(9kQW8( qˬq Gj\d4b x,/k 3Pg?laV/bf^I̞ $5kEJY M%è*[p]gL(ٕm,d Xa{A0d{ Vz `  n*QCZ pB摟h ,!& Rpyog!Y) VTlU9¿?jx oeluX&\}/EnPiҋ*g|/8] b \Co¢EYXUQ ^NJ?}_#"k&=? (ŐdW,̩NJRm-s9$7&6uP]#7pn=jpe;=)~6|]7bH"$._=c HXMspCש_X? F7浢Ym{K3aۍE#֋?"d]i{ `¶gA߽zဓ9& sVEKiqBOo~ `B38hJU̷O-5Kja#Ί$Zf#B iBhwbLy )X" ReJ&W s"U5a,/ y+t|w$, $暽fF_/ OB&|x77LqG3C1*ի$B_酨ФfO򣟘Z9 eW\sZ$ꏗ䁲|z1c!89ƫ ->8<8fBۄ9qȅ7Mov_{|ۘޒ'oe\Xcެ)u/gDwOgjgF0~q Ft%DB}bN;ӴXp,y@Ce_;CP..Smu@ko _oϱ16Ị$~  G \~UܤMHo /om3rL1I$To7-/b1<{G n|4ix%"Q*v%2h=ӮG mհ䀘ǝr}&o~»E7ʑ+ lC2"vylZ~jSw !yVǦ[7N3'Z6|>=ɝِ;<\tںFJay.q(˕gn5e"JV%/d+r2tahT6aw6MT 8͹b)'&RyE0`qٯ K̭T0*ԌEQ>~6b;N?6/7j,C[%W7 KƧ6, ?yRL HyC[oVe7guK~>[~0všS0 7VH;x%syէcg~4eަAb܊b|MB*7FCΟ"ƈv脯;Z=]kX'9ޫU^|rCY93G*{fgה;|k'H AȅPiş'~%08]މSsXǢT3Rn`F0|&-S%sly\cCዼ{VIQմ{Scq;)@'xϥ#Ґxipr\L"}mjj6?|)ItɒF)N; -HU3TPĨHĸ/v[e5zG{%4g]/ω5lo ~B?g |AڪENLτxLIԣ.<ҚTVR F[tˡtqJ3gv !癘_M@jzΙcm~Z#ODm˳H0>ջ(K-SwqWB};x֗Scٴ-Jtw4G%NuUč~*ݟF;AwGꚲt>K*S,/RՒ_ؖ2{'/&Cym̻gJt.U>: xNX3l; X@$O\f)hbӥw ND0A/czP&L7ԷzPeEO,W|wbj'XX7@yңe0gTz_͈ay gM%\9RFwmӶ rz{;2,PxbV>gɪpl'7 jdLFtr}-c_>PF|Rѷ&$ 9DM911tmSX)~3ݿΘ2ĠP`YA6=g:/3Ɉ= :rG̬za_4xBsn%Ѳ <;SJM5[UeY uĒn ǒJ_yX؏#!͵:('5Ri&)2o`O܀Hy :ee1J).P`,=ktt7xSexQhݍrͰ}ew(ey\]~4y00MoryjY4TAH#MM5vnHEBVD3 TrF*PY&"\Iޮh %y1컽\8{^O %FREO߼%ʙM'W!*>&c_/=vjnw!勌+0_4.]H4}ҾQך}6Hs$Fʯ~8Xr(l-,?g;3omXh+o@Wbt@- vuMTgaC?zٵ,Pʊ1/*+ʣ|?ºOxFqLx:. IFЭIJ-2*y1fm@{nqku߇ 3$,j]]`tk-׸8|h(vu@aPB!P f3 +U[xvҳCEZ@a_%5tN(cR k%mm S!kTJ;ߣ5~2~!oԥ%!/&KKT,Ɗe7im#?imCHpІ3ٍ. a7Jf|vJtQD*&|Ln6j0Nb2^ ma 4|ҶzJ(AXD" "q'+"?i`e8 PiGXݿH) ^%~X)rR& Af-aLo1ofO熱KBǐn' %T +[`~*<h2gly6ౙv3HC ?f\/dp'¸>?2ydEAW3!|T|'jrs(b0_Fqnr.(^ڠ}M.sGV1ףOφRΞM\9ieBfyxygԯǍ]|5O}ewTIhs^Q&fނ)!նE0rU0'Y(^{!b{e$oCmK}g`)mWfQT5f̈;_4Ʋb6[=|ڹKp2t]Ƌ'q 'DHrт兜jp'> Zj.t^Z^."ڴ xk#"RpK763!v[b7 򵦗(뒳-Y@-F?ސ'9?r31l͍qЉ7| xb+-d}6W\Azy~~(*"!ɖc"Iyaa*WG_1|od> b"$W 7."?SA[;cUɪ N#/x^tՓb%z~?aݼޜ2]"`'Nsb$ FiG.L{qTwk<`2;A٦=ʟ$B>zl݄Z7fBou&}M714%PW 4:erk'.,mfyY9U7PW:X3/?@;,oݭ:[8C"E^.Pm s_ pP.Y4*0z_^ VY9[~6au.I?eDPw rf:ko?3{6G's 7r\RchmMˎI0gH!5<W &ގhbD It'; wg&p໴kP$-Rj?x dZ-Nҍv?/Y]ό:jf;nؾbɆi6#MJ Pm,)y9iWy#^?IFA Mvfz,1(< 2r(r>]֯=Ho[jyp]䈱uţƍi}ଟ<0oqGSٕWusz fp   P=aN,s)o,n潷^>.%M@:^OZZވڬjsr̥"|aOw<)TE5TZA*7m^' h?EYlx8éO?yQLj gG)eE^D?E4uq+{;q9:5H4u][DDsknI )b" 7 Q7gH42;bDB䬶o5Ť]P*^$9X0Ktp ᐸ{|zFEű|ya Qh'_2Ih;M Դ{y|n+Znz,pL)'pC6f$ &i'3܋Lg)Oy!$~e2xxh5{lAQAG>Z&J;o޵ƻZ46Lsgeޠ:&7RWQ°cZ1l u BySMA6\vk$*ľL3V}I22Y6E /] 5^]ZC ;|8`A}H%o?%D@= ϙY>hdגk[?/ʍA>A/j3؅(iWHe"Lu¤BEѠb&~} d rT ȓIO,7';ˢҨj9j)T6QUq@v&C};>IK,I%~WS]WJ@hExv@&YHb)b68@Չ|E}ܢ4'%(0-kޕ(쒶[I+UUE8h[`Ϛ"]#^0hK2&h[+Ž(qP=t=}p2ԮtSѡ}(^AZ`Sf/7|ls*C&~71r &@D0+a skEK)*Fo[B8oBȻUIJ֝j(\竺GtzGg\ 7>yH>aRKMh1'&]A)eRsLapgWhum%5ߋi*=lc8g.W 3ձ}'O  ssi0-xOlsشX5bhm^Wϳ.[ $[hk2[\CRM:ގ)$ȆVdSZ_JN΢hU+2H++ggsl '*!{5Las$Vi7|OF$nd:U Bq~jضl qɾOaVVcw-$M0IFM'/gU57gtvYFmtN,c 4~מz@P@;5ncPXS/>s:OWP'dD'i^ѯ0Z|6-6!P7r6ӹWCXC8Wnk3?6;^.-B:u|^%S}-'E/>iH4$?,9 ފz͛S뱃uG2G1𛏼ɡ}AN9MNAU vTl4x4Jlo2g$r7%(zvznmbI̔wʾ!ʽo@ûWTdA~lV]3j>_-c3شnH7&|zVYe!9vH>oحXn͝$E;{Gx5z,!2]qȡƟxmy_k OH~!T*S [:* Ezb篣_"Ibr4l @R1T0m"+0@S0Hd9 8k1PO`i3V .vs˟&AVq&@r$" Ab/ ̰I0\Y1DLHg]'']q"  1H ( &yyyvyx%I:0घc|ْr%A¶V7Ba{kۨCrvq|xqڇa0`==ڃn:Db_{XvE! ,Ӳ `y&0:?Ww7`s>98@b`\.lab0{!s f6x kriafzٝWet l^`vږH'ɥIm"x韶Eqmbqqavvaqql^]`c'gG 㳓1(yt]ksjj&u."6(F'ʣ~ќ.Q  Gd"' rI8m `{%TQװ9HB]f,`cjs u  "f(;[9kX> 0rV;k0;,\6@(gecc-:?p^^PZUN`f`0 **@ c0,lX۰m",6p0_6@(dlb8/)@*O+s\mX7) aaAsȎb1m1ׇ7PK Q,3 `s_=~  TX *G % E3hf%YÙ 0D9ؘ8D@(K򰱲H>N@0a@QI8gVOa/aasz@<2a[ 0yZ=ru3QEB uNL*Z5 9U0570]6: :S+ih*ASSAi F 58Eu5נ)C(@LMj5*PXd@LC 6R/$kXH&b mLBJ '&m$XEbÉb*F49L G6P5pDX@B] @/eM'HADCb!Na"#!0/쭅` 'R[SDwTt\#`ڲWTԾȢX)׉򑮦*RF隼{ۉs'`Y>y鎈 :@] UW(TCjޢJsntڭ\ɬ!qsGn c>Xnb|nZ>+^-=xQfT3Tei+/"k9 NL2bi-[2s}L,mjG c']Zv-/~3 6jic\ōw9} ido]gpIjUwe:(Dk2gc^aYQ!Ԥ3)6mBAT*n3#IW`}F6?/By,z W_Tjou6ߜg}l"bv#B5NuĻ]ŷO32?{/^uF8<ˏRpVF{XXui"\}ѽ=t$]LپSLOе%gb\?^ }6Xg55+';]f@9b41˞7ǫ5iX'; ի+d2%m%%7ޔF$ֆZ$|SNbQ@@;!ׯ+wRfS(l~c|#{GGG/Y|8`j#4AhRwUۆ܆+^ Kuiv1,iRom׋׳c7uG~.6{dtC㦶o@ K&KK~vgp$$}~G:qquVh'E%e{6wWf2.T_UyjYmVWW|67 @$3/&2I4ȶ dj'R׵666||<2wBF㴾pe}|y~_BB PK@6PRk1C<=qNGҺ_ʦCg#)]"$ %8C(fUN$x[Tb `:4\Ӱug+-M4sSPΟT:k@%K!cyT,ZGTk65s\P+BF w19;%Fae*w&]|krͯ Gwxng<~_8Y BQ# CS+?/IV0wQ,Pt yݕx ?f~>ތW^Z}cmи3xf F@^ ,[=\2r.516Ky[BTt EU36oC2wjD\4BT"b]˙mp9lհ^imWߴnVFĢ7wS<{8uǏՖ яt\X@ñ kȔAW FcΒ+ tk$:.*՛Qγ6zcX ړAEbן&r$re%rrp~; <{u]aAzq9~gP8L~֞j{E9?&C2Y]ͱԢM&UO`=i+VW b?=5B-t;|Aɳ>zJx6tZ-p$|j{jBc fb5r$ނ=P6l9[ٚg>1feX0$Pxʊ& 7Pld&+*Eσ9#ߠ1EρUqvQF"_#㍙@;^ &.__==Z#}6xҧ-4h6ZĬ~)o*8qUJ!2إcǶ0If^٥gnm'j2\yTyke^˜D*3%_'[Im[h)}>vNw ד',\ɛĭoҿ0hɷTq1z]Ԩkb ϖEK H*",6)|dqk^$!$T寙zZlb~.ҹF&Fed?[u[vv5'ar)V.'A:=d.-=!8gJ`kc"j08"aH׹yǮ)/EZ@a'2z,V[Ui~k rzi;X8=Zۙjl]F>Bd =c;acS뇂7&;)+46,R/2 6؎VL 1 sh[w HWpj?!fAfŖ 6+wu$(]q.-:|yt[x+#^|iE*zd*\L\)>x$vu ҫ,6H$CЕisr-,7$}?ɺ!mgvke΃tzxdȳ R&~:^f읒=ɷyY j]NNߩGb%^\ x)ܻۋ/Z f(1s)aa䠏UZ&.mgHu褣6w.LZ|r 7IO`q텻4U _i^ >Æ拻hDv>}#)NA>3Xe"vfwk4/wIzNAr}۝RE#$MoupbZ@Qo3&vfYuAKIBM;#H}c`rQ e\PGJ1rꚯů- r]˝Qh!lu01;zpl:2Ba xz?תcq3H[$L܄ڐ08!t,qty#ʳir &*J"i:7mLE 7]^PsA~{rT8 Ɠ ,r\/ )Dk3Wy$L'J02TrwS5uS^\zŃ Z= IXܼ.kwA4X4 o["虣{k}Ϻl:To&8eZqM-˵zP-P:z"3.+4,gnE_ U/DenŹ&_y*"uQǹjb41iҘc:#PFxvsY')( qtKEퟕ-Y*UyM(Կ$4EټU9CơChyŵ$~@}s fq])K҇3⺌A;@ѳw~O <+<+ohgu䄳fvC5HH {1zRc[ڨ[ڄ KTE,@q19s P]gTsqz>3c}U H(v9${W83F,l擒o6ՍgZF ̉ rgN4O0gV(iƢݑ`= Y< QU^ v[s??eW5MɨWʳեA~Α;:J t0OXY vw~r)ʻi2KR hcT`18Ded}.̦ )gbJnxmƂ j5m\X/l݀-PM~ N a-N#tkf+!oSUD9>3~`5/Oj|w fՎ ?/Sgbu0[hBTK9LA^s)?S׵MoP\Sr^4w$z=+N |9|L,40翢F'B.4M.ZԶ*}[Ù&ڟH soR),Ϡ`8BOWZpO)W.q7ߵ#7=^۰[%^b.69# L?†`.ݜ|H^_"EP.W!N~|3D+8б\ qHJP 9w+Br)#'8, lEZrBK?(L68\L|E1ΆKx]ȒͲVz=z`ZM :E *y*PPP Ȍ{Qd]~>)Ac*iya04Hdd1CWx6b+rI<]vEWqr(t/S?G_?%447=}'_3Ϯm8헾[5pk6ȫb<@q/`2`%-#ЏdBX;+ίrr'oJw }s:__:p6[0鳰t5u!-q{6f5pt<  &wPF-Q 윹2G>"kBs97<l!`Ч=7?h]迌5cZ>k.hKF .abi۾R[y)0O [`3Q^ݿQGՈ |zf@'͔h@1o~VοL AF2o"j{s<;hz!#V!o4`+,IЬKm DO/+1Cw~}Ӥ[`/ fkS7W?Vٵ_lX+Sc>',VmGRnϭ~s{wscU`Nb1C}MH|1S0S֕AZ;?ӢTNhQ O[_WuĄ>K@ j -@<;h:@9pX\"̧kâ-7t-Fbǵ/bG KW酔T/[ a_0x;or)ɢeUgt A mkL+ Ջ*8Rw=`o|o٧j2%[P^$.Ëvf@l./n`/a$t1)K/_ ߃zN nWʼnPFTG{OlOF̍!Yab6!.S_Pɰ-EBc J\w"♢>O7V+®t"kQTo -%*1'p89œV)_a j:6QHRa25.yO4:35߇Zf u!Þo+4)$}g;stW5űiXq.J);ߜ[Ө~)΍&PPqE{\UΥO(OO(U~R9y{ak]J|̒Lٶf̲q=%V5̗6Lq6h8c1\Fpt @'9o2Tɾ7r*rmfs9wEdf A3V!L8#mOҷ񠹱g(MD$M<b6 b ei^ ַWﰯv.Yv(7_PAOD;LSUSdy?P]3 ٶ(Yo:-8kj Ja8!C+`Tʍأ9%_ >6ħ㲋-N1ƮjzG mQv1cʦ~1]Z:%-;SV6lLѿPaWP bO!S@^%u\3vD:^X dF1AΧl;J?E{#>|֍WϜISujc[XG5 UC!+<ҌU! #zȐ=t +Y9gtJY*oo-g6"EGA٣jgIugj{#PU@k5]':QuyYQ-XmF\wL 1kO}~JcG($W[o֛y$4upQ"myV[7GlL/$5ESòa\FK`eu04RW?;~mK?Nhun/ |ABSzw}zïˤcbGЍ-ib]âP zEWrre%DLY@Y0GB$ig{074-A]05%xvb(тp<})n~X~:w0tRVT7ٶk ~ƬӅ{fc ajJ/Qw"0yXr-/G.ޭSk+ dϠֻՃqݓe){m'֍z\l}>rmο4c{8YGkޫ&@ӡB-&LR/o{;]*ܘr侺dG&nƉެKshb0DO83h_e !>)ɬzӝ,`bjjH=25Ig61##5%LTzw e4 tO[-n7'[  M +Qj3wHJ=l~ 6b 4w{tSu0יzeP%~%I١$=9-H#_[;[{_Lr eVMAX& 3Ɵ7Y ̎knJeB /'.ݚ֌Q}G&k d@S[[@YOBF8>ъRCYtJL/BY܊ юstK {=z!x7Cѷοpx?C1~b =:/ɺ6h(LRgO鼥~6^k~,7`vHؘoҹ^JL?_X%swXS1zuW56vn)%`9{E_ZG\7떷];t5q<4Z'xۭK)':Rs:oGMy?_Iy.1D W Z]ǯ ZRߟwVo_l_P:bfؿ9ǀO{f;!T_5~p~CXkV4n1E4cH'@hچřJsZ.NV\K^4W@ء(}cZ_+D Gb+]Dk `Ljxm{™cQz2S*8$h85?Ppt_D'a˸ϵ^mfӖ9iP>ܛ!YR{A/2xl '5n3{[t" ^3GK}IZ@sya~,ݹҰ +S9{PˈYZnrwY6lƋͯnT9O1ڻ{̳e>bE}_WKLE#W#ψerGCM@'5{N\tzc_ -/51vNvo햟rh*6=CY-8WUx>:< f*wp[\(0%m#U=+-*G+sLIw:h#|]l-C/Er[E6`[:/9JD{W3=&Q*̕8|?Vћ·`W(clYE^hNk/\Z[N;KytcI_~Wˋ7oqvj'm˓kuP,M"oҾ95ޘ[/"%^ eӅ)ݗj~SKĵEs]^l(zg#Fj ( Ǭ tj&) IXRNsT=&Ug:8drБН_ !]lV?]??$X~W6՞ZA8ζ}.^,{jl_yV.70ż2WEh[@Ds&fJS` Y\O͑8&};&n j$/n/nFFDZ< ;wvt ay XkO\vKTK.iE{ܳ3cG1INry;`tS4i{,N z$ԨI%YK1$qsrə.v'Ft8Υ ~9(yw"3])r#VΥTUz?E9%7#߮Cff*kma+n5K!Լ_ $;>ttV7ƃbN*w1OQh!F:k;-? Bg’\?^5ZN]E3 8,kQlmuw]οAfk~Na 0wBZ| `lMqR fߐ꣏Yt=MM8sv5Yа_D[6@nN6y%Eo'lDq^suw?7T|VĔa~R-8tUs\_@ O-0%?|M`B?iyF!*; NN7 [`hٯj_-_{^[h(5G8"ҠPR =:TU WTyp!綬~ cdOr%Ǖ,)Ρ[b^{mx4FV;G]Gt"L]NX ݅vYj\1$bөPš:>IvGAǒv:L"֘Qe|@$+[xxVp떾9,AW˷)>d"0ؒq2d]9ǩ?eNt7;R52"~d@0MW;?a=lS9-L~qFɬ})6h{&B):M9FVUhT]J qŨ_N)]a7FcL:o<@G|TFJh~pzF{KߝͦEՙU'~aEE|)]˾\OԼKǒ*'IeiV$)PJ+hTܻ9o0#SH74ʉj$a*tUX,˯T?GˤBS|q[ 5L]dX?&tl[-՗c^ҹN$IQ̪-ٛ>>Z֗-C^nIռfT6ߞb/xycl:A-yՋp˗W jw783lᧅ~]YXz~ye yIi"ܰ5lmW+0ό,[Li[8)8"橨DgmӊfjVRoa۬AUJbDz90 3Γ4Aϙ던->x޷+×'Zq;hzilAS'oPqʼC/_i :JgɘC Yfen>cδv+2$sit庪Jke!yFOtXW?:ۨ? 딑rBOxHU$1܉m&:[6@=sLk%+̠U8:2֬(Fhi^N6ծ[!><;ݟ󺘀X;l[bi+ EVvI:u ١ʋ/Rf\ӊi#;.qh/Jq=ϋlFtf迠jػ=6jf ج)zqHxzZ-#۴g/R~CA'J o,Dף L'Wyĝp#ɆQb_7ȗR(S(Ԟ߂ (*Pny\haoπA:-S4['㊰oOw!*7܃vm=%YivB~ap[b=ZTۨ-*j9m~Ǩb»N#x{=1/ԇaℸwS [kiqZɎ.Rl˹v5w,y= aqw) MY&=Vz(_]h6K'tԭh\'C&kďqC. `񮅇[8裾fk1_gpۙDz*+irh#2逾Kwe,38QHg]n.J-ot8%Fc!{̾pXh+yw(܏c?lrVȰF[++:TsC7cݣ2@Lg O2*.;?̪@adf8hxWue*VDrp9}8fmOW\@XQ5oOkۨ=L8M[ܭ|5=0E8NUhS=%U{OGBCYvŤIq bU1I4moz;GjWž=Ze{>a 1WJZ~nř/8D\5Ykh80亦AqE1~4۲Sa:shB%9b9dJrQ]3TH tSoH;/WGOmNIi oxk2~3r }z?-\5_})ft30WZzC5;ϷGHݭi|W~C:(tK7Y{O*pCŏ^\IxP{.pN%!`+<> 0kUc[e4^KkKqӫč3X <EhWFrnP+چ){rтZ'ZRɳn=*-S!J{H2EE"n o=O7 3I'V/@a4VZt/xjߪx9󁋊>+ϝbdߙ.ѯAq9%\=>_gp~5UK MM2Iӈry?߻0)g^5)Fc 5&v]f%pRFItbF<=Zs{ÏG_\zM/B%2y=` ]h>L$ @z[+BmoFAGШddЉ>+熸54Y .M4?2k|ZF 벚GT1S{>Q /1Ϣss>j2>PA| U7ϣC%p 'V!Sݠ$ Py\l }4^ܰbsIDm\ɭj95ysUpG9`@wnTps**/ 0{O;S=;2%o`tF2D6.ͦS ~?<=PH(k#G/Ϳݗ{cy+Nu1Ϙ Ybm(h5̹u}'Afvh4Y6*zUp춮LlO~TE=8xէ'  `<$&e :{B5.hCw УZf8q [x~G7z c ~P'a{rՃ Rrn ew-҂EYMGP4RfՂD%n?=Peibƿr\b^.-wn iRnZu)3[d/ CEa`v]ͫkd0LB"K?-5T"hXk2Ås+:l:J],>_®H\pީ:'%CMr۠pRH6kPvDC.N[GAжA 7YI0$0/ x,s@8_ bBYTa2 ABɨJw/Hk)ĉlvG!%D@v֛?<Di ;$ ~[0h& `p M! %d-EUXk `ZwJ6r4( u5W J@L>u,Hu}#>gp[rC$E }D}HYK)B*j4$ b;x\N7ad }vTpmM(O2ª &{~Q]ƙڟY^6]c0=ʙo`"5ӂS2Y_HP,+Aٲ 0%>"G=/=| I;s b4zٟF%ͪ1[u]wHh<T~q #JO))/F , PB*W^.3&Pq~厙5~]$yH UI~pz I0ԋ3\1G8J 8>DËAUCp~fV>p2YUa</N_S/bu23>6bw+xr'?nZs+P^1䢟kDqCqP\sHJa[a`o;6 ]y ;\oв3'I0V(z &"ȉ8䶗y`twtJ I+NqL]lt> J|M V|$59hCԦz2>L^֏[1@ĄTGgToDe8zrVTVYh?@&|6A &:RV_鵶/%-`f p#&QtbfpE\un8!_$t,HzX(KlcO /-~xL|/7 p# b_^l~c+{c/KA2 뺏 3^@{Ϡ;T8ұ!jmc~\XC@aѪ^]=4 +wx uR?Y#U}+^R _J!oJ@ϝMW!_~YFBcɭ Qfݤkd]$Y)؟WQ-+m!N !A_q!!ᐬKKc*BD*Vs0&3ɸ! حuTIK>;+ <߆GtmZ`<&@+((tPW+Tx0tN3ud$6𭊼 ) )mz,o`:r7zIGϔp73s=A@QK> Lcsv'J0jeg IbK(`9`a9dSLk9J L RX ,{ ԶqEG?FC*UX B*fǻn~\EUFoʥcܵ0B!Si+(L#bnH7a\x6IE@ )@lSWW*Ft YTM_b|$Jv{Y!F9َ#Z±<'J;BI\7-N!(tihRd'BuUQ c`B)DNJPeP_ w B>c)~뭺Ώ>}?KJb'"%ኊ0̂f+[oFP̈́ZOX  /@lúPUc@Tb37>TOQ$RB[.]?f&8{j(uYJSX+J9gpYcXnqXt *wP7tw690sNE o<%$m }yͿQszKOطc2O 0673t0%ť13VxBo .= 'Yqʼ;%+q2@O-FzX G3B6:a#"Ɠߐ Lj@g?)s6gco1VA\ Ɔg=s[p'yTƩ:? ߂q p +P-PGEYK6؀xK.r >^'olD `R ˞ !R(n[ ( 5.B%=x飵Y:tRI@O"7vlIJ9ytYk[h.` [>ByHD )/E<'Z}e}dD=JE= o7J,@@We1Ӵ2#kyГJ*w)nD6vg~'ߵ;o^׀T [],ZvQ>S`HTU仿 ?v=֠h\ZXL;AHr_3סnʷ2tklsrG3 B$|xsCQb(wc'i4u%_\H4V,\c!c+>n@Ġߤ:̬&đr Ihgr髒+:|/aϼQks]8^'XW:C|绗cHc~)<m̟fFCEhS;wKUബ$"r*Gx9" :-(%$χiIDnԨ^-)`?JvjOrBabQfOQTrlN}Vɥ4CXz,@>rïwY/|77"9> ۖf8@YIȻX%|s?7r[R2w0OMR\%;yFs]# B?KSo=3aaOd( մ{c[*Bᰴ;~ [T8Y]WW07+r`z@ddvUE"餙T\~8ɥZ|8ѯz=jȋ7Ĺ'Yo8 e9 O@5&Q2v(_ C|>29)S:6pǧ %mðs v]bO Cȷjd0e`BŪQ Y`B|e%|ꏸgQwOz)`-uU2qrͲy]VATAZ<0^@l~w-3~Ѱ޵ASYYwĶ?Ywr!u$#T`9۵-gF`Þ-<o2I^t1Y?U-V&̘k\KX=klМ\ekJ|p!ĄHV}w?<`*RKE?dA1$ y1|\ԔU-Ogk%u}`ĿS,4 vMoR&$-ՙ_xVbG}oU˴P`oH$1S k%]/ÉKe'?e;Y|0:R xdV<9.rpݽ+Ne8/>V@{9.t E謇縥k{t=PX 0-_]ZkqX(56_@-ޔ"b&[(LPEqz_r4̂xGS_ !Ϙܦ18-ZNݺ= ިwPuC/JJKkt(iT ZD;L2%իڢiFV7&=E; r+=u9a~6ۣK5l;_ˇU87 g-~|8"Voj|G\ɺk[8v{I(Wi<\vv/ 撴 9jo S>|Ό^#'c "\qI J,] ƹ pjHXGԒb,(Qd?8LPq{o bF-`,Cy>]8\u-\TF䩻 ]~6(c[Ym^V1yx~7P5d]Ap©h+ܑ* t;\υ܋Τe"^n q{hw=q̱{<_t'x\~*eG?!U:϶6';Ծ]\uV^E@-_؜nNlDX7a(y: T+Co) n{ j&Mr|6_Fsij_TB.4kW?RS>_jl-<0tq "[T9ZU3fm& A^+C,{B'|T&u(&l2R[\&iw&AJBsVûAhĮ+@hqdYrYm&KU1 {B"(LY~*@MS2Βِ1K jd"cqc-! Tꡯde҆ŪURcq 8_@ދmC?ӂq)F w.~lG؊X蛿_'?J؀s]6Nnc4_@})r{'/zt٤\ɓnM;w5F`z=6pХms&AE!^ _A`(CB0N8/FTR`|!U#yfE][{)UY]4 P yAp{A~M!6xY |݃;TǬ=Oh~s[`GwB(RȻ4b=хC_ԁ@KW>C9!B~}\mkwОɭׂ ;W4 cV5ig<EЈ(vĹW)|Rmw@Y Ɛd[PTMm(rnqezx@[kLY x"'(B2 "4IB"C.ϱ PB.]!{6u$4ʀ_-g3^3uI&xʓ6)zqsw8Fn>6l9?SA>l)@"Cv;'`&%tm"qw{8>,XGm0fn)XzwȬ]p쀚^`'N6)-thW*H_$H{'rK S%G Ǜ* qqF«;f^7L2n$ D$u{-!VJ9Dꪶp.0b|0nc"|_%1tۊx6b[1]$g6nR7=@P/3X/^ B(!(P͇QрzZYbBs<2mXdN\&V:ށM<óRE~g||[ԬL{LIRM8ٽz6EE6-yՀɹ<૞VR_"5|s"G?|Џ#XMTHѽڷgDTaboQj,d@"Y&c}pAOh")bOEc߸b50d7Ck@}~qBd h4uar=q"^Ci 'ˆDzXU&krA`ma%ͻT^PIuȎ St7egͣL`0:VD7s@.xWv4B O~>*,ͫ]\_f:ǣaL~Bcl՝jTՀd dqE=r;޽wVA7 tF@oTKAl0O0YC]7sk?ڒE/RԪKd!;<U+c@]]hu˵g? 4)0*ᇒCx-A2e}[Plo<'k \SKF?ɼh nEk^tC D:ruM}ivh]+@`FyniNH=I9z{ۿ˧o]'G \T_ xo Aa28--fJaP⽚<뱯SHa Ш-!Ò$`YPJ>>k@ u^`dDMm)!js!j(cVh 8X]텼 IHZ>.t_Pz'|XCAVXU>˄+WԦ& VYD@ } d)"(0;_q!) +^!@{X7 7//KX_r sk$:7>PFv/9ƫ8۬FMo=:E̅'OGUS~Z,nHt ]*B<((H6楼܀H)1h:Pgm5kZ )`pcXoݼw? Pô96 -b+پ s ^k{G /@B2S%c\^t_zNTH[F|Ǽ\QaY[qP;nC 1hr/P^s`:-O6:Ir9j $w9ГHJ{`r9lŎNVB2P@HlUA` 9'yil*{C#D%8T`#NMH ֈ7H`}v48D13 p1[wʧ? B.Pj8ȇkڮs붂v2aNƛ74BMzEH)ođ*hܪUu+ X5ݽ>EAw!bpPEu*NQ~\X^a02֘_!`̮ھJ K1ɏ'Lb:|j'~ L04k">9PT$Lḅ2QIX ̗'V_bHiNo0V0{ytl v U$eZޢ5!7n |VL'Cޠw9 7W5J DR W;1`U,u *sVJ._n0ęxڵpzd>L t;8Dy, p~|LMlǹF) R*|*f<*.v*, TH̅-cSwߟa:c*+"}t(R:?Dt"}|Ј~4:U"3x#2z֓9T8{ yU~ ]=/q(|<2(amS} G)c~bq_^P&Z鵆k'l ʹnѶMO9=ƌ#"[kًئ&z36^с_V7 s^@)(+#brӃ[O*W^ gݢ3|S-HI]Oj!MQ &qv7Shxf[ ԨAa"Z^1>F\CWOW::dJpoXJK/maX ›4Rz;mZFg 3ȯ9LTǩ}rtOlm|]=lRJv-,|U8-G))3wԢG.Ϩ >p">kl)ZS2٨a&)}hgvs,u=#ʓtA0%d&AˊQ8*%c~2澤~Pa+gD@Ùf{!VeҔwm :B|cOVIδWAn {GO(Du㌨Y%=M@r(}"}beqM8Ҍs?|i546O.qPFܔ E71ttK.(4'dMj\0ߠ _5q 1-4͉N [ը!ִX)sضFJa6?!Je.2>3מnX(uS_sr(8L˹wYa袬X L<-˻4}o3!7LM7ƅn}&zb}2qׅwBPi-'x/-v@xMqh湠M1H2b0KJ7BML>ܘ`rL6nf5Ygjjd$C6Sw"Ŵ5XψٲOd[L XnZW 3 jVakXq&⯞ "/lDuG^:4v[Mдn ˛<(%VICaY&(˻<3@lKTnB7aJ^ ˊ.o.IcT&md/]^Yz LMEьZpڙZX5t윭P_? Dt{SLXÊC3\j*gI}oO@[5Y"nQBJ(:>uˑ(.U<H{v ^)8WaJ&p{w 0~FvqKqOv=}`YiMMDĶGG6j-yNfH:QI+ݡS^6 0s`ag~WCGsg$Fh$r0{m+2xF7U|k?[+/s{t I#"qxE*

5osc3ԯMR`9kJaXv{i1-?`? X>fۗJ^bNm5|ٵaZ3Y"1-zÓylxGxzgK`MW|k’JEж 6ꁭWr`X:e4칠^L~f U|(m{SiB4pǥȅ^.LEp+C+nrqX2g\‘]Fʂa#KwR!BJ5щgaf5/y@"9a*ZEp5fo}OaW`oUfP(*v wVprx^6Oe϶ ) nekus-s ŊM [CWvGpg#F2oZ4!%?uUɕnKœ[vCEvi(У*r^?)I~[  @b5:##[788oD^ E v{d߅x{mFשxbǛ8 Q'7SͅTDf9KMt=AVY'- Y2a,ʍv٨-V/,'U:^qvYB%j I`::؁3ЊAYԔte`squțqUݿ} }ILe7P,Un]T.VĈia"Twـ8Z3;[QW!'簰sœ4g}{d.=BSuf1<2ꄠ"ZuQճI9g5ϯW%R 6Z@e|gԧ9o6\n{}tF!E`Ȅ$\$=nb1Dw}ґwv a{0/DE@ #UZӈ^ =71?la[z=Ԡs@6K<]v 8(jI_gfӸ08. e R9^3a\\gU ֪kYwhͥDXiؕK_PprN8Kg 9zB^[SIWk;[!8ZWmha_MV 5j^x`yx^VJb+ӍEkVJrES%KDPgž7XN\srB<@̷Ў=?lYi\]v saA y%;m/;[q#\{&O*J ͒Cdc.( On a:n%5LF֌*r$̷gr.4aêigjiIoJ|b!qN..pS d~]]sgoGHxrE۰m@+aLLjV5"u]n= D7jq(&`&O9j! R}N+Mc;kAq7fw5E"J[`} sEjdf!$uS_sd{^~ӱQ69׀+M<鳬DMx,;kF_[?`  ½fkF>TgVckv75moZN 0`sラN<AUtvaղíNq1$2̅ KJn~WG7rӗq&,LG5,o7ݒP@Y{]`-j& 9F2gv%*˶ƺ k^3A4wN+ A`\ d ?"Wՙ'X@r$zD45bȺ1+\M]Fi#oz=52ݣNW,=|]==s<."@*dAXaJf[gjh"qNymNcgmhLd7woIͻH\Q Cz ?r V06ۛjzvHYtX*喷e"خr߶d'gpS;=˄M&[J$1;a9T=+3J$/R]cI"PV`F*3I WySQEpFAFL.l)/ BZH=eD?DX^B}GlcUS+]m ^!z/("PAE+S*%qvr_dB `Ok7<1reeRfG+f&s.ºͦa*ds#\Ǭ|a\y O_h.MHkAaF>ͩxG xrnWl/J S<|ĘTCz5LaA6Wq!Gk^PX1o7-;U! Cq欭_aK+4n?3h+ f_4A]=߈<g4D&`h}jɰv*v3#soK9EB qóƛqǖf[Fcj N]ٗHALqdA[nwnK<9Šh64>燘&Ջ ֎t9<`\hY:G"aċYoO]bt!5mF=U{M3LHyzKW -ߩO 5s]  RpEQ7f^%5f9y҄~q6)3F$ j`1PHP?N{wl\mGZl}%xƶ}Wy׈8Q#I8ț YLQ NB-3#C26byS;ջTf=k5H 5̰qeCTqg=V@*!l|OK4bPÇީG4"oe:q9ܔiBDT:m] 8%li̮l+؂ᢏ댛~5tNQE(oc $sBt-o=PM%SSB6x|'tŸו@yz-qZ(F)Rl,^bZ1Z=mG5 L%Mpj_j^Es 0! #%gr2PE#]f;]ՐDQ0Б.eLy#~{RtMϥWSRm;Ic bC+S0zh$8"gKfB46 )D#qUʂ9VxtUCMJ6~121k g _ΆuOqsOW>Zc248'cn%j 2QP(nhi<}3WsDԩ_J-4!lFJ֧`] Eήz8-ٔTf>G1cg֔Cr2)E]ܨ=hd B2{Q{*7n B/y?ą?fS؏.M(laO\VTB!e#UE0EJq$i5Sx{轺ܤ8?!kh<&烐~чc.)v~amcᆲSw5d /*o2+Ŵn_+T?<&Rm}k:~aԑ؊tN(ň.v*s1;$$G͏p3!QXD1VE<~itXX6UXUbqi˃MUxWxp̔Z9 f+8Ξ+~jН7=g-mO_VG`$MlbS@4Lޓhńm輺F?OŨνxxCF:?Ci7BVK[䡒cU!t|ϪGz͆~B21.]qzu8`NR6\c`G38We$ Ljh=[9s g)#{UZyzaͷq74?AJs4ݰWJB8o2>0~ȹ>K#d-DVNw:݌sLbܙj١ e N\7UjoEv b&S1BEL'TM TEY#څc &Sx =0?yTC dRLǓfr_3I\v&O50N0ösլ7Zze3<ڊN}EN:mJWq)X}pq' o*:?GҮ,`Q e) U#M<ˉ僩n*νt3Htc5 cfI+sց R%XMY)߶U rBˆRۼ">.=;i.q=+ j6k 0ҔQ'X%Ȩ7?bu(D02i=a8Nk.WOYLZuұ ן%.k_b ,JٌE7XCb饬#"@ eU=hY}\oa)H4240[ *aX~錽n ե`dw~`RG*:,`1|w1\Ih.ޚeV2\ 쌷u"vw1)'>2[?]v65/U/ nhrg>_Q3ijZ.o;{t+EQ{i;w!4 jY*" 4_ ."V"O!tJ3;Q3p /Mf$z 9ʠ[4?z̪z0z {:` r70lY7N d/  \ -g\֨yV?Tksy1F_&} f.B䮰ҜiViLͧBe80g sO:n}3COqu|2̥LQ85(Пc'Q8vT n{(ENX :FȪkW1fzP/Cߵ׹\Ot;(U%\$b)zh:pKG Ry"F\@(l S&wu͇Yp;ɾY6p+Ii‚J@CzeB!%G^ۻႾ@xa[z"D>- Q 0~tnq]uM@TqטGE%y<$ ɂ͋vi7q?cKOj0޽WRneYl`B^駹dSn)ΏO .zcoZtn qJAա]1wEh:Lk=$m[WqWt1NYws2{Cǵ%^5~BFWBor#DRCܺW]3 8WgvZj1ԄۙXE;zԤbձw75]úRaGv[Tx1rxY7VBO^|]_]nT={j'2K+w}PmI Zeuo7py=D ,X-!A/hO[ T)Ut #c#2TBU.-,GZzpYag }M8o1fH%4$OC[˴nW#Xonhan:8ViӬ7^1 @׼j ~~| -O q0Ώǭ(~"ۃn۬^.=WyĿ NwVaN*5# oğb?2l0f[8(Wd|\sDj1/kW\FKN/Z?Xk"|k‰ /T1mNsW.[o,6.w}0WR6jfIճ2in!@Qٌ\H5O/B"`ߩw_,b̧:g+LXszpMn(I5&"aaI4mjǧE~mnlZ̋ Vq('n&C6rEt{_6 EDw6xp-'+X܀mUM` mY.EI*Q4g;pCh0YG\oGbp4XmLÉ"n] .^ʍ0$*C \짅T ǿ|lfVW;2 (up̋(u+3F!Ï[|KZi"iCv7<Ȧs)uK͝7_L k_fY l2a6u xr2+Ҙ )AN¦&`rTőj$iҴ͙T&n)c^:U buO@B NSyxݣgp [ Y#Ap>GE#ت Tح N-tǂfF ix%1%aN9_1[_9t`N˳?uKiEֿpHS8n^OZX|EAy`'ඊ*Z$ - 0JbgQ6bA*.[ 4Nu$ n@a>wg?6Vuk./cU)'PW練0'l[,K ɾp_(q6)qӞuiDnJ |Ϲ\ڼyo%1aҥ#DS"Ҁ7@  =25wvG%?_D.'??r`C`l}7tY/\j0[&^uKKGdOX#i?E{G=Ef(h2%Ru[^27Le ih{Hs; usT/pCW _+L;xP5Y* #yp`-~wi?:epC-NZ^91=G=sWۀ 7Cwjȍ}T(0'EgBrj1vƗV% N;Yĉ3wZ(V2,\ LC>nt=Ix*c&4mnk[- BB;X\`Gպk:j0P2&йmKЙt2D!fb{cc‒h5 7غCh0%!4ƯQE{ɽm*>V|eaWHPjnݍ4ӿcY|IÎW48y@~N'1B^j4crS(ꪠiF r"-X%Fz u^?b[ǫ/Ouj?G!kT|$$^;QZkoO4q[C%L~W^娣bg.%c%.EC5:`2 CvNAQSY4u7rXUzeF^J8MG!jp)~评V\&j!(Lpt^ɮ!K.y"#%}t> ghP#VX՞6߫6s|h7ۙC{KT{3]^AoHAZS`Ѥ`3bÄp䨃xWpR(^LPt2RDt$m,+솣VQ?wou9 sQ$J;d 2?5B^`s;7.u_nXh3jR7i|8(u hǤw~-|/gTp$ f_Kޭ<[shǒD!dVE)I&C뤊lR&` 1Bz_5In`q džݫI9zT6,{Jǧ܅#po 1PhkYB,mLŜ5 ͹͵QQ7pV qDfryJP*tɸ%ʮQ9^+<t.>Ba7I mG.;> ?a%5@yvQA~UxVx5|!(6Bb1nuVH.s(xJ3BkY~P[#K B ̢tG&jHW> %/X(ե'tPU&"\Q)}t[3wY4 uNyXi-!ra@ L!3~B(@OgH⚂ Qa}xwFtGG*j|H{wD F rξ)hm=(ul!,4W-p πR-tcd JFl: .lÔ>+?wd͜˼(}MoXtI%0 j#Ք}WɧLmt# FqD j {N4ov4`s+[צzc%_*N&v¼M] /ir5Ŋ<`BGn/S'wXSe(8v"fx}4)ᨩSY˜_ra#r%_Y0V׌K'']].(MiũOwHg0;pGmmM-̃bdd6 +PW}~Ӝz^2m%O0'h (*``jC'z@4_VS- 1u\2?OI8F`8$z=* B>%s z!|A l*jTaKYw*P".4K}7[54TzwF.3?*9e@rüLCJ⏄(s`15zܧ0fa] O2ޔlN"&<)'MnU+*kq SgaFws4BQbZGi7Zt`R8Vq~"pT|Saʨ=Vo^@5=MaXmimv:6oy<*޼+JWL3ĥg hQ8 Vԥw+N58&TݡG../ 4x0L9/:asXL sH~x*@;BS mum3{ύAR">|9Ut b,p@GؗIn@!u[ ,DH>Fh}9SNpO/xV-vfbH,lA\M_;xp9E>~MB>[+sa$F95?ͯ1Cnfp+KP_~cBR.Bh"FV*|'p+7 )F}B*_Xtt:8[FQ"̈΁Ż#_3ϵBYdYS ڲ0P*|$jm$7_pM,HW_]bvq j?;;k-i %Q_`}|Z׫s3?j^bu=KJOᢹd7eSdR VN K'V^t~K}~j7b4Y( }pNA4!; ,dcvh(JD&:4*"[;gB s&>F l&bμw.RT_?Bn޾qwrq4sPڈ Lܵ-m͌zvk~=i*<7>i++7jOV^._R.1o{!.#Iˑ0#^Vޔ{HeywQy|@~@R9k-ARyl;1Ǘ2cQ']4td:2&Տs~ni7)ydITBÆ9_7?UЬ6Xz&Kۣm ]t=]/]QD+ O;:=ϗCmmq>JQ}2VL}#ۇCo[._Z8?6z֤u9+e4N9lt f?z`7blwIV=fG |J/E jo斝@}|^;9M{j^&ywtD5GJ^F/lfT$utLe߻#m Aq'k ά1n:7Jfq#)@/.I(LgugIRk42Oo{OdjE{[,H-)+!4HVaAI8 %0cjtVK nvx?x#ƳQjOߍd#oVDERykOό~.(ϰ~}Y}9e4W)7x[& 7Jbqzttā36yѡxM!81KM/&M"_ Y/e{*8Db|k{D=kumbG󪖳DT6Nl TSSY .Ыx]k{/l)ؖʂu~4&FuS.LSxXQʔGkCE__u;3T_*_ćtꖙ}y{ywL_Z܀@.PS@z =@?sYݻex`am 50Za0"MPAŕ˹ Os/e^.;m]"p;mY׼M}5Mea~'Ǽ|*|wٕF #FS"RbHnso"23* `B {}$X9jgg\Mc#˼f̖ʻ:;z{hMzI+@Gw1FK74y̢zK9Dlo0,P\m)In4WxUX{ordM:a].$Zu!g0b=.;⦅ +#o˟֨j*{KӛDJ@WtDp ,sLB kEUhR9Gɑ6= &{OʾWi@,f6 `{apH,;sEwsJ,7)[wާgL|C91/-^ ?_P@%RHO.>[B}ww]_ .TgXy}RjP' ǮRƠ6 Yv W- &+9ޝZDk"űkBGwz-SHgffo&]]/SVK"CZV!ܟ3vG[pY^*k]Yt,F#hȦp7txW{ڮ.?vp'uO̘o,2辦Ze)\lu遰RR߂w2^.!x.[ݽ]x#>!mEvwm:(=}H^.15ȻIѲ<*ȕ|T%8*&ߙ5/vO׀%RRSHfdߧ,Âo~zb75<( ϳ~d#Xuuc܍M=I~1K _Wv֐F _'\Z߅NLXlLY8O(o֞syHq,t[B߉!PkrA80 w=w]%)gvZsZ~µwɷ(] 8.&F>vb:H!%e `[ډ V)+]OVw̍iNJJu͙.OǷ2նP 85%RXv_/\烟YZh#|<_i.Uf^݌`] %výbFw/6>oǂP$^x},(WZzs3 Oq9G~?kV@Ufr9YH®1u E܊] Ȣug#WZM1|&ǰ5ISX*9ȧvJNo=ĕؑGv>L%A@Q¯̖ū PSU*hx;|ra&Yc>|K7*fa[9ȆePK.1;wAv~dKgx1.M+;꟔Y"Y1-,oGb6Y |zEf8x2Ϫn$vt㧺H=[7TwofH|XyRa7t'ww3Fef閠v[bAǃPپɣi=D" YS}3g/L(ؿ){*K.!lyOg:3]4m08OFōͮgLSWEޯNPotnX"A_ź/"4]c&V oֻc0M9HýWy>sOۦs("q;*'JG20w^STw5wz^W6p:^pE^ F uwA/֮oB}_0[?/WU|HQCm2Db"4r ew'?]}gtWF4iqԬIR9ӯy)USa9 _X 3]!V A*ɂx*3Ah.u= Z\Ӡ91JiЩ)Xe8#QK'AU#1-ygm_kW # :a$R i*!h $l'#:Cab҈:S`43}WxC|ntVS L.[ R~+o C䜲BwQռK$y{nd]0Ӄx4 `;&btyɰbm6 Y\DUu B].۞`MGP|;*PG!Cm+3>QNx5I/:Q`،3eY7 ZHx>23x;Bw*2f :4](+GۄO)Iq?qL>~X 75Eܼ? u˽6) BM|bzka8sRۘ(zDʬR Q/th?7T<7IAl'mmj {|?i+_IdR s9BP2m_OINj`^q2Gv[X -AlCN$x)4bx~ʺ(!E%\@;[zÂ3ROf5%zm*X:hF6` ^y_> +{H ʌ}v#e$?X5A%kK!P Hh˙-Zq>(&|4L|.T8 '俫{LpJK\OO+tt$jj^3yoHw~˗ۊ0pK:نʮ`mR C ~)xZ ;lqk/_As 0%|"ZԫVvv^T yVyڷҖ"/f?[I+K^ d/Ioy?޾B-~~#R 'Wْdʙk0o2nWBo{yپ*ij)u(2idi?3 Ľ]F-X !Q\~ ;!Z IUr" uS~Y|Ad*J[[|LNKP +GL cT1 捎-55Gn| IJ"5@{Ne˴$W`O= tIWAUp - mZIު!rgNJ7@\e=;ߝcOq֠%ǍJ#W\m R-M]zCU3LKSvns]jtR˭Ci&}O;:j|<G^} Y8M 3~a`A,MݗV4@KUB)ML)A_Ybឲx-ij u/WvTٮB` {`e֛%VVr8h5咉?=[}2"@Et72<%00>aڍn!b~@'aT4ʥ_K ߰WF@4}[2$i4;3إà; K;צؔ^TϿ:R6y"1 Cs>?[똊C'qu;xt%M=A{WcRG?ho. YIJhƏ؍4~&q?G5dN|NдDt49v14TpzC'X5%]r6Q]1?_x-QNJ#Fi$ qwUPOOћ9_y):Ro=IFt-ʼs%!cR)%f+ u?3Rp gqV f. w]>78'_*Sz;}úYi%{.YKnYmU`LId|( ȋ/FY*ߓbgF]ՈYz.LF)0՞rveY'RܦoYuHNMs;p˸1!zF JcLK ozC6 QpW49i->(@ݺ܀60h}mnѫl͆mr7ۛBp'nm-]nv],#/YpvGb(=u@ KF4`JS6.n}lӟt/YTlqD\gyBA3rS@@nflHT+}y b.Sm> yy_DW*Rbll=b79l`Lj i4Ш@DkàZX|GQwׁ0ЄH7&N7K}$*h8_9WqݾEv6E  Ԑ%D&4Mb,/zRp]TC"Y OBPv{#Q-w㉏#S}]>sqdi Ǖ${N@|&@SM^&5+l>>!fD8.uGgxԧ_0~1!ՕG2 ȈMU /BѱbTՇrkS5_gil`e1so¿*:.{)@KaY;7> EƯgn@I^9}{-X=FbB{>^,pkA zl6MlmCVB>cOr̞ZK5J%r6 9k0jy])p >=}_dbstS~O_eɯ2,(B&HvŠ jc7O"ٞaahtn`^t]O(3Ӝ=t2{g.6O9unR [S#N36mYwO6 (5j$yڽC!Vg8gm]޼]B&DBm_YE֠*gAvlJ۝"r7iO韻@LzPE9f[" 6!-JH"u\K 1JRV53$Vq+vWh\ԿFx7mT' 1V?1C4pK^r=b;_gH T(8bJ' Reǿ"מ5JS r>JFF[he:8OtmBMMfM1TjZҍ}<5 d# T~1Ŗnn_LP'ݷ1a@4`n!l<7oqtd7"VNL%yfϮ?L,+ S3StDCnzJWŹd8\4mﯰ0 xve,oAۥQdtJyۅciΆINIP΋,ݺj[uE˔4Od£[O{[flf|l)ų;7sw񉰡&'P4o3v^qc.K{^3~bM.\ZeoOn`{R&F2 5[wO1<[ џRb}$ jY嬁Ÿϧ%8WUBqFN 9YJWRģEɴ_|@Sg͸ $e8qI[hZէizL5$D;ϛ7JE8ۅ낓 ;xҿzxg&zHODl5 M~jg>wA[K0LiyIx9q O Kz3جIv6V*ۧOʽ5үu2xwn{An/jYT1sK].,|O%^hSS#ħZ*_Os2|!1>0ՎL8\n]qGo]$no3Q'-REd7(Vլns0JP4ޫpLhOE~JsM*NӇ`حG99֝sQQTeoaEp˱T& ZȰ=>b:y=SvsI Z Qب'-Yf%Њ >󭍢㭵?ejj"'|>Rt4a4CJ&V znFu= T$Q*Y5D'  66OZ/q,9(/BH"}Z瘱f-=E#NF~Y`BJ5oݮ[o/rX27bƪڝ0 S~N Kn/:^2@ u ;C:J_טoT=?HHĬ2)9NBlhUԆWٱyR/8*%5Cv*/vnsWZs GzQKM GDg*U_{=~ೇHp܁7([^ ?R>cC]D:,БWB}~[`z o̊dD/FBkczKbE%VuE7J_uDu|W.>]r-V~*yQj}aЮ MHAν,> #qj#b/Rȴ?wVxI y+!g^2/yVZz[/)=)Ga!̩_2VѕdhqzsyqMLN _4i%MADmS+ܼaCv٩ϿGwA^rt=M=l߻&|4aD:OttoE!ٚ_G f AޒnZЯvEI. ff ÎU@j D 4mi:1x1PxlUdQӇ 8ҸŹnLS^⠎ *zzyPT"/ikeYw؟@:6<@d#5ڬTbh as5iOvG u%3[jܭPiV?W}!72' &7Y2v$a_&s} =C5+<}jKidw7hM>`ssiT#}~ʛ&V,IoU艛\޴$KA;}nގyVe*<]X*GBXBV:h~xy#U3-ͻR w٥b= -ifio?pw<\if˷R4X2mOTGf|bi sX J4/6 %fo*ƗD2"O4rl}vJ^Y(:MRU'oǺ?]:>@K56Pӟ} -21;77J[ʈpuě@+f'rjN^x_Q)/%fn%~N+6D I_2]C8()n6_Ayw=a Pǰ~oYH.Ϩ^^_-(ڿ-ea{z|^! ;ϊ1 }+D>+\L]M]P6!S0|[Nt?ٜ|Ehôz^Hn1<. 72eik G~o=C|[j-TUR(@<0u~ph{t^ޗ]4ϧ9g#Mj(ZRgO0K%ǻc}'][,㓼y@FO;<2k:-Y[-yun-E{պ;)ėd}PM쀈ΠLI]ch8BW(C_&'S6_gQJ0^Q&/Dyڻi(.s)sq)n켼Tc3<.ҷRdLı8InS獷TP"E!%3fNJlTkاV?ə_sJkIudK'eYt7O{k:"U5aUr\\"42 &@٢IpwFW&.%P %Zgt+}jOp:~kZywOqc/hs*~%Ɲ5EVqG1iį<{ Z;8[FGl{}F?)OtJnр096G]PoEo̖̤ gbgW-WKVz݈DjfVi~ݰ8tԿ~rMވh?xY llW 6m-ZFʍ I->vcnl|\DLj2{ڵJ5M;v~XoF4>q6yf.`'hɈ'Io'G"j*~NF6pNDw>ߠݞI\}GeThg FA_h-}8\!P#B,ܦp7J-r=h#u$\+ڢ8k+]{[:wߜ45})-Y]Vtic(_.=R+?'R{T`hh 5툪sG +$&uOɧṛ n3!gLܖשOL%C x&GßA8_=]o- QܗDm-M7Rh8޽-qLmjDIX9sǼqx2|ӷ0yR+)>hM}4+rTs\ֽIމVȑ5"ɳڀ9>QEMo)S'R]Dd@qp8<\l9\-m. 5h=аTWA;T|!01 !;+7_qX؛哒!HH>%A:9X*+w 3w3{gVO\-tm,ؤgE1c)Zi=Ppv']OH8xKּyPhxdL\BRZջ_~..?_ݝ \fE/ 7yc2WfO͟ h (\uA%)DCDN,ς!? &wM32z<Ԧ!sy))?խxЗOBu[S==1Vz>MR(CoD`Ġnz?#1 0v+Φ֞O'FT|qA= o-kJ 34Nd2̇t45u5j<~v_Y ގg;N)<AM:G7N-F};fCqyL8ʫaߜX9l;i 7kQl$?ȫJ`jik~d)K̛Y*+z>C4RG:)7VҫۤDA >Kp~L2Îҟi,fg  }49JW$6_[eq1c_"~CEYO:3)4\'d)w'ң >$ٸʷI%)\GfOΜ8Lz~y)ܠwcIPHf$6ug?$POj(g O= Lj TH"1ytч`uh8Jzj;˯Q6U+~+>It{km JU),ˈqN|~Nқ=\~XcvRYʣi@v㜽B A͎*W[hTtK#\68pBD,CܳǿDM\V;cӒ]!r1 xMr2Oy*aȣL#ıK?#0@ #bD`x6v3v'D4 `aA81hD1lkS4$ 0hp‚ fJhx$vƘF`m 2Hd M,q09pĈ`S  7a0@`LY*И@ O X?L,XπdJGQ$QG?浣r]DQ}l0L1hSlh1q#v@LjlXذn#)?a+m[66o&~_Czp'd)T22rgvouaQNdU{yzTY}{ow.zh-e쮺<:zFE(3m[M p!X9ucO%!̲GqSrgRt6ܸ.B Zĕu3Tv %Ec?I|}m/f[QǾTC2]V:}>} 7]&t'uIS/"5P_TFbY7eFY2Jq)q,®7Q,eͳ1AE Z:.BAR=6nZŠ%V6jӹjg[0}U?9ŮBqAmȺO=9|W%_Z2:`'<-Wsrp7sߔ~l52GPKZ`W?( .DS_StoreUT[eux PK``W  9.background/background.pngUTdeux PKS`WLĩ" .VolumeIcon.icnsUTMeux PKJMEGAcmd-2.5.2_Linux/build/installer/terms.txt000066400000000000000000001107071516543156300211020ustar00rootroot00000000000000MEGA LIMITED TERMS OF SERVICE ("TERMS") Scope of these terms 1. Welcome to Mega. Mega Limited ("Mega", "we", "us") provides cloud storage and communication services with user-controlled encryption. We provide services ourselves and via our related or affiliated entities, payment processors and resellers who act on our behalf, at our website at https://mega.nz, subdomains and related sites ("website"), using our mobile apps ("mobile apps"), and our application programming interface ("API"). Using Mega, you and other users can encrypt your files and data by way of user-controlled encryption ("UCE"), upload, access, store, manage, share, communicate, download and decrypt files, information, material, text chats, voice chats, video chats and other data (all of which we call "data" in these terms) and give access to that data to others (all together, "services" and each, a "service"). If you have questions about how to use our services or the great things you can do with Mega, check our Help Centre or, if you can't find the answer there, check our contacts page for details of who to contact. 2. These terms are binding and apply to any use of the services, website and our mobile apps by you and anyone who you allow to access your data or our services. By using our services, the website or our mobile apps, you and they irrevocably agree to these terms. If you do not like these terms or don't want to be bound, you can't use our services, the website or our mobile apps. In particular, OUR SERVICES ARE PROVIDED SUBJECT TO CERTAIN DISCLAIMERS BY US AND UNDERTAKINGS BY YOU, INCLUDING AN INDEMNITY FROM YOU IF YOU BREACH THESE TERMS - see clauses 37-48. NEW ZEALAND LAW AND ARBITRATION OF ANY DISPUTES APPLIES EXCLUSIVELY - see clauses 49 and 50. 3. We can change these terms at any time by providing you at least 30 days' prior notice of the change, whether via our website, via our mobile apps, by sending you an email or via any messaging service we provide. Your continued use after that notice means that you agree to the changed terms. If you have paid for a subscription that is due to expire after that 30 day notice period and you do not wish to continue to use our services under the new terms, you may terminate your subscription before the new terms come into force. We will then (but not otherwise) refund the unexpired portion of your subscription payment within 30 days and close your account. For more information about refunds, recurring paid subscriptions and their termination, see clauses 55-57. 4. If you comply with these terms, then we grant you a non-exclusive, non-transferable, worldwide licence to access and use our services via the website, our mobile apps and our API if applicable, in accordance with these terms and any plan you have subscribed for. Your data 5. If you allow others to access your data (e.g. by giving them a link to, and a key to decrypt, that data), in addition to them accepting these terms, you are responsible for their actions and omissions while they are using our services and you agree to fully indemnify us for any claim, loss, damage, fine, costs (including our legal fees) and other liability if they breach any of these terms. This is particularly the case where you are the administrator of a business account (see clauses 51-54 below). 6. UCE is fundamental to our services. This means that you, not us, have encrypted control of who has access to your data. You should keep your password and Recovery Key safe and confidential. You must not share your password with anyone else and should not release encryption keys to anyone else unless you wish them to have access to your data. If you lose or misplace your password, you will lose access to your data. Encryption won't help though if someone has full access to your system or device. We strongly urge you to use best practices for ensuring the safety and security of your system and devices (e.g. via unique passwords, security upgrades, firewall protection, anti-virus software, securing and encrypting your devices). Mega will never send you emails asking for your password or suggesting that you click a link to login to your account, so do not be fooled by any such email since it will not be from us. 7. You must maintain copies of all data stored by you on our services. We do not make any guarantees that there will be no loss of data or the services will be bug free. You should download all data prior to termination of services. 8. Our service may automatically (without us viewing the file content) delete a file you upload, store, access or share where it determines that the file is an exact duplicate of a file already on our service (a process usually referred to as deduplication). In that case, the original file will be accessed by you and any other user and that file will be retained as long as any user has a right to access it under these terms. Any right of deletion that you exercise will not apply to a deduplicated file that is associated with another user. 9. We will store your data on our service subject to these terms and any plan you subscribe to. If you choose to stop using our services, you must download your data first because after that we may, if we wish, delete it. If we suspend or terminate our services to you because you have breached these terms, or someone you have given access to has breached these terms, during the term of that suspension we may, if we wish, delete your data immediately or deny you access to your data but keep it for evidential purposes. See also clauses 51-54 below which set out details of what happens to users within a business account when the business account is suspended or terminated. In circumstances where we cease providing our services for other reasons, we will, if we consider it appropriate, it is reasonably practicable and we are not prevented by law or likely to incur any liability in doing so, give you 30 days' notice to retrieve your data. Your obligations 10. Once you have subscribed to a plan for our services (with payment having been made via the website, one of our mobile apps or one of our related or affiliated entities, payment processors and resellers), you need to pay the fees (if any) for that plan (and any other taxes or duties). No matter which reseller or related or affiliated entity of Mega you make payment to, your contract for services is with Mega Limited and is governed exclusively by these terms and our policies referenced in these terms. We can also change the fees for our services (other than those you have already contracted and paid for) at any time if we give you notice. In the absence of manifest error or other lawful error, you can't withhold payment or claim any set-off without getting our written agreement. 11. If at any time you do not make a payment to us when you are supposed to (including on termination), we can (and this doesn't affect any other rights we may have against you): 11.1. suspend or terminate your use of the service and/or; 11.2. make you pay, on demand, default interest on any amount you owe us at 10% per annum calculated on a daily basis, from the date when payment was due until the date when payment is actually made by you. You will also need to pay all expenses and costs (including our full legal costs) in connection with us trying to recover any unpaid amount from you. 12. You must: 12.1. where you have subscribed for a service, always give us and keep up to date, your correct contact and any billing details and those of any users within a business account; 12.2. comply with these terms and any other agreements you have with us and ensure that users within a business account, of which you are administrator, do likewise; 12.3. comply with all applicable laws, regulations and rules when using the website, our services and with respect to any data you upload, access or share using our services and ensure that users within a business account, of which you are administrator, do likewise. What you can't do 13. You can't, and will ensure that no users within a business account, of which you are administrator: 13.1. assign or transfer any rights you have under these terms to any other person (including by sharing your password with someone else) without our prior written consent; 13.2. do anything that would damage, disrupt or place an unreasonable burden on our website or service or anyone else's use of our website, our mobile apps or a service including but not limited to denial of service attacks or similar; 13.3. infringe anyone else's intellectual property (including but not limited to copyright) or other rights in any data; 13.4. resell or otherwise supply our services to anyone else without our prior written consent; 13.5. open multiple free accounts; 13.6. use our website, mobile apps, API, or any service, including, without limitation, any communication tools available through the website, our mobile apps, or our API, or any forum, chat facility or message centre that we provide: 13.6.1. to store, use, download, upload, share, access, transmit, or otherwise make available, data in violation of any law in any country (including to breach copyright or other intellectual property rights held by us or anyone else); 13.6.2. to send unwelcome communications of any kind (including but not limited to unlawful unsolicited commercial communications) to anyone (e.g. spam or chain letters); 13.6.3. to abuse, defame, threaten, stalk or harass anyone, or to harm them as defined in the Harmful Digital Communications Act 2015 (NZ) or any similar law in any jurisdiction; 13.6.4. to store, use, download, upload, share, access, transmit, or otherwise make available, unsuitable, offensive, obscene or discriminatory information of any kind; 13.6.5. to run any network scanning software, spiders, spyware, robots, open relay software or similar software; 13.6.6. to upload anything or otherwise introduce any spyware, viruses, worms, trojan horses, time bombs or bots or any other damaging items which could interfere with our, or anyone else's, network, device or computer system; 13.6.7. to use any software or device which may hinder the services (like mail bombs, war dialing, automated multiple pinging etc.); 13.6.8. to attempt to gain unauthorised access to any services other than those to which you have been given express permission to access; or 13.6.9. to impersonate anyone or to try to trick or defraud anyone for any reason (e.g. by claiming to be someone you are not). 14. If you register with us, you will need to use a password in conjunction with your specific account email address. You need to make sure your password is secure, not used by you on other sites and confidential. Make sure you tell us straight away if you think or know someone else has used your password or there has been any other security breach. We will hold you responsible for anything done using your account and password. MAKE YOUR PASSWORD A STRONG ONE AND KEEP IT SECURE. We are not responsible if someone else gains access to your computer or other device and/or your Mega password and/or encryption keys for any files. Intellectual Property Our IP 15. You are not allowed to, and you can't let anyone else (including in particular any user within a business account of which you are administrator), use, copy, alter, distribute, display, licence, modify or reproduce, reverse assemble, reverse compile, communicate, share, transmit or otherwise make available, (whether digitally, electronically, by linking, or in hard copy or by any means whatsoever), any of our code, content, copyright materials, intellectual property or other rights without getting our permission in writing, other than in order to use our services as intended or as allowed under any open source licences under which we use intellectual property provided by others. The open source code that we use, where we obtained it, and licences for that code, are all referenced on our website and via our mobile apps. 16. Without limiting any other provision of these terms, you are only permitted to directly and specifically use the API if you register at the developer registration page and agree that you may only publish or make available your application after we have approved it pursuant to our application approval process and licence agreement available on request at api@mega.nz Your IP 17. You own, or undertake that you are authorised to use, any intellectual property in any data you store on, use, download, upload, share, access, transmit or otherwise make available to or from, our systems or using our services. You grant us a worldwide, royalty-free licence to use, store, back-up, copy, transmit, distribute, communicate, modify and otherwise make available, your data, solely for the purposes of enabling you and those you give access to, to use our services and for any other purpose related to provision of the services to you and them. Copyright Infringement Notices 18. We respect the copyright of others and require that users of our services comply with copyright laws. You are strictly prohibited from using our services to infringe copyright. You may not upload, download, store, share, access, display, stream, distribute, e-mail, link to, communicate, transmit, or otherwise make available any files, data, or content that infringes any copyright or other proprietary rights of any person or entity. 19. We will respond to notices of alleged copyright infringement that comply with applicable law and are properly provided to us. If you believe that your content has been copied or used in a way that constitutes copyright infringement, please provide us with the following information: i. a physical or electronic signature of the copyright owner or a person authorised to act on their behalf; ii. identification of the copyrighted work claimed to have been infringed; iii. identification of the material that is claimed to be infringing or to be the subject of infringing activity and that is to be removed or access to which is to be disabled, and information reasonably sufficient to permit us to locate the material including the exact URL link (with decryption key) to that material on Mega; iv. your contact information, including your address, telephone number, and an email address; a statement by you that you have a good faith belief that use of the material in the manner complained of is not authorised by the copyright owner, its agent, or the law; and v. a statement that the information in the notification is accurate, and, under penalty of perjury (unless applicable law says otherwise), that you are authorised to act on behalf of the copyright owner. 20. We reserve the right to remove data alleged to be infringing without prior notice, at our sole discretion, and without liability to you. In appropriate circumstances, we will also terminate your account if we consider you to be a repeat infringer. Details of our designated copyright agent for notice of alleged copyright infringement are on our contacts page. Copyright Counter-Notices 21. We process all takedown notices based on good faith acceptance of the representations from the party submitting the takedown notice. We do not review the material before processing the takedown notice. 22. You may file a counter-notice if you believe that access to a file you have uploaded has been wrongly disabled because it was the subject of an incorrect takedown notice. You should only do so if you are confident that no other party owns copyright in the material, or you have rights to store the material and, if you are sharing it, that you have the right to do so. 23. Please understand that: 23.1. When we receive your counter-notice, we pass it, including your address and other contact information, to the party who issued the original takedown notice. By submitting your counter-notice you authorise us to do so. 23.2. Filing a counter-notification may lead to legal proceedings between you and the complaining party. 23.3. There may be adverse legal consequences in New Zealand and/or your jurisdiction if you make a false or bad faith allegation by using this process. 23.4. If, when using this counter-notice process, you make a false or bad faith allegation or otherwise breach these terms or any of our policies and that causes us any loss, costs (including legal costs), damages or other liability, we reserve the right to claim for and recover from you that loss, those costs (including full legal costs on a solicitor-client basis), damages and other liability, by deduction from any balance in our account and/or by proceedings in New Zealand and/or the jurisdiction of the address in your counter-notice. 23.5. We provide this counter-notice process voluntarily for the purposes of all applicable copyright takedown and counter-notice regimes in New Zealand and other jurisdictions, but, in doing so, we do not submit to any jurisdiction, law, tribunal or court other than those of New Zealand, as set out in these terms. We may amend, suspend or withdraw this counter-notice process at any time, provided that any counter-notices in train at that time shall continue to be processed. 24. By filing a counter-notice, you are deemed to have accepted the above terms. If you do not accept the above terms, do not file a counter-notice. 25. To file a counter-notice with us, you must provide a written communication at https://mega.nz/copyright or by email to copyright@mega.nz that includes substantially the following: 25.1. Identification of the specific URL(s) of material that has been removed or to which access has been disabled. 25.2. Your full name, address, telephone number, email address and the username of your Mega account. 25.3. The statement: "I have a good faith belief that the material was removed or disabled as a result of a mistake or misidentification of the material to be removed or disabled." 25.4. The reasons for that good faith belief, sufficient to explain the mistake or misidentification to the person who filed the original takedown notice. 25.5. The statement "I will accept service of proceedings in New Zealand or in the jurisdiction where my address in this counter-notice is located, from the person who provided Mega Limited with the original copyright takedown notice or an agent of such person." 25.6. Signature. A scanned physical signature or usual signoff in an email or using our webform will be accepted. 25.7. You may also provide comments. 26. We will only accept a counter-notification directly from the user from whose account a folder or file has been disabled. Counter-notifications must be submitted from the email address associated with the Mega account. 27. If we do not receive any further communication from or on behalf of the person who originally submitted the takedown notice, or any communication we do receive does not in our sole opinion adequately justify the original takedown notice, we may, but shall not be obliged to, reinstate the material in approximately 10-14 days provided we have no reason to believe that the material infringes copyright. 28. Nothing in this counter-notice section prejudices our right to remove or disable access to any material at any time, for any reason or no reason. Other Infringement Notices 29. If you consider there has been some other infringement or breach of law, or of these terms, and wish to file a complaint, contact us at the relevant address on our contacts page. We will generally require the same amount of detail as set out above for copyright infringement notices. See also our Takedown Guidance Policy. Suspension and Termination 30. You can terminate your access to our services at any time by following the 'Cancel your account' link in the Account section of the website or the Settings section of our mobile apps. However, we will not provide any part-refund for any allowance not used on any subscription you may have, other than under clause 3 above. If you are a business account administrator you may also terminate access to any user within the business account. 31. We can immediately suspend or terminate your, and that of other users within a business account, , access, to the website and our services without notice to you: 31.1. if you or they breach any of these terms or any other agreement you or they have with us; 31.2. at any time if you are not a registered user; 31.3. if you are using a free account and that account has been inactive for over 3 months or we have been unable to contact you using the email address in your account details. 32. We may also terminate or suspend our services or any part of our services, for all users or for groups of users, without notice, at any time, for any reason or no reason. 33. All charges outstanding on your account must be paid at termination. Export Control 34. You may not use, export, re-export, import, or transfer any software or code supplied as part of your use of our services: (a) into any United States or New Zealand embargoed countries; or (b) to anyone listed as a specifically prohibited recipient by the United States Government or the New Zealand Government. By using the website and our services, you represent and warrant that you are not located in any such country or on any such list. You also will not use the website or our services for any purpose prohibited by United States, New Zealand or any other law, including, without limitation, the development, design, manufacture or production of missiles, nuclear, chemical or biological weapons. Severability and Waiver 35. If any provision of these terms is held to be invalid or unenforceable, the remaining provisions will remain in full force and effect. If we do not enforce any right or provision of these terms or if we in any instance grant any concession or indulgence, that will not be deemed a waiver of such right or provision or obligate us to grant any concession or indulgence to anyone else. Force Majeure 36. We will not be liable by reason of any failure or delay in the performance of our obligations because of events beyond our reasonable control, which may include, without limitation, denial-of-service attacks, strikes, shortages, riots, insurrection, fires, flood, storm, explosions, acts of God, war, terrorism, governmental action, labour conditions, earthquakes, material shortages, extraordinary internet congestion or extraordinary connectivity issues or failure of a third party host, (each a "Force Majeure Event"). Upon the occurrence of a Force Majeure Event, we will be excused from any further performance of the obligations which are affected by that Force Majeure Event for so long as the event continues. DISCLAIMERS 37. WE DON'T GIVE YOU ANY WARRANTY OR UNDERTAKING ABOUT THE SERVICES OR THE WEBSITE WHICH ARE PROVIDED "AS IS". TO AVOID DOUBT, ALL IMPLIED CONDITIONS OR WARRANTIES ARE EXCLUDED AS MUCH AS IS PERMITTED BY LAW, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF MERCHANTABILITY, FITNESS FOR PURPOSE, SAFETY, RELIABILITY, DURABILITY, TITLE AND NON-INFRINGEMENT. 38. We will try to give you access to our website and our mobile apps all the time, but we do not make any promises or provide you with a warranty that our website or the services will be without any faults, bugs or interruptions. 39. Whilst we intend that the services should be available 24 hours a day, seven days a week, it is possible that on occasions the website or services may be unavailable to permit maintenance or other development activity to take place or be periodically interrupted for reasons outside our control. 40. Information on our website will change regularly. We will try to keep our website up to date and correct, but again, we do not make any promises or guarantees about the accuracy of the information on our website. 41. We do not warrant that the services will meet your requirements or that they will be suitable for any particular purpose. 42. You are the controller in respect of some data Mega holds about you and Mega is the processor, for General Data Protection Regulation ("GDPR") purposes. Mega is the controller in respect of some other data. See our Privacy & Data Policy for more details. These terms, our Privacy & Data Policy and our Takedown Guidance Policy are the contract between us that governs our processing of that data. It is your sole responsibility to determine that the services meet the needs of you, your business or otherwise and are suitable for the purposes for which they are used. 43. We also aren't legally responsible for: 43.1. any corruption or loss of data or other content which you or anyone else may experience after using our website or our mobile apps, or any problems you may have when you view or navigate our website or use any of our mobile apps; 43.2. devices or equipment that we do not own or have not given you; 43.3. any loss or damage if you do not follow our reasonable instructions, these terms, our Privacy & Data Policy and our Takedown Guidance Policy; 43.4. any actions or non-actions of other people which disrupt access to our website, our mobile apps, or our API, including the 43.4.1. content and nature of any data that you upload, access or share; 43.4.2. content of ads appearing on our website or our mobile apps (including links to advertisers' own websites) as the advertisers are responsible for the ads and we don't endorse the advertisers' products; 43.4.3. content of other people's websites even if a link to their website is included on our website or our mobile apps. 44. You warrant that if you are accessing and using the services for the purposes of a business then, to the maximum extent permitted by law, any statutory consumer guarantees or legislation intended to protect non-business consumers in any jurisdiction (such as the Consumer Guarantees Act 1993 in New Zealand) do not apply to the supply of the services, the website, our mobile apps or these terms. LIMITATION OF LIABILITY AND INDEMNITY BY YOU 45. TO THE MAXIMUM EXTENT PERMITTED BY LAW, WE (THIS INCLUDES OUR EMPLOYEES, OFFICERS, AGENTS AND AUTHORISED RESELLERS) ARE NOT LIABLE WHETHER IN CONTRACT, TORT (INCLUDING NEGLIGENCE), EQUITY OR ON ANY OTHER GROUNDS TO YOU OR ANYONE ELSE FOR ANY DIRECT, INDIRECT OR CONSEQUENTIAL DAMAGE, LOSS, COST OR EXPENSE, DAMAGE TO PROPERTY, INJURY TO PERSONS, LOSS OF PROFITS, LOSS OF DATA OR REVENUE, LOSS OF USE, LOST BUSINESS OR MISSED OPPORTUNITIES, WASTED EXPENDITURE OR SAVINGS WHICH YOU MIGHT HAVE HAD, DENIAL OF SERVICE OR ACCESS TO OUR WEBSITE, OUR MOBILE APPS OR OUR API, OCCURRING DIRECTLY OR INDIRECTLY FROM THE USE OR ABILITY OR INABILITY TO USE, OR RELIANCE ON, OUR WEBSITE, OUR MOBILE APPS OR OUR API, OR THE SERVICES, AND BASED ON ANY TYPE OF LIABILITY INCLUDING BREACH OF CONTRACT, BREACH OF WARRANTY, TORT (INCLUDING NEGLIGENCE), STATUTORY OR PRODUCT LIABILITY, OR OTHERWISE. 46. YOU SHALL INDEMNIFY US AGAINST ALL CLAIMS, COSTS (INCLUDING ALL OUR LEGAL COSTS), EXPENSES, DEMANDS OR LIABILITY, DAMAGES AND LOSSES WHETHER DIRECT, INDIRECT, CONSEQUENTIAL, OR OTHERWISE, AND WHETHER ARISING IN CONTRACT, TORT (INCLUDING IN EACH CASE NEGLIGENCE), OR EQUITY OR OTHERWISE, ARISING DIRECTLY OR INDIRECTLY FROM BREACH BY YOU OR ANYONE YOU GIVE ACCESS TO YOUR DATA, OF ANY OF THESE TERMS OR ANY POLICY REFERENCED IN THESE TERMS. 47. IF YOU ARE NOT SATISFIED WITH THE SERVICES, THEN YOUR SOLE AND EXCLUSIVE REMEDY IS TO TERMINATE YOUR USE OF OUR SERVICES AND THE CONTRACT YOU HAVE WITH US. 48. DESPITE THE ABOVE, IF ANY COURT OR OTHER COMPETENT AUTHORITY HOLDS US (THIS INCLUDES OUR OFFICERS, STAFF AND AGENTS) LIABLE FOR ANY MATTER RELATED TO THESE TERMS OR OUR SERVICES, OUR TOTAL COMBINED LIABILITY WILL BE LIMITED TO THE MOST RECENT SUBSCRIPTION AMOUNT YOU HAVE PAID TO US. Disputes and Choice of Law 49. Any and all disputes arising out of this agreement, its termination, or our relationship with you shall be determined by binding arbitration under the Arbitration Act 1996 in Auckland, New Zealand, by one arbitrator who shall be a lawyer knowledgeable in relevant technology matters appointed by the President for the time being of the Arbitrators and Mediators Institute of New Zealand Incorporated (AMINZ) on a request by either you or us. The following terms apply to the arbitration in addition to those implied by New Zealand law: 49.1. Notice must be given to apply for any interim measure in the arbitration proceeding; 49.2. The arbitration proceeding will commence when a request is made to AMINZ to appoint an arbitrator; 49.3. The arbitration shall be in English. The Arbitrator shall permit the parties and witnesses to appear by videoconference that we will organise and pay for; 49.4. We will pay the arbitrator's fees and expenses unless the arbitrator determines that you should meet some or all of those fees and expenses because your dispute is frivolous or vexatious. 50. The relationship we have with you under these terms and their interpretation and construction together with any dispute, suspension or termination arising out of or in connection with them, is governed exclusively by New Zealand law. Mega does not submit to any other jurisdiction other than New Zealand and New Zealand law. You and we submit to the exclusive jurisdiction of the New Zealand arbitral tribunals (and courts for the purposes of the enforcement of any arbitral award or appeal on question of law).The parties agree to enforcement of the arbitral award and orders and any judgement in New Zealand and in any other country. Business Accounts 51. For business accounts, the administrator of that account can see and deal with the files and data associated with all users within that account (including any data and any personal information). In addition: 51.1. if the business account is suspended or terminated, the action will affect the data and personal information of every user within that account; 51.2. the administrator of the business account will be able to see and deal with, change or delete the files and data associated with every user within that account (including any of data and personal information); 51.3. the administrator of the business account will be able to terminate any user's account within the business account, restrict or disable usage of the account, change any user's password and otherwise deny access to the account and all data and personal information and such users will then lose access to all their data and all personal information associated their account. 52. We will charge the credit card associated with the business account with the applicable fees per user (for a minimum of three users) at the monthly billing date, on a recurring basis. 53. Where a business account recurring payment fails for any reason, after 30 days we may suspend the account and all users within that account until payment is made. If no payment is made within a reasonable period of time, we will be entitled to terminate the business account and all users within that account, in which case all data and personal information associated with those users and the account will be subject to deletion in accordance with these terms. 54. Since business accounts and the users within them are entitled to unlimited storage, they are subject to a fair use policy as follows: 54.1. Business accounts are only to be used for business purposes; 54.2. Business accounts are intended for multiple users and are not to be held or used by one person; 54.3. Each user must comply with these terms. Any breach of these terms by one user will be treated as a breach of these terms in respect of the whole account; 54.4. Mega will not be liable to any business account user should the actions of another user within the account, including the administrator of the business account, cause any loss or damage to another user within the business account (including by way of deletion, amendment, sharing or any other dealing with data or personal information); 54.5. Each user's use of the business service must be fair, reasonable and not excessive, as reasonably determined by us by reference to average and/or estimated typical per business user usage of the business service. We will consider usage to be excessive and unreasonable where it materially exceeds the average and/or estimated use patterns over any day, week or month (or other period of time as determined by us) ("excessive usage"). If we identify excessive usage or consider that usage patterns on any business account indicate that any of the usage is not for business purposes we may suspend, and after 30 days' notice terminate, any or all of the users or the whole business account, in which case data and personal information associated with those users and the account will be subject to deletion in accordance with these terms. Examples of such unreasonable usage patterns also include: making non-business data publicly available, adding users who do not appear to Mega to be associated with the business, and uploading or sharing files from non-business related third party sites. Refunds 55. Unless otherwise provided by New Zealand law or by a particular service offer, all purchases are final and non-refundable. If you believe that Mega has charged you in error, you must contact us within 90 days of such charge. No refunds will be given for any charges more than 90 days old. We reserve the right to issue refunds or credits at our sole discretion. If we issue a refund or credit, we are under no obligation to issue the same or similar refund in the future. This refund policy does not affect any statutory rights that may apply. If you have made a payment by mistake and have not used the subscription plan services, you must contact support@mega.nz within 24 hours. This will be acknowledged promptly and answered within 7 days. Recurring Paid Subscriptions 56. Recurring subscriptions will renew indefinitely, either monthly or annually, based upon your chosen subscription period, unless the subscription is cancelled prior to a renewal date. For recurring subscriptions established via mobile apps using in-app-purchase platforms, you should refer to your app store account for details of the dates and terms of the subscription. Any other recurring subscription will renew on the same day of month as it was established, except in cases where the day is not available due to a short month, in which case the renewal date will be moved to the first day of the following month. Cancellation of Recurring Paid Subscriptions 57. Recurring subscriptions established through the mobile app using in-app-purchase platforms should be cancelled through the relevant app store account directly. Any other recurring subscription should be cancelled by navigating to https://mega.nz/account in your browser and selecting the option to cancel your subscription. Any payments processed after an effective subscription cancellation will be promptly refunded by us. If you cancel a paid subscription, but you maintain your Mega account as a free account, access to your account may be restricted or blocked if the level of use is above the limits applying to free accounts at that time. Information and Privacy 58. We reserve the right to disclose data and other information as required by law or any competent authority. Our approach is referenced in our Privacy & Data Policy and Takedown Guidance Policy, both of which are subject to these terms. 59. You and anyone else you give access to are also bound by our Privacy & Data Policy and Takedown Guidance Policy. By accepting these terms, you also accept our Privacy & Data Policy and Takedown Guidance Policy. Notices 60. You can contact us by sending an email to support@mega.nz. If we need to contact you or provide you with notice we will email you at the email address you have recorded in your account details and such notices will be valid and deemed to be received by you whether or not you are using that address. We may also send notices via any chat facility or internal messaging system we may provide. Rights to Third Parties 61. Mega Limited employees, officers, agents, related companies and affiliates together with authorised suppliers of services to and authorised resellers of, our services, are entitled to the benefit of all indemnities and other provisions of these terms which are for the benefit of Mega in these terms. Entire Agreement 62. These terms, our Privacy & Data Policy, and Takedown Guidance Policy, the terms of any plan you purchase and any other terms and policies expressly referenced in these terms, together constitute the entire agreement between us relating to your use of the website, our mobile apps, our API and our services. From the date they come into force, in respect of any use of any of our services after that, they supersede and replace any prior agreement, arrangement or understanding between you and us regarding the use of our services. No agreement, arrangement or understanding alleged to be made between us, or representation alleged to be made, by us or on our behalf, to you, if inconsistent with these terms, shall be valid unless agreed to in writing by an executive officer of Mega Limited. Last updated 15 November 2018, effective 17 December 2018. MEGAcmd-2.5.2_Linux/build/installer/uninstall_ico.ico000066400000000000000000005362121516543156300225510ustar00rootroot00000000000000 ( h   (00 (->%@@ (PfR`` ( (@V%ޖ(   چۃ  T         S S           S   !  !   ##   & &       '!'!    )#)#-&,&.'.'0)0)"SSS      S&    !!&(0 @ [٬٬[ S           T ܓ             ܔ ߋ 2,        2,  ߊ V 3-        3-  U ""[$### \     %%%%           &&             '!'!       )#)#*$*$,&,&,&,&[.'-&-&-&\*' /(/(' *UA;0)0)A;TB<)")"B<Q        R** V !  ! !U**( @ >ڋڋ>bb(              ( I                   I I                     I (   eaea            eaea    (   c^          d_    c   c_      d`    c  ?    ?                                                                           ??bminicnioj 'njoknjok(HH@ >!              !b          b >!      ! >(0` -?ـخخـ? 2ڙڙ 2/ڶڶ/ۏ                      ۏ%                        & 6                             7 7                               7$                                $     :4zv93                  :4zv93          ?9^Y                _Z;5      /     }y_Z              `[zv      /      >9_Z            `[;5      1`\_[`\`\2a\`[a\a\a\`[a\a\>           b]a\    b]b]           >             b^a]  b^b^                           c^b]c^c^                             c^c^                               d_d_                                 d`d`                 e`e`eaeaeaeafbeafbfbgbfagbgb>gcgbgcgc>$gcgbgcgc$hdgchdhd1idhcidid2JEhcidGA/idie/KFiejeHBHC}HBHC}HB$$775 7$$                       /                     /1!!!!!!!!!!!!!!!!1** ?!! !  ! !! ?**(@ P6s٢٢t 7OڭڭO ڃڃ  څڅ MMܓ                                ܔ                                    &                                      &$                                        $                                                                                         ޜ                                             ޜ Q       RMWR                        XSRM        P        ^Y                      _Z                 _Z                    `[                   YT_Z                  `[YT           `\_[`\`\ `\_[`\`\Oa\`[a\a\Oa\`[a\a\                b]a\        b]b]                <                 b^a]      b^b^                 ; s                  b^a]    b^b^                   s                    c^b]  c^c^                                         c^b]c^c^                                           d_d_                                             d_d_                                               d`c_                        e`d_ e`e` eaeaeae`eaeafbeafbfbsfbeafbfbs<gbfagbgb;$gcgbgcgc$gcgbgcgcOhdgchdhdO*hdgchdhd*idhcidid c^hcidc^ idie$idie$O^Yc^d_^YO$$$$$$!&!&                                 !M                            !M                                                 $$ W  !!!!!!!!!!!!!!   W$$!6!s!!  !!  !!!s!6(` !R}ءء~R! d٫٫d ll 0ڠڟ 0 4ڴڵ 4ۦۦtt&                                              &]                                                  ]ܒ                                                    ܒۮ                                                      ۮ                                                                                                                                                                                     ޲                                                               ޴ ݒ                                                                 ݒ ^                                                                   ^ &                                                                     &                                                             l                                                         l                                                                                                                  2                                                      3                                                        0/lm   d                                                           d                                                                                                                      #                                                            # Q                                                             Q }                                                             }                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            }}QQ##ddk   l      0!! !. !  2! 2"!"!k"!l"#""&&]\""]]!&                                              !&? t                                             t?"                                          ""4                                      "4"-                                  "-!l !!!!!!!!!!!!!!!!!!!!!!!!!! !l ]!!!!!!!!!!!!!!!!!!!!!! ] Q |!!!  ! !!!!! |Q ( @/Z}ٝغٸٝ}Z/ \ؙؙ\ 2چچ2ققGٷٷG^^WV 8 8ۘۘML ۗ                                                                ۖ '                                                                  ' O                                                                       Ps                                                                        sܔ                                                                          ܔ ܣ                                                                             ܢ ܬ                                                                               ܬ ܢ                                                                                 ܢ ޖ                                                                                   ޖ x                                                                                     x O                                                                                       R &                                                                                         '                                                                                           ޖ                                                                                           ޖ M               2,2,                                                  2,2,                L               1+@;                                                A<1+                ߙ               @;                                              A<                ߙ 4                @;                                            A<                 5                 A<                                          B=                  V                 A<                                        B=                  U                  1+A<                                      B=1+                  [                   AD>C=C=D>D>C= 2C=D>D>C= 2C=D>D>C=                               D>D?                  D?D>                                                                 D>D?                D?D>                                  Z                                  D>D?              D?D>                                  Z                                    D?E@            E@D?                                                                         D?E@          E@D?                                                                           D?E@        E@D?                                       /                                       D?E@      E@D?                                        - [                                        E@FA    FAE@                                         Z |                                         E@FA  FAE@                                          |                                           E@FAFAE@                                                                                       F@F@                                                                                         F@F@                                                                                           F@F@                                                                                             F@F@                                                                                               GAGA                                                                                                 GAGA                                                                                                  HBHB                                                  HCHC  HCHC  HCHC  HCHC IDIDIDHCHCID|IDHCHCID|[JDIDIDJDZ/JDIDIDJD-JDIDIDJDJDIDIDJDKEICJDKEYKEICJDKEZKEICJDKELFJDKELFLFJDKELF2LFJDKELF2LFJDKELFLGJEKFLGLGJEKFLGLGJEKFLGGLGJEKFLGGMHKFLGMH[MHKFLGMH[>8KFLG>8VKFLGULGMG4LGMG4LGMG">8LGMG>8"M?9?9?9?9L  &&MMxxssN O!&!&                                                                   !M                                                              !L"                                                          " 7                                                        8 V                                                     V!\                                              !\ G                                             G"! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !"2 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 2![!! !!!!!!!!!!!!!!!!!!!! !!![33!."Y!{    ! !! ! !! !{"Y!.33PNG  IHDR\rfsRGB,%fIDATx] tyJz!B~HM0! Ƅ`0  !$$iBMۓ:IL&IllӘ@%?,Ǯs+Wٝ{=g5f80#0#0#0#0#0#0#0#0#0#0#0#0#0#0O>*9cFVF[@B3e9 DL{}# a\l4ZJr:t`%D= 'yX#*ow"i-vB֊[Q@XZT\ȍMͧO3uh?F!^F 2Ύװ!"$F gI?y@d9Hy)=\Nc ձ{bֲ4eX\ E4k 0"A &-eIy6kS5lkpC9 dɵPe)SJȂC`p;j~,zSmy=gg"IdSHQ#Ѝ= f`+ gGq۵H;6pЉfjW CnɦK4'*o S^/vwtbe> o¸Ftn]*9 ON]gVѾ2&ڔSX.m$~ޒ\=ؼ~l3rY>0y,3#`*W|reV$Z̔ɯ d4tm +#bAM'0b9_1]0lJѹ`_eu%3Ko7aK9+VS6y=G@4(|3U60z&Y=@a$dbώ7 4La5*ޝVR5&,z =eY&5 K7ZdZr#nĘ>Ł(l;L,.nމfo^}z'F@ R%$LZM%T@3I|<HdX@ˈSsâ6b&/ {Cah3O:O0i]t0rA6Ȧt:?p5/j"Ik^_gS@쓿2+/뵩pEK :?Sڦ9"X?):;L\@HgA&/QY * 8@J's8JM 0|~KBbj( 5m֍L 52L6jڼR bXTubŕzC Ȕڼ?}byV @̦QPF&̯KE|=@o0&v'Al Yd_Tϡa܂XB,J齣P 04uU^3N |t2"aktTd\F_=>S4*r+!P(؏3C{~>mm3lضI U 8}a> `\ҜNoeg y7{4 sIU׮o PgEDUh~ư? |2TY|8/t|9? ;*3}mƯ1wl򩬀YaV @_I355[x̗Ar4Cq)WŦ1upT)0L$eXdFomS6G-$"fub7ogBj7xֳm^}E@/R<| %W OBroIžMEę }ȷ=*^] =7:`\_.Ǟ!ApB~uս9o{'H68WI}KhI`#:%HNu_Q'o]S17?vHeS"I#8n2$5-.r A:AWҀ_)}|=ȑ8@\ȹ'nT]Ɏ1@NXܱv:#:+oO `k~j۟‹caSa#p?d -4WWuO͚/}hpϗ&r}PΘQL6ɡJ?ߖ6 XHs+0t.6)B0Wu~b -Jgx\;w&t2:o=D2Jo_oѻFjBpE~t#HM W'VTo.=_\$(W$LǓ%1׽ +Vzxl4sW=d:O5W}y1U' _Y?04+/,g]dCCmBVv15Y7QdlM7C/-/5 `/} $hL~BtWT7?W @vSox&MU<}9~ه ~Zx 07 yp= ,c}Bv>|AӉ;xL$YUj1\JF?oWP|QHLj:e^XnWc#f&!ON8i78 h?.npG-+#Cz< (T.LeӤ&(6V 4fV}׎7qL cMc͕ BuMR1M~骐 WE*jOv8@1q#)A1B~@: rZX7?50(MLpCRKH.~Qeo/n&BKw'O˰'p??ڞ IX],crc\VLHt$L dK/ˆ9ZfNczq#Vz%Nk$)%`.۷{cƋ@'rRC50H%_m )`UTm?Ei(7M PTdF`XUL~mMvd2I(r7L~5#T|ФF|2 g8XwD5?6:{7m 4. ,fVNFZ3Na~U P!b=I900;WEE[DG8f+HJ )l;K&-9XeJ ~XUփj4&,9vʒP?}(R^EzƁܬmIJ NV5~)˯Dp|<jغȸ1&ȵPGXS.u9; iOؤJիqٰ]z=؄yW_n[ptnJeu uU$ ,#O@8=KP@8\+bBp:)4;s)BjA1L~ߵZe\S2ИN& D:ᩚVTg}f-E+k{zfH;["@_V姊sJ D0KTKɐsJJ bfAhfj8d @TzW ?'Ugp][O;Ҟ%/T?DzlxzU}n1X@F#_k4PفQOEv8ީ8;f̨b;UÑ $o`DA8#Tȴ8F4AOH ə5\`U-af}b9d?k6緣 ,Ef/l9I #ЇFV/ 6jEMuZi9- ]~3JYHأּy(.dJq."_] Ꝗ޶Bpz{0Pm,8 Q Js##{e0G"/^4>~Z9A A\<{^t'A` aq0m mFOL~Ouhx@귕 e[mE,ɱUȟo@'<$$Q=`&L ]>#"M~ 469vpϑ @t펤04lF#qN0| `N?ئ Lدq`uPH?=ܻ*̓`uu+ ꛄS=G $,e/զh|`E˲iU4>t %&vawV3<08v{*=(3U |'>?t}0:aci B)3R4A%U6kMT7nOTfTYmb}Oe {yC{vL.jJnf,.&O޿Ҕ¾ JOz]eL`;3"@SE~]$ Xd1Os&>=;0vn!Y3{20=&ktntJ>FcOhW8)g"#uoi+ǔM6). -~ܨO*5PnTFBBQ_]4%Xgui҄t$O8=^蒦< mSANo8j)iJ(mgUh3h,jJ eAy2 h%Qw`__myYy9'R ׾G`3|Uw#݀mx\r?;0>F6bt5%JXJMbj}ъ=a_1H\A~2ٿ}?p˶Vw$ [`f@]bW a\+OɽvKivkv<6. %ֳKf l؝婏 0 )fM pnظ,`oF2>߉uGo%B r2Aũ&./haD tI5cJ1 T.Ap*,p1}-4z/"៍ZClchکiH-Hx Ȣ#e=ʼ 7lBƹ}L%\x -GGpEq\܍D߆9 |ƛIlWZ g 6ڠu/cڣV>`; ;8c5z1Wt ;= Oߍvt0#F`F`F`F`F`F`F`F`F`F`F`F`F`F`F`F`FPGP nIENDB`MEGAcmd-2.5.2_Linux/build/installer/web_ico.ico000066400000000000000000005440051516543156300213140ustar00rootroot00000000000000 G (ͤ`` @@ (BA00 %Ń  m  hPNG  IHDR\rfIDATxxǑ(\39'd Y,+-_~}p}w9I()f$H$A"yw^Wl`6 E?b'tUW. fafi;0 0 3`fY0 p,Y8a,0Kfaa%0 0Ysf ,9 `fY0 p,Y8a,0Kfaa%# r`i Af2,>ٯ3D\`Pf_N*l${ÒKw(ua6h{",#I~Y%g!TMQL`\٬RÞcAxl?V;P{ a,0 9P <~?t A{0'a$p nO Ȯ3" Q@N0 p~{g0KN#}sdžWzՖka/Yڸ.,/tv+ 3>CS t?ɰ\B>?k)?C =倜4;P^}~ǁq׈a0# a f͵ԗ-84\Ypo|!ɲ#f8$U|D\D X[[6vn`[A !i#6,1ϒ4V^ Nv 59X.7&+;[N44s㈂ AUB!H?l8X2]9e3 ?0KN8~l`3|W0aPf瀈ȌqF ? 0OKρq9DA7V}_Mn.,, 0}OaSDlE,fWKG1ADmvw5%]S_oqY0ױ{ɌZm:{WzY*,t~&e f2r$ YVI]R?uat3fAyNTeLFl{I5^v2QFqW}}N~܄aF  {a=_Y͋t,KS}OG J8pC+Cmrkz.;,#V% .i@D9Mlv. y+WboKv$xnAB ]$ rK0^ ?"B C~AIw|Z?f \u[oY%vKڊ)kH2!{d!BʼlXQU s8m(ʃ9kY!^^/d9{#Z`\vN?=R dě["s9 Z[$, &?@6 8C` 3$vڬ$W"Zx~_pyB9QHLtxQ_P|DhАX|%_<Ǖc}_xV`ox)7j3=Y%Azn۶W\vJ'F{WN)FGlr|iEc]$sBc;}m7P@uGaL.FJ1w +PWk&*af7p @@An$J|$u!b*Q"h@a;:vv?J]w&& O_`2lO6]2v I]C|2k8!-l5 ]君F[!?#(XT6y9Iavٵ M05S!\ `}vuF 7r*GȕaE8/hJAme p>Ƈ@kǯA&4;23v_|:,Y`;tl'bȀ:&66 28J3ڡwuy!}HWlXW[m||n3YkxaiQo69fZhYH{c#W ZAg?W&3߸WND9aĂ⦭ s33.N_`Y C6VB|YV,[}GTgCb?m2#"6ԧ>H4Bli䢹k~2xFQ8ӧ ;mQQR*D`ob`4*)] :?|vOu|`hǷʮ+(iu[`k< [1M y%vZZp:1܀Đ* vNjY&\cxDOYN|R&E=h? 0y{zNHe"߆#97Wv.@v n͛n\aI8=yi0X = Q@ }n#" vIo:#c&AuFCA2Udg.bf6w]L`TVvMK_ig6eu)9!&u ] ЏwKdA$" )Ʊ81!GؽD"xGD%!y2' ug+.ɤ_,sH|ɂ nMsױUq CrpY5"vy.:9`bs91xb+ gV nX1?hOῚXdWU# 8Y ˅%od1 &qG=/h@/Tj: ma #~b_#Q7nAuމc[mK5\H*ҋDhq~ FI!"=,Vi9p.>tQnY,4q'EgK>?AK3{uM90暶 'ס`A'OO+(f 7̐Qe?ji&5 r0?&vLxdo F.x88ػf8DT]|Wqƾ\0( 2i"AcAVZ[&&Uuq'v"/{i.Uf%9P45i=/n[w._Ca&~z:! r2L-#"$cs\qh$(6VB 8z_aM@D690z_{o | cI~ߤ]rޅ/Vf]$睬j T!fQ\ AKN@/5.$+:.kx~|Y˲Gڙl?i;="L%F(+W@æ%`bH$:-+/yŹyŰڛ70cp湈 cNgLzÒyq!͈.KtS:Q QC㜇 . 2Dw./P9aKQvF,"?WYZ~0`\^Ef=0A&@͇ ]^Yrr/vcK%8TA}QNGBp56W-%<43Xg$kvq/tEu?@' HעBl`AŒ4ˁ< hA3ǡst NDb2X0lRg4B#e档 hҙ$jL$ai6ѱ0YJ }S**m}xc1hc=Y3Kj޺1_Xc8B2?鱽C ӈ+ΟS;ۺ`og7E^XM&cÐnW~:/eR82բOn2hȓ t6&S>޽-8sL:a_@~%xv8ٌ+tI}2]N~YqFjŨBr<҈@P,F Kr+PKlt:ֶYAg2X=JmI1Cz3iłl`Iqpo{j!A^ Q4ץ 5QD7/Q B#"={`2c6a=:{/W3{{o;. +&Xgx1yZRUK+ aQY͡F#7t {cGkϺeg?1{Ū+$)GC^&ϝ5aOSp'frōw;,;;%Y*ҢГ;`XED|߸a\*1]ٌyc zHƋF4^ߏ+CPz8@zP;^d9EMc#ece 7@M^V]%Է;ff6<iJ܉@8M_[7~}=w=:FoNMqxȏYkTݦZJ!E93/侤 &=:8w.(E679K `Gl>`1-̑0_@ZX&[dq,0l!Yȗf:8Xd63n˸m -g}ANB_yW2H[셣FMPCp(Z"4#G:7^|gMXܹlsH'<{a&;t ke^@ar^>#0qÁ=wgh5,,Ƀ#}ʾUrZck BҪ1񈀐vKcTwmI͞%i:jW/U7.A~cw{vah(ʅUHFPW]Isב]V*GR$ξǯ\rBWIe^η\.L>ҏ{mLli#y[W8/)J2ҧN&M"6ewBn`$nӋkƛfjb+Q\9Zަ7({$rJ>t7"߈t]|IBLjD/r\ָ] F%EV~} yf l 344bчjr l(=[nL H@,C @ 2~{"*QGa@ BʓGB6 2c=N'_hp6|Z-(h '!i'Q%jYˆ,  &1 JNŒx|í7vE6g2H՟X{^Svɑ9?w[vɯDJ\6$xg:B*q>m?qmF!f~-EL(z -p.D-9shgj鷿=2d`}o׾sm6#?E ТzrGi'#'Vb5C<`H :O<!+cda0odK 3E~RinL,f x-/"aЃJaiy$#6n)){{Ӏ1%Z* Dп}Ӭ`O?]-D]R\n h;}c~x@+; 1A r7OIOq?m|B=(0qgiuLHG_ n,K<~2];`㉐aBJ~]4UJAD,qkkJawr DF\ LF"D=ƻO&CT3vcLYTAHJߕBێwG\҈7HޕZ4Gor_fRb]b0h&AA?mOuU[,F{ܜr(RDALku›f90͹Dd/(^&8s|9~3C ccs@{3$2>?wMw16B 7i_N;)qA8lCTyU%F}56[oyXGF8D#;_#a>k8Ý7QD4`}eGkrQ~Ch  k:14@-#Wl.m&A20r0QW]}nR/CxHj_Ѹzf1* ?bUJMn4ݫ}RjaB700'>4kK܏4mrEOf}"RU@1]vj^eDݭ˲3)B˦hX)Wnf瑠v u͕gBzkNUI関\S_(cTp$ 2V౏S&`4XWUFa_1.kr.0:sKu`f '- ّl$Oty] Ƃ|ZF̆F Rif\PWVB WP E{p{|ϝg nز/ (~E> _#W&"v%u @(ht շ!((R@7Gqv8qL\$xP>$ QDT8!]Żo81 : ;;{}0^Xﶶ5a)&0ߞ^I_߸Ip7ݎz `z -qkȏp > c>*~Q}%Tf^{bpNԈ'D%Pf}PmPLvR?VR\:ET[#V*=iMrgŭ;Yw+&[5_1Lr̮3eK%ټVcjm>g<1#EbNP~&scN>,΂k)dCYNixDS"sbd_d@  E+ \To/\o4rH_tJ b9YӣK4~+?ᗖ͏t7'"qMjclIir:*Ԃ*xm!% ^&'>D#06nH Ъ xD<ygw$g+Ja.zrOrSd'c 1w(is\PN,?3tͭ ) ӂc $\)鬜d|*ψ׆C VCq*EVPyrt; q LhF,ɸ,b%SwjI"}lj:GTbY3 FAerNpCk>BR/\&b'}C8iVVJ/BX= a !wӦVF㱀|lr )64NiOO*a#VGyFTui?D lZh[˸B]0mzRm9aH$ . Iqg[{靦.&E,Ti;4K R~9.e'-f 7mY)P+*`W?#&]1Głω-;/738Q$$DR[e㡊1 Ӣ6o\'< b@h"$)U^oPyokxAw_).~CN?ВB[}Ѻ~Ӵjη!/4@m~IcwQFD۰vfU=W᧵\9'k'r\od׆X}Y3rWTw0Ω; =Gl)l~ r\@$M] G۵tt}7{<}?@/}a;uG%O)^EU2bЫl_W7,v*,\Sϫ"^+m;8iqi3 b5+H'Ƞ'>yjz0Lj: =:P\x&"Db7O͟{ Ny뭷o߾XVd&e{_W9Џ\Sd~Dswޜ =߼lE{a!;p{"P1g$-G)u]hF!: @=wmaÆSxgż~=nۢgђ|ۯEvHWI!P!H<ϯ:`gv7Q 3܄yQxod~QOXRn+)B"/:ӝ ƒē!F"Ѣ^^팍yÿxOϼхyzGȭ@Ve}kMoTNi#xjg3PW9YP jϤp&#a"Us+5[_|f OuS\ }-N`ETᙄٹw/NaS:+ _|oSW^fzY{q7XA~".*=ȯ<ɡQ}|͜xvYO0@nЮ]_K,+32NrodTLŀV bg@՟DfýĈx=P\7Y0"{q"/@Q-REDwx /B)O/rr: ^(j,h?xLvܸ?Z/&4+ChÎ`.ZDaaMf_(D}n e%P^vLɎr wEKL /kfـ⪬͟}|_IDl1WV6~{[ll9NQj4/@);W00pNJPb/l`-SJ|{Ǿ9sH8ˠ#31鬿81HgJ~]Cc~1lA*e rod|h 58!ED~|dxؖW Wp9J3@*tv\(Ɍfk<hϢۚuNqHT/.+"N{bs XPV0"QWxxqN幙p~U9% ;{^bbvy 48U@2熛nhʰQ-OWjt_;0qrGO2\W|d z[a0UGc:r@aDwCN,iN[J{˙m'^ v}NHžIM^6\0b1K1\/x$ݙG>*O: ܸq|8;[ /*`N~6D Ao: 'hղG2⟁ߎOGN,OWu%_Is;ny1K$KKb^&&rP1яn&\s$>g@g$\ l`Czsj6t(Ӧ 8g0d*-g@x:t%gѱNcS LB+ŅHJm? =i6;-2e'4;w|#l^)u`D0 a#'HT_KJ8Om=^?+W^wp Nl.**r?;2 ņὲL%DDp^ ~0K7B>L>h~ "0"(/ߠC 1dĴa*ɹ &mƒ۬6O,ӲF$2玟SrjҲBXVqDW>LY+st+GLIq/;DY0} jbWaƠK-Fo{*ft[j[vU_R]T ?٪Og^JU}ȏ@孜H„'5G,bYA+S ziңgk6ĺHWxɰ3TsWbvx̶6]5/n10<{>O0jٳQg`py~|ebwѭL26PVbh1l=4x7V=N,>?v;DZje옺ǿc=u&<3,J4Z?'lɅ _\{ȺqhGj:!FT@S޶bdFݲPv5@jWW z:h??a$0e9:QD/\P_ʋ #$#T +ͮ8)nf1`0 U'+2:矬!44bʽ~&ѮRi685yD}*~=S ʺ$s^<MLo;GH$d(^ qMgr8T mc683L@r<\5ric&Gda[p=e`h A?H.d"e➁'a}CFF tv~r0bt_~]Ҝ\</AŃmh/) ?TSGVK  KtUV[ϑ$Ud RsDRSPTq{s7"o*fSX3?pt3h:k\ibm;|q|"#RH[y5eځb`p+x̘*?`.}^;^ j$6,'=ܒۇM|>qO󿙖&q>h!동7`?B o<ډc E@|OI&Lމ \"Y<y.W5ޱ# I&6v~PJ"Ss.bc>A5?)TBFl QFC MqBo"FP7^;E9eXWS&=< o5%. CF<O_0brׯ~Ңe?Q?sV kfJ%[dr)Xdx:Tmd%HI}zE. +UV[ͳЫ79D4!FJ3rWC‘FH4w؀5 0ħ%E,hPYf X@DeeE0@p R9t:FIp3qs1E^ XEh: /ZVk>Ȯ >9b+7?oK&(t ǛE3p"ujՃ|8~4LGYfcP?!*ΟS3Db$0.[ Jjru2J"!tɀyE am3<N&" Ӣ@|lǻy%PsP=0ͦc_{9edgHY@3" Ɖ)#a.rZY9@7hDFD H5HG1hGH'-?Pd`ay>3bw{7E܊z/AWGZK4=Es0f kƼao 7 5:kown~L1[jQmݗmf#@t f"&6,vԉamҝ.𳉜fO/#+pnHFQN<-ߕr9!'aȘ5PCoG|yi.zQmIƍ43BaG! ~]tbǿJ@taoOUsFK? w]m&=͎|vSCeIR![xsߌVk3)k/=d6[ n:S*(_^Eam}W4=ΦYmCAeT}}22*X,-/bB~R\U)ÿo[`*@#1+O7k['>(yYpM^ LKpta VZPSb=*mmpQ&"M'%WgNƶ'uY ([@9HGxfA~RMO4 _`Q_UlobbBP8V_V~sla@B2]뗨DA r,/Gbjc'v%TZ>&+EhWo{r?}rc `K\[WUXQNVTWTgY U5˪M}d:$^1τY9kBx&&fץJ1 3y[d_+:uVEHs?۾ #77 FS04{.bx |pE߆& fA +@R0lbBTrˀchz+M0 z"?W4rH\HY~PU# w(,6QV^cHTI&rThFHof5CDf⍛ >YB!@o%!cOވx^gb@R%(=7VYF_%KYPOr3>_yao~/A0]tC'{((NV %t,G^}Y͑΀uLJC{{b+:hsPy(D+~dz9Z [?쓤e!"^C4\6hZTi%UD6c{_^uk>>r0-n˻kwoWS^G@wÁVry !nncB+d}}y#d]0B:#XVY%`p"`\u/H Q0'/izU!ѭ-,8`׽SVAE׬}$7[$]}pټX&)"s.S.UB(V肷c 9*D0W ,; 36FIT%u @IF:T0K-䘴qJ@>]G-~nz2"b5j|`;[{=*'F-GlAȽ7Bހ2PT3KJ4})0xo귙9+.]dKE9V257kC#[i_>in ,(ͅk*ҋ}|ksNHg Dc~z_[N+7y BF٪=zmFsԮ"ҢZ,&݄19 >m :Jx8q*Aҁ(*®m(̃jb}uBnPG@&pV' G <솵?7wLLҵks9$l):ɣ @i2{Ja,<`0F8ok"%L!LLx$ LhD's&gR 4NTAĚQ^BlX`Lל̮ 8vXx87Au<@hHh`j9"@DxϽ)Z=ҽXdq%\Xf[VtEV)M+CQV:mj#T&h&;@;ğhǧ6KJ I (Io$ގ.ٗ1aG3 {|L)P)-q7$:jtdDbP DTw/"-2C/lۂ/ MFeUS%PTT F9G~$uB $)iPv4fvN1yq%5uYDǃD^f|7]_]eL&F͵$/t?ɿ&Aff\8uz3Ã$t4cSV:XTV uK@D+7O)tA;`h׳giV\TW YnMr>Qe'B7nK = 4Pq7^ IqYE1Tn()N j@*$peKӁO>#c7?o}+wl./)@?;۠!/C1^0~0 M/,k'v`"i:!a^T=s*mԬ_4R Xp]sDj@T<V|f2:z |4s-fNI8n{:~l{.S v[m[EQ~:0~tAbAHeEglXՉ #:pep"D~JEU$Mc5e|O&UzL%qbcGt3}ovQiOo ¹PCt!Is՞ƋjD'=_jJ)}Go3HƂAx|!v# J` /8VWO8P0յyZBZ,.'\XE~6V9/M~l? GGLrP´d8NETe<Mq;H ?"RD/FUO痋:` !@p=zL!߅1*׋$'w\$b!Q/A1AS~߮~mc6?­Upyeـ)=l`~a\ȇ>ObE8 0]ms;&k2YS>FɸP1ȟ$x:a\rQ1a_U MݽDpa'T&@Ee1sK/\/ pxr[k+JH%PH?Sکb]?!6IپjQM`AGp%jՈB^U1c(SF.cQMޟd׉R-=>N'_107OQFgb[QFFq ~zk8/O#XO\cR$mQ 33⠖bb-3% F"ћ*5L5Fͽ#āܱf!a0_xo~z] 3Hl^~y~"Xzb@`kla2R9Y&Z6QED35ɪ&Zu{x}f\I<o՟h_V@nZO'i . ;=OHqΉ ^R,! Ѿ!LlgQ9#mB_r0-Bg[,{s׭߁)8M}/_ꋜq j& I5&r  ' [)'On"MЩHTTn~mrOO>]{3 h:'ɟa$Chq]46^jP쇴%OjO(#cA}|T{{U3T'_xˮPsKD RbNUdArJ VsPJ]Yh#QpDv* 'PB E7Mz`vX]YL1J@btڜɄ+!<xn~S0%B7.Y<$svutQ,uP>eIZO '-pV7u38jW ^T<6CMN6 7?IB)fWXd Gc+08%6>*? se J SϨP0:JJJɀ)mDMY;:$$3]/ho(ei _,iNk mu:_~qxatU揘(}/e&z0@7/o$o o?s ?њWJlhŜBfp;U/X\KK c/ã;0aY9؜t7\&'6Ŷ\7{SMO)x֢cTMU* RR';!i_mncd%Z2hBi@/Gv65zFe?XCڴÿ*OY]l=Z9[P FB*jeofrQK6:IMu~Ny) +\R/2&7L͎]f6YR}D?DB )ڢl]$L3'$.fi= \:g7 oD8v'9,~޿)_9\:|Dvo0 Uݔۓi 5 y$'0Ǎ,rŃd5Y=_Dx_񻬪,YO80LIfnXE HgǺDB U$k?/^3h}vfH< yn79`ye N Z5sx`BErzdkMu:!d6& K aqIƨh,T揭eL̄ d{H$urS #?|qANpjA#[W0.,`ߍ̬Fpp3nBV{ 7l|^5@q5Ɉ봠',zqbgvcuKHO~a]P(IW';n[oz} |>fT .,-/uTHZ{}-vRkat emӠ4=9+4MolbU2J=?=M +=,B"@9 wpq1[^QDq4@@ ok1,stNS;^m ?ES. 0v4W #/; l,X*BQxO6Jsf}wd.6D'ܰ?9iNqEp6.")(c !?}/8-60q9/h0}Q)h :FfAÁ2NЊzH*_918v=s'G #XUqKyAxnaJ-_,šKD0RɢٳR\<0 8epeCm±0L9㉎q!׫!,@bĮZՠ0)o_H͞M> hDor_2+[+J"*Hv&j~X)V} nH_pA]ը?>4Da">Wk߾z?88Qc?H~"z,6~=hnt~/8?uLSzKo]ij,`>ҚP!~?j )'4 ґPDIS4" ?@I QC!HHスvz^}$m,H y0@FsBDF*0I1}zЂX!v {no"Hzʜ7A.bF̻/K ɣE+K~b0kuGIA,Srl\ɴ_l}Ā̀Hz Jo1:7@czH 8!Y0 &!m-ZO1w|-ExnMKtVT@USil_SA!%exዕuR [YD}Dx$&h%mb`m[F>dj/o\$B%Zf;\TSXwkgj CVk(X[9(H,>DO8pcOHmx\g[H M2aѳG2xuFߚ+6d^\hBO0:n()#+ VZ9>5cO[ iv:Cah)9 lIzE)cqW| XX{?|uxXǕh xɣ^:thKQ,E_YϽf%3"Z\nNe!|PvjO#VᴱQ҈W7@3J'/4VI,#. 4$M$ūǾ4pɣ+ ;a E%L(85J#N? b_{?_NHV#GЗTM@J/\4~vP@ìKJ܀ ~SHC.2uD!%6i{`SQQcxg.9f!w'(&;CۜLg10GU_^Iq.bL# ?Otx:tTp>)@ef)~㗹!2lGDāI-1@+l9)8--f %qwگd>NˀfY!TM|4 5[vAHCnB3V+:܁e*H,!iF?$ p c7AT}Hc)^/hy†@rED qɴDŶ=n=-#KDu&td?*up+N8z 1@O]FC&1!,v+ + 1@@^h!զ$ݕ$},]WTV;I}x) 2M60[?z(Ypm@Qss-Fn|{>xÈzT55|6NXORR!4Zy 8% .0kǧ?[F̳$V,@͍~njyQ[KbO+6w߰N_eosDGA$adq0ءj#Yx&{CD+-X6^>=~ Iڹ E@.)\@g\N?u \rHHHD`$hZL8a܏\W ,Z|֊B*EE4_ S#,wq@lk}3o s-LQs"Z-|}͖/r\B{='30%¡!Ev˩ײ׹HPFEY{-;h=j`Hw]<1 ~h$$$wʌkz1 nߥ 4gP;t Nx=q&~>ca5FpŮ4/>qngpJו Xs8<Wa?O N9hBTXdxmEb`fo|i\ n\6 m~(dd4?jyٗ\.B8d+^IyyT1Ei߇ qTT,FbUUv$]qh؀!Pvq,>f2vv8h1dF66y!lٻ,ڲiIB؀d4 R$xC^d)V]{ 'ڹ3GUCMGX5q#I"\68b.YP-?ԐW>$24Gc`As)ci 1Hp0s4x^f>$ "pfpV$F65E{3s-.EJ\O/uTv-b#4=H)C56~n8mZRcuh BF 2Ptkl(8UcCf+C.J.OP}5I)wy qν[jY`T_WagtͿDIӔZEɛƍד=.s6Tx00@Cq܈+ ˲m\j w3]Ƿen/_;B)rW6 [CQRck,т $@V>@ElY7VMߊf(RSmkwp>_ X>Cb!hZWhDaMhjuޜ|J%E,]_b+j{a m3@> h^4hZB\'V0\t8 yc(hlP(eCFlOTvԚ64翫4S=zWGق9vFW+5h`$_hYK%PᶃEsԓ{'v1[5^<|nZ$h DE>y@_(P(d,S9RQoމDzSTGH,Vxʓ m~'\q:a࿵ MK z2UZ)2 )Qo//I^I튁O[/hRqKi&@>9 CLm(42 hD4Y0ƃ %Mr$F8ZK$ H ;&eTa@(j&R~AAWm5_fL)?[J"4qk^ d})eJY|+k|R~<:`f'|aFE{âvq|^˻օ{DA+Y8S0D@.3\sY3=@ȾqM[Sₜ,)MԿ )Kk{;:UPv(Zఱ'6eR7UiAK]1y\wYPx;N ,fRA@s?/ț?[ϵR][l< GVMBW4xa`.>(o6!v ^,aJ8ep_`V΋:"fH\5 nZZk]|[RZExvn#z-4JпyJ̚%ȭx|^hs3/ZN!X rDuعG!(Q3TH:7>o1J+ַySAݟLrC O]x"ID`NnH\ WoH2A}*+_u)X,"uV3\6ӂÂtdLLߵ]apI'l|c_TTGoú7#?*=vm s<Qm5]V`w vAszjݾ]۫9K Q=SL+ @Qhu0g`b^X[`m`$RY$H'"dr]ٙ;k(v ` p*Bb,FF$HxBҶ{qVh 2\Y}V܆q\}K=g:6Zdݽ.J$ԨE\2n\хQmrQ7 \s"]|{Zq@ĸ(b'x0먾g3?CI7>3w7؝E\ XD=eRYKT(Sa=j&1fT04yO ^0 $L@ѳ+.lT6<Ne#g.'x\%A U,5<D2:][7Fb4L=jabް)A8"`qwo?c2t +U`HZufZ5Ud /6ieYђF{ޅL^]WK-.`p,@7h$k}"LfFl&,HJ>Z67$":sy߄8$bd`=! Bư+MIRD=yp]# >N5DULs9(`Ps?%{^爺_Hsh,WO@+;YM dySbʪ=`?5a%5#t\͸)yR"bVK5[aPvhW@ԟ%h4ŻW׎=ڮƧza|EɈQc ).8Js-ORغ2J*pJBNQ`_sv5ahAuRrhVG\vo,[F[jUTi#bd/~tdTǬQ_ AF?N]qɊ.` g=npx:DiBEbR5~Ut0Rܠ"t{ v:)7+}*ٮkK[7RN6UK Can=שQ\67AHs0ptv|S9biq4tu)'Qhyd&iF,FrLb83JH Rgl ԇ"qDbqnpE&@i>fKGO&8iԟ4v5&Z\EsdH?OڒWM=?5jAJavQa@*2q,"wL˚ 7rR8"&3kK!*Co^=Ќ' CyAd#ZD]ו[7˟/ N D4pl\]rZ\>_o Yą)AP $rV"%n /H`k Fg[w2t!=4m$6.2_`֭Tߌ}Q};x/t=+z#rXsӶgL>fȑv{5=~+T>b:8{b }zkSoAV6"  ԁ%`0\%g o'r2V@Su5y"S|9ۡ9j$Qmqӣ7;7{JPt N\!\AzRh@F%1xԺeՑXSc_;~DzDŽ7[,'F_}D\fby?~zyqȘ@'s=9IJ(% C"c נҷJ +t^y/%_l^~>?IDcujKʜt"6{Pj1 SM.{h.L T mf,WX 89` KtʻjN*#QpY$6Nf3(Z-ULTDb[^HcTXٹj%Tz<[91 x>4xasڞC|տ%2A"|>:[p0f@=( e<_6,q''d%AGrrR3(t]c""v0xO}%p;l!~GFN*5<MWW!С6߰/xD,esqm#D"qtE~43-Yahk J&Cx{jXaY2>YS!dd揙=~ xYg'W_i|Z xqEp۷g LPTfYHkjǓػ֌e'd2R5H$74 ?v}X_|~詹⇢ծ)ۈ^U") ˆB,$-5ǦybeX+ܸzh9NYS+΂5UȦ@Lcپ10w{clmܲ}i aQpQLƥdg§)WŖ*0E,F,B]G9u\K!A4,+ m]Q_{_ ߬Xn  kZ:4$'l C3&NMKAifZ/m41.@& 7RM' UsaB3-\{vrmLˆ NXGouOU>m3需F2Ab# 1?N{XuTVB;I*)n%2E?њ777w:yK#RٰB%(!v=T}›=k fl|=0HS%ZE(_#|\h3#ՠvpDï=w/m1OG?Ü}5= 0O n9g (chjTj+`9=PH' _.U`G+as]i!?D 1GK#\íb(68D|ϡS`X qb!Ѹi1#3:z:m~bϣ k B0.11,#"Qn;R Kfxvh5~C4IeJZhPfnxvٹŌਿo v%swN]W<^S}H%ߧm|gH7h{rHJ*l`܀vDQ@JCaʀw]#!ZրSCǡ)pjhH7zwusح_`㾗WG~ ϛ-Ad9^7Fd5*};/VciT J\CŁ9,(<,c շ@ Ua!捒Q UF*L}i \,IڦDt7vlLXDOSϒeXCz*k@%z|%#oEx0,VwiF0bHsP2!z\ 2Sdpߗw[wŹ&5< 8׵a7a0s0,G u)P6"TV?Ix_!,}モo|=- MK^'hnx^ͪm+IC52La>0sTo/tNH m a㸐%C XlIJiޙ4QLy]6=d٨q~d:4='N/~a0w0`ayXO=EvYu_WdHqo<<{n߬cܙgpo/dԟ 3E\ Q)pZ)Om6Mc"NN{J|پG͏(?8N:ƎgT.FxeׅeϘiӘ7O[7V!PF}B j(dUgE>O_8NNU*Ro.uuM$>3@Y^Ϳ.d$xMUG?I&4\4 L6sRh!Bcg^=Ҍ-=d"Aq6 -j?UdQXs]Iҍ͟ ~g:B 2Vfcsj/3BZ%`rK!G VE)A2KK{Y.WK" s^}Ϸfڍ6˺W &bIC`G,I&2(ڃM11埔B׭KLA",Ns}6mJrݟ\`2AŕrfCzw'M23D e*oϔ3;/e[wD 6;Uэ0?^F_mi80b]zvKVSBEf?x ]ihBN(P3'E8vL?cH@, m p +հ|UXsl-Mf϶Y2ϥw_>"_?߽HN6N'K9 vkV̡OFbi8t^';{r!.ܘbQbœiPS^PY Os (v}T,bEbXy{zpy] H yN#pF]9R7oJdM%~A2WjDvRl{|pȬrU8"f 1q, 4ߙݪ}gI"Qڄ ./[XPO#bn p::{H)H&Fwwf,+(.1HPs}p E$FZNϙeņo=q=f̱Ɋs͊=D+(8곧q7D>+lɐZJn~Vl^ɐeP Ca3k&OE3tSyZ4oFP)!/#_#v-Y*8dkyw]s`x$)h%~=]c h姜C '$Iw=A21 F̲?Sbw^NSFҟ%Sg&v.=)%z\),gz 6]{y,ih1+؛CHe M?$A>㞹N m Ru?;i_]|ʁ9%QLH6p<NbcPum1A1r! 9r|xm"IDmO(9:[5*dJ@ʂ7?nz=ZJH8U]ʏ=QyKLսrƟofQz C~ CsEO{eŨq% 469F$\yXe風` h^*BDP3￷>YraV7 u|mkMpIc/BD 캚s: 7/2s;S4HxQx@Dܙ ((>\Q9E"BdK^ƿz":I?ł25T`TXN* =$9$c I F5OPzA"2 WmlB r\J6ZӶZQȾT)Ԇ>'3ՠOL 8ALp%,Vj ?%d!YВDqXn,^ vlvlv)lu'zb@N~4a]i{x>J$8b1G(8q0"@8iyh8{ɃT(&@֧G46FQBommx4~{2.~ K02ias7>Nt2v)yt3`U9&fڽ?mV!0\rw#Jv@O-4+A?tZNT  O`qf>f'?sJ1hw7!7\>MT! #0 |DEf`'a"bVc ek(G”ǚ& _/ġ*yW'C(\v÷"qHqX!A$_h6 y`SJz%Q~_/Wo-8 j/{b[Ӡ!v#KɸHSN>ߺYAٴNS1o>mzR|͟ :{\JJ^sxڐX`c LfNCfS/FRR~3Y7'Wͻ@BvŦ$HI"Qj\г ,12b'A$"X< ]O(ϴ--_CR +_?HE]mi@j7cq5sѽj8P! p gJ%fGߟ~CۏI"i PF[+=5}p64 M~Bj 6 x b$?|zx:AD>y;-Z:/%qگzWwމCXr8=%zn"nhEqހ ~ZD Fԟ\0,$f$#'‚m;Ybi|og#lFʩcE73T_3"$R9qz=Hs FI9ѵaz"A`@Iңǝ=8%Ҡ-[-_Q"IiTR n ^{gN,!P$d~ߦ=ோHoCrޛ95B(BΘ}qA,Ut,{?`y gFD`h "\C][9ㅻnY׎ip" 8+,QQ3ZڈcJI$ S&}.bnD_?$??[ _'X`t#\ךgX7Q0s[fw6W'?h!ܶ>8.`kM(jgHQKIA74ǯpHa0QUqγ2vڌ}p )^% b.GG׀ҁ};.Ӆ5ܦמrk;f BsTw">A =`pEW,8_ KnD)堿GH 8lCcCrςI/HD}ЌҊ(Ob.R6E=s4 |i#@8p$X "aG^`B/%!UWpn Nٗ (G4ӣQdJzj{B"ē~ Csg;e*h| AE!>c8$ex[Np\D*h@:!P +[~2R>f7v8t$te10kCԇ÷Z5V F@ N^ hAF-zZhXS 6efYx7ߥ!K{9D8JΊCiZx-ZK t*pd!Qr=}wCp" /Sm2W~4^$q=UZ)hFh ^4'bS?d&$ : e}!۵<%x;r:'ф2w >?=&GƊO^ܧ%KL`AY3RKA l!^ 7ua?ԑCaүIڳ@)=ί;0D"_PD[^?k%d`gUg¡J?3C-s^ $w{xφ reJ&dxad >\) V[ʘ) `ĭmzr4Y{kݾK^;|@ ,34EիDk纋fBUWJ{|n :` CF~;$kV-\"s)71M7lzC4,a9r}`'F_yu^;wǬ@do!`{HKMH ܿxHҍ(67M6'srfαH5\޴Mo}aP9B*ѐꔓd[>aerϭ~% VgLNev~l6ckz*" G02qEͣ tS?ǯwO# 7=gZ{챣̜U>M$ObO $ *5qKMU; &EH|w+s dMv O7>S;כ >QM%`X%TiyoN+s[,@~cdr&@c\Ny)ǀjVow)PrMg!l"%k/+Q?L@G(lIi| m)ޔ~\<I6_gcن`x5:dM*Ouq(0`?}Ua-vPFTSS}ެ- s@?}4FG+6ºC #EK)lL/L%k/*))]VH)3B۬#eC/(Mlr E.4Ak OQ$ŗezJK 7&}1|WPF<^V%<3W9n,zn GHU\3 &dkzjBUҮѿ1^~2}?FI X憤C~wMCԙ}I~=,ut@qq?w"9jw]֫ $ Ҹr"\!G,&gX,' o;"&syr]ߪ @ow^^qnDopD:izpGKAM;fnE@~ϋ*i{y7'"DŠUInvJzeiX_숆2(=x5P/{> w"P%z^g\6iM.dz].Ws! 'kUw SR]s뾶MEAt\7- +'TTMYv0#O lg{&Y$~YUQWyG22/ܻ}*$*GE6Rj;3f?`pf.!UOtQMHNAYګ^ڹe0At2\K3Ysq"`Y@@NvLnBtՌ29?Tv=X{5Ғpj|6hĈמrT^'Bt!x`>{ u3 a{kz9ˀ B `i1Ob8겚{w?kݝ7eࡌ(Y ںcotZ,!OTipBjUfu{g,U2+h0yqsZX/ ƭZ/fU!;O#=eH28y͵nž;Y9QV>TQ^(T'$\[J|mČL|RcLDTHXTJ( ^QmD>#F.9n꾘9yxǪ A曪v/IEm}/mj,nj\@#Z,HDL&e,v)*$$A9Dh,WVoسN!*% 6U#O̚%ȝ=eh;@pC G0@pC G0@pC G0@pC G0@pC G0@pC G0@pC G0@pC G0iZIENDB`( ?ugD%vi>Rxi=ywj7m8u;҈v:v:x:xRugD%?ii<qh=S{l7o9Єu}>}>}>}>}>}>}>~=~=|={=z{=??@@@@@@@AAAAAAAA@@@@@@@??{=w>p;ui:ylF(ynMsg:~q<ڋx?|@~@@AAAABBBBBBBBBBBBBBBBBBBBAAAA@~@|@x?q}?@@AAABBBBBCCCCCCCCCCCCCCCCCCBBBBBAAA@@}?v>xj8{oA>xoMYbғNEDCCCDDDEGGEEEEEEFFHHHFFHFFGEGGEGGGEDDDCCEBB|@q<~rCWe_CSǿFEFFGEGFHHHFIIIIIGGIIIIIGIIIGIIIIIIIFHHHHGGGFFEECs?}pCO]X91i˷OGHIIJJHJJKKKKKKIKKKKKLJLLLLKKKKIKKKKKKKJJJJJIIHHGHDu@|qF/ffL fZ1pӿayFEIJJJJKKKKKKKKLLLLLLLLLLLLLLLLLLLLKKKKKKKKJJJJIIHHGG|Cn?qT rgBEl`3䝏rѿӽԿv}DJKLLLLLLLMMMMMMMMMMMMMMMMMMMMMMMMMMMMLLLLLJG~D}DEEHIIHGw>{pGDffyk@~j;pҿѻһӽȼozIIMMMMMNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNMMIs>~e6{c6~f8m<{CGKKJKGoBffwLtA҈tB~XҿҾкѻӼ|GNNPPPPOOOOOOOPOOOOOOOOOPOOPPPPOOOPOPPOPPPMл͹ðj?q>~GLKLMJuAwL}oGG}H{FoBȲҼҺѺռ̶~HPQQQQQPQQQQQQQQQQPQQQQPQQQQQQQQQQQQQQQQQQPqԿqj;vAGNNNM~I{qFH~rFmJJq>տһѻһӼPLNPQRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRMǼĮ[l={ELMMNJ~rFm~uEMMzDhտԽӼӽվrIo>yDHMRTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTMʿtIxCNOPOM~uEwHNOJyKнֽռսֽ׾׿bb~MPUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUOĪqxCKOPPNvGxINSNwBŹ׿׿׾׾׿NUVVVVVVVVVXXXVVVVVVVVXXVXVVVXVVVVVVVVRåƯXp=HORQNwFxLRUTLx׿NX[[Y[Y[[Y[[[[[[[Y[[[[[[[[Y[[[[[[[[[[[Zy̸UNVVURxLzOSVWQNdZ\\\\\\\\\Y\\\\\\\\\\\Y\\\\\\\\\\\\\\\UȽ}HSWWVSzOyRoSWXVKdY\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\Pζb~IQVXWSyRoyQETXYXPX_U]^^^^^^^^^^^^^^^^^^^]]]^^^^^^^^^^^^^]PͷdyBLSWXTzRG[TZZYR|EǼJW]__```````_`_``````````_``````````__cǻRGTYZT[fPңZ[XN|FsJY`aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaafNX\ZPҙfSZ]YNWʳ£aabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbnxT]]ZSXBY]\QfncccccccccccccccccccccccccccccccccccccnҼM[^]YVA_X⨗^]WP̱bddddddddddeddddddddedddddddddddddddedbvY`^^X_X^`[Nggggggggggfgggggggggfgfgggffggggggffggggj̳Wbba^Xc)_b_T~tiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii`ӼYcecb_a*[êbbY`ֿŹxljjjjjjjjjjjjjjjjjjjjjjjjjjjjjnjjjjjjjjjjjjaŤo_gedb[`Jcc^Oйˬnkkkkkkokkkkkkkkkkokkkkkkkkokkkkkkokkkokkkkkkkh^ƺWdgfdb^I_ӭhcUĸӹnɦonnmmmnnnmmmnnmmmnmnnmmmnnnnnmnmmnnnnnnnmmnmmnld[^]ghie^`Ofi_tҸbkpppppnpppppppnppppppppppppppppppppnpppppppppppppm[սVahkhf`O^ЮjjfnqqqqqqqqqqqqqqnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqlpǩUfllj`g;hlj_rsssssssssssssssssssssssssssssssssssssssssssssssssoӸ·Yimlhg;blnlstttttttttttttttttttttttttttttttttttttttttttttttttsw^knlbmgnooʧsuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuoɪdbnngmevnpqsp׹ĝxwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwvvwwwtomg־ZionfwfְoprsgŝvwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwslijjlljggjoqjZSQȿ[lohn%mrrvvgбvxzzxxzzzzzxzzxxzzxzxzzzzzzxxzxzxzzzzzzwqfXRMQRTQU\QZ`XҶʨvcomn%jzruwxwkٿŜ||||||||||||||||z||||||||||||||||||||||zsc`պͭȽbúcmrjzmɴtvxyw}ά{}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}{p^sßgsloovxy{{ŻھƝ}~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~wbȽbqooqJuwz{}~{ټϭ~µµµµµµµµµµµµµµµµµµoȡptqJowy{}~|ttȻ˜µöööööööö¶ööööööööööööööööööööööööööö¶öööööµvb׹αswop̸y{}~|rf}kֺµµµµµµµµµµµµµµµµµµµµµµµµµµµµµµµµµµµµµµµµµµµudǻƺwʨyypUtz|xo׹ǻ÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ydʏ|duzuU{!v~~oбöķķķķķķķķķķķķķķķķķķķķķķķķķķķķķķķķķķķķķķķķķķķķķösԷ׽xɽhwv{vK{xõöŸŸŸŸŸŸŸŸŸŸŸŸŸŸŸŸŸŸŸŸŸŸŸŸŸŸŸŸŸŸŸŸŸŸŸŸŸŸŸŸŸŸŸŸŸŸöyyŸxnxuJuo}}mϮΫŹŹŹŹŹŹŹŹŹŹŹŹŹŹŹŹŹŹŹŹŹŹŹŹŹŹŹŹŹŹŹŹŹŹŹŹŹŹŹŹŹŹŹŹŹŹĸȞѲyisuouu}̬ǺǺǺǺǺƹƹŸŶĵôķǺƹƹƹǺǺǺǺǺƹǺǺǺǺƹǺƹǺǺǺƹƹƹƹǺǺǺƹƹƹǺƹƹƹǺŸĜ}ʦдnsxznͪķȻȻȻȻȻƺķµķöƹŸȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻǻǞŶyԵsq}˽~lӶЯŹȻȻȻȻȻörsŹȣƺȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻȻķ~ts{߼}rɽپĘǻɼɼȻǻǻĸȠȻɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼȻôնzÙȨmxﶧsyyĸȻȻȻƹķ˾бȻɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼɼȻµǞwkqyʾöɼʽǺ}~ɢɼʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽɼ–öȼyiֻȻǻ³Ȼʾʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽʽɼбu׹¤ɾƺĵǺķɺշ˿˾˾˾˾˾˾˾˾˾˾˾˾˾˾˾˾˾˾˾˾˾˿նά¶|ѹzĹƜ×ʥɼ˾˾˾˾˿ٽŹͧѳ}Żʾʾ˿ŗϬƘ˿ŝtӹŻǻʾ˿˿ɼȼʾ’’’’źɪƺȼ˿ʾŹѮʽ“““““““““““““““““““““““““““““““““““““““““““““ٽhԹӼźȽ“ɽˡɽÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÑѮ’ͦٹ̥ƙʠʢѳԹ˚}ƻɽʾ“ʽɽÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔÔԒپ×Ôԓ׸̣ÔÔÔÔÔÔѮ˚xźĸȼɽ̿ƹӰɼÔԕÔÔÔÔÔԕÔÔÔÔÔÔÔÔÔԕÔԕÔÔԕÔÔÔÔÔԕ••Ôԕ•Ô•ÔÔԕÔԕÔÔÔĕѱϩǝÔÔÔÔÔÔÔÔÔÔԓÔϩϼw̽UȨ}÷øŸ÷uٽƹ•••••••Ôԕ•••••••••••••••••••••Ô•••••••••••Ôԕ•••••••Ǻ“••’•˾ɼɽȻˡDZRŲ(rpƼnǼzŹʾĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖÔӱˢʿʾ“ĖĖƻŹȼ÷Ĺvv“%ոŹ{ǻĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖĖɽĸȾŹw|˾Դˡ˿ˢԕɽƚյɽʾ¶ٻغöȻ“ÖÖĘĘÖÖÖÖÖÖĘĘĘÖÖĘĘÖÖĘĘÖÖÖÖÖÖÖÖÖÖĘÖÖÖÖÖÖĘĘÖÖÖÖÖĘÖ֕ɼwŸֶ˾ǻ“ÖÖÖĘȞˡ˦ʢ•ȝֶȻ˾̿Ÿ•Ȼвӻʑ̿ĘĘĘĘĘĘĘĘĘĘĘĘĘĘĘĘĘĘĘĘĘĘĘĘĘĘĘĘĘĘ×ĘĘĘĘĘĘĘĘĘĘĘĘĘĘĘĘĘÔ˾ȻĘĘĘĘĘĘĘĘʕ˾ƹնȻǺѱɼƹʪ͑оS׹˿ĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚśƺÙĚĚĚĚĚ×Ȼĵʥַzw׻}ʸS¶Ź̿–ĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚĚ̖ƺŹ×ÚěĚ×̿ŸɼŹϬɦźŹöɼ•řƚƚƛƛƛƛƚƛƛƚƛƛƚƚƛƛƛƚƚƛƛƛƚƛƛƛƛƛƛƚƛƛƚƚƚƚƛƛƛƛƛƛƛĚɠřŹɽʾ•ŹҲѯƺȼʤřŚΪ̤˿¶ĸϳȃĸ¶ŹŚƛƛĚƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛ˿ѮwĸöŹĸЭŻ÷׹ɽ–ɽ¶ȠԹ|ȃʹ,ڽӲŹÙř–Śƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛǝ~ƜŹĖǻɽɽǼĸҲøͯë+ˤ÷ǻɽ¶ŹʾĘƛƛƛřĘřƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛƛȨ̌ٿѯɠĚ˿˿ǻǻӱϭӴǻ|άƺzӳŚŝś˿ɽǻ˿śǞƜƜƜƜƜǞƜǞƜǞƜƜǞǞƜƜƜƜƜƜƜǞǞǞƜƜƜƜƜǞƜǞś–ǻ׻Ɯ}̪ɽɽ}ϼϭŝƜŹ÷ʤƻǞǞǞǞǞǞǞǞǞǞǞǞǞǞǞǞǞǞǞǞǞǞǞǞǞǞǞǞǞǞƜǞŝ˜ȼǺϰվǻİѳŜƝŜ̨ƝƝƝƝƝƝƝƝƝƝƝƝƝƝƝƝƝƝƝƝƝƝƝƝƝƝƝƝƝƝƝƝŜ—˧ӴBھƜĜę™ÚǞǞǞǞǞǞǞǞǞǞǞǞǞǞǞǞǞǞǞǞǞǞǞǟǞǟǞǞǞǞǟǞśɢAάʣɽǽżĺĻغЮƜǟǟǟǟǟǟǞǟǟǞǟǟǟǟǟǟǟǟǟǟǞǟǟǟǞǟǟǟǟȞȞŝÚʾǻżԴWӴ~~ɽ˥ھ׹ƝǼŚǟǟǟǟǟǟǞǟǟǟǟǟǟǟǟǟǟǟǟǞǟǟǟǟǟǟǟǟƜ˜ƝΫƜȡWęǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟę–úĻɿҲɡɡRƛȞȞȞȞȞȞȞȞȞȞȞȞȞȞȞȞȞȞȞȞȞȞȞȞȞȞȞȞ×Ù˥ǝΩQʣȞǟǟǟɟɟɟǟɟǟɟɟɟǟǟǟǟǟɟɟǟǟǟɟɟǟǟǟǟƜѱŝśѯɞǟɟǟǝǟ׹̧ʣ0•ʣɟɟɟɟɟɟɟɟɟɟɟɟɟɟɟȞɟɟɟɟɟɟɟɟɟɟɟɟɟȞؼЮϫȝɟɟɟɟȝɞ“ȼʿǜȞ“/ԕɽΩѮƛɟɟɟɟɟɟɟɟɟɟɟɟɟȞɟɟȞɟɟɟɟɟɟɟɟɟɟɟɟɟɟɟͩʣȝŚɟɟɟȝƚĖЬǻʢřȞʠԗԿ ǜĖƙřɟɟɟɟɟɟɟɟɟɟɟɟɟɟɟɟɟɟɟɟɟɟɟɟɟɟɟɟɟɟɟɟɟƚǛɟɟȞĖǽĹǼɽĖ–ȝˡʡй IśǠӲÙȟȟȟȟȟȟȟȟȟȟȟȟȟȟȟȟȟȟȟȟȟȟǠȟȟȟȟȟȟȟȟǠȟǝԳǞȟȟƛ˾Ҳś÷پ–ɽGГϭƞƟǠǠƜȟȟǠȟǠǠǠȟȟȟȟƜٙŝǠȟȟȟȟȟǠǠȟȟȟȟȟȟȟǠǠǠȟɥǠȟȟȟÙʧȟȻȡֹГڶԸŝƟƟƟŜ˾ɡշƟȟȟǠǠǠǠǠǠƟě—˿ƹƹŝǠȟǠǠǠǠǠǠȟǠǠǠȟǠǠǠǠǠǠȟǠȟȟǠַɼ˧ؾڶ"ǢƜƞƞƞŝַØĚŜǟƞǟǟǟǟǟǟǟƞ–ɺƷŸ̿•ǟǟȞǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟ—ڽ̿Ŝ!HĠʽĜŝƞƞƞƝϭ̿–śǟǟǟǟǟǟěʻҰ̿ƞǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟƞĜʻHtƺÛƝƞƞƞŜȻָƝǟǟǟǟǟʣɺ–ƝǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǟǠƞ˼tГ¶ȼšƜƝƝĚŜѰ˿ӳŸĜ–ęƜǠǠȟȡǻÙǠǠȟǠǠǠǠǠǠǠǠǠȟǠǠǠǠǠǠǠǠǠǠǠƝʤ׼Г՟Þ~ĺʿěǻǻŜ™ȼŜȻƺ˿ƝǠǠĚֻʾǞȟȟǠǠǠǠǠǠǠǠǠǠǠǠǠǠǠǠǠǠǠǠǠǠȡʤʧĸĻ՟֩ĹĸǻּÛʥµÚǠǠ™̪ĸʾ˿ƝȟǠǠǠǠǠǠǠǠǠǠǠǠǠǠǠǠǠǠǠǠ™׽Ġ֩ՠβƹͫбո˩ÛƟƟ׻}ƹõɼÛƟƟƟƟƟƟƟƟƟƟƟƟƟƟƟƟƟƟƟƟÛֽĸ՟ԑвĠƹķɺšͫĝƟƟĜš˾ÚŝƟƟƟƟƟƟƟƟƟƟƟƟƟƟƟƟƟƟŝԷԽƣʾĹ֑pԹҵŸ˾ÛšؽɺĜƟƟƟƠؽ´Ǻ̿ĜŞƟƟƟƟƟƟƟƟǡǡƟƟƟƟƟŞŞšȥ˿ʾȼĸpJʽ̿ȼֺŞƟƟƟŝ׼άĶʾŞƟƟǡƟǡǡƟǡǡƟǡǡǡŞŞƠŞŞÜʨȤȽǼźI#ʾ÷ϰƠǡǡǡƠϮŞƠǡǡƟǡƟǡǡǡǡǡƠƠŞƠŞĝşĞÝÝ˿ʾȽǻĻ˿߿ՓĹàĹǡȼֹĝƟƟƟǠǠɢęǞǠǠǠǠǠǠƞƟƟƟƟƟƟŞŞĝÜܛȽǼźԪ@Ĺᾳ̳һĹ˫ÝşşşşşşşϰέֻϱşƠƠƠƠşşşşşşĞĞĞÝʿɾȽǼźú߶? ֗ǨζȩʿĹƺʾ›ĞĞşşşşšȢşşşşşşşşĞĞĞĞÝʿɾȽǼźĹ 4ļû·źǼͳƺʾĸȼ››ÝĞĝӶ̫˿şşşşşĞĞĞĠĞÝʿʿɾȽǼƻź-RºúźźƻǤĹøźĹĹȼ›ÜɽʾĝĞĞĞĝĝÜܛʿɾȿɾȽźƻĻѷKQ˿úɫ̰˯ƺƻȤĹǻÝݛ›ʿʿɾȿȽǼǼźĹ˶Q;úĻѺɫ¶ƻȤʿʿʿʿɾȿȽǼǼźźú;vĹźŢȿͳ׿ͲŢֿʿɾɾɾɾȿȿȽǼǼƻźźĹֵv&~¹äȽɬ¡ѺԾźƻƻƻƻźĻĹúʵ}&P̻ŨǾĦϸĥźĻĻĻĻɽƻĻĹĹúú̺P$Owлུ콲񼳎񽴍컳߻̺wO$?????????????(` ̙ƍ̌<ʍ\ˋqʍ~ˊʌˊvˌdɌCȋ!̙Ɋe7tPr}rKzoE|oByEvCu?r?u?vCyE|oBylBwjDvmJ{a7͉ڑ̙ eA|pJwjDwl:l6y=y}?~@~@@@@AAAAAAA@@@~@~@}?{>z={k8sg>xnIl=|rN{yoHxh:{>|?@AAABBBBBBBCCCBBBBBBBAAA@|?{>wh9wiB܌YrVR;wX\EDCCDDDFEEEEEEEGGGEEGEGGGGDDDCCD}@q>vi>yRsQL8qj`H𴭗{CFGGGHHHHIIIIIIIIIIIIIIIIIHHHHGGGFECw?|mD}YS``F_Y>{sU̻q@FIJJJJKKKKKKKKKLLLLLKKKKKKKKKJJIGFFGFEv?{rKĢhskK_ibҼѺһԽr>~FJNPQQQQQQQQQQQQQQQPQQQQQQPQQQJͷvk̲һγǦǻʿŹȨ˯››ʿʿɾȿǼǼź¶XŹ{¹ʭĢзȼ־ƻʿʿʿɾȿȽǼǼźú󵬈ùM̙DϸϸťɽǼǼǼƻźź¹ӺzĽ#Ʃ 5|л§Ӿη¢øĹŹĹ뷪[̲ (Ltҵᳬ񵬇絮׸b>ĺ̙?????????????????????(@ B|oG'vj@gzj>}k6Ņt9x:xuiBhylF(UUskC@xk9t=x>|?ABBBBBBBBA|?x>t=xk9skC@UUjj? ~vL{t?{ACDDEEEEEGEEEEGGDDC{Ar<{n@zL UU*|v^yxBHIJJKKKKKKKKKKKKKKJJHF|Cv?zmB{f`X9BqѿwzAILLLLMMMMMMMMMMMMLLLHyAq=u>yAt=sJ>Uh^;rª|PINPOOOOOOOOOOOPOOPPOK|wQi{>{>{>{>z>z>n?xkExoJ{f\W@c}pK|B~ACDEEEGEEEDCA{?o?tiEmmHGG2=upW줙zv@HJJKKKKKKKKKKJI{Bq>}n{y>{{oC|rkC&lfMR|n?ّCHIJJJJIH}Czi=icBMQQ9pȿoBIMOOOOOOOJ`~f>mc9ff?idF3{^pzGOTTTTTTTNɾлjzf:peG2niK3sCηSZ[[[[[[Wöo?pkL2kkPtF󽱒̲^`aaaaaa^zJxkPzNpgggggggg]Ҹ}T}ZIuƤlmmmmmojjj^˯ʰN}ZI]ռsttttttpja__^Rm#gֻʧ}zzxxzxxkZnjb|t`%qsoٿٿƜ~µycҳӼetm}ôöķķŸŸķŸŸŸ{Ƞ›`hίôŸʽƹȻȻȻȻȻǺȻƷپϰžʣʽěʽʽʽʽʽʽʽʽɼҰӱоǿśɼ˾˾˿ƽ罯sÔǻÔÔÔÔÔÔÔÔÔÔÓͦغȝѾʦĸƻԕĖĖĖĖĖřřȼپ˿¶ʾˡýι}wŸ˾×ĚĚĚĚĚ×Эz{ě˧ìy(•ʽȼɽƛƛƛƛƛŚʡǻҺ&öƝƝƝƝƝś–ȱOŚɞȝȝɟɟȞֵͪſLظƚř“ǜɟɟˣŚ’ڽȼɼǜ˿̿ƜŝǟȟʡŚָ󸸛ʰ1ķƟѰǠĵ˾ěǟǟƟֹè/ɱ+ȥƟŞǡǡȢè̲(̻ηʰѳӴɣƟƟŞÜĹ BøȰȪʾɽǼ¹Ժ?úl©ɳưɵ躲i??(0 ` Ћ n3kZlUs, aY@?wkDwA}@~A~A|@vAvjDhbG'BB,}waϙfxAKMMMMJ|I~i?cZ;TN:'uVëSOUUUUTmlla<<<-TF8}m@˵˿U]]]]]`xmFtIǮifgggg^ǩ{mPvYGøqnqpnfc]ĢռPmeP#eԼȽvwywqcSMOΰâ}[s lԷָ÷µµµurqf r<ɽ÷ĸŹŹŹŹŹ}|1fͱƹɼɼɼɼɼɼƺԵ~WžnĻȼʢԻĸ[HuŖö~ʽ“•ÔÔԒʠͨ˾Ǻú8ěöɺ–ĚĚĚØŜǝɺϭȶѱ•ƼĚƛƛĚřطΫ¬[ٿǠƜƜƞȞǝϭ1•“Ėȝˢ׷ЭѿǻΫ˼ěЮȹƝǟԲq ȭ8ˬӵֺͭȻÚǡĜо˜śƟÜƽĜ FƼǫĹ7Ž>ƿdǿa5й (  @??/`X4y{Dɑ~A~A|CujBs\\E \X?HLITTM^dh`ABmeD?¹ӺMaa\kaZ?D__?ȼWjohVnTT8 yRv}kttinrzUwaʿrvķ÷tˬ{yȻɼɼyλ÷ɽ““Ȼɽķ˾ĘĘȻӶֻơȣyÜȿĚǝ˜wUU? ȣ׌˜ʨ˨\EE DɾվŸ̮àʽķʫCBƲ̮ʫʿûq?_O?uo򷭉Zf &&&&&&&&&&&&&&&&MEGAcmd-2.5.2_Linux/build/installer/winversion.rc.in000066400000000000000000000022571516543156300223450ustar00rootroot00000000000000#include VS_VERSION_INFO VERSIONINFO FILEVERSION @MEGACMD_MAJOR_VERSION@, @MEGACMD_MINOR_VERSION@, @MEGACMD_MICRO_VERSION@, 0 PRODUCTVERSION @MEGACMD_MAJOR_VERSION@, @MEGACMD_MINOR_VERSION@, @MEGACMD_MICRO_VERSION@, 0 FILEFLAGSMASK 0x3fL FILEFLAGS 0 FILEOS VOS_NT_WINDOWS32 FILETYPE VFT_APP FILESUBTYPE VFT2_UNKNOWN BEGIN BLOCK "VarFileInfo" BEGIN VALUE "Translation", 0x409, 1200 END BLOCK "StringFileInfo" BEGIN BLOCK "040904b0" BEGIN VALUE "CompanyName", "Mega Limited\0" VALUE "FileDescription", "@MEGACMD_RESOURCE_NAME@\0" VALUE "InternalName", "@MEGACMD_RESOURCE_NAME@.exe\0" VALUE "LegalCopyright", "Mega Limited 2019\0" VALUE "OriginalFilename", "@MEGACMD_RESOURCE_NAME@.exe\0" VALUE "ProductName", "@MEGACMD_RESOURCE_NAME@\0" VALUE "ProductVersion", "@MEGACMD_MAJOR_VERSION@.@MEGACMD_MINOR_VERSION@.@MEGACMD_MICRO_VERSION@.0\0" END END END IDI_ICON1 ICON DISCARDABLE ".\\app_ico.ico" IDI_ICON2 ICON DISCARDABLE ".\\web_ico.ico" MEGAcmd-2.5.2_Linux/build/installer_mac.sh000077500000000000000000000220211516543156300203550ustar00rootroot00000000000000#!/bin/zsh -e Usage () { echo "Usage: installer_mac.sh [--arch [arm64|x86_64|universal]] [--build] | [--sign] | [--create-dmg] | [--notarize] | [--full-pkg]" echo " --build : Builds the app and creates the bundle using cmake" echo " --sign : Sign the app" echo " --create-dmg : Create the dmg package" echo " --notarize : Notarize package against Apple systems." echo " --full-pkg : Implies and overrides all the above" echo " --arch [arm64|x86_64|universal] : Arch target. It will use the host arch if none specified." echo "" } if [ $# -eq 0 ]; then Usage exit 1 fi APP_NAME=MEGAcmd VOLUME_NAME="Install MEGAcmd" MOUNTDIR=tmp RESOURCES=installer/resourcesDMG SERVER_PREFIX=MEGAcmdServer/ CLIENT_PREFIX=MEGAcmdClient/ SHELL_PREFIX=MEGAcmdShell/ LOADER_PREFIX=MEGAcmdLoader/ UPDATER_PREFIX=MEGAcmdUpdater/ host_arch=`uname -m` target_arch=${host_arch} full_pkg=0 build=0 sign=0 createdmg=0 notarize=0 sign_time=0 dmg_time=0 notarize_time=0 total_time=0 while [ "$1" != "" ]; do case $1 in --arch ) shift target_arch="${1}" if [ "${target_arch}" != "arm64" ] && [ "${target_arch}" != "x86_64" ] && [ "${target_arch}" != "universal" ]; then Usage; echo "Error: Invalid arch value."; exit 1; fi ;; --build ) build=1 ;; --sign ) sign=1 ;; --create-dmg ) createdmg=1 ;; --notarize ) notarize=1 ;; --full-pkg ) full_pkg=1 ;; -h | --help ) Usage exit ;; * ) Usage echo "Unknown parameter: ${1}" exit 1 esac shift done if [ ${full_pkg} -eq 1 ]; then build=1 sign=1 createdmg=1 notarize=1 fi if [ ${build} -ne 1 -a ${sign} -ne 1 -a ${createdmg} -ne 1 -a ${notarize} -ne 1 ]; then Usage echo "Error: No action selected. Nothing to do." exit 1 fi build_archs=${target_arch} if [ "$target_arch" = "universal" ]; then build_archs=(arm64 x86_64) fi for build_arch in $build_archs; do eval build_time_${build_arch}=0 done if [ ${build} -eq 1 ]; then for build_arch in $build_archs; do build_time_start=`date +%s` # Clean previous build [ -z "${SKIP_REMOVAL}" ] && rm -rf Release_${build_arch} mkdir Release_${build_arch} || true cd Release_${build_arch} # Build binaries # Detect crosscompilation and set CMAKE_OSX_ARCHITECTURES. if [ "${build_arch}" != "${host_arch}" ]; then CMAKE_EXTRA="-DCMAKE_OSX_ARCHITECTURES=${build_arch}" fi # If VCPKGPATH environment variable set if [ -n "${VCPKGPATH}" ]; then VCPKG_ROOT_ARGUMENT="-DVCPKG_ROOT=${VCPKGPATH}" fi cmake -B build-cmake-Release_${build_arch} -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_VERBOSE_MAKEFILE=ON ${VCPKG_ROOT_ARGUMENT} ${CMAKE_EXTRA} -S ../../ cmake --build build-cmake-Release_${build_arch} -j $(sysctl -n hw.ncpu) cmake --install build-cmake-Release_${build_arch} --prefix ./ SERVER_PREFIX="" CLIENT_PREFIX="" SHELL_PREFIX="" LOADER_PREFIX="" UPDATER_PREFIX="" # main bundle: # place (and strip) executables typeset -A execOriginTarget execOriginTarget[mega-exec]=mega-exec execOriginTarget[mega-cmd]=MEGAcmdShell execOriginTarget[mega-cmd-updater]=MEGAcmdUpdater execOriginTarget[MEGAcmd]=mega-cmd for k in "${(@k)execOriginTarget}"; do #Place it in ${APP_NAME} with its final name mv $k.app/Contents/MacOS/$k ${APP_NAME}.app/Contents/MacOS/$execOriginTarget[$k] #get symbols out: dsymutil ${APP_NAME}.app/Contents/MacOS/$execOriginTarget[$k] -o $execOriginTarget[$k].dSYM #strip executable strip ${APP_NAME}.app/Contents/MacOS/$execOriginTarget[$k] # have dylibs use relative paths: done # copy initialize script (it will simple launch MEGAcmdLoader in a terminal) as the new main executable: MEGAcmd cp ../installer/MEGAcmd.sh ${APP_NAME}.app/Contents/MacOS/MEGAcmd # place commands and completion scripts cp ../../src/client/mega-* ${APP_NAME}.app/Contents/MacOS/ cp ../../src/client/megacmd_completion.sh ${APP_NAME}.app/Contents/MacOS/ otool -L ${APP_NAME}.app/Contents/MacOS/mega-cmd otool -L ${APP_NAME}.app/Contents/MacOS/mega-exec otool -L ${APP_NAME}.app/Contents/MacOS/MEGAcmdLoader || true otool -L ${APP_NAME}.app/Contents/MacOS/MEGAcmdUpdater otool -L ${APP_NAME}.app/Contents/MacOS/MEGAcmdShell cd .. #popd Release_${build_arch} eval build_time_${build_arch}=`expr $(date +%s) - $build_time_start` unset build_time_start done fi if [ "$target_arch" = "universal" ]; then # Remove Release_universal folder (despite SKIP_REMOVAL, since it does not contain previous compilation results) rm -rf Release_${target_arch} || true for i in `find Release_arm64/MEGAcmd.app -type d`; do mkdir -p $i ${i/Release_arm64/Release_universal}; done # copy x86_64 files for i in `find Release_x86_64/MEGAcmd.app -type f`; do cp $i ${i/Release_x86_64/Release_universal} done # replace them by arm64 for i in `find Release_arm64/MEGAcmd.app -type f`; do cp $i ${i/Release_arm64/Release_universal}; done # merge binaries: for i in `find Release_arm64/MEGAcmd.app -type f ! -size 0 ! -name app.icns | perl -lne 'print if -B'`; do lipo -create $i ${i/Release_arm64/Release_x86_64} -output ${i/Release_arm64/Release_universal} || true; done fi if [ "$sign" = "1" ]; then sign_time_start=`date +%s` cd Release_${target_arch} cp -R $APP_NAME.app ${APP_NAME}_unsigned.app echo "Signing 'APPBUNDLE'" codesign --force --verify --verbose --preserve-metadata=entitlements --options runtime --sign "Developer ID Application: Mega Limited" --deep $APP_NAME.app echo "Checking signature" spctl -vv -a $APP_NAME.app cd .. sign_time=`expr $(date +%s) - $sign_time_start` fi if [ "$createdmg" = "1" ]; then dmg_time_start=`date +%s` cd Release_${target_arch} [ -f $APP_NAME.dmg ] && rm $APP_NAME.dmg echo "DMG CREATION PROCESS..." echo "Creating temporary Disk Image (1/7)" #Create a temporary Disk Image /usr/bin/hdiutil create -srcfolder $APP_NAME.app/ -volname $VOLUME_NAME -ov $APP_NAME-tmp.dmg -fs HFS+ -format UDRW >/dev/null echo "Attaching the temporary image (2/7)" #Attach the temporary image mkdir $MOUNTDIR /usr/bin/hdiutil attach $APP_NAME-tmp.dmg -mountroot $MOUNTDIR >/dev/null echo "Copying resources (3/7)" #Copy the background, the volume icon and DS_Store files unzip -d $MOUNTDIR/$VOLUME_NAME ../$RESOURCES.zip /usr/bin/SetFile -a C $MOUNTDIR/$VOLUME_NAME echo "Adding symlinks (4/7)" #Add a symbolic link to the Applications directory ln -s /Applications/ $MOUNTDIR/$VOLUME_NAME/Applications # Delete unnecessary file system events log if possible echo "Deleting .fseventsd" rm -rf $MOUNTDIR/$VOLUME_NAME/.fseventsd || true echo "Detaching temporary Disk Image (5/7)" #Detach the temporary image /usr/bin/hdiutil detach $MOUNTDIR/$VOLUME_NAME >/dev/null echo "Compressing Image (6/7)" #Compress it to a new image /usr/bin/hdiutil convert $APP_NAME-tmp.dmg -format UDZO -o $APP_NAME.dmg >/dev/null echo "Deleting temporary image (7/7)" #Delete the temporary image rm $APP_NAME-tmp.dmg rmdir $MOUNTDIR cd .. dmg_time=`expr $(date +%s) - $dmg_time_start` fi if [ "$notarize" = "1" ]; then notarize_time_start=`date +%s` cd Release_${target_arch} if [ ! -f $APP_NAME.dmg ];then echo "" echo "There is no dmg to be notarized." echo "" exit 1 fi echo "Sending dmg for notarization (1/3)" xcrun notarytool submit $APP_NAME.dmg --keychain-profile "AC_PASSWORD" --wait 2>&1 | tee notarylog.txt echo >> notarylog.txt xcrun stapler staple -v $APP_NAME.dmg 2>&1 | tee -a notarylog.txt echo "Stapling ok (2/3)" #Mount dmg volume to check if app bundle is notarized echo "Checking signature and notarization (3/3)" mkdir $MOUNTDIR || : hdiutil attach $APP_NAME.dmg -mountroot $MOUNTDIR >/dev/null spctl --assess -vv -a $MOUNTDIR/$VOLUME_NAME/$APP_NAME.app hdiutil detach $MOUNTDIR/$VOLUME_NAME >/dev/null rmdir $MOUNTDIR cd .. notarize_time=`expr $(date +%s) - $notarize_time_start` fi echo "" if [ ${build} -eq 1 ]; then for build_arch in $build_archs; do echo "Build ${build_arch}: ${(P)$(echo build_time_${build_arch})} s"; done; fi if [ ${sign} -eq 1 ]; then echo "Sign: ${sign_time} s"; fi if [ ${createdmg} -eq 1 ]; then echo "dmg: ${dmg_time} s"; fi if [ ${notarize} -eq 1 ]; then echo "Notarization: ${notarize_time} s"; fi echo "" echo "DONE in "`expr $(for build_arch in $build_archs; do echo "${(P)$(echo build_time_${build_arch})} + "; done) ${sign_time} + ${dmg_time} + ${notarize_time}`" s" MEGAcmd-2.5.2_Linux/build/installer_win.nsi000066400000000000000000001372431516543156300206030ustar00rootroot00000000000000 ManifestDPIAware true RequestExecutionLevel user Unicode true #!define BUILD_UNINSTALLER #!define BUILD_X64_VERSION #!define ENABLE_DEBUG_MESSAGES #!define WINKITVER "${WINKITVER}" !macro DEBUG_MSG message !ifdef ENABLE_DEBUG_MESSAGES MessageBox MB_OK "${message}" !endif !macroend ; HM NIS Edit Wizard helper defines BrandingText "MEGA Limited" !define PRODUCT_NAME "MEGAcmd" VIAddVersionKey "CompanyName" "MEGA Limited" VIAddVersionKey "FileDescription" "MEGAcmd" VIAddVersionKey "LegalCopyright" "MEGA Limited 2019" VIAddVersionKey "ProductName" "MEGAcmd" !define PRODUCT_PUBLISHER "Mega Limited" !define PRODUCT_WEB_SITE "http://www.mega.nz" !define PRODUCT_DIR_REGKEY "Software\Microsoft\Windows\CurrentVersion\App Paths\MEGAcmdShell.exe" !define PRODUCT_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" !define PRODUCT_UNINST_ROOT_KEY "HKLM" !define PRODUCT_STARTMENU_REGVAL "NSIS:StartMenuDir" !define CSIDL_STARTUP '0x7' ;Startup path !define CSIDL_LOCALAPPDATA '0x1C' ;Local Application Data path !define CSIDL_COMMON_APPDATA '0x23' ; To be defined depending on your working environment !define BUILDPATH_X64 "built64" !ifndef BUILD_X64_VERSION !define BUILDPATH_X86 "built32" !endif !ifdef BUILD_X64_VERSION !define SRCDIR_MEGACMD "${BUILDPATH_X64}" !else !define SRCDIR_MEGACMD "${BUILDPATH_X86}" !endif !define SRCDIR_BATFILES "..\src\client\win" ; Version info: get version directly from the binary !getdllversion "${SRCDIR_MEGACMD}/MEGAcmdServer.exe" Expv_ VIAddVersionKey "FileVersion" "${Expv_1}.${Expv_2}.${Expv_3}.${Expv_4}" !ifdef VERSION_SUFFIX VIProductVersion "${Expv_1}.${Expv_2}.${Expv_3}.${Expv_4}-${VERSION_SUFFIX}" VIAddVersionKey "ProductVersion" "${Expv_1}.${Expv_2}.${Expv_3}.${Expv_4}-${VERSION_SUFFIX}" !define PRODUCT_VERSION "${Expv_1}.${Expv_2}.${Expv_3}-${VERSION_SUFFIX}" !else VIProductVersion "${Expv_1}.${Expv_2}.${Expv_3}.${Expv_4}" VIAddVersionKey "ProductVersion" "${Expv_1}.${Expv_2}.${Expv_3}.${Expv_4}" !define PRODUCT_VERSION "${Expv_1}.${Expv_2}.${Expv_3}" !endif !define VcRedistBasePath "C:\Program Files\Microsoft Visual Studio\2022\Professional\VC\Redist\MSVC\14.42.34433" !define VcRedist32BasePath "${VcRedistBasePath}\x86" !define VcRedist32Path "${VcRedist32BasePath}\Microsoft.VC143.CRT" !define VcRedist64BasePath "${VcRedistBasePath}\x64" !define VcRedist64Path "${VcRedist64BasePath}\Microsoft.VC143.CRT" !define WinRedistBasePath "C:\Program Files (x86)\Windows Kits\10\Redist\${WINKITVER}\ucrt\DLLs" !define WinRedist32BasePath "${winRedistBasePath}\x86" !define WinRedist64BasePath "${winRedistBasePath}\x64" !define MULTIUSER_MUI !define MULTIUSER_INSTALLMODE_COMMANDLINE !define MULTIUSER_EXECUTIONLEVEL Standard !define MULTIUSER_EXECUTIONLEVEL_ALLUSERS !define MULTIUSER_INSTALLMODE_DEFAULT_CURRENTUSER !define WinFspInstaller "winfsp-installer.msi" !define MEGA_DATA "mega.ini" !define UNINSTALLER_NAME "uninst.exe" !include "MUI2.nsh" !include "Library.nsh" !include "UAC.nsh" !include "MultiUser.nsh" !include "x64.nsh" !include "WinVer.nsh" ; MUI Settings !define MUI_ABORTWARNING !define MUI_ICON "installer\app_ico.ico" !define MUI_UNICON "installer\uninstall_ico.ico" ; Language Selection Dialog Settings !define MUI_LANGDLL_REGISTRY_ROOT "${PRODUCT_UNINST_ROOT_KEY}" !define MUI_LANGDLL_REGISTRY_KEY "${PRODUCT_UNINST_KEY}" !define MUI_LANGDLL_REGISTRY_VALUENAME "NSIS:Language" ; Settings !define MUI_STARTMENUPAGE_NODISABLE !define MUI_STARTMENUPAGE_DEFAULTFOLDER "MEGAcmd" !define MUI_STARTMENUPAGE_REGISTRY_ROOT "${PRODUCT_UNINST_ROOT_KEY}" !define MUI_STARTMENUPAGE_REGISTRY_KEY "${PRODUCT_UNINST_KEY}" !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "${PRODUCT_STARTMENU_REGVAL}" !define MUI_FINISHPAGE_RUN ;"$INSTDIR\MEGAcmdShell.exe" !define MUI_FINISHPAGE_RUN_FUNCTION RunFunction !define MUI_WELCOMEFINISHPAGE_BITMAP "installer\left_banner.bmp" ;!define MUI_FINISHPAGE_NOAUTOCLOSE var APP_NAME var ICONS_GROUP var USERNAME var CURRENT_USER_INSTDIR var ALL_USERS_INSTDIR ; Installer pages !insertmacro MUI_PAGE_WELCOME !insertmacro MUI_PAGE_LICENSE "installer\terms.txt" !insertmacro MUI_PAGE_STARTMENU Application $ICONS_GROUP !insertmacro MUI_PAGE_INSTFILES Page Custom WinFspPage WinFspPageLeave !insertmacro MUI_PAGE_FINISH ; Uninstaller pages !insertmacro MUI_UNPAGE_CONFIRM !insertmacro MUI_UNPAGE_INSTFILES UninstPage Custom un.WinFspUninstall un.WinFspUninstallLeave !insertmacro MUI_UNPAGE_FINISH ; Language files (the ones available in MEGAsync (not MEGAcmd), with locale code) !insertmacro MUI_LANGUAGE "Afrikaans" ;af !insertmacro MUI_LANGUAGE "Albanian" !insertmacro MUI_LANGUAGE "Arabic" ;ar !insertmacro MUI_LANGUAGE "Armenian" !insertmacro MUI_LANGUAGE "Basque" ;eu !insertmacro MUI_LANGUAGE "Belarusian" !insertmacro MUI_LANGUAGE "Bosnian" ;bs !insertmacro MUI_LANGUAGE "Breton" !insertmacro MUI_LANGUAGE "Bulgarian" ;bg !insertmacro MUI_LANGUAGE "Catalan" ;ca #!insertmacro MUI_LANGUAGE "Cibemba" !insertmacro MUI_LANGUAGE "Croatian" ;hr !insertmacro MUI_LANGUAGE "Czech" ;cs !insertmacro MUI_LANGUAGE "Danish" ;da !insertmacro MUI_LANGUAGE "Dutch" ;nl #!insertmacro MUI_LANGUAGE "Efik" ;locale code not found !insertmacro MUI_LANGUAGE "English" !insertmacro MUI_LANGUAGE "Esperanto" !insertmacro MUI_LANGUAGE "Estonian" !insertmacro MUI_LANGUAGE "Farsi" ;locale code not found !insertmacro MUI_LANGUAGE "Finnish" ;fi !insertmacro MUI_LANGUAGE "French" ;fr !insertmacro MUI_LANGUAGE "Galician" !insertmacro MUI_LANGUAGE "Georgian" !insertmacro MUI_LANGUAGE "German" ;de !insertmacro MUI_LANGUAGE "Greek" ;el !insertmacro MUI_LANGUAGE "Hebrew" ;he #!insertmacro MUI_LANGUAGE "Hindi" ;hi !insertmacro MUI_LANGUAGE "Hungarian" ;hu !insertmacro MUI_LANGUAGE "Icelandic" #!insertmacro MUI_LANGUAGE "Igbo" !insertmacro MUI_LANGUAGE "Indonesian" ;in !insertmacro MUI_LANGUAGE "Irish" !insertmacro MUI_LANGUAGE "Italian" ;it !insertmacro MUI_LANGUAGE "Japanese" ;ja #!insertmacro MUI_LANGUAGE "Khmer" !insertmacro MUI_LANGUAGE "Korean" ;ko !insertmacro MUI_LANGUAGE "Kurdish" !insertmacro MUI_LANGUAGE "Latvian" ;lv !insertmacro MUI_LANGUAGE "Lithuanian" ;lt !insertmacro MUI_LANGUAGE "Luxembourgish" !insertmacro MUI_LANGUAGE "Macedonian" ;mk #!insertmacro MUI_LANGUAGE "Malagasy" !insertmacro MUI_LANGUAGE "Malay" ;ms !insertmacro MUI_LANGUAGE "Mongolian" !insertmacro MUI_LANGUAGE "Norwegian" ;no !insertmacro MUI_LANGUAGE "NorwegianNynorsk" !insertmacro MUI_LANGUAGE "Polish" ;pl !insertmacro MUI_LANGUAGE "Portuguese" ;pt !insertmacro MUI_LANGUAGE "PortugueseBR" ;pt_BR !insertmacro MUI_LANGUAGE "Romanian" ;ro !insertmacro MUI_LANGUAGE "Russian" ;ru !insertmacro MUI_LANGUAGE "Serbian" !insertmacro MUI_LANGUAGE "SerbianLatin" #!insertmacro MUI_LANGUAGE "Sesotho" !insertmacro MUI_LANGUAGE "SimpChinese" ;zh_CN !insertmacro MUI_LANGUAGE "Slovak" ;sk !insertmacro MUI_LANGUAGE "Slovenian" ;sl !insertmacro MUI_LANGUAGE "Spanish" ;es !insertmacro MUI_LANGUAGE "SpanishInternational" #!insertmacro MUI_LANGUAGE "Swahili" !insertmacro MUI_LANGUAGE "Swedish" ;sv #!insertmacro MUI_LANGUAGE "Tamil" !insertmacro MUI_LANGUAGE "Thai" ;th !insertmacro MUI_LANGUAGE "TradChinese" ;zh_TW !insertmacro MUI_LANGUAGE "Turkish" ;tr #!insertmacro MUI_LANGUAGE "Twi" !insertmacro MUI_LANGUAGE "Ukrainian" ;uk #!insertmacro MUI_LANGUAGE "Uyghur" !insertmacro MUI_LANGUAGE "Uzbek" !insertmacro MUI_LANGUAGE "Vietnamese" ;vn !insertmacro MUI_LANGUAGE "Welsh" ;cy #!insertmacro MUI_LANGUAGE "Yoruba" #!insertmacro MUI_LANGUAGE "Zulu" ; MUI end ------ Name $APP_NAME !ifdef BUILD_UNINSTALLER OutFile "UninstallerGenerator.exe" !else !pragma warning disable 6020 ; Disable warning: Uninstaller script code found but WriteUninstaller never used - no uninstaller will be created !ifdef BUILD_X64_VERSION OutFile "MEGAcmdSetup64.exe" !else OutFile "MEGAcmdSetup32.exe" !endif !endif InstallDir "$PROGRAMFILES\MEGAcmd" InstallDirRegKey HKLM "${PRODUCT_DIR_REGKEY}" "" ShowInstDetails nevershow ShowUnInstDetails nevershow Function RunFunction ${UAC.CallFunctionAsUser} RunMegaCmd FunctionEnd Function RunMegaCmd Exec "$INSTDIR\MEGAcmdShell.exe" Sleep 2000 FunctionEnd Var Dialog Var WinFspCheckbox Var WinFspCheckboxState Var UninstallWinFspCheckbox Var WinFspAlreadyInstalled Var KeepWinFspAtUninstall Var WinFspGuid Var WinFspVer Function WinFspPage ${If} $WinFspAlreadyInstalled == "True" WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "KeepWinFspAtUninstall" "True" Abort ${EndIf} nsDialogs::Create 1018 Pop $Dialog ${If} $Dialog == error Abort ${EndIf} ${NSD_CreateLabel} 0 10u 100% 12u "${PRODUCT_NAME} includes a feature feature called FUSE." ${NSD_CreateLabel} 0 22u 100% 24u "FUSE allows you to view the content of your MEGA folders on your computer without the need to download them. In order for FUSE to work, you need to install WinFSP." ${NSD_CreateCheckbox} 0 54u 100% 12u "Install WinFSP" Pop $WinFspCheckbox ${NSD_CreateLabel} 0 74u 100% 24u "If you don't install WinFSP now, you won't be able to establish FUSE mounts." ${NSD_CreateLabel} 0 98u 100% 24u "Please, tick above checkbox to install it in your system." ${NSD_SetState} $WinFspCheckbox $WinFspCheckboxState nsDialogs::Show FunctionEnd Var PREVIOUS_OUTPATH Function WinFspPageLeave ${NSD_GetState} $WinFspCheckbox $WinFspCheckboxState ReadRegStr $0 ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "KeepWinFspAtUninstall" ${If} $0 == "" ${If} $WinFspCheckboxState == ${BST_CHECKED} StrCpy $KeepWinFspAtUninstall "False" ${Else} StrCpy $KeepWinFspAtUninstall "True" ${EndIf} ${Else} ; In case MEGAcmd was already installed, don't override WinFSP initial installation value StrCpy $KeepWinFspAtUninstall "$0" ${EndIf} WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "KeepWinFspAtUninstall" "$KeepWinFspAtUninstall" ${If} $WinFspCheckboxState == ${BST_CHECKED} strCpy $PREVIOUS_OUTPATH GetOutPath SetOutPath "$TEMP" File "${WinFspInstaller}" SetOutPath $PREVIOUS_OUTPATH ExecWait 'msiexec /i "$TEMP\\${WinFspInstaller}" /qb' Delete "$TEMP\\${WinFspInstaller}" ${EndIf} FunctionEnd Function .onInit !insertmacro MULTIUSER_INIT StrCpy $APP_NAME "${PRODUCT_NAME} ${PRODUCT_VERSION}" !ifdef BUILD_UNINSTALLER WriteUninstaller "$EXEDIR\${UNINSTALLER_NAME}" Quit !endif ;!insertmacro MUI_UNGETLANGUAGE !insertmacro MUI_LANGDLL_DISPLAY ${NSD_SetState} $WinFspCheckbox ${BST_CHECKED} StrCpy $WinFspCheckboxState ${BST_CHECKED} ReadRegStr $0 HKLM "SOFTWARE\Classes\Installer\Dependencies\WinFsp" "" ${If} $0 == "" StrCpy $WinFspAlreadyInstalled "False" ${Else} StrCpy $WinFspAlreadyInstalled "True" ${EndIf} !ifdef BUILD_X64_VERSION ${If} ${RunningX64} ${Else} MessageBox MB_OK "This is an installer for 64-bit MEGAcmd, but you are using a 32-bit Windows. Please, download the 32-bit MEGAcmd version from https://mega.nz/cmd." Quit ${EndIf} !endif ${IfNot} ${AtLeastWin8} MessageBox MB_OK "This MEGAcmd installer is for Windows 8 or above" Quit ${EndIf} UserInfo::GetAccountType pop $0 ${If} $0 != "admin" ;Require admin rights at least in win10; TODO: only ask for this if that's the case MessageBox MB_YESNO "If you would like ${PRODUCT_NAME} to be listed in the installed applications, Admin Privileges are needed. Press Yes to grant that, or No for a plain install." /SD IDNO IDYES elevate IDNO next ${EndIf} elevate: UAC::RunElevated ${Switch} $0 ${Case} 0 ${IfThen} $1 = 1 ${|} Quit ${|} ;User process. The installer has finished. Quit. ${IfThen} $3 <> 0 ${|} ${Break} ${|} ;Admin process, continue the installation ${If} $1 = 3 ;RunAs completed successfully, but with a non-admin user ;MessageBox mb_YesNo|mb_IconExclamation|mb_TopMost|mb_SetForeground "This requires admin privileges, try again" /SD IDNO IDYES uac_tryagain IDNO 0 Quit ${EndIf} ${Case} 1223 ;MessageBox mb_IconStop|mb_TopMost|mb_SetForeground "This requires admin privileges, aborting!" Quit ${Default} MessageBox mb_IconStop|mb_TopMost|mb_SetForeground "This installer requires Administrator privileges. Error $0" Quit ${EndSwitch} Goto next next: !insertmacro DEBUG_MSG "exiting oninit" FunctionEnd Function GetPaths System::Call 'shell32::SHGetSpecialFolderPath(i $HWNDPARENT, t .r1, i ${CSIDL_COMMON_APPDATA}, i0)i.r0' strCpy $ALL_USERS_INSTDIR $1 System::Call "advapi32::GetUserName(t .r0, *i ${NSIS_MAX_STRLEN} r1) i.r2" strCpy $USERNAME $0 System::Call 'shell32::SHGetSpecialFolderPath(i $HWNDPARENT, t .r1, i ${CSIDL_LOCALAPPDATA}, i0)i.r0' strCpy $CURRENT_USER_INSTDIR $1 ClearErrors FileOpen $0 "$ALL_USERS_INSTDIR\megatmp.M1.txt" w IfErrors done FileWriteUTF16LE $0 "$CURRENT_USER_INSTDIR" FileClose $0 FileOpen $0 "$ALL_USERS_INSTDIR\megatmp.M2.txt" w IfErrors done FileWriteUTF16LE $0 "$USERNAME" FileClose $0 done: FunctionEnd !macro Install3264DLL source target File "${source}" AccessControl::SetFileOwner ${target} "$USERNAME" AccessControl::GrantOnFile ${target} "$USERNAME" "GenericRead + GenericWrite" !macroend Section "Principal" SEC01 !insertmacro DEBUG_MSG "Getting needed information" System::Call 'shell32::SHGetSpecialFolderPath(i $HWNDPARENT, t .r1, i ${CSIDL_COMMON_APPDATA}, i0)i.r0' strCpy $ALL_USERS_INSTDIR $1 ${UAC.CallFunctionAsUser} GetPaths readpaths: Sleep 1000 FileOpen $R0 "$ALL_USERS_INSTDIR\megatmp.M1.txt" r IfErrors done FileReadUTF16LE $R0 "$CURRENT_USER_INSTDIR" FileOpen $R1 "$ALL_USERS_INSTDIR\megatmp.M2.txt" r IfErrors done FileReadUTF16LE $R1 "$USERNAME" IfErrors done done: FileClose $R0 FileClose $R1 StrCmp $CURRENT_USER_INSTDIR "" readpaths 0 StrCmp $USERNAME "" readpaths 0 !insertmacro DEBUG_MSG "Checking install mode" Delete "$ALL_USERS_INSTDIR\megatmp.M1.txt" Delete "$ALL_USERS_INSTDIR\megatmp.M2.txt" StrCmp "CurrentUser" $MultiUser.InstallMode currentuser !insertmacro DEBUG_MSG "Install for all" SetShellVarContext all StrCpy $INSTDIR "$ALL_USERS_INSTDIR\MEGAcmd" goto modeselected currentuser: !insertmacro DEBUG_MSG "Install for current user" SetShellVarContext current StrCpy $INSTDIR "$CURRENT_USER_INSTDIR\MEGAcmd" modeselected: !insertmacro DEBUG_MSG "Closing MEGAcmd" ExecDos::exec /DETAILED /DISABLEFSR "taskkill /f /IM MEGAcmdShell.exe" ExecDos::exec /DETAILED /DISABLEFSR "taskkill /f /IM MEGAcmd.exe" ExecDos::exec /DETAILED /DISABLEFSR "taskkill /f /IM MEGAcmdServer.exe" ExecDos::exec /DETAILED /DISABLEFSR "taskkill /f /IM MEGAclient.exe" ExecDos::exec /DETAILED /DISABLEFSR "taskkill /f /IM MEGAcmdUpdater.exe" Sleep 1000 !insertmacro DEBUG_MSG "Installing files" ;SetRebootFlag true SetOverwrite on SetOutPath "$INSTDIR" !ifdef BUILD_X64_VERSION !insertmacro Install3264DLL "${VcRedist64Path}\vcruntime140.dll" "$INSTDIR\vcruntime140.dll" !insertmacro Install3264DLL "${VcRedist64Path}\vcruntime140_1.dll" "$INSTDIR\vcruntime140_1.dll" !insertmacro Install3264DLL "${VcRedist64Path}\msvcp140.dll" "$INSTDIR\msvcp140.dll" !insertmacro Install3264DLL "${VcRedist64Path}\msvcp140_1.dll" "$INSTDIR\msvcp140_1.dll" !insertmacro Install3264DLL "${VcRedist64Path}\msvcp140_2.dll" "$INSTDIR\msvcp140_2.dll" !insertmacro Install3264DLL "${VcRedist64Path}\msvcp140_atomic_wait.dll" "$INSTDIR\msvcp140_atomic_wait.dll" !insertmacro Install3264DLL "${VcRedist64Path}\msvcp140_codecvt_ids.dll" "$INSTDIR\msvcp140_codecvt_ids.dll" !insertmacro Install3264DLL "${VcRedist64Path}\concrt140.dll" "$INSTDIR\concrt140.dll" !insertmacro Install3264DLL "${VcRedist64Path}\vccorlib140.dll" "$INSTDIR\vccorlib140.dll" !insertmacro Install3264DLL "${VcRedist64BasePath}\Microsoft.VC143.OpenMP\vcomp140.dll" "$INSTDIR\vcomp140.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\ucrtbase.dll" "$INSTDIR\ucrtbase.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-crt-utility-l1-1-0.dll" "$INSTDIR\api-ms-win-crt-utility-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-crt-time-l1-1-0.dll" "$INSTDIR\api-ms-win-crt-time-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-crt-string-l1-1-0.dll" "$INSTDIR\api-ms-win-crt-string-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-crt-stdio-l1-1-0.dll" "$INSTDIR\api-ms-win-crt-stdio-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-crt-runtime-l1-1-0.dll" "$INSTDIR\api-ms-win-crt-runtime-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-crt-process-l1-1-0.dll" "$INSTDIR\api-ms-win-crt-process-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-crt-private-l1-1-0.dll" "$INSTDIR\api-ms-win-crt-private-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-crt-multibyte-l1-1-0.dll" "$INSTDIR\api-ms-win-crt-multibyte-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-crt-math-l1-1-0.dll" "$INSTDIR\api-ms-win-crt-math-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-crt-locale-l1-1-0.dll" "$INSTDIR\api-ms-win-crt-locale-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-crt-heap-l1-1-0.dll" "$INSTDIR\api-ms-win-crt-heap-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-crt-filesystem-l1-1-0.dll" "$INSTDIR\api-ms-win-crt-filesystem-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-crt-environment-l1-1-0.dll" "$INSTDIR\api-ms-win-crt-environment-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-crt-convert-l1-1-0.dll" "$INSTDIR\api-ms-win-crt-convert-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-crt-conio-l1-1-0.dll" "$INSTDIR\api-ms-win-crt-conio-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-core-util-l1-1-0.dll" "$INSTDIR\api-ms-win-core-util-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-core-timezone-l1-1-0.dll" "$INSTDIR\api-ms-win-core-timezone-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-core-sysinfo-l1-1-0.dll" "$INSTDIR\api-ms-win-core-sysinfo-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-core-synch-l1-2-0.dll" "$INSTDIR\api-ms-win-core-synch-l1-2-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-core-synch-l1-1-0.dll" "$INSTDIR\api-ms-win-core-synch-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-core-string-l1-1-0.dll" "$INSTDIR\api-ms-win-core-string-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-core-rtlsupport-l1-1-0.dll" "$INSTDIR\api-ms-win-core-rtlsupport-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-core-profile-l1-1-0.dll" "$INSTDIR\api-ms-win-core-profile-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-core-processthreads-l1-1-1.dll" "$INSTDIR\api-ms-win-core-processthreads-l1-1-1.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-core-processthreads-l1-1-0.dll" "$INSTDIR\api-ms-win-core-processthreads-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-core-processenvironment-l1-1-0.dll" "$INSTDIR\api-ms-win-core-processenvironment-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-core-namedpipe-l1-1-0.dll" "$INSTDIR\api-ms-win-core-namedpipe-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-core-memory-l1-1-0.dll" "$INSTDIR\api-ms-win-core-memory-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-core-localization-l1-2-0.dll" "$INSTDIR\api-ms-win-core-localization-l1-2-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-core-libraryloader-l1-1-0.dll" "$INSTDIR\api-ms-win-core-libraryloader-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-core-interlocked-l1-1-0.dll" "$INSTDIR\api-ms-win-core-interlocked-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-core-heap-l1-1-0.dll" "$INSTDIR\api-ms-win-core-heap-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-core-handle-l1-1-0.dll" "$INSTDIR\api-ms-win-core-handle-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-core-file-l2-1-0.dll" "$INSTDIR\api-ms-win-core-file-l2-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-core-file-l1-2-0.dll" "$INSTDIR\api-ms-win-core-file-l1-2-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-core-file-l1-1-0.dll" "$INSTDIR\api-ms-win-core-file-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-core-errorhandling-l1-1-0.dll" "$INSTDIR\api-ms-win-core-errorhandling-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-core-debug-l1-1-0.dll" "$INSTDIR\api-ms-win-core-debug-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-core-datetime-l1-1-0.dll" "$INSTDIR\api-ms-win-core-datetime-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist64BasePath}\api-ms-win-core-console-l1-1-0.dll" "$INSTDIR\api-ms-win-core-console-l1-1-0.dll" !else !insertmacro Install3264DLL "${VcRedist32Path}\vcruntime140.dll" "$INSTDIR\vcruntime140.dll" !insertmacro Install3264DLL "${VcRedist32Path}\msvcp140.dll" "$INSTDIR\msvcp140.dll" !insertmacro Install3264DLL "${VcRedist32Path}\msvcp140_1.dll" "$INSTDIR\msvcp140_1.dll" !insertmacro Install3264DLL "${VcRedist32Path}\msvcp140_2.dll" "$INSTDIR\msvcp140_2.dll" !insertmacro Install3264DLL "${VcRedist32Path}\msvcp140_atomic_wait.dll" "$INSTDIR\msvcp140_atomic_wait.dll" !insertmacro Install3264DLL "${VcRedist32Path}\msvcp140_codecvt_ids.dll" "$INSTDIR\msvcp140_codecvt_ids.dll" !insertmacro Install3264DLL "${VcRedist32Path}\concrt140.dll" "$INSTDIR\concrt140.dll" !insertmacro Install3264DLL "${VcRedist32Path}\vccorlib140.dll" "$INSTDIR\vccorlib140.dll" !insertmacro Install3264DLL "${VcRedist32BasePath}\Microsoft.VC143.OpenMP\vcomp140.dll" "$INSTDIR\vcomp140.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\ucrtbase.dll" "$INSTDIR\ucrtbase.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-crt-utility-l1-1-0.dll" "$INSTDIR\api-ms-win-crt-utility-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-crt-time-l1-1-0.dll" "$INSTDIR\api-ms-win-crt-time-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-crt-string-l1-1-0.dll" "$INSTDIR\api-ms-win-crt-string-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-crt-stdio-l1-1-0.dll" "$INSTDIR\api-ms-win-crt-stdio-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-crt-runtime-l1-1-0.dll" "$INSTDIR\api-ms-win-crt-runtime-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-crt-process-l1-1-0.dll" "$INSTDIR\api-ms-win-crt-process-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-crt-private-l1-1-0.dll" "$INSTDIR\api-ms-win-crt-private-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-crt-multibyte-l1-1-0.dll" "$INSTDIR\api-ms-win-crt-multibyte-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-crt-math-l1-1-0.dll" "$INSTDIR\api-ms-win-crt-math-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-crt-locale-l1-1-0.dll" "$INSTDIR\api-ms-win-crt-locale-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-crt-heap-l1-1-0.dll" "$INSTDIR\api-ms-win-crt-heap-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-crt-filesystem-l1-1-0.dll" "$INSTDIR\api-ms-win-crt-filesystem-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-crt-environment-l1-1-0.dll" "$INSTDIR\api-ms-win-crt-environment-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-crt-convert-l1-1-0.dll" "$INSTDIR\api-ms-win-crt-convert-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-crt-conio-l1-1-0.dll" "$INSTDIR\api-ms-win-crt-conio-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-core-util-l1-1-0.dll" "$INSTDIR\api-ms-win-core-util-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-core-timezone-l1-1-0.dll" "$INSTDIR\api-ms-win-core-timezone-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-core-sysinfo-l1-1-0.dll" "$INSTDIR\api-ms-win-core-sysinfo-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-core-synch-l1-2-0.dll" "$INSTDIR\api-ms-win-core-synch-l1-2-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-core-synch-l1-1-0.dll" "$INSTDIR\api-ms-win-core-synch-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-core-string-l1-1-0.dll" "$INSTDIR\api-ms-win-core-string-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-core-rtlsupport-l1-1-0.dll" "$INSTDIR\api-ms-win-core-rtlsupport-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-core-profile-l1-1-0.dll" "$INSTDIR\api-ms-win-core-profile-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-core-processthreads-l1-1-1.dll" "$INSTDIR\api-ms-win-core-processthreads-l1-1-1.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-core-processthreads-l1-1-0.dll" "$INSTDIR\api-ms-win-core-processthreads-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-core-processenvironment-l1-1-0.dll" "$INSTDIR\api-ms-win-core-processenvironment-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-core-namedpipe-l1-1-0.dll" "$INSTDIR\api-ms-win-core-namedpipe-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-core-memory-l1-1-0.dll" "$INSTDIR\api-ms-win-core-memory-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-core-localization-l1-2-0.dll" "$INSTDIR\api-ms-win-core-localization-l1-2-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-core-libraryloader-l1-1-0.dll" "$INSTDIR\api-ms-win-core-libraryloader-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-core-interlocked-l1-1-0.dll" "$INSTDIR\api-ms-win-core-interlocked-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-core-heap-l1-1-0.dll" "$INSTDIR\api-ms-win-core-heap-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-core-handle-l1-1-0.dll" "$INSTDIR\api-ms-win-core-handle-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-core-file-l2-1-0.dll" "$INSTDIR\api-ms-win-core-file-l2-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-core-file-l1-2-0.dll" "$INSTDIR\api-ms-win-core-file-l1-2-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-core-file-l1-1-0.dll" "$INSTDIR\api-ms-win-core-file-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-core-errorhandling-l1-1-0.dll" "$INSTDIR\api-ms-win-core-errorhandling-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-core-debug-l1-1-0.dll" "$INSTDIR\api-ms-win-core-debug-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-core-datetime-l1-1-0.dll" "$INSTDIR\api-ms-win-core-datetime-l1-1-0.dll" !insertmacro Install3264DLL "${WinRedist32BasePath}\api-ms-win-core-console-l1-1-0.dll" "$INSTDIR\api-ms-win-core-console-l1-1-0.dll" !endif !ifndef BUILD_UNINSTALLER ; if building uninstaller, skip files below ;x86_32 files Delete "$INSTDIR\MEGAcmd.exe" ; delete older name of server Delete "$INSTDIR\mega-history.bat" ; delete older bat SetOutPath "$INSTDIR" SetOverwrite on AllowSkipFiles off !insertmacro Install3264DLL "${SRCDIR_MEGACMD}\MEGAcmdServer.exe" "$INSTDIR\MEGAcmdServer.exe" !insertmacro Install3264DLL "${SRCDIR_MEGACMD}\MEGAclient.exe" "$INSTDIR\MEGAclient.exe" !insertmacro Install3264DLL "${SRCDIR_MEGACMD}\MEGAcmdShell.exe" "$INSTDIR\MEGAcmdShell.exe" !insertmacro Install3264DLL "${SRCDIR_MEGACMD}\MEGAcmdUpdater.exe" "$INSTDIR\MEGAcmdUpdater.exe" !insertmacro Install3264DLL "${SRCDIR_MEGACMD}\avcodec-61.dll" "$INSTDIR\avcodec-61.dll" !insertmacro Install3264DLL "${SRCDIR_MEGACMD}\avformat-61.dll" "$INSTDIR\avformat-61.dll" !insertmacro Install3264DLL "${SRCDIR_MEGACMD}\avutil-59.dll" "$INSTDIR\avutil-59.dll" !insertmacro Install3264DLL "${SRCDIR_MEGACMD}\swscale-8.dll" "$INSTDIR\swscale-8.dll" !insertmacro Install3264DLL "${SRCDIR_MEGACMD}\swresample-5.dll" "$INSTDIR\swresample-5.dll" ;remove old DLLs that we no longer use (some became static; some have later version number) Delete "$INSTDIR\avcodec-57.dll" Delete "$INSTDIR\avcodec-59.dll" Delete "$INSTDIR\avformat-57.dll" Delete "$INSTDIR\avformat-59.dll" Delete "$INSTDIR\avutil-55.dll" Delete "$INSTDIR\avutil-57.dll" Delete "$INSTDIR\swscale-4.dll" Delete "$INSTDIR\swscale-6.dll" Delete "$INSTDIR\swresample-2.dll" Delete "$INSTDIR\swresample-4.dll" Delete "$INSTDIR\libsodium.dll" Delete "$INSTDIR\pdfium.dll" Delete "$INSTDIR\FreeImage.dll" Delete "$INSTDIR\avcodec-58.dll" Delete "$INSTDIR\avformat-58.dll" Delete "$INSTDIR\avutil-56.dll" Delete "$INSTDIR\swscale-5.dll" Delete "$INSTDIR\swresample-3.dll" Delete "$INSTDIR\libcrypto-1_1-x64.dll" Delete "$INSTDIR\libssl-1_1-x64.dll" Delete "$INSTDIR\libcrypto-1_1.dll" Delete "$INSTDIR\libssl-1_1.dll" ; BAT files !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-attr.bat" "$INSTDIR\mega-attr.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-cd.bat" "$INSTDIR\mega-cd.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-confirm.bat" "$INSTDIR\mega-confirm.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-cp.bat" "$INSTDIR\mega-cp.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-debug.bat" "$INSTDIR\mega-debug.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-du.bat" "$INSTDIR\mega-du.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-df.bat" "$INSTDIR\mega-df.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-proxy.bat" "$INSTDIR\mega-proxy.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-export.bat" "$INSTDIR\mega-export.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-find.bat" "$INSTDIR\mega-find.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-get.bat" "$INSTDIR\mega-get.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-help.bat" "$INSTDIR\mega-help.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-https.bat" "$INSTDIR\mega-https.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-webdav.bat" "$INSTDIR\mega-webdav.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-deleteversions.bat" "$INSTDIR\mega-deleteversions.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-transfers.bat" "$INSTDIR\mega-transfers.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-import.bat" "$INSTDIR\mega-import.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-invite.bat" "$INSTDIR\mega-invite.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-ipc.bat" "$INSTDIR\mega-ipc.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-killsession.bat" "$INSTDIR\mega-killsession.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-lcd.bat" "$INSTDIR\mega-lcd.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-log.bat" "$INSTDIR\mega-log.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-login.bat" "$INSTDIR\mega-login.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-logout.bat" "$INSTDIR\mega-logout.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-lpwd.bat" "$INSTDIR\mega-lpwd.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-ls.bat" "$INSTDIR\mega-ls.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-backup.bat" "$INSTDIR\mega-backup.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-mkdir.bat" "$INSTDIR\mega-mkdir.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-mount.bat" "$INSTDIR\mega-mount.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-mv.bat" "$INSTDIR\mega-mv.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-passwd.bat" "$INSTDIR\mega-passwd.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-preview.bat" "$INSTDIR\mega-preview.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-put.bat" "$INSTDIR\mega-put.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-pwd.bat" "$INSTDIR\mega-pwd.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-quit.bat" "$INSTDIR\mega-quit.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-reload.bat" "$INSTDIR\mega-reload.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-rm.bat" "$INSTDIR\mega-rm.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-session.bat" "$INSTDIR\mega-session.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-share.bat" "$INSTDIR\mega-share.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-showpcr.bat" "$INSTDIR\mega-showpcr.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-signup.bat" "$INSTDIR\mega-signup.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-speedlimit.bat" "$INSTDIR\mega-speedlimit.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-sync.bat" "$INSTDIR\mega-sync.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-exclude.bat" "$INSTDIR\mega-exclude.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-thumbnail.bat" "$INSTDIR\mega-thumbnail.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-userattr.bat" "$INSTDIR\mega-userattr.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-users.bat" "$INSTDIR\mega-users.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-version.bat" "$INSTDIR\mega-version.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-whoami.bat" "$INSTDIR\mega-whoami.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-cat.bat" "$INSTDIR\mega-cat.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-tree.bat" "$INSTDIR\mega-tree.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-mediainfo.bat" "$INSTDIR\mega-mediainfo.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-graphics.bat" "$INSTDIR\mega-graphics.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-ftp.bat" "$INSTDIR\mega-ftp.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-cancel.bat" "$INSTDIR\mega-cancel.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-confirmcancel.bat" "$INSTDIR\mega-confirmcancel.bat" !insertmacro Install3264DLL "${SRCDIR_BATFILES}\mega-errorcode.bat" "$INSTDIR\mega-errorcode.bat" ; Uninstaller ;!ifndef BUILD_UNINSTALLER ; if building uninstaller, skip this check !insertmacro Install3264DLL "${SRCDIR_MEGACMD}\${UNINSTALLER_NAME}" "$INSTDIR\${UNINSTALLER_NAME}" !endif !insertmacro DEBUG_MSG "Creating shortcuts" SetRebootFlag false StrCmp "CurrentUser" $MultiUser.InstallMode currentuser2 SetShellVarContext all !insertmacro MUI_STARTMENU_WRITE_BEGIN Application CreateDirectory "$SMPROGRAMS\$ICONS_GROUP" CreateShortCut "$SMPROGRAMS\$ICONS_GROUP\MEGAcmd.lnk" "$INSTDIR\MEGAcmdShell.exe" CreateShortCut "$SMPROGRAMS\$ICONS_GROUP\MEGAcmdServer.lnk" "$INSTDIR\MEGAcmdServer.exe" CreateShortCut "$SMPROGRAMS\$ICONS_GROUP\Update MEGAcmd.lnk" "$INSTDIR\MEGAcmdUpdater.exe" CreateShortCut "$DESKTOP\MEGAcmd.lnk" "$INSTDIR\MEGAcmdShell.exe" WriteIniStr "$INSTDIR\MEGA Website.url" "InternetShortcut" "URL" "${PRODUCT_WEB_SITE}" CreateShortCut "$SMPROGRAMS\$ICONS_GROUP\MEGA Website.lnk" "$INSTDIR\MEGA Website.url" "" "$INSTDIR\MEGAcmdShell.exe" 1 CreateShortCut "$SMPROGRAMS\$ICONS_GROUP\Uninstall MEGAcmd.lnk" "$INSTDIR\${UNINSTALLER_NAME}" !insertmacro MUI_STARTMENU_WRITE_END goto modeselected2 currentuser2: SetShellVarContext current !insertmacro MUI_STARTMENU_WRITE_BEGIN Application CreateDirectory "$SMPROGRAMS\$ICONS_GROUP" CreateShortCut "$SMPROGRAMS\$ICONS_GROUP\MEGAcmd.lnk" "$INSTDIR\MEGAcmdShell.exe" CreateShortCut "$SMPROGRAMS\$ICONS_GROUP\MEGAcmdServer.lnk" "$INSTDIR\MEGAcmdServer.exe" CreateShortCut "$SMPROGRAMS\$ICONS_GROUP\Update MEGAcmd.lnk" "$INSTDIR\MEGAcmdUpdater.exe" CreateShortCut "$DESKTOP\MEGAcmd.lnk" "$INSTDIR\MEGAcmdShell.exe" WriteIniStr "$INSTDIR\MEGA Website.url" "InternetShortcut" "URL" "${PRODUCT_WEB_SITE}" CreateShortCut "$SMPROGRAMS\$ICONS_GROUP\MEGA Website.lnk" "$INSTDIR\MEGA Website.url" "" "$INSTDIR\MEGAcmdShell.exe" 1 CreateShortCut "$SMPROGRAMS\$ICONS_GROUP\Uninstall MEGAcmd.lnk" "$INSTDIR\${UNINSTALLER_NAME}" !insertmacro MUI_STARTMENU_WRITE_END modeselected2: SectionEnd Section -AdditionalIcons SectionEnd Section -Post WriteRegStr HKLM "${PRODUCT_DIR_REGKEY}" "" "$INSTDIR\MEGAcmdShell.exe" WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayName" "${PRODUCT_NAME}" WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "UninstallString" "$INSTDIR\${UNINSTALLER_NAME}" WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayIcon" "$INSTDIR\MEGAcmdShell.exe" WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayVersion" "" WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}" WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "Publisher" "${PRODUCT_PUBLISHER}" AccessControl::SetFileOwner "$INSTDIR\MEGA Website.url" "$USERNAME" AccessControl::GrantOnFile "$INSTDIR\MEGA Website.url" "$USERNAME" "GenericRead + GenericWrite" SectionEnd Function un.onInit !insertmacro MULTIUSER_UNINIT StrCpy $APP_NAME "${PRODUCT_NAME}" ReadIniStr $0 "$ExeDir\${MEGA_DATA}" UAC first ${IF} $0 <> 1 ;SetSilent silent InitPluginsDir WriteIniStr "$PluginsDir\${MEGA_DATA}" UAC first 1 CopyFiles /SILENT "$EXEPATH" "$PluginsDir\${UNINSTALLER_NAME}" ExecWait '"$PluginsDir\${UNINSTALLER_NAME}" _?=$INSTDIR' $0 SetErrorLevel $0 Quit ${EndIf} !insertmacro MUI_UNGETLANGUAGE FunctionEnd Function un.UninstallCmd ExecDos::exec "$INSTDIR\MEGAcmdServer.exe --uninstall" FunctionEnd Function un.WinFspUninstall !insertmacro MUI_HEADER_TEXT "Uninstallation complete" "${PRODUCT_NAME} was uninstalled successfully" ${If} $KeepWinFspAtUninstall == "True" Abort ${EndIf} ReadRegStr $WinFspGuid HKLM "SOFTWARE\Classes\Installer\Dependencies\WinFsp" "" ${If} $WinFspGuid == "" Abort ${EndIf} ReadRegStr $WinFspVer HKLM "SOFTWARE\Classes\Installer\Dependencies\WinFsp" "Version" ; TODO: improvement abort in case Version differs from the one installed by MEGAcmd nsDialogs::Create 1018 Pop $Dialog ${If} $Dialog == error Abort ${EndIf} ${NSD_CreateLabel} 0 10u 100% 12u "WinFsp $WinFspVer was installed by ${PRODUCT_NAME} to support some features." ${NSD_CreateLabel} 0 22u 100% 24u "Do you also want to uninstall WinFsp?" ${NSD_CreateCheckbox} 0 54u 100% 12u "Uninstall WinFsp" Pop $UninstallWinFspCheckbox ${NSD_SetState} $UninstallWinFspCheckbox ${BST_CHECKED} nsDialogs::Show FunctionEnd Function un.WinFspUninstallLeave ${NSD_GetState} $UninstallWinFspCheckbox $WinFspCheckboxState ${If} $WinFspCheckboxState == ${BST_CHECKED} ExecWait 'msiexec /uninstall $WinFspGuid /qb' ${EndIf} FunctionEnd Section Uninstall ExecDos::exec /DETAILED "taskkill /f /IM MEGAcmdShell.exe" ExecDos::exec /DETAILED "taskkill /f /IM MEGAclient.exe" ExecDos::exec /DETAILED "taskkill /f /IM MEGAcmd.exe" ExecDos::exec /DETAILED "taskkill /f /IM MEGAcmdServer.exe" ExecDos::exec /DETAILED "taskkill /f /IM MEGAcmdUpdater.exe" Sleep 1000 ${UAC.CallFunctionAsUser} un.UninstallCmd Sleep 1000 !insertmacro MUI_STARTMENU_GETFOLDER "Application" $ICONS_GROUP Delete "$INSTDIR\${PRODUCT_NAME}.url" Delete "$INSTDIR\${UNINSTALLER_NAME}" ;VC++ Redistributable Delete "$INSTDIR\vcruntime140.dll" Delete "$INSTDIR\vcruntime140_1.dll" Delete "$INSTDIR\msvcp140.dll" Delete "$INSTDIR\msvcp140_1.dll" Delete "$INSTDIR\msvcp140_2.dll" Delete "$INSTDIR\msvcp140_codecvt_ids.dll" Delete "$INSTDIR\msvcp140_atomic_wait.dll" Delete "$INSTDIR\concrt140.dll" Delete "$INSTDIR\vccorlib140.dll" Delete "$INSTDIR\vcomp140.dll" Delete "$INSTDIR\ucrtbase.dll" Delete "$INSTDIR\api-ms-win-crt-utility-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-crt-time-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-crt-string-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-crt-stdio-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-crt-runtime-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-crt-process-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-crt-private-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-crt-multibyte-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-crt-math-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-crt-locale-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-crt-heap-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-crt-filesystem-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-crt-environment-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-crt-convert-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-crt-conio-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-core-util-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-core-timezone-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-core-sysinfo-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-core-synch-l1-2-0.dll" Delete "$INSTDIR\api-ms-win-core-synch-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-core-string-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-core-rtlsupport-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-core-profile-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-core-processthreads-l1-1-1.dll" Delete "$INSTDIR\api-ms-win-core-processthreads-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-core-processenvironment-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-core-namedpipe-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-core-memory-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-core-localization-l1-2-0.dll" Delete "$INSTDIR\api-ms-win-core-libraryloader-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-core-interlocked-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-core-heap-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-core-handle-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-core-file-l2-1-0.dll" Delete "$INSTDIR\api-ms-win-core-file-l1-2-0.dll" Delete "$INSTDIR\api-ms-win-core-file-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-core-errorhandling-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-core-debug-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-core-datetime-l1-1-0.dll" Delete "$INSTDIR\api-ms-win-core-console-l1-1-0.dll" ;Common files Delete "$INSTDIR\MEGAcmdUpdater.exe" Delete "$INSTDIR\MEGAcmdServer.exe" Delete "$INSTDIR\MEGAcmd.exe" Delete "$INSTDIR\MEGAcmdShell.exe" Delete "$INSTDIR\MEGAclient.exe" Delete "$INSTDIR\libeay32.dll" Delete "$INSTDIR\ssleay32.dll" Delete "$INSTDIR\libcurl.dll" Delete "$INSTDIR\cares.dll" Delete "$INSTDIR\libuv.dll" Delete "$INSTDIR\NSIS.Library.RegTool*.exe" Delete "$INSTDIR\avcodec-61.dll" Delete "$INSTDIR\avformat-61.dll" Delete "$INSTDIR\avutil-59.dll" Delete "$INSTDIR\swscale-8.dll" Delete "$INSTDIR\swresample-5.dll" ;!ifdef BUILD_X64_VERSION Delete "$INSTDIR\libssl-3-x64.dll" Delete "$INSTDIR\libcrypto-3-x64.dll" ;!else Delete "$INSTDIR\libcrypto-3.dll" Delete "$INSTDIR\libssl-3.dll" ;!endif ;Still remove old DLLs though we no longer produce them (non-VCPKG may still produce them) Delete "$INSTDIR\avcodec-57.dll" Delete "$INSTDIR\avformat-57.dll" Delete "$INSTDIR\avutil-55.dll" Delete "$INSTDIR\swscale-4.dll" Delete "$INSTDIR\swresample-2.dll" Delete "$INSTDIR\libsodium.dll" Delete "$INSTDIR\pdfium.dll" Delete "$INSTDIR\FreeImage.dll" Delete "$INSTDIR\libcrypto-1_1-x64.dll" Delete "$INSTDIR\libssl-1_1-x64.dll" Delete "$INSTDIR\libcrypto-1_1.dll" Delete "$INSTDIR\libssl-1_1.dll" Delete "$INSTDIR\avcodec-58.dll" Delete "$INSTDIR\avformat-58.dll" Delete "$INSTDIR\avutil-56.dll" Delete "$INSTDIR\swscale-5.dll" Delete "$INSTDIR\swresample-3.dll" Delete "$INSTDIR\avcodec-59.dll" Delete "$INSTDIR\avformat-59.dll" Delete "$INSTDIR\avutil-57.dll" Delete "$INSTDIR\swscale-6.dll" Delete "$INSTDIR\swresample-4.dll" ; BAT files Delete "$INSTDIR\mega-attr.bat" Delete "$INSTDIR\mega-cd.bat" Delete "$INSTDIR\mega-confirm.bat" Delete "$INSTDIR\mega-cp.bat" Delete "$INSTDIR\mega-debug.bat" Delete "$INSTDIR\mega-du.bat" Delete "$INSTDIR\mega-df.bat" Delete "$INSTDIR\mega-proxy.bat" Delete "$INSTDIR\mega-export.bat" Delete "$INSTDIR\mega-find.bat" Delete "$INSTDIR\mega-get.bat" Delete "$INSTDIR\mega-help.bat" Delete "$INSTDIR\mega-history.bat" Delete "$INSTDIR\mega-https.bat" Delete "$INSTDIR\mega-webdav.bat" Delete "$INSTDIR\mega-deleteversions.bat" Delete "$INSTDIR\mega-transfers.bat" Delete "$INSTDIR\mega-import.bat" Delete "$INSTDIR\mega-invite.bat" Delete "$INSTDIR\mega-ipc.bat" Delete "$INSTDIR\mega-killsession.bat" Delete "$INSTDIR\mega-lcd.bat" Delete "$INSTDIR\mega-log.bat" Delete "$INSTDIR\mega-login.bat" Delete "$INSTDIR\mega-logout.bat" Delete "$INSTDIR\mega-lpwd.bat" Delete "$INSTDIR\mega-ls.bat" Delete "$INSTDIR\mega-backup.bat" Delete "$INSTDIR\mega-mkdir.bat" Delete "$INSTDIR\mega-mount.bat" Delete "$INSTDIR\mega-mv.bat" Delete "$INSTDIR\mega-passwd.bat" Delete "$INSTDIR\mega-preview.bat" Delete "$INSTDIR\mega-put.bat" Delete "$INSTDIR\mega-pwd.bat" Delete "$INSTDIR\mega-quit.bat" Delete "$INSTDIR\mega-reload.bat" Delete "$INSTDIR\mega-rm.bat" Delete "$INSTDIR\mega-session.bat" Delete "$INSTDIR\mega-share.bat" Delete "$INSTDIR\mega-showpcr.bat" Delete "$INSTDIR\mega-signup.bat" Delete "$INSTDIR\mega-speedlimit.bat" Delete "$INSTDIR\mega-sync.bat" Delete "$INSTDIR\mega-exclude.bat" Delete "$INSTDIR\mega-thumbnail.bat" Delete "$INSTDIR\mega-userattr.bat" Delete "$INSTDIR\mega-users.bat" Delete "$INSTDIR\mega-version.bat" Delete "$INSTDIR\mega-whoami.bat" Delete "$INSTDIR\mega-cat.bat" Delete "$INSTDIR\mega-tree.bat" Delete "$INSTDIR\mega-mediainfo.bat" Delete "$INSTDIR\mega-graphics.bat" Delete "$INSTDIR\mega-ftp.bat" Delete "$INSTDIR\mega-cancel.bat" Delete "$INSTDIR\mega-confirmcancel.bat" Delete "$INSTDIR\mega-errorcode.bat" ; Cache RMDir /r "$INSTDIR\.megaCmd" ReadRegStr $KeepWinFspAtUninstall ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "KeepWinFspAtUninstall" SetShellVarContext current Delete "$SMPROGRAMS\$ICONS_GROUP\Uninstall MEGAcmd.lnk" Delete "$SMPROGRAMS\$ICONS_GROUP\MEGA Website.lnk" Delete "$INSTDIR\MEGA Website.url" Delete "$DESKTOP\MEGAcmd.lnk" Delete "$SMPROGRAMS\$ICONS_GROUP\MEGAcmd.lnk" Delete "$SMPROGRAMS\$ICONS_GROUP\MEGAcmdServer.lnk" System::Call 'shell32::SHGetSpecialFolderPath(i $HWNDPARENT, t .r1, i ${CSIDL_STARTUP}, i0)i.r0' Delete "$1\MEGAcmd.lnk" RMDir "$SMPROGRAMS\$ICONS_GROUP" RMDir "$INSTDIR" SetShellVarContext all Delete "$SMPROGRAMS\$ICONS_GROUP\Uninstall MEGAcmd.lnk" Delete "$SMPROGRAMS\$ICONS_GROUP\MEGA Website.lnk" Delete "$INSTDIR\MEGA Website.url" Delete "$DESKTOP\MEGAcmd.lnk" Delete "$SMPROGRAMS\$ICONS_GROUP\MEGAcmd.lnk" Delete "$SMPROGRAMS\$ICONS_GROUP\MEGAcmdServer.lnk" System::Call 'shell32::SHGetSpecialFolderPath(i $HWNDPARENT, t .r1, i ${CSIDL_STARTUP}, i0)i.r0' Delete "$1\MEGAcmd.lnk" RMDir "$SMPROGRAMS\$ICONS_GROUP" RMDir "$INSTDIR" DeleteRegKey ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" DeleteRegKey HKLM "${PRODUCT_DIR_REGKEY}" SetAutoClose true SetRebootFlag false SectionEnd MEGAcmd-2.5.2_Linux/build/make_installers.cmd000066400000000000000000000033661516543156300210560ustar00rootroot00000000000000IF "%1%" EQU "-help" ( goto Usage ) SET SUFFIX_DEF= if not "%MEGA_VERSION_SUFFIX%" == "" ( SET SUFFIX_DEF=/DVERSION_SUFFIX=%MEGA_VERSION_SUFFIX% ) SET MEGA_SIGN=%1 :: DOUBLE CHECK SIGN IF "%MEGA_SIGN%" EQU "sign" ( echo "Info: Signed installer(s) will be generated. Checking if sources are signed" "C:\Program Files (x86)\Windows Kits\10\bin\%MEGA_WIN_KITVER%\x64\signtool.exe" verify /pa built64/*.exe || exit 1 /b "C:\Program Files (x86)\Windows Kits\10\bin\%MEGA_WIN_KITVER%\x64\signtool.exe" verify /pa built64/*.dll || exit 1 /b IF NOT "%MEGA_SKIP_32_BIT_BUILD%" == "true" ( "C:\Program Files (x86)\Windows Kits\10\bin\%MEGA_WIN_KITVER%\x64\signtool.exe" verify /pa built32/*.exe || exit 1 /b "C:\Program Files (x86)\Windows Kits\10\bin\%MEGA_WIN_KITVER%\x64\signtool.exe" verify /pa built32/*.dll || exit 1 /b ) ) ELSE ( IF "%MEGA_SIGN%" EQU "nosign" ( echo "Info: Unsigned installer(s) will be generated" ) ELSE ( echo "Please add a correct sign argument: sign or nosign" goto Usage ) ) erase MEGAcmdSetup64.exe 2>nul erase MEGAcmdSetup64_unsigned.exe 2>nul "C:\Program Files (x86)\NSIS\makensis.exe" /DWINKITVER=%MEGA_WIN_KITVER% /DBUILD_X64_VERSION %SUFFIX_DEF% installer_win.nsi || exit 1 /b IF "%MEGA_SIGN%" EQU "nosign" ( ren MEGAcmdSetup64.exe MEGAcmdSetup64_unsigned.exe ) IF "%MEGA_SKIP_32_BIT_BUILD%" == "true" ( GOTO :EOF ) erase MEGAcmdSetup32.exe >nul erase MEGAcmdSetup32_unsigned.exe >nul "C:\Program Files (x86)\NSIS\makensis.exe" /DWINKITVER=%MEGA_WIN_KITVER% %SUFFIX_DEF% installer_win.nsi || exit 1 /b IF "%MEGA_SIGN%" EQU "nosign" ( ren MEGAcmdSetup32.exe MEGAcmdSetup32_unsigned.exe ) exit /B :Usage echo "Usage: %~0 [-help] [sign|nosign]" echo Script for making the installer, expecting files in built folders" exit 2 /b MEGAcmd-2.5.2_Linux/build/make_uninstallers.cmd000066400000000000000000000012361516543156300214130ustar00rootroot00000000000000SET SUFFIX_DEF= if not "%MEGA_VERSION_SUFFIX%" == "" ( SET SUFFIX_DEF=/DVERSION_SUFFIX=%MEGA_VERSION_SUFFIX% ) "C:\Program Files (x86)\NSIS\makensis.exe" /DWINKITVER=%MEGA_WIN_KITVER% /DBUILD_UNINSTALLER /DBUILD_X64_VERSION %SUFFIX_DEF% installer_win.nsi || exit 1 /b UninstallerGenerator.exe erase UninstallerGenerator.exe copy uninst.exe built64 move uninst.exe sign64 IF "%MEGA_SKIP_32_BIT_BUILD%" == "true" ( GOTO :EOF ) "C:\Program Files (x86)\NSIS\makensis.exe" /DWINKITVER=%MEGA_WIN_KITVER% /DBUILD_UNINSTALLER %SUFFIX_DEF% installer_win.nsi || exit 1 /b UninstallerGenerator.exe erase UninstallerGenerator.exe copy uninst.exe built32 move uninst.exe sign32 MEGAcmd-2.5.2_Linux/build/megacmd/000077500000000000000000000000001516543156300166015ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/build/megacmd/debian.changelog000066400000000000000000000354241516543156300217040ustar00rootroot00000000000000megacmd (2.5.2) stable; urgency=low * Fix: address potential loop in sync engine with mtime changes within 2 seconds -- MEGA Team Wed, 08 Apr 2026 12:00:00 +0200 megacmd (2.5.1) stable; urgency=low * Fixed syncrhonizing updated empty files * WSL bash completion speedup -- MEGA Team Tue, 24 Mar 2026 09:36:34 +0100 megacmd (2.5.0) stable; urgency=low * Shell UX improvements: empty Enter shows a new prompt; no duplicate prompt while command is executing * Sync engine improvements: prevent re-uploading data when local drive fingerprint has changed * put: Fixed -c (autocreate) not working when destination is an absolute path * mediainfo: Fixed audio file duration not being displayed * Transfers: prevent failures on resumption * thumbnail/preview: Print error when the requested file does not exist * share: Return success (exit code 0) when no shares exist below the current folder * passwd: fixes issues when 2FA is enabled * Path and string handling fixes (trailing separators, trimming) * Expanded unit test coverage * Fix: Improved restart-after-update reliability on POSIX (server PID changes after update) * Stability, memory, typos and other improvements -- MEGA Team Wed, 11 Mar 2026 10:36:46 +0000 megacmd (2.4.0) stable; urgency=low * Improvements in uploads UX/UI: put now supports printing tag. Fixes in transfers table * Stability improvements: crashes fixes, FUSE deadlocks, ... * Performance boosts: lockless download URL retrievals, removed CPU bottlenecks for large accounts processing, db operations improvements, transfers improvements ... * Typos and other improvements -- MEGA Team Tue, 16 Dec 2025 17:14:03 +0100 megacmd (2.3.0) stable; urgency=low * FUSE (beta): Added fuse commands on Windows to allow your MEGA folders to be directly mounted to your local drive * Other fixes and improvements to enhance reliability and performance -- MEGA Team Tue, 02 Sep 2025 16:43:10 +0200 megacmd (2.2.0) stable; urgency=low * Support building for ARM64 in linux * New command "configure" to allow configuring some settings * Support setting max number of nodes in memory * Support setting number of SDK instances used for login in folders to download/import * Other fixes and improvements to enhance reliability and performance -- MEGA Team Tue, 03 Jun 2025 23:20:12 +0200 megacmd (2.1.1) stable; urgency=low * FUSE (beta): Added fuse commands on Linux to allow your MEGA folders to be directly mounted to your local drive * Delayed sync uploads: Introduced a mechanism to delay to frequently changed sync uploads, and the sync-config command * Logging: Messages are now printed in standard error, the rotating logger is now configurable and more verbose by default, passwords are now redacted from the logs, and other fixes and refinements * Fixed a crash when auto-completing a local folder that doesn't exist * Fixed the confirmcancel command incorrectly reporting failure on success * Extended speedlimit command to allow increasing max connections * Other fixes and improvements to enhance reliability and performance -- MEGA Team Wed, 02 Apr 2025 11:08:20 +0200 megacmd (2.0.0) stable; urgency=low * New Sync Engine: See sync-issues and sync-ignore commands * Rotating Logger: Introduced a robust rotating logging system across all platforms for better performance and debugging * Platform-specific enhancements: Addressed various file descriptor issues on Linux and macOS, and improved non-ascii support on Windows * Improved overall reliability: Fixed memory leaks, resolved potential data races, and eliminated deadlock scenarios * Fixed an issue when handling double-quoted arguments * Various fixes and refinements to enhance usability and performance -- MEGA Team Fri, 24 Jan 2025 13:20:06 +0100 megacmd (1.7.0) stable; urgency=low * Improved startup time * Reduced memory consumption: cached metadata is no longer loaded at startup * Fixes and improvements in whoami, ls, backup and export commands * Fixed several memory leaks and improved overall memory consumption * Fixed file permissions on Unix * Added support for the Apple silicon (M1) * Many other fixes and improvements -- MEGA Team Mon, 20 May 2024 14:07:19 +0200 megacmd (1.6.3) stable; urgency=low * Improvements in session resumption * Fixes for undecryptable nodes * Stability and preformance fixes -- MEGA Team Thu, 04 May 2023 16:18:29 +0200 megacmd (1.6.1) stable; urgency=low * Support login into a password protected link * Ease establishing shares * Fixes in tranfer resumption * Fixes in password protected links * Improvements shares management -- MEGA Team Fri, 24 Mar 2023 13:02:52 +0100 megacmd (1.6.0) stable; urgency=low * Improvements in "find" to search by type, or print only handles (to ease automation) * Improved state management and reporting in synchronizations * Improved information on shares * Improvements in "attr" * Security adjustments * Other fixes and adjustments -- MEGA Team Wed, 01 Mar 2023 13:26:37 +0100 megacmd (1.5.1) stable; urgency=low * Security adjustments -- MEGA Team Wed, 15 Jun 2022 17:55:34 +0200 megacmd (1.5.0) stable; urgency=low * Add support for pdfs (uploading them will create thumbnails/previews) * Improve communications with server in POSIX: no longer create multiple sockets * Support resuming session when logged into a folder * Renew and improve sync management and improve status reporting * Commands sync and transfers now allow for selecting --output-cols * Add creation time to ls * Fix issue running from php (due to a dangling file descriptor) * Fix several crashes, leaks and major performance improvements * Other fixes & adjustments -- MEGA Team Fri, 3 Dec 2021 13:44:32 +0100 megacmd (1.4.1) stable; urgency=low * Fix issues with backups timestamps in Raspbian and other OS * Allow uploads when reached bandwidth overquota * Fix for syncing mounted drives in Windows * Fix issues in sync resumption for MacOS * Improvements in filename escaping * Improvements in transfers cancellation * Improve uploads stalling in Windows * Fix crash in MEGAcmd shell when typing CTRL+D while inserting 2FA code * Warn incoming Windows XP deprecation * Improvements in information messages * Fix issues with too long paths in MEGAcmd server * Prepare for over storage quota announcements * Other fixes & adjustments -- MEGA Team Thu, 22 Oct 2020 13:21:02 +0200 megacmd (1.4.0) stable; urgency=low * Fix issues with backups timestamps in Raspbian and other OS * Allow uploads when reached bandwidth overquota * Fix for syncing mounted drives in Windows * Fix issues in sync resumption for MacOS * Improvements in filename escaping * Improvements in transfers cancellation * Improve uploads stalling in Windows * Fix crash in MEGAcmd shell when typing CTRL+D while inserting 2FA code * Warn incoming Windows XP deprecation * Improvements in information messages * Fix issues with too long paths in MEGAcmd server * Prepare for over storage quota announcements * Other fixes & adjustments -- MEGA Team Fri, 21 Aug 2020 17:54:49 +0200 megacmd (1.3.0) stable; urgency=low * Fix --path-display-size for commands that use it and improve display for "transfer" & "sync" * Support for blocked accounts with instructions to unblock them * Fix crash in libcryptopp for Ubuntu 19.10 and onwards * Fix issues with failed transfers * Fix trailing separator issue for local path in "put" * Speed up sync engine startup for windows * Other fixes & adjustments -- MEGA Team Mon, 22 Jun 2020 19:08:17 +0200 megacmd (1.2.0) stable; urgency=low * "put" now supports wildcard expressions * support setting a proxy with "proxy" command * add support for addressing inshares with //from/ * support for files/folders within public links * minor fix for ls --tree autocompletion * discard flags/options after "--" * --show-handles option added in ls & find * files/folders can be addressed using their handle H:XXXXXXX * support new links format * fixes in reported used storage * fix crash in find command * do not consider inshares for version storage used * win installer: do not ask for elevate permissions when running in silent mode * improved columned outputs to maximize screen use (syncs & transfers) * improve responsiveness at startup in interactive mode, to avoid hangs when session does not resumeAdded mode while logging in that allows certain actions (like setting a proxy) * non-interactive mode will not wait for commands that can be addressed before the session is resumed * speedup cancellation/startup of a huge number of transfers * cloud raid support * speedup improvements in cache and other CPU bottlenecks * many more fixes & adjustments -- MEGA Team Mon, 22 Jun 2020 19:06:13 +0200 megacmd (1.1.0) stable; urgency=low * added "cat" command to read text files (and potentially stream any file) * added update capabilities for Windows & MacOS (automatic updates are enabled by default) * added "media-info" command to show some information of multimedia files * added "df" command to show storage info * added tree-like listing command: "tree" or "ls --tree" * shown progress in non-interactive mode * improvements in progress and transfers results information * width output adjustments in non-interactive mode * output streamed partially from server to clients * added --time-format option to commands displaying times, to allow other formats * 2FA login auth code can be passed as parameter now * transfer now differentiate backup transfers * backup command completion for local paths now only looks for folders * backup transfers are no longer cached (no reason to: backups are considered failed in such case) * backup fix some halts and output improvements * added Public Service Announcements (PSA) support * killsession now allows multiple parameters * fix "clear" in some linuxes * add support for spaces in password prompts * many more minor fixes & adjustments -- MEGA Team Tue, 26 Mar 2019 10:52:57 +0100 megacmd (1.0.0) stable; urgency=low * added FTP beta support (See "ftp") * renew path parsing & improved completion with special characters * fix truncated redirected output in MacOS * added support for account cancellation. (See "cancel") * cp now allows multiple source paths and regular expressions * du path display size variable now * output error code always positive now * new command "errorcode" to translate error code into string * allow password protected links for PRO users * password changing no longer requires old one * webdav now allows stopping serving all locations * added "graphics" command to turn off thumbnails/previews generation * support login and password change using 2FA * limit one instance of server * many more minor fixes & adjustments -- MEGA Team Fri, 07 Sep 2018 14:00:51 +0200 megacmd (0.9.9) stable; urgency=low * Webdav: Serve a MEGA location as a WEBDAV server. * Streaming: Webdav command can also be used for HTTP(S) streaming. * Added thumbnails for video files. * Listing -l now list unix-like columned summary (extended info is now -a) * du & ls -v is now --versions * Fixed --mtime restrictions in find * Added support for compiling in NAS systems * Minor fixes & adjustements -- MEGA Team Tue, 27 Mar 2018 18:37:30 +0200 megacmd (0.9.8) stable; urgency=low * Backups: added the possibility to configure periodic backups * Full version support: listing, see space ocupied, accessing, copying, removing, ... * Added "deleteversions" command to remove versions (all or by path) * Permissions: you can change default permission for files and folders * Persistence of settings: speedlimit, permissions, https * Speedlimit takes units and allows human-readable output * Support for video metadata * Minor fixes and doc improvements -- MEGA Team Thu, 18 Jan 2018 19:15:35 +0100 megacmd (0.9.7) stable; urgency=low * Faster scanning of files * Improved the management of deleted files * Bug fixes and other minor improvements -- MEGA Team Mon, 11 Dec 2017 18:43:46 +0100 megacmd (0.9.6) stable; urgency=low * Added transfer resumption * Added file versioning for modified files(webclient can browse them) * Added time and size constrains for find * Reformated sync output * Added exclusions to syncs. Default: .* ~* desktop.ini Thumbs.db * Fixed detection of invalid TIMEVAL when no unit specified * Improved sync display format * Fix some "get" cases with "/" involved in Windows * Fix in email validation * Fix segfault in userattr * Added "--in" & "--out" to showpcr * Added masterkey command to show master key * All/None options in confirmation for deletion * Added multi-transfer progress (one single progress bar) * Fix mkdir loop * Minor fixes and improvements in error management * beautify whoami -l and added storage used by versions * Changed name for server in windows to MEGAcmdServer.exe -- MEGA Team Fri, 10 Nov 2017 14:36:44 +0100 megacmd (0.9.4) stable; urgency=low * Separated MEGAcmd into interactive shell (MEGAcmdShell) and server(MEGAcmdServer) * Added transfers management with "transfer" command * Uploads and downloads now support background mode with "-q" * Added confirmation on folder removal (interactive & non-interactive modes) * PCRE are now optional if available in all the commands with "--use-pcre" * Server initiated automatically in interactive and non-interactive mode * Added unicode support for Windows * Refurbished communications and secured non-interactive mode in Windows * Implemented copy (cp) to user's inbox * Several fixes and commands improvements -- MEGA Team Mon, 28 Aug 2017 18:06:34 +0200 megacmd (0.9.3) stable; urgency=low * fixed mkdir in MacOS * added command "https" to force HTTPS for file transfers * added -n to "users" to show users names * modified greeting * fixed "clear" for Windows * fixed download >4GB files * fixed bug in asynchronous transfers -- MEGA Team Thu, 02 Feb 2017 11:32:38 +0100 megacmd (0.9.1) stable; urgency=low * Initial version of megacmd * Features: * Interactive shell * Non interactive mode * Regular expresions * Contacts management * Public folders management * Files management * Synching -- MEGA Team Tue, 17 Jan 2017 14:22:10 +0100 MEGAcmd-2.5.2_Linux/build/megacmd/debian.compat000066400000000000000000000000021516543156300212200ustar00rootroot000000000000009 MEGAcmd-2.5.2_Linux/build/megacmd/debian.control000066400000000000000000000016511516543156300214300ustar00rootroot00000000000000Source: megacmd Section: Tools Priority: normal Maintainer: MEGA Linux Team Build-Depends: debhelper, wget, libtool, dh-autoreconf, cdbs, libfuse-dev | libfuse3-dev, autoconf, autoconf-archive, nasm, cmake Package: megacmd Architecture: any Depends: ${shlibs:Depends}, apt-transport-https, gpg, procps, fuse Description: MEGA Command Line Interactive and Scriptable Application MEGAcmd provides non UI access to MEGA services. It intends to offer all the functionality with your MEGA account via shell interaction. It features 2 modes of interaction - interactive. A shell to query your actions - scriptable. A way to execute commands from a shell/a script/another program. Package: megacmd-dbg Architecture: any Section: debug Priority: extra Depends: megacmd Description: debugging symbols for megacmd MEGAcmd-2.5.2_Linux/build/megacmd/debian.copyright000066400000000000000000000035411516543156300217600ustar00rootroot00000000000000Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: megacmd Upstream-Contact: Source: https://github.com/meganz/MEGAsync Files: * Copyright: 2013, Mega Limited License: Simplified (2-clause) BSD License ================================ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. 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. 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 HOLDER 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. The previous applies to all sources and binaries within the SDK save those explicitly distributed under the terms of the GNU General Public License (see http://www.gnu.org/copyleft/gpl.txt for details), and the interactive MEGAcmdShell executables (mega-cmd and MEGAcmdShell.exe), distributed under the aforementioned GNU General Public License. MEGAcmd-2.5.2_Linux/build/megacmd/debian.postinst000066400000000000000000000132741516543156300216370ustar00rootroot00000000000000#!/bin/bash #distro=$(lsb_release -ds) distro="@DISTRO@" #ver=$(lsb_release -rs) ver="@VERSION@" if [[ $distro = "Raspbian GNU/Linux 13"* ]]; then str="Debian_13" elif [[ $distro = "Raspbian GNU/Linux 12"* ]]; then str="Raspbian_12" elif [[ $distro = "Raspbian GNU/Linux 11"* ]]; then str="Raspbian_11" # Ubuntu elif [[ $ver = "26.04" ]]; then str="xUbuntu_26.04" elif [[ $ver = "25.10" ]]; then str="xUbuntu_25.10" elif [[ $ver = "25.04" ]]; then str="xUbuntu_25.04" elif [[ $ver = "24.10" ]]; then str="xUbuntu_24.10" elif [[ $ver = "24.04" ]]; then str="xUbuntu_24.04" elif [[ $ver = "23.10" ]]; then str="xUbuntu_23.10" elif [[ $ver = "23.04" ]]; then str="xUbuntu_23.04" elif [[ $ver = "22.10" ]]; then str="xUbuntu_22.10" elif [[ $ver = "22.04" ]]; then str="xUbuntu_22.04" # Debian elif [[ $ver = 11* ]]; then str="Debian_11" elif [[ $ver = 12* ]]; then str="Debian_12" elif [[ $ver = 13* ]]; then str="Debian_13" elif [[ $ver = "testing" ]]; then str="Debian_testing" # Elementary OS Freya Ubuntu 14.04 LTS elif [[ $distro = "elementary OS Odin" ]]; then str="xUbuntu_20.04" elif [[ $distro = "elementary OS Horus" ]]; then str="xUbuntu_22.04" elif [[ $distro = "elementary OS Circe" ]]; then str="xUbuntu_24.04" elif [[ $distro = "Linux Mint 21 Vanessa" ]]; then str="xUbuntu_22.04" elif [[ $distro = "Linux Mint 21.1 Vera" ]]; then str="xUbuntu_22.04" elif [[ $distro = "Linux Mint 21.2 Victoria" ]]; then str="xUbuntu_22.04" elif [[ $distro = "Linux Mint 21.3 Virginia" ]]; then str="xUbuntu_22.04" elif [[ $distro = "Linux Mint 22 Wilma" ]]; then str="xUbuntu_24.04" elif [[ $distro = "Linux Mint 22.1 Xia" ]]; then str="xUbuntu_24.04" elif [[ $distro = "Linux Mint 22.2 Zara" ]]; then str="xUbuntu_24.04" elif [[ $distro = "Linux Mint 22.3 Zena" ]]; then str="xUbuntu_24.04" # fallback to LTS version, unsupported else str="xUbuntu_22.04" fi ## Remove keys from main keyring apt-key del 03C3AD3A7F068E5D &>/dev/null || : apt-key del 1A664B787094A482 &>/dev/null || : mkdir -m 0755 -p /etc/apt/keyrings echo " -----BEGIN PGP PUBLIC KEY BLOCK----- mQINBGHe0SkBEADd5u7XBExxSg6stILhfNTNfhtTQ3ZSTLW0JZrni1inMS+P8aEM /GxtoK4+4LkLvbAiGkj7f6HEfKVuKUGN+RsHzpClEgyEZ4IY/Na37vJa+XE/zmNZ MbcyHGl5wV8flKHEl/tMAjPV/TUKfePqiyabHjNaZm3AGRGi0oxH2IL3vTOl5DbV sl1oMkfr0h5w4mZkAJqszGxt1nPVA8mn4a57kFJrxwDQX2LnyZWPG+0xIikg91Rz effa+VNh58bi5WPtHwBv9c8bHNjKi66CxK6DWISqLAO/IPpvyG0RRuju18tFQ1dU 2ZPI6R9+u6I4aEP2epfZI7b5n7MBLrSrDY95X3NxWhDdJeYaLwllQNi9NdBlGwrE i2q/NWvmkcHzByY7XfAuOzX08x0Z+fmghCh17dcZAtSzcihZKLDov+gyrbEJfT8G mfKS3NVU28giPa1mZat8JzDem44j2YXBJMxevz0/smTxJmx/69sH9lMRN0QCfnBE vFUGN2NJVbfoiuKzAdwz3FPJZP9n7iSXt4onab16J2i2GalRkL11SY8NbfbAAnhb uiBOQXt103yGh9NMxoyblV+d9dX+m/r5K/uby55rx3KiRxzVFNPNRjkU5kdOvc6H TSKKFD8jqoOIc3/q50Ty3Ga4Ny3Ke4CsYwnVVfJcI+VLt3ebdPuc4yneDwARAQAB tCBNZWdhTGltaXRlZCA8c3VwcG9ydEBtZWdhLmNvLm56PokCVAQTAQoAPhYhBLAc gRiASAyFTHPsfhpmS3hwlKSCBQJh3tEpAhsDBQkSzAMABQsJCAcCBhUKCQgLAgQW AgMBAh4BAheAAAoJEBpmS3hwlKSC2RIP/2/gBmdhW7MGiANE04kVKQBxKpsoFct+ qlr5Hzf3cuHVjtuSm7gv0swYXIr/WVxxpjFK7ipBV1XJIo5QJADTYIJQIFq0j31N 6NTPQpPPrA2vxAuFlSBn6MIfKZUZmSddCuv10rA8g1e8V7VnY+Q3VYOVo+aBToXI sDl8zXHlPElm067CnEbfrMlu1YHQghjPGlB3GHfdxeI/WwdAq00It5101KLqhqIL scsqWHUYFA2kUJGGY74uLKXnfnfzcsU4RMgTFBGqVwPBWLz7wPdxmq/jP7eVdHrN U882Csn5ZJZKHp/zznBAIUVCcTMs5l7FdPGu6dSgzj7QRx+bBNtc+4HSpdKL8ky2 3BsLMpBaRP71LPXajtJzb32rhzqDP2LKIIKytKsK2S/t8fyeZhp/xlKJ0QYgxnsK OYBZ3hmaYmIDmaxKvvc6UwPKqJiCvumPyTBwLLo0hz++pBAw4qh9ZaJL0+ReJjut X35E+uIsJqOcMGOKT03XMtRa0ByfG5gV7SjsHkxf3Z75BMAJE0gmYOQUqq8zLUhV 5ukiHfsWoVhZuTmv4pQJCxC4D3cnJlKKOAM0vZgL9ir0esyd5tvCchtjSphzRi/O DMB+T4GF1w1QUjRsKiJROMY9lWG48JYim7ZeAtOYEsA90zP6KDIs104++KrzGUj7 Nwy724hPw18ZuQINBGHe0SkBEAC7MvXFkM08aI0zSSLyB1ABEEJ+PbvGhLFLhieK f8a7uD4Q6Ddd+ctVNVEZzB90DuhU9RppUry6xlm3yCnSNIdxGBmHzyYL9Ic1HNGf zot/zpAs4Gbddqikfrn+zjkrYCKoIogjmyV1GF5Hx1A2JG4E3wyLRQ6I2OnHacGv P2OilUQx9MY1rcfsCw3Tyc4pRIRQqGN9cuUTM1TQk86SECTfTdYT+vbBTHWI48Ft udVlm11/Hbc8p25fqR8ogky2F9o8a0KZzCVlAFnSj+JGsP15OEx+Vz4ZXjckXARQ T91DfwsnPyfUe6K47ZJNWEiNNevCnE0v+0LgCJWBP2yeB/47D1graJIw/tbDZs18 XLbxJuRNQJX/nhuVWF/Ickfv07HySMThBQH4yEudc/ZIH+hMjZdqj2MuYbHlO412 bX0rj5HuKZ0SAr00IhdF9RX1K/wKXY3apYOPi1mr/VAB6Mx1zt8V4wXzQAXgr1N4 Gz83YLWWv/48XRbjuBCqQkRfs48lW15BKDaJaly3VyymrYVVXTSdKNkX3+BXP25T G2/RppYhAftHVb7ptU+CiycmCuT9OvG+xv+YGliqiEjE0Qy0hdgHngqt42UzHSd/ xqrOFTPMTAl1BDgFiMwwIH+JeYbpJ1ohKBaDMMG7IU4sp6YlIRj6iFeZCkwWjU7W zIqtvwARAQABiQI8BBgBCgAmFiEEsByBGIBIDIVMc+x+GmZLeHCUpIIFAmHe0SkC GwwFCRLMAwAACgkQGmZLeHCUpIIdohAA3c2/oLlrPTKEPCSlHvQYDpvTBQjdQ9GY 20pPHDom/T26qO5v36+vFfI47Z3uz8RX2vn83CEE467IjvGE3AyMp4cBODWgJJgG Wx8yH8ueR1Qk9AAZ/VZ8zD0rQ34Sk0uVl7voosJ5cH2hwdy6xXjR2dfFb1+wLjpi +Bdy3RU69Y2D7H8Okut8PpRgbd+u9JnK0+U0rzMJUICRIFC1NI8zaAw+ZpSTlTpY 622vp8ynkTk6TZ2D9e8yM70L/lwza5rloHi7NdCxEjly/O0JAON6if1kPbnteOUc 8pll57bPWxhUOnpcawDZa7i7E6WaN84gabnGE6l3DIGTp8Iatq+oT4mKDWLKotjA ZsdccUmxLqfMKHl8gjkxjyGlD85QdCKms5zZIzUXnO/0HKs7+vSmRaK5xaD62M2L h6q3it344NjV37v9Ofs2KroNovwfRBcjImblNv0DLERFeEIfzNJ4P9NsAW7Pvnem mTa7cc5kmtaxBYi5ZPR9l3A5kWv2BlhFV8jZF328eh+KgLKdRJPRIK6z7NU7yHAB cqHV7UnrSsJ2fzCOSOWULzW1ZhAGCP1I/kldxm1t5uzr0msZ9VFGlHYSkIAwBcys /xZLk+MVzXxJfRv+9viXL/SoNitOsh8ZUs3SjvJTVhxFDpAmGvNb3+jv3pNVU77S sAdVa6xer/c= =F8S0 -----END PGP PUBLIC KEY BLOCK----- " | gpg --dearmor > /etc/apt/keyrings/meganz-archive-keyring.gpg chmod 0644 /etc/apt/keyrings/meganz-archive-keyring.gpg if [ -d /etc/apt/sources.list.d ]; then cat >/etc/apt/sources.list.d/megaio.sources </dev/null || true fi sysctl -p /etc/sysctl.d/99-megacmd-inotify-limit.conf || true pkill --signal SIGUSR2 mega-cmd-server 2> /dev/null || true MEGAcmd-2.5.2_Linux/build/megacmd/debian.postrm000066400000000000000000000002731516543156300212730ustar00rootroot00000000000000#!/bin/bash # kill running megacmd instance (when removing) [ "$1" == "remove" ] && pkill mega-cmd 2> /dev/null || true [ "$1" == "remove" ] && pkill mega-cmd-server 2> /dev/null || true MEGAcmd-2.5.2_Linux/build/megacmd/debian.prerm000066400000000000000000000001421516543156300210670ustar00rootroot00000000000000#!/bin/bash [ "$1" == "upgrade" ] && pkill --signal SIGUSR1 mega-cmd-server 2> /dev/null || true MEGAcmd-2.5.2_Linux/build/megacmd/debian.rules000066400000000000000000000107101516543156300210760ustar00rootroot00000000000000#!/usr/bin/make -f NJOBS := $(shell echo ${DEB_BUILD_OPTIONS} | sed -rn 's/.*parallel=([0-9]*).*/\1/p') VCPKG_ROOT := $(shell [ -f /opt/vcpkg.tar.gz ] && echo "-DVCPKG_ROOT=vcpkg" || echo "") #TODO: pass deprecation to cmake (probably better not via ccpflags) EXTRADEFINES := $(shell cat /etc/issue | grep "Ubuntu 1[2345789]\|Ubuntu 20\|Ubuntu 21\|Ubuntu 22.10\|Ubuntu 23\|Ubuntu.24.10\|Ubuntu.25.04\|Debian.* [789]" >/dev/null && echo "$$CPPFLAGS -DMEGACMD_DEPRECATED_OS" || echo "") DPKGXZ := $(shell cat /etc/issue | grep "Ubuntu 26\|Resolute Raccoon\|Ubuntu 26\|Resolute Raccoon\|Ubuntu 26\|Resolute Raccoon\|Ubuntu 26\|Resolute Raccoon\|Ubuntu 25\|Questing Quokka\|Plucky Puffin\|Ubuntu 24\|Ubuntu 23\|Ubuntu 22" >/dev/null && echo "-- -Zxz" || echo "") #LDFLAGS := $(shell dpkg --print-architecture | grep -i "i386\|arm" >/dev/null && echo "" || echo "$$LDFLAGS -Wl,-rpath,/opt/megacmd/lib") EXPORT_VCPKG_FORCE_SYSTEM_BINARIES := $(shell uname -m | grep "armv7l" >/dev/null && echo "VCPKG_FORCE_SYSTEM_BINARIES=1" || echo "") DEB_TESTING := $(shell cat /etc/issue | grep "Debian" > /dev/null && cat /etc/apt/sources.list.d/debian.sources | grep -v "^\#" | grep testing > /dev/null && echo "testing") #TODO: Ensure raspbian builds with that export EXPORTCCMFPU := $(shell cat /etc/issue | grep "Raspbian GNU/Linux 12" >/dev/null && echo "export CC='cc -mfpu=vfp'" || echo ":") DISTRO := $(shell lsb_release -ds) VER := $(shell lsb_release -rs) export VCPKG_DEFAULT_BINARY_CACHE := $(shell [ -f /opt/vcpkg.tar.gz ] && echo "/opt/persistent/vcpkg_cache" || echo "") export PATH := $(shell [ -f /opt/cmake.tar.gz ] && echo "`pwd`/cmake_inst/bin:${PATH}" || echo "${PATH}") export CXXFLAGS := $(shell [ -n "${EXTRADEFINES}" ] && echo "${EXTRADEFINES} ${CXXFLAGS}" || echo "${CXXFLAGS}") MEGA_BUILD_ID := $(shell cat MEGA_BUILD_ID || echo "1") build: build-stamp build-stamp: sed -i -E "s/(^#define MEGACMD_BUILD_ID )[0-9]*/\1$(MEGA_BUILD_ID)/g" src/megacmdversion.h.in sed -i -E "0,/megacmd \(([0-9.]*)[^\)]*\)/s//megacmd \(\1-$(MEGA_BUILD_ID).1)/" debian.changelog || : if [ -f /opt/vcpkg.tar.gz ]; then \ tar xzf /opt/vcpkg.tar.gz; \ mkdir -p $(VCPKG_DEFAULT_BINARY_CACHE); \ fi if [ -f /opt/cmake.tar.gz ]; then \ HASH=$$(md5sum /opt/cmake.tar.gz | awk '{print $$1}'); \ if [ "$$HASH" = "ebc26503469f12bf1e956c564fcfa82a" ] || [ "$$HASH" = "2e278820bb4ec6fb71dbef1704fb5359" ]; then \ echo "Valid cmake.tar.gz (hash: $$HASH)"; \ else \ echo "Invalid cmake.tar.gz hash: $$HASH" >&2; \ exit 1; \ fi; \ fi if [ -f /opt/cmake.tar.gz ]; then \ tar xzf /opt/cmake.tar.gz; \ ln -s cmake-*-linux* cmake_inst; \ fi cmake --version $(EXPORT_VCPKG_FORCE_SYSTEM_BINARIES) cmake $(VCPKG_ROOT) -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo -S $(CURDIR) -B $(CURDIR)/build_dir cmake --build $(CURDIR)/build_dir -j$(NJOBS) cmake --install $(CURDIR)/build_dir --prefix $(CURDIR)/install_dir #generate file that would increase inotify limit: echo "fs.inotify.max_user_watches = 524288" > 99-megacmd-inotify-limit.conf if [ -n "$(DEB_TESTING)" ]; then \ sed -i "s#@VERSION@#$(DEB_TESTING)#g" debian/postinst; \ else \ sed -i "s#@VERSION@#$(VER)#g" debian/postinst; \ fi sed -i "s#@DISTRO@#$(DISTRO)#g" debian/postinst touch build-stamp clean: dh_testdir dh_testroot rm -f build-stamp install: build ###dh_auto_install --destdir=megacmd/megacmd dh_install install_dir/* / # Build architecture-independent files here. binary-indep: build install # We have nothing to do by default. # Build architecture-dependent files here. binary-arch: build install dh_testdir dh_testroot # dh_installdebconf dh_installdocs dh_installexamples dh_installmenu # dh_installlogrotate # dh_installemacsen # dh_installpam # dh_installmime # dh_installinit dh_installcron dh_installman dh_installinfo # dh_undocumented dh_installchangelogs dh_link dh_strip --dbg-package=megacmd-dbg dh_compress dh_fixperms # dh_makeshlibs dh_installdeb # dh_perl dh_shlibdeps --dpkg-shlibdeps-params=--ignore-missing-info -l$(CURDIR)/deps/lib dh_gencontrol dh_md5sums dh_builddeb $(DPKGXZ) binary: binary-indep binary-arch .PHONY: build clean binary-indep binary-arch binary install MEGAcmd-2.5.2_Linux/build/megacmd/megacmd.changes000066400000000000000000000326501516543156300215360ustar00rootroot00000000000000Wed Apr 08 12:00:00 CEST 2026 - linux@mega.co.nz - Update to version 2.5.2: * Fix: address potential loop in sync engine with mtime changes within 2 seconds ------------------------------------------------------------------- Tue Mar 24 09:36:34 CET 2026 - linux@mega.co.nz - Update to version 2.5.1: * Fixed syncrhonizing updated empty files * WSL bash completion speedup ------------------------------------------------------------------- Wed Mar 11 10:36:46 AM UTC 2026 - linux@mega.co.nz - Update to version 2.5.0: * Shell UX improvements: empty Enter shows a new prompt; no duplicate prompt while command is executing * Sync engine improvements: prevent re-uploading data when local drive fingerprint has changed * put: Fixed -c (autocreate) not working when destination is an absolute path * mediainfo: Fixed audio file duration not being displayed * Transfers: prevent failures on resumption * thumbnail/preview: Print error when the requested file does not exist * share: Return success (exit code 0) when no shares exist below the current folder * passwd: fixes issues when 2FA is enabled * Path and string handling fixes (trailing separators, trimming) * Expanded unit test coverage * Fix: Improved restart-after-update reliability on POSIX (server PID changes after update) * Stability, memory, typos and other improvements ------------------------------------------------------------------- Tue Dec 16 17:14:02 CET 2025 - linux@mega.co.nz - Update to version 2.4.0: * Improvements in uploads UX/UI: put now supports printing tag. Fixes in transfers table * Stability improvements: crashes fixes, FUSE deadlocks, ... * Performance boosts: lockless download URL retrievals, removed CPU bottlenecks for large accounts processing, db operations improvements, transfers improvements ... * Typos and other improvements ------------------------------------------------------------------- Tue Sep 2 16:43:10 CEST 2025 - linux@mega.co.nz - Update to version 2.3.0: * FUSE (beta): Added fuse commands on Windows to allow your MEGA folders to be directly mounted to your local drive * Other fixes and improvements to enhance reliability and performance ------------------------------------------------------------------- Tue Jun 3 23:20:12 CEST 2025 - linux@mega.co.nz - Update to version 2.2.0: * Support building for ARM64 in linux * New command "configure" to allow configuring some settings * Support setting max number of nodes in memory * Support setting number of SDK instances used for login in folders to download/import * Other fixes and improvements to enhance reliability and performance ------------------------------------------------------------------- Wed Apr 2 11:08:20 CEST 2025 - linux@mega.co.nz - Update to version 2.1.1: * FUSE (beta): Added fuse commands on Linux to allow your MEGA folders to be directly mounted to your local drive * Delayed sync uploads: Introduced a mechanism to delay to frequently changed sync uploads, and the sync-config command * Logging: Messages are now printed in standard error, the rotating logger is now configurable and more verbose by default, passwords are now redacted from the logs, and other fixes and refinements * Fixed a crash when auto-completing a local folder that doesn't exist * Fixed the confirmcancel command incorrectly reporting failure on success * Extended speedlimit command to allow increasing max connections * Other fixes and improvements to enhance reliability and performance ------------------------------------------------------------------- Fri Jan 24 01:20:16 PM CET 2025 - linux@mega.co.nz - Update to version 2.0.0: * New Sync Engine: See sync-issues and sync-ignore commands * Rotating Logger: Introduced a robust rotating logging system across all platforms for better performance and debugging * Platform-specific enhancements: Addressed various file descriptor issues on Linux and macOS, and improved non-ascii support on Windows * Improved overall reliability: Fixed memory leaks, resolved potential data races, and eliminated deadlock scenarios * Fixed an issue when handling double-quoted arguments * Various fixes and refinements to enhance usability and performance ------------------------------------------------------------------- Mon May 20 02:06:31 PM CEST 2024 - linux@mega.co.nz - Update to version 1.7.0: * Improved startup time * Reduced memory consumption: cached metadata is no longer loaded at startup * Fixes and improvements in whoami, ls, backup and export commands * Fixed several memory leaks and improved overall memory consumption * Fixed file permissions on Unix * Added support for the Apple silicon (M1) * Many other fixes and improvements ------------------------------------------------------------------- Thu May 4 16:18:29 CEST 2023 - linux@mega.co.nz - Update to version 1.6.3: * Improvements in session resumption * Fixes for undecryptable nodes * Stability and preformance fixes ------------------------------------------------------------------- Fri Mar 24 13:02:52 CET 2023 - linux@mega.co.nz - Update to version 1.6.1: * Support login into a password protected link * Ease establishing shares * Fixes in tranfer resumption * Fixes in password protected links * Improvements shares management ------------------------------------------------------------------- Wed Mar 1 13:26:37 CET 2023 - linux@mega.co.nz - Update to version 1.6.0: * Improvements in "find" to search by type, or print only handles (to ease automation) * Improved state management and reporting in synchronizations * Improved information on shares * Improvements in "attr" * Security adjustments * Other fixes and adjustments ------------------------------------------------------------------- Mon Jun 22 19:08:17 CEST 2020 - linux@mega.co.nz - Update to version 1.3.0: * Fix --path-display-size for commands that use it and improve display for "transfer" & "sync" * Support for blocked accounts with instructions to unblock them * Fix crash in libcryptopp for Ubuntu 19.10 and onwards * Fix issues with failed transfers * Fix trailing separator issue for local path in "put" * Speed up sync engine startup for windows * Other fixes & adjustments ------------------------------------------------------------------- Mon Jun 22 19:06:13 CEST 2020 - linux@mega.co.nz - Update to version 1.2.0: * "put" now supports wildcard expressions * support setting a proxy with "proxy" command * add support for addressing inshares with //from/ * support for files/folders within public links * minor fix for ls --tree autocompletion * discard flags/options after "--" * --show-handles option added in ls & find * files/folders can be addressed using their handle H:XXXXXXX * support new links format * fixes in reported used storage * fix crash in find command * do not consider inshares for version storage used * win installer: do not ask for elevate permissions when running in silent mode * improved columned outputs to maximize screen use (syncs & transfers) * improve responsiveness at startup in interactive mode, to avoid hangs when session does not resumeAdded mode while logging in that allows certain actions (like setting a proxy) * non-interactive mode will not wait for commands that can be addressed before the session is resumed * speedup cancellation/startup of a huge number of transfers * cloud raid support * speedup improvements in cache and other CPU bottlenecks * many more fixes & adjustments ------------------------------------------------------------------- Tue Mar 26 10:52:56 CET 2019 - linux@mega.co.nz - Update to version 1.1.0: * added "cat" command to read text files (and potentially stream any file) * added update capabilities for Windows & MacOS (automatic updates are enabled by default) * added "media-info" command to show some information of multimedia files * added "df" command to show storage info * added tree-like listing command: "tree" or "ls --tree" * shown progress in non-interactive mode * improvements in progress and transfers results information * width output adjustments in non-interactive mode * output streamed partially from server to clients * added --time-format option to commands displaying times, to allow other formats * 2FA login auth code can be passed as parameter now * transfer now differentiate backup transfers * backup command completion for local paths now only looks for folders * backup transfers are no longer cached (no reason to: backups are considered failed in such case) * backup fix some halts and output improvements * added Public Service Announcements (PSA) support * killsession now allows multiple parameters * fix "clear" in some linuxes * add support for spaces in password prompts * many more minor fixes & adjustments ------------------------------------------------------------------- Fri Sep 7 14:00:51 CEST 2018 - linux@mega.co.nz - Update to version 1.0.0: * added FTP beta support (See "ftp") * renew path parsing & improved completion with special characters * fix truncated redirected output in MacOS * added support for account cancellation. (See "cancel") * cp now allows multiple source paths and regular expressions * du path display size variable now * output error code always positive now * new command "errorcode" to translate error code into string * allow password protected links for PRO users * password changing no longer requires old one * webdav now allows stopping serving all locations * added "graphics" command to turn off thumbnails/previews generation * support login and password change using 2FA * limit one instance of server * many more minor fixes & adjustments ------------------------------------------------------------------- Tue Mar 27 18:37:30 CEST 2018 - linux@mega.co.nz - Update to version 0.9.9: * Webdav: Serve a MEGA location as a WEBDAV server. * Streaming: Webdav command can also be used for HTTP(S) streaming. * Added thumbnails for video files. * Listing -l now list unix-like columned summary (extended info is now -a) * du & ls -v is now --versions * Fixed --mtime restrictions in find * Added support for compiling in NAS systems * Minor fixes & adjustements ------------------------------------------------------------------- Thu Jan 18 19:15:35 CET 2018 - linux@mega.co.nz - Update to version 0.9.8: * Backups: added the possibility to configure periodic backups * Full version support: listing, see space ocupied, accessing, copying, removing, ... * Added "deleteversions" command to remove versions (all or by path) * Permissions: you can change default permission for files and folders * Persistence of settings: speedlimit, permissions, https * Speedlimit takes units and allows human-readable output * Support for video metadata * Minor fixes and doc improvements ------------------------------------------------------------------- Mon Dec 11 18:43:46 CET 2017 - linux@mega.co.nz - Update to version 0.9.7: * Faster scanning of files * Improved the management of deleted files * Bug fixes and other minor improvements ------------------------------------------------------------------- Fri Nov 10 14:36:44 CET 2017 - linux@mega.co.nz - Update to version 0.9.6: * Added transfer resumption * Added file versioning for modified files(webclient can browse them) * Added time and size constrains for find * Reformated sync output * Added exclusions to syncs. Default: .* ~* desktop.ini Thumbs.db * Fixed detection of invalid TIMEVAL when no unit specified * Improved sync display format * Fix some "get" cases with "/" involved in Windows * Fix in email validation * Fix segfault in userattr * Added "--in" & "--out" to showpcr * Added masterkey command to show master key * All/None options in confirmation for deletion * Added multi-transfer progress (one single progress bar) * Fix mkdir loop * Minor fixes and improvements in error management * beautify whoami -l and added storage used by versions * Changed name for server in windows to MEGAcmdServer.exe ------------------------------------------------------------------- Mon Aug 28 18:06:34 CEST 2017 - linux@mega.co.nz - Update to version 0.9.4: * Separated MEGAcmd into interactive shell (MEGAcmdShell) and server(MEGAcmdServer) * Added transfers management with "transfer" command * Uploads and downloads now support background mode with "-q" * Added confirmation on folder removal (interactive & non-interactive modes) * PCRE are now optional if available in all the commands with "--use-pcre" * Server initiated automatically in interactive and non-interactive mode * Added unicode support for Windows * Refurbished communications and secured non-interactive mode in Windows * Implemented copy (cp) to user's inbox * Several fixes and commands improvements ------------------------------------------------------------------- Thu Feb 2 11:32:38 CET 2017 - linux@mega.co.nz - Update to version 0.9.3: * fixed mkdir in MacOS * added command \ * added -n to \ * modified greeting * fixed \ * fixed download >4GB files * fixed bug in asynchronous transfers ------------------------------------------------------------------- Tue Jan 17 14:22:10 CET 2017 - linux@mega.co.nz - Update to version 0.9.1: * Initial version of megacmd * Features: * Interactive shell * Non interactive mode * Regular expresions * Contacts management * Public folders management * Files management * Synching ------------------------------------------------------------------- MEGAcmd-2.5.2_Linux/build/megacmd/megacmd.install000066400000000000000000000176501516543156300215770ustar00rootroot00000000000000post_install() { #remove repo if existing sed -n '1h;1!H;${g;s/\n###REPO for MEGA###\n.*###END REPO for MEGA###//;p;}' -i /etc/pacman.conf #include repo #Add a new line if required lastC=`tail -c1 /etc/pacman.conf || echo -n "x"` if [ "$lastC" != "" ]; then echo >> /etc/pacman.conf; fi cat >> /etc/pacman.conf <<-EOF ###REPO for MEGA### [DEB_Arch_Extra] SigLevel = Required TrustedOnly Server = https://mega.nz/linux/repo/Arch_Extra/\$arch ###END REPO for MEGA### EOF # Remove old key pacman-key -d 8F208FBF12FEE766AA32AEAF03C3AD3A7F068E5D 2>/dev/null || : #Add public key used for signing pacman-key --add - < /dev/null || true } post_upgrade() { # Remove old key pacman-key -d 8F208FBF12FEE766AA32AEAF03C3AD3A7F068E5D 2>/dev/null || : # Check if new key has been added if ! (pacman-key -l B01C811880480C854C73EC7E1A664B787094A482 &> /dev/null); then #remove repo if existing sed -n '1h;1!H;${g;s/\n###REPO for MEGA###\n.*###END REPO for MEGA###//;p;}' -i /etc/pacman.conf #include repo #Add a new line if required lastC=`tail -c1 /etc/pacman.conf || echo -n "x"` if [ "$lastC" != "" ]; then echo >> /etc/pacman.conf; fi cat >> /etc/pacman.conf <<-EOF ###REPO for MEGA### [DEB_Arch_Extra] SigLevel = Required TrustedOnly Server = https://mega.nz/linux/repo/Arch_Extra/\$arch ###END REPO for MEGA### EOF #Add public key used for signing pacman-key --add - < /dev/null || true } MEGAcmd-2.5.2_Linux/build/production_build.cmd000066400000000000000000000026631516543156300212450ustar00rootroot00000000000000IF NOT [%MEGA_VCPKGPATH%]==[] ( SET "VCPKG_ROOT_ARG=-DVCPKG_ROOT=%MEGA_VCPKGPATH%" ) IF [%MEGA_CORES%]==[] ( FOR /f "tokens=2 delims==" %%f IN ('wmic cpu get NumberOfLogicalProcessors /value ^| find "="') DO SET MEGA_CORES=%%f ) REM Clean up any previous leftovers IF EXIST build-x64-windows-mega ( rmdir /s /q build-x64-windows-mega ) REM download winsfp call get_winfsp.cmd || exit 1 /b mkdir build-x64-windows-mega cd build-x64-windows-mega cmake -G "Visual Studio 17 2022" -A x64 -DCMAKE_VERBOSE_MAKEFILE="ON" -DCMAKE_CXX_FLAGS_RELWITHDEBINFO="/Zi /O2 /Ob2 /DNDEBUG" %VCPKG_ROOT_ARG% -DCMAKE_C_FLAGS_RELWITHDEBINFO="/Zi /O2 /Ob2 /DNDEBUG" -S "..\.." -B . || exit 1 /b cmake --build . --config RelWithDebInfo --target mega-exec --target mega-cmd-server --target mega-cmd --target mega-cmd-updater -j%MEGA_CORES% || exit 1 /b cd .. IF "%MEGA_SKIP_32_BIT_BUILD%" == "true" ( GOTO :EOF ) REM Clean up any previous leftovers IF EXIST build-x86-windows-mega ( rmdir /s /q build-x86-windows-mega ) mkdir build-x86-windows-mega cd build-x86-windows-mega cmake -G "Visual Studio 17 2022" -A Win32 -DCMAKE_VERBOSE_MAKEFILE="ON" -DCMAKE_CXX_FLAGS_RELWITHDEBINFO="/Zi /O2 /Ob2 /DNDEBUG" %VCPKG_ROOT_ARG% -DCMAKE_C_FLAGS_RELWITHDEBINFO="/Zi /O2 /Ob2 /DNDEBUG" -S "..\.." -B . || exit 1 /b cmake --build . --config RelWithDebInfo --target mega-exec --target mega-cmd-server --target mega-cmd --target mega-cmd-updater -j%MEGA_CORES% || exit 1 /b cd .. MEGAcmd-2.5.2_Linux/build/templates/000077500000000000000000000000001516543156300172025ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/build/templates/megacmd/000077500000000000000000000000001516543156300205775ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/build/templates/megacmd/PKGBUILD000066400000000000000000000042541516543156300217300ustar00rootroot00000000000000## # @file build/megacmd/megacmd/PKGBUILD # @brief script to generate package megacmd for ArchLinux # # (c) 2013-2016 by Mega Limited, Auckland, New Zealand # # This file is part of the MEGAcmd. # # MEGAcmd 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. # # @copyright Simplified (2-clause) BSD License. # # You should have received a copy of the license along with this # program. ## # Maintainer: Pablo Martin pkgname=megacmd pkgver=megacmd_VERSION pkgrel=`cat MEGA_BUILD_ID || echo "1"` epoch= pkgdesc="MEGA Command Line Interactive and Scriptable Application" arch=('i686' 'x86_64') url="http://mega.nz/cmd" license=("https://github.com/meganz/megacmd/blob/master/LICENSE") groups=() depends=('glibc>=2.37' 'bash-completion' 'fuse2') makedepends=('unzip' 'wget' 'ca-certificates' 'cmake' 'zip' 'python3' 'autoconf-archive' 'nasm' 'fuse2') checkdepends=() optdepends=() provides=("megacmd=${pkgver}" 'ffmpeg-mega') replaces=() backup=() options=(!lto) # ffmpeg is not relocatable install=megacmd.install changelog= #TODO source=("./${pkgname}_$pkgver.tar.gz" ) noextract=() md5sums=('MD5SUM') # generated with makepkg -g validpgpkeys=() prepare() { cd "$pkgname-$pkgver" sed -i -E "s/(^#define MEGACMD_BUILD_ID )[0-9]*/\1${pkgrel}/g" src/megacmdversion.h.in if [ -f /opt/vcpkg.tar.gz ]; then tar xzf /opt/vcpkg.tar.gz fi } build() { set -x cd "$pkgname-$pkgver" megasrcdir="${PWD}" megabuilddir="${megasrcdir}/build_dir" if [ -d "${megasrcdir}/vcpkg" ]; then export VCPKG_DEFAULT_BINARY_CACHE=/opt/persistent/vcpkg_cache mkdir -p ${VCPKG_DEFAULT_BINARY_CACHE} vcpkg_root="-DVCPKG_ROOT=${megasrcdir}/vcpkg" fi cmake --version cmake ${vcpkg_root} -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo -S "${megasrcdir}" -B "${megabuilddir}" cmake --build "${megabuilddir}" ${BUILD_PARALLEL_JOBS} touch build-stamp } check() { cd "$pkgname-$pkgver" } package() { cd "$pkgname-$pkgver" megasrcdir="${srcdir}/${pkgname}-${pkgver}" megabuilddir="${megasrcdir}/build_dir" cmake --install "${megabuilddir}" --prefix $pkgdir } MEGAcmd-2.5.2_Linux/build/templates/megacmd/megacmd-Debian_testing.dsc000066400000000000000000000010121516543156300255760ustar00rootroot00000000000000Format: 1.0 Source: megacmd Binary: megacmd Standards-Version: 3.6.1 Architecture: any Version: megacmd_VERSION DEBTRANSFORM-RELEASE: 1 Maintainer: MEGA Linux Team Homepage: https://mega.nz/cmd Build-Depends: pkg-config, debhelper, wget, dh-autoreconf, cdbs, unzip, pkg-config, autoconf, autoconf-archive, nasm, cmake, libtool-bin (>= 2.4.2-1.10) | libtool (<< 2.4.2-1.10), libfuse3-dev Package-List: megacmd deb gnome optional Files: 00000000000000000000000000000000 0 megacmd_megacmd_VERSION.tar.gz MEGAcmd-2.5.2_Linux/build/templates/megacmd/megacmd.dsc000066400000000000000000000010111516543156300226600ustar00rootroot00000000000000Format: 1.0 Source: megacmd Binary: megacmd Standards-Version: 3.6.1 Architecture: any Version: megacmd_VERSION DEBTRANSFORM-RELEASE: 1 Maintainer: MEGA Linux Team Homepage: https://mega.nz/cmd Build-Depends: pkg-config, debhelper, wget, dh-autoreconf, cdbs, unzip, pkg-config, autoconf, autoconf-archive, nasm, cmake, libtool-bin (>= 2.4.2-1.10) | libtool (<< 2.4.2-1.10), libfuse-dev Package-List: megacmd deb gnome optional Files: 00000000000000000000000000000000 0 megacmd_megacmd_VERSION.tar.gz MEGAcmd-2.5.2_Linux/build/templates/megacmd/megacmd.spec000066400000000000000000000264341516543156300230610ustar00rootroot00000000000000Name: megacmd Version: megacmd_VERSION Release: %(cat MEGA_BUILD_ID || echo "1").1 Summary: MEGA Command Line Interactive and Scriptable Application License: https://github.com/meganz/megacmd/blob/master/LICENSE Group: Applications/Others Url: https://mega.nz Source0: megacmd_%{version}.tar.gz Vendor: MEGA Limited Packager: MEGA Linux Team %if 0%{?suse_version} Requires: procps %else Requires: procps-ng %endif %global __requires_exclude ^lib(avcodec|avformat|avutil|swresample|swscale)\\.so\\. BuildRequires: autoconf, autoconf-archive, automake, libtool, gcc-c++ BuildRequires: hicolor-icon-theme, zip, unzip, nasm, cmake, perl BuildRequires: fuse-devel Requires: fuse %if 0%{?fedora_version} >= 40 BuildRequires: wget2, wget2-wget %else BuildRequires: wget %endif %if 0%{?suse_version} || 0%{?sle_version} %if 0%{?suse_version} > 1500 BuildRequires: pkgconf-pkg-config %else BuildRequires: pkg-config %endif %endif %if 0%{?fedora} BuildRequires: pkgconf-pkg-config %endif #OpenSUSE %if 0%{?suse_version} || 0%{?sle_version} # disabling post-build-checks that ocassionally prevent opensuse rpms from being generated # plus it speeds up building process #!BuildIgnore: post-build-checks # OpenSUSE leap features too old compiler and python 3.10 by default: %if 0%{?suse_version} && 0%{?suse_version} <= 1500 BuildRequires: gcc13 gcc13-c++ BuildRequires: python311 %endif %endif #Fedora specific %if 0%{?fedora} # allowing for rpaths (taken as invalid, as if they were not absolute paths when they are) %if 0%{?fedora_version} >= 35 %define __brp_check_rpaths QA_RPATHS=0x0002 /usr/lib/rpm/check-rpaths %endif %endif # RHEL/CentOS/Alma/Rocky/Oracle >= 9: allow $ORIGIN %if (0%{?rhel} >= 9) || (0%{?rhel_version} >= 900) || (0%{?centos_version} >= 900) %define __brp_check_rpaths QA_RPATHS=$(( 0x0002|0x0008 )) /usr/lib/rpm/check-rpaths %endif %description MEGAcmd provides non UI access to MEGA services. It intends to offer all the functionality with your MEGA account via shell interaction. It features 2 modes of interaction: - interactive. A shell to query your actions - scriptable. A way to execute commands from a shell/a script/another program. %prep %setup -q mega_build_id=`echo %{release} | cut -d'.' -f 1` sed -i -E "s/(^#define MEGACMD_BUILD_ID )[0-9]*/\1${mega_build_id}/g" src/megacmdversion.h.in %define fullreqs -DREQUIRE_HAVE_PDFIUM -DREQUIRE_HAVE_FFMPEG -DREQUIRE_HAVE_LIBUV -DREQUIRE_USE_MEDIAINFO -DREQUIRE_USE_PCRE %if ( 0%{?fedora_version} && 0%{?fedora_version}<=42 ) || ( 0%{?centos_version} == 600 ) || ( 0%{?centos_version} == 800 ) || ( 0%{?sle_version} && 0%{?sle_version} <= 150600 ) %define extradefines -DMEGACMD_DEPRECATED_OS %else %define extradefines %{nil} %endif if [ -f /opt/vcpkg.tar.gz ]; then export VCPKG_DEFAULT_BINARY_CACHE=/opt/persistent/vcpkg_cache mkdir -p ${VCPKG_DEFAULT_BINARY_CACHE} tar xzf /opt/vcpkg.tar.gz vcpkg_root="-DVCPKG_ROOT=vcpkg" fi # use a custom cmake if required/available: if [ -f /opt/cmake.tar.gz ]; then echo "8dc99be7ba94ad6e14256b049e396b40 /opt/cmake.tar.gz" | md5sum -c - tar xzf /opt/cmake.tar.gz ln -s cmake-*-Linux* cmake_inst export PATH="${PWD}/cmake_inst/bin:${PATH}" fi # OpenSuse Leap 15.x defaults to gcc7. # Python>=10 needed for VCPKG pkgconf %if 0%{?suse_version} && 0%{?suse_version} <= 1500 export CC=gcc-13 export CXX=g++-13 mkdir python311 ln -sf /usr/bin/python3.11 python311/python3 export PATH=$PWD/python311:$PATH %endif if [ -n "%{extradefines}" ]; then export CXXFLAGS="%{extradefines} ${CXXFLAGS}" fi cmake --version cmake ${vcpkg_root} -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo -S . -B %{_builddir}/build_dir %build if [ -f /opt/cmake.tar.gz ]; then export PATH="${PWD}/cmake_inst/bin:${PATH}" fi cmake --build %{_builddir}/build_dir %{?_smp_mflags} %install if [ -f /opt/cmake.tar.gz ]; then export PATH="${PWD}/cmake_inst/bin:${PATH}" fi cmake --install %{_builddir}/build_dir --prefix %{buildroot} %post #TODO: source bash_completion? ## Configure repository ## %if 0%{?fedora} YUM_FILE="/etc/yum.repos.d/megasync.repo" cat > "$YUM_FILE" << DATA [MEGAsync] name=MEGAsync baseurl=https://mega.nz/linux/repo/Fedora_\$releasever/ gpgkey=https://mega.nz/linux/repo/Fedora_\$releasever/repodata/repomd.xml.key gpgcheck=1 enabled=1 DATA %endif %if 0%{?rhel_version} || 0%{?centos_version} %if 0%{?rhel_version} == 900 && 0%{?almalinux} %define reponame AlmaLinux_9 %endif %if 0%{?rhel_version} == 1000 && 0%{?almalinux} %define reponame AlmaLinux_10 %endif %if 0%{?centos_version} == 900 && ! 0%{?almalinux} %define reponame CentOS_Stream_9 %endif %if 0%{?centos_version} == 1000 && ! 0%{?almalinux} %define reponame CentOS_Stream_10 %endif YUM_FILE="/etc/yum.repos.d/megasync.repo" cat > "$YUM_FILE" << DATA [MEGAsync] name=MEGAsync baseurl=https://mega.nz/linux/repo/%{reponame}/ gpgkey=https://mega.nz/linux/repo/%{reponame}/repodata/repomd.xml.key gpgcheck=1 enabled=1 DATA %endif %if 0%{?sle_version} || 0%{?suse_version} %if 0%{?sle_version} == 150000 || 0%{?sle_version} == 150100 || 0%{?sle_version} == 150200 %define reponame openSUSE_Leap_15.0 %endif %if 0%{?sle_version} == 150300 %define reponame openSUSE_Leap_15.3 %endif %if 0%{?sle_version} == 150400 %define reponame openSUSE_Leap_15.4 %endif %if 0%{?sle_version} == 150500 %define reponame openSUSE_Leap_15.5 %endif %if 0%{?sle_version} == 150600 %define reponame openSUSE_Leap_15.6 %endif %if 0%{?sle_version} == 0 && 0%{?suse_version} >= 1550 %define reponame openSUSE_Tumbleweed %endif if [ -d "/etc/zypp/repos.d/" ]; then ZYPP_FILE="/etc/zypp/repos.d/megasync.repo" cat > "$ZYPP_FILE" << DATA [MEGAsync] name=MEGAsync type=rpm-md baseurl=https://mega.nz/linux/repo/%{reponame}/ gpgcheck=1 autorefresh=1 gpgkey=https://mega.nz/linux/repo/%{reponame}/repodata/repomd.xml.key enabled=1 DATA fi %endif ### include public signing key ##### # Install new key if it's not present # Notice, for openSuse, postinst is checked (and therefore executed) when creating the rpm # we need to ensure no command results in fail (returns !=0) # Remove old key if present. if (rpm -q gpg-pubkey-7f068e5d-563dc081 &> /dev/null); then mv /var/lib/rpm/.rpm.lock /var/lib/rpm/.rpm.lock_moved || : #to allow key management. %if 0%{?suse_version} #Key management would fail due to lock in /var/lib/rpm/Packages. We create a copy cp /var/lib/rpm/Packages{,_moved} mv /var/lib/rpm/Packages{_moved,} %endif rpm -e gpg-pubkey-7f068e5d-563dc081 mv /var/lib/rpm/.rpm.lock_moved /var/lib/rpm/.rpm.lock || : #take it back fi rpm -q gpg-pubkey-7094a482-61ded129 > /dev/null 2>&1 || KEY_NOT_FOUND=1 if [ ! -z "$KEY_NOT_FOUND" ]; then KEYFILE=$(mktemp /tmp/megasync.XXXXXX || :) if [ -n "$KEYFILE" ]; then cat > "$KEYFILE" <&1 || FAILED_IMPORT=1 mv /var/lib/rpm/.rpm.lock_moved /var/lib/rpm/.rpm.lock || : #take it back rm $KEYFILE || : fi fi sysctl -p /etc/sysctl.d/99-megacmd-inotify-limit.conf ### END of POSTINST %preun [ "$1" == "1" ] && pkill --signal SIGUSR1 mega-cmd-server 2> /dev/null || true %postun # kill running MEGAcmd instance when uninstall (!upgrade) [ "$1" == "0" ] && pkill mega-cmd 2> /dev/null || true [ "$1" == "0" ] && pkill mega-cmd-server 2> /dev/null || true %posttrans # to restore dormant MEGAcmd upon updates pkill --signal SIGUSR2 mega-cmd-server 2> /dev/null || true %clean %{?buildroot:%__rm -rf "%{buildroot}"} %files %defattr(-,root,root) %{_bindir}/* /opt/* /etc/bash_completion.d/megacmd_completion.sh /etc/sysctl.d/99-megacmd-inotify-limit.conf %changelog MEGAcmd-2.5.2_Linux/build/update_changelogs.sh000077500000000000000000000035131516543156300212210ustar00rootroot00000000000000#!/bin/bash ## # @file build/update_changelogs.sh # @brief Updates debian.changelog and megacmd.changes for the current version. # Run from the build/ directory before a release. # ## set -euo pipefail SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) cd "$SCRIPT_DIR" BASEPATH=$SCRIPT_DIR/../ # Get current version from CMakeLists.txt megacmd_VERSION=$(grep -Po "MEGACMD_.*_VERSION [0-9]*" "$BASEPATH/CMakeLists.txt" | awk '{print $2}' | paste -sd '.') echo "MEGAcmd version: $megacmd_VERSION" # Read the last version for which changelogs were generated version_file="version" if [ -s "$version_file" ]; then last_version=$(cat "$version_file") else last_version="none" fi if [ "$last_version" = "$megacmd_VERSION" ]; then echo "Changelogs already up to date for version $megacmd_VERSION (see build/version). Nothing to do." exit 0 fi # Update RPM changelog (megacmd.changes) changelog="megacmd/megacmd.changes" changelogold="megacmd/megacmd.changes.old" if [ -f "$changelog" ]; then mv "$changelog" "$changelogold" fi ./generate_rpm_changelog_entry.sh "$megacmd_VERSION" "$BASEPATH/src/megacmdversion.h" > "$changelog" if [ -f "$changelogold" ]; then cat "$changelogold" >> "$changelog" rm "$changelogold" fi echo "Updated $changelog" # Update DEB changelog (debian.changelog) changelog="megacmd/debian.changelog" changelogold="megacmd/debian.changelog.old" if [ -f "$changelog" ]; then mv "$changelog" "$changelogold" fi ./generate_deb_changelog_entry.sh "$megacmd_VERSION" "$BASEPATH/src/megacmdversion.h" > "$changelog" if [ -f "$changelogold" ]; then cat "$changelogold" >> "$changelog" rm "$changelogold" fi echo "Updated $changelog" # Record the version so we don't regenerate on subsequent runs echo "$megacmd_VERSION" > "$version_file" echo "Done. Version recorded as $megacmd_VERSION in build/version." MEGAcmd-2.5.2_Linux/build/version000066400000000000000000000000061516543156300166100ustar00rootroot000000000000002.5.2 MEGAcmd-2.5.2_Linux/contrib/000077500000000000000000000000001516543156300155455ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/contrib/docs/000077500000000000000000000000001516543156300164755ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/contrib/docs/BACKUPS.md000066400000000000000000000065631516543156300201210ustar00rootroot00000000000000# MEGA-BACKUPS - Backing up folders with MEGAcmd This is a brief tutorial on how to configure backups. Notice: the commands listed here assume you are using the interactive interaction mode: they are supposed to be executed within MEGAcmdShell. ## Creation Example: backup /path/mega/folder /remote/path --period="0 0 4 * * *" --num-backups=10 This will configure a backup of "myfolder" into /remote/path that will be carried out at 4:00 A.M. (UTC) every day. It will store the last 10 copies. Notice a first backup will be carried out immediately. In this example we are using cron-time expresions. You can find extra info on those using "backup --help". Backups will be stored as: ``` /remote/path/myfolder_bk_TIME1 /remote/path/myfolder_bk_TIME2 /remote/path/myfolder_bk_TIME3 ... ``` ## Listing You can list the backups configured typing `backup`: ``` TAG LOCALPATH REMOTEPARENTPATH STATUS 4 /path/mega/folder /remote/path ACTIVE ``` Notice the TAG. You can use it to refer to the backup if you wan to change its configuration or delete/abort it. ### Extra info If you type "backup -l" you will see extra information concerning the backup. Here, you will see when the next backup is scheduled for: ``` TAG LOCALPATH REMOTEPARENTPATH STATUS 4 /path/mega/folder /remote/path ONGOING Max Backups: 4 Period: "0 0 4 * * *" Next backup scheduled for: Fri, 19 Jan 2018 04:00:00 +0000 -- CURRENT/LAST BACKUP -- FILES UP/TOT FOLDERS CREATED PROGRESS 22/33 0 57.86/57.86 KB 1.27% ``` Also, you can see the progress of the current backup (or the last one is there is no backup being performed a the moment) ### Backup history: With "backup -h" you will be able to see the existing backups with their state and start time: ``` TAG LOCALPATH REMOTEPARENTPATH STATUS 4 /path/mega/folder /remote/path ONGOING -- SAVED BACKUPS -- NAME DATE STATUS FILES FOLDERS myfolder_bk_20180115175811 15Jan2018 17:58:11 COMPLETE 33 10 myfolder_bk_20180116182611 16Jan2018 18:26:11 COMPLETE 33 10 myfolder_bk_20180117182711 17Jan2018 18:27:11 ABORTED 13 10 myfolder_bk_20180118182911 18Jan2018 18:29:11 ONGOING 23 10 ``` Tip: If you are using linux/mac you can monitor the status actively in non-interactive mode with: ``` watch mega-backup -lh ``` # Control: ## Abort You can abort an ONGOING backup using its tag or it's local path. e.g.: ``` backup -a 4 ``` This will cancel all transfers and set the backup as ABORTED ## Delete Similarly you can remove a backup, to no longer backup that folder with: ``` backup -d /path/mega/folder ``` This will not remove the existing backups wich will be available in MEGA. ## Change configuration Similarly you can change the period or the number of backups to keep with: ``` backup 4 --period=2h ``` This will set our backup with TAG=4 to have a period of 2 hours. ``` backup /path/mega/folder --num-backups=1 ``` This will configure the backup to only keep one instance. Notice that in order not to lose data, older instances will not be deleted until the max number of backups is passed. MEGAcmd-2.5.2_Linux/contrib/docs/DEBUG.md000066400000000000000000000140341516543156300176470ustar00rootroot00000000000000# Debugging MEGAcmd There are two different kinds of logging messages: - MEGAcmd: these messages are reported by MEGAcmd itself. These messages will show information regarding the processing of user commands. They will be labeled with `cmd`. - SDK: these messages are reported by the sdk and its dependent libraries. These messages will show information regarding requests, transfers, network, etc. They will be labeled with `sdk`. The MEGAcmd server logs messages depending on the level of log adjusted to those two categories. You can adjust the level of logging for those kinds with `log` command. Log levels range from FATAL (the lowest) to VERBOSE (the highest). The log level of each message is displayed after the cmd/sdk prefix. Here's an example of some random log messages: ``` 2025-02-07_16-47-56.662269 cmd DBG Registering state listener petition with socket: 85 [comunicationsmanagerfilesockets.cpp:189] 2025-02-07_16-47-56.662366 cmd DTL Unregistering no longer listening client. Original petition: registerstatelistener [comunicationsmanagerfilesockets.cpp:346] 2025-02-07_16-47-56.662671 sdk INFO Request (RETRY_PENDING_CONNECTIONS) starting [megaapi_impl.cpp:16964] ``` The source file and line the message was logged from is appended at the end of the log message, to help developers quickly find out where they came from. ## How to access the logs Logs coming from MEGAcmd are written to the `megacmdserver.log` file. This file is not removed across restarts, so when it gets too large it is automatically compressed and renamed to include a timestamp (without stopping the logging). The log file is located in `$HOME/.megaCmd` for Linux and macOS, and `%LOCALAPPDATA%\MEGAcmd\.megaCmd` for Windows. ## Verbosity on startup You can start the server with higher level of verbosity in order to have log levels increased at startup, regardless of the log level configured by the `log` command mentioned above. You will need to pass `--debug-full` as an argument to the executable (e.g: `MEGAcmdServer.exe --debug-full`). Alternatively, you can set the `MEGACMD_LOGLEVEL` environment variable to `FULLVERBOSE` before starting the server. If you want other startup level of logging, you can use the following: * `--debug` or `MEGACMD_LOGLEVEL=DEBUG` will set ``` MEGAcmd log level = DEBUG SDK log level = DEFAULT ``` * `--debug-full` or `MEGACMD_LOGLEVEL=DEBUG` will set ``` MEGAcmd log level = DEBUG SDK log level = DEBUG ``` * `--verbose` or `MEGACMD_LOGLEVEL=VERBOSE` will set ``` MEGAcmd log level = VERBOSE SDK log level = DEFAULT ``` * `--verbose-full` or `MEGACMD_LOGLEVEL=FULLVERBOSE` will set ``` MEGAcmd log level = VERBOSE SDK log level = VERBOSE ``` ## Controlling verbosity of a single command In general, as we've mentioned before, lower verbosity log messages are not printed directly to the console. Only errors are. You can pass `-v`, `-vv`, and `-vvv` when running a command to ensure warning, debug, and verbose messages are printed (respectively). Note that this is only for the console; log level of `megacmdserver.log` will follow the rules explained above. ## JSON logs When the log level of the SDK is `VERBOSE`, the entire JSON payload of the HTTP requests sent and received from the API will be logged. This takes up a bit more space but provides more valuable info. Full JSON logging can be overwritten independently by setting the environment variable `MEGACMD_JSON_LOGS` to `0` or `1`. ## Configuring the Rotating Logger The MEGAcmd logger rotates and compresses log files to avoid taking up too much space. Some of its values can be configured to fit the needs of specific systems. These are: * `RotationType`: The type of rotation to use. Possible values are _Timestamp_ and _Numbered_. Defaults to _Timestamp_. * Numbered rotation will add a "file number" suffix when rotating files, keeping track of the total number and removing them if there are more than `MaxFilesToKeep`. * Timestamp rotation will keep track of the total number (similarly to above), while also keeping track of the creation date of files, removing them if they're older than `MaxFileAge`. * `CompressionType`: The type of compression to use. Possible values are _Gzip_ and _None_ (which disables compression). Defaults to _Gzip_. * `MaxFileMB`: The maximum size the `megacmdserver.log` file can be, in megabytes. If it gets over this size, it'll be renamed and compressed according to the rules stated above. Default is usually 50 MB, but will be less for disks with limited space. * `MaxFilesToKeep`: The maximum amount of rotated files allowed. When the total file count exceeds this value, older files will be removed. Default depends on `MaxFileMB`, the compression used, and the system specs. * `MaxFileAgeSeconds`: The maximum age the rotated files can be before being deleted, in seconds. Defaults to 1 month. _Note_: Only used by timestamp-based rotation. * `MaxMessageBusMB`: The maximum memory allowed by the logger's internal bus, in megabytes. Defaults to 512 MB. In most cases, the logger will use way less RAM; it is recommended to check memory usage before changing this value. To configure them we must manually edit the `megacmd.cfg` file. This file must be present in the same directory as the `megacmdserver.log` file; if not, we can manually create it. The following is an example of the syntax of this file: ``` RotatingLogger:RotationType=Timestamp RotatingLogger:CompressionType=None RotatingLogger:MaxFileMB=40.25 RotatingLogger:MaxFilesToKeep=20 RotatingLogger:MaxFileAgeSeconds=3600 RotatingLogger:MaxMessageBusMB=64.0 ``` Values not present in it will be set to their default. Invalid values (such as negative sizes) will be silently discarded. Note that this configuration is only loaded at the start, so the MEGAcmd server must be restarted after adding or changing any of the values. Configuring the Rotating Logger might result in previous log files not being rotated or deleted properly. It is recommended to delete them manually (or moving them somewhere else, if we want to preserve them) before changing the configuration. MEGAcmd-2.5.2_Linux/contrib/docs/FTP.md000066400000000000000000000152131516543156300174520ustar00rootroot00000000000000# MEGA-FTP - Serve you files as a FTP server with MEGAcmd This is a brief tutorial on how to configure [ftp](https://en.wikipedia.org/wiki/File_Transfer_Protocol) server. Configuring a FTP server will let you access your MEGA files as if they were located in your computer. All major platforms support access to FTP server. See [`Platform`](#platforms) usage. Notice: the commands listed here assume you are using the interactive interaction mode: they are supposed to be executed within MEGAcmdShell. ## Serving a folder Example: ``` ftp /path/mega/folder ``` This will configure a FTP server that will serve "myfolder". It'll show you the URL to access that path. You just use that location to configure access [according to your specific OS](#platforms). Once you have it configured, you can browse, edit, copy and delete your files using your favourite FTP client or add mount the network location and browse your files as if they were local file in your computer.. Caveat: They are not local, MEGAcmd transparently download/upload decrypt/encrypt those files. Hence, throughput will be decreased as compared to accessing to local files. Be patient. ## Streaming You can "ftp" a file, so as to offer streaming access to it: ``` ftp /path/to/myfile.mp4 ``` You will receive an URL that you can use in your preferred video player. ## Issues We have detected some issues with different software, when trying to save a file into a ftp served locations. Typically with software that creates temporary files. We will keep on trying to circumvent those. In FileZilla, default 20 seconds timeout (Edit -> Preferences -> Connection) will likely stop uploads before they are completed: once a upload is started, it begins to populate a temporary file. Once completed, the actual upload begins (the file gets transfered to MEGA), but no extra traffic is perceived by FileZilla during this time. If the upload takes longer than 20 seconds, it times out. You might want to increase that number for a correct behaviour. In Linux, using gvfsd-dav (Gnome's default ftp client), we have seen problems when trying to reproduce videos within an FTP served location. Recommended alternative: Use VLC. Open it, and drag & drop the file into VLC window. This will open the FTP URL and start streaming you video. If you find any more issues, don't hesitate to write to support@mega.nz, explaining what the problem is and how to reproduce it. ## Listing You can list the ftp served locations typing `ftp`: ``` FTP SERVED LOCATIONS: /path/mega/folder: ftp://127.0.0.1:4990/XXXXXXX/myfolder /path/to/myfile.mp4: ftp://127.0.0.1:4990/YYYYYYY/myfile.mp4 ``` These locations will be available as long as MEGAcmd is running. The configuration is persisted, and will be restored every time you restart MEGAcmd # Additional features/configurations ## Port & public server When you serve your first location, a FTP server is configured in port `4990`. You can change the port passing `--port=PORT` to your ftp command. Currently *only passive mode* is available. If your client does not work with passive FTP mode, it won't work. Data connections will work in the range of ports 1500 to 1600 by default. You can change those passing `--data-port=BEGIN-END` By default, the server is only accessible from the local machine. You can pass `--public` to your ftp command so as to allow remote access. In that case, use the IP of your server to access to it. ## FTPS Files in MEGA are encrypted, but you should bear in mind that the bare FTP server offers your files unencrypted. \ If you wish to add authenticity to your ftp server and integrity & privacy of the data transfered to/from the clients, you can secure it with [TLS](https://wikipedia.org/wiki/Transport_Layer_Security). Notice that FTPs is not the same as [SFTP](https://es.wikipedia.org/wiki/SSH_File_Transfer_Protocol), which is a complete different yet unsupported protocol. To serve via FTPS, you just need to pass `--tls` and the paths* to your certificate and key files (in PEM format): ``` ftp /path/mega/folder --tls --certificate=/path/to/certificate.pem --key=/path/to/certificate.key ``` *Those paths are local paths in your machine, not in MEGA. Currently, MEGAcmd only supports one server: although you can serve different locations, only one configuration is possible. The configuration used will be the one on your first served location. If you want to change that configuration you will need to stop serving each and every path and start over. ## Stop serving You can stop serving a MEGA location with: ``` ftp -d /path/mega/folder ``` If successfully, it will show a message indicating that the path is no longer served: ``` /path/mega/folder no longer served via ftp ``` ## Platforms All major platforms support accessing/mounting a ftp location. Here are some instructions to do that in Windows, Linux & Mac. ### Windows This instructions refer to Windows 10, but they are similar in other windows. Open an Explorer window, and then do right click on "This PC", and then "Add a network location...". ![ftpMenuWin.png](pics/ftpMenuWin.png?raw=true "ftpMenuWin.png") Then enter the URL MEGAcmd gave you ![ftpConnectToServerWin.png](pics/ftpConnectToServerWin.png?raw=true "ftpConnectToServerWin.png") And select "Log on Anonymously" ![ftpConnectToServerWin2.png](pics/ftpConnectToServerWin2.png?raw=true "ftpConnectToServerWin2.png") Then, you should see the new location in the navigation panel now. Windows FTP client does not allow writing, if you wish to modify your files you should either use an alternative client (like FileZilla), or perhaps serve your files using WEBDAV instead of FTP. ### Mac Open Find and in the Menu "Go", select "Connect to Server", or type **⌘ - k**: ![webdavMenuMac.png](pics/webdavMenuMac.png?raw=true "webdavMenuMac.png") Then enter the URL MEGAcmd gave you ![ftpConnectToServerMac.png](pics/ftpConnectToServerMac.png?raw=true "ftpConnectToServerMac.png") At the moment of writing this tutorial, there is no authentication mechanisms, hence you don't need to worry about providing a user name/password. Just proceed if you are prompted with default options. You should see the new location in the navigation panel now. ### Linux This instructions are for Nautilus, it should be similar using another file browser. Notice that `gvfs` currently does not support FTPs Click on File -> Connect to Server: ![webdavMenuLinux.png](pics/webdavMenuLinux.png?raw=true "webdavMenuLinux.png") Then enter the URL MEGAcmd gave you ![ftpConnectToServerLinux.png](pics/ftpConnectToServerLinux.png?raw=true "ftpConnectToServerLinux.png") You should see the new location in the navigation panel now. MEGAcmd-2.5.2_Linux/contrib/docs/FUSE.md000066400000000000000000000107511516543156300175650ustar00rootroot00000000000000# MEGA-FUSE — Serve your files as a "Filesystem in Userspace" (FUSE) with MEGAcmd This is a brief tutorial on how to configure and manage [FUSE](https://en.wikipedia.org/wiki/Filesystem_in_Userspace) with MEGAcmd. Configuring a FUSE mount will let you access your MEGA files as if they were located in your computer. After enabling a FUSE mount, you can use your favourite tools to browse, play, and edit your MEGA files. Note: this functionality is in beta, and not supported on macOS. ## Local cache MEGAcmd will use a cache to place files while working with mount points. This cache will be used to store both files downloaded from MEGA, and files being uploaded to MEGA. It will be created automatically in `$HOME/.megaCmd/fuse-cache` (`%LOCALAPPDATA%\MEGAcmd\.megaCmd\fuse-cache` in Windows). Bear in mind that this cache is fundamental to be able to work with FUSE mounts. Cache files are removed automatically. Restarting the MEGAcmd Server may help reduce the space used by the cache. ### Important caveats Files and folders created locally may not be immediately available in your MEGA cloud: the mount engine will transfer them transparently in the background. Note that the MEGAcmd Server needs to be running in order for these actions to complete. ## Streaming Currently, streaming is not directly supported. In order to open files in a FUSE mount point, they need to be downloaded completely to the local cache folder. You will need space in your hard drive to accomodate for these files. ## Usage ### Creating a new mount You can create a new FUSE mount with: ``` $ fuse-add /local/path/to/fuse/mountpoint /cloud/dir ``` After creating the mount, MEGAcmd will try to enable it; if it fails, the mount will remain disabled. See [`fuse-add`](commands/fuse-add.md) for all the possible options and arguments. The mount will have an associated name that we can use to refer to it when managing it. The name must be unique. A custom name can be chosen on creation with the `--name=custom_name` argument. We might also refer to a mount by its local path, but this is not necessarily unique: if multiple mounts share the same local path, we *must* use the name so we can distinguish between them. ### Displaying mounts The list of existing mounts can be displayed with: ``` $ fuse-show NAME LOCAL_PATH REMOTE_PATH PERSISTENT ENABLED dir /local/path/to/fuse/mountpoint /cloud/dir YES YES ``` Use `fuse-show ` to get further details on a specific mount. See [`fuse-show`](commands/fuse-show.md) for the list of all possible options and arguments. ### Enabling/disabling mounts To make your cloud files available in your local filesystem, the mount must be enabled. We can do so with: ``` $ fuse-enable ``` Note: mounts are enabled when created by default (unless `--disabled` is used). Similarly, we can stop exposing our cloud files locally with: ``` $ fuse-disable ``` Note: disabled mounts still exist and are shown in `fuse-show`. See [`fuse-enable`](commands/fuse-enable.md) and [`fuse-disable`](commands/fuse-disable.md) for more information on these commands. ### Adjusting configuration As we've mentioned before, the full details of a mount can be displayed with: ``` $ fuse-show dir Showing details of mount "dir" Local path: /local/path/to/fuse/mountpoint Remote path: /cloud/dir Name: dir Persistent: YES Enabled: YES Enable at startup: YES Read-only: NO ``` To configure these flags, we can use the [`fuse-config`](commands/fuse-config.md) command. For example, to make the mount read-only, we would do: ``` $ fuse-config --read-only=yes dir Mount "dir" now has the following flags Name: dir Enable at startup: YES Persistent: YES Read-only: YES ``` ### Removing mounts A mount can be removed with: ``` $ fuse-remove ``` Note: mounts must be disabled before they can be removed. ## Issues Occasionally, you may encounter issues in FUSE mounts that cannot be directly resolved within MEGAcmd (for example, if the MEGAcmd server was closed abruptly). The most common one is: "Error: cannot access '/local/path/to/fuse/mountpoint': Transport endpoint is not connected". To fix them, in Linux, you might need to use the `fusermount` command like so: ``` fusermount -u /local/path/to/fuse/mountpoint ``` Adding `-z` may help if the above fails. See the [fusermount man page](https://www.man7.org/linux/man-pages/man1/fusermount3.1.html). MEGAcmd-2.5.2_Linux/contrib/docs/WEBDAV.md000066400000000000000000000131231516543156300177670ustar00rootroot00000000000000# MEGA-WEBDAV - Serve you files as a WEBDAV server with MEGAcmd This is a brief tutorial on how to configure [webdav](https://wikipedia.org/wiki/WebDAV) server. Configuring a WEBDAV server will let you access your MEGA files as if they were located in your computer. All major platforms support access to WEBDAV server. See [`Platform`](#platforms) usage. Notice: the commands listed here assume you are using the interactive interaction mode: they are supposed to be executed within MEGAcmdShell. ## Serving a folder Example: ``` webdav /path/mega/folder ``` This will configure a WEBDAV server that will serve "myfolder". It'll show you the URL to access that path. You just use that location to configure access [according to your specific OS](#platforms). Once you have it configured, you can browse, edit, copy and delete your files as if they were local file in your computer. Caveat: They are not local, MEGAcmd transparently download/upload decrypt/encrypt those files. Hence, throughput will be decreased as compared to accessing to local files. Be patient. ## Streaming You can "webdav" a file, so as to offer streaming access to it: ``` webdav /path/to/myfile.mp4 ``` You will receive an URL that you can use in your favourite video player. ## Issues We have detected some issues with different software, when trying to save a file into a webdav served locations. Typically with software that creates temporary files. We will keep on trying to circumvent those. In Linux, using gvfsd-dav (Gnome's default webdav client), we have occasionally seen problems trying to open text files that have already been modified using some graphic editors. This is due to that gvfsd-dav tries to retrieve a URL different to the actual URL of the files. Reading the files through the console works just fine. This has been detected in Ubuntu 16.04. In Windows XP, copying a file from a MEGA webdav location, and pasting in a local folder does nothing. If you find any more issues, don't hesitate to write to support@mega.nz, explaining what the problem is and how to reproduce it. ## Listing You can list the webdav served locations typing `webdav`: ``` WEBDAV SERVED LOCATIONS: /path/mega/folder: http://127.0.0.1:4443/XXXXXXX/myfolder /path/to/myfile.mp4: http://127.0.0.1:4443/YYYYYYY/myfile.mp4 ``` These locations will be available as long as MEGAcmd is running. The configuration is persisted, and will be restored everytime you restart MEGAcmd # Additional features/configurations ## Port & public server When you serve your first location, a WEBDAV server is configured in port `4443`. You can change the port passing `--port=PORT` to your webdav command. By default, the server is only accessible from the local machine. You can pass `--public` to your webdav command so as to allow remote access. In that case, use the IP of your server to access to it. ## HTTPS Files in MEGA are encrypted, but you should bear in mind that the HTTP webdav server offers your files unencrypted. \ If you wish to add authenticity to your webdav server and integrity & privacy of the data transfered to/from the clients, you can secure it with [TLS](https://wikipedia.org/wiki/Transport_Layer_Security). You just need to pass `--tls` and the paths* to your certificate and key files (in PEM format): ``` webdav /path/mega/folder --tls --certificate=/path/to/certificate.pem --key=/path/to/certificate.key ``` *Those paths are local paths in your machine, not in MEGA. Currently, MEGAcmd only supports one server: although you can serve different locations, only one configuration is possible. The configuration used will be the one on your first served location. If you want to change that configuration you will need to stop serving each and every path and start over. ## Stop serving You can stop serving a MEGA location with: ``` webdav -d /path/mega/folder ``` If successfully, it will show a message indicating that the path is no longer served: ``` /path/mega/folder no longer served via webdav ``` ## Platforms All major platforms support accesing/mounting a webdav location. Here are some instructions to do that in Windows, Linux & Mac. ### Windows This instructions refer to Windows 10, but they are similar in other windows. Open an Explorer window, and then do right click on "This PC", and then "Map network drive...". ![webdavMenuWin.png](pics/webdavMenuWin.png?raw=true "webdavMenuWin.png") Then enter the URL MEGAcmd gave you ![webdavConnectToServerWin.png](pics/webdavConnectToServerWin.png?raw=true "webdavConnectToServerWin.png") Then, you should see the new location in the navigation panel now. ### Mac Open Find and in the Menu "Go", select "Connect to Server", or type **⌘ - k**: ![webdavMenuMac.png](pics/webdavMenuMac.png?raw=true "webdavMenuMac.png") Then enter the URL MEGAcmd gave you ![webdavConnectToServerMac.png](pics/webdavConnectToServerMac.png?raw=true "webdavConnectToServerMac.png") At the moment of writing this tutorial, there is no authentication mechanisms, hence you don't need to worry about providing a user name/password. Just proceed if you are prompted with default options. You should see the new location in the navigation panel now. ### Linux This instructions are for Nautilus, it should be similar using another file browser. Click on File -> Connect to Server: ![webdavMenuLinux.png](pics/webdavMenuLinux.png?raw=true "webdavMenuLinux.png") Then enter the URL MEGAcmd gave you ![webdavConnectToServerLinux.png](pics/webdavConnectToServerLinux.png?raw=true "webdavConnectToServerLinux.png") You should see the new location in the navigation panel now. MEGAcmd-2.5.2_Linux/contrib/docs/commands/000077500000000000000000000000001516543156300202765ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/contrib/docs/commands/attr.md000066400000000000000000000012771516543156300216010ustar00rootroot00000000000000### attr Lists/updates node attributes. Usage: `attr remotepath [--force-non-officialficial] [-s attribute value|-d attribute [--print-only-value]`

Options:
 -s     attribute value         Sets an attribute to a value
 -d     attribute               Removes the attribute
 --print-only-value     When listing attributes, print only the values, not the attribute names.
 --force-non-official   Forces using the custom attribute version for officially recognized attributes.
                        Note that custom attributes are internally stored with a `_` prefix.
                        Use this option to show, modify or delete a custom attribute with the same name as one official.
MEGAcmd-2.5.2_Linux/contrib/docs/commands/autocomplete.md000066400000000000000000000006731516543156300233270ustar00rootroot00000000000000### autocomplete Modifies how tab completion operates. Usage: `autocomplete [dos | unix]`
The default is to operate like the native platform. However
you can switch it between mode 'dos' and 'unix' as you prefer.
Options:
 dos	Each press of tab places the next option into the command line
 unix	Options are listed in a table, or put in-line if there is only one

Note: this command is only available on some versions of Windows
MEGAcmd-2.5.2_Linux/contrib/docs/commands/backup.md000066400000000000000000000117511516543156300220720ustar00rootroot00000000000000### backup Controls backups Usage: `backup (localpath remotepath --period="PERIODSTRING" --num-backups=N | [-lhda] [TAG|localpath] [--period="PERIODSTRING"] [--num-backups=N]) [--time-format=FORMAT]`
This command can be used to configure and control backups.
A tutorial can be found here: https://github.com/meganz/MEGAcmd/blob/master/contrib/docs/BACKUPS.md

If no argument is given it will list the configured backups
 To get extra info on backups use -l or -h (see Options below)

When a backup of a folder (localfolder) is established in a remote folder (remotepath)
 MEGAcmd will create subfolder within the remote path with names like: "localfoldername_bk_TIME"
 which shall contain a backup of the local folder at that specific time
In order to configure a backup you need to specify the local and remote paths,
the period and max number of backups to store (see Configuration Options below).
Once configured, you can see extended info asociated to the backup (See Display Options)
Notice that MEGAcmd server need to be running for backups to be created.

Display Options:
-l	Show extended info: period, max number, next scheduled backup
  	 or the status of current/last backup
-h	Show history of created backups
  	Backup states:
  	While a backup is being performed, the backup will be considered and labeled as ONGOING
  	If a transfer is cancelled or fails, the backup will be considered INCOMPLETE
  	If a backup is aborted (see -a), all the transfers will be canceled and the backup be ABORTED
  	If MEGAcmd server stops during a transfer, it will be considered MISCARRIED
  	  Notice that currently when MEGAcmd server is restarted, ongoing and scheduled transfers
  	  will be carried out nevertheless.
  	If MEGAcmd server is not running when a backup is scheduled and the time for the next one has already arrived,
  	 an empty BACKUP will be created with state SKIPPED
  	If a backup(1) is ONGOING and the time for the next backup(2) arrives, it won't start until the previous one(1)
  	 is completed, and if by the time the first one(1) ends the time for the next one(3) has already arrived,
  	 an empty BACKUP(2) will be created with state SKIPPED
 --path-display-size=N	Use a fixed size of N characters for paths
 --time-format=FORMAT	show time in available formats. Examples:
               RFC2822:  Example: Fri, 06 Apr 2018 13:05:37 +0200
               ISO6081:  Example: 2018-04-06
               ISO6081_WITH_TIME:  Example: 2018-04-06T13:05:37
               SHORT:  Example: 06Apr2018 13:05:37
               SHORT_UTC:  Example: 06Apr2018 13:05:37
               CUSTOM. e.g: --time-format="%Y %b":  Example: 2018 Apr
                 You can use any strftime compliant format: http://www.cplusplus.com/reference/ctime/strftime/

Configuration Options:
--period="PERIODSTRING"	Period: either time in TIMEFORMAT (see below) or a cron like expression
                       	 Cron like period is formatted as follows
                       	  - - - - - -
                       	  | | | | | |
                       	  | | | | | |
                       	  | | | | | +---- Day of the Week   (range: 1-7, 1 standing for Monday)
                       	  | | | | +------ Month of the Year (range: 1-12)
                       	  | | | +-------- Day of the Month  (range: 1-31)
                       	  | | +---------- Hour              (range: 0-23)
                       	  | +------------ Minute            (range: 0-59)
                       	  +-------------- Second            (range: 0-59)
                       	 examples:
                       	  - daily at 04:00:00 (UTC): "0 0 4 * * *"
                       	  - every 15th day at 00:00:00 (UTC) "0 0 0 15 * *"
                       	  - mondays at 04.30.00 (UTC): "0 30 4 * * 1"
                       	 TIMEFORMAT can be expressed in hours(h), days(d),
                       	   minutes(M), seconds(s), months(m) or years(y)
                       	   e.g. "1m12d3h" indicates 1 month, 12 days and 3 hours
                       	  Notice that this is an uncertain measure since not all months
                       	  last the same and Daylight saving time changes are not considered
                       	  If possible use a cron like expression
                       	Notice: regardless of the period expression, the first time you establish a backup,
                       	 it will be created immediately
--num-backups=N	Maximum number of backups to store
                 	 After creating the backup (N+1) the oldest one will be deleted
                 	  That might not be true in case there are incomplete backups:
                 	   in order not to lose data, at least one COMPLETE backup will be kept
Use backup TAG|localpath --option=VALUE to modify existing backups

Management Options:
-d TAG|localpath	Removes a backup by its TAG or local path
                	 Folders created by backup won't be deleted
-a TAG|localpath	Aborts ongoing backup

Caveat: This functionality is in BETA state. If you experience any issue with this, please contact: support@mega.nz
MEGAcmd-2.5.2_Linux/contrib/docs/commands/cancel.md000066400000000000000000000005471516543156300220530ustar00rootroot00000000000000### cancel Cancels your MEGA account Usage: `cancel`
Caution: The account under this email address will be permanently closed
 and your data deleted. This can not be undone.

The cancellation will not take place immediately. You will need to confirm the cancellation
using a link that will be delivered to your email. See "confirmcancel --help"
MEGAcmd-2.5.2_Linux/contrib/docs/commands/cat.md000066400000000000000000000004501516543156300213660ustar00rootroot00000000000000### cat Prints the contents of remote files Usage: `cat remotepath1 remotepath2 ...`
To avoid issues with encoding on Windows, if you want to cat the exact binary contents of a remote file into a local one,
use non-interactive mode with -o /path/to/file. See help "non-interactive"
MEGAcmd-2.5.2_Linux/contrib/docs/commands/cd.md000066400000000000000000000002201516543156300212000ustar00rootroot00000000000000### cd Changes the current remote folder Usage: `cd [remotepath]`
If no folder is provided, it will be changed to the root folder
MEGAcmd-2.5.2_Linux/contrib/docs/commands/clear.md000066400000000000000000000000471516543156300217070ustar00rootroot00000000000000### clear Clear screen Usage: `clear` MEGAcmd-2.5.2_Linux/contrib/docs/commands/codepage.md000066400000000000000000000012641516543156300223720ustar00rootroot00000000000000### codepage Switches the codepage used to decide which characters show on-screen. Usage: `codepage [N [M]]`
MEGAcmd supports unicode or specific code pages.  For european countries you may need
to select a suitable codepage or secondary codepage for the character set you use.
Of course a font containing the glyphs you need must have been selected for the terminal first.
Options:
 (no option)	Outputs the selected code page and secondary codepage (if configured).
 N	Sets the main codepage to N. 65001 is Unicode.
 M	Sets the secondary codepage to M, which is used if the primary can't translate a character.

Note: this command is only available on some versions of Windows
MEGAcmd-2.5.2_Linux/contrib/docs/commands/configure.md000066400000000000000000000016261516543156300226060ustar00rootroot00000000000000### configure Shows and modifies global configurations. Usage: `configure [key [value]]`
If no keys are provided, it will list all configuration keys and values.
If a key is provided, but no value given, it will only show the value of such key.
If a key and value are provided, it will set the value of that key.

Possible keys:
 - max_nodes_in_cache      Max nodes loaded in memory.
                           This controls the number of nodes that the SDK stores in memory.
 - exported_folders_sdks   Number of additional SDK instances loaded at startup.
                           This controls the number of SDK instances that are created at
                           startup in order to download or import contents from exported
                           folder links. Default 5. Min 0. Max 20. If set to 0, you will not
                           be able to download or import from folder links.
MEGAcmd-2.5.2_Linux/contrib/docs/commands/confirm.md000066400000000000000000000003061516543156300222540ustar00rootroot00000000000000### confirm Confirm an account using the link provided after the "signup" process. Usage: `confirm link email password`
It requires the email and the password used to obtain the link.
MEGAcmd-2.5.2_Linux/contrib/docs/commands/confirmcancel.md000066400000000000000000000003551516543156300234260ustar00rootroot00000000000000### confirmcancel Confirms the cancellation of your MEGA account Usage: `confirmcancel link password`
Caution: The account under this email address will be permanently closed
 and your data deleted. This can not be undone.
MEGAcmd-2.5.2_Linux/contrib/docs/commands/cp.md000066400000000000000000000012141516543156300212200ustar00rootroot00000000000000### cp Copies files/folders into a new location (all remotes) Usage: `cp [--use-pcre] srcremotepath [srcremotepath2 srcremotepath3 ..] dstremotepath|dstemail:`
If the location exists and is a folder, the source will be copied there
If the location doesn't exist, and only one source is provided,
 the file/folder will be copied and renamed to the destination name given.

If "dstemail:" provided, the file/folder will be sent to that user's inbox (//in)
 e.g: cp /path/to/file user@doma.in:
 Remember the trailing ":", otherwise a file with the name of that user ("user@doma.in") will be created
Options:
 --use-pcre	use PCRE expressions
MEGAcmd-2.5.2_Linux/contrib/docs/commands/debug.md000066400000000000000000000002001516543156300216760ustar00rootroot00000000000000### debug Enters debugging mode (HIGHLY VERBOSE) Usage: `debug`
For a finer control of log level see "log --help"
MEGAcmd-2.5.2_Linux/contrib/docs/commands/deleteversions.md000066400000000000000000000011551516543156300236550ustar00rootroot00000000000000### deleteversions Deletes previous versions. Usage: `deleteversions [-f] (--all | remotepath1 remotepath2 ...) [--use-pcre]`
This will permanently delete all historical versions of a file.
The current version of the file will remain.
Note: any file version shared to you from a contact will need to be deleted by them.

Options:
 -f   	Force (no asking)
 --all	Delete versions of all nodes. This will delete the version histories of all files (not current files).
 --use-pcre	use PCRE expressions

To see versions of a file use "ls --versions".
To see space occupied by file versions use "du --versions".
MEGAcmd-2.5.2_Linux/contrib/docs/commands/df.md000066400000000000000000000003251516543156300212110ustar00rootroot00000000000000### df Shows storage info Usage: `df [-h]`
Shows total storage used in the account, storage per main folder (see mount)

Options:
 -h	Human readable sizes. Otherwise, size will be expressed in Bytes
MEGAcmd-2.5.2_Linux/contrib/docs/commands/du.md000066400000000000000000000010441516543156300212270ustar00rootroot00000000000000### du Prints size used by files/folders Usage: `du [-h] [--versions] [remotepath remotepath2 remotepath3 ... ] [--use-pcre]`
remotepath can be a pattern (Perl Compatible Regular Expressions with "--use-pcre"
   or wildcarded expressions with ? or * like f*00?.txt)

Options:
 -h	Human readable
 --versions	Calculate size including all versions.
   	You can remove all versions with "deleteversions" and list them with "ls --versions"
 --path-display-size=N	Use a fixed size of N characters for paths
 --use-pcre	use PCRE expressions
MEGAcmd-2.5.2_Linux/contrib/docs/commands/errorcode.md000066400000000000000000000001121516543156300225760ustar00rootroot00000000000000### errorcode Translate error code into string Usage: `errorcode number` MEGAcmd-2.5.2_Linux/contrib/docs/commands/exclude.md000066400000000000000000000010421516543156300222460ustar00rootroot00000000000000### exclude Manages default exclusion rules in syncs. Usage: `exclude [(-a|-d) pattern1 pattern2 pattern3]`
These default rules will be used when creating new syncs. Existing syncs won't be affected. To modify the exclusion rules of existing syncs, use mega-sync-ignore.

Options:
 -a pattern1 pattern2 ...	adds pattern(s) to the exclusion list
                         	          (* and ? wildcards allowed)
 -d pattern1 pattern2 ...	deletes pattern(s) from the exclusion list

This command is DEPRECATED. Use sync-ignore instead.
MEGAcmd-2.5.2_Linux/contrib/docs/commands/exit.md000066400000000000000000000003051516543156300215670ustar00rootroot00000000000000### exit Quits MEGAcmd Usage: `exit [--only-shell]`
Notice that the session will still be active, and local caches available
The session will be resumed when the service is restarted
MEGAcmd-2.5.2_Linux/contrib/docs/commands/export.md000066400000000000000000000067271516543156300221550ustar00rootroot00000000000000### export Prints/Modifies the status of current exports Usage: `export [-d|-a [--writable] [--mega-hosted] [--password=PASSWORD] [--expire=TIMEDELAY] [-f]] [remotepath] [--use-pcre] [--time-format=FORMAT]`
Options:
 --use-pcre	The provided path will use Perl Compatible Regular Expressions (PCRE)
 -a	Adds an export.
   	Returns an error if the export already exists.
   	To modify an existing export (e.g. to change expiration time, password, etc.), it must be deleted and then re-added.
 --writable	Turn an export folder into a writable folder link. You can use writable folder links to share and receive files from anyone; including people who don’t have a MEGA account. 
           	This type of link is the same as a "file request" link that can be created through the webclient, except that writable folder links are not write-only. Writable folder links and file requests cannot be mixed, as they use different encryption schemes.
           	The auth-key shown has the following format #:. The auth-key must be provided at login, otherwise you will log into this link with read-only privileges. See "mega-login --help" for more details about logging into links.
 --mega-hosted	The share key of this specific folder will be shared with MEGA.
              	This is intended to be used for folders accessible through MEGA's S4 service.
              	Encryption will occur nonetheless within MEGA's S4 service.
 --password=PASSWORD	Protects the export with a password. Passwords cannot contain " or '.
                    	A password-protected link will be printed only after exporting it.
                    	If "mega-export" is used to print it again, it will be shown unencrypted.
                    	Note: only PRO users can protect an export with a password.
 --expire=TIMEDELAY	Sets the expiration time of the export.
                   	The time format can contain hours(h), days(d), minutes(M), seconds(s), months(m) or years(y).
                   	E.g., "1m12d3h" will set an expiration time of 1 month, 12 days and 3 hours (relative to the current time).
                   	Note: only PRO users can set an expiration time for an export.
 -f	Implicitly accepts copyright terms (only shown the first time an export is made).
   	MEGA respects the copyrights of others and requires that users of the MEGA cloud service comply with the laws of copyright.
   	You are strictly prohibited from using the MEGA cloud service to infringe copyright.
   	You may not upload, download, store, share, display, stream, distribute, email, link to, transmit or otherwise make available any files, data or content that infringes any copyright or other proprietary rights of any person or entity.
 -d	Deletes an export.
   	The file/folder itself is not deleted, only the export link.
 --time-format=FORMAT	show time in available formats. Examples:
               RFC2822:  Example: Fri, 06 Apr 2018 13:05:37 +0200
               ISO6081:  Example: 2018-04-06
               ISO6081_WITH_TIME:  Example: 2018-04-06T13:05:37
               SHORT:  Example: 06Apr2018 13:05:37
               SHORT_UTC:  Example: 06Apr2018 13:05:37
               CUSTOM. e.g: --time-format="%Y %b":  Example: 2018 Apr
                 You can use any strftime compliant format: http://www.cplusplus.com/reference/ctime/strftime/

If a remote path is provided without the add/delete options, all existing exports within its tree will be displayed.
If no remote path is given, the current working directory will be used.
MEGAcmd-2.5.2_Linux/contrib/docs/commands/find.md000066400000000000000000000044001516543156300215360ustar00rootroot00000000000000### find Find nodes matching a pattern Usage: `find [remotepath] [-l] [--pattern=PATTERN] [--type=d|f] [--mtime=TIMECONSTRAIN] [--size=SIZECONSTRAIN] [--use-pcre] [--time-format=FORMAT] [--show-handles|--print-only-handles]`
Options:
 --pattern=PATTERN	Pattern to match (Perl Compatible Regular Expressions with "--use-pcre"
   or wildcarded expressions with ? or * like f*00?.txt)
 --type=d|f           	Determines type. (d) for folder, f for files
 --mtime=TIMECONSTRAIN	Determines time constrains, in the form: [+-]TIMEVALUE
                      	  TIMEVALUE may include hours(h), days(d), minutes(M),
                      	   seconds(s), months(m) or years(y)
                      	  Examples:
                      	   "+1m12d3h" shows files modified before 1 month,
                      	    12 days and 3 hours the current moment
                      	   "-3h" shows files modified within the last 3 hours
                      	   "-3d+1h" shows files modified in the last 3 days prior to the last hour
 --size=SIZECONSTRAIN	Determines size constrains, in the form: [+-]TIMEVALUE
                      	  TIMEVALUE may include (B)ytes, (K)ilobytes, (M)egabytes, (G)igabytes & (T)erabytes
                      	  Examples:
                      	   "+1m12k3B" shows files bigger than 1 Mega, 12 Kbytes and 3Bytes
                      	   "-3M" shows files smaller than 3 Megabytes
                      	   "-4M+100K" shows files smaller than 4 Mbytes and bigger than 100 Kbytes
 --show-handles	Prints files/folders handles (H:XXXXXXXX). You can address a file/folder by its handle
 --print-only-handles	Prints only files/folders handles (H:XXXXXXXX). You can address a file/folder by its handle
 --use-pcre	use PCRE expressions
 -l	Prints file info
 --time-format=FORMAT	show time in available formats. Examples:
               RFC2822:  Example: Fri, 06 Apr 2018 13:05:37 +0200
               ISO6081:  Example: 2018-04-06
               ISO6081_WITH_TIME:  Example: 2018-04-06T13:05:37
               SHORT:  Example: 06Apr2018 13:05:37
               SHORT_UTC:  Example: 06Apr2018 13:05:37
               CUSTOM. e.g: --time-format="%Y %b":  Example: 2018 Apr
                 You can use any strftime compliant format: http://www.cplusplus.com/reference/ctime/strftime/
MEGAcmd-2.5.2_Linux/contrib/docs/commands/ftp.md000066400000000000000000000026671516543156300214240ustar00rootroot00000000000000### ftp Configures a FTP server to serve a location in MEGA Usage: `ftp [-d ( --all | remotepath ) ] [ remotepath [--port=PORT] [--data-ports=BEGIN-END] [--public] [--tls --certificate=/path/to/certificate.pem --key=/path/to/certificate.key]] [--use-pcre]`
This can also be used for streaming files. The server will be running as long as MEGAcmd Server is.
If no argument is given, it will list the ftp enabled locations.

Options:
 --d        	Stops serving that location
 --all      	When used with -d, stops serving all locations (and stops the server)
 --public   	*Allow access from outside localhost
 --port=PORT	*Port to serve. DEFAULT=4990
 --data-ports=BEGIN-END	*Ports range used for data channel (in passive mode). DEFAULT=1500-1600
 --tls      	*Serve with TLS (FTPs)
 --certificate=/path/to/certificate.pem	*Path to PEM formatted certificate
 --key=/path/to/certificate.key	*Path to PEM formatted key
 --use-pcre	use PCRE expressions

*If you serve more than one location, these parameters will be ignored and used those of the first location served.
 If you want to change those parameters, you need to stop serving all locations and configure them again.
Note: FTP settings and locations will be saved for the next time you open MEGAcmd, but will be removed if you logout.

Caveat: This functionality is in BETA state. It might not be available on all platforms. If you experience any issue with this, please contact: support@mega.nz
MEGAcmd-2.5.2_Linux/contrib/docs/commands/fuse-add.md000066400000000000000000000044271516543156300223170ustar00rootroot00000000000000### fuse-add Creates a new FUSE mount. Usage: `fuse-add [--name=name] [--disabled] [--transient] [--read-only] localPath remotePath`
Mounts are automatically enabled after being added, making the chosen MEGA folder accessible within the local filesystem.
When a mount is disabled, its configuration will be saved, but the cloud folder will not be mounted locally (see fuse-disable).
Mounts are persisted after restarts and writable by default. You may change these and other options of a FUSE mount with fuse-config.
Use fuse-show to display the list of mounts.

Parameters:
 localPath    [unix only] Specifies where the files contained by remotePath should be visible on the local filesystem.
 remotePath   Specifies what directory (or share) should be exposed on the local filesystem.

Options:
 --name=name  A user friendly name which the mount can be identified by. If not provided, the display name
              of the entity specified by remotePath will be used. If remotePath specifies the entire cloud
              drive, the mount's name will be "MEGA". If remotePath specifies the rubbish bin, the mount's
              name will be "MEGA Rubbish".
 --read-only  Specifies that the mount should be read-only. Otherwise, the mount is writable.
 --transient  Specifies that the mount should be transient, meaning it will be lost on restart.
              Otherwise, the mount is persistent, meaning it will remain across on restarts.
 --disabled   Specifies that the mount should not enabled after being added, and must be enabled manually. See fuse-enable.
              If this option is passed, the mount will not be automatically enabled at startup.

Disclaimer:
    - Streaming is not supported; entire files need to be downloaded completely before being opened.
    - FUSE uses a local cache located in the MEGAcmd configuration folder. Make sure you have enough available space in your hard drive to accommodate new files. Restarting MEGAcmd server can help discard old files.
    - File writes might be deferred. When files are updated in the local mount point, a transfer will be initiated. Your files will be available in MEGA only after pending transfers finish.

FUSE commands are in early BETA. They are not available in macOS. If you experience any issues, please contact support@mega.nz.
MEGAcmd-2.5.2_Linux/contrib/docs/commands/fuse-config.md000066400000000000000000000017611516543156300230320ustar00rootroot00000000000000### fuse-config Modifies the specified FUSE mount configuration. Usage: `fuse-config [--name=name] [--enable-at-startup=yes|no] [--persistent=yes|no] [--read-only=yes|no] (name|localPath)`
Parameters:
 name|localPath   The identifier of the mount we want to remove. It can be one of the following:
                   Name: the user-friendly name of the mount, specified when it was added or by fuse-config.
                   Local path: The local mount point in the filesystem.

Options:
 --name=name                  Sets the friendly name used to uniquely identify the mount.
 --enable-at-startup=yes|no   Controls whether or not the mount should be enabled automatically on startup.
 --persistent=yes|no          Controls whether or not the mount is saved across restarts.
 --read-only=yes|no           Controls whether the mount is read-only or writable.

Note: FUSE commands are in early BETA. They are not available in macOS. If you experience any issues, please contact support@mega.nz.
MEGAcmd-2.5.2_Linux/contrib/docs/commands/fuse-disable.md000066400000000000000000000016061516543156300231660ustar00rootroot00000000000000### fuse-disable Disables a specified FUSE mount. Usage: `fuse-disable [--temporarily] (name|localPath)`
After a mount has been disabled, its cloud entities will no longer be accessible via the mount's local path. You may enable it again via fuse-enable.

Parameters:
 name|localPath   The identifier of the mount we want to remove. It can be one of the following:
                   Name: the user-friendly name of the mount, specified when it was added or by fuse-config.
                   Local path: The local mount point in the filesystem.
Options:
 --temporarily   Specifies whether the mount should be disabled only until the server is restarted.
                 Has no effect on transient mounts, since any action on them is always temporary.

Note: FUSE commands are in early BETA. They are not available in macOS. If you experience any issues, please contact support@mega.nz.
MEGAcmd-2.5.2_Linux/contrib/docs/commands/fuse-enable.md000066400000000000000000000015171516543156300230120ustar00rootroot00000000000000### fuse-enable Enables a specified FUSE mount. Usage: `fuse-enable [--temporarily] (name|localPath)`
After a mount has been enabled, its cloud entities will be accessible via the mount's local path.

Parameters:
 name|localPath   The identifier of the mount we want to remove. It can be one of the following:
                   Name: the user-friendly name of the mount, specified when it was added or by fuse-config.
                   Local path: The local mount point in the filesystem.

Options:
 --temporarily   Specifies whether the mount should be enabled only until the server is restarted.
                 Has no effect on transient mounts, since any action on them is always temporary.

Note: FUSE commands are in early BETA. They are not available in macOS. If you experience any issues, please contact support@mega.nz.
MEGAcmd-2.5.2_Linux/contrib/docs/commands/fuse-remove.md000066400000000000000000000010151516543156300230520ustar00rootroot00000000000000### fuse-remove Deletes a specified FUSE mount. Usage: `fuse-remove (name|localPath)`
Parameters:
 name|localPath   The identifier of the mount we want to remove. It can be one of the following:
                   Name: the user-friendly name of the mount, specified when it was added or by fuse-config.
                   Local path: The local mount point in the filesystem.

Note: FUSE commands are in early BETA. They are not available in macOS. If you experience any issues, please contact support@mega.nz.
MEGAcmd-2.5.2_Linux/contrib/docs/commands/fuse-show.md000066400000000000000000000032741516543156300225460ustar00rootroot00000000000000### fuse-show Displays the list of FUSE mounts and their information. If a name or local path provided, displays information of that mount instead. Usage: `fuse-show [--only-enabled] [--disable-path-collapse] [[--limit=rowcount] | [name|localPath]]`
When all mounts are shown, the following columns are displayed:
   NAME: The user-friendly name of the mount, specified when it was added or by fuse-config.
   LOCAL_PATH: The local mount point in the filesystem.
   REMOTE_PATH: The cloud directory or share that is exposed locally.
   PERSISTENT: If the mount is saved across restarts, "YES". Otherwise, "NO".
   ENABLED: If the mount is currently enabled, "YES". Otherwise, "NO".

Parameters:
 name|localPath   The identifier of the mount we want to remove. It can be one of the following:
                   Name: the user-friendly name of the mount, specified when it was added or by fuse-config.
                   Local path: The local mount point in the filesystem.
                    If not provided, the list of mounts will be shown instead.

Options:
 --only-enabled           Only shows mounts that are enabled.
 --disable-path-collapse  Ensures all paths are fully shown. By default long paths are truncated for readability.
 --limit=rowcount         Limits the amount of rows displayed. Set to 0 to display unlimited rows. Default is unlimited.
 --col-separator=X	Uses the string "X" as column separator. Otherwise, spaces will be added between columns to align them.
 --output-cols=COLUMN_NAME_1,COLUMN_NAME2,...	Selects which columns to show and their order.

Note: FUSE commands are in early BETA. They are not available in macOS. If you experience any issues, please contact support@mega.nz.
MEGAcmd-2.5.2_Linux/contrib/docs/commands/get.md000066400000000000000000000025011516543156300213750ustar00rootroot00000000000000### get Downloads a remote file/folder or a public link Usage: `get [-m] [-q] [--ignore-quota-warn] [--use-pcre] [--password=PASSWORD] exportedlink|remotepath [localpath]`
In case it is a file, the file will be downloaded at the specified folder
                             (or at the current folder if none specified).
  If the localpath (destination) already exists and is the same (same contents)
  nothing will be done. If differs, it will create a new file appending " (NUM)"

For folders, the entire contents (and the root folder itself) will be
                    by default downloaded into the destination folder

Exported links: Exported links are usually formed as publiclink#key.
 Alternativelly you can provide a password-protected link and
 provide the password with --password. Please, avoid using passwords containing " or '


Options:
 -q	queue download: execute in the background. Don't wait for it to end
 -m	if the folder already exists, the contents will be merged with the
                     downloaded one (preserving the existing files)
 --ignore-quota-warn	ignore quota surpassing warning.
                    	  The download will be attempted anyway.
 --password=PASSWORD	Password to decrypt the password-protected link. Please, avoid using passwords containing " or '
 --use-pcre	use PCRE expressions
MEGAcmd-2.5.2_Linux/contrib/docs/commands/graphics.md000066400000000000000000000011171516543156300224200ustar00rootroot00000000000000### graphics Shows if special features related to images and videos are enabled. Usage: `graphics [on|off]`
Use "graphics on/off" to enable/disable it.

Disabling these features will avoid the upload of previews and thumbnails
for images and videos.

It's only recommended to disable these features before uploading files
with image or video extensions that are not really images or videos,
or that are encrypted in the local drive so they can't be analyzed anyway.

Notice that this setting will be saved for the next time you open MEGAcmd, but will be removed if you logout.
MEGAcmd-2.5.2_Linux/contrib/docs/commands/help.md000066400000000000000000000007431516543156300215540ustar00rootroot00000000000000### help Prints list of commands Usage: `help [-f|-ff|--non-interactive|--upgrade|--paths] [--show-all-options]`
Options:
 -f 	Include a brief description of the commands
 -ff	Get a complete description of all commands
 --non-interactive  Display information on how to use MEGAcmd with scripts
 --upgrade          Display information on PRO plans
 --paths            Show caveats of local and remote paths
 --show-all-options Display all options regardless of platform
MEGAcmd-2.5.2_Linux/contrib/docs/commands/https.md000066400000000000000000000005531516543156300217650ustar00rootroot00000000000000### https Shows if HTTPS is used for transfers. Use "https on" to enable it. Usage: `https [on|off]`
HTTPS is not necessary since all data is stored and transferred encrypted.
Enabling it will increase CPU usage and add network overhead.

Notice that this setting will be saved for the next time you open MEGAcmd, but will be removed if you logout.
MEGAcmd-2.5.2_Linux/contrib/docs/commands/import.md000066400000000000000000000006601516543156300221340ustar00rootroot00000000000000### import Imports the contents of a remote link into user's cloud Usage: `import exportedlink [--password=PASSWORD] [remotepath]`
If no remote path is provided, the current local folder will be used
Exported links: Exported links are usually formed as publiclink#key.
 Alternativelly you can provide a password-protected link and
 provide the password with --password. Please, avoid using passwords containing " or '
MEGAcmd-2.5.2_Linux/contrib/docs/commands/invite.md000066400000000000000000000005171516543156300221210ustar00rootroot00000000000000### invite Invites a contact / deletes an invitation Usage: `invite [-d|-r] dstemail [--message="MESSAGE"]`
Options:
 -d	Deletes invitation
 -r	Sends the invitation again
 --message="MESSAGE"	Sends inviting message

Use "showpcr" to browse invitations
Use "ipc" to manage invitations received
Use "users" to see contacts
MEGAcmd-2.5.2_Linux/contrib/docs/commands/ipc.md000066400000000000000000000005771516543156300214040ustar00rootroot00000000000000### ipc Manages contact incoming invitations. Usage: `ipc email|handle -a|-d|-i`
Options:
 -a	Accepts invitation
 -d	Rejects invitation
 -i	Ignores invitation [WARNING: do not use unless you know what you are doing]

Use "mega-invite" to send/remove invitations to other users
Use "mega-showpcr" to browse incoming/outgoing invitations
Use "mega-users" to see contacts
MEGAcmd-2.5.2_Linux/contrib/docs/commands/killsession.md000066400000000000000000000003221516543156300231540ustar00rootroot00000000000000### killsession Kills a session of current user. Usage: `killsession [-a | sessionid1 sessionid2 ... ]`
Options:
 -a	kills all sessions except the current one

To see all sessions use "whoami -l"
MEGAcmd-2.5.2_Linux/contrib/docs/commands/lcd.md000066400000000000000000000004041516543156300213600ustar00rootroot00000000000000### lcd Changes the current local folder for the interactive console Usage: `lcd [localpath]`
It will be used for uploads and downloads

If not using interactive console, the current local folder will be
 that of the shell executing mega comands
MEGAcmd-2.5.2_Linux/contrib/docs/commands/log.md000066400000000000000000000010131516543156300213740ustar00rootroot00000000000000### log Prints/Modifies the log level Usage: `log [-sc] level`
Options:
 -c	CMD log level (higher level messages).
   	 Messages captured by MEGAcmd server.
 -s	SDK log level (lower level messages).
   	 Messages captured by the engine and libs
Note: this setting will be saved for the next time you open MEGAcmd, but will be removed if you logout.

Regardless of the log level of the
 interactive shell, you can increase the amount of information given
   by any command by passing "-v" ("-vv", "-vvv", ...)
MEGAcmd-2.5.2_Linux/contrib/docs/commands/login.md000066400000000000000000000037141516543156300217350ustar00rootroot00000000000000### login Logs into a MEGA account, folder link or a previous session. You can only log into one entity at a time. Usage: `login [--auth-code=XXXX] email password | exportedfolderurl#key [--auth-key=XXXX] [--resume] | passwordprotectedlink [--password=PASSWORD] | session`
Logging into a MEGA account:
	You can log into a MEGA account by providing either a session ID or a username and password. A session ID simply identifies a session that you have previously logged in with using a username and password; logging in with a session ID simply resumes that session. If this is your first time logging in, you will need to do so with a username and password.
Options:
	--auth-code=XXXXXX: If you're logging in using a username and password, and this account has multifactor authentication (MFA) enabled, then this option allows you to pass the MFA token in directly rather than being prompted for it later on. For more information on this topic, please visit https://mega.nz/blog_48.

Logging into a MEGA folder link (an exported/public folder):
	MEGA folder links have the form URL#KEY. To log into one, simply execute the login command with the link.
Options:
	--password=PASSWORD: If the link is a password protected link, then this option can be used to pass in the password for that link.
	--auth-key=AUTHKEY: If the link is a writable folder link, then this option allows you to log in with write privileges. Without this option, you will log into the link with read access only.
	--resume: A convenience option to try to resume from cache. When login into a folder, contrary to what occurs with login into a user account, MEGAcmd will not try to load anything from cache: loading everything from scratch. This option changes that. Note, login using a session string, will of course, try to load from cache. This option may be convenient, for instance, if you previously logged out using --keep-session.

For more information about MEGA folder links, see "mega-export --help".
MEGAcmd-2.5.2_Linux/contrib/docs/commands/logout.md000066400000000000000000000003131516543156300221260ustar00rootroot00000000000000### logout Logs out Usage: `logout [--keep-session]`
Options:
 --keep-session	Keeps the current session. This will also prevent the deletion of cached data associated with current session.
MEGAcmd-2.5.2_Linux/contrib/docs/commands/lpwd.md000066400000000000000000000003711516543156300215670ustar00rootroot00000000000000### lpwd Prints the current local folder for the interactive console Usage: `lpwd`
It will be used for uploads and downloads

If not using interactive console, the current local folder will be
 that of the shell executing mega comands
MEGAcmd-2.5.2_Linux/contrib/docs/commands/ls.md000066400000000000000000000040541516543156300212410ustar00rootroot00000000000000### ls Lists files in a remote path Usage: `ls [-halRr] [--show-handles] [--tree] [--versions] [remotepath] [--use-pcre] [--show-creation-time] [--time-format=FORMAT]`
remotepath can be a pattern (Perl Compatible Regular Expressions with "--use-pcre"
   or wildcarded expressions with ? or * like f*00?.txt)
 Also, constructions like /PATTERN1/PATTERN2/PATTERN3 are allowed

Options:
 -R|-r	List folders recursively
 --tree	Prints tree-like exit (implies -r)
 --show-handles	Prints files/folders handles (H:XXXXXXXX). You can address a file/folder by its handle
 -l	Print summary (--tree has no effect)
   	 SUMMARY contents:
   	   FLAGS: Indicate type/status of an element:
   	     xxxx
   	     |||+---- Sharing status: (s)hared, (i)n share or not shared(-)
   	     ||+----- if exported, whether it is (p)ermanent or (t)temporal
   	     |+------ e/- whether node is (e)xported
   	     +-------- Type(d=folder,-=file,r=root,i=inbox,b=rubbish,x=unsupported)
   	   VERS: Number of versions in a file
   	   SIZE: Size of the file in bytes:
   	   DATE: Modification date for files and creation date for folders (in UTC time):
   	   NAME: name of the node
 -h	Show human readable sizes in summary
 -a	Include extra information
   	 If this flag is repeated (e.g: -aa) more info will appear
   	 (public links, expiration dates, ...)
 --versions	show historical versions
   	You can delete all versions of a file with "deleteversions"
 --show-creation-time	show creation time instead of modification time for files
 --time-format=FORMAT	show time in available formats. Examples:
               RFC2822:  Example: Fri, 06 Apr 2018 13:05:37 +0200
               ISO6081:  Example: 2018-04-06
               ISO6081_WITH_TIME:  Example: 2018-04-06T13:05:37
               SHORT:  Example: 06Apr2018 13:05:37
               SHORT_UTC:  Example: 06Apr2018 13:05:37
               CUSTOM. e.g: --time-format="%Y %b":  Example: 2018 Apr
                 You can use any strftime compliant format: http://www.cplusplus.com/reference/ctime/strftime/
 --use-pcre	use PCRE expressions
MEGAcmd-2.5.2_Linux/contrib/docs/commands/masterkey.md000066400000000000000000000011351516543156300226240ustar00rootroot00000000000000### masterkey Shows your master key. Usage: `masterkey pathtosave`
Your data is only readable through a chain of decryption operations that begins
with your master encryption key (Recovery Key), which MEGA stores encrypted with your password.
This means that if you lose your password, your Recovery Key can no longer be decrypted,
and you can no longer decrypt your data.
Exporting the Recovery Key and keeping it in a secure location
enables you to set a new password without data loss.
Always keep physical control of your master key (e.g. on a client device, external storage, or print)
MEGAcmd-2.5.2_Linux/contrib/docs/commands/mediainfo.md000066400000000000000000000002701516543156300225520ustar00rootroot00000000000000### mediainfo Prints media info of remote files Usage: `mediainfo remotepath1 remotepath2 ...`
Options:
 --path-display-size=N	Use a fixed size of N characters for paths
MEGAcmd-2.5.2_Linux/contrib/docs/commands/mkdir.md000066400000000000000000000002031516543156300217210ustar00rootroot00000000000000### mkdir Creates a directory or a directories hierarchy Usage: `mkdir [-p] remotepath`
Options:
 -p	Allow recursive
MEGAcmd-2.5.2_Linux/contrib/docs/commands/mount.md000066400000000000000000000003011516543156300217540ustar00rootroot00000000000000### mount Lists all the root nodes Usage: `mount`
This includes the root node in your cloud drive, Inbox, Rubbish Bin
and all the in-shares (nodes shares to you from other users)
MEGAcmd-2.5.2_Linux/contrib/docs/commands/mv.md000066400000000000000000000005601516543156300212430ustar00rootroot00000000000000### mv Moves file(s)/folder(s) into a new location (all remotes) Usage: `mv srcremotepath [--use-pcre] [srcremotepath2 srcremotepath3 ..] dstremotepath`
If the location exists and is a folder, the source will be moved there
If the location doesn't exist, the source will be renamed to the destination name given
Options:
 --use-pcre	use PCRE expressions
MEGAcmd-2.5.2_Linux/contrib/docs/commands/passwd.md000066400000000000000000000006101516543156300221160ustar00rootroot00000000000000### passwd Modifies user password Usage: `passwd [-f] [--auth-code=XXXX] newpassword`
Notice that modifying the password will close all your active sessions
 in all your devices (except for the current one)

 Please, avoid using passwords containing " or '

Options:
 -f   	Force (no asking)
 --auth-code=XXXX	Two-factor Authentication code. More info: https://mega.nz/blog_48
MEGAcmd-2.5.2_Linux/contrib/docs/commands/permissions.md000066400000000000000000000016751516543156300232040ustar00rootroot00000000000000### permissions Shows/Establish default permissions for files and folders created by MEGAcmd. Usage: `permissions [(--files|--folders) [-s XXX]]`
Permissions are unix-like permissions, with 3 numbers: one for owner, one for group and one for others
Options:
 --files	To show/set files default permissions.
 --folders	To show/set folders default permissions.
 --s XXX	To set new permissions for newly created files/folder.
        	 Notice that for files minimum permissions is 600,
        	 for folders minimum permissions is 700.
        	 Further restrictions to owner are not allowed (to avoid missfunctioning).
        	 Notice that permissions of already existing files/folders will not change.
        	 Notice that permissions of already existing files/folders will not change.

Note: permissions will be saved for the next time you execute MEGAcmd server. They will be removed if you logout. Permissions are not available on Windows.
MEGAcmd-2.5.2_Linux/contrib/docs/commands/preview.md000066400000000000000000000003321516543156300222770ustar00rootroot00000000000000### preview To download/upload the preview of a file. Usage: `preview [-s] remotepath localpath`
If no -s is inidicated, it will download the preview.

Options:
 -s	Sets the preview to the specified file
MEGAcmd-2.5.2_Linux/contrib/docs/commands/proxy.md000066400000000000000000000011431516543156300220000ustar00rootroot00000000000000### proxy Show or sets proxy configuration Usage: `proxy [URL|--auto|--none] [--username=USERNAME --password=PASSWORD]`
With no parameter given, this will print proxy configuration

Options:
URL	Proxy URL (e.g: https://127.0.0.1:8080)
 --none	To disable using a proxy
 --auto	To use the proxy configured in your system
 --username=USERNAME	The username, for authenticated proxies
 --password=PASSWORD	The password, for authenticated proxies. Please, avoid using passwords containing " or '

Note: Proxy settings will be saved for the next time you open MEGAcmd, but will be removed if you logout.
MEGAcmd-2.5.2_Linux/contrib/docs/commands/psa.md000066400000000000000000000002311516543156300213770ustar00rootroot00000000000000### psa Shows the next available Public Service Announcement (PSA) Usage: `psa [--discard]`
Options:
 --discard	Discards last received PSA
MEGAcmd-2.5.2_Linux/contrib/docs/commands/put.md000066400000000000000000000013311516543156300214260ustar00rootroot00000000000000### put Uploads files/folders to a remote folder Usage: `put [-c] [-q] [--print-tag-at-start] localfile [localfile2 localfile3 ...] [dstremotepath]`
Options:
 -c	Creates remote folder destination in case of not existing.
 -q	queue upload: execute in the background. Don't wait for it to end
 --print-tag-at-start	Prints start message including transfer TAG, even when using -q.

Notice that the dstremotepath can only be omitted when only one local path is provided.
 In such case, the current remote working dir will be the destination for the upload.
 Mind that using wildcards for local paths in non-interactive mode in a supportive console (e.g. bash),
 could result in multiple paths being passed to MEGAcmd.
MEGAcmd-2.5.2_Linux/contrib/docs/commands/pwd.md000066400000000000000000000000671516543156300214150ustar00rootroot00000000000000### pwd Prints the current remote folder Usage: `pwd` MEGAcmd-2.5.2_Linux/contrib/docs/commands/quit.md000066400000000000000000000003051516543156300216000ustar00rootroot00000000000000### quit Quits MEGAcmd Usage: `quit [--only-shell]`
Notice that the session will still be active, and local caches available
The session will be resumed when the service is restarted
MEGAcmd-2.5.2_Linux/contrib/docs/commands/reload.md000066400000000000000000000001771516543156300220730ustar00rootroot00000000000000### reload Forces a reload of the remote files of the user Usage: `reload`
It will also resume synchronizations.
MEGAcmd-2.5.2_Linux/contrib/docs/commands/rm.md000066400000000000000000000003051516543156300212340ustar00rootroot00000000000000### rm Deletes a remote file/folder Usage: `rm [-r] [-f] [--use-pcre] remotepath`
Options:
 -r	Delete recursively (for folders)
 -f	Force (no asking)
 --use-pcre	use PCRE expressions
MEGAcmd-2.5.2_Linux/contrib/docs/commands/session.md000066400000000000000000000000711516543156300223010ustar00rootroot00000000000000### session Prints (secret) session ID Usage: `session` MEGAcmd-2.5.2_Linux/contrib/docs/commands/share.md000066400000000000000000000022341516543156300217230ustar00rootroot00000000000000### share Prints/Modifies the status of current shares Usage: `share [-p] [-d|-a --with=user@email.com [--level=LEVEL]] [remotepath] [--use-pcre] [--time-format=FORMAT]`
Options:
 --use-pcre	use PCRE expressions
 -p	Show pending shares too
 --with=email	Determines the email of the user to [no longer] share with
 -d	Stop sharing with the selected user
 -a	Adds a share (or modifies it if existing)
 --level=LEVEL	Level of access given to the user
              	0: Read access
              	1: Read and write
              	2: Full access
              	3: Owner access

If a remote path is given it'll be used to add/delete or in case
 of no option selected, it will display all the shares existing
 in the tree of that path

When sharing a folder with a user that is not a contact (see "mega-users --help")
  the share will be in a pending state. You can list pending shares with
 "share -p". He would need to accept your invitation (see "mega-ipc")

Sharing folders will require contact verification (see "mega-users --help-verify")

If someone has shared something with you, it will be listed as a root folder
 Use "mega-mount" to list folders shared with you
MEGAcmd-2.5.2_Linux/contrib/docs/commands/showpcr.md000066400000000000000000000014461516543156300223120ustar00rootroot00000000000000### showpcr Shows incoming and outgoing contact requests. Usage: `showpcr [--in | --out] [--time-format=FORMAT]`
Options:
 --in	Shows incoming requests
 --out	Shows outgoing invitations
 --time-format=FORMAT	show time in available formats. Examples:
               RFC2822:  Example: Fri, 06 Apr 2018 13:05:37 +0200
               ISO6081:  Example: 2018-04-06
               ISO6081_WITH_TIME:  Example: 2018-04-06T13:05:37
               SHORT:  Example: 06Apr2018 13:05:37
               SHORT_UTC:  Example: 06Apr2018 13:05:37
               CUSTOM. e.g: --time-format="%Y %b":  Example: 2018 Apr
                 You can use any strftime compliant format: http://www.cplusplus.com/reference/ctime/strftime/

Use "mega-ipc" to manage invitations received
Use "mega-users" to see contacts
MEGAcmd-2.5.2_Linux/contrib/docs/commands/signup.md000066400000000000000000000014631516543156300221310ustar00rootroot00000000000000### signup Register as user with a given email Usage: `signup email password [--name="Your Name"]`
Please, avoid using passwords containing " or '

Options:
 --name="Your Name"	Name to register. e.g. "John Smith"

 You will receive an email to confirm your account.
 Once you have received the email, please proceed to confirm the link
 included in that email with "confirm".

Warning: Due to our end-to-end encryption paradigm, you will not be able to access your data
without either your password or a backup of your Recovery Key (master key).
Exporting the master key and keeping it in a secure location enables you
to set a new password without data loss. Always keep physical control of
your master key (e.g. on a client device, external storage, or print).
 See "masterkey --help" for further info.
MEGAcmd-2.5.2_Linux/contrib/docs/commands/speedlimit.md000066400000000000000000000020141516543156300227540ustar00rootroot00000000000000### speedlimit Displays/modifies upload/download rate limits: either speed or max connections Usage: `speedlimit [-u|-d|--upload-connections|--download-connections] [-h] [NEWLIMIT]`
NEWLIMIT is the new limit to set. If no option is provided, NEWLIMIT will be 
  applied for both download/upload speed limits. 0, for speeds, means unlimited.
 NEWLIMIT may include (B)ytes, (K)ilobytes, (M)egabytes, (G)igabytes & (T)erabytes.
  Examples: "1m12k3B" "3M". If no units are given, bytes are assumed.

Options:
 -d                       Set/Read download speed limit, expressed in size per second.
 -u                       Set/Read dpload speed limit, expressed in size per second
 --upload-connections     Set/Read max number of connections for an upload transfer
 --download-connections   Set/Read max number of connections for a download transfer

Display options:
 -h                       Human readable

Notice: these limits will be saved for the next time you execute MEGAcmd server. They will be removed if you logout.
MEGAcmd-2.5.2_Linux/contrib/docs/commands/sync-config.md000066400000000000000000000011731516543156300230410ustar00rootroot00000000000000### sync-config Controls sync configuration. Usage: `sync-config [--delayed-uploads-wait-seconds | --delayed-uploads-max-attempts]`
Displays current configuration.

New uploads for files that change frequently in syncs may be delayed until a wait time passes to avoid wastes of computational resources.
 Delay times and number of changes may change overtime
Options:
 --delayed-uploads-wait-seconds   Shows the seconds to be waited before a file that's being delayed is uploaded again.
 --delayed-uploads-max-attempts   Shows the max number of times a file can change in quick succession before it starts to get delayed.
MEGAcmd-2.5.2_Linux/contrib/docs/commands/sync-ignore.md000066400000000000000000000062201516543156300230550ustar00rootroot00000000000000### sync-ignore Manages ignore filters for syncs Usage: `sync-ignore [--show|[--add|--add-exclusion|--remove|--remove-exclusion] filter1 filter2 ...] (ID|localpath|DEFAULT)`
To modify the default filters, use "DEFAULT" instead of local path or ID.
Note: when modifying the default filters, existing syncs won't be affected. Only newly created ones.

If no action is provided, filters will be shown for the selected sync.
Only the filters at the root of the selected sync will be accessed. Filters beloging to sub-folders must be modified manually.

Options:
--show	Show the existing filters of the selected sync
--add	Add the specified filters to the selected sync
--add-exclusion	Same as "--add", but the  is 'exclude'
               	Note: the `-` must be omitted from the filter (using '--' is not necessary)
--remove	Remove the specified filters from the selected sync
--remove-exclusion	Same as "--remove", but the  is 'exclude'
                  	Note: the `-` must be omitted from the filter (using '--' is not necessary)

Filters must have the following format: :
	 Must be either exclude, or include
		exclude (`-`): This filter contains files or directories that *should not* be synchronized
		               Note: you must pass a double dash ('--') to signify the end of the parameters, in order to pass exclude filters
		include (`+`): This filter contains files or directories that *should* be synchronized
	 May be one of the following: directory, file, symlink, or all
		directory (`d`): This filter applies only to directories
		file (`f`): This filter applies only to files
		symlink (`s`): This filter applies only to symbolic links
		all (`a`): This filter applies to all of the above
	Default (when omitted) is `a`
	 May be one of the following: local name, path, or subtree name
		local name (`N`): This filter has an effect only in the root directory of the sync
		path (`p`): This filter matches against the path relative to the rooth directory of the sync
		            Note: the path separator is always '/', even on Windows
		subtree name (`n`): This filter has an effect in all directories below the root directory of the sync, itself included
	Default (when omitted) is `n`
	 May be one of the following: glob, or regexp
		glob (`G` or `g`): This filter matches against a name or path using a wildcard pattern
		regexp (`R` or `r`): This filter matches against a name or path using a pattern expressed as a POSIX-Extended Regular Expression
	Note: uppercase `G` or `R` specifies that the matching should be case-sensitive
	Default (when omitted) is `G`
	 Must be a file or directory pattern
Some examples:
	`-f:*.txt`  Filter will exclude all *.txt files in and beneath the sync directory
	`+fg:work*.txt`  Filter will include all work*.txt files excluded by the filter above
	`-N:*.avi`  Filter will exclude all *.avi files contained directly in the sync directory
	`-nr:.*foo.*`  Filter will exclude files whose name contains 'foo'
	`-d:private`  Filter will exclude all directories with the name 'private'

See: https://help.mega.io/installs-apps/desktop/megaignore more info.
MEGAcmd-2.5.2_Linux/contrib/docs/commands/sync-issues.md000066400000000000000000000060241516543156300231070ustar00rootroot00000000000000### sync-issues Show all issues with current syncs Usage: `sync-issues [[--detail (ID|--all)] [--limit=rowcount] [--disable-path-collapse]] | [--enable-warning|--disable-warning]`
When MEGAcmd detects conflicts with the data it's synchronizing, a sync issue is triggered. Syncing is stopped on the conflicting data, and no progress is made. Recovering from an issue usually requires user intervention.
A notification warning will appear whenever sync issues are detected. You can disable the warning if you wish. Note: the notification may appear even if there were already issues before.
Note: the list of sync issues provides a snapshot of the issues detected at the moment of requesting it. Thus, it might not contain the latest updated data. Some issues might still be being processed by the sync engine, and some might not have been removed yet.

Options:
 --detail (ID | --all) 	Provides additional information on a particular sync issue.
                       	The ID of the sync where this issue appeared is shown, alongside its local and cloud paths.
                       	All paths involved in the issue are shown. For each path, the following columns are displayed:
                       		PATH: The conflicting local or cloud path (cloud paths are prefixed with "").
                       		PATH_ISSUE: A brief explanation of the problem this file or folder has (if any).
                       		LAST_MODIFIED: The most recent date when this file or directory was updated.
                       		UPLOADED: For cloud paths, the date of upload or creation. Empty for local paths.
                       		SIZE: The size of the file. Empty for directories.
                       		TYPE: The type of the path (file or directory). This column is hidden if the information is not relevant for the particular sync issue.
                       	The "--all" argument can be used to show the details of all issues.
 --limit=rowcount 	Limits the amount of rows displayed. Set to 0 to display unlimited rows. Default is 10. Can also be combined with "--detail".
 --disable-path-collapse 	Ensures all paths are fully shown. By default long paths are truncated for readability.
 --enable-warning 	Enables the notification that appears when issues are detected. This setting is saved for the next time you open MEGAcmd, but will be removed if you logout.
 --disable-warning 	Disables the notification that appears when issues are detected. This setting is saved for the next time you open MEGAcmd, but will be removed if you logout.
 --col-separator=X	Uses the string "X" as column separator. Otherwise, spaces will be added between columns to align them.
 --output-cols=COLUMN_NAME_1,COLUMN_NAME2,...	Selects which columns to show and their order.

DISPLAYED columns:
	ISSUE_ID: A unique identifier of the sync issue. The ID can be used alongside the "--detail" argument.
	PARENT_SYNC: The identifier of the sync that has this issue.
	REASON: A brief explanation on what the issue is. Use the "--detail" argument to get extended information on a particular sync.
MEGAcmd-2.5.2_Linux/contrib/docs/commands/sync.md000066400000000000000000000041451516543156300216000ustar00rootroot00000000000000### sync Controls synchronizations. Usage: `sync [localpath dstremotepath| [-dpe] [ID|localpath]`
If no argument is provided, it lists current configured synchronizations.
If local and remote paths are provided, it will start synchronizing a local folder into a remote folder.
If an ID/local path is provided, it will list such synchronization unless an option is specified.

Note: use the "sync-config" command to show global sync configuration.

Options:
 -d | --delete ID|localpath	deletes a synchronization (not the files).
 -p | --pause ID|localpath	pauses (disables) a synchronization.
 -e | --enable ID|localpath	resumes a synchronization.
 [deprecated] --remove ID|localpath	same as --delete.
 [deprecated] -s | --disable ID|localpath	same as --pause.
 [deprecated] -r ID|localpath	same as --enable.
 --path-display-size=N	Use at least N characters for displaying paths.
 --show-handles	Prints remote nodes handles (H:XXXXXXXX).
 --col-separator=X	Uses the string "X" as column separator. Otherwise, spaces will be added between columns to align them.
 --output-cols=COLUMN_NAME_1,COLUMN_NAME2,...	Selects which columns to show and their order.

DISPLAYED columns:
 ID: an unique identifier of the sync.
 LOCALPATH: local synced path.
 REMOTEPATH: remote synced path (in MEGA).
 RUN_STATE: Indication of running state, possible values:
 	Pending: Sync config has loaded but we have not attempted to start it yet.
 	Loading: Sync is in the process of loading from disk.
 	Running: Sync loaded and active.
 	Suspended: Sync is not loaded, but it is on disk with the last known sync state.
 	Disabled: Sync has been disabled (no state cached). Starting it is like configuring a brand new sync with those settings.
 STATUS: State of the sync, possible values:
 	NONE: Status unknown.
 	Synced: Synced, no transfers/pending actions are ongoing.
 	Pending: Sync engine is doing some calculations.
 	Syncing: Transfers/pending actions are being carried out.
 	Processing: State cannot be determined (too busy, try again later).
 ERROR: Error, if any.
 SIZE, FILE & DIRS: size, number of files and number of dirs in the remote folder.
MEGAcmd-2.5.2_Linux/contrib/docs/commands/thumbnail.md000066400000000000000000000003441516543156300226040ustar00rootroot00000000000000### thumbnail To download/upload the thumbnail of a file. Usage: `thumbnail [-s] remotepath localpath`
If no -s is inidicated, it will download the thumbnail.

Options:
 -s	Sets the thumbnail to the specified file
MEGAcmd-2.5.2_Linux/contrib/docs/commands/transfers.md000066400000000000000000000023661516543156300226360ustar00rootroot00000000000000### transfers List or operate with transfers Usage: `transfers [-c TAG|-a] | [-r TAG|-a] | [-p TAG|-a] [--only-downloads | --only-uploads] [SHOWOPTIONS]`
If executed without option it will list the first 10 transfers
Options:
 -c (TAG|-a)	Cancel transfer with TAG (or all with -a)
 -p (TAG|-a)	Pause transfer with TAG (or all with -a)
 -r (TAG|-a)	Resume transfer with TAG (or all with -a)
 --only-uploads	Show/Operate only upload transfers
 --only-downloads	Show/Operate only download transfers

Show options:
 --summary	Prints summary of on going transfers
 --show-syncs	Show synchronization transfers
 --show-completed	Show completed transfers
 --only-completed	Show only completed download
 --limit=N	Show only first N transfers
 --path-display-size=N	Use at least N characters for displaying paths
 --col-separator=X	Uses the string "X" as column separator. Otherwise, spaces will be added between columns to align them.
 --output-cols=COLUMN_NAME_1,COLUMN_NAME2,...	Selects which columns to show and their order.

TYPE legend correspondence:
  ⇓ = 	Download transfer
  ⇑ = 	Upload transfer
  ⇵ = 	Sync transfer. The transfer is done in the context of a synchronization
  ⏫ = 	Backup transfer. The transfer is done in the context of a backup
MEGAcmd-2.5.2_Linux/contrib/docs/commands/tree.md000066400000000000000000000002201516543156300215510ustar00rootroot00000000000000### tree Lists files in a remote path in a nested tree decorated output Usage: `tree [remotepath]`
This is similar to "ls --tree"
MEGAcmd-2.5.2_Linux/contrib/docs/commands/unicode.md000066400000000000000000000005161516543156300222500ustar00rootroot00000000000000### unicode Toggle unicode input enabled/disabled in interactive shell Usage: `unicode`
Unicode mode is experimental, you might experience
 some issues interacting with the console
 (e.g. history navigation fails).

Type "help --unicode" for further info
Note: this command is only available on some versions of Windows
MEGAcmd-2.5.2_Linux/contrib/docs/commands/update.md000066400000000000000000000011361516543156300221030ustar00rootroot00000000000000### update Updates MEGAcmd Usage: `update [--auto=on|off|query]`
Looks for updates and applies if available.
This command can also be used to enable/disable automatic updates.
Options:
 --auto=ON|OFF|query	Enables/disables/queries status of auto updates.

If auto updates are enabled it will be checked while MEGAcmd server is running.
 If there is an update available, it will be downloaded and applied.
 This will cause MEGAcmd to be restarted whenever the updates are applied.

Further info at https://github.com/meganz/megacmd#megacmd-updates
Note: this command is not available on Linux
MEGAcmd-2.5.2_Linux/contrib/docs/commands/userattr.md000066400000000000000000000004131516543156300224670ustar00rootroot00000000000000### userattr Lists/updates user attributes Usage: `userattr [-s attribute value|attribute|--list] [--user=user@email]`
Options:
 -s	attribute value 	sets an attribute to a value
 --user=user@email	select the user to query
 --list	lists valid attributes
MEGAcmd-2.5.2_Linux/contrib/docs/commands/users.md000066400000000000000000000031731516543156300217650ustar00rootroot00000000000000### users List contacts Usage: `users [-s] [-h] [-n] [-d contact@email] [--time-format=FORMAT] [--verify|--unverify contact@email.com] [--help-verify [contact@email.com]]`
Options:
 -d	contact@email   	Deletes the specified contact.
--help-verify              	Prints general information regarding contact verification.
--help-verify contact@email	This will show credentials of both own user and contact
                           	 and instructions in order to proceed with the verifcation.
--verify contact@email     	Verifies contact@email.
                           	 CAVEAT: First you would need to manually ensure credentials match!
--unverify contact@email   	Sets contact@email as no longer verified. New shares with that user
                           	 will require verification.
Listing Options:
 -s	Show shared folders with listed contacts
 -h	Show all contacts (hidden, blocked, ...)
 -n	Show users names
 --time-format=FORMAT	show time in available formats. Examples:
               RFC2822:  Example: Fri, 06 Apr 2018 13:05:37 +0200
               ISO6081:  Example: 2018-04-06
               ISO6081_WITH_TIME:  Example: 2018-04-06T13:05:37
               SHORT:  Example: 06Apr2018 13:05:37
               SHORT_UTC:  Example: 06Apr2018 13:05:37
               CUSTOM. e.g: --time-format="%Y %b":  Example: 2018 Apr
                 You can use any strftime compliant format: http://www.cplusplus.com/reference/ctime/strftime/

Use "mega-invite" to send/remove invitations to other users
Use "mega-showpcr" to browse incoming/outgoing invitations
Use "mega-ipc" to manage invitations received
Use "mega-users" to see contacts
MEGAcmd-2.5.2_Linux/contrib/docs/commands/version.md000066400000000000000000000003201516543156300223000ustar00rootroot00000000000000### version Prints MEGAcmd versioning and extra info Usage: `version [-l][-c]`
Options:
 -c	Shows changelog for the current version
 -l	Show extended info: MEGA SDK version and features enabled
MEGAcmd-2.5.2_Linux/contrib/docs/commands/webdav.md000066400000000000000000000025151516543156300220730ustar00rootroot00000000000000### webdav Configures a WEBDAV server to serve a location in MEGA Usage: `webdav [-d (--all | remotepath ) ] [ remotepath [--port=PORT] [--public] [--tls --certificate=/path/to/certificate.pem --key=/path/to/certificate.key]] [--use-pcre]`
This can also be used for streaming files. The server will be running as long as MEGAcmd Server is.
If no argument is given, it will list the webdav enabled locations.

Options:
 --d        	Stops serving that location
 --all      	When used with -d, stops serving all locations (and stops the server)
 --public   	*Allow access from outside localhost
 --port=PORT	*Port to serve. DEFAULT= 4443
 --tls      	*Serve with TLS (HTTPS)
 --certificate=/path/to/certificate.pem	*Path to PEM formatted certificate
 --key=/path/to/certificate.key	*Path to PEM formatted key
 --use-pcre	use PCRE expressions

*If you serve more than one location, these parameters will be ignored and use those of the first location served.
 If you want to change those parameters, you need to stop serving all locations and configure them again.
Note: WEBDAV settings and locations will be saved for the next time you open MEGAcmd, but will be removed if you logout.

Caveat: This functionality is in BETA state. It might not be available on all platforms. If you experience any issue with this, please contact: support@mega.nz
MEGAcmd-2.5.2_Linux/contrib/docs/commands/whoami.md000066400000000000000000000003351516543156300221050ustar00rootroot00000000000000### whoami Prints info of the user Usage: `whoami [-l]`
Options:
 -l	Show extended info: total storage used, storage per main folder
   	(see mount), pro level, account balance, and also the active sessions
MEGAcmd-2.5.2_Linux/contrib/docs/generate_command_docs.py000066400000000000000000000146531516543156300233600ustar00rootroot00000000000000#!/usr/bin/env python3 import re import subprocess import os MEGA_EXEC_PATH = os.environ.get('MEGA_EXEC_PATH', 'mega-exec') def get_help_output(detail): det = '-ff' if detail else '-f' return subprocess.run([MEGA_EXEC_PATH, 'help', det, '--show-all-options'], capture_output=True, text=True).stdout def get_version(): out = subprocess.run([MEGA_EXEC_PATH, 'version'], capture_output=True, text=True).stdout return re.search(r'MEGAcmd version: (\d+\.\d+\.\d+)', out).group(1) class CommandSummary: def __init__(self, name, args, description): self.name = name self.args = args self.description = description def get_markdown_format(self): args = f'`{self.args}`' if self.args else '' return f'* [`{self.name}`](contrib/docs/commands/{self.name}.md){args} {self.description}' class CommandDetail: def __init__(self, name, summary, usage, description): self.name = name self.summary = summary self.usage = usage self.description = description def get_markdown_format(self): description = f'\n
\n{self.description}\n
' if self.description else '' return f'### {self.name}\n{self.summary}\n\nUsage: `{self.usage}`{description}' class CommandTable: command_summaries = {} command_details = {} def add_command_summary(self, command_line): command_line = command_line.split(':', maxsplit=1) command_name = command_line[0] args = '' description = command_line[1] if ' ' in command_name: command_line = command_name.split(' ', maxsplit=1) command_name = command_line[0] args = command_line[1] command_name = command_name.strip() # Skip debug commands if command_name == 'echo': return self.command_summaries[command_name] = CommandSummary( name=command_name, args=args.strip(), description=description.strip() ) def add_command_detail(self, command_detail): command_lines = command_detail.split('\n', maxsplit=3) # Does not start with ; not a command if not re.match(r'<[\w-]+>', command_lines[0]): return command_name = re.search(r'<([\w-]+)>', command_lines[0]).group(1) # Skip debug commands if command_name == 'echo': return usage = re.search(r'Usage: (.+)', command_lines[1]).group(1) summary = command_lines[2] description = command_lines[3] if len(command_lines) > 3 else '' command_name = command_name.strip() self.command_details[command_name] = CommandDetail( name=command_name, summary=summary.strip(), usage=usage.strip(), description=description.strip() ) def parse_commands_brief(self): output = get_help_output(detail=False) command_section = re.split(r'Commands:\n', output, 1)[-1] command_lines = command_section.strip().split('\n') for line in command_lines: line = line.strip() # Invalid format; not a command if not line or not line.split(' '): continue # Doesn't start with lowercase letter; not a command if not re.match(r'[a-z]', line[0]): continue self.add_command_summary(line) def parse_commands_detail(self): output = get_help_output(detail=True) min_cols = 10 commands = re.split(fr'-{{{min_cols},}}\n', output.strip()) for command_detail in commands: command_detail = command_detail.strip() # Invalid format; not a command if not command_detail: continue self.add_command_detail(command_detail) def get_commands_by_category(ct): categories = { 'Account / Contacts': ['signup', 'confirm', 'invite', 'showpcr', 'ipc', 'users', 'userattr', 'passwd', 'masterkey'], 'Login / Logout': ['login', 'logout', 'whoami', 'session', 'killsession'], 'Browse': ['cd', 'lcd', 'ls', 'pwd', 'lpwd', 'attr', 'du', 'find', 'mount'], 'Moving / Copying files': ['mkdir', 'cp', 'put', 'get', 'preview', 'thumbnail', 'mv', 'rm', 'transfers', 'speedlimit', 'sync', 'sync-issues', 'sync-ignore', 'sync-config', 'exclude', 'backup'], 'Sharing (your own files, of course, without infringing any copyright)': ['export', 'import', 'share', 'webdav'], 'FUSE (mount your cloud folder to the local system)': ['fuse-add', 'fuse-remove', 'fuse-enable', 'fuse-disable', 'fuse-show', 'fuse-config'], } # Commands that don't have a explicit group should go into Misc. commands = set([c for l in categories.values() for c in l]) misc_commands = set(ct.command_summaries.keys()) - commands categories['Misc.'] = sorted(misc_commands) return categories def generate_user_guide_summary(ct): categories = get_commands_by_category(ct) summary = [] for category, commands in categories.items(): summary.append('### ' + category) for command in commands: cs = ct.command_summaries[command] summary.append(cs.get_markdown_format()) summary.append('') return '\n'.join(summary) + '\n' def write_to_user_guide(summary): with open('UserGuide.md', 'r') as f: contents = f.read() # Write MEGAcmd version version_sentence = 'This document relates to MEGAcmd version ' contents = re.sub(version_sentence + r'\d+\.\d+\.\d+', version_sentence + get_version(), contents) # Write summary of all commands anchor = '<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>' contents = contents.replace('### Account / Contacts\n', anchor) contents = contents.replace('## Examples', anchor + '## Examples') contents = contents.split(anchor) contents = contents[0] + summary + contents[2] with open('UserGuide.md', 'w') as f: f.write(contents) def write_command_files(command_details): for cd in command_details: with open(f'contrib/docs/commands/{cd.name}.md', 'w') as f: f.write(cd.get_markdown_format() + '\n') if __name__=='__main__': ct = CommandTable() ct.parse_commands_brief() ct.parse_commands_detail() user_guide_summary = generate_user_guide_summary(ct) write_to_user_guide(user_guide_summary) write_command_files(ct.command_details.values()) MEGAcmd-2.5.2_Linux/contrib/docs/pics/000077500000000000000000000000001516543156300174335ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/contrib/docs/pics/ftpConnectToServerLinux.png000066400000000000000000000475621516543156300247740ustar00rootroot00000000000000PNG  IHDRq(*BHsBIT|d pHYs+tEXtSoftwareShutterc IDATxw|^ԫ-r/r n):1O ?z5iP ظqﲊ-U_'ْN:;|߯׾ss3[U M0Y6 B!0ML9qOj*J~~Yu7xV MNt}B$] tRS3$$$l۶|稩@UUA0Nt]B$UUXT !1]w#ڸ޲y1MDS!2M]74:l6ZY{ B  XJKJ0MCzB!D'Ayy)߇DKMӤ# bbXqډu:P5 ˲B0 41#㥶~LI8|AvWEA"&aY55 ח}znS]]MAa!$DPBqi(waiCc 4AQ@P,V AYzEr-De): 6JMDQYRXKT[֏n~Bya9`pv \`5 u[ch٫x|uf`~ғi(ϖ`*ʊnXn1UH%= 0` $tK[Վ݆䍔_RW]E7lv+iq1.ʻ0 Ι6IEEԗl~ mY(kܲDE9\VyE3fLnHix^,YFZ"6k6O.,r}g, ~Iu](|r=,Ub)CŬgָF%euQ=C05p3yd=^<^wAqv]#2t:գGv;~QK|rӧOCuzdu"74;8{)ٌZ.Ctz/Z FBr|,AE0mTWhh$j$TXi.ErcA_1Vvo e^MrUAy vݜn:hsfdV:ivFôQ][M%4G 5?n{JPPw9iP]*;+3!cG!:4Ԕ5R`OCkw@,l D^ I``lp0)6VT@- Kg=~G荽` [7tj2R]uCo1ְ7ywU>%nI/'UU~zO WΞd+F~-7og3Jv]w[ ~}[\ qWl/R=zm1k$6+U~zW}C3`)^5D|쏜 Po]=)fbքxJjaYpv:Sܬ~A~ݖD=ڜMKmt&$pmek9c fF jT lfW-9h5 cG!:` m bx4;3 .'f\i NĀ`!C0.; t|RUUU2u(+FLs&&.y~~ƨܸ Tu];qYsC/CW:?Y6{l7ι{xjt#<^ZAOg:u&dEG84p樣.`4{¨:`bb:>j[`M߯Ñ"H<[HIe5:* v jm| I2`l.?ϬHfv= n]IF˻RG-8~: 92aYg1s]b]bGnZ\mnZ % @=[֕,6 ~zf:d_ⱅ%Ďg8?afzn6--8eǐ%!`J6(B?Hiȴ7qè^,غ^uFĶXU=(^}ShGz eEؼÜ `LԞV#*{j 7/ړɍOaXX(mUp@R\)* h?46,]p#/sNUb3Hwd ;yB2cWm(ͣ+тVGdZ4gaǹQ̚ySODzd̙6_DD+^k5[lOAz {# wh)#3MjJ~}6iF y,;>>y"d@LSTw45M+۔( uuhֹaC8$"gT:5j 0ͮ,*4z +V4l5u@UCRc_Q?XAݡ2 C29G2 -дv[yCmݦAl6Ye-メȄkiIt?|trV%1v6.J g83=~.%_ز1% !yV ˫01sĞiO<2ŘЊ5ʶZ-י"_CüQZZ֢H([Yۤo9ru*23CY)f'la/cIÝ0>mE Y#,~FӛXj6vRJ%;8hz+9wZKK]M}ӧW,Żahz'X#q [a4nsԨ;<2;) TIಫ5uk<V͟~8%*XiΘX([!4lV)Q譙`|.DolV;y1 Bʪ WDBuUi)vLSN[ơaUU59[MiPYs1̞oes^[7?ͼSɹqI$:QaJGy^[)n^(H@``[آ2LZ,=3xӊj!w*v7}X,/jngV[n lswЎ|?,EkR ~'&Mk/~Tpڹ 䠎R.-NjaMn7Vj %v/zC-fЏi ϖZ;l@˲vtݠ2@BQeuhP?Q\sdS WPIt5y=P%lTgb zP8M1}k*|@(ǫ*&q[^y5AbUTi& 1&_~o.cq*o1M66o3n-N;~s'cbnJLJ|r!fWnUie[`g]`$eEYߢ8g}nI1(~6PJ5Vv-lYaңGvf۷U#lYV|7zg) 6Brf0-4^wIXJ\N8 4TquǧOAo|;L,+=~tߑ<Jz7xjkJVz;vͫx~7)$:mUQmlϖuv.#BfGfE[Bka̘q0ZD^)uJ*NvVK u|>?N'811TU0t/unlvke !'a(ӧM(:zPo|Qhh EseYB!ĉybm tᜨB4;5F!`*BDIB`g !iR[S-T!B!" B!D$ !Q`*BDIB% B!D$ !Q`*BDIB% B!D$ !Q`*BDIB%ˉ-%okEe CgL"ֵ V4R]kiӋ:e+ R0B!DNL}YQd/ওRسcq(M}7|j-cf^sGle|[,\’:-mS7yu䭼qw[ѝ's 71֕s݃drrM_!|P8ce׶|o-=f=|<5SUTyګ[B}BxEl8ЀWPλVfg)|&Bqs2۽Uq⬩i< jѕ}IÎ?8gԠj?_򩜝1K~UہZZy‰]zѻ<ԻHrTɽ1L !?ޛS& u'Zse3J,,ǨK'?=)^1fpf{u5&\08uM gB4&5*#fxAO_ ^:3f8iV lZ(]kZXma73r[AqYW/m>lxEU8]X_Zo3̹Wwx!U7i شiS W |B05Q-Ь6y~Yw>CM!g2vSzwd2-wkVo2 KWXY^? aprhsPf=5撛{&K3|=5߼չP_xAXݰ=ݯm?gدVO[ۮoO{˽ϨU;\ċ̍|3ۼ|qn )\ӟ}7{/!dkF%_ps/:3f0cL}y~|+y\ȗj6tt _L^fm]s]¬ɧ0_rɘy,d!epj{'iZ?»wl1.r'z?|%Y5 'ԃ~c\HzpUeͷq! }&72J?~q[;.rJyWT'ӎ,.}Z*?g<Š=VE6q0Jpl[:o̻}4CVR<-n2zIi:Bǻ(('8tX0}*wCZlG/^d1.MlV8uqj;lY&K㟖#tԞW{Y4 o5^$$Q_d[fC-l\pͬ9=RJGqKmtgfv˃-bk/3tOy/h^Ų|-ߔ ɶVcawp~XHz{>i*;?ŗnCXysW9:{MsOz7=D/sIK4}E IDATp஻瞣oPϠᄏEN{ߩi#-.>}ҷo_-gczr{N9i<&=ϭ9  x s1=456X[9R5$LDhC/J'~y}-?5*3ટ1!jqd;Z.1~cհ$t#zJGkQZq&,u6diZ]wˌә>2^ %3{k5'\-[|tIBSA?7 \W^zKNj5Y$Y_6(((o;D,]*VbltR6kA0zѓ %+>">s$4K qo};-nգЃkX°CqS%} yx,pk=d^;s<믽kM˫`p'|z6u+PAϹ4a>/m%+Q8ۙXS]qx3OWնqܘK|53,rbd?g/}8ef.9w8j~H6m]wTVVru1mڴTf0f0/‚/aW hk_/o ]r}l٩]4^! dG8Ɵèåw(g>r7Evk.v+?& װLǯUCA?ɓo-囕o/}6W\&ғTG(gn[G?M?dYBmoꯄ/=skd]ʽW MVKX /c\Л&'Ρy,mrtJ&Yu'vo ]vY]WiMać< u''rwr#/;1x^(5e?R?j@<7_UqD|G= ޲zJWW|or/l}iq@ʼ>NТGOQd_$O~ {;=R'*f U}Zo?.I; O%@G_@Q[Ey}w2>c-d̺?.+y%9V[?G_0~xZ5NXp.w57E+xC̦K]Wjk;5rcX|i2jJ˛x>8W/s<Q8dvI׮%X ױ5uT.]_V!41*Pk$_z`^4̫b0.=0j=|iyM(\8dv,tN7o t~6֎B4g׋:]ZKmP1DiJik5B$0!6pĒ1&} W!aۘ6ҹxa^}a(tin߈~KB!DЏxۿ%n4.DU!.1~Q㓺Ҙ󚦁={(uа 4>=$B!N83LL]`]`+1wRyV+ Ö [j&$EMUE,j&B7:#DMS'XW>,}pB u)*bp[P >ߋ`k!!({I-.-nQ.@^֥`DUUTUEQ3dW!wQ4CýZ3r! 6!{DB% B!D$ !Q`*BDIB% B!D$k]ٹr"ѠG..'BN }T%8A'fNe++ ^ppZ"`8Q?ݎ.'BNB0Qn5k㇦ s"H,Jt.qc$;U 3R!X{1<4 U/$pj#>|T馶-m g Qz0ڛ>jlh~͙Nfdw >vC |ABl"顪`{͙IAcN!Po7N|e d#e8ҿZM Κrw4՝4W?K`8MuI#)% tb̓m՛ %쪨$/uII ;)- 7{w!mL;03&Ll@R<]2^=%cvc8.[wO(T:Z El\‘}8qGpS~I ,p=5zL%g`j(]W(vPwtz0u$gXa@Jr + iͮ*aG9 A[ O% Š-?fFߡ=8ژAݬK=/5 @NjX, P,MT$e2zNԈh-~ =SS'g{~';vv⠲O\6$6KoFp؎.yD=ÝgBBf\z0bq%$fj193ĜiKl~+(Vg@CQC} ~[7>ACR>*.gO{_h-~DUd[X6EҎ0x&$oNv?QCh+ufc##{đ-xå6t2Oh3DBCՙJكL5eQ@Itx8b`bf2(M[ p&~HNL$6>8ܟa+G*1Kq;:XIG߱3v@<;8;w&Jkpӊz5nqѕczDs$lFq6X3¯aI-4V<_H`[&zƲiJCfz5{f)Rs*QcPkv1)8YnzvU줠$~p<ӊ TœA;)vZӳ&qhx񁚄UC&VK큭쩶2oh=*נ#ڬ3Lz8jyߏ8 h3[7zup%ɌΧ:} H\0ܲ0Qc6;9+ 2̶lcIÞu=ڿ!;s z &'K^F(b3fv[{u["iЯgLX%(ߒG~~wRFeӎ,P ,1VWQYG/>Kl&=G%JdMLNQn,LzIvF۷C (40e^$EWqҥb#ij).\X](tr@Eʨxg57y,cQfXfå] noNHtxc5Cg3DFոqQzlZIc`׿O %ei1Fw{)I0{ .!|A ,I;x = üB!D]B!D$ !Q`*BDIB% B!D$ !Q`*BDIB% B!D$ !Q`*BDIB% B!D$ !Q`*BDIB% B!D$ !Q`*BDIBM05u-c!tz /=[Rm L^s!Q;ƵYGo|f"ͯWohm^@%N=rwyB!NLM{ ,| :js?#:yX|h F֒~<]y!|ϴP'ZjW}fKm!ZGŘ2$7}@YJzAqJFe^eѶbJνk.CjVkY?3I/gE9 |NJEbώ}8ǡ4e^2z-IaIA6)~`.%ǧ9\~ۆ;xk7{LՙH=';ۿfj:O?%%6dr:Y3+]pz Q]yjم|6?(r8sm`蘪S!83nB{!+=b.cwz @̜RCUq"(#':)_BC͂i8Fq^:e5-U)% 3cޕCкR/@ZђF;X#o{=VSc,gp&w@~egcB|\vw/M9s>$Q !q5.w>Ϫz3x LS]$QjfLPVAQZQ(Xlm  5D$ 'sa\pm)_8]z.իz 9M|u`mn>B%ؿemYŶPxzV0~6 ]蟼d"-#c ŏ9@wSa$g ߲GK>1ޏ刨  jٳ ,~M pBX,Z,)ɍ7BĊxk4У);XOѓ)WΔKXĝeW716:B!c0S%q\2p2{?fƥO>/1= V-H@Ka9h铸xzE y gbUgO$eDRE{O&nkWozn~.ƻZv?rOfXEW/9PB3 EECSJ"iR]&J<;Z!DfOKgʕMz (qQtS@( ؓc28F_?;rm`6S-fˈI9gx:vbIk]i):~4W&9S"r,e׬0I7E(6L'=2b1(/&`㼞X#VB!:M=bA!>9)B!D$ !Q`*BDIB% B!D$ !Q`*BDIBN?e.ovJ/B@8S;LUV0yjngWB!V,]tisV@@EXkקny6*;xcTWU:͟?_>T_WW PzzcN-YDLFtZQCs"iZj zR)-ZH6m٭jjjTM)ŋy޸ :&1FA#TWW&)ܵ[V*R[[ɤ iAEH+@6U:Vww,X: y)CVjhhD*QSSo>49jnnР-Z;pZ38V"7P*U޲Kl6+eH) 2R&$a$w |?P&U2sQ/a b0T"M{^/$df(nΥE=OQ);%ۊJ@k2<ߗ {_u+K+1  e2I3(̅"ǷfA^nZ({y:|E l`=ό\0341ӣcٴvkmZV[R~5(WG[rK/a "7~fGީ?|D?xEmUYs։^?V~)Lɮ:+L>kZZsWgݩefWnS !NY3ܬGRms?Pݫo7J9؍vk.u*ЯiI@e1/'i7ؖF]]l'ϑUe-k砕->tܤ^=z-ڱ'-+)zs^m|!z|Tܬ/,?kI[s_ntǚf݅Rz0N15Rԯ> }ξj]ɹJWbk_x575sOw]ڦJ@Edkf}fe(zEf.>SWjiߛOl];>ۡ ܯmnԮڰGxPC .i $_ͫt/ov+:b<;^obk]LM_m)a%sj)WhAna 5`_*X >ằCWܹ-n-Ua Cq̈́ɨZ'zm{TTFG{QO1զZic~Qv=\mS6׵5cs s_Y[=2);k|MQnh:qkzw3&WL'`u33f#@E|W}z횝^ڸM/qef(s?Z7ogiFxw4/V?Mܦߚ~<,o nl͙3j r͊Uz*LmM%ZԦ_GدMK-Gk^k񵟸ƚ@4?:;@EƳ)-Z]Zztٚջm6}LWzZFO=_ja n(F0=c.;n^{e:9J/u>q28E+S+tZ~Ij5jnxNӮS:_IZFPk>ӡK\sLikW|}UG`e׵ouӤ'vݣzj:}yS FM|Sr0]wǭZ1׷H2n^N[b>?gl-<^o)0./^uU'WiבWݢխO={HJyZetUuZWL˿|&TQSt'ph~]+Sժm߷j^xxLu-Z7 u7|vYSN^֨"z`Pa֛=}w2i C셟O hNN>qX' 3ϪYN:RVVF^|嫵¬h^׵ڏouc
R=/2nG*B*xק]:c`}4F ז-oNdEF-quyEzvjy,1*Z0 o> ۯt:A)kxÞomW[{֌3Elĉ~_kO j Q15(}y_P uh̙qr)CQ4[M {!̟3-I6~>@cFwPr43޸$@ǫT= a #G)S8"LpD0a #G]zѠΉIENDB`MEGAcmd-2.5.2_Linux/contrib/docs/pics/ftpConnectToServerMac.png000066400000000000000000001235671516543156300243750ustar00rootroot00000000000000PNG  IHDR^ZL1sBIT|d pHYs%%IR$ IDATxwx̶lI/^ R+ޯrŎ"((X *"PfIl6 BQy̙93d=eHU 5;uYr$I$I $N*WB`T$I$IX 6k[}, `e]c,k]}76$I$I<+l 4Xpw2/J$It4f{}uJ-ey]뎥\CU$I4vΆ1օ߻eP5.G]˃tFc6m:t4L2A7(B*~ =w}cm#I$It"(mj[*ju]7 (%.غuk)_XZu 2}I&]ަeBl!񪪚E9@OFI$IFVXWB!tw;ȑ#k~w5@1hǚ _HBy3/Gw @w8bWI$I$)zh_yL]5kּ6eπRjBcPKB0s==or&'OI$IhXLUUtt535 z`6W 0RZj$nZNC$I$I:DPTK˄ymVP*u<0zFlP۵kK~3ҋ&hcO}|?]RRph:} CGGM;#k6. ~"3IXDFEP3`ԙ2*IҩQ9fD͆+πVV b95*}AbZvGIZzDnB~$$hYYVF#~\i_wx5/iUA|8Bb r $kcx;bƎQwu:ecmb*: U?ق$֮@>.Nc *32vfolFտpя,r.Z_f]$Iq lO6AƀC`^V^EEF_ZFc`Bݍ/Q~Y 63!Y+(.bߍj7kTUB~a>q1qGN^@Kng`.o2\F_aO?0nx",Fz(vaXiޢa6-+.`J )fJ]nB#ci׮1n7%9J 4)ZiQcv;lǪkd %en0ZiCB|,$JDt["62 !4Sc^$aC!. q*F/v")%CvyB # 7+SPiЊV-pԜ-J%9c.gس04z<2~XhQQ(+-:BT\sڶiEdCJJ ^HY5ZlY=2M+Qeߞ853ڷb2rq>B۔mZf/]{9K[M؟BqL%I+-mD|}3 $%Thbyy-jMANja?"d!*Q) Ź[͚JKJڬ]r*+ʄPTT$N{%هX/&1'LTi*-;i.нn֭ZʜO?#5r?hNca2d;JI:Yܣ{Vi1YΠ~r + y_{#8WYolB.f8Xi+Nص77z+-,][ / Gwf*F3ؓ;[aHߵ?mL!a0o9x0AёJ\D:BPC'\H11(<>l͝wA݄Ǚ$-0Á#y>,$6qwt-a!<30"Zxg|TYk(qWanۉ ,Yp.& f+ͣoFIN;̀֕ Q,6 >+ RF`p"7zsAQ e Jو^rT* @SxJ!7'f?ZFsgMU'oaRho``Yӷi֢DQ}{=\}w!z[6$.Š^R&s)Lٟ4>yo&kW^9[7g (m6a:c׆U|B -u>w3톩 :~%?,]Ͽ.c 󯺅svU`g F&\z5C{'<U[hk$7\zV7sfU \TMGurdamՑ6 bݤɡs8ٽa-E.â_1/$aއj f&`,#/+K]G89I4- ڂgMsAaM.W!4 /5n%^)^gIuTUm VQtt(϶>Π t7&b%q3Q&qq\YZK3+)7XHhӍiO"> qx#2īwF 6C q15>tZV& ]s CvEUbF8;Y6}G2QؐDtx46`˰4EDGHuV?Xb#4El֝4t O:q\{۵o[l߼}%߈Ys'Bh٢&%!.L6It\w%u[zUfžFo`=:+oW5xx~{c;SX4-!$Į]q6šګ.]|S.Fj+۷ܑ4l͙v-rtFy. L@«sztRF[_ш0 .LhvsN )^Uz#Ik躎zjN8z%]bVСmGpy@:B}B, \ZEIP^Nj`^%x.2 (uپO9|*|]JqQ sim(-"*4٣{ :t󼅯{}PKWנ3}ĵ@l*,ԅl B=d疍߸Tʎ-xĊ\d2ZMF M8w)q9).*#$BETxXiѩ X* 3;7t DS#k-fk{SF^"'wְp#M4ƅ55e; :I, J:ƠPn|!VD=$T ^CB|5WE? dT^`6Œi.Ib ,i٨ɄGQ>eYEL9:YKTy+eu=IԵlCcq8!`4sE8GQ1)*%e:kkRiֲ Za9o'?Mzퟌ!ylXܠk8׃Jy_xQ|$q0=5\8u vJ%lX_K(1:L T%IGPˎ5ZuE4>~ !Pڷd(zt4zM1oŶ^/Z"UC)2v"fho: +zlbXj}}E0(~rNŌ^z$Tq&j?C5<7IM L&3)[Q2vPT6A7+ߥ]NSP=ӯ't4- 3US5Y׭+bM~hG3Z )*-2?f ԅ@/C/C^k&I߁ hz|ܒ{pKC)gII܃* AQI)CfK}})F1C((2nŠ_U'I<i[ry 2hB\:4_]Gk>` 4W)EӔ+~##wl\Ǟ4"C#qXA ~u~wq @ؚ2d#, îX,B,!w ?} ROWqZ%v'aa咯X ]v!61ĴjIf>vLlՁϋw?WWfYO]/:/j[ jԠySnL0(x!e.7F"ڴZ.LZ9v/㪨_ʳ n7yt?AJdD$(RY=p(x^d4iOQM3a23cՏٵc; 7\DFsՓ5} ;e x$rpzJx\ &6]#o>ڼNrW~0g:EQAmIݳKW޺'cc" 3ySߓR_}MJV1.J]e >a! z⚠PƢP݅݇3dP?ıC)9oZ5n2b}MXXޠ>ntyd}&"yPscR@xPxH܃GcF"Kw dEUgayzJ9XvvlsVn|a $EĶJolVnƻoo\׶k_.t=;لKnr7^:e" fyŊjB`Bf*oGBdՀR~k5eOYr1Х.Lq+0|GXâ9gT.8o(f5"%g|5›!Uhڥ#nV3/G$!=L],m%@\30"%6/p|3ldo5*mS6EQ8)?q|,b *jgȫt5` oJϮ-*Rs,bY~D.Ze4 @%I$IY!էE 8O>s&$I$I_m[|9^& ɀVς$I$I*bD3T hC)I$I$ BdJՙ aK$I$Iq MM˫ Z4L >TI$I$/Vڦj>G=I$I$I AКbx!~:If@%I$Ij~,iT$I$& 8g5 6}PdT$I$'HZcQj2M@gcF ^$I$韭43ef@hW$I$I!Zܟ  _TE$I$IGa"&('4Leyr^2طo2(ӛihZ}__FvVc[58=5$I$IjDF׵z')nۮbȈQLh*\ͷkW mrr UGi ssSc~?!۸+I$Iy@cAa:i;7N /](:y|Ľv2Qls9@ßzhZ+]PhKI[v̕$I$ ּ`Iʀf FX];1+woiW!Fw_i6s 'rꭌn@ҧxY1Oޛs)kgC/pΘs0egvs+zrw1&>2Ă92Jg%dx5~b8R}X=wqmtg'{i@J6?LYxd a\?\%̈́5oIŶ}}ڑ~vdТ})$ٟC y2In:#$I$4RDYڞ??y mޑ0w=4-pDF㰚pe30WožpiBK`­:nlr%e:EsU> ՞_ӏ1ټ# |GϘ) nzL?aן ُ!gK||: 9|le, M̷ü 9ٕM&k*U)J9Yuz3Gk9'ٶ{7-aL|^ϳ)/~{?~Ϫ%?vEgWؙ9?/Va(]ɞ,u?B$I$IFM=J2 tJUL\c<Ƚ ݑoynˮGM Bp4 :Dڤ}C7\Mq;lD6gp`"qfǝI=V^O%)$4,v`mƾ# 4f1=OQ ~ZOvӱ I3yuJ`YVC. **>g]SFQ [V&qw0eH"1q p7Ft^a0m"mYOua(-sFhs1D*XGOo~4 # f`?+Wn"ˈI7ЧE8ι{.녱H$I7h`0J-˩l2A2d gʭp wPRyD ɵmJʇ_̔_) f+6{~f#!֣j&c:-о~a"6Uˡ ;|!e<_aAUk)[r&y6e7Wl1`g|[%I$I목h~_W|UEs 2Sٺs/- eTfK3|.do݉6+p5r.GH̡15HTEuy8pC;GR f3#9:GD6@^QmeɎs`V8k0>wmEwZ"N>  S쬲P[KEš4=7?D dpOpΤ}l߱+2Wx,7O޻&݆DAΝ\^xvƕZ H$Iwd&Կg]}B$-ۮEJv!q; ٝڴ&*cXZqֽmX;&%:WҌ.Òyqeeh]bwT}#JؕN(ܳ?y9eɷLW_|/"uWyg(Mжտʭj^no_>gX| :b3.%E궟5@3`B21N]ӵsӷݢ(R̓ ee)1_364ʴ17F|~L:Zڅ璚S @VN}k>$Iuu ڤQ5접)4>ѽZqb: ؼ~-.SOƞ {^Ïí"Y8EG29۟AQ2 }}>{u=/?p+-eӊď p^Z(ѹ?Uw5\pG_OFK8ΛL3tKJbdйמsE5Ц FlG=x5, ygvVw *x#0x 檏 = 1ñ|I0咞]ﻎXE4bo30xb:r V}>b$$3=7$ 9CFe3Ӿ;)))ЅJqW"9gx/FŅyEbʏ``-!%;o$n&I?T2KX&9v!F3]_ ؿ? gq-;w^_hE!S1񲩴=suI$ItҬ]v 8D֕AV Ѕ[:w?xΠ!NGC1jz=h`4a0隆E1 xQ0 7 ׋@`4Q T}X͋۫F̦J=7F CC*$I$oW_}'85z״L=D'h%E%樜Bf X 'oju)TN|LVKX 5_@1+I$IJ!D ʾԀ'}&I$I$4{ g=%I$IQM-5ZdAc2$I$IuY`Ah̀ %I$IH`YWs|}*f,$I$I?rt:275e@%I$I2O֫(Q$I$Ite@ Tf?%I$IʓɀKXYAB! :$I$ICu 8O8} 體3-DDD`4Oȱ76svv6DDD>ZtIo2$IFtMnE֭1͍|TUU4[V8t%%%8c u]hO_ŵ0 ǓIaa!'<1?#Z2' .$맲zINP_珨&td:uJLL ]B<BRg UU%//{55T!(p4szOU͒$I']>C:xc dBUUt]?fx(H+nq\(j=i⪪bZx<'44جQM$I$I@hHH1m:':t ""W4 a&,,BڵkYn7n$''H||<}o߾몋l/ ~!{NNy$I$IN@cn }] + uBwvr޽̜9?Dٳ'^4Ν_͸q꫉?jc0Z&? $I$Im?Ju]g߾}<#2a&MDV0͕SS޽/ 3}|}l J]dfeقmZéWq%(F |*)b %4"io ^rspy\X|9]r'~NO{G 473%,Y|`9o6~}c~w$I:u]e˖1vXMV-( QQQvm 0/;vݯt:/..:(mQDGs9#I)zpB$0{НI|ؘTiy|̙ \r^GNqPO^-śӯ صc _`񏿐} %ht_{FԼ_^}MV]-M]ʲ;V.Q4a3I xܫIߺD$8\LX+rmޯfї_Ad_$׍Aؿ{G#O~,[="!t9k^~*4 ^1̙ptoΘc:1tp;oo(3at3(#'Z_[#u,#tsK)=̾KdEQޒ#,9mI挡hDfWoI~dyテзߙ[3^_8GI,%_dm3X]?/B[('-!*(Bu$E@sK_fʕĉ@Qbbb8q"yyyZ*h_͜x f̘AQQQupn[8ćrCī78lR5{lٽ2c3wiENcvヘw08\~nYG@.4qVPm9 y弟B t>x_lcഛ4,T `iBC|tgMSi{E\V$Îoj:egk}5\r8T׃kyK>߽tI2(9Go?Ah{^CPC?a7˾ẉ>7sy\qd9#W#4~|6'2ƒzAJBڡ]8iSFpd>1صU=-.j<ofE>z`6h724M,"/+l!73gt.+xoh~B egsN8!p; J'/7朧椓y¢*.:)..m9O$IU~~>7ow޴nݺY/D4iڵkz74Mc|'Ci6t*w*=ɞ9\0y{+vϲe8/J|ڛ"ZpڝU?B%ŎψQxz:X,Ѵ=0&fFŸfp!,})b̮*Flqz,Yu#?Q *w)s7nKQMts-,fŸxXlڕ[˰*Ddҍ79kpZm|l1pEQib!Or|u85xFaM8{uu ?4ОL!B+zj\2W>KoaϲbAzLz+msս,]} zO| s?I=n}OTJ-$PE30gIQQsnb$m߁Gӱ5͔Mvl~X{ zZU֐w9s^y//'D/d/8.&=d#}#[[ZXjЬSF jK`Og%6o܌+0[1:&OT~W}|1th7/MqcdTk=I:Νڵk-Μ9 V[?|\.999۷ZC%KXti,ngԩ8233f@ccc IDAT??{Ea$Bo! ͂`W`= {?VPQA(*4A@zIHHH֙!$$6]ywgw0a/&99_q:nCQPM*IAUUT(nї+Lhdۺżu2+/ESY?1}% `)8ܝٽЄ{W$s{3":d"]G`G9UyWnðծ!l9YYx4f1* w3}}x忴JjIA4^?֭c򐒜-$t~Hq?.[E|bX;J;~W?{F<5{*}6v1k?wrǟ}4C_vD3FD!{1/NxWƎ&P}|b9"=}r+?j,:U7;b*aM0L|xo|%%a'+Ϛo%dڷZ'coSy[vZhڞ Ʀ2l0缅y*&j!{j&?2?7\EE k ˧oߚmԞlV bR5ˏT~6RښH0زe 6lK.v;fkFNxy/>}:4i҄^zz695\>I-6MA;N/o wOL'X#&-QC +=97_qNBU;&"BhTu*8nX[81᰽L 1G7#Zbxԋ`^=1wo%a֪laP{1Mwe3"u-PuP*^x0tmرc'?<-W^3b JJmzt-<3l !͹}f( /CaٮUw(8[2;y'3plvݭzNKt{C&r=9H)8~?&5 7E݈jݤǍe!.,ayQoLGOKwfsHfPQv93q3Ğ7a9$[twҍ \؉/byӏO<6S)[fd廩׸;c'rb*'sA\gZ6~v[fַs{2h^/=K/ϕ3>&DOqC kl_>7M;?ccYؿv.?r'M={6p]".: ɂV/WW^y%W^yQ;NZn-rRNll,{tVUvO?1p*M3*B˖-yǸ̤q<$$$O\: O ֊ׄf2j:TTCC38Ćorxwb9Ƿk@5YIt){#mf3{o[ӥ@Ydۃ՜~=(޾};UZsC zqlvL&$?_UWQ7nbϞ=|>֬YCVV:u:2kB_uZ^{5Zn}.z #a)q0m&,(RE@B@ťeTLPX vJBWFUj.5<[; 1 `XQ嚧KjٲYJ\NtH|T1R}ԛS#eaD18ʝGNN%e C'{T}Wr~|) n`:6nYIk²h C7 rVNˆN abFJ n<{_^/|@͙L6b94 iؠ.U} 33?wu ~jBQ}2̃TsdܾͤQT4W=ƺ b޼oq&TUaϰ'uc謘܎f2|739ƺKs61&TUeؙUtԺ,67C#լUp-j 8qylgJ(r y  !SE3nlj:>7ؓ'*1F3tϋ3{3S eʌ5Ad !l6өS'V+_~%UBnQF[6 0 222믉C'T٢E ƎKzg槁7z`j>y> ݴYk7-ȠŅC` ~>G94n83sկrmCqT?<-`c ^_l t?}Cw?^4G?G#GX { #!,b})>w)W.$(6;as6e E:z) tO>A7o=Jrt0E=HNТS躁<Ȗ8s6~mO`YK굵节p%%l[>)i*WI!^CDDluhZ8+ueKy`$u GA1ȴ"`ߜW3^MFS/:kfscYeO}k{|Ҡ=w<%o3IzSʝ4I.x6p?sIԺjhyz!~ڐKǛ_o}8w^zApIΞsqG1'S**."DV-YF]mτƤҹc_g׍sd4y(D`0@YyJK*}q%..tJBOTծ];z̙3q8r-*f{ݎc޽<3_ѣGӨQ.v'kB|sdyٷ)pk?~\ jC ݇F1YL E׳ Zc*;VCSRTBW~}[;.;F4(3s SAktZUńbLIy 1Cg^65(MϋCXP-A4eAXh{ٱl29tm3e~zT \;pJ>gIWԆd`el_5y%F?|699t-/`mZu %7y#mѽQCl,0\no\Ƭ;Ħ"agQWOHǡ eCbNخ9]oPgBs3i|7z|CC/dz\8Ǫ.gѤG9J,+aPyiA™ax6C|qDӒu-eռ\ԭ /- ~aSS®,ikrxb~(ͦsQe/,4K6 8h !oI|`]nWYd'P>Pʱ] !'88#F}vfΜI^^ Yfv(((`L:s=}ֶQM!-=*b/9bESD$7s ߾_O$&Eܷ[?:^ /;@ A߶?|% T_jxLl6A0"2szvl^{+906E1dVܿ O֘nW¹g./r)bo9kyqB|Ov9?]u~P"L8~\ɶSyiZ<0ywsөs*uSY[祝ZG-_ha❟ws0O{_O'4,!ޏ3xpL4WNO}̬.l_Ʒ=~^>7l"-ԫTyJ26n+םsNZ %7t1myb'H@|F|8cǎ%##1c#+?y܊Z7 ElJcV/f1i,Mg̎ճ*N2wnvhPEy:}ͭحژI`H$ u*^ݙ˲ K:Gj?_2ozY}^^<^z ;%n&{,Z)m;ht[v=z~O)Ɏ6٪WMwikMg A7< w{1N8u ƼHjz`qL,~UJO`|knxy*N:((DLh =l< PQ@!@d*),bjdRPdŬ6JswpǕODL?Ob%D%DA ƠFǙ:ڦR7Ocw'>DC mJKb2gt{|=g!qCʫqX= }z/@6mIcٶmZ\p==~/q[>z.> FXmZ*ύcӮ"z}V;U9{>;N澻ox\&-^{Wr!5U-Qo@2_ty582_4 ylam g.W (m>J`ÏkNӎi5o;]BبsҘFɻoyg?f]/aPKPʆz6v.\β%okV9<4⺌K1sc+r0Cix@P˟-mX,>#4irR 3bXl˖-#;;D2dڵqƧ6'o5 M𦈈n(ўBWo 8AbccQU9*>FB4jPUr{Ӭm{LY4{|*BR Z&jJhF EC\9YFo@׊fآ5 MnW4S6:'q1^^5MHnތ؄$bߒIjّ&McTrrwpPkUbt{h,b/ F-&Eрz`gc q^495AV2=%.FPLbjwZFt`q…=Uj@Q/ 9ӠM&|RҺZQ5V3Tryߜ&a; W!9jtjyE1(.8 ckX"_Bp ׃j'N2OS6ePi:>.mZ?.\ﺏS5ebґpc?IV kE=mZQ4IoBϋQqдAl٠KTyl؟Os/QvPӤmkJ6W8ͺd_E'rɨ1JWi*ulb'A b.6f^BZӲC+,%..Mh=^0nU yŅ/{̸,[&R}>YYY$''t7^/X?^eapA;g0عs'a8{w&L%P ( ?J#|~Q;A$ ,x1PPMкg 9cֲ<۷`+(|(hfTGOF~RM!5\8QRMŒO}S>|΃x57cا4'@QP53ITh(h2uYFUUTUFT'u(,,<ť9W&S2]D4΀'x'(ՌtI "[6F3.qh>qA8}[r IInۓba Ma}c}YS5խw[8p'OFyT3 \BkU0 U?NٴKG:3tB!_iyMHkYݶ[LRԚ.Y_ɓu"3J ٷov"!!W9Ut]'++ bccO:8fBBBؼy3Ʉj\mFtt_3ʯrՊZcυOA.i['wr v3LDEEENN^bqDGGcZO:*BPPn7Lxx8v6^`M㌭=2MFMC!r<>l(l6ԩCaa!`Ϟ=|S>5iX,f련iZź ضm] >ȿ{TUbp8=#B{Y:B!-!RUUV+f`|( b2NYS(hFPPPť3 èy5E}4/jh4/B XM`ygS<̝<+= 0*gB! ҮZ3MBtUV*%cBQXM1&:A3tFq{(4B')LGP!SơLK5Fk*إ+B!B!¯$ !B*B!JB!+ B!¯$ !B*B!JB!+ B!¯jJHa>ʄB!N}>|t.|1N C YYO6B!jd̳/G;k`z wcx9 h㢰EPp&D U?`Ÿ<>LԞ?j "PoB!ٯFk@uw+-eݟX7P, Ѽ~4+KJJV#lP%7֠@Js8P$nCRaJ\dρ0A]JLѬ4bLg^N_F~wsh*d֛ݹ ,"c^ ]^C(+g)  Ub#)*,!$, RJ>rb`PR˾}q*QDE`RtѼd?HhLPA-[K8u=f62=o}YxAFkue(t{okj{zAv egTL@VMǯLp!bk]׿L@%2͛'kHmNjb8|r) r׍C&ѫZ;|~Yq[oeE]12ɟ۹dHEQl\/>*w0K},p^<-k2CtYd]Є}+gbOM0Phuݯg## =Te4W6,Dsɵh@4Eex[׆7B! 7hs5\3i)/gl4UCKٔCK>Ò_ג&F{cyе}'i4j{ 1uBQ=%8عq/w8 ;q8R+ߔ{ddcv.ؘD&Lr/'st/wмڢ+YA]4߯EV[DO"sÉ0#Ȇ("^ PKL,JNlӅs|Ç>ˆGˆqu(D&4⢄FB!T%^Kz[e4etjLtp)O~bSX2W nVs4W/BUebBJ=J28](6J霣RJ_^'nu0J >%Hяqdqk6% ɜӭ']:Ϡ,^(6:_̥}P9\U^rfǪg93xw'4@0xFVz54ꦴx_*eLLOb"/_E?1͙֡C7uZ :p!S{"Pz)=xw )蕪H6xugl1woP'Wo!؅DP&)&4<9膁%(:q⛤qQyuNk53Hl[ ÇW )qy Ei&B6$8D> NѽS[n|X2_&imh(ME8"9\tQ/:H]R woJ {#cH6 s~zG>!Bc5eLB"D]̛ [ 8r[tMWSY#C@+1Mϡߥ}xqkh u d/ 8urleL![yiSlx9MW\a3[t<{](C.~ԺZ<^{>lHbt}̞9U ؉;{16)ke\ . c QIt Ma=7̈p~=ua9oO!%|Gm}oihuen]g)  /^ێU!g f#n}`:vOs>S1Cs_>ȰkbثG®W*\;iT}Sf/e`$q9-0YHILNp@mۃ#IlPUflN>f o\¥+nWk/;)~ ;k f koG?ʹYzo~*2F?&4C"HNL$z3?gr5Ήok|:k߰<4^-Gld\ B\xAa_u=c^i HMN"aD-JB!DmTl)))-.]rCq]Z\t\.Ɍtj3d`r^v-bl#,4vt|sXl6,SSh:RtEbVxN\& E9Bճ>ĉJ)kt=f;O0jE(fkPZOX XOxyEU oX]Ѫݶ- N!BQx<.i!MB!¯$ !B*B!JB!+ B!¯$ !B*B!JB!+ B!¯$ !B*B!JB!+ B!¯$ !B*B!JB!+ B!¯$ !B2tN'b!B*5]ӢQEB!uLk\?.B!#*B!JB!+ B!¯$ !B*B!JB!+ B!¯$ !B*B!JB!+ B!¯$ !B*B!JB!+SMLdFSO!aaRdNYgL5 ]+>(_waxފ|h`20hvV'|>%%%\kr#KaٰS-0؝*&|mPW^kt:qQLPujZZ5꺎BQmn߸\.N'^łj!N餸@l5]$Q%//V+|ǹ<:?{ѫt+o](}XϾZ@}>8Nv;QQQX,, vqݸ\.*6픇P0pݘL& .YGÁj%??SI/ʏg4d2aZ:u`e}Pw? U\hg޶R5u5*餰@l66 0$??lN'LSv@u] Z3iRPPPEAii)C pK(aRYO0>GBh 450 JJJ(--%&& {P'//SOuL&ϳDɉ5Bq:J<(B`` [lf*Ž(R Zc*2qnNHwNZ@ƝN' 8Zb!>>rss 9%5AEg +_w+>gՊ-nP1cr8TT*)j}ax<JJJns9(bRwL*Maaa +^Y>y !?ca>z5STϳN>ӏh5}Ewt @%_oP}o]\2>6`mO6 7UT_V=xJ:uرcN L: Ok߂W+ij"4<(2ߐO!pO2{1k!iK.x=:7a.NG~{>CZwE((Y!)Ld˳su~8mj R_l>$V(1k*u`_.F1eV>_m0k~*=l6EFFFgυ ax),($crsi&Iv37#;N]ijtvmZů f&zf<%I?e5#A:'EQkF`v }3FT mNٻʎP9KAP0(**",, R29%:^6:w}B8ؠ;u#^wp㶶b% K%sJrI xPR ;w[MۤU=M9hvfI[}g;̑|mMMMQ,gݯlU U^C+rhNNqߧiiD#WIAAUA+ p$J8ieMGQT"Әh2aXWeU @AUgaaΘ4 2a!"AXL,_ϰٚP]+W#_=~&$!Qf,B*fJaxtTc3M~bx 򽩕J4h4J8lQ,Q1bp%_!4]!i&JwjerCLʄc Z nju4~>I_EȆT"mTx(|mIq*%zNTDi@MuE!~%8>PW.Sx2{LtLss3\nA PS5cIg#(ʕ*L$G`dR\{5DS2gT đ0M"VF) sDnTڈUJ xaU*i`46(r`7czuWn`ýc,]%ZcG9RI5jy'pHfECxK-Oso[I IDAT!)X?m_Ö %ȗ%KO ^W^D"b/ŷs6g0[Un;ym/AUz?>C^Mo|=Gz1u&w[oyp0/_. %P$E/K 껫W<[E}-jn[c92ټDac+4xrLcq}\b?=vpqo'aڠ$.Uk(~mš  ^jj[6(l΂i M<tUִk*4ǠXlAـW.\Bitg^m}w<Mt^t Y#4MbaHFc/-q(wedD<؁ ٴ)Ge$"AF0MH76H(9yP5˚A18DM4ZwNn5%H+:YTܲMDAƵHrp_EPLs3 1N=xIu+g$B L?~pՕ+Nϱml}aϥ\~ŕv?ۘUhPt= .>kh\ʶ}gM!~q~vW- +xY|v}E=U~ , <_B?x50k^e2нl;>7>ړgG[`4ͤ+((Libzğ4 tJt_4WT hBaYC U^sO1+B ^!WQ >ۚv۲&kW<\Tዏ/*pY3|zh3>P#p^ 9!ay=#UwA$ lQ!R*P2j?;[= 座1M2|kɎڋfGѩ&jt P0M4ch(4eP0 d<_"z;DB /~/k#Yi!RQVDg6FnҼt-WuQƇzC<&Qzrf6oH*C#î9;HkJKPzgi6'd[HDC`p$OsR֭YI<\  0F{cBF "~!ly;uw曮@V^E{_  #<^Rfύ+Η[_ʍ:üڍ]D~FG8=3Zŵ/e}W7ԇN< Ǻ٨f|?ƪ-^H"&Ip7]d vu&.b'0%@|i>$mo-o YgG?Z7_ß 눛%z>-e/ 5?Dn!nj(v/:%>5Vy.VrIoSL+0:, .c` `;G@xre(0dBRn R~VKc1+_2/[Nxd&4/Sa \":\C&~ǡC8𷿄A&5֘av%zZe?#\7c}YTBE۾? ٰi?j3Zť/B txcdyvSgc LpC[3sjPX|#%ʺdc "&!nbRTH@ݜzPv5i P )WDsV^A5 S!o/"LM*Kӭw^ Јb JTQ?~v ??V}X*@#54\AEզT*$t67q޾A+@Q&0Q&HK2Zcuh, F!O1(K[RwD/MD e$2ca``gU6]X%X4Z[ӕU7Lrb 0/@Amf5KF>2mGa][GyfWZ$^ҫ};Hu/!rex{OtR.I>lj-x/F+8(9r1K„dv%7rѐ =c4Z@&+SGLJ`ܼ.iGgGB~,!& 1&l}QfAK3:ZW+yN(U4*|m )kZ 8N( O^8lhC[ia&Ԡy,y@TMEQtO׆ũ-dPH(&'g -Kbhd\b|d;&Βhj%93HXdr<ǑC/2CXHQ,m8:DѓĚI7Ĭ4E9DX=zRa|]-D$Ů];ٹ{7۞|'@Eml7ٿ6{9c{Yyqk:ܲFlIs Q.i C'i>:WUILvyoT,rce2Y \}Y?~AwmӭGAUi*N4x[,|9=/׬'lW;eꕰctmδל۴R.4js xQrElXG!^>ˁ Ca@S*Zd ,ZFޏ> aQ:훮m(˓!0ub2+^EKF,}c6>kY3װd3۷xuߪ^r.^fe rfAQxɵlR3hnkd~6ܷ/K6~zFl!Ïv![Ech}{u>" f2_}L?y3\OJ@M= G4=Ccumaؿa816H)!ej@nubC۰a%O=D"===]wE$l5gXobfrr9Z(B.YKrǹ\cFYs_*K- a\ZLӰVm4_N/Yl ^4tRh.:::>ӹ+MDaJj:gF!Dbfz a+o^`?ͨ $Cs:{k qUզ!:o T>}䞛G3v~{Ǻfz&`o4#GL&} xBDUHSd``e˖-H8M"n% O gEQ ;]Q@N a(3lyS:KՔ=2Grn^Xf;]T7[GYMMMEQfHss)B"~vիd2_XcǎL&imm%/FU4Md2"\0YTU7[8oQH&dY8t:=)[7A9+C&E+WX=SιBD'z5 ]ʬ>ל5zMi";Ɲ7Ic,6 XuB'''WhT*E:&:199 @&x<>V@2P('>oXd2I(p^( MMMB!zzz8x 9RRڝq^)BS*|Q8Pa3TOc{!dISWs0zN P{LD"AXdrrbn D"DQZZZH$S(僪֫*bu P(D(wA kK)?EYKwY]7?E!q!n x~q~ֈK3`9 D4%L&N?_2jta#S,ߡ-&|oUÖmjBjo;~zչ:"bCOs2[}D2"   U#u,*  87xlAA &`z PqAAA?JH,\AAXҊ&u *M  BPj @")@ AA8 fՌ0TL  9n\E*  TrESy  AAs]f~$  8҄jrw"AAu>tyTD  MYJ>  z0    a&xޯ}6>S5Mc  4bTĻgǁ&7PAA0'O ,^4@S&w` {5M+  1 <~xwWW @ǻ ގ{&clll{ޣ'OAAE\.| /piK`ζi~(ؽ{h,_z饛Dl AAm&&&&9´~.p;Ehd.mkkR *FAAaa|>?yC7{d\x3I ^mphXj[ouMRTVQiai(iiJƦi^4ۃ:vKwq歅,AA*.b橣UuF\,gj>L4UUt+2xX,߿O>ٽm۶P2ȶuxSRNvA>yg\Ѝ~{k6  6k^o_.\]^4,Y.DuÎu Ze%R0=(WzŹ&  >s^~ǵN+BcZe=ڴ03~*P&4{gν3=&" %qBtS_9Zՙ^W/w-  QD Zq ZWZz݃= dnKVøÑv* 0F: M9Ag-1ZuyA0ä IDAT.#:"ӹsO{W.6F"y|n5(xAA?jMx/GtLKثa7糖v$MAZN_|=]3ޯ~S2pk<ǙVkɩMg p~%.k VAŞ[\P'<7}"WzPlt% 38H  p2|j4;k nft?Zh JU|֛?y|{SAAX<vj 率SiO债iA* z9A\h1Ւ>AĦ}N^u~j / A]G88%򜎧[zPǓnT:~\ ɉ  v"  n#@q ~6bMH+Y׵;k578yWꅈTAAX<̦ez^[Pg3Ȁ$|{F^/9VYz떞"Q= M NBAt}-Q:-Hnu₺uk]Qg<>nb4[+ q:As-zuޛ_SAgӺ >$^a{|-4tsgAA .%Dc/~T4݄^z߳Aq:~afGA43~D3\(焺O3dfs|~dOc\8;z P/ B s^uqĻ;9^?:A _oAAOh%O5zPgl]j~ct{r@NAt4ݖt% u BUAs1uK *8Pc>A\u3?pjɴpM0):'ti Yg-(bRAz ftK%:I%_8/JH)H9Aav)UAOB^ygڏ8ADZAӽ{^}@tyqݬwԻKAA抟H%>s為噋CV_=|ŽH;suC8~ՊJ4g   zh>Рt6A_WG~O 4]u;ΧRAA8?ǀ0Z+^QZٔﺯ P94΅jΧ_]ivj lAA;uCO:(.~a:zwj2(!PtIIENDB`MEGAcmd-2.5.2_Linux/contrib/docs/pics/ftpConnectToServerWin.png000066400000000000000000000400051516543156300244130ustar00rootroot00000000000000PNG  IHDRU#9tsRGBgAMA a pHYs%%IR$?IDATx^ݽ$[^&rFZ ~KؔUh x1n}渰beO~Sr:xiQzvҖ%\z-)1~=5{Ɩxli??w|>5I]=lnZVJU?T&I9'{֭IrL>'/w<ڹJB~74rLs%zm~=뒴nmޱ]RnSIqףugV&kxdk{m) LƓiMK|iߩLUckvix}ѮO`[^wHcϖRg\I뚵0uvJm$w#:-[;rK*1Un=ck]44DzZ DZ41ϓ<^Y`gi4RziҞN%!1Y;|ֱ:V^yYmu[HJjjMki\0<)ukҰv,m<}$-KewwO~my?s9VH/O_o>Rgl{^O3_kiz5k-`b*?&ڄz\h=떖JUƔY'q%+S;r~4O}jgy2m׭Ϸص]z[պfL]T%>UJP* @P* @P* @P* @P* @P* @P* @P* @P* @P* @P* @P* @P* @P* @P*}O۔ _/ڱnel]N??|;,枸T}yF>01^ d3OҾOxӱz/c4vv].wZ**drD-M҆u'ާ3r&{ee~U0w,kssW&PXDZ>k|K\v`tH \dq BͿ\41+7ֿp6ڦD4x9|Br4d \$}rlO ϹvKZe|?NBo>uv3>mR2Gķyv<珥|v=x&sIKQc[ooΕx5,*&V&_׶hOH' X-ovsݢ޷Bv.ﻼ6ۮe׷ORKR\=h>r,H{:g֞uiwZuGrֹ1OҖ'Ou/U\5ql޳{>yx,U{%9EY}v׻\IznlRS}uj]M5jƵ,.bR"M-_T.ޣ%珥]>{y|5szTGq}Nܼދ{_]Yx4JշM6OҏT'yy_n/>A۟4UL~{Bmb:l߭3l<9]>a?-i8~oS8MIkFe=ZrX:R\;Ɨ{:=]T${x)=;s~9ߏM֖m{\~OeƄ3Oux*۵ߞl?esdo4u&vk0As~εkp㌖6gnt')^ӳ%pD٥js}OeKGvz뙏uv?Kqϥu6Só\$WJ;}ַ=@g<]J9|T'ok|6Iv?ɘl&oO6LH&IpM2c}kW2^O$yVss ޣ%\׭=>}9޼yLR^S®p.}Zy_nWdk1W2XK s |EJ&UIgwWe;k+R* @P* @P* @P* @P* @P* @P* @P* @P* @3_>9ymrݧ-۳Ow^o|p۾m~zw£gw=n:xt'{Fc?+\v{Ru{ܭ]myxo(kI~Rûnғ&NX:K {u~./Cå o{&Z5 u Tծ딁޸ىu>C~]sQ^~)UWmTm:m+U~^Tuf]Ʈݖ[{usί1W˵W~kwKTқw;f>T)UvKUkrn(UEӞs~XX^['jmL"~r c[y24rk{vk_/w~/=W,ι*8u\ma,vz۞ןc^ޏ_Ә'9=_}{׫׌WTM Lߔo&o3o.78VhʯOfXw6r.osk5^q7roנ>ycm?߇5>-ǵIM2 luc^:n&̏ѯX_-RsyT908]ػR]mȸVsxs gzs~aXZLSe{s5usb͹-^RU<7;_7-8v~,7IuxQgM[~^Y{:| Z+~?q<ϼ?~pfנ65S:'8[{kdszfkZNo=zN 7<:߼/ۯt{2=|2u5j-4sUp<ӯsg|L_֯ڳ8li4־w˯&λXy-Ji/Aґ}L2?Hc|TӘsJڞu vK{I>48\+ɘӯ}]cm~? ԮUkqӸo=k﹟mAkѺ{ 9ƹb1gql=MyQ.>.1b<=6ruz^딪ɘӯs{ry^k_+Ʒ>n^RUyhboM Jm7=c.}S)_۳ny92lSN{<xӦ"6myk }T 6k4|γkPqm&KW&3[Zw|y-O]}/tziyl>ЙoX֮S7wkZ>u܃9WESzbw6ruz^+ѺiskܾX~={z4*a{}ڵ{du{cgˇ_;BKF=W{^ƽM{Q3?}ka>k+|k/g [gwN}k,vNkX|/ߟmۮq+7]>mypi[V cK7M{t5x5Gj n[zz\c|77 -G9ϱ. Qh1W|>J@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RTF~zwx͛ûO[g].Jշﺉ/װR@RqzǷmR@Rǭh(U(U{(UK(UN|8Y^{3d<9gbr}:Ɨ׷dkoO^s9X7gzooT-a,VM޻ço[Eiۺs-\lv֮a|}<s'$t2lۛM1{U'84AtZc/;1{6~em[ՍyZ*Ei811m޻_+E1B<&SoKOEVǭNxj3/$>uGc%w\E<47+?g;;-xU^`|1Z.^T%e|Kq<[l2?CkscOQڳny?6lS.RiMg˫O)l>`|1L[ǭO$SW"F_˧W%T[3S^fٳn\I[{A)UmE i|J0LfsnѶۏq4[׵ סǂnPj\|'*ߋ9J[9zLNsy%}_O_,Bx0JU5.ڔN^9cWkꥄc5v>}, M.:SoY|'“\=FFܸ[}M^gu xW.HP* @P* @PTɊUJo?'ɣQhT9 ດ*YRu*Rd-J՝+oLDoBG(UwN[R<2J֢T9JDn!JȔ*YRu*(U#Sd-J՝SDTL(UwN[R<2J֢TݹRr=鿗ZR<2J֢TݹVzׯ]w]rrݶKU+:c}+sv9K罘|dH9غ^y?9:Δۺח{R<;J֢TݹV'7oaRImM8[ {$_ZQJri\+ntk9} Yʤ?;o]99y}h^-z)sWgTZ;Tfᓉd"_lW^;Am2l"/)/Aoojj??{w?뾑-Pd-J՝ROl}m$M [~rmd<,4GnY2]%^:ImJ?y/ ,wyqXFnsn~]my~;zy_Z:ɲb95>n$oTl\JѲkGs,U \ӟP,rӧU3T4GaՉUuE:v/kMqks:F1Ziymk Juy^k:{;[q[cQn].V?vKw}>7er(UwVB^;eoH,6d<%O67LӸ}ϹUSCmgC_k=aOi=;Wzl]^[dfzSWYTWTOdD(UwJRZpE1IpJYZVrݮMf5).3Yoy[\ݗo>vex&6#68nOfg87BLv6NuCBUUSx.uyve;'fo9NJޞ\9:><}Ѻ(=ǥum /ǩTlsߥj^jRNŦ,4çZ5{YqO뛏o+%踿cR>ìkQ%KUӕ~B_L鷭L^+ӚNmXKm⹔d5s+wq)NVaexk]};uߵ|'1M%oS.O<9~^9r xdWRgTKRcOf#iS)wRF[Ruv.Oyb&Ųr:kew)kOIی'е%ukۥ댎0`Z޲^l]~9GyλvY3>nN&oW{cǼNߴ^yNJ.Fe^Ei'Q/1)YLS8Cijʏ)..(Uwnoʓ0:M?NjX䋯Uo?\0pZ1șq*_z^._g4INOY/g<ut3-kc:m3elv?u-g{DubRQ^|R^)qѿay.S_>z٦Ȥ5NDs_*nޛJR/~,OGC[eNz}>L.q c~ZmwZg_'J՝SD/=,/*qXh)TK):P<~2V)G}[9(UEy(U#R%_%J՝XTZ;T\R%kQRp]JEsM.""""͹RWT=G*9T(U Dz(Up%JsD+Q#J\RQJRW&y4J@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RTjo~/otuO?T.I='URÇ9|]T]R#zR5$jn,"*ѓ\SxDOW?/_OgS-_R/wX ?ZV/GY QT2m14{gv,£]7xEWH&eTJ1g+Y;VlxexƟc0lL=>3]^; jJ6/ɴKMoRI%m/m9V2*g}A|~X;z\,=eʟ&볲r4)6YTR)HcO]A)KP^Nyۺ^Rz ܊,Uŧ$'*YL(r0/<Ӿxzl=`q'guv*-g|lcKKMymZ^O_Ǫmzp ԥVgTT잪TM~L>p |REOxvJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@RT(UJ@TW9QDDDDDDQDDDDDDQDDDDDDQDDDDDDQDDDDDDQDDDDDDQDDDDDDQDDDDDDQDDDDDDQDDDDDDQDDDDDDQDDDDDDQDDDDDDQDDDDn4o{7QL~?hjkO*Mm2/ZKwrcQDDDD8y_׉RxQDDDD8JEz(U""""T=^*Ru{Q/JG(UJDDD䁣T^NjR%"""Qn/JEy(UT7'JՖf:_~*XnTzcR5MO_:R=kXZNqOEze ER%""""-ӄy2魔al>/'ϳɴOگuTr. Ӥu_\\;J6KUY)'r~?OY|6;=u'{ ѶBfKU$;/+JUkR_[}7ٞ~ZE:/g]t~,UtG%XBKմHKd>J9nN3S܏ td\4+Eyv?eѯo++Uggs1i\kTr#coRM_>hݙYF?.V;Ru~kiUTmυR%""""헪.äfy[ʱJDDDsKO`'m_LԿNTj_Vѩ/(\u(U"""")Ur*YR%"""Qn/JEy(UTka_ |$̱z=,$@Z g>X^ۋ0PKHHcboHcZs32"#2#SUyWs*+DFߩ:=^RJ)RJ]Vc`? @* @* @* @ڻw^~ӟNr T-j[*kDV`EJ*iӭU"TO P TP'Rs'TiWt]OJ6 .3oK|r=yP_ۏTyϷ~΃4OH?iy{[Q-T6yzӶV̥vW5BUop.j<xm@\gޖrR'$Ӡ=7N%!1ڏ4}VHhTnO۶~?=]Z8U`Ֆi`5y1m?~ۮVp* V$TB@P TUB@P TUB@P TUB@P TUB@P TUB@P TUB@P TUB@P TUB@P TUB@P TUB@P TUB@P TUB@P TUB@P TUB@P T=_/J/ܯ_>׉k>?#"pOT_ç_Oܻ{0/9M oaRiR]~68iw@ncõG< ko{W_%PSc=|AL<o"T Wp@n=>2cx%}{ZT_ު^%|/,3Grz0|߳+/?#=;۵qc P5}:BSʀAz VBUݺ@*V;ߤ+k/ͅ?xY Uwͳhjho+` d 6ߧߕڷ Un盤f?0G2S}*b;9R7vWmߔ.+ 7IA}>tlGZ0_v;Y9lLσlo>nPuy5-3u:?Xwݦ i嵸D_P~x *ߙ:kr8~^2ikHϛeT/ڶڱvNL}653|Xw$5c ^:ްM1X8iipo1P?e)-3{l/4mXxA8>ۜ!\rZypm>+:vYi}g;lpP=J{}s`8i{29llzoѿξ[u> 1EnTm^ Nm/s1t_y}>)p#w֟~<zV%qS>v4!5x~6T?VAOF=K{*?~뷫G3yrzD-,eY9N?m6-f:7M6ox .ok;6n6Cf}{\mK4s y[Ѱ̮eCm8kmw"α݋s}7[q߾}:e= ˬЖ*l^?_tkJ-uwj5sa4YO77;FCסB%ӤBu2gZڠae~y]>[ݟ]r\rzֿ\٘u:Щ}l[qn]wZߧqsk7}_kocyu/z|CUZvi^pTpyzu.Ⱦ5X7]ojЕsVyHW1gryplH~WkƁa?ezH9>ӀKKjjP`m5F}~ޮ^w]OImiTm=w<òi8]+mOe܎J]okؼw1=|ܮowN:ZTγ-kLät= fos;+X=T> XzFݶV{S{<{z]Us¾[u>\#A[m_J=r[TC,I|{t·Srޭ^?>>e.kӆ\<.Gޑ1u5 U6AB93/} oN%_]Z~;TusOoخq}|2lm^wޟƶze. Unr|Pշ}3xhOL0{v/eWW8>=kՎ<<}4ټO1}}sBU Ty)aһLdP>:qajuXչog^+k8Nn뒾^?y¾k Scdƞj{K{d_Ky=Yxu>g<?=8+{Hi8ufIO.[7;'T*GpɷyP4_n}Zw6޴4Խ~o~, =XY3o뒾bT]6=Ǫ1K#W諍E νP w4I<}WolB~ZIiƃ04L Jxlg˯5j}}oo0T0~`aѧ6]rDr|=?_Kcaױ1h8f5aޟ}2 &v<-C`xy{P5nxjTCΧ5m۷~pjG8͏f:t\EOnCՠ,/fc:=1̱zg>iUq55{s*&08n |pϟ!?ۜ-sx,7nΖI柲]ap| VBICmgm`>q|* 4ǿ3{3hUcZ־Kyݡ*nhV\POzm,/lg_{0aZO!mmݟ*.:/v|z =8ٵu=h/7{BExp<PyCUE& *{#TuyP*K7BU US}PBG?lTF7BU U_[zM_ UF* @* @* @* @* @* @* @* @* >xǗy} Vq|~_{.o U?n=xcyr+ڗڮã˔p+wn}}߶#m<_}KsϽ?/mo톪V]:?K׵3znGV0q=p+wnI|/nqnӯ9qZm x8덆/| 0wu-naz1Vj\?6~=@ս c_hcý7>~\ )&Fݙ8TʇIADŽ[sqD&fs4}6>Af_юA:S;GǛF:5Ra1ߴn7M>_X7i{y4I!?i+x-]/}=?G65z>}OP/g9r5W:qzozXkQѳc;>ƎssjqçiE(]#}Rui_׮Ŷm8_g͕m+_GY>uq2q=jP>ښoXEVޟr^Z[^گs.mzm|E}ҸO{|{bi9КpJh~ f|~GoXv:f޴z}1׿?b9^x妛΢~\ :|Zy)_eΧM͏CI}}/䠜'_mg6\|LM7-?Qӿc_m]skWnsk}<|/Os3mq\;מklIc;kh-Q}X.[9<_CΧimםc>'.g;9S k>6:{l|kG\?&q\E6ZǶ?7'=,8ycBm- Z[;uڟƅQlp/x8_dǢSy>/NZt,g3os~jl'ߍ.?&+Nm= TVymVU_}8׋uؿv۳}oo?.v\x^(__UڽhZloa:;V8_Ϯ}/S;6\||{b6Zixۡj|y~Bx /DY7zvͫgf1u]RuhǢ)/;=˴qӲ`yC[jݠ<wuc6zޗKo8-:}JsnZf_}=F5us]QmqE?q1e5?7Nr8a׭te*OYsPչLݸ>.~Jy73V_qzrߪ͇m帵}Jϗ~֙ا嵿v7iM+)_ǎY>t݋֎ƹ1Rw־Fu_?|ܖ*+zڶ=LCcuR~[>\o}*!K]o wZG/vmc}Q U:.8ZQ//5vX[)s_n4Ɵ+ʶ4Weʹr阥uo o\e4tN \sˏڼ9V_nqXvmO.:}Jôq?VŶ;σ5}|;fدsk}fZyϪwW5un^etd|eWXEo}U.Po"Փla$YPo F=7άqYϙ]nwXn6OLFx=Ǣt86BzϗqБgO7a=ʼN˟N}ǝ}XZl{>8km-VZO.;WyvV{8e]#gkSoc{FY> 3}p~8yˆվkl~g1wrG[ ]kV{BGtw"XUnc8 d~g~8J7A=0zv"ZleL'Xk'UUI^]ݞ`cF6b3|*=8o>9_&6ˇf>|p\-̷w;pcr>ajY߶lO?sEgoyY{>^d?v^mWh|,h^4\8oumM7\q=m1k]wG6s;}Ѿy}lo/TObZgVx~g*R^[\l{+ x,gU-jC9s?~2B@P TUB@P TUo^޽õ9Hz |έ`.AU_Cרqϼ*ne+U__= Tq+W9G'( /M](TMo .FpWi&%iVE9{Z_oq{c5Sϼy2UA_K<͗iֳc;W:lZm>3><6Ql\*]Y}b}ֻg}=4,w>[@sƤӠq1~i; A ۿ_vw/uoݧ5o7TаryT= U Xs8,l_/MeP4-}s}yNX\߇1DͶsUZh_WYN?\?fηٷ6ܿ7A 1 Nacu `áj =p]_wi5mڼd11 'I|}4~5o{+y<{BPuvaZxto0TM_ޟ ",1M-.p1ڵlmڳ)< 򵶖vlkw'W>f7k霫- U{=LqYێ~A_<7` AiSYhHL6`jkٜw6Pn|{b߶vilxu6soP5?->|=wSWK@ UaPlLO+ =$TqfZDPQ~~oP.W(}̧TAb [O @* @* @* @* @* @* @* @* @xSꫯjPu-BUDP$BՅ*^G/ONW''/okG?xy5܊P)Pe??iB@x}BՆZJ*P@U POOcjWk/ˏ?L=:пfmXηwjqq+;GrvO],_X'&Cկ~T`X0xNi@4<:~0Nk䇓a,C_>L}[ZM1+Pu C ۘ-7;߹\כzU Ug3vs~iPWW_}l;VuޞCH[6s;lJ`Ce]_mvX(Maǚ-׷۴yyLi{r.]v~p]o:T%`9P,Vzy>@ZymVm>!ztr>}O{C/úǪs?7zUrP?Pďl+AkQov6ϧ? n{ڽ/TM<߯K&?ϺUryA6 VڳuLj߬cGS{_kVCPއiK. U9q\.7v^~Pg[PК~ /׽y 7u'Pı.8q"`u~p]BՅC4w6?3 Qֿnvo[JhcjW_v5=ZCըɇӲP5{b]'%T]a]|V@z6oe? U׬WUm =O[OB#J8,5=vS*nAwS=!#sF* @* @* @* @* @* @* @xH?ORJrp T<^0PC0x]'BuU<$s> p"T<^0PC0x]'BuU<$s> p"T<^0PC0˧?4@Pp"Tv?~>IUaBPC0/ Ca BPC0߾ U U'o2T}iii=ǞONZb!,աj&jճg>l}bvO],_XW› TU1?dbN1VwXS)aOY` Iyi}O|mOyY mwo:}6H8 Bc~׾Wjc&y7׹\.9mzXWkvޞCH[ BPUCü$ꡡ:Χ/?+?[rvMP5: U'BU^VViPu4im#TU|:zRγw]忤F@ojWf:G~+uPuvYOaPp"Tjb|ͥ[[&0ocgȌ{)4>퉆=&8߬mljC*:NB4m^s{?oi[^NzNO[ڶՠp(W T^8TڝX|*P0zZk̓Dq/a HF8 =Ab>-'l^<ü$~ U'o2T%p鴽״<ٞ[l֞aS*U'o6T%)̫w25yk{kPRJ)VwSz|zPuw~Tk*PRJ)u_뗯`2@Y k T_w%T)RJսÓPS_2TW _BRJ)Z_ʃcP5|)ӼS{=5ͿǠSzXyJRJ)RW{ U٠ir_iT]Vߨϻ<ǐ;?>yQBٰ%T)RJ=qgjvAr<ָ\,>[lBUߪw @w^UJ)Rjuj ;O+BUkP_/_OK.m <&ct6}xuF%T)RJ=qwTjW_U.`[)T)RJսWƾB0X>}ۙE;.VۆPuyM}iUq6{y!T)RJAy~\!ص}zPj:0aC}<.r}j8׽Ee[*J)R!BPy?y=<>,SjlCX9 ƩRtӧ6r^jmV%T)RJ=qMRzPJRJ)z+J)RK*RJ'.JzRJ)+JRJ)z+J)RK*RJ'?ד @ X X +'K~{xusBX X +~v~5\?/88OD׼`*LDZb` XznG>!wG _M95K5(G+M^o41}4Ὧݑh##|"VOpxKo.rPHS %%؁k!s@AWLܟ OW'=<'*>95ظkVm ^1ɇNO>S|Gx Wgpſ1+ȳ`mb^1ƛN?mwb.ϝa{ wܟ  P.M( P.6#\lW 7zzEk (l@1@[l(YJlv,f @1&Q>gvv_|+xɓT۸_:uT\%~T>`3Fɂ1ymٲ%,,, 6Iv} xlƘD*OR\\BpP}\oϏm>|X~+SF׏ ` ،Q`mXGJ&o%M\`3FɂM2yuk|۶mձ05ו{~< ?pcO|RƤ vnnn@&ȸr7_/<"~d%~kFMpß y]pӧ&#{iJ4a 4 C#_Zx.x'͇wt}Tb@ X4 pKyopxž7-ww}oN F`]}8'3~_~p9^ɎGS;G{փ``(x*\O:o[;}go7<_ w'mVe|n vwnD`_`/;`'v1zMjzeRvۥ\@ {; }'hu퟇_wW8nys}G_bxD%KåJv Q{UxǪ~;̼hxoW#w^yפ&z'ҩeFDP}>x*\rt!pݍ/zma5G9//~uϕ/I&۪?T+N Xrio )_j/evTB\RkqԊv;_Wfj.IdW##G:Z(MxUjĨl'z\&i~ٕ]G=Y<"qV2Jv^3?v(s;V?_FDWb $Y[9'̸O]bI}"m+X#Xri^~0{3x%(FXD?.ŗCc  P.'2\-o?/_3_\wq(Fk\l@`3(1Rw_8p@K³ Lu60>lX >c+^#[^sû珅_yUrV>n喾XgvL_~g.{߸Gz<-|熟gE#7 1V+X xƅ Oe~q.LjO=N]n}Z{vTȹ{.ɟ+5\zc S. ND79]~j»w4 0&l`^N#CZqB`cl N]l "X`3 ש_ A߸)~|333aqq1cVj ،1ԥ$΅e E`3877ʵ+f~9Ĺm۶jr`Tl`G$:`U}:lݺ5۷`TlX`/Z A߸^ &ǏWN5'O.^Y;a+g~~%L3lѶhZ3L.6cV+LBHįNBzQ_p_u/Xm0JD$%YIO3yKr|rS;ڗ Ӌ 7keZ,`3j?qG_εI[gj6kBSgQ}M 6esr.՗?7lM2'*W=&W}mIN&3{ V뤘*WjGku&xufqv\8-?sêw"ڎ5QJ$`Z`=R ،1W`#V뿛+\UG!D&Il`,XFJ<צ6`6cK_ǻ/yyz-\';j>R}~`}ݫYJ8N;Sj 0 1%Xs?${#Y'׾`rUq}~bHr{Xl#ض}H} |ٽ{1)X%X77.WXB` ΋S9+SNUs&Y?fq VuC`3.rOnD՝uYrt}ojl+8V㶯 vlcL|Bm\baDRcܭ\l{͢z`s`3Fi%l@`3(1,@ ،`f  P.6c XrA \l@`3(1,@ ،`f  P.6c XrA \l@`3(1,ӧO[V`86c&XMMMZggg\IӸl(UM\XX[l5mk@ 1JĹm۶jZ,XEa il(UW]uUUsl޷o߆ @T?~vaVNԩSrۈN|5Wk-5dʬ/9Ǐa__ܯUsvnnʱ<>ƭ>֍mqj _7lvl(UZ4(rV 펼uJ&7IքrRssk3~;\SmSul>~~{ AdƋPgQ}M 6_unJsS(he^yS=6?3znط5`u_gucגˉ1ffxֹ-&"/'a $j>H:#S<%N)V{E扯+ac?sj{636c.X}kpڂR-ð=^=6z{l5:yUf'kP{r|]Nյ:M| m1ffk"1o[Hd//ԞauFA%.ϗ۵\Co_R jjӶ隔[wo|=JY=ַrz}SJ 6c$X[L,hZixq6.tl/,|5}5msM& /$_uU3G5f(g%$ngHr`3F __|^kcuu]'g_QIXB2G{fN:7|V爸nXPꕧ\@l(IH7xP3h˨I猳TykW:KH*ӱ+f%!5nXsWs/1JTu|]n/|'g_ Fo8:3g%Rg#+T>u/1J-ZRBu~Z ` j-_|^=q8J X޾}GvU2kM+'Q+Fs/Ժ>l`A4 -re٢Z-xe8뫕~{qUW暄ӶmΝ;ba c~VzOTn9Ssπ׽XKl(QF-tEOZDF[|Ejap{ E3ξ7]0 Q:Fg5 U^Yx|SZ~<׽XKl(YEN1l %`3`Dl@],\l@`3(1,@ ،`f  P.6c XrA \l@`3(1,@ ،`f  P.6c XrAB[333aqq1Y1J~!9X!Ql&YQY=@1J-[BL*t>n>}:lݺ;Ul\۲{`66cL`*&q _ bLul>lƘZ-qJi^]Z`66cL`U-MW۾Ωvk;8Il]G.UXkmRUrݧa}ToX]KZa^)qLqT5h cӼv'oamUW^O|m4ǺwIubWQn[olw94rx-mY[c [uNa]c]疺6FBmMZ`L9î~fԱA\joج ،Q`' - @ ،`f  P.6c XrA \l@`3(1,@ ،`f  P.6c XrA \l@`3(1,@ ،`f  P.6c X4333aqq1Y?*O[nM6c&؅e˖055¦S8R(\g % 3Ψr[$ZY9r^ %`3Ƥ֘vu`Ek:r ` ،1iENM5,&X!jӂgB2m,d{7,'Ïs|%ˍyDe9)I?qͫn\cs=rl<mcq5Ǐ/lVum0Y ،1W'Ԣ}"vVstMj禲x!*;y67[z\o:b^U:+SvϭJ~.B):|6c@ԍs;u?E||=fI_(b/)|(OҼT;k^g(O1yƵG,*yUFyv^wط5`㟙aԍm_KjlGjϯ.'0y ،6-Jj^zz6ysMu׼R:%V{j^6XW^yerbة6Oc<lƘDƋWjS]ӂ5ljة6y67wn15/Wvtlj gjosTcة6O񽉯=fI-Dm9aVou V6{\&inb\9Ƹ%|{s7XS?ϰSum_LFd`3$ V a 7K}Z~q/ն.7.o?M(%ƵzV_敚K] VSZR}ԍ*kvϸ @ ،1)չL ^P̷K~deMs%榯v޽,Gm}N.&qkظ*۶m[عsg?__5F,-1:O6}'t>;٦[{kt % 2Tj3[}Ԃ -V>OvZhCcqϫn\r39X\mT'9TS1îW K}ZMq߰9A46c XrA \l@`3(1,@ ،`f  P.6c XrA \l@`3(1V##G:Zۀ`3;XrA \l@`3(1`y(`n@`7@ X@ ,@y < Pv(`[[W\ 螝}LqE`7@,Xq*>$XEsB@ J᛿TN :X+6`SFn(Ui_b\`o±VlVn`7@*X-^}"Pǩv}@kQ`-t\'@yѤ4Gɍ "1]ƳlӸ֏*Kc9^藎8Gm*S߰7Eص\m^_W:/k Q`S;J5-Nj()9y/Ke~KDk9U:+S6_kkP4^ܟ]8}m f.v\.u}M'Gy vDbT6-EuܶI-W;ե΍^CJ /yΪ^ej<;{j.:m i}8:GK*wq}L:3@[ZR UJ,uu]5&u=qy݂]j-Wg5]U<]loXn8W}v\`#د}k1J-j~a°0%-kxYxm0ԉA]_Xem_7 (s~||!ض`vDib-- [QrSY}F.ϗ_k ޕkVꕧkPO7\S]}<6yGu@ J -DZb\-~%7EjA֧k}]Q'k7,_b敚K]F_۹1mOuq[W0 $-z)b_謟x%7v3vMgkg?a*/_=H]]{<7̬,u ӗ^Q>^\u:Ouq56 n(IE+&zIB| U~ssR![sD۹ x|#՟U4a?>׮)uW3{TW0 $@h}{| o*񙣿ׇ>5&ʼn,$,'ۓΣ}>酃`+)V텵{ɷu H#j ւeZ_ߑo7==`-+- }`\&.ʗF6!qd$F=l7kzU``=IMo!~vV֓ւMX^]M XXOZ V󏃻J\`cxMP"X(f*zxZQ㒫@ v/؍\l@`3j{XRkpl` P.6c XrA \l@`3(FnS P.Z\pE?{8_}_Xk(ߑ,1`f ( P.6#\lF,@ ، XrAA혝 333aqq1YA)M a˖-kݿM,lFlFJgQ}O[nY׵a%1W28@vuuDCc!XȤ vf ;F.;wimI,%YLv_b;Ro` 1r?o EvewߢoyK M`f'/X Jcic9M[q`ȅwW뮷l_*vTI:Is/ضm5l۶ 'N̅M/s'e/~|w) mT^۶`m@Á<:ssnWIWGf{y19 Tvדd:@۽~K, vzVtDd0᭤MJm5|8zhuG햻}O8qtOBNh;9es=p*_*H=5Uq4=^jgl߹פ g$6&׶-8hDa;. 熃a13o\u:Lk{۝Swi>i7=ᆪss`sÞNݜXޠ.$X{DDc`۶Iɴm[q6u0s='cz U]KL=7ێƮpf9vUwyW}.ۮ{_='F&;/WCԽ"볻wzmskeM[q(ؽ {Jd aOl;ݫRM܎Xv/Si|_TKcxƂm&6mV P{ñ`Uu V2f̐9 +{ XǺUoR-W}rb5v'XXbjcr{ñJnKL3`czznn ]/( #J^uBU"Z1K;c슧;2kBM6_?ݿKyU_&N!| P.+{)br"ؾH?mŰMK;՘D 9(;^n=VvE;UvdXk[~O,(F& `L XrAA`3`f ( P.6#\lF,@ ، XrAA29MD1  P.FzFxuڰq P.!Sy P.X9 pzk;6ƲiguƏc[6W?UtV宕`?ĽDz|_ [wJ7޸있lP*# XGzǫ7 䩼#>ł#;|W e}YݲeKXXXI֭[ӧJftF;U1(鰷Ғp0EU8ߕM`Uz_P2# Y,mB*]zB0q+l] v%`بlܤk#~vvv*ȋ%m^Q=>yd5΋O24mЮ|qA)ziY$stEUc/]+2pg'}6Y./{Ю7"]j'?a`G WY޴ԓ@ 6N//Q'XΪ碭T,1}Y^]o;_5ͥ_TXWL$[kgcI/')GV*%iM 6}Y#آ?С]Hв3#7ڱjWy{`vu*LL0,`uv634,^0v&ABkƴl]_k!XD `32)5II\' ` fdR+$(t^*߽{w_m6O{\y ،L`mi3iyV֯!99h/@ Hi%lF,@ ، XrAA`3`f ̈́|#(  P66ll&,@4 {! >Xyqöx;zgH}HylCwC*;yӹ;?}<]C%3`ZJfPEG}y76>ӿ`:T/-}}Vn  P.;S} +[wra`ѹͩ;޴?IOo_:ܮwG%(Xۉ c@*`Nؗ/1jיh;=Q6cBjo|7G/Lک#XiyyL:6 +DZ+f [^.%|[`9@j nC`+n  ւY#XjKR,oJ[Yx)Ҷ[ v}`ed.m韵#æʰݦo䛜Z Rb`JQKm7I./&(m*^wޣ cСc`륥?0G&uDkڕ ۡv5l/UVtv{7q hGl=tm\ZFj,@`ʥQtV (F& `L XrAA`3`f ( P.6#\lF,@.H~R;44x˯033ST@_?tIPt[o]9sPVTp>8r(-[~U6;;;Y8}tغukumV` }E8?r@WУ1]yBnI6U_``O4öOө\r˝nfI!XX)Z^TǏ} >ϝ\XWQzZЮXN M3_7Q>ưv<?p>`ܽ؇ܦI$ibw>G]Ou:WÏ_7'kK[y~Mcعoݷo_u 5~jjk8jαEwWNٞ<亼ṓ X麽$Ԯ4JDj~RmE<׺9l1엇|L.`=zw@y՛t:h.msKlhXL@Q`|^['ȸ<5Tیaֹ}Kȝ; ;+^`c&q6տmuh-?E[ e V66fmcmRc@hھ+9`^a_z?w w\;(r\V&ƊsSmSԍFmLji{`s$ؗW;x_ ;t{ ]"ش:ǻU V3.Oj^TݻIyO8i @kd.Sepۜ`Ee0kƴX e8lNauu@, pn{z_,-0^Jz_gMFk|jw$ a ؕ```3`W]96:6#\lF,@ ، XrAA`3`f ( P.6#\lF,@ ،@~GVGVGVGVGVTAfȔIENDB`MEGAcmd-2.5.2_Linux/contrib/docs/pics/webdavConnectToServerLinux.png000066400000000000000000001177671516543156300254600ustar00rootroot00000000000000PNG  IHDRK^N[sBIT|d pHYsttfxtEXtSoftwaregnome-screenshot> IDATxwt]}>6tA vDPeْf'W&o2iykL&kd^$b%;qBQ] DsO@(Rl'ܟs9;#],K \a;?4"RƬ[?=*1 HQǹ,l%ϯ-$ qzN}ԕ}4#ǿ{Qi H+/w5 Om%s7p!܌׷o'7ş_R_$}o^[r7Ӽ_߅VG>{`ŖFjakc.w#Dҍ|JºȏMqMW9z*b֊-4MCӴu]l|ϋSEW=LCrDWȄc2>DO>^ۅ7Gw)$H;4Ao}=ES_Ly %2ΕNOx0Z$BD~~8Ewoほ* tOD :; s K',G޹OyNvjfayK2䘘^'e֒-<:9(!y|4kDW8r%R=b;L?N`\F:t7Af4 vљX_]x%%3KdFO+* $!xm;ɖ PzdWQ(v'YIǃe4Xp)%RJt]fאO܈}t^knVqpfvEQEQ~ ,QWҲic٥ >`Pܙ>H9>((BA_}}=l~+ƗOb>y ? :\ijbGӶ yFIJ7 t4]>R횮cQEQEp4My./S\RJ4H4:)srF&3wd@ʛg|'H<," Adx`tdB X9|8.Llơ B(d((il֪T2y^Hn%t d (.."4=Y D"a# R hXAct o6Òh4B2fx8IAA@$t((SRdpr9<ϛ4t}k#_wg&H iJl뚲ј 4 30MAFFn'G2XA욚mhF2}[xM/EQN }iъSOɹOe[D7`%%%:թ3I@IArX&uXAIg| ٯ_'Ќprt@D5nҒM={EQf~0Yu[ Bojt$@H& [dƲQS= A8lSH&,k|Ch~ ɒd&} @$& ah\q\[/0,-}\ ͐E'"+dfEvg#f]D#)().ˇ@34rM(jt4!!NX((*RB?(e*cK)14M|97sLA F_ds|Kya^ߏs?„, o4G0MPYٔծ7lN91Iqa[=:ԌyWKyC4t 9MhDB:4a u |k Fb[RQEQ`44M%!O]s\,4ņi GF0 ir9 =4Hi7kY&7u{oؿe8 ~#OeAђ|2S p+BIdKLNhfg}C]vdԔ>Nv̙^H/ύpC&YB XZSFL%zhme(! #eX:FzhAIM>Lf gZY&cW667?Ɓ /&A5UTX/EE{R@fا r Gz$",OдTR'JdW.7ӝd iMd:ptxk.`Id. WĘ>%1–t\|d(2FJyMfi,@K ]aHɔ|Ƕ7 BFFćIaa!q㔏eթ:j&uF-4tB{bs4O>CO3"}LcPj+d ˈjn=y⩧x)$xy[cD\{iI ̹x,_AHx!=4c\ӏo`M] %a3t/uЖYU3ɑrbW%?\ G^ҹNʅl [VgFa%ixHP1g}n(JiXCYW`Y{ͱ!=i7Xez ^f:լ SZS~BFN N Raܼa,2~̎Ce xgCD]REQ,cXf0'N9]/#5 I&H))(($PBCt$ǧtOWWnk{5W4t7?BoF2$>RȈJfD}v󽬾/{WݸA˸e^k'ó 6d0.S- d0~3- =2^ 'NӐӖqWAw; 1ysWdV)'sOqԗ\ JG"=8u^UKG" XQYimyJנ0Diq Wb라>Y@h&hcSEFcRO<`t(p ՕL Gh8EڭuN2) 4|"0~=/ŽWlݽ-[ƪ3>zY^Y[,;c)%uhAP$B©k:SQQDg/m9tau6>ɤH]4&l[Z3f Ǣʯ=R pn5>^|aBݼ4L`R"HTɩoEQf$į֙P=z)6h:Ãq**ȧ1$e]7 ?v`Y/ut:CAAp8H$Oȶmjdk:[`ؒ?zee`9˥^Kfd |gn'0 ›T^@&!J0CgG;ݷqu4@CO; \haر(^!%  >Ano`f8).]+#a 76pA4_JK c*(MJɂ )..A>Kc_u￴rR9}Z)BLuu=L^6Z=uH%Ri0ͱ+߮ (,,4n:i d˺Oӭ uilk65nHx. B5LL pr>wBq2i|)) i䄅 @3!sﺣ+i~{9hSJ1MO긮;0Sk1[ǻϔ/uܜy-OxwsM)mS2X?OR˯yI`[-S,r,^S&to;j4?xrA7m@'Ma2n(\Oʀsò,lƲ,L0Ϻg _iSOg躎89 bأWj;ND"A"]'"9:HyEEEDPrz I$Rq,i ]-6+Q`W.;w%k\X!s| z~y,CqJv"ej`^]?* Lx2m2OME&a cxX5~ J5OpOr܊(r+8vcnڔr"]x/)F<~ IΤN SsoD%EQEQ~g*c [&) Bܷhmm60UEQEhb_ɂ>4T: fն((ON\65:3fTj8EQEQ?5io}6Kj((t`hxxҌҘ; EQEQ4c{EQEMQ EQE\͊U((ʝ6:`IQEQ;0fWhU((ʝi+LeEQE&׮jEQECM2`嚚SEQNAeHϒ(((gIQEQe Rm2t2MbDJ)+7 |aR$)(QkJy+T1_ : V)-`?JF((w &X 5q~n?"(vzZ8ȿW o;]$%*x+_7; tNz$}n.4\d)(+u{ H8e> =qY[SB&EQߋɦ&>v[?3d0slh>Z)]i躁INgqfX"Q"!]@L'% 00XMH l,+t\/iDؾ LF0uq]wK7t G2ms1G# epwz砠u.p>oP}9 \"Ws-^sM <fsOg {~G bY!޼٥t|Rgٹ5:tD7cT~g>kfZk9zS>j<3Q(ޭwKFҒv2gQU9-@dzy?⭓ҙ+7IqՔ IDAT?g$+g7Sv?ɳslkPT!)]R^ȬFdFjB fVPU"zc\!:c.mFv2#M>*YDCf6!֍8_qQP{Nд mf^8Iv͗^d^(NMU/GAGA~W81YZEMNz59t>‰ddʚjl DlHud~̞Y,!q2維&K0|;j[qN2֓Y #p1:e@IQEQs[c_Mm}2'_NǬXbW 9qtdl+Ӵ\<͹5̬I}V!1 3\rN42tݙUHtp1WS=c$Ǚ8^3,%9o;v_|}-w-fZ"ŋx ۘ؅eTn3ô59SjsjW2?>K`BDƦ^r*6=[Tnw8;ˣzg贜|z$ڊ/_奷pkQ^+UGJMB֞4 ZI-=W(ʝf3X{2B7RHCt^nJoO?ZG[w/b>M9إI*d7_[8aE]E (9iw>S4"+_C$ť!Rۣat g bBƼEkMuRԟn1JX9w )]7Зė(op 5+,Y@"|#$]0 +r# 3yʘp A"WH(m<Ɓ˒yH04&.fެ{P#-\勉B, tA02r_+3U((׻IьZMEʻl[?S\:}Jx9zĎ f•TG5L1|[)pDYBIs7GO0iu2VNea; KfQ"ft\'-i|Q+\=W23jLXlZ Љ\֧Xśg/dAR pS,Y GvGi5E~m-fAQ3DvpYL#>&T^nj (9L|N4JTteu PKjyx6ED]qRvό Eًװ~bUi͟# 6o ~0K\rՔr ݿvmwd*gQu#)^}+ƃks^NU8AzfR\^I}NL* rtt1fTP\^E=Yl*{{8EQE7jVdi̅˗H\)G+]̼Ew~l {[`^h<V@kC/[jޣ},bfV"k1<Zyo=>OfJdg(n˓<~oՓ$)zpAkZ}}a Bu MG[[9:9zԜ>xd3=ggvk w?RȢ [9noTvp^7v1WΤr< UVIQEbrow"B^ r%,k=6dfLG0f.ϾCghʀ((aD5@hJʙ90S œG~ 2\(GWgg5З0ŕX4q*jjD*JCoÌ9ͮeIt,mU{uFHu!g~9)Zi̜ Є@+\'޿}5"tHtf-ZɂHY%j(*QXS_:setЌEs[XMX߽yMnYRuYz2X&= +v/qs((w̓زy|~QEQEٹsB!Bmc&ea&iba}EQEQ,T((r*XREQE5Y'o,)(܄ EQEQAz*(r F> o[0ޗظ((wʓ|f>t$} 2IdA"A}ϊ(((!@hψ|dhZ8?ϸu?1݂?܇?ҏL'Ap5Pу(((G2W&!BaihE1*j0J?!v$ C3Z4iуzEQEQI*G222 MD7lC >1v Ԯš k4@K#]oJ,)( r4kE0k0K+9{ҏe,8FlSb((2g0ъʰg }l%E3MgF kwEQdl=ʹa(+"D6z(N~l/ e?)|'(bBA7|7&僥[I-XA kDztLGO1KGD?Sf{i/ DW-W`E \mdUq1-X$FUbVQ_4M7^}d]Ӎ|] Fne-Ag~~ QRQ- *k6 % C bh2rI!%Ahn`I/B2eBCC-Z\`d`'\=0L#? ;}K"JZwyb=(-EA ""Q>dap٘gO3hQdznՠ -rX2p: DqBw8ň@΄sENeb ]{έDA)&H{RO B 'vtXBsS>{.ф?~}?RBڈi93@оG'#Jc|KZOCT0rO~O/8X)I$ S۶Mee%~ uaȏmU@o 0Zz\7KΙ1b.㭠0 fm~䳘l'W ҹjUZ(7?x✹?S\ K !"yÿEFmþIRK;^$% 1} {bDȀd?"wM^h\JE>Zm6BO==O߹nPz mS? qԟ?azZݧ ?)4g'P8󞧱`=Nn]T,9|ȳd_dfc=}':ݻ 3nx%L 7_EІ;t&b>eW;7Z^K`m}}2G1}̷ ;_(A o\dχ{d~<^2BLP8^Ǐ|Ê-ºX?΂ۂެ)r_?9UUU|ߤ6,4MSڿ7!&:̓&Ʋ2ޥ;\.?1qC},dAr'Y+bo3[/yUY^?4fm Kv5 a.yK"w.1V} {o:hb?+a]^%#{&pd򌒰sǾo=²'rڼ[̉ @7;H" b} wb6PDї@Ђ؁dp.'z3f]H?Yy+Pf1VM8@!=,}Uܶ&­ ֚p;!Rq S{;^[X=]d(]rZΙC/` ;2:bZeu6Yѷmº{ rf @Bsf͘^&^qH D|?y2oOگ!EQ&500پ}M%Λf&eE ڍ=2CQ>kZD4 Nk UC/v=Yt"qÎaݽٺov1kiϮ=g',Ę3 ڶ%ha.sq]?GvHu">—s1_Y‹xէk@v#w ָ0b8/NS§⫄V;蚰(p1mΞ\}&׼Ƭ5 s~+ީilaLxԠt ֺ3aϸ~bJGqRiSkFD\kGvz'B0}##ڵRxft|obz/ k݈Wq+FzMx=EbOPza kN"ڥ#xma *7bα/G eބmG3sDo?z}6z a.C/BAkla@QN#rZ>.3!UKWz\O'1!ĬDZ,~ &mOp ׋sʄbZBL>2mq6VuǒAL9&{ua1q_z ox }rHmez zV݋V< uMLD ~eD\ oÞ=/sxv܃jC<~}ad`W`,ߊaۻy*Rx&Ɗ={7#"1ڊ6༿yi:z@3!ʗcVȮc?A"B/6wEo}km=#DnE=}wK9ٿ`lZQf .$wf1OcaFD;8Ǐ] e 1U ͯ7\ x;?b"vizm1t\rz71y1lbpw]p&z\d[CiBBBp`KDLĒv} |30*߅Lї~{N!!Ӑh"AO?QF:D3w߷pζ3b'~s "='KlaVĐXp.U~ob/.@~ʴ/oHTIQ~ JCCCo۶ )% .+Vo`Ň MiPZy'йD ?Ěx˖\74NbB}?;LsbZI1 6k!0q W c,={Z ޝq}q ˖dK%˖-yЮޱGDlLĮ71YL8$ %KKI$ >誮++T})R'PwVe,/Տ.L,'k^v\IG`#8s=ԏyp"wlDP]SH;._LD0Jv5%L؆֡!e.f@h>O-'qzOSX;x;v׈Z}AWe7ƌ">p7f7/M16M| ƛh5i}oHa5V~Z WqP̮߇=4~|#o,laPyۿug7k߮:Nз1%=- +Ͽ>mZvGCۗA铫uSiCǰzxgGmp\@kFouh+a+8yhG>st g>q]8 bqVd5Lb%?>Ihƈ/39b>Xmb_FFqe׈96Wqf X} ޻pHkԞ~$]h-ɮEyO( _Af'B3% =3Q|~T;bdub `U'X[q]1vi _|"uIүF@Po >)֧+AmbZZ-` "+u+F?34VlVV26V59ȉcԞoG;n]{Ra=;nF'tX<z}&GHn:@? _$2u(G|[ny߾/J\.s뭷 GƌH$^VUZ_M]h[5`i݁N0!!ݻOF;;Om#gYj`u9'eߠuN~T_=yRbFs ~q~ՅH'N=C9tJm "%|!+9IZcmHz܅نп|@ 0}??4 E{0]&=ϿE`61!GM~GWzYڏ2ZoYބӋ.R_ߣbbxߥk>@%t)JϞ^>̻HP} /o@啉b'w7($$[A('^I h}ߦrd?żoί=ug;މ'G ;.~VM&Z."/Ϭ/|"B d}}.} wwctyA(\{ cDWwu(-6C-}d/[#Q ;Eyt]g׮]ٟO<Ǐu[MMM:t~۷??$^- Ϸx}l~ Y^FoE LsE @c6L WE=bl.^Y [#hV7,C^c_>unC= m1 !xSwo7@O6 hhrZ>S  "KU<~-xAdŀY9GX>\;w70S Uܙi|o)O sT5iP I uS[ $}s DWP ]ȱWNкnǺwqOUzE 2hQ]buSOw~sq`4b1/"O1o8Ulże|wWtzыT=;c OBN?]X|mġ};>F`"S8u߁Vř}f >>@%]diN5ifauDԕ\ jsxY=u+ze5,^ICGǟ>W@u.)_4ǏSҗt/"/x[{m ⤃8;fV3-v^i ԉֵ]."}nş8p֭EĊ~pe9z#M 8'o؋`嵹 ZgZC4rm+^wW2@HKkz Zтj i1~恛`ΥKx4} O {1wge-TvI8;-̗;#=y7߁~څ3o 0%~'؝ /P{nZ{1'75{Oa'@]׾}nDSjoj1waO~{i B|7ҫ3Gp`mCoG ,άD~ x;:P<}>·qvKW7?#jC02~]4ΡW[Ŀ4ήs/b;KG^YMVfq/<wގz}sebxK'OfkȪ F4WŸƼt>ދ5M-w(/v _}}}T*.[imm%\rY툐I˧WF_iWzQCfNa?X36K>!cV7SwǖVj6cM,bd\;6ttw Yە%c8:+s%"oP-u#K5d g\\_cV[97ǯTcGO }ΡE |Ñ7?1Yë!sxS+#3'ىC57jb0(״{nv^XDB71;>xꩧ} A tJd݈j ?}WަuKkLpMY\oa!M}̦ ڞ"| _i?`dM߹El߈Hw!"S[zw0Ey796աרd}_<L`0H0$`&ea0B[nJһ߅Ҏw(`3ZDa~ZJVFDeov(ue^y%mb=Q=JO* (bR"W~qo>X2L-VƽN_6Èh?"^z,D(z?`_?̈́Pp>{ݬ$ ?v4pZ8HnqBEQ>l|__%-/-ᕖ"J°Mo]lBkظX߇!bo`"v(ʻ·+<"N+o2XR'pgF-L"AXB耨F-6NQEQ-2H#]Y-gp}7"ߡ7ݳ$uk%z(B4EQE>ҿ0N R«C6\wq%) ĒRZF:5#((!veI52Fևhx%x3x~} 8܅Ѣ&SEQ׫^$1;(ҕz((T׷((ʇ EQEQ%EQEQ-`IQEQe *XREQEق ~-$SvBjؕ|.̱\񯾇wrs,q>뒽;$]\Pm(k_ 4{xN[gntr4k(Gpreu)qJ\:|)RXdvbYڹϧ0wXEQ>=OK)BLV&,.eXväILn t 05F̷>@$n1C}F݊((ozRJ^"3s$ r5.:AA!&Kes%ؒHצFSDdi9D;t M`NEQ=--6m6#]*.Vb%[[&s5e FC2QEQޣrYA(as͎07;G1v]鍪LaHu{[uT~5 " a[g'ѐ<=Ђ)| SC~z:;z ^"\:f>O-7,8#HdD&W(IHs?݄tA'%CzvtvjEJ@h[w,M1;1AT+Fr:ReLJ[ezdrOXNz1q#ORq=0n.i|$wս1uRʼnUGbR4wڔY#?q9=7-Xev0I$IS+QR4=dCh"-ߌWYNO07=r4ܵm- tK,͍273K1#v6ks08 QhoҡxK;ѳ]T.0<@#؅QƇ)VfLlD$=oGJ)sO0<4BYD74c_f|6'%SM2?3M0kJW:q VXPvftB)I,,:xVdaYus(fpx JL LWJ5IP)-Q4DjϘogI|+,bm}t6H s΁!:SXhd1급; ędgH$ z[&;vjh7 1yӓS5I"nTF#;c :hniZ "gS)dg5|\䉽f h5xv=LbQX-K&#'D^a鑱7~[U^!%WCjjf&V =.:~0?F>;OnamDVfIsjsK'i ih{Lߤ݅fbZv 0׮VY1bK $Q!2~rVB:7Tas/y zٽ'z lDH蘆9.1+;w|a+)kBGֶ~hPExgpȑ/SRgkNg0GHDMEㄯD BH$S{KDXXun:hjn&KD>KB1" \ 'vnF|jp^ 5HB 7AWwHXC.ǭɧЃ!B īr<ٔ DtMNeRa3 <r/ @0LȨR9hVdyBAt!2|]ע$:M.|r&곡M4{̲zWQY2If:H5K,svZډ瘞&ۘ H#/2E !S` ׬;zl/)LYe\/0GbH?N"&_b)b84fb; džLY:$#K#d*4켃d@C'Ib,y }܃3tyEDb XZV)q=Ѱm]Vc3Iks 2>a{خan|fEI%W#ק;Wdy֭A";hojԠC ƉdLe_F8Es_?-zJGS`v!.)ΐ4&~yLFuն'ս0;? zΛڞZ0E[>!cVƞlkrՏZ4\m 2;=RjhVh7(_"i"jh$h sn4[# WGpjF6$_$=> `M=;hoNb+вd'ȷt'iݾfnaRNJRoqj6/BÈwLXDbGs"̖| 3GshQo!TfNQSO:7HufbOh{.Rhi+:8fZB\R_#nwp\bbR0 zfc^ ו@}2E)<44^wzݴ58ȥ!ضzIh躁e)R{!!e==ϽZP[?&]^{bb7 8⺫[kxòVs<_0z/CJ 4E}ȚnhB[?jDs0Vib[iڳQ}s#Eo{ڞ%G3,tmawЫʽͥնv\00Ma ]OjyuJq1L}M _ ˜|nEͫs>[iah)}]Zn.n'<bgJ} !V&?ЯK8?D7њPR*̳ϑdAibYi^1 , f`Zo2!4tBaX\[_fUVu KalhF` 4#f<Vڣ@pBnmHRxN&[cuFTB Tt  ^~uA"ڳ"4GΌ@8m={0'Y!ˍzO9Ϟef1GFց[xg]gȫOCGF#ydOtJ,,0!{O}{wba6SDVRp0x{$*f{ n8u_Qf0HÿQß<\[>|`AlO}$x{y^; =BE#־;xw>ŋ/8bۍ?h 2y?DT(l+K3!Z;;s[éR?\ NMpyxgn"(/xaAs6]7݅KgO@ cph}www &e.>#yH)4Q.-×.|\ͤFCd6b )ssX%B,cÌSc䃦cڨ XcI)9ہ%̜>Υ4{PQz;L122'O=0@XTŏёa 544aI_|o.h; ދSEQEy .dx_aT(q?yl~w9pxi /6v&mN?u1"^nۑīt$-sSEx/}7٩Oҡ_pre=]])Ew=|ߤ ylcy^Աzz#߻kM0b=gxbO-x{Xg浗xIf0{(KOyO<ћvX/zG~$ANbr̼6D.6z#M<4ϟaKpo.vz;ϝس[Z%>fGi5d%^g>[oQzB62o `IQEQ6+Kw@́=-aN\R%FM@ԷWXȐ]xL{?ƟGhV^@YJ5EV^ L-}rC$$犸k ڻfU N83:K:⑧:q?9~#]MN3A =p -/|%ކZ8x]ܸhbgo-^4sL5>oF.вRD>=ndX\(⯩87]z \ٕH<2T2L/,ѵf*((p..hne{(85<<^)CAc; c#w{߃b趃j P@>v=-;zoK9uNpf"JkoP] =w#|mEWբmg?m-']Y&-ع#^dٮ,׵ipo^u"ޤJ$"e,˲Ǩg{'fw؈}5jb_̛ݍݍ-lH- |E@RI%a Teef=U`Ϗ$i9|/VM&8{=^dTbئ(\ؼX]2<{.&;i[EzZr]?:HQݾ{VWIN5Mβ9KN%PLhlo$Q s2oI&1\+"B81ȟ-#FŖ(8;P57by. ē1, X[?kkDoq۰? N"?\$^i=Iֲ5T/ \1fnM{wn$Lsy`F6>?g=My=ct}F9?ȤIc`I&x%vO[š!*|wi.9+N"B*LqINP6O?N_Ȕttl q)2͛ hvQ ,k.xn!HyLNNQ[SsKl+, }hhhbqKymihh`fz"|T*444H$p{v>۬1aHX>㌎RQYwY’ܸ01aH<SPeH$du(4X$Ũ(w/smۥ!CCCضMX\aUڳޖ$""7`mm5YV)d.ǘR$vP.ñm|Ƕm Vۮ'0), 3a8wWa3\< w[Ҵ6~/P} (w)}Y]>Ҁ Ks Lm4>LDDnf~uG e:YO'Xd\YZ؞_嶽qv9,Z2*K""r ʭ$9|/Gs%s;O<$w>i?[䦧EFI<mI>O\`l そ<>k?y]?gY~>mU^ d3ta\1`MT3WDD䦙e% {_xB.폞e[O-Á1fj#c$*jij!XA&|ЀƩi6S㓛Oh$+ik1$c#Lĩi&kd]EŗyJo˵!̏\W=uO>! cS|t ~ W),X:?3$3<~hrKT&VLath<咨T̙[& 2_fb: ٓIc91*VR}muGTSX7Vr2g'8n>'ɽqLH*̾gS,[6Sw6L 76)qڶ< w4' :{7b,6s|q.g&FӺ淿= sJ+7x 8ŁtÄai6dEi }W{b1z?'wn1mLOSc$[O>7Ӕ2xy~'2Ʒuz6'9k<78{,kdݫOq<8.ɖyϞ a^ƩhfS<؈ +lk-L\Hӓ)""_L `V-R s8az$h躓]ŋNW[oӟQf|}烬J=w]Ϛ6NiQv43$Sxy~Eڶ=:yk/,>6U[YVZMMA.=ͪmg _sW2||/sc%@4}'9w{ uMm<̏ޚ$?qxvbk܆]|ݤ|g_|zdī'/pev??XOj ?fyCfyӾ]du [ɝ>? ?ƚpnmrmd2\)TЖ ,3@:owyTyM]f7~w#ܻձw<d,r>Kd6Vwum$Ɵ-?҅!rB78/܅!8[ W?zw&Ƙ<Hѱv#w*r>{5OdϤn8y3#/t"M?yoa. zy5<o:2T@q|s5l\Csڹe!?u2e?w\I:7Ja 3}vSKlsn -;4zx ahputm#8:Ih ahj՛!\M&66`ǩnɝ&8qab>Û?k޷Jdt&˕)r#gQl{^~MaIDDnYfpbU4W8L@kg"#g?7?8ZgVUQ, !-s-&䍇Ű1~|#]IOoszrUEw]KnrH'SĦ<5t!2ym˄E&p{nmƲpb)b/샮>;vpb,sXad;=$ڱےtm%e2E?y' ̆ +,MnMU~#džXcb94]϶ggk&f- ūUʇڹ/^IbfjlЎ̻7^Cmܦ'wа`$9 M0^|KMS#-{~J;iY|T&CLd2M1 L\"N`-ц==y̕ij.xwZ؎M0vvRٲ-,+b^w=:r,v]o7Y=Þ/2ɒe3L 24U 4鵅mYXm[ !H!f .u ll‘p\|.G.[ Q Al,cJv _0)%;3(}mJqit|.ü)jY {~Wؼ5<_89|懝[){~qn_')ӃN3ld=WR=ܻqe~նV]>G__Ac";1Iy_N,IC_ c?ⷿoSk\:6kS;v>ttuZۚ.3uom~ k:]u#gϐȮGc}5f ]wsc7coՐ"L7I_uok?˪B1͝;un=·0χOo=K1kVRXXAs{ٱՋsyihl7x8ukظ[{bk\L{g=V6430NH`ū^Rv@._${kmJVX?uE<̷yxCN9'/s9;,i&gZz+ S}m)0)&&X [7P33dN5Y@ʵguXiEHִзUkXۜWf8Fz\yM' C.]\t9.85_mc^ -""OWL24ƲۘLgz?ݯZ1;WL2<>N<'Hg뚳4[DDn qp]\.Gee+ǚ݄!L]dl64㤒I)uOnb1.ДgXdll|.,y/EaIDDnX,ömvRQ94 ۶appe^,FE:e ͖*Kc)WA呯,S uu ЄE .гk-&ͯ.-Mp""rǡSOwl&jkk<۶ql۱Vt!AX_hjzaFGGhkmbǻK]m-/0qeT*]htgc(LCMu5+<`{\HWaIDDn񣶶;la`p|zl6K.'~Ճl5i6Pҳ^<޲@]m_{_E&d2䲹gfԱPUUIGg]mR)Xam{tj|6{`I$twuy5VYe,,b} /q;9fWLջʒܴ2&Cܯ]"?* -D)vPrյI@DDDn;ˌQZ*8),mDzXrRVTۍe˞8<]Zmbĥ--2NDDDn;az};ͪ*""" Z.0.ʒv셕$"""3\s1\t 0 À 1! Eߍ1&t_ S}ඒ)Kg7)-7}42 ZWS2p9Ke{)%,(dLz˼W3 @M3MuxzѿҲ .8Cj\ ,]TUT`z$'StQ;wLֶP( 3zz5M0͹2B3woO]uҽjv[70T*qlũA>=7Bu5ui`qC'G@_S6>re*LgNr>[æ $emK."9Y\vlrܒſtE b)E } (0y6@yqxp0v5vo}5ik} 'fUj5B&OۇFiڣјɟRqjPZ{\_Ȑ:'ʝkI._4cçxW/2ڶo&a[M<訴 zO†p㯾ț;ޟG6s|VvƸ?v6?؂;3xwG:2<82um~Yx{;j Cxqw?#W~6!)}}X^~ȏy掚뺔_f믲oGNoC8!kq;q)j=Ɏ m$#0kwkS<ׄgL ?$yZ  //.=j}oyeo c;7~͋Gf}GW}4΃dko/x[ܿkwũIu [0{1IZfw&ΧaC{'8 ןAgjgx7z?;HuyƟ_ӏS?Wky_#Y_]N[Nsx}~֎W^n_\řAshޗ8s?88x7k;?0Ff7ԓ p϶m_fo[>,Ye؎[ȱWv]ES\` q۲յkYY^)/Uzm SqҜX#'8zʦ1^dY.\"[]up]Bw6uZ}ٷ?ɚfǹ<488G p XD*05rvsmؓ<=DV1[igvN-I gG{EXNǽC qYb-ĒUV(YO!;RQSC2^M눿|S4ORiM \̼m:8S~^_4}qX`ai6(~-{0_Y .qd+ ۶\5) ùu, J`kXVc08>F&tI׭oM/uOP]*9Y:;L1nl\Ӻd ̏r>Bxe3=kݜ,Zye<咮ooM/mIa0c`LMر-kH:Rtl89C~jGOpqdMQ˺UT'=ey{?޷9HMgs5!̱-(?6qR6^-s>:+h꤭zy.1q;{DE-cP{9BL+됨WRUJê R SǎwWҰN.8z8):E]._{y{7IVN~qܹVַpvw>3lҁ H63C8Zy$cu&/E~O/yal]\<B1K?/5)ٳ KQUx *X) so<6nKTF_·5דfǯYömwӔ?z~fI SԱ\z`ƣع~a:AU% 9?FML3u-^wL [{jU + Ba2!u KO0kMT8d7PLR\gd7rm P ^,Fswń>\mܲ]ԂB@>_BeR/R(KcQMK$y ~q%I$INc1UUх^0Y> ;<ޠ38 _>HIJJ o뺡Ӑ$I$It h(*@AHlze[jT\vm۶'0npdS$I$d9.Jxxx#FtX|XQmh]Vl&Ո1l2'8(BcK$I$I [LX-UG6=`+Iukn35!N"'q(^/͆Ҷ~^QQz-ՈPl-*rRZZChfaZQUQ$I$I:~u͂TgQXTTڰ Z@^uCY㚕R>x(b Ԁz47p%ɋ(>/,&#Ql /-!p\xXPUEQp{( qqج6JYi*'L;iBhCFVcb,35r%0BihgbP TBJ(]0G|cơ5sbH_`1aa Qa-](޷wlp=Eb _v]Efz9z(DȾ_:vvV J\(%$}ѩ}K ܨ9z85k7PPDGjktn*$I'C=b"BixAh 4Ϧ%d\207^Q 0m\O׋vѥ`h {]CI_Dc1!LRu)`1K} g-(?!p8`@Q!PHDdJ >w)WsCtL)/`?s\vلYm,⟮ s?\'ӿsZJ XϢ߶2tytk3ºeӲE^#YN##1*‚|]lPI<SoN4_lhЊUW 4КOl1cc]tZzD $XYQUӭݍFӿFo[i>n>OrUU %/?K^ Il[+Κɹ.au76y Y\l q\>ߦ9(+7Qu-NIfݺ9Nœt}l \Ͱ3{c3 `sIjDNN.I_lh*;ż~-n #( PE~Ak VI%'Bp,x#W*l3҉(}8J0v#&3bX((* >6^Hҟ.懯PVj-= eCFKӸ(ˬ{ Aň>2qP:%N&-HJlURġT,M1S=EѶm[b#nN$,)Zi.jDӡ]["BmǪk^fs@*-[Z3tpA2QU$B##;wCAj^*'&>2ʵ=IN.]l <1v@-,AQ4ML"e"!5gH)ǘ+8,Le>:1Wi,tӦuQak#t/#afhh {T,͛&TtpjfڷKb2vs~Fה6[n/q85P8x ZÙJW&4Q[Yۈf?+!f@TIGAamgZ@h^ *K⿉)YP!R?*Ep$o+YzSe-סLHHr)9y4i?;bR+OҲCnzyZae3rK*ca+՗p)eUFt!z@Sx=^J!7'f?ZFsg* GR2(s أ yi{pg,mh-QTG4+m1Wٲu#߭HQQ #P g„qnj5s!'e ~RmMV޸O?_Ws>eݴ1̏Ya'8ۦ]ݠw_uKnr&^7+qշpNN#^ vyq6Y?nbe0W;"""):̧?gz.c&_K~D5k d0QDLs:5bk5\?cY9XG"!2x='IST[i.:6 _ !|ƒk0WeYsuUU( t(϶>Π}lYCK/gM㹪__OD`= u7^2H+s͙dt] t0[$avsžm!$V^/DGb6,MVQ!N;6'RSyJ)).(KaNͺ E|dE&{ c/_.zut (it&VRi4E DThթߞ,vmĆM[HLcmLʍMȣhneKl^'Nb&:h#«O}he__V3f`W9ܲئ:G>2Qܬ]me#uR7Eq?Gkx1QM4ƅ55e; :!Ћ8|Bjڍ/Ċ睑jgF\~z+UOd2zUFMiZKt:H2,+@hllTՀd«j(׭27%$E/v>qtߍ6mPv zZ J؍*.l\c: 0b-euDŹK,o& h`{}@+U/@iזB5ż|6kFzWm ؑp-cU**,>B &=W`(*.bz`TQN'fHҟ%4.03O?MnG!K~Mn悋&x8_DbHW) ,ʖ[\ٲ}AkD4OT9F]LfR g5A7||JTTFUKtl EA8.UQqrAt黷|$N=z,䍪Ң]\btмu/~acAtynXМUj:U~ݺbn6ی m}F 7!U%EW߭+BաIw}@ 4+zRDՇQu0 BP(;x(lϠň/vfR)D)Ci`)~-!x)!*VJfwnZt¢z@u-.d#< 1d=X,B,!DA+ I]jaүX]v&.2#bMjIV~vJlׁOշ?Td+{W>C>NïZ90H|r5BjD):)e&XddCᰑműS}=ԟqUT*_ tœ0AJTdE(RY=p(|>d4iOQM3a2Ggٽs Z杻Dp53h} ƚ x0t ,_L;#kFPiX:eˁr*@ca;+Fq^&!m-[ED$$ijOyOrˈ ~Y5%p(6?f[v/7*!:5o zӻ-:tgȈ,w"cR{Zc5nE9JD PZ]ph:(XV ZEib0TZ 4]E\ ]GtTcNtH\}JPL& c Єh2US:>P1aB]x|>!V+Uʮw|^.7BQ0-MΧ [U|> ﳞu蚆IV.XC~#ű˪=6Wd@~M-&ڴÙR\ 3, IX|XSfktٮLO>h.P ` CXY9JTU%22ׇ&#vtll@hxQU{O+J^k9U5ԽrJEj^`&4He C뫖5U0[+ ̯w"4MCn \x!`׳!u(?@~I?Bsm˂> @OEX",'J lI[IFTN$I$I?@m`C+}@+*B$I$I?D`BrfJj 8kV /PI$IAHi\6U)։%I$I403g>=s$I$IB|>4l\PmAj@y@+n[9$I$Ij+S> *}i3$I$Imߦ9 Z6iH" -|$I$IT#Bո1렂f@z+NI$I$鯯 $@Nl]Ѡ$I$IאHiyyAk*I$I$5`jVhyQO$I$I?H4cȠBPI$IOp>2$I$I ȀYx M@_u$I$I X`S1`#/I$IV[̀BѮ$I$I7UC?T&XF4p{PI$I$뚐L`2j穥(dJ햣7Mд ?m?/RRYOkqzk.#I$IԈvkYm1dj +OQ*O]͐xŌ;w4\z ߬_-*:[.%7|wG6~ˋ>Ρbovg|B׸+I$Iy@cAa:e'7{~J /)>yrLz4Ci[Xo+7Rl#ں)E2~pyd\I$INJ1`>C+/Xr2E{X%Qnn jDΣwl?vgѽI+tdžM)vz'з_ņHOɁ&vǐ^ IDATБ>Ma"?+~HRk$ͫr0!ܓng¤Up07#=bP˪DN"}Τ+RB<[wQQhҬ5vqhN6nݍ[7дMGުvfo^>@X \l߼}`жcould!(-bӦ-ebFЦCtHl4k^2%yn tލa(PTT-" A’$I7WׄT6@)(SFQ (@^r\YCQ f^x5-,͋ygn՞-}He|*Ys?{b}Lak@6d9B\.L#}-?tvا-)`gV!-ZٟZB~2{زr!/C>MuxoI$IN#uLD_9Y۽߃L<5L֏쯾elBbgGQm;[=t :ژ*\:s$Ojǚ/fNj4cLf3:GRW?aۮdf\'/g0~ 9\Zdъ3؞ŒaϞ=, KUpz漱7v'dޱJUjG g^:۶7S~\ǹ7>={ؾ~)c xaoyu\;V/nV:zMNi}^C*fCTw$55w$I$I5o(A>+plҟ2bGex|uu\~u<<|P}n&|'Fia$T%oUjڮ37OVMӲ#C[9R}vK$YXgŖxаĴe9q;Vcs.4.<ř,qO&&HoQ)=z Z.7huwMEỸl]̹W!]g:[ mi4P _Yg9g!%H:xĞFL?>-"p|uy/ՏF$I }@QjYN e z/Sy'$KxSn݁8]V1Dud^b/eb?fQDswٖJ_ȿM8sqrS0U׼$L@l_m+Xy3B{/}iaGJ.] |Nl._5Ыwmr_ <)!t9{(:\B[c=gzV^)4m [d\=y6^Wpٶ #?a+5nbGӳ_|C~$I$u4X_ת< ضk- etfK3.lۅ&+prHa5HTEm;9xC;ŠR f3yGsl֭cۖٛ0Es֐a|2o96b4FY8}T# Ye^Ju \AY(LV Lώ;ٲjhm(];I޵CӮݸjPR=$I$IAa}A+OhAr?H)B:g{R2qB|VD'X6_|ݺwuS~˺Z UBqcX|2W.$Iuu ڤV5접)4>ѽZqR9 ٲanSOƞ{]íKgUJ?75e*JGq҇t/jd9kw,+70:L&3#:y3ùeLt%Afٔ{/Ӯļ;okfdJf& Ed,%_FٽE?8xp͌z{1>vè9jO^}YƠ^eu=DXssbՊqxZR 0KS<;hG%J$IT 8RE8Ƹk>z2Z"tdGdP(ŇA^^Ax;juAٖ={q4,ygzVw *s#0x BUoc= 1ñI0瑑SﻞP# ũuM߮g` mkvK&BYB\B,> 1[C.⥇jaD̴u6hJjat$& 큩 HƎ?\Rs ! n^Fbf›CLO#+ǁ1_Գ1:t4Z8K\ԇy~cPCc.!%I$I)nݺv@XYWJNrZ-hUTҩSn't !t\R^ `ncPiф6J k^Šb2+2'EGh0$t|>D㟪n*4</(fSYc51+z=:M& X$IW_}'|r*\2`џ>[hŔD5h4cl_`b8uSMrMڷXj>`b^I$IVMD_yRH55WٗO$I$I${T|A7$I$It:eAk>hR$I$IP5a f@ %I$IH`YWs|}*f,$I$I?U]_2$I$I?S]`ek'(xI$I2_Ey*$I$IpeId@'v(ow BtJp]U+-ÕW^{ǰa}`!]׏~! g'ǻd{Gzz:s믿fܸq\s5$$$P}1 }-NPmK $I$Im?Ju]g<#1a&MDRRfbj={_pBRSSyO@?k}_}-8)=ʎտcM@v-9%:PFFblT أbZ߇$/ۇ)$ ;(bEaW:ywCHT [Xy8m%Gm<K-mW>6oCi< IKw%==+ Z%4Ӈ֭[cZ?>SN=n 3ШVUw_ɢ_н!u>\)j3);)v4~ }/[B_+̯G!trS1+mD_~~3♩8tQ-Yr'^|^u@dPS߮~]|}<7c;pӘ,eow(>/}67-:q̸>2Jj(B#z>zB_6KАF͙-S-*TG壹wr_yM<7grvwNG|T>RV}QN\7{/e!(=]8Ͽ_&w>pYuww⻕;qg} JدNy<<7w\u?yk?moO󟿁i;Okdm qlܛ^mȤ>txl(Mg<[m-7]'9E#1ڲ(I]ʌK)H+,!ysqQ7|!J}"`} ׍ě]ϫV۷#m9_=\Þ#EUiKWpwR}xtO!?['['sq*?w"u ,976~mS>;$8!֭c;SV >)Btt4v /`ΝA:8ΠKJJ(**:)wj'"anjF(:E߃p#Jq=h6:),J‘ Y>2|_P&< {rXt>3lcȡ߰Wκ=hb∈=ǜFʎdҎ\C+֣\ːK«.Gw&ױrSr'3gvw=NlE>xcY9JZ;wSPЀ_+,gv%ñè<~ {vnpznՋ'ټz%%yʟҴȭ,߶n;K&l^s&Vdl[K~B"2\LyYˬzؿ{˾f_AT_ti%$r`w{E JE0ނ&{9DC0r<}6U~pX@"|c͙L0vd#/c8>]G>f f FN"x7+xVﭴoe_St;Չ[2dW|,,뷦pQ4 欉DY[r} 2.b{)w&V:ʗoWW377'xW^y* xꩧxzO?'q\qcovмdw4Gydv đ .q;4(-̥07Y"X5 muE)lþe_`nс6۲yH+3tt;~]?ᶧ7eΆ% +.?|KxŏyAWFѿ*{g9 ኧgf`.IaΓ -x O>SKݛsfAع &9v[]i< eWϤflgKH.DFruno%" q޸BXvMU} d8x4އk7q~km}Nَ،hq So|SN0G-{r>23jc̝2fHW6/YCt;ĤgI,˚G?ڙ}Zv㓜?;糾透u=_rPk|Eb-|]$q4o?Ah{^}XPC?\| ,_ħ|˫[>Ǣ4~2FUՁk{?x [YO!f05onNgݴ\sXǂ2WVfӫfv4 ʲBP{ .wwuv:h]*y=fRS@3ngĹi߱UE:9e!?' s YH^vyx}5-4EfJ qt JJJgI(((`˖-ݛVZU[_>8K.4i҄uUkƁOx 55ӧh":t:f@kT>qcr $-f>kq}X"[21+Kx7ٹiEnJb>Lv#]렰o^M3{614y/qY_?z5!QC(38HIҤa{ظ-hZ%맥{X%hͶ_Rr}ϓhȯ?]VԼ3P p u,lt] @ՌH,]AU9rnlo`܈ޘ:373ɿrmN!<JqLJbm!(-/>ę$6y\SˌUSӷ9ъfC/NލE'{(c9O>2qݹz3l}eۂ&ӼvgP7q3b>9",hZvV]HZ]OH3z\p VaO_3K{;g39kR{ߞood0D-cxGrѴ?j8w|` ȃ7fyw&n/zݒI7_Lc IٔD[ EYWpx䋅F[$\}/\ˆfC`GR`>NӈwbN -$vPů0w7>{b &;]ιcz2c'^Mּ7Snm4~A&ezO!g a>a?K<0$HnrWX N7ٻ(ʽߙٞIB( *"p_D IDAT *vQTĂ]+{bT6z'{~<Gޙ}{p?1f"kyWoݐ7٦>[,9YGXtW[5bI* w>{{} {Hj!R-KevDGDTzIq6~_Mc?zN` s@~}oV!\3LD c݋xxg/%W'At`6z>wsu^u\3FDAo)/ƌ"CP?5&lS`W̛CT۹!-[%q?F= G李}/k\c&UDrY]:K~viT[(ʖ|)G=;Ov-f#~;%+#!̊i\§}{AW^'όeדޭ3u#L(W;4b$}Dklw5SJ*pA?2bEh' 6p\ŷ4Kߎ'EY qTܫ:Y+0fK46kmĄ*0)ǔU݌h Ბ/V ʬO%( W Y6Aޥ|>kwp W|‚KhF7CqVu@)7uwg׮\4o_qٹ3 AVӎw >ǜϧ۔oۉgtXEKNEޮQpD7g=fO mItey].gI@ݪ8)?!v>('}5 K툠*.괘"Sǀ`c'v TnEsr,~ WX; Ϭt!&"l3 3UBl?$yq%=ML|` k,%3MF]iZ?]K>WcW1<٬:ּ1u!e/5Ky`d?̊O̙1-_}E[L>Ԧw>W?bßᶛjعro?}?*ys[fOdкy=lAc]Vw{y0L& {&u^{-^{ ;NZl.vXۇT ( mڴ8@~~>u=iU͛OCqA5jSO=EBBBM3pQԠZڽ-Z̘*Z`ۺxUPTEUʛQU Ijhh&*(4ҏ+^jBҢ-\٦%yp tPKc{yaқDUٳc%;"hRܾWD.5|:ۇU}L',Gw!u7EM<,NQ cW0@hӝCUJ_! &[ym7:k={ȥс'o(¤_#<$+\!73#waAna@l8ٶ+lZQ^ dqtKk#ӬqIk IkTZUeU=EHNmOHXiwiahҸb˯!Q票*%"vmt-s`-,j"OnFL| &S_;tٚ=fpCDWlRBX.{+"C6',"ͺ U-Z}#*V/f֮݉Ay_LikB3P5]Kr Vɵ1kX:_ bC90o!9UT;m\[s9PMV:\I^=(J%Y|WL-ԫ?ނLCjݒ(ڹTXr}.r'uL2IJjj*;gPEQhԨ.}̤C\fYMoy7hٲd RJmEƿ̄UVHh Ƞt Q1^2EIL ͥԿۏà(k#S'̪BN7<%VmK5@',?'Ժ\ߥ( #W gc\2vp.Ek:Y1%$u7}eWQ0u~ܼ&"e2+ )00 uIpG`:e3)*'Q?xs|y c^͚0u%YjZWrS>ȑ4Mci,YI:gm^eKQ43:?|]U>Wwx8idgddGvëPT%NzslGyͤQX8o{eX3p&TUagϱ/ec謚1[D3xY+d%`g&TUe؝Yx²L-W 'b5k>VHylY~©ҰM7,']tB_7#9$tc'&PC"*lKk>Ca82ۘ0j` U=‘̓ⴘf:tj/$//R4lؐKVAFF_5kod6k֌1cPN.5U7טO8ELD*U |9 |^U;iY0f "B5,R#* J2iPf/_;r.~|EgS/^ /wm{:V>hv!>rI._ 80 >w [W/&0V];`s>u* u臘3A|IK>b2guymBoU'u=><'3_.ٱrS(WӪE\x {@<@7Yե3XΈ[b '˥9*ٺer]ߦq|(7BJ^'/a>7W=ei{[vWa[kՋtg2{HIBѕ<̔Wfbz=2 o3}d>Hh˘n@tO_z[Nj>3߼FSh٨Qw|[>}ج| +1cnnny =<:?ļ#h` ZVm;9M9uR| oyw[7gȅԏ[RTp)Iu'f 2/įk"4&b>n/^bA(4ظ7@S ˮ6s̔Nq{PL㚻{WCSMW6L&3+;cyv,cpqk _0v?`FRu k?y3еa}ly)`xqڼ9wcMYio<5:f=5 Q_Bb7AѾUL M~Xڵe.eʴ|7|jXN/՗ǚfɤ'袮H,-aPߝH)[n:7ຕ|4>{y xv鳗YȠb aռeꓬݓ6)ʗ]~S=Y^ؒkx͏'$M|@Sٲl>毠nǫv;m xz9Ay`seog/oxv`גiLz}$6s??IL(x` ^/N|fQe3Բ_[E_aG&UJޢLf1ɟKPt_ ƒؽugڋd4nB+ QvZͫw㥧m` kDkz7W^ȁojV+hv3=[tJ ynv|f nh }oϴvO{oÛg+Ox_$4,A3p`7L4W&LOq]޹o<>n|n6HZ)uʿ|i4w󞇸 -BuxV.|/+ߎ{V{_}DpK2A3۰{˾y{~֬ˠqa( z YfY36~{OsG>JX:p#wO=ŽCK w`mt:=;2Q5Jd6[GOw,O^Ԃ#VD s=zZeGhҮwG19p\Ҵ!7]~岂ǑoU#Q'6 Gl:C{( Sy s0M,?ѲiJFrkI {c/ {l$tBp|s p&ۖՋwQEҤC7 㣱cٶcyg9wӥwW L{A6mA~n6WxU(3aڵk5j7pÇgذa :1cƐѣy 녟[DH`H$ K;sX0bb]=;>:d܃yc2 5n?~?d2j7H֕~Zƒin5l$ɮ? Eo9 \-ejލ ߝ7 >0V8yy8GLj:`yl~[;:0+HJ.0%7: JtB-V |L&4̈́T~FiTcv(VG eV{ 3^NbaP#>CGkY52}E`=dd~ 8a!Fr9xOxQrXVIc#SLiS\ȬgƲ7YRa}^5rǰػ A\y8,•>¬^u$1S8PB':fDE?Oәek(Y`0f[:zBc KGF3h"Ia,)}ޢv;]GyLl2w;E1xt݄TDgl3EoDmh<^ˆytm<]+?+*^-|i&{||0j7lV;`h}+JvΠ ;ฦ4nƟPԏ:5}lyF:{|̯ i t(,s ]ź+Ymv#8=GC#rg(k?Gi=!7t lF,qX,|Ǥׄ 6liiiX+Vdnݺ 46mШQZY>/5 M𦈈o(Ѿ|W l 8bcc)"ݩރz z<>֗81U1SUgb°EBJ&hP=кhҺ- ,Y=Mz-\GIA1!Qqi}%7y6mJhMZ˗ ~˛a 3~cWoH=Өz܂NDD׃W n44mBlBun Hjޞƍ0i*98h]gKBtIoIQGԣcnXrY؃hԮ'ybJg 0[ 1boNˆP;:E5ڕ8X&sɍOq}4PDԉkOԫ}tS %$ĄX#8*b\.1uҢ PzR.EӤSV+AQFHxt+XI m,V;!жn, 1Sdqo{Ժѥ:RuI<~FtR+^6“nbsfI`-¡`GѨU3^;2~Zא+﹟b+Fdf:,avBS6PW_Bf 'uKJ6W8M:7ВE'rѴhP²Uk7&>،)NHf:\l8LN1pp%-[507'g/Cs罴NI,( q @ ЈPش 297kZzBIj/&3C"*sMPHZwVL=IըӸ#IJGq Cv nM3ak7u>aOЪ }Tb_D.](..&$It8G=FtرGh\KkEUԦͥ b@kcȁm IDAT7j!4mەpTYJ.Јhbw-eАpJO4KkzAg"*oP۩r999i6(STTDTT?HQj׮]>W2 , &דp8W1RL||7[x<>cٲeMc!r7 džꨀ599_;ckS{EEfNjjj#`Si~loIU{+VÇn(V:)錻q M[w*g= I 0J/5t&~u\EE#6XYRO7PMV[59UFN%O83ܹxvvw'f= ;Rxh+Jc_xߩQcOr|c}* C/ ^ Towݷs;1 i^yϝ;VGZ>4 @T'NF~RMN6\{ PBM𯂌ڸ! O?MsfnV >U3E0QCVKux<T ] pOAAijb _/yJxS3n5$Ջt`|;qzgj\tSZu o΍/}KHrjU#Knn.d>8^W9SS q*/oÄ O5+JQ ^PP@```M)A^^޿ gvx'[jZ!6H:+a_fjPÛĻ[bLR.I~i.3?e KY`` `Ϟ=$$$ ?gdffAll?fnJ  @999رh5-c],'KvvvcɅBȯBjv9ؙL&$;;HʈoKQQYYYnZ8*BPPn͛7Lxx8v&^`999M㜭=jVwQBzz#EfQV- 8|04MbHDD6_EMʗώ;xsϿĺ-{Oxz5woex\IqQP"(8D UɷS>dҷKqy|(ک=xh0E(R*B_րBVX-8nERYH0ACihVެ}/Uړ72!0N]/ %:<`GVZ!BUi^|~820p9|G 8fAI|UG(وBU }G٤KNaAesIL$]ģ" :ta&CPC+BRN<6*2yY>sm.w_x@+^Ws//`$E'cg!SM\LDi8K9OPpG"ϭIaA1!aa8lOsd%488P %*"<9'Bc&3c?GI݂|:IC[!BM4Ռbj=v3ThѦ _Mv8IfsdZn8]]T/mh 8}^26=hױW\՗m[qٙS@м}-w 7eI4q[d2G[N_ޙ~^[xΩm֭0qݞSZ>y`*(LOy=n_}މnݺѱS'F<1%^6}?͛re+֞~ϲ ڼ &/*?WfЇCs] Hܕ]:~ao^x)N9{xhq_.+^_Bͤw>. `Êy\vv˂y!F?6?OΟ٫ LHaTv{܀Aߘ-;@^?u9<٧}f,Jdso3wq(*TP\8.=H?o~ !٫2m4q  ͬ2Yhߣ1~AϿa5ԫC͌2'`+3}4ԙo?狶bP4 cb̛OоM7z ᛅpܬ<:t~Lt*_|!7ƱlK6(j m)_}۳aB^d>wu>4{@7 ֭I}¥;% !oM^~4i<]$ޛÜ+FbwX$/BuDpC.|dMuu,ѵ[[_4;&ЪEcvm-aەw?Ã7-JdbC6M~#m:oʽ &f@TFg>ǡg{'k g b]~ySM^čwK;c\ǧ[vr q#ټa=~z7)_޽ϾNuyW0v~EKȴ6S8=Pĸ #=z.q歧͐z4zAѲg𠹂a!($+?DGWAD4LoYv$!B#j@nHznf|w)FSO^ YhGVI]ZJ66fz:1͇mҰu;w 9{Jq{~e(*GᨇKBX|cxܐ=;Y|>Lɓ0a|?ϜM нݓOB!!Hc&<ά ` )z1b],18*;U'. =03?#:=}АKg'BQJ\ӧ $oI.>ҀBsLa f`C?:FaF.{5*PVx/ӅhCtUVu:pTh)DO|3?۳0z ׀ S0w>F˸O*QcT,$uX,&6avVo@8i FfOQ̑4V-[ҕYG1yXk@!B{jN8.^/%GycᅴW&92bB5 i]sOmvi@tjśj\\躏[aL!4Pt0"kROI^Bj *fޱᝯS>"'L9Op<\Е^.ku'N݌[K1h#9Vj@DbSB\.'%..ńz6.08/=$fAQ!6@f'BqR54{`T [H1asa{V>Ux^fiƴorوR; @L ye357\!1;{:L~{*u}AWv=aY -ho< 6'1: wuҹE ߁{^x:)~a,. QIpeB7>8u7Ȱnuh]ya9dO/ 9|'l+06f_ضw/i]ՙ19yv*}P]3 !RTlծK`t-Li`nQ\'Y0k&{+R~v[X,{vcc^=.\~"QϢB>_fK^~Q.&kɉ Hc=Z۸w;Wxy#L'uƫMbg*ƌk2ĉ|L_0ziѨW]Ңnrcy5.NŖ_<0ŷڻ *AD¬'n:S`}Cs_X~Z3a"aogsc( ܣ< }i^(+C;^u'/!{Kwk34 P7 HQ҅BQ)@ۣգ7[rrr˗Z%;un(&*:W%˅nf2c1s(q{1-X-^ A3 Ƥ%%}: -]QXUoUn'.ن"B!o„ _%63NrN*bTS,V-ֿ|EU /g۱U޶~qL3]!B%-B!¯$ !B*B!JB!+ B!¯$ !B*B!JB!+ B!¯$ !B*B!JB!+Su ;;UB!E!66qFU{-))XEB!Q\.@φPB!5JVVgu㌓>B!¯$ !B*B!JB!+ B!¯$ !B*B!JB!+ B!¯$ !B*B!JB!+ B!¯$ !B*B!JB!+ B!¯$ !B*B!JB!+ B!¯$ !B*B!TuWB!Qt]"5&\.B!Dxg4 !B*B!JB!+ B!¯$ !B*B!JB!+ B!¯$ !B*B!JB!+ B!¯$ !B*B!JB!+ B!¯$ !B*B!JB!+ B!¯$ !B*B!JB!+Su@QTUB!DEk>T4l6[uC!F1]B!ƒ*B!JB!+ B!¯$ !B*B!JB!+ B!¯$ !B*B!JB!+ B!¯$ !B*B!JB!+ B!¯$ !B*B!JB!+ B!¯$ !B*B!JB!+Su0 t]b!B(k>t:B!5"/B!JB!+ B!¯$ !B*B!JB!+ B!¯$ !B*B!JB!+ B!¯$ !B*B!JB!+ B!¯$ !B*B!JB!+Sutr.B!DRTTTE8+=Jqq1]!BdvUUwB!IP!BW@B!_IB!~%T!BP!BW@B!_IB!~%T!BP!BW@B!_IB!~%T!BP!BW@B!_"0*5( !1 Cg5(g%۾fuP0u( u-ɗܡ( & ٌiZ(..rp\c\4 ͆nl6 >V9Xbg{o?/U ifl,9Nn7aT:u(ZVVY rPÁb9`]sTm\.N׋bAU/館@l]$Q%77|V+ƹ<:?삅‰t+k]C0M}XϿ߫@}>EEE8Nv;QQQX,, vqݸ\.rss6팇P0pݘL& .eYoÁj%//SI_g#""B i111dje -RR*-%\wՄ֨jNILLe''ר $66V9@Q׭mn"+6+Pv @ZjS9,pkqB uՈZv6t:IHHpX,&''3RZT@}~(;>nEaZx< O7J]9>JDDDyS~XX)?ʩޯ4Bq:uL3 CUEl&3ͼ:Z}/gh(.DS ~4??P|O{k#9~fV]v1g;1GBK1$ @8cP!!1`;`lپ5tM:2JYIW_}4MN2)t*t|]H 0u ʹ44sAOA8=Xawݨ㙥GYi3opP.N ÀPk[HwI,azuU7+)B؂Qxӽ0)\9ˆc ) C΀Isp;Sq ݰ 3.@]PAn,LF!:kYULί;[^ZQ}:lTw%A&B)vM n^ŗQCs#mboGX@9Ke1>>N}}=h3@>n%K0MQKhO|l]]dY+=4a2BԳڽ#kg]{ia&iYPh$<0?(4 |ôGCX ҈D#SSsLc Je @i3W,ӴfjY&|ӴPZH$q>AViEA IDAT;61ʽ|OւeLcjё!&u304:FMm=u5X 9U1sShp6"LC'͢cĢBI EMTMS3<4D&O8~McO\LaS! oݠx(|mEv*R,Q %t)(Tf:FU) pRK*vv[hOz zu:1[9D./Va9tnZ,km 0_dU#pL"t*Tf{1 [\4ji L=`Q`v‘jVYEmu ,3"]v3ƒhNs=xϑFM}+ dAX?j$׽fB_#y@㩪ز>$w|Nnt ;~ d"Dlh~N3'/ؚ²rlW#;wͷ^=۟Z Wҳqpx^v~ͯ ==A&gĸe[}dI]bA=s#g ]T"E06 aqnwc ȇ~^m]Ґs<'Ȅ7]o>z9D^?GhMmuP|O[ t +rt=_kЛU&5G_Xx|.쓰g>5WAg Պ-pcoo[6XY$NmK_x ~QK#A_xi;|z+ ᎋ-@k-|;K!fh) rf{\a{>|>_4 e÷[8@?4j^X@jбLeY AGQFa1hP$y7ƫx 9:Bӊ\trNQr&Ge7Q$;>GxՆ wP3gW7M$z5TY9~ k0!äaʙ^˙= µ  JV|,>rj4KL[V(J2UR' $[WqUo$C94euD{%2 e!%^gF OV$y$ SWHx#TՆC&hN,MS#7#iJ>c]g8I>g=wsbc0ev+7/ EboCe30Jlc ;gáq<$ck_+o5I}[޶P~}ܵZjKomLC0蓣|_/}gzl~bCA7M" `ݤ XahB&}f;wj:o\_=oYE.h?xfyvS>]yX&nzf-SѼl-dД*<ҠF논KI[LE0'_yGv{Ͳ2-Ť0,,ݰy]e BT8~NLtҔZdQD?+ڒpQV\.Wz6H^_Gg =$҈0'Z]Kc"Zc h, Fi"RQ4I #fx( 5LMhq"=BbeC0F<ib45% nXټI$a.@Anfص0rG&J @N2[{wLC=H@TkJR|?{cT4p+y'Xr nhe19 myۻ.Dex] [YQ@܂D n ! ,Cc5Z@&a)icnZ0|8߂cö]kAKƴZ yN(*B |m5ppAvBUBO~i2iAXzrJ)0yOWg٩-dT(Jĕʼncӹ##{c;K( ?2,az1ڛqI#A VWK:f):JArLq`hDMAXȌܳݻwk=(O X? 5Ѳn\K/}>/t~խyz1%Wn E- h_q-+(,v?ѿ7f9uȑ~6o% :{xa0pCcѺ|9hKW%K]pk׬`1zca0utâ3 J6R;pVѐl$.QDͬZH!a: u+ih9v{?GT5Grc Z7^[?Q%B`)mb >sT &^^qݚ3XhW0|1ᳯSМ 8 aV-!{0 UcV4"(Th| Ӱ&:,|] Z[c_C[v]AJ|;|Pev,{B84 QSSCOO555eIz`7inr9iooZzT(Ʋeˈ%R3h^5eړӷH,hK)4MT" EhYT>s€fVHLwbT%SB$)Q}H< )ZpkQ\;c#hYD28L!jRu44$T Pj /l:2QGChPr ] ,]zI)囮_<Q@a#~,-_7L,WXhQf?eӞS͛?=}vqݍ7s;S-!7OxV͆WDiopv8;67@҂nax8G6#CCl4 uO|E d/08|GM <B@݀#p2XAF'haw@|o0`$뤐[PV֯_@65k֔\ +[MI"?KkMLӤt:MKK˜d2B.KȩrƹcFYs]R/,ͷp^0_Iww7wނk!,^ {`|-;d-x5K4; -^}60fpWÔtό>DCfuCvI1)HltxQ 4{pTƧW't&(|/F➛Fv~{ľwZu߾hj>L"(0|Åі900@?K.]qDSz\A JiLWJvZ(tf'ОLSM7Sϰ= _O4`2W>M#A>{ `8ڝ4l%VJujQbp(@]]'NUVW,bbb.jkkbs;K`.oXD"A(pNP(Dww70 y%”zdL)E]Mlrɒ#  2 ^AA8-ue  OΪݍ BAAq@^-.mP  6g=(I  N*ц">e0AA8G,r4;gu*gЩH0򺮏ϱpAAa`f6˥իf}HAAXLLL8qb0} O]92223 #ڂ  BIJ,رc]&y9s&slll{ĉݕ^  r}{?3 6OZ{b7oǫgS  pvN'|>OزP,rm ¾[wybxx5H$)RsRAA`'''===o׽`:t J&..l5ǧ+W\r-lظqcKMMMR*lY*~>xW287)xOJ ;XAAՁ8YhhF+/l8x#͎߿ēO>ٵm۶+m:>V+WT>\)HCB4Z؆=u ~-gOAY qD/wS-:4#,%@Ibx^T/%@K QUV*BAfOAPRNV*Bݮcre%ٴ03nVQm-i:[wZgKDJ' ⧜ -ʽ-焖κ^i^yݏ:;)D!J_2AAX-WJx^b1~y}(Do" {9^"+] ^yt'/lnǴTق  ,^:~q:K(vzD*+HWaf\9[y ByLY,fjUAMgU*@xDW}Ro/nZrtIDAT}Uӯ_WO^e)冖' %UNX%? ϧU~)׳T]WwYS֯P/xVn?AaPr.Ar2Z~Q^#W$,w?_3ͯ.  ,n*q<[xr+_^t`Dk&x-~9'UA? rhqu9u3ԴL^ǻM4^Nh/WwAn'Nx O {9\ORqAAA8(tDWU|9A׌/Uri2jw};w4AO\-n=Ⳕ`Tr~Ab*)[ilq* .-@rΧ3XI;\R ‖O?Zt6Rz;~x!TA2+uCKQgn%Ox2URiJHPI3sO/Znә^Źӂ!TA Pc й b49_%y]Gk:3ife8o{N1)Wv7/  {> ⬜s;^AODz]SXK9ܸJP a7n E%W,ng#FdBeTz> /\a~fwF;"޺ߔP~}C* ٴlS:" 2 qA=#{Pv3q y-@nr^NuRMp u.?|  ,.NuW"@*KRӍ5u~ͻ E[lv=[/9tAc.-ALJ⾗t+]JғWP/T2x³#\I~H  T/%:{[N qA=ӽz7)Jq^;^b4W/q:Aܥuu)Zŗr@+=}m*@֝MA' ;˹A Χ;+ tA9!-љ%\睍~랍 ,m}_l p1ff/~%[?W"Jg〖rB⧎Oj19>>agq=AAJh!ZNs~yq{;9YR(l <  Jbk8ٺ/?~6债 *H݂hz->ֽ-^N(v? x WX+ Dx~ǹ'7[L Rx F ĕ7s9,'EL P)A4l³^iD_|%x+W] i6i< 0t'* &h!qA~l "}~.y~}@tqW߳Yw  ̕R"tm)+\RKգdZdnhqi ͝. $O'4 ~PAY*/+/~hX<⡴w&O pnRW.o9a6'\Z)s[~sh :Rus9)'AA =N|x _}B-uIENDB`MEGAcmd-2.5.2_Linux/contrib/docs/pics/webdavConnectToServerWin.png000066400000000000000000000451371516543156300251050ustar00rootroot00000000000000PNG  IHDRlhaVsBIT|d pHYsod IDATx{t_AI1=c&yblnN$b"3&FrgŒ$9O8ϊ(Y퀁@d r$`H`;F:EխRWg--Iuy}v|dža|._+W0 ]r%Տop;k J.]JHΜ3osJrss+0{ʕuiÈmw˯@J.]$I: Ly.^?|rc@Pނ+WFB?%k99966{1zGNP؃y4':y6DŜ1`CHl64r8:|pU9 xW,ÇxWښRS$lSXXUaaaJer0Ȝ$ :Y{ҥKFp1'mEM} C.ix$|͚5m|ro64 ֍:{lo6!u7P?I( (2{{{{I0T>϶0ϼ7 +s57~ ە+m_ &]Ν;H~P fr*~+:ݙ1ޒ$\ vsG4a +9Ct… +9&O$~B`G9sй3 ,,wIQ*gZEpQzvo-Ι&>/^&@&M$>-.seLf:TW/Z<*Mʿ6ӡ{guSc0'wjNާc2NU[\0Ҧ[NK>vE9sF7|ZvoËW~ulZ&kTp~˺׺k-0n8M6U/_ן'S%@s_;]$\M=3{x>ˑ\]okz{F99Y1x`/{Y9Yk5eʔt}=E֟fW"sE{պkɝєz6W[ٳgu7G\yfM0AGr:{̙:+B;͛cnM:5`41czҥKt%iIy3f̰]jѿ.ܬ;9tOt 븠/srM0Ao9o=ٳzچ &СCz$ٳg|E?"bi&LЋ/o]gϞUAAx~N:(]Ǝ;~C}}}W?jJH&k%4AcNM>֙и|^q/sgtlzG4e I=c K/B{y-X@ǏOk#izo~SgϞɓ3^JSdM?g~}uQ.NĹ?f?G}T/BD#Gr@Yc>7oQM\.M2E/2}>̙W^yE ,-I/,X . hoRm$3e'Gw{t蘕ÇqU~qxtٓ&MRAA^~eI$9Έ~0 }zIm69sFojjj^Ogٳgo~Sody4{l|P?HIK/I^z%M:UgϞ׿u=s2 CoD|wwդI$ՇzH<4g"Ve?^wJ?~Owǎ) "m̘1;tuI>ϨD_Wc ɟO;E>ܧOpNg~_^wPoz_ž˯3< .Z=؀)^xACO0A555?~nv-\PovLO?}]qr8*//W[[ۀof… ڽ{ցh";0a^Iׄ hѢro<뮿GX)I{tY={V}}}Z`A9k~o}'zח|4PH_88OGӮWĩKʟxwroȽmceH_B===Z`A#GO>-˹pႺַ`uuu)oQ| Ða#]X_yTYYYh}*Ο? &0|>릛nɓl2رC;vPaaa8N\Z[tU<2}%]H'm999צí7'K\T%w7cr[?'Ucoou} ]{5e}3ӫ^wӧ#[ꫩ??Gn&?>u}__TXXI&P=TVhȑ#zc5a„uu M>Iph"o~˗^f /E6aNt8!͘6QlM5}ᜦ~uL3FS'~N9ɳj߁_'U lٲpBN L>c={vx݄ ꫯp;H:۵v;/ׯ_qsɓuo: o\h^oPY˖-/E?@Ο?[/&=}rt%M81Ck>jqCN?9;^&/^{ァ/~oYy}%j|>wMzu_HuQW?U'.li`q5,Q޸<3fē5I_4'OPoѯ0֡?}sOG<&0/ϤKӕ+Wt^83>ԻSnߧ*t?=,6)pC—ܪ/Ike:v؈ NC"a96#a2by>Z1$Jg{C~B$<֑pc ݧuN=:wL 'v諟{?t,d6I3 ,L/I1 +99I_t<kohر_ t+5d:4HRyL ͑ ͑ ͑ ]5 w]JvtYNut5QT'ρs9Ec[ؽ"yHJ$l/aDa`pN5ddtȳ 6eU{_r8x`&a+]0Sj/j_-F'f=:.H[ #5aNS5gzH*]ӻq0@6uh 16Tݪ9dIt-rvF:zT<+_j<)yŁL-a@FP:n;srٓ)_sfEuhgc*6ɭn޹;Y*%rbOdg+r}`zյGj?$WQY_+QŶq<ۺk!.#Xn3 Z +bwK%lsSŦUkdeWGTU1?*(_lJ:f艕 sˆw/IWϩ״1_M }5p7,z:IͺG$9O {9Yʍ5jTcb#'Pj)LLKϮ̏*7&i3Iy@n0vbXcMy3>P6=kzL.)Z^6 aRut=Q߱[U.5CR2u*fRs\| < :;U5pS[-5QIFBIB/"?LńL9̸tvE=jYZ:I  P}3Vѭ%󫥼(B٭ԚNTztSRo,%IV4Ƚyw}RHæxu%w6d<@ ө pŚX͠(CikbĈkF9cҀ=D?G\u+a3X͗OS'@Bi |齸:)ڭY8GI&8J̔jɯsj%Ul(iۻ9ۓwmɾJ3ˋڪSwrj:;XMzHsC2NVMJUj)5ۻ'ڣF;Lצ;xI*5nݯSSq<ztH7\EjnGZgG Tu 8a1LWGK͖fhIh ه+d:oqq^'+烖>#n Ku~Wπ5&'q/^Q6m]ZS.=uٺ XQ7SFj[7_$c!rԻ 3T1GUMlأ$]7JwzJTˮԞ|s2\ı8Hy$$IMwƈ8w@rlJT .V]c7FM/&Kx wٕ~S;mLS zVʊNCCK׹դ@9A++TtVE9)m=║wĖs7rEaXԻԼ[ؕ|S*Z!}^CE[-׮r]ߟU?P1o`L͖+Tumt|QoN'Y7);A$O| xbͩ*-vק^-n'gg:& SͷUǭ`הaQ)ol^6|tL)\ 6VJ+ KP1d炕/Nu["asC2g: >+]`p96#a96#a96#a96#a96#a96#a96#a96#a96#a96#aJؼ9GtQD5Jj䏻c,^y Yc:ې۲̵gHd? )'^]#dOQId2 Coӝ9+l4)I^yn2d4WʙT|r}E3L'h=j?i#QudlW[{1Rr3@|Rg>J*u? |$G;g}Ixޤa<NUocb>8+DY^pD#q H>jqCN?9;1+cFv.1Ĉ8Ly{&}z) W?];2!j6 DF)*E`s$l6G`s$l6G`s$l6G`s$l6G`s$l6G`s$l6G`s$l6G`s$l6G`s$l6G`s$l6G`sАXtiC!aE20$l)Q#a96#a96.QsR[xeյq-Fؼ8r~Jj7רQxfW4g IDATg2I˴d E $g0 F \rx֛:+l49>~9-۾QkIG:@,KJUVqNHԹm[]}5m6uf2(z~h׮]ڵK?Og 2>YIrN;R`ʴDOk~Ք81QR#: Mzɟy}ܦKk=ad]wuZvvڵKwڵkuue0B즃0)Irr[^ՖJ;^c)|.{._ԧ/ 02¨7ތ^dh\Q]#>LG`#a9frU6Sj]`QiQVpO`٬{3kWmbfe=RCҬA._PYvh}ͪtf:,/5<(MU`jh}{PZ!6{ #Ln[ыY3ΓZ-KdF7{4btm$ԹۻML$m(-=,5L7mrSuHniu!i9([gvHe<\4UPyu*?}JCǀe>ݩע2nI}3Fw׍Bߠ%unykg:, h CniuTGzW` ߎY}^RD)0De/NV-Krb.n8gg퇤|)ѰΪsFMWLw-ҁWsyipTvRȩhsvxc^8VÛ~.< >CmݺnuV},34Kt @z_TǛxEM=2Ge,=)\޷[ӉkFbŲ!iŹذa.Kw#-8Œ: ^@b9 'sD`d8t RmQ2%s9z2i `P:W;._뗿eDР_믿>ZطK-[m>TW/մiO YL[mJ 췭3o6-]OR-z(3X&YyqUɐa2|j+6-rgUuh6 hQۜge*Fڌ˭FR[=ɚOwGhFO&M2)(vt(Y7#]w L7$sh3Y; ##ֵ6kkV[YۻեkH 12=+R{ԸSԹQr͐j}fVkT`ۥL,K؊UJ Cj[KBٔRk-:߮j=QludN-UQhVFsKOG)Րq͔/#2xOi8yTOڶ).oyW賕w6myWi֯Ki I%8_;UgXLwmC {LS2VDUĜ/Yװ0GTvv]t(B+C ;)i߮v,Sۨ@3\*|-5'djHa_H4W,`84`-豷CZg IU;X\?ě fM4#|77(M!=}6#GZExf3٢Pu+usߔ2@\?s\>p@;|O`օUi鸺F٤-mM_$lzq!_{<H[k)5\K 5tP綥6mL۹yT~=Nbؠ9(B#n5*qVDn)JNڪ}j(UDUA+G>=8ٹMK2lun[%oVÊܶTYjwy( sR=jXѡQQۘw@/_ti DLy(o\e۔RVtիguYjܪ n*De9o1\@-^p!ZliJ2Ym7_U#mGƦ/(+<:}AڟkRݷ:|rpZ? 8Q!L!iy~`zN(#lah=5l\:^8{PZ>bas(7 DF76#a9aEg:0 HFDwcEȘLd6@Fya@Hl Hl Hl Hl Hlmpax3Kj#oeJ<"*Ū2OmiIUӛhY&-Հ RDڢ!edx ۨWMC%99G%y=ip [Ӵ$m,mKbqMDpmC4ղ`LZ)Qbs, țJZTAz0d4UWz6m[-UL i[_TZ^y\M6I^T7/nRUASoZ6YF׸Ug!{MI_Ҥ؟w J ۨbVTMRLj -S`TQ2 $u*]"UQ *]"Y5EoW9,S1TZ+#<PDo\sT\W9`=V'JS?x,qA1$lW#Jʤ&ÐaT]Bk;XW'ZRPM79+lzVGMA&SWnӆ5VQnJ5xbHцm NMRY'1ϑKol-ӂFշ$:pRNź-(ٛU. ~6hM3~)O<cN$l6Jj))S[* *_U%!T0b (9\U*0OW-U,.#D\U+P bN1քđ1dFG{ S}8~rvcBVؠ9f%M_G%O\ղL[<QaSհGy76OVCvV2nF%Dl6 Rj   ͑ 去&W9\X`6#a96#a96㫩FimmH@"$`I@f0% <71ٜ:j?Kd-6g0 MnՕy9WqAV9^&d-6X+]"[q$lݩ =otyͯ-s-Q#7OGx憇K%<2թY.s^i ovFș⛻ei]v nF#6CKd\@UtIKrPmihJW7Sͪ<$<\ɔ|ZJ 5MPZ,h)m'ǻI5E3b6P6f8.0$jTR?)7\Y$ ov苭oM$8WIwL<\9`5ƺTcH*Jbpݪأ[fIB;W=)wk0}W 6 \5oGBj2ըKS -[`Y>Ք$svhyz hS2ѫ#a%'u[4u8ITQb!a©U tQl.#=p(~ǿt,ӧ􊾨'g:X6˙v ro%z? 96#a96DG $lH* `<9*xL/ۚH`~mEL4[41W[q^yBGDmJs`#\Æ~-p蓆 #CzB\ArSV'w!*9?)u$SkJdԴعɴ+ѭx6_P}[KJmEsӥ[p4d'nn_uez?95zqNͪt9DE.+z}1ݲ%(UvoYن@Yq$P3Y%Qk:b<^i>@"aC[{++0g _,3'.ͪ<$ɭM+:jqCN?9;1aRj[4uTOCMVmSհGy76aMT撚8w~$le%qj2 DԪVe: Ul Hl Hl Hl Hl HlmUͿg: n;,%=xi'}uJoc/5说?g`4 aMΜV:2j?nĻxO\"Pzw^5%cgzզ 7WSƧrIakoU ڙE6tmOYr\"3bɒ mHF^ԽNΘ;]n?G`ɍ %j<$U5.S hqGkIixK Լ'JI.S{T.jbɻ4l5yC*ԣ3IsWz^бwiVl6E臘6dU_x#SΙ3O'[-_bo]ߠ9s0s=A_z+nq,b0wxqԼ_u=Os8WLu&|np%sx|-.q 2|'|Fی63Nkܠp[ kݿ7Oӏ^;a}h2Y*#ac0mw]xz6~jQpN/fXDeU;K7X"IƎ93q\g-QXۧ dKOEݖ2}M7u}>XsTFR?=g X嘍,Ln /Y:Ed5sW2?299v+^[ct<Jx&7wutDYWS-*5/:sz?sZ%?j U^Iױy}wIyWԣ?HnE=ګ?H;)j¹ w5d#'^tZmxN3IV)!?ŚcfK.#]a5Ʃ/UèΜVnhz $-`ߍݐ,񞹠%߉*/_zWe;9knWUXm$ t*ߒj;7G]ҡw|Ec0^sn2c=\dq )>fTJ2a;fV6{O*4=vbk$S~>M_]dxqj|(`m4Oda9BnQ.I]ixUu 2//"cE*-:_x%_>U\VFU+ݔ/Mb{ƫJ**%_o*7V1>)BMy? T;Ny:+V;a}b1^{cxXeEb^cg\s!K-As`UVıjE<߯xɍrq<"D{,f IDATɠx-甘8|M_c]@F92۫ :L kLdY6sZs'5\5-{U[c=(GwX[6#a96#a96#a96#a96#a96#a96#a96˙}3a U:C?:P`%G=$5L |]!2 ۫ ѸKr$W?ҮVr-z{P`%6װ ͑ ͑ F/@e@–8v:?Dl Hl Hl Hl |p.&Ӊ7 ޔٳg"> .$l 튊2i6g2-L'5l6G`s$l6GۧӦigNsN[m vO>NԩmKM}t:Gn+70" vVL7/[wvM6-^wk~m9ꐰԶFsr6}ET{u`J1O޶:rtvSئR{xx3oi4Q}7mj퓽0"kyRe:v`ⰯVgmle}:[6.͈zƮ`?lײ続5ՏڶtG>רK[m̫; jm(!芚3o ]QpG,F v Mo9SGBh蹈118j2zNCtd 5tui3R7 L_+V:i#.uu¡V,XV:6 ڿPY?tXL=UOCS{x8"I34KmD&Vk# e5]?qoT~ΛiekA]nLzC]Uzs'4EgI7֝ z?p BP菙EUQ.xEQ^wBP(va? 4!!K :2 7{?F`~MCe8jԫ .ӣHh"?ovnWLBH&q/ʛaB#0!;0ߣ7X+jy,H|^sq+?Lt|BcrrT^ʍk+ԍ$GGyW~ϑűbZ Q(tL2FDw~.=i6t,P3Ca{BOY4@^|P2 AzF#DY&RHUV{tg+k?-;wxJʦ9|fp*":ѳ0jk\%'EF+,97l{߅b`$2^eŹ[p !'86os۬Xbu_iSt,>XRuhhqǟ}_&0<=N=Ǧᾂ%al^['A>qEzL):ʁgyAfGRDԖٯ+uch?'ݿﮮ HvO_]ޠJ #sbS~y~}}ݦ3{QRT 2;,(%}qm*AI $(󯎙\gbr$C)&sc,] g>ZӺ2Jz/?x3GPneu9K6T%#ȓP| 7.|ᄍv ?ߜI]9uhnj׻8~.Ε2m{n]~U*7~顳sMgrnBBADE^3nӺq 1t6} ǖIVd PYLRU) ڔgˬlM)K.es\^fc((B sp"4scǎrAM %IHVwk6Vt|"AJS Y2]!K+x}EDա/PiҭW1UtE 9[XߚFu0'jhFɒbmRh0&mpi #JDՉD3Q̛ڨQn_,d=xX&wkexV/Q.$IdYE  4:]$#yxvL- ^1$@Crt@iPB&IBJlHTbh@> *;P[Yfa iRu]T^[$>'G;k,_yBRYC3p=@< Ù$x 7blXf0;N犤r96jvO{qfv  /JOSu׹pmpm8061j$ GAN()jSs'}IF˥(<^>vk|yGEW sk6iB|ģѻ_}{I7[QǙ:x#1s5^uz';8R޼Vg@﬇nS[ަ/L<4j6۩7R=̩ $kŊ=7߭sc1qx09xYnqFG[rC{oXYi_Ӷo6%MS$ j[Tk5ɳNGP*Tua_(W˴^4GvhBQ%= ERj/b;}tjqpl۶_(#pvpg.tT*M&hQl]GL'0_kt*;WW(Wl*Kи4ϦVכ?xH}\M{c-g={Х Yܼg R:p+s-W=-+8Ǝ]9IjJch$FLv WY)gzo92|-Mq%6i T׷*GT}WƟ@{ smgXz+;e7|9 fy|z lmOt4nQ3W^=%5/|ʗQf[#LjR8xAj(F4hnYygQ9F#f?1#2Rצv̱عklolRw;\F; |ڵK5C<+u[7 =7om vɓ΍72;4J jàӠ݊NΠfxlͶ |viK([ ={|g@Y,m"lЬE̤>k0iӣŅ%Gs I)N8Hy(5V(nnQ*$׸|v\`s}$%4=E2ty67<ؠ:P1ikNMebݵ:5۴n~>nM58;ͼ۹ŧl9˩c.1eXبR>`?<3G88Qd4c7q ]&\\BbMu (oYixP(2AI٠8`[>^1fXyǏ $a* 2$]z8"w>z)IUQebNBVTEb޳_huG.*)*hn[;w I߾]'~縚*[n(($3_2p H\\{L Xwi2w}C+'v|;\{Α: }v,| Iy>n?E~BF;x!BH; xO B᷋G{BP(/ Bo0BP( / Bo0BP( / Bo0BP( / Bo0BP( / Bo0BP(y:GB/L15G4loB? #RJ k.zkv+.o'{]CPuFDiIM[px5?aj|H5ou#DtH9Pb9e]~8ué11ttڮR=-BPU iYlTLCA"p]u66+T}M֯ ~ʑe4mHx~NeעK$fs~U?^R ܞơrM7*&Nd2Xbqaf2u; P({&$5127oh,l_BĈGc1zZ_B(nwj}GӤ w**bTT&0[ԫ-"p43<. ?=6; ]?ϗ7$PJz,__.%v5BF3L;HBU_Ygú[HG:tP3hlլP"°@Sfw.33<. ?d{~|2_<ëf=L\7_-8ÞwƟn_ 7pxsQ[˿8J_}us *ǻ[G \g)6ő8}|ᔁBv:+tls>~˓X^͵&0LCHzhnɄ^dqmZmޚ`|Q{<]ɥ!Q&Et4 n^l#:4dhnps@@swί@;h&%tk}9r|<~:K||P01gQ2|ʠr] L,.'^'/lbs r) P3)T:5UeV[I20S9!U (߼W 4n?|U;>'g9:rKs(#$$E!sNzAݩX6u6]\ $I2;&$ Yo~1(|ZN[i˄ IDATeaP#Dw<{zmV.15D ]?>( !wq@GF b1v#*1Ri#ץoO"vY:78ػ|f&SH$.zf(th6_Mq|Q&Ij.YJ<E\kP!d&BrsvICwb6YܨQw\&W:{mc>ufYA2[VONPJJج1ϼ t=>AV4zI|(~c>8=˘fo` ̎kO?CnA( = pq<-A,FR`6, nOݓFvnВ_. ?5q)g=Ad\G3v1`{/o~-;p, kK#*Rp(ʷMS;r4P:7ݥ5x>)1(RsKTz6>#8l+Pz jcxP((.en3{sF@4 amRIY*ЮQݙ,3_ ?0+,^·{&dzEU6=;hXv)u"AFdAo?0Jh9Wl:Ì9t4M$/]a=WI%*U=Ǵg ?%IJq:c TGZ;4B0BbMZG}JHb lPUu%d| 9c7˻8Mcs +Yw˜hG8aFQu ]вw:P۬mјD|}-L;bé̌H=V..0W0$ ;l'%=YW%RcヤiF&+\[, ^c7Y(xYGoXΈJIVPT c]Z6:TLid,:]A!Twscwv*l_-p?'~/>g9=spVfǑ4$7W^ʝG gFK9]殭dkLe4b@j,=ԻG3q*]Zfj⠐,1^L@81ލk3BJ/q IPh lҵ%CC% ۃۋgD:\YmcVFF 1Ȓ{"@6t'ŷmSJQ\rHma pgG`cu6;6ͷ Zdx!OpVV;.9)&IRm.-Y̧H2I"ؼźU9{\կ={0fL [vVftzZ<7e)$s %}[aW~IGʜۏ~o~[7g">ʉ¿va r\6N.ѫopc !!!+2>n%=iccHȚZM%텬a;x:!?9h̭ۢ}y~!!)*"#-,WBb=GUT]׷yRK*t+Km^I. ?AK}B|￿A׽ĥ3u >͌1.vҵ\]RqFOf .֩ @AK8W\>ʟtt2/Қ7> =NVær f!f awhn/0wKgoot(dXtDdX%U<]CD9x}#Tbbw\]i{ʼnrt4Ɉ*fy2EB#ETta7T]@[FuN1r'_[c+:12#3>{3(h6B`bH6n*rDa". d9)PE>gL1TQ|O(Dfg\ߢl(ك#5ϱ=(@"DQP5.a/Aﱄ$3 *I↎쭱(4B'譭R߅@H2I@A'iB'y6I1R9 FEEBڐB,' \$)hqB U%dI 11&GxT<[IA$%w=NIt!W`htJt^K|C{2,I&DMD1͟B:{aRS[wu:$)=C9c7wT׺4WiFįuO'N($b  QbEyM Q-2w߭ݮQ3=@ǘ:{b{Yk#S{9zTk_2W50H%K~yl[ Tz6n'dICȃ:38.NIg{vSôs^X LMUL#q\fMNA!:za߇kPK~Уckd1sxQgw&O (xsTR#14S \o` a8{: FȞc6_}[M*=\r-/Xߤ^aŽuloo]k &n%-S jc|\Gk{5B nYjʍG{#avFk N3IܭI' ɦطG>+ߡZz`@2+>{Oh=;i>25[4US)'n,KEWʯ?e, <~[6Kzȩ15~d*H Ym8/%E:\)LH'ktcq2%^V%1""o8Fc zsjM)_nI3H;nw[ll߭t4J:Wu:ߣ¥/ɞ}?˵|RL:uϯWt }keN3 ,7tEd 2rbgvrD"ERGBx:k0#d 3nwhXY^iyڑ.IP2D$]F34d!HhNBE7"D=7=qPe^zK .7{'ўıaus  O@0 N7h#w\)Ple.iV&ئfhZV3 3?"[ϡ tA~k[Q` y ICڤkX؎66:ݞ=eSե^`z$$A@ Ƣjic;6V=`tp{Y)R}^׻OV+"S ?g $mjm]WC*mztg9I7!iKerS;B,̘DcV^nf2$$2DV 5"|}$cX >ڹcc6 z66:7X+=:jVKc̠]&BQ$qHsQX۲qlI<y!/V1c(aACmiuۛ{oV~B>y$[y0jvy91ѻe^/s!ffgZtjtMG$ i=4 r2e6UβUh1;< iKe/=[l:MiJ)ghvmT DlD2JԦP/X^S9svNSVY),O8L'!N̵ΎsxaJDydۢ_]K廊#Vi8 VWwylB8D4 g,@mz-G$LeҰsMzցQ$ l:' PqLD4퇰q<~1~lR-U F(_c'[p0yr_ͳ2$;Scfq^xWǎqrob]Oa\]/MKKO33cvq}m6K''CHU_jPaXJ,+W]fY )H_nJ9hkv(Kywld5@L?⾛ԱWةh4_.z9Q&7i8zEϵ\pz5E/$ OGlR^޾߷nJ5(XcajM;fK A{Ԁ}N5v-Ei IDATt,D0 dh5*ur:r9Qb(o0X D!bz^y0(@r\PtvF~H\(Ǐ8xQ?|o'G%I*jm+AB dO4˔ej_rH"I2%14F@ޢu[Bthd[%zS4IN3z PdEskݶM2/L?M*'`h:JB$d_ }Qfa|A" ޗ& "8muZ%vS)ar CSsTAN27nmQ.=Ȓxaؖu4-/ :=[wr|A AG\gx%PЋPͻ?xp-h)xu,}Ct=>A~d)ؘڥg(1ZWo0HE{ZJ#Ă FUツG:N>#Q,ZF ^]je<x3CL9ǹݛsu* JRI*9]\\D"A0`dxEQ?zB$Oo鋔i=9 "(`w>{7 Z({b$'8tG_B2Wkwտ <ض SS?gl|p/^0. ?̷ΜE na 8##P0*6r  9O1 .|rIMw %g9֮osуܙ5wX+tMN1ޮ~P~ޝ瘙?őbbj0Ɂ!FFH`vk[lf=2 O2Geg;vC&~9l|vBcgc:ԠF$bn$N<Z.?Mx"X]MWw'Jzhё 1? 6fJ~kͽ*wq 43)ұ ,5(l[6Q&=ecгhFsc*P*O8z0cضeTU}5*d/_#Sikl5t`"(FNq!ļԣ'suD'1u?Q*JM7A s?lP+5) ~yGG kGl^c C0Z6)LfG c(4+ܸ|l/P aqN.L0Wp,n>I0tOxu|7;TJ_ GܿAn\x8( HzTt؇##{$}w5@# ͝KJ8(bZ?˫{:#:ϟc, ca#"H2I&y.쒻`8**K\^oPp;8"  pfflǁyB^g 9Ui7T/hCɋ8/tv7UN̟!/:VG6Gz0J膂X7e?PSqRQ@Q%T *UkXWXCu2ϾwxbkfQx/;~tf#MN\ (>ǡqR?_*H|zBO孋klug{ן a\ {2nwq/$xapt*q@0//~Ûo}gGPgc|o2/d'ynH}O 3OpeǷȮ^u=]\a}} 84? UV}%`U\[㟞KGxgPԡ cZ  vj&MO?HСqƉ#dO0D|Ym "S$ן`~H[_lxԻb J (#zXwXΨ*ۛ*Xٙ2)&cGeWh}XJssi"xXjfO+Asb6D&WoPD aDSR"i}/VuRƺ<$KAwd0M+ѭaX:,do,:C92LA[Ks BވVto7u 4M6- A4wAӜVjbcmh~u,;} DxʓA)l.{9z[uK\^823h$" ,,ԨY#L:i/VaMVM'>ɣ^NJ@/Ll0 C!…Krj&QJ1'&cxDQ|̟atOZayF5,%"-Q(cQ&'Lz?WZ3ǣ7tۢS`!RrKٮLj$I`"&XUYMi,SC!Wrr@s«& FIRd2I"Aއ]g}=աWyv9^QO11WX矰$sfS6JzWgQ_s)?N3qqfDᏍkkx/þwv.w0D/dkS]bQvlKiq#?"dƙS1i*78T5ԎJ?.|z0M>| 3?'sW_r8DC Y&`cvlnJ_2J+3:!Y\xLZ-J1!" :]~`=M{SXJ+ 7VY)Q혷^[d:cap!Ku}>t7r4/i׹ުq3'ILZ8Zeҫ\_F=(sLDnRSҲ}l^2ڦ~(O3Ži{8Fw=T81Q 똆Opˣb6[?,**n? Yz\N$_1ߝ?wm^WvbQgw"VjTďW JyrwDZ8+Gr/Ȩél9JrЫ_x/o۰-ۦYR.ثvܮ R)m^bFiLhHZp7r ]vv5 r7#7Ko̽-qhr^Bn=M:&,ٶ;%סz5^+tk&n^.lSVw{t $*Rjf..mVV|ك?$QDQĶ}v\\\\\8˲5g?W\\\\\:\sqqqqyp?W\\\\\:\sqqqqyp˛9eY81.....AA@Y=Ir C W!~'!d|pnnwþbO?.\S+~.....Oz~i֦{OtDHG206jBNg]ه7=ˑBu|#Z"h8.K9, d;6%Mߟ !Zܠaem<#sLO>o)"e6-XzGeMP9 ]}#ɋKmv !&!un/Sixm i]:. Hh@i:H k XZP~?~E׽1*O `I ,X:zBn:/-Yһ{_D"x&_(_)U[ >|^><' ~W.mMGݢ'M&:y/Aۆ{,lu['1Թo*,_u)_I}W!}WaoY*?Nߙ_ };bRi|;2@X2E/I*į8iVvd)2Zv Cq[m/%JN%HD5GUZ.S,ը<' 3{?K j=6ZB Nzpg'Y8>LJy?ZtiA" 6k̗fnSduIG0 pzEmP>o_G!gba<'7hoҘM3ٟ`h2oYc<8s'zK,fԴE>{%M{uwIv.! L3!,R}Y˓mZ>?F^CD 16?P2HRu1P"cǓ\]fO|'? /,Jݷ_}VKv@ g8aN=۠ͱt6u8`PŭYysk\׶YK}~[{& ݦUWQ=H{r5ǘ'24ɀgu'_nJ뀭klu\F14C0nv|e# *d]ޛ~S|x<wy%OC $|,llAJ 1<y S ͐(`vTv7X70AFW֮'֗9H 9 /c4"VQ`gAwK0s ^ehtU*UΟ_#_'ɇ/6S94"Wפ\jq/d7a06|/cThS4"IϼȟLkpeF#ȈNga*C:C2UڥmV/}[-:'Nq1NEhұ'9ӜO9u &=~?{uqy.[[JpFDVҵ\nt(03$T[X+|tʝc?0g( h)whiﳮ#sS'58I 6gVXkҲbI҉0~ 8jJ^a*'Md3\_*sn8OPc E}yvl{-4YEDÑ!FEz%CzRyI20@jox2+]֋Xji+י;P$o!;‰Y%^a se>j9*P tL_*M_Xs?* L3}$ {-T9@ph>R>C>_8UUN:F$"ȵ@@O2 r, )Lhx! 뚃 '&2ia#D蟘e> I '9888Xuۃ!ޗFThx O2З (7-00x4#5=Kcdt2H : zG$r+Ȉ(I^ RÌ KX6pсI>pj:F3%D@d osCD}L;ssQzgjuݭٻ"  ~}µJbfGe/q6;o|9un+ juKme<{nk-ꪉdYaK\vIT 3{GR2 3z4{f%./nװbÌr^/&ݺ~EkwB~Hq)K @K{ j ^P)2\byichgO:"7U2ڱ>&  )Q/K% &uvv[6Ǟ#(;x} 8S= /WXs J(N4$*7X=44nR;"%!KjmUA^&WA=4n#E|N5MA51 Yy6fQCЩ7EzH|O +.^|۾ovE(!_m*.>!Oy)Fq";SI嫼ϿOe`T1Nqxz8SG&wx뷿-ngݺAl}fO ߁!W̱mdc:zVĴ@ Hjt_bEdԩ#L*zuZ̓X Hd;Gf迒ecB( 3;9B؛2 $O$h&Hh L0"3nQP /iF_vVc:ݥqqfN"Qz`m޻|Pў y@t4:v&ZIT#S)mo۽_y( "$"L8(ӥe`*[ HCYwy3ABdLl6#!J  Jr,WY]m& ()& ۨ93L*&N [F"%NWZ9r ۚ)O\j!GIQ7D@kV eq,$zv[(05nM474n4k4r;D@*G#Q.z|P Y ED#WbuVmhJ!] m7"Bzt&Npr,Η%FjG8޾^4rmB h^ <[1wT{wޜ HQb:Py 1p{BYи{upRPMQrgˇN*He&8Ɨ_d0d$=%l׸8_Hd`{i8wÃk!:UأdQٴL@'傫\n5nR"^V[¥[mv\<ái cb!r<08QV `Х$dRF(Mf9|hz80K/MkZ!`` z&RrMLv-Zu ϵi W=b5 n|I=xb&p.*K EXb"8I*lڨ?V h#Gd⃖I뎸N'Yxέ*M[`v=u0 :I,lǡD'gyorK3Iō_8;~*YGDn\[>xuj|K6ct=׭/=qJa(AW0<8lfԠXږs,,tmT; PxPҥݴ t-jU / ُvz|7\A~v҈y!aWZߢilzX*&*"h Vvz¡6;lMyv}ƛ\[oа @!=9XҦt}K 4VDف$\TNBKN$h-nOSͦY^`v#ϖ(TZt%(A|6G9ɒIBgJz CQ2H% &:Q-C_˽{6Vb7`fفԶ:4m*}yעlod vY̷z/n%ϟa!Z? 7x[K`&K lU)vtuN}+63Cs[-M-4Op`r?GC}7i.,޶f 6Np6KE'О~ҵi拴äY]fjX= @!s%JW4ӢPln´]阴[m:9&BAm]m 0"I NBE< /Va,Ws^kt0.Zncz SjW܃ @,8.M;tr?SC:-n,7{^RשXƕ)^=͉`U[dycc T١^-rkΙ/#Te@*tW/P8ܯjWu5Ok8x(Qĭ:[rkEnϯQk,pgc,/ 67m Bфī)[ ;N<.ʳk12C )Z`Ќ!QVJGaM<7I$#Nn(cc#s=҉D#D#Amю~kaVب$1(%(ofeFm^[ ֫&ݏą^ի@H8vw 喋!r8;sԴ(]٢`:h:QP]V6N3sl\J$1LJrV=[_P+D-]ZMU&+Wi<:S!h x9ȍ-ZĈ+~cүeUiWU;Vh28DSrHkS[]b+lͽ7)=Fgpl,hw< 2,|#/e* *?#GmdN}.xCPʳz'hv~sopw8z;Mr FIe4ۛyV=lS|2WāFwBNP^9S 8tEn,4vY8* ׹N//R7 {M>}(* kIƲ1:ݡ%aAFm1UQ& uOU,.h=WŪw#Q ~ KJ<~4XD?ehvq 'u ١Uɳpc.4?@y֮?/qyS̍(פ[]ڭKsUj{- AjApn;pmX-Yڛ?^,pS9!&\vF#*bՂˁ.Ko[}t8\ν@,U#Z{Qn6/eBNs0B+kEW^>΢RZU+Tb?j/=UeY 3[xR2WXE6O*g^}SL`FsqVǵHx- 2<4@6'M@vIZ&jw4<5p&PTC!ԣߔkϳ:`H$G[dqNSH C dX.rb@Xi~t˄8q՗IiR>hU0SiR(ѐ lNN֦{,p |,8E`=]t[uj&Y:HT&E*&mۿ]ՠ^Rz^0tۤUvW SY24I"wg0LA0%GVu{ RdYZ)S6oHdH]kAPXtBy8fzN 8IVjs/PYǦytK%*՘PDfєB`y#"џe(P^]jmgZ#c};9V-u@H(iEskJKӗ;G(\MPSK3g$" axk/r7V??%՗! 244sm"f4}rr ijC%<ʫT;haDzcJD zAJITGHMS82rm#h?CVay RuNzDՔH%$d2)Rzgd'N6$SP.=WϲT0pP@ xZP$LXs֛r% B(f߫  v_ݫGhX؂OϓT덧I w? /> ]ەG?ѹ4[WXZ~.!SaG3dYt%\ A0& \7…uJt| ?U`NPԳu1;r8CF@G(ni;ܼr+_!Ogπ6{Q>>>>>;|~>>>>>;|~>>>>>;|~>>>>>;|3" fHs/>hD㌌07Y9ϖ@;^>,XϹU}|||>zBiA?׿ m")8>8›SxoΧGIǡƈ_T4?~2yVڻNIZ6ݦru~rk6@ʼn-՗}|||>-!#2=5I("0xͷ>g=y᧧?r/|,S._jMU>_>g_maK/~J4gT֨{GJebE]bs>}mnڛ^k h*Q5]l:UV&{M!B_|Rru&'&C/w.mۏYO\f9p W_Zch[up0uClrnB!1;5@d4LX%Wsj{T6b$c쟛aHY*%BgpD,LIi6laii]l)t<ēth46bjjNkv]hZ(I4!UآxJ#6qك [,.fd,_?YVoNg؋|q8nK]bc:7WԺʼF)EZͷF)ɓǙW_Ɠ׮l볞 07;*mO:SHMm2.aǘpa3Nb'pCR Yg>_|4 Lc (qN 8#0_vE=Nqv*F@؞=D($1c޹4lV: XtHw ВLHf(Ě =&#j;Vt]Fxp3^avA IDATo- $CS$(^a >gڸZC7,J}~6>Ee>J)YA4S'O2==kJqM#? ?-HfčRo, (cnq-%512,ߺrÃ@i&'Nl*Aʰ(&@sgyQ"%_Z`HN`vvg^jmr<',_3웞`s7 8<``ҝLun]: J`/\OL&ev#QX\Ns˯cSuZfK&B3<;ǑJ@ՠ~Xcy(X\Z6C\ZHxl*{ Œ rD3ÜMNFӉeijSNEMwqF83*7]\` $6ܡ)o]m=E}||2RYYŶ249:jFxiAC#MGYؤx@AG8t:ޛg~M,Cd7b,YQ[g"JJ%I4-6F$A>`TbEO gW{iwMB1҃ @F82'Z> 5SfHGzt*JY{.KŮs &1>>>A<ObB:AC>F6N@^Ş J`N I:""vY 4ɔF!3Z6M@"4> @TWt`g-&uWES>tj/c@ob(<ƼEDm]EЍƃjG$23fu7oˬol<'+.CӉ i,Zr3Zld(JhF!Lk^n=ZTn#G~C&+% UU'fCm2C4=`y*6+*Mf*΁}Hr2qMq#GKkT\l4g>yBf/}Ǟ! /x8'-dRA*37ZB-@fzl:^_UR@ +vlYخh9 $5 G=B*PJ:VM;prّ7Y+U)u>>>O9B24_xN>>>J) Nۼq-}үYO^pZ[\9^c7 %]N7xun.mo8{X\MeL]X~{/U ^=k(.lbTkzhA2 B:hH\KRf4"#LH8eo/iD&f2y`x|IwW7$w:|tQ^ek}<|$#cC  ֮"gDSdǘN5r0% [<: -DIf͵7ʴ g>>>4!o6{~&4?Xă}|||||>y|~>>>>>;|~>>>>>;TJ~nO'B K|`*yGhy=]hy w)~ pË_|0(FX!1#ģ'=._+NK6"I"4>>>>>Dl1vL/>>>>>Qv8g4?B')tu-:Q hA8$K5jRn;0L#;m 04$&~]=Syӡڶh{&tH0D2޻g=yj'meNBNcDCmTH]#$Oj8yUZ1nMGe0&Ni ~1 I,ˣF p:vS!?7iE^+h>j}$QOu:.m+'0'A7:9^>GG?D=rdN!Fc_pEFl 8zT.vN?BDY~,'j~O@II_bwP22H R'ɽS/rgxR.@v>%%Tj{zetMCaq}o_)< !u cO˟“?J)n)"чH 868ch}όR4O6vH)稶iC&1o9BB[ "񑃄S?K-ܞ~jg|<>WL דxJisCsV W]}CZx"': /.C:}*^MOJ DX[(p|Niv%J3Dd &bش VmB' ͤLm}Eǹ}0K$ʳȗMG"5p8@_AhKަXn4i:0A4sR(fŅ5*VZ#t_KweFOI2I?@S 169 Nq$B,*&z ͤKH{}+);-+q@hDG$.wO{"ޞ 3ӤbE~ywX,p,rs<:FF|8p,؛WL%D @FSatn?~pMpK_+&ↆ#e`} @/ޤ_ۋ޹WDRQ]]zGlV= An{V>SfB67Fh >9?}F>Y_Wx<+6s[wxf3w;yitūSl-x}a=pA2B@KtFXۦ'q] <<|X_7kr%>H2؇}g& 5&?Btݾi*&_DIVo2qfбjV RJ`>G╣dG %BU$pm:Sy{~rbFwyr܇iNj&r[#5"24CFݓ_&sz,Y& 0>EthOx0_^MCI cE ?0F\b8.DPCD(OPZ酰"]ΐ8N(cw$DϬg[DMҵpOJ,Q86}-ck߇8?ùlL6]:E4Qsg#km( TfWIŚxEdU\XF6϶TqϿ"+Dǎ\H6p;%+날ckT.yܶ zT%1s#ث;w߆E {{""+8WBvT55Q |DJOv-sG,V*=K5p?O@|6>oO>AG54IΓ%,*Ǟ/5Q%qBrVa5wyq7R :~qD8F^@j_C汊L B1- )7 /puAjz$3Ħ5^@v(ʆcvņo`U;ۇ.N}p^sC:[pSM%}dvi#$1Ft(աp#[Ǯ+b4m{->s@ePI~ Ցbpu U\AYDzġwjbD4Pu.ё1`lOlv~[O9j63OCyIALl/ѓ8AE\G{jx`'S^wt:xf~{^98B3@Iwi!4B ء8*$`6ҲQރa8 I~gvF2DQ붐޽ 0󠺀!acXAga~s}4<S8Ϙ׮bB4Xb@ A4#:x;#6$x BaN98C$! @HT 1ݗ=oFM+`aBFx~|$ܽm#@?w!THEW&zg];Zj<]5򹿽ګi ^ jh]D70q: %%{xUm- KJWH% qvqT$8ԁPJtSd }%RgB3 eC+k4swyIOB !wc Dk^* 6U~%bk< &Fܪ T-=@p@s;FƇY9uZ1̰l,g{V^ɝj wizۿRޯun UZtoi<%P@sPQD=حn={R#osOR سnA| \v>BADu ~R)ɮo :I_xL4d l4%FL`zeQwY}zm1&Na!x dE)hQ_{kC2׺ay Wt% I˖B'%?}|>>wxQ&lۚ#plQ m{ * "HQ#^1Dka=8pn]a=sW㾟~lmTMzT+L]I 74zժjDy}oP6hu)d$@0t(u$RD:/p=='\] B"Z$E'M}mv:B> ֪xυʞj<2zޞleTYBf=Q稵]uB !MeV&&Ї_$y(Ml@]W,0,d'CNF!ɒR Љj<зRЎ @1: XU,mZV`iq JlR{h>>>7>P+8ˣh 8mF_nrjev_FTϜ`yoVcY2GѪl![&Ji11c)z,{>@5- ϐ=]>| Sa A&F&8S/.dO5WE"P+AzrA MAh;mBE4k6:B!wxQCw&kI9KՍ*QT3 h+x˸υ@%FQ# R˶za*0b V]fvP [w/ΑyE:Vz͕MGBfca?{+,ؼusG8%,a.VtʂmH8KaRZc9.Blm1VOF=pߕ1 s/+8oܭ)#[W/^Dnkhi:qUKZ힙 {{7zN#_ 3c y.!"1ڇ忡[\Ø}/F!^7 xR=ujG㲵u IDATw9͘w_J{^;ͅ,-WG?gvfV~vÆ`p6ٿA.:6{GGi6%"d ˿xY"'1{HN(KTG]~+xhGq BbmMw7q29vl!GK>L"F^1-wsѹ0;w6i t[R!9ޏaptmT}睿CɗhWiV@`?ڹlng|3p I"P^8c(|O)}_abej&RGo!o8$|$w+Ms;_ J/^O8F&ٻ'9<||y] 9ٛ5j"zP(N ?P$E(nWp,C G@mо|<ԋnx .T_ff?{Kv"v`dgT8/d&ݭ#> K¥unįsp ,woN_" J4!Lhėrg!6@:vH[]|*O8!u'; UkDypE-n,^QWOG83ad"r_j@d}³7m~8CE{$wq񋿏sir.yyzWIV'irH~o.^%n{')l"7An#MN!ՋE=y\D[īKDs58f{$u؇MO;5Dր(sWi{)RCxHED}ZA$2"{M{ 03z:"mN&(O>Q%q=0z:&[ ,Yx;ж+9׏1IJr=1)D}̺ݔLdMh"cۡQg[R3SdFw1i,Qo?+a>UbzS{a} MG@Ȅ^-7!tp7=hFH[ 2=OnYCom:+\\ -5?}Ͻʁ T.i; 0 $I\n:v*ئD&!}nkVcc[-OMl-B=1LT&E28"{x|0lMLC]7ڙ{"`!^F/ư .Sn4HvYc8f"Nr,LCCH 2&t[DQ |LBޢj1T$٦>@3,R ٝ( #~##hhKIA6ަ~Y1>!J.z}˅am|KaUXvt6@$ e xDX6!~?%U%!70t }wjaD4c). I!a`Hīr*84Ivyo=!) ]DVN?lQ/=CSe"ߘ[quֻܞyC3P)G_7yG\\rgJ =7^{3$as[{w~ R9 ?2y O?}Bg?; 73&o)oܼZz٧_ॗslY-ۺʹ߾O]uovԷKaŖ[cW?tmqfde . KgKNnAzdl)N2up2W184Rû5ba9^}> ;4)RC4wZSbbl!#^+,li%L z%k(35>:}(-,S5]&;ssFGG*Ϥu:嫫#S/M;؆scoWY[z~,;>38cpp×V'&"5kya(I\ , cO Rs7Ng)*F5<326' t~#(gC/}W^=ƞlHI> cTz?~fgw1rGl::-7 pl$lyˍ -'JgxK&Hڬ[fkd|wb/2WF_J?`(I#:unw4,v꧋ :^jmPwE2^F!tYb>fhlq)L#WޖcҪ|B 24}Xbr][հ (X1aJÏzCR&QI0R)a&g19ÕjGH?)30i9閃mԗ>'N@ps`QV~VgὌM>_K[=^:i>/Б;Hy9U 7i|i︰١w]rb͡r7ySkm$;z׾[]íQn)gx-zvUYkD\Ea)RduZ(R팓)ZdOZ/-шA"q@|BG%2ާx!hCf|7#[XXعafP.St{4<7imVm&LH€0~Än`u~u/A!44]C}6ͳ'QafW"k7 ~F*!:!0 1fgٗ hϽǥ@:!l,\eq3}f*p}\ :zDmW`.w[*^GpA<Ğ:ٳ-#:UΞc)ڒ&'fh.*^<[CA ؾ7iV7Y_kѾtUf߱YƲ]y1Rhl .UaFwlP;&Eyr h5RBܣ^1͓d)g ~8txtt]i'ihiXvL$}|)MH 4Nʗ٤Ѝ5ThhިUo%ׇLT:o|FLuYWLnAӿ,K qMZ>^ h>}iR/3R("~'?*9fb9>Kg{]lP,l*+p8õԎ0Y32E~r+'tyI3qCh0${C(Wz =[lr%F ͮ03搱zإ& $ol3,;S {/7b!NeaFV ˣ(O@ohtƸ:J|:Clx NTN>E202eFƌ9nN2F/pG͆ &abFդV/ RfHLҐCn=@44,,[# =`(43]#fHڮuGke\8ws|J52Q-l |d"wSQAb YIL?ŷ=iu4K 44+K0S)3ioEhDݫs_=/#4]g8{eSgd6ۏ]w_YYzDbZAa]6.r&LbHr=/Ib 蘺ILoNYc%Wrna^yn+<5LjWnQ$ݮwq=C.!뤨T2w7-M[{#BKgɧM^D$2IHy66k\Y||] W 6[$"j~Cֹ{)Id#w8A&CqLxSEH*篺3]YYrz'AȾ2Th/4Y^q]4tMøX2oщQBQk=)Oph.?uaDy)8\^ٷo!~zY <~ba^x_)1uȘVQjBGr XkkUw g39!58HfG'\omw[Xđ,CR&7֐v۞}B`]H:sKe)}zDT |r>EEs.2!GHi ,Cp ' l;E`!_#&̽}8c~t]x[Wv86K\~KsNx¾知QXc |xٽǝV\9ukΜEiV7 Τe6WWXQA=|O(M8S&pCe)ןncfLiШn6hŠrrC%FT݄8HBd YGEp990Gҗ_cy>^*\8ȋ#-zDU':1 }jC&=Lxg:бlֹ~k%Nq'V&@y.5ݙ)(O$n9ddN!$ p]^nGNSp~A-Sl#"ѽ-In斁0~GVYS(۴}z달d# #} &i>QߥId֩ MPWN(1RFvã֩ҤӣTJdKDXab?'I:ڱA] Dn(ҿ{?18"'$X;sK,5ܨET(Az4޿ י;&*(lwl5ظ1gE 6=~=ҫuw|Y& il@B٢a[2Y%]}j?v}erqmDȘ$ڡ0=@iNB<oZ4Նt]v$^͆لץSM9iMU65LsJJr{Lֆ!ʓt1];.$x o|BQߣӉpQXYMaT=Ak#ÕŌ Bܘsĝe.ꄣ6%H>^D"%.U $b0oc9&=ڽzFqsmXޖ"=(&?"˥$OꝾLO_, /EQM8j(ROQEy| ^> Ĵ.ɛ]}Ƭ(?!@>}SXsSEQOڧlz'4 t]{WEQE^'֟(u^EQ' ~(G?EQ剣(QOQEy(SQh!!?:O 3'v9" wX[{sqZj[Q^tt;CixJb24H"KYgcmZ'#cC D#>:aOx+,s>` ;OX6DG" AK,SNK"5 8͡Qҭej-]`EQG-16=p>EʔaHHc er~ ׏ BV$  &k4Lzȭ[*+C&"cx[Zw:PHp IDAT(ɤlHIr1<'RsCc6pk\lc(D-:}ӄ(mAXaxj4Fo6^ NB0m&1MC5ėݗ&44a'vLJbK  ӱ1p n]082̰Җ]!0 tt&gKK? ?=?ήGy0~sltBn\dqA$ 186B!ejhqmPVYDab 0Oa- 6:+n45THB S$p6TugVEU48@ byaj7Ln°08HiDZcH$Ȱ4;C6mc[&!ID䷨ a3b4V[t[v2G1acXfPѺ K=İRd tG1tٽ{r?''h򶓃LH6.EVv#g94QhKwk3 ,74o=4)C 1™_!$33?y=9LW-SEQJPT,K47[m7}DLܨs Kd;~L1`MHS}?fNaY 칸? GR8:t?HM;l I H-vÈv{Q9ap"Cq'hG 'aϝpuEBiT_WsO&$2rY"~_o+Dw+dž>NHB֔(O&&ek8FH4)& ;l./Q eBH ۷>j4Ýǒ00w}>XEѡ:b'kU54gT 6U/nY Gˍϓ=]X8ġLZ 3SI#~}rkMZ.:W{+D( 0PILG$58Fɒxz]:]og(kWXθĽ*q4Cj.(_a:12%HLˆ0(N-'d'k4  $>i>aZضc;8 f`ZI7mc[\!2HT-E`~sZI+UrlMXI&-Q>@!w{JHBhh'hYi`q[}-1~2m9xlBiT9{#y4# K,..f;4T),>{̑W ;:pB(Zz_(ʗm{v#OgL Qʥ0NxC/1:S\|KsW$mC 'qGKwz}WEȘ D@-;" TL64@hN*0$1 A/V:GhT]Z ѩmyK2 l!-"v~`"7=-fb(]fH4!c$!:acnfX@$D":,W'8p(ǎavӼ<=oBL$/eLvPEdGC1XGՈE6.%ڛN첚llPkDRD!Z>=/T<!c.5GOEZÛPPtӘQD#L?v-_;rŋ5LëQ;6omNJ(_ vocY Tft|0!:8et\!JYF Cx&OTB? \qKklʭuRIAEѻuLFtU12il/dM=֮p94X 4M"bCC"VX| G94._dqIʵ4<xO1 l{3|\ōAX6N~#TFaM, ϿzK|8{O 1DR"eDgt@?EQb2oٸ&Ar|B*HݚBvbEZn$_g+p" 5ف!4/ Qg[!MagD?@-jK&k{`ي#wi6Yf'$诱ۼaXHӮ{UNKwk=SZأ%/4n=Z%Ξ6` [ ]c> ͝mZevl 1צBGMmWᐱdi)Ҥm]$ 톻gԣXgAr:]:qЯDt>S vlm/;2 }zk;ctnkj[`)!l~FCXF?~DK4M#߽ EQ1 A'4Z퇝((_=EQ' ~(G?EQ剣(QOQEy(޷o}*nZ8!hΒ8/?v'(FzdSGOpt8EϰgeP>w$0C<}~94'FwEQ@3ҕ ȥLG<<:ݞHXN\!G.m2:t=~|_'^s8tDžKalr[,~OJ:;O(/qo4]{8~c9qקDdrak!$"{MM) ٔ H\!DV&[(ˤp,MġOӢ|fH΂[]w@6QJzvE\o acE-ڭMo$mQ(v] V @4$f(б bmg'=?":iaYQ@pwnkG30RL`0or uL Uelr]L̄ZXy#Fzg٧oy_]ܑ"+^.EhV>^Ϋ;H9O5Ath~ę7O[_ <ʼn#|{=נQt{ai6vi~ϲk0x[|ίy.D/\`0qS<_w^~oiĀy?_dloKh[?2G:[Sh-RGx8Yfv Iv3Wjԓ1G), HZ+.-qz_)26Y_]ۧhiCjhޚgeuk5rʪ!R9PcL |ϡ{d^g߹J;sC?>V,+\Yd{W~=?+XP9+6&]9[=|-MJ΅^~^;ƌQ=6z)JS .- 3~riGm56X=Rdxjäp?9zO*0׸\>n 䢈g;t,[#!]I-4dp^fm:˗E`xZ/FF?ٷ93 6:oL㸲1@H@pQɒNwU {?OಽqKrUKU*I%R@̉Dy9HP(jvS'8"ƻrLn ғRX#fUJ 8S\"{|[`kͭ*LN%E P"9Vr xDY%6@JW{[>vࣙ*߶4B>b+cƼI$E$;F]z>i\\nng8} )I&֤a8`jZ?L%$NOk0[mLLL!7`Z/MoIq&R21fn%.]lҡ}osw4\m쯹?0 !M:;É &喉+HHvɞ2>5f苨\~*Ww_ϱ0=ɂҥswaڛ\e67=诳9q*4L@&sllƣS٢T] W 9/VJ(1׊q{kB2o9{iVٶt6u89򡅫F gQ (bB*N>.kHJɕITvg@ӊZd.@O>d9$$5v~c~@*." &JRH\epq|vB( 8XcB8z edLD?1@E&Nt4 5'$"cWS<~ˋ T}_6Рkˈ92o-O^A@QhO*4KWuf;[Yfzy. 3OD$>Da*CH`=iSުR;é\f# |%&)# HGX,!"$T<||DQ"$㾠X@ x92^dIS&&w*:C8&2G$~ցtC! raGo*dijL_a6ͶqP*SDd]$$-*{uz̃>|v>X)<(9Rt8<B:E6"=__X \IDATq^Wo괩uu8+"8$-v v8zg,MHqӢyxghtxX&3Ѵ G|HQ H#h]:msB4}9bdu,s.chsgGg`*crWOt?s+7-N0vc(aF"%'u7ԛl߷PHn|>Ai [ݫ&OD1YgeN4L'<_>#[=yk{9q}>=>=齼n̘׎t^,Bz"Aߣe_0p=P '__gtã%jh2LӨ>~ &1X3U5'ϢZk|%zh5ʻ%&/Zd*pK;7z OTEdriAaa]B}*zѭuop=CcGrW8Un[6 9M~.Q2Y>5uu5=AagKz&/#HV]n!<}t^T\m>- i6;8?mLDS*^3qt{Oq+4z]G{BӫiVi - %J-D}|Wc&YI JKTldp mSv[fg#HY`tjl~u^E{b2NgP_Sa0 0nhrW6uq;T*m EN/Đ]Vl×F4%}jnjy}.^@ϓaAh+H j8XX6Vo3"1 Q+h6m+9=[wp{Bd15F71 Tq[9{OMAD.p8"rGu0nqo[h;vw8{PPj{Kndi-@ V7 B$Frr"i -@G U6}˝{~"&_ޙ 1%0-sm?Fvb fOX(!h_qgFAbe{ hWh4-iG9R#_3ۊ&l;-xo'HpYpqr҉<3 "$mں:vp>Nz6B$ "0`8_ٮә"ʓ3ttMG{#8ZNqGUgv7sGEgHM簬ST)pqm ;Ϋ-ID0 E;Dk(ҵ,磭+U6Vn]g00:B Ih[mjLaY&O7N폪H*j$F,' 2ls&_&:њAբZbFA\Aze!U/3]Ix T:Ql0!"ͻݾe %Ulce<7#0$|ӦX9rȁ͠l5`9"Lrl^Pw|9$LtlY@wMٸAk`atHY%6(mq}+>; l@ >=ugO"  zHĵQ U޼ooюl%J,'v?|Ã;C6չ Q-x1i5iv5t'pGN/F5?E{]:][PQT i7Z=x#g OG1th~n} )QAH%<^pTE}~δ, L QqU1cƌCxl!喁+_(#APӈƌ3f̘$'2LY pirF~c7f̘1c|6}#Ē)D@z~~:7/x3f̘1c^h8FE,J>c#wOD 3f̘1/4F +&J`8f̘1ck%$IB8 eaƌ3f̘DA A=aIENDB`MEGAcmd-2.5.2_Linux/contrib/docs/pics/webdavMenuMac.png000066400000000000000000002043401516543156300226620ustar00rootroot00000000000000PNG  IHDR>vhRsBIT|d pHYs%%IR$ IDATxwee}){tA @ FA`Ccğ^"D!HHD`i[r.3 y^Ë眧sy~Cm;w].j9_dK|.>$t:x<4Bg raV۶sH8888`Y |8C]cȖe9>,s=> /i6Ȳ;8<mZiA۴m Rkg?GrWH{*?Ǔx=Ic[:wz=l)8xͫ.Q1$κW_v.Y`6{h& gh+3Ԋߪ/nFk;2pIk EںOC1tzq0m';88:S?֭g!.[~E/<'VyacGtn9ǵ&w u :N9xdYĢUU Yвq}U]2,wz{ֻxk/F%- 4PIZ\Q۶1tdYV}ـtV*vWCT4vqWLgk@HX}g %ZtXꮉ}8|{)flڽ[od7oR~P='LH .t-g69~q#V7+ᯃZkCb믿Stv=?OX~cLj؜Xt"#+Gx톺9 1i<~LTm^у,`tC|3_mQ_=Iބj{'NGS˃w}gQ~ӣgHQc|nձvpXkZ;nCP,#aق'x|A.y[.lվ.k&|}?'GDn6M:3ÆS򗜌$is-XN=HΖ1 oF/x|xPzPX$b詇>MNӃhAci2ѿK(\t_ԖzO3307.hCMm98MlR@10 j|>OR <^aj u[7l^n۶e&W?O=N7>C(DU4[,>2!uLw)HjrEsD(bUnOuٙ4zlz!UusykzR󝼣b, u],]7@|__2mDc*369=7`[&3fsc]\7QﷸGߣoHZ' ~|mݻ#^Nx_qTo]s#__w\,;88&m(R{mvog]o~5˘ַEVO]yKi%( g}.=(9x=D;IWllQB~NsCObX/y\t1=m l< -KYfhz\X`5puN~{Ȗ7K> ;*G)K>p榦E,PP L&s mcYpaxvmZ\Emֶ--͵y6ѧ>M;H$s_ 011xww[ @NmeB ˇsAYᘄ۳1 lzi~DMU9 CAX_0G9spp8p>cU_]y)I;GmS.}Gm ](KX6x^<1ZT*c#}]Z mzf"GY&{[KOH*wfo46olspXC߽|qΞK|*"}{P(Uظ}6mڈ[U b5hO"gՇ66l\$I((Ȳ,=&Gl&lDk{'{ajeIfIutwuP-I&3ضA~?BRrD˵f^^zGboA @c!2Rˍ, $\jmeh1'rl˥"4 ʥi=x= YP,W U*dY0twppXezl^߃RתUTH$Y-"s[妩Za6E^mH&\B,]ߋ]1?.U橧vխspx"b;w> "Z̼^\4k&|nֶ%r~ηXR*DmԶ ]n^\G(./hifYhȪMT`S;P׋m* J8ۮI ;^-{N*$c&Fm=95eYLLzشy3^ݵ۹↢%ATB3کM1_-dPO#53K0(0/ %Rvty&:;Ej"FuzBE7}pp8d(5l`۶ur)4Eql=q7bhR5)Q߲&.R{dR  #$DleBBX&Rp$ E ׆ɒ$l|L&ޡCDbq$dh \~98e[KE*U ל5ZbTXCf;dޫ64$I`[ꢣߋQ-ϧs GXk1w}~?a "aҳilB6p FbbϞH\nAvƼ#N34Џ$Y.Qہ'Hg߳ Q\蚔kcn*$gj4(K <^/w|˲Y`_-}JiJmo;??g:9!jrR29}XVkBHx}^LL ^W T+er `p~SmT%r<`(4kU "32cٶ=pL49Xarq/~eYK-<\*P(}]U]۶1M,$0_Ͷp Cgtx`4A<:bFnҫ=\vRE '[#0 M:A ۅ$m=lu]xw%T-omn,eT5F#pGFg=T5'pww=С7EU>2@suhLq?GgY֊ 0Uâ8VedxLzQu-&&$ ~ V)3914 |.4}[Bm۰m>L@I48LT*2=9^2=:XaHf[f$gJ455rD\eLCgo3"L024Z/`Zޞ|.O=I6.F>CR=_;' ztR Dj=_ŪQ :% Egg95{ڞt:MTFdњciJD:T I2Xh$K:A7,TE$% klr)ht~Ct _ BWͥttt mJZZ^ɶm| X K2uqy|z|D(ǧJ$Si,&;;dDcq]ţHBڎT,"# Terr׭6( ظ^6nRӿ u]H@Q5ث+!RH.W #2k^[r@_?RϋaRI m4-ITOT*Es[;-M  ==(]di"bS jNpy}KOqU+EfyB~{#ROK+ϧQe"/zHRd2l (=pxAfre!f)Ryx}V!;$p#166rc[dl>Oy@~n_ DCC{'>&ƨ 7h]g.J4446޺Y8#BQuK,ὌO&Q\.TEa&!LM,Z.2:2$+!PUJL:w&"Aw@'ĞA239bJ];wQt<^/NBg{sͱ]3[)Mmt5/ζINwx _0B8&M&&2F¤9paz:I[G' u1LCcxxNji5T|um曏$wpln|^Bhjn GI̒ 9iv0u S??Ye2d aKe?OU>EuSWc*a]~h75PISC֥ݽ bE%\5h袡.VgN&k+Xc (f$-ٸGyP$NϺenyuN{N|^TYqk.=]HDC-hibdə,bBh$<IUvB2yZ~/}u]l`L:L,FC"vNA DT 7uϩ6 NZЫe w.Xm9tw͇XHc|r[VIO388(-ϕ=2II2DOoQTZ,F)Vu:QW 359N]}-\Aks#(˥Ps&55ޡuէk]7)a&TEA+DV`܊M._0tD4#?:NRؿ*hBG8@u$F\Q4t[Vz{U ,-Ŕ$֎N| T|@~pN>Wkغu+e@, ۍmiZ@!BP Y6Chnj@UPZ[Zw1S>9G`dgg z7n%#==l&?[Qlۦb܂[̤3drEcѺzBssξZ{OddI\.&-u GfOR*U xki1jC[}dg|GEXh;kۏU7=HB ɋeEyW2 Zu M6@8 $54QЄas52jUv<Ȓ+U XEc#K/W5lB 1B_>\)euXA.cb|lvClسu3wdzk & Pʂ<ϋ,IvXضm`(!c*'f_Kk+=L:/uEJf*oBH}^n {B<њȪ 2tejB]slTٙ96:\3ԦZ$<+n M,@{~6HLxn^++E8YG:94QkvcVj>3w>$kij§kUf9D1 ?R HB aC*^,!"t.)J~6-tm4J>_qunϓO>IT0%S D=M uTC˘.ϙ30բIE/Lb!,ÓwΒ%PTؼyyDb1|^ues*mi;T5q%#ܾ34nz$ EQ|ww-YTI|.lT2Ů]n2w bi%Rc>b+sO 2 IDAT¶,t}kU"LsЪ`]Gyyx*?f`p?H4"N"1IQL"RThl1]dZ0-TUAQx*^R5x>l똀^6# t~#y(HH2HCשT J ⢥$p{*2bM ttzjy\.Q,u`8JGG;^ I(X,R*֖FdYFd.Ji+c MJLPT.n[/-N¦X*Q.7 uA$esx!"{Eq!jf ۢR̥碡.aZ TQ=b\MBtĿB|jLZU9Eb!P]ĢQ"Q.hA8K5x^CJK KD(2tJ2JIQhik>^:bP$2ozd6l/@$^2UbZN!ttv =>^Rb4FPUJ.W5`8JWgme0t2h׃CVgp(Zksx(p bDR!BKsCm~ԙ"Mu  &z϶-r|;. ,,Jr{-lbL Ep{=Z,F$kBv.}CPo a6 aK]Ӳ0L!уaY܋ 7AKsh˕Ͷ%+sKoCOUu-kOt`Z}+zO-W64jss0*;} b^lIJ@VU@q-i>*k?eͭċ34,ksݷ'ܶЪE⍭tw<m}nr-zeQ4@ _ <>.!vw4^C5y稪C־)˕MɲZE8)ihZNG^_{WV=y:!p is]ާzGV\ lpq$^xȲJK[. o##|988s8p1#| iG:a-pۥeM:888(Ea-8pۨ ~9x6p1#|988s8p19"Aӵ%QؗC$+vpppx>OsCWUBXc̈́o``1>޾ |>>1{1:gm躎H0t,[R^mfrtW0A<, @QUpd&~#䒗<mT%L4^^۶ETOW5ktUU1Mk%|>."BaE);?@c{7۶nZtcS;yǾ?9NJpwŸR1SW2x?2>s,GAs?!m 3 /=(TxQ1G?y:?]?y'?GO¿:5$5}<18Cco#Y^,S֟7m̈́'[z"~z9/0 yԖp#Hҁ!cU.{8ms3Ú trC,K.YvO$~?P>۶xhޏnn|}1LT*nMe&XYFs=J7]~.dw=w<!TC0-47zB:짙Ñezt_)a5&oi[~qrKX_'Mqv~}}+pul:T.< !Scڳu=x{mYʮMLL(s=F,N#3==itø=gm>D4GMw\~ {}6wnE @ 0MôP Cm4 ljao!$+cscp6A\GH|mˢ\.a&ˍϻwZ \RՐd׻pضMfb}體1d}k 7h`{F\rKG#xe*shN ?$Q=WyMDزeˊEQ0}}}zS<{ /}5] wy?nh%?o$f>;GyJi~yO6kn'ln Ϯ%]w2V!Hh4䘩׿Iɥ^-rm7q}P*kxCye勎C%,S片l'ͣl8D.eD%98 ۶ɥg>K?}_6.7շQ ڟuC;£t~܌NJEy Gfv|B!,+R)"d/mw)~νPEo꣋wn^~ ßwٰP׷t顧WױcL| O x՛Ƿ0B4kfEbObXJ @`sK3ܽI7NO??Kˋ嵕8~F.fry NekO3Ohl񛛸?rVdzA2vsN;m<,ƟC:.{ŹOZFOqw\W/{?:LN{.90شzJj=66zpP-dɏhVԇ?ǿ%NYo:^Zژس" 2N;e_ܧ0Oo{ 7\unߑӟ mdwP~&ͷE+6^|4x0D,x<,ׯxb1Klkk[܁;;4%n\,Ҳi >{ ،OW&gs.uvǭkbbp粫ވGo9礓yadM u۴r^>׾_~#躁$aϥ LfTuBF^z>l=b 80p\c`3|_S.]o[T!$y3>6F>!_(QM'cj:EXdB$DsKnuq$B]`~+F۲=L>.[n?lNƚկNYRp0$0dI:2lQl ` A7_ '-X:CQо`qC=ƯٶM2IJ, ===tI%fY2 lW2iNaW\pJmft7vxI6w-ęs={ = &:8n'RQ"uA{W=rȲ9ss_VZB&cǟ~.[8t]?_ȩ@]u\ Co?_^jNXbCY723?z8<^vmGeJ՚ @w]zzzx;I8&|k?O6 c4x!&g4pK8ܓ6?\v~p nsRIqqMザ3j=W"K"o%?3͵!4o-{%g/f:?57Lm6Å6!N9TI(@7H ilM?t~Mj'  rz CC|_EQ(Jݻt:M(BAT"-k y/ۼ~1w3O\ϵwl`}.W=Q{Y<_s2'6̥q]l> &36_|͖ Q;7ʕ|Vv)Txץu7󥝻8ḍLw"W\J9ܢ Cheo^+-|6;gy5s7ݰoWyelpWcbF:?򑏼ZQU6L&jFeFFFajj 0x:iHD/'lJFVۺXݺ̾W:^B}} \paCz'c%O1jᵯ ;:*ã.#BRz<1,Gٲe3>LKN9~#,p{t?7= IMq6<.IRhh׾:4ٙsx$!Fl=D:ꗴ)_zڙMj.} !ֵH21.7p;;ַyӠP(L_}7LF~V)1>я.?w%|>g]blFq/79+yU@]pmρ.3^x 88mc[6m C۶czxH211xww[ @_9ft@dY^v乆wʝ_^~q@`cƼ] D$j͑d7\Jy1-h:s)!+lXyﱃs'憃1#|988s8pqT7c-ve?|:::FMMM.N裏222C=UW] 'p4pض\'䨙by_{mmmݻ9ӹ?ƍW4Y|CehamWA*%LAp`Gr1tS(444.i:\\G8v=㶾g?я644& ҂$IB!TUK/E>ۻlRCnhW>ۆƶn."cFmۦ\*=([am'_ӹ"'>Icػ{gE~/p;0-xi)N*? Dg񉏿[ϫOq^o|ECak^ -}_ 4馛VB!FGG[;gYn&~?wSSLqOO iU4N~yx{xbWݎ)Ueώ{+MǟWܕ4'X!MwqC}\?^ wGvM3W2Ľ'#0VrIOEc>.tO𞕝5/`6B|;avw֭[innfǎXE*B4rg㪨?_ ஸnejVh^?zڳl,3lJ3RsP@Evp3?@R [Ks̜3_ΜswkwMg/|t^%1L\] $`5VKHey)FS ~n6 u;nnF A]J@(-.ʼn@4*%jJ*:Q'I"Ɗ2*kQktR$bsqb2R^iDw#BQSSՙ1pi8UVQuU:2$IdoO'6u /W/dn[_m w~Ma@X|F*M{kjA$Kooz56s K~ ۨQ(UObCهvќHHiǞ;Zm(Tj:=*g'/=:`گ8c)y:Gq|:,V;BP8Iѡ-DӾ`k 6|o2E\|ܥ^i旍;q [IREJt޴6rr5 @X$DD%Osmeg`qlUsجf ^uTÁ0CԎ#6iۥ?< عq[f#I/`$O0zXŒy 9pڬa.'϶Ys[P b„h@Qn}~9;Sq6>[)_$:9o ߞYEBlyeB4>p DOZARTJI7.Dvg]7lπ8?yֻ-)i{w8NN'tIhz4:_:4 IDAT8D'5]?$oNaͺ̈́^j׋7)Ij;ZCNG`xQA^$ņw1)7F\*<k":BX|Qx?wc1=Ņ|e cnhU73d6y ўqBҥK'@Jj )=E۾T4f :uEr'팽_Wt8xmz7K D%q\)oܙ0_Ϛe<36U[-8v}P7s ow$ -m=7zhF @uu5~)Jݧj(1 }uPVO]m}wMi(d-PŞ^ J 88QYգW 9VPlN1'Aň6#uJOtx3@Qk:G_鬎a>:{;F^kZvYH[9\1x .'>|y.kZ#Ҩ%_~TUټq^$O˭~JEVa$aj5#\uU|J}NQY\oWV4jg}) ݆/bdUvv$*5kb )x+s &zz22Yd'K/y[*lEKD8oۚVW^fwz͊D%2 Ym6xW3hPjtjs(ٔF.Z_|m;n\zVs5u̻jFƱw"4dLqޗfk)#wJ:=yok8;2?ciO+/ywX0^JImv7lR8wx/^>J>߭VStdcޯ?/c\w]APйkO6]Ĝy (FsN5=;ȟH^ЅS9GLKTzD^mC.=W_/ګT{һw{mwCڿ/ȫʫ]\E8cфw\r݀ʩC0V/#s)ԙ{k$a/XARa(* 75ҠQ 5瀑l?0 C{Y"lq$X=_``  vna.S\oBmXE5#oO0*%7O٬v[.TUO(w ^뼙s,}N_@5gNI!tV@1J(#nO~tV|lBйs[swMڎлW"+n՗xhڟv{X km5ڕkoA6i!>hϼUWU}D{DDxJ|6.j cLnt܍U63͗QK6 $s1߰<|ËRS9؝g-  Ey_BvF%Հo`(q1(z= H&uVTZ-mbB!LU<ݛ6,L%QmP+8ve$"4AMdgeNHdTEfF&U&3*("CZcr iG}i;`_$IR[EFqlNrs7(ƚ:j !QD4i"v2t'f,[ Ұ!]{F 4enݺ5z6hl6_tX.J{&###gOT2jԨ>E#/撑ddd8'##s!>+9\qrΝ;dwww:wk+LKLuVf>Ƨ~Jvv69TUUcx$:XgS ȾI"2O[F柆贱qb_xIm|eq{aq D'ָleThO@T_㶲S')(RC/>0.YI߇ˊ`}wgp< th" 7Yuē ;lٱEYO>7_y;ANZcItRu?{5f=Eox%em̘1-Tk4~"s?!+v&{86NdADE׳QMaQrEHJ5mbh&IՕTTXT͉|rǮ3}4J7QQ͗yJ "8ݯCt?`31CnۮF<S^ѧK -B_O*˨ AXJE}M{jhsZ8G*ݺv⭷zݝKW[ ^{k: 5)ia'BjW?&<(Rf KWoയRڡ;fEAoO~|qngчKM93>2> .ONZϤ@!(+[wqoy{*,u@-#Ӛk~;7SqEgw%I"}>nnUv(qՏOȴ_;[9A *.?HECP S ({<"kqk!4ųgH!:Ӊ ٩%ן|Sણ^$G=ERL ׻ժQոjC%/9KwvӾiȈ]Q)tjFd=,") 22͎5 '"ڌ|2DWC CdP ٌ(I:Ww b$3}bHz&S.eʸku4UpZVVs |("Z7a^ߓ++"pX;:tLbٻooM~}q34鴲jɗXVffOS[UEMPʊi0ߡ-VAP1)Vlu.Euqߠ@9^B2ꫛ?ΆMۨ49[j͚']{!%.b1QkOWCuYb}_2R^oqNfgQTPF@TJ+0ѣAjv͗<ɠYdfag5l+eEЎQ ~ D nETg-q8&e,f #2T$ .myw1z`HR EEET[D$S? M!.7$}0*|[ԔdeggO||<@||<Odd$nQ(RZx_L@nP(E`jL߾}۷/tםRro۞oW_a}:r2UnR(vzt1gg+hr|Q]Oq gjrZѪ=>AP*vT*(BCC9~8^^^oI(++}԰|r WǑݻY/-c維1`MHAOAwӦGWx̫ӨI̔i_ccnN^7N"]2s3ٟ}s?F@Us"#5йC2Qtd>9uK7#}#l{6dNɤ{10?zddZR$IT[H6 K9qꗟx{SUx,H*mSmsUKiRXp]TqU:ubSTTqww'33^O~~>%%%:uoFaa!v ϫ[P*Yu`K mxpZY)<\SǧOC\Ftzپ87m"W㶜\O≻G2`(|4˱[1c(>YP{0=Y FUlX ܃K Wȴ`cے:fҗI[7 $ M"W n}y_udX߄3L\>1kԮ -U]$IEa2.YՕÇw;tbٚ)JJy^yI1RcCRb0ӪˉN'fs-uf www4jU rQfTkP79lvBV$+vVNF$DI *5nn:N/NE0jL&Txx7IZ,筝{r.v‚, .F{IB 5!!!:6ls ZR㲺 `;V\ (p1bhKP*q5j8MP;XJ 'q\ O󔥳Fuztued. \ jf Dmv?ZJ}i\.y/DOBBG=Z6%/##2A^yſ4dd8ddd8'##s!>+>QWWGVV{L&t:tЁ<=>Q`…9rӉRyfcX~==ݻT222>%IĖ-[3guuuhZ]DQ$??Sr3bju35$I7HHǑi)LBs2.~k[kO$9tl>/MѠhp:̛7777 p:l8ŦhTͧ\.$Id)Z\$I5l,dwaPOTYŽ]{:$@nw&+I1#WRy`HpkJl\$IubvtC)knap*4t7mpD}]熪V|Vӧ#"Zm}b!99rEEElݺ777J%:?XΫm4=M#FqUEᨚw oeg n9Y0o. ,; ࡗR~ޕ~sJl㬩0^y; w݂#eixYɔ[{Qoᚹ{ 6fؽ?+I͛)((ͭ1KDjj*C i,o>lWYJE]]V{='(R(SރA&8CQڦyHRɪ3lڛ}$nw6ɕ*5g̟S޻b$AIT\{tJРX!:m+ ]h9Vs !3;S (,T< o"#s6 z1\I|uekWDq' {C.'PTGvQNऴ8'ӨpH{\V |f'NjP19-;!y*I(JJ%TTT\0𝋛w7|=fC\ݻ=S|0-icŲl3'[io DSxd'3>E|~{W˘ Ipnf_JF ~y'=G #:|L܌w Yx {ֲ|J\}+uJf|C0he*JP?Q$9,l:?n܇_`0Z+u_<|nQ#6 Ѓ/mc\|sо{e6PRSU2&8 KLm/|>Auu5JՊVEc0pqi*j wZա0Xf_KqHXΣ>nvJ{-K׫VK/&X{peC( N䞤0OiǤJXP& O?U[I?YiNoUiiw:o_wQ\KY{fx̞&vʩk9΂kO ۅ=f|֚"L#`{`,zĚ*Lf9!AA!Yi7kV,Ox;Jc`+y8csP6ZȾ\_7DE{y[u4lxIBBSLa֬YݻIلf͚|@np6 uvي(dڇow>s'~Z#dYI soa(0ۡ<혦vGkT Uw:iiGInZBht=)deF\~޻ 홌΄.jupAj0ꖻ As (<~`t=̘&~άgp[BAvw]>*pww/v@۸8ƴA[Hjooopqqa׮]8y<<f3seڵ $Ijw{vNChq9ǣu!:܋JtA'eϣ/;® NN3&ARuIT~ Z#ϨQ+TZB݁6pm3󳅼\=Ꙅ?KA2[lO HMDֲxzM&Na,eCڦ䍗^NqUҎV񹸸8n{ؤ IDATyjR3gn:  Npp0~~~twsdaj=]1Lgl3eEFZT*:vK{oǬ c`RwT^Gc#!ڿEŀHṁDeu97= Cq+S}ȜO?՗<<@Rf47r&H @Aɧ17k`ۯs E.#s.^1\ӿ;ŹYyo-;,~tԉ \HJJ]cg·p%[TV | c z=L:dbl޼WWW EF^~Sb`az @ ( iILet޹5NQ+$6UkZIjrD&wƧEk"BBQ:KulI(?u=^MMU5@F8$+@ղ :OU?-=p٪RO+0aW2cZޟ>Po@?4!;7fnxx_sK"X1CSC `K.YF|ާ 3DQ͵S|goSTKbccٳgFRB@TΝ;DբT*{z:G}f'1FNk]vJ@q "m8%yN=2IܓP_OB4пOGWR㨭HF( vCPQGfVǎ~5;ޣn$$'c*;EΉrN؍}(pp_<<}hBia.iiide}'."f@fR\\Daa!j?uk|ڶ#'؋17E/0ku![ d]Wn^-dq5FBNx_LiӦU%&77>4 *1Atrf#C{ s;NJv O| p#P(|rQ :8l&,Crf,`6k,Քrddvu.r$^ 'An#*! wF%(Ucoښ*v;v*ufv;F N:=hЭyUe'ٲ͋/Y b ֯_{RR/%ד:t3ca6{E$AUy5 ^hg^=Uݨp_@/(eɊs,:lԚo8e?49E柏B˻Ⰲ^>hy~D՛vcw8qWS`ꛯVIїYϩ;71H9wFRTlh^I+FFFDd%`x1x:wHM!0 ]vmd(j+ɝ2t8j(=UyYZeddVYHV?U6gٴ7ٶ>HzFmsVx翚6wӟ GދܜXSLڑc#8L\ND,[v{7~vH:>mz4[>0"sg&q0nKC\* |1[ʠaW8tnO8/&?/_FnQ%=fİ6F&veGL%]D2UCPPH4 mh" A'$|+W\>#ط.ed(:42$hn/#O?odUp\'yZ~zҍ~vAaq a+ÆݦEml.k7tcyX,O3f%%%|i;KWOWF@ Q$mYQT3\^KsqqB -DzTR ќvZMu(D\qȁOFFC|222WrGJDee%gP ~~~P\Z5L&x &H^eddYH)R3v[5f?άYP4[:u>iӦ]TVFll,z:/HmDjoidqyt`(h$ITN:K cMEIMSYSc)(pΟqY"[?Ns6k9Y~_͝87Wz_$qtZ1ejv\ =kxɔ,+:M7boQbNʙ6e7Sk?ݵa{-Ŵ(l۶ 1 ;wZ9ύB֬Z/ԤB<0b(O,*ޑ b_&܌=Ly1upy.4r I"yGڛܮᅲ@;I&O3x`B=3>md0Oa+L=)q`TqYc| [ԻjM/a0/҈2,ZWxv+Q!r!_-\)Y8}-7gLZrs2Yr1)ݹf@:&Rr(;vi<~,9T(KFr""y}T*탫֤Z9z`;?rc6}A8j"x.&3NԆ&>$.˛Àkyxt*%}{vIOm&F^sU@Ǿ2ѱ(Bi:& )m|e2bxjRc#o#TXk+ia8|pf7;E4u5]68w[SPBb%,\$Z6m-hJ|T#Sx gpg^0g2&?\/}(p>=zvC0ҹ#TkjJٳ}+_Îنl/1q6;T(Z-a!E|G6Beګ/ 7xCvrFa\:Ɔוw\\qsQCH%٧H;KȡU * NziAth߂C)xԨn%J2d$/5|αA|:#~|Vێs1fa1}?++,$33(aA'V=Qz=5غ &m&rGԨ6]vQRRrexxx˕BE'UF%e [IHHrHOBBWO7妈 mذ7(t9d2ݺucСuC !"5Օ "U[B:+!({SmzeE;qC 4" km*[Nn4aj4:4bU ^z%ZE֎ %_ܸGʢ39Q7AaˆVB NQRf/TUVdSQi70_AE' ZDU&,QYaTUf 7~Kh\u>ՌdFa(>CaYMcᢼ,!q-mhcFV‹/3iLye͛g!#\t?OzYZSKwHRSu6QF(9䃵e w<4xBuRNԦQr=0ly_2{Q :-̟>[9x <t3NgyΞ]ќDD'Gvm9hZ:a{G,jGz+y:aƏZxzo_uTp]D"dkS0ۡ|=,LN %ȽB3M;]7)k J}87Ԧ3V!hi}i(wO?F{|=mG4ALA!>>dsgL#NNgSS?b˪nҊOgV0oԵ/nT`T5%|՗,{£" SXsF! ~K|Ž<;9Ee za}:p:u__|[S&"f9E}EZF %鰱co̚- T+ԟ8N@H ogwyj ;¦$iVIN2bw]vq)Ѫmz/=]{$ԳVanE?A>b kPjԸ^PvRm;H}: _,~dgj]k%} yK>r$NnՑ?$a:Y `³ B8^{CKKl%u <(v`qs -{qϝqS1ܣi,ڴBcvj}8nIhl pQ8mF~e9q >swBHYQ1:yIO@n1{M9qsX&/gFEt6;:/jc qt5 6mpS6Wiֺ ~ 1v ϫ.ĵR@@E̢(T#>|<50)~^@p+p>2ku!0RRaF }s%nMԮZ~'P+/cu2,Gf{,?Mr+ db.8%Nd1^zuHiڵzF\ ?<J8M:N2d*W]$!<|' 8.ۻjE k׶g@!Rtj0\%: "\\=*S\58MyeȈT}-~0o7jWή]رc o>K>lڊ>]:$|]ƏK#)1˴뜀RC~=_}މ8wKVkĢnɐǜf{[J~T8]٩2 N˱ݻYj3 !lY݇N"jSWL)MѣS;B|߯Vo䫅:Eiv:|-g| WBXYpn%RYcAQ\!щLrshSVp'3rk: }qk ksE!CuGn7}:~_zw~Whd/̺unΝ;~LM@sUy՗6M0a̰(떳m Dщ_h&<8zW .9qD,yg v n0GTlˏD&8{wcמlIJ7^D@ĿI4㹩Sl]V "v@LD~nQUn3dATq[=Cf)*(l!qViRZV[K\Ey vi?d*O}lg$3|,V:/|}ugEQX^Bqi9}P˝N|jAV\| 2[P}к"֚j &ܽp<$n蠴[ZWn nU8.Ag3,2%Q\^'"sgPR^ O/6q9.VY L3NB)JHmAA*\ "Ss/Րq [IHHrHOBBC2|7}s:DFFUUUF6mfWKBB>Q9}4?#FIPP&;voѴiSFMͯlĵpS (ݻs3Ķ-e϶)8d*i?]]VNLL%GۮFf9s搘Ȉ#Xh[lsΌ9JKJٽg7-"%%zrk""" l(oX@td~o&*\F~_i3>epY#;0['24:=w0wND~#F5|(m6 {/֭#99'ҬY3cРA$$$0w\-ZĽݻٰa{eG}N{ 7l6s$9qD2l4!{s\<x(eElݼu˿'Q^,!>WFB⟌jo`潌|Ite Y0s:r^Obk-Me9Lq5h9g(ϳ^/'i_EW=Ny` ~o~#.60+{Y,vI.]YaÆ·~Hii)ٳO0~xf͚ŐCݻ7wfذaxx4C8Ӫ}SuWQw/i#ajwTz$!Ѵk֫]t ڵ[V,Y+XV;c1S3hBUѩ{}ra+QkiKӈ䂀TDDRYOzf6VmhVEBVk~۴я<ɠP*dv~d$.\l\QU擗G~AJDvi*"} k==0au:4|flѣGQ*$&&tRjjj6mnnnX,͛ŋy "yW2-Z`5hDQ]WèR]ۨ蟀[,LcGX)B0#Sjcm+^.Z/q7{a$*G ”FTޏN`+5Rm&sc住q* Oٳ$7+ ZCɈmό}inաNCB?.d$|r2wD5n (9|3D&ѷs_Dti}J REy",N& qs5}AӁp=sEEEݨ[_1[?Cga0 %??l6fsá#[MItNds2桵ڇ(D;1zh\4jz*$5iEl(ĩrgKR!g[%gЫc,2s\z]>NzFyQp0?--ğZÊXl=jwo'o#ESM!oVrK,|lѷc tds/;-rC9˖3CcrV3jx{zK}5QY,͵PŪ5P{zrj5syGQQ(d2 E]LV¿(<: JN]nzn;@.|1Cq=;$^L)Z 9x$8d}A)ѤYk9f,`OZ>] ̷2 1{m|ϜΕ Six^z9O?`$4FB+"9TWZO^SAҖ58"ڵ+ŧr\"/ycIr 05CG=L\oyF5|* OOO a˖-8NX|9ӦMc֬YFONDDdffҷo_ʐ常\*-NBNTijI;C_>"Wijs )\]Qf(dFP~u o\ 7 UV JBxRLD KMI1TVr:45'JOqN,7I~ݩM* 񝺢((*222񧴡,3spݵ +JPORe klV l"J>^@23A%1cUF =ha1O>`ozQ֝}{g'?Eލ6SlChTçT*ѣXj~p~_;)))<1 :u 4oޜI&q/^̰aPl޼N:v#d"L)}䥗2ujCƖ؜ש9{(e/->h ?.wTNv=n&yS" £Kcݴr[ ۓ`y{]_]%$R<ɬǞdKy' P{ &=^3Gf_rs1|H/ 'Yv?)ݨS]AСaaa,\J~h޼ym% F#K,C > NϞ=/YEvM6Шϋ4Mx;T][9,^VpPe*'1 LsC{u_h|:_Of΢Gb;g=D">cVYཏ7i{ٰe/-:#&"MFƉL%u*(FT7CٝHzf! "o6mYHܽ.P@ʆپa=wU Onj&L믿Β%K3f KˤI,[dL%e:j ,!&5M/pI8C+ow<==Q+ B/I&SቡIIuEstG<=pU 29G?Sfٺm|wLICQ.kJǙ }x{poA=/֡{a06Dmྵc#,_<Q۩:E tԮxxxԭC1l /ZɎh>MRY;t1_jРUUU1gRRR OIer% ^kLnj___xj޽{<\={1M]BR̥>~诮憄-d$$$n9$'!!q!> [a0HNN&%%jjjdҶm[v튯G]79NvO?k׮ ٸqcN׳{nDQ$ ZCDDz۷SXXȘ1cPHri%$ E>zqQ^E͊SPoDщjCTJ/y;vzjZlI@@lݺL@ՒVy攗}v૯"::Qi֬vp`۱lKVHJJl6ӢE ~,x-5ջf3Ok2gg9Y{8' 8m[jkC1kW'_s\e5HEy۷ Oh z Q01,^җ8?3j'=(@H<SD1aDJ',Yv2kW(t<7C. tr4Œ@dd$MHHEEE9rСCZ-۶md2! #eee Fs9ZXM;TCqWdV>\MV c|Fjf 9ǂk V/[_D<sx7kc{!:ml[E"cU+9p$p8!*9Rٴ-ZDk'DzHINۉ>( .!Xy(5W*2AN*d|0جV} |\qk(9u={p<43G8g"Ko.BQ!ywvw^xE1ϏF5|555P*puu%22B ++{RSSC=ٳ';wdǎrr9 PV^`S0pZ64]KZ5W Il\,VS)NDEe""UUAĎ?QFĖl޴6®٘XM}V{`ѱ-ѻ)پ76Koěl~i +Tj!SN:M\SKk~-UBf~4F"-.`Mc%e,[Dmx¸X;6RUiA9sSi0`~9 ^wOȋ/|:wUo&?"-:|>AUU J@@k׮NLL dggt:릶2 łz94w>z/?_K4k]J =O8k*ilX{kY&=>:WzA{*J; vJldȟ'}WWӡT*Q(`4Q(8dddjZX:k~Nnn2ZwaԠ|| ouWEGS8I?~ &QM`NN.] þ9yh^lbvo,mTçVٓdAfՍv;ڵcϞ=( :ẘ+\Jua\Őr0XHuaEFiQS%sg~RojEuIewSÚ5SNe#gE0tX6YFDP/RxzK+Q}"C!X}(Z+!q+-ԗ18:/t:lGVm؁bkz1e(*oZ!{?ƍQ\ƈf@ S^x%_}Nh+CF5|4i҄UV1i$Z-YYYR0Lm6z1cP]]MFF߿K%/F>Ƌo`qTU ۝়ܓh/EenA"ua BbZNPO&*>xx.:IZt)5Cu?s2W ?} ?ZBIOKfuC֬\ӣ wή9Wa(Je6o^뮣sBk.L|^^_|˫/=C׍uPP0h 0 jztA۶m ..wwwN'nnnTWWӪU+KTTϧ{5 iىG<%%n)LN(r'-NN%?u_hl=/-ԴOѭN=M6؄xo_Izv- YDVo؎^N}l?ʿ1>A~?;`$$Zd%/YVT1Fpd3K\4:OèQKݽ(8v߿?=k͍֬&M8h yFgtЁ:4Xx߅t:Ci'ٰ''CGÞ#ooSG6w@~rvk|lv])'2%zrZZj @X4"D7ujk(k:%){a3v̝"8@+;|,,[+7 Ϻz$ D.swr(QjQDx˶/:ȳA˟9{vmM"yKg9>eʔ1 a?L&E}j/~dF{d,b\_P(9AkLk#qF|bZˣj\]]!$$ !!!qe# QTDDDW^BBG:.!!q!> [IHHrHOBBC2|ɮ8κUPI%$ޜOE<{ߏb!22!CЫWjZ& E}âӉfCP"7c;QG"St9ŋ)!AII~߼F܍oQ1WWaw[Xz蠺'2n%/Gff&mڴᥗ^ŕGtR ;voq#w= c@t8[Wb@r#e,} Puwޜ-}d|^f9%׏ d.tDQDqYl[⢛\%(<yx__`ǜ/"5+)={YtXy\(Igψ:{mij|roNhh(eeetЁ.r O>G\&hWFD` Pϛͯv'I 6ߗ-f<)tl(:Ԙq8_R]m8Evo_)Q)q:l': !^޴!X+qS?G3f"Cc̘9瞞^{i;eNNs?EKrC[ރ7qau/^|/]ɦ]ܩ) `"T?79r͛73tPp8 2^XyAhҤ 111\vEdh4.ȱ,}+my nd>S,\Dמly6K *Dt: (bY*)fnh75=A>ONL.RA(d6A.G&.Z@|b\\Ryku:XvTj5r(:6URcPip׹ci$n(Ru'&N$ߋ`/7ޞ s}Gǣ܊2{E+{v <<2x<,_Gi.ok8f>Q)..fƍ); /d2t:&kr.,q<̓6"*}yG @F뎽18U3( WTZ [9gJb"<~\8)X/i"dU#T^$5d BD'PpwBy$mZˠ :nh)H=HvY9o{;hh":1O͘K@TJ&CEn[kweۺ_Ye&ƍqR+""%y'xyh_ۓ"Z&Ҕgxgl})#{ @1/h43%8r;tJ Mߔ65xnn.*U* Tj'NpqMŤڂRMNioMT`}JA1|̚5f/8|vNۯFRDə rG-9˱XNj>&R kNn}weڵu[ * D:+h)^f65&O{G3rL{IL,[5a\:  &0ѼKD5 |Qj(8?jlsqt,AAbNqT}}{>!k!+6&־4ۇKNh`FBb,URZ/W_# RlZϖ!(, ~]gv]1w ڷs{w[h:zwir:խ$--wy* fܸq?ҦM.AW׃(:(8MHd$M-J u7|>5^feo`:(wzD9\ǴYx]u . ~+HJ:@2o 4j\_j7\RAeifuI[OLV^$^\hqLR8VOXFфE^@jj*yTU9FaM)A>Qq8(30繡ntt4pEçT*#(́B}S UiQ*/dQw#VseXESE%C`.C}{&C[]G]㛱^rmiC^6;y5uyd.>5D!oH4 u.l7\~ޭb⋹uw* \mq0+;ӏGEZ%0_i=F ))}^)b帺^y$T8r*1uHq`/ؗ=EET[M)' BbEBȲR[ X9qh_- [)mxQ1bm \4TdԅonˠˠexxQiٱ/ {mlvйXčpv<°ѩe0_)Lg8î&N@E!>{6&} ȑ#3gXk``ڵ]yKAزo&9Ged),Y{ygߋQtP{_f' `7lW>֢M{,Z8YţNkKǘ0n:&0x|$>Wf)Ӝ;l3;/>d1#JVy)pAp?ezx&j.Z_I $&&2f,Xڵk/J{!>}p-\'Cj"1GGd>zMc"*9/G~?wEy?&=̃GXG+X9WD9=J>z/|$D.6kzav?@wNHKrIᅤJa4֠3qCOs^6$^՗>]UO+7S)MV<"rxX"f-ۧz*7iʷ_OW'qh&|>y".R5JE啠wo+(=}mӡkw}^?ƆR/ ^{\UƖۈJڭ/vk ;6smAМYD/QPѲ! ӳWo $o&e@P(lOK-s4ƺc8g${vp ع ^u}-i(=CpZ-%ue'ټ%6iߺEKNgcv.::vILD'sYD +R\ S(Eʆ͘qӯ{gvn"un 5{%Cl]6oڌOT۴Sus\) |SFF! LC>f,|222Yddd4;dᓑivm'I{elڴ⡧ddddb9z(sΥ﬊U N,C[O$F#={Ã֭[W{m6Ng;'fS\fD$ré]>5b1v G?It&~ἢ˦7*9t`?2(*wXQʁ}dfIfS%m ] J rرTIMDUU&Mb֭t҅CObb"WwrN$J0ut *D7 WP]^{Iߚv}wXn1 勢2Lƒ709Rd@ruN+#s1$I$+c#S>vK|x\ %eT1{ֱcꂱ$S>kg1E\u*T4uŀ _EE0h BBB(,,dKg#9I[m۶`^ &\pIP(\rpNZG%הeuEM`}rm}d\v2q4":%GA5&yHcKf.^zUs_4;54 [1UW}ZL+t)>})SлwoƏOPP#F@Ѱ!u↑j*Kؾk~9xN Hr8~"^ 1Feejc9ǎ؉8/gM 쟌\ ;8|8:߁N;>LN*cvsM X 0p0c#SU^BC9z PRSMɄ /Ç)4509‘ckBIܺw}0fCͷ[i 9zA>͆$Ux"#)(P:&RZmmpߣ;S5:mՂY7r|uDQdɒ%=zưanݺѮ];2223gW=ίDzqDvlXύ]ZcveܤY̗\ MGF?4A>޲[CNlްj-{S+U+k)8Ŵ/qdqm{Ɍz|$^ ʹi-) %ѱx:?TÉ5xrr;$yIta_1[퀀??8`OX`O *~!,ʌV$AՋ[|w{2 D'x~|&OFAQ  ]̔ $ȟǎc?\ʄlNf OYQ>fWZ׻VMhcxጛi[ѮEiR+--_Ϗo7VnChVo${ˀ mَ^dfl>]D $7ց,995kPQQA||kR)~V$Iwrϝ7RDE>K驓ҡ_7>^ƐpאdT Qat0=nWB !u* vE˖1t<hWl켑jf}z41<31ݐ6x^}36lF3zF>8Z~?̴( 7Sm4:`<\<7v%Fݯ/wF3Qmq[Wb &A9r=2AᾇcpX*!xY0V)ѧ$r0}e(:6k=Ysդ§P(???Ν^gРA{;++aÆGbb"t҅F]$Ipks$;ZEƦ zc7tuӵ8Hu{bرcX,Ӈ={ҷo%IӓI&aۙ2e o5m؊(\4Ζ!b:v| KT_QkTZB9AQD\QINBswQͧˎyĶ@LPtGvHEqz;BZtOJHtq^SF2`lٰvI}9 W\'f$]"[|[dGsڄ$d1w)*+q"Kj1҂y@w- p!t*Iue(ٻS}m^M/Ik֬ѣxzzr]w]gWtЁ"5#/<:h8O=N )X}]ٹ-Q ʮ#+~z V'yۋ Ow"Opήi ങI۰*yu4=> E I]4gj`?ΡjGc)+:z$\on{;h(N?f}O}4@F|7qx9c^{qA|8,V+7^:w/kӲH]Svg7S`"S~ }~tz5v͆fÃAqnnn۷W_}FJ]O]_shG*s[=V6pqe;i٥'uZNV͝՛8u*?M#eQbb ̄p\WbX۷,_'X27;:BA!ǡrjJ*={(Fsm**](ԯ~pq2woϦcQٷE͞'P=1w>Ŕ&FNd7&F#FD⧗J$6.P;p5xPY\;gF\bsivgԚ6zz=:]R5hj߾}Xn#G$))鲦 BQQ˗ӹsgΉV捻 $6&^~ƍ^;kZfL cbCY7,jBxusz(D0/+(0Uj߃JI 3z._}?L[~r2.: V9&\S$D  qB4'פ1 .&hf'_7_`6Ѿ筍JJtM^Hڝ71(%.a$wr8s+@w A|(gsH.ߒ&tYE3fyfƎK˖-訉$I_~R;CHHȹr6;RNXcN);gߞ?#Dc A!<^+SSїW!RZnD"OZ[JNމ轂2PQRHBXxx eEI'rOas nGF⮯Q:VN;FƅЈHE*7}P6K5's0GC 'N폟W[9srAPLXgPVtj;aaԫRqiq,̥%~V5R9$:8qF<|>R^tY1;bZ HDd`0Xb˗/K}5jC_qftPYe|(M*|J $P(PT̚5M6e1 ,YwwwnvmFvvvDAɄhpMEj1_tMeӦ~Šm8DìXBَ$9Y6{i>ՍAuڪYָUiQ)@t9{#9DUt(8u#9'9 8,Q贗.!#w":l?Y Vq}rMЪ<ՔaK$o򼀸J$;(5yR\RDyy9%E\qk0困JJ찻n+ݻw',,,t:Z9s^'<"BAvp:,^;`0\{8tiwɌ( L}b{W5yFݟ ߼'-2cd:Sx{0=v?~)(>4$vl@!H8"}ng 7u v֧j|x.߅?Jfx9ػ.ZόO&Ք0f,ZՅj؝VGFeY1Π Pm2ޖb6m eر̜9M6!"DEEq <=<$&&FC_) J~էQR1$I@@@ǸolʶWoWiP),֪6qvϓ㥱3/XGm;3N>z!C5пgW/XE5,] HzuT^Q|2Eг{7½,(pѨWfHsI7k˩G #[ˆUIM?caeԏqxD=KH J7=\s^H88u2/J$`uѻ HVCY%snk{i^TIznTr(-}mג7ˋ,_B,:[kjG fcݺu1uMvv6wfȑ g===>l8OhF͒%KHMM #̙_N~Z˶]x葑Dؑ9Kı=AS*PʫҺ)fe/,ㅧӪP8kv -xo1m*:S LjfaoȃO=I|dP22% ?J{i s nj mz{Ĵ^>!ِikkЭP|`zhR ‡~Y~=ݻwg?$a6RRV$j/Qkׯ+qqq,];x=AYf 5!Wk]B/_S R_ %w Wyh+|*KBmk`襇 su'/F HJJˋ|ZnMV.7OTү_?IKKc7dddd>A@Ю];ڵkwU㉏o4Wd f,|222Yddd4;dᓑiv'##쐅OFF١cTؘ.7u@_ho~.uedd*777\\ObMuu}F \\o4;dᓑiv'##쐅OFF! LC>fG8$)F͗$QDLɅO;xj.zWBBi/Y1vYtD5lFcMZ=tڵ) @ed1H@R5&49fz5րVb}(ܴ3񻙴s;E5Z/kU>}ehϴ,m:k䫏Dމ#dWpESgo&CYjmv;}vM:ʹ.C]A%ہ|FT Xf-?މZ>фwk}"d:*ki8N/_ΦM:t( ۻIu] U!IA=ˉ9@Pz8+Vbj- ]sC$gLN+[֮bLoP@\sa Uwkd&mh>MiwMiNqA-ZJ5g ~0e3`Z/!.d*+`[7ΟT*DIB ~>h ye:Xrʑ}tZd~OI'eA^zvlX͛y5.`W85^ #?'[(|yߍ,/E W}~^*jJ0+/jk8w~;.FY1>`"ѱsk fȦM{hBtҋNNg eDJYU5hj%IdTZFz*R73wiKQn  Z-!#9,|;}6ʧM\<:!#>w22ןӧO?!C۷/IM, EhT8n]tV %BbԘ$wj\ŏS?`QKMAyx4ٻ/Ά-tn3D2GHe)>. f$خt*,y,}yfύ/]wtGj|0֭;t!SGڊ-pǍ]6.|lz|#;0~ܳyqh&&N攥6Muf-@xBNvSg@m"qIy0pVbg)dEq! ?Qc餥q-N|Kp݄=֛(r[v;nF4?4$ $c*)rxV I<枺9>I0{ⴕSeܶWr pHXSP`k߷l$J JX=gUY>*ŹVSdy,ZE_6'Kjed ѳgOJXX؟. pn^$U?|Z⟸ JMHTo:!$ד^ QPTZM{~AZyO?r32$$IʾSWSW|||,n8$-j-yXE kH7.\PX@Bx]-&S]}}=&3*..>La~YXl)3JLB,#ډc ZM6lqqqvdM$$jgp6-Ǿ娴ĄQRIֱbqwwlN`9ӳ)G̈́/f[)2pwwǠב)f⺊jZ헔j:괽g/ela ~r*+/<²ei@ lذ&4.Ʋٳc햋,>ofsqâᦡdFU<J"))w}ӶmۿtOu]uTܹkl7Y3£t]:0>Ozdر+xz׉ ƜKxc!}{56-BcxW֭%'}nABj"5yuӺš?b,<KTAw53>}PԔk~|Zu{4ъ^tmCFFjMGp3WrBfF' lw FLFf&VBF^ 6[W@ 1^}22qki7(ȱcޣRtCQhJQ';mJ85FJ&{ӀB&6#jJ0V lɿ~.[BoAEE9eZW|'1V;:wϫv/$8_B G**#v qN*(e\{[7<Ե#̔awcK;P^V?ZVk#CCyEFc5㎛"9 sg< Zb:䩣J#^~!xQ%CT`6Re%_z$kh S 1xG vǜ{%22;j2N0$-rqqGD'vAP\tUa!J e#^!Ӂ)"(qJ^Q9T˟A<;o %Wv;$TP^\p]"JyEW(qYb\zߙB2E\RQ(P_W7iXEF9LC>f,|222Yddd4;dᓑiv'##쐅OFF١ 2222\q*TFFFFzpƩ6J;&###p1P`ťz7ddd4;dᓑiv'##쐅OFF! LC>FDl֤uײ$XmVNAPhP*Zs!QH<85&-6<=P).fd7OtDF:#I"dzvh&>LoY().. x?gj 'J||izˆN&*\Y9,FΟǦ]UPkDFpw6*O9~9/Jv2l&/\ɂ >>l8>_:iGy ro|SɃ[=~*O)î22w_QE&L[uQÌ)Yy' :RUO&}xѸ D,=%R=vTi܃{ᅬ6Ԇ25Wq||7&%Ѧe;,Zg]":mkXD(:doaMW1yE g.< #;)Qx/n4mHqo:ʼr#ׯf<>U=yX1 X|= f-Jexƽ<tȮ9-.v4yO,oMEt:)9} &￑ر%־$RZTbE犟W$bb!mTZtZ I"UeTM(.Q^zhߎƾTg|hbPT\SO/_<~NE؜xI # grN;55\tz, ʫ^{W_Fj${7GӢSoF= >n:^}o>~ϿY,f$hFtT%WNJRe˻k*RwV>w?mxפOKW3Wݞ&>U) mәoP%VTXv xPUo?"* y޻y0WSضEd_m<}H /Ͳ ;l aq<x%ju#CfI޳B~ I'm{Q6c,ok<|hi F A$" ?A>IҪC[bxa$7DF*$i%z_{h^{%>xS?g}@ﺼq{.O&ᣧ03\}(곏)E8mF֥lAψ4RSA /5#^P酯b{i6vÿP*-Ff 7[չ5,KYAYd‹U)5|+&v1qȑDd_e~ ݒu@/f+^y%7L6ei $ɡ}htlPҫG"_3ApXL hKҹ{3_s\%-`TWuK:3a@lΨkNun³C%tLGdEFCO] 6[\5III(Tvs:ѝCKhGܣ]5[αYE7FӷC @LTƿ) ֦^YՕ_ ӏ梦kbBusS :kXtnь(е3bfL'T-V;=j؟Z5&oCD{5%aҩU뗳 u/&D"}g&jwBSqqX*xm,V-a̸W3_8[8v}Q!INV5(Le) VWhrjNISL[,v'mbT9TtHLCTds6K<;F+M$ICW@AINx/NU(>%pչ Nrc:UL4s ]b,GB/ ZՙIףNJX̛C݂Gi^>Hi7@Ut,z2Wؓ8le~E.\Sq y4f6q`~*(.)l!--GSYncMx{vTjQUTWTBȁ]ٖSRh28mP ՞ݾ7&F#*j Qf]=vޥEZx9<:=n괣ջun5 ݵGE:&:lDRY'|-_/WpAIoI"]/y9hve|~<~sv;A-okРhqQ)*:AFAP3Ok5W_M/ܱS=-APk4*2vd` ÇKȉYwҸ{y`Vސ9UI{y7VD-NBRE'9Yf{R]E\|=q{}?=3~Ic*)9嘕FL,[.1KJUGJUqhű#GK]J"H 8cqݝݙ{ @O~upEK)WI2 <<~OpfvuKΥǹKE?W|>Qy>a]<{_Zȷ{FpzIj;_KkL?0H\ DZk|;k 6s+|K?+zCM22>Ùc|ŷ^xR_YNg<ݧSAxϟiG?y#O Vb{"B\Wyk?EcLlQUWWnR,] 7xa.../R"Co&_^R9<319O 8]_G71̇6(L-~ύ\*[U"1>=nNU oZm$w_wBgtr."A߮rVȏ]v?N3~\ixrA23ɅcBixs46B38pCsE-]']QÕX,FlhΜ$!U6+mN'MUJѪ9w<2H[8lv}> lm>w->;n#|q$K`yxxxx(J{^{S];O<<<=xͿwdCRN]obJi@Z0L:DZ=fn+;^ hJym$R-Uٲu:.Uӥ>|7R >G&V*zw+JQr{sG3,W1[w \Tv`3Q6W֨ț۹vТ;?KBH:Ǝ/_aۦSd.:)T+wbL pjM,5)?%Iʋ˷\#*( )z.NHdL%h,icx$q[Y[Ϗ*ң̏ıM6._X.p&ez;`4BԀU޸F"QP챶̅˫4A,:ݿ8s|Rf˭r#>ѱaʦJnWk]{d:MR.R3'h(FJF`"ESSé!]`֛l5[8@Oj=*d4Ykj#[U,d*OeUo۵4dR B~t.0N躟!fe]+h`Op:DbޠRDbqt4[l5Lꠂ]D8֥5ƠknXBc:#B] _(D:FP&f&2e&b$ #)umKڤ>] [4F6С2lRGtJ]t2FDlKX2;) A(B!?B*tMF 0)K Am5FD ّ _U@̇],-z0P&k+9F E_3a:Fc4!Zf$uCky+:AhxiMç$R7%ryr*6==0~VܹXm\:8 Dz>:CWrܲ g :qŁmMM 6d2{ MہTv(6H*CVcu-O琉`RA(5.~!iX* z:8Jx+"4!Rw]XDbhv;1 `(nv(N&;[ڤزfI gƉJ iuvVd{pQXH%p+;?Gpf_>Fǘir䚍=9qn|@crpOpZULB|M10jso[˳Q Dfx|PhllPhqfJj8eMVsyMOrxf7*UbY2Ajf^ 43LhL'YLG+MYeK1WV(Xd>W"_ܤ) "YNdR,*{\ &'FO'nhv:X&5eD-f: g9="h\ BuLV؎+iRE AR4i FJ>GcvnSChY+K'zw@Z&-{}:}x0@>ّ (؎pqqv}$Fo `<;v+A@׹*"mSA[vP=n}ߡ3thDNruF &Ҍ%>1Kyv򩝺 4mҹa Jheb,,i+E2` 4_@*@P2h'-M0&d(Ï7AS̟^@If|sߎᮋY(pPE"IDATJSSLL\B=lxivX4QcR4IRKu]43Zt MC9[}ՠgQ*7ĵmlׇ tK/4q|+7Up }:;AMCup#BMC)NIiRsfw4?u$(F.O۵ @M:vL3atצ[YRȭh/i)uh?3Iж~|ҡi#CA.} E5'mS-oc^mlGt|B~-]ʑ(RzB ㆑mb*Cٶp5&tJ;!!~eeF#BDa (@6:AcqmrAꬬXvQX˱)t‘QxU][snOJ͑t\ ڱ)RSSc1:N |"ALT l S)FUJ{k>N98ǃ̾O0,WIͤi6# %Ma.G#LҬlw|ӗ@ljc[[g1j5,tBfv CiH27?GQ*Hxih8N ēDԖWnId!3Pl8G2~z;Evr`ӖW۫zlt"# Qt7Sǘ rx'蚼qe}-%.DG ZrltP4Oaa3N*8Cz|Es |J o! ֯ڒ8TH*Bl-oj ӣ3갾I!-vT߶YOJlUk G8PIQ(=Jk)Ft07ȷ,.GS$C!P ٬s%AEXldcD)fBs1V+[ !FAb$2b۳(lnRv|pJy.#I#ZF>AW8k05azFs?88a2)ӌ6}u>p-k+YFZ_gl )o-pd1pE;H]CI]9} ]'WM0;6InXn :w,oE'©( qcŕ|("K&[0< /i 8A4 bv{ t-T>VpF̯:.CN&PRil@nK 3PX|SRn @۷iBA_lכ\7y]':r B; cCx(A4 ٱw\!l[R>_ @nRv-l{!;Iv}\ލ%Irh9-ܧ:Հpwj}f=bDN kav܎J!3xw6S xN[|wbGE>/kgϜ9AMn㨑R^}W]SO> n<<<\3N>}GG>rQ`Дm)y=/mnn.5ᣣ bi#}_lo?7BD;)G Įa |vA CpcFVC;ew &z;?EDV&zOWmG_TqmF{`w<-|EPB(b۽{IENDB`MEGAcmd-2.5.2_Linux/contrib/docs/pics/webdavMenuWin.png000066400000000000000000001103361516543156300227200ustar00rootroot00000000000000PNG  IHDRG^8sBIT|d pHYsod IDATxwt\>2FŖ䎍\S %`C.f7$dIސB* = I,1`rMƖmY{izaQL c DKg ЉҨu 43]%I"O$Az{p'x^(ȲZr^'*5J$!y%[Aܧi85@'UzI0(AlXp7\0™Vr|X,x+ @ @q}FoG ͯ׿6m6ڧs //΢E.\AΓZڔ@;~oϸOW1׾P~eXŰ JZ ?yKsǷyFl>,d2 h?i۳  aM%i+ϨwXhIΝՙu+*rǨ?UUw=gŨ y###@$dYaԑ(0}IĤpX,F"Z޷hoYKE …G"{DFTMG< 㤇lhqۺ*Tv LQ~|רQE{n *g C"  Z[gmAI.Ƞf%S$5$oVZ%QOҊe!Kf)ZC250Mc(l6V=>$p8& @4% b1A?yӥiZ7x/ܹSy+_ _(++{y^C.RN<ƍWڵkIN~M L*8& q6l@jj*)**3::V޷+2X@ @8&DbFch47o& zjV^MJJ h!.v5A61S__Oss3-b8NF#3ft[ZZ8tBf͚gVS[[Kww7DVKzz:K.E$N7p˗/˲臨Çh4~aXX~=*A$<,c%1w\ǓO>Iww7VULRT,] p6mL>Ԟh4Jgg'Ád:LKK _ٹs'k׮>۶m?{vU*999{dazzz'? 555̟?83ӟyٷoVFFF7 7@vv6jQ|Az=_XlG;O=))),]}v mt'An7@N:ӧIMMeٲet:ɡÇSUޙx'XnGI:!))EaX9s&322BAAX_;v; r رc bI>GfGBuu5۶mcxxsVX,F,CVc0PThZZ-Dh4ח$z7|x<΍7ވ$I<#M\[VuIII]c=Ǝ;K.!==P(Duu5W\TKApUpK)--ݻwAff&pf33::$I1::npkO,񩯯TWWxbPTZZZ(63gύ7ވ`>}d0|>v;˖-CVӃ^`y,B6Qד-³>  trJb[nrq" .dܹtvv{\., CCC?~{O܌"Oww7pLzz: ,`޼y\s5ڵ-[Dh4b *++1#mMp~lݺ_]y~ڵQZZb˗sM7QZZ륪>7_~VF^z%}GYYfyA>c4PȨDB3jd,zd) L8'dY&H0<>N(bs7ALX IB [H~믉WNƬI};iZCCCtttL ### FZ;-;3'ਕ¨J{&N4o#=Fo18>|Ix rrrضm>/ٱcDC c[QPVVFyG(]jz*X[b濫yǟO@ӱm6͛kFRRUUUIe;;;yٰax!",jd͚5bXɡ ۍnZBDB=ZTTTPQQd.cϞ=L&0CCC L<fr7e]Ƒ#G$ x<ӌBFCqq1]viii~A8qNjkٓ>K?^v󩬬v#$'Jz{Wqr4ݸb͔'$??CZ&++ } uVKTVc4cϞ=x<FFF6}, ٳf^/$p8HJJرc⋔(cDBI'1sLvM("33S9Hl!&0Ov|f3:{It$AB tlՌ|c.a˨U*YB+E sKj5)))Jkp0|z*233eDSXXȌ3xg3fVd2,QTX,X,rxe\fΜhbpWvy衇t|XVf3l~۷?? CsdYNd淊ʲ<~N<_y37>8L]0%( %G˚^ɊHOO?V,ȲL @+L$0AqBfZM4% Iz|" `2G"2v b=h4HD$!#2<̟?;J"  H$eZ^d2<UVde#8h]??׃+} ;|o WRqui*Vq:\z _ W pO9AVxBRSSϹ;9Ѫlh4X,ItIGFƩ;˾|ĄL4eΝAl6Z-vݧ v.^D?͐WCFi}/:ťKLyp[OuT3sKo`޼y8isc<g߾}۷Oglll+`…>aH%t9_}:c8S}G)\3>\.VZldƩLNfTWWi .\5\CIIɇ|Se_D%X@^nx=PvCc玲 CRRA{9bX4 bJ*.QHuaaay ?N[7xz!jlϯ5@-/gŮ …}*(O"E7=jM&8a @ҁt9e:i`AGA>m԰ GDGA)(0A DpA´9_$b I* ܹslDQz_㴶קpBǴ[.T*8z<>h4qi{/\r @۷7MPb"jZ9r\ȲL,gyjl<TTTňF$%%}C,a &o\s5~6n{~_RRR¿ۿ!IZ׿5III~̘qnɲLGG< ZVBCCClٲǏo}sc 'o|A8cljc ))),_W_}rrrCeeY&yaF$I(), zrxD! >k§ݴ oq8q(`#Gpi:l6>,ZP(ıct\uUTWWt:4ٿ?*Q.2zn7/2ǎ#a6?̙3ZJ}j5.x~/_g?YZZZسgt:̙u]GJJ uuuرt477b,[ncA磽ɄnGVo8H;wݍdbdd 6ѡd |{b-^`fa0D"lڴǏ+9~?ׯP(GRʕ+QT<<tvv1LFz=6Mq3Dw͜>}OKKd")) Ʉ^Wy1GA5ڣ:&O=O-oRirL$w^"uuu,Z)$ t:˖-cҥ'?[ўHzwARRwy' K/dnV222hjj>s%++  &[om۶qF;(7pW]u###=zロj5Jww7}QN>w-aln!R}F(āwp87a25k?Crr2ps*ےl6`P(4[NDۍN#''4L&TUUH$|ONիYd &+WU6s)֭[ǦMHjIII ^O4 |DKxא78ɛTG̷-JҔ2֓jeΜ9|_ /믿N4 bŕGYu*1?V0]ac9N &'',>QѐJ,!B455t:'M$ N:EKK ȲL0ĉvvb1IIIAl AѨ5uZT*@E$!I2Hk9O& ѣGȲe˸˹馛8r?fX,Ɗ+())l{Jh_ JE4Ee-[FAAmmm|>~߱tR***p8:tnz=*]w N'䡇bɒ%^ 1 'N'W_Mii锭0N̙3$''c4Yr%X,ekx6jbD"`P&B&fSSS馛ANFFFDX,\z,X$*++QfYx1,HMMoJQQV"99FVeΜ9d §I'fJ]]n#O*p1RKwRvZ>7L7^U>y.Km8Ι @FJĉ IDATD%HDaz[8 d&b,g[yϔQ+W۫X*y䑋S{AH{{%_A,Wyn;' IK0uA (:lbkNQ9 |"#' CAH=8NCo670h= D'A>>Z:c+ lIȉ5s |.95椒iDGGD ˗/gڵ ƔCA> 4 )))m˲5VHiI7L 'V Lg2ۿ5cM-c˘#fA4v |DpA SQa dYV_L&>2e󹺴W:,3-9s;v%K`ZbJKyfΝ;ٰat:/_Η%rssٽ{77nf/_>}(uuuJ-[Gee%z+W]uCCCJD"ARR?яؼy3[nR___F3c [Xx1j&8EBO?4/rwV\nСC< 7@,7r100ݻ1L,\P(箻"??+-_|\f͚uքV(b9r;3Z[[9uP83900@nn.k֬a^/twwsi%8t:ʢ_~/ddd0<T*&Yf1|t:jQlق$IXYfaM[J `||ʪU&)):%0@QQst*V)--n $122… IOO77$p8zhZe6\ΫP/'S{l;eggӧĉ*As燐#Gbfe9Zpp%`Z1vz=P1SVVbbp8h4B!B2H$bPXXj=>t|>`h4FFF" RTTD<GRa۱h4q\jeh4r1SIѣz\.Y$[t@nn.JBt:;NN'hf1 ddd`2u"Flg@Ι@GӳgfIN'Z_χtѡ7bP(߯LLL~e2b||@ h<3ޚ`ڵ@O*ᠯ&4 4551>~&3ft288 bHɤLh4%$It:chh4sxBGzC(k# %%%vލZ&''GG`0r뮻˛!ݭzq\hZ'yyoQѐMee%<c>OiMB!Tccc;vqBRPPpNQRVG jIPJ Fh4rssz۷Bv;3f`tta1 $''hKKK vNrwp88x MMMbI-cF$z RA?LV+ǎs璖djضmAe5A??ػw/###رx@? ir̛7+WrWyV"N8͛yj*KXZ-j6l؀gѢE|Svߏj;gW* JŒ%Kd׮]|fʕy撒|OD(--U/~}{ȲN+_ ߝ)F;z{j֬YL&+Vʕ+Onpgl6t3/X=ׯ+B A%ϖ'&?VJ&Lt11NG8\, ,3>>N{{;%%%f%bXh4deea4ٽ{7=~`ƌIuH$ʵZT*L$~e`ZZyyyb1%t:),,DRߏ른X.wuu!IyyyJxbRIh4̚5$j5ccctvv*vSPPhGJJ 3f̘4F W&l6e'O2c RRRPx<Z[[)..fVq`0(,,Tw<g||'O*0V+)v8nsb43ن_cp3H:@ c :R6NHlPdZuAW, „)>E:^2lOt abvgEѠM'#83UAnɝ@rK &v6 LgG֯!==ܬ$\9)d?TEr`(§zٺlvYj5VW6Hu1&-Ղ 1c+§֫(Q@/_ 0l֋]CA@[^>n(hAX;dA&h4 xP(D4U3 %Ʌ$Ib(gDN'$)il'\wɲ<FR.XGg/7n涟LӃ'Oŋ?> >^nVjkk 6m;菉^9I\av׿uԲp q_ZYQ$N<ɟg~_t:bkl޼H$Beeba…tZjXt)yyy}s/0cccgh+Y߼zB ][jYd VfH$| Wҧbirn>̪UXx@ղl2?H$޽{edd~%saa!:`0H{{;Ǐ'c4Yp!YYY r rrrhii! AYYyyyH$СC,XAkk+CCCv3RJKK'e$bAZZqrss)--Ux^>L?*TV\ɩS0͸\.%޽{r!2nA***1CCCtuuIFF,Ԅ륢ݎ磯шVĉ,XŢdH4>}JJJHMM%xطoAOOϤ{477s)%ļyKff&ȲLcc#~ l6^>v;0mi~  >?XhnjjjXlf͚T؈$I {:t_:zN>ͫ79‘#GtRWWǯ~+(7ofǎ477멪RgMĉ?eΜ9X,^x}YFFFxٽ{78N'M؄a^y}Q~?6mb׮]tuua())!~~ijjj8r̝;͛7ۋf#))~ӟNff&P#Gw^,Xtټy3P|=;v젨ESS{! vo~$_^yٴiwfhhEvv6;w?}144ftt+dٰaׯѣ۷D"AZZ)((3ϰgȠݻw#2bttt000B\2"fh0oBraۧFx^<, kgᮻbƍ:ua^y:::x衇xW{ H$ CcQVVƶmz;^Fx_n&lBYYn6|>---vZZZfppv\.פr(--eA|D"N8$I444H$lS8999A=㡩vm>,~z)n7﨩't:EEEa:;;aezdY~§Çop$g)iӭ3}}}x<YI III$%%RX,vm,XT˙;w.^!ٲe vB3gvzt֮]KJJ \z{{nr^Oii)^{-Ť+]wf6t}H~~>YYYx<D" s"2ze˖QYY,hnn3 H$[nF>p8Lcc#2N8ɓKkT VZF I$ahhP(ĬY?> ijjBRv3j2w\L&x\ڇaN:Ejj*< ̙3c.ŋSQ1̬9%ijj*.Z-CYJ`PңNeI,Ԧ:N)H$H$XV:*++&&HN]bddh4ZVŢ쎎z{{IOOpk.:::Sh4b,IFшFQ_O/59t:L&j5& ^?~FOGG~)'|zfL&i6*Ք]i5 TWWsIRSSՑH 6zjzD"z1JPMKK#99YE,,,K.Q+ pv2Zd,X@~~|<'33'OvIKKcƌ288$Idggc'-͎;$9st:q88pNZZ&jՅvڪ +L4>>(M__eeedddDȠFOƉ'8pIIIMS5mD+`ppPB!멪b޼yʄ$Im6z====OJJ %%%~zHKK#++bYZ-N9s@<'??NGVV3gΤGm݆b[VKPv̙úuXtDsrrضm]viii`0t:^YimmUZ]&6nHnn.̘1Ӊ鱗uq嗓d"33۷sW*h> < Ogdyy9_Xnop嗓A,CRa2D"lڴ^ MFFkIxGپ}; ,ছnf1c FRLP՘f1<~Sq+Zֲ} !̚3S hQ4ܯU9ssFj5vXRIjj*F P(HNNSp8P {g$IX,&MDVVP(6lJSVVJbРA}SHii)V4Z-EEE,[Bl6J,ߓ$PVB@R`0t:{xgYz5dee1l0;>g{=~m0~cǢI$rJ Bjj*jB/_NaaR _aܹtYU{ǪƌuߖH$]ZP($ZN^&OfB賜ˊ'z'걧vg;ש'EyxcN/z}չz_]k3Bm۶vI$ddd/WhZo_{?3T*vS?sc{_?ϟ}"<=kg ~Q|(c 7@aaiQT8 ]A.ʖ و(he7í8h3/ _?x[90`hj7 ae  2 5H(zwpT(!cLaIA.>X HXB i3w3A.&ʚ:qWBH@W7 L6QF%x͵4\uU U5t2בӻIZ>UDz3G4֑Pbttc޳j-mZ߆#<_M1i=PߟNr&|k8p 6E,CP`Z7nIIIajjjꢴT=BhmmvS[[+r(,,dTF0cǎ1rH,w$:::6li~ )ĉ=tj7Vq|> . ^Bl6P(?~y[BݘuÆ ,]G}DRR:[lq=nnW$™EiiiL0NuJ$ֲvZF~Z^{-cǎڳZ'h4L&bh`0HYYUUU|{joRBܹFW_ĉq\b1 .dÆ $ =wΝK{{;>;v@$L&z+Ç?)|"NGnn.YYY'umm66 OJJ /cbXW^aڴiRQQm۸IKK`ƍ\nV "'xWFL8ǟtlªUBT2rHfϞM^^XV/^Luu5====k|$Ibɒ%ر.),,|r6mD,#99QVV'|"nK/vb e]vNCeݍd2Jȑ#رS8 MJJ/YRCrr2j<\.===TWWr0x<}]4 CSqQ՘rDÿH$QՔ`6 lܸ!CѣG)++㪫nyfoСCA\D֭["''G֟())xؾ};c8&a2z<ի?>SL!99H$Çy剥<>fT*_~9FJK$p8?> @PPYYIgg'III̛7l1$|-]pD"|'YHaa! , 99ӎt\r%l6 9[|X,FWWjtv;zPTTD,; tT*H셒$FTv:;;KNFX~}}=ߘ>}:%%%}FI+| @ /F0aIcp<ŤtnCww7ڵKNn6ydV+*j֬Yq\L&Z-GaժU?^wj [A8]p4?g̙\.KMP(p8477@0<)HDCC̘1㼯$ I` [&ٳgb 7xy1h  FN'| /fĉL4Iaؼy3jh4hZ 5\ûڵkQ\z̘1TNsNF#̝;\̙ʕ+yp8( JKK:t(n2b;`0oM㴷s7jߢљ XfJR6'| inn& V| 6Ӟ`I$$ N'ŘL&&;;[>^P0|pYJBb2fdddňbfw+Np8l(J& Ljj*zfjkkIOOt8vSTTѣIMMEV2T*$0L :ӉV%55aÆ1p@0Z4n^GTVjdggs%PRRBZZɸ\.1 FfE @Tb0p: 2Nzz|O |RSS B~(((`ԩfzX,2e(ApJp m>>,o>Xz5We۶m,Z,L&iYoh4JKK .DדrD"ٳ/dffb4OcǎcѢErTᛯTl_o`ݺurJѣt@:EEEf#IgĚV̤ 2*ωHH$lݺ`0HVVl6;l2PTXVrss1 =O7$I:t~y-?̟'nwηD"g444DcMMMlڴ C,v_ZVN ~0cA5+Yi5Mlp [5ю,_,Kwz7j̘1Yfa۩駟fŊ3i$JJJ())UHy뭷(..s/W_%-- goJ?ᅮ5t2|q nF {[ZMvv6<O<ǎhph|x^$I3FT*`.0X,9}kkh4N `P|o@ @ww7&nb$93$ItvvH$h4T/))IN hhh ))IV㭰xVq\Z^{3fc]&Mog֬Y=zgy˗H$grZ׳pB wM{{;~)|̚5xѣGeF:;;Yr%F￟y摝e?% bZLVVկhnn뿸r1i$ƍG(_& qw0`9tII ÇgÆ <8{6l<Äa )))L>;3ݻwG1fV+[lᣏ>'33:~?f_|>dddo> 7[o#0|pٰaow}|N'?())!P($?V~qe-Z8xGk1b~5kȭY8ޒh4̘1cbX~=#F`0pa^/SN_|RnJ%555[+] _SI8#X,@T*ZZZq.RZ[[qD"NСCtuuڊ+`РAb1<7oٌF!33.Ln|tvv P*$%%1~xFI0] vEyy9JCAATVVŘ:u*EEEr X, B^^^cƎKaa!>۩ χj`X0az)((a***lj***5jgȐ!F*++aʔ)DhiiCyyyGmm- O?Aii)'O>-Mk8KNm[RRBww7---?*tJKK)**[!MMME$֬YCww7ǃ`N˩`߾}|> +_pAR,ݿc<T'r\;lۙ_ss3~_1顮8JNvv6z6jkk 0E4ѣttt`ʒgtX,8N4<D<n[~l6ᠶF#!;;[.GRPPlMpA8O BDpA }Q"8 AGA>sx;u; ]:h l}s ] [-AH֌Qkh5|5rZssŞ={{eANUZZ瞜.A0tQi&c=ĤZ61- 鶳rjDTUQ?tl-n:B9@<@{i#+~2NcX9ry F{wNjn6V)S8K2 g&qi^4Dh?$+[ݶcT @Evl@U^ yJHc2dcY10Qjo7hPP[F3₩`߶U:A/րU'ɢERAbY8aIf*u~x h=jA.B}w&Ab FjĠRQAZ -A% d2c'Ԥh1F^@iQZbDRp< ϳtR>c9D"oŋinn>xc--- ݻ@ 5:;;Ylmmm_빅_GϾ}X|9k֬h4z%I?>c(bŊzzzs& #+ IDAT˖-Bg^ޣ+$In֮]KWWXD"$ID"Β]Ϊ_F9tHjXv~߱c~Dزe ǎ#aۙ3g7|3.BWQWcjJ%ƍ#Hk.dBPH$H$}%I"Ő$s[#'P(PThZ~ĖMoVѠhP*$ bpD"B@V-x AO?ncΜ9سg=ٳ)--駟vOb  DRRƍcȑq,YB]]~;999gɨQطo=q&Mw|<3I$ ʕ+y饗(--### 6_P(D"c 2D-Zillrc1zhV\ɻKyy9z)Sp7oJEE3fছnb;vnxٴi$t:Y`cƌAVrJ.z-BSNn`ܸq_oP5?[mmm2`hmm%FFF:/3uTgƍ|>@7|w܁ȑ#twwD<ѣIMMN{=n7㤽-8MMM1~x|I***(..pp,YIMMX,F {gƌL8VfÆ NSSMMM^{5OΠAСC,YF#b1&NȰa0(J(7nkq}&p]v|rΝb|r-h"۹FQ\\^g̘1AGGGeܸqtuuQVVƼy(--%55n:::x^^z%L&bETWWʂ cϞ=lڴFy?' .D"FӑJRRhg\.̙32dMMM466A"f ={6;w|3v!Z-ܹK]]]]]HtڱJtƎ˘1c(,,dʹAmm-6m"cx]r%L6_FE<sNzzzph49NÇ#G0~xH8?X,FGGdΜ9̚5dL VR*;FCCtwwx7q>Lnx^ nj%.R0`  bZnT*)..&%%q(֭[hjj"Hj5YYYVIOO`0DB!$I `vZYFOOmmm9rFZZ粒FHJJ n@ZZHzz:O<GVp8y({1cƐ}$#If4T*dddz|h4~?83cǎwZncXt\.  #T*v;%%%VsV% &8FQZ[[hmme׮]r&HCcc# BP $'' L|0$j.uۉX~=-[~z&OLww7^Źعs'>roߎAP`innf޽9rD^[X__R׭Wgg'* J<ѣTVV~hl6'|"wϤ5V\\{G4%33T\.W&++ ˅jl6x(//' ʡChmmFs9 FsrsQ/_~ֵ^O{{;HDSS"PTTDvv6.rN8TVV~Z^jI$ȑ#p8p$)N2drwƈ#8|0p뭷_dy j,Y[nĉIJJB;};Czz:vKu:cƌ?)Xd J NzK,!-- ÁNCדA4[oeڴi?cn R` 2A?UE:3fU/%pA, X,ޯ`p\477{n$I"-- VK4eذaX,<444ͥ<9bn9B8jVt }a4E#I۷o'==LT*l߾]^shZ%??r0 瓝erJ%ٳRZIN*[TMQQ& Iسgz=p۷EZZ>CbZ,J<η}v,/׽ HAAFQ'{'h #FnsjkkOA*ZZZ`РA Q n`tĆV{](BPP+5g^ͨFNiS@':F~AQo? N$/4X"+Yr͟gE3dxaZWJ ]-Ao u~7 fÉ /1UyZᜩgX@(x^***[%AΤϝcr~6dNyW}0aT9gYtvSn& UksEgΠh~>i3N=~?jM@YOĄ A)>[`5UEᛣ.p>(A BDpA }QܳKWWz3fp7VcϞ=nc \H]~Ȋ}K^p<@zZ-$p,YDz$ ONNH$fŊ\~l6:::D"~LBMM W\qǏ'##jz- f~. uA8+=)9dnj5x<ÇYz5^H$B(P(DUUׯ' DÕW^fcԩL6 9ªUN+] ƀ`4wMZDP(D,bh|x^n݊VEP'oӃ磳> JB ??5Pp׋磬L.;33S, o~p*Z[[jfZ-yyyKO!I{Ç^pu"'OPᥗ^"//O.[o$ xロK'h`\+X.҆26ƩT*1'?X,R$99ӧcP_njj*fB I$$''Orr2wq199k$I$  fq-PSSCJJ '.nbA~~>i)D ٽ{73+ c۶mnĿcZVRlc _(p$` }Q"8 AGA>(A'dOh1?̕(4VxCL( pq9rCmnq;Ic]Ǒ.: CAsVOKtƻ8Մ=-BQkP.aJ/dI]clH&sS$Jr}Z7䣱Od@%CTY5 EBWQWoFqjֿUGt녮 ”uUAJN5T\v#= ]sz򗿤H$n理DFᜩz!2& EZGܳN2*_D"$ITUUŐ!C#?07Aծ᭍=2#l T@pe1k,t:7oGlARlfi X= ]VfaZbb1$IZbjl`#GBgg'PDFFNfZZZT*l6h4fЀ$I\.V+~cǎrHMMb BTWWwQUǿw~wIHHbA@%dTp8ђN1VMigj<3^x˱5X*S DD H$tݻ{? }`h87OUaꕽ+k%I R Z&J߶ [=R{».֩[?СCTUUQYYje``kONq8c߿L.V^y:::_ʪUٺu+;v`ʕ|g466o~|Af̘0~ wA&\8ʝwI]]]]]߿98Ga֭>|,ZJv; , H{{;mmmD"k֬a˖-$II^q裏bʔ)TVV"Ik׮%sQhoo^tPsrAG.F#\wutvvfvAKKKqi`0Ȃ 0HD(x<(TVVbXe͆` Dظq#̝;t:M:d2 INzEnc4 Ӊ$I8TUP(daBYY,p8KeMA@>.XV3g֭Ӊl& xbNZzNimm=myK[[MMMs=twwcǎsʲbAUU͛5\=A8{:?>6~?3fW^aǎ\y4McդiEfaXifmmmtttׇiT?UU b0BQn7PH9 8gz$d…, $Id2 [شi`n]O?\~,YxW%I*9uTl믿΋/H}}= `8+,)mSp8G/_kF.;_WŲA8$ 9CCC̙3|N>8g4Rb:HD( fN'D$Iqb@H4ett|>_vQUD"A2( Bh4JPvc2( D"L&NYrdc'Z۾};@u0B~3ٻia ( ie&evA~^&v "l %pA(A B "AJ(PGAD8 Q %2l۶m<A8IKK >;/w\l63y(Z$fy\plL2e<A8/Ę B "AJ(PGAd \|rD:s& n76\O70zBw(EggC{phD™b>>gm%r+ QP( T/L$ؿٮ^Utp"[OX9+L}}=^mv&@M2Q ;;z_ lƨPՐ.3D~ٳg?Lxf h^7% c&t8T*ņ Xjܹzɓ'oϓL&xHyy9mmmʂ01LpO>ZR)r3fg?\Hww7k׮eݺu|'388͛;Yl_|o6f" oL&y|ݻ^/tL&CSSw[o=Css36\.W|uuuOp뭷#L&eƍat]gŊ̘1LUU^C.Ƅ L&êU裏4￟jL&,z5k^z)H~l6ϧFvE*""Iܹs)//h4W_ׇ$I߿]vىjE ͬY4m۶hjjX,V,h/X,Fkk+lMhhhNE+sg 8pfv88NYpah0lA$I"N:6 ۍ$I\. Bl6[ ӧ[,$d|>f+anaXBȲiZ|>O*B$^/Yq8L6jټy3;wꫯ&+݂pᨪ*7|3/+R D" LŊH$4,th4ٌba,[bDݻw]?`@UUV+~;^{-}xoq8|>pr\v:;;ٵk0_5T6&Oj QN{{;|7tio:$ шiū|gUUٺu+^bh*B"  r7t:*^i?Gƒ%KxYb*/X,ŮxGG+ViӦb ^/,sw`,YϴZTUUi&~-Z_|h4RQQO>c=ƛoIP  r 7p5ϳw^L&3ggW;$麮9CCC̙3|N.믿NiyaҤIXV$I"Gww7|Y:wf? ш餦U,,//G4)m$D"磲^dYEQHR8pQt]/(=q .&F#$]/ؾ};@uSsWx´.477vY ÄᓶJuuIcc2LP(e]:e?UUgBNy~l6fΜY<_r  8 B "@$l6͸ndYU<0( SN_U<-GAD8 pus??}\T]DZ(;BP@zEQNdبNwgGA.:k}>"AJ(P #o tsطo8La8Id 6o(E8:NN)͂fI&iŜ؄J=b1,IMf@d.bɦSvtz"eΓJ\Jx97Ha0YnN31وRr(hdDeL d,e>FH2x8Jub;Cg8kKp7qɍģd*dn3.s\*N 9mH&k0c zqz!pwjˁ| L6,~7cHi˃czڧaq"{I;p{TLbvU#!s:L32/NpEh ߅U!9@ǰ͎iHgع :gFsF2ʝgӶːex R]DqP<Zoh҅g9+O]]uuulfb1":,hwdx/Z(ؽgx=;CaRytłjw4_bVTΏYEbڬzC_gt)W;ٻn#_ڰϽ .FsW/_p)3xK/_]'k RҀCl[)KjMIc2~-R5J(=Y3p\fj5||NQb+[1z>CFw- 7|[9VPW3Y5ݦ&2cM76!vaOola#tҺj3f3Rᶔ$~SǔK/9B(szo$>Wx@0ֵ2`1ipd!!ѩjnbK(hе[Un>UK=9жV}Ƕmx3{Ӯ{cUU8f,RtC.rdPZTRф`8qc$a2Z\:,cK ol P(r ɽPC$^7dSk;;+,G6Fs ~wj @`YnKоê#hQf\9iAFzJ*o@@mW D)b%O0Dȣ8 ݍWIٷ?u'za*Wp:]C=\WVAZ*̓ƍ{"% Մl, P۰&܁!?Nم'"9g">'_7V$es?sG0Nv 7@%3/[Ûf^w3/,QnL(6Pf;7meMW[pqẋ۱@2?i0+غ rS !C\U) /h:Hͼj;m*i²a3/pD'\N-5N*GqnHT@1"&8y2LqV?c u{]-ϳ~7S^^~99E4\NFTUuv W͛wut=hɌd&aͺM.@3E`WqXrdSIGFQ*BU#%I I@-0$ńnԁ f;)+?^ ߍk yFI 9\]vR|v#IԀ͌YΑK'Ta HHoAss<)b @V8Ux-)}14U&H v08!)ٱ9Tx%ʍ}K?z e4Ϊ&lԐM*`5>  iqRiNw_4VoeX3]9ֈGMMȊ;9Lv RĢ `2g,efp(Nv6?IYұ8,pnGjdcFB جN:IQev;|^ϣn@0H"|VX$ۑ S&>EEic1$W2y\wjN9U*n;O)9=u[J4tN޷Y\VR6/vٌj W̦9<(߶tLo9bq2Lh version.txt" withCredentials([string(credentialsId: 'MEGACMD_ARTIFACTORY_TOKEN', variable: 'MEGACMD_ARTIFACTORY_TOKEN')]) { sh """ jf rt del \ --url ${REPO_URL} \ --access-token ${MEGACMD_ARTIFACTORY_TOKEN} \ MEGAcmd-releases/$MEGACMD_VERSION/$RELEASE_CANDIDATE_NAME/macOS/ """ // arm64 files sh """ cd build/Release_arm64 zip -r mega-cmd.dSYM.zip mega-cmd.dSYM zip -r MEGAcmdUpdater.dSYM.zip MEGAcmdUpdater.dSYM zip -r mega-exec.dSYM.zip mega-exec.dSYM zip -r MEGAcmdShell.dSYM.zip MEGAcmdShell.dSYM jf rt upload \ --url ${REPO_URL} \ --access-token ${MEGACMD_ARTIFACTORY_TOKEN} \ --regexp '(MEGAcmd.*dmg|.*zip)' \ MEGAcmd-releases/$MEGACMD_VERSION/$RELEASE_CANDIDATE_NAME/macOS/arm64/ """ // x86_64 files sh """ cd build/Release_x86_64 zip -r mega-cmd.dSYM.zip mega-cmd.dSYM zip -r MEGAcmdUpdater.dSYM.zip MEGAcmdUpdater.dSYM zip -r mega-exec.dSYM.zip mega-exec.dSYM zip -r MEGAcmdShell.dSYM.zip MEGAcmdShell.dSYM jf rt upload \ --url ${REPO_URL} \ --access-token ${MEGACMD_ARTIFACTORY_TOKEN} \ --regexp '(MEGAcmd.*dmg|.*zip)' \ MEGAcmd-releases/$MEGACMD_VERSION/$RELEASE_CANDIDATE_NAME/macOS/x86_64/ """ // universal files sh """ cd build/Release_universal jf rt upload \ --url ${REPO_URL} \ --access-token ${MEGACMD_ARTIFACTORY_TOKEN} \ MEGAcmd.dmg \ MEGAcmd-releases/$MEGACMD_VERSION/$RELEASE_CANDIDATE_NAME/macOS/universal/ """ } } archiveArtifacts artifacts: 'version.txt', onlyIfSuccessful: true } } post { success { script { echo "Installers successfully uploaded. URL: [${env.REPO_URL}/MEGAcmd-releases/$MEGACMD_VERSION/$RELEASE_CANDIDATE_NAME/macOS]" } } } } } post { always { deleteDir() } } } MEGAcmd-2.5.2_Linux/jenkinsfile/Branch_status/Jenkinsfile_windows_installers000066400000000000000000000176051516543156300274150ustar00rootroot00000000000000pipeline { agent { label 'windows && amd64' } options { buildDiscarder(logRotator(numToKeepStr: '25', daysToKeepStr: '30')) gitLabConnection('GitLabConnectionJenkins') copyArtifactPermission('*'); } parameters { booleanParam(name: 'IS_RELEASE_CANDIDATE', defaultValue: false, description: 'Is it a Release Candidate?') string(name: 'RELEASE_CANDIDATE_NAME', defaultValue: '', description: 'i.e: RC2, RC3 (only needed if IS_RELEASE_CANDIDATE is true)') choice(name: 'ARCHITECTURE', choices: ['64', '32/64'], description: 'To build either for 64 bit or both 32 and 64 bit') string(name: 'MEGACMD_BRANCH', defaultValue: "${env.BRANCH_NAME}", description: 'Which version of MEGAcmd should we build? Default is current branch.') string(name: 'SDK_BRANCH', defaultValue: ' ', description: 'Optionally, define a custom SDK branch.') } environment { MEGACMD_BRANCH = "${params.MEGACMD_BRANCH}" SDK_BRANCH = "${params.SDK_BRANCH}" } stages { stage('Clean previous runs'){ steps{ deleteDir() } } stage('Checkout windows'){ steps { checkout([ $class: 'GitSCM', branches: [[name: "${env.MEGACMD_BRANCH}"]], userRemoteConfigs: [[ url: "${env.GIT_URL_MEGACMD}", credentialsId: "12492eb8-0278-4402-98f0-4412abfb65c1" ]], extensions: [ [$class: "UserIdentity",name: "jenkins", email: "jenkins@jenkins"] ] ]) script { windows_sources_workspace = WORKSPACE def branchToCheckout = '' if (env.SDK_BRANCH && env.SDK_BRANCH != ' ') { branchToCheckout = env.SDK_BRANCH shouldCloneSDK = true } else if (env.MEGACMD_BRANCH in ['develop', 'master']) { branchToCheckout = "develop" shouldCloneSDK = true } else { def status = sh(script: "git submodule status", returnStdout: true).trim() branchToCheckout = status.tokenize(' ')[0].substring(1) } dir('sdk') { sh "echo Cloning SDK branch ${branchToCheckout}" checkout([ $class: 'GitSCM', branches: [[name: branchToCheckout]], userRemoteConfigs: [[url: "${env.GIT_URL_SDK}", credentialsId: "12492eb8-0278-4402-98f0-4412abfb65c1"]], extensions: [ [$class: "UserIdentity", name: "jenkins", email: "jenkins@jenkins"] ] ]) } } } } stage ('Build Windows installers'){ environment{ MEGA_VCPKGPATH = "${windows_sources_workspace}\\..\\vcpkg_cmdins" MEGA_WIN_KITVER = "10.0.22621.0" } steps { dir(windows_sources_workspace + '\\build'){ bat """ @echo on mkdir tmp set _MSPDBSRV_ENDPOINT_= ${BUILD_TAG} set TMP=${windows_sources_workspace}\\build\\tmp set TEMP=${windows_sources_workspace}\\build\\tmp set TMPDIR=${windows_sources_workspace}\\build\\tmp full_build_process.cmd ${params.ARCHITECTURE} nosign 2 """ } } } stage ('Upload Installers') { when { beforeAgent true anyOf { expression { params.IS_RELEASE_CANDIDATE == true} } } environment{ JF_PATH = "${windows_sources_workspace}\\.." } steps { dir(windows_sources_workspace + '\\build'){ script { def windows_artifactory_upload = { String ART_UPLOAD_PATH, String buildDir, String arch -> withCredentials([string(credentialsId: 'MEGACMD_ARTIFACTORY_TOKEN', variable: 'MEGACMD_ARTIFACTORY_TOKEN')]) { powershell """ ${JF_PATH}\\jf rt del --url ${REPO_URL} --access-token $MEGACMD_ARTIFACTORY_TOKEN ${ART_UPLOAD_PATH} ${JF_PATH}\\jf rt upload --url ${REPO_URL} --access-token $MEGACMD_ARTIFACTORY_TOKEN MEGAcmdSetup${arch}_unsigned.exe ${ART_UPLOAD_PATH} cd ${buildDir} ${JF_PATH}\\jf rt upload --url ${REPO_URL} --access-token $MEGACMD_ARTIFACTORY_TOKEN legacy.pdb ${ART_UPLOAD_PATH} ${JF_PATH}\\jf rt upload --url ${REPO_URL} --access-token $MEGACMD_ARTIFACTORY_TOKEN LMegacmdClient.pdb ${ART_UPLOAD_PATH} ${JF_PATH}\\jf rt upload --url ${REPO_URL} --access-token $MEGACMD_ARTIFACTORY_TOKEN LMegacmdServer.pdb ${ART_UPLOAD_PATH} ${JF_PATH}\\jf rt upload --url ${REPO_URL} --access-token $MEGACMD_ARTIFACTORY_TOKEN MegacmdServer.pdb ${ART_UPLOAD_PATH} ${JF_PATH}\\jf rt upload --url ${REPO_URL} --access-token $MEGACMD_ARTIFACTORY_TOKEN Mega.pdb ${ART_UPLOAD_PATH} ${JF_PATH}\\jf rt upload --url ${REPO_URL} --access-token $MEGACMD_ARTIFACTORY_TOKEN MEGAclient.exe ${ART_UPLOAD_PATH} ${JF_PATH}\\jf rt upload --url ${REPO_URL} --access-token $MEGACMD_ARTIFACTORY_TOKEN MEGAcmdServer.exe ${ART_UPLOAD_PATH} ${JF_PATH}\\jf rt upload --url ${REPO_URL} --access-token $MEGACMD_ARTIFACTORY_TOKEN MEGAcmdShell.exe ${ART_UPLOAD_PATH} ${JF_PATH}\\jf rt upload --url ${REPO_URL} --access-token $MEGACMD_ARTIFACTORY_TOKEN MEGAcmdUpdater.exe ${ART_UPLOAD_PATH} """ } } MEGACMD_VERSION = getVersionFromHeader("../src/megacmdversion.h") sh "echo $MEGACMD_VERSION > version.txt" if (params.ARCHITECTURE == "64") { windows_artifactory_upload("MEGAcmd-releases/$MEGACMD_VERSION/$RELEASE_CANDIDATE_NAME/windows/", "built64", "64") } else if (params.ARCHITECTURE == "32/64") { windows_artifactory_upload("MEGAcmd-releases/$MEGACMD_VERSION/$RELEASE_CANDIDATE_NAME/windows/built64/", "built64", "64") windows_artifactory_upload("MEGAcmd-releases/$MEGACMD_VERSION/$RELEASE_CANDIDATE_NAME/windows/built32/", "built32", "32") } } archiveArtifacts artifacts: 'version.txt', onlyIfSuccessful: true } } post { success { script { echo "Installers successfully uploaded. URL: [${env.REPO_URL}/MEGAcmd-releases/$MEGACMD_VERSION/$RELEASE_CANDIDATE_NAME/windows]" } } } } } post { always { deleteDir() } } } def getVersionFromHeader(String versionFilePath) { return sh(script: """ awk '/#define MEGACMD_MAJOR_VERSION/ { MAJOR=\$3 }; \ /#define MEGACMD_MINOR_VERSION/ { MINOR=\$3 }; \ /#define MEGACMD_MICRO_VERSION/ { MICRO=\$3 }; \ END { print MAJOR"."MINOR"."MICRO }' \ $versionFilePath """ , returnStdout: true).trim() } MEGAcmd-2.5.2_Linux/jenkinsfile/Jenkinsfile_MR_linux000066400000000000000000000554621516543156300224230ustar00rootroot00000000000000pipeline { agent { label "docker" } options { buildDiscarder(logRotator(numToKeepStr: '135', daysToKeepStr: '21')) gitLabConnection('GitLabConnectionJenkins') timestamps() // Add timestamps to all console output } stages { stage('Clean previous runs and update gitlab commit status') { steps { deleteDir() updateGitlabCommitStatus(name: 'Build linux', state: 'running') } } stage('Get build parameters') { parallel { stage('Get build options') { when { allOf { expression { env.gitlabTriggerPhrase != null } expression { env.gitlabTriggerPhrase.contains('BUILD_OPTIONS') } } } steps { script{ BUILD_OPTIONS = sh(script: 'echo "$gitlabTriggerPhrase" | grep BUILD_OPTIONS | awk -F "BUILD_OPTIONS=" \'{print \$2}\' | cut -d"\"" -f2', returnStdout: true).trim() println BUILD_OPTIONS } } post{ always { script{ if (currentBuild.currentResult == 'FAILURE'){ addGitLabMRComment(comment: ":red_circle: ${env.JOB_NAME} FAILURE when getting the additional build parameters :worried:
Build results: [Jenkins [${env.JOB_NAME} ${env.BUILD_DISPLAY_NAME}]](${env.RUN_DISPLAY_URL})
Commit: ${env.GIT_COMMIT}" ) } } } } } stage('Get SDK branch'){ steps { script{ env.SDK_BRANCH = sh(script: 'echo "$gitlabMergeRequestDescription" | grep SDK_SUBMODULE_TEST | awk -F "SDK_SUBMODULE_TEST=" \'{print \$2}\' | cut -d" " -f1', returnStdout: true).trim() if (SDK_BRANCH == ""){ echo "SDK_BRANCH was not found on description so develop will be used by default" env.SDK_BRANCH = "develop" } println SDK_BRANCH } } post{ always { script{ if (currentBuild.currentResult == 'FAILURE'){ addGitLabMRComment(comment: ":red_circle: ${env.JOB_NAME} FAILURE when getting the SDK branch :worried:
Build results: [Jenkins [${env.JOB_NAME} ${env.BUILD_DISPLAY_NAME}]](${env.RUN_DISPLAY_URL})
Commit: ${env.GIT_COMMIT}" ) } } } } } } } stage('Checkout sources') { parallel { stage('Checkout MEGAcmd with prebuildmerge') { steps { checkout([ $class: 'GitSCM', branches: [[name: "origin/${env.gitlabSourceBranch}"]], userRemoteConfigs: [[url: "${env.GIT_URL_MEGACMD}", credentialsId: "12492eb8-0278-4402-98f0-4412abfb65c1"]], extensions: [ [$class: "UserIdentity",name: "jenkins", email: "jenkins@jenkins"], [$class: 'PreBuildMerge', options: [fastForwardMode: 'FF', mergeRemote: "origin", mergeStrategy: 'DEFAULT', mergeTarget: "${env.gitlabTargetBranch}"]] ] ]) } } stage('Checkout SDK') { steps { dir('sdk') { checkout([ $class: 'GitSCM', branches: [[name: "origin/${SDK_BRANCH}"]], userRemoteConfigs: [[url: "${env.GIT_URL_SDK}", credentialsId: "12492eb8-0278-4402-98f0-4412abfb65c1"]], extensions: [ [$class: "UserIdentity",name: "jenkins", email: "jenkins@jenkins"], [$class: "CloneOption", depth: 1, shallow: true, noTags: false, reference: ''] ] ]) } script { megacmd_sources_workspace = WORKSPACE sdk_sources_workspace = "${megacmd_sources_workspace}/sdk" } } } } } stage("Build and test MEGAcmd") { matrix { axes { axis { name 'SANITIZERS' values 'baseline', 'ubsan', 'asan', 'tsan' } axis { name 'BUILD_SYSTEM' values 'cmake' } } stages { stage("Build MEGAcmd container image") { options { timeout(time: 3, unit: 'HOURS') } environment { DOCKER_BUILDKIT=1 BUILD_CORES=4 } steps { sh """ docker build -t meganz/megacmd-${BUILD_SYSTEM}-${SANITIZERS}:${env.BUILD_NUMBER} \ -f ${megacmd_sources_workspace}/build-with-docker/Dockerfile.${BUILD_SYSTEM} \ --build-arg=ENABLE_${SANITIZERS}=ON \ --build-arg=BUILD_CORES \ --ulimit=core=-1 \ --cpuset-cpus=0,1 \ -- ${megacmd_sources_workspace} """ } } stage("MEGAcmd unit tests") { agent { docker { image "meganz/megacmd-${BUILD_SYSTEM}-${SANITIZERS}:${env.BUILD_NUMBER}" reuseNode true args "--name megacmd-${BUILD_SYSTEM}-${SANITIZERS}-${env.BUILD_NUMBER}" } } options { timeout(time: 10, unit: 'MINUTES') } environment { ASAN_OPTIONS="print_stats=1,log_path=${megacmd_sources_workspace}/test-dir-${SANITIZERS}/results/unit-asan-report.log,suppressions=${megacmd_sources_workspace}/contrib/sanitizer/asan.suppressions" TSAN_OPTIONS="log_path=${megacmd_sources_workspace}/test-dir-${SANITIZERS}/results/unit-tsan-report.log,suppressions=${megacmd_sources_workspace}/contrib/sanitizer/tsan.suppressions" UBSAN_OPTIONS="log_path=${megacmd_sources_workspace}/test-dir-${SANITIZERS}/results/unit-ubsan-report.log" LSAN_OPTIONS="suppressions=${megacmd_sources_workspace}/contrib/sanitizer/lsan.suppressions" HOME="${megacmd_sources_workspace}/test-dir-${SANITIZERS}" } steps { dir("test-dir-${SANITIZERS}") { sh """ ulimit -c unlimited; # Execute with a timeout lower than Jenkins stage timeout, enforcing a SIGSEGV to ensure core dumping timeout -s 11 550 /usr/bin/mega-cmd-tests-unit \ --gtest_output=xml:${megacmd_sources_workspace}/test-dir-${SANITIZERS}/results/mega-cmd-tests-unit.xml \ --gtest_shuffle """ } } post { always { sh """ find . -name core -exec zstd -z --fast=3 {} -o core_unit.zstd \\; -exec rm {} \\; \ && [ -f core_unit.zstd ] \ && zstd -z --fast=3 /usr/bin/mega-cmd-tests-unit -o ./mega-cmd-tests-unit.zstd \ || true """ archiveArtifacts artifacts: "test-dir-${SANITIZERS}/.megaCmd/megacmdserver.log*", allowEmptyArchive: true archiveArtifacts artifacts: "**/core*zstd", allowEmptyArchive: true archiveArtifacts artifacts: "**/mega-cmd-tests-unit.zstd", allowEmptyArchive: true archiveArtifacts artifacts: "test-dir-${SANITIZERS}/results/unit-*-report.log.*", allowEmptyArchive: true junit "test-dir-${SANITIZERS}/results/mega-cmd-tests-unit.xml" } } } stage("MEGAcmd integration tests") { agent { docker { image "meganz/megacmd-${BUILD_SYSTEM}-${SANITIZERS}:${env.BUILD_NUMBER}" reuseNode true args "--name megacmd-${BUILD_SYSTEM}-${SANITIZERS}-${env.BUILD_NUMBER} --cap-add SYS_ADMIN --device /dev/fuse --security-opt apparmor:unconfined" } } options { timeout(time: 1, unit: 'HOURS') } environment { MEGACMD_TEST_USER='' MEGACMD_TEST_PASS=credentials('MEGACMD_TESTS_PASSWORD') ASAN_OPTIONS="print_stats=1,log_path=${megacmd_sources_workspace}/test-dir-${SANITIZERS}/results/integration-asan-report.log,suppressions=${megacmd_sources_workspace}/contrib/sanitizer/asan.suppressions" TSAN_OPTIONS="log_path=${megacmd_sources_workspace}/test-dir-${SANITIZERS}/results/integration-tsan-report.log,suppressions=${megacmd_sources_workspace}/contrib/sanitizer/tsan.suppressions" UBSAN_OPTIONS="log_path=${megacmd_sources_workspace}/test-dir-${SANITIZERS}/results/integration-ubsan-report.log" LSAN_OPTIONS="suppressions=${megacmd_sources_workspace}/contrib/sanitizer/lsan.suppressions" HOME="${megacmd_sources_workspace}/test-dir-${SANITIZERS}" } steps { lock(label: 'testing_accounts_megacmd', variable: 'MEGACMD_TEST_USER', quantity: 1, resource: null) { dir("test-dir-${SANITIZERS}") { sh """ ulimit -c unlimited; # Execute with a timeout lower than Jenkins stage timeout, enforcing a SIGSEGV to ensure core dumping timeout -s 11 3500 /usr/bin/mega-cmd-tests-integration \ --gtest_output=xml:${megacmd_sources_workspace}/test-dir-${SANITIZERS}/results/mega-cmd-tests-integration.xml \ --gtest_shuffle \ """ } } } post { always { sh """ find . -name core -exec zstd -z --fast=3 {} -o core_integration.zstd \\; -exec rm {} \\; \ && [ -f core_integration.zstd ] \ && zstd -z --fast=3 /usr/bin/mega-cmd-tests-integration -o ./mega-cmd-tests-integration.zstd \ || true """ archiveArtifacts "test-dir-${SANITIZERS}/.megaCmd/megacmdserver.log*" archiveArtifacts artifacts: "**/core*zstd", allowEmptyArchive: true archiveArtifacts artifacts: "**/mega-cmd-tests-integration.zstd", allowEmptyArchive: true archiveArtifacts artifacts: "test-dir-${SANITIZERS}/results/integration-*-report.log.*", allowEmptyArchive: true junit "test-dir-${SANITIZERS}/results/mega-cmd-tests-integration.xml" } } } stage("MEGAcmd Python tests") { agent { docker { image "meganz/megacmd-${BUILD_SYSTEM}-${SANITIZERS}:${env.BUILD_NUMBER}" reuseNode true args "--name megacmd-${BUILD_SYSTEM}-${SANITIZERS}-${env.BUILD_NUMBER}" } } options { timeout(time: 2, unit: 'HOURS') } environment { MEGA_EMAILS='' MEGA_PWD=credentials('MEGACMD_TESTS_PASSWORD') MEGA_PWD_AUX=credentials('MEGACMD_TESTS_PASSWORD') ASAN_OPTIONS="print_stats=1,log_path=${megacmd_sources_workspace}/pytest-dir-${SANITIZERS}/results/pyserver-asan-report.log,suppressions=${megacmd_sources_workspace}/contrib/sanitizer/asan.suppressions" TSAN_OPTIONS="log_path=${megacmd_sources_workspace}/pytest-dir-${SANITIZERS}/results/pyserver-tsan-report.log,suppressions=${megacmd_sources_workspace}/contrib/sanitizer/tsan.suppressions" UBSAN_OPTIONS="log_path=${megacmd_sources_workspace}/pytest-dir-${SANITIZERS}/results/pyserver-ubsan-report.log" LSAN_OPTIONS="suppressions=${megacmd_sources_workspace}/contrib/sanitizer/lsan.suppressions" HOME="${megacmd_sources_workspace}/pytest-dir-${SANITIZERS}" YES_I_KNOW_THIS_WILL_CLEAR_MY_MEGA_ACCOUNT=1 VERBOSE=1 } steps { dir("pytest-dir-${SANITIZERS}") { sh """ # Execute MEGAcmd server in the background with a timeout lower than Jenkins stage timeout, enforcing a SIGSEGV to ensure core dumping # If a core is dumped, compress it bash -c \"ulimit -c unlimited && \ timeout -s 11 3550 nohup /usr/bin/mega-cmd-server --verbose-full --log-to-file; \ zstd -z --fast=3 core -o core_server.zstd \ && rm core \ && zstd -z --fast=3 /usr/bin/mega-cmd-server -o ./mega-cmd-server.zstd \ || true \" & """ } sleep 1 lock(label: 'testing_accounts_megacmd', variable: 'MEGA_EMAILS', quantity: 2, resource: null) { script { // Skipping the 'serving' test until CMD-389 is resolved for (test in ['misc', 'put', 'rm', 'find', 'get']) { for (cmdshell in ['', 'cmdshell-']) { def testDir = "pytest-dir-${SANITIZERS}/${cmdshell}${test}-results" def asanOptions = "print_stats=1,log_path=${megacmd_sources_workspace}/${testDir}/py-asan-report.log" def tsanOptions = "log_path=${megacmd_sources_workspace}/${testDir}/py-tsan-report.log" def ubsanOptions = "log_path=${megacmd_sources_workspace}/${testDir}/py-ubsan-report.log" stage("MEGAcmd Python ${cmdshell}${test} test") { withEnv([ "MEGA_EMAIL=${env.MEGA_EMAILS0}", "MEGA_EMAIL_AUX=${env.MEGA_EMAILS1}", "ASAN_OPTIONS=${asanOptions}", "TSAN_OPTIONS=${tsanOptions}", "UBSAN_OPTIONS=${ubsanOptions}", "OUT_DIR_JUNIT_XML=${megacmd_sources_workspace}/${testDir}" ]) { def envStr = '' if (cmdshell != '') { envStr = "MEGACMDSHELL=/usr/bin/mega-cmd " } try { dir("pytest-dir-${SANITIZERS}/working-dir") { sh """ # Note: to detect client side issues, we could execute with a timeout lower than Jenkins stage timeout, enforcing a SIGSEGV to ensure core dumping # In this case, splitting the timeout between all test cases #ulimit -c unlimited; #${envStr}timeout -s 11 710 /usr/local/bin/megacmd_${test}_test.py ${envStr} /usr/local/bin/megacmd_${test}_test.py """ } junit "${testDir}/TEST-*.xml" } finally { archiveArtifacts artifacts: "${testDir}/py-*-report.log.*", allowEmptyArchive: true dir("pytest-dir-${SANITIZERS}") { sh "find . -name core -exec zstd -z --fast=3 {} -o core_client_${cmdshell}${test}.zstd \\; -exec rm {} \\; || true" } dir("pytest-dir-${SANITIZERS}/working-dir") { deleteDir() } } } } } } } } } post { always { sh "/usr/bin/mega-exec logout || true" sh "/usr/bin/mega-exec quit || true" sh """ find . -name core_client*.zstd | grep . \ && tar -cf - /usr/bin/mega-cmd /usr/bin/mega-exec | zstd -z --fast=3 -o ./mega-cmd-client-executables.tar.zstd \ || true """ archiveArtifacts "pytest-dir-${SANITIZERS}/.megaCmd/megacmdserver.log*" archiveArtifacts artifacts: "pytest-dir-${SANITIZERS}/results/pyserver-*-report.log.*", allowEmptyArchive: true archiveArtifacts artifacts: "**/core*zstd", allowEmptyArchive: true archiveArtifacts artifacts: "**/mega-*.zstd", allowEmptyArchive: true } } } } post { always { sh "docker image rm -f -- meganz/megacmd-${BUILD_SYSTEM}-${SANITIZERS}:${env.BUILD_NUMBER} || true" } } } } } post { always { script { if (currentBuild.currentResult == 'SUCCESS') { addGitLabMRComment(comment: ":white_check_mark: ${currentBuild.projectName} :penguin: Linux SUCCEEDED :muscle:
Build results: [Jenkins [${currentBuild.displayName}]](${currentBuild.absoluteUrl})
Commit: ${env.GIT_COMMIT}" ) updateGitlabCommitStatus(name: 'Build linux', state: 'success') } if (currentBuild.currentResult == 'FAILURE') { addGitLabMRComment(comment: ":red_circle: ${currentBuild.projectName} :penguin: Linux FAILURE :worried:
Build results: [Jenkins [${currentBuild.displayName}]](${currentBuild.absoluteUrl})
Commit: ${env.GIT_COMMIT}" ) updateGitlabCommitStatus(name: 'Build linux', state: 'failed') } if (currentBuild.currentResult == 'ABORTED') { addGitLabMRComment(comment: ":interrobang: ${currentBuild.projectName} :penguin: Linux ABORTED :confused:
Build results: [Jenkins [${currentBuild.displayName}]](${currentBuild.absoluteUrl})
Commit: ${env.GIT_COMMIT}" ) updateGitlabCommitStatus(name: 'Build linux', state: 'canceled') } if (currentBuild.currentResult == 'UNSTABLE') { addGitLabMRComment(comment: ":interrobang: ${currentBuild.projectName} :penguin: Linux UNSTABLE :confused:
Build results: [Jenkins [${currentBuild.displayName}]](${currentBuild.absoluteUrl})
Commit: ${env.GIT_COMMIT}" ) updateGitlabCommitStatus(name: 'Build linux', state: 'failed') } } } } } MEGAcmd-2.5.2_Linux/jenkinsfile/Jenkinsfile_MR_linux_packages000066400000000000000000000247371516543156300242620ustar00rootroot00000000000000pipeline { agent { label "linux-testing-package-builder" } options { buildDiscarder(logRotator(numToKeepStr: '135', daysToKeepStr: '21')) gitLabConnection('GitLabConnectionJenkins') timestamps() // Add timestamps to all console output } stages { stage('Clean previous runs and update gitlab commit status') { steps { deleteDir() updateGitlabCommitStatus(name: 'Build linux packages', state: 'running') } } stage('Get build parameters') { parallel { stage('Get build options') { steps { script{ BUILD_OPTIONS = ' ' env.ARCHITECTURE = sh(script: 'echo "$gitlabTriggerPhrase" | grep ARCHITECTURE | awk -F "ARCHITECTURE=" \'{print \$2}\' | cut -d\\" -f2', returnStdout: true).trim() if (env.ARCHITECTURE == "") { env.ARCHITECTURE = "amd64" } env.BUILD_OPTIONS = sh(script: 'echo "$gitlabTriggerPhrase" | grep BUILD_OPTIONS | awk -F "BUILD_OPTIONS=" \'{print \$2}\' | cut -d"\\"" -f2 || :', returnStdout: true).trim() env.DISTRIBUTION = sh(script: 'echo "$gitlabTriggerPhrase" | grep DISTRIBUTION | awk -F "DISTRIBUTION=" \'{print \$2}\' | cut -d\\" -f2', returnStdout: true).trim() if (env.DISTRIBUTION == "") { env.DISTRIBUTION = "xUbuntu_22.04" } println BUILD_OPTIONS println DISTRIBUTION println ARCHITECTURE } } post{ always { script{ if (currentBuild.currentResult == 'FAILURE'){ addGitLabMRComment(comment: ":red_circle: ${env.JOB_NAME} FAILURE when getting the additional build parameters :worried:
Build results: [Jenkins [${env.JOB_NAME} ${env.BUILD_DISPLAY_NAME}]](${env.RUN_DISPLAY_URL})
Commit: ${env.GIT_COMMIT}" ) } } } } } stage('Get SDK branch'){ steps { script{ env.SDK_BRANCH = sh(script: 'echo "$gitlabMergeRequestDescription" | grep SDK_SUBMODULE_TEST | awk -F "SDK_SUBMODULE_TEST=" \'{print \$2}\' | cut -d" " -f1', returnStdout: true).trim() if (SDK_BRANCH == ""){ echo "SDK_BRANCH was not found on description so develop will be used by default" env.SDK_BRANCH = "develop" } println SDK_BRANCH } } post{ always { script{ if (currentBuild.currentResult == 'FAILURE'){ addGitLabMRComment(comment: ":red_circle: ${env.JOB_NAME} FAILURE when getting the SDK branch :worried:
Build results: [Jenkins [${env.JOB_NAME} ${env.BUILD_DISPLAY_NAME}]](${env.RUN_DISPLAY_URL})
Commit: ${env.GIT_COMMIT}" ) } } } } } } } stage('Checkout sources') { parallel { stage('Checkout MEGAcmd') { steps { checkout([ $class: 'GitSCM', branches: [[name: "origin/${env.gitlabSourceBranch}"]], userRemoteConfigs: [[url: "${env.GIT_URL_MEGACMD}", credentialsId: "12492eb8-0278-4402-98f0-4412abfb65c1"]], extensions: [ [$class: "UserIdentity",name: "jenkins", email: "jenkins@jenkins"], ] ]) } } stage('Checkout SDK') { steps { dir('sdk') { checkout([ $class: 'GitSCM', branches: [[name: "${SDK_BRANCH}"]], userRemoteConfigs: [[url: "${env.GIT_URL_SDK}", credentialsId: "12492eb8-0278-4402-98f0-4412abfb65c1"]], extensions: [ [$class: "UserIdentity",name: "jenkins", email: "jenkins@jenkins"], [$class: "CloneOption", depth: 1, shallow: true, noTags: false, reference: ''] ] ]) } script { linux_sources_workspace = WORKSPACE sdk_sources_workspace = "${linux_sources_workspace}/sdk" } } } } } stage('Build packages'){ when { beforeAgent true expression { env.gitlabTriggerPhrase != null && (env.gitlabTriggerPhrase == 'trigger package' || env.gitlabTriggerPhrase.startsWith('trigger package linux')) } } stages{ stage('Build linux package'){ options { timeout(time: 840, unit: 'MINUTES') } steps { echo "Do Build for $DISTRIBUTION-$ARCHITECTURE" dir(linux_sources_workspace){ lock(resource: "$DISTRIBUTION-$ARCHITECTURE-MEGAcmd-build", quantity: 1, ){ sh "/opt/buildTools/build/buildManager.sh -a $ARCHITECTURE -j 3 build $DISTRIBUTION . megacmd" sh "/opt/buildTools/repo/repoManager.sh add /srv/builder/results/$DISTRIBUTION/$ARCHITECTURE/megacmd/ $DISTRIBUTION" sh "SIGN_KEY_PATH=/srv/sign_test/ /opt/buildTools/repo/repoManager.sh build -n $DISTRIBUTION" } } } } stage ('Upload Linux package') { steps { dir(linux_sources_workspace){ script{ def ARCH_REGEX switch (ARCHITECTURE) { case 'amd64': ARCH_REGEX = 'x86_64|amd64' break case 'arm64': ARCH_REGEX = 'aarch64|arm64' break default: ARCH_REGEX = ARCHITECTURE } withCredentials([string(credentialsId: 'MEGACMD_ARTIFACTORY_TOKEN', variable: 'MEGACMD_ARTIFACTORY_TOKEN')]) { sh """ jf rt del \ --url ${REPO_URL} \ --access-token \${MEGACMD_ARTIFACTORY_TOKEN} \ MEGAcmd-sprints/${env.gitlabMergeRequestIid}/linux/$DISTRIBUTION """ dir("/srv/repo/private/$DISTRIBUTION"){ sh """ jf rt upload \ --url ${REPO_URL} \ --access-token \${MEGACMD_ARTIFACTORY_TOKEN} \ --regexp '(($ARCH_REGEX)/megacmd.*deb\$|($ARCH_REGEX)/($ARCH_REGEX)/megacmd.*rpm\$|($ARCH_REGEX)/megacmd.*\\.pkg\\.tar\\.zst\$|($ARCH_REGEX)/megacmd.*\\.pkg\\.tar\\.xz\$)' \ MEGAcmd-sprints/${env.gitlabMergeRequestIid}/linux/$DISTRIBUTION/ """ } } uploadPackagesExecuted = true } } } } } } } post { always { script { if (currentBuild.currentResult == 'SUCCESS') { addGitLabMRComment(comment: ":white_check_mark: ${currentBuild.projectName} :penguin: LINUX SUCCEEDED :muscle:
Build results: [Jenkins [${currentBuild.displayName}]](${currentBuild.absoluteUrl})
Commit: ${env.GIT_COMMIT}
Packages URL: [${env.REPO_URL}/MEGAcmd-sprints/${env.gitlabMergeRequestIid}/linux/$DISTRIBUTION]" ) updateGitlabCommitStatus(name: 'Build linux packages', state: 'success') } if (currentBuild.currentResult == 'FAILURE') { addGitLabMRComment(comment: ":red_circle: ${currentBuild.projectName} :penguin: Linux FAILURE :worried:
Build results: [Jenkins [${currentBuild.displayName}]](${currentBuild.absoluteUrl})
Commit: ${env.GIT_COMMIT}" ) updateGitlabCommitStatus(name: 'Build linux packages', state: 'failed') } if (currentBuild.currentResult == 'ABORTED') { addGitLabMRComment(comment: ":interrobang: ${currentBuild.projectName} :penguin: Linux ABORTED :confused:
Build results: [Jenkins [${currentBuild.displayName}]](${currentBuild.absoluteUrl})
Commit: ${env.GIT_COMMIT}" ) updateGitlabCommitStatus(name: 'Build linux packages', state: 'canceled') } if (currentBuild.currentResult == 'UNSTABLE') { addGitLabMRComment(comment: ":interrobang: ${currentBuild.projectName} :penguin: Linux UNSTABLE :confused:
Build results: [Jenkins [${currentBuild.displayName}]](${currentBuild.absoluteUrl})
Commit: ${env.GIT_COMMIT}" ) updateGitlabCommitStatus(name: 'Build linux packages', state: 'failed') } } } } } MEGAcmd-2.5.2_Linux/jenkinsfile/Jenkinsfile_MR_macos000066400000000000000000000233731516543156300223620ustar00rootroot00000000000000pipeline { agent { label "osx && arm64" } environment { BUILD_OPTIONS = '' } options { buildDiscarder(logRotator(numToKeepStr: '135', daysToKeepStr: '21')) gitLabConnection('GitLabConnectionJenkins') timestamps() // Add timestamps to all console output } stages { stage('clean previous runs and update gitlab commit status'){ steps{ deleteDir() updateGitlabCommitStatus(name: 'Build macos', state: 'running') } } stage('Get build parameters'){ parallel{ stage('Get build options'){ when { allOf { expression { env.gitlabTriggerPhrase != null } expression { env.gitlabTriggerPhrase.contains('BUILD_OPTIONS') } } } steps { script{ env.BUILD_OPTIONS = sh(script: 'echo "$gitlabTriggerPhrase" | grep BUILD_OPTIONS | awk -F "BUILD_OPTIONS=" \'{print \$2}\' | cut -d"\"" -f2', returnStdout: true).trim() println BUILD_OPTIONS } } post{ always { script{ if (currentBuild.currentResult == 'FAILURE'){ addGitLabMRComment(comment: ":red_circle: ${env.JOB_NAME} FAILURE when getting the additional build parameters :worried:
Build results: [Jenkins [${env.JOB_NAME} ${env.BUILD_DISPLAY_NAME}]](${env.RUN_DISPLAY_URL})
Commit: ${env.GIT_COMMIT}" ) } } } } } stage('Get SDK branch'){ steps { script{ env.SDK_BRANCH = sh(script: 'echo "$gitlabMergeRequestDescription" | grep SDK_SUBMODULE_TEST | awk -F "SDK_SUBMODULE_TEST=" \'{print \$2}\' | cut -d" " -f1', returnStdout: true).trim() if (SDK_BRANCH == ""){ echo "SDK_BRANCH was not found on description so develop will be used by default" env.SDK_BRANCH = "develop" } println SDK_BRANCH } } post{ always { script{ if (currentBuild.currentResult == 'FAILURE'){ addGitLabMRComment(comment: ":red_circle: ${env.JOB_NAME} FAILURE when getting the SDK branch :worried:
Build results: [Jenkins [${env.JOB_NAME} ${env.BUILD_DISPLAY_NAME}]](${env.RUN_DISPLAY_URL})
Commit: ${env.GIT_COMMIT}" ) } } } } } } } stage('Checkout sources'){ parallel{ stage('Checkout MEGAcmd with prebuildmerge'){ steps{ checkout([ $class: 'GitSCM', branches: [[name: "origin/${env.gitlabSourceBranch}"]], userRemoteConfigs: [[ url: "${env.GIT_URL_MEGACMD}", credentialsId: "12492eb8-0278-4402-98f0-4412abfb65c1" ]], extensions: [ [$class: "UserIdentity",name: "jenkins", email: "jenkins@jenkins"], [$class: 'PreBuildMerge', options: [fastForwardMode: 'FF', mergeRemote: "origin", mergeStrategy: 'DEFAULT', mergeTarget: "${env.gitlabTargetBranch}"]] ] ]) } } stage('Checkout SDK'){ steps{ dir('sdk'){ checkout([ $class: 'GitSCM', branches: [[name: "origin/${SDK_BRANCH}"]], userRemoteConfigs: [[ url: "${env.GIT_URL_SDK}", credentialsId: "12492eb8-0278-4402-98f0-4412abfb65c1" ]], extensions: [ [$class: "UserIdentity",name: "jenkins", email: "jenkins@jenkins"], [$class: "CloneOption", depth: 1, shallow: true, noTags: false, reference: ''] ] ]) } script{ megacmd_sources_workspace = WORKSPACE sdk_sources_workspace = "${megacmd_sources_workspace}/sdk" } } } } } stage('Build MEGAcmd'){ environment{ PATH = "cmdinstall/bin:${env.PATH}" BUILD_DIR = "cmdbuild" VCPKGPATH = "${env.HOME}/jenkins/vcpkg" } options{ timeout(time: 3, unit: 'HOURS') } steps{ sh "rm -rf ${BUILD_DIR}; mkdir ${BUILD_DIR}" sh "cmake -DCMAKE_VERBOSE_MAKEFILE=1 -DENABLE_MEGACMD_TESTS=ON -DCMAKE_BUILD_TYPE=Debug -DVCPKG_ROOT=${VCPKGPATH} -DCMAKE_INSTALL_PREFIX=cmdinstall -S '${megacmd_sources_workspace}' -B ${BUILD_DIR} '${env.BUILD_OPTIONS}'" sh "cmake --build ${megacmd_sources_workspace}/${BUILD_DIR} -j 2" sh "cmake --install ${megacmd_sources_workspace}/${BUILD_DIR}" } } stage('Run unit tests') { environment { INSTALL_DIR = "${megacmd_sources_workspace}/cmdinstall" PATH = "${env.PATH}:${env.INSTALL_DIR}/mega-cmd-tests-unit.app/Contents/MacOS/" HOME = "${megacmd_sources_workspace}/unit-test-dir/home" } options { timeout(time: 15, unit: 'MINUTES') } steps { dir('unit-test-dir') { echo "Running tests" sh "mkdir -p \$HOME/Library/Caches" sh "mega-cmd-tests-unit --gtest_output=xml:${megacmd_sources_workspace}/unit-test-dir/mega-cmd-tests.xml --gtest_shuffle" } } post { always { archiveArtifacts artifacts: "unit-test-dir/home/.megaCmd/megacmdserver.log*", allowEmptyArchive: true junit testResults: "unit-test-dir/mega-cmd-tests.xml", keepProperties: true } } } stage('Run integration tests') { options { timeout(time: 1, unit: 'HOURS') } environment { INSTALL_DIR = "${megacmd_sources_workspace}/cmdinstall" PATH = "${env.PATH}:${env.INSTALL_DIR}/bin:${env.INSTALL_DIR}/mega-cmd-tests-integration.app/Contents/MacOS/" HOME = "${megacmd_sources_workspace}/integration-test-dir/home" MEGACMD_TEST_USER = '' MEGACMD_TEST_PASS = credentials('MEGACMD_TESTS_PASSWORD') MEGACMD_SOCKET_NAME = "sock_${env.BUILD_ID}" } steps { lock(label: 'testing_accounts_megacmd', variable: 'MEGACMD_TEST_USER', quantity: 1, resource: null) { dir('integration-test-dir') { echo "Running tests" sh "mkdir -p \$HOME/Library/Caches" sh "mega-cmd-tests-integration --gtest_output=xml:${megacmd_sources_workspace}/integration-test-dir/mega-cmd-tests.xml --gtest_shuffle" } } } post { always { archiveArtifacts "integration-test-dir/home/.megaCmd/megacmdserver.log*" junit testResults: "integration-test-dir/mega-cmd-tests.xml", keepProperties: true } } } } post { always { script { if (currentBuild.currentResult == 'SUCCESS') { addGitLabMRComment(comment: ":white_check_mark: ${currentBuild.projectName} :green_apple: macOS SUCCEEDED :muscle:
Build results: [Jenkins [${currentBuild.displayName}]](${currentBuild.absoluteUrl})
Commit: ${env.GIT_COMMIT}" ) updateGitlabCommitStatus(name: 'Build macos', state: 'success') } if (currentBuild.currentResult == 'FAILURE') { addGitLabMRComment(comment: ":red_circle: ${currentBuild.projectName} :green_apple: macOS FAILURE :worried:
Build results: [Jenkins [${currentBuild.displayName}]](${currentBuild.absoluteUrl})
Commit: ${env.GIT_COMMIT}" ) updateGitlabCommitStatus(name: 'Build macos', state: 'failed') } if (currentBuild.currentResult == 'ABORTED') { addGitLabMRComment(comment: ":interrobang: ${currentBuild.projectName} :green_apple: macOS ABORTED :confused:
Build results: [Jenkins [${currentBuild.displayName}]](${currentBuild.absoluteUrl})
Commit: ${env.GIT_COMMIT}" ) updateGitlabCommitStatus(name: 'Build macos', state: 'canceled') } if (currentBuild.currentResult == 'UNSTABLE') { addGitLabMRComment(comment: ":interrobang: ${currentBuild.projectName} :green_apple: macOS UNSTABLE :confused:
Build results: [Jenkins [${currentBuild.displayName}]](${currentBuild.absoluteUrl})
Commit: ${env.GIT_COMMIT}" ) updateGitlabCommitStatus(name: 'Build macos', state: 'failed') } } deleteDir() } } } MEGAcmd-2.5.2_Linux/jenkinsfile/Jenkinsfile_MR_windows000066400000000000000000000264431516543156300227530ustar00rootroot00000000000000pipeline { agent { label "windows && amd64" } environment { BUILD_OPTIONS = '' } options { buildDiscarder(logRotator(numToKeepStr: '135', daysToKeepStr: '21')) gitLabConnection('GitLabConnectionJenkins') timestamps() // Add timestamps to all console output } stages { stage('clean previous runs and update gitlab commit status'){ steps{ deleteDir() updateGitlabCommitStatus(name: 'Build windows', state: 'running') } } stage('Get build parameters'){ parallel{ stage('Get build options'){ when { allOf { expression { env.gitlabTriggerPhrase != null } expression { env.gitlabTriggerPhrase.contains('BUILD_OPTIONS') } } } steps { script{ env.BUILD_OPTIONS = sh(script: 'echo "$gitlabTriggerPhrase" | grep BUILD_OPTIONS | awk -F "BUILD_OPTIONS=" \'{print \$2}\' | cut -d"\"" -f2', returnStdout: true).trim() println BUILD_OPTIONS } } post{ always { script{ if (currentBuild.currentResult == 'FAILURE'){ addGitLabMRComment(comment: ":red_circle: ${env.JOB_NAME} FAILURE when getting the additional build parameters :worried:
Build results: [Jenkins [${env.JOB_NAME} ${env.BUILD_DISPLAY_NAME}]](${env.RUN_DISPLAY_URL})
Commit: ${env.GIT_COMMIT}" ) } } } } } stage('Get SDK branch'){ steps { script{ env.SDK_BRANCH = sh(script: 'echo "$gitlabMergeRequestDescription" | grep SDK_SUBMODULE_TEST | awk -F "SDK_SUBMODULE_TEST=" \'{print \$2}\' | cut -d" " -f1', returnStdout: true).trim() if (SDK_BRANCH == ""){ echo "SDK_BRANCH was not found on description so develop will be used by default" env.SDK_BRANCH = "develop" } println SDK_BRANCH } } post{ always { script{ if (currentBuild.currentResult == 'FAILURE'){ addGitLabMRComment(comment: ":red_circle: ${env.JOB_NAME} FAILURE when getting the SDK branch :worried:
Build results: [Jenkins [${env.JOB_NAME} ${env.BUILD_DISPLAY_NAME}]](${env.RUN_DISPLAY_URL})
Commit: ${env.GIT_COMMIT}" ) } } } } } } } stage('Checkout sources'){ parallel{ stage('Checkout MEGAcmd with prebuildmerge'){ steps{ checkout([ $class: 'GitSCM', branches: [[name: "origin/${env.gitlabSourceBranch}"]], userRemoteConfigs: [[ url: "${env.GIT_URL_MEGACMD}", credentialsId: "12492eb8-0278-4402-98f0-4412abfb65c1" ]], extensions: [ [$class: "UserIdentity",name: "jenkins", email: "jenkins@jenkins"], [$class: 'PreBuildMerge', options: [fastForwardMode: 'FF', mergeRemote: "origin", mergeStrategy: 'DEFAULT', mergeTarget: "${env.gitlabTargetBranch}"]] ] ]) script{ megacmd_sources_workspace = WORKSPACE } } } stage('Checkout VCPKG'){ steps{ dir('vcpkg'){ checkout([ $class: 'GitSCM', branches: [[name: "origin/master"]], userRemoteConfigs: [[ url: "https://github.com/microsoft/vcpkg" ]], extensions: [ [$class: "UserIdentity",name: "jenkins", email: "jenkins@jenkins"], [$class: "CloneOption", noTags: false, reference: ''] ] ]) } } } stage('Checkout SDK'){ steps{ dir('sdk'){ checkout([ $class: 'GitSCM', branches: [[name: "origin/${SDK_BRANCH}"]], userRemoteConfigs: [[ url: "${env.GIT_URL_SDK}", credentialsId: "12492eb8-0278-4402-98f0-4412abfb65c1" ]], extensions: [ [$class: "UserIdentity",name: "jenkins", email: "jenkins@jenkins"], [$class: "CloneOption", depth: 1, shallow: true, noTags: false, reference: ''] ] ]) } } } } } stage('Build MEGAcmd'){ environment{ VCPKGPATH = "${megacmd_sources_workspace}\\..\\vcpkg" BUILD_DIR = "cmdbuild" _MSPDBSRV_ENDPOINT_ = "${BUILD_TAG}" TMP = "${megacmd_sources_workspace}\\tmp" TEMP = "${megacmd_sources_workspace}\\tmp" TMPDIR = "${megacmd_sources_workspace}\\tmp" } options{ timeout(time: 3, unit: 'HOURS') } steps{ sh "rm -rf ${BUILD_DIR}; mkdir ${BUILD_DIR}" sh "mkdir tmp" sh "cmake -DCMAKE_VERBOSE_MAKEFILE=1 -DENABLE_MEGACMD_TESTS=ON -DCMAKE_BUILD_TYPE=Debug -DVCPKG_ROOT='${VCPKGPATH}' -DCMAKE_INSTALL_PREFIX=cmdinstall -S '${megacmd_sources_workspace}' -B ${BUILD_DIR} '${env.BUILD_OPTIONS}'" sh "cmake --build ${BUILD_DIR} -j 2" sh "cmake --install ${BUILD_DIR} --config Debug" } } stage('Run unit tests') { environment { INSTALL_DIR = "${megacmd_sources_workspace}\\cmdbuild\\Debug" MEGACMD_PIPE_SUFFIX = "${env.BUILD_ID}" } options { timeout(time: 3, unit: 'HOURS') } steps { dir('unit-test-dir') { echo "Running tests" bat "${INSTALL_DIR}\\mega-cmd-tests-unit.exe --gtest_shuffle --gtest_output=xml:${megacmd_sources_workspace}\\unit-test-dir\\mega-cmd-tests.xml" } } post { always { archiveArtifacts artifacts: "cmdbuild\\Debug\\.megaCmd\\megacmdserver.log*", allowEmptyArchive: true junit "unit-test-dir\\mega-cmd-tests.xml" bat "rmdir ${INSTALL_DIR}\\.megaCmd /s /q" } } } stage('Run integration tests') { environment { INSTALL_DIR = "${megacmd_sources_workspace}\\cmdbuild\\Debug" MEGACMD_PIPE_SUFFIX = "${env.BUILD_ID}" MEGACMD_TEST_USER = '' MEGACMD_TEST_PASS = credentials('MEGACMD_TESTS_PASSWORD') } options { timeout(time: 3, unit: 'HOURS') } steps { lock(label: 'testing_accounts_megacmd', variable: 'MEGACMD_TEST_USER', quantity: 1, resource: null) { dir('integration-test-dir') { echo "Running tests" script { def status = bat script: "${INSTALL_DIR}\\mega-cmd-tests-integration.exe --gtest_shuffle --gtest_output=xml:${megacmd_sources_workspace}\\integration-test-dir\\mega-cmd-tests.xml", returnStatus: true // There is an unresolved issue on Windows that results in an incorrect status code being returned // from the process above; so we use a file to get the actual value if (fileExists("exit_code.txt")) { def exitCode = readFile("exit_code.txt").trim().toInteger() if (status != exitCode) { echo "Incorrect status code detected (expected: ${exitCode} actual: ${status})" } if (exitCode != 0) { error "Integration tests failed with exit code ${exitCode}" } } else { unstable "Exit code file does not exist" } } } } } post { always { archiveArtifacts "cmdbuild\\Debug\\.megaCmd\\megacmdserver.log*" junit "integration-test-dir\\mega-cmd-tests.xml" bat "rmdir ${INSTALL_DIR}\\.megaCmd /s /q" } } } } post{ always{ script{ if (currentBuild.currentResult == 'SUCCESS'){ addGitLabMRComment(comment: ":white_check_mark: ${currentBuild.projectName} Windows SUCCEEDED :muscle:
Build results: [Jenkins [${currentBuild.displayName}]](${currentBuild.absoluteUrl})
Commit: ${env.GIT_COMMIT}" ) updateGitlabCommitStatus(name: 'Build windows', state: 'success') } if (currentBuild.currentResult == 'FAILURE'){ addGitLabMRComment(comment: ":red_circle: ${currentBuild.projectName} Windows FAILURE :worried:
Build results: [Jenkins [${currentBuild.displayName}]](${currentBuild.absoluteUrl})
Commit: ${env.GIT_COMMIT}" ) updateGitlabCommitStatus(name: 'Build windows', state: 'failed') } if (currentBuild.currentResult == 'ABORTED'){ addGitLabMRComment(comment: ":interrobang: ${currentBuild.projectName} Windows ABORTED :confused:
Build results: [Jenkins [${currentBuild.displayName}]](${currentBuild.absoluteUrl})
Commit: ${env.GIT_COMMIT}" ) updateGitlabCommitStatus(name: 'Build windows', state: 'canceled') } if (currentBuild.currentResult == 'UNSTABLE'){ addGitLabMRComment(comment: ":interrobang: ${currentBuild.projectName} Windows UNSTABLE :confused:
Build results: [Jenkins [${currentBuild.displayName}]](${currentBuild.absoluteUrl})
Commit: ${env.GIT_COMMIT}" ) updateGitlabCommitStatus(name: 'Build windows', state: 'failed') } } deleteDir() } } } MEGAcmd-2.5.2_Linux/sdk/000077500000000000000000000000001516543156300146665ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/src/000077500000000000000000000000001516543156300146745ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/src/client/000077500000000000000000000000001516543156300161525ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/src/client/mega-attr000077500000000000000000000000401516543156300177530ustar00rootroot00000000000000#!/bin/bash mega-exec attr "$@" MEGAcmd-2.5.2_Linux/src/client/mega-backup000077500000000000000000000000421516543156300202500ustar00rootroot00000000000000#!/bin/bash mega-exec backup "$@" MEGAcmd-2.5.2_Linux/src/client/mega-cancel000077500000000000000000000000421516543156300202300ustar00rootroot00000000000000#!/bin/bash mega-exec cancel "$@" MEGAcmd-2.5.2_Linux/src/client/mega-cat000077500000000000000000000000371516543156300175560ustar00rootroot00000000000000#!/bin/bash mega-exec cat "$@" MEGAcmd-2.5.2_Linux/src/client/mega-cd000077500000000000000000000000361516543156300173740ustar00rootroot00000000000000#!/bin/bash mega-exec cd "$@" MEGAcmd-2.5.2_Linux/src/client/mega-configure000077500000000000000000000000451516543156300207670ustar00rootroot00000000000000#!/bin/bash mega-exec configure "$@" MEGAcmd-2.5.2_Linux/src/client/mega-confirm000077500000000000000000000000431516543156300204410ustar00rootroot00000000000000#!/bin/bash mega-exec confirm "$@" MEGAcmd-2.5.2_Linux/src/client/mega-confirmcancel000077500000000000000000000000511516543156300216060ustar00rootroot00000000000000#!/bin/bash mega-exec confirmcancel "$@" MEGAcmd-2.5.2_Linux/src/client/mega-cp000077500000000000000000000000361516543156300174100ustar00rootroot00000000000000#!/bin/bash mega-exec cp "$@" MEGAcmd-2.5.2_Linux/src/client/mega-debug000077500000000000000000000000411516543156300200700ustar00rootroot00000000000000#!/bin/bash mega-exec debug "$@" MEGAcmd-2.5.2_Linux/src/client/mega-deleteversions000077500000000000000000000000521516543156300220370ustar00rootroot00000000000000#!/bin/bash mega-exec deleteversions "$@" MEGAcmd-2.5.2_Linux/src/client/mega-df000077500000000000000000000000361516543156300173770ustar00rootroot00000000000000#!/bin/bash mega-exec df "$@" MEGAcmd-2.5.2_Linux/src/client/mega-du000077500000000000000000000000361516543156300174160ustar00rootroot00000000000000#!/bin/bash mega-exec du "$@" MEGAcmd-2.5.2_Linux/src/client/mega-errorcode000077500000000000000000000000451516543156300207720ustar00rootroot00000000000000#!/bin/bash mega-exec errorcode "$@" MEGAcmd-2.5.2_Linux/src/client/mega-exclude000077500000000000000000000000431516543156300204350ustar00rootroot00000000000000#!/bin/bash mega-exec exclude "$@" MEGAcmd-2.5.2_Linux/src/client/mega-export000077500000000000000000000000421516543156300203240ustar00rootroot00000000000000#!/bin/bash mega-exec export "$@" MEGAcmd-2.5.2_Linux/src/client/mega-find000077500000000000000000000000401516543156300177210ustar00rootroot00000000000000#!/bin/bash mega-exec find "$@" MEGAcmd-2.5.2_Linux/src/client/mega-ftp000077500000000000000000000000371516543156300176000ustar00rootroot00000000000000#!/bin/bash mega-exec ftp "$@" MEGAcmd-2.5.2_Linux/src/client/mega-fuse-add000077500000000000000000000000441516543156300204750ustar00rootroot00000000000000#!/bin/bash mega-exec fuse-add "$@" MEGAcmd-2.5.2_Linux/src/client/mega-fuse-config000077500000000000000000000000471516543156300212150ustar00rootroot00000000000000#!/bin/bash mega-exec fuse-config "$@" MEGAcmd-2.5.2_Linux/src/client/mega-fuse-disable000077500000000000000000000000501516543156300213450ustar00rootroot00000000000000#!/bin/bash mega-exec fuse-disable "$@" MEGAcmd-2.5.2_Linux/src/client/mega-fuse-enable000077500000000000000000000000471516543156300211760ustar00rootroot00000000000000#!/bin/bash mega-exec fuse-enable "$@" MEGAcmd-2.5.2_Linux/src/client/mega-fuse-remove000077500000000000000000000000471516543156300212450ustar00rootroot00000000000000#!/bin/bash mega-exec fuse-remove "$@" MEGAcmd-2.5.2_Linux/src/client/mega-fuse-show000077500000000000000000000000451516543156300207260ustar00rootroot00000000000000#!/bin/bash mega-exec fuse-show "$@" MEGAcmd-2.5.2_Linux/src/client/mega-get000077500000000000000000000000371516543156300175660ustar00rootroot00000000000000#!/bin/bash mega-exec get "$@" MEGAcmd-2.5.2_Linux/src/client/mega-graphics000077500000000000000000000000441516543156300206050ustar00rootroot00000000000000#!/bin/bash mega-exec graphics "$@" MEGAcmd-2.5.2_Linux/src/client/mega-help000077500000000000000000000000401516543156300177310ustar00rootroot00000000000000#!/bin/bash mega-exec help "$@" MEGAcmd-2.5.2_Linux/src/client/mega-https000077500000000000000000000000411516543156300201440ustar00rootroot00000000000000#!/bin/bash mega-exec https "$@" MEGAcmd-2.5.2_Linux/src/client/mega-import000077500000000000000000000000421516543156300203150ustar00rootroot00000000000000#!/bin/bash mega-exec import "$@" MEGAcmd-2.5.2_Linux/src/client/mega-invite000077500000000000000000000000421516543156300203010ustar00rootroot00000000000000#!/bin/bash mega-exec invite "$@" MEGAcmd-2.5.2_Linux/src/client/mega-ipc000077500000000000000000000000371516543156300175620ustar00rootroot00000000000000#!/bin/bash mega-exec ipc "$@" MEGAcmd-2.5.2_Linux/src/client/mega-killsession000077500000000000000000000000471516543156300213470ustar00rootroot00000000000000#!/bin/bash mega-exec killsession "$@" MEGAcmd-2.5.2_Linux/src/client/mega-lcd000077500000000000000000000000371516543156300175510ustar00rootroot00000000000000#!/bin/bash mega-exec lcd "$@" MEGAcmd-2.5.2_Linux/src/client/mega-log000077500000000000000000000000371516543156300175700ustar00rootroot00000000000000#!/bin/bash mega-exec log "$@" MEGAcmd-2.5.2_Linux/src/client/mega-login000077500000000000000000000000411516543156300201120ustar00rootroot00000000000000#!/bin/bash mega-exec login "$@" MEGAcmd-2.5.2_Linux/src/client/mega-logout000077500000000000000000000000421516543156300203140ustar00rootroot00000000000000#!/bin/bash mega-exec logout "$@" MEGAcmd-2.5.2_Linux/src/client/mega-lpwd000077500000000000000000000000401516543156300177470ustar00rootroot00000000000000#!/bin/bash mega-exec lpwd "$@" MEGAcmd-2.5.2_Linux/src/client/mega-ls000077500000000000000000000000361516543156300174240ustar00rootroot00000000000000#!/bin/bash mega-exec ls "$@" MEGAcmd-2.5.2_Linux/src/client/mega-mediainfo000077500000000000000000000000451516543156300207410ustar00rootroot00000000000000#!/bin/bash mega-exec mediainfo "$@" MEGAcmd-2.5.2_Linux/src/client/mega-mkdir000077500000000000000000000000411516543156300201100ustar00rootroot00000000000000#!/bin/bash mega-exec mkdir "$@" MEGAcmd-2.5.2_Linux/src/client/mega-mount000077500000000000000000000000411516543156300201440ustar00rootroot00000000000000#!/bin/bash mega-exec mount "$@" MEGAcmd-2.5.2_Linux/src/client/mega-mv000077500000000000000000000000361516543156300174300ustar00rootroot00000000000000#!/bin/bash mega-exec mv "$@" MEGAcmd-2.5.2_Linux/src/client/mega-passwd000077500000000000000000000000421516543156300203040ustar00rootroot00000000000000#!/bin/bash mega-exec passwd "$@" MEGAcmd-2.5.2_Linux/src/client/mega-permissions000077500000000000000000000000471516543156300213630ustar00rootroot00000000000000#!/bin/bash mega-exec permissions "$@" MEGAcmd-2.5.2_Linux/src/client/mega-preview000077500000000000000000000000431516543156300204650ustar00rootroot00000000000000#!/bin/bash mega-exec preview "$@" MEGAcmd-2.5.2_Linux/src/client/mega-proxy000077500000000000000000000000411516543156300201630ustar00rootroot00000000000000#!/bin/bash mega-exec proxy "$@" MEGAcmd-2.5.2_Linux/src/client/mega-put000077500000000000000000000000371516543156300176170ustar00rootroot00000000000000#!/bin/bash mega-exec put "$@" MEGAcmd-2.5.2_Linux/src/client/mega-pwd000077500000000000000000000000371516543156300176010ustar00rootroot00000000000000#!/bin/bash mega-exec pwd "$@" MEGAcmd-2.5.2_Linux/src/client/mega-quit000077500000000000000000000000401516543156300177630ustar00rootroot00000000000000#!/bin/bash mega-exec quit "$@" MEGAcmd-2.5.2_Linux/src/client/mega-reload000077500000000000000000000000421516543156300202510ustar00rootroot00000000000000#!/bin/bash mega-exec reload "$@" MEGAcmd-2.5.2_Linux/src/client/mega-rm000077500000000000000000000000361516543156300174240ustar00rootroot00000000000000#!/bin/bash mega-exec rm "$@" MEGAcmd-2.5.2_Linux/src/client/mega-session000077500000000000000000000000431516543156300204670ustar00rootroot00000000000000#!/bin/bash mega-exec session "$@" MEGAcmd-2.5.2_Linux/src/client/mega-share000077500000000000000000000000411516543156300201040ustar00rootroot00000000000000#!/bin/bash mega-exec share "$@" MEGAcmd-2.5.2_Linux/src/client/mega-showpcr000077500000000000000000000000431516543156300204710ustar00rootroot00000000000000#!/bin/bash mega-exec showpcr "$@" MEGAcmd-2.5.2_Linux/src/client/mega-signup000077500000000000000000000000421516543156300203100ustar00rootroot00000000000000#!/bin/bash mega-exec signup "$@" MEGAcmd-2.5.2_Linux/src/client/mega-speedlimit000077500000000000000000000000461516543156300211460ustar00rootroot00000000000000#!/bin/bash mega-exec speedlimit "$@" MEGAcmd-2.5.2_Linux/src/client/mega-sync000077500000000000000000000000401516543156300177550ustar00rootroot00000000000000#!/bin/bash mega-exec sync "$@" MEGAcmd-2.5.2_Linux/src/client/mega-sync-config000077500000000000000000000000471516543156300212270ustar00rootroot00000000000000#!/bin/bash mega-exec sync-config "$@" MEGAcmd-2.5.2_Linux/src/client/mega-sync-ignore000077500000000000000000000000471516543156300212450ustar00rootroot00000000000000#!/bin/bash mega-exec sync-ignore "$@" MEGAcmd-2.5.2_Linux/src/client/mega-sync-issues000077500000000000000000000000471516543156300212750ustar00rootroot00000000000000#!/bin/bash mega-exec sync-issues "$@" MEGAcmd-2.5.2_Linux/src/client/mega-thumbnail000077500000000000000000000000451516543156300207710ustar00rootroot00000000000000#!/bin/bash mega-exec thumbnail "$@" MEGAcmd-2.5.2_Linux/src/client/mega-transfers000077500000000000000000000000451516543156300210150ustar00rootroot00000000000000#!/bin/bash mega-exec transfers "$@" MEGAcmd-2.5.2_Linux/src/client/mega-tree000077500000000000000000000000401516543156300177400ustar00rootroot00000000000000#!/bin/bash mega-exec tree "$@" MEGAcmd-2.5.2_Linux/src/client/mega-userattr000077500000000000000000000000441516543156300206560ustar00rootroot00000000000000#!/bin/bash mega-exec userattr "$@" MEGAcmd-2.5.2_Linux/src/client/mega-users000077500000000000000000000000411516543156300201430ustar00rootroot00000000000000#!/bin/bash mega-exec users "$@" MEGAcmd-2.5.2_Linux/src/client/mega-version000077500000000000000000000000431516543156300204710ustar00rootroot00000000000000#!/bin/bash mega-exec version "$@" MEGAcmd-2.5.2_Linux/src/client/mega-webdav000077500000000000000000000000421516543156300202530ustar00rootroot00000000000000#!/bin/bash mega-exec webdav "$@" MEGAcmd-2.5.2_Linux/src/client/mega-whoami000077500000000000000000000000421516543156300202670ustar00rootroot00000000000000#!/bin/bash mega-exec whoami "$@" MEGAcmd-2.5.2_Linux/src/client/megacmd_client_main.cpp000066400000000000000000000013621516543156300226170ustar00rootroot00000000000000/** * @file src/client/megacmd_client_main.cpp * @brief MEGAcmd: Interactive CLI and service application * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of the MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include "megacmdclient.h" int main(int argc, char* argv[]) { #ifdef WIN32 megacmd::Instance windowsConsoleController; #endif return megacmd::executeClient(argc, argv, COUT, CERR); } MEGAcmd-2.5.2_Linux/src/client/megacmd_completion.sh000066400000000000000000000036751516543156300223470ustar00rootroot00000000000000 _megacmd() { local cur opts COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" if [[ ${cur} == '=' ]]; then cur="" fi COMP_WORDS[0]="${COMP_WORDS[0]/mega-/}" linetoexec="" lasta="" for a in "${COMP_WORDS[@]}"; do if [[ $a =~ ^.*([ \\]).*$ ]] && [[ $a != "\""* ]] && [[ $a != "'"* ]]; then lastcharina="${a: -1}" #add trailing space if ended in \ (it would have been strimmed) #and unescape symbols that dont need scaping between single quotes linetoexec=$linetoexec" '"$(echo $a | sed 's#\([^\\]\)\\$#\1\\ #g' | sed "s#\\\\\([ \<\>\|\`;:\"\!]\)#\1#g")"'" else if [[ ${a} == '=' ]] || [[ ${lasta} == '=' ]] || [[ ${a} == ':' ]] || [[ ${lasta} == ':' ]]; then linetoexec=$linetoexec$a else linetoexec=$linetoexec" "$a if [[ $a == "\""* ]] && [[ $a != *"\"" ]];then linetoexec=$linetoexec"\"" fi if [[ $a == "'"* ]] && [[ $a != *"'" ]];then linetoexec=$linetoexec"'" fi fi fi lasta=$a done if [[ "$linetoexec" == *" " ]] then linetoexec="$linetoexec\"\"" fi COMPREPLY="" opts="$(mega-exec completion ${linetoexec/#mega-/} 2>/dev/null)" if [ $? -ne 0 ]; then COMPREPLY="" return $? fi opts=$(echo "${opts/\`/\\\`}") opts=$(echo "${opts/\|/\\\|}") declare -a "aOPTS=(${opts})" || declare -a 'aOPTS=(${opts})' #escape characters that need to be scaped for a in `seq 0 $(( ${#aOPTS[@]} -1 ))`; do if [[ $lasta != "\""* ]] && [[ $lasta != "'"* ]]; then COMPREPLY[$a]=$( echo ${aOPTS[$a]} | sed "s#\([ \!;:\|\`\(\)\<\>\"\'\\]\)#\\\\\1#g") #OK else COMPREPLY[$a]="${aOPTS[$a]}" fi done for i in "${COMPREPLY[@]}"; do if [[ ${i} == --*= ]] || [[ ${i} == */ ]]; then hash compopt 2>/dev/null >/dev/null && compopt -o nospace fi done if [[ $opts == "MEGACMD_USE_LOCAL_COMPLETION" ]]; then COMPREPLY=() fi if [[ $opts == "" ]]; then COMPREPLY="" compopt -o nospace fi return 0 } for i in $(compgen -ca mega-); do IFS=" " complete -o default -F _megacmd $i done MEGAcmd-2.5.2_Linux/src/client/megacmdclient.cpp000066400000000000000000000657271516543156300214730ustar00rootroot00000000000000/** * @file src/client/megacmdclient.cpp * @brief MEGAcmdClient: Client application of MEGAcmd * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of the MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include "megacmdclient.h" #include "../megacmdcommonutils.h" #include "../megacmdshell/megacmdshellcommunications.h" #include "../megacmdshell/megacmdshellcommunicationsnamedpipes.h" #include #include #include #include #include #include #include #include #include #ifdef _WIN32 #include //PathAppend #include //CommandLineToArgvW #include #include #else #include #include #include #endif #ifdef _WIN32 #else #include // console size #endif #include #define PROGRESS_COMPLETE -2 #define SPROGRESS_COMPLETE "-2" #define SSTR( x ) static_cast< const std::ostringstream & >( \ ( std::ostringstream() << std::dec << x ) ).str() namespace megacmd { using namespace std; void printprogress(long long completed, long long total, const char *title = "TRANSFERRING"); #ifdef _WIN32 // convert UTF-8 to Windows Unicode void path2local(string* path, string* local) { // make space for the worst case local->resize((path->size() + 1) * sizeof(wchar_t)); int len = MultiByteToWideChar(CP_UTF8, 0, path->c_str(), -1, (wchar_t*)local->data(), int(local->size() / sizeof(wchar_t) + 1)); if (len) { // resize to actual result local->resize(sizeof(wchar_t) * (len - 1)); } else { local->clear(); } } // convert to Windows Unicode Utf8 void local2path(string* local, string* path) { path->resize((local->size() + 1) * 4 / sizeof(wchar_t)); path->resize(WideCharToMultiByte(CP_UTF8, 0, (wchar_t*)local->data(), int(local->size() / sizeof(wchar_t)), (char*)path->data(), int(path->size() + 1), NULL, NULL)); //normalize(path); } //TODO: delete the 2 former?? wstring getWAbsPath(wstring localpath) { if (!localpath.size()) { return localpath; } wstring utf8absolutepath; wstring absolutelocalpath; if (!PathIsRelativeW((LPCWSTR)localpath.data())) { if (localpath.find(L"\\\\?\\") != 0) { localpath.insert(0, L"\\\\?\\"); } return localpath; } int len = GetFullPathNameW((LPCWSTR)localpath.data(), 0, NULL, NULL); if (len <= 0) { return localpath; } absolutelocalpath.resize(len); int newlen = GetFullPathNameW((LPCWSTR)localpath.data(), len, (LPWSTR)absolutelocalpath.data(), NULL); if (newlen <= 0 || newlen >= len) { cerr << " failed to get CWD" << endl; return localpath; } absolutelocalpath.resize(newlen); if (absolutelocalpath.find(L"\\\\?\\") != 0) { absolutelocalpath.insert(0, L"\\\\?\\"); } return absolutelocalpath; } #endif string getAbsPath(string relativePath) { if (!relativePath.size()) { return relativePath; } #ifdef _WIN32 string utf8absolutepath; string localpath; path2local(&relativePath, &localpath); string absolutelocalpath; localpath.append("", 1); if (!PathIsRelativeW((LPCWSTR)localpath.data())) { utf8absolutepath = relativePath; if (utf8absolutepath.find("\\\\?\\") != 0) { utf8absolutepath.insert(0, "\\\\?\\"); } return utf8absolutepath; } int len = GetFullPathNameW((LPCWSTR)localpath.data(), 0, NULL, NULL); if (len <= 0) { return relativePath; } absolutelocalpath.resize(len * sizeof(wchar_t)); int newlen = GetFullPathNameW((LPCWSTR)localpath.data(), len, (LPWSTR)absolutelocalpath.data(), NULL); if (newlen <= 0 || newlen >= len) { cerr << " failed to get CWD" << endl; return relativePath; } absolutelocalpath.resize(newlen* sizeof(wchar_t)); local2path(&absolutelocalpath, &utf8absolutepath); if (utf8absolutepath.find("\\\\?\\") != 0) { utf8absolutepath.insert(0, "\\\\?\\"); } return utf8absolutepath; #else if (relativePath.size() && relativePath.at(0) == '/') { return relativePath; } else { char cCurrentPath[PATH_MAX]; if (!getcwd(cCurrentPath, sizeof(cCurrentPath))) { cerr << " failed to get CWD" << endl; return relativePath; } string absolutepath = cCurrentPath; absolutepath.append("/"); absolutepath.append(relativePath); return absolutepath; } return relativePath; #endif } string parseArgs(int argc, char* argv[], MegaCmdShellCommunications& comsManager) { vector absolutedargs; int totalRealArgs = 0; if (argc>1) { absolutedargs.push_back(argv[1]); if ( strcmp(argv[1],"quit") && strcmp(argv[1],"exit") ) { string clientWidth = "--client-width="; clientWidth+= SSTR(getNumberOfCols(80)); absolutedargs.push_back(clientWidth); } if (!strcmp(argv[1],"get") || !strcmp(argv[1],"put") || !strcmp(argv[1],"login") || !strcmp(argv[1],"reload") ) { auto clientIdOpt = comsManager.tryToGetClientId(); if (clientIdOpt) { absolutedargs.push_back("--clientID=" + *clientIdOpt); } } if (!strcmp(argv[1],"backup") || !strcmp(argv[1],"du") || !strcmp(argv[1],"mediainfo") || !strcmp(argv[1],"sync") || !strcmp(argv[1],"downloads") || !strcmp(argv[1],"transfers") ) { unsigned int width = getNumberOfCols(80); int pathSize = width/2; if ( !strcmp(argv[1],"downloads") ) { pathSize = int((width-46)/2); } else if ( !strcmp(argv[1],"transfers") ) { pathSize = int((width-46)/2); } else if ( !strcmp(argv[1],"du") ) { pathSize = int(width-13); for (int i = 1; i < argc; i++) { if (strstr(argv[i], "--versions")) { pathSize -= 11; break; } } } else if ( !strcmp(argv[1],"sync") ) { pathSize = int((width-46)/2); } else if ( !strcmp(argv[1],"mediainfo") ) { pathSize = int(width - 28); } else if ( !strcmp(argv[1],"backup") ) { pathSize = int((width-21)/2); } string spathSize = "--path-display-size="; spathSize+=SSTR(pathSize); absolutedargs.push_back(spathSize); } if (!strcmp(argv[1],"sync")) { for (int i = 2; i < argc; i++) { if (strlen(argv[i]) && argv[i][0] !='-' ) { totalRealArgs++; } } bool firstrealArg = true; for (int i = 2; i < argc; i++) { if (strlen(argv[i]) && argv[i][0] !='-' ) { if (totalRealArgs >=2 && firstrealArg) { absolutedargs.push_back(getAbsPath(argv[i])); firstrealArg=false; } else { absolutedargs.push_back(argv[i]); } } else { absolutedargs.push_back(argv[i]); } } } else if (!strcmp(argv[1],"lcd")) //localpath args { for (int i = 2; i < argc; i++) { if (strlen(argv[i]) && argv[i][0] !='-' ) { absolutedargs.push_back(getAbsPath(argv[i])); } else { absolutedargs.push_back(argv[i]); } } } else if (!strcmp(argv[1],"get") || !strcmp(argv[1],"preview") || !strcmp(argv[1],"thumbnail")) { for (int i = 2; i < argc; i++) { if (strlen(argv[i]) && argv[i][0] != '-' ) { totalRealArgs++; if (totalRealArgs>1) { absolutedargs.push_back(getAbsPath(argv[i])); } else { absolutedargs.push_back(argv[i]); } } else { absolutedargs.push_back(argv[i]); } } if (totalRealArgs == 1) { absolutedargs.push_back(getAbsPath(".")); } } else if (!strcmp(argv[1],"put")) { int lastRealArg = 0; for (int i = 2; i < argc; i++) { if (strlen(argv[i]) && argv[i][0] !='-' ) { lastRealArg = i; } } bool firstRealArg = true; for (int i = 2; i < argc; i++) { if (strlen(argv[i]) && argv[i][0] !='-') { if (firstRealArg || i absolutedargs; int totalRealArgs = 0; if (argc>1) { absolutedargs.push_back(argv[1]); if ( wcscmp(argv[1],L"quit") && wcscmp(argv[1],L"exit") ) { wstring clientWidth = L"--client-width="; string scw = SSTR(getNumberOfCols(80)); std::wstring wsclientWidth(scw.begin(), scw.end()); clientWidth+=wsclientWidth; absolutedargs.push_back(clientWidth); } if (!wcscmp(argv[1],L"get") || !wcscmp(argv[1],L"put") || !wcscmp(argv[1],L"login") || !wcscmp(argv[1],L"reload") ) { auto clientIdOpt = comsManager.tryToGetClientId(); if (clientIdOpt) { absolutedargs.push_back(L"--clientID=" + std::wstring(clientIdOpt->begin(), clientIdOpt->end())); } } if (!wcscmp(argv[1],L"backup") || !wcscmp(argv[1],L"du") || !wcscmp(argv[1],L"mediainfo") || !wcscmp(argv[1],L"sync") || !wcscmp(argv[1],L"downloads") || !wcscmp(argv[1],L"transfers") ) { unsigned int width = getNumberOfCols(80); int pathSize = width/2; if ( !wcscmp(argv[1],L"downloads") ) { pathSize = int((width-46)/2); } else if ( !wcscmp(argv[1],L"transfers") ) { pathSize = int((width-46)/2); } else if ( !wcscmp(argv[1],L"du") ) { pathSize = int(width-13); for (int i = 1; i < argc; i++) { if (wcsstr(argv[i], L"--versions")) { pathSize -= 11; break; } } } else if ( !wcscmp(argv[1],L"sync") ) { pathSize = int((width-46)/2); } else if ( !wcscmp(argv[1],L"mediainfo") ) { pathSize = int(width - 28); } else if ( !wcscmp(argv[1],L"backup") ) { pathSize = int((width-21)/2); } wstring spathSize = L"--path-display-size="; string sps = SSTR(pathSize); std::wstring wspathSize(sps.begin(), sps.end()); spathSize+=wspathSize; absolutedargs.push_back(spathSize); } if (!wcscmp(argv[1],L"sync")) { for (int i = 2; i < argc; i++) { if (wcslen(argv[i]) && argv[i][0] !='-' ) { totalRealArgs++; } } bool firstrealArg = true; for (int i = 2; i < argc; i++) { if (wcslen(argv[i]) && argv[i][0] !='-' ) { if (totalRealArgs >=2 && firstrealArg) { absolutedargs.push_back(getWAbsPath(argv[i])); firstrealArg=false; } else { absolutedargs.push_back(argv[i]); } } else { absolutedargs.push_back(argv[i]); } } } else if (!wcscmp(argv[1],L"lcd")) //localpath args { for (int i = 2; i < argc; i++) { if (wcslen(argv[i]) && argv[i][0] !='-' ) { absolutedargs.push_back(getWAbsPath(argv[i])); } else { absolutedargs.push_back(argv[i]); } } } else if (!wcscmp(argv[1],L"get") || !wcscmp(argv[1],L"preview") || !wcscmp(argv[1],L"thumbnail")) { for (int i = 2; i < argc; i++) { if (wcslen(argv[i]) && argv[i][0] != '-' ) { totalRealArgs++; if (totalRealArgs>1) { absolutedargs.push_back(getWAbsPath(argv[i])); } else { absolutedargs.push_back(argv[i]); } } else { absolutedargs.push_back(argv[i]); } } if (totalRealArgs == 1) { absolutedargs.push_back(getWAbsPath(L".")); } } else if (!wcscmp(argv[1],L"put")) { int lastRealArg = 0; for (int i = 2; i < argc; i++) { if (wcslen(argv[i]) && argv[i][0] !='-' ) { lastRealArg = i; } } bool firstRealArg = true; for (int i = 2; i < argc; i++) { if (wcslen(argv[i]) && argv[i][0] !='-') { if (firstRealArg || i 1 ) width--; static string lastMessage; while (nextstatedelimitpos!=string::npos && statestring.size()) { string newstate = statestring.substr(0,nextstatedelimitpos); statestring=statestring.substr(nextstatedelimitpos+1); nextstatedelimitpos = statestring.find(statedelim); if (newstate.compare(0, strlen("prompt:"), "prompt:") == 0) { // This is always received after server is first ready comsManager.markServerRegistrationFailed(); } else if (newstate.compare(0, strlen("endtransfer:"), "endtransfer:") == 0) { string rest = newstate.substr(strlen("endtransfer:")); if (rest.size() >=3) { bool isdown = rest.at(0) == 'D'; string path = rest.substr(2); stringstream os; if (shown_partial_progress) { os << endl; } os << (isdown?"Download":"Upload") << " finished: " << path << endl; #ifdef _WIN32 wstring wbuffer; stringtolocalw((const char*)os.str().data(),&wbuffer); WindowsUtf8StdoutGuard utf8Guard; OUTSTREAM << wbuffer << flush; #else StdoutMutexGuard stdoutGuard; OUTSTREAM << os.str(); #endif } } else if (newstate.compare(0, strlen("loged:"), "loged:") == 0) { // non interactive client doesn't need to wait for prompt if login finished comsManager.markServerReady(); } else if (newstate.compare(0, strlen("login:"), "login:") == 0) { StdoutMutexGuard stdoutGuard; printCenteredContentsCerr(string("Resuming session ... ").c_str(), width, false); } else if (newstate.compare(0, strlen("message:"), "message:") == 0) { if (lastMessage.compare(newstate)) //to avoid repeating messages { lastMessage = newstate; std::string_view messageContents = std::string_view(newstate).substr(strlen("message:")); string contents = std::string(shown_partial_progress ? "\n": "").append(messageContents); replaceAll(contents, "%mega-%", "mega-"); if (messageContents.rfind("-----", 0) != 0) { printCenteredContentsCerr(contents, width); } else { StdoutMutexGuard stdoutGuard; cerr << endl << contents << endl; } } } else if (newstate.compare(0, strlen("clientID:"), "clientID:") == 0) { std::string clientId = newstate.substr(strlen("clientID:")); comsManager.setClientIdPromise(clientId); } else if (newstate.compare(0, strlen("progress:"), "progress:") == 0) { string rest = newstate.substr(strlen("progress:")); size_t nexdel = rest.find(":"); string received = rest.substr(0,nexdel); rest = rest.substr(nexdel+1); nexdel = rest.find(":"); string total = rest.substr(0,nexdel); string title; if ( (nexdel != string::npos) && (nexdel < rest.size() ) ) { rest = rest.substr(nexdel+1); nexdel = rest.find(":"); title = rest.substr(0,nexdel); } if (received!=SPROGRESS_COMPLETE) { shown_partial_progress = true; } else { shown_partial_progress = false; } long long completed = received == SPROGRESS_COMPLETE ? PROGRESS_COMPLETE : charstoll(received.c_str()); const char * progressTitle = title.empty() ? "TRANSFERRING" : title.c_str(); printprogress(completed, charstoll(total.c_str()), progressTitle); } else if (newstate == "ack") { // do nothing, all good } else if (newstate == "restart") { // ignore restart command } else { //received unrecognized state change. sleep a while to avoid continuous looping sleepMilliSeconds(1000); } if (newstate.compare(0, strlen("progress:"), "progress:") != 0) { shown_partial_progress = false; } } } int executeClient(int argc, char* argv[], OUTSTREAMTYPE & outstream, OUTSTREAMTYPE &errorOutput) { #ifdef _WIN32 setlocale(LC_ALL, "en-US"); bool redirectedoutput = false; //Redirect stdout to a file for (int i=1;i comms(new MegaCmdShellCommunicationsNamedPipes(redirectedoutput)); #else std::unique_ptr comms(new MegaCmdShellCommunicationsPosix()); #endif string command = argv[1]; bool mayInitiateServer = command.compare(0,4,"exit") && command.compare(0,4,"quit") && command.compare(0,10,"completion"); bool registeredOk = comms->registerForStateChanges(false, statechangehandle, mayInitiateServer); if (!registeredOk) { return -2; } #if defined _WIN32 && !defined MEGACMD_TESTING_CODE int wargc; // This function (i.e., `executeClient`) should not deal with the command line arguments directly; that's above its responsability now. // For now we'll disable this call to `CommandLineToArgvW` in integration tests (the only other consumer of `executeClient` that's not main) because it was causing issues. // TODO: In the future we should refactor this to move the responsability away from `executeClient`. We should also avoid having two separate `parseArgs` functions. LPWSTR *szArglist = CommandLineToArgvW(GetCommandLineW(), &wargc); if (szArglist == NULL) { return -3; } wstring wParsedArgs = parsewArgs(wargc, szArglist, *comms); LocalFree(szArglist); #else string parsedArgs = parseArgs(argc, argv, *comms); #endif bool isInloginInValidCommands = false; if (argc > 1) { isInloginInValidCommands = std::find(loginInValidCommands.begin(), loginInValidCommands.end(), string(argv[1])) != loginInValidCommands.end(); } // If the command requires login, let's give it some time before executing it. // We will wait for server to signal readyness // Server readyness is marked by the arrival of prompt // or loging completes. Note: if the login takes larger than RESUME_SESSION_TIMEOUT we will continue and let the command fail if requires login if (!isInloginInValidCommands) { comms->waitForServerReadyOrRegistrationFailed(); } #if defined _WIN32 && !defined MEGACMD_TESTING_CODE int outcode = comms->executeCommandW(wParsedArgs, readresponse, outstream, errorOutput, false); #else int outcode = comms->executeCommand(parsedArgs, readresponse, outstream, errorOutput, false); #endif // do always return positive error codes (POSIX compliant) if (outcode < 0) { outcode = - outcode; } comms->shutdown(); return outcode; } } //end namespace MEGAcmd-2.5.2_Linux/src/client/megacmdclient.h000066400000000000000000000013131516543156300211150ustar00rootroot00000000000000/** * @file src/client/megacmdclient.h * @brief MEGAcmd: Interactive CLI and service application * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of the MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #pragma once #include "../megacmdcommonutils.h" namespace megacmd { int executeClient(int argc, char* argv[], OUTSTREAMTYPE &outstream, OUTSTREAMTYPE &errorOutput = CERR); } // end namespace MEGAcmd-2.5.2_Linux/src/client/python/000077500000000000000000000000001516543156300174735ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/src/client/python/mega-execports000077500000000000000000000050751516543156300223530ustar00rootroot00000000000000#!/usr/bin/python import socket, sys, struct, os localpathcommands=["lcd"] def parseArgs(argsin): itochange = None totalRealArgs = 0 realargs = [] if argsin[0] in ["sync"]: #get first argument (local path) and append fullpath for i in range(1,len(argsin)): if not argsin[i].startswith("-"): if (itochange is None): itochange = i; totalRealArgs+=1 if argsin[0] in ["sync"]: if (itochange is not None and totalRealArgs>=2): argsin[itochange]=os.path.abspath(argsin[itochange]) if argsin[0] in localpathcommands: #get first argument (local path) and append fullpath for i in range(1,len(argsin)): if not argsin[i].startswith("-"): argsin[i]=os.path.abspath(argsin[i]) if argsin[0] in ["get", "preview", "thumbnail"]: #get second argument (local path) and append fullpath for i in range(1,len(argsin)): if not argsin[i].startswith("-"): totalRealArgs+=1 if (totalRealArgs>1): argsin[i]=os.path.abspath(argsin[i]) if argsin[0] in ["put"]: for i in range(1,len(argsin)): if not argsin[i].startswith("-"): totalRealArgs+=1 realargs.append(argsin[i]) totalRealArgs=0 for i in range(1,len(argsin)): if not argsin[i].startswith("-"): totalRealArgs+=1 if ((totalRealArgs == 1) or (totalRealArgs < (len(realargs))) ): argsin[i]=os.path.abspath(argsin[i]) for i in range(1,len(argsin)): if " " in argsin[i]: argsin[i]='"'+argsin[i]+'"' for i in range(1,len(argsin)): if "" == argsin[i]: argsin[i]='"'+argsin[i]+'"' return argsin; sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: sock.connect(('127.0.0.1',12300)) commandArgs=parseArgs(sys.argv[1:]) if (sys.version_info[0] == 3): sock.send(" ".join(commandArgs).encode(sys.stdin.encoding)) else: sock.send(" ".join(commandArgs)) data = sock.recv(100); sockOutId, = struct.unpack('H', data[:2]) sock.close() sockout = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sockout.connect(('127.0.0.1',12300+sockOutId)) outCode, = struct.unpack('i', sockout.recv(4))#get out code data = sockout.recv(100); commandOutput="" while (len(data)): if (sys.version_info[0] == 3): commandOutput+=data.decode(sys.stdout.encoding) else: commandOutput+=data data = sockout.recv(100); sockout.close() if "" != commandOutput: sys.stdout.write(commandOutput) sys.stdout.flush() if (outCode <0): exit(-outCode) exit(outCode) except Exception as ex: sys.stderr.write("Unable to connect to service"+" "+str(ex)+"\n") sys.stderr.write("Please ensure MEGAcmd is running\n") exit(1) MEGAcmd-2.5.2_Linux/src/client/win/000077500000000000000000000000001516543156300167475ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/src/client/win/mega-attr.bat000066400000000000000000000000501516543156300213130ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" attr %* MEGAcmd-2.5.2_Linux/src/client/win/mega-backup.bat000077500000000000000000000000521516543156300216130ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" backup %* MEGAcmd-2.5.2_Linux/src/client/win/mega-cancel.bat000066400000000000000000000000521516543156300215700ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" cancel %* MEGAcmd-2.5.2_Linux/src/client/win/mega-cat.bat000066400000000000000000000000471516543156300211160ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" cat %* MEGAcmd-2.5.2_Linux/src/client/win/mega-cd.bat000066400000000000000000000000461516543156300207340ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" cd %* MEGAcmd-2.5.2_Linux/src/client/win/mega-configure.bat000066400000000000000000000000551516543156300223270ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" configure %* MEGAcmd-2.5.2_Linux/src/client/win/mega-confirm.bat000066400000000000000000000000531516543156300220010ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" confirm %* MEGAcmd-2.5.2_Linux/src/client/win/mega-confirmcancel.bat000066400000000000000000000000611516543156300231460ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" confirmcancel %* MEGAcmd-2.5.2_Linux/src/client/win/mega-cp.bat000066400000000000000000000000461516543156300207500ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" cp %* MEGAcmd-2.5.2_Linux/src/client/win/mega-debug.bat000066400000000000000000000000511516543156300214300ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" debug %* MEGAcmd-2.5.2_Linux/src/client/win/mega-deleteversions.bat000077500000000000000000000000621516543156300234020ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" deleteversions %* MEGAcmd-2.5.2_Linux/src/client/win/mega-df.bat000066400000000000000000000000461516543156300207370ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" df %* MEGAcmd-2.5.2_Linux/src/client/win/mega-du.bat000066400000000000000000000000461516543156300207560ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" du %* MEGAcmd-2.5.2_Linux/src/client/win/mega-errorcode.bat000066400000000000000000000000551516543156300223320ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" errorcode %* MEGAcmd-2.5.2_Linux/src/client/win/mega-exclude.bat000077500000000000000000000000531516543156300220000ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" exclude %* MEGAcmd-2.5.2_Linux/src/client/win/mega-export.bat000066400000000000000000000000521516543156300216640ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" export %* MEGAcmd-2.5.2_Linux/src/client/win/mega-find.bat000066400000000000000000000000501516543156300212610ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" find %* MEGAcmd-2.5.2_Linux/src/client/win/mega-ftp.bat000066400000000000000000000000471516543156300211400ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" ftp %* MEGAcmd-2.5.2_Linux/src/client/win/mega-fuse-add.bat000077500000000000000000000000541516543156300220400ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" fuse-add %* MEGAcmd-2.5.2_Linux/src/client/win/mega-fuse-config.bat000077500000000000000000000000571516543156300225600ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" fuse-config %* MEGAcmd-2.5.2_Linux/src/client/win/mega-fuse-disable.bat000077500000000000000000000000601516543156300227100ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" fuse-disable %* MEGAcmd-2.5.2_Linux/src/client/win/mega-fuse-enable.bat000077500000000000000000000000571516543156300225410ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" fuse-enable %* MEGAcmd-2.5.2_Linux/src/client/win/mega-fuse-remove.bat000077500000000000000000000000571516543156300226100ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" fuse-remove %* MEGAcmd-2.5.2_Linux/src/client/win/mega-fuse-show.bat000077500000000000000000000000551516543156300222710ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" fuse-show %* MEGAcmd-2.5.2_Linux/src/client/win/mega-get.bat000066400000000000000000000000471516543156300211260ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" get %* MEGAcmd-2.5.2_Linux/src/client/win/mega-graphics.bat000066400000000000000000000000541516543156300221450ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" graphics %* MEGAcmd-2.5.2_Linux/src/client/win/mega-help.bat000066400000000000000000000000501516543156300212710ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" help %* MEGAcmd-2.5.2_Linux/src/client/win/mega-https.bat000077500000000000000000000000511516543156300215070ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" https %* MEGAcmd-2.5.2_Linux/src/client/win/mega-import.bat000066400000000000000000000000521516543156300216550ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" import %* MEGAcmd-2.5.2_Linux/src/client/win/mega-invite.bat000066400000000000000000000000521516543156300216410ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" invite %* MEGAcmd-2.5.2_Linux/src/client/win/mega-ipc.bat000066400000000000000000000000471516543156300211220ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" ipc %* MEGAcmd-2.5.2_Linux/src/client/win/mega-killsession.bat000066400000000000000000000000571516543156300227070ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" killsession %* MEGAcmd-2.5.2_Linux/src/client/win/mega-lcd.bat000066400000000000000000000000471516543156300211110ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" lcd %* MEGAcmd-2.5.2_Linux/src/client/win/mega-log.bat000066400000000000000000000000471516543156300211300ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" log %* MEGAcmd-2.5.2_Linux/src/client/win/mega-login.bat000066400000000000000000000000511516543156300214520ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" login %* MEGAcmd-2.5.2_Linux/src/client/win/mega-logout.bat000066400000000000000000000000521516543156300216540ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" logout %* MEGAcmd-2.5.2_Linux/src/client/win/mega-lpwd.bat000066400000000000000000000000501516543156300213070ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" lpwd %* MEGAcmd-2.5.2_Linux/src/client/win/mega-ls.bat000077500000000000000000000000461516543156300207670ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" ls %* MEGAcmd-2.5.2_Linux/src/client/win/mega-mediainfo.bat000066400000000000000000000000551516543156300223010ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" mediainfo %* MEGAcmd-2.5.2_Linux/src/client/win/mega-mkdir.bat000066400000000000000000000000511516543156300214500ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" mkdir %* MEGAcmd-2.5.2_Linux/src/client/win/mega-mount.bat000066400000000000000000000000511516543156300215040ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" mount %* MEGAcmd-2.5.2_Linux/src/client/win/mega-mv.bat000066400000000000000000000000461516543156300207700ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" mv %* MEGAcmd-2.5.2_Linux/src/client/win/mega-passwd.bat000066400000000000000000000000521516543156300216440ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" passwd %* MEGAcmd-2.5.2_Linux/src/client/win/mega-preview.bat000066400000000000000000000000531516543156300220250ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" preview %* MEGAcmd-2.5.2_Linux/src/client/win/mega-proxy.bat000066400000000000000000000000511516543156300215230ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" proxy %* MEGAcmd-2.5.2_Linux/src/client/win/mega-put.bat000066400000000000000000000000471516543156300211570ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" put %* MEGAcmd-2.5.2_Linux/src/client/win/mega-pwd.bat000066400000000000000000000000471516543156300211410ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" pwd %* MEGAcmd-2.5.2_Linux/src/client/win/mega-quit.bat000066400000000000000000000000501516543156300213230ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" quit %* MEGAcmd-2.5.2_Linux/src/client/win/mega-reload.bat000066400000000000000000000000521516543156300216110ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" reload %* MEGAcmd-2.5.2_Linux/src/client/win/mega-rm.bat000066400000000000000000000000461516543156300207640ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" rm %* MEGAcmd-2.5.2_Linux/src/client/win/mega-session.bat000066400000000000000000000000531516543156300220270ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" session %* MEGAcmd-2.5.2_Linux/src/client/win/mega-share.bat000066400000000000000000000000511516543156300214440ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" share %* MEGAcmd-2.5.2_Linux/src/client/win/mega-showpcr.bat000066400000000000000000000000531516543156300220310ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" showpcr %* MEGAcmd-2.5.2_Linux/src/client/win/mega-signup.bat000066400000000000000000000000521516543156300216500ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" signup %* MEGAcmd-2.5.2_Linux/src/client/win/mega-speedlimit.bat000066400000000000000000000000561516543156300225060ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" speedlimit %* MEGAcmd-2.5.2_Linux/src/client/win/mega-sync-config.bat000077500000000000000000000000571516543156300225720ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" sync-config %* MEGAcmd-2.5.2_Linux/src/client/win/mega-sync-ignore.bat000077500000000000000000000000571516543156300226100ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" sync-ignore %* MEGAcmd-2.5.2_Linux/src/client/win/mega-sync-issues.bat000077500000000000000000000000571516543156300226400ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" sync-issues %* MEGAcmd-2.5.2_Linux/src/client/win/mega-sync.bat000077500000000000000000000000501516543156300213200ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" sync %* MEGAcmd-2.5.2_Linux/src/client/win/mega-thumbnail.bat000066400000000000000000000000551516543156300223310ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" thumbnail %* MEGAcmd-2.5.2_Linux/src/client/win/mega-transfers.bat000077500000000000000000000000551516543156300223600ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" transfers %* MEGAcmd-2.5.2_Linux/src/client/win/mega-tree.bat000066400000000000000000000000501516543156300213000ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" tree %* MEGAcmd-2.5.2_Linux/src/client/win/mega-userattr.bat000066400000000000000000000000541516543156300222160ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" userattr %* MEGAcmd-2.5.2_Linux/src/client/win/mega-users.bat000066400000000000000000000000511516543156300215030ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" users %* MEGAcmd-2.5.2_Linux/src/client/win/mega-version.bat000066400000000000000000000000531516543156300220310ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" version %* MEGAcmd-2.5.2_Linux/src/client/win/mega-webdav.bat000077500000000000000000000000521516543156300216160ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" webdav %* MEGAcmd-2.5.2_Linux/src/client/win/mega-whoami.bat000066400000000000000000000000521516543156300216270ustar00rootroot00000000000000@echo off "%~dp0MegaClient.exe" whoami %* MEGAcmd-2.5.2_Linux/src/comunicationsmanager.cpp000066400000000000000000000121251516543156300216070ustar00rootroot00000000000000/** * @file src/comunicationsmanager.cpp * @brief MEGAcmd: Communications manager non supporting non-interactive mode * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of the MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include "comunicationsmanager.h" #include using namespace mega; namespace megacmd { ComunicationsManager::ComunicationsManager() { } bool ComunicationsManager::receivedPetition() { return false; } CmdPetition* ComunicationsManager::registerStateListener(std::unique_ptr &&inf) { std::lock_guard g(mStateListenersMutex); // Always remove closed listeners before a new one comes in ackStateListenersAndRemoveClosed(); if (stateListenersPetitions.size() >= getMaxStateListeners()) { LOG_warn << "Max number of state listeners (" << stateListenersPetitions.size() << ") reached"; return nullptr; } stateListenersPetitions.emplace_back(std::move(inf)); return stateListenersPetitions.back().get(); } int ComunicationsManager::waitForPetition() { return 0; } void ComunicationsManager::stopWaiting() { } int ComunicationsManager::get_next_comm_id() { return 0; } int ComunicationsManager::getMaxStateListeners() const { return 200; // default value } void ComunicationsManager::ackStateListenersAndRemoveClosed() { informStateListeners("ack"); } bool ComunicationsManager::informStateListeners(const string &s) { std::lock_guard g(mStateListenersMutex); for (auto it = stateListenersPetitions.begin(); it != stateListenersPetitions.end();) { if (informStateListener(it->get(), s + (char) 0x1F) < 0) { it = stateListenersPetitions.erase(it); continue; } ++it; } return !stateListenersPetitions.empty(); } void ComunicationsManager::informStateListenerByClientId(const string &s, int clientID) { std::lock_guard g(mStateListenersMutex); for (auto it = stateListenersPetitions.begin(); it != stateListenersPetitions.end(); ++it) { if (clientID == (*it)->clientID) { if (informStateListener(it->get(), s + (char) 0x1F) < 0) { stateListenersPetitions.erase(it); } return; } } } int ComunicationsManager::informStateListener(CmdPetition *inf, const string &s) { return 0; } void ComunicationsManager::returnAndClosePetition(std::unique_ptr inf, OUTSTRINGSTREAM *s, int outCode) { } /** * @brief getPetition * @return pointer to new CmdPetition. Petition returned must be properly deleted (this can be calling returnAndClosePetition) */ std::unique_ptr ComunicationsManager::getPetition() { return std::make_unique(); } int ComunicationsManager::getConfirmation(CmdPetition *inf, string message) { return MCMDCONFIRM_NO; } std::string ComunicationsManager::getUserResponse(CmdPetition *inf, string message) { return string(); } void CmdPetition::setLine(std::string_view line) { mLine = line; } std::string_view CmdPetition::getLine() const { return mLine; } std::string_view CmdPetition::getUniformLine() const { return ltrim(std::string_view(mLine), 'X'); } std::string CmdPetition::getRedactedLine() const { const char* doNotRedactEnv = getenv("MEGACMD_DO_NOT_REDACT_LINES"); if (doNotRedactEnv) { return mLine; } static const std::string redacted = "$1"; static const std::string asterisks = "$1********"; static const std::regex fullCommandRegex(R"(^((X?)(passwd|login|confirm|confirmcancel)\s+).*$)"); static const std::regex passwordRegex(R"((--password=)("[^"]+"|'[^']+'|\S+))"); static const std::regex authRegex(R"((--auth-(code|key)=)\S+)"); static const std::regex linkRegex(R"((https://mega\.nz/(file|folder)/[^#]+#)\S+)"); static const std::regex oldLinkRegex(R"((https://mega\.nz/#F?![^!]+#)\S+)"); static const std::regex encryptedLinkRegex(R"((https://mega\.nz/#P!)\S+)"); if (std::regex_match(mLine, fullCommandRegex)) { return std::regex_replace(mLine, fullCommandRegex, redacted); } std::string output = mLine; output = std::regex_replace(output, passwordRegex, asterisks); output = std::regex_replace(output, authRegex, asterisks); output = std::regex_replace(output, linkRegex, asterisks); output = std::regex_replace(output, oldLinkRegex, asterisks); output = std::regex_replace(output, encryptedLinkRegex, asterisks); return output; } bool CmdPetition::isFromCmdShell() const { return startsWith(mLine, "X"); } MegaThread *CmdPetition::getPetitionThread() const { return petitionThread; } void CmdPetition::setPetitionThread(MegaThread *value) { petitionThread = value; } }//end namespace MEGAcmd-2.5.2_Linux/src/comunicationsmanager.h000066400000000000000000000071671516543156300212660ustar00rootroot00000000000000/** * @file src/comunicationsmanager.h * @brief MEGAcmd: Communications manager non supporting non-interactive mode * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of the MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #ifndef COMUNICATIONSMANAGER_H #define COMUNICATIONSMANAGER_H #include "megacmd.h" #include "megacmdcommonutils.h" namespace megacmd { class CmdPetition { std::string mLine; public: mega::MegaThread *petitionThread = nullptr; int clientID = -27; bool clientDisconnected = false; virtual ~CmdPetition() = default; void setLine(std::string_view line); std::string_view getLine() const; // Remove the starting 'X' if present (petitions coming from interactive mode) std::string_view getUniformLine() const; // Remove possible confidential info from the line std::string getRedactedLine() const; bool isFromCmdShell() const; mega::MegaThread *getPetitionThread() const; void setPetitionThread(mega::MegaThread *value); virtual std::string getPetitionDetails() const { return {}; } }; class ComunicationsManager { private: fd_set fds; std::recursive_mutex mStateListenersMutex; std::vector> stateListenersPetitions; public: ComunicationsManager(); virtual ~ComunicationsManager() = default; virtual bool receivedPetition(); virtual CmdPetition *registerStateListener(std::unique_ptr &&inf); virtual int waitForPetition(); virtual void stopWaiting(); virtual int get_next_comm_id(); virtual int getMaxStateListeners() const; void ackStateListenersAndRemoveClosed(); /** * @brief returnAndClosePetition * It will clean struct and close the socket within */ virtual void returnAndClosePetition(std::unique_ptr inf, OUTSTRINGSTREAM *s, int); virtual void sendPartialOutput(CmdPetition *inf, OUTSTRING *s) = 0; virtual void sendPartialOutput(CmdPetition *inf, char *s, size_t size, bool binaryContents = false) = 0; virtual void sendPartialError(CmdPetition *inf, OUTSTRING *s) = 0; virtual void sendPartialError(CmdPetition *inf, char *s, size_t size, bool binaryContents = false) = 0; /** * @brief Sends an status message (e.g. prompt:who@/new/prompt:) to all registered listeners * @param s * @returns if state listeners left */ bool informStateListeners(const std::string &s); void informStateListenerByClientId(const std::string &s, int clientID); /** * @brief informStateListener * @param inf This contains the petition that originated the register. It should contain the implementation details that identify a listener * (e.g. In a socket implementation, the socket identifier) * @param s * @return -1 if connection closed by listener (removal required) */ virtual int informStateListener(CmdPetition *inf, const std::string &s); /** * @brief getPetition * @return pointer to new CmdPetition. Petition returned must be properly deleted (this can be calling returnAndClosePetition) */ virtual std::unique_ptr getPetition(); virtual int getConfirmation(CmdPetition *inf, std::string message); virtual std::string getUserResponse(CmdPetition *inf, std::string message); }; } //end namespace #endif // COMUNICATIONSMANAGER_H MEGAcmd-2.5.2_Linux/src/comunicationsmanagerfilesockets.cpp000066400000000000000000000376361516543156300240610ustar00rootroot00000000000000/** * @file src/comunicationsmanagerfilesockets.cpp * @brief MEGAcmd: Communications manager using Network Sockets * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of the MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include "megacmdcommonutils.h" #ifndef WIN32 #include "comunicationsmanagerfilesockets.h" #include "megacmdutils.h" #include #include #include #ifdef __MACH__ #define MSG_NOSIGNAL 0 #endif #ifndef SOCKET_ERROR #define SOCKET_ERROR -1 #endif using namespace mega; namespace megacmd { ComunicationsManagerFileSockets::ComunicationsManagerFileSockets() { count = 0; mHasPetition = false; initialize(); } int ComunicationsManagerFileSockets::initialize() { auto socketPath = getOrCreateSocketPath(true); struct sockaddr_un addr; if (socketPath.empty()) { LOG_fatal << "Could not create runtime directory for socket file: " << strerror(errno); } if (socketPath.size() >= (ARRAYSIZE(addr.sun_path) - 1)) { LOG_fatal << "Server socket path is too long: '" << socketPath << "'. Exceeds " << ARRAYSIZE(addr.sun_path) - 1 << " max allowance"; return -1; } sockfd = socket(AF_UNIX, SOCK_STREAM, 0); int errno_saved = errno; if (sockfd < 0) { LOG_fatal << "ERROR opening socket"; return errno_saved; } if (fcntl(sockfd, F_SETFD, FD_CLOEXEC) == -1) { LOG_err << "ERROR setting CLOEXEC to socket: " << errno; } socklen_t saddrlen = sizeof( addr ); memset(&addr, 0, sizeof( addr )); addr.sun_family = AF_UNIX; strncpy(addr.sun_path, socketPath.c_str(), socketPath.size()); unlink(addr.sun_path); LOG_debug << "Binding socket to path " << socketPath; if (::bind(sockfd, (struct sockaddr *)&addr, saddrlen)) { if (errno == EADDRINUSE) { LOG_warn << "ERROR on binding socket: " << socketPath << ": Already in use."; } else { LOG_fatal << "ERROR on binding socket: " << socketPath << ": " << strerror(errno); } close(sockfd); sockfd = -1; } else { LOG_debug << "Listening for commands at socket path " << socketPath; int returned = listen(sockfd, 150); if (returned) { LOG_fatal << "ERROR on listen socket initializing communications manager: " << socketPath << ": " << strerror(errno); close(sockfd); sockfd = -1; return errno; } } return 0; } bool ComunicationsManagerFileSockets::receivedPetition() { return mHasPetition; } int ComunicationsManagerFileSockets::waitForPetition() { if (sockfd < 0 || sockfd >= FD_SETSIZE) { LOG_fatal << "Invalid socket descriptor: " << sockfd; mHasPetition = false; return EBADF; } fd_set fds; FD_ZERO(&fds); FD_SET(sockfd, &fds); int rc = select(FD_SETSIZE, &fds, NULL, NULL, NULL); if (rc < 0) { if (errno != EINTR) //syscall { LOG_fatal << "Error at select: " << errno; mHasPetition = false; return errno; } } mHasPetition.store(FD_ISSET(sockfd, &fds) != 0); return 0; } void ComunicationsManagerFileSockets::stopWaiting() { #ifdef _WIN32 shutdown(sockfd,SD_BOTH); #else LOG_verbose << "Shutting down main socket "; if (shutdown(sockfd,SHUT_RDWR) == -1) { //shutdown failed. we need to send something to the blocked thread so as to wake up from select int clientsocket = socket(AF_UNIX, SOCK_STREAM, 0); auto socketPath = getOrCreateSocketPath(false); LOG_info << "listening at " << socketPath; if (clientsocket < 0 ) { LOG_err << "ERROR opening client socket to exit select: " << errno; close (sockfd); } else { if (fcntl(clientsocket, F_SETFD, FD_CLOEXEC) == -1) { LOG_err << "ERROR setting CLOEXEC to socket: " << errno; } struct sockaddr_un addr; if (socketPath.size() >= (ARRAYSIZE(addr.sun_path) - 1)) { LOG_fatal << "Server socket path is too long: '" << socketPath << "'"; return; } memset(&addr, 0, sizeof( addr )); addr.sun_family = AF_UNIX; strncpy(addr.sun_path, socketPath.c_str(), socketPath.size()); if (::connect(clientsocket, (struct sockaddr*)&addr, sizeof( addr )) != -1) { if (send(clientsocket,"no matter",1,MSG_NOSIGNAL) == -1) { LOG_err << "ERROR sending via client socket to exit select: " << errno; close (sockfd); } close(clientsocket); } else { LOG_err << "ERROR connecting client socket to exit select: " << errno; close (sockfd); } } } LOG_verbose << "Main socket shut down"; #endif } CmdPetition* ComunicationsManagerFileSockets::registerStateListener(std::unique_ptr &&inf) { const int socket = ((CmdPetitionPosixSockets*) inf.get())->outSocket; LOG_debug << "Registering state listener petition with socket: " << socket; #ifndef NDEBUG // let's not be gentle with state listener sockets to prevent frozen clients from stopping the server const timeval timeout { .tv_sec = 10, .tv_usec = 0}; if (setsockopt(socket, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)) < 0) { LOG_err << "ERROR setting state listener socket timeout: " << errno; } #endif return ComunicationsManager::registerStateListener(std::move(inf)); } int ComunicationsManagerFileSockets::getMaxStateListeners() const { static int maxListenersAllowed = [this](){ struct rlimit limit; if (getrlimit(RLIMIT_NOFILE, &limit) != 0) { LOG_err << "Failed to get ulimit -n (errno: " << errno << "); falling back to max state listeners default"; return ComunicationsManager::getMaxStateListeners(); } int systemNumFilesLimit = static_cast(limit.rlim_cur); int maxListeners = systemNumFilesLimit - std::max(100, static_cast(systemNumFilesLimit * 0.20)); // leave 20% or 100 file descriptors for libraries and other fds: maxListeners = std::min(maxListeners, static_cast(FD_SETSIZE * 0.4)); // we don't want to use fd with numbers > 1024: select will not digest them well: lets play a safe 60% margin. return std::max(2/*minimum requirement*/, maxListeners); // maxListeners may be negative based on above calculations (unexpected). Let's play our chances of survival despite that. }(); return maxListenersAllowed; } /** * @brief returnAndClosePetition * I will clean struct and close the socket within */ void ComunicationsManagerFileSockets::returnAndClosePetition(std::unique_ptr inf, OUTSTRINGSTREAM *s, int outCode) { const int socket = ((CmdPetitionPosixSockets *) inf.get())->outSocket; assert(socket != -1); LOG_verbose << "Output to write in socket " << socket; if (socket == -1) { LOG_fatal << "Return and close: not valid outsocket " << socket; return; } string sout = s->str(); auto n = send(socket, &outCode, sizeof(outCode), MSG_NOSIGNAL); if (n < 0) { LOG_err << "ERROR writing output Code to socket: " << errno; } if (sout.empty()) { return; // socket shutdown will send EOF (0 bytes upon recv) } n = send(socket, sout.data(), sout.size(), MSG_NOSIGNAL); if (n < 0) { LOG_err << "ERROR writing to socket: " << errno; } } void ComunicationsManagerFileSockets::sendPartialOutput(CmdPetition *inf, OUTSTRING *s) { sendPartialOutputImpl(inf, s->data(), s->size(), false/*never binary*/, false/*not error*/); } void ComunicationsManagerFileSockets::sendPartialOutput(CmdPetition *inf, char *s, size_t size, bool binaryContents) { return sendPartialOutputImpl(inf, s, size, binaryContents, false); } void ComunicationsManagerFileSockets::sendPartialError(CmdPetition *inf, OUTSTRING *s) { sendPartialOutputImpl(inf, s->data(), s->size(), false/*never binary*/, true/*error*/); } void ComunicationsManagerFileSockets::sendPartialError(CmdPetition *inf, char *s, size_t size, bool binaryContents) { return sendPartialOutputImpl(inf, s, size, binaryContents, true); } void ComunicationsManagerFileSockets::sendPartialOutputImpl(CmdPetition *inf, char *s, size_t size, bool binaryContents, bool sendAsError) { if (inf->clientDisconnected) { return; } int connectedsocket = ((CmdPetitionPosixSockets *)inf)->outSocket; assert(connectedsocket != -1); if (connectedsocket == -1) { std::cerr << "Return and close: no valid outsocket " << ((CmdPetitionPosixSockets *)inf)->outSocket << endl; return; } if (!binaryContents && !isValidUtf8(s, size)) { std::cerr << "Attempt to sendPartialOutput of invalid utf8 of size " << size << std::endl; ASSERT_UTF8_BREAK("Attempt to sendPartialOutput of invalid utf8"); return; } if (size) { int outCode = sendAsError ? MCMD_PARTIALERR : MCMD_PARTIALOUT; auto n = send(connectedsocket, (void*)&outCode, sizeof( outCode ), MSG_NOSIGNAL); if (n < 0) { std::cerr << "ERROR writing MCMD_PARTIALOUT/MCMD_PARTIALERR to socket: " << errno << endl; if (errno == EPIPE) { std::cerr << "WARNING: Client disconnected, the rest of the output will be discarded" << endl; inf->clientDisconnected = true; } return; } n = send(connectedsocket, (void*)&size, sizeof( size ), MSG_NOSIGNAL); if (n < 0) { std::cerr << "ERROR writing size of partial output to socket: " << errno << endl; return; } n = send(connectedsocket, s, size, MSG_NOSIGNAL); // for some reason without the max recv never quits in the client for empty responses if (n < 0) { std::cerr << "ERROR writing to socket partial output: " << errno << endl; return; } } } int ComunicationsManagerFileSockets::informStateListener(CmdPetition *inf, const std::string &s) { if (!isValidUtf8(s)) { LOG_err << "Attempt to write an invalid utf-8 string of size " << s.size(); ASSERT_UTF8_BREAK("Attempt to write an invalid utf-8 string"); return 0; } std::lock_guard g(informerMutex); LOG_verbose << "Inform State Listener: Output to write in socket " << ((CmdPetitionPosixSockets *)inf)->outSocket << ": <<" << s << ">>"; int connectedsocket = ((CmdPetitionPosixSockets *)inf)->outSocket; assert(connectedsocket != -1); if (connectedsocket == -1) { LOG_err << "Informing state listener: Not valid on outsocket " << ((CmdPetitionPosixSockets *)inf)->outSocket; return 0; } #ifdef __MACH__ #define MSG_NOSIGNAL 0 #endif auto n = send(connectedsocket, s.data(), s.size(), MSG_NOSIGNAL); if (n < 0) { if (errno == EPIPE) //socket closed { LOG_verbose << "Unregistering no longer listening client. Original petition: " << inf->getRedactedLine(); return -1; } else if (errno == EAGAIN || errno == EWOULDBLOCK) // timed out { LOG_warn << "Unregistering timed out listening client. Original petition: " << inf->getRedactedLine(); return -1; } else { LOG_err << "ERROR writing to socket: " << errno; } } return 0; } /** * @brief getPetition * @return pointer to new CmdPetitionPosix. Petition returned must be properly deleted (this can be calling returnAndClosePetition) */ std::unique_ptr ComunicationsManagerFileSockets::getPetition() { auto inf = std::make_unique(); static socklen_t clilen = sizeof(cli_addr); int newsockfd = accept(sockfd, (struct sockaddr*) &cli_addr, &clilen); if (newsockfd < 0) { if (errno == EMFILE) { LOG_fatal << "ERROR on accept at getPetition: TOO many open files."; ackStateListenersAndRemoveClosed(); } else { LOG_fatal << "ERROR on accept at getPetition: " << errno; } sleep(1); inf->setLine("ERROR"); return inf; } if (fcntl(newsockfd, F_SETFD, FD_CLOEXEC) == -1) { LOG_err << "ERROR setting CLOEXEC to socket: " << errno; } string wholepetition; int n = read(newsockfd, buffer, 1023); while(n == 1023) { unsigned long int total_available_bytes; if (-1 == ioctl(newsockfd, FIONREAD, &total_available_bytes)) { LOG_err << "Failed to PeekNamedPipe. errno: " << errno; break; } if (total_available_bytes == 0) { break; } buffer[n] = '\0'; wholepetition.append(buffer); n = read(newsockfd, buffer, 1023); } if (n >=0 ) { buffer[n] = '\0'; wholepetition.append(buffer); } if (n < 0) { LOG_fatal << "ERROR reading from socket at getPetition: " << errno; inf->setLine("ERROR"); close(newsockfd); return inf; } inf->outSocket = newsockfd; inf->setLine(wholepetition); return inf; } int ComunicationsManagerFileSockets::getConfirmation(CmdPetition *inf, string message) { int connectedsocket = ((CmdPetitionPosixSockets *)inf)->outSocket; assert(connectedsocket != -1); if (connectedsocket == -1) { LOG_fatal << "Getting Confirmation: invalid outsocket " << ((CmdPetitionPosixSockets *)inf)->outSocket; return false; } int outCode = MCMD_REQCONFIRM; auto n = send(connectedsocket, (void*)&outCode, sizeof( outCode ), MSG_NOSIGNAL); if (n < 0) { LOG_err << "ERROR writing output Code to socket: " << errno; } n = send(connectedsocket, message.data(), max(1,(int)message.size()), MSG_NOSIGNAL); // for some reason without the max recv never quits in the client for empty responses if (n < 0) { LOG_err << "ERROR writing to socket: " << errno; } int response; n = recv(connectedsocket,&response, sizeof(response), MSG_NOSIGNAL); return response; } string ComunicationsManagerFileSockets::getUserResponse(CmdPetition *inf, string message) { int connectedsocket = ((CmdPetitionPosixSockets *)inf)->outSocket; assert(connectedsocket != -1); if (connectedsocket == -1) { LOG_fatal << "Getting Confirmation: Invalid outsocket " << ((CmdPetitionPosixSockets *)inf)->outSocket; return "FAILED"; } int outCode = MCMD_REQSTRING; auto n = send(connectedsocket, (void*)&outCode, sizeof( outCode ), MSG_NOSIGNAL); if (n < 0) { LOG_err << "ERROR writing output Code to socket: " << errno; } n = send(connectedsocket, message.data(), max(static_cast(1), message.size()), MSG_NOSIGNAL); // for some reason without the max recv never quits in the client for empty responses if (n < 0) { LOG_err << "ERROR writing to socket: " << errno; } string response; int BUFFERSIZE = 1024; char buffer[1025]; do{ n = recv(connectedsocket, buffer, BUFFERSIZE, MSG_NOSIGNAL); if (n) { buffer[n] = '\0'; response += buffer; } } while(n == BUFFERSIZE && n != SOCKET_ERROR); return response; } ComunicationsManagerFileSockets::~ComunicationsManagerFileSockets() { } }//end namespace #endif MEGAcmd-2.5.2_Linux/src/comunicationsmanagerfilesockets.h000066400000000000000000000057411516543156300235160ustar00rootroot00000000000000/** * @file src/comunicationsmanagerfilesockets.h * @brief MEGAcmd: Communications manager using Network Sockets * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of the MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #ifndef COMUNICATIONSMANAGERFILESOCKETS_H #define COMUNICATIONSMANAGERFILESOCKETS_H #ifndef WIN32 #include "comunicationsmanager.h" #include #include #include namespace megacmd { struct CmdPetitionPosixSockets: public CmdPetition { int outSocket = -1; virtual ~CmdPetitionPosixSockets() { shutdown(outSocket, SHUT_RDWR); close(outSocket); } std::string getPetitionDetails() const override { return "socket output: " + std::to_string(outSocket); } }; OUTSTREAMTYPE &operator<<(OUTSTREAMTYPE &os, CmdPetitionPosixSockets &p); class ComunicationsManagerFileSockets : public ComunicationsManager { private: std::atomic mHasPetition; // sockets and asociated variables int sockfd; char buffer[1024]; struct sockaddr_in serv_addr, cli_addr; // to get next socket id int count; std::mutex mtx; std::mutex informerMutex; void sendPartialOutputImpl(CmdPetition *inf, char *s, size_t size, bool binaryContents, bool sendAsError); public: ComunicationsManagerFileSockets(); int initialize(); bool receivedPetition(); int waitForPetition() override; virtual void stopWaiting(); CmdPetition* registerStateListener(std::unique_ptr &&inf) override; int getMaxStateListeners() const override; /** * @brief returnAndClosePetition * I will clean struct and close the socket within */ void returnAndClosePetition(std::unique_ptr inf, OUTSTRINGSTREAM *s, int) override; void sendPartialOutput(CmdPetition *inf, OUTSTRING *s) override; void sendPartialOutput(CmdPetition *inf, char *s, size_t size, bool binaryContents = false) override; void sendPartialError(CmdPetition *inf, OUTSTRING *s) override; void sendPartialError(CmdPetition *inf, char *s, size_t size, bool binaryContents = false) override; int informStateListener(CmdPetition *inf, const std::string &s) override; /** * @brief getPetition * @return pointer to new CmdPetitionPosix. Petition returned must be properly deleted (this can be calling returnAndClosePetition) */ std::unique_ptr getPetition() override; virtual int getConfirmation(CmdPetition *inf, std::string message); virtual std::string getUserResponse(CmdPetition *inf, std::string message); ~ComunicationsManagerFileSockets(); }; }//end namespace #endif #endif // COMUNICATIONSMANAGERPOSIX_H MEGAcmd-2.5.2_Linux/src/comunicationsmanagernamedpipes.cpp000066400000000000000000000416001516543156300236550ustar00rootroot00000000000000/** * @file src/comunicationsmanagernamedPipes.cpp * @brief MegaCMD: Communications manager using Network NamedPipes * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of the MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #ifdef _WIN32 #include "comunicationsmanagernamedpipes.h" #include "megacmdutils.h" #include "megacmdcommonutils.h" #include #include #include //getusername using std::wstring; #define ERRNO WSAGetLastError() using namespace mega; namespace megacmd { bool namedPipeValid(HANDLE namedPipe) { return namedPipe != INVALID_HANDLE_VALUE; } bool ComunicationsManagerNamedPipes::ended; int ComunicationsManagerNamedPipes::get_next_comm_id() { mtx->lock(); ++count; mtx->unlock(); return count; } HANDLE ComunicationsManagerNamedPipes::doCreatePipe(wstring nameOfPipe) { return CreateNamedPipeW( nameOfPipe.c_str(), // name of the pipe /*PIPE_ACCESS_DUPLEX | FILE_FLAG_WRITE_THROUGH, // 2-way pipe, synchronous PIPE_TYPE_BYTE, // send data as a byte stream */ PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE, // this treats the pipe as if FILE_FLAG_WRITE_THROUGH anyhow 1, // only allow 1 instance of this pipe 0, // no outbound buffer 0, // no inbound buffer 0, // use default wait time NULL // use default security attributes ); } HANDLE ComunicationsManagerNamedPipes::create_new_namedPipe(int *pipeId) { *pipeId = get_next_comm_id(); HANDLE thepipe = INVALID_HANDLE_VALUE; wstring nameOfPipe; int attempts = 10; bool namedPipesucceded = false; while (--attempts && !namedPipesucceded) { nameOfPipe += getNamedPipeName(); if (*pipeId) { #ifdef __MINGW32__ wostringstream wos; wos << *pipeId; nameOfPipe += wos.str(); #else nameOfPipe += std::to_wstring(*pipeId); #endif } // Create a pipe to send data thepipe = doCreatePipe(nameOfPipe); if (!namedPipeValid(thepipe)) { if (errno == EMFILE) //TODO: review possible out { LOG_verbose << " Trying to reduce number of used files by sending ACK to listeners to discard disconnected ones."; ackStateListenersAndRemoveClosed(); } if (attempts !=10) { LOG_fatal << "ERROR opening namedPipe ID=" << pipeId << " errno: " << ERRNO << ". Attempts: " << attempts; } sleepMilliSeconds(500); } else { namedPipesucceded = true; } } if (!namedPipeValid(thepipe)) { return INVALID_HANDLE_VALUE; } return thepipe; } ComunicationsManagerNamedPipes::ComunicationsManagerNamedPipes() { count = 0; mtx = new std::mutex(); informerMutex = new std::mutex(); initialize(); } int ComunicationsManagerNamedPipes::initialize() { petitionready = false; wstring nameOfPipe = getNamedPipeName(); pipeGeneral = doCreatePipe(nameOfPipe); if (!namedPipeValid(pipeGeneral)) { if (ERRNO == ERROR_ACCESS_DENIED) { LOG_fatal << "ERROR initiating communications. Couldn't create NamedPipe: Access denied. Ensure there is no other instance running."; } else { LOG_fatal << "ERROR initiating communications. Couldn't create NamedPipe: " << ERRNO; } Sleep(6000); exit(1); return -1; } return 0; } bool ComunicationsManagerNamedPipes::receivedPetition() { return petitionready; } int ComunicationsManagerNamedPipes::waitForPetition() { petitionready = false; if (!ConnectNamedPipe(pipeGeneral, NULL)) { if (ERRNO == ERROR_PIPE_CONNECTED) { LOG_debug << "Client arrived first when connecting to pipeGeneral."; } else { LOG_fatal << "ERROR on connecting to namedPipe. errno: " << ERRNO; sleepMilliSeconds(1000); pipeGeneral = INVALID_HANDLE_VALUE; return ERRNO; } } petitionready = true; return 0; } void ComunicationsManagerNamedPipes::stopWaiting() { wstring nameOfPipe = getNamedPipeName(); DeleteFile(nameOfPipe.c_str()); // without this, CloseHandle will hang, and loop will be stuck in ConnectNamedPipe if (pipeGeneral != INVALID_HANDLE_VALUE) { CloseHandle(pipeGeneral); } } CmdPetition* ComunicationsManagerNamedPipes::registerStateListener(std::unique_ptr &&inf) { LOG_debug << "Registering state listener petition with namedPipe: " << ((CmdPetitionNamedPipes*) inf.get())->outNamedPipe; return ComunicationsManager::registerStateListener(std::move(inf)); } //TODO: implement unregisterStateListener, not 100% necesary, since when a state listener is not accessible it is unregistered (to deal with sudden deaths). // also, unregistering might not be straight forward since we need to correlate the thread doing the unregistration with the one who registered. /** * @brief returnAndClosePetition * I will clean struct and close the namedPipe within */ void ComunicationsManagerNamedPipes::returnAndClosePetition(std::unique_ptr inf, OUTSTRINGSTREAM *s, int outCode) { HANDLE outNamedPipe = ((CmdPetitionNamedPipes*) inf.get())->outNamedPipe; LOG_verbose << "Output to write in namedPipe " << *(long*)(&outNamedPipe); bool connectsucceeded = false; int attempts = 10; while (--attempts && !connectsucceeded) { if (!ConnectNamedPipe(outNamedPipe, NULL)) { if (ERRNO == ERROR_PIPE_CONNECTED) { LOG_verbose << "Client arrived first when connecting to namedPipe " << outNamedPipe; connectsucceeded = true; break; } else if (ERRNO == ERROR_NO_DATA) { break; } else { // Problem: this, caused by a client shutdown, entails trying to sendPartialOutput again (via MEGAcmdLogger::formatLogToStream) LOG_err << "ERROR on connecting to namedPipe " << outNamedPipe << ". errno: " << ERRNO << ". Attempts: " << attempts; assert(false); } sleepMilliSeconds(50); } else { connectsucceeded = true; } } if (!connectsucceeded) { LOG_err << "Return and close: Unable to connect on outnamedPipe " << outNamedPipe << " error: " << ERRNO; return; } OUTSTRING sout = s->str(); DWORD n; if (!WriteFile(outNamedPipe,(const char*)&outCode, sizeof(outCode), &n, NULL)) { LOG_err << "ERROR writing output Code to namedPipe: " << ERRNO; } string sutf8; localwtostring(&sout,&sutf8); if (!WriteFile(outNamedPipe,sutf8.data(), max(1,(int)sutf8.size()), &n, NULL)) // client does not like empty responses { LOG_err << "ERROR writing to namedPipe: " << ERRNO; } DisconnectNamedPipe(outNamedPipe); } void ComunicationsManagerNamedPipes::sendPartialOutput(CmdPetition *inf, OUTSTRING *s) { string sutf8; localwtostring(s, &sutf8); return sendPartialOutputImpl(inf, sutf8.data(), sutf8.size(), false/*never binary*/, false/*not error*/); } void ComunicationsManagerNamedPipes::sendPartialOutput(CmdPetition *inf, char *s, size_t size, bool binaryContents) { return sendPartialOutputImpl(inf, s, size, binaryContents, false); } void ComunicationsManagerNamedPipes::sendPartialError(CmdPetition *inf, OUTSTRING *s) { string sutf8; localwtostring(s, &sutf8); return sendPartialOutputImpl(inf, sutf8.data(), sutf8.size(), false/*never binary*/, true/*error*/); } void ComunicationsManagerNamedPipes::sendPartialError(CmdPetition *inf, char *s, size_t size, bool binaryContents) { return sendPartialOutputImpl(inf, s, size, binaryContents, true); } void ComunicationsManagerNamedPipes::sendPartialOutputImpl(CmdPetition *inf, char *s, size_t size, bool binaryContents, bool sendAsError) { if (inf->clientDisconnected) { return; } HANDLE outNamedPipe = ((CmdPetitionNamedPipes *)inf)->outNamedPipe; if (!binaryContents &&!isValidUtf8(s, size)) { std::cerr << "Attempt to sendPartialOutput of invalid utf8 of size " << size << std::endl; ASSERT_UTF8_BREAK("Attempt to sendPartialOutput of invalid utf8"); return; } bool connectsucceeded = false; int attempts = 10; while (--attempts && !connectsucceeded) { if (!ConnectNamedPipe(outNamedPipe, NULL)) { if (ERRNO == ERROR_PIPE_CONNECTED) { //cerr << "Client arrived first when connecting to namedPipe " << outNamedPipe << endl; connectsucceeded = true; break; } else if (ERRNO == ERROR_NO_DATA) { break; } else { cerr << "ERROR on connecting to namedPipe " << outNamedPipe << ". errno: " << ERRNO << ". Attempts: " << attempts << endl; assert(false); } sleepMilliSeconds(50); } else { connectsucceeded = true; } } if (!connectsucceeded) { cerr << "sendPartialOutput: Unable to connect on outnamedPipe " << outNamedPipe << " error: " << ERRNO << endl; if (ERRNO == ERROR_NO_DATA) { std::cerr << "WARNING: Client disconnected, the rest of the output will be discarded" << endl; inf->clientDisconnected = true; } return; } int outCode = sendAsError ? MCMD_PARTIALERR : MCMD_PARTIALOUT; DWORD n; if (!WriteFile(outNamedPipe,(const char*)&outCode, sizeof(outCode), &n, NULL)) { std::cerr << "ERROR writing output Code to namedPipe: " << ERRNO << std::endl; if (ERRNO == ERROR_NO_DATA) { std::cerr << "WARNING: Client disconnected, the rest of the output will be discarded" << std::endl; inf->clientDisconnected = true; } return; } size_t thesize = size > 1 ? size : 1; // client does not like empty responses if (!WriteFile(outNamedPipe,(const char*)&thesize, sizeof(thesize), &n, NULL)) { std::cerr << "ERROR writing output Code to namedPipe: " << ERRNO << std::endl; return; } if (!WriteFile(outNamedPipe,s, DWORD(thesize), &n, NULL)) { std::cerr << "ERROR writing to namedPipe: " << ERRNO << std::endl; } } int ComunicationsManagerNamedPipes::informStateListener(CmdPetition *inf, const string &s) { if (!isValidUtf8(s)) { LOG_err << "Attempt to write an invalid utf8 string of size " << s.size(); ASSERT_UTF8_BREAK("Attempt to write an invalid utf8 string"); return 0; } std::lock_guard g(*informerMutex); LOG_verbose << "Inform State Listener: Output to write in namedPipe " << ((CmdPetitionNamedPipes *)inf)->outNamedPipe << ": <<" << s << ">>"; HANDLE outNamedPipe = ((CmdPetitionNamedPipes *)inf)->outNamedPipe; if (!ConnectNamedPipe(outNamedPipe, NULL)) { if (ERRNO == ERROR_PIPE_CONNECTED) { LOG_debug << "Client arrived first when connecting to namedPipe to inform state. " << outNamedPipe; } else if(ERRNO == ERROR_NO_DATA) { LOG_debug << "Client probably disconnected: " << outNamedPipe; return -1; } else { LOG_fatal << "ERROR on connecting to namedPipe " << outNamedPipe << ". errno: " << ERRNO; return -1; } } DWORD n; if (!WriteFile(outNamedPipe, s.data(), DWORD(s.size()), &n, NULL)) { if (ERRNO == 32 || ERRNO == 109 || (ERRNO == 232 && s == "ack")) //namedPipe closed | pipe has been ended { LOG_debug << "namedPipe closed. Client probably disconnected. Original petition: " << inf->getRedactedLine(); return -1; } else { LOG_err << "ERROR writing to namedPipe to inform state: " << ERRNO; } } return 0; } /** * @brief getPetition * @return pointer to new CmdPetitionPosix. Petition returned must be properly deleted (this can be calling returnAndClosePetition) */ std::unique_ptr ComunicationsManagerNamedPipes::getPetition() { auto inf = std::make_unique(); wstring wread; wchar_t wbuffer[1024]= {}; DWORD n; //ZeroMemory( wbuffer, sizeof(wbuffer)); bool readok = ReadFile(pipeGeneral, wbuffer, 1023*sizeof(wchar_t), &n, NULL ); while(readok && n == 1023*sizeof(wchar_t)) { DWORD total_available_bytes; if (FALSE == PeekNamedPipe(pipeGeneral,0,0,0,&total_available_bytes,0)) { LOG_err << "Failed to PeekNamedPipe. errno: L" << ERRNO; break; } if (total_available_bytes == 0) { break; } wbuffer[n/sizeof(wchar_t)]=0; wread.append(wbuffer); readok = ReadFile(pipeGeneral, wbuffer, 1023*sizeof(wchar_t), &n, NULL ); } if (readok) { wbuffer[n/sizeof(wchar_t)]=0; wread.append(wbuffer); } if (!readok) { LOG_err << "Failed to read petition from named pipe. errno: L" << ERRNO; inf->setLine("ERROR"); return inf; } string receivedutf8; localwtostring(&wread,&receivedutf8); int namedPipe_id = 0; // this value shouldn't matter inf->outNamedPipe = create_new_namedPipe(&namedPipe_id); if (!namedPipeValid(inf->outNamedPipe) || !namedPipe_id) { LOG_fatal << "ERROR creating output namedPipe at getPetition"; inf->setLine("ERROR"); return inf; } if(!WriteFile(pipeGeneral,(const char*)&namedPipe_id, sizeof( namedPipe_id ), &n, NULL)) { LOG_fatal << "ERROR writing to namedPipe at getPetition: ERRNO = " << ERRNO; inf->setLine("ERROR"); return inf; } if (!DisconnectNamedPipe(pipeGeneral) ) { LOG_fatal << " Error disconnecting from general pip. errno: " << ERRNO; } inf->setLine(receivedutf8); return inf; } int ComunicationsManagerNamedPipes::getConfirmation(CmdPetition *inf, string message) { HANDLE outNamedPipe = ((CmdPetitionNamedPipes *)inf)->outNamedPipe; int outCode = MCMD_REQCONFIRM; DWORD n; if (!WriteFile(outNamedPipe, (const char *)&outCode, sizeof( outCode ), &n, NULL)) { LOG_err << "ERROR writing output Code to namedPipe: " << ERRNO; } if (!WriteFile(outNamedPipe, message.data(), max(1,(int)message.size()), &n, NULL) ) { LOG_err << "ERROR writing to namedPipe: " << ERRNO; } int response = MCMDCONFIRM_NO; if (!ReadFile(outNamedPipe,(char *)&response, sizeof(response), &n, NULL)) { LOG_err << "ERROR receiving confirmation response: " << ERRNO; } return response; } string ComunicationsManagerNamedPipes::getUserResponse(CmdPetition *inf, string message) { HANDLE outNamedPipe = ((CmdPetitionNamedPipes *)inf)->outNamedPipe; int outCode = MCMD_REQSTRING; DWORD n; if (!WriteFile(outNamedPipe, (const char *)&outCode, sizeof( outCode ), &n, NULL)) { LOG_err << "ERROR writing output Code to namedPipe: " << ERRNO; } if (!WriteFile(outNamedPipe, message.data(), max(1,(int)message.size()), &n, NULL) ) { LOG_err << "ERROR writing to namedPipe: " << ERRNO; } wstring wread; wchar_t wbuffer[1024]= {}; //ZeroMemory( wbuffer, sizeof(wbuffer)); bool readok = ReadFile(outNamedPipe, wbuffer, 1023*sizeof(wchar_t), &n, NULL ); while(readok && n == 1023*sizeof(wchar_t)) { DWORD total_available_bytes; if (FALSE == PeekNamedPipe(outNamedPipe,0,0,0,&total_available_bytes,0)) { LOG_err << "Failed to PeekNamedPipe. errno: L" << ERRNO; break; } if (total_available_bytes == 0) { break; } wbuffer[n/sizeof(wchar_t)]=0; wread.append(wbuffer); readok = ReadFile(outNamedPipe, wbuffer, 1023*sizeof(wchar_t), &n, NULL ); } if (readok) { wbuffer[n/sizeof(wchar_t)]=0; wread.append(wbuffer); } if (!readok) { LOG_err << "Failed to read user response from named pipe. errno: L" << ERRNO; return "FAILED"; } string receivedutf8; localwtostring(&wread,&receivedutf8); return receivedutf8; } ComunicationsManagerNamedPipes::~ComunicationsManagerNamedPipes() { delete mtx; delete informerMutex; } }//end namespace #endif MEGAcmd-2.5.2_Linux/src/comunicationsmanagernamedpipes.h000066400000000000000000000064461516543156300233330ustar00rootroot00000000000000/** * @file src/comunicationsmanagerportnamedPipes.h * @brief MegaCMD: Communications manager using Network NamedPipes * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of the MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #ifndef COMUNICATIONSMANAGERNAMEDPIPES_H #define COMUNICATIONSMANAGERNAMEDPIPES_H #include "comunicationsmanager.h" #include #include #include #define MEGACMDINITIALPORTNUMBER 12300 namespace megacmd { struct CmdPetitionNamedPipes: public CmdPetition { HANDLE outNamedPipe; virtual ~CmdPetitionNamedPipes() { if (outNamedPipe != INVALID_HANDLE_VALUE) { CloseHandle(outNamedPipe); } } std::string getPetitionDetails() const override { std::ostringstream details; details << "namedPipe output: " << outNamedPipe; return details.str(); } }; OUTSTREAMTYPE &operator<<(OUTSTREAMTYPE &os, CmdPetitionNamedPipes &p); class ComunicationsManagerNamedPipes : public ComunicationsManager { private: bool petitionready; // namedPipes and asociated variables HANDLE pipeGeneral; static bool ended; char buffer[1024]; // to get next namedPipe id int count; std::mutex *mtx; std::mutex *informerMutex; /** * @brief create_new_namedPipe * The caller is responsible for deleting the newly created namedPipe * @return */ HANDLE create_new_namedPipe(int *pipeId); void sendPartialOutputImpl(CmdPetition *inf, char *s, size_t size, bool binaryContents, bool sendAsError); public: ComunicationsManagerNamedPipes(); int initialize(); bool receivedPetition(); int waitForPetition() override; virtual void stopWaiting(); int get_next_comm_id(); CmdPetition* registerStateListener(std::unique_ptr &&inf) override; /** * @brief returnAndClosePetition * I will clean struct and close the namedPipe within */ void returnAndClosePetition(std::unique_ptr inf, OUTSTRINGSTREAM *s, int) override; void sendPartialOutput(CmdPetition *inf, OUTSTRING *s) override; void sendPartialOutput(CmdPetition *inf, char *s, size_t size, bool binaryContents = false) override; void sendPartialError(CmdPetition *inf, OUTSTRING *s) override; void sendPartialError(CmdPetition *inf, char *s, size_t size, bool binaryContents = false) override; int informStateListener(CmdPetition *inf, const std::string &s) override; /** * @brief getPetition * @return pointer to new CmdPetitionPosix. Petition returned must be properly deleted (this can be calling returnAndClosePetition) */ std::unique_ptr getPetition() override; virtual int getConfirmation(CmdPetition *inf, std::string message); virtual std::string getUserResponse(CmdPetition *inf, std::string message); ~ComunicationsManagerNamedPipes(); HANDLE doCreatePipe(std::wstring nameOfPipe); }; }//end namespace #endif // COMUNICATIONSMANAGERPOSIX_H MEGAcmd-2.5.2_Linux/src/configurationmanager.cpp000066400000000000000000000726721516543156300216200ustar00rootroot00000000000000/** * @file src/configurationmanager.cpp * @brief MEGAcmd: configuration manager * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of the MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include "megacmdcommonutils.h" #include "configurationmanager.h" #include "megacmdversion.h" #include "megacmdutils.h" #include "listeners.h" #include "updater/Preferences.h" #include "sync_ignore.h" #include #ifndef ERRNO #ifdef _WIN32 #include #define ERRNO WSAGetLastError() #else #define ERRNO errno #endif #endif #ifdef _WIN32 #include //SHGetFolderPath #include //PathAppend #include #else #include //getpwuid_r #include //flock #endif #ifdef _WIN32 #define PATH_MAX_LOCAL_BACKUP MAX_PATH #else #define PATH_MAX_LOCAL_BACKUP PATH_MAX #endif using namespace mega; using namespace std; namespace megacmd { static const char* const LOCK_FILE_NAME = "lockMCMD"; #ifdef WIN32 HANDLE ConfigurationManager::mLockFileHandle; #elif defined(LOCK_EX) && defined(LOCK_NB) int ConfigurationManager::fd; #endif map ConfigurationManager::oldConfiguredSyncs; string ConfigurationManager::session; map ConfigurationManager::configuredBackups; std::recursive_mutex ConfigurationManager::settingsMutex; fs::path ConfigurationManager::getConfigFolder() { if (mConfigFolder.empty()) { ConfigurationManager::loadConfigDir(); } return mConfigFolder; } bool ConfigurationManager::getHasBeenUpdated() { return hasBeenUpdated; } static const char* const persistentmcmdconfigurationkeys[] = { "autoupdate", "updaterregistered" }; void ConfigurationManager::createFolderIfNotExisting(const fs::path &folder) { std::error_code ec; auto createdOk = fs::create_directory(folder, ec); bool alreadyExisting = !createdOk && !ec; if (alreadyExisting) { return; } LOG_debug << "Creating directory " << folder; if (!createdOk) { LOG_err << "Directory " << folder << " creation failed: " << errorCodeStr(ec); return; } #ifndef WIN32 fs::permissions(folder, fs::perms::owner_all, fs::perm_options::replace, ec); if (ec) { LOG_warn << "Failed to set permissions on new folder " << folder << ": " << errorCodeStr(ec); } #endif } void ConfigurationManager::loadConfigDir() { auto dirs = PlatformDirectories::getPlatformSpecificDirectories(); std::lock_guard g(settingsMutex); mConfigFolder = dirs->configDirPath(); if (mConfigFolder.empty()) { LOG_fatal << "Could not get config directory path"; return; } createFolderIfNotExisting(mConfigFolder); } fs::path ConfigurationManager::getAndCreateConfigDir() { auto dirs = PlatformDirectories::getPlatformSpecificDirectories(); const fs::path configDir = dirs->configDirPath(); if (configDir.empty()) { LOG_fatal << "Could not get config directory path"; throw std::runtime_error("Could not get config directory path"); } createFolderIfNotExisting(configDir); return configDir; } fs::path ConfigurationManager::getAndCreateRuntimeDir() { auto dirs = PlatformDirectories::getPlatformSpecificDirectories(); const fs::path runtimeDir = dirs->runtimeDirPath(); if (runtimeDir.empty()) { LOG_fatal << "Could not get runtime directory path"; throw std::runtime_error("Could not get runtime directory path"); } createFolderIfNotExisting(runtimeDir); return runtimeDir; } fs::path ConfigurationManager::getConfigFolderSubdir(const fs::path& subdirName) { auto dirs = PlatformDirectories::getPlatformSpecificDirectories(); const fs::path configDir = dirs->configDirPath(); assert(!configDir.empty()); fs::path configSubDir = configDir / subdirName; createFolderIfNotExisting(configSubDir); return configSubDir; } void ConfigurationManager::saveSession(const char*session) { std::lock_guard g(settingsMutex); auto configDir = getAndCreateConfigDir(); assert(!configDir.empty()); const fs::path sessionFilePath = configDir / "session"; LOG_debug << "Session file: " << sessionFilePath; ofstream fo(sessionFilePath, ios::out); if (fo.is_open()) { if (session) { fo << session; } fo.close(); } } string ConfigurationManager::saveProperty(const char *property, const char *value) { std::lock_guard g(settingsMutex); if (mConfigFolder.empty()) { loadConfigDir(); } assert(!mConfigFolder.empty()); bool found = false; const fs::path configFilePath = mConfigFolder / "megacmd.cfg"; stringstream formerlines; ifstream infile(configFilePath); string line, prevValue; while (getline(infile, line)) { if (line.length() > 0 && line[0] != '#') { string key; size_t pos = line.find("="); if (pos != string::npos) { key = line.substr(0, pos); rtrimProperty(key, ' '); if (!strcmp(key.c_str(), property) && !found) { formerlines << property << "=" << value << endl; prevValue = line.substr(pos + 1); found = true; } else { formerlines << line << endl; } } else { formerlines << line << endl; } } } ofstream fo(configFilePath); if (fo.is_open()) { fo << formerlines.str(); if (!found) { fo << property << "=" << value << endl; } fo.close(); } return prevValue; } void ConfigurationManager::migrateSyncConfig(MegaApi *api) { bool informed = false; std::map> listeners; { std::lock_guard g(settingsMutex); for (auto itr = oldConfiguredSyncs.begin(); itr != oldConfiguredSyncs.end(); itr++) { if (!informed) { LOG_debug << "copying sync config"; informed = true; } sync_struct *thesync = ((sync_struct*)( *itr ).second ); auto newListenerPair = listeners.emplace(thesync, std::make_unique(api)); api->copySyncDataToCache(thesync->localpath.c_str(), thesync->handle, nullptr, thesync->fingerprint, thesync->active, false, newListenerPair.first->second.get()); } } for (auto &lPair : listeners) { auto &thesync = lPair.first; auto &listener = lPair.second; listener->wait(); auto e = listener->getError(); if (e && e->getErrorCode() == API_OK) { removeSyncConfig(thesync); } else { LOG_err << " fail to copy old sync cache data: " << thesync->localpath; } } } void ConfigurationManager::removeSyncConfig(sync_struct *syncToRemove) { std::lock_guard g(settingsMutex); for (map::iterator itr = oldConfiguredSyncs.begin(); itr != oldConfiguredSyncs.end(); itr++) { sync_struct *thesync = ((sync_struct*)( *itr ).second ); if (thesync == syncToRemove) { oldConfiguredSyncs.erase(itr); delete thesync; ConfigurationManager::saveSyncs(&ConfigurationManager::oldConfiguredSyncs); return; } } } void ConfigurationManager::saveSyncs(map *syncsmap) { std::lock_guard g(settingsMutex); if (mConfigFolder.empty()) { loadConfigDir(); } if (!mConfigFolder.empty()) { const fs::path syncsFilePath = mConfigFolder / "syncs"; LOG_debug << "Syncs file: " << syncsFilePath; ofstream fo(syncsFilePath, ios::out | ios::binary); if (fo.is_open()) { map::iterator itr; int i = 0; if (syncsmap) { for (itr = syncsmap->begin(); itr != syncsmap->end(); ++itr, i++) { sync_struct *thesync = ((sync_struct*)( *itr ).second ); long long controlvalue = CONFIGURATIONSTOREDBYVERSION; fo.write((char*)&controlvalue, sizeof( long long )); int versionmcmd = MEGACMD_CODE_VERSION; fo.write((char*)&versionmcmd, sizeof( int )); fo.write((char*)&thesync->fingerprint, sizeof( long long )); fo.write((char*)&thesync->handle, sizeof( MegaHandle )); const char * localPath = thesync->localpath.c_str(); size_t lengthLocalPath = thesync->localpath.size(); fo.write((char*)&lengthLocalPath, sizeof( size_t )); fo.write((char*)localPath, sizeof( char ) * lengthLocalPath); } } fo.close(); } } else { LOG_err << "Couldnt access configuration folder "; } } void ConfigurationManager::saveBackups(map *backupsmap) { std::lock_guard g(settingsMutex); if (mConfigFolder.empty()) { loadConfigDir(); } if (!mConfigFolder.empty()) { const fs::path backupsFilePath = mConfigFolder / "backups"; LOG_debug << "Backups file: " << backupsFilePath; ofstream fo(backupsFilePath, ios::out | ios::binary); if (fo.is_open()) { map::iterator itr; int i = 0; if (backupsmap) { for (itr = backupsmap->begin(); itr != backupsmap->end(); ++itr, i++) { backup_struct *thebackup = ((backup_struct*)( *itr ).second ); int versionmcmd = MEGACMD_CODE_VERSION; fo.write((char*)&versionmcmd, sizeof( int )); fo.write((char*)&thebackup->handle, sizeof( MegaHandle )); const char * localPath = thebackup->localpath.c_str(); size_t lengthLocalPath = thebackup->localpath.size(); fo.write((char*)&lengthLocalPath, sizeof( size_t )); fo.write((char*)localPath, sizeof( char ) * lengthLocalPath); fo.write((char*)&thebackup->numBackups, sizeof( int )); fo.write((char*)&thebackup->period, sizeof( int64_t )); const char * speriod = thebackup->speriod.c_str(); size_t lengthLocalPeriod = thebackup->speriod.size(); fo.write((char*)&lengthLocalPeriod, sizeof( size_t )); fo.write((char*)speriod, sizeof( char ) * lengthLocalPeriod); } } fo.close(); } } else { LOG_err << "Couldnt access configuration folder "; } } void ConfigurationManager::transitionLegacyExclusionRules(MegaApi& api) { // Note that using `MegaApi::exportLegacyExclusionRules` here won't simplify much. // Since we still need to manually load all legacy rules into a vector, call `setLegacyExcludedNames`, // then call `exportLegacyExclusionRules` to generate a .megaignore file, and finally move it manually // to our location and add the .default prefix. // Since we need to have all the custom mega ignore functionality for the `mega-ignore` command anyway, // and since we also need to manually read the legacy file, it's just easier to rely on our custom // method to generate the default file. const fs::path defaultMegaIgnorePath = MegaIgnoreFile::getDefaultPath(); if (fs::exists(defaultMegaIgnorePath)) { LOG_debug << "Default .megaignore file already exists. Skipping legacy transition"; return; } const fs::path excludeFilePath = mConfigFolder / "excluded"; const fs::path hiddenExcludeFilePath = mConfigFolder / ".excluded"; if (!fs::exists(excludeFilePath)) { LOG_debug << "Missing legacy exclude file. Skipping transition"; return; } LOG_debug << "Transitioning legacy exclusion rules from " << excludeFilePath << " to " << defaultMegaIgnorePath; std::ifstream excludeFile(excludeFilePath); if (!excludeFile.is_open() || excludeFile.fail()) { LOG_err << "There was an error opening legacy exclude file " << excludeFilePath; return; } MegaIgnoreFile megaIgnoreFile(defaultMegaIgnorePath); if (!megaIgnoreFile.isValid()) { LOG_err << "There was an error opening default .megaignore file " << defaultMegaIgnorePath; return; } sendEvent(StatsManager::MegacmdEvent::TRANSITIONING_PRE_SRW_EXCLUSIONS, &api, false); std::set excludeFilters; std::vector excludePatterns; for (std::string line; getline(excludeFile, line);) { if (line.empty()) { continue; } excludePatterns.push_back(line); string filter = SyncIgnore::getFilterFromLegacyPattern(line); if (!MegaIgnoreFile::isValidFilter(filter)) { LOG_warn << "Found invalid pattern \"" << line << "\" in legacy exclude file"; continue; } excludeFilters.insert(filter); } // This ensures transition works for active syncs api.setLegacyExcludedNames(&excludePatterns); // This ensures transition works in case there are no existing syncs megaIgnoreFile.addFilters(excludeFilters); LOG_debug << "Transition of legacy exclusion rules completed successfully. Hiding legacy exclude file"; try { fs::rename(excludeFilePath, hiddenExcludeFilePath); } catch (const fs::filesystem_error& e) { LOG_err << "Could not hide legacy exclude file " << excludeFilePath << " (error: " << e.what() << ")"; } string message = "Your legacy sync exclusion rules have been ported to \"" + pathAsUtf8(defaultMegaIgnorePath) + "\"\n" + "See \"%mega-%sync-ignore\" for more info."; broadcastMessage(message, true); } void ConfigurationManager::unloadConfiguration() { std::lock_guard g(settingsMutex); for (map::iterator itr = oldConfiguredSyncs.begin(); itr != oldConfiguredSyncs.end(); ) { sync_struct *thesync = ((sync_struct*)( *itr ).second ); oldConfiguredSyncs.erase(itr++); delete thesync; } for (map::iterator itr = configuredBackups.begin(); itr != configuredBackups.end(); ) { backup_struct *thebackup = ((backup_struct*)( *itr ).second ); configuredBackups.erase(itr++); delete thebackup; } ConfigurationManager::session = string(); } void ConfigurationManager::loadsyncs() { std::lock_guard g(settingsMutex); if (mConfigFolder.empty()) { loadConfigDir(); } if (!mConfigFolder.empty()) { const fs::path syncsFilePath = mConfigFolder / "syncs"; LOG_debug << "Syncs file: " << syncsFilePath; ifstream fi(syncsFilePath, ios::in | ios::binary); if (fi.is_open()) { if (fi.fail()) { LOG_err << "fail with sync file"; } bool updateSavedFormatRequired = false; while (!( fi.peek() == EOF )) { int versioncodeStoredValues; sync_struct *thesync = new sync_struct; //Load syncs fi.read((char*)&thesync->fingerprint, sizeof( long long )); if (thesync->fingerprint == CONFIGURATIONSTOREDBYVERSION) { fi.read((char*)&versioncodeStoredValues, sizeof(int)); } else { versioncodeStoredValues = 90500; } if (versioncodeStoredValues > 90500) { fi.read((char*)&thesync->fingerprint, sizeof( long long )); } fi.read((char*)&thesync->handle, sizeof( MegaHandle )); size_t lengthLocalPath; fi.read((char*)&lengthLocalPath, sizeof( size_t )); thesync->localpath.resize(lengthLocalPath); fi.read((char*)thesync->localpath.c_str(), sizeof( char ) * lengthLocalPath); if (oldConfiguredSyncs.find(thesync->localpath) != oldConfiguredSyncs.end()) { delete oldConfiguredSyncs[thesync->localpath]; } oldConfiguredSyncs[thesync->localpath] = thesync; if (versioncodeStoredValues != MEGACMD_CODE_VERSION) { updateSavedFormatRequired = true; } } if (updateSavedFormatRequired) { ConfigurationManager::saveSyncs(&ConfigurationManager::oldConfiguredSyncs); } if (fi.bad()) { LOG_err << "fail with sync file at the end"; } fi.close(); } } } void ConfigurationManager::loadbackups() { std::lock_guard g(settingsMutex); if (mConfigFolder.empty()) { loadConfigDir(); } if (!mConfigFolder.empty()) { const fs::path backupsFilePath = mConfigFolder / "backups"; LOG_debug << "Backups file: " << backupsFilePath; ifstream fi(backupsFilePath, ios::in | ios::binary); if (fi.is_open()) { if (fi.fail()) { LOG_err << "fail with backup file"; } while (!( fi.peek() == EOF )) { backup_struct *thebackup = new backup_struct; //Load backups int versionmcmd; fi.read((char*)&versionmcmd, sizeof( int )); fi.read((char*)&thebackup->handle, sizeof( MegaHandle )); size_t lengthLocalPath; fi.read((char*)&lengthLocalPath, sizeof( size_t )); if (lengthLocalPath && lengthLocalPath <= PATH_MAX_LOCAL_BACKUP) { thebackup->localpath.resize(lengthLocalPath); fi.read((char*)thebackup->localpath.c_str(), sizeof( char ) * lengthLocalPath); fi.read((char*)&thebackup->numBackups, sizeof( int )); fi.read((char*)&thebackup->period, sizeof( int64_t )); size_t lengthLocalPeriod; fi.read((char*)&lengthLocalPeriod, sizeof( size_t )); if (lengthLocalPeriod && lengthLocalPeriod <= PATH_MAX_LOCAL_BACKUP) { thebackup->speriod.resize(lengthLocalPeriod); fi.read((char*)thebackup->speriod.c_str(), sizeof( char ) * lengthLocalPeriod); } if (configuredBackups.find(thebackup->localpath) != configuredBackups.end()) { delete configuredBackups[thebackup->localpath]; } thebackup->id = -1; //id will be set upon resumption thebackup->tag = -1; //tag will be set upon resumption configuredBackups[thebackup->localpath] = thebackup; } else { LOG_err << " Failed to restore backup info"; } } if (fi.bad()) { LOG_err << "fail with backup file at the end"; } fi.close(); } } } void ConfigurationManager::loadConfiguration(bool debug) { std::lock_guard g(settingsMutex); #ifdef _WIN32 WindowsUtf8StdoutGuard utf8Guard; #endif // SESSION const fs::path configDir = getAndCreateConfigDir(); if (!configDir.empty()) { const fs::path sessionFilePath = configDir / "session"; if (debug) { COUT << "Session file: " << sessionFilePath << std::endl; } ifstream fi(sessionFilePath, ios::in); if (fi.is_open()) { string line; getline(fi, line); { session = line; if (debug) { COUT << "Session read from configuration: " << line.substr(0, 5) << "..." << std::endl; } } fi.close(); } // Check if version has been updated. if (mConfigFolder.empty()) { loadConfigDir(); } const fs::path versionFilePath = mConfigFolder / VERSION_FILE_NAME; // Get latest version if any. string latestVersion; ifstream fiVer(versionFilePath); if (fiVer.is_open()) { fiVer >> latestVersion; fiVer.close(); } if (!latestVersion.empty() && (MEGACMD_CODE_VERSION > stol(latestVersion))) { hasBeenUpdated = true; if (debug) { COUT << "MEGAcmd has been updated." << std::endl; } } // Store current version. ofstream fo(versionFilePath, ios::out); if (fo.is_open()) { fo << MEGACMD_CODE_VERSION; fo.close(); } else { CERR << "Could not write MEGAcmd version to " << versionFilePath << std::endl; } } else { if (debug) { COUT << "Couldnt access data folder " << std::endl; } } } bool ConfigurationManager::lockExecution() { // Check for legacy lock (in config path) auto dirs = PlatformDirectories::getPlatformSpecificDirectories(); auto configDir = dirs->configDirPath(); auto runtimeDir = getAndCreateRuntimeDir(); if (runtimeDir != configDir) { const fs::path lockFileAtConfigDir = configDir / LOCK_FILE_NAME; if (fs::is_regular_file(lockFileAtConfigDir)) { LOG_warn << "Found a lock file from a previous MEGAcmd version. Will try to acquire the lock and remove it"; bool legacyLockResult = lockExecution(configDir); if (!legacyLockResult) { LOG_err << "Failed to acquire lock from a previous version. You may need to ensure there is " "no MEGAcmd Server running and try again or manually remove lock file: " << lockFileAtConfigDir; return false; } if (!unlockExecution(configDir)) { LOG_err << "Failed to release lock from a previous version. You may need to ensure there is " "no MEGAcmd Server running and try again or manually remove lock file: " << lockFileAtConfigDir; return false; } } } return lockExecution(runtimeDir); } bool ConfigurationManager::lockExecution(const fs::path &lockFileFolder) { assert(!lockFileFolder.empty()); const fs::path lockFilePath = lockFileFolder / LOCK_FILE_NAME; LOG_info << "Lock file: " << lockFilePath; #ifdef _WIN32 mLockFileHandle = CreateFileW(lockFilePath.wstring().c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (mLockFileHandle == INVALID_HANDLE_VALUE) { LOG_err << "ERROR creating lock file: " << ERRNO; return false; } #elif defined(LOCK_EX) && defined(LOCK_NB) fd = open(lockFilePath.string().c_str(), O_RDWR | O_CREAT, 0666); // open or create lockfile //check open success... if (flock(fd, LOCK_EX | LOCK_NB)) { LOG_err << "ERROR locking lock fd: " << errno; close(fd); return false; } if (fcntl(fd, F_SETFD, FD_CLOEXEC) == -1) { LOG_err << "ERROR setting CLOEXEC to lock fd: " << errno; } #else ifstream fi(lockFilePath); if(!fi.fail()) { close(fd); return false; } if (fi.is_open()) { fi.close(); } ofstream fo(lockFilePath); if (fo.is_open()) { fo.close(); } #endif return true; } bool ConfigurationManager::unlockExecution() { return unlockExecution(getAndCreateRuntimeDir()); } bool ConfigurationManager::unlockExecution(const fs::path &lockFileFolder) { assert(!lockFileFolder.empty()); #ifdef _WIN32 CloseHandle(mLockFileHandle); #elif defined(LOCK_EX) && defined(LOCK_NB) flock(fd, LOCK_UN | LOCK_NB); close(fd); #endif const fs::path lockFilePath = lockFileFolder / LOCK_FILE_NAME; if (std::error_code ec; !fs::remove(lockFilePath, ec)) { LOG_err << "Failed to remove lock file, error = " << errorCodeStr(ec); return false; } return true; } string ConfigurationManager::getConfigurationSValue(string propertyName) { std::lock_guard g(settingsMutex); if (mConfigFolder.empty()) { loadConfigDir(); } if (!mConfigFolder.empty()) { const fs::path configFilePath = mConfigFolder / "megacmd.cfg"; return getPropertyFromFile(configFilePath, propertyName.c_str()); } return ""; } void ConfigurationManager::clearConfigurationFile() { std::lock_guard g(settingsMutex); if (mConfigFolder.empty()) { loadConfigDir(); } if (!mConfigFolder.empty()) { const fs::path configFilePath = mConfigFolder / "megacmd.cfg"; ifstream infile(configFilePath); stringstream formerlines; string line; while (getline(infile, line)) { size_t pos; if (line.length() > 0 && line[0] != '#' && (pos = line.find("=")) != string::npos) { string key; key = line.substr(0, pos); rtrimProperty(key, ' '); for (unsigned int i = 0; i < sizeof(persistentmcmdconfigurationkeys)/sizeof(persistentmcmdconfigurationkeys[0]); i++) { if (!strcmp(key.c_str(), persistentmcmdconfigurationkeys[i])) { formerlines << line << endl; } } } else { formerlines << line << endl; } } ofstream fo(configFilePath); if (fo.is_open()) { fo << formerlines.str(); fo.close(); } } } ConfiguratorMegaApiHelper::ConfiguratorMegaApiHelper() { auto configSetterSyncULLCb = [this](auto cb) { return [this, cb](MegaApi *api, const std::string &/*name*/, const char *value) { try { return cb(api, std::stoull(value)); } catch(...) { return false; } return true; }; }; auto confGetter = [](::mega::MegaApi */*api*/,const char *key){ return ConfigurationManager::getConfigurationValueOpt(key); }; auto validatorULL = [](std::optional minOpt = {}, std::optional maxOpt = {}) { return [minOpt, maxOpt](const char *value){ if (value && value[0] == '-') { return false; } try { auto v = std::stoull(value); if (maxOpt && v > *maxOpt) { return false; } if (minOpt && v < *minOpt) { return false; } return true; } catch (...) { return false; } }; }; mConfigurators.emplace_back("max_nodes_in_cache", "Max nodes loaded in memory", "This controls the number of nodes that the SDK stores in memory.", configSetterSyncULLCb([](MegaApi *api, auto value){ api->setLRUCacheSize(value); return true; }), confGetter, std::nullopt/*megaApiGetter*/, validatorULL()); mConfigurators.emplace_back("exported_folders_sdks", "Number of additional SDK instances loaded at startup", "This controls the number of SDK instances that are created at startup in order to download or import contents from exported folder links. " "Default 5. Min 0. Max 20. If set to 0, you will not be able to download or import from folder links.", configSetterSyncULLCb([](MegaApi *api, auto value){ return true;/*TODO: implement reactiveness to value changes*/ }), confGetter, std::nullopt/*megaApiGetter*/, validatorULL(0, 20)); } const std::vector & ConfiguratorMegaApiHelper::getConfigurators() { return mConfigurators; } }//end namespace MEGAcmd-2.5.2_Linux/src/configurationmanager.h000066400000000000000000000227751516543156300212640ustar00rootroot00000000000000/** * @file src/configurationmanager.h * @brief MEGAcmd: configuration manager * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of the MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #ifndef CONFIGURATIONMANAGER_H #define CONFIGURATIONMANAGER_H #include "megacmd.h" #include #include #ifndef _WIN32 #include // LOCK_EX and LOCK_NB #endif #define CONFIGURATIONSTOREDBYVERSION -2 namespace megacmd { class ConfigurationManager { private: inline static fs::path mConfigFolder; inline static bool hasBeenUpdated = false; #ifdef WIN32 static HANDLE mLockFileHandle; #elif defined(LOCK_EX) && defined(LOCK_NB) static int fd; #endif static void loadConfigDir(); static void removeSyncConfig(sync_struct *syncToRemove); #ifdef MEGACMD_TESTING_CODE public: #endif static void createFolderIfNotExisting(const fs::path &folder); static bool lockExecution(const fs::path &lockFileFolder); static bool unlockExecution(const fs::path &lockFileFolder); public: static std::map oldConfiguredSyncs; //This will refer to old syncs from now on static std::map configuredBackups; static std::recursive_mutex settingsMutex; static std::string session; static void loadConfiguration(bool debug); static void clearConfigurationFile(); static bool lockExecution(); static bool unlockExecution(); static void loadsyncs(); static void loadbackups(); static void saveSyncs(std::map *syncsmap); static void saveBackups(std::map *backupsmap); static void transitionLegacyExclusionRules(mega::MegaApi& api); static void saveSession(const char*session); static std::string /* prev value, if any */ saveProperty(const char* property, const char* value); template, const char*>, std::string, T>>> static Opt_T savePropertyValue(const char* property, const T& value) { constexpr bool isStr = std::is_same_v, std::string>; constexpr bool isConstChar = std::is_same_v, const char*>; std::string prevValueStr; if constexpr (isStr) { prevValueStr = saveProperty(property, value.c_str()); } else if constexpr (isConstChar) { prevValueStr = saveProperty(property, value); } else { prevValueStr = saveProperty(property, std::to_string(value).c_str()); } if (prevValueStr.empty()) { return std::nullopt; } if constexpr (isStr || isConstChar) { return prevValueStr; } else { T prevValue; std::istringstream is(prevValueStr); is >> prevValue; return prevValue; } } template static void savePropertyValueList(const char* property, std::list value, char separator = ';') { std::ostringstream os; typename std::list::iterator it = value.begin(); for (; it != value.end(); ++it){ os << (*it); if ( std::distance( it, value.end() ) != 1 ) // not last { os << separator; } } saveProperty(property,os.str().c_str()); } template static void savePropertyValueSet(const char* property, std::set value, char separator = ';') { std::ostringstream os; typename std::set::iterator it = value.begin(); for (; it != value.end(); ++it){ os << (*it); if ( std::distance( it, value.end() ) != 1 ) // not last { os << separator; } } saveProperty(property,os.str().c_str()); } static std::string getConfigurationSValue(std::string propertyName); template static T getConfigurationValue(std::string propertyName, T defaultValue) { std::string propValue = getConfigurationSValue(propertyName); if (!propValue.size()) return defaultValue; T i; std::istringstream is(propValue); is >> i; return i; } template static std::optional getConfigurationValueOpt(std::string propertyName) { std::string propValue = getConfigurationSValue(propertyName); if (!propValue.size()) return {}; T i; std::istringstream is(propValue); is >> i; return i; } template static std::list getConfigurationValueList(std::string propertyName, char separator = ';') { std::list toret; std::string propValue = getConfigurationSValue(propertyName); if (!propValue.size()) { return toret; } size_t possep; do { possep = propValue.find(separator); std::string current = propValue.substr(0,possep); if (possep != std::string::npos && ((possep + 1 ) != propValue.size())) { propValue = propValue.substr(possep + 1); } else { possep = std::string::npos; } if (current.size()) { T i; std::istringstream is(current); is >> i; toret.push_back(i); } } while (possep != std::string::npos); return toret; } static std::list getConfigurationValueList(std::string propertyName, char separator = ';') { std::list toret; std::string propValue = getConfigurationSValue(propertyName); if (!propValue.size()) { return toret; } size_t possep; do { possep = propValue.find(separator); std::string current = propValue.substr(0,possep); if (possep != std::string::npos && ((possep + 1 ) != propValue.size())) { propValue = propValue.substr(possep + 1); } else { possep = std::string::npos; } if (current.size()) { toret.push_back(current); } } while (possep != std::string::npos); return toret; } template static std::set getConfigurationValueSet(std::string propertyName, char separator = ';') { std::set toret; std::string propValue = getConfigurationSValue(propertyName); if (!propValue.size()) { return toret; } size_t possep; do { possep = propValue.find(separator); std::string current = propValue.substr(0,possep); if (possep != std::string::npos && ((possep + 1 ) != propValue.size())) { propValue = propValue.substr(possep + 1); } else { possep = std::string::npos; } if (current.size()) { T i; std::istringstream is(current); is >> i; toret.insert(i); } } while (possep != std::string::npos); return toret; } static fs::path getConfigFolder(); // creates a subfolder within the state dir and returns it (utf8) static fs::path getConfigFolderSubdir(const fs::path& subdirName); static fs::path getAndCreateRuntimeDir(); static fs::path getAndCreateConfigDir(); static bool getHasBeenUpdated(); static void unloadConfiguration(); static void migrateSyncConfig(mega::MegaApi *api); }; class ConfiguratorMegaApiHelper { struct ValueConfigurator { using Setter = std::function; using Getter = std::function(::mega::MegaApi *api, const char *)>; using Validator = std::function; std::string mKey; std::string mDescription; std::string mFullDescription; Setter mSetter; Getter mGetter; std::optional mMegaApiGetter; std::optional mValidator; template ValueConfigurator(const char *key, const char *description, const char *fullDescription, S &&setter, G &&getter, MG &&megaApiGetter, V &&validator) : mKey(key) , mDescription(description) , mFullDescription(fullDescription) , mSetter(std::forward(setter)) , mGetter(std::forward(getter)) , mMegaApiGetter(std::forward(megaApiGetter)) , mValidator(std::forward(validator)) { assert(mSetter != nullptr); assert(mGetter != nullptr); } }; std::vector mConfigurators; public: const std::vector & getConfigurators(); ConfiguratorMegaApiHelper(); }; }//end namespace #endif // CONFIGURATIONMANAGER_H MEGAcmd-2.5.2_Linux/src/deferred_single_trigger.h000066400000000000000000000057161516543156300217220ustar00rootroot00000000000000/** * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #pragma once #include #include #include #include #include #include "megacmdcommonutils.h" class DeferredSingleTrigger { std::unique_ptr mThread; std::condition_variable mConditionVariable; std::mutex mConditionVariableMutex; std::mutex mThreadMutex; bool mDestroyed; size_t mCounter; std::chrono::seconds mSecondsToWait; size_t incrementCounter() { // Since the counter is used in the CV predicate, it must be protected with the CV mutex std::unique_lock lock(mConditionVariableMutex); return ++mCounter; } void destroy() { // Since the destroyed flag is used in the CV predicate, it must be protected with the CV mutex std::unique_lock lock(mConditionVariableMutex); assert(!mDestroyed); mDestroyed = true; } bool isDestroyed() { // Since the destroyed flag is used in the CV predicate, it must be protected with the CV mutex std::unique_lock lock(mConditionVariableMutex); return mDestroyed; } std::unique_lock ensureThreadIsStopped() { std::unique_lock lock(mThreadMutex); if (mThread) { mConditionVariable.notify_one(); mThread->join(); mThread.reset(); } return lock; } public: DeferredSingleTrigger(std::chrono::seconds secondsToWait) : mDestroyed(false), mCounter(0), mSecondsToWait(secondsToWait) {} ~DeferredSingleTrigger() { destroy(); ensureThreadIsStopped(); } template void triggerDeferredSingleShot(CB&& callback) { const size_t id = incrementCounter(); std::unique_lock lock = ensureThreadIsStopped(); if (isDestroyed()) { // Without this check, we might spawn a new thread after the destructor is done // (i.e., if the lock above is waiting on the destructor's lock) return; } mThread.reset(new std::thread([this, id, FWD_CAPTURE(callback)] () { std::unique_lock lock(mConditionVariableMutex); bool cancelled = mConditionVariable.wait_for(lock, mSecondsToWait, [this, id] { assert(mCounter >= id); return mDestroyed || id != mCounter; }); if (!cancelled) { callback(); } })); } }; MEGAcmd-2.5.2_Linux/src/listeners.cpp000066400000000000000000001367711516543156300174270ustar00rootroot00000000000000/** * @file src/listeners.cpp * @brief MEGAcmd: Listeners * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of the MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include "listeners.h" #include "configurationmanager.h" #include "megacmdutils.h" #ifdef MEGACMD_TESTING_CODE #include "../tests/common/Instruments.h" #endif #include using namespace mega; namespace megacmd { #ifdef ENABLE_CHAT void MegaCmdGlobalListener::onChatsUpdate(MegaApi*, MegaTextChatList*) { } #endif void MegaCmdGlobalListener::onUsersUpdate(MegaApi *api, MegaUserList *users1) { if (users1) { if (users1->size() == 1) { LOG_debug << " 1 user received or updated"; } else { LOG_debug << users1->size() << " users received or updated"; } } else //initial update or too many changes { std::unique_ptr users2(api->getContacts()); if (users2 && users2->size()) { if (users2->size() == 1) { LOG_debug << " 1 user received or updated"; } else { LOG_debug << users2->size() << " users received or updated"; } } } } MegaCmdGlobalListener::MegaCmdGlobalListener(MegaCmdLogger *logger, MegaCmdSandbox *sandboxCMD) { this->loggerCMD = logger; this->sandboxCMD = sandboxCMD; ongoing = false; } void MegaCmdGlobalListener::onNodesUpdate(MegaApi *api, MegaNodeList *nodes) { long long nfolders = 0; long long nfiles = 0; long long rfolders = 0; long long rfiles = 0; if (nodes) { for (int i = 0; i < nodes->size(); i++) { MegaNode *n = nodes->get(i); if (n->getType() == MegaNode::TYPE_FOLDER) { if (n->isRemoved()) { rfolders++; } else { nfolders++; } } else if (n->getType() == MegaNode::TYPE_FILE) { if (n->isRemoved()) { rfiles++; } else { nfiles++; } } } if (nfolders) { LOG_debug << nfolders << " folders " << "added or updated "; } if (nfiles) { LOG_debug << nfiles << " files " << "added or updated "; } } else if (loggerCMD->getMaxLogLevel() >= logInfo) //initial update or too many changes { api->getAccountDetails(new MegaCmdListenerFuncExecuter( [nfiles, nfolders](mega::MegaApi *api, mega::MegaRequest *request, mega::MegaError *e) { auto details = std::unique_ptr(request->getMegaAccountDetails()); if (details == nullptr) { LOG_err << "could not get account details"; return; } auto nFiles = nfiles; auto nFolders = nfolders; auto nodeRoot = std::unique_ptr(api->getRootNode()); if (nodeRoot != nullptr) { auto handle = nodeRoot->getHandle(); nFiles += details->getNumFiles(handle); nFolders += details->getNumFolders(handle); } auto vaultNode = std::unique_ptr(api->getVaultNode()); if (vaultNode != nullptr) { auto handle = vaultNode->getHandle(); nFiles += details->getNumFiles(handle); nFolders += details->getNumFolders(handle); } auto rubbishNode = std::unique_ptr(api->getRubbishNode()); if (rubbishNode != nullptr) { auto handle = rubbishNode->getHandle(); nFiles += details->getNumFiles(handle); nFolders += details->getNumFolders(handle); } auto inshares = std::unique_ptr(api->getInShares()); if (inshares) { nFolders += inshares->size(); // add the shares themselves. for (int i = 0; i < inshares->size(); i++) { auto handle = inshares->get(i)->getHandle(); nFiles += details->getNumFiles(handle); nFolders += details->getNumFolders(handle); } } if (nFolders) { LOG_debug << nFolders << " folders added or updated"; } if (nFiles) { LOG_debug << nFiles << " files added or updated"; } }, true)); } if (rfolders) { LOG_debug << rfolders << " folders " << "removed"; } if (rfiles) { LOG_debug << rfiles << " files " << "removed"; } } void MegaCmdGlobalListener::onAccountUpdate(MegaApi *api) { if (!api->getBandwidthOverquotaDelay()) { sandboxCMD->setOverquota(false); } sandboxCMD->temporalbandwidth = 0; //This will cause account details to be queried again } void MegaCmdGlobalListener::onEvent(MegaApi *api, MegaEvent *event) { if (event->getType() == MegaEvent::EVENT_ACCOUNT_BLOCKED) { if (getBlocked() == event->getNumber()) { LOG_debug << " receivied EVENT_ACCOUNT_BLOCKED: number = " << event->getNumber(); return; } setBlocked(int(event->getNumber())); //this should be true always switch (event->getNumber()) { case MegaApi::ACCOUNT_BLOCKED_VERIFICATION_EMAIL: { sandboxCMD->setReasonblocked( "Your account has been temporarily suspended for your safety. " "Please verify your email and follow its steps to unlock your account."); break; } case MegaApi::ACCOUNT_BLOCKED_VERIFICATION_SMS: { if (!ongoing) { ongoing = true; sandboxCMD->setReasonPendingPromise(); api->getSessionTransferURL("", new MegaCmdListenerFuncExecuter( [this](mega::MegaApi* api, mega::MegaRequest *request, mega::MegaError *e) { string reason("Your account has been suspended temporarily due to potential abuse. " "Please verify your phone number to unlock your account." ); if (e->getValue() == MegaError::API_OK) { reason.append(" Open the following link: "); reason.append(request->getLink()); } sandboxCMD->setPromisedReasonblocked(reason); ongoing = false; },true)); } break; } case MegaApi::ACCOUNT_BLOCKED_SUBUSER_DISABLED: { sandboxCMD->setReasonblocked("Your account has been disabled by your administrator. Please contact your business account administrator for further details."); break; } default: { sandboxCMD->setReasonblocked(event->getText()); LOG_err << "Received event account blocked: " << event->getText(); } } } else if (event->getType() == MegaEvent::EVENT_STORAGE) { if (event->getNumber() == MegaApi::STORAGE_STATE_CHANGE) { api->getAccountDetails(); } else { int previousStatus = sandboxCMD->storageStatus; sandboxCMD->storageStatus = int(event->getNumber()); if (sandboxCMD->storageStatus == MegaApi::STORAGE_STATE_PAYWALL || sandboxCMD->storageStatus == MegaApi::STORAGE_STATE_RED || sandboxCMD->storageStatus == MegaApi::STORAGE_STATE_ORANGE) { ConfigurationManager::savePropertyValue("ask4storage",true); if (previousStatus < sandboxCMD->storageStatus) { if (sandboxCMD->storageStatus == MegaApi::STORAGE_STATE_PAYWALL) { api->getUserData(new MegaCmdListenerFuncExecuter( [this](mega::MegaApi* api, mega::MegaRequest *request, mega::MegaError *e) { if (e->getValue() != MegaError::API_OK) { // We don't have access to MegaCmdExecuter's error handling methods here std::string errorString(e->getErrorString()); LOG_err << "Failed to get user data: " << errorString; return; } api->getAccountDetails(new MegaCmdListenerFuncExecuter( [this](mega::MegaApi *api, mega::MegaRequest *request, mega::MegaError *e) { if (e->getValue() != MegaError::API_OK) { std::string errorString(e->getErrorString()); LOG_err << "Failed to get account details: " << errorString; return; } std::unique_ptr myEmail(api->getMyEmail()); std::unique_ptr warningsList(api->getOverquotaWarningsTs()); std::string s("We have contacted you by email"); if (myEmail != nullptr) { s.append(" to ").append(myEmail.get()); } if (warningsList != nullptr) { s.append(" on ").append(getReadableTime(warningsList->get(0), "%b %e %Y")); if (warningsList->size() > 1) { for (int i = 1; i < warningsList->size() - 1; i++) { s += ", " + getReadableTime(warningsList->get(i), "%b %e %Y"); } s += " and " + getReadableTime(warningsList->get(warningsList->size() - 1), "%b %e %Y"); } } std::unique_ptr rootNode(api->getRootNode()); std::unique_ptr details(request->getMegaAccountDetails()); if (details != nullptr && rootNode != nullptr) { auto rootNodeHandle = rootNode->getHandle(); long long totalFiles = details->getNumFiles(rootNodeHandle); long long totalFolders = details->getNumFolders(rootNodeHandle); s += ", but you still have " + std::to_string(totalFiles) + " files and " + std::to_string(totalFolders) + " folders taking up " + sizeToText(sandboxCMD->receivedStorageSum); s += " in your MEGA account, which requires you to upgrade your account.\n\n"; } else // If we weren't able to get the root node or details object, lets just display the total storage used. { s += ", but your MEGA account still uses " + sizeToText(sandboxCMD->receivedStorageSum) + ", which requires you to upgrade your account.\n\n"; } long long daysLeft = (api->getOverquotaDeadlineTs() - m_time(nullptr)) / 86400; if (daysLeft > 0) { s += "You have " + std::to_string(daysLeft) + " days left to upgrade. "; s += "After that, your data is subject to deletion.\n"; } else { s += "You must act immediately to save your data. From now on, your data is subject to deletion.\n"; } s += "See \"help --upgrade\" for further details."; broadcastMessage(s); })); },true)); } else { string s; if (sandboxCMD->storageStatus == MegaApi::STORAGE_STATE_RED) { s+= "You have exeeded your available storage.\n"; } else { s+= "You are running out of available storage.\n"; } s+="You can change your account plan to increase your quota limit.\nSee \"help --upgrade\" for further details"; broadcastMessage(s); } } } else { ConfigurationManager::savePropertyValue("ask4storage",false); } } LOG_info << "Received event storage changed: " << event->getNumber(); } else if (event->getType() == MegaEvent::EVENT_STORAGE_SUM_CHANGED) { sandboxCMD->receivedStorageSum = event->getNumber(); } else if (event->getType() == MegaEvent::EVENT_SYNCS_DISABLED) { std::unique_ptr megaSyncErrorCode {MegaSync::getMegaSyncErrorCode(int(event->getNumber()))}; removeDelayedBroadcastMatching("Your sync has been disabled"); broadcastMessage(std::string("Your syncs have been disabled. Reason: ") .append(megaSyncErrorCode.get()), true); } else if (event->getType() == MegaEvent::EVENT_UPGRADE_SECURITY) { std::stringstream ss; ss << "" "Your account's security needs upgrading.\n" "Please execute: \"confirm --security\".\n" "This only needs to be done once. If you have seen this message for\n" "this account before, please exit MEGAcmd."; auto msg = ss.str(); appendGreetingStatusAllListener(std::string("message:") + msg); broadcastMessage(std::move(msg)); // broadcast the message, so that it reaches currently open shell too! } else if (event->getType() == MegaEvent::EVENT_NODES_CURRENT) { sandboxCMD->mNodesCurrentPromise.fulfil(); } } //////////////////////////////////////////// /// MegaCmdMegaListener methods /// //////////////////////////////////////////// void MegaCmdMegaListener::onRequestFinish(MegaApi *api, MegaRequest *request, MegaError *e) { if (request->getType() == MegaRequest::TYPE_APP_VERSION) { LOG_verbose << "TYPE_APP_VERSION finished"; } else if (request->getType() == MegaRequest::TYPE_LOGOUT) { LOG_debug << "Session closed"; sandboxCMD->resetSandBox(); reset(); } else if (request->getType() == MegaRequest::TYPE_WHY_AM_I_BLOCKED) { if (e->getErrorCode() == MegaError::API_OK && request->getNumber() == MegaApi::ACCOUNT_NOT_BLOCKED) { if (getBlocked()) { unblock(); } } } else if (request->getType() == MegaRequest::TYPE_ACCOUNT_DETAILS) { if (e->getErrorCode() != MegaError::API_OK) { return; } bool storage = (request->getNumDetails() & 0x01) != 0; if (storage) { unique_ptr details(request->getMegaAccountDetails()); sandboxCMD->totalStorage = details->getStorageMax(); } } else if (e && ( e->getErrorCode() == MegaError::API_ESID )) { LOG_err << "Session is no longer valid (it might have been invalidated from elsewhere) "; changeprompt(prompts[COMMAND]); } else if (e && request->getType() == MegaRequest::TYPE_WHY_AM_I_BLOCKED && e->getErrorCode() == API_EBLOCKED) { LOG_err << "Your account has been blocked. Reason: " << request->getText(); } } std::atomic DisableMountErrorsBroadcastingGuard::sDisableBroadcasting = 0; MegaCmdMegaListener::MegaCmdMegaListener(MegaApi *megaApi, MegaListener *parent, MegaCmdSandbox *sandboxCMD) { this->megaApi = megaApi; this->listener = parent; this->sandboxCMD = sandboxCMD; } MegaCmdMegaListener::~MegaCmdMegaListener() { this->listener = NULL; if (megaApi) { megaApi->removeListener(this); } } #ifdef ENABLE_CHAT void MegaCmdMegaListener::onChatsUpdate(MegaApi *api, MegaTextChatList *chats) {} #endif //backup callbacks: void MegaCmdMegaListener::onBackupStateChanged(MegaApi *api, MegaScheduledCopy *backup) { LOG_verbose << " At onBackupStateChanged: " << backupSatetStr(backup->getState()); } void MegaCmdMegaListener::onBackupStart(MegaApi *api, MegaScheduledCopy *backup) { LOG_verbose << " At onBackupStart"; } void MegaCmdMegaListener::onBackupFinish(MegaApi* api, MegaScheduledCopy *backup, MegaError* error) { LOG_verbose << " At onBackupFinish"; if (error->getErrorCode() == MegaError::API_EEXPIRED) { LOG_warn << "Backup skipped (the time for the next one has been reached)"; } else if (error->getErrorCode() != MegaError::API_OK) { LOG_err << "Backup failed: " << error->getErrorString(); } } void MegaCmdMegaListener::onBackupUpdate(MegaApi *api, MegaScheduledCopy *backup) { LOG_verbose << " At onBackupUpdate"; } void MegaCmdMegaListener::onBackupTemporaryError(MegaApi *api, MegaScheduledCopy *backup, MegaError* error) { LOG_verbose << " At onBackupTemporaryError"; if (error->getErrorCode() != MegaError::API_OK) { LOG_err << "Backup temporary error: " << error->getErrorString(); } } void MegaCmdMegaListener::onSyncAdded(MegaApi *api, MegaSync *sync) { LOG_verbose << "Sync added: " << sync->getLocalFolder() << " to " << sync->getLastKnownMegaFolder(); if (!ConfigurationManager::getConfigurationValue("firstSyncConfigured", false)) { sendEvent(StatsManager::MegacmdEvent::FIRST_CONFIGURED_SYNC, api, false); ConfigurationManager::savePropertyValue("firstSyncConfigured", true); } else { sendEvent(StatsManager::MegacmdEvent::SUBSEQUENT_CONFIGURED_SYNC, api, false); } } void MegaCmdMegaListener::onSyncStateChanged(MegaApi *api, MegaSync *sync) { std::stringstream ss; ss << "Your sync " << sync->getLocalFolder() << " to: " << sync->getLastKnownMegaFolder() << " has transitioned to state " << syncRunStateStr(sync->getRunState()); if (sync->getError()) { std::unique_ptr megaSyncErrorCode {sync->getMegaSyncErrorCode()}; ss << ". ErrorCode: " << megaSyncErrorCode.get(); } auto msg = ss.str(); if (sync->getError() || sync->getRunState() >= MegaSync::RUNSTATE_SUSPENDED) { broadcastDelayedMessage(msg, true); } LOG_debug << msg; } void MegaCmdMegaListener::onSyncDeleted(MegaApi *api, MegaSync *sync) { LOG_verbose << "Sync deleted: " << sync->getLocalFolder() << " to " << sync->getLastKnownMegaFolder(); } void MegaCmdMegaListener::onMountAdded(mega::MegaApi* api, const char* path, int result) { onMountEvent("Added", "add", path, result); } void MegaCmdMegaListener::onMountRemoved(mega::MegaApi* api, const char* path, int result) { onMountEvent("Removed", "remove", path, result); } void MegaCmdMegaListener::onMountChanged(mega::MegaApi* api, const char* path, int result) { onMountEvent("Changed", "change", path, result); } void MegaCmdMegaListener::onMountEnabled(mega::MegaApi* api, const char* path, int result) { onMountEvent("Enabled", "enable", path, result); } void MegaCmdMegaListener::onMountDisabled(mega::MegaApi* api, const char* path, int result) { onMountEvent("Disabled", "disable", path, result); } void MegaCmdMegaListener::onMountEvent(std::string_view pastTense, std::string_view presentTense, std::string_view path, int result) { if (result != MegaMount::SUCCESS) { std::ostringstream oss; oss << "Failed to " << presentTense << " mount \"" << path << "\" " << "due to error:\n" << getMountResultStr(result); assert(getMountResultStr(result) != "Mount was successful"); const std::string msg = oss.str(); LOG_err << msg; if (DisableMountErrorsBroadcastingGuard::shouldBroadcast()) { broadcastDelayedMessage(msg, true); } return; } LOG_debug << pastTense << " mount \"" << path << "\""; } //////////////////////////////////////// /// MegaCmdListener methods /// //////////////////////////////////////// void MegaCmdListener::onRequestStart(MegaApi* api, MegaRequest *request) { if (!request) { LOG_err << " onRequestStart for undefined request "; return; } LOG_verbose << "onRequestStart request->getType(): " << request->getType(); } void MegaCmdListener::doOnRequestFinish(MegaApi* api, MegaRequest *request, MegaError* e) { if (!request) { LOG_err << " onRequestFinish for undefined request "; return; } LOG_verbose << "onRequestFinish request->getType(): " << request->getType(); switch (request->getType()) { case MegaRequest::TYPE_FETCH_NODES: { informProgressUpdate(PROGRESS_COMPLETE, request->getTotalBytes(), this->clientID, "Fetching nodes"); break; } default: break; } } void MegaCmdListener::onRequestUpdate(MegaApi* api, MegaRequest *request) { if (!request) { LOG_err << " onRequestUpdate for undefined request "; return; } LOG_verbose << "onRequestUpdate request->getType(): " << request->getType(); switch (request->getType()) { case MegaRequest::TYPE_FETCH_NODES: { #ifdef MEGACMD_TESTING_CODE TestInstruments::Instance().fireEvent(TestInstruments::Event::FETCH_NODES_REQ_UPDATE); #endif unsigned int cols = getNumberOfCols(80); string outputString; outputString.resize(cols+1); for (unsigned int i = 0; i < cols; i++) { outputString[i] = '.'; } outputString[cols] = '\0'; char *ptr = (char *)outputString.c_str(); sprintf(ptr, "%s", "Fetching nodes ||"); ptr += strlen("Fetching nodes ||"); *ptr = '.'; //replace \0 char float oldpercent = percentFetchnodes; if (request->getTotalBytes() == 0) { percentFetchnodes = 0; } else { percentFetchnodes = float(request->getTransferredBytes() * 1.0 / request->getTotalBytes() * 100.0); } if (alreadyFinished || ( ( percentFetchnodes == oldpercent ) && ( oldpercent != 0 )) ) { return; } if (percentFetchnodes < 0) { percentFetchnodes = 0; } char aux[40]; if (request->getTotalBytes() < 0) { return; // after a 100% this happens } if (request->getTransferredBytes() < 0.001 * request->getTotalBytes()) { return; // after a 100% this happens } if (request->getTotalBytes() < 1048576) { sprintf(aux,"||(%lld/%lld KB: %.2f %%) ", request->getTransferredBytes() / 1024, request->getTotalBytes() / 1024, percentFetchnodes); } else { sprintf(aux,"||(%lld/%lld MB: %.2f %%) ", request->getTransferredBytes() / 1024 / 1024, request->getTotalBytes() / 1024 / 1024, percentFetchnodes); } sprintf((char *)outputString.c_str() + cols - strlen(aux), "%s", aux); for (int i = 0; i <= ( cols - strlen("Fetching nodes ||") - strlen(aux)) * 1.0 * percentFetchnodes / 100.0; i++) { *ptr++ = '#'; } if (percentFetchnodes == 100 && !alreadyFinished) { alreadyFinished = true; //cout << outputString << endl; LOG_debug << outputString; } else { LOG_debug << outputString; //cout << outputString << '\r' << flush; } informProgressUpdate(request->getTransferredBytes(), request->getTotalBytes(), this->clientID, "Fetching nodes"); break; } default: LOG_debug << "onRequestUpdate of unregistered type of request: " << request->getType(); break; } } void MegaCmdListener::onRequestTemporaryError(MegaApi *api, MegaRequest *request, MegaError* e) { } MegaCmdListener::~MegaCmdListener() { } MegaCmdListener::MegaCmdListener(MegaApi *megaApi, MegaRequestListener *listener, int clientID) { this->megaApi = megaApi; this->listener = listener; percentFetchnodes = 0.0f; alreadyFinished = false; this->clientID = clientID; } //////////////////////////////////////////////// /// MegaCmdTransferListener methods /// //////////////////////////////////////////////// void MegaCmdTransferListener::onTransferStart(MegaApi* api, MegaTransfer *transfer) { if (listener) { listener->onTransferStart(api,transfer); } if (!transfer) { LOG_err << " onTransferStart for undefined Transfer "; return; } LOG_verbose << "onTransferStart Transfer->getType(): " << transfer->getType(); } void MegaCmdTransferListener::doOnTransferFinish(MegaApi* api, MegaTransfer *transfer, MegaError* e) { if (listener) { listener->onTransferFinish(api,transfer,e); } if (!transfer) { LOG_err << " onTransferFinish for undefined transfer "; return; } LOG_verbose << "doOnTransferFinish Transfer->getType(): " << transfer->getType(); informProgressUpdate(PROGRESS_COMPLETE, transfer->getTotalBytes(), clientID); } void MegaCmdTransferListener::onTransferUpdate(MegaApi* api, MegaTransfer *transfer) { if (listener) { listener->onTransferUpdate(api,transfer); } if (!transfer) { LOG_err << " onTransferUpdate for undefined Transfer "; return; } unsigned int cols = getNumberOfCols(80); string outputString; outputString.resize(cols + 1); for (unsigned int i = 0; i < cols; i++) { outputString[i] = '.'; } outputString[cols] = '\0'; char *ptr = (char *)outputString.c_str(); sprintf(ptr, "%s", "TRANSFERRING ||"); ptr += strlen("TRANSFERRING ||"); *ptr = '.'; //replace \0 char float oldpercent = percentDownloaded; if (transfer->getTotalBytes() == 0) { percentDownloaded = 0; } else { percentDownloaded = float(transfer->getTransferredBytes() * 1.0 / transfer->getTotalBytes() * 100.0); } if (alreadyFinished || ( (percentDownloaded == oldpercent ) && ( oldpercent != 0 ) ) ) { return; } if (percentDownloaded < 0) { percentDownloaded = 0; } char aux[40]; if (transfer->getTotalBytes() < 0) { return; // after a 100% this happens } if (transfer->getTransferredBytes() < 0.001 * transfer->getTotalBytes()) { return; // after a 100% this happens } if (transfer->getTotalBytes() < 1048576) { sprintf(aux,"||(%lld/%lld KB: %.2f %%) ", (long long)(transfer->getTransferredBytes() / 1024), (long long)(transfer->getTotalBytes() / 1024), (float)percentDownloaded); } else { sprintf(aux,"||(%lld/%lld MB: %.2f %%) ", (long long)(transfer->getTransferredBytes() / 1024 / 1024), (long long)(transfer->getTotalBytes() / 1024 / 1024), (float)percentDownloaded); } sprintf((char *)outputString.c_str() + cols - strlen(aux), "%s", aux); for (int i = 0; i <= ( cols - strlen("TRANSFERRING ||") - strlen(aux)) * 1.0 * percentDownloaded / 100.0; i++) { *ptr++ = '#'; } if (percentDownloaded == 100 && !alreadyFinished) { alreadyFinished = true; //cout << outputString << endl; LOG_debug << outputString; } else { LOG_debug << outputString; //cout << outputString << '\r' << flush; } LOG_verbose << "onTransferUpdate transfer->getType(): " << transfer->getType() << " clientID=" << this->clientID; informTransferUpdate(transfer, this->clientID); } void MegaCmdTransferListener::onTransferTemporaryError(MegaApi *api, MegaTransfer *transfer, MegaError* e) { if (listener) { listener->onTransferTemporaryError(api,transfer,e); } } MegaCmdTransferListener::~MegaCmdTransferListener() { } MegaCmdTransferListener::MegaCmdTransferListener(MegaApi *megaApi, MegaCmdSandbox *sandboxCMD, MegaTransferListener *listener, int clientID) { this->megaApi = megaApi; this->sandboxCMD = sandboxCMD; this->listener = listener; percentDownloaded = 0.0f; alreadyFinished = false; this->clientID = clientID; } bool MegaCmdTransferListener::onTransferData(MegaApi *api, MegaTransfer *transfer, char *buffer, size_t size) { return true; } ///////////////////////////////////////////////////// /// MegaCmdMultiTransferListener methods /// ///////////////////////////////////////////////////// void MegaCmdMultiTransferListener::onTransferStart(MegaApi* api, MegaTransfer *transfer) { if (!transfer) { LOG_err << " onTransferStart for undefined Transfer "; return; } alreadyFinished = false; if (totalbytes == 0) { percentDownloaded = 0; } else { percentDownloaded = float((transferredbytes + getOngoingTransferredBytes()) * 1.0 / totalbytes * 1.0); } onTransferUpdate(api,transfer); LOG_verbose << "onTransferStart Transfer->getType(): " << transfer->getType(); if (mOnTransferStartCb) { mOnTransferStartCb(transfer); } } void MegaCmdMultiTransferListener::doOnTransferFinish(MegaApi* api, MegaTransfer *transfer, MegaError* e) { finished++; finalerror = (finalerror!=API_OK)?finalerror:e->getErrorCode(); if (!transfer) { LOG_err << " onTransferFinish for undefined transfer "; return; } LOG_verbose << "doOnTransferFinish MegaCmdMultiTransferListener Transfer->getType(): " << transfer->getType() << " transferring " << transfer->getFileName(); if (e->getErrorCode() == API_OK) { // communicate status info string s= "endtransfer:"; s+=((transfer->getType() == MegaTransfer::TYPE_DOWNLOAD)?"D":"U"); s+=":"; if (transfer->getType() == MegaTransfer::TYPE_UPLOAD) { std::unique_ptr n(api->getNodeByHandle(transfer->getNodeHandle())); if (n) { std::unique_ptr path(api->getNodePath(n.get())); if (path) { s += path.get(); } } } else { s+=transfer->getPath(); } informStateListenerByClientId(this->clientID, s); } map::iterator itr = ongoingtransferredbytes.find(transfer->getTag()); if ( itr!= ongoingtransferredbytes.end()) { ongoingtransferredbytes.erase(itr); } itr = ongoingtotalbytes.find(transfer->getTag()); if ( itr!= ongoingtotalbytes.end()) { ongoingtotalbytes.erase(itr); } transferredbytes+=transfer->getTransferredBytes(); totalbytes+=transfer->getTotalBytes(); } void MegaCmdMultiTransferListener::waitMultiEnd() { for (unsigned i = 0; i < created; i++) { wait(); } } void MegaCmdMultiTransferListener::waitMultiStart() { std::unique_lock lock(mStartedTransfersMutex); mStartedConditionVariable.wait(lock, [this]() { return (mStartedTransfersCount >= created); }); } void MegaCmdMultiTransferListener::onTransferUpdate(MegaApi* api, MegaTransfer *transfer) { if (!transfer) { LOG_err << " onTransferUpdate for undefined Transfer "; return; } ongoingtransferredbytes[transfer->getTag()] = transfer->getTransferredBytes(); ongoingtotalbytes[transfer->getTag()] = transfer->getTotalBytes(); unsigned int cols = getNumberOfCols(80); string outputString; outputString.resize(cols + 1); for (unsigned int i = 0; i < cols; i++) { outputString[i] = '.'; } outputString[cols] = '\0'; char *ptr = (char *)outputString.c_str(); sprintf(ptr, "%s", "TRANSFERRING ||"); ptr += strlen("TRANSFERRING ||"); *ptr = '.'; //replace \0 char float oldpercent = percentDownloaded; if ((totalbytes + getOngoingTotalBytes() ) == 0) { percentDownloaded = 0; } else { percentDownloaded = float((transferredbytes + getOngoingTransferredBytes()) * 1.0 / (totalbytes + getOngoingTotalBytes() ) * 100.0); } if (alreadyFinished || ( (percentDownloaded == oldpercent ) && ( oldpercent != 0 ) ) ) { return; } if (percentDownloaded < 0) { percentDownloaded = 0; } assert(percentDownloaded <=100); char aux[40]; if (transfer->getTotalBytes() < 0) { return; // after a 100% this happens } if (transfer->getTransferredBytes() < 0.001 * transfer->getTotalBytes()) { return; // after a 100% this happens } if (totalbytes + getOngoingTotalBytes() < 1048576) { sprintf(aux,"||(%lld/%lld KB: %.2f %%) ", (transferredbytes + getOngoingTransferredBytes()) / 1024, (totalbytes + getOngoingTotalBytes() ) / 1024, percentDownloaded); } else { sprintf(aux,"||(%lld/%lld MB: %.2f %%) ", (transferredbytes + getOngoingTransferredBytes()) / 1024 / 1024, (totalbytes + getOngoingTotalBytes() ) / 1024 / 1024, percentDownloaded); } sprintf((char *)outputString.c_str() + cols - strlen(aux), "%s", aux); for (int i = 0; i <= ( cols - strlen("TRANSFERRING ||") - strlen(aux)) * 1.0 * min (100.0f, percentDownloaded) / 100.0; i++) { *ptr++ = '#'; } if (percentDownloaded == 100 && !alreadyFinished) { alreadyFinished = true; //cout << outputString << endl; LOG_debug << outputString; } else { LOG_debug << outputString; //cout << outputString << '\r' << flush; } LOG_verbose << "onTransferUpdate transfer->getType(): " << transfer->getType() << " clientID=" << this->clientID; informProgressUpdate((transferredbytes + getOngoingTransferredBytes()),(totalbytes + getOngoingTotalBytes() ), clientID); progressinformed = true; } void MegaCmdMultiTransferListener::onTransferTemporaryError(MegaApi *api, MegaTransfer *transfer, MegaError* e) { } MegaCmdMultiTransferListener::~MegaCmdMultiTransferListener() { } int MegaCmdMultiTransferListener::getFinalerror() const { return finalerror; } long long MegaCmdMultiTransferListener::getTotalbytes() const { return totalbytes; } long long MegaCmdMultiTransferListener::getOngoingTransferredBytes() { long long total = 0; for (map::iterator itr = ongoingtransferredbytes.begin(); itr!= ongoingtransferredbytes.end(); itr++) { total += itr->second; } return total; } long long MegaCmdMultiTransferListener::getOngoingTotalBytes() { long long total = 0; for (map::iterator itr = ongoingtotalbytes.begin(); itr!= ongoingtotalbytes.end(); itr++) { total += itr->second; } return total; } bool MegaCmdMultiTransferListener::getProgressinformed() const { return progressinformed; } MegaCmdMultiTransferListener::MegaCmdMultiTransferListener(MegaApi *megaApi, MegaCmdSandbox *sandboxCMD, MegaTransferListener *listener, int clientID) { this->megaApi = megaApi; this->sandboxCMD = sandboxCMD; this->listener = listener; percentDownloaded = 0.0f; alreadyFinished = false; this->clientID = clientID; created = 0; finished = 0; totalbytes = 0; transferredbytes = 0; progressinformed = false; finalerror = MegaError::API_OK; } bool MegaCmdMultiTransferListener::onTransferData(MegaApi *api, MegaTransfer *transfer, char *buffer, size_t size) { return true; } void MegaCmdMultiTransferListener::onNewTransfer() { std::lock_guard g(mStartedTransfersMutex); created ++; } void MegaCmdMultiTransferListener::onTransferStarted(const std::string &path, int tag) { { std::lock_guard g(mStartedTransfersMutex); mStartedTransfersCount++; mStartedConditionVariable.notify_one(); } } //////////////////////////////////////// /// MegaCmdGlobalTransferListener /// //////////////////////////////////////// const int MegaCmdGlobalTransferListener::MAXCOMPLETEDTRANSFERSBUFFER = 10000; MegaCmdGlobalTransferListener::MegaCmdGlobalTransferListener(MegaApi *megaApi, MegaCmdSandbox *sandboxCMD, MegaTransferListener *parent) { this->megaApi = megaApi; this->sandboxCMD = sandboxCMD; this->listener = parent; } void MegaCmdGlobalTransferListener::onTransferFinish(MegaApi* api, MegaTransfer *transfer, MegaError* error) { completedTransfersMutex.lock(); completedTransfers.push_front(transfer->copy()); // source MegaNode * node = api->getNodeByHandle(transfer->getNodeHandle()); if (node) { char * nodepath = api->getNodePath(node); completedPathsByHandle[transfer->getNodeHandle()]=nodepath; delete []nodepath; delete node; } if (completedTransfers.size()>MAXCOMPLETEDTRANSFERSBUFFER) { MegaTransfer * todelete = completedTransfers.back(); completedPathsByHandle.erase(todelete->getNodeHandle()); //TODO: this can be potentially eliminate a handle that has been added twice delete todelete; completedTransfers.pop_back(); } completedTransfersMutex.unlock(); } void MegaCmdGlobalTransferListener::onTransferTemporaryError(MegaApi *api, MegaTransfer *transfer, MegaError* e) { if (e && e->getErrorCode() == MegaError::API_EOVERQUOTA && e->getValue()) { if (!sandboxCMD->isOverquota()) { LOG_warn << "Reached bandwidth quota. Your download could not proceed " "because it would take you over the current free transfer allowance for your IP address. " "This limit is dynamic and depends on the amount of unused bandwidth we have available. " "You can change your account plan to increase such bandwidth. " "See \"help --upgrade\" for further details"; } sandboxCMD->setOverquota(true); sandboxCMD->timeOfOverquota = m_time(NULL); sandboxCMD->secondsOverQuota=e->getValue(); } } bool MegaCmdGlobalTransferListener::onTransferData(MegaApi *api, MegaTransfer *transfer, char *buffer, size_t size) {return false;}; MegaCmdGlobalTransferListener::~MegaCmdGlobalTransferListener() { completedTransfersMutex.lock(); while (completedTransfers.size()) { delete completedTransfers.front(); completedTransfers.pop_front(); } completedTransfersMutex.unlock(); } bool MegaCmdCatTransferListener::onTransferData(MegaApi *api, MegaTransfer *transfer, char *buffer, size_t size) { if (!ls->isClientConnected()) { LOG_verbose << " CatTransfer listener, cancelled transfer due to client disconnected"; api->cancelTransfer(transfer); } else { LOG_verbose << " CatTransfer listener, streaming " << size << " bytes"; *ls << BinaryStringView(buffer, size); } return true; } ATransferListener::ATransferListener(const std::shared_ptr &mMultiTransferListener, const std::string &path) : mMultiTransferListener(mMultiTransferListener), mPath(path) { assert(mMultiTransferListener); } ATransferListener::~ATransferListener() { } void ATransferListener::onTransferStart(MegaApi *api, MegaTransfer *transfer) { auto tag = transfer->getTag(); mMultiTransferListener->onTransferStarted(mPath, tag); mMultiTransferListener->onTransferStart(api, transfer); } void ATransferListener::onTransferFinish(MegaApi *api, MegaTransfer *transfer, MegaError *e) { static_cast(mMultiTransferListener.get())->onTransferFinish(api, transfer, e); delete this; } void ATransferListener::onTransferUpdate(MegaApi *api, MegaTransfer *transfer) { mMultiTransferListener->onTransferUpdate(api, transfer); } void ATransferListener::onTransferTemporaryError(MegaApi *api, MegaTransfer *transfer, MegaError *e) { mMultiTransferListener->onTransferTemporaryError(api, transfer, e); } bool ATransferListener::onTransferData(MegaApi *api, MegaTransfer *transfer, char *buffer, size_t size) { return mMultiTransferListener->onTransferData(api, transfer, buffer, size); } std::string_view MegaCmdFatalErrorListener::getFatalErrorStr(int64_t fatalErrorType) { switch (fatalErrorType) { case MegaEvent::REASON_ERROR_UNKNOWN: return "REASON_ERROR_UNKNOWN"; case MegaEvent::REASON_ERROR_NO_ERROR: return "REASON_ERROR_NO_ERROR"; case MegaEvent::REASON_ERROR_FAILURE_UNSERIALIZE_NODE: return "REASON_ERROR_FAILURE_UNSERIALIZE_NODE"; case MegaEvent::REASON_ERROR_DB_IO_FAILURE: return "REASON_ERROR_DB_IO_FAILURE"; case MegaEvent::REASON_ERROR_DB_FULL: return "REASON_ERROR_DB_FULL"; case MegaEvent::REASON_ERROR_DB_INDEX_OVERFLOW: return "REASON_ERROR_DB_INDEX_OVERFLOW"; case MegaEvent::REASON_ERROR_NO_JSCD: return "REASON_ERROR_NO_JSCD"; case MegaEvent::REASON_ERROR_REGENERATE_JSCD: return "REASON_ERROR_REGENERATE_JSCD"; default: assert(false); return ""; } } template MegaRequestListener* MegaCmdFatalErrorListener::createLogoutListener(std::string_view msg) { return new MegaCmdListenerFuncExecuter( [this, msg] (mega::MegaApi *api, mega::MegaRequest *request, mega::MegaError *e) { broadcastMessage(std::string(msg)); mCmdSandbox.cmdexecuter->actUponLogout(*api, e, localLogout); }, true /* autoremove */ ); } void MegaCmdFatalErrorListener::onEvent(mega::MegaApi *api, mega::MegaEvent *event) { assert(api); assert(event); if (event->getType() != MegaEvent::EVENT_FATAL_ERROR) { return; } const int64_t fatalErrorType = event->getNumber(); LOG_err << "Received fatal error " << getFatalErrorStr(fatalErrorType) << " (type: " << fatalErrorType << ")"; switch (fatalErrorType) { case MegaEvent::REASON_ERROR_UNKNOWN: { broadcastMessage("An error is causing the communication with MEGA to fail. Your syncs and backups " "are unable to update, and there may be further issues if you continue using MEGAcmd " "without restarting. We strongly recommend immediately restarting the MEGAcmd server to " "resolve this problem. If the issue persists, please contact support."); break; } case MegaEvent::REASON_ERROR_NO_ERROR: { break; } case MegaEvent::REASON_ERROR_FAILURE_UNSERIALIZE_NODE: { broadcastMessage("A serious issue has been detected in MEGAcmd, or in the connection between " "this device and MEGA. Delete your local \".megaCmd\" folder and reinstall the app " "from https://mega.io/cmd, or contact support for further assistance."); break; } case MegaEvent::REASON_ERROR_DB_IO_FAILURE: { std::string_view msg = "Critical system files which are required by MEGACmd are unable to be reached. " "Please check permissions in the \".megaCmd\" folder, or try restarting the " "MEGAcmd server. If the issue still persists, please contact support."; api->localLogout(createLogoutListener(msg)); break; } case MegaEvent::REASON_ERROR_DB_FULL: { std::string_view msg = "There's not enough space in your local storage to run MEGAcmd. Please make " "more space available before running MEGAcmd."; api->localLogout(createLogoutListener(msg)); break; } case MegaEvent::REASON_ERROR_DB_INDEX_OVERFLOW: { std::string_view msg = "MEGAcmd has detected a critical internal error and needs to reload. " "You've been logged out. If you experience this issue more than once, please contact support."; // According to the Confluence documentation on fatal errors, this should be an account reload // We'll do a logout instead to avoid problems with api folders api->logout(false, createLogoutListener(msg)); break; } case MegaEvent::REASON_ERROR_NO_JSCD: { broadcastMessage("MEGAcmd has detected an error in your sync configuration data. You need to manually " "logout of MEGAcmd, and log back in, to resolve this issue. If the problem persists " "afterwards, please contact support."); break; } case MegaEvent::REASON_ERROR_REGENERATE_JSCD: { broadcastMessage("MEGAcmd has detected an error in your sync data. Please, reconfigure your syncs now. " "If the issue persists afterwards, please contact support."); break; } default: { LOG_err << "Unhandled fatal error type " << fatalErrorType; assert(false); break; } } } MegaCmdFatalErrorListener::MegaCmdFatalErrorListener(MegaCmdSandbox& cmdSandbox) : mCmdSandbox(cmdSandbox) { } } //end namespace MEGAcmd-2.5.2_Linux/src/listeners.h000066400000000000000000000257721516543156300170720ustar00rootroot00000000000000/** * @file src/listeners.h * @brief MEGAcmd: Listeners * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of the MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #ifndef LISTENERS_H #define LISTENERS_H #include "megacmdlogger.h" #include "megacmdsandbox.h" namespace megacmd { class MegaCmdSandbox; class MegaCmdListener : public mega::SynchronousRequestListener { private: float percentFetchnodes; bool alreadyFinished; public: int clientID; MegaCmdListener(mega::MegaApi *megaApi, mega::MegaRequestListener *listener = NULL, int clientID=-1); virtual ~MegaCmdListener(); //Request callbacks virtual void onRequestStart(mega::MegaApi* api, mega::MegaRequest *request); virtual void doOnRequestFinish(mega::MegaApi* api, mega::MegaRequest *request, mega::MegaError* error); virtual void onRequestUpdate(mega::MegaApi* api, mega::MegaRequest *request); virtual void onRequestTemporaryError(mega::MegaApi *api, mega::MegaRequest *request, mega::MegaError* e); protected: mega::MegaRequestListener *listener; }; /** * @brief The MegaCmdListenerFuncExecuter class * * it takes an std::function as parameter that will be called upon request finish. * */ class MegaCmdListenerFuncExecuter : public mega::MegaRequestListener { private: std::function onRequestFinishCallback; bool mAutoremove = true; public: /** * @brief MegaCmdListenerFuncExecuter * @param func to call upon onRequestFinish * @param autoremove whether this should be deleted after func is called */ MegaCmdListenerFuncExecuter(std::function func, bool autoremove = false) { onRequestFinishCallback = std::move(func); mAutoremove = autoremove; } void onRequestFinish(mega::MegaApi *api, mega::MegaRequest *request, mega::MegaError *e) { onRequestFinishCallback(api, request, e); if (mAutoremove) { delete this; } } virtual void onRequestStart(mega::MegaApi* api, mega::MegaRequest *request) {} virtual void onRequestUpdate(mega::MegaApi* api, mega::MegaRequest *request) {} virtual void onRequestTemporaryError(mega::MegaApi *api, mega::MegaRequest *request, mega::MegaError* e) {} }; class MegaCmdTransferListener : public mega::SynchronousTransferListener { private: MegaCmdSandbox * sandboxCMD; float percentDownloaded; bool alreadyFinished; int clientID; public: MegaCmdTransferListener(mega::MegaApi *megaApi, MegaCmdSandbox * sandboxCMD, mega::MegaTransferListener *listener = NULL, int clientID=-1); virtual ~MegaCmdTransferListener(); //Transfer callbacks virtual void onTransferStart(mega::MegaApi* api, mega::MegaTransfer *transfer); virtual void doOnTransferFinish(mega::MegaApi* api, mega::MegaTransfer *transfer, mega::MegaError* e); virtual void onTransferUpdate(mega::MegaApi* api, mega::MegaTransfer *transfer); virtual void onTransferTemporaryError(mega::MegaApi *api, mega::MegaTransfer *transfer, mega::MegaError* e); virtual bool onTransferData(mega::MegaApi *api, mega::MegaTransfer *transfer, char *buffer, size_t size); protected: mega::MegaTransferListener *listener; }; class MegaCmdCatTransferListener : public MegaCmdTransferListener { private: LoggedStream *ls; public: std::string contents; MegaCmdCatTransferListener(LoggedStream *_ls, mega::MegaApi *megaApi, MegaCmdSandbox * sandboxCMD, mega::MegaTransferListener *listener = NULL, int clientID=-1) :MegaCmdTransferListener(megaApi,sandboxCMD,listener,clientID),ls(_ls){}; bool onTransferData(mega::MegaApi *api, mega::MegaTransfer *transfer, char *buffer, size_t size); }; class MegaCmdMultiTransferListener : public mega::SynchronousTransferListener { private: MegaCmdSandbox * sandboxCMD; float percentDownloaded; bool alreadyFinished; int clientID; unsigned created; int finished; long long transferredbytes; std::map ongoingtransferredbytes; std::map ongoingtotalbytes; long long totalbytes; int finalerror; long long getOngoingTransferredBytes(); long long getOngoingTotalBytes(); bool progressinformed; std::mutex mStartedTransfersMutex; //to protect mStartedTransfers std::condition_variable mStartedConditionVariable; unsigned mStartedTransfersCount = 0; std::function mOnTransferStartCb; public: MegaCmdMultiTransferListener(mega::MegaApi *megaApi, MegaCmdSandbox * sandboxCMD, mega::MegaTransferListener *listener = NULL, int clientID=-1); virtual ~MegaCmdMultiTransferListener(); //Transfer callbacks virtual void onTransferStart(mega::MegaApi* api, mega::MegaTransfer *transfer); virtual void doOnTransferFinish(mega::MegaApi* api, mega::MegaTransfer *transfer, mega::MegaError* e); virtual void onTransferUpdate(mega::MegaApi* api, mega::MegaTransfer *transfer); virtual void onTransferTemporaryError(mega::MegaApi *api, mega::MegaTransfer *transfer, mega::MegaError* e); virtual bool onTransferData(mega::MegaApi *api, mega::MegaTransfer *transfer, char *buffer, size_t size); void onNewTransfer(); void onTransferStarted(const std::string &path, int tag); void waitMultiEnd(); // waits untill all transfers have started void waitMultiStart(); int getFinalerror() const; long long getTotalbytes() const; bool getProgressinformed() const; template void setOnTransferStartCb(Cb &&cb) { mOnTransferStartCb = std::forward(cb); } protected: mega::MegaTransferListener *listener; }; /** * @brief TODO: document and rename * Note: self destructive */ class ATransferListener : public mega::MegaTransferListener { private: std::shared_ptr mMultiTransferListener; //the listener this belongs too const std::string mPath; //The path that originated the transfer public: ATransferListener(const std::shared_ptr &mMultiTransferListener, const std::string &path); virtual ~ATransferListener(); //Transfer callbacks virtual void onTransferStart(mega::MegaApi* api, mega::MegaTransfer *transfer); virtual void onTransferFinish(mega::MegaApi* api, mega::MegaTransfer *transfer, mega::MegaError* e); virtual void onTransferUpdate(mega::MegaApi* api, mega::MegaTransfer *transfer); virtual void onTransferTemporaryError(mega::MegaApi *api, mega::MegaTransfer *transfer, mega::MegaError* e); virtual bool onTransferData(mega::MegaApi *api, mega::MegaTransfer *transfer, char *buffer, size_t size); }; class MegaCmdGlobalListener : public mega::MegaGlobalListener { private: MegaCmdLogger *loggerCMD; MegaCmdSandbox *sandboxCMD; std::atomic_bool ongoing; public: MegaCmdGlobalListener(MegaCmdLogger *logger, MegaCmdSandbox *sandboxCMD); void onNodesUpdate(mega::MegaApi* api, mega::MegaNodeList *nodes); void onUsersUpdate(mega::MegaApi* api, mega::MegaUserList *users); void onAccountUpdate(mega::MegaApi *api); void onEvent(mega::MegaApi *api, mega::MegaEvent *event); #ifdef ENABLE_CHAT void onChatsUpdate(mega::MegaApi*, mega::MegaTextChatList*); #endif }; class DisableMountErrorsBroadcastingGuard { static std::atomic sDisableBroadcasting; public: DisableMountErrorsBroadcastingGuard() { sDisableBroadcasting++; } ~DisableMountErrorsBroadcastingGuard() { sDisableBroadcasting--; } static bool shouldBroadcast() { return sDisableBroadcasting == 0; } }; class MegaCmdMegaListener : public mega::MegaListener { void onMountEvent(std::string_view pastTense, std::string_view presentTense, std::string_view path, int result); public: MegaCmdMegaListener(mega::MegaApi *megaApi, mega::MegaListener *parent=NULL, MegaCmdSandbox *sandboxCMD = NULL); virtual ~MegaCmdMegaListener(); virtual void onRequestFinish(mega::MegaApi* api, mega::MegaRequest *request, mega::MegaError* e); #ifdef ENABLE_CHAT void onChatsUpdate(mega::MegaApi *api, mega::MegaTextChatList *chats); #endif virtual void onBackupStateChanged(mega::MegaApi *api, mega::MegaScheduledCopy *backup); virtual void onBackupStart(mega::MegaApi *api, mega::MegaScheduledCopy *backup); virtual void onBackupFinish(mega::MegaApi* api, mega::MegaScheduledCopy *backup, mega::MegaError* error); virtual void onBackupUpdate(mega::MegaApi *api, mega::MegaScheduledCopy *backup); virtual void onBackupTemporaryError(mega::MegaApi *api, mega::MegaScheduledCopy *backup, mega::MegaError* error); void onMountAdded(mega::MegaApi* api, const char* path, int result) override; void onMountRemoved(mega::MegaApi* api, const char* path, int result) override; void onMountChanged(mega::MegaApi* api, const char* path, int result) override; void onMountEnabled(mega::MegaApi* api, const char* path, int result) override; void onMountDisabled(mega::MegaApi* api, const char* path, int result) override; void onSyncAdded(mega::MegaApi *api, mega::MegaSync *sync) override; void onSyncStateChanged(mega::MegaApi *api, mega::MegaSync *sync) override; void onSyncDeleted(mega::MegaApi *api, mega::MegaSync *sync) override; protected: mega::MegaApi *megaApi; mega::MegaListener *listener; MegaCmdSandbox *sandboxCMD; }; class MegaCmdGlobalTransferListener : public mega::MegaTransferListener { private: MegaCmdSandbox *sandboxCMD; static const int MAXCOMPLETEDTRANSFERSBUFFER; public: std::mutex completedTransfersMutex; std::deque completedTransfers; std::map completedPathsByHandle; public: MegaCmdGlobalTransferListener(mega::MegaApi *megaApi, MegaCmdSandbox *sandboxCMD, mega::MegaTransferListener *parent = NULL); virtual ~MegaCmdGlobalTransferListener(); //Transfer callbacks void onTransferFinish(mega::MegaApi* api, mega::MegaTransfer *transfer, mega::MegaError* error); void onTransferTemporaryError(mega::MegaApi *api, mega::MegaTransfer *transfer, mega::MegaError* e); bool onTransferData(mega::MegaApi *api, mega::MegaTransfer *transfer, char *buffer, size_t size); protected: mega::MegaApi *megaApi; mega::MegaTransferListener *listener; }; class MegaCmdFatalErrorListener : public mega::MegaGlobalListener { MegaCmdSandbox& mCmdSandbox; static std::string_view getFatalErrorStr(int64_t fatalErrorType); template mega::MegaRequestListener* createLogoutListener(std::string_view msg); void onEvent(mega::MegaApi *api, mega::MegaEvent *event) override; public: MegaCmdFatalErrorListener(MegaCmdSandbox& cmdSandbox); }; } //end namespace #endif // LISTENERS_H MEGAcmd-2.5.2_Linux/src/loader/000077500000000000000000000000001516543156300161425ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/src/loader/megacmdloader.cpp000066400000000000000000000012321516543156300214300ustar00rootroot00000000000000/** * @file src/loader/megacmdloader.cpp * @brief MEGAcmdClient: Loader application of MEGAcmd for MAC * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of the MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include int main(int argc, char *argv[]) { execl("/Applications/MEGAcmd.app/Contents/MacOS/mega-cmd", NULL); return 0; } MEGAcmd-2.5.2_Linux/src/megacmd.cpp000066400000000000000000006514421516543156300170110ustar00rootroot00000000000000/** * @file src/megacmd.cpp * @brief MEGAcmd: Interactive CLI and service application * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of the MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include "mega/log_level.h" #include "megacmdcommonutils.h" #include "megacmd.h" #include "megaapi.h" #include "megacmdsandbox.h" #include "megacmdexecuter.h" #include "megacmdutils.h" #include "configurationmanager.h" #include "megacmdlogger.h" #include "comunicationsmanager.h" #include "listeners.h" #include "megacmd_fuse.h" #include "sync_command.h" #include "megacmdplatform.h" #include "megacmdversion.h" #ifdef MEGACMD_TESTING_CODE #include "../tests/common/Instruments.h" #endif #define USE_VARARGS #define PREFER_STDARG #include #include #include #include #ifdef __linux__ #include #endif #ifndef _WIN32 #include "signal.h" #include #else #include #include #include #include #include #include #define strdup _strdup // avoid warning #endif #ifdef __APPLE__ #include #endif #if !defined (PARAMS) # if defined (__STDC__) || defined (__GNUC__) || defined (__cplusplus) # define PARAMS(protos) protos # else # define PARAMS(protos) () # endif #endif #define SSTR( x ) static_cast< const std::ostringstream & >( \ ( std::ostringstream() << std::dec << x ) ).str() #ifndef ERRNO #ifdef _WIN32 #include #define ERRNO WSAGetLastError() #else #define ERRNO errno #endif #endif #ifdef _WIN32 #include "comunicationsmanagernamedpipes.h" #define COMUNICATIONMANAGER ComunicationsManagerNamedPipes #else #include "comunicationsmanagerfilesockets.h" #define COMUNICATIONMANAGER ComunicationsManagerFileSockets #include #endif using namespace mega; namespace megacmd { using namespace std; typedef char *completionfunction_t PARAMS((const char *, int)); MegaCmdExecuter *cmdexecuter; MegaCmdSandbox *sandboxCMD; MegaSemaphore semaphoreClients; //to limit max parallel petitions MegaApi *api = nullptr; //api objects for folderlinks std::queue apiFolders; std::vector occupiedapiFolders; MegaSemaphore semaphoreapiFolders; std::mutex mutexapiFolders; MegaCmdLogger *loggerCMD; std::mutex mutexEndedPetitionThreads; std::vector> petitionThreads; std::vector endedPetitionThreads; MegaThread *threadRetryConnections; std::mutex greetingsmsgsMutex; std::deque greetingsFirstClientMsgs; // to be given on first client to register as state listener std::deque greetingsAllClientMsgs; // to be given on all clients when registering as state listener std::mutex delayedBroadcastMutex; std::deque delayedBroadCastMessages; // messages to be brodcasted in a while //Comunications Manager ComunicationsManager * cm; // global listener MegaCmdGlobalListener* megaCmdGlobalListener; MegaCmdMegaListener* megaCmdMegaListener; vector validCommands = allValidCommands; // password change-related state information string oldpasswd; string newpasswd; std::atomic_bool doExit = false; bool consoleFailed = false; bool alreadyCheckingForUpdates = false; std::atomic_bool stopCheckingforUpdaters = false; string dynamicprompt = "MEGA CMD> "; static prompttype prompt = COMMAND; static std::atomic_bool loginInAtStartup(false); static std::atomic blocked(0); time_t lastTimeCheckBlockStatus = 0; static std::atomic<::mega::m_time_t> timeOfLoginInAtStartup(0); ::mega::m_time_t timeLoginStarted(); // local console Console* console; std::mutex mutexHistory; map threadline; char ** mcmdMainArgv; int mcmdMainArgc; void printWelcomeMsg(); void delete_finished_threads(); void appendGreetingStatusFirstListener(const std::string &msj) { std::lock_guard g(greetingsmsgsMutex); greetingsFirstClientMsgs.push_front(msj); } void removeGreetingStatusFirstListener(const std::string &msj) { std::lock_guard g(greetingsmsgsMutex); for(auto it = greetingsFirstClientMsgs.begin(); it != greetingsFirstClientMsgs.end();) { if (*it == msj) { it = greetingsFirstClientMsgs.erase(it); } else { ++it; } } } void appendGreetingStatusAllListener(const std::string &msj) { std::lock_guard g(greetingsmsgsMutex); greetingsAllClientMsgs.push_front(msj); } void clearGreetingStatusAllListener() { std::lock_guard g(greetingsmsgsMutex); greetingsAllClientMsgs.clear(); } void clearGreetingStatusFirstListener() { std::lock_guard g(greetingsmsgsMutex); greetingsFirstClientMsgs.clear(); } void removeGreetingStatusAllListener(const std::string &msj) { std::lock_guard g(greetingsmsgsMutex); for(auto it = greetingsAllClientMsgs.begin(); it != greetingsAllClientMsgs.end();) { if (*it == msj) { it = greetingsAllClientMsgs.erase(it); } else { ++it; } } } string getCurrentThreadLine() { uint64_t currentThread = MegaThread::currentThreadId(); if (threadline.find(currentThread) == threadline.end()) { // not found thread return string(); } else { return threadline[currentThread]; } } void setCurrentThreadLine(string s) { threadline[MegaThread::currentThreadId()] = s; } void setCurrentThreadLine(const vector& vec) { setCurrentThreadLine(joinStrings(vec)); } void sigint_handler(int signum) { LOG_verbose << "Received signal: " << signum; LOG_debug << "Exiting due to SIGINT"; stopCheckingforUpdaters = true; doExit = true; } #ifdef _WIN32 BOOL __stdcall CtrlHandler( DWORD fdwCtrlType ) { LOG_verbose << "Reached CtrlHandler: " << fdwCtrlType; switch( fdwCtrlType ) { // Handle the CTRL-C signal. case CTRL_C_EVENT: sigint_handler((int)fdwCtrlType); return( TRUE ); default: return FALSE; } } #endif prompttype getprompt() { return prompt; } void setprompt(prompttype p, string arg) { prompt = p; if (p == COMMAND) { console->setecho(true); } else { if (arg.size()) { OUTSTREAM << arg << flush; } else { OUTSTREAM << prompts[p] << flush; } console->setecho(false); } } void changeprompt(const char *newprompt) { dynamicprompt = newprompt; string s = "prompt:"; s+=dynamicprompt; cm->informStateListeners(s); } void informStateListeners(string s) { cm->informStateListeners(s); } void informStateListener(string message, int clientID) { string s; if (message.size()) { s += "message:"; s+=message; cm->informStateListenerByClientId(s, clientID); } } void broadcastMessage(string message, bool keepIfNoListeners) { string s; if (message.size()) { s += "message:"; s+=message; string unalteredCopy(s); if (!cm->informStateListeners(s) && keepIfNoListeners) { appendGreetingStatusFirstListener(unalteredCopy); } } } void removeDelayedBroadcastMatching(const string &toMatch) { std::lock_guard g(delayedBroadcastMutex); for (auto it = delayedBroadCastMessages.begin(); it != delayedBroadCastMessages.end(); ) { if ((*it).find(toMatch) != string::npos) { it = delayedBroadCastMessages.erase(it); } else { ++it; } } } void removeGreetingMatching(const string &toMatch) { std::lock_guard g(greetingsmsgsMutex); for (auto it = greetingsAllClientMsgs.begin(); it != greetingsAllClientMsgs.end(); ) { if ((*it).find(toMatch) != string::npos) { it = greetingsAllClientMsgs.erase(it); } else { ++it; } } } void broadcastDelayedMessage(string message, bool keepIfNoListeners) { static bool ongoing = false; std::lock_guard g(delayedBroadcastMutex); delayedBroadCastMessages.push_back(message); if (!ongoing) { ongoing = true; std::thread([keepIfNoListeners]() { sleepSeconds(4); std::unique_lock g(delayedBroadcastMutex); while (!delayedBroadCastMessages.empty()) { auto msg = std::move(delayedBroadCastMessages.front()); delayedBroadCastMessages.pop_front(); g.unlock(); // Broadcast without mutex locked broadcastMessage(msg, keepIfNoListeners); g.lock(); } ongoing = false; }).detach(); } } void informTransferUpdate(MegaTransfer *transfer, int clientID) { informProgressUpdate(transfer->getTransferredBytes(),transfer->getTotalBytes(), clientID); } void informStateListenerByClientId(int clientID, string s) { cm->informStateListenerByClientId(s, clientID); } void informProgressUpdate(long long transferred, long long total, int clientID, string title) { string s = "progress:"; s+=SSTR(transferred); s+=":"; s+=SSTR(total); if (title.size()) { s+=":"; s+=title; } informStateListenerByClientId(clientID, s); } void insertValidParamsPerCommand(set *validParams, string thecommand, set *validOptValues = nullptr, bool skipDeprecated = false) { if (!validOptValues) { validOptValues = validParams; } validOptValues->insert("client-width"); if ("ls" == thecommand) { validParams->insert("R"); validParams->insert("r"); validParams->insert("l"); validParams->insert("a"); validParams->insert("h"); validParams->insert("show-handles"); validParams->insert("versions"); validParams->insert("show-creation-time"); validOptValues->insert("time-format"); validParams->insert("tree"); #ifdef USE_PCRE validParams->insert("use-pcre"); #endif } else if ("passwd" == thecommand) { validParams->insert("f"); validOptValues->insert("auth-code"); } else if ("du" == thecommand) { validParams->insert("h"); validParams->insert("versions"); validOptValues->insert("path-display-size"); #ifdef USE_PCRE validParams->insert("use-pcre"); #endif } else if ("help" == thecommand) { validParams->insert("f"); validParams->insert("ff"); validParams->insert("non-interactive"); validParams->insert("upgrade"); validParams->insert("paths"); validParams->insert("show-all-options"); #ifdef _WIN32 validParams->insert("unicode"); #endif } else if ("version" == thecommand) { validParams->insert("l"); validParams->insert("c"); } else if ("rm" == thecommand) { validParams->insert("r"); validParams->insert("f"); #ifdef USE_PCRE validParams->insert("use-pcre"); #endif } else if ("mv" == thecommand) { #ifdef USE_PCRE validParams->insert("use-pcre"); #endif } else if ("cp" == thecommand) { #ifdef USE_PCRE validParams->insert("use-pcre"); #endif } else if ("speedlimit" == thecommand) { validParams->insert("u"); validParams->insert("d"); validParams->insert("h"); validParams->insert("upload-connections"); validParams->insert("download-connections"); } else if ("whoami" == thecommand) { validParams->insert("l"); } else if ("df" == thecommand) { validParams->insert("h"); } else if ("mediainfo" == thecommand) { validOptValues->insert("path-display-size"); } else if ("log" == thecommand) { validParams->insert("c"); validParams->insert("s"); } #ifndef _WIN32 else if ("permissions" == thecommand) { validParams->insert("s"); validParams->insert("files"); validParams->insert("folders"); } #endif else if ("deleteversions" == thecommand) { validParams->insert("all"); validParams->insert("f"); #ifdef USE_PCRE validParams->insert("use-pcre"); #endif } else if ("exclude" == thecommand) { validParams->insert("a"); validParams->insert("d"); } #ifdef HAVE_LIBUV else if ("webdav" == thecommand) { validParams->insert("d"); validParams->insert("all"); validParams->insert("tls"); validParams->insert("public"); validOptValues->insert("port"); validOptValues->insert("certificate"); validOptValues->insert("key"); #ifdef USE_PCRE validParams->insert("use-pcre"); #endif } else if ("ftp" == thecommand) { validParams->insert("d"); validParams->insert("all"); validParams->insert("tls"); validParams->insert("public"); validOptValues->insert("port"); validOptValues->insert("data-ports"); validOptValues->insert("certificate"); validOptValues->insert("key"); #ifdef USE_PCRE validParams->insert("use-pcre"); #endif } #endif else if ("backup" == thecommand) { validOptValues->insert("period"); validOptValues->insert("num-backups"); validParams->insert("d"); // validParams->insert("s"); // validParams->insert("r"); validParams->insert("a"); // validParams->insert("i"); validParams->insert("l"); validParams->insert("h"); validOptValues->insert("path-display-size"); validOptValues->insert("time-format"); } else if ("sync" == thecommand) { validParams->insert("p"); validParams->insert("pause"); validParams->insert("e"); validParams->insert("enable"); validParams->insert("d"); validParams->insert("delete"); validParams->insert("show-handles"); validOptValues->insert("path-display-size"); validOptValues->insert("col-separator"); validOptValues->insert("output-cols"); if (!skipDeprecated) { // Deprecated (kept for backwards compatibility) validParams->insert("remove"); validParams->insert("s"); validParams->insert("disable"); validParams->insert("r"); } } else if ("sync-issues" == thecommand) { validParams->insert("enable-warning"); validParams->insert("disable-warning"); validParams->insert("disable-path-collapse"); validParams->insert("detail"); validParams->insert("all"); validOptValues->insert("limit"); validOptValues->insert("col-separator"); validOptValues->insert("output-cols"); } else if ("sync-ignore" == thecommand) { validParams->insert("show"); validParams->insert("add"); validParams->insert("add-exclusion"); validParams->insert("remove"); validParams->insert("remove-exclusion"); } else if ("sync-config" == thecommand) { validParams->insert("delayed-uploads-wait-seconds"); validParams->insert("delayed-uploads-max-attempts"); } else if ("configure" == thecommand) { } else if ("export" == thecommand) { validParams->insert("a"); validParams->insert("d"); validParams->insert("f"); validParams->insert("writable"); validParams->insert("mega-hosted"); validOptValues->insert("expire"); validOptValues->insert("password"); #ifdef USE_PCRE validParams->insert("use-pcre"); #endif } else if ("share" == thecommand) { validParams->insert("a"); validParams->insert("d"); validParams->insert("p"); validOptValues->insert("with"); validOptValues->insert("level"); validOptValues->insert("personal-representation"); #ifdef USE_PCRE validParams->insert("use-pcre"); #endif validOptValues->insert("time-format"); } else if ("find" == thecommand) { validOptValues->insert("pattern"); validOptValues->insert("l"); validParams->insert("show-handles"); validParams->insert("print-only-handles"); #ifdef USE_PCRE validParams->insert("use-pcre"); #endif validOptValues->insert("mtime"); validOptValues->insert("size"); validOptValues->insert("time-format"); validOptValues->insert("type"); } else if ("mkdir" == thecommand) { validParams->insert("p"); } else if ("users" == thecommand) { validParams->insert("help-verify"); validParams->insert("verify"); validParams->insert("unverify"); validParams->insert("s"); validParams->insert("h"); validParams->insert("d"); validParams->insert("n"); validOptValues->insert("time-format"); } else if ("killsession" == thecommand) { validParams->insert("a"); } else if ("invite" == thecommand) { validParams->insert("d"); validParams->insert("r"); validOptValues->insert("message"); } else if ("signup" == thecommand) { validOptValues->insert("name"); } else if ("logout" == thecommand) { validParams->insert("keep-session"); } else if ("attr" == thecommand) { validParams->insert("d"); validParams->insert("s"); validParams->insert("force-non-official"); validParams->insert("print-only-value"); } else if ("userattr" == thecommand) { validOptValues->insert("user"); validParams->insert("s"); validParams->insert("list"); } else if ("ipc" == thecommand) { validParams->insert("a"); validParams->insert("d"); validParams->insert("i"); } else if ("showpcr" == thecommand) { validParams->insert("in"); validParams->insert("out"); validOptValues->insert("time-format"); } else if ("thumbnail" == thecommand) { validParams->insert("s"); } else if ("preview" == thecommand) { validParams->insert("s"); } else if ("put" == thecommand) { validParams->insert("c"); validParams->insert("q"); validParams->insert("print-tag-at-start"); validParams->insert("ignore-quota-warn"); //deprecated: no use validOptValues->insert("clientID"); } else if ("get" == thecommand) { validParams->insert("m"); validParams->insert("q"); validParams->insert("ignore-quota-warn"); validOptValues->insert("password"); #ifdef USE_PCRE validParams->insert("use-pcre"); #endif validOptValues->insert("clientID"); } else if ("import" == thecommand) { validOptValues->insert("password"); } else if ("login" == thecommand) { validOptValues->insert("clientID"); validOptValues->insert("auth-code"); validOptValues->insert("auth-key"); validOptValues->insert("password"); validOptValues->insert("resume"); } else if ("psa" == thecommand) { validParams->insert("discard"); } else if ("reload" == thecommand) { validOptValues->insert("clientID"); } else if ("transfers" == thecommand) { validParams->insert("show-completed"); validParams->insert("summary"); validParams->insert("only-uploads"); validParams->insert("only-completed"); validParams->insert("only-downloads"); validParams->insert("show-syncs"); validParams->insert("c"); validParams->insert("a"); validParams->insert("p"); validParams->insert("r"); validOptValues->insert("limit"); validOptValues->insert("path-display-size"); validOptValues->insert("col-separator"); validOptValues->insert("output-cols"); } else if ("proxy" == thecommand) { validParams->insert("auto"); validParams->insert("none"); validOptValues->insert("username"); validOptValues->insert("password"); } else if ("confirm" == thecommand) { validParams->insert("security"); } else if ("exit" == thecommand || "quit" == thecommand) { validParams->insert("only-shell"); } #if defined(_WIN32) || defined(__APPLE__) else if ("update" == thecommand) { validOptValues->insert("auto"); } #endif else if ("tree" == thecommand) { validParams->insert("show-handles"); } #ifdef WITH_FUSE if (thecommand == "fuse-add") { validParams->emplace("disabled"); validParams->emplace("transient"); validParams->emplace("read-only"); validOptValues->emplace("name"); } else if (thecommand == "fuse-enable") { validParams->emplace("temporarily"); } else if (thecommand == "fuse-disable") { validParams->emplace("temporarily"); } else if (thecommand == "fuse-show") { validParams->emplace("only-enabled"); validParams->emplace("disable-path-collapse"); validOptValues->emplace("limit"); } else if (thecommand == "fuse-config") { validOptValues->emplace("name"); validOptValues->emplace("enable-at-startup"); validOptValues->emplace("persistent"); validOptValues->emplace("read-only"); } #endif #if defined(DEBUG) || defined(MEGACMD_TESTING_CODE) else if ("echo" == thecommand) { validParams->insert("log-as-err"); } #endif } void escapeEspace(string &orig) { replaceAll(orig," ", "\\ "); } void unescapeEspace(string &orig) { replaceAll(orig,"\\ ", " "); } char* empty_completion(const char* text, int state) { // we offer 2 different options so that it doesn't complete (no space is inserted) if (state == 0) { return strdup(" "); } if (state == 1) { return strdup(text); } return NULL; } char* generic_completion(const char* text, int state, vector validOptions) { static size_t list_index, len; static bool foundone; string name; if (!validOptions.size()) // no matches { return empty_completion(text,state); //dont fall back to filenames } if (!state) { list_index = 0; foundone = false; len = strlen(text); } while (list_index < validOptions.size()) { name = validOptions.at(list_index); //Notice: do not escape options for cmdshell. Plus, we won't filter here, because we don't know if the value of rl_completion_quote_chararcter of cmdshell // The filtering and escaping will be performed by the completion function in cmdshell if (isCurrentThreadInteractive() && !isCurrentThreadCmdShell()) { escapeEspace(name); } list_index++; if (!( strcmp(text, "")) || (( name.size() >= len ) && ( strlen(text) >= len ) && ( name.find(text) == 0 ) ) || isCurrentThreadCmdShell() //do not filter if cmdshell (it will be filter there) ) { foundone = true; return dupstr((char*)name.c_str()); } } if (!foundone) { return empty_completion(text,state); //dont fall back to filenames } return((char*)NULL ); } char* commands_completion(const char* text, int state) { return generic_completion(text, state, validCommands); } char* local_completion(const char* text, int state) { return((char*)NULL ); } void addGlobalFlags(set *setvalidparams) { for (auto &v : validGlobalParameters) { setvalidparams->insert(v); } } char * flags_completion(const char*text, int state) { static vector validparams; if (state == 0) { validparams.clear(); string saved_line = getCurrentThreadLine(); vector words = getlistOfWords(saved_line.c_str(), !isCurrentThreadCmdShell()); if (words.size()) { set setvalidparams; set setvalidOptValues; addGlobalFlags(&setvalidparams); string thecommand = words[0]; insertValidParamsPerCommand(&setvalidparams, thecommand, &setvalidOptValues, true /* skipDeprecated*/); set::iterator it; for (it = setvalidparams.begin(); it != setvalidparams.end(); it++) { string param = *it; string toinsert; if (param.size() > 1) { toinsert = "--" + param; } else { toinsert = "-" + param; } validparams.push_back(toinsert); } for (it = setvalidOptValues.begin(); it != setvalidOptValues.end(); it++) { string param = *it; string toinsert; if (param.size() > 1) { toinsert = "--" + param + '='; } else { toinsert = "-" + param + '='; } validparams.push_back(toinsert); } } } char *toret = generic_completion(text, state, validparams); return toret; } char * flags_value_completion(const char*text, int state) { static vector validValues; if (state == 0) { validValues.clear(); string saved_line = getCurrentThreadLine(); vector words = getlistOfWords(saved_line.c_str(), !isCurrentThreadCmdShell()); if (words.size() > 1) { string thecommand = words[0]; string currentFlag = words[words.size() - 1]; map cloptions; map clflags; set validParams; insertValidParamsPerCommand(&validParams, thecommand, nullptr /* validOptValues */, true /* skipDeprecated */); if (setOptionsAndFlags(&cloptions, &clflags, &words, validParams, true)) { // return invalid?? } if (currentFlag.find("--time-format=") == 0) { string prefix = strncmp(text, "--time-format=", strlen("--time-format="))?"":"--time-format="; for (int i = 0; i < MCMDTIME_TOTAL; i++) { validValues.push_back(prefix+getTimeFormatNameFromId(i)); } } if (thecommand == "share") { if (currentFlag.find("--level=") == 0) { string prefix = strncmp(text, "--level=", strlen("--level="))?"":"--level="; validValues.push_back(prefix+getShareLevelStr(MegaShare::ACCESS_UNKNOWN)); validValues.push_back(prefix+getShareLevelStr(MegaShare::ACCESS_READ)); validValues.push_back(prefix+getShareLevelStr(MegaShare::ACCESS_READWRITE)); validValues.push_back(prefix+getShareLevelStr(MegaShare::ACCESS_FULL)); validValues.push_back(prefix+getShareLevelStr(MegaShare::ACCESS_OWNER)); validValues.push_back(prefix+getShareLevelStr(MegaShare::ACCESS_UNKNOWN)); } if (currentFlag.find("--with=") == 0) { validValues = cmdexecuter->getlistusers(); string prefix = strncmp(text, "--with=", strlen("--with="))?"":"--with="; for (unsigned int i=0;i validpaths; if (state == 0) { string wildtext(text); bool usepcre = false; //pcre makes no sense in paths completion if (usepcre) { #ifdef USE_PCRE wildtext += "."; #elif __cplusplus >= 201103L wildtext += "."; #endif } wildtext += "*"; unescapeEspace(wildtext); validpaths = cmdexecuter->listpaths(usepcre, wildtext, onlyfolders); // we need to escape '\' to fit what's done when parsing words if (!isCurrentThreadCmdShell()) { for (int i = 0; i < (int)validpaths.size(); i++) { replaceAll(validpaths[i],"\\","\\\\"); } } } return generic_completion(text, state, validpaths); } char* remotepaths_completion(const char* text, int state) { return remotepaths_completion(text, state, false); } char* remotefolders_completion(const char* text, int state) { return remotepaths_completion(text, state, true); } char* loglevels_completion(const char* text, int state) { static vector validloglevels; if (state == 0) { validloglevels.push_back(getLogLevelStr(MegaApi::LOG_LEVEL_FATAL)); validloglevels.push_back(getLogLevelStr(MegaApi::LOG_LEVEL_ERROR)); validloglevels.push_back(getLogLevelStr(MegaApi::LOG_LEVEL_WARNING)); validloglevels.push_back(getLogLevelStr(MegaApi::LOG_LEVEL_INFO)); validloglevels.push_back(getLogLevelStr(MegaApi::LOG_LEVEL_DEBUG)); validloglevels.push_back(getLogLevelStr(MegaApi::LOG_LEVEL_MAX)); } return generic_completion(text, state, validloglevels); } char* localfolders_completion(const char* text, int state) { static vector validpaths; if (state == 0) { string what(text); unescapeEspace(what); validpaths = cmdexecuter->listLocalPathsStartingBy(what, true); } return generic_completion(text, state, validpaths); } char* transfertags_completion(const char* text, int state) { static vector validtransfertags; if (state == 0) { std::unique_ptr transferdata(api->getTransferData()); if (transferdata) { for (int i = 0; i < transferdata->getNumUploads(); i++) { validtransfertags.push_back(SSTR(transferdata->getUploadTag(i))); } for (int i = 0; i < transferdata->getNumDownloads(); i++) { validtransfertags.push_back(SSTR(transferdata->getDownloadTag(i))); } // TODO: reconsider including completed transfers (sth like this:) // globalTransferListener->completedTransfersMutex.lock(); // for (unsigned int i = 0;i < globalTransferListener->completedTransfers.size() && shownCompleted < limit; i++) // { // MegaTransfer *transfer = globalTransferListener->completedTransfers.at(shownCompleted); // if (!transfer->isSyncTransfer()) // { // validtransfertags.push_back(SSTR(transfer->getTag())); // shownCompleted++; // } // } // globalTransferListener->completedTransfersMutex.unlock(); } } return generic_completion(text, state, validtransfertags); } char* config_completion(const char* text, int state) { static vector validkeys = [](){ static vector keys; for (auto &vc : Instance::Get().getConfigurators()) { keys.push_back(vc.mKey); } return keys; }(); return generic_completion(text, state, validkeys); } char* contacts_completion(const char* text, int state) { static vector validcontacts; if (state == 0) { validcontacts = cmdexecuter->getlistusers(); } return generic_completion(text, state, validcontacts); } char* sessions_completion(const char* text, int state) { static vector validSessions; if (state == 0) { validSessions = cmdexecuter->getsessions(); } if (validSessions.size() == 0) { return empty_completion(text, state); } return generic_completion(text, state, validSessions); } char* nodeattrs_completion(const char* text, int state) { static vector validAttrs; if (state == 0) { validAttrs.clear(); string saved_line = getCurrentThreadLine(); vector words = getlistOfWords(saved_line.c_str(), !isCurrentThreadCmdShell()); if (words.size() > 1) { validAttrs = cmdexecuter->getNodeAttrs(words[1]); } } if (validAttrs.size() == 0) { return empty_completion(text, state); } return generic_completion(text, state, validAttrs); } char* userattrs_completion(const char* text, int state) { static vector validAttrs; if (state == 0) { validAttrs.clear(); validAttrs = cmdexecuter->getUserAttrs(); } if (validAttrs.size() == 0) { return empty_completion(text, state); } return generic_completion(text, state, validAttrs); } completionfunction_t *getCompletionFunction(vector words) { // Strip words without flags string thecommand = words[0]; if (words.size() > 1) { string lastword = words[words.size() - 1]; if (lastword.find_first_of("-") == 0) { if (lastword.find_last_of("=") != string::npos) { return flags_value_completion; } else { return flags_completion; } } } discardOptionsAndFlags(&words); int currentparameter = int(words.size() - 1); if (stringcontained(thecommand.c_str(), localremotefolderpatterncommands)) { if (currentparameter == 1) { return local_completion; } if (currentparameter == 2) { return remotefolders_completion; } } else if (thecommand == "put") { if (currentparameter == 1) { return local_completion; } else { return remotepaths_completion; } } else if (thecommand == "backup") { if (currentparameter == 1) { return localfolders_completion; } else { return remotefolders_completion; } } else if (stringcontained(thecommand.c_str(), remotepatterncommands)) { if (currentparameter == 1) { return remotepaths_completion; } } else if (stringcontained(thecommand.c_str(), remotefolderspatterncommands)) { if (currentparameter == 1) { return remotefolders_completion; } } else if (stringcontained(thecommand.c_str(), multipleremotepatterncommands)) { if (currentparameter >= 1) { return remotepaths_completion; } } else if (stringcontained(thecommand.c_str(), localfolderpatterncommands)) { if (currentparameter == 1) { return localfolders_completion; } } else if (stringcontained(thecommand.c_str(), remoteremotepatterncommands)) { if (( currentparameter == 1 ) || ( currentparameter == 2 )) { return remotepaths_completion; } } else if (stringcontained(thecommand.c_str(), remotelocalpatterncommands)) { if (currentparameter == 1) { return remotepaths_completion; } if (currentparameter == 2) { return local_completion; } } else if (stringcontained(thecommand.c_str(), emailpatterncommands)) { if (currentparameter == 1) { return contacts_completion; } } else if (thecommand == "import") { if (currentparameter == 2) { return remotepaths_completion; } } else if (thecommand == "killsession") { if (currentparameter == 1) { return sessions_completion; } } else if (thecommand == "attr") { if (currentparameter == 1) { return remotepaths_completion; } if (currentparameter == 2) { return nodeattrs_completion; } } else if (thecommand == "userattr") { if (currentparameter == 1) { return userattrs_completion; } } else if (thecommand == "log") { if (currentparameter == 1) { return loglevels_completion; } } else if (thecommand == "transfers") { if (currentparameter == 1) { return transfertags_completion; } } else if (thecommand == "configure") { if (currentparameter == 1) { return config_completion; } } return empty_completion; } string getListOfCompletionValues(vector words, char separator = ' ', const char * separators = " :;!`\"'\\()[]{}<>", bool suppressflag = true) { string completionValues; completionfunction_t * compfunction = getCompletionFunction(words); if (compfunction == local_completion) { if (!isCurrentThreadInteractive()) { return "MEGACMD_USE_LOCAL_COMPLETION"; } else { string toret="MEGACMD_USE_LOCAL_COMPLETION"; toret+=cmdexecuter->getLPWD(); return toret; } } #ifdef _WIN32 // // let MEGAcmdShell handle the local folder completion (available via autocomplete.cpp stuff that takes into account units/unicode/etc...) // else if (compfunction == localfolders_completion) // { // if (!interactiveThread()) // { // return "MEGACMD_USE_LOCAL_COMPLETIONFOLDERS"; // } // else // { // string toret="MEGACMD_USE_LOCAL_COMPLETIONFOLDERS"; // toret+=cmdexecuter->getLPWD(); // return toret; // } // } #endif int state=0; if (words.size()>1) while (true) { char *newval; string &lastword = words[words.size()-1]; if (suppressflag && lastword.size()>3 && lastword[0]== '-' && lastword[1]== '-' && lastword.find('=')!=string::npos) { newval = compfunction(lastword.substr(lastword.find_first_of('=')+1).c_str(), state); } else { newval = compfunction(lastword.c_str(), state); } if (!newval) break; if (completionValues.size()) { completionValues+=separator; } string snewval=newval; if (snewval.find_first_of(separators) != string::npos) { completionValues+="\""; replaceAll(snewval,"\"","\\\""); completionValues+=snewval; completionValues+="\""; } else { completionValues+=newval; } free(newval); state++; } return completionValues; } MegaApi* getFreeApiFolder() { { std::lock_guard g(mutexapiFolders); if (apiFolders.empty() && occupiedapiFolders.empty()) { return nullptr; } } semaphoreapiFolders.wait(); mutexapiFolders.lock(); MegaApi* toret = apiFolders.front(); apiFolders.pop(); occupiedapiFolders.push_back(toret); mutexapiFolders.unlock(); return toret; } void freeApiFolder(MegaApi *apiFolder) { mutexapiFolders.lock(); occupiedapiFolders.erase(std::remove(occupiedapiFolders.begin(), occupiedapiFolders.end(), apiFolder), occupiedapiFolders.end()); apiFolders.push(apiFolder); semaphoreapiFolders.release(); mutexapiFolders.unlock(); } const char * getUsageStr(const char *command, const HelpFlags& flags) { if (!strcmp(command, "login")) { if (isCurrentThreadInteractive()) { return "login [--auth-code=XXXX] [email [password]] | exportedfolderurl#key" " [--auth-key=XXXX] [--resume] | passwordprotectedlink [--password=PASSWORD]" " | session"; } else { return "login [--auth-code=XXXX] email password | exportedfolderurl#key" " [--auth-key=XXXX] [--resume] | passwordprotectedlink [--password=PASSWORD]" " | session"; } } if (!strcmp(command, "psa")) { return "psa [--discard]"; } if (!strcmp(command, "cancel")) { return "cancel"; } if (!strcmp(command, "confirmcancel")) { if (isCurrentThreadInteractive()) { return "confirmcancel link [password]"; } else { return "confirmcancel link password"; } } if (!strcmp(command, "begin")) { return "begin [ephemeralhandle#ephemeralpw]"; } if (!strcmp(command, "signup")) { if (isCurrentThreadInteractive()) { return "signup email [password] [--name=\"Your Name\"]"; } else { return "signup email password [--name=\"Your Name\"]"; } } if (!strcmp(command, "confirm")) { if (isCurrentThreadInteractive()) { return "confirm link email [password]"; } else { return "confirm link email password"; } } if (!strcmp(command, "errorcode")) { return "errorcode number"; } if (!strcmp(command, "graphics")) { return "graphics [on|off]"; } if (!strcmp(command, "session")) { return "session"; } if (!strcmp(command, "mount")) { return "mount"; } if (((flags.win && !flags.readline) || flags.showAll) && !strcmp(command, "unicode")) { return "unicode"; } if (!strcmp(command, "ls")) { if (flags.usePcre || flags.showAll) { return "ls [-halRr] [--show-handles] [--tree] [--versions] [remotepath] [--use-pcre] [--show-creation-time] [--time-format=FORMAT]"; } else { return "ls [-halRr] [--show-handles] [--tree] [--versions] [remotepath] [--show-creation-time] [--time-format=FORMAT]"; } } if (!strcmp(command, "tree")) { return "tree [remotepath]"; } if (!strcmp(command, "cd")) { return "cd [remotepath]"; } if (!strcmp(command, "log")) { return "log [-sc] level"; } if (!strcmp(command, "du")) { if (flags.usePcre || flags.showAll) { return "du [-h] [--versions] [remotepath remotepath2 remotepath3 ... ] [--use-pcre]"; } else { return "du [-h] [--versions] [remotepath remotepath2 remotepath3 ... ]"; } } if (!strcmp(command, "pwd")) { return "pwd"; } if (!strcmp(command, "lcd")) { return "lcd [localpath]"; } if (!strcmp(command, "lpwd")) { return "lpwd"; } if (!strcmp(command, "import")) { return "import exportedlink [--password=PASSWORD] [remotepath]"; } if (!strcmp(command, "put")) { return "put [-c] [-q] [--print-tag-at-start] localfile [localfile2 localfile3 ...] [dstremotepath]"; } if (!strcmp(command, "putq")) { return "putq [cancelslot]"; } if (!strcmp(command, "get")) { if (flags.usePcre || flags.showAll) { return "get [-m] [-q] [--ignore-quota-warn] [--use-pcre] [--password=PASSWORD] exportedlink|remotepath [localpath]"; } else { return "get [-m] [-q] [--ignore-quota-warn] [--password=PASSWORD] exportedlink|remotepath [localpath]"; } } if (!strcmp(command, "getq")) { return "getq [cancelslot]"; } if (!strcmp(command, "pause")) { return "pause [get|put] [hard] [status]"; } if (!strcmp(command, "attr")) { return "attr remotepath [--force-non-official] [-s attribute value|-d attribute [--print-only-value]"; } if (!strcmp(command, "userattr")) { return "userattr [-s attribute value|attribute|--list] [--user=user@email]"; } if (!strcmp(command, "mkdir")) { return "mkdir [-p] remotepath"; } if (!strcmp(command, "rm")) { if (flags.usePcre || flags.showAll) { return "rm [-r] [-f] [--use-pcre] remotepath"; } else { return "rm [-r] [-f] remotepath"; } } if (!strcmp(command, "mv")) { if (flags.usePcre || flags.showAll) { return "mv srcremotepath [--use-pcre] [srcremotepath2 srcremotepath3 ..] dstremotepath"; } else { return "mv srcremotepath [srcremotepath2 srcremotepath3 ..] dstremotepath"; } } if (!strcmp(command, "cp")) { if (flags.usePcre || flags.showAll) { return "cp [--use-pcre] srcremotepath [srcremotepath2 srcremotepath3 ..] dstremotepath|dstemail:"; } else { return "cp srcremotepath [srcremotepath2 srcremotepath3 ..] dstremotepath|dstemail:"; } } if (!strcmp(command, "deleteversions")) { if (flags.usePcre || flags.showAll) { return "deleteversions [-f] (--all | remotepath1 remotepath2 ...) [--use-pcre]"; } else { return "deleteversions [-f] (--all | remotepath1 remotepath2 ...)"; } } if (!strcmp(command, "exclude")) { return "exclude [(-a|-d) pattern1 pattern2 pattern3]"; } if ((flags.haveLibuv || flags.showAll) && !strcmp(command, "webdav")) { if (flags.usePcre || flags.showAll) { return "webdav [-d (--all | remotepath ) ] [ remotepath [--port=PORT] [--public] [--tls --certificate=/path/to/certificate.pem --key=/path/to/certificate.key]] [--use-pcre]"; } else { return "webdav [-d (--all | remotepath ) ] [ remotepath [--port=PORT] [--public] [--tls --certificate=/path/to/certificate.pem --key=/path/to/certificate.key]]"; } } if ((flags.haveLibuv || flags.showAll) && !strcmp(command, "ftp")) { if (flags.usePcre || flags.showAll) { return "ftp [-d ( --all | remotepath ) ] [ remotepath [--port=PORT] [--data-ports=BEGIN-END] [--public] [--tls --certificate=/path/to/certificate.pem --key=/path/to/certificate.key]] [--use-pcre]"; } else { return "ftp [-d ( --all | remotepath ) ] [ remotepath [--port=PORT] [--data-ports=BEGIN-END] [--public] [--tls --certificate=/path/to/certificate.pem --key=/path/to/certificate.key]]"; } } if (!strcmp(command, "sync")) { return "sync [localpath dstremotepath| [-dpe] [ID|localpath]"; } if (!strcmp(command, "sync-issues")) { return "sync-issues [[--detail (ID|--all)] [--limit=rowcount] [--disable-path-collapse]] | [--enable-warning|--disable-warning]"; } if (!strcmp(command, "sync-ignore")) { return "sync-ignore [--show|[--add|--add-exclusion|--remove|--remove-exclusion] filter1 filter2 ...] (ID|localpath|DEFAULT)"; } if (!strcmp(command, "sync-config")) { return "sync-config [--delayed-uploads-wait-seconds | --delayed-uploads-max-attempts]"; } if (!strcmp(command, "configure")) { return "configure [key [value]]"; } if (!strcmp(command, "backup")) { return "backup (localpath remotepath --period=\"PERIODSTRING\" --num-backups=N | [-lhda] [TAG|localpath] [--period=\"PERIODSTRING\"] [--num-backups=N]) [--time-format=FORMAT]"; } if (!strcmp(command, "https")) { return "https [on|off]"; } if ((!flags.win || flags.showAll) && !strcmp(command, "permissions")) { return "permissions [(--files|--folders) [-s XXX]]"; } if (!strcmp(command, "export")) { return "export [-d|-a" " [--writable]" " [--mega-hosted]" " [--password=PASSWORD] [--expire=TIMEDELAY] [-f]] [remotepath]" #ifdef USE_PCRE " [--use-pcre]" #endif " [--time-format=FORMAT]"; } if (!strcmp(command, "share")) { if (flags.usePcre || flags.showAll) { return "share [-p] [-d|-a --with=user@email.com [--level=LEVEL]] [remotepath] [--use-pcre] [--time-format=FORMAT]"; } else { return "share [-p] [-d|-a --with=user@email.com [--level=LEVEL]] [remotepath] [--time-format=FORMAT]"; } } if (!strcmp(command, "invite")) { return "invite [-d|-r] dstemail [--message=\"MESSAGE\"]"; } if (!strcmp(command, "ipc")) { return "ipc email|handle -a|-d|-i"; } if (!strcmp(command, "showpcr")) { return "showpcr [--in | --out] [--time-format=FORMAT]"; } if (!strcmp(command, "masterkey")) { return "masterkey pathtosave"; } if (!strcmp(command, "users")) { return "users [-s] [-h] [-n] [-d contact@email] [--time-format=FORMAT] [--verify|--unverify contact@email.com] [--help-verify [contact@email.com]]"; } if (!strcmp(command, "getua")) { return "getua attrname [email]"; } if (!strcmp(command, "putua")) { return "putua attrname [del|set string|load file]"; } if (!strcmp(command, "speedlimit")) { return "speedlimit [-u|-d|--upload-connections|--download-connections] [-h] [NEWLIMIT]"; } if (!strcmp(command, "killsession")) { return "killsession [-a | sessionid1 sessionid2 ... ]"; } if (!strcmp(command, "whoami")) { return "whoami [-l]"; } if (!strcmp(command, "df")) { return "df [-h]"; } if (!strcmp(command, "proxy")) { return "proxy [URL|--auto|--none] [--username=USERNAME --password=PASSWORD]"; } if (!strcmp(command, "cat")) { return "cat remotepath1 remotepath2 ..."; } if (!strcmp(command, "mediainfo")) { return "mediainfo remotepath1 remotepath2 ..."; } if (!strcmp(command, "passwd")) { if (isCurrentThreadInteractive()) { return "passwd [-f] [--auth-code=XXXX] [newpassword]"; } else { return "passwd [-f] [--auth-code=XXXX] newpassword"; } } if (!strcmp(command, "retry")) { return "retry"; } if (!strcmp(command, "recon")) { return "recon"; } if (!strcmp(command, "reload")) { return "reload"; } if (!strcmp(command, "logout")) { return "logout [--keep-session]"; } if (!strcmp(command, "symlink")) { return "symlink"; } if (!strcmp(command, "version")) { return "version [-l][-c]"; } if (!strcmp(command, "debug")) { return "debug"; } if (!strcmp(command, "chatf")) { return "chatf "; } if (!strcmp(command, "chatc")) { return "chatc group [email ro|rw|full|op]*"; } if (!strcmp(command, "chati")) { return "chati chatid email ro|rw|full|op"; } if (!strcmp(command, "chatr")) { return "chatr chatid [email]"; } if (!strcmp(command, "chatu")) { return "chatu chatid"; } if (!strcmp(command, "chatga")) { return "chatga chatid nodehandle uid"; } if (!strcmp(command, "chatra")) { return "chatra chatid nodehandle uid"; } if (!strcmp(command, "exit")) { return "exit [--only-shell]"; } if (!strcmp(command, "quit")) { return "quit [--only-shell]"; } if (!strcmp(command, "history")) { return "history"; } if (!strcmp(command, "thumbnail")) { return "thumbnail [-s] remotepath localpath"; } if (!strcmp(command, "preview")) { return "preview [-s] remotepath localpath"; } if (!strcmp(command, "find")) { if (flags.usePcre || flags.showAll) { return "find [remotepath] [-l] [--pattern=PATTERN] [--type=d|f] [--mtime=TIMECONSTRAIN] [--size=SIZECONSTRAIN] [--use-pcre] [--time-format=FORMAT] [--show-handles|--print-only-handles]"; } else { return "find [remotepath] [-l] [--pattern=PATTERN] [--type=d|f] [--mtime=TIMECONSTRAIN] [--size=SIZECONSTRAIN] [--time-format=FORMAT] [--show-handles|--print-only-handles]"; } } if (!strcmp(command, "help")) { return "help [-f|-ff|--non-interactive|--upgrade|--paths] [--show-all-options]"; } if (!strcmp(command, "clear")) { return "clear"; } if (!strcmp(command, "transfers")) { return "transfers [-c TAG|-a] | [-r TAG|-a] | [-p TAG|-a] [--only-downloads | --only-uploads] [SHOWOPTIONS]"; } if (((flags.win && !flags.readline) || flags.showAll) && !strcmp(command, "autocomplete")) { return "autocomplete [dos | unix]"; } if (!strcmp(command, "codepage")) { return "codepage [N [M]]"; } if ((flags.win || flags.apple || flags.showAll) && !strcmp(command, "update")) { return "update [--auto=on|off|query]"; } if ((flags.fuse || flags.showAll) && !strcmp(command, "fuse-add")) { if (flags.win && !flags.showAll) { return "fuse-add [--name=name] [--disabled] [--transient] [--read-only] remotePath"; } return "fuse-add [--name=name] [--disabled] [--transient] [--read-only] localPath remotePath"; } if ((flags.fuse || flags.showAll) && !strcmp(command, "fuse-remove")) { return "fuse-remove (name|localPath)"; } if ((flags.fuse || flags.showAll) && !strcmp(command, "fuse-enable")) { return "fuse-enable [--temporarily] (name|localPath)"; } if ((flags.fuse || flags.showAll) && !strcmp(command, "fuse-disable")) { return "fuse-disable [--temporarily] (name|localPath)"; } if ((flags.fuse || flags.showAll) && !strcmp(command, "fuse-show")) { return "fuse-show [--only-enabled] [--disable-path-collapse] [[--limit=rowcount] | [name|localPath]]"; } if ((flags.fuse || flags.showAll) && !strcmp(command, "fuse-config")) { return "fuse-config [--name=name] [--enable-at-startup=yes|no] [--persistent=yes|no] [--read-only=yes|no] (name|localPath)"; } #if defined(DEBUG) || defined(MEGACMD_TESTING_CODE) else if (!strcmp(command, "echo")) { return "echo [--log-as-err]"; } #endif return "command not found: "; } bool validCommand(string thecommand) { return stringcontained((char*)thecommand.c_str(), validCommands); } string getsupportedregexps() { #ifdef USE_PCRE return "Perl Compatible Regular Expressions with \"--use-pcre\"\n or wildcarded expressions with ? or * like f*00?.txt"; #elif __cplusplus >= 201103L return "c++11 Regular Expressions"; #else return "it accepts wildcards: ? and *. e.g.: f*00?.txt"; #endif } void printTimeFormatHelp(ostringstream &os) { os << " --time-format=FORMAT" << "\t" << "show time in available formats. Examples:" << endl; os << " RFC2822: " << " Example: Fri, 06 Apr 2018 13:05:37 +0200" << endl; os << " ISO6081: " << " Example: 2018-04-06" << endl; os << " ISO6081_WITH_TIME: " << " Example: 2018-04-06T13:05:37" << endl; os << " SHORT: " << " Example: 06Apr2018 13:05:37" << endl; os << " SHORT_UTC: " << " Example: 06Apr2018 13:05:37" << endl; os << " CUSTOM. e.g: --time-format=\"%Y %b\": "<< " Example: 2018 Apr" << endl; os << " You can use any strftime compliant format: http://www.cplusplus.com/reference/ctime/strftime/" << endl; } void printColumnDisplayerHelp(ostringstream &os) { os << " --col-separator=X" << "\t" << "Uses the string \"X\" as column separator. Otherwise, spaces will be added between columns to align them." << endl; os << " --output-cols=COLUMN_NAME_1,COLUMN_NAME2,..." << "\t" << "Selects which columns to show and their order." << endl; } string getHelpStr(const char *command, const HelpFlags& flags = {}) { ostringstream os; os << "Usage: " << getUsageStr(command, flags) << endl; if (!strcmp(command, "login")) { os << "Logs into a MEGA account, folder link or a previous session. You can only log into one entity at a time." << endl; os << "Logging into a MEGA account:" << endl; os << "\tYou can log into a MEGA account by providing either a session ID or a username and password. A session " "ID simply identifies a session that you have previously logged in with using a username and password; " "logging in with a session ID simply resumes that session. If this is your first time logging in, you " "will need to do so with a username and password." << endl; os << "Options:" << endl; os << "\t--auth-code=XXXXXX: If you're logging in using a username and password, and this account has multifactor " "authentication (MFA) enabled, then this option allows you to pass the MFA token in directly rather than " "being prompted for it later on. For more information on this topic, please visit https://mega.nz/blog_48." << endl; os << endl; os << "Logging into a MEGA folder link (an exported/public folder):" << endl; os << "\tMEGA folder links have the form URL#KEY. To log into one, simply execute the login command with the link." << endl; os << "Options:" << endl; os << "\t--password=PASSWORD: If the link is a password protected link, then this option can be used to pass in " "the password for that link." << endl; os << "\t--auth-key=AUTHKEY: If the link is a writable folder link, then this option allows you to log in with " "write privileges. Without this option, you will log into the link with read access only." << endl; os << "\t--resume: A convenience option to try to resume from cache. When login into a folder, contrary to what occurs with login into a user account," " MEGAcmd will not try to load anything from cache: loading everything from scratch. This option changes that. Note, " "login using a session string, will of course, try to load from cache. This option may be convenient, for instance, if you previously " "logged out using --keep-session." << endl; os << endl; os << "For more information about MEGA folder links, see \"" << getCommandPrefixBasedOnMode() << "export --help\"." << endl; } else if (!strcmp(command, "cancel")) { os << "Cancels your MEGA account" << endl; os << " Caution: The account under this email address will be permanently closed" << endl; os << " and your data deleted. This can not be undone." << endl; os << endl; os << "The cancellation will not take place immediately. You will need to confirm the cancellation" << endl; os << "using a link that will be delivered to your email. See \"confirmcancel --help\"" << endl; } else if (!strcmp(command, "psa")) { os << "Shows the next available Public Service Announcement (PSA)" << endl; os << endl; os << "Options:" << endl; os << " --discard" << "\t" << "Discards last received PSA" << endl; os << endl; } else if (!strcmp(command, "confirmcancel")) { os << "Confirms the cancellation of your MEGA account" << endl; os << " Caution: The account under this email address will be permanently closed" << endl; os << " and your data deleted. This can not be undone." << endl; } else if (!strcmp(command, "errorcode")) { os << "Translate error code into string" << endl; } else if (!strcmp(command, "graphics")) { os << "Shows if special features related to images and videos are enabled. " << endl; os << "Use \"graphics on/off\" to enable/disable it." << endl; os << endl; os << "Disabling these features will avoid the upload of previews and thumbnails" << endl; os << "for images and videos." << endl; os << endl; os << "It's only recommended to disable these features before uploading files" << endl; os << "with image or video extensions that are not really images or videos," << endl; os << "or that are encrypted in the local drive so they can't be analyzed anyway." << endl; os << endl; os << "Notice that this setting will be saved for the next time you open MEGAcmd, but will be removed if you logout." << endl; } else if (!strcmp(command, "signup")) { os << "Register as user with a given email" << endl; os << endl; os << " Please, avoid using passwords containing \" or '" << endl; os << endl; os << "Options:" << endl; os << " --name=\"Your Name\"" << "\t" << "Name to register. e.g. \"John Smith\"" << endl; os << endl; os << " You will receive an email to confirm your account." << endl; os << " Once you have received the email, please proceed to confirm the link" << endl; os << " included in that email with \"confirm\"." << endl; os << endl; os << "Warning: Due to our end-to-end encryption paradigm, you will not be able to access your data" << endl; os << "without either your password or a backup of your Recovery Key (master key)." << endl; os << "Exporting the master key and keeping it in a secure location enables you" << endl; os << "to set a new password without data loss. Always keep physical control of" << endl; os << "your master key (e.g. on a client device, external storage, or print)." << endl; os << " See \"masterkey --help\" for further info." << endl; } else if (!strcmp(command, "clear")) { os << "Clear screen" << endl; } else if (!strcmp(command, "help")) { os << "Prints list of commands" << endl; os << endl; os << "Options:" << endl; os << " -f" << " \t" << "Include a brief description of the commands" << endl; os << " -ff" << "\t" << "Get a complete description of all commands" << endl; os << " --non-interactive" << " " << "Display information on how to use MEGAcmd with scripts" << endl; os << " --upgrade" << " " << "Display information on PRO plans" << endl; os << " --paths" << " " << "Show caveats of local and remote paths" << endl; os << " --show-all-options" << " " << "Display all options regardless of platform" << endl; } else if (!strcmp(command, "history")) { os << "Prints history of used commands" << endl; os << " Only commands used in interactive mode are registered" << endl; } else if (!strcmp(command, "confirm")) { os << "Confirm an account using the link provided after the \"signup\" process." << endl; os << " It requires the email and the password used to obtain the link." << endl; os << endl; } else if (!strcmp(command, "session")) { os << "Prints (secret) session ID" << endl; } else if (!strcmp(command, "mount")) { os << "Lists all the root nodes" << endl; os << endl; os << "This includes the root node in your cloud drive, Inbox, Rubbish Bin" << endl; os << "and all the in-shares (nodes shares to you from other users)" << endl; } else if (((flags.win && flags.readline) || flags.showAll) && !strcmp(command, "unicode")) { os << "Toggle unicode input enabled/disabled in interactive shell" << endl; os << endl; os << " Unicode mode is experimental, you might experience" << endl; os << " some issues interacting with the console" << endl; os << " (e.g. history navigation fails)." << endl; os << endl; os << "Type \"help --unicode\" for further info" << endl; os << "Note: this command is only available on some versions of Windows" << endl; } else if (!strcmp(command, "ls")) { os << "Lists files in a remote path" << endl; os << " remotepath can be a pattern (" << getsupportedregexps() << ")" << endl; os << " Also, constructions like /PATTERN1/PATTERN2/PATTERN3 are allowed" << endl; os << endl; os << "Options:" << endl; os << " -R|-r" << "\t" << "List folders recursively" << endl; os << " --tree" << "\t" << "Prints tree-like exit (implies -r)" << endl; os << " --show-handles" << "\t" << "Prints files/folders handles (H:XXXXXXXX). You can address a file/folder by its handle" << endl; os << " -l" << "\t" << "Print summary (--tree has no effect)" << endl; os << " " << "\t" << " SUMMARY contents:" << endl; os << " " << "\t" << " FLAGS: Indicate type/status of an element:" << endl; os << " " << "\t" << " xxxx" << endl; os << " " << "\t" << " |||+---- Sharing status: (s)hared, (i)n share or not shared(-)" << endl; os << " " << "\t" << " ||+----- if exported, whether it is (p)ermanent or (t)temporal" << endl; os << " " << "\t" << " |+------ e/- whether node is (e)xported" << endl; os << " " << "\t" << " +-------- Type(d=folder,-=file,r=root,i=inbox,b=rubbish,x=unsupported)" << endl; os << " " << "\t" << " VERS: Number of versions in a file" << endl; os << " " << "\t" << " SIZE: Size of the file in bytes:" << endl; os << " " << "\t" << " DATE: Modification date for files and creation date for folders (in UTC time):" << endl; os << " " << "\t" << " NAME: name of the node" << endl; os << " -h" << "\t" << "Show human readable sizes in summary" << endl; os << " -a" << "\t" << "Include extra information" << endl; os << " " << "\t" << " If this flag is repeated (e.g: -aa) more info will appear" << endl; os << " " << "\t" << " (public links, expiration dates, ...)" << endl; os << " --versions" << "\t" << "show historical versions" << endl; os << " " << "\t" << "You can delete all versions of a file with \"deleteversions\"" << endl; os << " --show-creation-time" << "\t" << "show creation time instead of modification time for files" << endl; printTimeFormatHelp(os); if (flags.usePcre || flags.showAll) { os << " --use-pcre" << "\t" << "use PCRE expressions" << endl; } } else if (!strcmp(command, "tree")) { os << "Lists files in a remote path in a nested tree decorated output" << endl; os << endl; os << "This is similar to \"ls --tree\"" << endl; } else if ((flags.win || flags.apple || flags.showAll) && !strcmp(command, "update")) { os << "Updates MEGAcmd" << endl; os << endl; os << "Looks for updates and applies if available." << endl; os << "This command can also be used to enable/disable automatic updates." << endl; os << "Options:" << endl; os << " --auto=ON|OFF|query" << "\t" << "Enables/disables/queries status of auto updates." << endl; os << endl; os << "If auto updates are enabled it will be checked while MEGAcmd server is running." << endl; os << " If there is an update available, it will be downloaded and applied." << endl; os << " This will cause MEGAcmd to be restarted whenever the updates are applied." << endl; os << endl; os << "Further info at https://github.com/meganz/megacmd#megacmd-updates" << endl; os << "Note: this command is not available on Linux" << endl; } else if (!strcmp(command, "cd")) { os << "Changes the current remote folder" << endl; os << endl; os << "If no folder is provided, it will be changed to the root folder" << endl; } else if (!strcmp(command, "log")) { os << "Prints/Modifies the log level" << endl; os << endl; os << "Options:" << endl; os << " -c" << "\t" << "CMD log level (higher level messages)." << endl; os << " " << "\t" << " Messages captured by MEGAcmd server." << endl; os << " -s" << "\t" << "SDK log level (lower level messages)." << endl; os << " " << "\t" << " Messages captured by the engine and libs" << endl; os << "Note: this setting will be saved for the next time you open MEGAcmd, but will be removed if you logout." << endl; os << endl; os << "Regardless of the log level of the" << endl; os << " interactive shell, you can increase the amount of information given" << endl; os << " by any command by passing \"-v\" (\"-vv\", \"-vvv\", ...)" << endl; } else if (!strcmp(command, "du")) { os << "Prints size used by files/folders" << endl; os << " remotepath can be a pattern (" << getsupportedregexps() << ")" << endl; os << endl; os << "Options:" << endl; os << " -h" << "\t" << "Human readable" << endl; os << " --versions" << "\t" << "Calculate size including all versions." << endl; os << " " << "\t" << "You can remove all versions with \"deleteversions\" and list them with \"ls --versions\"" << endl; os << " --path-display-size=N" << "\t" << "Use a fixed size of N characters for paths" << endl; if (flags.usePcre || flags.showAll) { os << " --use-pcre" << "\t" << "use PCRE expressions" << endl; } } else if (!strcmp(command, "pwd")) { os << "Prints the current remote folder" << endl; } else if (!strcmp(command, "lcd")) { os << "Changes the current local folder for the interactive console" << endl; os << endl; os << "It will be used for uploads and downloads" << endl; os << endl; os << "If not using interactive console, the current local folder will be" << endl; os << " that of the shell executing mega comands" << endl; } else if (!strcmp(command, "lpwd")) { os << "Prints the current local folder for the interactive console" << endl; os << endl; os << "It will be used for uploads and downloads" << endl; os << endl; os << "If not using interactive console, the current local folder will be" << endl; os << " that of the shell executing mega comands" << endl; } else if (!strcmp(command, "logout")) { os << "Logs out" << endl; os << endl; os << "Options:" << endl; os << " --keep-session" << "\t" << "Keeps the current session. This will also prevent the deletion of cached data associated " "with current session." << endl; } else if (!strcmp(command, "import")) { os << "Imports the contents of a remote link into user's cloud" << endl; os << endl; os << "If no remote path is provided, the current local folder will be used" << endl; os << "Exported links: Exported links are usually formed as publiclink#key." << endl; os << " Alternativelly you can provide a password-protected link and" << endl; os << " provide the password with --password. Please, avoid using passwords containing \" or '" << endl; } else if (!strcmp(command, "put")) { os << "Uploads files/folders to a remote folder" << endl; os << endl; os << "Options:" << endl; os << " -c" << "\t" << "Creates remote folder destination in case of not existing." << endl; os << " -q" << "\t" << "queue upload: execute in the background. Don't wait for it to end" << endl; os << " --print-tag-at-start" << "\t" << "Prints start message including transfer TAG, even when using -q." << endl; os << endl; os << "Notice that the dstremotepath can only be omitted when only one local path is provided." << endl; os << " In such case, the current remote working dir will be the destination for the upload." << endl; os << " Mind that using wildcards for local paths in non-interactive mode in a supportive console (e.g. bash)," << endl; os << " could result in multiple paths being passed to MEGAcmd." << endl; } else if (!strcmp(command, "get")) { os << "Downloads a remote file/folder or a public link " << endl; os << endl; os << "In case it is a file, the file will be downloaded at the specified folder" << endl; os << " (or at the current folder if none specified)." << endl; os << " If the localpath (destination) already exists and is the same (same contents)" << endl; os << " nothing will be done. If differs, it will create a new file appending \" (NUM)\"" << endl; os << endl; os << "For folders, the entire contents (and the root folder itself) will be" << endl; os << " by default downloaded into the destination folder" << endl; os << endl; os << "Exported links: Exported links are usually formed as publiclink#key." << endl; os << " Alternativelly you can provide a password-protected link and" << endl; os << " provide the password with --password. Please, avoid using passwords containing \" or '" << endl; os << "" << endl; os << endl; os << "Options:" << endl; os << " -q" << "\t" << "queue download: execute in the background. Don't wait for it to end" << endl; os << " -m" << "\t" << "if the folder already exists, the contents will be merged with the" << endl; os << " downloaded one (preserving the existing files)" << endl; os << " --ignore-quota-warn" << "\t" << "ignore quota surpassing warning." << endl; os << " " << "\t" << " The download will be attempted anyway." << endl; os << " --password=PASSWORD" << "\t" << "Password to decrypt the password-protected link. Please, avoid using passwords containing \" or '" << endl; if (flags.usePcre || flags.showAll) { os << " --use-pcre" << "\t" << "use PCRE expressions" << endl; } } if (!strcmp(command, "attr")) { os << "Lists/updates node attributes." << endl; os << endl; os << "Options:" << endl; os << " -s" << "\tattribute value \t" << "Sets an attribute to a value" << endl; os << " -d" << "\tattribute \t" << "Removes the attribute" << endl; os << " --print-only-value "<< "\t" << "When listing attributes, print only the values, not the attribute names." << endl; os << " --force-non-official"<< "\t" << "Forces using the custom attribute version for officially recognized attributes." << endl; os << " "<< "\t" << "Note that custom attributes are internally stored with a `_` prefix." << endl; os << " "<< "\t" << "Use this option to show, modify or delete a custom attribute with the same name as one official." << endl; } if (!strcmp(command, "userattr")) { os << "Lists/updates user attributes" << endl; os << endl; os << "Options:" << endl; os << " -s" << "\tattribute value \t" << "sets an attribute to a value" << endl; os << " --user=user@email" << "\t" << "select the user to query" << endl; os << " --list" << "\t" << "lists valid attributes" << endl; } else if (!strcmp(command, "mkdir")) { os << "Creates a directory or a directories hierarchy" << endl; os << endl; os << "Options:" << endl; os << " -p" << "\t" << "Allow recursive" << endl; } else if (!strcmp(command, "rm")) { os << "Deletes a remote file/folder" << endl; os << endl; os << "Options:" << endl; os << " -r" << "\t" << "Delete recursively (for folders)" << endl; os << " -f" << "\t" << "Force (no asking)" << endl; if (flags.usePcre || flags.showAll) { os << " --use-pcre" << "\t" << "use PCRE expressions" << endl; } } else if (!strcmp(command, "mv")) { os << "Moves file(s)/folder(s) into a new location (all remotes)" << endl; os << endl; os << "If the location exists and is a folder, the source will be moved there" << endl; os << "If the location doesn't exist, the source will be renamed to the destination name given" << endl; if (flags.usePcre || flags.showAll) { os << "Options:" << endl; os << " --use-pcre" << "\t" << "use PCRE expressions" << endl; } } else if (!strcmp(command, "cp")) { os << "Copies files/folders into a new location (all remotes)" << endl; os << endl; os << "If the location exists and is a folder, the source will be copied there" << endl; os << "If the location doesn't exist, and only one source is provided," << endl; os << " the file/folder will be copied and renamed to the destination name given." << endl; os << endl; os << "If \"dstemail:\" provided, the file/folder will be sent to that user's inbox (//in)" << endl; os << " e.g: cp /path/to/file user@doma.in:" << endl; os << " Remember the trailing \":\", otherwise a file with the name of that user (\"user@doma.in\") will be created" << endl; if (flags.usePcre || flags.showAll) { os << "Options:" << endl; os << " --use-pcre" << "\t" << "use PCRE expressions" << endl; } } else if ((!flags.win || flags.showAll) && !strcmp(command, "permissions")) { os << "Shows/Establish default permissions for files and folders created by MEGAcmd." << endl; os << endl; os << "Permissions are unix-like permissions, with 3 numbers: one for owner, one for group and one for others" << endl; os << "Options:" << endl; os << " --files" << "\t" << "To show/set files default permissions." << endl; os << " --folders" << "\t" << "To show/set folders default permissions." << endl; os << " --s XXX" << "\t" << "To set new permissions for newly created files/folder." << endl; os << " " << "\t" << " Notice that for files minimum permissions is 600," << endl; os << " " << "\t" << " for folders minimum permissions is 700." << endl; os << " " << "\t" << " Further restrictions to owner are not allowed (to avoid missfunctioning)." << endl; os << " " << "\t" << " Notice that permissions of already existing files/folders will not change." << endl; os << " " << "\t" << " Notice that permissions of already existing files/folders will not change." << endl; os << endl; os << "Note: permissions will be saved for the next time you execute MEGAcmd server. They will be removed if you logout. Permissions are not available on Windows." << endl; } else if (!strcmp(command, "https")) { os << "Shows if HTTPS is used for transfers. Use \"https on\" to enable it." << endl; os << endl; os << "HTTPS is not necessary since all data is stored and transferred encrypted." << endl; os << "Enabling it will increase CPU usage and add network overhead." << endl; os << endl; os << "Notice that this setting will be saved for the next time you open MEGAcmd, but will be removed if you logout." << endl; } else if (!strcmp(command, "deleteversions")) { os << "Deletes previous versions." << endl; os << endl; os << "This will permanently delete all historical versions of a file." << endl; os << "The current version of the file will remain." << endl; os << "Note: any file version shared to you from a contact will need to be deleted by them." << endl; os << endl; os << "Options:" << endl; os << " -f " << "\t" << "Force (no asking)" << endl; os << " --all" << "\t" << "Delete versions of all nodes. This will delete the version histories of all files (not current files)." << endl; if (flags.usePcre || flags.showAll) { os << " --use-pcre" << "\t" << "use PCRE expressions" << endl; } os << endl; os << "To see versions of a file use \"ls --versions\"." << endl; os << "To see space occupied by file versions use \"du --versions\"." << endl; } else if ((flags.haveLibuv || flags.showAll) && !strcmp(command, "webdav")) { os << "Configures a WEBDAV server to serve a location in MEGA" << endl; os << endl; os << "This can also be used for streaming files. The server will be running as long as MEGAcmd Server is." << endl; os << "If no argument is given, it will list the webdav enabled locations." << endl; os << endl; os << "Options:" << endl; os << " --d " << "\t" << "Stops serving that location" << endl; os << " --all " << "\t" << "When used with -d, stops serving all locations (and stops the server)" << endl; os << " --public " << "\t" << "*Allow access from outside localhost" << endl; os << " --port=PORT" << "\t" << "*Port to serve. DEFAULT= 4443" << endl; os << " --tls " << "\t" << "*Serve with TLS (HTTPS)" << endl; os << " --certificate=/path/to/certificate.pem" << "\t" << "*Path to PEM formatted certificate" << endl; os << " --key=/path/to/certificate.key" << "\t" << "*Path to PEM formatted key" << endl; if (flags.usePcre || flags.showAll) { os << " --use-pcre" << "\t" << "use PCRE expressions" << endl; } os << endl; os << "*If you serve more than one location, these parameters will be ignored and use those of the first location served." << endl; os << " If you want to change those parameters, you need to stop serving all locations and configure them again." << endl; os << "Note: WEBDAV settings and locations will be saved for the next time you open MEGAcmd, but will be removed if you logout." << endl; os << endl; os << "Caveat: This functionality is in BETA state. It might not be available on all platforms. If you experience any issue with this, please contact: support@mega.nz" << endl; os << endl; } else if ((flags.haveLibuv || flags.showAll) && !strcmp(command, "ftp")) { os << "Configures a FTP server to serve a location in MEGA" << endl; os << endl; os << "This can also be used for streaming files. The server will be running as long as MEGAcmd Server is." << endl; os << "If no argument is given, it will list the ftp enabled locations." << endl; os << endl; os << "Options:" << endl; os << " --d " << "\t" << "Stops serving that location" << endl; os << " --all " << "\t" << "When used with -d, stops serving all locations (and stops the server)" << endl; os << " --public " << "\t" << "*Allow access from outside localhost" << endl; os << " --port=PORT" << "\t" << "*Port to serve. DEFAULT=4990" << endl; os << " --data-ports=BEGIN-END" << "\t" << "*Ports range used for data channel (in passive mode). DEFAULT=1500-1600" << endl; os << " --tls " << "\t" << "*Serve with TLS (FTPs)" << endl; os << " --certificate=/path/to/certificate.pem" << "\t" << "*Path to PEM formatted certificate" << endl; os << " --key=/path/to/certificate.key" << "\t" << "*Path to PEM formatted key" << endl; if (flags.usePcre || flags.showAll) { os << " --use-pcre" << "\t" << "use PCRE expressions" << endl; } os << endl; os << "*If you serve more than one location, these parameters will be ignored and used those of the first location served." << endl; os << " If you want to change those parameters, you need to stop serving all locations and configure them again." << endl; os << "Note: FTP settings and locations will be saved for the next time you open MEGAcmd, but will be removed if you logout." << endl; os << endl; os << "Caveat: This functionality is in BETA state. It might not be available on all platforms. If you experience any issue with this, please contact: support@mega.nz" << endl; os << endl; } else if (!strcmp(command, "exclude")) { os << "Manages default exclusion rules in syncs." << endl; os << "These default rules will be used when creating new syncs. Existing syncs won't be affected. To modify the exclusion rules of existing syncs, use " << getCommandPrefixBasedOnMode() << "sync-ignore." << endl; os << endl; os << "Options:" << endl; os << " -a pattern1 pattern2 ..." << "\t" << "adds pattern(s) to the exclusion list" << endl; os << " " << "\t" << " (* and ? wildcards allowed)" << endl; os << " -d pattern1 pattern2 ..." << "\t" << "deletes pattern(s) from the exclusion list" << endl; os << endl; os << "This command is DEPRECATED. Use sync-ignore instead." << endl; } else if (!strcmp(command, "sync")) { os << "Controls synchronizations." << endl; os << endl; os << "If no argument is provided, it lists current configured synchronizations." << endl; os << "If local and remote paths are provided, it will start synchronizing a local folder into a remote folder." << endl; os << "If an ID/local path is provided, it will list such synchronization unless an option is specified." << endl; os << endl; os << "Note: use the \"sync-config\" command to show global sync configuration." << endl; os << endl; os << "Options:" << endl; os << " -d | --delete" << " " << "ID|localpath" << "\t" << "deletes a synchronization (not the files)." << endl; os << " -p | --pause" << " " << "ID|localpath" << "\t" << "pauses (disables) a synchronization." << endl; os << " -e | --enable" << " " << "ID|localpath" << "\t" << "resumes a synchronization." << endl; os << " [deprecated] --remove" << " " << "ID|localpath" << "\t" << "same as --delete." << endl; os << " [deprecated] -s | --disable" << " " << "ID|localpath" << "\t" << "same as --pause." << endl; os << " [deprecated] -r" << " " << "ID|localpath" << "\t" << "same as --enable." << endl; os << " --path-display-size=N" << "\t" << "Use at least N characters for displaying paths." << endl; os << " --show-handles" << "\t" << "Prints remote nodes handles (H:XXXXXXXX)." << endl; printColumnDisplayerHelp(os); os << endl; os << "DISPLAYED columns:" << endl; os << " " << "ID: an unique identifier of the sync." << endl; os << " " << "LOCALPATH: local synced path." << endl; os << " " << "REMOTEPATH: remote synced path (in MEGA)." << endl; os << " " << "RUN_STATE: Indication of running state, possible values:" << endl; #define SOME_GENERATOR_MACRO(_, ShortName, Description) \ os << " " << "\t" << ShortName << ": " << Description << "." << endl; GENERATE_FROM_SYNC_RUN_STATE(SOME_GENERATOR_MACRO) #undef SOME_GENERATOR_MACRO os << " " << "STATUS: State of the sync, possible values:" << endl; #define SOME_GENERATOR_MACRO(_, ShortName, Description) \ os << " " << "\t" << ShortName << ": " << Description << "." << endl; GENERATE_FROM_SYNC_PATH_STATE(SOME_GENERATOR_MACRO) #undef SOME_GENERATOR_MACRO os << " " << "ERROR: Error, if any." << endl; os << " " << "SIZE, FILE & DIRS: size, number of files and number of dirs in the remote folder." << endl; } else if (!strcmp(command, "sync-issues")) { os << "Show all issues with current syncs" << endl; os << endl; os << "When MEGAcmd detects conflicts with the data it's synchronizing, a sync issue is triggered. Syncing is stopped on the conflicting data, and no progress is made. Recovering from an issue usually requires user intervention." << endl; os << "A notification warning will appear whenever sync issues are detected. You can disable the warning if you wish. Note: the notification may appear even if there were already issues before." << endl; os << "Note: the list of sync issues provides a snapshot of the issues detected at the moment of requesting it. Thus, it might not contain the latest updated data. Some issues might still be being processed by the sync engine, and some might not have been removed yet." << endl; os << endl; os << "Options:" << endl; os << " --detail (ID | --all) " << "\t" << "Provides additional information on a particular sync issue." << endl; os << " " << "\t" << "The ID of the sync where this issue appeared is shown, alongside its local and cloud paths." << endl; os << " " << "\t" << "All paths involved in the issue are shown. For each path, the following columns are displayed:" << endl; os << " " << "\t" << "\t" << "PATH: The conflicting local or cloud path (cloud paths are prefixed with \"\")." << endl; os << " " << "\t" << "\t" << "PATH_ISSUE: A brief explanation of the problem this file or folder has (if any)." << endl; os << " " << "\t" << "\t" << "LAST_MODIFIED: The most recent date when this file or directory was updated." << endl; os << " " << "\t" << "\t" << "UPLOADED: For cloud paths, the date of upload or creation. Empty for local paths." << endl; os << " " << "\t" << "\t" << "SIZE: The size of the file. Empty for directories." << endl; os << " " << "\t" << "\t" << "TYPE: The type of the path (file or directory). This column is hidden if the information is not relevant for the particular sync issue." << endl; os << " " << "\t" << "The \"--all\" argument can be used to show the details of all issues." << endl; os << " --limit=rowcount " << "\t" << "Limits the amount of rows displayed. Set to 0 to display unlimited rows. Default is 10. Can also be combined with \"--detail\"." << endl; os << " --disable-path-collapse " << "\t" << "Ensures all paths are fully shown. By default long paths are truncated for readability." << endl; os << " --enable-warning " << "\t" << "Enables the notification that appears when issues are detected. This setting is saved for the next time you open MEGAcmd, but will be removed if you logout." << endl; os << " --disable-warning " << "\t" << "Disables the notification that appears when issues are detected. This setting is saved for the next time you open MEGAcmd, but will be removed if you logout." << endl; printColumnDisplayerHelp(os); os << endl; os << "DISPLAYED columns:" << endl; os << "\t" << "ISSUE_ID: A unique identifier of the sync issue. The ID can be used alongside the \"--detail\" argument." << endl; os << "\t" << "PARENT_SYNC: The identifier of the sync that has this issue." << endl; os << "\t" << "REASON: A brief explanation on what the issue is. Use the \"--detail\" argument to get extended information on a particular sync." << endl; } else if (!strcmp(command, "sync-ignore")) { os << "Manages ignore filters for syncs" << endl; os << endl; os << "To modify the default filters, use \"DEFAULT\" instead of local path or ID." << endl; os << "Note: when modifying the default filters, existing syncs won't be affected. Only newly created ones." << endl; os << endl; os << "If no action is provided, filters will be shown for the selected sync." << endl; os << "Only the filters at the root of the selected sync will be accessed. Filters beloging to sub-folders must be modified manually." << endl; os << endl; os << "Options:" << endl; os << "--show" << "\t" << "Show the existing filters of the selected sync" << endl; os << "--add" << "\t" << "Add the specified filters to the selected sync" << endl; os << "--add-exclusion" << "\t" << "Same as \"--add\", but the is 'exclude'" << endl; os << " " << "\t" << "Note: the `-` must be omitted from the filter (using '--' is not necessary)" << endl; os << "--remove" << "\t" << "Remove the specified filters from the selected sync" << endl; os << "--remove-exclusion" << "\t" << "Same as \"--remove\", but the is 'exclude'" << endl; os << " " << "\t" << "Note: the `-` must be omitted from the filter (using '--' is not necessary)" << endl; os << endl; os << "Filters must have the following format: :" << endl; os << "\t" << " Must be either exclude, or include" << endl; os << "\t" << "\t" << "exclude (`-`): This filter contains files or directories that *should not* be synchronized" << endl; os << "\t" << "\t" << " Note: you must pass a double dash ('--') to signify the end of the parameters, in order to pass exclude filters" << endl; os << "\t" << "\t" << "include (`+`): This filter contains files or directories that *should* be synchronized" << endl; os << "\t" << " May be one of the following: directory, file, symlink, or all" << endl; os << "\t" << "\t" << "directory (`d`): This filter applies only to directories" << endl; os << "\t" << "\t" << "file (`f`): This filter applies only to files" << endl; os << "\t" << "\t" << "symlink (`s`): This filter applies only to symbolic links" << endl; os << "\t" << "\t" << "all (`a`): This filter applies to all of the above" << endl; os << "\t" << "Default (when omitted) is `a`" << endl; os << "\t" << " May be one of the following: local name, path, or subtree name" << endl; os << "\t" << "\t" << "local name (`N`): This filter has an effect only in the root directory of the sync" << endl; os << "\t" << "\t" << "path (`p`): This filter matches against the path relative to the rooth directory of the sync" << endl; os << "\t" << "\t" << " Note: the path separator is always '/', even on Windows" << endl; os << "\t" << "\t" << "subtree name (`n`): This filter has an effect in all directories below the root directory of the sync, itself included" << endl; os << "\t" << "Default (when omitted) is `n`" << endl; os << "\t" << " May be one of the following: glob, or regexp" << endl; os << "\t" << "\t" << "glob (`G` or `g`): This filter matches against a name or path using a wildcard pattern" << endl; os << "\t" << "\t" << "regexp (`R` or `r`): This filter matches against a name or path using a pattern expressed as a POSIX-Extended Regular Expression" << endl; os << "\t" << "Note: uppercase `G` or `R` specifies that the matching should be case-sensitive" << endl; os << "\t" << "Default (when omitted) is `G`" << endl; os << "\t" << " Must be a file or directory pattern" << endl; os << "Some examples:" << endl; os << "\t" << "`-f:*.txt`" << " " << "Filter will exclude all *.txt files in and beneath the sync directory" << endl; os << "\t" << "`+fg:work*.txt`" << " " << "Filter will include all work*.txt files excluded by the filter above" << endl; os << "\t" << "`-N:*.avi`" << " " << "Filter will exclude all *.avi files contained directly in the sync directory" << endl; os << "\t" << "`-nr:.*foo.*`" << " " << "Filter will exclude files whose name contains 'foo'" << endl; os << "\t" << "`-d:private`" << " " << "Filter will exclude all directories with the name 'private'" << endl; os << endl; os << "See: https://help.mega.io/installs-apps/desktop/megaignore more info." << endl; } else if (!strcmp(command, "sync-config")) { os << "Controls sync configuration." << endl; os << endl; os << "Displays current configuration." << endl; os << endl; os << "New uploads for files that change frequently in syncs may be delayed until a wait time passes to avoid wastes of computational resources." << endl; os << " Delay times and number of changes may change overtime" << endl; os << "Options:" << endl; os << " --delayed-uploads-wait-seconds Shows the seconds to be waited before a file that's being delayed is uploaded again." << endl; os << " --delayed-uploads-max-attempts Shows the max number of times a file can change in quick succession before it starts to get delayed." << endl; } else if (!strcmp(command, "configure")) { os << "Shows and modifies global configurations." << endl; os << endl; os << "If no keys are provided, it will list all configuration keys and values." << endl; os << "If a key is provided, but no value given, it will only show the value of such key." << endl; os << "If a key and value are provided, it will set the value of that key." << endl; os << endl; os << "Possible keys:" << endl; for (auto &vc : Instance::Get().getConfigurators()) { os << " - " << getFixLengthString(vc.mKey, 23) << " " << vc.mDescription << "." << endl; os << wrapText(vc.mFullDescription, 120 - 27 - 1, 27) << endl; } } else if (!strcmp(command, "backup")) { os << "Controls backups" << endl; os << endl; os << "This command can be used to configure and control backups." << endl; os << "A tutorial can be found here: https://github.com/meganz/MEGAcmd/blob/master/contrib/docs/BACKUPS.md" << endl; os << endl; os << "If no argument is given it will list the configured backups" << endl; os << " To get extra info on backups use -l or -h (see Options below)" << endl; os << endl; os << "When a backup of a folder (localfolder) is established in a remote folder (remotepath)" << endl; os << " MEGAcmd will create subfolder within the remote path with names like: \"localfoldername_bk_TIME\"" << endl; os << " which shall contain a backup of the local folder at that specific time" << endl; os << "In order to configure a backup you need to specify the local and remote paths," << endl; os << "the period and max number of backups to store (see Configuration Options below)." << endl; os << "Once configured, you can see extended info asociated to the backup (See Display Options)" << endl; os << "Notice that MEGAcmd server need to be running for backups to be created." << endl; os << endl; os << "Display Options:" << endl; os << "-l\t" << "Show extended info: period, max number, next scheduled backup" << endl; os << " \t" << " or the status of current/last backup" << endl; os << "-h\t" << "Show history of created backups" << endl; os << " \t" << "Backup states:" << endl; os << " \t" << "While a backup is being performed, the backup will be considered and labeled as ONGOING" << endl; os << " \t" << "If a transfer is cancelled or fails, the backup will be considered INCOMPLETE" << endl; os << " \t" << "If a backup is aborted (see -a), all the transfers will be canceled and the backup be ABORTED" << endl; os << " \t" << "If MEGAcmd server stops during a transfer, it will be considered MISCARRIED" << endl; os << " \t" << " Notice that currently when MEGAcmd server is restarted, ongoing and scheduled transfers" << endl; os << " \t" << " will be carried out nevertheless." << endl; os << " \t" << "If MEGAcmd server is not running when a backup is scheduled and the time for the next one has already arrived," << endl; os << " \t" << " an empty BACKUP will be created with state SKIPPED" << endl; os << " \t" << "If a backup(1) is ONGOING and the time for the next backup(2) arrives, it won't start until the previous one(1)" << endl; os << " \t" << " is completed, and if by the time the first one(1) ends the time for the next one(3) has already arrived," << endl; os << " \t" << " an empty BACKUP(2) will be created with state SKIPPED" << endl; os << " --path-display-size=N" << "\t" << "Use a fixed size of N characters for paths" << endl; printTimeFormatHelp(os); os << endl; os << "Configuration Options:" << endl; os << "--period=\"PERIODSTRING\"\t" << "Period: either time in TIMEFORMAT (see below) or a cron like expression" << endl; os << " \t" << " Cron like period is formatted as follows" << endl; os << " \t" << " - - - - - -" << endl; os << " \t" << " | | | | | |" << endl; os << " \t" << " | | | | | |" << endl; os << " \t" << " | | | | | +---- Day of the Week (range: 1-7, 1 standing for Monday)" << endl; os << " \t" << " | | | | +------ Month of the Year (range: 1-12)" << endl; os << " \t" << " | | | +-------- Day of the Month (range: 1-31)" << endl; os << " \t" << " | | +---------- Hour (range: 0-23)" << endl; os << " \t" << " | +------------ Minute (range: 0-59)" << endl; os << " \t" << " +-------------- Second (range: 0-59)" << endl; os << " \t" << " examples:" << endl; os << " \t" << " - daily at 04:00:00 (UTC): \"0 0 4 * * *\"" << endl; os << " \t" << " - every 15th day at 00:00:00 (UTC) \"0 0 0 15 * *\"" << endl; os << " \t" << " - mondays at 04.30.00 (UTC): \"0 30 4 * * 1\"" << endl; os << " \t" << " TIMEFORMAT can be expressed in hours(h), days(d)," << endl; os << " \t" << " minutes(M), seconds(s), months(m) or years(y)" << endl; os << " \t" << " e.g. \"1m12d3h\" indicates 1 month, 12 days and 3 hours" << endl; os << " \t" << " Notice that this is an uncertain measure since not all months" << endl; os << " \t" << " last the same and Daylight saving time changes are not considered" << endl; os << " \t" << " If possible use a cron like expression" << endl; os << " \t" << "Notice: regardless of the period expression, the first time you establish a backup," << endl; os << " \t" << " it will be created immediately" << endl; os << "--num-backups=N\t" << "Maximum number of backups to store" << endl; os << " \t" << " After creating the backup (N+1) the oldest one will be deleted" << endl; os << " \t" << " That might not be true in case there are incomplete backups:" << endl; os << " \t" << " in order not to lose data, at least one COMPLETE backup will be kept" << endl; os << "Use backup TAG|localpath --option=VALUE to modify existing backups" << endl; os << endl; os << "Management Options:" << endl; os << "-d TAG|localpath\t" << "Removes a backup by its TAG or local path" << endl; os << " \t" << " Folders created by backup won't be deleted" << endl; os << "-a TAG|localpath\t" << "Aborts ongoing backup" << endl; os << endl; os << "Caveat: This functionality is in BETA state. If you experience any issue with this, please contact: support@mega.nz" << endl; } else if (!strcmp(command, "export")) { os << "Prints/Modifies the status of current exports" << endl; os << endl; os << "Options:" << endl; if (flags.usePcre || flags.showAll) { os << " --use-pcre" << "\t" << "The provided path will use Perl Compatible Regular Expressions (PCRE)" << endl; } os << " -a" << "\t" << "Adds an export." << endl; os << " " << "\t" << "Returns an error if the export already exists." << endl; os << " " << "\t" << "To modify an existing export (e.g. to change expiration time, password, etc.), it must be deleted and then re-added." << endl; os << " --writable" << "\t" << "Turn an export folder into a writable folder link. You can use writable folder links to " "share and receive files from anyone; including people who don’t have a MEGA account. " << endl; os << " " << "\t" << "This type of link is the same as a \"file request\" link that can be created through " "the webclient, except that writable folder links are not write-only. Writable folder " "links and file requests cannot be mixed, as they use different encryption schemes." << endl; os << " " << "\t" << "The auth-key shown has the following format #:. The " "auth-key must be provided at login, otherwise you will log into this link with " "read-only privileges. See \"" << getCommandPrefixBasedOnMode() << "login --help\" " "for more details about logging into links." << endl; os << " --mega-hosted" << "\t" << "The share key of this specific folder will be shared with MEGA." << endl; os << " " << "\t" << "This is intended to be used for folders accessible through MEGA's S4 service." << endl; os << " " << "\t" << "Encryption will occur nonetheless within MEGA's S4 service." << endl; os << " --password=PASSWORD" << "\t" << "Protects the export with a password. Passwords cannot contain \" or '." << endl; os << " " << "\t" << "A password-protected link will be printed only after exporting it." << endl; os << " " << "\t" << "If \"" << getCommandPrefixBasedOnMode() << "export\" is used to print it again, it will be shown unencrypted." << endl; os << " " << "\t" << "Note: only PRO users can protect an export with a password." << endl; os << " --expire=TIMEDELAY" << "\t" << "Sets the expiration time of the export." << endl; os << " " << "\t" << "The time format can contain hours(h), days(d), minutes(M), seconds(s), months(m) or years(y)." << endl; os << " " << "\t" << "E.g., \"1m12d3h\" will set an expiration time of 1 month, 12 days and 3 hours (relative to the current time)." << endl; os << " " << "\t" << "Note: only PRO users can set an expiration time for an export." << endl; os << " -f" << "\t" << "Implicitly accepts copyright terms (only shown the first time an export is made)." << endl; os << " " << "\t" << "MEGA respects the copyrights of others and requires that users of the MEGA cloud service comply with the laws of copyright." << endl; os << " " << "\t" << "You are strictly prohibited from using the MEGA cloud service to infringe copyright." << endl; os << " " << "\t" << "You may not upload, download, store, share, display, stream, distribute, email, link to, " "transmit or otherwise make available any files, data or content that infringes any copyright " "or other proprietary rights of any person or entity." << endl; os << " -d" << "\t" << "Deletes an export." << endl; os << " " << "\t" << "The file/folder itself is not deleted, only the export link." << endl; printTimeFormatHelp(os); os << endl; os << "If a remote path is provided without the add/delete options, all existing exports within its tree will be displayed." << endl; os << "If no remote path is given, the current working directory will be used."; } else if (!strcmp(command, "share")) { os << "Prints/Modifies the status of current shares" << endl; os << endl; os << "Options:" << endl; if (flags.usePcre || flags.showAll) { os << " --use-pcre" << "\t" << "use PCRE expressions" << endl; } os << " -p" << "\t" << "Show pending shares too" << endl; os << " --with=email" << "\t" << "Determines the email of the user to [no longer] share with" << endl; os << " -d" << "\t" << "Stop sharing with the selected user" << endl; os << " -a" << "\t" << "Adds a share (or modifies it if existing)" << endl; os << " --level=LEVEL" << "\t" << "Level of access given to the user" << endl; os << " " << "\t" << "0: " << "Read access" << endl; os << " " << "\t" << "1: " << "Read and write" << endl; os << " " << "\t" << "2: " << "Full access" << endl; os << " " << "\t" << "3: " << "Owner access" << endl; os << endl; os << "If a remote path is given it'll be used to add/delete or in case" << endl; os << " of no option selected, it will display all the shares existing" << endl; os << " in the tree of that path" << endl; os << endl; os << "When sharing a folder with a user that is not a contact (see \"" << getCommandPrefixBasedOnMode() << "users --help\")" << endl; os << " the share will be in a pending state. You can list pending shares with" << endl; os << " \"share -p\". He would need to accept your invitation (see \"" << getCommandPrefixBasedOnMode() << "ipc\")" << endl; os << endl; os << "Sharing folders will require contact verification (see \"" << getCommandPrefixBasedOnMode() << "users --help-verify\")" << endl; os << endl; os << "If someone has shared something with you, it will be listed as a root folder" << endl; os << " Use \"" << getCommandPrefixBasedOnMode() << "mount\" to list folders shared with you" << endl; } else if (!strcmp(command, "invite")) { os << "Invites a contact / deletes an invitation" << endl; os << endl; os << "Options:" << endl; os << " -d" << "\t" << "Deletes invitation" << endl; os << " -r" << "\t" << "Sends the invitation again" << endl; os << " --message=\"MESSAGE\"" << "\t" << "Sends inviting message" << endl; os << endl; os << "Use \"showpcr\" to browse invitations" << endl; os << "Use \"ipc\" to manage invitations received" << endl; os << "Use \"users\" to see contacts" << endl; } if (!strcmp(command, "ipc")) { os << "Manages contact incoming invitations." << endl; os << endl; os << "Options:" << endl; os << " -a" << "\t" << "Accepts invitation" << endl; os << " -d" << "\t" << "Rejects invitation" << endl; os << " -i" << "\t" << "Ignores invitation [WARNING: do not use unless you know what you are doing]" << endl; os << endl; os << "Use \"" << getCommandPrefixBasedOnMode() << "invite\" to send/remove invitations to other users" << endl; os << "Use \"" << getCommandPrefixBasedOnMode() << "showpcr\" to browse incoming/outgoing invitations" << endl; os << "Use \"" << getCommandPrefixBasedOnMode() << "users\" to see contacts" << endl; } if (!strcmp(command, "masterkey")) { os << "Shows your master key." << endl; os << endl; os << "Your data is only readable through a chain of decryption operations that begins" << endl; os << "with your master encryption key (Recovery Key), which MEGA stores encrypted with your password." << endl; os << "This means that if you lose your password, your Recovery Key can no longer be decrypted," << endl; os << "and you can no longer decrypt your data." << endl; os << "Exporting the Recovery Key and keeping it in a secure location" << endl; os << "enables you to set a new password without data loss." << endl; os << "Always keep physical control of your master key (e.g. on a client device, external storage, or print)" << endl; } if (!strcmp(command, "showpcr")) { os << "Shows incoming and outgoing contact requests." << endl; os << endl; os << "Options:" << endl; os << " --in" << "\t" << "Shows incoming requests" << endl; os << " --out" << "\t" << "Shows outgoing invitations" << endl; printTimeFormatHelp(os); os << endl; os << "Use \"" << getCommandPrefixBasedOnMode() << "ipc\" to manage invitations received" << endl; os << "Use \"" << getCommandPrefixBasedOnMode() << "users\" to see contacts" << endl; } else if (!strcmp(command, "users")) { os << "List contacts" << endl; os << endl; os << "Options:" << endl; os << " -d" << "\tcontact@email " << "\t" << "Deletes the specified contact." << endl; os << "--help-verify " << "\t" << "Prints general information regarding contact verification." << endl << "--help-verify contact@email" << "\t" << "This will show credentials of both own user and contact" << endl << " " << "\t" << " and instructions in order to proceed with the verifcation." << endl; os << "--verify contact@email " << "\t" << "Verifies contact@email." << endl << " " << "\t" << " CAVEAT: First you would need to manually ensure credentials match!" << endl; os << "--unverify contact@email " << "\t" << "Sets contact@email as no longer verified. New shares with that user" << endl << " " << "\t" << " will require verification." << endl; os << "Listing Options:" << endl; os << " -s" << "\t" << "Show shared folders with listed contacts" << endl; os << " -h" << "\t" << "Show all contacts (hidden, blocked, ...)" << endl; os << " -n" << "\t" << "Show users names" << endl; printTimeFormatHelp(os); os << endl; os << "Use \"" << getCommandPrefixBasedOnMode() << "invite\" to send/remove invitations to other users" << endl; os << "Use \"" << getCommandPrefixBasedOnMode() << "showpcr\" to browse incoming/outgoing invitations" << endl; os << "Use \"" << getCommandPrefixBasedOnMode() << "ipc\" to manage invitations received" << endl; os << "Use \"" << getCommandPrefixBasedOnMode() << "users\" to see contacts" << endl; } else if (!strcmp(command, "speedlimit")) { os << "Displays/modifies upload/download rate limits: either speed or max connections" << endl; os << endl; os << " NEWLIMIT is the new limit to set. If no option is provided, NEWLIMIT will be " << endl; os << " applied for both download/upload speed limits. 0, for speeds, means unlimited." << endl; os << " NEWLIMIT may include (B)ytes, (K)ilobytes, (M)egabytes, (G)igabytes & (T)erabytes." << endl; os << " Examples: \"1m12k3B\" \"3M\". If no units are given, bytes are assumed." << endl; os << endl; os << "Options:" << endl; os << " -d " << "Set/Read download speed limit, expressed in size per second." << endl; os << " -u " << "Set/Read dpload speed limit, expressed in size per second" << endl; os << " --upload-connections " << "Set/Read max number of connections for an upload transfer" << endl; os << " --download-connections " << "Set/Read max number of connections for a download transfer" << endl; os << endl; os << "Display options:" << endl; os << " -h " << "Human readable" << endl; os << endl; os << "Notice: these limits will be saved for the next time you execute MEGAcmd server. They will be removed if you logout." << endl; } else if (!strcmp(command, "killsession")) { os << "Kills a session of current user." << endl; os << endl; os << "Options:" << endl; os << " -a" << "\t" << "kills all sessions except the current one" << endl; os << endl; os << "To see all sessions use \"whoami -l\"" << endl; } else if (!strcmp(command, "whoami")) { os << "Prints info of the user" << endl; os << endl; os << "Options:" << endl; os << " -l" << "\t" << "Show extended info: total storage used, storage per main folder" << endl; os << " " << "\t" << "(see mount), pro level, account balance, and also the active sessions" << endl; } else if (!strcmp(command, "df")) { os << "Shows storage info" << endl; os << endl; os << "Shows total storage used in the account, storage per main folder (see mount)" << endl; os << endl; os << "Options:" << endl; os << " -h" << "\t" << "Human readable sizes. Otherwise, size will be expressed in Bytes" << endl; } else if (!strcmp(command, "proxy")) { os << "Show or sets proxy configuration" << endl; os << endl; os << "With no parameter given, this will print proxy configuration" << endl; os << endl; os << "Options:" << endl; os << "URL" << "\t" << "Proxy URL (e.g: https://127.0.0.1:8080)" << endl; os << " --none" << "\t" << "To disable using a proxy" << endl; os << " --auto" << "\t" << "To use the proxy configured in your system" << endl; os << " --username=USERNAME" << "\t" << "The username, for authenticated proxies" << endl; os << " --password=PASSWORD" << "\t" << "The password, for authenticated proxies. Please, avoid using passwords containing \" or '" << endl; os << endl; os << "Note: Proxy settings will be saved for the next time you open MEGAcmd, but will be removed if you logout." << endl; } else if (!strcmp(command, "cat")) { os << "Prints the contents of remote files" << endl; os << endl; if (flags.win || flags.showAll) { os << "To avoid issues with encoding on Windows, if you want to cat the exact binary contents of a remote file into a local one," << endl; os << "use non-interactive mode with -o /path/to/file. See help \"non-interactive\"" << endl; } } else if (!strcmp(command, "mediainfo")) { os << "Prints media info of remote files" << endl; os << endl; os << "Options:" << endl; os << " --path-display-size=N" << "\t" << "Use a fixed size of N characters for paths" << endl; } else if (!strcmp(command, "passwd")) { os << "Modifies user password" << endl; os << endl; os << "Notice that modifying the password will close all your active sessions" << endl; os << " in all your devices (except for the current one)" << endl; os << endl; os << " Please, avoid using passwords containing \" or '" << endl; os << endl; os << "Options:" << endl; os << " -f " << "\t" << "Force (no asking)" << endl; os << " --auth-code=XXXX" << "\t" << "Two-factor Authentication code. More info: https://mega.nz/blog_48" << endl; } else if (!strcmp(command, "reload")) { os << "Forces a reload of the remote files of the user" << endl; os << "It will also resume synchronizations." << endl; } else if (!strcmp(command, "version")) { os << "Prints MEGAcmd versioning and extra info" << endl; os << endl; os << "Options:" << endl; os << " -c" << "\t" << "Shows changelog for the current version" << endl; os << " -l" << "\t" << "Show extended info: MEGA SDK version and features enabled" << endl; } else if (!strcmp(command, "thumbnail")) { os << "To download/upload the thumbnail of a file." << endl; os << " If no -s is inidicated, it will download the thumbnail." << endl; os << endl; os << "Options:" << endl; os << " -s" << "\t" << "Sets the thumbnail to the specified file" << endl; } else if (!strcmp(command, "preview")) { os << "To download/upload the preview of a file." << endl; os << " If no -s is inidicated, it will download the preview." << endl; os << endl; os << "Options:" << endl; os << " -s" << "\t" << "Sets the preview to the specified file" << endl; } else if (!strcmp(command, "find")) { os << "Find nodes matching a pattern" << endl; os << endl; os << "Options:" << endl; os << " --pattern=PATTERN" << "\t" << "Pattern to match"; os << " (" << getsupportedregexps() << ")" << endl; os << " --type=d|f " << "\t" << "Determines type. (d) for folder, f for files" << endl; os << " --mtime=TIMECONSTRAIN" << "\t" << "Determines time constrains, in the form: [+-]TIMEVALUE" << endl; os << " " << "\t" << " TIMEVALUE may include hours(h), days(d), minutes(M)," << endl; os << " " << "\t" << " seconds(s), months(m) or years(y)" << endl; os << " " << "\t" << " Examples:" << endl; os << " " << "\t" << " \"+1m12d3h\" shows files modified before 1 month," << endl; os << " " << "\t" << " 12 days and 3 hours the current moment" << endl; os << " " << "\t" << " \"-3h\" shows files modified within the last 3 hours" << endl; os << " " << "\t" << " \"-3d+1h\" shows files modified in the last 3 days prior to the last hour" << endl; os << " --size=SIZECONSTRAIN" << "\t" << "Determines size constrains, in the form: [+-]TIMEVALUE" << endl; os << " " << "\t" << " TIMEVALUE may include (B)ytes, (K)ilobytes, (M)egabytes, (G)igabytes & (T)erabytes" << endl; os << " " << "\t" << " Examples:" << endl; os << " " << "\t" << " \"+1m12k3B\" shows files bigger than 1 Mega, 12 Kbytes and 3Bytes" << endl; os << " " << "\t" << " \"-3M\" shows files smaller than 3 Megabytes" << endl; os << " " << "\t" << " \"-4M+100K\" shows files smaller than 4 Mbytes and bigger than 100 Kbytes" << endl; os << " --show-handles" << "\t" << "Prints files/folders handles (H:XXXXXXXX). You can address a file/folder by its handle" << endl; os << " --print-only-handles" << "\t" << "Prints only files/folders handles (H:XXXXXXXX). You can address a file/folder by its handle" << endl; if (flags.usePcre || flags.showAll) { os << " --use-pcre" << "\t" << "use PCRE expressions" << endl; } os << " -l" << "\t" << "Prints file info" << endl; printTimeFormatHelp(os); } else if(!strcmp(command,"debug") ) { os << "Enters debugging mode (HIGHLY VERBOSE)" << endl; os << endl; os << "For a finer control of log level see \"log --help\"" << endl; } else if (!strcmp(command, "quit") || !strcmp(command, "exit")) { os << "Quits MEGAcmd" << endl; os << endl; os << "Notice that the session will still be active, and local caches available" << endl; os << "The session will be resumed when the service is restarted" << endl; if (isCurrentThreadCmdShell()) { os << endl; os << "Be aware that this will exit both the interactive shell and the server." << endl; os << "To only exit current shell and keep server running, use \"exit --only-shell\"" << endl; } } else if (!strcmp(command, "transfers")) { os << "List or operate with transfers" << endl; os << endl; os << "If executed without option it will list the first 10 transfers" << endl; os << "Options:" << endl; os << " -c (TAG|-a)" << "\t" << "Cancel transfer with TAG (or all with -a)" << endl; os << " -p (TAG|-a)" << "\t" << "Pause transfer with TAG (or all with -a)" << endl; os << " -r (TAG|-a)" << "\t" << "Resume transfer with TAG (or all with -a)" << endl; os << " --only-uploads" << "\t" << "Show/Operate only upload transfers" << endl; os << " --only-downloads" << "\t" << "Show/Operate only download transfers" << endl; os << endl; os << "Show options:" << endl; os << " --summary" << "\t" << "Prints summary of on going transfers" << endl; os << " --show-syncs" << "\t" << "Show synchronization transfers" << endl; os << " --show-completed" << "\t" << "Show completed transfers" << endl; os << " --only-completed" << "\t" << "Show only completed download" << endl; os << " --limit=N" << "\t" << "Show only first N transfers" << endl; os << " --path-display-size=N" << "\t" << "Use at least N characters for displaying paths" << endl; printColumnDisplayerHelp(os); os << endl; os << "TYPE legend correspondence:" << endl; #ifdef _WIN32 const string cD = utf16ToUtf8(L"\u25bc"); const string cU = utf16ToUtf8(L"\u25b2"); const string cS = utf16ToUtf8(L"\u21a8"); const string cB = utf16ToUtf8(L"\u2191"); #else const string cD = "\u21d3"; const string cU = "\u21d1"; const string cS = "\u21f5"; const string cB = "\u23eb"; #endif os << " " << cD <<" = \t" << "Download transfer" << endl; os << " " << cU <<" = \t" << "Upload transfer" << endl; os << " " << cS <<" = \t" << "Sync transfer. The transfer is done in the context of a synchronization" << endl; os << " " << cB <<" = \t" << "Backup transfer. The transfer is done in the context of a backup" << endl; } else if (((flags.win && !flags.readline) || flags.showAll) && !strcmp(command, "autocomplete")) { os << "Modifies how tab completion operates." << endl; os << endl; os << "The default is to operate like the native platform. However" << endl; os << "you can switch it between mode 'dos' and 'unix' as you prefer." << endl; os << "Options:" << endl; os << " dos" << "\t" << "Each press of tab places the next option into the command line" << endl; os << " unix" << "\t" << "Options are listed in a table, or put in-line if there is only one" << endl; os << endl; os << "Note: this command is only available on some versions of Windows" << endl; } else if (((flags.win && !flags.readline) || flags.showAll) && !strcmp(command, "codepage")) { os << "Switches the codepage used to decide which characters show on-screen." << endl; os << endl; os << "MEGAcmd supports unicode or specific code pages. For european countries you may need" << endl; os << "to select a suitable codepage or secondary codepage for the character set you use." << endl; os << "Of course a font containing the glyphs you need must have been selected for the terminal first." << endl; os << "Options:" << endl; os << " (no option)" << "\t" << "Outputs the selected code page and secondary codepage (if configured)." << endl; os << " N" << "\t" << "Sets the main codepage to N. 65001 is Unicode." << endl; os << " M" << "\t" << "Sets the secondary codepage to M, which is used if the primary can't translate a character." << endl; os << endl; os << "Note: this command is only available on some versions of Windows" << endl; } else if ((flags.fuse || flags.showAll) && !strcmp(command, "fuse-add")) { os << "Creates a new FUSE mount." << endl; os << endl; os << "Mounts are automatically enabled after being added, making the chosen MEGA folder accessible within the local filesystem." << endl; os << "When a mount is disabled, its configuration will be saved, but the cloud folder will not be mounted locally (see fuse-disable)." << endl; os << "Mounts are persisted after restarts and writable by default. You may change these and other options of a FUSE mount with fuse-config." << endl; os << "Use fuse-show to display the list of mounts." << endl; os << endl; os << "Parameters:" << endl; if (!flags.win || getenv("MEGACMD_FUSE_ALLOW_LOCAL_PATHS")) { os << " localPath [unix only] Specifies where the files contained by remotePath should be visible on the local filesystem." << endl; } if (flags.win && getenv("MEGACMD_FUSE_ALLOW_LOCAL_PATHS")) { os << " In Windows, localPath must not exist" << endl; } os << " remotePath Specifies what directory (or share) should be exposed on the local filesystem." << endl; os << endl; os << "Options:" << endl; os << " --name=name A user friendly name which the mount can be identified by. If not provided, the display name" << endl; os << " of the entity specified by remotePath will be used. If remotePath specifies the entire cloud" << endl; os << " drive, the mount's name will be \"MEGA\". If remotePath specifies the rubbish bin, the mount's" << endl; os << " name will be \"MEGA Rubbish\"." << endl; os << " --read-only Specifies that the mount should be read-only. Otherwise, the mount is writable." << endl; os << " --transient Specifies that the mount should be transient, meaning it will be lost on restart." << endl; os << " Otherwise, the mount is persistent, meaning it will remain across on restarts." << endl; os << " --disabled Specifies that the mount should not enabled after being added, and must be enabled manually. See fuse-enable." << endl; os << " If this option is passed, the mount will not be automatically enabled at startup." << endl; os << endl; os << FuseCommand::getDisclaimer() << endl; os << endl; os << FuseCommand::getBetaMsg() << endl; } else if ((flags.fuse || flags.showAll) && !strcmp(command, "fuse-remove")) { os << "Deletes a specified FUSE mount." << endl; os << endl; os << "Parameters:" << endl; os << FuseCommand::getIdentifierParameter() << endl; os << endl; os << "Note: " << FuseCommand::getBetaMsg() << endl; } else if ((flags.fuse || flags.showAll) && !strcmp(command, "fuse-enable")) { os << "Enables a specified FUSE mount." << endl; os << endl; os << "After a mount has been enabled, its cloud entities will be accessible via the mount's local path." << endl; os << endl; os << "Parameters:" << endl; os << FuseCommand::getIdentifierParameter() << endl; os << endl; os << "Options:" << endl; os << " --temporarily Specifies whether the mount should be enabled only until the server is restarted." << endl; os << " Has no effect on transient mounts, since any action on them is always temporary." << endl; os << endl; os << "Note: " << FuseCommand::getBetaMsg() << endl; } else if ((flags.fuse || flags.showAll) && !strcmp(command, "fuse-disable")) { os << "Disables a specified FUSE mount." << endl; os << endl; os << "After a mount has been disabled, its cloud entities will no longer be accessible via the mount's local path. You may enable it again via fuse-enable." << endl; os << endl; os << "Parameters:" << endl; os << FuseCommand::getIdentifierParameter() << endl; os << "Options:" << endl; os << " --temporarily Specifies whether the mount should be disabled only until the server is restarted." << endl; os << " Has no effect on transient mounts, since any action on them is always temporary." << endl; os << endl; os << "Note: " << FuseCommand::getBetaMsg() << endl; } else if ((flags.fuse || flags.showAll) && !strcmp(command, "fuse-show")) { os << "Displays the list of FUSE mounts and their information. If a name or local path provided, displays information of that mount instead." << endl; os << endl; os << "When all mounts are shown, the following columns are displayed:" << endl; os << " NAME: The user-friendly name of the mount, specified when it was added or by fuse-config." << endl; os << " LOCAL_PATH: The local mount point in the filesystem." << endl; os << " REMOTE_PATH: The cloud directory or share that is exposed locally." << endl; os << " PERSISTENT: If the mount is saved across restarts, \"YES\". Otherwise, \"NO\"." << endl; os << " ENABLED: If the mount is currently enabled, \"YES\". Otherwise, \"NO\"." << endl; os << endl; os << "Parameters:" << endl; os << FuseCommand::getIdentifierParameter() << endl; os << " If not provided, the list of mounts will be shown instead." << endl; os << endl; os << "Options:" << endl; os << " --only-enabled Only shows mounts that are enabled." << endl; os << " --disable-path-collapse Ensures all paths are fully shown. By default long paths are truncated for readability." << endl; os << " --limit=rowcount Limits the amount of rows displayed. Set to 0 to display unlimited rows. Default is unlimited." << endl; printColumnDisplayerHelp(os); os << endl; os << "Note: " << FuseCommand::getBetaMsg() << endl; } else if ((flags.fuse || flags.showAll) && !strcmp(command, "fuse-config")) { os << "Modifies the specified FUSE mount configuration." << endl; os << endl; os << "Parameters:" << endl; os << FuseCommand::getIdentifierParameter() << endl; os << endl; os << "Options:" << endl; os << " --name=name Sets the friendly name used to uniquely identify the mount." << endl; os << " --enable-at-startup=yes|no Controls whether or not the mount should be enabled automatically on startup." << endl; os << " --persistent=yes|no Controls whether or not the mount is saved across restarts." << endl; os << " --read-only=yes|no Controls whether the mount is read-only or writable." << endl; os << endl; os << "Note: " << FuseCommand::getBetaMsg() << endl; } return os.str(); } #define SSTR( x ) static_cast< const std::ostringstream & >( \ ( std::ostringstream() << std::dec << x ) ).str() void printAvailableCommands(int extensive = 0, bool showAllOptions = false) { std::set validCommandSet(validCommands.begin(), validCommands.end()); if (showAllOptions) { validCommandSet.emplace("webdav"); validCommandSet.emplace("ftp"); validCommandSet.emplace("autocomplete"); validCommandSet.emplace("codepage"); validCommandSet.emplace("unicode"); validCommandSet.emplace("permissions"); validCommandSet.emplace("update"); validCommandSet.emplace("fuse-add"); validCommandSet.emplace("fuse-remove"); validCommandSet.emplace("fuse-enable"); validCommandSet.emplace("fuse-disable"); validCommandSet.emplace("fuse-show"); validCommandSet.emplace("fuse-config"); } if (!extensive) { const size_t size = validCommandSet.size(); const size_t third = (size / 3) + ((size % 3 > 0) ? 1 : 0); const size_t twoThirds = 2 * (size / 3) + size % 3; // iterators for each column auto it1 = validCommandSet.begin(); auto it2 = std::next(it1, third); auto it3 = std::next(it1, twoThirds); for (; it1 != validCommandSet.end() && it2 != validCommandSet.end() && it3 != validCommandSet.end(); ++it1, ++it2, ++it3) { OUTSTREAM << " " << getLeftAlignedStr(*it1, 20) << getLeftAlignedStr(*it2, 20) << " " << *it3 << endl; } if (size % 3) { OUTSTREAM << " " << getLeftAlignedStr(*it1, 20); if (size % 3 > 1 ) { OUTSTREAM << getLeftAlignedStr(*it2, 20); } OUTSTREAM << endl; } return; } HelpFlags helpFlags(showAllOptions); for (const string& command : validCommandSet) { if (command == "completion") { continue; } if (extensive > 1) { OUTSTREAM << "<" << command << ">" << endl; OUTSTREAM << getHelpStr(command.c_str(), helpFlags); unsigned int width = getNumberOfCols(); for (unsigned int j = 0; j < width; j++) OUTSTREAM << "-"; OUTSTREAM << endl; } else { OUTSTREAM << " " << getUsageStr(command.c_str(), helpFlags); string helpstr = getHelpStr(command.c_str(), helpFlags); helpstr = string(helpstr, helpstr.find_first_of("\n") + 1); OUTSTREAM << ": " << string(helpstr, 0, helpstr.find_first_of("\n")); OUTSTREAM << endl; } } } void checkBlockStatus(bool waitcompletion = true) { time_t tnow = time(NULL); if ( (tnow - lastTimeCheckBlockStatus) > 30) { std::unique_ptr megaCmdListener{waitcompletion?new MegaCmdListener(api, NULL):nullptr}; api->whyAmIBlocked(megaCmdListener.get());//TO enforce acknowledging unblock transition if (megaCmdListener) { megaCmdListener->wait(); } lastTimeCheckBlockStatus = tnow; } } void executecommand(const char* ptr) { vector words = getlistOfWords(ptr, !isCurrentThreadCmdShell()); if (!words.size()) { return; } string thecommand = words[0]; if (( thecommand == "?" ) || ( thecommand == "h" )) { printAvailableCommands(); return; } if (words[0] == "completion") { if (words.size() >= 2 && words[1].find("--client-width=") == 0) { words.erase(++words.begin()); } if (words.size() < 3) words.push_back(""); vector wordstocomplete(words.begin()+1,words.end()); setCurrentThreadLine(wordstocomplete); OUTSTREAM << getListOfCompletionValues(wordstocomplete); return; } if (getBlocked()) { checkBlockStatus(!validCommand(thecommand) && thecommand != "retrycons"); } if (words[0] == "retrycons") { api->retryPendingConnections(); return; } if (words[0] == "loggedin") { if (!api->isFilesystemAvailable()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); } return; } if (words[0] == "completionshell") { if (words.size() == 2) { vector validCommandsOrdered = validCommands; sort(validCommandsOrdered.begin(), validCommandsOrdered.end()); for (size_t i = 0; i < validCommandsOrdered.size(); i++) { if (validCommandsOrdered.at(i)!="completion") { OUTSTREAM << validCommandsOrdered.at(i); if (i != validCommandsOrdered.size() -1) { OUTSTREAM << (char)0x1F; } } } } else { if (words.size() < 3) words.push_back(""); vector wordstocomplete(words.begin()+1,words.end()); setCurrentThreadLine(wordstocomplete); OUTSTREAM << getListOfCompletionValues(wordstocomplete,(char)0x1F, string().append(1, (char)0x1F).c_str(), false); } return; } words = getlistOfWords(ptr, !isCurrentThreadCmdShell(), true); //Get words again ignoring trailing spaces (only reasonable for completion) map cloptions; map clflags; set validParams; addGlobalFlags(&validParams); if (setOptionsAndFlags(&cloptions, &clflags, &words, validParams, true)) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr(thecommand.c_str()); return; } insertValidParamsPerCommand(&validParams, thecommand); if (!validCommand(thecommand)) //unknown command { setCurrentThreadOutCode(MCMD_EARGS); if (loginInAtStartup) { LOG_err << "Command not valid while login in: " << thecommand; } else { LOG_err << "Command not found: " << thecommand; } return; } if (setOptionsAndFlags(&cloptions, &clflags, &words, validParams)) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr(thecommand.c_str()); return; } setCurrentThreadLogLevel(MegaApi::LOG_LEVEL_ERROR + (getFlag(&clflags, "v")?(1+getFlag(&clflags, "v")):0)); if (getFlag(&clflags, "help")) { string h = getHelpStr(thecommand.c_str()); OUTSTREAM << h << endl; return; } if ( thecommand == "help" ) { if (getFlag(&clflags,"upgrade")) { const char *userAgent = api->getUserAgent(); char* url = new char[strlen(userAgent)+10]; sprintf(url, "pro/uao=%s",userAgent); string theurl; if (api->isLoggedIn()) { MegaCmdListener *megaCmdListener = new MegaCmdListener(api, NULL); api->getSessionTransferURL(url, megaCmdListener); megaCmdListener->wait(); if (megaCmdListener->getError() && megaCmdListener->getError()->getErrorCode() == MegaError::API_OK) { theurl = megaCmdListener->getRequest()->getLink(); } else { setCurrentThreadOutCode(MCMD_EUNEXPECTED); LOG_warn << "Unable to get session transfer url: " << megaCmdListener->getError()->getErrorString(); } delete megaCmdListener; } if (!theurl.size()) { theurl = "https://mega.nz/pro"; } OUTSTREAM << "MEGA offers different PRO plans to increase your allowed transfer quota and user storage." << endl; OUTSTREAM << "Open the following link in your browser to obtain a PRO account: " << endl; OUTSTREAM << " " << theurl << endl; delete [] url; } else if (getFlag(&clflags,"paths")) { OUTSTREAM << "MEGAcmd will allow you to enter local and remote paths." << endl; OUTSTREAM << " - REMOTE paths are case-sensitive, and use '/' as path separator." << endl; OUTSTREAM << " The root folder in your cloud will be `/`. " << endl; OUTSTREAM << " There are other possible root folders (Rubbish Bin, Inbox & in-shares). " << endl; OUTSTREAM << " For further info on root folders, see \"mount --help\"" << endl; OUTSTREAM << " - LOCAL paths are system dependant. " << endl; OUTSTREAM << " In Windows, you will be able to use both '\\' and '/' as separator." << endl; OUTSTREAM << endl; OUTSTREAM << "To refer to paths that include spaces, you will need to either surround the path between quotes \"\"," << endl; OUTSTREAM << " or scape the space with '\\ '." << endl; OUTSTREAM << " e.g: or will list the contents of a folder named 'a folder' " << endl; OUTSTREAM << " located in the root folder of your cloud." << endl; OUTSTREAM << endl; OUTSTREAM << "USE autocompletion! MEGAcmd features autocompletion. Pressing will autocomplete paths" << endl; OUTSTREAM << " (both LOCAL & REMOTE) along with other parameters of commands. It will surely save you some typing!" << endl; } else if (getFlag(&clflags,"non-interactive")) { OUTSTREAM << "MEGAcmd features two modes of interaction:" << endl; OUTSTREAM << " - interactive: entering commands in this shell. Enter \"help\" to list available commands" << endl; OUTSTREAM << " - non-interactive: MEGAcmd is also listening to outside petitions" << endl; OUTSTREAM << "For the non-interactive mode, there are client commands you can use. " << endl; #ifdef _WIN32 OUTSTREAM << "Along with the interactive shell, there should be several mega-*.bat scripts" << endl; OUTSTREAM << "installed with MEGAcmd. You can use them writting their absolute paths, " << endl; OUTSTREAM << "or including their location into your environment PATH and execute simply with mega-*" << endl; OUTSTREAM << "If you use PowerShell, you can add the the location of the scripts to the PATH with:" << endl; OUTSTREAM << " $env:PATH += \";$env:LOCALAPPDATA\\MEGAcmd\"" << endl; OUTSTREAM << "Client commands completion requires bash, hence, it is not available for Windows. " << endl; OUTSTREAM << "You can add \" -o outputfile\" to save the output into a file instead of to standard output." << endl; OUTSTREAM << endl; #elif __MACH__ OUTSTREAM << "After installing the dmg, along with the interactive shell, client commands" << endl; OUTSTREAM << "should be located at /Applications/MEGAcmd.app/Contents/MacOS" << endl; OUTSTREAM << "If you wish to use the client commands from MacOS Terminal, open the Terminal and " << endl; OUTSTREAM << "include the installation folder in the PATH. Typically:" << endl; OUTSTREAM << endl; OUTSTREAM << " export PATH=/Applications/MEGAcmd.app/Contents/MacOS:$PATH" << endl; OUTSTREAM << endl; OUTSTREAM << "And for bash completion, source megacmd_completion.sh:" << endl; OUTSTREAM << " source /Applications/MEGAcmd.app/Contents/MacOS/megacmd_completion.sh" << endl; #else OUTSTREAM << "If you have installed MEGAcmd using one of the available packages" << endl; OUTSTREAM << "both the interactive shell (mega-cmd) and the different client commands (mega-*) " << endl; OUTSTREAM << "will be in your PATH (you might need to open your shell again). " << endl; OUTSTREAM << "If you are using bash, you should also have autocompletion for client commands working. " << endl; #endif } #if defined(_WIN32) && defined(NO_READLINE) else if (getFlag(&clflags, "unicode")) { OUTSTREAM << "Unicode support has been considerably improved in the interactive console since version 1.0.0." << endl; OUTSTREAM << "If you do experience issues with it, please do not hesistate to contact us." << endl; OUTSTREAM << endl; OUTSTREAM << "Known issues: " << endl; OUTSTREAM << endl; OUTSTREAM << "If some symbols are not displaying, or displaying correctly, please first check you have a suitable font" << endl; OUTSTREAM << "selected, and a suitable codepage. See \"help codepage\" for details on that." << endl; OUTSTREAM << "When using the non-interactive mode (See \"help --non-interactive\"), piping or redirecting can be quite" << endl; OUTSTREAM << "problematic due to different encoding expectations between programs. You can use \"-o outputfile\" with your " << endl; OUTSTREAM << "mega-*.bat commands to have the output written to a file in UTF-8, and then open it with a suitable editor." << endl; } #elif defined(_WIN32) else if (getFlag(&clflags,"unicode")) { OUTSTREAM << "A great effort has been done so as to have MEGAcmd support non-ASCII characters." << endl; OUTSTREAM << "However, it might still be consider in an experimantal state. You might experiment some issues." << endl; OUTSTREAM << "If that is the case, do not hesistate to contact us so as to improve our support." << endl; OUTSTREAM << endl; OUTSTREAM << "Known issues: " << endl; OUTSTREAM << endl; OUTSTREAM << "In Windows, when executing a client command in non-interactive mode or the interactive shell " << endl; OUTSTREAM << "Some symbols might not be printed. This is something expected, since your terminal (PowerShell/Command Prompt)" << endl; OUTSTREAM << "is not able to draw those symbols. However you can use the non-interactive mode to have the output " << endl; OUTSTREAM << "written into a file and open it with a graphic editor that supports them. The file will be UTF-8 encoded." << endl; OUTSTREAM << "To do that, use \"-o outputfile\" with your mega-*.bat commands. (See \"help --non-interactive\")." << endl; OUTSTREAM << "Please, restrain using \"> outputfile\" or piping the output into another command if you require unicode support" << endl; OUTSTREAM << "because for instance, when piping, your terminal does not treat the output as binary; " << endl; OUTSTREAM << "it will meddle with the encoding, resulting in unusable output." << endl; OUTSTREAM << endl; OUTSTREAM << "In the interactive shell, the library used for reading the inputs is not able to capture unicode inputs by default" << endl; OUTSTREAM << "There's a workaround to activate an alternative way to read input. You can activate it using \"unicode\" command. " << endl; OUTSTREAM << "However, if you do so, arrow keys and hotkeys combinations will be disabled. You can disable this input mode again. " << endl; OUTSTREAM << "See \"unicode --help\" for further info." << endl; } #endif else { OUTSTREAM << "Here is the list of available commands and their usage" << endl; OUTSTREAM << "Use \"help -f\" to get a brief description of the commands" << endl; OUTSTREAM << "You can get further help on a specific command with \"command --help\" " << endl; OUTSTREAM << "Alternatively, you can use \"help -ff\" to get a complete description of all commands" << endl; OUTSTREAM << "Use \"help --non-interactive\" to learn how to use MEGAcmd with scripts" << endl; OUTSTREAM << "Use \"help --upgrade\" to learn about the limitations and obtaining PRO accounts" << endl; OUTSTREAM << "Use \"help --paths\" to learn about paths and how to enter them" << endl; OUTSTREAM << endl << "Commands:" << endl; printAvailableCommands(getFlag(&clflags, "f"), getFlag(&clflags, "show-all-options")); OUTSTREAM << endl << "Verbosity: You can increase the amount of information given by any command by passing \"-v\" (\"-vv\", \"-vvv\", ...)" << endl; if (getBlocked()) { unsigned int width = getintOption(&cloptions, "client-width", getNumberOfCols(75)); if (width > 1 ) width--; OUTSTRINGSTREAM os; printCenteredContents(os, string("[BLOCKED]\n").append(sandboxCMD->getReasonblocked()).c_str(), width); OUTSTREAM << os.str(); } } return; } cmdexecuter->executecommand(words, &clflags, &cloptions); } bool executeUpdater(bool *restartRequired, bool doNotInstall = false) { LOG_debug << "Executing updater..." ; #ifdef _WIN32 #ifndef NDEBUG LPCWSTR szPath = TEXT(".\\MEGAcmdUpdater.exe"); #else TCHAR szPath[MAX_PATH]; if (!SUCCEEDED(GetModuleFileName(NULL, szPath , MAX_PATH))) { LOG_err << "Couldnt get EXECUTABLE folder: " << wstring(szPath); setCurrentThreadOutCode(MCMD_EUNEXPECTED); return false; } if (SUCCEEDED(PathRemoveFileSpec(szPath))) { if (!PathAppend(szPath,TEXT("MEGAcmdUpdater.exe"))) { LOG_err << "Couldnt append MEGAcmdUpdater exec: " << wstring(szPath); setCurrentThreadOutCode(MCMD_EUNEXPECTED); return false; } } else { LOG_err << "Couldnt remove file spec: " << wstring(szPath); setCurrentThreadOutCode(MCMD_EUNEXPECTED); return false; } #endif STARTUPINFO si; PROCESS_INFORMATION pi; ZeroMemory( &si, sizeof(si) ); ZeroMemory( &pi, sizeof(pi) ); si.cb = sizeof(si); si.dwFlags = STARTF_USESHOWWINDOW; TCHAR szPathUpdaterCL[MAX_PATH+30]; if (doNotInstall) { wsprintfW(szPathUpdaterCL, L"%ls --normal-update --do-not-install --version %d", szPath, MEGACMD_CODE_VERSION); } else { wsprintfW(szPathUpdaterCL, L"%ls --normal-update --version %d", szPath, MEGACMD_CODE_VERSION); } LOG_verbose << "Executing: " << wstring(szPathUpdaterCL); if (!CreateProcess( szPath,(LPWSTR) szPathUpdaterCL,NULL,NULL,TRUE, 0, NULL,NULL, &si,&pi) ) { LOG_err << "Unable to execute: <" << wstring(szPath) << "> errno = : " << ERRNO; setCurrentThreadOutCode(MCMD_EUNEXPECTED); return false; } WaitForSingleObject( pi.hProcess, INFINITE ); DWORD exit_code; GetExitCodeProcess(pi.hProcess, &exit_code); *restartRequired = exit_code != 0; LOG_verbose << " The execution of Updater returns: " << exit_code; CloseHandle( pi.hProcess ); CloseHandle( pi.hThread ); #else pid_t pidupdater = fork(); if ( pidupdater == 0 ) { const char * donotinstallstr = NULL; if (doNotInstall) { donotinstallstr = "--do-not-install"; } auto versionStr = std::to_string(MEGACMD_CODE_VERSION); const char* version = const_cast(versionStr.c_str()); #ifdef __MACH__ #ifndef NDEBUG const char * args[] = {"./mega-cmd-updater", "--normal-update", donotinstallstr, "--version", version, NULL}; #else const char * args[] = {"/Applications/MEGAcmd.app/Contents/MacOS/MEGAcmdUpdater", "--normal-update", donotinstallstr, "--version", version, NULL}; #endif #else //linux doesn't use autoupdater: this is just for testing #ifndef NDEBUG const char * args[] = {"./mega-cmd-updater", "--normal-update", donotinstallstr, "--version", version, NULL}; // notice: won't work after lcd #else const char * args[] = {"mega-cmd-updater", "--normal-update", donotinstallstr, "--version", version, NULL}; #endif #endif LOG_verbose << "Exec updater line: " << args[0] << " " << args[1] << " " << args[2]; if (execvp(args[0], const_cast(args)) < 0) { LOG_err << " FAILED to initiate updater. errno = " << ERRNO; } } int status; waitpid(pidupdater, &status, 0); if ( WIFEXITED(status) ) { int exit_code = WEXITSTATUS(status); LOG_debug << "Exit status of the updater was " << exit_code; *restartRequired = exit_code != 0; } else { LOG_err << " Unexpected error waiting for Updater. errno = " << ERRNO; } #endif if (*restartRequired && api) { sendEvent(StatsManager::MegacmdEvent::UPDATE_RESTART, api); } return true; } bool restartServer() { #ifdef _WIN32 LPWSTR szPathExecQuoted = GetCommandLineW(); wstring wspathexec = wstring(szPathExecQuoted); if (wspathexec.at(0) == '"') { wspathexec = wspathexec.substr(1); } size_t pos = wspathexec.find(L"--wait-for"); if (pos != string::npos) { wspathexec = wspathexec.substr(0,pos); } while (wspathexec.size() && ( wspathexec.at(wspathexec.size()-1) == '"' || wspathexec.at(wspathexec.size()-1) == ' ' )) { wspathexec = wspathexec.substr(0,wspathexec.size()-1); } LPWSTR szPathServerCommand = (LPWSTR) wspathexec.c_str(); TCHAR szPathServer[MAX_PATH]; if (!SUCCEEDED(GetModuleFileName(NULL, szPathServer , MAX_PATH))) { LOG_err << "Couldnt get EXECUTABLE folder: " << wstring(szPathServer); setCurrentThreadOutCode(MCMD_EUNEXPECTED); return false; } LOG_debug << "Restarting the server : <" << wstring(szPathServerCommand) << ">"; STARTUPINFO si; PROCESS_INFORMATION pi; ZeroMemory( &si, sizeof(si) ); ZeroMemory( &pi, sizeof(pi) ); si.cb = sizeof(si); si.dwFlags = STARTF_USESHOWWINDOW; TCHAR szPathServerCL[MAX_PATH+30]; wsprintfW(szPathServerCL,L"%ls --wait-for %d", szPathServerCommand, GetCurrentProcessId()); LOG_verbose << "Executing: " << wstring(szPathServerCL); if (!CreateProcess( szPathServer,(LPWSTR) szPathServerCL,NULL,NULL,TRUE, 0, NULL,NULL, &si,&pi) ) { LOG_debug << "Unable to execute: <" << wstring(szPathServerCL) << "> errno = : " << ERRNO; return false; } #else pid_t childid = fork(); if (childid == -1) { LOG_err << "fork() failed: " << strerror(errno); return false; } if (childid == 0) // child → execv to become the new server { const char **argv = new const char*[mcmdMainArgc+3]; int i = 0, j = 0; #ifdef __linux__ string executable = const_cast(mcmdMainArgv[0]); if (executable.find("/") != 0) { executable.insert(0, getCurrentExecPath()+"/"); } argv[0] = executable.c_str(); i++; j++; #endif for (;i < mcmdMainArgc; i++) { if ( (i+1) < mcmdMainArgc && !strcmp(mcmdMainArgv[i],"--wait-for")) { i+=2; } else { argv[j++]=mcmdMainArgv[i]; } } argv[j++] = "--wait-for"; string parentpidstr = std::to_string(getppid()); argv[j++] = parentpidstr.c_str(); argv[j++] = NULL; LOG_debug << "Restarting the server : <" << argv[0] << ">"; execv(argv[0], const_cast(argv)); // execv failed — child has no threads, skip C++ teardown _exit(1); } // parent falls through — all threads intact #endif LOG_debug << "Server restarted, indicating the shell to restart also"; setCurrentThreadOutCode(MCMD_REQRESTART); string s = "restart"; cm->informStateListeners(s); return true; } bool isBareCommand(const char *l, const string &command) { string what(l); string xcommand = "X" + command; if (what == command || what == xcommand) { return true; } if (what.find(command+" ") != 0 && what.find(xcommand+" ") != 0 ) { return false; } vector words = getlistOfWords(l, !isCurrentThreadCmdShell()); for (int i = 1; i transferData(api.getTransferData()); assert(transferData); if (transferData->getNumDownloads() > 0 || transferData->getNumUploads() > 0) { return true; } // We can only check for delayed sync uploads if there are no sync issues if (!api.isSyncing() || api.isSyncStalled()) { return false; } auto listener = std::make_unique(nullptr); api.checkSyncUploadsThrottled(listener.get()); listener->wait(); if (listener->getError()->getErrorCode() == MegaError::API_OK) { MegaRequest* request = listener->getRequest(); assert(request); return request->getFlag(); // delayed sync uploads } return false; } void MegaCmdExecuter::mayExecutePendingStuffInWorkerThread() { { // send INVALID_UTF8_INCIDENCES if there have been incidences static std::mutex mutexSendEventInvalidUtf8Incidences; std::lock_guard g(mutexSendEventInvalidUtf8Incidences); if (auto incidencesFound = sInvalidUtf8Incidences.exchange(0)) { static HammeringLimiter hammeringLimiter(10); if (!hammeringLimiter.runRecently()) { LOG_err << "Invalid utf8 accumulated occurrences: " << incidencesFound; sendEvent(StatsManager::MegacmdEvent::INVALID_UTF8_INCIDENCES, api, false); } else { // add them again to the count, to be reconsidered later. sInvalidUtf8Incidences += incidencesFound; } } } } static bool process_line(const std::string_view line) { cmdexecuter->mayExecutePendingStuffInWorkerThread(); const char* l = line.data(); assert(line.size() == strlen(l)); // string_view does not guarantee null termination, which is depended upon switch (prompt) { case AREYOUSURETODELETE: if (!strcmp(l,"yes") || !strcmp(l,"YES") || !strcmp(l,"y") || !strcmp(l,"Y")) { cmdexecuter->confirmDelete(); } else if (!strcmp(l,"no") || !strcmp(l,"NO") || !strcmp(l,"n") || !strcmp(l,"N")) { cmdexecuter->discardDelete(); } else if (!strcmp(l,"All") || !strcmp(l,"ALL") || !strcmp(l,"a") || !strcmp(l,"A") || !strcmp(l,"all")) { cmdexecuter->confirmDeleteAll(); } else if (!strcmp(l,"None") || !strcmp(l,"NONE") || !strcmp(l,"none")) { cmdexecuter->discardDeleteAll(); } else { //Do nth, ask again OUTSTREAM << "Please enter [y]es/[n]o/[a]ll/none: " << flush; } break; case LOGINPASSWORD: { if (!strlen(l)) { break; } if (cmdexecuter->confirming) { cmdexecuter->confirmWithPassword(l); } else if (cmdexecuter->confirmingcancel) { cmdexecuter->confirmCancel(cmdexecuter->link.c_str(), l); } else { cmdexecuter->loginWithPassword(l); } cmdexecuter->confirming = false; cmdexecuter->confirmingcancel = false; setprompt(COMMAND); break; } case NEWPASSWORD: { if (!strlen(l)) { break; } newpasswd = l; OUTSTREAM << endl; setprompt(PASSWORDCONFIRM); } break; case PASSWORDCONFIRM: { if (!strlen(l)) { break; } if (l != newpasswd) { OUTSTREAM << endl << "New passwords differ, please try again" << endl; } else { OUTSTREAM << endl; if (!cmdexecuter->signingup) { cmdexecuter->changePassword(newpasswd.c_str()); } else { cmdexecuter->signupWithPassword(l); cmdexecuter->signingup = false; } } setprompt(COMMAND); break; } case COMMAND: { if (!l || !strcmp(l, "q") || !strcmp(l, "quit") || !strcmp(l, "exit") || ( (!strncmp(l, "quit ", strlen("quit ")) || !strncmp(l, "exit ", strlen("exit ")) ) && !strstr(l,"--help") ) ) { if (isCurrentThreadCmdShell() && hasOngoingTransfersOrDelayedSyncs(*api)) { const string sureToExitMsg = "There are ongoing transfers and/or pending sync uploads.\n" "Are you sure you want to exit? (Yes/No): "; int confirmationResponse = askforConfirmation(sureToExitMsg); if (!confirmationResponse) { setCurrentThreadOutCode(MCMD_CONFIRM_NO); return false; } } if (strstr(l,"--wait-for-ongoing-petitions")) { int attempts=20; //give a while for ongoing petitions to end before killing the server delete_finished_threads(); while(petitionThreads.size() > 1 && attempts--) { LOG_debug << "giving a little longer for ongoing petitions: " << petitionThreads.size(); sleepSeconds(20-attempts); delete_finished_threads(); } } return true; // exit } else if (isBareCommand(l, "sendack")) { cm->informStateListeners("ack"); break; } #if defined(_WIN32) || defined(__APPLE__) else if (isBareCommand(l, "update")) //if extra args are received, it'll be processed by executer { string confirmationQuery("This might require restarting MEGAcmd. Are you sure to continue"); confirmationQuery+="? (Yes/No): "; int confirmationResponse = askforConfirmation(confirmationQuery); if (confirmationResponse != MCMDCONFIRM_YES && confirmationResponse != MCMDCONFIRM_ALL) { setCurrentThreadOutCode(MCMD_INVALIDSTATE); // so as not to indicate already updated return false; } bool restartRequired = false; if (!executeUpdater(&restartRequired)) { setCurrentThreadOutCode(MCMD_INVALIDSTATE); // so as not to indicate already updated return false; } if (restartRequired && restartServer()) { OUTSTREAM << " " << endl; int attempts=20; //give a while for ongoing petitions to end before killing the server while(petitionThreads.size() > 1 && attempts--) { sleepSeconds(20-attempts); } return true; } else { OUTSTREAM << "Update is not required. You are in the last version. Further info: \"version --help\", \"update --help\"" << endl; return false; } } #endif executecommand(l); break; } } return false; //Do not exit } void* doProcessLine(void* infRaw) { auto inf = std::unique_ptr((CmdPetition*) infRaw); OUTSTRINGSTREAM s; setCurrentThreadLogLevel(MegaApi::LOG_LEVEL_ERROR); setCurrentThreadOutCode(MCMD_OK); setCurrentThreadCmdPetition(inf.get()); LoggedStreamPartialOutputs ls(cm, inf.get()); LoggedStreamPartialErrors lserr(cm, inf.get()); setCurrentThreadOutStreams(ls, lserr); setCurrentThreadIsCmdShell(inf->isFromCmdShell()); LOG_verbose << " Processing " << inf->getRedactedLine() << " in thread: " << MegaThread::currentThreadId() << " " << inf->getPetitionDetails(); doExit = process_line(inf->getUniformLine()); if (doExit) { stopCheckingforUpdaters = true; LOG_verbose << " Exit registered upon process_line: " ; } LOG_verbose << " Processed " << inf->getRedactedLine() << " in thread: " << MegaThread::currentThreadId() << " " << inf->getPetitionDetails(); MegaThread * petitionThread = inf->getPetitionThread(); if (inf->clientID != -3) // -3 is self client (no actual client) { cm->returnAndClosePetition(std::move(inf), &s, getCurrentThreadOutCode()); } semaphoreClients.release(); if (doExit && (!isCurrentThreadInteractive() || isCurrentThreadCmdShell() )) { cm->stopWaiting(); } mutexEndedPetitionThreads.lock(); endedPetitionThreads.push_back(petitionThread); mutexEndedPetitionThreads.unlock(); return nullptr; } int askforConfirmation(string message) { CmdPetition *inf = getCurrentThreadCmdPetition(); if (inf) { return cm->getConfirmation(inf,message); } else { LOG_err << "Unable to get current petition to ask for confirmation"; } return MCMDCONFIRM_NO; } bool booleanAskForConfirmation(string messageHeading) { std::string confirmationQuery = messageHeading + "? ([y]es/[n]o): "; auto confirmationResponse = askforConfirmation(confirmationQuery); return (confirmationResponse == MCMDCONFIRM_YES || confirmationResponse == MCMDCONFIRM_ALL); } string askforUserResponse(string message) { CmdPetition *inf = getCurrentThreadCmdPetition(); if (inf) { return cm->getUserResponse(inf,message); } else { LOG_err << "Unable to get current petition to ask for confirmation"; } return string("NOCURRENPETITION"); } void delete_finished_threads() { std::unique_lock lock(mutexEndedPetitionThreads); for (MegaThread *mt : endedPetitionThreads) { for (auto it = petitionThreads.begin(); it != petitionThreads.end(); ) { if (mt == it->get()) { (*it)->join(); it = petitionThreads.erase(it); } else { ++it; } } } endedPetitionThreads.clear(); } void processCommandInPetitionQueues(CmdPetition *inf); void processCommandLinePetitionQueues(std::string what); bool waitForRestartSignal = false; #ifdef __linux__ std::mutex mtxcondvar; std::condition_variable condVarRestart; bool condVarRestartBool = false; string appToWaitForSignal; void LinuxSignalHandler(int signum) { if (signum == SIGUSR2) { std::unique_lock lock(mtxcondvar); condVarRestart.notify_one(); condVarRestartBool = true; } else if (signum == SIGUSR1) { if (!waitForRestartSignal) { waitForRestartSignal = true; LOG_debug << "Preparing MEGAcmd to restart: "; stopCheckingforUpdaters = true; doExit = true; } } } #endif void finalize(bool waitForRestartSignal_param) { static bool alreadyfinalized = false; if (alreadyfinalized) return; alreadyfinalized = true; LOG_info << "closing application ..."; delete_finished_threads(); if (!consoleFailed) { delete console; } delete megaCmdMegaListener; if (threadRetryConnections) { threadRetryConnections->join(); } delete threadRetryConnections; delete api; while (!apiFolders.empty()) { delete apiFolders.front(); apiFolders.pop(); } for (std::vector< MegaApi * >::iterator it = occupiedapiFolders.begin(); it != occupiedapiFolders.end(); ++it) { delete ( *it ); } occupiedapiFolders.clear(); delete megaCmdGlobalListener; delete cmdexecuter; #ifdef __linux__ if (waitForRestartSignal_param) { LOG_debug << "Waiting for signal to restart MEGAcmd ... "; std::unique_lock lock(mtxcondvar); if (condVarRestartBool || condVarRestart.wait_for(lock, std::chrono::minutes(30)) == std::cv_status::no_timeout ) { restartServer(); } else { LOG_err << "Former server still alive after waiting. Not restarted."; } } #endif delete cm; // closes the socket; must happen after restartServer() so clients // are notified, and before exit so the new server can bind it LOG_debug << "resources have been cleaned ..."; LOG_info << "----------------------------- program end -------------------------------"; MegaApi::removeLoggerObject(loggerCMD); delete loggerCMD; ConfigurationManager::unlockExecution(); ConfigurationManager::unloadConfiguration(); } void finalize() { finalize(false); } int currentclientID = 1; void * retryConnections(void *pointer) { while(!doExit) { LOG_verbose << "Calling recurrent retryPendingConnections"; api->retryPendingConnections(); int count = 100; while (!doExit && --count) { sleepMilliSeconds(300); } } return NULL; } void startcheckingForUpdates() { ConfigurationManager::savePropertyValue("autoupdate", 1); if (!alreadyCheckingForUpdates) { alreadyCheckingForUpdates = true; LOG_info << "Starting autoupdate check mechanism"; MegaThread *checkupdatesThread = new MegaThread(); checkupdatesThread->start(checkForUpdates, checkupdatesThread); } } void stopcheckingForUpdates() { ConfigurationManager::savePropertyValue("autoupdate", 0); stopCheckingforUpdaters = true; } void* checkForUpdates(void *param) { stopCheckingforUpdaters = false; LOG_debug << "Initiating recurrent checkForUpdates"; int secstosleep = 60; while (secstosleep > 0 && !stopCheckingforUpdaters) { sleepSeconds(2); secstosleep -= 2; } while (!doExit && !stopCheckingforUpdaters) { bool restartRequired = false; if (!executeUpdater(&restartRequired, true)) //only download & check { LOG_err << " Failed to execute updater"; } else if (restartRequired) { LOG_info << " There is a pending update. Will be applied in a few seconds"; broadcastMessage("A new update has been downloaded. It will be performed in 60 seconds"); int secstosleep = 57; while (secstosleep > 0 && !stopCheckingforUpdaters) { sleepSeconds(2); secstosleep -= 2; } if (stopCheckingforUpdaters) break; broadcastMessage(" Executing update in 3"); sleepSeconds(1); if (stopCheckingforUpdaters) break; broadcastMessage(" Executing update in 2"); sleepSeconds(1); if (stopCheckingforUpdaters) break; broadcastMessage(" Executing update in 1"); sleepSeconds(1); if (stopCheckingforUpdaters) break; while(petitionThreads.size() && !stopCheckingforUpdaters) { LOG_fatal << " waiting for petitions to end to initiate upload " << petitionThreads.size() << petitionThreads.at(0).get(); sleepSeconds(2); delete_finished_threads(); } if (stopCheckingforUpdaters) break; sendEvent(StatsManager::MegacmdEvent::UPDATE_START, api); broadcastMessage(" Executing update !"); LOG_info << " Applying update"; executeUpdater(&restartRequired); } else { LOG_verbose << " There is no pending update"; } if (stopCheckingforUpdaters) break; if (restartRequired && restartServer()) { int attempts = 20; //give a while for ingoin petitions to end before killing the server while(petitionThreads.size() && --attempts) { sleepSeconds(20 - attempts); delete_finished_threads(); } doExit = true; cm->stopWaiting(); break; } int secstosleep = 7200; while (secstosleep > 0 && !stopCheckingforUpdaters) { sleepSeconds(2); secstosleep -= 2; } } alreadyCheckingForUpdates = false; delete (MegaThread *)param; return NULL; } void processCommandInPetitionQueues(std::unique_ptr inf) { semaphoreClients.wait(); //append new one auto petitionThread = new MegaThread(); petitionThreads.emplace_back(petitionThread); inf->setPetitionThread(petitionThread); LOG_verbose << "starting processing: <" << inf->getRedactedLine() << ">"; petitionThread->start(doProcessLine, (void*) inf.release()); } void processCommandLinePetitionQueues(std::string what) { auto inf = std::make_unique(); inf->setLine(what); inf->clientDisconnected = true; // There's no actual client inf->clientID = -3; processCommandInPetitionQueues(std::move(inf)); } // main loop void megacmd() { threadRetryConnections = new MegaThread(); threadRetryConnections->start(retryConnections, NULL); LOG_info << "Listening to petitions ... "; #ifdef MEGACMD_TESTING_CODE TestInstruments::Instance().fireEvent(TestInstruments::Event::SERVER_ABOUT_TO_START_WAITING_FOR_PETITIONS); #endif for (;; ) { int err = cm->waitForPetition(); if (err != 0) { continue; } api->retryPendingConnections(); if (doExit) { LOG_verbose << "closing after wait ..." ; return; } if (cm->receivedPetition()) { LOG_verbose << "Client connected "; auto infOwned = cm->getPetition(); assert(infOwned); CmdPetition* inf = infOwned.get(); LOG_verbose << "petition registered: " << inf->getRedactedLine(); delete_finished_threads(); if (inf->getUniformLine() == "ERROR") { LOG_warn << "Petition couldn't be registered. Dismissing it."; } // if state register petition else if (startsWith(inf->getUniformLine(), "registerstatelistener")) { inf = cm->registerStateListener(std::move(infOwned)); if (!inf) { continue; } { // Communicate client ID string clientIdStr = "clientID:" + std::to_string(currentclientID) + (char) 0x1F; inf->clientID = currentclientID; currentclientID++; cm->informStateListener(inf, clientIdStr); } std::string s; #if defined(_WIN32) || defined(__APPLE__) ostringstream os; auto updatMsgOpt = lookForAvailableNewerVersions(api); //TODO: have this executed in worker thread instead (see MegaCmdExecuter::mayExecutePendingStuffInWorkerThread) // still store the update message to be consumed here if (updatMsgOpt) { os << *updatMsgOpt; } int autoupdate = ConfigurationManager::getConfigurationValue("autoupdate", -1); if (autoupdate == -1 || autoupdate == 2) { os << "ENABLING AUTOUPDATE BY DEFAULT. You can disable it with \"update --auto=off\"" << endl; autoupdate = 1; } if (autoupdate == 1) { startcheckingForUpdates(); } auto message = os.str(); if (message.size()) { s += "message:"; s += message; s += (char) 0x1F; } #endif bool isOSdeprecated = false; #ifdef MEGACMD_DEPRECATED_OS isOSdeprecated = true; #endif #ifdef _WIN32 OSVERSIONINFOEX osvi; ZeroMemory(&osvi, sizeof(OSVERSIONINFOEX)); osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX); #pragma warning(disable: 4996) // warning C4996: 'GetVersionExW': was declared deprecated if (GetVersionEx((OSVERSIONINFO*)&osvi) && osvi.dwMajorVersion < 6) { isOSdeprecated = true; } #endif if (isOSdeprecated) { s += "message:"; s += "Your Operative System is too old.\n"; s += "You might not receive new updates for this application.\n"; s += "We strongly recommend you to update to a new version.\n"; s += (char) 0x1F; } if (sandboxCMD->storageStatus != MegaApi::STORAGE_STATE_GREEN) { s += "message:"; if (sandboxCMD->storageStatus == MegaApi::STORAGE_STATE_PAYWALL) { std::unique_ptr myEmail(api->getMyEmail()); std::unique_ptr warningsList(api->getOverquotaWarningsTs()); s += "We have contacted you by email to " + string(myEmail.get()) + " on "; s += getReadableTime(warningsList->get(0),"%b %e %Y"); if (warningsList->size() > 1) { for (int i = 1; i < warningsList->size() - 1; i++) { s += ", " + getReadableTime(warningsList->get(i),"%b %e %Y"); } s += " and " + getReadableTime(warningsList->get(warningsList->size() - 1),"%b %e %Y"); } std::unique_ptr rootNode(api->getRootNode()); auto listener = std::make_unique(); api->getFolderInfo(rootNode.get(), listener.get()); listener->wait(); auto error = listener->getError(); assert(error != nullptr); if (error->getErrorCode() == MegaError::API_OK) { long long totalFiles = 0; auto info = listener->getRequest()->getMegaFolderInfo(); if (info != nullptr) { totalFiles += info->getNumFolders(); } s += ", but you still have " + std::to_string(totalFiles) + " files taking up " + sizeToText(sandboxCMD->receivedStorageSum); } else { s += ", but you still have files taking up" + sizeToText(sandboxCMD->receivedStorageSum); } s += " in your MEGA account, which requires you to upgrade your account.\n\n"; long long daysLeft = (api->getOverquotaDeadlineTs() - m_time(NULL)) / 86400; if (daysLeft > 0) { s += "You have " + std::to_string(daysLeft) + " days left to upgrade. "; s += "After that, your data is subject to deletion.\n"; } else { s += "You must act immediately to save your data. From now on, your data is subject to deletion.\n"; } } else if (sandboxCMD->storageStatus == MegaApi::STORAGE_STATE_RED) { s += "You have exeeded your available storage.\n"; s += "You can change your account plan to increase your quota limit.\n"; } else { s += "You are running out of available storage.\n"; s += "You can change your account plan to increase your quota limit.\n"; } s += "See \"help --upgrade\" for further details.\n"; s += (char) 0x1F; } // if server resuming session, lets give him a very litle while before sending greeting message to the early clients // (to aovid "Resuming session..." being printed fast resumed session) while (getloginInAtStartup() && ((m_time(nullptr) - timeLoginStarted() < RESUME_SESSION_TIMEOUT * 0.3))) { sleepMilliSeconds(300); } { std::lock_guard g(greetingsmsgsMutex); while(greetingsFirstClientMsgs.size()) { cm->informStateListener(inf,greetingsFirstClientMsgs.front().append(1, (char)0x1F)); greetingsFirstClientMsgs.pop_front(); } for (auto m: greetingsAllClientMsgs) { cm->informStateListener(inf, m.append(1, (char)0x1F)); } } // if server resuming session, lets give him a litle while before returning a prompt to the early clients // This will block the server from responging any commands in the meantime, but that assumable, it will only happen // the first time the server is initiated. while (getloginInAtStartup() && ((m_time(nullptr) - timeLoginStarted() < RESUME_SESSION_TIMEOUT * 0.7))) { sleepMilliSeconds(300); } // communicate status info s += "prompt:"; s += dynamicprompt; s += (char) 0x1F; if (!sandboxCMD->getReasonblocked().size()) { cmdexecuter->checkAndInformPSA(inf); } cm->informStateListener(inf, s); } else { // normal petition processCommandInPetitionQueues(std::move(infOwned)); } } } } class NullBuffer : public std::streambuf { public: int overflow(int c) { return c; } }; void printWelcomeMsg() { unsigned int width = getNumberOfCols(75); #ifdef _WIN32 width--; #endif std::ostringstream oss; oss << endl; oss << "."; for (unsigned int i = 0; i < width; i++) oss << "=" ; oss << "."; oss << endl; printCenteredLine(oss, " __ __ _____ ____ _ _ ",width); printCenteredLine(oss, "| \\/ | ___|/ ___| / \\ ___ _ __ ___ __| |",width); printCenteredLine(oss, "| |\\/| | \\ / | _ / _ \\ / __| '_ ` _ \\ / _` |",width); printCenteredLine(oss, "| | | | /__\\ |_| |/ ___ \\ (__| | | | | | (_| |",width); printCenteredLine(oss, "|_| |_|____|\\____/_/ \\_\\___|_| |_| |_|\\__,_|",width); oss << "|"; for (unsigned int i = 0; i < width; i++) oss << " " ; oss << "|"; oss << endl; printCenteredLine(oss, "SERVER",width); oss << "`"; for (unsigned int i = 0; i < width; i++) { oss << "=" ; } #ifndef _WIN32 oss << "\u00b4\n"; COUT << oss.str() << std::flush; #else WindowsUtf8StdoutGuard utf8Guard; // So far, all is ASCII. COUT << oss.str(); // Now let's tray the non ascii forward acute // We are about to write some non ascii character. // Let's set (from now on, the console code page to UTF-8 translation (65001) // but revert to the initial code page if outputing the special forward acute character // fails. <- This could happen, for instance in Windows 7. auto initialCP = GetConsoleOutputCP(); bool codePageChangedSuccesfully = false; if (initialCP != CP_UTF8 && !getenv("MEGACMDSERVER_DONOT_SET_CONSOLE_CP")) { codePageChangedSuccesfully = SetConsoleOutputCP(CP_UTF8); } if (!(COUT << L"\u00b4")) // failed to output using utf-8 { if (codePageChangedSuccesfully) // revert codepage { SetConsoleOutputCP(initialCP); } COUT << "/"; } COUT << std::endl; #endif } string getLocaleCode() { #if defined(_WIN32) && defined(LOCALE_SISO639LANGNAME) LCID lcidLocaleId; LCTYPE lctyLocaleInfo; PWSTR pstr; INT iBuffSize; lcidLocaleId = LOCALE_USER_DEFAULT; lctyLocaleInfo = LOCALE_SISO639LANGNAME; // Determine the size iBuffSize = GetLocaleInfo( lcidLocaleId, lctyLocaleInfo, NULL, 0 ); if(iBuffSize > 0) { pstr = (WCHAR *) malloc( iBuffSize * sizeof(WCHAR) ); if(pstr != NULL) { if(GetLocaleInfoW( lcidLocaleId, lctyLocaleInfo, pstr, iBuffSize )) { string toret; std::wstring ws(pstr); localwtostring(&ws,&toret); free(pstr); //free locale info string return toret; } free(pstr); //free locale info string } } #else try { locale l(""); string ls = l.name(); size_t posequal = ls.find("="); size_t possemicolon = ls.find_first_of(";."); if (posequal != string::npos && possemicolon != string::npos && posequal < possemicolon) { return ls.substr(posequal+1,possemicolon-posequal-1); } } catch (const std::exception& e) { #ifndef __MACH__ std::cerr << "Warning: unable to get locale " << std::endl; #endif } #endif return string(); } bool runningInBackground() { #ifndef _WIN32 pid_t fg = tcgetpgrp(STDIN_FILENO); if(fg == -1) { // Piped: return false; } else if (fg == getpgrp()) { // foreground return false; } else { // background return true; } #endif return false; } #ifndef MEGACMD_USERAGENT_SUFFIX #define MEGACMD_USERAGENT_SUFFIX #define MEGACMD_STRINGIZE(x) #else #define MEGACMD_STRINGIZE2(x) "-" #x #define MEGACMD_STRINGIZE(x) MEGACMD_STRINGIZE2(x) #endif #ifdef _WIN32 LPTSTR getCurrentSid() { HANDLE hTok = NULL; LPBYTE buf = NULL; DWORD dwSize = 0; LPTSTR stringSID = NULL; if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hTok)) { GetTokenInformation(hTok, TokenUser, NULL, 0, &dwSize); if (dwSize) { buf = (LPBYTE)LocalAlloc(LPTR, dwSize); if (GetTokenInformation(hTok, TokenUser, buf, dwSize, &dwSize)) { ConvertSidToStringSid(((PTOKEN_USER)buf)->User.Sid, &stringSID); } LocalFree(buf); } CloseHandle(hTok); } return stringSID; } #endif bool registerUpdater() { #ifdef _WIN32 ITaskService *pService = NULL; ITaskFolder *pRootFolder = NULL; ITaskFolder *pMEGAFolder = NULL; ITaskDefinition *pTask = NULL; IRegistrationInfo *pRegInfo = NULL; IPrincipal *pPrincipal = NULL; ITaskSettings *pSettings = NULL; IIdleSettings *pIdleSettings = NULL; ITriggerCollection *pTriggerCollection = NULL; ITrigger *pTrigger = NULL; IDailyTrigger *pCalendarTrigger = NULL; IRepetitionPattern *pRepetitionPattern = NULL; IActionCollection *pActionCollection = NULL; IAction *pAction = NULL; IExecAction *pExecAction = NULL; IRegisteredTask *pRegisteredTask = NULL; time_t currentTime; struct tm* currentTimeInfo; WCHAR currentTimeString[128]; _bstr_t taskBaseName = L"MEGAcmd Update Task "; LPTSTR stringSID = NULL; bool success = false; stringSID = getCurrentSid(); if (!stringSID) { LOG_err << "Unable to get the current SID"; return false; } time(¤tTime); currentTime += 60; // ensure task is triggered properly the first time currentTimeInfo = localtime(¤tTime); wcsftime(currentTimeString, 128, L"%Y-%m-%dT%H:%M:%S", currentTimeInfo); _bstr_t taskName = taskBaseName + stringSID; _bstr_t userId = stringSID; LocalFree(stringSID); TCHAR MEGAcmdUpdaterPath[MAX_PATH]; if (!SUCCEEDED(GetModuleFileName(NULL, MEGAcmdUpdaterPath , MAX_PATH))) { LOG_err << "Couldnt get EXECUTABLE folder: " << wstring(MEGAcmdUpdaterPath); return false; } if (SUCCEEDED(PathRemoveFileSpec(MEGAcmdUpdaterPath))) { if (!PathAppend(MEGAcmdUpdaterPath,TEXT("MEGAcmdUpdater.exe"))) { LOG_err << "Couldnt append MEGAcmdUpdater exec: " << wstring(MEGAcmdUpdaterPath); return false; } } else { LOG_err << "Couldnt remove file spec: " << wstring(MEGAcmdUpdaterPath); return false; } CoInitializeEx(NULL, COINIT_MULTITHREADED); if (SUCCEEDED(CoInitializeSecurity(NULL, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, 0, NULL)) && SUCCEEDED(CoCreateInstance(CLSID_TaskScheduler, NULL, CLSCTX_INPROC_SERVER, IID_ITaskService, (void**)&pService)) && SUCCEEDED(pService->Connect(_variant_t(), _variant_t(), _variant_t(), _variant_t())) && SUCCEEDED(pService->GetFolder(_bstr_t( L"\\"), &pRootFolder))) { if (pRootFolder->CreateFolder(_bstr_t(L"MEGA"), _variant_t(L""), &pMEGAFolder) == 0x800700b7) { pRootFolder->GetFolder(_bstr_t(L"MEGA"), &pMEGAFolder); } if (pMEGAFolder && SUCCEEDED(pService->NewTask(0, &pTask)) && SUCCEEDED(pTask->get_RegistrationInfo(&pRegInfo)) && SUCCEEDED(pRegInfo->put_Author(_bstr_t(L"MEGA Limited"))) && SUCCEEDED(pTask->get_Principal(&pPrincipal)) && SUCCEEDED(pPrincipal->put_Id(_bstr_t(L"Principal1"))) && SUCCEEDED(pPrincipal->put_LogonType(TASK_LOGON_INTERACTIVE_TOKEN)) && SUCCEEDED(pPrincipal->put_RunLevel(TASK_RUNLEVEL_LUA)) && SUCCEEDED(pPrincipal->put_UserId(userId)) && SUCCEEDED(pTask->get_Settings(&pSettings)) && SUCCEEDED(pSettings->put_StartWhenAvailable(VARIANT_TRUE)) && SUCCEEDED(pSettings->put_DisallowStartIfOnBatteries(VARIANT_FALSE)) && SUCCEEDED(pSettings->get_IdleSettings(&pIdleSettings)) && SUCCEEDED(pIdleSettings->put_StopOnIdleEnd(VARIANT_FALSE)) && SUCCEEDED(pIdleSettings->put_RestartOnIdle(VARIANT_FALSE)) && SUCCEEDED(pIdleSettings->put_WaitTimeout(_bstr_t())) && SUCCEEDED(pIdleSettings->put_IdleDuration(_bstr_t())) && SUCCEEDED(pTask->get_Triggers(&pTriggerCollection)) && SUCCEEDED(pTriggerCollection->Create(TASK_TRIGGER_DAILY, &pTrigger)) && SUCCEEDED(pTrigger->QueryInterface(IID_IDailyTrigger, (void**) &pCalendarTrigger)) && SUCCEEDED(pCalendarTrigger->put_Id(_bstr_t(L"Trigger1"))) && SUCCEEDED(pCalendarTrigger->put_DaysInterval(1)) && SUCCEEDED(pCalendarTrigger->put_StartBoundary(_bstr_t(currentTimeString))) && SUCCEEDED(pCalendarTrigger->get_Repetition(&pRepetitionPattern)) && SUCCEEDED(pRepetitionPattern->put_Duration(_bstr_t(L"P1D"))) && SUCCEEDED(pRepetitionPattern->put_Interval(_bstr_t(L"PT2H"))) && SUCCEEDED(pRepetitionPattern->put_StopAtDurationEnd(VARIANT_FALSE)) && SUCCEEDED(pTask->get_Actions(&pActionCollection)) && SUCCEEDED(pActionCollection->Create(TASK_ACTION_EXEC, &pAction)) && SUCCEEDED(pAction->QueryInterface(IID_IExecAction, (void**)&pExecAction)) && SUCCEEDED(pExecAction->put_Path(_bstr_t(MEGAcmdUpdaterPath))) && SUCCEEDED(pExecAction->put_Arguments(_bstr_t(L"--emergency-update")))) { if (SUCCEEDED(pMEGAFolder->RegisterTaskDefinition(taskName, pTask, TASK_CREATE_OR_UPDATE, _variant_t(), _variant_t(), TASK_LOGON_INTERACTIVE_TOKEN, _variant_t(L""), &pRegisteredTask))) { success = true; LOG_err << "Update task registered OK"; } else { LOG_err << "Error registering update task"; } } else { LOG_err << "Error creating update task"; } } else { LOG_err << "Error getting root task folder"; } if (pRegisteredTask) { pRegisteredTask->Release(); } if (pTrigger) { pTrigger->Release(); } if (pTriggerCollection) { pTriggerCollection->Release(); } if (pIdleSettings) { pIdleSettings->Release(); } if (pSettings) { pSettings->Release(); } if (pPrincipal) { pPrincipal->Release(); } if (pRegInfo) { pRegInfo->Release(); } if (pCalendarTrigger) { pCalendarTrigger->Release(); } if (pAction) { pAction->Release(); } if (pActionCollection) { pActionCollection->Release(); } if (pRepetitionPattern) { pRepetitionPattern->Release(); } if (pExecAction) { pExecAction->Release(); } if (pTask) { pTask->Release(); } if (pMEGAFolder) { pMEGAFolder->Release(); } if (pRootFolder) { pRootFolder->Release(); } if (pService) { pService->Release(); } return success; #elif defined(__MACH__) return registerUpdateDaemon(); #else return true; #endif } bool getloginInAtStartup() { return loginInAtStartup; } ::mega::m_time_t timeLoginStarted() { return timeOfLoginInAtStartup; } void setloginInAtStartup(bool value) { loginInAtStartup = value; if (value) { timeOfLoginInAtStartup = m_time(NULL); } updatevalidCommands(); } void unblock() { setBlocked(0); sandboxCMD->setReasonblocked(""); broadcastMessage("Your account is not longer blocked"); if (!api->isFilesystemAvailable()) { auto theSandbox = sandboxCMD; std::thread t([theSandbox](){ theSandbox->cmdexecuter->fetchNodes(); }); } } void setBlocked(int value) { if (blocked != value) { blocked = value; updatevalidCommands(); cmdexecuter->updateprompt(); } } int getBlocked() { return blocked; } void updatevalidCommands() { if (loginInAtStartup || blocked) { validCommands = loginInValidCommands; } else { validCommands = allValidCommands; } } void reset() { setBlocked(false); } void sendEvent(StatsManager::MegacmdEvent event, const char *msg, ::mega::MegaApi *megaApi, bool wait) { #if defined(DEBUG) || defined(MEGACMD_TESTING_CODE) LOG_debug << "Skipped MEGAcmd event " << eventName(event) << " - " << msg; #else std::unique_ptr megaCmdListener (wait ? new MegaCmdListener(megaApi) : nullptr); megaApi->sendEvent(static_cast(event), msg, false /*JourneyId*/, nullptr /*viewId*/, megaCmdListener.get()); if (wait) { megaCmdListener->wait(); assert(megaCmdListener->getError()); if (megaCmdListener->getError()->getErrorCode() != MegaError::API_OK) { LOG_err << "Failed to log event " << StatsManager::eventName(event) << ": " << msg << ", error: " << megaCmdListener->getError()->getErrorString(); } } #endif } void sendEvent(StatsManager::MegacmdEvent event, ::mega::MegaApi *megaApi, bool wait) { return sendEvent(event, StatsManager::defaultEventMsg(event), megaApi, wait); } #ifdef _WIN32 void uninstall() { std::error_code ec; // to use the non-throwing overload below fs::remove_all(ConfigurationManager::getConfigFolder(), ec); ITaskService *pService = NULL; ITaskFolder *pRootFolder = NULL; ITaskFolder *pMEGAFolder = NULL; _bstr_t taskBaseName = L"MEGAcmd Update Task "; LPTSTR stringSID = NULL; stringSID = getCurrentSid(); if (!stringSID) { std::cerr << "ERROR UNINSTALLING: Unable to get the current SID" << std::endl; return; } _bstr_t taskName = taskBaseName + stringSID; CoInitializeEx(NULL, COINIT_MULTITHREADED); if (SUCCEEDED(CoInitializeSecurity(NULL, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, 0, NULL)) && SUCCEEDED(CoCreateInstance(CLSID_TaskScheduler, NULL, CLSCTX_INPROC_SERVER, IID_ITaskService, (void**)&pService)) && SUCCEEDED(pService->Connect(_variant_t(), _variant_t(), _variant_t(), _variant_t())) && SUCCEEDED(pService->GetFolder(_bstr_t( L"\\"), &pRootFolder))) { if (pRootFolder->CreateFolder(_bstr_t(L"MEGA"), _variant_t(L""), &pMEGAFolder) == 0x800700b7) { pRootFolder->GetFolder(_bstr_t(L"MEGA"), &pMEGAFolder); } if (pMEGAFolder) { pMEGAFolder->DeleteTask(taskName, 0); pMEGAFolder->Release(); } pRootFolder->Release(); } if (pService) { pService->Release(); } } #endif void setFuseLogLevel(MegaApi& api, const std::string& fuseLogLevelStr) { std::unique_ptr fuseFlags(api.getFUSEFlags()); assert(fuseFlags); int sdkLogLevel = fuseFlags->getLogLevel(); try { sdkLogLevel = std::stoi(fuseLogLevelStr); } catch (...) {} sdkLogLevel = std::clamp(sdkLogLevel, (int) ::mega::LogLevel::logError, (int) ::mega::LogLevel::logVerbose); fuseFlags->setLogLevel(sdkLogLevel); api.setFUSEFlags(fuseFlags.get()); LOG_debug << "FUSE log level set to " << sdkLogLevel; } void disableFuseExplorerListView(MegaApi& api) { std::unique_ptr fuseFlags(api.getFUSEFlags()); assert(fuseFlags); fuseFlags->setFileExplorerView((int) MegaFuseFlags::FILE_EXPLORER_VIEW_NONE); api.setFUSEFlags(fuseFlags.get()); LOG_debug << "FUSE FileExplorer view set to NONE (disabled list view)"; } int executeServer(int argc, char* argv[], const std::function& createLoggedStream, const LogConfig& logConfig, bool skiplockcheck, std::string debug_api_url, bool disablepkp) { // Own global server instances here Instance sDefaultLoggedStream; Instance sConfiguratorHelper; #ifdef __linux__ // Ensure interesting signals are unblocked. sigset_t signalstounblock; sigemptyset (&signalstounblock); sigaddset(&signalstounblock, SIGUSR1); sigaddset(&signalstounblock, SIGUSR2); sigprocmask(SIG_UNBLOCK, &signalstounblock, NULL); if (signal(SIGUSR1, LinuxSignalHandler)) //TODO: do this after startup? { cerr << " Failed to register signal SIGUSR1 " << endl; } if (signal(SIGUSR2, LinuxSignalHandler)) { cerr << " Failed to register signal SIGUSR2 " << endl; } #endif string localecode = getLocaleCode(); #ifdef _WIN32 // Set Environment's default locale setlocale(LC_ALL, "en-US"); #endif printWelcomeMsg(); // keep a copy of argc & argv in order to allow restarts mcmdMainArgv = argv; mcmdMainArgc = argc; ConfigurationManager::loadConfiguration(logConfig.mCmdLogLevel >= MegaApi::LOG_LEVEL_DEBUG); if (!ConfigurationManager::lockExecution() && !skiplockcheck) { cerr << "Another instance of MEGAcmd Server is running. Execute with --skip-lock-check to force running (NOT RECOMMENDED)" << endl; sleepSeconds(5); return -2; } // The logger stream must be created after the configuration is loaded (so the .megaCmd directory is created if necessary) if (createLoggedStream) { Instance::Get().setLoggedStream(std::unique_ptr(createLoggedStream())); } // Establish the logger MegaApi::setLogLevel(MegaApi::LOG_LEVEL_MAX); // do not filter anything here, log level checking is done by loggerCMD loggerCMD = new MegaCmdSimpleLogger(logConfig.mLogToCout, logConfig.mSdkLogLevel, logConfig.mCmdLogLevel); MegaApi::addLoggerObject(loggerCMD); char userAgent[40]; sprintf(userAgent, "MEGAcmd" MEGACMD_STRINGIZE(MEGACMD_USERAGENT_SUFFIX) "/%d.%d.%d.%d", MEGACMD_MAJOR_VERSION,MEGACMD_MINOR_VERSION,MEGACMD_MICRO_VERSION,MEGACMD_BUILD_ID); LOG_info << "----------------------- program start -----------------------"; LOG_debug << "MEGAcmd version: " << MEGACMD_MAJOR_VERSION << "." << MEGACMD_MINOR_VERSION << "." << MEGACMD_MICRO_VERSION << "." << MEGACMD_BUILD_ID << ": code " << MEGACMD_CODE_VERSION; LOG_debug << "MEGA SDK version: " << SDK_COMMIT_HASH; const fs::path configDirPath = ConfigurationManager::getAndCreateConfigDir(); const std::string configDirStrUtf8 = pathAsUtf8(configDirPath); api = new MegaApi("BdARkQSQ", configDirStrUtf8.c_str(), userAgent); if (!debug_api_url.empty()) { api->changeApiUrl(debug_api_url.c_str(), disablepkp); } api->setLanguage(localecode.c_str()); if (logConfig.mJsonLogs) { MegaApi::setLogJSON(MegaApi::JSON_LOG_CHUNK_RECEIVED | MegaApi::JSON_LOG_CHUNK_CONSUMED | MegaApi::JSON_LOG_SENDING | MegaApi::JSON_LOG_NONCHUNK_RECEIVED); MegaApi::setMaxPayloadLogSize(0); // Max size } LOG_debug << "Language set to: " << localecode; sandboxCMD = new MegaCmdSandbox(); cmdexecuter = new MegaCmdExecuter(api, loggerCMD, sandboxCMD); sandboxCMD->cmdexecuter = cmdexecuter; auto cmdFatalErrorListener = std::make_unique(*sandboxCMD); auto numberOfApiFolders = ConfigurationManager::getConfigurationValue("exported_folders_sdks", 5); LOG_debug << "Loading " << numberOfApiFolders << " auxiliar MegaApi folders"; for (int i = 0; i < numberOfApiFolders; i++) { const fs::path apiFolderPath = ConfigurationManager::getConfigFolderSubdir("apiFolder_" + std::to_string(i)); const std::string apiFolderStrUtf8 = pathAsUtf8(apiFolderPath); MegaApi *apiFolder = new MegaApi("BdARkQSQ", apiFolderStrUtf8.c_str(), userAgent); apiFolder->setLanguage(localecode.c_str()); apiFolder->addGlobalListener(cmdFatalErrorListener.get()); apiFolders.push(apiFolder); semaphoreapiFolders.release(); } for (int i = 0; i < 100; i++) { semaphoreClients.release(); } if (const char* fuseLogLevelStr = getenv("MEGACMD_FUSE_LOG_LEVEL"); fuseLogLevelStr) { setFuseLogLevel(*api, fuseLogLevelStr); } if (getenv("MEGACMD_FUSE_DISABLE_LIST_VIEW")) { disableFuseExplorerListView(*api); } GlobalSyncConfig::loadFromConfigurationManager(*api); megaCmdGlobalListener = new MegaCmdGlobalListener(loggerCMD, sandboxCMD); megaCmdMegaListener = new MegaCmdMegaListener(api, NULL, sandboxCMD); api->addGlobalListener(megaCmdGlobalListener); api->addGlobalListener(cmdFatalErrorListener.get()); api->addListener(megaCmdMegaListener); // set up the console #ifdef _WIN32 console = new CONSOLE_CLASS; #else struct termios term; if ( ( tcgetattr(STDIN_FILENO, &term) < 0 ) || runningInBackground() ) //try console { consoleFailed = true; console = NULL; } else { console = new CONSOLE_CLASS; } #endif cm = new COMUNICATIONMANAGER(); #if _WIN32 if( SetConsoleCtrlHandler( (PHANDLER_ROUTINE) CtrlHandler, TRUE ) ) { LOG_debug << "Control handler set"; } else { LOG_warn << "Control handler set"; } #else // prevent CTRL+C exit if (!consoleFailed){ signal(SIGINT, sigint_handler); } #endif atexit(finalize); #if defined(_WIN32) || defined(__APPLE__) if (!ConfigurationManager::getConfigurationValue("updaterregistered", false)) { LOG_debug << "Registering automatic updater"; if (registerUpdater()) { ConfigurationManager::savePropertyValue("updaterregistered", true); LOG_verbose << "Registered automatic updater"; } else { LOG_err << "Failed to register automatic updater"; } } #endif int configuredProxyType = ConfigurationManager::getConfigurationValue("proxy_type", -1); auto configuredProxyUrl = ConfigurationManager::getConfigurationSValue("proxy_url"); auto configuredProxyUsername = ConfigurationManager::getConfigurationSValue("proxy_username"); auto configuredProxyPassword = ConfigurationManager::getConfigurationSValue("proxy_password"); if (configuredProxyType != -1 && configuredProxyType != MegaProxy::PROXY_AUTO) //AUTO is default, no need to set { std::string command("proxy "); command.append(configuredProxyUrl); if (configuredProxyUsername.size()) { command.append(" --username=").append(configuredProxyUsername); if (configuredProxyPassword.size()) { command.append(" --password=").append(configuredProxyPassword); } } processCommandLinePetitionQueues(command); } if (ConfigurationManager::getHasBeenUpdated()) { // Wait for this event to ensure an automatic login on startup doesn't prevent the event from being sent sendEvent(StatsManager::MegacmdEvent::UPDATE, api, true); stringstream ss; ss << "MEGAcmd has been updated to version " << MEGACMD_MAJOR_VERSION << "." << MEGACMD_MINOR_VERSION << "." << MEGACMD_MICRO_VERSION << "." << MEGACMD_BUILD_ID << " - code " << MEGACMD_CODE_VERSION << endl; broadcastMessage(ss.str(), true); } if (!ConfigurationManager::session.empty()) { loginInAtStartup = true; stringstream logLine; logLine << "login " << ConfigurationManager::session; LOG_debug << "Executing ... " << logLine.str().substr(0,9) << "..."; processCommandLinePetitionQueues(logLine.str()); } megacmd::megacmd(); finalize(waitForRestartSignal); return 0; } void stopServer() { LOG_debug << "Executing ... mega-quit ..."; processCommandLinePetitionQueues("quit"); //TODO: have set doExit instead, and wake the loop. } std::optional lookForAvailableNewerVersions(::mega::MegaApi *api) { #ifdef __linux__ return {}; // Linux updates are _announced_ via packages manageres #endif //NOTE: expected to be called from main megacmd thread (no concurrency control required) static HammeringLimiter hammeringLimiter(300); if (hammeringLimiter.runRecently()) { return {}; } ostringstream os; auto megaCmdListener = std::make_unique(api); api->getLastAvailableVersion("BdARkQSQ", megaCmdListener.get()); if (megaCmdListener->trywait(2000)) //timed out: { LOG_debug << "Couldn't get latests available version (petition timed out)"; api->removeRequestListener(megaCmdListener.get()); return {}; } if (!megaCmdListener->getError()) { LOG_fatal << "No MegaError at getLastAvailableVersion: "; } else if (megaCmdListener->getError()->getErrorCode() != MegaError::API_OK) { LOG_debug << "Couldn't get latests available version: " << megaCmdListener->getError()->getErrorString(); } else { if (megaCmdListener->getRequest()->getNumber() != MEGACMD_CODE_VERSION) { os << "---------------------------------------------------------------------" << endl; os << "-- There is a new version available of megacmd: " << setw(12) << left << megaCmdListener->getRequest()->getName() << "--" << endl; os << "-- Please, update this one: See \"update --help\". --" << endl; os << "-- Or download the latest from https://mega.nz/cmd --" << endl; #if defined(__APPLE__) os << "-- Before installing enter \"exit\" to close MEGAcmd --" << endl; #endif os << "---------------------------------------------------------------------" << endl; } return os.str(); } return {}; } } //end namespace MEGAcmd-2.5.2_Linux/src/megacmd.h000066400000000000000000000134021516543156300164420ustar00rootroot00000000000000/** * @file src/megacmd.h * @brief MEGAcmd: Interactive CLI and service application * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of the MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #ifndef MEGACMD_H #define MEGACMD_H #include #include #ifdef _WIN32 #include #endif using std::cout; using std::endl; using std::max; using std::min; using std::flush; using std::left; using std::cerr; using std::istringstream; using std::locale; using std::stringstream; using std::exception; #include "megacmdcommonutils.h" #include "megaapi_impl.h" #include "megacmd_events.h" #define PROGRESS_COMPLETE -2 namespace megacmd { typedef struct sync_struct { mega::MegaHandle handle; bool active; std::string localpath; long long fingerprint; bool loadedok; //ephimeral data } sync_struct; typedef struct backup_struct { mega::MegaHandle handle; bool active; std::string localpath; //TODO: review wether this is local or utf-8 representation and be consistent int64_t period; std::string speriod; int numBackups; bool failed; //This should mark the failure upon resuming. It shall not be persisted int tag; //This is depends on execution. should not be persisted int id; //Internal id for megacmd. Depends on execution should not be persisted } backup_istruct; enum prompttype { COMMAND, LOGINPASSWORD, NEWPASSWORD, PASSWORDCONFIRM, AREYOUSURETODELETE }; static const char* const prompts[] = { "MEGA CMD> ", "Password:", "New Password:", "Retype New Password:", "Are you sure to delete? " }; void changeprompt(const char *newprompt); void informStateListener(std::string message, int clientID); void broadcastMessage(std::string message, bool keepIfNoListeners = false); void informStateListeners(std::string s); void removeDelayedBroadcastMatching(const std::string &toMatch); void broadcastDelayedMessage(std::string message, bool keepIfNoListeners); void appendGreetingStatusFirstListener(const std::string &msj); void removeGreetingStatusFirstListener(const std::string &msj); void appendGreetingStatusAllListener(const std::string &msj); void clearGreetingStatusAllListener(); void clearGreetingStatusFirstListener(); void removeGreetingStatusAllListener(const std::string &msj); void removeGreetingMatching(const std::string &toMatch); void removeDelayedBroadcastMatching(const std::string &toMatch); void setloginInAtStartup(bool value); void setBlocked(int value); int getBlocked(); void unblock(); bool getloginInAtStartup(); void updatevalidCommands(); void reset(); /** * @brief A class to ensure clients are properly informed of login in situations */ class LoginGuard { public: LoginGuard() { appendGreetingStatusAllListener(std::string("login:")); setloginInAtStartup(true); } ~LoginGuard() { removeGreetingStatusAllListener(std::string("login:")); informStateListeners("loged:"); //send this even when failed! setloginInAtStartup(false); } }; mega::MegaApi* getFreeApiFolder(); void freeApiFolder(mega::MegaApi *apiFolder); struct HelpFlags { bool win = false; bool apple = false; bool usePcre = false; bool haveLibuv = false; bool readline = true; bool fuse = false; bool showAll = false; HelpFlags(bool showAll = false) : showAll(showAll) { #ifdef USE_PCRE usePcre = true; #endif #ifdef _WIN32 win = true; #endif #ifdef __APPLE__ apple = true; #endif #ifdef HAVE_LIBUV haveLibuv = true; #endif #ifdef NO_READLINE readline = false; #endif #ifdef WITH_FUSE fuse = true; #endif } }; const char * getUsageStr(const char *command, const HelpFlags& flags = {}); void unescapeifRequired(std::string &what); void setprompt(prompttype p, std::string arg = ""); prompttype getprompt(); void printHistory(); int askforConfirmation(std::string message); bool booleanAskForConfirmation(std::string messageHeading); std::string askforUserResponse(std::string message); void* checkForUpdates(void *param); void stopcheckingForUpdates(); void startcheckingForUpdates(); /** * @brief synchronously request the API for a a new version of MEGAcmd * It will skip the check if already checked in the past 5 minutes * @param api * @return a string with a msg with the announcement if a new version is available */ std::optional lookForAvailableNewerVersions(::mega::MegaApi *api); void informTransferUpdate(mega::MegaTransfer *transfer, int clientID); void informStateListenerByClientId(int clientID, std::string s); void informProgressUpdate(long long transferred, long long total, int clientID, std::string title = ""); void sendEvent(StatsManager::MegacmdEvent event, mega::MegaApi *megaApi, bool wait = true); void sendEvent(StatsManager::MegacmdEvent event, const char *msg, mega::MegaApi *megaApi, bool wait = true); #ifdef _WIN32 void uninstall(); #endif struct LogConfig { int mSdkLogLevel = mega::MegaApi::LOG_LEVEL_DEBUG; int mCmdLogLevel = mega::MegaApi::LOG_LEVEL_DEBUG; bool mLogToCout = true; bool mJsonLogs = false; }; class LoggedStream; // forward delaration int executeServer(int argc, char* argv[], const std::function& createLoggedStream = nullptr, const LogConfig& logConfig = {}, bool skiplockcheck = false, std::string debug_api_url = {}, bool disablepkp/*only for debugging*/ = false); void stopServer(); }//end namespace #endif MEGAcmd-2.5.2_Linux/src/megacmd_events.cpp000066400000000000000000000030011516543156300203530ustar00rootroot00000000000000/** * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include "megacmd_events.h" #include namespace megacmd::StatsManager { const char * defaultEventMsg(MegacmdEvent ev) { static auto sDefaultMsgs = []() { std::array(MegacmdEvent::LastEvent) - FIRST_EVENT_NUMBER> defaultMsgs; #define SOME_GENERATOR_MACRO(name, _, msg) defaultMsgs[static_cast(MegacmdEvent::name) - FIRST_EVENT_NUMBER] = msg; GENERATE_FROM_MEGACMD_EVENTS(SOME_GENERATOR_MACRO) #undef SOME_GENERATOR_MACRO return defaultMsgs; }(); return sDefaultMsgs[static_cast(ev) - FIRST_EVENT_NUMBER]; } const char * eventName(MegacmdEvent ev) { static auto sNames = []() { std::array(MegacmdEvent::LastEvent) - FIRST_EVENT_NUMBER> names; #define SOME_GENERATOR_MACRO(name, _, __) names[static_cast(MegacmdEvent::name) - FIRST_EVENT_NUMBER] = #name; GENERATE_FROM_MEGACMD_EVENTS(SOME_GENERATOR_MACRO) #undef SOME_GENERATOR_MACRO return names; }(); return sNames[static_cast(ev) - FIRST_EVENT_NUMBER]; } } // end of namespaces MEGAcmd-2.5.2_Linux/src/megacmd_events.h000066400000000000000000000062471516543156300200370ustar00rootroot00000000000000/** * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #pragma once namespace megacmd::StatsManager { /** MEGAcmd events **/ // Allocated ranges: // - regular: [98'900,98'999] // - extended [860'000,879'999] // Params: // - event name // - number // - Default message #define GENERATE_FROM_MEGACMD_EVENTS(GENERATOR_MACRO) \ GENERATOR_MACRO(UPDATE , 98900, "MEGAcmd update") \ GENERATOR_MACRO(UPDATE_START , 98901, "MEGAcmd auto-update start") \ GENERATOR_MACRO(UPDATE_RESTART , 98902, "MEGAcmd updated requiring restart") \ GENERATOR_MACRO(FIRST_CONFIGURED_SYNC , 98903, "MEGAcmd first sync configured") \ GENERATOR_MACRO(WAITED_TOO_LONG_FOR_NODES_CURRENT , 98904, "MEGAcmd nodes current wait timed out") \ GENERATOR_MACRO(ROOT_NODE_NOT_FOUND_AFTER_FETCHING , 98906, "MEGAcmd root node was not found after fetching nodes") \ GENERATOR_MACRO(TRANSITIONING_PRE_SRW_EXCLUSIONS , 98907, "MEGAcmd transition of legacy exclusion rules started") \ GENERATOR_MACRO(FIRST_CONFIGURED_WEBDAV , 98908, "MEGAcmd first WebDAV location configured") \ GENERATOR_MACRO(FIRST_CONFIGURED_FTP , 98909, "MEGAcmd first FTP location configured") \ GENERATOR_MACRO(FIRST_CONFIGURED_SCHEDULED_BACKUP , 98910, "MEGAcmd first scheduled backup configured") \ GENERATOR_MACRO(SUBSEQUENT_CONFIGURED_SYNC , 98911, "MEGAcmd subsequent sync configured") \ GENERATOR_MACRO(SUBSEQUENT_CONFIGURED_WEBDAV , 98912, "MEGAcmd subsequent WebDAV location configured") \ GENERATOR_MACRO(SUBSEQUENT_CONFIGURED_FTP , 98913, "MEGAcmd subsequent FPT location configured") \ GENERATOR_MACRO(SUBSEQUENT_CONFIGURED_SCHEDULED_BACKUP , 98914, "MEGAcmd subsequent scheduled backup configured") \ GENERATOR_MACRO(INVALID_UTF8_INCIDENCES , 98915, "MEGAcmd Found Invalid UTF-8 incidences") \ GENERATOR_MACRO(FIRST_CONFIGURED_FUSE_MOUNT , 98916, "MEGAcmd first FUSE mount configured") \ GENERATOR_MACRO(SUBSEQUENT_CONFIGURED_FUSE_MOUNT , 98917, "MEGAcmd subsequent FUSE mount configured") \ static constexpr auto FIRST_EVENT_NUMBER = 98900u; enum class MegacmdEvent { #define SOME_GENERATOR_MACRO(name, num, __) name = num, GENERATE_FROM_MEGACMD_EVENTS(SOME_GENERATOR_MACRO) #undef SOME_GENERATOR_MACRO LastEvent }; const char *defaultEventMsg(MegacmdEvent ev); const char *eventName(MegacmdEvent ev); } // end of namespaces MEGAcmd-2.5.2_Linux/src/megacmd_fuse.cpp000066400000000000000000000344031516543156300200230ustar00rootroot00000000000000/** * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include "megacmd_fuse.h" namespace FuseCommand { std::string_view getDisclaimer() { return "Disclaimer:\n" " - Streaming is not supported; entire files need to be downloaded completely before being opened.\n" " - FUSE uses a local cache located in the MEGAcmd configuration folder. Make sure you have enough available space " "in your hard drive to accommodate new files. Restarting MEGAcmd server can help discard old files.\n" " - File writes might be deferred. When files are updated in the local mount point, a transfer will be initiated. " "Your files will be available in MEGA only after pending transfers finish."; } std::string_view getBetaMsg() { return "FUSE commands are in early BETA. They are not available in macOS. If you experience any issues, please contact support@mega.nz."; } std::string_view getIdentifierParameter() { return " name|localPath The identifier of the mount we want to remove. It can be one of the following:\n" " Name: the user-friendly name of the mount, specified when it was added or by fuse-config.\n" " Local path: The local mount point in the filesystem."; } } #ifdef WITH_FUSE #include "listeners.h" #include "megacmdlogger.h" #include "configurationmanager.h" using namespace mega; using namespace megacmd; namespace FuseCommand { namespace { constexpr const char* sFirstMountConfigKey = "firstFuseMountConfigured"; std::string getNodePath(MegaApi& api, MegaNode& node) { std::unique_ptr nodePathPtr(api.getNodePath(&node)); return (nodePathPtr ? nodePathPtr.get() : ""); } std::string getDefaultNodeName(MegaNode& node) { switch (node.getType()) { case MegaNode::TYPE_FILE: case MegaNode::TYPE_FOLDER: return node.getName(); case MegaNode::TYPE_ROOT: return "MEGA"; case MegaNode::TYPE_RUBBISH: return "MEGA Rubbish"; case MegaNode::TYPE_VAULT: return "MEGA Vault"; case MegaNode::TYPE_UNKNOWN: default: return ""; } } // convenience function that returns the unique identifier (name) surrounded by quotes std::string getMountId(const MegaMount& mount) { return std::string("\"").append(mount.getFlags()->getName()).append("\""); } bool shouldRememberChange(const MegaMount& mount, bool temporarily) { MegaMountFlags* mountFlags = mount.getFlags(); assert(mountFlags != nullptr); if (!mountFlags->getPersistent()) { return false; } return !temporarily; } std::unique_ptr getMountByName(MegaApi& api, const std::string& name) { return std::unique_ptr(api.getMountInfo(name.c_str())); } std::unique_ptr getMountByPath(MegaApi& api, const std::string& path, bool& clash) { std::unique_ptr mounts(api.listMounts(false)); if (mounts == nullptr) { return nullptr; } std::unique_ptr firstFound; for (size_t i = 0; i < mounts->size(); ++i) { const MegaMount* mount = mounts->get(i); assert(mount != nullptr); if (mount->getPath() == path) { if (!firstFound) { firstFound.reset(mount->copy()); } else { clash = true; return nullptr; } } } return firstFound; } std::unique_ptr createMount(const fs::path& localPath, MegaNode& node, bool disabled, bool transient, bool readOnly, std::string name) { std::unique_ptr mount(MegaMount::create()); assert(mount != nullptr); mount->setHandle(node.getHandle()); const std::string localPathAsUtf8 = pathAsUtf8(localPath); mount->setPath(localPathAsUtf8.c_str()); MegaMountFlags* mountFlags = mount->getFlags(); assert(mountFlags != nullptr); if (name.empty()) { name = getDefaultNodeName(node); } mountFlags->setName(name.c_str()); mountFlags->setReadOnly(readOnly); mountFlags->setEnableAtStartup(!transient && !disabled); mountFlags->setPersistent(!transient); return mount; } void printFirstMountMessage() { OUTSTREAM << "\n---------------------\n"; OUTSTREAM << getDisclaimer(); OUTSTREAM << "\n"; OUTSTREAM << getBetaMsg(); OUTSTREAM << "\n---------------------\n\n"; } } // end namespace std::unique_ptr getMountByNameOrPath(MegaApi& api, const std::string& identifier) { auto mount = getMountByName(api, identifier); if (mount != nullptr) { return mount; } bool clash = false; mount = getMountByPath(api, identifier, clash); if (clash) { assert(mount == nullptr); LOG_err << "Multiple mounts exist with path \"" << identifier << "\". Use mount name to select one"; } else if (mount == nullptr) { LOG_err << "Identifier \"" << identifier << "\" does not correspond to a valid mount name or local path"; } return mount; } std::string getActionString(std::string_view action, const fs::path &localPath, const std::string &nodePath) { return std::string(action).append(" mount from \"").append(localPath.string()).append("\" to \"").append(nodePath).append("\""); } std::string getActionString(std::string_view action, const mega::MegaMount& mount) { return std::string(action).append(" mount ").append(getMountId(mount)).append(" on \"").append(mount.getPath()).append("\""); } void addMount(mega::MegaApi& api, const fs::path& localPath, MegaNode& node, bool disabled, bool transient, bool readOnly, const std::string& name) { const std::string nodePath = getNodePath(api, node); auto mountToCreate = createMount(localPath, node, disabled, transient, readOnly, name); auto listener = std::make_unique(nullptr); { DisableMountErrorsBroadcastingGuard disableErrorBroadcasting; api.addMount(mountToCreate.get(), listener.get()); if (!checkNoErrors(listener.get(), getActionString("add", localPath, nodePath))) { return; } } OUTSTREAM << "Added a new mount"; if (listener->getRequest()->getFile() && strlen(listener->getRequest()->getFile())) { OUTSTREAM << " from \"" << listener->getRequest()->getFile() << "\""; } OUTSTREAM << " to \"" << nodePath << '"' << endl; const bool isFirstMount = !ConfigurationManager::getConfigurationValue(sFirstMountConfigKey, false); if (isFirstMount) { printFirstMountMessage(); sendEvent(StatsManager::MegacmdEvent::FIRST_CONFIGURED_FUSE_MOUNT, &api, false); ConfigurationManager::savePropertyValue(sFirstMountConfigKey, true); } else { sendEvent(StatsManager::MegacmdEvent::SUBSEQUENT_CONFIGURED_FUSE_MOUNT, &api, false); } if (!disabled) { enableMount(api, *mountToCreate, transient); } } void removeMount(mega::MegaApi& api, const mega::MegaMount& mount) { { DisableMountErrorsBroadcastingGuard disableErrorBroadcasting; auto isEnabled = api.isMountEnabled(mount.getFlags()->getName()); if (isEnabled) { auto listener = std::make_unique(nullptr); api.disableMount(mount.getFlags()->getName(), listener.get(), true /*remember*/); if (!checkNoErrors(listener.get(), getActionString("disable before removing", mount))) { return; } } auto listener = std::make_unique(nullptr); api.removeMount(mount.getFlags()->getName(), listener.get()); if (!checkNoErrors(listener.get(), getActionString("remove", mount))) { return; } } OUTSTREAM << "Removed mount " << getMountId(mount) << " on \"" << mount.getPath() << '"' << endl; } void enableMount(mega::MegaApi& api, const mega::MegaMount& mount, bool temporarily) { auto listener = std::make_unique(nullptr); const bool remember = shouldRememberChange(mount, temporarily); { DisableMountErrorsBroadcastingGuard disableErrorBroadcasting; api.enableMount(mount.getFlags()->getName(), listener.get(), remember); if (!checkNoErrors(listener.get(), getActionString("enable", mount))) { return; } } auto mountEnabled = getMountByNameOrPath(api, mount.getFlags()->getName()); if (!mountEnabled) { LOG_err << "Unable to get mount info after enabled"; setCurrentThreadOutCode(MCMD_NOTFOUND); return; } OUTSTREAM << (temporarily ? "Temporarily enabled" : "Enabled") << " mount " << getMountId(*mountEnabled) << " on \"" << mountEnabled->getPath() << '"' << endl; } void disableMount(mega::MegaApi& api, const mega::MegaMount& mount, bool temporarily) { auto listener = std::make_unique(nullptr); const bool remember = shouldRememberChange(mount, temporarily); { DisableMountErrorsBroadcastingGuard disableErrorBroadcasting; api.disableMount(mount.getFlags()->getName(), listener.get(), remember); if (!checkNoErrors(listener.get(), getActionString("disable", mount))) { return; } } OUTSTREAM << (temporarily ? "Temporarily disabled" : "Disabled") << " mount " << getMountId(mount) << " on \"" << mount.getPath() << '"' << endl; } void printMount(mega::MegaApi& api, const mega::MegaMount& mount) { const MegaHandle handle = mount.getHandle(); std::unique_ptr remotePath(api.getNodePathByNodeHandle(handle)); const MegaMountFlags* flags = mount.getFlags(); assert(flags != nullptr); std::ostringstream oss; oss << "Showing details of mount " << getMountId(mount) << "\n" << " Local path: " << mount.getPath() << "\n" << " Remote path: " << (remotePath ? remotePath.get() : "") << "\n" << " Name: " << flags->getName() << "\n" << " Persistent: " << (flags->getPersistent() ? "YES" : "NO") << "\n" << " Enabled: " << (api.isMountEnabled(flags->getName()) ? "YES" : "NO") << "\n" << " Enable at startup: " << (flags->getEnableAtStartup() ? "YES" : "NO") << "\n" << " Read-only: " << (flags->getReadOnly() ? "YES" : "NO") << "\n"; OUTSTREAM << oss.str(); } void printAllMounts(mega::MegaApi& api, ColumnDisplayer& cd, bool onlyEnabled, bool disablePathCollapse, int rowCountLimit) { std::unique_ptr mounts(api.listMounts(onlyEnabled)); if (mounts == nullptr) { LOG_err << "Failed to retrieve the list of mounts"; return; } if (mounts->size() == 0) { OUTSTREAM << "There are no mounts" << (onlyEnabled ? " enabled" : "") << endl; return; } cd.addHeader("LOCAL_PATH", disablePathCollapse); cd.addHeader("REMOTE_PATH", disablePathCollapse); for (size_t i = 0; i < mounts->size() && i < rowCountLimit; ++i) { assert(mounts->get(i) != nullptr); const MegaMount& mount = *mounts->get(i); const MegaHandle handle = mount.getHandle(); std::unique_ptr remotePath(api.getNodePathByNodeHandle(handle)); const MegaMountFlags* flags = mount.getFlags(); assert(flags != nullptr); cd.addValue("NAME", flags->getName()); cd.addValue("LOCAL_PATH", mount.getPath()); cd.addValue("REMOTE_PATH", remotePath ? remotePath.get() : ""); cd.addValue("PERSISTENT", flags->getPersistent() ? "YES" : "NO"); cd.addValue("ENABLED", api.isMountEnabled(mount.getFlags()->getName()) ? "YES" : "NO"); } OUTSTREAM << cd.str(); OUTSTREAM << endl; if (rowCountLimit < mounts->size()) { OUTSTREAM << "Note: showing " << rowCountLimit << " out of " << mounts->size() << " mounts. " << "Use \"" << getCommandPrefixBasedOnMode() << "fuse-show --limit=0\" to see all of them." << endl; } OUTSTREAM << "Use \"" << getCommandPrefixBasedOnMode() << "fuse-show \" to get further details on a specific mount." << endl; } bool ConfigDelta::isAnyFlagSet() const { return mEnableAtStartup || mPersistent || mReadOnly || mName; } bool ConfigDelta::isPersistentStartupInvalid() const { bool enableAtStartup = mEnableAtStartup && *mEnableAtStartup; bool transient = mPersistent && !(*mPersistent); return enableAtStartup && transient; } void changeConfig(mega::MegaApi& api, const mega::MegaMount& mount, const ConfigDelta& delta) { assert(delta.isAnyFlagSet()); assert(!delta.isPersistentStartupInvalid()); MegaMountFlags* flags = mount.getFlags(); assert(flags != nullptr); if (delta.mEnableAtStartup) { flags->setEnableAtStartup(*delta.mEnableAtStartup); } if (delta.mPersistent) { flags->setPersistent(*delta.mPersistent); } if (delta.mReadOnly) { flags->setReadOnly(*delta.mReadOnly); } std::string currentName(mount.getFlags()->getName()); if (delta.mName) { flags->setName(delta.mName->c_str()); } auto listener = std::make_unique(nullptr); { DisableMountErrorsBroadcastingGuard disableErrorBroadcasting; api.setMountFlags(flags, currentName.c_str(), listener.get()); if (!checkNoErrors(listener.get(), getActionString("change the flags of", mount))) { return; } } OUTSTREAM << "Mount " << getMountId(mount) << " now has the following flags\n" << " Name: " << flags->getName() << "\n" << " Enable at startup: " << (flags->getEnableAtStartup() ? "YES" : "NO") << "\n" << " Persistent: " << (flags->getPersistent() ? "YES" : "NO") << "\n" << " Read-only: " << (flags->getReadOnly() ? "YES" : "NO") << "\n"; } } #endif // WITH_FUSE MEGAcmd-2.5.2_Linux/src/megacmd_fuse.h000066400000000000000000000034641516543156300174730ustar00rootroot00000000000000/** * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #pragma once #include namespace FuseCommand { std::string_view getDisclaimer(); std::string_view getBetaMsg(); std::string_view getIdentifierParameter(); } #ifdef WITH_FUSE #include "megaapi.h" #include "megacmdcommonutils.h" namespace FuseCommand { std::unique_ptr getMountByNameOrPath(mega::MegaApi& api, const std::string& identifier); void addMount(mega::MegaApi& api, const fs::path& localPath, mega::MegaNode& node, bool disabled, bool transient, bool readOnly, const std::string& name); void removeMount(mega::MegaApi& api, const mega::MegaMount& mount); void enableMount(mega::MegaApi& api, const mega::MegaMount& mount, bool temporarily); void disableMount(mega::MegaApi& api, const mega::MegaMount& mount, bool temporarily); void printMount(mega::MegaApi& api, const mega::MegaMount& mount); void printAllMounts(mega::MegaApi& api, megacmd::ColumnDisplayer& cd, bool onlyEnabled, bool disablePathCollapse, int rowCountLimit); struct ConfigDelta { std::optional mEnableAtStartup; std::optional mPersistent; std::optional mReadOnly; std::optional mName; bool isAnyFlagSet() const; bool isPersistentStartupInvalid() const; }; void changeConfig(mega::MegaApi& api, const mega::MegaMount& mount, const ConfigDelta& delta); } #endif // WITH_FUSE MEGAcmd-2.5.2_Linux/src/megacmd_rotating_logger.cpp000066400000000000000000001012661516543156300222510ustar00rootroot00000000000000/** * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include "megacmd_rotating_logger.h" #include "megacmdcommonutils.h" #include "configurationmanager.h" #include #include #include #include using megacmd::ConfigurationManager; namespace megacmd { namespace { constexpr uint64_t operator"" _KB(unsigned long long kilobytes) { return kilobytes * 1024ull; } constexpr uint64_t operator"" _MB(unsigned long long megabytes) { return megabytes * 1024ull * 1024ull; } constexpr uint64_t operator"" _GB(unsigned long long gigabytes) { return gigabytes * 1024ull * 1024ull * 1024ull; } constexpr uint64_t operator"" _TB(unsigned long long terabytes) { return terabytes * 1024ull * 1024ull * 1024ull * 1024ull; } class FileConfigCalculator { // The estimated max total space used by the logs is: // MaxTotalSizeUsed = MaxFilesToKeep * MaxFileSize * AvgCompressionRatio + MaxFileSize // // Then, we can estimate the max amount of files to keep with: // MaxFilesToKeep = (MaxTotalSizeUsed - MaxFileSize) / (MaxFileSize * AvgCompressionRatio) // where, giving MEGAcmd logs a max disk usage of 0.15%, // MaxTotalSizeUsed = TotalDiskSpace * 0.0015 // // If there's only enough space for less than 1 file, it means we have to reduce the max allowed // file size. So, we'll set MaxFilesToKeep=1, and then: // MaxFileSize = MaxTotalSizeUsed / (AvgCompressionRatio + 1) // // Using these formulas, and setting MaxFileSize=50MB and AvgCompressionRatio=0.15%, // we get the following possible defaults: // DISK SPACE MAX FILES MAX FILE SIZE // 1 TB 203 50 MB // 400 GB 75 50 MB // 100 GB 13 50 MB // 10 GB 1 13 MB // 1 GB 1 1.34 MB // // Note: We use the total disk space instead of the available disk space because it's not the // responsability of MEGACmd to ensure the disk doesn't run out of space. We just want to try // to choose more sensible defaults depending on the system specs. double mMaxAllowedMB; double mCompressionRatio; double getMaxFilesToKeepImpl(double maxFileMB) { return (mMaxAllowedMB - maxFileMB) / (maxFileMB * mCompressionRatio); } public: FileConfigCalculator(RotatingFileManager::CompressionType compressionType) : mMaxAllowedMB(300.0), mCompressionRatio(RotatingFileManager::getCompressionRatio(compressionType)) { constexpr double logsDiskUsagePercentage = 0.15 / 100.0; std::error_code ec; const auto spaceInfo = fs::space(ConfigurationManager::getConfigFolder(), ec); if (!ec) { const double totalAvailableMB = spaceInfo.capacity / (1024.0 * 1024.0); mMaxAllowedMB = totalAvailableMB * logsDiskUsagePercentage; } } double getMaxFileMB() { constexpr double defaultMaxFileMB = 50.0; double maxFileMB = defaultMaxFileMB; double maxFilesToKeep = getMaxFilesToKeepImpl(maxFileMB); if (maxFilesToKeep < 1.0) { maxFileMB = mMaxAllowedMB / (mCompressionRatio + 1); assert(maxFileMB < defaultMaxFileMB); } return maxFileMB; } int getMaxFilesToKeep(double maxFileMB) { constexpr int fileCountHardLimit = 50; double maxFilesToKeep = std::min((double) fileCountHardLimit, getMaxFilesToKeepImpl(maxFileMB)); return std::round(maxFilesToKeep); } }; class ReopenScope final { std::ofstream& mOutFile; const fs::path mOutFilePath; public: ReopenScope(std::ofstream& outFile, const fs::path& outFilePath) : mOutFile(outFile), mOutFilePath(outFilePath) { mOutFile.close(); } ~ReopenScope() { mOutFile.open(mOutFilePath, std::ofstream::out | std::ofstream::app); } }; } class BaseEngine { protected: std::stringstream mErrorStream; public: std::string popErrors(); }; class RotationEngine : public BaseEngine { protected: template void walkRotatedFiles(const fs::path& dir, const fs::path& baseFilename, F&& walker); public: virtual ~RotationEngine() = default; virtual void cleanupFiles(const fs::path& dir, const fs::path& baseFilename); virtual fs::path rotateFiles(const fs::path& dir, const fs::path& baseFilename) = 0; }; class NumberedRotationEngine final : public RotationEngine { const std::string mCompressionExt; const int mMaxFilesToKeep; fs::path getSrcFilePath(const fs::path& directory, const fs::path& baseFilename, int i) const; fs::path getDstFilePath(const fs::path& directory, const fs::path& baseFilename, int i) const; public: NumberedRotationEngine(const std::string &compressionExt, int maxFilesToKeep); fs::path rotateFiles(const fs::path& dir, const fs::path& baseFilePath) override; }; class TimestampRotationEngine final : public RotationEngine { const int mMaxFilesToKeep; const std::chrono::seconds mMaxFileAge; private: using Clock = std::chrono::system_clock; using Timestamp = std::chrono::time_point; struct TimestampFile { fs::path mPath; Timestamp mTimestamp; TimestampFile(const fs::path& path, const Timestamp ×tamp); bool operator>(const TimestampFile& other) const { return mTimestamp > other.mTimestamp; } }; using TimestampFileQueue = std::priority_queue, std::greater>; private: fs::path rotateBaseFile(const fs::path& directory, const fs::path& baseFilename); TimestampFileQueue getTimestampFileQueue(const fs::path& dir, const fs::path& baseFilename); void popAndRemoveFile(TimestampFileQueue& fileQueue); public: TimestampRotationEngine(int maxFilesToKeep, std::chrono::seconds maxFileAge); fs::path rotateFiles(const fs::path& dir, const fs::path& baseFilename) override; }; class CompressionEngine : public BaseEngine { public: virtual ~CompressionEngine() = default; virtual std::string getExtension() const { return ""; } virtual void cancelAll() {} virtual void compressFile(const fs::path&) {} }; class GzipCompressionEngine final : public CompressionEngine { struct GzipJobData { fs::path mSrcFilePath; fs::path mDstFilePath; GzipJobData(const fs::path& srcFilePath, const fs::path& dstFilePath); }; using GzipJobQueue = std::queue; GzipJobQueue mQueue; mutable std::mutex mQueueMtx; std::condition_variable mQueueCV; bool mCancelOngoingJob; bool mExit; std::thread mGzipThread; private: bool shouldCancelOngoingJob() const; void pushToQueue(const fs::path& srcFilePath, const fs::path& dstFilePath); void gzipFile(const fs::path& srcFilePath, const fs::path& dstFilePath); void mainLoop(); public: GzipCompressionEngine(); ~GzipCompressionEngine(); std::string getExtension() const override; void cancelAll() override; void compressFile(const fs::path& filePath) override; }; MessageBus::MessageBus(size_t reservedSize, size_t shouldSwapSize, size_t failSafeSize) : mShouldSwapSize(shouldSwapSize), mFailSafeSize(failSafeSize), mMemoryError(false) { assert(mFailSafeSize > 0); if (reservedSize > failSafeSize) { reservedSize = failSafeSize; } try { mFrontBuffer.reserve(reservedSize); mBackBuffer.reserve(reservedSize); } catch (const std::bad_alloc&) { // If this happens, we don't need to set the memory error flag, since this // is a memory error we don't want to log. It just means we might've overshot // our reserved size for a particular machine. // The vector will try to allocate again when inserting below; at that point // we will set the flag if the allocation fails as well. } } void MessageBus::append(const char* data, size_t size) { std::lock_guard lock(mListMtx); try { mBackBuffer.insert(mBackBuffer.end(), data, data + size); } catch (const std::bad_alloc&) { mMemoryError = true; } } std::pair MessageBus::swapBuffers() { std::lock_guard lock(mListMtx); bool memoryError = false; clearFrontBuffer(); std::swap(memoryError, mMemoryError); std::swap(mFrontBuffer, mBackBuffer); return {memoryError, mFrontBuffer}; } bool MessageBus::isEmpty() const { std::lock_guard lock(mListMtx); return mBackBuffer.empty(); } bool MessageBus::shouldSwapBuffers() const { std::lock_guard lock(mListMtx); return mBackBuffer.size() >= mShouldSwapSize; } bool MessageBus::reachedFailSafeSize() const { std::lock_guard lock(mListMtx); return mBackBuffer.size() >= mFailSafeSize; } void MessageBus::clearFrontBuffer() { if (mFrontBuffer.capacity() > mFailSafeSize) { mFrontBuffer.erase(mFrontBuffer.begin() + mFailSafeSize, mFrontBuffer.end()); mFrontBuffer.shrink_to_fit(); assert(mFrontBuffer.capacity() == mFailSafeSize); } mFrontBuffer.clear(); } float RotatingFileManager::getCompressionRatio(CompressionType compressionType) { switch (compressionType) { case CompressionType::None: return 1.f; case CompressionType::Gzip: return 0.15f; // this is conservative; it's generally ~10% for MEGAcmd logs default: assert(false); return 1.f; } } RotatingFileManager::RotationType RotatingFileManager::getRotationTypeFromStr(std::string_view str) { if (str == "Numbered") { return RotationType::Numbered; } return RotationType::Timestamp; } RotatingFileManager::CompressionType RotatingFileManager::getCompressionTypeFromStr(std::string_view str) { if (str == "None") { return CompressionType::None; } return CompressionType::Gzip; } RotatingFileManager::RotatingFileManager(const fs::path& filePath, const Config &config) : mConfig(config), mDirectory(filePath.parent_path()), mBaseFilename(filePath.filename()) { initializeCompressionEngine(); initializeRotationEngine(); } bool RotatingFileManager::shouldRotateFiles(size_t fileSize) const { return fileSize > mConfig.mMaxBaseFileSize; } void RotatingFileManager::cleanupFiles() { mCompressionEngine->cancelAll(); mRotationEngine->cleanupFiles(mDirectory, mBaseFilename); } void RotatingFileManager::rotateFiles() { auto newlyRotatedFilePath = mRotationEngine->rotateFiles(mDirectory, mBaseFilename); mCompressionEngine->compressFile(newlyRotatedFilePath); } std::string RotatingFileManager::popErrors() { return mRotationEngine->popErrors() + mCompressionEngine->popErrors(); } void RotatingFileManager::initializeCompressionEngine() { CompressionEngine* compressionEngine = nullptr; switch(mConfig.mCompressionType) { case CompressionType::None: { compressionEngine = new CompressionEngine(); break; } case CompressionType::Gzip: { compressionEngine = new GzipCompressionEngine(); break; } } assert(compressionEngine); mCompressionEngine = std::unique_ptr(compressionEngine); } void RotatingFileManager::initializeRotationEngine() { RotationEngine* rotationEngine = nullptr; switch (mConfig.mRotationType) { case RotationType::Numbered: { assert(mCompressionEngine); const std::string compressionExt = mCompressionEngine->getExtension(); rotationEngine = new NumberedRotationEngine(compressionExt, mConfig.mMaxFilesToKeep); break; } case RotationType::Timestamp: { rotationEngine = new TimestampRotationEngine(mConfig.mMaxFilesToKeep, mConfig.mMaxFileAge); break; } } assert(rotationEngine); mRotationEngine = std::unique_ptr(rotationEngine); } size_t FileRotatingLoggedStream::loadFailSafeSize() { // Since we have two buffers, and each can be up to FailSafeSize, the max size of the message bus is: // MessageBusMaxSize = FailSafeSize * 2 // // We could potentially get the total RAM to figure out a better default value; probably overkill // since there's not an easy, cross-platform way to do so. constexpr double defaultMessageBusMB = 512.0; double messageBusMB = ConfigurationManager::getConfigurationValue("RotatingLogger:MaxMessageBusMB", defaultMessageBusMB); if (messageBusMB < 0.0) { messageBusMB = defaultMessageBusMB; } return std::floor(messageBusMB/2.0 * 1024.0 * 1024.0); } RotatingFileManager::Config FileRotatingLoggedStream::loadFileConfig() { RotatingFileManager::Config config; std::string rotationTypeStr = ConfigurationManager::getConfigurationSValue("RotatingLogger:RotationType"); std::string compressionTypeStr = ConfigurationManager::getConfigurationSValue("RotatingLogger:CompressionType"); config.mRotationType = RotatingFileManager::getRotationTypeFromStr(rotationTypeStr); config.mCompressionType = RotatingFileManager::getCompressionTypeFromStr(compressionTypeStr); FileConfigCalculator calculator(config.mCompressionType); const double defaultMaxFileMB = calculator.getMaxFileMB(); double maxFileMB = ConfigurationManager::getConfigurationValue("RotatingLogger:MaxFileMB", defaultMaxFileMB); if (maxFileMB < 0.0) { maxFileMB = defaultMaxFileMB; } const int defaultMaxFilesToKeep = calculator.getMaxFilesToKeep(maxFileMB); int maxFilesToKeep = ConfigurationManager::getConfigurationValue("RotatingLogger:MaxFilesToKeep", defaultMaxFilesToKeep); if (maxFilesToKeep < 0) { maxFilesToKeep = defaultMaxFilesToKeep; } constexpr int defaultMaxFileAgeSeconds = 30 * 86400; // 1 month int maxFileAgeSeconds = ConfigurationManager::getConfigurationValue("RotatingLogger:MaxFileAgeSeconds", defaultMaxFileAgeSeconds); if (maxFileAgeSeconds < 0) { maxFileAgeSeconds = defaultMaxFileAgeSeconds; } config.mMaxBaseFileSize = std::floor(maxFileMB * 1024.0 * 1024.0); config.mMaxFileAge = std::chrono::seconds(maxFileAgeSeconds); config.mMaxFilesToKeep = maxFilesToKeep; return config; } bool FileRotatingLoggedStream::shouldRenew() const { std::lock_guard lock(mWriteMtx); return mForceRenew; } bool FileRotatingLoggedStream::shouldExit() const { std::lock_guard lock(mExitMtx); return mExit; } bool FileRotatingLoggedStream::shouldFlush() const { std::lock_guard lock(mWriteMtx); return mFlush || mNextFlushTime <= std::chrono::steady_clock::now(); } void FileRotatingLoggedStream::setForceRenew(bool forceRenew) { std::lock_guard lock(mWriteMtx); mForceRenew = forceRenew; } void FileRotatingLoggedStream::writeToBuffer(const char* msg, size_t size) const { std::lock_guard lock(mWriteMtx); if (mExit) { std::cerr.write(msg, size); return; } mMessageBus.append(msg, size); if (mMessageBus.shouldSwapBuffers()) { mWriteCV.notify_one(); } } void FileRotatingLoggedStream::writeMessagesToFile() { const auto& [memoryError, memoryBuffer] = mMessageBus.swapBuffers(); if (memoryError) { mOutputFile << "\n"; } if (!memoryBuffer.empty()) { // Write directly to the stream without relying on null termination mOutputFile.write(&memoryBuffer[0], memoryBuffer.size()); } if (memoryError) { mOutputFile << "<------------------------------>\n"; } } void FileRotatingLoggedStream::flushToFile() { mOutputFile.flush(); mNextFlushTime = std::chrono::steady_clock::now() + mFlushPeriod; { std::lock_guard lock(mWriteMtx); mFlush = false; } } void FileRotatingLoggedStream::markForExit() { { std::scoped_lock lock(mExitMtx, mWriteMtx); mExit = true; } mWriteCV.notify_one(); mExitCV.notify_one(); } bool FileRotatingLoggedStream::waitForOutputFile() { thread_local const int waitTimes[] = {0, 1, 5, 20, 60}; thread_local int i = 0; if (mOutputFile) { i = 0; return true; } { std::unique_lock lock(mExitMtx); mExitCV.wait_for(lock, std::chrono::seconds(waitTimes[i]), [this] { return mExit; }); } if (i < std::size(waitTimes) - 1) ++i; return false; } void FileRotatingLoggedStream::mainLoop() { while (!shouldExit() || !mMessageBus.isEmpty()) { std::ostringstream errorStream; bool reopenFile = false; if (!waitForOutputFile()) { errorStream << "Error writing to log file " << mOutputFilePath << '\n'; errorStream << "Re-opening...\n"; reopenFile = true; } const size_t outFileSize = mOutputFile ? static_cast(mOutputFile.tellp()) : 0; if (reopenFile) { ReopenScope s(mOutputFile, mOutputFilePath); } else if (shouldRenew()) { ReopenScope s(mOutputFile, mOutputFilePath); mFileManager.cleanupFiles(); setForceRenew(false); } else if (mFileManager.shouldRotateFiles(outFileSize)) { ReopenScope s(mOutputFile, mOutputFilePath); mFileManager.rotateFiles(); } errorStream << mFileManager.popErrors(); #ifdef WIN32 { WindowsUtf8StdoutGuard utf8Guard; std::wcerr << utf8StringToUtf16WString(errorStream.str().c_str()) << std::flush; } #else std::cerr << errorStream.str() << std::flush; #endif if (!mOutputFile) { // If we cannot write to file, do not keep trying if we should exit if (shouldExit()) { break; } // If we've reached the fail-safe size, try to write to file anyway // This clears the buffer, ensuring it doesn't grow beyond a certain threshold if (!mMessageBus.reachedFailSafeSize()) { continue; } } mOutputFile << errorStream.str(); bool writeMessages = false; { std::unique_lock lock(mWriteMtx); writeMessages = mWriteCV.wait_for(lock, std::chrono::milliseconds(500), [this] { return mForceRenew || mExit || mFlush || !mMessageBus.isEmpty(); }); } if (writeMessages) { writeMessagesToFile(); } if (shouldFlush()) { flushToFile(); } } } FileRotatingLoggedStream::FileRotatingLoggedStream(const OUTSTRING& outputFilePath) : mMessageBus(4_MB /* reservedSize */, 1_KB /* shouldSwapSize */, loadFailSafeSize()), mOutputFilePath(outputFilePath), mOutputFile(outputFilePath, std::ofstream::out | std::ofstream::app), mFileManager(mOutputFilePath, loadFileConfig()), mForceRenew(false), mExit(false), mFlush(false), mFlushPeriod(std::chrono::seconds(10)), mNextFlushTime(std::chrono::steady_clock::now() + mFlushPeriod), mWriteThread([this] () { mainLoop(); }) { } FileRotatingLoggedStream::~FileRotatingLoggedStream() { markForExit(); mWriteThread.join(); } const LoggedStream& FileRotatingLoggedStream::operator<<(const char& c) const { writeToBuffer(&c, sizeof(c)); return *this; } const LoggedStream& FileRotatingLoggedStream::operator<<(const char* str) const { writeToBuffer(str, strlen(str)); return *this; } const LoggedStream& FileRotatingLoggedStream::operator<<(std::string str) const { writeToBuffer(str.c_str(), str.size()); return *this; } const LoggedStream& FileRotatingLoggedStream::operator<<(BinaryStringView v) const { writeToBuffer(v.get().data(), v.get().size()); return *this; } const LoggedStream& FileRotatingLoggedStream::operator<<(std::string_view str) const { writeToBuffer(str.data(), str.size()); return *this; } const LoggedStream& FileRotatingLoggedStream::operator<<(int v) const { return operator<<(std::to_string(v)); } const LoggedStream& FileRotatingLoggedStream::operator<<(unsigned int v) const { return operator<<(std::to_string(v)); } const LoggedStream& FileRotatingLoggedStream::operator<<(long long v) const { return operator<<(std::to_string(v)); } const LoggedStream& FileRotatingLoggedStream::operator<<(long long unsigned int v) const { return operator<<(std::to_string(v)); } const LoggedStream& FileRotatingLoggedStream::operator<<(unsigned long v) const { return operator<<(std::to_string(v)); } #ifdef _WIN32 const LoggedStream& FileRotatingLoggedStream::operator<<(std::wstring wstr) const { std::string str; localwtostring(&wstr, &str); writeToBuffer(str.c_str(), str.size()); return *this; } #endif void FileRotatingLoggedStream::flush() { std::lock_guard lock(mWriteMtx); if (mExit) { return; } mFlush = true; mWriteCV.notify_one(); } std::string BaseEngine::popErrors() { std::string errorString = mErrorStream.str(); mErrorStream.str(""); return errorString; } template void RotationEngine::walkRotatedFiles(const fs::path& dir, const fs::path& baseFilename, F&& walker) { std::error_code ec; for (const auto& entry : fs::directory_iterator(dir, ec)) { if (ec) { mErrorStream << "Failed to walk directory " << dir << " (error: " << ec.message() << ")" << std::endl; return; } // Ignore non-files if (!entry.is_regular_file(ec) || ec) { continue; } const fs::path& filePath = entry.path(); std::string filename = filePath.filename().string(); // Ignore files that don't start with the base filename if (filename.rfind(baseFilename.string(), 0) != 0) { continue; } walker(filePath); } } void RotationEngine::cleanupFiles(const fs::path& dir, const fs::path& baseFilename) { walkRotatedFiles(dir, baseFilename, [this] (const fs::path& filePath) { std::error_code ec; fs::remove(filePath, ec); if (ec) { mErrorStream << "Error removing rotated file " << filePath << " (error: " << ec.message() << ")" << std::endl; } }); } fs::path NumberedRotationEngine::getSrcFilePath(const fs::path& directory, const fs::path& baseFilename, int i) const { fs::path filePath = directory / baseFilename; // The source path for the base file does not have an index or compression extension if (i != 0) { filePath += "." + std::to_string(i) + mCompressionExt; } return filePath; } fs::path NumberedRotationEngine::getDstFilePath(const fs::path& directory, const fs::path& baseFilename, int i) const { fs::path filePath = directory / baseFilename; // The destination path for the base file has an index, but doesn't have a compression extension // (since the file still needs to be sent to the compression engine) filePath += "." + std::to_string(i + 1); if (i != 0) { filePath += mCompressionExt; } return filePath; } NumberedRotationEngine::NumberedRotationEngine(const std::string& compressionExt, int maxFilesToKeep) : mCompressionExt(compressionExt), mMaxFilesToKeep(maxFilesToKeep) { } fs::path NumberedRotationEngine::rotateFiles(const fs::path& dir, const fs::path& baseFilename) { std::error_code ec; for (int i = mMaxFilesToKeep; i >= 0; --i) { fs::path srcFilePath = getSrcFilePath(dir, baseFilename, i); // Quietly ignore any numbered files that don't exist if (!fs::exists(srcFilePath) || ec) { continue; } // Delete the last file, which is the one with number == maxFilesToKeep if (i == mMaxFilesToKeep) { fs::remove(srcFilePath, ec); if (ec) { mErrorStream << "Error removing file " << srcFilePath << " (error: " << ec.message() << ")" << std::endl; } continue; } // For the rest, rename them to effectively do number++ (e.g., file.3.gz => file.4.gz) fs::path dstFilePath = getDstFilePath(dir, baseFilename, i); fs::rename(srcFilePath, dstFilePath, ec); if (ec) { mErrorStream << "Error renaming file " << srcFilePath << " to " << dstFilePath << " (error: " << ec.message() << ")" << std::endl; } } // The newly-rotated file is the destination path of the base file return getDstFilePath(dir, baseFilename, 0); } TimestampRotationEngine::TimestampFile::TimestampFile(const fs::path& path, const Timestamp ×tamp) : mPath(path), mTimestamp(timestamp) { } fs::path TimestampRotationEngine::rotateBaseFile(const fs::path& directory, const fs::path& baseFilename) { const std::string timestampStr = timestampToString(Clock::now()); fs::path srcFilePath = directory / baseFilename; fs::path dstFilePath = srcFilePath; dstFilePath += "." + timestampStr; std::error_code ec; fs::rename(srcFilePath, dstFilePath, ec); if (ec) { mErrorStream << "Error renaming file " << srcFilePath << " to " << dstFilePath << " (error: " << ec.message() << ")" << std::endl; } return dstFilePath; } TimestampRotationEngine::TimestampFileQueue TimestampRotationEngine::getTimestampFileQueue(const fs::path& dir, const fs::path& baseFilename) { TimestampFileQueue fileQueue; std::unordered_set addedFiles; const std::string baseFilenameStr = baseFilename.string(); walkRotatedFiles(dir, baseFilename, [this, &baseFilenameStr, &fileQueue, &addedFiles] (const fs::path& filePath) { const std::string filenameStr = filePath.filename().string(); const std::string timestampStr = filenameStr.substr(baseFilenameStr.size() + 1, LogTimestampSize); auto timestampOpt = stringToTimestamp(timestampStr); if (!timestampOpt) { // Ignore if there's not timestamp or it has a different format return; } if (addedFiles.find(timestampStr) != addedFiles.end()) { // Otherwise, in the rare case that a file finishes being zipped while this function is executing, // we might add it twice: one with "zipping" prefix and one without. This would lead to an incorrect number // of files, which would messs up the max file calculation. Regardless, the zipping file is the newest one so it won't be deleted. return; } addedFiles.emplace(timestampStr); fileQueue.emplace(filePath, *timestampOpt); }); return fileQueue; } void TimestampRotationEngine::popAndRemoveFile(TimestampFileQueue& fileQueue) { const TimestampFile file = fileQueue.top(); fileQueue.pop(); std::error_code ec; fs::remove(file.mPath, ec); if (ec) { mErrorStream << "Error removing rotated file " << file.mPath << " (error: " << ec.message() << ")" << std::endl; } } TimestampRotationEngine::TimestampRotationEngine(int maxFilesToKeep, std::chrono::seconds maxFileAge) : mMaxFilesToKeep(maxFilesToKeep), mMaxFileAge(maxFileAge) { } fs::path TimestampRotationEngine::rotateFiles(const fs::path& dir, const fs::path& baseFilename) { fs::path newlyRotatedFile = rotateBaseFile(dir, baseFilename); if (mMaxFileAge <= std::chrono::seconds(0) && mMaxFilesToKeep < 0) { return newlyRotatedFile; } auto fileQueue = getTimestampFileQueue(dir, baseFilename); // Rotate by timestamp if (mMaxFileAge > std::chrono::seconds(0)) { const Timestamp minFileTimestamp = Clock::now() - mMaxFileAge; while (!fileQueue.empty() && fileQueue.top().mTimestamp < minFileTimestamp) { popAndRemoveFile(fileQueue); } } // Rotate by file count if (mMaxFilesToKeep >= 0) { while (fileQueue.size() > static_cast(mMaxFilesToKeep)) { popAndRemoveFile(fileQueue); } } return newlyRotatedFile; } GzipCompressionEngine::GzipJobData::GzipJobData(const fs::path& srcFilePath, const fs::path& dstFilePath) : mSrcFilePath(srcFilePath), mDstFilePath(dstFilePath) { } bool GzipCompressionEngine::shouldCancelOngoingJob() const { std::lock_guard lock(mQueueMtx); return mCancelOngoingJob; } void GzipCompressionEngine::pushToQueue(const fs::path& srcFilePath, const fs::path& dstFilePath) { std::lock_guard lock(mQueueMtx); if (mExit) { return; } mQueue.emplace(GzipJobData(srcFilePath, dstFilePath)); mQueueCV.notify_one(); } void GzipCompressionEngine::gzipFile(const fs::path& srcFilePath, const fs::path& dstFilePath) { { std::ifstream srcFile(srcFilePath); if (!srcFile) { mErrorStream << "Failed to open " << srcFilePath << " for compression" << std::endl; return; } auto gzdeleter = [] (gzFile_s* f) { if (f) gzclose(f); }; auto gzOpenHelper = [](const fs::path& path) { #ifdef _WIN32 return gzopen_w(path.wstring().c_str(), "wb"); #else return gzopen(path.string().c_str(), "wb"); #endif }; std::unique_ptr gzFile(gzOpenHelper(dstFilePath), gzdeleter); if (!gzFile) { mErrorStream << "Failed to open gzfile " << dstFilePath << " for writing" << std::endl; return; } std::string line; while (std::getline(srcFile, line)) { if (shouldCancelOngoingJob()) { return; } line += '\n'; if (gzputs(gzFile.get(), line.c_str()) == -1) { mErrorStream << "Failed to gzip " << srcFilePath << std::endl; return; } } } std::error_code ec; fs::remove(srcFilePath, ec); if (ec) { mErrorStream << "Failed to remove temporary file " << srcFilePath << " after compression (error: " << ec.message() << ")" << std::endl; } } void GzipCompressionEngine::mainLoop() { while (true) { std::optional jobDataOpt; { std::unique_lock lock(mQueueMtx); mQueueCV.wait(lock, [this] () { return mExit || !mQueue.empty(); }); if (mExit && mQueue.empty()) { return; } assert(!mQueue.empty()); jobDataOpt = std::move(mQueue.front()); mQueue.pop(); mCancelOngoingJob = false; } assert(jobDataOpt); gzipFile(jobDataOpt->mSrcFilePath, jobDataOpt->mDstFilePath); } } GzipCompressionEngine::GzipCompressionEngine() : mCancelOngoingJob(false), mExit(false), mGzipThread([this] () { mainLoop(); }) { } GzipCompressionEngine::~GzipCompressionEngine() { { std::lock_guard lock(mQueueMtx); mExit = true; // We want to exit gracefull, so we don't call `cancelAll` } mQueueCV.notify_one(); mGzipThread.join(); } std::string GzipCompressionEngine::getExtension() const { return ".gz"; } void GzipCompressionEngine::cancelAll() { std::lock_guard lock(mQueueMtx); // Clear the queue mQueue = GzipJobQueue(); // This flag will ensure `gzipFile` returns as soon as possible (if it's running) // It'll be unset the next time we pop an item from the queue mCancelOngoingJob = true; } void GzipCompressionEngine::compressFile(const fs::path& filePath) { std::error_code ec; fs::path tmpFilePath = filePath; tmpFilePath += ".zipping"; // Ensure there is not a clashing .zipping file if (fs::exists(tmpFilePath, ec) && !ec) { fs::remove(tmpFilePath, ec); if (ec) { mErrorStream << "Failed to remove temporary compression file " << tmpFilePath << " (error: " << ec.message() << ")" << std::endl; return; } } fs::rename(filePath, tmpFilePath, ec); if (ec) { mErrorStream << "Failed to rename file " << filePath << " to " << tmpFilePath << " (error: " << ec.message() << ")" << std::endl; return; } fs::path targetFilePath = filePath; targetFilePath += getExtension(); pushToQueue(tmpFilePath, targetFilePath); } } MEGAcmd-2.5.2_Linux/src/megacmd_rotating_logger.h000066400000000000000000000115731516543156300217170ustar00rootroot00000000000000/** * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #pragma once #include #include #include #include #include #include #include #include "megacmdlogger.h" namespace megacmd { class MessageBus final { public: using MemoryBuffer = std::vector; public: // These are: // reservedSize: initial size of the front and back buffers // shouldSwapSize: suggested size at which we can attempt to swap the buffers // failSafeSize: fail safe size at which we should flush the message bus // after flushing, the underlying buffers are shrunk to fit the fail safe size MessageBus(size_t reservedSize, size_t shouldSwapSize, size_t failSafeSize); void append(const char* data, size_t size); std::pair swapBuffers(); bool isEmpty() const; bool shouldSwapBuffers() const; bool reachedFailSafeSize() const; private: void clearFrontBuffer(); const size_t mShouldSwapSize; const size_t mFailSafeSize; mutable std::mutex mListMtx; MemoryBuffer mFrontBuffer; MemoryBuffer mBackBuffer; bool mMemoryError; }; class RotationEngine; class CompressionEngine; class RotatingFileManager final { public: enum class RotationType { Numbered, Timestamp }; enum class CompressionType { None, Gzip }; static float getCompressionRatio(CompressionType compressionType); static RotationType getRotationTypeFromStr(std::string_view str); static CompressionType getCompressionTypeFromStr(std::string_view str); struct Config { size_t mMaxBaseFileSize; RotationType mRotationType; std::chrono::seconds mMaxFileAge; int mMaxFilesToKeep; CompressionType mCompressionType; }; public: RotatingFileManager(const fs::path& filePath, const Config& config); bool shouldRotateFiles(size_t fileSize) const; void cleanupFiles(); void rotateFiles(); std::string popErrors(); private: void initializeCompressionEngine(); void initializeRotationEngine(); private: const Config mConfig; const fs::path mDirectory; const fs::path mBaseFilename; std::unique_ptr mCompressionEngine; std::unique_ptr mRotationEngine; }; class FileRotatingLoggedStream final : public LoggedStream { mutable MessageBus mMessageBus; fs::path mOutputFilePath; std::ofstream mOutputFile; RotatingFileManager mFileManager; mutable std::mutex mWriteMtx; mutable std::condition_variable mWriteCV; mutable std::mutex mExitMtx; mutable std::condition_variable mExitCV; bool mForceRenew; bool mExit; bool mFlush; std::chrono::seconds mFlushPeriod; std::chrono::steady_clock::time_point mNextFlushTime; std::thread mWriteThread; private: static size_t loadFailSafeSize(); static RotatingFileManager::Config loadFileConfig(); bool shouldRenew() const; bool shouldExit() const; bool shouldFlush() const; void setForceRenew(bool forceRenew); void writeToBuffer(const char* msg, size_t size) const; void writeMessagesToFile(); void flushToFile(); void markForExit(); bool waitForOutputFile(); void mainLoop(); public: FileRotatingLoggedStream(const OUTSTRING& outputFilePath); ~FileRotatingLoggedStream(); const LoggedStream& operator<<(const char& c) const override; const LoggedStream& operator<<(const char* str) const override; const LoggedStream& operator<<(std::string str) const override; const LoggedStream& operator<<(BinaryStringView v) const override; const LoggedStream& operator<<(std::string_view str) const override; const LoggedStream& operator<<(int v) const override; const LoggedStream& operator<<(unsigned int v) const override; const LoggedStream& operator<<(long long v) const override; const LoggedStream& operator<<(long long unsigned int v) const override; const LoggedStream& operator<<(unsigned long v) const override; const LoggedStream& operator<<(std::ios_base) const override { return *this; } const LoggedStream& operator<<(std::ios_base*) const override { return *this; } const LoggedStream& operator<<(OUTSTREAMTYPE& (*)(OUTSTREAMTYPE&)) const override { return *this; } #ifdef _WIN32 const LoggedStream& operator<<(std::wstring v) const override; #endif virtual void flush() override; }; } MEGAcmd-2.5.2_Linux/src/megacmd_server_main.cpp000066400000000000000000000146051516543156300213750ustar00rootroot00000000000000/** * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include "megacmd.h" #include "megaapi.h" #include "megacmdlogger.h" #include "megacmd_rotating_logger.h" #include "configurationmanager.h" #include using std::vector; bool extractarg(vector& args, const char *what) { for (int i = int(args.size()); i--; ) { if (!strcmp(args[i], what)) { args.erase(args.begin() + i); return true; } } return false; } bool extractargparam(vector& args, const char *what, std::string& param) { for (int i = int(args.size()) - 1; --i >= 0; ) { if (!strcmp(args[i], what) && args.size() > i) { param = args[i + 1]; args.erase(args.begin() + i, args.begin() + i + 2); return true; } } return false; } megacmd::LogConfig parseLogConfig(std::vector& args) { using mega::MegaApi; using megacmd::ConfigurationManager; #ifndef DEBUG constexpr int defaultSdkLogLevel = (int) MegaApi::LOG_LEVEL_ERROR; #else constexpr int defaultSdkLogLevel = (int) MegaApi::LOG_LEVEL_DEBUG; #endif constexpr int defaultCmdLogLevel = (int) MegaApi::LOG_LEVEL_DEBUG; bool debug = extractarg(args, "--debug"); bool debugfull = extractarg(args, "--debug-full"); bool verbose = extractarg(args, "--verbose"); bool verbosefull = extractarg(args, "--verbose-full"); const char *envCmdLogLevel = getenv("MEGACMD_LOGLEVEL"); const char *envJsonLogs = getenv("MEGACMD_JSON_LOGS"); if (envCmdLogLevel) { std::string loglevelenv(envCmdLogLevel); debug |= !loglevelenv.compare("DEBUG"); debugfull |= !loglevelenv.compare("FULLDEBUG"); verbose |= !loglevelenv.compare("VERBOSE"); verbosefull |= !loglevelenv.compare("FULLVERBOSE"); } megacmd::LogConfig logConfig; logConfig.mSdkLogLevel = ConfigurationManager::getConfigurationValue("sdkLogLevel", defaultSdkLogLevel); logConfig.mCmdLogLevel = ConfigurationManager::getConfigurationValue("cmdLogLevel", defaultCmdLogLevel); logConfig.mLogToCout = !extractarg(args, "--do-not-log-to-stdout"); logConfig.mJsonLogs = ConfigurationManager::getConfigurationValue("jsonLogs", false); if (debug) { logConfig.mCmdLogLevel = MegaApi::LOG_LEVEL_DEBUG; } if (debugfull) { logConfig.mSdkLogLevel = MegaApi::LOG_LEVEL_DEBUG; logConfig.mCmdLogLevel = MegaApi::LOG_LEVEL_DEBUG; } if (verbose) { logConfig.mCmdLogLevel = MegaApi::LOG_LEVEL_MAX; } if (verbosefull) { logConfig.mSdkLogLevel = MegaApi::LOG_LEVEL_MAX; logConfig.mCmdLogLevel = MegaApi::LOG_LEVEL_MAX; logConfig.mJsonLogs = true; } if (envJsonLogs) { std::string envJsonLogsStr(envJsonLogs); logConfig.mJsonLogs = (envJsonLogsStr != "0"); } return logConfig; } #ifdef _WIN32 // returns true if attempted to uninstall bool mayUninstall(std::vector &args) { bool buninstall = extractarg(args, "--uninstall") || extractarg(args, "/uninstall"); if (buninstall) { megacmd::uninstall(); return true; } return false; } #endif #ifndef _WIN32 #include static bool is_pid_running(pid_t pid) { while(waitpid(-1, 0, WNOHANG) > 0) { // Wait for defunct.... } if (0 == kill(pid, 0)) { return true; // Process exists } return 0; } #endif // mechanism used for updates/restarts in order to wait for other process void waitIfRequired(std::vector &args) { std::string shandletowait; bool dowaitforhandle = extractargparam(args, "--wait-for", shandletowait); if (dowaitforhandle) { #ifdef _WIN32 DWORD processId = atoi(shandletowait.c_str()); HANDLE processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processId); cout << "Waiting for former server to end" << endl; WaitForSingleObject( processHandle, INFINITE ); CloseHandle(processHandle); #else pid_t processId = atoi(shandletowait.c_str()); if (processId <= 0) { cerr << "Invalid --wait-for pid. Waiting 3 seconds instead." << endl; sleep(3); } else { const int maxWaitSeconds = 5; int waited = 0; while (is_pid_running(processId) && waited < maxWaitSeconds) { cerr << "Waiting for former MEGAcmd server to end: " << processId << endl; sleep(1); ++waited; } if (waited >= maxWaitSeconds) { cerr << "Former server (pid " << processId << ") still alive after " << maxWaitSeconds << "s, giving up waiting." << endl; } } #endif } } //TODO: things that will require testing (make them reach Jira release somehow) // - ALL PARAMETERS (including api url) // - windows uninstall and updating // - linux updates (signals and waiting) // - windows execution with non utf-8 paths // - linux forking (clients into server) int main(int argc, char* argv[]) { #ifdef WIN32 megacmd::Instance windowsConsoleController; #endif std::vector args; if (argc > 1) { args = std::vector(argv + 1, argv + argc); } #ifdef _WIN32 if (mayUninstall(args)) { return 0; } #endif waitIfRequired(args); const auto logConfig = parseLogConfig(args); bool skiplockcheck = extractarg(args, "--skip-lock-check"); std::string debug_api_url; extractargparam(args, "--apiurl", debug_api_url); // only for debugging bool disablepkp = extractarg(args, "--disablepkp"); // only for debugging auto createLoggedStream = [] { return new megacmd::FileRotatingLoggedStream(megacmd::MegaCmdLogger::getDefaultFilePath()); }; return megacmd::executeServer(argc, argv, createLoggedStream, logConfig, skiplockcheck, debug_api_url, disablepkp); } MEGAcmd-2.5.2_Linux/src/megacmd_src_file_list.h.in000066400000000000000000000001101516543156300217400ustar00rootroot00000000000000#pragma once #define MEGACMD_SRC_FILE_LIST { @MEGACMD_SRC_FILE_LIST@ } MEGAcmd-2.5.2_Linux/src/megacmd_utf8.cpp000066400000000000000000000170371516543156300177530ustar00rootroot00000000000000/** * @file src/megacmd_utf8.cpp * @brief MEGAcmd: utf8 and console resources * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of the MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include "megacmd_utf8.h" #ifdef WIN32 #include #include #include #endif namespace megacmd { #ifdef _WIN32 std::wostream & operator<< ( std::wostream & ostr, std::string const & str ) { std::wstring toout; stringtolocalw(str.c_str(),&toout); ostr << toout; return ( ostr ); } std::wostream & operator<< ( std::wostream & ostr, const char * str ) { std::wstring toout; stringtolocalw(str,&toout); ostr << toout; return ( ostr ); } // convert UTF-8 to Windows Unicode wstring (UTF-16) void stringtolocalw(const char* path, size_t lenutf8, std::wstring* local) { ASSERT_UTF8_VALID(std::string(path, lenutf8)); // make space for the worst case local->resize((strlen(path) + 1) * sizeof(wchar_t)); int wchars_num = MultiByteToWideChar(CP_UTF8, 0, path, lenutf8 + 1, NULL,0); local->resize(wchars_num); int len = MultiByteToWideChar(CP_UTF8, 0, path, lenutf8 + 1, (wchar_t*)local->data(), wchars_num); if (len) { local->resize(len-1); } else { local->clear(); } } // convert UTF-8 to Windows Unicode wstring (UTF-16) void stringtolocalw(const char* path, std::wstring* local) { stringtolocalw(path, strlen(path), local); } std::wstring utf8StringToUtf16WString(const char* str) { std::wstring toret; stringtolocalw(str, &toret); return toret; } std::wstring utf8StringToUtf16WString(const char* str, size_t lenutf8) { std::wstring toret; stringtolocalw(str, lenutf8, &toret); return toret; } void localwtostring(const std::wstring* wide, std::string *multibyte) { utf16ToUtf8(wide->data(), (int)wide->size(), multibyte); } void utf16ToUtf8(const wchar_t* utf16data, int utf16size, std::string* utf8string) { if(!utf16size) { utf8string->clear(); return; } int size_needed = WideCharToMultiByte(CP_UTF8, 0, utf16data, utf16size, NULL, 0, NULL, NULL); utf8string->resize(size_needed); WideCharToMultiByte(CP_UTF8, 0, utf16data, utf16size, (char*)utf8string->data(), size_needed, NULL, NULL); ASSERT_UTF8_VALID(*utf8string); } std::string utf16ToUtf8(const std::wstring &ws) { std::string utf8s; utf16ToUtf8(ws.c_str(), ws.size(), &utf8s); return utf8s; } std::string utf16ToUtf8(const wchar_t *ws) { std::string utf8s; utf16ToUtf8(ws, int(wcslen(ws)), &utf8s); return utf8s; } std::wstring nonMaxPathLimitedWstring(const fs::path &localpath) { static const std::wstring wprefix(LR"(\\?\)"); if (localpath.wstring().rfind(wprefix, 0) == 0) { return localpath.wstring(); } auto prefixedPathWstring = std::wstring(wprefix).append(localpath.wstring()); assert(prefixedPathWstring.rfind(wprefix, 0) == 0); return prefixedPathWstring; } fs::path nonMaxPathLimitedPath(const fs::path &localpath) { auto prefixedPath = fs::path(nonMaxPathLimitedWstring(localpath)); assert(prefixedPath.wstring().rfind(LR"(\\?\)", 0) == 0); return prefixedPath; } WindowsConsoleController::WindowsConsoleController() { if (!getenv("MEGACMD_DISABLE_UTF8_OUTPUT_MODE_BY_DEFAULT")) { // set default mode to U8TEXT. // PROs: we could actually get rid of _setmode to U8TEXT in WindowsUtf8StdoutGuard // CONS: any low level printing to stdout may crash if the mode is not _O_TEXT. // Even if we do not do that, for instance, gtests would do. auto OUTPUT_MODE = _O_U8TEXT; auto oldModeStdout = _setmode(_fileno(stdout), OUTPUT_MODE); auto oldModeStderr = _setmode(_fileno(stderr), OUTPUT_MODE); } if (!getenv("MEGACMD_DISABLE_NARROW_OUTSTREAMS_INTERCEPTING")) { enableInterceptors(true); } } void WindowsConsoleController::enableInterceptors(bool enable) { std::cout.flush(); std::cerr.flush(); if (enable) { mInterceptCout.reset(new InterceptStreamBuffer(std::cout, std::wcout)); mInterceptCerr.reset(new InterceptStreamBuffer(std::cerr, std::wcerr)); } else { mInterceptCout.reset(); mInterceptCerr.reset(); } } #endif std::atomic sInvalidUtf8Incidences = 0; std::string pathAsUtf8(const fs::path& path) { #ifdef _WIN32 return utf16ToUtf8(path.wstring().c_str()); #else return path.string(); #endif } bool isValidUtf8(const char* data, size_t size) { #ifdef MEGACMD_TESTING_CODE bool disableUTF8Valiations = getenv("MEGACMD_DISABLE_UTF8_VALIDATIONS"); #else static bool disableUTF8Valiations = getenv("MEGACMD_DISABLE_UTF8_VALIDATIONS"); #endif if (disableUTF8Valiations) { return true; } // checks that the byte starts with bits 10 (i.e. continuation bytes) auto check10 = [&data](size_t n) -> bool { return (data[n] & 0xc0) == 0x80; }; while (size) { const uint8_t lead = static_cast(*data); // 0xxxxxxx -> U+0000..U+007F (1-byte character) if (lead < 0x80) { ++data; --size; continue; } // 110xxxxx -> U+0080..U+07FF (2-byte character) else if ((lead & 0xe0) == 0xc0) { // check codepoint is at least 0x80 and check continuation byte if (lead > 0xc1 && size >= 2 && check10(1)) { data += 2; size -= 2; continue; } } // 1110xxxx -> U+0800..U+FFFF (3-byte character) else if ((lead & 0xf0) == 0xe0) { // check continuation bytes if (size >= 3 && check10(1) && check10(2)) { const auto secondByte = static_cast(data[1]); // check codepoint is at least 0x800 and not a surrogate codepoint in the range 0xd800-0xdfff if (((lead << 8) | secondByte) > 0xe09f && (lead != 0xed || secondByte < 0xa0)) { data += 3; size -= 3; continue; } } } // 11110xxx -> U+10000..U+10FFFF (4-byte character) else if ((lead & 0xf8) == 0xf0) { // check continuation bytes5 if (size >= 4 && check10(1) && check10(2) && check10(3)) { const auto firstHalf = (lead << 8) | static_cast(data[1]); // check codepoint is at least 0x10000 and not greater than 0x10FFFF (not encodable by UTF-16) if (firstHalf > 0xf08f && firstHalf < 0xf490) { data += 4; size -= 4; continue; } } } sInvalidUtf8Incidences++; return false; } return true; } bool isValidUtf8(const std::string &str) { return isValidUtf8(str.data(), str.size()); } } //end namespace MEGAcmd-2.5.2_Linux/src/megacmd_utf8.h000066400000000000000000000205231516543156300174120ustar00rootroot00000000000000/** * @file src/megacmd_utf8.h * @brief MEGAcmd: utf8 and console resources * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of the MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #pragma once #include #include #include #include #ifdef _WIN32 #include #include #include #include #include #include #include #include #include #include #include #include #ifndef _O_U16TEXT #define _O_U16TEXT 0x00020000 #endif #ifndef _O_U8TEXT #define _O_U8TEXT 0x00040000 #endif #endif #include namespace fs = std::filesystem; #ifdef MEGACMD_TESTING_CODE #define ASSERT_UTF8_VALID(str) #define ASSERT_UTF8_BREAK(msg) #else #define ASSERT_UTF8_VALID(str) assert(isValidUtf8(str)) #define ASSERT_UTF8_BREAK(msg) assert(false && msg) #endif namespace megacmd { std::string pathAsUtf8(const fs::path& path); extern std::atomic sInvalidUtf8Incidences; bool isValidUtf8(const char* data, size_t size); bool isValidUtf8(const std::string &str); struct StdoutMutexGuard { inline static std::recursive_mutex sSetmodeMtx; std::lock_guard mGuard; StdoutMutexGuard() : mGuard(sSetmodeMtx) {} }; /* platform dependent */ #ifdef _WIN32 #define OUTSTREAMTYPE std::wostream #define OUTFSTREAMTYPE std::wofstream #define OUTSTRINGSTREAM std::wostringstream #define OUTSTRING std::wstring #define COUT std::wcout #define CERR std::wcerr //override << operators for wostream for string and const char * std::wostream & operator<< ( std::wostream & ostr, std::string const & str ); std::wostream & operator<< ( std::wostream & ostr, const char * str ); // UTF-8 to wstrings (UTF-16) conversions: void stringtolocalw(const char* path, std::wstring* local); void stringtolocalw(const char* path, size_t lenutf8, std::wstring* local); std::wstring utf8StringToUtf16WString(const char* str, size_t lenutf8); std::wstring utf8StringToUtf16WString(const char* path); // convert Utf-16 wide chars to UTF-8 std::strings void localwtostring(const std::wstring* wide, std::string *multibyte); void utf16ToUtf8(const wchar_t* utf16data, int utf16size, std::string* utf8string); std::string utf16ToUtf8(const wchar_t *ws); std::string utf16ToUtf8(const std::wstring &ws); std::wstring nonMaxPathLimitedWstring(const fs::path &localpath); fs::path nonMaxPathLimitedPath(const fs::path &localpath); /*** * operator<< overloads to ensure proper handling of paths and widestrings * This header would need to be included first in all project as a general rule, so that these are used * As a way to ensure this, we could only define fs namespace here. * * Note: beyond these, operator<< overloads for logging can be found at megacmdlogger.h **/ //// This is expected to be used when trying to << a wstring (supposedly in UTF-16, into a ostream (string/file/cout/...) template inline std::enable_if_t, std::wstring>, std::ostream&> operator<<(std::ostream& oss, const T& wstr) { static_assert(false); // Let's forbid this to better control that we just write utf8 std::strings in non wide streams (e.g cout) // Notice that in the end, in stdout we want to write widestrings (utf16) to wcout instead, to have console rendering properly. // If we were to automatically support this we should convert them to utf8 string as follows: oss << megacmd::utf16ToUtf8(wstr); return oss; } } // end of namespace megacmd namespace std::filesystem { // overload that may be used when building some stringstream. // Note: LOG_xxx << path should are handled by SimpleLogger overloads, not this one inline std::ostream &operator<<(std::ostream& oss, const fs::path& path) { // caveat: outputting its contents (utf-8) to stdout would need to be done converting to utf-16 and using wcout // and a valid mode to stdout (See WindowsUtf8StdoutGuard) assert(&oss != &std::cout); assert(&oss != &std::cerr); oss << megacmd::pathAsUtf8(path); return oss; } } // end of namespace std::filesystem namespace megacmd { /** * @brief This class is used to: * - guard no meddling while writting/setting output mode on stdout/stderr * - ensure setting the output modes to mOutputMode */ class OutputsModeGuard : public StdoutMutexGuard { unsigned int mOutputMode; int mOldModeStdout; int mOldModeStderr; public: OutputsModeGuard(unsigned int outputMode) : mOutputMode(outputMode) { fflush(stdout); fflush(stderr); mOldModeStdout = _setmode(_fileno(stdout), mOutputMode); mOldModeStderr = _setmode(_fileno(stderr), mOutputMode); assert(mOldModeStdout != -1); assert(mOldModeStderr != -1); } virtual ~OutputsModeGuard() { fflush(stdout); fflush(stderr); _setmode(_fileno(stdout), mOldModeStdout); _setmode(_fileno(stderr), mOldModeStderr); } }; template class WindowsOutputsModeGuardGeneric : public OutputsModeGuard { public: WindowsOutputsModeGuardGeneric() : OutputsModeGuard(OUTPUT_MODE) {} }; using WindowsUtf8StdoutGuard = WindowsOutputsModeGuardGeneric<_O_U8TEXT>; using WindowsNarrowStdoutGuard = WindowsOutputsModeGuardGeneric<_O_TEXT>; using WindowsBinaryStdoutGuard = WindowsOutputsModeGuardGeneric; class InterceptStreamBuffer : public std::streambuf { private: std::streambuf* mOriginalStreamBuffer; // Store the original buffer std::ostream& mNarrowStream; // Reference to the original stream (e.g., std::cout) std::wostream& mWideOstream; // Reference to the original stream (e.g., std::cout) bool hasNonAscii(const char* str, size_t length) { for (size_t i = 0; i < length; ++i) { if (static_cast(str[i]) > 127) { return true; } } return false; } public: InterceptStreamBuffer(std::ostream& outStream, std::wostream &wideStream) : mNarrowStream(outStream), mWideOstream(wideStream) { mOriginalStreamBuffer = mNarrowStream.rdbuf(); // Save the original buffer mNarrowStream.rdbuf(this); // Replace with this buffer } ~InterceptStreamBuffer() { mNarrowStream.rdbuf(mOriginalStreamBuffer); // Restore the original buffer on destruction } protected: virtual int overflow(int c) override { char cc = char(c); xsputn(&cc, 1); return c; } virtual std::streamsize xsputn(const char* s, std::streamsize count) override { if (hasNonAscii(s, count)) // why cannot: { WindowsUtf8StdoutGuard utf8Guard; // assert(false && "This should ideally be controlled in calling code"); //TODO: enable this assert to fix cases in origin (assumed better performance) mWideOstream << utf8StringToUtf16WString(s, count); return count; } else { WindowsNarrowStdoutGuard narrowGuard; return mOriginalStreamBuffer->sputn(s, count); // This is protesting when having _setmode and doing cout << "odd" (odd number of chars) } } }; class WindowsConsoleController { std::unique_ptr mInterceptCout; std::unique_ptr mInterceptCerr; public: WindowsConsoleController(); void enableInterceptors(bool enable); }; #else #define OUTSTREAMTYPE std::ostream #define OUTFSTREAMTYPE std::ofstream #define OUTSTRINGSTREAM std::ostringstream #define OUTSTRING std::string #define COUT std::cout #define CERR std::cerr #endif }//end namespace MEGAcmd-2.5.2_Linux/src/megacmdcommonutils.cpp000066400000000000000000001164011516543156300212720ustar00rootroot00000000000000/** * @file src/megacmdcommonutils.cpp * @brief MEGAcmd: Auxiliary methods * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of the MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include "megacmdcommonutils.h" #ifdef _WIN32 #include //PathAppend #include //CommandLineToArgvW #include //GetUserName #include //UNLEN #include //_waccess #else #include // console size #include #include #include #include #include #include #endif #include #include #include #include #include #include #include #include #include #include //split #include namespace megacmd { using namespace std; std::string errorCodeStr(const std::error_code& ec) { return ec ? "(error: " + ec.message() + ")" : ""; } bool canWrite(const string &path) { #ifdef _WIN32 auto bypassCanWrite = []() { const char *bypassCanWriteEnv = getenv("MEGACMD_BYPASS_CAN_WRITE"); const std::string bypassCanWriteString = bypassCanWriteEnv ? bypassCanWriteEnv : std::string{}; return bypassCanWriteString == "1"; }; if (bypassCanWrite()) { return true; } std::wstring wpath = utf8StringToUtf16WString(path.c_str()); if (_waccess(wpath.c_str(), 02) == 0) // 02 = write permission { return true; } else if (errno == EACCES) { return false; // explicitly not writable } else if (errno == ENOENT && wpath.size() > 0 && (wpath.back() == L'.' || wpath.back() == L' ')) { // Ignore ENOENT errors for files ending with a period or a space. CMD/SDK is able to sync these files. // // See: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions // Quote: // > The following fundamental rules enable applications to create and process valid names // > for files and directories, regardless of the file system: // > ... // > Do not end a file or directory name with a space or a period. return true; } else { return false; } #else return access(path.c_str(), W_OK) == 0; #endif } bool isPublicLink(const string &link) { //Old format: //https://mega.nz/#!ph!key //https://mega.nz/#F!ph!key //new format: //https://mega.nz/file/ph#key //https://mega.nz/folder/ph#key if (( link.find("http") == 0 ) && ( link.find("#") != string::npos || link.find("/file/") != string::npos || link.find("/folder/") != string::npos)) { return true; } return false; } bool isEncryptedLink(string link) { if (( link.find("http") == 0 ) && ( link.find("#") != string::npos ) && (link.substr(link.find("#"),3) == "#P!") ) { return true; } return false; } string getPublicLinkHandle(const string &link) { size_t posFolder = string::npos; size_t posLastSep = link.rfind("?"); if (posLastSep == string::npos ) { string rest = link; int count = 0; size_t posExc = rest.find_first_of("!"); while ( posExc != string::npos && (posExc +1) < rest.size()) { count++; if (count <= 3 ) { posLastSep += posExc + 1; } rest = rest.substr(posExc + 1); posExc = rest.find("!"); } if (count != 3) { posLastSep = string::npos; } } if (posLastSep == string::npos ) { posFolder = link.find("/folder/"); } if (posFolder != string::npos) { posLastSep = link.rfind("/file/"); if (posLastSep != string::npos) { posLastSep += strlen("/file/")-1; } else { posLastSep = link.rfind("/folder/"); if (posLastSep != string::npos && posFolder != posLastSep) { posLastSep += strlen("/folder/")-1; } else { return string(); } } } if (( posLastSep == string::npos ) || !( posLastSep + 1 < link.length())) { return string(); } else { return link.substr(posLastSep+1); } } bool hasWildCards(string &what) { return what.find('*') != string::npos || what.find('?') != string::npos; } string generateRandomAlphaNumericString(size_t len) { static const std::string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz" "0123456789"; thread_local static std::mt19937 generator(std::random_device{}()); thread_local static std::uniform_int_distribution<> distribution(0, alphabet.size() - 1); std::string randomString; randomString.reserve(len); for (size_t i = 0; i < len; ++i) { randomString += alphabet[distribution(generator)]; } return randomString; } std::vector split(const std::string& input, const std::string& pattern) { size_t start = 0, end; std::vector tokens; if (input.size()) { if (pattern.size()) { do { end = input.find(pattern, start); std::string token = input.substr(start, end - start); if (token.size()) { tokens.push_back(token); } start = end + pattern.size(); } while (end != std::string::npos); } else { tokens.push_back(input); } } return tokens; } long long charstoll(const char *instr) { long long retval; retval = 0; for (; *instr; instr++) { retval = 10*retval + (*instr - '0'); } return retval; } std::string <rim(std::string &s, const char &c) { size_t pos = s.find_first_not_of(c); s = s.substr(pos == string::npos ? s.length() : pos, s.length()); return s; } std::string_view ltrim(const std::string_view s, const char &c) { size_t pos = s.find_first_not_of(c); return s.substr(pos == std::string::npos ? s.size() : pos, s.length()); } std::string &rtrim(std::string &s, const char &c) { size_t pos = s.find_last_not_of(c); s.resize(pos == std::string::npos ? 0 : pos + 1); return s; } string removeTrailingSeparators(string &path) { if (path.empty()) { return path; } if (path.find_first_not_of('/') == string::npos) { return "/"; } return rtrim(rtrim(path,'/'),'\\'); } vector getlistOfWords(const char *ptr, bool escapeBackSlashInCompletion, bool ignoreTrailingSpaces) { vector words; const char* wptr = ""; // split line into words with quoting and escaping for (;; ) { // skip leading blank space while (*(const signed char*)ptr > 0 && *ptr <= ' ' && (ignoreTrailingSpaces || *(ptr+1))) { ptr++; } if (!*ptr) { break; } // quoted arg / regular arg if (*ptr == '"') { ptr++; wptr = ptr; words.push_back(string()); for (;; ) { if (( *ptr == '"' ) || ( *ptr == '\\' ) || !*ptr) { words[words.size() - 1].append(wptr, ptr - wptr); if (!*ptr || ( *ptr++ == '"' )) { break; } wptr = ptr - 1; } else { ptr++; } } } else if (*ptr == '\'') // quoted arg / regular arg { ptr++; wptr = ptr; words.push_back(string()); for (;; ) { if (( *ptr == '\'' ) || ( *ptr == '\\' ) || !*ptr) { words[words.size() - 1].append(wptr, ptr - wptr); if (!*ptr || ( *ptr++ == '\'' )) { break; } wptr = ptr - 1; } else { ptr++; } } } else { while (*ptr == ' ') ptr++;// only possible if ptr+1 is the end wptr = ptr; const char *prev = ptr; //while ((unsigned char)*ptr > ' ') while ((*ptr != '\0') && !(*ptr ==' ' && *prev !='\\')) { if (*ptr == '"' && *(ptr+1) != '\0') // if quote is found, look for the ending quote { do { ++ptr; } while (*ptr != '"' && *(ptr+1) != '\0'); } prev = ptr; ptr++; } words.emplace_back(wptr, ptr - wptr); } } if (escapeBackSlashInCompletion && words.size()> 1 && words[0] == "completion") { for (int i = 1; i < (int)words.size(); i++) { replaceAll(words[i],"\\","\\\\"); } } return words; } bool stringcontained(const char * s, vector list) { for (int i = 0; i < (int)list.size(); i++) { if (list[i] == s) { return true; } } return false; } char * dupstr(char* s) { char *r; r = (char*)malloc(sizeof( char ) * ( strlen(s) + 1 )); strcpy(r, s); return( r ); } bool replace(std::string& str, const std::string& from, const std::string& to) { size_t start_pos = str.find(from); if (start_pos == std::string::npos) { return false; } str.replace(start_pos, from.length(), to); return true; } void replaceAll(std::string& str, const std::string& from, const std::string& to) { if (from.empty()) { return; } size_t start_pos = 0; while (( start_pos = str.find(from, start_pos)) != std::string::npos) { str.replace(start_pos, from.length(), to); start_pos += to.length(); } } int toInteger(const string &str, int failValue) { if (str.empty()) { return failValue; } errno = 0; char *p; long long l = strtoll(str.c_str(), &p, 10); if (p == str.c_str() || *p != '\0' || errno != 0) { return failValue; } if (( l < INT_MIN ) || ( l > INT_MAX )) { return failValue; } return (int)l; } string joinStrings(const vector& vec, const char* delim, bool quoted) { stringstream res; if (!quoted) { std::copy(vec.begin(), vec.end(), ostream_iterator(res, delim)); } else { for(vector::const_iterator i = vec.begin(); i != vec.end(); ++i) { res << "\"" << *i << "\"" << delim; } } if (vec.size() > 0) { string toret = res.str(); return toret.substr(0,toret.size()-strlen(delim)); } return res.str(); } unsigned int getstringutf8size(const string &str) { int c,i,ix,q; for (q=0, i=0, ix=int(str.length()); i < ix; i++, q++) { c = (unsigned char) str[i]; if (c>=0 && c<=127) i+=0; else if ((c & 0xE0) == 0xC0) i+=1; #ifdef _WIN32 else if ((c & 0xF0) == 0xE0) i+=2; #else else if ((c & 0xF0) == 0xE0) { if ((i+2)>ix || c != 0xE2 || (strncmp(&str.c_str()[i],"\u21f5",3) && strncmp(&str.c_str()[i],"\u21d3",3) && strncmp(&str.c_str()[i],"\u21d1",3) ) ) { //known 1 character gliphs q++; } i+=2; } //these gliphs may occupy 2 characters! Problem: not always. Let's assume the worse #endif else if ((c & 0xF8) == 0xF0) i+=3; else return 0;//invalid utf8 } return q; } string getFixLengthString(const string &origin, unsigned int size, const char delim, bool alignedright) { string toret; size_t printableSize = getstringutf8size(origin); size_t bytesSize = origin.size(); if (printableSize <= size){ if (alignedright) { toret.insert(0,size-printableSize,delim); toret.insert(size-bytesSize,origin,0,bytesSize); } else { toret.insert(0,origin,0,bytesSize); toret.insert(bytesSize,size-printableSize,delim); } } else { toret.insert(0,origin,0,(size+1)/2-2); if (size > 3) toret.insert((size+1)/2-2,3,'.'); if (size > 1) toret.insert((size+1)/2+1,origin,bytesSize-(size)/2+1,(size)/2-1); //TODO: This could break characters if multibyte! //alternative: separate in multibyte strings and print one by one? } return toret; } string getRightAlignedString(const string origin, unsigned int minsize) { ostringstream os; os << std::setw(minsize) << origin; return os.str(); } bool startsWith(const std::string_view str, const std::string_view prefix) { return str.rfind(prefix, 0) == 0; } string toLower(const std::string& str) { std::string lower = str; for (char& c : lower) { c = std::tolower(c); } return lower; } template void printCenteredLine(Stream_T &os, string msj, unsigned int width, bool encapsulated) { unsigned int msjsize = getstringutf8size(msj); bool overflowed = false; if (msjsize>width) { overflowed = true; width = unsigned(msjsize); } if (encapsulated && !overflowed) os << "|"; for (unsigned int i = 0; i < (width-msjsize)/2; i++) os << " "; os << msj; for (unsigned int i = 0; i < (width-msjsize)/2 + (width-msjsize)%2 ; i++) os << " "; if (encapsulated && !overflowed) os << "|"; os << endl; } // Instantiate those that can be used: template void printCenteredLine(OUTSTREAMTYPE &os, string msj, unsigned int width, bool encapsulated); template void printCenteredLine(std::ostringstream &os, string msj, unsigned int width, bool encapsulated); void printCenteredContents(OUTSTREAMTYPE &os, string msj, unsigned int width, bool encapsulated) { string headfoot = " "; headfoot.append(width, '-'); //unsigned int msjsize = getstringutf8size(msj); bool printfooter = false; if (msj.size()) { string header; if (msj.at(0) == '<') { size_t possenditle = msj.find(">"); if (width >= 2 && possenditle < (width -2)) { header.append(" "); header.append((width - possenditle ) / 2, '-'); header.append(msj.substr(0,possenditle+1)); header.append(width - getstringutf8size(header) + 1, '-'); msj = msj.substr(possenditle + 1); } } if (header.size() || encapsulated) { os << (header.size()?header:headfoot) << endl; printfooter = true; } } size_t possepnewline = msj.find("\n"); size_t possep = msj.find(" "); if (possepnewline != string::npos && possepnewline < width) { possep = possepnewline; } size_t possepprev = possep; while (msj.size()) { if (possepnewline != string::npos && possepnewline <= width) { possep = possepnewline; possepprev = possep; } else { while (possep < width && possep != string::npos) { possepprev = possep; possep = msj.find_first_of(" ", possep+1); } } if (possepprev == string::npos || (possep == string::npos && msj.size() <= width)) { printCenteredLine(os, msj, width, encapsulated); break; } else { printCenteredLine(os, msj.substr(0,possepprev), width, encapsulated); if (possepprev < (msj.size() - 1)) { msj = msj.substr(possepprev + 1); possepnewline = msj.find("\n"); possep = msj.find(" "); possepprev = possep; } else { break; } } } if (printfooter) { os << headfoot << endl; } } void printCenteredLine(string msj, unsigned int width, bool encapsulated) { OUTSTRINGSTREAM os; printCenteredLine(os, msj, width, encapsulated); #ifdef _WIN32 WindowsUtf8StdoutGuard utf8Guard; #endif COUT << os.str(); } void printCenteredContents(string msj, unsigned int width, bool encapsulated) { OUTSTRINGSTREAM os; printCenteredContents(os, msj, width, encapsulated); #ifdef _WIN32 WindowsUtf8StdoutGuard utf8Guard; #else StdoutMutexGuard stdoutGuard; #endif COUT << os.str(); } void printCenteredContentsCerr(string msj, unsigned int width, bool encapsulated) { OUTSTRINGSTREAM os; printCenteredContents(os, msj, width, encapsulated); #ifdef _WIN32 WindowsUtf8StdoutGuard utf8Guard; #else StdoutMutexGuard stdoutGuard; #endif CERR << os.str(); } void printPercentageLineCerr(const char *title, long long completed, long long total, float percentDowloaded, bool cleanLineAfter) { int cols = getNumberOfCols(80); string outputString; outputString.resize(cols + 1); for (int i = 0; i < cols; i++) { outputString[i] = '.'; } outputString[cols] = '\0'; char *ptr = (char *)outputString.c_str(); sprintf(ptr, "%s%s", title, " ||"); ptr += strlen(title); ptr += strlen(" ||"); *ptr = '.'; //replace \0 char char aux[41]; if (total < 1048576) { sprintf(aux,"||(%lld/%lld KB: %6.2f %%) ", completed / 1024, total / 1024, percentDowloaded); } else { sprintf(aux,"||(%lld/%lld MB: %6.2f %%) ", completed / 1024 / 1024, total / 1024 / 1024, percentDowloaded); } sprintf((char *)outputString.c_str() + cols - strlen(aux), "%s", aux); for (int i = 0; i < ( cols - (strlen(title) + strlen(" ||")) - strlen(aux)) * 1.0 * min(100.0f,percentDowloaded) / 100.0; i++) { *ptr++ = '#'; } #ifdef _WIN32 WindowsUtf8StdoutGuard utf8Guard; #else StdoutMutexGuard stdoutGuard; #endif // TODO: should this be CERR ? (`\r`) may not behave as wanted if (cleanLineAfter) { cerr << outputString << '\r' << flush; } else { cerr << outputString << endl; } } std::string wrapText(const std::string &input, std::size_t maxWidth, int indentSpaces) { const std::string indent(indentSpaces, ' '); std::istringstream iss(input); std::ostringstream oss; std::string word, line = indent; while (iss >> word) { if (line.size() + word.size() + 1 > maxWidth) { oss << line << '\n'; line = indent + word; } else { if (line != indent) { line += ' '; } line += word; } } if (!line.empty()) { oss << line; } return oss.str(); } int getFlag(const map *flags, const char * optname) { auto i = flags->find(optname); if (i != flags->end()) return i->second; return 0; } string getOption(const map *cloptions, const char * optname, string defaultValue) { auto i = cloptions->find(optname); if (i != cloptions->end()) return i->second; return defaultValue; } std::optional getOptionAsOptional(const map& cloptions, const char * optname) { if (cloptions.find(optname) == cloptions.end()) { return std::nullopt; } return cloptions.at(optname); } int getintOption(const map *cloptions, const char * optname, int defaultValue) { auto optionalInt = getIntOptional(*cloptions, optname); return optionalInt.value_or(defaultValue); } std::optional getIntOptional(const std::map& cloptions, const char* optName) { auto it = cloptions.find(optName); if (it == cloptions.end()) { return std::nullopt; } try { return std::stoi(it->second); } catch (...) { return std::nullopt; } } void discardOptionsAndFlags(vector *ws) { for (std::vector::iterator it = ws->begin(); it != ws->end(); ) { /* std::cout << *it; ... */ string w = ( string ) * it; if (w.length() && ( w.at(0) == '-' )) //begins with "-" { it = ws->erase(it); } else //not an option/flag { ++it; } } } string sizeProgressToText(long long partialSize, long long totalSize, bool equalizeUnitsLength, bool humanreadable) { ostringstream os; os.precision(2); if (humanreadable) { string unit; unit = ( equalizeUnitsLength ? " B" : "B" ); double reducedPartSize = (double)totalSize; double reducedSize = (double)totalSize; if ( totalSize > 1099511627776LL *2 ) { reducedPartSize = totalSize / (double) 1099511627776ull; reducedSize = totalSize / (double) 1099511627776ull; unit = "TB"; } else if ( totalSize > 1073741824LL *2 ) { reducedPartSize = totalSize / (double) 1073741824L; reducedSize = totalSize / (double) 1073741824L; unit = "GB"; } else if (totalSize > 1048576 * 2) { reducedPartSize = totalSize / (double) 1048576; reducedSize = totalSize / (double) 1048576; unit = "MB"; } else if (totalSize > 1024 * 2) { reducedPartSize = totalSize / (double) 1024; reducedSize = totalSize / (double) 1024; unit = "KB"; } os << fixed << reducedPartSize << "/" << reducedSize; os << " " << unit; } else { os << partialSize << "/" << totalSize; } return os.str(); } string sizeToText(long long totalSize, bool equalizeUnitsLength, bool humanreadable) { ostringstream os; os.precision(2); if (humanreadable) { string unit; unit = ( equalizeUnitsLength ? " B" : "B" ); double reducedSize = (double)totalSize; if ( totalSize > 1099511627776LL *2 ) { reducedSize = totalSize / (double) 1099511627776ull; unit = "TB"; } else if ( totalSize > 1073741824LL *2 ) { reducedSize = totalSize / (double) 1073741824L; unit = "GB"; } else if (totalSize > 1048576 * 2) { reducedSize = totalSize / (double) 1048576; unit = "MB"; } else if (totalSize > 1024 * 2) { reducedSize = totalSize / (double) 1024; unit = "KB"; } os << fixed << reducedSize; os << " " << (reducedSize == 0.0 ? " " : "") << unit; } else { os << totalSize; } return os.str(); } int64_t textToSize(const char *text) { int64_t sizeinbytes = 0; char * ptr = (char *)text; char * last = (char *)text; while (*ptr != '\0') { if (( *ptr < '0' ) || ( *ptr > '9' ) || ( *ptr == '.' ) ) { switch (*ptr) { case 'b': //Bytes case 'B': *ptr = '\0'; sizeinbytes += int64_t(atof(last)); break; case 'k': //KiloBytes case 'K': *ptr = '\0'; sizeinbytes += int64_t(1024.0 * atof(last)); break; case 'm': //MegaBytes case 'M': *ptr = '\0'; sizeinbytes += int64_t(1048576.0 * atof(last)); break; case 'g': //GigaBytes case 'G': *ptr = '\0'; sizeinbytes += int64_t(1073741824.0 * atof(last)); break; case 't': //TeraBytes case 'T': *ptr = '\0'; sizeinbytes += int64_t(1125899906842624.0 * atof(last)); break; default: { return -1; } } last = ptr + 1; } char *prev = ptr; ptr++; if (*ptr == '\0' && ( ( *prev == '.' ) || ( ( *prev >= '0' ) && ( *prev <= '9' ) ) ) ) //reach the end with a number or dot { return -1; } } return sizeinbytes; } string percentageToText(float percentage) { ostringstream os; os.precision(2); if (percentage != percentage) //NaN { os << "----%"; } else { os << fixed << percentage*100.0 << "%"; } return os.str(); } unsigned int getNumberOfCols(unsigned int defaultwidth) { #ifdef _WIN32 CONSOLE_SCREEN_BUFFER_INFO csbi; int columns = defaultwidth; if (GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi)) { columns = csbi.srWindow.Right - csbi.srWindow.Left - 1; } return columns; #else struct winsize size; if ( ioctl(STDOUT_FILENO,TIOCGWINSZ,&size) != -1 || (ioctl(STDIN_FILENO,TIOCGWINSZ,&size) != -1)) { if (size.ws_col > 2) { return size.ws_col - 2; } } return defaultwidth; #endif } void sleepSeconds(int seconds) { #ifdef _WIN32 Sleep(1000*seconds); #else sleep(seconds); #endif } void sleepMilliSeconds(long milliseconds) { #ifdef _WIN32 Sleep(milliseconds); #else usleep(milliseconds *1000); #endif } bool isValidEmail(const string &email) { if (email.size() < 5) // At least "a@b.c" { return false; } size_t atPos = email.find("@"); if (atPos == string::npos || atPos == 0 || atPos + 1 >= email.length() || email.at(atPos + 1) == '.') { return false; } if (size_t dotPos = email.find(".", atPos + 1); dotPos == string::npos || dotPos == email.length() - 1) { return false; } return true; } #ifdef __linux__ std::string getCurrentExecPath() { std::string path = "."; pid_t pid = getpid(); char buf[20] = {0}; sprintf(buf,"%d",pid); std::string _link = "/proc/"; _link.append( buf ); _link.append( "/exe"); char proc[PATH_MAX]; int ch = readlink(_link.c_str(),proc,PATH_MAX); if (ch != -1) { proc[ch] = 0; path = proc; std::string::size_type t = path.find_last_of("/"); path = path.substr(0,t); } return path; } #endif string <rimProperty(string &s, const char &c) { size_t pos = s.find_first_not_of(c); s = s.substr(pos == string::npos ? s.length() : pos, s.length()); return s; } string &rtrimProperty(string &s, const char &c) { size_t pos = s.find_last_not_of(c); if (pos != string::npos) { pos++; } s = s.substr(0, pos); return s; } string &trimProperty(string &what) { rtrimProperty(what,' '); ltrimProperty(what,' '); if (what.size() > 1) { if (what[0] == '\'' || what[0] == '"') { rtrimProperty(what, what[0]); ltrimProperty(what, what[0]); } } return what; } string getPropertyFromFile(const fs::path &configFilePath, const char *propertyName) { ifstream infile(configFilePath); string line; while (getline(infile, line)) { if (line.length() > 0 && line[0] != '#') { if (!strlen(propertyName)) //if empty return first line { return trimProperty(line); } string key, value; size_t pos = line.find("="); if (pos != string::npos && ((pos + 1) < line.size())) { key = line.substr(0, pos); rtrimProperty(key, ' '); if (!strcmp(key.c_str(), propertyName)) { value = line.substr(pos + 1); return trimProperty(value); } } } } return string(); } void ColumnDisplayer::endregistry() { values.push_back(std::move(currentRegistry)); currentlength = 0; } void ColumnDisplayer::setPrefix(const std::string &prefix) { mPrefix = prefix; } void ColumnDisplayer::addHeader(const string &name, bool fixed, int minWidth) { fields[name] = Field(name, fixed, minWidth); } void ColumnDisplayer::addValue(const string &name, const string &value, bool replace) { int len = getstringutf8size(value); if (!replace) { if (currentRegistry.size() && currentRegistry.find(name) != currentRegistry.end()) { endregistry(); } } currentRegistry[name] = value; currentlength += len; if (fields.find(name) == fields.end()) { addHeader(name, true); } if (find (mFieldnames.begin(), mFieldnames.end(), name) == mFieldnames.end()) { mFieldnames.push_back(name); } fields[name].updateMaxValue(len); } ColumnDisplayer::ColumnDisplayer(std::map *clflags, std::map *cloptions) : mClflags(clflags), mCloptions(cloptions), mUnfixedColsMinSize(getintOption(cloptions,"path-display-size", 0)) { } OUTSTRING ColumnDisplayer::str(bool printHeader) { OUTSTRINGSTREAM oss; print(oss, printHeader); return oss.str(); } void ColumnDisplayer::printHeaders(OUTSTREAMTYPE &os) { print(os, getintOption(mCloptions, "client-width", getNumberOfCols(75)), true, true); } void ColumnDisplayer::print(OUTSTREAMTYPE &os, bool printHeader) { print(os, getintOption(mCloptions, "client-width", getNumberOfCols(75)), printHeader); } void ColumnDisplayer::print(OUTSTREAMTYPE &os, int fullWidth, bool printHeader, bool onlyHeaders) { if (currentRegistry.size()) { endregistry(); } auto outputcols = getOption(mCloptions,"output-cols", ""); decltype (mFieldnames) fieldnames; if (!outputcols.empty()) { auto listofCols = split(outputcols, ","); for (auto el : listofCols) { auto it = find (mFieldnames.begin(), mFieldnames.end(), el); if (it != mFieldnames.end()) { fieldnames.push_back(el); } } } else { fieldnames = mFieldnames; } auto colseparator = getOption(mCloptions,"col-separator", ""); if (!colseparator.empty()) //col separator separated values { if (printHeader) { bool first = true; os << mPrefix; for (auto el : fieldnames) { Field &f = fields[el]; if (!first) { os << colseparator; } first = false; os << f.name; } os << std::endl; } if (!onlyHeaders) { for (auto ®istry : values) { bool firstvalue = true; os << mPrefix; for (auto &el : fieldnames) { Field &f = fields[el]; if (!firstvalue) { os << colseparator; } firstvalue = false; if (registry.find(f.name) != registry.end()) { os << registry[f.name]; } } os << std::endl; } } return; } int unfixedfieldscount = 0; int unfixedFieldsMaxLengthSum = 0; int leftWidth = fullWidth; vector unfixedfields; for (auto &el : fields) { Field &f = el.second; if (f.fixedSize) { if (f.fixedWidth) { f.dispWidth = f.fixedWidth; } else { f.dispWidth = max((int)getstringutf8size(f.name),f.maxValueLength); } leftWidth-=(f.dispWidth + 1); } else { unfixedfieldscount++; unfixedfields.push_back(&f); unfixedFieldsMaxLengthSum+=f.maxValueLength; } } auto unfixedFieldsMaxLengthsLeft = unfixedFieldsMaxLengthSum; for (auto &f: unfixedfields) { unfixedFieldsMaxLengthsLeft -= f->maxValueLength; f->dispWidth = max( (int)getstringutf8size(f->name), // min limit: header size min( f->maxValueLength // max limit: its longest value , max(mUnfixedColsMinSize, // min limit 2: the min limit for unfixed columns max((leftWidth - unfixedfieldscount + 1)/(unfixedfieldscount), (leftWidth - unfixedfieldscount + 1 - unfixedFieldsMaxLengthsLeft)) //either an equitative share between all unfixedfields left, or all the space the other left me considering their maxLegnths ) ) ); leftWidth-=(f->dispWidth + 1); unfixedfieldscount--; } if (printHeader) { bool first = true; os << mPrefix; for (auto el : fieldnames) { Field &f = fields[el]; if (!first) { os << " "; } first = false; os << getFixLengthString(f.name, f.dispWidth); } os << std::endl; } if (!onlyHeaders) { for (auto ®istry : values) { bool firstvalue = true; os << mPrefix; for (auto &el : fieldnames) { Field &f = fields[el]; if (!firstvalue) { os << " "; } firstvalue = false; if (registry.find(f.name) != registry.end()) { os << getFixLengthString(registry[f.name], f.dispWidth); } else { os << getFixLengthString("", f.dispWidth); } } os << std::endl; } } } void ColumnDisplayer::clear() { *this = ColumnDisplayer(mClflags, mCloptions); } Field::Field() { } Field::Field(string name, bool fixed, int minWidth) :name(name), fixedSize(fixed), fixedWidth(minWidth) { if (fixed) { this->dispWidth = minWidth; } } void Field::updateMaxValue(int newcandidate) { if (newcandidate > this->maxValueLength) { this->maxValueLength = newcandidate; } } std::unique_ptr PlatformDirectories::getPlatformSpecificDirectories() { #ifdef _WIN32 return std::make_unique(); #elif defined(__APPLE__) return std::make_unique(); #else return std::make_unique(); #endif } #ifdef _WIN32 fs::path WindowsDirectories::configDirPath() { wchar_t szPath[MAX_PATH]; if (GetModuleFileNameW(NULL, szPath, MAX_PATH) == 0) { return {}; } fs::path folder = fs::path(szPath).parent_path() / ".megaCmd"; auto suffix = _wgetenv(L"MEGACMD_WORKING_FOLDER_SUFFIX"); if (suffix != nullptr) { folder += "_"; folder += suffix; } folder = nonMaxPathLimitedPath(folder); // prepend //?/ return folder; } std::wstring getNamedPipeName() { std::wstring name = L"\\\\.\\pipe\\megacmdpipe_"; wchar_t username[UNLEN + 1]; DWORD username_len = UNLEN + 1; wchar_t *suffix; GetUserNameW(username, &username_len); name += username; suffix = _wgetenv(L"MEGACMD_PIPE_SUFFIX"); if (suffix != nullptr) { name += L"_"; name += suffix; } return name; } #else // !defined(_WIN32) std::string PosixDirectories::homeDirPath() { const char *homedir = getenv("HOME"); if (homedir != nullptr) { return homedir; } struct passwd pwd = {}; struct passwd *pwdresult = nullptr; long int bufsize = sysconf(_SC_GETPW_R_SIZE_MAX); bufsize = bufsize == -1 ? 1024 : bufsize; auto pwdbuf = std::unique_ptr(new char[bufsize]); if (getpwuid_r(getuid(), &pwd, pwdbuf.get(), bufsize, &pwdresult)) { std::cerr << "Warn: Could not get HOME folder from getpwuid_r. errno = " << errno << std::endl; return std::string(); } return std::string(pwd.pw_dir); } fs::path PosixDirectories::configDirPath() { std::string home = homeDirPath(); if (home.empty()) { return noHomeFallbackFolder(); } struct stat path_stat = {}; bool exists = !stat(home.c_str(), &path_stat) && S_ISDIR(path_stat.st_mode); return exists ? home.append("/.megaCmd") : noHomeFallbackFolder(); } string PosixDirectories::noHomeFallbackFolder() { return std::string("/tmp/megacmd-").append(std::to_string(getuid())); } #ifdef __APPLE__ fs::path MacOSDirectories::runtimeDirPath() { std::string home = homeDirPath(); if (home.empty()) { // fallback to Posix: return PosixDirectories::runtimeDirPath(); } auto cachesPath = std::string(home).append("/Library/Caches"); struct stat path_stat = {}; bool exists = !stat(cachesPath.c_str(), &path_stat) && S_ISDIR(path_stat.st_mode); return exists ? cachesPath.append("/megacmd.mac") : noHomeFallbackFolder(); } #endif // !defined(__APPLE__) std::string getOrCreateSocketPath(bool createDirectory) { auto dirs = PlatformDirectories::getPlatformSpecificDirectories(); auto socketFolder = dirs->runtimeDirPath(); if (socketFolder.empty()) { std::cerr << "FATAL: Could not get runtime folder for socket path" << std::endl; throw std::runtime_error("Could not get runtime folder for socket path"); } const char *sockname_c = getenv("MEGACMD_SOCKET_NAME"); std::string sockname = sockname_c != nullptr ? std::string(sockname_c) : "megacmd.socket"; static auto MAX_SOCKET_PATH = sizeof(sockaddr_un::sun_path) / sizeof(decltype(sockaddr_un::sun_path[0])); if ((socketFolder.string().size() + 1 + sockname.size()) >= (MAX_SOCKET_PATH - 1)) { if (createDirectory) { std::cerr << "WARN: socket path in runtime dir would exceed max size. Falling back to /tmp" << std::endl; } socketFolder = PosixDirectories::noHomeFallbackFolder(); } struct stat path_stat = {}; if (createDirectory) { bool exists = !stat(socketFolder.c_str(), &path_stat) && S_ISDIR(path_stat.st_mode); if (!exists && createDirectory) { mode_t mode = umask(0); bool failed = mkdir(socketFolder.c_str(), 0700) != 0; if (failed) { std::cerr << "Failed to create folder for unix socket: " << socketFolder << ": " << std::strerror(errno) << std::endl; } umask(mode); if (failed) { return std::string(); } } } return (socketFolder / sockname).string(); } #endif // ifdef(_WIN32) else } //end namespace MEGAcmd-2.5.2_Linux/src/megacmdcommonutils.h000066400000000000000000000500011516543156300207300ustar00rootroot00000000000000/** * @file src/megacmdcommonutils.h * @brief MEGAcmd: Auxiliary methods * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of the MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #ifndef MEGACMDCOMMONUTILS_H #define MEGACMDCOMMONUTILS_H #include "megacmd_utf8.h" #include #ifndef _WIN32 #include #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef UNUSED #define UNUSED(x) (void)(x) #endif #define MOVE_CAPTURE(x) x{std::move(x)} #define FWD_CAPTURE(x) x{std::forward(x)} #define CONST_CAPTURE(x) &x = std::as_const(x) using std::setw; using std::left; #ifndef _WIN32 #define ARRAYSIZE(a) (sizeof((a)) / sizeof(*(a))) #endif namespace megacmd { // output codes enum { MCMD_OK = 0, ///< Everything OK MCMD_CONFIRM_NO = -12, ///< User response to confirmation is "no" MCMD_EARGS = -51, ///< Wrong arguments MCMD_INVALIDEMAIL = -52, ///< Invalid email MCMD_NOTFOUND = -53, ///< Resource not found MCMD_INVALIDSTATE = -54, ///< Invalid state MCMD_INVALIDTYPE = -55, ///< Invalid type MCMD_NOTPERMITTED = -56, ///< Operation not allowed MCMD_NOTLOGGEDIN = -57, ///< Needs logging in MCMD_NOFETCH = -58, ///< Nodes not fetched MCMD_EUNEXPECTED = -59, ///< Unexpected failure MCMD_REQCONFIRM = -60, ///< Confirmation required MCMD_REQSTRING = -61, ///< String required MCMD_PARTIALOUT = -62, ///< Partial output provided MCMD_PARTIALERR = -63, ///< Partial error output provided MCMD_EXISTS = -64, ///< Resource already exists MCMD_REQRESTART = -71, ///< Restart required }; enum confirmresponse { MCMDCONFIRM_NO=0, MCMDCONFIRM_YES, MCMDCONFIRM_ALL, MCMDCONFIRM_NONE }; /* commands */ static std::vector validGlobalParameters {"v", "help"}; static std::vector localremotefolderpatterncommands {"sync", "fuse-add"}; static std::vector remotepatterncommands {"export", "attr"}; static std::vector remotefolderspatterncommands {"cd", "share"}; static std::vector multipleremotepatterncommands {"ls", "tree", "mkdir", "rm", "du", "find", "mv", "deleteversions", "cat", "mediainfo" #ifdef HAVE_LIBUV , "webdav", "ftp" #endif }; static std::vector remoteremotepatterncommands {"cp"}; static std::vector remotelocalpatterncommands {"get", "thumbnail", "preview"}; static std::vector localfolderpatterncommands {"lcd", "sync-ignore", "fuse-remove", "fuse-enable", "fuse-disable", "fuse-show", "fuse-config"}; static std::vector emailpatterncommands {"invite", "signup", "ipc", "users"}; static std::vector loginInValidCommands { "log", "debug", "speedlimit", "help", "logout", "version", "quit", "clear", "https", "exit", "errorcode", "proxy", "sync-config", "configure" #if defined(_WIN32) && defined(NO_READLINE) , "autocomplete", "codepage" #elif defined(_WIN32) , "unicode" #endif #if defined(_WIN32) || defined(__APPLE__) , "update" #endif }; static std::vector allValidCommands { "login", "signup", "confirm", "session", "mount", "ls", "cd", "log", "debug", "pwd", "lcd", "lpwd", "import", "masterkey", "put", "get", "attr", "userattr", "mkdir", "rm", "du", "mv", "cp", "sync", "sync-ignore", "export", "share", "invite", "ipc", "df", "showpcr", "users", "speedlimit", "killsession", "whoami", "help", "passwd", "reload", "logout", "version", "quit", "thumbnail", "preview", "find", "completion", "clear", "https", "sync-issues", "transfers", "exclude", "exit", "errorcode", "graphics", "cancel", "confirmcancel", "cat", "tree", "psa", "proxy", "sync-config", "configure", "mediainfo" #ifdef HAVE_LIBUV , "webdav", "ftp" #endif , "backup" , "deleteversions" #if defined(_WIN32) && defined(NO_READLINE) , "autocomplete", "codepage" #elif defined(_WIN32) , "unicode" #else , "permissions" #endif #if defined(_WIN32) || defined(__APPLE__) , "update" #endif #ifdef WITH_FUSE , "fuse-add" , "fuse-remove" , "fuse-enable" , "fuse-disable" , "fuse-show" , "fuse-config" #endif #if defined(DEBUG) || defined(MEGACMD_TESTING_CODE) , "echo" #endif }; static const int RESUME_SESSION_TIMEOUT = 10; std::string errorCodeStr(const std::error_code& ec); /* Files and folders */ //tests if a path is writable //TODO: move to fsAccess /** * @brief tests if a path is writable. * @param path utf-8 path * @return true if writable or couldn't say. */ bool canWrite(const std::string &path); bool isPublicLink(const std::string &link); bool isEncryptedLink(std::string link); std::string getPublicLinkHandle(const std::string &link); bool hasWildCards(std::string &what); /** * @brief Removes trailing path separators from the path. * @param path Path string to process (modified in place) * @return Path with trailing separators removed * * Handles simple cases: removes trailing '/' and '\' characters. */ std::string removeTrailingSeparators(std::string &path); /* Strings related */ std::string generateRandomAlphaNumericString(size_t len); std::vector split(const std::string& input, const std::string& pattern); long long charstoll(const char *instr); // trim from start std::string <rim(std::string &s, const char &c); std::string_view ltrim(const std::string_view s, const char &c); // trim at the end std::string &rtrim(std::string &s, const char &c); std::vector getlistOfWords(const char *ptr, bool escapeBackSlashInCompletion = false, bool ignoreTrailingSpaces = false); bool stringcontained(const char * s, std::vector list); char * dupstr(char* s); bool replace(std::string& str, const std::string& from, const std::string& to); void replaceAll(std::string& str, const std::string& from, const std::string& to); /** * @brief Converts a string to an integer. * * Parses the string as a base-10 integer. Must contain only digits with * optional leading sign ('+' or '-'). No whitespace allowed. * * @param str The string to convert. * @param failValue Return value on conversion failure. * @return Converted integer or failValue if invalid/out of range. */ int toInteger(const std::string &str, int failValue = -1); std::string joinStrings(const std::vector& vec, const char* delim = " ", bool quoted=true); std::string getFixLengthString(const std::string &origin, unsigned int size, const char delimm=' ', bool alignedright = false); std::string getRightAlignedString(const std::string origin, unsigned int minsize); bool startsWith(const std::string_view str, const std::string_view prefix); /* Vector related */ template std::vector operator+(const std::vector& a, const std::vector& b) { std::vector result = a; result.insert(result.end(), b.begin(), b.end()); return result; } template std::vector operator+(std::vector&& a, std::vector&& b) { a.reserve(a.size() + b.size()); a.insert(a.end(), std::make_move_iterator(b.begin()), std::make_move_iterator(b.end())); return std::move(a); } std::string toLower(const std::string& str); template OUTSTRING getLeftAlignedStr(T what, int n) { OUTSTRINGSTREAM os; os << setw(n) << left << what; return os.str(); } template OUTSTRING getRightAlignedStr(T what, int n) { OUTSTRINGSTREAM os; os << setw(n) << what; return os.str(); } void printCenteredLine(std::string msj, unsigned int width, bool encapsulated = true); void printCenteredContents(std::string msj, unsigned int width, bool encapsulated = true); void printCenteredContentsCerr(std::string msj, unsigned int width, bool encapsulated = true); template void printCenteredLine(Stream_T &os, std::string msj, unsigned int width, bool encapsulated = true); void printCenteredContents(OUTSTREAMTYPE &os, std::string msj, unsigned int width, bool encapsulated = true); template void printCenteredContentsT(WHERE &&o, const std::string &msj, unsigned int width, bool encapsulated) { OUTSTRINGSTREAM os; printCenteredContents(os, msj, width, encapsulated); o << os.str(); } template bool onlyZeroOrOneOf(Bools... args) { return (args + ...) <= 1; } template bool onlyZeroOf(Bools... args) { return (args + ...) == 0; } void printPercentageLineCerr(const char *title, long long completed, long long total, float percentDowloaded, bool cleanLineAfter = true); std::string wrapText(const std::string& input, std::size_t maxWidth, int indentSpaces = 0); /* Flags and Options */ int getFlag(const std::map *flags, const char * optname); std::string getOption(const std::map *cloptions, const char * optname, std::string defaultValue = ""); std::optional getOptionAsOptional(const std::map& cloptions, const char * optname); int getintOption(const std::map *cloptions, const char * optname, int defaultValue = 0); std::optional getIntOptional(const std::map& cloptions, const char* optName); void discardOptionsAndFlags(std::vector *ws); //This has been defined in megacmdutils.h because it depends on logging. If ever require offer a version without it here //bool setOptionsAndFlags(map *opts, map *flags, vector *ws, set vvalidOptions, bool global) /* Others */ std::string sizeToText(long long totalSize, bool equalizeUnitsLength = true, bool humanreadable = true); std::string sizeProgressToText(long long partialSize, long long totalSize, bool equalizeUnitsLength = true, bool humanreadable = true); int64_t textToSize(const char *text); std::string percentageToText(float percentage); unsigned int getNumberOfCols(unsigned int defaultwidth = 90); void sleepSeconds(int seconds); void sleepMilliSeconds(long microseconds); /** * @brief Validates email address format using basic checks. * @param email Email address to validate * @return true if email passes basic format validation, false otherwise * * This function performs just minimal validations, it's not a full RFC-compliant validator. */ bool isValidEmail(const std::string &email); #ifdef __linux__ std::string getCurrentExecPath(); #endif /* Properties */ std::string <rimProperty(std::string &s, const char &c); std::string &rtrimProperty(std::string &s, const char &c); std::string &trimProperty(std::string &what); std::string getPropertyFromFile(const fs::path &configFilePath, const char *propertyName); template T getValueFromFile(const fs::path &configFilePath, const char *propertyName, T defaultValue) { std::string propValue = getPropertyFromFile(configFilePath, propertyName); if (!propValue.size()) return defaultValue; T i; std::istringstream is(propValue); is >> i; return i; } class Field { public: Field(); Field(std::string name, bool fixed = false, int fixedWidth = 0); public: std::string name; int fixedWidth; bool fixedSize; int dispWidth = 0; int maxValueLength = 0; void updateMaxValue(int newcandidate); }; class ColumnDisplayer { public: ColumnDisplayer(std::map *clflags, std::map *cloptions); OUTSTRING str(bool printHeader = true); void printHeaders(OUTSTREAMTYPE &os); void print(OUTSTREAMTYPE &os, bool printHeader = true); void clear(); void addHeader(const std::string &name, bool fixed = true, int minWidth = 0); void addValue(const std::string &name, const std::string & value, bool replace = false); void endregistry(); void setPrefix(const std::string &prefix); private: std::map *mClflags; std::map *mCloptions; std::map fields; std::vector mFieldnames; std::vector> values; std::vector lengths; std::map currentRegistry; int currentlength = 0; int mUnfixedColsMinSize = 0; std::string mPrefix; void print(OUTSTREAMTYPE &os, int fullWidth, bool printHeader=true, bool onlyHeaders = false); }; /** * @name PlatformDirectories * @brief PlatformDirectories provides methods for accessing directories for storing user data for * MEGAcmd. * * Note: returned values are encoded in utf-8. * * +---------------------------------------------------------------------------------------------------------------------+ * | DirPath | Windows | Linux | macOS | Linux/macOS (no HOME fallback) | * |---------|----------------------|----------------|----------------------------------|--------------------------------| * | runtime | $EXECFOLDER/.megaCmd | $HOME/.megaCmd | $HOME/Library/Caches/megacmd.mac | /tmp/megacmd-UID/.megacmd | * | config | $EXECFOLDER/.megaCmd | $HOME/.megaCmd | $HOME/.megaCmd | /tmp/megacmd-UID/.megacmd | * +---------------------------------------------------------------------------------------------------------------------+ * */ class PlatformDirectories { public: static std::unique_ptr getPlatformSpecificDirectories(); virtual ~PlatformDirectories() = default; /** * @brief runtimeDirPath returns the base path for storing runtime files: * * Meant for sockets, named pipes, file locks, command history, etc. */ virtual fs::path runtimeDirPath() { return configDirPath(); } /** * @brief configDirPath returns the base path for storing configuration and data files. * * Meant for user-editable configuration files, data files that should not be deleted * (session credentials, SDK workding directory, logs, etc). */ virtual fs::path configDirPath() = 0; }; #ifdef _WIN32 class WindowsDirectories : public PlatformDirectories { fs::path configDirPath() override; }; std::wstring getNamedPipeName(); #else // !defined(_WIN32) class PosixDirectories : public PlatformDirectories { public: std::string homeDirPath(); virtual fs::path configDirPath() override; static std::string noHomeFallbackFolder(); }; #ifdef __APPLE__ class MacOSDirectories : public PosixDirectories { fs::path runtimeDirPath() override; }; #endif // defined(__APPLE__) std::string getOrCreateSocketPath(bool createDirectory); #endif // _WIN32 /** * @brief Common infrastructure for classes that are designed to have a single instance for * the whole lifetime of the application. This approach is used instead of singletons, * in order to take control of the lifetime of the instance, and to avoid the inherent issues * of static initialization and destruction order. */ template class BaseInstance { protected: inline static T* sInstance = nullptr; virtual ~BaseInstance() { // having a base class allows to set sInstance to nullptr // _after_ the (derived) Instance's destructor destroys mInstance, // thus allowing T's destructor to still access Instance::Get sInstance = nullptr; } }; template class Instance : protected BaseInstance { T mInstance; inline static std::mutex sPendingRogueOnesMutex; inline static std::condition_variable sPendingRogueOnesCV; inline static unsigned sPendingRogueOnes = 0; inline static bool sAssertOnDestruction = false; public: static bool waitForExplicitDependents() { // wait for rogue ones std::unique_lock lockGuard(sPendingRogueOnesMutex); if (!sPendingRogueOnesCV.wait_for(lockGuard, std::chrono::seconds(20), [](){return !sPendingRogueOnes;})) { std::cerr << "Shutdown gracetime for explicit dependents for Instance<" << typeid(T).name() << "> passed without success. Rogue ones = " << sPendingRogueOnes << ". Will remove the instance anyway." << std::endl; sAssertOnDestruction = true; return false; } return true; } class ExplicitDependent { public: ExplicitDependent() { std::lock_guard g(Instance::sPendingRogueOnesMutex); Instance::sPendingRogueOnes++; } virtual ~ExplicitDependent() { assert(!sAssertOnDestruction); { std::lock_guard g(Instance::sPendingRogueOnesMutex); Instance::sPendingRogueOnes--; } Instance::sPendingRogueOnesCV.notify_one(); } }; template Instance(Args&&... args) : mInstance(std::forward(args)...) { assert(!BaseInstance::sInstance && "There must be one and only one instance of this class"); BaseInstance::sInstance = &mInstance; } ~Instance() { bool explicitDependentsFinishedInTime = waitForExplicitDependents(); UNUSED(explicitDependentsFinishedInTime); assert(explicitDependentsFinishedInTime); } static T& Get() { assert(BaseInstance::sInstance && "Must create the unique instance before trying to retrieve it"); return *BaseInstance::sInstance; } #ifdef MEGACMD_TESTING_CODE static bool exists() { return BaseInstance::sInstance; } #endif //MEGACMD_TESTING_CODE }; /** * @brief general purpose scope guard class */ template class ScopeGuard { ExitCallback mExitCb; public: ScopeGuard(ExitCallback&& exitCb) : mExitCb{std::move(exitCb)} { } ~ScopeGuard() { mExitCb(); } }; template void timelyRetry(const std::chrono::duration<_Rep, _Period> &maxTime, const std::chrono::duration<_Rep2, _Period2> &step , Condition&& endCondition, What&& what) { using Step_t = std::chrono::duration<_Rep2, _Period2>; for (auto timeLeft = std::chrono::duration_cast(maxTime); !endCondition() && timeLeft > Step_t(0); timeLeft -= step) { std::this_thread::sleep_for(step); what(); } } class HammeringLimiter { int mLimitSecs; std::optional mLastCall; public: HammeringLimiter(int seconds) : mLimitSecs(seconds) {} // Returns true if run recently (or sets the last call to current time otherwise) bool runRecently() { auto now = std::chrono::steady_clock::now(); if (mLastCall && std::chrono::duration_cast(now - *mLastCall).count() <= mLimitSecs) { return true; } mLastCall = now; return false; } }; }//end namespace #endif // MEGACMDCOMMONUTILS_H MEGAcmd-2.5.2_Linux/src/megacmdexecuter.cpp000066400000000000000000014327151516543156300205570ustar00rootroot00000000000000/** * @file src/megacmdexecuter.cpp * @brief MEGAcmd: Executer of the commands * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of the MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include "megacmdexecuter.h" #include "megaapi.h" #include "megacmd.h" #include "megacmdutils.h" #include "megacmdcommonutils.h" #include "configurationmanager.h" #include "megacmdlogger.h" #include "comunicationsmanager.h" #include "listeners.h" #include "megacmdversion.h" #include "sync_command.h" #include "sync_ignore.h" #include "megacmd_fuse.h" #include #include #include #include #include #include #include #ifdef MEGACMD_TESTING_CODE #include "../tests/common/Instruments.h" using TI = TestInstruments; #endif using namespace mega; using namespace std; namespace megacmd { static const char* rootnodenames[] = { "ROOT", "INBOX", "RUBBISH" }; static const char* rootnodepaths[] = { "/", "//in", "//bin" }; #define SSTR( x ) static_cast< const std::ostringstream & >( \ ( std::ostringstream() << std::dec << x ) ).str() #ifdef HAVE_GLOB_H std::vector resolvewildcard(const std::string& pattern) { vector filenames; glob_t glob_result; memset(&glob_result, 0, sizeof(glob_result)); if (!glob(pattern.c_str(), GLOB_TILDE, NULL, &glob_result)) { for(size_t i = 0; i < glob_result.gl_pathc; ++i) { filenames.push_back(std::string(glob_result.gl_pathv[i])); } } globfree(&glob_result); return filenames; } #endif #ifdef WITH_FUSE std::optional loadFuseConfigDelta(const std::map& cloptions) { FuseCommand::ConfigDelta configDelta; std::string enableAtStartupStr = getOption(&cloptions, "enable-at-startup", ""); if (!enableAtStartupStr.empty()) { if (enableAtStartupStr != "yes" && enableAtStartupStr != "no") { LOG_err << "Flag \"enable-at-startup\" can only be \"yes\" or \"no\""; return {}; } configDelta.mEnableAtStartup = (enableAtStartupStr == "yes"); } std::string persistentStr = getOption(&cloptions, "persistent", ""); if (!persistentStr.empty()) { if (persistentStr != "yes" && persistentStr != "no") { LOG_err << "Flag \"persistent\" can only be \"yes\" or \"no\""; return {}; } configDelta.mPersistent = (persistentStr == "yes"); } std::string readOnlyStr = getOption(&cloptions, "read-only", ""); if (!readOnlyStr.empty()) { if (readOnlyStr != "yes" && readOnlyStr != "no") { LOG_err << "Flag \"read-only\" can only be \"yes\" or \"no\""; return {}; } configDelta.mReadOnly = (readOnlyStr == "yes"); } configDelta.mName = getOptionAsOptional(cloptions, "name"); if (!configDelta.isAnyFlagSet()) { LOG_err << "At least one flag must be set"; return {}; } if (configDelta.isPersistentStartupInvalid()) { LOG_err << "A non-persistent mount cannot be enabled at startup"; return {}; } return configDelta; } #endif /** * @brief updateprompt updates prompt with the current user/location * @param api * @param handle */ void MegaCmdExecuter::updateprompt(MegaApi *api) { if (!api) api = this->api; MegaHandle handle = cwd; string newprompt; MegaNode *n = api->getNodeByHandle(handle); MegaUser *u = api->getMyUser(); if (u) { const char *email = u->getEmail(); newprompt.append(email); delete u; } if (n) { char *np = api->getNodePath(n); if (!newprompt.empty()) { newprompt.append(":"); } if (np) { newprompt.append(np); } delete n; delete []np; } if (getBlocked()) { newprompt.append("[BLOCKED]"); } if (newprompt.empty()) //i.e. !u && !n { newprompt = prompts[0]; } else { newprompt.append("$ "); } changeprompt(newprompt.c_str()); } MegaCmdExecuter::MegaCmdExecuter(MegaApi *api, MegaCmdLogger *loggerCMD, MegaCmdSandbox *sandboxCMD) : // Give a few seconds in order for key sharing to happen mFsAccessCMD(::mega::createFSA()), mDeferredSharedFoldersVerifier(std::chrono::seconds(5)), mSyncIssuesManager(api) { signingup = false; confirming = false; this->api = api; this->loggerCMD = loggerCMD; this->sandboxCMD = sandboxCMD; this->globalTransferListener = new MegaCmdGlobalTransferListener(api, sandboxCMD); api->addTransferListener(globalTransferListener); api->addGlobalListener(mSyncIssuesManager.getGlobalListener()); cwd = UNDEF; session = NULL; } MegaCmdExecuter::~MegaCmdExecuter() { delete globalTransferListener; } // list available top-level nodes and contacts/incoming shares void MegaCmdExecuter::listtrees() { for (int i = 0; i < (int)( sizeof rootnodenames / sizeof *rootnodenames ); i++) { OUTSTREAM << rootnodenames[i] << " on " << rootnodepaths[i] << endl; if (!api->isLoggedIn()) { break; //only show /root } } MegaShareList * msl = api->getInSharesList(); for (int i = 0; i < msl->size(); i++) { MegaShare *share = msl->get(i); MegaNode *n = api->getNodeByHandle(share->getNodeHandle()); OUTSTREAM << "INSHARE on //from/" << share->getUser() << ":" << n->getName() << " (" << getAccessLevelStr(share->getAccess()) << ")" << (!share->isVerified() ? " [UNVERIFIED]" : "") << endl; delete n; } delete ( msl ); } bool MegaCmdExecuter::includeIfIsExported(MegaApi *api, MegaNode * n, void *arg) { if (n->isExported()) { (( vector * )arg )->push_back(n->copy()); return true; } return false; } bool MegaCmdExecuter::includeIfIsShared(MegaApi *api, MegaNode * n, void *arg) { if (n->isShared()) { (( vector * )arg )->push_back(n->copy()); return true; } return false; } bool MegaCmdExecuter::includeIfIsPendingOutShare(MegaApi *api, MegaNode * n, void *arg) { MegaShareList* pendingoutShares = api->getPendingOutShares(n); if (pendingoutShares && pendingoutShares->size()) { (( vector * )arg )->push_back(n->copy()); return true; } if (pendingoutShares) { delete pendingoutShares; } return false; } bool MegaCmdExecuter::includeIfIsSharedOrPendingOutShare(MegaApi *api, MegaNode * n, void *arg) { if (n->isShared()) { (( vector * )arg )->push_back(n->copy()); return true; } MegaShareList* pendingoutShares = api->getPendingOutShares(n); if (pendingoutShares && pendingoutShares->size()) { (( vector * )arg )->push_back(n->copy()); return true; } if (pendingoutShares) { delete pendingoutShares; } return false; } struct patternNodeVector { string pattern; bool usepcre; vector *nodesMatching; }; struct criteriaNodeVector { string pattern; bool usepcre; m_time_t minTime; m_time_t maxTime; int64_t maxSize; int64_t minSize; int mType = MegaNode::TYPE_UNKNOWN; vector *nodesMatching; }; bool MegaCmdExecuter::includeIfMatchesPattern(MegaApi *api, MegaNode * n, void *arg) { struct patternNodeVector *pnv = (struct patternNodeVector*)arg; if (patternMatches(n->getName(), pnv->pattern.c_str(), pnv->usepcre)) { pnv->nodesMatching->push_back(n->copy()); return true; } return false; } bool MegaCmdExecuter::includeIfMatchesCriteria(MegaApi *api, MegaNode * n, void *arg) { struct criteriaNodeVector *pnv = (struct criteriaNodeVector*)arg; if (pnv->mType != MegaNode::TYPE_UNKNOWN && n->getType() != pnv->mType ) { return false; } if ( pnv->maxTime != -1 && (n->getModificationTime() >= pnv->maxTime) ) { return false; } if ( pnv->minTime != -1 && (n->getModificationTime() <= pnv->minTime) ) { return false; } if ( pnv->maxSize != -1 && (n->getType() != MegaNode::TYPE_FILE || (n->getSize() > pnv->maxSize) ) ) { return false; } if ( pnv->minSize != -1 && (n->getType() != MegaNode::TYPE_FILE || (n->getSize() < pnv->minSize) ) ) { return false; } if (!patternMatches(n->getName(), pnv->pattern.c_str(), pnv->usepcre)) { return false; } pnv->nodesMatching->push_back(n->copy()); return true; } bool MegaCmdExecuter::processTree(MegaNode *n, bool processor(MegaApi *, MegaNode *, void *), void *( arg )) { if (!n) { return false; } bool toret = true; MegaNodeList *children = api->getChildren(n); if (children) { for (int i = 0; i < children->size(); i++) { bool childret = processTree(children->get(i), processor, arg); toret = toret && childret; } delete children; } bool currentret = processor(api, n, arg); return toret && currentret; } // returns node pointer determined by path relative to cwd // path naming conventions: // * path is relative to cwd // * /path is relative to ROOT // * //in is in INBOX // * //bin is in RUBBISH // * X: is user X's INBOX // * X:SHARE is share SHARE from user X // * H:HANDLE is node with handle HANDLE // * : and / filename components, as well as the \, must be escaped by \. // (correct UTF-8 encoding is assumed) // returns nullptr if path malformed or not found std::unique_ptr MegaCmdExecuter::nodebypath(const char* ptr, string* user, string* namepart) { if (ptr && ptr[0] == 'H' && ptr[1] == ':') { std::unique_ptr n(api->getNodeByHandle(api->base64ToHandle(ptr+2))); if (n) { return n; } } string rest; std::unique_ptr baseNode(getBaseNode(ptr, rest)); if (baseNode && !rest.size()) { return baseNode; } if (!rest.size() && !baseNode) { string path(ptr); if (path.size() && path.find("@") != string::npos && path.find_last_of(":") == (path.size() - 1)) { if (user) { *user = path.substr(0,path.size()-1); } } } while (baseNode) { size_t possep = rest.find('/'); string curName = rest.substr(0,possep); if (curName != ".") { std::unique_ptr nextNode; if (curName == "..") { nextNode.reset(api->getParentNode(baseNode.get())); } else { replaceAll(curName, "\\\\", "\\"); //unescape '\\' replaceAll(curName, "\\ ", " "); //unescape '\ ' bool isversion = nodeNameIsVersion(curName); if (isversion) { std::unique_ptr childNode(api->getChildNode(baseNode.get(), curName.substr(0,curName.size()-11).c_str())); if (childNode) { std::unique_ptr versionNodes(api->getVersions(childNode.get())); if (versionNodes) { for (int i = 0; i < versionNodes->size(); i++) { MegaNode* versionNode = versionNodes->get(i); if (curName.substr(curName.size()-10) == SSTR(versionNode->getModificationTime())) { nextNode.reset(versionNode->copy()); break; } } } } } else { nextNode.reset(api->getChildNode(baseNode.get(), curName.c_str())); } } // mv command target? return name part of not found if (namepart && !nextNode && (possep == string::npos)) //if this is the last part, we will pass that one, so that a mv command know the name to give the new node { *namepart = rest; return baseNode; } baseNode = std::move(nextNode); } if (possep != string::npos && possep != (rest.size() - 1)) { rest = rest.substr(possep+1); } else { return baseNode; } } return nullptr; } /** * @brief MegaCmdExecuter::getPathsMatching Gets paths of nodes matching a pattern given its path parts and a parent node * * @param parentNode node for reference for relative paths * @param pathParts path pattern (separated in strings) * @param pathsMatching for the returned paths * @param usepcre use PCRE expressions if available * @param pathPrefix prefix to append to paths */ void MegaCmdExecuter::getPathsMatching(MegaNode *parentNode, deque pathParts, vector *pathsMatching, bool usepcre, string pathPrefix) { if (!pathParts.size()) { return; } string currentPart = pathParts.front(); pathParts.pop_front(); if (currentPart == "." || currentPart == "") { if (pathParts.size() == 0 /*&& currentPart == "."*/) //last leave. // for consistency we also take parent when ended in / even if it's not a folder { pathsMatching->push_back(pathPrefix+currentPart); } //ignore this part return getPathsMatching(parentNode, pathParts, pathsMatching, usepcre, pathPrefix+"./"); } if (currentPart == "..") { if (parentNode->getParentHandle()) { if (!pathParts.size()) { pathsMatching->push_back(pathPrefix+".."); } unique_ptr p(api->getNodeByHandle(parentNode->getParentHandle())); return getPathsMatching(p.get(), pathParts, pathsMatching, usepcre, pathPrefix+"../"); } else { return; //trying to access beyond root node } } MegaNodeList* children = api->getChildren(parentNode); if (children) { bool isversion = nodeNameIsVersion(currentPart); for (int i = 0; i < children->size(); i++) { MegaNode *childNode = children->get(i); // get childname from its path: alternative: childNode->getName() char *childNodePath = api->getNodePath(childNode); char *aux; aux = childNodePath+strlen(childNodePath); while (aux>childNodePath){ if (*aux=='/' && *(aux-1) != '\\') break; aux--; } if (*aux=='/') aux++; string childname(aux); delete []childNodePath; if (isversion) { if (childNode && patternMatches(childname.c_str(), currentPart.substr(0,currentPart.size()-11).c_str(), usepcre)) { MegaNodeList *versionNodes = api->getVersions(childNode); if (versionNodes) { for (int i = 0; i < versionNodes->size(); i++) { MegaNode *versionNode = versionNodes->get(i); if ( currentPart.substr(currentPart.size()-10) == SSTR(versionNode->getModificationTime()) ) { if (pathParts.size() == 0) //last leave { pathsMatching->push_back(pathPrefix+childname+"#"+SSTR(versionNode->getModificationTime())); //TODO: def version separator elswhere } else { getPathsMatching(versionNode, pathParts, pathsMatching, usepcre,pathPrefix+childname+"#"+SSTR(versionNode->getModificationTime())+"/"); } break; } } delete versionNodes; } } } else { if (patternMatches(childname.c_str(), currentPart.c_str(), usepcre)) { if (pathParts.size() == 0) //last leave { pathsMatching->push_back(pathPrefix+childname); } else { getPathsMatching(childNode, pathParts, pathsMatching, usepcre,pathPrefix+childname+"/"); } } } } delete children; } } /** * @brief MegaCmdExecuter::nodesPathsbypath returns paths of nodes that match a determined by path pattern * path naming conventions: * path is relative to cwd * /path is relative to ROOT * //in is in INBOX * //bin is in RUBBISH * X: is user X's INBOX * X:SHARE is share SHARE from user X * H:HANDLE is node with handle HANDLE * : and / filename components, as well as the \, must be escaped by \. * (correct UTF-8 encoding is assumed) * * You take the ownership of the returned value * @param ptr * @param usepcre use PCRE expressions if available * @param user * @param namepart * @return */ vector * MegaCmdExecuter::nodesPathsbypath(const char* ptr, bool usepcre, string* user, string* namepart) { vector *pathsMatching = new vector (); if (ptr && ptr[0] == 'H' && ptr[1] == ':') { MegaNode * n = api->getNodeByHandle(api->base64ToHandle(ptr+2)); if (n) { char * nodepath = api->getNodePath(n); pathsMatching->push_back(nodepath); delete []nodepath; return pathsMatching; } } string rest; bool isrelative; MegaNode *baseNode = getBaseNode(ptr, rest, &isrelative); if (baseNode) { string pathPrefix; if (!isrelative) { char * nodepath = api->getNodePath(baseNode); pathPrefix=nodepath; if (pathPrefix.size() && pathPrefix.at(pathPrefix.size()-1)!='/') pathPrefix+="/"; delete []nodepath; } if (string(ptr).find("//from/") == 0) { pathPrefix.insert(0,"//from/"); } deque c; getPathParts(rest, &c); if (!c.size()) { char * nodepath = api->getNodePath(baseNode); pathsMatching->push_back(nodepath); delete []nodepath; } else { getPathsMatching((MegaNode *)baseNode, c, (vector *)pathsMatching, usepcre, pathPrefix); } delete baseNode; } else if (!strncmp(ptr,"//from/",max(3,min((int)strlen(ptr)-1,7)))) //pattern trying to match inshares { string matching = ptr; unescapeifRequired(matching); unique_ptr inShares(api->getInSharesList()); if (inShares) { for (int i = 0; i < inShares->size(); i++) { unique_ptr n(api->getNodeByHandle(inShares->get(i)->getNodeHandle())); string tomatch = string("//from/")+inShares->get(i)->getUser() + ":"+n->getName(); if (patternMatches(tomatch.c_str(), matching.c_str(), false)) { pathsMatching->push_back(tomatch); } } } } return pathsMatching; } /** * You take the ownership of the nodes added in nodesMatching * @brief getNodesMatching * @param parentNode * @param c * @param nodesMatching */ void MegaCmdExecuter::getNodesMatching(MegaNode *parentNode, deque pathParts, vector>& nodesMatching, bool usepcre) { if (!pathParts.size()) { return; } string currentPart = pathParts.front(); pathParts.pop_front(); if (currentPart == "." || currentPart == "") { if (pathParts.size() == 0 /*&& currentPart == "."*/) //last leave. // for consistency we also take parent when ended in / even if it's not a folder { if (parentNode) { nodesMatching.emplace_back(parentNode->copy()); return; } } else { //ignore this part return getNodesMatching(parentNode, pathParts, nodesMatching, usepcre); } } if (currentPart == "..") { if (parentNode->getParentHandle()) { MegaNode *newparentNode = api->getNodeByHandle(parentNode->getParentHandle()); if (!pathParts.size()) //last leave { if (newparentNode) { nodesMatching.emplace_back(newparentNode); } return; } else { getNodesMatching(newparentNode, pathParts, nodesMatching, usepcre); delete newparentNode; return; } } else { return; //trying to access beyond root node } } MegaNodeList* children = api->getChildren(parentNode); if (children) { bool isversion = nodeNameIsVersion(currentPart); for (int i = 0; i < children->size(); i++) { MegaNode *childNode = children->get(i); if (isversion) { if (childNode && patternMatches(childNode->getName(), currentPart.substr(0,currentPart.size()-11).c_str(), usepcre)) { MegaNodeList *versionNodes = api->getVersions(childNode); if (versionNodes) { for (int i = 0; i < versionNodes->size(); i++) { MegaNode *versionNode = versionNodes->get(i); if ( currentPart.substr(currentPart.size()-10) == SSTR(versionNode->getModificationTime()) ) { if (pathParts.size() == 0) //last leave { nodesMatching.emplace_back(versionNode->copy()); } else { getNodesMatching(versionNode, pathParts, nodesMatching, usepcre); } break; } } delete versionNodes; } } } else { if (patternMatches(childNode->getName(), currentPart.c_str(), usepcre)) { if (pathParts.size() == 0) //last leave { nodesMatching.emplace_back(childNode->copy()); } else { getNodesMatching(childNode, pathParts, nodesMatching, usepcre); } } } } delete children; } } // TODO: docs MegaNode * MegaCmdExecuter::getBaseNode(string thepath, string &rest, bool *isrelative) { if (isrelative != NULL) { *isrelative = false; } MegaNode *baseNode = NULL; rest = string(); if (thepath == "//bin") { baseNode = api->getRubbishNode(); rest = ""; } else if (thepath == "//in") { baseNode = api->getVaultNode(); rest = ""; } else if (thepath == "/") { baseNode = api->getRootNode(); rest = ""; } else if (thepath.find("//bin/") == 0 ) { baseNode = api->getRubbishNode(); rest = thepath.substr(6); } else if (thepath.find("//in/") == 0 ) { baseNode = api->getVaultNode(); rest = thepath.substr(5); } else if (thepath.find("/") == 0 && !(thepath.find("//from/") == 0 )) { if ( thepath.find("//f") == 0 && string("//from/").find(thepath.substr(0,thepath.find("*"))) == 0) { return NULL; } baseNode = api->getRootNode(); rest = thepath.substr(1); } else if ( thepath == "//from/*" ) { return NULL; } else { bool from = false; if (thepath.find("//from/") == 0 && thepath != "//from/*" ) { thepath = thepath.substr(7); from = true; } size_t possep = thepath.find('/'); string base = thepath.substr(0,possep); size_t possepcol = base.find(":"); size_t possepat = base.find("@"); if ( possepcol != string::npos && possepat != string::npos && possepat < possepcol && possepcol < (base.size() + 1) ) { string userName = base.substr(0,possepcol); string inshareName = base.substr(possepcol + 1); unescapeifRequired(inshareName); MegaUserList * usersList = api->getContacts(); MegaUser *u = NULL; for (int i = 0; i < usersList->size(); i++) { if (usersList->get(i)->getEmail() == userName) { u = usersList->get(i); break; } } if (u) { MegaNodeList* inshares = api->getInShares(u); for (int i = 0; i < inshares->size(); i++) { if (inshares->get(i)->getName() == inshareName) { baseNode = inshares->get(i)->copy(); break; } } delete inshares; } delete usersList; if (possep != string::npos && possep != (thepath.size() - 1) ) { rest = thepath.substr(possep+1); } } else if (!from) { baseNode = api->getNodeByHandle(cwd); rest = thepath; if (isrelative != NULL) { *isrelative = true; } } } return baseNode; } void MegaCmdExecuter::getPathParts(string path, deque *c) { size_t possep = path.find('/'); do { string curName = path.substr(0,possep); replaceAll(curName, "\\\\", "\\"); //unescape '\\' replaceAll(curName, "\\ ", " "); //unescape '\ ' c->push_back(curName); if (possep != string::npos && possep < (path.size()+1)) { path = path.substr(possep+1); } else { break; } possep = path.find('/'); if (possep == string::npos || !(possep < (path.size()+1))) { string curName = path.substr(0,possep); replaceAll(curName, "\\\\", "\\"); //unescape '\\' replaceAll(curName, "\\ ", " "); //unescape '\ ' c->push_back(curName); break; } } while (path.size()); } bool MegaCmdExecuter::decryptLinkIfEncrypted(MegaApi *api, std::string &publicLink, std::map *cloptions) { if (isEncryptedLink(publicLink)) { string linkPass = getOption(cloptions, "password", ""); if (!linkPass.size()) { linkPass = askforUserResponse("Enter password: "); } if (linkPass.size()) { std::unique_ptrmegaCmdListener = std::make_unique(nullptr); api->decryptPasswordProtectedLink(publicLink.c_str(), linkPass.c_str(), megaCmdListener.get()); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "decrypt password protected link")) { publicLink = megaCmdListener->getRequest()->getText(); } else { setCurrentThreadOutCode(MCMD_NOTPERMITTED); LOG_err << "Invalid password"; return false; } } else { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Need a password to decrypt provided link (--password=PASSWORD)"; return false; } } return true; } bool MegaCmdExecuter::checkAndInformPSA(CmdPetition *inf, bool enforce) { bool toret = false; m_time_t now = m_time(); if ( enforce || (now - sandboxCMD->timeOfPSACheck > 86400) ) { MegaUser *u = api->getMyUser(); if (!u) { LOG_debug << "No PSA request (not logged into an account)"; return toret; //Not logged in, no reason to get PSA } delete u; sandboxCMD->timeOfPSACheck = now; LOG_verbose << "Getting PSA"; MegaCmdListener *megaCmdListener = new MegaCmdListener(api, NULL); api->getPSA(megaCmdListener); megaCmdListener->wait(); if (megaCmdListener->getError()->getErrorCode() == MegaError::API_ENOENT) { LOG_verbose << "No new PSA available"; sandboxCMD->lastPSAnumreceived = max(0,sandboxCMD->lastPSAnumreceived); } else if(checkNoErrors(megaCmdListener->getError(), "get PSA")) { int number = static_cast(megaCmdListener->getRequest()->getNumber()); std::string name = megaCmdListener->getRequest()->getName(); std::string text = megaCmdListener->getRequest()->getText(); sandboxCMD->lastPSAnumreceived = number; LOG_debug << "Informing PSA #" << number << ": " << name; stringstream oss; oss << "<" << name << ">"; oss << text; string action = megaCmdListener->getRequest()->getPassword(); string link = megaCmdListener->getRequest()->getLink(); if (action.empty()) { action = "read more: "; } if (link.size()) { oss << endl << action << ": " << link; } oss << endl << " Execute \"psa --discard\" to stop seeing this message"; if (inf) { informStateListener(oss.str(), inf->clientID); } else { broadcastMessage(oss.str()); } toret = true; } delete megaCmdListener; } return toret; } std::string MegaCmdExecuter::formatErrorAndMaySetErrorCode(const MegaError &error) { auto code = error.getErrorCode(); if (code == MegaError::API_OK) { return std::string(); } setCurrentThreadOutCode(code); if (code == MegaError::API_EBLOCKED) { auto reason = sandboxCMD->getReasonblocked(); auto reasonStr = std::string("Account blocked."); if (!reason.empty()) { reasonStr.append("Reason: ").append(reason); } return reasonStr; } else if (code == MegaError::API_EPAYWALL || (code == MegaError::API_EOVERQUOTA && sandboxCMD->storageStatus == MegaApi::STORAGE_STATE_RED)) { return "Reached storage quota. You can change your account plan to increase your quota limit. See \"help --upgrade\" for further details"; } return error.getErrorString(); } bool MegaCmdExecuter::checkNoErrors(int errorCode, const string &message) { MegaErrorPrivate e(errorCode); return checkNoErrors(&e, message); } bool MegaCmdExecuter::checkNoErrors(MegaError *error, const string &message, SyncError syncError) { if (!error) { LOG_fatal << "No MegaError at request: " << message; assert(false); return false; } if (error->getErrorCode() == MegaError::API_OK) { if (syncError) { std::unique_ptr megaSyncErrorCode(MegaSync::getMegaSyncErrorCode(syncError)); LOG_info << "Able to " << message << ", but received syncError: " << megaSyncErrorCode.get(); } return true; } auto logErrMessage = std::string("Failed to ").append(message).append(": ").append(formatErrorAndMaySetErrorCode(*error)); if (syncError) { std::unique_ptr megaSyncErrorCode(MegaSync::getMegaSyncErrorCode(syncError)); logErrMessage.append(". ").append(megaSyncErrorCode.get()); } LOG_err << logErrMessage; return false; } bool MegaCmdExecuter::checkNoErrors(::mega::SynchronousRequestListener *listener, const std::string &message, ::mega::SyncError syncError) { assert(listener); listener->wait(); assert(listener->getError()); return checkNoErrors(listener->getError(), message, syncError); } /** * @brief MegaCmdExecuter::nodesbypath * returns nodes determined by path pattern * path naming conventions: * path is relative to cwd * /path is relative to ROOT * //in is in INBOX * //bin is in RUBBISH * X: is user X's INBOX * X:SHARE is share SHARE from user X * H:HANDLE is node with handle HANDLE * : and / filename components, as well as the \, must be escaped by \. * (correct UTF-8 encoding is assumed) * @param ptr * @param usepcre use PCRE expressions if available * @param user * @return List of std::unique_ptr. All are guaranteed to be non-null. * The list is empty if the path is malformed or not found. */ vector> MegaCmdExecuter::nodesbypath(const char* ptr, bool usepcre, string* user) { vector> nodesMatching; if (ptr && ptr[0] == 'H' && ptr[1] == ':') { MegaNode* n = api->getNodeByHandle(api->base64ToHandle(ptr+2)); if (n) { nodesMatching.emplace_back(n); return nodesMatching; } } string rest; std::unique_ptr baseNode(getBaseNode(ptr, rest)); if (baseNode) { if (rest.empty()) { nodesMatching.emplace_back(std::move(baseNode)); return nodesMatching; } deque c; getPathParts(rest, &c); if (c.empty()) { nodesMatching.emplace_back(std::move(baseNode)); } else { getNodesMatching(baseNode.get(), c, nodesMatching, usepcre); } } else if (!strncmp(ptr, "//from/", max(3, min(static_cast(strlen(ptr)-1), 7)))) //pattern trying to match inshares { unique_ptr inShares(api->getInSharesList()); if (inShares) { string matching = ptr; unescapeifRequired(matching); for (int i = 0; i < inShares->size(); i++) { std::unique_ptr n(api->getNodeByHandle(inShares->get(i)->getNodeHandle())); if (!n) continue; string tomatch = string("//from/") + inShares->get(i)->getUser() + ":" + n->getName(); if (patternMatches(tomatch.c_str(), matching.c_str(), false)) { nodesMatching.emplace_back(std::move(n)); } } } } return nodesMatching; } void MegaCmdExecuter::dumpNode(MegaNode* n, const char *timeFormat, std::map *clflags, std::map *cloptions, int extended_info, bool showversions, int depth, const char* title) { if (!title && !( title = n->getName())) { title = "CRYPTO_ERROR"; } if (depth) { for (int i = depth - 1; i--; ) { OUTSTREAM << "\t"; } } OUTSTREAM << title; if (getFlag(clflags, "show-handles")) { OUTSTREAM << " getHandle()) << ">"; } if (extended_info) { OUTSTREAM << " ("; switch (n->getType()) { case MegaNode::TYPE_FILE: OUTSTREAM << sizeToText(n->getSize(), false); if (INVALID_HANDLE != n->getPublicHandle()) // if (n->isExported()) { OUTSTREAM << ", shared as exported"; if (n->getExpirationTime()) { OUTSTREAM << " temporal"; } else { OUTSTREAM << " permanent"; } OUTSTREAM << " file link"; if (extended_info > 1) { char * publicLink = n->getPublicLink(); OUTSTREAM << ": " << publicLink; if (n->getExpirationTime()) { if (n->isExpired()) { OUTSTREAM << " expired at "; } else { OUTSTREAM << " expires at "; } OUTSTREAM << getReadableTime(n->getExpirationTime(), timeFormat); } if (n->getWritableLinkAuthKey()) { string authKey(n->getWritableLinkAuthKey()); if (authKey.size()) { OUTSTREAM << " AuthKey="<< authKey; } } delete []publicLink; } } break; case MegaNode::TYPE_FOLDER: { OUTSTREAM << "folder"; MegaShareList* outShares = api->getOutShares(n); if (outShares) { for (int i = 0; i < outShares->size(); i++) { auto outShare = outShares->get(i); if (outShare->getNodeHandle() == n->getHandle() && !outShare->isPending()/*shall be listed via getPendingOutShares*/) { OUTSTREAM << ", shared with " << outShare->getUser() << ", access " << getAccessLevelStr(outShare->getAccess()) << (outShare->isVerified() ? "" : "[UNVERIFIED]"); } } MegaShareList* pendingoutShares = api->getPendingOutShares(n); if (pendingoutShares) { for (int i = 0; i < pendingoutShares->size(); i++) { auto pendingoutShare = pendingoutShares->get(i); if (pendingoutShare->getNodeHandle() == n->getHandle()) { OUTSTREAM << ", shared (still pending)"; if (pendingoutShare->getUser()) { OUTSTREAM << " with " << pendingoutShare->getUser(); } OUTSTREAM << " access " << getAccessLevelStr(pendingoutShare->getAccess()) << (pendingoutShare->isVerified() ? "" : "[UNVERIFIED]"); } } delete pendingoutShares; } if (UNDEF != n->getPublicHandle()) { OUTSTREAM << ", shared as exported"; if (n->getExpirationTime()) { OUTSTREAM << " temporal"; } else { OUTSTREAM << " permanent"; } OUTSTREAM << " folder link"; if (extended_info > 1) { std::unique_ptr publicLink(n->getPublicLink()); OUTSTREAM << ": " << publicLink.get(); if (n->getWritableLinkAuthKey()) { constexpr const char *prefix = "https://mega.nz/folder/"; string authKey(n->getWritableLinkAuthKey()); string nodeLink(publicLink.get()); if (authKey.size() && nodeLink.rfind(prefix, 0) == 0) { string authToken = nodeLink.substr(strlen(prefix)).append(":").append(authKey); OUTSTREAM << " AuthToken="<< authToken; } } } } delete outShares; } if (n->isInShare()) { OUTSTREAM << ", inbound " << getAccessLevelStr(api->getAccess(n)) << " share"; } break; } case MegaNode::TYPE_ROOT: { OUTSTREAM << "root node"; break; } case MegaNode::TYPE_INCOMING: { OUTSTREAM << "inbox"; break; } case MegaNode::TYPE_RUBBISH: { OUTSTREAM << "rubbish"; break; } default: OUTSTREAM << "unsupported type: " << n->getType() <<" , please upgrade"; } OUTSTREAM << ")" << ( n->isRemoved() ? " (DELETED)" : "" ); } OUTSTREAM << endl; if (showversions && n->getType() == MegaNode::TYPE_FILE) { std::unique_ptr versionNodes(api->getVersions(n)); if (versionNodes) { for (int i = 0; i < versionNodes->size(); i++) { MegaNode *versionNode = versionNodes->get(i); if (versionNode->getHandle() != n->getHandle()) { string fullname(n->getName()?n->getName():"NO_NAME"); fullname += "#"; fullname += SSTR(versionNode->getModificationTime()); OUTSTREAM << " " << fullname; if (versionNode->getName() && !strcmp(versionNode->getName(),n->getName()) ) { OUTSTREAM << "[" << (versionNode->getName()?versionNode->getName():"NO_NAME") << "]"; } OUTSTREAM << " (" << getReadableTime(versionNode->getModificationTime(), timeFormat) << ")"; if (extended_info) { OUTSTREAM << " (" << sizeToText(versionNode->getSize(), false) << ")"; } if (getFlag(clflags, "show-handles")) { OUTSTREAM << " getHandle()) << ">"; } OUTSTREAM << endl; } } } } } // 12 is the length of "999000000000" i.e 999 GiB. static constexpr size_t MAX_SIZE_LEN = 12; static unsigned int DUMPNODE_SIZE_WIDTH = static_cast((MAX_SIZE_LEN > strlen("SIZE ")) ? MAX_SIZE_LEN : strlen("SIZE ")); void MegaCmdExecuter::dumpNodeSummaryHeader(const char *timeFormat, std::map *clflags, std::map *cloptions) { int datelength = int(getReadableTime(m_time(), timeFormat).size()); OUTSTREAM << "FLAGS"; OUTSTREAM << " "; OUTSTREAM << getFixLengthString("VERS", 4); OUTSTREAM << " "; OUTSTREAM << getFixLengthString("SIZE ", DUMPNODE_SIZE_WIDTH - 1 /*-1 to compensate FLAGS header*/, ' ', true); OUTSTREAM << " "; OUTSTREAM << getFixLengthString("DATE ", datelength+1, ' ', true); if (getFlag(clflags, "show-handles")) { OUTSTREAM << " "; OUTSTREAM << " HANDLE"; } OUTSTREAM << " "; OUTSTREAM << "NAME"; OUTSTREAM << endl; } void MegaCmdExecuter::dumpNodeSummary(MegaNode *n, const char *timeFormat, std::map *clflags, std::map *cloptions, bool humanreadable, const char *title) { if (!title && !( title = n->getName())) { title = "CRYPTO_ERROR"; } switch (n->getType()) { case MegaNode::TYPE_FILE: OUTSTREAM << "-"; break; case MegaNode::TYPE_FOLDER: OUTSTREAM << "d"; break; case MegaNode::TYPE_ROOT: OUTSTREAM << "r"; break; case MegaNode::TYPE_INCOMING: OUTSTREAM << "i"; break; case MegaNode::TYPE_RUBBISH: OUTSTREAM << "b"; break; default: OUTSTREAM << "x"; break; } if (UNDEF != n->getPublicHandle()) { OUTSTREAM << "e"; if (n->getExpirationTime()) { OUTSTREAM << "t"; } else { OUTSTREAM << "p"; } } else { OUTSTREAM << "--"; } if (n->isShared()) { OUTSTREAM << "s"; } else if (n->isInShare()) { OUTSTREAM << "i"; } else { OUTSTREAM << "-"; } OUTSTREAM << " "; if (n->isFile()) { MegaNodeList *versionNodes = api->getVersions(n); int nversions = versionNodes ? versionNodes->size() : 0; if (nversions > 999) { OUTSTREAM << getFixLengthString(">999", 4, ' ', true); } else { OUTSTREAM << getFixLengthString(SSTR(nversions), 4, ' ', true); } delete versionNodes; } else { OUTSTREAM << getFixLengthString("-", 4, ' ', true); } OUTSTREAM << " "; if (n->isFile()) { if (humanreadable) { OUTSTREAM << getFixLengthString(sizeToText(n->getSize()), DUMPNODE_SIZE_WIDTH, ' ', true); } else { OUTSTREAM << getFixLengthString(SSTR(n->getSize()), DUMPNODE_SIZE_WIDTH, ' ', true); } } else { OUTSTREAM << getFixLengthString( "-", (unsigned int)std::max(MAX_SIZE_LEN, strlen("SIZE ")), ' ', true); } if (n->isFile() && !getFlag(clflags, "show-creation-time")) { OUTSTREAM << " " << getReadableTime(n->getModificationTime(), timeFormat); } else { OUTSTREAM << " " << getReadableTime(n->getCreationTime(), timeFormat); } if (getFlag(clflags, "show-handles")) { OUTSTREAM << " H:" << handleToBase64(n->getHandle()); } OUTSTREAM << " " << title; OUTSTREAM << endl; } void MegaCmdExecuter::createOrModifyBackup(string local, string remote, string speriod, int numBackups) { LocalPath locallocal = LocalPath::fromAbsolutePath(local); std::unique_ptr fa = mFsAccessCMD->newfileaccess(); if (!fa->isfolder(locallocal)) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Local path must be an existing folder: " << local; return; } int64_t period = -1; if (!speriod.size()) { MegaScheduledCopy *backup = api->getScheduledCopyByPath(local.c_str()); if (!backup) { backup = api->getScheduledCopyByTag(toInteger(local, -1)); } if (backup) { speriod = backup->getPeriodString(); if (!speriod.size()) { period = backup->getPeriod(); } delete backup; } else { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("backup"); return; } } if (speriod.find(" ") == string::npos && period == -1) { period = 10 * getTimeStampAfter(0,speriod); speriod = ""; } if (numBackups == -1) { MegaScheduledCopy *backup = api->getScheduledCopyByPath(local.c_str()); if (!backup) { backup = api->getScheduledCopyByTag(toInteger(local, -1)); } if (backup) { numBackups = backup->getMaxBackups(); delete backup; } } if (numBackups == -1) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("backup"); return; } std::unique_ptr n; if (remote.size()) { n = nodebypath(remote.c_str()); } else { std::unique_ptr backup(api->getScheduledCopyByPath(local.c_str())); if (!backup) { backup.reset(api->getScheduledCopyByTag(toInteger(local, -1))); } if (backup) { n.reset(api->getNodeByHandle(backup->getMegaHandle())); } } if (n) { if (n->getType() != MegaNode::TYPE_FOLDER) { setCurrentThreadOutCode(MCMD_INVALIDTYPE); LOG_err << remote << " must be a valid folder"; } else { if (establishBackup(local, n.get(), period, speriod, numBackups)) { { std::lock_guard g(mtxBackupsMap); ConfigurationManager::saveBackups(&ConfigurationManager::configuredBackups); } OUTSTREAM << "Backup established: " << local << " into " << remote << " period=" << ((period != -1)?getReadablePeriod(period/10):"\""+speriod+"\"") << " Number-of-Backups=" << numBackups << endl; } } } else { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << remote << " not found"; } } void MegaCmdExecuter::printTreeSuffix(int depth, vector &lastleaf) { #ifdef _WIN32 const wchar_t *c0 = L" "; const wchar_t *c1 = L"\u2502"; const wchar_t *c2 = L"\u2514"; const wchar_t *c3 = L"\u251c"; const wchar_t *c4 = L"\u2500\u2500"; #else const char *c0 = " "; const char *c1 = "\u2502"; const char *c2 = "\u2514"; const char *c3 = "\u251c"; const char *c4 = "\u2500\u2500"; #endif for (int i = 0; i < depth-1; i++) { OUTSTREAM << (lastleaf.at(i)?c0:c1) << " "; } if (lastleaf.size()) { OUTSTREAM << (lastleaf.back()?c2:c3) << c4 << " "; } } void MegaCmdExecuter::dumptree(MegaNode* n, bool treelike, vector &lastleaf, const char *timeFormat, std::map *clflags, std::map *cloptions, int recurse, int extended_info, bool showversions, int depth, string pathRelativeTo) { if (depth || ( n->getType() == MegaNode::TYPE_FILE )) { if (treelike) printTreeSuffix(depth, lastleaf); if (pathRelativeTo != "NULL") { if (!n->getName()) { dumpNode(n, timeFormat, clflags, cloptions, extended_info, showversions, treelike?0:depth, "CRYPTO_ERROR"); } else { char * nodepath = api->getNodePath(n); char *pathToShow = NULL; if (pathRelativeTo != "") { pathToShow = strstr(nodepath, pathRelativeTo.c_str()); } if (pathToShow == nodepath) //found at beginning { pathToShow += pathRelativeTo.size(); if (( *pathToShow == '/' ) && ( pathRelativeTo != "/" )) { pathToShow++; } } else { pathToShow = nodepath; } dumpNode(n, timeFormat, clflags, cloptions, extended_info, showversions, treelike?0:depth, pathToShow); delete []nodepath; } } else { dumpNode(n, timeFormat, clflags, cloptions, extended_info, showversions, treelike?0:depth); } if (!recurse && depth) { return; } } if (n->getType() != MegaNode::TYPE_FILE) { MegaNodeList* children = api->getChildren(n); if (children) { for (int i = 0; i < children->size(); i++) { vector lfs = lastleaf; lfs.push_back(i==(children->size()-1)); dumptree(children->get(i), treelike, lfs, timeFormat, clflags, cloptions, recurse, extended_info, showversions, depth + 1); } delete children; } } } void MegaCmdExecuter::dumpTreeSummary(MegaNode *n, const char *timeFormat, std::map *clflags, std::map *cloptions, int recurse, bool show_versions, int depth, bool humanreadable, string pathRelativeTo) { char * nodepath = api->getNodePath(n); string scryptoerror = "CRYPTO_ERROR"; char *pathToShow = NULL; if (pathRelativeTo != "") { pathToShow = strstr(nodepath, pathRelativeTo.c_str()); } if (pathToShow == nodepath) //found at beginning { pathToShow += pathRelativeTo.size(); if (( *pathToShow == '/' ) && ( pathRelativeTo != "/" )) { pathToShow++; } } else { pathToShow = nodepath; } if (!pathToShow && !( pathToShow = (char *)n->getName())) { pathToShow = (char *)scryptoerror.c_str(); } if (n->getType() != MegaNode::TYPE_FILE) { MegaNodeList* children = api->getChildren(n); if (children) { if (depth) { OUTSTREAM << endl; } if (recurse) { OUTSTREAM << pathToShow << ":" << endl; } for (int i = 0; i < children->size(); i++) { dumpNodeSummary(children->get(i), timeFormat, clflags, cloptions, humanreadable); } if (show_versions) { for (int i = 0; i < children->size(); i++) { MegaNode *c = children->get(i); MegaNodeList *vers = api->getVersions(c); if (vers && vers->size() > 1) { OUTSTREAM << endl << "Versions of " << pathToShow << "/" << c->getName() << ":" << endl; for (int i = 0; i < vers->size(); i++) { dumpNodeSummary(vers->get(i), timeFormat, clflags, cloptions, humanreadable); } } delete vers; } } if (recurse) { for (int i = 0; i < children->size(); i++) { MegaNode *c = children->get(i); dumpTreeSummary(c, timeFormat, clflags, cloptions, recurse, show_versions, depth + 1, humanreadable); } } delete children; } } else // file { if (!depth) { dumpNodeSummary(n, timeFormat, clflags, cloptions, humanreadable); if (show_versions) { MegaNodeList *vers = api->getVersions(n); if (vers && vers->size() > 1) { OUTSTREAM << endl << "Versions of " << pathToShow << ":" << endl; for (int i = 0; i < vers->size(); i++) { string nametoshow = n->getName()+string("#")+SSTR(vers->get(i)->getModificationTime()); dumpNodeSummary(vers->get(i), timeFormat, clflags, cloptions, humanreadable, nametoshow.c_str()); } } delete vers; } } } delete []nodepath; } /** * @brief Tests if a path can be created * @param path * @return */ bool MegaCmdExecuter::TestCanWriteOnContainingFolder(string path) { #ifdef _WIN32 replaceAll(path, "/", "\\"); #endif auto containingFolder = LocalPath::fromAbsolutePath(path); if (!containingFolder.isRootPath()) { containingFolder = containingFolder.parentPath(); } std::unique_ptr fa = mFsAccessCMD->newfileaccess(); if (!fa->isfolder(containingFolder)) { setCurrentThreadOutCode(MCMD_INVALIDTYPE); LOG_err << containingFolder.toPath(false) << " is not a valid Download Folder"; return false; } if (!canWrite(containingFolder.toPath(false))) { setCurrentThreadOutCode(MCMD_NOTPERMITTED); LOG_err << "Write not allowed in " << containingFolder.toPath(false); return false; } return true; } std::unique_ptr MegaCmdExecuter::getPcrByContact(string contactEmail) { unique_ptr icrl(api->getIncomingContactRequests()); if (icrl) { for (int i = 0; i < icrl->size(); i++) { if (icrl->get(i)->getSourceEmail() == contactEmail) { return std::unique_ptr(icrl->get(i)->copy()); } } } return NULL; } string MegaCmdExecuter::getDisplayPath(string givenPath, MegaNode* n_param) { char * pathToNode = api->getNodePath(n_param); if (!pathToNode) { LOG_err << " GetNodePath failed for: " << givenPath; return givenPath; } char * pathToShow = pathToNode; string pathRelativeTo = "NULL"; string cwpath = getCurrentPath(); string toret=""; if (givenPath.find('/') == 0 ) { pathRelativeTo = ""; } else if(givenPath.find("../") == 0 || givenPath.find("./") == 0 ) { pathRelativeTo = ""; MegaNode *n = api->getNodeByHandle(cwd); while(true) { if(givenPath.find("./") == 0) { givenPath=givenPath.substr(2); toret+="./"; if (n) { char *npath = api->getNodePath(n); pathRelativeTo = string(npath); delete []npath; } return toret; } else if(givenPath.find("../") == 0) { givenPath=givenPath.substr(3); toret+="../"; MegaNode *aux = n; if (n) { n=api->getNodeByHandle(n->getParentHandle()); } delete aux; if (n) { char *npath = api->getNodePath(n); pathRelativeTo = string(npath); delete []npath; } } else { break; } } delete n; } else { if (cwpath == "/") //TODO: //bin /X:share ... { pathRelativeTo = cwpath; } else { pathRelativeTo = cwpath + "/"; } } if (( "" == givenPath ) && !strcmp(pathToNode, cwpath.c_str())) { assert(strlen(pathToNode)>0); pathToNode[0] = '.'; pathToNode[1] = '\0'; } if (pathRelativeTo != "") { pathToShow = strstr(pathToNode, pathRelativeTo.c_str()); } if (pathToShow == pathToNode) //found at beginning { if (strcmp(pathToNode, "/")) { pathToShow += pathRelativeTo.size(); } } else { pathToShow = pathToNode; } toret+=pathToShow; delete []pathToNode; return toret; } int MegaCmdExecuter::dumpListOfExported(MegaNode* n_param, const char *timeFormat, std::map *clflags, std::map *cloptions, string givenPath) { int toret = 0; vector listOfExported; processTree(n_param, includeIfIsExported, (void*)&listOfExported); for (std::vector< MegaNode * >::iterator it = listOfExported.begin(); it != listOfExported.end(); ++it) { MegaNode * n = *it; if (n) { string pathToShow = getDisplayPath(givenPath, n); dumpNode(n, timeFormat, clflags, cloptions, 2, 1, false, pathToShow.c_str()); delete n; } } toret = int(listOfExported.size()); listOfExported.clear(); return toret; } void printOutShareInfo(const char *pathOrName, const char *email, int accessLevel, bool pending, bool verified) { OUTSTREAM << pathOrName; OUTSTREAM << ","; if (email) { OUTSTREAM << " shared"; } if (pending) { OUTSTREAM << " (still pending)"; } if (email) { OUTSTREAM << " with " << email; } OUTSTREAM << " (" << getAccessLevelStr(accessLevel) << ")"; if (!verified && !pending /*Do not indicate lack of verification for pending out shares*/) { OUTSTREAM << "[UNVERIFIED]"; } OUTSTREAM << endl; } /** * @brief listnodeshares For a node, it prints all the shares it has * @param n * @param name */ void MegaCmdExecuter::listnodeshares(MegaNode* n, string name, bool listPending = false, bool onlyPending = false) { assert(listPending || !onlyPending); auto printOutShares = [n, CONST_CAPTURE(name)](MegaShareList * outShares, bool skipPending = false) { if (outShares) { for (int i = 0; i < outShares->size(); i++) { auto outShare = outShares->get(i); if (outShare && (!skipPending || !outShare->isPending())) { printOutShareInfo(name.empty() ? n->getName() : name.c_str(), outShare->getUser(), outShare->getAccess(), outShare->isPending(), outShare->isVerified()); } } } }; if (!onlyPending) { std::unique_ptr allOutShares (api->getOutShares(n)); printOutShares(allOutShares.get(), true /*to not list them twice*/); } if (listPending) { std::unique_ptr pendingOutShares (api->getPendingOutShares(n)); printOutShares(pendingOutShares.get()); } } void MegaCmdExecuter::dumpListOfShared(MegaNode* n_param, string givenPath) { vector listOfShared; processTree(n_param, includeIfIsShared, (void*)&listOfShared); for (std::vector< MegaNode * >::iterator it = listOfShared.begin(); it != listOfShared.end(); ++it) { MegaNode * n = *it; if (n) { string pathToShow = getDisplayPath(givenPath, n); listnodeshares(n, pathToShow); delete n; } } listOfShared.clear(); } //includes pending and normal shares void MegaCmdExecuter::dumpListOfAllShared(MegaNode* n_param, string givenPath) { vector listOfShared; processTree(n_param, includeIfIsSharedOrPendingOutShare, (void*)&listOfShared); for (std::vector< MegaNode * >::iterator it = listOfShared.begin(); it != listOfShared.end(); ++it) { MegaNode * n = *it; if (n) { string pathToShow = getDisplayPath(givenPath, n); listnodeshares(n, pathToShow, true); delete n; } } listOfShared.clear(); } void MegaCmdExecuter::dumpListOfPendingShares(MegaNode* n_param, string givenPath) { vector listOfShared; processTree(n_param, includeIfIsPendingOutShare, (void*)&listOfShared); for (std::vector< MegaNode * >::iterator it = listOfShared.begin(); it != listOfShared.end(); ++it) { MegaNode * n = *it; if (n) { string pathToShow = getDisplayPath(givenPath, n); listnodeshares(n, pathToShow, true, true); delete n; } } listOfShared.clear(); } void MegaCmdExecuter::loginWithPassword(const char *password) { MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); sandboxCMD->resetSandBox(); api->login(login.c_str(), password, megaCmdListener); actUponLogin(megaCmdListener); delete megaCmdListener; } void MegaCmdExecuter::changePassword(const char *newpassword, string pin2fa) { MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); api->changePassword(NULL, newpassword, megaCmdListener); megaCmdListener->wait(); if (megaCmdListener->getError()->getErrorCode() == MegaError::API_EMFAREQUIRED) { MegaCmdListener *megaCmdListener2 = new MegaCmdListener(NULL); if (!pin2fa.size()) { pin2fa = askforUserResponse("Enter the code generated by your authentication app: "); } LOG_verbose << " Using confirmation pin: " << pin2fa; api->multiFactorAuthChangePassword(NULL, newpassword, pin2fa.c_str(), megaCmdListener2); megaCmdListener2->wait(); if (megaCmdListener2->getError()->getErrorCode() == MegaError::API_EFAILED) { setCurrentThreadOutCode(megaCmdListener2->getError()->getErrorCode()); LOG_err << "Password unchanged: invalid authentication code"; } else if (checkNoErrors(megaCmdListener2->getError(), "change password with auth code")) { OUTSTREAM << "Password changed successfully" << endl; } delete megaCmdListener2; } else if (!checkNoErrors(megaCmdListener->getError(), "change password")) { LOG_err << "Please, ensure you enter the old password correctly"; } else { OUTSTREAM << "Password changed successfully" << endl; } delete megaCmdListener; } void str_localtime(char s[32], ::mega::m_time_t t) { struct tm tms; strftime(s, 32, "%c", m_localtime(t, &tms)); } void MegaCmdExecuter::actUponGetExtendedAccountDetails(std::unique_ptr<::mega::MegaAccountDetails> storageDetails, std::unique_ptr<::mega::MegaAccountDetails> extAccountDetails) { char timebuf[32], timebuf2[32]; LOG_verbose << "actUponGetExtendedAccountDetails ok"; if (storageDetails) { OUTSTREAM << " Available storage: " << getFixLengthString(sizeToText(storageDetails->getStorageMax()), 9, ' ', true) << "ytes" << endl; std::unique_ptr n(api->getRootNode()); if (n) { OUTSTREAM << " In ROOT: " << getFixLengthString(sizeToText(storageDetails->getStorageUsed(n->getHandle())), 9, ' ', true) << "ytes in " << getFixLengthString(SSTR(storageDetails->getNumFiles(n->getHandle())), 5, ' ', true) << " file(s) and " << getFixLengthString(SSTR(storageDetails->getNumFolders(n->getHandle())), 5, ' ', true) << " folder(s)" << endl; } n = std::unique_ptr(api->getVaultNode()); if (n) { OUTSTREAM << " In INBOX: " << getFixLengthString(sizeToText(storageDetails->getStorageUsed(n->getHandle())), 9, ' ', true) << "ytes in " << getFixLengthString(SSTR(storageDetails->getNumFiles(n->getHandle())), 5, ' ', true) << " file(s) and " << getFixLengthString(SSTR(storageDetails->getNumFolders(n->getHandle())), 5, ' ', true) << " folder(s)" << endl; } n = std::unique_ptr(api->getRubbishNode()); if (n) { OUTSTREAM << " In RUBBISH: " << getFixLengthString(sizeToText(storageDetails->getStorageUsed(n->getHandle())), 9, ' ', true) << "ytes in " << getFixLengthString(SSTR(storageDetails->getNumFiles(n->getHandle())), 5, ' ', true) << " file(s) and " << getFixLengthString(SSTR(storageDetails->getNumFolders(n->getHandle())), 5, ' ', true) << " folder(s)" << endl; } long long usedinVersions = storageDetails->getVersionStorageUsed(); OUTSTREAM << " Total size taken up by file versions: " << getFixLengthString(sizeToText(usedinVersions), 12, ' ', true) << "ytes" << endl; std::unique_ptr inshares(api->getInShares()); if (inshares) { for (int i = 0; i < inshares->size(); i++) { auto node = inshares->get(i); OUTSTREAM << " In INSHARE " << node->getName() << ": " << getFixLengthString(sizeToText(storageDetails->getStorageUsed(node->getHandle())), 9, ' ', true) << "ytes in " << getFixLengthString(SSTR(storageDetails->getNumFiles(node->getHandle())), 5, ' ', true) << " file(s) and " << getFixLengthString(SSTR(storageDetails->getNumFolders(node->getHandle())), 5, ' ', true) << " folder(s)" << endl; } } OUTSTREAM << " Pro level: " << storageDetails->getProLevel() << endl; if (storageDetails->getProLevel()) { if (storageDetails->getProExpiration()) { str_localtime(timebuf, storageDetails->getProExpiration()); OUTSTREAM << " " << "Pro expiration date: " << timebuf << endl; } } } if (extAccountDetails) { std::unique_ptr subscriptionMethod(extAccountDetails->getSubscriptionMethod()); OUTSTREAM << " Subscription type: " << subscriptionMethod.get() << endl; OUTSTREAM << " Account balance:" << endl; for (int i = 0; i < extAccountDetails->getNumBalances(); i++) { std::unique_ptr balance(extAccountDetails->getBalance(i)); std::unique_ptr currency(balance->getCurrency()); char sbalance[50]; sprintf(sbalance, " Balance: %.3s %.02f", currency.get(), balance->getAmount()); OUTSTREAM << " " << "Balance: " << sbalance << endl; } if (extAccountDetails->getNumPurchases()) { OUTSTREAM << "Purchase history:" << endl; for (int i = 0; i < extAccountDetails->getNumPurchases(); i++) { std::unique_ptr purchase(extAccountDetails->getPurchase(i)); std::unique_ptr currency(purchase->getCurrency()); std::unique_ptr handle(purchase->getHandle()); char spurchase[150]; str_localtime(timebuf, purchase->getTimestamp()); sprintf(spurchase, "ID: %.11s Time: %s Amount: %.3s %.02f Payment method: %d\n", handle.get(), timebuf, currency.get(), purchase->getAmount(), purchase->getMethod()); OUTSTREAM << " " << spurchase << endl; } } if (extAccountDetails->getNumTransactions()) { OUTSTREAM << "Transaction history:" << endl; for (int i = 0; i < extAccountDetails->getNumTransactions(); i++) { std::unique_ptr transaction(extAccountDetails->getTransaction(i)); std::unique_ptr currency(transaction->getCurrency()); char stransaction[100]; str_localtime(timebuf, transaction->getTimestamp()); sprintf(stransaction, "ID: %.11s Time: %s Amount: %.3s %.02f\n", transaction->getHandle(), timebuf, currency.get(), transaction->getAmount()); OUTSTREAM << " " << stransaction << endl; } } int alive_sessions = 0; OUTSTREAM << "Current Active Sessions:" << endl; char sdetails[500]; for (int i = 0; i < extAccountDetails->getNumSessions(); i++) { std::unique_ptr session(extAccountDetails->getSession(i)); if (session->isAlive()) { str_localtime(timebuf, session->getCreationTimestamp()); str_localtime(timebuf2, session->getMostRecentUsage()); std::unique_ptr sid(api->userHandleToBase64(session->getHandle())); const char *current_session = ""; if (session->isCurrent()) { current_session = " * Current Session\n"; } std::unique_ptr userAgent(session->getUserAgent()); std::unique_ptr country(session->getCountry()); std::unique_ptr ip(session->getIP()); sprintf(sdetails, "%s Session ID: %s\n Session start: %s\n Most recent activity: %s\n IP: %s\n Country: %.2s\n User-Agent: %s\n -----\n", current_session, sid.get(), timebuf, timebuf2, ip.get(), country.get(), userAgent.get()); OUTSTREAM << sdetails; alive_sessions++; } } if (alive_sessions) { OUTSTREAM << alive_sessions << " active sessions opened" << endl; } } } void MegaCmdExecuter::verifySharedFolders(MegaApi *api) { auto getInstructionsNeedsVerification = [](const char *intro) { std::stringstream ss; ss << intro << ".\n"; ss << "You need to verify your contacts. \nUse "; ss << getCommandPrefixBasedOnMode(); ss << "users --help-verify to get instructions"; return ss.str(); }; auto getInstructionsNeedsLogin = [](const char *intro, const std::set &emails) { std::stringstream ss; ss << intro << "."; for (auto &email : emails) { ss << "\nYour contact " << email << " may need to resume using his MEGA application."; } return ss.str(); }; { std::unique_ptr shares(api->getUnverifiedInShares()); if (shares && shares->size()) { broadcastMessage(getInstructionsNeedsVerification("Some not verified contact is sharing a folder with you"), true); return; } std::set usersWithNonDecryptable; std::unique_ptr inSharesByAllUsers (api->getInSharesList());// this one should not return unverified ones if (inSharesByAllUsers) { for (int i = 0, total = inSharesByAllUsers->size(); i < total; i++) { auto share = inSharesByAllUsers->get(i); if (!share->isVerified()) // Just in case { broadcastMessage(getInstructionsNeedsVerification("Found some not verified share"), true); return; } std::unique_ptr n (api->getNodeByHandle(share->getNodeHandle())); if (n && !n->isNodeKeyDecrypted() && share->getUser()) { usersWithNonDecryptable.insert(share->getUser()); } } } if (!usersWithNonDecryptable.empty()) { std::string title("Found some inaccessible (in)share"); if (usersWithNonDecryptable.size() > 1) { title.append("s"); } broadcastMessage(getInstructionsNeedsLogin(title.c_str(), usersWithNonDecryptable), true); return; } } { std::unique_ptr shares(api->getUnverifiedOutShares()); for (int i = 0; shares && i < shares->size(); i++) { if (!shares->get(i)->isPending()) { broadcastMessage(getInstructionsNeedsVerification("You are sharing a folder with some unverified contact"), true); return; } } } } bool MegaCmdExecuter::actUponFetchNodes(MegaApi *api, SynchronousRequestListener *srl, int timeout) { if (timeout == -1) { srl->wait(); } else { int trywaitout = srl->trywait(timeout); if (trywaitout) { LOG_err << "Fetch nodes took too long, it may have failed. No further actions performed"; return false; } } if (srl->getError()->getErrorCode() == MegaError::API_EBLOCKED) { LOG_verbose << " EBLOCKED after fetch nodes. querying for reason..."; MegaCmdListener *megaCmdListener = new MegaCmdListener(api, NULL); api->whyAmIBlocked(megaCmdListener); //This shall cause event that sets reasonblocked megaCmdListener->wait(); auto reason = sandboxCMD->getReasonblocked(); LOG_warn << "Failed to fetch nodes. Account blocked." <<( reason.empty()?"":" Reason: "+reason); } else if (checkNoErrors(srl->getError(), "fetch nodes")) { // Let's save the session for future resumptions // Note: when logged into folders, // folder session depends on node handle, // which requires fetch nodes to be complete // i.e. dumpSession won't be valid after login session = std::unique_ptr(srl->getApi()->dumpSession()); ConfigurationManager::saveSession(session.get()); LOG_verbose << "ActUponFetchNodes ok. Let's wait for nodes current:"; auto futureNodesCurrent = sandboxCMD->mNodesCurrentPromise.getFuture(); bool discardGet = false; LOG_debug << "Waiting for nodes current ..."; if (futureNodesCurrent.wait_for(std::chrono::seconds(30)) == std::future_status::timeout) { LOG_warn << "Getting up to date with last changes in your account is taking long ..."; if (futureNodesCurrent.wait_for(std::chrono::seconds(120)) == std::future_status::timeout) { LOG_err << "Getting up to date with last changes in your account is taking more than expected. MEGAcmd will continue. " "Caveat: you may be interacting with an out-dated version of your account."; sendEvent(StatsManager::MegacmdEvent::WAITED_TOO_LONG_FOR_NODES_CURRENT, api, false); discardGet = true; } } if (!discardGet) { auto eventCurrentArrivedOk = futureNodesCurrent.get(); assert(eventCurrentArrivedOk); LOG_debug << "Waited for nodes current ... " << eventCurrentArrivedOk; } std::string sessionString(session ? session.get() : ""); if (!sessionString.empty()) { // Verify shared folders to brodcast caveat messages if required mDeferredSharedFoldersVerifier.triggerDeferredSingleShot([this, api] { verifySharedFolders(api); }); } return true; } else { sandboxCMD->mNodesCurrentPromise.reset(); } return false; } int MegaCmdExecuter::actUponLogin(SynchronousRequestListener *srl, int timeout) { if (timeout == -1) { srl->wait(); } else { int trywaitout = srl->trywait(timeout); if (trywaitout) { LOG_err << "Login took too long, it may have failed. No further actions performed"; return MegaError::API_EAGAIN; } } LOG_debug << "actUponLogin login"; setCurrentThreadOutCode(srl->getError()->getErrorCode()); if (srl->getRequest()->getEmail()) { LOG_debug << "actUponLogin login email: " << srl->getRequest()->getEmail(); } if (srl->getError()->getErrorCode() == MegaError::API_EMFAREQUIRED) // failed to login { return srl->getError()->getErrorCode(); } if (srl->getError()->getErrorCode() == MegaError::API_ENOENT) // failed to login { LOG_err << "Login failed: invalid email or password"; } else if (srl->getError()->getErrorCode() == MegaError::API_EFAILED) { LOG_err << "Login failed: incorrect authentication"; } else if (srl->getError()->getErrorCode() == MegaError::API_EINCOMPLETE) { LOG_err << "Login failed: unconfirmed account. Please confirm your account"; } else if (checkNoErrors(srl->getError(), "Login")) //login success: { LOG_debug << "Login correct ... " << (srl->getRequest()->getEmail()?srl->getRequest()->getEmail():""); if (srl->getRequest()->getEmail()) // login with email { // If login with email, here we will have a valid new session // otherwise, we can asume that session is already stored. // // Note, if logging into a folder (no email), // the dumpSession reported by the SDK at this point is not valid: // folder session depends on node handle, which requires fetching nodes session = std::unique_ptr(srl->getApi()->dumpSession()); ConfigurationManager::saveSession(session.get()); } /* Restoring configured values */ mtxSyncMap.lock(); ConfigurationManager::loadsyncs(); mtxSyncMap.unlock(); mtxBackupsMap.lock(); ConfigurationManager::loadbackups(); mtxBackupsMap.unlock(); ConfigurationManager::transitionLegacyExclusionRules(*api); long long maxspeeddownload = ConfigurationManager::getConfigurationValue("maxspeeddownload", -1); if (maxspeeddownload != -1) api->setMaxDownloadSpeed(maxspeeddownload); long long maxspeedupload = ConfigurationManager::getConfigurationValue("maxspeedupload", -1); if (maxspeedupload != -1) api->setMaxUploadSpeed(maxspeedupload); for (bool up :{true, false}) { auto megaCmdListener = std::make_unique(nullptr); auto value = ConfigurationManager::getConfigurationValue(up ? "maxuploadconnections" : "maxdownloadconnections", -1); if (value != -1) { api->setMaxConnections(up ? 1 : 0, value, megaCmdListener.get()); megaCmdListener->wait(); if (megaCmdListener->getError()->getErrorCode() != MegaError::API_OK) { LOG_err << "Failed to change max " << (up ? "upload" : "download") << " connections: " << megaCmdListener->getError()->getErrorString(); } } } for (auto &vc : Instance::Get().getConfigurators()) { auto name = vc.mKey.c_str(); auto newValueOpt = vc.mGetter(api, name); if (newValueOpt) { if (vc.mValidator && !vc.mValidator.value()(newValueOpt->data())) { LOG_err << "Failed to change " << vc.mKey << " (" << vc.mDescription << ") after login. Invalid value in configuration"; continue; } auto previousErrorCode = getCurrentThreadOutCode(); if (!vc.mSetter(api, std::string(name), newValueOpt->data())) { LOG_err << "Failed to change " << vc.mKey << " (" << vc.mDescription << ") after login. Setting failed"; setCurrentThreadOutCode(previousErrorCode); //Do not consider this a a failure on login ... } } } api->useHttpsOnly(ConfigurationManager::getConfigurationValue("https", false)); api->disableGfxFeatures(!ConfigurationManager::getConfigurationValue("graphics", true)); #ifndef _WIN32 string permissionsFiles = ConfigurationManager::getConfigurationSValue("permissionsFiles"); if (permissionsFiles.size()) { int perms = permissionsFromReadable(permissionsFiles); if (perms != -1) { api->setDefaultFilePermissions(perms); } } string permissionsFolders = ConfigurationManager::getConfigurationSValue("permissionsFolders"); if (permissionsFolders.size()) { int perms = permissionsFromReadable(permissionsFolders); if (perms != -1) { api->setDefaultFolderPermissions(perms); } } #endif ConfigurationManager::migrateSyncConfig(api); LOG_info << "Fetching nodes ... "; MegaApi *api = srl->getApi(); int clientID = static_cast(srl)->clientID; fetchNodes(api, clientID); } #if defined(_WIN32) || defined(__APPLE__) MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); srl->getApi()->getLastAvailableVersion("BdARkQSQ",megaCmdListener); megaCmdListener->wait(); if (!megaCmdListener->getError()) { LOG_fatal << "No MegaError at getLastAvailableVersion: "; } else if (megaCmdListener->getError()->getErrorCode() != MegaError::API_OK) { LOG_debug << "Couldn't get latests available version: " << megaCmdListener->getError()->getErrorString(); } else { if (megaCmdListener->getRequest()->getNumber() != MEGACMD_CODE_VERSION) { OUTSTREAM << "---------------------------------------------------------------------" << endl; OUTSTREAM << "-- There is a new version available of megacmd: " << getLeftAlignedStr(megaCmdListener->getRequest()->getName(),12) << "--" << endl; OUTSTREAM << "-- Please, update this one: See \"update --help\". --" << endl; OUTSTREAM << "-- Or download the latest from https://mega.nz/cmd --" << endl; OUTSTREAM << "---------------------------------------------------------------------" << endl; } } delete megaCmdListener; //this goes here in case server is launched directly and thus we ensure that's shown at the beginning int autoupdate = ConfigurationManager::getConfigurationValue("autoupdate", -1); bool enabledupdaterhere = false; if (autoupdate == -1 || autoupdate == 2) { OUTSTREAM << "ENABLING AUTOUPDATE BY DEFAULT. You can disable it with \"update --auto=off\"" << endl; autoupdate = 1; enabledupdaterhere = true; } if (autoupdate >= 1) { startcheckingForUpdates(); } if (enabledupdaterhere) { ConfigurationManager::savePropertyValue("autoupdate", 2); //save to special value to indicate first listener that it is enabled } #endif return srl->getError()->getErrorCode(); } void MegaCmdExecuter::fetchNodes(MegaApi *api, int clientID) { if (!api) api = this->api; sandboxCMD->mNodesCurrentPromise.initiatePromise(); auto megaCmdListener = std::make_unique(api, nullptr, clientID); api->fetchNodes(megaCmdListener.get()); if (!actUponFetchNodes(api, megaCmdListener.get())) { //Ideally we should enter an state that indicates that we are not fully logged. //Specially when the account is blocked return; } // This is the actual acting upon fetch nodes ended correctly: //automatic now: //api->enableTransferResumption(); auto cwdNode = (cwd == UNDEF) ? nullptr : std::unique_ptr(api->getNodeByHandle(cwd)); if (cwd == UNDEF || !cwdNode) { auto rootNode = std::unique_ptr(api->getRootNode()); if (rootNode) { cwd = rootNode->getHandle(); } else { LOG_err << "Root node was not found after fetching nodes"; sendEvent(StatsManager::MegacmdEvent::ROOT_NODE_NOT_FOUND_AFTER_FETCHING, api); } } setloginInAtStartup(false); //to enable all commands before giving clients the green light! informStateListeners("loged:"); // tell the clients login ended, before providing them the first prompt updateprompt(api); LOG_debug << " Fetch nodes correctly"; MegaUser *u = api->getMyUser(); if (u) { LOG_info << "Login complete as " << u->getEmail(); delete u; } if (ConfigurationManager::getConfigurationValue("ask4storage", true)) { ConfigurationManager::savePropertyValue("ask4storage",false); auto megaCmdListener = std::make_unique(nullptr); api->getAccountDetails(megaCmdListener.get()); megaCmdListener->wait(); // we don't call getAccountDetails on startup always: we ask on first login (no "ask4storage") or previous state was STATE_RED | STATE_ORANGE // if we were green, don't need to ask: if there are changes they will be received via action packet indicating STATE_CHANGE } checkAndInformPSA(NULL); // this needs broacasting in case there's another Shell running. // no need to enforce, because time since last check should has been restored mtxBackupsMap.lock(); if (ConfigurationManager::configuredBackups.size()) { LOG_info << "Restablishing backups ... "; unsigned int i=0; for (map::iterator itr = ConfigurationManager::configuredBackups.begin(); itr != ConfigurationManager::configuredBackups.end(); ++itr, i++) { backup_struct *thebackup = itr->second; MegaNode * node = api->getNodeByHandle(thebackup->handle); if (establishBackup(thebackup->localpath, node, thebackup->period, thebackup->speriod, thebackup->numBackups)) { thebackup->failed = false; const char *nodepath = api->getNodePath(node); LOG_debug << "Successfully resumed backup: " << thebackup->localpath << " to " << nodepath; delete []nodepath; } else { thebackup->failed = true; char *nodepath = api->getNodePath(node); LOG_err << "Failed to resume backup: " << thebackup->localpath << " to " << nodepath; delete []nodepath; } delete node; } ConfigurationManager::saveBackups(&ConfigurationManager::configuredBackups); } mtxBackupsMap.unlock(); #ifdef HAVE_LIBUV // restart webdav int port = ConfigurationManager::getConfigurationValue("webdav_port", -1); if (port != -1) { bool localonly = ConfigurationManager::getConfigurationValue("webdav_localonly", -1); bool tls = ConfigurationManager::getConfigurationValue("webdav_tls", false); string pathtocert, pathtokey; pathtocert = ConfigurationManager::getConfigurationSValue("webdav_cert"); pathtokey = ConfigurationManager::getConfigurationSValue("webdav_key"); api->httpServerEnableFolderServer(true); if (api->httpServerStart(localonly, port, tls, pathtocert.c_str(), pathtokey.c_str())) { list servedpaths = ConfigurationManager::getConfigurationValueList("webdav_served_locations"); bool modified = false; for ( std::list::iterator it = servedpaths.begin(); it != servedpaths.end(); ++it) { string pathToServe = *it; if (pathToServe.size()) { std::unique_ptr n = nodebypath(pathToServe.c_str()); if (n) { char* l = api->httpServerGetLocalWebDavLink(n.get()); char* actualNodePath = api->getNodePath(n.get()); LOG_debug << "Serving via webdav: " << actualNodePath << ": " << l; if (pathToServe != actualNodePath) { it = servedpaths.erase(it); servedpaths.insert(it,string(actualNodePath)); modified = true; } delete []l; delete []actualNodePath; } else { LOG_warn << "Could no find location to server via webdav: " << pathToServe; } } } if (modified) { ConfigurationManager::savePropertyValueList("webdav_served_locations", servedpaths); } LOG_info << "Webdav server restored due to saved configuration"; } else { LOG_err << "Failed to initialize WEBDAV server. Ensure the port is free."; } } //ftp // restart ftp int portftp = ConfigurationManager::getConfigurationValue("ftp_port", -1); if (portftp != -1) { bool localonly = ConfigurationManager::getConfigurationValue("ftp_localonly", -1); bool tls = ConfigurationManager::getConfigurationValue("ftp_tls", false); string pathtocert, pathtokey; pathtocert = ConfigurationManager::getConfigurationSValue("ftp_cert"); pathtokey = ConfigurationManager::getConfigurationSValue("ftp_key"); int dataPortRangeBegin = ConfigurationManager::getConfigurationValue("ftp_port_data_begin", 1500); int dataPortRangeEnd = ConfigurationManager::getConfigurationValue("ftp_port_data_end", 1500+100); if (api->ftpServerStart(localonly, portftp, dataPortRangeBegin, dataPortRangeEnd, tls, pathtocert.c_str(), pathtokey.c_str())) { list servedpaths = ConfigurationManager::getConfigurationValueList("ftp_served_locations"); bool modified = false; for ( std::list::iterator it = servedpaths.begin(); it != servedpaths.end(); ++it) { string pathToServe = *it; if (pathToServe.size()) { std::unique_ptr n = nodebypath(pathToServe.c_str()); if (n) { char* l = api->ftpServerGetLocalLink(n.get()); char* actualNodePath = api->getNodePath(n.get()); LOG_debug << "Serving via ftp: " << pathToServe << ": " << l << ". Data Channel Port Range: " << dataPortRangeBegin << "-" << dataPortRangeEnd; if (pathToServe != actualNodePath) { it = servedpaths.erase(it); servedpaths.insert(it,string(actualNodePath)); modified = true; } delete []l; delete []actualNodePath; } else { LOG_warn << "Could no find location to server via ftp: " << pathToServe; } } } if (modified) { ConfigurationManager::savePropertyValueList("ftp_served_locations", servedpaths); } LOG_info << "FTP server restored due to saved configuration"; } else { LOG_err << "Failed to initialize FTP server. Ensure the port is free."; } } #endif } void MegaCmdExecuter::actUponLogout(MegaApi& api, MegaError* e, bool keptSession) { if (e->getErrorCode() == MegaError::API_ESID || checkNoErrors(e, "logout")) { LOG_verbose << "actUponLogout logout ok"; cwd = UNDEF; session.reset(); mtxSyncMap.lock(); ConfigurationManager::unloadConfiguration(); if (!keptSession) { ConfigurationManager::saveSession(""); ConfigurationManager::saveBackups(&ConfigurationManager::configuredBackups); ConfigurationManager::saveSyncs(&ConfigurationManager::oldConfiguredSyncs); } ConfigurationManager::clearConfigurationFile(); mtxSyncMap.unlock(); // clear greetings (asuming they are account-related) clearGreetingStatusAllListener(); clearGreetingStatusFirstListener(); } updateprompt(&api); } void MegaCmdExecuter::actUponLogout(SynchronousRequestListener *srl, bool keptSession, int timeout) { if (!timeout) { srl->wait(); } else { int trywaitout = srl->trywait(timeout); if (trywaitout) { LOG_err << "Logout took too long, it may have failed. No further actions performed"; return; } } actUponLogout(*api, srl->getError(), keptSession); } int MegaCmdExecuter::actUponCreateFolder(SynchronousRequestListener *srl, int timeout) { if (!timeout) { srl->wait(); } else { int trywaitout = srl->trywait(timeout); if (trywaitout) { LOG_err << "actUponCreateFolder took too long, it may have failed. No further actions performed"; return 1; } } if (checkNoErrors(srl->getError(), "create folder")) { LOG_verbose << "actUponCreateFolder Create Folder ok"; return 0; } return 2; } void MegaCmdExecuter::confirmDelete() { if (mNodesToConfirmDelete.size()) { std::unique_ptr nodeToConfirmDelete = std::move(mNodesToConfirmDelete.front()); mNodesToConfirmDelete.erase(mNodesToConfirmDelete.begin()); doDeleteNode(nodeToConfirmDelete, api); } if (mNodesToConfirmDelete.size()) { string newprompt("Are you sure to delete "); newprompt += mNodesToConfirmDelete.front()->getName(); newprompt += " ? (Yes/No/All/None): "; setprompt(AREYOUSURETODELETE,newprompt); } else { setprompt(COMMAND); } } void MegaCmdExecuter::discardDelete() { if (mNodesToConfirmDelete.size()) { mNodesToConfirmDelete.erase(mNodesToConfirmDelete.begin()); } if (mNodesToConfirmDelete.size()) { string newprompt("Are you sure to delete "); newprompt += mNodesToConfirmDelete.front()->getName(); newprompt += " ? (Yes/No/All/None): "; setprompt(AREYOUSURETODELETE,newprompt); } else { setprompt(COMMAND); } } void MegaCmdExecuter::confirmDeleteAll() { while (mNodesToConfirmDelete.size()) { std::unique_ptr nodeToConfirmDelete = std::move(mNodesToConfirmDelete.front()); mNodesToConfirmDelete.erase(mNodesToConfirmDelete.begin()); doDeleteNode(nodeToConfirmDelete, api); } setprompt(COMMAND); } void MegaCmdExecuter::discardDeleteAll() { mNodesToConfirmDelete.clear(); setprompt(COMMAND); } void MegaCmdExecuter::doDeleteNode(const std::unique_ptr& nodeToDelete, MegaApi* api) { char* nodePath = api->getNodePath(nodeToDelete.get()); if (nodePath) { LOG_verbose << "Deleting: "<< nodePath; } else { LOG_warn << "Deleting node whose path could not be found " << nodeToDelete->getName(); } MegaCmdListener* megaCmdListener = new MegaCmdListener(api, nullptr); std::unique_ptr parent(api->getParentNode(nodeToDelete.get())); if (parent && parent->getType() == MegaNode::TYPE_FILE) { api->removeVersion(nodeToDelete.get(), megaCmdListener); } else { api->remove(nodeToDelete.get(), megaCmdListener); } megaCmdListener->wait(); string msj = "delete node "; if (nodePath) { msj += nodePath; } else { msj += nodeToDelete->getName(); } checkNoErrors(megaCmdListener->getError(), msj); delete megaCmdListener; delete []nodePath; } int MegaCmdExecuter::deleteNodeVersions(const std::unique_ptr& nodeToDelete, MegaApi* api, int force) { if (nodeToDelete->getType() == MegaNode::TYPE_FILE && api->getNumVersions(nodeToDelete.get()) < 2) { if (!force) { LOG_err << "No versions found for " << nodeToDelete->getName(); } return MCMDCONFIRM_YES; //nothing to do, no sense asking } int confirmationResponse; if (nodeToDelete->getType() != MegaNode::TYPE_FILE) { string confirmationQuery("Are you sure todelete the version histories of files within "); confirmationQuery += nodeToDelete->getName(); confirmationQuery += "? (Yes/No): "; confirmationResponse = force?MCMDCONFIRM_ALL:askforConfirmation(confirmationQuery); if (confirmationResponse == MCMDCONFIRM_YES || confirmationResponse == MCMDCONFIRM_ALL) { auto children = std::unique_ptr(api->getChildren(nodeToDelete.get())); if (children) { for (int i = 0; i < children->size(); i++) { auto child = std::unique_ptr(children->get(i)); // wrap the pointer into the expected type by deleteNodeVersion deleteNodeVersions(child, api, true); child.release(); // the MegaNodeList owns the child, we don't want to double free it } } } } else { string confirmationQuery("Are you sure todelete the version histories of "); confirmationQuery += nodeToDelete->getName(); confirmationQuery += "? (Yes/No): "; confirmationResponse = force?MCMDCONFIRM_ALL:askforConfirmation(confirmationQuery); if (confirmationResponse == MCMDCONFIRM_YES || confirmationResponse == MCMDCONFIRM_ALL) { MegaNodeList* versionsToDelete = api->getVersions(nodeToDelete.get()); if (versionsToDelete) { for (int i = 0; i < versionsToDelete->size(); i++) { MegaNode *versionNode = versionsToDelete->get(i); if (versionNode->getHandle() != nodeToDelete->getHandle()) { MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); api->removeVersion(versionNode,megaCmdListener); megaCmdListener->wait(); string fullname(versionNode->getName()?versionNode->getName():"NO_NAME"); fullname += "#"; fullname += SSTR(versionNode->getModificationTime()); if (checkNoErrors(megaCmdListener->getError(), "remove version: "+fullname)) { LOG_verbose << " Removed " << fullname << " (" << getReadableTime(versionNode->getModificationTime()) << ")"; } delete megaCmdListener; } } delete versionsToDelete; } } } return confirmationResponse; } /** * @brief MegaCmdExecuter::deleteNode * @param nodeToDelete this function will delete this accordingly * @param api * @param recursive * @param force * @return confirmation code */ int MegaCmdExecuter::deleteNode(const std::unique_ptr& nodeToDelete, MegaApi* api, int recursive, int force) { if (nodeToDelete->getType() != MegaNode::TYPE_FILE && !recursive) { char* nodePath = api->getNodePath(nodeToDelete.get()); setCurrentThreadOutCode(MCMD_INVALIDTYPE); LOG_err << "Unable to delete folder: " << nodePath << ". Use -r to delete a folder recursively"; delete []nodePath; } else { if (!isCurrentThreadCmdShell() && isCurrentThreadInteractive() && !force && nodeToDelete->getType() != MegaNode::TYPE_FILE) { bool alreadythere = false; for (const auto& node : mNodesToConfirmDelete) { if (node->getHandle() == nodeToDelete->getHandle()) { alreadythere= true; } } if (!alreadythere) { if (getprompt() != AREYOUSURETODELETE) { string newprompt("Are you sure to delete "); newprompt += nodeToDelete->getName(); newprompt += " ? (Yes/No/All/None): "; setprompt(AREYOUSURETODELETE, newprompt); } mNodesToConfirmDelete.emplace_back(nodeToDelete->copy()); } return MCMDCONFIRM_NO; //default return } else if (!force && nodeToDelete->getType() != MegaNode::TYPE_FILE) { string confirmationQuery("Are you sure to delete "); confirmationQuery += nodeToDelete->getName(); confirmationQuery += " ? (Yes/No/All/None): "; int confirmationResponse = askforConfirmation(confirmationQuery); if (confirmationResponse == MCMDCONFIRM_YES || confirmationResponse == MCMDCONFIRM_ALL) { LOG_debug << "confirmation received"; doDeleteNode(nodeToDelete, api); } else { LOG_debug << "confirmation denied"; } return confirmationResponse; } else //force { doDeleteNode(nodeToDelete, api); return MCMDCONFIRM_ALL; } } return MCMDCONFIRM_NO; //default return } void MegaCmdExecuter::downloadNode(string source, string path, MegaApi* api, MegaNode *node, bool background, bool ignorequotawarn, int clientID, std::shared_ptr multiTransferListener) { if (sandboxCMD->isOverquota() && !ignorequotawarn) { m_time_t ts = m_time(); // in order to speedup and not flood the server we only ask for the details every 1 minute or after account changes if (!sandboxCMD->temporalbandwidth || (ts - sandboxCMD->lastQuerytemporalBandwith ) > 60 ) { LOG_verbose << " Updating temporal bandwidth "; sandboxCMD->lastQuerytemporalBandwith = ts; MegaCmdListener *megaCmdListener = new MegaCmdListener(api, NULL); api->getExtendedAccountDetails(false, false, false, megaCmdListener); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "get account details")) { MegaAccountDetails *details = megaCmdListener->getRequest()->getMegaAccountDetails(); sandboxCMD->istemporalbandwidthvalid = details->isTemporalBandwidthValid(); if (details && details->isTemporalBandwidthValid()) { sandboxCMD->temporalbandwidth = details->getTemporalBandwidth(); sandboxCMD->temporalbandwithinterval = details->getTemporalBandwidthInterval(); } } delete megaCmdListener; } OUTSTREAM << "Transfer not started. " << endl; if (sandboxCMD->istemporalbandwidthvalid) { OUTSTREAM << "You have utilized " << sizeToText(sandboxCMD->temporalbandwidth) << " of data transfer in the last " << sandboxCMD->temporalbandwithinterval << " hours, " "which took you over our current limit"; } else { OUTSTREAM << "You have reached your bandwidth quota"; } OUTSTREAM << ". To circumvent this limit, " "you can upgrade to Pro, which will give you your own bandwidth " "package and also ample extra storage space. " "Alternatively, you can try again in " << secondsToText(sandboxCMD->secondsOverQuota-(ts-sandboxCMD->timeOfOverquota)) << "." << endl << "See \"help --upgrade\" for further details" << endl; OUTSTREAM << "Use --ignore-quota-warn to initiate nevertheless" << endl; return; } if (!ignorequotawarn) { MegaCmdListener *megaCmdListener = new MegaCmdListener(api, NULL); api->queryTransferQuota(node->getSize(),megaCmdListener); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "query transfer quota")) { if (megaCmdListener->getRequest() && megaCmdListener->getRequest()->getFlag() ) { OUTSTREAM << "Transfer not started: proceeding will exceed transfer quota. " "Use --ignore-quota-warn to initiate nevertheless" << endl; return; } } delete megaCmdListener; } multiTransferListener->onNewTransfer(); #ifdef _WIN32 replaceAll(path,"/","\\"); #endif LOG_debug << "Starting download: " << node->getName() << " to : " << path; api->startDownload( node, //MegaNode* node, path.c_str(), // const char* localPath, nullptr, // const char *customName, nullptr, // const char *appData, false, // bool startFirst, nullptr, // MegaCancelToken *cancelToken, MegaTransfer::COLLISION_CHECK_FINGERPRINT, // int collisionCheck, MegaTransfer::COLLISION_RESOLUTION_NEW_WITH_N, // int collisionResolution, false, // bool undelete new ATransferListener(multiTransferListener, source) // MegaTransferListener *listener ); } class SelfDestructingTransferStartCallbackListener: public MegaTransferListener { std::function mOnTransferStart; public: template SelfDestructingTransferStartCallbackListener(Cb &&cb) : mOnTransferStart(std::forward(cb)) {} void onTransferStart(MegaApi *api, MegaTransfer *transfer) override { mOnTransferStart(transfer); } public: void onTransferFinish(MegaApi *api, MegaTransfer *transfer, MegaError *error) override { delete this; } }; void MegaCmdExecuter::uploadNode(const std::map &clflags, const std::map &cloptions, const std::string &receivedPath, MegaApi* api, MegaNode *node, const string &newname, MegaCmdMultiTransferListener *multiTransferListener) { bool printTag = getFlag(&clflags,"print-tag-at-start"); std::string path = receivedPath; unescapeifRequired(path); if (!pathExists(path)) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Unable to open local path: " << path; return; } MegaTransferListener *thelistener = multiTransferListener; std::optional>> promiseStarted; if (printTag) { promiseStarted.emplace(); auto onTransferStartCb = [&promiseStarted](MegaTransfer *transfer) { promiseStarted->set_value({transfer->getTag(), transfer->getPath()}); }; if (multiTransferListener) { multiTransferListener->setOnTransferStartCb(std::move(onTransferStartCb)); } else { // create a listener for the shake of printing the tag. thelistener = new SelfDestructingTransferStartCallbackListener(std::move(onTransferStartCb)); } } if (multiTransferListener) { multiTransferListener->onNewTransfer(); } #ifdef _WIN32 replaceAll(path,"/","\\"); #endif LOG_debug << "Starting upload: " << path << " to : " << node->getName() << (newname.size()?"/":"") << newname; api->startUpload( removeTrailingSeparators(path).c_str(),//const char *localPath, node,//MegaNode *parent, newname.size() ? newname.c_str() : nullptr,//const char *fileName, MegaApi::INVALID_CUSTOM_MOD_TIME,//int64_t mtime, nullptr,//const char *appData, false, //bool isSourceTemporary, false, //bool startFirst, nullptr,//MegaCancelToken *cancelToken, thelistener); if (promiseStarted) { auto [tag, path] = promiseStarted->get_future().get(); OUTSTREAM << "Upload started: Tag = " << tag << ". Source = " << path << std::endl; } } bool MegaCmdExecuter::amIPro() { int prolevel = -1; #ifdef MEGACMD_TESTING_CODE auto proLevelOpt = TI::Instance().testValue(TI::TestValue::AMIPRO_LEVEL); if (proLevelOpt) { prolevel = static_cast(std::get(*proLevelOpt)); LOG_debug << "Enforced test value for Pro Level: " << prolevel; return prolevel; } #endif auto megaCmdListener = std::make_unique(api, nullptr); api->getAccountDetails(megaCmdListener.get()); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "get account details")) { std::unique_ptr details(megaCmdListener->getRequest()->getMegaAccountDetails()); prolevel = details->getProLevel(); } return prolevel > 0; } void MegaCmdExecuter::exportNode(MegaNode *n, int64_t expireTime, const std::optional& password, std::map *clflags, std::map *cloptions) { const bool force = getFlag(clflags,"f"); const bool writable = getFlag(clflags,"writable"); const bool megaHosted = getFlag(clflags,"mega-hosted"); bool alreadyAcceptedBefore = false; bool copyrightAccepted = force || [&alreadyAcceptedBefore]() { return alreadyAcceptedBefore = ConfigurationManager::getConfigurationValue("copyrightAccepted", false); }(); if (!copyrightAccepted) { auto publicLinks = std::unique_ptr(api->getPublicLinks()); // Implicit acceptance (the user already has public links) copyrightAccepted = (publicLinks && publicLinks->size()); } if (!copyrightAccepted) { string confirmationQuery("MEGA respects the copyrights of others and requires that users of the MEGA cloud service comply with the laws of copyright.\n" "You are strictly prohibited from using the MEGA cloud service to infringe copyright.\n" "You may not upload, download, store, share, display, stream, distribute, email, link to, " "transmit or otherwise make available any files, data or content that infringes any copyright " "or other proprietary rights of any person or entity.\n"); confirmationQuery += "Do you accept these terms? (Yes/No): "; const int confirmationResponse = askforConfirmation(confirmationQuery); if (confirmationResponse != MCMDCONFIRM_YES && confirmationResponse != MCMDCONFIRM_ALL) { return; } } if (!alreadyAcceptedBefore) { // Save as accepted regardless of the source of acceptance ConfigurationManager::savePropertyValue("copyrightAccepted", true); } auto megaCmdListener = std::make_unique(api, nullptr); api->exportNode(n, expireTime, writable, megaHosted, megaCmdListener.get()); megaCmdListener->wait(); auto error = megaCmdListener->getError(); assert(error != nullptr); if (error->getErrorCode() != MegaError::API_OK) { auto path = std::unique_ptr(api->getNodePath(n)); std::string msg = "Failed to export node"; if (path != nullptr) { msg.append(" ").append(path.get()); } if (expireTime != 0 && !amIPro()) { msg.append(": Only PRO users can set an expiry time for links"); } else if (path != nullptr && strcmp(path.get(), "/") == 0) { msg.append(": The root folder cannot be exported"); } else { msg.append(": ").append(formatErrorAndMaySetErrorCode(*error)); } LOG_err << msg; return; } auto nexported = std::unique_ptr(api->getNodeByHandle(megaCmdListener->getRequest()->getNodeHandle())); if (!nexported) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Exported node not found"; return; } auto publicLink = std::unique_ptr(nexported->getPublicLink()); if (!publicLink) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Public link for exported node not found"; return; } auto nodepath = std::unique_ptr(api->getNodePath(nexported.get())); if (!nodepath) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Path for exported node not found"; return; } string publicPassProtectedLink; if (password) { // Encrypting links with passwords is a client-side operation that will be done regardless // of PRO status of the account. So we need to manually check for it before calling // `encryptLinkWithPassword`; the function itself will not fail check this. if (amIPro()) { megaCmdListener.reset(new MegaCmdListener(api, nullptr)); api->encryptLinkWithPassword(publicLink.get(), password->c_str(), megaCmdListener.get()); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "protect public link with password")) { publicPassProtectedLink = megaCmdListener->getRequest()->getText(); } } else { LOG_err << "Only PRO users can protect links with passwords. Showing UNPROTECTED link"; } } const int64_t actualExpireTime = nexported->getExpirationTime(); if (expireTime != 0 && !actualExpireTime) { setCurrentThreadOutCode(MCMD_INVALIDSTATE); LOG_err << "Could not add expiration date to exported node"; } const string authKey(nexported->getWritableLinkAuthKey() ? nexported->getWritableLinkAuthKey() : ""); if (writable && authKey.empty()) { setCurrentThreadOutCode(MCMD_INVALIDSTATE); LOG_err << "Failed to generate writable folder: missing auth key. Showing read-only link"; } const string nodeLink(publicPassProtectedLink.size() ? publicPassProtectedLink : publicLink.get()); OUTSTREAM << "Exported " << nodepath.get() << ": " << nodeLink; constexpr const char* prefix = "https://mega.nz/folder/"; if (authKey.size() && nodeLink.rfind(prefix, 0) == 0) { string authToken = nodeLink.substr(strlen(prefix)).append(":").append(authKey); OUTSTREAM << "\n AuthToken = " << authToken; if (megaHosted && megaCmdListener->getRequest()->getPassword()) { OUTSTREAM << "\n Share key encryption key = " << megaCmdListener->getRequest()->getPassword(); } } if (actualExpireTime) { OUTSTREAM << " expires at " << getReadableTime(nexported->getExpirationTime()); } OUTSTREAM << endl; } void MegaCmdExecuter::disableExport(MegaNode *n) { if (!n->isExported()) { setCurrentThreadOutCode(MCMD_INVALIDSTATE); LOG_err << "Could not disable export: node not exported."; return; } MegaCmdListener *megaCmdListener = new MegaCmdListener(api, NULL); api->disableExport(n, megaCmdListener); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "disable export")) { MegaNode *nexported = api->getNodeByHandle(megaCmdListener->getRequest()->getNodeHandle()); if (nexported) { char *nodepath = api->getNodePath(nexported); OUTSTREAM << "Disabled export: " << nodepath << endl; delete[] nodepath; delete nexported; } else { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Exported node not found!"; } } delete megaCmdListener; } std::pair MegaCmdExecuter::isSharePendingAndVerified(MegaNode* n, const char *email) const { if (!email) { return std::make_pair(false, false); } std::unique_ptr outShares (api->getOutShares(n)); if (outShares) { for (int i = 0; i < outShares->size(); i++) { auto outShare = outShares->get(i); auto shareEmail = outShare->getUser(); if (shareEmail && !strcmp(email, shareEmail)) { return std::make_pair(outShare->isPending(), outShare->isVerified()); } } } // just in case: loop pending ones too: std::unique_ptr pendingoutShares (api->getPendingOutShares(n)); if (pendingoutShares) { for (int i = 0; i < pendingoutShares->size(); i++) { auto pendingoutShare = pendingoutShares->get(i); auto shareEmail = pendingoutShare->getUser(); if (shareEmail && !strcmp(email, shareEmail)) { return std::make_pair(true, pendingoutShare->isVerified()); } } } return std::make_pair(false, false); } void MegaCmdExecuter::shareNode(MegaNode *n, string with, int level) { std::unique_ptr megaCmdListener(new MegaCmdListener(api)); api->openShareDialog(n, megaCmdListener.get()); megaCmdListener->wait(); if (megaCmdListener->getError()->getErrorCode() == MegaError::API_EINCOMPLETE) { setCurrentThreadOutCode(MCMD_NOTPERMITTED); LOG_err << "Unable to share folder. Your account security may need upgrading. Type \"" <getError(), "prepare sharing")) { return; } megaCmdListener.reset(new MegaCmdListener(api)); api->share(n, with.c_str(), level, megaCmdListener.get()); megaCmdListener->wait(); if (megaCmdListener->getError()->getErrorCode() == MegaError::API_EINCOMPLETE) { setCurrentThreadOutCode(MCMD_NOTPERMITTED); LOG_err << "Unable to share folder. Your account security may need upgrading. Type \"" <getError(), ( level != MegaShare::ACCESS_UNKNOWN ) ? "share node" : "disable share")) { MegaNode *nshared = api->getNodeByHandle(megaCmdListener->getRequest()->getNodeHandle()); if (nshared) { char *nodepath = api->getNodePath(nshared); if (megaCmdListener->getRequest()->getAccess() == MegaShare::ACCESS_UNKNOWN) { OUTSTREAM << "Stopped sharing " << nodepath << " with " << megaCmdListener->getRequest()->getEmail() << endl; } else { auto pendingAndVerified = isSharePendingAndVerified(n, megaCmdListener->getRequest()->getEmail()); auto pending = pendingAndVerified.first; auto verified = pendingAndVerified.second; OUTSTREAM << "New share: "; printOutShareInfo(nodepath, megaCmdListener->getRequest()->getEmail(), megaCmdListener->getRequest()->getAccess(), pending, verified); } delete[] nodepath; delete nshared; } else { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Shared node not found!"; } } } void MegaCmdExecuter::disableShare(MegaNode *n, string with) { shareNode(n, with, MegaShare::ACCESS_UNKNOWN); } int MegaCmdExecuter::makedir(string remotepath, bool recursive, MegaNode *parentnode) { MegaNode *currentnode; if (parentnode) { currentnode = parentnode; } else { currentnode = api->getNodeByHandle(cwd); } if (currentnode) { string rest = remotepath; while (rest.length()) { bool lastleave = false; size_t possep = rest.find_first_of("/"); if (possep == string::npos) { possep = rest.length(); lastleave = true; } string newfoldername = rest.substr(0, possep); if (!rest.length()) { break; } if (newfoldername.length()) { MegaNode *existing_node = api->getChildNode(currentnode, newfoldername.c_str()); if (!existing_node) { if (!recursive && !lastleave) { LOG_err << "Use -p to create folders recursively"; if (currentnode != parentnode) delete currentnode; return MCMD_EARGS; } LOG_verbose << "Creating (sub)folder: " << newfoldername; MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); api->createFolder(newfoldername.c_str(), currentnode, megaCmdListener); actUponCreateFolder(megaCmdListener); delete megaCmdListener; MegaNode *prevcurrentNode = currentnode; currentnode = api->getChildNode(currentnode, newfoldername.c_str()); if (prevcurrentNode != parentnode) delete prevcurrentNode; if (!currentnode) { LOG_err << "Couldn't get node for created subfolder: " << newfoldername; if (currentnode != parentnode) delete currentnode; return MCMD_INVALIDSTATE; } } else { if (currentnode != parentnode) delete currentnode; currentnode = existing_node; } if (lastleave && existing_node) { LOG_err << ((existing_node->getType() == MegaNode::TYPE_FILE)?"File":"Folder") << " already exists: " << remotepath; if (currentnode != parentnode) delete currentnode; return MCMD_INVALIDSTATE; } } //string rest = rest.substr(possep+1,rest.length()-possep-1); if (!lastleave) { rest = rest.substr(possep + 1, rest.length()); } else { break; } } if (currentnode != parentnode) delete currentnode; } else { return MCMD_EARGS; } return MCMD_OK; } string MegaCmdExecuter::getCurrentPath() { string toret; MegaNode *ncwd = api->getNodeByHandle(cwd); if (ncwd) { char *currentPath = api->getNodePath(ncwd); toret = string(currentPath); delete []currentPath; delete ncwd; } return toret; } long long MegaCmdExecuter::getVersionsSize(MegaNode *n) { long long toret = 0; MegaNodeList *versionNodes = api->getVersions(n); if (versionNodes) { for (int i = 0; i < versionNodes->size(); i++) { MegaNode *versionNode = versionNodes->get(i); toret += api->getSize(versionNode); } delete versionNodes; } MegaNodeList *children = api->getChildren(n); if (children) { for (int i = 0; i < children->size(); i++) { MegaNode *child = children->get(i); toret += getVersionsSize(child); } delete children; } return toret; } vector MegaCmdExecuter::listpaths(bool usepcre, string askedPath, bool discardFiles) { vector paths; if ((int)askedPath.size()) { vector *pathsToList = nodesPathsbypath(askedPath.c_str(), usepcre); if (pathsToList) { for (std::vector< string >::iterator it = pathsToList->begin(); it != pathsToList->end(); ++it) { string nodepath= *it; MegaNode *ncwd = api->getNodeByHandle(cwd); if (ncwd) { std::unique_ptr n = nodebypath(nodepath.c_str()); if (n) { if (n->getType() != MegaNode::TYPE_FILE) { nodepath += "/"; } if (!(discardFiles && (n->getType() == MegaNode::TYPE_FILE))) { paths.push_back(nodepath); } } else { LOG_debug << "Unexpected: matching path has no associated node: " << nodepath << ". Could have been deleted in the process"; } delete ncwd; } else { setCurrentThreadOutCode(MCMD_INVALIDSTATE); LOG_err << "Couldn't find woking folder (it might been deleted)"; } } pathsToList->clear(); delete pathsToList; } } return paths; } #ifdef _WIN32 //TODO: try to use these functions from somewhere else static std::wstring toUtf16String(const std::string& s, UINT codepage = CP_UTF8) { std::wstring ws; ws.resize(s.size() + 1); int nwchars = MultiByteToWideChar(codepage, 0, s.data(), int(s.size()), (LPWSTR)ws.data(), int(ws.size())); ws.resize(nwchars); return ws; } std::string toUtf8String(const std::wstring& ws, UINT codepage = CP_UTF8) { std::string s; s.resize((ws.size() + 1) * 4); int nchars = WideCharToMultiByte(codepage, 0, ws.data(), int(ws.size()), (LPSTR)s.data(), int(s.size()), NULL, NULL); s.resize(nchars); return s; } bool replaceW(std::wstring& str, const std::wstring& from, const std::wstring& to) { size_t start_pos = str.find(from); if (start_pos == std::wstring::npos) { return false; } str.replace(start_pos, from.length(), to); return true; } #endif vector MegaCmdExecuter::listLocalPathsStartingBy(string askedPath, bool discardFiles) { vector paths; #ifdef WIN32 string actualaskedPath = fs::u8path(askedPath).u8string(); char sep = (!askedPath.empty() && askedPath.find('/') != string::npos ) ?'/':'\\'; size_t postlastsep = actualaskedPath.find_last_of("/\\"); #else string actualaskedPath = askedPath; char sep = '/'; size_t postlastsep = actualaskedPath.find_last_of(sep); #endif if (postlastsep == 0) postlastsep++; // absolute paths string containingfolder = postlastsep == string::npos ? string() : actualaskedPath.substr(0, postlastsep); bool removeprefix = false; bool requiresseparatorafterunit = false; if (!containingfolder.size()) { containingfolder = "."; removeprefix= true; } #ifdef WIN32 else if (containingfolder.find(":") == 1 && (containingfolder.size() < 3 || ( containingfolder.at(2) != '/' && containingfolder.at(2) != '\\'))) { requiresseparatorafterunit = true; } #endif std::error_code ec; fs::directory_iterator dirIt(fs::u8path(containingfolder), ec); if (ec) { // We need to check the error directly because the iterator // might not be end() in certain underlying OS errors return paths; } for (const auto& dirEntry : dirIt) { if (discardFiles && !dirEntry.is_directory(ec)) { continue; } #ifdef _WIN32 wstring path = dirEntry.path().wstring(); #else string path = dirEntry.path().string(); #endif if (removeprefix) { path = path.substr(2); } if (requiresseparatorafterunit) { path.insert(2, 1, sep); } if (dirEntry.is_directory(ec)) { path.append(1, sep); } #ifdef _WIN32 // try to mimic the exact startup of the asked path to allow mix of '\' & '/' fs::path paskedpath = fs::u8path(askedPath); paskedpath.make_preferred(); wstring toreplace = paskedpath.wstring(); if (path.find(toreplace) == 0) { replaceW(path, toreplace, toUtf16String(askedPath)); } paths.push_back(toUtf8String(path)); #else paths.push_back(path); #endif } return paths; } vector MegaCmdExecuter::getlistusers() { vector users; MegaUserList* usersList = api->getContacts(); if (usersList) { for (int i = 0; i < usersList->size(); i++) { users.push_back(usersList->get(i)->getEmail()); } delete usersList; } return users; } vector MegaCmdExecuter::getNodeAttrs(string nodePath) { vector attrs; std::unique_ptr n = nodebypath(nodePath.c_str()); if (n) { //List node custom attributes MegaStringList *attrlist = n->getCustomAttrNames(); if (attrlist) { for (int a = 0; a < attrlist->size(); a++) { attrs.push_back(attrlist->get(a)); } delete attrlist; } } return attrs; } vector MegaCmdExecuter::getUserAttrs() { vector attrs; int i = 0; do { const char *catrn = api->userAttributeToString(i); if (strlen(catrn)) { attrs.push_back(catrn); } else { delete [] catrn; break; } delete [] catrn; i++; } while (true); return attrs; } vector MegaCmdExecuter::getsessions() { vector sessions; MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); api->getExtendedAccountDetails(true, true, true, megaCmdListener); int trywaitout = megaCmdListener->trywait(3000); if (trywaitout) { return sessions; } if (checkNoErrors(megaCmdListener->getError(), "get sessions")) { MegaAccountDetails *details = megaCmdListener->getRequest()->getMegaAccountDetails(); if (details) { int numSessions = details->getNumSessions(); for (int i = 0; i < numSessions; i++) { MegaAccountSession * session = details->getSession(i); if (session) { if (session->isAlive()) { std::unique_ptr handle {api->userHandleToBase64(session->getHandle())}; sessions.push_back(handle.get()); } delete session; } } delete details; } } delete megaCmdListener; return sessions; } vector MegaCmdExecuter::getlistfilesfolders(string location) { vector toret; for (fs::directory_iterator iter(fs::u8path(location)); iter != fs::directory_iterator(); ++iter) { toret.push_back(iter->path().filename().u8string()); } return toret; } void MegaCmdExecuter::signup(string name, string passwd, string email) { MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); size_t spos = name.find(" "); string firstname = name.substr(0, spos); string lastname; if (spos != string::npos && ((spos + 1) < name.length())) { lastname = name.substr(spos+1); } if (lastname.empty()) { setCurrentThreadOutCode(MCMD_INVALIDSTATE); LOG_err << "Please provide a valid name (with name and surname separated by \" \")"; return; } OUTSTREAM << "Signing up. name=" << firstname << ". surname=" << lastname<< endl; api->createAccount(email.c_str(), passwd.c_str(), firstname.c_str(), lastname.c_str(), megaCmdListener); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "create account <" + email + ">")) { OUTSTREAM << "Account <" << email << "> created successfully. You will receive a confirmation link. Use \"confirm\" with the provided link to confirm that account" << endl; } delete megaCmdListener; MegaCmdListener *megaCmdListener2 = new MegaCmdListener(NULL); api->localLogout(megaCmdListener2); megaCmdListener2->wait(); checkNoErrors(megaCmdListener2->getError(), "logging out from ephemeral account"); delete megaCmdListener2; } void MegaCmdExecuter::signupWithPassword(string passwd) { return signup(name, passwd, login); } void MegaCmdExecuter::confirm(string passwd, string email, string link) { MegaCmdListener *megaCmdListener2 = new MegaCmdListener(NULL); api->confirmAccount(link.c_str(), passwd.c_str(), megaCmdListener2); megaCmdListener2->wait(); if (megaCmdListener2->getError()->getErrorCode() == MegaError::API_ENOENT) { LOG_err << "Invalid password"; } else if (checkNoErrors(megaCmdListener2->getError(), "confirm account")) { OUTSTREAM << "Account " << email << " confirmed successfully. You can login with it now" << endl; } delete megaCmdListener2; } void MegaCmdExecuter::confirmWithPassword(string passwd) { return confirm(passwd, login, link); } bool MegaCmdExecuter::pathExists(const std::string &path) { LocalPath localPath = LocalPath::fromAbsolutePath(path); std::unique_ptr fa(mFsAccessCMD->newfileaccess()); return fa->isfolder(localPath) || fa->isfile(localPath); } bool MegaCmdExecuter::IsFolder(string path) { #ifdef _WIN32 replaceAll(path,"/","\\"); #endif LocalPath localpath = LocalPath::fromAbsolutePath(path); std::unique_ptr fa = mFsAccessCMD->newfileaccess(); return fa->isfolder(localpath); } void MegaCmdExecuter::printTransfersHeader(const unsigned int PATHSIZE, bool printstate) { OUTSTREAM << "TYPE TAG " << getFixLengthString("SOURCEPATH ",PATHSIZE) << getFixLengthString("DESTINYPATH ",PATHSIZE) << " " << getFixLengthString(" PROGRESS",21); if (printstate) { OUTSTREAM << " " << "STATE"; } OUTSTREAM << endl; } void MegaCmdExecuter::printTransfer(MegaTransfer *transfer, const unsigned int PATHSIZE, bool printstate) { //Direction #ifdef _WIN32 OUTSTREAM << " " << ((transfer->getType() == MegaTransfer::TYPE_DOWNLOAD)?"D":"U") << " "; #else OUTSTREAM << " " << ((transfer->getType() == MegaTransfer::TYPE_DOWNLOAD)?"\u21d3":"\u21d1") << " "; #endif //TODO: handle TYPE_LOCAL_TCP_DOWNLOAD //type (transfer/normal) if (transfer->isSyncTransfer()) { #ifdef _WIN32 OUTSTREAM << "S"; #else OUTSTREAM << "\u21f5"; #endif } else if (transfer->isBackupTransfer()) { #ifdef _WIN32 OUTSTREAM << "B"; #else OUTSTREAM << "\u23eb"; #endif } else { OUTSTREAM << " " ; } OUTSTREAM << " " ; //tag OUTSTREAM << getRightAlignedString(SSTR(transfer->getTag()),7) << " "; if (transfer->getType() == MegaTransfer::TYPE_DOWNLOAD) { // source MegaNode * node = api->getNodeByHandle(transfer->getNodeHandle()); if (node) { char * nodepath = api->getNodePath(node); OUTSTREAM << getFixLengthString(nodepath,PATHSIZE); delete []nodepath; delete node; } else { globalTransferListener->completedTransfersMutex.lock(); OUTSTREAM << getFixLengthString(globalTransferListener->completedPathsByHandle[transfer->getNodeHandle()],PATHSIZE); globalTransferListener->completedTransfersMutex.unlock(); } OUTSTREAM << " "; //destination string dest = transfer->getParentPath() ? transfer->getParentPath() : ""; dest.append(transfer->getFileName()); OUTSTREAM << getFixLengthString(dest,PATHSIZE); } else { //source string source(transfer->getParentPath()?transfer->getParentPath():""); source.append(transfer->getFileName()); OUTSTREAM << getFixLengthString(source, PATHSIZE); OUTSTREAM << " "; //destination MegaNode * parentNode = api->getNodeByHandle(transfer->getParentHandle()); if (parentNode) { char * parentnodepath = api->getNodePath(parentNode); OUTSTREAM << getFixLengthString(parentnodepath ,PATHSIZE); delete []parentnodepath; delete parentNode; } else { OUTSTREAM << getFixLengthString("",PATHSIZE,'-'); LOG_warn << "Could not find destination (parent handle "<< ((transfer->getParentHandle()==INVALID_HANDLE)?" invalid":" valid") <<" ) for upload transfer. Source=" << transfer->getParentPath() << transfer->getFileName(); } } //progress float percent; if (transfer->getTotalBytes() == 0) { percent = 0; } else { percent = float(transfer->getTransferredBytes()*1.0/transfer->getTotalBytes()); } OUTSTREAM << " " << getFixLengthString(percentageToText(percent),7,' ',true) << " of " << getFixLengthString(sizeToText(transfer->getTotalBytes()),10,' ',true); //state if (printstate) { OUTSTREAM << " " << getTransferStateStr(transfer->getState()); } OUTSTREAM << endl; } void MegaCmdExecuter::printTransferColumnDisplayer(ColumnDisplayer *cd, MegaTransfer *transfer, bool printstate) { //Direction string type; #ifdef _WIN32 type += utf16ToUtf8((transfer->getType() == MegaTransfer::TYPE_DOWNLOAD)?L"\u25bc":L"\u25b2"); #else type += (transfer->getType() == MegaTransfer::TYPE_DOWNLOAD)?"\u21d3":"\u21d1"; #endif //TODO: handle TYPE_LOCAL_TCP_DOWNLOAD //type (transfer/normal) if (transfer->isSyncTransfer()) { #ifdef _WIN32 type += utf16ToUtf8(L"\u21a8"); #else type += "\u21f5"; #endif } else if (transfer->isBackupTransfer()) { #ifdef _WIN32 type += utf16ToUtf8(L"\u2191"); #else type += "\u23eb"; #endif } cd->addValue("TYPE",type); cd->addValue("TAG", SSTR(transfer->getTag())); //TODO: do SSTR within ColumnDisplayer if (transfer->getType() == MegaTransfer::TYPE_DOWNLOAD) { // source MegaNode * node = api->getNodeByHandle(transfer->getNodeHandle()); if (node) { char * nodepath = api->getNodePath(node); cd->addValue("SOURCEPATH",nodepath); delete []nodepath; delete node; } else { globalTransferListener->completedTransfersMutex.lock(); cd->addValue("SOURCEPATH",globalTransferListener->completedPathsByHandle[transfer->getNodeHandle()]); globalTransferListener->completedTransfersMutex.unlock(); } //destination string dest = transfer->getParentPath() ? transfer->getParentPath() : ""; dest.append(transfer->getFileName()); cd->addValue("DESTINYPATH",dest); } else { //source string source(transfer->getPath() ? transfer->getPath() : ""); if (source.empty()) //fallback to use parent + filename { source = (transfer->getParentPath() ? transfer->getParentPath() : ""); if (transfer->getFileName()) { source.append(transfer->getFileName()); //Notice, this may differ from name of the source file } } cd->addValue("SOURCEPATH",source); //destination std::unique_ptr parentNode(api->getNodeByHandle(transfer->getParentHandle())); if (parentNode) { std::unique_ptr parentNodePath{api->getNodePath(parentNode.get())}; std::string parentnodepathS(parentNodePath ? parentNodePath.get() : ""); if (transfer->getFileName()) { if (!parentnodepathS.empty() && parentnodepathS.back() != '/') { parentnodepathS.append("/"); } parentnodepathS.append(transfer->getFileName()); } cd->addValue("DESTINYPATH", parentnodepathS); } else { cd->addValue("DESTINYPATH","---------"); LOG_warn << "Could not find destination (parent handle "<< ((transfer->getParentHandle()==INVALID_HANDLE)?" invalid":" valid") <<" ) for upload transfer. Source=" << transfer->getParentPath() << transfer->getFileName(); } } //progress float percent; if (transfer->getTotalBytes() == 0) { percent = 0; } else { percent = float(transfer->getTransferredBytes()*1.0/transfer->getTotalBytes()); } stringstream osspercent; osspercent << percentageToText(percent) << " of " << getFixLengthString(sizeToText(transfer->getTotalBytes()),10,' ',true); cd->addValue("PROGRESS",osspercent.str()); //state if (printstate) { cd->addValue("STATE",getTransferStateStr(transfer->getState())); } } void MegaCmdExecuter::printBackupHeader(const unsigned int PATHSIZE) { OUTSTREAM << "TAG " << " "; OUTSTREAM << getFixLengthString("LOCALPATH ", PATHSIZE) << " "; OUTSTREAM << getFixLengthString("REMOTEPARENTPATH ", PATHSIZE) << " "; OUTSTREAM << getRightAlignedString("STATUS", 14); OUTSTREAM << endl; } void MegaCmdExecuter::printBackupSummary(int tag, const char * localfolder, const char *remoteparentfolder, string status, const unsigned int PATHSIZE) { OUTSTREAM << getFixLengthString(SSTR(tag),5) << " " << getFixLengthString(localfolder, PATHSIZE) << " " << getFixLengthString((remoteparentfolder?remoteparentfolder:"INVALIDPATH"), PATHSIZE) << " " << getRightAlignedString(status, 14) << endl; } void MegaCmdExecuter::printBackupDetails(MegaScheduledCopy *backup, const char *timeFormat) { if (backup) { string speriod = (backup->getPeriod() == -1)?backup->getPeriodString():getReadablePeriod(backup->getPeriod()/10); OUTSTREAM << " Max Backups: " << backup->getMaxBackups() << endl; OUTSTREAM << " Period: " << "\"" << speriod << "\"" << endl; OUTSTREAM << " Next backup scheduled for: " << getReadableTime(backup->getNextStartTime(), timeFormat); OUTSTREAM << endl; OUTSTREAM << " " << " -- CURRENT/LAST BACKUP --" << endl; OUTSTREAM << " " << getFixLengthString("FILES UP/TOT", 15); OUTSTREAM << " " << getFixLengthString("FOLDERS CREATED", 15); OUTSTREAM << " " << getRightAlignedString("PROGRESS ", 22); MegaTransferList * ml = backup->getFailedTransfers(); if (ml && ml->size()) { OUTSTREAM << " " << getRightAlignedString("FAILED TRANSFERS", 17); } OUTSTREAM << endl; string sfiles = SSTR(backup->getNumberFiles()) + "/" + SSTR(backup->getTotalFiles()); OUTSTREAM << " " << getRightAlignedString(sfiles, 8) << " "; OUTSTREAM << " " << getRightAlignedString(SSTR(backup->getNumberFolders()), 8) << " "; long long trabytes = backup->getTransferredBytes(); long long totbytes = backup->getTotalBytes(); double percent = totbytes?double(trabytes)/double(totbytes):0; string sprogress = sizeProgressToText(trabytes, totbytes) + " " + percentageToText(float(percent)); OUTSTREAM << " " << getRightAlignedString(sprogress,22); if (ml && ml->size()) { OUTSTREAM << getRightAlignedString(SSTR(backup->getFailedTransfers()->size()), 17); } delete ml; OUTSTREAM << endl; } } void MegaCmdExecuter::printBackupHistory(MegaScheduledCopy *backup, const char *timeFormat, MegaNode *parentnode, const unsigned int PATHSIZE) { bool firstinhistory = true; MegaStringList *msl = api->getBackupFolders(backup->getTag()); if (msl) { for (int i = 0; i < msl->size(); i++) { int datelength = int(getReadableTime(m_time(), timeFormat).size()); if (firstinhistory) { OUTSTREAM << " " << " -- HISTORY OF BACKUPS --" << endl; // print header OUTSTREAM << " " << getFixLengthString("NAME", PATHSIZE) << " "; OUTSTREAM << getFixLengthString("DATE", datelength+1) << " "; OUTSTREAM << getRightAlignedString("STATUS", 11)<< " "; OUTSTREAM << getRightAlignedString("FILES", 6)<< " "; OUTSTREAM << getRightAlignedString("FOLDERS", 7); OUTSTREAM << endl; firstinhistory = false; } string bpath = msl->get(i); size_t pos = bpath.find("_bk_"); string btime = ""; if (pos != string::npos) { btime = bpath.substr(pos+4); } pos = bpath.find_last_of("/\\"); string backupInstanceName = bpath; if (pos != string::npos) { backupInstanceName = bpath.substr(pos+1); } string printableDate = "UNKNOWN"; if (btime.size()) { struct tm dt; fillStructWithSYYmdHMS(btime,dt); printableDate = getReadableTime(m_mktime(&dt), timeFormat); } string backupInstanceStatus="NOT_FOUND"; long long nfiles = 0; long long nfolders = 0; if (parentnode) { std::unique_ptr backupInstanceNode = nodebypath(msl->get(i)); if (backupInstanceNode) { backupInstanceStatus = backupInstanceNode->getCustomAttr("BACKST"); auto listener = std::make_unique(); api->getFolderInfo(backupInstanceNode.get(), listener.get()); listener->wait(); if (checkNoErrors(listener->getError(), "get folder info")) { auto info = listener->getRequest()->getMegaFolderInfo(); nfiles += info->getNumFiles(); nfolders += info->getNumFolders(); } } } OUTSTREAM << " " << getFixLengthString(backupInstanceName, PATHSIZE) << " "; OUTSTREAM << getFixLengthString(printableDate, datelength+1) << " "; OUTSTREAM << getRightAlignedString(backupInstanceStatus, 11) << " "; OUTSTREAM << getRightAlignedString(SSTR(nfiles), 6)<< " "; OUTSTREAM << getRightAlignedString(SSTR(nfolders), 7); //OUTSTREAM << getRightAlignedString("PROGRESS", 10);// some info regarding progress or the like in case of failure could be interesting. Although we don't know total files/folders/bytes OUTSTREAM << endl; } delete msl; } } void MegaCmdExecuter::printBackup(int tag, MegaScheduledCopy *backup, const char *timeFormat, const unsigned int PATHSIZE, bool extendedinfo, bool showhistory, MegaNode *parentnode) { if (backup) { const char *nodepath = NULL; bool deleteparentnode = false; if (!parentnode) { parentnode = api->getNodeByHandle(backup->getMegaHandle()); if (parentnode) { nodepath = api->getNodePath(parentnode); deleteparentnode = true; } } else { nodepath = api->getNodePath(parentnode); } printBackupSummary(tag, backup->getLocalFolder(),nodepath,backupSatetStr(backup->getState()), PATHSIZE); if (extendedinfo) { printBackupDetails(backup, timeFormat); } delete []nodepath; if (showhistory && parentnode) { printBackupHistory(backup, timeFormat, parentnode, PATHSIZE); } if (deleteparentnode) { delete parentnode; } } else { OUTSTREAM << "BACKUP not found " << endl; } } void MegaCmdExecuter::printBackup(backup_struct *backupstruct, const char *timeFormat, const unsigned int PATHSIZE, bool extendedinfo, bool showhistory) { if (backupstruct->tag >= 0) { MegaScheduledCopy *backup = api->getScheduledCopyByTag(backupstruct->tag); if (backup) { printBackup(backupstruct->tag, backup, timeFormat, PATHSIZE, extendedinfo, showhistory); delete backup; } else { OUTSTREAM << "BACKUP not found: " << backupstruct->tag << endl; } } else { //merely print configuration printBackupSummary(backupstruct->tag, backupstruct->localpath.c_str(),"UNKNOWN"," FAILED", PATHSIZE); if (extendedinfo) { string speriod = (backupstruct->period == -1)?backupstruct->speriod:getReadablePeriod(backupstruct->period/10); OUTSTREAM << " Period: " << "\"" << speriod << "\"" << endl; OUTSTREAM << " Max. Backups: " << backupstruct->numBackups << endl; } } } void MegaCmdExecuter::doFind(MegaNode* nodeBase, const char *timeFormat, std::map *clflags, std::map *cloptions, string word, int printfileinfo, string pattern, bool usepcre, m_time_t minTime, m_time_t maxTime, int64_t minSize, int64_t maxSize) { struct criteriaNodeVector pnv; pnv.pattern = pattern; vector listOfMatches; pnv.nodesMatching = &listOfMatches; pnv.usepcre = usepcre; pnv.minTime = minTime; pnv.maxTime = maxTime; pnv.minSize = minSize; pnv.maxSize = maxSize; auto opt = getOption(cloptions, "type", ""); pnv.mType = opt == "f" ? MegaNode::TYPE_FILE : (opt == "d" ? MegaNode::TYPE_FOLDER : MegaNode::TYPE_UNKNOWN); processTree(nodeBase, includeIfMatchesCriteria, (void*)&pnv); for (std::vector< MegaNode * >::iterator it = listOfMatches.begin(); it != listOfMatches.end(); ++it) { MegaNode * n = *it; if (n) { string pathToShow; if ( word.size() > 0 && ( (word.find("/") == 0) || (word.find("..") != string::npos)) ) { char * nodepath = api->getNodePath(n); pathToShow = string(nodepath); delete [] nodepath; } else { pathToShow = getDisplayPath("", n); } if (getFlag(clflags, "print-only-handles")) { OUTSTREAM << "H:" << handleToBase64(n->getHandle()) << "" << endl; } else if (printfileinfo) { dumpNode(n, timeFormat, clflags, cloptions, 3, false, 1, pathToShow.c_str()); } else { OUTSTREAM << pathToShow; if (getFlag(clflags, "show-handles")) { OUTSTREAM << " getHandle()) << ">"; } OUTSTREAM << endl; } //notice: some nodes may be dumped twice delete n; } } listOfMatches.clear(); } string MegaCmdExecuter::getLPWD() { string relativePath = "."; LocalPath localRelativePath = LocalPath::fromRelativePath(relativePath); LocalPath localAbsolutePath; if (!mFsAccessCMD->expanselocalpath(localRelativePath, localAbsolutePath)) { LOG_err << " Unable to expanse local path . "; return "UNKNOWN"; } LOG_verbose << "LocalPath localRelativePath = LocalPath::fromRelativePath(" << relativePath << "): " << localAbsolutePath.toPath(false); return localAbsolutePath.toPath(false); } void MegaCmdExecuter::moveToDestination(const std::unique_ptr& n, string destiny) { assert(n); char* nodepath = api->getNodePath(n.get()); LOG_debug << "Moving : " << nodepath << " to " << destiny; delete []nodepath; string newname; std::unique_ptr tn = nodebypath(destiny.c_str(), nullptr, &newname); // target node // we have four situations: // 1. target path does not exist - fail // 2. target node exists and is folder - move // 3. target node exists and is file - delete and rename (unless same) // 4. target path exists, but filename does not - rename if (tn) { if (tn->getHandle() == n->getHandle()) { LOG_err << "Source and destiny are the same"; } else { if (newname.size()) //target not found, but tn has what was before the last "/" in the path. { if (tn->getType() == MegaNode::TYPE_FILE) { setCurrentThreadOutCode(MCMD_INVALIDTYPE); LOG_err << destiny << ": Not a directory"; return; } else //move and rename! { MegaCmdListener *megaCmdListener = new MegaCmdListener(nullptr); api->moveNode(n.get(), tn.get(), megaCmdListener); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "move")) { MegaCmdListener *megaCmdListener = new MegaCmdListener(nullptr); api->renameNode(n.get(), newname.c_str(), megaCmdListener); megaCmdListener->wait(); checkNoErrors(megaCmdListener->getError(), "rename"); delete megaCmdListener; } else { LOG_debug << "Won't rename, since move failed " << n->getName() << " to " << tn->getName() << " : " << megaCmdListener->getError()->getErrorCode(); } delete megaCmdListener; } } else //target found { if (tn->getType() == MegaNode::TYPE_FILE) //move & remove old & rename new { // (there should never be any orphaned filenodes) std::unique_ptr tnParentNode(api->getNodeByHandle(tn->getParentHandle())); if (tnParentNode) { //move into the parent of target node MegaCmdListener *megaCmdListener = new MegaCmdListener(nullptr); std::unique_ptr parentNode(api->getNodeByHandle(tn->getParentHandle())); api->moveNode(n.get(), parentNode.get(), megaCmdListener); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "move node")) { const char* name_to_replace = tn->getName(); //remove (replaced) target node if (n.get() != tn.get()) //just in case moving to same location { MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); api->remove(tn.get(), megaCmdListener); //remove target node megaCmdListener->wait(); if (!checkNoErrors(megaCmdListener->getError(), "remove target node")) { LOG_err << "Couldnt move " << n->getName() << " to " << tn->getName() << " : " << megaCmdListener->getError()->getErrorCode(); } delete megaCmdListener; } // rename moved node with the new name if (strcmp(name_to_replace, n->getName())) { MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); api->renameNode(n.get(), name_to_replace, megaCmdListener); megaCmdListener->wait(); if (!checkNoErrors(megaCmdListener->getError(), "rename moved node")) { LOG_err << "Failed to rename moved node: " << megaCmdListener->getError()->getErrorString(); } delete megaCmdListener; } } delete megaCmdListener; } else { setCurrentThreadOutCode(MCMD_INVALIDSTATE); LOG_fatal << "Destiny node is orphan!!!"; } } else // target is a folder { MegaCmdListener *megaCmdListener = new MegaCmdListener(nullptr); api->moveNode(n.get(), tn.get(), megaCmdListener); megaCmdListener->wait(); checkNoErrors(megaCmdListener->getError(), "move node"); delete megaCmdListener; } } } } else //target not found (not even its folder), cant move { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << destiny << ": No such directory"; } } bool MegaCmdExecuter::isValidFolder(string destiny) { bool isdestinyavalidfolder = true; std::unique_ptr ndestiny = nodebypath(destiny.c_str());; if (ndestiny) { if (ndestiny->getType() == MegaNode::TYPE_FILE) { isdestinyavalidfolder = false; } } else { isdestinyavalidfolder = false; } return isdestinyavalidfolder; } std::string MegaCmdExecuter::getNodePathString(MegaNode *n) { const char *path = api->getNodePath(n); string toret(path); delete[] path; return toret; } void MegaCmdExecuter::copyNode(MegaNode *n, string destiny, MegaNode * tn, string &targetuser, string &newname) { if (tn) { if (tn->getHandle() == n->getHandle()) { LOG_err << "Source and destiny are the same"; } else { if (newname.size()) //target not found, but tn has what was before the last "/" in the path. { if (n->getType() == MegaNode::TYPE_FILE) { LOG_debug << "copy with new name: \"" << getNodePathString(n) << "\" to \"" << destiny << "\" newname=" << newname; //copy with new name MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); api->copyNode(n, tn, newname.c_str(), megaCmdListener); //only works for files megaCmdListener->wait(); checkNoErrors(megaCmdListener->getError(), "copy node"); delete megaCmdListener; } else //copy & rename { LOG_debug << "copy & rename: \"" << getNodePathString(n) << "\" to \"" << destiny << "\""; //copy with new name MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); api->copyNode(n, tn, megaCmdListener); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "copy node")) { MegaNode * newNode = api->getNodeByHandle(megaCmdListener->getRequest()->getNodeHandle()); if (newNode) { MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); api->renameNode(newNode, newname.c_str(), megaCmdListener); megaCmdListener->wait(); checkNoErrors(megaCmdListener->getError(), "rename new node"); delete megaCmdListener; delete newNode; } else { LOG_err << " Couldn't find new node created upon cp"; } } delete megaCmdListener; } } else { //target exists if (tn->getType() == MegaNode::TYPE_FILE) { if (n->getType() == MegaNode::TYPE_FILE) { LOG_debug << "overwriding target: \"" << getNodePathString(n) << "\" to \"" << destiny << "\""; // overwrite target if source and target are files MegaNode *tnParentNode = api->getNodeByHandle(tn->getParentHandle()); if (tnParentNode) // (there should never be any orphaned filenodes) { const char* name_to_replace = tn->getName(); //copy with new name MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); api->copyNode(n, tnParentNode, name_to_replace, megaCmdListener); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "copy with new name") ) { MegaHandle newNodeHandle = megaCmdListener->getRequest()->getNodeHandle(); delete megaCmdListener; delete tnParentNode; //remove target node if (tn->getHandle() != newNodeHandle )//&& newNodeHandle != n->getHandle()) { megaCmdListener = new MegaCmdListener(NULL); api->remove(tn, megaCmdListener); megaCmdListener->wait(); checkNoErrors(megaCmdListener->getError(), "delete target node"); delete megaCmdListener; } } } else { setCurrentThreadOutCode(MCMD_INVALIDSTATE); LOG_fatal << "Destiny node is orphan!!!"; } } else { setCurrentThreadOutCode(MCMD_INVALIDTYPE); LOG_err << "Cannot overwrite file with folder"; return; } } else //copying into folder { LOG_debug << "Copying into folder: \"" << getNodePathString(n) << "\" to \"" << destiny << "\""; MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); api->copyNode(n, tn, megaCmdListener); megaCmdListener->wait(); checkNoErrors(megaCmdListener->getError(), "copy node"); delete megaCmdListener; } } } } else if (targetuser.size()) { LOG_debug << "Sending to user: \"" << getNodePathString(n) << "\" to \"" << targetuser << "\""; MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); api->sendFileToUser(n,targetuser.c_str(),megaCmdListener); megaCmdListener->wait(); checkNoErrors(megaCmdListener->getError(), "send file to user"); delete megaCmdListener; } else { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << destiny << " Couldn't find destination"; } } bool MegaCmdExecuter::establishBackup(string pathToBackup, MegaNode *n, int64_t period, string speriod, int numBackups) { bool attendpastbackups = true; //TODO: receive as parameter static int backupcounter = 0; LocalPath localAbsolutePath = LocalPath::fromAbsolutePath(pathToBackup); //this one would converts it to absolute if it's relative LocalPath expansedAbsolutePath; if (!mFsAccessCMD->expanselocalpath(localAbsolutePath, expansedAbsolutePath)) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << " Failed to expanse path"; } auto megaCmdListener = std::make_unique(api, nullptr); api->setScheduledCopy(expansedAbsolutePath.toPath(false).c_str(), n, attendpastbackups, period, speriod.c_str(), numBackups, megaCmdListener.get()); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "establish backup")) { backup_struct *thebackup = nullptr; bool sendBackupEvent = false; { std::lock_guard g(mtxBackupsMap); if (ConfigurationManager::configuredBackups.find(megaCmdListener->getRequest()->getFile()) != ConfigurationManager::configuredBackups.end()) { thebackup = ConfigurationManager::configuredBackups[megaCmdListener->getRequest()->getFile()]; if (thebackup->id == -1) { thebackup->id = backupcounter++; } } else { thebackup = new backup_struct; thebackup->id = backupcounter++; ConfigurationManager::configuredBackups[megaCmdListener->getRequest()->getFile()] = thebackup; sendBackupEvent = true; } thebackup->active = true; thebackup->handle = megaCmdListener->getRequest()->getNodeHandle(); thebackup->localpath = string(megaCmdListener->getRequest()->getFile()); thebackup->numBackups = numBackups; thebackup->period = period; thebackup->speriod = speriod; thebackup->failed = false; thebackup->tag = megaCmdListener->getRequest()->getTransferTag(); } if (sendBackupEvent) { auto wasFirstBackupConfiguredOpt = ConfigurationManager::savePropertyValue("firstBackupConfigured", true); if (!wasFirstBackupConfiguredOpt || !*wasFirstBackupConfiguredOpt) { sendEvent(StatsManager::MegacmdEvent::FIRST_CONFIGURED_SCHEDULED_BACKUP, api, false); } else { sendEvent(StatsManager::MegacmdEvent::SUBSEQUENT_CONFIGURED_SCHEDULED_BACKUP, api, false); } } std::unique_ptr nodepath(api->getNodePath(n)); LOG_info << "Added backup: " << megaCmdListener->getRequest()->getFile() << " to " << nodepath; return true; } else { bool foundbytag = false; // find by tag in configured (modification failed) for (std::map::iterator itr = ConfigurationManager::configuredBackups.begin(); itr != ConfigurationManager::configuredBackups.end(); itr++) { if (itr->second->tag == megaCmdListener->getRequest()->getTransferTag()) { backup_struct *thebackup = itr->second; foundbytag = true; thebackup->handle = megaCmdListener->getRequest()->getNodeHandle(); thebackup->localpath = string(megaCmdListener->getRequest()->getFile()); thebackup->numBackups = megaCmdListener->getRequest()->getNumRetry(); thebackup->period = megaCmdListener->getRequest()->getNumber(); thebackup->speriod = string(megaCmdListener->getRequest()->getText());; thebackup->failed = true; } } if (!foundbytag) { std::map::iterator itr = ConfigurationManager::configuredBackups.find(megaCmdListener->getRequest()->getFile()); if ( itr != ConfigurationManager::configuredBackups.end()) { if (megaCmdListener->getError()->getErrorCode() != MegaError::API_EEXIST) { itr->second->failed = true; } itr->second->id = backupcounter++; } } } return false; } void MegaCmdExecuter::confirmCancel(const char* confirmlink, const char* pass) { auto megaCmdListener = std::make_unique(nullptr); api->confirmCancelAccount(confirmlink, pass, megaCmdListener.get()); megaCmdListener->wait(); if (megaCmdListener->getError()->getErrorCode() == MegaError::API_ETOOMANY) { LOG_err << "Confirm cancel account failed: too many attempts"; } else if (megaCmdListener->getError()->getErrorCode() == MegaError::API_ENOENT || megaCmdListener->getError()->getErrorCode() == MegaError::API_EKEY) { LOG_err << "Confirm cancel account failed: invalid link/password"; } // We consider ESID (bad session ID) as successful because of a data race in the SDK else if (megaCmdListener->getError()->getErrorCode() == MegaError::API_ESID || checkNoErrors(megaCmdListener->getError(), "confirm cancel account")) { OUTSTREAM << "CONFIRM Account cancelled successfully" << endl; megaCmdListener = std::make_unique(nullptr); api->localLogout(megaCmdListener.get()); actUponLogout(megaCmdListener.get(), false); } } void MegaCmdExecuter::processPath(string path, bool usepcre, bool& firstone, void (*nodeprocessor)(MegaCmdExecuter *, MegaNode *, bool), MegaCmdExecuter *context) { if (isRegExp(path)) { vector> nodes = nodesbypath(path.c_str(), usepcre); if (nodes.empty()) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Path not found: " << path; } for (const auto& n : nodes) { assert(n); nodeprocessor(context, n.get(), firstone); firstone = false; } } else // non-regexp { std::unique_ptr n = nodebypath(path.c_str()); if (n) { nodeprocessor(context, n.get(), firstone); firstone = false; } else { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Path not found: " << path; } } } #ifdef HAVE_LIBUV void forwarderRemoveWebdavLocation(MegaCmdExecuter* context, MegaNode *n, bool firstone) { context->removeWebdavLocation(n, firstone); } void forwarderAddWebdavLocation(MegaCmdExecuter* context, MegaNode *n, bool firstone) { context->addWebdavLocation(n, firstone); } void forwarderRemoveFtpLocation(MegaCmdExecuter* context, MegaNode *n, bool firstone) { context->removeFtpLocation(n, firstone); } void forwarderAddFtpLocation(MegaCmdExecuter* context, MegaNode *n, bool firstone) { context->addFtpLocation(n, firstone); } void MegaCmdExecuter::removeWebdavLocation(MegaNode *n, bool firstone, string name) { char *actualNodePath = api->getNodePath(n); api->httpServerRemoveWebDavAllowedNode(n->getHandle()); mtxWebDavLocations.lock(); list servedpaths = ConfigurationManager::getConfigurationValueList("webdav_served_locations"); size_t sizeprior = servedpaths.size(); servedpaths.remove(actualNodePath); size_t sizeafter = servedpaths.size(); if (!sizeafter) { api->httpServerStop(); ConfigurationManager::savePropertyValue("webdav_port", -1); //so as not to load server on startup } ConfigurationManager::savePropertyValueList("webdav_served_locations", servedpaths); mtxWebDavLocations.unlock(); if (sizeprior != sizeafter) { OUTSTREAM << (name.size()?name:actualNodePath) << " no longer served via webdav" << endl; } else { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << (name.size()?name:actualNodePath) << " is not served via webdav"; } delete []actualNodePath; } void MegaCmdExecuter::addWebdavLocation(MegaNode *n, bool firstone, string name) { std::unique_ptr actualNodePath(api->getNodePath(n)); std::unique_ptr l(api->httpServerGetLocalWebDavLink(n)); OUTSTREAM << "Serving via webdav " << (name.size() ? name : actualNodePath.get()) << ": " << l.get() << endl; { std::lock_guard g(mtxWebDavLocations); list servedpaths = ConfigurationManager::getConfigurationValueList("webdav_served_locations"); if (!ConfigurationManager::getConfigurationValue("firstWebDavConfigured", false)) { sendEvent(StatsManager::MegacmdEvent::FIRST_CONFIGURED_WEBDAV, api, false); ConfigurationManager::savePropertyValue("firstWebDavConfigured", true); } else if (std::find(servedpaths.begin(), servedpaths.end(), actualNodePath.get()) == servedpaths.end()) { // Send event only if not already on the list sendEvent(StatsManager::MegacmdEvent::SUBSEQUENT_CONFIGURED_WEBDAV, api, false); } servedpaths.push_back(actualNodePath.get()); servedpaths.sort(); servedpaths.unique(); ConfigurationManager::savePropertyValueList("webdav_served_locations", servedpaths); } } void MegaCmdExecuter::removeFtpLocation(MegaNode *n, bool firstone, string name) { char *actualNodePath = api->getNodePath(n); api->ftpServerRemoveAllowedNode(n->getHandle()); mtxFtpLocations.lock(); list servedpaths = ConfigurationManager::getConfigurationValueList("ftp_served_locations"); size_t sizeprior = servedpaths.size(); servedpaths.remove(actualNodePath); size_t sizeafter = servedpaths.size(); if (!sizeafter) { api->ftpServerStop(); ConfigurationManager::savePropertyValue("ftp_port", -1); //so as not to load server on startup } ConfigurationManager::savePropertyValueList("ftp_served_locations", servedpaths); mtxFtpLocations.unlock(); if (sizeprior != sizeafter) { OUTSTREAM << (name.size()?name:actualNodePath) << " no longer served via ftp" << endl; } else { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << (name.size()?name:actualNodePath) << " is not served via ftp"; } delete []actualNodePath; } void MegaCmdExecuter::addFtpLocation(MegaNode *n, bool firstone, string name) { std::unique_ptr actualNodePath(api->getNodePath(n)); std::unique_ptr l(api->ftpServerGetLocalLink(n)); OUTSTREAM << "Serving via ftp " << (name.size() ? name : n->getName()) << ": " << l.get() << endl; { std::lock_guard g(mtxFtpLocations); list servedpaths = ConfigurationManager::getConfigurationValueList("ftp_served_locations"); if (!ConfigurationManager::getConfigurationValue("firstFtpConfigured", false)) { sendEvent(StatsManager::MegacmdEvent::FIRST_CONFIGURED_FTP, api, false); ConfigurationManager::savePropertyValue("firstFtpConfigured", true); } else if (std::find(servedpaths.begin(), servedpaths.end(), actualNodePath.get()) == servedpaths.end()) { // Send event only if not already on the list sendEvent(StatsManager::MegacmdEvent::SUBSEQUENT_CONFIGURED_FTP, api, false); } servedpaths.push_back(actualNodePath.get()); servedpaths.sort(); servedpaths.unique(); ConfigurationManager::savePropertyValueList("ftp_served_locations", servedpaths); } } #endif void MegaCmdExecuter::catFile(MegaNode *n) { if (n->getType() != MegaNode::TYPE_FILE) { LOG_err << " Unable to cat: not a file"; setCurrentThreadOutCode(MCMD_EARGS); return; } long long nsize = api->getSize(n); if (!nsize) { return; } long long end = nsize; long long start = 0; MegaCmdCatTransferListener *mcctl = new MegaCmdCatTransferListener(&OUTSTREAM, api, sandboxCMD); api->startStreaming(n, start, end-start, mcctl); mcctl->wait(); if (checkNoErrors(mcctl->getError(), "cat streaming from " +SSTR(start) + " to " + SSTR(end) )) { char * npath = api->getNodePath(n); LOG_verbose << "Streamed: " << npath << " from " << start << " to " << end; delete []npath; } delete mcctl; } void MegaCmdExecuter::printInfoFile(MegaNode *n, bool &firstone, int PATHSIZE) { char * nodepath = api->getNodePath(n); char *fattrs = n->getFileAttrString(); if (fattrs == NULL) { LOG_warn << " Unable to get attributes for node " << nodepath; } if (firstone) { OUTSTREAM << getFixLengthString("FILE", PATHSIZE); OUTSTREAM << getFixLengthString("WIDTH", 7); OUTSTREAM << getFixLengthString("HEIGHT", 7); OUTSTREAM << getFixLengthString("FPS", 4); OUTSTREAM << getFixLengthString("PLAYTIME", 10); OUTSTREAM << endl; firstone = false; } OUTSTREAM << getFixLengthString(nodepath, PATHSIZE-1) << " "; delete []nodepath; OUTSTREAM << getFixLengthString( (n->getWidth() == -1) ? "---" : SSTR(n->getWidth()) , 6) << " "; OUTSTREAM << getFixLengthString( (n->getHeight() == -1) ? "---" : SSTR(n->getHeight()) , 6) << " "; if (fattrs == NULL) { OUTSTREAM << getFixLengthString("---", 3) << " "; } else { MediaProperties mp = MediaProperties::decodeMediaPropertiesAttributes(fattrs, (uint32_t*)(n->getNodeKey()->data() + FILENODEKEYLENGTH / 2) ); OUTSTREAM << getFixLengthString( (mp.fps == 0) ? "---" : SSTR(mp.fps) , 3) << " "; } OUTSTREAM << getFixLengthString( (n->getDuration() == -1) ? "---" : getReadablePeriod(n->getDuration()) , 10) << " "; OUTSTREAM << endl; } bool MegaCmdExecuter::printUserAttribute(int a, string user, bool onlylist) { const char *catrn = api->userAttributeToString(a); string attrname = catrn; delete [] catrn; const char *catrln = api->userAttributeToLongName(a); string longname = catrln; delete [] catrln; if (attrname.size()) { if (onlylist) { OUTSTREAM << longname << " (" << attrname << ")" << endl; } else { MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); if (user.size()) { api->getUserAttribute(user.c_str(), a, megaCmdListener); } else { api->getUserAttribute(a, megaCmdListener); } megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), string("get user attribute ") + attrname)) { //int iattr = int(megaCmdListener->getRequest()->getParamType()); const char *value = megaCmdListener->getRequest()->getText(); //if (!value) value = megaCmdListener->getRequest()->getMegaStringMap()->; string svalue; try { if (value) { svalue = string(value); } else { svalue = "NOT PRINTABLE"; } } catch (exception e) { svalue = "NOT PRINTABLE"; } OUTSTREAM << "\t" << longname << " (" << attrname << ")" << " = " << svalue << endl; } delete megaCmdListener; } return true; } return false; } bool MegaCmdExecuter::setProxy(const std::string &url, const std::string &username, const std::string &password, int proxyType) { MegaProxy mpx; if (url.size()) { mpx.setProxyURL(url.c_str()); } if (username.size()) { mpx.setCredentials(username.c_str(), password.c_str()); } mpx.setProxyType(proxyType); std::vector megaapis; //TODO: add apiFolders to that list megaapis.push_back(api); bool failed = false; for (auto api: megaapis) { MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); api->setProxySettings(&mpx, megaCmdListener); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "(un)setting proxy")) { //TODO: connectivity check! // review connectivity check branch } else { failed = true; } delete megaCmdListener; } if (!failed) { ConfigurationManager::savePropertyValue("proxy_url", mpx.getProxyURL()?mpx.getProxyURL():""); ConfigurationManager::savePropertyValue("proxy_type", mpx.getProxyType()); ConfigurationManager::savePropertyValue("proxy_username", mpx.getUsername()?mpx.getUsername():""); ConfigurationManager::savePropertyValue("proxy_password", mpx.getPassword()?mpx.getPassword():""); if (mpx.getProxyType() == MegaProxy::PROXY_NONE) { OUTSTREAM << "Proxy unset correctly" << endl ; } else { OUTSTREAM << "Proxy set: " << (mpx.getProxyURL()?mpx.getProxyURL():"") << " type = " << getProxyTypeStr(mpx.getProxyType()) << endl; broadcastMessage(string("Using proxy: ").append((mpx.getProxyURL()?mpx.getProxyURL():"")) .append(" type = ").append(getProxyTypeStr(mpx.getProxyType())), true); } } else { LOG_err << "Unable to configure proxy"; broadcastMessage("Unable to configure proxy", true); } return !failed; } bool checkAtLeastNArgs(const vector &words, size_t n) { if (words.size() < n) { setCurrentThreadOutCode(MCMD_EARGS); assert(!words.empty()); LOG_err << words[0] << ": Invalid number of arguments"; LOG_err << "Usage: " << getUsageStr(words[0].c_str()); return false; } return true; } bool checkExactlyNArgs(const vector &words, size_t n) { if (words.size() != n) { setCurrentThreadOutCode(MCMD_EARGS); assert(!words.empty()); LOG_err << words[0] << ": Invalid number of arguments"; LOG_err << "Usage: " << getUsageStr(words[0].c_str()); return false; } return true; } void MegaCmdExecuter::executecommand(vector words, map *clflags, map *cloptions) { if (words[0] == "ls") { if (!api->isFilesystemAvailable()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in."; return; } int recursive = getFlag(clflags, "R") + getFlag(clflags, "r"); int extended_info = getFlag(clflags, "a"); int show_versions = getFlag(clflags, "versions"); bool summary = getFlag(clflags, "l"); bool firstprint = true; bool humanreadable = getFlag(clflags, "h"); bool treelike = getFlag(clflags,"tree"); recursive += treelike?1:0; if ((int)words.size() > 1) { unescapeifRequired(words[1]); string rNpath = "NULL"; if (words[1].find('/') != string::npos) { string cwpath = getCurrentPath(); if (words[1].find(cwpath) == string::npos) { rNpath = ""; } else { rNpath = cwpath; } } if (isRegExp(words[1])) { vector *pathsToList = nodesPathsbypath(words[1].c_str(), getFlag(clflags,"use-pcre")); if (pathsToList && pathsToList->size()) { for (std::vector< string >::iterator it = pathsToList->begin(); it != pathsToList->end(); ++it) { string nodepath= *it; MegaNode *ncwd = api->getNodeByHandle(cwd); if (ncwd) { std::unique_ptr n = nodebypath(nodepath.c_str()); if (n) { if (!n->getType() == MegaNode::TYPE_FILE) { OUTSTREAM << nodepath << ": " << endl; } if (summary) { if (firstprint) { dumpNodeSummaryHeader(getTimeFormatFromSTR(getOption(cloptions, "time-format","SHORT")), clflags, cloptions); firstprint = false; } dumpTreeSummary(n.get(), getTimeFormatFromSTR(getOption(cloptions, "time-format","SHORT")), clflags, cloptions, recursive, show_versions, 0, humanreadable, rNpath); } else { vector lfs; dumptree(n.get(), treelike, lfs, getTimeFormatFromSTR(getOption(cloptions, "time-format","RFC2822")), clflags, cloptions, recursive, extended_info, show_versions, 0, rNpath); } if ((!n->getType() == MegaNode::TYPE_FILE ) && ((it + 1) != pathsToList->end())) { OUTSTREAM << endl; } } else { LOG_debug << "Unexpected: matching path has no associated node: " << nodepath << ". Could have been deleted in the process"; } delete ncwd; } else { setCurrentThreadOutCode(MCMD_INVALIDSTATE); LOG_err << "Couldn't find woking folder (it might been deleted)"; } } pathsToList->clear(); delete pathsToList; } else { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Couldn't find \"" << words[1] << "\""; } } else { std::unique_ptr n = nodebypath(words[1].c_str()); if (n) { if (summary) { if (firstprint) { dumpNodeSummaryHeader( getTimeFormatFromSTR(getOption(cloptions, "time-format", "SHORT")), clflags, cloptions); firstprint = false; } dumpTreeSummary( n.get(), getTimeFormatFromSTR(getOption(cloptions, "time-format", "SHORT")), clflags, cloptions, recursive, show_versions, 0, humanreadable, rNpath); } else { if (treelike) OUTSTREAM << words[1] << endl; vector lfs; dumptree(n.get(), treelike, lfs, getTimeFormatFromSTR(getOption(cloptions, "time-format","RFC2822")), clflags, cloptions, recursive, extended_info, show_versions, 0, rNpath); } } else { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Couldn't find " << words[1]; } } } else { std::unique_ptr n(api->getNodeByHandle(cwd)); if (n) { if (summary) { if (firstprint) { dumpNodeSummaryHeader( getTimeFormatFromSTR(getOption(cloptions, "time-format", "SHORT")), clflags, cloptions); firstprint = false; } dumpTreeSummary(n.get(), getTimeFormatFromSTR(getOption(cloptions, "time-format", "SHORT")), clflags, cloptions, recursive, show_versions, 0, humanreadable, "NULL"); } else { if (treelike) OUTSTREAM << "." << endl; vector lfs; dumptree(n.get(), treelike, lfs, getTimeFormatFromSTR(getOption(cloptions, "time-format","RFC2822")), clflags, cloptions, recursive, extended_info, show_versions); } } } return; } else if (words[0] == "find") { string pattern = getOption(cloptions, "pattern", "*"); int printfileinfo = getFlag(clflags,"l"); if (!api->isFilesystemAvailable()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in."; return; } m_time_t minTime = -1; m_time_t maxTime = -1; string mtimestring = getOption(cloptions, "mtime", ""); if ("" != mtimestring && !getMinAndMaxTime(mtimestring, &minTime, &maxTime)) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Invalid time " << mtimestring; return; } int64_t minSize = -1; int64_t maxSize = -1; string sizestring = getOption(cloptions, "size", ""); if ("" != sizestring && !getMinAndMaxSize(sizestring, &minSize, &maxSize)) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Invalid time " << sizestring; return; } if (words.size() <= 1) { std::unique_ptr n(api->getNodeByHandle(cwd)); doFind(n.get(), getTimeFormatFromSTR(getOption(cloptions, "time-format","RFC2822")), clflags, cloptions, "", printfileinfo, pattern, getFlag(clflags,"use-pcre"), minTime, maxTime, minSize, maxSize); } for (int i = 1; i < (int)words.size(); i++) { if (isRegExp(words[i])) { vector> nodesToFind = nodesbypath(words[i].c_str(), getFlag(clflags,"use-pcre")); if (nodesToFind.size()) { for (const auto& node : nodesToFind) { assert(node); doFind(node.get(), getTimeFormatFromSTR(getOption(cloptions, "time-format","RFC2822")), clflags, cloptions, words[i], printfileinfo, pattern, getFlag(clflags,"use-pcre"), minTime, maxTime, minSize, maxSize); } } else { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << words[i] << ": No such file or directory"; } } else { std::unique_ptr n = nodebypath(words[i].c_str()); if (!n) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Couldn't find " << words[i]; } else { doFind(n.get(), getTimeFormatFromSTR(getOption(cloptions, "time-format","RFC2822")), clflags, cloptions, words[i], printfileinfo, pattern, getFlag(clflags,"use-pcre"), minTime, maxTime, minSize, maxSize); } } } } #if defined(_WIN32) || defined(__APPLE__) else if (words[0] == "update") { string sauto = getOption(cloptions, "auto", ""); transform(sauto.begin(), sauto.end(), sauto.begin(), [](char c) { return char(::tolower(c)); }); if (sauto == "off") { stopcheckingForUpdates(); OUTSTREAM << "Automatic updates disabled" << endl; } else if (sauto == "on") { startcheckingForUpdates(); OUTSTREAM << "Automatic updates enabled" << endl; } else if (sauto == "query") { OUTSTREAM << "Automatic updates " << (ConfigurationManager::getConfigurationValue("autoupdate", false)?"enabled":"disabled") << endl; } else { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("update"); } return; } #endif else if (words[0] == "cd") { if (!api->isFilesystemAvailable()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in."; return; } if (words.size() > 1) { std::unique_ptr n = nodebypath(words[1].c_str()); if (n) { if (n->getType() == MegaNode::TYPE_FILE) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << words[1] << ": Not a directory"; } else { cwd = n->getHandle(); updateprompt(api); } } else { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << words[1] << ": No such file or directory"; } } else { MegaNode * rootNode = api->getRootNode(); if (!rootNode) { LOG_err << "nodes not fetched"; setCurrentThreadOutCode(MCMD_NOFETCH); delete rootNode; return; } cwd = rootNode->getHandle(); updateprompt(api); delete rootNode; } return; } else if (words[0] == "rm") { if (!api->isFilesystemAvailable()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in."; return; } if (words.size() > 1) { if (isCurrentThreadInteractive()) { //clear all previous nodes to confirm delete (could have been not cleared in case of ctrl+c) mNodesToConfirmDelete.clear(); } bool force = getFlag(clflags, "f"); bool none = false; for (unsigned int i = 1; i < words.size(); i++) { unescapeifRequired(words[i]); if (isRegExp(words[i])) { vector> nodesToDelete = nodesbypath(words[i].c_str(), getFlag(clflags,"use-pcre")); if (nodesToDelete.empty()) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << words[i] << ": No such file or directory"; } for (const auto& node : nodesToDelete) { assert(node); int confirmationCode = deleteNode(node, api, getFlag(clflags, "r"), force); if (confirmationCode == MCMDCONFIRM_ALL) { force = true; } else if (confirmationCode == MCMDCONFIRM_NONE) { none = true; } } } else if (!none) { std::unique_ptr nodeToDelete = nodebypath(words[i].c_str()); if (!nodeToDelete) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << words[i] << ": No such file or directory"; } else { int confirmationCode = deleteNode(nodeToDelete, api, getFlag(clflags, "r"), force); if (confirmationCode == MCMDCONFIRM_ALL) { force = true; } else if (confirmationCode == MCMDCONFIRM_NONE) { none = true; } } } } } else { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("rm"); } return; } else if (words[0] == "mv") { if (!api->isFilesystemAvailable()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in."; return; } if (words.size() > 2) { string destiny = words[words.size()-1]; unescapeifRequired(destiny); if (words.size() > 3 && !isValidFolder(destiny)) { setCurrentThreadOutCode(MCMD_INVALIDTYPE); LOG_err << destiny << " must be a valid folder"; return; } for (unsigned int i=1;i<(words.size()-1);i++) { string source = words[i]; unescapeifRequired(source); if (isRegExp(source)) { vector> nodesToList = nodesbypath(words[i].c_str(), getFlag(clflags,"use-pcre")); if (nodesToList.empty()) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << source << ": No such file or directory"; } bool destinyisok = true; if (nodesToList.size() > 1 && !isValidFolder(destiny)) { destinyisok = false; setCurrentThreadOutCode(MCMD_INVALIDTYPE); LOG_err << destiny << " must be a valid folder"; } if (destinyisok) { for (const auto& node : nodesToList) { assert(node); moveToDestination(node, destiny); } } } else { std::unique_ptr n = nodebypath(source.c_str()); if (n) { moveToDestination(n, destiny); } else { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << source << ": No such file or directory"; } } } } else { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("mv"); } return; } else if (words[0] == "cp") { if (!api->isFilesystemAvailable()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in."; return; } if (words.size() > 2) { string destiny = words[words.size()-1]; string targetuser; string newname; std::unique_ptr tn = nodebypath(destiny.c_str(), &targetuser, &newname); if (words.size() > 3 && !isValidFolder(destiny) && !targetuser.size()) { setCurrentThreadOutCode(MCMD_INVALIDTYPE); LOG_err << destiny << " must be a valid folder"; return; } for (unsigned int i=1;i<(words.size()-1);i++) { string source = words[i]; if (isRegExp(source)) { vector> nodesToCopy = nodesbypath(words[i].c_str(), getFlag(clflags,"use-pcre")); if (nodesToCopy.empty()) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << source << ": No such file or directory"; } bool destinyisok = true; if (nodesToCopy.size() > 1 && !isValidFolder(destiny) && !targetuser.size()) { destinyisok = false; setCurrentThreadOutCode(MCMD_INVALIDTYPE); LOG_err << destiny << " must be a valid folder"; } if (destinyisok) { for (const auto& n : nodesToCopy) { assert(n); copyNode(n.get(), destiny, tn.get(), targetuser, newname); } } } else { std::unique_ptr n = nodebypath(source.c_str()); if (n) { copyNode(n.get(), destiny, tn.get(), targetuser, newname); } else { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << source << ": No such file or directory"; } } } } else { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("cp"); } return; } else if (words[0] == "du") { if (!api->isFilesystemAvailable()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in."; return; } int PATHSIZE = getintOption(cloptions,"path-display-size"); if (!PATHSIZE) { // get screen size for output purposes unsigned int width = getNumberOfCols(75); PATHSIZE = min(50,int(width-22)); } PATHSIZE = max(0, PATHSIZE); long long totalSize = 0; long long currentSize = 0; long long totalVersionsSize = 0; string dpath; if (words.size() == 1) { words.push_back("."); } bool humanreadable = getFlag(clflags, "h"); bool show_versions_size = getFlag(clflags, "versions"); bool firstone = true; for (unsigned int i = 1; i < words.size(); i++) { unescapeifRequired(words[i]); if (isRegExp(words[i])) { vector> nodesToList = nodesbypath(words[i].c_str(), getFlag(clflags,"use-pcre")); for (const auto& n : nodesToList) { assert(n); if (firstone)//print header { OUTSTREAM << getFixLengthString("FILENAME", PATHSIZE) << getFixLengthString("SIZE", 12, ' ', true); if (show_versions_size) { OUTSTREAM << getFixLengthString("S.WITH VERS", 12, ' ', true);; } OUTSTREAM << endl; firstone = false; } currentSize = api->getSize(n.get()); totalSize += currentSize; dpath = getDisplayPath(words[i], n.get()); OUTSTREAM << getFixLengthString(dpath+":",PATHSIZE) << getFixLengthString(sizeToText(currentSize, true, humanreadable), 12, ' ', true); if (show_versions_size) { long long sizeWithVersions = getVersionsSize(n.get()); OUTSTREAM << getFixLengthString(sizeToText(sizeWithVersions, true, humanreadable), 12, ' ', true); totalVersionsSize += sizeWithVersions; } OUTSTREAM << endl; } } else { std::unique_ptr n = nodebypath(words[i].c_str()); if (!n) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << words[i] << ": No such file or directory"; return; } currentSize = api->getSize(n.get()); totalSize += currentSize; dpath = getDisplayPath(words[i], n.get()); if (dpath.size()) { if (firstone)//print header { OUTSTREAM << getFixLengthString("FILENAME",PATHSIZE) << getFixLengthString("SIZE", 12, ' ', true); if (show_versions_size) { OUTSTREAM << getFixLengthString("S.WITH VERS", 12, ' ', true);; } OUTSTREAM << endl; firstone = false; } OUTSTREAM << getFixLengthString(dpath+":",PATHSIZE) << getFixLengthString(sizeToText(currentSize, true, humanreadable), 12, ' ', true); if (show_versions_size) { long long sizeWithVersions = getVersionsSize(n.get()); OUTSTREAM << getFixLengthString(sizeToText(sizeWithVersions, true, humanreadable), 12, ' ', true); totalVersionsSize += sizeWithVersions; } OUTSTREAM << endl; } } } if (!firstone) { for (int i = 0; i < PATHSIZE+12 +(show_versions_size?12:0) ; i++) { OUTSTREAM << "-"; } OUTSTREAM << endl; OUTSTREAM << getFixLengthString("Total storage used:",PATHSIZE) << getFixLengthString(sizeToText(totalSize, true, humanreadable), 12, ' ', true); //OUTSTREAM << "Total storage used: " << setw(22) << sizeToText(totalSize, true, humanreadable); if (show_versions_size) { OUTSTREAM << getFixLengthString(sizeToText(totalVersionsSize, true, humanreadable), 12, ' ', true); } OUTSTREAM << endl; } return; } else if (words[0] == "cat") { if (words.size() < 2) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("cat"); return; } for (int i = 1; i < (int)words.size(); i++) { if (isPublicLink(words[i])) { string publicLink = words[i]; if (!decryptLinkIfEncrypted(api, publicLink, cloptions)) { return; } if (getLinkType(publicLink) == MegaNode::TYPE_FILE) { MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); api->getPublicNode(publicLink.c_str(), megaCmdListener); megaCmdListener->wait(); if (!checkNoErrors(megaCmdListener->getError(), "cat public node")) { if (megaCmdListener->getError()->getErrorCode() == MegaError::API_EARGS) { LOG_err << "The link provided might be incorrect: " << publicLink.c_str(); } else if (megaCmdListener->getError()->getErrorCode() == MegaError::API_EINCOMPLETE) { LOG_err << "The key is missing or wrong " << publicLink.c_str(); } } else { if (megaCmdListener->getRequest()->getFlag()) { LOG_err << "Key not valid " << publicLink.c_str(); setCurrentThreadOutCode(MCMD_EARGS); } else { MegaNode *n = megaCmdListener->getRequest()->getPublicMegaNode(); if (n) { catFile(n); delete n; } } } delete megaCmdListener; } else //TODO: detect if referenced file within public link and in that case, do login and cat it { LOG_err << "Public link is not a file"; setCurrentThreadOutCode(MCMD_EARGS); } } else if (!api->isFilesystemAvailable()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Unable to cat " << words[i] << ": Not logged in."; } else { unescapeifRequired(words[i]); if (isRegExp(words[i])) { vector> nodes = nodesbypath(words[i].c_str(), getFlag(clflags,"use-pcre")); if (nodes.empty()) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Nodes not found: " << words[i]; } for (const auto& n : nodes) { assert(n); catFile(n.get()); } } else { std::unique_ptr n = nodebypath(words[i].c_str()); if (n) { catFile(n.get()); } else { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Node not found: " << words[i]; } } } } } else if (words[0] == "mediainfo") { if (!api->isFilesystemAvailable()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in."; return; } if (words.size() < 2) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("mediainfo"); return; } int PATHSIZE = getintOption(cloptions,"path-display-size"); if (!PATHSIZE) { // get screen size for output purposes unsigned int width = getNumberOfCols(75); PATHSIZE = min(50,int(width-28)); } PATHSIZE = max(0, PATHSIZE); bool firstone = true; for (int i = 1; i < (int)words.size(); i++) { unescapeifRequired(words[i]); if (isRegExp(words[i])) { vector> nodes = nodesbypath(words[i].c_str(), getFlag(clflags,"use-pcre")); if (nodes.empty()) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Nodes not found: " << words[i]; } for (const auto& n : nodes) { assert(n); printInfoFile(n.get(), firstone, PATHSIZE); } } else { std::unique_ptr n = nodebypath(words[i].c_str()); if (n) { printInfoFile(n.get(), firstone, PATHSIZE); } else { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Node not found: " << words[i]; } } } } else if (words[0] == "get") { bool background = getFlag(clflags,"q"); int clientID = getintOption(cloptions, "clientID", -1); if (words.size() > 1 && words.size() < 4) { string path = "./"; if (background) { clientID = -1; } auto megaCmdMultiTransferListener = std::make_shared(api, sandboxCMD, nullptr, clientID); bool ignorequotawarn = getFlag(clflags,"ignore-quota-warn"); bool destinyIsFolder = false; if (isPublicLink(words[1])) { string publicLink = words[1]; if (!decryptLinkIfEncrypted(api, publicLink, cloptions)) { return; } if (getLinkType(publicLink) == MegaNode::TYPE_FILE) { if (words.size() > 2) { path = words[2]; destinyIsFolder = IsFolder(path); if (destinyIsFolder) { if (! (path.find_last_of("/") == path.size()-1) && ! (path.find_last_of("\\") == path.size()-1)) { #ifdef _WIN32 path+="\\"; #else path+="/"; #endif } if (!canWrite(path)) { setCurrentThreadOutCode(MCMD_NOTPERMITTED); LOG_err << "Write not allowed in " << path; return; } } else { if (!TestCanWriteOnContainingFolder(path)) { return; } } } MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); api->getPublicNode(publicLink.c_str(), megaCmdListener); megaCmdListener->wait(); if (!megaCmdListener->getError()) { LOG_fatal << "No error in listener at get public node"; } else if (!checkNoErrors(megaCmdListener->getError(), "get public node")) { if (megaCmdListener->getError()->getErrorCode() == MegaError::API_EARGS) { LOG_err << "The link provided might be incorrect: " << publicLink.c_str(); } else if (megaCmdListener->getError()->getErrorCode() == MegaError::API_EINCOMPLETE) { LOG_err << "The key is missing or wrong " << publicLink.c_str(); } } else { if (megaCmdListener->getRequest() && megaCmdListener->getRequest()->getFlag()) { LOG_err << "Key not valid " << publicLink.c_str(); } if (megaCmdListener->getRequest()) { if (destinyIsFolder && getFlag(clflags,"m")) { while( (path.find_last_of("/") == path.size()-1) || (path.find_last_of("\\") == path.size()-1)) { path=path.substr(0,path.size()-1); } } MegaNode *n = megaCmdListener->getRequest()->getPublicMegaNode(); downloadNode(words[1], path, api, n, background, ignorequotawarn, clientID, megaCmdMultiTransferListener); delete n; } else { LOG_err << "Empty Request at get"; } } delete megaCmdListener; } else if (getLinkType(publicLink) == MegaNode::TYPE_FOLDER) { if (words.size() > 2) { path = words[2]; destinyIsFolder = IsFolder(path); if (destinyIsFolder) { if (! (path.find_last_of("/") == path.size()-1) && ! (path.find_last_of("\\") == path.size()-1)) { #ifdef _WIN32 path+="\\"; #else path+="/"; #endif } if (!canWrite(words[2])) { setCurrentThreadOutCode(MCMD_NOTPERMITTED); LOG_err << "Write not allowed in " << words[2]; return; } } else { setCurrentThreadOutCode(MCMD_INVALIDTYPE); LOG_err << words[2] << " is not a valid Download Folder"; return; } } MegaApi* apiFolder = getFreeApiFolder(); if (!apiFolder) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "No available Api folder. Use configure to increase exported_folders_sdks"; return; } char *accountAuth = api->getAccountAuth(); apiFolder->setAccountAuth(accountAuth); delete []accountAuth; MegaCmdListener *megaCmdListener = new MegaCmdListener(apiFolder, NULL); apiFolder->loginToFolder(publicLink.c_str(), megaCmdListener); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "login to folder")) { MegaCmdListener *megaCmdListener2 = new MegaCmdListener(apiFolder, NULL); apiFolder->fetchNodes(megaCmdListener2); megaCmdListener2->wait(); if (checkNoErrors(megaCmdListener2->getError(), "access folder link " + publicLink)) { MegaNode *nodeToDownload = NULL; bool usedRoot = false; string shandle = getPublicLinkHandle(publicLink); if (shandle.size()) { handle thehandle = apiFolder->base64ToHandle(shandle.c_str()); nodeToDownload = apiFolder->getNodeByHandle(thehandle); } else { nodeToDownload = apiFolder->getRootNode(); usedRoot = true; } if (nodeToDownload) { if (destinyIsFolder && getFlag(clflags,"m")) { while( (path.find_last_of("/") == path.size()-1) || (path.find_last_of("\\") == path.size()-1)) { path=path.substr(0,path.size()-1); } } MegaNode *authorizedNode = apiFolder->authorizeNode(nodeToDownload); if (authorizedNode != NULL) { downloadNode(words[1], path, api, authorizedNode, background, ignorequotawarn, clientID, megaCmdMultiTransferListener); delete authorizedNode; } else { LOG_debug << "Node couldn't be authorized: " << publicLink << ". Downloading as non-loged user"; downloadNode(words[1], path, apiFolder, nodeToDownload, background, ignorequotawarn, clientID, megaCmdMultiTransferListener); } delete nodeToDownload; } else { setCurrentThreadOutCode(MCMD_INVALIDSTATE); if (usedRoot) { LOG_err << "Couldn't get root folder for folder link"; } else { LOG_err << "Failed to get node corresponding to handle within public link " << shandle; } } } delete megaCmdListener2; { std::unique_ptr megaCmdListener(new MegaCmdListener(apiFolder)); apiFolder->logout(false, megaCmdListener.get()); megaCmdListener->wait(); if (megaCmdListener->getError()->getErrorCode() != MegaError::API_OK) { LOG_err << "Couldn't logout from apiFolder"; } } } delete megaCmdListener; freeApiFolder(apiFolder); } else { setCurrentThreadOutCode(MCMD_INVALIDTYPE); LOG_err << "Invalid link: " << publicLink; } } else //remote file { if (!api->isFilesystemAvailable()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in."; return; } unescapeifRequired(words[1]); if (isRegExp(words[1])) { vector> nodesToGet = nodesbypath(words[1].c_str(), getFlag(clflags,"use-pcre")); if (words.size() > 2) { path = words[2]; destinyIsFolder = IsFolder(path); if (destinyIsFolder) { if (! (path.find_last_of("/") == path.size()-1) && ! (path.find_last_of("\\") == path.size()-1)) { #ifdef _WIN32 path+="\\"; #else path+="/"; #endif } if (!canWrite(words[2])) { setCurrentThreadOutCode(MCMD_NOTPERMITTED); LOG_err << "Write not allowed in " << words[2]; return; } } else if (nodesToGet.size() > 1) //several files into one file! { setCurrentThreadOutCode(MCMD_INVALIDTYPE); LOG_err << words[2] << " is not a valid Download Folder"; return; } else //destiny non existing or a file { if (!TestCanWriteOnContainingFolder(path)) { return; } } } if (destinyIsFolder && getFlag(clflags,"m")) { while( (path.find_last_of("/") == path.size()-1) || (path.find_last_of("\\") == path.size()-1)) { path=path.substr(0,path.size()-1); } } for (const auto& n : nodesToGet) { assert(n); downloadNode(words[1], path, api, n.get(), background, ignorequotawarn, clientID, megaCmdMultiTransferListener); } if (nodesToGet.empty()) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Couldn't find " << words[1]; } } else //not regexp { std::unique_ptr n = nodebypath(words[1].c_str()); if (n) { if (words.size() > 2) { if (n->getType() == MegaNode::TYPE_FILE) { path = words[2]; destinyIsFolder = IsFolder(path); if (destinyIsFolder) { if (! (path.find_last_of("/") == path.size()-1) && ! (path.find_last_of("\\") == path.size()-1)) { #ifdef _WIN32 path+="\\"; #else path+="/"; #endif } if (!canWrite(words[2])) { setCurrentThreadOutCode(MCMD_NOTPERMITTED); LOG_err << "Write not allowed in " << words[2]; return; } } else { if (!TestCanWriteOnContainingFolder(path)) { return; } } } else { path = words[2]; destinyIsFolder = IsFolder(path); if (destinyIsFolder) { if (! (path.find_last_of("/") == path.size()-1) && ! (path.find_last_of("\\") == path.size()-1)) { #ifdef _WIN32 path+="\\"; #else path+="/"; #endif } if (!canWrite(words[2])) { setCurrentThreadOutCode(MCMD_NOTPERMITTED); LOG_err << "Write not allowed in " << words[2]; return; } } else { setCurrentThreadOutCode(MCMD_INVALIDTYPE); LOG_err << words[2] << " is not a valid Download Folder"; return; } } } if (destinyIsFolder && getFlag(clflags,"m")) { while( (path.find_last_of("/") == path.size()-1) || (path.find_last_of("\\") == path.size()-1)) { path=path.substr(0,path.size()-1); } } downloadNode(words[1], path, api, n.get(), background, ignorequotawarn, clientID, megaCmdMultiTransferListener); } else { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Couldn't find file"; } } } if (!background) { megaCmdMultiTransferListener->waitMultiEnd(); if (megaCmdMultiTransferListener->getFinalerror() != MegaError::API_OK) { setCurrentThreadOutCode(megaCmdMultiTransferListener->getFinalerror()); LOG_err << "Download failed. error code: " << MegaError::getErrorString(megaCmdMultiTransferListener->getFinalerror()); } if (megaCmdMultiTransferListener->getProgressinformed() || getCurrentThreadOutCode() == MCMD_OK ) { informProgressUpdate(PROGRESS_COMPLETE, megaCmdMultiTransferListener->getTotalbytes(), clientID); } } } else { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("get"); } return; } else if (words[0] == "backup") { bool dodelete = getFlag(clflags,"d"); bool abort = getFlag(clflags,"a"); bool listinfo = getFlag(clflags,"l"); bool listhistory = getFlag(clflags,"h"); // //TODO: do the following functionality // bool stop = getFlag(clflags,"s"); // bool resume = getFlag(clflags,"r"); // bool initiatenow = getFlag(clflags,"i"); int PATHSIZE = getintOption(cloptions,"path-display-size"); if (!PATHSIZE) { // get screen size for output purposes unsigned int width = getNumberOfCols(75); PATHSIZE = min(60,int((width-46)/2)); } PATHSIZE = max(0, PATHSIZE); bool firstbackup = true; string speriod=getOption(cloptions, "period"); int numBackups = int(getintOption(cloptions, "num-backups", -1)); if (words.size() == 3) { string local = words.at(1); string remote = words.at(2); unescapeifRequired(local); unescapeifRequired(remote); createOrModifyBackup(local, remote, speriod, numBackups); } else if (words.size() == 2) { string local = words.at(1); unescapeifRequired(local); MegaScheduledCopy *backup = api->getScheduledCopyByPath(local.c_str()); if (!backup) { backup = api->getScheduledCopyByTag(toInteger(local, -1)); } map::iterator itr; if (backup) { int backupid = -1; for ( itr = ConfigurationManager::configuredBackups.begin(); itr != ConfigurationManager::configuredBackups.end(); itr++ ) { if (itr->second->tag == backup->getTag()) { backupid = itr->second->id; break; } } if (backupid == -1) { LOG_err << " Requesting info of unregistered backup: " << local; } if (dodelete) { MegaCmdListener *megaCmdListener = new MegaCmdListener(api, NULL); api->removeScheduledCopy(backup->getTag(), megaCmdListener); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "remove backup")) { if (backupid != -1) { ConfigurationManager::configuredBackups.erase(itr); } mtxBackupsMap.lock(); ConfigurationManager::saveBackups(&ConfigurationManager::configuredBackups); mtxBackupsMap.unlock(); OUTSTREAM << " Backup removed succesffuly: " << local << endl; } } else if (abort) { MegaCmdListener *megaCmdListener = new MegaCmdListener(api, NULL); api->abortCurrentScheduledCopy(backup->getTag(), megaCmdListener); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "abort backup")) { OUTSTREAM << " Backup aborted succesffuly: " << local << endl; } } else { if (speriod.size() || numBackups != -1) { createOrModifyBackup(backup->getLocalFolder(), "", speriod, numBackups); } else { if(firstbackup) { printBackupHeader(PATHSIZE); firstbackup = false; } printBackup(backup->getTag(), backup, getTimeFormatFromSTR(getOption(cloptions, "time-format","RFC2822")), PATHSIZE, listinfo, listhistory); } } delete backup; } else { if (dodelete) //remove from configured backups { bool deletedok = false; for ( itr = ConfigurationManager::configuredBackups.begin(); itr != ConfigurationManager::configuredBackups.end(); itr++ ) { if (itr->second->tag == -1 && itr->second->localpath == local) { mtxBackupsMap.lock(); ConfigurationManager::configuredBackups.erase(itr); ConfigurationManager::saveBackups(&ConfigurationManager::configuredBackups); mtxBackupsMap.unlock(); OUTSTREAM << " Backup removed succesffuly: " << local << endl; deletedok = true; break; } } if (!deletedok) { setCurrentThreadOutCode(MCMD_NOTFOUND); OUTSTREAM << "Backup not found: " << local << endl; } } else { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Backup not found: " << local; } } } else if (words.size() == 1) //list backups { mtxBackupsMap.lock(); for (map::iterator itr = ConfigurationManager::configuredBackups.begin(); itr != ConfigurationManager::configuredBackups.end(); itr++ ) { if(firstbackup) { printBackupHeader(PATHSIZE); firstbackup = false; } printBackup(itr->second, getTimeFormatFromSTR(getOption(cloptions, "time-format","RFC2822")), PATHSIZE, listinfo, listhistory); } if (!ConfigurationManager::configuredBackups.size()) { setCurrentThreadOutCode(MCMD_NOTFOUND); OUTSTREAM << "No backup configured. " << endl << " Usage: " << getUsageStr("backup") << endl; } mtxBackupsMap.unlock(); } else { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("backup"); } } else if (words[0] == "put") { if (!api->isFilesystemAvailable()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in."; return; } bool background = getFlag(clflags,"q"); bool autocreate = getFlag(clflags, "c"); int clientID = getintOption(cloptions, "clientID", -1); if (words.size() < 2) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("put"); return; } string targetuser; string newname = ""; bool explicitRemotePath = words.size() > 2; std::size_t numberOfLocalPaths = explicitRemotePath ? words.size() - 2ul : 1; const std::string &destination = explicitRemotePath ? words.back() : ""; std::unique_ptr n = explicitRemotePath ? nodebypath(destination.c_str(), &targetuser, &newname) : std::unique_ptr(api->getNodeByHandle(cwd)); if (explicitRemotePath && !n && autocreate) { const auto posLastSeparator = destination.find_last_of("/"); string destinationfolder(destination, 0, posLastSeparator); newname = string(destination, posLastSeparator + 1, destination.size()); auto baseNode = (destinationfolder.size() > 0 && destinationfolder.front() == '/') ? std::unique_ptr(api->getRootNode()) : std::unique_ptr(api->getNodeByHandle(cwd)); if (makedir(destinationfolder, true, baseNode.get()) == MCMD_OK) { n = nodebypath(destinationfolder.c_str()); } } if (!n) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Couldn't find destination folder: " << destination << ". Use -c to create folder structure"; return; } std::optional foregroundMultiTransfersListener; auto mayCreateForegroundListener = [&]() -> MegaCmdMultiTransferListener * { if (background) { return nullptr; } if (!foregroundMultiTransfersListener) { foregroundMultiTransfersListener.emplace(api, sandboxCMD, nullptr, clientID); } return &foregroundMultiTransfersListener.value(); }; ScopeGuard g([&] { if (foregroundMultiTransfersListener) { foregroundMultiTransfersListener->waitMultiEnd(); checkNoErrors(foregroundMultiTransfersListener->getFinalerror(), "upload"); if (foregroundMultiTransfersListener->getProgressinformed() || getCurrentThreadOutCode() == MCMD_OK ) { informProgressUpdate(PROGRESS_COMPLETE, foregroundMultiTransfersListener->getTotalbytes(), clientID); } } }); if (n->getType() == MegaNode::TYPE_FILE) { bool replacing = words.size() == 3 && !IsFolder(words[1]); if (!replacing) { setCurrentThreadOutCode(MCMD_INVALIDTYPE); LOG_err << "Destination is not valid (expected folder or alike)"; return; } unique_ptr pn(api->getNodeByHandle(n->getParentHandle())); if (!pn) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Destination is not valid. Parent folder cannot be found"; return; } auto &localPath = words[1]; #ifdef HAVE_GLOB_H if (!fs::exists(localPath) && hasWildCards(localPath)) { LOG_err << "Invalid target for wildcard expression: " << localPath << ". Folder expected"; setCurrentThreadOutCode(MCMD_INVALIDTYPE); return; } #endif uploadNode(*clflags, *cloptions, localPath, api, pn.get(), n->getName(), mayCreateForegroundListener()); return; } assert(n->getType() != MegaNode::TYPE_FILE); for (std::size_t i = 1; i <= numberOfLocalPaths; i++) { std::vector paths{words[i] == "." ? getLPWD() : words[i]}; #ifdef HAVE_GLOB_H if (!newname.size() && !fs::exists(words[i]) && hasWildCards(words[i])) { paths = resolvewildcard(words[i]); if (!paths.size()) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << words[i] << " not found"; } } #endif for (auto &path : paths) { uploadNode(*clflags, *cloptions, path, api, n.get(), newname, mayCreateForegroundListener()); } } return; } else if (words[0] == "log") { const bool cmdFlag = getFlag(clflags, "c"); const bool sdkFlag = getFlag(clflags, "s"); const bool noFlags = !cmdFlag && !sdkFlag; if (words.size() == 1) { if (cmdFlag || noFlags) { OUTSTREAM << "MEGAcmd log level = " << getLogLevelStr(loggerCMD->getCmdLoggerLevel()) << endl; } if (sdkFlag || noFlags) { OUTSTREAM << "SDK log level = " << getLogLevelStr(loggerCMD->getSdkLoggerLevel()) << endl; } } else { auto newLogLevelOpt = getLogLevelNum(words[1].c_str()); if (!newLogLevelOpt) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Invalid log level"; return; } const int newLogLevel = *newLogLevelOpt; if (cmdFlag || noFlags) { loggerCMD->setCmdLoggerLevel(newLogLevel); ConfigurationManager::savePropertyValue("cmdLogLevel", newLogLevel); OUTSTREAM << "MEGAcmd log level = " << getLogLevelStr(loggerCMD->getCmdLoggerLevel()) << endl; } if (sdkFlag || noFlags) { const bool jsonLogs = (newLogLevel == MegaApi::LOG_LEVEL_MAX); loggerCMD->setSdkLoggerLevel(newLogLevel); if (jsonLogs) { MegaApi::setLogJSON(MegaApi::JSON_LOG_CHUNK_RECEIVED | MegaApi::JSON_LOG_CHUNK_CONSUMED | MegaApi::JSON_LOG_SENDING | MegaApi::JSON_LOG_NONCHUNK_RECEIVED); MegaApi::setMaxPayloadLogSize(0); // Max size } else // revert to defaults { MegaApi::setLogJSON(MegaApi::JSON_LOG_CHUNK_CONSUMED | MegaApi::JSON_LOG_SENDING | MegaApi::JSON_LOG_NONCHUNK_RECEIVED); MegaApi::setMaxPayloadLogSize(10240/*PAYLOAD_LOG_DEFAULT_SIZE*/); // Default } ConfigurationManager::savePropertyValue("sdkLogLevel", newLogLevel); ConfigurationManager::savePropertyValue("jsonLogs", jsonLogs); OUTSTREAM << "SDK log level = " << getLogLevelStr(loggerCMD->getSdkLoggerLevel()) << endl; } } } else if (words[0] == "pwd") { if (!api->isFilesystemAvailable()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in."; return; } string cwpath = getCurrentPath(); OUTSTREAM << cwpath << endl; return; } else if (words[0] == "lcd") //this only makes sense for interactive mode { if (words.size() > 1) { LocalPath localpath = LocalPath::fromAbsolutePath(words[1]); if (mFsAccessCMD->chdirlocal(localpath)) // maybe this is already checked in chdir { LOG_debug << "Local folder changed to: " << words[1]; } else { setCurrentThreadOutCode(MCMD_INVALIDTYPE); LOG_err << "Not a valid folder: " << words[1]; } } else { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("lcd"); } return; } else if (words[0] == "lpwd") { string absolutePath = getLPWD(); OUTSTREAM << absolutePath << endl; return; } else if (words[0] == "ipc") { if (!api->isFilesystemAvailable()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in."; return; } if (words.size() > 1) { int action; string saction; if (getFlag(clflags, "a")) { action = MegaContactRequest::REPLY_ACTION_ACCEPT; saction = "Accept"; } else if (getFlag(clflags, "d")) { action = MegaContactRequest::REPLY_ACTION_DENY; saction = "Reject"; } else if (getFlag(clflags, "i")) { action = MegaContactRequest::REPLY_ACTION_IGNORE; saction = "Ignor"; } else { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("ipc"); return; } std::unique_ptr cr; string shandle = words[1]; handle thehandle = api->base64ToUserHandle(shandle.c_str()); if (shandle.find('@') != string::npos) { cr = getPcrByContact(shandle); } else { cr.reset(api->getContactRequestByHandle(thehandle)); } if (cr) { MegaCmdListener *megaCmdListener = new MegaCmdListener(api, NULL); api->replyContactRequest(cr.get(), action, megaCmdListener); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "reply ipc")) { OUTSTREAM << saction << "ed invitation by " << cr->getSourceEmail() << endl; } delete megaCmdListener; } else { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Could not find invitation " << shandle; } } else { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("ipc"); return; } return; } else if (words[0] == "https") { if (words.size() > 1 && (words[1] == "on" || words[1] == "off")) { bool onlyhttps = words[1] == "on"; MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); api->useHttpsOnly(onlyhttps,megaCmdListener); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "change https")) { OUTSTREAM << "File transfer now uses " << (api->usingHttpsOnly()?"HTTPS":"HTTP") << endl; ConfigurationManager::savePropertyValue("https", api->usingHttpsOnly()); } delete megaCmdListener; return; } else if (words.size() > 1) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("https"); return; } else { OUTSTREAM << "File transfer is done using " << (api->usingHttpsOnly()?"HTTPS":"HTTP") << endl; } return; } else if (words[0] == "graphics") { if (words.size() == 2 && (words[1] == "on" || words[1] == "off")) { bool enableGraphics = words[1] == "on"; api->disableGfxFeatures(!enableGraphics); ConfigurationManager::savePropertyValue("graphics", !api->areGfxFeaturesDisabled()); } else if (words.size() > 1) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("https"); return; } OUTSTREAM << "Graphic features " << (api->areGfxFeaturesDisabled()?"disabled":"enabled") << endl; return; } #ifndef _WIN32 else if (words[0] == "permissions") { bool filesflagread = getFlag(clflags, "files"); bool foldersflagread = getFlag(clflags, "folders"); bool filesflag = filesflagread || (!filesflagread && !foldersflagread); bool foldersflag = foldersflagread || (!filesflagread && !foldersflagread); bool setperms = getFlag(clflags, "s"); if ( (!setperms && words.size() > 1) || (setperms && words.size() != 2) || ( setperms && filesflagread && foldersflagread ) || (setperms && !filesflagread && !foldersflagread)) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("permissions"); return; } int permvalue = -1; if (setperms) { if (words[1].size() != 3) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Invalid permissions value: " << words[1]; } else { int owner = words[1].at(0) - '0'; int group = words[1].at(1) - '0'; int others = words[1].at(2) - '0'; if ( (owner < 6) || (owner == 6 && foldersflag) || (owner > 7) || (group < 0) || (group > 7) || (others < 0) || (others > 7) ) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Invalid permissions value: " << words[1]; } else { permvalue = (owner << 6) + ( group << 3) + others; } } } if (filesflag) { if (setperms && permvalue != -1) { api->setDefaultFilePermissions(permvalue); ConfigurationManager::savePropertyValue("permissionsFiles", readablePermissions(permvalue)); } int filepermissions = api->getDefaultFilePermissions(); int owner = (filepermissions >> 6) & 0x07; int group = (filepermissions >> 3) & 0x07; int others = filepermissions & 0x07; OUTSTREAM << "Default files permissions: " << owner << group << others << endl; } if (foldersflag) { if (setperms && permvalue != -1) { api->setDefaultFolderPermissions(permvalue); ConfigurationManager::savePropertyValue("permissionsFolders", readablePermissions(permvalue)); } int folderpermissions = api->getDefaultFolderPermissions(); int owner = (folderpermissions >> 6) & 0x07; int group = (folderpermissions >> 3) & 0x07; int others = folderpermissions & 0x07; OUTSTREAM << "Default folders permissions: " << owner << group << others << endl; } } #endif else if (words[0] == "deleteversions") { bool deleteall = getFlag(clflags, "all"); bool forcedelete = getFlag(clflags, "f"); if (deleteall && words.size()>1) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("deleteversions"); return; } if (deleteall) { string confirmationQuery("Are you sure todelete the version histories of all files? (Yes/No): "); int confirmationResponse = forcedelete?MCMDCONFIRM_YES:askforConfirmation(confirmationQuery); while ( (confirmationResponse != MCMDCONFIRM_YES) && (confirmationResponse != MCMDCONFIRM_NO) ) { confirmationResponse = askforConfirmation(confirmationQuery); } if (confirmationResponse == MCMDCONFIRM_YES) { MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); api->removeVersions(megaCmdListener); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "remove all versions")) { OUTSTREAM << "File versions deleted successfully. Please note that the current files were not deleted, just their history." << endl; } delete megaCmdListener; } } else { for (unsigned int i = 1; i < words.size(); i++) { if (isRegExp(words[i])) { vector> nodesToDeleteVersions = nodesbypath(words[i].c_str(), getFlag(clflags,"use-pcre")); if (nodesToDeleteVersions.size()) { for (const auto& node : nodesToDeleteVersions) { assert(node); int ret = deleteNodeVersions(node, api, forcedelete); forcedelete = forcedelete || (ret == MCMDCONFIRM_ALL); } } else { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "No node found: " << words[i]; } } else // non-regexp { std::unique_ptr n = nodebypath(words[i].c_str()); if (n) { int ret = deleteNodeVersions(n, api, forcedelete); forcedelete = forcedelete || (ret == MCMDCONFIRM_ALL); } else { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Node not found: " << words[i]; } } } } } #ifdef HAVE_LIBUV else if (words[0] == "webdav") { bool remove = getFlag(clflags, "d"); bool all = getFlag(clflags, "all"); if (words.size() == 1 && remove && !all) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("webdav"); return; } if (words.size() == 1 && !remove) { //List served nodes MegaNodeList *webdavnodes = api->httpServerGetWebDavAllowedNodes(); if (webdavnodes) { bool found = false; for (int a = 0; a < webdavnodes->size(); a++) { MegaNode *n= webdavnodes->get(a); if (n) { char *link = api->httpServerGetLocalWebDavLink(n); //notice this is not only consulting but also creating, //had it been deleted in the meantime this will recreate it if (link) { if (!found) { OUTSTREAM << "WEBDAV SERVED LOCATIONS:" << endl; } found = true; char * nodepath = api->getNodePath(n); OUTSTREAM << nodepath << ": " << link << endl; delete []nodepath; delete []link; } } } if(!found) { OUTSTREAM << "No webdav links found" << endl; } delete webdavnodes; } else { OUTSTREAM << "Webdav server might not running. Add a new location to serve." << endl; } return; } if (!remove) { //create new link: bool tls = getFlag(clflags, "tls"); int port = getintOption(cloptions, "port", 4443); bool localonly = !getFlag(clflags, "public"); string pathtocert = getOption(cloptions, "certificate", ""); string pathtokey = getOption(cloptions, "key", ""); bool serverstarted = api->httpServerIsRunning(); if (!serverstarted) { LOG_info << "Starting http server"; api->httpServerEnableFolderServer(true); // api->httpServerEnableOfflineAttribute(true); //TODO: we might want to offer this as parameter if (api->httpServerStart(localonly, port, tls, pathtocert.c_str(), pathtokey.c_str())) { ConfigurationManager::savePropertyValue("webdav_port", port); ConfigurationManager::savePropertyValue("webdav_localonly", localonly); ConfigurationManager::savePropertyValue("webdav_tls", tls); if (pathtocert.size()) { ConfigurationManager::savePropertyValue("webdav_cert", pathtocert); } if (pathtokey.size()) { ConfigurationManager::savePropertyValue("webdav_key", pathtokey); } } else { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Failed to initialize WEBDAV server. Ensure the port is free."; return; } } } //add/remove if (remove && all) { api->httpServerRemoveWebDavAllowedNodes(); api->httpServerStop(); list servedpaths; ConfigurationManager::savePropertyValueList("webdav_served_locations", servedpaths); ConfigurationManager::savePropertyValue("webdav_port", -1); //so as not to load server on startup OUTSTREAM << "Wevdav server stopped: no path will be served." << endl; } else { bool firstone = true; for (unsigned int i = 1; i < words.size(); i++) { string pathToServe = words[i]; if (remove) { processPath(pathToServe, getFlag(clflags,"use-pcre"), firstone, forwarderRemoveWebdavLocation, this); } else { processPath(pathToServe, getFlag(clflags,"use-pcre"), firstone, forwarderAddWebdavLocation, this); } } } } else if (words[0] == "ftp") { bool remove = getFlag(clflags, "d"); bool all = getFlag(clflags, "all"); if (words.size() == 1 && remove && !all) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("ftp"); return; } if (words.size() == 1 && !remove) { //List served nodes MegaNodeList *ftpnodes = api->ftpServerGetAllowedNodes(); if (ftpnodes) { bool found = false; for (int a = 0; a < ftpnodes->size(); a++) { MegaNode *n= ftpnodes->get(a); if (n) { char *link = api->ftpServerGetLocalLink(n); //notice this is not only consulting but also creating, //had it been deleted in the meantime this will recreate it if (link) { if (!found) { OUTSTREAM << "FTP SERVED LOCATIONS:" << endl; } found = true; char * nodepath = api->getNodePath(n); OUTSTREAM << nodepath << ": " << link << endl; delete []nodepath; delete []link; } } } if(!found) { OUTSTREAM << "No ftp links found" << endl; } delete ftpnodes; } else { OUTSTREAM << "Ftp server might not running. Add a new location to serve." << endl; } return; } if (!remove) { //create new link: bool tls = getFlag(clflags, "tls"); int port = getintOption(cloptions, "port", 4990); bool localonly = !getFlag(clflags, "public"); string dataPorts = getOption(cloptions, "data-ports"); size_t seppos = dataPorts.find("-"); int dataPortRangeBegin = 1500; istringstream is(dataPorts.substr(0,seppos)); is >> dataPortRangeBegin; int dataPortRangeEnd = dataPortRangeBegin + 100; if (seppos != string::npos && seppos < (dataPorts.size()+1)) { istringstream is(dataPorts.substr(seppos+1)); is >> dataPortRangeEnd; } string pathtocert = getOption(cloptions, "certificate", ""); string pathtokey = getOption(cloptions, "key", ""); if (tls && (!pathtocert.size() || !pathtokey.size())) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Path to certificate/key not indicated"; return; } bool serverstarted = api->ftpServerIsRunning(); if (!serverstarted) { if (api->ftpServerStart(localonly, port, dataPortRangeBegin, dataPortRangeEnd, tls, pathtocert.c_str(), pathtokey.c_str())) { ConfigurationManager::savePropertyValue("ftp_port", port); ConfigurationManager::savePropertyValue("ftp_port_data_begin", dataPortRangeBegin); ConfigurationManager::savePropertyValue("ftp_port_data_end", dataPortRangeEnd); ConfigurationManager::savePropertyValue("ftp_localonly", localonly); ConfigurationManager::savePropertyValue("ftp_tls", tls); if (pathtocert.size()) { ConfigurationManager::savePropertyValue("ftp_cert", pathtocert); } if (pathtokey.size()) { ConfigurationManager::savePropertyValue("ftp_key", pathtokey); } LOG_info << "Started ftp server at port " << port << ". Data Channel Port Range: " << dataPortRangeBegin << "-" << dataPortRangeEnd; } else { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Failed to initialize FTP server. Ensure the port is free."; return; } } } //add/remove if (remove && all) { api->ftpServerRemoveAllowedNodes(); api->ftpServerStop(); list servedpaths; ConfigurationManager::savePropertyValueList("ftp_served_locations", servedpaths); ConfigurationManager::savePropertyValue("ftp_port", -1); //so as not to load server on startup OUTSTREAM << "ftp server stopped: no path will be served." << endl; } else { bool firstone = true; for (unsigned int i = 1; i < words.size(); i++) { string pathToServe = words[i]; if (remove) { processPath(pathToServe, getFlag(clflags,"use-pcre"), firstone, forwarderRemoveFtpLocation, this); } else { processPath(pathToServe, getFlag(clflags,"use-pcre"), firstone, forwarderAddFtpLocation, this); } } } } #endif #ifdef ENABLE_SYNC else if (words[0] == "exclude") { bool excludeAdd = getFlag(clflags, "a"); bool excludeRemove = getFlag(clflags, "d"); if (excludeAdd == excludeRemove) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("exclude"); return; } { SyncIgnore::Args args; args.mAction = (excludeAdd ? SyncIgnore::Action::Add : SyncIgnore::Action::Remove); std::transform(words.begin() + 1, words.end(), std::inserter(args.mFilters, args.mFilters.end()), SyncIgnore::getFilterFromLegacyPattern); SyncIgnore::executeCommand(args); } { SyncIgnore::Args args; args.mAction = SyncIgnore::Action::Show; OUTSTREAM << endl; OUTSTREAM << "Contents of .megaignore.default:" << endl; SyncIgnore::executeCommand(args); } } else if (words[0] == "sync") { if (!api->isFilesystemAvailable()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in"; return; } if (!api->isLoggedIn()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in"; return; } bool pauseSync = getFlag(clflags, "p") || getFlag(clflags, "pause") || getFlag(clflags, "s") || getFlag(clflags, "disable"); bool enableSync = getFlag(clflags, "e") || getFlag(clflags, "r") || getFlag(clflags, "enable"); bool deleteSync = getFlag(clflags, "delete") || getFlag(clflags, "d") || getFlag(clflags, "remove"); bool showHandles = getFlag(clflags, "show-handles"); if (!onlyZeroOrOneOf(pauseSync, enableSync, deleteSync)) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Only one action (disable, enable, or remove) can be specified at a time"; LOG_err << " " << getUsageStr("sync"); return; } if (words.size() == 3) // add a sync { fs::path localPath = fs::absolute(words[1]); if (!fs::exists(localPath)) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Local directory " << words[1] << " does not exist"; return; } std::unique_ptr n = nodebypath(words[2].c_str()); if (!n) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Remote directory " << words[2] << " does not exist"; return; } SyncCommand::addSync(*api, localPath, *n); } else if (words.size() == 2) // manage a sync { string pathOrId = words[1]; auto sync = SyncCommand::getSync(*api, pathOrId); if (!sync) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Sync not found: " << pathOrId; return; } if (deleteSync) { SyncCommand::modifySync(*api, *sync, SyncCommand::ModifyOpts::Delete); } else { SyncCommand::modifySync(*api, *sync, enableSync ? SyncCommand::ModifyOpts::Enable : SyncCommand::ModifyOpts::Pause); // Print the updated sync state if we didnt' remove sync = SyncCommand::reloadSync(*api, std::move(sync)); if (!sync) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Sync not found while reloading: " << pathOrId; return; } auto syncIssues = mSyncIssuesManager.getSyncIssues(); ColumnDisplayer cd(clflags, cloptions); SyncCommand::printSync(*api, cd, showHandles, *sync, syncIssues); OUTSTREAM << cd.str(); } } else if (words.size() == 1) // show all syncs { auto syncIssues = mSyncIssuesManager.getSyncIssues(); auto syncList = std::unique_ptr(api->getSyncs()); assert(syncList); ColumnDisplayer cd(clflags, cloptions); SyncCommand::printSyncList(*api, cd, showHandles, *syncList, syncIssues); OUTSTREAM << cd.str(); if (!syncIssues.empty()) { OUTSTREAM << endl; LOG_err << "You have sync issues. Use the \"" << getCommandPrefixBasedOnMode() << "sync-issues\" command to display them."; } else if (SyncCommand::isAnySyncUploadDelayed(*api)) { OUTSTREAM << endl; OUTSTREAM << "Some of your \"Pending\" sync uploads are being delayed due to very frequent changes. They will be uploaded once the delay finishes. " << "Use the \"" << getCommandPrefixBasedOnMode() << "sync-config\" command to show the upload delay." << endl; } } else { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << getUsageStr("sync"); return; } } else if (words[0] == "sync-ignore") { if (!api->isFilesystemAvailable()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in"; return; } if (!api->isLoggedIn()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in"; return; } if (words.size() < 2) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("sync-ignore"); return; } bool ignoreShow = getFlag(clflags, "show"); bool ignoreAdd = getFlag(clflags, "add"); bool ignoreAddExclusion = getFlag(clflags, "add-exclusion"); bool ignoreRemove = getFlag(clflags, "remove"); bool ignoreRemoveExclusion = getFlag(clflags, "remove-exclusion"); if (!onlyZeroOrOneOf(ignoreShow, ignoreAdd, ignoreAddExclusion, ignoreRemove, ignoreRemoveExclusion)) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Only one action (show, add, add-exclusion, remove, or remove-exclusion) can be specified at a time"; LOG_err << " " << getUsageStr("sync-ignore"); return; } SyncIgnore::Args args; args.mAction = SyncIgnore::Action::Show; if (ignoreAdd || ignoreAddExclusion) { args.mAction = SyncIgnore::Action::Add; } else if (ignoreRemove || ignoreRemoveExclusion) { args.mAction = SyncIgnore::Action::Remove; } // Show cannot have filters if (args.mAction == SyncIgnore::Action::Show && words.size() != 2) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("sync-ignore"); return; } string pathOrId = words.back(); // the last word is (ID|localpath|"DEFAULT") if (toLower(pathOrId) != "default") { auto sync = SyncCommand::getSync(*api, pathOrId); if (!sync) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Sync " << pathOrId << " was not found"; return; } args.mMegaIgnoreDirPath = std::string(sync->getLocalFolder()); } auto filterInserter = [ignoreAddExclusion, ignoreRemoveExclusion] (const string& word) { if (ignoreAddExclusion || ignoreRemoveExclusion) { return "-" + word; } return word; }; std::transform(words.begin() + 1, words.end() - 1, std::inserter(args.mFilters, args.mFilters.end()), filterInserter); SyncIgnore::executeCommand(args); } else if (words[0] == "sync-config") { using namespace GlobalSyncConfig; if (words.size() != 1) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << getUsageStr("sync-config"); return; } auto duWaitSecsOpt = getFlag(clflags, "delayed-uploads-wait-seconds"); auto duMaxAttemptsOpt = getFlag(clflags, "delayed-uploads-max-attempts"); bool all = !duWaitSecsOpt && !duMaxAttemptsOpt; { auto duConfigOpt = DelayedUploads::getCurrentConfig(*api); if (!duConfigOpt) { LOG_err << "Failed to retrieve delayed sync uploads config"; return; } DelayedUploads::Config duConfig = *duConfigOpt; if (all || duWaitSecsOpt) { OUTSTREAM << "Delayed uploads wait time: " << duConfig.mWaitSecs << " seconds" << endl; } if (all || duMaxAttemptsOpt) { OUTSTREAM << "Max attempts until uploads are delayed: " << duConfig.mMaxAttempts << endl; } return; } } #endif else if (words[0] == "configure") { std::optional key; std::optional value; if (words.size() > 1) { key = words[1]; } if (words.size() > 2) { value= words[2]; } bool found = false; for (auto &vc : Instance::Get().getConfigurators()) { auto name = vc.mKey.c_str(); if (key && *key != name) continue; found = true; if (value) { if (vc.mValidator && !vc.mValidator.value()(value->data())) { LOG_err << "Failed to set " << vc.mKey << " (" << vc.mDescription << "). Invalid value"; setCurrentThreadOutCode(MCMD_EARGS); return; } if (!vc.mSetter(api, std::string(name), value->data())) { if (getCurrentThreadOutCode() == MCMD_OK) // presuming error would be logged otherwise { LOG_err << "Failed to set " << vc.mKey << " (" << vc.mDescription << "). Setting failed"; setCurrentThreadOutCode(MCMD_EARGS); } return; } ConfigurationManager::saveProperty(name, value->data()); } auto &getter = vc.mMegaApiGetter ? *vc.mMegaApiGetter : vc.mGetter; auto newValueOpt = getter(api, name); if (newValueOpt) { OUTSTREAM << name << " = " << *newValueOpt << std::endl; if (value && *value != *newValueOpt) { LOG_warn << "Setting " << vc.mKey << " resulted in a value different than the one intended. " << *value << " vs " << *newValueOpt; } } else if (key) { LOG_err << "Configuration value is unset: " << *key; setCurrentThreadOutCode(MCMD_NOTFOUND); } } if (!found) { LOG_err << "Provided configuration key does not exist: " << *key; setCurrentThreadOutCode(MCMD_EARGS); } } else if (words[0] == "cancel") { MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); api->cancelAccount(megaCmdListener); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "cancel account")) { OUTSTREAM << "Account pending cancel confirmation. You will receive a confirmation link. Use \"confirmcancel\" with the provided link to confirm the cancellation" << endl; } delete megaCmdListener; } else if (words[0] == "confirmcancel") { if (words.size() < 2) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("confirmcancel"); return; } const char * confirmlink = words[1].c_str(); if (words.size() > 2) { const char * pass = words[2].c_str(); confirmCancel(confirmlink, pass); } else if (isCurrentThreadInteractive()) { link = confirmlink; confirmingcancel = true; setprompt(LOGINPASSWORD); } else { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Extra args required in non-interactive mode. Usage: " << getUsageStr("confirmcancel"); } } else if (words[0] == "login") { LoginGuard loginGuard; int clientID = getintOption(cloptions, "clientID", -1); if (api->isLoggedIn()) { setCurrentThreadOutCode(MCMD_INVALIDSTATE); LOG_err << "Already logged in. Please log out first."; return; } if (words.size() < 2) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("login"); return; } bool accountLogin = words[1].find('@') != std::string::npos; bool folderLinkLogin = !accountLogin && words[1].find('#') != std::string::npos; bool resumeFolderLink = getFlag(clflags, "resume"); string authKey = getOption(cloptions, "auth-key", ""); if (!folderLinkLogin) { if (resumeFolderLink) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Explicit resumption only required for folder links logins."; return; } if (!authKey.empty()) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Auth key only required for login in writable folder links."; return; } } if (accountLogin) { if (words.size() > 2) { MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL,NULL,clientID); sandboxCMD->resetSandBox(); api->login(words[1].c_str(), words[2].c_str(), megaCmdListener); if (actUponLogin(megaCmdListener) == MegaError::API_EMFAREQUIRED ) { MegaCmdListener *megaCmdListener2 = new MegaCmdListener(NULL,NULL,clientID); string pin2fa = getOption(cloptions, "auth-code", ""); if (!pin2fa.size()) { pin2fa = askforUserResponse("Enter the code generated by your authentication app: "); } LOG_verbose << " Using confirmation pin: " << pin2fa; api->multiFactorAuthLogin(words[1].c_str(), words[2].c_str(), pin2fa.c_str(), megaCmdListener2); actUponLogin(megaCmdListener2); delete megaCmdListener2; return; } delete megaCmdListener; } else { login = words[1]; if (isCurrentThreadInteractive()) { setprompt(LOGINPASSWORD); } else { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Extra args required in non-interactive mode. Usage: " << getUsageStr("login"); } } } else if (folderLinkLogin) // folder link indicator { string publicLink = words[1]; if (!decryptLinkIfEncrypted(api, publicLink, cloptions)) { return; } std::unique_ptrmegaCmdListener = std::make_unique(nullptr); sandboxCMD->resetSandBox(); api->loginToFolder(publicLink.c_str(), authKey.empty() ? nullptr : authKey.c_str(), resumeFolderLink, megaCmdListener.get()); actUponLogin(megaCmdListener.get()); return; } else // session resumption { LOG_info << "Resuming session..."; MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); sandboxCMD->resetSandBox(); api->fastLogin(words[1].c_str(), megaCmdListener); actUponLogin(megaCmdListener); delete megaCmdListener; return; } return; } else if (words[0] == "psa") { int psanum = sandboxCMD->lastPSAnumreceived; #ifndef NDEBUG if (words.size() >1) { psanum = toInteger(words[1], 0); } #endif bool discard = getFlag(clflags, "discard"); if (discard) { MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); api->setPSA(psanum, megaCmdListener); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "set psa " + SSTR(psanum))) { OUTSTREAM << "PSA discarded" << endl; } delete megaCmdListener; } if (!checkAndInformPSA(NULL, true) && !discard) // even when discarded: we need to read the next { OUTSTREAM << "No PSA available" << endl; setCurrentThreadOutCode(MCMD_NOTFOUND); } } else if (words[0] == "mount") { if (!api->isFilesystemAvailable()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in."; return; } listtrees(); verifySharedFolders(api); return; } else if (words[0] == "share") { if (!api->isFilesystemAvailable()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in."; return; } string with = getOption(cloptions, "with", ""); if (getFlag(clflags, "a") && ( "" == with )) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " Required --with=user"; LOG_err << " " << getUsageStr("share"); return; } string slevel = getOption(cloptions, "level", "NONE"); int level_NOT_present_value = -214; int level; if (slevel == "NONE") { level = level_NOT_present_value; } else { level = getShareLevelNum(slevel.c_str()); } if (( level != level_NOT_present_value ) && (( level < -1 ) || ( level > 3 ))) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Invalid level of access"; return; } bool listPending = getFlag(clflags, "p"); if (words.size() <= 1) { words.push_back(string(".")); //cwd } for (int i = 1; i < (int)words.size(); i++) { unescapeifRequired(words[i]); if (isRegExp(words[i])) { vector> nodes = nodesbypath(words[i].c_str(), getFlag(clflags,"use-pcre")); if (nodes.empty()) { setCurrentThreadOutCode(MCMD_NOTFOUND); if (words[i].find("@") != string::npos) { LOG_err << "Could not find " << words[i] << ". Use --with=" << words[i] << " to specify the user to share with"; } else { LOG_err << "Node not found: " << words[i]; } } for (const auto& n : nodes) { assert(n); if (getFlag(clflags, "a")) { LOG_debug << " sharing ... " << n->getName() << " with " << with; if (level == level_NOT_present_value) { level = MegaShare::ACCESS_READ; } if (n->getType() == MegaNode::TYPE_FILE) { setCurrentThreadOutCode(MCMD_INVALIDTYPE); LOG_err << "Cannot share file: " << n->getName() << ". Only folders allowed. You can send file to user's inbox with cp (see \"cp --help\")"; } else { shareNode(n.get(), with, level); } } else if (getFlag(clflags, "d")) { if ("" != with) { LOG_debug << " deleting share ... " << n->getName() << " with " << with; disableShare(n.get(), with); } else { MegaShareList* outShares = api->getOutShares(n.get()); if (outShares) { for (int i = 0; i < outShares->size(); i++) { if (outShares->get(i)->getNodeHandle() == n->getHandle()) { LOG_debug << " deleting share ... " << n->getName() << " with " << outShares->get(i)->getUser(); disableShare(n.get(), outShares->get(i)->getUser()); } } delete outShares; } } } else { if (( level != level_NOT_present_value ) || ( with != "" )) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Unexpected option received. To create/modify a share use -a"; } else if (listPending) { dumpListOfAllShared(n.get(), words[i]); } else { dumpListOfShared(n.get(), words[i]); } } } } else // non-regexp { std::unique_ptr n = nodebypath(words[i].c_str()); if (n) { if (getFlag(clflags, "a")) { LOG_debug << " sharing ... " << n->getName() << " with " << with; if (level == level_NOT_present_value) { level = MegaShare::ACCESS_READ; } shareNode(n.get(), with, level); } else if (getFlag(clflags, "d")) { if ("" != with) { LOG_debug << " deleting share ... " << n->getName() << " with " << with; disableShare(n.get(), with); } else { std::unique_ptr outShares(api->getOutShares(n.get())); if (outShares) { for (int i = 0; i < outShares->size(); i++) { if (outShares->get(i)->getNodeHandle() == n->getHandle()) { LOG_debug << " deleting share ... " << n->getName() << " with " << outShares->get(i)->getUser(); disableShare(n.get(), outShares->get(i)->getUser()); } } } } } else { if ((level != level_NOT_present_value ) || ( with != "" )) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Unexpected option received. To create/modify a share use -a"; } else if (listPending) { dumpListOfAllShared(n.get(), words[i]); } else { dumpListOfShared(n.get(), words[i]); } } } else { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Node not found: " << words[i]; } } } verifySharedFolders(api); return; } else if (words[0] == "users") { if (!api->isFilesystemAvailable()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in."; return; } bool verify = getFlag(clflags, "verify"); bool unverify = getFlag(clflags, "unverify"); if (verify || unverify) { if (!checkExactlyNArgs(words, 2)) { return; } const auto &contact = words[1]; std::unique_ptr user(api->getContact(contact.c_str())); if (!user) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Contact not found."; return; } auto megaCmdListener = std::make_unique(nullptr); if (unverify) { api->resetCredentials(user.get(), megaCmdListener.get()); } else { api->verifyCredentials(user.get(), megaCmdListener.get()); } megaCmdListener->wait(); if (megaCmdListener->getError()->getErrorCode() == MegaError::API_EINCOMPLETE) { setCurrentThreadOutCode(megaCmdListener->getError()->getErrorCode()); LOG_err << "Failed to " << " set contact as " << (unverify ? "no longer " : "") << "verified, your account's security may need upgrading"; } else if (checkNoErrors(megaCmdListener->getError(), unverify ? "unverify credentials" : "verify credentials")) { OUTSTREAM << "Contact " << contact << " set as " << (unverify ? "no longer " : "") << "verified." << endl; } return; } if (getFlag(clflags, "help-verify")) { if (words.size() == 1) // General information { OUTSTREAM << "In order to share data with your contacts you may need to verify them." << endl << endl << "Verifying means ensuring that the contact is who he/she claims to be." << endl << "To ensure that, both of you will need to share your credentials," << endl << " i.e. some numbers that uniquely identify you." << endl << "You can see a contact's credentials (and yours) and instructions on verifying," << endl << " by typing \"" << getCommandPrefixBasedOnMode() << "users --help-verify contact@email\"." << endl << endl << "To see which contacts are not verified, you can list them using \"" << getCommandPrefixBasedOnMode() << "users -n\"" << endl << "If you want the above listing to include information regarding your share folders," << endl << " type \"" < user(api->getContact(contact.c_str())); if (!user) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Contact not found."; return; } auto megaCmdListener = std::make_unique(nullptr); api->getUserCredentials(user.get(), megaCmdListener.get()); megaCmdListener->wait(); if (!checkNoErrors(megaCmdListener->getError(), "get user credentials")) { return; } auto contactCredentials = megaCmdListener->getRequest()->getPassword(); if (!contactCredentials) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Contact credentials not found."; return; } std::unique_ptr myCredentials(api->getMyCredentials()); if (!myCredentials) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Own credentials not found."; return; } auto beautifyCreds = [](std::string x) { auto nspaces = x.size() / 4; for (size_t i = 1 ; i < nspaces ; i++) { x.insert( i * 4 + i - 1, i == (nspaces / 2) ? "\n" : " "); } return x; }; OUTSTREAM << "Updated verification credentials were received for your contact: "; OUTSTREAM << contact << endl; { std::stringstream ss; ss << "Your Contact's credentials:\n"; ss << beautifyCreds(contactCredentials); printCenteredContentsT(OUTSTREAM, ss.str(), 32, true); } { std::stringstream ss; ss << "Your credentials:\n"; ss << beautifyCreds(myCredentials.get()); printCenteredContentsT(OUTSTREAM, ss.str(), 32, true); } OUTSTREAM << "Compare the listed credentials with the ones reported by your contact." << endl; OUTSTREAM << "This is best done in real life by meeting face to face.\n" "If you have another already-verified channel such as verified OTR or PGP, you may also use that."; OUTSTREAM << endl << "If both credentials match, type \"" << getCommandPrefixBasedOnMode() << "users --verify " << contact << "\" to set the contact as verified." << endl; OUTSTREAM << endl << "Important: verification is two sided. You need to tell your contact to do the same for you, using MEGAcmd or MEGA website to verify your credentials." << endl; return; } if (getFlag(clflags, "d") && ( words.size() <= 1 )) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Contact to delete not specified"; return; } MegaUserList* usersList = api->getContacts(); if (usersList) { for (int i = 0; i < usersList->size(); i++) { MegaUser *user = usersList->get(i); if (getFlag(clflags, "d") && ( words.size() > 1 ) && ( words[1] == user->getEmail())) { MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); api->removeContact(user, megaCmdListener); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "delete contact")) { OUTSTREAM << "Contact " << words[1] << " removed successfully" << endl; } delete megaCmdListener; } else { if (!(( user->getVisibility() != MegaUser::VISIBILITY_VISIBLE ) && !getFlag(clflags, "h"))) { auto email = user->getEmail(); std::string nameOrEmail; if (getFlag(clflags,"n")) //Show Names { // name: auto megaCmdListener = std::make_unique(nullptr); api->getUserAttribute(user, ATTR_FIRSTNAME, megaCmdListener.get()); megaCmdListener->wait(); if (megaCmdListener->getError()->getErrorCode() == MegaError::API_OK && megaCmdListener->getRequest()->getText() && *megaCmdListener->getRequest()->getText() // not empty ) { nameOrEmail += megaCmdListener->getRequest()->getText(); } // surname: megaCmdListener.reset(new MegaCmdListener(nullptr)); api->getUserAttribute(user, ATTR_LASTNAME, megaCmdListener.get()); megaCmdListener->wait(); if (megaCmdListener->getError()->getErrorCode() == MegaError::API_OK && megaCmdListener->getRequest()->getText() && *megaCmdListener->getRequest()->getText() // not empty ) { if (!nameOrEmail.empty()) { nameOrEmail+=" "; } nameOrEmail += megaCmdListener->getRequest()->getText(); } if (!nameOrEmail.empty()) { OUTSTREAM << "[" << nameOrEmail << "] "; } } if (nameOrEmail.empty()) { nameOrEmail = email; } OUTSTREAM << email; if (user->getTimestamp()) { OUTSTREAM << ". Contact since " << getReadableTime(user->getTimestamp(), getTimeFormatFromSTR(getOption(cloptions, "time-format","RFC2822"))); } OUTSTREAM << ". " << visibilityToString(user->getVisibility()) << endl; bool printedSomeUnaccesibleInShare = false; bool printedSomeUnaccesibleOutShare = false; auto printShares = [&, this](std::unique_ptr &shares, const std::string &title, bool isInShare = false) { if (!shares) { return; } bool first_share = true; for (int j = 0, total = shares->size(); j < total; j++) { auto share = shares->get(j); assert(share); if (!strcmp(share->getUser(), email)) { bool thisOneisUnverified = !share->isVerified(); std::unique_ptr n (api->getNodeByHandle(share->getNodeHandle())); if (n) { if (first_share) { OUTSTREAM << "> " << title << ":" << endl; first_share = false; } if (thisOneisUnverified) { if (isInShare) { printedSomeUnaccesibleInShare = true; OUTSTREAM << " (**)"; } else { printedSomeUnaccesibleOutShare = true; OUTSTREAM << " (*)"; } } else { OUTSTREAM << " "; } if (isInShare) { OUTSTREAM << "//from/"; } if (isInShare && (thisOneisUnverified || !n->isNodeKeyDecrypted())) { OUTSTREAM << email << (thisOneisUnverified ? ":[UNVERIFIED]" : "[UNDECRYPTABLE]") << endl; } else if (isInShare) { dumpNode(n.get(), getTimeFormatFromSTR(getOption(cloptions, "time-format","RFC2822")), clflags, cloptions, 2, false, 0, getDisplayPath("/", n.get()).c_str()); } else //outShare: { string path = getDisplayPath("", n.get()); printOutShareInfo(path.c_str(), nullptr/*dont print email*/, share->getAccess(), share->isPending(), share->isVerified()); } } } } }; bool isUserVerified = api->areCredentialsVerified(user); bool doPrint(getFlag(clflags, "s")); if (doPrint) { std::unique_ptr outSharesByAllUsers (api->getOutShares()); // this one retrieves both verified & unverified ones printShares(outSharesByAllUsers, std::string("Folders shared with ") + nameOrEmail); std::unique_ptr inSharesByAllUsers (api->getInSharesList()); // this one does not return unverified ones printShares(inSharesByAllUsers , std::string("Folders shared by ") + nameOrEmail, true); std::unique_ptr unverifiedInShares (api->getUnverifiedInShares()); printShares(unverifiedInShares , std::string("Folders shared by ") + nameOrEmail, true); } assert(!printedSomeUnaccesibleInShare || !isUserVerified); if (!isUserVerified) { std::stringstream ss; ss << "Contact [" << nameOrEmail << "] is not verified"; if (printedSomeUnaccesibleInShare || printedSomeUnaccesibleOutShare) { ss << "\n"; if (printedSomeUnaccesibleOutShare) { ss << "Shares marked with (*) may not be accessible by your contact.\n"; } if (printedSomeUnaccesibleInShare) { ss << "Shares marked with (**) may not be accessible by you.\n"; } } ss << "\nType \"" << getCommandPrefixBasedOnMode() <<"users --help-verify " << email << "\" to get instructions on verification" << endl; printCenteredContentsT(OUTSTREAM, ss.str(), 78, true); OUTSTREAM << endl; } } } } delete usersList; } return; } else if (words[0] == "mkdir") { if (!api->isFilesystemAvailable()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in."; return; } int globalstatus = MCMD_OK; if (words.size()<2) { globalstatus = MCMD_EARGS; } bool printusage = false; for (unsigned int i = 1; i < words.size(); i++) { unescapeifRequired(words[i]); //git first existing node in the asked path: MegaNode *baseNode; string rest = words[i]; if (rest.find("//bin/") == 0) { baseNode = api->getRubbishNode(); rest = rest.substr(6); }//elseif //in/ else if(rest.find("/") == 0) { baseNode = api->getRootNode(); rest = rest.substr(1); } else { baseNode = api->getNodeByHandle(cwd); } while (baseNode && rest.length()) { size_t possep = rest.find_first_of("/"); if (possep == string::npos) { break; } string next = rest.substr(0, possep); if (next == ".") { rest = rest.substr(possep + 1); continue; } else if(next == "..") { MegaNode *aux = baseNode; baseNode = api->getNodeByHandle(baseNode->getParentHandle()); if (aux!=baseNode) // let's be paranoid { delete aux; } } else { MegaNodeList *children = api->getChildren(baseNode); if (children) { bool found = false; for (int i = 0; i < children->size(); i++) { MegaNode *child = children->get(i); if (next == child->getName()) { MegaNode *aux = baseNode; baseNode = child->copy(); found = true; if (aux!=baseNode) // let's be paranoid { delete aux; } break; } } delete children; if (!found) { break; } } } rest = rest.substr(possep + 1); } if (baseNode) { int status = makedir(rest,getFlag(clflags, "p"),baseNode); if (status != MCMD_OK) { globalstatus = status; } if (status == MCMD_EARGS) { printusage = true; } delete baseNode; } else { setCurrentThreadOutCode(MCMD_INVALIDSTATE); LOG_err << "Folder navigation failed"; return; } } setCurrentThreadOutCode(globalstatus); if (printusage) { LOG_err << " " << getUsageStr("mkdir"); } return; } else if (words[0] == "attr") { if (!api->isFilesystemAvailable()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in."; return; } if (words.size() <= 1) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr(words[0].c_str()); return; } bool removingAttr(getFlag(clflags, "d")); bool settingattr(getFlag(clflags, "s")); bool forceCustom = getFlag(clflags, "force-non-official"); string nodePath = words.size() > 1 ? words[1] : ""; string attribute = words.size() > 2 ? words[2] : ""; string attrValue = words.size() > 3 ? words[3] : ""; bool listAll(attribute.empty()); std::unique_ptr node (nodebypath(nodePath.c_str()) ); if (!node) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Couldn't find node: " << nodePath; return; } auto isOfficial = [](const std::string &what) { for ( auto &a : {"s4"}) { if (what == a) { return true; } } return false; }; if (settingattr || removingAttr) { if (attribute.empty()) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Attribute not specified"; LOG_err << " " << getUsageStr(words[0].c_str()); return; } auto megaCmdListener = std::make_unique(nullptr); if (forceCustom || !isOfficial(attribute)) { const char *cattrValue = removingAttr ? NULL : attrValue.c_str(); api->setCustomNodeAttribute(node.get(), attribute.c_str(), cattrValue, megaCmdListener.get()); } else if (attribute == "s4") { const char *cattrValue = removingAttr ? NULL : attrValue.c_str(); api->setNodeS4(node.get(), cattrValue, megaCmdListener.get()); } else { assert(false); LOG_err << "Not implemented official attribute support"; setCurrentThreadOutCode(MCMD_INVALIDTYPE); return; } megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "set node attribute: " + attribute)) { OUTSTREAM << "Node attribute " << attribute << ( removingAttr ? " removed" : " updated" ) << " correctly" << endl; //reload the node for printing updated values node.reset(api->getNodeByHandle(megaCmdListener->getRequest()->getNodeHandle())); if (!node) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Couldn't find node after updating its attribute: " << nodePath; return; } } } //List node custom attributes std::unique_ptr attrlist (node->getCustomAttrNames()); if (attrlist) { if (listAll) { OUTSTREAM << "The node has " << attrlist->size() << " custom attributes:" << endl; } for (int a = 0; a < attrlist->size(); a++) { string iattr = attrlist->get(a); if (listAll || ( attribute == iattr && (forceCustom || !isOfficial(iattr)))) { const char* iattrval = node->getCustomAttr(iattr.c_str()); if (getFlag(clflags, "print-only-value")) { OUTSTREAM << ( iattrval ? iattrval : "NULL" ) << endl; } else { OUTSTREAM << "\t" << iattr << " = " << ( iattrval ? iattrval : "NULL" ) << endl; } } } } // List official node attributes: if (listAll || !forceCustom) //otherwise no sense in listing official ones { bool showOficialsHeader(listAll); using NameGetter = std::pair>; for (auto & pair : { NameGetter{"s4", [](MegaNode *node){return node->getS4();}} }) { if (!listAll && ( attribute != pair.first )) { continue; // looking for another one } auto officialAttrValue = pair.second(node.get()); if (officialAttrValue && strlen(officialAttrValue)) { if (showOficialsHeader) { OUTSTREAM << "Official attributes:" << endl; showOficialsHeader = false; } if (getFlag(clflags, "print-only-value")) { OUTSTREAM << ( pair.second(node.get()) ) << endl; } else { OUTSTREAM << "\t" << pair.first << " = " << pair.second(node.get()) << endl; } } } } return; } else if (words[0] == "userattr") { if (!api->isFilesystemAvailable()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in."; return; } bool settingattr = getFlag(clflags, "s"); bool listattrs = getFlag(clflags, "list"); bool listall = words.size() == 1; int attribute = api->userAttributeFromString(words.size() > 1 ? words[1].c_str() : "-1"); string attrValue = words.size() > 2 ? words[2] : ""; string user = getOption(cloptions, "user", ""); if (settingattr && user.size()) { LOG_err << "Can't change other user attributes"; return; } if (settingattr) { if (attribute != -1) { const char *catrn = api->userAttributeToString(attribute); string attrname = catrn; delete [] catrn; MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); api->setUserAttribute(attribute, attrValue.c_str(), megaCmdListener); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), string("set user attribute ") + attrname)) { OUTSTREAM << "User attribute " << attrname << " updated correctly" << endl; printUserAttribute(attribute, user, listattrs); } delete megaCmdListener; } else { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Attribute not specified"; LOG_err << " " << getUsageStr("userattr"); return; } } else // list { if (listall) { int a = 0; bool found = false; do { found = printUserAttribute(a, user, listattrs); a++; } while (found && listall); } else { if (!printUserAttribute(attribute, user, listattrs)) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Attribute not found: " << words[1]; } } } return; } else if (words[0] == "thumbnail") { if (!api->isFilesystemAvailable()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in."; return; } if (words.size() > 1) { string nodepath = words[1]; string path = words.size() > 2 ? words[2] : "./"; std::unique_ptr n = nodebypath(nodepath.c_str()); if (n) { MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); bool setting = getFlag(clflags, "s"); if (setting) { api->setThumbnail(n.get(), path.c_str(), megaCmdListener); } else { api->getThumbnail(n.get(), path.c_str(), megaCmdListener); } megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), ( setting ? "set thumbnail " : "get thumbnail " ) + nodepath + " to " + path)) { OUTSTREAM << "Thumbnail for " << nodepath << ( setting ? " loaded from " : " saved in " ) << megaCmdListener->getRequest()->getFile() << endl; } delete megaCmdListener; } else { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << nodepath << ": No such file or directory"; } } else { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr(words[0].c_str()); return; } return; } else if (words[0] == "preview") { if (!api->isFilesystemAvailable()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in."; return; } if (words.size() > 1) { string nodepath = words[1]; string path = words.size() > 2 ? words[2] : "./"; std::unique_ptr n = nodebypath(nodepath.c_str()); if (n) { MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); bool setting = getFlag(clflags, "s"); if (setting) { api->setPreview(n.get(), path.c_str(), megaCmdListener); } else { api->getPreview(n.get(), path.c_str(), megaCmdListener); } megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), ( setting ? "set preview " : "get preview " ) + nodepath + " to " + path)) { OUTSTREAM << "Preview for " << nodepath << ( setting ? " loaded from " : " saved in " ) << megaCmdListener->getRequest()->getFile() << endl; } delete megaCmdListener; } else { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << nodepath << ": No such file or directory"; } } else { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr(words[0].c_str()); return; } return; } else if (words[0] == "tree") { vector newcom; newcom.push_back("ls"); (*clflags)["tree"] = 1; if (words.size()>1) { for (vector::iterator it = ++words.begin(); it != words.end();it++) { newcom.push_back(*it); } } return executecommand(newcom, clflags, cloptions); } else if (words[0] == "debug") { vector newcom; newcom.push_back("log"); newcom.push_back("5"); return executecommand(newcom, clflags, cloptions); } else if (words[0] == "passwd") { string confirmationQuery("Changing password will close all your sessions. Are you sure?(Yes/No): "); bool force = getFlag(clflags, "f"); int confirmationResponse = force?MCMDCONFIRM_ALL:askforConfirmation(confirmationQuery); if (confirmationResponse != MCMDCONFIRM_YES && confirmationResponse != MCMDCONFIRM_ALL) { return; } if (api->isLoggedIn()) { if (words.size() == 1) { if (isCurrentThreadInteractive()) { setprompt(NEWPASSWORD); } else { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Extra args required in non-interactive mode. Usage: " << getUsageStr("passwd"); } } else if (words.size() == 2) { string pin2fa = getOption(cloptions, "auth-code", ""); changePassword(words[1].c_str(), pin2fa); } else { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("passwd"); } } else { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in."; } return; } else if (words[0] == "speedlimit") { bool uploadSpeed = getFlag(clflags, "u"); bool downloadSpeed = getFlag(clflags, "d"); bool uploadCons = getFlag(clflags, "upload-connections"); bool downloadCons = getFlag(clflags, "download-connections"); bool moreThanOne = !onlyZeroOrOneOf(uploadSpeed, downloadSpeed, uploadCons, downloadCons); bool noParam = onlyZeroOf(uploadSpeed, downloadSpeed, uploadCons, downloadCons); bool hr = getFlag(clflags,"h"); if (words.size() > 2 || moreThanOne) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("speedlimit"); return; } if (words.size() > 1) // setting { long long value = textToSize(words[1].c_str()); if (value == -1) { string s = words[1] + "B"; value = textToSize(s.c_str()); } if (noParam) { api->setMaxDownloadSpeed(value); api->setMaxUploadSpeed(value); ConfigurationManager::savePropertyValue("maxspeedupload", value); ConfigurationManager::savePropertyValue("maxspeeddownload", value); } else if (uploadSpeed) { api->setMaxUploadSpeed(value); ConfigurationManager::savePropertyValue("maxspeedupload", value); } else if (downloadSpeed) { api->setMaxDownloadSpeed(value); ConfigurationManager::savePropertyValue("maxspeeddownload", value); } else if (uploadCons || downloadCons) { auto megaCmdListener = std::make_unique(nullptr); api->setMaxConnections(uploadCons ? 1 : 0, value, megaCmdListener.get()); if (!checkNoErrors(megaCmdListener.get(), uploadCons ? "change max upload connections" : "change max download connections")) { return; } ConfigurationManager::savePropertyValue(uploadCons ? "maxuploadconnections" : "maxdownloadconnections", value); } } // listing: if (noParam) { long long us = api->getMaxUploadSpeed(); long long ds = api->getMaxDownloadSpeed(); OUTSTREAM << "Upload speed limit = " << (us?sizeToText(us,false,hr):"unlimited") << ((us && hr)?"/s":(us?" B/s":"")) << endl; OUTSTREAM << "Download speed limit = " << (ds?sizeToText(ds,false,hr):"unlimited") << ((ds && hr)?"/s":(us?" B/s":"")) << endl; auto upConns = ConfigurationManager::getConfigurationValue("maxuploadconnections", -1); auto downConns = ConfigurationManager::getConfigurationValue("maxdownloadconnections", -1); if (upConns != -1) { OUTSTREAM << "Upload max connections = " << upConns << std::endl; } if (downConns != -1) { OUTSTREAM << "Download max connections = " << downConns << std::endl;; } } else if (uploadSpeed) { long long us = api->getMaxUploadSpeed(); OUTSTREAM << "Upload speed limit = " << (us?sizeToText(us,false,hr):"unlimited") << ((us && hr)?"/s":(us?" B/s":"")) << endl; } else if (downloadSpeed) { long long ds = api->getMaxDownloadSpeed(); OUTSTREAM << "Download speed limit = " << (ds?sizeToText(ds,false,hr):"unlimited") << ((ds && hr)?"/s":(ds?" B/s":"")) << endl; } else if (uploadCons || downloadCons) { if (uploadCons) { auto upConns = ConfigurationManager::getConfigurationValue("maxuploadconnections", -1); if (upConns != -1) { OUTSTREAM << "Upload max connections = " << upConns << std::endl;; } } else { auto downConns = ConfigurationManager::getConfigurationValue("maxdownloadconnections", -1); if (downConns != -1) { OUTSTREAM << "Download max connections = " << downConns << std::endl;; } } } return; } else if (words[0] == "invite") { if (!api->isFilesystemAvailable()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in."; return; } if (words.size() > 1) { string email = words[1]; if (!isValidEmail(email)) { setCurrentThreadOutCode(MCMD_INVALIDEMAIL); LOG_err << "No valid email provided"; LOG_err << " " << getUsageStr("invite"); } else { int action = MegaContactRequest::INVITE_ACTION_ADD; if (getFlag(clflags, "d")) { action = MegaContactRequest::INVITE_ACTION_DELETE; } if (getFlag(clflags, "r")) { action = MegaContactRequest::INVITE_ACTION_REMIND; } string message = getOption(cloptions, "message", ""); MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); api->inviteContact(email.c_str(), message.c_str(), action, megaCmdListener); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), action==MegaContactRequest::INVITE_ACTION_DELETE?"remove invitation":"(re)invite user")) { OUTSTREAM << "Invitation to user: " << email << " " << (action==MegaContactRequest::INVITE_ACTION_DELETE?"removed":"sent") << endl; } else if (megaCmdListener->getError()->getErrorCode() == MegaError::API_EACCESS) { ostringstream os; os << "Reminder not yet available: " << " available after 15 days"; MegaContactRequestList *ocrl = api->getOutgoingContactRequests(); if (ocrl) { for (int i = 0; i < ocrl->size(); i++) { if (ocrl->get(i)->getTargetEmail() && megaCmdListener->getRequest()->getEmail() && !strcmp(ocrl->get(i)->getTargetEmail(), megaCmdListener->getRequest()->getEmail())) { os << " (" << getReadableTime(getTimeStampAfter(ocrl->get(i)->getModificationTime(), "15d")) << ")"; } } delete ocrl; } LOG_err << os.str(); } delete megaCmdListener; } } return; } else if (words[0] == "errorcode") { if (words.size() != 2) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("errorcode"); return; } int errCode = -1999; istringstream is(words[1]); is >> errCode; if (errCode == -1999) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Invalid error code: " << words[1]; return; } if (errCode > 0) { errCode = -errCode; } string errString = getMCMDErrorString(errCode); if (errString == "UNKNOWN") { errString = MegaError::getErrorString(errCode); } OUTSTREAM << errString << endl; } else if (words[0] == "signup") { if (api->isLoggedIn()) { setCurrentThreadOutCode(MCMD_INVALIDSTATE); LOG_err << "Please logout first "; } else if (words.size() > 1) { string email = words[1]; string defaultName = email; replaceAll(defaultName, "@"," "); if (words.size() > 2) { string name = getOption(cloptions, "name", defaultName); string passwd = words[2]; signup(name, passwd, email); } else { login = words[1]; name = getOption(cloptions, "name", defaultName); signingup = true; if (isCurrentThreadInteractive()) { setprompt(NEWPASSWORD); } else { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Extra args required in non-interactive mode. Usage: " << getUsageStr("signup"); } } } else { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("signup"); } return; } else if (words[0] == "whoami") { MegaUser *u = api->getMyUser(); if (u) { OUTSTREAM << "Account e-mail: " << u->getEmail() << endl; if (getFlag(clflags, "l")) { std::unique_ptr<::mega::MegaAccountDetails> storageDetails; std::unique_ptr<::mega::MegaAccountDetails> extAccountDetails; { auto megaCmdListener = std::make_unique(nullptr); api->getAccountDetails(megaCmdListener.get()); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "failed to get account details")) { storageDetails = std::unique_ptr<::mega::MegaAccountDetails>(megaCmdListener->getRequest()->getMegaAccountDetails()); } } { auto megaCmdListener = std::make_unique(nullptr); api->getExtendedAccountDetails(true, true, true, megaCmdListener.get()); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "get extended account details")) { extAccountDetails = std::unique_ptr<::mega::MegaAccountDetails>(megaCmdListener->getRequest()->getMegaAccountDetails()); } } actUponGetExtendedAccountDetails(std::move(storageDetails), std::move(extAccountDetails)); } delete u; } else { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in."; } return; } else if (words[0] == "df") { bool humanreadable = getFlag(clflags, "h"); MegaUser *u = api->getMyUser(); if (u) { MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); api->getSpecificAccountDetails(true, false, false, -1, megaCmdListener); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "failed to get used storage")) { unique_ptr details(megaCmdListener->getRequest()->getMegaAccountDetails()); if (details) { long long usedTotal = 0; long long rootStorage = 0; long long inboxStorage = 0; long long rubbishStorage = 0; long long insharesStorage = 0; long long storageMax = details->getStorageMax(); unique_ptr root(api->getRootNode()); unique_ptr inbox(api->getVaultNode()); unique_ptr rubbish(api->getRubbishNode()); unique_ptr inShares(api->getInShares()); if (!root || !inbox || !rubbish) { LOG_err << " Error retrieving storage details. Root node missing"; return; } MegaHandle rootHandle = root->getHandle(); MegaHandle inboxHandle = inbox->getHandle(); MegaHandle rubbishHandle = rubbish->getHandle(); rootStorage = details->getStorageUsed(rootHandle); OUTSTREAM << "Cloud drive: " << getFixLengthString(sizeToText(rootStorage, true, humanreadable), 12, ' ', true) << " in " << getFixLengthString(SSTR(details->getNumFiles(rootHandle)),7,' ',true) << " file(s) and " << getFixLengthString(SSTR(details->getNumFolders(rootHandle)),7,' ',true) << " folder(s)" << endl; inboxStorage = details->getStorageUsed(inboxHandle); OUTSTREAM << "Inbox: " << getFixLengthString(sizeToText(inboxStorage, true, humanreadable), 12, ' ', true ) << " in " << getFixLengthString(SSTR(details->getNumFiles(inboxHandle)),7,' ',true) << " file(s) and " << getFixLengthString(SSTR(details->getNumFolders(inboxHandle)),7,' ',true) << " folder(s)" << endl; rubbishStorage = details->getStorageUsed(rubbishHandle); OUTSTREAM << "Rubbish bin: " << getFixLengthString(sizeToText(rubbishStorage, true, humanreadable), 12, ' ', true) << " in " << getFixLengthString(SSTR(details->getNumFiles(rubbishHandle)),7,' ',true) << " file(s) and " << getFixLengthString(SSTR(details->getNumFolders(rubbishHandle)),7,' ',true) << " folder(s)" << endl; if (inShares) { for (int i = 0; i < inShares->size(); i++) { MegaNode *n = inShares->get(i); long long thisinshareStorage = details->getStorageUsed(n->getHandle()); insharesStorage += thisinshareStorage; if (i == 0) { OUTSTREAM << "Incoming shares:" << endl; } string name = n->getName(); name += ": "; name.append(max(0, int (21 - name.size())), ' '); OUTSTREAM << " " << name << getFixLengthString(sizeToText(thisinshareStorage, true, humanreadable), 12, ' ', true) << " in " << getFixLengthString(SSTR(details->getNumFiles(n->getHandle())),7,' ',true) << " file(s) and " << getFixLengthString(SSTR(details->getNumFolders(n->getHandle())),7,' ',true) << " folder(s)" << endl; } } usedTotal = sandboxCMD->receivedStorageSum; float percent = float(usedTotal * 1.0 / storageMax); if (percent < 0 ) percent = 0; string sof= percentageToText(percent); sof += " of "; sof += sizeToText(storageMax, true, humanreadable); for (int i = 0; i < 75 ; i++) { OUTSTREAM << "-"; } OUTSTREAM << endl; OUTSTREAM << "USED STORAGE: " << getFixLengthString(sizeToText(usedTotal, true, humanreadable), 12, ' ', true) << " " << getFixLengthString(sof, 39, ' ', true) << endl; for (int i = 0; i < 75 ; i++) { OUTSTREAM << "-"; } OUTSTREAM << endl; long long usedinVersions = details->getVersionStorageUsed(rootHandle) + details->getVersionStorageUsed(inboxHandle) + details->getVersionStorageUsed(rubbishHandle); OUTSTREAM << "Total size taken up by file versions: " << getFixLengthString(sizeToText(usedinVersions, true, humanreadable), 12, ' ', true) << endl; } } delete megaCmdListener; delete u; } else { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in."; } return; } else if (words[0] == "export") { if (!api->isFilesystemAvailable()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in."; return; } ::mega::m_time_t expireTime = 0; string sexpireTime = getOption(cloptions, "expire", ""); if ("" != sexpireTime) { expireTime = getTimeStampAfter(sexpireTime); } if (expireTime < 0) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Invalid time " << sexpireTime; return; } const bool add = getFlag(clflags, "a"); auto passwordOpt = getOptionAsOptional(*cloptions, "password"); // When the user passes "--password" without "=" it gets treated as a flag, so // it's inserted into `clflags`. We'll treat this as hasPassword=true as well // to ensure we log the "password is empty" error to the user. if (!passwordOpt && getFlag(clflags, "password")) { passwordOpt = ""; } if (!add && (passwordOpt || expireTime > 0 || getFlag(clflags, "f") || getFlag(clflags, "writable") || getFlag(clflags, "mega-hosted"))) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Option can only be used when adding an export (with -a)"; LOG_err << "Usage: " << getUsageStr("export"); return; } // This will be true for '--password', '--password=', and '--password=""' // Note: --password='' will use the '' string as the actual password if (passwordOpt && passwordOpt->empty()) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Password cannot be empty"; return; } if (words.size() <= 1) { LOG_warn << "No file/folder argument provided, will export the current working folder"; words.push_back(string(".")); } for (size_t i = 1; i < words.size(); i++) { unescapeifRequired(words[i]); if (isRegExp(words[i])) { vector> nodes = nodesbypath(words[i].c_str(), getFlag(clflags,"use-pcre")); if (nodes.empty()) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Nodes not found: " << words[i]; } for (const auto& n : nodes) { assert(n); if (add) { if (!n->isExported()) { LOG_debug << " exporting ... " << n->getName() << " expireTime=" << expireTime; exportNode(n.get(), expireTime, passwordOpt, clflags, cloptions); } else { setCurrentThreadOutCode(MCMD_EXISTS); LOG_err << "Node " << words[i] << " is already exported. " << "Use -d to delete it if you want to change its parameters. Note: the new link may differ"; } } else if (getFlag(clflags, "d")) { LOG_debug << " deleting export ... " << n->getName(); disableExport(n.get()); } else { int exportedCount = dumpListOfExported(n.get(), getTimeFormatFromSTR(getOption(cloptions, "time-format","RFC2822")), clflags, cloptions, words[i]); if (exportedCount == 0) { setCurrentThreadOutCode(MCMD_NOTFOUND); OUTSTREAM << words[i] << " is not exported. Use -a to export it" << endl; } } } } else { std::unique_ptr n = nodebypath(words[i].c_str()); if (n) { auto nodeName = [CONST_CAPTURE(words), i] { return (words[i] == "." ? "current folder" : "<" + words[i] + ">"); }; if (add) { if (!n->isExported()) { LOG_debug << " exporting ... " << n->getName(); exportNode(n.get(), expireTime, passwordOpt, clflags, cloptions); } else { setCurrentThreadOutCode(MCMD_EXISTS); LOG_err << nodeName() << " is already exported. " << "Use -d to delete it if you want to change its parameters. Note: the new link may differ"; } } else if (getFlag(clflags, "d")) { LOG_debug << " deleting export ... " << n->getName(); disableExport(n.get()); } else { int exportedCount = dumpListOfExported(n.get(), getTimeFormatFromSTR(getOption(cloptions, "time-format","RFC2822")), clflags, cloptions, words[i]); if (exportedCount == 0) { setCurrentThreadOutCode(MCMD_NOTFOUND); OUTSTREAM << "Couldn't find anything exported below " << nodeName() << ". Use -a to export " << (words[i].size() ? "it" : "something") << endl; } } } else { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Node not found: " << words[i]; } } } } else if (words[0] == "import") { if (!api->isFilesystemAvailable()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in."; return; } string remotePath = ""; std::unique_ptr dstFolder; if (words.size() > 1) //link { if (isPublicLink(words[1])) { string publicLink = words[1]; if (!decryptLinkIfEncrypted(api, publicLink, cloptions)) { return; } if (words.size() > 2) { remotePath = words[2]; dstFolder = nodebypath(remotePath.c_str()); } else { dstFolder.reset(api->getNodeByHandle(cwd)); remotePath = "."; //just to inform (alt: getpathbynode) } if (dstFolder && (!dstFolder->getType() == MegaNode::TYPE_FILE)) { if (getLinkType(publicLink) == MegaNode::TYPE_FILE) { MegaCmdListener *megaCmdListener = new MegaCmdListener(nullptr); api->importFileLink(publicLink.c_str(), dstFolder.get(), megaCmdListener); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "import node")) { MegaNode *imported = api->getNodeByHandle(megaCmdListener->getRequest()->getNodeHandle()); if (imported) { char *importedPath = api->getNodePath(imported); OUTSTREAM << "Import file complete: " << importedPath << endl; delete []importedPath; } else { LOG_warn << "Import file complete: Couldn't get path of imported file"; } delete imported; } delete megaCmdListener; } else if (getLinkType(publicLink) == MegaNode::TYPE_FOLDER) { MegaApi* apiFolder = getFreeApiFolder(); if (!apiFolder) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "No available Api folder. Use configure to increase exported_folders_sdks"; return; } char *accountAuth = api->getAccountAuth(); apiFolder->setAccountAuth(accountAuth); delete []accountAuth; MegaCmdListener *megaCmdListener = new MegaCmdListener(apiFolder, NULL); apiFolder->loginToFolder(publicLink.c_str(), megaCmdListener); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "login to folder")) { MegaCmdListener *megaCmdListener2 = new MegaCmdListener(apiFolder, NULL); apiFolder->fetchNodes(megaCmdListener2); megaCmdListener2->wait(); if (checkNoErrors(megaCmdListener2->getError(), "access folder link " + publicLink)) { MegaNode *nodeToImport = NULL; bool usedRoot = false; string shandle = getPublicLinkHandle(publicLink); if (shandle.size()) { handle thehandle = apiFolder->base64ToHandle(shandle.c_str()); nodeToImport = apiFolder->getNodeByHandle(thehandle); } else { nodeToImport = apiFolder->getRootNode(); usedRoot = true; } if (nodeToImport) { MegaNode *authorizedNode = apiFolder->authorizeNode(nodeToImport); if (authorizedNode != NULL) { MegaCmdListener *megaCmdListener3 = new MegaCmdListener(apiFolder, NULL); api->copyNode(authorizedNode, dstFolder.get(), megaCmdListener3); megaCmdListener3->wait(); if (checkNoErrors(megaCmdListener->getError(), "import folder node")) { MegaNode *importedFolderNode = api->getNodeByHandle(megaCmdListener3->getRequest()->getNodeHandle()); char *pathnewFolder = api->getNodePath(importedFolderNode); if (pathnewFolder) { OUTSTREAM << "Imported folder complete: " << pathnewFolder << endl; delete []pathnewFolder; } delete importedFolderNode; } delete megaCmdListener3; delete authorizedNode; } else { setCurrentThreadOutCode(MCMD_EUNEXPECTED); LOG_debug << "Node couldn't be authorized: " << publicLink; } delete nodeToImport; } else { setCurrentThreadOutCode(MCMD_INVALIDSTATE); if (usedRoot) { LOG_err << "Couldn't get root folder for folder link"; } else { LOG_err << "Failed to get node corresponding to handle within public link " << shandle; } } } { std::unique_ptr megaCmdListener(new MegaCmdListener(apiFolder)); apiFolder->logout(false, megaCmdListener.get()); megaCmdListener->wait(); if (megaCmdListener->getError()->getErrorCode() != MegaError::API_OK) { LOG_err << "Couldn't logout from apiFolder"; } } delete megaCmdListener2; } delete megaCmdListener; freeApiFolder(apiFolder); } else { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Invalid link: " << publicLink; LOG_err << " " << getUsageStr("import"); } } else { setCurrentThreadOutCode(MCMD_INVALIDTYPE); LOG_err << "Invalid destiny: " << remotePath; } } else { setCurrentThreadOutCode(MCMD_INVALIDTYPE); LOG_err << "Invalid link: " << words[1]; } } else { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("import"); } return; } else if (words[0] == "reload") { int clientID = getintOption(cloptions, "clientID", -1); OUTSTREAM << "Reloading account..." << endl; MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL, NULL, clientID); sandboxCMD->mNodesCurrentPromise.initiatePromise(); api->fetchNodes(megaCmdListener); actUponFetchNodes(api, megaCmdListener); delete megaCmdListener; return; } else if (words[0] == "logout") { OUTSTREAM << "Logging out..." << endl; MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); bool keepSession = getFlag(clflags, "keep-session"); char * dumpSession = NULL; if (keepSession) //local logout { dumpSession = api->dumpSession(); api->localLogout(megaCmdListener); } else { api->logout(false, megaCmdListener); } actUponLogout(megaCmdListener, keepSession); if (keepSession) { OUTSTREAM << "Session closed but not deleted. Warning: it will be restored the next time you execute the application. Execute \"logout\" to delete the session permanently." << endl; if (dumpSession) { OUTSTREAM << "You can also login with the session id: " << dumpSession << endl; delete []dumpSession; } } delete megaCmdListener; return; } else if (words[0] == "confirm") { if (getFlag(clflags, "security")) { auto megaCmdListener = std::make_unique(nullptr); api->upgradeSecurity(megaCmdListener.get()); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "confirm security upgrade")) { removeGreetingMatching("Your account's security needs upgrading"); OUTSTREAM << "Account security upgrade. You shall no longer see an account security warning." << endl; } return; } if (words.size() > 2) { string link = words[1]; string email = words[2]; // check email corresponds with link: MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); api->querySignupLink(link.c_str(), megaCmdListener); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "check email corresponds to link")) { if (megaCmdListener->getRequest()->getFlag()) { OUTSTREAM << "Account " << email << " confirmed successfully. You can login with it now" << endl; } else if (megaCmdListener->getRequest()->getEmail() && email == megaCmdListener->getRequest()->getEmail()) { string passwd; if (words.size() > 3) { passwd = words[3]; confirm(passwd, email, link); } else { this->login = email; this->link = link; confirming = true; if (isCurrentThreadInteractive() && !isCurrentThreadCmdShell()) { setprompt(LOGINPASSWORD); } else { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Extra args required in non-interactive mode. Usage: " << getUsageStr("confirm"); } } } else { setCurrentThreadOutCode(MCMD_INVALIDEMAIL); LOG_err << email << " doesn't correspond to the confirmation link: " << link; } } delete megaCmdListener; } else { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("confirm"); } return; } else if (words[0] == "session") { char * dumpSession = api->dumpSession(); if (dumpSession) { OUTSTREAM << "Your (secret) session is: " << dumpSession << endl; delete []dumpSession; } else { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in."; } return; } else if (words[0] == "history") { return; } else if (words[0] == "version") { OUTSTREAM << "MEGAcmd version: " << MEGACMD_MAJOR_VERSION << "." << MEGACMD_MINOR_VERSION << "." << MEGACMD_MICRO_VERSION << "." << MEGACMD_BUILD_ID << ": code " << MEGACMD_CODE_VERSION #ifdef _WIN64 << " (64 bits)" #endif << endl; MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); api->getLastAvailableVersion("BdARkQSQ",megaCmdListener); if (!megaCmdListener->trywait(2000)) { if (!megaCmdListener->getError()) { LOG_fatal << "No MegaError at getLastAvailableVersion: "; } else if (megaCmdListener->getError()->getErrorCode() != MegaError::API_OK) { LOG_debug << "Couldn't get latests available version: " << megaCmdListener->getError()->getErrorString(); } else { if (megaCmdListener->getRequest()->getNumber() != MEGACMD_CODE_VERSION) { OUTSTREAM << "---------------------------------------------------------------------" << endl; OUTSTREAM << "-- There is a new version available of megacmd: " << getLeftAlignedStr(megaCmdListener->getRequest()->getName(), 12) << "--" << endl; OUTSTREAM << "-- Please, download it from https://mega.nz/cmd --" << endl; #if defined(__APPLE__) OUTSTREAM << "-- Before installing enter \"exit\" to close MEGAcmd --" << endl; #endif OUTSTREAM << "---------------------------------------------------------------------" << endl; } } delete megaCmdListener; } else { LOG_debug << "Couldn't get latests available version (petition timed out)"; api->removeRequestListener(megaCmdListener); delete megaCmdListener; } if (getFlag(clflags,"c")) { OUTSTREAM << "Changes in the current version:" << endl; string thechangelog = megacmdchangelog; if (thechangelog.size()) { replaceAll(thechangelog,"\n","\n * "); OUTSTREAM << " * " << thechangelog << endl << endl; } OUTSTREAM << "Full changelog available at https://github.com/meganz/MEGAcmd/blob/master/build/megacmd/megacmd.changes" << endl << endl; } if (getFlag(clflags,"l")) { OUTSTREAM << "MEGA SDK version: " << SDK_COMMIT_HASH << endl; OUTSTREAM << "MEGA SDK Credits: https://github.com/meganz/sdk/blob/master/CREDITS.md" << endl; OUTSTREAM << "MEGA SDK License: https://github.com/meganz/sdk/blob/master/LICENSE" << endl; OUTSTREAM << "MEGAcmd License: https://github.com/meganz/megacmd/blob/master/LICENSE" << endl; OUTSTREAM << "MEGA Terms: https://mega.nz/terms" << endl; OUTSTREAM << "MEGA General Data Protection Regulation Disclosure: https://mega.nz/gdpr" << endl; OUTSTREAM << "Features enabled:" << endl; #ifdef USE_CRYPTOPP OUTSTREAM << "* CryptoPP" << endl; #endif #ifdef USE_SQLITE OUTSTREAM << "* SQLite" << endl; #endif #ifdef USE_BDB OUTSTREAM << "* Berkeley DB" << endl; #endif #ifdef USE_INOTIFY OUTSTREAM << "* inotify" << endl; #endif #ifdef HAVE_FDOPENDIR OUTSTREAM << "* fdopendir" << endl; #endif #ifdef HAVE_SENDFILE OUTSTREAM << "* sendfile" << endl; #endif #ifdef _LARGE_FILES OUTSTREAM << "* _LARGE_FILES" << endl; #endif #ifdef USE_FREEIMAGE OUTSTREAM << "* FreeImage" << endl; #endif #ifdef USE_PCRE OUTSTREAM << "* PCRE" << endl; #endif #ifdef ENABLE_SYNC OUTSTREAM << "* sync subsystem" << endl; #endif } return; } else if (words[0] == "masterkey") { if (!api->isFilesystemAvailable()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in."; return; } OUTSTREAM << api->exportMasterKey() << endl; api->masterKeyExported(); //we do not wait for this to end } else if (words[0] == "showpcr") { if (!api->isFilesystemAvailable()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in."; return; } bool incoming = getFlag(clflags, "in"); bool outgoing = getFlag(clflags, "out"); if (!incoming && !outgoing) { incoming = true; outgoing = true; } if (outgoing) { MegaContactRequestList *ocrl = api->getOutgoingContactRequests(); if (ocrl) { if (ocrl->size()) { OUTSTREAM << "Outgoing PCRs:" << endl; } for (int i = 0; i < ocrl->size(); i++) { auto cr = ocrl->get(i); OUTSTREAM << " " << getLeftAlignedStr(cr->getTargetEmail(),22); char * sid = api->userHandleToBase64(cr->getHandle()); OUTSTREAM << "\t (id: " << sid << ", creation: " << getReadableTime(cr->getCreationTime(), getTimeFormatFromSTR(getOption(cloptions, "time-format","RFC2822"))) << ", modification: " << getReadableTime(cr->getModificationTime(), getTimeFormatFromSTR(getOption(cloptions, "time-format","RFC2822"))) << ")"; delete[] sid; OUTSTREAM << endl; } delete ocrl; } } if (incoming) { MegaContactRequestList *icrl = api->getIncomingContactRequests(); if (icrl) { if (icrl->size()) { OUTSTREAM << "Incoming PCRs:" << endl; } for (int i = 0; i < icrl->size(); i++) { auto cr = icrl->get(i); OUTSTREAM << " " << getLeftAlignedStr(cr->getSourceEmail(), 22); MegaHandle id = cr->getHandle(); char sid[12]; Base64::btoa((unsigned char*)&( id ), sizeof( id ), sid); OUTSTREAM << "\t (id: " << sid << ", creation: " << getReadableTime(cr->getCreationTime(), getTimeFormatFromSTR(getOption(cloptions, "time-format","RFC2822"))) << ", modification: " << getReadableTime(cr->getModificationTime(), getTimeFormatFromSTR(getOption(cloptions, "time-format","RFC2822"))) << ")"; if (cr->getSourceMessage()) { OUTSTREAM << endl << "\t" << "Invitation message: " << cr->getSourceMessage(); } OUTSTREAM << endl; } delete icrl; } } return; } else if (words[0] == "killsession") { bool all = getFlag(clflags, "a"); if ((words.size() <= 1 && !all) || (all && words.size() > 1)) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("killsession"); return; } string thesession; MegaHandle thehandle = UNDEF; if (all) { // Kill all sessions (except current) words.push_back("all"); } for (unsigned int i = 1; i < words.size() ; i++) { thesession = words[i]; if (thesession == "all") { thehandle = INVALID_HANDLE; } else { thehandle = api->base64ToUserHandle(thesession.c_str()); } MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); api->killSession(thehandle, megaCmdListener); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "kill session " + thesession + ". Maybe the session was not valid.")) { if (thesession != "all") { OUTSTREAM << "Session " << thesession << " killed successfully" << endl; } else { OUTSTREAM << "All sessions killed successfully" << endl; } } delete megaCmdListener; } return; } else if (words[0] == "transfers") { bool showcompleted = getFlag(clflags, "show-completed"); bool onlycompleted = getFlag(clflags, "only-completed"); bool onlyuploads = getFlag(clflags, "only-uploads"); bool onlydownloads = getFlag(clflags, "only-downloads"); bool showsyncs = getFlag(clflags, "show-syncs"); bool printsummary = getFlag(clflags, "summary"); int PATHSIZE = getintOption(cloptions,"path-display-size"); if (!PATHSIZE) { // get screen size for output purposes unsigned int width = getNumberOfCols(75); PATHSIZE = min(60,int((width-46)/2)); } PATHSIZE = max(0, PATHSIZE); if (getFlag(clflags,"c")) { if (getFlag(clflags,"a")) { if (onlydownloads || (!onlyuploads && !onlydownloads) ) { MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); api->cancelTransfers(MegaTransfer::TYPE_DOWNLOAD, megaCmdListener); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "cancel all download transfers")) { OUTSTREAM << "Download transfers cancelled successfully." << endl; } delete megaCmdListener; } if (onlyuploads || (!onlyuploads && !onlydownloads) ) { MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); api->cancelTransfers(MegaTransfer::TYPE_UPLOAD, megaCmdListener); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "cancel all upload transfers")) { OUTSTREAM << "Upload transfers cancelled successfully." << endl; } delete megaCmdListener; } } else { if (words.size() < 2) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("transfers"); return; } for (unsigned int i = 1; i < words.size(); i++) { std::unique_ptr transfer(api->getTransferByTag(toInteger(words[i],-1))); if (transfer) { if (transfer->isSyncTransfer()) { LOG_err << "Unable to cancel transfer with tag " << words[i] << ". Sync transfers cannot be cancelled"; setCurrentThreadOutCode(MCMD_INVALIDTYPE); } else { MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); api->cancelTransfer(transfer.get(), megaCmdListener); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), "cancel transfer with tag " + words[i] + ".")) { OUTSTREAM << "Transfer " << words[i]<< " cancelled successfully." << endl; } delete megaCmdListener; } } else { LOG_err << "Could not find transfer with tag: " << words[i]; setCurrentThreadOutCode(MCMD_NOTFOUND); } } } return; } if (getFlag(clflags,"p") || getFlag(clflags,"r")) { if (getFlag(clflags,"a")) { if (onlydownloads || (!onlyuploads && !onlydownloads) ) { MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); api->pauseTransfers(getFlag(clflags,"p"), MegaTransfer::TYPE_DOWNLOAD, megaCmdListener); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), (getFlag(clflags,"p")?"pause all download transfers":"resume all download transfers"))) { OUTSTREAM << "Download transfers "<< (getFlag(clflags,"p")?"pause":"resume") << "d successfully." << endl; } delete megaCmdListener; } if (onlyuploads || (!onlyuploads && !onlydownloads) ) { MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); api->pauseTransfers(getFlag(clflags,"p"), MegaTransfer::TYPE_UPLOAD, megaCmdListener); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), (getFlag(clflags,"p")?"pause all download transfers":"resume all download transfers"))) { OUTSTREAM << "Upload transfers "<< (getFlag(clflags,"p")?"pause":"resume") << "d successfully." << endl; } delete megaCmdListener; } } else { if (words.size() < 2) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << " " << getUsageStr("transfers"); return; } for (unsigned int i = 1; i < words.size(); i++) { std::unique_ptr transfer(api->getTransferByTag(toInteger(words[i],-1))); if (transfer) { if (transfer->isSyncTransfer()) { LOG_err << "Unable to "<< (getFlag(clflags,"p")?"pause":"resume") << " transfer with tag " << words[i] << ". Sync transfers cannot be "<< (getFlag(clflags,"p")?"pause":"resume") << "d"; setCurrentThreadOutCode(MCMD_INVALIDTYPE); } else { MegaCmdListener *megaCmdListener = new MegaCmdListener(NULL); api->pauseTransfer(transfer.get(), getFlag(clflags,"p"), megaCmdListener); megaCmdListener->wait(); if (checkNoErrors(megaCmdListener->getError(), (getFlag(clflags,"p")?"pause transfer with tag ":"resume transfer with tag ") + words[i] + ".")) { OUTSTREAM << "Transfer " << words[i]<< " "<< (getFlag(clflags,"p")?"pause":"resume") << "d successfully." << endl; } delete megaCmdListener; } } else { LOG_err << "Could not find transfer with tag: " << words[i]; setCurrentThreadOutCode(MCMD_NOTFOUND); } } } return; } //show transfers std::unique_ptr transferdata(api->getTransferData()); int ndownloads = transferdata->getNumDownloads(); int nuploads = transferdata->getNumUploads(); if (printsummary) { long long transferredDownload = 0, transferredUpload = 0, totalDownload = 0, totalUpload = 0; for (int i = 0; i< ndownloads; i++) { MegaTransfer *t = api->getTransferByTag(transferdata->getDownloadTag(i)); { if (t) { transferredDownload += t->getTransferredBytes(); totalDownload += t->getTotalBytes(); delete t; } } } for (int i = 0; i< nuploads; i++) { MegaTransfer *t = api->getTransferByTag(transferdata->getUploadTag(i)); { if (t) { transferredUpload += t->getTransferredBytes(); totalUpload += t->getTotalBytes(); delete t; } } } float percentDownload = !totalDownload?0:float(transferredDownload*1.0/totalDownload); float percentUpload = !totalUpload?0:float(transferredUpload*1.0/totalUpload); OUTSTREAM << getFixLengthString("NUM DOWNLOADS", 16, ' ', true); OUTSTREAM << getFixLengthString("DOWNLOADED", 12, ' ', true); OUTSTREAM << getFixLengthString("TOTAL", 12, ' ', true); OUTSTREAM << getFixLengthString("% ", 8, ' ', true); OUTSTREAM << " "; OUTSTREAM << getFixLengthString("NUM UPLOADS", 16, ' ', true); OUTSTREAM << getFixLengthString("UPLOADED", 12, ' ', true); OUTSTREAM << getFixLengthString("TOTAL", 12, ' ', true); OUTSTREAM << getFixLengthString("% ", 8, ' ', true); OUTSTREAM << endl; OUTSTREAM << getFixLengthString(SSTR(ndownloads), 16, ' ', true); OUTSTREAM << getFixLengthString(sizeToText(transferredDownload), 12, ' ', true); OUTSTREAM << getFixLengthString(sizeToText(totalDownload), 12, ' ', true); OUTSTREAM << getFixLengthString(percentageToText(percentDownload),8,' ',true); OUTSTREAM << " "; OUTSTREAM << getFixLengthString(SSTR(nuploads), 16, ' ', true); OUTSTREAM << getFixLengthString(sizeToText(transferredUpload), 12, ' ', true); OUTSTREAM << getFixLengthString(sizeToText(totalUpload), 12, ' ', true); OUTSTREAM << getFixLengthString(percentageToText(percentUpload),8,' ',true); OUTSTREAM << endl; return; } int limit = getintOption(cloptions, "limit", min(10,ndownloads+nuploads+(int)globalTransferListener->completedTransfers.size())); if (!transferdata) { setCurrentThreadOutCode(MCMD_EUNEXPECTED); LOG_err << "No transferdata."; return; } bool downloadpaused = api->areTransfersPaused(MegaTransfer::TYPE_DOWNLOAD); bool uploadpaused = api->areTransfersPaused(MegaTransfer::TYPE_UPLOAD); int indexUpload = 0; int indexDownload = 0; int shown = 0; int showndl = 0; int shownup = 0; unsigned int shownCompleted = 0; vector transfersDLToShow; vector transfersUPToShow; vector transfersCompletedToShow; if (showcompleted) { globalTransferListener->completedTransfersMutex.lock(); size_t totalcompleted = globalTransferListener->completedTransfers.size(); for (size_t i = 0;(i < totalcompleted) && (shownCompleted < totalcompleted) && (shownCompleted < (size_t)(limit+1)); //Note limit+1 to seek for one more to show if there are more to show! i++) { MegaTransfer *transfer = globalTransferListener->completedTransfers.at(i); if ( ( (transfer->getType() == MegaTransfer::TYPE_UPLOAD && (onlyuploads || (!onlyuploads && !onlydownloads) )) || (transfer->getType() == MegaTransfer::TYPE_DOWNLOAD && (onlydownloads || (!onlyuploads && !onlydownloads) ) ) ) && !(!showsyncs && transfer->isSyncTransfer()) ) { transfersCompletedToShow.push_back(transfer); shownCompleted++; } } globalTransferListener->completedTransfersMutex.unlock(); } shown += shownCompleted; while (!onlycompleted) { //MegaTransfer *transfer = transferdata->get(i); MegaTransfer *transfer = NULL; //Next transfer to show if (onlyuploads && !onlydownloads && indexUpload < transferdata->getNumUploads()) //Only uploads { transfer = api->getTransferByTag(transferdata->getUploadTag(indexUpload++)); } else { if ( (!onlydownloads || (onlydownloads && onlyuploads)) //both && ( (shown >= (limit/2) ) || indexDownload == ndownloads ) // /already chosen half slots for dls or no more dls && indexUpload < transferdata->getNumUploads() ) //This is not 100 % perfect, it could show with a limit of 10 5 downloads and 3 uploads with more downloads on the queue. { transfer = api->getTransferByTag(transferdata->getUploadTag(indexUpload++)); } else if(indexDownload < ndownloads) { transfer = api->getTransferByTag(transferdata->getDownloadTag(indexDownload++)); } } if (!transfer) break; //finish if ( (showcompleted || transfer->getState() != MegaTransfer::STATE_COMPLETED) && !(onlyuploads && transfer->getType() != MegaTransfer::TYPE_UPLOAD && !onlydownloads ) && !(onlydownloads && transfer->getType() != MegaTransfer::TYPE_DOWNLOAD && !onlyuploads ) && !(!showsyncs && transfer->isSyncTransfer()) && (shown < (limit+1)) //Note limit+1 to seek for one more to show if there are more to show! ) { shown++; if (transfer->getType() == MegaTransfer::TYPE_DOWNLOAD) { transfersDLToShow.push_back(transfer); showndl++; } else { transfersUPToShow.push_back(transfer); shownup++; } } else { delete transfer; } if (shown>limit || transfer == NULL) //we-re done { break; } } vector::iterator itCompleted = transfersCompletedToShow.begin(); vector::iterator itDLs = transfersDLToShow.begin(); vector::iterator itUPs = transfersUPToShow.begin(); ColumnDisplayer cd(clflags, cloptions); cd.addHeader("SOURCEPATH", false); cd.addHeader("DESTINYPATH", false); for (unsigned int i=0;i 1) { proxyType = MegaProxy::PROXY_CUSTOM; urlProxy = words[1]; } else if (autoProxy) { proxyType = MegaProxy::PROXY_AUTO; } else if (noneProxy) { proxyType = MegaProxy::PROXY_NONE; } if (proxyType == -1) { int configuredProxyType = ConfigurationManager::getConfigurationValue("proxy_type", -1); auto configuredProxyUrl = ConfigurationManager::getConfigurationSValue("proxy_url"); auto configuredProxyUsername = ConfigurationManager::getConfigurationSValue("proxy_username"); auto configuredProxyPassword = ConfigurationManager::getConfigurationSValue("proxy_password"); if (configuredProxyType == -1) { OUTSTREAM << "No proxy configuration found" << endl; return; } if (configuredProxyType == MegaProxy::PROXY_NONE) { OUTSTREAM << "Proxy disabled" << endl; return; } OUTSTREAM << "Proxy configured. "; if (configuredProxyType == MegaProxy::PROXY_AUTO) { OUTSTREAM << endl << " Type = AUTO"; } else { OUTSTREAM << endl << " Type = CUSTOM"; } if (configuredProxyUrl.size()) { OUTSTREAM << endl << " URL = " << configuredProxyUrl; } if (configuredProxyUsername.size()) { OUTSTREAM << endl << " username = " << configuredProxyUsername; } if (configuredProxyPassword.size()) { OUTSTREAM << endl << " password = " << configuredProxyPassword; } OUTSTREAM << endl; } else { setProxy(urlProxy, username, password, proxyType); } } #ifdef WITH_FUSE else if (words[0] == "fuse-add") { if (!api->isFilesystemAvailable() || !api->isLoggedIn()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in"; return; } #ifdef WIN32 if (words.size() != 2 && (!getenv("MEGACMD_FUSE_ALLOW_LOCAL_PATHS") || words.size() != 3)) #else if (words.size() != 3) #endif { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << getUsageStr("fuse-add"); return; } bool remoteIsFirstArg(words.size() == 2); const std::string& localPathStr = remoteIsFirstArg ? std::string("") : words[1]; const std::string& remotePathStr = remoteIsFirstArg ? words[1] : words[2]; if (remotePathStr.empty() #ifndef WIN32 || localPathStr.empty() #endif ) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Path cannot be empty"; LOG_err << " " << getUsageStr("fuse-add"); return; } const fs::path localPath = localPathStr; #ifndef WIN32 if (std::error_code ec; !fs::exists(localPath, ec) || ec) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Local path " << localPath << " does not exist"; return; } #endif std::unique_ptr node = nodebypath(remotePathStr.c_str()); if (node == nullptr) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Remote path \"" << remotePathStr << "\" does not exist"; return; } const std::string name = getOption(cloptions, "name", ""); const bool disabled = getFlag(clflags, "disabled"); const bool transient = getFlag(clflags, "transient"); const bool readOnly = getFlag(clflags, "read-only"); FuseCommand::addMount(*api, localPath, *node, disabled, transient, readOnly, name); } else if (words[0] == "fuse-remove") { if (!api->isFilesystemAvailable() || !api->isLoggedIn()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in"; return; } if (words.size() != 2) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << getUsageStr("fuse-remove"); return; } const std::string& identifier = words[1]; auto mount = FuseCommand::getMountByNameOrPath(*api, identifier); if (!mount) { setCurrentThreadOutCode(MCMD_NOTFOUND); return; } FuseCommand::removeMount(*api, *mount); } else if (words[0] == "fuse-enable") { if (!api->isFilesystemAvailable() || !api->isLoggedIn()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in"; return; } if (words.size() != 2) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << getUsageStr("fuse-enable"); return; } const std::string& identifier = words[1]; auto mount = FuseCommand::getMountByNameOrPath(*api, identifier); if (!mount) { setCurrentThreadOutCode(MCMD_NOTFOUND); return; } const bool temporarily = getFlag(clflags, "temporarily"); FuseCommand::enableMount(*api, *mount, temporarily); } else if (words[0] == "fuse-disable") { if (!api->isFilesystemAvailable() || !api->isLoggedIn()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in"; return; } if (words.size() != 2) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << getUsageStr("fuse-disable"); return; } const std::string& identifier = words[1]; auto mount = FuseCommand::getMountByNameOrPath(*api, identifier); if (!mount) { setCurrentThreadOutCode(MCMD_NOTFOUND); return; } const bool temporarily = getFlag(clflags, "temporarily"); FuseCommand::disableMount(*api, *mount, temporarily); } else if (words[0] == "fuse-show") { if (!api->isFilesystemAvailable() || !api->isLoggedIn()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in"; return; } if (words.size() > 2) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << getUsageStr("fuse-show"); return; } const bool showAllMounts = (words.size() == 1); if (showAllMounts) { int rowCountLimit = getintOption(cloptions, "limit", 0); if (rowCountLimit < 0) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Row count limit cannot be less than 0"; return; } if (rowCountLimit == 0) { rowCountLimit = std::numeric_limits::max(); } const bool disablePathCollapse = getFlag(clflags, "disable-path-collapse"); const bool onlyEnabled = getFlag(clflags, "only-enabled"); ColumnDisplayer cd(clflags, cloptions); FuseCommand::printAllMounts(*api, cd, onlyEnabled, disablePathCollapse, rowCountLimit); } else { const std::string& identifier = words[1]; auto mount = FuseCommand::getMountByNameOrPath(*api, identifier); if (!mount) { setCurrentThreadOutCode(MCMD_NOTFOUND); return; } FuseCommand::printMount(*api, *mount); } } else if (words[0] == "fuse-config") { if (!api->isFilesystemAvailable() || !api->isLoggedIn()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in"; return; } if (words.size() != 2) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << getUsageStr("fuse-config"); return; } auto configDeltaOpt = loadFuseConfigDelta(*cloptions); if (!configDeltaOpt) { setCurrentThreadOutCode(MCMD_EARGS); return; } const std::string& identifier = words[1]; auto mount = FuseCommand::getMountByNameOrPath(*api, identifier); if (!mount) { setCurrentThreadOutCode(MCMD_NOTFOUND); return; } FuseCommand::changeConfig(*api, *mount, *configDeltaOpt); } #endif else if (words[0] == "sync-issues") { if (!api->isFilesystemAvailable() || !api->isLoggedIn()) { setCurrentThreadOutCode(MCMD_NOTLOGGEDIN); LOG_err << "Not logged in"; return; } bool disableWarning = getFlag(clflags, "disable-warning"); bool enableWarning = getFlag(clflags, "enable-warning"); if (!onlyZeroOrOneOf(disableWarning, enableWarning)) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Only one warning action (enable/disable) can be specified at a time"; LOG_err << " " << getUsageStr("sync-issues"); return; } int rowCountLimit = getintOption(cloptions, "limit", 10); if (rowCountLimit < 0) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Row count limit cannot be less than 0"; return; } if (rowCountLimit == 0) { rowCountLimit = std::numeric_limits::max(); } bool disablePathCollapse = getFlag(clflags, "disable-path-collapse"); if (disableWarning) { mSyncIssuesManager.disableWarning(); return; } if (enableWarning) { mSyncIssuesManager.enableWarning(); return; } auto syncIssues = mSyncIssuesManager.getSyncIssues(); #ifdef MEGACMD_TESTING_CODE // Do not trust empty results (SDK may send them after spurious scans delayed 20ds. SDK-4813) timelyRetry(std::chrono::milliseconds(2300), std::chrono::milliseconds(200), [&syncIssues]() { return !syncIssues.empty(); }, [this, &syncIssues, firstTime{true}]() mutable { if (firstTime) { LOG_warn << "sync-issues first retrieval returned empty"; firstTime = false; } syncIssues = mSyncIssuesManager.getSyncIssues(); LOG_warn << "sync-issues retrieval returned empty. Retried returned = " << syncIssues.size(); }); #endif ColumnDisplayer cd(clflags, cloptions); bool detailSyncIssue = getFlag(clflags, "detail"); if (detailSyncIssue) // get the details of one or more issues { bool showAll = getFlag(clflags, "all"); if (showAll) { if (words.size() != 1) { LOG_err << getUsageStr("sync-issues"); return; } SyncIssuesCommand::printAllIssuesDetail(*api, cd, syncIssues, disablePathCollapse, rowCountLimit); } else { if (words.size() != 2) { LOG_err << getUsageStr("sync-issues"); return; } const std::string syncIssueId = words.back(); auto syncIssuePtr = syncIssues.getSyncIssue(syncIssueId); if (!syncIssuePtr) { setCurrentThreadOutCode(MCMD_NOTFOUND); LOG_err << "Sync issue \"" << syncIssueId << "\" does not exist"; return; } SyncIssuesCommand::printSingleIssueDetail(*api, cd, *syncIssuePtr, disablePathCollapse, rowCountLimit); } } else // show all sync issues { if (syncIssues.empty()) { OUTSTREAM << "There are no sync issues" << endl; return; } SyncIssuesCommand::printAllIssues(*api, cd, syncIssues, disablePathCollapse, rowCountLimit); } } #if defined(DEBUG) || defined(MEGACMD_TESTING_CODE) else if (words[0] == "echo") { if (words.size() < 2 || words[1].empty()) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Missing message to echo"; return; } std::string str = words[1]; #ifdef _WIN32 if (str == "") { str = "\xf0\x8f\xbf\xbf"; } #endif if (getFlag(clflags, "log-as-err")) { LOG_err << str; } else { OUTSTREAM << str << endl; } } #endif else { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Invalid command: " << words[0]; } } }//end namespace MEGAcmd-2.5.2_Linux/src/megacmdexecuter.h000066400000000000000000000334671516543156300202240ustar00rootroot00000000000000/** * @file src/megacmdexecuter.h * @brief MEGAcmd: Executer of the commands * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of the MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #ifndef MEGACMDEXECUTER_H #define MEGACMDEXECUTER_H #include "megacmdlogger.h" #include "megacmdsandbox.h" #include "listeners.h" #include "deferred_single_trigger.h" #include "sync_issues.h" namespace megacmd { class MegaCmdGlobalTransferListener; class MegaCmdMultiTransferListener; class MegaCmdSandbox; class MegaCmdExecuter { private: mega::MegaApi *api; mega::handle cwd; std::unique_ptr session; std::unique_ptr mFsAccessCMD; MegaCmdLogger *loggerCMD; MegaCmdSandbox *sandboxCMD; MegaCmdGlobalTransferListener *globalTransferListener; std::mutex mtxSyncMap; std::mutex mtxWebDavLocations; std::mutex mtxFtpLocations; DeferredSingleTrigger mDeferredSharedFoldersVerifier; SyncIssuesManager mSyncIssuesManager; std::recursive_mutex mtxBackupsMap; // login/signup e-mail address std::string login; // signup name std::string name; // delete confirmation std::vector> mNodesToConfirmDelete; std::string getNodePathString(mega::MegaNode *n); void cancelOngoingVerification(mega::MegaApi* api, bool start_new_verification); /** * @name forEachFileInNode * @brief Traverses through all files in the given node with the provided callback. * @param n - the node object to traverse * @param recurse - whether to recursively traverse directories in this node * @param callback - the callback which receives child entries. Should be of the form `void * (MegaNode *)` */ template void forEachFileInNode(mega::MegaNode &n, bool recurse, Cb &&callback) { auto children = std::unique_ptr(api->getChildren(&n)); if (children != nullptr) { for (int i = 0; i < children->size(); i++) { auto child = children->get(i); if (child->getType() == mega::MegaNode::TYPE_FILE) { callback(children->get(i)); } else if (recurse) { forEachFileInNode(*child, true, std::forward(callback)); } } } } public: bool signingup; bool confirming; bool confirmingcancel; // link to confirm std::string link; MegaCmdExecuter(mega::MegaApi *api, MegaCmdLogger *loggerCMD, MegaCmdSandbox *sandboxCMD); ~MegaCmdExecuter(); void updateprompt(mega::MegaApi *api = nullptr); // nodes browsing void listtrees(); static bool includeIfIsExported(mega::MegaApi* api, mega::MegaNode * n, void *arg); static bool includeIfIsShared(mega::MegaApi* api, mega::MegaNode * n, void *arg); static bool includeIfIsPendingOutShare(mega::MegaApi* api, mega::MegaNode * n, void *arg); static bool includeIfIsSharedOrPendingOutShare(mega::MegaApi* api, mega::MegaNode * n, void *arg); static bool includeIfMatchesPattern(mega::MegaApi* api, mega::MegaNode * n, void *arg); static bool includeIfMatchesCriteria(mega::MegaApi* api, mega::MegaNode * n, void *arg); bool processTree(mega::MegaNode * n, bool(mega::MegaApi *, mega::MegaNode *, void *), void *( arg )); std::unique_ptr nodebypath(const char* ptr, std::string* user = nullptr, std::string* namepart = nullptr); std::vector> nodesbypath(const char* ptr, bool usepcre, std::string* user = nullptr); void getNodesMatching(mega::MegaNode* parentNode, std::deque pathParts, std::vector>& nodesMatching, bool usepcre); std::vector * nodesPathsbypath(const char* ptr, bool usepcre, std::string* user = NULL, std::string* namepart = NULL); void getPathsMatching(mega::MegaNode *parentNode, std::deque pathParts, std::vector *pathsMatching, bool usepcre, std::string pathPrefix = ""); void printTreeSuffix(int depth, std::vector &lastleaf); void dumpNode(mega::MegaNode* n, const char *timeFormat, std::map *clflags, std::map *cloptions, int extended_info, bool showversions = false, int depth = 0, const char* title = NULL); void dumptree(mega::MegaNode* n, bool treelike, std::vector &lastleaf, const char *timeFormat, std::map *clflags, std::map *cloptions, int recurse, int extended_info, bool showversions = false, int depth = 0, std::string pathRelativeTo = "NULL"); void dumpNodeSummaryHeader(const char *timeFormat, std::map *clflags, std::map *cloptions); void dumpNodeSummary(mega::MegaNode* n, const char *timeFormat, std::map *clflags, std::map *cloptions, bool humanreadable = false, const char* title = NULL); void dumpTreeSummary(mega::MegaNode* n, const char *timeFormat, std::map *clflags, std::map *cloptions, int recurse, bool show_versions, int depth = 0, bool humanreadable = false, std::string pathRelativeTo = "NULL"); std::unique_ptr getPcrByContact(std::string contactEmail); bool TestCanWriteOnContainingFolder(std::string path); std::string getDisplayPath(std::string givenPath, mega::MegaNode* n); int dumpListOfExported(mega::MegaNode* n, const char *timeFormat, std::map *clflags, std::map *cloptions, std::string givenPath); void listnodeshares(mega::MegaNode* n, std::string name, bool listPending, bool onlyPending); void dumpListOfShared(mega::MegaNode* n, std::string givenPath); void dumpListOfAllShared(mega::MegaNode* n, std::string givenPath); void dumpListOfPendingShares(mega::MegaNode* n, std::string givenPath); std::string getCurrentPath(); long long getVersionsSize(mega::MegaNode* n); //acting void verifySharedFolders(mega::MegaApi * api); //verifies unverified shares and broadcasts warning accordingly void loginWithPassword(const char *password); void changePassword(const char *newpassword, std::string pin2fa = ""); void actUponGetExtendedAccountDetails(std::unique_ptr storageDetails, std::unique_ptr extAccountDetails); bool actUponFetchNodes(mega::MegaApi * api, mega::SynchronousRequestListener *srl, int timeout = -1); int actUponLogin(mega::SynchronousRequestListener *srl, int timeout = -1); void actUponLogout(mega::MegaApi& api, mega::MegaError* e, bool keptSession); void actUponLogout(mega::SynchronousRequestListener *srl, bool keptSession, int timeout = 0); int actUponCreateFolder(mega::SynchronousRequestListener *srl, int timeout = 0); int deleteNode(const std::unique_ptr& nodeToDelete, mega::MegaApi* api, int recursive, int force = 0); int deleteNodeVersions(const std::unique_ptr& nodeToDelete, mega::MegaApi* api, int force = 0); void downloadNode(std::string source, std::string localPath, mega::MegaApi* api, mega::MegaNode *node, bool background, bool ignorequotawar, int clientID, std::shared_ptr listener); void uploadNode(const std::map &clflags, const std::map &cloptions, const std::string &receivedPath, mega::MegaApi* api, mega::MegaNode *node, const std::string &newname, MegaCmdMultiTransferListener *multiTransferListener = NULL); void exportNode(mega::MegaNode *n, int64_t expireTime, const std::optional& password = {}, std::map *clflags = nullptr, std::map *cloptions = nullptr); void disableExport(mega::MegaNode *n); std::pair isSharePendingAndVerified(mega::MegaNode *n, const char *email) const; void shareNode(mega::MegaNode *n, std::string with, int level = mega::MegaShare::ACCESS_READ); void disableShare(mega::MegaNode *n, std::string with); void createOrModifyBackup(std::string local, std::string remote, std::string speriod, int numBackups); std::vector listpaths(bool usepcre, std::string askedPath = "", bool discardFiles = false); std::vector listLocalPathsStartingBy(std::string askedPath, bool discardFiles); std::vector getlistusers(); std::vector getNodeAttrs(std::string nodePath); std::vector getUserAttrs(); std::vector getsessions(); std::vector getlistfilesfolders(std::string location); void executecommand(std::vector words, std::map *clflags, std::map *cloptions); //doomedtodie void syncstat(mega::Sync* sync); // const char* treestatename(treestate_t ts); bool is_syncable(const char* name); int loadfile(std::string* name, std::string* data); void signup(std::string name, std::string passwd, std::string email); void signupWithPassword(std::string passwd); void confirm(std::string passwd, std::string email, std::string link); void confirmWithPassword(std::string passwd); int makedir(std::string remotepath, bool recursive, mega::MegaNode *parentnode = NULL); bool IsFolder(std::string path); bool pathExists(const std::string &path); void doDeleteNode(const std::unique_ptr& nodeToDelete, mega::MegaApi* api); void confirmDelete(); void discardDelete(); void confirmDeleteAll(); void discardDeleteAll(); void printTransfersHeader(const unsigned int PATHSIZE, bool printstate=true); void printTransfer(mega::MegaTransfer *transfer, const unsigned int PATHSIZE, bool printstate=true); void printTransferColumnDisplayer(ColumnDisplayer *cd, mega::MegaTransfer *transfer, bool printstate=true); void printBackupHeader(const unsigned int PATHSIZE); void printBackupSummary(int tag, const char *localfolder, const char *remoteparentfolder, std::string status, const unsigned int PATHSIZE); void printBackupHistory(mega::MegaScheduledCopy *backup, const char *timeFormat, mega::MegaNode *parentnode, const unsigned int PATHSIZE); void printBackupDetails(mega::MegaScheduledCopy *backup, const char *timeFormat); void printBackup(int tag, mega::MegaScheduledCopy *backup, const char *timeFormat, const unsigned int PATHSIZE, bool extendedinfo = false, bool showhistory = false, mega::MegaNode *parentnode = NULL); void printBackup(backup_struct *backupstruct, const char *timeFormat, const unsigned int PATHSIZE, bool extendedinfo = false, bool showhistory = false); void doFind(mega::MegaNode* nodeBase, const char *timeFormat, std::map *clflags, std::map *cloptions, std::string word, int printfileinfo, std::string pattern, bool usepcre, mega::m_time_t minTime, mega::m_time_t maxTime, int64_t minSize, int64_t maxSize); void moveToDestination(const std::unique_ptr& n, std::string destiny); void copyNode(mega::MegaNode *n, std::string destiny, mega::MegaNode *tn, std::string &targetuser, std::string &newname); std::string getLPWD(); bool isValidFolder(std::string destiny); bool establishBackup(std::string local, mega::MegaNode *n, int64_t period, std::string periodstring, int numBackups); mega::MegaNode *getBaseNode(std::string thepath, std::string &rest, bool *isrelative = NULL); void getPathParts(std::string path, std::deque *c); // decrypt a link if it's encrypted. Returns false in case of error bool decryptLinkIfEncrypted(mega::MegaApi *api, std::string &publicLink, std::map *cloptions); bool checkAndInformPSA(CmdPetition *inf, bool enforce = false); // Provide a helpful error message for the provided error, setting the current error code in case of an error. std::string formatErrorAndMaySetErrorCode(const mega::MegaError &error); bool checkNoErrors(int errorCode, const std::string &message = ""); bool checkNoErrors(mega::MegaError *error, const std::string &message = "", mega::SyncError syncError = mega::SyncError::NO_SYNC_ERROR); bool checkNoErrors(::mega::SynchronousRequestListener *listener, const std::string &message = "", mega::SyncError syncError = mega::SyncError::NO_SYNC_ERROR); void confirmCancel(const char* confirmlink, const char* pass); bool amIPro(); void processPath(std::string path, bool usepcre, bool& firstone, void (*nodeprocessor)(MegaCmdExecuter *, mega::MegaNode *, bool), MegaCmdExecuter *context = NULL); void catFile(mega::MegaNode *n); void printInfoFile(mega::MegaNode *n, bool &firstone, int PATHSIZE); #ifdef HAVE_LIBUV void removeWebdavLocation(mega::MegaNode *n, bool firstone, std::string name = std::string()); void addWebdavLocation(mega::MegaNode *n, bool firstone, std::string name = std::string()); void removeFtpLocation(mega::MegaNode *n, bool firstone, std::string name = std::string()); void addFtpLocation(mega::MegaNode *n, bool firstone, std::string name = std::string()); #endif bool printUserAttribute(int a, std::string user, bool onlylist = false); bool setProxy(const std::string &url, const std::string &username, const std::string &password, int proxyType); void fetchNodes(mega::MegaApi *api = nullptr, int clientID = -27); void mayExecutePendingStuffInWorkerThread(); }; }//end namespace #endif // MEGACMDEXECUTER_H MEGAcmd-2.5.2_Linux/src/megacmdlogger.cpp000066400000000000000000000270261516543156300202040ustar00rootroot00000000000000/** * @file src/megacmdlogger.cpp * @brief MEGAcmd: Controls message logging * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of the MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include "megacmdlogger.h" #include "megacmdcommonutils.h" #include "megacmdutils.h" #include "megacmd_src_file_list.h" #include #include using namespace mega; namespace megacmd { namespace { constexpr const char* sLogTimestampFormat = "%04d-%02d-%02d_%02d-%02d-%02d.%06d"; thread_local bool isThreadDataSet = false; std::string getNowTimeStr() { return timestampToString(std::chrono::system_clock::now()); } } ThreadData &getCurrentThreadData() { thread_local ThreadData threadData; return threadData; } const char* getCommandPrefixBasedOnMode() { if (isCurrentThreadInteractive()) { return ""; } return "mega-"; } bool isCurrentThreadInteractive() { return isCurrentThreadCmdShell() || !isThreadDataSet; } void setCurrentThreadOutStreams(LoggedStream &outStream, LoggedStream &errStream) { isThreadDataSet = true; getCurrentThreadData().mOutStream = &outStream; getCurrentThreadData().mErrStream = &errStream; } void setCurrentThreadOutCode(int outCode) { isThreadDataSet = true; getCurrentThreadData().mOutCode = outCode; } void setCurrentThreadLogLevel(int logLevel) { isThreadDataSet = true; getCurrentThreadData().mLogLevel = logLevel; } void setCurrentThreadCmdPetition(CmdPetition *cmdPetition) { isThreadDataSet = true; getCurrentThreadData().mCmdPetition = cmdPetition; } void setCurrentThreadIsCmdShell(bool isCmdShell) { isThreadDataSet = true; getCurrentThreadData().mIsCmdShell = isCmdShell; } std::string formatErrorAndMaySetErrorCode(const MegaError &error) { auto code = error.getErrorCode(); if (code == MegaError::API_OK) { return std::string(); } setCurrentThreadOutCode(code); if (code == MegaError::API_EBLOCKED) { //auto reason = sandboxCMD->getReasonblocked(); std::string reason; auto reasonStr = std::string("Account blocked."); if (!reason.empty()) { reasonStr.append("Reason: ").append(reason); } return reasonStr; } else if (code == MegaError::API_EPAYWALL || (code == MegaError::API_EOVERQUOTA /*&& sandboxCMD->storageStatus == MegaApi::STORAGE_STATE_RED*/)) { return "Reached storage quota. You can change your account plan to increase your quota limit. See \"help --upgrade\" for further details"; } return error.getErrorString(); } bool checkNoErrors(int errorCode, const std::string &message) { MegaErrorPrivate e(errorCode); return checkNoErrors(&e, message); } bool checkNoErrors(MegaError *error, const std::string &message, SyncError syncError) { if (!error) { LOG_fatal << "No MegaError at request: " << message; assert(false); return false; } if (error->getErrorCode() == MegaError::API_OK) { if (syncError) { std::unique_ptr megaSyncErrorCode(MegaSync::getMegaSyncErrorCode(syncError)); LOG_info << "Able to " << message << ", but received syncError: " << megaSyncErrorCode.get(); } return true; } auto apiErrorString = formatErrorAndMaySetErrorCode(*error); auto logErrMessage = std::string("Failed to ").append(message).append(": "); if (auto mountResult = error->getMountResult(); mountResult != MegaMount::SUCCESS) //if there is mount error, prefer this one for log message { logErrMessage.append(getMountResultStr(mountResult)); } else { logErrMessage.append(apiErrorString); } if (syncError) { std::unique_ptr megaSyncErrorCode(MegaSync::getMegaSyncErrorCode(syncError)); logErrMessage.append(". ").append(megaSyncErrorCode.get()); } LOG_err << logErrMessage; return false; } bool checkNoErrors(::mega::SynchronousRequestListener *listener, const std::string &message, ::mega::SyncError syncError) { assert(listener); listener->wait(); assert(listener->getError()); return checkNoErrors(listener->getError(), message, syncError); } std::optional> stringToTimestamp(std::string_view str) { if (str.size() != LogTimestampSize) { return std::nullopt; } int years, months, days, hours, minutes, seconds, microseconds; int parsed = std::sscanf(str.data(), sLogTimestampFormat, &years, &months, &days, &hours, &minutes, &seconds, µseconds); if (parsed != 7) { return std::nullopt; } struct std::tm gmt; memset(&gmt, 0, sizeof(struct std::tm)); gmt.tm_year = years - 1900; gmt.tm_mon = months - 1; gmt.tm_mday = days; gmt.tm_hour = hours; gmt.tm_min = minutes; gmt.tm_sec = seconds; #ifdef _WIN32 const time_t t = _mkgmtime(&gmt); #else const time_t t = timegm(&gmt); #endif const auto time_point = std::chrono::system_clock::from_time_t(t); return time_point + std::chrono::microseconds(microseconds); } std::string timestampToString(std::chrono::time_point timestamp) { std::array timebuf; const time_t t = std::chrono::system_clock::to_time_t(timestamp); struct std::tm gmt; memset(&gmt, 0, sizeof(struct std::tm)); mega::m_gmtime(t, &gmt); auto microseconds = std::chrono::duration_cast(timestamp - std::chrono::system_clock::from_time_t(t)); std::snprintf(timebuf.data(), timebuf.size(), sLogTimestampFormat, gmt.tm_year + 1900, gmt.tm_mon + 1, gmt.tm_mday, gmt.tm_hour, gmt.tm_min, gmt.tm_sec, static_cast(microseconds.count() % 1000000)); return std::string(timebuf.data(), LogTimestampSize); } MegaCmdLogger::MegaCmdLogger() : mSdkLoggerLevel(mega::MegaApi::LOG_LEVEL_ERROR), mCmdLoggerLevel(mega::MegaApi::LOG_LEVEL_ERROR), mFlushOnLevel(mega::MegaApi::LOG_LEVEL_WARNING) { } fs::path MegaCmdLogger::getDefaultFilePath() { auto dirs = PlatformDirectories::getPlatformSpecificDirectories(); assert(!dirs->configDirPath().empty()); return dirs->configDirPath() / "megacmdserver.log"; } bool MegaCmdLogger::isMegaCmdSource(const std::string &source) { static const std::set megaCmdSourceFiles = MEGACMD_SRC_FILE_LIST; // Remove the line number (since source has the format "filename.cpp:1234") std::string_view filename(source); filename = filename.substr(0, filename.find(':')); return megaCmdSourceFiles.find(filename) != megaCmdSourceFiles.end(); } const char * loglevelToShortPaddedString(int loglevel) { static constexpr std::array logLevels = { "CRIT ", // LOG_LEVEL_FATAL "ERR ", // LOG_LEVEL_ERROR "WARN ", // LOG_LEVEL_WARNING "INFO ", // LOG_LEVEL_INFO "DBG ", // LOG_LEVEL_DEBUG "DTL " // LOG_LEVEL_MAX }; assert (loglevel >= 0 && loglevel < static_cast(logLevels.size())); return logLevels[static_cast(loglevel)]; } void MegaCmdLogger::formatLogToStream(LoggedStream &stream, std::string_view time, int logLevel, const char *source, const char *message, bool surround) { if (surround) { stream << "["; } stream << time; if (!isMegaCmdSource(source)) { stream << " sdk "; } else { stream << " cmd "; } stream << loglevelToShortPaddedString(logLevel) << message; if (surround) { stream << "]"; } else { stream << " [" << source << "]"; } stream << '\n'; if (logLevel <= mFlushOnLevel) { stream.flush(); } } bool MegaCmdLogger::shouldIgnoreMessage(int logLevel, const char *source, const char *message) const { UNUSED(logLevel); if (!isMegaCmdSource(source)) { const int sdkLoggerLevel = getSdkLoggerLevel(); if (sdkLoggerLevel <= MegaApi::LOG_LEVEL_DEBUG && !strcmp(message, "Request (RETRY_PENDING_CONNECTIONS) starting")) { return true; } if (sdkLoggerLevel <= MegaApi::LOG_LEVEL_DEBUG && !strcmp(message, "Request (RETRY_PENDING_CONNECTIONS) finished")) { return true; } } return false; } bool MegaCmdSimpleLogger::shouldLogToStream(int logLevel, const char* source) const { if (isMegaCmdSource(source)) { return logLevel <= getCmdLoggerLevel(); } return logLevel <= getSdkLoggerLevel(); } bool MegaCmdSimpleLogger::shouldLogToClient(int logLevel, const char* source) const { // If comming from this logger current thread if (&OUTSTREAM == &mLoggedStream) { return false; } const int defaultLogLevel = isMegaCmdSource(source) ? getCmdLoggerLevel() : getSdkLoggerLevel(); int currentThreadLogLevel = getCurrentThreadLogLevel(); if (currentThreadLogLevel < 0) // this thread has no log level assigned { currentThreadLogLevel = defaultLogLevel; } return logLevel <= currentThreadLogLevel; } MegaCmdSimpleLogger::MegaCmdSimpleLogger(bool logToOutStream, int sdkLoggerLevel, int cmdLoggerLevel) : MegaCmdLogger(), mLoggedStream(Instance::Get().getLoggedStream()), mOutStream(&COUT), mLogToOutStream(logToOutStream) { setSdkLoggerLevel(sdkLoggerLevel); setCmdLoggerLevel(cmdLoggerLevel); } int MegaCmdSimpleLogger::getMaxLogLevel() const { return std::max(getCurrentThreadLogLevel(), MegaCmdLogger::getMaxLogLevel()); } void MegaCmdSimpleLogger::log(const char * /*time*/, int logLevel, const char *source, const char *message) { thread_local bool isRecursive = false; if (isRecursive) { std::cerr << "ERROR: Attempt to log message recursively from " << source << ": " << message << std::endl; assert(false); return; } ScopeGuard g([] { isRecursive = false; }); isRecursive = true; if (!isValidUtf8(message, strlen(message))) { constexpr const char* invalid = ""; message = invalid; ASSERT_UTF8_BREAK("Attempt to log invalid utf8 string"); } if (shouldIgnoreMessage(logLevel, source, message)) { return; } if (shouldLogToStream(logLevel, source)) { // log to _file_ (e.g: FileRotatingLoggedStream) const std::string nowTimeStr = getNowTimeStr(); formatLogToStream(mLoggedStream, nowTimeStr, logLevel, source, message); if (mLogToOutStream) // log to stdout { #ifdef _WIN32 WindowsUtf8StdoutGuard utf8Guard; #endif formatLogToStream(mOutStream, nowTimeStr, logLevel, source, message); } } if (shouldLogToClient(logLevel, source)) { const std::string nowTimeStr = getNowTimeStr(); formatLogToStream(getCurrentThreadErrStream(), nowTimeStr, logLevel, source, message, true); } } LoggedStreamDefaultFile::LoggedStreamDefaultFile() : LoggedStreamOutStream(nullptr), mFstream(MegaCmdLogger::getDefaultFilePath()) { out = &mFstream; if (!mFstream.is_open()) { CERR << "Cannot open default log file " << MegaCmdLogger::getDefaultFilePath() << std::endl; } } }//end namespace MEGAcmd-2.5.2_Linux/src/megacmdlogger.h000066400000000000000000000400561516543156300176470ustar00rootroot00000000000000/** * @file src/megacmdlogger.h * @brief MEGAcmd: Controls message logging * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of the MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #ifndef MEGACMDLOGGER_H #define MEGACMDLOGGER_H #include "megacmdcommonutils.h" #include "megacmd.h" #include "comunicationsmanager.h" #define OUTSTREAM getCurrentThreadOutStream() namespace megacmd { // Provide a helpful error message for the provided error, setting the current error code in case of an error. std::string formatErrorAndMaySetErrorCode(const mega::MegaError &error); bool checkNoErrors(int errorCode, const std::string &message = ""); bool checkNoErrors(mega::MegaError *error, const std::string &message = "", mega::SyncError syncError = mega::SyncError::NO_SYNC_ERROR); bool checkNoErrors(::mega::SynchronousRequestListener *listener, const std::string &message = "", mega::SyncError syncError = mega::SyncError::NO_SYNC_ERROR); #ifdef WIN32 inline ::mega::SimpleLogger &operator<<(::mega::SimpleLogger& sl, const fs::path& path) { return sl << megacmd::pathAsUtf8(path); } template inline std::enable_if_t, std::wstring>, ::mega::SimpleLogger &> operator<<(::mega::SimpleLogger& sl, const T& wstr) { return sl << megacmd::utf16ToUtf8(wstr); } #endif // String used to transmit binary data class BinaryStringView { public: BinaryStringView(char *buffer, size_t size) : mValue(buffer, size) {} const std::string_view& get() const { return mValue; } std::string_view& get() { return mValue; } private: std::string_view mValue; }; class LoggedStream { public: LoggedStream() { out = nullptr; } LoggedStream(OUTSTREAMTYPE *_out) : out(_out) {} virtual ~LoggedStream() = default; virtual bool isClientConnected() { return true; } virtual const LoggedStream& operator<<(const char& v) const = 0; virtual const LoggedStream& operator<<(const char* v) const = 0; #ifdef _WIN32 virtual const LoggedStream& operator<<(std::wstring v) const = 0; #endif virtual const LoggedStream& operator<<(std::string v) const = 0; virtual const LoggedStream& operator<<(BinaryStringView v) const = 0; virtual const LoggedStream& operator<<(std::string_view v) const = 0; virtual const LoggedStream& operator<<(int v) const = 0; virtual const LoggedStream& operator<<(unsigned int v) const = 0; virtual const LoggedStream& operator<<(long unsigned int v) const = 0; virtual const LoggedStream& operator<<(long long int v) const = 0; virtual const LoggedStream& operator<<(long long unsigned int v) const = 0; virtual const LoggedStream& operator<<(std::ios_base v) const = 0; virtual const LoggedStream& operator<<(std::ios_base *v) const = 0; virtual LoggedStream const& operator<<(OUTSTREAMTYPE& (*F)(OUTSTREAMTYPE&)) const = 0; virtual void flush() {} protected: OUTSTREAMTYPE * out; }; class LoggedStreamNull : public LoggedStream { public: const LoggedStream& operator<<(const char& v) const override { return *this; } const LoggedStream& operator<<(const char* v) const override { return *this; } #ifdef _WIN32 const LoggedStream& operator<<(std::wstring v) const override { return *this; } #endif const LoggedStream& operator<<(std::string v) const override { return *this; } const LoggedStream& operator<<(BinaryStringView v) const override { return *this; } const LoggedStream& operator<<(std::string_view v) const override { return *this; } const LoggedStream& operator<<(int v) const override { return *this; } const LoggedStream& operator<<(unsigned int v) const override { return *this; } const LoggedStream& operator<<(long unsigned int v) const override { return *this; } const LoggedStream& operator<<(long long int v) const override { return *this; } const LoggedStream& operator<<(long long unsigned int v) const override { return *this; } const LoggedStream& operator<<(std::ios_base v) const override { return *this; } const LoggedStream& operator<<(std::ios_base *v) const override { return *this; } LoggedStream const& operator<<(OUTSTREAMTYPE& (*F)(OUTSTREAMTYPE&)) const override { return *this; } virtual ~LoggedStreamNull() = default; }; class DefaultLoggedStream { std::unique_ptr mTheStream; public: void setLoggedStream(std::unique_ptr &&loggedStream) { mTheStream = std::move(loggedStream); } LoggedStream &getLoggedStream() { if (!mTheStream) { mTheStream = std::make_unique(); } assert(mTheStream); return *mTheStream.get(); } }; class LoggedStreamOutStream : public LoggedStream { public: LoggedStreamOutStream(OUTSTREAMTYPE *out) : LoggedStream(out) {} virtual ~LoggedStreamOutStream() = default; virtual bool isClientConnected() override { return true; } virtual const LoggedStream& operator<<(const char& v) const override { *out << v;return *this; } virtual const LoggedStream& operator<<(const char* v) const override { *out << v;return *this; } #ifdef _WIN32 virtual const LoggedStream& operator<<(std::wstring v) const override { *out << v;return *this; } virtual const LoggedStream& operator<<(std::string_view v) const override { *out << std::string(v);return *this; } #else virtual const LoggedStream& operator<<(std::string_view v) const override { *out << v;return *this; } #endif virtual const LoggedStream& operator<<(std::string v) const override { *out << v;return *this; } virtual const LoggedStream& operator<<(BinaryStringView v) const override { #ifdef _WIN32 assert(false && "wostream cannot take binary data directly"); #else *out << v.get(); #endif return *this; } virtual const LoggedStream& operator<<(int v) const override { *out << v;return *this; } virtual const LoggedStream& operator<<(unsigned int v) const override { *out << v;return *this; } virtual const LoggedStream& operator<<(long unsigned int v) const override { *out << v;return *this; } virtual const LoggedStream& operator<<(long long int v) const override { *out << v;return *this; } virtual const LoggedStream& operator<<(long long unsigned int v) const override { *out << v;return *this; } virtual const LoggedStream& operator<<(std::ios_base v) const override { *out << &v;return *this; } virtual const LoggedStream& operator<<(std::ios_base *v) const override { *out << v;return *this; } virtual LoggedStream const& operator<<(OUTSTREAMTYPE& (*F)(OUTSTREAMTYPE&)) const override { if (out) F(*out); return *this; } virtual void flush() override { out->flush(); } }; class LoggedStreamDefaultFile : public LoggedStreamOutStream { OUTFSTREAMTYPE mFstream; public: LoggedStreamDefaultFile(); virtual ~LoggedStreamDefaultFile() = default; }; class LoggedStreamPartialOutputs : public LoggedStream { public: LoggedStreamPartialOutputs(ComunicationsManager *_cm, CmdPetition *_inf) : cm(_cm), inf(_inf) {} virtual bool isClientConnected() override { return inf && !inf->clientDisconnected; } virtual const LoggedStream& operator<<(const char& v) const override { OUTSTRINGSTREAM os; os << v; OUTSTRING s = os.str(); cm->sendPartialOutput(inf, &s); return *this; } virtual const LoggedStream& operator<<(const char* v) const override { OUTSTRINGSTREAM os; os << v; OUTSTRING s = os.str(); cm->sendPartialOutput(inf, &s); return *this; } #ifdef _WIN32 virtual const LoggedStream& operator<<(std::wstring v) const override { cm->sendPartialOutput(inf, &v); return *this; } virtual const LoggedStream& operator<<(std::string v) const override { cm->sendPartialOutput(inf, (char *)v.data(), v.size()); return *this; } #else virtual const LoggedStream& operator<<(std::string v) const override { cm->sendPartialOutput(inf, &v); return *this; } #endif virtual const LoggedStream& operator<<(BinaryStringView v) const override { cm->sendPartialOutput(inf, (char*) v.get().data(), v.get().size(), true); return *this; } virtual const LoggedStream& operator<<(std::string_view v) const override { cm->sendPartialOutput(inf, (char*) v.data(), v.size()); return *this; } virtual const LoggedStream& operator<<(int v) const override { OUTSTRINGSTREAM os; os << v; OUTSTRING s = os.str(); cm->sendPartialOutput(inf, &s); return *this; } virtual const LoggedStream& operator<<(unsigned int v) const override { OUTSTRINGSTREAM os; os << v; OUTSTRING s = os.str(); cm->sendPartialOutput(inf, &s); return *this; } virtual const LoggedStream& operator<<(long unsigned int v) const override { OUTSTRINGSTREAM os; os << v; OUTSTRING s = os.str(); cm->sendPartialOutput(inf, &s); return *this; } virtual const LoggedStream& operator<<(long long int v) const override { OUTSTRINGSTREAM os; os << v; OUTSTRING s = os.str(); cm->sendPartialOutput(inf, &s); return *this; } virtual const LoggedStream& operator<<(long long unsigned int v) const override { OUTSTRINGSTREAM os; os << v; OUTSTRING s = os.str(); cm->sendPartialOutput(inf, &s); return *this; } virtual const LoggedStream& operator<<(std::ios_base v) const override { *out << &v;return *this; } virtual const LoggedStream& operator<<(std::ios_base *v) const override { OUTSTRINGSTREAM os; os << v; OUTSTRING s = os.str(); cm->sendPartialOutput(inf, &s); return *this; } LoggedStream const& operator<<(OUTSTREAMTYPE& (*F)(OUTSTREAMTYPE&)) const override { OUTSTRINGSTREAM os; os << F; OUTSTRING s = os.str(); cm->sendPartialOutput(inf, &s); return *this; } virtual ~LoggedStreamPartialOutputs() = default; protected: ComunicationsManager *cm; CmdPetition *inf; }; class LoggedStreamPartialErrors : public LoggedStream { public: LoggedStreamPartialErrors(ComunicationsManager *_cm, CmdPetition *_inf) : cm(_cm), inf(_inf) {} virtual bool isClientConnected() override { return inf && !inf->clientDisconnected; } virtual const LoggedStream& operator<<(const char& v) const override { OUTSTRINGSTREAM os; os << v; OUTSTRING s = os.str(); cm->sendPartialError(inf, &s); return *this; } virtual const LoggedStream& operator<<(const char* v) const override { OUTSTRINGSTREAM os; os << v; OUTSTRING s = os.str(); cm->sendPartialError(inf, &s); return *this; } #ifdef _WIN32 virtual const LoggedStream& operator<<(std::wstring v) const override { cm->sendPartialError(inf, &v); return *this; } virtual const LoggedStream& operator<<(std::string v) const override { cm->sendPartialError(inf, (char *)v.data(), v.size()); return *this; } #else virtual const LoggedStream& operator<<(std::string v) const override { cm->sendPartialError(inf, &v); return *this; } #endif virtual const LoggedStream& operator<<(BinaryStringView v) const override { cm->sendPartialError(inf, (char*) v.get().data(), v.get().size(), true); return *this; } virtual const LoggedStream& operator<<(std::string_view v) const override { cm->sendPartialError(inf, (char*) v.data(), v.size()); return *this; } virtual const LoggedStream& operator<<(int v) const override { OUTSTRINGSTREAM os; os << v; OUTSTRING s = os.str(); cm->sendPartialError(inf, &s); return *this; } virtual const LoggedStream& operator<<(unsigned int v) const override { OUTSTRINGSTREAM os; os << v; OUTSTRING s = os.str(); cm->sendPartialError(inf, &s); return *this; } virtual const LoggedStream& operator<<(long unsigned int v) const override { OUTSTRINGSTREAM os; os << v; OUTSTRING s = os.str(); cm->sendPartialError(inf, &s); return *this; } virtual const LoggedStream& operator<<(long long int v) const override { OUTSTRINGSTREAM os; os << v; OUTSTRING s = os.str(); cm->sendPartialError(inf, &s); return *this; } virtual const LoggedStream& operator<<(long long unsigned int v) const override { OUTSTRINGSTREAM os; os << v; OUTSTRING s = os.str(); cm->sendPartialError(inf, &s); return *this; } virtual const LoggedStream& operator<<(std::ios_base v) const override { *out << &v;return *this; } virtual const LoggedStream& operator<<(std::ios_base *v) const override { OUTSTRINGSTREAM os; os << v; OUTSTRING s = os.str(); cm->sendPartialError(inf, &s); return *this; } LoggedStream const& operator<<(OUTSTREAMTYPE& (*F)(OUTSTREAMTYPE&)) const override { OUTSTRINGSTREAM os; os << F; OUTSTRING s = os.str(); cm->sendPartialError(inf, &s); return *this; } virtual ~LoggedStreamPartialErrors() = default; protected: ComunicationsManager *cm; CmdPetition *inf; }; struct ThreadData { LoggedStream *mOutStream = &Instance::Get().getLoggedStream(); LoggedStream *mErrStream = &Instance::Get().getLoggedStream(); int mLogLevel = -1; int mOutCode = 0; CmdPetition *mCmdPetition = nullptr; bool mIsCmdShell = false; }; ThreadData &getCurrentThreadData(); const char* getCommandPrefixBasedOnMode(); bool isCurrentThreadInteractive(); inline LoggedStream &getCurrentThreadOutStream() { return *getCurrentThreadData().mOutStream; } inline LoggedStream &getCurrentThreadErrStream() { return *getCurrentThreadData().mErrStream; } inline int getCurrentThreadLogLevel() { return getCurrentThreadData().mLogLevel; } inline int getCurrentThreadOutCode() { return getCurrentThreadData().mOutCode; } inline CmdPetition *getCurrentThreadCmdPetition() { return getCurrentThreadData().mCmdPetition; } inline bool isCurrentThreadCmdShell() { return getCurrentThreadData().mIsCmdShell; } void setCurrentThreadOutStreams(LoggedStream &outStream, LoggedStream &errStream); void setCurrentThreadOutCode(int outCode); void setCurrentThreadLogLevel(int logLevel); void setCurrentThreadCmdPetition(CmdPetition *cmdPetition); void setCurrentThreadIsCmdShell(bool isCmdShell); constexpr size_t LogTimestampSize = std::char_traits::length("2024-12-27_16-33-12.654787"); std::optional> stringToTimestamp(std::string_view str); std::string timestampToString(std::chrono::time_point timestamp); class MegaCmdLogger : public mega::MegaLogger { int mSdkLoggerLevel; int mCmdLoggerLevel; int mFlushOnLevel; protected: static bool isMegaCmdSource(const std::string &source); void formatLogToStream(LoggedStream& stream, std::string_view time, int logLevel, const char *source, const char *message, bool surround = false); bool shouldIgnoreMessage(int logLevel, const char *source, const char *message) const; public: MegaCmdLogger(); virtual ~MegaCmdLogger() = default; virtual void log(const char *time, int loglevel, const char *source, const char *message) override = 0; void setSdkLoggerLevel(int sdkLoggerLevel) { mSdkLoggerLevel = sdkLoggerLevel; } void setCmdLoggerLevel(int cmdLoggerLevel) { mCmdLoggerLevel = cmdLoggerLevel; } void setFlushOnLevel(int flushOnLevel) { mFlushOnLevel = flushOnLevel; } int getSdkLoggerLevel() const { return mSdkLoggerLevel; } int getCmdLoggerLevel() const { return mCmdLoggerLevel; } int getFlushOnLevel() const { return mFlushOnLevel; } virtual int getMaxLogLevel() const { return std::max(mSdkLoggerLevel, mCmdLoggerLevel); } static fs::path getDefaultFilePath(); }; class MegaCmdSimpleLogger final : public MegaCmdLogger { LoggedStream &mLoggedStream; // to log into files (e.g. FileRotatingLoggedStream) LoggedStreamOutStream mOutStream; // to log into stdout bool mLogToOutStream; bool shouldLogToStream(int logLevel, const char *source) const; bool shouldLogToClient(int logLevel, const char *source) const; public: MegaCmdSimpleLogger(bool logToOutStream, int sdkLoggerLevel, int cmdLoggerLevel); void log(const char *time, int loglevel, const char *source, const char *message) override; int getMaxLogLevel() const override; }; }//end namespace #endif // MEGACMDLOGGER_H MEGAcmd-2.5.2_Linux/src/megacmdplatform.h000066400000000000000000000002561516543156300202120ustar00rootroot00000000000000#ifndef MEGACMDPLATFORM_H #define MEGACMDPLATFORM_H namespace megacmd { #ifdef __MACH__ bool registerUpdateDaemon(); #endif }//end namespace #endif // MEGACMDPLATFORM_H MEGAcmd-2.5.2_Linux/src/megacmdplatform.mm000066400000000000000000000043331516543156300203740ustar00rootroot00000000000000#include "megacmdplatform.h" #include #import #include #include #include #include #ifdef __MACH__ namespace megacmd { bool registerUpdateDaemon() { NSDictionary *plistd = @{ @"Label": @"megacmd.mac.megaupdater", @"ProgramArguments": @[@"/Applications/MEGAcmd.app/Contents/MacOS/MEGAcmdUpdater", @"--emergency-update"], @"StartInterval": @7200, @"RunAtLoad": @true, @"StandardErrorPath": @"/dev/null", @"StandardOutPath": @"/dev/null", }; const char* home = getenv("HOME"); if (!home) { return false; } NSString *homepath = [NSString stringWithUTF8String:home]; if (!homepath) { return false; } NSFileManager *fileManager = [NSFileManager defaultManager]; NSString *dirPath = [homepath stringByAppendingString:@"/Library/LaunchAgents/"]; NSURL *dirURL = [NSURL fileURLWithPath:dirPath isDirectory:YES]; if ([fileManager createDirectoryAtURL:dirURL withIntermediateDirectories:YES attributes:nil error:nil] == NO) { return false; } NSString *fullPath = [dirPath stringByAppendingString:@"megacmd.mac.megaupdater.plist"]; if ([plistd writeToFile:fullPath atomically:YES] == NO) { return false; } std::string path = [fullPath UTF8String]; chmod(path.c_str(), S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); int pid = fork(); if (pid) { int status = 0; waitpid(pid, &status, 0); if ( WIFEXITED(status) ) { int exit_code = WEXITSTATUS(status); return (!exit_code); } else { std::cerr << " failed return. errno=" << errno << std::endl; return false; } } else { char **argv = new char*[4]; int i = 0; argv[i++]="/bin/bash"; argv[i++]="-c"; std::string ls="launchctl unload "; ls.append(path); ls.append(" ; launchctl load -w "); ls.append(path); argv[i++]=(char *)ls.c_str(); argv[i++]=NULL; execv(argv[0],argv); } return false; } }// end of namespace #endif MEGAcmd-2.5.2_Linux/src/megacmdsandbox.cpp000066400000000000000000000062361516543156300203630ustar00rootroot00000000000000/** * @file src/megacmdsandbox.cpp * @brief MegaCMD: A sandbox class to store status variables * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of the MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include "megacmdsandbox.h" using namespace mega; namespace megacmd { bool MegaCmdSandbox::isOverquota() const { return overquota; } void MegaCmdSandbox::setOverquota(bool value) { overquota = value; } void MegaCmdSandbox::resetSandBox() { this->overquota = false; this->istemporalbandwidthvalid = false; this->temporalbandwidth = 0; this->temporalbandwithinterval = 0; this->lastQuerytemporalBandwith = m_time(); this->timeOfOverquota = m_time(); this->secondsOverQuota = 0; this->storageStatus = 0; this->receivedStorageSum = 0; this->totalStorage = 0; this->timeOfPSACheck = 0; this->lastPSAnumreceived = -1; if (reasonblocked.size()) removeGreetingStatusAllListener(string("message:").append(reasonblocked)); this->reasonblocked = ""; this->reasonPending = false; } std::string MegaCmdSandbox::getReasonblocked() { std::unique_lock ul(reasonBlockedMutex); if (reasonPending) { std::future f = reasonPromise.get_future(); ul.unlock(); try { return f.get(); } catch(std::future_error) { ul.lock(); //already retrieved or broken promise (promise is replaced): another state. This is unlikely } } return reasonblocked; } void MegaCmdSandbox::setReasonPendingPromise() { std::lock_guard g(reasonBlockedMutex); reasonPending = true; this->reasonPromise = std::promise(); } void MegaCmdSandbox::doSetReasonBlocked(const std::string &value) { broadcastMessage(value); if (reasonblocked.size()) removeGreetingStatusAllListener(string("message:").append(reasonblocked)); if (value.size()) appendGreetingStatusAllListener(string("message:").append(value)); reasonblocked = value; } void MegaCmdSandbox::setReasonblocked(const std::string &value) { std::lock_guard g(reasonBlockedMutex); doSetReasonBlocked(value); } void MegaCmdSandbox::setPromisedReasonblocked(const std::string &value) { std::lock_guard g(reasonBlockedMutex); doSetReasonBlocked(value); reasonPending = false; this->reasonPromise.set_value(value); } MegaCmdSandbox::MegaCmdSandbox() { this->overquota = false; this->istemporalbandwidthvalid = false; this->temporalbandwidth = 0; this->temporalbandwithinterval = 0; this->lastQuerytemporalBandwith = m_time(); this->timeOfOverquota = m_time(); this->secondsOverQuota = 0; this->storageStatus = 0; this->receivedStorageSum = 0; this->totalStorage = 0; this->timeOfPSACheck = 0; this->lastPSAnumreceived = -1; } }//end namespace MEGAcmd-2.5.2_Linux/src/megacmdsandbox.h000066400000000000000000000061001516543156300200160ustar00rootroot00000000000000/** * @file src/megacmdsandbox.h * @brief MegaCMD: A sandbox class to store status variables * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of the MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #ifndef MEGACMDSANDBOX_H #define MEGACMDSANDBOX_H #include #include #include #include #include "megacmdexecuter.h" namespace megacmd { class MegaCmdExecuter; class NodesCurrentPromise { using Promise_t = std::promise; using Future_t = std::future; std::mutex mMutex; std::shared_ptr mAccountUpToDatePromise; bool pendingCompletion = false; public: void initiatePromise() { std::lock_guard g(mMutex); assert(!pendingCompletion); mAccountUpToDatePromise = std::make_shared(); pendingCompletion = true; } void setCompleted() { std::lock_guard g(mMutex); mAccountUpToDatePromise->set_value(true); } Future_t getFuture() { std::lock_guard g(mMutex); assert(mAccountUpToDatePromise); return mAccountUpToDatePromise->get_future(); } void reset() { std::lock_guard g(mMutex); if (mAccountUpToDatePromise && pendingCompletion) { mAccountUpToDatePromise->set_value(false); pendingCompletion = false; } } void fulfil() { std::lock_guard g(mMutex); if (mAccountUpToDatePromise && pendingCompletion) { mAccountUpToDatePromise->set_value(true); pendingCompletion = false; } } ~NodesCurrentPromise() { reset(); } }; class MegaCmdSandbox { private: bool overquota; std::mutex reasonBlockedMutex; std::string reasonblocked; bool reasonPending = false; std::promise reasonPromise; void doSetReasonBlocked(const std::string &value); public: NodesCurrentPromise mNodesCurrentPromise; bool istemporalbandwidthvalid; long long temporalbandwidth; long long temporalbandwithinterval; ::mega::m_time_t lastQuerytemporalBandwith; ::mega::m_time_t timeOfOverquota; ::mega::m_time_t secondsOverQuota; ::mega::m_time_t timeOfPSACheck; int lastPSAnumreceived; int storageStatus; long long receivedStorageSum; long long totalStorage; MegaCmdExecuter * cmdexecuter = nullptr; public: MegaCmdSandbox(); bool isOverquota() const; void setOverquota(bool value); void resetSandBox(); std::string getReasonblocked(); void setReasonPendingPromise(); void setReasonblocked(const std::string &value); void setPromisedReasonblocked(const std::string &value); }; }//end namespace #endif // MEGACMDSANDBOX_H MEGAcmd-2.5.2_Linux/src/megacmdshell/000077500000000000000000000000001516543156300173215ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/src/megacmdshell/megacmdshell.cpp000066400000000000000000001734011516543156300224600ustar00rootroot00000000000000/** * @file src/megacmdshell.cpp * @brief MEGAcmd: Interactive CLI and service application * This is the shell application * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is distributed under the terms of the GNU General Public * License, see http://www.gnu.org/copyleft/gpl.txt * for details. * * This file 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. */ #include "megacmdshell.h" #include "megacmdshellcommunications.h" #include "megacmdshellcommunicationsnamedpipes.h" #include "../megacmdcommonutils.h" #define USE_VARARGS #define PREFER_STDARG #ifdef NO_READLINE #include #else #include #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #define PROGRESS_COMPLETE -2 #define SPROGRESS_COMPLETE "-2" #define PROMPT_MAX_SIZE 128 #ifndef _WIN32 #include #include #include #else #define strdup _strdup #endif #define SSTR( x ) static_cast< const std::ostringstream & >( \ ( std::ostringstream() << std::dec << x ) ).str() using namespace mega; namespace megacmd { using namespace std; #if defined(NO_READLINE) && defined(_WIN32) CONSOLE_CLASS* console = NULL; #endif // utility functions #ifndef NO_READLINE string getCurrentLine() { char *saved_line = rl_copy_text(0, rl_point); string toret(saved_line); free(saved_line); saved_line = NULL; return toret; } #endif // end utily functions string clientID; //identifier for a registered state listener // Console related functions: void console_readpwchar(char* pw_buf, int pw_buf_size, int* pw_buf_pos, char** line) { #ifdef _WIN32 char c; DWORD cread; if (ReadConsole(GetStdHandle(STD_INPUT_HANDLE), &c, 1, &cread, NULL) == 1) { if ((c == 8) && *pw_buf_pos) { (*pw_buf_pos)--; } else if (c == 13) { *line = (char*)malloc(*pw_buf_pos + 1); memcpy(*line, pw_buf, *pw_buf_pos); (*line)[*pw_buf_pos] = 0; } else if (*pw_buf_pos < pw_buf_size) { pw_buf[(*pw_buf_pos)++] = c; } } #else // FIXME: UTF-8 compatibility char c; if (read(STDIN_FILENO, &c, 1) == 1) { if (c == 8 && *pw_buf_pos) { (*pw_buf_pos)--; } else if (c == 13) { *line = (char*) malloc(*pw_buf_pos + 1); memcpy(*line, pw_buf, *pw_buf_pos); (*line)[*pw_buf_pos] = 0; } else if (*pw_buf_pos < pw_buf_size) { pw_buf[(*pw_buf_pos)++] = c; } } #endif } void console_setecho(bool echo) { #ifdef _WIN32 HANDLE hCon = GetStdHandle(STD_INPUT_HANDLE); DWORD mode; GetConsoleMode(hCon, &mode); if (echo) { mode |= ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT; } else { mode &= ~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT); } SetConsoleMode(hCon, mode); #else //do nth #endif } std::atomic_bool alreadyFinished = false; //flag to show progress std::atomic percentDowloaded = 0.0; // to show progress // password change-related state information string oldpasswd; string newpasswd; bool doExit = false; bool doReboot = false; static std::atomic_bool handlerOverridenByExternalThread(false); static std::mutex handlerInstallerMutex; static std::atomic_bool requirepromptinstall(true); std::atomic_bool procesingline = false; std::atomic_bool promptreinstalledwhenprocessingline = false; std::atomic_bool serverTryingToLog = false; static char dynamicprompt[PROMPT_MAX_SIZE]; static char* g_line; static prompttype prompt = COMMAND; static char pw_buf[256]; static int pw_buf_pos = 0; string loginname; bool signingup = false; string signupline; string passwdline; string linktoconfirm; bool confirminglink = false; bool confirmingcancellink = false; // communications with megacmdserver: MegaCmdShellCommunications *comms; std::mutex mutexPrompt; void printWelcomeMsg(unsigned int width = 0); #ifndef NO_READLINE void install_rl_handler(const char *theprompt, bool external = true); #endif std::mutex lastMessageMutex; std::string lastMessage; bool notRepeatedMessage(const string &newMessage) { std::lock_guard g(lastMessageMutex); bool toret = lastMessage.compare(newMessage); if (toret) { lastMessage = newMessage; } return toret; } void cleanLastMessage() { std::lock_guard g(lastMessageMutex); lastMessage = string(); } void statechangehandle(string statestring, MegaCmdShellCommunications &comsManager) { char statedelim[2]={(char)0x1F,'\0'}; size_t nextstatedelimitpos = statestring.find(statedelim); static bool shown_partial_progress = false; bool promtpReceivedBool = false; unsigned int width = getNumberOfCols(75); if (width > 1 ) width--; while (nextstatedelimitpos!=string::npos && statestring.size()) { string newstate = statestring.substr(0,nextstatedelimitpos); statestring=statestring.substr(nextstatedelimitpos+1); nextstatedelimitpos = statestring.find(statedelim); if (newstate.compare(0, strlen("prompt:"), "prompt:") == 0) { if (serverTryingToLog) { printCenteredContentsCerr(string("MEGAcmd Server is still trying to log in. Still, some commands are available.\n" "Type \"help\", to list them.").c_str(), width); } // When main thread is executing a command (procesingline), do not redisplay here. // The state listener runs concurrently with command output; redisplay would use // readline (\r, control chars) and interleave with stdout. // The main loop will show the prompt after process_line returns. const bool redisplay = !procesingline; changeprompt(newstate.substr(strlen("prompt:")).c_str(), redisplay); comsManager.markServerReady(); promtpReceivedBool = true; } else if (newstate.compare(0, strlen("endtransfer:"), "endtransfer:") == 0) { string rest = newstate.substr(strlen("endtransfer:")); if (rest.size() >=3) { bool isdown = rest.at(0) == 'D'; string path = rest.substr(2); stringstream os; if (shown_partial_progress) { os << endl; } os << (isdown?"Download":"Upload") << " finished: " << path << endl; #ifdef _WIN32 wstring wbuffer; stringtolocalw((const char*)os.str().data(),&wbuffer); WindowsUtf8StdoutGuard utf8Guard; OUTSTREAM << wbuffer << flush; #else OUTSTREAM << os.str(); #endif } } else if (newstate.compare(0, strlen("loged:"), "loged:") == 0) { serverTryingToLog = false; } else if (newstate.compare(0, strlen("login:"), "login:") == 0) { serverTryingToLog = true; printCenteredContentsCerr(string("Resuming session ... ").c_str(), width, false); } else if (newstate.compare(0, strlen("message:"), "message:") == 0) { if (notRepeatedMessage(newstate)) //to avoid repeating messages { std::string_view messageContents = std::string_view(newstate).substr(strlen("message:")); string contents(messageContents); replaceAll(contents, "%mega-%", ""); #ifdef _WIN32 WindowsUtf8StdoutGuard utf8Guard; #else StdoutMutexGuard stdoutGuard; #endif if (messageContents.rfind("-----", 0) != 0) { if (!procesingline || promptreinstalledwhenprocessingline || shown_partial_progress) { OUTSTREAM << endl; } printCenteredContents(contents, width); requirepromptinstall = true; #ifndef NO_READLINE if (prompt == COMMAND && promtpReceivedBool) { std::lock_guard g(mutexPrompt); redisplay_prompt(); } #endif } else { requirepromptinstall = true; OUTSTREAM << endl << contents << endl; } } } else if (newstate.compare(0, strlen("clientID:"), "clientID:") == 0) { clientID = newstate.substr(strlen("clientID:")).c_str(); } else if (newstate.compare(0, strlen("progress:"), "progress:") == 0) { string rest = newstate.substr(strlen("progress:")); size_t nexdel = rest.find(":"); string received = rest.substr(0,nexdel); rest = rest.substr(nexdel+1); nexdel = rest.find(":"); string total = rest.substr(0,nexdel); string title; if ( (nexdel != string::npos) && (nexdel < rest.size() ) ) { rest = rest.substr(nexdel+1); nexdel = rest.find(":"); title = rest.substr(0,nexdel); } if (received!=SPROGRESS_COMPLETE) { shown_partial_progress = true; } else { shown_partial_progress = false; } long long completed = received == SPROGRESS_COMPLETE ? PROGRESS_COMPLETE : charstoll(received.c_str()); const char * progressTitle = title.empty() ? "TRANSFERRING" : title.c_str(); printprogress(completed, charstoll(total.c_str()), progressTitle); } else if (newstate == "ack") { // do nothing, all good } else if (newstate == "restart") { doExit = true; doReboot = true; comsManager.markServerIsUpdating(); // to avoid mensajes about server down sleepSeconds(3); // Give a while for server to restart changeprompt("RESTART REQUIRED BY SERVER (due to an update). Press any key to continue.", true); } else { if (shown_partial_progress) { OUTSTREAM << endl; } cerr << "received unrecognized state change: [" << newstate << "]" << endl; //sleep a while to avoid continuous looping sleepSeconds(1); } if (newstate.compare(0, strlen("progress:"), "progress:") != 0) { shown_partial_progress = false; } } } void sigint_handler(int signum) { if (prompt != COMMAND) { setprompt(COMMAND); } #ifndef NO_READLINE // reset position and print prompt rl_replace_line("", 0); //clean contents of actual command rl_crlf(); //move to nextline if (RL_ISSTATE(RL_STATE_ISEARCH) || RL_ISSTATE(RL_STATE_ISEARCH) || RL_ISSTATE(RL_STATE_ISEARCH)) { RL_UNSETSTATE(RL_STATE_ISEARCH); RL_UNSETSTATE(RL_STATE_NSEARCH); RL_UNSETSTATE( RL_STATE_SEARCH); history_set_pos(history_length); rl_restore_prompt(); // readline has stored it when searching } else { rl_reset_line_state(); } rl_redisplay(); #endif } void printprogress(long long completed, long long total, const char *title) { float oldpercent = percentDowloaded; if (total == 0) { percentDowloaded = 0; } else { percentDowloaded = float(completed * 1.0 / total * 100.0); } if (completed != PROGRESS_COMPLETE && (alreadyFinished || ( ( percentDowloaded == oldpercent ) && ( oldpercent != 0 ) ) )) { return; } if (percentDowloaded < 0) { percentDowloaded = 0; } if (total < 0) { return; // after a 100% this happens } if (completed != PROGRESS_COMPLETE && completed < 0.001 * total) { return; // after a 100% this happens } if (completed == PROGRESS_COMPLETE) { alreadyFinished = true; completed = total; percentDowloaded = 100; } printPercentageLineCerr(title, completed, total, percentDowloaded, !alreadyFinished); } #ifdef _WIN32 BOOL WINAPI CtrlHandler( DWORD fdwCtrlType ) { cerr << "Reached CtrlHandler: " << fdwCtrlType << endl; switch( fdwCtrlType ) { // Handle the CTRL-C signal. case CTRL_C_EVENT: sigint_handler((int)fdwCtrlType); return( TRUE ); default: return FALSE; } } #endif prompttype getprompt() { return prompt; } void setprompt(prompttype p, string arg) { prompt = p; #ifndef NO_READLINE if (p == COMMAND) { console_setecho(true); } else { pw_buf_pos = 0; if (arg.size()) { OUTSTREAM << arg << flush; } else { OUTSTREAM << prompts[p] << flush; } console_setecho(false); } #else console->setecho(p == COMMAND); if (p != COMMAND) { pw_buf_pos = 0; console->updateInputPrompt(arg.empty() ? prompts[p] : arg); } #endif } #ifndef NO_READLINE // readline callback - exit if EOF, add to history unless password static void store_line(char* l) { procesingline = true; if (!l) { #ifndef _WIN32 // to prevent exit with Supr key doExit = true; rl_set_prompt("(CTRL+D) Exiting ...\n"); #ifndef NDEBUG if (comms->mServerInitiatedFromShell) { OUTSTREAM << " Forwarding exit command to the server, since this cmd shell (most likely) initiated it" << endl; comms->executeCommand("exit", readresponse); } #endif #endif return; } if (*l && ( prompt == COMMAND )) { add_history(l); } g_line = l; } #endif #ifdef _WIN32 bool validoptionforreadline(const string& string) {// TODO: this has not been tested in 100% cases (perhaps it is too diligent or too strict) int c,i,ix,n,j; for (i=0, ix=int(string.length()); i < ix; i++) { c = (unsigned char) string[i]; //if (c>0xC0) return false; //if (c==0x09 || c==0x0a || c==0x0d || (0x20 <= c && c <= 0x7e) ) n = 0; // is_printable_ascii if (0x00 <= c && c <= 0x7f) n=0; // 0bbbbbbb else if ((c & 0xE0) == 0xC0) n=1; // 110bbbbb else if ( c==0xed && i<(ix-1) && ((unsigned char)string[i+1] & 0xa0)==0xa0) return false; //U+d800 to U+dfff else if ((c & 0xF0) == 0xE0) {return false; /*n=2;*/} // 1110bbbb else if ((c & 0xF8) == 0xF0) {return false; /*n=3;*/} // 11110bbb //else if (($c & 0xFC) == 0xF8) n=4; // 111110bb //byte 5, unnecessary in 4 byte UTF-8 //else if (($c & 0xFE) == 0xFC) n=5; // 1111110b //byte 6, unnecessary in 4 byte UTF-8 else return false; for (j=0; j lkrlhandler(handlerInstallerMutex); if (procesingline) { promptreinstalledwhenprocessingline = true; } rl_restore_prompt(); rl_callback_handler_install(theprompt, store_line); handlerOverridenByExternalThread = external; requirepromptinstall = false; } void redisplay_prompt() { int saved_point = rl_point; char *saved_line = rl_copy_text(0, rl_end); rl_clear_message(); // enter a new line if not processing sth (otherwise, the newline should already be there) if (!procesingline) { rl_crlf(); } if (prompt == COMMAND) { install_rl_handler(*dynamicprompt ? dynamicprompt : prompts[COMMAND]); } // restore line if (saved_line) { rl_replace_line(saved_line, 0); free(saved_line); saved_line = NULL; } rl_point = saved_point; rl_redisplay(); } #endif void changeprompt(const char *newprompt, bool redisplay) { std::lock_guard g(mutexPrompt); if (*dynamicprompt) { if (!strcmp(newprompt,dynamicprompt)) return; //same prompt. do nth } strncpy(dynamicprompt, newprompt, sizeof( dynamicprompt )); if (strlen(newprompt) >= PROMPT_MAX_SIZE) { strncpy(dynamicprompt, newprompt, PROMPT_MAX_SIZE/2-1); dynamicprompt[PROMPT_MAX_SIZE/2-1] = '.'; dynamicprompt[PROMPT_MAX_SIZE/2] = '.'; strncpy(dynamicprompt+PROMPT_MAX_SIZE/2+1, newprompt+(strlen(newprompt)-PROMPT_MAX_SIZE/2+2), PROMPT_MAX_SIZE/2-2); dynamicprompt[PROMPT_MAX_SIZE-1] = '\0'; } #ifdef NO_READLINE console->updateInputPrompt(newprompt); #else if (redisplay) { // save line redisplay_prompt(); } #endif static bool firstime = true; if (firstime) { firstime = false; #if _WIN32 if( !SetConsoleCtrlHandler( CtrlHandler, TRUE ) ) { cerr << "Control handler set failed" << endl; } #else // prevent CTRL+C exit signal(SIGINT, sigint_handler); #endif } } void escapeEspace(string &orig) { replaceAll(orig," ", "\\ "); } void unescapeEspace(string &orig) { replaceAll(orig,"\\ ", " "); } #ifndef NO_READLINE char* empty_completion(const char* text, int state) { // we offer 2 different options so that it doesn't complete (no space is inserted) if (state == 0) { return strdup(" "); } if (state == 1) { return strdup(text); } return NULL; } char* generic_completion(const char* text, int state, vector validOptions) { static size_t list_index, len; static bool foundone; string name; if (!validOptions.size()) // no matches { return empty_completion(text,state); //dont fall back to filenames } if (!state) { list_index = 0; foundone = false; len = strlen(text); } while (list_index < validOptions.size()) { name = validOptions.at(list_index); if (!rl_completion_quote_character) { escapeEspace(name); } list_index++; if (!( strcmp(text, "")) || (( name.size() >= len ) && ( strlen(text) >= len ) && ( name.find(text) == 0 ))) { if (name.size() && (( name.at(name.size() - 1) == '=' ) || ( name.at(name.size() - 1) == '\\' ) || ( name.at(name.size() - 1) == '/' ))) { rl_completion_suppress_append = 1; } foundone = true; return dupstr((char*)name.c_str()); } } if (!foundone) { return empty_completion(text,state); //dont fall back to filenames } return((char*)NULL ); } #endif char* local_completion(const char* text, int state) { return((char*)NULL ); //matches will be NULL: readline will use local completion } void pushvalidoption(vector *validOptions, const char *beginopt) { #ifdef _WIN32 if (validoptionforreadline(beginopt)) { validOptions->push_back(beginopt); } else { wstring input; stringtolocalw(beginopt,&input); wstring output = escapereadlinebreakers(input.c_str()); string soutput; localwtostring(&output,&soutput); validOptions->push_back(soutput.c_str()); } #else validOptions->push_back(beginopt); #endif } void changedir(const string& where) { #ifdef _WIN32 wstring wwhere; stringtolocalw(where.c_str(), &wwhere); wwhere.append(L"\\"); int r = SetCurrentDirectoryW((LPCWSTR)wwhere.data()); if (!r) { cerr << "Error at SetCurrentDirectoryW before local completion to " << where << ". errno: " << ERRNO << endl; } #else chdir(where.c_str()); #endif } #ifndef NO_READLINE char* remote_completion(const char* text, int state) { string saved_line = getCurrentLine(); static vector validOptions; if (state == 0) { validOptions.clear(); string completioncommand("completionshell "); completioncommand += saved_line; OUTSTRING s; OUTSTRINGSTREAM oss(s); comms->executeCommand(completioncommand, readresponse, oss); string outputcommand; outputcommand = oss.str(); if (outputcommand == "MEGACMD_USE_LOCAL_COMPLETION") { return local_completion(text,state); //fallback to local path completion } if (outputcommand.find("MEGACMD_USE_LOCAL_COMPLETION") == 0) { string where = outputcommand.substr(strlen("MEGACMD_USE_LOCAL_COMPLETION")); changedir(where); return local_completion(text,state); //fallback to local path completion } char *ptr = (char *)outputcommand.c_str(); char *beginopt = ptr; while (*ptr) { if (*ptr == 0x1F) { *ptr = '\0'; if (strcmp(beginopt," ")) //the server will give a " " for empty_completion (no matches) { pushvalidoption(&validOptions,beginopt); } beginopt=ptr+1; } ptr++; } if (*beginopt && strcmp(beginopt," ")) { pushvalidoption(&validOptions,beginopt); } } return generic_completion(text, state, validOptions); } static char** getCompletionMatches(const char * text, int start, int end) { rl_filename_quoting_desired = 1; char **matches; matches = (char**)NULL; matches = rl_completion_matches((char*)text, remote_completion); return( matches ); } void printHistory() { int length = history_length; int offset = 1; int rest = length; while (rest >= 10) { offset++; rest = rest / 10; } for (int i = 0; i < length; i++) { history_set_pos(i); OUTSTREAM << setw(offset) << i << " " << current_history()->line << endl; } } void wait_for_input(int readline_fd) { fd_set fds; FD_ZERO(&fds); FD_SET(readline_fd, &fds); int rc = select(FD_SETSIZE, &fds, NULL, NULL, NULL); if (rc < 0) { if (ERRNO != EINTR) //syscall { cerr << "Error at select at wait_for_input errno: " << ERRNO << endl; return; } } } #else vector remote_completion(string linetocomplete) { using namespace autocomplete; vector result; // normalize any partially or intermediately quoted strings, eg. `put c:\Program" Fi` or `/My" Documents/"` ACState acs = prepACState(linetocomplete, linetocomplete.size(), console->getAutocompleteStyle()); string refactoredline; for (auto& s : acs.words) { refactoredline += (refactoredline.empty() ? "" : " ") + s.getQuoted(); } OUTSTRING s; OUTSTRINGSTREAM oss(s); comms->executeCommand(string("completionshell ") + refactoredline, readresponse, oss); string outputcommand; auto ossstr=oss.str(); localwtostring(&ossstr, &outputcommand); ACState::quoted_word completionword = acs.words.size() ? acs.words[acs.words.size() - 1] : string(); if (outputcommand.find("MEGACMD_USE_LOCAL_COMPLETION") == 0) { string where; bool folders = false; if (outputcommand.find("MEGACMD_USE_LOCAL_COMPLETIONFOLDERS") == 0) { where = outputcommand.substr(strlen("MEGACMD_USE_LOCAL_COMPLETIONFOLDERS")); folders = true; } else { where = outputcommand.substr(strlen("MEGACMD_USE_LOCAL_COMPLETION")); } changedir(where); if (acs.words.size()) { string l = completionword.getQuoted(); CompletionState cs = autoComplete(l, l.size(), folders ? localFSFolder() : localFSPath(), console->getAutocompleteStyle()); result.swap(cs.completions); } return result; } else { char *ptr = (char *)outputcommand.c_str(); char *beginopt = ptr; while (*ptr) { if (*ptr == 0x1F) { *ptr = '\0'; if (strcmp(beginopt, " ")) //the server will give a " " for empty_completion (no matches) { result.push_back(autocomplete::ACState::Completion(beginopt, false)); } beginopt = ptr + 1; } ptr++; } if (*beginopt && strcmp(beginopt, " ")) { result.push_back(autocomplete::ACState::Completion(beginopt, false)); } if (result.size() == 1 && result[0].s == completionword.s) { result.clear(); // for parameters it returns the same string when there are no matches } return result; } } void exec_clear(autocomplete::ACState& s) { console->clearScreen(); } void exec_history(autocomplete::ACState& s) { console->outputHistory(); } void exec_dos_unix(autocomplete::ACState& s) { if (s.words.size() < 2) { OUTSTREAM << "autocomplete style: " << (console->getAutocompleteStyle() ? "unix" : "dos") << endl; } else { console->setAutocompleteStyle(s.words[1].s == "unix"); } } void exec_codepage(autocomplete::ACState& s) { if (s.words.size() == 1) { UINT cp1, cp2; console->getShellCodepages(cp1, cp2); cout << "Current codepage is " << cp1; if (cp2 != cp1) { cout << " with failover to codepage " << cp2 << " for any absent glyphs"; } cout << endl; for (int i = 32; i < 256; ++i) { string theCharUtf8 = WinConsole::toUtf8String(WinConsole::toUtf16String(string(1, (char)i), cp1)); cout << " dec/" << i << " hex/" << hex << i << dec << ": '" << theCharUtf8 << "'"; if (i % 4 == 3) { cout << endl; } } } else if (s.words.size() == 2 && atoi(s.words[1].s.c_str()) != 0) { if (!console->setShellConsole(atoi(s.words[1].s.c_str()), atoi(s.words[1].s.c_str()))) { cout << "Code page change failed - unicode selected" << endl; } } else if (s.words.size() == 3 && atoi(s.words[1].s.c_str()) != 0 && atoi(s.words[2].s.c_str()) != 0) { if (!console->setShellConsole(atoi(s.words[1].s.c_str()), atoi(s.words[2].s.c_str()))) { cout << "Code page change failed - unicode selected" << endl; } } else { cout << " codepage [N [N]]" << endl; } } autocomplete::ACN autocompleteSyntax; autocomplete::ACN buildAutocompleteSyntax() { using namespace autocomplete; std::unique_ptr p(new Either(" ")); p->Add(exec_clear, sequence(text("clear"))); p->Add(exec_codepage, sequence(text("codepage"), opt(sequence(wholenumber(65001), opt(wholenumber(65001)))))); p->Add(exec_dos_unix, sequence(text("autocomplete"), opt(either(text("unix"), text("dos"))))); p->Add(exec_history, sequence(text("history"))); return autocompleteSyntax = std::move(p); } void printHistory() { console->outputHistory(); } #endif bool isserverloggedin() { if (comms->executeCommand(("loggedin")) == MCMD_NOTLOGGEDIN ) { return false; } return true; } void process_line(const char * line) { string refactoredline; switch (prompt) { case AREYOUSURE: //this is currently never used if (!strcasecmp(line,"yes") || !strcasecmp(line,"y")) { comms->setResponseConfirmation(true); setprompt(COMMAND); } else if (!strcasecmp(line,"no") || !strcasecmp(line,"n")) { comms->setResponseConfirmation(false); setprompt(COMMAND); } else { //Do nth, ask again OUTSTREAM << "Please enter: [y]es/[n]o: " << flush; } break; case LOGINPASSWORD: { if (!strlen(line)) { break; } if (confirminglink) { string confirmcommand("confirm "); confirmcommand+=linktoconfirm; confirmcommand+=" " ; confirmcommand+=loginname; confirmcommand+=" \""; confirmcommand+=line; confirmcommand+="\"" ; OUTSTREAM << endl; comms->executeCommand(confirmcommand.c_str(), readresponse); } else if (confirmingcancellink) { string confirmcommand("confirmcancel "); confirmcommand+=linktoconfirm; confirmcommand+=" \""; confirmcommand+=line; confirmcommand+="\"" ; OUTSTREAM << endl; comms->executeCommand(confirmcommand.c_str(), readresponse); } else { string logincommand("login -v "); if (clientID.size()) { logincommand += "--clientID="; logincommand+=clientID; logincommand+=" "; } logincommand+=loginname; logincommand+=" \"" ; logincommand+=line; logincommand+="\"" ; OUTSTREAM << endl; comms->executeCommand(logincommand.c_str(), readresponse); } confirminglink = false; confirmingcancellink = false; setprompt(COMMAND); break; } case NEWPASSWORD: { if (!strlen(line)) { break; } newpasswd = line; OUTSTREAM << endl; setprompt(PASSWORDCONFIRM); break; } case PASSWORDCONFIRM: { if (!strlen(line)) { break; } if (line != newpasswd) { OUTSTREAM << endl << "New passwords differ, please try again" << endl; } else { OUTSTREAM << endl; if (signingup) { signupline += " \""; signupline += newpasswd; signupline += "\""; comms->executeCommand(signupline.c_str(), readresponse); signingup = false; } else { string changepasscommand(passwdline); passwdline = " "; changepasscommand+=" " ; if (oldpasswd.size()) { changepasscommand+="\"" ; changepasscommand+=oldpasswd; changepasscommand+="\"" ; } changepasscommand+=" \"" ; changepasscommand+=newpasswd; changepasscommand+="\"" ; comms->executeCommand(changepasscommand.c_str(), readresponse); } } setprompt(COMMAND); break; } case COMMAND: { #ifdef NO_READLINE // if local command and syntax is satisfied, execute it string consoleOutput; if (autocomplete::autoExec(line, string::npos, autocompleteSyntax, false, consoleOutput, false)) { COUT << consoleOutput << flush; return; } // normalize any partially or intermediately quoted strings, eg. `put c:\Program" Fi` or get `/My" Documents/"` autocomplete::ACState acs = autocomplete::prepACState(line, strlen(line), console->getAutocompleteStyle()); for (auto& s : acs.words) { refactoredline += (refactoredline.empty() ? "" : " ") + s.getQuoted(); } line = refactoredline.c_str(); #endif vector words = getlistOfWords(line); string clientWidth = "--client-width="; clientWidth+= SSTR(getNumberOfCols(80)); words.insert(words.begin()+1, clientWidth); string scommandtoexec(words[0]); scommandtoexec+=" "; scommandtoexec+=clientWidth; scommandtoexec+=" "; if (strlen(line)>(words[0].size()+1)) { scommandtoexec+=line+words[0].size()+1; } const char *commandtoexec = scommandtoexec.c_str(); bool helprequested = false; for (unsigned int i = 1; i< words.size(); i++) { if (words[i]== "--help") helprequested = true; } if (words.size()) { if ( words[0] == "exit" || words[0] == "quit") { if (find(words.begin(), words.end(), "--only-shell") == words.end()) { if (comms->executeCommand(commandtoexec, readresponse) == MCMD_CONFIRM_NO) { return; } } if (find(words.begin(), words.end(), "--help") == words.end() && find(words.begin(), words.end(), "--only-server") == words.end() ) { doExit = true; } } #if defined(_WIN32) || defined(__APPLE__) else if (words[0] == "update") { comms->markServerIsUpdating(); int ret = comms->executeCommand(commandtoexec, readresponse); if (ret == MCMD_REQRESTART) { OUTSTREAM << "MEGAcmd has been updated ... this shell will be restarted before proceding...." << endl; doExit = true; doReboot = true; } else if (ret != MCMD_INVALIDSTATE && words.size() == 1) { comms->unmarkServerIsUpdating(); } } #endif else if (words[0] == "history") { if (helprequested) { OUTSTREAM << " Prints commands history" << endl; } else { printHistory(); } } #if defined(_WIN32) && !defined(NO_READLINE) else if (!helprequested && words[0] == "unicode" && words.size() == 1) { rl_getc_function=(rl_getc_function==&getcharacterreadlineUTF16support)?rl_getc:&getcharacterreadlineUTF16support; OUTSTREAM << "Unicode shell input " << ((rl_getc_function==&getcharacterreadlineUTF16support)?"ENABLED":"DISABLED") << endl; return; } #endif else if (!helprequested && words[0] == "passwd") { if (isserverloggedin()) { passwdline = commandtoexec; discardOptionsAndFlags(&words); if (words.size() == 1) { setprompt(NEWPASSWORD); } else { comms->executeCommand(commandtoexec, readresponse); } } else { cerr << "Not logged in." << endl; } return; } else if (!helprequested && words[0] == "login") { if (!isserverloggedin()) { discardOptionsAndFlags(&words); if ( (words.size() == 2 || ( words.size() == 3 && !words[2].size() ) ) && (words[1].find("@") != string::npos)) { loginname = words[1]; setprompt(LOGINPASSWORD); } else { string s = commandtoexec; if (clientID.size()) { s = "login --clientID="; s+=clientID; s.append(string(commandtoexec).substr(5)); } comms->executeCommand(s, readresponse); } } else { cerr << "Already logged in. Please log out first." << endl; } return; } else if (!helprequested && words[0] == "signup") { if (!isserverloggedin()) { signupline = commandtoexec; discardOptionsAndFlags(&words); if (words.size() == 2) { loginname = words[1]; signingup = true; setprompt(NEWPASSWORD); } else { comms->executeCommand(commandtoexec, readresponse); } } else { cerr << "Please logout first." << endl; } return; } else if (!helprequested && words[0] == "confirm") { discardOptionsAndFlags(&words); if (words.size() == 3) { linktoconfirm = words[1]; loginname = words[2]; confirminglink = true; setprompt(LOGINPASSWORD); } else { comms->executeCommand(commandtoexec, readresponse); } } else if (!helprequested && words[0] == "confirmcancel") { discardOptionsAndFlags(&words); if (words.size() == 2) { linktoconfirm = words[1]; confirmingcancellink = true; setprompt(LOGINPASSWORD); } else { comms->executeCommand(commandtoexec, readresponse); } return; } else if (!helprequested && words[0] == "clear") { #ifdef _WIN32 HANDLE hStdOut; CONSOLE_SCREEN_BUFFER_INFO csbi; DWORD count; hStdOut = GetStdHandle( STD_OUTPUT_HANDLE ); if (hStdOut == INVALID_HANDLE_VALUE) return; /* Get the number of cells in the current buffer */ if (!GetConsoleScreenBufferInfo( hStdOut, &csbi )) return; /* Fill the entire buffer with spaces */ if (!FillConsoleOutputCharacter( hStdOut, (TCHAR) ' ', csbi.dwSize.X *csbi.dwSize.Y, { 0, 0 }, &count )) { return; } /* Fill the entire buffer with the current colors and attributes */ if (!FillConsoleOutputAttribute(hStdOut, csbi.wAttributes, csbi.dwSize.X *csbi.dwSize.Y, { 0, 0 }, &count)) { return; } /* Move the cursor home */ SetConsoleCursorPosition( hStdOut, { 0, 0 } ); #elif __linux__ printf("\033[H\033[J"); #else rl_clear_screen(0,0); #endif return; } else if ( (words[0] == "transfers")) { string toexec; if (!strstr (commandtoexec,"path-display-size")) { unsigned int width = getNumberOfCols(75); int pathSize = int((width-46)/2); toexec+=words[0]; toexec+=" --path-display-size="; toexec+=SSTR(pathSize); toexec+=" "; if (strlen(commandtoexec)>(words[0].size()+1)) { toexec+=commandtoexec+words[0].size()+1; } } else { toexec+=commandtoexec; } comms->executeCommand(toexec.c_str(), readresponse); } else if ( (words[0] == "du")) { string toexec; if (!strstr (commandtoexec,"path-display-size")) { unsigned int width = getNumberOfCols(75); int pathSize = int(width-13); if (strstr(commandtoexec, "--versions")) { pathSize -= 11; } toexec+=words[0]; toexec+=" --path-display-size="; toexec+=SSTR(pathSize); toexec+=" "; if (strlen(commandtoexec)>(words[0].size()+1)) { toexec+=commandtoexec+words[0].size()+1; } } else { toexec+=commandtoexec; } comms->executeCommand(toexec.c_str(), readresponse); } else if (words[0] == "sync") { string toexec; if (!strstr (commandtoexec,"path-display-size")) { unsigned int width = getNumberOfCols(75); int pathSize = int((width-46)/2); toexec+="sync --path-display-size="; toexec+=SSTR(pathSize); toexec+=" "; if (strlen(commandtoexec)>strlen("sync ")) { toexec+=commandtoexec+strlen("sync "); } } else { toexec+=commandtoexec; } comms->executeCommand(toexec.c_str(), readresponse); } else if (words[0] == "mediainfo") { string toexec; if (!strstr (commandtoexec,"path-display-size")) { unsigned int width = getNumberOfCols(75); int pathSize = int(width - 28); toexec+=words[0]; toexec+=" --path-display-size="; toexec+=SSTR(pathSize); toexec+=" "; if (strlen(commandtoexec)>(words[0].size()+1)) { toexec+=commandtoexec+words[0].size()+1; } } else { toexec+=commandtoexec; } comms->executeCommand(toexec.c_str(), readresponse); } else if (words[0] == "backup") { string toexec; if (!strstr (commandtoexec,"path-display-size")) { unsigned int width = getNumberOfCols(75); int pathSize = int((width-21)/2); toexec+="backup --path-display-size="; toexec+=SSTR(pathSize); toexec+=" "; if (strlen(commandtoexec)>strlen("backup ")) { toexec+=commandtoexec+strlen("backup "); } } else { toexec+=commandtoexec; } comms->executeCommand(toexec.c_str(), readresponse); } else { if ( words[0] == "get" || words[0] == "put" || words[0] == "reload") { string s = commandtoexec; if (clientID.size()) { string sline = commandtoexec; size_t pspace = sline.find_first_of(" "); s=""; s=sline.substr(0,pspace); s += " --clientID="; s+=clientID; if (pspace!=string::npos) { s+=sline.substr(pspace); } words.push_back(s); } comms->executeCommand(s, readresponse); #ifdef _WIN32 Sleep(200); // give a brief while to print progress ended #endif } else { // execute user command comms->executeCommand(commandtoexec, readresponse); } } } else { cerr << "failed to interprete input commandtoexec: " << commandtoexec << endl; } break; } } } // main loop #ifndef NO_READLINE void readloop() { time_t lasttimeretrycons = 0; char *saved_line = NULL; int saved_point = 0; rl_save_prompt(); int readline_fd = -1; readline_fd = fileno(rl_instream); procesingline = true; comms->registerForStateChanges(true, statechangehandle); if (!comms->waitForServerReadyOrRegistrationFailed(std::chrono::seconds(2*RESUME_SESSION_TIMEOUT))) { std::cerr << "Server seems irresponsive" << endl; } procesingline = false; promptreinstalledwhenprocessingline = false; for (;; ) { if (prompt == COMMAND) { mutexPrompt.lock(); if (requirepromptinstall) { install_rl_handler(*dynamicprompt ? dynamicprompt : prompts[COMMAND], false); // display prompt if (saved_line) { rl_replace_line(saved_line, 0); free(saved_line); saved_line = NULL; } rl_point = saved_point; rl_redisplay(); } mutexPrompt.unlock(); } // command editing loop - exits when a line is submitted for (;; ) { if (prompt == COMMAND || prompt == AREYOUSURE) { procesingline = false; promptreinstalledwhenprocessingline = false; wait_for_input(readline_fd); bool retryComms = false; time_t tnow = time(NULL); { std::lock_guard g(mutexPrompt); rl_callback_read_char(); //this calls store_line if last char was enter if ( (tnow - lasttimeretrycons) > 5 && !doExit && !comms->isServerUpdating()) { char * sl = rl_copy_text(0, rl_end); if (string("quit").find(sl) != 0 && string("exit").find(sl) != 0) { retryComms = true; } free(sl); } rl_resize_terminal(); // to always adjust to new screen sizes if (doExit) { if (saved_line != NULL) free(saved_line); saved_line = NULL; return; } } if (retryComms) { comms->executeCommand("retrycons"); lasttimeretrycons = tnow; } } else { console_readpwchar(pw_buf, sizeof pw_buf, &pw_buf_pos, &g_line); } if (g_line) { break; } } cleanLastMessage();// clean last message that avoids broadcasts repetitions mutexPrompt.lock(); // save line saved_point = rl_point; if (saved_line != NULL) free(saved_line); saved_line = rl_copy_text(0, rl_end); // remove prompt rl_save_prompt(); rl_replace_line("", 0); rl_redisplay(); mutexPrompt.unlock(); if (g_line) { if (strlen(g_line)) { alreadyFinished = false; percentDowloaded = 0.0; handlerOverridenByExternalThread = false; process_line(g_line); { //after processing the line, we want to reinstall the handler (except if during the process, or due to it, // the handler is reinstalled by e.g: a change in prompt) std::lock_guard lkrlhandler(handlerInstallerMutex); if (!handlerOverridenByExternalThread) { requirepromptinstall = true; } } if (comms->registerRequired()) { comms->registerForStateChanges(true, statechangehandle); } // sleep, so that in case there was a changeprompt waiting, gets executed before relooping // this is not 100% guaranteed to happen sleepSeconds(0); } else { // Empty line (user pressed Enter with no input): show new prompt like bash requirepromptinstall = true; } free(g_line); g_line = NULL; } if (doExit) { if (saved_line != NULL) free(saved_line); saved_line = NULL; return; } } } #else // NO_READLINE void readloop() { time_t lasttimeretrycons = 0; comms->registerForStateChanges(true, statechangehandle); //give it a while to communicate the state sleepMilliSeconds(700); for (;; ) { if (prompt == COMMAND) { console->updateInputPrompt(*dynamicprompt ? dynamicprompt : prompts[COMMAND]); } // command editing loop - exits when a line is submitted for (;; ) { g_line = console->checkForCompletedInputLine(); if (g_line) { break; } else { time_t tnow = time(NULL); if ((tnow - lasttimeretrycons) > 5 && !doExit && !comms->isServerUpdating()) { if (wstring(L"quit").find(console->getInputLineToCursor()) != 0 && wstring(L"exit").find(console->getInputLineToCursor()) != 0 ) { comms->executeCommand("retrycons"); lasttimeretrycons = tnow; } } if (doExit) { return; } } } cleanLastMessage();// clean last message that avoids broadcasts repetitions if (g_line) { if (strlen(g_line)) { alreadyFinished = false; percentDowloaded = 0.0; // mutexPrompt.lock(); process_line(g_line); requirepromptinstall = true; // mutexPrompt.unlock(); if (comms->registerRequired()) { comms->registerForStateChanges(true, statechangehandle); } // sleep, so that in case there was a changeprompt waiting, gets executed before relooping // this is not 100% guaranteed to happen sleepSeconds(0); } free(g_line); g_line = NULL; } if (doExit) { return; } } } #endif class NullBuffer : public std::streambuf { public: int overflow(int c) { return c; } }; void printWelcomeMsg(unsigned int width) { if (!width) { width = getNumberOfCols(75); } std::ostringstream oss; oss << endl; oss << "."; for (unsigned int i = 0; i < width; i++) oss << "=" ; oss << "."; oss << endl; printCenteredLine(oss, " __ __ _____ ____ _ _ ",width); printCenteredLine(oss, "| \\/ | ___|/ ___| / \\ ___ _ __ ___ __| |",width); printCenteredLine(oss, "| |\\/| | \\ / | _ / _ \\ / __| '_ ` _ \\ / _` |",width); printCenteredLine(oss, "| | | | /__\\ |_| |/ ___ \\ (__| | | | | | (_| |",width); printCenteredLine(oss, "|_| |_|____|\\____/_/ \\_\\___|_| |_| |_|\\__,_|",width); oss << "|"; for (unsigned int i = 0; i < width; i++) oss << " " ; oss << "|"; oss << endl; printCenteredLine(oss, "Welcome to MEGAcmd! A Command Line Interactive and Scriptable",width); printCenteredLine(oss, "Application to interact with your MEGA account.",width); printCenteredLine(oss, "Please write to support@mega.nz if you find any issue or",width); printCenteredLine(oss, "have any suggestion concerning its functionalities.",width); printCenteredLine(oss, "Enter \"help --non-interactive\" to learn how to use MEGAcmd with scripts.",width); printCenteredLine(oss, "Enter \"help\" for basic info and a list of available commands.",width); #if defined(_WIN32) && defined(NO_READLINE) printCenteredLine(oss, "Unicode support in the console is improved, see \"help --unicode\"", width); #elif defined(_WIN32) printCenteredLine(oss, "Enter \"help --unicode\" for info regarding non-ASCII support.",width); #endif oss << "`"; for (unsigned int i = 0; i < width; i++) { oss << "=" ; } #ifndef _WIN32 oss << "\u00b4\n"; COUT << oss.str() << std::flush; #else WindowsUtf8StdoutGuard utf8Guard; // So far, all is ASCII. COUT << oss.str(); // Now let's tray the non ascii forward acute. Note: codepage should have been set to UTF-8 // set via console->setShellConsole(CP_UTF8, GetConsoleOutputCP()); assert(GetConsoleOutputCP() == CP_UTF8); if (!(COUT << L"\u00b4")) // still, Windows 7 or depending on the fonts, the console may struggle to render this { COUT << "/"; //fallback character } COUT << endl; #endif } int quote_detector(char *line, int index) { return ( index > 0 && line[index - 1] == '\\' && !quote_detector(line, index - 1) ); } bool runningInBackground() { #ifndef _WIN32 pid_t fg = tcgetpgrp(STDIN_FILENO); if(fg == -1) { // Piped: return false; } else if (fg == getpgrp()) { // foreground return false; } else { // background return true; } #endif return false; } #ifndef NO_READLINE std::string readresponse(const char* question) { string response; auto responseRaw = readline(question); if (responseRaw) { response = responseRaw; } rl_set_prompt(""); rl_replace_line("", 0); rl_callback_handler_remove(); //To fix broken readline (e.g: upper key wouldnt work) return response; } #else std::string readresponse(const char* question) { std::string questionStr(question); size_t pos = questionStr.rfind('\n'); if (pos != std::string::npos) { std::string questionPrev = questionStr.substr(0, pos); std::string prompt = questionStr.substr(pos + 1); COUT << questionPrev << std::endl; console->updateInputPrompt(prompt); } else { console->updateInputPrompt(questionStr); } for (;;) { if (char* line = console->checkForCompletedInputLine()) { console->updateInputPrompt(""); string response(line); free(line); return response; } else { sleepMilliSeconds(200); } } } #endif } //end namespace using namespace megacmd; int main(int argc, char* argv[]) { // intialize the comms object #if defined(_WIN32) comms = new MegaCmdShellCommunicationsNamedPipes(); #else comms = new MegaCmdShellCommunicationsPosix(); #endif #ifndef NO_READLINE rl_attempted_completion_function = getCompletionMatches; rl_completer_quote_characters = "\"'"; rl_filename_quote_characters = " "; rl_completer_word_break_characters = (char *)" "; rl_char_is_quoted_p = "e_detector; if (!runningInBackground()) { rl_initialize(); // initializes readline, // so that we can use rl_message or rl_resize_terminal safely before ever // prompting anything. } #endif #if defined(_WIN32) && defined(NO_READLINE) console = new CONSOLE_CLASS; console->setAutocompleteSyntax(buildAutocompleteSyntax()); console->setAutocompleteFunction(remote_completion); console->setShellConsole(CP_UTF8, GetConsoleOutputCP()); console->blockingConsolePeek = true; #endif #ifdef _WIN32 // in windows, rl_resize_terminal fails to resize before first prompt appears, we take the width from elsewhere CONSOLE_SCREEN_BUFFER_INFO csbi; int columns; GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi); columns = csbi.srWindow.Right - csbi.srWindow.Left - 2; printWelcomeMsg(columns); #else sleepMilliSeconds(200); // this gives a little while so that the console is ready and rl_resize_terminal works fine printWelcomeMsg(); #endif readloop(); #ifndef NO_READLINE clear_history(); if (!doReboot) { rl_callback_handler_remove(); //To avoid having the terminal messed up (requiring a "reset") } #endif comms->shutdown(); delete comms; if (doReboot) { #ifdef _WIN32 sleepSeconds(5); // Give a while for server to restart LPWSTR szPathExecQuoted = GetCommandLineW(); wstring wspathexec = wstring(szPathExecQuoted); if (wspathexec.at(0) == '"') { wspathexec = wspathexec.substr(1); } while (wspathexec.size() && ( wspathexec.at(wspathexec.size()-1) == '"' || wspathexec.at(wspathexec.size()-1) == ' ' )) { wspathexec = wspathexec.substr(0,wspathexec.size()-1); } LPWSTR szPathExec = (LPWSTR) wspathexec.c_str(); STARTUPINFO si; PROCESS_INFORMATION pi; ZeroMemory( &si, sizeof(si) ); ZeroMemory( &pi, sizeof(pi) ); si.cb = sizeof(si); if (!CreateProcess( szPathExec,szPathExec,NULL,NULL,TRUE, CREATE_NEW_CONSOLE, NULL,NULL, &si,&pi) ) { COUT << "Unable to execute: " << szPathExec << " errno = : " << ERRNO << endl; sleepSeconds(5); } #elif defined(__linux__) system("reset -I"); string executable = argv[0]; if (executable.find("/") != 0) { executable.insert(0, getCurrentExecPath()+"/"); } execv(executable.c_str(), argv); #else system("reset -I"); execv(argv[0], argv); #endif } } MEGAcmd-2.5.2_Linux/src/megacmdshell/megacmdshell.h000066400000000000000000000026511516543156300221230ustar00rootroot00000000000000/** * @file src/megacmdshell.h * @brief MEGAcmd: Interactive CLI and service application * This is the shell application * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is distributed under the terms of the GNU General Public * License, see http://www.gnu.org/copyleft/gpl.txt * for details. * * This file 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. */ #ifndef MEGACMDSHELL_H #define MEGACMDSHELL_H #include #include #define OUTSTREAM COUT namespace megacmd { enum prompttype { COMMAND, LOGINPASSWORD, NEWPASSWORD, PASSWORDCONFIRM, AREYOUSURE }; static const char* const prompts[] = { "MEGA CMD> ", "Password:", "New Password:", "Retype New Password:", "Are you sure to delete? " }; void sleepSeconds(int seconds); void sleepMilliSeconds(long milliseconds); void restoreprompt(); void printprogress(long long completed, long long total, const char *title = "TRANSFERRING"); void changeprompt(const char *newprompt, bool redisplay = false); void redisplay_prompt(); const char * getUsageStr(const char *command); void unescapeifRequired(std::string &what); void setprompt(prompttype p, std::string arg = ""); prompttype getprompt(); void printHistory(); std::string readresponse(const char *question); }//end namespace #endif // MEGACMDSHELL_H MEGAcmd-2.5.2_Linux/src/megacmdshell/megacmdshellcommunications.cpp000066400000000000000000000543431516543156300254340ustar00rootroot00000000000000/** * @file src/megacmdshellcommunications.cpp * @brief MEGAcmd: Communications module to connect to server * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of the MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. * * This file is also distributed under the terms of the GNU General * Public License, see http://www.gnu.org/copyleft/gpl.txt for details. */ #include "megacmdshellcommunications.h" #include "../megacmdcommonutils.h" #include #include #include #include #ifdef _WIN32 #include //SHGetFolderPath #include //PathAppend #else #include #include #include //getpwuid_r #include #include #include #include #include #endif #ifdef __FreeBSD__ #include #endif #ifndef INVALID_SOCKET #define INVALID_SOCKET -1 #endif #ifndef ENOTCONN #define ENOTCONN 107 #endif using namespace std; namespace megacmd { #ifndef _WIN32 string createAndRetrieveConfigFolder() { auto dirs = PlatformDirectories::getPlatformSpecificDirectories(); auto dir = dirs->configDirPath().string(); struct stat st = {}; if (stat(dir.c_str(), &st) == -1) { mkdir(dir.c_str(), 0700); } return dir; } #endif #ifndef _WIN32 #include bool is_pid_running(pid_t pid) { while(waitpid(-1, 0, WNOHANG) > 0) { // Wait for defunct.... } if (0 == kill(pid, 0)) return 1; // Process exists return 0; } #endif #ifndef _WIN32 bool MegaCmdShellCommunicationsPosix::isSocketValid(SOCKET socket) { return socket >= 0; } SOCKET MegaCmdShellCommunicationsPosix::createSocket(int number, bool initializeserver) { SOCKET thesock = socket(AF_UNIX, SOCK_STREAM, 0); if (!isSocketValid(thesock)) { cerr << "ERROR opening socket: " << ERRNO << endl; return INVALID_SOCKET; } bool ok = false; ScopeGuard g([this, &thesock, &ok]() { if (!ok) //something failed { close(thesock); } }); if (fcntl(thesock, F_SETFD, FD_CLOEXEC) == -1) { cerr << "ERROR setting CLOEXEC to socket: " << errno << endl; } std::string socketPath = getOrCreateSocketPath(false); if (socketPath.empty()) { std::cerr << "Error creating runtime directory for socket file: " << strerror(errno) << std::endl; return INVALID_SOCKET; } struct sockaddr_un addr; memset(&addr, 0, sizeof( addr )); addr.sun_family = AF_UNIX; strncpy(addr.sun_path, socketPath.c_str(), socketPath.size()); if (::connect(thesock, (struct sockaddr*)&addr, sizeof( addr )) == SOCKET_ERROR) { if (!number && initializeserver) { //launch server int forkret = fork(); // if (forkret) //-> child is megacmdshell (debug megacmd server) if (!forkret) //-> child is server. (debug megacmdshell) { signal(SIGINT, SIG_IGN); //ignore Ctrl+C in the server setsid(); //create new session so as not to receive parent's Ctrl+C // Give an indication of where the logs will be find: string pathtolog = createAndRetrieveConfigFolder()+"/megacmdserver.log"; CERR << "[Initiating MEGAcmd server in background. Log: " << pathtolog << "]" << endl; freopen(std::string(pathtolog).append(".out").c_str(),"w",stdout); freopen(std::string(pathtolog).append(".err").c_str(),"w",stderr); #ifndef NDEBUG const char executable[] = "./mega-cmd-server"; #else #ifdef __MACH__ const char executable[] = "/Applications/MEGAcmd.app/Contents/MacOS/mega-cmd"; const char executable2[] = "./mega-cmd"; #else const char executable[] = "mega-cmd-server"; char executable2[PATH_MAX]; sprintf(executable2, "%s/mega-cmd-server", getCurrentExecPath().c_str()); #endif #endif std::vector argsVector{ executable, "--do-not-log-to-stdout", nullptr }; auto args = const_cast(argsVector.data()); int ret = execvp(executable, args); if (ret && errno == 2) { cerr << "Couln't initiate MEGAcmd server: executable not found: " << executable << endl; #ifdef NDEBUG cerr << "Trying to use alternative executable: " << executable2 << endl; argsVector[0] = executable2; ret = execvp(executable2, args); if (ret && errno == 2) { cerr << "Couln't initiate MEGAcmd server: executable not found: " << executable2 << endl; } #endif } if (ret && errno != 2) { cerr << "MEGAcmd server exit with code " << ret << " . errno = " << errno << endl; } exit(0); } //try again: int attempts = 12; #ifdef __MACH__ int waitimet = 15000; // Give a longer while for the user to insert password to unblock fsevents. TODO: this should only be required the first time using megacmd #else int waitimet = 1500; static int relaunchnumber = 1; waitimet=waitimet*(relaunchnumber++); #endif usleep(waitimet*100); while ( ::connect(thesock, (struct sockaddr*)&addr, sizeof( addr )) == SOCKET_ERROR && attempts--) { usleep(waitimet); waitimet=waitimet*2; } if (attempts < 0) //too many attempts { cerr << "Unable to connect to " << (number?("response socket N "+std::to_string(number)):"service") << ": error=" << ERRNO << endl; #ifdef __linux__ cerr << "Please ensure mega-cmd-server is running" << endl; #else cerr << "Please ensure MEGAcmdServer is running" << endl; #endif return INVALID_SOCKET; } else { if (forkret && is_pid_running(forkret)) // server pid is alive (most likely because I initiated the server) { mServerInitiatedFromShell = true; } setForRegisterAgain(true); } } else { #ifdef ECONNREFUSED if (!initializeserver && ERRNO == ECONNREFUSED) { cerr << "MEGAcmd Server is not responding" << endl; } else #endif { cerr << "Unable to connect to socket " << number << " : " << ERRNO << endl; } return INVALID_SOCKET; } } ok = true; return thesock; } #endif MegaCmdShellCommunications::MegaCmdShellCommunications() { #ifdef _WIN32 setlocale(LC_ALL, "en-US"); #endif #if _WIN32 WORD wVersionRequested; WSADATA wsaData; int err; /* Use the MAKEWORD(lowbyte, highbyte) macro declared in Windef.h */ wVersionRequested = MAKEWORD(2, 2); err = WSAStartup(wVersionRequested, &wsaData); if (err != 0) { cerr << "ERROR initializing WSA" << endl; } #endif } #ifdef _WIN32 std::string to_utf8(uint32_t cp) //TODO: move this to a common place { // // c++11 // std::wstring_convert, char32_t> conv; // return conv.to_bytes( (char32_t)cp ); std::string result; int count; if (cp < 0x0080) count = 1; else if (cp < 0x0800) count = 2; else if (cp < 0x10000) count = 3; else if (cp <= 0x10FFFF) count = 4; else return result; // or throw an exception result.resize(count); for (int i = count-1; i > 0; --i) { result[i] = (char) (0x80 | (cp & 0x3F)); cp >>= 6; } for (int i = 0; i < count; ++i) cp |= (1 << (7-i)); result[0] = (char) cp; return result; } string unescapeutf16escapedseqs(const char *what) { // string toret; // size_t len = strlen(what); // for (int i=0;i that's only one gliph // i+=6; // } // else // { // toret+=what[i]; // i++; // } // } // return toret; std::string str = what; std::string::size_type startIdx = 0; do { startIdx = str.find("\\u", startIdx); if (startIdx == std::string::npos) break; std::string::size_type endIdx = str.find_first_not_of("0123456789abcdefABCDEF", startIdx+2); if (endIdx == std::string::npos) break; std::string tmpStr = str.substr(startIdx+2, endIdx-(startIdx+2)); std::istringstream iss(tmpStr); uint32_t cp; if (iss >> std::hex >> cp) { std::string utf8 = to_utf8(cp); str.replace(startIdx, 2+tmpStr.length(), utf8); startIdx += utf8.length(); } else startIdx += 2; } while (true); return str; } #endif int MegaCmdShellCommunications::executeCommandW(wstring wcommand, std::string (*readresponse)(const char *), OUTSTREAMTYPE &output, OUTSTREAMTYPE &errorOutput, bool interactiveshell) { return executeCommand("", readresponse, output, errorOutput, interactiveshell, wcommand); } #ifndef _WIN32 int MegaCmdShellCommunicationsPosix::executeCommand(string command, std::string (*readresponse)(const char *), OUTSTREAMTYPE &output, OUTSTREAMTYPE &errorOutput, bool interactiveshell, wstring /*wcommand*/) { SOCKET thesock = createSocket(0, command.compare(0,4,"exit") && command.compare(0,4,"quit") && command.compare(0,10,"completion")); if (!isSocketValid(thesock)) { return -1; } ScopeGuard g([this, &thesock]() { close(thesock); }); if (interactiveshell) { command="X"+command; } auto n = send(thesock,command.data(),command.size(), MSG_NOSIGNAL); if (n == SOCKET_ERROR) { if ( (!command.compare(0,5,"Xexit") || !command.compare(0,5,"Xquit") ) && (ERRNO == ENOTCONN) ) { cerr << "Could not send exit command to MEGAcmd server (probably already down)" << endl; } else { cerr << "ERROR writing command to socket: " << ERRNO << endl; } return -1; } int outcode = -1; n = recv(thesock, (char *)&outcode, sizeof(outcode), MSG_NOSIGNAL); if (n == SOCKET_ERROR) { cerr << "ERROR reading output code: " << ERRNO << endl; return -1; } while (outcode == MCMD_REQCONFIRM || outcode == MCMD_REQSTRING || outcode == MCMD_PARTIALOUT || outcode == MCMD_PARTIALERR) { if (outcode == MCMD_PARTIALOUT || outcode == MCMD_PARTIALERR) { bool useErrorStream = outcode == MCMD_PARTIALERR; auto &partialOutputStream = useErrorStream ? errorOutput : output; size_t partialoutsize; n = recv(thesock, (char *)&partialoutsize, sizeof(partialoutsize), MSG_NOSIGNAL); if (n && partialoutsize > 0) { StdoutMutexGuard stdOutLockGuard; do{ char *buffer = new char[partialoutsize+1]; n = recv(thesock, (char *)buffer, partialoutsize, MSG_NOSIGNAL); if (n) { partialOutputStream << string(buffer,partialoutsize) << flush; partialoutsize-=n; } delete[] buffer; } while(n != 0 && partialoutsize && n !=SOCKET_ERROR); } else { std::cerr << "Error reading size of partial output: " << ERRNO << std::endl; return -1; } } else { //REQCONFIRM|REQSTRING size_t BUFFERSIZE = 1024; string confirmQuestion; char buffer[1025]; do{ n = recv(thesock, buffer, BUFFERSIZE, MSG_NOSIGNAL); if (n) { buffer[n]='\0'; confirmQuestion.append(buffer); } } while(n == BUFFERSIZE && n != SOCKET_ERROR); if (outcode == MCMD_REQCONFIRM) { int response = MCMDCONFIRM_NO; if (readresponse != NULL) { response = readconfirmationloop(confirmQuestion.c_str(), readresponse); } n = send(thesock, (const char *) &response, sizeof(response), MSG_NOSIGNAL); } else // MCMD_REQSTRING { string response = (readresponse != NULL) ? readresponse(confirmQuestion.c_str()) : "FAILED"; n = send(thesock, response.data(), response.size(), MSG_NOSIGNAL); } if (n == SOCKET_ERROR) { cerr << "ERROR writing confirm response to socket: " << ERRNO << endl; return -1; } } n = recv(thesock, (char *)&outcode, sizeof(outcode), MSG_NOSIGNAL); if (n == SOCKET_ERROR) { cerr << "ERROR reading output code: " << ERRNO << endl; return -1; } } int BUFFERSIZE = 1024; char buffer[1025]; do{ n = recv(thesock, buffer, BUFFERSIZE, MSG_NOSIGNAL); if (n) { if (n != 1 || buffer[0] != 0) //To avoid outputing 0 char in binary outputs { StdoutMutexGuard stdOutLockGuard; output << string(buffer,n) << flush; } } } while(n != 0 && n !=SOCKET_ERROR); if (n == SOCKET_ERROR) { cerr << "ERROR reading output: " << ERRNO << endl; return -1; } return outcode; } int MegaCmdShellCommunicationsPosix::listenToStateChanges(int receiveSocket, StateChangedCb_t statechangehandle) { assert(isSocketValid(receiveSocket)); ScopeGuard g([this, receiveSocket]() { mStateListenerSocket = -1; // we don't want shutdowns after close! close(receiveSocket); }); while (!mStopListener) { string newstate; int BUFFERSIZE = 1024; char buffer[1025]; int n = SOCKET_ERROR; do{ n = recv(receiveSocket, buffer, BUFFERSIZE, MSG_NOSIGNAL); if (n) { buffer[n]='\0'; newstate += buffer; } } while(n == BUFFERSIZE && n !=SOCKET_ERROR); if (n == SOCKET_ERROR) { cerr << "ERROR reading state from MEGAcmd server: " << ERRNO << endl; return -1; } if (!n) // server closed the connection { return -1; } if (statechangehandle) { statechangehandle(newstate, *this); } } return 0; } #endif int MegaCmdShellCommunications::readconfirmationloop(const char *question, string (*readresponse)(const char *)) { bool firstime = true; for (;; ) { string response; if (firstime) { response = readresponse(question); } else { response = readresponse("Please enter [y]es/[n]o/[a]ll/none:"); } firstime = false; if (response == "yes" || response == "y" || response == "YES" || response == "Y" || response == "Yes") { return MCMDCONFIRM_YES; } if (response == "no" || response == "n" || response == "NO" || response == "N" || response == "No") { return MCMDCONFIRM_NO; } if (response == "All" || response == "ALL" || response == "a" || response == "A" || response == "all") { return MCMDCONFIRM_ALL; } if (response == "none" || response == "NONE" || response == "None") { return MCMDCONFIRM_NONE; } } } bool MegaCmdShellCommunications::waitForServerReadyOrRegistrationFailed(std::optional timeout) { return timeout ? mPromiseServerReadyOrRegistrationFailed.get_future().wait_for(*timeout) != std::future_status::timeout : (mPromiseServerReadyOrRegistrationFailed.get_future().wait(), true); } void MegaCmdShellCommunications::markServerIsUpdating() { mUpdating = true; } void MegaCmdShellCommunications::unmarkServerIsUpdating() { mUpdating = false; } bool MegaCmdShellCommunications::isServerUpdating() { return mUpdating; } void MegaCmdShellCommunications::markServerReadyOrRegistrationFailed(bool readyOrFailed) { if (!mPromiseServerReadyOrRegistrationFailedAttended.test_and_set()) // only once { mPromiseServerReadyOrRegistrationFailed.set_value(readyOrFailed); } } bool MegaCmdShellCommunications::registerForStateChanges(bool interactive, StateChangedCb_t statechangehandle, bool initiateServer) { if (mStopListener || mUpdating) // finished { return false; } { std::lock_guard g(mRegistrationMutex); if (mLastFailedRegistration && ( (std::chrono::steady_clock::now() - *mLastFailedRegistration) < std::chrono::seconds(30))) { // defer registration attempt return false; } } // if there's a listener thread running if (mListenerThread) { mStopListener = true; triggerListenerThreadShutdown(); mListenerThread->join(); } mClientIdPromise = std::promise(); auto resultRegistration = registerForStateChangesImpl(interactive, initiateServer); if (!resultRegistration) { markServerRegistrationFailed(); return false; } mRegisterRequired = false; mStopListener = false; mListenerThread.reset(new std::thread( [this, fd{resultRegistration.value()}, statechangeCb{std::move(statechangehandle)}]() { bool everSucceeded = false; auto stateChangedWrapped = [&everSucceeded, statechangeCb{std::move(statechangeCb)}](std::string state, MegaCmdShellCommunications &comsManager) { everSucceeded = true; statechangeCb(std::move(state), comsManager); }; auto r = listenToStateChanges(fd, stateChangedWrapped); // Logic to consider registration again: if (r < 0 && !mStopListener && !mUpdating) { auto errorLine = !everSucceeded ? "\nWarning: Unable to register to state changes.\n" // This could happen if for instance, the server rejects registering a listener because max descriptors allowed for it has been depleted #ifdef WIN32 : "\n[MEGAcmdServer.exe process seems to have stopped. Type to respawn or reconnect to it]\n"; #else : "\n[mega-cmd-server process seems to have stopped. Type to respawn or reconnect to it]\n"; #endif setForRegisterAgain(); std::cerr << errorLine << std::flush; } // In either case the above may have failed before receiving server readyness. // This will ensure we don't halt main thread execution if that's the case: markServerRegistrationFailed(); } )); return true; } #ifndef _WIN32 std::optional MegaCmdShellCommunicationsPosix::registerForStateChangesImpl(bool interactive, bool initiateServer) { SOCKET thesock = createSocket(0, initiateServer); if (thesock == INVALID_SOCKET) { cerr << "Failed to create socket for registering for state changes" << endl; return {}; } bool ok = false; ScopeGuard g([this, &thesock, &ok]() { if (!ok) //something failed { close(thesock); } }); string command=interactive?"Xregisterstatelistener":"registerstatelistener"; auto n = send(thesock,command.data(),command.size(), MSG_NOSIGNAL); if (n == SOCKET_ERROR) { cerr << "ERROR writing output Code to socket: " << ERRNO << endl; return {}; } ok = true; mStateListenerSocket = thesock; // to be able to trigger shutdown return thesock; } void MegaCmdShellCommunicationsPosix::triggerListenerThreadShutdown() { if (auto socket = mStateListenerSocket.exchange(-1); socket != -1) { // enforce socket shutdown: this will wake listenerThread ::shutdown(socket, SHUT_RDWR); } } #endif void MegaCmdShellCommunications::setResponseConfirmation(bool /*confirmation*/) { } void MegaCmdShellCommunications::shutdown() { if (mListenerThread) { mStopListener = true; triggerListenerThreadShutdown(); mListenerThread->join(); mListenerThread.reset(); } } bool MegaCmdShellCommunications::registerRequired() { std::lock_guard g(mRegistrationMutex); return mRegisterRequired; } void MegaCmdShellCommunications::setForRegisterAgain(bool dontWait) { std::lock_guard g(mRegistrationMutex); mRegisterRequired = true; if (dontWait) { mLastFailedRegistration = {}; } else { mLastFailedRegistration = std::chrono::steady_clock::now(); } } void MegaCmdShellCommunications::setClientIdPromise(const std::string& clientId) { mClientIdPromise.set_value(clientId); } std::optional MegaCmdShellCommunications::tryToGetClientId(std::chrono::seconds waitForSecs) { auto f = mClientIdPromise.get_future(); if (f.wait_for(waitForSecs) == std::future_status::timeout) { return std::nullopt; } return f.get(); } MegaCmdShellCommunications::~MegaCmdShellCommunications() { assert(!mListenerThread); } } //end namespace MEGAcmd-2.5.2_Linux/src/megacmdshell/megacmdshellcommunications.h000066400000000000000000000120231516543156300250660ustar00rootroot00000000000000/** * @file src/megacmdshellcommunications.h * @brief MEGAcmd: Communications module to connect to server * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of the MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. * * This file is also distributed under the terms of the GNU General * Public License, see http://www.gnu.org/copyleft/gpl.txt for details. */ #ifndef MEGACMDSHELLCOMMUNICATIONS_H #define MEGACMDSHELLCOMMUNICATIONS_H #include "../megacmdcommonutils.h" // In the server, OUTSTREAM is defined in megacmdlogger.h: #define OUTSTREAM getCurrentOut() // However in the exec and cmd apps: #define OUTSTREAM COUT #include #include #include #include #ifdef _WIN32 #include #include //PathAppend #else #include #include #include #include #include #endif #ifdef _WIN32 #include "mega/thread/cppthread.h" class MegaThread : public ::mega::CppThread {}; #else #include "mega/thread/posixthread.h" class MegaThread : public ::mega::PosixThread {}; #endif #ifdef _WIN32 #else typedef int SOCKET; #endif #ifdef _WIN32 #include #define ERRNO WSAGetLastError() #else #define ERRNO errno #endif #ifndef SOCKET_ERROR #define SOCKET_ERROR -1 #endif #ifdef __MACH__ #define MSG_NOSIGNAL 0 #elif _WIN32 #define MSG_NOSIGNAL 0 #endif #define MEGACMDINITIALPORTNUMBER 12300 namespace megacmd { class MegaCmdShellCommunications { public: using StateChangedCb_t = std::function; MegaCmdShellCommunications(); virtual ~MegaCmdShellCommunications(); virtual int executeCommand(std::string command, std::string (*readresponse)(const char *) = NULL, OUTSTREAMTYPE &output = COUT, OUTSTREAMTYPE &errorOutput = CERR, bool interactiveshell = true, std::wstring = L"") = 0; virtual int executeCommandW(std::wstring command, std::string (*readresponse)(const char *) = NULL, OUTSTREAMTYPE &output = COUT, OUTSTREAMTYPE &errorOutput = CERR, bool interactiveshell = true); virtual bool registerForStateChanges(bool interactive, StateChangedCb_t statechangehandle, bool initiateServer = true); virtual void setResponseConfirmation(bool confirmation); bool mServerInitiatedFromShell = false; int readconfirmationloop(const char *question, std::string (*readresponse)(const char *)); // returns true if did not timeout bool waitForServerReadyOrRegistrationFailed(std::optional timeout = {}); void markServerReady() { markServerReadyOrRegistrationFailed(true); } void markServerRegistrationFailed() { markServerReadyOrRegistrationFailed(false); } void markServerIsUpdating(); void unmarkServerIsUpdating(); bool isServerUpdating(); void shutdown(); bool registerRequired(); void setForRegisterAgain(bool dontWait = false); void setClientIdPromise(const std::string& clientId); std::optional tryToGetClientId(std::chrono::seconds waitForSecs = std::chrono::seconds(15)); std::lock_guard getStdoutLockGuard(); private: virtual void triggerListenerThreadShutdown() {}; virtual std::optional registerForStateChangesImpl(bool interactive, bool initiateServer = true) = 0; virtual int listenToStateChanges(int receiveSocket, StateChangedCb_t statechangehandle) = 0; std::mutex mStdoutMutex; std::promise mClientIdPromise; std::unique_ptr mListenerThread; std::mutex mRegistrationMutex; std::optional mLastFailedRegistration; bool mRegisterRequired = true; void markServerReadyOrRegistrationFailed(bool readyOrFailed); protected: std::promise mPromiseServerReadyOrRegistrationFailed; std::atomic_flag mPromiseServerReadyOrRegistrationFailedAttended = ATOMIC_FLAG_INIT; std::atomic_bool mStopListener = false; std::atomic_bool mUpdating = false; }; #ifndef _WIN32 class MegaCmdShellCommunicationsPosix : public MegaCmdShellCommunications { public: int executeCommand(std::string command, std::string (*readresponse)(const char *) = NULL, OUTSTREAMTYPE &output = COUT, OUTSTREAMTYPE &errorOutput = CERR, bool interactiveshell = true, std::wstring = L"") override; private: bool isSocketValid(SOCKET socket); SOCKET createSocket(int number = 0, bool initializeserver = true); std::atomic_int mStateListenerSocket = -1; std::optional registerForStateChangesImpl(bool interactive, bool initiateServer = true) override; int listenToStateChanges(int receiveSocket, StateChangedCb_t statechangehandle) override; void triggerListenerThreadShutdown() override; }; #endif }//end namespace #endif // MEGACMDSHELLCOMMUNICATIONS_H MEGAcmd-2.5.2_Linux/src/megacmdshell/megacmdshellcommunicationsnamedpipes.cpp000066400000000000000000000631561516543156300275040ustar00rootroot00000000000000/** * @file src/megacmdshellcommunicationsnamedpipes.cpp * @brief MEGAcmd: Communications module to connect to server using NamedPipes * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of the MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. * * This file is also distributed under the terms of the GNU General * Public License, see http://www.gnu.org/copyleft/gpl.txt for details. */ #ifdef _WIN32 #include "megacmdshellcommunicationsnamedpipes.h" #include "../megacmdcommonutils.h" #include #include #include #include //SHGetFolderPath #include //PathAppend #include //GetSecurityInfo #include //ConvertSidToStringSid #include namespace megacmd { bool MegaCmdShellCommunicationsNamedPipes::confirmResponse; //TODO: do all this only in parent class bool MegaCmdShellCommunicationsNamedPipes::namedPipeValid(HANDLE namedPipe) { return namedPipe != INVALID_HANDLE_VALUE; } void MegaCmdShellCommunicationsNamedPipes::closeNamedPipe(HANDLE namedPipe){ CloseHandle(namedPipe); } using namespace std; BOOL GetLogonSID (PSID *ppsid) { BOOL bSuccess = FALSE; DWORD dwIndex; DWORD dwLength = 0; PTOKEN_GROUPS ptg = NULL; HANDLE hToken = NULL; DWORD dwErrorCode = 0; // Open the access token associated with the calling process. if (OpenProcessToken( GetCurrentProcess(), TOKEN_QUERY, &hToken ) == FALSE) { dwErrorCode = GetLastError(); wprintf(L"OpenProcessToken failed. GetLastError returned: %d\n", dwErrorCode); return HRESULT_FROM_WIN32(dwErrorCode); } // Verify the parameter passed in is not NULL. if (NULL == ppsid) goto Cleanup; // Get required buffer size and allocate the TOKEN_GROUPS buffer. if (!GetTokenInformation( hToken, // handle to the access token TokenGroups, // get information about the token's groups (LPVOID) ptg, // pointer to TOKEN_GROUPS buffer 0, // size of buffer &dwLength // receives required buffer size )) { if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) goto Cleanup; ptg = (PTOKEN_GROUPS)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwLength); if (ptg == NULL) goto Cleanup; } // Get the token group information from the access token. if (!GetTokenInformation( hToken, // handle to the access token TokenGroups, // get information about the token's groups (LPVOID) ptg, // pointer to TOKEN_GROUPS buffer dwLength, // size of buffer &dwLength // receives required buffer size )) { goto Cleanup; } // Loop through the groups to find the logon SID. for (dwIndex = 0; dwIndex < ptg->GroupCount; dwIndex++) if ((ptg->Groups[dwIndex].Attributes & SE_GROUP_LOGON_ID) == SE_GROUP_LOGON_ID) { // Found the logon SID; make a copy of it. dwLength = GetLengthSid(ptg->Groups[dwIndex].Sid); *ppsid = (PSID) HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwLength); if (*ppsid == NULL) goto Cleanup; if (!CopySid(dwLength, *ppsid, ptg->Groups[dwIndex].Sid)) { HeapFree(GetProcessHeap(), 0, (LPVOID)*ppsid); goto Cleanup; } break; } bSuccess = TRUE; Cleanup: // Free the buffer for the token groups. if (ptg != NULL) HeapFree(GetProcessHeap(), 0, (LPVOID)ptg); return bSuccess; } bool MegaCmdShellCommunicationsNamedPipes::isFileOwnerCurrentUser(HANDLE hFile) { DWORD dwRtnCode = 0; PSID pSidOwner = NULL; BOOL bRtnBool = TRUE; LPWSTR AcctName = NULL; LPTSTR DomainName = NULL; DWORD dwAcctName = 0, dwDomainName = 0; SID_NAME_USE eUse = SidTypeUnknown; PSECURITY_DESCRIPTOR pSD = NULL; // Get the owner SID of the file. dwRtnCode = GetSecurityInfo( hFile, SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION, &pSidOwner, NULL, NULL, NULL, &pSD); // Check GetLastError for GetSecurityInfo error condition. if (dwRtnCode != ERROR_SUCCESS) { cerr << "GetSecurityInfo error = " << ERRNO << endl; return false; } // First call to LookupAccountSid to get the buffer sizes. bRtnBool = LookupAccountSidW( NULL, // local computer pSidOwner, AcctName, (LPDWORD)&dwAcctName, DomainName, (LPDWORD)&dwDomainName, &eUse); // Reallocate memory for the buffers. AcctName = (LPWSTR)GlobalAlloc(GMEM_FIXED, dwAcctName * sizeof(wchar_t)); if (AcctName == NULL) { cerr << "GlobalAlloc error = " << ERRNO << endl; return false; } DomainName = (LPTSTR)GlobalAlloc(GMEM_FIXED,dwDomainName * sizeof(wchar_t)); if (DomainName == NULL) { cerr << "GlobalAlloc error = " << ERRNO << endl; return false; } // Second call to LookupAccountSid to get the account name. bRtnBool = LookupAccountSidW( NULL, // name of local or remote computer pSidOwner, // security identifier AcctName, // account name buffer (LPDWORD)&dwAcctName, // size of account name buffer DomainName, // domain name (LPDWORD)&dwDomainName, // size of domain name buffer &eUse); // SID type if (bRtnBool == FALSE) { if (ERRNO == ERROR_NONE_MAPPED) { cerr << "Account owner not found for specified SID." << endl; } else { cerr << "Error in LookupAccountSid: " << ERRNO << endl; } return false; } wchar_t username[UNLEN+1]; DWORD username_len = UNLEN+1; GetUserNameW(username, &username_len); LPWSTR stringSIDOwner; if (ConvertSidToStringSidW(pSidOwner, &stringSIDOwner)) { if (!wcscmp(username, AcctName) || ( #ifndef __MINGW32__ IsUserAnAdmin() && #endif !wcscmp(stringSIDOwner, L"S-1-5-32-544"))) // owner == user or owner == administrators and current process running as admin { LocalFree(stringSIDOwner); return true; } else { std::wcerr << L"Unmatched owner - current user -> " << AcctName << L" - " << username; #ifndef __MINGW32__ std::wcerr << L" IsUserAdmin=" << IsUserAnAdmin(); #endif std::wcerr << L" SIDOwner=" << stringSIDOwner << endl; LocalFree(stringSIDOwner); return false; } } return false; } HANDLE MegaCmdShellCommunicationsNamedPipes::doOpenPipe(wstring nameOfPipe) { wstring serverPipeName = getNamedPipeName(); if (nameOfPipe != serverPipeName) { if (!WaitNamedPipeW(nameOfPipe.c_str(),16000)) //TODO: use a real time and report timeout error. { OUTSTREAM << "ERROR WaitNamedPipe: " << nameOfPipe << " . errno: " << ERRNO << endl; } } HANDLE theNamedPipe = CreateFileW( nameOfPipe.c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,/*FILE_FLAG_WRITE_THROUGH,*/ NULL ); if (namedPipeValid(theNamedPipe)) { if (!isFileOwnerCurrentUser(theNamedPipe)) { OUTSTREAM << "ERROR: Pipe owner does not match current user!" << endl; return INVALID_HANDLE_VALUE; } } return theNamedPipe; } HANDLE MegaCmdShellCommunicationsNamedPipes::createNamedPipe(int number, bool initializeserver) { wstring nameOfPipe = getNamedPipeName(); if (number) { #ifdef __MINGW32__ wostringstream wos; wos << number; nameOfPipe += wos.str(); #else nameOfPipe += std::to_wstring(number); #endif } // Open the named pipe HANDLE theNamedPipe = doOpenPipe(nameOfPipe); if (!namedPipeValid(theNamedPipe)) { if (!number && initializeserver) { if (ERRNO == ERROR_PIPE_BUSY) { int attempts = 10; while (--attempts && !namedPipeValid(theNamedPipe)) { theNamedPipe = doOpenPipe(nameOfPipe); Sleep(200*(10-attempts)); } if (!namedPipeValid(theNamedPipe)) { if (ERRNO == ERROR_PIPE_BUSY) { OUTSTREAM << "Failed to access server: " << ERRNO << endl; } else { OUTSTREAM << "Failed to access server. Server busy." << endl; } } } else if (ERRNO != ERROR_FILE_NOT_FOUND) { OUTSTREAM << "Unexpected failure to access server: " << ERRNO << endl; } else { //launch server cerr << "MEGAcmd Server not running. Initiating in the background..." << endl; STARTUPINFO si; PROCESS_INFORMATION pi; ZeroMemory( &si, sizeof(si) ); ZeroMemory( &pi, sizeof(pi) ); #ifndef NDEBUG LPCWSTR t = TEXT(".\\MEGAcmdServer.exe"); if (true) { #else wchar_t foldercontainingexec[MAX_PATH+1]; bool okgetcontaningfolder = false; if (S_OK != SHGetFolderPathW(NULL,CSIDL_LOCAL_APPDATA,NULL,0,(LPWSTR)foldercontainingexec)) { if(S_OK != SHGetFolderPathW(NULL,CSIDL_COMMON_APPDATA,NULL,0,(LPWSTR)foldercontainingexec)) { cerr << " Could not get LOCAL nor COMMON App Folder : " << ERRNO << endl; } else { okgetcontaningfolder = true; } } else { okgetcontaningfolder = true; } if (okgetcontaningfolder) { wstring fullpathtoexec(foldercontainingexec); fullpathtoexec+=L"\\MEGAcmd\\MEGAcmdServer.exe"; LPCWSTR t = fullpathtoexec.c_str(); #endif LPWSTR t2 = (LPWSTR) t; si.cb = sizeof(si); // si.wShowWindow = SW_SHOWNOACTIVATE | SW_SHOWMINIMIZED; si.wShowWindow = SW_HIDE; si.dwFlags = STARTF_USESHOWWINDOW; if (!CreateProcess( t,t2,NULL,NULL,TRUE, CREATE_NEW_CONSOLE, NULL,NULL, &si,&pi) ) { COUT << "Unable to execute: " << t << " errno = : " << ERRNO << endl; } // Sleep(2000); // Give it a initial while to start. } //try again: int attempts = 10; int waitimet = 1500; theNamedPipe = INVALID_HANDLE_VALUE; while ( attempts && !namedPipeValid(theNamedPipe)) { Sleep(waitimet/1000); waitimet=waitimet*2; attempts--; theNamedPipe = doOpenPipe(nameOfPipe); } if (attempts < 0) { cerr << "Unable to connect to " << (number?("response namedPipe N "+number):"MEGAcmd server") << ": error=" << ERRNO << endl; cerr << "Please ensure MEGAcmdServer is running" << endl; return INVALID_HANDLE_VALUE; } else { mServerInitiatedFromShell = true; setForRegisterAgain(true); } } } else if (initializeserver) // initializeserver=false when closing server. we dont need to state the error { OUTSTREAM << "ERROR opening namedPipe: " << nameOfPipe << ": " << ERRNO << endl; } return theNamedPipe; } return theNamedPipe; } MegaCmdShellCommunicationsNamedPipes::MegaCmdShellCommunicationsNamedPipes() { #ifdef _WIN32 setlocale(LC_ALL, "en-US"); #endif redirectedstdout = false; } /** * @brief Determines it outputing to console (true) or pipe/file (false) * @return */ bool outputtobinaryorconsole(void) { HANDLE stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE); if (stdoutHandle == INVALID_HANDLE_VALUE) { return false; } DWORD fileType = GetFileType(stdoutHandle); if ((fileType == FILE_TYPE_UNKNOWN) && (GetLastError() != ERROR_SUCCESS)) { return false; } fileType &= ~(FILE_TYPE_REMOTE); if (fileType == FILE_TYPE_CHAR) { DWORD consoleMode; BOOL result = GetConsoleMode(stdoutHandle, &consoleMode); if ((result == FALSE) && (GetLastError() == ERROR_INVALID_HANDLE)) { return false; } else { return true; } } else { return true; } } int MegaCmdShellCommunicationsNamedPipes::executeCommand(string command, std::string (*readresponse)(const char *), OUTSTREAMTYPE &output, OUTSTREAMTYPE &errorOutput, bool interactiveshell, wstring wcommand) { HANDLE theNamedPipe = createNamedPipe(0,command.compare(0,4,"exit") && command.compare(0,4,"quit") && command.compare(0,7,"sendack")); if (!namedPipeValid(theNamedPipe)) { cerr << "MegaCmdShellCommunicationsNamedPipes::executeCommand ERROR, named pipe not valid: " << theNamedPipe << " errno = " << ERRNO << endl; return -1; } ScopeGuard g([this, &theNamedPipe]() { closeNamedPipe(theNamedPipe); }); bool isCat = command.rfind("cat", 0) == 0 || wcommand.rfind(L"cat", 0) == 0; if (interactiveshell) { command="X"+command; } // //unescape \uXXXX sequences // command=unescapeutf16escapedseqs(command.c_str()); //get local wide chars string (utf8 -> utf16) if (!wcommand.size()) { stringtolocalw(command.c_str(),&wcommand); } else if (interactiveshell) { wcommand=L"X"+wcommand; } DWORD n; if (!WriteFile(theNamedPipe,(char *)wcommand.data(),DWORD(wcslen(wcommand.c_str())*sizeof(wchar_t)), &n, NULL)) { cerr << "ERROR writing command to namedPipe: " << ERRNO << endl; return -1; } int receiveNamedPipeNum = -1 ; if (!ReadFile(theNamedPipe, (char *)&receiveNamedPipeNum, sizeof(receiveNamedPipeNum), &n, NULL) ) { cerr << "ERROR reading output namedPipe" << endl; return -1; } HANDLE newNamedPipe = createNamedPipe(receiveNamedPipeNum); if (!namedPipeValid(newNamedPipe)) { cerr << "MegaCmdShellCommunicationsNamedPipes::executeCommand ERROR, receive named pipe not valid: " << receiveNamedPipeNum << " pipe= " << newNamedPipe << " errno = " << ERRNO << endl; return -1; } ScopeGuard g2([this, &newNamedPipe]() { closeNamedPipe(newNamedPipe); }); int outcode = -1; if (!ReadFile(newNamedPipe, (char *)&outcode, sizeof(outcode),&n, NULL)) { cerr << "ERROR reading output code: " << ERRNO << endl; return -1; } bool binaryoutput = isCat && redirectedstdout; bool shouldPrintAdditionalLine = false; while (outcode == MCMD_REQCONFIRM || outcode == MCMD_REQSTRING || outcode == MCMD_PARTIALOUT || outcode == MCMD_PARTIALERR) { if (outcode == MCMD_PARTIALOUT || outcode == MCMD_PARTIALERR) { bool useErrorStream = outcode == MCMD_PARTIALERR; auto &partialOutputStream = useErrorStream ? errorOutput : output; size_t partialoutsize; if (!ReadFile(newNamedPipe, (char *)&partialoutsize, sizeof(partialoutsize),&n, NULL)) { std::cerr << "Error reading size of partial output: " << ERRNO << std::endl; return -1; } if (partialoutsize > 0) { std::unique_ptr outputModeGuard; if (binaryoutput) { outputModeGuard.reset(new WindowsBinaryStdoutGuard()); } else { outputModeGuard.reset(new WindowsUtf8StdoutGuard()); } size_t BUFFERSIZE = 10024; char buffer[10025]; do{ BOOL readok; readok = ReadFile(newNamedPipe, buffer, DWORD(std::min(BUFFERSIZE,partialoutsize)),&n,NULL); if (readok) { if (binaryoutput && !useErrorStream) { Instance::Get().enableInterceptors(false); std::cout << string(buffer,n) << flush; Instance::Get().enableInterceptors(true); } else { buffer[n]='\0'; wstring wbuffer; stringtolocalw((const char*)&buffer,&wbuffer); partialOutputStream << wbuffer << flush; } partialoutsize-=n; } } while(n != 0 && partialoutsize && n !=SOCKET_ERROR); if (isCat && !binaryoutput && interactiveshell && !useErrorStream) { shouldPrintAdditionalLine = true; } } else { cerr << "Invalid size of partial output: " << partialoutsize << endl; return -1; } } else { DWORD BUFFERSIZE = 1024; string confirmQuestion; char bufferQuestion[1025]; memset(bufferQuestion,'\0',1025); BOOL readok; do{ readok = ReadFile(newNamedPipe, bufferQuestion, BUFFERSIZE, &n, NULL); confirmQuestion.append(bufferQuestion); } while(n == BUFFERSIZE && readok); if (!readok) { cerr << "ERROR reading confirm question: " << ERRNO << endl; } if (outcode == MCMD_REQCONFIRM) { int response = MCMDCONFIRM_NO; if (readresponse != NULL) { response = readconfirmationloop(confirmQuestion.c_str(), readresponse); } if (!WriteFile(newNamedPipe, (const char *) &response, sizeof(response), &n, NULL)) { cerr << "ERROR writing confirm response to namedPipe: " << ERRNO << endl; return -1; } } else // MCMD_REQSTRING { string response = "FAILED"; if (readresponse != NULL) { response = readresponse(confirmQuestion.c_str()); } wstring wresponse; stringtolocalw(response.c_str(),&wresponse); if (!WriteFile(newNamedPipe, (char *) wresponse.data(), DWORD(wcslen(wresponse.c_str())*sizeof(wchar_t)), &n, NULL)) { cerr << "ERROR writing confirm response to namedPipe: " << ERRNO << endl; return -1; } } } if (!ReadFile(newNamedPipe, (char *)&outcode, sizeof(outcode),&n, NULL)) { cerr << "ERROR reading output code: " << ERRNO << endl; return -1; } } DWORD BUFFERSIZE = 1024; char buffer[1025]; BOOL readok; do{ readok = ReadFile(newNamedPipe, buffer, BUFFERSIZE,&n,NULL); if (readok) { buffer[n]='\0'; wstring wbuffer; stringtolocalw((const char*)&buffer,&wbuffer); int oldmode; WindowsUtf8StdoutGuard utf8Guard; output << wbuffer << flush; } if (readok && n == BUFFERSIZE) { DWORD total_available_bytes; if (FALSE == PeekNamedPipe(newNamedPipe,0,0,0,&total_available_bytes,0)) { break; } if (total_available_bytes == 0) { break; } } } while(n == BUFFERSIZE && readok); if (!readok) { cerr << "ERROR reading output: " << ERRNO << endl; return -1; } if (shouldPrintAdditionalLine) { WindowsUtf8StdoutGuard utf8Guard; output << std::endl; } return outcode; } int MegaCmdShellCommunicationsNamedPipes::listenToStateChanges(int receiveNamedPipeNum, StateChangedCb_t statechangehandle) { { std::lock_guard g(mStateListenerNamedPipeMutex); mStateListenerNamedPipeHandle = createNamedPipe(receiveNamedPipeNum); if (!namedPipeValid(mStateListenerNamedPipeHandle)) { return -1; } assert(!mStateListenerNamedPipeResetPromise); mStateListenerNamedPipeResetPromise.reset(new std::promise()); } ScopeGuard g([this]() { if (namedPipeValid(mStateListenerNamedPipeHandle)) { closeNamedPipe(mStateListenerNamedPipeHandle); std::lock_guard g(mStateListenerNamedPipeMutex); mStateListenerNamedPipeHandle = INVALID_HANDLE_VALUE; auto promise = std::move(mStateListenerNamedPipeResetPromise); promise->set_value(); } }); int timeout_notified_server_might_be_down = 0; while (!mStopListener) { string newstate; DWORD BUFFERSIZE = 1024; char buffer[1025]; DWORD n; bool readok; do{ readok = ReadFile(mStateListenerNamedPipeHandle, buffer, BUFFERSIZE, &n, NULL); if (readok) { buffer[n]='\0'; newstate += buffer; } } while(n == BUFFERSIZE && readok); if (!readok) { if ((mStopListener || mUpdating) && (ERRNO == ERROR_BROKEN_PIPE || ERRNO == ERROR_OPERATION_ABORTED)) { // expected errors after shutdown: return 0; } cerr << "ERROR reading output (state change): " << ERRNO << endl; return -1; } if (!n) // server closed the connection { return -1; } if (statechangehandle) { statechangehandle(newstate, *this); } } return 0; } std::optional MegaCmdShellCommunicationsNamedPipes::registerForStateChangesImpl(bool interactive, bool initiateServer) { HANDLE theNamedPipe = createNamedPipe(0, initiateServer); if (!namedPipeValid(theNamedPipe)) { return {}; } ScopeGuard g([this, &theNamedPipe]() { closeNamedPipe(theNamedPipe); }); wstring wcommand=interactive?L"Xregisterstatelistener":L"registerstatelistener"; DWORD n; if (!WriteFile(theNamedPipe,(char *)wcommand.data(),DWORD(wcslen(wcommand.c_str())*sizeof(wchar_t)), &n, NULL)) { cerr << "ERROR writing command to namedPipe: " << ERRNO << endl; return {}; } int receiveNamedPipeNum = -1; if (!ReadFile(theNamedPipe, (char *)&receiveNamedPipeNum, sizeof(receiveNamedPipeNum), &n, NULL) ) { cerr << "ERROR reading output namedPipe" << endl; return {}; } return receiveNamedPipeNum; } void MegaCmdShellCommunicationsNamedPipes::setResponseConfirmation(bool confirmation) { confirmResponse = confirmation; } void MegaCmdShellCommunicationsNamedPipes::triggerListenerThreadShutdown() { std::unique_lock g(mStateListenerNamedPipeMutex); // this would cause the wake of the listener thread: if (namedPipeValid(mStateListenerNamedPipeHandle)) { std::optional> fut; if (mStateListenerNamedPipeResetPromise) { fut = mStateListenerNamedPipeResetPromise->get_future(); } CancelIoEx(mStateListenerNamedPipeHandle, NULL); // if we were extremely unlucky and above cancel occurred when still not in ReadFile, below wait would fail. If that's the case, // a second attempt should work g.unlock(); if (fut && fut->wait_for(std::chrono::milliseconds(200)) == std::future_status::ready) { return; } g.lock(); if (namedPipeValid(mStateListenerNamedPipeHandle)) { CancelIoEx(mStateListenerNamedPipeHandle, NULL); } }; } MegaCmdShellCommunicationsNamedPipes::~MegaCmdShellCommunicationsNamedPipes() { } } //end namespace #endif MEGAcmd-2.5.2_Linux/src/megacmdshell/megacmdshellcommunicationsnamedpipes.h000066400000000000000000000051361516543156300271430ustar00rootroot00000000000000/** * @file src/megacmdshellcommunicationsnamedpipes.h * @brief MEGAcmd: Communications module to connect to server using NamedPipes * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of the MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. * * This file is also distributed under the terms of the GNU General * Public License, see http://www.gnu.org/copyleft/gpl.txt for details. */ #ifndef MEGACMDSHELLCOMMUNICATIONSNAMEDPIPES_H #define MEGACMDSHELLCOMMUNICATIONSNAMEDPIPES_H #ifdef _WIN32 #include "megacmdshellcommunications.h" #include #include //getusername #define ERRNO WSAGetLastError() #include #include #include #include #include //PathAppend namespace megacmd { typedef struct structListenStateChangesNamedPipe{ int receiveNamedPipeNum; void (*statechangehandle)(std::string); } sListenStateChangesNamedPipe; class MegaCmdShellCommunicationsNamedPipes : public MegaCmdShellCommunications { private: bool redirectedstdout; public: MegaCmdShellCommunicationsNamedPipes(); MegaCmdShellCommunicationsNamedPipes(bool _redirectedstdout):redirectedstdout(_redirectedstdout){}; ~MegaCmdShellCommunicationsNamedPipes(); virtual int executeCommand(std::string command, std::string (*readresponse)(const char *) = NULL, OUTSTREAMTYPE &output = COUT, OUTSTREAMTYPE &errorOutput = CERR, bool interactiveshell = true, std::wstring = L"") override; void setResponseConfirmation(bool confirmation); private: bool namedPipeValid(HANDLE namedPipe); void closeNamedPipe(HANDLE namedPipe); std::optional registerForStateChangesImpl(bool interactive, bool initiateServer = true) override; virtual int listenToStateChanges(int receiveNamedPipeNum, StateChangedCb_t statechangehandle) override; void triggerListenerThreadShutdown() override; static bool confirmResponse; std::mutex mStateListenerNamedPipeMutex; HANDLE mStateListenerNamedPipeHandle = INVALID_HANDLE_VALUE; std::unique_ptr> mStateListenerNamedPipeResetPromise; HANDLE doOpenPipe(std::wstring nameOfPipe); HANDLE createNamedPipe(int number = 0,bool initializeserver = true); static bool isFileOwnerCurrentUser(HANDLE hFile); }; }//end namespace #endif #endif // MEGACMDSHELLCOMMUNICATIONS_H MEGAcmd-2.5.2_Linux/src/megacmdutils.cpp000066400000000000000000000726161516543156300200720ustar00rootroot00000000000000/** * @file src/megacmdutils.cpp * @brief MEGAcmd: Auxiliary methods * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of the MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include "megacmdutils.h" #include "mega/types.h" #ifdef USE_PCRE #include #elif __cplusplus >= 201103L && !defined(__MINGW32__) #include #endif #ifdef _WIN32 #else #include // console size #endif #include #include #include using namespace mega; namespace megacmd { string getUserInSharedNode(MegaNode *n, MegaApi *api) { MegaShareList * msl = api->getInSharesList(); for (int i = 0; i < msl->size(); i++) { MegaShare *share = msl->get(i); if (share->getNodeHandle() == n->getHandle()) { string suser = share->getUser(); delete ( msl ); return suser; } } delete ( msl ); return ""; } const char* getAccessLevelStr(int level) { switch (level) { case MegaShare::ACCESS_UNKNOWN: return "unknown access"; break; case MegaShare::ACCESS_READ: return "read access"; break; case MegaShare::ACCESS_READWRITE: return "read/write access"; break; case MegaShare::ACCESS_FULL: return "full access"; break; case MegaShare::ACCESS_OWNER: return "owner access"; break; } return "undefined"; } const char* getSyncPathStateStr(int state) { static std::vector svalue { #define SOME_GENERATOR_MACRO(_, ShortName, __) ShortName, GENERATE_FROM_SYNC_PATH_STATE(SOME_GENERATOR_MACRO) #undef SOME_GENERATOR_MACRO }; if (state < static_cast(svalue.size())) { return svalue[state]; } return "undefined"; } const char * syncRunStateStr(unsigned e) { static std::vector svalue { #define SOME_GENERATOR_MACRO(_, ShortName, __) ShortName, GENERATE_FROM_SYNC_RUN_STATE(SOME_GENERATOR_MACRO) #undef SOME_GENERATOR_MACRO }; if (e < svalue.size()) { return svalue[e]; } return "Unknown"; } string visibilityToString(int visibility) { if (visibility == MegaUser::VISIBILITY_VISIBLE) { return "Visible"; } if (visibility == MegaUser::VISIBILITY_HIDDEN) { return "Hidden"; } if (visibility == MegaUser::VISIBILITY_UNKNOWN) { return "Unknown visibility"; } if (visibility == MegaUser::VISIBILITY_INACTIVE) { return "Inactive"; } if (visibility == MegaUser::VISIBILITY_BLOCKED) { return "Blocked"; } return "Undefined visibility"; } const char * getMCMDErrorString(int errorCode) { switch(errorCode) { case MCMD_OK: return "Everything OK"; case MCMD_EARGS: return "Wrong arguments"; case MCMD_INVALIDEMAIL: return "Invalid email"; case MCMD_NOTFOUND: return "Resource not found"; case MCMD_INVALIDSTATE: return "Invalid state"; case MCMD_INVALIDTYPE: return "Invalid type"; case MCMD_NOTPERMITTED: return "Operation not allowed"; case MCMD_NOTLOGGEDIN: return "Needs logging in"; case MCMD_NOFETCH: return "Nodes not fetched"; case MCMD_EUNEXPECTED: return "Unexpected failure"; case MCMD_REQCONFIRM: return "Confirmation required"; default: return "UNKNOWN"; } } const char * getErrorCodeStr(MegaError *e) { if (e) { return MegaError::getErrorString(e->getErrorCode()); } return "NullError"; } const char * getMountResultStr(int mountResult) { #ifdef WIN32 if (mountResult == mega::MegaMount::BACKEND_UNAVAILABLE) { return "WinFSP is not installed. It needs to be installed for the operation to succeed. You can get it " "from https://github.com/winfsp/winfsp/releases/download/v2.1/winfsp-2.1.25156.msi"; } #endif return MegaMount::getResultDescription(mountResult); } const char * getLogLevelStr(int loglevel) { switch (loglevel) { case MegaApi::LOG_LEVEL_FATAL: return "FATAL"; break; case MegaApi::LOG_LEVEL_ERROR: return "ERROR"; break; case MegaApi::LOG_LEVEL_WARNING: return "WARNING"; break; case MegaApi::LOG_LEVEL_INFO: return "INFO"; break; case MegaApi::LOG_LEVEL_DEBUG: return "DEBUG"; break; case MegaApi::LOG_LEVEL_MAX: return "VERBOSE"; break; default: return "UNKNOWN"; break; } } std::optional getLogLevelNum(const char* level) { if (!strcmp(level, "FATAL") || !strcmp(level, "fatal")) { return MegaApi:: LOG_LEVEL_FATAL; } if (!strcmp(level, "ERROR") || !strcmp(level, "error")) { return MegaApi:: LOG_LEVEL_ERROR; } if (!strcmp(level, "WARNING") || !strcmp(level, "warning")) { return MegaApi:: LOG_LEVEL_WARNING; } if (!strcmp(level, "INFO") || !strcmp(level, "info")) { return MegaApi:: LOG_LEVEL_INFO; } if (!strcmp(level, "DEBUG") || !strcmp(level, "debug")) { return MegaApi:: LOG_LEVEL_DEBUG; } if (!strcmp(level, "VERBOSE") || !strcmp(level, "verbose")) { return MegaApi:: LOG_LEVEL_MAX; } int i = -1; std::istringstream is(level); if (!(is >> i)) { return {}; } return std::clamp(i, (int) MegaApi::LOG_LEVEL_FATAL, (int) MegaApi::LOG_LEVEL_MAX); } const char * getShareLevelStr(int sharelevel) { switch (sharelevel) { case MegaShare::ACCESS_UNKNOWN: return "UNKNOWN"; break; case MegaShare::ACCESS_READ: return "READ"; break; case MegaShare::ACCESS_READWRITE: return "READWRITE"; break; case MegaShare::ACCESS_FULL: return "FULL"; break; case MegaShare::ACCESS_OWNER: return "OWNER"; break; default: return "UNEXPECTED"; break; } } int getShareLevelNum(const char* level) { if (!strcmp(level, "UNKNOWN")) { return MegaShare::ACCESS_UNKNOWN; } if (!strcmp(level, "READ")) { return MegaShare::ACCESS_READ; } if (!strcmp(level, "READWRITE")) { return MegaShare::ACCESS_READWRITE; } if (!strcmp(level, "FULL")) { return MegaShare::ACCESS_FULL; } if (!strcmp(level, "OWNER")) { return MegaShare::ACCESS_OWNER; } if (!strcmp(level, "UNEXPECTED")) { return -9; } return atoi(level); } const char * getTransferStateStr(int transferState) { switch (transferState) { case MegaTransfer::STATE_QUEUED: return "QUEUED"; break; case MegaTransfer::STATE_ACTIVE: return "ACTIVE"; break; case MegaTransfer::STATE_PAUSED: return "PAUSED"; break; case MegaTransfer::STATE_RETRYING: return "RETRYING"; break; case MegaTransfer::STATE_COMPLETING: return "COMPLETING"; break; case MegaTransfer::STATE_COMPLETED: return "COMPLETED"; break; case MegaTransfer::STATE_CANCELLED: return "CANCELLED"; break; case MegaTransfer::STATE_FAILED: return "FAILED"; break; default: return ""; break; } } string backupSatetStr(int backupstate) { if (backupstate == MegaScheduledCopy::SCHEDULED_COPY_FAILED) { return "FAILED"; } if (backupstate == MegaScheduledCopy::SCHEDULED_COPY_CANCELED) { return "CANCELED"; } if (backupstate == MegaScheduledCopy::SCHEDULED_COPY_INITIALSCAN) { return "INITIALSCAN"; } if (backupstate == MegaScheduledCopy::SCHEDULED_COPY_ACTIVE) { return "ACTIVE"; } if (backupstate == MegaScheduledCopy::SCHEDULED_COPY_ONGOING) { return "ONGOING"; } if (backupstate == MegaScheduledCopy::SCHEDULED_COPY_SKIPPING) { return "SKIPPING"; } if (backupstate == MegaScheduledCopy::SCHEDULED_COPY_REMOVING_EXCEEDING) { return "EXCEEDREMOVAL"; } return "UNDEFINED"; } const char * getProxyTypeStr(int proxyType) { switch (proxyType) { case MegaProxy::PROXY_AUTO: return "PROXY_AUTO"; break; case MegaProxy::PROXY_NONE: return "PROXY_NONE"; break; case MegaProxy::PROXY_CUSTOM: return "PROXY_CUSTOM"; break; default: return "INVALID"; break; } } int getLinkType(string link) { if (link.find("/folder/") != string::npos) { return MegaNode::TYPE_FOLDER; } if (link.find("/file/") != string::npos) { return MegaNode::TYPE_FILE; } size_t posHash = link.find_first_of("#"); if (( posHash == string::npos ) || !( posHash + 1 < link.length())) { return MegaNode::TYPE_UNKNOWN; } if (link.at(posHash + 1) == 'F') { return MegaNode::TYPE_FOLDER; } return MegaNode::TYPE_FILE; } const char *fillStructWithSYYmdHMS(string &stime, struct tm &dt) { memset(&dt, 0, sizeof(struct tm)); #ifdef _WIN32 if (stime.size() != 14) { return NULL; } for(int i=0;i<14;i++) { if ( (stime.at(i) < '0') || (stime.at(i) > '9') ) { return NULL; } } dt.tm_year = atoi(stime.substr(0,4).c_str()) - 1900; dt.tm_mon = atoi(stime.substr(4,2).c_str()) - 1; dt.tm_mday = atoi(stime.substr(6,2).c_str()); dt.tm_hour = atoi(stime.substr(8,2).c_str()); dt.tm_min = atoi(stime.substr(10,2).c_str()); dt.tm_sec = atoi(stime.substr(12,2).c_str()); #else strptime(stime.c_str(), "%Y%m%d%H%M%S", &dt); #endif dt.tm_isdst = -1; //let mktime interprete if time has Daylight Saving Time flag correction //TODO: would this work cross platformly? At least I believe it'll be consistent with localtime. Otherwise, we'd need to save that return stime.c_str(); } const char *getFormatStrFromId(int strftimeformatid) { switch (strftimeformatid) { case MCMDTIME_RFC2822: return "%a, %d %b %Y %T %z"; break; case MCMDTIME_ISO6081: return "%F"; break; case MCMDTIME_ISO6081WITHTIME: return "%FT%T"; break; case MCMDTIME_SHORT: return "%d%b%Y %T"; break; case MCMDTIME_SHORTWITHUTCDEVIATION: return "%d%b%Y %T %z"; break; default: return "%a, %d %b %Y %T %z"; break; } } const char *getTimeFormatNameFromId(int strftimeformatid) { switch (strftimeformatid) { case MCMDTIME_RFC2822: return "RFC2822"; break; case MCMDTIME_ISO6081: return "ISO6081"; break; case MCMDTIME_ISO6081WITHTIME: return "ISO6081_WITH_TIME"; break; case MCMDTIME_SHORT: return "SHORT"; break; case MCMDTIME_SHORTWITHUTCDEVIATION: return "SHORT_UTC"; break; default: return "INVALID"; break; } } string secondsToText(m_time_t seconds, bool humanreadable) { ostringstream os; os.precision(2); if (humanreadable) { m_time_t reducedSize = m_time_t( seconds > 3600 * 2 ? seconds / 3600.0 : ( seconds > 60 * 2 ? seconds / 60.0 : seconds) ); os << std::fixed << reducedSize; os << ( seconds > 3600 * 2 ? " hours" : ( seconds > 60 * 2 ? " minutes" : " seconds" )); } else { os << seconds; } return os.str(); } const char *getTimeFormatFromSTR(string formatName) { string lformatName = formatName; transform(lformatName.begin(), lformatName.end(), lformatName.begin(), [](char c) { return (char)::tolower(c); }); if (lformatName == "rfc2822") { return getFormatStrFromId(MCMDTIME_RFC2822); } else if (lformatName == "iso6081") { return getFormatStrFromId(MCMDTIME_ISO6081); } else if (lformatName == "iso6081_with_time") { return getFormatStrFromId(MCMDTIME_ISO6081WITHTIME); } else if (lformatName == "short") { return getFormatStrFromId(MCMDTIME_SHORT); } else if (lformatName == "short_utc") { return getFormatStrFromId(MCMDTIME_SHORTWITHUTCDEVIATION); } else { return formatName.c_str(); } } std::string getReadableTime(const m_time_t rawtime, const char* strftimeformat) { struct tm dt; char buffer [40]; m_localtime(rawtime, &dt); strftime(buffer, sizeof( buffer ), strftimeformat, &dt); // Following RFC 2822 (as in date -R) return std::string(buffer); } std::string getReadableTime(const m_time_t rawtime, int strftimeformatid) { return getReadableTime(rawtime, getFormatStrFromId(strftimeformatid)); } std::string getReadableShortTime(const m_time_t rawtime, bool showUTCDeviation) { if (rawtime != -1) { return getReadableTime(rawtime, showUTCDeviation?MCMDTIME_SHORTWITHUTCDEVIATION:MCMDTIME_SHORT); } else { char buffer [40]; sprintf(buffer,"INVALID_TIME %lld", (long long)rawtime); return std::string(buffer); } } std::string getReadablePeriod(const m_time_t rawtime) { long long rest = rawtime; long long years = rest/31557600; //365.25 days rest = rest%31557600; long long months = rest/2629800; // average month 365.25/12 days rest = rest%2629800; long long days = rest/86400; rest = rest%86400; long long hours = rest/3600; rest = rest%3600; long long minutes = rest/60; long long seconds = rest%60; ostringstream ostoret; if (years) ostoret << years << "y"; if (months) ostoret << months << "m"; if (days) ostoret << days << "d"; if (hours) ostoret << hours << "h"; if (minutes) ostoret << minutes << "M"; if (seconds) ostoret << seconds << "s"; string toret = ostoret.str(); return toret.size()?toret:"0s"; } m_time_t getTimeStampAfter(m_time_t initial, string timestring) { char *buffer = new char[timestring.size() + 1]; strcpy(buffer, timestring.c_str()); int days = 0, hours = 0, minutes = 0, seconds = 0, months = 0, years = 0; char * ptr = buffer; char * last = buffer; while (*ptr != '\0') { if (( *ptr < '0' ) || ( *ptr > '9' )) { switch (*ptr) { case 'd': *ptr = '\0'; days = atoi(last); break; case 'h': *ptr = '\0'; hours = atoi(last); break; case 'M': *ptr = '\0'; minutes = atoi(last); break; case 's': *ptr = '\0'; seconds = atoi(last); break; case 'm': *ptr = '\0'; months = atoi(last); break; case 'y': *ptr = '\0'; years = atoi(last); break; default: { delete[] buffer; return -1; } } last = ptr + 1; } char *prev = ptr; ptr++; if (*ptr == '\0' && ( *prev >= '0' ) && ( *prev <= '9' )) //reach the end with a number { delete[] buffer; return -1; } } struct tm dt; m_localtime(initial, &dt); dt.tm_mday += days; dt.tm_hour += hours; dt.tm_min += minutes; dt.tm_sec += seconds; dt.tm_mon += months; dt.tm_year += years; delete [] buffer; return m_mktime(&dt); } m_time_t getTimeStampAfter(string timestring) { m_time_t initial = m_time(); return getTimeStampAfter(initial, timestring); } m_time_t getTimeStampBefore(m_time_t initial, string timestring) { char *buffer = new char[timestring.size() + 1]; strcpy(buffer, timestring.c_str()); int days = 0, hours = 0, minutes = 0, seconds = 0, months = 0, years = 0; char * ptr = buffer; char * last = buffer; while (*ptr != '\0') { if (( *ptr < '0' ) || ( *ptr > '9' )) { switch (*ptr) { case 'd': *ptr = '\0'; days = atoi(last); break; case 'h': *ptr = '\0'; hours = atoi(last); break; case 'M': *ptr = '\0'; minutes = atoi(last); break; case 's': *ptr = '\0'; seconds = atoi(last); break; case 'm': *ptr = '\0'; months = atoi(last); break; case 'y': *ptr = '\0'; years = atoi(last); break; default: { delete[] buffer; return -1; } } last = ptr + 1; } char *prev = ptr; ptr++; if (*ptr == '\0' && ( *prev >= '0' ) && ( *prev <= '9' )) //reach the end with a number { delete[] buffer; return -1; } } struct tm dt; m_localtime(initial, &dt); dt.tm_mday -= days; dt.tm_hour -= hours; dt.tm_min -= minutes; dt.tm_sec -= seconds; dt.tm_mon -= months; dt.tm_year -= years; delete [] buffer; return m_mktime(&dt); } m_time_t getTimeStampBefore(string timestring) { m_time_t initial = m_time(); return getTimeStampBefore(initial, timestring); } bool getMinAndMaxTime(string timestring, m_time_t *minTime, m_time_t *maxTime) { m_time_t initial = m_time(); return getMinAndMaxTime(initial, timestring, minTime, maxTime); } bool getMinAndMaxTime(m_time_t initial, string timestring, m_time_t *minTime, m_time_t *maxTime) { *minTime = -1; *maxTime = -1; if (!timestring.size()) { return false; } if (timestring.at(0) == '+') { size_t posmin = timestring.find("-"); string maxTimestring = timestring.substr(1,posmin-1); *maxTime = getTimeStampBefore(initial,maxTimestring); if (*maxTime == -1) { return false; } if (posmin == string::npos) { *minTime = 0; return true; } string minTimestring = timestring.substr(posmin+1); *minTime = getTimeStampBefore(initial,minTimestring); if (*minTime == -1) { return false; } } else if (timestring.at(0) == '-') { size_t posmax = timestring.find("+"); string minTimestring = timestring.substr(1,posmax-1); *minTime = getTimeStampBefore(initial,minTimestring); if (*minTime == -1) { return false; } if (posmax == string::npos) { *maxTime = initial; return true; } string maxTimestring = timestring.substr(posmax+1); *maxTime = getTimeStampBefore(initial,maxTimestring); if (*maxTime == -1) { return false; } } else { return false; } return true; } bool getMinAndMaxSize(string sizestring, int64_t *minSize, int64_t *maxSize) { *minSize = -1; *maxSize = -1; if (!sizestring.size()) { return false; } if (sizestring.at(0) == '+') { size_t posmax = sizestring.find("-"); string minSizestring = sizestring.substr(1,posmax-1); *minSize = textToSize(minSizestring.c_str()); if (*minSize == -1) { return false; } if (posmax == string::npos) { *maxSize = -1; return true; } string maxSizestring = sizestring.substr(posmax+1); *maxSize = textToSize(maxSizestring.c_str()); if (*maxSize == -1) { return false; } } else if (sizestring.at(0) == '-') { size_t posmin = sizestring.find("+"); string maxSizestring = sizestring.substr(1,posmin-1); *maxSize = textToSize(maxSizestring.c_str()); if (*maxSize == -1) { return false; } if (posmin == string::npos) { *minSize = -1; return true; } string minSizestring = sizestring.substr(posmin+1); *minSize = textToSize(minSizestring.c_str()); if (*minSize == -1) { return false; } } else { return false; } return true; } bool isRegExp(string what) { #ifdef USE_PCRE if (( what == "." ) || ( what == ".." ) || ( what == "/" )) { return false; } while (true){ if (what.find("./") == 0) { what=what.substr(2); } else if(what.find("../") == 0) { what=what.substr(3); } else if(what.size()>=3 && (what.find("/..") == what.size()-3)) { what=what.substr(0,what.size()-3); } else if(what.size()>=2 && (what.find("/.") == what.size()-2)) { what=what.substr(0,what.size()-2); } else if(what.size()>=2 && (what.find("/.") == what.size()-2)) { what=what.substr(0,what.size()-2); } else { break; } } replaceAll(what, "/../", "/"); replaceAll(what, "/./", "/"); replaceAll(what, "/", ""); string s = pcrecpp::RE::QuoteMeta(what); string ns = s; replaceAll(ns, "\\\\\\", "\\"); bool isregex = strcmp(what.c_str(), ns.c_str()); return isregex; //TODO: #elif __cplusplus >= 201103L && !defined(__MINGW32__) //TODO?? #else return hasWildCards(what); #endif } string unquote(string what) { #ifdef USE_PCRE if (( what == "." ) || ( what == ".." ) || ( what == "/" )) { return what; } string pref=""; while (true){ if (what.find("./") == 0) { what=what.substr(2); pref+="./"; } else if(what.find("../") == 0) { what=what.substr(3); pref+="../"; } else { break; } } string s = pcrecpp::RE::QuoteMeta(what.c_str()); string ns = s; replaceAll(ns, "\\\\\\", "\\"); replaceAll(ns, "\\/\\.\\.\\/", "/../"); replaceAll(ns, "\\/\\.\\/", "/./"); return pref+ns; #else return what; #endif } bool megacmdWildcardMatch(const char *pszString, const char *pszMatch) // cf. http://www.planet-source-code.com/vb/scripts/ShowCode.asp?txtCodeId=1680&lngWId=3 { const char *cp = nullptr; const char *mp = nullptr; while ((*pszString) && (*pszMatch != '*')) { if ((*pszMatch != *pszString) && (*pszMatch != '?')) { return false; } pszMatch++; pszString++; } while (*pszString) { if (*pszMatch == '*') { if (!*++pszMatch) { return true; } mp = pszMatch; cp = pszString + 1; } else if ((*pszMatch == *pszString) || (*pszMatch == '?')) { pszMatch++; pszString++; } else { pszMatch = mp; pszString = cp++; } } while (*pszMatch == '*') { pszMatch++; } return !*pszMatch; } bool patternMatches(const char *what, const char *pattern, bool usepcre) { if (usepcre) { #ifdef USE_PCRE pcrecpp::RE re(pattern); if (re.error().length()) { //In case the user supplied non-pcre regexp with * or ? in it. string newpattern(pattern); replaceAll(newpattern,"*",".*"); replaceAll(newpattern,"?","."); re=pcrecpp::RE(newpattern); } if (!re.error().length()) { bool toret = re.FullMatch(what); return toret; } else { LOG_warn << "Invalid PCRE regex: " << re.error(); return false; } #elif __cplusplus >= 201103L && !defined(__MINGW32__) try { return std::regex_match(what, std::regex(pattern)); } catch (std::regex_error e) { LOG_warn << "Couldn't compile regex: " << pattern; return false; } #else LOG_warn << " PCRE not supported"; return false; #endif } return megacmdWildcardMatch(what,pattern); } bool nodeNameIsVersion(string &nodeName) { bool isversion = false; if (nodeName.size() > 12 && nodeName.at(nodeName.size()-11) =='#') //TODO: def version separator elswhere { for (size_t i = nodeName.size()-10; i < nodeName.size() ; i++) { if (nodeName.at(i) > '9' || nodeName.at(i) < '0') break; if (i == (nodeName.size() - 1)) { isversion = true; } } } return isversion; } bool setOptionsAndFlags(map *opts, map *flags, vector *ws, set vvalidOptions, bool global) { bool discarded = false; for (std::vector::iterator it = ws->begin(); it != ws->end(); ) { string w = ( string ) * it; if (w.length() && ( w.at(0) == '-' )) //begins with "-" { if (( w.length() > 1 ) && ( w.at(1) != '-' )) //single character flags! { for (unsigned int i = 1; i < w.length(); i++) { string optname = w.substr(i, 1); if (vvalidOptions.find(optname) != vvalidOptions.end()) { ( *flags )[optname] = ( flags->count(optname) ? ( *flags )[optname] : 0 ) + 1; } else { LOG_err << "Invalid argument: " << optname; discarded = true; } } } else if (w == "--") { it = ws->erase(it); return discarded; // cease to look for options & leave the rest } else if (w.find_first_of("=") == std::string::npos) //flag { string optname = ltrim(w, '-'); if (vvalidOptions.find(optname) != vvalidOptions.end()) { ( *flags )[optname] = ( flags->count(optname) ? ( *flags )[optname] : 0 ) + 1; } else { LOG_err << "Invalid argument: " << optname; discarded = true; } } else //option=value { string cleared = ltrim(w, '-'); size_t p = cleared.find_first_of("="); string optname = cleared.substr(0, p); if (vvalidOptions.find(optname) != vvalidOptions.end()) { string value = cleared.substr(p + 1); value = rtrim(ltrim(value, '"'), '"'); ( *opts )[optname] = value; } else { LOG_err << "Invalid argument: " << optname; discarded = true; } } it = ws->erase(it); } else //not an option/flag { if (global) { return discarded; //leave the others } ++it; } } return discarded; } #ifndef _WIN32 string readablePermissions(int permvalue) { stringstream os; int owner = (permvalue >> 6) & 0x07; int group = (permvalue >> 3) & 0x07; int others = permvalue & 0x07; os << owner << group << others; return os.str(); } int permissionsFromReadable(string permissions) { if (permissions.size()==3) { int owner = permissions.at(0) - '0'; int group = permissions.at(1) - '0'; int others = permissions.at(2) - '0'; if ( (owner < 0) || (owner > 7) || (group < 0) || (group > 7) || (others < 0) || (others > 7) ) { LOG_err << "Invalid permissions value: " << permissions; return -1; } return (owner << 6) + ( group << 3) + others; } return -1; } #endif std::string handleToBase64(const MegaHandle &handle) { char *base64Handle = MegaApi::handleToBase64(handle); std::string toret{base64Handle}; delete [] base64Handle; return toret; } std::string syncBackupIdToBase64(const MegaHandle &handle) { char *base64Handle = MegaApi::userHandleToBase64(handle); std::string toret{base64Handle}; delete [] base64Handle; return toret; } mega::MegaHandle base64ToSyncBackupId(const std::string &shandle) { return MegaApi::base64ToUserHandle(shandle.c_str()); } }//end namespace MEGAcmd-2.5.2_Linux/src/megacmdutils.h000066400000000000000000000122551516543156300175300ustar00rootroot00000000000000/** * @file src/megacmdutils.h * @brief MEGAcmd: Auxiliary methods * * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of the MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #ifndef MEGACMDUTILS_H #define MEGACMDUTILS_H #include "megacmdcommonutils.h" #include "megacmd.h" #include namespace megacmd { using ::mega::m_time_t; std::string getUserInSharedNode(mega::MegaNode *n, mega::MegaApi *api); /* code translation*/ const char* getAccessLevelStr(int level); // Parameters: // - name // - ShortName // - Description #define GENERATE_FROM_SYNC_RUN_STATE(GENERATOR_MACRO) \ GENERATOR_MACRO(RUNSTATE_PENDING, "Pending", "Sync config has loaded but we have not attempted to start it yet")\ GENERATOR_MACRO(RUNSTATE_LOADING, "Loading", "Sync is in the process of loading from disk")\ GENERATOR_MACRO(RUNSTATE_RUNNING, "Running", "Sync loaded and active")\ GENERATOR_MACRO(RUNSTATE_SUSPENDED, "Suspended", "Sync is not loaded, but it is on disk with the last known sync state")\ GENERATOR_MACRO(RUNSTATE_DISABLED, "Disabled", "Sync has been disabled (no state cached). Starting it is like configuring a brand new sync with those settings")\ // Parameters: // - name // - ShortName // - Description #define GENERATE_FROM_SYNC_PATH_STATE(GENERATOR_MACRO) \ GENERATOR_MACRO(STATE_NONE, "NONE", "Status unknown")\ GENERATOR_MACRO(STATE_SYNCED, "Synced", "Synced, no transfers/pending actions are ongoing")\ GENERATOR_MACRO(STATE_PENDING, "Pending", "Sync engine is doing some calculations")\ GENERATOR_MACRO(STATE_SYNCING, "Syncing", "Transfers/pending actions are being carried out")\ GENERATOR_MACRO(STATE_IGNORED, "Processing", "State cannot be determined (too busy, try again later)") // Note: STATE_IGNORED can happen when attempting to get the state, but it fails to block the sdk in a timely attempt. // Also with excluded paths, but root shall not be excluded. const char* getSyncPathStateStr(int state); const char * syncRunStateStr(unsigned e); std::string visibilityToString(int visibility); const char * getMCMDErrorString(int errorCode); const char * getErrorCodeStr(mega::MegaError *e); const char * getMountResultStr(int mountResult); const char * getLogLevelStr(int loglevel); std::optional getLogLevelNum(const char* level); const char * getShareLevelStr(int sharelevel); int getShareLevelNum(const char* level); const char * getTransferStateStr(int transferState); std::string backupSatetStr(int backupstate); const char * getProxyTypeStr(int proxyType); /* Files and folders */ int getLinkType(std::string link); /* Time related */ const char *fillStructWithSYYmdHMS(std::string &stime, struct tm &dt); enum MegaCmdStrfTimeFormatId { MCMDTIME_RFC2822, MCMDTIME_ISO6081, MCMDTIME_ISO6081WITHTIME, MCMDTIME_SHORT, MCMDTIME_SHORTWITHUTCDEVIATION, MCMDTIME_TOTAL }; const char *getTimeFormatFromSTR(std::string formatName); const char *getFormatStrFromId(int strftimeformatid); const char *getTimeFormatNameFromId(int strftimeformatid); std::string secondsToText(m_time_t seconds, bool humanreadable = true); std::string getReadableTime(const m_time_t rawtime, const char* strftimeformat); std::string getReadableTime(const m_time_t rawtime, int strftimeformatid = MCMDTIME_RFC2822); std::string getReadableShortTime(const m_time_t rawtime, bool showUTCDeviation = false); std::string getReadablePeriod(const m_time_t rawtime); m_time_t getTimeStampAfter(m_time_t initial, std::string timestring); m_time_t getTimeStampAfter(std::string timestring); m_time_t getTimeStampBefore(m_time_t initial, std::string timestring); m_time_t getTimeStampBefore(std::string timestring); bool getMinAndMaxTime(std::string timestring, m_time_t *minTime, m_time_t *maxTime); bool getMinAndMaxTime(m_time_t initial, std::string m_timestring, m_time_t *minTime, m_time_t *maxTime); /* Strings related */ bool isRegExp(std::string what); std::string unquote(std::string what); bool megacmdWildcardMatch(const char *pszString, const char *pszMatch); bool patternMatches(const char *what, const char *pattern, bool usepcre); bool nodeNameIsVersion(std::string &nodeName); std::string handleToBase64(const mega::MegaHandle &handle); //node handles std::string syncBackupIdToBase64(const mega::MegaHandle &handle); //sync handles mega::MegaHandle base64ToSyncBackupId(const std::string &shandle); /* Flags and Options */ bool setOptionsAndFlags(std::map *opts, std::map *flags, std::vector *ws, std::set vvalidOptions, bool global = false); bool getMinAndMaxSize(std::string sizestring, int64_t *minSize, int64_t *maxSize); /* Others */ std::string readablePermissions(int permvalue); int permissionsFromReadable(std::string permissions); }//end namespace #endif // MEGACMDUTILS_H MEGAcmd-2.5.2_Linux/src/megacmdversion.h.in000066400000000000000000000035531516543156300204630ustar00rootroot00000000000000#ifndef MEGACMDVERSION_H #define MEGACMDVERSION_H #ifndef SDK_COMMIT_HASH #define SDK_COMMIT_HASH "@SDK_COMMIT_HASH@" #endif #ifndef MEGACMD_MAJOR_VERSION #define MEGACMD_MAJOR_VERSION @MEGACMD_MAJOR_VERSION@ #endif #ifndef MEGACMD_MINOR_VERSION #define MEGACMD_MINOR_VERSION @MEGACMD_MINOR_VERSION@ #endif #ifndef MEGACMD_MICRO_VERSION #define MEGACMD_MICRO_VERSION @MEGACMD_MICRO_VERSION@ #endif #ifndef MEGACMD_BUILD_ID #define MEGACMD_BUILD_ID 0 #endif #ifndef MEGACMD_CODE_VERSION #define MEGACMD_CODE_VERSION (MEGACMD_BUILD_ID+MEGACMD_MICRO_VERSION*100+MEGACMD_MINOR_VERSION*10000+MEGACMD_MAJOR_VERSION*1000000) #endif namespace megacmd { const char * const megacmdchangelog = "Fix: address potential loop in sync engine with mtime changes within 2 seconds""\n" "Shell UX improvements: empty Enter shows a new prompt; no duplicate prompt while command is executing""\n" "Sync engine improvements: prevent re-uploading data when local drive fingerprint has changed""\n" "put: Fixed -c (autocreate) not working when destination is an absolute path""\n" "Fixed syncrhonizing updated empty files (on top of 2.5.0)""\n" "WSL bash completion speedup""\n" "mediainfo: Fixed audio file duration not being displayed""\n" "Transfers: prevent failures on resumption""\n" "thumbnail/preview: Print error when the requested file does not exist""\n" "share: Return success (exit code 0) when no shares exist below the current folder""\n" "passwd: fixes issues when 2FA is enabled""\n" "Path and string handling fixes (trailing separators, trimming)""\n" "Expanded unit test coverage""\n" "Fix: Improved restart-after-update reliability on POSIX (server PID changes after update)""\n" "Stability, memory, typos and other improvements" ; }//end namespace #endif // VERSION_H MEGAcmd-2.5.2_Linux/src/sync_command.cpp000066400000000000000000000363041516543156300200600ustar00rootroot00000000000000/** * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include "sync_command.h" #include "listeners.h" #include "megacmdutils.h" #include "megacmdlogger.h" #include "configurationmanager.h" using std::string; namespace { bool isSyncTransfer(mega::MegaApi& api, int transferTag) { std::unique_ptr transfer(api.getTransferByTag(transferTag)); return transfer && transfer->isSyncTransfer(); } bool hasPendingSyncTransfers(mega::MegaApi& api) { std::unique_ptr transferData(api.getTransferData()); assert(transferData); for (int i = 0; i < transferData->getNumDownloads(); ++i) { const int tag = transferData->getDownloadTag(i); if (isSyncTransfer(api, tag)) { return true; } } for (int i = 0; i < transferData->getNumUploads(); ++i) { const int tag = transferData->getUploadTag(i); if (isSyncTransfer(api, tag)) { return true; } } return false; } string getSyncId(mega::MegaSync& sync) { return syncBackupIdToBase64(sync.getBackupId()); } void getInfoFromFolder(mega::MegaNode& n, mega::MegaApi& api, long long& nFiles, long long& nFolders, long long *nVersions = nullptr) { auto megaCmdListener = std::make_unique(nullptr); api.getFolderInfo(&n, megaCmdListener.get()); megaCmdListener->wait(); if (megaCmdListener->getError()->getErrorCode() != mega::MegaError::API_OK) { LOG_err << "Failed to get folder info for " << n.getName(); return; } mega::MegaFolderInfo* mfi = megaCmdListener->getRequest()->getMegaFolderInfo(); if (!mfi) { return; } nFiles = mfi->getNumFiles(); nFolders = mfi->getNumFolders(); if (nVersions) { *nVersions = mfi->getNumVersions(); } } void printSyncHeader(ColumnDisplayer &cd) { // Add headers with unfixed width (those that can be ellided) cd.addHeader("LOCALPATH", false); cd.addHeader("REMOTEPATH", false); } void printSingleSync(mega::MegaApi& api, mega::MegaSync& sync, mega::MegaNode* node, long long nFiles, long long nFolders, ColumnDisplayer &cd, bool showHandle, int syncIssuesCount) { cd.addValue("ID", getSyncId(sync)); cd.addValue("LOCALPATH", sync.getLocalFolder()); if (showHandle) { cd.addValue("REMOTEHANDLE", string(""))); } cd.addValue("REMOTEPATH", sync.getLastKnownMegaFolder()); cd.addValue("RUN_STATE", syncRunStateStr(sync.getRunState())); std::string folder = sync.getLocalFolder(); string localFolder; mega::LocalPath::path2local(&folder, &localFolder); int statepath = api.syncPathState(&localFolder); cd.addValue("STATUS", getSyncPathStateStr(statepath)); if (sync.getError()) { std::unique_ptr megaSyncErrorCode(sync.getMegaSyncErrorCode()); cd.addValue("ERROR", megaSyncErrorCode.get()); } else { string syncIssuesMsg = "NO"; if (syncIssuesCount > 0) { syncIssuesMsg = "Sync Issues (" + std::to_string(syncIssuesCount) + ")"; } cd.addValue("ERROR", syncIssuesMsg); } cd.addValue("SIZE", sizeToText(node ? api.getSize(node) : 0)); cd.addValue("FILES", std::to_string(nFiles)); cd.addValue("DIRS", std::to_string(nFolders)); } std::pair, std::optional> getErrorsAndSetOutCode(MegaCmdListener& listener) { std::optional errorOpt, syncErrorOpt; int errorCode = listener.getError()->getErrorCode(); bool hasMegaError = errorCode != mega::MegaError::API_OK; auto syncError = static_cast(listener.getRequest()->getNumDetails()); if (hasMegaError) { setCurrentThreadOutCode(errorCode); errorOpt = listener.getError()->getErrorString(); } if (syncError != mega::SyncError::NO_SYNC_ERROR) { if (!hasMegaError) { setCurrentThreadOutCode(MCMD_INVALIDSTATE); } std::unique_ptr syncErrorStr(mega::MegaSync::getMegaSyncErrorCode(syncError)); syncErrorOpt = syncErrorStr.get(); } return {errorOpt, syncErrorOpt}; } } // end namespace namespace SyncCommand { std::unique_ptr getSync(mega::MegaApi& api, const string& pathOrId) { std::unique_ptr sync; sync.reset(api.getSyncByBackupId(base64ToSyncBackupId(pathOrId))); if (!sync) { sync.reset(api.getSyncByPath(pathOrId.c_str())); } return sync; } std::unique_ptr reloadSync(mega::MegaApi& api, std::unique_ptr&& sync) { assert(sync); return std::unique_ptr(api.getSyncByBackupId(sync->getBackupId())); } bool isAnySyncUploadDelayed(mega::MegaApi& api) { const bool isSyncing = api.isSyncing(); if (!isSyncing) { return false; } const bool syncStalled = api.isSyncStalled(); if (syncStalled) { return false; } const bool pendingSyncTransfers = hasPendingSyncTransfers(api); if (pendingSyncTransfers) { return false; } auto listener = std::make_unique(nullptr); api.checkSyncUploadsThrottled(listener.get()); listener->wait(); auto [errorOpt, syncErrorOpt] = getErrorsAndSetOutCode(*listener); if (errorOpt) { LOG_err << "Failed to get the list of delayed sync uploads " << "(Error: " << *errorOpt << (syncErrorOpt ? ". Reason: " + *syncErrorOpt : "") << ")"; return false; } mega::MegaRequest* request = listener->getRequest(); assert(request != nullptr); return request->getFlag(); } void printSync(mega::MegaApi& api, ColumnDisplayer& cd, bool showHandle, mega::MegaSync& sync, const SyncIssueList& syncIssues) { std::unique_ptr node(api.getNodeByHandle(sync.getMegaHandle())); if (!node) { LOG_err << "Remote node not found for sync " << getSyncId(sync); return; } long long nFiles = 0; long long nFolders = 0; getInfoFromFolder(*node, api, nFiles, nFolders); printSyncHeader(cd); unsigned int syncIssuesCount = syncIssues.getSyncIssuesCount(sync); printSingleSync(api, sync, node.get(), nFiles, nFolders, cd, showHandle, syncIssuesCount); } void printSyncList(mega::MegaApi& api, ColumnDisplayer& cd, bool showHandles, const mega::MegaSyncList& syncList, const SyncIssueList& syncIssues) { if (syncList.size() > 0) { printSyncHeader(cd); } for (int i = 0; i < syncList.size(); ++i) { mega::MegaSync& sync = *syncList.get(i); std::unique_ptr node(api.getNodeByHandle(sync.getMegaHandle())); long long nFiles = 0; long long nFolders = 0; if (node) { getInfoFromFolder(*node, api, nFiles, nFolders); } else { LOG_warn << "Remote node not found for sync " << getSyncId(sync); } unsigned int syncIssuesCount = syncIssues.getSyncIssuesCount(sync); printSingleSync(api, sync, node.get(), nFiles, nFolders, cd, showHandles, syncIssuesCount); } } void addSync(mega::MegaApi& api, const fs::path& localPath, mega::MegaNode& node) { std::unique_ptr nodePathPtr(api.getNodePath(&node)); const char* nodePath = (nodePathPtr ? nodePathPtr.get() : ""); if (node.getType() == mega::MegaNode::TYPE_FILE) { setCurrentThreadOutCode(MCMD_NOTPERMITTED); LOG_err << "Remote sync root " << nodePath << " must be a directory"; return; } if (api.getAccess(&node) < mega::MegaShare::ACCESS_FULL) { setCurrentThreadOutCode(MCMD_NOTPERMITTED); LOG_err << "Syncing requires full access to path (current access: " << api.getAccess(&node) << ")"; return; } auto megaCmdListener = std::make_unique(nullptr); api.syncFolder(mega::MegaSync::TYPE_TWOWAY, localPath.string().c_str(), nullptr, node.getHandle(), nullptr, megaCmdListener.get()); megaCmdListener->wait(); auto [errorOpt, syncErrorOpt] = getErrorsAndSetOutCode(*megaCmdListener); if (errorOpt) { LOG_err << "Failed to sync " << localPath.string() << " to " << nodePath << " (Error: " << *errorOpt << (syncErrorOpt ? ". Reason: " + *syncErrorOpt : "") << ")"; return; } string syncLocalPath = megaCmdListener->getRequest()->getFile(); OUTSTREAM << "Added sync: " << syncLocalPath << " to " << nodePath << endl; if (syncErrorOpt) { LOG_err << "Sync added as temporarily disabled. Reason: " << *syncErrorOpt; } } void modifySync(mega::MegaApi& api, mega::MegaSync& sync, ModifyOpts opts) { auto megaCmdListener = std::make_unique(nullptr); if (opts == ModifyOpts::Delete) { api.removeSync(sync.getBackupId(), megaCmdListener.get()); megaCmdListener->wait(); auto [errorOpt, syncErrorOpt] = getErrorsAndSetOutCode(*megaCmdListener); if (!errorOpt) { OUTSTREAM << "Sync removed: " << sync.getLocalFolder() << " to " << sync.getLastKnownMegaFolder() << endl; } else { LOG_err << "Failed to remove sync " << getSyncId(sync) << " (Error: " << *errorOpt << (syncErrorOpt ? ". Reason: " + *syncErrorOpt : "") << ")"; } } else { auto newState = (opts == ModifyOpts::Pause ? mega::MegaSync::RUNSTATE_SUSPENDED : mega::MegaSync::RUNSTATE_RUNNING); api.setSyncRunState(sync.getBackupId(), newState, megaCmdListener.get()); megaCmdListener->wait(); auto [errorOpt, syncErrorOpt] = getErrorsAndSetOutCode(*megaCmdListener); if (!errorOpt) { const char* action = (opts == ModifyOpts::Pause ? "paused" : "enabled"); OUTSTREAM << "Sync " << action << ": " << sync.getLocalFolder() << " to " << sync.getLastKnownMegaFolder() << std::endl; if (syncErrorOpt) { LOG_err << "Sync might be temporarily disabled. Reason: " << *syncErrorOpt; } } else { const char* action = (opts == ModifyOpts::Pause ? "pause" : "enable"); LOG_err << "Failed to " << action << " sync " << getSyncId(sync) << " (Error: " << *errorOpt << (syncErrorOpt ? ". Reason: " + *syncErrorOpt : "") << ")"; } } } } // end namespace namespace GlobalSyncConfig { void loadFromConfigurationManager(mega::MegaApi& api) { const int duWaitSecs = ConfigurationManager::getConfigurationValue("GlobalSyncConfig:delayedUploadsWaitSecs", -1); if (duWaitSecs != -1) { DelayedUploads::updateWaitSecs(api, duWaitSecs); } const int duMaxAttempts = ConfigurationManager::getConfigurationValue("GlobalSyncConfig:delayedUploadsMaxAttempts", -1); if (duMaxAttempts != -1) { DelayedUploads::updateMaxAttempts(api, duMaxAttempts); } } namespace DelayedUploads { namespace { template std::optional makeApiRequest(RequestFunc&& requestFunc) { auto listener = std::make_unique(nullptr); requestFunc(listener.get()); listener->wait(); mega::MegaError* error = listener->getError(); assert(error); if (error->getErrorCode() != mega::MegaError::API_OK) { setCurrentThreadOutCode(error->getErrorCode()); return std::nullopt; } mega::MegaRequest* request = listener->getRequest(); assert(request); Config config; config.mWaitSecs = request->getNumber(); config.mMaxAttempts = request->getTotalBytes(); return config; } std::optional getCurrentLowerLimits(mega::MegaApi& api) { return makeApiRequest([&api] (mega::MegaRequestListener* listener) { api.getSyncUploadThrottleLowerLimits(listener); }); } std::optional getCurrentUpperLimits(mega::MegaApi& api) { return makeApiRequest([&api] (mega::MegaRequestListener* listener) { api.getSyncUploadThrottleUpperLimits(listener); }); } } // end namespace std::optional getCurrentConfig(mega::MegaApi& api) { return makeApiRequest([&api] (mega::MegaRequestListener* listener) { api.getSyncUploadThrottleValues(listener); }); } bool Limits::isWaitSecsValid(int waitSecs) const { return waitSecs >= mLower.mWaitSecs && waitSecs <= mUpper.mWaitSecs; } bool Limits::isMaxAttemptsValid(int maxAttempts) const { return maxAttempts >= mLower.mMaxAttempts && maxAttempts <= mUpper.mMaxAttempts; } std::optional getCurrentLimits(mega::MegaApi& api) { auto lowerLimitsOpt = getCurrentLowerLimits(api); if (!lowerLimitsOpt) { LOG_warn << "Failed to get the delayed uploads lower limits"; return std::nullopt; } auto upperLimitsOpt = getCurrentUpperLimits(api); if (!upperLimitsOpt) { LOG_warn << "Failed to get the delayed uploads upper limits"; return std::nullopt; } return Limits{*lowerLimitsOpt, *upperLimitsOpt}; } void updateWaitSecs(mega::MegaApi& api, int waitSecs) { auto limits = getCurrentLimits(api); if (limits && !limits->isWaitSecsValid(waitSecs)) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Delayed sync uploads wait time must be between " << limits->mLower.mWaitSecs << " and " << limits->mUpper.mWaitSecs << " seconds"; return; } auto result = makeApiRequest([&api, waitSecs] (mega::MegaRequestListener* listener) { api.setSyncUploadThrottleUpdateRate(waitSecs, listener); }); if (!result) { LOG_err << "Failed to set the wait time for delayed sync uploads"; return; } OUTSTREAM << "Successfully set the wait time for delayed sync uploads to " << result->mWaitSecs << " seconds" << endl; ConfigurationManager::savePropertyValue("GlobalSyncConfig:delayedUploadsWaitSecs", result->mWaitSecs); } void updateMaxAttempts(mega::MegaApi& api, int maxAttempts) { auto limits = getCurrentLimits(api); if (limits && !limits->isMaxAttemptsValid(maxAttempts)) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "Max delayed uploads attempts must be between " << limits->mLower.mMaxAttempts << " and " << limits->mUpper.mMaxAttempts; return; } auto result = makeApiRequest([&api, maxAttempts] (mega::MegaRequestListener* listener) { api.setSyncMaxUploadsBeforeThrottle(maxAttempts, listener); }); if (!result) { LOG_err << "Failed to set the max attempts for delayed sync uploads"; return; } OUTSTREAM << "Successfully set the max attempts for delayed sync uploads to " << result->mMaxAttempts << endl; ConfigurationManager::savePropertyValue("GlobalSyncConfig:delayedUploadsMaxAttempts", result->mMaxAttempts); } } // end namespace } // end namespace MEGAcmd-2.5.2_Linux/src/sync_command.h000066400000000000000000000040351516543156300175210ustar00rootroot00000000000000/** * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #pragma once #include #include "megacmdcommonutils.h" #include "sync_issues.h" using namespace megacmd; namespace SyncCommand { std::unique_ptr getSync(mega::MegaApi& api, const std::string& pathOrId); std::unique_ptr reloadSync(mega::MegaApi& api, std::unique_ptr&& sync); bool isAnySyncUploadDelayed(mega::MegaApi& api); void printSync(mega::MegaApi& api, ColumnDisplayer& cd, bool showHandle, mega::MegaSync& sync, const SyncIssueList& syncIssues); void printSyncList(mega::MegaApi& api, ColumnDisplayer& cd, bool showHandles, const mega::MegaSyncList& syncList, const SyncIssueList& syncIssues); void addSync(mega::MegaApi& api, const fs::path& localPath, mega::MegaNode& node); enum class ModifyOpts { Pause, Enable, Delete }; void modifySync(mega::MegaApi& api, mega::MegaSync& sync, ModifyOpts opts); } namespace GlobalSyncConfig { void loadFromConfigurationManager(mega::MegaApi& api); namespace DelayedUploads { struct Config { int mWaitSecs = 0; int mMaxAttempts = 0; }; std::optional getCurrentConfig(mega::MegaApi& api); struct Limits { Config mLower; Config mUpper; bool isWaitSecsValid(int waitSecs) const; bool isMaxAttemptsValid(int maxAttempts) const; }; std::optional getCurrentLimits(mega::MegaApi& api); void updateWaitSecs(mega::MegaApi& api, int waitSecs); void updateMaxAttempts(mega::MegaApi& api, int waitSecs); } } MEGAcmd-2.5.2_Linux/src/sync_ignore.cpp000066400000000000000000000203761516543156300177270ustar00rootroot00000000000000/** * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include "sync_ignore.h" #include #include #include #include "megacmdcommonutils.h" #include "megacmdlogger.h" using namespace megacmd; namespace { const char* BOMStr = "\xEF\xBB\xBF"; #ifdef _WIN32 const char* NL = "\r\n"; #else const char* NL = "\n"; #endif void trimSpaces(std::string& str) { auto end = std::find_if_not(str.rbegin(), str.rend(), [] (char c) { return std::isspace(static_cast(c)) != 0; }).base(); str.erase(end, str.end()); } std::unique_lock getFileLock(const fs::path& path) { static std::mutex mapMutex; static std::map fileMutexMap; std::lock_guard mapLock(mapMutex); return std::unique_lock(fileMutexMap[path]); } void executeAdd(MegaIgnoreFile& megaIgnoreFile, const std::set& filters) { // Only check the format of the filters when adding, to allow users to remove invalid filters from the file for (const std::string& filter : filters) { if (MegaIgnoreFile::isValidFilter(filter)) { continue; } // At least one incorrect filter format must cause the entire command to fail // Otherwise, if we allowed partial filters to come through, it could result in unwanted sync states setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "The filter \"" << filter << "\" does not have valid format"; return; } std::set toAdd; for (const std::string& filter : filters) { if (megaIgnoreFile.containsFilter(filter)) { OUTSTREAM << "Cannot add filter \"" << filter << "\" because it's already in the .megaignore file (skipped)" << endl; continue; } toAdd.insert(filter); OUTSTREAM << "Added filter \"" << filter << "\"" << endl; } megaIgnoreFile.addFilters(toAdd); } void executeRemove(MegaIgnoreFile& megaIgnoreFile, const std::set& filters) { std::set toRemove; for (const std::string& filter : filters) { if (!megaIgnoreFile.containsFilter(filter)) { OUTSTREAM << "Cannot remove filter \"" << filter << "\" because it's not in the .megaignore file (skipped)" << endl; continue; } toRemove.insert(filter); OUTSTREAM << "Removed filter \"" << filter << "\"" << endl; } megaIgnoreFile.removeFilters(toRemove); } void executeSyncIgnoreCommand(const SyncIgnore::Args& args, MegaIgnoreFile& megaIgnoreFile) { switch (args.mAction) { case SyncIgnore::Action::Show: { OUTSTREAM << megaIgnoreFile.getFilterContents(); break; } case SyncIgnore::Action::Add: { executeAdd(megaIgnoreFile, args.mFilters); break; } case SyncIgnore::Action::Remove: { executeRemove(megaIgnoreFile, args.mFilters); break; } } } } // end namespace namespace SyncIgnore { void executeCommand(const Args& args) { if (args.mFilters.empty() && (args.mAction == Action::Add || args.mAction == Action::Remove)) { setCurrentThreadOutCode(MCMD_EARGS); LOG_err << "At least one name filter is required for add or remove"; LOG_err << " " << getUsageStr("sync-ignore"); return; } fs::path megaIgnoreFilePath; bool isDefault = true; if (args.mMegaIgnoreDirPath.empty()) { megaIgnoreFilePath = MegaIgnoreFile::getDefaultPath(); } else { megaIgnoreFilePath = args.mMegaIgnoreDirPath / ".megaignore"; isDefault = false; } std::error_code ec; if (!fs::exists(megaIgnoreFilePath, ec)) { setCurrentThreadOutCode(MCMD_NOTFOUND); if (isDefault) { // For the default file to be created, a sync that DOES NOT already contain a .megaignore file has to be added LOG_err << "Default .megaignore file does not exist (will be created when a new sync is added)" << ( ec ? std::string(": ").append(errorCodeStr(ec).c_str()) : ""); } else { LOG_err << "Mega ignore file \"" << megaIgnoreFilePath << "\" does not exist" << ( ec ? std::string(": ").append(errorCodeStr(ec).c_str()) : ""); } return; } MegaIgnoreFile megaIgnoreFile(megaIgnoreFilePath); if (!megaIgnoreFile.isValid()) { setCurrentThreadOutCode(MCMD_INVALIDSTATE); LOG_err << "There was an error opening mega ignore file \"" << megaIgnoreFilePath; return; } executeSyncIgnoreCommand(args, megaIgnoreFile); } std::string getFilterFromLegacyPattern(const std::string& pattern) { return "-:" + pattern; } } // end namespace bool MegaIgnoreFile::checkBOMAndSkip(std::ifstream& file) { char bom[4]; file.read(bom, 3); bom[3] = '\0'; if (strcmp(bom, BOMStr)) { // If there's no utf-8 BOM, seek to the begining of the file file.seekg(0); return false; } return true; } void MegaIgnoreFile::loadFilters(std::ifstream& file) { mFilters.clear(); for (std::string line; getline(file, line);) { trimSpaces(line); if (line.empty() || line[0] == '#') { continue; } mFilters.insert(line); } } fs::path MegaIgnoreFile::getDefaultPath() { auto platformDirs = megacmd::PlatformDirectories::getPlatformSpecificDirectories(); return platformDirs->configDirPath() / ".megaignore.default"; } bool MegaIgnoreFile::isValidFilter(const std::string& filter) { thread_local std::regex filterRegex(R"([-+][adfs]?[Nnp]?[GgRr]?:\S+)"); return std::regex_match(filter, filterRegex); } MegaIgnoreFile::MegaIgnoreFile(const fs::path& path) : mPath(path), mValid(false) { auto fileLock = getFileLock(mPath); std::error_code ec; if (fs::exists(mPath, ec)) { loadFromPath(); } else if (ec) { LOG_err << "MegaIgnoreFile existence check failure: " << mPath << ": " << errorCodeStr(ec); return; } else { createWithBOM(); } } void MegaIgnoreFile::loadFromPath() { mValid = false; std::ifstream file(mPath); if (!file.is_open() || file.fail()) { return; } checkBOMAndSkip(file); loadFilters(file); mValid = true; } void MegaIgnoreFile::createWithBOM() { std::ofstream file(mPath); if (!file.is_open() || file.fail()) { return; } file << BOMStr; mValid = true; } void MegaIgnoreFile::addFilters(const std::set& filters) { auto fileLock = getFileLock(mPath); { std::ofstream file(mPath, std::ios_base::app); for (const std::string& filter : filters) { file << filter << NL; } } loadFromPath(); } void MegaIgnoreFile::removeFilters(const std::set& filters) { auto fileLock = getFileLock(mPath); std::string newContents; bool hasBOM = false; { std::ifstream file(mPath); hasBOM = checkBOMAndSkip(file); for (std::string line; getline(file, line);) { trimSpaces(line); if (filters.count(line)) { continue; } newContents += line + NL; } } { std::ofstream outFile(mPath, std::ios_base::trunc); outFile << (hasBOM ? BOMStr : "") << newContents; } loadFromPath(); } bool MegaIgnoreFile::containsFilter(const std::string& filter) const { assert(mValid); std::string cleanFilter = filter; trimSpaces(cleanFilter); return mFilters.count(cleanFilter) > 0; } std::string MegaIgnoreFile::getFilterContents() const { assert(mValid); std::string contents; for (const std::string& filter : mFilters) { contents += filter + '\n'; } return contents; } MEGAcmd-2.5.2_Linux/src/sync_ignore.h000066400000000000000000000030021516543156300173570ustar00rootroot00000000000000/** * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #pragma once #include "megacmdcommonutils.h" #include #include namespace SyncIgnore { enum class Action { Show, Add, Remove }; struct Args { Action mAction; fs::path mMegaIgnoreDirPath; std::set mFilters; }; void executeCommand(const Args& args); std::string getFilterFromLegacyPattern(const std::string& pattern); } class MegaIgnoreFile { std::set mFilters; fs::path mPath; bool mValid; bool checkBOMAndSkip(std::ifstream& file); void loadFilters(std::ifstream& file); void loadFromPath(); void createWithBOM(); public: static fs::path getDefaultPath(); static bool isValidFilter(const std::string& filter); MegaIgnoreFile(const fs::path& path); bool isValid() const { return mValid; } void addFilters(const std::set& filters); void removeFilters(const std::set& filters); bool containsFilter(const std::string& filter) const; std::string getFilterContents() const; // without comments, bom, etc. }; MEGAcmd-2.5.2_Linux/src/sync_issues.cpp000066400000000000000000000623061516543156300177560ustar00rootroot00000000000000/** * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include "sync_issues.h" #include #include #include "megacmdlogger.h" #include "configurationmanager.h" #include "listeners.h" #include "megacmdutils.h" #ifdef MEGACMD_TESTING_CODE #include "../tests/common/Instruments.h" using TI = TestInstruments; #endif using namespace megacmd; using namespace std::string_literals; namespace { bool startsWith(const char* str, const char* prefix) { if (!str || !prefix) { return false; } return std::strncmp(str, prefix, std::strlen(prefix)) == 0; } std::string getPathFileName(const std::string& path) { auto pos = std::find_if(path.rbegin(), path.rend(), [] (char c) { return c == '/' || c == '\\'; }); if (pos == path.rend()) { return path; } return std::string(pos.base(), path.end()); } } class SyncIssuesGlobalListener : public mega::MegaGlobalListener { using SyncStalledChangedCb = std::function; SyncStalledChangedCb mSyncStalledChangedCb; bool mSyncStalled; void onGlobalSyncStateChanged(mega::MegaApi* api) override { const bool syncStalled = api->isSyncStalled(); const bool syncStalledChanged = (syncStalled != mSyncStalled || (syncStalled && api->isSyncStalledChanged())); if (!syncStalledChanged) { return; } mSyncStalledChangedCb(); mSyncStalled = syncStalled; } public: template SyncIssuesGlobalListener(SyncStalledChangedCb&& syncStalledChangedCb) : mSyncStalledChangedCb(std::move(syncStalledChangedCb)), mSyncStalled(false) {} }; class SyncIssuesRequestListener : public mega::SynchronousRequestListener { std::mutex mSyncIssuesMtx; SyncIssueList mSyncIssues; void doOnRequestFinish(mega::MegaApi *api, mega::MegaRequest *request, mega::MegaError *e) override { assert(e); if (e->getValue() != mega::MegaError::API_OK) { LOG_err << "Failed to get list of sync issues: " << e->getErrorString(); return; } assert(request && request->getType() == mega::MegaRequest::TYPE_GET_SYNC_STALL_LIST); auto stalls = request->getMegaSyncStallList(); if (!stalls) { LOG_err << "Sync issue list pointer is null"; return; } SyncIssueList syncIssues; for (size_t i = 0; i < stalls->size(); ++i) { auto stall = stalls->get(i); assert(stall); SyncIssue syncIssue(*stall); syncIssues.mIssuesMap.emplace(syncIssue.getId(), std::move(syncIssue)); } onSyncIssuesChanged(syncIssues); { std::lock_guard lock(mSyncIssuesMtx); mSyncIssues = std::move(syncIssues); } } protected: virtual void onSyncIssuesChanged(const SyncIssueList& syncIssues) {} public: virtual ~SyncIssuesRequestListener() = default; SyncIssueList releaseSyncIssues() { std::lock_guard lock(mSyncIssuesMtx); return std::move(mSyncIssues); } }; // Whenever a callback happens we start a timer. Any subsequent callbacks after the first // timer restart it. When the timer reaches a certain threshold, the broadcast is triggered. // This effectivelly "combines" near callbacks into a single trigger. class SyncIssuesBroadcastListener : public SyncIssuesRequestListener { using SyncStalledChangedCb = std::function; using Clock = std::chrono::high_resolution_clock; using TimePoint = Clock::time_point; SyncStalledChangedCb mBroadcastSyncIssuesCb; unsigned int mSyncIssuesSize; bool mRunning; TimePoint mLastTriggerTime; std::mutex mDebouncerMtx; std::condition_variable mDebouncerCv; std::thread mDebouncerThread; void onSyncIssuesChanged(const SyncIssueList& syncIssues) override { { std::lock_guard lock(mDebouncerMtx); mSyncIssuesSize = syncIssues.size(); mLastTriggerTime = Clock::now(); } mDebouncerCv.notify_one(); } void debouncerLoop() { while (true) { std::unique_lock lock(mDebouncerMtx); bool stopOrTrigger = mDebouncerCv.wait_for(lock, std::chrono::milliseconds(20), [this] { constexpr auto minTriggerTime = std::chrono::milliseconds(300); return !mRunning || Clock::now() - mLastTriggerTime > minTriggerTime; }); if (!mRunning) // stop { return; } if (stopOrTrigger) // trigger { mBroadcastSyncIssuesCb(mSyncIssuesSize); mLastTriggerTime = TimePoint::max(); } } } public: template SyncIssuesBroadcastListener(BroadcastSyncIssuesCb&& broadcastSyncIssuesCb) : mBroadcastSyncIssuesCb(std::move(broadcastSyncIssuesCb)), mSyncIssuesSize(0), mRunning(true), mLastTriggerTime(TimePoint::max()), mDebouncerThread([this] { debouncerLoop(); }) {} virtual ~SyncIssuesBroadcastListener() { { std::lock_guard lock(mDebouncerMtx); mRunning = false; } mDebouncerCv.notify_one(); mDebouncerThread.join(); } }; template std::string SyncIssue::getFilePath(int i) const { assert(mMegaStall); const char* path = mMegaStall->path(isCloud, i); if (path && strcmp(path, "")) { if constexpr (isCloud) { return CloudPrefix + path; } else { return path; } } return ""; } template std::string SyncIssue::getDefaultFileName() const { std::string filePath = getFilePath(0); if (filePath.empty()) { filePath = getFilePath(0); } return getPathFileName(filePath); } template std::string SyncIssue::getFileName(int i) const { std::string filePath = getFilePath(i); return getPathFileName(filePath); } template std::optional SyncIssue::getPathProblem(mega::PathProblem pathProblem) const { assert(mMegaStall); #ifdef MEGACMD_TESTING_CODE auto pathProblemOpt = TI::Instance().testValue(TI::TestValue::SYNC_ISSUE_ENFORCE_PATH_PROBLEM); if (pathProblemOpt) { auto pathProblemEnum = static_cast(std::get(*pathProblemOpt)); return (pathProblemEnum == pathProblem) ? std::optional(0) /*first index*/ : std::nullopt; } #endif for (int i = 0; i < mMegaStall->pathCount(isCloud); ++i) { if (mMegaStall->pathProblem(isCloud, i) == static_cast(pathProblem)) { return i; } } return {}; } std::string_view SyncIssue::PathProblem::getProblemStr() const { switch(mProblem) { #define SOME_GENERATOR_MACRO(pathProblem, str) case pathProblem: return str; GENERATE_FROM_PATH_PROBLEM(SOME_GENERATOR_MACRO) #undef SOME_GENERATOR_MACRO default: assert(false); return ""; } } SyncIssue::SyncIssue(const mega::MegaSyncStall &stall) : mMegaStall(stall.copy()) { } std::string SyncIssue::getId() const { assert(mMegaStall); const size_t hash = mMegaStall->getHash(); std::string id(11, '\0'); id.resize(mega::Base64::btoa(reinterpret_cast(&hash), sizeof(hash), reinterpret_cast(id.data()))); return id; } SyncInfo SyncIssue::getSyncInfo(mega::MegaSync const* parentSync) const { assert(mMegaStall); SyncInfo info; info.mReasonType = static_cast(mMegaStall->reason()); #ifdef MEGACMD_TESTING_CODE auto reasonTypeOpt = TI::Instance().testValue(TI::TestValue::SYNC_ISSUE_ENFORCE_REASON_TYPE); if (reasonTypeOpt) { info.mReasonType = static_cast(std::get(*reasonTypeOpt)); } #endif switch(info.mReasonType) { case mega::SyncWaitReason::NoReason: { info.mReason = "Error detected with '" + getDefaultFileName() + "'"; info.mDescription = "Reason not found"; break; } case mega::SyncWaitReason::FileIssue: { constexpr bool cloud = false; if (auto i = getPathProblem(mega::PathProblem::DetectedSymlink); i) { info.mReason = "Detected symlink: '" + getFileName(*i) + "'"; } else if (auto i = getPathProblem(mega::PathProblem::DetectedHardLink); i) { info.mReason = "Detected hard link: '" + getFileName(*i) + "'"; } else if (auto i = getPathProblem(mega::PathProblem::DetectedSpecialFile); i) { info.mReason = "Detected special link: '" + getFileName(*i) + "'"; } else { info.mReason = "Can't sync '" + getDefaultFileName() + "'"; } break; } case mega::SyncWaitReason::MoveOrRenameCannotOccur: { std::string syncName = (parentSync ? "'"s + parentSync->getName() + "'" : "the sync"); info.mReason = "Can't move or rename some items in " + syncName; info.mDescription = "The local and remote locations have changed at the same time"; break; } case mega::SyncWaitReason::DeleteOrMoveWaitingOnScanning: { info.mReason = "Can't find '" + getDefaultFileName() + "'"; info.mDescription = "Waiting to finish scan to see if the file was moved or deleted"; break; } case mega::SyncWaitReason::DeleteWaitingOnMoves: { info.mReason = "Waiting to move '" + getDefaultFileName() + "'"; info.mDescription = "Waiting for other processes to complete"; break; } case mega::SyncWaitReason::UploadIssue: { info.mReason = "Can't upload '" + getDefaultFileName() + "' to the selected location"; info.mDescription = "Cannot reach the destination folder"; break; } case mega::SyncWaitReason::DownloadIssue: { constexpr bool cloud = true; std::string fileName = getDefaultFileName(); if (getPathProblem(mega::PathProblem::CloudNodeInvalidFingerprint)) { info.mDescription = "File fingerprint missing"; } else if (auto i = getPathProblem(mega::PathProblem::CloudNodeIsBlocked); i) { fileName = getFileName(*i); info.mDescription = "The file '" + fileName + "' is unavailable because it was reported to contain content in breach of MEGA's Terms of Service (https://mega.io/terms)"; } else { info.mDescription = "A failure occurred either downloading the file, or moving the downloaded temporary file to its final name and location"; } info.mReason = "Can't download '" + fileName + "' to the selected location"; break; } case mega::SyncWaitReason::CannotCreateFolder: { info.mReason = "Cannot create '" + getDefaultFileName() + "'"; info.mDescription = "Filesystem error preventing folder access"; break; } case mega::SyncWaitReason::CannotPerformDeletion: { info.mReason = "Cannot perform deletion '" + getDefaultFileName() + "'"; info.mDescription = "Filesystem error preventing folder access"; break; } case mega::SyncWaitReason::SyncItemExceedsSupportedTreeDepth: { info.mReason = "Unable to sync '" + getDefaultFileName() + "'"; info.mDescription = "Target is too deep on your folder structure; please move it to a location that is less than " + std::to_string(mega::Sync::MAX_CLOUD_DEPTH) + " folders deep"; break; } case mega::SyncWaitReason::FolderMatchedAgainstFile: { info.mReason = "Unable to sync '" + getDefaultFileName() + "'"; info.mDescription = "Cannot sync folders against files"; break; } case mega::SyncWaitReason::LocalAndRemoteChangedSinceLastSyncedState_userMustChoose: case mega::SyncWaitReason::LocalAndRemotePreviouslyUnsyncedDiffer_userMustChoose: { info.mReason = "Unable to sync '" + getDefaultFileName() + "'"; info.mDescription = "This file has conflicting copies"; break; } case mega::SyncWaitReason::NamesWouldClashWhenSynced: { info.mReason = "Name conflicts: '" + getDefaultFileName() + "'"; info.mDescription = "These items contain multiple names on one side that would all become the same single name on the other side (this may be due to syncing to case insensitive local filesystems, or the effects of escaped characters)"; break; } default: { assert(false); info.mReason = ""; break; } } return info; } std::unique_ptr SyncIssue::getParentSync(mega::MegaApi& api) const { auto syncList = std::unique_ptr(api.getSyncs()); assert(syncList); for (int i = 0; i < syncList->size(); ++i) { mega::MegaSync* sync = syncList->get(i); assert(sync); if (belongsToSync(*sync)) { return std::unique_ptr(sync->copy()); } } // This is a valid result that must be taken into account // E.g., the sync was removed by another client while this issue is being handled return nullptr; } std::vector SyncIssue::getPathProblems(mega::MegaApi& api) const { return getPathProblems(api) + getPathProblems(api); } template std::vector SyncIssue::getPathProblems(mega::MegaApi& api) const { std::vector pathProblems; for (int i = 0; i < mMegaStall->pathCount(isCloud); ++i) { PathProblem pathProblem; pathProblem.mIsCloud = isCloud; pathProblem.mPath = (isCloud ? CloudPrefix : "") + mMegaStall->path(isCloud, i); pathProblem.mProblem = static_cast(std::max(0, mMegaStall->pathProblem(isCloud, i))); #ifdef MEGACMD_TESTING_CODE auto pathProblemOpt = TI::Instance().testValue(TI::TestValue::SYNC_ISSUE_ENFORCE_PATH_PROBLEM); if (pathProblemOpt) { pathProblem.mProblem = static_cast(std::get(*pathProblemOpt)); } #endif if constexpr (isCloud) { auto nodeHandle = mMegaStall->cloudNodeHandle(i); std::unique_ptr n(api.getNodeByHandle(nodeHandle)); if (n) { if (n->isFile()) { pathProblem.mPathType = "File"; pathProblem.mFileSize = n->getSize(); } else if (n->isFolder()) { pathProblem.mPathType = "Folder"; } pathProblem.mUploadedTime = n->getCreationTime(); pathProblem.mModifiedTime = n->getModificationTime(); } else { LOG_warn << "Path problem " << pathProblem.mPath << " does not have a valid node associated with it"; } } else if (std::error_code ec; fs::exists(pathProblem.mPath, ec) && !ec) { if (fs::is_directory(pathProblem.mPath, ec) && !ec) { pathProblem.mPathType = "Folder"; } else if (fs::is_regular_file(pathProblem.mPath, ec) && !ec) { pathProblem.mPathType = "File"; } else if (fs::is_symlink(pathProblem.mPath, ec) && !ec) { pathProblem.mPathType = "Symlink"; } auto lastWriteTime = fs::last_write_time(pathProblem.mPath, ec); if (!ec) { auto systemNow = std::chrono::system_clock::now(); auto fileTimeNow = fs::file_time_type::clock::now(); auto lastWriteTimePoint = std::chrono::time_point_cast(lastWriteTime - fileTimeNow + systemNow); pathProblem.mModifiedTime = lastWriteTimePoint.time_since_epoch().count(); } if (auto fileSize = fs::file_size(pathProblem.mPath, ec); !ec) { pathProblem.mFileSize = fileSize; } } pathProblems.emplace_back(std::move(pathProblem)); } return pathProblems; } bool SyncIssue::belongsToSync(const mega::MegaSync& sync) const { const char* issueCloudPath = mMegaStall->path(true, 0); const char* syncCloudPath = sync.getLastKnownMegaFolder(); assert(syncCloudPath); if (startsWith(issueCloudPath, syncCloudPath)) { return true; } const char* issueLocalPath = mMegaStall->path(false, 0); const char* syncLocalPath = sync.getLocalFolder(); assert(syncLocalPath); if (startsWith(issueLocalPath, syncLocalPath)) { return true; } return false; } SyncIssue const* SyncIssueList::getSyncIssue(const std::string& id) const { auto it = mIssuesMap.find(id); if (it == mIssuesMap.end()) { return nullptr; } return &it->second; } unsigned int SyncIssueList::getSyncIssuesCount(const mega::MegaSync& sync) const { unsigned int count = 0; for (const auto& syncIssue : *this) { if (syncIssue.second.belongsToSync(sync)) { ++count; } } return count; } void SyncIssuesManager::onSyncIssuesChanged(unsigned int newSyncIssuesSize) { std::lock_guard lock(mWarningMtx); if (mWarningEnabled && newSyncIssuesSize > 0) { std::string message = "Sync issues detected: your syncs have encountered conflicts that may require your intervention.\n"s + "Use the \"%mega-%sync-issues\" command to display them.\n" + "This message can be disabled with \"%mega-%sync-issues --disable-warning\"."; broadcastMessage(message, true); } #ifdef MEGACMD_TESTING_CODE TI::Instance().setTestValue(TI::TestValue::SYNC_ISSUES_LIST_SIZE, static_cast(newSyncIssuesSize)); TI::Instance().fireEvent(TI::Event::SYNC_ISSUES_LIST_UPDATED); #endif } SyncIssuesManager::SyncIssuesManager(mega::MegaApi *api) : mApi(*api) { // The global listener will be triggered whenever there's a change in the sync state // It'll request the sync issue list from the API (if the stalled state changed) // This will be used to notify the user if they have sync issues mGlobalListener = std::make_unique( [this, api] { api->getMegaSyncStallList(mRequestListener.get()); }); // The broadcast listener will be triggered whenever the api call above finishes // getting the list of stalls; it'll be used to notify the user (and the integration tests) mRequestListener = std::make_unique( [this] (unsigned int newSyncIssuesSize) { onSyncIssuesChanged(newSyncIssuesSize); }); mWarningEnabled = ConfigurationManager::getConfigurationValue("stalled_issues_warning", true); } SyncIssueList SyncIssuesManager::getSyncIssues() const { auto listener = std::make_unique(); mApi.getMegaSyncStallList(listener.get()); listener->wait(); return listener->releaseSyncIssues(); } void SyncIssuesManager::disableWarning() { std::lock_guard lock(mWarningMtx); if (!mWarningEnabled) { OUTSTREAM << "Note: warning is already disabled" << endl; } mWarningEnabled = false; ConfigurationManager::savePropertyValue("stalled_issues_warning", mWarningEnabled); } void SyncIssuesManager::enableWarning() { std::lock_guard lock(mWarningMtx); if (mWarningEnabled) { OUTSTREAM << "Note: warning is already enabled" << endl; } mWarningEnabled = true; ConfigurationManager::savePropertyValue("stalled_issues_warning", mWarningEnabled); } namespace SyncIssuesCommand { void printAllIssues(mega::MegaApi& api, ColumnDisplayer& cd, const SyncIssueList& syncIssues, bool disablePathCollapse, int rowCountLimit) { cd.addHeader("PARENT_SYNC", disablePathCollapse); syncIssues.forEach([&api, &cd] (const SyncIssue& syncIssue) { auto parentSync = syncIssue.getParentSync(api); cd.addValue("ISSUE_ID", syncIssue.getId()); cd.addValue("PARENT_SYNC", parentSync ? parentSync->getName() : ""); cd.addValue("REASON", syncIssue.getSyncInfo(parentSync.get()).mReason); }, rowCountLimit); OUTSTREAM << cd.str(); OUTSTREAM << endl; if (rowCountLimit < syncIssues.size()) { OUTSTREAM << "Note: showing " << rowCountLimit << " out of " << syncIssues.size() << " issues. " << "Use \"" << getCommandPrefixBasedOnMode() << "sync-issues --limit=0\" to see all of them." << endl; } OUTSTREAM << "Use \"" << getCommandPrefixBasedOnMode() << "sync-issues --detail \" to get further details on a specific issue." << endl; } void printSingleIssueDetail(mega::MegaApi& api, megacmd::ColumnDisplayer& cd, const SyncIssue& syncIssue, bool disablePathCollapse, int rowCountLimit) { auto parentSync = syncIssue.getParentSync(api); auto syncInfo = syncIssue.getSyncInfo(parentSync.get()); OUTSTREAM << syncInfo.mReason; if (!syncInfo.mDescription.empty()) { OUTSTREAM << ". " << syncInfo.mDescription << "."; } OUTSTREAM << endl; OUTSTREAM << "Parent sync: "; if (parentSync) { OUTSTREAM << syncBackupIdToBase64(parentSync->getBackupId()) << " (" << parentSync->getLocalFolder() << " to " << SyncIssue::CloudPrefix << parentSync->getLastKnownMegaFolder() << ")"; } else { OUTSTREAM << ""; } OUTSTREAM << endl << endl; cd.addHeader("PATH", disablePathCollapse); mega::SyncWaitReason syncIssueReason = syncIssue.getSyncInfo(parentSync.get()).mReasonType; auto pathProblems = syncIssue.getPathProblems(api); for (int i = 0; i < pathProblems.size() && i < rowCountLimit; ++i) { const auto& pathProblem = pathProblems[i]; const char* timeFmt = "%Y-%m-%d %H:%M:%S"; cd.addValue("PATH", pathProblem.mPath); if (syncIssueReason == mega::SyncWaitReason::FolderMatchedAgainstFile) { cd.addValue("TYPE", pathProblem.mPathType); } else { cd.addValue("PATH_ISSUE", std::string(pathProblem.getProblemStr())); } cd.addValue("LAST_MODIFIED", pathProblem.mModifiedTime ? getReadableTime(pathProblem.mModifiedTime, timeFmt) : "-"); cd.addValue("UPLOADED", pathProblem.mUploadedTime ? getReadableTime(pathProblem.mUploadedTime, timeFmt) : "-"); cd.addValue("SIZE", pathProblem.mFileSize >= 0 ? sizeToText(pathProblem.mFileSize) : "-"); } OUTSTREAM << cd.str(); if (rowCountLimit < pathProblems.size()) { OUTSTREAM << endl; OUTSTREAM << "Note: showing " << rowCountLimit << " out of " << static_cast(pathProblems.size()) << " path problems. " << "Use \"" << getCommandPrefixBasedOnMode() << "sync-issues --detail " << syncIssue.getId() << " --limit=0\" to see all of them." << endl; } } void printAllIssuesDetail(mega::MegaApi& api, megacmd::ColumnDisplayer& cd, const SyncIssueList& syncIssues, bool disablePathCollapse, int rowCountLimit) { // We'll use the row count limit only for the path problems; since the user has added "--all", // we assume they want to see all issues, and thus the limit doesn't apply to the issue list in this case for (auto it = syncIssues.begin(); it != syncIssues.end(); ++it) { const auto& syncIssue = *it; assert(syncIssue.first == syncIssue.second.getId()); OUTSTREAM << "[Details on issue " << syncIssue.first << "]" << endl; printSingleIssueDetail(api, cd, syncIssue.second, disablePathCollapse, rowCountLimit); if (it != std::prev(syncIssues.end())) { OUTSTREAM << endl << endl; } cd.clear(); } } } MEGAcmd-2.5.2_Linux/src/sync_issues.h000066400000000000000000000174451516543156300174270ustar00rootroot00000000000000/** * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #pragma once #include #include #include #include #include "megaapi.h" #include "mega/types.h" #include "megacmdcommonutils.h" #define GENERATE_FROM_PATH_PROBLEM(GENERATOR_MACRO) \ GENERATOR_MACRO(mega::PathProblem::NoProblem, "-") \ GENERATOR_MACRO(mega::PathProblem::FileChangingFrequently, "File is changing frequently") \ GENERATOR_MACRO(mega::PathProblem::IgnoreRulesUnknown, "Ignore rules are unknown") \ GENERATOR_MACRO(mega::PathProblem::DetectedHardLink, "Hard link detected") \ GENERATOR_MACRO(mega::PathProblem::DetectedSymlink, "Symlink detected") \ GENERATOR_MACRO(mega::PathProblem::DetectedSpecialFile, "Special file detected") \ GENERATOR_MACRO(mega::PathProblem::DifferentFileOrFolderIsAlreadyPresent, "A different file/folder is already present") \ GENERATOR_MACRO(mega::PathProblem::ParentFolderDoesNotExist, "Parent folder does not exist") \ GENERATOR_MACRO(mega::PathProblem::FilesystemErrorDuringOperation, "There was a filesystem error during operation") \ GENERATOR_MACRO(mega::PathProblem::NameTooLongForFilesystem, "Name is too long for filesystem") \ GENERATOR_MACRO(mega::PathProblem::CannotFingerprintFile, "File fingerprint cannot be obtained") \ GENERATOR_MACRO(mega::PathProblem::DestinationPathInUnresolvedArea, "Destination path (or one of its parents) is unresolved") \ GENERATOR_MACRO(mega::PathProblem::MACVerificationFailure, "Failure verifying MAC") \ GENERATOR_MACRO(mega::PathProblem::UnknownDownloadIssue, "There was an issue downloading to destination directory") \ GENERATOR_MACRO(mega::PathProblem::DeletedOrMovedByUser, "Deleted or moved by user") \ GENERATOR_MACRO(mega::PathProblem::FileFolderDeletedByUser, "File or folder deleted by user") \ GENERATOR_MACRO(mega::PathProblem::MoveToDebrisFolderFailed, "Move to debris folder failed") \ GENERATOR_MACRO(mega::PathProblem::IgnoreFileMalformed, "Ignore file is malformed") \ GENERATOR_MACRO(mega::PathProblem::FilesystemErrorListingFolder, "There was a filesystem error listing the folder") \ GENERATOR_MACRO(mega::PathProblem::WaitingForScanningToComplete, "Waiting for scanning to complete") \ GENERATOR_MACRO(mega::PathProblem::WaitingForAnotherMoveToComplete, "Waiting for another move to complete") \ GENERATOR_MACRO(mega::PathProblem::SourceWasMovedElsewhere, "The source was moved somewhere else") \ GENERATOR_MACRO(mega::PathProblem::FilesystemCannotStoreThisName, "The filesystem cannot store this name") \ GENERATOR_MACRO(mega::PathProblem::CloudNodeInvalidFingerprint, "Cloud node has invalid fingerprint") \ GENERATOR_MACRO(mega::PathProblem::CloudNodeIsBlocked, "Cloud node is blocked") \ GENERATOR_MACRO(mega::PathProblem::PutnodeDeferredByController, "Putnode deferred by controller") \ GENERATOR_MACRO(mega::PathProblem::PutnodeCompletionDeferredByController, "Putnode completion deferred by controller") \ GENERATOR_MACRO(mega::PathProblem::PutnodeCompletionPending, "Putnode completion pending") \ GENERATOR_MACRO(mega::PathProblem::UploadDeferredByController, "Upload deferred by controller") \ GENERATOR_MACRO(mega::PathProblem::DetectedNestedMount, "Nested mount detected") struct SyncInfo { mega::SyncWaitReason mReasonType = mega::SyncWaitReason::NoReason; std::string mReason; std::string mDescription; }; class SyncIssue { std::unique_ptr mMegaStall; // Returns the cloud/local file path with index i template std::string getFilePath(int i = 0) const; // Returns the file name of the cloud/local path with index i template std::string getFileName(int i) const; // Returns the file name of path 0, with preference for cloud or local template std::string getDefaultFileName() const; // Returns the index of the first path problem of a particular type (if any) template std::optional getPathProblem(mega::PathProblem pathProblem) const; public: // We add this prefix at the start to distinguish between cloud and local absolute paths inline static const std::string CloudPrefix = ""; struct PathProblem { bool mIsCloud = false; std::string mPath; std::string mPathType = ""; mega::PathProblem mProblem = mega::PathProblem::NoProblem; int64_t mModifiedTime = 0; // in seconds since epoch int64_t mUploadedTime = 0; // in seconds since epoch int64_t mFileSize = -1; std::string_view getProblemStr() const; }; SyncIssue(const mega::MegaSyncStall& stall); std::string getId() const; SyncInfo getSyncInfo(mega::MegaSync const* parentSync) const; std::vector getPathProblems(mega::MegaApi& api) const; template std::vector getPathProblems(mega::MegaApi& api) const; std::unique_ptr getParentSync(mega::MegaApi& api) const; bool belongsToSync(const mega::MegaSync& sync) const; }; class SyncIssueList { using SyncIssuesMapT = std::map; SyncIssuesMapT mIssuesMap; friend class SyncIssuesRequestListener; // only one that can actually populate this public: template void forEach(Cb&& callback, size_t sizeLimit) const { for (auto it = begin(); it != end() && std::distance(begin(), it) < sizeLimit; ++it) { callback(it->second); } } SyncIssue const* getSyncIssue(const std::string& id) const; unsigned int getSyncIssuesCount(const mega::MegaSync& sync) const; bool empty() const { return mIssuesMap.empty(); } unsigned int size() const { return mIssuesMap.size(); } SyncIssuesMapT::const_iterator begin() const { return mIssuesMap.begin(); } SyncIssuesMapT::const_iterator end() const { return mIssuesMap.end(); } }; class SyncIssuesManager final { mega::MegaApi& mApi; bool mWarningEnabled; std::mutex mWarningMtx; std::unique_ptr mGlobalListener; std::unique_ptr mRequestListener; private: void onSyncIssuesChanged(unsigned int newSyncIssuesSize); public: SyncIssuesManager(mega::MegaApi *api); SyncIssueList getSyncIssues() const; void disableWarning(); void enableWarning(); mega::MegaGlobalListener* getGlobalListener() const { return mGlobalListener.get(); } }; namespace SyncIssuesCommand { void printAllIssues(mega::MegaApi& api, megacmd::ColumnDisplayer& cd, const SyncIssueList& syncIssues, bool disablePathCollapse, int rowCountLimit); void printSingleIssueDetail(mega::MegaApi& api, megacmd::ColumnDisplayer& cd, const SyncIssue& syncIssue, bool disablePathCollapse, int rowCountLimit); void printAllIssuesDetail(mega::MegaApi& api, megacmd::ColumnDisplayer& cd, const SyncIssueList& syncIssues, bool disablePathCollapse, int rowCountLimit); } MEGAcmd-2.5.2_Linux/src/updater/000077500000000000000000000000001516543156300163405ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/src/updater/MacUtils.h000066400000000000000000000002241516543156300202300ustar00rootroot00000000000000#ifndef MACUTILS_H #define MACUTILS_H #include bool downloadFileSynchronously(std::string url, std::string path); #endif // MACUTILS_H MEGAcmd-2.5.2_Linux/src/updater/MacUtils.mm000066400000000000000000000011031516543156300204070ustar00rootroot00000000000000#include "MacUtils.h" #include bool downloadFileSynchronously(std::string url, std::string path) { NSString *stringURL = [NSString stringWithCString:url.c_str() encoding:NSUTF8StringEncoding]; NSURL *myURL = [NSURL URLWithString:stringURL]; NSData *data = [NSData dataWithContentsOfURL:myURL]; if (data == nil) { return false; } BOOL result = [data writeToFile:[NSString stringWithCString:path.c_str() encoding:NSUTF8StringEncoding] atomically:NO]; if (result == NO) { return false; } return true; } MEGAcmd-2.5.2_Linux/src/updater/MegaUpdater.cpp000066400000000000000000000153161516543156300212500ustar00rootroot00000000000000#ifdef _WIN32 #define _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING #include #include #include #include #include #else #include #include #include //flock #include #include #endif #include #include #include #include #include "UpdateTask.h" #include "Preferences.h" using namespace std; namespace megacmdupdater { bool extractarg(vector& args, const char *what) { for (int i = int(args.size()); i--; ) { if (!strcmp(args[i], what)) { args.erase(args.begin() + i); return true; } } return false; } int extractVersionArg(vector& args) { auto it = std::find_if(args.begin(), args.end(), [] (const char* s) { return !strcmp(s, "--version"); }); if (it == args.end() || std::next(it) == args.end()) { return -1; } const char* versionStr = *std::next(it); args.erase(it, std::next(it, 2)); try { return std::stoi(versionStr); } catch (const std::exception& e) { return -1; } } #if !defined(_WIN32) && defined(LOCK_EX) && defined(LOCK_NB) static int fdMcmdUpdaterLockFile; #endif #ifdef _WIN32 void utf8ToUtf16(const char* utf8data, string* utf16string) { if(!utf8data) { utf16string->clear(); utf16string->append("", 1); return; } DWORD size = (DWORD)strlen(utf8data) + 1; // make space for the worst case utf16string->resize(size * sizeof(wchar_t)); // resize to actual result utf16string->resize(sizeof(wchar_t) * MultiByteToWideChar(CP_UTF8, 0, utf8data, size, (wchar_t*)utf16string->data(), int(utf16string->size() / sizeof(wchar_t) + 1))); if (utf16string->size()) { utf16string->resize(utf16string->size() - 1); } else { utf16string->append("", 1); } } void utf16ToUtf8(const wchar_t* utf16data, int utf16size, string* utf8string) { if(!utf16size) { utf8string->clear(); return; } utf8string->resize((utf16size + 1) * 4); utf8string->resize(WideCharToMultiByte(CP_UTF8, 0, utf16data, utf16size, (char*)utf8string->data(), int(utf8string->size()) + 1, NULL, NULL)); } #endif string getLockFile() { string configFolder; #ifdef _WIN32 TCHAR szPath[MAX_PATH]; if (!SUCCEEDED(GetModuleFileName(NULL, szPath , MAX_PATH))) { cerr << "Couldnt get EXECUTABLE folder" << endl; } else { if (SUCCEEDED(PathRemoveFileSpec(szPath))) { utf16ToUtf8(szPath, lstrlen(szPath), &configFolder); } } #else const char *homedir = NULL; homedir = getenv("HOME"); if (!homedir) { struct passwd pd; struct passwd* pwdptr = &pd; struct passwd* tempPwdPtr; char pwdbuffer[200]; int pwdlinelen = sizeof( pwdbuffer ); if (( getpwuid_r(22, pwdptr, pwdbuffer, pwdlinelen, &tempPwdPtr)) != 0) { cerr << "Couldnt get HOME folder" << endl; return string(); } else { homedir = pwdptr->pw_dir; } } configFolder = homedir; #endif if (configFolder.size()) { stringstream lockfile; lockfile << configFolder << "/" << "lockMCMDUpdater"; return lockfile.str(); } return string(); } bool lockExecution() { string thelockfile = getLockFile(); if (thelockfile.size()) { #ifdef _WIN32 string wlockfile; utf8ToUtf16(thelockfile.c_str(),&wlockfile); OFSTRUCT offstruct; if (LZOpenFileW((LPWSTR)wlockfile.data(), &offstruct, OF_CREATE | OF_READWRITE |OF_SHARE_EXCLUSIVE ) == HFILE_ERROR) { return false; } #elif defined(LOCK_EX) && defined(LOCK_NB) fdMcmdUpdaterLockFile = open(thelockfile.c_str(), O_RDWR | O_CREAT, 0666); // open or create lockfile //check open success... if (flock(fdMcmdUpdaterLockFile, LOCK_EX | LOCK_NB)) { return false; } if (fcntl(fdMcmdUpdaterLockFile, F_SETFD, FD_CLOEXEC) == -1) { cerr << "ERROR setting CLOEXEC to lock fdMcmdUpdaterLockFile: " << errno << endl; } #else ifstream fi(thelockfile.c_str()); if(!fi.fail()){ return false; } if (fi.is_open()) { fi.close(); } ofstream fo(thelockfile.c_str()); if (fo.is_open()) { fo.close(); } #endif return true; } else { cerr << "ERROR figuring out lock file " << endl; return false; } } void unlockExecution() { string thelockfile = getLockFile(); if (thelockfile.size()) { #if !defined(_WIN32) && defined(LOCK_EX) && defined(LOCK_NB) flock(fdMcmdUpdaterLockFile, LOCK_UN | LOCK_NB); close(fdMcmdUpdaterLockFile); #endif #ifdef WIN32 _unlink(thelockfile.c_str()); #else unlink(thelockfile.c_str()); #endif } else { cerr << "ERROR figuring out lock file " << endl; } } }//end of namespace using namespace megacmdupdater; #ifdef _WIN32 int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR szCmdLine,int iCmdShow) { int argc = __argc; char** argv = __argv; #else int main(int argc, char *argv[]) { #endif vector args; if (argc > 1) { args = vector(argv + 1, argv + argc); } bool doNotInstall = extractarg(args, "--do-not-install"); bool emergencyupdate = extractarg(args, "--emergency-update"); bool skiplockcheck = extractarg(args, "--skip-lock-check"); int currentVersion = extractVersionArg(args); #ifdef _WIN32 if(!emergencyupdate) { AllocConsole(); freopen("CONOUT$", "w+", stdout); } #endif if (!lockExecution() && !skiplockcheck) { cerr << "Another instance of MEGAcmd Updater is running. Execute with --skip-lock-check to force running (NOT RECOMMENDED)" << endl; return 0; } time_t currentTime = time(NULL); cout << "Process started at " << ctime(¤tTime) << endl; srand(unsigned(currentTime)); UpdateTask updater; bool updated = updater.checkForUpdates(emergencyupdate, doNotInstall, currentVersion); currentTime = time(NULL); cout << "Process finished at " << ctime(¤tTime) << endl; unlockExecution(); if (getenv("SLEEP_AFTER_UPDATE")) { #ifdef _WIN32 Sleep(15*1000); #else sleep(15); #endif } return updated; } MEGAcmd-2.5.2_Linux/src/updater/Preferences.h000066400000000000000000000032701516543156300207540ustar00rootroot00000000000000#ifndef PREFERENCES_H #define PREFERENCES_H const char CLIENT_KEY[] = "BdARkQSQ"; const char USER_AGENT[] = "MEGA/MEGAcmdUpdaterTask"; #ifdef _WIN32 #ifdef _WIN64 const char UPDATE_CHECK_URL[] = "http://g.static.mega.co.nz/upd/wcmd64/v.txt"; const char EMERGENCY_UPDATE_CHECK_URL[] = "http://g.static.mega.co.nz/eupd/wcmd64/v.txt"; #else const char UPDATE_CHECK_URL[] = "http://g.static.mega.co.nz/upd/wcmd/v.txt"; const char EMERGENCY_UPDATE_CHECK_URL[] = "http://g.static.mega.co.nz/eupd/wcmd/v.txt"; #endif #else const char UPDATE_CHECK_URL[] = "http://g.static.mega.co.nz/upd/mcmd/v.txt"; const char EMERGENCY_UPDATE_CHECK_URL[] = "http://g.static.mega.co.nz/eupd/mcmd/v.txt"; const char APP_DIR_BUNDLE[] = "/Applications/MEGAcmd.app/"; #endif const char UPDATE_PUBLIC_KEY[] = "EACuusrffoqCAsjS824EsQ04pvuDKHUd8PCAfPLsowLBMGWmcxmX-tgk05EXkdyHZPixEda96XadqudlQOEYLBlxMgcAwDkewKoW1XtBQVm3wXeHIvEeBimpWWU6Z6qYP5BiCbW-wCTDS67f4LdkrbA4hcJiVTxQxRUxoJU-I-DCh0ZLCxdrXHpWBrXGv9hFfxMFhr01YGxcW1tgqRx5r6q3YPpFaGo-q9ib3YIBE2HMAc_n_ayz4g5HMAq-C61WT_vhHzPnoCUwpRH68TCoop-3VlYTo8Ps-Xzno1OazCH6VM5MiCLS0X5jhh54RveVZn44MRMLDdj3jzUed4-hFgOXNYLLX1kDRlXiDwGtht-wPJF7BLNVkRolfOUrsONmw3Tbz_ywB-TK7pjhlYoMo7enwABT1iBQoub80OEi-1ltMnzFRWu6o7KCsCqnDtJpALG04B6E1aWvO2HK0-UQNIAPfbErtG4pVVirIaSq4vX5BlmYJ_XCAsaFuLufJvBQBIEwCJRdQ96o4plgzHuJwlaEeYd2dMEjafOi3DBPV7OjN9H7yI0NyHDrqV_hDAmMqHHPu3hdhqkdrSFmcMVGsxXkaOnXDNmJfdwr_mostPY-Uwx_g2vRHKgLpkntlIlvh0GALEzLpukdmnKgXrUNRXrIyyjoRXGNMlBTdV8vAMlGVQAFEQ"; const char UPDATE_FILENAME[] = "v.txt"; const char UPDATE_FOLDER_NAME[] = "eupdate"; const char BACKUP_FOLDER_NAME[] = "ebackup"; const char VERSION_FILE_NAME[] = "megacmd.version"; #endif // PREFERENCES_H MEGAcmd-2.5.2_Linux/src/updater/UpdateTask.cpp000066400000000000000000001102451516543156300211140ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include #include #ifdef _WIN32 #include #include #include #include #include #include #include #include #include #else #include #include #include #include #include #endif #include "UpdateTask.h" #include "Preferences.h" #include "MacUtils.h" #ifndef WIN32 #include "../megacmdcommonutils.h" // Platform dirs #endif #include "../megacmdversion.h" using std::string; using CryptoPP::Integer; using std::cout; using std::endl; using std::vector; enum { LOG_LEVEL_FATAL = 0, // Very severe error event that will presumably lead the application to abort. LOG_LEVEL_ERROR, // Error information but will continue application to keep running. LOG_LEVEL_WARNING, // Information representing errors in application but application will keep running LOG_LEVEL_INFO, // Mainly useful to represent current progress of application. LOG_LEVEL_DEBUG, // Informational logs, that are useful for developers. Only applicable if DEBUG is defined. LOG_LEVEL_MAX }; #define MAX_LOG_SIZE 1024 char log_message[MAX_LOG_SIZE]; #define LOG(logLevel, ...) snprintf(log_message, MAX_LOG_SIZE, __VA_ARGS__); \ cout << log_message << endl; #ifdef _WIN32 #ifndef PATH_MAX #define PATH_MAX _MAX_PATH #endif #define MEGA_SEPARATOR '\\' void utf8ToUtf16(const char* utf8data, string* utf16string) { if(!utf8data) { utf16string->clear(); utf16string->append("", 1); return; } DWORD size = (DWORD)strlen(utf8data) + 1; // make space for the worst case utf16string->resize(size * sizeof(wchar_t)); // resize to actual result utf16string->resize(sizeof(wchar_t) * MultiByteToWideChar(CP_UTF8, 0, utf8data, size, (wchar_t*)utf16string->data(), int(utf16string->size() / sizeof(wchar_t) + 1))); if (utf16string->size()) { utf16string->resize(utf16string->size() - 1); } else { utf16string->append("", 1); } } void utf16ToUtf8(const wchar_t* utf16data, int utf16size, string* utf8string) { if(!utf16size) { utf8string->clear(); return; } utf8string->resize((utf16size + 1) * 4); utf8string->resize(WideCharToMultiByte(CP_UTF8, 0, utf16data, utf16size, (char*)utf8string->data(), int(utf8string->size()) + 1, NULL, NULL)); } int mega_mkdir(const char *path) { string wpath; utf8ToUtf16(path, &wpath); wpath.append("", 1); return _wmkdir((LPCWSTR)wpath.data()); } int mega_access(const char *path) { string wpath; utf8ToUtf16(path, &wpath); wpath.append("", 1); return _waccess((LPCWSTR)wpath.data(), 0); } FILE *mega_fopen(const char *path, const char *mode) { string wpath; utf8ToUtf16(path, &wpath); wpath.append("", 1); string wmode; utf8ToUtf16(mode, &wmode); wmode.append("", 1); return _wfopen((LPCWSTR)wpath.data(), (LPCWSTR)wmode.data()); } int mega_remove(const char *path) { string wpath; utf8ToUtf16(path, &wpath); wpath.append("", 1); return _wremove((LPCWSTR)wpath.data()); } int64_t mega_size(const char *path) { string wpath; utf8ToUtf16(path, &wpath); wpath.append("", 1); WIN32_FILE_ATTRIBUTE_DATA fad; if (!GetFileAttributesExW((LPCWSTR)wpath.data(), GetFileExInfoStandard, (LPVOID)&fad)) { //DWORD e = GetLastError(); return -1; } return ((int64_t)fad.nFileSizeHigh << 32) + (int64_t)fad.nFileSizeLow; } int mega_rename(const char *srcpath, const char *dstpath) { string wsrcpath; utf8ToUtf16(srcpath, &wsrcpath); wsrcpath.append("", 1); string wdstpath; utf8ToUtf16(dstpath, &wdstpath); wdstpath.append("", 1); return _wrename((LPCWSTR)wsrcpath.data(),(LPCWSTR)wdstpath.data()); } int mega_rmdir(const char *path) { string wpath; utf8ToUtf16(path, &wpath); wpath.append("", 1); return _wrmdir((LPCWSTR)wpath.data()); } string UpdateTask::getAppDataDir() { string path; TCHAR szPath[MAX_PATH]; if (!SUCCEEDED(GetModuleFileName(NULL, szPath , MAX_PATH))) { LOG(LOG_LEVEL_ERROR, "Couldnt get EXECUTABLE folder"); } else { if (SUCCEEDED(PathRemoveFileSpec(szPath))) { if (PathAppend(szPath,TEXT(".megaCmd"))) { utf16ToUtf8(szPath, lstrlen(szPath), &path); path.append("\\"); } } } return path; } string UpdateTask::getAppDir() { string path; string wpath; wpath.resize(MAX_PATH * sizeof (wchar_t)); if (SUCCEEDED(GetModuleFileNameW(NULL, (LPWSTR)wpath.data() , MAX_PATH)) && SUCCEEDED(PathRemoveFileSpecW((LPWSTR)wpath.data()))) { utf16ToUtf8((LPWSTR)wpath.data(), lstrlen((LPCWSTR)wpath.data()), &path); path.append("\\"); } return path; } #define MEGA_TO_NATIVE_SEPARATORS(x) std::replace(x.begin(), x.end(), '/', '\\'); #define MEGA_SET_PERMISSIONS #else int64_t mega_size(const char *path) { struct stat statbuf; string localname = path; #ifdef USE_IOS if (PosixFileSystemAccess::appbasepath) { if (localname.size() && localname.at(0) != '/') { localname.insert(0, PosixFileSystemAccess::appbasepath); } } #endif if (!stat(localname.c_str(), &statbuf)) { if (S_ISDIR(statbuf.st_mode)) { return -1; } return statbuf.st_size; } return -1; } #define MEGA_SEPARATOR '/' #define mega_mkdir(x) mkdir(x, S_IRWXU) #define mega_access(x) access(x, F_OK) #define mega_fopen fopen #define mega_remove remove #define mega_rename rename #define mega_rmdir rmdir string UpdateTask::getAppDataDir() { auto dirs = megacmd::PlatformDirectories::getPlatformSpecificDirectories(); return dirs->configDirPath().string(); } #define MEGA_TO_NATIVE_SEPARATORS(x) std::replace(x.begin(), x.end(), '\\', '/'); #define MEGA_SET_PERMISSIONS chmod("/Applications/MEGAcmd.app/Contents/MacOS/mega-cmd", S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); \ chmod("/Applications/MEGAcmd.app/Contents/MacOS/MEGAcmdShell", S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); \ chmod("/Applications/MEGAcmd.app/Contents/MacOS/MEGAcmd", S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); \ chmod("/Applications/MEGAcmd.app/Contents/MacOS/MEGAcmdUpdater", S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); string UpdateTask::getAppDir() { return APP_DIR_BUNDLE; } #endif #define mega_base_path(x) x.substr(0, x.find_last_of("/\\") + 1) int mkdir_p(const char *path) { /* Adapted from http://stackoverflow.com/a/2336245/119527 */ const size_t len = strlen(path); char _path[PATH_MAX]; char *p; errno = 0; /* Copy string so its mutable */ if (len > sizeof(_path) - 1) { return -1; } strcpy(_path, path); /* Iterate the string */ for (p = _path + 1; *p; p++) { if (*p == '\\' || *p == '/') { /* Temporarily truncate */ *p = '\0'; if (mega_mkdir(_path) != 0 && errno != EEXIST) { return -1; } *p = MEGA_SEPARATOR; } } if (mega_mkdir(_path) != 0 && errno != EEXIST) { return -1; } return 0; } UpdateTask::UpdateTask() { isPublic = false; string updatePublicKey = UPDATE_PUBLIC_KEY; if (getenv("MEGA_UPDATE_PUBLIC_KEY")) { updatePublicKey = getenv("MEGA_UPDATE_PUBLIC_KEY"); } signatureChecker = new SignatureChecker(updatePublicKey.c_str()); currentFile = 0; updateVersion = 0; currentVersion = MEGACMD_CODE_VERSION; appDataFolder = getAppDataDir(); appFolder = getAppDir(); updateFolder = appDataFolder + UPDATE_FOLDER_NAME + MEGA_SEPARATOR; backupFolder = appDataFolder + BACKUP_FOLDER_NAME + MEGA_SEPARATOR; #ifdef _WIN32 WCHAR commonPath[MAX_PATH + 1]; if (SHGetSpecialFolderPathW(NULL, commonPath, CSIDL_COMMON_APPDATA, FALSE)) { string wappFolder; int len = lstrlen(commonPath); utf8ToUtf16(appFolder.c_str(), &wappFolder); if (!memcmp(commonPath, (WCHAR *)wappFolder.data(), len * sizeof(WCHAR)) && wappFolder.size() > (len * sizeof(WCHAR)) && *((WCHAR *)(wappFolder.data() + (len * sizeof(WCHAR)))) == L'\\') { isPublic = true; LOG(LOG_LEVEL_INFO, "The installation is public"); } } #endif } UpdateTask::~UpdateTask() { delete signatureChecker; } bool UpdateTask::checkForUpdates(bool emergencyUpdater, bool doNotInstall, int _currentVersion) { LOG(LOG_LEVEL_INFO, "Starting update check"); bool hasOverride = getenv("MEGACMD_VERSION_OVERRIDE"); if (hasOverride) { try { currentVersion = std::stoi(getenv("MEGACMD_VERSION_OVERRIDE")); } catch (...) { hasOverride = false; } } if (!hasOverride && _currentVersion != -1) { currentVersion = _currentVersion; } // Create random sequence for http request string randomSec("?"); for (int i = 0; i < 10; i++) { randomSec += char('A'+ (rand() % 26)); } if (!appFolder.size() || !appDataFolder.size()) { LOG(LOG_LEVEL_ERROR, "No app or data folder set"); return false; } string appData = appDataFolder; string updateFile = appData.append(UPDATE_FILENAME); string updateurl=emergencyUpdater?EMERGENCY_UPDATE_CHECK_URL:UPDATE_CHECK_URL; updateurl.append(randomSec); if (getenv("USE_UPDATE_TEST_FILE")) { if (updateurl.find("v.txt") != string::npos) { updateurl = updateurl.replace(updateurl.find("v.txt"),strlen("v.txt"),"vv.txt"); } } if (getenv("MEGA_UPDATE_CHECK_URL")) { updateurl = getenv("MEGA_UPDATE_CHECK_URL"); } if (downloadFile((char *)(updateurl.c_str()), updateFile.c_str())) { FILE * pFile; pFile = mega_fopen(updateFile.c_str(), "r"); if (!pFile) { LOG(LOG_LEVEL_ERROR, "Error opening update file"); mega_remove(updateFile.c_str()); return false; } if (!processUpdateFile(pFile)) { fclose(pFile); mega_remove(updateFile.c_str()); return false; } initialCleanup(); fclose(pFile); mega_remove(updateFile.c_str()); currentFile = 0; while (currentFile < downloadURLs.size()) { if (!alreadyDownloaded(localPaths[currentFile], fileSignatures[currentFile])) { //Create the folder for the new file string localFile = updateFolder + localPaths[currentFile]; if (mkdir_p(mega_base_path(localFile).c_str()) == -1) { LOG(LOG_LEVEL_INFO, "Unable to create folder for file: %s", localFile.c_str()); return false; } //Delete the file if exists if (fileExist(localFile.c_str())) { mega_remove(localFile.c_str()); } //Download file to specific folder if (downloadFile(string(downloadURLs[currentFile] + randomSec).c_str(), localFile)) { LOG(LOG_LEVEL_INFO, "File ready: %s", localPaths[currentFile].c_str()); if (!alreadyDownloaded(localPaths[currentFile], fileSignatures[currentFile])) { LOG(LOG_LEVEL_ERROR, "Signature of downloaded file doesn't match: %s", localPaths[currentFile].c_str()); return false; } LOG(LOG_LEVEL_INFO, "File signature OK: %s", localPaths[currentFile].c_str()); currentFile++; continue; } return false; } LOG(LOG_LEVEL_INFO, "File already downloaded: %s", localPaths[currentFile].c_str()); currentFile++; } if (!doNotInstall) { //All files have been processed. Apply update if (!performUpdate()) { LOG(LOG_LEVEL_INFO, "Error applying update"); return false; } finalCleanup(); } else { LOG(LOG_LEVEL_INFO, "Do Not Install requested. Perform update skipped."); } return true; } else { LOG(LOG_LEVEL_ERROR, "Unable to download file"); return false; } } #ifndef WIN32 #ifdef __linux__ string <rimEtcProperty(string &s, const char &c) { size_t pos = s.find_first_not_of(c); s = s.substr(pos == string::npos ? s.length() : pos, s.length()); return s; } string &rtrimEtcProperty(string &s, const char &c) { size_t pos = s.find_last_not_of(c); if (pos != string::npos) { pos++; } s = s.substr(0, pos); return s; } string &trimEtcproperty(string &what) { rtrimEtcProperty(what,' '); ltrimEtcProperty(what,' '); if (what.size() > 1) { if (what[0] == '\'' || what[0] == '"') { rtrimEtcProperty(what, what[0]); ltrimEtcProperty(what, what[0]); } } return what; } string getPropertyFromEtcFile(const char *configFile, const char *propertyName) { std::ifstream infile(configFile); string line; while (getline(infile, line)) { if (line.length() > 0 && line[0] != '#') { if (!strlen(propertyName)) //if empty return first line { return trimEtcproperty(line); } string key, value; size_t pos = line.find("="); if (pos != string::npos && ((pos + 1) < line.size())) { key = line.substr(0, pos); rtrimEtcProperty(key, ' '); if (!strcmp(key.c_str(), propertyName)) { value = line.substr(pos + 1); return trimEtcproperty(value); } } } } return string(); } string getDistro() { string distro; distro = getPropertyFromEtcFile("/etc/lsb-release", "DISTRIB_ID"); if (!distro.size()) { distro = getPropertyFromEtcFile("/etc/os-release", "ID"); } if (!distro.size()) { distro = getPropertyFromEtcFile("/etc/redhat-release", ""); } if (!distro.size()) { distro = getPropertyFromEtcFile("/etc/debian-release", ""); } if (distro.size() > 20) { distro = distro.substr(0, 20); } transform(distro.begin(), distro.end(), distro.begin(), ::tolower); return distro; } string getDistroVersion() { string version; version = getPropertyFromEtcFile("/etc/lsb-release", "DISTRIB_RELEASE"); if (!version.size()) { version = getPropertyFromEtcFile("/etc/os-release", "VERSION_ID"); } if (version.size() > 10) { version = version.substr(0, 10); } transform(version.begin(), version.end(), version.begin(), ::tolower); return version; } #endif string osversion() { string toret; auto u = &toret; #ifdef __linux__ string distro = getDistro(); if (distro.size()) { u->append(distro); string distroversion = getDistroVersion(); if (distroversion.size()) { u->append(" "); u->append(distroversion); u->append("/"); } else { u->append("/"); } } #endif utsname uts; if (!uname(&uts)) { u->append(uts.sysname); u->append(" "); u->append(uts.release); u->append(" "); u->append(uts.machine); } return toret; } #endif bool UpdateTask::downloadFile(string url, string dstPath) { LOG(LOG_LEVEL_INFO, "Downloading updated file from: %s", url.c_str()); #ifdef _WIN32 string wurl; string wdstPath; wurl.resize((url.size() + 1) * 4); utf8ToUtf16(url.c_str(), &wurl); wurl.append("", 1); wdstPath.resize((url.size() + 1) * 4); utf8ToUtf16(dstPath.c_str(), &wdstPath); wdstPath.append("", 1); HRESULT res = URLDownloadToFileW(NULL, (LPCWSTR)wurl.data(), (LPCWSTR)wdstPath.data(), 0, NULL); if (res != S_OK) { LOG(LOG_LEVEL_ERROR, "Unable to download file. Error code: %d", res); return false; } if (!fileExist(dstPath.c_str())) // URLDownloadToFileW does not create an empty file { //create empty file FILE * pFileEmpty = mega_fopen(dstPath.c_str(), "wt+"); if (pFileEmpty == NULL) { LOG(LOG_LEVEL_ERROR, "Couldn't create empty file: %s", dstPath.c_str()); return false; } fclose(pFileEmpty); } #elif defined(__MACH__) bool success = downloadFileSynchronously(url, dstPath); if (!success) { LOG(LOG_LEVEL_ERROR, "Unable to download file %s to %s.", url.c_str(), dstPath.c_str()); return false; } #else string dlcommand = "curl "; dlcommand.append(" -A "); dlcommand.append(" \"MEGAcmdUpdater "); dlcommand.append(osversion()); dlcommand.append("\" "); dlcommand.append(url.c_str()); dlcommand.append(" -o "); dlcommand.append(dstPath); system(dlcommand.c_str()); #endif LOG(LOG_LEVEL_INFO, "File downloaded OK"); return true; } bool UpdateTask::processUpdateFile(FILE *fd) { LOG(LOG_LEVEL_DEBUG, "Reading update info"); string version = readNextLine(fd); if (version.empty()) { LOG(LOG_LEVEL_WARNING, "Invalid update info"); return false; } try { updateVersion = std::stoi(version); } catch (const std::exception& e) { LOG(LOG_LEVEL_ERROR, "Error converting the version string to an integer"); return false; } if (updateVersion <= currentVersion) { LOG(LOG_LEVEL_INFO, "Update not needed. Last version: %d - Current version: %d", updateVersion, currentVersion); return false; } LOG(LOG_LEVEL_WARNING, "Update needed"); string updateSignature = readNextLine(fd); if (updateSignature.empty()) { LOG(LOG_LEVEL_ERROR, "Invalid update info (empty info signature)"); return false; } initSignature(); addToSignature(version.data(), version.length()); while (true) { string url = readNextLine(fd); if (url.empty()) { break; } string localPath = readNextLine(fd); if (localPath.empty()) { LOG(LOG_LEVEL_ERROR, "Invalid update info (empty path)"); return false; } string fileSignature = readNextLine(fd); if (fileSignature.empty()) { LOG(LOG_LEVEL_ERROR, "Invalid update info (empty file signature)"); return false; } addToSignature(url.data(), url.length()); addToSignature(localPath.data(), localPath.length()); addToSignature(fileSignature.data(), fileSignature.length()); MEGA_TO_NATIVE_SEPARATORS(localPath); if (alreadyInstalled(localPath, fileSignature)) { LOG(LOG_LEVEL_INFO, "File already installed: %s", localPath.c_str()); continue; } downloadURLs.push_back(url); localPaths.push_back(localPath); fileSignatures.push_back(fileSignature); } if (!downloadURLs.size()) { LOG(LOG_LEVEL_WARNING, "All files are up to date"); return false; } if (!checkSignature(updateSignature)) { LOG(LOG_LEVEL_ERROR, "Invalid update info (invalid signature)"); return false; } return true; } bool UpdateTask::fileExist(const char *path) { return (mega_access(path) != -1); } void UpdateTask::addToSignature(const char* bytes, size_t length) { signatureChecker->add(bytes, int(length)); } void UpdateTask::initSignature() { signatureChecker->init(); } bool UpdateTask::checkSignature(string value) { bool result = signatureChecker->checkSignature(value.c_str()); if (!result) { LOG(LOG_LEVEL_ERROR, "Invalid signature"); } return result; } bool UpdateTask::performUpdate() { LOG(LOG_LEVEL_INFO, "Applying update..."); for (vector::size_type i = 0; i < localPaths.size(); i++) { string file = backupFolder + localPaths[i]; if (mkdir_p(mega_base_path(file).c_str()) == -1) { LOG(LOG_LEVEL_ERROR, "Error creating backup folder for: %s", file.c_str()); rollbackUpdate(i); return false; } string origFile = appFolder + localPaths[i]; if (mega_rename(origFile.c_str(), file.c_str()) && errno != ENOENT) { LOG(LOG_LEVEL_ERROR, "Error creating backup of file %s to %s. errno=%d", origFile.c_str(), file.c_str(), errno); rollbackUpdate(i); return false; } string update = updateFolder + localPaths[i]; if (mega_size(update.c_str())) { if (mkdir_p(mega_base_path(origFile).c_str()) == -1) { LOG(LOG_LEVEL_ERROR, "Error creating target folder for: %s", origFile.c_str()); rollbackUpdate(i); return false; } setPermissions(mega_base_path(origFile).c_str()); if (mega_rename(update.c_str(), origFile.c_str())) { LOG(LOG_LEVEL_ERROR, "Error installing file %s in %s", update.c_str(), origFile.c_str()); rollbackUpdate(i); return false; } setPermissions(origFile.c_str()); LOG(LOG_LEVEL_INFO, "File correctly installed: %s", localPaths[i].c_str()); } else { LOG(LOG_LEVEL_INFO, "File correctly removed: %s", localPaths[i].c_str()); } } LOG(LOG_LEVEL_INFO, "Update successfully installed"); return true; } void UpdateTask::rollbackUpdate(size_t fileNum) { LOG(LOG_LEVEL_INFO, "Uninstalling update..."); for (int i = fileNum; i >= 0; i--) { string origFile = appFolder + localPaths[i]; mega_rename(origFile.c_str(), (updateFolder + localPaths[i]).c_str()); mega_rename((backupFolder + localPaths[i]).c_str(), origFile.c_str()); LOG(LOG_LEVEL_INFO, "File restored: %s", localPaths[i].c_str()); } LOG(LOG_LEVEL_INFO, "Update uninstalled"); } void UpdateTask::initialCleanup() { removeRecursively(backupFolder); } void UpdateTask::finalCleanup() { removeRecursively(updateFolder); MEGA_SET_PERMISSIONS; LOG(LOG_LEVEL_INFO, "Version code updated to %d", updateVersion); } bool UpdateTask::setPermissions(const char *path) { #ifndef _WIN32 if (path && string(path).find("/mega-") != string::npos) { chmod(path, S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); } return true; #else if (!isPublic) { return true; } string wfileName; utf8ToUtf16(path, &wfileName); wfileName.append("", 1); bool result = false; PACL pOldDACL = NULL, pNewDACL = NULL; PSECURITY_DESCRIPTOR pSD = NULL; DWORD sidSize = SECURITY_MAX_SID_SIZE; EXPLICIT_ACCESS ea; ZeroMemory(&ea, sizeof(EXPLICIT_ACCESS)); ea.grfAccessPermissions = GENERIC_READ | GENERIC_EXECUTE; ea.grfAccessMode = GRANT_ACCESS; ea.grfInheritance = NO_INHERITANCE; ea.Trustee.TrusteeForm = TRUSTEE_IS_SID; ea.Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP; if ((GetNamedSecurityInfo((LPCWSTR)wfileName.data(), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, NULL, NULL, &pOldDACL, NULL, &pSD) == ERROR_SUCCESS) && (ea.Trustee.ptstrName = (LPWSTR)LocalAlloc(LMEM_FIXED, sidSize)) && CreateWellKnownSid(WinBuiltinUsersSid, NULL, ea.Trustee.ptstrName, &sidSize) && (SetEntriesInAcl(1, &ea, pOldDACL, &pNewDACL) == ERROR_SUCCESS) && (SetNamedSecurityInfo((LPWSTR)wfileName.data(), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, NULL, NULL, pNewDACL, NULL) == ERROR_SUCCESS)) { result = true; LOG(LOG_LEVEL_INFO, "Permissions updated"); } if (ea.Trustee.ptstrName != NULL) { LocalFree(ea.Trustee.ptstrName); } if(pSD != NULL) { LocalFree((HLOCAL) pSD); } if(pNewDACL != NULL) { LocalFree((HLOCAL) pNewDACL); } return result; #endif } bool UpdateTask::removeRecursively(string path) { #ifndef _WIN32 string spath = path; emptydirlocal(&spath); #else string utf16path; utf8ToUtf16(path.c_str(), &utf16path); if (utf16path.size() > 1) { utf16path.resize(utf16path.size() - 1); emptydirlocal(&utf16path); } #endif return !mega_rmdir(path.c_str()); } bool UpdateTask::alreadyInstalled(string relativePath, string fileSignature) { return alreadyExists(appFolder + relativePath, fileSignature); } bool UpdateTask::alreadyDownloaded(string relativePath, string fileSignature) { return alreadyExists(updateFolder + relativePath, fileSignature); } bool UpdateTask::alreadyExists(string absolutePath, string fileSignature) { string updatePublicKey = UPDATE_PUBLIC_KEY; if (getenv("MEGA_UPDATE_PUBLIC_KEY")) { updatePublicKey = getenv("MEGA_UPDATE_PUBLIC_KEY"); } SignatureChecker tmpHash(updatePublicKey.c_str()); char *buffer; long fileLength; FILE * pFile = mega_fopen(absolutePath.c_str(), "rb"); if (pFile == NULL) { return false; } //Get size of file and rewind FILE pointer fseek(pFile, 0, SEEK_END); fileLength = ftell(pFile); rewind(pFile); buffer = (char *)malloc(fileLength); if (buffer == NULL) { fclose(pFile); return false; } size_t sizeRead = fread(buffer, 1, fileLength, pFile); if (sizeRead != fileLength) { fclose(pFile); return false; } tmpHash.add(buffer, unsigned(sizeRead)); fclose(pFile); free(buffer); return tmpHash.checkSignature(fileSignature.data()); } string UpdateTask::readNextLine(FILE *fd) { char line[4096]; if (!fgets(line, sizeof(line), fd)) { return string(); } line[strcspn(line, "\n")] = '\0'; return string(line); } #ifdef _WIN32 // delete all files and folders contained in the specified folder // (does not recurse into mounted devices) void UpdateTask::emptydirlocal(string* name, dev_t basedev) { HANDLE hDirectory, hFind; dev_t currentdev; int added = 0; if (name->size() > sizeof(wchar_t) && !memcmp(name->data() + name->size() - sizeof(wchar_t), (char*)(void*)L":", sizeof(wchar_t))) { name->append((char*)(void*)L"\\", sizeof(wchar_t)); added = sizeof(wchar_t); } name->append("", 1); WIN32_FILE_ATTRIBUTE_DATA fad; if (!GetFileAttributesExW((LPCWSTR)name->data(), GetFileExInfoStandard, (LPVOID)&fad) || !(fad.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) || fad.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) { // discard files and resparse points (links, etc.) name->resize(name->size() - added - 1); return; } #ifdef WINDOWS_PHONE CREATEFILE2_EXTENDED_PARAMETERS ex = { 0 }; ex.dwSize = sizeof(ex); ex.dwFileFlags = FILE_FLAG_BACKUP_SEMANTICS; hDirectory = CreateFile2((LPCWSTR)name->data(), GENERIC_READ, FILE_SHARE_WRITE | FILE_SHARE_READ, OPEN_EXISTING, &ex); #else hDirectory = CreateFileW((LPCWSTR)name->data(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); #endif name->resize(name->size() - added - 1); if (hDirectory == INVALID_HANDLE_VALUE) { // discard not accessible folders return; } #ifdef WINDOWS_PHONE FILE_ID_INFO fi = { 0 }; if(!GetFileInformationByHandleEx(hDirectory, FileIdInfo, &fi, sizeof(fi))) #else BY_HANDLE_FILE_INFORMATION fi; if (!GetFileInformationByHandle(hDirectory, &fi)) #endif { currentdev = 0; } else { #ifdef WINDOWS_PHONE currentdev = fi.VolumeSerialNumber + 1; #else currentdev = fi.dwVolumeSerialNumber + 1; #endif } CloseHandle(hDirectory); if (basedev && currentdev != basedev) { // discard folders on different devices return; } bool removed; for (;;) { // iterate over children and delete removed = false; name->append((char*)(void*)L"\\*", 5); WIN32_FIND_DATAW ffd; #ifdef WINDOWS_PHONE hFind = FindFirstFileExW((LPCWSTR)name->data(), FindExInfoBasic, &ffd, FindExSearchNameMatch, NULL, 0); #else hFind = FindFirstFileW((LPCWSTR)name->data(), &ffd); #endif name->resize(name->size() - 5); if (hFind == INVALID_HANDLE_VALUE) { break; } bool morefiles = true; while (morefiles) { if (!(ffd.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) && (!(ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) || *ffd.cFileName != '.' || (ffd.cFileName[1] && ((ffd.cFileName[1] != '.') || ffd.cFileName[2])))) { string childname = *name; childname.append((char*)(void*)L"\\", 2); childname.append((char*)ffd.cFileName, sizeof(wchar_t) * wcslen(ffd.cFileName)); if (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { emptydirlocal(&childname , currentdev); childname.append("", 1); removed |= !!RemoveDirectoryW((LPCWSTR)childname.data()); } else { childname.append("", 1); removed |= !!DeleteFileW((LPCWSTR)childname.data()); } } morefiles = FindNextFileW(hFind, &ffd); } FindClose(hFind); if (!removed) { break; } } } #else // delete all files, folders and symlinks contained in the specified folder // (does not recurse into mounted devices) void UpdateTask::emptydirlocal(string* name, dev_t basedev) { #ifdef USE_IOS string absolutename; if (appbasepath) { if (name->size() && name->at(0) != '/') { absolutename = appbasepath; absolutename.append(*name); name = &absolutename; } } #endif DIR* dp; dirent* d; int removed; struct stat statbuf; int t; if (!basedev) { if (lstat(name->c_str(), &statbuf) || !S_ISDIR(statbuf.st_mode) || S_ISLNK(statbuf.st_mode)) return; basedev = statbuf.st_dev; } if ((dp = opendir(name->c_str()))) { for (;;) { removed = 0; while ((d = readdir(dp))) { if (d->d_type != DT_DIR || *d->d_name != '.' || (d->d_name[1] && (d->d_name[1] != '.' || d->d_name[2]))) { t = name->size(); name->append("/"); name->append(d->d_name); if (!lstat(name->c_str(), &statbuf)) { if (!S_ISLNK(statbuf.st_mode) && S_ISDIR(statbuf.st_mode) && statbuf.st_dev == basedev) { emptydirlocal(name, basedev); removed |= !rmdir(name->c_str()); } else { removed |= !unlink(name->c_str()); } } name->resize(t); } } if (!removed) { break; } rewinddir(dp); } closedir(dp); } } #endif SignatureChecker::SignatureChecker(const char *base64Key) { string pubks; size_t len = strlen(base64Key)/4*3+3; pubks.resize(len); pubks.resize(Base64::atob(base64Key, (byte *)pubks.data(), int(len))); byte *data = (byte*)pubks.data(); int datalen = int(pubks.size()); int p, i, n; p = 0; for (i = 0; i < 2; i++) { if (p + 2 > datalen) { break; } n = ((data[p] << 8) + data[p + 1] + 7) >> 3; p += 2; if (p + n > datalen) { break; } key[i] = Integer(data + p, n); p += n; } assert(i == 2 && len - p < 16); } SignatureChecker::~SignatureChecker() { } void SignatureChecker::init() { string out; out.resize(hash.DigestSize()); hash.Final((byte*)out.data()); } void SignatureChecker::add(const char *data, unsigned size) { hash.Update((const byte *)data, size); } bool SignatureChecker::checkSignature(const char *base64Signature) { byte signature[512]; int l = Base64::atob(base64Signature, signature, sizeof(signature)); if (l != sizeof(signature)) return false; string h, s; unsigned size; h.resize(hash.DigestSize()); hash.Final((byte*)h.data()); s.resize(h.size()); byte* buf = (byte *)s.data(); Integer t (signature, sizeof(signature)); t = a_exp_b_mod_c(t, key[1], key[0]); unsigned int i = t.ByteCount(); if (i > s.size()) { return 0; } while (i--) { *buf++ = t.GetByte(i); } size = t.ByteCount(); if (!size) { return 0; } if (size < h.size()) { // left-pad with 0 s.insert(0, h.size() - size, 0); s.resize(h.size()); } return s == h; } unsigned char Base64::to64(byte c) { c &= 63; if (c < 26) { return c + 'A'; } if (c < 52) { return c - 26 + 'a'; } if (c < 62) { return c - 52 + '0'; } if (c == 62) { return '-'; } return '_'; } unsigned char Base64::from64(byte c) { if ((c >= 'A') && (c <= 'Z')) { return c - 'A'; } if ((c >= 'a') && (c <= 'z')) { return c - 'a' + 26; } if ((c >= '0') && (c <= '9')) { return c - '0' + 52; } if (c == '-' || c == '+') { return 62; } if (c == '_' || c == '/') { return 63; } return 255; } int Base64::atob(const string &in, string &out) { out.resize(in.size() * 3 / 4 + 3); out.resize(Base64::atob(in.data(), (byte *) out.data(), int(out.size()))); return int(out.size()); } int Base64::atob(const char* a, byte* b, int blen) { byte c[4]; int i; int p = 0; c[3] = 0; for (;;) { for (i = 0; i < 4; i++) { if ((c[i] = from64(*a++)) == 255) { break; } } if ((p >= blen) || !i) { return p; } b[p++] = (c[0] << 2) | ((c[1] & 0x30) >> 4); if ((p >= blen) || (i < 3)) { return p; } b[p++] = (c[1] << 4) | ((c[2] & 0x3c) >> 2); if ((p >= blen) || (i < 4)) { return p; } b[p++] = (c[2] << 6) | c[3]; } } int Base64::btoa(const string &in, string &out) { out.resize(in.size() * 4 / 3 + 4); out.resize(Base64::btoa((const byte*) in.data(), int(in.size()), (char *) out.data())); return int(out.size()); } int Base64::btoa(const byte* b, int blen, char* a) { int p = 0; for (;;) { if (blen <= 0) { break; } a[p++] = to64(*b >> 2); a[p++] = to64((*b << 4) | (((blen > 1) ? b[1] : 0) >> 4)); if (blen < 2) { break; } a[p++] = to64(b[1] << 2 | (((blen > 2) ? b[2] : 0) >> 6)); if (blen < 3) { break; } a[p++] = to64(b[2]); blen -= 3; b += 3; } a[p] = 0; return p; } MEGAcmd-2.5.2_Linux/src/updater/UpdateTask.h000066400000000000000000000052041516543156300205570ustar00rootroot00000000000000#ifndef UPDATETASK_H #define UPDATETASK_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if CRYPTOPP_VERSION >= 600 && ((__cplusplus >= 201103L) || (__RPCNDR_H_VERSION__ == 500)) using byte = CryptoPP::byte; #elif __RPCNDR_H_VERSION__ != 500 typedef unsigned char byte; #endif class Base64 { static byte to64(byte); static byte from64(byte); public: static int btoa(const std::string&, std::string&); static int btoa(const byte*, int, char*); static int atob(const std::string&, std::string&); static int atob(const char*, byte*, int); }; class SignatureChecker { public: SignatureChecker(const char *base64Key); ~SignatureChecker(); void init(); void add(const char *data, unsigned size); bool checkSignature(const char *base64Signature); protected: CryptoPP::Integer key[2]; CryptoPP::SHA512 hash; }; class UpdateTask { public: explicit UpdateTask(); ~UpdateTask(); bool checkForUpdates(bool emergencyUpdater = false, bool doNotInstall = false, int _currentVersion = -1); protected: bool downloadFile(std::string url, std::string dstPath); bool processUpdateFile(FILE *fd); bool fileExist(const char* path); void initSignature(); void addToSignature(const char *bytes, size_t length); bool checkSignature(std::string value); bool alreadyInstalled(std::string relativePath, std::string fileSignature); bool alreadyDownloaded(std::string relativePath, std::string fileSignature); bool alreadyExists(std::string absolutePath, std::string fileSignature); bool performUpdate(); void rollbackUpdate(size_t fileNum); void initialCleanup(); void finalCleanup(); bool setPermissions(const char *path); bool removeRecursively(std::string path); std::string getAppDataDir(); std::string getAppDir(); std::string readNextLine(FILE *fd); void emptydirlocal(std::string* name, dev_t basedev = 0); std::string appFolder; std::string appDataFolder; std::string updateFolder; std::string backupFolder; bool isPublic; SignatureChecker *signatureChecker; unsigned int currentFile; int updateVersion; int currentVersion; std::vector downloadURLs; std::vector localPaths; std::vector fileSignatures; }; #endif // UPDATETASK_H MEGAcmd-2.5.2_Linux/tests/000077500000000000000000000000001516543156300152475ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/tests/common/000077500000000000000000000000001516543156300165375ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/tests/common/Instruments.cpp000066400000000000000000000121451516543156300216010ustar00rootroot00000000000000/** * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include "Instruments.h" TestInstruments& TestInstruments::Instance() { static TestInstruments instance; return instance; } void TestInstruments::clearAll() { std::lock_guard lock(mMutex); mFlags.clear(); clearEvents(); mProbes.clear(); } bool TestInstruments::flag(Flag flag) { std::lock_guard lock(mMutex); return mFlags.find(flag) != mFlags.end(); } void TestInstruments::setFlag(Flag flag) { std::lock_guard lock(mMutex); mFlags.insert(flag); } void TestInstruments::resetFlag(Flag flag) { std::lock_guard lock(mMutex); mFlags.erase(flag); } void TestInstruments::clearFlags() { std::lock_guard lock(mMutex); mFlags.clear(); } void TestInstruments::throwIfFlagAndReset(TestInstruments::Flag flag) { std::lock_guard lock(mMutex); if (mFlags.find(flag) != mFlags.end()) { mFlags.erase(flag); throw InstrumentsException(); } } void TestInstruments::onEventOnce(Event event, EventCallback&& handler) { std::lock_guard lock(mMutex); mSingleEventHandlers.insert_or_assign(event, std::move(handler)); } void TestInstruments::onEventOnce(Event event, EventCallback& handler) { std::lock_guard lock(mMutex); mSingleEventHandlers.insert_or_assign(event, handler); } void TestInstruments::clearEvent(Event event) { std::lock_guard lock(mMutex); mSingleEventHandlers.erase(event); mEventMultiHandlers.erase(event); } void TestInstruments::clearEvent(MegaCmdEvent event) { std::lock_guard lock(mMutex); mMegaCmdSingleEventHandlers.erase(event); mMegaCmdEventMultiHandlers.erase(event); } void TestInstruments::fireEvent(Event event) { std::vector handlersToExecute; { std::lock_guard lock(mMutex); auto handlerIt = mSingleEventHandlers.find(event); if (handlerIt != mSingleEventHandlers.end()) { handlersToExecute.push_back(std::move(handlerIt->second)); mSingleEventHandlers.erase(handlerIt); } auto range = mEventMultiHandlers.equal_range(event); for (auto handlerIt = range.first; handlerIt != range.second; handlerIt++) { handlerIt->second(); } } for (auto &handler : handlersToExecute) { handler(); } } void TestInstruments::fireEvent(MegaCmdEvent event) { std::vector handlersToExecute; { std::lock_guard lock(mMutex); auto handlerIt = mMegaCmdSingleEventHandlers.find(event); if (handlerIt != mMegaCmdSingleEventHandlers.end()) { handlersToExecute.push_back(std::move(handlerIt->second)); mMegaCmdSingleEventHandlers.erase(handlerIt); } auto range = mMegaCmdEventMultiHandlers.equal_range(event); for (auto handlerIt = range.first; handlerIt != range.second; handlerIt++) { handlerIt->second(); } } for (auto &handler : handlersToExecute) { handler(); } } void TestInstruments::clearEvents() { std::lock_guard lock(mMutex); mSingleEventHandlers.clear(); mMegaCmdSingleEventHandlers.clear(); mEventMultiHandlers.clear(); mMegaCmdEventMultiHandlers.clear(); } std::optional TestInstruments::testValue(TestValue key) { std::lock_guard lock(mMutex); auto it = mTestValues.find(key); if (it != mTestValues.end()) { return it->second; } return std::nullopt; } void TestInstruments::setTestValue(TestValue key, TestValue_t value) { std::lock_guard lock(mMutex); mTestValues.insert_or_assign(key, value); } void TestInstruments::resetTestValue(TestValue key) { std::lock_guard lock(mMutex); mTestValues.erase(key); } void TestInstruments::increaseTestValue(TestValue key) { std::lock_guard lock(mMutex); auto it = mTestValues.find(key); if (it == mTestValues.end()) { return; } mTestValues.insert_or_assign(key, ++std::get(it->second)); } void TestInstruments::clearTestValues() { std::lock_guard lock(mMutex); mTestValues.clear(); } bool TestInstruments::probe(const std::string &str) { std::lock_guard lock(mMutex); return mProbes.find(str) != mProbes.end(); } void TestInstruments::clearProbes() { std::lock_guard lock(mMutex); mProbes.clear(); } MEGAcmd-2.5.2_Linux/tests/common/Instruments.h000066400000000000000000000277771516543156300212670ustar00rootroot00000000000000/** * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #pragma once //#include "EventsDefinitions.h" //TODO: Have the above use MEGAcmd definitions! #include #include #include #include #include #include #include #include #include #include #include using MegaCmdEvent = int; //TODO: instead have MEGAcmd events used struct InstrumentsException : public std::exception { virtual ~InstrumentsException() = default; }; class TestInstruments { public: static TestInstruments& Instance(); void clearAll(); // // Flags // enum class Flag { SOME_MEGACMD_INSTRUMENT_FLAG, }; bool flag(Flag flag); void setFlag(Flag flag); void resetFlag(Flag flag); void clearFlags(); void throwIfFlagAndReset(Flag flag); // // Events // enum class Event { SERVER_ABOUT_TO_START_WAITING_FOR_PETITIONS, SYNC_ISSUES_LIST_UPDATED, FETCH_NODES_REQ_UPDATE, }; typedef std::function EventCallback; void onEventOnce(Event event, EventCallback&&); void onEventOnce(Event event, EventCallback&); void clearEvent(Event event); void clearEvent(MegaCmdEvent event); void fireEvent(Event event); void fireEvent(MegaCmdEvent event); void clearEvents(); // // Test Values // enum class TestValue { AMIPRO_LEVEL, SYNC_ISSUES_LIST_SIZE, SYNC_ISSUE_ENFORCE_PATH_PROBLEM, SYNC_ISSUE_ENFORCE_REASON_TYPE, }; using TestValue_t = std::variant< uint64_t, int64_t, std::string, std::chrono::steady_clock::time_point>; std::optional testValue(TestValue key); void setTestValue(TestValue key, TestValue_t value); void resetTestValue(TestValue key); void increaseTestValue(TestValue key); void clearTestValues(); // // Probes // bool probe(const std::string &); template void setProbe(T&& str) { std::lock_guard lock(mMutex); mProbes.insert(std::forward(str)); } size_t probeCount() const { return mProbes.size(); } void clearProbes(); private: std::recursive_mutex mMutex; std::unordered_set mFlags; std::unordered_set mProbes; // Only triggered once, only one allowed std::unordered_map mSingleEventHandlers; std::unordered_map mMegaCmdSingleEventHandlers; // Triggering does not imply execution std::multimap mEventMultiHandlers; std::multimap mMegaCmdEventMultiHandlers; std::unordered_map mTestValues; public: template void onEventOnce(MegaCmdEvent event, Cb &&handler) { std::lock_guard lock(mMutex); mMegaCmdSingleEventHandlers.insert_or_assign(event, std::forward(handler)); } template std::multimap::iterator onEveryEvent(Event event, Cb &&handler) { std::lock_guard lock(mMutex); return mEventMultiHandlers.emplace(event, std::forward(handler)); } template std::multimap::iterator onEveryEvent(MegaCmdEvent event, Cb &&handler) { std::lock_guard lock(mMutex); return mMegaCmdEventMultiHandlers.emplace(event, std::forward(handler)); } void removeEventMultiHandle(const std::multimap::iterator &it) { std::lock_guard lock(mMutex); mEventMultiHandlers.erase(it); } void removeEventMultiHandle(const std::multimap::iterator &it) { std::lock_guard lock(mMutex); mMegaCmdEventMultiHandlers.erase(it); } /** * @brief general purpose scope guard class */ template class ScopeGuard { ExitCallback mExitCb; public: ScopeGuard(ExitCallback&& exitCb) : mExitCb{std::move(exitCb)} { } ~ScopeGuard() { mExitCb(); } }; template auto onEveryEventGuard(Event event, Cb &&handler) { auto it = onEveryEvent(event, std::forward(handler)); return ScopeGuard([this, it{std::move(it)}](){ removeEventMultiHandle(it); }); } }; class TestInstrumentsFlagGuard { const TestInstruments::Flag mFlag; public: TestInstrumentsFlagGuard(TestInstruments::Flag flag) : mFlag(flag) { TestInstruments::Instance().setFlag(mFlag); } ~TestInstrumentsFlagGuard() { TestInstruments::Instance().resetFlag(mFlag); } }; class TestInstrumentsWaitForEventGuard { bool mEventHappened; bool mDoNotWait = false; std::mutex mEventHappenedMutex; std::condition_variable mEventHappenedCv; public: TestInstrumentsWaitForEventGuard(TestInstruments::Event event) : mEventHappened(false) { TestInstruments::Instance().onEventOnce(event, [this]() { std::lock_guard guard(mEventHappenedMutex); mEventHappened = true; mEventHappenedCv.notify_all(); }); } bool disableWaitAndGetValue() { std::unique_lock lock(mEventHappenedMutex); mDoNotWait = true; return mEventHappened; } bool waitForEvent(std::chrono::duration timeout = std::chrono::seconds(30)) { std::unique_lock lock(mEventHappenedMutex); if (mDoNotWait) { return mEventHappened; } mDoNotWait = true; mEventHappenedCv.wait_for(lock, timeout, [this](){ return mEventHappened; }); EXPECT_TRUE(mEventHappened); return mEventHappened; } ~TestInstrumentsWaitForEventGuard() { waitForEvent(); } }; struct TestInstrumentsTestValueGuard { TestInstruments::TestValue mTestValueKey; TestInstrumentsTestValueGuard(TestInstruments::TestValue key) { mTestValueKey = key; } TestInstrumentsTestValueGuard(TestInstruments::TestValue key, TestInstruments::TestValue_t value) { mTestValueKey = key; TestInstruments::Instance().setTestValue(key, value); } virtual ~TestInstrumentsTestValueGuard() { TestInstruments::Instance().resetTestValue(mTestValueKey); } }; class TestInstrumentsClearGuard { public: ~TestInstrumentsClearGuard() { TestInstruments::Instance().clearAll(); } }; class TestInstrumentsProbesGuard { const std::vector mProbes; public: TestInstrumentsProbesGuard(std::vector&& probles) : mProbes(std::move(probles)) { } ~TestInstrumentsProbesGuard() { TestInstruments::Instance().clearProbes(); } bool probe() const { for (const auto& probe : mProbes) { if (!TestInstruments::Instance().probe(probe)) { return false; } } return true; } }; class TestInstrumentsEnvVarGuard { private: std::string mVar; bool mHasInitValue; std::string mInitValue; public: TestInstrumentsEnvVarGuard(std::string variable, const std::string &value) : mVar(std::move(variable)), mHasInitValue(false), mInitValue() { const char *initValue = getenv(mVar.c_str()); if (initValue != nullptr) { mHasInitValue = true; mInitValue = std::string(initValue); } #ifdef _WIN32 auto envStr = std::string(mVar).append("=").append(value); _putenv(envStr.c_str()); #else setenv(mVar.c_str(), value.c_str(), 1); #endif } virtual ~TestInstrumentsEnvVarGuard() { if (mHasInitValue) { #ifdef _WIN32 auto envStr = std::string(mVar).append("=").append(mInitValue); _putenv(envStr.c_str()); #else setenv(mVar.c_str(), mInitValue.c_str(), 1); #endif } else { #ifdef _WIN32 auto envStr = std::string(mVar).append("="); _putenv(envStr.c_str()); #else unsetenv(mVar.c_str()); #endif } } protected: explicit TestInstrumentsEnvVarGuard(std::string variable) : mVar(std::move(variable)), mHasInitValue(false), mInitValue() { const char *initValue = getenv(mVar.c_str()); if (initValue != nullptr) { mHasInitValue = true; mInitValue = std::string(initValue); } #ifdef _WIN32 auto envStr = std::string(mVar).append("="); _putenv(envStr.c_str()); #else unsetenv(mVar.c_str()); #endif } }; #ifdef WIN32 class TestInstrumentsEnvVarGuardW { std::wstring mVar; bool mHasInitValue; std::wstring mInitValue; public: TestInstrumentsEnvVarGuardW(const std::wstring& variable, const std::wstring& value) : mVar(variable), mHasInitValue(false), mInitValue() { const wchar_t* initValue = _wgetenv(mVar.c_str()); if (initValue != nullptr) { mHasInitValue = true; mInitValue = std::wstring(initValue); } _wputenv_s(mVar.c_str(), value.c_str()); } virtual ~TestInstrumentsEnvVarGuardW() { if (mHasInitValue) { _wputenv_s(mVar.c_str(), mInitValue.c_str()); } else { _wputenv_s(mVar.c_str(), L""); } } }; #endif class TestInstrumentsUnsetEnvVarGuard : TestInstrumentsEnvVarGuard { public: explicit TestInstrumentsUnsetEnvVarGuard(std::string variable) : TestInstrumentsEnvVarGuard(variable) { } }; /** * Convenience class that tracks the occurrence of events within its lifetime. * * Caveat: onEventOnce only allows for a single handler, * this will install/replace event handlers for tracked events * These handlers will be removed after EventTracker dtor */ class EventTracker { using TI = TestInstruments; std::mutex mMutex; std::condition_variable mCV; std::map, bool> mTrackedEvents; void initializeTracking() { for (auto &pTracked : mTrackedEvents) { std::visit([this, &pTracked](const auto & event){ TI::Instance().onEventOnce(event, [this, &pTracked](){ pTracked.second = true; mCV.notify_all(); }); }, pTracked.first); } } public: EventTracker(const std::vector> &events) { for (auto &event : events) { mTrackedEvents[event] = false; } initializeTracking(); } ~EventTracker() { for (auto &pTracked : mTrackedEvents) { std::visit([](const auto & event){ TI::Instance().clearEvent(event); }, pTracked.first); } } bool eventHappened(const std::variant &event) { std::unique_lock lock(mMutex); assert(mTrackedEvents.find(event) != mTrackedEvents.end()); return mTrackedEvents[event]; } template > bool waitForEvent(const std::variant &event, std::chrono::duration timeout = std::chrono::seconds(30)) { std::unique_lock lock(mMutex); mCV.wait_for(lock, timeout, [this, &event](){ return mTrackedEvents[event]; }); return mTrackedEvents[event]; } }; MEGAcmd-2.5.2_Linux/tests/common/TestUtils.cpp000066400000000000000000000006721516543156300212100ustar00rootroot00000000000000/** * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include "TestUtils.h" MEGAcmd-2.5.2_Linux/tests/common/TestUtils.h000066400000000000000000000201571516543156300206550ustar00rootroot00000000000000/** * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #pragma once #include #include #include #ifdef _WIN32 #include #else #include #endif #include #include #include #include #include #include "megacmdcommonutils.h" #define ULOG_COLOR(color, fmt, ...) \ do { if (isatty(1)) printf(color fmt "\033[0m\n", ##__VA_ARGS__); \ else printf(fmt "\n", ##__VA_ARGS__); \ } while(0) #define ULOG(fmt, ...) ULOG_COLOR("\033[38;5;250m", fmt, ##__VA_ARGS__) #define ULOG_ERR(fmt, ...) ULOG_COLOR("\033[31m", fmt, ##__VA_ARGS__) #define ULOG_WARN(fmt, ...) ULOG_COLOR("\033[33m", fmt, ##__VA_ARGS__) #define ULOG_NOTE(fmt, ...) ULOG_COLOR("\033[38;5;81m", fmt, ##__VA_ARGS__) namespace gti { enum GTestColor { COLOR_DEFAULT, COLOR_RED, COLOR_GREEN, COLOR_YELLOW }; static void ColoredPrintf(GTestColor color, const char* fmt, ...) { #ifndef _WIN32 va_list args; va_start(args, fmt); #ifdef WIN32 static const bool in_color_mode = _isatty(_fileno(stdout)) != 0; #else static const bool in_color_mode = isatty(STDOUT_FILENO) != 0; #endif const bool use_color = in_color_mode && (color != GTestColor::COLOR_DEFAULT); if (use_color) { printf("\033[0;3%sm", std::to_string(static_cast(color)).c_str()); } vprintf(fmt, args); if (use_color) { printf("\033[m"); } va_end(args); #endif } } class GTestMessager { using GTestColor = gti::GTestColor; std::ostringstream mOstr; GTestColor mColor; std::string mTitle; public: GTestMessager(GTestColor color, std::string title = {}) : mColor(color), mTitle(title) { } ~GTestMessager() { gti::ColoredPrintf(GTestColor::COLOR_GREEN, "[ %8s ] ", mTitle.c_str()); gti::ColoredPrintf(GTestColor::COLOR_DEFAULT, "%s\n", mOstr.str().c_str()); } template GTestMessager& operator<<(T* obj) { if (obj) { mOstr << obj; } else { mOstr << "(NULL)"; } return *this; } template ::value>::type> GTestMessager& operator<<(const T obj) { static_assert(!std::is_same::value, "T cannot be nullptr_t"); mOstr << obj; return *this; } template ::value>::type> GTestMessager& operator<<(const T& obj) { mOstr << obj; return *this; } }; // Fancy message defines #define G_TEST_TITLED_MSG(title) GTestMessager(gti::COLOR_GREEN, title) #define G_TEST_MSG GTestMessager(gti::COLOR_GREEN) #define G_TEST_INFO GTestMessager(gti::COLOR_GREEN, "INFO") #define G_SUBTEST GTestMessager(gti::COLOR_GREEN, "SUBTEST") #define G_SUBSUBTEST GTestMessager(gti::COLOR_GREEN, "SSUBTEST") // For some reason, the regex matches included with GTest fail to match against // regex expressions using ERE syntax. The following two custom matchers use std::regex, // which provides consistent, cross-platform behavior, as opposed to GTest's regex // peculiarities (https://google.github.io/googletest/advanced.html#regular-expression-syntax) MATCHER_P(MatchesStdRegex, pattern, "") { *result_listener << "where the string '" << arg << "' matches the std::regex '" << pattern << "'"; std::regex regex(pattern); return std::regex_match(arg, regex); } MATCHER_P(ContainsStdRegex, pattern, "") { *result_listener << "where the string '" << arg << "' contains the std::regex '" << pattern << "'"; std::regex regex(pattern); return std::regex_search(arg, regex); } class SelfDeletingTmpFolder { fs::path mTempDir; public: SelfDeletingTmpFolder() { mTempDir = fs::temp_directory_path() / fs::path(megacmd::generateRandomAlphaNumericString(10)); fs::create_directory(mTempDir); } SelfDeletingTmpFolder(const fs::path& subPath) : SelfDeletingTmpFolder() { mTempDir /= subPath; fs::create_directories(mTempDir); } ~SelfDeletingTmpFolder() { fs::remove_all(mTempDir); } std::string string() const { return mTempDir.string(); } fs::path path() const { return mTempDir; } }; #ifdef WIN32 /** * @brief Google tests outputing needs stdout mode to be _O_TEXT * otherwise low level printing will fail. * This is a custom listener to wrapp writing routines */ struct CustomTestEventListener : public ::testing::EmptyTestEventListener { std::unique_ptr<::testing::TestEventListener> mDefault; void OnTestProgramStart(const ::testing::UnitTest& unit_test) override { megacmd::WindowsNarrowStdoutGuard smg; mDefault->OnTestProgramStart(unit_test); } void OnTestIterationStart(const ::testing::UnitTest& unit_test, int iteration) override { megacmd::WindowsNarrowStdoutGuard smg; mDefault->OnTestIterationStart(unit_test, iteration); } void OnEnvironmentsSetUpStart(const ::testing::UnitTest& unit_test) override { megacmd::WindowsNarrowStdoutGuard smg; mDefault->OnEnvironmentsSetUpStart(unit_test); } void OnEnvironmentsSetUpEnd(const ::testing::UnitTest& unit_test) override { megacmd::WindowsNarrowStdoutGuard smg; mDefault->OnEnvironmentsSetUpEnd(unit_test); } void OnTestSuiteStart(const ::testing::TestSuite& test_suite) override { megacmd::WindowsNarrowStdoutGuard smg; mDefault->OnTestSuiteStart(test_suite); } #ifndef GTEST_REMOVE_LEGACY_TEST_CASEAPI_ void OnTestCaseStart(const ::testing::TestCase& test_case) override { megacmd::WindowsNarrowStdoutGuard smg; mDefault->OnTestCaseStart(test_case); } #endif // GTEST_REMOVE_LEGACY_TEST_CASEAPI_ void OnTestStart(const ::testing::TestInfo& test_info) override { megacmd::WindowsNarrowStdoutGuard smg; mDefault->OnTestStart(test_info); } void OnTestDisabled(const ::testing::TestInfo& test_info) override { megacmd::WindowsNarrowStdoutGuard smg; mDefault->OnTestDisabled(test_info); } void OnTestPartResult(const ::testing::TestPartResult& result) override { megacmd::WindowsNarrowStdoutGuard smg; mDefault->OnTestPartResult(result); } void OnTestEnd(const ::testing::TestInfo& test_info) override { megacmd::WindowsNarrowStdoutGuard smg; mDefault->OnTestEnd(test_info); } void OnTestSuiteEnd(const ::testing::TestSuite& test_suite) override { megacmd::WindowsNarrowStdoutGuard smg; mDefault->OnTestSuiteEnd(test_suite); } #ifndef GTEST_REMOVE_LEGACY_TEST_CASEAPI_ void OnTestCaseEnd(const ::testing::TestCase& test_case) override { megacmd::WindowsNarrowStdoutGuard smg; mDefault->OnTestCaseEnd(test_case); } #endif // GTEST_REMOVE_LEGACY_TEST_CASEAPI_ void OnEnvironmentsTearDownStart(const ::testing::UnitTest& unit_test) override { megacmd::WindowsNarrowStdoutGuard smg; mDefault->OnEnvironmentsTearDownStart(unit_test); } void OnEnvironmentsTearDownEnd(const ::testing::UnitTest& unit_test) override { megacmd::WindowsNarrowStdoutGuard smg; mDefault->OnEnvironmentsTearDownEnd(unit_test); } void OnTestIterationEnd(const ::testing::UnitTest& unit_test, int iteration) override { megacmd::WindowsNarrowStdoutGuard smg; mDefault->OnTestIterationEnd(unit_test, iteration); } void OnTestProgramEnd(const ::testing::UnitTest& unit_test) override { megacmd::WindowsNarrowStdoutGuard smg; mDefault->OnTestProgramEnd(unit_test); } }; #endif MEGAcmd-2.5.2_Linux/tests/integration/000077500000000000000000000000001516543156300175725ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/tests/integration/BasicTests.cpp000066400000000000000000000114711516543156300223460ustar00rootroot00000000000000/** * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include #include #include "MegaCmdTestingTools.h" #include "Instruments.h" #include "TestUtils.h" using TI = TestInstruments; TEST_F(NOINTERACTIVEBasicTest, Version) { executeInClient({"version"}); } TEST_F(NOINTERACTIVEBasicTest, Help) { executeInClient({"help"}); } TEST_F(NOINTERACTIVENotLoggedTest, Folderlogin) { { auto rLoging = executeInClient({"login", LINK_TESTEXPORTFOLDER}); ASSERT_TRUE(rLoging.ok()); } { auto rLogout = executeInClient({"logout", "--keep-session"}); ASSERT_TRUE(rLogout.ok()); } { EventTracker evTracker({TestInstruments::Event::FETCH_NODES_REQ_UPDATE}); auto rLoging = executeInClient({"login", "--resume", LINK_TESTEXPORTFOLDER}); ASSERT_TRUE(rLoging.ok()); ASSERT_STREQ(rLoging.out().c_str(), ""); ASSERT_STREQ(rLoging.err().c_str(), ""); ASSERT_FALSE(evTracker.eventHappened(TestInstruments::Event::FETCH_NODES_REQ_UPDATE)); } } TEST_F(NOINTERACTIVEReadTest, Find) { auto r = executeInClient({"find"}); ASSERT_TRUE(r.ok()); std::vector result_paths = splitByNewline(r.out()); ASSERT_THAT(result_paths, testing::Not(testing::IsEmpty())); EXPECT_THAT(result_paths, testing::Contains(".")); EXPECT_THAT(result_paths, testing::Contains("testReadingFolder01")); EXPECT_THAT(result_paths, testing::Contains("testReadingFolder01/file03.txt")); EXPECT_THAT(result_paths, testing::Contains("testReadingFolder01/folder01/file03.txt")); EXPECT_THAT(result_paths, testing::Contains("testReadingFolder01/folder02/subfolder03/file02.txt")); } TEST_F(NOINTERACTIVELoggedInTest, Whoami) { { G_SUBTEST << "Basic"; auto r = executeInClient({"whoami"}); ASSERT_TRUE(r.ok()); auto out = r.out(); ASSERT_THAT(out, testing::Not(testing::IsEmpty())); ASSERT_THAT(getenv("MEGACMD_TEST_USER"), testing::NotNull()); EXPECT_EQ(out, std::string("Account e-mail: ").append(getenv("MEGACMD_TEST_USER")).append("\n")); } { G_SUBTEST << "Extended"; { // Ensure there's at least a file around contributing to ROOT usage: auto result = executeInClient({"import", LINK_TESTEXPORTFILE01TXT}); ASSERT_TRUE(result.ok()) << "could not import testExportFile01.txt"; } auto r = executeInClient({"whoami", "-l"}); ASSERT_TRUE(r.ok()); std::vector details_out = splitByNewline(r.out()); ASSERT_THAT(details_out, testing::Not(testing::IsEmpty())); ASSERT_THAT(details_out, testing::Contains(testing::HasSubstr("Available storage:"))); ASSERT_THAT(details_out, testing::Contains(testing::HasSubstr("Pro level:"))); ASSERT_THAT(details_out, testing::Contains(testing::HasSubstr("Current Session"))); ASSERT_THAT(details_out, testing::Contains(testing::HasSubstr("In RUBBISH:"))); EXPECT_THAT(details_out, testing::Not(testing::Contains(ContainsStdRegex("Available storage:\\s+0\\.00\\s+Bytes")))); EXPECT_THAT(details_out, testing::Not(testing::Contains(ContainsStdRegex("In ROOT:\\s+0\\.00\\s+Bytes")))); } } TEST_F(NOINTERACTIVEBasicTest, EchoInvalidUtf8) { const std::string validUtf8 = u8"\uc548\uc548\ub155\ud558\uc138\uc694\uc138\uacc4"; #ifdef _WIN32 // Required because windows converts the args to UTF-16 before sending them to the server, // so we can't pass an invalid UTF-8 string all the way to the executer; we need to escape it const std::string invalidUtf8 = ""; #else const std::string invalidUtf8 = "\xf0\x8f\xbf\xbf"; #endif auto result = executeInClient({"echo", validUtf8}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr(validUtf8)); result = executeInClient({"echo", invalidUtf8}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::Not(testing::HasSubstr(invalidUtf8))); result = executeInClient({"echo", "--log-as-err", validUtf8}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.err(), testing::HasSubstr(validUtf8)); result = executeInClient({"echo", "--log-as-err", invalidUtf8}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.err(), testing::HasSubstr("")); EXPECT_THAT(result.err(), testing::Not(testing::HasSubstr(invalidUtf8))); EXPECT_THAT(result.out(), testing::Not(testing::HasSubstr(invalidUtf8))); } MEGAcmd-2.5.2_Linux/tests/integration/CatTests.cpp000066400000000000000000000052401516543156300220310ustar00rootroot00000000000000/** * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include #include #include "MegaCmdTestingTools.h" #include "TestUtils.h" class CatTests : public NOINTERACTIVELoggedInTest { SelfDeletingTmpFolder mTmpDir; void SetUp() override { NOINTERACTIVELoggedInTest::SetUp(); TearDown(); } void TearDown() override { auto result = executeInClient({"rm", "-f", fileName}); NOINTERACTIVELoggedInTest::TearDown(); } protected: const std::string fileName = "file.txt"; fs::path localPath() const { return mTmpDir.path(); } }; TEST_F(CatTests, NoFile) { auto result = executeInClient({"cat", fileName}); ASSERT_FALSE(result.ok()); } TEST_F(CatTests, AsciiContents) { const fs::path filePath = localPath() / "file_ascii.txt"; const std::string contents = "Hello world!"; { std::ofstream file(filePath); file << contents; } auto result = executeInClient({"put", filePath.string(), fileName}); ASSERT_TRUE(result.ok()); result = executeInClient({"cat", fileName}); ASSERT_TRUE(result.ok()); EXPECT_EQ(contents, result.out()); } TEST_F(CatTests, NonAsciiContents) { const fs::path filePath = localPath() / "file_non_ascii.txt"; const std::string contents = u8"\u3053\u3093\u306b\u3061\u306f\u3001\u4e16\u754c"; { std::ofstream file(filePath, std::ios::binary); file << contents; } auto result = executeInClient({"put", filePath.string(), fileName}); ASSERT_TRUE(result.ok()); result = executeInClient({"cat", fileName}); ASSERT_TRUE(result.ok()); EXPECT_EQ(contents, result.out()); } TEST_F(CatTests, NonAsciiContentsWithNewlines) { const fs::path filePath = localPath() / "file_non_ascii_newlines.txt"; const std::string contents = u8"\u3053\u3093\u306b\u3061\r\n\u306f\u3001\n\u4e16\u754c"; { std::ofstream file(filePath, std::ios::binary); file << contents; } auto result = executeInClient({"put", filePath.string(), fileName}); ASSERT_TRUE(result.ok()); result = executeInClient({"cat", fileName}); ASSERT_TRUE(result.ok()); EXPECT_EQ(contents, result.out()); } MEGAcmd-2.5.2_Linux/tests/integration/ExportTests.cpp000066400000000000000000000266131516543156300226120ustar00rootroot00000000000000/** * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include #include #include "MegaCmdTestingTools.h" #include "TestUtils.h" using TI = TestInstruments; class ExportTest : public NOINTERACTIVELoggedInTest { protected: void SetUp() override { NOINTERACTIVELoggedInTest::SetUp(); // Do not assert the output of this; we just want to ensure a clean state in the setup executeInClient({"rm", "-r", "-f", "testExportFolder"}); executeInClient({"rm", "-f", "testExportFile01.txt"}); auto result = executeInClient({"import", LINK_TESTEXPORTFOLDER}); ASSERT_TRUE(result.ok()) << "could not import testExportFolder"; ASSERT_TRUE(executeInClient({"ls", "testExportFolder"}).ok()) << "could not find folder testExportFolder"; result = executeInClient({"import", LINK_TESTEXPORTFILE01TXT}); ASSERT_TRUE(result.ok()) << "could not import testExportFile01.txt"; ASSERT_TRUE(executeInClient({"ls", "testExportFile01.txt"}).ok()) << "could not find file testExportFile01.txt"; } void TearDown() override { ASSERT_TRUE(executeInClient({"rm", "-r", "-f", "testExportFolder"}).ok()) << "could not delete folder testExportFolder"; ASSERT_TRUE(executeInClient({"rm", "-f", "testExportFile01.txt"}).ok()) << "could not delete file testExportFile01.txt"; NOINTERACTIVELoggedInTest::TearDown(); } }; namespace { // We'll use these regex to verify the links and authentication keys are well-formed // Links end in #, whereas tokens end in #: // is simply alphanumeric, while and can also contain '-' or '_' // Since GTest uses a very simple Regex implementation on Windows, we cannot use groups or brackets (see: https://google.github.io/googletest/advanced.html#regular-expression-syntax) const std::string megaFileLinkRegex(R"(https:\/\/mega.nz\/file\/\w+\#\S+)"); const std::string megaFolderLinkRegex(R"(https:\/\/mega.nz\/folder\/\w+\#\S+)"); const std::string megaPasswordLinkRegex(R"(https:\/\/mega.nz\/\S+)"); const std::string authTokenRegex(R"(\w+\#\S+\:\S+)"); } TEST_F(ExportTest, Basic) { { G_SUBTEST << "File"; const std::string file_path = "testExportFile01.txt"; auto rExport = executeInClient({"export", file_path}); ASSERT_FALSE(rExport.ok()); EXPECT_THAT(rExport.out(), testing::HasSubstr(file_path + " is not exported")); EXPECT_THAT(rExport.out(), testing::HasSubstr("Use -a to export it")); auto rCreate = executeInClient({"export", "-a", "-f", file_path}); ASSERT_TRUE(rCreate.ok()); EXPECT_THAT(rCreate.out(), testing::HasSubstr("Exported /" + file_path)); EXPECT_THAT(rCreate.out(), ContainsStdRegex(megaFileLinkRegex)); // Verify it shows up as exported rExport = executeInClient({"export", file_path}); ASSERT_TRUE(rExport.ok()); EXPECT_THAT(rExport.out(), testing::HasSubstr(file_path)); EXPECT_THAT(rExport.out(), testing::HasSubstr("shared as exported permanent file link")); EXPECT_THAT(rExport.out(), ContainsStdRegex(megaFileLinkRegex)); auto rDisable = executeInClient({"export", "-d", file_path}); ASSERT_TRUE(rDisable.ok()); EXPECT_THAT(rDisable.out(), testing::StartsWith("Disabled export: /" + file_path)); // Again, verify it's not exported rExport = executeInClient({"export", file_path}); ASSERT_FALSE(rExport.ok()); EXPECT_THAT(rExport.out(), testing::HasSubstr(file_path + " is not exported")); EXPECT_THAT(rExport.out(), testing::HasSubstr("Use -a to export it")); } { G_SUBTEST << "Directory"; const std::string dir_path = "testExportFolder"; auto rExport = executeInClient({"export", dir_path}); ASSERT_FALSE(rExport.ok()); EXPECT_THAT(rExport.out(), testing::HasSubstr("Couldn't find anything exported below")); EXPECT_THAT(rExport.out(), testing::HasSubstr(dir_path)); EXPECT_THAT(rExport.out(), testing::HasSubstr("Use -a to export it")); auto rCreate = executeInClient({"export", "-a", "-f", dir_path}); ASSERT_TRUE(rCreate.ok()); EXPECT_THAT(rCreate.out(), testing::HasSubstr("Exported /" + dir_path)); EXPECT_THAT(rCreate.out(), ContainsStdRegex(megaFolderLinkRegex)); // Verify it shows up as exported rExport = executeInClient({"export", dir_path}); ASSERT_TRUE(rExport.ok()); EXPECT_THAT(rExport.out(), testing::HasSubstr(dir_path)); EXPECT_THAT(rExport.out(), testing::HasSubstr("shared as exported permanent folder link")); EXPECT_THAT(rExport.out(), ContainsStdRegex(megaFolderLinkRegex)); auto rDisable = executeInClient({"export", "-d", dir_path}); ASSERT_TRUE(rDisable.ok()); EXPECT_THAT(rDisable.out(), testing::StartsWith("Disabled export: /" + dir_path)); // Again, verify it's not exported rExport = executeInClient({"export", dir_path}); ASSERT_FALSE(rExport.ok()); EXPECT_THAT(rExport.out(), testing::HasSubstr("Couldn't find anything exported below")); EXPECT_THAT(rExport.out(), testing::HasSubstr(dir_path)); EXPECT_THAT(rExport.out(), testing::HasSubstr("Use -a to export it")); } } TEST_F(ExportTest, FailedRecreation) { const std::string file_path = "testExportFile01.txt"; const std::vector createCommand{"export", "-a", "-f", file_path}; auto rCreate = executeInClient(createCommand); ASSERT_TRUE(rCreate.ok()); EXPECT_THAT(rCreate.out(), testing::HasSubstr("Exported /" + file_path)); EXPECT_THAT(rCreate.out(), ContainsStdRegex(megaFileLinkRegex)); rCreate = executeInClient(createCommand); ASSERT_FALSE(rCreate.ok()); EXPECT_THAT(rCreate.err(), testing::HasSubstr(file_path + " is already exported")); auto rDisable = executeInClient({"export", "-d", file_path}); ASSERT_TRUE(rDisable.ok()); EXPECT_THAT(rDisable.out(), testing::StartsWith("Disabled export: /" + file_path)); } TEST_F(ExportTest, Writable) { const std::string dir_path = "testExportFolder"; auto rOnlyWritable = executeInClient({"export", "--writable", dir_path}); ASSERT_FALSE(rOnlyWritable.ok()); EXPECT_THAT(rOnlyWritable.err(), testing::HasSubstr("Option can only be used when adding an export")); auto rCreate = executeInClient({"export", "-a", "-f", "--writable", dir_path}); ASSERT_TRUE(rCreate.ok()); EXPECT_THAT(rCreate.out(), testing::HasSubstr("Exported /" + dir_path)); EXPECT_THAT(rCreate.out(), ContainsStdRegex(megaFolderLinkRegex)); EXPECT_THAT(rCreate.out(), ContainsStdRegex("AuthToken = " + authTokenRegex)); // Verify the authToken is also present when checking the export (not just when creating it) auto rCheck = executeInClient({"export", dir_path}); ASSERT_TRUE(rCheck.ok()); EXPECT_THAT(rCheck.out(), testing::HasSubstr(dir_path)); EXPECT_THAT(rCheck.out(), testing::HasSubstr("shared as exported permanent folder link")); EXPECT_THAT(rCheck.out(), ContainsStdRegex(megaFolderLinkRegex)); EXPECT_THAT(rCheck.out(), ContainsStdRegex("AuthToken=" + authTokenRegex)); auto rDisable = executeInClient({"export", "-d", dir_path}); ASSERT_TRUE(rDisable.ok()); EXPECT_THAT(rDisable.out(), testing::StartsWith("Disabled export: /" + dir_path)); } TEST_F(ExportTest, NestedDirectoryStructure) { { G_SUBTEST << "Directory"; const std::string dir_path = "testExportFolder/subDirectoryExport"; auto rCreate = executeInClient({"export", "-a", "-f", dir_path}); ASSERT_TRUE(rCreate.ok()); EXPECT_THAT(rCreate.out(), testing::HasSubstr("Exported /" + dir_path)); EXPECT_THAT(rCreate.out(), ContainsStdRegex(megaFolderLinkRegex)); auto rExport = executeInClient({"export", dir_path}); ASSERT_TRUE(rExport.ok()); EXPECT_THAT(rExport.out(), testing::HasSubstr(dir_path)); EXPECT_THAT(rExport.out(), testing::HasSubstr("shared as exported permanent folder link")); EXPECT_THAT(rExport.out(), ContainsStdRegex(megaFolderLinkRegex)); auto rDisable = executeInClient({"export", "-d", dir_path}); ASSERT_TRUE(rDisable.ok()); EXPECT_THAT(rDisable.out(), testing::StartsWith("Disabled export: /" + dir_path)); } { G_SUBTEST << "File"; const std::string file_path = "testExportFolder/subDirectoryExport/file01.txt"; auto rCreate = executeInClient({"export", "-a", "-f", file_path}); ASSERT_TRUE(rCreate.ok()); EXPECT_THAT(rCreate.out(), testing::HasSubstr("Exported /" + file_path)); EXPECT_THAT(rCreate.out(), ContainsStdRegex(megaFileLinkRegex)); auto rExport = executeInClient({"export", file_path}); ASSERT_TRUE(rExport.ok()); EXPECT_THAT(rExport.out(), testing::HasSubstr(file_path)); EXPECT_THAT(rExport.out(), testing::HasSubstr("shared as exported permanent file link")); EXPECT_THAT(rExport.out(), ContainsStdRegex(megaFileLinkRegex)); auto rDisable = executeInClient({"export", "-d", file_path}); ASSERT_TRUE(rDisable.ok()); EXPECT_THAT(rDisable.out(), testing::StartsWith("Disabled export: /" + file_path)); } } TEST_F(ExportTest, PasswordProtected) { const std::string file_path = "testExportFile01.txt"; const std::vector createCommand{"export", "-a", "-f", "--password=SomePassword", file_path}; const std::vector disableCommand{"export", "-d", file_path}; { G_SUBTEST << "Non-pro account"; TestInstrumentsTestValueGuard proLevelValueGuard(TI::TestValue::AMIPRO_LEVEL, int64_t(0)); auto rCreate = executeInClient(createCommand); ASSERT_TRUE(rCreate.ok()); EXPECT_THAT(rCreate.err(), testing::HasSubstr("Only PRO users can protect links with passwords")); EXPECT_THAT(rCreate.err(), testing::HasSubstr("Showing UNPROTECTED link")); EXPECT_THAT(rCreate.out(), testing::HasSubstr("Exported /" + file_path)); EXPECT_THAT(rCreate.out(), ContainsStdRegex(megaFileLinkRegex)); auto rDisable = executeInClient(disableCommand); ASSERT_TRUE(rDisable.ok()); EXPECT_THAT(rDisable.out(), testing::StartsWith("Disabled export: /" + file_path)); } { G_SUBTEST << "Pro account"; TestInstrumentsTestValueGuard proLevelValueGuard(TI::TestValue::AMIPRO_LEVEL, int64_t(1)); auto rCreate = executeInClient(createCommand); ASSERT_TRUE(rCreate.ok()); EXPECT_THAT(rCreate.out(), testing::Not(testing::HasSubstr("Only PRO users can protect links with passwords"))); EXPECT_THAT(rCreate.out(), testing::Not(testing::HasSubstr("Showing UNPROTECTED link"))); EXPECT_THAT(rCreate.out(), testing::HasSubstr("Exported /" + file_path)); EXPECT_THAT(rCreate.out(), ContainsStdRegex(megaPasswordLinkRegex)); auto rDisable = executeInClient(disableCommand); ASSERT_TRUE(rDisable.ok()); EXPECT_THAT(rDisable.out(), testing::StartsWith("Disabled export: /" + file_path)); } } MEGAcmd-2.5.2_Linux/tests/integration/FuseTests.cpp000066400000000000000000000154311516543156300222270ustar00rootroot00000000000000/** * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #ifdef WITH_FUSE #include #include "TestUtils.h" #include "MegaCmdTestingTools.h" namespace fs = std::filesystem; class FuseTests : public NOINTERACTIVELoggedInTest { SelfDeletingTmpFolder mTmpDir; #ifdef _WIN32 TestInstrumentsEnvVarGuard mEnvGuard{"MEGACMD_FUSE_ALLOW_LOCAL_PATHS", "1"}; #endif void SetUp() override { NOINTERACTIVELoggedInTest::SetUp(); TearDown(); auto result = executeInClient({"mkdir", "-p", mountDirCloud() + "/"}).ok(); ASSERT_TRUE(result); result = fs::create_directory(mountDirLocal()); ASSERT_TRUE(result); } void TearDown() override { removeAllMounts(); } protected: void removeAllMounts() { auto result = executeInClient({"fuse-show"}); ASSERT_TRUE(result.ok()); auto lines = splitByNewline(result.out()); for (size_t i = 1; i < lines.size(); ++i) { if (lines[i].empty() || megacmd::startsWith(lines[i], "Use " /* skip detail usage line */)) { continue; } auto words = megacmd::split(lines[i], " "); ASSERT_TRUE(!words.empty()); const std::string mountId = words[0]; auto result = executeInClient({"fuse-disable", "--", mountId}); ASSERT_TRUE(result.ok()) << "Could not disable mount " << mountId; result = executeInClient({"fuse-remove", "--", mountId}); ASSERT_TRUE(result.ok()) << "Could not remove mount " << mountId; } } fs::path mountDirLocalPath() const { return mTmpDir.path() / "tests_integration_mount_dir"; } std::string mountDirLocal() const { return mountDirLocalPath().string(); } std::string mountDirCloud() const { return "/tests_integration_mount_dir"; } std::string qw(const std::string& str) // quote wrap { return "\"" + str + "\""; } }; TEST_F(FuseTests, NoMounts) { auto result = executeInClient({"fuse-show"}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr("There are no mounts")); } TEST_F(FuseTests, AddAndRemoveMount) { #ifdef WIN32 // Windows expects mount dir to not exist fs::remove_all(mountDirLocal()); #endif auto result = executeInClient({"fuse-add", "--disabled", mountDirLocal(), mountDirCloud()}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr("Added a new mount from " + qw(mountDirLocal()) + " to " + qw(mountDirCloud()))); result = executeInClient({"fuse-show", "--disable-path-collapse"}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::Not(testing::HasSubstr("There are no mounts"))); EXPECT_THAT(result.out(), testing::HasSubstr(mountDirLocal())); EXPECT_THAT(result.out(), testing::HasSubstr(mountDirCloud())); result = executeInClient({"fuse-show", "--only-enabled"}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr("There are no mounts")); result = executeInClient({"fuse-show", mountDirLocal()}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::ContainsRegex("Enabled:\\s*NO")); result = executeInClient({"fuse-remove", mountDirLocal()}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr("Removed mount")); EXPECT_THAT(result.out(), testing::HasSubstr("on " + qw(mountDirLocal()))); } TEST_F(FuseTests, AddMountEnabled) { #ifdef WIN32 // Windows expects mount dir to not exist fs::remove_all(mountDirLocal()); #endif auto result = executeInClient({"fuse-add", mountDirLocal(), mountDirCloud()}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr("Added a new mount from " + qw(mountDirLocal()) + " to " + qw(mountDirCloud()))); EXPECT_THAT(result.out(), testing::HasSubstr("Enabled mount")); result = executeInClient({"fuse-show", mountDirLocal()}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::ContainsRegex("Enabled:\\s*YES")); result = executeInClient({"fuse-disable", mountDirLocal()}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr("Disabled mount")); EXPECT_THAT(result.out(), testing::HasSubstr("on " + qw(mountDirLocal()))); result = executeInClient({"fuse-remove", mountDirLocal()}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr("Removed mount")); EXPECT_THAT(result.out(), testing::HasSubstr("on " + qw(mountDirLocal()))); } TEST_F(FuseTests, EnsureUniqueId) { const std::string dir1 = (mountDirLocalPath() / "dir1").string(); const std::string dir2 = (mountDirLocalPath() / "dir2").string(); #ifndef WIN32 fs::create_directory(dir1); fs::create_directory(dir2); #endif auto result = executeInClient({"fuse-add", dir1, mountDirCloud(), "--name=name1"}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr("Added a new mount from " + qw(dir1) + " to " + qw(mountDirCloud()))); EXPECT_THAT(result.out(), testing::HasSubstr("Enabled mount")); result = executeInClient({"fuse-add", dir2, mountDirCloud(), "--name=name2"}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr("Added a new mount from " + qw(dir2) + " to " + qw(mountDirCloud()))); EXPECT_THAT(result.out(), testing::HasSubstr("Enabled mount")); result = executeInClient({"fuse-show"}); ASSERT_TRUE(result.ok()); auto lines = splitByNewline(result.out()); ASSERT_EQ(lines.size(), 5); // Column names + 2 mounts + newline + detail usage const std::string firstMountId = megacmd::split(lines[1], " ")[0]; const std::string secondMountId = megacmd::split(lines[2], " ")[0]; EXPECT_NE(firstMountId, secondMountId); // change names result = executeInClient({"fuse-config", "name2", "--name=name1"}); ASSERT_FALSE(result.ok()); // not allowed: name clash result = executeInClient({"fuse-config", "name2", "--name=name3"}); ASSERT_TRUE(result.ok()); // allowed { result = executeInClient({"fuse-show"}); ASSERT_TRUE(result.ok()); auto lines = splitByNewline(result.out()); ASSERT_EQ(lines.size(), 5); // Column names + 2 mounts + newline + detail usage const std::string secondMountId = megacmd::split(lines[2], " ")[0]; EXPECT_EQ(secondMountId, "name3"); } removeAllMounts(); } #endif // WITH_FUSE MEGAcmd-2.5.2_Linux/tests/integration/MegaCmdTestingTools.cpp000066400000000000000000000056041516543156300241570ustar00rootroot00000000000000/** * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include #include #include "MegaCmdTestingTools.h" std::vector splitByNewline(const std::string& str) { std::vector result; std::string line; std::istringstream iss(str); while (std::getline(iss, line)) { result.push_back(line); } return result; } std::string joinString(const std::vector &vec, std::string_view sep) { std::ostringstream oss; for (size_t i = 0; i < vec.size(); ++i) { if (i) oss << sep; oss << vec[i]; } return oss.str(); } ClientResponse executeInClient(const std::vector& command, bool /*nonInteractive: TODO: give support to shell execs*/) { // To manage the memory of the first arg const std::string firstArg("args0_test_client"); std::vector args{const_cast(firstArg.c_str())}; for (const auto& word : command) { args.push_back(const_cast(word.c_str())); } args.push_back(nullptr); OUTSTRINGSTREAM stream; OUTSTRINGSTREAM streamErr; auto code = megacmd::executeClient(static_cast(args.size() - 1), args.data(), stream, streamErr); return {code, stream, streamErr}; } bool isServerLogged() { return executeInClient({"whoami"}).ok(); } void ensureLoggedIn() { if (!isServerLogged()) { const char* user = getenv("MEGACMD_TEST_USER"); const char* pass = getenv("MEGACMD_TEST_PASS"); ASSERT_THAT(user, testing::NotNull()) << "Missing testing user env variable. Ensure that MEGACMD_TEST_USER is set!"; ASSERT_STRNE(user, "") << "Missing testing user env variable. Ensure that MEGACMD_TEST_USER is set!"; ASSERT_THAT(pass, testing::NotNull()) << "Missing testing user password env variable. Ensure that MEGACMD_TEST_PASS is set!"; ASSERT_STRNE(pass, "") << "Missing testing user password env variable. Ensure that MEGACMD_TEST_PASS is set!"; auto result = executeInClient({"login", user, pass}); ASSERT_TRUE(result.ok()); result = executeInClient({"whoami"}); ASSERT_TRUE(result.ok()); ASSERT_THAT(result.out(), testing::HasSubstr(user)); } } bool hasReadStructure() { return executeInClient({"ls", "testReadingFolder01"}).ok(); } void ensureReadStructure() { if (!hasReadStructure()) { auto result = executeInClient({"import", LINK_TESTREADINGFOLDER01}); ASSERT_TRUE(result.ok()); } } MEGAcmd-2.5.2_Linux/tests/integration/MegaCmdTestingTools.h000066400000000000000000000075021516543156300236230ustar00rootroot00000000000000/** * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #pragma once #include #include "megacmdcommonutils.h" #include "megacmd.h" #include "client/megacmdclient.h" #include "megacmdlogger.h" #include #include "Instruments.h" #include #include #include constexpr const char* LINK_TESTEXPORTFILE01TXT = "https://mega.nz/file/YfNngDKR#qk9THHhxbakddRmt_tLR8OhInexzVCpPPG6M6feFfZg"; constexpr const char* LINK_TESTEXPORTFOLDER = "https://mega.nz/folder/saMRXBYL#9GETCO4E-Po45d3qSjZhbQ"; constexpr const char* LINK_TESTREADINGFOLDER01 = "https://mega.nz/folder/YPV0nCKS#bSruKSPPubdCmm5harBJOQ"; std::vector splitByNewline(const std::string& str); class ClientResponse { int mStatus = -1; OUTSTRING mErr; OUTSTRING mOut; #ifdef _WIN32 std::string mUtf8String; std::string mUtf8ErrString; #endif public: ClientResponse(int status, OUTSTRINGSTREAM &streamOut, OUTSTRINGSTREAM &streamErr) : mStatus(status) , mOut (streamOut.str()) , mErr (streamErr.str()) #ifdef _WIN32 , mUtf8String(megacmd::utf16ToUtf8(mOut)) , mUtf8ErrString(megacmd::utf16ToUtf8(mErr)) #endif {} int status() { return mStatus; } bool ok() { return !mStatus; } // returns an utf8 std::string with the response from the server std::string &out() { #ifdef _WIN32 return mUtf8String; #else return mOut; #endif } //returns an utf8 std::string with the response from the server std::string &err() { #ifdef _WIN32 return mUtf8ErrString; #else return mErr; #endif } }; std::string joinString(const std::vector &vec, std::string_view sep = " "); ClientResponse executeInClient(const std::vector& command, bool nonInteractive = true); void ensureLoggedIn(); void ensureReadStructure(); class BasicGenericTest : public ::testing::Test { }; class NotLoggedTest : public BasicGenericTest { protected: void SetUp() override { BasicGenericTest::SetUp(); auto result = executeInClient({"logout"}).ok(); ASSERT_TRUE(result); } void TearDown() override { BasicGenericTest::SetUp(); auto result = executeInClient({"logout"}).ok(); ASSERT_TRUE(result); } }; class LoggedInTest : public BasicGenericTest { protected: void removeAllSyncs() { auto result = executeInClient({"sync"}); ASSERT_TRUE(result.ok()); auto lines = splitByNewline(result.out()); for (size_t i = 1; i < lines.size(); ++i) { // After an empty line, there are only user-facing messages (if any) if (lines[i].empty()) { return; } auto words = megacmd::split(lines[i], " "); ASSERT_FALSE(words.empty()); std::string syncId = words[0]; auto result = executeInClient({"sync", "--remove", "--", syncId}).ok(); ASSERT_TRUE(result); } } void SetUp() override { BasicGenericTest::SetUp(); ensureLoggedIn(); } }; class ReadTest : public LoggedInTest { protected: void SetUp() override { LoggedInTest::SetUp(); ensureReadStructure(); } }; class NOINTERACTIVEBasicTest : public BasicGenericTest{}; class NOINTERACTIVELoggedInTest : public LoggedInTest{}; class NOINTERACTIVENotLoggedTest : public NotLoggedTest{}; class NOINTERACTIVEReadTest : public ReadTest{}; MEGAcmd-2.5.2_Linux/tests/integration/PutTests.cpp000066400000000000000000000400541516543156300220740ustar00rootroot00000000000000/** * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include "MegaCmdTestingTools.h" #include "TestUtils.h" #include #include #include #include #include #include namespace fs = std::filesystem; namespace { std::string stripTrailingNewlines(std::string s) { while (!s.empty() && (s.back() == '\n' || s.back() == '\r')) { s.pop_back(); } return s; } std::string makeUniqueRemoteBase() { const auto* ti = ::testing::UnitTest::GetInstance()->current_test_info(); std::string name = std::string(ti->test_suite_name()) + "_" + std::string(ti->name()); // Keep it filesystem/MEGA-path friendly for (auto& ch: name) { if (!(std::isalnum(static_cast(ch)) || ch == '_' || ch == '-')) { ch = '_'; } } return "put_test_dir_" + name; } std::string readLocalFile(const fs::path& p) { std::ifstream in(p, std::ios::binary); return std::string((std::istreambuf_iterator(in)), std::istreambuf_iterator()); } } // namespace class PutTests: public NOINTERACTIVELoggedInTest { SelfDeletingTmpFolder mTmpDir; std::string mRemoteBase; void SetUp() override { NOINTERACTIVELoggedInTest::SetUp(); mRemoteBase = makeUniqueRemoteBase(); // Ensure a clean remote sandbox (void)executeInClient({"cd", "/"}); (void)executeInClient({"rm", "-rf", mRemoteBase}); auto r = executeInClient({"mkdir", mRemoteBase}); ASSERT_TRUE(r.ok()) << r.err(); r = executeInClient({"cd", mRemoteBase}); ASSERT_TRUE(r.ok()) << r.err(); } void TearDown() override { (void)executeInClient({"cd", "/"}); (void)executeInClient({"rm", "-rf", mRemoteBase}); NOINTERACTIVELoggedInTest::TearDown(); } protected: fs::path localPath() const { return mTmpDir.path(); } void createLocalFile(const std::string& filename, const std::string& content = "") { fs::create_directories((localPath() / filename).parent_path()); std::ofstream file(localPath() / filename, std::ios::binary); file << content; } void createLocalDir(const std::string& dirname) { fs::create_directories(localPath() / dirname); } std::string remoteBase() const { return mRemoteBase; } }; TEST_F(PutTests, SingleFileUpload) { const std::string filename = "test_file.txt"; const std::string content = "Hello, MEGAcmd!"; createLocalFile(filename, content); // Upload into current remote cwd (mRemoteBase) auto result = executeInClient({"put", (localPath() / filename).string()}); ASSERT_TRUE(result.ok()) << result.err(); // Verify file was uploaded + content matches (normalize trailing newline) result = executeInClient({"cat", filename}); ASSERT_TRUE(result.ok()) << result.err(); EXPECT_EQ(content, stripTrailingNewlines(result.out())); } TEST_F(PutTests, SingleFileUploadToDestination) { const std::string filename = "test_file.txt"; const std::string content = "Hello, MEGAcmd!"; const std::string remoteDir = "dest_dir"; createLocalFile(filename, content); auto result = executeInClient({"mkdir", remoteDir}); ASSERT_TRUE(result.ok()) << result.err(); result = executeInClient({"put", (localPath() / filename).string(), remoteDir}); ASSERT_TRUE(result.ok()) << result.err(); result = executeInClient({"cat", remoteDir + "/" + filename}); ASSERT_TRUE(result.ok()) << result.err(); EXPECT_EQ(content, stripTrailingNewlines(result.out())); } TEST_F(PutTests, EmptyFileUpload) { const std::string filename = "empty_file.txt"; createLocalFile(filename); auto result = executeInClient({"put", (localPath() / filename).string()}); ASSERT_TRUE(result.ok()) << result.err(); result = executeInClient({"ls", filename}); ASSERT_TRUE(result.ok()) << result.err(); EXPECT_THAT(result.out(), testing::HasSubstr(filename)); } TEST_F(PutTests, MultipleFileUpload) { const std::string remoteDir = "multi_dest"; auto result = executeInClient({"mkdir", remoteDir}); ASSERT_TRUE(result.ok()) << result.err(); const std::vector files = {"file1.txt", "file2.txt", "file3.txt"}; for (const auto& f: files) { createLocalFile(f, "Content of " + f); } std::vector command = {"put"}; for (const auto& f: files) { command.push_back((localPath() / f).string()); } command.push_back(remoteDir); result = executeInClient(command); ASSERT_TRUE(result.ok()) << " command = " << joinString(command) << " err=" << result.err(); for (const auto& f: files) { result = executeInClient({"cat", remoteDir + "/" + f}); ASSERT_TRUE(result.ok()) << "Failed to verify file: " << f << " err=" << result.err(); EXPECT_EQ("Content of " + f, stripTrailingNewlines(result.out())); } } TEST_F(PutTests, DirectoryUpload) { const std::string dirname = "test_dir"; const std::string subfile = "subfile.txt"; const std::string content = "Content in subdirectory"; createLocalDir(dirname); createLocalFile(dirname + "/" + subfile, content); auto result = executeInClient({"put", (localPath() / dirname).string()}); ASSERT_TRUE(result.ok()) << result.err(); result = executeInClient({"ls", dirname}); ASSERT_TRUE(result.ok()) << result.err(); EXPECT_THAT(result.out(), testing::HasSubstr(subfile)); result = executeInClient({"cat", dirname + "/" + subfile}); ASSERT_TRUE(result.ok()) << result.err(); EXPECT_EQ(content, stripTrailingNewlines(result.out())); } TEST_F(PutTests, DirectoryUploadToDestination) { const std::string dirname = "test_dir"; const std::string subfile = "subfile.txt"; const std::string content = "Content in subdirectory"; const std::string remoteDest = "dest_dir"; createLocalDir(dirname); createLocalFile(dirname + "/" + subfile, content); auto result = executeInClient({"mkdir", remoteDest}); ASSERT_TRUE(result.ok()) << result.err(); result = executeInClient({"put", (localPath() / dirname).string(), remoteDest}); ASSERT_TRUE(result.ok()) << result.err(); result = executeInClient({"ls", remoteDest + "/" + dirname}); ASSERT_TRUE(result.ok()) << result.err(); EXPECT_THAT(result.out(), testing::HasSubstr(subfile)); result = executeInClient({"cat", remoteDest + "/" + dirname + "/" + subfile}); ASSERT_TRUE(result.ok()) << result.err(); EXPECT_EQ(content, stripTrailingNewlines(result.out())); } TEST_F(PutTests, UploadWithCreateFlag) { const std::string filename = "test_file.txt"; const std::string content = "Hello, MEGAcmd!"; const std::string remotePath = "new_dir/test_file.txt"; createLocalFile(filename, content); auto result = executeInClient({"put", "-c", (localPath() / filename).string(), remotePath}); ASSERT_TRUE(result.ok()) << result.err(); result = executeInClient({"cat", remotePath}); ASSERT_TRUE(result.ok()) << result.err(); EXPECT_EQ(content, stripTrailingNewlines(result.out())); } TEST_F(PutTests, UploadWithCreateFlagAndAbsoluteDestinationPath) { // When cwd is in a subfolder (e.g. /put_test_xxx) and put -c uses an absolute // destination path, the path must be resolved from root, not from cwd. const std::string filename = "file01.txt"; createLocalFile(filename); const std::string absoluteDestFolder = "/" + remoteBase() + "/newfile/"; auto result = executeInClient({"put", "-c", (localPath() / filename).string(), absoluteDestFolder}); ASSERT_TRUE(result.ok()) << result.err(); const std::string absoluteRemotePath = absoluteDestFolder + filename; result = executeInClient({"cat", absoluteRemotePath}); ASSERT_TRUE(result.ok()) << result.err(); EXPECT_EQ("", stripTrailingNewlines(result.out())); } TEST_F(PutTests, UploadNonAsciiFilename) { #ifdef _WIN32 // Skip on Windows due to encoding issues in test environment GTEST_SKIP() << "Skipping non-ASCII filename test on Windows due to encoding limitations"; return; #endif const std::string filename = "\xE3\x83\x86\xE3\x82\xB9\xE3\x83\x88\xE3\x83\x95\xE3\x82\xA1" "\xE3\x82\xA4\xE3\x83\xAB.txt"; const std::string content = "Content with non-ASCII filename"; createLocalFile(filename, content); auto result = executeInClient({"put", (localPath() / filename).string()}); ASSERT_TRUE(result.ok()) << result.err(); result = executeInClient({"ls"}); ASSERT_TRUE(result.ok()) << result.err(); EXPECT_THAT(result.out(), testing::HasSubstr(filename)); } TEST_F(PutTests, UploadFileWithSpaces) { const std::string filename = "test file with spaces.txt"; const std::string content = "Content in file with spaces"; createLocalFile(filename, content); auto result = executeInClient({"put", (localPath() / filename).string()}); ASSERT_TRUE(result.ok()) << result.err(); result = executeInClient({"ls"}); ASSERT_TRUE(result.ok()) << result.err(); EXPECT_THAT(result.out(), testing::HasSubstr(filename)); } TEST_F(PutTests, ReplaceExistingFile) { const std::string filename = "test_file.txt"; const std::string originalContent = "Original content"; const std::string newContent = "New content replacing original"; createLocalFile(filename, originalContent); auto result = executeInClient({"put", (localPath() / filename).string()}); ASSERT_TRUE(result.ok()) << result.err(); result = executeInClient({"cat", filename}); ASSERT_TRUE(result.ok()) << result.err(); EXPECT_EQ(originalContent, stripTrailingNewlines(result.out())); createLocalFile(filename, newContent); result = executeInClient({"put", (localPath() / filename).string(), filename}); ASSERT_TRUE(result.ok()) << result.err(); result = executeInClient({"cat", filename}); ASSERT_TRUE(result.ok()) << result.err(); EXPECT_EQ(newContent, stripTrailingNewlines(result.out())); } TEST_F(PutTests, ReplaceFileInSubdirectory) { const std::string dirname = "test_dir"; const std::string filename = "test_file.txt"; const std::string originalContent = "Original content"; const std::string newContent = "New content in subdirectory"; ASSERT_TRUE(executeInClient({"mkdir", dirname}).ok()); createLocalFile(filename, originalContent); auto result = executeInClient({"put", (localPath() / filename).string(), dirname}); ASSERT_TRUE(result.ok()) << result.err(); createLocalFile(filename, newContent); result = executeInClient({"put", (localPath() / filename).string(), dirname + "/" + filename}); ASSERT_TRUE(result.ok()) << result.err(); result = executeInClient({"cat", dirname + "/" + filename}); ASSERT_TRUE(result.ok()) << result.err(); EXPECT_EQ(newContent, stripTrailingNewlines(result.out())); } TEST_F(PutTests, MultipleFilesToFileTargetShouldFail) { const std::string file1 = "file1.txt"; const std::string file2 = "file2.txt"; const std::string targetFile = "target.txt"; createLocalFile(file1, "Content 1"); createLocalFile(file2, "Content 2"); auto result = executeInClient({"put", (localPath() / file1).string(), targetFile}); ASSERT_TRUE(result.ok()) << result.err(); result = executeInClient({"put", (localPath() / file1).string(), (localPath() / file2).string(), targetFile}); ASSERT_FALSE(result.ok()); EXPECT_THAT(result.err(), testing::HasSubstr("Destination")); } TEST_F(PutTests, UploadToNonExistentPathWithoutCreateFlag) { const std::string filename = "test_file.txt"; const std::string nonExistentPath = "non_existent_dir/file.txt"; createLocalFile(filename, "Test content"); auto result = executeInClient({"put", (localPath() / filename).string(), nonExistentPath}); ASSERT_FALSE(result.ok()); EXPECT_THAT(result.err(), testing::AnyOf(testing::HasSubstr("Couldn't find destination"), testing::HasSubstr("destination"))); } TEST_F(PutTests, UploadDirectoryToFileTargetShouldFail) { const std::string dirname = "test_dir"; const std::string targetFile = "target.txt"; createLocalDir(dirname); createLocalFile("temp.txt", "temp"); auto result = executeInClient({"put", (localPath() / "temp.txt").string(), targetFile}); ASSERT_TRUE(result.ok()) << result.err(); result = executeInClient({"put", (localPath() / dirname).string(), targetFile}); ASSERT_FALSE(result.ok()); EXPECT_THAT(result.err(), testing::HasSubstr("Destination")); } TEST_F(PutTests, PutSingleFileRoundTripViaGet) { const std::string filename = "roundtrip.txt"; const std::string content = "roundtrip-content\nline2\n"; createLocalFile(filename, content); auto r = executeInClient({"put", (localPath() / filename).string()}); ASSERT_TRUE(r.ok()) << r.err(); // Download back and compare bytes (stronger than `cat`) SelfDeletingTmpFolder dlTmp; r = executeInClient({"get", filename, dlTmp.path().string()}); ASSERT_TRUE(r.ok()) << r.err(); const fs::path downloaded = dlTmp.path() / filename; ASSERT_TRUE(fs::exists(downloaded)); EXPECT_EQ(readLocalFile(downloaded), content); } TEST_F(PutTests, PutCanRenameRemoteFile) { const std::string localName = "src.txt"; const std::string remoteName = "dst.txt"; const std::string content = "rename me\n"; createLocalFile(localName, content); auto r = executeInClient({"put", (localPath() / localName).string(), remoteName}); ASSERT_TRUE(r.ok()) << r.err(); r = executeInClient({"ls"}); ASSERT_TRUE(r.ok()) << r.err(); EXPECT_THAT(r.out(), testing::HasSubstr(remoteName)); SelfDeletingTmpFolder dlTmp; r = executeInClient({"get", remoteName, dlTmp.path().string()}); ASSERT_TRUE(r.ok()) << r.err(); const fs::path downloaded = dlTmp.path() / remoteName; ASSERT_TRUE(fs::exists(downloaded)); EXPECT_EQ(readLocalFile(downloaded), content); } TEST_F(PutTests, PutDirectoryTreeShowsUp) { // Local tree: // tree/ // a.txt // sub/b.txt const std::string treeDir = "tree"; createLocalDir(treeDir + "/sub"); createLocalFile(treeDir + "/a.txt", "A\n"); createLocalFile(treeDir + "/sub/b.txt", "B\n"); auto r = executeInClient({"put", (localPath() / treeDir).string()}); ASSERT_TRUE(r.ok()) << r.err(); r = executeInClient({"ls", treeDir}); ASSERT_TRUE(r.ok()) << r.err(); EXPECT_THAT(r.out(), testing::HasSubstr("a.txt")); EXPECT_THAT(r.out(), testing::HasSubstr("sub")); r = executeInClient({"ls", treeDir + "/sub"}); ASSERT_TRUE(r.ok()) << r.err(); EXPECT_THAT(r.out(), testing::HasSubstr("b.txt")); } TEST_F(PutTests, PutPrintTagAtStartPrintsDecimalTag) { const std::string filename = "tag_test.txt"; createLocalFile(filename, "tag\n"); auto r = executeInClient({"put", "--print-tag-at-start", (localPath() / filename).string()}); ASSERT_TRUE(r.ok()) << r.err(); const std::string out = r.out(); // Must contain the prefix const std::string prefix = "Upload started: Tag = "; auto pos = out.find(prefix); ASSERT_NE(pos, std::string::npos) << "Output did not contain expected prefix. out=\n" << out; pos += prefix.size(); ASSERT_LT(pos, out.size()) << "No characters after tag prefix. out=\n" << out; // Parse consecutive decimal digits after the prefix std::size_t end = pos; while (end < out.size() && std::isdigit(static_cast(out[end]))) { ++end; } ASSERT_GT(end, pos) << "Tag was not a decimal number. out=\n" << out; // Optional: sanity check it is followed by "." // (keeps it aligned with the format, but still doesn't validate path) ASSERT_TRUE(end < out.size() && out[end] == '.') << "Expected '.' after decimal tag. out=\n" << out; } MEGAcmd-2.5.2_Linux/tests/integration/SyncIgnoreTests.cpp000066400000000000000000000241121516543156300234010ustar00rootroot00000000000000/** * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include #include #include #include "megacmdcommonutils.h" #include "TestUtils.h" #include "MegaCmdTestingTools.h" using namespace megacmd; using TI = TestInstruments; class SyncIgnoreTests : public NOINTERACTIVELoggedInTest { void SetUp() override { NOINTERACTIVELoggedInTest::SetUp(); TearDown(); } void TearDown() override { removeAllSyncs(); writeToDefaultFile(); } protected: SelfDeletingTmpFolder mTmpDir; void writeToDefaultFile(const std::vector& lines = {}) { auto platformDirs = PlatformDirectories::getPlatformSpecificDirectories(); auto configDirPath = platformDirs->configDirPath(); const fs::path megaIgnorePath = configDirPath / ".megaignore.default"; std::ofstream file(megaIgnorePath); ASSERT_TRUE(file.is_open()); for (const std::string& line : lines) { file << line << '\n'; } ASSERT_FALSE(file.fail()); } void readFromDefaultFile(std::string& contents) // cannot return string or asserts won't work { auto platformDirs = PlatformDirectories::getPlatformSpecificDirectories(); auto configDirPath = platformDirs->configDirPath(); const fs::path megaIgnorePath = configDirPath / ".megaignore.default"; std::ifstream file(megaIgnorePath); ASSERT_TRUE(file.is_open()); for (std::string line; getline(file, line);) { contents += line + "\n"; } ASSERT_TRUE(file.eof()); } std::string qw(const std::string& str) // quote wrap { return "\"" + str + "\""; } }; TEST_F(SyncIgnoreTests, DefaultIgnoreFile) { auto result = executeInClient({"sync-ignore", "DEFAULT"}); ASSERT_TRUE(result.ok()); result = executeInClient({"sync-ignore", "default"}); ASSERT_TRUE(result.ok()); } TEST_F(SyncIgnoreTests, AddAndShow) { std::string filter1 = "-:some_file.txt"; std::string filter2 = "-fp:video/*.avi"; std::string filter3 = "+fng:family*.jpg"; std::string filter4 = "-d:private"; std::string filter5 = "-:*"; auto result = executeInClient({"sync-ignore", "--add", "--", filter1, filter2, filter3, filter4, filter5, "DEFAULT"}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr("Added filter " + qw(filter1))); EXPECT_THAT(result.out(), testing::HasSubstr("Added filter " + qw(filter2))); EXPECT_THAT(result.out(), testing::HasSubstr("Added filter " + qw(filter3))); EXPECT_THAT(result.out(), testing::HasSubstr("Added filter " + qw(filter4))); EXPECT_THAT(result.out(), testing::HasSubstr("Added filter " + qw(filter5))); result = executeInClient({"sync-ignore", "--show", "DEFAULT"}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr(filter1)); EXPECT_THAT(result.out(), testing::HasSubstr(filter2)); EXPECT_THAT(result.out(), testing::HasSubstr(filter3)); EXPECT_THAT(result.out(), testing::HasSubstr(filter4)); EXPECT_THAT(result.out(), testing::HasSubstr(filter5)); } TEST_F(SyncIgnoreTests, AddAndRemove) { std::string filter = "-N:*.avi"; auto result = executeInClient({"sync-ignore", "--add", "--", filter, "DEFAULT"}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr("Added filter " + qw(filter))); result = executeInClient({"sync-ignore", "--remove", "--", filter, "DEFAULT"}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr("Removed filter " + qw(filter))); result = executeInClient({"sync-ignore", "--show", "DEFAULT"}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::Not(testing::HasSubstr(filter))); } TEST_F(SyncIgnoreTests, AddAndRemoveWithExclusionArgument) { std::string filter1 = "n:*.mp4"; std::string filter2 = "fp:pics/*.jpg"; std::string filter3 = "d:private"; auto result = executeInClient({"sync-ignore", "--add-exclusion", filter1, filter2, filter3, "DEFAULT"}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr("Added filter " + qw("-" + filter1))); EXPECT_THAT(result.out(), testing::HasSubstr("Added filter " + qw("-" + filter2))); EXPECT_THAT(result.out(), testing::HasSubstr("Added filter " + qw("-" + filter3))); result = executeInClient({"sync-ignore", "--remove-exclusion", filter1, filter2, filter3, "DEFAULT"}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr("Removed filter " + qw("-" + filter1))); EXPECT_THAT(result.out(), testing::HasSubstr("Removed filter " + qw("-" + filter2))); EXPECT_THAT(result.out(), testing::HasSubstr("Removed filter " + qw("-" + filter3))); result = executeInClient({"sync-ignore", "--show", "DEFAULT"}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::Not(testing::HasSubstr(filter1))); EXPECT_THAT(result.out(), testing::Not(testing::HasSubstr(filter2))); EXPECT_THAT(result.out(), testing::Not(testing::HasSubstr(filter3))); } TEST_F(SyncIgnoreTests, CannotAddIfAlreadyExists) { std::string filter = "+fg:work*.txt"; auto result = executeInClient({"sync-ignore", "--add", "--", filter, "DEFAULT"}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr("Added filter " + qw(filter))); result = executeInClient({"sync-ignore", "--add", "--", filter, "DEFAULT"}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr("Cannot add filter " + qw(filter))); EXPECT_THAT(result.out(), testing::HasSubstr("already in the .megaignore file")); } TEST_F(SyncIgnoreTests, CannotRemoveIfDoesntExist) { std::string filter = "+fnpg:family*.jpg"; auto result = executeInClient({"sync-ignore", "--remove", "--", filter, "DEFAULT"}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr("Cannot remove filter " + qw(filter))); EXPECT_THAT(result.out(), testing::HasSubstr("it's not in the .megaignore file")); } TEST_F(SyncIgnoreTests, NonDefaultIgnoreFile) { std::string localDir = mTmpDir.string(); #ifdef __APPLE__ localDir = "/private" + localDir; #endif std::string cloudDir = "cloud_dir"; TI::ScopeGuard cloudRmGuard([&cloudDir] { executeInClient({"rm", "-r", "-f", cloudDir}); }); executeInClient({"rm", "-r", "-f", cloudDir}); auto result = executeInClient({"mkdir", "-p", cloudDir}); ASSERT_TRUE(result.ok()); result = executeInClient({"sync", localDir, cloudDir}); ASSERT_TRUE(result.ok()); result = executeInClient({"sync-ignore", "--show", localDir}); ASSERT_TRUE(result.ok()); std::string filter = "-d:.vscode"; result = executeInClient({"sync-ignore", "--add", "--", filter, localDir}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr("Added filter " + qw(filter))); result = executeInClient({"sync-ignore", "--show", localDir}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr(filter)); result = executeInClient({"sync-ignore", "--remove", "--", filter, localDir}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr("Removed filter " + qw(filter))); } TEST_F(SyncIgnoreTests, NonExistingIgnoreFile) { std::string invalidSyncDir = "some_sync_dir"; auto result = executeInClient({"sync-ignore", "--show", invalidSyncDir}); ASSERT_FALSE(result.ok()); EXPECT_THAT(result.err(), testing::HasSubstr("Sync " + invalidSyncDir + " was not found")); // Must also work with implicit "--show" result = executeInClient({"sync-ignore", invalidSyncDir}); ASSERT_FALSE(result.ok()); EXPECT_THAT(result.err(), testing::HasSubstr("Sync " + invalidSyncDir + " was not found")); } TEST_F(SyncIgnoreTests, IncorrectFilterFormat) { std::string validFilter = "-fp:video/*.avi"; std::string invalidFilter = "video/family/*.avi"; auto result = executeInClient({"sync-ignore", "--add", "--", validFilter, invalidFilter, "DEFAULT"}); ASSERT_FALSE(result.ok()); EXPECT_THAT(result.err(), testing::HasSubstr("The filter " + qw(invalidFilter) + " does not have valid format")); } TEST_F(SyncIgnoreTests, CommentsAndBOMNotRemoved) { std::string BOM = "\xEF\xBB\xBF"; std::string comment = "# This is a comment"; std::string filter = "-nr:.*foo"; writeToDefaultFile({BOM, comment, filter}); auto result = executeInClient({"sync-ignore", "--remove", "--", filter, "DEFAULT"}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr("Removed filter " + qw(filter))); std::string contents; readFromDefaultFile(contents); EXPECT_THAT(contents, testing::HasSubstr(BOM)); EXPECT_THAT(contents, testing::HasSubstr(comment)); } TEST_F(SyncIgnoreTests, UseExcludeInterface) { std::string pattern = "*.pdf"; auto result = executeInClient({"sync-ignore", "--show", "DEFAULT"}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::Not(testing::HasSubstr(pattern))); result = executeInClient({"exclude", "-a", pattern}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr("Added filter")); EXPECT_THAT(result.out(), testing::HasSubstr(pattern)); result = executeInClient({"sync-ignore", "--show", "DEFAULT"}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr(pattern)); result = executeInClient({"exclude", "-d", pattern}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr("Removed filter")); EXPECT_THAT(result.out(), testing::HasSubstr(pattern)); result = executeInClient({"sync-ignore", "--show", "DEFAULT"}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::Not(testing::HasSubstr(pattern))); } MEGAcmd-2.5.2_Linux/tests/integration/SyncIssuesTests.cpp000066400000000000000000000352451516543156300234420ustar00rootroot00000000000000/** * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include #include #include #include "megacmdcommonutils.h" #include "TestUtils.h" #include "MegaCmdTestingTools.h" using TI = TestInstruments; // This class ensures that the sync issue list eventually // reaches the expected size (before a certain timeout) class SyncIssueListGuard { uint64_t mCurrentListSize; const uint64_t mExpectedListSize; bool mTriggered; std::mutex mMtx; std::condition_variable mCv; public: SyncIssueListGuard(uint64_t expectedListSize) : mCurrentListSize(0), mExpectedListSize(expectedListSize), mTriggered(false) { TI::Instance().onEveryEvent(TI::Event::SYNC_ISSUES_LIST_UPDATED, [this] { auto syncIssueListSizeOpt = TI::Instance().testValue(TI::TestValue::SYNC_ISSUES_LIST_SIZE); EXPECT_TRUE(syncIssueListSizeOpt.has_value()); { std::lock_guard guard(mMtx); mCurrentListSize = std::get(*syncIssueListSizeOpt); mTriggered = true; } mCv.notify_one(); }); } ~SyncIssueListGuard() { { std::unique_lock lock(mMtx); mCv.wait_for(lock, std::chrono::seconds(5), [this] { return mTriggered && mCurrentListSize == mExpectedListSize; }); EXPECT_EQ(mCurrentListSize, mExpectedListSize); } TI::Instance().clearEvent(TI::Event::SYNC_ISSUES_LIST_UPDATED); TI::Instance().resetTestValue(TI::TestValue::SYNC_ISSUES_LIST_SIZE); } }; template class SyncIssuesTests_ : public NOINTERACTIVELoggedInTest { SelfDeletingTmpFolder mTmpDir; void SetUp() override { NOINTERACTIVELoggedInTest::SetUp(); TearDown(); auto result = executeInClient({"mkdir", "-p", syncDirCloud()}).ok(); ASSERT_TRUE(result); result = fs::create_directory(syncDirLocal()); ASSERT_TRUE(result); if constexpr (runSync) { SyncIssueListGuard guard(0); // ensure there are no sync issues before we start the test result = executeInClient({"sync", syncDirLocal(), syncDirCloud()}).ok(); ASSERT_TRUE(result); } } void TearDown() override { removeAllSyncs(); fs::remove_all(syncDirLocal()); executeInClient({"rm", "-r", "-f", syncDirCloud()}); } protected: std::string syncDirLocal() const { return mTmpDir.string() + "/tests_integration_sync_dir/"; } std::string syncDirCloud() const { return "tests_integration_sync_dir/"; } std::string qw(const std::string& str) // quote wrap { return "'" + str + "'"; } }; using SyncIssuesTests = SyncIssuesTests_; using ManualSyncIssuesTests = SyncIssuesTests_; TEST_F(SyncIssuesTests, NoIssues) { auto result = executeInClient({"sync-issues"}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr("There are no sync issues")); } TEST_F(SyncIssuesTests, NameConflict) { #ifdef _WIN32 const char* conflictingName = "F01"; #else const char* conflictingName = "f%301"; #endif auto rMkdir = executeInClient({"mkdir", syncDirCloud() + "f01"}); ASSERT_TRUE(rMkdir.ok()); { // Register the event callback *before* causing the sync issue SyncIssueListGuard guard(1); // Cause the name conclict rMkdir = executeInClient({"mkdir", syncDirCloud() + conflictingName}); ASSERT_TRUE(rMkdir.ok()); } // Check the name conclict appears in the list of sync issues auto result = executeInClient({"sync-issues", "--disable-path-collapse"}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::Not(testing::HasSubstr("There are no sync issues"))); EXPECT_THAT(result.out(), testing::AnyOf(testing::HasSubstr(qw("f01")), testing::HasSubstr(qw(conflictingName)))); } TEST_F(SyncIssuesTests, SymLink) { const std::string dirPath = syncDirLocal() + "some_dir"; ASSERT_TRUE(fs::create_directory(dirPath)); std::string linkName = "some_link"; std::string linkPath = syncDirLocal() + linkName; #ifdef _WIN32 megacmd::replaceAll(linkPath, "/", "\\"); #endif { SyncIssueListGuard guard(1); fs::create_directory_symlink(dirPath, linkPath); } // Check the symlink in the list of sync issues auto result = executeInClient({"sync-issues", "--disable-path-collapse"}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::Not(testing::HasSubstr("There are no sync-issues issues"))); EXPECT_THAT(result.out(), testing::HasSubstr(qw(linkName))); { SyncIssueListGuard guard(0); fs::remove(linkPath); } result = executeInClient({"sync-issues"}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr("There are no sync issues")); } TEST_F(ManualSyncIssuesTests, FileAgainstFolder) { const std::string fileName = "fake_file"; auto result = executeInClient({"mkdir", "-p", syncDirCloud() + fileName}); ASSERT_TRUE(result.ok()); { std::ofstream file(syncDirLocal() + fileName); file << "Some data"; } // Start syncing once we've setup inconsistent data in cloud and local { SyncIssueListGuard guard(1); result = executeInClient({"sync", syncDirLocal(), syncDirCloud()}); ASSERT_TRUE(result.ok()); } result = executeInClient({"sync-issues", "--disable-path-collapse", "--detail", "--all"}); ASSERT_TRUE(result.ok()); auto lines = splitByNewline(result.out()); ASSERT_THAT(lines, testing::Contains(testing::HasSubstr("Unable to sync " + qw(fileName)))); ASSERT_THAT(lines, testing::Contains(testing::HasSubstr("Cannot sync folders against files"))); ASSERT_THAT(lines, testing::Contains(testing::AllOf(testing::HasSubstr(""), testing::HasSubstr("Folder")))); ASSERT_THAT(lines, testing::Contains(testing::AllOf(testing::Not(testing::HasSubstr("")), testing::HasSubstr("File")))); } TEST_F(SyncIssuesTests, IncorrectSyncIssueListSizeOnSecondSymlink) { // This tests against an internal issue that caused the sync issue list to have incorrect // size for a brief period of time after creating a second symlink. This is unlikely to // affect how users interact with MEGAcmd, since it happened over a short timespan. But the test // is still useful to prove the internal correctness of our sync issues is not broken. // Note: required sdk fix (+our changes) to pass (see: SDK-4016) const std::string dirPath = syncDirLocal() + "some_dir"; ASSERT_TRUE(fs::create_directory(dirPath)); // The first link doesn't cause an issue, but we still need to wait for it { SyncIssueListGuard guard(1); fs::create_directory_symlink(dirPath, syncDirLocal() + "link1"); } // The second link causes the issue { SyncIssueListGuard guard(2); fs::create_directory_symlink(dirPath, syncDirLocal() + "link2"); } } TEST_F(SyncIssuesTests, LimitedSyncIssueList) { const std::string dirPath = syncDirLocal() + "some_dir"; ASSERT_TRUE(fs::create_directory(dirPath)); // Create 5 sync issues for (int i = 1; i <= 5; ++i) { SyncIssueListGuard guard(i); fs::create_directory_symlink(dirPath, syncDirLocal() + "link" + std::to_string(i)); } auto result = executeInClient({"sync-issues", "--limit=" + std::to_string(3)}); ASSERT_TRUE(result.ok()); // There should be 7 lines (column header + limit=3 + newline + detail usage + limit-specific note) auto lines = splitByNewline(result.out()); EXPECT_THAT(lines, testing::SizeIs(7)); EXPECT_THAT(lines.at(lines.size()-2), testing::HasSubstr("showing 3 out of 5 issues")); } TEST_F(SyncIssuesTests, ShowSyncIssuesInSyncCommand) { auto result = executeInClient({"sync"}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr(" NO ")); EXPECT_THAT(result.out(), testing::Not(testing::HasSubstr("You have sync issues"))); const std::string dirPath = syncDirLocal() + "some_dir"; ASSERT_TRUE(fs::create_directory(dirPath)); { SyncIssueListGuard guard(1); fs::create_directory_symlink(dirPath, syncDirLocal() + "link1"); } result = executeInClient({"sync"}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr("Sync Issues (1)")); EXPECT_THAT(result.out(), testing::Not(testing::HasSubstr(" NO "))); EXPECT_THAT(result.err(), testing::HasSubstr("You have sync issues")); { SyncIssueListGuard guard(2); fs::create_directory_symlink(dirPath, syncDirLocal() + "link2"); } result = executeInClient({"sync"}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr("Sync Issues (2)")); EXPECT_THAT(result.out(), testing::Not(testing::HasSubstr(" NO "))); EXPECT_THAT(result.err(), testing::HasSubstr("You have sync issues")); } TEST_F(SyncIssuesTests, SyncIssueDetail) { const std::string dirPath = syncDirLocal() + "some_dir"; ASSERT_TRUE(fs::create_directory(dirPath)); std::string linkPath = syncDirLocal() + "some_link"; #ifdef _WIN32 megacmd::replaceAll(linkPath, "/", "\\"); #endif { SyncIssueListGuard guard(1); fs::create_directory_symlink(dirPath, linkPath); } // Get the sync issue id auto result = executeInClient({"sync-issues"}); ASSERT_TRUE(result.ok()); auto lines = splitByNewline(result.out()); EXPECT_EQ(lines.size(), 4); // Column names + issue + newline + detail usage auto words = megacmd::split(lines[1], " "); EXPECT_THAT(words, testing::Not(testing::IsEmpty())); std::string syncIssueId = words[0]; // Get the parent sync ID result = executeInClient({"sync"}); ASSERT_TRUE(result.ok()); lines = splitByNewline(result.out()); EXPECT_THAT(lines.size(), 3); words = megacmd::split(lines[1], " "); EXPECT_THAT(words, testing::Not(testing::IsEmpty())); std::string parentSyncId = words[0]; result = executeInClient({"sync-issues", "--disable-path-collapse", "--detail", "--", syncIssueId}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr("Parent sync: " + parentSyncId)); EXPECT_THAT(result.out(), testing::HasSubstr("Symlink detected")); EXPECT_THAT(result.out(), testing::HasSubstr(linkPath)); } TEST_F(SyncIssuesTests, AllSyncIssuesDetail) { const std::string dirPath = syncDirLocal() + "some_dir"; ASSERT_TRUE(fs::create_directory(dirPath)); std::string linkPath = syncDirLocal() + "some_link"; #ifdef _WIN32 megacmd::replaceAll(linkPath, "/", "\\"); #endif { SyncIssueListGuard guard(1); fs::create_directory_symlink(dirPath, linkPath); } // Get the sync issue id auto result = executeInClient({"sync-issues"}); ASSERT_TRUE(result.ok()); auto lines = splitByNewline(result.out()); EXPECT_EQ(lines.size(), 4); // Column names + issue + newline + detail usage auto words = megacmd::split(lines[1], " "); EXPECT_THAT(words, testing::Not(testing::IsEmpty())); std::string syncIssueId = words[0]; // Get the parent sync ID result = executeInClient({"sync"}); ASSERT_TRUE(result.ok()); lines = splitByNewline(result.out()); EXPECT_THAT(lines.size(), 3); words = megacmd::split(lines[1], " "); EXPECT_THAT(words, testing::Not(testing::IsEmpty())); std::string parentSyncId = words[0]; result = executeInClient({"sync-issues", "--disable-path-collapse", "--detail", "--all"}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr("Parent sync: " + parentSyncId)); EXPECT_THAT(result.out(), testing::HasSubstr("Symlink detected")); EXPECT_THAT(result.out(), testing::HasSubstr(linkPath)); EXPECT_THAT(result.out(), testing::HasSubstr("Details on issue " + syncIssueId)); } TEST_F(ManualSyncIssuesTests, AllSyncIssuesDetailEnforceReasonsAndPathProblems) { // Configure a conflicting issue and iterate over possible sync issues reasons and path problems, // to exercise code paths for all: const std::string fileName = "fake_file"; auto result = executeInClient({"mkdir", "-p", syncDirCloud() + fileName}); ASSERT_TRUE(result.ok()); { std::ofstream file(syncDirLocal() + fileName); file << "Some data"; } // Start syncing once we've setup inconsistent data in cloud and local { SyncIssueListGuard guard(1); result = executeInClient({"sync", syncDirLocal(), syncDirCloud()}); ASSERT_TRUE(result.ok()); } // Get the sync issue id result = executeInClient({"sync-issues"}); ASSERT_TRUE(result.ok()); auto lines = splitByNewline(result.out()); EXPECT_EQ(lines.size(), 4); // Column names + issue + newline + detail usage auto words = megacmd::split(lines[1], " "); EXPECT_THAT(words, testing::Not(testing::IsEmpty())); std::string syncIssueId = words[0]; // Get the parent sync ID result = executeInClient({"sync"}); ASSERT_TRUE(result.ok()); lines = splitByNewline(result.out()); EXPECT_THAT(lines.size(), 3); words = megacmd::split(lines[1], " "); EXPECT_THAT(words, testing::Not(testing::IsEmpty())); std::string parentSyncId = words[0]; for (int reasonType = static_cast(mega::SyncWaitReason::NoReason); reasonType < static_cast(mega::SyncWaitReason::SyncWaitReason_LastPlusOne); ++reasonType) { for (int pathProblem = static_cast(mega::PathProblem::NoProblem); pathProblem < static_cast(mega::PathProblem::PathProblem_LastPlusOne); ++pathProblem) { if (pathProblem == 19) { continue; // deprecated } TestInstrumentsTestValueGuard reasonGuard(TI::TestValue::SYNC_ISSUE_ENFORCE_REASON_TYPE, (int64_t) reasonType); TestInstrumentsTestValueGuard pathProblemGuard(TI::TestValue::SYNC_ISSUE_ENFORCE_PATH_PROBLEM, (int64_t) pathProblem); result = executeInClient({"sync-issues", "--disable-path-collapse", "--detail", "--all"}); ASSERT_TRUE(result.ok()); EXPECT_THAT(result.out(), testing::HasSubstr("Parent sync: " + parentSyncId)); EXPECT_THAT(result.out(), testing::HasSubstr("Details on issue " + syncIssueId)); } } } MEGAcmd-2.5.2_Linux/tests/integration/main.cpp000066400000000000000000000061441516543156300212270ustar00rootroot00000000000000/** * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include #include "megacmd.h" #include "megacmdlogger.h" #include "megacmd_rotating_logger.h" #include "Instruments.h" #include "TestUtils.h" #ifdef __linux__ #include #endif #include #include #ifndef UNUSED #define UNUSED(x) (void)(x) #endif #ifdef __linux__ static void setUpUnixSignals() { sigset_t set; sigemptyset(&set); sigaddset(&set, SIGPIPE); pthread_sigmask(SIG_BLOCK, &set, NULL); } #endif int main (int argc, char *argv[]) { #ifdef __linux__ if (getenv("MEGA_INTEGRATION_TEST_ENFORCE_SINGLE_CPU")) { cpu_set_t set; CPU_ZERO(&set); CPU_SET(1, &set); if (sched_setaffinity(getpid(), sizeof(set), &set) == -1) { std::cerr << "sched_setaffinity error: " << errno << std::endl; exit(errno); } } setUpUnixSignals(); #endif testing::InitGoogleTest(&argc, argv); #ifdef WIN32 megacmd::Instance windowsConsoleController; // Set custom gtests event listener to control the output to stdout ::testing::TestEventListeners& listeners = ::testing::UnitTest::GetInstance()->listeners(); auto customListener = new CustomTestEventListener(); customListener->mDefault.reset(listeners.Release(listeners.default_result_printer())); listeners.Append(customListener); #endif using TI = TestInstruments; std::promise serverWaitingPromise; TI::Instance().onEventOnce(TI::Event::SERVER_ABOUT_TO_START_WAITING_FOR_PETITIONS, [&serverWaitingPromise]() { serverWaitingPromise.set_value(); }); std::thread serverThread([] { std::vector args{ (char *)"argv0_INTEGRATION_TESTS", nullptr }; megacmd::LogConfig logConfig; logConfig.mSdkLogLevel = mega::MegaApi::LOG_LEVEL_MAX; logConfig.mCmdLogLevel = mega::MegaApi::LOG_LEVEL_MAX; logConfig.mLogToCout = false; logConfig.mJsonLogs = true; auto createDefaultStream = [] { return new megacmd::FileRotatingLoggedStream(megacmd::MegaCmdLogger::getDefaultFilePath()); }; megacmd::executeServer(1, args.data(), createDefaultStream, logConfig); }); auto waitReturn = serverWaitingPromise.get_future().wait_for(std::chrono::seconds(10)); UNUSED(waitReturn); assert(waitReturn != std::future_status::timeout); auto exitCode = RUN_ALL_TESTS(); megacmd::stopServer(); serverThread.join(); #ifdef _WIN32 // We use a file to pass the exit code to Jenkins, // since it fails to get the actual value otherwise std::ofstream("exit_code.txt") << exitCode; #endif return exitCode; } MEGAcmd-2.5.2_Linux/tests/megacmd_create_dir_tree.sh000077500000000000000000000021641516543156300224060ustar00rootroot00000000000000#!/bin/sh # This is the directory structure expected by some of our read integration tests # We can use this script to re-populate it if it gets deleted from our exports account for whatever reason set -e mega-login $MEGACMD_TEST_USER $MEGACMD_TEST_PASS TMP=$(mktemp --directory) cleanup() { ARG=$? echo "Removing $TMP" rm -rf $TMP exit $ARG } trap cleanup EXIT mkdir -p $TMP/testReadingFolder01 for i in 01 02; do mkdir -p $TMP/testReadingFolder01/folder$i for n in 01 02 03; do echo $TMP/testReadingFolder01/file$n.txt > $TMP/testReadingFolder01/file$n.txt echo $TMP/testReadingFolder01/folder$i/file$n.txt > $TMP/testReadingFolder01/folder$i/file$n.txt mkdir -p $TMP/testReadingFolder01/folder$i/subfolder$n for n2 in 01 02 03; do echo $TMP/testReadingFolder01/folder$i/subfolder$n/file$n2.txt > $TMP/testReadingFolder01/folder$i/subfolder$n/file$n2.txt done done done echo /file01.txt > $TMP/file01.txt mkdir -p $TMP/testExportFolder/subDirectoryExport echo /testExportFoldersubDirectoryExport/file01.txt > $TMP/testExportFolder/subDirectoryExport/file01.txt cd $TMP && mega-put * / MEGAcmd-2.5.2_Linux/tests/megacmd_find_test.py000077500000000000000000000146511516543156300212670ustar00rootroot00000000000000#!/usr/bin/python3 # -*- coding: utf-8 -*- import os, shutil import unittest import xmlrunner from megacmd_tests_common import * def setUpModule(): global ABSPWD ABSPWD = os.getcwd() def initialize_contents(): contents=" ".join(['"localtmp/'+x+'"' for x in os.listdir('localtmp/')]) cmd_ef(PUT+" "+contents+" /") shutil.copytree('localtmp', 'localUPs') def clean_all(): if not clean_root_confirmed_by_user(): raise Exception("Tests need to be run with YES_I_KNOW_THIS_WILL_CLEAR_MY_MEGA_ACCOUNT=1") if cmd_es(WHOAMI) != osvar("MEGA_EMAIL"): cmd_ef(LOGOUT) cmd_ef(LOGIN+" " +osvar("MEGA_EMAIL")+" "+osvar("MEGA_PWD")) #~ rm pipe > /dev/null 2>/dev/null || : cmd_ec(RM+' -rf "/*"') cmd_ec(RM+' -rf "*"') cmd_ec(RM+' -rf "//bin/*"') rmfolderifexisting("localUPs") rmfolderifexisting("localtmp") rmfileifexisting("megafind.txt") rmfileifexisting("localfind.txt") def clear_local_and_remote(): rmfolderifexisting("localUPs") cmd_ec(RM+' -rf "/*"') initialize_contents() def initialize(): if cmd_es(WHOAMI) != osvar("MEGA_EMAIL"): cmd_es(LOGOUT) cmd_ef(LOGIN+" " +osvar("MEGA_EMAIL")+" "+osvar("MEGA_PWD")) if len(os.listdir(".")): raise Exception("initialization folder not empty!") #~ cd $ABSPWD if cmd_es(FIND+" /") != b"/": raise Exception("REMOTE Not empty, please clear it before starting!") #initialize localtmp estructure: makedir("localtmp") touch("localtmp/file01.txt") out('file01contents', 'localtmp/file01nonempty.txt') #local empty folders structure for f in ['localtmp/le01/'+a for a in ['les01/less01']+ ['les02/less0'+z for z in ['1','2']] ]: makedir(f) #local filled folders structure for f in ['localtmp/lf01/'+a for a in ['lfs01/lfss01']+ ['lfs02/lfss0'+z for z in ['1','2']] ]: makedir(f) for f in ['localtmp/lf01/'+a for a in ['lfs01/lfss01']+ ['lfs02/lfss0'+z for z in ['1','2']] ]: touch(f+"/commonfile.txt") #spaced structure for f in ['localtmp/ls 01/'+a for a in ['ls s01/ls ss01']+ ['ls s02/ls ss0'+z for z in ['1','2']] ]: makedir(f) for f in ['localtmp/ls 01/'+a for a in ['ls s01/ls ss01']+ ['ls s02/ls ss0'+z for z in ['1','2']] ]: touch(f+"/common file.txt") # localtmp/ # ├── file01nonempty.txt # ├── file01.txt # ├── le01 # │   ├── les01 # │   │   └── less01 # │   └── les02 # │   ├── less01 # │   └── less02 # ├── lf01 # │   ├── lfs01 # │   │   └── lfss01 # │   │   └── commonfile.txt # │   └── lfs02 # │   ├── lfss01 # │   │   └── commonfile.txt # │   └── lfss02 # │   └── commonfile.txt # └── ls 01 # ├── ls s01 # │   └── ls ss01 # │   └── common file.txt # └── ls s02 # ├── ls ss01 # │   └── common file.txt # └── ls ss02 # └── common file.txt #initialize dynamic contents: clear_local_and_remote() class MEGAcmdFindTest(unittest.TestCase): @classmethod def setUpClass(cls): #INITIALIZATION clean_all() initialize() @classmethod def tearDownClass(cls): if not VERBOSE: clean_all() def setUp(self): print(f"\n=== Running test: {self._testMethodName} ===") def compare_find(self, what, localFindPrefix='localUPs'): if not isinstance(what, list): what = [what] megafind=b"" localfind="" for w in what: megafind+=cmd_ef(FIND+" "+w)+b"\n" localfind+=find(localFindPrefix+'/'+w,w)+"\n" megafind=sort(megafind).strip() localfind=sort(localfind).strip() #~ megafind=$FIND "$@" | sort > $ABSPWD/megafind.txt #~ (cd localUPs 2>/dev/null; find "$@" | sed "s#\./##g" | sort) > $ABSPWD/localfind.txt self.assertEqual(megafind, localfind) def test_empty_file(self): self.compare_find('file01.txt') def test_entire_empty_folders(self): self.compare_find('le01/les01/less01') def test_1_file_folder(self): self.compare_find('lf01/lfs01/lfss01') def test_entire_non_empty_folders_structure(self): self.compare_find('lf01') def test_multiple_names(self): self.compare_find(['lf01','le01/les01']) def test_current_wd(self): """.""" cmd_ef(CD+" le01") self.compare_find('.','localUPs/le01') cmd_ef(CD+" /") def test_current_wd_global(self): """. global""" self.compare_find('.') def test_spaced(self): megafind=sort(cmd_ef(FIND+" "+"ls\\ 01")) localfind=sort(find('localUPs/ls 01',"ls 01")) self.assertEqual(megafind,localfind) def test_multiple(self): """XX/..""" megafind=sort(cmd_ef(FIND+" "+"ls\\ 01/..")) localfind=sort(find('localUPs/',"/")) self.assertEqual(megafind,localfind) def test_multiple_2(self): cmd_ef(CD+' le01') megafind=sort(cmd_ef(FIND+" "+"..")) cmd_ef(CD+' /') localfind=sort(find('localUPs/',"/")) self.assertEqual(megafind,localfind) def test_complex(self): megafind=sort(cmd_ef(FIND+" "+"ls\\ 01/../le01/les01" +" " +"lf01/../ls\\ *01/ls\\ s02")) localfind=sort(find('localUPs/le01/les01',"/le01/les01")) localfind+="\n"+sort(find('localUPs/ls 01/ls s02',"/ls 01/ls s02")) self.assertEqual(megafind,localfind) def test_inside_folder(self): megafind=sort(cmd_ef(FIND+" "+"le01/")) localfind=sort(find('localUPs/le01',"le01")) self.assertEqual(megafind,localfind) def test_non_existent(self): file = 'file01.txt/non-existent' out, status, err = cmd_ec(f'{FIND} {file}') if not CMDSHELL: self.assertNotEqual(status, 0) self.assertIn(f'{file}: no such file or directory', err.decode().lower()) def test_root(self): self.compare_find('/') ###TODO: do stuff in shared folders... if __name__ == '__main__': if "OUT_DIR_JUNIT_XML" in os.environ: unittest.main(testRunner=xmlrunner.XMLTestRunner(output=os.environ["OUT_DIR_JUNIT_XML"]), failfast=False, buffer=False, catchbreak=False) else: unittest.main() MEGAcmd-2.5.2_Linux/tests/megacmd_find_test.sh000066400000000000000000000163301516543156300212420ustar00rootroot00000000000000#!/bin/bash GET=mega-get PUT=mega-put RM=mega-rm CD=mega-cd LCD=mega-lcd MKDIR=mega-mkdir EXPORT=mega-export FIND=mega-find if [ "x$VERBOSE" == "x" ]; then VERBOSE=0 fi if [ "x$MEGACMDSHELL" != "x" ]; then FIND="executeinMEGASHELL find" CMDSHELL=1 fi function executeinMEGASHELL() { command=$1 shift; echo $command "$@" > /tmp/shellin $MEGACMDSHELL < /tmp/shellin | sed "s#^.*\[K##g" | grep $MEGA_EMAIL -A 1000 | grep -v $MEGA_EMAIL } function clean_all() { if [[ $(mega-whoami) != *"$MEGA_EMAIL" ]]; then mega-logout || : mega-login $MEGA_EMAIL $MEGA_PWD || exit -1 fi rm pipe > /dev/null 2>/dev/null || : $RM -rf "*" > /dev/null $RM -rf "//bin/*" > /dev/null if [ -e localUPs ]; then rm -r localUPs; fi if [ -e localtmp ]; then rm -r localtmp; fi if [ -e megafind.txt ]; then rm megafind.txt; fi if [ -e localfind.txt ]; then rm localfind.txt; fi } function clear_local_and_remote() { rm -r localUPs/* 2>/dev/null $RM -rf "/*" 2>/dev/null || : initialize_contents } ABSPWD=`pwd` currentTest=1 function compare_and_clear() { if [ "$VERBOSE" == "1" ]; then echo "test $currentTest" fi $FIND | sort > megafind.txt (cd localUPs; find | sed "s#\./##g" | sort) > localfind.txt if diff --side-by-side megafind.txt localfind.txt 2>/dev/null >/dev/null; then if [ "$VERBOSE" == "1" ]; then echo "diff megafind vs localfind:" diff --side-by-side megafind.txt localfind.txt fi echo "test $currentTest succesful!" else echo "test $currentTest failed!" echo "diff megafind vs localfind:" diff --side-by-side megafind.txt localfind.txt cd $ABSPWD exit 1 fi clear_local_and_remote currentTest=$((currentTest+1)) $CD / } function check_failed_and_clear() { if [ "$?" != "0" ]; then echo "test $currentTest succesful!" else echo "test $currentTest failed!" cd $ABSPWD exit 1 fi clear_local_and_remote currentTest=$((currentTest+1)) $CD / } function initialize () { if [[ $(mega-whoami) != *"$MEGA_EMAIL" ]]; then mega-logout || : mega-login $MEGA_EMAIL $MEGA_PWD || exit -1 fi if [ "$(ls -A .)" ]; then echo "initialization folder not empty!" cd $ABSPWD exit 1 fi if [ $($FIND / | grep -v "^$" |wc -l) != 1 ]; then echo "REMOTE Not empty, please clear it before starting!" #~ $FIND / cd $ABSPWD exit 1 fi #initialize localtmp estructure: mkdir -p localtmp touch localtmp/file01.txt echo file01contents > localtmp/file01nonempty.txt #local empty folders structure mkdir -p localtmp/le01/{les01/less01,les02/less0{1,2}} #local filled folders structure mkdir -p localtmp/lf01/{lfs01/lfss01,lfs02/lfss0{1,2}} touch localtmp/lf01/{lfs01/lfss01,lfs02/lfss0{1,2}}/commonfile.txt #spaced structure mkdir -p localtmp/ls\ 01/{ls\ s01/ls\ ss01,ls\ s02/ls\ ss0{1,2}} touch localtmp/ls\ 01/{ls\ s01/ls\ ss01,ls\ s02/ls\ ss0{1,2}}/common\ file.txt # localtmp/ # ├── file01nonempty.txt # ├── file01.txt # ├── le01 # │   ├── les01 # │   │   └── less01 # │   └── les02 # │   ├── less01 # │   └── less02 # ├── lf01 # │   ├── lfs01 # │   │   └── lfss01 # │   │   └── commonfile.txt # │   └── lfs02 # │   ├── lfss01 # │   │   └── commonfile.txt # │   └── lfss02 # │   └── commonfile.txt # └── ls 01 # ├── ls s01 # │   └── ls ss01 # │   └── common file.txt # └── ls s02 # ├── ls ss01 # │   └── common file.txt # └── ls ss02 # └── common file.txt #initialize dynamic contents: clear_local_and_remote } function initialize_contents() { $PUT localtmp/* / mkdir -p localUPs cp -r localtmp/* localUPs/ } function compare_find(){ if [ "$VERBOSE" == "1" ]; then echo "test $currentTest" fi $FIND "$@" | sort > $ABSPWD/megafind.txt (cd localUPs 2>/dev/null; find "$@" | sed "s#\./##g" | sort) > $ABSPWD/localfind.txt if diff -B --side-by-side $ABSPWD/megafind.txt $ABSPWD/localfind.txt 2>/dev/null >/dev/null; then if [ "$VERBOSE" == "1" ]; then echo "diff megafind vs localfind:" diff -B --side-by-side $ABSPWD/megafind.txt $ABSPWD/localfind.txt fi echo "test $currentTest succesful!" else echo "test $currentTest failed!" echo "diff megafind vs localfind:" diff -B --side-by-side $ABSPWD/megafind.txt $ABSPWD/localfind.txt cd $ABSPWD exit 1 fi currentTest=$((currentTest+1)) } function find_remote(){ if [ "$VERBOSE" == "1" ]; then echo "test $currentTest" fi $FIND "$@" | sort > $ABSPWD/megafind.txt } function find_local(){ if [ "Darwin" == `uname` ]; then (cd localUPs 2>/dev/null; find "$@" | sed "s#\.///#/#g" | sed "s#\.//#/#g" | sort) > $ABSPWD/localfind.txt else (cd localUPs 2>/dev/null; find "$@" | sed "s#\./##g" | sort) > $ABSPWD/localfind.txt fi } function find_local_append(){ (cd localUPs 2>/dev/null; find "$@" | sed "s#\./##g" | sort) >> $ABSPWD/localfind.txt } function compare_remote_local(){ if diff -B --side-by-side $ABSPWD/megafind.txt $ABSPWD/localfind.txt 2>/dev/null >/dev/null; then if [ "$VERBOSE" == "1" ]; then echo "diff megafind vs localfind:" diff -B --side-by-side $ABSPWD/megafind.txt $ABSPWD/localfind.txt fi echo "test $currentTest succesful!" else echo "test $currentTest failed!" echo "diff megafind vs localfind:" diff -B --side-by-side $ABSPWD/megafind.txt $ABSPWD/localfind.txt cd $ABSPWD exit 1 fi currentTest=$((currentTest+1)) } if [ "$MEGA_EMAIL" == "" ] || [ "$MEGA_PWD" == "" ]; then echo "You must define variables MEGA_EMAIL MEGA_PWD. WARNING: Use an empty account for $MEGA_EMAIL" cd $ABSPWD exit 1 fi #INITIALIZATION clean_all initialize ABSMEGADLFOLDER=$PWD/megaDls #Test 01 #destiny empty file compare_find file01.txt #Test 02 #entire empty folders compare_find le01/les01/less01 #Test 03 #1 file folder compare_find lf01/lfs01/lfss01 #Test 04 #entire non empty folders structure compare_find lf01 #Test 05 #multiple compare_find lf01 le01/les01 #Test 06 #. $CD le01 pushd localUPs/le01 > /dev/null compare_find . $CD / popd > /dev/null #Test 07 #. global compare_find . #Test 08 # spaced find_remote "ls\ 01" find_local ls\ 01 compare_remote_local #Test 09 #XX/.. find_remote "ls\ 01/.." find_local .// compare_remote_local #Test 10 #.. $CD le01 find_remote .. $CD / find_local .// compare_remote_local #Test 11 #complex stuff #$RM -rf ls\ 01/../le01/les01 lf01/../ls*/ls\ s02 #This one fails since it is PCRE expresion TODO: if ever supported PCRE enabling/disabling, consider that find_remote "ls\ 01/../le01/les01" "lf01/../ls\ *01/ls\ s02" find_local .//le01/les01 find_local_append .//ls\ 01/ls\ s02 compare_remote_local #Test 12 #folder/ find_remote le01/ find_local le01 compare_remote_local if [ "x$CMDSHELL" != "x1" ]; then #TODO: currently there is no way to know last CMSHELL status code #Test 13 #file01.txt/non-existent $FIND file01.txt/non-existent >/dev/null 2>/dev/null if [ $? == 0 ]; then echo "test $currentTest failed!"; cd $ABSPWD; exit 1; else echo "test $currentTest succesful!"; fi fi currentTest=14 #Test 14 #/ find_remote / find_local .// compare_remote_local ###TODO: do stuff in shared folders... ######## # Clean all if [ "$VERBOSE" != "1" ]; then clean_all fi MEGAcmd-2.5.2_Linux/tests/megacmd_get_test.py000077500000000000000000000320311516543156300211160ustar00rootroot00000000000000#!/usr/bin/python3 # -*- coding: utf-8 -*- #better run in an empty folder import os, shutil import unittest import xmlrunner from megacmd_tests_common import * def setUpModule(): global ABSPWD global ABSMEGADLFOLDER ABSPWD = os.getcwd() ABSMEGADLFOLDER=ABSPWD+'/megaDls' def clean_all(): if not clean_root_confirmed_by_user(): raise Exception("Tests need to be run with YES_I_KNOW_THIS_WILL_CLEAR_MY_MEGA_ACCOUNT=1") if cmd_es(WHOAMI) != osvar("MEGA_EMAIL"): cmd_ef(LOGOUT) cmd_ef(LOGIN+" " +osvar("MEGA_EMAIL")+" "+osvar("MEGA_PWD")) cmd_ec(RM+' -rf "/*"') cmd_ec(RM+' -rf "*"') cmd_ec(RM+' -rf "//bin/*"') rmfolderifexisting("localUPs") rmfolderifexisting("localtmp") rmfolderifexisting("origin") rmfolderifexisting("megaDls") rmfolderifexisting("localDls") rmfileifexisting("megafind.txt") rmfileifexisting("localfind.txt") def clear_dls(): rmcontentsifexisting("megaDls") rmcontentsifexisting("localDls") def safe_export(path): command = EXPORT + ' ' + path stdout, _, err = cmd_esc(command + ' -f -a') # Run export without the `-a` option (also need to remove `-f` or it'll fail) # Output is slightly different in this case: # * Link is surrounded by parenthesis, so we need to remove the trailing ')' # * If path is a folder it'll also return the list of nodes inside, so we # need to keep only the first line # Note: We can't rely on the exit code, since we can't get it reliably from shell commands if b'already exported' in err: stdout = cmd_ef(command).split(b'\n')[0].strip(b')') if b'AuthKey=' in stdout: # In this case the authkey is the last word, # so the link is second from the right return stdout.split(b' ')[-2] else: return stdout.split(b' ')[-1] else: return stdout.split(b' ')[-1] def initialize_contents(): global URIFOREIGNEXPORTEDFOLDER global URIFOREIGNEXPORTEDFILE global URIEXPORTEDFOLDER global URIEXPORTEDFILE if cmd_es(WHOAMI) != osvar("MEGA_EMAIL_AUX"): cmd_ef(LOGOUT) cmd_ef(LOGIN+" " +osvar("MEGA_EMAIL_AUX")+" "+osvar("MEGA_PWD_AUX")) if len(os.listdir(".")): raise Exception("initialization folder not empty!") #initialize localtmp estructure: makedir('foreign') for f in ['cloud0'+a for a in ['1','2']] + \ ['bin0'+a for a in ['1','2']] + \ ['cloud0'+a+'/c0'+b+'s0'+c for a in ['1','2'] for b in ['1','2'] for c in ['1','2']] + \ ['foreign'] + \ ['foreign/sub0'+a for a in ['1','2']] + \ ['bin0'+a+'/c0'+b+'s0'+c for a in ['1','2'] for b in ['1','2'] for c in ['1','2']]: makedir(f) out(f,f+'/fileat'+f.split('/')[-1]+'.txt') cmd_ef(PUT+' foreign /') cmd_ef(SHARE+' foreign -a --with='+osvar("MEGA_EMAIL")) URIFOREIGNEXPORTEDFOLDER=safe_export('foreign/sub01').decode() URIFOREIGNEXPORTEDFILE=safe_export('foreign/sub02/fileatsub02.txt').decode() print(f'URIFOREIGNEXPORTEDFOLDER={URIFOREIGNEXPORTEDFOLDER}') print(f'URIFOREIGNEXPORTEDFILE={URIFOREIGNEXPORTEDFILE}') cmd_ef(LOGOUT) cmd_ef(LOGIN+" " +osvar("MEGA_EMAIL")+" "+osvar("MEGA_PWD")) cmd_ec(IPC+" -a "+osvar("MEGA_EMAIL_AUX")) cmd_ef(PUT+' foreign /') #~ mega-put cloud0* / cmd_ef(PUT+" cloud01 cloud02 /") #~ mega-put bin0* //bin cmd_ef(PUT+" bin01 bin02 //bin") URIEXPORTEDFOLDER=safe_export('cloud01/c01s01').decode() URIEXPORTEDFILE=safe_export('cloud02/fileatcloud02.txt').decode() print(f'URIEXPORTEDFOLDER={URIEXPORTEDFOLDER}') print(f'URIEXPORTEDFILE={URIEXPORTEDFILE}') class MEGAcmdGetTest(unittest.TestCase): @classmethod def setUpClass(cls): global URIEXPORTEDFOLDER global URIEXPORTEDFILE clean_all() makedir('origin') os.chdir('origin') initialize_contents() os.chdir(ABSPWD) makedir('megaDls') makedir('localDls') URIEXPORTEDFOLDER=safe_export('cloud01/c01s01').decode() URIEXPORTEDFILE=safe_export('cloud02/fileatcloud02.txt').decode() clear_dls() @classmethod def tearDownClass(cls): clean_all() def setUp(self): print(f"\n=== Running test: {self._testMethodName} ===") def tearDown(self): clear_dls() cmd_ef(CD+" /") def check_failed(self, o, status): self.assertNotEqual(status, 0, o) def compare(self): megaDls=sort(find('megaDls')) localDls=sort(find('localDls')) self.assertEqual(megaDls, localDls) print(f"megaDls: {megaDls}, localDls: {localDls}") def test_01(self): cmd_ef(GET+' /cloud01/fileatcloud01.txt '+ABSMEGADLFOLDER+'/') shutil.copy2('origin/cloud01/fileatcloud01.txt','localDls/') self.compare() def test_02(self): cmd_ef(GET+' //bin/bin01/fileatbin01.txt '+ABSMEGADLFOLDER) shutil.copy2('origin/bin01/fileatbin01.txt','localDls/') self.compare() def test_03(self): #Test 03 cmd_ef(GET+' //bin/bin01/fileatbin01.txt '+ABSMEGADLFOLDER+'/') shutil.copy2('origin/bin01/fileatbin01.txt','localDls/') self.compare() def test_04(self): cmd_ef(GET+' '+osvar('MEGA_EMAIL_AUX')+':foreign/fileatforeign.txt '+ABSMEGADLFOLDER+'/') shutil.copy2('origin/foreign/fileatforeign.txt','localDls/') self.compare() def test_05(self): cmd_ef(GET+' '+osvar('MEGA_EMAIL_AUX')+':foreign/fileatforeign.txt '+ABSMEGADLFOLDER+'/') shutil.copy2('origin/foreign/fileatforeign.txt','localDls/') self.compare() def test_06(self): cmd_ef(CD+' cloud01') cmd_ef(GET+' "*.txt" '+ABSMEGADLFOLDER+'') copybyfilepattern('origin/cloud01/','*.txt','localDls/') self.compare() def test_07(self): cmd_ef(CD+' //bin/bin01') cmd_ef(GET+' "*.txt" '+ABSMEGADLFOLDER+'') copybyfilepattern('origin/bin01/','*.txt','localDls/') self.compare() def test_08(self): cmd_ef(CD+' '+osvar('MEGA_EMAIL_AUX')+':foreign') cmd_ef(GET+' "*.txt" '+ABSMEGADLFOLDER+'') copybyfilepattern('origin/foreign/','*.txt','localDls/') self.compare() def test_09(self): cmd_ef(GET+' cloud01/c01s01 '+ABSMEGADLFOLDER+'') copyfolder('origin/cloud01/c01s01','localDls/') self.compare() def test_10(self): cmd_ef(GET+' cloud01/c01s01 '+ABSMEGADLFOLDER+'/') copyfolder('origin/cloud01/c01s01','localDls/') self.compare() def test_11(self): cmd_ef(GET+' cloud01/c01s01 '+ABSMEGADLFOLDER+' -m') shutil.copytree('origin/cloud01/c01s01/', 'localDls/', dirs_exist_ok=True) self.compare() def test_12(self): cmd_ef(GET+' cloud01/c01s01 '+ABSMEGADLFOLDER+'/ -m') shutil.copytree('origin/cloud01/c01s01/', 'localDls/', dirs_exist_ok=True) self.compare() def test_13(self): #Test 13 cmd_ef(GET+' "'+URIEXPORTEDFOLDER+'" '+ABSMEGADLFOLDER+'') copyfolder('origin/cloud01/c01s01','localDls/') self.compare() def test_14(self): cmd_ef(GET+' "'+URIEXPORTEDFILE+'" '+ABSMEGADLFOLDER+'') shutil.copy2('origin/cloud02/fileatcloud02.txt','localDls/') self.compare() def test_15(self): cmd_ef(GET+' "'+URIEXPORTEDFOLDER+'" '+ABSMEGADLFOLDER+' -m') shutil.copytree('origin/cloud01/c01s01/', 'localDls/', dirs_exist_ok=True) self.compare() def test_16(self): cmd_ef(CD+' /cloud01/c01s01') cmd_ef(GET+' . '+ABSMEGADLFOLDER+'') copyfolder('origin/cloud01/c01s01','localDls/') self.compare() def test_17(self): cmd_ef(CD+' /cloud01/c01s01') cmd_ef(GET+' . '+ABSMEGADLFOLDER+' -m') shutil.copytree('origin/cloud01/c01s01/', 'localDls/', dirs_exist_ok=True) self.compare() def test_18(self): cmd_ef(CD+' /cloud01/c01s01') cmd_ef(GET+' ./ '+ABSMEGADLFOLDER+'') copyfolder('origin/cloud01/c01s01','localDls/') self.compare() def test_19(self): cmd_ef(CD+' /cloud01/c01s01') cmd_ef(GET+' ./ '+ABSMEGADLFOLDER+' -m') shutil.copytree('origin/cloud01/c01s01/', 'localDls/', dirs_exist_ok=True) self.compare() def test_20(self): cmd_ef(CD+' /cloud01/c01s01') cmd_ef(GET+' .. '+ABSMEGADLFOLDER+' -m') shutil.copytree('origin/cloud01/', 'localDls/', dirs_exist_ok=True) self.compare() def test_21(self): cmd_ef(CD+' /cloud01/c01s01') cmd_ef(GET+' ../ '+ABSMEGADLFOLDER+'') copyfolder('origin/cloud01','localDls/') self.compare() def test_22(self): out("existing",ABSMEGADLFOLDER+'/existing') cmd_ef(GET+' /cloud01/fileatcloud01.txt '+ABSMEGADLFOLDER+'/existing') shutil.copy2('origin/cloud01/fileatcloud01.txt','localDls/existing (1)') out("existing",'localDls/existing') self.compare() def test_23(self): out("existing",'megaDls/existing') cmd_ef(GET+' /cloud01/fileatcloud01.txt megaDls/existing') shutil.copy2('origin/cloud01/fileatcloud01.txt','localDls/existing (1)') out("existing",'localDls/existing') self.compare() def test_24(self): cmd_ef(GET+' cloud01/c01s01 megaDls') copyfolder('origin/cloud01/c01s01','localDls/') self.compare() def test_25(self): cmd_ef(GET+' cloud01/fileatcloud01.txt megaDls') shutil.copy2('origin/cloud01/fileatcloud01.txt','localDls/') self.compare() def test_26(self): o, status, e = cmd_ec(f'{GET} cloud01/fileatcloud01.txt /no/where') if not CMDSHELL: self.check_failed(o, status) self.assertIn('is not a valid download folder', e.decode().lower()) def test_27(self): o, status, e = cmd_ec(f'{GET} /cloud01/cloud01/fileatcloud01.txt /no/where') if not CMDSHELL: self.check_failed(o, status) self.assertIn('is not a valid download folder', e.decode().lower()) def test_28(self): cmd_ef(GET+' /cloud01/fileatcloud01.txt '+ABSMEGADLFOLDER+'/newfile') shutil.copy2('origin/cloud01/fileatcloud01.txt','localDls/newfile') self.compare() def test_29(self): os.chdir(ABSMEGADLFOLDER) cmd_ef(GET+' /cloud01/fileatcloud01.txt .') os.chdir(ABSPWD) shutil.copy2('origin/cloud01/fileatcloud01.txt','localDls/') self.compare() def test_30(self): os.chdir(ABSMEGADLFOLDER) cmd_ef(GET+' /cloud01/fileatcloud01.txt ./') os.chdir(ABSPWD) shutil.copy2('origin/cloud01/fileatcloud01.txt','localDls/') self.compare() def test_31(self): makedir(ABSMEGADLFOLDER+'/newfol') os.chdir(ABSMEGADLFOLDER+'/newfol') cmd_ef(GET+' /cloud01/fileatcloud01.txt ..') os.chdir(ABSPWD) makedir('localDls/newfol') shutil.copy2('origin/cloud01/fileatcloud01.txt','localDls/') self.compare() def test_32(self): makedir(ABSMEGADLFOLDER+'/newfol') os.chdir(ABSMEGADLFOLDER+'/newfol') cmd_ef(GET+' /cloud01/fileatcloud01.txt ../') os.chdir(ABSPWD) makedir('localDls/newfol') shutil.copy2('origin/cloud01/fileatcloud01.txt','localDls/') self.compare() def test_33(self): o, status, e = cmd_ec(f'{GET} path/to/nowhere {ABSMEGADLFOLDER}') if not CMDSHELL: self.check_failed(o, status) self.assertIn("couldn't find file", e.decode().lower()) def test_34(self): o, status, e = cmd_ec(f'{GET} /path/to/nowhere {ABSMEGADLFOLDER}') if not CMDSHELL: self.check_failed(o, status) self.assertIn("couldn't find file", e.decode().lower()) def test_35(self): os.chdir(ABSMEGADLFOLDER) cmd_ef(GET+' /cloud01/fileatcloud01.txt') os.chdir(ABSPWD) shutil.copy2('origin/cloud01/fileatcloud01.txt','localDls/') self.compare() def test_36_import_folder(self): cmd_ex(RM+' -rf /imported') cmd_ef(MKDIR+' -p /imported') cmd_ef(IMPORT+' '+URIFOREIGNEXPORTEDFOLDER+' /imported') cmd_ef(GET+' /imported/* '+ABSMEGADLFOLDER+'') copyfolder('origin/foreign/sub01','localDls/') self.compare() def test_37_import_file(self): cmd_ex(RM+' -rf /imported') cmd_ef(MKDIR+' -p /imported') cmd_ef(IMPORT+' '+URIFOREIGNEXPORTEDFILE+' /imported') cmd_ef(GET+' /imported/fileatsub02.txt '+ABSMEGADLFOLDER+'') shutil.copy2('origin/foreign/sub02/fileatsub02.txt','localDls/') self.compare() def test_38_download_file_from_inshare(self): """get from //from/XXX""" cmd_ef(GET+' //from/'+osvar('MEGA_EMAIL_AUX')+':foreign/sub02/fileatsub02.txt '+ABSMEGADLFOLDER+'') shutil.copy2('origin/foreign/sub02/fileatsub02.txt','localDls/') self.compare() if __name__ == '__main__': if "OUT_DIR_JUNIT_XML" in os.environ: unittest.main(testRunner=xmlrunner.XMLTestRunner(output=os.environ["OUT_DIR_JUNIT_XML"]), failfast=False, buffer=False, catchbreak=False) else: unittest.main() MEGAcmd-2.5.2_Linux/tests/megacmd_get_test.sh000066400000000000000000000202341516543156300210770ustar00rootroot00000000000000#!/bin/bash #better run in an empty folder GET=mega-get RM=mega-rm CD=mega-cd LCD=mega-lcd EXPORT=mega-export MKDIR=mega-mkdir IMPORT=mega-import ABSPWD=`pwd` if [ "x$VERBOSE" == "x" ]; then VERBOSE=0 fi if [ "x$MEGACMDSHELL" != "x" ]; then GET="executeinMEGASHELL get" CMDSHELL=1 fi function executeinMEGASHELL() { command=$1 shift; echo lcd "$PWD" > /tmp/shellin echo $command "$@" >> /tmp/shellin #~ echo $command "$@" > /tmp/shellin $MEGACMDSHELL < /tmp/shellin | sed "s#^.*\[K##g" | grep $MEGA_EMAIL -A 1000 | grep -v $MEGA_EMAIL } function clean_all() { if [[ $(mega-whoami) != *"$MEGA_EMAIL" ]]; then mega-logout || : mega-login $MEGA_EMAIL $MEGA_PWD || exit -1 fi $RM -rf "*" > /dev/null $RM -rf "//bin/*" > /dev/null if [ -e origin ]; then rm -r origin; fi if [ -e megaDls ]; then rm -r megaDls; fi if [ -e localDls ]; then rm -r localDls; fi } function clear_dls() { rm -r megaDls/* 2>/dev/null rm -r localDls/* 2>/dev/null } currentTest=1 function compare_and_clear() { if [ "$VERBOSE" == "1" ]; then echo "test $currentTest" echo "megaDls:" find megaDls echo echo "localDls:" find localDls echo fi if diff megaDls localDls 2>/dev/null >/dev/null; then echo "test $currentTest succesful!" else echo "test $currentTest failed!" cd $ABSPWD exit 1 fi clear_dls currentTest=$((currentTest+1)) $CD / } function check_failed_and_clear() { if [ "$?" != "0" ]; then echo "test $currentTest succesful!" else echo "test $currentTest failed!" cd $ABSPWD exit 1 fi clear_dls currentTest=$((currentTest+1)) $CD / } function initialize_contents() { if [[ $(mega-whoami) != *"$MEGA_EMAIL_AUX" ]]; then mega-logout || : mega-login $MEGA_EMAIL_AUX $MEGA_PWD_AUX || exit -1 fi if [ "$(ls -A .)" ]; then echo "initialization folder not empty!" cd $ABSPWD exit 1 fi mkdir -p cloud0{1,2}/c0{1,2}s0{1,2} mkdir -p foreign/sub0{1,2} mkdir -p bin0{1,2}/c0{1,2}s0{1,2} for i in `find *`; do echo $i > $i/fileat`basename $i`.txt; done mega-put foreign / mega-share foreign -a --with=$MEGA_EMAIL URIFOREIGNEXPORTEDFOLDER=`$EXPORT foreign/sub01 -a | awk '{print $NF}'` URIFOREIGNEXPORTEDFILE=`$EXPORT foreign/sub02/fileatsub02.txt -a | awk '{print $NF}'` mega-logout mega-login $MEGA_EMAIL $MEGA_PWD mega-ipc -a $MEGA_EMAIL_AUX >/dev/null mega-put cloud0* / mega-put bin0* //bin URIEXPORTEDFOLDER=`$EXPORT cloud01/c01s01 -a | awk '{print $NF}'` URIEXPORTEDFILE=`$EXPORT cloud02/fileatcloud02.txt -a | awk '{print $NF}'` } if [ "$MEGA_EMAIL" == "" ] || [ "$MEGA_PWD" == "" ] || [ "$MEGA_EMAIL_AUX" == "" ] || [ "$MEGA_PWD_AUX" == "" ]; then echo "You must define variables MEGA_EMAIL MEGA_PWD MEGA_EMAIL_AUX MEGA_PWD_AUX. WARNING: Use an empty account for $MEGA_EMAIL" cd $ABSPWD exit 1 fi #INITIALIZATION clean_all mkdir origin pushd origin > /dev/null initialize_contents popd > /dev/null mkdir -p megaDls mkdir -p localDls ABSMEGADLFOLDER=$PWD/megaDls URIEXPORTEDFOLDER=`mega-export cloud01/c01s01 -a | awk '{print $NF}'` URIEXPORTEDFILE=`mega-export cloud02/fileatcloud02.txt -a | awk '{print $NF}'` clear_dls #Test 01 $GET /cloud01/fileatcloud01.txt $ABSMEGADLFOLDER/ cp origin/cloud01/fileatcloud01.txt localDls/ compare_and_clear #Test 02 $GET //bin/bin01/fileatbin01.txt $ABSMEGADLFOLDER cp origin/bin01/fileatbin01.txt localDls/ compare_and_clear #Test 03 $GET //bin/bin01/fileatbin01.txt $ABSMEGADLFOLDER/ cp origin/bin01/fileatbin01.txt localDls/ compare_and_clear #Test 04 $GET $MEGA_EMAIL_AUX:foreign/fileatforeign.txt $ABSMEGADLFOLDER/ cp origin/foreign/fileatforeign.txt localDls/ compare_and_clear #Test 05 $GET $MEGA_EMAIL_AUX:foreign/fileatforeign.txt $ABSMEGADLFOLDER/ cp origin/foreign/fileatforeign.txt localDls/ compare_and_clear #Test 06 $CD cloud01 $GET "*.txt" $ABSMEGADLFOLDER cp origin/cloud01/*.txt localDls/ compare_and_clear #Test 07 $CD //bin/bin01 $GET "*.txt" $ABSMEGADLFOLDER cp origin/bin01/*.txt localDls/ compare_and_clear #Test 08 $CD $MEGA_EMAIL_AUX:foreign $GET "*.txt" $ABSMEGADLFOLDER cp origin/foreign/*.txt localDls/ compare_and_clear #Test 09 $GET cloud01/c01s01 $ABSMEGADLFOLDER cp -r origin/cloud01/c01s01 localDls/ compare_and_clear #Test 10 $GET cloud01/c01s01 $ABSMEGADLFOLDER/ cp -r origin/cloud01/c01s01 localDls/ compare_and_clear #Test 11 $GET cloud01/c01s01 $ABSMEGADLFOLDER -m rsync -r origin/cloud01/c01s01/ localDls/ compare_and_clear #Test 12 $GET cloud01/c01s01 $ABSMEGADLFOLDER/ -m rsync -r origin/cloud01/c01s01/ localDls/ compare_and_clear #Test 13 $GET "$URIEXPORTEDFOLDER" $ABSMEGADLFOLDER cp -r origin/cloud01/c01s01 localDls/ compare_and_clear #Test 14 $GET "$URIEXPORTEDFILE" $ABSMEGADLFOLDER cp origin/cloud02/fileatcloud02.txt localDls/ compare_and_clear #Test 15 $GET "$URIEXPORTEDFOLDER" $ABSMEGADLFOLDER -m rsync -r origin/cloud01/c01s01/ localDls/ compare_and_clear #Test 16 $CD /cloud01/c01s01 $GET . $ABSMEGADLFOLDER cp -r origin/cloud01/c01s01 localDls/ compare_and_clear #Test 17 $CD /cloud01/c01s01 $GET . $ABSMEGADLFOLDER -m rsync -r origin/cloud01/c01s01/ localDls/ compare_and_clear #Test 18 $CD /cloud01/c01s01 $GET ./ $ABSMEGADLFOLDER cp -r origin/cloud01/c01s01 localDls/ compare_and_clear #Test 19 $CD /cloud01/c01s01 $GET ./ $ABSMEGADLFOLDER -m rsync -r origin/cloud01/c01s01/ localDls/ compare_and_clear #Test 20 $CD /cloud01/c01s01 $GET .. $ABSMEGADLFOLDER -m rsync -r origin/cloud01/ localDls/ compare_and_clear #Test 21 $CD /cloud01/c01s01 $GET ../ $ABSMEGADLFOLDER cp -r origin/cloud01 localDls/ compare_and_clear #Test 22 echo "existing" > $ABSMEGADLFOLDER/existing $GET /cloud01/fileatcloud01.txt $ABSMEGADLFOLDER/existing cp origin/cloud01/fileatcloud01.txt "localDls/existing (1)" echo "existing" > localDls/existing compare_and_clear #Test 23 echo "existing" > megaDls/existing $GET /cloud01/fileatcloud01.txt megaDls/existing cp origin/cloud01/fileatcloud01.txt "localDls/existing (1)" echo "existing" > localDls/existing compare_and_clear #Test 24 $GET cloud01/c01s01 megaDls cp -r origin/cloud01/c01s01 localDls/ compare_and_clear #Test 25 $GET cloud01/fileatcloud01.txt megaDls cp -r origin/cloud01/fileatcloud01.txt localDls/ compare_and_clear if [ "x$CMDSHELL" != "x1" ]; then #TODO: currently there is no way to know last CMSHELL status code #Test 26 $GET cloud01/fileatcloud01.txt /no/where > /dev/null check_failed_and_clear #Test 27 $GET /cloud01/cloud01/fileatcloud01.txt /no/where > /dev/null check_failed_and_clear fi currentTest=28 #Test 28 $GET /cloud01/fileatcloud01.txt $ABSMEGADLFOLDER/newfile cp origin/cloud01/fileatcloud01.txt localDls/newfile compare_and_clear #Test 29 pushd $ABSMEGADLFOLDER > /dev/null $GET /cloud01/fileatcloud01.txt . popd > /dev/null cp origin/cloud01/fileatcloud01.txt localDls/ compare_and_clear #Test 30 pushd $ABSMEGADLFOLDER > /dev/null $GET /cloud01/fileatcloud01.txt ./ popd > /dev/null cp origin/cloud01/fileatcloud01.txt localDls/ compare_and_clear #Test 31 mkdir $ABSMEGADLFOLDER/newfol pushd $ABSMEGADLFOLDER/newfol > /dev/null $GET /cloud01/fileatcloud01.txt .. popd > /dev/null mkdir localDls/newfol cp origin/cloud01/fileatcloud01.txt localDls/ compare_and_clear #Test 32 mkdir $ABSMEGADLFOLDER/newfol pushd $ABSMEGADLFOLDER/newfol > /dev/null $GET /cloud01/fileatcloud01.txt ../ popd > /dev/null mkdir localDls/newfol cp origin/cloud01/fileatcloud01.txt localDls/ compare_and_clear if [ "x$CMDSHELL" != "x1" ]; then #TODO: currently there is no way to know last CMSHELL status code #Test 33 $GET path/to/nowhere $ABSMEGADLFOLDER > /dev/null check_failed_and_clear #Test 34 $GET /path/to/nowhere $ABSMEGADLFOLDER > /dev/null check_failed_and_clear fi currentTest=35 #Test 35 pushd $ABSMEGADLFOLDER > /dev/null $GET /cloud01/fileatcloud01.txt popd > /dev/null cp origin/cloud01/fileatcloud01.txt localDls/ compare_and_clear currentTest=36 #Test 36 # imported stuff (to test import folder) $RM -rf /imported 2>&1 >/dev/null || : $MKDIR -p /imported $IMPORT $URIFOREIGNEXPORTEDFOLDER /imported > /dev/null $GET /imported/* $ABSMEGADLFOLDER cp -r origin/foreign/sub01 localDls/ compare_and_clear #Test 37 # imported stuff (to test import file) $RM -rf /imported 2>&1 >/dev/null || : $MKDIR -p /imported $IMPORT $URIFOREIGNEXPORTEDFILE /imported/ > /dev/null $GET /imported/fileatsub02.txt $ABSMEGADLFOLDER cp origin/foreign/sub02/fileatsub02.txt localDls/ compare_and_clear # Clean all clean_all MEGAcmd-2.5.2_Linux/tests/megacmd_misc_test.py000077500000000000000000000401461516543156300213000ustar00rootroot00000000000000#!/usr/bin/python3 # -*- coding: utf-8 -*- import sys, os, shutil, filecmp import unittest import xmlrunner from megacmd_tests_common import * def setUpModule(): global ABSPWD global ABSMEGADLFOLDER ABSPWD = os.getcwd() ABSMEGADLFOLDER=ABSPWD+'/megaDls' def initialize_contents(): cmd_ec(INVITE+" "+osvar("MEGA_EMAIL_AUX")) cmd_ef(LOGOUT) cmd_ef(LOGIN+" " +osvar("MEGA_EMAIL_AUX")+" "+osvar("MEGA_PWD_AUX")) cmd_ec(IPC+" -a "+osvar("MEGA_EMAIL")) cmd_ef(LOGOUT) cmd_ef(LOGIN+" " +osvar("MEGA_EMAIL")+" "+osvar("MEGA_PWD")) contents=" ".join(['"localtmp/'+x+'"' for x in os.listdir('localtmp/')]) cmd_ef(PUT+" "+contents+" /") shutil.copytree('localtmp', 'localUPs') def clean_all(): if not clean_root_confirmed_by_user(): raise Exception("Tests need to be run with YES_I_KNOW_THIS_WILL_CLEAR_MY_MEGA_ACCOUNT=1") if cmd_es(WHOAMI) != osvar("MEGA_EMAIL"): cmd_ef(LOGOUT) cmd_ef(LOGIN+" " +osvar("MEGA_EMAIL")+" "+osvar("MEGA_PWD")) #~ rm pipe > /dev/null 2>/dev/null || : cmd_ec(RM+' -rf "/*"') cmd_ec(RM+' -rf "*"') cmd_ec(RM+' -rf "//bin/*"') rmfolderifexisting("localUPs") rmfolderifexisting("localtmp") rmfileifexisting("megafind.txt") rmfileifexisting("localfind.txt") rmfileifexisting("thumbnail.jpg") def clear_local_and_remote(): rmfolderifexisting("localUPs") cmd_ec(RM+' -rf "/*"') initialize_contents() opts='";X()[]{}<>|`\'' if os.name == 'nt': opts=";()[]{}`'" def initialize(): if cmd_es(WHOAMI) != osvar("MEGA_EMAIL"): cmd_es(LOGOUT) cmd_ef(LOGIN+" " +osvar("MEGA_EMAIL")+" "+osvar("MEGA_PWD")) if len(os.listdir(".")) and ( len(os.listdir(".")) != 1 and os.listdir(".")[0] != 'images'): print("initialization folder not empty!", "\n",os.listdir("."), file=sys.stderr) #~ cd $ABSPWD exit(1) if cmd_es(FIND+" /") != b"/": print("REMOTE Not empty, please clear it before starting!", file=sys.stderr) #~ cd $ABSPWD exit(1) #initialize localtmp estructure: makedir("localtmp") touch("localtmp/file01.txt") out('file01contents', 'localtmp/file01nonempty.txt') #local empty folders structure for f in ['localtmp/le01/'+a for a in ['les01/less01']+ ['les02/less0'+z for z in ['1','2']] ]: makedir(f) #local filled folders structure for f in ['localtmp/lf01/'+a for a in ['lfs01/lfss01']+ ['lfs02/lfss0'+z for z in ['1','2']] ]: makedir(f) for f in ['localtmp/lf01/'+a for a in ['lfs01/lfss01']+ ['lfs02/lfss0'+z for z in ['1','2']] ]: touch(f+"/commonfile.txt") #spaced structure for f in ['localtmp/ls 01/'+a for a in ['ls s01/ls ss01']+ ['ls s02/ls ss0'+z for z in ['1','2']] ]: makedir(f) for f in ['localtmp/ls 01/'+a for a in ['ls s01/ls ss01']+ ['ls s02/ls ss0'+z for z in ['1','2']] ]: touch(f+"/common file.txt") #weird chars makedir("localtmp/odd") for f in ['localtmp/odd/'+a for a in ['file'+z for z in opts+"X"]]: touch(f+'01.txt') #~ localtmp/ #~ ├── file01nonempty.txt #~ ├── file01.txt #~ ├── le01 #~ │   ├── les01 #~ │   │   └── less01 #~ │   └── les02 #~ │   ├── less01 #~ │   └── less02 #~ ├── lf01 #~ │   ├── lfs01 #~ │   │   └── lfss01 #~ │   │   └── commonfile.txt #~ │   └── lfs02 #~ │   ├── lfss01 #~ │   │   └── commonfile.txt #~ │   └── lfss02 #~ │   └── commonfile.txt #~ ├── ls 01 #~ │   ├── ls s01 #~ │   │   └── ls ss01 #~ │   │   └── common file.txt #~ │   └── ls s02 #~ │   ├── ls ss01 #~ │   │   └── common file.txt #~ │   └── ls ss02 #~ │   └── common file.txt #~ └── odd #~ ├── file;.txt #~ ├── .... #initialize dynamic contents: clear_local_and_remote() if VERBOSE: print("STARTING...") class MEGAcmdMiscTest(unittest.TestCase): @classmethod def setUpClass(cls): cls.bar = "" if not CMDSHELL: cls.bar="\\" if os.name == 'nt': cls.bar="" cls.imagesUrl = os.environ.get('MEGACMD_TESTS_IMAGES_URL', "https://mega.nz/folder/bxomFKwL#3V1dUJFzL98t1GqXX29IXg") cls.pdfsURL = os.environ.get('MEGACMD_TESTS_PDFS_URL', "https://mega.nz/folder/D0w0nYiY#egvjqP5R-anbBdsJg8QRVg") clean_all() initialize() @classmethod def tearDownClass(cls): if not VERBOSE: clean_all() def setUp(self): print(f"\n=== Running test: {self._testMethodName} ===") def compare_remote_local(self, megafind, localfind): self.assertEqual(megafind, localfind) print(f"megafind: {megafind}, localfind: {localfind}") def compare_find(self, what, localFindPrefix='localUPs'): if not isinstance(what, list): what = [what] megafind=b"" localfind="" for w in what: megafind+=cmd_ef(FIND+" "+w)+b"\n" localfind+=find(localFindPrefix+'/'+w,w)+"\n" megafind=sort(megafind).strip() localfind=sort(localfind).strip() #~ megafind=$FIND "$@" | sort > $ABSPWD/megafind.txt #~ (cd localUPs 2>/dev/null; find "$@" | sed "s#\./##g" | sort) > $ABSPWD/localfind.txt self.assertEqual(megafind, localfind) print(f"megafind: {megafind}, localfind: {localfind}") def check_failed_and_clear(self, o,status): self.addCleanup(lambda : cmd_ef(CD+" /")) self.assertNotEqual(status, 0, o) def test_01_special_characters(self): for c in opts: with self.subTest(c=c): megafind=sort(cmd_ef(FIND+" "+'odd/file'+self.bar+c+'01.txt')) self.compare_remote_local(megafind,'odd/file'+c+'01.txt') @unittest.skipIf(os.name == "nt", "only for non NT environments") def test_02_unix_special_characters(self): def cleanup(): #revert original situation cmd_ef(MV+" "+'odd/file\\\\01.txt odd/fileX01.txt') shutil.move('localUPs/odd/file\\01.txt', 'localUPs/odd/fileX01.txt') #Test 15 # very special \ character c='\\' cmd_ef(MV+" "+'odd/fileX01.txt odd/file'+self.bar+'\\01.txt') #upload of file containing "\\" fails (SDK issue) shutil.move('localUPs/odd/fileX01.txt', 'localUPs/odd/file\\01.txt') megafind=sort(cmd_ef(FIND+" "+'odd/file'+self.bar+c+'01.txt')) self.addCleanup(cleanup) self.compare_remote_local(megafind,'odd/file'+c+'01.txt') def test_03_move_and_rename(self): def cleanup(): #revert original situation cmd_ef(MV+' /le01/moved.txt'+" file01nonempty.txt ") shutil.move("localUPs/le01/moved.txt", "localUPs/file01nonempty.txt") #Test 16 #move & rename cmd_ef(MV+" file01nonempty.txt "+'/le01/moved.txt') shutil.move("localUPs/file01nonempty.txt", "localUPs/le01/moved.txt") self.addCleanup(cleanup) self.compare_find('/') def test_04_new_empty_file(self): #Test 17 touch("localUPs/newemptyfile.txt") cmd_ef(PUT+" "+"localUPs/newemptyfile.txt"+" "+"/") #shutil.copy2('auxx/01/s01/another.txt','localUPs/01/s01') self.compare_find('/') @unittest.skipIf(os.name == "nt", "only for non NT environments") def test_05_unicode(self): for fname in UNICODE_NAME_LIST: touch("localUPs/"+fname) cmd_ef(PUT+" "+"localUPs/"+fname+" "+"/") self.compare_find('/') def test_06_copy_and_rename_2(self): #Test 19 #copy & rename cmd_ef(CP+" file01nonempty.txt "+'/le01/copied') copybyfilepattern("localUPs/","file01nonempty.txt", "localUPs/le01/copied") self.compare_find('/') def test_07_multicopy(self): #Test 20 #multicopy cmd_ef(CP+" *.txt "+'/le01') copybyfilepattern("localUPs/","*.txt", "localUPs/le01/") self.compare_find('/') def test_08_multicopy_with_trailing_slash(self): #Test 21 #multicopy with trailing / cmd_ef(CP+" *.txt "+'/le01/les01/') copybyfilepattern("localUPs/","*.txt", "localUPs/le01/les01/") self.compare_find('/') # ~ #Test 22 #multisend # ~ cmd_ef(CP+" *.txt "+MEGA_EMAIL_AUX+':') # ~ print "test "+str(currentTest)+" succesful!" # ~ currentTest+=1 def test_09_copy_folder(self): #Test 23 #copy folder cmd_ef(CP+" le01 "+'lf01') copyfolder("localUPs/le01", "localUPs/lf01/") self.compare_find('/') def test_10_send_to_non_contact(self): o, status, e = cmd_ec(f'{CP} *.txt badContact{MEGA_EMAIL_AUX}:') if not CMDSHELL: self.check_failed_and_clear(o, status) self.assertIn('failed to send file to user: not found', e.decode().lower()) def test_11_multicopy_into_file(self): bad_folder = '/le01/file01nonempty.txt' o, status, e = cmd_ec(f'{CP} *.txt {bad_folder}') if not CMDSHELL: self.check_failed_and_clear(o, status) self.assertIn(f'{bad_folder} must be a valid folder', e.decode().lower()) def test_12_copy_into_existing_file(self): #Test 25 #copy into existing file cmd_ef(CP+" -vvv file01nonempty.txt "+'copied2') cmd_ef(CP+" -vvv file01nonempty.txt "+'copied2') copybyfilepattern("localUPs/","file01nonempty.txt", "localUPs/copied2") self.compare_find('/') def test_13_move_into_non_existent_file(self): #Test 26 #move into non existing file cmd_ef(CP+" -vvv file01nonempty.txt "+'copied3') cmd_ef(MV+" -vvv copied3 "+'moved3') copybyfilepattern("localUPs/","file01nonempty.txt", "localUPs/moved3") self.compare_find('/') def test_14_move_into_existing_different_file(self): #Test 28 #move into existing (different) file cmd_ef(CP+" -vvv file01nonempty.txt "+'copied4') cmd_ef(CP+" -vvv file01.txt "+'moved4') cmd_ef(MV+" -vvv copied4 "+'moved4') copybyfilepattern("localUPs/","file01nonempty.txt", "localUPs/moved4") self.compare_find('/') def test_15_move_into_existing_equal_file(self): #Test 29 #move into existing equal file cmd_ef(CP+" -vvv file01nonempty.txt "+'copied5') cmd_ef(CP+" -vvv copied5 "+'moved5') cmd_ef(MV+" -vvv copied5 "+'moved5') copybyfilepattern("localUPs/","file01nonempty.txt", "localUPs/moved5") self.compare_find('/') def test_16_move_into_other_path(self): #Test 30 #move into other path cmd_ef(CP+" -vvv file01nonempty.txt "+'copied6') cmd_ef(MV+" -vvv copied6 "+'/le01/moved6') copybyfilepattern("localUPs/","file01nonempty.txt", "localUPs/le01/moved6") self.compare_find('/') def test_17_move_existing_other_path_different_file(self): #Test 31 #move existing other path (different file) cmd_ef(CP+" -vvv file01nonempty.txt "+'copied7') cmd_ef(CP+" -vvv file01.txt "+'/le01/moved7') cmd_ef(MV+" -vvv copied7 "+'/le01/moved7') copybyfilepattern("localUPs/","file01nonempty.txt", "localUPs/le01/moved7") self.compare_find('/') def test_18_move_existing_other_path(self): #Test 32 #move existing other path cmd_ef(CP+" -vvv file01nonempty.txt "+'copied7') cmd_ef(CP+" -vvv file01nonempty.txt "+'/le01/moved7') cmd_ef(MV+" -vvv copied7 "+'/le01/moved7') copybyfilepattern("localUPs/","file01nonempty.txt", "localUPs/le01/moved7") self.compare_find('/') def test_19_thumbnails(self): #Test 33 #ensure thumnail generation #1st get images selection print(f"using images URL: {self.imagesUrl}") cmd_ef(GET+" "+self.imagesUrl+" localtmp/") #2nd, upload folder cmd_ef(PUT+" localtmp/images") #3rd, for each file, download thumbnail folder="/images" o,status,e=cmd_ec(FIND+" "+folder+"/*") fullout="" fullStatus=1 for f in o.split(): if b"." not in f: continue # Ignore folders rmfileifexisting("thumbnail.jpg") o,status,e=cmd_ec(THUMB+" "+f.decode()+" thumbnail.jpg") ext=f.decode().split(".")[-1].lower().strip() allowedFailure=["ai","ani","cur","eps","exe","gif","heic","html","idx","j2c","jpm","md","mj2","pdf","psd","sgi","svg","txt","webp","xmp", "pnm","ppm", "tiff", "tif", "x3f"] if not ext in allowedFailure and b"saved in" not in o: #note: output code is not trustworthy: check for "saved in" fullout=fullout+str("missing thumbnail for:"+str(f)+"\n") fullStatus=0 print (status, ext," missing thumbnail:",f,"\n",o, self.check_failed_and_clear(fullout,fullStatus)) @unittest.skipIf('SKIP_PDF_THUMBNAIL_TESTS' in os.environ, "only for systems where pdfium is enabled") def test_20_pdf_thumbnail(self): print(f"using pdfsURL: {self.pdfsURL}") cmd_ef(GET+" "+self.pdfsURL+" localtmp/") #2nd, upload folder cmd_ef(PUT+" localtmp/pdfs") #3rd, for each file, download thumbnail folder="/pdfs" o,status,e=cmd_ec(FIND+" "+folder+"/*") fullout="" fullStatus=1 print(f"output = {o}") split = o.split(b"\n") print(f"split output = {split}") for f in split: if not len(f): continue rmfileifexisting("thumbnail.jpg") o,status,e=cmd_ec(THUMB+" '"+f.decode()+"' thumbnail.jpg") allowedFailure=["very big sheet size", "protected", "non-pdf-file","with-password","_TFG"] if not True in [x.encode() in f for x in allowedFailure] and b"saved in" not in o: #note: output code is not trustworthy: check for "saved in" fullout=fullout+str("missing thumbnail for:"+str(f)+"\n") fullStatus=0 print(f'{status} missing thumbnail: {f}') print(o) self.check_failed_and_clear(fullout,fullStatus) @unittest.skipIf(platform.system() != 'Windows' or CMDSHELL, "The `-o` argument in `cat` is only available on Windows and in non-interactive mode") def test_21_ascii_cat_to_file(self): ascii_string_list = [ 'Hello world', 'The quick brown fox jumps over the lazy dog.', '0123456789', '!@#$%^&*()_+-=[]{}|;:\'",.<>?/', ] for i, file_contents in enumerate(ascii_string_list, start=1): file_name = f'unicode_{i}.txt' file_path = f'localUPs/{file_name}' with open(file_path, mode='w', encoding='ascii') as f: f.write(file_contents) cmd_ef(f'{PUT} {file_path} /') cat_contents = cmd_es(f'{CAT} /{file_name}') self.assertEqual(cat_contents.decode('ascii'), file_contents) cmd_ef(f'{CAT} /{file_name} -o localUPs/cat_output.txt') self.assertTrue(filecmp.cmp('localUPs/cat_output.txt', file_path, shallow=False)) @unittest.skipIf(platform.system() != 'Windows' or CMDSHELL, "The `-o` argument in `cat` is only available on Windows and in non-interactive mode") def test_22_unicode_cat_to_file(self): for i, file_contents in enumerate(UNICODE_NAME_LIST, start=1): file_name = f'unicode_{i}.txt' file_path = f'localUPs/{file_name}' with open(file_path, mode='w', encoding='utf-8') as f: f.write(file_contents) cmd_ef(f'{PUT} {file_path} /') cat_contents = cmd_es(f'{CAT} /{file_name}') self.assertEqual(cat_contents.decode('utf-8'), file_contents) cmd_ef(f'{CAT} /{file_name} -o localUPs/cat_output.txt') self.assertTrue(filecmp.cmp('localUPs/cat_output.txt', file_path, shallow=False)) if __name__ == '__main__': if "OUT_DIR_JUNIT_XML" in os.environ: unittest.main(testRunner=xmlrunner.XMLTestRunner(output=os.environ["OUT_DIR_JUNIT_XML"]), failfast=False, buffer=False, catchbreak=False) else: unittest.main() MEGAcmd-2.5.2_Linux/tests/megacmd_put_test.py000077500000000000000000000234011516543156300211500ustar00rootroot00000000000000#!/usr/bin/python3 # -*- coding: utf-8 -*- #better run in an empty folder import os, shutil, platform import unittest import xmlrunner from megacmd_tests_common import * def setUpModule(): global ABSPWD global ABSMEGADLFOLDER ABSPWD = os.getcwd() ABSMEGADLFOLDER = ABSPWD+'/megaDls' def clean_all(): if not clean_root_confirmed_by_user(): raise Exception("Tests need to be run with YES_I_KNOW_THIS_WILL_CLEAR_MY_MEGA_ACCOUNT=1") if cmd_es(WHOAMI) != osvar("MEGA_EMAIL"): cmd_ef(LOGOUT) cmd_ef(LOGIN+" " +osvar("MEGA_EMAIL")+" "+osvar("MEGA_PWD")) cmd_ec(RM+' -rf "/*"') cmd_ec(RM+' -rf "*"') cmd_ec(RM+' -rf "//bin/*"') rmfolderifexisting("localUPs") rmfolderifexisting("localtmp") rmfileifexisting("megafind.txt") rmfileifexisting("localfind.txt") def clear_local_and_remote(): rmfolderifexisting("localUPs") cmd_ec(RM+' -rf "/*"') initialize_contents() currentTest=1 def check_failed_and_clear(o,status): global currentTest if status == 0: print("test "+str(currentTest)+" failed!") print(o) exit(1) else: print("test "+str(currentTest)+" succesful!") clear_local_and_remote() currentTest+=1 cmd_ef(CD+" /") def initialize(): if cmd_es(WHOAMI) != osvar("MEGA_EMAIL"): cmd_ef(LOGOUT) cmd_ef(LOGIN+" " +osvar("MEGA_EMAIL")+" "+osvar("MEGA_PWD")) if len(os.listdir(".")): print("initialization folder not empty!", file=sys.stderr) #~ cd $ABSPWD exit(1) if cmd_es(FIND+" /") != b"/": print("REMOTE Not empty, please clear it before starting!", file=sys.stderr) #~ cd $ABSPWD exit(1) #initialize localtmp estructure: makedir("localtmp") touch("localtmp/file01.txt") out('file01contents', 'localtmp/file01nonempty.txt') #local empty folders structure for f in ['localtmp/le01/'+a for a in ['les01/less01']+ ['les02/less0'+z for z in ['1','2']] ]: makedir(f) #local filled folders structure for f in ['localtmp/lf01/'+a for a in ['lfs01/lfss01']+ ['lfs02/lfss0'+z for z in ['1','2']] ]: makedir(f) for f in ['localtmp/lf01/'+a for a in ['lfs01/lfss01']+ ['lfs02/lfss0'+z for z in ['1','2']] ]: touch(f+"/commonfile.txt") #spaced structure for f in ['localtmp/ls 01/'+a for a in ['ls s01/ls ss01']+ ['ls s02/ls ss0'+z for z in ['1','2']] ]: makedir(f) for f in ['localtmp/ls 01/'+a for a in ['ls s01/ls ss01']+ ['ls s02/ls ss0'+z for z in ['1','2']] ]: touch(f+"/common file.txt") # localtmp/ # ├── file01nonempty.txt # ├── file01.txt # ├── le01 # │   ├── les01 # │   │   └── less01 # │   └── les02 # │   ├── less01 # │   └── less02 # ├── lf01 # │   ├── lfs01 # │   │   └── lfss01 # │   │   └── commonfile.txt # │   └── lfs02 # │   ├── lfss01 # │   │   └── commonfile.txt # │   └── lfss02 # │   └── commonfile.txt # └── ls 01 # ├── ls s01 # │   └── ls ss01 # │   └── common file.txt # └── ls s02 # ├── ls ss01 # │   └── common file.txt # └── ls ss02 # └── common file.txt #initialize dynamic contents: clear_local_and_remote() def initialize_contents(): remotefolders=['01/'+a for a in ['s01/ss01']+ ['s02/ss0'+z for z in ['1','2']] ] cmd_ef(MKDIR+" -p "+" ".join(remotefolders)) for f in ['localUPs/01/'+a for a in ['s01/ss01']+ ['s02/ss0'+z for z in ['1','2']] ]: makedir(f) class MEGAcmdPutTests(unittest.TestCase): @classmethod def setUpClass(cls): clean_all() initialize() clear_local_and_remote() @classmethod def tearDownClass(cls): if not VERBOSE: clean_all() def setUp(self): print(f"\n=== Running test: {self._testMethodName} ===") def compare_and_clear(self) : def cleanup(): clear_local_and_remote() cmd_ef(CD+ " /") megafind=sort(cmd_ef(FIND)) localfind=sort(find('localUPs','.')) self.addCleanup(cleanup) self.assertEqual(megafind, localfind) def test_clean_comparison(self): #Test 01 #clean comparison self.compare_and_clear() def test_no_dest_empty_file_upload(self): #Test 02 #no destiny empty file upload cmd_ef(PUT+' '+'localtmp/file01.txt') shutil.copy2('localtmp/file01.txt','localUPs/') self.compare_and_clear() def test_dest_empty_file_upload(self): #Test 03 #/ destiny empty file upload cmd_ef(PUT+' '+'localtmp/file01.txt /') shutil.copy2('localtmp/file01.txt','localUPs/') self.compare_and_clear() def test_no_dest_non_empty_file_upload(self): #Test 04 #no destiny non empty file upload cmd_ef(PUT+' '+'localtmp/file01nonempty.txt') shutil.copy2('localtmp/file01nonempty.txt','localUPs/') self.compare_and_clear() def test_update_non_empty_file_upload(self): #Test 05 #update non empty file upload out('newfile01contents', 'localtmp/file01nonempty.txt') cmd_ef(PUT+' '+'localtmp/file01nonempty.txt') shutil.copy2('localtmp/file01nonempty.txt','localUPs/file01nonempty.txt') self.compare_and_clear() def test_empty_folder(self): #Test 06 #empty folder cmd_ef(PUT+' '+'localtmp/le01/les01/less01') copyfolder('localtmp/le01/les01/less01','localUPs/') self.compare_and_clear() def test_1_file_folder(self): #Test 07 #1 file folder cmd_ef(PUT+' '+'localtmp/lf01/lfs01/lfss01') copyfolder('localtmp/lf01/lfs01/lfss01','localUPs/') self.compare_and_clear() def test_entire_empty_folder_structure(self): #Test 08 #entire empty folders structure cmd_ef(PUT+' '+'localtmp/le01') copyfolder('localtmp/le01','localUPs/') self.compare_and_clear() def test_entire_non_empty_folder_structure(self): #Test 09 #entire non empty folders structure cmd_ef(PUT+' '+'localtmp/lf01') copyfolder('localtmp/lf01','localUPs/') self.compare_and_clear() def test_copy_structure_into_subfolder(self): #Test 10 #copy structure into subfolder cmd_ef(PUT+' '+'localtmp/le01 /01/s01') copyfolder('localtmp/le01','localUPs/01/s01') self.compare_and_clear() def test_copy_exact_structure(self): #~ #Test 11 #copy exact structure makedir('auxx') copyfolder('localUPs/01','auxx') cmd_ef(PUT+' '+'auxx/01/s01 /01/s01') copyfolder('auxx/01/s01','localUPs/01/s01') rmfolderifexisting("auxx") self.compare_and_clear() def test_merge_increased_structure(self): #~ #Test 12 #merge increased structure makedir('auxx') copyfolder('localUPs/01','auxx') touch('auxx/01/s01/another.txt') cmd_ef(PUT+' '+'auxx/01/s01 /01/') shutil.copy2('auxx/01/s01/another.txt','localUPs/01/s01') self.compare_and_clear() rmfolderifexisting("auxx") def test_multiple_upload(self): #Test 13 #multiple upload cmd_ef(PUT+' '+'localtmp/le01 localtmp/lf01 /01/s01') copyfolder('localtmp/le01','localUPs/01/s01') copyfolder('localtmp/lf01','localUPs/01/s01') self.compare_and_clear() @unittest.skipIf(platform.system() == "Windows" or CMDSHELL, "skipping test") def test_local_regexp(self): #Test 14 #local regexp cmd_ef(PUT+' '+'localtmp/*txt /01/s01') copybyfilepattern('localtmp/','*.txt','localUPs/01/s01') self.compare_and_clear() def test_prev_dir(self): """../""" #Test 15 #../ cmd_ef(CD+' 01') cmd_ef(PUT+' '+'localtmp/le01 ../01/s01') cmd_ef(CD+' /') copyfolder('localtmp/le01','localUPs/01/s01') self.compare_and_clear() def test_spaces(self): #Test 16 #spaced stuff if CMDSHELL: #TODO: think about this again cmd_ef(PUT+' '+'localtmp/ls\\ 01') else: cmd_ef(PUT+' '+'"localtmp/ls 01"') copyfolder('localtmp/ls 01','localUPs') self.compare_and_clear() def test_put_create_flag_absolute_dest_path(self): """ When cwd is in a subfolder (e.g. /put_test_xxx) and put -c uses an absolute destination path, the path must be resolved from root, not from cwd. """ # Test 17 #put -c with absolute path cmd_ef(MKDIR+' /put_abs_dest') cmd_ef(CD+' /put_abs_dest') cmd_ef(PUT+' -c '+'localtmp/file01.txt /put_abs_dest/newfile/') cmd_ef(CD+' /') makedir('localUPs/put_abs_dest/newfile') shutil.copy2('localtmp/file01.txt', 'localUPs/put_abs_dest/newfile/') self.compare_and_clear() if __name__ == '__main__': if "OUT_DIR_JUNIT_XML" in os.environ: unittest.main(testRunner=xmlrunner.XMLTestRunner(output=os.environ["OUT_DIR_JUNIT_XML"]), failfast=False, buffer=False, catchbreak=False) else: unittest.main() ###TODO: do stuff in shared folders... ######## ##Test XX #merge structure with file updated ##This test fails, because it creates a remote copy of the updated file. ##That's expected. If ever provided a way to create a real merge (e.g.: put -m ...) to do BACKUPs, reuse this test. #mkdir aux #shutil.copy2('-pr localUPs/01','aux') #touch aux/01/s01/another.txt #cmd_ef(PUT+' '+'aux/01/s01 /01/') #rsync -aLp aux/01/s01/ localUPs/01/s01/ #echo "newcontents" > aux/01/s01/another.txt #cmd_ef(PUT+' '+'aux/01/s01 /01/') #rsync -aLp aux/01/s01/ localUPs/01/s01/ #rm -r aux #compare_and_clear() # Clean all MEGAcmd-2.5.2_Linux/tests/megacmd_put_test.sh000066400000000000000000000147511516543156300211370ustar00rootroot00000000000000#!/bin/bash GET=mega-get PUT=mega-put RM=mega-rm CD=mega-cd LCD=mega-lcd MKDIR=mega-mkdir EXPORT=mega-export FIND=mega-find if [ "x$VERBOSE" == "x" ]; then VERBOSE=0 fi if [ "x$MEGACMDSHELL" != "x" ]; then PUT="executeinMEGASHELL put" CMDSHELL=1 fi function executeinMEGASHELL() { command=$1 shift; echo lcd "$PWD" > /tmp/shellin echo $command "$@" >> /tmp/shellin #~ echo $command "$@" > /tmp/shellin $MEGACMDSHELL < /tmp/shellin | sed "s#^.*\[K##g" | grep $MEGA_EMAIL -A 1000 | grep -v $MEGA_EMAIL } ABSPWD=`pwd` function clean_all() { if [[ $(mega-whoami) != *"$MEGA_EMAIL" ]]; then mega-logout || : mega-login $MEGA_EMAIL $MEGA_PWD || exit -1 fi $RM -rf "*" > /dev/null $RM -rf "//bin/*" > /dev/null if [ -e localUPs ]; then rm -r localUPs; fi if [ -e localtmp ]; then rm -r localtmp; fi if [ -e megafind.txt ]; then rm megafind.txt; fi if [ -e localfind.txt ]; then rm localfind.txt; fi } function clear_local_and_remote() { rm -r localUPs/* 2>/dev/null $RM -rf /01/../* >/dev/null 2>/dev/null || : initialize_contents } currentTest=1 function compare_and_clear() { if [ "$VERBOSE" == "1" ]; then echo "test $currentTest" fi $FIND | sort > megafind.txt (cd localUPs; find . | sed "s#\./##g" | sort) > localfind.txt if diff --side-by-side megafind.txt localfind.txt 2>/dev/null >/dev/null; then if [ "$VERBOSE" == "1" ]; then echo "diff megafind vs localfind:" diff --side-by-side megafind.txt localfind.txt fi echo "test $currentTest succesful!" else echo "test $currentTest failed!" echo "diff megafind vs localfind:" diff --side-by-side megafind.txt localfind.txt cd $ABSPWD exit 1 fi clear_local_and_remote currentTest=$((currentTest+1)) $CD / } function check_failed_and_clear() { if [ "$?" != "0" ]; then echo "test $currentTest succesful!" else echo "test $currentTest failed!" exit 1 fi clear_local_and_remote currentTest=$((currentTest+1)) $CD / } function initialize () { if [[ $(mega-whoami) != *"$MEGA_EMAIL" ]]; then mega-logout || : mega-login $MEGA_EMAIL $MEGA_PWD || exit -1 fi if [ "$(ls -A .)" ]; then echo "initialization folder not empty!" exit 1 fi if [ $($FIND / | wc -l) != 1 ]; then echo "REMOTE Not empty, please clear it before starting!" exit 1 fi #initialize localtmp estructure: mkdir -p localtmp touch localtmp/file01.txt echo file01contents > localtmp/file01nonempty.txt #local empty folders structure mkdir -p localtmp/le01/{les01/less01,les02/less0{1,2}} #local filled folders structure mkdir -p localtmp/lf01/{lfs01/lfss01,lfs02/lfss0{1,2}} touch localtmp/lf01/{lfs01/lfss01,lfs02/lfss0{1,2}}/commonfile.txt #spaced structure mkdir -p localtmp/ls\ 01/{ls\ s01/ls\ ss01,ls\ s02/ls\ ss0{1,2}} touch localtmp/ls\ 01/{ls\ s01/ls\ ss01,ls\ s02/ls\ ss0{1,2}}/common\ file.txt # localtmp/ # ├── file01nonempty.txt # ├── file01.txt # ├── le01 # │   ├── les01 # │   │   └── less01 # │   └── les02 # │   ├── less01 # │   └── less02 # ├── lf01 # │   ├── lfs01 # │   │   └── lfss01 # │   │   └── commonfile.txt # │   └── lfs02 # │   ├── lfss01 # │   │   └── commonfile.txt # │   └── lfss02 # │   └── commonfile.txt # └── ls 01 # ├── ls s01 # │   └── ls ss01 # │   └── common file.txt # └── ls s02 # ├── ls ss01 # │   └── common file.txt # └── ls ss02 # └── common file.txt #initialize dynamic contents: clear_local_and_remote } function initialize_contents() { $MKDIR -p 01/{s01/ss01,s02/ss0{1,2}} mkdir -p localUPs/01/{s01/ss01,s02/ss0{1,2}} } if [ "$MEGA_EMAIL" == "" ] || [ "$MEGA_PWD" == "" ]; then echo "You must define variables MEGA_EMAIL MEGA_PWD. WARNING: Use an empty account for $MEGA_EMAIL" cd $ABSPWD exit 1 fi #INITIALIZATION clean_all initialize ABSMEGADLFOLDER=$PWD/megaDls clear_local_and_remote #Test 01 #clean comparison compare_and_clear #Test 02 #no destiny empty file upload $PUT localtmp/file01.txt cp localtmp/file01.txt localUPs/ compare_and_clear #Test 03 #/ destiny empty file upload $PUT localtmp/file01.txt / cp localtmp/file01.txt localUPs/ compare_and_clear #Test 04 #no destiny nont empty file upload $PUT localtmp/file01nonempty.txt cp localtmp/file01nonempty.txt localUPs/ compare_and_clear #Test 05 #empty folder $PUT localtmp/le01/les01/less01 cp -r localtmp/le01/les01/less01 localUPs/ compare_and_clear #Test 06 #1 file folder $PUT localtmp/lf01/lfs01/lfss01 cp -r localtmp/lf01/lfs01/lfss01 localUPs/ compare_and_clear #Test 07 #entire empty folders structure $PUT localtmp/le01 cp -r localtmp/le01 localUPs/ compare_and_clear #Test 08 #entire non empty folders structure $PUT localtmp/lf01 cp -r localtmp/lf01 localUPs/ compare_and_clear #Test 09 #copy structure into subfolder $PUT localtmp/le01 /01/s01 cp -r localtmp/le01 localUPs/01/s01 compare_and_clear #~ #Test 10 #copy exact structure mkdir aux cp -pr localUPs/01 aux $PUT aux/01/s01 /01/s01 cp -r aux/01/s01 localUPs/01/s01 rm -r aux compare_and_clear #Test 11 #merge increased structure mkdir aux cp -pr localUPs/01 aux touch aux/01/s01/another.txt $PUT aux/01/s01 /01/ rsync -aLp aux/01/s01/ localUPs/01/s01/ rm -r aux compare_and_clear #Test 12 #multiple upload $PUT localtmp/le01 localtmp/lf01 /01/s01 cp -r localtmp/le01 localtmp/lf01 localUPs/01/s01 compare_and_clear #Test 13 #local regexp $PUT localtmp/*txt /01/s01 cp -r localtmp/*txt localUPs/01/s01 compare_and_clear #Test 14 #../ $CD 01 $PUT localtmp/le01 ../01/s01 $CD / cp -r localtmp/le01 localUPs/01/s01 compare_and_clear currentTest=15 #Test 15 #spaced stuff if [ "x$CMDSHELL" != "x1" ]; then #TODO: think about this again $PUT localtmp/ls\ 01 else $PUT "localtmp/ls\ 01" fi cp -r localtmp/ls\ 01 localUPs/ls\ 01 compare_and_clear ###TODO: do stuff in shared folders... ######## ##Test XX #merge structure with file updated ##This test fails, because it creates a remote copy of the updated file. ##That's expected. If ever provided a way to create a real merge (e.g.: put -m ...) to do BACKUPs, reuse this test. #mkdir aux #cp -pr localUPs/01 aux #touch aux/01/s01/another.txt #$PUT aux/01/s01 /01/ #rsync -aLp aux/01/s01/ localUPs/01/s01/ #echo "newcontents" > aux/01/s01/another.txt #$PUT aux/01/s01 /01/ #rsync -aLp aux/01/s01/ localUPs/01/s01/ #rm -r aux #compare_and_clear # Clean all if [ "$VERBOSE" != "1" ]; then clean_all fi MEGAcmd-2.5.2_Linux/tests/megacmd_rm_test.py000077500000000000000000000207171516543156300207650ustar00rootroot00000000000000#!/usr/bin/python3 # -*- coding: utf-8 -*- #better run in an empty folder import os, shutil, platform import unittest import xmlrunner from megacmd_tests_common import * def setUpModule(): global ABSPWD global ABSMEGADLFOLDER ABSPWD = os.getcwd() ABSMEGADLFOLDER = ABSPWD+'/megaDls' def clean_all(): if not clean_root_confirmed_by_user(): raise Exception("Tests need to be run with YES_I_KNOW_THIS_WILL_CLEAR_MY_MEGA_ACCOUNT=1") if cmd_es(WHOAMI) != osvar("MEGA_EMAIL"): cmd_ef(LOGOUT) cmd_ef(LOGIN+" " +osvar("MEGA_EMAIL")+" "+osvar("MEGA_PWD")) cmd_ec(RM+' -rf "/*"') cmd_ec(RM+' -rf "*"') cmd_ec(RM+' -rf "//bin/*"') rmfolderifexisting("localUPs") rmfolderifexisting("localtmp") rmfileifexisting("megafind.txt") rmfileifexisting("localfind.txt") def clear_local_and_remote(): rmfolderifexisting("localUPs") cmd_ec(RM+' -rf "/*"') initialize_contents() currentTest=1 def check_failed_and_clear(o,status): global currentTest if status == 0: print("test "+str(currentTest)+" failed!") print(o) exit(1) else: print("test "+str(currentTest)+" succesful!") clear_local_and_remote() currentTest+=1 cmd_ef(CD+" /") def initialize(): if cmd_es(WHOAMI) != osvar("MEGA_EMAIL"): cmd_ef(LOGOUT) cmd_ef(LOGIN+" " +osvar("MEGA_EMAIL")+" "+osvar("MEGA_PWD")) if len(os.listdir(".")): print("initialization folder not empty!", file=sys.stderr) #~ cd $ABSPWD exit(1) if cmd_es(FIND+" /") != b"/": print("REMOTE Not empty, please clear it before starting!", file=sys.stderr) #~ cd $ABSPWD exit(1) #initialize localtmp estructure: makedir("localtmp") touch("localtmp/file01.txt") out('file01contents', 'localtmp/file01nonempty.txt') #local empty folders structure for f in ['localtmp/le01/'+a for a in ['les01/less01']+ ['les02/less0'+z for z in ['1','2']] ]: makedir(f) #local filled folders structure for f in ['localtmp/lf01/'+a for a in ['lfs01/lfss01']+ ['lfs02/lfss0'+z for z in ['1','2']] ]: makedir(f) for f in ['localtmp/lf01/'+a for a in ['lfs01/lfss01']+ ['lfs02/lfss0'+z for z in ['1','2']] ]: touch(f+"/commonfile.txt") #spaced structure for f in ['localtmp/ls 01/'+a for a in ['ls s01/ls ss01']+ ['ls s02/ls ss0'+z for z in ['1','2']] ]: makedir(f) for f in ['localtmp/ls 01/'+a for a in ['ls s01/ls ss01']+ ['ls s02/ls ss0'+z for z in ['1','2']] ]: touch(f+"/common file.txt") # localtmp/ # ├── file01nonempty.txt # ├── file01.txt # ├── le01 # │   ├── les01 # │   │   └── less01 # │   └── les02 # │   ├── less01 # │   └── less02 # ├── lf01 # │   ├── lfs01 # │   │   └── lfss01 # │   │   └── commonfile.txt # │   └── lfs02 # │   ├── lfss01 # │   │   └── commonfile.txt # │   └── lfss02 # │   └── commonfile.txt # └── ls 01 # ├── ls s01 # │   └── ls ss01 # │   └── common file.txt # └── ls s02 # ├── ls ss01 # │   └── common file.txt # └── ls ss02 # └── common file.txt #initialize dynamic contents: clear_local_and_remote() def initialize_contents(): contents=" ".join(['"localtmp/'+x+'"' for x in os.listdir('localtmp/')]) cmd_ef(PUT+" "+contents+" /") makedir('localUPs') copybypattern('localtmp/','*','localUPs') class MEGAcmdRmTest(unittest.TestCase): @classmethod def setUpClass(cls): #INITIALIZATION clean_all() initialize() clear_local_and_remote() @classmethod def tearDownClass(cls): if not VERBOSE: clean_all() def setUp(self): print(f"\n=== Running test: {self._testMethodName} ===") def compare_and_clear(self): def cleanup(): clear_local_and_remote() cmd_ef(CD+" /") megafind=sort(cmd_ef(FIND)) localfind=sort(find('localUPs','.')) self.addCleanup(cleanup) self.assertEqual(megafind, localfind) print(f"megafind: {megafind}, localfind: {localfind}") def test_01_clean_comparison(self): #Test 01 #clean comparison self.compare_and_clear() def test_02_dest_empty_file(self): #Test 02 #destiny empty file cmd_ef(RM+' '+'file01.txt') rmfileifexisting('localUPs/file01.txt') self.compare_and_clear() def test_03_dest_empty_file_upload(self): #Test 03 #/ destiny empty file upload cmd_ef(PUT+' '+'localtmp/file01.txt /') shutil.copy2('localtmp/file01.txt','localUPs') self.compare_and_clear() def test_04_no_dest_non_empty_file_upload(self): #Test 04 #no destiny nont empty file upload cmd_ef(RM+' '+'file01nonempty.txt') rmfileifexisting('localUPs/file01nonempty.txt') self.compare_and_clear() def test_05_empty_folder(self): #Test 05 #empty folder cmd_ef(RM+' '+'-rf le01/les01/less01') rmfolderifexisting('localUPs/le01/les01/less01') self.compare_and_clear() def test_06_1_file_folder(self): #Test 06 #1 file folder cmd_ef(RM+' '+'-rf lf01/lfs01/lfss01') rmfolderifexisting('localUPs/lf01/lfs01/lfss01') self.compare_and_clear() def test_07_entire_empty_folders_structure(self): #Test 07 #entire empty folders structure cmd_ef(RM+' '+'-rf le01') rmfolderifexisting('localUPs/le01') self.compare_and_clear() def test_08_entire_non_empty_folders_structure(self): #Test 08 #entire non empty folders structure cmd_ef(RM+' '+'-rf lf01') rmfolderifexisting('localUPs/lf01') self.compare_and_clear() def test_09_multiple(self): #Test 09 #multiple cmd_ef(RM+' '+'-rf lf01 le01/les01') rmfolderifexisting('localUPs/lf01') rmfolderifexisting('localUPs/le01/les01') self.compare_and_clear() def test_10_current_wd(self): #Test 10 #. cmd_ef(CD+' '+'le01') cmd_ef(RM+' '+'-rf .') cmd_ef(CD+' '+'/') rmfolderifexisting('localUPs/le01') self.compare_and_clear() def test_11_prev_dir(self): #Test 11 #.. cmd_ef(CD+' '+'le01/les01') cmd_ef(RM+' '+'-rf ..') cmd_ef(CD+' '+'/') rmfolderifexisting('localUPs/le01') self.compare_and_clear() def test_12_prev_dir_2(self): """../XX""" #Test 12 #../XX cmd_ef(CD+' '+'le01/les01') cmd_ef(RM+' '+'-rf ../les01') cmd_ef(CD+' '+'/') rmfolderifexisting('localUPs/le01/les01') self.compare_and_clear() def test_13_spaces(self): #Test 13 #spaced stuff cmd_ef(RM+' '+'-rf "ls 01"') rmfolderifexisting('localUPs/ls 01') self.compare_and_clear() def test_14_complex(self): #Test 14 #complex stuff cmd_ef(RM+' '+'-rf "ls 01/../le01/les01" "lf01/../ls*/ls s02"') rmfolderifexisting('localUPs/ls 01/../le01/les01') [rmfolderifexisting('localUPs/lf01/../'+f+'/ls s02') for f in os.listdir('localUPs/lf01/..') if f.startswith('ls')] self.compare_and_clear() @unittest.skipIf(platform.system() == "Windows", "skipping for Windows systems") def test_15_complex_pcre_expr(self): #Test 15 #complex stuff with PCRE exp cmd_ef(RM+' '+'-rf --use-pcre "ls 01/../le01/les0[12]" "lf01/../ls.*/ls s0[12]"') rmfolderifexisting('localUPs/ls 01/../le01/les01') rmfolderifexisting('localUPs/ls 01/../le01/les02') [rmfolderifexisting('localUPs/lf01/../'+f+'/ls s01') for f in os.listdir('localUPs/lf01/..') if f.startswith('ls')] [rmfolderifexisting('localUPs/lf01/../'+f+'/ls s02') for f in os.listdir('localUPs/lf01/..') if f.startswith('ls')] self.compare_and_clear() def test_16_spaces_2(self): #Test 16 #spaced stuff2 cmd_ef(RM+' '+'-rf ls\\ 01') rmfolderifexisting('localUPs/ls 01') self.compare_and_clear() if __name__ == '__main__': if "OUT_DIR_JUNIT_XML" in os.environ: unittest.main(testRunner=xmlrunner.XMLTestRunner(output=os.environ["OUT_DIR_JUNIT_XML"]), failfast=False, buffer=False, catchbreak=False) else: unittest.main() MEGAcmd-2.5.2_Linux/tests/megacmd_rm_test.sh000066400000000000000000000132331516543156300207370ustar00rootroot00000000000000#!/bin/bash GET=mega-get PUT=mega-put RM=mega-rm CD=mega-cd LCD=mega-lcd MKDIR=mega-mkdir EXPORT=mega-export FIND=mega-find if [ "x$VERBOSE" == "x" ]; then VERBOSE=0 fi if [ "x$MEGACMDSHELL" != "x" ]; then RM="executeinMEGASHELL rm" CMDSHELL=1 fi function executeinMEGASHELL() { command=$1 shift; echo $command "$@" > /tmp/shellin $MEGACMDSHELL < /tmp/shellin | sed "s#^.*\[K##g" | grep $MEGA_EMAIL -A 1000 | grep -v $MEGA_EMAIL } ABSPWD=`pwd` function clean_all() { if [[ $(mega-whoami) != *"$MEGA_EMAIL" ]]; then mega-logout || : mega-login $MEGA_EMAIL $MEGA_PWD || exit -1 fi $RM -rf "/*" > /dev/null $RM -rf "//bin/*" > /dev/null if [ -e localUPs ]; then rm -r localUPs; fi if [ -e localtmp ]; then rm -r localtmp; fi if [ -e megafind.txt ]; then rm megafind.txt; fi if [ -e localfind.txt ]; then rm localfind.txt; fi } function clear_local_and_remote() { rm -r localUPs/* 2>/dev/null $RM -rf "/*" 2>/dev/null || : initialize_contents } currentTest=1 function compare_and_clear() { if [ "$VERBOSE" == "1" ]; then echo "test $currentTest" fi $FIND | sort > megafind.txt (cd localUPs; find . | sed "s#\./##g" | sort) > localfind.txt if diff --side-by-side megafind.txt localfind.txt 2>/dev/null >/dev/null; then if [ "$VERBOSE" == "1" ]; then echo "diff megafind vs localfind:" diff --side-by-side megafind.txt localfind.txt fi echo "test $currentTest succesful!" else echo "test $currentTest failed!" echo "diff megafind vs localfind:" diff --side-by-side megafind.txt localfind.txt cd $ABSPWD exit 1 fi clear_local_and_remote currentTest=$((currentTest+1)) $CD / } function check_failed_and_clear() { if [ "$?" != "0" ]; then echo "test $currentTest succesful!" else echo "test $currentTest failed!" cd $ABSPWD exit 1 fi clear_local_and_remote currentTest=$((currentTest+1)) $CD / } function initialize () { if [[ $(mega-whoami) != *"$MEGA_EMAIL" ]]; then mega-logout || : mega-login $MEGA_EMAIL $MEGA_PWD || exit -1 fi if [ "$(ls -A .)" ]; then echo "initialization folder not empty!" cd $ABSPWD exit 1 fi if [ $($FIND / | wc -l) != 1 ]; then echo "REMOTE Not empty, please clear it before starting!" cd $ABSPWD exit 1 fi #initialize localtmp estructure: mkdir -p localtmp touch localtmp/file01.txt echo file01contents > localtmp/file01nonempty.txt #local empty folders structure mkdir -p localtmp/le01/{les01/less01,les02/less0{1,2}} #local filled folders structure mkdir -p localtmp/lf01/{lfs01/lfss01,lfs02/lfss0{1,2}} touch localtmp/lf01/{lfs01/lfss01,lfs02/lfss0{1,2}}/commonfile.txt #spaced structure mkdir -p localtmp/ls\ 01/{ls\ s01/ls\ ss01,ls\ s02/ls\ ss0{1,2}} touch localtmp/ls\ 01/{ls\ s01/ls\ ss01,ls\ s02/ls\ ss0{1,2}}/common\ file.txt # localtmp/ # ├── file01nonempty.txt # ├── file01.txt # ├── le01 # │   ├── les01 # │   │   └── less01 # │   └── les02 # │   ├── less01 # │   └── less02 # ├── lf01 # │   ├── lfs01 # │   │   └── lfss01 # │   │   └── commonfile.txt # │   └── lfs02 # │   ├── lfss01 # │   │   └── commonfile.txt # │   └── lfss02 # │   └── commonfile.txt # └── ls 01 # ├── ls s01 # │   └── ls ss01 # │   └── common file.txt # └── ls s02 # ├── ls ss01 # │   └── common file.txt # └── ls ss02 # └── common file.txt #initialize dynamic contents: clear_local_and_remote } function initialize_contents() { $PUT localtmp/* / mkdir -p localUPs cp -r localtmp/* localUPs/ } if [ "$MEGA_EMAIL" == "" ] || [ "$MEGA_PWD" == "" ]; then echo "You must define variables MEGA_EMAIL MEGA_PWD. WARNING: Use an empty account for $MEGA_EMAIL" cd $ABSPWD exit 1 fi #INITIALIZATION clean_all initialize ABSMEGADLFOLDER=$PWD/megaDls clear_local_and_remote #Test 01 #clean comparison compare_and_clear #Test 02 #destiny empty file $RM file01.txt rm localUPs/file01.txt compare_and_clear #Test 03 #/ destiny empty file upload $PUT localtmp/file01.txt / cp localtmp/file01.txt localUPs/ compare_and_clear #Test 04 #no destiny nont empty file upload $RM file01nonempty.txt rm localUPs/file01nonempty.txt compare_and_clear #Test 05 #empty folder $RM -rf le01/les01/less01 rm -r localUPs/le01/les01/less01 compare_and_clear #Test 06 #1 file folder $RM -rf lf01/lfs01/lfss01 rm -r localUPs/lf01/lfs01/lfss01 compare_and_clear #Test 07 #entire empty folders structure $RM -rf le01 rm -r localUPs/le01 compare_and_clear #Test 08 #entire non empty folders structure $RM -rf lf01 rm -r localUPs/lf01 compare_and_clear #Test 09 #multiple $RM -rf lf01 le01/les01 rm -r localUPs/{lf01,le01/les01} compare_and_clear #Test 10 #. $CD le01 $RM -rf . $CD / rm -r localUPs/le01 compare_and_clear #Test 11 #.. $CD le01/les01 $RM -rf .. $CD / rm -r localUPs/le01 compare_and_clear #Test 12 #../XX $CD le01/les01 $RM -rf ../les01 $CD / rm -r localUPs/le01/les01 compare_and_clear #Test 13 #spaced stuff $RM -rf "ls\ 01" rm -r localUPs/ls\ 01 compare_and_clear #Test 14 #complex stuff $RM -rf "ls\ 01/../le01/les01" "lf01/../ls*/ls\ s02" rm -r localUPs/{ls\ 01/../le01/les01,lf01/../ls*/ls\ s02} compare_and_clear #Test 15 #complex stuff with PCRE exp $RM -rf --use-pcre "ls\ 01/../le01/les0[12]" "lf01/../ls.*/ls\ s0[12]" rm -r localUPs/{ls\ 01/../le01/les0[12],lf01/../ls*/ls\ s0[12]} compare_and_clear ###TODO: do stuff in shared folders... ######## #~ #Test XX #regexp #yet unsupported #~ $RM -rf "le01/les*" #~ rm -r localUPs/le01/les* #~ compare_and_clear # Clean all if [ "$VERBOSE" != "1" ]; then clean_all fi MEGAcmd-2.5.2_Linux/tests/megacmd_serving_test.py000077500000000000000000000262421516543156300220230ustar00rootroot00000000000000#!/usr/bin/python3 # -*- coding: utf-8 -*- import sys, os, shutil import ftplib import unittest import xmlrunner from megacmd_tests_common import * def initialize_contents(): contents=" ".join(['"localtmp/'+x+'"' for x in os.listdir('localtmp/')]) cmd_ef(PUT+" "+contents+" /") shutil.copytree('localtmp', 'localUPs') def clean_all(): if cmd_es(WHOAMI) != osvar("MEGA_EMAIL"): cmd_ef(LOGOUT) cmd_ef(LOGIN+" " +osvar("MEGA_EMAIL")+" "+osvar("MEGA_PWD")) cmd_ec(RM+' -rf "*"') cmd_ec(RM+' -rf "//bin/*"') rmfolderifexisting("localUPs") rmfolderifexisting("localtmp") rmfolderifexisting("megaDls") rmfolderifexisting("localDls") rmfileifexisting("megafind.txt") rmfileifexisting("localfind.txt") def clear_local_and_remote(): rmfolderifexisting("localUPs") cmd_ec(RM+' -rf "/*"') clear_dls() initialize_contents() def clear_dls(): rmcontentsifexisting("megaDls") rmcontentsifexisting("localDls") def check_failed_and_clear(self, o,status): self.assertNotEqual(status, 0) if status == 0: print(o) clear_dls() cmd_ef(CD+" /") opts='";X()[]{}<>|`\'' if os.name == 'nt': opts=";()[]{}`'" def initialize(): if cmd_es(WHOAMI) != osvar("MEGA_EMAIL"): cmd_es(LOGOUT) cmd_ef(LOGIN+" " +osvar("MEGA_EMAIL")+" "+osvar("MEGA_PWD")) if len(os.listdir(".")): print("initialization folder not empty!", file=sys.stderr) #~ cd $ABSPWD exit(1) if cmd_es(FIND+" /") != b"/": print("REMOTE Not empty, please clear it before starting!", file=sys.stderr) #~ cd $ABSPWD exit(1) #initialize localtmp estructure: makedir("localtmp") touch("localtmp/file01.txt") out('file01contents', 'localtmp/file01nonempty.txt') #local empty folders structure for f in ['localtmp/le01/'+a for a in ['les01/less01']+ ['les02/less0'+z for z in ['1','2']] ]: makedir(f) #local filled folders structure for f in ['localtmp/lf01/'+a for a in ['lfs01/lfss01']+ ['lfs02/lfss0'+z for z in ['1','2']] ]: makedir(f) for f in ['localtmp/lf01/'+a for a in ['lfs01/lfss01']+ ['lfs02/lfss0'+z for z in ['1','2']] ]: touch(f+"/commonfile.txt") #spaced structure for f in ['localtmp/ls 01/'+a for a in ['ls s01/ls ss01']+ ['ls s02/ls ss0'+z for z in ['1','2']] ]: makedir(f) for f in ['localtmp/ls 01/'+a for a in ['ls s01/ls ss01']+ ['ls s02/ls ss0'+z for z in ['1','2']] ]: touch(f+"/common file.txt") #weird chars makedir("localtmp/odd") for f in ['localtmp/odd/'+a for a in ['file'+z for z in opts+"X"]]: touch(f+'01.txt') #~ localtmp/ #~ ├── file01nonempty.txt #~ ├── file01.txt #~ ├── le01 #~ │   ├── les01 #~ │   │   └── less01 #~ │   └── les02 #~ │   ├── less01 #~ │   └── less02 #~ ├── lf01 #~ │   ├── lfs01 #~ │   │   └── lfss01 #~ │   │   └── commonfile.txt #~ │   └── lfs02 #~ │   ├── lfss01 #~ │   │   └── commonfile.txt #~ │   └── lfss02 #~ │   └── commonfile.txt #~ ├── ls 01 #~ │   ├── ls s01 #~ │   │   └── ls ss01 #~ │   │   └── common file.txt #~ │   └── ls s02 #~ │   ├── ls ss01 #~ │   │   └── common file.txt #~ │   └── ls ss02 #~ │   └── common file.txt #~ └── odd #~ ├── file;.txt #~ ├── .... #initialize dynamic contents: clear_local_and_remote() makedir('megaDls') makedir('localDls') #ABSMEGADLFOLDER=ABSPWD+'/megaDls' class MEGAcmdServingTest(unittest.TestCase): @classmethod def setUpClass(cls): if VERBOSE: print("STARTING...") clean_all() initialize() # Initialize FTP connection cmd_ec(FTP+' -d --all') url=cmd_ef(FTP+' /').split(b' ')[-1] server=url.split(b"//")[1].split(b":")[0] port=url.split(b"//")[1].split(b":")[1].split(b"/")[0] cls.subpath=b"/"+b"/".join(url.split(b"//")[1].split(b":")[1].split(b"/")[1:]) cls.subpath=cls.subpath.replace(b'%20',b' ') if VERBOSE: print(" connecting ... to "+server.decode()+" port="+port.decode()+" path="+cls.subpath.decode()) cls.ftp=ftplib.FTP() cls.ftp.connect(server.decode(),int(port.decode()), timeout=30) cls.ftp.login("anonymous", "nomatter") cls.ftp.cwd(cls.subpath.decode()) @classmethod def tearDownClass(cls): cls.ftp.close() if not VERBOSE: clean_all() def setUp(self): print(f"\n=== Running test: {self._testMethodName} ===") def compare_find(self, what, localFindPrefix='localUPs'): if not isinstance(what, list): what = [what] megafind=b"" localfind="" for w in what: megafind+=cmd_ef(FIND+" "+w)+b"\n" localfind+=find(localFindPrefix+'/'+w,w)+"\n" megafind=sort(megafind).strip() localfind=sort(localfind).strip() #~ megafind=$FIND "$@" | sort > $ABSPWD/megafind.txt #~ (cd localUPs 2>/dev/null; find "$@" | sed "s#\./##g" | sort) > $ABSPWD/localfind.txt self.assertEqual(megafind, localfind) if (megafind == localfind): if VERBOSE: print("diff megafind vs localfind:") #diff --side-by-side megafind.txt localfind.txt#TODO: do this print("MEGAFIND:") print(megafind) print("LOCALFIND") print(localfind) else: print("diff megafind vs localfind:") #~ diff --side-by-side megafind.txt localfind.txt #TODO: do this print("MEGAFIND:") print(megafind) print("LOCALFIND") print(localfind) #cd $ABSPWD #TODO: consider this def compare_remote_local(self, megafind, localfind): if (isinstance(megafind, bytes)): megafind = megafind.decode() if (isinstance(localfind, bytes)): localfind = localfind.decode() self.assertEqual(megafind, localfind) if (megafind == localfind): if VERBOSE: print("diff megafind vs localfind:") #diff --side-by-side megafind.txt localfind.txt#TODO: do this print("MEGAFIND:") print(megafind) print("LOCALFIND") print(localfind) else: print("diff megafind vs localfind:") #~ diff --side-by-side megafind.txt localfind.txt #TODO: do this print("MEGAFIND:") print(megafind) print("LOCALFIND") print(localfind) def compare_and_clear(self): megaDls=sort(find('megaDls')) localDls=sort(find('localDls')) self.assertEqual(megaDls, localDls) if (megaDls == localDls): if VERBOSE: print("megaDls:") print(megaDls) print() print("localDls:") print(localDls) print() else: print("megaDls:") print(megaDls) print() print("localDls:") print(localDls) print() exit(1) clear_dls() cmd_ef(CD+" /") def getFile(self, filename, destiny): try: self.ftp.retrbinary("RETR " + filename ,open(destiny, 'wb').write) except Exception as ex: print("Error: "+str(ex)) exit(1) def upload(self, source, destination): ext = os.path.splitext(source)[1] if ext in (".txt", ".htm", ".html"): self.ftp.storlines("STOR " + destination, open(source, "rb", 1024)) else: self.ftp.storbinary("STOR " + destination, open(source, "rb"), 1024) def lsftp(self): data=[] self.ftp.dir(data.append) toret="" for l in data: toret+=l[49:]+"\n" return toret def test_01_compare_root(self): self.compare_remote_local(self.ftp.pwd(), self.subpath) def test_02_mkdir(self): self.ftp.mkd('newfolder') makedir('localUPs/newfolder') self.compare_remote_local(sort(self.lsftp().strip()),sort(ls('localUPs').strip())) def test_03_download_txt_file(self): self.getFile('file01nonempty.txt', 'megaDls/file01nonempty.txt') shutil.copy2('localUPs/file01nonempty.txt','localDls/') self.compare_and_clear() def test_04_upload_txt_file(self): self.upload('localUPs/file01nonempty.txt', 'newfile.txt') shutil.copy2('localUPs/file01nonempty.txt','localUPs/newfile.txt') self.compare_remote_local(sort(self.lsftp().strip()),sort(ls('localUPs').strip())) def test_05_upload_non_txt_file(self): shutil.copy2('localUPs/file01nonempty.txt','localUPs/newfile') self.upload('localUPs/newfile', 'newfile') self.compare_remote_local(sort(self.lsftp().strip()),sort(ls('localUPs').strip())) def test_06_rename(self): shutil.move('localUPs/lf01/lfs01/lfss01/commonfile.txt','localUPs/lf01/lfs01/lfss01/renamed.txt') self.ftp.rename('lf01/lfs01/lfss01/commonfile.txt', 'lf01/lfs01/lfss01/renamed.txt') self.compare_remote_local(sort(self.lsftp().strip()),sort(ls('localUPs').strip())) def test_07_rename_folder(self): shutil.move('localUPs/lf01/lfs01/lfss01','localUPs/lf01/lfs01/lfss01_renamed') self.ftp.rename('lf01/lfs01/lfss01', 'lf01/lfs01/lfss01_renamed') self.compare_remote_local(sort(self.lsftp().strip()),sort(ls('localUPs').strip())) def test_08_remove_file(self): os.remove('localUPs/lf01/lfs02/lfss01/commonfile.txt') self.ftp.delete('lf01/lfs02/lfss01/commonfile.txt') self.compare_remote_local(sort(self.lsftp().strip()),sort(ls('localUPs').strip())) def test_09_remove_folder(self): shutil.rmtree('localUPs/lf01/lfs02/lfss02') self.ftp.rmd('lf01/lfs02/lfss02') self.compare_remote_local(sort(self.lsftp().strip()),sort(ls('localUPs').strip())) #FTPS #~ if ftp != None: ftp.close() #~ cmd_ec(FTP+' -d --all') #~ url=cmd_ef(FTP+' / --tls --certificate=/assets/others/certs/selfsignedSDK/pepitopalotes.pem --key=/assets/others/certs/selfsignedSDK/pepitopalotes.key --port=21 ').split(' ')[-1] #~ server=url.split("//")[1].split(":")[0] #~ #server="pepitopalotes" #~ port=url.split("//")[1].split(":")[1].split("/")[0] #~ subpath="/"+"/".join(url.split("//")[1].split(":")[1].split("/")[1:]) #~ subpath=subpath.replace('%20',' ') #~ if VERBOSE: #~ print " connecting ... to "+server+" port="+port+" path="+subpath #~ ftp=ftplib.FTP_TLS() #~ ftp.connect(server,port, timeout=5) #~ ftp.login("anonymous", "nomatter") #this fails! #~ ftp.cwd(subpath) #~ executeTests() ################### if __name__ == '__main__': if "OUT_DIR_JUNIT_XML" in os.environ: unittest.main(testRunner=xmlrunner.XMLTestRunner(output=os.environ["OUT_DIR_JUNIT_XML"]), failfast=False, buffer=False, catchbreak=False) else: unittest.main() MEGAcmd-2.5.2_Linux/tests/megacmd_tests_common.py000066400000000000000000000365201516543156300220160ustar00rootroot00000000000000#!/usr/bin/python3 # -*- coding: utf-8 -*- import sys, os, subprocess, shutil, re, platform import fnmatch import pexpect VERBOSE = 'VERBOSE' in os.environ CMDSHELL = 'MEGACMDSHELL' in os.environ if CMDSHELL: MEGACMDSHELL = os.environ['MEGACMDSHELL'] if 'MEGA_EMAIL' in os.environ and 'MEGA_PWD' in os.environ and 'MEGA_EMAIL_AUX' in os.environ and 'MEGA_PWD_AUX' in os.environ: MEGA_EMAIL = os.environ['MEGA_EMAIL'] MEGA_PWD = os.environ['MEGA_PWD'] MEGA_EMAIL_AUX = os.environ['MEGA_EMAIL_AUX'] MEGA_PWD_AUX = os.environ['MEGA_PWD_AUX'] else: raise Exception('Environment variables MEGA_EMAIL, MEGA_PWD, MEGA_EMAIL_AUX, MEGA_PWD_AUX are not defined. WARNING: Use a test account for $MEGA_EMAIL') def build_command_name(command): if platform.system() == 'Windows': return 'MEGAclient.exe ' + command elif platform.system() == 'Darwin': return 'mega-exec ' + command else: return 'mega-' + command GET = build_command_name('get') PUT = build_command_name('put') RM = build_command_name('rm') MV = build_command_name('mv') CD = build_command_name('cd') CP = build_command_name('cp') THUMB = build_command_name('thumbnail') LCD = build_command_name('lcd') MKDIR = build_command_name('mkdir') EXPORT = build_command_name('export') SHARE = build_command_name('share') INVITE = build_command_name('invite') FIND = build_command_name('find') WHOAMI = build_command_name('whoami') LOGOUT = build_command_name('logout') LOGIN = build_command_name('login') IPC = build_command_name('ipc') FTP = build_command_name('ftp') IMPORT = build_command_name('import') CAT = build_command_name('cat') #execute command def ec(what): if VERBOSE: print("Executing "+what) process = subprocess.Popen(what, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdoutdata, stderrdata = process.communicate() stdoutdata=stdoutdata.replace(b'\r\n',b'\n') stderrdata=stderrdata.replace(b'\r\n',b'\n') if VERBOSE: print(stdoutdata.strip()) print(stderrdata.strip()) return stdoutdata,process.returncode,stderrdata #execute and return only stdout contents def ex(what): return ec(what)[0] #return subprocess.Popen(what, shell=True, stdout=subprocess.PIPE).stdout.read() #Execute and strip, return only stdout def es(what): return ec(what)[0].strip() #Execute and strip with status code and err def esc(what): out, status, err =ec(what) return out.strip(),status, err.strip() #exit if failed def ef(what): out,code,err=esc(what) if code != 0: print("FAILED trying "+ what, file=sys.stderr) print(out, file=sys.stderr) print(err, file=sys.stderr) exit(code) return out def cmdshell_ec(what): what = re.sub('^mega-', '', what) if VERBOSE: print(f'Executing in cmdshell: {what}') # We must ensure there are enough columns so long commands don't get truncated child = pexpect.spawn(MEGACMDSHELL, encoding='utf-8', dimensions=(32, 256), timeout=None) quit_command = 'quit --only-shell' def has_prompt(s, command): return any(f'{p} {command}' in s for p in ['$', 'MEGA CMD>']) def wait_shell_prompt(): child.expect([r'(?=.+:.+\$ )', r'(?=MEGA CMD> )']) try: wait_shell_prompt() # Having two lcd commands is not necessary and messes up the output parsing if not what.startswith('lcd'): child.sendline(f'lcd {os.getcwd()}') child.sendline(what) wait_shell_prompt() # Stop the shell and wait for end-of-file child.sendline(quit_command) child.expect(pexpect.EOF) # The whole output of the shell split by newlines lines = child.before.replace('\r\n', '\n').split('\n') # Find the start of our command start = next(i for i, s in enumerate(lines) if has_prompt(s, what)) # Find the end of our shell by searching for the quit command end = next(i for i, s in enumerate(lines[start+1:], start+1) if quit_command in s) # The output of our command is the string in-between the start and end indices out = '\n'.join(lines[start+1:end]).strip() except pexpect.EOF: print('Shell session ended') return '', -1, '' except pexpect.TIMEOUT: print('Timed out waiting for output') return '', -1, '' finally: child.close() out = re.sub(r'.*\x1b\[K','', out) # erase line controls out = re.sub(r'.*\r', '', out) # erase non printable stuff # extract log lines as stderr output. This is subideal: but pexpect.spawn does not seem to support providing separated stderr stderr_pattern = r'\[\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\.\d{6} (sdk|cmd)' stdout_content, stderr_content = '\n'.join([line for line in out.split('\n') if not re.match(stderr_pattern, line)]), \ '\n'.join([line for line in out.split('\n') if re.match(stderr_pattern, line)]) if VERBOSE: print(f'Exit code: {child.exitstatus}') print(f'Out: {stdout_content}') print(f'Err: {stderr_content}') return stdout_content.encode('utf-8'), child.exitstatus, stderr_content.encode('utf-8') #execute and return only stdout contents def cmdshell_ex(what): return cmdshell_ec(what)[0] #Execute and strip, return only stdout def cmdshell_es(what): return cmdshell_ec(what)[0].strip() #Execute and strip with status code and err def cmdshell_esc(what): out,code,err=cmdshell_ec(what) return out.strip(), code, err.strip() #exit if failed def cmdshell_ef(what): out,code, err=cmdshell_ec(what) if code != 0: print("FAILED trying "+str(what), file=sys.stderr) print(out, file=sys.stderr) print(err, file=sys.stderr) exit(code) return out def cmd_ec(what): if CMDSHELL: return cmdshell_ec(what) else: return ec(what) #execute and return only stdout contents def cmd_ex(what): if CMDSHELL: return cmdshell_ex(what) else: return ex(what) #Execute and strip, return only stdout def cmd_es(what): if CMDSHELL: return cmdshell_es(what) else: return es(what) #Execute and strip with status code and err def cmd_esc(what): if CMDSHELL: return cmdshell_esc(what) else: return esc(what) #exit if failed def cmd_ef(what): if CMDSHELL: return cmdshell_ef(what) else: return ef(what) def rmfolderifexisting(what): if os.path.exists(what): shutil.rmtree(what) def rmfileifexisting(what): if os.path.exists(what): os.remove(what) def rmcontentsifexisting(what): if os.path.exists(what) and os.path.isdir(what): shutil.rmtree(what) os.makedirs(what) def copybyfilepattern(origin,pattern,destiny): for f in fnmatch.filter(os.listdir(origin),pattern): shutil.copy2(origin+'/'+f,destiny) def copyfolder(origin,destiny): shutil.copytree(origin,destiny+'/'+origin.split('/')[-1]) def copybypattern(origin,pattern,destiny): for f in fnmatch.filter(os.listdir(origin),pattern): if (os.path.isdir(origin+'/'+f)): copyfolder(origin+'/'+f,destiny) else: shutil.copy2(origin+'/'+f,destiny) def makedir(what): if (not os.path.exists(what)): os.makedirs(what) def osvar(what): try: return os.environ[what] except: return "" def sort(what): if isinstance(what, bytes): return b"\n".join(sorted(what.split(b"\n"))).decode() return "\n".join(sorted(what.split("\n"))) def findR(where, prefix=""): toret="" #~ print "e:",where for f in os.listdir(where): #~ print "f:",where+"/"+f toret+=prefix+f+"\n" if (os.path.isdir(where+"/"+f)): #~ print "entering ",prefix+f toret=toret+findR(where+"/"+f,prefix+f+"/") return toret def find(where, prefix=""): if not os.path.exists(where): if VERBOSE: print("file not found in find: {}, {} ".format(where, os.getcwd())) return "" if (not os.path.isdir(where)): return prefix if (prefix == ""): toret ="." else: toret=prefix if (prefix == "."): toret+="\n"+findR(where).strip() else: if prefix.endswith('/'): toret+="\n"+findR(where, prefix).strip() else: toret+="\n"+findR(where, prefix+"/").strip() return toret.strip() def ls(where, prefix=""): if not os.path.exists(where): if VERBOSE: print("file not found in find: {}, {}".format(where, os.getcwd())) return "" toret=".\n" for f in os.listdir(where): toret+=prefix+f+"\n" return toret def touch(what, times=None): with open(what, 'a'): os.utime(what, times) def out(what, where): #~ print(what, file=where) with open(where, 'w') as f: f.write(what) def clean_root_confirmed_by_user(): if "YES_I_KNOW_THIS_WILL_CLEAR_MY_MEGA_ACCOUNT" in os.environ: val = os.environ["YES_I_KNOW_THIS_WILL_CLEAR_MY_MEGA_ACCOUNT"] return bool(int(val)) else: return False UNICODE_NAME_LIST = [u'\U000213e4\ucc90\u6361\U0002569b\U00028106\u1575\u416d\u267c\u11f3\ua091\u38b2\U000201b0\u594d\U00029839\u3f46\u481c\u65b8\u25cc\U00026f3b\u26ff\u292c\U0001d6d1\u7b2b\uca6a\uc23b\U000202e5\u90d7\u7200\u6c2c\U00021cf2\u63e6\U00023cfd\U0001d7f5\U00010305\u4a06\u7716\U000247b9\ufdbb\ua841\U00021965\U0001e8d4\U00026d38\U00023a82\U0001d118\u1fc7\u114d\U000112e2\U0002a465\u90a9', u'\U0002bfb4\ua320\U0002682f\U00028d67\U0002c09e\U0002aaa3\uc703\u937a\U00028fe6\U0002c3e2\u5886\U000278ff\U000266de\U0002928c\u6c62\U0002166c\u15cc\u4fda\U00029cbb\u3308\U0002a5fd\U00029059\u4144\U0001f3d7\U00024d9d\u6117\U00029823\uc063\U0002be26\u254c\u866d\U00024837\u8e36\U0002bc25\U000205c4\U00025083\u606a\U0002323b\u1a0e\u388f\U0002b5c2\ubff5\U0002b26f\U000144f5\u4b17\u872f\ub5f4\ua2d5\U0002863a\U0002abc3', u'\u2978\U00024714\ua343\U0001d954\uff51\U00028694\u1da0\u63a1\u68c4\U0002c23f\u8315\uad85\U0002024a\ucbcb\uba86\u7cb1\u3658\u68a0\ucb6e\u7262\u021e\u98a3\U0002177d\U00025a63\U0001f47e\U000293a1\U000270f0\U00022477\u3701\U0001085a\U00022c60\U00024344\u34a9\ua2b8\u7af8\U0002b288\U00023acc\u08fa\U00013058\U00021e39\U00026f4a\U000275a1\u100b\U0002236a\ucd7b\U0002805b\U00011221\u3cd8\U0002ab7b\U0001e8cd', u'\u5c63\u1684\u8988\U0002ca91\U0002ce04\U000295a9\U00010034\u63a8\ub021\u7ac2\u99df\U0002b983\U00027c91\u32ed\U00023677\U00011156\U00025f85\u8e9b\u092f\U000293d1\u4c84\u6f2f\U00024eb7\U0001f705\u9ecd\U000217d8\U00029064\ufc23\U000222f3\U00026bc9\u581c\u95c1\ua56c\u56aa\u1266\u2f6b\U00022ee0\u5c2b\U00024506\U00022112\u387f\U0001ee2b\U00022ced\ua028\U0002566d\u1c3c\U00021489\U0002ae1a\U00028972\U0002356e', u'\U0002c6f7\u8396\U00024ec9\U0002cab5\U0002389c\U00029663\ub237\U000213fe\u3079\u664e\u1ce6\u527a\u6696\u9df5\u2de0\u6a04\U000282b9\U0002bea5\u6a06\U00020575\ub734\U0002cc4c\U00022487\U00023409\ubb28\U00025a22\U00022ad3\U000274e6\ub86a\U00021d31\u998b\U0002c3d6\U000120a0\U00028188\u26b9\U0002740f\U0002cb90\U00023559\U0002b3b9\uff19\ufe08\ufc4e\u0a71\U00020524\u4304\ua395\u3421\U0001d802\U0002be22\U0002ba22', u'\U00013146\U00029f94\U00025fb9\U000243fe\u58eb\ua42d\u9b0c\ud4c7\U0002afd9\U00020ecd\U0002b10c\U00027562\U0002295e\U00023fd2\U0001f701\U0002c5e9\U0002b12b\u3bd0\u25a1\ud170\U000277e2\U0002828c\u3f27\u949a\u5d40\u5f08\U000268bf\u9851\U000238a7\U0002a997\U00023659\U00012180\U00020e3c\u45ab\U0001f70e\U00010634\U0002421f\U0002374e\u59f9\u044c\u3b42\u3901\U00020015\U00027b6f\u6765\U00029c00\U0002a047\U0002b07d\u23dc\U0001f453', u'\U000102a0\U00029697\U00026514\ua344\U00028bcc\u7c1c\U000293a1\u45ad\U0002ae08\u19f8\U00020114\U0002b2d8\U0002432d\u1387\U000245c4\U00024710\U00020528\U000225a2\U00028f2c\u765f\U0002286b\u4715\U00027e51\U000270d4\u99fb\U00021aeb\U0002aa25\u449a\ub86c\U00022672\ud4af\U000296ea\u511a\u7889\u9cfe\U00024d16\u893b\U0002a362\u3cb4\U00026383\ud3cc\u8750\U0001e859\U00028aa5\u0af9\u056f\u4404\U0002a82e\U00020524\u55c9', u'\U0002151f\U000269cb\u8040\U0002842b\U0001d11d\U0002a212\U00025584\u142c\u2415\U00021cec\U000268e3\U00027392\u5e02\u1b6a\U000233a1\u9a28\U00022dee\u97bd\U000109eb\U0002762e\uce08\U00026d94\u2ba8\u3d7c\u9219\u2bac\U00029398\U00023b98\U00025124\U00024cad\u463b\u8b37\u5265\U00026451\u61ce\ud62c\U0001d7bd\U0001d342\u37bf\U00028105\U00025a01\u7699\u3def\U000276d6\U00011483\ucdbf\u15bc\U000286da\u8dca\ubb06', u'\u9004\U00027165\U000252e6\U00020bee\U00023f5c\uc17c\U00025a8f\U00021c40\U00021e77\U0002ae0c\U0002538a\u0b92\U0002874e\U000233ed\U0001f573\U00027be4\U0001170c\U00020755\u8b8e\u2e06\U0002525d\U0002ab05\U00010666\ub337\u3c91\ucac1\u2ab1\U000212d0\U00020e29\u5081\u18e5\u37de\U00021de9\u0786\U00023de9\u7061\u0625\u399e\U0002043e\u828a\U0002c208\u05ab\U00028859\u0100\ufcb7\U00016861\u4762\ua5e4\U000268d0\u234b', u'\U00023911\U00024084\ua5f7\U0002aaa5\u1798\uc82c\ucfc0\U0002b4fe\U00014456\U000286bb\U0002897e\U000217c5\U0002a0c0\U0002a160\ub5e8\U00022abe\U000285be\U0002cd8d\U0002c36a\U0002c040\ub183\ub7c7\U0002b082\ud724\u7dd5\u0986\U00029507\uabaa\U00021920\u811c\u29c5\u7d3f\U00027831\U000227c2\u1f31\U00021153\u79a1\U0002b233\u8d1d\U000100b9\u8a5a\u9f16\u3e9a\U0002bd58\ua738\U00021110\U00025739\U0002cd05\U0002c7bf\U00024547', u'\U0002afa6\u3eed\u399c\U0002ca82\U0001f3ed\ua8a2\U0002c898\U0001ee72\U000298ef\u24a5\U0002b2c2\u5f30\ub387\u44b5\u7280\ua040\U0001d7f4\U0002270e\u2650\U0002ad54\uc07f\u889d\U0002caf7\U0002a77c\U000132a3\U0002353e\U00026115\u9c5d\uc4a5\U0002ccb1\u6d9c\u15b6\U0002a1ce\U00022dc4\u134a\u3aba\U00010020\U000259e8\u194f\u134d\u6865\U0002b139\U00021928\U000253d0\U00028a77\U0002783d\U00022b62\u36f7\u954f\U0002b6b3', u'\u1d61\U0002be0b\U00029142\U0002b41a\U0002cc5c\U00025583\u512c\u0d91\u668a\ud36e\U00022b9a\u6766\u1ec7\u8d0d\U000218c4\U0002c073\u53de\U00029bd6\U00027727\U0002c87b\u90e5\u967c\ucef8\U00027830\u725e\u445f\u71b6\U0002676e\ua740\uca76\U00028aa8\U00029761\U000280ee\u1bde\U00028c03\ud534\U00013022\U0002b023\ufc50\U0002b5f6\U000262a1\U0002ba31\U0002844c\U0002230f\uc4c0\U00029187\U00027b98\U00026446\U00023cd3\u0942', u'\uc288\u799e\ufe86\U00020b7d\ub663\u3e50\U000115a7\ufeb2\U000222af\U00012351\U000203ff\U000131be\u15b9\U000248b2\U00023206\u3460\U00028076\U0002800d\U0002896f\ua842\U0002ce7d\U0002bad7\u4288\u0489\ufb65\u238d\U00020ffe\U000100d5\U0002c100\u58f5\ucdbd\U000252fa\uc0b6\u7e29\u1186\u8e45\U00029acf\U000271d5\U000133e9\U00021991\U00027e04\U00026111\U00029842\u7724\u26d0\u45cb\U00024507\U00020567\ud006\U000245e5', u'\U0002cbba\u9b3e\U00025a9e\ud6a7\u7be3\ub389\U000203c2\ub501\U0001087d\u5bd9\U0002250d\uacb0\u8082\U00012071\ub2af\U00022b2f\u7c4f\u0cc2\u7670\U00029653\uff1d\U00021736\ud422\uc308\ud46b\U0002a3fa\u2d4e\u7981\ub73e\u5ec3\U00020a56\U000209db\U000240c1\u04a5\U00025d84\U00029cda\u8b52\U000249af\U0002c3d7\U000210ac\U00027944\U00021488\U0001211f\U00021235\ubb61\u6453\U0002651d\u7490\u6160\U00028827', u'\U00028f6f\u62e2\U0002362b\U00028c10\U0002b62a\u99b8\u837e\u5299\U000261e5\ubf41\u7d4a\U00027de5\U00028a26\uc9e5\u561e\u948c\U0002a4e0\U00028c25\U000260a9\U0002b48e\U00028132\U00020fa0\U00028a9e\U0002b09c\U00025a63\uae03\U0002c87a\U00020e63\u8471\U00029b91\U000246f3\U0002ae88\U000264bf\U00021ae2\U0002b3cd\U0001d0b3\uadf6\U0002a488\U0002701a\U00022dd6\u38c7\uacba\U0002152a\u718b\u37bb\U00024d74\u893c\u16ab\U000280fe\U00013404', u'\U00027436\ua44f\uc908\u217f\U00027425\U0002ba8c\uc30a\U00023dc5\ud325\u70c8\U00024bca\U0002914f\U0002b522\u4651\U00023606\U000169dd\u6e8d\u2b81\u3bfc\U00028217\ua1d0\u37c7\U00025caa\u7d9f\U000e0138\U0002682a\U0002543f\U00028e37\ufb79\U00020924\U00023089\u5673\u425e\ua30a\uce45\U000275cd\u4033\U00025d88\u7eeb\u7589\u1e9b\U00027992\uc74e\U00028bd4\U00022670\u926e\U0002ba1a\U0002a0b8\U00022f27\u3a0f'] MEGAcmd-2.5.2_Linux/tests/unit/000077500000000000000000000000001516543156300162265ustar00rootroot00000000000000MEGAcmd-2.5.2_Linux/tests/unit/ComunicationsManagerFileSocketsTests.cpp000066400000000000000000000507251516543156300262300ustar00rootroot00000000000000/** * (c) 2025 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #ifndef _WIN32 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Instruments.h" #include "comunicationsmanagerfilesockets.h" #include "megacmdcommonutils.h" using namespace megacmd; using namespace std::chrono_literals; class TestSocketClient { int mSocket; public: TestSocketClient() : mSocket(-1) { if (mSocket = socket(AF_UNIX, SOCK_STREAM, 0); mSocket >= 0) { struct sockaddr_un addr; memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; auto socketPath = getOrCreateSocketPath(false); strncpy(addr.sun_path, socketPath.c_str(), sizeof(addr.sun_path) - 1); if (connect(mSocket, (struct sockaddr*)&addr, sizeof(addr)) != 0) { ::close(mSocket); mSocket = -1; } } } ~TestSocketClient() { close(); } bool isConnected() const { return mSocket >= 0; } bool send(const std::string& data) { if (!isConnected()) { return false; } return ::send(mSocket, data.c_str(), data.size(), 0) >= 0; } bool send(const void* data, size_t size) { if (!isConnected()) { return false; } return ::send(mSocket, data, size, 0) >= 0; } ssize_t receive(void* buffer, size_t size, int flags = 0) { if (!isConnected()) { return -1; } return recv(mSocket, buffer, size, flags); } void close() { if (mSocket >= 0) { ::close(mSocket); mSocket = -1; } } TestSocketClient(const TestSocketClient&) = delete; TestSocketClient& operator=(const TestSocketClient&) = delete; }; std::unique_ptr sendPetition(ComunicationsManagerFileSockets &manager, TestSocketClient &client, const std::string &data) { std::promise waitStarted; auto waitStartedFuture = waitStarted.get_future(); std::thread waitThread([&manager, &waitStarted]() { waitStarted.set_value(); manager.waitForPetition(); }); // Wait for waitForPetition to start waitStartedFuture.wait(); if (!client.send(data)) { waitThread.join(); return nullptr; } // Poll for petition availability instead of fixed sleep constexpr int maxAttempts = 10; for (int i = 0; i < maxAttempts; i++) { if (manager.receivedPetition()) { auto petition = manager.getPetition(); waitThread.join(); return petition; } std::this_thread::sleep_for(10ms); } waitThread.join(); return nullptr; } #ifdef __MACH__ // On macOS, MSG_NOSIGNAL doesn't work, so we need to ignore SIGPIPE // to prevent the process from being killed when writing to closed socket struct sigaction ignoreSigpipeAndGetOldHandler() { struct sigaction sa, oldSa; sa.sa_handler = SIG_IGN; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGPIPE, &sa, &oldSa); return oldSa; } void restoreSigpipeHandler(const struct sigaction& oldSa) { sigaction(SIGPIPE, &oldSa, nullptr); } #endif class ComunicationsManagerFileSocketsTest : public ::testing::Test { protected: void SetUp() override { TestInstruments::Instance().clearAll(); } void TearDown() override { TestInstruments::Instance().clearAll(); } }; class ComunicationsManagerFileSocketsWithClientTest : public ComunicationsManagerFileSocketsTest { protected: ComunicationsManagerFileSockets mManager; std::unique_ptr mClient; void SetUp() override { ComunicationsManagerFileSocketsTest::SetUp(); mClient = std::make_unique(); ASSERT_TRUE(mClient->isConnected()); } }; TEST_F(ComunicationsManagerFileSocketsTest, InitializeSuccess) { ComunicationsManagerFileSockets manager; int result = manager.initialize(); EXPECT_EQ(result, 0); } TEST_F(ComunicationsManagerFileSocketsTest, InitializeWithLongSocketPath) { std::string longName(300, 'a'); auto guard = TestInstrumentsEnvVarGuard("MEGACMD_SOCKET_NAME", longName); ComunicationsManagerFileSockets manager; int result = manager.initialize(); EXPECT_EQ(result, -1); } TEST_F(ComunicationsManagerFileSocketsWithClientTest, GetPetitionReadsData) { std::string testData = "test command"; auto petition = sendPetition(mManager, *mClient, testData); ASSERT_NE(petition, nullptr); EXPECT_EQ(petition->getLine(), testData); } TEST_F(ComunicationsManagerFileSocketsWithClientTest, GetPetitionReadsLargeData) { std::string largeData(2048, 'A'); auto petition = sendPetition(mManager, *mClient, largeData); ASSERT_NE(petition, nullptr); EXPECT_EQ(petition->getLine(), largeData); } TEST_F(ComunicationsManagerFileSocketsWithClientTest, ReturnAndClosePetitionSendsData) { auto petition = sendPetition(mManager, *mClient, "test"); ASSERT_NE(petition, nullptr); OUTSTRINGSTREAM response; response << "response data"; mManager.returnAndClosePetition(std::move(petition), &response, 0); int outCode = MCMD_OK; ssize_t n = mClient->receive(&outCode, sizeof(outCode)); ASSERT_GT(n, 0); EXPECT_EQ(outCode, 0); char buffer[1024]; n = mClient->receive(buffer, sizeof(buffer) - 1); ASSERT_GT(n, 0); buffer[n] = '\0'; EXPECT_STREQ(buffer, "response data"); } TEST_F(ComunicationsManagerFileSocketsWithClientTest, ReturnAndClosePetitionWithEmptyResponse) { auto petition = sendPetition(mManager, *mClient, "test"); ASSERT_NE(petition, nullptr); OUTSTRINGSTREAM response; response << ""; mManager.returnAndClosePetition(std::move(petition), &response, 42); int outCode = MCMD_OK; ssize_t n = mClient->receive(&outCode, sizeof(outCode)); ASSERT_GT(n, 0); EXPECT_EQ(outCode, 42); char buffer[64]; n = mClient->receive(buffer, sizeof(buffer), MSG_DONTWAIT); EXPECT_LE(n, 0); } TEST_F(ComunicationsManagerFileSocketsWithClientTest, SendPartialOutput) { auto petition = sendPetition(mManager, *mClient, "test"); ASSERT_NE(petition, nullptr); std::string partialData = "partial output"; mManager.sendPartialOutput(petition.get(), partialData.data(), partialData.size(), false); int outCode = MCMD_OK; mClient->receive(&outCode, sizeof(outCode)); EXPECT_EQ(outCode, MCMD_PARTIALOUT); size_t size = 0; mClient->receive(&size, sizeof(size)); EXPECT_EQ(size, partialData.size()); char buffer[1024]; ssize_t n = mClient->receive(buffer, size); ASSERT_GT(n, 0); buffer[size] = '\0'; EXPECT_STREQ(buffer, partialData.c_str()); } TEST_F(ComunicationsManagerFileSocketsWithClientTest, SendPartialError) { auto petition = sendPetition(mManager, *mClient, "test"); ASSERT_NE(petition, nullptr); std::string errorData = "error message"; mManager.sendPartialError(petition.get(), errorData.data(), errorData.size(), false); int outCode = MCMD_OK; mClient->receive(&outCode, sizeof(outCode)); EXPECT_EQ(outCode, MCMD_PARTIALERR); } TEST_F(ComunicationsManagerFileSocketsWithClientTest, SendPartialOutputHandlesEPIPE) { auto petition = sendPetition(mManager, *mClient, "test"); ASSERT_NE(petition, nullptr); mClient->close(); #ifdef __MACH__ auto oldSa = ignoreSigpipeAndGetOldHandler(); #endif std::string data = "test data"; mManager.sendPartialOutput(petition.get(), data.data(), data.size(), false); EXPECT_TRUE(petition->clientDisconnected); #ifdef __MACH__ restoreSigpipeHandler(oldSa); #endif } TEST_F(ComunicationsManagerFileSocketsWithClientTest, SendPartialOutputValidatesUTF8) { auto petition = sendPetition(mManager, *mClient, "test"); ASSERT_NE(petition, nullptr); char invalidUtf8[] = "\xC0\x80"; mManager.sendPartialOutput(petition.get(), invalidUtf8, 2, false); // Invalid UTF-8 must not be sent. char buffer[64]; ssize_t n = mClient->receive(buffer, sizeof(buffer), MSG_DONTWAIT); EXPECT_LE(n, 0); } TEST_F(ComunicationsManagerFileSocketsWithClientTest, RegisterStateListener) { auto petition = sendPetition(mManager, *mClient, "test"); ASSERT_NE(petition, nullptr); auto listener = mManager.registerStateListener(std::move(petition)); EXPECT_NE(listener, nullptr); } TEST_F(ComunicationsManagerFileSocketsTest, GetMaxStateListeners) { ComunicationsManagerFileSockets manager; int maxListeners = manager.getMaxStateListeners(); EXPECT_GT(maxListeners, 0); EXPECT_LE(maxListeners, FD_SETSIZE); } TEST_F(ComunicationsManagerFileSocketsWithClientTest, InformStateListener) { auto petition = sendPetition(mManager, *mClient, "test"); ASSERT_NE(petition, nullptr); auto listener = mManager.registerStateListener(std::move(petition)); ASSERT_NE(listener, nullptr); std::string message = "state update"; int result = mManager.informStateListener(listener, message); EXPECT_EQ(result, 0); char buffer[1024]; ssize_t n = mClient->receive(buffer, sizeof(buffer) - 1, MSG_DONTWAIT); EXPECT_GT(n, 0); buffer[n] = '\0'; EXPECT_STREQ(buffer, message.c_str()); } TEST_F(ComunicationsManagerFileSocketsWithClientTest, InformStateListenerHandlesEPIPE) { auto petition = sendPetition(mManager, *mClient, "test"); ASSERT_NE(petition, nullptr); auto listener = mManager.registerStateListener(std::move(petition)); mClient->close(); #ifdef __MACH__ auto oldSa = ignoreSigpipeAndGetOldHandler(); #endif std::string message = "state update"; int result = mManager.informStateListener(listener, message); EXPECT_EQ(result, -1); #ifdef __MACH__ restoreSigpipeHandler(oldSa); #endif } TEST_F(ComunicationsManagerFileSocketsWithClientTest, InformStateListenerValidatesUTF8) { auto petition = sendPetition(mManager, *mClient, "test"); ASSERT_NE(petition, nullptr); auto listener = mManager.registerStateListener(std::move(petition)); std::string invalidUtf8 = "\xC0\x80"; int result = mManager.informStateListener(listener, invalidUtf8); EXPECT_EQ(result, 0); } TEST_F(ComunicationsManagerFileSocketsWithClientTest, GetConfirmation) { auto petition = sendPetition(mManager, *mClient, "test"); ASSERT_NE(petition, nullptr); std::thread responseThread([this]() { int outCode = MCMD_OK; mClient->receive(&outCode, sizeof(outCode)); EXPECT_EQ(outCode, MCMD_REQCONFIRM); char buffer[1024]; mClient->receive(buffer, sizeof(buffer) - 1); int response = 1; mClient->send(&response, sizeof(response)); }); std::string message = "Confirm action?"; int result = mManager.getConfirmation(petition.get(), message); responseThread.join(); EXPECT_EQ(result, 1); } TEST_F(ComunicationsManagerFileSocketsWithClientTest, GetUserResponse) { auto petition = sendPetition(mManager, *mClient, "test"); ASSERT_NE(petition, nullptr); std::thread responseThread([this]() { int outCode = MCMD_OK; mClient->receive(&outCode, sizeof(outCode)); EXPECT_EQ(outCode, MCMD_REQSTRING); char buffer[1024]; mClient->receive(buffer, sizeof(buffer) - 1); std::string response = "user input"; mClient->send(response); }); std::string message = "Enter value:"; std::string result = mManager.getUserResponse(petition.get(), message); responseThread.join(); EXPECT_EQ(result, "user input"); } TEST_F(ComunicationsManagerFileSocketsWithClientTest, GetUserResponseLargeData) { auto petition = sendPetition(mManager, *mClient, "test"); ASSERT_NE(petition, nullptr); std::thread responseThread([this]() { int outCode = MCMD_OK; mClient->receive(&outCode, sizeof(outCode)); EXPECT_EQ(outCode, MCMD_REQSTRING); char buffer[1024]; mClient->receive(buffer, sizeof(buffer) - 1); std::string largeResponse(3000, 'A'); mClient->send(largeResponse); }); std::string message = "Enter value:"; std::string result = mManager.getUserResponse(petition.get(), message); responseThread.join(); EXPECT_EQ(result, std::string(3000, 'A')); } TEST_F(ComunicationsManagerFileSocketsTest, StopWaiting) { ComunicationsManagerFileSockets manager; manager.stopWaiting(); } TEST_F(ComunicationsManagerFileSocketsTest, StressMultipleConcurrentClients) { ComunicationsManagerFileSockets manager; constexpr int numClients = 20; std::vector> clients; std::vector threads; std::atomic successCount{0}; for (int i = 0; i < numClients; i++) { clients.push_back(std::make_unique()); ASSERT_TRUE(clients.back()->isConnected()); } std::thread waitThread([&manager]() { for (int i = 0; i < 20; i++) { manager.waitForPetition(); std::this_thread::sleep_for(10ms); } }); waitThread.detach(); for (int i = 0; i < numClients; i++) { threads.emplace_back([&clients, i]() { std::string testData = "test command " + std::to_string(i); clients[i]->send(testData); }); } for (auto& t : threads) { t.join(); } constexpr int maxAttempts = 50; for (int i = 0; i < maxAttempts && successCount < numClients; i++) { if (manager.receivedPetition()) { auto petition = manager.getPetition(); if (petition != nullptr && !petition->getLine().empty()) { successCount++; } } std::this_thread::sleep_for(10ms); } EXPECT_EQ(successCount.load(), numClients); } TEST_F(ComunicationsManagerFileSocketsTest, StressMaxStateListeners) { ComunicationsManagerFileSockets manager; int maxListeners = manager.getMaxStateListeners(); EXPECT_GT(maxListeners, 0); constexpr int testListeners = 10; std::vector> clients; std::vector listeners; int registeredCount = 0; std::thread waitThread([&manager]() { for (int i = 0; i < testListeners; i++) { manager.waitForPetition(); std::this_thread::sleep_for(5ms); } }); waitThread.detach(); for (int i = 0; i < testListeners; i++) { clients.push_back(std::make_unique()); ASSERT_TRUE(clients.back()->isConnected()); clients.back()->send("test"); } // Poll for petitions with timeout constexpr int maxAttempts = 50; for (int attempt = 0; attempt < maxAttempts && registeredCount < testListeners; attempt++) { if (manager.receivedPetition()) { if (auto petition = manager.getPetition(); petition != nullptr) { if (auto listener = manager.registerStateListener(std::move(petition)); listener != nullptr) { listeners.push_back(listener); registeredCount++; } } } std::this_thread::sleep_for(10ms); } EXPECT_EQ(registeredCount, testListeners); } TEST_F(ComunicationsManagerFileSocketsTest, StressConcurrentPartialOutputs) { ComunicationsManagerFileSockets manager; constexpr int numClients = 20; std::vector> clients; std::vector> petitions; for (int i = 0; i < numClients; i++) { clients.push_back(std::make_unique()); } std::thread waitThread([&manager]() { for (int i = 0; i < numClients; i++) { manager.waitForPetition(); std::this_thread::sleep_for(10ms); } }); waitThread.detach(); for (int i = 0; i < numClients; i++) { ASSERT_TRUE(clients[i]->isConnected()); clients[i]->send("test"); } // Poll for petitions with timeout constexpr int maxAttempts = 50; for (int attempt = 0; attempt < maxAttempts && petitions.size() < numClients; attempt++) { if (manager.receivedPetition()) { if (auto petition = manager.getPetition(); petition != nullptr) { petitions.push_back(std::move(petition)); } } std::this_thread::sleep_for(10ms); } EXPECT_EQ(petitions.size(), numClients); std::vector threads; std::atomic successCount{0}; for (size_t i = 0; i < petitions.size(); i++) { threads.emplace_back([&manager, &petitions, i, &successCount]() { std::string data = "partial output " + std::to_string(i); manager.sendPartialOutput(petitions[i].get(), data.data(), data.size(), false); successCount++; }); } for (auto& t : threads) { t.join(); } EXPECT_EQ(successCount.load(), static_cast(petitions.size())); } TEST_F(ComunicationsManagerFileSocketsTest, StressRapidConnectDisconnect) { constexpr int expectedPetitionCount = 30; ComunicationsManagerFileSockets manager; std::thread waitThread([&manager, expectedPetitionCount]() { for (int i = 0; i < expectedPetitionCount; i++) { manager.waitForPetition(); std::this_thread::sleep_for(5ms); } }); waitThread.detach(); // Small delay to ensure waitThread is running std::this_thread::sleep_for(10ms); for (int i = 0; i < expectedPetitionCount; i++) { TestSocketClient client; ASSERT_TRUE(client.isConnected()); client.send("test " + std::to_string(i)); client.close(); std::this_thread::sleep_for(5ms); } // Poll for petitions with timeout int receivedCount = 0; constexpr int maxAttempts = 50; for (int attempt = 0; attempt < maxAttempts && receivedCount < expectedPetitionCount; attempt++) { if (manager.receivedPetition()) { if (auto petition = manager.getPetition(); petition != nullptr) { receivedCount++; } } std::this_thread::sleep_for(10ms); } EXPECT_EQ(receivedCount, expectedPetitionCount); } TEST_F(ComunicationsManagerFileSocketsTest, StressLargeDataMultipleClients) { ComunicationsManagerFileSockets manager; constexpr int numClients = 10; std::vector> clients; for (int i = 0; i < numClients; i++) { clients.push_back(std::make_unique()); } std::thread waitThread([&manager]() { for (int i = 0; i < numClients; i++) { manager.waitForPetition(); std::this_thread::sleep_for(10ms); } }); waitThread.detach(); for (int i = 0; i < numClients; i++) { ASSERT_TRUE(clients[i]->isConnected()); std::string largeData(5000, static_cast('A' + i)); clients[i]->send(largeData); } // Poll for petitions with timeout (larger timeout for large data) int successCount = 0; constexpr int maxAttempts = 150; for (int attempt = 0; attempt < maxAttempts && successCount < numClients; attempt++) { if (manager.receivedPetition()) { if (auto petition = manager.getPetition(); petition != nullptr) { // Try to match with expected data for (int i = 0; i < numClients; i++) { std::string expected(5000, static_cast('A' + i)); if (petition->getLine() == expected) { successCount++; break; } } } } std::this_thread::sleep_for(10ms); } EXPECT_EQ(successCount, numClients); } #endif // _WIN32 MEGAcmd-2.5.2_Linux/tests/unit/OptionsFlagsUtilsTests.cpp000066400000000000000000000505361516543156300234170ustar00rootroot00000000000000/** * (c) 2025 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include #include #include #include #include #include #include "TestUtils.h" TEST(OptionsFlagsUtilsTest, getFlag) { using megacmd::getFlag; G_SUBTEST << "Basic case"; { std::map flags = {{"verbose", 1}, {"debug", 0}}; EXPECT_EQ(getFlag(&flags, "verbose"), 1); EXPECT_EQ(getFlag(&flags, "debug"), 0); } G_SUBTEST << "Flag does not exist"; { std::map flags = {{"verbose", 1}}; EXPECT_EQ(getFlag(&flags, "nonexistent"), 0); } G_SUBTEST << "Empty map"; { std::map flags; EXPECT_EQ(getFlag(&flags, "verbose"), 0); } G_SUBTEST << "Negative flag value"; { std::map flags = {{"flag", -1}}; EXPECT_EQ(getFlag(&flags, "flag"), -1); } G_SUBTEST << "Large flag value"; { std::map flags = {{"flag", 1000}}; EXPECT_EQ(getFlag(&flags, "flag"), 1000); } G_SUBTEST << "Multiple flags"; { std::map flags = {{"a", 1}, {"b", 2}, {"c", 3}}; EXPECT_EQ(getFlag(&flags, "a"), 1); EXPECT_EQ(getFlag(&flags, "b"), 2); EXPECT_EQ(getFlag(&flags, "c"), 3); } G_SUBTEST << "Empty flag name"; { std::map flags = {{"", 5}}; EXPECT_EQ(getFlag(&flags, ""), 5); } G_SUBTEST << "Long flag name"; { std::string longName(1000, 'a'); std::map flags = {{longName, 42}}; EXPECT_EQ(getFlag(&flags, longName.c_str()), 42); } } TEST(OptionsFlagsUtilsTest, getOption) { using megacmd::getOption; G_SUBTEST << "Basic case"; { std::map options = {{"path", "/tmp"}, {"name", "test"}}; EXPECT_EQ(getOption(&options, "path"), "/tmp"); EXPECT_EQ(getOption(&options, "name"), "test"); } G_SUBTEST << "Option does not exist"; { std::map options = {{"path", "/tmp"}}; EXPECT_EQ(getOption(&options, "nonexistent"), ""); } G_SUBTEST << "Custom default value"; { std::map options = {{"path", "/tmp"}}; EXPECT_EQ(getOption(&options, "nonexistent", "default"), "default"); } G_SUBTEST << "Empty option value"; { std::map options = {{"empty", ""}}; EXPECT_EQ(getOption(&options, "empty"), ""); } G_SUBTEST << "Empty map"; { std::map options; EXPECT_EQ(getOption(&options, "path"), ""); } G_SUBTEST << "Long option value"; { std::string longValue(1000, 'a'); std::map options = {{"long", longValue}}; EXPECT_EQ(getOption(&options, "long"), longValue); } G_SUBTEST << "Option with special characters"; { std::map options = {{"path", "/tmp/file with spaces.txt"}}; EXPECT_EQ(getOption(&options, "path"), "/tmp/file with spaces.txt"); } G_SUBTEST << "Option with unicode"; { std::string unicodeValue = u8"\u043F\u0440\u0438\u0432\u0435\u0442"; std::map options = {{"unicode", unicodeValue}}; EXPECT_EQ(getOption(&options, "unicode"), unicodeValue); } G_SUBTEST << "Empty option name"; { std::map options = {{"", "value"}}; EXPECT_EQ(getOption(&options, ""), "value"); } G_SUBTEST << "Multiple options"; { std::map options = {{"a", "1"}, {"b", "2"}, {"c", "3"}}; EXPECT_EQ(getOption(&options, "a"), "1"); EXPECT_EQ(getOption(&options, "b"), "2"); EXPECT_EQ(getOption(&options, "c"), "3"); } } TEST(OptionsFlagsUtilsTest, getOptionAsOptional) { using megacmd::getOptionAsOptional; G_SUBTEST << "Basic case"; { std::map options = {{"path", "/tmp"}}; auto result = getOptionAsOptional(options, "path"); ASSERT_TRUE(result.has_value()); EXPECT_EQ(*result, "/tmp"); } G_SUBTEST << "Option does not exist"; { std::map options = {{"path", "/tmp"}}; auto result = getOptionAsOptional(options, "nonexistent"); EXPECT_FALSE(result.has_value()); } G_SUBTEST << "Empty map"; { std::map options; auto result = getOptionAsOptional(options, "path"); EXPECT_FALSE(result.has_value()); } G_SUBTEST << "Empty option value"; { std::map options = {{"empty", ""}}; auto result = getOptionAsOptional(options, "empty"); ASSERT_TRUE(result.has_value()); EXPECT_EQ(*result, ""); } G_SUBTEST << "Long option value"; { std::string longValue(1000, 'a'); std::map options = {{"long", longValue}}; auto result = getOptionAsOptional(options, "long"); ASSERT_TRUE(result.has_value()); EXPECT_EQ(*result, longValue); } G_SUBTEST << "Option with special characters"; { std::map options = {{"path", "/tmp/file.txt"}}; auto result = getOptionAsOptional(options, "path"); ASSERT_TRUE(result.has_value()); EXPECT_EQ(*result, "/tmp/file.txt"); } G_SUBTEST << "Empty option name"; { std::map options = {{"", "value"}}; auto result = getOptionAsOptional(options, ""); ASSERT_TRUE(result.has_value()); EXPECT_EQ(*result, "value"); } } TEST(OptionsFlagsUtilsTest, getintOption) { using megacmd::getintOption; G_SUBTEST << "Basic case"; { std::map options = {{"timeout", "30"}, {"port", "8080"}}; EXPECT_EQ(getintOption(&options, "timeout"), 30); EXPECT_EQ(getintOption(&options, "port"), 8080); } G_SUBTEST << "Invalid integer option"; { std::map options = {{"timeout", "abc"}}; EXPECT_EQ(getintOption(&options, "timeout"), 0); } G_SUBTEST << "Option does not exist"; { std::map options = {{"timeout", "30"}}; EXPECT_EQ(getintOption(&options, "nonexistent"), 0); } G_SUBTEST << "Custom default value"; { std::map options = {{"timeout", "30"}}; EXPECT_EQ(getintOption(&options, "nonexistent", 42), 42); } G_SUBTEST << "Negative integer"; { std::map options = {{"value", "-10"}}; EXPECT_EQ(getintOption(&options, "value"), -10); } G_SUBTEST << "Zero value"; { std::map options = {{"value", "0"}}; EXPECT_EQ(getintOption(&options, "value"), 0); } G_SUBTEST << "INT_MAX value"; { std::map options = {{"value", "2147483647"}}; EXPECT_EQ(getintOption(&options, "value"), INT_MAX); } G_SUBTEST << "INT_MIN value"; { std::map options = {{"value", "-2147483648"}}; EXPECT_EQ(getintOption(&options, "value"), INT_MIN); } G_SUBTEST << "Number exceeding INT_MAX"; { std::map options = {{"value", "2147483648"}}; EXPECT_EQ(getintOption(&options, "value"), 0); } G_SUBTEST << "Number below INT_MIN"; { std::map options = {{"value", "-2147483649"}}; EXPECT_EQ(getintOption(&options, "value"), 0); } G_SUBTEST << "Integer with leading spaces"; { std::map options = {{"value", " 123"}}; EXPECT_EQ(getintOption(&options, "value"), 123); } G_SUBTEST << "Integer with trailing spaces"; { std::map options = {{"value", "123 "}}; EXPECT_EQ(getintOption(&options, "value"), 123); } G_SUBTEST << "Integer with spaces on both sides"; { std::map options = {{"value", " 123 "}}; EXPECT_EQ(getintOption(&options, "value"), 123); } G_SUBTEST << "Partial integer string"; { std::map options = {{"value", "123abc"}}; EXPECT_EQ(getintOption(&options, "value"), 123); } G_SUBTEST << "Empty string"; { std::map options = {{"value", ""}}; EXPECT_EQ(getintOption(&options, "value"), 0); } G_SUBTEST << "Empty map"; { std::map options; EXPECT_EQ(getintOption(&options, "value"), 0); } G_SUBTEST << "Custom default with invalid option"; { std::map options = {{"value", "invalid"}}; EXPECT_EQ(getintOption(&options, "value", 99), 99); } G_SUBTEST << "Custom default with empty string"; { std::map options = {{"value", ""}}; EXPECT_EQ(getintOption(&options, "value", 99), 99); } G_SUBTEST << "Custom default with spaces"; { std::map options = {{"value", " 123 "}}; EXPECT_EQ(getintOption(&options, "value", 99), 123); } G_SUBTEST << "Very large int number string"; { std::map options = {{"value", "999999999999999999999"}}; EXPECT_EQ(getintOption(&options, "value"), 0); } G_SUBTEST << "Very large negative int number string"; { std::map options = {{"value", "-999999999999999999999"}}; EXPECT_EQ(getintOption(&options, "value"), 0); } G_SUBTEST << "String with only spaces"; { std::map options = {{"value", " "}}; EXPECT_EQ(getintOption(&options, "value"), 0); } G_SUBTEST << "String starting with non-digit"; { std::map options = {{"value", "abc123"}}; EXPECT_EQ(getintOption(&options, "value"), 0); } G_SUBTEST << "String with special characters"; { std::map options = {{"value", "12@34"}}; EXPECT_EQ(getintOption(&options, "value"), 12); } } TEST(OptionsFlagsUtilsTest, getIntOptional) { using megacmd::getIntOptional; G_SUBTEST << "Basic case"; { std::map options = {{"timeout", "30"}, {"port", "8080"}}; auto result = getIntOptional(options, "timeout"); ASSERT_TRUE(result.has_value()); EXPECT_EQ(*result, 30); result = getIntOptional(options, "port"); ASSERT_TRUE(result.has_value()); EXPECT_EQ(*result, 8080); } G_SUBTEST << "Invalid integer option"; { std::map options = {{"timeout", "abc"}}; auto result = getIntOptional(options, "timeout"); EXPECT_FALSE(result.has_value()); } G_SUBTEST << "Option does not exist"; { std::map options = {{"timeout", "30"}}; auto result = getIntOptional(options, "nonexistent"); EXPECT_FALSE(result.has_value()); } G_SUBTEST << "Empty map"; { std::map options; auto result = getIntOptional(options, "timeout"); EXPECT_FALSE(result.has_value()); } G_SUBTEST << "Negative integer"; { std::map options = {{"value", "-10"}}; auto result = getIntOptional(options, "value"); ASSERT_TRUE(result.has_value()); EXPECT_EQ(*result, -10); } G_SUBTEST << "Zero value"; { std::map options = {{"value", "0"}}; auto result = getIntOptional(options, "value"); ASSERT_TRUE(result.has_value()); EXPECT_EQ(*result, 0); } G_SUBTEST << "INT_MAX value"; { std::map options = {{"value", "2147483647"}}; auto result = getIntOptional(options, "value"); ASSERT_TRUE(result.has_value()); EXPECT_EQ(*result, INT_MAX); } G_SUBTEST << "INT_MIN value"; { std::map options = {{"value", "-2147483648"}}; auto result = getIntOptional(options, "value"); ASSERT_TRUE(result.has_value()); EXPECT_EQ(*result, INT_MIN); } G_SUBTEST << "Number exceeding INT_MAX"; { std::map options = {{"value", "2147483648"}}; auto result = getIntOptional(options, "value"); EXPECT_FALSE(result.has_value()); } G_SUBTEST << "Number below INT_MIN"; { std::map options = {{"value", "-2147483649"}}; auto result = getIntOptional(options, "value"); EXPECT_FALSE(result.has_value()); } G_SUBTEST << "Partial integer string"; { std::map options = {{"value", "123abc"}}; auto result = getIntOptional(options, "value"); ASSERT_TRUE(result.has_value()); EXPECT_EQ(*result, 123); } G_SUBTEST << "Empty string"; { std::map options = {{"value", ""}}; auto result = getIntOptional(options, "value"); EXPECT_FALSE(result.has_value()); } G_SUBTEST << "Integer with leading spaces"; { std::map options = {{"value", " 123"}}; auto result = getIntOptional(options, "value"); ASSERT_TRUE(result.has_value()); EXPECT_EQ(*result, 123); } G_SUBTEST << "Integer with trailing spaces"; { std::map options = {{"value", "123 "}}; auto result = getIntOptional(options, "value"); ASSERT_TRUE(result.has_value()); EXPECT_EQ(*result, 123); } G_SUBTEST << "Integer with spaces on both sides"; { std::map options = {{"value", " 123 "}}; auto result = getIntOptional(options, "value"); ASSERT_TRUE(result.has_value()); EXPECT_EQ(*result, 123); } G_SUBTEST << "Very large number string"; { std::map options = {{"value", "999999999999999999999"}}; auto result = getIntOptional(options, "value"); EXPECT_FALSE(result.has_value()); } G_SUBTEST << "Very large negative number string"; { std::map options = {{"value", "-999999999999999999999"}}; auto result = getIntOptional(options, "value"); EXPECT_FALSE(result.has_value()); } G_SUBTEST << "String with only spaces"; { std::map options = {{"value", " "}}; auto result = getIntOptional(options, "value"); EXPECT_FALSE(result.has_value()); } G_SUBTEST << "String starting with non-digit"; { std::map options = {{"value", "abc123"}}; auto result = getIntOptional(options, "value"); EXPECT_FALSE(result.has_value()); } G_SUBTEST << "String with special characters"; { std::map options = {{"value", "12@34"}}; auto result = getIntOptional(options, "value"); ASSERT_TRUE(result.has_value()); EXPECT_EQ(*result, 12); } G_SUBTEST << "Positive number with plus sign"; { std::map options = {{"value", "+123"}}; auto result = getIntOptional(options, "value"); ASSERT_TRUE(result.has_value()); EXPECT_EQ(*result, 123); } G_SUBTEST << "Single digit"; { std::map options = {{"value", "5"}}; auto result = getIntOptional(options, "value"); ASSERT_TRUE(result.has_value()); EXPECT_EQ(*result, 5); } G_SUBTEST << "Large positive number within range"; { std::map options = {{"value", "1000000"}}; auto result = getIntOptional(options, "value"); ASSERT_TRUE(result.has_value()); EXPECT_EQ(*result, 1000000); } G_SUBTEST << "Large negative number within range"; { std::map options = {{"value", "-1000000"}}; auto result = getIntOptional(options, "value"); ASSERT_TRUE(result.has_value()); EXPECT_EQ(*result, -1000000); } } TEST(OptionsFlagsUtilsTest, discardOptionsAndFlags) { using megacmd::discardOptionsAndFlags; G_SUBTEST << "Basic case"; { std::vector words = {"cmd", "-v", "--path=/tmp", "arg1"}; discardOptionsAndFlags(&words); std::vector expected = {"cmd", "arg1"}; EXPECT_EQ(words, expected); } G_SUBTEST << "No options or flags"; { std::vector words = {"cmd", "arg1", "arg2"}; discardOptionsAndFlags(&words); std::vector expected = {"cmd", "arg1", "arg2"}; EXPECT_EQ(words, expected); } G_SUBTEST << "Only options and flags"; { std::vector words = {"-v", "--path=/tmp"}; discardOptionsAndFlags(&words); std::vector expected = {}; EXPECT_EQ(words, expected); } G_SUBTEST << "Empty vector"; { std::vector words; discardOptionsAndFlags(&words); std::vector expected = {}; EXPECT_EQ(words, expected); } G_SUBTEST << "Single dash"; { std::vector words = {"cmd", "-", "arg1"}; discardOptionsAndFlags(&words); std::vector expected = {"cmd", "arg1"}; EXPECT_EQ(words, expected); } G_SUBTEST << "Double dash flag"; { std::vector words = {"cmd", "--flag", "arg1"}; discardOptionsAndFlags(&words); std::vector expected = {"cmd", "arg1"}; EXPECT_EQ(words, expected); } G_SUBTEST << "Multiple flags"; { std::vector words = {"cmd", "-a", "-b", "-c", "arg1"}; discardOptionsAndFlags(&words); std::vector expected = {"cmd", "arg1"}; EXPECT_EQ(words, expected); } G_SUBTEST << "Mixed flags and options"; { std::vector words = {"cmd", "-v", "--path=/tmp", "--name=test", "arg1", "arg2"}; discardOptionsAndFlags(&words); std::vector expected = {"cmd", "arg1", "arg2"}; EXPECT_EQ(words, expected); } G_SUBTEST << "Flags at the end"; { std::vector words = {"cmd", "arg1", "-v"}; discardOptionsAndFlags(&words); std::vector expected = {"cmd", "arg1"}; EXPECT_EQ(words, expected); } G_SUBTEST << "Flags at the beginning"; { std::vector words = {"-v", "--path=/tmp", "cmd", "arg1"}; discardOptionsAndFlags(&words); std::vector expected = {"cmd", "arg1"}; EXPECT_EQ(words, expected); } G_SUBTEST << "Consecutive flags"; { std::vector words = {"cmd", "-a", "-b", "-c", "arg1"}; discardOptionsAndFlags(&words); std::vector expected = {"cmd", "arg1"}; EXPECT_EQ(words, expected); } G_SUBTEST << "Long flag name"; { std::vector words = {"cmd", "--very-long-flag-name", "arg1"}; discardOptionsAndFlags(&words); std::vector expected = {"cmd", "arg1"}; EXPECT_EQ(words, expected); } G_SUBTEST << "Option with empty value"; { std::vector words = {"cmd", "--option=", "arg1"}; discardOptionsAndFlags(&words); std::vector expected = {"cmd", "arg1"}; EXPECT_EQ(words, expected); } G_SUBTEST << "Only command"; { std::vector words = {"cmd"}; discardOptionsAndFlags(&words); std::vector expected = {"cmd"}; EXPECT_EQ(words, expected); } } MEGAcmd-2.5.2_Linux/tests/unit/PlatformDirectoriesTests.cpp000066400000000000000000000473251516543156300237510ustar00rootroot00000000000000/** * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include #include #include #include "Instruments.h" #include "TestUtils.h" #include "configurationmanager.h" TEST(PlatformDirectoriesTest, runtimeDirPath) { using megacmd::ConfigurationManager; using megacmd::PlatformDirectories; auto dirs = PlatformDirectories::getPlatformSpecificDirectories(); ASSERT_TRUE(dirs != nullptr); #ifndef __APPLE__ EXPECT_EQ(dirs->runtimeDirPath(), dirs->configDirPath()); #endif #ifndef _WIN32 G_SUBTEST << "Non existing HOME folder"; { auto homeGuard = TestInstrumentsEnvVarGuard("HOME", "/non-existent-dir"); EXPECT_STREQ(dirs->runtimeDirPath().c_str(), megacmd::PosixDirectories::noHomeFallbackFolder().c_str()); ASSERT_TRUE(ConfigurationManager::lockExecution()); ASSERT_TRUE(ConfigurationManager::unlockExecution()); } G_SUBTEST << "Empty HOME folder"; { auto homeGuard = TestInstrumentsEnvVarGuard("HOME", ""); EXPECT_STREQ(dirs->runtimeDirPath().c_str(), megacmd::PosixDirectories::noHomeFallbackFolder().c_str()); ASSERT_TRUE(ConfigurationManager::lockExecution()); ASSERT_TRUE(ConfigurationManager::unlockExecution()); } #ifdef __APPLE__ G_SUBTEST << "Existing HOME folder (macOS)"; { SelfDeletingTmpFolder tmpFolder; auto homeGuard = TestInstrumentsEnvVarGuard("HOME", tmpFolder.string()); fs::create_directories(tmpFolder.path() / "Library" / "Caches"); EXPECT_STREQ(dirs->runtimeDirPath().c_str(), tmpFolder.string().append("/Library/Caches/megacmd.mac").c_str()); ASSERT_TRUE(ConfigurationManager::lockExecution()); ASSERT_TRUE(ConfigurationManager::unlockExecution()); } #else G_SUBTEST << "Existing HOME folder"; { auto homeGuard = TestInstrumentsEnvVarGuard("HOME", "/tmp"); EXPECT_EQ(dirs->runtimeDirPath(), "/tmp/.megaCmd"); ASSERT_TRUE(ConfigurationManager::lockExecution()); ASSERT_TRUE(ConfigurationManager::unlockExecution()); } #endif G_SUBTEST << "Existing non-ascii HOME folder"; { SelfDeletingTmpFolder tmpFolder("file_\u5f20\u4e09"); #ifdef __APPLE__ fs::path runtimeFolder = tmpFolder.path() / "Library" / "Caches" / "megacmd.mac"; fs::create_directories(runtimeFolder); #else fs::path runtimeFolder = tmpFolder.path() / ".megaCmd"; #endif auto homeGuard = TestInstrumentsEnvVarGuard("HOME", tmpFolder.string()); EXPECT_EQ(dirs->runtimeDirPath().string(), runtimeFolder.string()); ASSERT_TRUE(ConfigurationManager::lockExecution()); ASSERT_TRUE(ConfigurationManager::unlockExecution()); } #ifdef __APPLE__ G_SUBTEST << "HOME exists but Library/Caches does not exist"; { SelfDeletingTmpFolder tmpFolder; auto homeGuard = TestInstrumentsEnvVarGuard("HOME", tmpFolder.string()); fs::path runtimePath = dirs->runtimeDirPath(); EXPECT_EQ(runtimePath.string(), megacmd::PosixDirectories::noHomeFallbackFolder()); } G_SUBTEST << "HOME exists but Library/Caches is not a directory"; { SelfDeletingTmpFolder tmpFolder; fs::path cachesFile = tmpFolder.path() / "Library" / "Caches"; fs::create_directories(cachesFile.parent_path()); std::ofstream(cachesFile) << "not a directory"; auto homeGuard = TestInstrumentsEnvVarGuard("HOME", tmpFolder.string()); fs::path runtimePath = dirs->runtimeDirPath(); EXPECT_EQ(runtimePath.string(), megacmd::PosixDirectories::noHomeFallbackFolder()); } #endif #endif } TEST(PlatformDirectoriesTest, configDirPath) { using megacmd::ConfigurationManager; using megacmd::PlatformDirectories; auto dirs = PlatformDirectories::getPlatformSpecificDirectories(); ASSERT_TRUE(dirs != nullptr); #ifdef _WIN32 G_SUBTEST << "With $MEGACMD_WORKING_FOLDER_SUFFIX"; { auto guard = TestInstrumentsEnvVarGuard("MEGACMD_WORKING_FOLDER_SUFFIX", "foobar"); EXPECT_THAT(dirs->configDirPath().wstring(), testing::EndsWith(L".megaCmd_foobar")); ConfigurationManager::saveSession("test_session_data"); EXPECT_TRUE(fs::is_regular_file(dirs->configDirPath() / "session")); } G_SUBTEST << "Without $MEGACMD_WORKING_FOLDER_SUFFIX"; { auto guard = TestInstrumentsUnsetEnvVarGuard("MEGACMD_WORKING_FOLDER_SUFFIX"); EXPECT_THAT(dirs->configDirPath().wstring(), testing::EndsWith(L".megaCmd")); ConfigurationManager::saveSession("test_session_data"); EXPECT_TRUE(fs::is_regular_file(dirs->configDirPath() / "session")); } G_SUBTEST << "With non-ascii $MEGACMD_WORKING_FOLDER_SUFFIX"; { auto guard = TestInstrumentsEnvVarGuardW(L"MEGACMD_WORKING_FOLDER_SUFFIX", L"file_\u5f20\u4e09"); EXPECT_THAT(dirs->configDirPath().wstring(), testing::EndsWith(L"file_\u5f20\u4e09")); ConfigurationManager::saveSession("test_session_data"); EXPECT_TRUE(fs::is_regular_file(dirs->configDirPath() / "session")); } G_SUBTEST << "With path length exceeding MAX_PATH"; { // To reach maximum length for the folder path; since len(".megaCmd_")=9, and 9+246=255 // Full path can exceed MAX_PATH=260, but each individual file or folder must have length < 255 constexpr int suffixLength = 246; const std::string suffix = megacmd::generateRandomAlphaNumericString(suffixLength); auto guard = TestInstrumentsEnvVarGuard("MEGACMD_WORKING_FOLDER_SUFFIX", suffix); const fs::path configDir = dirs->configDirPath(); EXPECT_THAT(configDir.string(), testing::EndsWith(suffix)); EXPECT_THAT(configDir.string(), testing::StartsWith(R"(\\?\)")); ConfigurationManager::saveSession("test_session_data"); EXPECT_TRUE(fs::is_regular_file(configDir / "session")); // This would throw an exception without the \\?\ prefix SelfDeletingTmpFolder tmpFolder(configDir); } #else G_SUBTEST << "With alternative existing HOME"; { auto homeGuard = TestInstrumentsEnvVarGuard("HOME", "/tmp"); EXPECT_EQ(dirs->configDirPath().string(), "/tmp/.megaCmd"); ConfigurationManager::saveSession("test_session_data"); EXPECT_TRUE(fs::is_regular_file(dirs->configDirPath() / "session")); } G_SUBTEST << "With alternative existing non-ascii HOME"; { SelfDeletingTmpFolder tmpFolder("file_\u5f20\u4e09"); fs::path configFolder = tmpFolder.path() / ".megaCmd"; auto homeGuard = TestInstrumentsEnvVarGuard("HOME", tmpFolder.string()); EXPECT_EQ(dirs->configDirPath().string(), configFolder.string()); ConfigurationManager::saveSession("test_session_data"); EXPECT_TRUE(fs::is_regular_file(dirs->configDirPath() / "session")); } G_SUBTEST << "With alternative NON existing HOME"; { auto homeGuard = TestInstrumentsEnvVarGuard("HOME", "/non-existent-dir"); EXPECT_EQ(dirs->configDirPath().string(), megacmd::PosixDirectories::noHomeFallbackFolder()); ConfigurationManager::saveSession("test_session_data"); EXPECT_TRUE(fs::is_regular_file(dirs->configDirPath() / "session")); } G_SUBTEST << "HOME exists but is not a directory"; { SelfDeletingTmpFolder tmpFolder; fs::path homeFile = tmpFolder.path() / "home_file"; std::ofstream(homeFile) << "not a directory"; auto homeGuard = TestInstrumentsEnvVarGuard("HOME", homeFile.string()); fs::path configPath = dirs->configDirPath(); EXPECT_EQ(configPath.string(), megacmd::PosixDirectories::noHomeFallbackFolder()); } G_SUBTEST << "Correct 0700 permissions"; { using megacmd::ConfigurationManager; using megacmd::ScopeGuard; const mode_t oldUmask = umask(0); ScopeGuard g([oldUmask] { umask(oldUmask); }); SelfDeletingTmpFolder tmpFolder; const fs::path dirPath = tmpFolder.path() / "some_folder"; ConfigurationManager::createFolderIfNotExisting(dirPath); struct stat info; EXPECT_EQ(stat(dirPath.c_str(), &info), 0); EXPECT_TRUE(S_ISDIR(info.st_mode)); const mode_t actualPerms = info.st_mode & 0777; const mode_t expectedPerms = 0700; EXPECT_EQ(actualPerms, expectedPerms); } G_SUBTEST << "Very long path"; { SelfDeletingTmpFolder tmpFolder(std::string(250, 'a')); auto homeGuard = TestInstrumentsEnvVarGuard("HOME", tmpFolder.string()); fs::path configPath = dirs->configDirPath(); EXPECT_FALSE(configPath.empty()); ConfigurationManager::saveSession("test_session_data"); EXPECT_TRUE(fs::is_regular_file(configPath / "session")); } G_SUBTEST << "Path with special characters"; { #ifdef _WIN32 SelfDeletingTmpFolder tmpFolder("test!@#$%^&()_+-=[]{};',."); #else SelfDeletingTmpFolder tmpFolder("test@#$%^&*()_+-=[]{}|;':\",.<>?"); #endif auto homeGuard = TestInstrumentsEnvVarGuard("HOME", tmpFolder.string()); fs::path configPath = dirs->configDirPath(); EXPECT_FALSE(configPath.empty()); ConfigurationManager::saveSession("test_session_data"); EXPECT_TRUE(fs::is_regular_file(configPath / "session")); } G_SUBTEST << "Path with spaces"; { SelfDeletingTmpFolder tmpFolder("test folder with spaces"); auto homeGuard = TestInstrumentsEnvVarGuard("HOME", tmpFolder.string()); fs::path configPath = dirs->configDirPath(); EXPECT_FALSE(configPath.empty()); ConfigurationManager::saveSession("test_session_data"); EXPECT_TRUE(fs::is_regular_file(configPath / "session")); } #ifndef _WIN32 G_SUBTEST << "Path with newlines and tabs"; { SelfDeletingTmpFolder tmpFolder("test\n\tfolder"); auto homeGuard = TestInstrumentsEnvVarGuard("HOME", tmpFolder.string()); fs::path configPath = dirs->configDirPath(); EXPECT_FALSE(configPath.empty()); ConfigurationManager::saveSession("test_session_data"); EXPECT_TRUE(fs::is_regular_file(configPath / "session")); } #endif #endif } TEST(PlatformDirectoriesTest, lockExecution) { using megacmd::ConfigurationManager; using megacmd::PlatformDirectories; G_SUBTEST << "With default paths"; { ASSERT_TRUE(ConfigurationManager::lockExecution()); ASSERT_TRUE(ConfigurationManager::unlockExecution()); } #ifndef _WIN32 auto dirs = PlatformDirectories::getPlatformSpecificDirectories(); G_SUBTEST << "With legacy one"; { auto legacyLockFolder = ConfigurationManager::getConfigFolder(); if (dirs->runtimeDirPath() != legacyLockFolder) { EXPECT_STRNE(dirs->runtimeDirPath().c_str(), legacyLockFolder.c_str()); ASSERT_TRUE(ConfigurationManager::lockExecution(legacyLockFolder)); ASSERT_FALSE(ConfigurationManager::lockExecution()); ASSERT_TRUE(ConfigurationManager::unlockExecution(legacyLockFolder)); // All good after that ASSERT_TRUE(ConfigurationManager::lockExecution()); ASSERT_TRUE(ConfigurationManager::unlockExecution()); } else { G_TEST_INFO << "LEGACY path is the same as runtime one. All good."; } } G_SUBTEST << "Double lock fails"; { ASSERT_TRUE(ConfigurationManager::lockExecution()); ASSERT_FALSE(ConfigurationManager::lockExecution()); ASSERT_TRUE(ConfigurationManager::unlockExecution()); } G_SUBTEST << "Unlock without lock"; { ConfigurationManager::unlockExecution(); ASSERT_FALSE(ConfigurationManager::unlockExecution()); } G_SUBTEST << "Lock after unlock"; { ASSERT_TRUE(ConfigurationManager::lockExecution()); ASSERT_TRUE(ConfigurationManager::unlockExecution()); ASSERT_TRUE(ConfigurationManager::lockExecution()); ASSERT_TRUE(ConfigurationManager::unlockExecution()); } G_SUBTEST << "Lock with very long path"; { SelfDeletingTmpFolder tmpFolder("this_is_a_very_very_very_lengthy_folder_name_meant_to_test_lock_with_long_path"); #ifdef __APPLE__ fs::create_directories(tmpFolder.path() / "Library" / "Caches"); #endif auto homeGuard = TestInstrumentsEnvVarGuard("HOME", tmpFolder.string()); ASSERT_TRUE(ConfigurationManager::lockExecution()); ASSERT_TRUE(ConfigurationManager::unlockExecution()); } G_SUBTEST << "Lock with special characters in path"; { #ifdef _WIN32 SelfDeletingTmpFolder tmpFolder("test!@#$%^&()_+-=[]{};',."); #else SelfDeletingTmpFolder tmpFolder("test@#$%^&*()_+-=[]{}|;':\",.<>?"); #endif #ifdef __APPLE__ fs::create_directories(tmpFolder.path() / "Library" / "Caches"); #endif auto homeGuard = TestInstrumentsEnvVarGuard("HOME", tmpFolder.string()); ASSERT_TRUE(ConfigurationManager::lockExecution()); ASSERT_TRUE(ConfigurationManager::unlockExecution()); } #endif } #ifndef _WIN32 TEST(PlatformDirectoriesTest, getOrCreateSocketPath) { using megacmd::getOrCreateSocketPath; using megacmd::PlatformDirectories; auto dirs = PlatformDirectories::getPlatformSpecificDirectories(); G_SUBTEST << "With $MEGACMD_SOCKET_NAME (normal or fallback case)"; { auto guard = TestInstrumentsEnvVarGuard("MEGACMD_SOCKET_NAME", "test.sock"); auto socketPath = getOrCreateSocketPath(false); auto expectedNormalFile = (dirs->runtimeDirPath() / "test.sock").string(); // normal case auto expectedFallbackFile = megacmd::PosixDirectories::noHomeFallbackFolder().append("/test.sock"); // too length case EXPECT_THAT(socketPath, testing::AnyOf(expectedNormalFile, expectedFallbackFile)); } G_SUBTEST << "Socket path with very long HOME"; { SelfDeletingTmpFolder tmpFolder(std::string(150, 'x')); #ifdef __APPLE__ fs::create_directories(tmpFolder.path() / "Library" / "Caches"); #endif auto homeGuard = TestInstrumentsEnvVarGuard("HOME", tmpFolder.string()); auto guard = TestInstrumentsEnvVarGuard("MEGACMD_SOCKET_NAME", "test.sock"); auto socketPath = getOrCreateSocketPath(false); EXPECT_FALSE(socketPath.empty()); } G_SUBTEST << "Socket path with special characters in HOME"; { SelfDeletingTmpFolder tmpFolder("test@#$%"); #ifdef __APPLE__ fs::create_directories(tmpFolder.path() / "Library" / "Caches"); #endif auto homeGuard = TestInstrumentsEnvVarGuard("HOME", tmpFolder.string()); auto guard = TestInstrumentsEnvVarGuard("MEGACMD_SOCKET_NAME", "test.sock"); auto socketPath = getOrCreateSocketPath(false); EXPECT_FALSE(socketPath.empty()); } G_SUBTEST << "With non-ascii $MEGACMD_SOCKET_NAME (normal or fallback case)"; { auto guard = TestInstrumentsEnvVarGuard("MEGACMD_SOCKET_NAME", "file_\u5f20\u4e09"); auto socketPath = getOrCreateSocketPath(false); auto expectedNormalFile = (dirs->runtimeDirPath() / "file_\u5f20\u4e09").string(); // normal case auto expectedFallbackFile = megacmd::PosixDirectories::noHomeFallbackFolder().append("/file_\u5f20\u4e09"); // too length case EXPECT_THAT(socketPath, testing::AnyOf(expectedNormalFile, expectedFallbackFile)); } G_SUBTEST << "Without $MEGACMD_SOCKET_NAME (normal or fallback case)"; { auto socketPath = getOrCreateSocketPath(false); auto expectedNormalFile = (dirs->runtimeDirPath() / "megacmd.socket").string(); // normal case auto expectedFallbackFile = megacmd::PosixDirectories::noHomeFallbackFolder().append("/megacmd.socket"); // too length case EXPECT_THAT(socketPath, testing::AnyOf(expectedNormalFile, expectedFallbackFile)); } G_SUBTEST << "Without $MEGACMD_SOCKET_NAME, short path: no /tmp/megacmd-UID fallback"; { auto homeGuard = TestInstrumentsEnvVarGuard("HOME", "/tmp"); auto guard = TestInstrumentsUnsetEnvVarGuard("MEGACMD_SOCKET_NAME"); auto runtimeDir = dirs->runtimeDirPath(); auto socketPath = getOrCreateSocketPath(false); EXPECT_STREQ(socketPath.c_str(), (dirs->runtimeDirPath() / "megacmd.socket").string().c_str()); } G_SUBTEST << "Without $MEGACMD_SOCKET_NAME, longth path: /tmp/megacmd-UID fallback"; { SelfDeletingTmpFolder tmpFolder("this_is_a_very_very_very_lengthy_folder_name_meant_to_make_socket_path_exceed_max_unix_socket_path_allowance"); #ifdef __APPLE__ fs::create_directories(tmpFolder.path() / "Library" / "Caches"); #endif auto homeGuard = TestInstrumentsEnvVarGuard("HOME", tmpFolder.string()); auto guard = TestInstrumentsUnsetEnvVarGuard("MEGACMD_SOCKET_NAME"); auto runtimeDir = dirs->runtimeDirPath(); auto socketPath = getOrCreateSocketPath(false); EXPECT_STRNE(socketPath.c_str(), (dirs->runtimeDirPath() / "megacmd.socket").string().c_str()); EXPECT_STREQ(socketPath.c_str(), megacmd::PosixDirectories::noHomeFallbackFolder().append("/megacmd.socket").c_str()); } G_SUBTEST << "With createDirectory=true, directory created"; { SelfDeletingTmpFolder tmpFolder; auto homeGuard = TestInstrumentsEnvVarGuard("HOME", tmpFolder.string()); auto socketPath = getOrCreateSocketPath(true); EXPECT_FALSE(socketPath.empty()); fs::path socketDir = fs::path(socketPath).parent_path(); EXPECT_TRUE(fs::is_directory(socketDir)); } G_SUBTEST << "With createDirectory=true, directory exists"; { SelfDeletingTmpFolder tmpFolder; auto homeGuard = TestInstrumentsEnvVarGuard("HOME", tmpFolder.string()); auto dirs = PlatformDirectories::getPlatformSpecificDirectories(); fs::path runtimeDir = dirs->runtimeDirPath(); fs::create_directories(runtimeDir); auto socketPath = getOrCreateSocketPath(true); EXPECT_FALSE(socketPath.empty()); EXPECT_TRUE(fs::is_directory(runtimeDir)); } G_SUBTEST << "With createDirectory=true and long path fallback"; { SelfDeletingTmpFolder tmpFolder("this_is_a_very_very_very_lengthy_folder_name_meant_to_make_socket_path_exceed_max_unix_socket_path_allowance"); #ifdef __APPLE__ fs::create_directories(tmpFolder.path() / "Library" / "Caches"); #endif auto homeGuard = TestInstrumentsEnvVarGuard("HOME", tmpFolder.string()); auto socketPath = getOrCreateSocketPath(true); EXPECT_FALSE(socketPath.empty()); fs::path socketDir = fs::path(socketPath).parent_path(); EXPECT_TRUE(fs::is_directory(socketDir)); EXPECT_EQ(socketDir.string(), megacmd::PosixDirectories::noHomeFallbackFolder()); } } #else TEST(PlatformDirectoriesTest, getNamedPipeName) { using megacmd::getNamedPipeName; G_SUBTEST << "With $MEGACMD_PIPE_SUFFIX"; { auto guard = TestInstrumentsEnvVarGuard("MEGACMD_PIPE_SUFFIX", "foobar"); auto name = getNamedPipeName(); EXPECT_THAT(name, testing::EndsWith(L"foobar")); } G_SUBTEST << "Without $MEGACMD_PIPE_SUFFIX"; { auto guard = TestInstrumentsUnsetEnvVarGuard("MEGACMD_PIPE_SUFFIX"); auto name = getNamedPipeName(); EXPECT_THAT(name, testing::Not(testing::EndsWith(L"foobar"))); } G_SUBTEST << "With non-ascii $MEGACMD_PIPE_SUFFIX"; { auto guard = TestInstrumentsEnvVarGuardW(L"MEGACMD_PIPE_SUFFIX", L"file_\u5f20\u4e09"); auto name = getNamedPipeName(); EXPECT_THAT(name, testing::EndsWith(L"file_\u5f20\u4e09")); } } #endif MEGAcmd-2.5.2_Linux/tests/unit/StringUtilsTests.cpp000066400000000000000000001706171516543156300222600ustar00rootroot00000000000000/** * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include #include "Instruments.h" #include "TestUtils.h" #include "comunicationsmanager.h" #include "megacmdcommonutils.h" TEST(StringUtilsTest, rtrim) { using megacmd::rtrim; G_SUBTEST << "Basic case"; { std::string s("123456"); EXPECT_EQ(rtrim(s, '2'), "123456"); EXPECT_EQ(rtrim(s, '6'), "12345"); } G_SUBTEST << "Empty string"; { std::string s; EXPECT_EQ(rtrim(s, ' ').length(), 0); } G_SUBTEST << "Only trim character"; { std::string s(" "); EXPECT_EQ(rtrim(s, ' ').length(), 0); } G_SUBTEST << "Multiple consecutive"; { std::string s("11223344"); EXPECT_EQ(rtrim(s, '4'), "112233"); } G_SUBTEST << "Special case with next character check"; { std::string s("aab"); rtrim(s, 'a'); EXPECT_EQ(s, "aab"); } G_SUBTEST << "Single character at end"; { std::string s("abc123"); EXPECT_EQ(rtrim(s, '3'), "abc12"); } G_SUBTEST << "Unicode characters"; { std::string s("\xD0\x9C\xD0\x95\xD0\x93\xD0\x90 "); EXPECT_TRUE(megacmd::isValidUtf8(s)); std::string result = rtrim(s, ' '); EXPECT_TRUE(megacmd::isValidUtf8(result)); EXPECT_EQ(result, "\xD0\x9C\xD0\x95\xD0\x93\xD0\x90"); } G_SUBTEST << "Single character string (trim character)"; { std::string s("X"); EXPECT_EQ(rtrim(s, 'X').length(), 0); } G_SUBTEST << "Single character string (not trim character)"; { std::string s("X"); EXPECT_EQ(rtrim(s, 'Y'), "X"); } G_SUBTEST << "Special characters"; { std::string s("test\t\t\t"); EXPECT_EQ(rtrim(s, '\t'), "test"); } } TEST(StringUtilsTest, ltrim) { using megacmd::ltrim; G_SUBTEST << "Basic cases"; { std::string s("123456"); EXPECT_EQ(ltrim(s, '2'), "123456"); EXPECT_EQ(ltrim(s, '1'), "23456"); } G_SUBTEST << "Empty string"; { std::string s(""); EXPECT_EQ(ltrim(s, ' '), ""); } G_SUBTEST << "Only trim character"; { std::string s(" "); EXPECT_EQ(ltrim(s, ' '), ""); } G_SUBTEST << "Multiple consecutive"; { std::string s("1111222333"); EXPECT_EQ(ltrim(s, '1'), "222333"); } G_SUBTEST << "Character not at the beginning"; { std::string s("abc123"); EXPECT_EQ(ltrim(s, 'a'), "bc123"); std::string s2("abc123"); EXPECT_EQ(ltrim(s2, 'b'), "abc123"); } G_SUBTEST << "Unicode characters"; { std::string s(" \xD0\x9C\xD0\x95\xD0\x93\xD0\x90"); EXPECT_TRUE(megacmd::isValidUtf8(s)); std::string result = ltrim(s, ' '); EXPECT_TRUE(megacmd::isValidUtf8(result)); EXPECT_EQ(result, "\xD0\x9C\xD0\x95\xD0\x93\xD0\x90"); } G_SUBTEST << "Very long string"; { std::string s(10000, '1'); s += "test"; std::string result = ltrim(s, '1'); EXPECT_EQ(result, "test"); } G_SUBTEST << "Single character string with trim character"; { std::string s("X"); EXPECT_EQ(ltrim(s, 'X'), ""); } G_SUBTEST << "Single character string with no trim characters"; { std::string s("X"); EXPECT_EQ(ltrim(s, 'Y'), "X"); } G_SUBTEST << "Mixed characters at start"; { std::string s("XXYXXtest"); EXPECT_EQ(ltrim(s, 'X'), "YXXtest"); } G_SUBTEST << "Special characters"; { std::string s("\t\t\ttest"); EXPECT_EQ(ltrim(s, '\t'), "test"); } G_SUBTEST << "string_view overload"; { std::string_view sv(" test"); std::string_view result = ltrim(sv, ' '); EXPECT_EQ(result, "test"); EXPECT_EQ(sv, " test"); } G_SUBTEST << "Empty string_view"; { std::string_view sv; std::string_view result = ltrim(sv, ' '); EXPECT_EQ(result.length(), 0); } G_SUBTEST << "string_view with multiple characters"; { std::string_view sv("XXXtest"); std::string_view result = ltrim(sv, 'X'); EXPECT_EQ(result, "test"); } G_SUBTEST << "string_view with only trim characters"; { std::string_view sv(" "); std::string_view result = ltrim(sv, ' '); EXPECT_EQ(result.length(), 0); } G_SUBTEST << "string_view with no trim characters"; { std::string_view sv("test"); std::string_view result = ltrim(sv, ' '); EXPECT_EQ(result, "test"); } G_SUBTEST << "string_view with a single character"; { std::string_view sv("X"); std::string_view result = ltrim(sv, 'X'); EXPECT_EQ(result.length(), 0); } G_SUBTEST << "string_view from string"; { std::string str(" test"); std::string_view sv(str); std::string_view result = ltrim(sv, ' '); EXPECT_EQ(result, "test"); EXPECT_EQ(str, " test"); } } TEST(StringUtilsTest, split) { using megacmd::split; G_SUBTEST << "Basic split"; { std::vector result = split("a,b,c", ","); EXPECT_THAT(result, testing::ElementsAre("a", "b", "c")); } G_SUBTEST << "Empty string"; { std::vector result = split("", ","); EXPECT_TRUE(result.empty()); } G_SUBTEST << "No delimiter found"; { std::vector result = split("abc", ","); EXPECT_THAT(result, testing::ElementsAre("abc")); } G_SUBTEST << "Multiple consecutive delimiters"; { std::vector result = split("a,,b,,c", ","); EXPECT_THAT(result, testing::ElementsAre("a", "b", "c")); } G_SUBTEST << "Delimiter at start"; { std::vector result = split(",a,b", ","); EXPECT_THAT(result, testing::ElementsAre("a", "b")); } G_SUBTEST << "Delimiter at end"; { std::vector result = split("a,b,", ","); EXPECT_THAT(result, testing::ElementsAre("a", "b")); } G_SUBTEST << "Multi-character delimiter"; { std::vector result = split("a::b::c", "::"); EXPECT_THAT(result, testing::ElementsAre("a", "b", "c")); } G_SUBTEST << "Single character"; { std::vector result = split("a", ","); EXPECT_THAT(result, testing::ElementsAre("a")); } } TEST(StringUtilsTest, nonAsciiToStringstream) { const char* char_str = u8"\uc548\uc548\ub155\ud558\uc138\uc694\uc138\uacc4"; const wchar_t* wchar_str = L"\uc548\uc548\ub155\ud558\uc138\uc694\uc138\uacc4"; const std::string str = u8"\uc548\uc548\ub155\ud558\uc138\uc694\uc138\uacc4"; const std::wstring wstr = L"\uc548\uc548\ub155\ud558\uc138\uc694\uc138\uacc4"; { std::ostringstream ostream; ostream << char_str; EXPECT_STREQ(ostream.str().c_str(), char_str); std::wostringstream wostream; wostream << wchar_str; EXPECT_STREQ(wostream.str().c_str(), wchar_str); } { std::ostringstream ostream; ostream << str; EXPECT_EQ(ostream.str(), str); std::wostringstream wostream; wostream << wstr; EXPECT_EQ(wostream.str(), wstr); } #ifdef _WIN32 using namespace megacmd; // otherwise the ostream overload for wstrings won't be visible // we need it to ensure the static_assert is not triggered { std::ostringstream ostream; ostream << utf16ToUtf8(wchar_str); EXPECT_STREQ(ostream.str().c_str(), char_str); std::wostringstream wostream; wostream << char_str; EXPECT_STREQ(wostream.str().c_str(), wchar_str); } { std::ostringstream ostream; ostream << utf16ToUtf8(wstr); EXPECT_EQ(ostream.str(), str); std::wostringstream wostream; wostream << str; EXPECT_EQ(wostream.str(), wstr); } #endif } TEST(StringUtilsTest, redactedCmdPetition) { using megacmd::CmdPetition; { G_SUBTEST << "Redact password"; CmdPetition petition; petition.setLine("some-command --password=MySecretPassword --some-arg=Something -fv"); const std::string redacted = petition.getRedactedLine(); EXPECT_THAT(redacted, testing::Not(testing::HasSubstr("MySecretPassword"))); EXPECT_THAT(redacted, testing::HasSubstr("--password=********")); EXPECT_THAT(redacted, testing::HasSubstr("--some-arg=Something")); } { G_SUBTEST << "Redact password with single quotes"; CmdPetition petition; petition.setLine("some-command --password='My Secret Password' --some-arg=Something -fv"); const std::string redacted = petition.getRedactedLine(); EXPECT_THAT(redacted, testing::Not(testing::HasSubstr("My Secret Password"))); EXPECT_THAT(redacted, testing::HasSubstr("--password=********")); EXPECT_THAT(redacted, testing::HasSubstr("--some-arg=Something")); // Ensure the full password gets redacted, not only the first word EXPECT_THAT(redacted, testing::Not(testing::HasSubstr("Secret Password"))); EXPECT_THAT(redacted, testing::Not(testing::HasSubstr("Password"))); } { G_SUBTEST << "Redact password with double quotes"; CmdPetition petition; petition.setLine("some-command --password=\"My Secret Password\" --some-arg=Something -fv"); const std::string redacted = petition.getRedactedLine(); EXPECT_THAT(redacted, testing::Not(testing::HasSubstr("My Secret Password"))); EXPECT_THAT(redacted, testing::HasSubstr("--password=********")); EXPECT_THAT(redacted, testing::HasSubstr("--some-arg=Something")); // Ensure the full password gets redacted, not only the first word EXPECT_THAT(redacted, testing::Not(testing::HasSubstr("Secret Password"))); EXPECT_THAT(redacted, testing::Not(testing::HasSubstr("Password"))); } { G_SUBTEST << "Redact auth-key and auth-code"; CmdPetition petition; petition.setLine("some-command --auth-code=abc123 --some-arg=Something --auth-key=def456"); const std::string redacted = petition.getRedactedLine(); EXPECT_THAT(redacted, testing::Not(testing::HasSubstr("abc123"))); EXPECT_THAT(redacted, testing::Not(testing::HasSubstr("def456"))); EXPECT_THAT(redacted, testing::HasSubstr("--auth-code=********")); EXPECT_THAT(redacted, testing::HasSubstr("--auth-key=********")); EXPECT_THAT(redacted, testing::HasSubstr("--some-arg=Something")); } { G_SUBTEST << "Redact login"; CmdPetition petition; petition.setLine("login some-email@real-website.com SuperSecret1234!'"); const std::string redacted = petition.getRedactedLine(); EXPECT_EQ(redacted, "login "); } { G_SUBTEST << "Redact login no email"; CmdPetition petition; petition.setLine("login https://mega.nz/folder/bxomFKwL#3V1dUJFzL98t1GqXX29IXg"); const std::string redacted = petition.getRedactedLine(); EXPECT_EQ(redacted, "login "); } { G_SUBTEST << "Redact login even with auth-code"; CmdPetition petition; petition.setLine("login --auth-code=1SomeAuthCode1 SomeSuperSecret"); const std::string redacted = petition.getRedactedLine(); EXPECT_EQ(redacted, "login "); } { G_SUBTEST << "Redact passwd"; CmdPetition petition; petition.setLine("passwd -f NewPassword"); const std::string redacted = petition.getRedactedLine(); EXPECT_EQ(redacted, "passwd "); } { G_SUBTEST << "Redact passwd from shell"; CmdPetition petition; petition.setLine("Xpasswd -f NewPassword"); const std::string redacted = petition.getRedactedLine(); EXPECT_EQ(redacted, "Xpasswd "); } { G_SUBTEST << "Redact confirm"; CmdPetition petition; petition.setLine("confirm somelink1234 Password"); const std::string redacted = petition.getRedactedLine(); EXPECT_EQ(redacted, "confirm "); } { G_SUBTEST << "Redact confirmcancel"; CmdPetition petition; petition.setLine("confirmcancel somelink123 Password"); const std::string redacted = petition.getRedactedLine(); EXPECT_EQ(redacted, "confirmcancel "); } { G_SUBTEST << "Redact folder link"; CmdPetition petition; petition.setLine("import https://mega.nz/folder/bxomFKwL#3V1dUJFzL98t1GqXX29IXg"); const std::string redacted = petition.getRedactedLine(); EXPECT_THAT(redacted, testing::Not(testing::HasSubstr("3V1dUJFzL98t1GqXX29IXg"))); EXPECT_THAT(redacted, testing::HasSubstr("https://mega.nz/folder/bxomFKwL#********")); } { G_SUBTEST << "Redact file link"; CmdPetition petition; petition.setLine("get https://mega.nz/file/d0w0nyiy#egvjqp5r-anbbdsjg8qrvg"); const std::string redacted = petition.getRedactedLine(); EXPECT_THAT(redacted, testing::Not(testing::HasSubstr("egvjqp5r-anbbdsjg8qrvg"))); EXPECT_THAT(redacted, testing::HasSubstr("https://mega.nz/file/d0w0nyiy#********")); } { G_SUBTEST << "Redact old folder link"; CmdPetition petition; petition.setLine("get https://mega.nz/#F!bxomFKwL#3V1dUJFzL98t1GqXX29IXg"); const std::string redacted = petition.getRedactedLine(); EXPECT_THAT(redacted, testing::Not(testing::HasSubstr("3V1dUJFzL98t1GqXX29IXg"))); EXPECT_THAT(redacted, testing::HasSubstr("https://mega.nz/#F!bxomFKwL#********")); } { G_SUBTEST << "Redact old file link"; CmdPetition petition; petition.setLine("import https://mega.nz/#!d0w0nyiy#egvjqp5r-anbbdsjg8qrvg"); const std::string redacted = petition.getRedactedLine(); EXPECT_THAT(redacted, testing::Not(testing::HasSubstr("egvjqp5r-anbbdsjg8qrvg"))); EXPECT_THAT(redacted, testing::HasSubstr("https://mega.nz/#!d0w0nyiy#********")); } { G_SUBTEST << "Redact encrypted link"; CmdPetition petition; petition.setLine("some-other-command https://mega.nz/#P!egvjqp5r-anbbdsjg8qrvg"); const std::string redacted = petition.getRedactedLine(); EXPECT_THAT(redacted, testing::Not(testing::HasSubstr("egvjqp5r-anbbdsjg8qrvg"))); EXPECT_THAT(redacted, testing::HasSubstr("https://mega.nz/#P!********")); } { G_SUBTEST << "DO_NOT_REDACT env variable override"; auto doNotRedactGuard = TestInstrumentsEnvVarGuard("MEGACMD_DO_NOT_REDACT_LINES", "1"); { CmdPetition petition; petition.setLine("some-command --password=MySecretPassword --some-arg=Something -fv"); const std::string redacted = petition.getRedactedLine(); EXPECT_THAT(redacted, testing::HasSubstr("--password=MySecretPassword")); } { CmdPetition petition; petition.setLine("login some-email@real-website.com SuperSecret1234!'"); const std::string redacted = petition.getRedactedLine(); EXPECT_THAT(redacted, testing::HasSubstr("some-email@real-website.com")); EXPECT_THAT(redacted, testing::HasSubstr("SuperSecret1234!'")); EXPECT_THAT(redacted, testing::Not(testing::HasSubstr(""))); } } } TEST(StringUtilsTest, hasWildCards) { using megacmd::hasWildCards; G_SUBTEST << "Both wildcards"; { std::string str = "file*?.txt"; EXPECT_TRUE(hasWildCards(str)); str = "*?"; EXPECT_TRUE(hasWildCards(str)); } G_SUBTEST << "Asterisk at different positions"; { std::string str1 = "*file.txt"; EXPECT_TRUE(hasWildCards(str1)); std::string str2 = "file*.txt"; EXPECT_TRUE(hasWildCards(str2)); std::string str3 = "file.txt*"; EXPECT_TRUE(hasWildCards(str3)); std::string str4 = "*"; EXPECT_TRUE(hasWildCards(str4)); } G_SUBTEST << "Question mark at different positions"; { std::string str1 = "?file.txt"; EXPECT_TRUE(hasWildCards(str1)); std::string str2 = "file?.txt"; EXPECT_TRUE(hasWildCards(str2)); std::string str3 = "file.txt?"; EXPECT_TRUE(hasWildCards(str3)); std::string str4 = "?"; EXPECT_TRUE(hasWildCards(str4)); } G_SUBTEST << "Multiple wildcards"; { std::string str1 = "file**.*"; EXPECT_TRUE(hasWildCards(str1)); std::string str2 = "file??.txt"; EXPECT_TRUE(hasWildCards(str2)); std::string str3 = "*file*.txt"; EXPECT_TRUE(hasWildCards(str3)); std::string str4 = "?file?.txt"; EXPECT_TRUE(hasWildCards(str4)); } G_SUBTEST << "Special characters without wildcards"; { std::string str1 = "file@.txt"; EXPECT_FALSE(hasWildCards(str1)); std::string str2 = "file#.txt"; EXPECT_FALSE(hasWildCards(str2)); std::string str3 = "file$.txt"; EXPECT_FALSE(hasWildCards(str3)); std::string str4 = "file[].txt"; EXPECT_FALSE(hasWildCards(str4)); } G_SUBTEST << "Unicode characters"; { std::string str = u8"\u043F\u0440\u0438\u0432\u0435\u0442.txt"; EXPECT_FALSE(hasWildCards(str)); str = u8"\u043F\u0440\u0438*.txt"; EXPECT_TRUE(hasWildCards(str)); } G_SUBTEST << "Long strings"; { std::string longStr(10000, 'a'); EXPECT_FALSE(hasWildCards(longStr)); longStr[5000] = '*'; EXPECT_TRUE(hasWildCards(longStr)); longStr[5000] = 'a'; longStr[5000] = '?'; EXPECT_TRUE(hasWildCards(longStr)); } G_SUBTEST << "Wildcards with spaces"; { std::string str1 = "file *.txt"; EXPECT_TRUE(hasWildCards(str1)); std::string str2 = "file ?.txt"; EXPECT_TRUE(hasWildCards(str2)); std::string str3 = "file .txt"; EXPECT_FALSE(hasWildCards(str3)); } G_SUBTEST << "Numbers and wildcards"; { std::string str1 = "file123*.txt"; EXPECT_TRUE(hasWildCards(str1)); std::string str2 = "file?123.txt"; EXPECT_TRUE(hasWildCards(str2)); std::string str3 = "file123.txt"; EXPECT_FALSE(hasWildCards(str3)); } G_SUBTEST << "No wildcards"; { std::string str = "file.txt"; EXPECT_FALSE(hasWildCards(str)); } G_SUBTEST << "Empty string"; { std::string str; EXPECT_FALSE(hasWildCards(str)); } } TEST(StringUtilsTest, isValidEmail) { using megacmd::isValidEmail; G_SUBTEST << "Valid emails"; { EXPECT_TRUE(isValidEmail("t@x.be")); EXPECT_TRUE(isValidEmail("test@example.com")); EXPECT_TRUE(isValidEmail("user.name@domain.co.uk")); EXPECT_TRUE(isValidEmail("user+tag@example.com")); EXPECT_TRUE(isValidEmail("user_x@example.dot.com")); } G_SUBTEST << "Invalid emails"; { EXPECT_FALSE(isValidEmail("notanemail")); EXPECT_FALSE(isValidEmail("@example.com")); EXPECT_FALSE(isValidEmail("user@")); EXPECT_FALSE(isValidEmail("user@.")); EXPECT_FALSE(isValidEmail("user@.com")); EXPECT_FALSE(isValidEmail("user@com.")); EXPECT_FALSE(isValidEmail(" ")); } G_SUBTEST << "Empty email"; { EXPECT_FALSE(isValidEmail("")); } } TEST(StringUtilsTest, removeTrailingSeparators) { using megacmd::removeTrailingSeparators; G_SUBTEST << "Empty string"; { std::string path; std::string result = removeTrailingSeparators(path); EXPECT_EQ(result, ""); } G_SUBTEST << "Root path"; { std::string path = "/"; std::string result = removeTrailingSeparators(path); EXPECT_EQ(result, "/"); } G_SUBTEST << "Two slashes only"; { std::string path = "//"; std::string result = removeTrailingSeparators(path); EXPECT_EQ(result, "/"); } G_SUBTEST << "Windows backslash only"; { std::string path = R"(\)"; std::string result = removeTrailingSeparators(path); EXPECT_EQ(result, ""); } G_SUBTEST << "Path with only backslashes"; { std::string path = R"(\\)"; std::string result = removeTrailingSeparators(path); EXPECT_EQ(result, ""); } G_SUBTEST << "Unix path without trailing separator"; { std::string path = "/path/to/dir"; std::string result = removeTrailingSeparators(path); EXPECT_EQ(result, "/path/to/dir"); } G_SUBTEST << "Unix path with trailing separator"; { std::string path = "/path/to/dir/"; std::string result = removeTrailingSeparators(path); EXPECT_EQ(result, "/path/to/dir"); } G_SUBTEST << "Unix path with multiple trailing separators"; { std::string path = "/path/to/dir///"; std::string result = removeTrailingSeparators(path); EXPECT_EQ(result, "/path/to/dir"); } G_SUBTEST << "Unix path with file"; { std::string path = "/path/to/dir/file.txt"; std::string result = removeTrailingSeparators(path); EXPECT_EQ(result, "/path/to/dir/file.txt"); } G_SUBTEST << "Windows path without trailing separator"; { std::string path = R"(C:\path\to\dir)"; std::string result = removeTrailingSeparators(path); EXPECT_EQ(result, R"(C:\path\to\dir)"); } G_SUBTEST << "Windows path with trailing backslash"; { std::string path = R"(C:\path\to\dir\)"; std::string result = removeTrailingSeparators(path); EXPECT_EQ(result, R"(C:\path\to\dir)"); } G_SUBTEST << "Windows path with multiple trailing backslashes"; { std::string path = R"(C:\path\to\dir\\)"; std::string result = removeTrailingSeparators(path); EXPECT_EQ(result, R"(C:\path\to\dir)"); } G_SUBTEST << "Windows root path C:\\\\"; { std::string path = R"(C:\\)"; std::string result = removeTrailingSeparators(path); EXPECT_EQ(result, "C:"); } G_SUBTEST << "Windows root path C:\\"; { std::string path = R"(C:\)"; std::string result = removeTrailingSeparators(path); EXPECT_EQ(result, "C:"); } G_SUBTEST << "Windows path with file"; { std::string path = R"(C:\path\to\dir\file.txt)"; std::string result = removeTrailingSeparators(path); EXPECT_EQ(result, R"(C:\path\to\dir\file.txt)"); } G_SUBTEST << "Windows path ending with file extension"; { std::string path = R"(C:\path\file.txt\)"; std::string result = removeTrailingSeparators(path); EXPECT_EQ(result, R"(C:\path\file.txt)"); } G_SUBTEST << "Mixed trailing separators (not fully removed)"; { { std::string path = R"(/path/to/dir/\)"; std::string result = removeTrailingSeparators(path); EXPECT_EQ(result, "/path/to/dir/"); } { std::string path = R"(C:\path\to\dir/\)"; std::string result = removeTrailingSeparators(path); EXPECT_EQ(result, R"(C:\path\to\dir/)"); } } G_SUBTEST << "Mixed separators in middle and trailing"; { std::string path = R"(C:\path/to\dir/)"; std::string result = removeTrailingSeparators(path); EXPECT_EQ(result, R"(C:\path/to\dir)"); } G_SUBTEST << "Single character path without separator"; { std::string path = "a"; std::string result = removeTrailingSeparators(path); EXPECT_EQ(result, "a"); } G_SUBTEST << "Single character path with forward slash"; { std::string path = "a/"; std::string result = removeTrailingSeparators(path); EXPECT_EQ(result, "a"); } G_SUBTEST << "Single character path with backslash"; { std::string path = R"(a\)"; std::string result = removeTrailingSeparators(path); EXPECT_EQ(result, "a"); } G_SUBTEST << "UNC path with trailing backslash"; { std::string path = R"(\\server\share\)"; std::string result = removeTrailingSeparators(path); EXPECT_EQ(result, R"(\\server\share)"); } G_SUBTEST << "UNC path with multiple trailing backslashes"; { std::string path = R"(\\server\share\\)"; std::string result = removeTrailingSeparators(path); EXPECT_EQ(result, R"(\\server\share)"); } G_SUBTEST << "Relative path with trailing separator"; { std::string path = R"(relative\path\)"; std::string result = removeTrailingSeparators(path); EXPECT_EQ(result, R"(relative\path)"); } G_SUBTEST << "Relative path without trailing separator"; { std::string path = R"(relative\path)"; std::string result = removeTrailingSeparators(path); EXPECT_EQ(result, R"(relative\path)"); } } TEST(StringUtilsTest, toInteger) { using megacmd::toInteger; G_SUBTEST << "Valid positive integer"; { EXPECT_EQ(toInteger("0"), 0); EXPECT_EQ(toInteger("12"), 12); EXPECT_EQ(toInteger("999"), 999); } G_SUBTEST << "Valid negative integer"; { EXPECT_EQ(toInteger("-1", 0), -1); EXPECT_EQ(toInteger("-123"), -123); } G_SUBTEST << "Invalid string"; { EXPECT_EQ(toInteger("abc"), -1); EXPECT_EQ(toInteger("12abc"), -1); EXPECT_EQ(toInteger("abc12"), -1); } G_SUBTEST << "Custom fail value"; { EXPECT_EQ(toInteger("xyz", 0), 0); EXPECT_EQ(toInteger("abc", 999), 999); } G_SUBTEST << "Empty string"; { EXPECT_EQ(toInteger(""), -1); EXPECT_EQ(toInteger("", 42), 42); } G_SUBTEST << "String with spaces"; { EXPECT_EQ(toInteger(" 123 "), -1); EXPECT_EQ(toInteger("123 "), -1); } G_SUBTEST << "Out of range values"; { std::string maxPlusOne = std::to_string(static_cast(INT_MAX) + 1); EXPECT_EQ(toInteger(maxPlusOne), -1); EXPECT_EQ(toInteger(maxPlusOne, 42), 42); std::string minMinusOne = std::to_string(static_cast(INT_MIN) - 1); EXPECT_EQ(toInteger(minMinusOne), -1); EXPECT_EQ(toInteger(minMinusOne, 99), 99); EXPECT_EQ(toInteger("999999999999999999999"), -1); EXPECT_EQ(toInteger("999999999999999999999", 0), 0); EXPECT_EQ(toInteger("-999999999999999999999"), -1); EXPECT_EQ(toInteger("-999999999999999999999", 0), 0); } } TEST(StringUtilsTest, getListOfWords) { using megacmd::getlistOfWords; G_SUBTEST << "Basic case"; { std::vector words = getlistOfWords("mega-share -a --with=some-email@mega.co.nz some_dir/some_file.txt"); EXPECT_THAT(words, testing::ElementsAre("mega-share", "-a", "--with=some-email@mega.co.nz", "some_dir/some_file.txt")); } G_SUBTEST << "Quotes"; { { std::vector words = getlistOfWords(R"(mkdir "some dir" "another dir")"); EXPECT_THAT(words, testing::ElementsAre("mkdir", "some dir", "another dir")); } { std::vector words = getlistOfWords(R"(mkdir 'some dir' 'another dir')"); EXPECT_THAT(words, testing::ElementsAre("mkdir", "some dir", "another dir")); } { std::vector words = getlistOfWords(R"(some "case" here)"); EXPECT_THAT(words, testing::ElementsAre("some", "case", "here")); } { std::vector words = getlistOfWords(R"(some "other case with spaces" here)"); EXPECT_THAT(words, testing::ElementsAre("some", "other case with spaces", "here")); } { std::vector words = getlistOfWords(R"(--another="here")"); EXPECT_THAT(words, testing::ElementsAre(R"(--another="here")")); } { std::vector words = getlistOfWords(R"(--another="here" with extra bits)"); EXPECT_THAT(words, testing::ElementsAre(R"(--another="here")", "with", "extra", "bits")); } { std::vector words = getlistOfWords(R"(--arg1="a b" word --arg2="c d")"); EXPECT_THAT(words, testing::ElementsAre(R"(--arg1="a b")", "word", R"(--arg2="c d")")); } { std::vector words = getlistOfWords(R"(command "text\"quoted")"); EXPECT_THAT(words, testing::ElementsAre("command", R"(text\)", R"(quoted")")); } } G_SUBTEST << "Unmatched quotes"; { { std::vector words = getlistOfWords(R"(find "something with quotes" odd/file"01.txt)"); EXPECT_THAT(words, testing::ElementsAre("find", "something with quotes", R"(odd/file"01.txt)")); } { std::vector words = getlistOfWords(R"(--something="quote missing)"); EXPECT_THAT(words, testing::ElementsAre(R"(--something="quote missing)")); } { std::vector words = getlistOfWords(R"(find 'something with quotes' odd/file'01.txt)"); EXPECT_THAT(words, testing::ElementsAre("find", "something with quotes", R"(odd/file'01.txt)")); } { std::vector words = getlistOfWords(R"(--something='quote missing)"); EXPECT_THAT(words, testing::ElementsAre(R"(--something='quote)", "missing")); } { std::vector words = getlistOfWords(R"(command 'unclosed quote)"); EXPECT_THAT(words, testing::ElementsAre("command", "unclosed quote")); } } G_SUBTEST << "Completion mode and flags"; { { constexpr const char* str = R"(get root_dir\some_dir\some_file.jpg another_file.txt)"; constexpr const char* completion_str = R"(completion get root_dir\some_dir\some_file.jpg another_file.txt)"; std::vector words = getlistOfWords(str, true); EXPECT_THAT(words, testing::ElementsAre("get", R"(root_dir\some_dir\some_file.jpg)", "another_file.txt")); words = getlistOfWords(completion_str, true); EXPECT_THAT(words, testing::ElementsAre("completion", "get", R"(root_dir\\some_dir\\some_file.jpg)", "another_file.txt")); words = getlistOfWords(str, false); EXPECT_THAT(words, testing::ElementsAre("get", R"(root_dir\some_dir\some_file.jpg)", "another_file.txt")); } { std::vector words = getlistOfWords("completion", true); EXPECT_THAT(words, testing::ElementsAre("completion")); } { std::vector words = getlistOfWords("completion ", true, true); EXPECT_THAT(words, testing::ElementsAre("completion")); } { std::vector words = getlistOfWords(R"(notcompletion path\file.txt)", true); EXPECT_THAT(words, testing::ElementsAre("notcompletion", R"(path\file.txt)")); } { constexpr const char* str = R"(completion get path\file.txt )"; std::vector words = getlistOfWords(str, true, false); EXPECT_THAT(words, testing::ElementsAre("completion", "get", R"(path\\file.txt)", "")); } { constexpr const char* str = R"(completion get path\file.txt )"; std::vector words = getlistOfWords(str, true, true); EXPECT_THAT(words, testing::ElementsAre("completion", "get", R"(path\\file.txt)")); } { constexpr const char* str = "completion arg "; std::vector words = getlistOfWords(str, true, true); EXPECT_THAT(words, testing::ElementsAre("completion", "arg")); } { constexpr const char* str = " completion arg "; std::vector words = getlistOfWords(str, true, true); EXPECT_THAT(words, testing::ElementsAre("completion", "arg")); } } G_SUBTEST << "Very long strings"; { { std::string longString = "command "; longString += std::string(10000, 'a'); longString += " argument"; std::vector words = getlistOfWords(longString.c_str()); EXPECT_THAT(words, testing::ElementsAre("command", testing::SizeIs(10000), "argument")); } { std::string longQuotedString = R"(command ")"; longQuotedString += std::string(5000, 'b'); longQuotedString += R"(" argument)"; std::vector words = getlistOfWords(longQuotedString.c_str()); EXPECT_THAT(words, testing::ElementsAre("command", testing::SizeIs(5000), "argument")); } } G_SUBTEST << "Nested quotes"; { { std::vector words = getlistOfWords(R"(command 'text "nested" text')"); EXPECT_THAT(words, testing::ElementsAre("command", R"(text "nested" text)")); } { std::vector words = getlistOfWords(R"(command "text 'nested' text")"); EXPECT_THAT(words, testing::ElementsAre("command", R"(text 'nested' text)")); } { std::vector words = getlistOfWords(R"(cmd1 'outer "inner" outer' cmd2 "outer 'inner' outer")"); EXPECT_THAT(words, testing::ElementsAre("cmd1", R"(outer "inner" outer)", "cmd2", R"(outer 'inner' outer)")); } } G_SUBTEST << "Backslash handling"; { { std::vector words = getlistOfWords("command \"text\\nnewline\" \"text\\ttab\""); EXPECT_THAT(words, testing::ElementsAre("command", "text\\nnewline", "text\\ttab")); } { std::vector words = getlistOfWords("command 'text\\nnewline' 'text\\ttab'"); EXPECT_THAT(words, testing::ElementsAre("command", "text\\nnewline", "text\\ttab")); } { std::vector words = getlistOfWords(R"(command "path\\to\\file")"); EXPECT_THAT(words, testing::ElementsAre("command", R"(path\\to\\file)")); } { std::vector words = getlistOfWords(R"(command "path\\\\to\\\\file")"); EXPECT_THAT(words, testing::ElementsAre("command", R"(path\\\\to\\\\file)")); } { std::vector words = getlistOfWords(R"(completion get path\\\\file.txt)", true); EXPECT_THAT(words, testing::ElementsAre("completion", "get", R"(path\\\\\\\\file.txt)")); } { std::vector words = getlistOfWords(R"(cmd path\\\\\file)"); EXPECT_THAT(words, testing::ElementsAre("cmd", R"(path\\\\\file)")); } { std::vector words = getlistOfWords(R"(command "text\" arg)"); EXPECT_THAT(words, testing::ElementsAre("command", R"(text\)", "arg")); } { std::vector words = getlistOfWords(R"(command 'text\' arg)"); EXPECT_THAT(words, testing::ElementsAre("command", R"(text\)", "arg")); } { std::vector words = getlistOfWords(R"(command path\)"); EXPECT_THAT(words, testing::ElementsAre("command", R"(path\)")); } { std::vector words = getlistOfWords(R"(command path\ with\ spaces)"); EXPECT_THAT(words, testing::ElementsAre("command", R"(path\ with\ spaces)")); } { std::vector words = getlistOfWords(R"(cmd file\ name)"); EXPECT_THAT(words, testing::ElementsAre("cmd", R"(file\ name)")); } } G_SUBTEST << "Special characters"; { { std::vector words = getlistOfWords("command file@name#123$test%value"); EXPECT_THAT(words, testing::ElementsAre("command", "file@name#123$test%value")); } { constexpr const char* unicodeStr = "command \"\xD0\x9C\xD0\x95\xD0\x93\xD0\x90\""; std::vector words = getlistOfWords(unicodeStr); EXPECT_THAT(words, testing::ElementsAre("command", "\xD0\x9C\xD0\x95\xD0\x93\xD0\x90")); } { std::vector words = getlistOfWords("command \"text\1bell\""); EXPECT_THAT(words, testing::ElementsAre("command", "text\1bell")); } { std::vector words = getlistOfWords("cmd\1arg1\2arg2"); EXPECT_THAT(words, testing::ElementsAre("cmd\1arg1\2arg2")); } } G_SUBTEST << "Whitespace and empty strings"; { { std::vector words = getlistOfWords(""); EXPECT_TRUE(words.empty()); } { std::vector words = getlistOfWords(" ", false, true); EXPECT_TRUE(words.empty()); } { std::vector words = getlistOfWords(" ", false, false); EXPECT_THAT(words, testing::ElementsAre("")); } { constexpr const char* str = "export -f --writable some_dir/some_file.txt "; std::vector words = getlistOfWords(str, false, false); EXPECT_THAT(words, testing::ElementsAre("export", "-f", "--writable", "some_dir/some_file.txt", "")); words = getlistOfWords(str, false, true); EXPECT_THAT(words, testing::ElementsAre("export", "-f", "--writable", "some_dir/some_file.txt")); } { std::vector words = getlistOfWords(" command arg", false, false); EXPECT_THAT(words, testing::ElementsAre("command", "arg")); } { std::vector words = getlistOfWords(" command arg", false, true); EXPECT_THAT(words, testing::ElementsAre("command", "arg")); } { std::vector words = getlistOfWords("\t\tcommand\targ", false, true); EXPECT_THAT(words, testing::ElementsAre("command\targ")); } { std::vector words = getlistOfWords("command\targ1\targ2"); EXPECT_THAT(words, testing::ElementsAre("command\targ1\targ2")); } { std::vector words = getlistOfWords(" \t command \t arg \t "); EXPECT_THAT(words, testing::ElementsAre("command", "arg", "")); } { std::vector words = getlistOfWords("command \"text\twith\ttabs\""); EXPECT_THAT(words, testing::ElementsAre("command", "text\twith\ttabs")); } { std::vector words = getlistOfWords("command\narg1\narg2"); EXPECT_THAT(words, testing::ElementsAre("command\narg1\narg2")); } { std::vector words = getlistOfWords(R"(command "" "")"); EXPECT_THAT(words, testing::ElementsAre("command", "", "")); } { std::vector words = getlistOfWords(R"(command " " ' ')"); EXPECT_THAT(words, testing::ElementsAre("command", " ", " ")); } { std::vector words = getlistOfWords("command ", false, false); EXPECT_THAT(words, testing::ElementsAre("command", "")); } } G_SUBTEST << "Unquoted text with embedded quotes"; { { std::vector words = getlistOfWords(R"(command path"with"quote)"); EXPECT_THAT(words, testing::ElementsAre("command", R"(path"with"quote)")); } { std::vector words = getlistOfWords(R"(cmd file"name.txt)"); EXPECT_THAT(words, testing::ElementsAre("cmd", R"(file"name.txt)")); } { std::vector words = getlistOfWords(R"(cmd file'name.txt)"); EXPECT_THAT(words, testing::ElementsAre("cmd", R"(file'name.txt)")); } { std::vector words = getlistOfWords(R"(cmd "quoted"and"more")"); EXPECT_THAT(words, testing::ElementsAre("cmd", "quoted", R"(and"more")")); } } G_SUBTEST << "Invalid UTF-8 sequences"; { { // Invalid UTF-8: continuation byte without start byte constexpr const char* invalidUtf8 = "command \x80\x81\x82"; std::vector words = getlistOfWords(invalidUtf8); EXPECT_THAT(words, testing::ElementsAre("command", "\x80\x81\x82")); } { // Invalid UTF-8: incomplete sequence constexpr const char* invalidUtf8 = "cmd \"\xC2\""; std::vector words = getlistOfWords(invalidUtf8); EXPECT_THAT(words, testing::ElementsAre("cmd", "\xC2")); } { // Invalid UTF-8: overlong encoding constexpr const char* invalidUtf8 = "command \xC0\x80"; std::vector words = getlistOfWords(invalidUtf8); EXPECT_THAT(words, testing::ElementsAre("command", "\xC0\x80")); } } } TEST(StringUtilsTest, replace) { using megacmd::replace; G_SUBTEST << "Basic case"; { std::string str("hello world"); EXPECT_TRUE(replace(str, "world", "mega")); EXPECT_EQ(str, "hello mega"); } G_SUBTEST << "Replace not found"; { std::string str("test string"); EXPECT_FALSE(replace(str, "xyz", "mega")); EXPECT_EQ(str, "test string"); } G_SUBTEST << "Replace empty string"; { std::string str("hello"); EXPECT_TRUE(replace(str, "", "test")); EXPECT_EQ(str, "testhello"); } G_SUBTEST << "Replace with empty string"; { std::string str("remove this"); EXPECT_TRUE(replace(str, "this", "")); EXPECT_EQ(str, "remove "); } G_SUBTEST << "Multiple occurrences with only first replaced"; { std::string str("hello hello"); EXPECT_TRUE(replace(str, "hello", "hi")); EXPECT_EQ(str, "hi hello"); } G_SUBTEST << "Replace at beginning"; { std::string str("start end"); EXPECT_TRUE(replace(str, "start", "begin")); EXPECT_EQ(str, "begin end"); } G_SUBTEST << "Replace at end"; { std::string str("begin end"); EXPECT_TRUE(replace(str, "end", "finish")); EXPECT_EQ(str, "begin finish"); } G_SUBTEST << "Empty input string"; { std::string str; EXPECT_FALSE(replace(str, "test", "mega")); EXPECT_EQ(str, ""); } G_SUBTEST << "Self-replacement"; { std::string str("test"); EXPECT_TRUE(replace(str, "test", "test")); EXPECT_EQ(str, "test"); } G_SUBTEST << "Pattern longer than string"; { std::string str("short"); EXPECT_FALSE(replace(str, "very long pattern", "mega")); EXPECT_EQ(str, "short"); } G_SUBTEST << "Unicode characters"; { std::string str("\xD0\x9C\xD0\x95\xD0\x93\xD0\x90"); EXPECT_TRUE(replace(str, "\xD0\x9C\xD0\x95\xD0\x93\xD0\x90", "MEGA")); EXPECT_EQ(str, "MEGA"); } G_SUBTEST << "Special characters"; { std::string str("test\ttest"); EXPECT_TRUE(replace(str, "\t", " ")); EXPECT_EQ(str, "test test"); } G_SUBTEST << "Pattern is substring of replacement"; { std::string str("a"); EXPECT_TRUE(replace(str, "a", "ab")); EXPECT_EQ(str, "ab"); } G_SUBTEST << "Very long string"; { std::string str(10000, 'a'); str += "test"; EXPECT_TRUE(replace(str, "test", "mega")); EXPECT_EQ(str, std::string(10000, 'a') + "mega"); } G_SUBTEST << "Replace entire string"; { std::string str("full"); EXPECT_TRUE(replace(str, "full", "empty")); EXPECT_EQ(str, "empty"); } G_SUBTEST << "Single character string"; { std::string str("a"); EXPECT_TRUE(replace(str, "a", "b")); EXPECT_EQ(str, "b"); } G_SUBTEST << "Pattern at multiple positions but only first replaced"; { std::string str("abcabcabc"); EXPECT_TRUE(replace(str, "abc", "x")); EXPECT_EQ(str, "xabcabc"); } } TEST(StringUtilsTest, replaceAll) { using megacmd::replaceAll; G_SUBTEST << "Basic replaceAll"; { std::string str("hello world world"); replaceAll(str, "world", "mega"); EXPECT_EQ(str, "hello mega mega"); } G_SUBTEST << "No occurrences"; { std::string str("test string"); replaceAll(str, "xyz", "mega"); EXPECT_EQ(str, "test string"); } G_SUBTEST << "Multiple occurrences"; { std::string str("a b a b a"); replaceAll(str, "a", "x"); EXPECT_EQ(str, "x b x b x"); } G_SUBTEST << "Replace with empty string"; { std::string str("remove this"); replaceAll(str, "this", ""); EXPECT_EQ(str, "remove "); } G_SUBTEST << "Overlapping patterns"; { std::string str("aaa"); replaceAll(str, "aa", "b"); EXPECT_EQ(str, "ba"); replaceAll(str, "a", "b"); EXPECT_EQ(str, "bb"); } G_SUBTEST << "Empty input string"; { std::string str; replaceAll(str, "test", "mega"); EXPECT_EQ(str, ""); } G_SUBTEST << "Empty pattern"; { std::string str("test string"); replaceAll(str, "", "mega"); EXPECT_EQ(str, "test string"); } G_SUBTEST << "Self-replacement"; { std::string str("test test"); replaceAll(str, "test", "test"); EXPECT_EQ(str, "test test"); } G_SUBTEST << "Pattern longer than string"; { std::string str("short"); replaceAll(str, "very long pattern", "mega"); EXPECT_EQ(str, "short"); } G_SUBTEST << "Unicode characters"; { std::string str("\xD0\x9C\xD0\x95\xD0\x93\xD0\x90 \xD0\x9C\xD0\x95\xD0\x93\xD0\x90"); replaceAll(str, "\xD0\x9C\xD0\x95\xD0\x93\xD0\x90", "MEGA"); EXPECT_EQ(str, "MEGA MEGA"); } G_SUBTEST << "Special characters"; { std::string str("test\ttest\ntest"); replaceAll(str, "\t", " "); EXPECT_EQ(str, "test test\ntest"); replaceAll(str, "\n", " "); EXPECT_EQ(str, "test test test"); } G_SUBTEST << "Adjacent occurrences"; { std::string str("abcabc"); replaceAll(str, "ab", "x"); EXPECT_EQ(str, "xcxc"); } G_SUBTEST << "Pattern at beginning and end"; { std::string str("test content test"); replaceAll(str, "test", "start"); EXPECT_EQ(str, "start content start"); } G_SUBTEST << "Pattern is substring of replacement"; { std::string str("a a a"); replaceAll(str, "a", "ab"); EXPECT_EQ(str, "ab ab ab"); } G_SUBTEST << "Very long string"; { std::string str(10000, 'a'); str += "test" + std::string(10000, 'a'); replaceAll(str, "test", "mega"); EXPECT_EQ(str, std::string(10000, 'a') + "mega" + std::string(10000, 'a')); } G_SUBTEST << "Replace entire string"; { std::string str("full"); replaceAll(str, "full", "empty"); EXPECT_EQ(str, "empty"); } G_SUBTEST << "Single character string"; { std::string str("a"); replaceAll(str, "a", "b"); EXPECT_EQ(str, "b"); } } TEST(StringUtilsTest, joinStrings) { using megacmd::joinStrings; G_SUBTEST << "Basic join"; { std::vector vec = {"a", "b", "c"}; std::string result = joinStrings(vec); EXPECT_EQ(result, R"("a" "b" "c")"); } G_SUBTEST << "Empty vector"; { std::vector vec; std::string result = joinStrings(vec); EXPECT_EQ(result, ""); } G_SUBTEST << "Single element"; { { std::vector vec = {"a"}; std::string result = joinStrings(vec); EXPECT_EQ(result, R"("a")"); } { std::vector vec = {"a"}; std::string result = joinStrings(vec, " ", false); EXPECT_EQ(result, "a"); } { std::vector vec = {"a"}; std::string result = joinStrings(vec, ",", true); EXPECT_EQ(result, R"("a")"); } { std::vector vec = {"a"}; std::string result = joinStrings(vec, "/", false); EXPECT_EQ(result, "a"); } } G_SUBTEST << "With quotes"; { { std::vector vec = {"a", "b", "c"}; std::string result = joinStrings(vec, "/", true); EXPECT_EQ(result, R"("a"/"b"/"c")"); } { std::vector vec = {"a", "", "c"}; std::string result = joinStrings(vec, ",", true); EXPECT_EQ(result, R"("a","","c")"); } { std::vector vec = {"a", "b", ""}; std::string result = joinStrings(vec, ",", true); EXPECT_EQ(result, R"("a","b","")"); } } G_SUBTEST << "Without quotes"; { { std::vector vec = {"a", "b", "c"}; std::string result = joinStrings(vec, " ", false); EXPECT_EQ(result, "a b c"); } { std::vector vec = {"a", "", "c"}; std::string result = joinStrings(vec, " ", false); EXPECT_EQ(result, "a c"); } { std::vector vec = {"", "a", "b"}; std::string result = joinStrings(vec, " ", false); EXPECT_EQ(result, " a b"); } { std::vector vec = {"a", "b", ""}; std::string result = joinStrings(vec, " ", false); EXPECT_EQ(result, "a b "); } } G_SUBTEST << "Strings containing quotes"; { { std::vector vec = {R"(a"b)", R"(c"d)", "e"}; std::string result = joinStrings(vec, " ", true); EXPECT_EQ(result, R"("a"b" "c"d" "e")"); } { std::vector vec = {R"("quoted")", "normal", R"("another")"}; std::string result = joinStrings(vec, ",", true); EXPECT_EQ(result, R"(""quoted"","normal",""another"")"); } } G_SUBTEST << "Strings containing delimiter"; { { std::vector vec = {"a,b", "c,d", "e"}; std::string result = joinStrings(vec, ",", false); EXPECT_EQ(result, "a,b,c,d,e"); } { std::vector vec = {"a/b", "c/d", "e"}; std::string result = joinStrings(vec, "/", true); EXPECT_EQ(result, R"("a/b"/"c/d"/"e")"); } { std::vector vec = {"a::b", "c::d", "e"}; std::string result = joinStrings(vec, "::", false); EXPECT_EQ(result, "a::b::c::d::e"); } } G_SUBTEST << "Special characters"; { { std::vector vec = {"a\nb", "c\td", "e"}; std::string result = joinStrings(vec, " ", true); EXPECT_EQ(result, "\"a\nb\" \"c\td\" \"e\""); } { std::vector vec = {"a\r\nb", "c", "d"}; std::string result = joinStrings(vec, ",", false); EXPECT_EQ(result, "a\r\nb,c,d"); } } G_SUBTEST << "Unicode characters"; { { std::vector vec = {u8"\u043F\u0440\u0438\u0432\u0435\u0442", u8"\u043C\u0438\u0440", u8"\u0442\u0435\u0441\u0442"}; std::string result = joinStrings(vec, " ", true); EXPECT_TRUE(megacmd::isValidUtf8(result)); std::string expected = u8"\"\u043F\u0440\u0438\u0432\u0435\u0442\" \"\u043C\u0438\u0440\" \"\u0442\u0435\u0441\u0442\""; EXPECT_EQ(result, expected); } { std::vector vec = {u8"\u4F60\u597D", u8"\u4E16\u754C", u8"\u6D4B\u8BD5"}; std::string result = joinStrings(vec, ",", true); EXPECT_TRUE(megacmd::isValidUtf8(result)); std::string expected = u8"\"\u4F60\u597D\",\"\u4E16\u754C\",\"\u6D4B\u8BD5\""; EXPECT_EQ(result, expected); } } G_SUBTEST << "Empty delimiter"; { { std::vector vec = {"a", "b", "c"}; std::string result = joinStrings(vec, "", true); EXPECT_EQ(result, R"("a""b""c")"); } { std::vector vec = {"a", "b", "c"}; std::string result = joinStrings(vec, "", false); EXPECT_EQ(result, "abc"); } } G_SUBTEST << "Long delimiter"; { { std::vector vec = {"a", "b", "c"}; std::string result = joinStrings(vec, "---", true); EXPECT_EQ(result, R"("a"---"b"---"c")"); } { std::vector vec = {"a", "b", "c"}; std::string longDelim = std::string(100, '-'); std::string result = joinStrings(vec, longDelim.c_str(), false); EXPECT_EQ(result, "a" + longDelim + "b" + longDelim + "c"); } } G_SUBTEST << "Multiple consecutive empty strings"; { { std::vector vec = {""}; std::string result = joinStrings(vec, ",", true); EXPECT_EQ(result, R"("")"); } { std::vector vec = {""}; std::string result = joinStrings(vec, " ", false); EXPECT_EQ(result, ""); } { std::vector vec = {"", "", ""}; std::string result = joinStrings(vec, ",", true); EXPECT_EQ(result, R"("","","")"); } { std::vector vec = {"", "", ""}; std::string result = joinStrings(vec, " ", false); EXPECT_EQ(result, " "); } { std::vector vec = {"", "a", "b"}; std::string result = joinStrings(vec, ",", true); EXPECT_EQ(result, R"("","a","b")"); } { std::vector vec = {"a", "", "", "b"}; std::string result = joinStrings(vec, ",", true); EXPECT_EQ(result, R"("a","","","b")"); } } G_SUBTEST << "Strings with spaces and space delimiter"; { { std::vector vec = {" a ", " b ", " c "}; std::string result = joinStrings(vec, " ", true); EXPECT_EQ(result, R"(" a " " b " " c ")"); } { std::vector vec = {" a ", " b ", " c "}; std::string result = joinStrings(vec, " ", false); EXPECT_EQ(result, " a b c "); } } G_SUBTEST << "Long strings"; { std::string longStr(10000, 'x'); std::vector vec = {longStr, "b", longStr}; std::string result = joinStrings(vec, " ", true); EXPECT_EQ(result.length(), (longStr.length() + 2) * 2 + /* "b" */ 3 + /* 2 separators */ 2); EXPECT_EQ(result.substr(0, 1), R"(")"); EXPECT_EQ(result.substr(1, longStr.length()), longStr); EXPECT_EQ(result.substr(longStr.length() + 1, 2), R"(" )"); EXPECT_EQ(result.substr(longStr.length() + 3, 3), R"("b")"); } } TEST(StringUtilsTest, startsWith) { using megacmd::startsWith; G_SUBTEST << "Basic case"; { EXPECT_TRUE(startsWith("test string", "test")); EXPECT_FALSE(startsWith("test string", "string")); } G_SUBTEST << "Empty prefix"; { EXPECT_TRUE(startsWith("hello", "")); EXPECT_TRUE(startsWith("", "")); } G_SUBTEST << "Prefix longer than string"; { EXPECT_FALSE(startsWith("hi", "hello")); } G_SUBTEST << "Exact match"; { EXPECT_TRUE(startsWith("hello", "hello")); } G_SUBTEST << "Single character prefix"; { EXPECT_TRUE(startsWith("hello", "h")); EXPECT_FALSE(startsWith("hello", "e")); } G_SUBTEST << "Empty string"; { EXPECT_FALSE(startsWith("", "a")); } G_SUBTEST << "Prefix in middle of string"; { EXPECT_FALSE(startsWith("atest", "test")); EXPECT_FALSE(startsWith("prefix suffix", "fix")); } G_SUBTEST << "Case sensitivity"; { EXPECT_FALSE(startsWith("Hello", "hello")); EXPECT_FALSE(startsWith("hello", "Hello")); } G_SUBTEST << "Prefix differs by one character"; { EXPECT_FALSE(startsWith("hello", "hellp")); } G_SUBTEST << "Newline and tab in string"; { EXPECT_TRUE(startsWith("hello\nworld", "hello")); EXPECT_TRUE(startsWith("hello\tworld", "hello")); EXPECT_FALSE(startsWith("hello\nworld", "world")); EXPECT_FALSE(startsWith("hello\r\nworld", "world")); } G_SUBTEST << "Unicode characters"; { EXPECT_TRUE(startsWith(u8"\u043F\u0440\u0438\u0432\u0435\u0442", u8"\u043F\u0440\u0438")); EXPECT_FALSE(startsWith(u8"hello\u043F\u0440\u0438\u0432\u0435\u0442", u8"\u043F\u0440\u0438\u0432\u0435\u0442")); EXPECT_FALSE(startsWith(u8"test\u4F60\u597D", u8"\u4F60\u597D")); EXPECT_FALSE(startsWith("hello", u8"\u043F\u0440\u0438\u0432\u0435\u0442")); } G_SUBTEST << "Strings with spaces"; { EXPECT_FALSE(startsWith(" hello", "hello")); EXPECT_FALSE(startsWith("hello ", " ")); } G_SUBTEST << "Embedded null (string_view is byte-oriented)"; { std::string withNull("a\0b", 3); std::string prefixA("a", 1); std::string prefixFull("a\0b", 3); EXPECT_TRUE(startsWith(withNull, prefixA)); EXPECT_TRUE(startsWith(withNull, prefixFull)); EXPECT_FALSE(startsWith(withNull, std::string("b", 1))); } G_SUBTEST << "Long string (prefix longer, first char differs)"; { std::string longStr(10000, 'a'); std::string prefix = "b" + std::string(9999, 'a'); EXPECT_FALSE(startsWith(longStr, prefix)); } } TEST(StringUtilsTest, toLower) { using megacmd::toLower; G_SUBTEST << "Basic case"; { EXPECT_EQ(toLower("HELLO"), "hello"); EXPECT_EQ(toLower("Hello"), "hello"); EXPECT_EQ(toLower("hElLo"), "hello"); } G_SUBTEST << "Already lowercase"; { EXPECT_EQ(toLower("hello"), "hello"); EXPECT_EQ(toLower("hello world"), "hello world"); } G_SUBTEST << "Empty string"; { EXPECT_EQ(toLower(""), ""); } G_SUBTEST << "Special characters"; { EXPECT_EQ(toLower("HELLO@WORLD"), "hello@world"); EXPECT_EQ(toLower("TEST#123$"), "test#123$"); EXPECT_EQ(toLower("A-B_C.D"), "a-b_c.d"); EXPECT_EQ(toLower("@#$%"), "@#$%"); } G_SUBTEST << "Strings with spaces"; { EXPECT_EQ(toLower("HELLo WORLD"), "hello world"); EXPECT_EQ(toLower(" TEST "), " test "); EXPECT_EQ(toLower("A B C"), "a b c"); } G_SUBTEST << "Numbers only"; { EXPECT_EQ(toLower("123456"), "123456"); EXPECT_EQ(toLower("0"), "0"); } G_SUBTEST << "Newlines, tabs, and control characters"; { EXPECT_EQ(toLower("HELLO\nWORLD"), "hello\nworld"); EXPECT_EQ(toLower("HELLO\tWORLD"), "hello\tworld"); EXPECT_EQ(toLower("HELLO\r\nWORLD"), "hello\r\nworld"); EXPECT_EQ(toLower("\n\t\r"), "\n\t\r"); } G_SUBTEST << "Whitespace-only"; { EXPECT_EQ(toLower(" "), " "); EXPECT_EQ(toLower(" "), " "); EXPECT_EQ(toLower("\t\t"), "\t\t"); EXPECT_EQ(toLower(" \t \n "), " \t \n "); } G_SUBTEST << "Long strings"; { std::string longStr(10000, 'A'); std::string result = toLower(longStr); EXPECT_EQ(result.length(), 10000); EXPECT_EQ(result, std::string(10000, 'a')); } G_SUBTEST << "Single character"; { EXPECT_EQ(toLower("A"), "a"); EXPECT_EQ(toLower("Z"), "z"); EXPECT_EQ(toLower("a"), "a"); EXPECT_EQ(toLower("1"), "1"); } G_SUBTEST << "Original string is not modified"; { const std::string original = "HELLO"; std::string result = toLower(original); EXPECT_EQ(result, "hello"); EXPECT_EQ(original, "HELLO"); } } MEGAcmd-2.5.2_Linux/tests/unit/Utf8Tests.cpp000066400000000000000000001031461516543156300206100ustar00rootroot00000000000000/** * (c) 2026 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include #include "Instruments.h" #include "TestUtils.h" #include "megacmd_utf8.h" #include #include #include namespace fs = std::filesystem; class Utf8Test : public ::testing::Test { protected: void SetUp() override { unsetEnvGuard = std::make_unique("MEGACMD_DISABLE_UTF8_VALIDATIONS"); } void TearDown() override { unsetEnvGuard.reset(); } std::unique_ptr unsetEnvGuard; }; TEST_F(Utf8Test, ValidAscii) { using megacmd::isValidUtf8; G_SUBTEST << "Empty string"; { EXPECT_TRUE(isValidUtf8("")); EXPECT_TRUE(isValidUtf8(std::string())); } G_SUBTEST << "Single ASCII characters"; { EXPECT_TRUE(isValidUtf8("a")); EXPECT_TRUE(isValidUtf8("Z")); EXPECT_TRUE(isValidUtf8("0")); EXPECT_TRUE(isValidUtf8(" ")); EXPECT_TRUE(isValidUtf8("\t")); EXPECT_TRUE(isValidUtf8("\n")); EXPECT_TRUE(isValidUtf8("\r")); } G_SUBTEST << "ASCII strings"; { EXPECT_TRUE(isValidUtf8("Hello, World!")); EXPECT_TRUE(isValidUtf8("The quick brown fox jumps over the lazy dog")); EXPECT_TRUE(isValidUtf8("1234567890")); EXPECT_TRUE(isValidUtf8("!@#$%^&*()_+-=[]{}|;':\",./<>?")); } G_SUBTEST << "ASCII control characters"; { EXPECT_TRUE(isValidUtf8("\x00", 1)); EXPECT_TRUE(isValidUtf8("\x01")); EXPECT_TRUE(isValidUtf8("\x1f")); EXPECT_TRUE(isValidUtf8("\x7f")); } G_SUBTEST << "Long ASCII string"; { std::string longStr(10000, 'a'); EXPECT_TRUE(isValidUtf8(longStr)); } } TEST_F(Utf8Test, Valid2ByteSequences) { using megacmd::isValidUtf8; G_SUBTEST << "Minimum 2-byte character (U+0080)"; { EXPECT_TRUE(isValidUtf8("\xc2\x80")); } G_SUBTEST << "Maximum 2-byte character (U+07FF)"; { EXPECT_TRUE(isValidUtf8("\xdf\xbf")); } G_SUBTEST << "Latin extended characters"; { // ñ (U+00F1) EXPECT_TRUE(isValidUtf8("\xc3\xb1")); // ü (U+00FC) EXPECT_TRUE(isValidUtf8("\xc3\xbc")); // é (U+00E9) EXPECT_TRUE(isValidUtf8("\xc3\xa9")); // ö (U+00F6) EXPECT_TRUE(isValidUtf8("\xc3\xb6")); } G_SUBTEST << "Cyrillic characters"; { // МЕГА EXPECT_TRUE(isValidUtf8("\xD0\x9C\xD0\x95\xD0\x93\xD0\x90")); // Привіт EXPECT_TRUE(isValidUtf8("\xD0\x9F\xD1\x80\xD0\xB8\xD0\xB2\xD1\x96\xD1\x82")); } G_SUBTEST << "Greek characters"; { // Ω (U+03A9) EXPECT_TRUE(isValidUtf8("\xCE\xA9")); // α (U+03B1) EXPECT_TRUE(isValidUtf8("\xCE\xB1")); // β (U+03B2) EXPECT_TRUE(isValidUtf8("\xCE\xB2")); } G_SUBTEST << "Hebrew characters"; { // א (U+05D0) EXPECT_TRUE(isValidUtf8("\xD7\x90")); // ב (U+05D1) EXPECT_TRUE(isValidUtf8("\xD7\x91")); } G_SUBTEST << "Arabic characters"; { // ا (U+0627) EXPECT_TRUE(isValidUtf8("\xD8\xA7")); // ب (U+0628) EXPECT_TRUE(isValidUtf8("\xD8\xA8")); } } TEST_F(Utf8Test, Valid3ByteSequences) { using megacmd::isValidUtf8; G_SUBTEST << "Minimum 3-byte character (U+0800)"; { EXPECT_TRUE(isValidUtf8("\xe0\xa0\x80")); } G_SUBTEST << "Maximum valid 3-byte before surrogates (U+D7FF)"; { EXPECT_TRUE(isValidUtf8("\xed\x9f\xbf")); } G_SUBTEST << "Minimum valid 3-byte after surrogates (U+E000)"; { EXPECT_TRUE(isValidUtf8("\xee\x80\x80")); } G_SUBTEST << "Maximum 3-byte character (U+FFFF)"; { // Note: U+FFFF is valid UTF-8 but is a "noncharacter" EXPECT_TRUE(isValidUtf8("\xef\xbf\xbf")); } G_SUBTEST << "Euro sign (U+20AC)"; { EXPECT_TRUE(isValidUtf8("\xe2\x82\xac")); } G_SUBTEST << "Cent sign (U+20A1)"; { EXPECT_TRUE(isValidUtf8("\xe2\x82\xa1")); } G_SUBTEST << "Japanese Hiragana"; { // あ (U+3042) EXPECT_TRUE(isValidUtf8("\xe3\x81\x82")); // い (U+3044) EXPECT_TRUE(isValidUtf8("\xe3\x81\x84")); // う (U+3046) EXPECT_TRUE(isValidUtf8("\xe3\x81\x86")); } G_SUBTEST << "Japanese Katakana"; { // ア (U+30A2) EXPECT_TRUE(isValidUtf8("\xe3\x82\xa2")); // イ (U+30A4) EXPECT_TRUE(isValidUtf8("\xe3\x82\xa4")); } G_SUBTEST << "Chinese characters"; { // 中 (U+4E2D) EXPECT_TRUE(isValidUtf8("\xe4\xb8\xad")); // 文 (U+6587) EXPECT_TRUE(isValidUtf8("\xe6\x96\x87")); // 字 (U+5B57) EXPECT_TRUE(isValidUtf8("\xe5\xad\x97")); } G_SUBTEST << "Korean characters"; { // 안 (U+C548) EXPECT_TRUE(isValidUtf8("\xec\x95\x88")); // 녕 (U+B155) EXPECT_TRUE(isValidUtf8("\xeb\x85\x95")); // 하 (U+D558) EXPECT_TRUE(isValidUtf8("\xed\x95\x98")); } G_SUBTEST << "Thai characters"; { // ก (U+0E01) EXPECT_TRUE(isValidUtf8("\xe0\xb8\x81")); // ข (U+0E02) EXPECT_TRUE(isValidUtf8("\xe0\xb8\x82")); } G_SUBTEST << "BOM (U+FEFF)"; { EXPECT_TRUE(isValidUtf8("\xef\xbb\xbf")); } G_SUBTEST << "Replacement character (U+FFFD)"; { EXPECT_TRUE(isValidUtf8("\xef\xbf\xbd")); } } TEST_F(Utf8Test, Valid4ByteSequences) { using megacmd::isValidUtf8; G_SUBTEST << "Minimum 4-byte character (U+10000)"; { EXPECT_TRUE(isValidUtf8("\xf0\x90\x80\x80")); } G_SUBTEST << "Maximum valid 4-byte character (U+10FFFF)"; { EXPECT_TRUE(isValidUtf8("\xf4\x8f\xbf\xbf")); } G_SUBTEST << "Emoji characters"; { // 😀 (U+1F600) EXPECT_TRUE(isValidUtf8("\xf0\x9f\x98\x80")); // 😂 (U+1F602) EXPECT_TRUE(isValidUtf8("\xf0\x9f\x98\x82")); // ❤️ base character (U+2764) EXPECT_TRUE(isValidUtf8("\xe2\x9d\xa4")); // 🎉 (U+1F389) EXPECT_TRUE(isValidUtf8("\xf0\x9f\x8e\x89")); // 🚀 (U+1F680) EXPECT_TRUE(isValidUtf8("\xf0\x9f\x9a\x80")); } G_SUBTEST << "Musical symbols"; { // 𝄞 (U+1D11E) - G clef EXPECT_TRUE(isValidUtf8("\xf0\x9d\x84\x9e")); } G_SUBTEST << "Mathematical symbols"; { // 𝔸 (U+1D538) EXPECT_TRUE(isValidUtf8("\xf0\x9d\x94\xb8")); } G_SUBTEST << "Historic scripts"; { // Linear B syllable (U+10000) EXPECT_TRUE(isValidUtf8("\xf0\x90\x80\x80")); // Old Italic letter (U+1033C) EXPECT_TRUE(isValidUtf8("\xf0\x90\x8c\xbc")); // Egyptian hieroglyph (U+13000) EXPECT_TRUE(isValidUtf8("\xf0\x93\x80\x80")); } G_SUBTEST << "Additional 4-byte characters"; { // Rumi Numeral One (U+1E6E1) EXPECT_TRUE(isValidUtf8("\xf3\xa1\xa1\xa1")); } G_SUBTEST << "Range boundary tests"; { // U+10FFFF - last valid codepoint EXPECT_TRUE(isValidUtf8("\xf4\x8f\xbf\xbf")); // U+100000 EXPECT_TRUE(isValidUtf8("\xf4\x80\x80\x80")); } } TEST_F(Utf8Test, InvalidSequences) { using megacmd::isValidUtf8; G_SUBTEST << "Invalid continuation byte at start"; { // Continuation byte without leading byte EXPECT_FALSE(isValidUtf8("\x80")); EXPECT_FALSE(isValidUtf8("\x80\x80")); EXPECT_FALSE(isValidUtf8("\xa0\xa1")); EXPECT_FALSE(isValidUtf8("\xbf")); } G_SUBTEST << "Invalid 5-byte sequences (not valid UTF-8)"; { // 5-byte sequences starting with 0xf8 EXPECT_FALSE(isValidUtf8("\xf8\xa1\xa1\xa1\xa1")); } G_SUBTEST << "Invalid 6-byte sequences (not valid UTF-8)"; { // 6-byte sequences starting with 0xfc EXPECT_FALSE(isValidUtf8("\xfc\xa1\xa1\xa1\xa1\xa1")); } G_SUBTEST << "Invalid lead bytes"; { // 0xfe and 0xff are never valid EXPECT_FALSE(isValidUtf8("\xfe")); EXPECT_FALSE(isValidUtf8("\xff")); EXPECT_FALSE(isValidUtf8("\xfe\x80\x80\x80")); EXPECT_FALSE(isValidUtf8("\xff\x80\x80\x80")); } } TEST_F(Utf8Test, InvalidContinuationBytes) { using megacmd::isValidUtf8; G_SUBTEST << "2-byte sequence with invalid continuation"; { EXPECT_FALSE(isValidUtf8("\xc3\x28")); // 2nd byte: 00xx xxxx EXPECT_FALSE(isValidUtf8("\xc3\xc0")); // 2nd byte: 11xx xxxx EXPECT_FALSE(isValidUtf8("\xc3\x00")); // 2nd byte: 0000 0000 } G_SUBTEST << "3-byte sequence with invalid continuation"; { EXPECT_FALSE(isValidUtf8("\xe2\x28\xa1")); // 2nd byte: 00xx xxxx EXPECT_FALSE(isValidUtf8("\xe2\x82\x28")); // 3rd byte: 00xx xxxx EXPECT_FALSE(isValidUtf8("\xe0\xa1\xd0")); // 3rd byte: 11xx xxxx EXPECT_FALSE(isValidUtf8("\xe2\xc0\xa1")); // 2nd byte: 11xx xxxx } G_SUBTEST << "4-byte sequence with invalid continuation"; { EXPECT_FALSE(isValidUtf8("\xf0\x28\x8c\xbc")); // 2nd byte: 00xx xxxx EXPECT_FALSE(isValidUtf8("\xf0\x90\x28\xbc")); // 3rd byte: 00xx xxxx EXPECT_FALSE(isValidUtf8("\xf0\x90\x8c\x28")); // 4th byte: 00xx xxxx EXPECT_FALSE(isValidUtf8("\xf0\x28\x8c\x28")); // 2nd & 4th bytes invalid } } TEST_F(Utf8Test, TruncatedSequences) { using megacmd::isValidUtf8; G_SUBTEST << "Truncated 2-byte sequence"; { EXPECT_FALSE(isValidUtf8("\xc2")); EXPECT_FALSE(isValidUtf8("\xdf")); } G_SUBTEST << "Truncated 3-byte sequence"; { EXPECT_FALSE(isValidUtf8("\xe0\xa0")); EXPECT_FALSE(isValidUtf8("\xe2\x82")); EXPECT_FALSE(isValidUtf8("\xe2")); } G_SUBTEST << "Truncated 4-byte sequence"; { EXPECT_FALSE(isValidUtf8("\xf0\x90\x8c")); EXPECT_FALSE(isValidUtf8("\xf0\x90")); EXPECT_FALSE(isValidUtf8("\xf4\x8f\xbf")); EXPECT_FALSE(isValidUtf8("\xf0")); } G_SUBTEST << "Truncated sequence in middle of string"; { std::string str = "Hello "; str += "\xc2"; // Incomplete 2-byte sequence EXPECT_FALSE(isValidUtf8(str)); str = "Test "; str += "\xe2\x82"; // Incomplete 3-byte sequence EXPECT_FALSE(isValidUtf8(str)); } } TEST_F(Utf8Test, OverlongEncodings) { using megacmd::isValidUtf8; G_SUBTEST << "Overlong 2-byte encoding of ASCII"; { // Overlong encoding of '/' (U+002F) as 2 bytes EXPECT_FALSE(isValidUtf8("\xc0\xaf")); // Overlong encoding of NUL (U+0000) as 2 bytes EXPECT_FALSE(isValidUtf8("\xc0\x80")); // Any C0 or C1 lead byte creates overlong encoding EXPECT_FALSE(isValidUtf8("\xc1\xbf")); } G_SUBTEST << "Overlong 3-byte encoding"; { // Overlong encoding of characters < U+0800 EXPECT_FALSE(isValidUtf8("\xe0\x9f\xbf")); // codepoint less than U+0800 EXPECT_FALSE(isValidUtf8("\xe0\x80\x80")); // overlong NUL } G_SUBTEST << "Overlong 4-byte encoding"; { // Overlong encoding of characters < U+10000 EXPECT_FALSE(isValidUtf8("\xf0\x8f\xbf\xbf")); // codepoint less than U+10000 EXPECT_FALSE(isValidUtf8("\xf0\x80\x80\x80")); // overlong NUL } } TEST_F(Utf8Test, SurrogateCodepoints) { using megacmd::isValidUtf8; G_SUBTEST << "High surrogate (U+D800)"; { EXPECT_FALSE(isValidUtf8("\xed\xa0\x80")); } G_SUBTEST << "Low surrogate (U+DFFF)"; { EXPECT_FALSE(isValidUtf8("\xed\xbf\xbf")); } G_SUBTEST << "Middle of surrogate range"; { // U+D800 to U+DFFF are all invalid EXPECT_FALSE(isValidUtf8("\xed\xa0\x80")); // U+D800 EXPECT_FALSE(isValidUtf8("\xed\xad\xbf")); // U+DB7F EXPECT_FALSE(isValidUtf8("\xed\xae\x80")); // U+DB80 EXPECT_FALSE(isValidUtf8("\xed\xaf\xbf")); // U+DBFF EXPECT_FALSE(isValidUtf8("\xed\xb0\x80")); // U+DC00 EXPECT_FALSE(isValidUtf8("\xed\xbf\xbf")); // U+DFFF } } TEST_F(Utf8Test, CodepointsBeyondUnicode) { using megacmd::isValidUtf8; G_SUBTEST << "Codepoints greater than U+10FFFF"; { // U+110000 (just beyond valid range) EXPECT_FALSE(isValidUtf8("\xf4\x90\x80\x80")); // U+13FFFF EXPECT_FALSE(isValidUtf8("\xf4\xbf\xbf\xbf")); // U+1FFFFF (would need all 4-byte space) EXPECT_FALSE(isValidUtf8("\xf7\xbf\xbf\xbf")); } } TEST_F(Utf8Test, MixedValidAndInvalid) { using megacmd::isValidUtf8; G_SUBTEST << "Invalid byte in middle of valid string"; { std::string str = "Hello"; str += "\xff"; // Invalid byte str += "World"; EXPECT_FALSE(isValidUtf8(str)); } G_SUBTEST << "Valid multibyte characters mixed with invalid"; { std::string str = "\xc3\xb1"; // Valid ñ str += "\x80"; // Invalid continuation byte alone EXPECT_FALSE(isValidUtf8(str)); } G_SUBTEST << "Invalid sequence at beginning"; { std::string str = "\xfe\x80\x80\x80\x80\x80Hello World"; EXPECT_FALSE(isValidUtf8(str)); } G_SUBTEST << "Invalid sequence at end"; { std::string str = "Hello World\xfe"; EXPECT_FALSE(isValidUtf8(str)); } } TEST_F(Utf8Test, ComplexValidStrings) { using megacmd::isValidUtf8; G_SUBTEST << "Mixed languages"; { // English + Cyrillic + Chinese + Emoji std::string str = "Hello \xD0\x9C\xD0\x98\xD0\xA0 \xe4\xb8\x96\xe7\x95\x8c \xf0\x9f\x8c\x8d"; EXPECT_TRUE(isValidUtf8(str)); } G_SUBTEST << "All byte-length characters together"; { std::string str; str += "A"; // 1 byte str += "\xc3\xa9"; // 2 bytes (é) str += "\xe4\xb8\xad"; // 3 bytes (中) str += "\xf0\x9f\x98\x80"; // 4 bytes (😀) EXPECT_TRUE(isValidUtf8(str)); } G_SUBTEST << "Long multilingual string"; { std::string str; for (int i = 0; i < 1000; ++i) { str += "A"; str += "\xc3\xb1"; // ñ str += "\xe4\xb8\xad"; // 中 str += "\xf0\x9f\x98\x80"; // 😀 } EXPECT_TRUE(isValidUtf8(str)); } G_SUBTEST << "Real-world file paths"; { EXPECT_TRUE(isValidUtf8("/home/user/Documents/\xD0\x94\xD0\xBE\xD0\xBA\xD1\x83\xD0\xBC\xD0\xB5\xD0\xBD\xD1\x82.txt")); // Ukrainian filename EXPECT_TRUE(isValidUtf8("/Users/\xe7\x94\xa8\xe6\x88\xb7/Desktop/file.txt")); // Chinese username EXPECT_TRUE(isValidUtf8("C:\\Users\\\xc3\x9cser\\Documents\\file.txt")); // German umlaut } G_SUBTEST << "URL-like strings with Unicode"; { EXPECT_TRUE(isValidUtf8("https://example.com/path/\xe4\xb8\xad\xe6\x96\x87")); EXPECT_TRUE(isValidUtf8("mega://\xD0\xBF\xD1\x83\xD1\x82\xD1\x8C/file")); } } TEST_F(Utf8Test, DisableValidationsEnvVar) { using megacmd::isValidUtf8; G_SUBTEST << "Invalid UTF-8 returns true when validations disabled"; { // Temporarily disable validations unsetEnvGuard.reset(); TestInstrumentsEnvVarGuard bypassGuard("MEGACMD_DISABLE_UTF8_VALIDATIONS", "1"); // These would normally fail, but with MEGACMD_DISABLE_UTF8_VALIDATIONS=1 they should pass EXPECT_TRUE(isValidUtf8("\xff")); EXPECT_TRUE(isValidUtf8("\xfe")); EXPECT_TRUE(isValidUtf8("\xc0\x80")); EXPECT_TRUE(isValidUtf8("\xed\xa0\x80")); } G_SUBTEST << "Invalid UTF-8 returns false when validations re-enabled"; { // Guard is destroyed, validations should be re-enabled // Re-initialize unsetEnvGuard to ensure clean state unsetEnvGuard = std::make_unique("MEGACMD_DISABLE_UTF8_VALIDATIONS"); EXPECT_FALSE(isValidUtf8("\xff")); EXPECT_FALSE(isValidUtf8("\xfe")); } } TEST_F(Utf8Test, BoundaryConditions) { using megacmd::isValidUtf8; G_SUBTEST << "Size-based overloads"; { const char data[] = "Hello\x00World"; EXPECT_TRUE(isValidUtf8(data, 11)); EXPECT_TRUE(isValidUtf8("Hello\xff", 5)); EXPECT_FALSE(isValidUtf8("Hello\xff", 6)); } G_SUBTEST << "std::string overload"; { std::string validStr = "\xc3\xb1"; // ñ EXPECT_TRUE(isValidUtf8(validStr)); std::string invalidStr; invalidStr += '\xc3'; // Incomplete sequence EXPECT_FALSE(isValidUtf8(invalidStr)); } G_SUBTEST << "Edge case character counts"; { // Single multibyte character EXPECT_TRUE(isValidUtf8("\xf0\x9f\x98\x80")); // Just emoji // Two adjacent 4-byte chars EXPECT_TRUE(isValidUtf8("\xf0\x9f\x98\x80\xf0\x9f\x98\x82")); } } TEST(PathUtf8Test, BasicPaths) { using megacmd::pathAsUtf8; G_SUBTEST << "ASCII path"; { fs::path p = "/home/user/file.txt"; std::string result = pathAsUtf8(p); #ifdef _WIN32 // On Windows, path might use backslashes EXPECT_TRUE(result.find("home") != std::string::npos); EXPECT_TRUE(result.find("user") != std::string::npos); EXPECT_TRUE(result.find("file.txt") != std::string::npos); #else EXPECT_EQ(result, "/home/user/file.txt"); #endif } G_SUBTEST << "Empty path"; { fs::path p; std::string result = pathAsUtf8(p); EXPECT_TRUE(result.empty()); } G_SUBTEST << "Current directory"; { fs::path p = "."; std::string result = pathAsUtf8(p); EXPECT_EQ(result, "."); } G_SUBTEST << "Parent directory"; { fs::path p = ".."; std::string result = pathAsUtf8(p); EXPECT_EQ(result, ".."); } } TEST(PathUtf8Test, UnicodePathsAllPlatforms) { using megacmd::pathAsUtf8; using megacmd::isValidUtf8; G_SUBTEST << "Cyrillic in path"; { std::string cyrillic = "\xD0\x9C\xD0\x95\xD0\x93\xD0\x90"; // МЕГА fs::path p = fs::u8path(cyrillic); std::string result = pathAsUtf8(p); EXPECT_TRUE(isValidUtf8(result)); // The result should contain the Cyrillic characters // Note: On Unix systems, pathAsUtf8() uses path.string() which returns the path // in the native filesystem encoding (depends on locale). On modern systems with // UTF-8 locale, Cyrillic characters are preserved. However, on systems with // non-UTF-8 locales (e.g., old systems with ISO-8859-1, KOI8-R, or misconfigured // locales), the characters may be lost or corrupted. The "MEGA" fallback checks // for potential transliteration by filesystem/OS, though this is rare in practice. // Reference: C++17 std::filesystem::path::string() uses native encoding, // not guaranteed to be UTF-8 on Unix (unlike path.u8string()). EXPECT_TRUE(result.find("\xD0\x9C") != std::string::npos || result.find("MEGA") != std::string::npos || // Fallback for non-UTF-8 locales !result.empty()); } G_SUBTEST << "Chinese characters in path"; { std::string chinese = "\xe4\xb8\xad\xe6\x96\x87"; // 中文 fs::path p = fs::u8path(chinese); std::string result = pathAsUtf8(p); EXPECT_TRUE(isValidUtf8(result)); } G_SUBTEST << "Japanese characters in path"; { std::string japanese = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88"; // テスト fs::path p = fs::u8path(japanese); std::string result = pathAsUtf8(p); EXPECT_TRUE(isValidUtf8(result)); } G_SUBTEST << "Emoji in path"; { std::string emoji = "\xf0\x9f\x93\x81"; // 📁 fs::path p = fs::u8path(emoji); std::string result = pathAsUtf8(p); EXPECT_TRUE(isValidUtf8(result)); } G_SUBTEST << "Mixed scripts in path"; { std::string mixed = "test_\xD0\xA4\xD0\xB0\xD0\xB9\xD0\xBB_\xe4\xb8\xad"; fs::path p = fs::u8path(mixed); std::string result = pathAsUtf8(p); EXPECT_TRUE(isValidUtf8(result)); } } #ifdef _WIN32 TEST(Utf16ConversionTest, Utf8ToUtf16Basic) { using megacmd::utf8StringToUtf16WString; G_SUBTEST << "Empty string"; { std::wstring result = utf8StringToUtf16WString(""); EXPECT_TRUE(result.empty()); } G_SUBTEST << "ASCII string"; { std::wstring result = utf8StringToUtf16WString("Hello World"); EXPECT_EQ(result, L"Hello World"); } G_SUBTEST << "Single ASCII character"; { std::wstring result = utf8StringToUtf16WString("A"); EXPECT_EQ(result, L"A"); } } TEST(Utf16ConversionTest, Utf8ToUtf16Multibyte) { using megacmd::utf8StringToUtf16WString; G_SUBTEST << "2-byte UTF-8 characters"; { // ñ (U+00F1) std::wstring result = utf8StringToUtf16WString("\xc3\xb1"); EXPECT_EQ(result, L"\x00F1"); } G_SUBTEST << "3-byte UTF-8 characters"; { // 中 (U+4E2D) std::wstring result = utf8StringToUtf16WString("\xe4\xb8\xad"); EXPECT_EQ(result, L"\x4E2D"); } G_SUBTEST << "4-byte UTF-8 characters (surrogate pairs in UTF-16)"; { // 😀 (U+1F600) - requires surrogate pair in UTF-16 std::wstring result = utf8StringToUtf16WString("\xf0\x9f\x98\x80"); // UTF-16 surrogate pair: D83D DE00 EXPECT_EQ(result, L"\xD83D\xDE00"); } G_SUBTEST << "Korean characters"; { // 안녕하세요세계 (Korean hello world) const char* utf8 = "\xec\x95\x88\xec\x95\x88\xeb\x85\x95\xed\x95\x98\xec\x84\xb8\xec\x9a\x94\xec\x84\xb8\xea\xb3\x84"; std::wstring result = utf8StringToUtf16WString(utf8); EXPECT_EQ(result, L"\uc548\uc548\ub155\ud558\uc138\uc694\uc138\uacc4"); } G_SUBTEST << "Cyrillic characters"; { // МЕГА std::wstring result = utf8StringToUtf16WString("\xD0\x9C\xD0\x95\xD0\x93\xD0\x90"); EXPECT_EQ(result, L"\x041C\x0415\x0413\x0410"); } } TEST(Utf16ConversionTest, Utf8ToUtf16WithLength) { using megacmd::utf8StringToUtf16WString; G_SUBTEST << "Partial string conversion"; { std::wstring result = utf8StringToUtf16WString("Hello World", 5); EXPECT_EQ(result, L"Hello"); } G_SUBTEST << "Zero length"; { std::wstring result = utf8StringToUtf16WString("Hello", 0); EXPECT_TRUE(result.empty()); } G_SUBTEST << "Partial multibyte character (edge case)"; { constexpr const char* utf8 = "\xc3\xb1"; // ñ std::wstring result = utf8StringToUtf16WString(utf8, 2); EXPECT_EQ(result, L"\x00F1"); } } TEST(Utf16ConversionTest, Utf16ToUtf8Basic) { using megacmd::utf16ToUtf8; using megacmd::isValidUtf8; G_SUBTEST << "Empty string"; { std::string result = utf16ToUtf8(L""); EXPECT_TRUE(result.empty()); } G_SUBTEST << "ASCII string"; { std::string result = utf16ToUtf8(L"Hello World"); EXPECT_EQ(result, "Hello World"); EXPECT_TRUE(isValidUtf8(result)); } G_SUBTEST << "Single ASCII character"; { std::string result = utf16ToUtf8(L"A"); EXPECT_EQ(result, "A"); } } TEST(Utf16ConversionTest, Utf16ToUtf8Multibyte) { using megacmd::utf16ToUtf8; using megacmd::isValidUtf8; G_SUBTEST << "BMP characters"; { // ñ (U+00F1) std::string result = utf16ToUtf8(L"\x00F1"); EXPECT_EQ(result, "\xc3\xb1"); EXPECT_TRUE(isValidUtf8(result)); } G_SUBTEST << "CJK characters"; { // 中 (U+4E2D) std::string result = utf16ToUtf8(L"\x4E2D"); EXPECT_EQ(result, "\xe4\xb8\xad"); EXPECT_TRUE(isValidUtf8(result)); } G_SUBTEST << "Surrogate pairs (emoji)"; { // 😀 (U+1F600) as UTF-16 surrogate pair std::wstring ws; ws += (wchar_t)0xD83D; ws += (wchar_t)0xDE00; std::string result = utf16ToUtf8(ws); EXPECT_EQ(result, "\xf0\x9f\x98\x80"); EXPECT_TRUE(isValidUtf8(result)); } G_SUBTEST << "Korean characters"; { std::string result = utf16ToUtf8(L"\uc548\uc548\ub155\ud558\uc138\uc694\uc138\uacc4"); EXPECT_TRUE(isValidUtf8(result)); EXPECT_EQ(result, "\xec\x95\x88\xec\x95\x88\xeb\x85\x95\xed\x95\x98\xec\x84\xb8\xec\x9a\x94\xec\x84\xb8\xea\xb3\x84"); } G_SUBTEST << "Cyrillic characters"; { std::string result = utf16ToUtf8(L"\x041C\x0415\x0413\x0410"); EXPECT_EQ(result, "\xD0\x9C\xD0\x95\xD0\x93\xD0\x90"); EXPECT_TRUE(isValidUtf8(result)); } } TEST(Utf16ConversionTest, Utf16ToUtf8Overloads) { using megacmd::utf16ToUtf8; using megacmd::isValidUtf8; G_SUBTEST << "const wchar_t* overload"; { constexpr const wchar_t* ws = L"Hello"; std::string result = utf16ToUtf8(ws); EXPECT_EQ(result, "Hello"); EXPECT_TRUE(isValidUtf8(result)); } G_SUBTEST << "std::wstring overload"; { std::wstring ws = L"Hello"; std::string result = utf16ToUtf8(ws); EXPECT_EQ(result, "Hello"); EXPECT_TRUE(isValidUtf8(result)); } G_SUBTEST << "Output parameter overload"; { std::string output; megacmd::utf16ToUtf8(L"Test", 4, &output); EXPECT_EQ(output, "Test"); EXPECT_TRUE(isValidUtf8(output)); } } TEST(Utf16ConversionTest, RoundTrip) { using megacmd::utf8StringToUtf16WString; using megacmd::utf16ToUtf8; using megacmd::isValidUtf8; G_SUBTEST << "ASCII round-trip"; { std::string original = "Hello World 123!@#"; std::wstring utf16 = utf8StringToUtf16WString(original.c_str()); std::string back = utf16ToUtf8(utf16); EXPECT_EQ(back, original); EXPECT_TRUE(isValidUtf8(back)); } G_SUBTEST << "Multibyte round-trip"; { std::string original = "\xD0\x9C\xD0\x95\xD0\x93\xD0\x90"; // МЕГА std::wstring utf16 = utf8StringToUtf16WString(original.c_str()); std::string back = utf16ToUtf8(utf16); EXPECT_EQ(back, original); EXPECT_TRUE(isValidUtf8(back)); } G_SUBTEST << "Emoji round-trip"; { std::string original = "\xf0\x9f\x98\x80"; // 😀 std::wstring utf16 = utf8StringToUtf16WString(original.c_str()); std::string back = utf16ToUtf8(utf16); EXPECT_EQ(back, original); EXPECT_TRUE(isValidUtf8(back)); } G_SUBTEST << "Mixed content round-trip"; { std::string original = "Hello \xD0\x9C\xD0\x98\xD0\xA0 \xe4\xb8\x96\xe7\x95\x8c \xf0\x9f\x8c\x8d!"; std::wstring utf16 = utf8StringToUtf16WString(original.c_str()); std::string back = utf16ToUtf8(utf16); EXPECT_EQ(back, original); EXPECT_TRUE(isValidUtf8(back)); } G_SUBTEST << "Long string round-trip"; { std::string original; for (int i = 0; i < 1000; ++i) { original += "Test \xc3\xb1 "; } std::wstring utf16 = utf8StringToUtf16WString(original.c_str()); std::string back = utf16ToUtf8(utf16); EXPECT_EQ(back, original); EXPECT_TRUE(isValidUtf8(back)); } } TEST(Utf16ConversionTest, EdgeCases) { using megacmd::utf8StringToUtf16WString; using megacmd::utf16ToUtf8; G_SUBTEST << "BOM handling (UTF-8 BOM)"; { std::string withBom = "\xef\xbb\xbf" "Hello"; std::wstring utf16 = utf8StringToUtf16WString(withBom.c_str()); // BOM should be converted to UTF-16 BOM (U+FEFF) EXPECT_EQ(utf16[0], 0xFEFF); } G_SUBTEST << "Special Unicode characters"; { // Zero-width joiner (U+200D) std::string zwj = "\xe2\x80\x8d"; std::wstring utf16 = utf8StringToUtf16WString(zwj.c_str()); EXPECT_EQ(utf16, L"\x200D"); } G_SUBTEST << "Arrows and symbols"; { // ▼ (U+25BC) std::wstring input = L"\u25bc"; std::string result = utf16ToUtf8(input); EXPECT_EQ(result, "\xe2\x96\xbc"); } } TEST(StringToLocalWTest, BasicConversion) { using megacmd::stringtolocalw; G_SUBTEST << "Simple ASCII"; { std::wstring result; stringtolocalw("Hello", &result); EXPECT_EQ(result, L"Hello"); } G_SUBTEST << "Empty string"; { std::wstring result; stringtolocalw("", &result); EXPECT_TRUE(result.empty()); } G_SUBTEST << "UTF-8 to wstring"; { std::wstring result; stringtolocalw("\xc3\xb1", &result); // ñ EXPECT_EQ(result, L"\x00F1"); } } TEST(LocalWToStringTest, BasicConversion) { using megacmd::localwtostring; G_SUBTEST << "Simple ASCII"; { std::wstring input = L"Hello"; std::string result; localwtostring(&input, &result); EXPECT_EQ(result, "Hello"); } G_SUBTEST << "Empty string"; { std::wstring input; std::string result; localwtostring(&input, &result); EXPECT_TRUE(result.empty()); } G_SUBTEST << "Wide char to UTF-8"; { std::wstring input = L"\x00F1"; // ñ std::string result; localwtostring(&input, &result); EXPECT_EQ(result, "\xc3\xb1"); } } TEST(NonMaxPathLimitedTest, PathPrefixing) { using megacmd::nonMaxPathLimitedWstring; using megacmd::nonMaxPathLimitedPath; G_SUBTEST << "Regular path gets prefixed"; { fs::path p = "C:\\test\\path"; std::wstring result = nonMaxPathLimitedWstring(p); EXPECT_TRUE(result.find(L"\\\\?\\") == 0); } G_SUBTEST << "Already prefixed path unchanged"; { fs::path p = "\\\\?\\C:\\test\\path"; std::wstring result = nonMaxPathLimitedWstring(p); EXPECT_TRUE(result.find(L"\\\\?\\") == 0); // Should not double-prefix EXPECT_TRUE(result.find(L"\\\\?\\\\\\?\\") == std::wstring::npos); } G_SUBTEST << "Path version"; { fs::path p = "C:\\test\\path"; fs::path result = nonMaxPathLimitedPath(p); EXPECT_TRUE(result.wstring().find(L"\\\\?\\") == 0); } } #endif // _WIN32 TEST_F(Utf8Test, InvalidUtf8Incidences) { using megacmd::isValidUtf8; using megacmd::sInvalidUtf8Incidences; // Note: This test depends on internal state which may be affected by other tests uint64_t initialCount = sInvalidUtf8Incidences.load(); G_SUBTEST << "Counter increments on invalid UTF-8"; { EXPECT_FALSE(isValidUtf8("\xff")); EXPECT_GT(sInvalidUtf8Incidences.load(), initialCount); uint64_t afterFirst = sInvalidUtf8Incidences.load(); EXPECT_FALSE(isValidUtf8("\xfe")); EXPECT_GT(sInvalidUtf8Incidences.load(), afterFirst); } G_SUBTEST << "Counter does not increment on valid UTF-8"; { uint64_t before = sInvalidUtf8Incidences.load(); EXPECT_TRUE(isValidUtf8("Hello World")); EXPECT_TRUE(isValidUtf8("\xc3\xb1")); EXPECT_TRUE(isValidUtf8("\xe4\xb8\xad")); EXPECT_TRUE(isValidUtf8("\xf0\x9f\x98\x80")); EXPECT_EQ(sInvalidUtf8Incidences.load(), before); } } TEST_F(Utf8Test, StressLargeValidStrings) { using megacmd::isValidUtf8; G_SUBTEST << "Very long ASCII string"; { std::string longStr(1000000, 'a'); EXPECT_TRUE(isValidUtf8(longStr)); } G_SUBTEST << "Very long multibyte string"; { std::string longStr; longStr.reserve(1000000 * 4); for (int i = 0; i < 250000; ++i) { longStr += "\xf0\x9f\x98\x80"; // 4-byte emoji } EXPECT_TRUE(isValidUtf8(longStr)); } G_SUBTEST << "Mixed content large string"; { std::string mixedStr; mixedStr.reserve(100000); for (int i = 0; i < 10000; ++i) { mixedStr += "ASCII "; mixedStr += "\xc3\xb1"; // 2-byte mixedStr += " "; mixedStr += "\xe4\xb8\xad"; // 3-byte mixedStr += " "; mixedStr += "\xf0\x9f\x98\x80"; // 4-byte mixedStr += " "; } EXPECT_TRUE(isValidUtf8(mixedStr)); } } TEST_F(Utf8Test, StressManySmallValidations) { using megacmd::isValidUtf8; std::vector testStrings = { "a", "\xc3\xb1", "\xe4\xb8\xad", "\xf0\x9f\x98\x80", "Hello World", "\xD0\x9C\xD0\x95\xD0\x93\xD0\x90", "" }; G_SUBTEST << "Many repeated validations"; { for (int i = 0; i < 100000; ++i) { for (const auto& str : testStrings) { EXPECT_TRUE(isValidUtf8(str)); } } } } #ifdef _WIN32 TEST_F(Utf8Test, StressLargeConversions) { using megacmd::utf8StringToUtf16WString; using megacmd::utf16ToUtf8; using megacmd::isValidUtf8; G_SUBTEST << "Large UTF-8 to UTF-16 conversion"; { std::string largeUtf8; largeUtf8.reserve(100000); for (int i = 0; i < 10000; ++i) { largeUtf8 += "\xD0\x9C\xD0\x95\xD0\x93\xD0\x90 "; } std::wstring utf16 = utf8StringToUtf16WString(largeUtf8.c_str()); EXPECT_GT(utf16.size(), 0); std::string backToUtf8 = utf16ToUtf8(utf16); EXPECT_EQ(backToUtf8, largeUtf8); EXPECT_TRUE(isValidUtf8(backToUtf8)); } G_SUBTEST << "Many small conversions"; { for (int i = 0; i < 10000; ++i) { std::wstring utf16 = utf8StringToUtf16WString("Test \xc3\xb1 string"); std::string utf8 = utf16ToUtf8(utf16); EXPECT_EQ(utf8, "Test \xc3\xb1 string"); } } } #endif MEGAcmd-2.5.2_Linux/tests/unit/UtilsTests.cpp000066400000000000000000000450651516543156300210670ustar00rootroot00000000000000/** * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include #include #include #include #include #include #ifdef _WIN32 #include #include #else #include #include #endif #include #include #include "TestUtils.h" #include "megacmdcommonutils.h" #include "Instruments.h" namespace fs = std::filesystem; // Returns a list of path variants for testing: // 1. Original path // 2. Kernel format (\\?\ prefix) on Windows // 3. UNC format on Windows std::vector pathVariants(const std::string& path) { std::vector variants{path}; #ifdef _WIN32 std::string_view pathView{path}; bool isKernel = pathView.size() >= 4 && pathView.substr(0, 4) == R"(\\?\)"; bool isKernelWithUnc = pathView.size() >= 8 && pathView.substr(0, 8) == R"(\\?\UNC\)"; bool isUnc = pathView.size() >= 2 && pathView.substr(0, 2) == R"(\\)" && !isKernel; if (isKernelWithUnc) { std::string uncPath = R"(\\)" + std::string(pathView.substr(8)); variants.push_back(uncPath); variants.push_back(path); } else if (isUnc) { std::string kernelUncPath = R"(\\?\UNC\)" + std::string(pathView.substr(2)); variants.push_back(kernelUncPath); variants.push_back(path); } else if (isKernel) { std::string localPath = std::string(pathView.substr(4)); variants.push_back(localPath); if (localPath.size() >= 2 && localPath[1] == ':') { std::string uncPath = R"(\\localhost\)" + localPath.substr(0, 1) + R"($\)" + localPath.substr(3); variants.push_back(uncPath); } else { variants.push_back(path); } } else { std::string kernelPath = R"(\\?\)" + path; variants.push_back(kernelPath); if (pathView.size() >= 2 && pathView[1] == ':') { std::string uncPath = R"(\\localhost\)" + path.substr(0, 1) + R"($\)"; if (pathView.size() > 3) { uncPath += pathView.substr(3); } variants.push_back(uncPath); } else { variants.push_back(kernelPath); // duplicate } } #endif return variants; } void testCanWriteAllPathVariants(const std::string& path, bool expectedResult) { using megacmd::canWrite; auto variants = pathVariants(path); #ifdef _WIN32 ASSERT_EQ(variants.size(), 3); #else ASSERT_EQ(variants.size(), 1); #endif for (const auto& variant : variants) { EXPECT_EQ(canWrite(variant), expectedResult) << "Path: " << variant; } } TEST(UtilsTest, nonAsciiConsolePrint) { // No need to check the output, we just want to ensure // the test doesn't crash and no asserts are triggered const char* char_str = u8"\uc548\uc548\ub155\ud558\uc138\uc694\uc138\uacc4"; std::cout << "something before" << char_str << std::endl; std::cout << char_str << std::endl; std::cout << char_str << "something after" << std::endl; std::cerr << "something before" << char_str << std::endl; std::cerr << char_str << std::endl; std::cerr << char_str << "something after" << std::endl; const std::string str = u8"\u3053\u3093\u306b\u3061\u306f\u4e16\u754c"; std::cout << "something before" << str << std::endl; std::cout << str << std::endl; std::cout << str << "something after" << std::endl; std::cerr << "something before" << str << std::endl; std::cerr << str << std::endl; std::cerr << str << "something after" << std::endl; #ifdef _WIN32 const wchar_t* wchar_str = L"\uc548\uc548\ub155\ud558\uc138\uc694\uc138\uacc4"; std::wcout << "something before" << wchar_str << std::endl; std::wcout << wchar_str << std::endl; std::wcout << wchar_str << "something after" << std::endl; std::wcerr << "something before" << wchar_str << std::endl; std::wcerr << wchar_str << std::endl; std::wcerr << wchar_str << "something after" << std::endl; const std::wstring wstr = L"\u3053\u3093\u306b\u3061\u306f\u4e16\u754c"; std::wcout << "something before" << wstr << std::endl; std::wcout << wstr << std::endl; std::wcout << wstr << "something after" << std::endl; std::wcerr << "something before" << wstr << std::endl; std::wcerr << wstr << std::endl; std::wcerr << wstr << "something after" << std::endl; std::wcout << megacmd::utf8StringToUtf16WString(char_str) << std::endl; std::wcerr << megacmd::utf8StringToUtf16WString(char_str) << std::endl; std::wcout << megacmd::utf8StringToUtf16WString(str.data(), str.size()) << std::endl; std::wcerr << megacmd::utf8StringToUtf16WString(str.data(), str.size()) << std::endl; std::cout << megacmd::utf16ToUtf8(wchar_str) << std::endl; std::cerr << megacmd::utf16ToUtf8(wchar_str) << std::endl; std::cout << megacmd::utf16ToUtf8(wstr) << std::endl; std::cerr << megacmd::utf16ToUtf8(wstr) << std::endl; #endif } TEST(StringUtilsTest, generateRandomAlphaNumericString) { using megacmd::generateRandomAlphaNumericString; G_SUBTEST << "Generate string of specific length"; { std::string result = generateRandomAlphaNumericString(10); EXPECT_EQ(result.length(), 10); } G_SUBTEST << "Generate empty string"; { std::string result = generateRandomAlphaNumericString(0); EXPECT_EQ(result.length(), 0); } G_SUBTEST << "Generate long string"; { std::string result = generateRandomAlphaNumericString(100); EXPECT_EQ(result.length(), 100); } G_SUBTEST << "Generated strings are different"; { std::string result1 = generateRandomAlphaNumericString(20); std::string result2 = generateRandomAlphaNumericString(20); EXPECT_NE(result1, result2); } G_SUBTEST << "Generated string contains alphanumeric characters"; { std::string result = generateRandomAlphaNumericString(1000); for (char c : result) { EXPECT_TRUE(std::isalnum(c)); } } } TEST(UtilsTest, getNumberOfCols) { using megacmd::getNumberOfCols; G_SUBTEST << "Base case"; { auto cols = getNumberOfCols(); EXPECT_GT(cols, 0u); } G_SUBTEST << "With default value"; { auto cols = getNumberOfCols(80); EXPECT_GT(cols, 0u); } G_SUBTEST << "Multiple calls return consistent results"; { auto cols1 = getNumberOfCols(80); auto cols2 = getNumberOfCols(80); EXPECT_EQ(cols1, cols2); } G_SUBTEST << "Result is reasonable size"; { unsigned int cols = getNumberOfCols(); EXPECT_LE(cols, 10000u); } } TEST(UtilsTest, canWrite) { using megacmd::canWrite; G_SUBTEST << "Writable directory"; { SelfDeletingTmpFolder tmpFolder; testCanWriteAllPathVariants(tmpFolder.string(), true); } G_SUBTEST << "Non-existent path"; { #ifdef _WIN32 testCanWriteAllPathVariants(R"(C:\non\existent\path\12345)", false); #else testCanWriteAllPathVariants("/non/existent/path/12345", false); #endif } G_SUBTEST << "Empty string"; { testCanWriteAllPathVariants("", false); } G_SUBTEST << "File path"; { SelfDeletingTmpFolder tmpFolder; fs::path filePath = tmpFolder.path() / "test.txt"; std::ofstream file(filePath); file << "test"; file.close(); #ifdef _WIN32 std::string pathStr = filePath.string(); #else std::string pathStr = filePath.string(); #endif testCanWriteAllPathVariants(pathStr, true); } G_SUBTEST << "Path with trailing slash"; { SelfDeletingTmpFolder tmpFolder; #ifdef _WIN32 std::string pathWithSlash = tmpFolder.string() + R"(\)"; #else std::string pathWithSlash = tmpFolder.string() + "/"; #endif testCanWriteAllPathVariants(pathWithSlash, true); } G_SUBTEST << "Path with only spaces"; { #ifdef _WIN32 testCanWriteAllPathVariants(" ", true); // Edge case on Windows, see canWrite() implementation #else testCanWriteAllPathVariants(" ", false); #endif } G_SUBTEST << "Very long path"; { SelfDeletingTmpFolder tmpFolder; std::string longPath = tmpFolder.string(); #ifdef _WIN32 longPath += R"(\)" + std::string(200, 'a'); #else longPath += "/" + std::string(200, 'a'); #endif EXPECT_TRUE(fs::create_directories(longPath)); testCanWriteAllPathVariants(longPath, true); } G_SUBTEST << "Very long path exceeding 255 characters and MAX_PATH"; { SelfDeletingTmpFolder tmpFolder; std::string longPath = tmpFolder.string(); #ifdef _WIN32 longPath += R"(\)" + std::string(300, 'a'); #else longPath += "/" + std::string(300, 'a'); #endif EXPECT_THROW(fs::create_directories(longPath), std::filesystem::filesystem_error); testCanWriteAllPathVariants(longPath, false); } G_SUBTEST << "Path with special characters"; { SelfDeletingTmpFolder tmpFolder; fs::path specialPath = tmpFolder.path() / "test@#$%^&()_+-=[]{};',."; EXPECT_TRUE(fs::create_directory(specialPath)); testCanWriteAllPathVariants(specialPath.string(), true); } G_SUBTEST << "Read-only directory"; { SelfDeletingTmpFolder tmpFolder; fs::path readOnlyDir = tmpFolder.path() / "readonly"; EXPECT_TRUE(fs::create_directory(readOnlyDir)); #ifdef _WIN32 // On Windows, FILE_ATTRIBUTE_READONLY on directories is an Explorer // folder-customization signal (desktop.ini), not an access control flag. // canWrite must ignore it and rely on _waccess() / ACLs instead. // See Microsoft KB326549. std::wstring wpath = readOnlyDir.wstring(); DWORD oldAttrs = GetFileAttributesW(wpath.c_str()); SetFileAttributesW(wpath.c_str(), oldAttrs | FILE_ATTRIBUTE_READONLY); testCanWriteAllPathVariants(readOnlyDir.string(), true); SetFileAttributesW(wpath.c_str(), oldAttrs); #else chmod(readOnlyDir.c_str(), 0555); testCanWriteAllPathVariants(readOnlyDir.string(), false); chmod(readOnlyDir.c_str(), 0755); #endif } G_SUBTEST << "Read-only file"; { SelfDeletingTmpFolder tmpFolder; fs::path readOnlyFile = tmpFolder.path() / "readonly.txt"; std::ofstream file(readOnlyFile); file << "test"; file.close(); #ifdef _WIN32 std::wstring wpath = readOnlyFile.wstring(); DWORD oldAttrs = GetFileAttributesW(wpath.c_str()); SetFileAttributesW(wpath.c_str(), FILE_ATTRIBUTE_READONLY); testCanWriteAllPathVariants(readOnlyFile.string(), false); SetFileAttributesW(wpath.c_str(), oldAttrs); #else chmod(readOnlyFile.c_str(), 0444); testCanWriteAllPathVariants(readOnlyFile.string(), false); chmod(readOnlyFile.c_str(), 0644); #endif } G_SUBTEST << "Symlink to writable directory"; { SelfDeletingTmpFolder tmpFolder; fs::path targetDir = tmpFolder.path() / "target"; EXPECT_TRUE(fs::create_directory(targetDir)); fs::path symlinkPath = tmpFolder.path() / "symlink"; fs::create_symlink(targetDir, symlinkPath); testCanWriteAllPathVariants(symlinkPath.string(), true); } G_SUBTEST << "Symlink to read-only directory"; { SelfDeletingTmpFolder tmpFolder; fs::path targetDir = tmpFolder.path() / "target"; EXPECT_TRUE(fs::create_directory(targetDir)); #ifndef _WIN32 chmod(targetDir.c_str(), 0555); fs::path symlinkPath = tmpFolder.path() / "symlink"; fs::create_symlink(targetDir, symlinkPath); testCanWriteAllPathVariants(symlinkPath.string(), false); chmod(targetDir.c_str(), 0755); #endif } G_SUBTEST << "Nested directory structure"; { SelfDeletingTmpFolder tmpFolder; fs::path nestedPath = tmpFolder.path() / "level1" / "level2" / "level3"; EXPECT_TRUE(fs::create_directories(nestedPath)); testCanWriteAllPathVariants(nestedPath.string(), true); } G_SUBTEST << "Directory with UTF-8 characters"; { SelfDeletingTmpFolder tmpFolder; std::string utf8DirName = u8"\u041f\u0440\u0438\u0432\u0456\u0442"; // Ukrainian fs::path utf8Path = tmpFolder.path() / utf8DirName; EXPECT_TRUE(fs::create_directory(utf8Path)); #ifdef _WIN32 std::string utf8PathStr = megacmd::pathAsUtf8(utf8Path); #else std::string utf8PathStr = utf8Path.string(); #endif testCanWriteAllPathVariants(utf8PathStr, true); } G_SUBTEST << "Nested directory with UTF-8 characters"; { SelfDeletingTmpFolder tmpFolder; std::string utf8DirName = u8"\u4e2d\u6587"; // Chinese fs::path utf8Path = tmpFolder.path() / utf8DirName / "subdir"; EXPECT_TRUE(fs::create_directories(utf8Path)); #ifdef _WIN32 std::string utf8PathStr = megacmd::pathAsUtf8(utf8Path); #else std::string utf8PathStr = utf8Path.string(); #endif testCanWriteAllPathVariants(utf8PathStr, true); } } #ifdef _WIN32 // Regression: FILE_ATTRIBUTE_READONLY on directories is an Explorer // folder-customization hint (desktop.ini), not an access restriction. // canWrite() used to check this attribute and return false for directories // that were perfectly writable. See Microsoft KB326549. TEST(UtilsTest, canWriteIgnoresReadOnlyAttributeOnDirectories) { using megacmd::canWrite; G_SUBTEST << "Directory with FILE_ATTRIBUTE_READONLY is still writable"; { SelfDeletingTmpFolder tmpFolder; fs::path dir = tmpFolder.path() / "customized_folder"; EXPECT_TRUE(fs::create_directory(dir)); std::wstring wpath = dir.wstring(); DWORD oldAttrs = GetFileAttributesW(wpath.c_str()); // Simulate what Explorer does for folder customization SetFileAttributesW(wpath.c_str(), oldAttrs | FILE_ATTRIBUTE_READONLY); // Verify the attribute is actually set DWORD newAttrs = GetFileAttributesW(wpath.c_str()); EXPECT_TRUE(newAttrs & FILE_ATTRIBUTE_READONLY); // canWrite must return true — the directory is writable despite the attribute testCanWriteAllPathVariants(dir.string(), true); SetFileAttributesW(wpath.c_str(), oldAttrs); } G_SUBTEST << "Directory with FILE_ATTRIBUTE_READONLY and trailing separator"; { SelfDeletingTmpFolder tmpFolder; fs::path dir = tmpFolder.path() / "customized_folder2"; EXPECT_TRUE(fs::create_directory(dir)); std::wstring wpath = dir.wstring(); DWORD oldAttrs = GetFileAttributesW(wpath.c_str()); SetFileAttributesW(wpath.c_str(), oldAttrs | FILE_ATTRIBUTE_READONLY); testCanWriteAllPathVariants(dir.string() + R"(\)", true); SetFileAttributesW(wpath.c_str(), oldAttrs); } G_SUBTEST << "Read-only file is still correctly rejected"; { SelfDeletingTmpFolder tmpFolder; fs::path file = tmpFolder.path() / "readonly.txt"; std::ofstream ofs(file); ofs << "test"; ofs.close(); std::wstring wpath = file.wstring(); DWORD oldAttrs = GetFileAttributesW(wpath.c_str()); SetFileAttributesW(wpath.c_str(), oldAttrs | FILE_ATTRIBUTE_READONLY); // For files, FILE_ATTRIBUTE_READONLY *does* mean read-only, // and _waccess should return EACCES EXPECT_FALSE(canWrite(file.string())); SetFileAttributesW(wpath.c_str(), oldAttrs); } } #endif #ifdef _WIN32 TEST(UtilsTest, canWriteWithBypassEnvVar) { using megacmd::canWrite; G_SUBTEST << "MEGACMD_BYPASS_CAN_WRITE=1 makes canWrite always return true"; { TestInstrumentsEnvVarGuard bypassGuard("MEGACMD_BYPASS_CAN_WRITE", "1"); G_SUBSUBTEST << "Non-existent path should return true"; { EXPECT_TRUE(canWrite(R"(C:\non\existent\path\12345)")); } G_SUBSUBTEST << "Empty string should return true"; { EXPECT_TRUE(canWrite("")); } G_SUBSUBTEST << "Read-only directory should return true"; { SelfDeletingTmpFolder tmpFolder; fs::path readOnlyDir = tmpFolder.path() / "readonly"; EXPECT_TRUE(fs::create_directory(readOnlyDir)); std::wstring wpath = readOnlyDir.wstring(); SetFileAttributesW(wpath.c_str(), FILE_ATTRIBUTE_READONLY); EXPECT_TRUE(canWrite(readOnlyDir.string())); } G_SUBSUBTEST << "Read-only file should return true"; { SelfDeletingTmpFolder tmpFolder; fs::path readOnlyFile = tmpFolder.path() / "readonly.txt"; std::ofstream file(readOnlyFile); file << "test"; file.close(); std::wstring wpath = readOnlyFile.wstring(); SetFileAttributesW(wpath.c_str(), FILE_ATTRIBUTE_READONLY); EXPECT_TRUE(canWrite(readOnlyFile.string())); } G_SUBSUBTEST << "Writable directory should return true"; { SelfDeletingTmpFolder tmpFolder; EXPECT_TRUE(canWrite(tmpFolder.string())); } } G_SUBTEST << "MEGACMD_BYPASS_CAN_WRITE not set or not equal to 1 should work normally"; { TestInstrumentsUnsetEnvVarGuard unsetGuard("MEGACMD_BYPASS_CAN_WRITE"); G_SUBSUBTEST << "Non-existent path should return false"; { EXPECT_FALSE(canWrite(R"(C:\non\existent\path\12345)")); } G_SUBSUBTEST << "Empty string should return false"; { EXPECT_FALSE(canWrite("")); } G_SUBSUBTEST << "Writable directory should return true"; { SelfDeletingTmpFolder tmpFolder; EXPECT_TRUE(canWrite(tmpFolder.string())); } } G_SUBTEST << "MEGACMD_BYPASS_CAN_WRITE set to value other than 1 should work normally"; { TestInstrumentsEnvVarGuard bypassGuard("MEGACMD_BYPASS_CAN_WRITE", "0"); G_SUBSUBTEST << "Non-existent path should return false"; { EXPECT_FALSE(canWrite(R"(C:\non\existent\path\12345)")); } G_SUBSUBTEST << "Writable directory should return true"; { SelfDeletingTmpFolder tmpFolder; EXPECT_TRUE(canWrite(tmpFolder.string())); } } } #endif MEGAcmd-2.5.2_Linux/tests/unit/main.cpp000066400000000000000000000022611516543156300176570ustar00rootroot00000000000000/** * (c) 2013 by Mega Limited, Auckland, New Zealand * * This file is part of MEGAcmd. * * MEGAcmd 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. * * @copyright Simplified (2-clause) BSD License. * * You should have received a copy of the license along with this * program. */ #include "megacmdcommonutils.h" #include "megaapi.h" #include #include "TestUtils.h" int main (int argc, char *argv[]) { mega::MegaApi::setLogToConsole(true); mega::MegaApi::setLogLevel(mega::MegaApi::LOG_LEVEL_MAX); testing::InitGoogleTest(&argc, argv); #ifdef WIN32 megacmd::Instance windowsConsoleController; // Set custom gtests event listener to control the output to stdout ::testing::TestEventListeners& listeners = ::testing::UnitTest::GetInstance()->listeners(); auto customListener = new CustomTestEventListener(); customListener->mDefault.reset(listeners.Release(listeners.default_result_printer())); listeners.Append(customListener); #endif return RUN_ALL_TESTS(); } MEGAcmd-2.5.2_Linux/vcpkg.json000066400000000000000000000047041516543156300161170ustar00rootroot00000000000000{ "$schema" : "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", "name" : "sdklib", "homepage" : "https://github.com/meganz/megacmd", "features" : { "megacmd-enable-tests": { "description": "Compile MEGAcmd tests", "dependencies": [ "gtest" ] }, "use-openssl": { "description": "OpenSSL library", "dependencies": [ "openssl" ] }, "use-mediainfo": { "description": "MediaInfo library", "dependencies": [ "libmediainfo" ] }, "use-freeimage": { "description": "FreeImage library", "dependencies": [ "freeimage", { "name": "jasper", "default-features": false } ] }, "use-ffmpeg": { "description": "FFMpeg library", "dependencies": [ { "name": "ffmpeg", "default-features": false, "features": ["avcodec", "avformat", "swresample", "swscale"] } ] }, "use-libuv": { "description": "libuv library", "dependencies": [ "libuv" ] }, "use-pdfium": { "description": "pdfium library", "dependencies": [ "pdfium" ] }, "use-readline": { "description": "Readline library", "dependencies": [ "readline" ] }, "sdk-tests": { "description": "gtests library for the integration and unit tests", "dependencies": [ "gtest" ] }, "c-ares-backend-curl": { "description": "Enable c-ares backend for curl", "dependencies": [ { "name": "curl", "features": [ "c-ares" ] } ] } }, "dependencies": [ "pcre", "cryptopp", { "name": "curl", "features": [ "zstd" ] }, "icu", "libsodium", "sqlite3" ], "builtin-baseline": "ef7dbf94b9198bc58f45951adcf1f041fcbc5ea0", "overrides": [ { "name": "icu", "version": "74.2#4" }, { "name": "libuv", "version": "1.49.2" } ] }