pax_global_header00006660000000000000000000000064147656440030014523gustar00rootroot0000000000000052 comment=be13f9759b126eee197775608bb4d6a902b87a5c onedrive-2.5.5/000077500000000000000000000000001476564400300133475ustar00rootroot00000000000000onedrive-2.5.5/.github/000077500000000000000000000000001476564400300147075ustar00rootroot00000000000000onedrive-2.5.5/.github/FUNDING.yml000066400000000000000000000000261476564400300165220ustar00rootroot00000000000000--- github: abrauneggonedrive-2.5.5/.github/ISSUE_TEMPLATE/000077500000000000000000000000001476564400300170725ustar00rootroot00000000000000onedrive-2.5.5/.github/ISSUE_TEMPLATE/bug_report.yml000066400000000000000000000125671476564400300220000ustar00rootroot00000000000000name: "Bug Report" description: Before proceeding, please ensure your issue is a genuine software bug. This form is exclusively for reporting actual software bugs that need fixing. For other items, use GitHub Discussions instead. title: "Bug: " labels: ["Bug"] body: - type: markdown attributes: value: | **Note:** Before submitting a bug report, please ensure you are running the latest 'onedrive' client as built from 'master' and compile by using the latest available DMD or LDC compiler. Refer to the [install](https://github.com/abraunegg/onedrive/blob/master/docs/install.md) document on how to build the client for your system. - type: textarea id: bugDescription attributes: label: Describe the bug description: | Add a clear and concise description of what you think the bug is. validations: required: true - type: textarea id: operatingSystemDetails attributes: label: Operating System Details description: | * What is your Operating System (`uname -a`) * Output of: (`cat /etc/redhat-release`) or (`lsb_release -a`) render: shell validations: required: true - type: dropdown id: installMethod attributes: label: Client Installation Method description: | How did you install the client? multiple: false options: - From Source - From Distribution Package - From 3rd Party Source (PPA, OpenSuSE Build Service etc) validations: required: true - type: dropdown id: accountType attributes: label: OneDrive Account Type description: | What is your OneDrive Account Type? multiple: false options: - Personal - Business | Office365 - SharePoint validations: required: true - type: input id: applicationVersion attributes: label: What is your OneDrive Application Version description: | * What is your 'onedrive' client version (`onedrive --version`)? validations: required: true - type: textarea id: applicationConfig attributes: label: What is your OneDrive Application Configuration description: | * What is your Application Configuration (`onedrive --display-config`)? render: shell validations: required: true - type: textarea id: curlVersion attributes: label: What is your 'curl' version description: | * What is your output of (`curl --version`)? render: shell validations: required: true - type: dropdown id: syncdirLocation attributes: label: Where is your 'sync_dir' located description: | Is your 'sync_dir' a local directory or on a network mount point? multiple: false options: - Local - Network validations: required: true - type: textarea id: mountPoints attributes: label: What are all your system 'mount points' description: | * What is your output of (`mount`)? render: shell validations: required: true - type: textarea id: partitionTypes attributes: label: What are all your local file system partition types description: | * What is your output of (`lsblk -f`)? render: shell validations: required: true - type: textarea id: usageDetails attributes: label: How do you use 'onedrive' description: | Explain your entire configuration setup - is the OneDrive folder shared with any other system, shared with any other platform at the same time, is the OneDrive account you use shared across multiple systems / platforms / Operating Systems and in use at the same time validations: required: true - type: textarea id: howToReproduce attributes: label: Steps to reproduce the behaviour description: | List all the steps required to reproduce the issue. If issue is replicated by a specific 'file' or 'path' please archive the file and path tree & email to support@mynas.com.au validations: required: true - type: textarea id: applicationVerboseLog attributes: label: Complete Verbose Log Output description: | A clear and full log of the problem when running the application in the following manner (ie, not in monitor mode): (`onedrive --synchronize --verbose `) Run the application in a separate terminal window or SSH session and provide the entire application output including the error & crash. Please also generate a full debug log whilst reproducing the issue as per [https://github.com/abraunegg/onedrive/wiki/Generate-debug-log-for-support](https://github.com/abraunegg/onedrive/wiki/Generate-debug-log-for-support) and email to support@mynas.com.au render: shell validations: required: true - type: textarea id: screenshots attributes: label: Screenshots description: | If applicable, add screenshots to help explain your problem. - type: textarea id: otherLogs attributes: label: Other Log Information or Details description: | If applicable, add the relevant output from `dmesg` or similar. render: shell - type: textarea id: additionalContext attributes: label: Additional context description: | Add any other relevant additional context for the problem. onedrive-2.5.5/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000011321476564400300210570ustar00rootroot00000000000000blank_issues_enabled: false contact_links: - name: "Have a question?" url: https://github.com/abraunegg/onedrive/discussions about: "Please refrain from using GitHub Issues for asking questions or reporting non-software bugs. Instead, post your questions, installation issues, dependency concerns, or anything else that isn't a software bug under GitHub Discussions. When starting a new GitHub Discussion, be sure to include all relevant details, such as your application version and installation method. Thank you for helping us keep the issue tracker focused on actual software bugs!" onedrive-2.5.5/.github/ISSUE_TEMPLATE/feature_request.yml000066400000000000000000000023341476564400300230220ustar00rootroot00000000000000name: "Feature Request" description: Suggest an idea for this project title: "Feature Request: " labels: ["Feature Request"] body: - type: markdown attributes: value: | Suggest an idea for this project - type: textarea id: featureProblem attributes: label: Is your feature request related to a problem? Please describe. description: | A clear and concise description of what the problem is. Ex. I'm always frustrated when ... validations: required: true - type: textarea id: featureSolution attributes: label: Describe the solution you'd like description: | A clear and concise description of what you want to happen. validations: required: true - type: textarea id: featureAlternatives attributes: label: Describe alternatives you've considered description: | A clear and concise description of any alternative solutions or features you've considered. validations: required: true - type: textarea id: additionalContext attributes: label: Additional context description: | Add any other context or information about the feature request here. validations: required: falseonedrive-2.5.5/.github/actions/000077500000000000000000000000001476564400300163475ustar00rootroot00000000000000onedrive-2.5.5/.github/actions/spelling/000077500000000000000000000000001476564400300201645ustar00rootroot00000000000000onedrive-2.5.5/.github/actions/spelling/allow.txt000066400000000000000000000071351476564400300220510ustar00rootroot00000000000000# # Allowed words not found in configured dictionaries # AADSTS aarch abraunegg accrights accrightslen adamdruppe addrepo adr ags aip alex alpinelinux annobin antix aothmane apng archlinux ARequest armhf ARMv arsd arsdnet artix ASCIIHTML ASpurious AThings aufxw aur autoclean automake autoprocess autoupdate avmkfdiitirnrenzljwc avx baus bcdefghi beffdb bindir bir blargh bools bpozdena brp btw bugzilla buildfiles buildroot bytecompile cancelfd CApath cattr cbf ccc certbot checkinterval chkconfig chpst classinfo cloexec Cloudflare cmptr cmsghdr codacy commandline concated confdir constness controllen crt ctl ctls cyb datadir dchar debian dechunk Deepin deimos devuan dhparams dirmask dlang dlnow dltotal dmd dnf dnotify Dockerfiles dotfile dphys driveid driveone druntime drwx drwxr drwxrwxr dryrun DTime Dynu eis ele endinaness enduml envp epfd estr eventfd evt fasynchronous fbc fcf fcgid fcgienv FCGX fcontext fedoraproject fefefe fexceptions ffat FFFD fhandler flto fstack FState fullchain fullscan gdc gdk gerror getenforce getxattr gfortran GFree Gibi gmake gmodule GObject gpg GPLv GPOs grecord groupinstall gshared GVariant hideonindex hnsecs hskrieg htons idk idlol idup ifrom includedir ine infodir initted initval intercambio iocp ioop ioops iov iovec iovlen ipresolve isv itemdb itimerspec journalctl jsvar kdbx keepass lalen lbl lcurl ldc ldconfig ldl letsencrypt lgdk lgio lglib lgobject libcrypto libdir libexec libexecdir libgcc libgdk libgio libglib libgobject libinotify liblphobos libm libnotify libsqlite Lighttpd lintian llclose llsend lnotify localstatedir lol lpdw lpfn lrwxrwxrwx lsb lsqlite ltmain Lyncredible makepkg mangleof maxfiles mayne mbr memtest microsoftonline mountpoint mozram msghdr msonenote mswsock mtune mydir mynasau myusername nadded nadjusted namelen nas nativeclient nbd nbytes ncache ndata netinet nev nevent newfd newfile nexisting nfds nfor nintegrity niop nobj nodelay nolan nomount nosignal nosync notif nph nrecords nto nuntil nupload nvia objectsx odata ofonedrive onbinarymessage onedrive onetoc onmicrosoft ontextmessage opensuse overallocated pacman pamac parentid passoff pastebin pfa phlibi phobos pidx pixbuf pkgconf pki pkolmann podman pollfd pollfds postun prefork preun privkey Privs prueba prw Pseudoblocking puml qewrqwerwqer QWords qxor raf ralen raspberrypi raspi raspios rbtree rdo readdata readln readret reauth Recvd recvfd redhat relro restorecon retu revents rko rlc robertschulze rpcing rpmbuild rpmlib Rproj rrodrigueznt rsv rtud rul runsvdir Ruppe rwxr sargs sbindir scgi sdlang semanage sendfd setsebool settime setxattr sev sfn sharedstatedir sharepoint shortnames sigaction sigchldhandler sigemptyset signo sigpipe skilion skinparam Sockaddrs somevar sooooo startuml statm stdc stringof Stringz subobject subobjects svdir swapfile swapon swp symcode syncable syncdir sysconf sysconfdir systemdsystemunitdir systemduserunitdir tbh tdcockers templ Thh thnk tidx timerfd tlsv Tting typecons uda udas ulnow uload ultotal undefiened unistd unittests urlify userinfo usermod userns userpass usl valgrind vti wal websockets webtemplate weburl Werror wpath writefln wrt wtf xca xcbac xdeadbeef xdg xlaunch xof xored XXYYZZ yann yourapp yourdomain yourfile yourprogram Zorin zypper onedrive-2.5.5/.github/actions/spelling/excludes.txt000066400000000000000000000002001476564400300225310ustar00rootroot00000000000000# Ignore the action's configuration data ^\.github/action/spelling/ # Ignore all GitHub workflow files ^\.github/workflows/ onedrive-2.5.5/.github/actions/spelling/only.txt000066400000000000000000000001721476564400300217060ustar00rootroot00000000000000# Only process the following files # *.md # onedrive.1.in # *.d # *.puml \.md$ \.d$ \.puml$ ^onedrive\\.1\\.in$ onedrive-2.5.5/.github/actions/spelling/patterns.txt000066400000000000000000000017331476564400300225710ustar00rootroot00000000000000# https/http/file urls (?:\b(?:https?|ftp|file)://)[-A-Za-z0-9+&@#/*%?=~_|!:,.;]+[-A-Za-z0-9+&@#/*%=~_|] # uuid: \b[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}\b # sha256: \bsha256:[0-9a-fA-F]{64}\b # sha256 string without any prefix \b[0-9a-fA-F]{64}\b # 32 character string as generated from /dev/urandom \b[a-zA-Z0-9]{32}\b # 16 character string as generated from /dev/urandom \b[a-zA-Z0-9]{16}\b # Microsoft URL's \b(?:https?://|)(?:(?:download\.visualstudio|docs|msdn2?|research)\.microsoft|blogs\.msdn)\.com/[-_a-zA-Z0-9()=./%]* # Microsoft OneDrive Business Account DriveID identifiers \b[bB]![A-Za-z0-9_-]{64}\b # UTF-16 Hex Values \b0x(?:D[89A-F][0-9A-F]{2}|E[0-9A-F]{3}|F[0-9A-F]{3})\b # man troff content # https://www.gnu.org/software/groff/manual/groff.html \\f[BCIPR] # All UPPERCASE letters only (no underscores) \b[A-Z]+\b # Ignore UPPERCASE letters separated by an underscore '_' \b[A-Z]+(?:_[A-Z]+)*\b onedrive-2.5.5/.github/workflows/000077500000000000000000000000001476564400300167445ustar00rootroot00000000000000onedrive-2.5.5/.github/workflows/docker.yaml000066400000000000000000000056001476564400300211000ustar00rootroot00000000000000name: Build Docker Images on: push: branches: [ master ] tags: [ 'v*' ] pull_request: # Comment these out to force a test build on a PR branches: - master types: [closed] env: DOCKER_HUB_SLUG: driveone/onedrive jobs: build: # Comment this out to force a test build on a PR if: (!(github.event.action == 'closed' && github.event.pull_request.merged != true)) # Build runs on runs-on: ubuntu-latest strategy: matrix: flavor: [ fedora, debian, alpine ] include: - flavor: fedora dockerfile: ./contrib/docker/Dockerfile platforms: linux/amd64,linux/arm64 - flavor: debian dockerfile: ./contrib/docker/Dockerfile-debian platforms: linux/386,linux/amd64,linux/arm64,linux/arm/v7 - flavor: alpine dockerfile: ./contrib/docker/Dockerfile-alpine platforms: linux/amd64,linux/arm64 steps: - name: Check out code from GitHub uses: actions/checkout@v3 with: submodules: recursive fetch-depth: 0 - name: Docker meta id: docker_meta uses: marcelcoding/ghaction-docker-meta@v2 with: tag-edge: true images: | ${{ env.DOCKER_HUB_SLUG }} tag-semver: | {{version}} {{major}}.{{minor}} flavor: ${{ matrix.flavor }} main-flavor: ${{ matrix.flavor == 'debian' }} - uses: docker/setup-qemu-action@v2 with: image: tonistiigi/binfmt:latest platforms: all if: matrix.platforms != 'linux/amd64' - uses: docker/setup-buildx-action@v2 - name: Cache Docker layers uses: actions/cache@v3 with: path: /tmp/.buildx-cache key: ${{ runner.os }}-buildx-${{ matrix.flavor }}-${{ github.sha }} restore-keys: | ${{ runner.os }}-buildx-${{ matrix.flavor }} - name: Login to Docker Hub uses: docker/login-action@v2 if: github.event_name != 'pull_request' with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Build and Push to Docker uses: docker/build-push-action@v3 with: context: . file: ${{ matrix.dockerfile }} platforms: ${{ matrix.platforms }} push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.docker_meta.outputs.tags }} labels: ${{ steps.docker_meta.outputs.labels }} cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache-new - name: Move cache run: | rm -rf /tmp/.buildx-cache mv /tmp/.buildx-cache-new /tmp/.buildx-cache onedrive-2.5.5/.github/workflows/lock.yml000066400000000000000000000021221476564400300204140ustar00rootroot00000000000000name: 'Lock Threads' on: schedule: - cron: '19 0 * * *' jobs: lock: runs-on: ubuntu-latest steps: - name: Lock Threads uses: dessant/lock-threads@v2.0.3 with: github-token: ${{ secrets.LOCK_THREADS }} issue-lock-inactive-days: '7' issue-exclude-created-before: '' issue-exclude-labels: '' issue-lock-labels: '' issue-lock-comment: > This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. issue-lock-reason: 'resolved' pr-lock-inactive-days: '7' pr-exclude-created-before: '' pr-exclude-labels: '' pr-lock-labels: '' pr-lock-comment: > This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. pr-lock-reason: '' process-only: '' onedrive-2.5.5/.github/workflows/spellcheck.yaml000066400000000000000000000164141476564400300217530ustar00rootroot00000000000000name: Check Spelling # Comment management is handled through a secondary job, for details see: # https://github.com/check-spelling/check-spelling/wiki/Feature%3A-Restricted-Permissions # # `jobs.comment-push` runs when a push is made to a repository and the `jobs.spelling` job needs to make a comment # (in odd cases, it might actually run just to collapse a comment, but that's fairly rare) # it needs `contents: write` in order to add a comment. # # `jobs.comment-pr` runs when a pull_request is made to a repository and the `jobs.spelling` job needs to make a comment # or collapse a comment (in the case where it had previously made a comment and now no longer needs to show a comment) # it needs `pull-requests: write` in order to manipulate those comments. # Updating pull request branches is managed via comment handling. # For details, see: https://github.com/check-spelling/check-spelling/wiki/Feature:-Update-expect-list # # These elements work together to make it happen: # # `on.issue_comment` # This event listens to comments by users asking to update the metadata. # # `jobs.update` # This job runs in response to an issue_comment and will push a new commit # to update the spelling metadata. # # `with.experimental_apply_changes_via_bot` # Tells the action to support and generate messages that enable it # to make a commit to update the spelling metadata. # # `with.ssh_key` # In order to trigger workflows when the commit is made, you can provide a # secret (typically, a write-enabled github deploy key). # # For background, see: https://github.com/check-spelling/check-spelling/wiki/Feature:-Update-with-deploy-key # Sarif reporting # # Access to Sarif reports is generally restricted (by GitHub) to members of the repository. # # Requires enabling `security-events: write` # and configuring the action with `use_sarif: 1` # # For information on the feature, see: https://github.com/check-spelling/check-spelling/wiki/Feature:-Sarif-output # Minimal workflow structure: # # on: # push: # ... # pull_request_target: # ... # jobs: # # you only want the spelling job, all others should be omitted # spelling: # # remove `security-events: write` and `use_sarif: 1` # # remove `experimental_apply_changes_via_bot: 1` # ... otherwise adjust the `with:` as you wish on: push: branches: - "**" tags-ignore: - "**" pull_request_target: branches: - "**" types: - 'opened' - 'reopened' - 'synchronize' issue_comment: types: - 'created' jobs: spelling: name: Check Spelling permissions: contents: read pull-requests: read actions: read security-events: write outputs: followup: ${{ steps.spelling.outputs.followup }} runs-on: ubuntu-latest if: ${{ contains(github.event_name, 'pull_request') || github.event_name == 'push' }} concurrency: group: spelling-${{ github.event.pull_request.number || github.ref }} # note: If you use only_check_changed_files, you do not want cancel-in-progress cancel-in-progress: true steps: - name: check-spelling id: spelling uses: check-spelling/check-spelling@main with: suppress_push_for_open_pull_request: ${{ github.actor != 'dependabot[bot]' && 1 }} checkout: true check_file_names: 0 spell_check_this: check-spelling/spell-check-this@prerelease post_comment: 0 use_magic_file: 1 report-timing: 1 warnings: bad-regex,binary-file,deprecated-feature,large-file,limited-references,no-newline-at-eof,noisy-file,non-alpha-in-dictionary,token-is-substring,unexpected-line-ending,whitespace-in-dictionary,minified-file,unsupported-configuration,no-files-to-check experimental_apply_changes_via_bot: 1 use_sarif: ${{ (!github.event.pull_request || (github.event.pull_request.head.repo.full_name == github.repository)) && 1 }} extra_dictionary_limit: 40 extra_dictionaries: cspell:en_AU/src/hunspell-en_AU-large/en_AU-large.dic cspell:en_GB/src/hunspell/en_GB.dic cspell:software-terms/dict/softwareTerms.txt cspell:php/dict/php.txt cspell:node/dict/node.txt cspell:python/src/python/python-lib.txt cspell:python/src/common/extra.txt cspell:shell/dict/shell-all-words.txt cspell:filetypes/filetypes.txt cspell:k8s/dict/k8s.txt cspell:fullstack/dict/fullstack.txt cspell:golang/dict/go.txt cspell:aws/aws.txt cspell:java/src/java-terms.txt cspell:java/src/java.txt cspell:html/dict/html.txt cspell:typescript/dict/typescript.txt cspell:cpp/src/stdlib-c.txt cspell:dotnet/dict/dotnet.txt cspell:cpp/src/compiler-msvc.txt cspell:cpp/src/stdlib-cpp.txt cspell:cpp/src/ecosystem.txt cspell:r/src/r.txt cspell:cpp/src/compiler-gcc.txt cspell:rust/dict/rust.txt cspell:swift/src/swift.txt cspell:css/dict/css.txt cspell:cpp/src/lang-jargon.txt cspell:docker/src/docker-words.txt cspell:csharp/csharp.txt cspell:clojure/src/clojure.txt cspell:public-licenses/src/generated/public-licenses.txt cspell:lisp/lisp.txt cspell:django/dict/django.txt comment-push: name: Report (Push) # If your workflow isn't running on push, you can remove this job runs-on: ubuntu-latest needs: spelling permissions: contents: write if: (success() || failure()) && needs.spelling.outputs.followup && github.event_name == 'push' steps: - name: comment uses: check-spelling/check-spelling@main with: checkout: true spell_check_this: check-spelling/spell-check-this@prerelease task: ${{ needs.spelling.outputs.followup }} comment-pr: name: Report (PR) # If you workflow isn't running on pull_request*, you can remove this job runs-on: ubuntu-latest needs: spelling permissions: contents: read pull-requests: write if: (success() || failure()) && needs.spelling.outputs.followup && contains(github.event_name, 'pull_request') steps: - name: comment uses: check-spelling/check-spelling@main with: checkout: true spell_check_this: check-spelling/spell-check-this@prerelease task: ${{ needs.spelling.outputs.followup }} experimental_apply_changes_via_bot: 1 update: name: Update PR permissions: contents: write pull-requests: write actions: read runs-on: ubuntu-latest if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request && contains(github.event.comment.body, '@check-spelling-bot apply') }} concurrency: group: spelling-update-${{ github.event.issue.number }} cancel-in-progress: false steps: - name: apply spelling updates uses: check-spelling/check-spelling@main with: experimental_apply_changes_via_bot: 1 checkout: true ssh_key: "${{ secrets.CHECK_SPELLING }}"onedrive-2.5.5/.github/workflows/testbuild.yaml000066400000000000000000000015111476564400300216250ustar00rootroot00000000000000name: Test Build on: push: branches: [ "master" ] pull_request: branches: [ "master" ] jobs: build: #runs-on: ubuntu-latest runs-on: ubuntu-20.04 steps: - name: Check out code from GitHub uses: actions/checkout@v3 with: submodules: recursive fetch-depth: 0 - name: Update Image run: | sudo apt-get clean sudo apt-get update -y - name: Install build-essential run: sudo apt install -y build-essential - name: Install build-dependencies run: sudo apt install -y libcurl4-openssl-dev libsqlite3-dev pkg-config git curl ldc - name: Configure run: ./configure - name: Compile run: make clean; make; - name: Install run: sudo make install - name: Run run: onedrive --versiononedrive-2.5.5/.gitignore000066400000000000000000000005411476564400300153370ustar00rootroot00000000000000.* onedrive onedrive.1 onedrive.o version Makefile config.log config.status autom4te.cache/ contrib/pacman/PKGBUILD contrib/spec/onedrive.spec # Ignore everything in the .github folder .github/* # Allow actions/spelling in .github !.github/actions/spelling/ # Ensure spellcheck.yaml in .github is not ignored !.github/actions/spelling/spellcheck.yamlonedrive-2.5.5/LICENSE000066400000000000000000001045151476564400300143620ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . onedrive-2.5.5/Makefile.in000066400000000000000000000132541476564400300154210ustar00rootroot00000000000000package = @PACKAGE_NAME@ version = @PACKAGE_VERSION@ prefix = @prefix@ # we don't use @exec_prefix@ because it usually contains '${prefix}' literally # but we use @prefix@/bin/onedrive in the systemd unit files which are generated # from the configure script. # Thus, set exec_prefix unconditionally to prefix # Alternative approach would be add dep on sed, and do manual generation in the Makefile. # exec_prefix = @exec_prefix@ exec_prefix = @prefix@ datarootdir = @datarootdir@ datadir = @datadir@ srcdir = @srcdir@ bindir = @bindir@ mandir = @mandir@ sysconfdir = @sysconfdir@ docdir = $(datadir)/doc/$(package) VPATH = @srcdir@ INSTALL = @INSTALL@ NOTIFICATIONS = @NOTIFICATIONS@ HAVE_SYSTEMD = @HAVE_SYSTEMD@ systemduserunitdir = @systemduserunitdir@ systemdsystemunitdir = @systemdsystemunitdir@ curl_LIBS = @curl_LIBS@ sqlite_LIBS = @sqlite_LIBS@ notify_LIBS = @notify_LIBS@ bsd_inotify_LIBS = @bsd_inotify_LIBS@ dynamic_linker_LIBS = @dynamic_linker_LIBS@ COMPLETIONS = @COMPLETIONS@ BASH_COMPLETION_DIR = @BASH_COMPLETION_DIR@ ZSH_COMPLETION_DIR = @ZSH_COMPLETION_DIR@ FISH_COMPLETION_DIR = @FISH_COMPLETION_DIR@ DEBUG = @DEBUG@ DC = @DC@ DC_TYPE = @DC_TYPE@ DCFLAGS = @DCFLAGS@ DCFLAGS += -w -J. ifeq ($(DEBUG),yes) ifeq ($(DC_TYPE),dmd) # Add DMD Debugging Flags DCFLAGS += -g -debug -gs else # Add LDC Debugging Flags DCFLAGS += -g -d-debug -gc endif else # Only add optimisation flags if debugging is not enabled DCFLAGS += -O endif ifeq ($(NOTIFICATIONS),yes) NOTIF_VERSIONS=-version=NoPragma -version=NoGdk -version=Notifications # support ldc2 which needs -d prefix for version specification ifeq ($(DC_TYPE),ldc) NOTIF_VERSIONS := $(addprefix -d,$(NOTIF_VERSIONS)) endif DCFLAGS += $(NOTIF_VERSIONS) endif system_unit_files = contrib/systemd/onedrive@.service user_unit_files = contrib/systemd/onedrive.service DOCFILES = readme.md config LICENSE changelog.md docs/advanced-usage.md docs/application-config-options.md docs/application-security.md docs/business-shared-items.md docs/client-architecture.md docs/contributing.md docs/docker.md docs/install.md docs/national-cloud-deployments.md docs/podman.md docs/privacy-policy.md docs/sharepoint-libraries.md docs/terms-of-service.md docs/ubuntu-package-install.md docs/usage.md docs/known-issues.md docs/webhooks.md ifneq ("$(wildcard /etc/redhat-release)","") RHEL = $(shell cat /etc/redhat-release | grep -E "(Red Hat Enterprise Linux|CentOS)" | wc -l) RHEL_VERSION = $(shell rpm --eval "%{rhel}") else RHEL = 0 RHEL_VERSION = 0 endif SOURCES = \ src/main.d \ src/config.d \ src/log.d \ src/util.d \ src/qxor.d \ src/curlEngine.d \ src/onedrive.d \ src/webhook.d \ src/sync.d \ src/itemdb.d \ src/sqlite.d \ src/clientSideFiltering.d \ src/monitor.d \ src/arsd/cgi.d \ src/xattr.d ifeq ($(NOTIFICATIONS),yes) SOURCES += src/notifications/notify.d src/notifications/dnotify.d endif all: onedrive clean: rm -f onedrive onedrive.o version rm -rf autom4te.cache rm -f config.log config.status # Remove files generated via ./configure distclean: clean rm -f Makefile contrib/pacman/PKGBUILD contrib/spec/onedrive.spec onedrive.1 $(system_unit_files) $(user_unit_files) onedrive: $(SOURCES) if [ -f .git/HEAD ] ; then \ git describe --tags > version ; \ else \ echo $(version) > version ; \ fi $(DC) $(DCFLAGS) $(addprefix -L,$(curl_LIBS)) $(addprefix -L,$(sqlite_LIBS)) $(addprefix -L,$(notify_LIBS)) $(addprefix -L,$(bsd_inotify_LIBS)) $(addprefix -L,$(dynamic_linker_LIBS)) $(SOURCES) -of$@ install: all mkdir -p $(DESTDIR)$(bindir) $(INSTALL) onedrive $(DESTDIR)$(bindir)/onedrive mkdir -p $(DESTDIR)$(mandir)/man1 $(INSTALL) -m 0644 onedrive.1 $(DESTDIR)$(mandir)/man1/onedrive.1 mkdir -p $(DESTDIR)$(sysconfdir)/logrotate.d $(INSTALL) -m 0644 contrib/logrotate/onedrive.logrotate $(DESTDIR)$(sysconfdir)/logrotate.d/onedrive mkdir -p $(DESTDIR)$(docdir) for file in $(DOCFILES); do \ $(INSTALL) -m 0644 $$file $(DESTDIR)$(docdir); \ done ifeq ($(HAVE_SYSTEMD),yes) mkdir -p $(DESTDIR)$(systemduserunitdir) mkdir -p $(DESTDIR)$(systemdsystemunitdir) ifeq ($(RHEL),1) $(INSTALL) -m 0644 $(system_unit_files) $(DESTDIR)$(systemdsystemunitdir) $(INSTALL) -m 0644 $(user_unit_files) $(DESTDIR)$(systemdsystemunitdir) else $(INSTALL) -m 0644 $(system_unit_files) $(DESTDIR)$(systemdsystemunitdir) $(INSTALL) -m 0644 $(user_unit_files) $(DESTDIR)$(systemduserunitdir) endif else ifeq ($(RHEL_VERSION),6) $(INSTALL) contrib/init.d/onedrive.init $(DESTDIR)/etc/init.d/onedrive $(INSTALL) contrib/init.d/onedrive_service.sh $(DESTDIR)$(bindir)/onedrive_service.sh endif endif ifeq ($(COMPLETIONS),yes) mkdir -p $(DESTDIR)$(ZSH_COMPLETION_DIR) $(INSTALL) -m 0644 contrib/completions/complete.zsh $(DESTDIR)$(ZSH_COMPLETION_DIR)/_onedrive mkdir -p $(DESTDIR)$(BASH_COMPLETION_DIR) $(INSTALL) -m 0644 contrib/completions/complete.bash $(DESTDIR)$(BASH_COMPLETION_DIR)/onedrive mkdir -p $(DESTDIR)$(FISH_COMPLETION_DIR) $(INSTALL) -m 0644 contrib/completions/complete.fish $(DESTDIR)$(FISH_COMPLETION_DIR)/onedrive.fish endif uninstall: rm -f $(DESTDIR)$(bindir)/onedrive rm -f $(DESTDIR)$(mandir)/man1/onedrive.1 rm -f $(DESTDIR)$(sysconfdir)/logrotate.d/onedrive ifeq ($(HAVE_SYSTEMD),yes) ifeq ($(RHEL),1) rm -f $(DESTDIR)$(systemdsystemunitdir)/onedrive*.service else rm -f $(DESTDIR)$(systemdsystemunitdir)/onedrive*.service rm -f $(DESTDIR)$(systemduserunitdir)/onedrive*.service endif else ifeq ($(RHEL_VERSION),6) rm -f $(DESTDIR)/etc/init.d/onedrive rm -f $(DESTDIR)$(bindir)/onedrive_service.sh endif endif for i in $(DOCFILES) ; do rm -f $(DESTDIR)$(docdir)/$$i ; done ifeq ($(COMPLETIONS),yes) rm -f $(DESTDIR)$(ZSH_COMPLETION_DIR)/_onedrive rm -f $(DESTDIR)$(BASH_COMPLETION_DIR)/onedrive rm -f $(DESTDIR)$(FISH_COMPLETION_DIR)/onedrive.fish endif onedrive-2.5.5/aclocal.m4000066400000000000000000000252401476564400300152120ustar00rootroot00000000000000# generated automatically by aclocal 1.16.1 -*- Autoconf -*- # Copyright (C) 1996-2018 Free Software Foundation, Inc. # This file is free software; the Free Software Foundation # gives unlimited permission to copy and/or distribute it, # with or without modifications, as long as this notice is preserved. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY, to the extent permitted by law; without # even the implied warranty of MERCHANTABILITY or FITNESS FOR A # PARTICULAR PURPOSE. m4_ifndef([AC_CONFIG_MACRO_DIRS], [m4_defun([_AM_CONFIG_MACRO_DIRS], [])m4_defun([AC_CONFIG_MACRO_DIRS], [_AM_CONFIG_MACRO_DIRS($@)])]) dnl pkg.m4 - Macros to locate and utilise pkg-config. -*- Autoconf -*- dnl serial 11 (pkg-config-0.29) dnl dnl Copyright © 2004 Scott James Remnant . dnl Copyright © 2012-2015 Dan Nicholson dnl dnl This program is free software; you can redistribute it and/or modify dnl it under the terms of the GNU General Public License as published by dnl the Free Software Foundation; either version 2 of the License, or dnl (at your option) any later version. dnl dnl This program is distributed in the hope that it will be useful, but dnl WITHOUT ANY WARRANTY; without even the implied warranty of dnl MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU dnl General Public License for more details. dnl dnl You should have received a copy of the GNU General Public License dnl along with this program; if not, write to the Free Software dnl Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA dnl 02111-1307, USA. dnl dnl As a special exception to the GNU General Public License, if you dnl distribute this file as part of a program that contains a dnl configuration script generated by Autoconf, you may include it under dnl the same distribution terms that you use for the rest of that dnl program. dnl PKG_PREREQ(MIN-VERSION) dnl ----------------------- dnl Since: 0.29 dnl dnl Verify that the version of the pkg-config macros are at least dnl MIN-VERSION. Unlike PKG_PROG_PKG_CONFIG, which checks the user's dnl installed version of pkg-config, this checks the developer's version dnl of pkg.m4 when generating configure. dnl dnl To ensure that this macro is defined, also add: dnl m4_ifndef([PKG_PREREQ], dnl [m4_fatal([must install pkg-config 0.29 or later before running autoconf/autogen])]) dnl dnl See the "Since" comment for each macro you use to see what version dnl of the macros you require. m4_defun([PKG_PREREQ], [m4_define([PKG_MACROS_VERSION], [0.29]) m4_if(m4_version_compare(PKG_MACROS_VERSION, [$1]), -1, [m4_fatal([pkg.m4 version $1 or higher is required but ]PKG_MACROS_VERSION[ found])]) ])dnl PKG_PREREQ dnl PKG_PROG_PKG_CONFIG([MIN-VERSION]) dnl ---------------------------------- dnl Since: 0.16 dnl dnl Search for the pkg-config tool and set the PKG_CONFIG variable to dnl first found in the path. Checks that the version of pkg-config found dnl is at least MIN-VERSION. If MIN-VERSION is not specified, 0.9.0 is dnl used since that's the first version where most current features of dnl pkg-config existed. AC_DEFUN([PKG_PROG_PKG_CONFIG], [m4_pattern_forbid([^_?PKG_[A-Z_]+$]) m4_pattern_allow([^PKG_CONFIG(_(PATH|LIBDIR|SYSROOT_DIR|ALLOW_SYSTEM_(CFLAGS|LIBS)))?$]) m4_pattern_allow([^PKG_CONFIG_(DISABLE_UNINSTALLED|TOP_BUILD_DIR|DEBUG_SPEW)$]) AC_ARG_VAR([PKG_CONFIG], [path to pkg-config utility]) AC_ARG_VAR([PKG_CONFIG_PATH], [directories to add to pkg-config's search path]) AC_ARG_VAR([PKG_CONFIG_LIBDIR], [path overriding pkg-config's built-in search path]) if test "x$ac_cv_env_PKG_CONFIG_set" != "xset"; then AC_PATH_TOOL([PKG_CONFIG], [pkg-config]) fi if test -n "$PKG_CONFIG"; then _pkg_min_version=m4_default([$1], [0.9.0]) AC_MSG_CHECKING([pkg-config is at least version $_pkg_min_version]) if $PKG_CONFIG --atleast-pkgconfig-version $_pkg_min_version; then AC_MSG_RESULT([yes]) else AC_MSG_RESULT([no]) PKG_CONFIG="" fi fi[]dnl ])dnl PKG_PROG_PKG_CONFIG dnl PKG_CHECK_EXISTS(MODULES, [ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND]) dnl ------------------------------------------------------------------- dnl Since: 0.18 dnl dnl Check to see whether a particular set of modules exists. Similar to dnl PKG_CHECK_MODULES(), but does not set variables or print errors. dnl dnl Please remember that m4 expands AC_REQUIRE([PKG_PROG_PKG_CONFIG]) dnl only at the first occurence in configure.ac, so if the first place dnl it's called might be skipped (such as if it is within an "if", you dnl have to call PKG_CHECK_EXISTS manually AC_DEFUN([PKG_CHECK_EXISTS], [AC_REQUIRE([PKG_PROG_PKG_CONFIG])dnl if test -n "$PKG_CONFIG" && \ AC_RUN_LOG([$PKG_CONFIG --exists --print-errors "$1"]); then m4_default([$2], [:]) m4_ifvaln([$3], [else $3])dnl fi]) dnl _PKG_CONFIG([VARIABLE], [COMMAND], [MODULES]) dnl --------------------------------------------- dnl Internal wrapper calling pkg-config via PKG_CONFIG and setting dnl pkg_failed based on the result. m4_define([_PKG_CONFIG], [if test -n "$$1"; then pkg_cv_[]$1="$$1" elif test -n "$PKG_CONFIG"; then PKG_CHECK_EXISTS([$3], [pkg_cv_[]$1=`$PKG_CONFIG --[]$2 "$3" 2>/dev/null` test "x$?" != "x0" && pkg_failed=yes ], [pkg_failed=yes]) else pkg_failed=untried fi[]dnl ])dnl _PKG_CONFIG dnl _PKG_SHORT_ERRORS_SUPPORTED dnl --------------------------- dnl Internal check to see if pkg-config supports short errors. AC_DEFUN([_PKG_SHORT_ERRORS_SUPPORTED], [AC_REQUIRE([PKG_PROG_PKG_CONFIG]) if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then _pkg_short_errors_supported=yes else _pkg_short_errors_supported=no fi[]dnl ])dnl _PKG_SHORT_ERRORS_SUPPORTED dnl PKG_CHECK_MODULES(VARIABLE-PREFIX, MODULES, [ACTION-IF-FOUND], dnl [ACTION-IF-NOT-FOUND]) dnl -------------------------------------------------------------- dnl Since: 0.4.0 dnl dnl Note that if there is a possibility the first call to dnl PKG_CHECK_MODULES might not happen, you should be sure to include an dnl explicit call to PKG_PROG_PKG_CONFIG in your configure.ac AC_DEFUN([PKG_CHECK_MODULES], [AC_REQUIRE([PKG_PROG_PKG_CONFIG])dnl AC_ARG_VAR([$1][_CFLAGS], [C compiler flags for $1, overriding pkg-config])dnl AC_ARG_VAR([$1][_LIBS], [linker flags for $1, overriding pkg-config])dnl pkg_failed=no AC_MSG_CHECKING([for $1]) _PKG_CONFIG([$1][_CFLAGS], [cflags], [$2]) _PKG_CONFIG([$1][_LIBS], [libs], [$2]) m4_define([_PKG_TEXT], [Alternatively, you may set the environment variables $1[]_CFLAGS and $1[]_LIBS to avoid the need to call pkg-config. See the pkg-config man page for more details.]) if test $pkg_failed = yes; then AC_MSG_RESULT([no]) _PKG_SHORT_ERRORS_SUPPORTED if test $_pkg_short_errors_supported = yes; then $1[]_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "$2" 2>&1` else $1[]_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "$2" 2>&1` fi # Put the nasty error message in config.log where it belongs echo "$$1[]_PKG_ERRORS" >&AS_MESSAGE_LOG_FD m4_default([$4], [AC_MSG_ERROR( [Package requirements ($2) were not met: $$1_PKG_ERRORS Consider adjusting the PKG_CONFIG_PATH environment variable if you installed software in a non-standard prefix. _PKG_TEXT])[]dnl ]) elif test $pkg_failed = untried; then AC_MSG_RESULT([no]) m4_default([$4], [AC_MSG_FAILURE( [The pkg-config script could not be found or is too old. Make sure it is in your PATH or set the PKG_CONFIG environment variable to the full path to pkg-config. _PKG_TEXT To get pkg-config, see .])[]dnl ]) else $1[]_CFLAGS=$pkg_cv_[]$1[]_CFLAGS $1[]_LIBS=$pkg_cv_[]$1[]_LIBS AC_MSG_RESULT([yes]) $3 fi[]dnl ])dnl PKG_CHECK_MODULES dnl PKG_CHECK_MODULES_STATIC(VARIABLE-PREFIX, MODULES, [ACTION-IF-FOUND], dnl [ACTION-IF-NOT-FOUND]) dnl --------------------------------------------------------------------- dnl Since: 0.29 dnl dnl Checks for existence of MODULES and gathers its build flags with dnl static libraries enabled. Sets VARIABLE-PREFIX_CFLAGS from --cflags dnl and VARIABLE-PREFIX_LIBS from --libs. dnl dnl Note that if there is a possibility the first call to dnl PKG_CHECK_MODULES_STATIC might not happen, you should be sure to dnl include an explicit call to PKG_PROG_PKG_CONFIG in your dnl configure.ac. AC_DEFUN([PKG_CHECK_MODULES_STATIC], [AC_REQUIRE([PKG_PROG_PKG_CONFIG])dnl _save_PKG_CONFIG=$PKG_CONFIG PKG_CONFIG="$PKG_CONFIG --static" PKG_CHECK_MODULES($@) PKG_CONFIG=$_save_PKG_CONFIG[]dnl ])dnl PKG_CHECK_MODULES_STATIC dnl PKG_INSTALLDIR([DIRECTORY]) dnl ------------------------- dnl Since: 0.27 dnl dnl Substitutes the variable pkgconfigdir as the location where a module dnl should install pkg-config .pc files. By default the directory is dnl $libdir/pkgconfig, but the default can be changed by passing dnl DIRECTORY. The user can override through the --with-pkgconfigdir dnl parameter. AC_DEFUN([PKG_INSTALLDIR], [m4_pushdef([pkg_default], [m4_default([$1], ['${libdir}/pkgconfig'])]) m4_pushdef([pkg_description], [pkg-config installation directory @<:@]pkg_default[@:>@]) AC_ARG_WITH([pkgconfigdir], [AS_HELP_STRING([--with-pkgconfigdir], pkg_description)],, [with_pkgconfigdir=]pkg_default) AC_SUBST([pkgconfigdir], [$with_pkgconfigdir]) m4_popdef([pkg_default]) m4_popdef([pkg_description]) ])dnl PKG_INSTALLDIR dnl PKG_NOARCH_INSTALLDIR([DIRECTORY]) dnl -------------------------------- dnl Since: 0.27 dnl dnl Substitutes the variable noarch_pkgconfigdir as the location where a dnl module should install arch-independent pkg-config .pc files. By dnl default the directory is $datadir/pkgconfig, but the default can be dnl changed by passing DIRECTORY. The user can override through the dnl --with-noarch-pkgconfigdir parameter. AC_DEFUN([PKG_NOARCH_INSTALLDIR], [m4_pushdef([pkg_default], [m4_default([$1], ['${datadir}/pkgconfig'])]) m4_pushdef([pkg_description], [pkg-config arch-independent installation directory @<:@]pkg_default[@:>@]) AC_ARG_WITH([noarch-pkgconfigdir], [AS_HELP_STRING([--with-noarch-pkgconfigdir], pkg_description)],, [with_noarch_pkgconfigdir=]pkg_default) AC_SUBST([noarch_pkgconfigdir], [$with_noarch_pkgconfigdir]) m4_popdef([pkg_default]) m4_popdef([pkg_description]) ])dnl PKG_NOARCH_INSTALLDIR dnl PKG_CHECK_VAR(VARIABLE, MODULE, CONFIG-VARIABLE, dnl [ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND]) dnl ------------------------------------------- dnl Since: 0.28 dnl dnl Retrieves the value of the pkg-config variable for the given module. AC_DEFUN([PKG_CHECK_VAR], [AC_REQUIRE([PKG_PROG_PKG_CONFIG])dnl AC_ARG_VAR([$1], [value of $3 for $2, overriding pkg-config])dnl _PKG_CONFIG([$1], [variable="][$3]["], [$2]) AS_VAR_COPY([$1], [pkg_cv_][$1]) AS_VAR_IF([$1], [""], [$5], [$4])dnl ])dnl PKG_CHECK_VAR onedrive-2.5.5/changelog.md000066400000000000000000002077401476564400300156320ustar00rootroot00000000000000# Changelog The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## 2.5.5 - 2025-03-17 ### Added * Implement Feature Request: Implement 'transfer_order' configuration option to allow the user to determine what order files are transferred in * Implement Feature Request: Implement 'disable_permission_set' configuration option to not set directory and file permissions * Implement Feature Request: Implement 'write_xattr_data' configuration option to add information about file creator/last editor as extended file attributes * Enhancement: Add support for --share-password option when --create-share-link is called * Enhancement: Add support 'localizedMessage' error messages in application output if this is provided in the JSON response from Microsoft Graph API ### Changed * Changed curl debug logging to --debug-https as this is more relevant * Comprehensively overhauled how OneDrive Personal Shared Folders are handled due to major OneDrive API backend platform user migration and major differences in API response output * Comprehensively changed OneDrive Personal 'driveId' value checking due to major OneDrive API backend platform user migration and major differences in API response output ### Fixed * Fix Bug: Fix path calculation for Client Side Filtering evaluations for Personal Accounts * Fix Bug: Fix path calculation for Client Side Filtering evaluations for Business Accounts * Fix Bug: Only perform path calculation if this is actually required * Fix Bug: Fix check for 'globbing' and 'wildcard' rules, that the number of segments before the first wildcard character need to match before the actual rule can be applied * Fix Bug: When using 'sync_list' , ignore specific exclusion to scan that path for new data, which may be actually included by an include rule, but the parent path is excluded * Fix Bug: When removing a OneDrive Personal Shared Folder, remove the actual link, not the remote user folder * Fix Bug: Fix 'Unsupported platform' for inotify watches by using the correct predefined version definition for Linux. ### Updated * Updated Fedora Docker OS version to Fedora 41 * Updated Alpine Docker OS version to Alpine 3.21 * Updated documentation ## 2.5.4 - 2025-02-03 ### Added * Implement Feature Request: Support Permanent Delete on OneDrive * Implement Feature Request: Support the moving of Shared Folder Links to other folders (Business Accounts only) * Enhancement: Added due to ongoing Ubuntu issues with 'curl' and 'libcurl', updated the documentation to include all relevant curl bugs and affected versions * Enhancement: Added quota status messages for nearing | critical | exceeded based on OneDrive Account API response * Enhancement: Added Docker variable to implement a sync once option * Enhancement: Added configuration option 'create_new_file_version' to force create new versions if that is the desire * Enhancement: Added support for adding SharePoint Libraries as Shared Folder Links * Enhancement: Added code and documentation changes to support FreeBSD * Enhancement: Added a check for the 'sea8cc6beffdb43d7976fbc7da445c639' string in the Microsoft OneDrive Personal Account Root ID response that denotes that the account cannot access Microsoft OneDrive at this point in time * Enhancement: Added './' sync_list rule check as this does not align to the documentation and these rules will not get matched correctly. ### Changed * Changed how debug logging outputs HTTP response headers and when this occurs * Changed when the check for no --sync | --monitor occurs so that this fails faster to avoid setting up all the other components * Changed isValidUTF8 function to use 'validate' rather than individual character checking and enhance checks including length constraints * Changed --dry-run authentication message to remove ambiguity that --dry-run cannot be used to authenticate the application ### Fixed * Fix Regression: Fixed regression that sync_list does not traverse shared directories * Fix Regression: Fixed regression of --display-config use after fast failing if --sync or --monitor has not been used * Fix Regression: Fixed regression from v2.4.x in handling uploading new and modified content to OneDrive Business and SharePoint to not create new versions of files post upload which adds to user quota * Fix Regression: Add back file transfer metrics which was available in v2.4.x * Fix Regression: Add code to support using 'display_processing_time' for functional performance which was available in v2.4.x * Fix Bug: Fixed build issue for OpenBSD (however support for OpenBSD itself is still a work-in-progress) * Fix Bug: Fixed issue regarding parsing OpenSSL and when unable to be parsed, do not force the application to exit * Fix Bug: Fixed the import of 'sync_list' rules due to OneDriveGUI creating a blank empty file by default * Fix Bug: Fixed the display of 'sync_list' rules due to OneDriveGUI creating a blank empty file by default * Fix Bug: Fixed that Business Shared Items shortcuts are skipped as being incorrectly detected as Microsoft OneNote Notebook items * Fix Bug: Fixed space calculations due to using ulong variable type to ensure that if calculation is negative, value is negative * Fix Bug: Fixed issue when downloading a file, and this fails due to an API error (400, 401, 5xx), online file is then not deleted * Fix Bug: Fixed skip_dir logic when reverse traversing folder structure * Fix Bug: Fixed issue that when using 'sync_list' if a file is moved to a newly created online folder, whilst the folder is created database wise, ensure this folder exists on local disk * Fix Bug: Fixed path got deleted in handling of move & close_write event when using 'vim'. * Fix Bug: Fixed that the root Personal Shared Folder is not handled due to missing API data European Data Centres * Fix Bug: Fixed the the local timestamp is not set when using --disable-download-validation * Fix Bug: Fixed Upload|Download Loop for AIP Protected File in Monitor Mode * Fix Bug: Fixed --single-directory Shared Folder DB entry creation * Fix Bug: Fixed API Bug to ensure that OneDrive Personal Drive ID and Remote Drive ID values are 16 characters, padded by leading zeros if the provided JSON data has dropped these leading zeros * Fix Bug: Fixed testInternetReachability function so that this always returns a boolean value and not throw an exception ### Updated * Updated documentation ## 2.5.3 - 2024-11-16 ### Added * Implement Feature Request: Implement Docker ENV variable for --cleanup-local-files * Enhancement: Setup a specific SIGPIPE Signal handler for curl/openssl generated signals * Enhancement: Add Check Spelling GitHub Action * Enhancement: Add passive database checkpoints to optimise database operations * Enhancement: Ensure application notifies user of curl versions that contain HTTP/2 bugs that impact the operation of this client * Enhancement: Add OpenSSL version warning * Enhancement: Improve performance with reduced execution time and lower CPU/system resource usage ### Changed * Specifically use a 'mutex' to perform the lock on database actions * Update safeBackup to use a new filename format for easier identification: filename-hostname-safeBackup-number.file_extension * Allow no-sync operations to complete online account checks ### Fixed * Fix Regression: Fix regression for Docker 'sync_dir' use * Fix Bug: Fix that a 'sync_list' entry of '/' will cause a index [0] is out of bounds * Fix Bug: Fix that when creating a new folder online the application generates an exception if it is in a Shared Online Folder * Fix Bug: Fix application crash when session upload files contain zero data or are corrupt * Fix Bug: Fix that curl generates a SIGPIPE that causes application to exit due to upstream device killing idle TCP connection * Fix Bug: Fix that skip_dir is not flagging directories correctly causing deletion if parental path structure needs to be created for sync_list handling * Fix Bug: Fix application crash caused by unable to drop table * Fix Bug: Fix that skip_file in config does not override defaults * Fix Bug: Handle DB upgrades from v2.4.x without causing application crash * Fix Bug: Fix a database statement execution error occurred: NOT NULL constraint failed: item.type due to Microsoft OneNote items * Fix Bug: Fix Operation not permitted FileException Error when attempting to use setTimes() function * Fix Bug: Fix that files with no mime type cause sync to crash * Fix Bug: Fix that bypass_data_preservation operates as intended ### Updated * Fixed spelling errors across all documentation and code * Update Dockerfile-debian to fix that libcurl4 does not get applied despite being pulled in. Explicitly install it from Debian 12 Backports * Add Ubuntu 24.10 OpenSuSE Build Service details * Update Dockerfile-alpine - revert to Alpine 3.19 as application fails to run on Alpine 3.20 * Updated documentation ## 2.5.2 - 2024-09-29 ### Added * Added 15 second sleep to systemd services to allow d-bus daemon to start and be available if present ### Fixed * Fix Bug: Application crash unable to correctly process a timestamp that has fractional seconds * Fix Bug: Fixed application logging output of Personal Shared Folder incorrectly advising there is no free space ### Updated * Updated documentation ## 2.5.1 - 2024-09-27 (DO NOT USE. CONTAINS A MAJOR TIMESTAMP ISSUE BUG) ### Special Thankyou A special thankyou to @phlibi for assistance with diagnosing and troubleshooting the database timestamp issue ### Added * Implement Feature Request: Don't print the d-bus WARNING if disable_notifications is set on cmd line or in config ### Changed * Add --enable-debug to Docker files when building client application to allow for better diagnostics when issues occur * Update Debian Dockerfile to use 'curl' from backports so a more modern curl version is used ### Fixed * Fix Regression: Fix regression of extra quotation marks when using ONEDRIVE_SINGLE_DIRECTORY with Docker * Fix Regression: Fix regression that real-time synchronization is not occurring when using --monitor and sync_list * Fix Regression: Fix regression that --remove-source-files doesn’t work * Fix Bug: Application crash when run synchronize due to negative free space online * Fix Bug: Application crash when performing a URL decode * Fix Bug: Application crash when using sync_list and Personal Shared Folders the root folder fails to present the item id * Fix Bug: Application crash when attempting to read timestamp from database as invalid data was written ### Updated * Updated documentation (various) ## 2.5.0 - 2024-09-16 ### Special Thankyou A special thankyou to all those who helped with testing and providing feedback during the development of this major release. A big thankyou to: * @JC-comp * @Lyncredible * @rrodrigueznt * @bpozdena * @hskrieg * @robertschulze * @aothmane-control * @mozram * @LunCh-CECNL * @pkolmann * @tdcockers * @undefiened * @cyb3rko ### Notable Changes * This version introduces significant changes regarding how the integrity and validation of your data is determined and is not backwards compatible with v2.4.x. * OneDrive Business Shared Folder Sync has been 100% re-written in v2.5.0. If you are using this feature, please read the new documentation carefully. * The application function --download-only no longer automatically deletes local files. Please read the new documentation regarding this feature. ### Added * Implement Feature Request: Multi-threaded uploading/downloading of files * Implement Feature Request: Renaming/Relocation of OneDrive Business shared folders * Implement Feature Request: Support the syncing of individual business shared files * Implement Feature Request: Implement application output to detail upload|download failures at the end of a sync process * Implement Feature Request: Log when manual Authorization is required when using --auth-files * Implement Feature Request: Add cmdline parameter to display (human readable) quota status * Implement Feature Request: Add capability to disable 'fullscan_frequency' * Implement Feature Request: Ability to set --disable-download-validation from Docker environment variable * Implement Feature Request: Ability to set --sync-shared-files from Docker environment variable * Implement Feature Request: file sync (upload/download/delete) notifications ### Changed * Renamed various documentation files to align with document content * Implement buffered logging so that all logging from all upload & download activities are handled correctly * Replace polling monitor loop with blocking wait * Update how the application utilises curl to fix socket reuse * Various performance enhancements * Implement refactored OneDrive API logic * Enforcement of operational conflicts * Enforcement of application configuration defaults and minimums * Utilise threadsafe sqlite DB access methods * Various bugs and other issues identified during development and testing * Various code cleanup and optimisations ### Fixed * Fix Bug: Upload only not working with Business shared folders * Fix Bug: Business shared folders with same basename get merged * Fix Bug: --dry-run prevents authorization * Fix Bug: Log timestamps lacking trailing zeros, leading to poor log file output alignment * Fix Bug: Subscription ID already exists when using webhooks * Fix Bug: Not all files being downloaded when API data includes HTML ASCII Control Sequences * Fix Bug: --display-sync-status does not work when OneNote sections (.one files) are in your OneDrive * Fix Bug: vim backups when editing files cause edited file to be deleted rather than the edited file being uploaded * Fix Bug: skip_dir does not always work as intended for all directory entries * Fix Bug: Online date being changed in download-only mode * Fix Bug: Resolve that download_only = "true" and cleanup_local_files = "true" also deletes files present online * Fix Bug: Resolve that upload session are not canceled with resync option * Fix Bug: Local files should be safely backed up when the item is not in sync locally to prevent data loss when they are deleted online * Fix Bug: Files with newer timestamp are not chosen as version to be kept * Fix Bug: Synced file is removed when updated on the remote while being processed by onedrive * Fix Bug: Cannot select/filter within Personal Shared Folders * Fix Bug: HTML encoding requires to add filter entries twice * Fix Bug: Uploading files using fragments stuck at 0% * Fix Bug: Implement safeguard when sync_dir is missing and is re-created data is not deleted online * Fix Bug: Fix that --get-sharepoint-drive-id does not handle a SharePoint site with more than 200 entries * Fix Bug: Fix that 'sync_list' does not include files that should be included, when specified just as *.ext_type * Fix Bug: Fix 'sync_list' processing so that '.folder_name' is excluded but 'folder_name' is included ### Updated * Overhauled all documentation ## 2.4.25 - 2023-06-21 ### Fixed * Fixed that the application was reporting as v2.2.24 when in fact it was v2.4.24 (release tagging issue) * Fixed that the running version obsolete flag (due to above issue) was causing a false flag as being obsolete * Fixed that zero-byte files do not have a hash as reported by the OneDrive API thus should not generate an error message ### Updated * Update to Debian Docker file to resolve Docker image Operating System reported vulnerabilities * Update to Alpine Docker file to resolve Docker image Operating System reported vulnerabilities * Update to Fedora Docker file to resolve Docker image Operating System reported vulnerabilities * Updated documentation (various) ## 2.4.24 - 2023-06-20 ### Fixed * Fix for extra encoded quotation marks surrounding Docker environment variables * Fix webhook subscription creation for SharePoint Libraries * Fix that a HTTP 504 - Gateway Timeout causes local files to be deleted when using --download-only & --cleanup-local-files mode * Fix that folders are renamed despite using --dry-run * Fix deprecation warnings with dmd 2.103.0 * Fix error that the application is unable to perform a database vacuum: out of memory when exiting ### Removed * Remove sha1 from being used by the client as this is being deprecated by Microsoft in July 2023 * Complete the removal of crc32 elements ### Added * Added ONEDRIVE_SINGLE_DIRECTORY configuration capability to Docker * Added --get-file-link shell completion * Added configuration to allow HTTP session timeout(s) tuning via config (taken from v2.5.x) ### Updated * Update to Debian Docker file to resolve Docker image Operating System reported vulnerabilities * Update to Alpine Docker file to resolve Docker image Operating System reported vulnerabilities * Update to Fedora Docker file to resolve Docker image Operating System reported vulnerabilities * Updated cgi.d to commit 680003a - last upstream change before requiring `core.d` dependency requirement * Updated documentation (various) ## 2.4.23 - 2023-01-06 ### Fixed * Fixed RHEL7, RHEL8 and RHEL9 Makefile and SPEC file compatibility ### Removed * Disable systemd 'PrivateUsers' due to issues with systemd running processes when option is enabled, causes local file deletes on RHEL based systems ### Updated * Update --get-O365-drive-id error handling to display a more a more appropriate error message if the API cannot be found * Update the GitHub version check to utilise the date a release was done, to allow 1 month grace period before generating obsolete version message * Update Alpine Dockerfile to use Alpine 3.17 and Golang 1.19 * Update handling of --source-directory and --destination-directory if one is empty or missing and if used with --synchronize or --monitor * Updated documentation (various) ## 2.4.22 - 2022-12-06 ### Fixed * Fix application crash when local file is changed to a symbolic link with non-existent target * Fix build error with dmd-2.101.0 * Fix build error with LDC 1.28.1 on Alpine * Fix issue of silent exit when unable to delete local files when using --cleanup-local-files * Fix application crash due to access permissions on configured path for sync_dir * Fix potential application crash when exiting due to failure state and unable to cleanly shutdown the database * Fix creation of parent empty directories when parent is excluded by sync_list ### Added * Added performance output details for key functions ### Changed * Switch Docker 'latest' to point at Debian builds rather than Fedora due to ongoing Fedora build failures * Align application logging events to actual application defaults for --monitor operations * Performance Improvement: Avoid duplicate costly path calculations and DB operations if not required * Disable non-working remaining sandboxing options within systemd service files * Performance Improvement: Only check 'sync_list' if this has been enabled and configured * Display 'Sync with OneDrive is complete' when using --synchronize * Change the order of processing between Microsoft OneDrive restrictions and limitations check and skip_file|skip_dir check ### Removed * Remove building Fedora ARMv7 builds due to ongoing build failures ### Updated * Update config change detection handling * Updated documentation (various) ## 2.4.21 - 2022-09-27 ### Fixed * Fix that the download progress bar doesn't always reach 100% when rate_limit is set * Fix --resync handling of database file removal * Fix Makefile to be consistent with permissions that are being used * Fix that logging output for skipped uploaded files is missing * Fix to allow non-sync tasks while sync is running * Fix where --resync is enforced for non-sync operations * Fix to resolve segfault when running 'onedrive --display-sync-status' when run as 2nd process * Fix DMD 2.100.2 depreciation warning ### Added * Add GitHub Action Test Build Workflow (replacing Travis CI) * Add option --display-running-config to display the running configuration as used at application startup * Add 'config' option to request readonly access in oauth authorization step * Add option --cleanup-local-files to cleanup local files regardless of sync state when using --download-only * Add option --with-editing-perms to create a read-write shareable link when used with --create-share-link ### Changed * Change the exit code of the application to 126 when a --resync is required ### Updated * Updated --get-O365-drive-id implementation for data access * Update what application options require an argument * Update application logging output for error messages to remove certain \n prefix when logging to a file * Update onedrive.spec.in to fix error building RPM * Update GUI notification handling for specific skipped scenarios * Updated documentation (various) ## 2.4.20 - 2022-07-20 ### Fixed * Fix 'foreign key constraint failed' when using OneDrive Business Shared Folders due to change to using /delta query * Fix various little spelling errors (checked with lintian during Debian packaging) * Fix handling of a custom configuration directory when using --confdir * Fix to ensure that any active http instance is shutdown before any application exit * Fix to enforce that --confdir must be a directory ### Added * Added 'force_http_11' configuration option to allow forcing HTTP/1.1 operations ### Changed * Increased thread sleep for better process I/O wait handling * Removed 'force_http_2' configuration option ### Updated * Update OneDrive API response handling for National Cloud Deployments * Updated to switch to using curl defaults for HTTP/2 operations * Updated documentation (various) ## 2.4.19 - 2022-06-15 ### Fixed * Update Business Shared Folders to use a /delta query * Update when DB is updated by OneDrive API data and update when file hash is required to be generated ### Added * Added ONEDRIVE_UPLOADONLY flag for Docker ### Updated * Updated GitHub workflows * Updated documentation (various) ## 2.4.18 - 2022-06-02 ### Fixed * Fixed various database related access issues stemming from running multiple instances of the application at the same time using the same configuration data * Fixed --display-config being impacted by --resync flag * Fixed installation permissions for onedrive man-pages file * Fixed that in some situations that users try --upload-only and --download-only together which is not possible * Fixed application crash if unable to read required hash files ### Added * Added Feature Request to add an override for skip_dir|skip_file through flag to force sync * Added a check to validate local filesystem available space before attempting file download * Added GitHub Actions to build Docker containers and push to DockerHub ### Updated * Updated all Docker build files to current distributions, using updated distribution LDC version * Updated logging output to logfiles when an actual sync process is occurring * Updated output of --display-config to be more relevant * Updated manpage to align with application configuration * Updated documentation and Docker files based on minimum compiler versions to dmd-2.088.0 and ldc-1.18.0 * Updated documentation (various) ## 2.4.17 - 2022-04-30 ### Fixed * Fix docker build, by add missing git package for Fedora builds * Fix application crash when attempting to sync a broken symbolic link * Fix Internet connect disruption retry handling and logging output * Fix local folder creation timestamp with timestamp from OneDrive * Fix logging output when download failed ### Added * Add additional logging specifically for delete event to denote in log output the source of a deletion event when running in --monitor mode ### Changed * Improve when the local database integrity check is performed and on what frequency the database integrity check is performed ### Updated * Remove application output ambiguity on how to access 'help' for the client * Update logging output when running in --monitor --verbose mode in regards to the inotify events * Updated documentation (various) ## 2.4.16 - 2022-03-10 ### Fixed * Update application file logging error handling * Explicitly set libcurl options * Fix that when a sync_list exclusion is matched, the item needs to be excluded when using --resync * Fix so that application can be compiled correctly on Android hosts * Fix the handling of 429 and 5xx responses when they are generated by OneDrive in a self-referencing circular pattern * Fix applying permissions to volume directories when running in rootless podman * Fix unhandled errors from OneDrive when initialising subscriptions fail ### Added * Enable GitHub Sponsors * Implement --resync-auth to enable CLI passing in of --rsync approval * Add function to check client version vs latest GitHub release * Add --reauth to allow easy re-authentication of the client * Implement --modified-by to display who last modified a file and when the modification was done * Implement feature request to mark partially-downloaded files as .partial during download * Add documentation for Podman support ### Changed * Document risk regarding using --resync and force user acceptance of usage risk to proceed * Use YAML for Bug Reports and Feature Requests * Update Dockerfiles to use more modern base Linux distribution ### Updated * Updated documentation (various) ## 2.4.15 - 2021-12-31 ### Fixed * Fix unable to upload to OneDrive Business Shared Folders due to OneDrive API restricting quota information * Update fixing edge case with OneDrive Personal Shared Folders and --resync --upload-only ### Added * Add SystemD hardening * Add --operation-timeout argument ### Changed * Updated minimum compiler versions to dmd-2.087.0 and ldc-1.17.0 ### Updated * Updated Dockerfile-alpine to use Alpine 3.14 * Updated documentation (various) ## 2.4.14 - 2021-11-24 ### Fixed * Support DMD 2.097.0 as compiler for Docker Builds * Fix getPathDetailsByDriveId query when using --dry-run and a nested path with --single-directory * Fix edge case when syncing OneDrive Personal Shared Folders * Catch unhandled API response errors when querying OneDrive Business Shared Folders * Catch unhandled API response errors when listing OneDrive Business Shared Folders * Fix error 'Key not found: remaining' with Business Shared Folders (OneDrive API change) * Fix overwriting local files with older versions from OneDrive when items.sqlite3 does not exist and --resync is not used ### Added * Added operation_timeout as a new configuration to assist in cases where operations take longer that 1h to complete * Add Real-Time syncing of remote updates via webhooks * Add --auth-response option and expose through entrypoint.sh for Docker * Add --disable-download-validation ### Changed * Always prompt for credentials for authentication rather than re-using cached browser details * Do not re-auth on --logout ### Updated * Updated documentation (various) ## 2.4.13 - 2021-7-14 ### Fixed * Support DMD 2.097.0 as compiler * Fix to handle OneDrive API Bad Request response when querying if file exists * Fix application crash and incorrect handling of --single-directory when syncing a OneDrive Business Shared Folder due to using 'Add Shortcut to My Files' * Fix application crash due to invalid UTF-8 sequence in the pathname for the application configuration * Fix error message when deleting a large number of files * Fix Docker build process to source GOSU keys from updated GPG key location * Fix application crash due to a conversion overflow when calculating file offset for session uploads * Fix Docker Alpine build failing due to filesystem permissions issue due to Docker build system and Alpine Linux 3.14 incompatibility * Fix that Business Shared Folders with parentheses are ignored ### Updated * Updated Lock Bot to run daily * Updated documentation (various) ## 2.4.12 - 2021-5-28 ### Fixed * Fix an unhandled Error 412 when uploading modified files to OneDrive Business Accounts * Fix 'sync_list' handling of inclusions when name is included in another folders name * Fix that options --upload-only & --remove-source-files are ignored on an upload session restore * Fix to add file check when adding item to database if using --upload-only --remove-source-files * Fix application crash when SharePoint displayName is being withheld ### Updated * Updated Lock Bot to use GitHub Actions * Updated documentation (various) ## 2.4.11 - 2021-4-07 ### Fixed * Fix support for '/*' regardless of location within sync_list file * Fix 429 response handling correctly check for 'retry-after' response header and use set value * Fix 'sync_list' path handling for sub item matching, so that items in parent are not implicitly matched when there is no wildcard present * Fix --get-O365-drive-id to use 'nextLink' value if present when searching for specific SharePoint site names * Fix OneDrive Business Shared Folder existing name conflict check * Fix incorrect error message 'Item cannot be deleted from OneDrive because it was not found in the local database' when item is actually present * Fix application crash when unable to rename folder structure due to unhandled file-system issue * Fix uploading documents to Shared Business Folders when the shared folder exists on a SharePoint site due to Microsoft Sharepoint 'enrichment' of files * Fix that a file record is kept in database when using --no-remote-delete & --remove-source-files ### Added * Added support in --get-O365-drive-id to provide the 'drive_id' for multiple 'document libraries' within a single Shared Library Site ### Removed * Removed the deprecated config option 'force_http_11' which was flagged as deprecated by PR #549 in v2.3.6 (June 2019) ### Updated * Updated error output of --get-O365-drive-id to provide more details why an error occurred if a SharePoint site lacks the details we need to perform the match * Updated Docker build files for Raspberry Pi to dedicated armhf & aarch64 Dockerfiles * Updated logging output when in --monitor mode, avoid outputting misleading logging when the new or modified item is a file, not a directory * Updated documentation (various) ## 2.4.10 - 2021-2-19 ### Fixed * Catch database assertion when item path cannot be calculated * Fix alpine Docker build so it uses the same golang alpine version * Search all distinct drive id's rather than just default drive id for --get-file-link * Use correct driveId value to query for changes when using --single-directory * Improve upload handling of files for SharePoint sites and detecting when SharePoint modifies the file post upload * Correctly handle '~' when present in 'log_dir' configuration option * Fix logging output when handing downloaded new files * Fix to use correct path offset for sync_list exclusion matching ### Added * Add upload speed metrics when files are uploaded and clarify that 'data to transfer' is what is needed to be downloaded from OneDrive * Add new config option to rate limit connection to OneDrive * Support new file maximum upload size of 250GB * Support sync_list matching full path root wildcard with exclusions to simplify sync_list configuration ### Updated * Rename Office365.md --> SharePoint-Shared-Libraries.md which better describes this document * Updated Dockerfile config for arm64 * Updated documentation (various) ## 2.4.9 - 2020-12-27 ### Fixed * Fix to handle case where API provided deltaLink generates a further API error * Fix application crash when unable to read a local file due to local file permissions * Fix application crash when calculating the path length due to invalid UTF characters in local path * Fix Docker build on Alpine due missing symbols due to using the edge version of ldc and ldc-runtime * Fix application crash with --get-O365-drive-id when API response is restricted ### Added * Add debug log output of the configured URL's which will be used throughout the application to remove any ambiguity as to using incorrect URL's when making API calls * Improve application startup when using --monitor when there is no network connection to the OneDrive API and only initialise application once OneDrive API is reachable * Add Docker environment variable to allow --logout for re-authentication ### Updated * Remove duplicate code for error output functions and enhance error logging output * Updated documentation ## 2.4.8 - 2020-11-30 ### Fixed * Fix to use config set option for 'remove_source_files' and 'skip_dir_strict_match' rather than ignore if set * Fix download failure and crash due to incorrect local filesystem permissions when using mounted external devices * Fix to not change permissions on pre-existing local directories * Fix logging output when authentication authorisation fails to not say authorisation was successful * Fix to check application_id before setting redirect URL when using specific Azure endpoints * Fix application crash in --monitor mode due to 'Failed to stat file' when setgid is used on a directory and data cannot be read ### Added * Added advanced-usage.md to document advanced client usage such as multi account configurations and Windows dual-boot ### Updated * Updated --verbose logging output for config options when set * Updated documentation (man page, USAGE.md, Office365.md, BusinessSharedFolders.md) ## 2.4.7 - 2020-11-09 ### Fixed * Fix debugging output for /delta changes available queries * Fix logging output for modification comparison source data * Fix Business Shared Folder handling to process only Shared Folders, not individually shared files * Fix cleanup dryrun shm and wal files if they exist * Fix --list-shared-folders to only show folders * Fix to check for the presence of .nosync when processing DB entries * Fix skip_dir matching when using --resync * Fix uploading data to shared business folders when using --upload-only * Fix to merge contents of SQLite WAL file into main database file on sync completion * Fix to check if localModifiedTime is >= than item.mtime to avoid re-upload for equal modified time * Fix to correctly set config directory permissions at first start ### Added * Added environment variable to allow easy HTTPS debug in docker * Added environment variable to allow download-only mode in Docker * Implement Feature: Allow config to specify a tenant id for non-multi-tenant applications * Implement Feature: Adding support for authentication with single tenant custom applications * Implement Feature: Configure specific File and Folder Permissions ### Updated * Updated documentation (readme.md, install.md, usage.md, bug_report.md) ## 2.4.6 - 2020-10-04 ### Fixed * Fix flagging of remaining free space when value is being restricted * Fix --single-directory path handling when path does not exist locally * Fix checking for 'Icon' path as no longer listed by Microsoft as an invalid file or folder name * Fix removing child items on OneDrive when parent item responds with access denied * Fix to handle deletion events for files when inotify events are missing * Fix uninitialised value error as reported by valgrind * Fix to handle deletion events for directories when inotify events are missing ### Added * Implement Feature: Create shareable link * Implement Feature: Support wildcard within sync_list entries * Implement Feature: Support negative patterns in sync_list for fine grained exclusions * Implement Feature: Multiple skip_dir & skip_file configuration rules * Add GUI notification to advise users when the client needs to be reauthenticated ### Updated * Updated documentation (readme.md, install.md, usage.md, bug_report.md) ## 2.4.5 - 2020-08-13 ### Fixed * Fixed fish auto completions installation destination ## 2.4.4 - 2020-08-11 ### Fixed * Fix 'skip_dir' & 'skip_file' pattern matching to ensure correct matching is performed * Fix 'skip_dir' & 'skip_file' so that each directive is only used against directories or files as required in --monitor * Fix client hand when attempting to sync a Unix pipe file * Fix --single-directory & 'sync_list' performance * Fix erroneous 'return' statements which could prematurely end processing all changes returned from OneDrive * Fix segfault when attempting to perform a comparison on an inotify event when determining if event path is directory or file * Fix handling of Shared Folders to ensure these are checked against 'skip_dir' entries * Fix 'Skipping uploading this new file as parent path is not in the database' when uploading to a Personal Shared Folder * Fix how available free space is tracked when uploading files to OneDrive and Shared Folders * Fix --single-directory handling of parent path matching if path is being seen for first time ### Added * Added Fish auto completions ### Updated * Increase maximum individual file size to 100GB due to Microsoft file limit increase * Update Docker build files and align version of compiler across all Docker builds * Update Docker documentation * Update NixOS build information * Update the 'Processing XXXX' output to display the full path * Update logging output when a sync starts and completes when using --monitor * Update Office 365 / SharePoint site search query and response if query return zero match ## 2.4.3 - 2020-06-29 ### Fixed * Check if symbolic link is relative to location path * When using output logfile, fix inconsistent output spacing * Perform initial sync at startup in monitor mode * Handle a 'race' condition to process inotify events generated whilst performing DB or filesystem walk * Fix segfault when moving folder outside the sync directory when using --monitor on Arch Linux ### Added * Added additional inotify event debugging * Added support for loading system configs if there's no user config * Added Ubuntu installation details to include installing the client from a PPA * Added openSUSE installation details to include installing the client from a package * Added support for comments in sync_list file * Implement recursive deletion when Retention Policy is enabled on OneDrive Business Accounts * Implement support for National cloud deployments * Implement OneDrive Business Shared Folders Support ### Updated * Updated documentation files (various) * Updated log output messaging when a full scan has been set or triggered * Updated buildNormalizedPath complexity to simplify code * Updated to only process OneDrive Personal Shared Folders only if account type is 'personal' ## 2.4.2 - 2020-05-27 ### Fixed * Fixed the catching of an unhandled exception when inotify throws an error * Fixed an uncaught '100 Continue' response when files are being uploaded * Fixed progress bar for uploads to be more accurate regarding percentage complete * Fixed handling of database query enforcement if item is from a shared folder * Fixed compiler depreciation of std.digest.digest * Fixed checking & loading of configuration file sequence * Fixed multiple issues reported by Valgrind * Fixed double scan at application startup when using --monitor & --resync together * Fixed when renaming a file locally, ensure that the target filename is valid before attempting to upload to OneDrive * Fixed so that if a file is modified locally and --resync is used, rename the local file for data preservation to prevent local data loss ### Added * Implement 'bypass_data_preservation' enhancement ### Changed * Changed the monitor interval default to 300 seconds ### Updated * Updated the handling of out-of-space message when OneDrive is out of space * Updated debug logging for retry wait times ## 2.4.1 - 2020-05-02 ### Fixed * Fixed the handling of renaming files to a name starting with a dot when skip_dotfiles = true * Fixed the handling of parentheses from path or file names, when doing comparison with regex * Fixed the handling of renaming dotfiles to another dotfile when skip_dotfile=true in monitor mode * Fixed the handling of --dry-run and --resync together correctly as current database may be corrupt * Fixed building on Alpine Linux under Docker * Fixed the handling of --single-directory for --dry-run and --resync scenarios * Fixed the handling of .nosync directive when downloading new files into existing directories that is (was) in sync * Fixed the handling of zero-byte modified files for OneDrive Business * Fixed skip_dotfiles handling of .folders when in monitor mode to prevent monitoring * Fixed the handling of '.folder' -> 'folder' move when skip_dotfiles is enabled * Fixed the handling of folders that cannot be read (permission error) if parent should be skipped * Fixed the handling of moving folders from skipped directory to non-skipped directory via OneDrive web interface * Fixed building on CentOS Linux under Docker * Fixed Codacy reported issues: double quote to prevent globbing and word splitting * Fixed an assertion when attempting to compute complex path comparison from shared folders * Fixed the handling of .folders when being skipped via skip_dir ### Added * Implement Feature: Implement the ability to set --resync as a config option, default is false ### Updated * Update error logging to be consistent when initialising fails * Update error logging output to handle HTML error response reasoning if present * Update link to new Microsoft documentation * Update logging output to differentiate between OneNote objects and other unsupported objects * Update RHEL/CentOS spec file example * Update known-issues.md regarding 'SSL_ERROR_SYSCALL, errno 104' * Update progress bar to be more accurate when downloading large files * Updated #658 and #865 handling of when to trigger a directory walk when changes occur on OneDrive * Updated handling of when a full scan is required due to utilising sync_list * Updated handling of when OneDrive service throws a 429 or 504 response to retry original request after a delay ## 2.4.0 - 2020-03-22 ### Fixed * Fixed how the application handles 429 response codes from OneDrive (critical update) * Fixed building on Alpine Linux under Docker * Fixed how the 'username' is determined from the running process for logfile naming * Fixed file handling when a failed download has occurred due to exiting via CTRL-C * Fixed an unhandled exception when OneDrive throws an error response on initialising * Fixed the handling of moving files into a skipped .folder when skip_dotfiles = true * Fixed the regex parsing of response URI to avoid potentially generating a bad request to OneDrive, leading to a 'AADSTS9002313: Invalid request. Request is malformed or invalid.' response. ### Added * Added a Dockerfile for building on Raspberry Pi / ARM platforms * Implement Feature: warning on big deletes to safeguard data on OneDrive * Implement Feature: delete local files after sync * Implement Feature: perform skip_dir explicit match only * Implement Feature: provide config file option for specifying the Client Identifier ### Changed * Updated the 'Client Identifier' to a new Application ID ### Updated * Updated relevant documentation (README.md, USAGE.md) to add new feature details and clarify existing information * Update completions to include the --force-http-2 option * Update to always log when a file is skipped due to the item being invalid * Update application output when just authorising application to make information clearer * Update logging output when using sync_list to be clearer as to what is actually being processed and why ## 2.3.13 - 2019-12-31 ### Fixed * Change the sync list override flag to false as default when not using sync_list * Fix --dry-run output when using --upload-only & --no-remote-delete and deleting local files ### Added * Add a verbose log entry when a monitor sync loop with OneDrive starts & completes ### Changed * Remove logAndNotify for 'processing X changes' as it is excessive for each change bundle to inform the desktop of the number of changes the client is processing ### Updated * Updated INSTALL.md with Ubuntu 16.x i386 build instructions to reflect working configuration on legacy hardware * Updated INSTALL.md with details of Linux packages * Updated INSTALL.md build instructions for CentOS platforms ## 2.3.12 - 2019-12-04 ### Fixed * Retry session upload fragment when transient errors occur to prevent silent upload failure * Update Microsoft restriction and limitations about windows naming files to include '~' for folder names * Docker guide fixes, add multiple account setup instructions * Check database for excluded sync_list items previously in scope * Catch DNS resolution error * Fix where an item now out of scope should be flagged for local delete * Fix rebuilding of onedrive, but ensure version is properly updated * Update Ubuntu i386 build instructions to use DMD using preferred method ### Added * Add debug message to when a message is sent to dbus or notification daemon * Add i386 instructions for legacy low memory platforms using LDC ## 2.3.11 - 2019-11-05 ### Fixed * Fix typo in the documentation regarding invalid config when upgrading from 'skilion' codebase * Fix handling of skip_dir, skip_file & sync_list config options * Fix typo in the documentation regarding sync_list * Fix log output to be consistent with sync_list exclusion * Fix 'Processing X changes' output to be more reflective of actual activity when using sync_list * Remove unused and unexported SED variable in Makefile.in * Handle curl exceptions and timeouts better with backoff/retry logic * Update skip_dir pattern matching when using wildcards * Fix when a full rescan is performed when using sync_list * Fix 'Key not found: name' when computing skip_dir path * Fix call from --monitor to observe --no-remote-delete * Fix unhandled exception when monitor initialisation failure occurs due to too many open local files * Fix unhandled 412 error response from OneDrive API when moving files right after upload * Fix --monitor when used with --download-only. This fixes a regression introduced in 12947d1. * Fix if --single-directory is being used, and we are using --monitor, only set inotify watches on the single directory ### Changed * Move JSON logging output from error messages to debug output ## 2.3.10 - 2019-10-01 ### Fixed * Fix searching for 'name' when deleting a synced item, if the OneDrive API does not return the expected details in the API call * Fix abnormal termination when no Internet connection * Fix downloading of files from OneDrive Personal Shared Folders when the OneDrive API responds with unexpected additional path data * Fix logging of 'initialisation' of client to actually when the attempt to initialise is performed * Fix when using a sync_list file, using deltaLink will actually 'miss' changes (moves & deletes) on OneDrive as using sync_list discards changes * Fix OneDrive API status code 500 handling when uploading files as error message is not correct * Fix crash when resume_upload file is not a valid JSON * Fix crash when a file system exception is generated when attempting to update the file date & time and this fails ### Added * If there is a case-insensitive match error, also return the remote name from the response * Make user-agent string a configuration option & add to config file * Set default User-Agent to 'OneDrive Client for Linux v{version}' ### Changed * Make verbose logging output optional on Docker * Enable --resync & debug client output via environment variables on Docker ## 2.3.9 - 2019-09-01 ### Fixed * Catch a 403 Forbidden exception when querying Sharepoint Library Names * Fix unhandled error exceptions that cause application to exit / crash when uploading files * Fix JSON object validation for queries made against OneDrive where a JSON response is expected and where that response is to be used and expected to be valid * Fix handling of 5xx responses from OneDrive when uploading via a session ### Added * Detect the need for --resync when config changes either via config file or cli override ### Changed * Change minimum required version of LDC to v1.12.0 ### Removed * Remove redundant logging output due to change in how errors are reported from OneDrive ## 2.3.8 - 2019-08-04 ### Fixed * Fix unable to download all files when OneDrive fails to return file level details used to validate file integrity * Included the flag "-m" to create the home directory when creating the user * Fix entrypoint.sh to work with "sudo docker run" * Fix docker build error on stretch * Fix hidden directories in 'root' from having prefix removed * Fix Sharepoint Document Library handling for .txt & .csv files * Fix logging for init.d service * Fix OneDrive response missing required 'id' element when uploading images * Fix 'Unexpected character '<'. (Line 1:1)' when OneDrive has an exception error * Fix error when creating the sync dir fails when there is no permission to create the sync dir ### Added * Add explicit check for hashes to be returned in cases where OneDrive API fails to provide them despite requested to do so * Add comparison with sha1 if OneDrive provides that rather than quickXor * Add selinux configuration details for a sync folder outside of the home folder * Add date tag on docker.hub * Add back CentOS 6 install & uninstall to Makefile * Add a check to handle moving items out of sync_list sync scope & delete locally if true * Implement --get-file-link which will return the weburl of a file which has been synced to OneDrive ### Changed * Change unauthorized-api exit code to 3 * Update LDC to v1.16.0 for Travis CI testing * Use replace function for modified Sharepoint Document Library files rather than delete and upload as new file, preserving file history * Update Sharepoint modified file handling for files > 4Mb in size ### Removed * Remove -d shorthand for --download-only to avoid confusion with other GNU applications where -d stands for 'debug' ## 2.3.7 - 2019-07-03 ### Fixed * Fix not all files being downloaded due to OneDrive query failure * False DB update which potentially could had lead to false data loss on OneDrive ## 2.3.6 - 2019-07-03 (DO NOT USE) ### Fixed * Fix JSONValue object validation * Fix building without git being available * Fix some spelling/grammatical errors * Fix OneDrive error response on creating upload session ### Added * Add download size & hash check to ensure downloaded files are valid and not corrupt * Added --force-http-2 to use HTTP/2 if desired ### Changed * Deprecated --force-http-1.1 (enabled by default) due to OneDrive inconsistent behavior with HTTP/2 protocol ## 2.3.5 - 2019-06-19 ### Fixed * Handle a directory in the sync_dir when no permission to access * Get rid of forced root necessity during installation * Fix broken autoconf code for --enable-XXX options * Fix so that skip_size check should only be used if configured * Fix a OneDrive Internal Error exception occurring before attempting to download a file ### Added * Check for supported version of D compiler ## 2.3.4 - 2019-06-13 ### Fixed * Fix 'Local files not deleted' when using bad 'skip_file' entry * Fix --dry-run logging output for faking downloading new files * Fix install unit files to correct location on RHEL/CentOS 7 * Fix up unit file removal on all platforms * Fix setting times on a file by adding a check to see if the file was actually downloaded before attempting to set the times on the file * Fix an unhandled curl exception when OneDrive throws an internal timeout error * Check timestamp to ensure that latest timestamp is used when comparing OneDrive changes * Fix handling responses where cTag JSON elements are missing * Fix Docker entrypoint.sh failures when GID is defined but not UID ### Added * Add autoconf based build system * Add an encoding validation check before any path length checks are performed as if the path contains any invalid UTF-8 sequences * Implement --sync-root-files to sync all files in the OneDrive root when using a sync_list file that would normally exclude these files from being synced * Implement skip_size feature request * Implement feature request to support file based OneDrive authorization (request | response) ### Updated * Better handle initialisation issues when OneDrive / MS Graph is experiencing problems that generate 401 & 5xx error codes * Enhance error message when unable to connect to Microsoft OneDrive service when the local CA SSL certificate(s) have issues * Update Dockerfile to correctly build on Docker Hub * Rework directory layout and re-factor MD files for readability ## 2.3.3 - 2019-04-16 ### Fixed * Fix --upload-only check for Sharepoint uploads * Fix check to ensure item root we flag as 'root' actually is OneDrive account 'root' * Handle object error response from OneDrive when uploading to OneDrive Business * Fix handling of some OneDrive accounts not providing 'quota' details * Fix 'resume_upload' handling in the event of bad OneDrive response ### Added * Add debugging for --get-O365-drive-id function * Add shell (bash,zsh) completion support * Add config options for command line switches to allow for better config handling in docker containers ### Updated * Implement more meaningful 5xx error responses * Update onedrive.logrotate indentations and comments * Update 'min_notif_changes' to 'min_notify_changes' ## 2.3.2 - 2019-04-02 ### Fixed * Reduce scanning the entire local system in monitor mode for local changes * Resolve file creation loop when working directly in the synced folder and Microsoft Sharepoint ### Added * Add 'monitor_fullscan_frequency' config option to set the frequency of performing a full disk scan when in monitor mode ### Updated * Update default 'skip_file' to include tmp and lock files generated by LibreOffice * Update database version due to changing defaults of 'skip_file' which will force a rebuild and use of new skip_file default regex ## 2.3.1 - 2019-03-26 ### Fixed * Resolve 'make install' issue where rebuild of application would occur due to 'version' being flagged as .PHONY * Update readme build instructions to include 'make clean;' before build to ensure that 'version' is cleanly removed and can be updated correctly * Update Debian Travis CI build URL's ## 2.3.0 - 2019-03-25 ### Fixed * Resolve application crash if no 'size' value is returned when uploading a new file * Resolve application crash if a 5xx error is returned when uploading a new file * Resolve not 'refreshing' version file when rebuilding * Resolve unexpected application processing by preventing use of --synchronize & --monitor together * Resolve high CPU usage when performing DB reads * Update error logging around directory case-insensitive match * Update Travis CI and ARM dependencies for LDC 1.14.0 * Update Makefile due to build failure if building from release archive file * Update logging as to why a OneDrive object was skipped ### Added * Implement config option 'skip_dir' ## 2.2.6 - 2019-03-12 ### Fixed * Resolve application crash when unable to delete remote folders when business retention policies are enabled * Resolve deprecation warning: loop index implicitly converted from size_t to int * Resolve warnings regarding 'bashisms' * Resolve handling of notification failure is dbus server has not started or available * Resolve handling of response JSON to ensure that 'id' key element is always checked for * Resolve excessive & needless logging in monitor mode * Resolve compiling with LDC on Alpine as musl lacks some standard interfaces * Resolve notification issues when offline and cannot act on changes * Resolve Docker entrypoint.sh to accept command line arguments * Resolve to create a new upload session on reinit * Resolve where on OneDrive query failure, default root and drive id is used if a response is not returned * Resolve Key not found: nextExpectedRanges when attempting session uploads and incorrect response is returned * Resolve application crash when re-using an authentication URI twice after previous --logout * Resolve creating a folder on a shared personal folder appears successful but returns a JSON error * Resolve to treat mv of new file as upload of mv target * Update Debian i386 build dependencies * Update handling of --get-O365-drive-id to print out all 'site names' that match the explicit search entry rather than just the last match * Update Docker readme & documentation * Update handling of validating local file permissions for new file uploads ### Added * Add support for install & uninstall on RHEL / CentOS 6.x * Add support for when notifications are enabled, display the number of OneDrive changes to process if any are found * Add 'config' option 'min_notif_changes' for minimum number of changes to notify on, default = 5 * Add additional Docker container builds utilising a smaller OS footprint * Add configurable interval of logging in monitor mode * Implement new CLI option --skip-dot-files to skip .files and .folders if option is used * Implement new CLI option --check-for-nosync to ignore folder when special file (.nosync) present * Implement new CLI option --dry-run ## 2.2.5 - 2019-01-16 ### Fixed * Update handling of HTTP 412 - Precondition Failed errors * Update --display-config to display sync_list if configured * Add a check for 'id' key on metadata update to prevent 'std.json.JSONException@std/json.d(494): Key not found: id' * Update handling of 'remote' folder designation as 'root' items * Ensure that remote deletes are handled correctly * Handle 'Item not found' exception when unable to query OneDrive 'root' for changes * Add handling for JSON response error when OneDrive API returns a 404 due to OneDrive API regression * Fix items highlighted by codacy review ### Added * Add --force-http-1.1 flag to downgrade any HTTP/2 curl operations to HTTP 1.1 protocol * Support building with ldc2 and usage of pkg-config for lib finding ## 2.2.4 - 2018-12-28 ### Fixed * Resolve JSONException when supplying --get-O365-drive-id option with a string containing spaces * Resolve 'sync_dir' not read from 'config' file when run in Docker container * Resolve logic where potentially a 'default' ~/OneDrive sync_dir could be set despite 'config' file configured for an alternate * Make sure sqlite checkpointing works by properly finalizing statements * Update logic handling of --single-directory to prevent inadvertent local data loss * Resolve signal handling and database shutdown on SIGINT and SIGTERM * Update man page * Implement better help output formatting ### Added * Add debug handling for sync_dir operations * Add debug handling for homePath calculation * Add debug handling for configDirBase calculation * Add debug handling if syncDir is created * Implement Feature Request: Add status command or switch ## 2.2.3 - 2018-12-20 ### Fixed * Fix syncdir option is ignored ## 2.2.2 - 2018-12-20 ### Fixed * Handle short lived files in monitor mode * Provide better log messages, less noise on temporary timeouts * Deal with items that disappear during upload * Deal with deleted move targets * Reinitialize sync engine after three failed attempts * Fix activation of dmd for docker builds * Fix to check displayName rather than description for --get-O365-drive-id * Fix checking of config file keys for validity * Fix exception handling when missing parameter from usage option ### Added * Notification support via libnotify * Add very verbose (debug) mode by double -v -v * Implement option --display-config ## 2.2.1 - 2018-12-04 ### Fixed * Gracefully handle connection errors in monitor mode * Fix renaming of files when syncing * Installation of doc files, addition of man page * Adjust timeout values for libcurl * Continue in monitor mode when sync timed out * Fix unreachable statements * Update Makefile to better support packaging * Allow starting offline in monitor mode ### Added * Implement --get-O365-drive-id to get correct SharePoint Shared Library (#248) * Docker buildfiles for onedrive service (#262) ## 2.2.0 - 2018-11-24 ### Fixed * Updated client to output additional logging when debugging * Resolve database assertion failure due to authentication * Resolve unable to create folders on shared OneDrive Personal accounts ### Added * Implement feature request to Sync from Microsoft SharePoint * Implement feature request to specify a logging directory if logging is enabled ### Changed * Change '--download' to '--download-only' to align with '--upload-only' * Change logging so that logging to a separate file is no longer the default ## 2.1.6 - 2018-11-15 ### Fixed * Updated HTTP/2 transport handling when using curl 7.62.0 for session uploads ### Added * Added PKGBUILD for makepkg for building packages under Arch Linux ## 2.1.5 - 2018-11-11 ### Fixed * Resolve 'Key not found: path' when syncing from some shared folders due to OneDrive API change * Resolve to only upload changes on remote folder if the item is in the database - dont assert if false * Resolve files will not download or upload when using curl 7.62.0 due to HTTP/2 being set as default for all curl operations * Resolve to handle HTTP request returned status code 412 (Precondition Failed) for session uploads to OneDrive Personal Accounts * Resolve unable to remove '~/.config/onedrive/resume_upload: No such file or directory' if there is a session upload error and the resume file does not get created * Resolve handling of response codes when using 2 different systems when using '--upload-only' but the same OneDrive account and uploading the same filename to the same location ### Updated * Updated Travis CI building on LDC v1.11.0 for ARMHF builds * Updated Makefile to use 'install -D -m 644' rather than 'cp -raf' * Updated default config to be aligned to code defaults ## 2.1.4 - 2018-10-10 ### Fixed * Resolve syncing of OneDrive Personal Shared Folders due to OneDrive API change * Resolve incorrect systemd installation location(s) in Makefile ## 2.1.3 - 2018-10-04 ### Fixed * Resolve File download fails if the file is marked as malware in OneDrive * Resolve high CPU usage when running in monitor mode * Resolve how default path is set when running under systemd on headless systems * Resolve incorrectly nested configDir in X11 systems * Resolve Key not found: driveType * Resolve to validate filename length before download to conform with Linux FS limits * Resolve file handling to look for HTML ASCII codes which will cause uploads to fail * Resolve Key not found: expirationDateTime on session resume ### Added * Update Travis CI building to test build on ARM64 ## 2.1.2 - 2018-08-27 ### Fixed * Resolve skipping of symlinks in monitor mode * Resolve Gateway Timeout - JSONValue is not an object * Resolve systemd/user is not supported on CentOS / RHEL * Resolve HTTP request returned status code 429 (Too Many Requests) * Resolve handling of maximum path length calculation * Resolve 'The parent item is not in the local database' * Resolve Correctly handle file case sensitivity issues in same folder * Update unit files documentation link ## 2.1.1 - 2018-08-14 ### Fixed * Fix handling no remote delete of remote directories when using --no-remote-delete * Fix handling of no permission to access a local file / corrupt local file * Fix application crash when unable to access login.microsoft.com upon application startup ### Added * Build instructions for openSUSE Leap 15.0 ## 2.1.0 - 2018-08-10 ### Fixed * Fix handling of database exit scenarios when there is zero disk space left on drive where the items database resides * Fix handling of incorrect database permissions * Fix handling of different database versions to automatically re-create tables if version mis-match * Fix handling timeout when accessing the Microsoft OneDrive Service * Fix localFileModifiedTime to not use fraction seconds ### Added * Implement Feature: Add a progress bar for large uploads & downloads * Implement Feature: Make checkinterval for monitor configurable * Implement Feature: Upload Only Option that does not perform remote delete * Implement Feature: Add ability to skip symlinks * Add dependency, ebuild and build instructions for Gentoo distributions ### Changed * Build instructions for x86, x86_64 and ARM32 platforms * Travis CI files to automate building on x32, x64 and ARM32 architectures * Travis CI files to test built application against valid, invalid and problem files from previous issues ## 2.0.2 - 2018-07-18 ### Fixed * Fix systemd service install for builds with DESTDIR defined * Fix 'HTTP 412 - Precondition Failed' error handling * Gracefully handle OneDrive account password change * Update logic handling of --upload-only and --local-first ## 2.0.1 - 2018-07-11 ### Fixed * Resolve computeQuickXorHash generates a different hash when files are > 64Kb ## 2.0.0 - 2018-07-10 ### Fixed * Resolve conflict resolution issue during syncing - the client does not handle conflicts very well & keeps on adding the hostname to files * Resolve skilion #356 by adding additional check for 409 response from OneDrive * Resolve multiple versions of file shown on website after single upload * Resolve to gracefully fail when 'onedrive' process cannot get exclusive database lock * Resolve 'Key not found: fileSystemInfo' when then item is a remote item (OneDrive Personal) * Resolve skip_file config entry needs to be checked for any characters to escape * Resolve Microsoft Naming Convention not being followed correctly * Resolve Error when trying to upload a file with weird non printable characters present * Resolve Crash if file is locked by online editing (status code 423) * Resolve compilation issue with dmd-2.081.0 * Resolve skip_file configuration doesn't handle spaces or specified directory paths ### Added * Implement Feature: Add a flag to detect when the sync-folder is missing * Implement Travis CI for code testing ### Changed * Update Makefile to use DESTDIR variables * Update OneDrive Business maximum path length from 256 to 400 * Update OneDrive Business allowed characters for files and folders * Update sync_dir handling to use the absolute path for setting parameter to something other than ~/OneDrive via config file or command line * Update Fedora build instructions ## 1.1.2 - 2018-05-17 ### Fixed * Fix 4xx errors including (412 pre-condition, 409 conflict) * Fix Key not found: lastModifiedDateTime (OneDrive API change) * Fix configuration directory not found when run via init.d * Fix skilion Issues #73, #121, #132, #224, #257, #294, #295, #297, #298, #300, #306, #315, #320, #329, #334, #337, #341 ### Added * Add logging - log client activities to a file (/var/log/onedrive/%username%.onedrive.log or ~/onedrive.log) * Add https debugging as a flag * Add `--synchronize` to prevent from syncing when just blindly running the application * Add individual folder sync * Add sync from local directory first rather than download first then upload * Add upload long path check * Add upload only * Add check for max upload file size before attempting upload * Add systemd unit files for single & multi user configuration * Add init.d file for older init.d based services * Add Microsoft naming conventions and namespace validation for items that will be uploaded * Add remaining free space counter at client initialisation to avoid out of space upload issue * Add large file upload size check to align to OneDrive file size limitations * Add upload file size validation & retry if does not match * Add graceful handling of some fatal errors (OneDrive 5xx error handling) ## Unreleased - 2018-02-19 ### Fixed * Crash when the delta link is expired ### Changed * Disabled buffering on stdout ## 1.1.1 - 2018-01-20 ### Fixed * Wrong regex for parsing authentication uri ## 1.1.0 - 2018-01-19 ### Added * Support for shared folders (OneDrive Personal only) * `--download` option to only download changes * `DC` variable in Makefile to chose the compiler ### Changed * Print logs on stdout instead of stderr * Improve log messages ## 1.0.1 - 2017-08-01 ### Added * `--syncdir` option ### Changed * `--version` output simplified * Updated README ### Fixed * Fix crash caused by remotely deleted and recreated directories ## 1.0.0 - 2017-07-14 ### Added * `--version` option onedrive-2.5.5/config000066400000000000000000000033721476564400300145440ustar00rootroot00000000000000# Configuration for OneDrive Linux Client # This file contains the list of supported configuration fields # with their default values. # All values need to be enclosed in quotes # When changing a config option below, remove the '#' from the start of the line # For explanations of all config options below see docs/usage.md or the man page. # # sync_dir = "~/OneDrive" # skip_file = "~*|.~*|*.tmp" # monitor_interval = "300" # skip_dir = "" # log_dir = "/var/log/onedrive/" # drive_id = "" # upload_only = "false" # check_nomount = "false" # check_nosync = "false" # download_only = "false" # disable_notifications = "false" # disable_upload_validation = "false" # enable_logging = "false" # force_http_11 = "false" # local_first = "false" # no_remote_delete = "false" # skip_symlinks = "false" # debug_https = "false" # skip_dotfiles = "false" # skip_size = "1000" # dry_run = "false" # min_notify_changes = "5" # monitor_log_frequency = "6" # monitor_fullscan_frequency = "12" # sync_root_files = "false" # classify_as_big_delete = "1000" # user_agent = "" # remove_source_files = "false" # skip_dir_strict_match = "false" # application_id = "" # resync = "false" # resync_auth = "false" # bypass_data_preservation = "false" # azure_ad_endpoint = "" # azure_tenant_id = "common" # sync_business_shared_items = "false" # sync_dir_permissions = "700" # sync_file_permissions = "600" # rate_limit = "131072" # operation_timeout = "3600" # webhook_enabled = "false" # webhook_public_url = "" # webhook_listening_host = "" # webhook_listening_port = "8888" # webhook_expiration_interval = "600" # webhook_renewal_interval = "300" # webhook_retry_interval = "60" # space_reservation = "50" # display_running_config = "false" # read_only_auth_scope = "false" # cleanup_local_files = "false" onedrive-2.5.5/configure000077500000000000000000003332651476564400300152720ustar00rootroot00000000000000#! /bin/sh # Guess values for system-dependent variables and create Makefiles. # Generated by GNU Autoconf 2.69 for onedrive v2.5.5. # # Report bugs to . # # # Copyright (C) 1992-1996, 1998-2012 Free Software Foundation, Inc. # # # This configure script is free software; the Free Software Foundation # gives unlimited permission to copy, distribute and modify it. ## -------------------- ## ## M4sh Initialization. ## ## -------------------- ## # Be more Bourne compatible DUALCASE=1; export DUALCASE # for MKS sh if test -n "${ZSH_VERSION+set}" && (emulate sh) >/dev/null 2>&1; then : emulate sh NULLCMD=: # Pre-4.2 versions of Zsh do word splitting on ${1+"$@"}, which # is contrary to our usage. Disable this feature. alias -g '${1+"$@"}'='"$@"' setopt NO_GLOB_SUBST else case `(set -o) 2>/dev/null` in #( *posix*) : set -o posix ;; #( *) : ;; esac fi as_nl=' ' export as_nl # Printing a long string crashes Solaris 7 /usr/bin/printf. as_echo='\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\' as_echo=$as_echo$as_echo$as_echo$as_echo$as_echo as_echo=$as_echo$as_echo$as_echo$as_echo$as_echo$as_echo # Prefer a ksh shell builtin over an external printf program on Solaris, # but without wasting forks for bash or zsh. if test -z "$BASH_VERSION$ZSH_VERSION" \ && (test "X`print -r -- $as_echo`" = "X$as_echo") 2>/dev/null; then as_echo='print -r --' as_echo_n='print -rn --' elif (test "X`printf %s $as_echo`" = "X$as_echo") 2>/dev/null; then as_echo='printf %s\n' as_echo_n='printf %s' else if test "X`(/usr/ucb/echo -n -n $as_echo) 2>/dev/null`" = "X-n $as_echo"; then as_echo_body='eval /usr/ucb/echo -n "$1$as_nl"' as_echo_n='/usr/ucb/echo -n' else as_echo_body='eval expr "X$1" : "X\\(.*\\)"' as_echo_n_body='eval arg=$1; case $arg in #( *"$as_nl"*) expr "X$arg" : "X\\(.*\\)$as_nl"; arg=`expr "X$arg" : ".*$as_nl\\(.*\\)"`;; esac; expr "X$arg" : "X\\(.*\\)" | tr -d "$as_nl" ' export as_echo_n_body as_echo_n='sh -c $as_echo_n_body as_echo' fi export as_echo_body as_echo='sh -c $as_echo_body as_echo' fi # The user is always right. if test "${PATH_SEPARATOR+set}" != set; then PATH_SEPARATOR=: (PATH='/bin;/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 && { (PATH='/bin:/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 || PATH_SEPARATOR=';' } fi # IFS # We need space, tab and new line, in precisely that order. Quoting is # there to prevent editors from complaining about space-tab. # (If _AS_PATH_WALK were called with IFS unset, it would disable word # splitting by setting IFS to empty value.) IFS=" "" $as_nl" # Find who we are. Look in the path if we contain no directory separator. as_myself= case $0 in #(( *[\\/]* ) as_myself=$0 ;; *) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS test -z "$as_dir" && as_dir=. test -r "$as_dir/$0" && as_myself=$as_dir/$0 && break done IFS=$as_save_IFS ;; esac # We did not find ourselves, most probably we were run as `sh COMMAND' # in which case we are not to be found in the path. if test "x$as_myself" = x; then as_myself=$0 fi if test ! -f "$as_myself"; then $as_echo "$as_myself: error: cannot find myself; rerun with an absolute file name" >&2 exit 1 fi # Unset variables that we do not need and which cause bugs (e.g. in # pre-3.0 UWIN ksh). But do not cause bugs in bash 2.01; the "|| exit 1" # suppresses any "Segmentation fault" message there. '((' could # trigger a bug in pdksh 5.2.14. for as_var in BASH_ENV ENV MAIL MAILPATH do eval test x\${$as_var+set} = xset \ && ( (unset $as_var) || exit 1) >/dev/null 2>&1 && unset $as_var || : done PS1='$ ' PS2='> ' PS4='+ ' # NLS nuisances. LC_ALL=C export LC_ALL LANGUAGE=C export LANGUAGE # CDPATH. (unset CDPATH) >/dev/null 2>&1 && unset CDPATH # Use a proper internal environment variable to ensure we don't fall # into an infinite loop, continuously re-executing ourselves. if test x"${_as_can_reexec}" != xno && test "x$CONFIG_SHELL" != x; then _as_can_reexec=no; export _as_can_reexec; # We cannot yet assume a decent shell, so we have to provide a # neutralization value for shells without unset; and this also # works around shells that cannot unset nonexistent variables. # Preserve -v and -x to the replacement shell. BASH_ENV=/dev/null ENV=/dev/null (unset BASH_ENV) >/dev/null 2>&1 && unset BASH_ENV ENV case $- in # (((( *v*x* | *x*v* ) as_opts=-vx ;; *v* ) as_opts=-v ;; *x* ) as_opts=-x ;; * ) as_opts= ;; esac exec $CONFIG_SHELL $as_opts "$as_myself" ${1+"$@"} # Admittedly, this is quite paranoid, since all the known shells bail # out after a failed `exec'. $as_echo "$0: could not re-execute with $CONFIG_SHELL" >&2 as_fn_exit 255 fi # We don't want this to propagate to other subprocesses. { _as_can_reexec=; unset _as_can_reexec;} if test "x$CONFIG_SHELL" = x; then as_bourne_compatible="if test -n \"\${ZSH_VERSION+set}\" && (emulate sh) >/dev/null 2>&1; then : emulate sh NULLCMD=: # Pre-4.2 versions of Zsh do word splitting on \${1+\"\$@\"}, which # is contrary to our usage. Disable this feature. alias -g '\${1+\"\$@\"}'='\"\$@\"' setopt NO_GLOB_SUBST else case \`(set -o) 2>/dev/null\` in #( *posix*) : set -o posix ;; #( *) : ;; esac fi " as_required="as_fn_return () { (exit \$1); } as_fn_success () { as_fn_return 0; } as_fn_failure () { as_fn_return 1; } as_fn_ret_success () { return 0; } as_fn_ret_failure () { return 1; } exitcode=0 as_fn_success || { exitcode=1; echo as_fn_success failed.; } as_fn_failure && { exitcode=1; echo as_fn_failure succeeded.; } as_fn_ret_success || { exitcode=1; echo as_fn_ret_success failed.; } as_fn_ret_failure && { exitcode=1; echo as_fn_ret_failure succeeded.; } if ( set x; as_fn_ret_success y && test x = \"\$1\" ); then : else exitcode=1; echo positional parameters were not saved. fi test x\$exitcode = x0 || exit 1 test -x / || exit 1" as_suggested=" as_lineno_1=";as_suggested=$as_suggested$LINENO;as_suggested=$as_suggested" as_lineno_1a=\$LINENO as_lineno_2=";as_suggested=$as_suggested$LINENO;as_suggested=$as_suggested" as_lineno_2a=\$LINENO eval 'test \"x\$as_lineno_1'\$as_run'\" != \"x\$as_lineno_2'\$as_run'\" && test \"x\`expr \$as_lineno_1'\$as_run' + 1\`\" = \"x\$as_lineno_2'\$as_run'\"' || exit 1" if (eval "$as_required") 2>/dev/null; then : as_have_required=yes else as_have_required=no fi if test x$as_have_required = xyes && (eval "$as_suggested") 2>/dev/null; then : else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR as_found=false for as_dir in /bin$PATH_SEPARATOR/usr/bin$PATH_SEPARATOR$PATH do IFS=$as_save_IFS test -z "$as_dir" && as_dir=. as_found=: case $as_dir in #( /*) for as_base in sh bash ksh sh5; do # Try only shells that exist, to save several forks. as_shell=$as_dir/$as_base if { test -f "$as_shell" || test -f "$as_shell.exe"; } && { $as_echo "$as_bourne_compatible""$as_required" | as_run=a "$as_shell"; } 2>/dev/null; then : CONFIG_SHELL=$as_shell as_have_required=yes if { $as_echo "$as_bourne_compatible""$as_suggested" | as_run=a "$as_shell"; } 2>/dev/null; then : break 2 fi fi done;; esac as_found=false done $as_found || { if { test -f "$SHELL" || test -f "$SHELL.exe"; } && { $as_echo "$as_bourne_compatible""$as_required" | as_run=a "$SHELL"; } 2>/dev/null; then : CONFIG_SHELL=$SHELL as_have_required=yes fi; } IFS=$as_save_IFS if test "x$CONFIG_SHELL" != x; then : export CONFIG_SHELL # We cannot yet assume a decent shell, so we have to provide a # neutralization value for shells without unset; and this also # works around shells that cannot unset nonexistent variables. # Preserve -v and -x to the replacement shell. BASH_ENV=/dev/null ENV=/dev/null (unset BASH_ENV) >/dev/null 2>&1 && unset BASH_ENV ENV case $- in # (((( *v*x* | *x*v* ) as_opts=-vx ;; *v* ) as_opts=-v ;; *x* ) as_opts=-x ;; * ) as_opts= ;; esac exec $CONFIG_SHELL $as_opts "$as_myself" ${1+"$@"} # Admittedly, this is quite paranoid, since all the known shells bail # out after a failed `exec'. $as_echo "$0: could not re-execute with $CONFIG_SHELL" >&2 exit 255 fi if test x$as_have_required = xno; then : $as_echo "$0: This script requires a shell more modern than all" $as_echo "$0: the shells that I found on your system." if test x${ZSH_VERSION+set} = xset ; then $as_echo "$0: In particular, zsh $ZSH_VERSION has bugs and should" $as_echo "$0: be upgraded to zsh 4.3.4 or later." else $as_echo "$0: Please tell bug-autoconf@gnu.org and $0: https://github.com/abraunegg/onedrive about your $0: system, including any error possibly output before this $0: message. Then install a modern shell, or manually run $0: the script under such a shell if you do have one." fi exit 1 fi fi fi SHELL=${CONFIG_SHELL-/bin/sh} export SHELL # Unset more variables known to interfere with behavior of common tools. CLICOLOR_FORCE= GREP_OPTIONS= unset CLICOLOR_FORCE GREP_OPTIONS ## --------------------- ## ## M4sh Shell Functions. ## ## --------------------- ## # as_fn_unset VAR # --------------- # Portably unset VAR. as_fn_unset () { { eval $1=; unset $1;} } as_unset=as_fn_unset # as_fn_set_status STATUS # ----------------------- # Set $? to STATUS, without forking. as_fn_set_status () { return $1 } # as_fn_set_status # as_fn_exit STATUS # ----------------- # Exit the shell with STATUS, even in a "trap 0" or "set -e" context. as_fn_exit () { set +e as_fn_set_status $1 exit $1 } # as_fn_exit # as_fn_mkdir_p # ------------- # Create "$as_dir" as a directory, including parents if necessary. as_fn_mkdir_p () { case $as_dir in #( -*) as_dir=./$as_dir;; esac test -d "$as_dir" || eval $as_mkdir_p || { as_dirs= while :; do case $as_dir in #( *\'*) as_qdir=`$as_echo "$as_dir" | sed "s/'/'\\\\\\\\''/g"`;; #'( *) as_qdir=$as_dir;; esac as_dirs="'$as_qdir' $as_dirs" as_dir=`$as_dirname -- "$as_dir" || $as_expr X"$as_dir" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ X"$as_dir" : 'X\(//\)[^/]' \| \ X"$as_dir" : 'X\(//\)$' \| \ X"$as_dir" : 'X\(/\)' \| . 2>/dev/null || $as_echo X"$as_dir" | sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ s//\1/ q } /^X\(\/\/\)[^/].*/{ s//\1/ q } /^X\(\/\/\)$/{ s//\1/ q } /^X\(\/\).*/{ s//\1/ q } s/.*/./; q'` test -d "$as_dir" && break done test -z "$as_dirs" || eval "mkdir $as_dirs" } || test -d "$as_dir" || as_fn_error $? "cannot create directory $as_dir" } # as_fn_mkdir_p # as_fn_executable_p FILE # ----------------------- # Test if FILE is an executable regular file. as_fn_executable_p () { test -f "$1" && test -x "$1" } # as_fn_executable_p # as_fn_append VAR VALUE # ---------------------- # Append the text in VALUE to the end of the definition contained in VAR. Take # advantage of any shell optimizations that allow amortized linear growth over # repeated appends, instead of the typical quadratic growth present in naive # implementations. if (eval "as_var=1; as_var+=2; test x\$as_var = x12") 2>/dev/null; then : eval 'as_fn_append () { eval $1+=\$2 }' else as_fn_append () { eval $1=\$$1\$2 } fi # as_fn_append # as_fn_arith ARG... # ------------------ # Perform arithmetic evaluation on the ARGs, and store the result in the # global $as_val. Take advantage of shells that can avoid forks. The arguments # must be portable across $(()) and expr. if (eval "test \$(( 1 + 1 )) = 2") 2>/dev/null; then : eval 'as_fn_arith () { as_val=$(( $* )) }' else as_fn_arith () { as_val=`expr "$@" || test $? -eq 1` } fi # as_fn_arith # as_fn_error STATUS ERROR [LINENO LOG_FD] # ---------------------------------------- # Output "`basename $0`: error: ERROR" to stderr. If LINENO and LOG_FD are # provided, also output the error to LOG_FD, referencing LINENO. Then exit the # script with STATUS, using 1 if that was 0. as_fn_error () { as_status=$1; test $as_status -eq 0 && as_status=1 if test "$4"; then as_lineno=${as_lineno-"$3"} as_lineno_stack=as_lineno_stack=$as_lineno_stack $as_echo "$as_me:${as_lineno-$LINENO}: error: $2" >&$4 fi $as_echo "$as_me: error: $2" >&2 as_fn_exit $as_status } # as_fn_error if expr a : '\(a\)' >/dev/null 2>&1 && test "X`expr 00001 : '.*\(...\)'`" = X001; then as_expr=expr else as_expr=false fi if (basename -- /) >/dev/null 2>&1 && test "X`basename -- / 2>&1`" = "X/"; then as_basename=basename else as_basename=false fi if (as_dir=`dirname -- /` && test "X$as_dir" = X/) >/dev/null 2>&1; then as_dirname=dirname else as_dirname=false fi as_me=`$as_basename -- "$0" || $as_expr X/"$0" : '.*/\([^/][^/]*\)/*$' \| \ X"$0" : 'X\(//\)$' \| \ X"$0" : 'X\(/\)' \| . 2>/dev/null || $as_echo X/"$0" | sed '/^.*\/\([^/][^/]*\)\/*$/{ s//\1/ q } /^X\/\(\/\/\)$/{ s//\1/ q } /^X\/\(\/\).*/{ s//\1/ q } s/.*/./; q'` # Avoid depending upon Character Ranges. as_cr_letters='abcdefghijklmnopqrstuvwxyz' as_cr_LETTERS='ABCDEFGHIJKLMNOPQRSTUVWXYZ' as_cr_Letters=$as_cr_letters$as_cr_LETTERS as_cr_digits='0123456789' as_cr_alnum=$as_cr_Letters$as_cr_digits as_lineno_1=$LINENO as_lineno_1a=$LINENO as_lineno_2=$LINENO as_lineno_2a=$LINENO eval 'test "x$as_lineno_1'$as_run'" != "x$as_lineno_2'$as_run'" && test "x`expr $as_lineno_1'$as_run' + 1`" = "x$as_lineno_2'$as_run'"' || { # Blame Lee E. McMahon (1931-1989) for sed's syntax. :-) sed -n ' p /[$]LINENO/= ' <$as_myself | sed ' s/[$]LINENO.*/&-/ t lineno b :lineno N :loop s/[$]LINENO\([^'$as_cr_alnum'_].*\n\)\(.*\)/\2\1\2/ t loop s/-\n.*// ' >$as_me.lineno && chmod +x "$as_me.lineno" || { $as_echo "$as_me: error: cannot create $as_me.lineno; rerun with a POSIX shell" >&2; as_fn_exit 1; } # If we had to re-execute with $CONFIG_SHELL, we're ensured to have # already done that, so ensure we don't try to do so again and fall # in an infinite loop. This has already happened in practice. _as_can_reexec=no; export _as_can_reexec # Don't try to exec as it changes $[0], causing all sort of problems # (the dirname of $[0] is not the place where we might find the # original and so on. Autoconf is especially sensitive to this). . "./$as_me.lineno" # Exit status is that of the last command. exit } ECHO_C= ECHO_N= ECHO_T= case `echo -n x` in #((((( -n*) case `echo 'xy\c'` in *c*) ECHO_T=' ';; # ECHO_T is single tab character. xy) ECHO_C='\c';; *) echo `echo ksh88 bug on AIX 6.1` > /dev/null ECHO_T=' ';; esac;; *) ECHO_N='-n';; esac rm -f conf$$ conf$$.exe conf$$.file if test -d conf$$.dir; then rm -f conf$$.dir/conf$$.file else rm -f conf$$.dir mkdir conf$$.dir 2>/dev/null fi if (echo >conf$$.file) 2>/dev/null; then if ln -s conf$$.file conf$$ 2>/dev/null; then as_ln_s='ln -s' # ... but there are two gotchas: # 1) On MSYS, both `ln -s file dir' and `ln file dir' fail. # 2) DJGPP < 2.04 has no symlinks; `ln -s' creates a wrapper executable. # In both cases, we have to default to `cp -pR'. ln -s conf$$.file conf$$.dir 2>/dev/null && test ! -f conf$$.exe || as_ln_s='cp -pR' elif ln conf$$.file conf$$ 2>/dev/null; then as_ln_s=ln else as_ln_s='cp -pR' fi else as_ln_s='cp -pR' fi rm -f conf$$ conf$$.exe conf$$.dir/conf$$.file conf$$.file rmdir conf$$.dir 2>/dev/null if mkdir -p . 2>/dev/null; then as_mkdir_p='mkdir -p "$as_dir"' else test -d ./-p && rmdir ./-p as_mkdir_p=false fi as_test_x='test -x' as_executable_p=as_fn_executable_p # Sed expression to map a string onto a valid CPP name. as_tr_cpp="eval sed 'y%*$as_cr_letters%P$as_cr_LETTERS%;s%[^_$as_cr_alnum]%_%g'" # Sed expression to map a string onto a valid variable name. as_tr_sh="eval sed 'y%*+%pp%;s%[^_$as_cr_alnum]%_%g'" test -n "$DJDIR" || exec 7<&0 &1 # Name of the host. # hostname on some systems (SVR3.2, old GNU/Linux) returns a bogus exit status, # so uname gets run too. ac_hostname=`(hostname || uname -n) 2>/dev/null | sed 1q` # # Initializations. # ac_default_prefix=/usr/local ac_clean_files= ac_config_libobj_dir=. LIBOBJS= cross_compiling=no subdirs= MFLAGS= MAKEFLAGS= # Identity of this package. PACKAGE_NAME='onedrive' PACKAGE_TARNAME='onedrive' PACKAGE_VERSION='v2.5.5' PACKAGE_STRING='onedrive v2.5.5' PACKAGE_BUGREPORT='https://github.com/abraunegg/onedrive' PACKAGE_URL='' ac_unique_file="src/main.d" ac_subst_vars='LTLIBOBJS LIBOBJS DEBUG FISH_COMPLETION_DIR ZSH_COMPLETION_DIR BASH_COMPLETION_DIR bashcompdir COMPLETIONS dynamic_linker_LIBS bsd_inotify_LIBS NOTIFICATIONS notify_LIBS notify_CFLAGS HAVE_SYSTEMD systemduserunitdir systemdsystemunitdir sqlite_LIBS sqlite_CFLAGS curl_LIBS curl_CFLAGS PACKAGE_DATE DC_TYPE PKG_CONFIG_LIBDIR PKG_CONFIG_PATH PKG_CONFIG INSTALL_DATA INSTALL_SCRIPT INSTALL_PROGRAM DCFLAGS DC target_alias host_alias build_alias LIBS ECHO_T ECHO_N ECHO_C DEFS mandir localedir libdir psdir pdfdir dvidir htmldir infodir docdir oldincludedir includedir localstatedir sharedstatedir sysconfdir datadir datarootdir libexecdir sbindir bindir program_transform_name prefix exec_prefix PACKAGE_URL PACKAGE_BUGREPORT PACKAGE_STRING PACKAGE_VERSION PACKAGE_TARNAME PACKAGE_NAME PATH_SEPARATOR SHELL' ac_subst_files='' ac_user_opts=' enable_option_checking enable_version_check with_systemdsystemunitdir with_systemduserunitdir enable_notifications enable_completions with_bash_completion_dir with_zsh_completion_dir with_fish_completion_dir enable_debug ' ac_precious_vars='build_alias host_alias target_alias DC DCFLAGS PKG_CONFIG PKG_CONFIG_PATH PKG_CONFIG_LIBDIR curl_CFLAGS curl_LIBS sqlite_CFLAGS sqlite_LIBS notify_CFLAGS notify_LIBS bashcompdir' # Initialize some variables set by options. ac_init_help= ac_init_version=false ac_unrecognized_opts= ac_unrecognized_sep= # The variables have the same names as the options, with # dashes changed to underlines. cache_file=/dev/null exec_prefix=NONE no_create= no_recursion= prefix=NONE program_prefix=NONE program_suffix=NONE program_transform_name=s,x,x, silent= site= srcdir= verbose= x_includes=NONE x_libraries=NONE # Installation directory options. # These are left unexpanded so users can "make install exec_prefix=/foo" # and all the variables that are supposed to be based on exec_prefix # by default will actually change. # Use braces instead of parens because sh, perl, etc. also accept them. # (The list follows the same order as the GNU Coding Standards.) bindir='${exec_prefix}/bin' sbindir='${exec_prefix}/sbin' libexecdir='${exec_prefix}/libexec' datarootdir='${prefix}/share' datadir='${datarootdir}' sysconfdir='${prefix}/etc' sharedstatedir='${prefix}/com' localstatedir='${prefix}/var' includedir='${prefix}/include' oldincludedir='/usr/include' docdir='${datarootdir}/doc/${PACKAGE_TARNAME}' infodir='${datarootdir}/info' htmldir='${docdir}' dvidir='${docdir}' pdfdir='${docdir}' psdir='${docdir}' libdir='${exec_prefix}/lib' localedir='${datarootdir}/locale' mandir='${datarootdir}/man' ac_prev= ac_dashdash= for ac_option do # If the previous option needs an argument, assign it. if test -n "$ac_prev"; then eval $ac_prev=\$ac_option ac_prev= continue fi case $ac_option in *=?*) ac_optarg=`expr "X$ac_option" : '[^=]*=\(.*\)'` ;; *=) ac_optarg= ;; *) ac_optarg=yes ;; esac # Accept the important Cygnus configure options, so we can diagnose typos. case $ac_dashdash$ac_option in --) ac_dashdash=yes ;; -bindir | --bindir | --bindi | --bind | --bin | --bi) ac_prev=bindir ;; -bindir=* | --bindir=* | --bindi=* | --bind=* | --bin=* | --bi=*) bindir=$ac_optarg ;; -build | --build | --buil | --bui | --bu) ac_prev=build_alias ;; -build=* | --build=* | --buil=* | --bui=* | --bu=*) build_alias=$ac_optarg ;; -cache-file | --cache-file | --cache-fil | --cache-fi \ | --cache-f | --cache- | --cache | --cach | --cac | --ca | --c) ac_prev=cache_file ;; -cache-file=* | --cache-file=* | --cache-fil=* | --cache-fi=* \ | --cache-f=* | --cache-=* | --cache=* | --cach=* | --cac=* | --ca=* | --c=*) cache_file=$ac_optarg ;; --config-cache | -C) cache_file=config.cache ;; -datadir | --datadir | --datadi | --datad) ac_prev=datadir ;; -datadir=* | --datadir=* | --datadi=* | --datad=*) datadir=$ac_optarg ;; -datarootdir | --datarootdir | --datarootdi | --datarootd | --dataroot \ | --dataroo | --dataro | --datar) ac_prev=datarootdir ;; -datarootdir=* | --datarootdir=* | --datarootdi=* | --datarootd=* \ | --dataroot=* | --dataroo=* | --dataro=* | --datar=*) datarootdir=$ac_optarg ;; -disable-* | --disable-*) ac_useropt=`expr "x$ac_option" : 'x-*disable-\(.*\)'` # Reject names that are not valid shell variable names. expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && as_fn_error $? "invalid feature name: $ac_useropt" ac_useropt_orig=$ac_useropt ac_useropt=`$as_echo "$ac_useropt" | sed 's/[-+.]/_/g'` case $ac_user_opts in *" "enable_$ac_useropt" "*) ;; *) ac_unrecognized_opts="$ac_unrecognized_opts$ac_unrecognized_sep--disable-$ac_useropt_orig" ac_unrecognized_sep=', ';; esac eval enable_$ac_useropt=no ;; -docdir | --docdir | --docdi | --doc | --do) ac_prev=docdir ;; -docdir=* | --docdir=* | --docdi=* | --doc=* | --do=*) docdir=$ac_optarg ;; -dvidir | --dvidir | --dvidi | --dvid | --dvi | --dv) ac_prev=dvidir ;; -dvidir=* | --dvidir=* | --dvidi=* | --dvid=* | --dvi=* | --dv=*) dvidir=$ac_optarg ;; -enable-* | --enable-*) ac_useropt=`expr "x$ac_option" : 'x-*enable-\([^=]*\)'` # Reject names that are not valid shell variable names. expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && as_fn_error $? "invalid feature name: $ac_useropt" ac_useropt_orig=$ac_useropt ac_useropt=`$as_echo "$ac_useropt" | sed 's/[-+.]/_/g'` case $ac_user_opts in *" "enable_$ac_useropt" "*) ;; *) ac_unrecognized_opts="$ac_unrecognized_opts$ac_unrecognized_sep--enable-$ac_useropt_orig" ac_unrecognized_sep=', ';; esac eval enable_$ac_useropt=\$ac_optarg ;; -exec-prefix | --exec_prefix | --exec-prefix | --exec-prefi \ | --exec-pref | --exec-pre | --exec-pr | --exec-p | --exec- \ | --exec | --exe | --ex) ac_prev=exec_prefix ;; -exec-prefix=* | --exec_prefix=* | --exec-prefix=* | --exec-prefi=* \ | --exec-pref=* | --exec-pre=* | --exec-pr=* | --exec-p=* | --exec-=* \ | --exec=* | --exe=* | --ex=*) exec_prefix=$ac_optarg ;; -gas | --gas | --ga | --g) # Obsolete; use --with-gas. with_gas=yes ;; -help | --help | --hel | --he | -h) ac_init_help=long ;; -help=r* | --help=r* | --hel=r* | --he=r* | -hr*) ac_init_help=recursive ;; -help=s* | --help=s* | --hel=s* | --he=s* | -hs*) ac_init_help=short ;; -host | --host | --hos | --ho) ac_prev=host_alias ;; -host=* | --host=* | --hos=* | --ho=*) host_alias=$ac_optarg ;; -htmldir | --htmldir | --htmldi | --htmld | --html | --htm | --ht) ac_prev=htmldir ;; -htmldir=* | --htmldir=* | --htmldi=* | --htmld=* | --html=* | --htm=* \ | --ht=*) htmldir=$ac_optarg ;; -includedir | --includedir | --includedi | --included | --include \ | --includ | --inclu | --incl | --inc) ac_prev=includedir ;; -includedir=* | --includedir=* | --includedi=* | --included=* | --include=* \ | --includ=* | --inclu=* | --incl=* | --inc=*) includedir=$ac_optarg ;; -infodir | --infodir | --infodi | --infod | --info | --inf) ac_prev=infodir ;; -infodir=* | --infodir=* | --infodi=* | --infod=* | --info=* | --inf=*) infodir=$ac_optarg ;; -libdir | --libdir | --libdi | --libd) ac_prev=libdir ;; -libdir=* | --libdir=* | --libdi=* | --libd=*) libdir=$ac_optarg ;; -libexecdir | --libexecdir | --libexecdi | --libexecd | --libexec \ | --libexe | --libex | --libe) ac_prev=libexecdir ;; -libexecdir=* | --libexecdir=* | --libexecdi=* | --libexecd=* | --libexec=* \ | --libexe=* | --libex=* | --libe=*) libexecdir=$ac_optarg ;; -localedir | --localedir | --localedi | --localed | --locale) ac_prev=localedir ;; -localedir=* | --localedir=* | --localedi=* | --localed=* | --locale=*) localedir=$ac_optarg ;; -localstatedir | --localstatedir | --localstatedi | --localstated \ | --localstate | --localstat | --localsta | --localst | --locals) ac_prev=localstatedir ;; -localstatedir=* | --localstatedir=* | --localstatedi=* | --localstated=* \ | --localstate=* | --localstat=* | --localsta=* | --localst=* | --locals=*) localstatedir=$ac_optarg ;; -mandir | --mandir | --mandi | --mand | --man | --ma | --m) ac_prev=mandir ;; -mandir=* | --mandir=* | --mandi=* | --mand=* | --man=* | --ma=* | --m=*) mandir=$ac_optarg ;; -nfp | --nfp | --nf) # Obsolete; use --without-fp. with_fp=no ;; -no-create | --no-create | --no-creat | --no-crea | --no-cre \ | --no-cr | --no-c | -n) no_create=yes ;; -no-recursion | --no-recursion | --no-recursio | --no-recursi \ | --no-recurs | --no-recur | --no-recu | --no-rec | --no-re | --no-r) no_recursion=yes ;; -oldincludedir | --oldincludedir | --oldincludedi | --oldincluded \ | --oldinclude | --oldinclud | --oldinclu | --oldincl | --oldinc \ | --oldin | --oldi | --old | --ol | --o) ac_prev=oldincludedir ;; -oldincludedir=* | --oldincludedir=* | --oldincludedi=* | --oldincluded=* \ | --oldinclude=* | --oldinclud=* | --oldinclu=* | --oldincl=* | --oldinc=* \ | --oldin=* | --oldi=* | --old=* | --ol=* | --o=*) oldincludedir=$ac_optarg ;; -prefix | --prefix | --prefi | --pref | --pre | --pr | --p) ac_prev=prefix ;; -prefix=* | --prefix=* | --prefi=* | --pref=* | --pre=* | --pr=* | --p=*) prefix=$ac_optarg ;; -program-prefix | --program-prefix | --program-prefi | --program-pref \ | --program-pre | --program-pr | --program-p) ac_prev=program_prefix ;; -program-prefix=* | --program-prefix=* | --program-prefi=* \ | --program-pref=* | --program-pre=* | --program-pr=* | --program-p=*) program_prefix=$ac_optarg ;; -program-suffix | --program-suffix | --program-suffi | --program-suff \ | --program-suf | --program-su | --program-s) ac_prev=program_suffix ;; -program-suffix=* | --program-suffix=* | --program-suffi=* \ | --program-suff=* | --program-suf=* | --program-su=* | --program-s=*) program_suffix=$ac_optarg ;; -program-transform-name | --program-transform-name \ | --program-transform-nam | --program-transform-na \ | --program-transform-n | --program-transform- \ | --program-transform | --program-transfor \ | --program-transfo | --program-transf \ | --program-trans | --program-tran \ | --progr-tra | --program-tr | --program-t) ac_prev=program_transform_name ;; -program-transform-name=* | --program-transform-name=* \ | --program-transform-nam=* | --program-transform-na=* \ | --program-transform-n=* | --program-transform-=* \ | --program-transform=* | --program-transfor=* \ | --program-transfo=* | --program-transf=* \ | --program-trans=* | --program-tran=* \ | --progr-tra=* | --program-tr=* | --program-t=*) program_transform_name=$ac_optarg ;; -pdfdir | --pdfdir | --pdfdi | --pdfd | --pdf | --pd) ac_prev=pdfdir ;; -pdfdir=* | --pdfdir=* | --pdfdi=* | --pdfd=* | --pdf=* | --pd=*) pdfdir=$ac_optarg ;; -psdir | --psdir | --psdi | --psd | --ps) ac_prev=psdir ;; -psdir=* | --psdir=* | --psdi=* | --psd=* | --ps=*) psdir=$ac_optarg ;; -q | -quiet | --quiet | --quie | --qui | --qu | --q \ | -silent | --silent | --silen | --sile | --sil) silent=yes ;; -sbindir | --sbindir | --sbindi | --sbind | --sbin | --sbi | --sb) ac_prev=sbindir ;; -sbindir=* | --sbindir=* | --sbindi=* | --sbind=* | --sbin=* \ | --sbi=* | --sb=*) sbindir=$ac_optarg ;; -sharedstatedir | --sharedstatedir | --sharedstatedi \ | --sharedstated | --sharedstate | --sharedstat | --sharedsta \ | --sharedst | --shareds | --shared | --share | --shar \ | --sha | --sh) ac_prev=sharedstatedir ;; -sharedstatedir=* | --sharedstatedir=* | --sharedstatedi=* \ | --sharedstated=* | --sharedstate=* | --sharedstat=* | --sharedsta=* \ | --sharedst=* | --shareds=* | --shared=* | --share=* | --shar=* \ | --sha=* | --sh=*) sharedstatedir=$ac_optarg ;; -site | --site | --sit) ac_prev=site ;; -site=* | --site=* | --sit=*) site=$ac_optarg ;; -srcdir | --srcdir | --srcdi | --srcd | --src | --sr) ac_prev=srcdir ;; -srcdir=* | --srcdir=* | --srcdi=* | --srcd=* | --src=* | --sr=*) srcdir=$ac_optarg ;; -sysconfdir | --sysconfdir | --sysconfdi | --sysconfd | --sysconf \ | --syscon | --sysco | --sysc | --sys | --sy) ac_prev=sysconfdir ;; -sysconfdir=* | --sysconfdir=* | --sysconfdi=* | --sysconfd=* | --sysconf=* \ | --syscon=* | --sysco=* | --sysc=* | --sys=* | --sy=*) sysconfdir=$ac_optarg ;; -target | --target | --targe | --targ | --tar | --ta | --t) ac_prev=target_alias ;; -target=* | --target=* | --targe=* | --targ=* | --tar=* | --ta=* | --t=*) target_alias=$ac_optarg ;; -v | -verbose | --verbose | --verbos | --verbo | --verb) verbose=yes ;; -version | --version | --versio | --versi | --vers | -V) ac_init_version=: ;; -with-* | --with-*) ac_useropt=`expr "x$ac_option" : 'x-*with-\([^=]*\)'` # Reject names that are not valid shell variable names. expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && as_fn_error $? "invalid package name: $ac_useropt" ac_useropt_orig=$ac_useropt ac_useropt=`$as_echo "$ac_useropt" | sed 's/[-+.]/_/g'` case $ac_user_opts in *" "with_$ac_useropt" "*) ;; *) ac_unrecognized_opts="$ac_unrecognized_opts$ac_unrecognized_sep--with-$ac_useropt_orig" ac_unrecognized_sep=', ';; esac eval with_$ac_useropt=\$ac_optarg ;; -without-* | --without-*) ac_useropt=`expr "x$ac_option" : 'x-*without-\(.*\)'` # Reject names that are not valid shell variable names. expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && as_fn_error $? "invalid package name: $ac_useropt" ac_useropt_orig=$ac_useropt ac_useropt=`$as_echo "$ac_useropt" | sed 's/[-+.]/_/g'` case $ac_user_opts in *" "with_$ac_useropt" "*) ;; *) ac_unrecognized_opts="$ac_unrecognized_opts$ac_unrecognized_sep--without-$ac_useropt_orig" ac_unrecognized_sep=', ';; esac eval with_$ac_useropt=no ;; --x) # Obsolete; use --with-x. with_x=yes ;; -x-includes | --x-includes | --x-include | --x-includ | --x-inclu \ | --x-incl | --x-inc | --x-in | --x-i) ac_prev=x_includes ;; -x-includes=* | --x-includes=* | --x-include=* | --x-includ=* | --x-inclu=* \ | --x-incl=* | --x-inc=* | --x-in=* | --x-i=*) x_includes=$ac_optarg ;; -x-libraries | --x-libraries | --x-librarie | --x-librari \ | --x-librar | --x-libra | --x-libr | --x-lib | --x-li | --x-l) ac_prev=x_libraries ;; -x-libraries=* | --x-libraries=* | --x-librarie=* | --x-librari=* \ | --x-librar=* | --x-libra=* | --x-libr=* | --x-lib=* | --x-li=* | --x-l=*) x_libraries=$ac_optarg ;; -*) as_fn_error $? "unrecognized option: \`$ac_option' Try \`$0 --help' for more information" ;; *=*) ac_envvar=`expr "x$ac_option" : 'x\([^=]*\)='` # Reject names that are not valid shell variable names. case $ac_envvar in #( '' | [0-9]* | *[!_$as_cr_alnum]* ) as_fn_error $? "invalid variable name: \`$ac_envvar'" ;; esac eval $ac_envvar=\$ac_optarg export $ac_envvar ;; *) # FIXME: should be removed in autoconf 3.0. $as_echo "$as_me: WARNING: you should use --build, --host, --target" >&2 expr "x$ac_option" : ".*[^-._$as_cr_alnum]" >/dev/null && $as_echo "$as_me: WARNING: invalid host type: $ac_option" >&2 : "${build_alias=$ac_option} ${host_alias=$ac_option} ${target_alias=$ac_option}" ;; esac done if test -n "$ac_prev"; then ac_option=--`echo $ac_prev | sed 's/_/-/g'` as_fn_error $? "missing argument to $ac_option" fi if test -n "$ac_unrecognized_opts"; then case $enable_option_checking in no) ;; fatal) as_fn_error $? "unrecognized options: $ac_unrecognized_opts" ;; *) $as_echo "$as_me: WARNING: unrecognized options: $ac_unrecognized_opts" >&2 ;; esac fi # Check all directory arguments for consistency. for ac_var in exec_prefix prefix bindir sbindir libexecdir datarootdir \ datadir sysconfdir sharedstatedir localstatedir includedir \ oldincludedir docdir infodir htmldir dvidir pdfdir psdir \ libdir localedir mandir do eval ac_val=\$$ac_var # Remove trailing slashes. case $ac_val in */ ) ac_val=`expr "X$ac_val" : 'X\(.*[^/]\)' \| "X$ac_val" : 'X\(.*\)'` eval $ac_var=\$ac_val;; esac # Be sure to have absolute directory names. case $ac_val in [\\/$]* | ?:[\\/]* ) continue;; NONE | '' ) case $ac_var in *prefix ) continue;; esac;; esac as_fn_error $? "expected an absolute directory name for --$ac_var: $ac_val" done # There might be people who depend on the old broken behavior: `$host' # used to hold the argument of --host etc. # FIXME: To remove some day. build=$build_alias host=$host_alias target=$target_alias # FIXME: To remove some day. if test "x$host_alias" != x; then if test "x$build_alias" = x; then cross_compiling=maybe elif test "x$build_alias" != "x$host_alias"; then cross_compiling=yes fi fi ac_tool_prefix= test -n "$host_alias" && ac_tool_prefix=$host_alias- test "$silent" = yes && exec 6>/dev/null ac_pwd=`pwd` && test -n "$ac_pwd" && ac_ls_di=`ls -di .` && ac_pwd_ls_di=`cd "$ac_pwd" && ls -di .` || as_fn_error $? "working directory cannot be determined" test "X$ac_ls_di" = "X$ac_pwd_ls_di" || as_fn_error $? "pwd does not report name of working directory" # Find the source files, if location was not specified. if test -z "$srcdir"; then ac_srcdir_defaulted=yes # Try the directory containing this script, then the parent directory. ac_confdir=`$as_dirname -- "$as_myself" || $as_expr X"$as_myself" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ X"$as_myself" : 'X\(//\)[^/]' \| \ X"$as_myself" : 'X\(//\)$' \| \ X"$as_myself" : 'X\(/\)' \| . 2>/dev/null || $as_echo X"$as_myself" | sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ s//\1/ q } /^X\(\/\/\)[^/].*/{ s//\1/ q } /^X\(\/\/\)$/{ s//\1/ q } /^X\(\/\).*/{ s//\1/ q } s/.*/./; q'` srcdir=$ac_confdir if test ! -r "$srcdir/$ac_unique_file"; then srcdir=.. fi else ac_srcdir_defaulted=no fi if test ! -r "$srcdir/$ac_unique_file"; then test "$ac_srcdir_defaulted" = yes && srcdir="$ac_confdir or .." as_fn_error $? "cannot find sources ($ac_unique_file) in $srcdir" fi ac_msg="sources are in $srcdir, but \`cd $srcdir' does not work" ac_abs_confdir=`( cd "$srcdir" && test -r "./$ac_unique_file" || as_fn_error $? "$ac_msg" pwd)` # When building in place, set srcdir=. if test "$ac_abs_confdir" = "$ac_pwd"; then srcdir=. fi # Remove unnecessary trailing slashes from srcdir. # Double slashes in file names in object file debugging info # mess up M-x gdb in Emacs. case $srcdir in */) srcdir=`expr "X$srcdir" : 'X\(.*[^/]\)' \| "X$srcdir" : 'X\(.*\)'`;; esac for ac_var in $ac_precious_vars; do eval ac_env_${ac_var}_set=\${${ac_var}+set} eval ac_env_${ac_var}_value=\$${ac_var} eval ac_cv_env_${ac_var}_set=\${${ac_var}+set} eval ac_cv_env_${ac_var}_value=\$${ac_var} done # # Report the --help message. # if test "$ac_init_help" = "long"; then # Omit some internal or obsolete options to make the list less imposing. # This message is too long to be a string in the A/UX 3.1 sh. cat <<_ACEOF \`configure' configures onedrive v2.5.5 to adapt to many kinds of systems. Usage: $0 [OPTION]... [VAR=VALUE]... To assign environment variables (e.g., CC, CFLAGS...), specify them as VAR=VALUE. See below for descriptions of some of the useful variables. Defaults for the options are specified in brackets. Configuration: -h, --help display this help and exit --help=short display options specific to this package --help=recursive display the short help of all the included packages -V, --version display version information and exit -q, --quiet, --silent do not print \`checking ...' messages --cache-file=FILE cache test results in FILE [disabled] -C, --config-cache alias for \`--cache-file=config.cache' -n, --no-create do not create output files --srcdir=DIR find the sources in DIR [configure dir or \`..'] Installation directories: --prefix=PREFIX install architecture-independent files in PREFIX [$ac_default_prefix] --exec-prefix=EPREFIX install architecture-dependent files in EPREFIX [PREFIX] By default, \`make install' will install all the files in \`$ac_default_prefix/bin', \`$ac_default_prefix/lib' etc. You can specify an installation prefix other than \`$ac_default_prefix' using \`--prefix', for instance \`--prefix=\$HOME'. For better control, use the options below. Fine tuning of the installation directories: --bindir=DIR user executables [EPREFIX/bin] --sbindir=DIR system admin executables [EPREFIX/sbin] --libexecdir=DIR program executables [EPREFIX/libexec] --sysconfdir=DIR read-only single-machine data [PREFIX/etc] --sharedstatedir=DIR modifiable architecture-independent data [PREFIX/com] --localstatedir=DIR modifiable single-machine data [PREFIX/var] --libdir=DIR object code libraries [EPREFIX/lib] --includedir=DIR C header files [PREFIX/include] --oldincludedir=DIR C header files for non-gcc [/usr/include] --datarootdir=DIR read-only arch.-independent data root [PREFIX/share] --datadir=DIR read-only architecture-independent data [DATAROOTDIR] --infodir=DIR info documentation [DATAROOTDIR/info] --localedir=DIR locale-dependent data [DATAROOTDIR/locale] --mandir=DIR man documentation [DATAROOTDIR/man] --docdir=DIR documentation root [DATAROOTDIR/doc/onedrive] --htmldir=DIR html documentation [DOCDIR] --dvidir=DIR dvi documentation [DOCDIR] --pdfdir=DIR pdf documentation [DOCDIR] --psdir=DIR ps documentation [DOCDIR] _ACEOF cat <<\_ACEOF _ACEOF fi if test -n "$ac_init_help"; then case $ac_init_help in short | recursive ) echo "Configuration of onedrive v2.5.5:";; esac cat <<\_ACEOF Optional Features: --disable-option-checking ignore unrecognized --enable/--with options --disable-FEATURE do not include FEATURE (same as --enable-FEATURE=no) --enable-FEATURE[=ARG] include FEATURE [ARG=yes] --disable-version-check Disable checks of compiler version during configure time --enable-notifications Enable desktop notifications via libnotify --enable-completions Install shell completions for bash, zsh, and fish --enable-debug Pass debug option to the compiler Optional Packages: --with-PACKAGE[=ARG] use PACKAGE [ARG=yes] --without-PACKAGE do not use PACKAGE (same as --with-PACKAGE=no) --with-systemdsystemunitdir=DIR Directory for systemd system service files --with-systemduserunitdir=DIR Directory for systemd user service files --with-bash-completion-dir=DIR Directory for bash completion files --with-zsh-completion-dir=DIR Directory for zsh completion files --with-fish-completion-dir=DIR Directory for fish completion files Some influential environment variables: DC D compiler executable DCFLAGS flags for D compiler PKG_CONFIG path to pkg-config utility PKG_CONFIG_PATH directories to add to pkg-config's search path PKG_CONFIG_LIBDIR path overriding pkg-config's built-in search path curl_CFLAGS C compiler flags for curl, overriding pkg-config curl_LIBS linker flags for curl, overriding pkg-config sqlite_CFLAGS C compiler flags for sqlite, overriding pkg-config sqlite_LIBS linker flags for sqlite, overriding pkg-config notify_CFLAGS C compiler flags for notify, overriding pkg-config notify_LIBS linker flags for notify, overriding pkg-config bashcompdir value of completionsdir for bash-completion, overriding pkg-config Use these variables to override the choices made by `configure' or to help it to find libraries and programs with nonstandard names/locations. Report bugs to . _ACEOF ac_status=$? fi if test "$ac_init_help" = "recursive"; then # If there are subdirs, report their specific --help. for ac_dir in : $ac_subdirs_all; do test "x$ac_dir" = x: && continue test -d "$ac_dir" || { cd "$srcdir" && ac_pwd=`pwd` && srcdir=. && test -d "$ac_dir"; } || continue ac_builddir=. case "$ac_dir" in .) ac_dir_suffix= ac_top_builddir_sub=. ac_top_build_prefix= ;; *) ac_dir_suffix=/`$as_echo "$ac_dir" | sed 's|^\.[\\/]||'` # A ".." for each directory in $ac_dir_suffix. ac_top_builddir_sub=`$as_echo "$ac_dir_suffix" | sed 's|/[^\\/]*|/..|g;s|/||'` case $ac_top_builddir_sub in "") ac_top_builddir_sub=. ac_top_build_prefix= ;; *) ac_top_build_prefix=$ac_top_builddir_sub/ ;; esac ;; esac ac_abs_top_builddir=$ac_pwd ac_abs_builddir=$ac_pwd$ac_dir_suffix # for backward compatibility: ac_top_builddir=$ac_top_build_prefix case $srcdir in .) # We are building in place. ac_srcdir=. ac_top_srcdir=$ac_top_builddir_sub ac_abs_top_srcdir=$ac_pwd ;; [\\/]* | ?:[\\/]* ) # Absolute name. ac_srcdir=$srcdir$ac_dir_suffix; ac_top_srcdir=$srcdir ac_abs_top_srcdir=$srcdir ;; *) # Relative name. ac_srcdir=$ac_top_build_prefix$srcdir$ac_dir_suffix ac_top_srcdir=$ac_top_build_prefix$srcdir ac_abs_top_srcdir=$ac_pwd/$srcdir ;; esac ac_abs_srcdir=$ac_abs_top_srcdir$ac_dir_suffix cd "$ac_dir" || { ac_status=$?; continue; } # Check for guested configure. if test -f "$ac_srcdir/configure.gnu"; then echo && $SHELL "$ac_srcdir/configure.gnu" --help=recursive elif test -f "$ac_srcdir/configure"; then echo && $SHELL "$ac_srcdir/configure" --help=recursive else $as_echo "$as_me: WARNING: no configuration information is in $ac_dir" >&2 fi || ac_status=$? cd "$ac_pwd" || { ac_status=$?; break; } done fi test -n "$ac_init_help" && exit $ac_status if $ac_init_version; then cat <<\_ACEOF onedrive configure v2.5.5 generated by GNU Autoconf 2.69 Copyright (C) 2012 Free Software Foundation, Inc. This configure script is free software; the Free Software Foundation gives unlimited permission to copy, distribute and modify it. _ACEOF exit fi ## ------------------------ ## ## Autoconf initialization. ## ## ------------------------ ## cat >config.log <<_ACEOF This file contains any messages produced by compilers while running configure, to aid debugging if configure makes a mistake. It was created by onedrive $as_me v2.5.5, which was generated by GNU Autoconf 2.69. Invocation command line was $ $0 $@ _ACEOF exec 5>>config.log { cat <<_ASUNAME ## --------- ## ## Platform. ## ## --------- ## hostname = `(hostname || uname -n) 2>/dev/null | sed 1q` uname -m = `(uname -m) 2>/dev/null || echo unknown` uname -r = `(uname -r) 2>/dev/null || echo unknown` uname -s = `(uname -s) 2>/dev/null || echo unknown` uname -v = `(uname -v) 2>/dev/null || echo unknown` /usr/bin/uname -p = `(/usr/bin/uname -p) 2>/dev/null || echo unknown` /bin/uname -X = `(/bin/uname -X) 2>/dev/null || echo unknown` /bin/arch = `(/bin/arch) 2>/dev/null || echo unknown` /usr/bin/arch -k = `(/usr/bin/arch -k) 2>/dev/null || echo unknown` /usr/convex/getsysinfo = `(/usr/convex/getsysinfo) 2>/dev/null || echo unknown` /usr/bin/hostinfo = `(/usr/bin/hostinfo) 2>/dev/null || echo unknown` /bin/machine = `(/bin/machine) 2>/dev/null || echo unknown` /usr/bin/oslevel = `(/usr/bin/oslevel) 2>/dev/null || echo unknown` /bin/universe = `(/bin/universe) 2>/dev/null || echo unknown` _ASUNAME as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS test -z "$as_dir" && as_dir=. $as_echo "PATH: $as_dir" done IFS=$as_save_IFS } >&5 cat >&5 <<_ACEOF ## ----------- ## ## Core tests. ## ## ----------- ## _ACEOF # Keep a trace of the command line. # Strip out --no-create and --no-recursion so they do not pile up. # Strip out --silent because we don't want to record it for future runs. # Also quote any args containing shell meta-characters. # Make two passes to allow for proper duplicate-argument suppression. ac_configure_args= ac_configure_args0= ac_configure_args1= ac_must_keep_next=false for ac_pass in 1 2 do for ac_arg do case $ac_arg in -no-create | --no-c* | -n | -no-recursion | --no-r*) continue ;; -q | -quiet | --quiet | --quie | --qui | --qu | --q \ | -silent | --silent | --silen | --sile | --sil) continue ;; *\'*) ac_arg=`$as_echo "$ac_arg" | sed "s/'/'\\\\\\\\''/g"` ;; esac case $ac_pass in 1) as_fn_append ac_configure_args0 " '$ac_arg'" ;; 2) as_fn_append ac_configure_args1 " '$ac_arg'" if test $ac_must_keep_next = true; then ac_must_keep_next=false # Got value, back to normal. else case $ac_arg in *=* | --config-cache | -C | -disable-* | --disable-* \ | -enable-* | --enable-* | -gas | --g* | -nfp | --nf* \ | -q | -quiet | --q* | -silent | --sil* | -v | -verb* \ | -with-* | --with-* | -without-* | --without-* | --x) case "$ac_configure_args0 " in "$ac_configure_args1"*" '$ac_arg' "* ) continue ;; esac ;; -* ) ac_must_keep_next=true ;; esac fi as_fn_append ac_configure_args " '$ac_arg'" ;; esac done done { ac_configure_args0=; unset ac_configure_args0;} { ac_configure_args1=; unset ac_configure_args1;} # When interrupted or exit'd, cleanup temporary files, and complete # config.log. We remove comments because anyway the quotes in there # would cause problems or look ugly. # WARNING: Use '\'' to represent an apostrophe within the trap. # WARNING: Do not start the trap code with a newline, due to a FreeBSD 4.0 bug. trap 'exit_status=$? # Save into config.log some information that might help in debugging. { echo $as_echo "## ---------------- ## ## Cache variables. ## ## ---------------- ##" echo # The following way of writing the cache mishandles newlines in values, ( for ac_var in `(set) 2>&1 | sed -n '\''s/^\([a-zA-Z_][a-zA-Z0-9_]*\)=.*/\1/p'\''`; do eval ac_val=\$$ac_var case $ac_val in #( *${as_nl}*) case $ac_var in #( *_cv_*) { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: cache variable $ac_var contains a newline" >&5 $as_echo "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} ;; esac case $ac_var in #( _ | IFS | as_nl) ;; #( BASH_ARGV | BASH_SOURCE) eval $ac_var= ;; #( *) { eval $ac_var=; unset $ac_var;} ;; esac ;; esac done (set) 2>&1 | case $as_nl`(ac_space='\'' '\''; set) 2>&1` in #( *${as_nl}ac_space=\ *) sed -n \ "s/'\''/'\''\\\\'\'''\''/g; s/^\\([_$as_cr_alnum]*_cv_[_$as_cr_alnum]*\\)=\\(.*\\)/\\1='\''\\2'\''/p" ;; #( *) sed -n "/^[_$as_cr_alnum]*_cv_[_$as_cr_alnum]*=/p" ;; esac | sort ) echo $as_echo "## ----------------- ## ## Output variables. ## ## ----------------- ##" echo for ac_var in $ac_subst_vars do eval ac_val=\$$ac_var case $ac_val in *\'\''*) ac_val=`$as_echo "$ac_val" | sed "s/'\''/'\''\\\\\\\\'\'''\''/g"`;; esac $as_echo "$ac_var='\''$ac_val'\''" done | sort echo if test -n "$ac_subst_files"; then $as_echo "## ------------------- ## ## File substitutions. ## ## ------------------- ##" echo for ac_var in $ac_subst_files do eval ac_val=\$$ac_var case $ac_val in *\'\''*) ac_val=`$as_echo "$ac_val" | sed "s/'\''/'\''\\\\\\\\'\'''\''/g"`;; esac $as_echo "$ac_var='\''$ac_val'\''" done | sort echo fi if test -s confdefs.h; then $as_echo "## ----------- ## ## confdefs.h. ## ## ----------- ##" echo cat confdefs.h echo fi test "$ac_signal" != 0 && $as_echo "$as_me: caught signal $ac_signal" $as_echo "$as_me: exit $exit_status" } >&5 rm -f core *.core core.conftest.* && rm -f -r conftest* confdefs* conf$$* $ac_clean_files && exit $exit_status ' 0 for ac_signal in 1 2 13 15; do trap 'ac_signal='$ac_signal'; as_fn_exit 1' $ac_signal done ac_signal=0 # confdefs.h avoids OS command line length limits that DEFS can exceed. rm -f -r conftest* confdefs.h $as_echo "/* confdefs.h */" > confdefs.h # Predefined preprocessor variables. cat >>confdefs.h <<_ACEOF #define PACKAGE_NAME "$PACKAGE_NAME" _ACEOF cat >>confdefs.h <<_ACEOF #define PACKAGE_TARNAME "$PACKAGE_TARNAME" _ACEOF cat >>confdefs.h <<_ACEOF #define PACKAGE_VERSION "$PACKAGE_VERSION" _ACEOF cat >>confdefs.h <<_ACEOF #define PACKAGE_STRING "$PACKAGE_STRING" _ACEOF cat >>confdefs.h <<_ACEOF #define PACKAGE_BUGREPORT "$PACKAGE_BUGREPORT" _ACEOF cat >>confdefs.h <<_ACEOF #define PACKAGE_URL "$PACKAGE_URL" _ACEOF # Let the site file select an alternate cache file if it wants to. # Prefer an explicitly selected file to automatically selected ones. ac_site_file1=NONE ac_site_file2=NONE if test -n "$CONFIG_SITE"; then # We do not want a PATH search for config.site. case $CONFIG_SITE in #(( -*) ac_site_file1=./$CONFIG_SITE;; */*) ac_site_file1=$CONFIG_SITE;; *) ac_site_file1=./$CONFIG_SITE;; esac elif test "x$prefix" != xNONE; then ac_site_file1=$prefix/share/config.site ac_site_file2=$prefix/etc/config.site else ac_site_file1=$ac_default_prefix/share/config.site ac_site_file2=$ac_default_prefix/etc/config.site fi for ac_site_file in "$ac_site_file1" "$ac_site_file2" do test "x$ac_site_file" = xNONE && continue if test /dev/null != "$ac_site_file" && test -r "$ac_site_file"; then { $as_echo "$as_me:${as_lineno-$LINENO}: loading site script $ac_site_file" >&5 $as_echo "$as_me: loading site script $ac_site_file" >&6;} sed 's/^/| /' "$ac_site_file" >&5 . "$ac_site_file" \ || { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 $as_echo "$as_me: error: in \`$ac_pwd':" >&2;} as_fn_error $? "failed to load site script $ac_site_file See \`config.log' for more details" "$LINENO" 5; } fi done if test -r "$cache_file"; then # Some versions of bash will fail to source /dev/null (special files # actually), so we avoid doing that. DJGPP emulates it as a regular file. if test /dev/null != "$cache_file" && test -f "$cache_file"; then { $as_echo "$as_me:${as_lineno-$LINENO}: loading cache $cache_file" >&5 $as_echo "$as_me: loading cache $cache_file" >&6;} case $cache_file in [\\/]* | ?:[\\/]* ) . "$cache_file";; *) . "./$cache_file";; esac fi else { $as_echo "$as_me:${as_lineno-$LINENO}: creating cache $cache_file" >&5 $as_echo "$as_me: creating cache $cache_file" >&6;} >$cache_file fi # Check that the precious variables saved in the cache have kept the same # value. ac_cache_corrupted=false for ac_var in $ac_precious_vars; do eval ac_old_set=\$ac_cv_env_${ac_var}_set eval ac_new_set=\$ac_env_${ac_var}_set eval ac_old_val=\$ac_cv_env_${ac_var}_value eval ac_new_val=\$ac_env_${ac_var}_value case $ac_old_set,$ac_new_set in set,) { $as_echo "$as_me:${as_lineno-$LINENO}: error: \`$ac_var' was set to \`$ac_old_val' in the previous run" >&5 $as_echo "$as_me: error: \`$ac_var' was set to \`$ac_old_val' in the previous run" >&2;} ac_cache_corrupted=: ;; ,set) { $as_echo "$as_me:${as_lineno-$LINENO}: error: \`$ac_var' was not set in the previous run" >&5 $as_echo "$as_me: error: \`$ac_var' was not set in the previous run" >&2;} ac_cache_corrupted=: ;; ,);; *) if test "x$ac_old_val" != "x$ac_new_val"; then # differences in whitespace do not lead to failure. ac_old_val_w=`echo x $ac_old_val` ac_new_val_w=`echo x $ac_new_val` if test "$ac_old_val_w" != "$ac_new_val_w"; then { $as_echo "$as_me:${as_lineno-$LINENO}: error: \`$ac_var' has changed since the previous run:" >&5 $as_echo "$as_me: error: \`$ac_var' has changed since the previous run:" >&2;} ac_cache_corrupted=: else { $as_echo "$as_me:${as_lineno-$LINENO}: warning: ignoring whitespace changes in \`$ac_var' since the previous run:" >&5 $as_echo "$as_me: warning: ignoring whitespace changes in \`$ac_var' since the previous run:" >&2;} eval $ac_var=\$ac_old_val fi { $as_echo "$as_me:${as_lineno-$LINENO}: former value: \`$ac_old_val'" >&5 $as_echo "$as_me: former value: \`$ac_old_val'" >&2;} { $as_echo "$as_me:${as_lineno-$LINENO}: current value: \`$ac_new_val'" >&5 $as_echo "$as_me: current value: \`$ac_new_val'" >&2;} fi;; esac # Pass precious variables to config.status. if test "$ac_new_set" = set; then case $ac_new_val in *\'*) ac_arg=$ac_var=`$as_echo "$ac_new_val" | sed "s/'/'\\\\\\\\''/g"` ;; *) ac_arg=$ac_var=$ac_new_val ;; esac case " $ac_configure_args " in *" '$ac_arg' "*) ;; # Avoid dups. Use of quotes ensures accuracy. *) as_fn_append ac_configure_args " '$ac_arg'" ;; esac fi done if $ac_cache_corrupted; then { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 $as_echo "$as_me: error: in \`$ac_pwd':" >&2;} { $as_echo "$as_me:${as_lineno-$LINENO}: error: changes in the environment can compromise the build" >&5 $as_echo "$as_me: error: changes in the environment can compromise the build" >&2;} as_fn_error $? "run \`make distclean' and/or \`rm $cache_file' and start over" "$LINENO" 5 fi ## -------------------- ## ## Main body of script. ## ## -------------------- ## ac_ext=c ac_cpp='$CPP $CPPFLAGS' ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' ac_compiler_gnu=$ac_cv_c_compiler_gnu ac_aux_dir= for ac_dir in "$srcdir" "$srcdir/.." "$srcdir/../.."; do if test -f "$ac_dir/install-sh"; then ac_aux_dir=$ac_dir ac_install_sh="$ac_aux_dir/install-sh -c" break elif test -f "$ac_dir/install.sh"; then ac_aux_dir=$ac_dir ac_install_sh="$ac_aux_dir/install.sh -c" break elif test -f "$ac_dir/shtool"; then ac_aux_dir=$ac_dir ac_install_sh="$ac_aux_dir/shtool install -c" break fi done if test -z "$ac_aux_dir"; then as_fn_error $? "cannot find install-sh, install.sh, or shtool in \"$srcdir\" \"$srcdir/..\" \"$srcdir/../..\"" "$LINENO" 5 fi # These three variables are undocumented and unsupported, # and are intended to be withdrawn in a future Autoconf release. # They can cause serious problems if a builder's source tree is in a directory # whose full name contains unusual characters. ac_config_guess="$SHELL $ac_aux_dir/config.guess" # Please don't use this var. ac_config_sub="$SHELL $ac_aux_dir/config.sub" # Please don't use this var. ac_configure="$SHELL $ac_aux_dir/configure" # Please don't use this var. # Find a good install program. We prefer a C program (faster), # so one script is as good as another. But avoid the broken or # incompatible versions: # SysV /etc/install, /usr/sbin/install # SunOS /usr/etc/install # IRIX /sbin/install # AIX /bin/install # AmigaOS /C/install, which installs bootblocks on floppy discs # AIX 4 /usr/bin/installbsd, which doesn't work without a -g flag # AFS /usr/afsws/bin/install, which mishandles nonexistent args # SVR4 /usr/ucb/install, which tries to use the nonexistent group "staff" # OS/2's system install, which has a completely different semantic # ./install, which can be erroneously created by make from ./install.sh. # Reject install programs that cannot install multiple files. { $as_echo "$as_me:${as_lineno-$LINENO}: checking for a BSD-compatible install" >&5 $as_echo_n "checking for a BSD-compatible install... " >&6; } if test -z "$INSTALL"; then if ${ac_cv_path_install+:} false; then : $as_echo_n "(cached) " >&6 else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS test -z "$as_dir" && as_dir=. # Account for people who put trailing slashes in PATH elements. case $as_dir/ in #(( ./ | .// | /[cC]/* | \ /etc/* | /usr/sbin/* | /usr/etc/* | /sbin/* | /usr/afsws/bin/* | \ ?:[\\/]os2[\\/]install[\\/]* | ?:[\\/]OS2[\\/]INSTALL[\\/]* | \ /usr/ucb/* ) ;; *) # OSF1 and SCO ODT 3.0 have their own names for install. # Don't use installbsd from OSF since it installs stuff as root # by default. for ac_prog in ginstall scoinst install; do for ac_exec_ext in '' $ac_executable_extensions; do if as_fn_executable_p "$as_dir/$ac_prog$ac_exec_ext"; then if test $ac_prog = install && grep dspmsg "$as_dir/$ac_prog$ac_exec_ext" >/dev/null 2>&1; then # AIX install. It has an incompatible calling convention. : elif test $ac_prog = install && grep pwplus "$as_dir/$ac_prog$ac_exec_ext" >/dev/null 2>&1; then # program-specific install script used by HP pwplus--don't use. : else rm -rf conftest.one conftest.two conftest.dir echo one > conftest.one echo two > conftest.two mkdir conftest.dir if "$as_dir/$ac_prog$ac_exec_ext" -c conftest.one conftest.two "`pwd`/conftest.dir" && test -s conftest.one && test -s conftest.two && test -s conftest.dir/conftest.one && test -s conftest.dir/conftest.two then ac_cv_path_install="$as_dir/$ac_prog$ac_exec_ext -c" break 3 fi fi fi done done ;; esac done IFS=$as_save_IFS rm -rf conftest.one conftest.two conftest.dir fi if test "${ac_cv_path_install+set}" = set; then INSTALL=$ac_cv_path_install else # As a last resort, use the slow shell script. Don't cache a # value for INSTALL within a source directory, because that will # break other packages using the cache if that directory is # removed, or if the value is a relative name. INSTALL=$ac_install_sh fi fi { $as_echo "$as_me:${as_lineno-$LINENO}: result: $INSTALL" >&5 $as_echo "$INSTALL" >&6; } # Use test -z because SunOS4 sh mishandles braces in ${var-val}. # It thinks the first close brace ends the variable substitution. test -z "$INSTALL_PROGRAM" && INSTALL_PROGRAM='${INSTALL}' test -z "$INSTALL_SCRIPT" && INSTALL_SCRIPT='${INSTALL}' test -z "$INSTALL_DATA" && INSTALL_DATA='${INSTALL} -m 644' if test "x$ac_cv_env_PKG_CONFIG_set" != "xset"; then if test -n "$ac_tool_prefix"; then # Extract the first word of "${ac_tool_prefix}pkg-config", so it can be a program name with args. set dummy ${ac_tool_prefix}pkg-config; ac_word=$2 { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 $as_echo_n "checking for $ac_word... " >&6; } if ${ac_cv_path_PKG_CONFIG+:} false; then : $as_echo_n "(cached) " >&6 else case $PKG_CONFIG in [\\/]* | ?:[\\/]*) ac_cv_path_PKG_CONFIG="$PKG_CONFIG" # Let the user override the test with a path. ;; *) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS test -z "$as_dir" && as_dir=. for ac_exec_ext in '' $ac_executable_extensions; do if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then ac_cv_path_PKG_CONFIG="$as_dir/$ac_word$ac_exec_ext" $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 break 2 fi done done IFS=$as_save_IFS ;; esac fi PKG_CONFIG=$ac_cv_path_PKG_CONFIG if test -n "$PKG_CONFIG"; then { $as_echo "$as_me:${as_lineno-$LINENO}: result: $PKG_CONFIG" >&5 $as_echo "$PKG_CONFIG" >&6; } else { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 $as_echo "no" >&6; } fi fi if test -z "$ac_cv_path_PKG_CONFIG"; then ac_pt_PKG_CONFIG=$PKG_CONFIG # Extract the first word of "pkg-config", so it can be a program name with args. set dummy pkg-config; ac_word=$2 { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 $as_echo_n "checking for $ac_word... " >&6; } if ${ac_cv_path_ac_pt_PKG_CONFIG+:} false; then : $as_echo_n "(cached) " >&6 else case $ac_pt_PKG_CONFIG in [\\/]* | ?:[\\/]*) ac_cv_path_ac_pt_PKG_CONFIG="$ac_pt_PKG_CONFIG" # Let the user override the test with a path. ;; *) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS test -z "$as_dir" && as_dir=. for ac_exec_ext in '' $ac_executable_extensions; do if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then ac_cv_path_ac_pt_PKG_CONFIG="$as_dir/$ac_word$ac_exec_ext" $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 break 2 fi done done IFS=$as_save_IFS ;; esac fi ac_pt_PKG_CONFIG=$ac_cv_path_ac_pt_PKG_CONFIG if test -n "$ac_pt_PKG_CONFIG"; then { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_pt_PKG_CONFIG" >&5 $as_echo "$ac_pt_PKG_CONFIG" >&6; } else { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 $as_echo "no" >&6; } fi if test "x$ac_pt_PKG_CONFIG" = x; then PKG_CONFIG="" else case $cross_compiling:$ac_tool_warned in yes:) { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 $as_echo "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} ac_tool_warned=yes ;; esac PKG_CONFIG=$ac_pt_PKG_CONFIG fi else PKG_CONFIG="$ac_cv_path_PKG_CONFIG" fi fi if test -n "$PKG_CONFIG"; then _pkg_min_version=0.9.0 { $as_echo "$as_me:${as_lineno-$LINENO}: checking pkg-config is at least version $_pkg_min_version" >&5 $as_echo_n "checking pkg-config is at least version $_pkg_min_version... " >&6; } if $PKG_CONFIG --atleast-pkgconfig-version $_pkg_min_version; then { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5 $as_echo "yes" >&6; } else { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 $as_echo "no" >&6; } PKG_CONFIG="" fi fi for ac_prog in dmd ldmd2 ldc2 do # Extract the first word of "$ac_prog", so it can be a program name with args. set dummy $ac_prog; ac_word=$2 { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 $as_echo_n "checking for $ac_word... " >&6; } if ${ac_cv_prog_DC+:} false; then : $as_echo_n "(cached) " >&6 else if test -n "$DC"; then ac_cv_prog_DC="$DC" # Let the user override the test. else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS test -z "$as_dir" && as_dir=. for ac_exec_ext in '' $ac_executable_extensions; do if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then ac_cv_prog_DC="$ac_prog" $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 break 2 fi done done IFS=$as_save_IFS fi fi DC=$ac_cv_prog_DC if test -n "$DC"; then { $as_echo "$as_me:${as_lineno-$LINENO}: result: $DC" >&5 $as_echo "$DC" >&6; } else { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 $as_echo "no" >&6; } fi test -n "$DC" && break done test -n "$DC" || DC="NOT_FOUND" DC_TYPE= case $(basename $DC) in dmd) DC_TYPE=dmd ;; ldmd2) DC_TYPE=dmd ;; ldc2) DC_TYPE=ldc ;; NOT_FOUND) as_fn_error 1 "Could not find any compatible D compiler" "$LINENO" 5 esac vercomp () { IFS=. read -r a0 a1 a2 aa <' $bb then return 1 else return 0 fi fi fi fi } DO_VERSION_CHECK=1 # Check whether --enable-version-check was given. if test "${enable_version_check+set}" = set; then : enableval=$enable_version_check; fi if test "x$enable_version_check" = "xno"; then : DO_VERSION_CHECK=0 fi if test "$DO_VERSION_CHECK" = "1"; then : { $as_echo "$as_me:${as_lineno-$LINENO}: checking version of D compiler" >&5 $as_echo_n "checking version of D compiler... " >&6; } # check for valid versions case $(basename $DC) in ldmd2|ldc2) # LDC - the LLVM D compiler (1.12.0): ... VERSION=`$DC --version` # remove everything up to first ( VERSION=${VERSION#* (} # remove everything after ): VERSION=${VERSION%%):*} # now version should be something like L.M.N MINVERSION=1.18.0 ;; dmd) # DMD64 D Compiler v2.085.1\n... VERSION=`$DC --version | tr '\n' ' '` VERSION=${VERSION#*Compiler v} VERSION=${VERSION%% *} # now version should be something like L.M.N MINVERSION=2.088.0 ;; esac { $as_echo "$as_me:${as_lineno-$LINENO}: result: $VERSION" >&5 $as_echo "$VERSION" >&6; } vercomp $MINVERSION $VERSION if test $? = 1 then as_fn_error 1 "Compiler version insufficient, current compiler version $VERSION, minimum version $MINVERSION" "$LINENO" 5 fi #echo "MINVERSION=$MINVERSION VERSION=$VERSION" fi PACKAGE_DATE="March 2025" pkg_failed=no { $as_echo "$as_me:${as_lineno-$LINENO}: checking for curl" >&5 $as_echo_n "checking for curl... " >&6; } if test -n "$curl_CFLAGS"; then pkg_cv_curl_CFLAGS="$curl_CFLAGS" elif test -n "$PKG_CONFIG"; then if test -n "$PKG_CONFIG" && \ { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"libcurl\""; } >&5 ($PKG_CONFIG --exists --print-errors "libcurl") 2>&5 ac_status=$? $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; }; then pkg_cv_curl_CFLAGS=`$PKG_CONFIG --cflags "libcurl" 2>/dev/null` test "x$?" != "x0" && pkg_failed=yes else pkg_failed=yes fi else pkg_failed=untried fi if test -n "$curl_LIBS"; then pkg_cv_curl_LIBS="$curl_LIBS" elif test -n "$PKG_CONFIG"; then if test -n "$PKG_CONFIG" && \ { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"libcurl\""; } >&5 ($PKG_CONFIG --exists --print-errors "libcurl") 2>&5 ac_status=$? $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; }; then pkg_cv_curl_LIBS=`$PKG_CONFIG --libs "libcurl" 2>/dev/null` test "x$?" != "x0" && pkg_failed=yes else pkg_failed=yes fi else pkg_failed=untried fi if test $pkg_failed = yes; then { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 $as_echo "no" >&6; } if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then _pkg_short_errors_supported=yes else _pkg_short_errors_supported=no fi if test $_pkg_short_errors_supported = yes; then curl_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "libcurl" 2>&1` else curl_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "libcurl" 2>&1` fi # Put the nasty error message in config.log where it belongs echo "$curl_PKG_ERRORS" >&5 as_fn_error $? "Package requirements (libcurl) were not met: $curl_PKG_ERRORS Consider adjusting the PKG_CONFIG_PATH environment variable if you installed software in a non-standard prefix. Alternatively, you may set the environment variables curl_CFLAGS and curl_LIBS to avoid the need to call pkg-config. See the pkg-config man page for more details." "$LINENO" 5 elif test $pkg_failed = untried; then { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 $as_echo "no" >&6; } { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 $as_echo "$as_me: error: in \`$ac_pwd':" >&2;} as_fn_error $? "The pkg-config script could not be found or is too old. Make sure it is in your PATH or set the PKG_CONFIG environment variable to the full path to pkg-config. Alternatively, you may set the environment variables curl_CFLAGS and curl_LIBS to avoid the need to call pkg-config. See the pkg-config man page for more details. To get pkg-config, see . See \`config.log' for more details" "$LINENO" 5; } else curl_CFLAGS=$pkg_cv_curl_CFLAGS curl_LIBS=$pkg_cv_curl_LIBS { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5 $as_echo "yes" >&6; } fi pkg_failed=no { $as_echo "$as_me:${as_lineno-$LINENO}: checking for sqlite" >&5 $as_echo_n "checking for sqlite... " >&6; } if test -n "$sqlite_CFLAGS"; then pkg_cv_sqlite_CFLAGS="$sqlite_CFLAGS" elif test -n "$PKG_CONFIG"; then if test -n "$PKG_CONFIG" && \ { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"sqlite3\""; } >&5 ($PKG_CONFIG --exists --print-errors "sqlite3") 2>&5 ac_status=$? $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; }; then pkg_cv_sqlite_CFLAGS=`$PKG_CONFIG --cflags "sqlite3" 2>/dev/null` test "x$?" != "x0" && pkg_failed=yes else pkg_failed=yes fi else pkg_failed=untried fi if test -n "$sqlite_LIBS"; then pkg_cv_sqlite_LIBS="$sqlite_LIBS" elif test -n "$PKG_CONFIG"; then if test -n "$PKG_CONFIG" && \ { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"sqlite3\""; } >&5 ($PKG_CONFIG --exists --print-errors "sqlite3") 2>&5 ac_status=$? $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; }; then pkg_cv_sqlite_LIBS=`$PKG_CONFIG --libs "sqlite3" 2>/dev/null` test "x$?" != "x0" && pkg_failed=yes else pkg_failed=yes fi else pkg_failed=untried fi if test $pkg_failed = yes; then { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 $as_echo "no" >&6; } if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then _pkg_short_errors_supported=yes else _pkg_short_errors_supported=no fi if test $_pkg_short_errors_supported = yes; then sqlite_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "sqlite3" 2>&1` else sqlite_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "sqlite3" 2>&1` fi # Put the nasty error message in config.log where it belongs echo "$sqlite_PKG_ERRORS" >&5 as_fn_error $? "Package requirements (sqlite3) were not met: $sqlite_PKG_ERRORS Consider adjusting the PKG_CONFIG_PATH environment variable if you installed software in a non-standard prefix. Alternatively, you may set the environment variables sqlite_CFLAGS and sqlite_LIBS to avoid the need to call pkg-config. See the pkg-config man page for more details." "$LINENO" 5 elif test $pkg_failed = untried; then { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 $as_echo "no" >&6; } { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 $as_echo "$as_me: error: in \`$ac_pwd':" >&2;} as_fn_error $? "The pkg-config script could not be found or is too old. Make sure it is in your PATH or set the PKG_CONFIG environment variable to the full path to pkg-config. Alternatively, you may set the environment variables sqlite_CFLAGS and sqlite_LIBS to avoid the need to call pkg-config. See the pkg-config man page for more details. To get pkg-config, see . See \`config.log' for more details" "$LINENO" 5; } else sqlite_CFLAGS=$pkg_cv_sqlite_CFLAGS sqlite_LIBS=$pkg_cv_sqlite_LIBS { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5 $as_echo "yes" >&6; } fi # Check whether --with-systemdsystemunitdir was given. if test "${with_systemdsystemunitdir+set}" = set; then : withval=$with_systemdsystemunitdir; else with_systemdsystemunitdir=auto fi if test "x$with_systemdsystemunitdir" = "xyes" -o "x$with_systemdsystemunitdir" = "xauto"; then : def_systemdsystemunitdir=$($PKG_CONFIG --variable=systemdsystemunitdir systemd) if test "x$def_systemdsystemunitdir" = "x"; then : if test "x$with_systemdsystemunitdir" = "xyes"; then : as_fn_error $? "systemd support requested but pkg-config unable to query systemd package" "$LINENO" 5 fi with_systemdsystemunitdir=no else with_systemdsystemunitdir="$def_systemdsystemunitdir" fi fi if test "x$with_systemdsystemunitdir" != "xno"; then : systemdsystemunitdir=$with_systemdsystemunitdir fi # Check whether --with-systemduserunitdir was given. if test "${with_systemduserunitdir+set}" = set; then : withval=$with_systemduserunitdir; else with_systemduserunitdir=auto fi if test "x$with_systemduserunitdir" = "xyes" -o "x$with_systemduserunitdir" = "xauto"; then : def_systemduserunitdir=$($PKG_CONFIG --variable=systemduserunitdir systemd) if test "x$def_systemduserunitdir" = "x"; then : if test "x$with_systemduserunitdir" = "xyes"; then : as_fn_error $? "systemd support requested but pkg-config unable to query systemd package" "$LINENO" 5 fi with_systemduserunitdir=no else with_systemduserunitdir="$def_systemduserunitdir" fi fi if test "x$with_systemduserunitdir" != "xno"; then : systemduserunitdir=$with_systemduserunitdir fi if test "x$with_systemduserunitdir" != "xno" -a "x$with_systemdsystemunitdir" != "xno"; then : havesystemd=yes else havesystemd=no fi HAVE_SYSTEMD=$havesystemd # Check whether --enable-notifications was given. if test "${enable_notifications+set}" = set; then : enableval=$enable_notifications; fi if test "x$enable_notifications" = "xyes"; then : enable_notifications=yes else enable_notifications=no fi if test "x$enable_notifications" = "xyes"; then : pkg_failed=no { $as_echo "$as_me:${as_lineno-$LINENO}: checking for notify" >&5 $as_echo_n "checking for notify... " >&6; } if test -n "$notify_CFLAGS"; then pkg_cv_notify_CFLAGS="$notify_CFLAGS" elif test -n "$PKG_CONFIG"; then if test -n "$PKG_CONFIG" && \ { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"libnotify\""; } >&5 ($PKG_CONFIG --exists --print-errors "libnotify") 2>&5 ac_status=$? $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; }; then pkg_cv_notify_CFLAGS=`$PKG_CONFIG --cflags "libnotify" 2>/dev/null` test "x$?" != "x0" && pkg_failed=yes else pkg_failed=yes fi else pkg_failed=untried fi if test -n "$notify_LIBS"; then pkg_cv_notify_LIBS="$notify_LIBS" elif test -n "$PKG_CONFIG"; then if test -n "$PKG_CONFIG" && \ { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"libnotify\""; } >&5 ($PKG_CONFIG --exists --print-errors "libnotify") 2>&5 ac_status=$? $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; }; then pkg_cv_notify_LIBS=`$PKG_CONFIG --libs "libnotify" 2>/dev/null` test "x$?" != "x0" && pkg_failed=yes else pkg_failed=yes fi else pkg_failed=untried fi if test $pkg_failed = yes; then { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 $as_echo "no" >&6; } if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then _pkg_short_errors_supported=yes else _pkg_short_errors_supported=no fi if test $_pkg_short_errors_supported = yes; then notify_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "libnotify" 2>&1` else notify_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "libnotify" 2>&1` fi # Put the nasty error message in config.log where it belongs echo "$notify_PKG_ERRORS" >&5 enable_notifications=no elif test $pkg_failed = untried; then { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 $as_echo "no" >&6; } enable_notifications=no else notify_CFLAGS=$pkg_cv_notify_CFLAGS notify_LIBS=$pkg_cv_notify_LIBS { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5 $as_echo "yes" >&6; } fi else notify_LIBS="" fi NOTIFICATIONS=$enable_notifications # Conditionally set bsd_inotify_LIBS based on the platform case "$(uname -s)" in Linux) bsd_inotify_LIBS="" ;; FreeBSD) bsd_inotify_LIBS="-L/usr/local/lib -linotify" ;; OpenBSD) bsd_inotify_LIBS="-L/usr/local/lib/inotify -linotify" ;; *) bsd_inotify_LIBS="" ;; esac # Conditionally set dynamic_linker_LIBS based on the platform case "$(uname -s)" in Linux) dynamic_linker_LIBS="-ldl" ;; *) dynamic_linker_LIBS="" ;; esac # Check whether --enable-completions was given. if test "${enable_completions+set}" = set; then : enableval=$enable_completions; fi if test "x$enable_completions" = "xyes"; then : enable_completions=yes else enable_completions=no fi COMPLETIONS=$enable_completions if test "x$enable_completions" = "xyes"; then : # Check whether --with-bash-completion-dir was given. if test "${with_bash_completion_dir+set}" = set; then : withval=$with_bash_completion_dir; else with_bash_completion_dir=auto fi if test "x$with_bash_completion_dir" = "xyes" -o "x$with_bash_completion_dir" = "xauto"; then : if test -n "$bashcompdir"; then pkg_cv_bashcompdir="$bashcompdir" elif test -n "$PKG_CONFIG"; then if test -n "$PKG_CONFIG" && \ { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"bash-completion\""; } >&5 ($PKG_CONFIG --exists --print-errors "bash-completion") 2>&5 ac_status=$? $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; }; then pkg_cv_bashcompdir=`$PKG_CONFIG --variable="completionsdir" "bash-completion" 2>/dev/null` test "x$?" != "x0" && pkg_failed=yes else pkg_failed=yes fi else pkg_failed=untried fi bashcompdir=$pkg_cv_bashcompdir if test "x$bashcompdir" = x""; then : bashcompdir="${sysconfdir}/bash_completion.d" fi with_bash_completion_dir=$bashcompdir fi BASH_COMPLETION_DIR=$with_bash_completion_dir # Check whether --with-zsh-completion-dir was given. if test "${with_zsh_completion_dir+set}" = set; then : withval=$with_zsh_completion_dir; else with_zsh_completion_dir=auto fi if test "x$with_zsh_completion_dir" = "xyes" -o "x$with_zsh_completion_dir" = "xauto"; then : with_zsh_completion_dir="/usr/local/share/zsh/site-functions" fi ZSH_COMPLETION_DIR=$with_zsh_completion_dir # Check whether --with-fish-completion-dir was given. if test "${with_fish_completion_dir+set}" = set; then : withval=$with_fish_completion_dir; else with_fish_completion_dir=auto fi if test "x$with_fish_completion_dir" = "xyes" -o "x$with_fish_completion_dir" = "xauto"; then : with_fish_completion_dir="/usr/local/share/fish/completions" fi FISH_COMPLETION_DIR=$with_fish_completion_dir fi # Check whether --enable-debug was given. if test "${enable_debug+set}" = set; then : enableval=$enable_debug; fi if test "x$enable_debug" = "xyes"; then : DEBUG=yes else DEBUG=no fi ac_config_files="$ac_config_files Makefile contrib/pacman/PKGBUILD contrib/spec/onedrive.spec onedrive.1 contrib/systemd/onedrive.service contrib/systemd/onedrive@.service" cat >confcache <<\_ACEOF # This file is a shell script that caches the results of configure # tests run on this system so they can be shared between configure # scripts and configure runs, see configure's option --config-cache. # It is not useful on other systems. If it contains results you don't # want to keep, you may remove or edit it. # # config.status only pays attention to the cache file if you give it # the --recheck option to rerun configure. # # `ac_cv_env_foo' variables (set or unset) will be overridden when # loading this file, other *unset* `ac_cv_foo' will be assigned the # following values. _ACEOF # The following way of writing the cache mishandles newlines in values, # but we know of no workaround that is simple, portable, and efficient. # So, we kill variables containing newlines. # Ultrix sh set writes to stderr and can't be redirected directly, # and sets the high bit in the cache file unless we assign to the vars. ( for ac_var in `(set) 2>&1 | sed -n 's/^\([a-zA-Z_][a-zA-Z0-9_]*\)=.*/\1/p'`; do eval ac_val=\$$ac_var case $ac_val in #( *${as_nl}*) case $ac_var in #( *_cv_*) { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: cache variable $ac_var contains a newline" >&5 $as_echo "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} ;; esac case $ac_var in #( _ | IFS | as_nl) ;; #( BASH_ARGV | BASH_SOURCE) eval $ac_var= ;; #( *) { eval $ac_var=; unset $ac_var;} ;; esac ;; esac done (set) 2>&1 | case $as_nl`(ac_space=' '; set) 2>&1` in #( *${as_nl}ac_space=\ *) # `set' does not quote correctly, so add quotes: double-quote # substitution turns \\\\ into \\, and sed turns \\ into \. sed -n \ "s/'/'\\\\''/g; s/^\\([_$as_cr_alnum]*_cv_[_$as_cr_alnum]*\\)=\\(.*\\)/\\1='\\2'/p" ;; #( *) # `set' quotes correctly as required by POSIX, so do not add quotes. sed -n "/^[_$as_cr_alnum]*_cv_[_$as_cr_alnum]*=/p" ;; esac | sort ) | sed ' /^ac_cv_env_/b end t clear :clear s/^\([^=]*\)=\(.*[{}].*\)$/test "${\1+set}" = set || &/ t end s/^\([^=]*\)=\(.*\)$/\1=${\1=\2}/ :end' >>confcache if diff "$cache_file" confcache >/dev/null 2>&1; then :; else if test -w "$cache_file"; then if test "x$cache_file" != "x/dev/null"; then { $as_echo "$as_me:${as_lineno-$LINENO}: updating cache $cache_file" >&5 $as_echo "$as_me: updating cache $cache_file" >&6;} if test ! -f "$cache_file" || test -h "$cache_file"; then cat confcache >"$cache_file" else case $cache_file in #( */* | ?:*) mv -f confcache "$cache_file"$$ && mv -f "$cache_file"$$ "$cache_file" ;; #( *) mv -f confcache "$cache_file" ;; esac fi fi else { $as_echo "$as_me:${as_lineno-$LINENO}: not updating unwritable cache $cache_file" >&5 $as_echo "$as_me: not updating unwritable cache $cache_file" >&6;} fi fi rm -f confcache test "x$prefix" = xNONE && prefix=$ac_default_prefix # Let make expand exec_prefix. test "x$exec_prefix" = xNONE && exec_prefix='${prefix}' # Transform confdefs.h into DEFS. # Protect against shell expansion while executing Makefile rules. # Protect against Makefile macro expansion. # # If the first sed substitution is executed (which looks for macros that # take arguments), then branch to the quote section. Otherwise, # look for a macro that doesn't take arguments. ac_script=' :mline /\\$/{ N s,\\\n,, b mline } t clear :clear s/^[ ]*#[ ]*define[ ][ ]*\([^ (][^ (]*([^)]*)\)[ ]*\(.*\)/-D\1=\2/g t quote s/^[ ]*#[ ]*define[ ][ ]*\([^ ][^ ]*\)[ ]*\(.*\)/-D\1=\2/g t quote b any :quote s/[ `~#$^&*(){}\\|;'\''"<>?]/\\&/g s/\[/\\&/g s/\]/\\&/g s/\$/$$/g H :any ${ g s/^\n// s/\n/ /g p } ' DEFS=`sed -n "$ac_script" confdefs.h` ac_libobjs= ac_ltlibobjs= U= for ac_i in : $LIBOBJS; do test "x$ac_i" = x: && continue # 1. Remove the extension, and $U if already installed. ac_script='s/\$U\././;s/\.o$//;s/\.obj$//' ac_i=`$as_echo "$ac_i" | sed "$ac_script"` # 2. Prepend LIBOBJDIR. When used with automake>=1.10 LIBOBJDIR # will be set to the directory where LIBOBJS objects are built. as_fn_append ac_libobjs " \${LIBOBJDIR}$ac_i\$U.$ac_objext" as_fn_append ac_ltlibobjs " \${LIBOBJDIR}$ac_i"'$U.lo' done LIBOBJS=$ac_libobjs LTLIBOBJS=$ac_ltlibobjs : "${CONFIG_STATUS=./config.status}" ac_write_fail=0 ac_clean_files_save=$ac_clean_files ac_clean_files="$ac_clean_files $CONFIG_STATUS" { $as_echo "$as_me:${as_lineno-$LINENO}: creating $CONFIG_STATUS" >&5 $as_echo "$as_me: creating $CONFIG_STATUS" >&6;} as_write_fail=0 cat >$CONFIG_STATUS <<_ASEOF || as_write_fail=1 #! $SHELL # Generated by $as_me. # Run this file to recreate the current configuration. # Compiler output produced by configure, useful for debugging # configure, is in config.log if it exists. debug=false ac_cs_recheck=false ac_cs_silent=false SHELL=\${CONFIG_SHELL-$SHELL} export SHELL _ASEOF cat >>$CONFIG_STATUS <<\_ASEOF || as_write_fail=1 ## -------------------- ## ## M4sh Initialization. ## ## -------------------- ## # Be more Bourne compatible DUALCASE=1; export DUALCASE # for MKS sh if test -n "${ZSH_VERSION+set}" && (emulate sh) >/dev/null 2>&1; then : emulate sh NULLCMD=: # Pre-4.2 versions of Zsh do word splitting on ${1+"$@"}, which # is contrary to our usage. Disable this feature. alias -g '${1+"$@"}'='"$@"' setopt NO_GLOB_SUBST else case `(set -o) 2>/dev/null` in #( *posix*) : set -o posix ;; #( *) : ;; esac fi as_nl=' ' export as_nl # Printing a long string crashes Solaris 7 /usr/bin/printf. as_echo='\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\' as_echo=$as_echo$as_echo$as_echo$as_echo$as_echo as_echo=$as_echo$as_echo$as_echo$as_echo$as_echo$as_echo # Prefer a ksh shell builtin over an external printf program on Solaris, # but without wasting forks for bash or zsh. if test -z "$BASH_VERSION$ZSH_VERSION" \ && (test "X`print -r -- $as_echo`" = "X$as_echo") 2>/dev/null; then as_echo='print -r --' as_echo_n='print -rn --' elif (test "X`printf %s $as_echo`" = "X$as_echo") 2>/dev/null; then as_echo='printf %s\n' as_echo_n='printf %s' else if test "X`(/usr/ucb/echo -n -n $as_echo) 2>/dev/null`" = "X-n $as_echo"; then as_echo_body='eval /usr/ucb/echo -n "$1$as_nl"' as_echo_n='/usr/ucb/echo -n' else as_echo_body='eval expr "X$1" : "X\\(.*\\)"' as_echo_n_body='eval arg=$1; case $arg in #( *"$as_nl"*) expr "X$arg" : "X\\(.*\\)$as_nl"; arg=`expr "X$arg" : ".*$as_nl\\(.*\\)"`;; esac; expr "X$arg" : "X\\(.*\\)" | tr -d "$as_nl" ' export as_echo_n_body as_echo_n='sh -c $as_echo_n_body as_echo' fi export as_echo_body as_echo='sh -c $as_echo_body as_echo' fi # The user is always right. if test "${PATH_SEPARATOR+set}" != set; then PATH_SEPARATOR=: (PATH='/bin;/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 && { (PATH='/bin:/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 || PATH_SEPARATOR=';' } fi # IFS # We need space, tab and new line, in precisely that order. Quoting is # there to prevent editors from complaining about space-tab. # (If _AS_PATH_WALK were called with IFS unset, it would disable word # splitting by setting IFS to empty value.) IFS=" "" $as_nl" # Find who we are. Look in the path if we contain no directory separator. as_myself= case $0 in #(( *[\\/]* ) as_myself=$0 ;; *) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS test -z "$as_dir" && as_dir=. test -r "$as_dir/$0" && as_myself=$as_dir/$0 && break done IFS=$as_save_IFS ;; esac # We did not find ourselves, most probably we were run as `sh COMMAND' # in which case we are not to be found in the path. if test "x$as_myself" = x; then as_myself=$0 fi if test ! -f "$as_myself"; then $as_echo "$as_myself: error: cannot find myself; rerun with an absolute file name" >&2 exit 1 fi # Unset variables that we do not need and which cause bugs (e.g. in # pre-3.0 UWIN ksh). But do not cause bugs in bash 2.01; the "|| exit 1" # suppresses any "Segmentation fault" message there. '((' could # trigger a bug in pdksh 5.2.14. for as_var in BASH_ENV ENV MAIL MAILPATH do eval test x\${$as_var+set} = xset \ && ( (unset $as_var) || exit 1) >/dev/null 2>&1 && unset $as_var || : done PS1='$ ' PS2='> ' PS4='+ ' # NLS nuisances. LC_ALL=C export LC_ALL LANGUAGE=C export LANGUAGE # CDPATH. (unset CDPATH) >/dev/null 2>&1 && unset CDPATH # as_fn_error STATUS ERROR [LINENO LOG_FD] # ---------------------------------------- # Output "`basename $0`: error: ERROR" to stderr. If LINENO and LOG_FD are # provided, also output the error to LOG_FD, referencing LINENO. Then exit the # script with STATUS, using 1 if that was 0. as_fn_error () { as_status=$1; test $as_status -eq 0 && as_status=1 if test "$4"; then as_lineno=${as_lineno-"$3"} as_lineno_stack=as_lineno_stack=$as_lineno_stack $as_echo "$as_me:${as_lineno-$LINENO}: error: $2" >&$4 fi $as_echo "$as_me: error: $2" >&2 as_fn_exit $as_status } # as_fn_error # as_fn_set_status STATUS # ----------------------- # Set $? to STATUS, without forking. as_fn_set_status () { return $1 } # as_fn_set_status # as_fn_exit STATUS # ----------------- # Exit the shell with STATUS, even in a "trap 0" or "set -e" context. as_fn_exit () { set +e as_fn_set_status $1 exit $1 } # as_fn_exit # as_fn_unset VAR # --------------- # Portably unset VAR. as_fn_unset () { { eval $1=; unset $1;} } as_unset=as_fn_unset # as_fn_append VAR VALUE # ---------------------- # Append the text in VALUE to the end of the definition contained in VAR. Take # advantage of any shell optimizations that allow amortized linear growth over # repeated appends, instead of the typical quadratic growth present in naive # implementations. if (eval "as_var=1; as_var+=2; test x\$as_var = x12") 2>/dev/null; then : eval 'as_fn_append () { eval $1+=\$2 }' else as_fn_append () { eval $1=\$$1\$2 } fi # as_fn_append # as_fn_arith ARG... # ------------------ # Perform arithmetic evaluation on the ARGs, and store the result in the # global $as_val. Take advantage of shells that can avoid forks. The arguments # must be portable across $(()) and expr. if (eval "test \$(( 1 + 1 )) = 2") 2>/dev/null; then : eval 'as_fn_arith () { as_val=$(( $* )) }' else as_fn_arith () { as_val=`expr "$@" || test $? -eq 1` } fi # as_fn_arith if expr a : '\(a\)' >/dev/null 2>&1 && test "X`expr 00001 : '.*\(...\)'`" = X001; then as_expr=expr else as_expr=false fi if (basename -- /) >/dev/null 2>&1 && test "X`basename -- / 2>&1`" = "X/"; then as_basename=basename else as_basename=false fi if (as_dir=`dirname -- /` && test "X$as_dir" = X/) >/dev/null 2>&1; then as_dirname=dirname else as_dirname=false fi as_me=`$as_basename -- "$0" || $as_expr X/"$0" : '.*/\([^/][^/]*\)/*$' \| \ X"$0" : 'X\(//\)$' \| \ X"$0" : 'X\(/\)' \| . 2>/dev/null || $as_echo X/"$0" | sed '/^.*\/\([^/][^/]*\)\/*$/{ s//\1/ q } /^X\/\(\/\/\)$/{ s//\1/ q } /^X\/\(\/\).*/{ s//\1/ q } s/.*/./; q'` # Avoid depending upon Character Ranges. as_cr_letters='abcdefghijklmnopqrstuvwxyz' as_cr_LETTERS='ABCDEFGHIJKLMNOPQRSTUVWXYZ' as_cr_Letters=$as_cr_letters$as_cr_LETTERS as_cr_digits='0123456789' as_cr_alnum=$as_cr_Letters$as_cr_digits ECHO_C= ECHO_N= ECHO_T= case `echo -n x` in #((((( -n*) case `echo 'xy\c'` in *c*) ECHO_T=' ';; # ECHO_T is single tab character. xy) ECHO_C='\c';; *) echo `echo ksh88 bug on AIX 6.1` > /dev/null ECHO_T=' ';; esac;; *) ECHO_N='-n';; esac rm -f conf$$ conf$$.exe conf$$.file if test -d conf$$.dir; then rm -f conf$$.dir/conf$$.file else rm -f conf$$.dir mkdir conf$$.dir 2>/dev/null fi if (echo >conf$$.file) 2>/dev/null; then if ln -s conf$$.file conf$$ 2>/dev/null; then as_ln_s='ln -s' # ... but there are two gotchas: # 1) On MSYS, both `ln -s file dir' and `ln file dir' fail. # 2) DJGPP < 2.04 has no symlinks; `ln -s' creates a wrapper executable. # In both cases, we have to default to `cp -pR'. ln -s conf$$.file conf$$.dir 2>/dev/null && test ! -f conf$$.exe || as_ln_s='cp -pR' elif ln conf$$.file conf$$ 2>/dev/null; then as_ln_s=ln else as_ln_s='cp -pR' fi else as_ln_s='cp -pR' fi rm -f conf$$ conf$$.exe conf$$.dir/conf$$.file conf$$.file rmdir conf$$.dir 2>/dev/null # as_fn_mkdir_p # ------------- # Create "$as_dir" as a directory, including parents if necessary. as_fn_mkdir_p () { case $as_dir in #( -*) as_dir=./$as_dir;; esac test -d "$as_dir" || eval $as_mkdir_p || { as_dirs= while :; do case $as_dir in #( *\'*) as_qdir=`$as_echo "$as_dir" | sed "s/'/'\\\\\\\\''/g"`;; #'( *) as_qdir=$as_dir;; esac as_dirs="'$as_qdir' $as_dirs" as_dir=`$as_dirname -- "$as_dir" || $as_expr X"$as_dir" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ X"$as_dir" : 'X\(//\)[^/]' \| \ X"$as_dir" : 'X\(//\)$' \| \ X"$as_dir" : 'X\(/\)' \| . 2>/dev/null || $as_echo X"$as_dir" | sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ s//\1/ q } /^X\(\/\/\)[^/].*/{ s//\1/ q } /^X\(\/\/\)$/{ s//\1/ q } /^X\(\/\).*/{ s//\1/ q } s/.*/./; q'` test -d "$as_dir" && break done test -z "$as_dirs" || eval "mkdir $as_dirs" } || test -d "$as_dir" || as_fn_error $? "cannot create directory $as_dir" } # as_fn_mkdir_p if mkdir -p . 2>/dev/null; then as_mkdir_p='mkdir -p "$as_dir"' else test -d ./-p && rmdir ./-p as_mkdir_p=false fi # as_fn_executable_p FILE # ----------------------- # Test if FILE is an executable regular file. as_fn_executable_p () { test -f "$1" && test -x "$1" } # as_fn_executable_p as_test_x='test -x' as_executable_p=as_fn_executable_p # Sed expression to map a string onto a valid CPP name. as_tr_cpp="eval sed 'y%*$as_cr_letters%P$as_cr_LETTERS%;s%[^_$as_cr_alnum]%_%g'" # Sed expression to map a string onto a valid variable name. as_tr_sh="eval sed 'y%*+%pp%;s%[^_$as_cr_alnum]%_%g'" exec 6>&1 ## ----------------------------------- ## ## Main body of $CONFIG_STATUS script. ## ## ----------------------------------- ## _ASEOF test $as_write_fail = 0 && chmod +x $CONFIG_STATUS || ac_write_fail=1 cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 # Save the log message, to keep $0 and so on meaningful, and to # report actual input values of CONFIG_FILES etc. instead of their # values after options handling. ac_log=" This file was extended by onedrive $as_me v2.5.5, which was generated by GNU Autoconf 2.69. Invocation command line was CONFIG_FILES = $CONFIG_FILES CONFIG_HEADERS = $CONFIG_HEADERS CONFIG_LINKS = $CONFIG_LINKS CONFIG_COMMANDS = $CONFIG_COMMANDS $ $0 $@ on `(hostname || uname -n) 2>/dev/null | sed 1q` " _ACEOF case $ac_config_files in *" "*) set x $ac_config_files; shift; ac_config_files=$*;; esac cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 # Files that config.status was made for. config_files="$ac_config_files" _ACEOF cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 ac_cs_usage="\ \`$as_me' instantiates files and other configuration actions from templates according to the current configuration. Unless the files and actions are specified as TAGs, all are instantiated by default. Usage: $0 [OPTION]... [TAG]... -h, --help print this help, then exit -V, --version print version number and configuration settings, then exit --config print configuration, then exit -q, --quiet, --silent do not print progress messages -d, --debug don't remove temporary files --recheck update $as_me by reconfiguring in the same conditions --file=FILE[:TEMPLATE] instantiate the configuration file FILE Configuration files: $config_files Report bugs to ." _ACEOF cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 ac_cs_config="`$as_echo "$ac_configure_args" | sed 's/^ //; s/[\\""\`\$]/\\\\&/g'`" ac_cs_version="\\ onedrive config.status v2.5.5 configured by $0, generated by GNU Autoconf 2.69, with options \\"\$ac_cs_config\\" Copyright (C) 2012 Free Software Foundation, Inc. This config.status script is free software; the Free Software Foundation gives unlimited permission to copy, distribute and modify it." ac_pwd='$ac_pwd' srcdir='$srcdir' INSTALL='$INSTALL' test -n "\$AWK" || AWK=awk _ACEOF cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 # The default lists apply if the user does not specify any file. ac_need_defaults=: while test $# != 0 do case $1 in --*=?*) ac_option=`expr "X$1" : 'X\([^=]*\)='` ac_optarg=`expr "X$1" : 'X[^=]*=\(.*\)'` ac_shift=: ;; --*=) ac_option=`expr "X$1" : 'X\([^=]*\)='` ac_optarg= ac_shift=: ;; *) ac_option=$1 ac_optarg=$2 ac_shift=shift ;; esac case $ac_option in # Handling of the options. -recheck | --recheck | --rechec | --reche | --rech | --rec | --re | --r) ac_cs_recheck=: ;; --version | --versio | --versi | --vers | --ver | --ve | --v | -V ) $as_echo "$ac_cs_version"; exit ;; --config | --confi | --conf | --con | --co | --c ) $as_echo "$ac_cs_config"; exit ;; --debug | --debu | --deb | --de | --d | -d ) debug=: ;; --file | --fil | --fi | --f ) $ac_shift case $ac_optarg in *\'*) ac_optarg=`$as_echo "$ac_optarg" | sed "s/'/'\\\\\\\\''/g"` ;; '') as_fn_error $? "missing file argument" ;; esac as_fn_append CONFIG_FILES " '$ac_optarg'" ac_need_defaults=false;; --he | --h | --help | --hel | -h ) $as_echo "$ac_cs_usage"; exit ;; -q | -quiet | --quiet | --quie | --qui | --qu | --q \ | -silent | --silent | --silen | --sile | --sil | --si | --s) ac_cs_silent=: ;; # This is an error. -*) as_fn_error $? "unrecognized option: \`$1' Try \`$0 --help' for more information." ;; *) as_fn_append ac_config_targets " $1" ac_need_defaults=false ;; esac shift done ac_configure_extra_args= if $ac_cs_silent; then exec 6>/dev/null ac_configure_extra_args="$ac_configure_extra_args --silent" fi _ACEOF cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 if \$ac_cs_recheck; then set X $SHELL '$0' $ac_configure_args \$ac_configure_extra_args --no-create --no-recursion shift \$as_echo "running CONFIG_SHELL=$SHELL \$*" >&6 CONFIG_SHELL='$SHELL' export CONFIG_SHELL exec "\$@" fi _ACEOF cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 exec 5>>config.log { echo sed 'h;s/./-/g;s/^.../## /;s/...$/ ##/;p;x;p;x' <<_ASBOX ## Running $as_me. ## _ASBOX $as_echo "$ac_log" } >&5 _ACEOF cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 _ACEOF cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 # Handling of arguments. for ac_config_target in $ac_config_targets do case $ac_config_target in "Makefile") CONFIG_FILES="$CONFIG_FILES Makefile" ;; "contrib/pacman/PKGBUILD") CONFIG_FILES="$CONFIG_FILES contrib/pacman/PKGBUILD" ;; "contrib/spec/onedrive.spec") CONFIG_FILES="$CONFIG_FILES contrib/spec/onedrive.spec" ;; "onedrive.1") CONFIG_FILES="$CONFIG_FILES onedrive.1" ;; "contrib/systemd/onedrive.service") CONFIG_FILES="$CONFIG_FILES contrib/systemd/onedrive.service" ;; "contrib/systemd/onedrive@.service") CONFIG_FILES="$CONFIG_FILES contrib/systemd/onedrive@.service" ;; *) as_fn_error $? "invalid argument: \`$ac_config_target'" "$LINENO" 5;; esac done # If the user did not use the arguments to specify the items to instantiate, # then the envvar interface is used. Set only those that are not. # We use the long form for the default assignment because of an extremely # bizarre bug on SunOS 4.1.3. if $ac_need_defaults; then test "${CONFIG_FILES+set}" = set || CONFIG_FILES=$config_files fi # Have a temporary directory for convenience. Make it in the build tree # simply because there is no reason against having it here, and in addition, # creating and moving files from /tmp can sometimes cause problems. # Hook for its removal unless debugging. # Note that there is a small window in which the directory will not be cleaned: # after its creation but before its name has been assigned to `$tmp'. $debug || { tmp= ac_tmp= trap 'exit_status=$? : "${ac_tmp:=$tmp}" { test ! -d "$ac_tmp" || rm -fr "$ac_tmp"; } && exit $exit_status ' 0 trap 'as_fn_exit 1' 1 2 13 15 } # Create a (secure) tmp directory for tmp files. { tmp=`(umask 077 && mktemp -d "./confXXXXXX") 2>/dev/null` && test -d "$tmp" } || { tmp=./conf$$-$RANDOM (umask 077 && mkdir "$tmp") } || as_fn_error $? "cannot create a temporary directory in ." "$LINENO" 5 ac_tmp=$tmp # Set up the scripts for CONFIG_FILES section. # No need to generate them if there are no CONFIG_FILES. # This happens for instance with `./config.status config.h'. if test -n "$CONFIG_FILES"; then ac_cr=`echo X | tr X '\015'` # On cygwin, bash can eat \r inside `` if the user requested igncr. # But we know of no other shell where ac_cr would be empty at this # point, so we can use a bashism as a fallback. if test "x$ac_cr" = x; then eval ac_cr=\$\'\\r\' fi ac_cs_awk_cr=`$AWK 'BEGIN { print "a\rb" }' /dev/null` if test "$ac_cs_awk_cr" = "a${ac_cr}b"; then ac_cs_awk_cr='\\r' else ac_cs_awk_cr=$ac_cr fi echo 'BEGIN {' >"$ac_tmp/subs1.awk" && _ACEOF { echo "cat >conf$$subs.awk <<_ACEOF" && echo "$ac_subst_vars" | sed 's/.*/&!$&$ac_delim/' && echo "_ACEOF" } >conf$$subs.sh || as_fn_error $? "could not make $CONFIG_STATUS" "$LINENO" 5 ac_delim_num=`echo "$ac_subst_vars" | grep -c '^'` ac_delim='%!_!# ' for ac_last_try in false false false false false :; do . ./conf$$subs.sh || as_fn_error $? "could not make $CONFIG_STATUS" "$LINENO" 5 ac_delim_n=`sed -n "s/.*$ac_delim\$/X/p" conf$$subs.awk | grep -c X` if test $ac_delim_n = $ac_delim_num; then break elif $ac_last_try; then as_fn_error $? "could not make $CONFIG_STATUS" "$LINENO" 5 else ac_delim="$ac_delim!$ac_delim _$ac_delim!! " fi done rm -f conf$$subs.sh cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 cat >>"\$ac_tmp/subs1.awk" <<\\_ACAWK && _ACEOF sed -n ' h s/^/S["/; s/!.*/"]=/ p g s/^[^!]*!// :repl t repl s/'"$ac_delim"'$// t delim :nl h s/\(.\{148\}\)..*/\1/ t more1 s/["\\]/\\&/g; s/^/"/; s/$/\\n"\\/ p n b repl :more1 s/["\\]/\\&/g; s/^/"/; s/$/"\\/ p g s/.\{148\}// t nl :delim h s/\(.\{148\}\)..*/\1/ t more2 s/["\\]/\\&/g; s/^/"/; s/$/"/ p b :more2 s/["\\]/\\&/g; s/^/"/; s/$/"\\/ p g s/.\{148\}// t delim ' >$CONFIG_STATUS || ac_write_fail=1 rm -f conf$$subs.awk cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 _ACAWK cat >>"\$ac_tmp/subs1.awk" <<_ACAWK && for (key in S) S_is_set[key] = 1 FS = "" } { line = $ 0 nfields = split(line, field, "@") substed = 0 len = length(field[1]) for (i = 2; i < nfields; i++) { key = field[i] keylen = length(key) if (S_is_set[key]) { value = S[key] line = substr(line, 1, len) "" value "" substr(line, len + keylen + 3) len += length(value) + length(field[++i]) substed = 1 } else len += 1 + keylen } print line } _ACAWK _ACEOF cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 if sed "s/$ac_cr//" < /dev/null > /dev/null 2>&1; then sed "s/$ac_cr\$//; s/$ac_cr/$ac_cs_awk_cr/g" else cat fi < "$ac_tmp/subs1.awk" > "$ac_tmp/subs.awk" \ || as_fn_error $? "could not setup config files machinery" "$LINENO" 5 _ACEOF # VPATH may cause trouble with some makes, so we remove sole $(srcdir), # ${srcdir} and @srcdir@ entries from VPATH if srcdir is ".", strip leading and # trailing colons and then remove the whole line if VPATH becomes empty # (actually we leave an empty line to preserve line numbers). if test "x$srcdir" = x.; then ac_vpsub='/^[ ]*VPATH[ ]*=[ ]*/{ h s/// s/^/:/ s/[ ]*$/:/ s/:\$(srcdir):/:/g s/:\${srcdir}:/:/g s/:@srcdir@:/:/g s/^:*// s/:*$// x s/\(=[ ]*\).*/\1/ G s/\n// s/^[^=]*=[ ]*$// }' fi cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 fi # test -n "$CONFIG_FILES" eval set X " :F $CONFIG_FILES " shift for ac_tag do case $ac_tag in :[FHLC]) ac_mode=$ac_tag; continue;; esac case $ac_mode$ac_tag in :[FHL]*:*);; :L* | :C*:*) as_fn_error $? "invalid tag \`$ac_tag'" "$LINENO" 5;; :[FH]-) ac_tag=-:-;; :[FH]*) ac_tag=$ac_tag:$ac_tag.in;; esac ac_save_IFS=$IFS IFS=: set x $ac_tag IFS=$ac_save_IFS shift ac_file=$1 shift case $ac_mode in :L) ac_source=$1;; :[FH]) ac_file_inputs= for ac_f do case $ac_f in -) ac_f="$ac_tmp/stdin";; *) # Look for the file first in the build tree, then in the source tree # (if the path is not absolute). The absolute path cannot be DOS-style, # because $ac_f cannot contain `:'. test -f "$ac_f" || case $ac_f in [\\/$]*) false;; *) test -f "$srcdir/$ac_f" && ac_f="$srcdir/$ac_f";; esac || as_fn_error 1 "cannot find input file: \`$ac_f'" "$LINENO" 5;; esac case $ac_f in *\'*) ac_f=`$as_echo "$ac_f" | sed "s/'/'\\\\\\\\''/g"`;; esac as_fn_append ac_file_inputs " '$ac_f'" done # Let's still pretend it is `configure' which instantiates (i.e., don't # use $as_me), people would be surprised to read: # /* config.h. Generated by config.status. */ configure_input='Generated from '` $as_echo "$*" | sed 's|^[^:]*/||;s|:[^:]*/|, |g' `' by configure.' if test x"$ac_file" != x-; then configure_input="$ac_file. $configure_input" { $as_echo "$as_me:${as_lineno-$LINENO}: creating $ac_file" >&5 $as_echo "$as_me: creating $ac_file" >&6;} fi # Neutralize special characters interpreted by sed in replacement strings. case $configure_input in #( *\&* | *\|* | *\\* ) ac_sed_conf_input=`$as_echo "$configure_input" | sed 's/[\\\\&|]/\\\\&/g'`;; #( *) ac_sed_conf_input=$configure_input;; esac case $ac_tag in *:-:* | *:-) cat >"$ac_tmp/stdin" \ || as_fn_error $? "could not create $ac_file" "$LINENO" 5 ;; esac ;; esac ac_dir=`$as_dirname -- "$ac_file" || $as_expr X"$ac_file" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ X"$ac_file" : 'X\(//\)[^/]' \| \ X"$ac_file" : 'X\(//\)$' \| \ X"$ac_file" : 'X\(/\)' \| . 2>/dev/null || $as_echo X"$ac_file" | sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ s//\1/ q } /^X\(\/\/\)[^/].*/{ s//\1/ q } /^X\(\/\/\)$/{ s//\1/ q } /^X\(\/\).*/{ s//\1/ q } s/.*/./; q'` as_dir="$ac_dir"; as_fn_mkdir_p ac_builddir=. case "$ac_dir" in .) ac_dir_suffix= ac_top_builddir_sub=. ac_top_build_prefix= ;; *) ac_dir_suffix=/`$as_echo "$ac_dir" | sed 's|^\.[\\/]||'` # A ".." for each directory in $ac_dir_suffix. ac_top_builddir_sub=`$as_echo "$ac_dir_suffix" | sed 's|/[^\\/]*|/..|g;s|/||'` case $ac_top_builddir_sub in "") ac_top_builddir_sub=. ac_top_build_prefix= ;; *) ac_top_build_prefix=$ac_top_builddir_sub/ ;; esac ;; esac ac_abs_top_builddir=$ac_pwd ac_abs_builddir=$ac_pwd$ac_dir_suffix # for backward compatibility: ac_top_builddir=$ac_top_build_prefix case $srcdir in .) # We are building in place. ac_srcdir=. ac_top_srcdir=$ac_top_builddir_sub ac_abs_top_srcdir=$ac_pwd ;; [\\/]* | ?:[\\/]* ) # Absolute name. ac_srcdir=$srcdir$ac_dir_suffix; ac_top_srcdir=$srcdir ac_abs_top_srcdir=$srcdir ;; *) # Relative name. ac_srcdir=$ac_top_build_prefix$srcdir$ac_dir_suffix ac_top_srcdir=$ac_top_build_prefix$srcdir ac_abs_top_srcdir=$ac_pwd/$srcdir ;; esac ac_abs_srcdir=$ac_abs_top_srcdir$ac_dir_suffix case $ac_mode in :F) # # CONFIG_FILE # case $INSTALL in [\\/$]* | ?:[\\/]* ) ac_INSTALL=$INSTALL ;; *) ac_INSTALL=$ac_top_build_prefix$INSTALL ;; esac _ACEOF cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 # If the template does not know about datarootdir, expand it. # FIXME: This hack should be removed a few years after 2.60. ac_datarootdir_hack=; ac_datarootdir_seen= ac_sed_dataroot=' /datarootdir/ { p q } /@datadir@/p /@docdir@/p /@infodir@/p /@localedir@/p /@mandir@/p' case `eval "sed -n \"\$ac_sed_dataroot\" $ac_file_inputs"` in *datarootdir*) ac_datarootdir_seen=yes;; *@datadir@*|*@docdir@*|*@infodir@*|*@localedir@*|*@mandir@*) { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: $ac_file_inputs seems to ignore the --datarootdir setting" >&5 $as_echo "$as_me: WARNING: $ac_file_inputs seems to ignore the --datarootdir setting" >&2;} _ACEOF cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 ac_datarootdir_hack=' s&@datadir@&$datadir&g s&@docdir@&$docdir&g s&@infodir@&$infodir&g s&@localedir@&$localedir&g s&@mandir@&$mandir&g s&\\\${datarootdir}&$datarootdir&g' ;; esac _ACEOF # Neutralize VPATH when `$srcdir' = `.'. # Shell code in configure.ac might set extrasub. # FIXME: do we really want to maintain this feature? cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 ac_sed_extra="$ac_vpsub $extrasub _ACEOF cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 :t /@[a-zA-Z_][a-zA-Z_0-9]*@/!b s|@configure_input@|$ac_sed_conf_input|;t t s&@top_builddir@&$ac_top_builddir_sub&;t t s&@top_build_prefix@&$ac_top_build_prefix&;t t s&@srcdir@&$ac_srcdir&;t t s&@abs_srcdir@&$ac_abs_srcdir&;t t s&@top_srcdir@&$ac_top_srcdir&;t t s&@abs_top_srcdir@&$ac_abs_top_srcdir&;t t s&@builddir@&$ac_builddir&;t t s&@abs_builddir@&$ac_abs_builddir&;t t s&@abs_top_builddir@&$ac_abs_top_builddir&;t t s&@INSTALL@&$ac_INSTALL&;t t $ac_datarootdir_hack " eval sed \"\$ac_sed_extra\" "$ac_file_inputs" | $AWK -f "$ac_tmp/subs.awk" \ >$ac_tmp/out || as_fn_error $? "could not create $ac_file" "$LINENO" 5 test -z "$ac_datarootdir_hack$ac_datarootdir_seen" && { ac_out=`sed -n '/\${datarootdir}/p' "$ac_tmp/out"`; test -n "$ac_out"; } && { ac_out=`sed -n '/^[ ]*datarootdir[ ]*:*=/p' \ "$ac_tmp/out"`; test -z "$ac_out"; } && { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: $ac_file contains a reference to the variable \`datarootdir' which seems to be undefined. Please make sure it is defined" >&5 $as_echo "$as_me: WARNING: $ac_file contains a reference to the variable \`datarootdir' which seems to be undefined. Please make sure it is defined" >&2;} rm -f "$ac_tmp/stdin" case $ac_file in -) cat "$ac_tmp/out" && rm -f "$ac_tmp/out";; *) rm -f "$ac_file" && mv "$ac_tmp/out" "$ac_file";; esac \ || as_fn_error $? "could not create $ac_file" "$LINENO" 5 ;; esac done # for ac_tag as_fn_exit 0 _ACEOF ac_clean_files=$ac_clean_files_save test $ac_write_fail = 0 || as_fn_error $? "write failure creating $CONFIG_STATUS" "$LINENO" 5 # configure is writing to config.log, and then calls config.status. # config.status does its own redirection, appending to config.log. # Unfortunately, on DOS this fails, as config.log is still kept open # by configure, so config.status won't be able to write to it; its # output is simply discarded. So we exec the FD to /dev/null, # effectively closing config.log, so it can be properly (re)opened and # appended to by config.status. When coming back to configure, we # need to make the FD available again. if test "$no_create" != yes; then ac_cs_success=: ac_config_status_args= test "$silent" = yes && ac_config_status_args="$ac_config_status_args --quiet" exec 5>/dev/null $SHELL $CONFIG_STATUS $ac_config_status_args || ac_cs_success=false exec 5>>config.log # Use ||, not &&, to avoid exiting from the if with $? = 1, which # would make configure fail if this is the last instruction. $ac_cs_success || as_fn_exit 1 fi if test -n "$ac_unrecognized_opts" && test "$enable_option_checking" != no; then { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: unrecognized options: $ac_unrecognized_opts" >&5 $as_echo "$as_me: WARNING: unrecognized options: $ac_unrecognized_opts" >&2;} fi onedrive-2.5.5/configure.ac000066400000000000000000000247631476564400300156510ustar00rootroot00000000000000dnl configure.ac for OneDrive Linux Client dnl Copyright 2019 Norbert Preining dnl Licensed GPL v3 or later dnl How to make a release dnl - increase the version number in the AC_INIT call below dnl - run autoconf which generates configure dnl - commit the changed files (configure.ac, configure) dnl - tag the release AC_PREREQ([2.69]) AC_INIT([onedrive],[v2.5.5], [https://github.com/abraunegg/onedrive], [onedrive]) AC_CONFIG_SRCDIR([src/main.d]) AC_ARG_VAR([DC], [D compiler executable]) AC_ARG_VAR([DCFLAGS], [flags for D compiler]) dnl necessary programs: install, pkg-config AC_PROG_INSTALL PKG_PROG_PKG_CONFIG dnl Determine D compiler dnl we check for dmd, dmd2, and ldc2 in this order dnl furthermore, we set DC_TYPE to either dmd or ldc and export this into the dnl Makefile so that we can adjust command line arguments AC_CHECK_PROGS([DC], [dmd ldmd2 ldc2], NOT_FOUND) DC_TYPE= case $(basename $DC) in dmd) DC_TYPE=dmd ;; ldmd2) DC_TYPE=dmd ;; ldc2) DC_TYPE=ldc ;; NOT_FOUND) AC_MSG_ERROR(Could not find any compatible D compiler, 1) esac dnl dash/POSIX version of version comparison vercomp () { IFS=. read -r a0 a1 a2 aa <' $bb then return 1 else return 0 fi fi fi fi } DO_VERSION_CHECK=1 AC_ARG_ENABLE(version-check, AS_HELP_STRING([--disable-version-check], [Disable checks of compiler version during configure time])) AS_IF([test "x$enable_version_check" = "xno"], DO_VERSION_CHECK=0,) AS_IF([test "$DO_VERSION_CHECK" = "1"], [ dnl do the version check AC_MSG_CHECKING([version of D compiler]) # check for valid versions case $(basename $DC) in ldmd2|ldc2) # LDC - the LLVM D compiler (1.12.0): ... VERSION=`$DC --version` # remove everything up to first ( VERSION=${VERSION#* (} # remove everything after ): VERSION=${VERSION%%):*} # now version should be something like L.M.N MINVERSION=1.18.0 ;; dmd) # DMD64 D Compiler v2.085.1\n... VERSION=`$DC --version | tr '\n' ' '` VERSION=${VERSION#*Compiler v} VERSION=${VERSION%% *} # now version should be something like L.M.N MINVERSION=2.088.0 ;; esac AC_MSG_RESULT([$VERSION]) vercomp $MINVERSION $VERSION if test $? = 1 then AC_MSG_ERROR([Compiler version insufficient, current compiler version $VERSION, minimum version $MINVERSION], 1) fi #echo "MINVERSION=$MINVERSION VERSION=$VERSION" ]) AC_SUBST([DC_TYPE]) dnl In case the environment variable DCFLAGS is set, we export it to the dnl generated Makefile at configure run: AC_SUBST([DCFLAGS]) dnl The package date is only used in the man page onedrive.1.in dnl we generate onedrive.1 from it during configure run, but we want dnl to have the same date, namely the one when the configure script dnl was generated from the configure.ac (i.e., on release time). dnl this uses a call to the underlying m4 engine to call an external cmd PACKAGE_DATE="m4_esyscmd([date "+%B %Y" | tr -d '\n'])" AC_SUBST([PACKAGE_DATE]) dnl Check for required modules: curl and sqlite at the moment PKG_CHECK_MODULES([curl],[libcurl]) PKG_CHECK_MODULES([sqlite],[sqlite3]) dnl dnl systemd and unit file directories dnl This is a bit tricky, because we want to allow for dnl --with-systemdsystemunitdir=auto dnl as well as =/path/to/dir dnl The first step is that we check whether the --with options is passed to configure run dnl if yes, we don't do anything (the ,, at the end of the next line), and if not, we dnl set with_systemdsystemunitdir=auto, meaning we will try pkg-config to find the correct dnl value. AC_ARG_WITH([systemdsystemunitdir], [AS_HELP_STRING([--with-systemdsystemunitdir=DIR], [Directory for systemd system service files])],, [with_systemdsystemunitdir=auto]) dnl If no value is passed in (or auto/yes is passed in), then we try to find the correct dnl value via pkg-config and put it into $def_systemdsystemunitdir AS_IF([test "x$with_systemdsystemunitdir" = "xyes" -o "x$with_systemdsystemunitdir" = "xauto"], [ dnl true part, so try to determine with pkg-config def_systemdsystemunitdir=$($PKG_CONFIG --variable=systemdsystemunitdir systemd) dnl if we cannot find it via pkg-config, *and* the user explicitly passed it in with, dnl we warn, and in all cases we unset (set to no) the respective variable AS_IF([test "x$def_systemdsystemunitdir" = "x"], [ dnl we couldn't find the default value via pkg-config AS_IF([test "x$with_systemdsystemunitdir" = "xyes"], [AC_MSG_ERROR([systemd support requested but pkg-config unable to query systemd package])]) with_systemdsystemunitdir=no ], [ dnl pkg-config found the value, use it with_systemdsystemunitdir="$def_systemdsystemunitdir" ] ) ] ) dnl finally, if we found a value, put it into the generated Makefile AS_IF([test "x$with_systemdsystemunitdir" != "xno"], [AC_SUBST([systemdsystemunitdir], [$with_systemdsystemunitdir])]) dnl Now do the same as above for systemduserunitdir! AC_ARG_WITH([systemduserunitdir], [AS_HELP_STRING([--with-systemduserunitdir=DIR], [Directory for systemd user service files])],, [with_systemduserunitdir=auto]) AS_IF([test "x$with_systemduserunitdir" = "xyes" -o "x$with_systemduserunitdir" = "xauto"], [ def_systemduserunitdir=$($PKG_CONFIG --variable=systemduserunitdir systemd) AS_IF([test "x$def_systemduserunitdir" = "x"], [ AS_IF([test "x$with_systemduserunitdir" = "xyes"], [AC_MSG_ERROR([systemd support requested but pkg-config unable to query systemd package])]) with_systemduserunitdir=no ], [ with_systemduserunitdir="$def_systemduserunitdir" ] ) ] ) AS_IF([test "x$with_systemduserunitdir" != "xno"], [AC_SUBST([systemduserunitdir], [$with_systemduserunitdir])]) dnl We enable systemd integration only if we have found both user/system unit dirs AS_IF([test "x$with_systemduserunitdir" != "xno" -a "x$with_systemdsystemunitdir" != "xno"], [havesystemd=yes], [havesystemd=no]) AC_SUBST([HAVE_SYSTEMD], $havesystemd) dnl dnl Notification support dnl only check for libnotify if --enable-notifications is given AC_ARG_ENABLE(notifications, AS_HELP_STRING([--enable-notifications], [Enable desktop notifications via libnotify])) AS_IF([test "x$enable_notifications" = "xyes"], [enable_notifications=yes], [enable_notifications=no]) dnl if --enable-notifications was given, check for libnotify, and disable if not found dnl otherwise substitute the notifu AS_IF([test "x$enable_notifications" = "xyes"], [PKG_CHECK_MODULES(notify,libnotify,,enable_notifications=no)], [AC_SUBST([notify_LIBS],"")]) AC_SUBST([NOTIFICATIONS],$enable_notifications) dnl dnl iNotify Support # Conditionally set bsd_inotify_LIBS based on the platform case "$(uname -s)" in Linux) bsd_inotify_LIBS="" ;; FreeBSD) bsd_inotify_LIBS="-L/usr/local/lib -linotify" ;; OpenBSD) bsd_inotify_LIBS="-L/usr/local/lib/inotify -linotify" ;; *) bsd_inotify_LIBS="" ;; esac AC_SUBST([bsd_inotify_LIBS]) dnl dnl Dynamic Linker Support # Conditionally set dynamic_linker_LIBS based on the platform case "$(uname -s)" in Linux) dynamic_linker_LIBS="-ldl" ;; *) dynamic_linker_LIBS="" ;; esac AC_SUBST([dynamic_linker_LIBS]) dnl dnl Completion support dnl First determine whether completions are requested, pass that to Makefile AC_ARG_ENABLE([completions], AS_HELP_STRING([--enable-completions], [Install shell completions for bash, zsh, and fish])) AS_IF([test "x$enable_completions" = "xyes"], [enable_completions=yes], [enable_completions=no]) AC_SUBST([COMPLETIONS],$enable_completions) dnl if completions are enabled, search for the bash/zsh completion directory in the dnl similar way as we did for the systemd directories AS_IF([test "x$enable_completions" = "xyes"],[ AC_ARG_WITH([bash-completion-dir], [AS_HELP_STRING([--with-bash-completion-dir=DIR], [Directory for bash completion files])], , [with_bash_completion_dir=auto]) AS_IF([test "x$with_bash_completion_dir" = "xyes" -o "x$with_bash_completion_dir" = "xauto"], [ PKG_CHECK_VAR(bashcompdir, [bash-completion], [completionsdir], , bashcompdir="${sysconfdir}/bash_completion.d") with_bash_completion_dir=$bashcompdir ]) AC_SUBST([BASH_COMPLETION_DIR], $with_bash_completion_dir) AC_ARG_WITH([zsh-completion-dir], [AS_HELP_STRING([--with-zsh-completion-dir=DIR], [Directory for zsh completion files])],, [with_zsh_completion_dir=auto]) AS_IF([test "x$with_zsh_completion_dir" = "xyes" -o "x$with_zsh_completion_dir" = "xauto"], [ with_zsh_completion_dir="/usr/local/share/zsh/site-functions" ]) AC_SUBST([ZSH_COMPLETION_DIR], $with_zsh_completion_dir) AC_ARG_WITH([fish-completion-dir], [AS_HELP_STRING([--with-fish-completion-dir=DIR], [Directory for fish completion files])],, [with_fish_completion_dir=auto]) AS_IF([test "x$with_fish_completion_dir" = "xyes" -o "x$with_fish_completion_dir" = "xauto"], [ with_fish_completion_dir="/usr/local/share/fish/completions" ]) AC_SUBST([FISH_COMPLETION_DIR], $with_fish_completion_dir) ]) dnl dnl Debug support AC_ARG_ENABLE(debug, AS_HELP_STRING([--enable-debug], [Pass debug option to the compiler])) AS_IF([test "x$enable_debug" = "xyes"], AC_SUBST([DEBUG],yes), AC_SUBST([DEBUG],no)) dnl generate necessary files AC_CONFIG_FILES([ Makefile contrib/pacman/PKGBUILD contrib/spec/onedrive.spec onedrive.1 contrib/systemd/onedrive.service contrib/systemd/onedrive@.service ]) AC_OUTPUT onedrive-2.5.5/contrib/000077500000000000000000000000001476564400300150075ustar00rootroot00000000000000onedrive-2.5.5/contrib/completions/000077500000000000000000000000001476564400300173435ustar00rootroot00000000000000onedrive-2.5.5/contrib/completions/complete.bash000066400000000000000000000031301476564400300220070ustar00rootroot00000000000000# BASH completion code for OneDrive Linux Client # (c) 2019 Norbert Preining # License: GPLv3+ (as with the rest of the OneDrive Linux client project) _onedrive() { local cur prev COMPREPLY=() cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]} options='--check-for-nomount --check-for-nosync --debug-https --disable-notifications --display-config --display-sync-status --download-only --disable-upload-validation --dry-run --enable-logging --force-http-1.1 --force-http-2 --get-file-link --local-first --logout -m --monitor --no-remote-delete --print-token --reauth --resync --skip-dot-files --skip-symlinks --synchronize --upload-only -v --verbose --version -h --help' argopts='--create-directory --get-O365-drive-id --remove-directory --single-directory --source-directory' # Loop on the arguments to manage conflicting options for (( i=0; i < ${#COMP_WORDS[@]}-1; i++ )); do #exclude some mutually exclusive options [[ ${COMP_WORDS[i]} == '--synchronize' ]] && options=${options/--monitor} [[ ${COMP_WORDS[i]} == '--monitor' ]] && options=${options/--synchronize} done case "$prev" in --confdir|--syncdir) _filedir return 0 ;; --get-file-link) if command -v sed &> /dev/null; then pushd "$(onedrive --display-config | sed -n "/sync_dir/s/.*= //p")" &> /dev/null _filedir popd &> /dev/null fi return 0 ;; --create-directory|--get-O365-drive-id|--remove-directory|--single-directory|--source-directory) return 0 ;; *) COMPREPLY=( $( compgen -W "$options $argopts" -- "$cur")) return 0 ;; esac # notreached return 0 } complete -F _onedrive onedrive onedrive-2.5.5/contrib/completions/complete.fish000066400000000000000000000065151476564400300220350ustar00rootroot00000000000000# FISH completions for OneDrive Linux Client # License: GPLv3+ (as with the rest of the OneDrive Linux client project) complete -c onedrive -f complete -c onedrive -l check-for-nomount -d 'Check for the presence of .nosync in the syncdir root. If found, do not perform sync.' complete -c onedrive -l check-for-nosync -d 'Check for the presence of .nosync in each directory. If found, skip directory from sync.' complete -c onedrive -l create-directory -d 'Create a directory on OneDrive - no sync will be performed.' complete -c onedrive -l debug-https -d 'Debug OneDrive HTTPS communication.' complete -c onedrive -l disable-notifications -d 'Do not use desktop notifications in monitor mode.' complete -c onedrive -l disable-upload-validation -d 'Disable upload validation when uploading to OneDrive.' complete -c onedrive -l display-config -d 'Display what options the client will use as currently configured - no sync will be performed.' complete -c onedrive -l display-sync-status -d 'Display the sync status of the client - no sync will be performed.' complete -c onedrive -l download-only -d 'Only download remote changes.' complete -c onedrive -l dry-run -d 'Perform a trial sync with no changes made.' complete -c onedrive -l enable-logging -d 'Enable client activity to a separate log file.' complete -c onedrive -l force-http-1.1 -d 'Force the use of HTTP 1.1 for all operations.' complete -c onedrive -l force-http-2 -d 'Force the use of HTTP 2 for all operations.' complete -c onedrive -l get-file-link -d 'Display the file link of a synced file.' complete -c onedrive -l get-O365-drive-id -d 'Query and return the Office 365 Drive ID for a given Office 365 SharePoint Shared Library.' complete -c onedrive -s h -l help -d 'Print help information.' complete -c onedrive -l local-first -d 'Synchronize from the local directory source first, before downloading changes from OneDrive.' complete -c onedrive -l logout -d 'Logout the current user.' complete -c onedrive -n "not __fish_seen_subcommand_from --synchronize" -a "-m --monitor" -d 'Keep monitoring for local and remote changes.' complete -c onedrive -l no-remote-delete -d 'Do not delete local file deletes from OneDrive when using --upload-only.' complete -c onedrive -l print-token -d 'Print the access token, useful for debugging.' complete -c onedrive -l remote-directory -d 'Remove a directory on OneDrive - no sync will be performed.' complete -c onedrive -l reauth -d 'Reauthenticate the client with OneDrive.' complete -c onedrive -l resync -d 'Forget the last saved state, perform a full sync.' complete -c onedrive -l single-directory -d 'Specify a single local directory within the OneDrive root to sync.' complete -c onedrive -l skip-dot-files -d 'Skip dot files and folders from syncing.' complete -c onedrive -l skip-symlinks -d 'Skip syncing of symlinks.' complete -c onedrive -l source-directory -d 'Source directory to rename or move on OneDrive - no sync will be performed.' complete -c onedrive -n "not __fish_seen_subcommand_from --monitor; and not __fish_seen_subcommand_from -m" -l synchronize -d 'Perform a synchronization.' complete -c onedrive -l upload-only -d 'Only upload to OneDrive, do not sync changes from OneDrive locally' complete -c onedrive -s v -l verbose -d 'Print more details, useful for debugging (repeat for extra debugging).' complete -c onedrive -l version -d 'Print the version and exit.' onedrive-2.5.5/contrib/completions/complete.zsh000066400000000000000000000060431476564400300217040ustar00rootroot00000000000000#compdef onedrive # # ZSH completion code for OneDrive Linux Client # (c) 2019 Norbert Preining # License: GPLv3+ (as with the rest of the OneDrive Linux client project) local -a all_opts all_opts=( '--check-for-nomount[Check for the presence of .nosync in the syncdir root. If found, do not perform sync.]' '--check-for-nosync[Check for the presence of .nosync in each directory. If found, skip directory from sync.]' '--confdir[Set the directory used to store the configuration files]:config directory:_files -/' '--create-directory[Create a directory on OneDrive - no sync will be performed.]:directory name:' '--debug-https[Debug OneDrive HTTPS communication.]' '--destination-directory[Destination directory for renamed or move on OneDrive - no sync will be performed.]:directory name:' '--disable-notifications[Do not use desktop notifications in monitor mode.]' '--display-config[Display what options the client will use as currently configured - no sync will be performed.]' '--display-sync-status[Display the sync status of the client - no sync will be performed.]' '--download-only[Only download remote changes]' '--disable-upload-validation[Disable upload validation when uploading to OneDrive]' '--dry-run[Perform a trial sync with no changes made]' '--enable-logging[Enable client activity to a separate log file]' '--force-http-1.1[Force the use of HTTP 1.1 for all operations]' '--force-http-2[Force the use of HTTP 2 for all operations]' '--get-file-link[Display the file link of a synced file.]:file name:' '--get-O365-drive-id[Query and return the Office 365 Drive ID for a given Office 365 SharePoint Shared Library]:' '--local-first[Synchronize from the local directory source first, before downloading changes from OneDrive.]' '--logout[Logout the current user]' '(-m --monitor)'{-m,--monitor}'[Keep monitoring for local and remote changes]' '--no-remote-delete[Do not delete local file deletes from OneDrive when using --upload-only]' '--print-token[Print the access token, useful for debugging]' '--reauth[Reauthenticate the client with OneDrive]' '--resync[Forget the last saved state, perform a full sync]' '--remove-directory[Remove a directory on OneDrive - no sync will be performed.]:directory name:' '--single-directory[Specify a single local directory within the OneDrive root to sync.]:source directory:_files -/' '--skip-dot-files[Skip dot files and folders from syncing]' '--skip-symlinks[Skip syncing of symlinks]' '--source-directory[Source directory to rename or move on OneDrive - no sync will be performed.]:source directory:' '--syncdir[Specify the local directory used for synchronization to OneDrive]:sync directory:_files -/' '--synchronize[Perform a synchronization]' '--upload-only[Only upload to OneDrive, do not sync changes from OneDrive locally]' '(-v --verbose)'{-v,--verbose}'[Print more details, useful for debugging (repeat for extra debugging)]' '--version[Print the version and exit]' '(-h --help)'{-h,--help}'[Print help information]' ) _arguments -S "$all_opts[@]" && return 0 onedrive-2.5.5/contrib/docker/000077500000000000000000000000001476564400300162565ustar00rootroot00000000000000onedrive-2.5.5/contrib/docker/Dockerfile000066400000000000000000000016511476564400300202530ustar00rootroot00000000000000# -*-Dockerfile-*- ARG FEDORA_VERSION=41 ARG DEBIAN_VERSION=bullseye ARG GO_VERSION=1.22 ARG GOSU_VERSION=1.17 FROM golang:${GO_VERSION}-${DEBIAN_VERSION} AS builder-gosu ARG GOSU_VERSION RUN go install -ldflags "-s -w" github.com/tianon/gosu@${GOSU_VERSION} FROM fedora:${FEDORA_VERSION} AS builder-onedrive RUN dnf install -y ldc pkgconf libcurl-devel sqlite-devel git ENV PKG_CONFIG=/usr/bin/pkgconf COPY . /usr/src/onedrive WORKDIR /usr/src/onedrive RUN ./configure --enable-debug\ && make clean \ && make \ && make install FROM fedora:${FEDORA_VERSION} RUN dnf clean all \ && dnf -y update RUN dnf install -y libcurl sqlite ldc-libs \ && dnf clean all \ && mkdir -p /onedrive/conf /onedrive/data COPY --from=builder-gosu /go/bin/gosu /usr/local/bin/ COPY --from=builder-onedrive /usr/local/bin/onedrive /usr/local/bin/ COPY contrib/docker/entrypoint.sh / RUN chmod +x /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] onedrive-2.5.5/contrib/docker/Dockerfile-alpine000066400000000000000000000017441476564400300215240ustar00rootroot00000000000000# -*-Dockerfile-*- ARG ALPINE_VERSION=3.21 ARG GO_VERSION=1.22 ARG GOSU_VERSION=1.17 FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS builder-gosu ARG GOSU_VERSION RUN go install -ldflags "-s -w" github.com/tianon/gosu@${GOSU_VERSION} FROM alpine:${ALPINE_VERSION} AS builder-onedrive RUN apk add --update --no-cache alpine-sdk gnupg xz curl-dev sqlite-dev binutils-gold autoconf automake ldc COPY . /usr/src/onedrive WORKDIR /usr/src/onedrive RUN autoreconf -fiv \ && ./configure --enable-debug\ && make clean \ && make \ && make install FROM alpine:${ALPINE_VERSION} RUN apk add --upgrade apk-tools \ && apk upgrade --available RUN apk add --update --no-cache bash libcurl libgcc shadow sqlite-libs ldc-runtime \ && mkdir -p /onedrive/conf /onedrive/data COPY --from=builder-gosu /go/bin/gosu /usr/local/bin/ COPY --from=builder-onedrive /usr/local/bin/onedrive /usr/local/bin/ COPY contrib/docker/entrypoint.sh / RUN chmod +x /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"]onedrive-2.5.5/contrib/docker/Dockerfile-debian000066400000000000000000000032471476564400300214760ustar00rootroot00000000000000# -*-Dockerfile-*- ARG DEBIAN_VERSION=stable FROM debian:${DEBIAN_VERSION} AS builder-onedrive # Add backports repository and update before initial DEBIAN_FRONTEND installation RUN apt-get clean \ && echo "deb http://deb.debian.org/debian bookworm-backports main" > /etc/apt/sources.list.d/debian-12-backports.list \ && apt-get update \ && apt-get upgrade -y \ && apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends build-essential curl ca-certificates libcurl4-openssl-dev libsqlite3-dev libxml2-dev pkg-config git ldc \ # Install|update curl from backports && apt-get install -t bookworm-backports -y curl \ && rm -rf /var/lib/apt/lists/* COPY . /usr/src/onedrive WORKDIR /usr/src/onedrive RUN ./configure --enable-debug\ && make clean \ && make \ && make install FROM debian:${DEBIAN_VERSION}-slim # Add backports repository and update after DEBIAN_FRONTEND installation RUN apt-get clean \ && echo "deb http://deb.debian.org/debian bookworm-backports main" > /etc/apt/sources.list.d/debian-12-backports.list \ && apt-get update \ && apt-get upgrade -y \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends gosu libsqlite3-0 ca-certificates libphobos2-ldc-shared100 \ # Install|update curl and libcurl4 from backports && apt-get install -t bookworm-backports -y curl libcurl4 \ && rm -rf /var/lib/apt/lists/* \ # Fix bug with ssl on armhf: https://serverfault.com/a/1045189 && /usr/bin/c_rehash \ && mkdir -p /onedrive/conf /onedrive/data COPY --from=builder-onedrive /usr/local/bin/onedrive /usr/local/bin/ COPY contrib/docker/entrypoint.sh / RUN chmod +x /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] onedrive-2.5.5/contrib/docker/entrypoint.sh000077500000000000000000000147431476564400300210410ustar00rootroot00000000000000#!/bin/bash -eu set +H -euo pipefail : ${ONEDRIVE_UID:=$(stat /onedrive/data -c '%u')} : ${ONEDRIVE_GID:=$(stat /onedrive/data -c '%g')} # Create new group using target GID if ! odgroup="$(getent group "$ONEDRIVE_GID")"; then odgroup='onedrive' groupadd "${odgroup}" -g "$ONEDRIVE_GID" else odgroup=${odgroup%%:*} fi # Create new user using target UID if ! oduser="$(getent passwd "$ONEDRIVE_UID")"; then oduser='onedrive' useradd -m "${oduser}" -u "$ONEDRIVE_UID" -g "$ONEDRIVE_GID" else oduser="${oduser%%:*}" usermod -g "${odgroup}" "${oduser}" fi # Root privilege check # Containers should not be run as 'root', but allow via environment variable override if [ "${ONEDRIVE_RUNAS_ROOT:=0}" == "1" ]; then echo "# Running container as root due to environment variable override" oduser='root' odgroup='root' else grep -qv root <( groups "${oduser}" ) || { echo 'ROOT level privileges prohibited!'; exit 1; } echo "# Running container as user: ${oduser}" fi # Default parameters ARGS=(--confdir /onedrive/conf --syncdir /onedrive/data) echo "# Base Args: ${ARGS}" # Tell client to use Standalone Mode, based on an environment variable. Otherwise Monitor Mode is used. if [ "${ONEDRIVE_SYNC_ONCE:=0}" == "1" ]; then echo "# We run in Standalone Mode" echo "# Adding --sync" ARGS=(--sync ${ARGS[@]}) else echo "# We run in Monitor Mode" echo "# Adding --monitor" ARGS=(--monitor ${ARGS[@]}) fi # Make Verbose output optional, based on an environment variable if [ "${ONEDRIVE_VERBOSE:=0}" == "1" ]; then echo "# We are being verbose" echo "# Adding --verbose" ARGS=(--verbose ${ARGS[@]}) fi # Tell client to perform debug output, based on an environment variable if [ "${ONEDRIVE_DEBUG:=0}" == "1" ]; then echo "# We are performing debug output" echo "# Adding --verbose --verbose" ARGS=(--verbose --verbose ${ARGS[@]}) fi # Tell client to perform HTTPS debug output, based on an environment variable if [ "${ONEDRIVE_DEBUG_HTTPS:=0}" == "1" ]; then echo "# We are performing HTTPS debug output" echo "# Adding --debug-https" ARGS=(--debug-https ${ARGS[@]}) fi # Tell client to perform a resync based on environment variable if [ "${ONEDRIVE_RESYNC:=0}" == "1" ]; then echo "# We are performing a --resync" echo "# Adding --resync --resync-auth" ARGS=(--resync --resync-auth ${ARGS[@]}) fi # Tell client to sync in download-only mode based on environment variable if [ "${ONEDRIVE_DOWNLOADONLY:=0}" == "1" ]; then echo "# We are synchronizing in download-only mode" echo "# Adding --download-only" ARGS=(--download-only ${ARGS[@]}) fi # Tell client to clean up local files when in download-only mode based on environment variable if [ "${ONEDRIVE_CLEANUPLOCAL:=0}" == "1" ]; then echo "# We are cleaning up local files that are not present online" echo "# Adding --cleanup-local-files" ARGS=(--cleanup-local-files ${ARGS[@]}) fi # Tell client to sync in upload-only mode based on environment variable if [ "${ONEDRIVE_UPLOADONLY:=0}" == "1" ]; then echo "# We are synchronizing in upload-only mode" echo "# Adding --upload-only" ARGS=(--upload-only ${ARGS[@]}) fi # Tell client to sync in no-remote-delete mode based on environment variable if [ "${ONEDRIVE_NOREMOTEDELETE:=0}" == "1" ]; then echo "# We are synchronizing in no-remote-delete mode" echo "# Adding --no-remote-delete" ARGS=(--no-remote-delete ${ARGS[@]}) fi # Tell client to logout based on environment variable if [ "${ONEDRIVE_LOGOUT:=0}" == "1" ]; then echo "# We are logging out" echo "# Adding --logout" ARGS=(--logout ${ARGS[@]}) fi # Tell client to re-authenticate based on environment variable if [ "${ONEDRIVE_REAUTH:=0}" == "1" ]; then echo "# We are logging out to perform a reauthentication" echo "# Adding --reauth" ARGS=(--reauth ${ARGS[@]}) fi # Tell client to utilize auth files at the provided locations based on environment variable if [ -n "${ONEDRIVE_AUTHFILES:=""}" ]; then echo "# We are using auth files to perform authentication" echo "# Adding --auth-files ARG" ARGS=(--auth-files ${ONEDRIVE_AUTHFILES} ${ARGS[@]}) fi # Tell client to utilize provided auth response based on environment variable if [ -n "${ONEDRIVE_AUTHRESPONSE:=""}" ]; then echo "# We are providing the auth response directly to perform authentication" echo "# Adding --auth-response ARG" ARGS=(--auth-response \"${ONEDRIVE_AUTHRESPONSE}\" ${ARGS[@]}) fi # Tell client to print the running configuration at application startup if [ "${ONEDRIVE_DISPLAY_CONFIG:=0}" == "1" ]; then echo "# We are printing the application running configuration at application startup" echo "# Adding --display-running-config" ARGS=(--display-running-config ${ARGS[@]}) fi # Tell client to use sync single dir option if [ -n "${ONEDRIVE_SINGLE_DIRECTORY:=""}" ]; then echo "# We are synchronizing in single-directory mode" echo "# Adding --single-directory ARG" ARGS=(--single-directory \"${ONEDRIVE_SINGLE_DIRECTORY}\" ${ARGS[@]}) fi # Tell client run in dry-run mode if [ "${ONEDRIVE_DRYRUN:=0}" == "1" ]; then echo "# We are running in dry-run mode" echo "# Adding --dry-run" ARGS=(--dry-run ${ARGS[@]}) fi # Tell client to disable download validation if [ "${ONEDRIVE_DISABLE_DOWNLOAD_VALIDATION:=0}" == "1" ]; then echo "# We are disabling the download integrity checks performed by this client" echo "# Adding --disable-download-validation" ARGS=(--disable-download-validation ${ARGS[@]}) fi # Tell client to disable upload validation if [ "${ONEDRIVE_DISABLE_UPLOAD_VALIDATION:=0}" == "1" ]; then echo "# We are disabling the upload integrity checks performed by this client" echo "# Adding --disable-upload-validation" ARGS=(--disable-upload-validation ${ARGS[@]}) fi # Tell client to download OneDrive Business Shared Files if 'sync_business_shared_items' option has been enabled in the configuration files if [ "${ONEDRIVE_SYNC_SHARED_FILES:=0}" == "1" ]; then echo "# We are attempting to sync OneDrive Business Shared Files if 'sync_business_shared_items' has been enabled in the config file" echo "# Adding --sync-shared-files" ARGS=(--sync-shared-files ${ARGS[@]}) fi if [ ${#} -gt 0 ]; then ARGS=("${@}") fi # Only switch user if not running as target uid (ie. Docker) if [ "$ONEDRIVE_UID" = "$(id -u)" ]; then echo "# Launching 'onedrive' as ${oduser}" /usr/local/bin/onedrive "${ARGS[@]}" else echo "# Changing ownership permissions on /onedrive/data and /onedrive/conf to ${oduser}:${odgroup}" chown "${oduser}:${odgroup}" /onedrive/data /onedrive/conf echo "# Launching 'onedrive' as ${oduser} via gosu" exec gosu "${oduser}" /usr/local/bin/onedrive "${ARGS[@]}" fionedrive-2.5.5/contrib/docker/hooks/000077500000000000000000000000001476564400300174015ustar00rootroot00000000000000onedrive-2.5.5/contrib/docker/hooks/post_push000077500000000000000000000002171476564400300213530ustar00rootroot00000000000000#!/bin/bash BUILD_DATE=`date "+%Y%m%d%H%M"` docker tag ${IMAGE_NAME} "${IMAGE_NAME}-${BUILD_DATE}" docker push "${IMAGE_NAME}-${BUILD_DATE}" onedrive-2.5.5/contrib/gentoo/000077500000000000000000000000001476564400300163025ustar00rootroot00000000000000onedrive-2.5.5/contrib/gentoo/onedrive-2.5.5.ebuild000066400000000000000000000013551476564400300217540ustar00rootroot00000000000000# Copyright 1999-2018 Gentoo Foundation # Distributed under the terms of the GNU General Public License v2 EAPI=6 DESCRIPTION="Onedrive sync client for Linux" HOMEPAGE="https://github.com/abraunegg/onedrive" SRC_URI="https://github.com/abraunegg/onedrive/archive/v${PV}.tar.gz -> ${P}.tar.gz" LICENSE="GPL-3" SLOT="0" KEYWORDS="~amd64 ~x86" IUSE="" DEPEND=" >=dev-lang/dmd-2.081.1 dev-db/sqlite " RDEPEND="${DEPEND} net-misc/curl " src_prepare() { default # Copy line 38 to 44 as systemd path needs to be created in portage sandbox # Update the makefile so that it doesnt use git commands to get the version during build. sed -i -e "38h; 44p; 44x" \ -e "s/version:.*/version:/" \ -e "\$s/.*/\techo v${PV} > version/" \ Makefile } onedrive-2.5.5/contrib/init.d/000077500000000000000000000000001476564400300161745ustar00rootroot00000000000000onedrive-2.5.5/contrib/init.d/onedrive.init000066400000000000000000000026041476564400300206760ustar00rootroot00000000000000#!/bin/sh # # chkconfig: 2345 20 80 # description: Starts and stops OneDrive Free Client # # Source function library. if [ -f /etc/init.d/functions ] ; then . /etc/init.d/functions elif [ -f /etc/rc.d/init.d/functions ] ; then . /etc/rc.d/init.d/functions else exit 1 fi # Source networking configuration. . /etc/sysconfig/network # Check that networking is up. [ ${NETWORKING} = "no" ] && exit 1 APP_NAME="OneDrive Free Client" STOP_TIMEOUT=${STOP_TIMEOUT-5} RETVAL=0 start() { export PATH=/usr/local/bin/:$PATH echo -n "Starting $APP_NAME: " daemon --user root onedrive_service.sh RETVAL=$? echo [ $RETVAL -eq 0 ] && touch /var/lock/subsys/onedrive || \ RETVAL=1 return $RETVAL } stop() { echo -n "Shutting down $APP_NAME: " killproc onedrive RETVAL=$? echo [ $RETVAL = 0 ] && rm -f /var/lock/subsys/onedrive ${pidfile} } restart() { stop start } rhstatus() { status onedrive return $? } # Allow status as non-root. if [ "$1" = status ]; then rhstatus exit $? fi case "$1" in start) start ;; stop) stop ;; restart) restart ;; reload) reload ;; status) rhstatus ;; *) echo "Usage: $0 {start|stop|restart|reload|status}" exit 2 esac exit $? onedrive-2.5.5/contrib/init.d/onedrive_service.sh000066400000000000000000000002761476564400300220700ustar00rootroot00000000000000#!/bin/bash # This script is to assist in starting the onedrive client when using init.d APP_OPTIONS="--monitor --verbose --enable-logging" onedrive "$APP_OPTIONS" > /dev/null 2>&1 & exit 0 onedrive-2.5.5/contrib/logrotate/000077500000000000000000000000001476564400300170075ustar00rootroot00000000000000onedrive-2.5.5/contrib/logrotate/onedrive.logrotate000066400000000000000000000010721476564400300225440ustar00rootroot00000000000000# Any OneDrive Client logs configured for here /var/log/onedrive/*log { # What user / group should logrotate use? # Logrotate 3.8.9 or greater required otherwise: # "unknown option 'su' -- ignoring line" is generated su root users # rotate log files weekly weekly # keep 4 weeks worth of backlogs rotate 4 # create new (empty) log files after rotating old ones create # use date as a suffix of the rotated file dateext # compress the log files compress # missing files OK missingok } onedrive-2.5.5/contrib/pacman/000077500000000000000000000000001476564400300162465ustar00rootroot00000000000000onedrive-2.5.5/contrib/pacman/PKGBUILD.in000066400000000000000000000014751476564400300200060ustar00rootroot00000000000000pkgname=onedrive pkgver=@PACKAGE_VERSION@ pkgrel=1 # Patch-level (increment this when a patch is applied) pkgdesc="OneDrive Client for Linux" license=("GPL3") url="https://github.com/abraunegg/onedrive/" arch=("i686" "x86_64") depends=("curl" "gcc-libs" "glibc" "sqlite") makedepends=("dmd" "git" "tar" "make") source=("https://github.com/abraunegg/onedrive/archive/v$pkgver.tar.gz") sha256sums=('SKIP') # Use SKIP or actual checksum prepare() { cd "$srcdir" tar -xzf "$pkgname-$pkgver.tar.gz" --one-top-level="$pkgname-$pkgver" --strip-components 1 } build() { cd "$srcdir/$pkgname-$pkgver" git init git add . git commit --allow-empty-message -m "" git tag "v$pkgver" make PREFIX=/usr onedrive } package() { cd "$srcdir/$pkgname-$pkgver" make PREFIX=/usr DESTDIR="$pkgdir" install } onedrive-2.5.5/contrib/spec/000077500000000000000000000000001476564400300157415ustar00rootroot00000000000000onedrive-2.5.5/contrib/spec/onedrive.spec.in000066400000000000000000000032321476564400300210350ustar00rootroot00000000000000# Determine based on distribution & version what options & packages to include %if 0%{?fedora} || 0%{?rhel} >= 7 %global with_systemd 1 %else %global with_systemd 0 %endif %if 0%{?rhel} >= 7 %global rhel_unitdir 1 %else %global rhel_unitdir 0 %endif Name: onedrive Version: 2.5.5 Release: 1%{?dist} Summary: OneDrive Client for Linux Group: System Environment/Network License: GPLv3 URL: https://github.com/abraunegg/onedrive Source0: v%{version}.tar.gz BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) BuildRequires: dmd >= 2.088.0 BuildRequires: sqlite-devel >= 3.7.15 BuildRequires: libcurl-devel Requires: sqlite >= 3.7.15 Requires: libcurl %if 0%{?with_systemd} Requires(post): systemd Requires(preun): systemd Requires(postun): systemd %else Requires(post): chkconfig Requires(preun): chkconfig Requires(preun): initscripts Requires(postun): initscripts %endif %define debug_package %{nil} %description OneDrive Client for Linux %prep %setup -q # This creates the directory %{_builddir}/%{name}-%{version}/ %build # cd %{_builddir}/%{name}-%{version} %configure --enable-debug --enable-notifications make %install %make_install PREFIX="%{buildroot}" %clean %files %doc readme.md LICENSE changelog.md %config %{_sysconfdir}/logrotate.d/onedrive %{_mandir}/man1/%{name}.1.gz %{_docdir}/%{name} %{_bindir}/%{name} %if 0%{?with_systemd} %if 0%{?rhel_unitdir} %{_unitdir}/%{name}.service %{_unitdir}/%{name}@.service %else %{_userunitdir}/%{name}.service %{_unitdir}/%{name}@.service %endif %else %{_bindir}/onedrive_service.sh /etc/init.d/onedrive %endif %changelog onedrive-2.5.5/contrib/systemd/000077500000000000000000000000001476564400300164775ustar00rootroot00000000000000onedrive-2.5.5/contrib/systemd/onedrive.service.in000066400000000000000000000016161476564400300223050ustar00rootroot00000000000000[Unit] Description=OneDrive Client for Linux Documentation=https://github.com/abraunegg/onedrive After=network-online.target Wants=network-online.target [Service] # Commented out hardenings are disabled because they may not work out of the box on your distribution # If you know what you are doing please try to enable them. ProtectSystem=full #PrivateUsers=true #PrivateDevices=true ProtectHostname=true #ProtectClock=true ProtectKernelTunables=true #ProtectKernelModules=true #ProtectKernelLogs=true ProtectControlGroups=true RestrictRealtime=true ExecStartPre=/usr/bin/sleep 15 ExecStart=@prefix@/bin/onedrive --monitor Restart=on-failure RestartSec=3 # Do not restart the service if a --resync is required which is done via a 126 exit code RestartPreventExitStatus=126 # Time to wait for the service to stop gracefully before forcefully terminating it TimeoutStopSec=90 [Install] WantedBy=default.targetonedrive-2.5.5/contrib/systemd/onedrive@.service.in000066400000000000000000000017071476564400300224060ustar00rootroot00000000000000[Unit] Description=OneDrive Client for Linux running for %i Documentation=https://github.com/abraunegg/onedrive After=network-online.target Wants=network-online.target [Service] # Commented out hardenings are disabled because they may not work out of the box on your distribution # If you know what you are doing please try to enable them. ProtectSystem=full #PrivateDevices=true ProtectHostname=true #ProtectClock=true ProtectKernelTunables=true #ProtectKernelModules=true #ProtectKernelLogs=true ProtectControlGroups=true RestrictRealtime=true ExecStartPre=/usr/bin/sleep 15 ExecStart=@prefix@/bin/onedrive --monitor --confdir=/home/%i/.config/onedrive User=%i Group=users Restart=on-failure RestartSec=3 # Do not restart the service if a --resync is required which is done via a 126 exit code RestartPreventExitStatus=126 # Time to wait for the service to stop gracefully before forcefully terminating it TimeoutStopSec=90 [Install] WantedBy=multi-user.target onedrive-2.5.5/docs/000077500000000000000000000000001476564400300142775ustar00rootroot00000000000000onedrive-2.5.5/docs/advanced-usage.md000066400000000000000000000372221476564400300174760ustar00rootroot00000000000000# Advanced Configuration of the OneDrive Free Client This document covers the following scenarios: * [Configuring the client to use multiple OneDrive accounts / configurations](#configuring-the-client-to-use-multiple-onedrive-accounts--configurations) * [Configuring the client to use multiple OneDrive accounts / configurations using Docker](#configuring-the-client-to-use-multiple-onedrive-accounts--configurations-using-docker) * [Configuring the client for use in dual-boot (Windows / Linux) situations](#configuring-the-client-for-use-in-dual-boot-windows--linux-situations) * [Configuring the client for use when 'sync_dir' is a mounted directory](#configuring-the-client-for-use-when-sync_dir-is-a-mounted-directory) * [Upload data from the local ~/OneDrive folder to a specific location on OneDrive](#upload-data-from-the-local-onedrive-folder-to-a-specific-location-on-onedrive) ## Configuring the client to use multiple OneDrive accounts / configurations Essentially, each OneDrive account or SharePoint Shared Library which you require to be synced needs to have its own and unique configuration, local sync directory and service files. To do this, the following steps are needed: 1. Create a unique configuration folder for each onedrive client configuration that you need 2. Copy to this folder a copy of the default configuration file 3. Update the default configuration file as required, changing the required minimum config options and any additional options as needed to support your multi-account configuration 4. Authenticate the client using the new configuration directory 5. Test the configuration using '--display-config' and '--dry-run' 6. Sync the OneDrive account data as required using `--synchronize` or `--monitor` 7. Configure a unique systemd service file for this account configuration ### 1. Create a unique configuration folder for each onedrive client configuration that you need Make the configuration folder as required for this new configuration, for example: ```text mkdir ~/.config/my-new-config ``` ### 2. Copy to this folder a copy of the default configuration file Copy to this folder a copy of the default configuration file by downloading this file from GitHub and saving this file in the directory created above: ```text wget https://raw.githubusercontent.com/abraunegg/onedrive/master/config -O ~/.config/my-new-config/config ``` ### 3. Update the default configuration file The following config options *must* be updated to ensure that individual account data is not cross populated with other OneDrive accounts or other configurations: * sync_dir Other options that may require to be updated, depending on the OneDrive account that is being configured: * drive_id * application_id * sync_business_shared_folders * skip_dir * skip_file * Creation of a 'sync_list' file if required * Creation of a 'business_shared_folders' file if required ### 4. Authenticate the client Authenticate the client using the specific configuration file: ```text onedrive --confdir="~/.config/my-new-config" ``` You will be asked to open a specific URL by using your web browser where you will have to login into your Microsoft Account and give the application the permission to access your files. After giving permission to the application, you will be redirected to a blank page. Copy the URI of the blank page into the application. ```text [user@hostname ~]$ onedrive --confdir="~/.config/my-new-config" Configuration file successfully loaded Configuring Global Azure AD Endpoints Authorize this app visiting: https://..... Enter the response uri: ``` ### 5. Display and Test the configuration Test the configuration using '--display-config' and '--dry-run'. By doing so, this allows you to test any configuration that you have currently made, enabling you to fix this configuration before using the configuration. #### Display the configuration ```text onedrive --confdir="~/.config/my-new-config" --display-config ``` #### Test the configuration by performing a dry-run ```text onedrive --confdir="~/.config/my-new-config" --synchronize --verbose --dry-run ``` If both of these operate as per your expectation, the configuration of this client setup is complete and validated. If not, amend your configuration as required. ### 6. Sync the OneDrive account data as required Sync the data for the new account configuration as required: ```text onedrive --confdir="~/.config/my-new-config" --synchronize --verbose ``` or ```text onedrive --confdir="~/.config/my-new-config" --monitor --verbose ``` * `--synchronize` does a one-time sync * `--monitor` keeps the application running and monitoring for changes both local and remote ### 7. Automatic syncing of new OneDrive configuration In order to automatically start syncing your OneDrive accounts, you will need to create a service file for each account. From the applicable 'systemd folder' where the applicable systemd service file exists: * RHEL / CentOS: `/usr/lib/systemd/system` * Others: `/usr/lib/systemd/user` and `/lib/systemd/system` ### Step1: Create a new systemd service file #### Red Hat Enterprise Linux, CentOS Linux Copy the required service file to a new name: ```text sudo cp /usr/lib/systemd/system/onedrive.service /usr/lib/systemd/system/onedrive-my-new-config ``` or ```text sudo cp /usr/lib/systemd/system/onedrive@.service /usr/lib/systemd/system/onedrive-my-new-config@.service ``` #### Others such as Arch, Ubuntu, Debian, OpenSuSE, Fedora Copy the required service file to a new name: ```text sudo cp /usr/lib/systemd/user/onedrive.service /usr/lib/systemd/user/onedrive-my-new-config.service ``` or ```text sudo cp /lib/systemd/system/onedrive@.service /lib/systemd/system/onedrive-my-new-config@.service ``` ### Step 2: Edit new systemd service file Edit the new systemd file, updating the line beginning with `ExecStart` so that the confdir mirrors the one you used above: ```text ExecStart=/usr/local/bin/onedrive --monitor --confdir="/full/path/to/config/dir" ``` Example: ```text ExecStart=/usr/local/bin/onedrive --monitor --confdir="/home/myusername/.config/my-new-config" ``` > [!IMPORTANT] > When running the client manually, `--confdir="~/.config/......` is acceptable. In a systemd configuration file, the full path must be used. The `~` must be manually expanded when editing your systemd file. ### Step 3: Enable the new systemd service Once the file is correctly edited, you can enable the new systemd service using the following commands. #### Red Hat Enterprise Linux, CentOS Linux ```text systemctl enable onedrive-my-new-config systemctl start onedrive-my-new-config ``` #### Others such as Arch, Ubuntu, Debian, OpenSuSE, Fedora ```text systemctl --user enable onedrive-my-new-config systemctl --user start onedrive-my-new-config ``` or ```text systemctl --user enable onedrive-my-new-config@myusername.service systemctl --user start onedrive-my-new-config@myusername.service ``` ### Step 4: Viewing systemd status and logs for the custom service #### Viewing systemd service status - Red Hat Enterprise Linux, CentOS Linux ```text systemctl status onedrive-my-new-config ``` #### Viewing systemd service status - Others such as Arch, Ubuntu, Debian, OpenSuSE, Fedora ```text systemctl --user status onedrive-my-new-config ``` #### Viewing journalctl systemd logs - Red Hat Enterprise Linux, CentOS Linux ```text journalctl --unit=onedrive-my-new-config -f ``` #### Viewing journalctl systemd logs - Others such as Arch, Ubuntu, Debian, OpenSuSE, Fedora ```text journalctl --user --unit=onedrive-my-new-config -f ``` ### Step 5: (Optional) Run custom systemd service at boot without user login In some cases it may be desirable for the systemd service to start without having to login as your 'user' All the systemd steps above that utilise the `--user` option, will run the systemd service as your particular user. As such, the systemd service will not start unless you actually login to your system. To avoid this issue, you need to reconfigure your 'user' account so that the systemd services you have created will startup without you having to login to your system: ```text loginctl enable-linger ``` Example: ```text alex@ubuntu-headless:~$ loginctl enable-linger alex ``` Repeat these steps for each OneDrive new account that you wish to use. ## Configuring the client to use multiple OneDrive accounts / configurations using Docker In some situations it may be desirable to run multiple Docker containers at the same time, each with their own configuration. To run the Docker container successfully, it needs two unique Docker volumes to operate: * Your configuration Docker volumes * Your data Docker volume When running multiple Docker containers, this is no different - each Docker container must have it's own configuration and data volume. ### High level steps: 1. Create the required unique Docker volumes for the configuration volume 2. Create the required unique local path used for the Docker data volume 3. Start the multiple Docker containers with the required configuration for each container #### Create the required unique Docker volumes for the configuration volume Create the required unique Docker volumes for the configuration volume(s): ```text docker volume create onedrive_conf_sharepoint_site1 docker volume create onedrive_conf_sharepoint_site2 docker volume create onedrive_conf_sharepoint_site3 ... docker volume create onedrive_conf_sharepoint_site50 ``` #### Create the required unique local path used for the Docker data volume Create the required unique local path used for the Docker data volume ```text mkdir -p /use/full/local/path/no/tilde/SharePointSite1 mkdir -p /use/full/local/path/no/tilde/SharePointSite2 mkdir -p /use/full/local/path/no/tilde/SharePointSite3 ... mkdir -p /use/full/local/path/no/tilde/SharePointSite50 ``` #### Start the Docker container with the required configuration (example) ```text docker run -it --name onedrive -v onedrive_conf_sharepoint_site1:/onedrive/conf -v "/use/full/local/path/no/tilde/SharePointSite1:/onedrive/data" driveone/onedrive:latest docker run -it --name onedrive -v onedrive_conf_sharepoint_site2:/onedrive/conf -v "/use/full/local/path/no/tilde/SharePointSite2:/onedrive/data" driveone/onedrive:latest docker run -it --name onedrive -v onedrive_conf_sharepoint_site3:/onedrive/conf -v "/use/full/local/path/no/tilde/SharePointSite3:/onedrive/data" driveone/onedrive:latest ... docker run -it --name onedrive -v onedrive_conf_sharepoint_site50:/onedrive/conf -v "/use/full/local/path/no/tilde/SharePointSite50:/onedrive/data" driveone/onedrive:latest ``` > [!TIP] > To avoid 're-authenticating' and 'authorising' each individual Docker container, if all the Docker containers are using the 'same' OneDrive credentials, you can reuse the 'refresh_token' from one Docker container to another by copying this file to the configuration Docker volume of each Docker container. > > If the account credentials are different .. you will need to re-authenticate each Docker container individually. ## Configuring the client for use in dual-boot (Windows / Linux) situations When dual booting Windows and Linux, depending on the Windows OneDrive account configuration, the 'Files On-Demand' option may be enabled when running OneDrive within your Windows environment. When this option is enabled in Windows, if you are sharing this location between your Windows and Linux systems, all files will be a 0 byte link, and cannot be used under Linux. To fix the problem of windows turning all files (that should be kept offline) into links, you have to uncheck a specific option in the onedrive settings window. The option in question is `Save space and download files as you use them`. To find this setting, open the onedrive pop-up window from the taskbar, click "Help & Settings" > "Settings". This opens a new window. Go to the tab "Settings" and look for the section "Files On-Demand". After unchecking the option and clicking "OK", the Windows OneDrive client should restart itself and start actually downloading your files so they will truly be available on your disk when offline. These files will then be fully accessible under Linux and the Linux OneDrive client. | OneDrive Personal | Onedrive Business
SharePoint | |---|---| | ![Uncheck-Personal](./images/personal-files-on-demand.png) | ![Uncheck-Business](./images/business-files-on-demand.png) | ## Configuring the client for use when 'sync_dir' is a mounted directory In some environments, your setup might be that your configured 'sync_dir' is pointing to another mounted file system - a NFS|CIFS location, an external drive (USB stick, eSATA etc). As such, you configure your 'sync_dir' as follows: ```text sync_dir = "/path/to/mountpoint/OneDrive" ``` The issue here is - how does the client react if the mount point gets removed - network loss, device removal? The client has zero knowledge of any event that causes a mountpoint to become unavailable, thus, the client (if you are running as a service) will assume that you deleted the files, thus, will go ahead and delete all your files on OneDrive. This is most certainly an undesirable action. There are a few options here which you can configure in your 'config' file to assist you to prevent this sort of item from occurring: 1. classify_as_big_delete 2. check_nomount 3. check_nosync > [!NOTE] > Before making any change to your configuration, stop any sync process & stop any onedrive systemd service from running. ### classify_as_big_delete By default, this uses a value of 1000 files|folders. An undesirable unmount if you have more than 1000 files, this default level will prevent the client from executing the online delete. Modify this value up or down as desired ### check_nomount & check_nosync These two options are really the right safe guards to use. In your 'mount point', *before* you mount your external folder|device, create empty `.nosync` file, so that this is the *only* file present in the mount location before you mount your data to your mount point. When you mount your data, this '.nosync' file will not be visible, but, if the device you are mounting goes away - this '.nosync' file is the only file visible. Next, in your 'config' file, configure the following options: `check_nomount = "true"` and `check_nosync = "true"` What this will do is tell the client, if at *any* point you see this file - stop syncing - thus, protecting your online data from being deleted by the mounted device being suddenly unavailable. After making this sort of change - test with `--dry-run` so you can see the impacts of your mount point being unavailable, and how the client is now reacting. Once you are happy with how the system will react, restart your sync processes. ## Upload data from the local ~/OneDrive folder to a specific location on OneDrive In some environments, you may not want your local ~/OneDrive folder to be uploaded directly to the root of your OneDrive account online. Unfortunately, the OneDrive API lacks any facility to perform a re-direction of data during upload. The workaround for this is to structure your local filesystem and reconfigure your client to achieve the desired goal. ### High level steps: 1. Create a new folder, for example `/opt/OneDrive` 2. Configure your application config 'sync_dir' to look at this folder 3. Inside `/opt/OneDrive` create the folder you wish to sync the data online to, for example: `/opt/OneDrive/RemoteOnlineDestination` 4. Configure the application to only sync `/opt/OneDrive/RemoteDestination` via 'sync_list' 5. Symbolically link `~/OneDrive` -> `/opt/OneDrive/RemoteOnlineDestination` ### Outcome: * Your `~/OneDrive` will look / feel as per normal * The data will be stored online under `/RemoteOnlineDestination` ### Testing: * Validate your configuration with `onedrive --display-config` * Test your configuration with `onedrive --dry-run` onedrive-2.5.5/docs/application-config-options.md000066400000000000000000001771671476564400300221030ustar00rootroot00000000000000# Application Configuration Options for the OneDrive Client for Linux ## Application Version Before reading this document, please ensure you are running application version [![Version](https://img.shields.io/github/v/release/abraunegg/onedrive)](https://github.com/abraunegg/onedrive/releases) or greater. Use `onedrive --version` to determine what application version you are using and upgrade your client if required. ## Table of Contents - [Configuration File Options](#configuration-file-options) - [application_id](#application_id) - [azure_ad_endpoint](#azure_ad_endpoint) - [azure_tenant_id](#azure_tenant_id) - [bypass_data_preservation](#bypass_data_preservation) - [check_nomount](#check_nomount) - [check_nosync](#check_nosync) - [classify_as_big_delete](#classify_as_big_delete) - [cleanup_local_files](#cleanup_local_files) - [connect_timeout](#connect_timeout) - [create_new_file_version](#create_new_file_version) - [data_timeout](#data_timeout) - [debug_https](#debug_https) - [disable_download_validation](#disable_download_validation) - [disable_notifications](#disable_notifications) - [disable_permission_set](#disable_permission_set) - [disable_upload_validation](#disable_upload_validation) - [display_running_config](#display_running_config) - [display_transfer_metrics](#display_transfer_metrics) - [dns_timeout](#dns_timeout) - [download_only](#download_only) - [drive_id](#drive_id) - [dry_run](#dry_run) - [enable_logging](#enable_logging) - [force_http_11](#force_http_11) - [ip_protocol_version](#ip_protocol_version) - [local_first](#local_first) - [log_dir](#log_dir) - [max_curl_idle](#max_curl_idle) - [monitor_fullscan_frequency](#monitor_fullscan_frequency) - [monitor_interval](#monitor_interval) - [monitor_log_frequency](#monitor_log_frequency) - [no_remote_delete](#no_remote_delete) - [notify_file_actions](#notify_file_actions) - [operation_timeout](#operation_timeout) - [permanent_delete](#permanent_delete) - [rate_limit](#rate_limit) - [read_only_auth_scope](#read_only_auth_scope) - [remove_source_files](#remove_source_files) - [resync](#resync) - [resync_auth](#resync_auth) - [skip_dir](#skip_dir) - [skip_dir_strict_match](#skip_dir_strict_match) - [skip_dotfiles](#skip_dotfiles) - [skip_file](#skip_file) - [skip_size](#skip_size) - [skip_symlinks](#skip_symlinks) - [space_reservation](#space_reservation) - [sync_business_shared_items](#sync_business_shared_items) - [sync_dir](#sync_dir) - [sync_dir_permissions](#sync_dir_permissions) - [sync_file_permissions](#sync_file_permissions) - [sync_root_files](#sync_root_files) - [threads](#threads) - [transfer_order](#transfer_order) - [upload_only](#upload_only) - [user_agent](#user_agent) - [webhook_enabled](#webhook_enabled) - [webhook_expiration_interval](#webhook_expiration_interval) - [webhook_listening_host](#webhook_listening_host) - [webhook_listening_port](#webhook_listening_port) - [webhook_public_url](#webhook_public_url) - [webhook_renewal_interval](#webhook_renewal_interval) - [write_xattr_data](#write_xattr_data) - [Command Line Interface (CLI) Only Options](#command-line-interface-cli-only-options) - [CLI Option: --auth-files](#cli-option---auth-files) - [CLI Option: --auth-response](#cli-option---auth-response) - [CLI Option: --confdir](#cli-option---confdir) - [CLI Option: --create-directory](#cli-option---create-directory) - [CLI Option: --create-share-link](#cli-option---create-share-link) - [CLI Option: --destination-directory](#cli-option---destination-directory) - [CLI Option: --display-config](#cli-option---display-config) - [CLI Option: --display-sync-status](#cli-option---display-sync-status) - [CLI Option: --display-quota](#cli-option---display-quota) - [CLI Option: --force](#cli-option---force) - [CLI Option: --force-sync](#cli-option---force-sync) - [CLI Option: --get-file-link](#cli-option---get-file-link) - [CLI Option: --get-sharepoint-drive-id](#cli-option---get-sharepoint-drive-id) - [CLI Option: --list-shared-items](#cli-option---list-shared-items) - [CLI Option: --logout](#cli-option---logout) - [CLI Option: --modified-by](#cli-option---modified-by) - [CLI Option: --monitor | -m](#cli-option---monitor--m) - [CLI Option: --print-access-token](#cli-option---print-access-token) - [CLI Option: --reauth](#cli-option---reauth) - [CLI Option: --remove-directory](#cli-option---remove-directory) - [CLI Option: --share-password](#cli-option---share-password) - [CLI Option: --single-directory](#cli-option---single-directory) - [CLI Option: --source-directory](#cli-option---source-directory) - [CLI Option: --sync | -s](#cli-option---sync--s) - [CLI Option: --sync-shared-files](#cli-option---sync-shared-files) - [CLI Option: --verbose | -v+](#cli-option---verbose--v) - [CLI Option: --with-editing-perms](#cli-option---with-editing-perms) - [Deprecated Configuration File and CLI Options](#deprecated-configuration-file-and-cli-options) - [force_http_2](#force_http_2) - [min_notify_changes](#min_notify_changes) - [CLI Option: --synchronize](#cli-option---synchronize) ## Configuration File Options ### application_id _**Description:**_ This is the config option for application id that used to identify itself to Microsoft OneDrive. In some circumstances, it may be desirable to use your own application id. To do this, you must register a new application with Microsoft Azure via https://portal.azure.com/, then use your new application id with this config option. _**Value Type:**_ String _**Default Value:**_ d50ca740-c83f-4d1b-b616-12c519384f0c _**Config Example:**_ `application_id = "d50ca740-c83f-4d1b-b616-12c519384f0c"` ### azure_ad_endpoint _**Description:**_ This is the config option to change the Microsoft Azure Authentication Endpoint that the client uses to conform with data and security requirements that requires data to reside within the geographic borders of that country. _**Value Type:**_ String _**Default Value:**_ *Empty* - not required for normal operation _**Valid Values:**_ USL4, USL5, DE, CN _**Config Example:**_ `azure_ad_endpoint = "DE"` ### azure_tenant_id _**Description:**_ This config option allows the locking of the client to a specific single tenant and will configure your client to use the specified tenant id in its Azure AD and Graph endpoint URIs, instead of "common". The tenant id may be the GUID Directory ID or the fully qualified tenant name. _**Value Type:**_ String _**Default Value:**_ *Empty* - not required for normal operation _**Config Example:**_ `azure_tenant_id = "example.onmicrosoft.us"` or `azure_tenant_id = "0c4be462-a1ab-499b-99e0-da08ce52a2cc"` > [!IMPORTANT] > Must be configured if 'azure_ad_endpoint' is configured. ### bypass_data_preservation _**Description:**_ This config option allows the disabling of preserving local data by renaming the local file in the event of data conflict. If this is enabled, you will experience data loss on your local data as the local file will be over-written with data from OneDrive online. Use with care and caution. _**Value Type:**_ Boolean _**Default Value:**_ False _**Config Example:**_ `bypass_data_preservation = "false"` or `bypass_data_preservation = "true"` ### check_nomount _**Description:**_ This config option is useful to prevent application startup & ongoing use in 'Monitor Mode' if the configured 'sync_dir' is a separate disk that is being mounted by your system. This option will check for the presence of a `.nosync` file in your mount point, and if present, abort any sync process to preserve data. _**Value Type:**_ Boolean _**Default Value:**_ False _**Config Example:**_ `check_nomount = "false"` or `check_nomount = "true"` _**CLI Option:**_ `--check-for-nomount` > [!TIP] > Create a `.nosync` file in your mount point *before* you mount your disk so that this `.nosync` file visible, in your mount point if your disk is unmounted at any point to preserve your data when you enable this option. ### check_nosync _**Description:**_ This config option is useful to prevent the sync of a *local* directory to Microsoft OneDrive. It will *not* check for this file online to prevent the download of directories to your local system. _**Value Type:**_ Boolean _**Default Value:**_ False _**Config Example:**_ `check_nosync = "false"` or `check_nosync = "true"` _**CLI Option Use:**_ `--check-for-nosync` > [!IMPORTANT] > Create a `.nosync` file in any *local* directory that you wish to not sync to Microsoft OneDrive when you enable this option. ### classify_as_big_delete _**Description:**_ This config option defines the number of children in a path that is locally removed which will be classified as a 'big data delete' to safeguard large data removals - which are typically accidental local delete events. _**Value Type:**_ Integer _**Default Value:**_ 1000 _**Config Example:**_ `classify_as_big_delete = "2000"` _**CLI Option Use:**_ `--classify-as-big-delete 2000` > [!NOTE] > If this option is triggered, you will need to add `--force` to force a sync to occur. ### cleanup_local_files _**Description:**_ This config option provides the capability to cleanup local files and folders if they are removed online. _**Value Type:**_ Boolean _**Default Value:**_ False _**Config Example:**_ `cleanup_local_files = "false"` or `cleanup_local_files = "true"` _**CLI Option Use:**_ `--cleanup-local-files` > [!IMPORTANT] > This configuration option can only be used with `--download-only`. It cannot be used with any other application option. ### connect_timeout _**Description:**_ This configuration setting manages the TCP connection timeout duration in seconds for HTTPS connections to Microsoft OneDrive when using the curl library (CURLOPT_CONNECTTIMEOUT). _**Value Type:**_ Integer _**Default Value:**_ 10 _**Config Example:**_ `connect_timeout = "15"` ### create_new_file_version _**Description:**_ This setting controls how the application handles the Microsoft SharePoint *feature* which modifies all PDF, MS Office & HTML files post upload, effectively breaking the integrity of your data online. By default, when the application determines that this *feature* has modified your file post upload, the now online modified file will be downloaded. When this option is enabled, rather than downloading the file, a new online file version is created which negates the download of the modified file. _**Value Type:**_ Boolean _**Default Value:**_ False _**Config Example:**_ `create_new_file_version = "false"` or `create_new_file_version = "true"` _**CLI Option Use:**_ *None - this is a config file option only* > [!IMPORTANT] > If you enable 'disable_upload_validation' via `disable_upload_validation = "true"` there is zero facility to determine if a file was modified post upload. As such, the application will default to the state that the upload integrity check has failed. When `create_new_file_version = "false"` your uploaded file will be downloaded *regardless* of the online modification state. > [!WARNING] > When this option is set to 'true', new file versions will be created online which will count towards your Microsoft OneDrive Quota. ### data_timeout _**Description:**_ This setting controls the timeout duration, in seconds, for when data is not received on an active connection to Microsoft OneDrive over HTTPS when using the curl library, before that connection is timeout out. _**Value Type:**_ Integer _**Default Value:**_ 60 _**Config Example:**_ `data_timeout = "300"` ### debug_https _**Description:**_ This setting controls whether the curl library is configured to output additional data to assist with diagnosing HTTPS issues and problems. _**Value Type:**_ Boolean _**Default Value:**_ False _**Config Example:**_ `debug_https = "false"` or `debug_https = "true"` _**CLI Option Use:**_ `--debug-https` > [!WARNING] > Whilst this option can be used at any time, it is advisable that you only use this option when advised as this will output your `Authorization: bearer` - which is your authentication token to Microsoft OneDrive. ### disable_download_validation _**Description:**_ This option determines whether the client will conduct integrity validation on files downloaded from Microsoft OneDrive. Sometimes, when downloading files, particularly from SharePoint, there is a discrepancy between the file size reported by the OneDrive API and the byte count received from the SharePoint HTTP Server for the same file. Enable this option to disable the integrity checks performed by this client. _**Value Type:**_ Boolean _**Default Value:**_ False _**Config Example:**_ `disable_download_validation = "false"` or `disable_download_validation = "true"` _**CLI Option Use:**_ `--disable-download-validation` > [!CAUTION] > If you're downloading data from SharePoint or OneDrive Business Shared Folders, you might find it necessary to activate this option. It's important to note that any issues encountered aren't due to a problem with this client; instead, they should be regarded as issues with the Microsoft OneDrive technology stack. Enabling this option disables all download integrity checks. > [!CAUTION] > If you are using OneDrive Business Accounts and your organisation implements Azure Information Protection, these AIP files will report as one size & hash online, but when downloaded, will report a totally different size and hash. > > By default these files will fail integrity checking and be deleted, meaning that AIP files will not reside on your platform. > > When you enable this option, the AIP files will download to your platform, however, if there are any other genuine download failures where the size and hash are different, these too will be retained locally meaning you may experience data integrity loss. Use this option with extreme caution. ### disable_notifications _**Description:**_ This setting controls whether GUI notifications are sent from the client to your display manager session. _**Value Type:**_ Boolean _**Default Value:**_ False _**Config Example:**_ `disable_notifications = "false"` or `disable_notifications = "true"` _**CLI Option Use:**_ `--disable-notifications` ### disable_permission_set _**Description:**_ This setting controls whether the application will set the permissions on files and directories using the values of 'sync_dir_permissions' and 'sync_file_permissions'. When this option is enabled, file system permission inheritance will be used to assign the permissions for your data. This option may be useful if the file system configured does not allow setting of POSIX permissions. _**Value Type:**_ Boolean _**Default Value:**_ False _**Config Example:**_ `disable_permission_set = "false"` or `disable_permission_set = "true"` _**CLI Option Use:**_ *None - this is a config file option only* ### disable_upload_validation _**Description:**_ This option determines whether the client will conduct integrity validation on files uploaded to Microsoft OneDrive. Sometimes, when uploading files, particularly to SharePoint, SharePoint will modify your file post upload by adding new data to your file which breaks the integrity checking of the upload performed by this client. Enable this option to disable the integrity checks performed by this client. _**Value Type:**_ Boolean _**Default Value:**_ False _**Config Example:**_ `disable_upload_validation = "false"` or `disable_upload_validation = "true"` _**CLI Option Use:**_ `--disable-upload-validation` > [!CAUTION] > If you're uploading data to SharePoint or OneDrive Business Shared Folders, you might find it necessary to activate this option. It's important to note that any issues encountered aren't due to a problem with this client; instead, they should be regarded as issues with the Microsoft OneDrive technology stack. Enabling this option disables all upload integrity checks. ### display_running_config _**Description:**_ This option will include the running config of the application at application startup. This may be desirable to enable when running in containerised environments so that any application logging that is occurring, will have the application configuration being consumed at startup, written out to any applicable log file. _**Value Type:**_ Boolean _**Default Value:**_ False _**Config Example:**_ `display_running_config = "false"` or `display_running_config = "true"` _**CLI Option Use:**_ `--display-running-config` ### display_transfer_metrics _**Description:**_ This option will display file transfer metrics when enabled. _**Value Type:**_ Boolean _**Default Value:**_ False _**Config Example:**_ `display_transfer_metrics = "false"` or `display_transfer_metrics = "true"` _**Output Example:**_ `Transfer Metrics - File: path/to/file.data | Size: 35768 Bytes | Duration: 2.27 Seconds | Speed: 0.02 Mbps (approx)` _**CLI Option Use:**_ *None - this is a config file option only* ### dns_timeout _**Description:**_ This setting controls the libcurl DNS cache value. By default, libcurl caches this info for 60 seconds. This libcurl DNS cache timeout is entirely speculative that a name resolves to the same address for a small amount of time into the future as libcurl does not use DNS TTL properties. We recommend users not to tamper with this option unless strictly necessary. _**Value Type:**_ Integer _**Default Value:**_ 60 _**Config Example:**_ `dns_timeout = "90"` ### download_only _**Description:**_ This setting forces the client to only download data from Microsoft OneDrive and replicate that data locally. No changes made locally will be uploaded to Microsoft OneDrive when using this option. _**Value Type:**_ Boolean _**Default Value:**_ False _**Config Example:**_ `download_only = "false"` or `download_only = "true"` _**CLI Option Use:**_ `--download-only` > [!IMPORTANT] > When using this option, the default mode of operation is to not clean up local files that have been deleted online. This ensures that the local data is an *archive* of what was stored online. To cleanup local files use `--cleanup-local-files`. ### drive_id _**Description:**_ This setting controls the specific drive identifier the client will use when syncing with Microsoft OneDrive. _**Value Type:**_ String _**Default Value:**_ *None* _**Config Example:**_ `drive_id = "b!bO8V6s9SSk9R7mWhpIjUrotN73WlW3tEv3OxP_QfIdQimEdOHR-1So6CqeG1MfDB"` > [!NOTE] > This option is typically only used when configuring the client to sync a specific SharePoint Library. If this configuration option is specified in your config file, a value must be specified otherwise the application will exit citing a fatal error has occurred. ### dry_run _**Description:**_ This setting controls the application capability to test your application configuration without actually performing any actual activity (download, upload, move, delete, folder creation). _**Value Type:**_ Boolean _**Default Value:**_ False _**Config Example:**_ `dry_run = "false"` or `dry_run = "true"` _**CLI Option Use:**_ `--dry-run` ### enable_logging _**Description:**_ This setting controls the application logging all actions to a separate file. By default, all log files will be written to `/var/log/onedrive`, however this can changed by using the 'log_dir' config option _**Value Type:**_ Boolean _**Default Value:**_ False _**Config Example:**_ `enable_logging = "false"` or `enable_logging = "true"` _**CLI Option Use:**_ `--enable-logging` > [!IMPORTANT] > Additional configuration is potentially required to configure the default log directory. Refer to the [Enabling the Client Activity Log](./usage.md#enabling-the-client-activity-log) section in usage.md for details ### force_http_11 _**Description:**_ This setting controls the application HTTP protocol version. By default, the application will use libcurl defaults for which HTTP protocol version will be used to interact with Microsoft OneDrive. Use this setting to downgrade libcurl to only use HTTP/1.1. _**Value Type:**_ Boolean _**Default Value:**_ False _**Config Example:**_ `force_http_11 = "false"` or `force_http_11 = "true"` _**CLI Option Use:**_ `--force-http-11` ### ip_protocol_version _**Description:**_ This setting controls the application IP protocol that should be used when communicating with Microsoft OneDrive. The default is to use IPv4 and IPv6 networks for communicating to Microsoft OneDrive. _**Value Type:**_ Integer _**Default Value:**_ 0 _**Valid Values:**_ 0 = IPv4 + IPv6, 1 = IPv4 Only, 2 = IPv6 Only _**Config Example:**_ `ip_protocol_version = "0"` or `ip_protocol_version = "1"` or `ip_protocol_version = "2"` > [!IMPORTANT] > In some environments where IPv4 and IPv6 are configured at the same time, this causes resolution and routing issues to Microsoft OneDrive. If this is the case, it is advisable to change 'ip_protocol_version' to match your environment. ### local_first _**Description:**_ This setting controls what the application considers the 'source of truth' for your data. By default, what is stored online will be considered as the 'source of truth' when syncing to your local machine. When using this option, your local data will be considered the 'source of truth'. _**Value Type:**_ Boolean _**Default Value:**_ False _**Config Example:**_ `local_first = "false"` or `local_first = "true"` _**CLI Option Use:**_ `--local-first` ### log_dir _**Description:**_ This setting controls the custom application log path when 'enable_logging' has been enabled. By default, all log files will be written to `/var/log/onedrive`. _**Value Type:**_ String _**Default Value:**_ *None* _**Config Example:**_ `log_dir = "~/logs/"` _**CLI Option Use:**_ `--log-dir "~/logs/"` ### max_curl_idle _**Description:**_ This configuration option controls the number of seconds that elapse after a cURL engine was last used before it is considered stale and destroyed. Evidence suggests that some upstream network devices ignore the cURL keep-alive setting and forcibly close the active TCP connection when idle. _**Value Type:**_ Integer _**Default Value:**_ 120 _**Config Example:**_ `monitor_fullscan_frequency = "120"` _**CLI Option Use:**_ *None - this is a config file option only* > [!IMPORTANT] > It is strongly recommended not to modify this setting without conducting thorough network testing. Changing this option may lead to unexpected behaviour or connectivity issues, especially if upstream network devices handle idle connections in non-standard ways. ### monitor_fullscan_frequency _**Description:**_ This configuration option controls the number of 'monitor_interval' iterations between when a full scan of your data is performed to ensure data integrity and consistency. _**Value Type:**_ Integer _**Default Value:**_ 12 _**Config Example:**_ `monitor_fullscan_frequency = "24"` _**CLI Option Use:**_ `--monitor-fullscan-frequency '24'` > [!NOTE] > By default without configuration, 'monitor_fullscan_frequency' is set to 12. In this default state, this means that a full scan is performed every 'monitor_interval' x 'monitor_fullscan_frequency' = 3600 seconds. This setting is only applicable when running in `--monitor` mode. Setting this configuration option to '0' will *disable* the full scan of your data online. ### monitor_interval _**Description:**_ This configuration setting determines how often the synchronisation loops run in --monitor mode, measured in seconds. When this time period elapses, the client will check for online changes in Microsoft OneDrive, conduct integrity checks on local data and scan the local 'sync_dir' to identify any new content that hasn't been uploaded yet. _**Value Type:**_ Integer _**Default Value:**_ 300 _**Config Example:**_ `monitor_interval = "600"` _**CLI Option Use:**_ `--monitor-interval '600'` > [!NOTE] > A minimum value of 300 is enforced for this configuration setting. ### monitor_log_frequency _**Description:**_ This configuration option controls the suppression of frequently printed log items to the system console when using `--monitor` mode. The aim of this configuration item is to reduce the log output when near zero sync activity is occurring. _**Value Type:**_ Integer _**Default Value:**_ 12 _**Config Example:**_ `monitor_log_frequency = "24"` _**CLI Option Use:**_ `--monitor-log-frequency '24'` _**Usage Example:**_ By default, at application start-up when using `--monitor` mode, the following will be logged to indicate that the application has correctly started and has performed all the initial processing steps: ```text Reading configuration file: /home/user/.config/onedrive/config Configuration file successfully loaded Configuring Global Azure AD Endpoints Sync Engine Initialised with new Onedrive API instance All application operations will be performed in: /home/user/OneDrive OneDrive synchronisation interval (seconds): 300 Initialising filesystem inotify monitoring ... Performing initial synchronisation to ensure consistent local state ... Starting a sync with Microsoft OneDrive Fetching items from the OneDrive API for Drive ID: b!bO8V6s9SSk9R7mWhpIjUrotN73WlW3tEv3OxP_QfIdQimEdOHR-1So6CqeG1MfDB .. Processing changes and items received from Microsoft OneDrive ... Performing a database consistency and integrity check on locally stored data ... Scanning the local file system '~/OneDrive' for new data to upload ... Performing a final true-up scan of online data from Microsoft OneDrive Fetching items from the OneDrive API for Drive ID: b!bO8V6s9SSk9R7mWhpIjUrotN73WlW3tEv3OxP_QfIdQimEdOHR-1So6CqeG1MfDB .. Processing changes and items received from Microsoft OneDrive ... Sync with Microsoft OneDrive is complete ``` Then, based on 'monitor_log_frequency', the following output will be logged until the suppression loop value is reached: ```text Starting a sync with Microsoft OneDrive Syncing changes from Microsoft OneDrive ... Sync with Microsoft OneDrive is complete ``` > [!NOTE] > The additional log output `Performing a database consistency and integrity check on locally stored data ...` will only be displayed when this activity is occurring which is triggered by 'monitor_fullscan_frequency'. > [!NOTE] > If verbose application output is being used (`--verbose`), then this configuration setting has zero effect, as application verbose output takes priority over application output suppression. ### no_remote_delete _**Description:**_ This configuration option controls whether local file and folder deletes are actioned on Microsoft OneDrive. _**Value Type:**_ Boolean _**Default Value:**_ False _**Config Example:**_ `local_first = "false"` or `local_first = "true"` _**CLI Option Use:**_ `--no-remote-delete` > [!IMPORTANT] > This configuration option can *only* be used in conjunction with `--upload-only` ### notify_file_actions _**Description:**_ This configuration option controls whether the client will log via GUI notifications successful actions that the client performs. _**Value Type:**_ Boolean _**Default Value:**_ False _**Config Example:**_ `notify_file_actions = "true"` > [!NOTE] > GUI Notification Support must be compiled in first, otherwise this option will have zero effect and will not be used. ### operation_timeout _**Description:**_ This configuration option controls the maximum amount of time (seconds) a file operation is allowed to take. This includes DNS resolution, connecting, data transfer, etc. We recommend users not to tamper with this option unless strictly necessary. This option controls the CURLOPT_TIMEOUT setting of libcurl. _**Value Type:**_ Integer _**Default Value:**_ 3600 _**Config Example:**_ `operation_timeout = "3600"` ### permanent_delete _**Description:**_ Permanently delete an item online when it is removed locally. When using this method, they're permanently removed and aren't sent to the Microsoft OneDrive Recycle Bin. Therefore, permanently deleted drive items can't be restored afterward. Online data loss MAY occur in this scenario. _**Value Type:**_ Boolean _**Default Value:**_ False _**Config Example:**_ `permanent_delete = "true"` _**CLI Option Use:**_ *None - this is a config file option only* > [!IMPORTANT] > The Microsoft OneDrive API for this capability is also very narrow: > | Account Type | Config Option is Supported | > |--------------|------------------| > | Personal | ❌ | > | Business | ✔ | > | SharePoint | ✔ | > | Microsoft Cloud Germany | ✔ | > | Microsoft Cloud for US Government | ❌ | > | Azure and Office365 operated by VNET in China | ❌ | > > When using this config option against an unsupported Personal Accounts the following message will be generated: > ``` > WARNING: The application is configured to permanently delete files online; however, this action is not supported by Microsoft OneDrive Personal Accounts. > ``` > > When using this config option against a supported account the following message will be generated: > ``` > WARNING: Application has been configured to permanently remove files online rather than send to the recycle bin. Permanently deleted items can't be restored. > WARNING: Online data loss MAY occur in this scenario. > ``` > ### rate_limit _**Description:**_ This configuration option controls the bandwidth used by the application, per thread, when interacting with Microsoft OneDrive. _**Value Type:**_ Integer _**Default Value:**_ 0 (unlimited, use available bandwidth per thread) _**Valid Values:**_ Valid tested values for this configuration option are as follows: * 131072 = 128 KB/s - absolute minimum for basic application operations to prevent timeouts * 262144 = 256 KB/s * 524288 = 512 KB/s * 1048576 = 1 MB/s * 10485760 = 10 MB/s * 104857600 = 100 MB/s _**Config Example:**_ `rate_limit = "131072"` ### read_only_auth_scope _**Description:**_ This configuration option controls whether the OneDrive Client for Linux operates in a totally in read-only operation. _**Value Type:**_ Boolean _**Default Value:**_ False _**Config Example:**_ `read_only_auth_scope = "false"` or `read_only_auth_scope = "true"` > [!IMPORTANT] > When using 'read_only_auth_scope' you also will need to remove your existing application access consent otherwise old authentication consent will be valid and will be used. This will mean the application will technically have the consent to upload data until you revoke this consent. ### remove_source_files _**Description:**_ This configuration option controls whether the OneDrive Client for Linux removes the local file post successful transfer to Microsoft OneDrive. _**Value Type:**_ Boolean _**Default Value:**_ False _**Config Example:**_ `remove_source_files = "false"` or `remove_source_files = "true"` _**CLI Option Use:**_ `--remove-source-files` > [!IMPORTANT] > This configuration option can *only* be used in conjunction with `--upload-only` ### resync _**Description:**_ This configuration option controls whether the known local sync state with Microsoft OneDrive is removed at application startup. When this option is used, a full scan of your data online is performed to ensure that the local sync state is correctly built back up. _**Value Type:**_ Boolean _**Default Value:**_ False _**Config Example:**_ `resync = "false"` or `resync = "true"` _**CLI Option Use:**_ `--resync` > [!CAUTION] > It's highly recommended to use this option only if the application prompts you to do so. Don't blindly use this option as a default option. If you alter any of the subsequent configuration items, you will be required to execute a `--resync` to make sure your client is syncing your data with the updated configuration: > * drive_id > * sync_dir > * skip_file > * skip_dir > * skip_dotfiles > * skip_symlinks > * sync_business_shared_items > * Creating, Modifying or Deleting the 'sync_list' file ### resync_auth _**Description:**_ This configuration option controls the approval of performing a 'resync' which can be beneficial in automated environments. _**Value Type:**_ Boolean _**Default Value:**_ False _**Config Example:**_ `resync_auth = "false"` or `resync_auth = "true"` _**CLI Option Use:**_ `--resync-auth` > [!TIP] > In certain automated environments (assuming you know what you're doing due to using automation), to avoid the 'proceed with acknowledgement' resync requirement, this option allows you to automatically acknowledge the resync prompt. ### skip_dir _**Description:**_ This configuration option controls whether the application skips certain directories from being synced. Directories can be specified in 2 ways: * As a single entry. This will search the respective path for this entry and skip all instances where this directory is present, where ever it may exist. * As a full path entry. This will skip the explicit path as set. > [!IMPORTANT] > Entries for 'skip_dir' are *relative* to your 'sync_dir' path. _**Value Type:**_ String _**Default Value:**_ *Empty* - not required for normal operation _**Config Example:**_ Patterns are case insensitive. `*` and `?` [wildcards characters](https://technet.microsoft.com/en-us/library/bb490639.aspx) are supported. Use `|` to separate multiple patterns. ```text skip_dir = "Desktop|Documents/IISExpress|Documents/SQL Server Management Studio|Documents/Visual Studio*|Documents/WindowsPowerShell|.Rproj-user" ``` The 'skip_dir' option can also be specified multiple times within your config file, for example: ```text skip_dir = "SkipThisDirectoryAnywhere" skip_dir = ".SkipThisOtherDirectoryAnywhere" skip_dir = "/Explicit/Path/To/A/Directory" skip_dir = "/Another/Explicit/Path/To/Different/Directory" ``` This will be interpreted the same as: ```text skip_dir = "SkipThisDirectoryAnywhere|.SkipThisOtherDirectoryAnywhere|/Explicit/Path/To/A/Directory|/Another/Explicit/Path/To/Different/Directory" ``` _**CLI Option Use:**_ `--skip-dir 'SkipThisDirectoryAnywhere|.SkipThisOtherDirectoryAnywhere|/Explicit/Path/To/A/Directory|/Another/Explicit/Path/To/Different/Directory'` > [!NOTE] > This option is considered a 'Client Side Filtering Rule' and if configured, is utilised for all sync operations. If using the config file and CLI option is used, the CLI option will *replace* the config file entries. After changing or modifying this option, you will be required to perform a resync. ### skip_dir_strict_match _**Description:**_ This configuration option controls whether the application performs strict directory matching when checking 'skip_dir' items. When enabled, the 'skip_dir' item must be a full path match to the path to be skipped. _**Value Type:**_ Boolean _**Default Value:**_ False _**Config Example:**_ `skip_dir_strict_match = "false"` or `skip_dir_strict_match = "true"` _**CLI Option Use:**_ `--skip-dir-strict-match` ### skip_dotfiles _**Description:**_ This configuration option controls whether the application will skip all .files and .folders when performing sync operations. _**Value Type:**_ Boolean _**Default Value:**_ False _**Config Example:**_ `skip_dotfiles = "false"` or `skip_dotfiles = "true"` _**CLI Option Use:**_ `--skip-dot-files` > [!NOTE] > This option is considered a 'Client Side Filtering Rule' and if configured, is utilised for all sync operations. After changing this option, you will be required to perform a resync. ### skip_file _**Description:**_ This configuration option controls whether the application skips certain files from being synced. _**Value Type:**_ String _**Default Value:**_ `~*|.~*|*.tmp|*.swp|*.partial` _**Config Example:**_ Patterns are case insensitive. `*` and `?` [wildcards characters](https://technet.microsoft.com/en-us/library/bb490639.aspx) are supported. Use `|` to separate multiple patterns. By default, the following files will be skipped: * Files that start with ~ * Files that start with .~ (like .~lock.* files generated by LibreOffice) * Files that end in .tmp, .swp and .partial Files can be skipped in the following fashion: * Specify a wildcard, eg: '*.txt' (skip all txt files) * Explicitly specify the filename and it's full path relative to your sync_dir, eg: '/path/to/file/filename.ext' * Explicitly specify the filename only and skip every instance of this filename, eg: 'filename.ext' ```text skip_file = "~*|/Documents/OneNote*|/Documents/config.xlaunch|myfile.ext|/Documents/keepass.kdbx" ``` > [!IMPORTANT] > Entries for 'skip_file' are *relative* to your 'sync_dir' path. The 'skip_file' option can be specified multiple times within your config file, for example: ```text skip_file = "~*|.~*|*.tmp|*.swp" skip_file = "*.blah" skip_file = "never_sync.file" skip_file = "/Documents/keepass.kdbx" ``` This will be interpreted the same as: ```text skip_file = "~*|.~*|*.tmp|*.swp|*.blah|never_sync.file|/Documents/keepass.kdbx" ``` _**CLI Option Use:**_ `--skip-file '~*|.~*|*.tmp|*.swp|*.blah|never_sync.file|/Documents/keepass.kdbx'` > [!NOTE] > This option is considered a 'Client Side Filtering Rule' and if configured, is utilised for all sync operations. If using the config file and CLI option is used, the CLI option will *replace* the config file entries. After changing or modifying this option, you will be required to perform a resync. ### skip_size _**Description:**_ This configuration option controls whether the application skips syncing certain files larger than the specified size. The value specified is in MB. _**Value Type:**_ Integer _**Default Value:**_ 0 (all files, regardless of size, are synced) _**Config Example:**_ `skip_size = "50"` _**CLI Option Use:**_ `--skip-size '50'` ### skip_symlinks _**Description:**_ This configuration option controls whether the application will skip all symbolic links when performing sync operations. Microsoft OneDrive has no concept or understanding of symbolic links, and attempting to upload a symbolic link to Microsoft OneDrive generates a platform API error. All data (files and folders) that are uploaded to OneDrive must be whole files or actual directories. _**Value Type:**_ Boolean _**Default Value:**_ False _**Config Example:**_ `skip_symlinks = "false"` or `skip_symlinks = "true"` _**CLI Option Use:**_ `--skip-symlinks` > [!NOTE] > This option is considered a 'Client Side Filtering Rule' and if configured, is utilised for all sync operations. After changing this option, you will be required to perform a resync. ### space_reservation _**Description:**_ This configuration option controls how much local disk space should be reserved, to prevent the application from filling up your entire disk due to misconfiguration _**Value Type:**_ Integer _**Default Value:**_ 50 MB (expressed as Bytes when using `--display-config`) _**Config Example:**_ `space_reservation = "100"` _**CLI Option Use:**_ `--space-reservation '100'` ### sync_business_shared_items _**Description:**_ This configuration option controls whether OneDrive Business | Office 365 Shared Folders, when added as a 'shortcut' to your 'My Files', will be synced to your local system. _**Value Type:**_ Boolean _**Default Value:**_ False _**Config Example:**_ `sync_business_shared_items = "false"` or `sync_business_shared_items = "true"` _**CLI Option Use:**_ *None - this is a config file option only* > [!NOTE] > This option is considered a 'Client Side Filtering Rule' and if configured, is utilised for all sync operations. After changing this option, you will be required to perform a resync. > [!CAUTION] > This option is *not* backwards compatible with any v2.4.x application version. If you are enabling this option on *any* system running v2.5.x application version, all your application versions being used *everywhere* must be v2.5.x codebase. ### sync_dir _**Description:**_ This configuration option determines the location on your local filesystem where your data from Microsoft OneDrive will be saved. _**Value Type:**_ String _**Default Value:**_ `~/OneDrive` _**Config Example:**_ `sync_dir = "~/MyDirToSync"` _**CLI Option Use:**_ `--syncdir '~/MyDirToSync'` > [!CAUTION] > After changing this option, you will be required to perform a resync. Do not change or modify this option without fully understanding the implications of doing so. ### sync_dir_permissions _**Description:**_ This configuration option defines the directory permissions applied when a new directory is created locally during the process of syncing your data from Microsoft OneDrive. _**Value Type:**_ Integer _**Default Value:**_ `700` - This provides the following permissions: `drwx------` _**Config Example:**_ `sync_dir_permissions = "700"` > [!IMPORTANT] > Use the [Unix Permissions Calculator](https://chmod-calculator.com/) to help you determine the necessary new permissions. You will need to manually update all existing directory permissions if you modify this value. ### sync_file_permissions _**Description:**_ This configuration option defines the file permissions applied when a new file is created locally during the process of syncing your data from Microsoft OneDrive. _**Value Type:**_ Integer _**Default Value:**_ `600` - This provides the following permissions: `-rw-------` _**Config Example:**_ `sync_file_permissions = "600"` > [!IMPORTANT] > Use the [Unix Permissions Calculator](https://chmod-calculator.com/) to help you determine the necessary new permissions. You will need to manually update all existing directory permissions if you modify this value. ### sync_root_files _**Description:**_ This configuration option manages the synchronisation of files located in the 'sync_dir' root when using a 'sync_list.' It enables you to sync all these files by default, eliminating the need to repeatedly modify your 'sync_list' and initiate resynchronisation. _**Value Type:**_ Boolean _**Default Value:**_ False _**Config Example:**_ `sync_root_files = "false"` or `sync_root_files = "true"` _**CLI Option Use:**_ `--sync-root-files` > [!IMPORTANT] > Although it's not mandatory, it's recommended that after enabling this option, you perform a `--resync`. This ensures that any previously excluded content is now included in your sync process. ### threads _**Description:**_ This configuration option controls the number of 'threads' for upload and download operations when files need to be transferred between your local system and Microsoft OneDrive. _**Value Type:**_ Integer _**Default Value:**_ `8` _**Maximum Value:**_ `16` _**Config Example:**_ `threads = "16"` > [!WARNING] > Increasing the threads beyond the default will lead to increased system utilisation and local TCP port use, which may lead to unpredictable behaviour and/or may lead application stability issues. ### transfer_order _**Description:**_ This configuration option controls the transfer order of files between your local system and Microsoft OneDrive. _**Value Type:**_ String _**Default Value:**_ `default` _**Config Example:**_ #### Transfer by size, smallest first ``` transfer_order = "size_asc" ``` #### Transfer by size, largest first ``` transfer_order = "size_dsc" ``` #### Transfer by file name sorted A to Z ``` transfer_order = "name_asc" ``` #### Transfer by file name sorted Z to A ``` transfer_order = "name_dsc" ``` ### upload_only _**Description:**_ This setting forces the client to only upload data to Microsoft OneDrive and replicate the locate state online. By default, this will also remove content online, that has been removed locally. _**Value Type:**_ Boolean _**Default Value:**_ False _**Config Example:**_ `upload_only = "false"` or `upload_only = "true"` _**CLI Option Use:**_ `--upload-only` > [!IMPORTANT] > To ensure that data deleted locally remains accessible online, you can use the 'no_remote_delete' option. If you want to delete the data from your local storage after a successful upload to Microsoft OneDrive, you can use the 'remove_source_files' option. ### user_agent _**Description:**_ This configuration option controls the 'User-Agent' request header that is presented to Microsoft Graph API when accessing the Microsoft OneDrive service. This string lets servers and network peers identify the application, operating system, vendor, and/or version of the application making the request. We recommend users not to tamper with this option unless strictly necessary. _**Value Type:**_ String _**Default Value:**_ `ISV|abraunegg|OneDrive Client for Linux/vX.Y.Z-A-bcdefghi` _**Config Example:**_ `user_agent = "ISV|CompanyName|AppName/Version"` > [!IMPORTANT] > The default 'user_agent' value conforms to specific Microsoft requirements to identify as an ISV that complies with OneDrive traffic decoration requirements. Changing this value potentially will impact how Microsoft see's your client, thus your traffic may get throttled. For further information please read: https://learn.microsoft.com/en-us/sharepoint/dev/general-development/how-to-avoid-getting-throttled-or-blocked-in-sharepoint-online ### webhook_enabled _**Description:**_ This configuration option controls the application feature 'webhooks' to allow you to subscribe to remote updates as published by Microsoft OneDrive. This option only operates when the client is using 'Monitor Mode'. _**Value Type:**_ Boolean _**Default Value:**_ False _**Config Example:**_ The following is the minimum working example that needs to be added to your 'config' file to enable 'webhooks' successfully: ```text webhook_enabled = "true" webhook_public_url = "https:///webhooks/onedrive" ``` > [!NOTE] > Setting `webhook_enabled = "true"` enables the webhook feature in 'monitor' mode. The onedrive process will listen for incoming updates at a configurable endpoint, which defaults to `0.0.0.0:8888`. > [!IMPORTANT] > A valid HTTPS certificate is required for your public-facing URL if using nginx. Self signed certificates will be rejected. Consider using https://letsencrypt.org/ to utilise free SSL certificates for your public-facing URL. > [!TIP] > If you receive this application error: `Subscription validation request failed. Response must exactly match validationToken query parameter.` the most likely cause for this error will be your nginx configuration. > > To resolve this configuration issue, potentially investigate adding the following 'proxy' configuration options to your nginx configuration file: > ```text > server { > listen 443; > server_name ; > location /webhooks/onedrive { > proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; > proxy_set_header X-Original-Request-URI $request_uri; > proxy_read_timeout 300s; > proxy_connect_timeout 75s; > proxy_buffering off; > proxy_http_version 1.1; > proxy_pass http://127.0.0.1:8888; > } > } > ``` > For any further nginx configuration assistance, please refer to: https://docs.nginx.com/ ### webhook_expiration_interval _**Description:**_ This configuration option controls the frequency at which an existing Microsoft OneDrive webhook subscription expires. The value is expressed in the number of seconds before expiry. _**Value Type:**_ Integer _**Default Value:**_ 600 _**Config Example:**_ `webhook_expiration_interval = "1200"` ### webhook_listening_host _**Description:**_ This configuration option controls the host address that this client binds to, when the webhook feature is enabled. _**Value Type:**_ String _**Default Value:**_ 0.0.0.0 _**Config Example:**_ `webhook_listening_host = ""` - this will use the default value. `webhook_listening_host = "192.168.3.4"` - this will bind the client to use the IP address 192.168.3.4. > [!NOTE] > Use in conjunction with 'webhook_listening_port' to change the webhook listening endpoint. ### webhook_listening_port _**Description:**_ This configuration option controls the TCP port that this client listens on, when the webhook feature is enabled. _**Value Type:**_ Integer _**Default Value:**_ 8888 _**Config Example:**_ `webhook_listening_port = "9999"` > [!NOTE] > Use in conjunction with 'webhook_listening_host' to change the webhook listening endpoint. ### webhook_public_url _**Description:**_ This configuration option controls the URL that Microsoft will send subscription notifications to. This must be a valid Internet accessible URL. _**Value Type:**_ String _**Default Value:**_ *empty* _**Config Example:**_ ```text webhook_public_url = "https:///webhooks/onedrive" ``` ### webhook_renewal_interval _**Description:**_ This configuration option controls the frequency at which an existing Microsoft OneDrive webhook subscription is renewed. The value is expressed in the number of seconds before renewal. _**Value Type:**_ Integer _**Default Value:**_ 300 _**Config Example:**_ `webhook_renewal_interval = "600"` ### webhook_retry_interval _**Description:**_ This configuration option controls the frequency at which an existing Microsoft OneDrive webhook subscription is retried when creating or renewing a subscription failed. The value is expressed in the number of seconds before retry. _**Value Type:**_ Integer _**Default Value:**_ 60 _**Config Example:**_ `webhook_retry_interval = "120"` ### write_xattr_data _**Description:**_ This setting enables writing xattr values detailing the 'createdBy' and 'lastModifiedBy' information provided by the OneDrive API _**Value Type:**_ Boolean _**Default Value:**_ False _**Config Example:**_ `write_xattr_data = "false"` or `write_xattr_data = "true"` _**CLI Option Use:**_ *None - this is a config file option only* _**xattr Data Example:**_ ``` user.onedrive.createdBy="Account Display Name" user.onedrive.lastModifiedBy="Account Display Name" ``` ## Command Line Interface (CLI) Only Options ### CLI Option: --auth-files _**Description:**_ This CLI option allows the user to perform application authentication not via an interactive dialog but via specific files that the application uses to read the authentication data from. _**Usage Example:**_ `onedrive --auth-files authUrl:responseUrl` > [!IMPORTANT] > The authorisation URL is written to the specified 'authUrl' file, then onedrive waits for the file 'responseUrl' to be present, and reads the authentication response from that file. Example: > > ```text > onedrive --auth-files '~/onedrive-auth-url:~/onedrive-response-url' > Reading configuration file: /home/alex/.config/onedrive/config > Configuration file successfully loaded > Configuring Global Azure AD Endpoints > Client requires authentication before proceeding. Waiting for --auth-files elements to be available. > ``` > At this point, the client has written the file `~/onedrive-auth-url` which contains the authentication URL that needs to be visited to perform the authentication process. The client will now wait and watch for the presence of the file `~/onedrive-response-url`. > > Visit the authentication URL, and then create a new file called `~/onedrive-response-url` with the response URI. Once this has been done, the application will acknowledge the presence of this file, read the contents, and authenticate the application. > ```text > Sync Engine Initialised with new Onedrive API instance > > --sync or --monitor switches missing from your command line input. Please add one (not both) of these switches to your command line or use 'onedrive --help' for further assistance. > > No OneDrive sync will be performed without one of these two arguments being present. > ``` ### CLI Option: --auth-response _**Description:**_ This CLI option allows the user to perform application authentication not via an interactive dialog but via providing the authentication response URI directly. _**Usage Example:**_ `onedrive --auth-response https://login.microsoftonline.com/common/oauth2/nativeclient?code=` > [!TIP] > Typically, unless the application client identifier has been modified, authentication scopes are being modified or a specific Azure Tenant is being specified, the authentication URL will most likely be as follows: > ```text > https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=d50ca740-c83f-4d1b-b616-12c519384f0c&scope=Files.ReadWrite%20Files.ReadWrite.All%20Sites.ReadWrite.All%20offline_access&response_type=code&prompt=login&redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient > ``` > With this URL being known, it is possible ahead of time to request an authentication token by visiting this URL, and performing the authentication access request. ### CLI Option: --confdir _**Description:**_ This CLI option allows the user to specify where all the application configuration and relevant components are stored. _**Usage Example:**_ `onedrive --confdir '~/.config/onedrive-business/'` > [!IMPORTANT] > If using this option, it must be specified each and every time the application is used. If this is omitted, the application default configuration directory will be used. ### CLI Option: --create-directory _**Description:**_ This CLI option allows the user to create the specified directory path on Microsoft OneDrive without performing a sync. _**Usage Example:**_ `onedrive --create-directory 'path/of/new/folder/structure/to/create/'` > [!IMPORTANT] > The specified path to create is relative to your configured 'sync_dir'. ### CLI Option: --create-share-link _**Description:**_ This CLI option enables the creation of a shareable file link that can be provided to users to access the file that is stored on Microsoft OneDrive. By default, the permissions for the file will be 'read-only'. _**Usage Example:**_ `onedrive --create-share-link 'relative/path/to/your/file.txt'` > [!IMPORTANT] > If writable access to the file is required, you must add `--with-editing-perms` to your command. See below for details. ### CLI Option: --destination-directory _**Description:**_ This CLI option specifies the 'destination' portion of moving a file or folder online, without performing a sync operation. _**Usage Example:**_ `onedrive --source-directory 'path/as/source/' --destination-directory 'path/as/destination'` > [!IMPORTANT] > All specified paths are relative to your configured 'sync_dir'. ### CLI Option: --display-config _**Description:**_ This CLI option will display the effective application configuration _**Usage Example:**_ `onedrive --display-config` ### CLI Option: --display-sync-status _**Description:**_ This CLI option will display the sync status of the configured 'sync_dir' _**Usage Example:**_ `onedrive --display-sync-status` > [!TIP] > This option can also use the `--single-directory` option to determine the sync status of a specific directory within the configured 'sync_dir' ### CLI Option: ---display-quota _**Description:**_ This CLI option will display the quota status of the account drive id or the configured 'drive_id' value _**Usage Example:**_ `onedrive --display-quota` ### CLI Option: --force _**Description:**_ This CLI option enables the force the deletion of data when a 'big delete' is detected. _**Usage Example:**_ `onedrive --sync --verbose --force` > [!IMPORTANT] > This option should only be used exclusively in cases where you've initiated a 'big delete' and genuinely intend to remove all the data that is set to be deleted online. ### CLI Option: --force-sync _**Description:**_ This CLI option enables the syncing of a specific directory, using the Client Side Filtering application defaults, overriding any user application configuration. _**Usage Example:**_ `onedrive --sync --verbose --force-sync --single-directory 'Data' > [!NOTE] > When this option is used, you will be presented with the following warning and risk acceptance: > ```text > WARNING: Overriding application configuration to use application defaults for skip_dir and skip_file due to --synch --single-directory --force-sync being used > > The use of --force-sync will reconfigure the application to use defaults. This may have untold and unknown future impacts. > By proceeding in using this option you accept any impacts including any data loss that may occur as a result of using --force-sync. > > Are you sure you wish to proceed with --force-sync [Y/N] > ``` > To proceed with this sync task, you must risk accept the actions you are taking. If you have any concerns, first use `--dry-run` and evaluate the outcome before proceeding with the actual action. ### CLI Option: --get-file-link _**Description:**_ This CLI option queries the OneDrive API and return's the WebURL for the given local file. _**Usage Example:**_ `onedrive --get-file-link 'relative/path/to/your/file.txt'` > [!IMPORTANT] > The path that you should use *must* be relative to your 'sync_dir' ### CLI Option: --get-sharepoint-drive-id _**Description:**_ This CLI option queries the OneDrive API and return's the Office 365 Drive ID for a given Office 365 SharePoint Shared Library that can then be used with 'drive_id' to sync a specific SharePoint Library. _**Usage Example:**_ `onedrive --get-sharepoint-drive-id '*'` or `onedrive --get-sharepoint-drive-id 'PointPublishing Hub Site'` ### CLI Option: --list-shared-items _**Description:**_ This CLI option lists all OneDrive Business Shared items with your account. The resulting list shows shared files and folders that you can configure this client to sync. _**Usage Example:**_ `onedrive --list-shared-items` _**Example Output:**_ ``` ... Listing available OneDrive Business Shared Items: ----------------------------------------------------------------------------------- Shared File: large_document_shared.docx Shared By: test user (testuser@domain.tld) ----------------------------------------------------------------------------------- Shared File: no_download_access.docx Shared By: test user (testuser@domain.tld) ----------------------------------------------------------------------------------- Shared File: online_access_only.txt Shared By: test user (testuser@domain.tld) ----------------------------------------------------------------------------------- Shared File: read_only.txt Shared By: test user (testuser@domain.tld) ----------------------------------------------------------------------------------- Shared File: qewrqwerwqer.txt Shared By: test user (testuser@domain.tld) ----------------------------------------------------------------------------------- Shared File: dummy_file_to_share.docx Shared By: testuser2 testuser2 (testuser2@domain.tld) ----------------------------------------------------------------------------------- Shared Folder: Sub Folder 2 Shared By: test user (testuser@domain.tld) ----------------------------------------------------------------------------------- Shared File: file to share.docx Shared By: test user (testuser@domain.tld) ----------------------------------------------------------------------------------- Shared Folder: Top Folder Shared By: test user (testuser@domain.tld) ----------------------------------------------------------------------------------- ... ``` ### CLI Option: --logout _**Description:**_ This CLI option removes this clients authentication status with Microsoft OneDrive. Any further application use will require the application to be re-authenticated with Microsoft OneDrive. _**Usage Example:**_ `onedrive --logout` ### CLI Option: --modified-by _**Description:**_ This CLI option queries the OneDrive API and return's the last modified details for the given local file. _**Usage Example:**_ `onedrive --modified-by 'relative/path/to/your/file.txt'` > [!IMPORTANT] > The path that you should use *must* be relative to your 'sync_dir' ### CLI Option: --monitor | -m _**Description:**_ This CLI option controls the 'Monitor Mode' operational aspect of the client. When this option is used, the client will perform on-going syncs of data between Microsoft OneDrive and your local system. Local changes will be uploaded in near-realtime, whilst online changes will be downloaded on the next sync process. The frequency of these checks is governed by the 'monitor_interval' value. _**Usage Example:**_ `onedrive --monitor` or `onedrive -m` ### CLI Option: --print-access-token _**Description:**_ Print the current access token being used to access Microsoft OneDrive. _**Usage Example:**_ `onedrive --verbose --verbose --debug-https --print-access-token` > [!CAUTION] > Do not use this option if you do not know why you are wanting to use it. Be highly cautious of exposing this object. Change your password if you feel that you have inadvertently exposed this token. ### CLI Option: --reauth _**Description:**_ This CLI option controls the ability to re-authenticate your client with Microsoft OneDrive. _**Usage Example:**_ `onedrive --reauth` ### CLI Option: --remove-directory _**Description:**_ This CLI option allows the user to remove the specified directory path on Microsoft OneDrive without performing a sync. _**Usage Example:**_ `onedrive --remove-directory 'path/of/new/folder/structure/to/remove/'` > [!IMPORTANT] > The specified path to remove is relative to your configured 'sync_dir'. ### CLI Option: --share-password _**Description:**_ This CLI option enables the creation of a shareable file link that can only be accessed by providing the valid password. This option can only be used in conjunction with `--create-share-link` _**Usage Example:**_ `onedrive --create-share-link 'relative/path/to/your/file.txt' --share-password 'valid password'` ### CLI Option: --single-directory _**Description:**_ This CLI option controls the applications ability to sync a specific single directory. _**Usage Example:**_ `onedrive --sync --single-directory 'Data'` > [!IMPORTANT] > The path specified is relative to your configured 'sync_dir' path. If the physical local path 'Folder' to sync is `~/OneDrive/Data/Folder` then the command would be `--single-directory 'Data/Folder'`. ### CLI Option: --source-directory _**Description:**_ This CLI option specifies the 'source' portion of moving a file or folder online, without performing a sync operation. _**Usage Example:**_ `onedrive --source-directory 'path/as/source/' --destination-directory 'path/as/destination'` > [!IMPORTANT] > All specified paths are relative to your configured 'sync_dir'. ### CLI Option: --sync | -s _**Description:**_ This CLI option controls the 'Standalone Mode' operational aspect of the client. When this option is used, the client will perform a one-time sync of data between Microsoft OneDrive and your local system. _**Usage Example:**_ `onedrive --sync` or `onedrive -s` ### CLI Option: --sync-shared-files _**Description:**_ Sync OneDrive Business Shared Files to the local filesystem. _**Usage Example:**_ `onedrive --sync --sync-shared-files` > [!IMPORTANT] > To use this option you must first enable 'sync_business_shared_items' within your application configuration. Please read 'business-shared-items.md' for more information regarding this option. ### CLI Option: --verbose | -v+ _**Description:**_ This CLI option controls the verbosity of the application output. Use the option once, to have normal verbose output, use twice to have debug level application output. _**Usage Example:**_ `onedrive --sync --verbose` or `onedrive --monitor --verbose` ### CLI Option: --with-editing-perms _**Description:**_ This CLI option enables the creation of a writable shareable file link that can be provided to users to access the file that is stored on Microsoft OneDrive. This option can only be used in conjunction with `--create-share-link` _**Usage Example:**_ `onedrive --create-share-link 'relative/path/to/your/file.txt' --with-editing-perms` > [!IMPORTANT] > Placement of `--with-editing-perms` is critical. It *must* be placed after the file path as per the example above. ## Deprecated Configuration File and CLI Options The following configuration options are no longer supported: ### force_http_2 _**Description:**_ Force the use of HTTP/2 for all operations where applicable _**Deprecated Config Example:**_ `force_http_2 = "true"` _**Deprecated CLI Option:**_ `--force-http-2` _**Reason for depreciation:**_ HTTP/2 will be used by default where possible, when the OneDrive API platform does not downgrade the connection to HTTP/1.1, thus this configuration option is no longer required. ### min_notify_changes _**Description:**_ Minimum number of pending incoming changes necessary to trigger a GUI desktop notification. _**Deprecated Config Example:**_ `min_notify_changes = "50"` _**Deprecated CLI Option:**_ `--min-notify-changes '50'` _**Reason for depreciation:**_ Application has been totally re-written. When this item was introduced, it was done so to reduce spamming of all events to the GUI desktop. ### CLI Option: --synchronize _**Description:**_ Perform a synchronisation with Microsoft OneDrive _**Deprecated CLI Option:**_ `--synchronize` _**Reason for depreciation:**_ `--synchronize` has been deprecated in favour of `--sync` or `-s` onedrive-2.5.5/docs/application-security.md000066400000000000000000000150661476564400300210010ustar00rootroot00000000000000# OneDrive Client for Linux Application Security This document details the following information: * Why is this application an 'unverified publisher'? * Application Security and Permission Scopes * How to change Permission Scopes * How to review your existing application access consent ## Why is this application an 'unverified publisher'? Publisher Verification, as per the Microsoft [process](https://learn.microsoft.com/en-us/azure/active-directory/develop/publisher-verification-overview) has actually been configured, and, actually has been verified! ### Verified Publisher Configuration Evidence As per the image below, the Azure portal shows that the 'Publisher Domain' has actually been verified: ![confirmed_verified_publisher](./images/confirmed_verified_publisher.jpg) * The 'Publisher Domain' is: https://abraunegg.github.io/ * The required 'Microsoft Identity Association' is: https://abraunegg.github.io/.well-known/microsoft-identity-association.json ## Application Security and Permission Scopes There are 2 main components regarding security for this application: * Azure Application Permissions * User Authentication Permissions Keeping this in mind, security options should follow the security principal of 'least privilege': > The principle that a security architecture should be designed so that each entity > is granted the minimum system resources and authorizations that the entity needs > to perform its function. Reference: [https://csrc.nist.gov/glossary/term/least_privilege](https://csrc.nist.gov/glossary/term/least_privilege) As such, the following API permissions are used by default: ### Default Azure Application Permissions | API / Permissions name | Type | Description | Admin consent required | |---|---|---|---| | Files.Read | Delegated | Have read-only access to user files | No | | Files.Read.All | Delegated | Have read-only access to all files user can access | No | | Sites.Read.All | Delegated | Have read-only access to all items in all site collections | No | | offline_access | Delegated | Maintain access to data you have given it access to | No | ![default_authentication_scopes](./images/default_authentication_scopes.jpg) ### Default User Authentication Permissions When a user authenticates with Microsoft OneDrive, additional account permissions are provided by service to give the user specific access to their data. These are delegated permissions provided by the platform: | API / Permissions name | Type | Description | Admin consent required | |---|---|---|---| | Files.ReadWrite | Delegated | Have full access to user files | No | | Files.ReadWrite.All | Delegated | Have full access to all files user can access | No | | Sites.ReadWrite.All | Delegated | Have full access to all items in all site collections | No | | offline_access | Delegated | Maintain access to data you have given it access to | No | When these delegated API permissions are combined, these provide the effective authentication scope for the OneDrive Client for Linux to access your data. The resulting effective 'default' permissions will be: | API / Permissions name | Type | Description | Admin consent required | |---|---|---|---| | Files.ReadWrite | Delegated | Have full access to user files | No | | Files.ReadWrite.All | Delegated | Have full access to all files user can access | No | | Sites.ReadWrite.All | Delegated | Have full access to all items in all site collections | No | | offline_access | Delegated | Maintain access to data you have given it access to | No | These 'default' permissions will allow the OneDrive Client for Linux to read, write and delete data associated with your OneDrive Account. ## How are the Authentication Scopes used? When using the OneDrive Client for Linux, the above authentication scopes will be presented to the Microsoft Authentication Service (login.microsoftonline.com), where the service will validate the request and provide an applicable token to access Microsoft OneDrive with. This can be illustrated as the following: ![Linux Authentication to Microsoft OneDrive](./puml/onedrive_linux_authentication.png) This is similar to the Microsoft Windows OneDrive Client: ![Windows Authentication to Microsoft OneDrive](./puml/onedrive_windows_authentication.png) In a business setting, IT staff who need to authorise the use of the OneDrive Client for Linux in their environment can be assured of its safety. The primary concern for IT staff should be securing the device running the OneDrive Client for Linux. Unlike in a corporate environment where Windows devices are secured through Active Directory and Group Policy Objects (GPOs) to protect corporate data on the device, it is beyond the responsibility of this client to manage security on Linux devices. ## Configuring read-only access to your OneDrive data In some situations, it may be desirable to configure the OneDrive Client for Linux totally in read-only operation. To change the application to 'read-only' access, add the following to your configuration file: ```text read_only_auth_scope = "true" ``` This will change the user authentication scope request to use read-only access. > [!IMPORTANT] > When changing this value, you *must* re-authenticate the client using the `--reauth` option to utilise the change in authentication scopes. When using read-only authentication scopes, the uploading of any data or local change to OneDrive will fail with the following error: ``` 2022-Aug-06 13:16:45.3349625 ERROR: Microsoft OneDrive API returned an error with the following message: 2022-Aug-06 13:16:45.3351661 Error Message: HTTP request returned status code 403 (Forbidden) 2022-Aug-06 13:16:45.3352467 Error Reason: Access denied 2022-Aug-06 13:16:45.3352838 Error Timestamp: 2022-06-12T13:16:45 2022-Aug-06 13:16:45.3353171 API Request ID: ``` As such, it is also advisable for you to add the following to your configuration file so that 'uploads' are prevented: ```text download_only = "true" ``` > [!IMPORTANT] > Additionally when using 'read_only_auth_scope' you also will need to remove your existing application access consent otherwise old authentication consent will be valid and will be used. This will mean the application will technically have the consent to upload data. See below on how to remove your prior application consent. ## Reviewing your existing application access consent To review your existing application access consent, you need to access the following URL: https://account.live.com/consent/Manage From here, you are able to review what applications have been given what access to your data, and remove application access as required. onedrive-2.5.5/docs/build-rpm-howto.md000066400000000000000000000435661476564400300176700ustar00rootroot00000000000000# RPM Package Build Process The instructions below have been tested on the following systems: * CentOS Stream release 9 These instructions should also be applicable for RedHat & Fedora platforms, or any other RedHat RPM based distribution. ## Prepare Package Development Environment Install the following dependencies on your build system: ```text sudo yum groupinstall -y 'Development Tools' sudo yum install -y libcurl-devel sudo yum install -y sqlite-devel sudo yum install -y libnotify-devel sudo yum install -y wget sudo yum install -y https://downloads.dlang.org/releases/2.x/2.088.0/dmd-2.088.0-0.fedora.x86_64.rpm mkdir -p ~/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS} ``` ## Build RPM from spec file Build the RPM from the provided spec file: ```text wget https://github.com/abraunegg/onedrive/archive/refs/tags/v2.5.0.tar.gz -O ~/rpmbuild/SOURCES/v2.5.0.tar.gz #wget https://raw.githubusercontent.com/abraunegg/onedrive/master/contrib/spec/onedrive.spec.in -O ~/rpmbuild/SPECS/onedrive.spec wget https://raw.githubusercontent.com/abraunegg/onedrive/onedrive-v2.5.0-release-candidate-3/contrib/spec/onedrive.spec.in -O ~/rpmbuild/SPECS/onedrive.spec rpmbuild -ba ~/rpmbuild/SPECS/onedrive.spec ``` ## RPM Build Example Results Below are example output results of building, installing and running the RPM package on the respective platforms: ### CentOS Stream release 9 RPM Build Process ```text [alex@centos9stream ~]$ rpmbuild -ba ~/rpmbuild/SPECS/onedrive.spec Executing(%prep): /bin/sh -e /var/tmp/rpm-tmp.V7l9aO + umask 022 + cd /home/alex/rpmbuild/BUILD + cd /home/alex/rpmbuild/BUILD + rm -rf onedrive-2.5.0 + /usr/bin/tar -xof - + /usr/bin/gzip -dc /home/alex/rpmbuild/SOURCES/v2.5.0.tar.gz + STATUS=0 + '[' 0 -ne 0 ']' + cd onedrive-2.5.0 + /usr/bin/chmod -Rf a+rX,u+w,g-w,o-w . + RPM_EC=0 ++ jobs -p + exit 0 Executing(%build): /bin/sh -e /var/tmp/rpm-tmp.x8hFro + umask 022 + cd /home/alex/rpmbuild/BUILD + cd onedrive-2.5.0 + CFLAGS='-O2 -flto=auto -ffat-lto-objects -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -fstack-protector-strong -specs=/usr/lib/rpm/redhat/redhat-annobin-cc1 -m64 -march=x86-64-v2 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection' + export CFLAGS + CXXFLAGS='-O2 -flto=auto -ffat-lto-objects -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -fstack-protector-strong -specs=/usr/lib/rpm/redhat/redhat-annobin-cc1 -m64 -march=x86-64-v2 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection' + export CXXFLAGS + FFLAGS='-O2 -flto=auto -ffat-lto-objects -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -fstack-protector-strong -specs=/usr/lib/rpm/redhat/redhat-annobin-cc1 -m64 -march=x86-64-v2 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -I/usr/lib64/gfortran/modules' + export FFLAGS + FCFLAGS='-O2 -flto=auto -ffat-lto-objects -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -fstack-protector-strong -specs=/usr/lib/rpm/redhat/redhat-annobin-cc1 -m64 -march=x86-64-v2 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -I/usr/lib64/gfortran/modules' + export FCFLAGS + LDFLAGS='-Wl,-z,relro -Wl,--as-needed -Wl,-z,now -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -specs=/usr/lib/rpm/redhat/redhat-annobin-cc1 ' + export LDFLAGS + LT_SYS_LIBRARY_PATH=/usr/lib64: + export LT_SYS_LIBRARY_PATH + CC=gcc + export CC + CXX=g++ + export CXX + '[' '-flto=auto -ffat-lto-objectsx' '!=' x ']' ++ find . -type f -name configure -print + for file in $(find . -type f -name configure -print) + /usr/bin/sed -r --in-place=.backup 's/^char \(\*f\) \(\) = /__attribute__ ((used)) char (*f) () = /g' ./configure + diff -u ./configure.backup ./configure + mv ./configure.backup ./configure + /usr/bin/sed -r --in-place=.backup 's/^char \(\*f\) \(\);/__attribute__ ((used)) char (*f) ();/g' ./configure + diff -u ./configure.backup ./configure + mv ./configure.backup ./configure + /usr/bin/sed -r --in-place=.backup 's/^char \$2 \(\);/__attribute__ ((used)) char \$2 ();/g' ./configure + diff -u ./configure.backup ./configure + mv ./configure.backup ./configure + /usr/bin/sed --in-place=.backup '1{$!N;$!N};$!N;s/int x = 1;\nint y = 0;\nint z;\nint nan;/volatile int x = 1; volatile int y = 0; volatile int z, nan;/;P;D' ./configure + diff -u ./configure.backup ./configure + mv ./configure.backup ./configure + /usr/bin/sed --in-place=.backup 's#^lt_cv_sys_global_symbol_to_cdecl=.*#lt_cv_sys_global_symbol_to_cdecl="sed -n -e '\''s/^T .* \\(.*\\)$/extern int \\1();/p'\'' -e '\''s/^$symcode* .* \\(.*\\)$/extern char \\1;/p'\''"#' ./configure + diff -u ./configure.backup ./configure + mv ./configure.backup ./configure + '[' 1 = 1 ']' +++ dirname ./configure ++ find . -name config.guess -o -name config.sub + '[' 1 = 1 ']' + '[' x '!=' 'x-Wl,-z,now -specs=/usr/lib/rpm/redhat/redhat-hardened-ld' ']' ++ find . -name ltmain.sh + ./configure --build=x86_64-redhat-linux-gnu --host=x86_64-redhat-linux-gnu --program-prefix= --disable-dependency-tracking --prefix=/usr --exec-prefix=/usr --bindir=/usr/bin --sbindir=/usr/sbin --sysconfdir=/etc --datadir=/usr/share --includedir=/usr/include --libdir=/usr/lib64 --libexecdir=/usr/libexec --localstatedir=/var --sharedstatedir=/var/lib --mandir=/usr/share/man --infodir=/usr/share/info --enable-debug --enable-notifications configure: WARNING: unrecognized options: --disable-dependency-tracking checking for a BSD-compatible install... /usr/bin/install -c checking for x86_64-redhat-linux-gnu-pkg-config... /usr/bin/x86_64-redhat-linux-gnu-pkg-config checking pkg-config is at least version 0.9.0... yes checking for dmd... dmd checking version of D compiler... 2.088.0 checking for curl... yes checking for sqlite... yes checking for notify... yes configure: creating ./config.status config.status: creating Makefile config.status: creating contrib/pacman/PKGBUILD config.status: creating contrib/spec/onedrive.spec config.status: creating onedrive.1 config.status: creating contrib/systemd/onedrive.service config.status: creating contrib/systemd/onedrive@.service configure: WARNING: unrecognized options: --disable-dependency-tracking + make if [ -f .git/HEAD ] ; then \ git describe --tags > version ; \ else \ echo v2.5.0 > version ; \ fi dmd -w -J. -g -debug -gs -version=NoPragma -version=NoGdk -version=Notifications -L-lcurl -L-lsqlite3 -L-lnotify -L-lgdk_pixbuf-2.0 -L-lgio-2.0 -L-lgobject-2.0 -L-lglib-2.0 -L-ldl src/main.d src/config.d src/log.d src/util.d src/qxor.d src/curlEngine.d src/onedrive.d src/webhook.d src/sync.d src/itemdb.d src/sqlite.d src/clientSideFiltering.d src/monitor.d src/arsd/cgi.d src/notifications/notify.d src/notifications/dnotify.d -ofonedrive + RPM_EC=0 ++ jobs -p + exit 0 Executing(%install): /bin/sh -e /var/tmp/rpm-tmp.Oj0XhN + umask 022 + cd /home/alex/rpmbuild/BUILD + '[' /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.0-1.el9.x86_64 '!=' / ']' + rm -rf /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.0-1.el9.x86_64 ++ dirname /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.0-1.el9.x86_64 + mkdir -p /home/alex/rpmbuild/BUILDROOT + mkdir /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.0-1.el9.x86_64 + cd onedrive-2.5.0 + /usr/bin/make install DESTDIR=/home/alex/rpmbuild/BUILDROOT/onedrive-2.5.0-1.el9.x86_64 'INSTALL=/usr/bin/install -p' PREFIX=/home/alex/rpmbuild/BUILDROOT/onedrive-2.5.0-1.el9.x86_64 /usr/bin/install -p -D onedrive /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.0-1.el9.x86_64/usr/bin/onedrive /usr/bin/install -p -D -m 0644 onedrive.1 /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.0-1.el9.x86_64/usr/share/man/man1/onedrive.1 /usr/bin/install -p -D -m 0644 contrib/logrotate/onedrive.logrotate /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.0-1.el9.x86_64/etc/logrotate.d/onedrive mkdir -p /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.0-1.el9.x86_64/usr/share/doc/onedrive /usr/bin/install -p -D -m 0644 readme.md config LICENSE changelog.md docs/advanced-usage.md docs/application-config-options.md docs/application-security.md docs/business-shared-items.md docs/client-architecture.md docs/contributing.md docs/docker.md docs/install.md docs/national-cloud-deployments.md docs/podman.md docs/privacy-policy.md docs/sharepoint-libraries.md docs/terms-of-service.md docs/ubuntu-package-install.md docs/usage.md docs/known-issues.md /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.0-1.el9.x86_64/usr/share/doc/onedrive /usr/bin/install -p -d -m 0755 /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.0-1.el9.x86_64/usr/lib/systemd/user /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.0-1.el9.x86_64/usr/lib/systemd/system /usr/bin/install -p -m 0644 contrib/systemd/onedrive@.service /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.0-1.el9.x86_64/usr/lib/systemd/system /usr/bin/install -p -m 0644 contrib/systemd/onedrive.service /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.0-1.el9.x86_64/usr/lib/systemd/system + /usr/lib/rpm/check-buildroot + /usr/lib/rpm/redhat/brp-ldconfig + /usr/lib/rpm/brp-compress + /usr/lib/rpm/brp-strip /usr/bin/strip + /usr/lib/rpm/brp-strip-comment-note /usr/bin/strip /usr/bin/objdump + /usr/lib/rpm/redhat/brp-strip-lto /usr/bin/strip + /usr/lib/rpm/brp-strip-static-archive /usr/bin/strip + /usr/lib/rpm/redhat/brp-python-bytecompile '' 1 0 + /usr/lib/rpm/brp-python-hardlink + /usr/lib/rpm/redhat/brp-mangle-shebangs Processing files: onedrive-2.5.0-1.el9.x86_64 Executing(%doc): /bin/sh -e /var/tmp/rpm-tmp.vy1y65 + umask 022 + cd /home/alex/rpmbuild/BUILD + cd onedrive-2.5.0 + DOCDIR=/home/alex/rpmbuild/BUILDROOT/onedrive-2.5.0-1.el9.x86_64/usr/share/doc/onedrive + export LC_ALL=C + LC_ALL=C + export DOCDIR + /usr/bin/mkdir -p /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.0-1.el9.x86_64/usr/share/doc/onedrive + cp -pr readme.md /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.0-1.el9.x86_64/usr/share/doc/onedrive + cp -pr LICENSE /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.0-1.el9.x86_64/usr/share/doc/onedrive + cp -pr changelog.md /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.0-1.el9.x86_64/usr/share/doc/onedrive + RPM_EC=0 ++ jobs -p + exit 0 warning: File listed twice: /usr/share/doc/onedrive warning: File listed twice: /usr/share/doc/onedrive/LICENSE warning: File listed twice: /usr/share/doc/onedrive/changelog.md warning: File listed twice: /usr/share/doc/onedrive/readme.md Provides: config(onedrive) = 2.5.0-1.el9 onedrive = 2.5.0-1.el9 onedrive(x86-64) = 2.5.0-1.el9 Requires(rpmlib): rpmlib(CompressedFileNames) <= 3.0.4-1 rpmlib(FileDigests) <= 4.6.0-1 rpmlib(PayloadFilesHavePrefix) <= 4.0-1 Requires(post): systemd Requires(preun): systemd Requires(postun): systemd Requires: ld-linux-x86-64.so.2()(64bit) ld-linux-x86-64.so.2(GLIBC_2.3)(64bit) libc.so.6()(64bit) libc.so.6(GLIBC_2.14)(64bit) libc.so.6(GLIBC_2.15)(64bit) libc.so.6(GLIBC_2.17)(64bit) libc.so.6(GLIBC_2.2.5)(64bit) libc.so.6(GLIBC_2.3.2)(64bit) libc.so.6(GLIBC_2.3.4)(64bit) libc.so.6(GLIBC_2.32)(64bit) libc.so.6(GLIBC_2.33)(64bit) libc.so.6(GLIBC_2.34)(64bit) libc.so.6(GLIBC_2.4)(64bit) libc.so.6(GLIBC_2.6)(64bit) libc.so.6(GLIBC_2.7)(64bit) libc.so.6(GLIBC_2.8)(64bit) libc.so.6(GLIBC_2.9)(64bit) libcurl.so.4()(64bit) libgcc_s.so.1()(64bit) libgcc_s.so.1(GCC_3.0)(64bit) libgcc_s.so.1(GCC_4.2.0)(64bit) libgdk_pixbuf-2.0.so.0()(64bit) libgio-2.0.so.0()(64bit) libglib-2.0.so.0()(64bit) libgobject-2.0.so.0()(64bit) libm.so.6()(64bit) libm.so.6(GLIBC_2.2.5)(64bit) libnotify.so.4()(64bit) libsqlite3.so.0()(64bit) rtld(GNU_HASH) Checking for unpackaged file(s): /usr/lib/rpm/check-files /home/alex/rpmbuild/BUILDROOT/onedrive-2.5.0-1.el9.x86_64 Wrote: /home/alex/rpmbuild/SRPMS/onedrive-2.5.0-1.el9.src.rpm Wrote: /home/alex/rpmbuild/RPMS/x86_64/onedrive-2.5.0-1.el9.x86_64.rpm Executing(%clean): /bin/sh -e /var/tmp/rpm-tmp.pM33Kl + umask 022 + cd /home/alex/rpmbuild/BUILD + cd onedrive-2.5.0 + RPM_EC=0 ++ jobs -p + exit 0 ``` ### CentOS Stream release 9 RPM Package Install Process ```text [alex@centos9stream ~]$ sudo yum -y install /home/alex/rpmbuild/RPMS/x86_64/onedrive-2.5.0-1.el9.x86_64.rpm [sudo] password for alex: Last metadata expiration check: 0:33:14 ago on Mon 19 Aug 2024 17:22:48. Dependencies resolved. =============================================================================================================================================================================================== Package Architecture Version Repository Size =============================================================================================================================================================================================== Installing: onedrive x86_64 2.5.0-1.el9 @commandline 1.5 M Transaction Summary =============================================================================================================================================================================================== Install 1 Package Total size: 1.5 M Installed size: 7.6 M Downloading Packages: Running transaction check Transaction check succeeded. Running transaction test Transaction test succeeded. Running transaction Preparing : 1/1 Installing : onedrive-2.5.0-1.el9.x86_64 1/1 Running scriptlet: onedrive-2.5.0-1.el9.x86_64 1/1 Verifying : onedrive-2.5.0-1.el9.x86_64 1/1 Installed: onedrive-2.5.0-1.el9.x86_64 Complete! [alex@centos9stream ~]$ onedrive --version onedrive v2.5.0 [alex@centos9stream ~]$ onedrive --display-config WARNING: D-Bus message bus daemon is not available; GUI notifications are disabled Application version = onedrive v2.5.0 Compiled with = DMD 2088 User Application Config path = /home/alex/.config/onedrive System Application Config path = /etc/onedrive Applicable Application 'config' location = /home/alex/.config/onedrive/config Configuration file found in config location = false - using application defaults Applicable 'sync_list' location = /home/alex/.config/onedrive/sync_list Applicable 'items.sqlite3' location = /home/alex/.config/onedrive/items.sqlite3 Config option 'drive_id' = Config option 'sync_dir' = ~/OneDrive Config option 'enable_logging' = false Config option 'log_dir' = /var/log/onedrive Config option 'disable_notifications' = false Config option 'skip_dir' = Config option 'skip_dir_strict_match' = false Config option 'skip_file' = ~*|.~*|*.tmp|*.swp|*.partial Config option 'skip_dotfiles' = false Config option 'skip_symlinks' = false Config option 'monitor_interval' = 300 Config option 'monitor_log_frequency' = 12 Config option 'monitor_fullscan_frequency' = 12 Config option 'read_only_auth_scope' = false Config option 'dry_run' = false Config option 'upload_only' = false Config option 'download_only' = false Config option 'local_first' = false Config option 'check_nosync' = false Config option 'check_nomount' = false Config option 'resync' = false Config option 'resync_auth' = false Config option 'cleanup_local_files' = false Config option 'classify_as_big_delete' = 1000 Config option 'disable_upload_validation' = false Config option 'disable_download_validation' = false Config option 'bypass_data_preservation' = false Config option 'no_remote_delete' = false Config option 'remove_source_files' = false Config option 'sync_dir_permissions' = 700 Config option 'sync_file_permissions' = 600 Config option 'space_reservation' = 52428800 Config option 'application_id' = d50ca740-c83f-4d1b-b616-12c519384f0c Config option 'azure_ad_endpoint' = Config option 'azure_tenant_id' = Config option 'user_agent' = ISV|abraunegg|OneDrive Client for Linux/v2.5.0 Config option 'force_http_11' = false Config option 'debug_https' = false Config option 'rate_limit' = 0 Config option 'operation_timeout' = 3600 Config option 'dns_timeout' = 60 Config option 'connect_timeout' = 10 Config option 'data_timeout' = 60 Config option 'ip_protocol_version' = 0 Config option 'threads' = 8 Environment var 'XDG_RUNTIME_DIR' = true Environment var 'DBUS_SESSION_BUS_ADDRESS' = true Selective sync 'sync_list' configured = false Config option 'sync_business_shared_items' = false Config option 'webhook_enabled' = false ```onedrive-2.5.5/docs/business-shared-items.md000066400000000000000000000420331476564400300210410ustar00rootroot00000000000000# How to sync OneDrive Business Shared Items > [!CAUTION] > Before reading this document, please ensure you are running application version [![Version](https://img.shields.io/github/v/release/abraunegg/onedrive)](https://github.com/abraunegg/onedrive/releases) or greater. Use `onedrive --version` to determine what application version you are using and upgrade your client if required. > [!CAUTION] > This feature has been 100% re-written from v2.5.0 onwards and is not backwards compatible with v2.4.x client versions. If enabling this feature, you must upgrade to v2.5.0 or above on all systems that are running this client. > > An additional pre-requisite before using this capability in v2.5.0 and above is for you to revert any v2.4.x Shared Business Folder configuration you may be currently using, including, but not limited to: > * Removing `sync_business_shared_folders = "true|false"` from your 'config' file > * Removing the 'business_shared_folders' file > * Removing any local data | shared folder data from your configured 'sync_dir' to ensure that there are no conflicts or issues. > * Removing any configuration online that might be related to using this feature prior to v2.5.0 ## Process Overview Syncing OneDrive Business Shared Folders requires additional configuration for your 'onedrive' client: 1. From the OneDrive web interface, review the 'Shared' objects that have been shared with you. 2. Select the applicable folder, and click the 'Add shortcut to My files', which will then add this to your 'My files' folder 3. Update your OneDrive Client for Linux 'config' file to enable the feature by adding `sync_business_shared_items = "true"`. Adding this option will trigger a `--resync` requirement. 4. Test the configuration using '--dry-run' 5. Remove the use of '--dry-run' and sync the OneDrive Business Shared folders as required ### Enable syncing of OneDrive Business Shared Items via config file ```text sync_business_shared_items = "true" ``` ### Disable syncing of OneDrive Business Shared Items via config file ```text sync_business_shared_items = "false" ``` ## Syncing OneDrive Business Shared Folders Use the following steps to add a OneDrive Business Shared Folder to your account: 1. Login to Microsoft OneDrive online, and navigate to 'Shared' from the left hand side pane ![objects_shared_with_me](./images/objects_shared_with_me.png) 2. Select the respective folder you wish to sync, and click the 'Add shortcut to My files' at the top of the page ![add_shared_folder](./images/add_shared_folder.png) 3. The final result online will look like this: ![shared_folder_added](./images/shared_folder_added.png) When using Microsoft Windows, this shared folder will appear as the following: ![windows_view_shared_folders](./images/windows_view_shared_folders.png) 4. Sync your data using `onedrive --sync --verbose`. If you have just enabled the `sync_business_shared_items = "true"` configuration option, you will be required to perform a resync. During the sync, the selected shared folder will be downloaded: ``` ... Processing API Response Bundle: 1 - Quantity of 'changes|items' in this bundle to process: 4 Finished processing /delta JSON response from the OneDrive API Processing 3 applicable changes and items received from Microsoft OneDrive Processing OneDrive JSON item batch [1/1] to ensure consistent local state Creating local directory: ./my_shared_folder Quota information is restricted or not available for this drive. Syncing this OneDrive Business Shared Folder: my_shared_folder Fetching /delta response from the OneDrive API for Drive ID: b!BhWyqa7K_kqXqHtSIlsqjR5iJogxpWxDradnpVGTU2VxBOJh82Y6S4he4rdnGPBT Processing API Response Bundle: 1 - Quantity of 'changes|items' in this bundle to process: 6 Finished processing /delta JSON response from the OneDrive API Processing 6 applicable changes and items received from Microsoft OneDrive Processing OneDrive JSON item batch [1/1] to ensure consistent local state Creating local directory: ./my_shared_folder/asdf Creating local directory: ./my_shared_folder/original_data Number of items to download from OneDrive: 3 Downloading file: my_shared_folder/my_folder/file_one.txt ... done Downloading file: my_shared_folder/my_folder/file_two.txt ... done Downloading file: my_shared_folder/original_data/file1.data ... done Performing a database consistency and integrity check on locally stored data ... ``` When this is viewed locally, on Linux, this shared folder is seen as the following: ![linux_shared_folder_view](./images/linux_shared_folder_view.png) Any shared folder you add can utilise any 'client side filtering' rules that you have created. ## Syncing OneDrive Business Shared Files There are two methods to support the syncing OneDrive Business Shared Files with the OneDrive Application 1. Add a 'shortcut' to your 'My Files' for the file, which creates a URL shortcut to the file which can be followed when using a Linux Window Manager (Gnome, KDE etc) and the link will open up in a browser. Microsoft Windows only supports this option. 2. Use `--sync-shared-files` option to sync all files shared with you to your local disk. If you use this method, you can utilise any 'client side filtering' rules that you have created to filter out files you do not want locally. This option will create a new folder locally, with sub-folders named after the person who shared the data with you. ### Syncing OneDrive Business Shared Files using Option 1 1. As per the above method for adding folders, select the shared file, then select to 'Add shortcut' to the file ![add_shared_file_shortcut](./images/add_shared_file_shortcut.png) 2. The final result online will look like this: ![add_shared_file_shortcut_added](./images/online_shared_file_link.png) When using Microsoft Windows, this shared file will appear as the following: ![windows_view_shared_file_link](./images/windows_view_shared_file_link.png) 3. Sync your data using `onedrive --sync --verbose`. If you have just enabled the `sync_business_shared_items = "true"` configuration option, you will be required to perform a resync. ``` ... All application operations will be performed in the configured local 'sync_dir' directory: /home/alex/OneDrive Fetching /delta response from the OneDrive API for Drive ID: b!bO8V7s9SSk6r7mWHpIjURotN33W1W2tEv3OXV_oFIdQimEdOHR-1So7CqeT1MfHA Processing API Response Bundle: 1 - Quantity of 'changes|items' in this bundle to process: 2 Finished processing /delta JSON response from the OneDrive API Processing 1 applicable changes and items received from Microsoft OneDrive Processing OneDrive JSON item batch [1/1] to ensure consistent local state Number of items to download from OneDrive: 1 Downloading file: ./file to share.docx.url ... done Syncing this OneDrive Business Shared Folder: my_shared_folder Fetching /delta response from the OneDrive API for Drive ID: b!BhWyqa7K_kqXqHtSIlsqjR5iJogxpWxDradnpVGTU2VxBOJh82Y6S4he4rdnGPBT Processing API Response Bundle: 1 - Quantity of 'changes|items' in this bundle to process: 0 Finished processing /delta JSON response from the OneDrive API No additional changes or items that can be applied were discovered while processing the data received from Microsoft OneDrive Quota information is restricted or not available for this drive. Performing a database consistency and integrity check on locally stored data Processing DB entries for this Drive ID: b!BhWyqa7K_kqXqHtSIlsqjR5iJogxpWxDradnpVGTU2VxBOJh82Y6S4he4rdnGPBT Quota information is restricted or not available for this drive. ... ``` When this is viewed locally, on Linux, this shared folder is seen as the following: ![linux_view_shared_file_link](./images/linux_view_shared_file_link.png) Any shared file link you add can utilise any 'client side filtering' rules that you have created. ### Syncing OneDrive Business Shared Files using Option 2 > [!IMPORTANT] > When using option 2, all files that have been shared with you will be downloaded by default. To reduce this, first use `--list-shared-items` to list all shared items with your account, then use 'client side filtering' rules such as 'sync_list' configuration to selectively sync all the files to your local system. 1. Review all items that have been shared with you by using `onedrive --list-shared-items`. This should display output similar to the following: ``` ... Listing available OneDrive Business Shared Items: ----------------------------------------------------------------------------------- Shared File: large_document_shared.docx Shared By: test user (testuser@mynasau3.onmicrosoft.com) ----------------------------------------------------------------------------------- Shared File: no_download_access.docx Shared By: test user (testuser@mynasau3.onmicrosoft.com) ----------------------------------------------------------------------------------- Shared File: online_access_only.txt Shared By: test user (testuser@mynasau3.onmicrosoft.com) ----------------------------------------------------------------------------------- Shared File: read_only.txt Shared By: test user (testuser@mynasau3.onmicrosoft.com) ----------------------------------------------------------------------------------- Shared File: qewrqwerwqer.txt Shared By: test user (testuser@mynasau3.onmicrosoft.com) ----------------------------------------------------------------------------------- Shared File: dummy_file_to_share.docx Shared By: testuser2 testuser2 (testuser2@mynasau3.onmicrosoft.com) ----------------------------------------------------------------------------------- Shared Folder: Sub Folder 2 Shared By: test user (testuser@mynasau3.onmicrosoft.com) ----------------------------------------------------------------------------------- Shared File: file to share.docx Shared By: test user (testuser@mynasau3.onmicrosoft.com) ----------------------------------------------------------------------------------- Shared Folder: Top Folder Shared By: test user (testuser@mynasau3.onmicrosoft.com) ----------------------------------------------------------------------------------- Shared Folder: my_shared_folder Shared By: testuser2 testuser2 (testuser2@mynasau3.onmicrosoft.com) ----------------------------------------------------------------------------------- Shared Folder: Jenkins Shared By: test user (testuser@mynasau3.onmicrosoft.com) ----------------------------------------------------------------------------------- ... ``` 2. If applicable, add entries to a 'sync_list' file, to only sync the shared files that are of importance to you. 3. Run the command `onedrive --sync --verbose --sync-shared-files` to sync the shared files to your local file system. This will create a new local folder called 'Files Shared With Me', and will contain sub-directories named after the entity account that has shared the file with you. In that folder will reside the shared file: ``` ... Finished processing /delta JSON response from the OneDrive API No additional changes or items that can be applied were discovered while processing the data received from Microsoft OneDrive Syncing this OneDrive Business Shared Folder: my_shared_folder Fetching /delta response from the OneDrive API for Drive ID: b!BhWyqa7K_kqXqHtSIlsqjR5iJogxpWxDradnpVGTU2VxBOJh82Y6S4he4rdnGPBT Processing API Response Bundle: 1 - Quantity of 'changes|items' in this bundle to process: 0 Finished processing /delta JSON response from the OneDrive API No additional changes or items that can be applied were discovered while processing the data received from Microsoft OneDrive Quota information is restricted or not available for this drive. Creating the OneDrive Business Shared Files Local Directory: /home/alex/OneDrive/Files Shared With Me Checking for any applicable OneDrive Business Shared Files which need to be synced locally Creating the OneDrive Business Shared File Users Local Directory: /home/alex/OneDrive/Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com) Creating the OneDrive Business Shared File Users Local Directory: /home/alex/OneDrive/Files Shared With Me/testuser2 testuser2 (testuser2@mynasau3.onmicrosoft.com) Number of items to download from OneDrive: 7 Downloading file: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/file to share.docx ... done OneDrive returned a 'HTTP 403 - Forbidden' - gracefully handling error Unable to download this file as this was shared as read-only without download permission: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/no_download_access.docx ERROR: File failed to download. Increase logging verbosity to determine why. Downloading file: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/no_download_access.docx ... failed! Downloading file: Files Shared With Me/testuser2 testuser2 (testuser2@mynasau3.onmicrosoft.com)/dummy_file_to_share.docx ... done Downloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 0% | ETA --:--:-- Downloading file: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/online_access_only.txt ... done Downloading file: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/read_only.txt ... done Downloading file: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/qewrqwerwqer.txt ... done Downloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 5% | ETA 00:00:00 Downloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 10% | ETA 00:00:00 Downloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 15% | ETA 00:00:00 Downloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 20% | ETA 00:00:00 Downloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 25% | ETA 00:00:00 Downloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 30% | ETA 00:00:00 Downloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 35% | ETA 00:00:00 Downloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 40% | ETA 00:00:00 Downloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 45% | ETA 00:00:00 Downloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 50% | ETA 00:00:00 Downloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 55% | ETA 00:00:00 Downloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 60% | ETA 00:00:00 Downloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 65% | ETA 00:00:00 Downloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 70% | ETA 00:00:00 Downloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 75% | ETA 00:00:00 Downloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 80% | ETA 00:00:00 Downloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 85% | ETA 00:00:00 Downloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 90% | ETA 00:00:00 Downloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 95% | ETA 00:00:00 Downloading: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... 100% | DONE in 00:00:00 Quota information is restricted or not available for this drive. Downloading file: Files Shared With Me/test user (testuser@mynasau3.onmicrosoft.com)/large_document_shared.docx ... done Quota information is restricted or not available for this drive. Quota information is restricted or not available for this drive. Performing a database consistency and integrity check on locally stored data Processing DB entries for this Drive ID: b!BhWyqa7K_kqXqHtSIlsqjR5iJogxpWxDradnpVGTU2VxBOJh82Y6S4he4rdnGPBT Quota information is restricted or not available for this drive. ... ``` When this is viewed locally, on Linux, this 'Files Shared With Me' and content is seen as the following: ![files_shared_with_me_folder](./images/files_shared_with_me_folder.png) Unfortunately there is no Microsoft Windows equivalent for this capability. ## Known Issues Shared folders, shared with you from people outside of your 'organisation' are unable to be synced. This is due to the Microsoft Graph API not presenting these folders. Shared folders that match this scenario, when you view 'Shared' via OneDrive online, will have a 'world' symbol as per below: ![shared_with_me](./images/shared_with_me.JPG) This issue is being tracked by: [#966](https://github.com/abraunegg/onedrive/issues/966) onedrive-2.5.5/docs/client-architecture.md000066400000000000000000000533101476564400300205610ustar00rootroot00000000000000# OneDrive Client for Linux Application Architecture ## How does the client work at a high level? The client utilises the 'libcurl' library to communicate with Microsoft OneDrive via the Microsoft Graph API. The diagram below shows this high level interaction with the Microsoft and GitHub API services online: ![client_use_of_libcurl](./puml/client_use_of_libcurl.png) Depending on your operational environment, it is possible to 'tweak' the following options which will modify how libcurl operates with it's interaction with Microsoft OneDrive services: * Downgrade all HTTPS operations to use HTTP1.1 (Config Option: `force_http_11`) * Control how long a specific transfer should take before it is considered too slow and aborted (Config Option: `operation_timeout`) * Control libcurl handling of DNS Cache Timeout (Config Option: `dns_timeout`) * Control the maximum time allowed for the connection to be established (Config Option: `connect_timeout`) * Control the timeout for activity on an established HTTPS connection (Config Option: `data_timeout`) * Control what IP protocol version should be used when communicating with OneDrive (Config Option: `ip_protocol_version`) * Control what User Agent is presented to Microsoft services (Config Option: `user_agent`) > [!IMPORTANT] > The default 'user_agent' value conforms to specific Microsoft requirements to identify as an ISV that complies with OneDrive traffic decoration requirements. Changing this value potentially will impact how Microsoft see's your client, thus your traffic may get throttled. For further information please read: https://learn.microsoft.com/en-us/sharepoint/dev/general-development/how-to-avoid-getting-throttled-or-blocked-in-sharepoint-online Diving a little deeper into how the client operates, the diagram below outlines at a high level the operational workflow of the OneDrive Client for Linux, demonstrating how it interacts with the OneDrive API to maintain synchronisation, manage local and cloud data integrity, and ensure that user data is accurately mirrored between the local filesystem and OneDrive cloud storage. ![High Level Application Sequence](./puml/high_level_operational_process.png) The application operational processes have several high level key stages: 1. **Access Token Validation:** Initially, the client validates its access and the existing access token, refreshing it if necessary. This step ensures that the client has the required permissions to interact with the OneDrive API. 2. **Query Microsoft OneDrive API:** The client queries the /delta API endpoint of Microsoft OneDrive, which returns JSON responses. The /delta endpoint is particularly used for syncing changes, helping the client to identify any updates in the OneDrive storage. 3. **Process JSON Responses:** The client processes each JSON response to determine if it represents a 'root' or 'deleted' item. Items not marked as 'root' or 'deleted' are temporarily stored for further processing. For 'root' or 'deleted' items, the client processes them immediately, otherwise, the client evaluates the items against client-side filtering rules to decide whether to discard them or to process and save them in the local database cache for actions like creating directories or downloading files. 4. **Local Cache Database Processing for Data Integrity:** The client processes its local cache database to check for data integrity and differences compared to the OneDrive storage. If differences are found, such as a file or folder change including deletions, the client uploads these changes to OneDrive. Responses from the API, including item metadata, are saved to the local cache database. 5. **Local Filesystem Scanning:** The client scans the local filesystem for new files or folders. Each new item is checked against client-side filtering rules. If an item passes the filtering, it is uploaded to OneDrive. Otherwise, it is discarded if it doesn't meet the filtering criteria. 6. **Final Data True-Up:** Lastly, the client queries the /delta link for a final true-up, processing any further online JSON changes if required. This ensures that the local and OneDrive storages are fully synchronised. ## What are the operational modes of the client? There are 2 main operational modes that the client can utilise: 1. Standalone sync mode that performs a single sync action against Microsoft OneDrive. This method is used when you utilise `--sync`. 2. Ongoing sync mode that continuously syncs your data with Microsoft OneDrive and utilises 'inotify' to watch for local system changes. This method is used when you utilise `--monitor`. By default, both modes consider all data stored online within Microsoft OneDrive as the 'source-of-truth' - that is, what is online, is the correct data (file version, file content, file timestamp, folder structure and so on). This consideration also matches how the Microsoft OneDrive Client for Windows operates. However, in standalone mode (`--sync`), you can *change* what reference the client will use as the 'source-of-truth' for your data by using the `--local-first` option so that the application will look at your local files *first* and consider your local files as your 'source-of-truth' to replicate that directory structure to Microsoft OneDrive. > [!IMPORTANT] > Please be aware that if you designate a network mount point (such as NFS, Windows Network Share, or Samba Network Share) as your `sync_dir`, this setup inherently lacks 'inotify' support. Support for 'inotify' is essential for real-time tracking of file changes, which means that the client's 'Monitor Mode' cannot immediately detect changes in files located on these network shares. Instead, synchronisation between your local filesystem and Microsoft OneDrive will occur at intervals specified by the `monitor_interval` setting. This limitation regarding 'inotify' support on network mount points like NFS or Samba is beyond the control of this client. ## OneDrive Client for Linux High Level Activity Flows The diagrams below show the high level process flow and decision making when running the application ### Main functional activity flows ![Main Activity](./puml/main_activity_flows.png) ### Processing a potentially new local item ![applyPotentiallyNewLocalItem](./puml/applyPotentiallyNewLocalItem.png) ### Processing a potentially changed local item ![applyPotentiallyChangedItem](./puml/applyPotentiallyChangedItem.png) ### Download a file from Microsoft OneDrive ![downloadFile](./puml/downloadFile.png) ### Upload a modified file to Microsoft OneDrive ![uploadModifiedFile](./puml/uploadModifiedFile.png) ### Upload a new local file to Microsoft OneDrive ![uploadFile](./puml/uploadFile.png) ### Determining if an 'item' is synchronised between Microsoft OneDrive and the local file system ![Item Sync Determination](./puml/is_item_in_sync.png) ### Determining if an 'item' is excluded due to 'Client Side Filtering' rules By default, the OneDrive Client for Linux will sync all files and folders between Microsoft OneDrive and the local filesystem. Client Side Filtering in the context of this client refers to user-configured rules that determine what files and directories the client should upload or download from Microsoft OneDrive. These rules are crucial for optimising synchronisation, especially when dealing with large numbers of files or specific file types. The OneDrive Client for Linux offers several configuration options to facilitate this: * **skip_dir:** This option allows the user to specify directories that should not be synchronised with OneDrive. It's particularly useful for omitting large or irrelevant directories from the sync process. * **skip_dotfiles:** Dotfiles, usually configuration files or scripts, can be excluded from the sync. This is useful for users who prefer to keep these files local. * **skip_file:** Specific files can be excluded from synchronisation using this option. It provides flexibility in selecting which files are essential for cloud storage. * **skip_symlinks:** Symlinks often point to files outside the OneDrive directory or to locations that are not relevant for cloud storage. This option prevents them from being included in the sync. This exclusion process can be illustrated by the following activity diagram. A 'true' return value means that the path being evaluated needs to be excluded: ![Client Side Filtering Determination](./puml/client_side_filtering_rules.png) ## File conflict handling - default operational modes When using the default operational modes (`--sync` or `--monitor`) the client application is conforming to how the Microsoft Windows OneDrive client operates in terms of resolving conflicts for files. When using `--resync` this conflict resolution can differ slightly, as, when using `--resync` you are *deleting* the known application state, thus, the application has zero reference as to what was previously in sync with the local file system. Due to this factor, when using `--resync` the online source is always going to be considered accurate and the source-of-truth, regardless of the local file state, local file timestamp or local file hash. When a difference in local file hash is detected, the file will be renamed to prevent local data loss. > [!IMPORTANT] > In v2.5.3 and above, when a local file is renamed due to conflict handling, this will be in the following format pattern to allow easier identification: > > **filename-hostname-safeBackup-number.file_extension** > > For example: > ``` > -rw-------. 1 alex alex 53402 Sep 21 08:25 file5.data > -rw-------. 1 alex alex 53423 Nov 13 18:18 file5-onedrive-client-dev-safeBackup-0001.data > -rw-------. 1 alex alex 53422 Nov 13 18:19 file5-onedrive-client-dev-safeBackup-0002.data > ``` > > In client versions v2.5.2 and below, the renamed file have the following naming convention: > > **filename-hostname-number.file_extension** > > resulting in backup filenames of the following format: > ``` > -rw-------. 1 alex alex 53402 Sep 21 08:25 file5.data > -rw-------. 1 alex alex 53432 Nov 14 05:22 file5-onedrive-client-dev-2.data > -rw-------. 1 alex alex 53435 Nov 14 05:24 file5-onedrive-client-dev-3.data > -rw-------. 1 alex alex 53419 Nov 14 05:22 file5-onedrive-client-dev.data > ``` > > [!CAUTION] > The creation of backup files when there is a conflict to avoid local data loss can be disabled. > > To do this, utilise the configuration option **bypass_data_preservation** > > If this is enabled, you will experience data loss on your local data as the local file will be over-written with data from OneDrive online. Use with care and caution. ### Default Operational Modes - Conflict Handling #### Scenario 1. Create a local file 2. Perform a sync with Microsoft OneDrive using `onedrive --sync` 3. Modify file online 4. Modify file locally with different data|contents 5. Perform a sync with Microsoft OneDrive using `onedrive --sync` ![conflict_handling_default](./puml/conflict_handling_default.png) #### Evidence of Conflict Handling ``` ... Processing API Response Bundle: 1 - Quantity of 'changes|items' in this bundle to process: 2 Finished processing /delta JSON response from the OneDrive API Processing 1 applicable changes and items received from Microsoft OneDrive Processing OneDrive JSON item batch [1/1] to ensure consistent local state Number of items to download from OneDrive: 1 The local file to replace (./1.txt) has been modified locally since the last download. Renaming it to avoid potential local data loss. The local item is out-of-sync with OneDrive, renaming to preserve existing file and prevent local data loss: ./1.txt -> ./1-onedrive-client-dev.txt Downloading file ./1.txt ... done Performing a database consistency and integrity check on locally stored data Processing DB entries for this Drive ID: b!bO8V7s9SSk6r7mWHpIjURotN33W1W2tEv3OXV_oFIdQimEdOHR-1So7CqeT1MfHA Processing ~/OneDrive The directory has not changed Processing α ... The file has not changed Processing เอกสาร The directory has not changed Processing 1.txt The file has not changed Scanning the local file system '~/OneDrive' for new data to upload ... New items to upload to OneDrive: 1 Total New Data to Upload: 52 Bytes Uploading new file ./1-onedrive-client-dev.txt ... done. Performing a last examination of the most recent online data within Microsoft OneDrive to complete the reconciliation process Fetching /delta response from the OneDrive API for Drive ID: b!bO8V7s9SSk6r7mWHpIjURotN33W1W2tEv3OXV_oFIdQimEdOHR-1So7CqeT1MfHA Processing API Response Bundle: 1 - Quantity of 'changes|items' in this bundle to process: 2 Finished processing /delta JSON response from the OneDrive API Processing 1 applicable changes and items received from Microsoft OneDrive Processing OneDrive JSON item batch [1/1] to ensure consistent local state Sync with Microsoft OneDrive is complete Waiting for all internal threads to complete before exiting application ``` ### Default Operational Modes - Conflict Handling with --resync #### Scenario 1. Create a local file 2. Perform a sync with Microsoft OneDrive using `onedrive --sync` 3. Modify file online 4. Modify file locally with different data|contents 5. Perform a sync with Microsoft OneDrive using `onedrive --sync --resync` ![conflict_handling_default_resync](./puml/conflict_handling_default_resync.png) #### Evidence of Conflict Handling ``` ... Deleting the saved application sync status ... Using IPv4 and IPv6 (if configured) for all network operations Checking Application Version ... ... Processing API Response Bundle: 1 - Quantity of 'changes|items' in this bundle to process: 14 Finished processing /delta JSON response from the OneDrive API Processing 13 applicable changes and items received from Microsoft OneDrive Processing OneDrive JSON item batch [1/1] to ensure consistent local state Local file time discrepancy detected: ./1.txt This local file has a different modified time 2024-Feb-19 19:32:55Z (UTC) when compared to remote modified time 2024-Feb-19 19:32:36Z (UTC) The local file has a different hash when compared to remote file hash Local item does not exist in local database - replacing with file from OneDrive - failed download? The local item is out-of-sync with OneDrive, renaming to preserve existing file and prevent local data loss: ./1.txt -> ./1-onedrive-client-dev.txt Number of items to download from OneDrive: 1 Downloading file ./1.txt ... done Performing a database consistency and integrity check on locally stored data Processing DB entries for this Drive ID: b!bO8V7s9SSk6r7mWHpIjURotN33W1W2tEv3OXV_oFIdQimEdOHR-1So7CqeT1MfHA Processing ~/OneDrive The directory has not changed Processing α ... Processing เอกสาร The directory has not changed Processing 1.txt The file has not changed Scanning the local file system '~/OneDrive' for new data to upload ... New items to upload to OneDrive: 1 Total New Data to Upload: 52 Bytes Uploading new file ./1-onedrive-client-dev.txt ... done. Performing a last examination of the most recent online data within Microsoft OneDrive to complete the reconciliation process Fetching /delta response from the OneDrive API for Drive ID: b!bO8V7s9SSk6r7mWHpIjURotN33W1W2tEv3OXV_oFIdQimEdOHR-1So7CqeT1MfHA Processing API Response Bundle: 1 - Quantity of 'changes|items' in this bundle to process: 2 Finished processing /delta JSON response from the OneDrive API Processing 1 applicable changes and items received from Microsoft OneDrive Processing OneDrive JSON item batch [1/1] to ensure consistent local state Sync with Microsoft OneDrive is complete Waiting for all internal threads to complete before exiting application ``` ## File conflict handling - local-first operational mode When using `--local-first` as your operational parameter the client application is now using your local filesystem data as the 'source-of-truth' as to what should be stored online. However - Microsoft OneDrive itself, has *zero* acknowledgement of this concept, thus, conflict handling needs to be aligned to how Microsoft OneDrive on other platforms operate, that is, rename the local offending file. Additionally, when using `--resync` you are *deleting* the known application state, thus, the application has zero reference as to what was previously in sync with the local file system. Due to this factor, when using `--resync` the online source is always going to be considered accurate and the source-of-truth, regardless of the local file state, file timestamp or file hash or use of `--local-first`. ### Local First Operational Modes - Conflict Handling #### Scenario 1. Create a local file 2. Perform a sync with Microsoft OneDrive using `onedrive --sync --local-first` 3. Modify file locally with different data|contents 4. Modify file online with different data|contents 5. Perform a sync with Microsoft OneDrive using `onedrive --sync --local-first` ![conflict_handling_local-first_default](./puml/conflict_handling_local-first_default.png) #### Evidence of Conflict Handling ``` Reading configuration file: /home/alex/.config/onedrive/config ... Using IPv4 and IPv6 (if configured) for all network operations Checking Application Version ... ... Sync Engine Initialised with new Onedrive API instance All application operations will be performed in the configured local 'sync_dir' directory: /home/alex/OneDrive Performing a database consistency and integrity check on locally stored data Processing DB entries for this Drive ID: b!bO8V7s9SSk6r7mWHpIjURotN33W1W2tEv3OXV_oFIdQimEdOHR-1So7CqeT1MfHA Processing ~/OneDrive The directory has not changed Processing α The directory has not changed ... The file has not changed Processing เอกสาร The directory has not changed Processing 1.txt Local file time discrepancy detected: 1.txt The file content has changed locally and has a newer timestamp, thus needs to be uploaded to OneDrive Changed local items to upload to OneDrive: 1 The local item is out-of-sync with OneDrive, renaming to preserve existing file and prevent local data loss: 1.txt -> 1-onedrive-client-dev.txt Uploading new file 1-onedrive-client-dev.txt ... done. Scanning the local file system '~/OneDrive' for new data to upload ... Fetching /delta response from the OneDrive API for Drive ID: b!bO8V7s9SSk6r7mWHpIjURotN33W1W2tEv3OXV_oFIdQimEdOHR-1So7CqeT1MfHA Processing API Response Bundle: 1 - Quantity of 'changes|items' in this bundle to process: 3 Finished processing /delta JSON response from the OneDrive API Processing 2 applicable changes and items received from Microsoft OneDrive Processing OneDrive JSON item batch [1/1] to ensure consistent local state Number of items to download from OneDrive: 1 Downloading file ./1.txt ... done Sync with Microsoft OneDrive is complete Waiting for all internal threads to complete before exiting application ``` ### Local First Operational Modes - Conflict Handling with --resync #### Scenario 1. Create a local file 2. Perform a sync with Microsoft OneDrive using `onedrive --sync --local-first` 3. Modify file locally with different data|contents 4. Modify file online with different data|contents 5. Perform a sync with Microsoft OneDrive using `onedrive --sync --local-first --resync` ![conflict_handling_local-first_resync](./puml/conflict_handling_local-first_resync.png) #### Evidence of Conflict Handling ``` ... The usage of --resync will delete your local 'onedrive' client state, thus no record of your current 'sync status' will exist. This has the potential to overwrite local versions of files with perhaps older versions of documents downloaded from OneDrive, resulting in local data loss. If in doubt, backup your local data before using --resync Are you sure you wish to proceed with --resync? [Y/N] y Deleting the saved application sync status ... Using IPv4 and IPv6 (if configured) for all network operations ... Sync Engine Initialised with new Onedrive API instance All application operations will be performed in the configured local 'sync_dir' directory: /home/alex/OneDrive Performing a database consistency and integrity check on locally stored data Processing DB entries for this Drive ID: b!bO8V7s9SSk6r7mWHpIjURotN33W1W2tEv3OXV_oFIdQimEdOHR-1So7CqeT1MfHA Processing ~/OneDrive The directory has not changed Scanning the local file system '~/OneDrive' for new data to upload Skipping item - excluded by sync_list config: ./random_25k_files OneDrive Client requested to create this directory online: ./α The requested directory to create was found on OneDrive - skipping creating the directory: ./α ... New items to upload to OneDrive: 9 Total New Data to Upload: 49 KB ... The file we are attempting to upload as a new file already exists on Microsoft OneDrive: ./1.txt Skipping uploading this item as a new file, will upload as a modified file (online file already exists): ./1.txt The local item is out-of-sync with OneDrive, renaming to preserve existing file and prevent local data loss: ./1.txt -> ./1-onedrive-client-dev.txt Uploading new file ./1-onedrive-client-dev.txt ... done. Fetching /delta response from the OneDrive API for Drive ID: b!bO8V7s9SSk6r7mWHpIjURotN33W1W2tEv3OXV_oFIdQimEdOHR-1So7CqeT1MfHA Processing API Response Bundle: 1 - Quantity of 'changes|items' in this bundle to process: 15 Finished processing /delta JSON response from the OneDrive API Processing 14 applicable changes and items received from Microsoft OneDrive Processing OneDrive JSON item batch [1/1] to ensure consistent local state Number of items to download from OneDrive: 1 Downloading file ./1.txt ... done Sync with Microsoft OneDrive is complete Waiting for all internal threads to complete before exiting application ``` ## Client Functional Component Architecture Relationships The diagram below shows the main functional relationship of application code components, and how these relate to each relevant code module within this application: ![Functional Code Components](./puml/code_functional_component_relationships.png) ## Database Schema The diagram below shows the database schema that is used within the application ![Database Schema](./puml/database_schema.png) onedrive-2.5.5/docs/contributing.md000066400000000000000000000155361476564400300173420ustar00rootroot00000000000000# OneDrive Client for Linux: Coding Style Guidelines ## Introduction This document outlines the coding style guidelines for code contributions for the OneDrive Client for Linux. These guidelines are intended to ensure the codebase remains clean, well-organised, and accessible to all contributors, new and experienced alike. ## Code Layout > [!NOTE] > When developing any code contribution, please utilise either Microsoft Visual Studio Code or Notepad++. ### Indentation Most of the codebase utilises tabs for space indentation, with 4 spaces to a tab. Please keep to this convention. ### Line Length Try and keep line lengths to a reasonable length. Do not constrain yourself to short line lengths such as 80 characters. This means when the code is being displayed in the code editor, lines are correctly displayed when using screen resolutions of 1920x1080 and above. If you wish to use shorter line lengths (80 characters for example), please do not follow this sort of example: ```code ... void functionName( string somevar, bool someOtherVar, cost(char) anotherVar=null ){ .... ``` ### Coding Style | Braces Please use 1TBS (One True Brace Style) which is a variation of the K&R (Kernighan & Ritchie) style. This approach is intended to improve readability and maintain consistency throughout the code. When using this coding style, even when the code of the `if`, `else`, `for`, or function definition contains only one statement, braces are used to enclose it. ```code // What this if statement is doing if (condition) { // The condition was true ..... } else { // The condition was false ..... } // Loop 10 times to do something for (int i = 0; i < 10; i++) { // Loop body } // This function is to do this void functionExample() { // Function body } ``` ## Naming Conventions ### Variables and Functions Please use `camelCase` for variable and function names. ### Classes and Interfaces Please use `PascalCase` for classes, interfaces, and structs. ### Constants Use uppercase with underscores between words. ## Documentation ### Language and Spelling To maintain consistency across the project's documentation, comments, and code, all written text must adhere to British English spelling conventions, not American English. This requirement applies to all aspects of the codebase, including variable names, comments, and documentation. For example, use "specialise" instead of "specialize", "colour" instead of "color", and "organise" instead of "organize". This standard ensures that the project maintains a cohesive and consistent linguistic style. ### Code Comments Please comment code at all levels. Use `//` for all line comments. Detail why a statement is needed, or what is expected to happen so future readers or contributors can read through the intent of the code with clarity. If fixing a 'bug', please add a link to the GitHub issue being addressed as a comment, for example: ```code ... // Before discarding change - does this ID still exist on OneDrive - as in IS this // potentially a --single-directory sync and the user 'moved' the file out of the 'sync-dir' to another OneDrive folder // This is a corner edge case - https://github.com/skilion/onedrive/issues/341 // What is the original local path for this ID in the database? Does it match 'syncFolderChildPath' if (itemdb.idInLocalDatabase(driveId, item["id"].str)){ // item is in the database string originalLocalPath = computeItemPath(driveId, item["id"].str); ... ``` All code should be clearly commented. ### Application Logging Output If making changes to any application logging output, please first discuss this either via direct communication or email. For reference, below are the available application logging output functions and examples: ```code // most used addLogEntry("Basic 'info' message", ["info"]); .... or just use addLogEntry("Basic 'info' message"); addLogEntry("Basic 'verbose' message", ["verbose"]); addLogEntry("Basic 'debug' message", ["debug"]); // GUI notify only addLogEntry("Basic 'notify' ONLY message and displayed in GUI if notifications are enabled", ["notify"]); // info and notify addLogEntry("Basic 'info and notify' message and displayed in GUI if notifications are enabled", ["info", "notify"]); // log file only addLogEntry("Information sent to the log file only, and only if logging to a file is enabled", ["logFileOnly"]); // Console only (session based upload|download) addLogEntry("Basic 'Console only with new line' message", ["consoleOnly"]); // Console only with no new line addLogEntry("Basic 'Console only with no new line' message", ["consoleOnlyNoNewLine"]); ``` ### Documentation Updates If the code changes any of the functionality that is documented, it is expected that any PR submission will also include updating the respective section of user documentation and/or man page as part of the code submission. ## Development Testing Whilst there are more modern DMD and LDC compilers available, ensuring client build compatibility with older platforms is a key requirement. The issue stems from Debian and Ubuntu LTS versions - such as Ubuntu 20.04. It's [ldc package](https://packages.ubuntu.com/focal/ldc) is only v1.20.1 , thus, this is the minimum version that all compilation needs to be tested against. The reason LDC v1.20.1 must be used, is that this is the version that is used to compile the packages presented at [OpenSuSE Build Service ](https://build.opensuse.org/package/show/home:npreining:debian-ubuntu-onedrive/onedrive) - which is where most Debian and Ubuntu users will install the client from. It is assumed here that you know how to download and install the correct LDC compiler for your platform. ## Submitting a PR When submitting a PR, please provide your testing evidence in the PR submission of what has been fixed, in the format of: ### Without PR ``` Application output that is doing whatever | or illustration of issue | illustration of bug ``` ### With PR ``` Application output that is doing whatever | or illustration of issue being fixed | illustration of bug being fixed ``` Please also include validation of compilation using the minimum LDC package version. To assist with your testing validation against the minimum LDC compiler version, a script as per below could assist you with this validation: ```bash #!/bin/bash PR= rm -rf ./onedrive-pr${PR} git clone https://github.com/abraunegg/onedrive.git onedrive-pr${PR} cd onedrive-pr${PR} git fetch origin pull/${PR}/head:pr${PR} git checkout pr${PR} # MIN LDC Version to compile # MIN Version for ARM / Compiling with LDC source ~/dlang/ldc-1.20.1/activate # Compile code with specific LDC version ./configure --enable-debug --enable-notifications; make clean; make; deactivate ./onedrive --version ``` ## References * D Language Official Style Guide: https://dlang.org/dstyle.html * British English spelling conventions: https://www.collinsdictionary.com/onedrive-2.5.5/docs/docker.md000066400000000000000000000520151476564400300160730ustar00rootroot00000000000000# Run the OneDrive Client for Linux under Docker This client can be run as a Docker container, with 3 available container base options for you to choose from: | Container Base | Docker Tag | Description | i686 | x86_64 | ARMHF | AARCH64 | |----------------|-------------|----------------------------------------------------------------|:------:|:------:|:-----:|:-------:| | Alpine Linux | edge-alpine | Docker container based on Alpine 3.20 using 'master' |❌|✔|❌|✔| | Alpine Linux | alpine | Docker container based on Alpine 3.20 using latest release |❌|✔|❌|✔| | Debian | debian | Docker container based on Debian Stable using latest release |✔|✔|✔|✔| | Debian | edge | Docker container based on Debian Stable using 'master' |✔|✔|✔|✔| | Debian | edge-debian | Docker container based on Debian Stable using 'master' |✔|✔|✔|✔| | Debian | latest | Docker container based on Debian Stable using latest release |✔|✔|✔|✔| | Fedora | edge-fedora | Docker container based on Fedora 40 using 'master' |❌|✔|❌|✔| | Fedora | fedora | Docker container based on Fedora 40 using latest release |❌|✔|❌|✔| These containers offer a simple monitoring-mode service for the OneDrive Client for Linux. The instructions below have been validated on: * Fedora 40 The instructions below will utilise the 'edge' tag, however this can be substituted for any of the other docker tags such as 'latest' from the table above if desired. The 'edge' Docker Container will align closer to all documentation and features, where as 'latest' is the release version from a static point in time. The 'latest' tag however may contain bugs and/or issues that will have been fixed, and those fixes are contained in 'edge'. Additionally there are specific version release tags for each release. Refer to https://hub.docker.com/r/driveone/onedrive/tags for any other Docker tags you may be interested in. > [!NOTE] > The below instructions for docker has been tested and validated when logging into the system as an unprivileged user (non 'root' user). ## High Level Configuration Steps 1. Install 'docker' as per your distribution platform's instructions if not already installed. 2. Configure 'docker' to allow non-privileged users to run Docker commands 3. Disable 'SELinux' as per your distribution platform's instructions 4. Test 'docker' by running a test container without using `sudo` 5. Prepare the required docker volumes to store the configuration and data 6. Run the 'onedrive' container and perform authorisation 7. Running the 'onedrive' container under 'docker' ## Configuration Steps ### 1. Install 'docker' on your platform Install Docker for your system using the official instructions found at https://docs.docker.com/engine/install/. > [!CAUTION] > If you are using Ubuntu or any distribution based on Ubuntu, do not install Docker from your distribution's repositories, as they may contain obsolete versions. Instead, you must install Docker using the packages provided directly by Docker. ### 2. Configure 'docker' to allow non-privileged users to run Docker commands Read https://docs.docker.com/engine/install/linux-postinstall/ to configure the 'docker' user group with your user account to allow your non 'root' user to run 'docker' commands. ### 3. Disable SELinux on your platform In order to run the Docker container, SELinux must be disabled. Without doing this, when the application is authenticated in the steps below, the following error will be presented: ```text ERROR: The local file system returned an error with the following message: Error Message: /onedrive/conf/refresh_token: Permission denied The database cannot be opened. Please check the permissions of ~/.config/onedrive/items.sqlite3 ``` The only known work-around for the above problem at present is to disable SELinux. Please refer to your distribution platform's instructions on how to perform this step. * Fedora: https://docs.fedoraproject.org/en-US/quick-docs/selinux-changing-states-and-modes/#_disabling_selinux * Red Hat Enterprise Linux: https://access.redhat.com/solutions/3176 Post disabling SELinux and reboot your system, confirm that `getenforce` returns `Disabled`: ```text $ getenforce Disabled ``` If you are still experiencing permission issues despite disabling SELinux, please read https://www.redhat.com/sysadmin/container-permission-denied-errors ### 4. Test 'docker' on your platform Ensure that 'docker' is running as a system service, and is enabled to be activated on system reboot: ```bash sudo systemctl enable --now docker ``` Test that 'docker' is operational for your 'non-root' user, as per below: ```bash [alex@fedora-40-docker-host ~]$ docker run hello-world Unable to find image 'hello-world:latest' locally latest: Pulling from library/hello-world 719385e32844: Pull complete Digest: sha256:88ec0acaa3ec199d3b7eaf73588f4518c25f9d34f58ce9a0df68429c5af48e8d Status: Downloaded newer image for hello-world:latest Hello from Docker! This message shows that your installation appears to be working correctly. To generate this message, Docker took the following steps: 1. The Docker client contacted the Docker daemon. 2. The Docker daemon pulled the "hello-world" image from the Docker Hub. (amd64) 3. The Docker daemon created a new container from that image which runs the executable that produces the output you are currently reading. 4. The Docker daemon streamed that output to the Docker client, which sent it to your terminal. To try something more ambitious, you can run an Ubuntu container with: $ docker run -it ubuntu bash Share images, automate workflows, and more with a free Docker ID: https://hub.docker.com/ For more examples and ideas, visit: https://docs.docker.com/get-started/ [alex@fedora-40-docker-host ~]$ ``` ### 5. Configure the required docker volumes The 'onedrive' Docker container requires 2 docker volumes to operate: * Config Volume * Data Volume The first volume is the configuration volume that stores all the applicable application configuration + current runtime state. In a non-containerised environment, this normally resides in `~/.config/onedrive` - in a containerised environment this is stored in the volume tagged as `/onedrive/conf` The second volume is the data volume, where all your data from Microsoft OneDrive is stored locally. This volume is mapped to an actual directory point on your local filesystem and this is stored in the volume tagged as `/onedrive/data` #### 5.1 Prepare the 'config' volume Create the 'config' volume with the following command: ```bash docker volume create onedrive_conf ``` This will create a docker volume labeled `onedrive_conf`, where all configuration of your onedrive account will be stored. You can add a custom config file in this location at a later point in time if required. #### 5.2 Prepare the 'data' volume Create the 'data' volume with the following command: ```bash docker volume create onedrive_data ``` This will create a docker volume labeled `onedrive_data` and will map to a path on your local filesystem. This is where your data from Microsoft OneDrive will be stored. Keep in mind that: * The owner of this specified folder must not be root * The owner of this specified folder must have permissions for its parent directory * Docker will attempt to change the permissions of the volume to the user the container is configured to run as > [!IMPORTANT] > Issues occur when this target folder is a mounted folder of an external system (NAS, SMB mount, USB Drive etc) as the 'mount' itself is owed by 'root'. If this is your use case, you *must* ensure your normal user can mount your desired target without having the target mounted by 'root'. If you do not fix this, your Docker container will fail to start with the following error message: > ```bash > ROOT level privileges prohibited! > ``` ### 6. First run of Docker container under docker and performing authorisation The 'onedrive' client within the container first needs to be authorised with your Microsoft account. This is achieved by initially running docker in interactive mode. Run the docker image with the commands below and make sure to change the value of `ONEDRIVE_DATA_DIR` to the actual onedrive data directory on your filesystem that you wish to use (e.g. `export ONEDRIVE_DATA_DIR="/home/abraunegg/OneDrive"`). > [!IMPORTANT] > The 'target' folder of `ONEDRIVE_DATA_DIR` must exist before running the docker container. The script below will create 'ONEDRIVE_DATA_DIR' so that it exists locally for the docker volume mapping to occur. It is also a requirement that the container be run using a non-root uid and gid, you must insert a non-root UID and GID (e.g.` export ONEDRIVE_UID=1000` and export `ONEDRIVE_GID=1000`). The script below will use `id` to evaluate your system environment to use the correct values. ```bash export ONEDRIVE_DATA_DIR="${HOME}/OneDrive" export ONEDRIVE_UID=`id -u` export ONEDRIVE_GID=`id -g` mkdir -p ${ONEDRIVE_DATA_DIR} docker run -it --name onedrive -v onedrive_conf:/onedrive/conf \ -v "${ONEDRIVE_DATA_DIR}:/onedrive/data" \ -e "ONEDRIVE_UID=${ONEDRIVE_UID}" \ -e "ONEDRIVE_GID=${ONEDRIVE_GID}" \ driveone/onedrive:edge ``` When the Docker container successfully starts: * You will be asked to open a specific link using your web browser * Login to your Microsoft Account and give the application the permission * After giving the permission, you will be redirected to a blank page * Copy the URI of the blank page into the application prompt to authorise the application Once the 'onedrive' application is authorised, the client will automatically start monitoring your `ONEDRIVE_DATA_DIR` for data changes to be uploaded to OneDrive. Files stored on OneDrive will be downloaded to this location. If the client is working as expected, you can detach from the container with Ctrl+p, Ctrl+q. ### 7. Running the 'onedrive' container under 'docker' #### 7.1 Check if the monitor service is running ```bash docker ps -f name=onedrive ``` #### 7.2 Show 'onedrive' runtime logs ```bash docker logs onedrive ``` #### 7.3 Stop running 'onedrive' container ```bash docker stop onedrive ``` #### 7.4 Start 'onedrive' container ```bash docker start onedrive ``` #### 7.5 Remove 'onedrive' container ```bash docker rm -f onedrive ``` ## Advanced Usage ### How to use Docker-compose You can utilise `docker-compose` if available on your platform if you are able to use docker compose schemas > 3. In the following example it is assumed you have a `ONEDRIVE_DATA_DIR` environment variable and have already created the `onedrive_conf` volume. You can also use docker bind mounts for the configuration folder, e.g. `export ONEDRIVE_CONF="${HOME}/OneDriveConfig"`. ``` version: "3" services: onedrive: image: driveone/onedrive:edge restart: unless-stopped environment: - ONEDRIVE_UID=${PUID} - ONEDRIVE_GID=${PGID} volumes: - onedrive_conf:/onedrive/conf - ${ONEDRIVE_DATA_DIR}:/onedrive/data ``` > [!IMPORTANT] > Before you run the container using your compose file you must first authenticate the client following [step 6](https://github.com/abraunegg/onedrive/blob/master/docs/docker.md#6-first-run-of-docker-container-under-docker-and-performing-authorisation) above. > Failure to perform this step before running your container using your compose file will see your container detail that an invalid response uri was entered. ### Editing the running configuration and using a 'config' file The 'onedrive' client should run in default configuration, however you can change this default configuration by placing a custom config file in the `onedrive_conf` docker volume. First download the default config from [here](https://raw.githubusercontent.com/abraunegg/onedrive/master/config) Then put it into your onedrive_conf volume path, which can be found with: ```bash docker volume inspect onedrive_conf ``` Or you can map your own config folder to the config volume. Make sure to copy all files from the docker volume into your mapped folder first. The detailed document for the config can be found here: [Application Configuration Options for the OneDrive Client for Linux](https://github.com/abraunegg/onedrive/blob/master/docs/application-config-options.md) ### Syncing multiple accounts There are many ways to do this, the easiest is probably to do the following: 1. Create a second docker config volume (replace `Work` with your desired name): `docker volume create onedrive_conf_Work` 2. And start a second docker monitor container (again replace `Work` with your desired name): ``` export ONEDRIVE_DATA_DIR_WORK="/home/abraunegg/OneDriveWork" mkdir -p ${ONEDRIVE_DATA_DIR_WORK} docker run -it --restart unless-stopped --name onedrive_Work -v onedrive_conf_Work:/onedrive/conf -v "${ONEDRIVE_DATA_DIR_WORK}:/onedrive/data" driveone/onedrive:edge ``` ### Run or update the Docker container with one script If you are experienced with docker and onedrive, you can use the following script: ```bash # Update ONEDRIVE_DATA_DIR with correct OneDrive directory path ONEDRIVE_DATA_DIR="${HOME}/OneDrive" # Create directory if non-existent mkdir -p ${ONEDRIVE_DATA_DIR} firstRun='-d' docker pull driveone/onedrive:edge docker inspect onedrive_conf > /dev/null 2>&1 || { docker volume create onedrive_conf; firstRun='-it'; } docker inspect onedrive > /dev/null 2>&1 && docker rm -f onedrive docker run $firstRun --restart unless-stopped --name onedrive -v onedrive_conf:/onedrive/conf -v "${ONEDRIVE_DATA_DIR}:/onedrive/data" driveone/onedrive:edge ``` ## Supported Docker Environment Variables | Variable | Purpose | Sample Value | | ---------------- | --------------------------------------------------- |:--------------------------------------------------------------------------------------------------------------------------------:| | ONEDRIVE_UID | UserID (UID) to run as | 1000 | | ONEDRIVE_GID | GroupID (GID) to run as | 1000 | | ONEDRIVE_VERBOSE | Controls "--verbose" switch on onedrive sync. Default is 0 | 1 | | ONEDRIVE_DEBUG | Controls "--verbose --verbose" switch on onedrive sync. Default is 0 | 1 | | ONEDRIVE_DEBUG_HTTPS | Controls "--debug-https" switch on onedrive sync. Default is 0 | 1 | | ONEDRIVE_RESYNC | Controls "--resync" switch on onedrive sync. Default is 0 | 1 | | ONEDRIVE_DOWNLOADONLY | Controls "--download-only" switch on onedrive sync. Default is 0 | 1 | | ONEDRIVE_CLEANUPLOCAL | Controls "--cleanup-local-files" to cleanup local files and folders if they are removed online. Default is 0 | 1 | | ONEDRIVE_UPLOADONLY | Controls "--upload-only" switch on onedrive sync. Default is 0 | 1 | | ONEDRIVE_NOREMOTEDELETE | Controls "--no-remote-delete" switch on onedrive sync. Default is 0 | 1 | | ONEDRIVE_LOGOUT | Controls "--logout" switch. Default is 0 | 1 | | ONEDRIVE_REAUTH | Controls "--reauth" switch. Default is 0 | 1 | | ONEDRIVE_AUTHFILES | Controls "--auth-files" option. Default is "" | Please read [CLI Option: --auth-files](./application-config-options.md#cli-option---auth-files) | | ONEDRIVE_AUTHRESPONSE | Controls "--auth-response" option. Default is "" | Please read [CLI Option: --auth-response](./application-config-options.md#cli-option---auth-response) | | ONEDRIVE_DISPLAY_CONFIG | Controls "--display-running-config" switch on onedrive sync. Default is 0 | 1 | | ONEDRIVE_SINGLE_DIRECTORY | Controls "--single-directory" option. Default = "" | "mydir" | | ONEDRIVE_DRYRUN | Controls "--dry-run" option. Default is 0 | 1 | | ONEDRIVE_DISABLE_DOWNLOAD_VALIDATION | Controls "--disable-download-validation" option. Default is 0 | 1 | | ONEDRIVE_DISABLE_UPLOAD_VALIDATION | Controls "--disable-upload-validation" option. Default is 0 | 1 | | ONEDRIVE_SYNC_SHARED_FILES | Controls "--sync-shared-files" option. Default is 0 | 1 | | ONEDRIVE_RUNAS_ROOT | Controls if the Docker container should be run as the 'root' user instead of 'onedrive' user. Default is 0 | 1 | | ONEDRIVE_SYNC_ONCE | Controls if the Docker container should be run in Standalone Mode. It will use Monitor Mode otherwise. Default is 0 | 1 | ### Environment Variables Usage Examples **Verbose Output:** ```bash docker container run -e ONEDRIVE_VERBOSE=1 -v onedrive_conf:/onedrive/conf -v "${ONEDRIVE_DATA_DIR}:/onedrive/data" driveone/onedrive:edge ``` **Debug Output:** ```bash docker container run -e ONEDRIVE_DEBUG=1 -v onedrive_conf:/onedrive/conf -v "${ONEDRIVE_DATA_DIR}:/onedrive/data" driveone/onedrive:edge ``` **Perform a --resync:** ```bash docker container run -e ONEDRIVE_RESYNC=1 -v onedrive_conf:/onedrive/conf -v "${ONEDRIVE_DATA_DIR}:/onedrive/data" driveone/onedrive:edge ``` **Perform a --resync and --verbose:** ```bash docker container run -e ONEDRIVE_RESYNC=1 -e ONEDRIVE_VERBOSE=1 -v onedrive_conf:/onedrive/conf -v "${ONEDRIVE_DATA_DIR}:/onedrive/data" driveone/onedrive:edge ``` **Perform a --logout:** ```bash docker container run -it -e ONEDRIVE_LOGOUT=1 -v onedrive_conf:/onedrive/conf -v "${ONEDRIVE_DATA_DIR}:/onedrive/data" driveone/onedrive:edge ``` **Perform a --logout and re-authenticate:** ```bash docker container run -it -e ONEDRIVE_REAUTH=1 -v onedrive_conf:/onedrive/conf -v "${ONEDRIVE_DATA_DIR}:/onedrive/data" driveone/onedrive:edge ``` ## Building a custom Docker image ### Build Environment Requirements * Build environment must have at least 1GB of memory & 2GB swap space You can validate your build environment memory status with the following command: ```text cat /proc/meminfo | grep -E 'MemFree|Swap' ``` This should result in the following similar output: ```text MemFree: 3704644 kB SwapCached: 0 kB SwapTotal: 8117244 kB SwapFree: 8117244 kB ``` If you do not have enough swap space, you can use the following script to dynamically allocate a swapfile for building the Docker container: ```bash cd /var sudo fallocate -l 1.5G swapfile sudo chmod 600 swapfile sudo mkswap swapfile sudo swapon swapfile # make swap permanent sudo nano /etc/fstab # add "/swapfile swap swap defaults 0 0" at the end of file # check it has been assigned swapon -s free -h ``` If you are running a Raspberry Pi, you will need to edit your system configuration to increase your swapfile: * Modify the file `/etc/dphys-swapfile` and edit the `CONF_SWAPSIZE`, for example: `CONF_SWAPSIZE=2048`. > [!IMPORTANT] > A reboot of your Raspberry Pi is required to make this change effective. ### Building and running a custom Docker image You can also build your own image instead of pulling the one from [hub.docker.com](https://hub.docker.com/r/driveone/onedrive): ```bash git clone https://github.com/abraunegg/onedrive cd onedrive docker build . -t local-onedrive -f contrib/docker/Dockerfile docker container run -v onedrive_conf:/onedrive/conf -v "${ONEDRIVE_DATA_DIR}:/onedrive/data" local-onedrive:latest ``` There are alternate, smaller images available by using `Dockerfile-debian` or `Dockerfile-alpine`. These [multi-stage builder pattern](https://docs.docker.com/develop/develop-images/multistage-build/) Dockerfiles require Docker version at least 17.05. ### How to build and run a custom Docker image based on Debian ``` bash docker build . -t local-onedrive-debian -f contrib/docker/Dockerfile-debian docker container run -v onedrive_conf:/onedrive/conf -v "${ONEDRIVE_DATA_DIR}:/onedrive/data" local-onedrive-debian:latest ``` ### How to build and run a custom Docker image based on Alpine Linux ``` bash docker build . -t local-onedrive-alpine -f contrib/docker/Dockerfile-alpine docker container run -v onedrive_conf:/onedrive/conf -v "${ONEDRIVE_DATA_DIR}:/onedrive/data" local-onedrive-alpine:latest ``` ### How to build and run a custom Docker image for ARMHF (Raspberry Pi) Compatible with: * Raspberry Pi * Raspberry Pi 2 * Raspberry Pi Zero * Raspberry Pi 3 * Raspberry Pi 4 ``` bash docker build . -t local-onedrive-armhf -f contrib/docker/Dockerfile-debian docker container run -v onedrive_conf:/onedrive/conf -v "${ONEDRIVE_DATA_DIR}:/onedrive/data" local-onedrive-armhf:latest ``` ### How to build and run a custom Docker image for AARCH64 Platforms ``` bash docker build . -t local-onedrive-aarch64 -f contrib/docker/Dockerfile-debian docker container run -v onedrive_conf:/onedrive/conf -v "${ONEDRIVE_DATA_DIR}:/onedrive/data" local-onedrive-aarch64:latest ``` ### How to support double-byte languages In some geographic regions, you may need to change and/or update the locale specification of the Docker container to better support the local language used for your local filesystem. To do this, follow the example below: ``` FROM driveone/onedrive ENV DEBIAN_FRONTEND noninteractive RUN apt-get update RUN apt-get install -y locales RUN echo "ja_JP.UTF-8 UTF-8" > /etc/locale.gen && \ locale-gen ja_JP.UTF-8 && \ dpkg-reconfigure locales && \ /usr/sbin/update-locale LANG=ja_JP.UTF-8 ENV LC_ALL ja_JP.UTF-8 ``` The above example changes the Docker container to support Japanese. To support your local language, change `ja_JP.UTF-8` to the required entry.onedrive-2.5.5/docs/images/000077500000000000000000000000001476564400300155445ustar00rootroot00000000000000onedrive-2.5.5/docs/images/SharedLibraries.jpg000066400000000000000000000417461476564400300213250ustar00rootroot00000000000000JFIF``ExifMM*;JiP  >Alex5959 2020:11:29 10:02:472020:11:29 10:02:47Alex http://ns.adobe.com/xap/1.0/ 2020-11-29T10:02:47.593Alex C   '!%"."%()+,+ /3/*2'*+*C  ***************************************************W" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?͏z/G_(3O͏z/G_( ?6?l̢4TyE2c}Q=(O͏z/G_( ?6?l̢4TyE2c}Q=(O͏z/G_( ?6?l̢4TyE2c}Q=(O͏z/SKnZZGZG=yE>bG=ZMԣXk[SYWji ՙyŇ5:.ZxAn]FFvMv;确7~%Ҽ/Zw6C*x]T,DGPH5+NRܾ.ӞR~o1gq5,jX>=ںprSaeesug,1!T3L@OR@#Pz):Y]W+.kCM2wS`06 <%$QX5Gni k:*3uԫ|],x^5kyK {+මo, c%NICr}HMcOi_]R_5mZL 2XyT7ّIUc v("oIIau7Prռ'mqgc>ofV̶)ܜBQI 9#^ug%@2 F(QIr(0((((((-#[f֖-?ր4b (9*أoΏGߝ!bտ:>~tJ([Q@W}Q sCAGN^~([ TU߱Gߝbտ:E]~([ TU߱Gߝbտ:E]~([ TU߱Gߝbտ:E]~([ TU߱Gߝbտ:E]~([ TU߱Gߝbտ:ZZGZQW,aXw8hQLAEP Ïm(>V3))m`x|7u+ yyo43t27M7) Uaֵdu η|@wE00#af%@浨 ›^e+*{mqgsR22:uhG+i{-xG$f7Krq>bH%I<~ʟEbFx^.&EnX 2O9裥N+oeq]1bm;HB8r;G=GSڬjΡg^|BNz ~rwnvt[V-^;Y:;-g+0db|cpv:\ڢssm <,™Jpv[ִu+9mA#h|Cm hX,،@  }jKkK⿼ԬǕq,걾FFF(CEXF#Q<Ѐp"97glEPEPEPEPEPEPEPV-b ( KfxE.x]cީ)',@ uP mHd@jp%` $>TϔV+{KMՖq\-U*9g$cnyt=M},r:^xu.\i$;X{Xψ-n [nEv#8*H=8(iokGq3fqK[yS[VhI_8 3Y闺Dbf 1IJyQ 6I4[lݶK{*[c!By$d}Mԣ?gV0Am4HlUSi%֣Oִ^X.dBְA,gK1j;o &x:dSBW ~RWkXZ4-rI3|ݖv%qD/Kus~#ԵMB8`s].[!QL8`焘jzqˋ})Qc}YN{+xo,EEw#jS٥Vq# #=Ns[x5}6ORZ߶&4Fx.f6۷ctW7j[mq́m7#D4;HpI矩'ėZEyz-VxU QI\е/oGmEq񻾓fW5춿o*¯:ƇFB=Ns[:w"lntFXxs$WUFy oQ\xOZkB;ӞĦXeܹ r (+=OVmIa W@ywͽ1`uW3'l`U9s<v8 [W-ygm5>s B0pHؑ@EQEQEQEQEQEUo*X/€'))1-B j:m&Vb"I* @e]%},r3,~ߟQϳϷJm׭598Kǟ"a弻 n<K6r y~9OMF`XђAR3H| ֑Ϧi*Kg,pdhŠ}n74 4/#5^zlMWnu 5ݜo]RPwlc^+o տS%ͽεצۭGy<" c$p,%} Qv{;[tO-pܼ##?u4Qu*JРZHU '[< o]k2[O$7Ж*NI <몢nr0x h 7u2EX$E cӭn@д+]5n$I'S8gv ѢQEQEQEQEQEQEUo*X/€')(E+3$-Ө@w`2rZ߉Ì ̯2}4WW]oP%N@k`~?*:8?#>+=ࢊ((((((((((Uzm_OESQE!lu3@k]_Gϗ|y.I.٧{yk dqWr w5յ/?B3vb&չѐ 6-ƣj,-8FJ!y9/k-31/&TXT+}Z{q<qoyBꮜ*FNcVƿwj7W$kh%t[1KpPڰM(j~$ףоѨ3BگM.ya @vna4k1tTYdTPZF'ըl$-N(pJOt' f7+i%<gCIV]մFUf'R 8,t;bMIhG#Bdf%}dK? iL@mG +d}КL=#Z*r*?#'9ͫY [$4G2:*im.; l]<2Ȗi6g,<`ԳxMxfgYFi_~#hFsic͂\Y,n/&X8w۝ 7/ n YD5MȬr>RA6H.I$yYc$]n6qlh_=2j(Q@Q@Q@X/«Ջo(z(( 呞MOwcfBI'\cQFQpcL~Ze(.”#-F4 SzR+ǣir>Zz'?r|QESpYL~gi*Ί((((((((((([Uŷ~=QLAEP ʾvۜgf~×y?˜c֫P>ѵFPv[&CZZ6ueCm:bhNcbvl?}B$.u:/H4cHj?` }<ߜg>Y]&ڝkohMFylk&z~"UxFSPD(wdkVUZҮ or!Hf|[[-XxC43ǩdIe@p{siV0^j6g$Ik#KҼk&Ү&ZP,~n8llɦmQ; cw|}߻|OU݊uWzΗaڕ<7ndzӡt-e>#ef_Jš4X9vV>fz}ܞJOCkgk%J\##|zׯ_޿n{O)CO^TR[ W!94zq[jVFitf c Yv:XSԖT7fp"0&O(?11rjƋ.8[ Zk6[IR ~fʉ!8hv1_#K+`e%ëb85-dxjK=6C8Ygo`c8k^PQE ((((*ŷ~^[@EQHFz8 5X7wbS?)gpHmW'Y֟Oz|˻mzEP ((((((((((Uzm_OESQEBK#9ctK~?krx[Eq 5ÑV,tgc+h9zbק>SZ4-r;51mmv̠6v(yK9#&l;vfQNt"*NlH0 vCz ^6-ii6ҟ.9r##<mmpz+[(Ɲ-.nRI6ܦqq"g;GfrNrvj?v#%CE(C"!l4p:+$ֵxfc>fp>ltl=[1-2:;6;z)_+}fktw%ԖO퉉&)gn:ugK5^]6KH*7B~Bp3Z kkED{T+=© PQ@Q@Q@Q@Q@X/«Ջo(z(( }Yh;[4UW:ɐc瞾MXNB%ZYYJvAWj_Ͷj9k%Rv,@?  ʓU;!vI$Nz֭74ᕯٙ)Fp%RxyEڭ ;Ay?X ( ( ( ( ( m_WPQE1!~ERQEQEc6]M7. 9渺0 #8 NўMEa9$6hS[hזts*?.s G]YKymk|Y#<8zs-]Iv<{//tcqs @2 EYM1^ZAjm;.URM У'[UK[Nom$eF N:F]5wO˜ Ùw,Tu}6-n"k: m1&26.̻tVcuK1)so&`o5`p~oܜqIg +ϒy| N ڎI ?jQT4Ȧ)uDguqFx#$McصGݳYԦMir>e{\EUSE}luJq~ujO`) (*ŷ~^[@EQEP) (+:EM}O$Oр\F w *+(ooFɨrV$Ia=Oe-pj{ 2/~Ÿ$#oPI:ŠUkv?twi:iגmu4ͫAz Ego֝wjk F18]JiTk!kAȒ1{7`|4o%=q5SZE^3#2vaЌrxvRڵIu9OH =oQ5XCN]ou_ #ثo?}*^ׯk FZ{K@jd.0YwVۻ+>C9WKh<ˆ# "dm<xkq)_^O֗B;(EQEQEQEQEQEQEQEQEQEQEQEQEb ( 袊b (?onedrive-2.5.5/docs/images/add_shared_file_shortcut.png000066400000000000000000001063671476564400300232770ustar00rootroot00000000000000PNG  IHDRqB6sRGBgAMA a pHYsodIDATx^m}yp/׻q:cmkmx/|k*m#6ezX >DeX"=N!@֚(:`$ab8W̮̬'gMstt+jjS[a;Kyj=X=ȩ@rd[jӪSgږi;ܟvX_rj+lSB 65:_S[Um-S)MOryItLm4W]s*E{t.wڋm=mBu+6M.쪶|ݛߢ]8Uզ*KBX˩M5/ 9b.T%D]or[av8mL2eZi_Cm%7 Jn봇yjm;ZɅm/VUj]rkw9%7׃ T7 6nVUFzqB{yaU-AKm3^*]iRY4-_gc1],h/t]8UwJFu&?j[Le_m[Um*W]NmJniVvJ^ڃڪjSJ{ [rS>詭MUT]NmU%o.bANmmj˾jVU=.ir=mjT:ҥdjJz+4]^Nn;V-ϦnXN 9V%QɅ_b**m۪jSУNVU. 9bXv8ZT쉵Oe]éqkU/ю9/ycP=ȩM 7ڪjSTU=.l)S7uVn %^\875;YyˋzڶNr[v~Q{Knh/BAd*iמ T㺸 S[a 5]Nmm}/h7MXE/egE;>>4=W͔)SLcm/tz(aX*VU~ڣ4UՎV¯VUvfᬚ{0b 5SJߖG.^ֽΟ{'(VغU4wI_YPs/ϛ]S,Ưbx̤|; `E(!P +XJـtO͙]S,|(0P2rm3c(-^0y;s^2ZyS%IMX0ˠԢzu jnu{~ǯ_&J j&{0 74bBa{5?&c'沓S[%_ڢֵ4^H-ciзڱ!_(K, +Yg"Yw&d}95}S]re@)vͮn l?꽔1T&PJgO_??{Y͝{M-ҍɲVRś*E+Sۣ$WdM7<͡_7_ _-x73}3xʴcJy+|l~{S>em7w 87qW\~xY5}s2uiEkx7vsg& ' InG_hv}rܦ6cA5{~QEs"QYuj76ޓlwdՊÛjgQ0}g?c:w{u U =d&=X@IB3(ԑH)5)'S7XrWidcp.]?>.o? ɼHHuU7+ǝyɼPjM-^k=.- Asy˽»V;PyrGMg"7TʅI:P J| _?{.xzlk<{ ߧf_\m ApG_ 5.KM~G=FI7<n^{迯q#UŹ1L\`:1$,'~^}-˷XCտuvuUdfr~_v(e~{4_Nz۲c7a7d$%;SRv-%7OmǶtFa{Cyc{M\'kͿ1w~DžH_IOdIDkZ5Qk;w GO{.kGx\oזfSmNoW7gY23*Ѷ _7}ۇ߷9m^~Ip/%aҿkԁi;\l۰$\LG*0Ɔ"v fdyQ$1_II[ y܃O|+%i'=$@.هJױu$wS׬>9^wz:s'!{^wwe9g?VDoV]}~sY>~ misgoលڰmwG~lcwivsG܅˙M}K^æٟ},Eϟ=wp=o1=-8W6c(yUS}I}oWGǍݪ$"4{u/8 '\~a"|m*}C6H֜[2A5'7k{imjC_z)=J~`|7:{`\qDmFT=GSSzLo_U۹f HPi;;Iޤd~箿a|fA,(_?l:f u͕2Nh M:!KY0qAS£ܼ15@II:tjF=n$ϨY=ſSk1]Ou~ƕ\\yܢ璪"ɏTMkhs9egmۉZ?wN%m'KnۊS5_zM_m+*CT*n=JRwc(I?/C'.ѩ_6 W/t{7Wz/݊c7e=Nutj#jNk&o"}f#|Y}Kk*e]7]t\Z[,o_ߌfߗM,T[W䜯}uA$boתo= o^~B! ~Wר(Da`%xJA@S(IFr,gߥRqN}<(IO+y>աSM/Nz}@5鯓kR^˲5ncx C\s?j 'UmC6%<<R=E?oku~59XcWsv(>}~൩_Wǚ۶͗=^~k\PPhݺǒyvy߁R/T,*SK:rK{&fR/sᄀ olwjruɹ p %)z* ^-:2?\?6 CD5iVxq?FXPP$pqۇQkkԡC|pݰ]T obA̳J,d1%[:2O?=dWu $Mnhч,>?f6y5k/8\^;Psw%*T{wTPn}97!Ǘu4.a;QoSWp~.ϻ=% =ܸ.ws us1©+)6ATJ!%ҿ\7 /<r~Z/'ɹJs 6F!{>.&}0h+o#7Cװ/ ]>Z2o"yS'=o(q_9o&bo(v̦V{;wNXPPPhbbz9o J•^bB7豽l"Af[N>Y r|-ρ^y>r#8Z2Lk\u٤3w})?+y.?;ß =A\_nP.O#ܗ9o{嵯9AKg?,nS-rM^s1~wnbqmF+\sߕ<qym^;(xm[}rO:.;1W^# H e_OA[]?y/0,_Qbze:t-cx\Y}ނ?|P79L޼7mEf9fdμNmPsgK U j#}I뼶7x1-7׈}<$7ۋGaj:b{^Ө/cVZm[{n{{{V(TPu&c9d٧X ir =A@JTJmeHʞo|m bDHרk&2LrzBR5]3Ϊώ޲C\Uffg->џ%Jj?s++c&ywjN5ɚ],(_\ש6RPA$t]ԺV u|kAL] ]G_?CC Sv;5ΛEXЪr{kԽnPݳ׿^e:djo?XPP^>\2j HfP|G=BP0V:}F8Pda`*1v8+UU v֍}w_=v~x[| ;&K~=u#V }ڽ;Ojj9̨-dž:;6U{X)Zd11_yc(ٿy%J x =b5t1JJiPX A O+J X1.^ PtJ0b8C?=`t˜*lMXIES䎡tC0XӤNz(]8롴x&+oIC%(J_9N%fL>MQrhCI=N}Ĭ=`R5е).=.K/@ :P(;w0b7Ro6EQ+\kS P:wZ8}Fͫ'g'xc*A׷zWoz7(Zkн&c@R:6>rӴKoz`sՙsꕹt`NS0qސڞrzUꫯ+W .Q%ע\rm5jWW=l/% Ο4;N?}IPQވʍ|x˗ko\<嚔kSQVchV0IRJ??vY5?=:quŗԉ'y#|^r*7BnZ /FZk6[ Nz(Id{)9sV2ꥣԋ/Q/x$Yv0J0Iz9h *'ת\rڞJJ__yTEdpn;:T:Qu$c+]8A~u7R6LS7~Na>hCI*KɎ$}KN{*yzϽ?wX=w:t9u٤:x0.TL ynWقv#\v85j-fRO=zOgSO>c~%O$5| k:Lqkד한ڪjSEQ~?t5m5mR:R*סR҉Y= KK=|R2Y~(X^Ӷpj۲#Xl`u[o. T ۣP=g/'UQSDB=\O?\rm?sX=#u/Գf9}fM=Ng:j3eקmôߟ_iں-SI /z*ɘJ'Ov ,_;z%0 ANmUkyjm;Zlrj+lS녂vS[a:ٶÇzs^P`i߾'#0z}[t'ldGmLfӮ/i??Gii%[dvVҽΞ7ӹ|-935<ն5ۃڪjS:W\ 6S[Um:]Ю3=Yr C¥'ԱN*DW䚖I\s5/5 ^ɩ6EQ5mw55c(]ɏ{Tն=z=.J||0XK2M)VGrzf{S[vr;`s5.ʺڶګlڪjtY1ҀIB%kIz)_0zANIҫhnzEi\L7=]^hOmu|4ۯl26pzAU<-MUim5mwoP~m$ǒ'ΝWgϞUgΜ(ӧ+/fggձcԡC| er ˵,״\r˵.׼\ EQEQnT5Huv*Οyo`hh~[[믿⢾0kYik]y{L.^+y~Jx#Pm1XI1|,ⶄI/_60Pz7իW[Jo{u߬֯_խeJrm5N覇3f&LT(͙5kz-.ա>GՒ`&Pm$괋2| 0z:vwVoޥ>ZfuW'0+sKw6lؠ=j4s]w-!~{r#Pm l /7%S(---/(¢nϩz.Y҃U_͛7}CjfnFB}JϢ@ Fa&P0c(ec5#)֖I0(PTs[CIs%~>_AQCH /M(=p^^FvYǑ̓^L?H܀)(rOJ.F*C)Jmy](aᑜ ~b!lStdVǐ>XNJP:{=`z$<cOQ(rEۺdzd̓lX J-3;NmϣH[Ou7m( m$qå04rveAO@Iv?f \yrrr#PmCiMm% od$) D(H* drL{2B%%VJۨN;6%P6mb?,ة(Ho@ +@ 51`C$k7J2OB' 4 ,s?fua@[f %g Fc&SPqy![6]43jz:ꐙѽX;%V7=C JR㛽^;uu߫o Ղ@ z -S%m|NB+0jDb %@jՌ@ ePc#5C)&PF7%V=1zm3@iAMߴV]{f5&׮U;LF0JuoTݳ[ [Ս_kZ3Uz 7֊7krihدi5C=&9LCIzɴi;6]RR]^25%@ oJM[Mj6u;ymP;O*,ۢ66sh]S;xxZMnڙ;J@C(a0Rc&P 5kkջ>W]5K %`(5Upy*{Zuͭ vL$7j@ɼn罧kSNGcF:"P1c(Ta %CiuGk֮S?@iQsz\Ϛenu N/\7mO_UWަ&6ת~5$tϼ z}0*?nM7oW.Pַ~򓟘0lhm6uugF=^w&k5n['P{|Qy+߮ޛlaqߩ6t;(-H뷨(ͨm]_TͅW)7]GI֩nْl3nzq#&պ=ju%`(5U(J--_1iߨJ/&ޚUӲ6 Oo=uPvw>Q7ܺMz:ת wͨ3MW j-j}dž?6ZiZ4V{ےs}>gmW>0x?^0\zuPmfjxURrx׷LTSqdX@Wzc(SmT@)a~GF(n懕s fN'N~x쯯c[s=gdd,u^Jx#Pjn>[ꚵR[6 fqz~Hn*?~QWtw|O&4 ߑ^Qz(MMMiw]|Y}U0s* :J>{83 @ݮnpJ jө P;Mwmp*K?~\}̽?'?Ic(5J2'z98/ڦnp=i7\1ɿ!IIg֏T2o*XFT}5jFz#˿Kj?) "ZlWQԵX}Mu{ϙ;Epd$G>/T*uTt?gd4uO1wܫ{0qClp}yXJvldjL%]9w? ڄڲoV-_TWP(t;l/_ 0 P(@M8]K{Wӽ%|s4o>"cKݾZs;ޤ_>=#Jړn -..&!u3w9 ת-up LԌLj* Ch̤ t4aŬfBm{zA->o,=֩c&s۝_IE"PRS}-?3n5F;[E3VJ{|FnF>:Ψg-_PGަ6?ݮ˟:7c]YޱUGN=KYT { |tVydKr>rS,PgtcgϪ?? ~0iUuLo"GmY5}(]n%?3vMI(NK[mw~wxOo %]$ӰtUJ#_r.S8bRN.l@)!ԆMsԉ'K/dZ@;^]mj#~j>~tݦr̪1>P"܇I+%I*(U\eiudCm۶]v0iM%Tz'k`XTue-'}m-f (s-438w0Rw?0 @ oJ6*V{N@ @Wޚ(h#P#j0PS@ F0` wͯL-?2tr vlJ@ oJ-P(@ eQ1m%mJx#Pm{( J0FڪC)6%`(C C7%V;RU)AJΝS?S /5,%VJ Iww:x?Cϛ9hr vlJmĄ_{^RXқK_pYѣGՆ ߿,{W59?#Pm1 g;{LP*ۮ]͛M'adCICO|z@)|\@%VJ ` *P S:vY#_=ƑeH"(=N ` *PWm%F›~WZ2>>d|lP®ng~,@)vx cWw׳ YOֿX6mBڪC) ۧ>Ǒ//Up# e\Jy qrȀIVLoB.s\tחr"ٟ].ω{>@ Jz)}H=8&vmСCjǎfbt lp"ukWupn0c2YGu.##%'W1 cuU.{J1iH `D~6783<&''k*q̗I;pW?YOz sp{:=.'캖,r7!mc4( (ɛ(j4KLΝ?O>ɓ'Çͯ$(|@Ժ^{5_1rE/|-c$(M%4=F,y7y\9XҶ;n\̋#F\r-5-׶\rk^ EQEQC =>炪Т. D±Y rCrױcM/]^y0zڕkXe-8h1l6@iaaAO&PCxj{$ګ)Pzt^5uji'.E˗ٳguG]Z. {(TwN? 힑daDŽZi&i[+JR6Pzԕ+Wԅ zcv 4ZkJz'Puz(8g% %۞:uJsWtv 1TPIn6e^]>*#=fggM)'ת\r5,ײ\rm@^UC),CR=v[ ʗG"T"*#=d'N/ #FZkV]q7WJm#o qɓ'̜AYP7UkښP'l1)[gbG6/77& o!9F6y];V76*9u&L0P ?&9w:}t9&7r )kQI6kUYvÏ(JAN koS:T:v%3k&pCd3D/@ݗLz0) cNy3@%8W,N%ցMzaQ3硓8< X6P{T%=׃ ^~e}K/#G_|Q /@Q2kQI6m$׬\r In6Pci@={ۇkXKZ@ ?yd_z)8!T$h7+_`c`lo {)ٱl$VWn`WBQkQI6m䎝D$Fc(IaJ>H#fP|+Xz6&P*x,do_ǠE0D>f`p@I$flty}*l$%){5(ע\rm0IYw줰wb pzmv(E{ϤaG<%aG@I[ƄrA#vE{~=Jo2.,I_%(Lךl$צ[&; 1Z@^{d%˫}ڱ7(Cɕ/89:( ЀA7E|Va%)){5(ע\rmڏIRͩ]׫;LJO0!k<~5~*΍.ۯWmm$*>V.1Y7=<0S56hJ^AH(h(%t=i2;X eyV~YѾJQ(ئsyJa$`XX0ْ[Sf?#Ira(B #6]g?({Y yvN9ɇ|P*;d== NheZ3ۆ޹5/"q~\Ҽ,y]Ccj浾\U 1:Cz(A}D0\Q{sj%7\rR5]{tUF}i˻IԡsS ~P1_o4co^$\߮;\rM][=~7~;փ'@)< qNrM֛>FsCl{1r}Eu\^k-xo.k=|+1ZȽ!roVmYbdxݢF/ ϔdʍhι%PQkQ"]Ku6Pcg땿> 0| P=eJE%.vJQpU1}ػ o4+ބCl*Sn}R9('tǞD,dpǭ!PQyqk:z}̺N  %[=Pj@ R5 oȕՙHo4܄J͓uP&<ϴߤ~1RKwmݢL+W}J.P*c:}J) r&A6jh1J={<{uY?yQ>V'c,Pʂ +)8ӽbQuAd1۶GR\m* c$De=GymfVdHaiF$P5N@I4z@0a %4C8jL W0R|LvlJ0C 0c(-UTp pj+i@鮭[Ғi5#ҽQfΨQk'r?}jMj46i10CC K=uꔺ#P`\=²=v)Rsv[ ʗ T"P`\PJK1c@9w[*g?'ۦ乌m#>߲o/4-sJԝn '0 Ԩ Q*'94F;ȱsU&xTϹcd< WZ~tx$"Kl>mP=ꌑT5%PβJўFi0 Ķl9e_ZKZ@)nz.fv9v<"z9 :+ݏ=P/CsZ%6X@,zH |i/7hI{dA5rM$xi"'疴{}MyyK/Xs֏!_J rC1"aJ)8?^|>hjNڸ^m9gV:i[c9,Svn}8]_~Sۥ6&F~xl< 5P?QY6u}mUjy%aB$+ Dv_{֤aY`&:yN/4J?/YX7M9 XǙ3_z˜`;g??{t8&HQ8Y@žXc*@ǮGnܨ6nܥb{=&~J}FuJg+ 1:m(EBw&I8Tyz{9Vg {ȱ|  /fjkF)z(!թ:C\/]~"me+ >Q$NʽLL&z?< Í]jt N,IbLz[8*nR9@a s?^kfu ;u| ㊄uegkS3eJ%([T}=1~rpA}}zn0_awC bN?`h޼I]w!Ѯ=uPR/Μ~T~H/s'+ AzgJIo2F487~oGƁRcUv^s<9͙O4)g-bۻZK_5^2㯼 >7ϩ>/J#>A%a +72ϻA o4+ܨ&Pwl[Mq<2cv#~Q(% 9wwJ#*|-`'zp޵^m%;v^@ Ç1D3w?6wX-Od޺u̟jJ( f*ލdxY\Lٰ# Trݧ{vw:Byd({]z 5>rײe֕u _W^r⯼ Z\\R<~ԡҥKKznmQ0A+Yw3߻h' Û'묷LxiۿIygnNrJ9Cj~. Ux~{~SJ#*(zdYu\(MΩڲR^C1VS$ܱszG}t:1wQ ׫J(~3QeBEɅ~ʘ$Oϡ9O]S~",g@Ǒ{帑`_nk _Ck̻΂V~?wu_W5k@ çJvV[$%/\_KW^;s|"P@DmCV~j wQYnD_MUi5(_y+{  Jc=kf$Gp|hXҏ?@[JH{{;EXמ3[7CD鿇ԸJE. *gCp8f֕B)Xvsá4٥vUM*%7D CxJԝP {B^~d(Qe=أf}O:LYAWR.q>{ł@I+J ?ȴCV~j Frsٶ4%2eҏ[mm#=Qoϒ@;:%_y+(` nJJvRJJS%Jh$CiIu1*d+Ҿ䶓 JJ c(CV؎a /^((((Zs%aC `|PT&P_-Po(/P@#74| )%E%4Jhd͕QgJ0ԥ6j1|@ `|u7M0+ohJa;RJJzt^5uO->m0Cə@鮭[Ғi5#ҽQf ,֪}9I5c0M7M'`0 LWk$PdSN{JZ1njC"d+lGRE@9Iw V`\CVMԜ N IfdokĎ:0sBM tK! @PDZ áxp$kw_>#!\ĂDM%/w}1?%y.@)=܊nZsY}jPШJ~M'y6(19VǁR㋜G@sq`ZsFq? JoRțf$$P#@#n,78-cy$,̰5Pه==ƪ@)Qu<^{#L&@IJ{ !8$FuIȲQ8X@qa\.EC8g)蠨}nRS#稗m_-n(܏p̤6R,0S<[Yn4u 4Ƒִ VK_^t:@"v5neM~ }<>:E4NdM:CcOHN"=fV\O'Sιy  ݶn0%M sHod]+'Yfo}< ]Gr3a=\cop=!Q,vNT6Ynyw,yem癭k3}=u\Wu5`"~x}b?H{5J'o}aGO̫\_߸i^[7W ܏&4$Pǹsæ,sjuFl? O:%܄e޾E,pfMorve7z>x8=ķ鱡Pȯ{e7׻}"[5ȫQ]g0᯼ vUzueCzO>YYn4yX,z&3 LBf~Flxٍ1 3mϱĎ [J) b|k:"Pmyk4}=PCq䵓b ,$<{c{{(ۏ<^[PPX›9aLn6sDQ 7`Xn;-8[WS~_1Ɏ?ȍ ct^8e]| / K D^/>z Dz5螇-_+JRܐ}{\8|F}gԦ{wTݺ)5}[WW)%a̕>}ػ o4+ 6b7<hc78r(8^x#f>v܄w`qƢ`ąk]eAFd}ueX(-v+1©k&~~'utҒ^n[TnD0 7s~PJn$7IDnBu w&ym&U3+n޽mppXbDw\.}ɲ8Pz -WkX~PC=w?wݣN]ꠇ0pCnmPŸxD;E~ od]/H]QMmM09;k*ُ{>sm9}9ٟw.c )s |-];Q:YbՕGg[5]kNʾ 0|JN#T՘Iy %)/i~Z^Wܙaj(d%~]wT*(WS u%WiҞ<J"q~aQY6u R;ocٻ6⟁j~dFэb >|CC(Vw>((0l{(jC >>AlHL޺`p\(%˹wsn8>îٿIłxHATz(uh%P {B^V'c,Pʂ +)8ӽbQuAdJz(٪6Rnc{, s(P?d+l'E0C DPڑb %ՋJh16T5FRQN WI%[ugm%JI(/P@#1vT"P_P@#ݏ%Px"EQEQEQEQ=H~ H)*dy@ `|u?RRJ1H~ 1l(/z(PJJJh$?RRUc$vє@ `|PռM0ꏡd+lG@ `|1ɏ#U4-%EݻWMS O=c(S[j ںE---V3*ݻ5??o挾MkĎ}jMjGԒBmO:+z:kEH)R=v[ ʗ"T"P`PռMT NEP#(DG*_oyȶ[Y%[a;REc*"PjgLÜg}d,];@ՁRA"7j(aU[Pji%7_? v"2v <JG#tVP pj+l'EβJ:Rw%K΍[?~5cΒGz`*ۏfYbV7챀0J 3u,D?7&`'C%|ӌ?jSRf17 Y6]+"R˂4 5:CnHiTȾ@Jg{ƍT(aY4 t`ĭ {IUdESv(XЙ  ǡC7]H' 碷RSE5%S%=l5o@c{d[kkc{]Jy%\'PlkKK^GX"@_'%˦ HuTv?[_׵ :15s0+XP#EvTc'+U7n“[i7ܸ{nvK$KgM/\'9:ʯWS+NJ 0w@I'=7Vc(D% T4n:eye#61poÀ'1=)bTvC7:Fu^bۻGHV0|JvREc(1unݰTQ}v$FWߨyZ}sp{x,os&(j\:T SA􎝭WuõCĂƲ &A ]ykK{@EnJ}7qwRԮ^$I2 v@=vFd}uK_ c(S[a;)z(C0lbRz&띛pyfS}J~cXO֊PtlpGnrQ;Y5&kJ~ s;k^. z(D%sK I\bn$*Y쭷u*w4eE9}7Gkr ιx~I_Gho]]~Ty=l{˂׿V0|քꎙT4R;J`PռME"Tɿzr1pEQ. d)z(3@IfM2Ey94⟁h1Z"P4%MCP*j(3@TΆ4`qz4>6  d`9w@::CilnKX0)>~ӢrvR)T(`,c^OAx R;(I/tbzQ_Rzl0&*H[JM?CnQKKKޕ+W>%(1No>9YH~Du F[5a"c&MWSg#GQ5{^#oqBNS(}eA bA:P  FR2%=l5o@i 8mJ QyYO Q0o^&hY>6Rnc{, s(P?c(٪jG@ `|1z(9U4fR6jCV~9*CjZRJ1ȚXc&M WI%[Jx %[UH(/P@#Y%Lj3kCV~9*C) ۑ@ŋEQEQEQEQTPFHc&S%UCV6*CVU;RJ1ЈC)1ڌ0ZP_N0P vc(=R1)*dy@ `|d)%JhDVјIUmP-z(/_Mң{;W}ji'Puc|I5#3ՄZI[&oږe>wLdgfr65.ϩC~i|B'1.fM 9'=dtrcp *CVU;RJ/աұc̜A F% r+D8a@JT:qY_3=vJ!'#D+|1>oߑ b J{<k_׬ѱ\/#az%,( ×l"9NB!7:ȹ}׺Gvۧl+_'ǐ^GY8U?wc`\1*3)lM >5G! ,m/A4Y ak'FUɺ2Vױs4+(9NXsNe!RرqԢrv(zy68D^g&.z(<7Iב}9nV36Xr{l&{Nr~1ܠ zy~h"Pc(S[a;RJ ,PҡP BtR禴 ɇ+zn,?PJ{@%Bٷ/? 9J4Hp ;n\gNڸ^_cnF_XSK,+ه0C D*e!iTxJ>* WP* y41HmR$\ؓ-N W6]+"ROEdҡVYT#Pr)}qe7nTwFc^`Ge:*~P*#]4%PjgRh傐x3\@ V#(ACwRN{ *ztl@^Geר\/2u=Vlo59fr]+x %[aFCAR rvQxVݤGDpGqW?&/\'.NN^רʲוFφz; WHäzpm+1ZJ b*nc7]yI{FNq }g7qJXFkiz6*~6&}kGVјIahJvM,s-zTw]guMΩ፮Q %3rr{x豙͹{$cרyF;v^6~PZiCV~9==Y,iΒœA ]'rPҢ}9fxp1M]`Ipe] VUF@2^\+>c(S[a;RPj@ `L;7ͦ C;WI$=瑛d,kMm ځR_g)pů PjJA,PJK I\bn$*Y쭷u*w4eE9}7Gk7@I[d,uT|_ c,x{IkkGT4FRUhJv0+dyJ͋JE b|]/mTJvR; kf$s]C@)h@ C%A#P`1*3)lM T *gCp8fx ˅R2; vsá4N67?Xe,JnJu?iCV~9R;C)q zP* H~ɱ tgx/ Pj| )v-P _t ã ]UaMTF=>~1RKwmݢL+W}J.P*c:}J) r&A6jhz 1E(Qe=أf}-7NQ `%,ҽrA7;(UJZA(Pj@(8mSi#!J6/ d> ˤm3˷'F {lO6z%wuJJ5@ `|1c(U4fR.(=l(c(H(/P@#z 1v)*dy@ `|5CF(/P@#z XHU)jCV~9j>R؎TUӟ/0rm ~S?UQl鍷ko REQEQEQEQZz 1vݩ(I$aQ b'GQEQEQEQE _Pռm:a ((((FTlD$(((񫁍$c&)vBEQEQEQEQpC),;RUh*((((ƳZP_.RE_s((((j>R؎CLd((((ᯁT%v2EQEQEQEQCh]wJDQEQEQEQ5UCV6EQEQEQEQV1jEQEQEQEQc(QEQEQEQEQJ ]4%P((((jCV~j ~G}׷C99/EQEQEQEQ0U1vVk_R,G،Jw}nS}wY?ZvmRۯ|&WG7:fWVݾ'((((P$l{Ĭڲ#'| j(_ϫǖ*%(((c(N J4JGG(QEQEQEQUJz(j^@oKoKWRoTt5'b  p,z~Q7S:z,%@)d~* eaUX6Ps+Op+fv%z38€(((X5CF :P[m9sX̟vͽN~s+KiV}cNkV?%^k=;˃Hm)(jkdPVwn=:y!^{圚tl+io_?Lϓm8JJR_ no*/tH[:h^{(J{@I*EQEGM{-}V>xd2߾k]ooT=u wT@)9Ν/wG\xMQT՝b˚TW/=RIU %I[=f-Gf?)-&^UYLt5 x7V*u(QEQEE+(N^K6TjS55KQVwnUw]OR`cSw[QϼN\|R$4طg˸>@:J(?'O:Ϫw;Jx&IɶؾV,,Su EQEQ[򱲹 k]At'_VnIn9YC 7Wfu+c\̯?zwy?Rf׫^STE}L l$q\>FQ+Hi/q+(׬eh]ˑcg·% c(ըO>Uk/IN->W?9q>]ӋoRJEQJꙿV&ᖿVO4Ey9Otٙݗ N2uU\]R{}D9_-J}cRnManus*DQTr=kɿ^5_WPoÏ>~޸Iύ 1jGޒQP(LJ?͠nHkϬ_TƇԋ'/3.gu/}rC%%(Z%5s Uۀ. Sأ1?i7y`{vtI].C$ent8d aTM vܠTn,PJoPn lpC+} %E]a5^ l[/[O#)7$fjqqIm/WzRJ.-em*(QEQ,JKO)EsǏ˺8àRn(mςtwCV jiDQT]gew:׫UJ~oMnGD _JR2 R48+t5~T=w?wݣN]ꠇEQEQ+RM.iẻx\5=$r+7qs8fKn@ [f(%%E5޵ {Mο~ ʻm@JvYp~؟JChvtP|lM:=dY "/i~Z^WܙEQEQRWU_esn]9Fa۳mʞN+ϪܹQ?zV=||}⷟U'Z{SEQ,UCV %)m$aq$43.Z2Y˞I nyJJ;g_ H˺REQ3/|_}ߥѺY}#c,uQGvݔ=NoېItzGQEQUPkԠ%[Hr(\Ѷ/u+ɭ{?Fe `yAP}wXj(/@DQEQC];x[cp%(Z1+Ϫ;EXמ3%%(((c(U4fR.{TTt@TΆ4`qf]c`p\(K:Ci-}l@δҁ6`,(!R(}v}(((P_NԝP {B^yn ϼCЮMi!x>Xґk z`@y! @I!-yCtv&% \%RG 0b4`DWYY]U=3W9==]_dlryE Kn/ww$yJWxMˑ0H?ښ]ZT[Tߢ*TI!ˑ~_Y f_,_rO[r<|rv<Ҏr=--7B#,#w{OCndMXr5˭K)k#s{BMr[gk"ɠQnM$!5#۪ikleq nkRwh"}y_&f}i&N٭fkR'ݵ&u퓵5rKZzk:HFt֤`[fk0 5H ^՗_gG[>iY5:RekIP?Jci|AiϹ$iiiii}NcŶ4pmjVYP?ʭI~$OZ>[> $A5hd P֤OG lM>~7j&U}BNۑ(u~[vm&eܺTޗ{ۋ+y./'_^qno*"VBa&~?nO۪ﭽXߢ*TI!K5mZ>H6.7_\3}m<^ﳽPO;~Qnwԧք嫳vA_FhHJ)$5bUQnM;ICٚTW%۪僵K~V#\.}5Yk}?UȋPϖ &Ny5r.p}ZZ&]x%卜Z5F?ie'6JKt#oUF&ťiHL0[>!mz6;A~[5j9!ei5V֏fX_ΣVY뼬7gωϊ3bau1b!jU!﫶:cG[Z ^Y;hu>)ffMkR_{y=pON?Y>k妟^&fyzguH;O{;}kyhi-/,>BOy;}ky姼~rndMXr5^7}~nON,UeJZK 3%_Z5-YhꬽZUZ.TA¢|>= nFnjɏ ZϝקT7VSKM̱e [ g ) }=ϖu[@:raőW-d_vHK!ʿz b#Q[/!ο .]Ohޜ[~[y?-n壶.^X-gօ%q:"ZQ#diV*ˢkK-9ʕ}@sCxx뭷i͹sn9tJGhU-smɯ..ֹiq hch#-YܒI`0sneH[='W:zKι% \/,Y[hmh#ϫ[Sl(`-j?U̹|Q,,-GPJ6U>#`---Yܚ}MB{W-nqBLLλŞ5eh;Ej  F9X(E9t[=.[Sຢ&ŭtVK<&v̌>QlqHx0 Yb{0 bi[q-D8޻A\ N1΍}?Vl7ݹSE ޭ[=ח~Wg;o7u7/Ilz7.Ol7ܵ[ ~hYؐ|?6$ߏ@ eTZznt6qEZ|l[Nq욾9tߞs+[-u{ܗUI2˭ܲxMb#Xp@lƹGħn$7.p-;6o,o\WUWpaK<&Kyar{!3aGstV*S7wVVJ2w)0dSӑ]ۑ疬;ZL'w{re&LI4QQ>̎$jhpڙwx0WŁm3b;_]IT{=Uua%WҤCy ͖窷Nte1coWš/$o6~\k_ هd  ۂob5'xqs7[cnqCOï1㶳ľ#~w-=Tɏ^|?n{,DXZTs~&}N~V1?RO/_,8]Lg1Yp[Y2XĮr}#3J[O>8rS2gu'Z>VOv?dǬ/k'=fя͌FLf/ Br[YIӎ|;%v+Pؒ ۔_ &?_gk^6gK7]кjAz]^oھ_ϓ7^2}Ra~gx>ICws|-=ЏϹumYۻMܦjw&=&w/a/7T_O )jb1tsSۖۧ?v \C>Ǧn[$MjtF-M3pc"fuߞOl]fGf>wj8#9 &s*Goqq~Vc.+8~>G2۵qϻo[_8O[}Y7T/T2џUk>~[6CxNY|2ϫ3'c>c$v_"yNv24V0L!&4IxB!S<}(qJۅ';r}{cnfZn,Xs?M(-nɾ|\R؇>G;Qx̾>/$GlYYq?O| f}ynEn,61kWruIn^5/aw^βw~t׷ˎ΅lky}PDzQ}ߒ߾p|z՟[~O%8 cGn7?żi1[(__C/jgyh]Q/\H٤x_ζu|sZaKkE蘘b㍷-$ D ;li(Kֶc*%FTA %#S~7Ic:>7ܦxkPqS–~ۏ؏`C<\}񐘱9_~nFoXb,_7>i1} /B\m+ ħA, HQQ(uuXq/9MU-tt(n M%`9w},{kBU.ƾŴ#s+M|ͱ?8֫uݾk^ww~kaWFy|c\mќem$a0B0d+tέK\YSˌӑ[f-3r9fwM Y%/ _K$*.u;j/cG:#f"o,c=CMĬ}_:Ħ6LxT$Gސ7@=yC⬫~Fn.mx3jo܇\oܺr] %Fs+lt69޷SkXlzeѹsbߜ|{I:r5knsCrI> Or6,LĊ'^dCۋ)\ Fe,cY.-nEckB?:BTV>ɔ[f-Be'S%NYuvsyE4_{6e|}Mk~ ^7kF<}#~?Ccu ʷ+n}˶l:gƷ->_KO8?Wjsq2#=q_7}XTB=7//ߌ,E%G/6)%7j{L!Έ{n좰rT!qߜ6=Xso> LHgyٖ:v]/܊os3_m+qg4JVxqm=,]~LaP?c꽤˷->}<ُq3kBB[gX;62 [pO==~|ՅT?R/[VpMCs5Y0cNͺdӽņe-}t?q3Z$M[2ĜG{XL;3no2 8]x_|/l+/^*_M~G.o3>ߪmsvkZ˶7}e^8?1{_K,=W[&z|NΜ[eZeέ)vBsn5Q u[ܵIl{޽_|dt#V 55fhVLaEqŭ7*>wFTMbrX'&p:Y}hQDt-.F#-=3Y*,>e86R,Fg %%/濛NVbyF&1pQt\Yu~x5tx49NDZkV9Ⱥ3nҏwwyvC!eqKbd5zέK5Vڲ:ٜ[Zr9m6&c;o&Mƍ6Jz/na*·?^0g}1D~T{ı^V#1ŐZ"Y?1y,h`MQ$Q=r P,u#k7KܺΕZr{N~͜[%.v7r X0!`1r c9ґ[[ȭ--g--+ޜ[K[s@;:r4Ϲ5`+Gmsnz7i u Kȭ˗.s*ΩuŹV?:8K%:Y1?7gi m䖜Pތ:z:+O?VU?Ҧ#.KKUq¹˯ .Sȭ.3gx)U&`sntjRKy1{zN17;O[2f,n]81wkv^z>% rd,@#[9K[GGϞ9>878yU/ӳ|D u,n[gϞ/Q5+' Ͽry}z@\Ŝ[Wۃ:}&.g5Qν57=5'N8)^~eqWϋQݾ,dD 2rK&8zk*p'~uV}DQŗ^|I#~:l52S?ʭI>$On/_~7Hy+^.Ӿߚ[Odr$5yG}&U}2<fkR'>0 IU_ܒ'wU?`Fm~ZJޒ.9ˉg^?#_W#N^=-^=qJr8 ҋ/_yI%⸴&yc}ն^nbn0Z{/noя&~\ekI1".[>)W5/o71󬗾߶]NK:/쯥iil4Em;[&f.U#dK57;'NM\'O\rN.W^9^ٚT,rkҶK5}&~ˑ~''K5mZ>)Ś6IU4 5}&~~_֤O!4}5QhMmS1VU{x{g{+{kI\<$Ν Ω\̂b~.SyK~tQNWucٞ=nMV<'k{k_rV}OĉH n/'_rO[?r<׏.mg#,?nۚˏ{g}oyey5~`Gnd.,u\ZZ[><(wٚ}2'NiMW#.[OdgkR'Dj&U}B!4chM6UH@?QYtKť/("K^9#^7t~&_U[5akW˯[IUZekI1s~'ՙlM~9͎|k5מ?NyǡmWe䖉% \fy3.rs:gU+~ pW?ʭI~0 5}&U}qp^۴&U}rV,D]&~3_ekIVgkI_֤O!dm5iZ5g^'_-%e9²bZZ_Υi.X_c5}&U59\~ob~I+m7ÜoӚTI1~'9wٚTIy=fkgf-&m~NWК4fuGn-so–ɕJ.ϟΝgϞ%B!B!I9Z9J/ .& z-qu-?rbyYq{qj-9*~?ޚXNZxⒸ|>-pGn-0|έ[9N?yGV}έ.2ܒ9jUf}9jKUD`:r%[oSrFb QYurͬȭsSsnQTZ~?ޚYjDȭ>-Ϲ5`+GaZ[hcUܢ6Ԝ[rU6gV9jc}MS@Ŝ[6s c˛sooO,7}6S@]ŭE1}DŽ(Ჾ͘'&EKMX},&n}7M_8 ScFV꼶}`haZL&!y{rP9eE?ԮVwlV -G_~d8ġ$w[ߑvoNVaNw} ݷI]>/=Ǧw-Q̹]zϤ qz -(r;\cBl=tRLTA?wziܜX\}44#[Ûs7ǖ諶no뢸uǴ86qbiw޹Q6퇳zy< ?)nRwUqbRmsskz;=[ЧnNޗm[{J^Rc\9.?w[^7#wu&&Gw{ϫg#eyR#ٲW_ޫwxvŭ~X?=>L{~׌kCb7%וb;mO?6)_#nKݲ*_'cduY/ŭE☼ye8ބŭ{̫bys|cNpV}q1JtKM֏Lvܦ^=]=]lJyL,^)nX%Vű=.<,_N]r[6Q#6wLhw}n޵[rF7-͈Wg fn^Q].};v$`d?eov˺V#\7{ħ{d0z|H]%O\3e{;Fz}:+7#+Ք:wlz0N*ܪj(:P~[ j/cߏ/biiI|#?%'o:< >}8q'Hwܓ1ڢ6q\~Ty6O{.~_ޗkKj賸555%ooI]|Y|Gշ`]y>ǓsS`=VkE8בb_-Hī*>яy}K?g?+^b1Vo▼%QQ;_ؿ[lrPBqzT|sOH셯,bS 6cfF1bF(}W|uij̧\?9HS _\{b[߳IܠeG:[W.;^S}dcS ݆uQzUO&2`.fQ \JQUԨM{ 5ܩo<-;Ws,?.幽e_0}H~ҽ(OG4+n*]ܲo]Z^^Tϣ\5>yvڧ/}yRǦI#̸b-nGn؊뮸%N'_7 &gryA[r5Blfd_c iN~~O@~;nW6nHrѪ)YǞWg=V-_X-y$tQ+M/1=-u[~QYp\Ss*{ꉫ7ʉ_ǟؙnVܒ?/“)n;wNٟXTL)lK>s#=Jλ7쬘}vZl{5[ŭj~5cvG 6~+&)om'7)la*ܒieq+q/Y/DsXAf8W?q|6/l[v(GSN'N=,7M[~{V/rL^~#{8Ҷ7m7n9kHFfV2_V_HS/ձȖ1/[U=_8wk2ݻwVMeg׮$֑~GV(>]csߊ^A{ss J^|E1`(s Xw]}{ u*n91͹eFdn)nhB~!@;Wަ> F`|;rb.[ K9)v>X#0n*[9*k~6s c˛so@_F[9~6*ܒi)n #VZαUӖvsn`:sj)n ڐέe2seϡMh7r[~Kq mldɌ?HKq m0Ɩk+-|^n)n~V!B!B(zVl,BҮ[uc%?Gbʯmzu̟JM}k+ͻ9zFבֿ]ࢸuDLm/uzys-bډz*`/f]ޜ[f,7#9?`,r:udf1uTwoѼu_z5/oMp Q[ Ԫjzq<',b[w[7Cq9JȪXvbaz`>>g@;oݗ_mBU_.oLH.n-U[fŭ:r"FnE[ [?/JcoD[M?|-=j~kqKsB*R~!kVzL{TT KzUjzдĊͭkkq];ʊw.[e#t1)[gVJQ%U2Klz[뵸ujUϞ{Fc*ԾU( YQTBE)L5*n5`;疗Z d}u,#r]o#clϒsuF K>tDUWm}[h|䖙#?`Kq mldɌh)n~2#"}[h#sKo庱>-nVd.--Q{έ --[~-[h9}u~)n[Hhڨ=V--[fg:Ps-/7#JqkiiB!B!Bs c90Vg-[h[.6sڈι5v-323r =9VͩUݗ--1؜[~ڈιiuKq mt;3R3r 9}u~ۧ6ܑ[#n)nlέh)nvsnXUH[O:$Ѓ3g`Ԝs+ޚXUP[)VVVtYz`baaA2fvLɽדӢUT>rHz*nՠssso}Sk-ЅlέAF^m)nco}kEq tݜ[UaVd \OַaQL1!VhFl}8.J{'e5[vImtj^gnj^[bYsn ޗ_緻}[uΪɓ'-]K[n*-^m??)4`{uUޗ,:=ٛᝃ,7ǵVceFVZ[Ԗ=+t?ݑ\E@q._'%MIyT?2s]Xڠgfs(pLE?Wš v99_<3rFng>\[_"O]TBΑ]Hum.ݏy%-?:|\OޟTyT~~yqX=g烽6 -ȭYk#֖PQ6t,W}W^LQS_l/=&bX^p!v瓎G.1DQwl]6k\49[-Qf_ZE,$bRvᚭkp0$.>m_;*; [rU|şٺ*n1X Ζy?v29ssn #Ϝ[~?2rysDLo0 ZeC(snsl˯M<2r>[rCY ȇyq+y MlX(I=a] ޞԟ.LN*҅[vA+nս`͹eF`[[ m?++HEF7܊"[?/JcG('Rjz99ҶjN~V;C+nbURQ=B/d TJ\RUXң>vhV&<]Oŭv+++ޕ+W>R(nŤl[)YT F"T=jZ,CÜ[CrjUϞ{^cy|V1hBQ5*bUHaQql-?f$߯ٮQE%#wdA'-!?ܖK?~2ŧ|dF`eۨ}s Ө%5`X[[Mx?R@ȭ#jM}[h902#Іs+#oH --ܪ9Uk)n *s*fD߯R@zέo}u~{j)ntV͑V&~? -[[snWU[[hCϹϑfUUv!B!B*dJ@̹U>3R@%vR@D-Z"sn`l 66[+\[yߌrUEq mt?疉œ[&`snuR@ȭshՙS6soM~Tz!1 |P,9csnTܺoN{ H͹lSqK%뛪@jC:wJ67#~q -ŭz̶o~~-~ 걷5ӧO[(i&Nbdlv1#o\ke&nBl?zi_.Kw{zú;-ۦfvX%9e}>כ'_|VET/c:ك&;=}:9{{>؏[> a-r[uΪɓ'-]KBvF"_ɋJ^y",WZr o?XaGbWR_ھ7ٟ)BS L Yj*&m乙}ɯ+:OV~C ee=}/g0lV[~-ŭvا42좌Zlgˢ,ŝ|dz…#o gɺU$p&_ud[gvApΩ't~i?VUkPjVhԓebU.#ҽob'4rKfu侬QZwLSns+fvs.ELz9[QKCs+Rj*E&U`2TRzXEnkP@*T V(xmFH9f$Yʻt?j+ ݏ([v,HprȱEGeQ|ɗZy>֮!̹eR\ȭv(n[ * U]E+eo2RĻv#t?"5HqKm?+tד.LN~˟vV.;rT]ܨxmG3AJ [OznsX9䈪P[ͳz#P-T ]#$oߕӹtDG5d87xʟQڠ7uce#eV;FM(T.F^ f{]ku M<ٿ-Q瘁q=E 9p!k?]՝h5T 삖Wܪ{P7V;C RMe#ŭ_ϋRؑ?#)Je04Cs+Rjgh-Y̱ A*أGH[1?T KzUjz0֐ܷkXYYѽ\ٗBql$.&e XJɢR^0 QS0cM? Z+[~9Sq'H.=޽ ZbPYqK+/JZE5j*TĪ.n)TV9\诧QE%#wdA'-!?ܖK?~2ŧ|dF`eۨ}s Ө%5`X0IOq m -Oq md#S6sBq m aέxKq m -6"snTŜ\F9L~ *2))n[}&Iڈ̹͜ZU--|-5'a-tisn۵RZZZ"B!B![~[h#2Pࢸ6Ϲeb-t9L.Fxέ.[@ڈ̹[~Kq m4soM~@:skϹlM>-sk902'c9Х2-sk8--~VIs+C׿6PzAx[뵸u߮beeEv/-1!&&{'b1]"OLOE1}DŽ~Xw2#nMz-nՠssso}S9*fZd]zske(spQjl+ [7z+pZ;b%([`4TϹeWͩŜ[a]$S:}uTU,nbXMx%a7/|Yy,|,Sܒُwɽf\v,%˲WVKsn])n557; \'OԷt0rV Hb23NqNaogCokkn+Hն5I(n5wj{?zn/n>ryTc%$~hdW`ྍV>`t0rkVwgZ*@%ߜu} R5 Tv9U#uus-sn, E Y!Sqt,n9ŦWQ*ȭ@ Fn*?fW-Liap@KIE((UQr?QRҷEG8vzq1Uk=)8SGw['#L䈪[@ "ܯ_V%a֓ \– V;4;Rґa6IHiQ-4ѴgU/rȮb#J-u[WU~vBū:_U.pQG5V,`m@sS=I_5VoZ[-4QQr>JhLqKUj&yG>ϵ~lU/>r~M' k6[&uT:=,~zAآ2{:,JQXM%E&ukp.^husVq?;zr^9UQr^+=N_7{?QzS=rmkGH|8{2{]_~.[I_PQ(B4pqe–χTH%ۮVh{%۪_gj~kL#oSq}^<ثO<>xSjn% J]d^:麡Vz\vq.c#35*2sP=' )(nIٱd!9U`-H\멸/=%pV|[;|A__wInw_|Y WW)[)~AY^j>rK/QʋiNJUx2vQ2ԾrL[X#`q'byyEy3u)y2{]X-TrAF^.pŭqr.}˥/g;JFZ.nկz}SV. X>ޜ[I=d?y^Mũ%u^%e+}\K.ŲQ^ɞKYY+y>8d[8 f<.-Oȭ+*Gފ[r~Kk9ʕ_?{Y֣[&~?I՜Zk[qn߸.[GT? {'[62i_ŭUY6u=rKx("۠ԍŭ((n0Voέ$k? BX|;뭛Eq [kGܧ3!)5">obBvߞ0.ToOOG tEMH.n-U|-Hl.[u?++HEF7܊dA/R%FZuIn>Xgʊ[j_yQJ*(QS"VuqK0\ϹjN-ӏ뵸5TQȝ뤅!YoGH9ҏL)F.<X6j%-\4*nI?2iާ6sBq mt0rk|-Hl.[h[[Ϲ&>-ܺ⍬}F9Sc--[&Fsn@(nFn R@ܪ˄`V!B!B*9'aFn]FV>Z[h8rR5Z[h9L)n2P@6GnT܊)n Fn`lu?ߚ$F6rJZ[h8rRwXKq mԘsˤ}[h[&U@(n#k)n[&~?؜[z-n=y萘C>(Ϝ{X߆0rˤ|۵S^3‚eXfI1=vjQL1!&&&E1#znj^6o-|V9ZddjPr۹9qI-LɉɞBq _6rJ`dU-ŭ̶o~c/ȑZrV-Qq䖉[Vsui}Kv +B[x1G>Ң=g}owL'ghE K} G6_:~h;_",n7! ylOj׏M52~Vs|]suI}KB]HNyaVq*S 3 Q)׉ CY(0rKXiNsxF QYaߑuU|\u2BqPηkt :QUZrzEŭc_]qQ(SۅO~G l+"!ȭZ[t[Xi\ܒ¨w 7u O-?9BOd8ŤDT|Ծ㖎Q8xqKg]%z䖉OcnVw:ݧ_myaeՋ[VFKH(vqpku7%ˬ? GFnQ!2)._ŭwD?xVrAt[0K *v!Hd9Ta*ZJbdBT.UY~FV ^deʋ[ce-&6PڼYl2҇yu[B49˷o;ok,Q@Oף]vk?VU[^g|,X*ܦs~[~4XZ0U~/MdP[tQ/*du–d69#$Xu#[7on+GБ] ԠU_^Hfn;/ﲊ]ɺs?]gןm~ 4b-bέWu"뿜]㣎/PX `A3Z乴v/^FG{.Z*Ju.Fq`5_*6d#dUURwXKq+:5(){7r)K͏-֑r2HsC.b.꼏E!sF4M.bqm/Q9%Jbjs9:25b(@{ᵟ_ϩzyu(Sck%p#8Tcmg_?fQo#zfeRwyz*n_׏? ~~lQ}}jng_UqK[E!s1[(IGvօX裆a-[~ p%9YsFIg9pő~*[Gˏʊi1*TJKzŢ 짼}"7l9֢snTYOŭg1nxEqeQ=ϩeҿ3 \c'д.BE yqf_. h>B}HA g9-F+Z?P8S)H =n矗_t:9ċWU<_2r9zdŃ*nAS[cO;ֱGnK] BYBͺ Q#BUb \׾9i ^`ϝpB| Q▤Gr_s.P,++}*3_'x%邸Wv*z=j_E#L~[U88YoRxq}Eܵb&})g:.\_(n/BjbK2[t.F,eaz^u ϋb'ZQWŭ~CBzE"_uq+ f~T?t_ECeR\['"WĞGON= \A..pQXEɅ;bɺ0Uj%yQ+_0(8m[SWmrS2u]8){N&vYERy.y )m(ܢ؏zIַ۾'cJM/֢snd=d?y^Mũ%u^!"V^q/ElhP3KŹPu.R~ UF_3t9 v"YP 3bV▽nsOCS(DY(~)Xŏ+n9Z0V_kaZN,r6:^_(n…ZO䅳s ڰ!I% N֨B?I9vFבֿ]Z-B ?.መ} 4gX-֨#L._ŭUY6u=rK72Ir9E9sG"V(|yu3벸剞\r-0*#]Ss"Idn' & s88E!8d k9d7EtUwda8E=+3~qfVþtY#q2e*bŊ[VwQT=r$9eέQ/nINA*T Q({t[W"w׷5H*B%z)*.ym_rlGR~-X` aIq9ŭZܒX#h[L_(RK >vh1Yȑ%_] m}슏yǣȭ[~kYŭv+++ޕ+W>T(nхlx"ŋ[}8[ۮj)s]ҜK+Fn>Z>FZuIn :edWH\/T,ɷWPA/7(]q.UrPm} >caouהW(*l Rnq˿i)缭"װ%M FL[&uS@2Bq m0Vȭت30[&F9Bq m0ֆl$,8Yd9~6j2~h?IU?[h90#L~ 9~60rˤu~-5[o}]y[ɲWV !B!Bz̹%YUFBwB!B!lFRɂ5Jcsh5ZSܒ-9")lB!B!75Fn]^o["B!B![&~FdqKαUF 8!B!B!>!B!BȠ)2V|[eB!B!$!2).ŭq["x҄B!B!ȴsoM~ fB2&B!B!Dfܪ:iB!B!Bd6d#dU%9c--B!B!&5Fn]^OqB!B!I9L~P"B!B!mœ[B!B!dlSeͱc--B!B!&CeR\Nq/<$^-F?$?eq }=K}\B!B!o-Z/n-/ĕo^>]Ƥw_<\ -B!B!0Vȿb~UƤu5GlP"B!BzɆl$,8Yd?6V~Vqkkׅ%}\ڦ Xey??!Qqs,G>#sGQJG&&>*۪F6uMGȑQαv9!yy}7هg~~6fYr[UdO<&Vq؏׷N!B!eRwy~ŭ+osWϼ!O?"ernhm*`O)zO qNO(w^C!!2).%?Z( }JőyU6mWO9 [ITz/[E((ۯڧ^zdVIq)QEtNo~U7GG[BV3˿}x|pyqbA'8ğ c׹W?%kЪJ.mz{2+u>\OOsگF{xk,W!}{=SuR%~sGqK:~sלko~^LEzV#惡ےwhm_DWJ I煢tBs_,%c9ׅ&oTd؏_~U7-x!E SN Gwy8~(~7,lWi_Va|\6oCw_wCVˋI [ud~鶳⡯Y?3ɺ hW ݂|^JWifŬ9.Ky!Y|gV|/oߜ–x}/䶗i/$: !(n{C_ 2#_;!|oڽ_.;Q[f=5zK>%]%zx qb*DGPZ乴v/^FG{. EJ_ƿUj&UɆl$,8Yd?6VUnGqKN ~6+nyRz`^g7;;b֑[:aD~DO~sDB!F%4 鲅S/`:+?Rw7jK]k/rI|rˡ\ZiȤ}#}j;B,Qp}[)+>Uۯw 2J)l?ά8pP]OEq_SEzZ js.Eo`?-~e9y,?eZLsnH?K},1ɗ^y6/p [2;Ps Bt)Խȋ/Bœ'>)*G& \ā]w e%д. Iř}f]E)$0j#~kc9%IA΋!ٮxGх繥*~ I =nɏ^~ɬC^u[s+?~(\57VtB/,>3"^]\)lt:w%CkC@!QXS|_>BΖ?[(l#qr+p]J.s 'RgBLu!ح Jls~~9/x ϝpUn+>Ž#TqhqKm(WU5Vpy QBje'rź0#yQ+S~Pº|۪ sΥ&U{. Y;ed׬DRy.y )mXprVvJoבld}?~2M/Yo-H_ŭ+oIOXu6r>cIdkc?tB!iq#/f򋨅Oϋ|)I&/\my -}Z/_Rq.Tݤ.R~ U=~JiXPV8ɺbYǷBijsOCc^euCsyz+bǏuUxYOYssnȏbU\rnWG4dHm!B:}*9^ngx;[Ŗ^& G_ms'~S~}ÿ'~Cl`xQJB gBYِ'kTЪm,nQXcr-Y 7>Z&ueO*ׇԆamǭGK)oxkާ<0@BYYx+w NW1*W=i)x;U"Bhj2~▉%%=K_VzqK?^sXZﱰ׾J7肔[s|y缭"OVF!B!2b1rˤ}[B!B!MsF(nB!B!6a-B!B!2)2!B!Bi!2).E!B!Bڤ9~ um]Ŋ>qB!B!BV}έ7]eW8!B!B!TdZU,nu^RߒC'N!B!BH[&u9E!B!BIsnՈ)nu5B)pB!B!7>uFpB!B!f)2.nr,rɿK.E."!B!B-Pq 9~ ̹4!I% N֨*ُ͡մk)n#L.ߧ6sF(n *2-r[h[~k6s ckC6JQUCi?R@5Fn]^Oq ms-[[ő[&~?[~?R@CeR\Nqk.&I1cF\3;&ʴsoM~ kw/}U\Z_k[!Ŝ[=byo-G-06d#dU%9c/n Xey??!i}]޿v3*bbB*V^>)n};1gm1#۽c-)ny^!f~CBy{rRCNq+-HŤbm'&B Xe -h0-&}3)nsˤZ/n] ܟE$?iqĝI>(YIw.n"vaRuQ%_3Z-h1< XjIENDB`onedrive-2.5.5/docs/images/application_registration.jpg000066400000000000000000002015301476564400300233440ustar00rootroot00000000000000JFIF``ExifMM*;JiP  >Alex1515 2020:06:02 09:20:592020:06:02 09:20:59Alex http://ns.adobe.com/xap/1.0/ 2020-06-02T09:20:59.151Alex C   '!%"."%()+,+ /3/*2'*+*C  ***************************************************}<" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?F+:ǦXaĒBNH\#.R'.XۢIrStIEjuk zY[I4fA i6sӞ4t%WEs.,vjI !xX) A*ck\ۯبwLpz`՝wWFWCS wn[^O'۳g9Jҝ'RgRk;Z+5d_blLl^e ~m!lҖWռV[_-W+!ӎkI^_ׯ35}J{Mn3,DgoNvWeYgd.%Yٳ]|[N)BFMڛW k :z]Fݼ$0ٍc8횒}J9&ۖ .0;Hg:(簧%|^?ԯ5)ԭc*9S&Uq Sx cQ} \YHR[Ye#ۤ$ GjڜCu }h& i* vg ӲYO=(rAwIru!,D_OO:J+}gTE[O}+FYYp껳.}F{rzvή- $5C) 9 `Q[E tzʶGQTu3Z'pG;ccc}*( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 7y2 S08<}MME4M'^[iyWv۹ R:Uk]OE#oř$ˎVJrJɉ-ݢz}M G 0N8Vt#mAs$L#Ң9"B"T](nbOO&Fxbrp:aRQPXAjƒt'sպ)i=+ [}ZO6LUzd+W=WVWd[yYB(ĈVO#zܢ;}nm!Er:#! yY\-t.2HY9a׮qWԥn15MNʄL c-]t,݁[hS7N0e 3WuW؅F m XGo.'F%eN[q_'9<$,%Nd˙ё9!ԆO~~i>gjNKT"V9q q!n'4bEZ'YRd47ka<2E:‡搁~.PҮ&/$s>p~o5j1u &HV8h2ɪ_%w:ԾTr6Bo(0zQ;UNK)<ϖ'xWPǖZ72socn Hlz6+OkN|F'kamcjv?)O{[y;K+ r=)ڿftǎSi?< +v"Gz7"rstޟl?E̡d]$l^~|?7Se{  0= ~5/UiڽHnIN:8Nh᷉^MkH,\j:5א0A28 |Iuugo,f@1s|q C#,܎8jQ^/gQ#м]yo-MeErǧ2|1$cԤ+mGUP1K6O"=f~S&嶣޽ĻVRŎ1]QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE|IѯڞAqUֺ݇(_ m4TAYFntQCc"g>&.sQ]i!YA=Fz}9(twt? 11b06mYѼoK#E,}&姶C}ݝpY~55 M<}yecpf r4`OJ>Y^|CK˹̌lY$㌜[^àA].&DczO? iMRIf<[c,- SD~j5mWᥞhWnKltHrrvq6j#Pz+gWеMI3g qQ@c[⏇|E6VvHA Il3d$?|]}6ڍjbw8uwP?|]}6ڍjbw8uKC:|&7%Žwgsӿ~+<\-K8n㑣PFd-1P\I[~֮<=oow_~׏`sվW]X[]pmpV Mnj8c_S~֞Oݫ"R{vއֽzxO]mgE?E-mY# ǎf24˶m}y5Q@-xoz3eaAb;ٯ6FBq,I89Mw:w7-s0 18=k8&_x7QG[Y-8L|Ú[?OZ[Na 1\aB<Du[bL[;pzәsU%m^U'IuEp |9~kq}qom|pk(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((sUlzv*@g>Mt`PTA Xue(FmRJQE|9sv-m2kHEy9# InbkUྂNݚKy"*1,dNM#WP0RPyC@(kq!HIb9 8jEǞ(9%DyI0 hTpL #$I!jJ(p =h_ӓ>d*̲B%mFA$chcOL D,702?:}^46iTNzuaQEQEQQ:ѫ .ـ8'8p;wK,pBh(I=:E`H*FAEwu 7Os *9[xYQ#PIIEUQk9' w ( r3uZ(*+m|=rǠ%eI)$bHRPE2Ig*G0DncSJ}QEQET0q,FCc߆=jj(jv]%"I$Jnw_`GcWmQ7ҙv7 3 R+@rdZ(%O-DNrTqH ` \Hw8A6~O<%S%(ZyR0Kbp=DO*F)v,N=QP\^Io{@D7drix{Zԡ{\K"F1@V%զkmu$RMw pKM2X9#^Eyƫgi;l 60105حI.gy7'#{dgܢv˽.h$Tb샕ʕ9#S7 8m5[tvqBq]CwI W2ڻ ]=@^VkQERA1„1?#\w2A2Fz{WQ@%A'ҿR)bOVb]~ Gl #V%7,F$ӗѐ,Xmt;s]ޖ/5+[g5{n4wB7YXp9'yoӾ߫\ǭ:b/DvvٕmCcm{dCA=9& 'h|?~!̴" P>ܜ'5TP[. /m~j:{,wXKlBxR:-ޱ)Kvv0CqHZc;@@^.[lO퉗Q^<JF1rݑ<<'M-343^l)r3]Gq 1^xytmvI5o!A(_$瓎+Vi7ֶ}c jRw(pIG98^8m- X5Ku U5pxSӁI-qpq9fo||5V^JCu5h0=M!W>K;FGE8ueqCoj6w*ЮlJ,y`sn;uaiSpOLC0f$tF5P_УZJڤrݼݼAh"Pqئ4}K3\} "x` }]mX("0uoD#2)hC]A>u@Z[֖5kaq;9۰ 8r8 b |Zm$LKNbǠ@>:iuwnw nNZ"v.'e3_˩S%C!$ ;a~2c^+|Z]Z[hX1cPEPEPEPEPEP\Iڮ&lΘ#Qf^1]uS]#MMU4Vk]Jо3zķmXdOo!N@ ۯZU Ij亖-)A'1Civײ^[鶑]J8]d'j|5O?Eq$=_=x 'oo5It5`#Bٖ\.*Kx'm"O[^h^[dJpR"ݕ6v: ༰M,cS$b [5ˌq a{CRh⑶l' N,1YO45ۛҥA@x<[kimchV[61 DQTz.[YpI8-BF6C\TqG83+;pvH (3QFPSkĿl[~o9;k7kic"F_B=:uoivp>]K@1 au>zX$$E/ʄ(|?N}^Ki$dzi(3RJ0z`sŢP_}gܭKQ##%y]D V&NݖypP nŸ3V~7&)ۤ9,SLO}j^%ҫ*TOFJ=؁OA@R+PAR2=ih((((((((((((((((((((((((((((((((((((((((((((((((Fw"m-ūdcp~+8;}3]iucxole$̻Е6v7'Sh;ũ JQK'/^r{mErz'Mm3_4.b珺zU|;0׵).4[RZW{dO݄d<| H8@@ֱA l$ ;@'RQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQ\o \&HXlwnCEq^$-I?ԤL~Z0f`/a{a7q[[.qv]Cǔ`7nϰjRkTKxm- n/$j(:n%:`Mis98'Y *MVtYkNf%;B7FKSu!NҦ[08{u  Fȑd)b[i+:+GԤԯ5WV"ч3m'Q_Er njAmii َ7w:hpnv_KN.uqtcͲц:kEsV6.-֛2HY Xk6o{o$\A)p/"pA$1Ҁ;+UPNcp@Y6FdVRKq5c[gH,|ꁁ ?{@:+u˯BnuY^[6L2FYr:ǥckOhښe]7wv;mLg=ހ;j+MWU0jpVhD,BT$%x [ڭCX46?kI Aّܳs7[RZH9'kC&U ˷?1ק+_$da:,m'˗W\(sCյ{(qdRqTe0rwҨϫZZ[,vA,rP@C(N;(˿^km&h`hHq;L]U&ha{仴;1&3+)bK W qwڮ{nykKki1eOl[ޯxְ ֲOXN )X>B3sȠ,I6WO<^Mn$Co-vLju{5լB+hd,ada~Q\U56-L)$P#`;8=_5$fuM/31StTW5)]T\Eje-0ؤ"NA&kgMd+'ҥ[ e0sr 8|kut-77KܠFIę okDWw 7w2DRۇΡI!qj@>V{Ď;]u%QYyO/y%Ff)^bCan62yZPUdž˘2%%yZ8&dgmͼ+~VRooݬ,xTE])q+2n|3^q+; ab8^8Ђj-OҢ+hVi 0ۂ8峜v.,ຖIymms^7^\%KfU6ܯ_WP$\Ip>W3e#f(e@%NI?Oa,<p\5L% qm=}\r p'0se0]1i/tI}Ŀ ҿi'#ՋsO̫s+hr[$U'"jRi 舍#RBp*FA 2˷a̻~e?:W $z>__4k:رGm&{ d{{}z5XI"܌ Nǀ3ےh]?e>%NI?:W $zGf3=pY|껊y[w;q9/W,5D-*Ho#n9h]a̻~di/tI}Ŀ ҿi'#Zι5vFfy%{)PL* r*{Oi7HˈiChٔ BzZ9es.ߘߴ:W $z>__4͞io'H6AȨU _}26!_99=R;Q̻~˷E'JG|K@+=-tAl]gx$BC'Wnᴝqjkzu3EpظU,pg_ i_+(]?*I4>XdS:ytU2>SڨxWQODoDwWI(I|>P;w_j7-OH%?"U~?Pѵv1ek1 TV`HQI0y9΄ך<ֳp ݸ9tĿҿW'$Qo[J\G*ru-BHycTf&Yr< c q9lQҧ Ly%dKo[J\GټKAm+rE9W̬-Gsi[[<")-1*TL&r 9E]Z\/0Dq$[AD 9$uu_ i_+(7-OHw9W|7qiuatGlO, u)K"$%}eX\i3y2b9~S)7>_ i_+(7-OHw9W̗RPzIIɇDn-ԅq[PKo+p3fR9 zΏټKAm+rEf/r*l;);UN1V$:^uڅm9-YdI|lϧZ7-OH%?"U ]2s Nԥb pZhXM4æyɺ}ʅOvz1o[J\GټKAm+rEAʻc<7QOk$MdUP1#+袶%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊+%?"xW 9Ww͊Ͻ{Y]=LѨ>j}FĿҿW'$Qo[J\S*q"f6v{=2G}O2揳xW 䊗Mn FTBp]s#sJI=M:( (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((q׊xkmXGNťϕR2)9px2Tttx'継q.OW+o8=ϫi76^u$1;lG I#UEa~,Қ:<1o@U2:0i'2EZ]iRBn/6`@b*3xPjVjS\̓Cn+Ⴣs׎޽bk^,lQ#+~B^3{SHf!4SelɒF1wip& 7(YmiS ʧLq\s^-u6KXkkb24YJ[ gW]P]X_7)cFGq> AZks.%VGNӌa~Wn|7x3X= ,̩nNx&Un!UWE y Ԕi>OI#KyDNYa&%vO sҽ6HFxJkGFGebfr]AI'Ҳ42SP_(o,ᾇ>H9+K#gkԨ*4[tKMDV{F% @2 8K bC$\Tl޾C|=z^EqюַP 9L/Nk2\YZIe-鼬@, Hkh>Vi+7^F%*wy[fv3; 1°&]2 eݹw)N9J (/tY"O[m:] qjEC>Gct'Lc1c4VNe d }Eqj>%[jSvOxKC'0n70aې Ǧk|9siiw1\OC~'[R'rziCfOypֱI ޟ&PIv23!届MijaPni^XYȉɓ3}6+<=? o6]ldh BC x5ҵo^no.RH';$ IҊΗ{幇c12~@Ikn((((((((((((((((((((((((((((((((((77^A6:3QcU+;j`xJyZ_LBO2(_3SkJ*SkI[%IK%byC2rr{t0>杬r!X#@naFYacS\ }+ |@O>ZAjbV285꯬xkA$7x}IDN?9Ew¸ҍGm/OVPSm}'}w-|۷wyy{c-R-R}w-|۷wyy{ckv]o:ifmdٿly;: ł֚\a]Æm3wgq˷gyJ A3flt#g: SD`͵,O> U>n=vZ±M[=Βw4Pʷ[ʅ)H;-t&{}B() M(?x9al%HG'ǦNW "䲶:9$}+ ~F-5r#ML4ʻBcR9nWӼKqɦKtmr̬G6m÷9`:o7R6ZMn:R3y҅Lr򎼂~qmCMXaxm ULnRkW_BZMmd3$FӻZe$l /k`X8}A|z/ϋkbT$}q|znYtI2;>vpyӊԵkcXw,p Ioۘn,(]0? {: @")XNK+c>aӒGңu/n%>v/9l~5xtT{9}鑊*pC|=Wk)V鮟H-̌\OxcfA67qGZZO5 HOZjݘ9q!a ss.:#%mlJ䏴73վOM֜yDE3+ eltg:rHV$~'NҒI^Mu则$8VwgjS 5Jq®18LmvW"ϛ}w-|۷wyy{cJ.NF8']6FK,G3;I]8׌Q̻~a/|+yLrY[>}w-|۷wyy{cmZe32oiF i`3O}1E52Qҭa+V[k֕dw`'^r;9o9%#uΔ,f&3Fϔu/k`X8}A|zxiq9[Dq&7q;Nn=TǶk3 Z2 A~%qPJgsG2>W#2_ĩHC|=[xitADS2V@6|ç$P vYis/d򡐠o,Y={Y^kQhi\FHn :/)p03˰r~nmdgڛnu9Ƥg  36:y*=w׺fqƵwEZDpp.p*y1oQKk1p-ܑ+M=$x]1rKG@$mb|zqd>-RR(V1;EKuģ}4+k~\a:"OvÒ_:{: @")XNK+c>aӒGңu/n%>v/9l~5j(]?nan$EV8'30NN-;Ÿtcm* tnHv pI`x$NeI3?g  36:y)Kf'hog7cy˦G_5Y}wٍN2utGrDH%̊gz̻3?yl%HG'ǦNW "䲶:9$})~m%&⸹dU8sz7/#o_To]L(O9Ɯ`gx+֎e9_?Ծͻ얾~lSmێ=Ԍt`11fB6| bxoW =UTinP ^>=Q.-$,rbY`[XzqG2_;%3ml 73~O]փ%mlJ䏴73վOMֹmk\խmoZ8mBo`B Rj>5avtƗQ`T@9;}#s.ߘrK_?NyDE3+ eltg:rHT~nmdgڛnu9ƲiJ ~l9}ڬ#rB9ƀjڅXR2ݜ9IM;LPwg LT|lGvu9 LIH$LF z6G?K5sZi>TmlʆR ( t({}NfF;wky^^ӵB*=^yΕ=ɾ tzl2H=U^=V}LHZ"\86:y3܆Et+mhn!i1gGEo T= ;_Zk ,'*d1]Z< Qi6:ٛY  m.{t=}L4Ӵxx Zdޡ\cćӵQѬmn4|Sub$dPh-$H0+w>s6Ngx]H,dI[4BNzw E'Ԛ 9 I I x$y=i_wf.tuIMf..!%#%rAcVM3K.53mw1b=zU GN+z/+T'9g6gZV{fxq)O)`U&i^Ky@ ƶ 7$X6'}j4O*+-5DzxbÎsOL-Z|_~$}=S54S+ypp}: ({_pZu0c-uZ.t:?(>i+G 0\㚩e.-4Wqw< pQʖ4Q}L+x4}.I/-~ЇqIr dgHI7zsLUI}2[kwZ}lt{)kuiՑUJr=)[djB6+u=}鎢/+T'1KiZuXTD$e ă$9cCNQ x,Ndm j({_pZuu9t!NJJ A61"#rjJӡ[ˋ0$yY#۳ɮ=}NvGoOqyo?ۦ3̠} 㼳 [^JζK, xmrB{i_wTӼ7ݵβloLM юHfoUČdc֩4DIc#bm suTQ OlF`Y{=4GVPptU#qLZc|B8;~X3tQxZu0,,t[tQ[o̐3C8a-om&ץKD0Ic4v $\r8tQxNsJ,-H6T(8(eJ<^k̺tI-_gyl۸Cz㓞/}NcSE|ۉIcǾZncPes/[_>(m#ЁXxWEwWQ:Dr꤁)22M~ |>T&+ =}+No3ȓ ev8]>ٮRtu˛f2<+ FXheq37ab)!qk>=[}FK{u_ݫ<\2 "0p=F:TZy53k%?}Cr|+*P~ހ5m5{\FFN|qci.o6*m,c U$zӾ*hڽͭvZssFܭ`^ޟXʡ w%Ѕp>_6@q?.ҭma7yd>e1a0I55yy7[5/B{k FޜH.mHF'-3g *Ԛ^lf/!(2+edn#94Kto%\JqʲKl1#ȫ:VLa+SXZO$B ES^2Ay3W6\}}+Ə2 :LX]j>,o\dTxIΒI(Xg)diڶ%Ώb[Y"e!1!N9:=OQw|--丵c 34p˿Rrvrź=]MqZlBdy'OaV$$0Ucm'Xv Gϵr~(kcKҞ}]]b#! LI\d@ ~#YQh\~yd'y$8{ogvcylPVahbΔZ |+,uPE,>(ҧXPE6` ȅwH*=Y.Y^I,-nZKHw vbG(j}gEԮnfeKh+- Wb*Leր&+ݽh9#+[[*8aսO\]#y<`A$Tlbxb=DBKCw- Д H798ӥhjzi g([yԭk1U}Ecs^A } !۲ARFF2O5j_iPD\Ga.pOJH8/_ _4۴Οgk͍HYN?;]5PݵI6se.G݈z0:x׻>iu5kIqҨ!wl.T69۝܎9Z:}ƁaO&7%Y$[x.:sȧڭŢ-d0q:oٳn1ިhkm5ՍY<2\vFJ2H H/#\Cİ$8-S =;u;Kmms -Hh:NK_&$L*cEُ]NT!%=,ykTz4忏ne*s^&ASW9men;Sӌ}y&[}sܪTe/<` 6 joU{&Dh$n"AA<۠ů4V(gdiD:PH=8&]v$6E#Dir@\2+P2 a>[˲q3Z76%o$iNIcpX@!Pkv-8%褫0P3)^{$qx|CYDB# FB vڲеYu8͜om23Ancrzqҫ9+N &7k!c1;ag29Ft<6G }W=8U/5ۦL:j ] @wg+y5WlROYm!DˀOn%5,o.L H3Ɏ$`.1(wJ${k__ZDyC #`S$xW);G/;]nI圶VLg<H*/u&]mސR%Q) ,w3cF`_gߋ6E ) F~tFX"G='/{I~p1'Xc[T kk~N:o3ߎ4uhm {ľJ q8<iq4pq0ĎqiBV9)`yr*͞Y[ն՝sGwR4r'w+tnӗGLQLq!O/nx=8FiW7eFk|@`ݔWnᱸϧknĆh n^Es79}-ło[+JF8$=MHLK"R>c7,Oԓ|O_C!f>r ?\եMٴ+k(7X'€3XxR[OK?hErxǜ^xealͼ6X u#YگӒ(-8]݋uS94mZ[;=l&Iy5 ,FˀO"Α%ka.-^0Ӭf4@Xγ'o8<'4nYIHͬcD9SYw,KdXftY"vr li۞zֺֻ% -k][,@d OC(R\6lYG۴7''hT0 G6XՑ>#2 OQU.~4[S`ltzɷ%4eqle: YP9T^{& vm`ed;>dW\֍$K-@+#: AR Vn᫛! 7LE~l~Sf/.|Eo5ݰ(љ$e8F* Z F++%Ie*8Pؤ'#֢EΤ0ܻLҼ  9#1YZ߇}S[ 1'Y 6Icq*>K,~H>hY+ g3\N[Ȩcv&7 +lgMnVYMeX`gdQI 3Mj t[-!,r|E%6n<1Q4MCi<٬ ]L.4Ko,-$w BblzI|Ouq\{` 2ag-d\߂nڵ[Yj\Ňp%a=kY4mxKo-âX0,)@'@.2 DP 펧 =v).UZGn\'ڢ3B[3Ƞ*)Dhi+=L&1^c0.ł[vX-0+cx#IXك"(K.#jV:YFXhМ "MYOKt{aq;$&&UF` c?7PJM>+:gI1Qy|I  Y,mda!N YzͭziO$= , eDkO]xcQ#=4SI} pYS P曪}{^ ?j((((((((((((((((((((((((((((((((((((((+BѬioxQ B-z'8֊]:`WGnd$k1F'c'!~~nlK&li3:\i[/cX]  40WwEy慧\oEdc]j)cH4,XYarJ Nr{C-.}KT㷙.Dr'꿈tTPKc.. L "*qܒئej/,z-;Uo|\6YsEp#CDiMͩF6R :em999eh+ MOFX T2)md.@WLJ[hƕ[Ѽ$aeĮ~SKK}v{ EvY+pd+Xx&(^<=5fuYq9ku(W3ش: z\22iƆ2a{v`󁃏E8#VeaJgjA9@>Cgim?)=>1C&/{AyF66Q@N4{a|ity-]#S庌18=zHF B qiϻѥ^d`vbd`^psB}?\xymh74Ynnu\3 FN O֎̑}dV 10g=sRrϕ~hs$tW-ΡgfZ]\M-Η̦iY 8QqkRg+K[m#g0228xw9\q%w71Ysp" `2NHC/ .mn4c:{6``ݻv=Njks )%fhǙG@s$@ kO5; +8n+>m\.Dd$@1m?fl@<NF; ྲྀMnSi;=~kY%݄lHWW8$5c}e_fmH:ܜO JX5Q؂} -WYc =K8c.B6n*Q\Ưt{]NQgr؅.PO< `uMsVDĺdVx0W-!uc.^xj?Cn5xB srKܧ_*U.|8殒ĕ[I8?:ןRo-KU pJ!dF=j-7\uRE0$3;IJDXݢFNGZV]]Za_9n"v6'9;u0O պOm*M rIVGsKSG$&2F@#<'<sOYV7WQgHK]fcp1Px-ڹdb;J+=cB׭TkɭykuU71cpjڰe # ָmkX/q\(H2kgܰe2~DG€7((((((((((((((((((((((((((((((((((((((((*ݍķqVdé<{Պ(:A5;fwkg62A2 vim-n-sr%[۩nw/s+1 { i}pC3[XevXUKX66 庞sZPLҦʩ%F:9'vȰMڡ?&Xcasٲ+fɏA-`:5I=f+"y*9#Tddd@ZYvl&((((((((((((((((((((((((((((((((((((((((((k{im*nْ7m ZZh].-$[hu?wqHϽ5|Qa%nBݼE^6q+<5+V\wTKm*ȹFTQǭirOefv˺xu2D=Ysk+ Ֆ0ͮ`?ˎq`G>86_l}g6c;t9sZ44D{ٌt܌*S/u.;p';;9ApGҀ4.`K)99֦UӠb翵e̢GMKئom7'<.ݤ`|d1j+ ?[4k1"pf 0J0籠 MkK{kJtA#E z1%eY\LCp9߃=} qz}x_FӛMK['d2&H%6༯Z_fblZX ?&W9m:ޕp-oʶ7%.~^y?Dc_X[[dwFNށx<{VހdGvG ] #'{Ѽ|I?uT22Yv)VJ 91:gZtYMFeXހdIy?Hmb4Hsu3Ti?_ˣ ZO"[Mـ˞qTR[˫G]N8vІ 0)._>l 2kU!YS,$C-)mgnmd{j.:g4[KgÛ} H;OR=PnoO6"y+$z͝wSꗶ6v̲]L0{⤽-l#reI#Dhۼ seW5}_%VZܶ'K}!0R O9}ZP|.:ughѰ"2PO@J@ fMtZtvrHQx>u%+=іH$ @M1bHR@xo-x],PzxxLL6eԥ~Fl2*澚B鐼M!TL~oɼpF>+C8"rjS ` yXL䌅@KEUTK(dhK6 FO ZƘgߨ/B3:47͞\6mtBy?2LN7zK:Ԑ]l[)T̒I6GsҀ7]?L0J\"eoO'Tkۻc}o2S-ڌ#FR%xݐsm{^Emqmylr4s\5 H#$pAy&NT|yW2\"&yXW?עKqTӯn]e"M09x5J@-ZuH^3@MN.MG8!G^p䊂Siom+*bOʸei1,; 2WGۻ]F[BmR[YLs# :95&3jQ}gtZjv'P7'U0'x jZwuimtSY-8?!<玄^iwZ6_h#2*# -1 ,noʎcbv# {V$dI'M9CD+Ԃr@ʒF ȨA]r{C ђK< H$j>6~ g^xͺF=98ojǭisjXEY ZkeLݗ9֖S!ڛp~44FM》AMZK%v޸J+XfR9 q88vM iˣ&)t-zq={m ké]܆bb9s>Zwsmp@y.RRyX-c1}̍(v;N3:nO.{WQyQKh.]q8<ރi%XmbH{'X7}F8@he_Yçk< 9r}zVusg +܀P*gzbt.RL=%NcfF,_<ϥfà[>W]>8Ŝ4E+0L*~lzPce>/ἷ̡\c*:-Zj+pbXBGP?/Y6;KKBP3PCcۜ㊥zvcciձ#`/((]æ9 -kKԭ};RH'WRAqS-e- #0xs7v:-Ȋmi(~]cMSNPa+WD9!9=Aڒ-kKP K}'S& >sO/H~Ɨv)Г!olm^zZ6V ڬsEu!R \>AP#m.Z&xK۝ಌ15~kJkxۧ )9gw\ sXzv 1i2zVPcuH-)oiwz|qg̸uhrcMg{k%֟s ռrh$ۂ85jZF}Ccu$̖X XnKŸo3ZY\Kt5!qu9A3;B&0ր6袊((5SPM^m8+“36bc!*psxoM'+ "1ê')A\*oww >f^it)!"^\2I5z5x|BjQgH0O_,>zcmzw ٣e%AL=??o7󳁷n~u>BLZd k9.d%rZݒWaA X'$C4d{Tis|ҍ˷y=*G LJ 4#92) ƒ@Ey]~lu12>.xdr %LsEz 0K3đ,H9' n/!\5Lms *yC&$էVoEcFcZ2O}IWzڹ.b4A{k&F0pßqS52I2Wr% 6q'8X%ŽM1lf4Oe䌍>5G9蠓浫 5Eu"+{nC>°ͬx.ҖQ%ƞ1!ӤXbIB}$U#%!@@ŭSl2/ujz%YW:6bJ#2Zyk"1a'Xv6Qif rSK7 ]10$/-CKi@^7KKp98 l<zvSZG&AhB9=B+$ p``alպ଴Xl/ #첮'(U)-ᴷZ_\K,c#y>`\|m@p cr@iH$[h.W'+-tLW"XhDX`1ٷ@A8kf:qwZY.-٥ަ0@"dw4W m{E>GܼU#mWa063%Uq/e0`Bnߢ%ѭ1Hr +_XҌڜ+oot{36g`JGn~Rd%VV PEq?5ޤl\I#s0DZ5}j%J,KrisE!b>P vϒ26wGʶHO%Qc avfh;+]14[&Ƹ HeQk[iHM>XCo˷3eݐH@xRamAd8Bk,>c|,pvEPEPEP]CK{qoX_BpA3S[[Agm1J8@t((((((( :3vW,f%ٞ:U;QEQEQEQEQE2hbḍ%@U2 EQEQEQEQEQEQEQEQEQEQEQEVe,u{M>O;3+N(()$YEE`9`:d'Q@Q@Q@Q@Q@Q@Q@Cg6#;HFI1ԓSQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQECs\opdxUrA75X?;іt/.j7/a? 3y̗Z60_/Jo':mpOTcX}Þ]oE?U{ou+V>jʌʣw2  u5Kao%͊#FE6"F)$C '4{Ia."Qj7GIuMOgZZZ@\+`)l s^ ];I{hnn3]F""mVRĖ ryGv7{ou(^~5_D"p4qLd7Idr8=;?j7^uڻ[č إcqׂz'݇<"Qj7k:LjLJoVKy4[xd۹3q5s5>pr`Vr7sGvIj7/a? ei!NΜ΢%t;*R =Zɽ{jv^g.smy6 #>8 {Ia.Wj7/a? gueβdDrZwrg+~>~RinRK]2Cm@RX|ۉt{Iyw6W_rE?U R/"HhB ntk[KIg]di,cqqʶuOyw6W_rE?UuuYtA~,B ^nr.FgqyHgQ|=be[;ouÞ]E?U{ou+(jSjYjvr=sD%22{X^*M>Io#ƺeɸ,زEp:z=s^~5_?W_rPpFFpihÞ]E?U{ou+bk1oaC$ċs68ʃ 'vV^~5_?W_rGnD]#A1,(K YA4nӡR[3,Yʣd2IɣOyw(x|47ş4Z_iYTsOpM[GI< 1QAPSIm}OB?~7ߐOpô1z.oƗ]_̾DE/V tV+لhkU9Sl1A;O?? g:XMsg=FmFDY[q Nў=֦{.^۪4B"v|va[i=oc?XZֹfoӧ..aP]v3uZ+eu,f629DDeF;@>ԯw%^RQﺀ,a[i=oc?Xơ@q" nb6Nijd1Nؔd,ڴW[y\$yMEmGU^4cz.;OKD}ݽċtN1]=c3GM]uŭB W RI0&;O?? gFô1z.h ;O?? gFô1z.h ;O?? gFô1z.h ;O?? gFô1z.h ;O?? gFô1z.h ;O?? gFô1z.h ;O?? gFô1z.h ;O?? gFô1z.h ;O?? gFô1z.h ;O?? g{zIb-H}=X}9"v'.r.ikOpHI*R7Gb:+;S_%Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@cxKlU[2TcmNUn!Y>jzԒ|ޛ9V Tzmٛkvppcu`r }G нMxo+iv'5[ED+%̪RK q54>;aYKJ嘜=$_xo+hC/i_EC}φuopK舾ȖTX 1x8=W5I7Dۣ ІpyU WG^ҿ ?&C-No+ &F?4kO^Ï~^ KKtM"߱s ! нMwwѬ.#nZ,62♧h IG;$j8RK1csϵA^ҿ ?&D<7BQ4Z-X;{CjM Q2 CM" {D7k=ҹU$X`Fj_D<7BQ4! GIksw9yF,Nr39o?j۬]̶688"{J(? WGhww{_yvNIB7`břH<'D{w[YHt܂IP. o^3۽C^ҿ ?&D<7BQ4Z-Ou鷑ܤUFY]XH* G*EC'lb Y"Y>/m+˻yln'#AI^ҿ ?&D<7BQ4Z=Ooiۤ6]Wv, ĒjZ٣ݭbXc6׳BJ/Vxg4! нMZ=ѹnyQ4|@<}MCiYKlxcxcvÓ݆j"{J(? WGhww{_yEc! нMw GKil]Ks",o0@I!Ku dvɩ нMxo+h;Z=4C[$r?WI`3CH7YZXAȎ(h.1]JJނ41@΋$R)WF Pj: "k){`Q@O RIkٿ" ^TXesGʫ;z˳:gcg9jG_X2hd }&>t;o==j }5η>w*|m I%'#q4G_X2hd {;#C,[=F{#Vxږ͢'X&y>Ps,I 8]{EE'Q@O:(R'fwI 469O\К0]>ѧܑL&^,{!; G#/,??€ Nnkfe .$EXlI\ n =VxSkLd$l @O @4Vw#/,??G_X2hYhd ?aEg="({EE'P@O @4Vw#/,??G_X2hYhd ?aEg="({EE'P@O @4Vw#/,??G_X2hYhd ?aEg="({EE'P@O @4Vw#/,??G_X2hYhd ?aEg="({EE'P@O @4Vw#/,??G_X2hYhd ?a#$XaAƐıĊ*C G#/,??€ LG KZ5RIӬl-m7 @Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@onedrive-2.5.5/docs/images/application_registration_done.jpg000066400000000000000000002204171476564400300243560ustar00rootroot00000000000000JFIF``ExifMM*;JiP  >Alex1313 2020:06:02 09:29:272020:06:02 09:29:27Alex http://ns.adobe.com/xap/1.0/ 2020-06-02T09:29:27.129Alex C   '!%"."%()+,+ /3/*2'*+*C  ***************************************************-&" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?F(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((n-Gf[ 6&9Kﴊ6ֵ&G=G2ⴧk6Tg;$w0OO1G|E@3RRtOu)C)xy)\c=]<*BA1x={UrKDiI* =[Z i42HY!'FQsoٕ\O5.l`&.rdmq[yG<~?g}_ ?e'>"^)?>-v E$w$/#rwf8=*"^)?iI)Z>rºmʹ48)'S|@pApLS@%FJaz\c4sWa//2Q/Lb^0J<;);c@3G$:~my[4vwthގx*9%!iI(OO1UtI>Isܛbh_s珦HE.aXY3&G<~?gk>"^)?iI*ڽ%+w'^]Kqsi 8x!G<~?g|E@3R>/2VsWa/?iI(OO1ZQ_$d_ ?e'>"^)?kQG<~?g/Lbzg EUrKFO|E@3R>/2VsWa/?iI(Ե혬5+X;U!B=͒_JoڢOeaxF$)1ݠO)mXoHM:wᶝgw~3om g L[Jw8.2 : +UEeksrnb1 ţ^(`H*FA-Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@d>F\Vf]>ڝ匲Bb* ҴZFsOFӦկ-)Vfyr?ݴӠyd4$=OMO?dݯ?̿y+?֮GD5 $du{˔C 9ʷ= 4cɩ߻_3G$~?WR‘Z,B+.c/DۻۜqPC.9\ZMFZݵnocNkKMO?dݯ?̿y+?̒m6uGXoBppqGN)M_hϑxY;zړMO?dݯ?̿y+?̒WXr[̹;2ǽ6M[hPoݣ݃Acɩ߻_3G= 4rGA/Фm?̻YHie`F[}C&~>Fx54/UPE^QQ&NRӗnSt#Bq 2ѷ*ONƚ ka U?q.QEQEH÷I o#VY9'J*.Y)v&Q拏slyؗ_݌kRCT7u*,H<֥J$eN$͙ZMEm4"o.fXle#r~oJ5vwE|# ,q~9BXp8㞮79+O !2B-Op=uZ0Dl8WKU͵ŝjXgC3Iǜc`WoEqz+$Imnm֮B$`! Z-[{hvhWq8c<(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((Tjk ^KM70p0B~utʬbEJiڦ(Ty_PI_&;aw0?wU΋ɣZM >7AU+h{R(R$TTK[ieAʓblh}oMT4PjSZiZWUFHD.߀PI)7ڦ(TCEM >7AP@}oMT4PjS 7ڦ(TCEM >7AP@}oMT4PjS 7ڦ(TCEM >7AP@}oMT4PQLC$dr>55cƏ4:/?ۿ7m3L?e>55cƏMECX/z_SQ@?Kh,ޗ?P?e>55cƏMECX/z_SQ@?Kh,ޗ?P?e>551fxDi#=2;fX/z_SQ@?Kh,ޗ?8#2O"ƃg`MR$6X}=)gcƏME!?Kh,ޗ?P?e>55cƏMECX/z_SQ@?Kh,ޗ?P?e>55cƏMECX/z_SQ@?Kh,ޗ?P?e>55cƏMECX/z_SQ@?Kh,ޗ?P?e>55cƏMECX/z_SQ@?Kh,ޗ?P?e>55cƏMECX/z_SQ@?Kh,ޗ?P?e>55cƏMECX/z_SQ@?Kh,ޗ?P?e>55cƏMECX/z_SQ@?Kh,ޗ?P?e>55cƏMECX/z_SQ@?Kh,ޗ?P?e>55cƏMECX/z_SQ@?Kh,ޗ?P?e>55d?e>4Cympm" 0=~4}?Kjj(ez_GcƦ!,ޗ?X/񩨠~4}?KjG8]c84tGuVN `g^( ?Kh,ޗ?P?e>55cƏM Hn" +g3SS[CX/z_SQHd?e>55cƏMECX/񢦢2++譨Vtg̑z*y[<OY_K xؖms=+ʃ)DFC'2y8>ul j^([N|˒FU>V?g]MMwᛵd%ݤZ4޼1ls\Yj֙:F{-8ʊL{3OΡ{w5Īl59ʞ^eKd9iEp__xjI4>,BYKbm)Kg[X"Wx02f7 lnwEpT?&cj[JlN!S<]1PGmsԓ(((((((((( z(sZwڟ&TIxB!:*.=MH"wLq9 v{GfkӠ$ro(qzBP.34A)L κ;:dvvT,XrsT.<3\[4io¶mn?AV3iOaOnU-_R&{_S1|3:M&g2 h?tWZ O⵱qVyF H8<~tVKUҔh%(?t%1:W/'Se%A{㕟fA%q8=I<1KA,h2hrzQcip*~SӞAkJ-yslbݤ8aǜ zdQRknuIoxL(w9Zu"ZXZbT+cqYڮݶЍWrh業j7w;v*#"=?NUHt:K{ XWlV(( E\BCjL0D 9 ʥBpS@>oxln.|NS~YZŠMz<:Yӥh`GynPp rus I4R Xzz{}+O{K[ hmcU=r`M:)'tg<96ږ5^cg513QW7TO߯5A9!Dq=o!ې9Qb%FMuȸò% m^PU C:}euxzC`#HgZivAXR H9.09OkXm.rz{uW-r:q5vkx% 0FU\wVuzY7Z\F6ĥ qNNH<ԆѡHZl6@shY{4o#,7vr9Ϩ"˥ܔK.AXx=Gx'm:nCN8*q qKeaA@ ,{o\n!1)2U@9P y?iZ}[XZ3w8UY@<}WZ~^2Tg}_F'5K?5Ėy$cPIA*v篵ZD_j6]yP"7 qѻҴR[ k## PzXxTIV 8#U].+>lWKK'6S;cp67g~\ZV:ֶ72y2L#$_jA[os+nK>cmqyo rsN=)hd˧Z=1B)HϪ`~ njB8{}F8M]b7`lB[ROk-YUf-āʝ.>@^s]'}R v qӦy6K kYrL:Q~^?'noʆ[N펫%f"?XpyGUig+r>^}U/K;vmI%2( --(ܻ(f=X>'Tt] cMs79Y'ՁvBWT4DXM>Imb9SQz#d:No|XZu&wΐv\5+Y<-cKgvqsWҺ]S?`ݛRc֭:lqG l5;T`rrZusN\lybQE%~e0qJM,?Meo,|c3 cf)el~?SGai ʐy$`u$4[N-}&m%"'E~ӧmid^8ۯTh;[in'mDݰN'oah,?M>)RxRXC)2ȠPL\Gn* \d}es550O0 P̪Ǹn N\d}es55ah,?MMECX>?SSQ@~P?es55ah,?MMECX>?SSQ@~P?es55ah,?MMEQEQYZ~iha.˻N$w~~Ef]Lw #VUHU8ټKAw%-&U.` p69$բ`^MR{Y5򙌠BNOyzd+,Α [iJmIjQXSxfKx'[+m.A$29nAm\$5 ;@VT%ҡH3đA# 7ʪNAe'0bKcpíiXk94+:7X!@͈ԝs1|GpῳQ 73>Vlv{4E`!Nce86PH7NpkOUSKeB = tV^*Fyx% 2 RNcKlS})+&u~μmIUA| 9@ +VZlifK[y& 3M[7WQYZqpXGmBP ' wokf`k岮!tIܽhŖKE=MD m~#RJXgEsm+ }bahH%$v@Bxu2#XwgzYV%/-n o&<ǒKi#V^pY@pq\[/ˠE66IdC%̊QNV5'rx +R׮m t;{1QG*в4˯x-"ٴH.2$EV2OO8ߢ"<ǰ ݳqxLiEfY_qR@r;ۢ[^Vu 9 X-&2pA;shE;;wgΨ a4QonFOdfjt__-˺k]H"%$e S~MN^iehRM ʅ|IzѢ((((((((((((((((((((((((((((((((((((((((((/ǑjWmKhor9Ƿ5QZRc7 WLcf?g# gs^==K=xqEiVkP:.cSiwW:mߙvYw$`#lwWrx#SլmBױ]5*\g Ļ H@#>Uy49o⾖JGp")c J:?QZ&xmJm1(8i7vW p%&nGvoy?{wtP_FFGj\ߵX*peQvG`Ŭp&DyΌ*|LJaw 7am^iuQjl!{x]^ fKG3I,D<ץӬt߈7WBitP]|C]EQEQEQEQEQEQEQEQEQEQEQE^RS}RksyHX 2^2x㣃Y5D[,\6#%Qx;~dcXY]ƾL`@!2Wֻ*(o\gb~k&fnIđ)Dꥎz]%Y{QKyGh1=Ԓ y#náVCwgYkbFEhx’`v#ֺJ( NmuX/'&ǜO,^9gsJtkK!GZ TǧE`E ׽m*][r+NBW̶<*ͻ28i$p8}=k9X7tX"Ϸc!ޑ/gZ}I{5ݯ qCa8=( &9_R$IT:,@-3sPMjVm5Z$7vJɻ0Fn訠 6;A7W!T %"EnI$g=Zo=O<33~Fv7gJI{?(.fO*=L=Fi|%?Q@oBַYܛL{6C!H`7iZEjlMaɚG!BHFv0[~/]nΕy~S^xhg{'l 4ѠliKFF3m@n_L_ۥޥL)GQϐW =烯DV艾.lW2>HA, x{j(>oŧ6Fk`T>g^+ry{ is![;xڋ N٭z(mWլa.NKHyAf@iEf_YȐYXKa9%0,yZ(izeaMQqqh4_!'5Cn9~ZjQ]Orx6p.>Xs㞦WA^k (12OUېFK*][+Z͵WP$n1ќ瓜5Q@F6yeuaai$_ΊK D8 Wk?Gq 1&Te,Gv$8WYErz+ a̭֑SaP$ev#Fe8׹FqH[g((((((((((((((((((((((((((((((((((((((((((((]w+Z;s{wBgж8>wFmn58V c$J<省!;ӋI˴ShdY$T26 n8?>d3Eq Mo"KV(4[\J&@dH*4^i"Q hlvb0߆qޤ(*8Y}|l>#"$<+*4] pHʟ@')#IeDy[ljsp=N?$FXBHAbp= (*8YdH MF <0Þ#j*79IQe>Z3ρ%Q@Q@Q@Q@W5: Pr02'vXu;p&DX dӑy <{q\MU-N[ukk Iv` r>}N+0%?af ]w`vW&/xE]ZPyxU "%3r: ➖w ]?~ss|ؑ>S1GNr\Fn5@yزF}:Jtm೸ϝWtQEQEQEQEQU]^ȋ6Wt+ jij * ]K,e w#V4k iu퍎q*89>6Sm5^(- F=v0 Szz1gxT2h pHk1M}cB&{Q]n!Ah I-Ʊ{ue˦z=6w ɼ7wW)tVqGw4:yO'8*k{>y$vܔG%cLS;PgEssjZ]kS]Ǩ >H- *oٮgk<$"hW~Q@=\UZt"CiFVmʭ.IڻJ=zV gѮ6k1W͓d>Chqj0.W(^ adw=y ֋enǪ=y6DP vK}NG+ԇnBd(n+&[oɩ_Mim#Dtۥ BcݱA^@A4ԭ,`XH徹&B,*`1e ) Ȭѝ8}pk"خg-ry8JҴi~4A]fsFcdUpzdɣ)"WVF|IyTiPnA3aimi t %iff'-6~bIPMEpk yaH;+(eQ%8zmioK$.qح: q8y#KT/H ԁSIwxYԗR [Gȑ0vB!rO-%մisxη̪UO73ր; +ubY< p%im$a:I+͕ [5՜!o*E R:A8=9Ͷyu=uk%۸XIEkE=՗;/h5P N>\9>-.--4,Ar sVRΠMmm71#c, FK`sUFѯ!{ y3KuKh 0\dp}duv= rScv%2M[mOITY_RوU9Lj ุd,Z/I3pO^'((((F:6ݴ|!rL*VKQX]xU:?*F=4m١ ةrT;:VJK 6+i(G?94:4X-QmIh BOR|["}|I o܇ƛcmf$o ǻ3Z ( ( ( ( a&YHfU(8@=Rѭu;H/-؂D$C ^VQY63Zž\pIlhR0aZP H?+ǿn/AҖDҵ }CL6VI'We+8#'P$)dmѳ(%8$~5R=JR}F-2/d9{Q#c%zk7WV:}C znX[đF3E NOޟETt/n-gSq$*EQo֗W6}G7 M-tP I[uV=w#:zU(,,:ĂWP Q=C6csoqŝ+,|7uf[Zkm>pUQª601V袀*Aڄ\ Mq*IӽL֖4#rx>3?ZiM6SͶ93rӽOcYiFgogsıLt4PMmKd;מj"~ lP= +RdEq CqK2:V>}sak5ݸ7B$_[*Aڄ\ Mq*IӽL$r"A$wzi ccZ[Z9%R6\9>DB'gnۜ>O(IBM2ͯXnZLn8w,T zO ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( NmۙbK8>Z`G:Gb GN?겪I{!urxYm>@>EQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEVvEt Pl;d \49gI( \pHMcbz }(\ӭmkQñ^ `~$g_: QC,J m>oAGނV_[ϼ_)<y(a̅%| >m>oAGނV_[ϼ_)<y(a̅%| >m>oAGނV_[ϼ_)<y(a̅%| >m>oAGނV_[ϼ_)<y(a̅%| >m>oAGނV_[ϼ_)<y(a̅%| >m>oAGނV_[ϼ_)<y(a̅%| >m>oAGނV_[ϼ_)<y(a̅%| >m>oAGނV_[ϼ_)<y(a̅%| >m>oAGނV_[ϼ_)<y(a̅%| >m>oAGނV_[ϼ_)<y(a̅%| >m>oAGނV_[ϼ_)<y(a̅%| >m>oAGނV_[ϼ_)<y(a̅%| >m>oAGނV_[ϼ_)<y(a̅%| >m>oAGނV_[ϼ_)<y(a̅%| >m>oAGނV_[ϼ_)<y(a̅%| >m>oAGނV_[ϼ_)<y(a̅%| >m>oAGނV_[ϼ_)<y(a̅%| >m>oAGނV_[ϼ_)<y(a̅%| >m>oAGނV_[ϼ_)<y(a̅%| >m>oAGނV_[ϼ_)<y(a̅%| >m>oAGނV_[ϼ_)<y(a̅%| >m>oAGނV_[ϼ_)<y(a̅%| >m>oAGނV_[ϼ_)<y(a̅%| >m>oAGނV_[ϼ_)<y(a̅%| >m>oAGނV_[ϼ_)<y(a̅%| >m>oAGނV_[ϼ_)<y(a̅%| >m>oAGނV_[ϼ_)<y(a̅%| >m>oAGނV_[ϼ_)<y(a̅%| >m>oAGނV_[ϼ_)<y(a̅%| >m>oAGނV_[ϼ_)<y(a̅%| >m>oAGނV_[ϼ_)<y(a̅%| >m>oAGނV_[ϼ_)<y(a̅%| )<QÙEG$N핞H:(_ H(>/M'Gɿ$hziڭ&R;BujO?~Iyo?L{1kCŸ31ĒM[>/M'Gɿ$hz*?~Iyo?ߒ4}_O&'<7Mg& >/M'Gɿ$hz*?~Iyo?ߒ4}_O&'<7Mg& >/M'Gɿ$hz*?~Iyo?ߒ4}_O&'<7MImwLxEQE U,Uv!T}w,6y$cŠ-Ι˽:ԁ5ߙ`'s]TpիaR:=&߉bLN"q+b% 8YHI^;QR0((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((ej^$(l.>%R( i P@'k񭽔u|{F>Va<=Q+k؀IԀ$ xizdMRCy)2'=^UݐB5#P89UKK~Ɲx]}6d۟<]EpȎmR8)W Tp:;+m{R]q[x ƄS.<9ugIӒQ7;^DD ;Nŏ@s ;Sy$ 7s$ s̉IseI% (R{[yYc$l"2A`v4W5MAͦQnOeDT߿ KpGc@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@?ER?EZb>_63^eYew⫄m͛x8#5%坾e-+5RHp¸TKֺeCql#0Cs^J;]7]/H_9hQٟyoM{)}⻇M߆t ibL%sI+YdFGPH#Ҽ~"8VVIF[s$^%Z0NFœkcBΟqkzgG.# AL~ڋ+U0a~c_N8`n*W RiΛS3c$pX鹚5k&sڋ42=Ҳ;Fg<[wXӯf64OWk͸8Ci:sI_F¦U^x ޙk61gntA!`'L Q^]>^m7J$ۂsnݏuoV՝<)DIqQff-1WD֖ZwY aPEijrj0i֑J1%@Wp\ j½5M?k{HA,l6we _y$FF9ҮO嶸O>}R6{GJ6cqrn.,a2J垩3tltovVPn-[ıO|(4@ud}i5ծġe0` jks|+c[˓!U挨'屐9u10=VV<<f$TWۤeu 9t{tuV=H`Ͻ R ;EWWsLe*2F=y鑜kȱQ8J0O?yz`D0#hQ1&@!t;Qlדbkk04R$!>U* gk.y+lFw.Iʏ=uu{XX<^K;^O9\[^Z=ݼSȻ^P20*x"9k ^Rcin"!ܨcA,\񊭨뚞"O3ͺI.¶F 箵,,R4X¨ 8~B.Ҭku+ P`C}0L1K\D]r>#X]|T3Ih$+`9ݾ][ƌ#` zj&͖/h=@rUn-V|7Ҥ1d0xb\ ^I26U!po#h<|´5oKn[9M(OO:c lm'x&H3F`sFstEk7ssm "Cm?FNw)* jY+3CR],xxBA t]e-Mϔy>Ҁ2,k֮`Lm e,‡||Vw#f^:A T7Ǿ-kkv:nikxyrYܴѕx׵-VMI+b498B/EG;P?/5#HNq2C!&cBYKyşH?QOy({7gh$e0o;v4t5l7屓#d[@XC(Z^>Qڀ0u{/]ۢ,HR!XNPYF{w10EFb8y]`cȏʏiD6߻/4}7Pӭ.ď+619847u8\핣R9Cִoa~2"2}rC~O'eK.ذyc ҕ-[ i]%@Lts:t7s)YLg6@1麍ݞb mn-3F_9]Fy }t aީtLҴl-m.NgUS Sր1//5“Zck4iy_x 9vqj[\\/d g(s 1] >=Nb7 `)Z]/ 퍵N7IaWVPr9l|Aj[AYZ^2܉ZxDrr<ÏSڤl'ONKɣFc#: 2Q8tuYTDd c^MZ{u Ek ~Pݧ5<>P*Er/ iW-cp|u^k%-SM0#+ĻG KFH ۚEb%m TFGS߽-Iug[J(]$ykRk?Ꚕ`Ym|~dH79ޯZ>Ѧij3%C|9|k{KyYH"rC@wm9\=*#MOL$ Mc\|pzQ bve!  1/JӵWocL)U>[2$v8SZd , 9-G-ͻ@`l`rrr:ryL}M*i֋$\@ɞ9ϽYkx\xcb*TC%SVPo6%f;fGp"OzEeb8 8S]C eGqJj@J(˄H99qJ&6XLӽԍdPWp;adھT{5P`Wp=JڷtXO'Zª$xN/gkka9IaVd<8ҫB;nZ+p((((((((((((((((((UlZ)FEbNF2V3?m~^XE4UQ>nRҧx"Xjfl`ʹd\ -ciVvl#TD]4T#to&JK2ζLO}MZ[KuƘA!Tz¥3?Y@66>w$VȪ 9UgjVPm"g 1095,VA'6\I$n$b:((((((((((((gƢ!|cG7"Ոi܎oQGޢ ̎oQGޢddtTCz<s #Q7'ޢ!E,<y (AfGEI7oQG2 2:*O!ECz9YRy (Q̂̎oQGޢddtTCz<s #Q7'ޢ!E,<y (AfGEI7oQG2 2:*O!ECz9YRy (Q̂̎oQGޢddtTCz<s #Q7'ޢ!E,<y (AfGEI7oQG2 2:*O!ECz9YRy (Q̂̎oQGޢddtTCz<s #Q7'ޢ!E,<y (AfGEI7oQG2 2:*O!ECz9YRy (Q̂̎oQGޢddtTCz<s #Q7'ޢ!E,<y (AfGEI7oQG2 2:*O!ECz9YRy (Q̂̎oQGޢddtTCz<s #Q7'ޢ!E,<y (AfGEI7oQG2 2:*O!ECz9YRy (Q̂̎oQGޢddtTCz<s #Q7'ޢ!E,<y (AfGEI7oQG2 2:*O!ECz9YRy (Q̂̎oQGޢddtTCz<s #Q7'ޢ!E,<y (AfGEI7oQG2 2:*O!ECz9YRy (=dĖ^ӖJd(7αH;pOhڍ7?& Ib n@Q\ ;[H|[;4d0[Vc)9"@{)u6՛AfotI.bC \ utmv4܋M[3h I :y5%qo_loTЬ1YYF` q~6-jޭ}zK͛hlEX w t4I"EI+"p#%dee9 B p6W7ԦYñ\+^VtOq]SԮᾺ]eNiBYɋyi7);Iʀz79\\$z,ʺD]]#A((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((:Ƌ{Yjq9*}A*'u%g <9%u,G1}S"ӏ|YEu*ΣȧJՠ1WHʂW=p{fՙolm#K +dr$ժ+3B4c#YMA_/+n1L ,B2yyc\5&bw VklIA|տOU jv*={TPK}#M0ZiBcqB9pOӓU>hdbG\`qZP74}>՚(I`#.uqmqu5>$+4?2JEBlJ'`҃[((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((()M-,͵d}Cwj #깪mW*6\l,7up>ڴ+дI{۶O?tx?^SNX.Go17~UUd1|:79;ZX|7 [띒ǥn#6U%"0n2A=C9.!Hicm0z |AyWZc4 v4%@ހnv8O 3I]VeԮ-ʠLx7 uwtֺ#ά r &A-o&7E&0K#."~gw^ZiQZCnb"ܡ ̛S&:+ҥ5]k@oX⽍( oouY<\B1O#֡>FjROq5EAeai%k gl0F<)fF[eu@8Ң)ϤiZb:u*VHt㊊-E͵0lqHҴh shmƝi-ͨ FW4۽;>Oˏi!V򑎼պ(.kk?"ѷ[u žOOZv=AZ͹cR}psV((((((([f?hw/@.w}ݹ;w{o]MW4]WOP.-]Fmi,^OuBծՓo+#1_j:^x61Cjs1*܂C#SY&xAid c,b\ڟvj77; +` g *ƝK2-8%a@[e;R[[u \1T|sj6vf0q7 I}:KiPiKt# 0~`)Y%{ ;pdOj|!YgL7aqm$-ʡdU' q5߆-?H}F1qvې4">Hϵw-ns`_1 t;,WJ?*x+ÚLG" ߻'I$5mX7[ڸ{{[ۀFm׭S.m'SXf%ZPbq#ֲ<0~Z+]>;Wew`( NmN=>ͽD٤09^88=?XhC{#ȃDH߀z:LkZ<{cԑ*#sϥS>ӛN,e3K+p vh'VGltk$.Z,W/+9LN:]b+k@o˒.V"1%r;C/6}ɛɿG`Cm+lzWtBh}H/M̛˕R3?OL(}ជ5{Khg@ʹ?*H8lIlj[\Wv:`4i]qvucsV|imGGQUo$Dqެju/k."ES*FFz)|ME\K.=zBk2FzFkJþ'n! ]g/6`ʝQ@5{[OLAi\6A zb4((((((* #ha iՍN>s͸>=jkDQ% EoJm؋ 0qkd(Rw} MZK(cOdNU唶vytW!jZťjrjrBʕǿWnu;MZQXH\,/+GuۆzҀ:=[Nmktw̮~S{[S->;1G 4i7"r{GJm%`I#TfVB>T=`/^j.65ji"hcE!6<9O=1Һ(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((G4MISq@QPEyǕy|gUt=.,mI0Cd3qMG-ӭ"xU_ ƮQ@imEod[P-x}SQEQEQEQEQEQEQEQEQESe8ci&upQ^Z)k;gU NMERQEQE!`x4S|toCїQEQEQEQEQEQEQHIc:.O oR8 VlzǙ^WjI{[v-ddwb[k u'$0A^l+,~)j35aaMzMjn65}23JIgQ6(((((((((((((((((((((((((((((((((((F\NAXhVD!s#{ފڝNDՌSi"m#LM)#8"THbau.]OvVwPBrnį@pǵuƚ4#yd;+__e[wLZK݉dnNs?s8hoZsiv101}؝T`'ڻJ){u/ ~gxweZfq{,q?J] I \pqi֖~6A"[q  8s4ik'ٵ Nc/$S+KwfV݁I[#'fj),EZ_1=Z_!xQ('ErAEPEPEPEPA84V/u9p"[o/\^ݩ6K'esN𸴺r Ho*zRԡ6 ɹKE\H]Ĝarqs^hڢL"&d⭯u=ʍWQjȚ~Z(mo CUc x̽խ?MN{S  `J+ӛN .mQG_8#J.H-$sMh3^{.V}6)&(i"1u[y4hgYtk2+8@wq,> huzE|h~f3p('&xn{iRhdH2uiu++/u 9"H|q.WpHۅ: /I8"Xb1 :0 y";J+:uuokAzElHD\ auPOڶ=67Jj,&b+&A>CA ;@:J( ( ( ( ( ( ( ( ( ]Nf(]|/%񦮳F7$~=p^epݐZ/גV&D?$cst=7RoL5o}ȿ+}O%(G+C*n\w}kW?畣&.-v޸=Gֺ:&㺴& !k)yc/I|aXy)EsEKUQE 6S]8P# -+P~ zN6g p~>)Q 1uM>6}>2z(#I WxwVbh~Y/#qWIoG#W twi:<^Fv0q̟wJip{r=]'mG/xGs=1ֵNhW#PAE hq@he}Q'̋OOsƼ5m&?KxgB5lWZjizy7-S ;o좺mLxͩ-{8Z{Ț(3) I773- \H SI &ݑ#AWj4+ɯP L! {RYVo/wwRJq]]A4)8ں/q Hw,k1 ju2B|y<=RY[o89?JɭNLky'b ώ5u"\M}QhU'ᶹ|V^sm\\FՍa|γ:I"$yy϶I-{l!c=wɽe`c hZNr{QEhzNO[VBC*cx^~? /L׿eY.NvFIקҪ*,֭vǩj?4?+웯K '>ճkqxM7D(W z}k/P%r@^ܷ}euV/!6W],=;sb13kיEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQET2I\nF i!U@9PzKdF"F2 "I $1hT2F+jsSMo^5!)4SҢ7\rL2݅WYZUYj(ўt]˷ :贽> ;{ʐvnFIIfel38Nǁ=_o/?22NO#<8 Գ -32 H~PNrGs}\j'V4e,P31pAG/bH4Kы*KRy$8&6dSo@ `:E}נ*3'=݆]C3Mz.z:+s5M!E nĚu[;wR3G#izoٮv*\11ړ_e\j}cRɒTk~yq,]2#ֺԯb576r蛣 I鴱##nfKM l$ Ѐ0G&:ugnaBIv^ T&Ԓ=6-M؋].PTw@<[FK+fo<Π33*:}0oiQ@ۢD*y@OOSR1*hBK'>FJœǩ_[HJ6-1R~Nqؚޯ i7׏ 7ڇ]zepi֑ZB?@Ki3Ek6e%9{t)zc=>k _0o5+xmZ8d*ƌuf :>)ծω-=ԖSܽHD6B OںilVE6m >p>qQ.-^c8|qmcӷ鷺9KR[=&{׸kٜ<^Z*X!F=TdaIg{4}Zɩm]6p$m-bTD$@<2=:zN Eڣ܂'eA>oƇZ*SV|ʚ47].t" F `)Su>PYiE6>\\TڔFN1IQPXQEQEQEQEQEQEƂ'9^$i#* \획d3m LvC,vWbI<1񴑣m (%8$~5rY ෻@#d`NEQR(c+T AS `x㚞K+I|6G.g }G'j惤j14+2ygGw#3~4m1u?ӭ͟j/gm΅^y\Clsrۣ,GGU(&v&iG!( uŸQxm2-H1i|SaXJWgjEQEQEQEQEQEQEQEQEQExhv{{`;lt4VN̪ҧZ<­߁4­߁5Q]_Oil,oRK,X\һ(zVwuҠi+QX?|Y-.e?4icpB\1sW?넕<%7|2/i$Z E4#?CK4ՃX5Р<oxս^4KӚQ^UbS\u֣w%ė7fcMzݿ"bMnk,CH*1aQ#N@Ju HPFF y`S\[5 |No"ѩ]]rAoPItaU$}Qx'DojAF7/݌,`{ްK}v", n-|E |:DnWx/sZ4x5u/%bnvuN~g5;Ro 6_Zj!9>n2?H& *ORd;T_f?}Q_ |QEQEA-Q-OQQ;}*e'5J5[{&pUԩ א? XuHSd[1u+ "s~>*N&' ONǐ_-]6$@Jvyl3^bfy1n𮦊Ҷ>j~{GFOi Š(((((((((((((((((((((((((((((((((+@u[4XݑUb6 V1ɧZsG-ȅUڮAGzV9H"ibrvQ.K۞NqBj:nPwmRpOѬl&47up8vjӍW:U%ng9>թ}pk(tXJ._v:m# ٺW~Rӌۼ5}=嵗s\o*țda s}+H{K6l֠Ф bM,Z l)|+fj:}&{k}0q^^i> 5,uEʋd1A{dSijj8Xf%|:uW{<:%VBy"Vs[^WhnSYy_u# +:jsms}%ŭ԰Iq1T=}+_DSTPԚ/l>Cˆ"&ӿ#hO,PK.٬grA*0SrG:ulgkMLWemERxAPWVփ\ u@Q@Q@Q@Q@Q@Q@Q@Q@Q@6IR%#`U{'Vk37$Mj26=`֕m>wcր.QEQEWx?Xlέ8gO&!hj 'z`*y_6&QRI)C4{I)?@KmVA%jj#4֕<t=(&wLehr[2PK'w*JZ4Q\IYQH`@#d{gy'RQ@}俕#Z[xQ+EMEs^(VnM'nfmpyy;W‚y\tWM^7),l$"Fݫ٨aYM%(X(@$*rI\^໎k2}_\z~us:ǚѲF}+֣=7qFz<,~*OReF=Z= f GJͿGt=:?&o0f%zq^oqݴs۸)r+xyig"KrJ(#luah 8oF 밞Im0uGxdjJ}C) dx"of\Lj.嶆8-+~h$,̓߆3޽ju|jV[?~[v9kM)7Jp}ߠXҷϤh'͹n y 7xbBXp;v+ kh0Õ7<Ԕ_EoQ^A4T~8,-Ii%Y 3a^r>4L.bI8 shqWP>IMc^ji0,$ Ԩ89aOOD :y\__jZD0rhJ<ώ\} *t*kO6V4=wU1|%+_i\w1=pN8#Ƽ#W_>vqkN 5^igGkEW}(QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE&/ĒO/Aup"`C6!fqt W$mO٩N6Zm-'դhekIg @b/xOX&5_*(H@=H|T/tm,0wV 98^&},sjW%EmaF\`sg?_zlO}e5Y3{q܅#=W.nZ--(!m*wq5WtA۷nnyj)<47W]C%e5R"BYU*FA9QI/f'nB[sfwlldWjsh7Q\6}3"s"n䞧>q"&IʞI"1RpCgʗ#K 5yt K+ k{˩bOV[mV&y.,˜vFKdAɧϦ^G5ޓ{\cΎ̌@`*_}9V;dv,w?NErw?i^%{/,/mZQ)]x}9#a՝ScFg X=GP~=ٌ) {,,|VL,Ojst6XX4VR7)brNrsE_K3y._//?7S]DN{Y}ʱGCWjo.7Km6B=*q˖ ((((((((g^0]tny0}s]5y)Gf1I?x>1ӍJWVˊ*t1"B(oL5bc7+2ʞ=1^Bۣq8j1UpTi+8IU%*Gonuw\Y>F،CX|?E8`5X @60.q#iEr :Kfʪqv,s`ƷijfaD@QЩUݟ$#Muvm$)"[FTQ9B~OzmOM׬QObȖdf7 L`z(((((((((((( ȫה5keI1+ Nj1 YH^'mz|3 t?VUmN>N Ekw~<еO hn2jZRlT]^r3T5Zh0ʽ~\s\Gi-#%X 2+'R"iRO$ݳ~Ҭ)Fv,J~=+gu4B]?!E(>U/WM'vd$cFU~gӴɤg}YY1=M%PEP^k^dJ;iqqoJ҆($.3j?G&𳷗ެ/<axSZu'G؞r>+TɳP$d׎9[~i}|nms$!r (=kxWv,Q:( ]V>vE:qMꭢ t)2bB "ִyiekF3FҼ{M靈{.2ʏJÚ<|ݰiP>UOSq+_?:(丆&,pTխ;RhZCR,oPqVMԒHqCy-ܐwp:Oq[iuO%] )O n%hcHD@Te̮hisW,%gi5% A 9)h(GuE˰QhWW\z1Ȏky|Tϩ]ay"!X+": W_H| ?԰d2r[8j,*oSKo[Q=|;VHo09e4Ty8'TQE|qEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP3Rp'dzEqktXK?#,;j #dl7ҥվrpI3jJ >o12忈a[C%#\ qr3C7@2zsV\QnRI&JAv!A*2qYhږpvBy|ˀK#F$x!±`dc=xCX-Rx.6`)/?u ‹{Z ꥷUu'"X O,mzOZM,[iP,[H#!v=N,Z hKJ1RDqKd oK1?ȧo%!QJAySkJײ_ {)m+m300a!8TqӚXwɞT0}u=k~ūӓw (4 ( ( ( ( ( ( ( ( ( ( [[yI&)>:W{TQvs6A jXU$ǩilE jZ+(ѧj`Gsm崖p<Y"+x"t*խ [(hXԓ@n"a3n8JFUK;h%X 1 өSQ@lG7$T_4[yR~n eQ'\ ( ( ( ( ( ( ( ( ( ( ( ( (ἵh$ )T)t" #d=bE3cH'*S+jhuÎGB:i'(QW_z/GkEw5Tf_ʀ1}.Y屻Ex uM+$Wf_ʬAfoSڶZTՑÉJm2 LM#G9 c$jVMF0vAER(wۼwM1@ ܼe U=lCs?"ҞuouaNx g4WE:k{5\:f v>UtGkB33u$W_z/Z4V238kw~UrV/}jJ*K ( (1nj[)%I(ʱԢ1OԖ+..)0m!sViqe3Gd-Ն3#`% j7NnVj/Bk=[1W60 ¸=1;p<թo-,&2wp1s==kM6ohA\ c 09YK˄(#i;I=c KGiGc:h}6k?èZmg)Mޛ5}5~2c}j\;}}17-%t, PNp9;Xk c12@f~yxak_wNͫifojRc2lrp*Lt^{l.cd "n>\zUԷwryhuhYA'Ź;IZTڎ@tsnR (3A^Bj% j.__2:JOO#R(s(((((((++T t] KZyE41iY>"=uGX?ƶ4u;yrgn#Wj(#*6cQ:+ԼI>IĜխl75a{j訮s6MC\4N6n/#{%  Nj:]J;<[@ʐy$`u$y| 9:bjUlXX[iQXAv $''5bTLiSbc$j6܅8'}>h֦Сs=:UsgΕm4'99Xd-.YkFqp8OEW cgGc`z5H[;3i) l؎z\`zբ 66oM,X Pq=PEPEPEPEPEPEPEPEPEPEPEP;tq8+çyd> :T>nbK Fg9匓@Hw0Rl}OS6Cz&V pO~Y3a uHd2w*ͼ,e XsZެ]mmGuIGrK_jWgEKd;T )Su_ j-," m`rν{v2º=sm#n$Xۀ+0H@6ku[VeP'zqֲ,l\[C=}:e'Vv*( 7\va7 (>H >= tnN~n玞aj5ɲ:v/ q0A*&q{xWMoڔ!>=ЁTK$lY N9<9_hdΨg$E!99*Y;OC`Ȑ[LSyYpz:P%o?w8x$9MmP^Z}VDTK;%]@-e_-3nu^]KH+wٻ-\7;cשɦYxgK?aq3 po'jr~Q@֎7l. Hېplxemw@Z3oԔ#<Gk2!X7721&E! eY!*;qzu<#uwn_CwV۔I8!iz[RK}7K66>O*Li@g Vɷv{3ƣ9e:U[dxD$&N3\twŗ򾝩YO-lc یV)=(&@^[#CILZ53F^{ewE$v-I8KΙmܱ^3;E 8-ي {]T2Le=;jZ%=x.$q%*r:P9 & LM*A7rɌaǧNxfjZZAemhVhFi%/sUAֺk}N6h@vTڽ{n/dۼgvp1Ҁ35/jPGm7 og%ġȌgq*57qi5Hm|持 hXz>Mٖ;24_Nk0q8vsz4M8K-VbP"g((^ha[ǵ[p6.2$Q`rs1xTm-b]2G67W@s29ؼ|ΊUv` Ht`˕OjEƒNNAt/CrOocQ)Qy[L[H.ah^|Ȥff$d?}# \XX-#y-c%w+AeXާZ\70ƯzRWqXPr9χX:3M'qtIpv/ AxR hdR,P#s [KHQ* ی>YBs0pk{'ÿoU_1IJW!r[og B2*_ƱH*xM-ĚRG2x ܂91@㸴t.&ՄQǕ#'XQG'.˧qgikԒdt-P$܁V#8]BxՖK LӆbzlxŒdiCC gP '4{ڄP$+w "hVbx +IXJ[Nן;`J;:U_ YZivq;Gip.ga8`E [?+-Ԇ lvAӥeG9ז4w GM˹K\3);]EܶLE!#yWd`G ?U'$$i]y;qߦ}kBJGaX/ ph j}kֳQ[ Bd0ːs7b%ռRG$QMϚ2W&Nyi-".іvLBm拏 !Y-/,nVie[ e9uã)R@ s@i:xNH$[cWn)^&gd""%$\Bppsx&<Q̒˩Ff,@,Tg$#vwH'oVv hX|Ar1VAzn'Hn ufWV(*9YyKNK{4\ykN@'=*F=$޴71y$RZjH$77S ]:4ep9;qKw6B +DbbϴRI# Bo5]%)حd2 jt7w b$` `Gomk\%лiy |0{`P|`c+[o RIٳpzjhڝQQXT0*Tu) {սop>W-죷FbLUB~1#[{Aɉ_˙ns<~nbL">Z[H;fk5eRnd] 0G# sn׷`2(2$rG@qKy5+% {tE`Wb* H$< Sv- ke0ԴiڦJ:u t )1r矔 q[. ]Tb`^Օ3[nH @sڽQa~MSf`1߃j_ ]-jmmx}6,,kXo-dh' }F@d3𖱨jPcI$򛧙mA`p+h*'"m5FK%m4LY7YGB{N.}3điZƣi&T28ەW@eO`QPږjR=|qVe^hjrj3 m {p<ac{ח].. yIm rʻ?($Pڝiw`o%j"fܨJrCo-zUǕ{օgٽ鼐=46]Cpz/O{s5彦yo8";cH I'9zUMk4f[xn$H6w*8ݜ溏]ޕ3]ƚ%36F8OOP FOtm[B~Fm}I$噏rM^((((((?V՗KH^Oq' pX0<ӊyh1\-k%䐈3˂H@QAMclfE7(@`pAYȬuPW6Wځ7]T dқ] bT2y:[/GدLvr (E$P#A$ÜcԊx^审ԣh 6 ARx/ WGxM=aG)R+LzuqDj_mqG((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((g-#UF%˿?U.?Uk~=k-P$hƅcL.˽!&G5Vn\pA*FA 2 $`.?TlߓTP.?TlߓTP.?TlߓTP.?TlߓTP.?TlߓTP.?TlߓTP.?TlߓTP.?TlߓTP.?TlߓTP.?TlߓTP.?TlߓTP.?TlߓTP.?U$BQ96!\~EQEQEW|au-.=!411`m19#^_ _=d^+"'BWMŻM_2W/j' _=d]ؕ?TM_47PG;:!!$7Rlu8+? hŨϦ诙?+? h_2Oڧn{S?ƏJEAO$?*:Ϧ诙?+? h_2Oڧn{S?ƏJEAO$?*:Ϧ诚g-%α̡U$Ҙ($1ªI>ibԵ{r3+y|Kh&hֵhCG>L_2OSп#>d_2O{S?ƏJ΃j3+OJEAO$ΥG}qx!0+~pO?+IrNF4zW=.( ((((((((((((((((((((((((((((((((((((((((((((((((((((((}t{^vH2՘ /Vcܐsφ﵇4XZˈ+3<˶[ T\ɪTMR]_GvlU d).3+xnMgIF?.=H>jsbiZUCa[09$$'js6xV\^A5DCmӡȪBC`vdYn.&,$ʉONqqZAe5yL$LOQ3:`V%]Аx&9k[mmM$"d۔lo0ޝ9.{ J4MČM(ֵ<14Q[$sFaeIzc\GtTkY&KX72*e =P6y՚gK&mk6fnF޹=)>./. 9ei`X.Sٸ7$yL L`Ów ֗o ư&fyd!>wbpǷ4=[dx㼙b%yAz 瞼1qjZeA5XGrn3 U:քzֵ!]3uNA&Đm'Dz1hZ9'n Hly94Ʊwd$۹Ea&ݪ<yVmFK[̌\ ea }9;֚5Fְl1`O9>?RKZ%MJxʧ# ق9gk>y ,J$Sv𭵴P'ݍ  ( ( ( ( ( ( ( (<iדWp:j\?͟ O6 tMb<>%^aS}woo5C8ıVAƺq]Xg'zʔ=7O-ZˋkXE[K\IaI8}+5 ^\!03O\q^-^V$%E' PTo[0-JZZߗxcF21-]/yGT^x_zv8/P\;:SO^:.Yqe_*းeeMFm ۉFG8zͭv"VPOMzbUJ7Dϥ!.(tw$[pA8#ӫj-pњHۙ?NrG(][QRoH"fctZ^ؾJ_r=6hZ[,텨s b ezUH3[y,t6/<ˉ2<+8s9 zC$fʡ _ȧ~]CzT:rˤϫ;MPrN9f~LO;N#;^i V6JRǘ}[uD"Ż,<=J>W/_Z{~ o iI[ƲV{%CGpzMoUͩInak(y:NpORkZ^?c+ɫ~sr~h?QE|QaEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEVvqpcAI9;dz-Ȅ,b3ef2sv3Ҷ)ӳ&2QE"(((((((((((((( ׺mǨ[ݢ*"qkQVN*ɲ8Iݣ'OL8E<;@3 ֢2t(Sÿ??j(?*E<;@3 ?t+Z=Ox{*ʾ'OL8E<;@3 ֢kSʟSÿ??OL8µg2t(Sÿ??j(?*E<;@3 ?t+Z=Ox{*ʾ'OL8E<;@3 ֢kSʟSÿ??OL8µg2t*햛cV,D E'RrVmS] (((((((((((((((((((((((((((((((((((((((((((((((((((((((rEk| a ZЬOyC-DLX~쏨)rwds}WGcdA)nm#Vp=GjyBuxr}k8TesʨVtCn┻44,"Dn0%%FGqYxHdh-4Cq&1ר#9{XndMdg$ml~ h:dWF@-HzzB"v#̹eq:FXkIM0IhE5'mnN% /5Ē,BAʨf! |P Ci{\.1M7Q OHgrdkB6 fm.8 !}>\^Q$H Nep8YxgK?aq3 po'jr~Q@Zh6yw<0<1*2. `u㩡ԭf[ݡ]Fe`1Z0xcJC46qu,υ9UX(#;A^95qY5H.%e (8ls'cjZ`lU1K.D"/#6źIİM&sgo#o͹T֭uqpЫIs1bHt M/c=K- s_M(M+s/ی`c j֗7cAlex6565 2020:06:04 06:37:542020:06:04 06:37:54Alex http://ns.adobe.com/xap/1.0/ 2020-06-04T06:37:54.650Alex C   '!%"."%()+,+ /3/*2'*+*C  ***************************************************" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?F(+$k%E(*H֢MO f OAB.!!&Eu9-Q@Q@T7HRm7 z՘4{h"52Ϸ^M%QNW31TA4* +]cOQ\FFzFKUuKF4{8de#?ji ՙiic{gIo;T0t*q=Uu-FJ]Esq(= 'M#Gsir7 s;OU`xavoY{U;rYZF=Yr}k~h/yo۳'Ἤڤ5.u%_kp%%ɥ7Oj d+"'|Jp0I\[Xqoywg:ܽw09; JSi'%[m#ʒ!9բ)Py>CfЭ7hZKrb6hHlRIexjz~OOXY΍3tUO&2|wc Mij^R:70CP8*A =7w"բ:ySmUPSpQ3()|Xxej #-wӎjbR6ic ż>n19t뎕S[zaK̘9ټs8jxN7` vn#+ޥwh&)^P-F\`&VT*9 1r,`6cR٪VZ^CnĒ (HI#ZQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEV!pAq'/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kG/TP?kEMEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQErSTƣ[뷚uƖ@wi$*_]'Ē^ XE = Pq Xé|B'MB;calow=HCKI6289}ީ}]zugg41a!O!8Q 'ue,M Za;g׊g(їE}ZK7$bmv$d0˞M? \Y_cy`)3a@\ 98:PaԵ7"F74pF@^qu6''$c{9@q0 3z4 mo?>6Y]~I5ͬ h*+, t:8dhcI%T%W>^=> 6خ>5u6mݻ6NhL0ŏ@;+'-b5&KA ňZOs}y7@>Js9ѽO:(KA*J Fq}pAl+67= |۱v8z:0[_}|kCo79;ʙ$DѬ40v8±OyuiokyJ+aX1+\܍^-Ӫ++ $f;Zh! n_d $ZVPza^}jW/^i7PMjf2p `w 56LA"`=ϥI\t뷱Rfm4Q1H>d;UW8 W?s}yi\]5O&EI(7m$;?Q\nbHmۍB;WfBX(,3cbk+LXt{kC9b|(>;wu_Z$q d9 8\lwraox-(˂ zKmF NHr:LBWq@[j7O f}zro+H`df 5jzvvڋ4ZIUvTW%iw&.+ob=AK&PnU#kr zZK4:A 40dѨ咛9zPmEdxkx~׷+nnB~P f&WsK_<,Pn vR16v9=Zcp΅9fTfPpH\,jmaiބ<\1v=0G9{}GT`ԧWɄ8i |#o5MClk lib,|BQXմژ k7T_ԦVL~aހ:9T54N2"wU'+Um -aUg[vY @ۙeمw8ڊԺκ=HAysOSbAmv~ɔmf*Hh 5~!K/x)w|- HLb9@XoejܚjM|-EpWa[`ʑy7Qڤ^\ܖZ8H-YT)ڭS\[j۹K!Ťv*B(q=wȦxĐH#td`AErzzvwq!vr4ʒ]H-Ҵ' <ŗza_,y' +Ե-SMR}ylRlA)xs=k!-%eh9cIQd>Z3_'t[Q 3NM5z=*x=ZΊo\E,HBH AnF1!1((((((((((((((((((((( ?\k\!nFHC>&ZgzМ6!t+ zwӥH# #*IS:`a6qؿk`}.#g'ɕ6Єpr9 |3s][]\8 $R؍S $gfr1VDҥSTL}AʼcGk/ⲹYڤY$$dWeƫiܭwG rץKm ,Ah:==c YhLb9z%+ [X$&iaod]F@Ke00ȟO Jc i hq1O| ϰl>ِG"c`)qԼ*ڦ ,*PnM7,˜)=@NzlwsbxMع`Dǧ\g:C{82$b2|@uFDҬ~-2d&sz%楩6[1ě .aBXB3*A=Vo?ϙo:(P'"Žƭyh@*|p8+MBm %:A湻jVR&co YK5(Am\꫕>"tXV5]q:*TymqglQ-*!(`99-'>^Jԭ8It貾z;nֆ5k+{89Nn/ZSjek4GC{ڣʂ;n8<Ҁ:]cHY+(ng*Aw{^io1h#Uʀ0'ZL[{Hb Snbv. $r:XdeXyVf8A!h-4-&ie쭤qD2n99t!B,1*Rr@p WO^'ڢgxf /t`#d4Z53+'E<߭eO&ck}&=paK'Qq29$7#+ @_zl3i8+sI$`Yv 0# v'8='etgK0]?M.n ܻ`:un[OUKIT'˒٢@1s\K#e5-8 n\@2=qOk?blm1q"yÞz='M>8QU9@:)zvm?{ HLeԑ.v3\ů]f .&ʪ^Y|̻_i_:|[ǧ*%u#Gf@˻'xf *( o4R4CbvA 3 36 D\4ZeH#GD\ǒbq 7}bP3< ! T Ƿ^yek=m ռ~ߐx5ZMGMtM؆ - Yc5ӵfk}BU&Y]`3ASߜRWhso2 8I!\\PIqi7vkwOo ȭsnu衳{[&ffUB<19=6"ֵx M_ۼEo@3 VbH01Ӟ7:!1QJ$ޙiiVfL.$0n61ϰ}5\. SiQjr]ki" #!q|{q _iQK5S7P ʳ*niZv QLTʓ®msNlm4PWVֲ[_[so($S tqA9|;imk>a%ͼ/j!>a Ѣ"6#F^De“rx4ưuekXYUXSvP9wgnLnMtʤFgcigQkۺ_1e }*JEEgqh[`9F3:qt|Z{ܯX+NOJEPд{x/4+m`[te ֖ҳ2%.8S9<{EP@ C^Wm y] ~Gڴ̶F#R[i>u,RO&cgsq'p8FdEs CsK2HH4M*{Km2kLGn,`jWO,4oiVVPn-[DOSfD 8̡@qQ>'4(>5~Ť}bɶD<>5Z]Iv6XX G;[4P;}#M"`6  H8 qK>ik696B?tuV <=coe.`; DުžOJ1"45舠 }J Lӭ!yUy~4YhVCiv2dQH=At($lY0HpHը+ i,B(rOԒM>tM)3fo6t֝aiPiz}hV=vW(((((((((((((((((((((( Ri2{~srIv1=jΛOɢiIB3CkZ˶ޕfȷlh ;fIbWNsׯ4{MKobHlA :(&eV:3[)D^%*y*{$oZ^[;|%m@;32NzU(&U]1y Q.H#5`j&i(@^ I$P1vTPcz}$ᾚ9upǀ;ަSNgX6"E'lJ$OSzU(V垦k^[K5ԲGuʬHA;@⧲Ιgo<gi~a<0)NZPBx_L]:KsA#+_+/BYT<9, Iٚ[$fXf${zӢ3-6Hn#`p I!=t8tӶI5g7\5vƋšDV0yW-*J^M,)%q#9`A$#9|+%.$YeegP~|ERDZ1\Fa%neY̌w_ʢڔ)$,]!%yy=s9iQ@`(;8p)bv#kI XTW܂0;ZP(-^%ѸW`Ix\qStE,.w\+uS6F@=NǛšb6G'շ.ѭ[Ȅ3px*VNQr6R@+c5J_ R5$S۵ѭ.倕!Xx'Sk^ϛB'h]^UE2W;wc݀9<=$`!Gc1hĒ ''|֝ueq;nn'$a-e "#6Z_bM-5D..@%N+*؋9Qɍ  G@8V vڑ R@|s"OO<Ԣ Oo W$tUYDp=kQ@JKgHpְ!;ʇnG^~y?4,{\ ZM˜G=xJ((((((((((((((((((((((((((((((((((((((([ fnOKYYTVpa[i=oc?Tt ӣk&7Ρ?AUҸIVw]v3thYSim\^]eq{9{2jIt6 "I.ydJ'/'Ҁ5;O?? gFʸH{"O$%O;OѢ.2oneTG%*H{cY"tpYu)`zwi=oc?G]hYv3ta[-ږm -Ԓ/#tv#kb ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (3?__>JѬ ľ_kˬd,N\+ Q#GEK%D{S?q){CHUX` SHzZc9ӌE[k}?|pFߘu$WڝxS鷰[k3y/cˏG2#IhExeaT2ir٥={vB\``LT夒<]NUD/5eo7_Mq9$s7=JWYUo9F@Hۉ@҅^fY;OtF-zК^G٥In۱sHf5c[m:ܬv@ŕ11 9♪kkME?Fm8N GZ+>р4 [׏t]Od G>Nm=M5R )-#2:pH ֹx{L}CYʭ)V` @OSW u _1j'P˨#AuNUiqH[0MQO€; Zy\q#E* ){5zBCZ@&fgM2Am=6VUԼҢdDB੍ A=hkJԡ4ȯE\aO2Z{+oMȌsjl~lk iX#0ʈv*_$2qjW[UУHJKgʀ: :K* 0Ͽ5=yZh60Ǥq,vraTb)W$<`z58YOl:)d1r1q@tWj6SЯ5[Tb$E%dipO9zIqj-T}>i7 m(άaprlc@UQ}ZעJIm6ݪHs\ůu[*k_y c.L~C)֖w_-P2(Ed(=sj>.OEn%6f*sOj^݋7J $Evw rpk{ x^TMv{zHQ\&Uյ/6ا%*[|naR0H=sV=Ro }oU2K#KyPg6e Čz(u}=o-E *OB{3I*C,r<-EV`C^RV٣i'OOI%(lۦ}?'ɃaЮ_O_^e3H$!d"08 c'(:}sD׮u;76&%cFBz<_νZǭͧY\F}YaV \zgPUEcWSռ?_+"Y9 ۜvs$iOD.౓V6Ďz1biu]Km&eQٜp]X` 9qRo#k}m4x'*8gnG;g=qY4FKH9`qY^v-2STUkY x1?qG^k3:͞{hdoufzxXP{Erյ;]vmf{XnXMyB,Y # 9=8mGĚ:wiml[^AUݶf##lދZi"Y"85miIλ}w_K9/](_ ql/l}ItsU4٭XP@FİvW}j"k|mF%CB;GRz~խiW ׫ʶ6Dil x^=8㬵Kmj׺m]:ؽb!vJPrbˁ]QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEV& ֝e*Ie*HLrYFݪGFq8]G9ePtT.fhb_2;= G"?j׺\m3,E*g1 :Ma_քJ֧UEsڴ7khHJ(vYI!&$:vE{,v=>1}Y'd$t8yKfʊ[^xosmk0]c9$:Һt #Q! }N?:TRmF3i!h|MshmȲFB2W EJ\l֢y@gD>HbUF*8^ Pܝ6IlHBVnSy;^oUKFt4V,ڕk%mX9ʂ$>WAg5*{-"pHv؅r8sPԯh.o%g˜l7~U-c[؋I/gڷVA~u%ҶjdبIVf\]Ykd.3 [SZuoC?DfFt}3*UR\N|L*6{[>ڮo|yzT߻mB:oI޺ ȥײ*޲Il~6M]kT6AST6A\i5Q@#KHtvKQ[75-QEQմ5HDH"2x:xurۛ`nszϡ5Eb-&Os3Gqt{xf߹OO }}Q/iTK.-KbXm$v':sxzkYTW}GN+V—mk >ZջF]|;rLdֵZZ$2\tsTP3RRQa=Ŝ2"]˃9vQEki^;bP_( Oؾ-u5Gq$3܇$`$Ӭ<15 c-& *" gkn4Xu<yV60qA ҩZxF BQ[Ke"D^휓Ec~K05N+DQA ާ'eBW5kx[bb Fi#-ךۢ9 'Q(ŝT!*A;դ ^݋ ɂ426л]`6=Z\jz关h*r8^هP׉Eu^}J$!Aǘ8%8( nmcڀc$I(<#!#g~-KlTՊyWc)lҳ89:cW_EcXr;k/ou N0,f0bUV4U q𵕌Z4qK; 6s) ;LViu,DQ+Fa@% sv8/+)mRԚ $o!70xfn @]@ıO1L;K*Gl/lky4yU**AV6:7ww& Ɋy0@QB(P2rrjo{漣:u-| L1Т1G1\PLgs7+sb?![TQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Z[_[W07G2Stur"? 9MiQT$.nᰳEX- UJHc2IӬZ۠$P 'wU(pb>kpu3*yk$p*\ch g4sI>V|'ɞN8=B~}2YZ m O0VN1ҥDXTB"T`;KEMVAQ\Ayn]G*Vh6Cj2eYY[jq*gT WGOkӭ͎Z@2}JܞH)ǣEqiЧ%R6 І m?>&(ii@yj+of({ƚ^i%1Kwݣ6@Tq܊jMIi1$Moyq,JUI9 $tٵ8lE0^pTl0:. 6beĐ\&(Aٌ28\0'FrhF[ޓ-?hx34]7)F1r3uۻhp>i"1fٔ ۰ljew~cKQ&J\qd5j-? Y4hVexyL.:ڀ"%Cku GN01w-%}%lEnd6_qFJm#W|#M$ 4V]N#x T.owS ![y9 ^uE[[.Sk| -=2. Q|= _Uխn#1G3F8;dc' cG57V}"; ;VgL۾ЭAW>^c}0ID*Sn1v(ng7wۜڱuvm4aky6'w.X>6Ekjv:ƌ-m[kZ$t,0uV*A6*φ]k/l((BCm2OA@:=;-9+ ,6U$IR iOwEi.mNhDJF79Ƞe9ִf.!zaT#rW,*z+_ j#:N6[;o/my-p7n랝 v0K/d m sq,q<`@l{(((((((((((((((((((((((((((((((((((((((((((((((((((((m?>m?>&(ii@yj+of()x`2Xn*0 d~bEPEPEPEPEdEtKfbɼLmcl# tQEQEQEQEShG$VxCW=2;Sh˙7#'d$ԔQEFn!"ʞyM-q\8 (((C (縆T%#Q:(.!i!TibǘdddvI@Q@Q@Q@Q@Q@')#IeDy[ljsp=N?> ( (//mtG.a<{rO'X2R AK@Q@Q@Q@ O%r:FGqC\ŒTƒOOZ()OM*#J# lp (QES&+xk(R#UP:OANV dzEG~5_P{{ismꤎ7"[K$M/͍@#e*2zO~+^X]C+BZC&uĞj)Ehܪqa@$;.F. $'a󳟛; O&{ڴ`1h^h>0MVcctW&!X$Ree{P#ͷHDx.+pqx1GLѤ 6Xoe;S$6`4]/O{+ 6BK *J{ K6M"@F$2{da@\RKyQy\N< HR:zUM{Y )ͷʤjʸ9yt6RPbb[UAu o, m2)E,q\=qwv֫Py!}<[ V8 琄1@#jkZ-fYn"!U[.>R%AO'Z:&y%ޙg=xm-kR01Vbma(< m @wap6]MrXu9dG='2y=|퓁zYZvУEX(|p8 2c&ŏhqNUqj-Zשwq&#KK3~`rKvX^kVji\I,qBd90{Wiٵ[+k7bY#3Q\hZEݼ6zU۾ccoU`q@Qu-YuG+a$ЮBq/n c$k^$y-ry~lH{X>vIӎMta_4/g٬ i-awcgIvMojdlcw~xuc7 N'$#۷Íޤ56c(ir@Cc@QV-]*[+]2I2nP0s5$mo|VYl gL{GAcêkho,1ew,wrFQ@ elȮ[$H{+tT-\1xH=*V΂AsEnFV&Qz sm崖p<Y"+x"8;CPү˝Fk{Vm >a=nI{+}6UmFI>,kh^Q&ݻy6q]HM=J[9+ t\`|3 iYZGNIN>^y9xJPj5xJmU$axIF6~La Erwn =k}/OK,myB %hT9\d=*Om+hviX|w<qPꚍqnx'+UPք.\ 65[vphB5&(UTk4 nlt;i%tFry$2sޟki++ [a\C rÁ}Mr[+[G#MԢ=GOMxUr8g5඾ӮgD;!ՂP~RhǧfxWX"M-* XIې|$yں t$P( Ȭ-!0m`ۡ)U}(nMA/R*)UwB.9bxh;li$1I Z2"F9cWrv,K(b 2AoS֪[xwE3=ܐgQ|AlpyV cPoH)TEuAgI;_Xo#ru3a(dp/_'f!fiV58 e>?E%KiqF ˻zwsؘ)/9m '͹w/ˎLZǕ.KqjTZ\(We$8 ~^hM=+""!{u[oiRj/YQS/>|gaѴk/-H}O 2X y4hJ{y-o2FOoy5zx T,zډ%hD(Sն䏺:v^h,mm&bTQ^ZFjڎew9bUr}rN[a.eY12Fb8AަnQppDQ7,]0;y'~kb9#r(a #S$Ҵ^[ gD d!IH=h Fi>ݫkkmi4𵐆6D{[ s:iڤG&A >PF@l«\euŚcm+F#P8o[K؅Iź۶$/yvKd"5Bu65wTA<]~ly5/d4[H-4 :*eG'U mkiY\^uݎZL՚5+i\b8FPo͹Fq4}t̚hb# 6~PI<]L:FmM[^1-p* i:SNkt(XKA `d?@lޭocIͨy\Bݗ2vApOfKAjak ?ڞiQ.ҿ{xtW*iM&`{6"'9Lc9Ú6hx6_E~|(I,@!£^{EwL7~o0|r ]W0#'ڻ;7y"hY#27#d:t$S$Ѵɖeӭ^AS |z8sU4Ffӵ''tP 1.v9 p3V5W\7V:=bkud,r R矼8_oXZAo 1[`8V"AhO +{OY8FTf9$0 =Msj>zu}  e2DdGl?yTqW%[bMק.΃*@D;3W혆yc@*^6WRI[ހ* =E<ֱ~4 G K+ꚴ_jzQ2Z(oգ'滹,me63[E%0:Bҧ1ڢ4 :$p0**@Oh&KemUcS7&䎜F3\vqK4vG`izF Z~kkAエ{f]F溆"w%fUPTwLzU;~XuIVv`C#o~x#M 'So #q巪$6s 6[ Pr3ӽ657e%K,qcln;|`1۽AfjmkV]Jh4 jef[/[NJtO?Znϵ+ߌ5 xwD--u+j&>1?Z$kseio$i5k(nۼ8qm[W/ﭾ Ks@+/FC'_ *-BpdSx}2N|e|=w>j-$0ܫ,k9UQG泞y.ۥ]!ݰX$mqwfVg??́[s#:zTAp,8>oڀ1FRNrf2x"7}¨Yws1 R?֙|tfR]\]MwrHmU K;'ڝuWױ^wQԞ[uwA@\- t+k_dPB䧙@]ml/l]^餓7*DŅ =~`@=wGF΢M7uϐccc'SJn$HT;8]I#>Q[q}w;]BvkjĻb tZ\_ͤg]̂FU$d }7P{k: x]YA'ޤӬ! ,q*tAt;@\kzjSB` c݀A#$9߂P.KkcЫ[$qpȬY)lJZ}Lӭn)+ꃎt}#Mf YuꚕoKWMO  p>nkFRtkk;zd$0]*<`c溋M IYlt+i&exm qih]i*5 PCn yǯ4eޝ.oc(&fS¨S.AsZz 5[Id"M6<8#[Mp9ύp|"; 7x[:{8Os*ڵı,[>^ݑ:CM!;x_!ma (̀S"m3M8:84@  MA-ZiV0pcE>cr{+e19{V<?}P8@ǁXOaBF4f8c[i $Iov54<#Iv9PJloai%$zт.v׍>ȾXCwo\AI4)trI͇j8RH dc%:u}.-7[5p_9FW :Veoqeo ~V6x;nk :KeD6,h p^jŽy󓩛 8C |76FA=c0kw:/ƥbl!2y ,: vRXYk=<4`{Ggupiu198QQ`HϽr:N+œP{{崖d */cI~ZYB]rs7eGB/vrpO=ct Ҭu+5۠@zldim-d%hPE8Gp=p"O t"leF{+KNY.um:ijP3v(loCiVg.W\ j[JM,ًk{uqp;g(e|M=R-Bb!ӠuTLN6sZ[YGOfclGp~TmnH'mOjuO&ٽnX̧ՈO_Z5_V]~+dK;H ,fH.H*!"YZ^\_is,ߥNbrF"&0N32O=+Q4=>(m\>\I9# yj=w %eF63(RN+k{9OH/m--ȶpҋ9LӅRjڍj2B-ൄdo5Ubwg Sua9X煃3nXrFU۞vx{;s1D*HGBk09UZ*I/g3e.&᳆Y/^"r<7786/ɫK n4py;ʳGFf͙1Ҙ=j&14s3na̋61mҕ]?[{]xNR[s,*1>pG>umwv c}r@ c=LcY`.Y#3c32[R![,d, bIM'RV[;솩umُ;ᐈ&?>޳m+=[г0nhoni7H{pG t~ OdXϋvI[gC;==ΗgvgXe0SuhFJr+[ vEۻ$wS'$֮4g,%IgfLg`͑ȴz^0Oq#FBAp3j))yoa穩s֟"*43*moT^ K)F+*9Ǫ^_^25X}S#y oݎzw[J1!eiFGOJoimq,]YYD#.!wf5i&ReNM_?Պ cwp"Lvǜ8㎞=sB2o0fG;g@vt/ H$den $cMM2#X,*nxIO*cV* -ZJNnIZi^̶YUO,.Tߗ}NV-&Pʓ çI#;H| XtLG_W7a{[C[DF 1 z0~,oHJH>FPT8 o&?ȣɏy+>Zu=¬b3ZKw0 8_#{朷,WB.y15ELX6~(MFq[6ƪIPK6NQ],͛yF#k/'onqNhKɏy(ck|OP#4[eB̜c dc=K>ΰ/#<^teT%@;I4"&?Ȭiwx9!6` .'##':/Y^-ה[2\ZȯQ1"5<"&?Ȭfgb;[J" 0SM_v)ҭV#3* ّP} _&?ȣɏy*_R[Ye Zy$U(@$*_wI"$f bZ4"&?ȮCB_]i^[RdǦİe!(qnKh-ew#/SI /&?ȣɏy+>{qO ȑ̡sf8v3VhDZ`|ȹU'ʼnck|<"K90B_;Sn8ۜsӚiGRBRFvAsZ<y15Eeikd,o<%󉙱vo#n1Eϊ{[{y3ĐK+R|\=5|"&?Ȭ7ĚVt`ӮĨ' *z7 +H8'%Cܬ\x|y'evK~4<y15Ee&Xy$k!e%od`?핦\DHHD ]F<@LG_VI^ݴcnYDh #'u+ѵ+m쮝7M;,T d9<y15EgZKkKy2]X+HT#“n}'XHt͌"ʣ(̠62;4"&?Ȭ|Mgm#[o$쫗)lp&s}.%1S *)(C  i15ELU[XX.&BYeuR@Vsk QvY$M,ź[%XX㸨^q@?_Q<77VtZAm.9imQ;!K(xj15ELX1nV:E4k Jʤ#Hjp2zsSKk=|J폲KMxFq!I /&?ȣɏy+Ӽcgj2YC%֒m+>Q9㡭koO>98B;(% $P&?ȣɏy+1MfI\Ƒ%1a|fGqZ14P$9iiI;ck|<"SS\B.$]EF 9]cRqT񮔺MMtm-#үpv|O.@@LG_T:uj:t$ `ѰG*3ɏy(ck|uK W]_wg4Iu 98JjYMg_Q<(LG_Sy15ELO<y15E>g_Q<(LG_Sy15ELO<y15E>g_Q<(LG_Sy15ELO<y15E>g_Q<(LG_Sy15ELO<y15E>g_Q<(LG_Sy15ELO<y15E>g_Q<(LG_Sy15ELO<y15E>g_Q<(LG_Sy15ELO<y15E>g_SQE {T {PQEM-#@75-EmԴQEWh2 B:Ƌ?c –+ :κ0Uѩ-ωQ8Ϝ m-s_[Kk?׶𻼾6䑙ݵI&A5<@~_MJT46 ^l7,b?cby%eGHr0+vo,ﰵx%e$xdwtTWLs66j4;3& ̫p1HA' Ꚇ6fQ $3gk9"Yg+a ce~Ta["'eV;p8OK'5s~xE ϥtP7_u=\soł2cP"ҼT,o>Ⱥ1ۑy̙݌]7zbKrMt26ƥ>aq î+JC,tqn֦N{|m!Dܯ= tTP7?gQ%{k6:ucRs\ ! &B,!#@x}9X Ԃvћ'Ү_Xj7BYL3-ԭ:Fמ7Z? Z% Hm B*nփn,1TVFl[m(bmb!@Y $|@^=!A-22d,izg=F9,4]_˱E[V b1T9 3+9; MModXƪ8ܯZ÷o!}>o~0qKe}JYa%c3*: m#'xhR=#PѬ4X&t-Lqnb}ܩ8Rh|zd7g+Z%~ϽUm˟sӭt4P1qea{A L#[?#mъ4,Mݦ<$~VA~KEs]m}̇f7?ws!-y!Ʃ=)'8ך(cO5h0O ddPTHf;gUiGqsڴE# ݐ(39d/'хŔ/ ,mU.~ONCQγ=pC4r,4&=Iv h=NNƊ} eW m;T!ҭj:sqȿ:+\=xz騠 SLվuo68#Y#TqPg}"XiZe 1ZNUahi' 9ɮ.eVM+Oʪ"xکYxwW(Q%I%>o+) rq``|'z筢9m+wgŨ{6eG)Tc!NG85ZO_͡'bIUXU2 RI#8f=G^ʊ4 OLM[Q=Χ< oBu<{4v Fp8Z(UժOm$(EktTfف88;Ia߇g"bOKb!&h jܦn WXe2 # Exkj=mn4_T_-I!$aJt״RFDL$n]d0=Ad!7W-lT6vmco BrrpMM@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Ci5Ci5Q@#KHtvKQ[75-QE!PxZu*us+öWqiq%e<T:-?Zu*_+/ei{uzq# V _WOJj1k|oc{{3M?܎ i&`3Nހ.yiW9NmZ ,p'\gQW6..-XPȎ@Ga$`硪iHx~WME `z>Xە: _ʏ-?c[:7ZklrZݙfQY`8 y*O\jˤirMqj,Ar3  -?Zu*-CO>T񬋑3O@ QW3]zg~zSDPt}=5ōȄ˦[@,@.1pyH@_ʏ-?V;mcLɘ08ܻYH$#}[iSiSiSiSiSiSiSiSiSiSiSiSiSiSiSiSiSiSiSiSiSiSiSiSiSiSiSiSiSiSiSiSiSiSiSi:QE {T {PQEM-#@75-EmԴQEV?9o \S=b!IA$o]e8$pz(ax@vӿ??ZE*;fn 2,ZNU@ _5aflm[k\cCNWO\ǎjRYIuw%MJ[-&鎧-OX,4l!ߐ">_3ykOG_ʹ Y6e lKg(]aD"Mw]Gu5n#n#8xQJOɑ#xu .$12G/WszPAWOT4F{帊;[Y5Tyi6Cghkٞ <*ɀgI9.ShKvx$I FGWOT, BK;I%He 0YJb 5M{Oъh<-(QHE`P-?Zu*|I=ob}"\9DC^;M%A/qv9ۻ>WOX>]gd9[ۈD$N݆p޵uNJ^(f؉m#ފ 1'=ϖ_ʏ-?bj*M7N8H10R3a ASV5dfUvmn݁ -?Zu*Mj ^K]V -b[h8澎Ub*7@ W!1'K.|BQ@]Օܓߜx`ϠXqg-y%/$$Fn%g8=~+хڼs4 *\4prA^Pfh:lZUق& {< r19xBB6㮢1/!rE-$d/ߏ\ \ǣ_@f c6][TzJIQk9/ Iakz#ң 0@o8'۠s5渊;,5ʚL0TcaF7$9YEs^Γuk^euyB:l!zX3g3T<',ZeY5"-ā1+dI xO_Esz_,F<:六]L'n(Q79}9ne⻱/aS|2R J-1U4Mj3>]c/_aMR3&?a}q#X) AnqgҴ[F-t*8J勴ɴqlp:V+{wn,4H߳4Qy-۹ͲĠ|@]}h坱Ԋ5+,B\dzU{_m=s^ lĶ>̓$g##gaEr.iң$}9V@ݳNjëo i֯m3Jֻ<ܻNYnv: (%PKr5+I x#³_OldvÚ׈g)Rg&Cq6ʢ"s1Q@~/O!X:tx\c|Q <3[hݮ^Cmܩ*I) .SErv=* ,0m\<ض2aúa:mu< wumYWtPv{*( JvZiXnRb *6wXxKX{MLCqw MJC$A(7k]_0xm Dl[UK

"YYj:$G ڛdBOE`GIi:aVxmus6ߖI.5K^ow,rAotҬѕ`l7d(ZC#)eU$Wy&VFUb@R3gc]C,g-yRɏk BGl'tP$3=)Rv۱"ͤM[mj.c`9w-#>Q@{3Kdm%D&{; ,y= f=Fm6[ mtk[{]:_1m>o.Y’=I&*(&5/Kot MH&( QTgi9pզ DZgUVYzc*(е[N HiIְ5fu A>ׯ|=s,C¦K0p݇Lf*(Hn6 jQ@q)ZO{[De3Hb qһl^,Φ\Qgq:U}05ӭ>:eq0?*/.QRe%_!B+_3u-Tqm;+?TRviF0V~Nk]'NM@ߥ7[ N*W!'RL =r;լBMۯKWcV28JƤ!W'i9sI+)QRPQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQET6AST6A@QER?4MGmԵ3REPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPPǺ}MPǺ}MEPHt0vKU6NG! qF.:?€-U1u=?bz(S_F.:?€-U1u=?bz(S_F.:?€-U1u=?bz(S_F.:?€-U1u=?bz(S_F.:?€-U1u=?bz(S_F.:?€-U1u=?bz(S_F.:?€-U1u=?bz(S_F.:?€-U1u=?bz(S_F.:?€-U1u=?bz(S_F.:?€-U1u=?bz(S_F.:?€-U1u=?bz(S_F.:?€-U1u=?bz(S_F.:?€-U1u=?bz(S_F.:?€-U1u=?bz(S_F.:?€-U1u=?bz(S_F.:?€-U1u=?bz(S_F.:?€-U1u=?bz(S_F.:?€-U1u=?bz(S_F.:?€-U1u=?bz(S_F.:?€-U1u=?bz(S_F.:?€-U1u=?bz(S_F.:?€-U1u=?bz(S_F.:?€-U1u=?bz(S_F.:?€-U1u=?bz(S_F.:?€-U1u=?bz(S_F.:?€-U1u=?bz(S_F.:?€-U1u=?bz(S_F.:?€-U1u=?bz(S_F.:?€-U1u=?bz(S_F.:?€-U1u=?bz(S_F.:?€-U1u=?bz(Ci=?  1@QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE{%^0*i(7Һ)'IΕ᠖ȁS(3jυ!e_ݦ.|Ѿ82:eN*KxAom{\J֯'9FHϿhS/fQ&!JǗ@灒I/dukBf61\ݼ*˄bU+(#:+SΩ]kz\^yE|Ih* 1N*ݝ׉+=N{H Z\4id$pf;֕r [?x.X!PI:!0'2r}IѬmRh!L.F I"x?(kKrHcy}jĚz[jMuؖ/;b{1۳chA;+{kwjkU"cbSFy.lm5.~1,* ;MB..pJM Ѿ85Z_m丳լn!rI2NGEQ#ykj3prGYGc-ƗqowvkiI8Ozעdl,Ľ(] Z|r@h'\җP jvb~nٌۜ/QXWtz {I|ܮT1BN3ڮ[kzUZgp UK=[Nf-?P y<>Zƙc{ioupq NIҀ.QUbusy~%_3~\Lm_MMUt-VrJ&sjES]_M}Q-Z4e2LG8P'494tMpHcyZӢ˭Vd;8ۏQٓu3֟_cmm5ݿxV/ƀ-\ߍ4_Uhno4[uC #W\it>G:!kI\iQU tm]oEWծX$21 ʹTv9Bs@V&eӯwƲ@"1­H.)'j]llw% E|0G J*}Aync(qƬEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP5{ SN=Uv`sՙ)<3ڬ:m #s}q]j5Էv+srZmêHr*9Fϟ}uh?l]megL \WqbxkZ䠃Xn5t3H43r۷܆P{F@nnp-=}3W:m#7d1oa+toS=~lJd X~Nkb-nt95 8hV|]x`txz N!"c!"ʒ].WvP=x]j5ٖ].s/tQloK;Y$Q6['1'dEqˈTkar= [\Z]jWZC[}%6i6L>fYw`rxQUGPT}6K2Yc.4pdmW'p(X"PĒpA<f@mhw pA "/.{*@pyn4r%V^b6ؠ\n E(6T{{Sk%s96%"᳌ȣimJc6H|66IFSl{Xp;(tilnF[T-t!PtBuӏΪV֭Қ5u Xl6x utP6cOi6Ir3"(8n"&Y}alȸ:*(n&}# oX˹lpL8l<2bW!ҵ-2K)`]˼XAq8}+PӛO}&VOMt &{nb C䌊Э ]b$Ҳ4 1yl[a$9Z=x,x(1^=d s=1;iSeS3?@oZq%ݮdlH_9bX!@xq啚J+x-g#]YePrN=}+9domRSތY3.EYNKmp%n)3"R7|{}kcEH*ª=)h ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (+i{qePZ1+&O3^uV|M{HaeR ~;}9T(ƭHݜK^̫{VWo,LTW7MRmS,^Ky V? .1jV8fEVvJqRF|]kZ%zU 8[#.m"J^zgh8=kR{/6$/s>5(ma>UO*JQ$Y 6\ssۥvW[iGMԗ:ڊ6K2s`/=wSg?f}+T{FWDF<6}ހ=6n&OoLr]Xj\8 @¤O:ŖyeᇞIyb`6ۜt2suEssmaK{h&7^fAyA`qpI~iW/5 <V83mY;Z+~xh5dks>/S`vJݰ} Zh^KmF}ڑt:HAx0Afӣo3SHHXO IӚ~${> ]Kk۝n~|F 8zǚi^ִ k6d=:/e Kd3;}@U%[>KAq ,¢`@9c㩭x:2_/HxӨ/'bpFrN8OMG;6ĩkf}KnuUCi˪ 1E.hf_47j?t 먭,մs0heWܣbySw? _N1 5Rw9\%q+hA}KBҞ+}kMW[s;FM)KY}i}n&7p%`S9~lTV2~C/VS{G$g@T(`ϋdkKGT|&A*Ɓ2}cRb6zR{ƺWHYv`}{c隷6.}}V 9l캴7K2y <{r%j[fk[Bw3nrgW!&njR}<S6 Qq`{UtqJo,7Db Y0 Q@׆eNR]CSﮐFϜ(8fq=.P{%֡]p$B/ubrh pRH$}P ]#P/-gT !X(\.>v7ګ%H tPUB0$5ج֎GaݴiAn$&8-]4XPS*ymgtb/A*IM> 5mmJmg *!UE8& dFʈҶ0z / |9Y$=펟-p^VqeQFS޺>h^romYnnrL븢9Szf6-jڕVynjF۔T1|U.@ʫ6P)k8ᎏV׺)a+QW(~JOHu+e+EA"l)pw'uiYhmԐM!x߅WSLxP`@`pG"6kmOX[\kiF /w\)#wƻ (ឋ>ڋjB .Ԛ`ŜwQWHo h:,ծ.n$$Ӥ~c~e @}:ڂx-kf Ulcn5ILOyr<9]cmP⺪(o,KHZ ԏ3VGkSڨ?o]Amu!19>WiQ\[Zsym47Lc?Mj`{Iwv%aDZڊ#ڣ_Fǧ2JTęqYzgM/LLR.ˈ쫫ch5>w:ιwkpѲ5rQ_-u}OZPGqޕ]@ 3| ֊Ŵ n5mGII/U"f4ڈ( ktuMV; ^kH t!7%j쨠5O zfi 0aB 1VAq}ii\SHsdd=|?v ܑMm$YqdCLSWK()9Q0ʪW Qs]! AP=BEo&o$3iS\0;F@G'}-6+N2 RPI`H\`#q޺q kq4fu@a)$G\>ƤN펝ʹ2J>-ǎZMkI%XCHX> (6a:e.\/cPkdx/M\3bDqp3ں (|.햐^Khq# UrBjh (2A*H,A%unJC ܒF#q sG .|f'(J(I()Pچ,^GWskoyk%Oo"x@ЃQ M)44m Js'Ҁ9;+kzYplS'y ͤgjwЮr!]5c)RwYG+=8)#[HrID0#8TrdZuЍ]< es|du=z3jTuXf;|ojdЯG@5t4S:}~BqoqՈd$YENIrhz^C,3Q[]bND`OOZ؜_}gܗ2oZ(9/6KL7c8f9{:1Z#b.%3r @MqvZ 2 bglEy<08m+hviX|w<%kk \?3S+tu>8"J66KF͑ ~\F$A{cƥUQ`?,4GۿyxqBڣ#%*ѳ:Y5 4ӪLP ] 1@zO,iufѭ8U偖ܸ[E{|D-.ЦM$.N8k-y{-_-D`lf9=}i &(#bĢ9'~R-|)%cmm;ckWZVa ڕ2 +<3!qq]ۡBF4ЄjqASմ/`F1sn̚qeix`2Q^(;z,-BAkҭ:h?.1oP[7tVZ6zvih1*-q ҦEIGZ%}_TQPhStg^p@Xo嶸ҥ&זN.<̼lau[hZEz]7YDq[ami`T@Bg>9GdR_>1ʱCnǿ4g/&֚M0FP!*_'vFQ8WUqy&FH tE9ysfcQ#FX :[JE.Jyssk.[4+%SiaUmI6y8LAΛfoL 612q6A-m4 "f] 7@<7&%k^D. srA)w8xSMvx֗vfA1;WSaX6I%viڸH.n%Hs(I8TI GE2uuaϞ8CB ʤ|2 O>kZ^_iCqc!6L8 *G_jܴ 9,t+kk$ܠ`q:EmR#UNT pNG D/oCGg>1@QXjƠmڥŌ$nnFQx;ANQ{]By>3 k.zUԖhllōکMG-n(F(yR*.*3)pw_cԄpi](ě%@Ӑ溛 ڍݙR۫A(Gg+E)c %a )3O0Ἰon%ZMb{E= r['wJk RG&m-+E,70Az5l:}.خ^2F9XLt{0ҬmᄾM's=jKڋ"ei14@88}i4|ėr lCxijle!T Һ4>&U_,g >'eًK6H%@rϵSdR(TE[ Ux$VUK]UV.ͩDqqoB9b8nG#Uy.@,@Z=#MT}N-:/]v(Hw@CR{.m4EFLg$+uő]h\=43>O,75Go 6a8@T)gjjе z`tJ])n5cwg K+Xҙ̐dl 8|+<_y}7mu{ִP 4K*$$LV(cD@dž&m;hqi n2#Ó2yTfԬcRsm>un^RyaL۱vT1=0Dn$3*m3jbZ*-!Q̊1s9<䭵]Y-F,'4 q[a%@ j7VZvI-]rQSM^>mueU o7|*;)Trxj[h'eiFPʥ 3ހ8F/M֒nϝ$pH^H̐Ğ8"[Zj"ϫͪ3^̑)LF;}u in^"Z>D8ʏAUk-IӑSO,d2n8ϥsk4*Ύ{t%2" zz+_j^tѮnIR5E`Q7szv.aq$:mҳtX015~'X1_0u'$܂~ns$f&XTW2A  #✾YhӰyaqBˌ=M^}>]8IiٔͳD {:mیcڀ9{mF KoOsGMPŜ>rQȧqmu_k. [v["$?]c4Rii6/59 OX}$kiU "@cJ.=ATԡZnD+q 9*_sk(x<3jTuXf;|z)t}2}N-Fm:KFخ29>2:HN53ڝ@.w/gq_;}6-BXZDPs7g`ߜct" >('@$ p q{M5ƕc,dGFh؝Ŕ'GzN-Mgoyn K":Eq^gN:ŋIߝ*LʊO lgvTmGWekXiLK| 6tRJ-+O/vyjʞ\J08MC>csLmnE7"hSc"cUU @: ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( xtyZOp #oUmGNlPM2Bd= ?Vk-@\OO-4sHT؆O;95mwSR#DH\Hbo 5t|++u+100wq1LҖ[ȖI4ZйwW Ԝ zk4/c*& $p%z=;jjay~. ukO4{L^fnҺ 0UIV*s`ϐ:ss{>mZK~I(u%d EZ_ oX$'eg8ğ=ޔiyQI+}Uv* A@V%oJIX4 =(Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@ VzY9c m`5#%{)E-((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((4{Xgm\B .$0I Y؜N9{TV/<g)+xQ+pAAsu߄m&}.th,W}2,ŝH$~I%dhֵ4ZeX[fE̐<0#)|#t:歨_ibybd'UC^N2xC/lĶۼ-eUz1"2J=p4KI9.mAR6vNWqI988tEK5pzzQկm|C2ĈGX'x A [i$m%WHao$p7ޤ׳i:}/, ۼ@FEnww jseyʅ^rͨR$K: cs@i.u-r4Y.")!Ǖǝ1@8+CQ5_6yuSqnaE*ʥ.ďa;[ҭHԼi *M+nAdÓ d`C4ΡϺfBv%W?.XNQ[#X'B /#H-gjT׊-E%r O8= ?^k,5b+I *AR F5t17yIbı u9@{ul1t"Ӓ9®$ǽt^ڭ!lG]}T6mh閣V6E5ԑ.8G#;+c晥oacihk!dfݕAa{(kz֙ungsmg )?#ϦEόr¡0-%1L&cpy WQw_k-"G΀F.Km>R)g7-)9.2R(YjH@d Mh_XzΫ[\ilqlXoӞ:;(llHaNx ۩tB7C>hX [H ⸈5̲D,Ģ.VBcī'g8ր1<0Z71G|RHY cbdq8s׉K[[[sw8% ,ȫLIr7[=0:׵o>Ca-w[ HJd;gk t%w4; 1)de{ ̯4s3on]@-oxcU%Awjm-BGFo8".V퀞NB$+8uhZw!Y<Ffc 4jIM^{Ix*4CN:b_E8W+vTrHR QӴ{834b&{gr.l׋/ĒZ[K%[sed˶^X uNZ65p aAsR3vw6㑣o:ܱy([A\os*W,eF2,24Jql.y>1ֵx M_ۼEo@3 VbH01Ӟg騷ziGU̫ĸˌVxaGHX՘@TZmͼpMm8Ͳ 6vsH=F84EZE|CE{FTİ'x[s[3\,sc,Ѭl}\_>ぷ.ϧIc%$ HBۋ[q vBMlHĞcNo'ҧӚM^c$Lćc5F ק=NksW!kY+$,Q~_mhkFˍb) Eu]ȣwUк]!n#r uG4=m"rG,QX!I$lKQ[[pDd)S,#=Y$MK@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@To[NҮ.@L+"q `w,:5njbjB[vYXnNASb ;MXEggw},D+؏Le ``Ʃ=iP0F/6Lrs|F<'¦=WPKՙyBMDwuo_ Et+vP`8% u( /^\m4lp闲yv䴤/60'I r>3l9ۏƝe,e̖ReHP3QxF9,1 DHe\ 'bcB_4m'Lk"{n77Igh`D@sX&mmKPgnO*PgF n>+Wn,-=,Hx4Ț1=71yw/w\[` Z} gվywrɶqv@=~=徉 f%MްHmڪ82+@K/5q((f6G̀Dͤח)kc #GN<Ԛ ]on.<4xE(й =Nyyywgiqj<}?)*9 ̻X[[]8 ]AbWĒ0xC#l6i/Մb0Cƅ' x.!aZ 9_v+?MĶl>͘V6z[beynCXO̤]=NH ]Ni4rp-ȚQ[%U>Cv7zY>&B<ȭ9/4:+}zÑ_5X+d p8 X7ywFknȱ$,PڛkOsr#\{Hld ^dk,L:Mj-&X?7`1d$cs}ҽyK/yo k5t*p V9PD.]sshfR8}hQP€rOMm!x14ilPC 8# 4O][[+y )p.J#5&xBOI/.d%y +JH*O3<_. |vIR8uܫU\wqUY"p#$*9|QXMqh[i%ղEy?6py>R7:0A=QU~P5znymq >^N@-r.5~_fdޡ ^fռ>Нo,}SUMq%Ş{lOo!` ) v+//~H,%U%ed,w6\tZVӡ^$yaSsӞxt~KIK\Aj [\0`c`͌`qOb9,"ԯg>!Jrm 4GromaV+Tp@Dݸ8^?*λGiak ͥOvJFXm(` r1IK '5.Yrp1@n;wKyoXG 2 ѵumF )h2œ<ȶ w{+Z-]Hwc`ʨ ;澏UbD:HrvOG5'-n59tkyXHE$x(<)MBƚ> 4_ho]DYde);pjxMŭ6R =/FmC:֥SE=ޙ,px!ua#'6Qol`6cY e,yR<@SiEsi$0۹݀IJ|Q}i1]jM_zOтlтG^kbHPI%FQ&đ1GpAAVt7!]sSi|(@ހ mR]'+{庖;T):}WS{ AN5fo;@ Npzt5domCd*PGp}ƧGki`q$FFTHzP 52Y,mZ)I 'ҵ/][մ)I< k`d@ҳDŽXn~/Bzy.f T^xjiri,HIgp8axk:{y>ж[E{Q+8<>l :Vsw-Ůk hʶvU'*0GTp.R˧1lbɳE^ '94,i+Iuq{s1[taT;=zhz{i>Kw7V &XjR]eg r`ޏA!)Tk9vbt?\oYjv# w[4h DKg2u4x+sKe,3NU+ _,3|Sygikm_NkIW1-vH$pxIKR\M=eFU)*FjxjiVNe 8U|˴Rό-Ey5>̿`'rmpzG5+TFIm /H@v&N GejuVn_Т)?V$Qڝ߼8xt* [ˋ}>\-ÿS$|ڣךVgFb{y/B"JUm b%3[_Y}Deeї>6]6H^gv٭ʖIh5"]I<ԕpVNz)zV+ة+YmXϘqwJp˱Ry88f/ m$ -Y-$lD=:s*oFW{MN%q$M;=rh RV6iPXE5UU2 ?(8-G"V"krȑH]U)#Yx^+zڎ{zPɍJ\[q{h^8KE f(g>h?@KEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPUuB-2ɮ'0D0 Hp$H Y!!%Ǘ"ЉSra 8:oJ#?oyߺmvwsP$$4!XR4GN6 E>3XX&_A;Jey6v 2@i! q"\O;U@xր!Ӽ[=΅\M^KAûh2e$c-Zo--m,'#)ڊ*co,OP2x8T֧ciaZ.a6$LI @w ^9x}texr c>{s1@5k,4Bě0,@C-6ؠn d Pi7^}>Irlk? j^!\i3A&š!i}a5Mj:TXQ`a{4!XcTGp۲$x]8M5^iw$6 DRn$7a۞Ͼ`lqLF1lI;M |?YP}ntm_ڪn~2wv-O< mqk<KG8\la U)|QWRET/S.2| gȭjaHm鴱}~HY{=1VQ\i/kmxGK;yfyז{n;s!Y,wxΪd`ĶrAyk3,qgz"Oq<;PwFPG8d{ɮDK$GHp2v9['} B t|]s0ۿ  ⟪jk=𪗖XUJģ}p:V~XtT\l8ލ\gi4>X[O8y%F9ҋP_w+oM~CbjKwֶA>Ј B~lǖ' n1kr!{ +>߃xzEQleO U_wA:ԍNeQM? ɻvM Xt*O$g/~_%~%@")n^Fw`{PhέKque% RS a<`TI]%GA> ]GmV}܌#~5wJbeyb=@J-GqIo ᰙ@"eQ&$yj/WtjJ9`hA=kh幉D8l8|;1ޫN;ݎZYRyeQ{f'P``?ZպefxǿoCyu6VdEā3'Җ]"K{mgp\z~`2ik"hSFB#w\37G:=Zk"t' q5BIֺ] ԥx'"CIOPBJ6WJUfڥp A44wWZ!1[- *sT,<2}PǢF#2'ϟA*Q6˿r,k}nc)Mh;HG|gz[OE~3mayYT&O%#`a=A9. 򥦙o%l&<ɳ׏֩6% {we^:ʒAYQ y<1Z}r+yw$m6[{.ALY0yG&rF /qJY 帒/;Ɩ9qQYSaR ;~=G pǾ\<g(T2+,m89#T[Ks+Ȇ(p,x$tcCAusv\edj¨Jy'_v[y`[Q ;4ֆG`w9`)~1mkEAm {8gy@ .G^A9t) ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((onedrive-2.5.5/docs/images/authentication_scopes.jpg000066400000000000000000002536621476564400300226570ustar00rootroot00000000000000JFIF``ExifMM*;JiP  >Alex2929 2022:08:06 07:59:082022:08:06 07:59:08Alex http://ns.adobe.com/xap/1.0/ 2022-08-06T07:59:08.290Alex C   '!%"."%()+,+ /3/*2'*+*C  ***************************************************Z" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?F(((l$1(3=I"5KL,/&_HѢ((Ǎ(_`6[8b9Y`dUѲz(QY~#oe,UZ Ӓ0z@iEQEQTcL#WյKngXX?4K}}hv2ۿ(V-ߌ4?L+}Ulݣޗ^1cmPEPEPECwwoag-o Y*I&|;ŐO7vyr0 ?Q@QEQUf-౞+L6*%U''j_Zu(3$N$sL;=kKtťo@>4n((*9IeHaK<0UEI'(J+:ڽίa ^ 4(~pu=Eh3*!g`$-^P-L݉ 5"8`32*hZ6|',x("z+`q+,q^~5lPEPEo=*ėZ [;Hijا;~{Q@Q@Q@Q@Q@]oPk$bF1Pn+b *ZڵƛۮIeiU9zn *WYKw]̥i@:\OK㷂%$UAI ˱F\}3\o'#"+{cM] -g+D/ .і'=Q@Q@Q@Q@o\sd2\2;O@T {k%is ]DH POEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEKѥH;ŷyrRr?S)F vBm%vhRXRM `zs^e OBu)5MvK}YXsuwv%159ۡǸ)VJ*'' ,SmE篭V|[GmkG,[$UD(JdgoB>g4].^]~i_8皿OO _x~tON-nmAsU_g;\^:t->캔6˺Hq_og5RzƇM?PԵ;0cЀOg- k=KQ|TLEx̶x/O3wg[ X躄+}JYW$䃎b_/d{mF+$fǶIώ$iAz޷fi{u5]S6mg[C$V0Gg"na0֦YxԒ1:KX ~gG, ziVk- FQk y ,('+~V¯>.t^y^* 9`B3ö>$GM[MJt m,{pq&wi=ߊznI4ַ-b5bc;( 8$~Z+CyDl5X%<5Cˤ6Pv'cGoAU}t?]]x?@@Ԯ"2ڣ\$Σ3j]̹奈MSPKحd휀9ӡKPo߆/Hl"۫<QU^OkI?Tt[:Hm}aC}Vlso/$6C߂^%]H|}SL,-[Q[݋@ʣqa3,\H$$qys.9t5sK9HuS.u+Ǚ|p[#'h^Ek OAX>3hQZR̶_ZAUo x?Xru;4^KBC0 }+E^/,z]M+8F K ΊǹJ%# _йߒkwt j6G29dlzCu :J4sapA cVyzsWk0|1߉ő\Aa}QXZH%TݏV.>;{K]!ڽeKy{7|As[%>.j:V<wz`fR%ҪB.j]N[mcIYRSrYn3kZ \Nw-|w2[my/Ĉg!xO|[9H9|K{³V>\w=k;K *u[{7$bY+~AOrT`q]֗'-?TG_}#Ե`T=~x>LH|U8mPyXD^1qc~$4|gi# C?|1N1AxxLӌԫs;[ߩ+O~ ;(hxdc8+Iec{gK;e$ۑϸ?x| 嵲`VtVol+xZH"7)q9T ?5|߉G8f~+l*dh> K—]w;TPAXy*Lj8|?̚MavTΎ-?we7dg`սI&_5+ ̲ϬxkY5R LdIdVO$ԥS^ER'e]ᧃilЭDDlk)cAMbűzɸ>?ro1nsZ>}M&R-22Pr7*x+gG'}CXwXxUn{mneo2are1޼>5Ѽ;[x3%;J#o^5[y~?:+ƺuu{NkIotq׶1"ڝzK- BkiF +#^FpďJ$nᴚmv6v4qې~pqTOj">+uKIytn=Q#ޞO--;ƏxFҥno-{uP#'#`,NUX9sbXIbwIt@~φn" Y%ʼRDs;HZL֡Zd^2fJ\e\aYqtѿ5؃G얰qc zd}W.&M{I#d[oU A{ɠGOm2]B$;9Vr#v^ դo]ڽ̿jB ۿ, +q(-c\k[+K\/^4f<}ݜvMJOzziu|1Ufp A\ :eٟOxB6o1G\FI@u+xLEJ3Co?]_mjmDžgF v .J`gڥƯ圶n($O|OP=RLH6D8u<2xc@2i渑"5,;U$dsqKUu PA(><͛7cޖ&%E #Vufݞ^v޻sK 1[@ƑEHB: }Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@StE {A3F5rN*J z}30=bBƜM)-.q o4 "tLT9WHT> Т&:dQE"((((((((((((((((Wh2inYmPeNUc5ECiYq:J}2jj(p(((((((((((+5]SG5K _6MSu/ =6ikQEVnyXiOiCU7 O'sq洨?~SּD>%*8HN3]PEPEPEP_im[x2g p:Q?a^CXԥm͕QI|̆Mw( ( ( ڔ>Ls2ļ ]%ut ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( uXز{bܨUL2jgC#Eﵝ5?!/a)5f;(WvGc-#`MGk[ַ 嗦TFxzYZF"E A{ OwWv6q{q+Lgx.-&x\e$+}ԔSE A{ O(;G$:/?%?ƭ }>'6R9ՇSPw$:/?%?ƏHt_ JK6[YwqEm!M$18=SEwm;*qG(z0c@?!/a)4C^Sj^@ٳyci0yU X~u5gC^ShEճyjѮa,&A]q;G$:/?%?ƴh Ht_ JѢ3!/a)4C^SkFE A{ O(;G$:/?%?ƴh Ht_ JXԬo^XC*>5fC4hcFy*3=hhCN(++_;M5/$ɷ,r烜0H5QcvjYm<: S<*Cb@@ϭXҵ /]ưڸINTd2G#;^J{]ZսKU]CSm~ӪA/s*ƹ=XPK nI:78 fhMET-,ly >Tq2h;TnfsVRO67)@ECȓ)S hj( +5Asc$qVU$<* x"m 2.S^2&h؁ǡz&#limgCEu PVninQ$| )99=+sB)ORMQ0;XuP誖ڥ՝H%#;CqV[ (dv8P2NGomuq4IHiw _Q@(Ӭ˂%ﴜ'kV |ۙV52 Քu5x:0YNAEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEVt3ם5hL_xn)aDhm_ZBA>@j]i1$عvƀY ڶS[*(r T1~yjXmo# M6q'$Τi<]?ZHSwd""CiquçKo?oqp>0 `n5 fNvO_ ,U`URpspsu_ۖnZ+OV&jj_!Ԛ5UXjM{%_#o,;oow\p3c* qnZ+OQi<]?OкZitJ{CitD_H<\ wn F펙l]7)g3H+`?1!H5i<]?H՛)V0AӧkGs)n4o&$68v V<IآĐC=ۇELp0ZGmmDQZ; &ܴW.7t? mE'YҦ%ɃO?0=1WK}VQQh h|m{UOTe : FLoĖfK7O՛N FX"l$VEco#WyൖFno{K{-JY!`6AUX$z CK>eEq3Ks'Ulq9$=&vpWtMZ Nkh%ۮHۖ#pb?Z=n"qybRxef5eڄBTA9)p`.ԯuC=6HĊ l$1]1ZckƴF8 3kQ[[m:8o&6|HO8͓]4^E4 qŅEFPnF8SYқS6ӄ/ UЮf=$\m.%%d]#H` We-K0 7']|[Y\,U4&'T9.`bA9sX?keuqm{wornLBxr  0 9W yapbXY&2:6໎y}2sP U$WHoa6mv(̅9 =*yModKyP'ύr%eTXdTo  D1Y_w= vMK2:'\?w6T|`z]_u<[Zk'@jU7#nx%ycr3Kts-\bb"Ip{nq]M/ÅQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Vn=Vkm+XE,[]$Bq?:Ң(Z\"X %,(9fU!IFr2-Q@Q@ZPMŌlBG>dr9H*QEQUu JJMT>bGio*zZdmo$B'7ECiu 7vAJoj6izs5glrwfU H 1ߚ#tحlmZ|u <=h@.妶nc0${v7^1xsވ=?mC'V[1m]&iI3275RVm/5 mMmnf[X$2*䁕]sÚjjopyP]#3S([u45WڮsgbpJ{x2ѕTl$rI9ηr4{(M^O>lq) +kƕw<3Dl`X$D7+SGi-js-)n1!9I^kFӓ}?ok~z_-u>QN3sp Ij؞EYbԮ5 % V AV DR1GJuׇ4۷y#9.fYX\C#>QV>K[k,rřfbK1Lx5HnQQؠ j+3{NTtFUԤ@6m#r8I-+wլDqnaJo2̹ʎG'PFIe[iZ\B/m\F7uXvOiXζc7-0z *kDSŪY<6lVEB9zk{k}fvW0H7$z@MEg[E{M^{x\" ,9{r;Ki&Kj3p=PgL*cZR[V4b>;YL7012Ҁ&K:=[|-5B 9'%Fzt ֭)1HW0GO:Q_yz϶{dzŬn#yFEb@ XI{J.t𱔺j`3899(Z>&Ѕ7ZӅ?dlaF ulF%I}I 'kEd>%|?[jE<ĻUՈIb]sImR; pUXv:\EEsu7oKIepԓEK,V6N-\&'9ӢO/Pi[o?~1աjhd1*G w$22h\vwzufqzFnXӬAe|#1۵QV<rn4ԢZju76k̯_qޟ&gH% Qy x^=84bDZ6uKc=͵ฒm|ɂue^ =*zƛ5ŜZVxu/eT}h\ U;-_MԦ;PTuvF@qxEO7jY7 r0d8EQ]oJk{5~l *`ʹ r=*[JJ7:录!Lh 2 6FMsI8T%bp9 s BHDbXnnPp:q(H5ԭgN љ}' 5dF dͫX8䎙@4VN; 2_i/G?=VR꛶ݞgB%.ൌ< XO` hFMsIJ]R]R4۶AxNX!hmF6GZEeS ҝwMeG=sqgjmR\m H§g(|Cv6`cr9z.eek=Ϋ{ago/9(=+ր4hig֩e%*:q'|C֝>=\VpdTϜcfOtQ@hW]-#xHs44 x8*،8!2 G(ڽW)s A6.Q *#]j}[[CnB$*,D/QT$֬Fo:]Jʑ=R }iƙ}y=isslq<0ή yEb/4uEyn.cV[slp?g1z$)]gO!29y64-%nl a%aa d`F#"Ԥ4GtH"v<3 嵝\Ek¢Rǀ*" bf,#tH"¶pO *k8|ˈ7GL>S i#T:.ҞI97x@V|m3\0pKUOBAztN&MsXlub;qB(JOquoc$` @+vue|K/yFg'Һ#~ލ:->Kw5*U_-q2`.5.ڋJz@'U6dGOQUf*;,8 K,QGRI 'tob\b9BpO 47擧2ڳm'D'vv'Ǯ>][NRN;tV2dNH4n׋dcBmz㡪4;LTYDyWn mQUg>d R0lpt9$c"].Me5#7#F/Y-_Xկ6 7 ld^^=ni$'`\d]ܧ͑kF sI mRib3F9 P?2k#O(͹yc|zr('tXYu{iL1;\ 8(y`x׼ISͷI᳒RI)bONP4j* kKq5VŒ85!dt\'9:׭ܿEQ}oJ+yd; 0$T;[ y9~̡cR@@ESш&M[ϴ A0\`ϧWo+ޜ,-.M~[?Cgx J+>Mb;[[Y4t0U?X`d=N}sI;5K4KGp#pI EP]-n.5K( [H9=6p-.Ѯ;{;u 4h P+xLV6mT\8t'9k:eqE}0VN,U zzQ.]7vKYD9 䁏qY(ikP7 l 6hmcZ ڐHa'lcf\S$z⤗\-RfTH^C;UIs@n_u6侵SKN,9 <㱧Xj\7Ic6~YsxCG}+#].X$Vm"ƒVWӣԡӥdLW^NBg$pyc@y(((($!+D򤱶p`ppy( ( ( ( ( ( ( ( ( ( ( ( ( ( (1ZヂK>- JeX, 1s=9>]?Wo3tWCO[&AfzSqO2nsk{ෞalZ=$zqQD6Əٵi]i3ʇd*8;vj։qZiAYGK3Xʱέ:㹢w^G)hdK4m̑;x HE)d*$`&s`;_𓋘Ud1`g˒68Q+$Iq 9$sx .%hy~֯gEցr .Ч#9'8ڮMkͫa8y B~9;y^ktH$v1HT{ 4Pܸ+ ciO?~Ĝ9vhvL`xTBc29i E$cR{ZXw8[VL3oIc)lXW#jịK H2;c0H a cz?Ju;[[o+?{S@cXq%_X"YP8U{kd/#4G qr4BCg,w+#3lv]s'-LԢ,u fL{ P7peAq5QK{%.4;[Mf֖X䳙ԕ}ā{OhڍizDj ,X[Mֻ,湔1Hci/RI'[A <U3Ge\N2@zʽIZM&I bm\H n$l 9klbT84$}p8r;l՚tīr" lK]ʦe X  eG#s~+e^ZEwc$bqnBSd'vM^+d/`@^G[N 4=Gx\_ۋwz^Vgc|cƥX6kk[{5yPI dC–9'à6kɬWM%B,ImuɿuY-㱞i9͕?3'q#GF<6vT>WcWg?wckή!L[ݜX\[6a8;_C->8ev v ? O_r [Y]q/C qfi [km#dw,AP&gԜ{bHRWӿ;il~J*/wosI.>aK="KYH\~GV8,^u2,HQ0 uTGݵ~? ]ߛrZφ.lϡ`]; at]SRf5;(K  ǿ+9n_d0y@8ԠPGCҍw]?4SʺG7^n߼l(%F*Q 0{U3&K#|L [F.ጌ:OQ3I;fPJ6= Bi-CNCYmྴk+-h?")-ǘcO{4"K .|uwzWWLFF,O v*Fcv'V`~᫛Csa-[Y+y?/~GywPavy+ţ(9.!wbT RTS]0H18PII玹czGMwT(#5yo$18iz.kߤWl-1&%ٶUrp{*vw^={JD%o4#*v\gfriX]Aqf^j` y**cXg# vQ:m}BKmI,fxxE,AMA?<8:U喛g,1-CP0%8hs8|9YR5H I| X+g҅Û+ F]֒0RTH#bYj7Q_^ϣPI}kg5\CB>cyR2{_ M>vQEmv4 wn .@8꾉,Zf]M-Ƥ.`6OH LL18=e|1GPWӣ7n۸XCw[oqƏC}1K7:}ݰwka2I+2 ˜8zCiw Z;[gkj ٜ6ռ/Ղ /#[v>V8;ּ7sr#k;^hZZQwHw9һ*)i%ndMfwcHK`̿^2*=GI-n ߶=i5CCo0JfS dc{:)_!vM0{2-]FAfn3J5M9'T:H.7ْ&ӉBb,NsGVsKWR98Rg3dsYVo}|% f{Ubya Fyoe)y$[M>Kv_@,"i,09QZ/-䵚 ACM71w$[ldA{v漏KSmMz*t%%m3}Y6Zfk-UH§FX>)#E6 }j,Z f\&I Kb"I\{9@Tɷo4[m$iqzFv#pBT`vU6ؖk9cllwNK ~\zFG d̟d]̤sQHqn.b!:(?tȻC * 7mKSlI<:}+/oO3 ]gKx. ~`ci|z<75q簉M+|gq_E79u 0io5,e 1֝=dӭn~ǩ =܍ O mQ5QB[84VWT:h [G*ME7V >ٚDWӯ,V('hhv IH'uS/dtp6avuw2\l#k~oߊ-K+]B[&{@$"vA'E.o]:폈 &(aUI9; g3Sݫ驥4TJȾUx,rv#ug}{$MVyF#1~p*7UѮ-oN-Ioխ!" |ĐouR r28ҊKD 3EOG[[㶅b/T1 t2÷7$CF(l)gWˍRL*g6D;+~+-|.}IN)%s~^_Emka}){HlQ$aA\T>DpvvAF9V^ 9կ[* <kBѫ=1Y^%ϧ[K{ `><.ή:$L~~MI/B͡ΉfHr\*0H3iZieK1T.)צ:7m.[ҍ֭l4 K$㜯maדRFPmһ4dq]e13Z6ֱ\XHE7 */ 7ޟ/z-ߑgivm/u;p8!@/>.;FL![k6Q#eT1<]N_-ʗPJg1**u ZJR鰅Yw;o*v83O4ucgҡo \_gw Hݖ&|ےAa?Ł]e`nY/E:j?mO2?q䃑9ާo (Q@Q@Q@Q@Q@Q@GA|wI8 _GZ?L= ejG³@lnKmy~DU$rN1ֽ:[ύ5? .[{i#o0E|p H$sǻSinncN <yRg=0;)k]Ǟ闧O-WIhoR?+a6`m `n=.OZsLR͗)hCwj >Z[Ao+>  ]w+oo&n sj+9kZpkM}WY`]*k.c_$e8lG<`R/AnŚu([ sxD7XGB$t;LNVw|}<>b)19zuץGF O̺k-wwj[*&ў`$a7pyox;bwD>qsHZ~}˖Y ykQN`?_qq#z_w,[mx]#Sk>_e J-q"is /&@d!;IQz-#TZhym-CҴˋ[uǶht0aTJиzT%MMݬEu^?DlRAӭm4xZE#N sӾsR[[gi X\d 'isy1m$OpatNĵHXpr67 >'"<8-KYɡO+,͕ٛ!X;c;6;}ԗc5(+A0/lxȓ(UM#KoMn\f y'x-ߟƊbw=*->^pڄش)0E-6d(9`W = Ʈl FWi+o- Z;q M/ mv~V29L]Y68Q2aMZ%Q a:Mf dfbݸ{8Tw_gN-)5x*y, eEWO%P_3ⶳx~oy1O{HS01!X2ho4c.Y8_n@I9ފk$)aԿ_83BH.ZR3o;| 8l4?EeI#A2-͸ݿ0hiM42J1iEVEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPE^=GE}&\AiaC0q(Ks+LӮpΗ2# *<4C5ޫm=Xiߪj"lg$-y⍁ks]\_]^i\b) FďCUcosv0i|%gth ˑ:ߘ_#;+k+u~sJTmL$8<1=9؛Ϸ76 fۊ\5_Kl9Utkq{y'o(&mRVko?19ITVe2_ʶ[FH2Ī<ՇkF=4>Y|AlbkG|D$9Cw7zMHȌU(HƈVq6Xp}8 bKY2UKWqd6`BtnO5 C_]NK8$ vq kcU>\1Hq.BnywD%9$apFG~[_V!5.N1=J'UA ]zSjw7QXG$l87وRE? ;4-RBf}/~)="EٿOEAȿ7vo@P}/~(d_ݛ?PTl߇ >f="EٿOEAȿ7vo@P}/~(d_ݛ?PTl߇ >f="EٿOEAȿ7"eߍ}Q@Q@Q@}/W9袛m$cA4czAsX^ ݕ1lJbHVOժ) Κyl~mdW8UK+[66D10p=@*z(R\]2K#> '$~U3ꚅ㷔`۫vw̬sr:c(Ή^j1jzecʹHr6'[̰Iy Dw wn< E;=?!_VVCHR H O)'Zɷc7`"ֿtuU}Ǒ y)$mc89$\}ljYFs,FS%hm7)cɨź:C3O83JФ?d.ny\Jڢk?%0j i)-*cӾ W.5rUZI!t}0{'9GK]íJ*[j{5ۛY`' IU(((((((@@kzƧs 1eS-[ H\H4zŴ/۴h[N1ZM_wVo izʢMyi,` $sQŭ--6Cq眰b1m>usZ'ǚZNY4vN~ %KAw޸$ps[sx[?Q-k$Hed#V sw r3/"o}iItN+Gxܰ \I!rpx2 @Gsf Tu]yG9G[Esx&ݭo>ťs|LJY1~}%;8]4y| snd_)9YU }I=:Z+񭆡uji{ ͍Ѩh,va7f iZ_sg%\ġdH.'##6Y,."` a˱c;qcZ[1O53F7"'18)¾:Z+񝵅:Z-&Hȸ,oه8<oZ,SHm-nsz);?G]Es7muso1su `k>H/[}:O:T,ƹOJW]Gf֊iZ}ܘ!6ݖ6YGsI8O/(mye=5ѩO1P+w0 UuZ\H# &*gk2AMiR((^&0kZisiWsDlRG4$KgtoEssMX.H8˵S~XA rqR'k{ӌq2\ߑbi ]vvh-x+:)~GAdB|c_Ex+{5Kt/\$t2h/SDz[l4k׻]J++IDbHiR6zk}EpW%VQC#BlS $*a-OA_qQ\ň~ne6 f#_6)6398h ΠI-q \v:(Z^ot#4/kbmY8I\s=s$:oh_sIURwqg{WEE>mx.j4VC-ɒTLee i$,r@52;hM딍.2xfֶOں3΃hpMgpG3>ld#pN~Lt uo^VZjY.fFV,8?tu9㳢W3'[)52b 73E-&6Gemtfmyb6U mN;*cZhru=j ۻ- g̪ oW Wn-/,.;i"{:MjDl(Z Qig[kGkoeԮ1smRѼq+mQm,+mLib R}86_uSZuk4w#F-K*^TQL((((((k63jz%卵ѳg Ǹ`29ǽ]vwG3t7To [M*[21AF#Ny)|;3Úc[fcl'CcJ4CyYe pOld1lCiV x(vP0*۽69;ᾉc tV3\Z{m9e;$m$Z<{y$O;OVp@`'@WiE. gYwxٲN'f ӎiǃ?u ?l-NDLǷ^zWQE;&}#XSRGO έ,@_Cc$l:_4T14f8|u<ֻ*m~!~).k7b~c|?g( ڌ@_Sm$%>an185O0|/z<ג+Ko${'98秢vtZ7ڍ}Tv8+AW?n'cg1\d݌)twwI<{qyYNmBDvD~id\׏%֣ڄז[Sy#?7%-WYE;#DOlxk?D%76 3[]Z%;8&m[NKƐF''x(hr^:_UQ~W]xzqΞMk^#R,na"CRqzآOA_y]&Kn6YOj6B4#d*]uGs|/E~e{K]}mKf ۩iXx&K;KCq$Ơy&M7ݜc묢/@͜Tm8eK 0h}  sVYZ\hlڵAD._)` ' WEQ +GXݛwuN?:Ң ( ( ( ( (kƇΰ|D&d8y#UNFH忾컲''Ζ=מ.hxy0AֽgGWWlԢB?ÑUӔZRS5dWS]3KA ekQNm?\JI;0b0#8fttV-&*奼y6‡'xQx]JO.u{H$)ʫɂtx慫4M訬6%-Æ]A+)$`)o *6jCѐ=YrrqJWv:*+Ėw*2- dO Ŵ[3V˹K g 0#r^`Wcɏy(ck|}A{y(n{y15ELOX跚{FuT398鞜ӌ\iniy15ELPGqwqlH1̬2`aTfMn <y15E>C<y15E>g_Q<(LG_Sy15ELO<y15E>g_Q<(LG_Sy15ELO<y15E>g_Q<(LG_Sy15ELO<y15E>g_Q<(LG_Sy15ELO<y15E>g_Q<(LG_Sy15ELO<y15E>g_Q<(LG_Sy15ELO<y15E>g_Q<(LG_Sy15ELO<y15E>g_Q<(LG_Sy15ELO<y15E>g_Q<(LG_Sy15ELO<y15E>g_Q<(LG_Sy15ELO<y15E>g_Q<(LG_Sy15ELO<y15E>g_Q<(LG_Sy15ELO<y15E>g_Q<(LG_Sy15ELTc|A.F%h~tk;[h4er='qڀ4"T/Um7R4oلc3rTQEV'tsxv)CISۢ-Eݫ8EmE$;j P1HXæi6mX=p՚*Q8B*1Zwңۓwc:=η7gw <ɔ(ueIYCZ݋[7]C} ePq"UR '$mrCYA`׿ډ~,#"/NvG&&xKI%pJ{ ƺ(Z;o@i  HC4vM]> Qf4%I8>b2ět$3#G};WEE++[lwO9֫xM;ZܵHҠ AUICyoTICngVq-q9 Wb0&;O*i> tNFӸg'gž,:qO$:b2ʡ`y㬢1pzݺI-؜$=mqQH((((((((( ]K@{5&.4 'LxY?>us$gg?'kuꨮN32j+ [D]P*)RDy#SA2YYc#KfW7wPkMO=M։y4L1Ky%iguƱ(fVvIxN #Ԧ:ܒjp\ExG`d#9>TQ_/?!:j^or{7%UgFR)$(a 0g^iB>sj"r:hif(=b+8֔UnlFX5th&x"O?z('y:-5ΘcNYl k܄IXb]ˢȌ(eaVzPPU +ov>d{ү5YH\QӐ>kGO 4o2{VػX7IS >ywVq8˛1t,WI-y#]d$+j+)˙q* ((((((((((((((((((((((((((((((((((((((((((((((((((((i=.Bl6 +Oxm:$*>n;k8c8*((ROj3 {泫QR=Q4-Q\syXFzk65Eї+w +?W!ak}mqoVk+Jrd,A?vW$q5VQ7RFvbNUHIw>&--k ]#EQ՚0Gf y j+6_CffYG8RUXjxFͮyaV028sPϿlY:K= 7Ʌ 0b ckKL-5xK=B1$DBK@((((((dE6i>߱S@}}abϬ?T YOu(b0^ʍOϬ?QOgu &yjBH]vj͍zoye%CdgߚErT>i>߱Gm?*zǶwcy䷊A$ܣX}*J# Jtinh}abϬ?TT6}}ab5l׻&HKVQWOF=jվŴP,o6eqz}hnrϬ?QOl7JHdHBpOR u5c>i>߱Gm?*z) 6}}ab ~aXة>i>߱Gm?*z(Ϭ?QO6}}ab ~aXة>i>߱Gm?*z(Ϭ?QO6}}abz_$ s,>}ڀ4Ifڲ6:rp2h`6i>߱Gm?*)5}6-F>]B/n|6:$^yUH= Qxeծmc=¢G#'l m?( ~Ewiim% 4_A?7Q֛&E}oe.fW*UsGOO>i>߱Y^-SVirf0w2qʰV״tl˶AdGKc6}}ab5լ^"Ќ>9Y7gnz6}}ab5h[MS q q'#vlO>i>߱Pɬ06_Nyeq! zz/mdKYdI?/= 6}}abm;WүoFm2Ȫބ8i>߱Ut6]FUQ#DI=/e4m\5><O>i>߱QjTo}_7og8Ba]"18$w^ ~aXة%("2M"G]j+F9仼-<,%9bO}(~i>߱Gm?* ̺AxBJ(r{!𩆧`u cy?g<S#րO>i>߱Xrxg&c }DFcn22ĎX sSN{fuT$JWm?( ~C>=\VpdTϜcfOtQWim?( ~OEAO>i>߱S@}}abϬ?TPaXأ6=m?( ~OEAO>i>߱S@}}abϬ?TPaXأ6=m?( ~OEAO>i>߱S@}}abϬ?TPaXأ6=m?( ~OEAO>i>߱S@}}abϬ?TPaXأ6=m?( ~OEAO>i>߱U5v@e1I0j%۟O8go0nq}}abϬ?TZvkFi|kYPЕ'PaXأ6=m?( ~OEAO>i>߱S@}}abϬ?TPaXأ6=m?( ~OEAO>i>߱S@}}abϬ?TPaXأ6=m?( ~U_e"\E~wTV~)ѯoUٖʲjO(">LIzP3PiC'#EZ((  eDM2a 2z{r+^+  sJI3/\ӮL$΅fϗ.U'Yѵz[--|)WR7~lqQ]&g5rm?Rk7,D>BI8=6Q qti#fWh2Fsں)NO \CmƉc{k#I`gd*8 ssJ[̶R d&HǾv6w+)tr:G=)tʹk=ٝ^Fs_!K;(wx.L\X[mo$6q]2L@ Jaw4Qհ?PQi]=Ɂ/ '9lvހW1/ut{8-J=M!}E3U6rK`@'N>2K[C ,-fpXr*oHʰPrJ|#H-.%u[co4gXڻ bmH=vI]IK uĖ#Xʧ|2W=sTn%zҭhӥwطZ pAsQCpZG \,38г61_ji ,`!rUU*06:[Q(((((((((jIu[y8*@պ)gp0֣J[Aq"9c=S0:p]HsXx^Y[:gofڱ~̣r ǰ}EtC8?08򜾓ewh؊yAi?_rZhnQ4x. P0T1 >\8//xc/vv m\#cMEA:1n_ (M((((((((%]im5"EZuI<#w/cIsoCU~R$Q~sK[]FJI+Cm3HKljx ( ( ( ( ( ( ( ( (+.f-"+No v\S`Ҵhg(I3BJO]sVmmc8 a#B`*Z((+}h>\Xny?0\0<)N\ά>MWKՔ2G˨LJ {zWGkj*Z?2_ڶ]u}ZCgl,mR't!Tgo׃B|S|@#$,Q$+ D`G stV'$Q[7̩r 6wM= p'MO4&wrYc}|#{$JW@jQYZWtjo/My&BI$,8dqTZ텦JaQʤ&OBgҘ4VUtD[#K]Xeʒ`3//eq4qI|_ H<:\ +Kmٵ1+3n#tlF*i>߱S@}}abϬ?T YOu(b0^ʍOϬ?QOgu &yjBH]vj͍zoye%CdgߚErT>i>߱Gm?*zǶwcy䷊A$ܣX}*J# Jtinh}abϬ?TT6}}ab5l׻&HKVQWOF=jվŴP,o6eqz}hnrϬ?QOl7JHdHBpOR u5c>i>߱Gm?*Zpتz eQ™olK g7=zm(I.e{~i>߱Gm?*z* ~aXة>i>߱Gm?*z(Ϭ?QO6}}ab ~aXة>i>߱Gm?*z(Ϭ?QO6}}abz_$ s,>}ڀ4Ifڲ6:rp2h`6i>߱Gm?*)5}6-F>]B/n|6:$^yUH= Qxeծmc=¢G#'l m?( ~Ewiim% 4_A?7Q֛&E}oe.fW*UsGOO>i>߱Y^-SVirf0w2qʰV״tl˶AdGKc6}}ab5լ^"Ќ>9Y7gnz6}}ab5h[MS q q'#vlO>i>߱Pɬ06_Nyeq! zz/mdKYdI?/= 6}}abm;WүoFm2Ȫބ8i>߱Ut6]FUQ#DI=/e4m\5><O>i>߱QjTo}_7og8Ba]"18$w^ ~aXاq 2 1wF I<YrxLL BP6=ȤFNFAW{Pm?( ~fxB@Lp=U<`hq-(;-G#Λ,p: ;d;v:Ϭ?QOWO}o/nlq,];SiNkw0esz6"OϬ?QOUeV[J%! AQ#x5^=sIRF!6w:6}}abA95}B.~Ҟ[6ppVn ;@-<@'~taXأ6TEZ!7֫= ߆jCik%ԬU-1$I6}}abum;GYk[Y,3,j[2 i/53NCQ->uO4.O'ր% ~aXبƭPP7wO9|о3{⢇5Ο=c- rGјY ~aXت V̶bš7)r3ִ(Ϭ?QO6}}ab ~aXة>i>߱Gm?*z(Ϭ?QO6}}ab ~aXة>i>߱Gm?*z(Ϭ?QO6}}ab ~aXة>i>߱Gm?*z(Ϭ?QO붺7)IEP| .ygh?ys;6}}abӵ3X5K䍶Zβ>85r ~aXة>i>߱Gm?*z(Ϭ?QO6}}ab ~aXة>i>߱Gm?*z(Ϭ?QO6}}ab ~aXة>i>߱Gm?*z/%B(ĭxӸ⢳Ngukz̷.UGd;PϬ?TA9bHbMԭ5:6a=y*QEΏz/(} ]E_:'+>s[]Yڥ<>#Yp R^xYM;x{;qm$&;j꛰(#Ώz/(} 78rBo6r`.wd|;dQ}X7sN#Bu_-CItWG=yE4M][g3!m~o~ [{(*Wn xxIhvw!cqe*2 d*p$3gxB:?Щ*^aiͅcg썼I;/L|vG=yE6)tG_PyEt@G=yE>g_QG=)tG_PyEt@(((]h@DE2J5ni Җ\Hdtbqm/ΰY6md_s(ܿwFrq3Q].O%B|N `0=bXn&z%팺n,S[&4f'+#<裥Ds>ӮỺSW j&T>޼S/4B)uK,Z aI4ѬQ#m,@*1ǾkhqiαmqkF#2Dl`3sW4 OOom` m*X. {8'tQޟp6>%Q o~{H1FX.ORW#է͠-y[/SE!t?EGKӫk%rBfےJ<@;03`〰iڊ76K26H1Y@\ n;G@zLOzqi$D1..nFs1YGZm\ tka4m+1bs&® k/[E[åsai#bm8^)`ٻ!skJuwDp^3=}&w=kcN=iw-xon:+3P59.+mO&y;RliE;^8_᭼=cؚ2^f;T6'85H&P߼-4` fTB%$kXOWsV <5yoFj.H2A >׶6vP \Im悒d+qNVwwqV:䗺mjNU7W_fzWݸmom,U]y nGVpo]4q0ِggGIȻ/RDsU<]m֎~k9G%0i,EhH<7n-5i#_-lKm0w` 73QB0zl^6h?ٍXd r'tki. r&m'km9Hdu=3*p@ɠ75hOLt@X5ԪQpֲvd׭-$Mϛ\{Y@ZݫݕК֢.fY'wP-cvf UiSܽ]C!˯-C2#ZڢWYIkIɆ8R|,xd56K,xӍ4J) (Q@Q@Q@Q@Q@Q@Q@Q@QE1 ǝITaXة>i>߱Gm?*z(Ϭ?QO6}}ab4nWugj@p2O*$. ,n;Fo,$ 5qrKrm?( ~I ${'䍎A<gG=}ADaXأ6=m?( ~OEAO>i>߱S@}}abϬ?TPaXأ6=m?( ~OEAO>i>߱S@}}abϬ?TPaXأ6=m?( ~OEAO>i>߱S@}}abϬ?TPaXأ6=s kf%exM6ՐɴgG[O>i>߱QIj0{piI"ʮrGj(ƛ-smCցn9>`d'΀4i>߱Gm?*+cLKi.N IT2 5*+{)u;4P@$OB=(Ϭ?QOͲn|RK4𼑓Uj&C&.e&6]'>p::\<aXأ6b(n&aɻ>slaXأ6=fZ宯q} ʒX`eP3F9RAO>i>߱Yv~.uIn;Ox9'[a suf- 7#;I᱌l?GП6}}abciTN\FvHI݌rGP5swHenIo|y 8i>߱T_mZ}SQӭ<9]Q PX&xNBɼH+=1m?( ~W״:mBU 1\GNG't(tu u9,b\alϰϬ?QOpM7[_9|ҿ ㎸G4f[]^)s>~~^A뎔k6}}abkzu&א=DeBJ}899UzE6a̳]F纃:Pm?( ~LΨق,OzE4ۋk{i5=w*2N{fm?( ~dpX9 ۱p3GhK>hYʣ_Ϭ?QOs]۫ʍQL ~aXب]>{+[YEytCj!ITiHQҀ$ ~aXبb{ l |r߱Gm?*kKK{JyQ"uVfo'$ýK{uHr(iU o,`pprރ ~aXتZs]GqS`mSt&ls9(m?( ~Fu]9uAd-Lu%O^KyEBH%IKO>i>߱YzotSG RVi,d9PI @ W'Gj^[_Gucqͼ)4.z84aXأ6=m?( ~OEAO>i>߱S@}}abϬ?TPaXأ6=m?( ~OEAO>i>߱S@}}abϬ?TPaXأ6=m?( ~T]$"Ѩ>_n~ I< Eͼgݹ_ƀO>i>߱QiƙFiF]gYBBT@}}abϬ?TPaXأ6=m?( ~OEAO>i>߱S@}}abϬ?TPaXأ6=m?( ~OEAO>i>߱S@}}abϬ?TPaXأ6V=MpbViwqQYFVOf[*#2zGm?*H1$y@AVƝ 0`LrFppx`OG#aH-g;܀l y26MLͿG[_֠?e&OɣɓyhS?&&OɠQOdm|1L=̷FX!hlۖSg=lhsKD:1n5cAZ?ʑ"o0.G*RzZ(*Tsj/`+2Š((((((((%]im5"EZuI<#w/cIsog?,1ysK=KZgs1m_sҽ ;ymZ[[/)|dži@n3烊zݏ5]^-WZR3h!ؒF&S"p^O9(w*~ oMPjn][a4dsSwN+FO6!-O81q)=Uw8 m\3\6Wd\Z-N"ߐv<)nb51"96Ly# 5=NCk)'frrfM[)"*(pZ+A^q%?d,Y9&ͬ%J1~I|{ w__iVW7pʸDӳŢ)gsu눖{D,F''$n\u/?ڝ.̶ҥMع60Ӻշܘ8DE-m$s> \<~kF+2 ^xWE\^[X-{y«ǟAZSnwB/bb){q"HYJʲJ$5=GP#>^Cl-ML"?'28wR*ua`ᶶ:\Yrb!%YwtZz5{h.썴v/bd[Æ븖ykMkia>4%oKsBnkySj`1_ &QG8[e0`pPzyNEcLlmT'N.,c1ҕhn_1NoCm"[ۛܙٙKB6*%rJ8$eբޮEPEPEPEPEPEPEPEPEPng-ƘbHyu=ya7qTɪi$O9Ip0ϟ!*?){j( 󹃥yyqKfx%ap@<Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@NMAv7;g)iZ}3ocmw$!P%'9tPVVYF0ơUG-PEPExI=>qF^5 f'AJҝ9T,HN<:+ ދp$c >` _+5 (2Gi[*ZWs:USE=WZ|VCuqup Ds %9辔îc .bK,&~Fإp0rÎFyj+F[72y0O.)W}Pˍg II LC FBv r>R"Ԣ*ݺ(,o"!]Ii fsȇar29r&Ҿ=M*KHdcv=/YӮF,˾IY@u\oljQX׈G3=ʛ n9!l/RJF1T$"yeXOZ<íc V=D@۳tjo®EPEPEPEPEPEPEPEPEP%)Dcm?*z(Ϭ?QO6}}abo.ᰲN<maXأ6=%ү-\HbyW#x6mnζ}dX)*I ~aXة"1j[ܑB#3Gq8NpRQWfm?( ~OEH>i>߱Gm?*:ͼڣتJ#{ Q랒ݏnmVWKm~}sO<"m?( ~,7Qy=dE$3u A" ~aXب]R "VFE mw1 { G}[X3 VGnn2T0yJ{!9%g6}}abdaXأ6=m?( ~OEAO>i>߱S@}}abϬ?TPaXأ6=m?( ~OEAO>i>߱S@}}abϬ?TK䁮eGԗOP&6VC&G]NMlm?( ~E&Ũç˨Z٧Q$*G0xOlڵͶ wZTVhd́: ~aXب2X#m-8&P':zdC\ U= rà i>߱Gm?+6źbJ .BLӦsFN9VԚz|v"lpp,}}abϬ?T>ܢUG+&ޯQ}}abϬ?Tco|K$&(Q@i>߱Gm?+3{ Vwec. ߕ{CKc2\EKsoE'~Q)B /Ϭ?QOY/ ԭ-nHC4)'*SƧ`u cy?g<S#րO>i>߱Y߉4)RkfXhRYTr8i>߱L4EM>d&l 52=U45E$]hӨ L۱ ~i>߱Gm?+'K/i>߱Xz-兎c-յݍhb>`I 88VSkB]oLԬPb{&xTXd@HeϬ?QOMkJ,eԬG1NF`*9' zӠtBk kY-fei#(9GZ6}}ab ~aXة>i>߱Gm?*z(Ϭ?QO6}}ab ~aXة>i>߱Gm?*z(Ϭ?QO6}}ab ~aXة>i>߱Gm?*z(Ϭ?QO6}}ab ~aXت޻kZq|DZ5I'V~ ۷8i>߱Gm?*-;X5M#QHk(SJW(Ϭ?QO6}}ab ~aXة>i>߱Gm?*z(Ϭ?QO6}}ab ~aXة>i>߱Gm?*z(Ϭ?QO6}}abǯ]."Jm?w;*+?wVirYD{FOQ_ ~IC&$=v(4JXӡfL @"PEP\׉|'&[jZ~l/mǸ$WLuZS*r扝JqY[/;뛗^Fy$WhZLZm"@oaĒIɭ +jغQ hhѓV&ckV6:f{]A*av?vsN ?ʴ%rCk=sg@O[ErunqZsXƖKa6y@* `1eso UC>c:\&F|Ҟ^ݧ`G=EtAe߄-B-R n **$SD}KNtȮ^[Nv5Y81[PV[[&'>]bc R|81>siMoii&3>CV-60z^rڗnOS.l⻉mRE OD1gG{-<$wl+Sio RR{Wp8`{SE @zJ62ZZÔ)w2V~@gaK/_Z] d$]P7d̻ԆL m$aw__iVW7pʸD~?#Uuyd ڑjKm$DhPIެBz5[KEW .Xx8Q][uً;;B0^ZQ l 8+j( ¸M"/X?ux&G ,U>Ar#:h{Ov}%H?I,책TO+KG<@$vUЬRDdܬ7g89#U_V1tKSmMz*t%%m3}Z=\F?Ү"1H*N9>E%VjCihjbkm+#%y^Hd[7"w=;g<RF1An;k8c8*((Gusg5aqB.rQ]LTʣjQ^iugBK1ze9פC' I "Oɏ}P°[|hm- h).ͯ-!h؞"*0 >YEPmoOI"k[y1t;#qU|WjWiogvή8p`lQXnI5:Q3Kaqw_li}qHfSygjEQEQEQEQEQEQEQEQEQEQEQE`ly$AGO6}}ab ~aXة>i>߱Gm?*z(Ϭ?QO6}}ab ~aXة>i>߱Gm?*z(Ϭ?QO6}}ab ~aXة>i>߱Gm?*z(Ϭ?QO6}}ab ~aXة>i>߱Gm?*z(Ϭ?QO6}}ab ~aXة>i>߱Gm?*z(Ϭ?QO6}}ab ~aXة>i>߱Gm?*z(Ϭ?QO6}}ab ~aXة>i>߱Gm?*z(Ϭ?QO6}}ab ~aXة>i>߱Gm?*z(Ϭ?QO6}}ab ~aXة>i>߱Gm?*z(Ϭ?QO6}}ab ~aXة>i>߱Gm?*z(Ϭ?QO6}}ab ~aXة>i>߱Gm?*z(Ϭ?QO6}}ab ~aXة>i>߱Gm?*z(Ϭ?QO6}}ab ~aXة>i>߱Gm?*z(Ϭ?QO6}}ab ~aXة>i>߱Gm?*z(Ϭ?QO6}}ab ~aXة>i>߱Gm?*z(Ϭ?QO6}}ab ~IC&$=v(@Q@r>2ub3!]4N]EiN.daE7N{>ǘ=sWMVUmIPThZW&k.EYdd~ +Zb-K ۅzux:d%t+6|rG 9h2x--Yg 2&ʄ@F0y9㤢S3^Mney0.SnG*<돺R O6 LIdTr9?qSEF%6i0AwF+n(uf%CK'=P[G5HD@cH ͕H;Qⲵ(aEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPE\5 tW 6NҮ,LT8MwTWxVV{I6&:ʽ7I$sp?\Ԧ3V\z4VGG \$9 i[)]P/\ }<w=b:6͚+$xJt4, #TFWkRj%M֟{5{SuMi./(!|}2(aW1 ԞE+ ,VC"f' |S \Ҙ+g1x*a y^4t+İ_xFKk{h#{iU[v2PMmPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP\?=jn0H 'WqEiN.dcZkGG]3ssiyĹ7(vt Y_G}e؎GEkW:hʎgg{[+nsX15S7qy r}q]M-G1bQP<@|d]0ɜrG\zGo[nL dm9ɜp8WeE K隍$ ,yc6=6~:ۢ{+QHaEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP\ϊCw^iAv\>FL1V\,z+sNZvkk$}dzJLhx;WS%ݎ/nu;ٖTnH隕=A$;a|Ip26y-K{Tu-m(ismĬ軶7b13UljӯēkHop0z3Ӑ)V#$gX>1{{4Kt0\!nOx.k6:{u]#FD[m kgT>m>}T 9=h_ןEœYmavwcgͻv1޴=r zkx. 5H3'k|6tu/nԚ׺nDB#ҵ| e{iͩ$-=҉$}@ޠ4-~O!-6k騢(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((*9.`4q3p*J(Z?QO ֟}~ uCn>i?PGۭ?*z(Z?QO ֟}~ uCn>i?PGۭ?*z(Z?QO ֟}~ uCn>i?PGۭ?*z(Z?QO ֟$S6|RLuEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQERX'-p6m\=m jL}VQ qDXR5 0J|?|mI11f%QHw\47i%͜+'қ+G:~eŜěH'ϭg^hWz$@["+;{+&fOczҎY4z-gX O1\y=~4Ʊ8 41 Ynz`.dhKYYBf`V ~ƽkhm,`+k&FYY[k29=3ȕ׉񧋽2RAy X#I"o?^M[NTMկ]j)Jg r9q\jrߦ&|3K#9̸ Nsq0i"֌ZCyqu#ٛW7+!Rh?΂z\qG-k[i++nH$#8:UiIӺ} :׭sFuggksap,,䷇Qhh@s(*&I|jΑiI[l-U$Ey=鹂 +[[o\`STLl{6za֪ [NQ3jQm1"rwgsXj4.q)c1,j#/67lkM/P4b̞}ٖL,I@ͷ<xMK[RlÂۍ#vC6:V.2X#m-8&P':z;xzOPLAOnX<{e-CCI[ᶚW-(2˹LrWvFF)#ZhQȥ]-VS؅nJ͢b (Š((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((]:EWU8&G0r0d`_ ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 72[bte\'J\׍.%m\9]s~EiJQωQG_.aZyR$\);#n9wD#ı[\ Mi\ox!'`ٟ)/\hͣ嬂=(ib-%bIM,F`ßVVm]ȕO:{MGV ,ܣ[?h:aZ\ܘD!Ì$2F:kT#he3b`2A;p]CAv%K1 +|pw6=Wto%u;/;1CrF99*{]Nxm Gpd p=k"C5anc2ӑUoVK&[mYmVVK!(sf?@092puoMZZ]R$3,WqS; +y罽܁4ʨn$g#=tw܋F!,9̬Sy@\ ZjQY2)r[5G/#ǡ=! jukEƩe7 ig'N>M/Yu >)n4dV99q'2\:ka.gikھd' q͘`:w{[=w[Cwr0R<Ю30(z`ij(EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPM46UWGYXd0=A('KFݓhYBt8HU@ ;R@Q@Q@ XbYep 袀 ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (?onedrive-2.5.5/docs/images/business-files-on-demand.png000066400000000000000000000354301476564400300230520ustar00rootroot00000000000000PNG  IHDR܈sRGBgAMA a pHYsttfx:IDATx^߫mYvow?IC~~z0S $>1X%)B@Z 4:m:h4-__S[Js)?k_[ۏ³sv~;߹N#No톀GjϿ4u6"|7NE4-  1_{/HZѻ0%UO֮^?]ֺ(?oT{\ўPsevڝځݦ}'v8'~~BFޒƉLϪWou^ 󶗰d[N'4$ַ޽l?[1/_3WڟG>:D;I7;_9/r僕T_)Z*SPZy/++ %-Z_.pdݢ \q'hm*wo[۟{淾gmѱ38 65_j}oroY4K)e\uhT֋pשEajMS^~o D4];C{>=?,w "|;&:I暪6Ŵe}Z}V$փ';w_a/;>R{t͛1F;y;|{C?{*ܶR_ꢣʖ/߾Pb 8,O*e8S|ąv6sc"vV;Tc 8"'^wʤJU:Fs9f;@ߐH/`K[YFM}/_/~N2~~7eMR3î|B" I$EjnOkWT[\Yr p-|VQ ߧ?zJ8 4"=YO[ Gqk=3쮫!Ef۾OBs^dg|> w+(n'7/ް}ǐlK#[>Ηk.,|*xH]kGnܒܚ[I+9BBo4R5MUw/ʿrd3PO"S|/Ta<|dT̂)!־eo;wt+}s%ۿ}{.۵$؞eJi%Q򝞚}<ǰh UDb{{[LfO5J{=Bnzq[[+N"&]|m^8g/՜yGVw\~V t> OրFlSg.x2%d% _Tv6ul |`*^Ƀ9zg <RǼFWj.XYNU&]ޑml{-΢6VO[wh[}ƚ{G=/s~=_~_z5o1OQ1ڸYF+NwS{ewRBdpس/u!+U ,v;}q24IL_l%^n{>+HF̤IwD:G7uyֶΙS{\GTL h ΑM'\E}XYzc;Q{s`Jx˥zkZW{jON;Ke}m91}JN539haϞ)HwS X^5VspΑŶΙՕV޷rvuqڛlZGg3;pISB GӦnuxۧ?b!fwNX>Ej'l )~:k+,f@SO/>Y{+ѺT5ʫUmSиv>zRm] , };5%[u/Yx|xzmyW%cʴ/)ҀnZy&/AyiӤn!:-56/ekj6pӷ@͆˳z_aCz!߰u3/a]OZȔ$k)kCmK07T͖66v,L ˙8[U(p-|VQ|ߚGtDYO[ G|-~pg=n%}ys['ܭ@O9/23n >pOSU=|< o !AB|  $7@Ho !AB|b_ pf^o^Qie؜Xv5|{4IE 6'o5 J{mV"[}|@XN*߫jt]"Yٝn!<|O:&]dmF7{d!y24eUVx #M_sߊmfUG&$S)8oIgsWVߩ+GN1:4-EZ~b#ǐ>pf|'D]"괂; 'RSvmh8+7,AB|  $7@Ho !AB|I4'J$ AB|CwϞ_{1½%lr!NfcVZwk<@زNT~ w#&uKפʩL w[pD;gG@L\ =w)ڮ[szX$qvn[ĹԻ5Fӱ|K`:yLI\6O75 u S~Vhe-}4[igVZkZCB%CiPLF )/Neҋu0չw"Mߎ:vNWv^1!eJ[-6o{C++&V-4eKbkD]WjuSjJ)kΞr~Mڵ[ il݂Y >oeS'ӧbüW'C1 < c;-""i%%i)l.bT6Y9l)LH;!GaHrzm8n)k9Ŧ3Uq+$ZWǵ_vBqlZʢڲRu* nJ "g<2-,}͵E0״$\jJ٢&;ZG)Ύ.D;i*v)Ӏm ǷVhvSq Zn5}_"ֱ=fY;^N+UYaI^){54;}l^eo pXѸE4TƤ2Xɫb5;}ƩñO~S@C+'d#&fqm0u!YExFUHny&c{9t,cYj룭5Eyi|[{CfMjvV"x3kFjWJ-bmr_[sw9ӧ7oo!gTֹNKR"z^)#r)w{hďHvcd3*$0ܲ;T[efZnC,õzVo^jd{E- -Rl;qYJt#'"r 9م?~RLRd;Iw]j>.} }_Cl]8NvODv )?9y  awW79ԮJvL6$t[bJ|1*eWXEѶ5ZȐr0̫J̡pFF$r2Ȃww3֩3gjz=i) &פ.B5,plRn5=-$.ITa'idHo!{-jWg1H<~U1I N۟}fm^A7Ūf^ܯ;פ]/8ՐVm$]b|{UzrJq\":Z]^DVo3xFS'MA1{Rݩ-Rm8JvXk:K_oeO #.OkՂvfgivל˶XYi|Zc)P暦C%J;c&香Et~@˗/|͏>8KN򝶷Ttkg2 ЎEYlnL^elYUla8ީ\K4lP{oRȁ\aݦ,sHq2nÖaݘle Кb76Kk N-)R)(Qm!SrOׯ_۪v'&-Mm>isVaY‹nѬX]z5F[b+ki”znu[b4\/.hO\݋X P8٘.Ck)&#}Y^P$Wb6L`+y#Th4X0EIUp6!RtY &rD#`Pw]N6nvۦq&sCf{ZU[ eO)ͫ޷0sٜ(YC>483+܁/1U0}:ȁ.,2%6lu[Sr-Nx/wȅgsnɎcl(ՆԼC! p |'iwbﴧÖ; Vm]ofw'kq-&+O[(ڟ&Lr_YD^Jvv}^N8)ifUȖ"ŞDҞ?=z߷L$fW:bawI՝h#: {RsKV3e5vRvvlV$'Y/Nev;f21jeݩH l7X]o2*[:H'v^)1Ms5&z|&LMJcK*+L=[ͫFP";wP:}n2I]؍z~0'ΆٹHGl`Ej3-p. !CღnءOl$"|osTpL@~8'dcɝ@cuCy0a/I=Qoh,{s䎄o !AB|  $7@Ho !AB|I!iOH]@Ƀ|>/a8h7,79 <)~ׯe:`KABLwcݹ\Ǧr{*}w,xy;tK1I'NG:Aݰ,7k{ߟ;<"];,eMY];W1j׽ɡv LUc!S!4(V.,*rG+oc^Udˬ522 D1g/N՝9TKNSN1U0& j؜>2L^$W|P<ϯKZL;"]]oTjWy9U wWLڝXwļ3coړn5nNwNekծjH+f6.v1*[=9%иzabYU/"V7Jjd@ 1u=; ڝޅv펫mq=g-ʪ^ٓkd-&L\}6`7J֐ mf/w`$KLmLNA/r`-@m/+i)˰LM?[T\2khVhPA1{nlԘ:oHN2쥻L+wϞ__zS'*R|zOmOo=r4{o2e?DcK;vYӝ:j瀗rnL )1/]E|Sٖ>\P@`ׯ~mUpW|> Ng)Ni۝ kN#:d26?wxC ]I3'4nolHR߼vkO=wos+ȼ< |&OȐq & Uw},^߁Cn 4TX hvÉ)+GY&rЎ HE,zJW{H P,? p f`ŏ}s{*7W;m:=x( ݉|;ӗ:i)i&u[YSY6 q!.d4M/r16/R3e-feA-XHsH@+Q5W>wD& BRI?Yos3OU8<4TX ƲY]h`{m̊DZݡ+E$:͚3C 6fnHm#H9&Kk W!Q{1:#7_Di[~\7Xo !AB|  $7@Ho !AB|$|4'J$ AB|F/3I˗/_~18|m?lvTm/ dx:{菱^D{6w| T )p<.ڝ~Rg󰵯Dy߲X"3)`Tͮe09Ԯ4e.~eYs-ГRڕͽ5j*NXgXsY޲%9q^5Seƭ-'߯^*2iw‘o*y֫Z;OqӍ>GRC5 XQzWD,dN;>*ۺb#-5BsL݉nWڻ\n'I!ZVөEr4ȚlV2x4z9]T$̉J֐6xxw(v'V;?vm?aD[!!1#-Hזwq"EBjϮGt# 2eH{iӵ dW.)[u>l:m^Q )߉|A,XaOm4_z65q0<6rn Uy05"bF;1>hqf7{J$wsG[6{tnq.} #U9*xD fz"+<|WaP:>Gj^e¢2ρpjov\+ƒy =`'aGR뻾-~Z6L8!ZpT0LL ` y|cx0}!\= 8;\IzkfW{ ޾Cp!oX_p}]XMnwO}屰P[` o'w}ɿcA7p`/޶\ޝt;9b<=GJ\Ug`ջ=[ǝ1a h_P459TC&p.F.*9C뛉LziK2v쐔a+LN]=<",x0\۷* -gϚ\f\ 25PܽX.̌c6w X,RufHsYgdę9%kXu0ZӖL;6q\^j/ڸtS3 &R\۷PNhAz"J^~ӒkTexb:ɵ9`:aQgQk hR:sG.Ό^-pef7rNn|vżR.t4l/`9{Qkٚ F6L8!=`tRm;}D ,!flf1zTa1WƖ@Ͷ 7`Oݢ!Gf#vcHkX6}2~ސVQ n8 7ywh$!uL];Q {x}[g Ҵ|d֨Z}ؕ!í cm@ Hv?LULjeٙ#.xp݌=G ܚ9J"cY[P*V~}Y#& xN`OOvR7;۸i*_Xg@*wt~3cuniqL+{5y\ Vjx[ABu&/ץx2N<)jnKxOGG:3ۙ,G K2 K۞έRlfk;n@dg;IԞ;qzp7Qש)+;7'82mi6*lDx0\ x*`Z~zKx~|'WNH$>qpv ?1O㧲` #p*x0y Fa+F hǵ©Ai4q>pjov\+w_~].y{gOWj\|=6p4#_yqg?_"_Z zN/πᏁ t+Gqջtk)g:$ƨC)$5bHp"0ڳz{9q\|KkM&Z$"[m8˒»ύt|v[4Wv'v8 Nz6!oCDOp'I=W؃ﵻ|-PV5r:<OA)"˓퇢6uDž7S"仾2q 8޾B|  $7@Ho !AB| oUFWxp  $7@Ho !AB|  $7@Ho !AB|  $7@Ho !AB|  $7@Ho !AB|  $7@Ho !AB|  $7@Ho !AB|  $7@Ho !AB|  $7@Ho !AB|  $7@Ho !AB|  $7@Ho !AB|  $7@Ho !AB|  $7@Ho !AB|  $7@Ho !AB|  $7@Ho !AB|  $7@Ho !AB|  $7@Ho !AB|  $7@Ho !AB|  $7@Ho !AB|  $7@Ho !AB|  $7@Ho !AB|  $7@Ho !AB|  $7@Ho !AB|  $7@Ho !AB|  $7@Ho !AB|  $7@Ho !AB|  $7@Ho !AB|  $7@Ho !AB| ;7*[ g  $7@Ho !AB| ,x^x1|]`/)V 8kw݈,@Fx u#Fo|׍An]7b8 w݈,@Fx'?ڹ#7Y~YȌdߟmm/ٳObxLee}>Urdr7-6rvGy)uyD+d5z]zލekh 7%xhjM6>Oޛz[dQ.fwKt*f7exTV,X,Uzk۝jPS;6 gɷ#a5H?O{q۽2{nd8xo{@DG<>{y<^aK&t?\yD4*M}hQ˅YĪzƶܻ|5)߉,B#3`=?|'7)Om+nT7e p<|-@n]7b8 w݈,@Fx u#Fo|׍An]7b8 ]r|׍A΂I.ՍHuԍAB|  $7@Ho !ABRFhO}8> WIENDB`onedrive-2.5.5/docs/images/confirmed_verified_publisher.jpg000066400000000000000000001313731476564400300241560ustar00rootroot00000000000000JFIF``ExifMM*;JiP  >Alex5151 2023:09:11 06:03:282023:09:11 06:03:28Alex http://ns.adobe.com/xap/1.0/ 2023-09-11T06:03:28.508Alex C   '!%"."%()+,+ /3/*2'*+*C  ***************************************************+" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?F(ybʜs}uPpFFy 7ԸTKVdKaEdǯI0E, {Vf\^F eڍTes)sZά}vm(`c3+מ,<ZFJQ\k:%b$WlЄo1o݀yFxoo-Z?]y%Bʃc6p˭C$hhūZtW-?/SaieHnpd\cwˮjfFe6 f<mᱞ.=x澯=<5:j+T MAhmI$tzS Ԏwp:q6'*wd`vnkE>G\Mug҉"કۛZRM[HńMnar q=sJ׸{x:+ӵ+[ M),lً @JD RK {ExhEFNr '0S$Z WWNLj5'OD{h̊ Xyclq۞t ˅E=̒il5h24ޱ9xX ?#-u58]\g,"+ݕ4W'qi}R4z[}unvK 1`⥻xRX4ЌVe`;ov3m/׿=xuK-heH%癦RH>Dn=9hFZ K R:=JM_3+S.l\M,񳻀x4ZLIkqIa[l@pc\$ҮT$DkEt4n\Ğ%M(]>\M'sWI@ᑥ7)U''9CrR3}QYQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@J) -%!4BhjT և6~Φ6I3V(!+2ζe=:đ"c;@1:+5m/*Ri^Z7 rA#b+5; &9Ǚs2ƹ-QUmu]>_[\ZIu;qk24/4-eHdjZ8n.hM:|co燒{ ~Cޝ3q֛~-MOƲIoy46M$O `>{m|/kSaI' :uV~#%Կt>ʷHeϦ4CZ&:êRKW—:jI B~ᬗ =*[{[Ξ:͕=H>-%,CTH:8"ps9?ZӢy{+KWKpx Z aMǾkB)9IƢȯ Pq<zOS;YmJ8 W yTV籶XlҤ@'VA%S%M$gb!;wa>EhME289)4aS$juUO/,sׁZq$LH#>SQJU%'v4dhr1!u$jt,[9+R۾omkgKdUdBTFH`ItUF&Ta-џ>apf+42DŘyFRby>@XdS?)P@8GS'^%V* UCq$L6VC[A-a3=TÁ(Nֻg le/*Hฒ'U=Wzb:pN8b2 -3mE@U8U(u&՛-R2Úc2\5#2džjQE)JRݎ1vExlk$(OrzՊ((Oip2a_XMĆl"-=:֬˥I^7[ԛݲ8-B][w` znU`Ӑx⢗ÚlR,S9ʐ-pEjQMTٰtHp}\0 3cQU΋{8M18xߟ5zi-bE N'l̐c5f!YaAZkr]73c,#+q2R|e?)0]  |-O7Y%tq&1YH*qqZE?i;^R .t۫,o4#KOAץFt[ș3+%8$үG<${k ĐI2nkw#9#k`MT:oג̞iO91fsZѢn{{XmLHe~IdRMMAңYɣW 2OEhB`R *X!N7{PKidO%Ho9aX N8*)I$-kxK;Ec,Udү D'4)9J[; *J ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (IAZi4iM4c9է5NhGNjUZ5>~05{dɽS{+e knRB1X𝏃twӴn%y\2nncJ|W/$=I%mq 6j2A^xFֱKrj YYJ ( sܚo|fQD.dJ }s@3jO鲼/-A$pcF A׬xg3MM-hU%`iYg^~m uYo7[VfrA*gh 4RVO2>[ўLsrh(|my Còskl$(++@Tua^Ta PO" 'n̗:t7-gcԲМ{bt?h<3u\<ŸpX2qq1:uquccmoqw#KJ3m X̍uជw3äJpo+M֥Gjn.C9/ ֕ǁ4;cP-bHi[`0e 1@ş h~𵎵:KլaZ(${\:w5~4y @UQ޺/Fa_^j̖m5K2[Wqڹ׍񵧊d_Z[FQS6< ]&g׈.2]I\f3vz}Žޛmqdۭ^#d~b 1qTE  }QEQEQEQEQEQEQEQEQEQEW):&P,,n dUF|]2uy3OJ?:L1楡iJYK vMQ4/mkL%Y['Y(ϧ=O\!a ?KЬ?1!rÿqXHߣgc|R^-.Q$GyR>º!'D| iְkjJ"8`n cnVcrL9\ЬBOW?wG_|?pmfk} pjV&lm5 x[En}ځpl9eI+,x-SX."'o}}+ eku6͝Fe>da0sBM/J5/_z-x$ Cy-+`-i'ڬc `*?ZZƹn ]]R1bpzkVvWW;};Ŗ:5OA]鈏4E05\K.;d{hܗd. .ljm}:^Z%fC"$f~*YZ',! ^N:ǟyiK,&Վh%p럥5cRW,O x2V}{e8NGu|oϪk>=}5Aq2 ^ԮMmmjwTl0DQp$о!x:拢ĭvifI ' c% 'Sx<ދbg.Rq}6m.C(zy1zJ\oGa/Qڋjl<'6Ok>Qڥko;!/ovtWG:B.xmhmoa<.~mu5/ x kk}DH~'v9=:PW@v-V IMR(n?0kgu/_͏7 -l`YgP6q9좖9I`u92:dӾiimbn 2xi_%rFyžG> X_hVYoNU%X=QiW+/?+ӯ<_-uhmsr'r6z [T,9דް<+mfWrmI}RrjwY2:Ex:OzM7}JHv";-jO񥦃4MN)$Ӽ:< j-Ί⏈M cP\Vl[Oo>e2ƩN y*xV׵XkV6RiS %y23Bz~G]\A ;f/l#Ҟ=EX~3O[]Š /𵾩Y趤Vk7lFTWn$u% 5Ee}lȱJ8 }l;;wW'5GYY[qƯckjDI3$ 'sB5/D(/UC3ncKmžuEqzh별X*Z"˃)$ Qx^,eM6{r]=Z;)m:#o^ub>"h^4F7gX.B]m 322ZM'Uv:ڊ;yl| ¸M_ߊtK-WOЬl-]G,H3X?6\˧‘psgsW;MZ:FcPZT}˸A#{Ur~:Ŋ\꺬ܔYfg`,N䓞}&KNmm4ȭo+0|dc5[2z_vW' * )kc`ʹ=RyWXdGp-n$,/OV=9 QEQEQEQEQEQEQEQEQEQEQEQEQE! %4Қicyڀ"sT97@gFM+A#V((((((((((((((((((((Zjaid;9^H8xRةaivT.|'?{&PfOgPT #.69@z< az ,G_|K9_9;pK73{V|3ce+ i*O$15sY J# gcڜV]H*Ӌ(RTqRI5IO9J>Gךz_x:u_´=U+Ҩt~ms^0Ƒ7ZK_'s}(xm7T|q|'_y4CUncs{*jB2W>$#o7s]"KQեUn8=詷ÿo:-- 8 bSyyQHހO#ᫍ&$IeY:׺Oq_ ώ4x6v 噇Ozp(M0mFn5e '2r=z=mk~7֖-"]cP}=7tWzdRHBH9횇Gwhچ^m,Kc*9=h>=J^i.xd\0u>e[ -ݾ}' t`װKgti[x/džo4Pvl<]KQs1ڲA%I^5<628U {rzqxsA]V|>͠I5I8nJ^E+{>\ڧqKӾ csiwO $c#"x[U/pqvn29_j!F4S.8`U`ĩ>Gl}+h_p/u+t|S NXHY s;קQEvxd>#KM}zB5k[y|ѧڴ09j~*|K-N2Sl<#uf砯I~W?|M}-ջMld|P21ֺo^7NK5HdhiT+Pxs^EhVYjz~yYhwPYGd0^}8Kt~vM%B2䟻qroy#^~Ry]IYMŗÝ:Ú"Sk.o礣n097sb&Sy;޻UFH ׆|a_ ô$9va@=tmW;An4D9Ϻdè>Į?Ż񦯩hi~gqn'#Pu$WyEEM=ktytۿ <ӴOݳn'H3q_};Fu]>+خ+o *F۶ 'b@S]7t+[\±wq0YLL\q[ӻm+ieߙ!aSUQRx8N0< m=  +HBdyڹ߆FHxSQu{<s%)qkhm}إ/C?~}!nts6r68hlaF}*xJ[m/ƥcqCsyfmد!3| `.k(m,yVH! ,-{FC9DK]d˧Fhx^XoWp(^5QUJ<͸IǒSA5{p@e_!Uf 7H EQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE%!4iƚhjyڀ!zpx5vJq ȿ_OI\_/((((((((((((((((((((iocv䑌hOƶhIY6a,=iA7oD?SZxwIK[5Tc:(uj5g'aEA}( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (CM4L4ƨڤj *CW k TЍ]l$1 ,@ILы}fgvzcي薊Eet Xdr0((G"HE,czOds,"pcm q z}UqZpS#MiQE!Pd xh}dr33;1] EC2pcJ98=jjkpM= n$8_s@\ml~i4 (Š((((((((u[ZN!yv 3N8zT?ۖEgnZ+OQi<]?@4VwtEۖEgnZ+OQi<]?@4VwtEۖEgnZ+OQi<]?@4VwtEۖEgnZ+OQi<]?@4VwtEۖEgnZ+OQi<]?@4VwtEۖEgnZ+OQi<]?@4VwtEۖET-0$wH#z=jQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE%!4ihj&$= _\4./jR? B5v(k.?'kFF4Q~S2uk[Zf5)sfꚕŎ(MGpЉX%@#lN{q.y56Wn>mmݢOS8װ >99st62?9%A}J̆IJ}oopnmRDm0ps=\җ./*d.u4R$[IvPϼ d$(6 '-1hs_`^.Y qq]_XI4/qRAau9Y.^6ͻ9!rrONsZ^࿔e֙pvæP W捚|8cVkV'"6<ߑkawi4J\ ;@o9kZV*EF,sѬC7+82nN?SKUYOuIgBUIb}Fy,ЙneXD[JTյh&6t+L[ё:`|ۻc mǧ٥%W$rXRI5>Ѹ6@8Q"tj.D-Q\EPEPEPEPEPEPEPt3ם5X6yˊ<I'H^Cܼb[ XԈYZL1N;t޻94ܽm{3^}`2''pH #yėڍ vTwoEU o/I%XQHUQII%Wa\-4r{I!|vATy{j_g߱#*r;]J=KPMVKl2J3n\0eb~$asUk+ ?e{kx$᳕}߸#lڤЪU١y@Rri,KI$Gڇ͖zUTG5,z.kV9[F=c`1F@4-mt=?3<7xnd d0b((((((((:Ck:Ch;m{(3@$dz*i-5Y$1asqƍ"k VU6sJ$yOsr( ( OcI8Ԓ@7iGFt3:VZŦ[.nk#qbjZ!_<rW'#++Y3쳞JÒ9?Ep&1ʶ賸YtYk!K3@ 7P-m)$H o;ove.k?cܠ4kxF6M}rF.gBLU|̹)i*O6=-oEbn ($֏5vǠ Si_Ά;bҮΠsPǮK1^e۾DBI#}g]G>Kqq#JY22UK+p q O).Af~Nyog ;Z;//Ԣ)QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEaa5D+TM@IY _PhcHT?/ڥ*jR:R BAEX/ ~4}?Khj*ez_Gcƀ&,ޗ?X/ ~4}?Khj*ez_Gcƀ&,ޗ?X/ ~4}?Khj*ez_Gcƀ&,ޗ?X/ ~4}?Khj*ez_Gcƀ&,ޗ?X/ ~4}?Khj*5Q@sZ+(rBsҟw@K;Z4Pw/?w㵣Egiw@K;Z4Pw/?w㵣Egiw@K;Z4Pw/?w㵣Egiw@K;Z4Pw/?w㵣Egiw@K;Z4Pw/?w㵣Egiw@K;Z4Pw/?w㵣Egiw@K;Z4P\i֡kH?γoa,TQn9$أm\~kza Ma0m$r>QޖO"Zj2=Cm5Fl~T~)X[kbgCvSgQAq)2 WC?ɡIG^ɨ]UE&Uw#EqdwHSxz(w,FT\wm$^w_j۾YIk$2)?1V4zi]m 9?uϣr}oO+[EcgJ0`RGB<拟)e6wgNjPRc\ d@~6]֭m4}4Io>nfG#NXcY͖ii:r\DqQ)mrI s(v Pͨڍ(㙳L6AUVKegRK ;F98=3vWg[۶u'Øgݴm~t>cҭ\x~1ND $IەS22uWX֖:t^lk19ϳa00kκS=7:+egfbEPT2> A,9|o!im#k.fo3T]9'˗>NituQۼ[D"6g% ;Q@Š((((((((((((((((((((((((((((((((((((((((((((((4 < 1&Zj'zq; 64CЍ]ZGFEP>%)5/ D6ҒBF2HVk^X <ٚq8 yW^gm';C`K ~.oZcT n] |j!P66Ԧ!Ք{Erᮐ$vzbɬXe_FK1*H9br.*x/L{ྑ%6 `$s] b[=F9g,da ` Vw"JkB4H#qg$qz|d߽Ix~&!v67{my+^^C{4: (;GutP.^_^M "yD(PG@;sZo Do6Ze%E`i> 4[YZu6mf*ϼnڪ;{$uFk8 /,empT]Mi[Yo$p7Iw'?Gn [RY{GUf2A؊آ9h~鶚um5̦h̬YYJ Aߚk:k k'$y] 3Hq_-[v6Nϛ>>ީ|?ӭ/# Z=6"yi#;;]U mf~e!nŒ1D,$y^9.%&R #ae=F((((|%kzj^}maHYx:Mi"ra9o, <ytVDy1 R3"TtQTm& baI)\@dAO#8$`$A"QY:㴞[VX=痕*ƪ!rx8m`ml-/om䳔:A'rl)p o@*i}q{uyuko-o,FF%*-@gUo ioCp s3s\HК-a[XK5~SC+`N,gk(=I& $e-p+ApG}5EQEQEQEQEQEQEQEQEQEQEQEQEs1MN=&I$4l"p@?::+M|bFaiicErR=\H[jƾn J6+GHBX~1ʜdgGU}Q\5ouZԗqǬ6еJ&ñU }O-WR{[\DSix/cyŲ<.1M4oXDUy3IluNW[-uH U (ی]U%sI)'~D77vP[ǜoŒMvװw\Go Ee j({ #'BXh;Tn]pWO([JPmwfmEQEQEQEQEVNkcCi˕C.px :֬g;?a="*nÎw ]OhiHہ~RrÐ2xk2Jt {H.^O>+8u\n ^#뱿@O \k:^j -Zƫ*D6(nrA<5-xkˍ:j fU٧LSi^P3{EE'TgFNA⠀[2@Ͻr#liW֑*ĻW1Jс>@$ea^.\\KmRKIzHb& T'4'tt3{EE'Q@O[S>u3hŵ{AqK0 ZxkyW γWwXF7!N3JOD֭$_BТJfkt@IGˉ;{ .WUWeHcb +t_ Y5]2Ynn,e){bbwJ~g 'CisCf]AD/;*̦B3 k~7"o_@O \~'Nӯ.oqy4kqsx'|@Aɨux~{Zͷ:8 ?0M͔.ݴ˶MwxcO=&M /:(sW#}5c Y>!K4{oncCpgc+!PpG*e߾ڏ|vDd  )[/9<K="*9Z$ӴPi Af (I ܚ|֥%բ-%T(.ryJ?5[ӡc0XӗqK{[O6a="+5 i.5f {0F"Y&U9 xؼwwVZ'R\jhYUx#+늋_oGgkcw G#/,??²^\y{!X=†ěﺺZb3a="+F{EE'Q@O(; G#/,??´h G_X2hd Ѣ3a="+F{EE'Q@O(; G#/,??´h G_X2hd Ѣ3a="+F{EE'Q@O(; G#/,??´h G_X2hd Ѣ3a="+F{EE'Q@O(; G#/,??´h G_X2hd Ѣ3ahd Ҥ4|=")7,?´0Y@O@ѿcPDJ?¨hZ@]VC@ [hEoE"' *jQEb9coq"'#"q$tܠTwմQFJ" T*py/Z #QkfxĊnc0>jI:f-mܡKwh@EtԽ]Rx ,yXy3֋oy8YZ;HW!G̬2}=qYom dn<y7s+uዩa ]M 8'{Tdc GZ/⼚gxct"qY#qTK:Upr9>#-ml;t׶T)!˷˒F RyU) ' G8{)5v-D rCX2^i2O6†a?A#1?y帷Xk弌;H!Rx90 $n!x^.B >pyd2y5ݼF3Y?Zh8/|8m}Ҡ{[9dxa[} 0͸ | MMB&)s"";yP3V5,cu<1*|jm VI#ԥW*T;elpyI?&K6ݖHrR@;YG5о`n8}A T5ZE$p&Vt5kMѮ'n.X4k $ Me\xA'[/>p%B#_zu69ڀ.73liǖ>țci1Xnݜ O^cMF4 nʗl ! T,9YWn/LVhC-TI7|v ^im5BiλJZCϖm:ͯF% grXwbpz}nz/MmsGU;5-Ë{cv!c T#77*jbl} VѹH~YlӧLUS HmG^7d@#Sf,Pqn?8]>lF?<ڬC"YһYY7s!8jK횂2wRs{+>pSe|^>"%aˬ&@ݿ?UOUdV:M:Beyǿ8̓\kͨ$QD ȍSi4$1{ mDvsZo5@bs'ֱ4\]JG7vI#8w>z÷P\ZQlm06n=W: ZF7^!ff+ 2[1hq57(Cay8IKl.Wj bp[),4b`-'qR1g"6g$bH"*M!AX/R nmv3 !wgZb =5Kjaʦ=N[ nݥO9*c}_;_LGo_?C#HApAb7Ǫp99y4FW "iaE"J A'p c%}b'w=T=6 ( FCt zFqw1pR wgaڵjIe3Y#"PBKu9CQEQEQ\utb⸓Σr:V<!1biњq< Zrs2&EVԴ-# ۸OE%TF̪x-@'$`ӳ;M]llQ\֧xVXo`{k9UٕhTr9' {o%,6Z;|0yۜJ].3kQ5zn$X#hq;F\tP?g'>k lϛo ##rG=<<'Y U<Ϗp\ns qZP K:lhX9:ӭMgxoNOҭmH`4E<(8Es\^4C O$<`9lZ|-$3C%ΎOdi"+";  ѿ-c)5k7%?ƵZjȓ_wH TݓPhWA+ mIcmt`uTCЍ](((((((((((((((((((( OCr +*@qg85Eh*ɳP'yE7GooAn8UUմ5Ei&Hqm@u< AEgY}3jvVi2Bj4^ZIw"To8dw6ܺ**O'h?0υfFK#Kb^@C8[SaXɤچ"Xr?oRZ.q6!s%sh(((((((((((((((((((((() -!4O4@ jV+ި\t5~NB㡠 #AP#Wj _QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQETW7VV= }%p>wuK*%Lp?  1\5$F$<`FaNUE&GT0?i}RQ4Q 0;*z+8mmPG #GjsiI[_-GOJr+SJ,[=cEmk#^Zhڌ"oGk6wZtyҢuvo"c,Vqʱ\mc4دKexj,d+,?R=/@h^j^KMT\BY xʐTC-um-p/#Fy+UUbX.[3FcyWn[>j_ 'K)hUq!@JF~PG̀p3jsjR=ݨwhVO1C)U2`@<4W\jvma4Pv:+>yD lqd?eoXy#g!6xk=.u4W'whiPhԈշ4[ *kz~&֡6Vv07v:NРr@:_c?fXQYb2˴d 60q3lju="OYׄbv7pœ$ Wo?gkEPEPEPEPEPEPEP\4m-Ay".brǁɮ5MoEԵ"km=ɬ;wkFLUÜgUP?jZ]QKe-rdڊ@`8~n}u} Ma>Z:ysZ[yJ|22=yKiݜuޣyZXLQݼ_hsK"Ù8(CtQ٥kXBPt GODXQUQaKT%-QEQEQEQEQEQEQEQEQE?4 _{H4Ҽ6Er ό7iism0c*I!۲>xO+Goqqmw`|rq~»|MOMX-}FXIQO1lQmʹ{[=R[FUBXFOI6egԀS X(v:J|߁u }6亳id'uRtހou Kڝ֮\0xշpt}ۑw:V[d`~W94b ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (CL4L4ƨjCWT.:? B5vi @"u #"%| (%| (%| (%| (%| (%| (%| (%| (%| (%| (%| (%| (%| (%| (%| (%| (%| (%| ( EټAur,VQ̀N~f~KԧF*q93|ע~Gf_ wc춲J rm#϶՚+2m;imI1]>K,$7?z1~5Agm [IhkCteY8x3pr+8i"g[{'+ `M Ĵ(@VVIa5(!g q |[QѮ;ޭ歨y P0D"vxʄTO*A5=DžmLyx.nՐI@6TcܢX|3lm,WW70]%d4[D7PlaBoo)v[ZY7v1*n헄>\~o#3oTq c0K>ھyZFP!L.rϗ}y]XSv ]  ÷i0¢>V?iK{z/VXyDFҙ Iq(ooA_xQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEWx/Gt 1 ܼ-T~a>ae}ZH>M|]fHrwr? M&;{Ք,!eHco?:Jk6"p]ycҊa!S.שu\yn|SCM6ko&GO7c'3~6]z%̀_#U!SGA_Q׼9F[As;ם|8özm[(ag?qZyMjNf ƽyGrŽLN#+Gvz|]*E,c==MJ=V iۤn?(c댊GS[8,tԖH,$aV$͎8=,u{5XX;^d'+L[O''q?t?Gg^8S-U;?|#<<&{ƬyyMC<`ayi,z} <),LC)&VI:;bI}6H%!Op8sQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@ Hii !ycTMRD^NB㡫w li TCЍ]((((((((((((((((((((-֭m\FԖB,HcaѲUc _VyaH4?۬i!Xqnega0 *iZ︳>6on3T|)Xe QX4n͵ X68(<x^(dYR |neF;c5jz&${b%im͉<܆?pRzqvj?ڐ[i5DIs``i.y쭘-L9(U>OF~n:v"7/υIt'WH}MN7kMoya.k]m b& F  Rߡ^E}7+#<\t7(_0!sW|U/'|ΐi X+fZ?ٷ ./x ^Yqvk^|BIY,F0 SLsR"ٷ>ǥBTՙ>IlRYN#!D3- Pbh =qYveڋ}6;hƸK2L%Q ջ0AּNX'>ߩ}E*Mo#]Q>ek2NASV}|'Sg(0*QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE%!4ihQ5JPy; ި\t4*jR? B5v ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (!T? j) %#acAE3HϴUh _C}?MUV?owHϴUh _C}?MUV?owHϴUh _C}?MUV?owHϴUh _C}?MUV?owHϴUh _C}?MUV?owHϴUh _C}?MUV?owHϴUh _C}?MUVrgz^멥dݔS\Ir Y=MA^MHll"'RD ӺZӰ6V۳1 cΝuGjP[N&G2C޵Ui͵S()*QSߍ˳Lv2\ZntP7"|*FXz ' Hi!]S˘ʸr7(c]Z]\^,\s1#:®ճ鈘XA"Iw vMV ~0͹R",#bA!I8L@\i9#7w6V]?oID6Nsۚu!;jSI-Z/fcE`/<oЗ?-.-GkrJD9m0w*)n1a|r-R /)m})F_'Ǫqw|{V5ל}x\ٮZJKE$ҥG 7 @9$KKfѿfMKhdu̖` DHCHx=Nе 4[}̄~v#psV.o4nw3Fn)0=Z.t]RB)1}`s$q'vIȼ_ ˸?U5) 0X!An:tOEs~ϹTSYWŷ\df>} i->et.RDنp $d:f7V] - vvg1ǹORs3^(((((")((((((((((((((((((((((((((((((((((((JCKHh 00j& w _PhcHT?/ڥ*jQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEg}o~VY>7?QZ4Pwf(,Eh@esEg}o~VY>7?QZ4Pwf(,Eh@esEg}o~VY>7?QZ4Pwf(,Eh@esEg}o~VY>7?QZ4Pwf(,Eh@esEg}o~h ( (X`ֲyfmBA3ßQGTM5(/'_[G{ed儑sףδ}ZQE (zfg2|BG?J֡k~O @(YӼb6w]1<=zFh(S(/-e/6ew`/Z5?_q@ֿ_SYxwP}CnpBX<2z|~^Pz#Z;GO4ŖPDs c2:ԣ5y .u>!9EsѠ #Z;Gxi #FUӃ֡:Lv\+w/O^z#Z;G(;ֿ_k_a/kF5y Ѣ3k?F?vh #Z;G(;ֿ_k_a/kF5y Ѣ3k?F?vh #Z;G(;ֿ_k_a/kF5y Ѣ3k?F?vh #Z;G(;ֿ_k_a/kF5y Ѣ3k?F?vh #Z;G(;ֿ_k_a/kF5y Ѣ3k?F?vh #Z;G(;ֿ_k_a/kF5㴞F?v fu??v0YY;Q:j"HuӬ QW; 5t* I$ȅAbO *jQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEonedrive-2.5.5/docs/images/default_authentication_scopes.jpg000066400000000000000000002543461476564400300243630ustar00rootroot00000000000000JFIF``ExifMM*;JiP  >Alex4242 2022:08:06 08:07:072022:08:06 08:07:07Alex http://ns.adobe.com/xap/1.0/ 2022-08-06T08:07:07.420Alex C   '!%"."%()+,+ /3/*2'*+*C  ***************************************************p" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?F,kmSVo%ψ4Ớͱ9a[:+JqREI86#[|S"sTηz|}urʂH9Fas2:]*=ŒVn*b1nv8}O0ŷ]_;J+ԬjZKEM$!IX9^U}bSG-#6FU{Pr#V_ OL`g8nm_;jHQ%NpGQJ3nq+6V#[A\qnmCӪ{mj~\z¤k,Pw'5 SN=JvKϜT }sƥNF\>}ë\ϫCj,߹X01gBd#<z !/VX6c# mqGiZy,Jko(o+>n OwO"V|8\xkv)KEդx_caIAhBVPzE*˿hqWWsFC~Rb 9-Ks-bfV.AdB({Q}ROk"2(L~tC* J¸?⴦ڕ<2"]$ \d$dq8^% =oY%<r.y885p"Jnu"b{{>\ L#f+cϹ@&8, $x핕]Bl.6Xf ֟wԑ$+JY =r 3L{umrFe/tF'0H!-`Ka9u\aKٶ$.q \ƭOfmdmtӁ>S`ޕ55]<*qzÝ]y:_,x2X=Cd{rjFxebUrB9'ӌV9l?̉bw_ֿtsL)_ɥ$( Nj+t!+f5+jY3p*=2/+bw`vE`gt>JR]:k6?^Q <)* :oiA_Z|q4jV=_m~V'Pj[kپolKc%%e"lAOY1J?iFZ%BN0Ayp7W4o8[lx+GDZux囟#ӯw4c$_JQm.ߍ Q\Ygc\ yov\<)oD2^bP0 犣d&{Er0 RGN)Eq{nWc ;W6R鏪 6\,}}GdSp32[v/n瑃ե}_Y31(Mh|C!}>5s=ҵAERQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQESQ0B*$IiPEP{ (Qo 3 w|嘱M%vP2)GWbɫ4U97{%E+[QE%fn!IDjZƬ9_8,}ZtQT䨥QE%Q@Q@5#"K dEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEV 7C[Y7l3[_OZfM3YY.CpK}9+_cSz\ڝ QYό|ux6u{kcjg\Wd_x3Qt( 3x 69>Ʃڨ/@ndOֻCmqMn;[wl! AԷryr~&uoŴ,2tt9 djJ+_Yӿg_ X\],*֕K-7$?O Zes5]Adrn@[}I5׏xS~6ν粎_"VO1 klGxPo j^#ޑin\\X畀!XzcIMD'3 ޽hֽbV7Jt#3[R^_|,һk,>q0Nw9RNesaCvWUKǰjzmP#* }NjVpчm22+u ~:yդRgK cs]^ :{8$]y7OaUmdK~7&&Ckƞ'֥}(Hf =O4]xJ7^,NmV%#8}N+󤛏ͩh^-J/#MEuGoJ^/LG^nIj@;r:0=ɩ-g^I#? [RHݮ/ _OK95X]+es&pA$du5IwWcWWt_ }ՎGO ݧ9Ǩ]F:mS52dg3ys8¸?zWpAj>Wa$Iw& S߬Lm╰qQrsVjM7ۑ'\g]H'WX#8 A3F[i^G= %8yr09 3Bn;~tWx?'BiFՃHH >R9涯K9/nVM){Q+pOji_z+~.Ǣ^ujwAizr^ܭSf3|1\>ՃxYmk<7Zj'J%Bs/j'%qk=#H"6cٌ1hךm5]FQB\G@=?WUVlĵW:e<wC%]\2ɷUiO qߚ+ƥw?xOߴi<_ˈW;R9-ٍ-5=YDv(a׌ןi|MpqGC)TI6 ʩ'Z|5͸GAfA<5ź_ /Gyv\IGhı-gӵ6ݼv-;:t?mn෌ʉN7#^Л_<%K"-f$u\Yq^ToOՅ[joikwz]Hbnv/RvzM7Q5MQlbҎloh±W28 EN g}?Ýⶾ%Q05ɣ3E=R="N}kuwV qgs`s.-Ӽ9U~z@FnWkn"xR/'0Kmw/BU pOS H/sk2\k:鷭comO4owqN(lh+:SY%eBJ; n5=X?ë-GPVͻ[\ l3!8q8u#z_?[Ey#׵:]UӢ:B +k28IA%m1PVmg#ҝkgSERUmCPҬ%&X-2p:{*9=\k^5ľ"k&$ŲaC 9)V pr$$du9 BqNO hEyimXms H=z*@HRTdぞJ Ӽ:zٛnBzkf3ByV8TQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEV/Ciq :\WѲ#HvnQR.qvծy}%SöW2]É$QӦ㏮*y{-[VVMVඓl 3uWPrI"!w6}H3s JcC?,ƯCVUY%y)T~mF {flI* Q]MUJNX)YN7ƾojzމX Av]g 炼}WN4Gy(kӨұǞ_nxw춑]E",ʢ4VsWoe; 2[h;7 ʥT0 aO6I.ksHd~V" ?z֥U|>tsY5yEc1A~Q8'85QI1si\Y\q0=8o~ ֿ/Ox3PԚ[\#4t=MZ+|sG\WWz[4^\0:,1Gn1T+ ;]PK|3#]-'=}o,\x?Tn'InYw1e<'kS:?-tip>%6Bm_7o<_{M'xZአHx)|WKk;k[|Tc8#]U6я5g[o=gJM!$Wi8i [yʵj>?c%VZ2Ȳe $WgE>uoW%ռgxdSi:bGES{ )[TG/kzg[ 6~h# e/ǦMQ־P\Y-ʿ+pW8][@o5pna:F d%T%]Vs6m$ i#(ޛVobiC2eFG f\xC"xJN2*1䊯gj׊5 YV]Q{2#$[x8>tQ&i7)}4fI[i\|m^r@p9G}GHwz5DmL0Fw9e-G,s+2MȪqy^oDoq=/scUIq 2=:cE6w5xrY׮X[82I#$O_Jχc޼Oqo#D$<[Rz_֡h>(ρ4/HIќN.L)k^,AkK}GMU3Yv9wER ɴꁊ @g++.6ʌCZξL+y:~]]iZi y(>VlU[škODm#kV=M1F0 X״HuԲ4ܠuuH eel70l=*kΗ=Y洴, NHA'9F9Ko?q ӦY.$I"PFUR=Ef[K&BNȰH1o $w=ygiN-Hx iQS[Dp? GΟagxZM. ;oˑ>pt5bn0y)5Ti\E ,H iw=)wd&sjwpK_+E-o4n,p;Cnڧq|.ӜyY֬t6kصa$ o&O71ObiL_h{(8.鹗80Nq^fFIuČ']`̣>n4i< [aYC(]G[9 c$S4CoshOo$aЂꠞ98=Eg UC>c:\&F|Ҟ^ݧ`G:z5ޟu, $%J#څ?k˰rKu|EgkrB4ˁ39#P2x9)ScotgD6/pT<6@x85V2F-I#^MkZ2L|y'G#w Xn,g%I#9ar{9V\s-/Vl~-Ӵ:ya-6H^XJ3 qPciijyQۤ.($qd\mMVMӴ",gі/, p#qRzu8솜1@4c0PII^xoK 74}Z=:ieY`fFBv GE-"_Aa-{8݃=C`#7~ծ5%2,.ڄ4+lrbO#&Kk4Vk<$ozl wfֶmt$voi)ᮧkxc2߼89U6[Ƿ٧d00gimil&wwRѫ@|ˎ$7\t ((((((((((((((((((((((((((((((((((((((((((((((((((+>A."fR\}ZPwֿ_֍k_a/h5㵣Egy 45cƏMECX/z_@QP?Kh,ޗ?T?e>45cƏMECX/z_@QP?Kh,ޗ?T?e>45cƏMECX/z_@QP?Kh,ޗ?T?e>45cƏMECX/z_@QP?Kh,ޗ?T?e>45cƏMECX/z_@QP?Kh,ޗ?T?e>45cƏMECX/z_@QP?Kh,ޗ?T?e>45cƏMECX/z_@QP?Kh,ޗ?T?e>45cƏMECX/z_@QP?Kh,ޗ?T?e>45cƏMECX/z_@QP?Kh,ޗ?T?e>45cƏMECX/z_@QP?Kh,ޗ?T?e>45cƏMECX/z_@QP?Kh,ޗ?T?e>45cƏMECX/z_@QP?Kh,ޗ?T?e>45cƏMECX/z_@QP?Kh,ޗ?T?e>45cƏMECX/z_@QP?Kh,ޗ?T?e>45cƏMECX/z_@QP?Kh,ޗ?T?e>45cƏMECX/&S~<漚\Rf{рP_ݟκψQ㨑a?6ٍs+ N5*r_FrbJ>hjm'⭖vu!>f^kż7e|چqml',e$䁌q^ЭfN9M`kT;Es+19ҭofuz"Y‚:6BʼDflPXI-(zyDU 8yF8֊/7Q6n^P[[fyXݓ! ,65-׈n]+N.{{f;vUXe@JͷN]_=6=Ա c/$TzzT-\w ۝.O%~{& 1=jY5o4{緳٣}! ߇f@/^֊+"D}9lf@ѿi>h;hi *sN>ԃ%Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Na.$~'WEܛܕdGqm崖p<I"C+<PiN۴M,;hV5-g ڭH>6tI,O[WL]s1Nllmh¨#V  Q@Οdpg[81.&8-pT3U֧sY}wR[J##=]'W gnY`Jbxui=mh$oZF#ddW +!pѮ"{b"&5he\ '<sxvGt_XܛH-[Kwgde#RC_Əs\?~ Ȍ/!@9ӭ>KwolvCkgO+ggrRZΙƏ6 !ڍu:f rh|W}w[Z_[Ƒ>%Hh2yVma_ k-F*mD([zH#SPӷ=?7|3ͬwr@63ƧyŒ o"mZ\"ܦ1RsA`wZeM=-ĦB`ccW'na~Zr_:"Yeiػ0>8w/Xvѿ?=Z-u JgQ9ʤmZܻ*2N[;@RrISmukz852q~RR1fP7du&1{4].+ܻ ǵ%D봁9BwW^v|AA-gŴڄ.Hv~7s۫o]622@G;#S9.wEtoo:( ( ( ( ( ( ( ( ( ( ( ( ( 4))uKG$V F̤5ECgie B+xGkT SQEQEU-+KH fI圙't\,qWh((((((((((((((((((((((((((((((((((((((((((((((((((((((((((\a0#4P<"EŴ[J$U EfYSCӮѦiؒ^;L>xЄs`HZP ]Hk{=*4Q["p:Isj]63hP݌b@~ѯM1DMlS^G@yO __ܯ8>N>^8U(:ú%rGe[$Ȱڢ@*Ԗ6^y-2\ۆт[|u((((((((((( ~M?GKbR[nW!S#$ eG/[҂˅ UF⵵m \:-ռd4YH9N==KVD$ז |T`|#=sp{}{K7 wm3ژR1|< %~BfۻGIx RKk!oI$l. rG—8Z_M?ڮ12ИrX(|1kinnyYw#FђP1 Z_ :_//t9tM-g-c$j*#O4_2mtǹ)/aVU<؂kf4KP6ֱ9?i?r|ɴǞ} #,h?.#&!'bE9jݼ}*DŽm'XQg6UBiP[*p$x~QV[)V67 0 AۧoJWٿf]P F`1Cmy$$.He'Wy8jڂ\j[lm$fHrpYۀv *oEa Pocq98X*˓O3nova24Y"J:0'A>]<ïna|A{ ;?+]7OZTE,*1DxD@3Kp>T^-*NY@'8ғs9U!mʄ+)g M)[xao" r6e S\g8qIxRtXϣ,2O%ݬNgUE#;pN2$d w[y/%LK 7v*v-6a«˞p8MOHS҅V"t` APy{U+ sJu iVʺ`w6SraPqӮkK䯅">o٠7WRmi6+f…V$N=^`խ4w)# Fp0eb9 [mrK5&rHH"o|R֨]Ȧ1 ccf޼Rfh]yb(ay`߳lt5;gjB69”xj_̟p0AϮ{j^u(uF8%< g}h{%l_0uO4:$:xn^iB%]&eaKlAs!N9nV ,^derޢؼآoDtKqlNbL` yo~_V.oqh _oJ X8 B歪\][X閑\ȉ4ng r rOM -]m5KhI5~SC+cYrQzM_M/.lmGcK{a$jn\N' (迯ԭoˋ kmOv]XE6$9rp:dZjK%k,>YShdcw|5 { *@.e3c#dq֘Z˩j[I0E(00\'&忕n@=6+t{j9$\S7wX0ԗL e]LFl@fl)8!*3gG4w&RY5`3u=:v+5JF$b.PFH`'BWӭsvE%,&E|G9*N~R9:qɧedtWU"7}sN@'ZV72[{(FUu}ۿi'4i7ZI&[^4\@J`AF0z1Ka3tuGn^6UCYqd0HO|k4Sg7`$ad(JrV%A_{13;?2 6m\W`+Z`.@ݟ@'~?s~1/0x~&cU}G lySyN0*ɨZHeҴL*qZ@ Dzŗ,?'os3%߻^* o Ccnj7%+<~S0"N ۧ]6 3~:H m60pTZM/B\޺-ܾ*2*O;{?;drO0/p%p@> [}ظty޸ee*v =~XeSn,lt侹[,rq+ *wpB 庻mⲷ{/<8F]pI/y+OZY\$-s.`|++;K.xv䳒J'sVVf#SR[s,ʟwpdӃ|{Ա YE*_j)lUx2mPd MxtJCi;Tu'J\is[A.nd.q͟mt=CiYZ Е/!H?5BO 4,NyW#]@MG͸_Qv.zM[kXЀ$xۦzVl)/$bZj(eq18 1Y6gY4/""F/-}躄pT`j5| @0 AۥYӬfȒbVQS8IBs{躝Gwmuh9H!?*1]G ,^dew w*=?F8T_3r>[;s-#~iFjIy= `cXy5?gikun>C8￯J4;-նcC=mWDh!$ 9i nrՠ<lrꚕIe%1Bx]FIOz_Ibn&HnQ?(}A-n;I $nRdbw.3HnRR.DB>jH $Gp@5vAcnX7:Umec-е qԒ`ob2TwB}bK7 KUZx~?ʒc$nao뻏X RMWܴK“`Xu_\GiӬZ;#$sX(؞o;[9зk.+WU(Y8re ##ڡ ,tmj;.])(z慶֯o!m~X M;Ok~-&abU9=:& K&)# H<'='׳H/o"`V(ʗrrN^'ɯl_g]΋yǷZGIie?]Dw.|#2.:?Q{_$/sٳGAHo+o$%4{0RsHX ˌ1EϏ|Ea;%_n A,9x>Rh5 H/t >`bJ @;FAm4#זzR8k4X ;$֢)QEQEs~4}?OrܤkzQXb+*Fcj4jhZg$Ӭ."}?Ʀѵtk[-$3n*Okmbxpy[fJsԔlMa5 +]DX}2w̮͛DDfGO%N~z'?[y_;z+ʹ[OLJ4;YZkah&@qycO! ~Qu-jfMCQmMT4 &w F2?7L+\i]h*Zv5wmo 0wu{7N>Pv@9P/WMmŹF>d~2l@lGRN-Z]?I9ibǙ,5%qڶ.D]Z-CBvfP#2WIXXZ=AkfBo!@r \>5ḴEVt^A` psvwywmeq}/l գEjnFrV8 ?(.-Tq\C;HM' Fc`޸ۍOPI6*Syvj1ݟ,5=uo5>Ul=a<ѰNyqO#8|{Zb/Z}6"N)c*6$c'sa-urKY,5xm<حuCI<ӌM%+{G֮٬u`Y8JѤǗ1Ogz>_(@QEQEQEQEQEQEUodAcs!` .ʠq܏́\⫧4$E.n9b]M*r1>kwĚkp²_0F=˻9qYKEMM=/R*M h(Z?:K^'=&K%D)¼:sR3Mg'fֵhe/28hE;o;wOi") RXVz+;i!K_T9Y,Ɖvg85mA|$Kw>ʶF6Lì{~rݹiu=|ivڅԐ4V rÂyY3qA=ikv$㑕$3Z2}&-6!{S4Y2!/G]?Ȟ_YKciy~$LnFzFX8njuN@WPf$8123BYnf[;kj b XbppsӁZ2U_OmcHSSd*,bHylg?/ԍiRN:FtFHź?ݑr x]0rI/!8%%,TmK|5 hlyp{7sלT9SR[#4s$@Dc z_~-pͫ}-6hlز }OmYYꚻhR$"Ho ȹ8o2~SLȾӮeDXf?l>c]~j / pi y7]çrt|ggk,qZG{,ѡDa3c5ԾեHYoDP8\NSs׭UZĶ3L1 =l>@~_Fj "pipnDŽe*FrWg1m2ݵ.5oMh:*,8eS-OEU!m5ޒm?) 6Trr1R0iwW%KN}q@ǗЂ9-o//o%+Z5ޟq,!Xfʰr1_-Ν} s;G *yRr pPL 'ё 7X,~u5<Sī=Ť ķ !JpQw(U4KAKo⶝Bk5 YʀdA#x/4+n$Á~io]o[=0 {!hc9%e*975jax [B^0ecQ;IG&V#+Y$k˫EFDX@~%NKˍUZ~ Xڠo HmWJA/Qg̻e`H# A *5MCOvČq$ dIߝhK7Vŕ孞y#6ķ/R-l'('L˪\^Wkiia2p@e=ֲo|({vtG18')8覩;sʣȿ#QSK+g*S6ۺduk^fNd3+o|drq"_ _QiXE9H̻(od_i >^<x)b'ǨH;$Jde8 ۱"tWaҵ'kβH?99E[Ww}6_֣\;jBRp9@'4נ[vnHJ!euwOqJԣWnϓ}}P4i[_?X|soo%O|W^lu[U[7 >Wumn'kuo R~iUv$cޘj~36ե\2۹ڼqw"߹a x㔦J<.ήryaǠiڍN2yDk[/#86I$A.9%_+$ ⑱ͻ?2pe.-v\\4g24d6!ӊ8W[7ʚ|IX g Yw tpsQKگ_{ou4K,Pd=Ag'cH ZúƱݣZ˴H7Ӟ6C5?hK.s/^1uF&bm^u5DFT7''$1_?Tr/QElQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEjw]^b]l/\Z8T\WEr]Z >kbT`z=STU%݌d2ƎcmYA##kej* #F9aNzZ{EKI -$/lCʑk3Z!]"= 44y]%+[v᷌ @NA 'sU;YMkiAop&+TTy ѢZhSm#M{I-_OkyQcuQRpjt^koookcm 6UblaNzZE28c,T }Mg'4=5"ș0w sAZPiކhiXڃj)Fct2ygG#cP;#M{HИ 1>:RZ] go8cEV]*ivhucm:fY!VA9QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQU/' AiXvP~bKHoa a Ԫ8 ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (A8C#M&-e w}{.g׭mAuғmO<ʰ#hQT-kx5[B1Frza״kX% >JD~֥d`mV;rrsր4ۭ{NKhu=!3Im-ڡ@s(8I6Zay{pI&xT HZZ `u+1y#Kc:@s@EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP\--}{%ҋ\Ȋ? yE&Y^=3yළܱn}LO Y7WI]]mNK" F!o9D$:`WCvy5n;GР53!X Y }Rxtku .M`i}8=*dTW6.2jT8oYH 7KٵujwBWZ̬>p9sOw'{/K!h.u򷝃eR7cgh%$q)v $@@Ҵh (Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@E=ҁss9D Υml _s&_a+w[PZکGc#@Z*RRN1OXZBp+-Hlğf6 |Y < f2vz/XۙeMb{mڵ0<4y?!<0r*^ Yk{"1y;v_ƺ)[owS~Di5C)PL` g:uYt_OΟaxo:#X,v p{ފKDoK@NSuCaeymsnFbLu"5=ޟӮj+,{ ڧ U#8hhSw9)ɧGWӉoU>"npzc+6SաӴ)QngO j"#`FwMb!mA"4B閺f-[uė$\+K]jZ>+ojZS˰mr /[{M?Mwq)v֦H|ٙ~dr$1H2]IaiiiڷǨ)anUc ?xsu=WHe X' <+??KC\n21c;ʯsml3Su﵏ҢI da95iwkHge#Ԁ$-T/$2Gpaw peU!]s[pɡ݊6EI|K 2E 0Y%"vCI=1jZqӯoh$2$bee.9/=|ߤ"j'7He <@Ҳ5/Zˡc`q=%̳r 2ǦIm%tho ȍ=yY݊bBc|I޵1 n-DX|~)N{V~cӮ,湖IY6\ T ZH^YK.;FCyߡY^jpڼzy-Wa 7L;4zhl$ 6+h%7\61t_i&MN,|`m4K\>pp%!ǿbRod4d^Ԯu+ nQIe%C;Y$f$ #zRtM&9f&F$I#t{ :ynT>RG6.um{V-kwhfY8 2F zעiokjCnb@vѭ- M"ddxEVu9 @$4u)q>!ռCM!iˢiqc;pr|(.s뺮dޝ}{4kXO6c]+dHze>eo/ڐG>¶GOM\F M6!(V'##1(_jMC=m1>P`/@"zwOiw8XY[*#}LNt@d4c\gZl|H 1#Zo_'+m{5FAs p)s4Ck!3I.4;Q u[-#Mp;gem}* ۼhV5ϮKaTQ4 Bˑ׎l޷':񞤷7r麍W:50[ƈ/ϷxԼN^¾!`#УUŜ[_l.<'9oFG)GC0q![Tx:ij1%|wNju?ۯU--lnR3Hep:)rOJgT'д-{@InaGˏjTBŠ(0((((((;o i'iQPk2F|$n^qWUi-o[yP IGS4z[7^,oXApZ$eKD?xGu vӪf;A3QjG5''6]]ф~_9ecv< umevue(Q=U_+fx$Sm%[΅; PI1+mmm. X'Vh`QE ( t_it-/b;.D-QIl8zgG-M(f,KlROR_JgsOK :+RЧVZC)Á Uּ5kAVr L2sG_ɇOn;|&H8 [ l,݀jΡmK:;AuCf>~نX3t HQƗd4s1>Ko=uq:6Jˀ1-?OG~q#^$MU]}~FZxdQZ<3MrK&)h;Ol8,.^gC+I0II Oyj:^-4Vxf+PGZ]i#u4m71Hb`/vAw0ZEB;}.(#KtP }À:oN+ia,<ħ|s,6^8=pv-؋I Y(# vT|qk{}9o\۳3yR2$(km4$K+etF(+# A9=NMKmiogGio2āAf9fI$[zS:~ nL1:xEVe;7m"^4t}\5—A|O&6 I}kCa{K8+n7KZGgmYmT;uTW!A\?]?ּHUO˚4K0,"VaP^x^xrKJ U-cLiS`I砮5"͐K'lrp\ uމj ,5?G;R^~__Zj:j%!mFUBKcvAA5xō6qpH#3 LvqМKIVKgY!Þ9apz=)SNQ;;uor(tR֟w~8mCZݍHe/ ؐ ۼx|MmŌ7x-tC %$BEwchpk -8%p簪-6M:=OK)[tj7>qx O:ڢ[G>QHsơp1r0sǵ-./i! hm-ܨVA$t XZj6[^_^Kn8UvxVZNF`c1He*69K8$pO-__qV$x{rI}{uqo2Ik4[vwU5~Nյ%M>k,.e(9d( NrbIt].L]:}6[WLC0cCg)9# #ۡXHPw}GWIQAgm1k"@TMK`)QEQEQEQEP5?@rX9'N1rvKQJJ*bi<7_Kk,}"'N}]m]JS5b)ՅEx;QYQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@GE-7{&ooߘ7o>fM{&ooߘ7o>fM{&ooߘ7o>fM{&ooߘ7o>fM{&ooߘ7o>fM{&ooߘ7o>fM{&ooߘ7o>fM{&ooߘ7o>fM{&ooߘ7o>fM{&ooߘ7o>fM{&ooߘ7o>fM{&ooߘ7o>fM{&ooߘ7o>fM{&ooߘ7o>fM{&ooߘ7o>fM{&ooߘ7o>fM{&ooߘ7o>fM{&ooߘ7o>fM{&ooߘ7o>fM{&ooߘ7o>fM{&ooߘ7o>fM{&ooߘ7o>fM{&ooߘ7o>fM{&ooߘ7o>fM{&ooߘ7o>fM{&ooߘ7o>fM{&ooߘ7o>fM{&ooߘ7o>fM{&ooߘ7o>fM{&ooߘ7o>fM{&ooߘ7o>fM+JqKEQEi.ߪKE6H:mNOZڬnr_PXfN:0?N)7g'5D_i˵Q7BXA*Ӌs(99'6,|$L72 }a-Wh,kݴ jm$3I6sإm#}xK{e佷?ʅބmat5C2k.QnHNb-զlXFXGn֪uX$Ng H1k^]n;ic;Ђ;h0KyJ)%FЧ$)ko5^Zq1`jR~xnn^Ye܍R8`yrw?1_ax(f9oWv+g*psq=A1K☭qu^AtGHЇ07Xu#6D[{FTK+@pT 3۵@|:^eoe*Ks"@ь==~f_ys4g4H^EXڂ3b'f{^OWY?UeqN>ea|'l!^h$3FuQ $ex; AVt(. Ex.| RR1TeF/k2IF׳ \\o9bJT vpqk--\jzwtImUyXԩ Hlcklz,RHn" IA A*IWsq6o,>YT`=9QST[R _Yl]i$w6м̦X\7WF.yaB4)3FYA*>VaHKxcȶGK‚rOV':+%[˻<|O(*Wjۉ³b_hi>Ѝkqi5+Y_ZuՌk HA!ӧK p^\\!bKxF+!_iYk6wDyJ$ /˻dqNT-ԭ[.gŬy@3et~cZUaPUHa(F' -0Jk8^څQYQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEbu7.-岄h1n̞N94Ijϩ]D60v;9ǧ] CCR--e&hb9U98'SW袀 ( (wXл$ԛI]U[mF |܌Wz޿jHT4R($((((((((((((((d4x ˟"D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*F;xD(ݷw Պ+:Vz8o~f[ry⺊鉧$s}x7I{ͶO֥Ӻ1H ?Bq `ş-,bARC6 ![eh'Xq(s@#QՀxQIi!BRZ-̧0k uȾS5&]"7Jm$r< @<sȬO$pz:/35K[frEt'h7Qk 0T29ݚƫyq$Iu.50g H+m8 Gdk-S Pϻs1 z viVc|72\Hg|́ SO袊@QEQEQEQEQEQEQEQESVKmxl[RA7vye=MFzF?:q:fh_Qծ^b٬ĄdUhuYiqjﻸỉ6o̥s#9Ӣ˳9xG'c'KGk Fʆ8Fca:wCva:2mZ_QE$:f"eVvkV,0G\톹ruL屁kBM=}y\Η+ʡ|%IP HY:ڗֵZX_!_TךKmï<'sqwI-%uڕ@>iPDdt͹;OՍ^նkWMZI4Fk4K_!R_(e\cqYRzK?!e6myW|R,J19RAIoq?SIXȳ KPp<@8lSt 弊8,.h%ӵs ʪ*K|gi[k'."1q <2p :tBpzus2Bs!^35PAsH=#9WzƳ&!GpeeIBduv-<Λ$7x4g7WeL\劌 UQ`95%^Gg1۷q3:,ZFEc6, Hʹ-(A<O8oCzmR[ZPP#p3}\LGp${}6lE7?0oCH6#Љk/汸kDGF0{;j2R{M65GqKf ڲ"#0x5 JG U#kw2.ncks+86 isfG-9Ji-4r0s yZ_5k[1m#Aqgw"&<ē(xFpu  WY=os/Lk?UFp*Rۯ5VG+< vRa#_q5wKbt䴁UIH,Y bNg袊C ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (0Qsh$W3]Ċ ;l9jRok%6cYSLԵ VSl-!?9W4_!n-u> +3( 0$aesZh qs^jW;<ǹ9"H$xV0 ( ( ( ( ( ( ( ( (1aכB4E7 OLg^ޜe$ӵghwύdf;w𮆊ϩ\k߲41cQ@Q@Vvhb.W7iiþpNHTb쉔U٣Es~5Ӭua1E3ߛʂ;ӕ9^KУRvŠ(,((((((((((((((d4x ˟"D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*F;xD(Ii!'Fq#ڷ*FK&y_eY;ۯZ֔f0FR7p!4/ zX'A}G$p{ V+lEXJ։~>HM--G;a'+Ya{[`! wN"yХZiHQK5̷.S*$i3@ wO秾l'̷7H=*tJ7vd(O.y#YtYUX,'<:Q[շinPH0X@9c]3ZɔZmfWs yr]߇$ؙ$9L++²THm9\5ܠ8xEOr؋v$(}jZvvJ#7Aֵ>=\YԴyWܤA ֭Tn`eO8Vx^I&,) 00HlLsgo%ֳw3+ןx7s$REq$k&ӕު_Z_u5ZtJ;Vm[ ZM9 qM+^-ƺH0-Ԍ p2z ukn 4Q⑖(Ok [{8 dV(c]v)]Ǯj^hqxsEBiK ,I$J7f'%vHϵm˭:m&s%$B&d v+N@hn;3g3!l16 4_ ]Ix-k I.qhO]7oЛovK5ZYč2wV'gAyMFvmskukMw9 m|0{o igicI]2J3;1f#80:)eΕ6o;YD^bٻ2hVޗ{5Yj>n<Zh6(ZF@sZ~Oܛ#j[ Y. Svj^ߴ<]7m\l 2:UṶul,fy(gqHBrrT5:mn>uM-VB%nCp!67Zꝶcgvl[axjSAQH((((((((jmm+y H<պ/,IhدPN6#Lѭ :LP5693 -.-V}w;y&|YNx@c#=tSvqG/dF?QÉhayPl'Xpyg,ơ*;%(r>_$~H5 2Ni #S^_OK=ZsMJV.経Wpb*85Գ}PߋK0Hlcrn1֦k;[;8^7~lRNgpdYd_۪[pݴ6G_[SU?,2h>̀3vQBY0@.y뚞ӓkcU{jQEHŠ((((((((%Z볉-n.<)2Iwypzo $.q3.LB,K%c$Rө7[3b$)cc(R6@GΣzŀqTѭZ &PU>nFT=1]7X{=N&u{Hf.Nf\=rjxR<b&}:2݌er09`oU<_=zb[]e0 VWl)YWGƃNVh+億QT&F JI;_ĚEipkOuh-.!8.:n&% p|HjWDCMoG3ŷkP`8ȷKA?f_ jmq,#Im,v -00pK$p3jmme{I$`#Yꄑ[&.v8OJe۸2lqUKs;W9#+m,De]|Q8<o6T!<#c8Sc >ڞ% -bikC J_A9]o} zgVH.7֭[nHbX ;p9TԴ,`XomZˑՁ$DXJW71VԒ,n}/BW_^NfZ[Vhm?و$'!؈88 c/?9KGs+k DqF&MF㌮[j4Ou]LFQI!@c,NH'@=Š((((((((((((((( ?\pA`,a1"Abdr3|T Mv4AeY6x3uwc䶲J rm#϶t.u-BUh {D#9FA/?~kO3- (`Yxzֽfi'}\\)f1Dv2I2xL((((((((( XnżPa;y׷5{4Y=2+Yنq8+(hsW:%v G*'jPEPO?z?zHeϷ?z?z@>L>LE\3Ǩ3ǪstPϷ?z?z@>L>LE\3Ǩ3ǪstPϷ?z?z@>L>LE\3Ǩ3ǪstPϷ?z?z@>L>LE\3Ǩ3ǪESdh?+EGo?ϜߚT}_O* D7Uh|>/G%94z(K>s~iQo?ϜߚT}_O* D7Uh|>/G%94z(K>s~iQo?ϜߚT}_O* D7Uh|>/G%94z(K>s~iQo?ϜߚT}_O* D7Uh|>/G%94z(K>s~iQo?ϜߚT}_O* D7Uh|>/G%94z(K>s~iQo?ϜߚT}_O* D7Uh|>/G%94z(K>s~iQo?ϜߚT}_O* D7Uh|>/G%94z(K>s~iQo?ϜߚT}_O* D7Uh|>/G%94z(K>s~iQo?ϜߚT}_O* D7Uh|>/G%94z(K>s~iQo?ϜߚT}_O* D7Uh|>/G%94z(K>s~iQo?ϜߚT}_O* D7Uh|>/G%94z(K>s~iQo?ϜߚT}_O* D7Uh|>/G%94z(K>s~iQo?ϜߚT}_O* D7Uh|>/G%94z(K>s~iQo?ϜߚT}_O* D7Uh|>/G%94z(K>s~iQo?ϜߚT}_O* D7Uh|>/G%94z(K>s~iQo?ϜߚT}_O* D7UI&wBcy@Q@˨Gi)XL4mSڵjl).ec$Pig绶gwcrr+3t7kB2J4蠊1${N4/uGWns*4]5*c4%,w[xˆ<|+:յ烼)bIe?5v9k `T%f~RF L]"{Ǻfsc&6,A'ԭV9c%:r.3jzyd$7 ?<ح]VT=_hg&fƌ *;=so$ɹb9#kmeϿ ßZ|>gwp̶[Mn$PѐUuNk?1.Ϊ< n棓S°K E\YX^Klc75q.1_2N#;7 cI.caVwz k^yIul!iG`ݿ8=v5Yq1&WxpB6ႇ9Fb325+ rrͯ7]( ( ( ( ( ( ( ( ( ܖ:k$򬦆l HA' u'Γ꺄},q1\,THW' K[Ndlb#͊ kZ) P}v)إo[I҄}_T'Ib69vڇԒ٣.m*9M:3PYxn'g7%C0>xGsRk'ݥ6ңvy;~t=kzwmR9!q $f]6a@b@k:hѭ$ԼxVqUĞQff@$`dF'ITn.hoY"hleG _  3.{mymXpzPGb2ru]>>%a] 6S~F#/vg1;yM[ .R6iut Y=3gY`, 3$8'+=|(#Kfu(.`[~NG2.NP'N_WEv[}zk Fo,O@w:VTZkontrB6&XȻxq)g6e6D#@w \m!}Ef7n^T.72Aʐ !yRph mQJFnjp96b>>׵dWkq8 b1r9oChz:fe餹1l)T y8JЎ.}NK%R)2=TymO%J>`R]Z0 {iկūo7 7 4ṯ!1U8wg*,PspFN[o_/㩦MEŅ7M%FVbBP z5մc^CfW^Ok5(ΧjhV] q=1nMP&gQo|N淇r7#N pŗ MKDyq}qr᥸UQ; 9Ě*Q.BI3*bHԤ".[kYL7$bm 6 )J;G oym$mY}BmƖ&nq,McYSLԵ VSl-!?9W4_!n-u> +3( 0$aesZh qs^jW;<ǹ9"H$xV0 ( ( ( ( ( ( ( ( (1aכB4E7 OLg^ޜe$ӵghwύdf;w𮆊ϩ\k߲41cQ@Q@>M?>M?ڷHX*b$W/Pk{+f'VT(IXQeOOvOvR2?;G?;V ``[*}}niiպ(?;G?;V ``[*}}niiպ(?;G?;V ``[*}}niiպ(?;G?;V(K#G^?2*?K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94D7UOEAo?>/S@}_O*K>s~iTPh|ϜߚU=%94Hi3+M>(+Ʃ2z=+|=Hҩ%s Rv :U[VCOGf޸n͜ OI-6#Aq$.pHޫ iFX4>FepŁ݃s?(ǞA_y&$ڍ<͙L}V[8:6T74Lg|K3$.Ð %jeKDq' R#8+H$`xޖuCy&iy- Ża~W9¸eoM,bxR H7J#G7:O^+S:_u ptPFxlGN*q(43\)!l)9*c88\t_AzvEm%ӫ6nl4`qBl$mZ)%շp c#|s\}pkCDuhud{c)1x1xOGKyq"{Y#=ނ#ӭa co #vu'(_SoQu{`[緖b7d`7|/Ң.V_wԗmco|Hvf>gJXbry1$ms*:qQi=t<$ of&v$7([譤M<$.aB@I?kT0CmrN(pJ ( ( ( ( ( ( ( ( ( ( ( i-"v"c F|p#4/1ޤoZkFKꚅԆ8n-heؠ)prUhsֳhKĚ ϩZ]_NDJvT ہCx~'Yd3M+H >W >51^|~q1ҥ 0K /1q缘$Iks /i ˢ<(#x!VVP$ȎJЏp(RN4-7s\K;X.U#jQ8bҪ֖1r3s(Ƕ~fzy~FM-B((((((((((+?rXz8ʲM,n)!GrK:OL屁kBM=}y\Η+ʡ|%IP HY:ڗֵZX_!_TךKmï<'sqwI-%uڕ@>iPDdt͹;OՍ^նkWMZI4Fk4K_!R_(e\cqYRzK?!e6myW|R,J19RAIoq?SIXȳ KPp<@8lSt 弊8,.h%ӵs ʪ*K|gi[k'."1q <2p :tBpzus2Bs!^35PAsH=#9WzƳ&!GpeeIBrN6mծE7e2n95쩚k\Q8z5TȺH/Im.2a w4^fFPԚ$YMC, >lAs!N9nR bhM`5ܬ|$OviyE5/ ٿ=iI`l,9O{:l/KOz dkt su"2A>-Jmd.,3T Eᤎ{ ڍ춺zѼdMgLqRkIG`>Iv<1ۯK_D˭N{KN[)Vq懍ompW7F;i%ۉ4_Qܤm21q[R701h$f uVBكzt-@00ֿ̯ ]j:j\*3y a885tt(3^v|Q;$)m7 ϧxzL[y&f{u8r(Ѕ֡w$P }w98f:TʗƷoG9\Gs=pqqj 0UrzV|eo+mUb'|G#Ո<#cVЬ 6N(V09RywJUm^PB݈DR~IW y[)6wO"ͤK"L"w(#nK 6::ji$u*j|~RÆ灛pĩ{vL> 3.c3dmXaԯP[6!J,pēpkbJI_s =r5sDd/0– A#l^@ R mÇFFqsgVQ.d1BPxȭeE:]Ӣ)QEQEQEQEQEQEQEQEQEQEQEQEQEQEQE⋝N,E"&$PV@a8 T FsUO{^)(2,&ҟqηncIYMrYyϚeΥ^Jaoqa!?¾0~^_u[oíiY\<&xѝYA% /#Z׬3DϸkBR,f=șrFI OQEQEQEQEQEQEQEQEQE ּ0, qo2zc=:/|Q&>[Eu|k 0۸8t4P }JD\T=M_((4h2QI]ʹ%LrTiلdtQE!Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@ Fays¼~dTh|>/G%94z(K>s~iQo?ϜߚT}_O* D7Uh|>/G%94z(K>s~iQo?ϜߚT}_O* D7Uh|>/G%94z(K>s~iQo?ϜߚT}_O* D7Uh|>/G%94z(K>s~iQo?ϜߚT}_O* D7Uh|>/G%94z(K>s~iQo?ϜߚT}_O* D7Uh|>/G%94z(K>s~iQo?ϜߚT}_O* D7Uh|>/G%94z(K>s~iQo?ϜߚT}_O* D7Uh|>/G%94z(K>s~iQo?ϜߚT}_O* D7Uh|>/G%94z(K>s~iQo?ϜߚT}_O* D7Uh|>/G%94z(K>s~iQo?ϜߚT}_O* D7Uh|>/G%94z(K>s~iQo?ϜߚT}_O* D7Uh|>/G%94z(K>s~iQo?ϜߚT}_O* D7Uh|>/G%94z(K>s~iQo?ϜߚT}_O* D7Uh|>/G%94z(K>s~iQo?ϜߚT}_O* D7Uh|>/G%94z(K>s~iQo?ϜߚT}_O* D7Uh|>/G%94z(K>s~iTHgt/?WȚ}QEW3mFJ[;eMc]5c>u۔e.lskjj)Kdrc)ԫBTMK*ڋ;}Fh^ 6ڏF9Aq^pk: jM!a895em.cI3) [EcE訸V&hF1Z&]="fL⸒=B46#wڭ%c#3}%Dž/4nk-$e14y(žer1j:md֚"XX*2V ~i?GeKGE/#3m8\dɯbߋz<72[K#J|9vfBo-qm/?tܼG=xDtz[֍:ۭLztKR>32QTn#pxZ} Mن[4{^AjH; @ N%։Cuž*eð$Xr`WjM=lmH®_?ڷ;#/n#qIlEjV:x8⅀2G8$r6dF/-0ʐ-HȞZP8$ddU1+ (Š(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((+u +:vi+ˤ_˲;8n\pXi7as趷,tؼ7~gPc$t^#^Kyҝw=Wz_L-DͼQC>Gt;]Im]$fSgnT:>qk_&K0daF y~[ilTs6}?K[[=nkW rPZGo 1f6Ȳ3d{|7IS֌^KvZFʏm {E~7gQEs:=fU:v"j2Hbʶ6Ay:WWRF ʊ뛫]t1%ͶkmG$3nAjT-Ϡͨp E۴UFp<еv= ;wh^I\*'Iqs]I+U+ہ]i~8qդ+ VѴtKJ@c`'ۏI^[ZXmⴖYa1˞ZwN[Ɩ˝HN=NjH=& Y`,㷸bDV` 5>?M?4 djRi֏| t)G|d~um+m"pvȢF!k}&deiZ8MHakM@"Q*]7htҎm0(1ҕ4}2% h$`P U{1RѾxC@߇Q (ZyO[ c.\9f3,<G `ZxCF?M @ܞ}&}&}&}&s"'-P袊((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((nB܇IEom̗?'@uxóz,(2Am-XKնxճku \[>88    "T|>rڄ"{ӫ\h7#w +p=ǂ49|9 ]68^i(DewF\[K2) \Dn8U[.]BO[BTY6>U8bE0PQ?fʥI' 6\{kMJK#I£]Bya "oh%7 DU LMij't+>I ˏVIޡ{}u1)X`\"*nnսԒMխ]};wX H*0C鍚(/@a?`1Bj;Ffr vp6Sv=]]E5PU0@1 Gl+bӼ=iOI34nU9'Esz., ]aX% ŲA2*b\r(8L.ғ.IHG^sW|%}Zƻ|>Lxdɦe{WF1d;0I9Nw(^ zw׳-[:_%cf`@U~N6{͟D SQk-Ⱦdf Wnxo@- [IXgy}g8r"z|[c/o!l^$ pw7dqA_ ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (?onedrive-2.5.5/docs/images/files_shared_with_me_folder.png000066400000000000000000005571611476564400300237700ustar00rootroot00000000000000PNG  IHDRu;&Q>sRGBgAMA a pHYsodIDATx^ֆYC.օBݽnJݽHŊ.,?ϙ|5VXhL|2sL:uv"n[=rǥ}+:T%JS88 qaG8U9d#S8:cS<{a㊺pY'Kf)j:r3O̜#ӡH^҈WBFN!3"Ȫۨ[uS>M!IFƒY.gow=8/// gd(JŕD"2EޕDbKƾWJ~8J!$sk%$ۥ1J>&Y<*G<(;!iӮ}8HK]uNSd;~[ M UQL~ַqy⨓L7*y{gFBחoyf}[C?Fn32]}du}9n/n HݯYwQI㉏: kHOx(FF*#tT:#b%[Ypa3aSѲ"SLVlBS&O&AѠ ?̌LnՊT}([1^│4"Gq+ q2ʐ&rvGr}rFm"Rw߫ץZF<~$KIJ]ϕ7JC!))y79hRB0OFPFyWWYK+疑y_Fh IP!K'u f/' XYnr4ѩR8RK]L8J u@'u1bVdu#kpʾդΈ֐Tԁ+m+Rg"qsvGŹFI2ݑ$XG莹O[2@iȨ~]ԋ*׿UJ$N^&u ~AMvz:ꂾȪ _MRHJʍ18ID+,1"/uQä.K v%C' M (s"($wY+|BW>RsKb'Y5j%u FJE lkץJꊸBRJ=^hsV<>A{Cy (q: e߃-ŠgIQ^})g5#pJЭ"u EieHݣ.DžsdN{U5 Q. K8K9FD`(ɝr׆xXfU B'eEy[3xqTK"n¤n(msRWzEVtPD؊D9+l.ROK2K3wj沎/wJa=ꞿ*rLgֵ\ :՚@$1VRP6N@}3w3 . <.kb$.b:1Ơ( i@D*#iEbr?ݖ lk3[D*J\x%uxL TYn#N* %kH]ʼn]22w;\('HYU N9BU⚿-dH&L[䙦c]r_;:͜K=,7HI}]o;=oTnew?foR'ʉ//J+O\}*7!9m(N ڇ\Kws}]ϗ,k2VU.`A uU=y%'x%- ܶ)'Q5;+GiĮ6#gJ2s{1%uAЁbQ1VQ#iE%{b#e a*%v!$bWd쑂 _q($txBqKȩVϙ:2w }ȭRב#n Z]Fi $u3x~^CNK)7V ~h3=8b2̜|ߡҤ:'Ii =+Vԭԝ]xtdaU\ h%99SOL eE%u>#NF uUkKqRD|\I ?V>*  4إQۉG̔UUؕDH~֠4pdx LH"nEw*Dm'u m-Ҥ.ؕ-'uYA*WJKJawJwI#ҸMRpc`bVDwyowKY!,tz|)}#p+As ·냕qώ;nw{L vV$I@(2ZT.v5TIHT=+_1SrVRդ嗶WAQR2Ɖ]5jj+g/m-:KԞ;H]Jܯ2w~;ɞ9Gz7۵LOv6^:wòE~ ( 7H 磼aJKE138!v|ϲ&wxZH$JR硁#́C;MN;vjbWmN]ݏ}廨[V^ґ&c9#w_2p[s; bDsX pd vB-8*c CC#>@nqw> ,^IHʳ"(V @]:AbuEP$x܄P?Z U nvf_Vxեc nX~Kr/T+tA).Yg8mgOS쬨_]$;koiAcPد6eo7+@WDuYnҐt~BB {;oq7urX TæGW},7 !v+s/ ؕSuB%({K!kw_1dVU98?<(K#{!CqduA)웻I~ݒumzߙi JH(L$m-AàtNǬb+.}`np!F_ePbيeeb!oG.}kj3%!#ۑ7_jHwrej]]u)H'c~xw.>nʻ~v}RonGBR0ӜzJsEo"~B[d ADIaKWTbnIյ׏_tIb-Fv]o G2#Z:'I=8f\l#>vnᷞ7pFsSbwk{ZyQ/!vyt9f l8R*|PZ]&[~!`K!%A8>yՁ#ʀ rᨢХ0<+r]^+O Yp+W[ҕ:qfIf}7W i;9YEM6Rإ &2a7 vsǮݐrL7EЊ`e;9b!{wq#v^!9]/9 uѹ;v9MPw#ؓs'!uoߵHS+"Kܳʁ//,[ʍqW爱ovjx{d'9 :A?ˌ'J?R.H_Gw>#zI]/̝ΊHPH&lAl&CIL w<ݞ3IS@q% Iq􀀊ۍ|18hJi($';e9q8&^q0IL"\ǵսvn+ 7h&K+q!+CR2"zJn+wGD6gDMőv|MD:&+gweGpI 6Cwwd߶)e-)LY n{eT[#5Hr7k߭#ilwsשeu/vkw-Kd㥛*0YR͒ori%5u̺9ho뱼0Pydf@%P6Wu–l-ޖhpe5AlySiK<<+xzYR꣬奕Y6m9&G&RH( rq$.G|&9"9^yQldeEJݿzq{RA,t^^Att##42)@g?( ?lVΡnb}Ts"ȨWRKKT?29w:jpT1&GVY:U+̺CvcKN'tM뜝Oz,w=E\0q[+yebS5!Va%!HQI".)-Rp=B)$dPW*+IiVʦf*Q$$~jIBR>┆?-~IqIw>̽(CQ}0̺+m,=F@XF.>!I<\i@,Tbn4/gX$LX[dk=;M;&9LI0;#u H]5hi_ץEH]EVL4o;n  $'{'$??_dÆ n:Yv{mZfׯ_/zqF5q3? }{NX(ة?+g<|U cl2u xܣvwtsm()N@='{~*~Vt~]@@@@m}\37_ՠo$OK'nu(_(|6Q. kHJs[/sczyX{][tK.qZ==؊%g.ӁR5v|"A5}9'G'nɈzg#r>9%s99|d|yKے~`V ֐p @1ieaZr"iU'7!T~VW@?ŦMdŊ$i3 (_@ [ go爉OU!=7F=)N@@@s0gfViWQ2MCJFyʇ)v޶墜K);_/{8>td68Yr|A)󮍝19r!)iyhGAv]2[M겳,Βȝezz۸90|X%l<ߝFG9q͛ :Aȗ@hh!^ꀯiz ʂpāӫ(,]wJB S_ `[@(#$c/K++ =) }(PQV߷fmeV~S娛>y*ʷB{h uINXC*$#Ǒ;G첳ɜR:g娑UKW6<$5Vs[< qn5Q~&[~]٤ ;v+{u7K򳼭~37Lp720Bi;&|Jmu:+n]g3ĬG[ mm' 6,l^bnf/ IiViy_w2-tU>~nnطA[1|F}j26rԩ{Fɮ{\>Y #sS2wJ}|}vG.OTEE֮n,Z*g`˯h  CLqR>@òFgU],V>HU7 ̎ue(/&QA[Ť.y6<3j'|*8ұ͖&g~GŜ@x8qk܆}3D3慄 +-W&GyWqM1qθRV˓lۙy[cG~~.Ewے'Yu٫NRg~Mnf>0Q-AYx8Νb i V,{vH{ұcjGn]gҪU Mڴi-|V:uTh׮#F ՛n|~:V۷֭[J>.ڶZvѡ7*(t;(І[Rj'h<3<ƏLhWbjر_KK wn-l\;i߾eEćGYcniK>g5ok@:7k|@2ްvX^dHֱR:|BN]bWHtK1wj9"]g8g$9V:;$3+kWGv5re.{pI~5  e>uTS.{ci{Vw QF=o|mW'+77"e˖[u큩NP?K.ѣep~xE5\ӈ3f,\0V%)͊ Mw^=\Nʖ-fS;vL4!8/t2{xvi\'f*C{QCjs0GQ5J c\iH'+^gF(/ies)+s`N܌c^ Y :]:wbt fߎ<9;>c;NLh)&߱D2gn]>2h%F`x< )ShY`…YyƂVIKLЪԍ5VZ=܏@:޽i?Tӫ(LjJRPtIuP~=͟?7ݖiӦ6?)m4=~7\|YwXh{a M`nIż~ ^zɒ%K3fLS%dΜY7c%\(_(|6r`~^%jc&|'U"urP#vu';u۹d;iE]vˁ9s{\ {^H e:uv-v𠔌 zamx,宏XڵϕݏwZYnbڕaMP v f:٘X~qPMFeOL$D X 6GeͤѪnlar͒-/+gq(ũ, Zρ:)oSwpi! 8;os,|ǎpgm;R:x zn*_vYab(-/;kИ:?rh=BRgnYYaΚ5C;p:s#<>H]@@@@QX'% )6 ]@#rŝqj9607Gm\Q_ZU;Bc^a9.rˬHX2v:G2YW;v9w2rm;RB Ӈ&3kֹ XBz"ݰFnX& k8b4ʓVԷ4fɎ:5Dk $/#u6KU-ȫ6:*"%17`vz+ 67se8/>^iH sc%3@SS1w>+uxʅwv9߅kmԱN^9[&֬=tk;7lx.HGVn(sJYyPmΒ/zLON[] 9 U͜9]5s~tq}u;2uei u (}>)+)[0_.^Dw^ G kW+[Z>l,HA喎Ar uzʥe~^Dv`ۑܼ:Yg%8d&KJnS>~3Ӿ3ӥi|IE~2uJYYVY^Z<}D |ȕ& IuedyfR8me߾n[oYgGBR]R B7=8W n3gϒAC˓O?%W^}}9r0a5_qqԟrMx+ZUYl,HCԹ]yq٤2 sP9Ra Y6F5v3go3ID'-l7/_!}Fϑṷ]Q6Um*F޺!u|րo$`f)=g,Z2_M9KN"yL3w~:\ItT(0&c6+>I+^zQ#]jb##/_> (3e˖9 .Y&Z?BBǽFPY<׏Y.pY3d2x4yu933ΔNo,'vMLEF%SgM)ĉeI L,3MwaI)g#'7>[N=kG$-[]Z S'h'R&QFu7&_dƬ?q41Ņ)&Y3j3ah\SLs'cK$+TW)~܏qLs>I~]'LRrUªWɊ+*/\. `GGӏ1nWfΝ#:vϿ@Nktin^pcΕ%9_:t䏱qqSi ʖ0sv77YXv47_ȹ͛/g1/VwȋLg1gbE%fL6kWRZ\ĞT* Gh!Q()nnL[#u9w~|<#t9)3JDJCpEljۣ"BR+Ek.w:2n #Fjʔ)Z6ݴe1]3fNq#XKʝ'o.ט6c0ٳ .vڻDãtMN/`"x<%ӦN6%`&d&q!D:Ik'LH L@y5ucǎ>L/nH{%W\H#rWi6rbaܨ1F,7aH3o¸Jƌ+\tg_Fn9,GN?M~t=\i){ }rk^zOs:?~Oqqu.%k'39HnVWQFs9=2T^- e:tIIƍ'. `GI$Mq%aQJy<ȏdѲhJo~/'H. ӯՌ)7?)CeG^,'J߹PN:tV,ȆΐN9ͥ+yiΉR(|mjRG,n܌R()NEQ.E4.׎qp#vNS~UaȐAc#|'| o/yÑ7O^/:pmԸI 0L[5T̛-$oS!zL8Mʣ zY(CEIoN}=ygՄ&???_5JIq|OeH?KԭXJz_44ފ¼t|R -g:90Z-W^ҨEET+hb7Y*_~xirίWZW3̙͙r6x}λFM˄IuݠxN7jiJԟ6ܧpvgΘڽB ?:K҄X4#w>_~9Y򄉣O8E^xePW_M;BƛXAO'Sqm}qZ2*irǤ l|֙J.!u'rЕ37Rg4TRǼ3rgT>V5cʫe+G֮j TNX/ T1bHu+=R1[ߤ8z<0y7>)O~6BYQ䦧HqR&:J6/k0e:kfk;Do'9Bw):!EMh"א k?$(?3yi|ӈXTQ; h,Y&gpq )fQ'hJXx|]ֈ;w)O~4gZɗcN8A;_2ej4gI'nVM:A s3 sy\x%Ҳ׺4SnQsj q#ϼ|gG~ȅ^x8GZ? /=1=Y%uhƎe3)wu3Z%<Я~[}[gѣ\.?s.P. z,>L?eW]Z:HUsϖn^+YsֲM[9Yeo_ry=oaѣZ,ES3إKHKȻ_}4Xwr%E]aA:(Z8%p4gq>i#ch{(#LCz$ExAA /o7U: Q{XsIW_jGXǷ .Fl,-.tN>UWuwgD1GN<$}xo@ZfYzkֹxؼE$oSlӹ%erSwɧea+ZΪ'u0V%3g:yW-)NEQ.:fnz7Y_BR+}G.{"|}0]R_gy c~+=JxhVa5^` 4RG7D67{CotL~q)}̜;O'h $+JX 4b,dY ?8>ȧ2IihP_~<D70n3B!tMY${_ rƙiǷyH)B5='>mu<%Kn6}Ve,Ō:Ymԡ>rjdk6;GF6?oG.Q}:#@cteW]-_|Q-[.|BӴ)w)/ڇUqoO#$"eƏr4Elґ3gW޵Q^^L"}\:\ϓOhGLGG.r%};vph-#$qبѾRt_~w=CJ!oԩihk@\ VO?>8̨Ln8=óiYFvzM;~߼eb&JˑH]@@*<Ἴc ŋ8R7YfϚI2cw7ԱMK$-^4uq .t7xB|+oc~bB>{LAH@[ܮmՑI3XTդnr7g%uƍ=Iq* u+3F[Flpޅԭ /[(4D}a視"9RwӃ͌*ϑ:4uiRIyDn٭0;w㭷ĩӑXܒC*J~A%A>!u쩣 ߿< DS Ο5M@PX;[gg4VmɧEkYGGBQ2L;W3Ur-YVocI:⢚?r)'˹知]BSy1?p; /˴Yu`@K#trYH.drIh9{#uo<tb%dNۗ^&XRqJ^z5GFʴu _U]ݜ!?կ^RHϿ^{Miu[]{\F97yL%̧r 2Tig6R 7e.BxzW*9#omw7Ќ1;ӑ?%֣ѣO_7xJ^z|M{M2ROZJQQWwPS?yҋ)"Pa/D0KBYNSR=z @?D}TQRVKZn{꜏|]{qje6ES>{9fِ;nߴI܍2V QF5~ZQ>;19uCia{9);(%^wl,jR );=ykunbMNRvoiֹEJSJ '{!rs#:shar3ߪzOH9Zx~qX,1١[4CJj6ѻ[<7MFIƛȆVlbS|V.77W'}*^{59LJzk4uʊ٧Oҽ&ɱ„%-;FK0,\z.[`m$7,G ,s"oL5٣Nvx|`kgth1ykM9瞟nBkT[g-z6mm g_n &P><}G!"MБW>4t;:O Z״򐺙hɜrE(O9A#cD'ZN6m2>sJNi޲Hआ 7n,=uWc2r3•eТt]zFA&yhO!_'ruQG?/#a}K[>Joh-/ɮ};Z7EI<3Ygg^xQ&ZGҡnyn,RHgKE&#QUHJ8Rb\Iۿ/~߸14ѱ7OUAi%2jgS` I3\s<⑿(`2bf ԝrja2.Seai3XTդs-p@Mݩ/KSQT?+۲Kdo[&%kuFrsEroNR~Xczkf䆧HQsmp =u_~'uƈi?z]쨧V7I:M&ܤϲKc:Hw::SG ]6}jH xgt]=4iD~_00МEc IFG&}Z[ܔXgohKI$"cu ŠÕ\qJ&Lu0/SMg_|n .=p#]#cr#O~dR!*NJFO/OӟK^t<ٻ?ڼpnpc}䏫6r͸ZĒ123qïy $^:}=6fVgH|7A|;W6m1B~y:>2(O 6U5cn <oYr 7+)NEQ/];(%}XJ#6#u!ZjbfSTZ <;Z~[oL߽:U~4YWOа/ d䮖 fpL|h|I(nUNsL5th.)򱼌1ɓ5yjXv Y^ ʐ:!kǾ: cHo.0twPGL0!E|ΒSO=ݹqȑNkƏ<4}n8dͥIZh0{&qon(Hq !|ĔoA"ug@dm!kVg@_JǼ hсw]k%uiZ #vnmPWq~gΙ,;$:oaƦ~+%7| x[w5včt)'?ާj1$4oonV]65ռ .$Hi=$I҉vohWUC6u <@Ob94i3Gƍ>z[=<5uKkzaRH-&'5d,|晧qÏ0%qGY7sϩwM. `O^in]tVո14otI1uÓNpjC;d ö̙߀1܌T|l3`dEpv㚹;BҨ7ֹʌYۮfΕiX0De:J)2*+kU;}@8#[KV(_R 'uYGKFN9{!+ ͑)8]4qav́.}vIʆ{K.;ƟRz^$=?V~hCg>"O}YN/y3ΧGJ7vͺŒq+(Hs.Bmڴr];QfȃD^Z{,/;p靴_*~cH '(w}> ,e)@!RA~~)d0 'o䱟X 1iBJ2 7TkA7N,#SNo=u@q%o@!L_O>+{OK䌺>6Yc /v# a-^,R=^4mT%)?l2 ؾP8o+JJJq ?}B.2֜s~ ݦf\}z+N4w6jY<0nLbE{v3~p1;{TGs-+ǟxaXv8Kd_9e_2n#.[0*+kU:ǞEqZ_R .3TA =kG.gK֞K~WIA7o;R~:]E5seU1w\uoΖ5 <3@Oߝ;-KNj:Gpir92dΛHS;1=o&yfn2aCc߉\~JN~_hCֵ{7 ziN ΗZ4C $'iYLFg2Z̈́ D8KzG~g/ B;ǩ}Z6C u:u#!\r;:,4nxj2nF<;J5r ?.Dr~ ,f'y_,'kӽ,Ճ#bԫ1kwQ6ܸ6F%<ۃԱ07@/X4H |٘/Y)+V'nͷߦ\]w2ħMCi1܍K,E ccc\&sBY>H/AO=wyPDfdjeχI]ͺD;J7[ʒ}s$#sH82wtz=mGVܵ<,weS^ Z#ogȽO .<1B|bd/n}Vn1wPGhe‘:~L`Qsw-5@8ROtuL=,#Pp!u\cǴ:I(C$ CҋH=MfL]C>\O>Y2Pq񧞪o %c..i#mhlbM>xΛ'D9mܸA݉;w:g͉|:G)[l:p/Y =F+mxh: Ro&V/fyInjq(O}J kǏwaL\,uq;vXǛQ9sv-đ/& Ck4d}J*ݳ 7zH/J:wzCz w2&:KgpLLȋ UtFr<aǝqA '&q2?7nxpV>(bM!  C:~\ A#xSِ'6jH&ک07’/x#Svf4LUb ?“i#"0'FFLSAXĥc5vț_ilv#-cP)Rä|NH{^GrQ>G$:ؙ¢D1cDKGJܽw .,J][=~؍|/"sEFVV>"<vs' aõ_I(iOxq ;Sˑ. `?I)˶`/};.zaz2q럧MMh}ERͤ/lH1^`㚱;RlQ/ly`dGYKSxNW\[* ?l|/!NQiR2KBa#s'{]eRݽy6#u"ʐѲuVKM!Odƪ-by4_j,Uk}ŊU|f([(sׯ^#.<}6IPl|{I49"6DCdI#ޤ8q4RgdLjViб 'Zh'Aʐ:[FVv;e@{EX]:BJ8DeHuauO0621@ηM*3M왳dԈzM:,/ٚF t<蘰[>6Ybk =?yI1q7[ۋ/"Z-cXznyI=OYl$.an+f'>&܈ 7diiN]cEߔÏ,nyć/]FQL!GzVDŽ7vҳ{qÏ0N\ Sʳ~#8aH]@@?~~VR?ꃾ~%|*!u&ۑ:BVW2a^a㑍3N| 12pv&xunXK< [V9UMP -inίHحEeI.=s2K4s^/ه,9.Y8Bw]6ԭݠߖc?MzvcϫseKcS =ٱD`_R7,݃XY^VyQz+݃ɋRiY(i@^hm@<>Iq >֐:9)՘W({ҧp#AzJԝ' -Y'vک1FXH_\3Y3:?&S݄M5&ϛ9/ɣS#:dy%Dd 1CF$k~0yƸQF@`n + Co EZhX&>3ҔH $5ڪ?];91U<&:nhլС0$S :N:P&K HwȜ.M1 ^* ^XA8rw-]@@@êU,>e1EGH濵`G3|)C(_(|0!nJ͟k:vlBU|J+U[io3(`(צ i⿕# i `^xKxƋwd`[idp͋q^扡|Q.#sƽԭq.nDS\a̲A{\(^)9^/Yߤ)dBפDfͮ0BGU 9l(4~&Cַ:eoAV' dѢEJ$mjQ:ꋲ־}BKJ"kzIBڪuޖNCaw_異?AkK47I6)â _^淵ɏ4Sɗ4tU>#s^4)$lذa]죕)CS琱y咵JI\Ʉ|oID%" ԡe2‚ɤ`jó4xjK|eC|r%N9Pd-" N]ϐ}(\jyЍu-w4KD"#Ё5@X5tܣmF1!ƤQV'Ȥ8Qn&u\W/"<ߒ0`#㔴F0Y~9r+`ծY&Y~q |j=ZK#HBavx1@}񖌺ݻ%ij \K)_Bm$f*v!E+_ʀm^HyQXaX׾};%&,Lr&#&Kp+e6_|{unfOkpdzdqd{9%t|̃o.$u H&t{}P e̝;7ݠOޑ>yךI&/IӦ/W+׭{zdǎCKиT'vyA1ғ=p:49y޽{b{(C.]MR˻ᆳuK/h֬s/;%-Z|9jGK{Xo*~hyYB Ѡo€ڂ>?/cx&1SzW~e|#:5k~[X":r#$cӔeA(|sI]C݃H\Ʉ|oI--a2;o/4u;!hR$&:eE91NoJ6mڤEY1YOܤ4+G=B}$W@!#-?aj쥳N`~֊V*ƦMyAp傑څH~c[f}(󧪜쬠`|%oq+ey;chn]=ϒ^!_#\-4RܢK0sUL|$:P R@n.#]N6 ؑ@cM- ɾo~~؀:b~RZ]T֯%H%cK%c$sk%k$ynVbںrL@H]ffd֫3sds5:;2 q4rGhX. *v-/O!'uR؀aF_ PU kdՒ钱J2Ƒ%〫k'CKDzH[WOsvիl1vT6)3,a ,Vdn^a¯+bB=V-֬^.s׆seL23wr Օ … p6_XpbѢk>;=jU於<΁m\.a¤NCU--+͖{){_܅μR2kkԕJ_̈M4E͛'%}{) `G¬Y3dڴ)zSSL)S'T&O/ӦOIi>}d( ;&NuP9̛;SF!>[Ik%cK[9G4W]2k[y=4 `GG>Sz0XS>}4{QrI^g{BͤuH]Z&:<ևoIF$$%cTSuZF sYYb4 1/:? `GG4PH`ͲeKf<W7oޤ|^lIpɰyɒ%nuhuPu$ ֧5ur_HZ2w$I"ÎnOg.G6~W)7;; v| :GDα $H ATlrp)[s 3aʖym'A)]_fϾj?H]D·S?>Gͪ&cC}{ہ4VIiVIih;~ѿlBYSfE&NHN@h$H AlsJ$yy)ljH|,(%m1p]8%~&|cZyoJ_DmJ7`g7.~ؤ1ٴeo,¦ͮ[J.^n=p$"eHpMq%u7dz++I]EZ@&CRAR5H(u.iB:BtIҵ<|܈wz& tI $H ۃcOo@L,<qAJ"ux}vQ6o*y!<#݄gO'mnexI/ #Iz[+)RW$Y$PS(Yǁ;AߡI!yB%uHtUY $H R0cwI,M*H (&6)MSgv'Ya\Xcbn_:#OL?C7?ʆYZ*#] u~fEf@M<C;<1?/27nN'NY D)i"H $H AjmI/D~Ux菋~ظKX 4f)!)µzݺu89w &fgC9ej}t 6 ĉϏ얇LT,m`2VVj1K88%:ɓ#)͊ )̀BY|C|-5f}(_|\6+L,-?͒f<0A $Hm,I?vq]vgG}$_~?=oU# 'I$5!%jDO?fo.{y DhS^h+^j4iR{fϞ-_| 6LHEһw_&O0mLiu[yÏ>HV_K߾}bɵ=O_l΅a#)BPtVj9+@&CRAR5򐺷y[^s{/W<W_*#B|fn"VR?a}xK/H Aג_o >_C׮]dʕ'7||m⿜ۋ.SR{kԍJ F]ސxK \PPnivV.&4oke٭{Oݘ/698BF+C~'K.W׭["d;>'s?9so)CyS&ӨlRW9$Y$PS(O>VBG'D}٧福ul~B87Yfev[aiaa $HE6ԙ0zwbEKVac1$ήB[m4u?T^}M-['} 1bb8kl]4nxk~"0` %t;sO费}7w\ 2XWI!RANH]! ΐ:3ɿ?&:02mP1޳+Bu-בݻ=tPh0֑MFu!+K|K"uH2NN2YC0BT7g&`䥬D]0%5Ċ uҕ+t~4 &sfsCGsfGڹQWqc)%Op{&|o1"=qHRL+#:I]t`A+ 1 rc յkg?.wy\r/۷ͬ~MS)IvM0MSW_6w|"0ٳg;+/PkZ%\$?c2g,WgyÄKiK*X0MǞ=#5)\$_`HjHN7fP !::D:1LƎ'tYt%)^mۦ6&.kad!v5jb$H AjX|"d}7|78 "L!LCZxI^HNzd K,9 1ctĈz4mOjI`[ҦM'2xWu߳gOtv5rHVAX\W:i޲kCK/@8rqCt(,Mq )k(/I7s%y Y:{u2JvRdfbVAj0M6k ;kz@̝;;תU /.]ws]2udtA fF&S!]v+c4g/yeBe{S~E`C+/i%#9݀AY7E (,7^<5tNl6V I#d >lHfZ\odzkN Jf>s4h1>,=u_b%dM6Ցifj d|&&>]a4sxV6s|}3akd$u$(lF7oyUW]!bdfcדּ_'Y`̘1HIÆɣ>КAktWF挔nE-9O ʣ3J`BDW!ehL:/Rv] $H %HƼhLr˒›=u&LmЇRBD(æiDۃFcaLg;7gV4΍4--ܜ#v)&#~ވW{̋K3 \7Koke$uLx&%|]ثW%t?#O n=R~_B䡱k߾6/D]i$9%ui% ?W,_{fiaZx#<)4aH'ki׿TRgez*ʼnF@͡,RWۅ PRga&! $H AUH%H^ 'aβ]Nk$cKyP2V2+5HݍJ2%MZyHD*W^ytM{2l 3j=,eРMr /<$H!L#f!N5~2Mqoko}`Bt:@?A=أrj|䓏1cFi,W\I3BH!SiU6K6ӱorGtF8!^ bh8eĈ?YdIC U)l >b%u;Έ$榛nPfaA܆ F"$-߁b4aٙNUCrNL8^!ą\6i\tJ!zו;Z22h8#eO?U-''KwiGhz?멟4p(Yܹ~ l-w $H*F[ˊմz /^9Ov;C_ՎI 6f)B{;… a`AX 2A/W瞭95^=]h u{?Hreٳ&Rv(Q$Cj'ѼoЩS'}˓I2sO/MSgagaI|▎dP3ݸV5utD:/J b+bp[lY@+8QTԱgJ~KAj h"£b%ߡc#+#UF~qJ&= 2Eҵ퓣ƍPRg2~XM iA p,y;jxȓ4LÈ[.GbtX JKH&5{V^k-Z&G,3 `xغ`%qh}ىqzÜ;wnzbaRXx,YL2꟬/!u=u]c:H@8S ޛJ?Z:S@HR~OWÛ?A@8TCk^}VH>o^eB!sNd D8ǟR7׽{T#'jr 'jZ=dPCXJ'oe A ĵVζ5U\m!VaRM⪗O,2G._dpu:%drNN.ܮIfάR\T$ Mf޼=rO͚5Cĩe˯??A [FЌ7HYҵ(YT!g7^\5A5Ry,fm'H A $HMAjdz#pH:bvsnowCS[y#n)du_  udZqd1S{+"Bi;9S~R` rWz}ݣ<~(# y#iJ,5@țCIo,$OIǒQM剝2N8bx\rpM%(YT}lr]Clvy(38#a"H A $H A>Sxa|tV{͎]/G8"tD${$hG⎹?ũuCp̣%N:?e E3"{EH'!EY„$ɱCE0!8&X/ CJ\Ⓐ@;i>i3rfikg?i~A&4'>~89ԈnsQlM$?o}!# ;$H A Rb=ICR:q#u;*#G=:BюؕGtJC $O~ #u6DĨ<@n L5eIJqe!^8#`Fp3GNBL#dF[:;a0YIzs賈t$A:ZI(@Yqp8&u $H AԬsϐ}Swdp#vGE<4u(8>Ey#z)$8Rw2]}$+c?عBeKA`Ap A"#I32F,,0D<a'B&H qO8#gCw!iduce)d#fPݤ.wK/I[)ِ爝|69G7OdC A $Hڣ~ G㠔qڥK2Sdz PKS"ʡ1BS2!v{KV>EI#)IHQ@Rkp}X\5!On "ŲHƝ!yvX5Yı2X|kț2<yRK\#e`<ԑ* u\kyۜ s68y,3H A $H K&[uJnt])z{ veD61L2|@|oUAj҃ 12ĵ(A7Y#=)7v+0`ƴk -D#PݤnڀOdpۗ^_O|[mlҳ;p8i ՙA $H AjZ|RiMO2<:cw`G sD(XfdT|O]]SRUI倀M~YF@Lm.PLZ&o)=[Hׯޑ+湌7Rp G@F;ςMA $H 7E2:2exdp#tGĎuRWTK#rYGLG}v%!@u._<)vh&߶M~ hk̙:Y-m#5׋,t1MSncp"biF(H A $H% M#uMe`&ҿ]~m_߼ehפ0c|(ú+>{Ji?/ן>+=Y0݇+24uA $H 7 . `;Bu-V6J0!t:6qD%߶ tf/1? [ʐ1gA>MclOnoȷ_mGUdsJS%R $H AR}I]ϯJH/٧ 3S"22AV/ɀ֎}b@kG=oDѫ 22{3_u_QD $H ATX$*5Am"[([(}[D ֫sҧSңœҷ gsg3ҷ~_>Wz^ѫ]jt <> }$H A H]@&uݚgO*;{ٟN>.>p$7oHזHvKϟhU:zySzxIz;eʘ.W>wQ A $HJ u]S‘VOJ%>tHϞpH/^mޒӇeռɒtlX2]Ο,N &)V̟(Mes+'K 5sʩ}e1ҭc)(X́ $H AlRa*ԩl޼ّ\$M8{*H]^}t)%p>_o>x\:|>#dݼ2tW?,Yf讻ydƳacIy]yRlGXn:~~~O߼%/N2Q&*=Z6M{&պi,1\VN.3Ec1]D|ELy?Ype˖)ذa]V=Pѣe]VM!]+'x[ƠA(‘N;$}-?ƍՎv/Gnnnt]^Nd/͚5SFY--ˈ#4/\CF}W:c=uy9h]A(>:w~iӦ;׍bq} A)>>!I&ȑ#e}YN]:g_|uoo\ y?y +է@BwZx3o}7&}nޙ{|es:}g;_I~7LxM%{:W-[s#gZj/:8kl}V~6ɓ4=TG u utw `۴9ٻ 3B)揖 ƤaX]4pbo1ޑ2wdG ROF)!A뮻O+.in> t)}M!²ҥKt}$ou\&rƼy ͙3G [c_ VQF,?OW7@^~eMzF(J3: ?մuVu;eyNӥ'|3&\zt&-޿93nݺZ%K< Jp7y?CwA#V1b;o_<,\tiV Ë/$Ͽ$ Ro˻ᆱc47|˗/]Qo2zh㏥iӦ[oСC'-*G3'WΞ=K%عt]̯F#}>_z1،e%&3`I&O8Ey[8@#E4i"@`2ih 9{v `uM;}M1ב.60tg%gt˜±]e2sDG+e::(^x5kȊ+taɂ ڹaÆe{#hX6^3S!0H="oAԧ0FXА?KGjC ,#%o4mRD)r=ܣӕ,)n6~x6G{Xy+O?ujPƠ~?<]6H%,vMoHegUW]yPN{i3eI-aׯ/Sw>k#beiG"ZSO AԔXGy::bDŽf}yM/\o~ҺꎰRm۶JH1BfD~"jIX A`Ǐ\rQw=Dby݆\X~:V^{Zn%K/ӽw\gdc&oQl%ov~2m< ؎PK:ya 3D{돟u,j,[,vc:Ȃeox|w]n9gD%xؕ9R u:XMZ&7߬~/!>2aPsCC|81̙E:Z[k/?_u -_ =%:{OKeA>CpM'TK\ވioƌf˓2GYcGK/'nh [CY]>2(njGlBC %K.D7o.p rɳ v7#>+?8 $HMƅeL3Ǟ+OcÿiW 941pA }aKH8|;Z].w rĮsv&ĎQޜ;}G+_*YR6yYWXØOYyN2Y5,Y u:(bdq~ HhO~ȔIz~%} 'Z6#;;BR.h\Y0M"r>CI:w (͖A> uTJ{ 7ܠ*`BGKktސ#4d"WZL|-.$m}Q;@91-a)oOl gp EF26 I&F;<g}V. s)(ɲ{iS_:OgRbMƴ~@)S(GOφpSVhr}GkX$ Al Ewm߽gոgğ?).\HI\޽]o7g gBiM4 N5FO=X${^7uD[=vRf)-p ~&a/_y5βYO-B!q\@'IDC +,O3|j u'-=9*|`>_Qd>kPX{ʯ3߫ )|$N 2WRic&n%{tnF (chB~+!ھ򄠙RgV"3V>oˋ5R#Q38#My{ QD2gbDt7 $HMkIӘqc!(ܡ1M[T˃;6`[ha:=;AM„èZn`-i> ϔE[J}۞:>-d}%6⦵4u=ԡ)"Gvv_4{Hָ 5Wb:Ś5d(~nz#?1͍k­Xk&V^NzӦt9V\ׄO|$,n08ųI8-mLҴpD#@/0 rAimKLxQOI6GFvkU~9c#.ͅKsHK-E$FXE{Ύ^HӣLyՖ/ij7H3>H6H-d{ƍndZFYImbN=YğxA?hzFm2F|jL7f_I|!Vvʈ8nvOV=!fg!Ux,?>TZşO AԔR PDb qޑe{:1Y7Rge۲;# =^zFʯOzp5 j ! Ua߇lKb9C{ʾ8 ژhh9Gs! <=!&GZ! 2pa&7;tRwsHň#tH&qql3Rgdq`7B8a7SP lQ<`Bp'dbdv$F,K͛4 K5 nPҦ,CE}YzY8p;n7jY울vaF@͡,Ra&Ӯ5F)nľNf꾼dRwv!F(o`_rC8 OWKð7 !"HX="eFÅЈkJXb!,A-^{v$q< `v?$ я~iYĵ% M!{ q,Y8&ʏX6ãd'6Z2;?&uoMCp_ݻwO6m}+&4@Sg~<\^H PpBbf Al+ϳBAT}㛎Eyuvm[\&v:hʣ0FRpR?Rj&u#u}[ދ%Đvz`rb? $HMEЇ".3<&@Yߋtkb|hu8x; v:㷃E8KQ2IϼD&/.0}1-_5f8zYPxF쥃A)hղivM7<ςSdI-D-T{jb{G 1F ;&$7g?)kގG1܍'L[<@ c# G ADH ea$Xfe5G\' vH"5:@lC(-ܿ:B?i"u& )F|ɺݫ!_7pC~x_._:3>B|gwK K4wv_0rO>Jtce7xMJ*_ ATXdGIIREfዯA|ߴ=x_}?Y=ix}!l gqG޼tȻHrQwi0j뮑ѣGjx#RI2mǍ4yG);@9 enZH('vZ8 cϏGZ7۔)\|k4%CYww :@`)-2@&u92"uhڒZE`NMkdҤ/nW=H$6XǯmV哷ft/si8ﭦfZ.<r˕W^.?B҄7"hD ;$;]N Ir rN<’v,,ab udΜYeFJ&5h(éRd@;֑.ANYenք: /tv~J]D:SMc>% X~goʒ.SѨEcAJ{~Suڇ?!$SwbDUqzO aÆmݢ$nA!Bڸ6e w#U{챛:!`F卿oU?bwႠY07#}OP)N>xODaO>+~/&P[Y ],Hd_~׿.լ[7[805=(CN:Ye[.ҨQC-9 =]Px=ؓF@͡,Rẅ)=吺^tH!KL1IM?eȁtյ'VRx{4^/ŷc~Grp~^ L~Nﳰ}3>~8kM? w g⧈GI7'~7Z8X58;:|wpܰ"V喐kZ_&yƅdY5Ls-7!Y2|tx B#H<)[~}x5L uUOi 'nӦ}rFZyiw'e„qJXIFX d#fP+K6 (sVVDۼZ~UVL#ww+:$Rɵ\9"eٴ\\\9 $H ATLԉd~dsdxdwdn1I K!Id R q ]|V8[AvJuGV"fx4m# "zrCo482O K>瞭Xb}<'NnIuD[d#fPnfEK4WJoޕCZɌo[[ȴ_YYLL 2cX+.UP A $H LJ;&p14R6 R6 BDzKr!sL#mʒ:BLnӆU2W[ _*fMؼR=K'},|&C{-) $H A[9~嗐;H[#:HD "q1-`9-ahy!C:> a=Zm?K3!AƏ+N~-4uoia&?;ܸBHt!u͞=3MLc&AeI]$,7Ek7?qIKV)5f6/:JGW.VMG۷e^Rm< A $H͛ I>WHAA` +, 񌄱 E]Sgz AC"nCI5&p;-o&-U+g3hAv-#2?n,$ߦrFX]`^ߴIYx;A4(K9TZSaͶ|YdL5N|7JFAo*WǸqȄ1CdE.sG.at{ $H AWy#sWJ!)j!IQ2dj Ww IXLjn37%\r9bw2yDyT ׼p;ЃeY֭[)}3?֑=c"ONdI%v?أr?ޱlzis[MwMUrwsٹ $H A RAq*>t$YG8bw#o 9H%kLAT P0T,D5|Us%u_~EZ+id."=Oa `?ׄ8-O,,옔']vb7 'm!CPa(E! 9?x0z:#k;."upIdPHH"u8 vݸV5u8>Ca!ys#vԡceV{ܣG%]9E:S.$2gu֮[)mڴիW… eѢ%j.^X,Yf@@@@@@@@ ͗˗Ig;u:]~S!d;Bsس~d($lh$._Y|PcXJ?g[SW)f{ oIjA A$&RHi|j}A)#YGޭ:S@Np89c]r]DڶGD; @'  $H AXbNzdvj{Ա#uY'T}"sGs}̣s)r~Yq$Oj $H ARWedpdujꊜ~S-ÌH]GNCDrN;R?r<*G:r@ lj=i>4w$}Bέor]դA $H A uo&[ޑ7Ḳud"g:;'Bk>W./%؇}wߎS3vdDG uqvRE\xk_+as (!yyRy $H AT\Ա$X=w,<"A u[N \Ii7b wEýuFw+iA $H A*"KrN~c%w!dDG uq~RYd&G69:Lu6h[6CSY֬[=< $H ATP=7z㮳w.n=8Y"H]@@?p58.Svz-΢7u䦚;] u[ND:,1ÛKOI/?z\:~|KO*]=%~Ri_5Yᰆ $H ATHa_] u[ZC㖷^vPLZ Z& h-~%ZޟˤAdpOK`(r A $H JvCD"] u[N"l͟^ȀMge`Wo2m3|%G>!>^>U4 A $H j>(.eJH&5N ֻt?2]3Mu3EߦrD_IvMePH/]*~y'do{& *eӦM,}ķהa1ʅP6nVvsGJ 당Y-Z< {zvVμ<6&]uHRvV/zx=pGx[նV~>%k_ޚxvme2qI~ݘ_Ijh ]?x{gYZ{ eTViղXFx KW;cPD`eΚ+iށ_&vL%~ژ6Ŀ/_|_VZ@-yuMJ9˞{'MtvTףՉ?yۖԶjX[6dnVWYg]PNH|w(y&gdL{n~{?_} M,P(Td#fPI6n_=Hݛҳs#_}Z$=z^~Q|#vNMnb1jB}ۼ$/$2SSi79RWTDlZ)*_MIe+x+"o?HRԏ +Nl{Ix񗌌0X FV7 #l j;یoۿ.=|Ftj?2MSүMSSsgkGܚ? >. `eH_G| hHfV!@uaWysMJ&'.;vwnARY㓋$Ð&ie.gi~ʏ{UB~ԔϺzk}l P ~~nKxFGv_kR60?KpUl1ɶ/+6ؙgmhzLetfE2bԯ{eig-.bvtۄէ_gRysVޚhc~{/~;31d嫩6BYH l&= yju7=Hٳ&7?!~+Y 0F@͠-]JϟvxRz|tl?ɓHgiC`4_8Qr獓U '+V.X1,7Alxɝ;L&53ȊɽeңgK:_ UG>ׁ?)' D#6ilu`kޮ=֤&P~:{#?MܭL~xVG%Q.nuP$<ք}yf,oo0ķS& X^f/ _ǟ_W[[ #?ZU\~0!-[rTd#fPId үSҽ'{6Kڿi?'}ھ)k獒UsGqdh\O5S3uT!9#:dư2oײ`tgY4~Y)p uI(F*w5!#y;VNodwoQf̘Qbq>JښՇ[oG[oU]{gC|_yyU'~5!3tP>XsO3g'v,IO4a&첋p rkY9BLxX{ȄrX[mڴC9DeE(o$a叇OGpݓ”&XYHwzǍ6L~ En"niOkbmO{y+P.RV:[+ʫdƬlљ 7o|+-ZNpOa K}@ XsW2ag:u4y7gu;shx_x/6m"6dȐ!Ү7JY@iR,G"u|p&u bb2%AFp5jtʖd#I&u <5s=a{w+NnθT@D(:!ܗ?1[~H,r[7GZA."uc|R7+2:LFX~' Lfj֬CxCB P L5c=3&t믿'_~e$փI17=x`n!ArGOVℊ{hE%'ă]qQ'd~$@g&z%Ƅ<'*7b ƎI1qcY ^!["5&DMu Ćʌ'"bY]㥗^ʄ[T|=SE⢏Ap x>BϤr- @n oyz*(;PPd0 =8OAhi+(^ ]=]w] '}aEڀs=O=cn-6?A.{NKą.K,u[/ѣGt"&X+u䉺i_Օq,`T<^uUMX}} ѤoꋱbSXGy1K]S!14?G CtG t3fPmڴ3ɽKz%K|IꟺaS_#+oK,sছnmIx{t P#U^(=Mx|@L߶n_@mM$㰷hЫ"u͞;j=3:^wݚ|>HY/BO=HA֭lIoɝ Vfm dIݜys|6n fC m>K.Ƒ')RR/Fͨyе(N%$uX 0Ή 1+_N:!R:}&VpG?yx#f6qpk2(-o\#DͻOF FAˑ1兇?3-= L`Ex|cY^| 1fB dNHWya)D^?xЭGPx2aa 6 m&e2'@$Ix+ |}?K(~行L0YA3%'c2 ЇO21nCl&8z$k#{M6٤*:2R$0T:#CF@iI_^D@B(+.1'Ym%׋H40 ELYJ %pF 4 %L9gLwνN@=b `Y!z\O1)W,j-X ~W瀲WăpqN[m_y„rHvPuGP,̝o{MY|~=cR3x{1>d=&}hM-+@b;7 )k*$7^G{oA$!/nj˽>K]@}iҔ1=Jjc #ɓ'Fݻ*wg[V-;d}vIasnxx0BH|!#<"L#,q9RUNi|r4ke=\aDLH]Ő u1A꘤rM(z@hm6iP׸sl,mGb7!:vDDjeeʘ!L5_ԁB(/!Znρ/#y[#:_Dur.k.N yN0>Z&L.Ɲ<⿩DX@@)3uRNsڄvNt^Y~:pL^ ^hGL{PGt-%ꍸL |#a7dX( ʀ>,L3B^BO GYuIYE~D!?ɣܪkK״&3光̑Qey&@SV@|86jH /뾐B^.x |p̐!̷Ն%i9/dPc( e?W#X!uß} N. ^mH;rn {}kH7/ rfA^?.`deT:#M_3rpNyB}>}Y~8ХX@kYNa=vL}r( L_Sčz Z*=s H|h@y :>yf&&u5q|ӧTLv $R^~ȓ,(;;(}^a頗WV,)ߖzPS:c~w|d?-ݧP=AVX~^?1}zַܻ8GH +^Z@ظdF$I=APi eO $iW-(?MSSo(`dް]^A%pޭ{MAM-4#+[=1I&XL&X\d_3=H˦"C]6AU L"ےj ă0-C9hge">ʨt'&%3D) 9HҘBH4+D"Q=}Jޱ@1bo.\jU6+lġ_ wE_Y/ek|}Խ ѐ&&GYiSI:82%+o C)dC6Y^iSMJo,uӸKПYʒOmŋ!V[O'RRki}[ݩ;Ac`Y=俢ֲHoo{lZxE[-?aK1_M,B ı(/Ǹ?5z2ved_ 'sx*!@؜aaNXtJ-^iܸfMo {i6_{>,ec}OF+H鯳Z֬YSkPG|v$wCYnk!iAjÖ0HR>,ο`XR:&jgS d܍63!$qҀ6f}7RWQdpɤ9#ނ'm&ܸ3Yz{ &QLdi!.9 ?fDI/19ĝI m09dFV^!r!&Lѭ%}I' <Yq&LK|$h}9WDJ:`K#&x(HM ېS7i j$@c ]LT ē|WdMiS&tLڰτҗyx$ @/yc-0^Ж|XDȧDzau;/Mܫo&?cKꍾU[3^@SoeM$KuÄA}/ʁv`uJl@)ˎI!'Q1 [''u,;&?Zˆ.NdHBް`'I'J#^/ʀ0]biWڂXG,{C]? c Ÿ#/L/i ^">.Cs>Ҁč2Jx@^I?h'{T䛺V1ḥlp!U,F~H9Kbg2y",>" 9}x< HWH6C_{V{׉㭛 (^Z0YJ )]oO~yRH~>o\+2:S>'tPk$x#ԭoSޅo v"},\`ն<>N[NWRwUb+H]H*(A 7?]9B $. BX܎=xh": s(wDqFKnF}I[ݢѹ[۳֋իW:t6>aWN kh&Gs,_yTD qy,] $#JtA<@j)z؈p& lcct:D m XX~9fp{vx@꞊#lv0Ti $??rH1YisY\Z|^q0QXe0\+.@!}"z8|td<~˚W *2=ˆ'$[JÃ<ϗW  zU կۧe K=>xK@%+';P8ǕGAV6\GXGA6HvBzЯ4|]>,|=l5:' f |~@uT6N:}>8׽~ .YR}Y"K@kQ}ټBSU;>$ }(6rQI%0P{O:t{,F|qRW} .YnWZRwuNX^H:!;76b'Lt;a" 3 !^/8~7'aiIw뭷w>Cm̘Qv?PSb֪ ^tB_0Yi;HСpT֭K$%} lYh$)?YH4pkr 2@մ)C{[لAlyR7apQ iewúXOYc#(Ċ:X4(40Y:W{4_ =mLQйKXd͖Iz)>ٸٴM{!a=h *Hat{ɑt|ϗ)Vt?g?}¢o$e,d !f!vg;ȦAu|%> ٗkr>37!a )#W)@ ρO<>’TB: 27pN)Y>-g#:W=n;kSYꋂDyνѹn*m_(= n8u',|eka=ԇ4*,ns Z6˹Vmch sUbWk tQ z%/Rs6@ZqAp7I|\ci$6YϟjK!XXA?پIÒשSHO0w91}܈7t|SG:l&O c {≚1򇠟ޭtYh$)?YK7A}eGXGHݰ0~K(%oJ~|' kQ,( >JxO84gdu<ʏׁ~6a.ρ/;aOlxtgݸFйٸ҇Sg˚E 9l|x?֑OKz/.ȶg6+-G4  V>|4B?jwM(ORNO]KB#{-(o!osA k|JGPΕ~&[ *M^Aұ46V}9ב?}8M_edPBy^_w|[W"p|+~r#ʆWKs+䧶ҹ0^{ă?<ϑγ;q!mw a#D[o8D +DNketoX؆ rɸmmЖ6up?~uhR;T\P}<݀}@^HmաS~qyT~7;|#JǗP\ȃOY(ʆϿfA#yPY}|~tN|,|aWnEyiǧ יM<~uT>Nq|2t,iPxd}ټn8>--<:S|l^e|Z> WFuuJχ0ss_f=:G.Gޏ|yBN~TVGOa ^Q >V\ X>?#]iN ۃrr ~.VT)MwAPٶ*Ԟf!H׵@؟B9RW\[ss΁~U wz #uAv 2*sOtDHnX IE! a>!h2B']QJ)?@XÝ5z7!~^i wl)L6R*@:7z<~f{)L2qZٔAlbC;[GH]3N($Iz`sS=tpeY?zHn}x^wu.g'Q8A0>e_>P|YdhY(]ޢ]_z]ٺ |q;l^|_ݷ OlY.מDte_J[6o_U.z}޼G#={7~s_np-NNX#Fax?a9z?is׷˃0پ|gnuY}jc*'VƟtgk Jۗpp>?OuWZ8WQ+/jBg^]mP~Z屁ԝgke;^i|SWMR n{@$9BWI$%҃QO $/҅p-B9 GtCJ Hľ}uKx߾V߾>>( j$ԅq.ٻ t cq@td8Y g6OY=_6~gP&a_CJ<[?~ӧ)l4 _[Zey_ $mﯾG@\@8_e~+h]HW<{O}:eQ^^([識xNMvP(-T.~%0)\6B-tyAt)OSnY|x [>^/G/3_Tm#>}ݾ}]ӧ}DȈoHbw웞Wn=Z=c6?.ZPy Z,V dn@-y#@y}]սnj{߾UIRl$)YIܟf۠>]]㧭O[^YF-kR:4~ZWZ_:4}7g?mcs~$ K!u^iŻ_#n:5RwbniH]d#IjOZVtO_6AzZ]_OK>}vizχ ʄJI].uW< ]d_.MRl$)YIݜܷm6wٌO3sKY6g=3y3m i'$$$$$$$?&uUw"U]؍UHR=K u%Rd#INROusqj]NY~O-\ZBBBBBBBBA٤`RWuW~rֺjeU3GD2Ts$ +F՝Ա |b["ae\j*-F^DvoK,}E("lD~Rl$)6H pvB&D<"%%#*E-ȑzNi˄ʈIAj]u"v,\ؕv)BHݯV $#-WMzXr 'k>k/KH]+ K"sXKv K!u=]yRnۭ~w9bYd;I}_~K?&P#u[kE5α۝iUŠw܊w|KvԱ%d}o .7<9B极#)^Kʬӣ7w.sN,IX>$I$I…ܹP%RP1IH w숙o oSN oL!z"r"{&]v$%.߂x_rr_.X0/!pXڹOu/_-ȓ=e/-LHHHHHHXxwDbWmswNVyY9 q.kHrL 7eʤ CG|O>6d8 >(.4_?~l޺~=!!!!!!b%[b#uwNVyY9i8Xx 7 NֻB$IK* ,8Ҷ oK/DSK.{˯ C]Nˇ*ʋ]UFnj$i1aXd˹l'IRk'u*CaI1c 2(\Ș^pνɽrLm!wÆ }k_}Mbt~՗P)_g w*#u)qBǦ)_g;NO89h'IR^RYIK/On} W kO׶'iu<Q{fǢ?n!<=zdU/hL Mn7Re|&zȝyA v#'|w1vg/Y~];Y\68j&zq;CC^vmc]tA` !dn&&׸q8ǝxJfS8ŗNNr' *inҫ4tD?)#U7э([,It*T&RiӬ'.2˾{?٤Imjw}g3~ }um q?}0a\-kMU|^7yY鄬6m9'O6!OQe/ ,d󓍣kD4d||܅@>&lyJ?ߗ%n鐞B} !!aŠ'p{Q{[cPY:u@cWdu&,;xmvֽIDAT;ql}:KTvM!"e:$ 1-tDBÿq1 "D\4AGi_믿oZn믿u%4̜]C$GB9G5Gs-Dz"\O7Ǎ8MDЉESι Ǎ| ?_Q>aie%.K$ ͚==}'ֹSԱuhzvf?$SYoonݻZV-MV֯o2u􅾝#Vf{?T@͑4 . ~xJNgiDu|r,KOYV(= S8}: *wڷO?Tr$),-Aγ}G>/6m׀Mt.1 pZG aѥ |H(~-Toʂjeۜl{qȭ|IbJ-uĄs=;Z 3TA@ܝqiqy&ֻ ϓ!@RЁ|*g".^}8✼1.%= Bw"ㇰ܌7Z#:H%^n#_)ԓ"I{:?r'!DzeISnٜa{' $< }enÄI`-SGn^4e١AK{s,4AJO>+NړIOsi1G|Yٺ*TEu| Q}WNxחpeNd9!!aA3N},#,w+ Ǎ,rcx ׻UP]l >ŇRJUN H[jS d㏵SN9) K'e"Hf#>tAz0OlUWяvam 8 1cF]walmd~g6nܘPkcLhhv_{5_ch!d)&]zQ=dirV5ҺmM6"2(uֶmX>Hp*R_}euםcM7rO5)[ "u> Z9i֩SҥS$g:umYݭ[>־s7ܽM~!Og)=Ali^~:z (_V>#, {]^uͷçO;:[N/PV7呺ϖ׃r#t2_s\Z~gϳnܛU!g 7~c aQ?bL^@/^?rm[~.}W^VTA|8'd҆5'b3i҄BB8HARdq".~^,Wq:6r`?iH`EUW]IҀRk*-BYF ϭjբ+/ERGz:e]w=-,; &#GyD;:~~DF]P7n8f w:KVE=IyDOQe˒D2Ie'u]{ .gBưlN8 G#fΚg7m: Ӄ{3 ֻOد˂ _Ǿ=vxj=@ !ǗW rd!ʃp~O17s<3j(kٲ=s /X-d-;n1thO>}G%9U3&^|5j(3?1Ν;X~+0soN:z){GAm[>|" &-FK꼬2Rq`ǤG,W|3ǵoTk DE %ėA{쑼;’C{a =EEkXmcZoVnA:ɋCT~1!OSG|KNdVvDb>]RWUq;իZfM>xף瞻"Iqġ/W-NeNzf".nonx}M}n rG1g.rY(X}ϩ?MG~|i \ ! 䙰eqNTByc;S'Ǻlm֭[G=뭷^$u`CRv%s@|<,]YVZk'QY(!seȋ}H(Ծhp>]81!!a#{N:5aÆIq mmqc+JzuOˍWQgyƾp͝ƶ>cx|{6k" s1~?G ~ík׮h;gRg{kw%U# eC E"?"ZXXp@.Dt~6LgD 8sDa~yzq"qD!SXٰq,g f"3nPZq^/PIφEC7yx智瞻E\6y|;nKᑵ֪ᣏ]#u]'5:êײe|5? ǴG&I9G8ɩlRI]ΝUxx͛azuYv er{vڽ7kw_svv8w^֣G0^w. yȑ^)<~`؇ z'eO {W5\I7wu׵mF?Gt*đ_W;su!2E( ^7u!1Wu ^iVZ:0igz\<%$$,?8=AiG;>jsNc8oZjM{g8[m=?ϕf͚YǎyA]M;\~nk?fg}[V2+9ڮA 9/+A0A$ P Hs!9K/Yj(5]>䐃Ћ? y⛺G$č]2 : >bWMⳬrvɓH߼o?H;I wGy";bm/eS=7~c $j),˖INeNZRe=p_/~=~ϙV S?p=Pc;/ qx٤c,c0^-^zmƶF}O(@8K1_}p//kmlw??.ѹl-[XFX %gqF M7o[zO=.x饗#y~BC}&yǝֽk;C$gy :4[|Mkl5׌uò}_~y!CDKV^=wÆ b&˓]]`4?q=s6hH>h>;CeSz~,KX6Pg?VssT!_%TաzߠA_Wlkڝ+3DMDyiۦMFw!z#_˧cBBBzuoFIdzpDF}:{/|7ӾyN]죦9:r$R,]=֦^ze_zR8xK6qqvvK^ ֮];:0g\gkæY&mނioe͞-u92wM vEBV}-oD{B%@uU D.%dUVW,+D伬RDBɣC҂ȗH GܰA!K:Yj 7t1q=.#G elOQ ̉G=^x~r?2I#`?!4򉥎<A,u|aF6i([`'&B$(aUd2:.Yn;=X鸦X $1mUX "5l cLQӭYmGoj ke;޹(U4فaA3 0 K{1g[77LYB 髯~E}HĒkDk }x,A,+o+2`9$}H#nN,QG9G#X AboxL'XĂV]p,<6%e3|a⍦G se>yR>D_W10|ЖX XNlFIGϕ6:?ƾ⋘_\T$tKٱɟ硺lSw/b$G6 +w!ecǎPc!``{K(8?e7^ӦϰzZsۢ ^/ \f 7 G{=k6yM/ ڜ:C|=?\s +reCʪ/_7k4pp Xr?>:jK8  acܶ&~hdc#!DΕBvrG\[oih䛶=-QpO'@N7C "%93%Z~zRV~ eeP6~{<adBM+Y.u~ g^nu6M63wn&?~z)gaw!>=<)Q>!W>o &<&pR>_Z%hIY⇵P&圷v[񜼳? )@pWqG<ӟ'GOV`gNeF7onzh;8E7u5ofoĈ ^/Yh?[ڂϨݼ@irP7uW*#+-=]s&(wΪpumH\D.:*#u") ^bz94|A8' i"CbHKd?He<(_Ԉl1"aAW)DDo ⑖↨xG^O)ɃjKqI7GRT줮U0X}Z  ΄cbvÍчѣ9X ^x>dϏҫ :wj-Z D[., n!Q'/[ncDB\F_~;N:`N ִobrM[F=o뮋ER'·ӵ+, !yTD :5%O@Ё w+!0"7Htp!V˲BB/"`?yA'EtK98Ie!TrRMqG+] "ԵӬ-ߍ9s8.Ȩc6m͙O#nan?Ɠ# ~_GXwGe Km,u+fyl=ȗ v\ȒAȇ]aQ.NHu I`"l7p,;.ΩS=XzL h`S OvaqB'!Ar!ENe]b};CscJEYWy' )#bn, NBT>ꌝ+u颬;۩ՈGH})%ձ1J q Ż\k;v,9++E%,\eNdB=q(V&RH ~@D&E+-bExYA҂PF҃\V7@ЉEOу?:U?nEңɷ*t\$Ie'umf3 6>?>< }m}X^C7ݻO~ݺ[ރ⤝, ~/w8uQaY#NYȷZLHK4wު^N=a ua}$¨`mf&\ l,Φ'CKx.R^x@` lsOܑpX$֭a}:)D?;FX0p\b#|OE 𠄘euBY9t R.&M[`cz(/N'l$Nv|OF%tBTF Ϥih#&SwuWYt~o$2ĔuOGC\O2V,G=Ƴu|wQ;aDr\ Ԏ KqJ7>/zI,u Xtgl7/O/vG9<n9ǍʇOz'݊C|Q9p>ǏpQ9DzeISF[ь_40fe6';7\Fr>Ĝ~4N'`4/_itX z|D zN@K_YHHLJkwI!o78@yuuC<&^--\BB!;-qO?-ݟ"]y jV "u5 -B%|X } #a٠z˷Eɑ:-m,u~R&K]}g(Y6۵A.'qÔJr^Ey9Bp"#v_eD9I"K#uLtݲзIه eHmDz)-&,Iݻw;bd9wLX\8;Tp\`ZS|ɋ +3iuhC,<нp_̎YfBBܨh@?ߪx^|U1nWIE""BWE.)EKȲX@Vl@XpTyQkӶE \LLT)?[G ax9w΂XsU|PfeKHX=SJ{ 3OA!,Ye4KЄ6Y`śkE5η;kUw n j xKո8@D$Yu,:=M?=Me-Mi_~z4ZV ec l 9~cÿR7|06~\T].k'$$腛ɢsJ {8keӽ[iѧ#t, %$$`e u;\`kCvd,ۜgEJηx}aNDK|eY,uyYV=Ͼ](ԵkǦ!9?n?2qDݪosogZ-,!u%.mO$IV{=AylZ#Qd&Wz}.'-Z4K4˳l+nLV?!!aՠиX$w)^'瞨xu'$$kn{Ig[mE[jE#t[ ZH]$NRQhRܥ]>YڴiJN?k,zCDr>&5l9?zߒ wPl]&$$$$$$ FRwUcÔ-ΰ*[#u[Hܹ% 7HI.IN>:unk4VsFڵoi";_GcH߾}XY>P/GBBBBBBmH6甈ҭ0Ru$R$Iik'u,ᇛ'Olƍcm„ ҄pC2 078Ȫlzo}m[~Yeh@6I[ňrA!B$R$IiRI`Yc{mmV#p,KQqD2ń D$Xk'u*$KbIe'u[F\BBBBBB¯%IRd"heG!",IHHHHHHHH.I%-PBBBBBBBBB%E"uIT,qڷʴчyY~N؄ D$X2kh?OZ IBaP7i˄x\Ѥ2P IBjWCR!sAʖ9sfwMYfD6Lgۏ?~D̜SA~ϑyD=CaC8dY/L?]>M̆S:Ql/K$ޚ4il'NI&]XL8RK:I$I$KBcU iRHO$%S&6ŪwUԊ6?ѪnsUܜ.nZ [ɤ=/b;3"MK2!G 3E`A|)V/"}-z v LPR.HP'&%$$$$$$$$,0d%R:.Q*IJ!v`x9!(Jwdž"/<ꔵnX.mu׉>b\jc9ǎŏrf|YT&YI<ƕ%, lYmˬ ls;,XX wqf^ 7wp$ a"%ER)̭/%A< 7,L/ z?Ək#F|'䐺^:uËAV֪n͚5BV7}><OW%Ne՝A ABv ^ -̭7'69l\Z.N!3g88!!!!!!!"̑X~7J)Q {fArn5 $GF|A4&9\ γO?5;4‘KJ?ypVA9/r%\V"=E\k!B()MܔB'M`Gr]ώ9樸|QOwum1_;X^}XF/db26f̨_5 6le?hL?t:˒²$ѩL@ne-tH߬mvuɾ ;!He#/]w^DM7IE׽{W-^z72eR q'=Cy߿oq$_ߪ/e$jo$k5y 뇏ZnMMޭ [& Y9Zfd[8gffdՠ 0"㎉n:y;唓"(idGznra?RYX;֦MHKz"|ww}۞yn=nb~8_7-` evm u r?f-XHkH{e$ JҩRehĝ%"amְQixwTe!itYT6n5{uo\ۺ5c]`=n?xֺ5~:uhP˺j }ھ|??IEÚ֪A:4{n.tPk"u" "D@D& Q'I'K!tBn?N_RǒGt&u1$,t*O*MKfe":~qO%VZ<V8c#Ĭic~!C7\tQ7$ KCхutWM:n5h~$ i y$ y_aYT6YI<{kWA?i]?aݚֶMkZuu=ylx&6{ɀNmmHV/اm_6^5?,!!!!!!!bM8:\sqiBa9!$d+ 7\JBH/DYb9:,uG,A#i`c k+.yn9&iBRх RE8g$zp\' ˨0?7|Ӹl^2(AN 7g ) zYJp*',e˒D2Iyq޼Sqi`JZ7xz}uyv r5{ʺ5]@jZ{ǬcÇuFs'.KOX&Y. ^O?cm<`Xˣ̙3QnDaΜ92 \Ȇf{{/dUϕ/O:6\PXs#9fy?|. fbs|}?(oٴ]^?i|g %ϫS'[JOacٳKex-=|>N/At dPY<qw*!0PHdA&ؐ CA~MkԨAĐD|^ق2  V2垐<6$!wI!;s{Wno'e'|*oY}ٸp) T0JK - 8}^s#r},V7*ea@X߯Suv/+O aX I.O6+!usÄwﭕ% ㉙I;,sV:cYCXɃMߠAt8n4,ZOx9;n6qJ$?҅9  !"dF8|wzA8 ?Ōnlm6i6$ !g7E)ر#|o>ZX~.{tuV=dIQ9 #7UV6+M?1': fֵvo>jڼ|?֩q-{º6}Z;&u] [L:6H]-kq֣ۼ!}ؼO (CeTq|AV wWu I4yH z}oyu<ʦV\(o@IvfSj˶xx]#.n"[\{l~T^w7/pχ—t S!>Raf/sPk0@}K(P!0>=Xf7`{v:L" YYA"Bđk?,T3ӵI\n>lPB|-nß?WMQ?A(+aȏ,?Εz7qNVxǝ4qlYT6)/Kuԅux5? <`z̚7kOZF~~fO`Ǝ2s7y1zD^~S~d7~tWunX{?#=[ S=r e!ʪg=<5 zh& J* ùy7tSt{]b> 81>J)] Mh|.&Qõ($|| .'_ˆ_GQqu$o+M }>2]{}"g 7MGܕv6O;K}NެNF )?/&>^?&|i|'PͲn\޾xD/t ]', yP[vT8@칹v$|8c`a$uE=`n3@ɶyK]BQk\-u^jbW2R c!9" HKYBXpDD\DD?S ђ?sD2lܗQR&Bk@2p$s~=$ꍸM G^yٲTF)/RWdCY?ZFZoϚk߬gM^uk;ئl_nmctZۨAM2rGQa^ a' ng[uB/*CaBHO KS -PCE'~j({JCR_:/Q=IOlXtzx7Ҧ_}U;餓l^sxQHP^9AԆ C\QY L6ҋ7HC8p]r%oo7pM<9QY8<*s4,|xOT}Q4iR='79@Fn~'M1P>(i>rgv-NڦNW\/|~Q%萻Sw믿~$ [/ lݫZhKoGt W_N:Y&M~@F׊˧)uTvPw 6DZdz.p~'C;~֬Y3{c\'[ѥqN!GZd+V> G8'-E#J 7>*AXO"s,w rIXѯ2qĝpDХ;؄@4}"7}gw^<?\]vstH9q9n_wC =͆NBX/M%w Oy;\+-u?^PdP_lx՛_q|?e@v o,ʁ13,ʡ6mڴxTH8-Aု_ _6H(_d7/vDEBUн{2 _6.)A|>KX8WZ>}γ'+ޗ_~D_u>mcǏ+ʵI|1ЪePzH/۬de\$t"sAɥVed+*ԉTXd$"C8z]%$CH WK7M^ +&sN7(qTJQGC䉣"kᚴ~No+An_aYT6)R7nܸƉeַoxޫWoǏ_#7àoe#Y}ݷkYh?&mk.(C|dB$~GKȾmbv6OKk۬Vx"9WQQQ+D93֋kƶb x㤍4_u{ュjժ.ĥbN On#hC'NqC{k8Nkժ[*mM7/1ozXz֡C;C9G c]wwy'܃sb=(ߤy)D0E,o~a뭷)AT &#F㭷޲w9GC=暱^y|}ry<w_VZf%5jԈUVvAťs,zG% ,U/` b[nՋaO=T[9 =q]euYgׯ_ Gye>HW7ߌn yQ_ͱbmov>F|Gm.snwYt@~^alu5HСCYJ~92FFZkh>/Xڕ MڈX~O̿F/cJ=@ޙRzڊai_4q?)3u>|Fmc?H:8#su} '%G>d^^ҁ!X>Mx3??ڟx }[d7S@ܼa }Fe}=U=^|5k'k?eu1~I/{7"AhШM2>lQ\+/[n >,N8ncrP8񉱈dEL˖-c)+E4S/Rtc\>܉KMtÏwc;bs]GaRW[_hU:6gi%dmNV -oΗlRJ:#"3rp輐N/"ćp-+dǓ"a"]>qF~Z)B&(n\y ?)\*S><ᆟ.GG᯴7/]'9Mʃ}q;;{'N~q2dRȧ ecvZٸ2qC|?W&o\3Y$?vΨG>4ၩ|L w&_|q?IJ0YfpY$޽{x8 ~B9K[9ehB̈́ o!`,dy aAj& G@d<%-u .8.rOHP'&R$}4E|=<{xc|(ReHK)7e|AC&a:vhիW×\uK29䏸_}$LꙌo&oDFQI]Q~!oe~gڐsڞ4Eꄗ*Tx/iA!5I kɦ [ѹ뮻b} @D+IS 4mD@^(+#<裏Nrt9oi%h3(fmuC >2&[oy%,a(7/Uڂ {n]@\'|ͱ d%O[2$b}m,}QB[yj͛7QƑS]V2ۻ$|2_zB"[oZ'$OS$3E[nM-yL+;^:2/t(W-!usȄ n#ײp)niB9Gc Wp_H!Vɝ#EΥ#UN@Dp?nJOp٣M!V\S\KDlɑ*MDIo!/ysnV"AU<B7~qYۄ!-m ##Q̒)I`I94S L\V &^{ktc͂As]󐄴i 챴&L:Pۑ_MH , C?7g j@h:z&Lf^&W8α xwH`Z~HĨ7&CDwO$T0焃аIq<J?@!XD ҥKLW^yb@SGXpDy_@=AhԑT>' B~ʡ>M}v( upO,|oEx&i;C} XyAepΩKyz>G&/]@z-<.n Oj )&Rā\po]_z+wT:CHOe Ԟ^^^0ЏKݩS>ʬx!8 /ڋK"Y;wG^cyn] ?|@K }-uXqN-[͛sqG=>߸ G o*; Fmͦ%<Ԏ12@HաB~dıs.6aD9{V|^Lg <RaV)VƩVͅVm@?ӊk\ꎗd+T(Ż Z][$\LƾKYF8nJQ F8_c"H ukl7ʑ|I*&;Ek, XnAe{>x& j_2ǒI[hv[>YRȟLC>Ca k 1sE T")XV߳h%&= @8OX)KBκ{ŢJփ| S<_-,<)Kcf/$0yg@~TL G[!%x/}#8Xxc^"Gi/'"6-9C{5@Xa寺Y}Dި3- 菊 &-QЏ .hID/rT>SI?͘0*7_$Pj!,1'q3!2!d'Oy`u$?fx2ϮݻG7y!w`M6/kH맙3b<|rT/+^D T29%Q9K}=KcED$-vtcPm#x3gXLUR++rUV}kmݮ`vފq1g=noRm[K0'RHaSYde:=~3yb2ˮViAZ/ğ2] u-66ePz(0g)?DJPf,~RAL$;H2!0LY&PbbDʥo q#/+&Kt0Px@|i; :doKh_,Yb\$kUIщG}oh3YЃ BR9ꆺ'FIO9&k0^ K=wT qW= +u\[(/}RB:epGrKM K/4q#rQfy^P|Bԡo5>+t1#R2 lZ*?n(қPP iWO^76۩qy$:[biH&,gyn=Gt?=䋼B9hqp}ڗCK5?K%m OO? l Jgj]ZEy[H|g ;يK~n{v,ߥr+(;^m;dkYؗH]_"Ney0!y$B.+4[ԍBDȨmkI̓v6upkۨV.?j :HlYx->8.>I߿6kYQDK,>-b$aqLN:1;V[(-7|ݙ(b @,LiʪS] (=}Sp|''}b, Qj5DE\ƗECtM]z>#ۗB:HM !|k)d[)`9'B7pK]JK*u @Rp1jC!";M-K oli>+ЮC/a RX)z>+}",POAQ,],e K&XH"䏾#@Ôӓ#t=H6v:ס6I=M|VG[>[f-$<:sݏV6Q$:/H]hSڌt~I?}Ս曺he ?E "'  bR #GW@:VՆX[!_j}'AM8!ǍOc}.K?qaǍlHVe6V] t[2A7u;|?h<'W-&1\)D)Lv*F)& b|ÑAPZ AbiaF&P|CH\H>V8 'M>H~ﯱ{46`KJ{!K)n "bB wCH+e&Z9Ȇ:v[=҂$q(3~LߍRN!/_Y TuP8@Q_^Q䗶!`}_?6ң> X<9䅊ak3|zh7@e(~"mQˀg }Kj'V0!J| #Yr2ϳšSuNyNRg:`l0y?]|QNkGu< uw?|o+*pW=JH9qˢ-O/RWVov$@춿4%Uw<ȕy`aI./dHy:MP@44u!uStCIݘ@b2zH%HݷC:.A;LaaC $0!p1A"2g vضOy BX*{_'L1ĢY H1IdR?=y??6a k2]G>3$b|I2䙉Vư`[/}oEH!>,iz%}Ud$Pya"+q-ȏBC Q_l).M!C @Zeɣ&ې@\fɒ1/FTׇ&0%\Iܸ9feÇUg plx͖dџ >.P< Ϧ \:B@!}W߻*tgR=WM(Tl8هJ_wȷ$~>8_Z}&Q_P?yR\b ,d`t z տF:vRȆ뤭u6+ћͳRKXY";2%$+G "M F+ނ:GAR7~rʀ6v`37SC_?z%Y,<\cu@+*%,n1,Y6l -;oi}P^`I_?v ˍ"kA: oz&7;Z$AY KO+a|ĺ+[Uworm(V2t(>-8QJՍ~H_e[I]mϵ7 |OwsqC’H]_(NeԡG8j)m]~}3nye6nK1;FIwf Oj}ÒW?K^Tcׁl_ޘ,]h"2>̒NtM ~>h9]3x&݂'k3< "%B?pWs.?NYֺiCˋ79mrNJ6Jb.8t㻺U(]eя/)ԭ2)LT Y@5Iֽ4HO%ƨmؤ/zلc'ɱC. jkcF#:kU'SG౬_@ք峚 ~P7a %$$DGҜ0' #ulV ;`6J忡nD]--M[eR,$s,uM_@!hcv\*/f&fQioncdq°6[EkaP?~PAJ#kqY)䷼yY 񄄄_JIabHymd9t/f7|~:M& jaB6m8o\?C:&)Rעq͘v¢%<@d4"TqeBV;B]c~ T"HH( 5*9R,$RO$b& !hٸ~m' mT&6unۈ [>C[}ۻZ7y2]١>B~ N{l Jz"HH( zq*1c/J'B!e7ìWWlh692׫}] r^ džY7ؠ퓮oڂ?'4t`Obyɠ ٣ΗR ]"&$, EmPD*& I*i aM}×{Y熵Sǭ{ZdžOXj.iujTeS:֡f1I&/Mׁʾ"(ku~!gk3!!"<tR,$:qbk?۷o>}'|\t"_|:-KHHHHHHH2JabNM6- [AJھ>!!!!!!!B % ٷN׳g t3gVt1xf?|Νm}75ŏxys;c9 sD Gzmԉ׻8IHRdu'uK#e/GJHHHHHHH4X I]Ѷ'H Į2:k*3dɑ<7O hAǏ~?ܹ˝x)}&)?L#qɃ" I*TKyl!+*Yʟb_KQHW$%Y6ϊ68Īlrbo}n3VNR'k $f* n$ ExDPw!GpCH$!qDHSy?G^_,$2mwqW/ϕ J,uknzU4IUjK/ p_ rEX?DDd1ޤIrMwHQytpC_.X;n'd!I,u-[6V7C~V%m LHHHHHHHP?f\+PImYI;ԓSNN:!ibgyz<3NO>1O< X#*Uְ~>4#̑Gʲ >4ƃA b"2A=]G쫯ȓIȝHXOB#?N6CDjuƊ_"n oX~ձ"w%$3 "wG駟j'x|_!S&1d7mGկU^ժU+>}> c$s7!}fZfM ," I*̚==Z 7˒_ ~mIHHHH(Y2Hd5C;Gxzpu:kY׮;C;oeY#ZDޤWkBC.BW!&.n/DT֤IЖ?ڄ &MDZ,8qrI!IT)ԧ$IR1=2ZDo/?eYo#G縑&:_~Ea>q;Fvmcc>nG&#x ㏍K< $Hf`-Z4oefKHHHH('d--Bi/$$R,hSrԱ2Zu:.LnW":ap'GYCGUlrAx}gVju!|'ְ!C_ʵ <0O(_x9m]_oذ!ֱc{EGqƄo>( aFؗ_ ゚7o޼  G zBBBBBBBBC +@N۞HݹYNۜύxqE%uXdaq5Rk%%q#.V0ߧzJabHe&ufͲ!Cy3-Z$s͌[$ Ή)S'D28yx";H %xhx#umϲ.KrƑ%:<2GXµI-B!q1cgՙ3Yws2e+r3ɽ`1+`vِ\:%$$$$$$T@)̼H~gEgնun+`#/# Lx1_K)(rTXpC8)LT lN?~Ǧǥ9f˯Z:uYZnݺO3<ܞz / 7ӆb͋_|fF} ;wn<&$$$$$$$T8R77UYo_VVYV͙񛺢o. $.=jFbpdbpq?nnSc GdVІѪp) E}e13ζڻ[uHV7{Rw`8dU=؊?܊7UxVm zɦ)E5Jd ;B.;\H "%I˥v]2nŁ{f|x׻b7vvMv miw~λo0O]rUK{ .Aw1>%lvv8իW-,?|y8l ׿?x}<nq558 "5_}v7k= <1{=J @3ό=JYȣ JpNln:^‰ɇS|y/ao>!uǏ/qYt<*@$בQh\qljn@dIy8ޜȎя=V&N`ѳs?yB&i6sƱmvֱs>~m|R~=C83IMJg͞koSz}'_Ήd +:׳?-JDo,uu*aU~{U8щViVYVm qت#rE5lwU: OH]$@*;k٥Mf3`u?)O?j{nq]vۣwkZMTԱ?9뮋m 7c9&6(.kiCs e]f[lm6->i<ӬJ*al[#g >ckwqm %wlvyjf͚w{a믿z駟%u֊:Hc7;w?@.zm6{7ӟb'7#dMo"Ñ:~cSN9F~wygzvF^{I9'NK/%$$$l0J2t`Xg#9_ѣGGة̟ <8Hd?S6rd:GOoe f-[Oy[}|V2*ІO=TMg;@ Y>}g<uԉm3~ʇ}G};ocɓrz oK!uXHݺY[9rᱹyr-@"C+D$Y줮EV7seMw} j_{{ѱc'Y϶[.^)࿧ڼ oxA`dƍR<4 W^yp \|_׻wx&uY@2ƌc.]s~X:s [n%ȓ¨dzWQFEˤ ѻc~ BDK.Q=\,,=;+c}O6 }7< #u?ߢPgsO ?bZDiaFkԨkGYjBBBʁ^891R7b741V۩ZO>Pg_~3]gIq&؏ӧ忙8'}ze7}2 S ]^aiT[}ڭG'bx0@ƍ͛E <(ܞW7.˥oL2>i$!~+C٤hCT;<\n>Jd#KqIf6@d .-LIe'u;~# wu}8Y 'omkk?^3oSbAApu.p ^qPAbXM8F$<K5>xNx=eݺ#Qۨ2|-^}xMgdxX?zH 4(\,dEZjZf3,J92oPڵkL~62fUV1?|cO>U'>AHrXM":7vL/| %YpỺb,w?rwt w[MO[-wX㥑i f"uI,TzRץsxxyDt[8aW ε?}6'w gZ#s|`#Q"GϯhxNp`iǢ"u]7b5'#]tQ\stac&K)y7o9*,K"i$B )MRG8cjl=*#6'鱌0,ԕ/+]w3dM*Jy%ЇMHHHXQ`|8.hb \ɸȑ0=y6mdZlm|0\K䅥^uQ[,^=nXZm".i*v2~\P?u~h#}S|?0iby޳SujGyK puyӻK:ZAn#:uC;Ҋ~{mtmrBޮx[~IJ%R$/Nux;\M{z˳lΰ;; 1-Mkzۏ~o d^`9Ԑzi*knIl Ơ7`|[(m۶O֪TٍRx XjՒE@qci%$+&/ؽS7²Kv m9[~ T=&r_d ]4 ,Di,_}w$}]^xa;Fbtt<7tS$O?tWz +{o,m/ dJz}Y7hАhAt i&Ku>`gãEG}~\槍R$X c ڏ:{lgciB!u.:PG[x;ޏo>HL$Dw6u ^lׯ_=I6I>Y##';y[6NHHH%`XsXnLj/†eo۴ H,ɄidRuf3*6G!M69S]ҭk$|H/~χP?L1.rU-oikРA|gzj4~o u~yU;1򱚏Grokס=#[*,y^x I /i·i|qsa-B7dqЫ~#.E ݡPhq JۏZܝO q<@,U]Z85m^r`c'O?.E߰sKqYjG?/@y J uĎTIrR:B'u~&!h̹y%Bwt?`>:d5.@;e*ڵ D#$k;xG,<MzKXVu06Hc2x !nzpIaqf4qVp8?'}i?m~)T7~k-/-oDl8oYeZSamdכֿ 3-klLqDڞ{{&鿁+UX$V;HW'u..!NZwlY1;@z\~>@$zIkҫo?Vb#]/9(N6cKiG`zKJ8Z'F:ꐕW<鰁xnrv0Pn 3C\XY>)6l Xgaq?bv&3a_Zxz,xd6y%:Wuӥ}8N\\K ԵjS?.Z0_HY,p^:-Bg{o tvIK' ,u3gNW`ֹZb!COAƃ3:-~z`cwO#~ǃH >Gc7 a7MbırnP]&#n_nC!f n ^Xܸ}cw<-c|*Z:(+^p Fx++&r:{uyx'μystO:Y&t}=qO)){M<20%<•8;.#cIC8e.O<1WO[xpYu:U:6NN{P#[6ĝ)G p8:-!qXږF&$.[V)B QCrmD r?D+lArҲ?+ MJ"5CLC'ݓ+%Iu '֭'GZp8#?Qb\|A{jq;裏TN8Nî %P##S/Aps}٧e睛Dwf;s=SA*!#>0aA<ԓsx.ģ ͬpFml&a2qOz}pYuRh.vcMp̘o%ɷ~-?0F~{N0NƎa)2V{8q'Of,gMi?p8$uH ( dcR„/{GID Kpyq쑧&r qRqktyro+<򐒲F]z5Q9b09Cs8/UĖ{I~P3n4hPO4n܏rٿ6@7p]"O=jMʈe3)6pY5RȤ(;֟cqCXJ=% ͜Sp8H 0:[Aaw}F?qx8;q a&G̟<^|%NL&!Yyч^ӧ%K?sd!}~V~3g[5owԨRTTGF$O?);찝ơ~IVW_}N;E]wm.wy>JK!]vg U'@eŕ ]ʒv]K,!='xp8ή,;ꆵԡC;%`\?\9眳wJh>Cni&1Xa⊬:J^OA7xA!!S7|SB_R}33O)ЙHiƏs}d2EvxIUI˪B&unc~gI0]5U=q8KB: D`,A yĹ r`C?D+S(N@p{ac{z5Y"Q:u$!}'%z7r*:5ot%%EҮ]B x:"RNY{Pui{BHk".Nf̜bp8(p,Si v{HQ]n@nLR>9 nH { ocy$e8݅!e?jA"(NqjeY$exIի{ mߊk7,a|>HicO ٹgJ(Zy(La"ME &.FAAy3p8Q( 4j.k-M.F{K݆ARɗ]tRӽ{W9)ի#?U=# d"GR(Z<=zt \E ,IKH&~Fɞ7Wt8VJT[ԕ4X z[q hy4sM6R]tY2}Ɣ;?p8p[8Go/uJw:vkH]@DR1a@88+@ 1X ,T<;DqC a$RqPI'/G1Xd8Ãq " I\uRˏztvi/S!KUWA-6\h:;o>SH3bK%Ɋ"K˚">ֹ䣰\HZ~w v]3Id K AsXXz ,nX< 1rdɬZ QTA{%u/O*@ұ];hd SC~41e[6%~1cO2vm=xZYy!o>C}:e欟ޓӧq2~D÷׸,?>ťVˏceq7h@vWK]QY 5AjBr *|,u}v<~bpϷc6r8H9SF@ d;BX6G'Ёx7!:Hq0 V?/+ q,\q'lᲊd4iٲ)S*~3R8pNdM$" RWɞRARdde=\K @ EfiMܐ#ădB\RIE瞰YXId/yOip8X) n@နpmop_i1eZ 5#A<FZv-K=a\!t3(R<V,^Y#nVVigbݦtr%-ag U'Np8U!uM ҍa[@k˚I&.."k:c)𙎛v}Vs8XK -pсt"AQ}by[`%pdnĮ:qR►Q^^B 9kT: wM+F\W5:qRR\YY D:^6Hw8?yQ&EQR/xc*,s)4_sDI*U"6gD0 ;pf,J(u6uy[0၁حi+jCl寜Թj)RG]E@uB_p8#_RWR`'y"KM vvZ pjsRʥH]uD,[r+{Nvb Yt>Yy:p~${J$4IIGKGI驕UeI!Yb^oJF.AȐ!dz{qCpO&O99p8kte uuj&Emu]݆n j^AKQih:U&BwaF-T뛹.,+y5G?1|p?~̟??;pS@f=unոƻJIn}zRRWIRw;\ \i BeɚNYu#u!zX̙# ϕ@H>de ,W_}! A}DZA+ee˾<wq5a`\7s\y6YyY˃tKk匟)*Xyn_7t\Ν[qW7qa q=T& y .+Xn?eRT7 nށk}qc*u7>^J6?UJ wgt9&3&G(i쥉\,N\\Cy fO< ><#Ѓ?{[<#aCµLoQF,3EHkJ*V&([vVtY-Y\kJ-m 0@7nqyX}qq/6VEDzeLXҿWwQiGt}'q>ĺ #N[HחaaqX{\H{kW K&u i OlpmrbB8bAC&)i[rWUԹ,:ݧZqO?ɴidi2eD2eL0.MS'*v S#ɤIrՉx@JV{vYڂ4'oe$.^&/Yv8۵~b=1a~kLpnrjCbu)2Z[ZkZzhs$dadRXSb+)Dxt*u;"“%k$%B i":@XN9h ޽tQzt*ݺvPS.!ZT°2{.H:wLsC;oפ蔭Cw5I'Ԙ4tH/nx_lH~ g?:Ufñ7k~̚5K-k7ka%$ KS[@y|i Y6q< Yjű4RWA'MQh))RwGU;>ypbi@4IrH=FNǘ]AWoQk߲] ,Wʹ&ׯOZq@^˜<اMX-L2b3b~c״tc8. ʰ*XouչՄ̥U% kD?+B2MQaz`JfIgH;%kֺ[Y8sqY)tR׳w7[Y({={J׮]{.*=zt.]:I]cdҤ JHcWS]iKFdmPfڲ6wyaoMM_kOY'G9uY D$~orIX`3go|Q˼È?kl"qTD`WtXoH]bWqױrxӤxu],LdC#r8sqY)tRץga h>RfQoBiʒLq_RFIVM C_YGƎX+h^-n(S~yr}Ï>"=p<ұgUFiLci^z>ڵ}ţI6r{GF*N@_&NWRw8oE@b'aÆ0Oq- qU@č{oW 5%u9?T_JծjSEAnt5>X44pY1&.F u%s C/=Mnxs_w\}墳n8QX3YaPK:V:ҩSG6mjI ĿuLYm:8^zn=^5D\'8͘1cnݺUH]\x!Nos,5Noyso~qO^Mr`I!NgJOYq m _)I'l!uSNU=ʆjq1iG6 Ӹ[n%:N]:}o*7'v !O?lX Q-˯&u#,|s.j4wk$ՈuvZ6K:*g6/ճE*~ ?[j ZVHt.YO7x 5$Pʸ=w>g,#xt 1LoN7eHr&Kڷo_q'rM7[l!7vA~ܠިQ#}ӷK&MtF_}y҈Ϭ-SL޿Ϫ2 zF @sl馲f_:¹z' em2tI_w}q/,͚5S7eZgumrg .7\Oh\ڈ&[of4|JvuW]NwjN?ϲ6ˇ;N _O> AœXA]w]cɰ߫DRGGn]?P>@@:u`^{Msi߱| o7#X9iD% 7``MkO>V띑 ,xwj;>#A~a3PmcOK#sF䞀ec~^ e띷!w,=T1F^;ɚ0ib=ڶ |qmhGth!^F@{|@ :f{ʤ)eIJ%fqX}R Qo"JԽ;ҷ?-#V5H4X0)xYk3KݍN r!}vJvuvRǒM6\V:k۽ dQzHc JN>#u#ɿ"ݟiжNl@edb~>-+D:d{,MV:wnDh{Nzk s KCraurx^>иF TF{ֶ+}e%xFrEՅAvc鰾2^@쳧tY,Aq*l}/{|7`@a5cy㝷g >x0bc"#4RA!BX˰bŠOML He{7+bmg;sBvNʻ}|;;yTK\93=H:S^=fI++X[i:wh^aiѫXʧ*H+Ht0%M \@8t3h +,u|I]>K6pY5aPjq,tȬ¬>L'Oʻ!9sk2ceKGȼ ɿYڴiGM'L@3)Rx[A&Gf)Ĥ mDX[n%}IAk}o R4a4CH+/WK'ApXL{B@-=RI6rGe=vW|P {'XȞ@wXb*&֬m>&E|H,XU͟DQςC"y>3oײQ`vW\qSOi: `5Ɲwީcw $ ?wk}ĉ}=⮢ -:o8(~ܣ;LU3 d}Zx!}Fͫ/?Z:kW:R[ ua:pC젔V> $ezێ=u饗^20)f@77DΖkǏHyHֿ%t[aM/c@a2O>,jdDKǡv&7mO+gRdVD!:"òe),#/Ӗi n,^ℵ,u1; ?4XۘoJATϚZ=ԝvGϚ}t52X aF)Cd }E~ģxnmm95T,--uzcU#=Ymp8"T xURO%u,{p^KdSK"DWb KCJқtUAI_ uM;~S *aVM{Qg)X#>ACЋO?%IT$iq > {c}>$n ԅ%kSɖ˜A1߱b~ vsRϒM6\V:;W $)͟]Ƌ49"'˜yDfa #T?nvIC5GrEa,9rtY_&*a ^Ĥ77urB?#%s um:; А~S[a{8]Rgvɗ^y9g,vc":q$d "E̜5G'*ҋ!eA)hg~ ,2@ C^XId4Y&j$L,-쓿\~h?%vEuc_{HR)RiR VRa)q^{͝;;?ǞoZ׫#!'G.Y1WuzHs衿.DﷇIN7rP:aJ">+aV c'ʀL2IZ,deHMH]ܱǰYm!n Bm۷ Ybb̆r&{VbbU{7HbHʎۇ*IZcC9X|C| )ArXlBdcYG&ڐ:&d $/}o%6K!6 &{0H#ϒ:ʇeXeh_g858ìsH:H !%{ `)$/kcճB >S4&#> k%'RRow| :5 B:OFڍs5ƧCexR_E8ugF{7HY a==8pNA:2Zkg,C L(XvdydtP"{ߛ6a"Yx H@ </yk_^l4nږm{9qXI چt/}Y =@}d4br}巤'O-(s>n/ȓͶqgI]l ,ޑJ6m3ջH"#3"=4Yz jLcqc{&L6q~?mJ6d0!LZ0CF&EEu믿IL3RXzY%DeH~tjm)nt~Xes"}+N~L~C0WRjfCW, ip[8%t¸Rtg ?(3,OpNǍA^q;t g@J˙_FVC'?Wj7Xܛ;[[ޮĵ@? co64FqEb޸\V iX\t-=-MNbM/c~GLp']4<Ր.Ok:@N )4 fzH57#wֳ3,^;[ {qKoq,K%.!tdp o:ֈ^ӿm8}܏ñ.Xq+⾞mi־Y0+ݶ nq{Fڕg?7巼X-;UO4}ĩo%[ԟvbwmrnv[mΖN2]pyj'*qĝpqrI'==WHq!dCi Q4qʕW^FN=d/Ygg?lBrsC]Ș1j0>fm"7p>\-[:4;D 9/Jf;8ߵ Yd U#5Y~iPz"թ6Xԥ-L,r7g̟ż6xnmy[V_)P4a[X> [7?W X֙@G:0He!A'nD2Tx<*_<9?lYJe#+i3FF,Nܦ˝ Mt=M\t]-`L?rYVgñdYeS~~φwI7]'}hV[WW_:{qF~Q'$K?po,d[@iqdRs!R1R]SVXAp cI,A:%nCL7Hz~qTq5駟"IĽ:Z urSN9Iswׯ _nOuL?Sw9g+4h'td~嗣k 7\_IYX#gZN"q[eK6pY5RKu.^m)nNoԨQjƞ>?W97n>[ݸY^:XԱJx '辦M745驕:ݖă]oyyeՅ+XG[atꞑ-LY:)!.ncuÍe0]㲦8~֖<32ⷴgp8 ~ӿo.~q?A~1 Q]XmG\t[' i/4n3Ao8PF+oʶݺ$ <ĮhAB\Ab p,߫E + :#u &tAґ?zÍ b^?ƍ5{@wD8LL{+ICc%+))RJ8${C9X_n22ܧ+QH|ġ,Xʖlj&:CGm!n];JdJgI4d͗E啝-~֙;t闼pYĤ&ÔR8!.3i#]@X_&Mt ;xvoW+i- r-FNO5nG$a,}\.`@sDiuaa\M?nt2M'}?<@oZwV$* u7~#uAd ;?~v)ƲE7& tt#F~}^z.$>ċx5ک7q@+i<8 ~{pckذ IӦ;(1k׮AXIkO?T暫4#_E:U/deȲ|!n[poeG`AB萸zSok1pT}Vb@i|<g qZӍ_Rkf" Yq@\,'aq-IuJljx'2amcI\sz61eYReA_KDž[- 1!Xeqtv857{4o1}.˫J~|[OFp=/CK:wyOi؟:Ͻ]cg` {f L+X/+6]ݵIWC @/T2K7pg$2d+y@̊wwJ6xCym "֩SG`B3=,aҥ9R #OC^XHo8zvۭw}[80%O>xFL,deHvZ+[,/Vxqg~@dI#+u:8~րbǵ,M< AVi,ib8䋤sg[V/ji q i,`lȪH盆頞Vt~7I.s[z/#b=Kzf8q'p8$ukH[HۺU8ЭXõVYJI!AzXZUeX }0:79Dv/v|o_=0]]4LU-<?P:tZ uX!нC Gnjm63e@o&|MuI&q^U=$CSՂH]Z٫ljH]Wҭ[jR6,c7u-Bp8GaE-u RW49$9r]҅0M{w^}0T7K}6۾&p8 Э #ͺG8JIpct)mV@zH,AtAn@!%#L1#^L,.'Ӂ?n,<ć=$le .z2@Č)Vқ7Gʍ>˳zYh:)$RSaCz1iPp8Q/B:Hc%K1irmzrY{?:;ph3 R_L1bf &0,dGt&/Zf54?ľeg;]%Q+kF3cVْM6\V8K c5+^ o,XrqqqqYp8]~Y CpX!(i/tӓeRI_+ [{dAj H|ˈ.BzL3e~HHge/^it`&acqV&NҚN`Q_qRdZ1s:5ղAxOO.ƍ'u„ 2qD.IƏզ.S,,o_o\\VL=Ti*K.cKNV2*5p8eEtdݥJQ=x}KJR]WRoSe/?i|]Ro q,%{$%19pשAsV,qRM6\V8s8" 9cMEeNH[H;_rHJݵp]) i㥲n7ɺM3Wʾ= Ż\*%q\ԲnI]J6pY5p8Qe?Jq=hmN D ߠ;T8AJw4 ڿ]}~] iK=/HNKWJ.Wɖ:9{&.F9p8"N{Xlzdi5h7Jɾq)i{s;]-ͮRBWok>4Ż^[B!jsnI6YpYI]VK?>|Z<[jg VVV xqp8yEe2o,ӰY sH͎/TV^WK=%rX. rZJv\Jw"H]p8J2g'uMɂK~H!Zz͛7%z'b.⯩p8c G̕RRIh糤nWIAv$!NRɂK~/DV}Y]H_:}>p80sõVHΗF{^, v Y pM/ ׋+RvRM\C eoa{{./_ s5 ys/Z[\K/}p8EJJ<.  Dn˥hsh3}"X_XDdB&u/2rp6l=R 'C Ս:tpN;% Җc|Hor}xzq8/ ԕl}Z uA0oɒ̦!bRsRM\C Աr̙Iᆜq9W,Wk,1cU:p8EaÉd3xKQ;_J]XiR)e-l~ERn%:s;wvŸ8YM~\@:tqM'ֹ L!N#C[[e{8bq]]8#Ne5}\ӕ.gzЊAaY>Vq^F::Ֆgum{>o= v~%[8,@7/Hqoi_qrWWΕʚn 2<0&<{,!cⶊ=@\Q^%V+kI +Atp MXpC##.B,0) 7eJdDK~5%uiGz򶴐8"n8/(:ԥZVAȐ!CdjB7bDp.0,zqGo_Y\0>П vo ȱz⚮k<Hwij@Ni'|R~iiٲq o=?VW̍8itפ?J+#.>W^y§r<ҥcWsm ^Cܯom!.oNՀV[ɤFpXB +-i473gV D8n,(cC+[&qzҲ e>|N\]ٰ~ׁbV8@[>#a~?16.=wԡN[^MJK! 3ʅK'oZLHY~a@Hg3(:3=0'|Abc;T BLJ٠{MO~qA<7n7`nmˇV8 튿FPtXeY*nva6l0>UwVVǹ[u%zr#p8}֢nݺ~.` \=Ѓ ɳk_K yX~qx_P\6[ W7Znu%XgV4. s慱YNqOm }6vǀY^rvTHcm+p1L(@RgDsQG(y!'p;ܐ7 yR;B&2W&\qoZnOrAoc*oZ,#E96~Nt-" .#N:w_|!շtT?uR7`1ޠ).]:ԟ&;%nN )@p! :cjXBto.mOޯ4o\I!o񆆕hK\Q^S+-@7L[R\\ρ7、pތnW]uUN7 / կr~6.rrxvaDgܸqDN\|A:sv857+0 S1o7o~co7+ ?|i y䉧7zS`KB5?Oz-ߥ}O>xsZRr#'+aXC3I|=?mKDpLl^йsgt9)ci VWo mT+H]$/!wunWRx{#d!߈122fq7Y C +;O-Zkbpꩧ/\\tw }ꫯdwWBx\~-@=|ƌ,jE Fl%mdhc=VmCL?YgnDn6S9#wiXcr^j{:J_d߿Yݾ[yǫqhѺ?MS} 4P n[ 郠K奔--$i>ZL9!| ]+O^}iq>#y9q$ytK52̯0q˞V3<>X\s9 KdQy|c̷ɬuΐ &hvQۗng)7<tKy"띷e!z(oN|/KH$!t-!vKF d "RJ,p ±~PAž:,vDJE@FHd zjٻkd7e[or=ҴҨQr0ao~ki|'%xrT$%I]XxؿܞÆk%+o~n,@oSEu1A  eB8ZoZ N^oҤ`كxAc?A Hs=ra`eA- g G$A2@L^ =cno'0Ȝm=H~U$oc!׼qXźvnk[5ir@ hMIna3PV)iz)}ƴ¸qR]X9B;~?%s `} msJ.տJq9?{dFLcϤEOM~Xأa2CX=/ڰq%EƧeu2Cr!vm iY;W_L,|Ҕɚw}WI;uO#g,\twɘO!q q^~2pDW< s| ucs繒Ss˟2+*[yhso~{.1lp8 ߷ݧeN0֟ f4&8 #/H!AXLҹkì_~.+R&V%BB۶o{u|,-Dz:;Qg#dp^Y}x!h9{-H1ǟ|BC<!ܰ]!_`c +J{1%|r!&MT}Xuha#˫nS Ա͈ k %SO=9C D 80di:1gCZ ܬ c0x@ӧ M;TSO=!oƃ&eqg)tR׮CС%X<`j/$&ia'(V-XiWɿ{Jpd Ki3AlneLْDPX _mO߀?eErܑ`}X4!3ʗ'=mAb,`<5+;oA:(m;+@Y 5ĕx_!?M'=>߂edB0Bgg|H=̏!l!Fjc@V9qr"@LjdQp)tRצ]йU3.U}:x6p7oߞǷ1h2)NbU P.~!wL,-')Cd!aeg2)X ,$dčtľ5⚿IdU 1! REk]y0Rxc?z9.F*<2S'ޜ3=Z8 "hҞ5-9<ivV8/Ñ_6bd`6K ,carTunبgƲ{_O 1#GX~ٲu~:ǝwߕ[vHq18x V7,fU30A^!YVwɠMZ9<þݛgr?ĂaJKyvv z7K$ B!P ԱԊ(K/A >CX=$ RhQz诔ؑ$ "%W#aG>V8Z{cIez%je׏MänW^Ҳ@.Y~Ʉ;oe -񲈂KH[,Yž 0P'ξU?mGl `j])(B Q1 %],ҳ ^.0/B>l \r›^!0PeG9m Ƞe *yJ\IE|tK$ma.[lu 䆍유)5Ha;S<#@CG۞w}N 'a,sk1LYK/}E)i#o״iS-7o mpwj,}>t7ԍbKoy##?nvA  g,ѫNc:;xܽY31Tx0%{Ӧ][=w{ 27nxYGRF~PgHӾc=utVdL[ݠ }ugWbmd?uN^NI,.ssPVDxXiRN{POR?H`!b`:b@Zvi'ți@>îf?%0X\,OOLZ! nJhLòKQ<0w( !eu衇斡2m);mg@ #:G Ic/) oq!{Eh/ȤIul~|?YzˉrRmft.^0XYmp86C ,@XB cE mh|8 xqa a;#Gҽc$ӈ"/N/Cf,o^azG:+/+Jd뺺AB돭XD!HË_8Eݭ!ar} ֞X>!Yw{ګԕKF B|F`2q)$27 Zn?i=,A=Z d) ]^^9#Y:~ H񳥚FI4RƒI7{zɍ7,$o& A*_~t C-D x77BE\G Եj"thɩt|nBKl#nlX۷o8x6nbt|JYc?0AXnoӺ׸lOWWD-]&7.c6wVɦzZt9M%<q= ~{FvÑ7~&/0IO(Cվ/|\I>No{ _F@̲0! o2? 7YV} hbkm=3vLG_6@"#q9cuj8[ _UH]@,\7>H^ΆPr5kb @ qdA?#a=8ďI: 3?}~#u|R t~lK(*\6!˒Xy1g˲PV-H Br`qe¡)8)a|f "L19D*G\h#2R?il#ˣ;?6" .#5Yg`'t~+Rשs{;,6g Uv:,FxXa/^/~5QM}8چ Qx~Y{A 8Ff C EF@Ytr% 7P1yDK-0CpS&tY# H$Bm 鹒gQpYUСDݫrR[kݦEKˆbш[abppt~;@z(PQ`}bNO0>1cO:"1YpA3rDϸsn"i\k6+@R!!#zF(yJ\jK'=D \ 1?ğ{fÏrNG0a\\qHO~FQ4YD%dUYV7#u?k# x8;/|!o)UI&HϞbN[sQ=A2M֪[L:p}Yc4'(Ĥ.E]$uHnHd!Ɉ0Hď B#K'stț&~1G\хXZXӋFZoiYD%I݀kiˏUOs3Fڶk)m~ZKmԟ=tZ}*ڵQw=%ٓĮ0ߒ 6!7ߑ;s8 8j?`.QI]]\,LJlny2k6f%eM'u6NX*9qD=~cP\[tzyFp8#M@NϓgK$bĮy u/Hk~e@*H[qkT$%Z iJK칉I˚.NxȞV6p=W^Hb15{p8Q۰PVGԩ.:RWK u.ֺkޮU+Հ!1ˑI$K]\T)嗫~p8UI]K, .2!u:%uD.&v :,uBuUAwDeMB'u+ZG,}X'xn...)cI-bm{ )B)jzN uKqR@좥Fvdk*RFrP R K6\ԹnŐf&p8RRo/$|)ݱb/Ra%AݮݯץK:uw i-E;dD8Dxˠ`K[tS|/Np81 tAJ7?2!u;&GJvH\i s+J) (qSGU)׍ǥJ:Żrd@cZm&~gx.u͒aY:p,uM[A.̑:]%F%2H:I{HyNCEE,ߌIݜ&..k8[9˕#....U|.&uvx:tY^:;%/{ѫ#wYRA겉]@Lԙ(: 䮙A37LvK5\fI>=4i~mܸ 2yd?~su^&LZ%L$YmR$rq2aXTRg$ozn%WSrLJsT>;,yw^EOer:uֱnpA)[Rw8,eM fKvmm"%3.mc8V/Kp.%˃,=..J/J {v8Sm{o}xN y,EELeRR̝&a9s+&->p8Ñ,mwl}lsZw2d ߿ :8 ÇjA?I&>`.ˊp8Ñ?pR=ٯd\ҹ,sY$KgȚNCxay͝73Ey/f&~ɡ*e^>/!|ZeYp8NP'5,"Y:Et.d,)TK-V)!g t)9/8!yQ g_)} ZUp8jdOk.Y:Et.d\Y8R.FLf̘!C L] sgϔsg _&\usʂ>{7O`b6}|W2b0p8ÑpR=ٯd\ҹ,sY$KgH!:&u{T†@dO[ɒJG1칃BhGHݤI4>XÚp8GI]Jd撥sY$KHe,#JB>W[K%@a2`>| 2L!Ch/RvUwh%۴ޟwWK_^=rYp8|L2e.JϜ9}1`=:ua3fLS8vOiӦj^C?ş>'ɕ8YeD|V%Kޚ ఎP'WG ~]tRFz2t[>  +`&.=r ǒq;M4XkYA8Yb=SFz,qaI|KtK+\8/瑮㙿ay?p85?\'- }&D|RJtߖ>X4;O|:3J1#˞a{咺43rZR` d "=$J qhX##Fp~H G?apO|&vO$~M|gD+FB)S>Kޚ _`v(?4E:#F6[(R褮gnm1a:tx fdȠ2x  AIdX8z(oMV,݄^c0?}v~k # ||c _ñx/2o|7Bg6ُB`}u1q0Kge+}\hiLǬY b=A>yN~ &$?f׫R\f?zF&Om"岲wqR-t!`^Bሑ¸3o9J#~ ?Ǐae |jH.VˋKsm駟n7 ʕ9GM@_a{'_|N o(~U'aǐ_;О>X<{z-/>3W :t~.ɳ]դW[hך;>AX:22ýY,2̚/$Dȡ%oY%ɓvH7 e0I:#Hm [gHJQ&h^ tnfbp^vIJK4 | jq; 412.M sZ7:om ~vom_1gqmyȯ*3x@np85Z/'qc՝^AC>O>/$4 #N ڵ -ZTeDM:uI*g=gϞ;o6qq̐GĽzHRkI(#Y\-M,:w)VC}]妛K !6fC,oiX6/~~!ڒ"he%C9XqZd뭩XPGQ#(9w)tRYCL:;nx# 5nРAj5jj:ܟ|-g s,+_B^6`yOɔ)S4O?$wf_򗜞N:I-E>ޤIw\cƌ:uzIe=^;&is9G6xci֬\y6qD9cW3$wl6[ȵ^[4lPZ~cǎ6 eח]wU%&MҸ .@6h#z[nQebƍ}rjLO+{Y;ҐB>zX~Ԁ!|AyګҭG3^~H}yTo)?M&H֭O[h<Çɫ&{r=>͝@Z|Jziy'GjAF W/{׭_~K1G% W1y;[o2Z"L o/ݺu :Ԥp@Ε.Hk/߿o];Nۊ![h+$(@/miR&O.mN>2o{LY3X?)R?_wҔJ&NPpE{ ocǏS2d%}LG)ϽɹD œ'<3SG|CZmd/.@?4s.%L sY&5bvNI a"saȵ^~kJN8ԯ_*-Z|~Iٓ Q'r`%-YT}TrIZFyCrI~YH.!uth#G&ߦSa. Q?cM0ߗ!B0uT)..=zT~ K(# 8 #+eK.QIO׿Jh b=)d ?,M?֬2zhMGɠ.ȧ@VZ]".M?/m~/>|Q,cCG:tx^ cqn&%1>S=Z6tz7Qْ#'vcy@e}07} Ï>"|kA.N}>W M#-X8lߜysW! B y,ӕZ}?M!<̛@lIUS]UX ov$md,J!F8@^zeu8AA,F{3cW:ȃ#\!PvPt Jp F֖$X=jn<6Bqڷo ?UII%K:ͻ^%Xg/`2:Ǐ?~lZZ-+zlҶmkk=du:ZA[~I~XI7xSSiܸ},#ѭRF:n:]VlvW^yEa?B^?;rtj[X Ҏv)--va)e/u]G6xC鲈R>IvZ: ktٞ:,u坞wYkqX& 7n7 oAf/~{#V0:i,T-[XrȒG@֭[E^}AӖ4.M?wIJFAA"l闦uQҋuecDҥɗzR?a豶+H0QZ ֶby 0,#}\6#vñ'1!\ߎNXF_Z%rV&:&,dxm{^z 6Ty@Ï?0? wˆ@8}:[jX)w:a.'9*a՞@%%/2&υ%<43rbK/<*Q:$1b=+D  r%  X )X\_,)ǎMޢ2Hu1Sqq]%]ϷOQ֭#W^yt1LC@D=?P%u.d~Q^.Gȗ_VRGC?e9RG;%^z.oZ.Jg[o}DQ/ \bwm^%&L6!uK}u~ev+wZwH0 oډv|(%дYQ')tRײ'[ -[E &\7XA I iŊ`!/:h Ԗ[nˌ ڵ][i{4[mbuVVM\VZvyi7|sԑ5KKIW1VFgX(O $Ͼ3Im? [ E';u:{u,yv6`ٕ8)MC@ uq*%a.x9+oVVZRxĂ;72ea%8RjХ-9Tg K2-ɞ9o:9e.p;CY>B+?]߀p A"(bZj:'~MtV,TO5/x)7 @8<Wԡ9/ ci?K 9DጯQ;3=#_f|x^1;yṳ=P[~p7~!( 1bW°2!N/!1Y+{v s{'U@< s 7G%38MfE{ڭ^%2|8RH>ĥ|F >c[\ġ ԍyrϒ{;Wt@8b?J@ 'D,$?Z&mgNtOLZYWĐI%o(g:%fǎUd?7mKy|u}=qR>k]mU4wW r`L~7 oc[p Z[XY' EH XH@nafqFj O}Z|O2@ư)~ R k=VX 8E P'{`v_|~@̚6mTK>K*z-g (˺Y' q8OoN:i_~ucxsxГňQ*&tBEyGyD?&nYX.2Myl >B90WVPw}W`-hph yPN+%Xbn,YٳfUګ/g!oXTa5zڹkϕg* Al 3L!+LRpZ@lwR\vy督!80%VvP nǍE4bŒFmbr`# F舋^4;H\i'!'9e_}| K$E7qH#{ocBN\FRgci[{{3ńg4F,+dkϗ2Pg2V'uqG{ڶo:`Q?P;uXp/IXtXv倜7bd%-4IJK#}YZ[$:l!P~Kgg! xpf'n k~庤6s8%>4qLY8}m8S%~XL!F>q\8 vJs0賯$WGm,\У!#~ yH̒c- L [! c%FV֗^4~zCrts ;;e͚(hG񇼰Dr@VXRF+{ァ%b~1cDpRt Os9[g<0LUWAIq!-||Ԩ@{G )&'5޸B.˿矯ٗ G}OXXXJ=i,ULs/a.pLEI }0$IN5/۴iXW4P3i,jl%uqgNx EGpUg],M, >lll`z@+4R [knXHp8j= 8Hn}s,Ӭ?vW@GJЊ~Gًgaqގ3xiR ah2}6. ԙH G7S[YuU'ąPAɇ+0>R@>/(#4hB\Xʘ{9fPSR|0eFL\GXAXK,sX );颋.6@a1e]D>_7`awnO?x#e  dyAXgR' 9zAn&B=l=!e2AgME Ե&tnQ^ Ӥd۳8HюIgW/Α 9sC&K"b-.t wep{8efc~\!D#i ^Z. 2O l K?FXvN:_ qH]ӥvΖ⦁td˪Z_%%,勬/A\sC8,uxFؐL'&> g\=( ³1gebu,LYYjc#`@2)O+ ^ ;'Łȱ8CU!v:l/Rhkߡ/TU%n%$n c_&{MkbZUX/ xN}LO;cWh0$ugK@7PZ(}bO]l:l/R(.]dO][7 X,d i2a >9HKClͳʺ*?k; ߞuñNs{Z0}CS@y2p8cM22[7?FSe%3Xn@落 .GIJv1GbbW5RoWԹ,L1%2ڌ^>p8Dtte/ϑ.˽o [kzY¿tTP]+t Ir'=׸qdz0aL8QKǯdtqqqqqYS:|IFM/ˑk,u9 w_,̞&-[з2p85iDmc" R-u;W.z\tK7)Y3:W?9}QboB6;'u..!qp82ԕ;uU_!{PAUZT o5'u..!NꨣWp8N\\KRp8'u..%Np8U%IݒA,Ip888sq/qRp8'u..%NAk"p8kԹ:?b2o޼p8'u..%@zn?|-ÆɠA/T72t@懶~kܹs<,xtu#F 2؍=R%t`,|p8y'u..%%K;lTGf̘!*,q\/"3 n#x@ڍcЭ)p8GIK~ɪ u1c [XypPvFaFr6De2h #G8_}L4Iz%[V>~y3{)eep8GIK~ɪ\~AVA9 u}R5kNp !!Q `]R2=leA0Op8N\\KV_*IOurK,k܏1J.C b:t^nQ7Uq履2p8IK~ɚ~P!nXvd_O,e<\+u\ZGZ iM>yн{Ю34\ Mc2l劑3fԩSG693+~qƍK۶m+*XR^M?: Vatujp8O4Kw}>~HxygyFg͚%>#.s>ur=w'vy "p[*#+$~/}an%#EMϓΕ.2wq}R褮KBP/Bun:x ˰aBaųvᇠO;֭L2i.]4!t\`DYHԱ0qWGq.^zҥKu|q[~{2׿{ t8p8%QQ_;yde_cO<}{ܧC>\g+0?)_P~>MI zx B~e Iߚٳ4.20r˻ᆱn@)%#Q=q8k+2$R^Hw)/)?MUEmjm'i}I g% / =@^ҿSnC6Vx9oނ@x~[BO?i\ڋy<>HZl;mt%,]l l~:GvIK-B'u~d`M!'__ȈaX}s$o VǬI OԤr-`%!Txy䑪t I|Y,|8,t/ 鎜zȋ<6dy6gvQ 6 m򙦥NԗI']N= mڴ210AGK?It:Gy8F}D"ACE'ӦIΝ"f{3G(3'<cΏ~%h\=D0r_園ptCM,!dwWVO^1hY n=`E/7RsȰ ϫ{Fwȼs}t#1/5BȟaKWK*N:uasTŞ:Cq`&ߺ[6AK[hQq'6%!Kfr:vdfuYGIO߾}O>HN`=@ׯV75CIX-i(+@VCH ҟ~j#,M76r$ X,-1V v8|WzL>ѷ[Ob$OW_'9Rh^t\YRkʳ2K,!tf7V^j@rPJOB)tR׮C0$Yz?e ,g"櫥/i :[6HV7>=ܳ. '>!x,O4|0[lKzK`ydɤp +X>r,PorI'@??>w@[9%CXIgk:cM#ZZ݈$??[zo}Yl1 =,vw<؃i`6ݽߗGYX$ecmF+O>ZF+R@gǰRs,dO*8,s{v|>c`bTKH] _t4lv4) v*5J1!u{eUK[HaFCwұD8 !}xR:߅Ҫէ2}zj!=h4]tQ)Ulp IvPJNrr$}yu km?Y,{&Y'W\qZB>H k4O&t\kyH(e$NgeD3>rI>h2!_}A8Cd f >#ÑϠo>0Vq!twZp ;^~]Ȋ^qs6~aiO[,UK ]CX ٻe#a%z띷|O 2m!,χLSVc2BY^2Y5J^}U=0=ož)ϡ]sc= bR4RנRɞ]RWoO:%u%{\$,BeK? ]2P`u:t:'_:qo!Xac`6dml^b1m/dHz޾H=:Ic`jnrN% 2uUXx6w V,3p8ڀ!u;nXtbXNjy;NTdInwm8.V`tͿ?t [|%BO{$]gce u;^|Ҡy v_^+ˑ=orR:IgۇivtIV( G + `Yqiq` ,])ҏ8KDt9LOăaqx4Yumj6ƽp81t_ ⰸ? Ozq.;q} ;\{}[eKh~?s4(?ex܅3vk >cjs@ZVW%uw<'!u\*%_R`=uekaRGBEξWItCb_b/wή qGky?ݡ'eZ\gh!gy6QGTtX}ɪ[LF VXp }ْ4HR:~¬ξ;qY@OM\xs`^!žiֳq_3?/cK]ɮWV!upԹ.=u+B_Gi~q}H>>A)5 :f3 qlW#RY[Y-N,@MX0ـe K/z{@8n2 Iq8|^?cn}g} ߮pqJX#Od+]F~֯ǺWU_KK[EP!!n?sN<qT'8=uK_N>7I} :/NR=_<4G7d~U,t$p>ddΏe[ :C;ۚu7mPN8}]>=,?`*[t< K˔SZ'8qψje8Y:6~a K嚎K8ܐIK:MieϹCAeD\B!!~6qxhOH`gHt2J?i:,u9ȭ.U'N\\CtRAgvBur˃$KNr8[SSRWӹpKyR%9K]bN]}$ ~gimRR7K1Yy6H[^dIrH!:SR*1ޔm,FKp8V;{ߤ:]_[ֈ%r/.N\\C A8g2s&1lqWR-!#p8 Au.D}Bu1":B%uAz&ݺuN:Jǎ}zeKޒ}m۶AO7=9K,#;u4`~)sRH]ƚ6{L3[曯o~#?^'L'cp#ɓ'v)p80KtF/K] ԱpBnNT c)%s^̙3p8ÑXFR˘ԱU%IrH!x$uzq%Ȫiײ$]K!D p8E-q%vhO]La: L ^[8 "c]\\V8% DjJNbS/+"kߔ9sqY 2sOҪէbAp8e2W7?FI]IΗ.ӏR/[UZ_&3qR2cTAX:p8(pTXnsrRb)&u#N\\~&>cJne Hp8Qx೽emvx.ϑ]/U,\RH "wN\\CΛ/!qx%%Ɋ"Kg>7.(iK{5K#u;>;S.MLԹ,ޓӧ˸qdz0a~{3]\j;I&N+u78TrOݮ.Ӟ4CҤ.MԹ,̞&-[з2cO]Pp8Y}K"fk jԕ4=W-u%;|O]L*%!q1ɝ:Bp8cPAtOgU2i{[RWRE13˝:'up8* u[';_].>H]^,FeIp8o,8m9v08')333iRnooes6 !df!0c;hm9g5j%KdKy;;>RK]L5&Bάv;uNgEyt8pt2s:.xp8c`:hs:@~p8(^u&\9m~R:p8&\9muU__« aa۶m$O:һs8@QgtQt{BmݺUڱ6< +V, ˗/ +W.oKxpEXjIUgJ~[4_( ?p86EٵѢKa*jkW܉it u[-֪ \nML '@sp8ul]9m~ ^ K,}V) mftCXgXՑ#[3cs8GW=`n;2ټƜ W4Yz | t8:5hn7}N qi0_?q~ 9=Q#eqy~eJ>N :}'D D-^P|-*ܚ#ظMaٲ%݊աF?w83#?Yۢ??ZX^Hu.ȧEEn ȣ^y}q_3 / XE'g峛7z!ꔯHX9ʗE QgǦM¢E QPˋ<pdzu2\[և~:˿p8Ǟ@^<7ď8do};i~-Y7ˌ4Ն}.ny:_{y|=bXq8+ϐ_xa̍uF˛8mdI̙3_.p /p_hƻ[uq[򗿄z( m簸moVZn?9L4)lg{Nß.e ]rq8뜳ٳ4Nܷ6Gc5\9p]{N7]vrܹ_԰8/.ԱD؝lT!wt%":s7˙3?5&òet|dzoWP>ma?f_=PLh4G>uۤFX(skal=`>}qvqqv>< ϧzJE3<~:Xc8` p[ݹ&fG8m(ϮB{3ña]~-7tSxG¹瞫?o,X[S㵍n!;Vei-tԛ9tĉ1ۑk[zuذaC7ި}[m 5۶s;7^L}b}@#>8GY?͟?_R*9Us,u/o3un9: &K^Ν-gAPplLL&&b>sqlP@ՅomկjZ8p;"&O/!}Ç?a=m_!pʋAabeEmq}c\&VTT/q1AL+׎q|s>nW^a,,:Gg,u1mƬaʴcnU[o/ʼn8 ukqRȏ'x,%dڌ7$/NbX}w$^wlnp¬̯Xyv[,YB v K/ ?H}v\%b\ > 1_ǝ!q_ գbM_rV2'T^%"NN: .TiS8KI2Ps=QsZw}7]bm&f̚Z"*s"3PK:(moS!r0`h>>φ8 y?i0C}{U?|߯b_~󟇣>ZEه>!mn׿e!B;0&ξ}|3u\z?a1bDٳg>|x5OKZD^:fJv>`HN;-xaO׶bhpQGAiя~uwG mʯC W\qk׈rؚW_yӦM z,} h`d>u6WM9#̙7W7G-`o9sSXx Àea/nD?e\ƌ^ذ^m623]wu1"x…_nfos͖0al<1gPgv\uU /W0:YӸc+%ĠfuXWg̘no[Xxq6}>UYאyE ÿ:I1Q~͋RW}ckn/|Aӭ/f z!>OeN8A?2X;H}cX7 L8#w]o}k6b0[ um'oRiv>Xdx_d /~ X/A(R7 o,2ȇ w}.:RBC 5,o~K&+˷ =X9:ԅVD@"i ᓷЌu\,9,{V06"ݽ8S5z44c,~ljS?>,\YYb~$a•W^ ̀pDe,hXNM00Icԑ8V&cTUUlD}tBi+o$:/me`a/ѣe&ĥl,O^B>p,wީnKur$Bp8]o+R8˳Uxg‹xq Txg Qc&a?sY睫BKѼ8s]ˢmÙg:~H$=& Ge(0Yt[,.l9rd~pyWv*3p<Q>z{z"NF''ݎDr'5GuNgE)7f͐٭E=bNإ}kXLaa`5jΜ9*؞C(p%*xپȖC@L\<'YzPm?Tp@Vo¤~6Q>,CUvPZ%+ħjKyl-u9@ҏl~.]*;um<ҚGEN'QOzpt xuqlAhc2c*nR xⱖ_t٥kԋ{?qe?0[nl#DcO<[FَɋAXm,08q$}!Bln]k{V_i¸7]-Xigh{9R;S`ԙ3t,s5; 헩<+:"!BRNο fy3,slSf#/&BV4X*j{/JIђ0b'b&Y  ׈deeeoC˟s"/䒬8*H[VO9R݀<1ʍp8]& :FFI|vİ * 1",n ۹ &%Ï[%ci_4c(J.r]'cq4nD0R+>N-CRq00%> M͗][jUP|7f0OD1md'^}_/Ua:8(p8 fl ^~b7sc`g~ [$8 ?SִuۓaK ?1O#m|?_gJV?4\.Rul..U1L۬}{h ~P^rv_Zҙ[p߼mk^3Q׀DԙNCPuM"TŢo)cQ;tLSaCM?($olo,}-u0~nWάJ1^A F5[vSNQ`Z>[x& "$/nlD1uMlDPw]}ê0 /"m=D}3yb&>+mM_ !mتj[6CLX)_kElĂFZKGHדּ|7~&ITPrɖU^ n5J2bQg `CH=:1fM;6Ysf'ds P̝|풿9L1]u5"lnğKY2^[6g(y&r3h!o uǕWX{w-Ctc,ג4:i1^'{W40KnAr]x%[^?{2o3u~2

݈Q,rL>ε1`CzQ.=LX;Gg1x3q}WԒeKõ_, \2عa|[^698-- o9Z"0Sq:10sրgLCai9MxĂ>c&m8Ű{{`y^kV^NXx(={C'\g%NuG~8Tl9U7ݱ__&Vt]9mu`\.)+[bXML{ %Q3hW0qs/mOt o,FHydzs[&9M V&Z5X<m!y2oV8N>\|0'9gZ1vE>nq~(pj~>c95Wl7dOho:#o=poG'|jv'ax[k(guGsQtvN$Ob?uYb:zdxv%l< p/0ıʈ͋dPVv$,GX PO(a7>7X?Ű|,^|] q8GgB~͘X8V4^$y+c7l3W{!FV>T"*ݡk.pO:y#}'  h-2-25?@9ѕ`"*ubr ˢ0`"䏖'G7y2LxZ]0aaiΠ?ߧ n`}m'L$sE=;YWQ|]sq }O)aϱ?tU(9E]Y"l Q]BD\TйshQg/ aam E,&p8D**~{MԕX\9%/" .lNM<1#߰zgp8MQW^RA.vcgkիWk׆gyFt\)efꌼ|㺺s8 s:HD2ںuܶg4p8mDWu(EәEyp4wG=p8ñO3:v's: ',u %oj%:;Nsp4鴢΄:6Ə#M'9p8as:7nzQ-u=5p8щE]3&϶a>>:- -u"p8%a]"F|0Tiw";=T%E(9KNt:;Ggں~[P~GCKD1Q7v~w֬YߑKkºuNo+Yt:ʢq| χAoWK]9u~-uNgg$oB>GWDgp8{EcަYP_C-uG~8u#>:rOE1p83u5XDUiuNgg# w߯8hع>Մʇ@~YuE)U#Dܹs:;ɇpaWݎ;ڧ\p8GKP=^G}>Tyԕ)uNggcG|FºmaGvQo_p8=OEK]"`YI Έ>BE]Yu[m 5[dѢng}#; wY턵pzp8سuZ~9tJ?>^1bE;\cg}ߡGηm$RxwCܰMu8pYs:;Z!׶3,$n}C@mC. [kTԹp8coLShQج’qW&Lz30߇ۮ$zEa<ŽI<_Sp8= uNgbGI7_GX1qcʹ7oPyug5ap5gP\YWe~p8E٥ѢnMgsn L,%]f97,0?3W-K2p8"Q7.Ζ7Î#J40߆]za3Vĝp]_c "q_VWX1JI0_i7>LrS2pRiGR'p8@;lhִD!Lw"sY ǜG[ n<3,zE30 ²)WmϽ6,~r鴛2.zcƕO,^? ,)u p8R$wyQWQuJ0W3]Y"NУQGr~l(+jj[^rQܕ3?DYTD!;#TPyוNNFD]HQjE)ʼD]ru#T:EsbK,u6Hp J'%ߠnU5wa- SodKoSn&Lau'߇6L0}̹a ǝ_θN03q;^[za^n:љ 㹖9x?6o۶$N.yďyQtz  GuB 5{}tuɻxp;BoQQW}D}QDgB%cYVj;JWqnN}/w^&_SkK]pu"D(wQwQx3HD9$ժcLQmxexJ8߄I!}Z榈H:p5 ?+L9"q2ͱ)yԕ%*M'CȏNGAޗBWq\*ҝPqQ""Vqb"F}?Tq&أC)OM+=7a3چ)Q!)6?"lްGŽu 럾OSG=|WC3ú{WIw/F)>*y+JmnmDAE{,/ێ_^NX\^2?Gc 3Xu _h40q/nZr;Bτ^}&T/{sFݲoNZ[)O4=A>S*OMGYW~E3|Y*Nn Cʏt>S3(=n.J7JZĝQp0/vJd91ul۲EsW{ I(8j&A q\6>4l^7?.p8?}{.-GFgNpϙ778Dcxme1b@X|7 6Vf s޹ᅗ9qz Kt̿=p;-s Ͻg\oj?S왺+ M^Ry|*N>Gdzu%ש3bKٌlaN *zs[ܕQ7G={D0QW_'pNxdɤSO!fBv-uko7ᨣR}({N Vگ׾1–7 &O('mMꪫ'*++GH|8/ 4~aÆ0rp5I[ ٺ׾V8nذAEt~aȐ!?&ZO~S0~-u}j]7imO~F>z(Xb[/~1 0@vgh6)!/;,tAg?Y"qhbp8 lf|87?;/OC$|~uk;NW%aH=/_!&3fP7?^d6_*?|`{(m8묳!3r)*hptW0s!4Ǟx\x t.K/ /I[1[cTnZKpť,~V=Y+\xE*?!qQ7Nbz3I^nEWgE\GYєiSÄ)<,N_ /픏>u,y'>j5 Nʵ{b-{T5\џ*}%3 /&ߦ3A1Rl-y~|$]<;߹=)Շ7֮#Yq{xfma[gVLcQ)zQ l{r_9G4KW޽u)}e<ol"-GyDL,lJj7DP PCZJI˃Ӗmc$m9o~u·*mA DYaUÚi`'=y<7nX_vy>-cDy"& n|FͯA%b7]:(0pn Ay`1QsFsx!ƛo K/ڼj`rΟk+<-W7`C,xNuQtv)3أzh:IO><..]:#~i_#&[8vN207V;CDU.׬atL]^k헏(y03M* n.+Dj^<-"e| @<y3p$.y3zիb!hC^Vȅ  VDfaI&d,Hbc*^lA";#9B8@6XؚJbbCspbQpt8 b7c='(,lv,1X8gK:q,ߩzГ:]9]-Šja1Mь{澙u1[ S?ip&E 6mmq͚d>9}c$/3~#v!xˆbK⩧ 5+V뮻N?A'Yk=Ax~rf[3+y&E٥ؒ0Y0pDM᫣iQ*bQ{|w4HE4uL$:H搟4M J vتy؄}#,*ؤ5? Q=[+X;);_'(8`L%@YŎ v;GwC<62X/d,Z`^aٱ1M!~FeMqiis#tו>x)Ac| 7A\rI݄K⻨s:["@ShFԭ{pNxm[jR&^]g ZCul\0릳-u6X1;1:Цm쭗q> dqM~? >2CȗGzK畇'x~v/yKcyCZzC8h`YM\e!T ᧅ ǜ:(2u*v'%NqyA]9m`KE`_dY3ه憧VD> wTuk$lw4w.E X "bԨQ_Wv~O9G _bcXH8k*^<~ՃsOOX\b`ukv5~͗p8rlNjnQucQ샋X C}ci8->a㢼H .+A~'۳cq~q\]ctvޔe /.D0qL5 Xĉ7Yp8..]:[a,ma!^5eh7e"f|ND,bb/&@\n/E!c72>yaG}(X|^>s+'vy7UWp8 uNgck_eyHm3>@ ~C|guLѕQǑsp=RxGxa+XÀY%p8uNgc{z,45ԾĂ7Gݔ{cxx z|dŇf]#k]ts4r){Ta%2V*p[p8\9]+Q_XI>)䟺aၙ*} 9hnxB5|bU"V͹:,zSYZ\~ ?Ή;pYs:wW%ng"ښ-U[ n>S9NsNx˙)7^nF0eaiaMZ$bf?#p8cEӹXtO/j%T a5ឥк',[\|ኰ|lޒta{V,  X^znTI%T'9p8v uN^`=r۹=OpgY6v 5;6 5ƭ–͡fn 5[jwԅ7l[ojPa:p8ZuN^`=r˶cg@HHM<,p8%pQtݓ-n(EKX[Ca}At,>9ݓNsߠh .ν{ۖ:}p8юpQtݓ-^u;t8p#\9{Ed˹Ep8EӹXtO{DQ{kt:GCgut :D<>uRQגEs~tv"ƌ%aaݺk֬ k׮m,ʳ5,sON=Ǣkt:]hLݗX']EmjO> eQ\ȏ_AP}tǝ"u*N :%"AW"ܚv.6f0~kbm,FQΖp(-美>,QQkCհs:ox>~ء#O>H^Dp8GgC&bSꨏv%uNgg/ رCdX}96]B܉z#*٧|Z>q9:hEsgK K]o?ʑ_VQW5.F_Npgjuap8PvK,u#?zDEuNgg$:_b?S.2'Z1Yp8Nd=SK}/#ӷ_s:;oҐ?oqMqΰMuam?3Zp8h_$!o헕#>N f:SqÝwޞ/E̥Z˝pKě m۶I>_QTyhp8vFLe/J/^^*ꄈ^'Tǝw9?kw-lvG|6P>Qlj;"쾙GǫF!p; s:uϹ*50߿ӮMq0_767"9uwKEQ|HZgh:}s ;p8cN^q! G T8">Y*EUfeBWqDJ 7uNgE݌[ ˦^Vͼ:3p̽%7[’I f\gjcvvMooɬ_^IM&Bx:p8ZX ~o8=|D Z*R:PuCՉPagbNR܉XDS1Kl;ZMway]zI30kErXc. ]N",pM:暰sKBغ9kIJvnu~9^Xp8Cd`t3H<á#>ʏT"9]-vU'|K_V,olÌE]]9Ύuo %50xNq~^9aŜI[0q̯Gl^F{9k1xYT4bN@yp8J*}8TDZw#>*r&G}GAؙSKѢnʍPQ7 TqDͿ|z깊c/ fVVLV͸.זpɴk K&_Okz˥!Qp8D"*>T y_:ݡ|GCaQtMDaD؍<-T9t;&X$.6E60un-g0?9w]0?9$l167=;,xUgJVޤ ;p8,^kا%hj-k>D:mAԕ<,yPsyA ?|ˎOo&[2c!7ʷ_:=!=?L0s̛ n<[ Ɯnugn-IםI)'pn7F g7\(% 6 Tp8G'DظqMxŢ mmm;|AĂ uC>*_(dJY>&NX1PqWCqX N;Ψ.~(®穡GG0A?fs,^sq/3&;τ#YYZX]رs{dNc7Aquz”k~fxV|*7߆; ~9U. ?3LsA2Zc/v?ߘDܝ|]^:] ]>Wgh)㺷;"fQrMJwdnձՊ(·Eu+bQ$*](,D`'tfS~1T0}Zxޮ^{Fߩ QPPOaڰiP텰ysMܲek&i蚵#nF˟ ֿ-叛%떿u=^]/+&_0UݤkE*Lwaտ ]aU^{tiشCaCڇ-k />*lx꾰+Z>(cw'Ə^x֖ 팉q0y/caGvgnhy8^[_۷?򿅛;nyXz |z7eyu"<xq?Xޒ:Ywaq|@ˆocq̿~VWGs}`5wO Q+ 8^GI=U?O5b_T?h[e=Zաn88鱾"b- oO?gq40+=ώvx)XB+뛯KVEz;C>q# ۺ7y';$qǬV+ Ѭ (#bO2"ze[Q=pgM^*}(?| THеPUUvS]# .Yԕe-УУ~˶Pccxi갵fCf ٩Xt:w>&,tB咻񗝲N5O+{V- ˗-w͟V\:ƒwE ˖.7g~;L}^X4oa +_sy0冿_`QX|NʰfŬ0ƿi7\&]siœ1W\6>rwx9fz{-qN.=\p^Xhk 1Jms93}Np{qCߍP^#º,ܸL/X~K^y?|~;i>˖. ? e=zSO91 ݫ‡>^͏||EYZfGKm7/}a̩G?0izS}H(p7wM_>^͖ Gְ!׮%חq̙==<=%yvFY(<]yru{tJKw+wGhīyt°p\%JQ/޷~/GoKC}|"XziZO]&aw.]N5]h^X !kvHr}W]!e.{\_ҕ%$)xG I~dcԟxԍr9'/E K/ KYSBM -_~ӟg?Yn3Vt}_uO?{Ng%oS&Mn$ߵ ܷr!鲼gxᢰ|˖/3lX1e7`8áZ"꾤3v#H,v^0N~"F˪ByW^*{$nX*X↰Y5rsͦ.-NM[䏪”iSokm0wN(uLʲ[m#lV޻Hعa7]&^}g|i70g<2=<~[O#ǧu`/7H%#4-疗֣lƫ;2 |X.mH@1MQ ?ae)^NK\+CϥE|^,I|)Υq(s,6tYZ7qk<ŭq8֖/9u!^Ed r>*=2T9F4l_)Oesyv_E?iā.=V?>xY>R_%-Բ?#$\/aӸifhZn%DwK_벋i}$O[z?>aH>뱲?J~x'K^QRrN\MۋK$>>TژQW:աzPRw̓~i~Cq4l~su篯ueiyK=?kCI:V*zze_4U*ows5ny_GGWjc$q)fuHӒ7i-e$eK]_,tKx^_ V?G~,SoaP߉GrSB_|%8i<(S*F\tX&T-W{h hWYe2.1> $/_7/S6)amԶj^*{M7ޠč^?yIS*}g2jI[17Ԑ+2wެG~/i{_?-jЛ%7H.׺2~3tK9t^T%Juj]6=叶{u W&#CG]-T1)sQ&,eQ#i Q- L&=z 7L 1D_+qE# h,XcCB%elͤD0YKFz'oRy,ˤb.JYT(/#/mGګǹI#`bуk2)Q:걜'NrIyӣ !DX# .w[tNUioZk*~6Q빔g(ґ^Ι5ʹ3-_ȹ?<-~&!eiӶ i_^F#}%*+ X ngeLD(l59iIHkܞ"ɜ[ˆOZ=Iǹ֩`\e!N_e0p/KO97=j{%յ5t/>`AO+,줯I^qJkMCKWEa\ #uJ1W_?w軓UrȢIGXI\IG$rH'k:(kO:p.i{}GR'OO?=IzuxZqNWk~iy}yWRɟ~]uJEi$/c8MyAo?)A= 8^?:,Ur;d$3J. R%sRNA5.Oևz>6J=;yHF;#ɧBăgyh:m(y?LDͱϰwiߞɇsjxo.I^Ҿߦt>@ޤR³zYm%Ӳ]Cm" !. ICR/==i?eK=pSGiIgIK[i~?Ƨ-I<(7?H-dp-I7D)v[?CI>?"p94K\DzOEŁF 7ߠ׆Gʘ5P~/#4r,/;쭡0{뇼5T*|H% K܃Zyߣdg8.e qXu-|G#IR0憽3Г&ݡqPyaK] E]/qW>2a"v&@hK=Y(ȂE,jzʤ)7V>j}eqѥ) ;#֖(eY?Yw {d!/w aɢaz^5Epcm ~T%M )8:Y0EmU¯ϩyR% ʞ,EU91'rx7V"(d!_-*gA*GYh+a,.^zsV{KXO%_iX6=,mV/Ⲹ "MGVI_,OL+#1 QHZ^r.T-unkBGDp5\z HNz/ͯ uX'yq4jIv&kGH>VNq/rOaE#c_6dA?_ev/e\(rqn8H" #,-rnGx0fnK?='+CfyAʍҦ-ITs[Z8s˵kB%bU=UCD,B!@h8scW, Gi|_~pT6X*Yԑ`)ǎOZCoy?YX4t%Լ8%6NM/'!i8bYBX*ağIYF$|uJq^R~#⏘*K7"2dJ~2ȓxVs4h,1NoFWTi]Ŵ䜅.x# IY?Շاݼv^7\5:餌ʡ"z Io|SeZFIvIa" 0IA@4Hq# z%n0<sKˇRD;hw'-EӞ%6(u?PNI<+iyiՇ`/mHϽxyk腀Yu\?9<'qEhV1(jį PP)7?v x7Pyݡf!eC$ Xy5XʠlNMSyCaSQW}gD:Q k}2QXN~E_vQ[u}D3%aWK&jY6^2ueuERQ7;}Kn9<6(KI"L/M[!+lw Ʉ"%#0>!U "_/Xcvđ ~‚KZHc^L/A!07"{>{#y}* &L&uY>Q-X0Q<:jV/!*'d/OX߫(}'Ha;+7͇BN` ;[yI9zL2Y;KYZk.md"/Ęnes~p/Մ/qCܖ.Ώ9q`\a/, nG ? _ܣ|4Msq,HXsVJ~H' Yh .,3XxgV<%??-4IY~#YM\LALHqr9 0M'6Aq|kU qHkypN|Va/chٸ KS7Y?IK!r,KgEjaie,,p$}9aI~%>d!:LDXMHKofƧ4~48^ I7J(CO<?_=Lrsi7MҎ4cP,퓅=~)5mOӤ[y&~&5\Hڄ%*['`p}s q*7Twe-L꿿O n$,rdi5 AR'~wVizˏJ*>&&mCk{=-L&"H!RǃC#}U"刟Z3+I#^KKo s~kRz:I]hԿrED~@ҿߑKp%׬jEP17]"(bH|\OD}U@n8HȄY앲D.8 RE3,h#L(νI;lGfXtO:ۏ*cZgGƹR%Ј_aW_q,v/W.M> gaEuK.eQ"LH9Q77ClTOw8yƢ(5*d*:YG昺4,e]$bK׮I dطEG%›dQ]i;h`I_Z]b|E>/WZT`@E, NL}T7DM|U;k[Dq% Oܩ 7AXzلNllJ7"AEEDF6 gcPO۲E`//Z" KUGSX(噚݉ȳŦl g`?}^KBtMXW XH8tYbMň"@tÒ xeOkvADF2x|KRg)n'Cp5mL[D9ؔ;s9A v+܆ caט<߇hE Ē`lX,E7,fNgKY4&,|/oKN^uSVtzo1*uG6!E "¾5,^ t,Tm/HOG}bd!, RD.~kYr-#XS JC""xE> ~[^'KUR%8=fV,~PDȬ60Q'BQW9Ҷ&P`K-sf ! QWݲH=# ]12cт+qWqFbK3,;g&HD ;ޠrp?pEB.,M63²82߾Ƽʳ(B,2aok RQ+.[ vNEbW<,1a؊9jh,:$%?E,YwSKM,J@UaiXW0s>HMM] *FX/%mB5c!rwCU+U2i+T5)"׌k Ĝ}D] `g@*bѢ3qu6;;N ߮+fɽpF}ׇ}M%Nɹ1TTET0^GRʾL}E7GELf$ g΍:oIE{ޯY A7Y@,ɫ Rۜ}M%~W~L?`Y$:S=ek$6;D*uVjW)91ëؙ1q u%n7ۑ6EW~W>l9./2WKQ9s71g8Qק}_xH̋gRn&v{َ>'Gcj +A ֞ddKX9t"ϥĂ8B" /v#xjů-7"aV 8f) kO$&] ڴ'iBO_IJĒtE,4fq :y]*,=XĿ˓ ;9>RtZ䘊:#" دsRu;Y`%;-TbL5%ꎈry&Dn)E^CVB1f|J߫)Zw'7g2gE]bE rl`+:\~ؖVH!vML#r;~Zxx6rn̟,XO[bE![tGC, +g7efj+34c~7 N91])c+NKhb0W~W8*땊j ]JkQ ʑ_w+c>ތEU3"F2J"WQ".fpk^u.u"2}+߿E6* }Ae{ϹhQ7 }M 27J5OV gA֟LLf>[tAX bҷM|mQ[j`[fʂ4]W ʜjőeZKkb*%;,;?\Aueuf,YցLEUq1trmU}.u%.#Ovub^5<}-vζ"^*Oݙ&\uȿD/4+3'\o߾}I4u!"da"B_SAP$vG( cQ,]+% TVȪC>,B@+ZߚyGz>#vXTб 1'db^u9Җ6!hDp$"%+Xy!0yԗBѧ+3aRwt0t 9F.f3nW̋̄]EPwc^Yr/:e1DW^ EY1@po"?& .!޷OC>GX6XK=w,Y_ki"^R-ۏ߶o5X8¢4])km>4Y|,fqKZCKߐOTPQ-=1 MC"NR,E?ixQ.ʹrqfVFsKֵ-PS16{e.sF¡H8ufMyb;u%hN܈.]$6Y9ֲHh-/[Ŝ'[$viE&MeֺĒSh.L5[yAW]ʢÄE~M GeLuyԤi;#AǢZ7-˶1E]B:~4*lG/(qW# `%Kx[+B)b꘯+~ uÓ&ʽ#؋QQQr~.ڑ+v> 겭{M09[fD]#RA=3WV9m%l y᥂1_N[K?4h.Y7! b63T-dR/@IJ5_ W#vj1U$[*!K &Mn/EQ[; t,t<_U~ ~gx?¦ Sۑ :X!`bUu;EB]i퍐@X@!*F>Э^IpKvKݞu&by](P?^bO)현QF1bf 2< @_`N%I{9RBAo|. *wf,OS"Qg=C㿭V;F/Y)LrE]'#bĘ| /XtQk6%&tEXtCreation TimeTue 27 Aug 2024 17:52:27 IDATx]yMU~ֽiBD) 7Iy<[o9"H4IT mhq>^W:w}]Ϛȋ|EJ 3BL>-];_`8UB4UNio!uqU5"ڸ'{l 0?qTyR\'T$ o6ᄿT`AxA%fhw!^sW(d%w8F)PИ5ndʇA2x-FAEf2,RX*"H9x2(BQ6Pʑ4j%D̫C>J y 3Bxթ`xrRCZ]]/JY!2!uRq(R0PweXy pMgaq[,?n0dAegʒ*a $$[K-CZ!ZtMRs`G yuߺQi zE*ٗ*,)K{@0;fXB{ǠBzeFvoF%!#䏋JH@aսJkI4Vg=8FZ2qjͿ̎Kcq#&r+ATyX零(%iN ~ 'X²A"'4h(U.sQ:0(Ry9#;(3@-ka`+FE%rK(͠޹u F2,OCĘ}B+)ʐ.biTNId\\f(Ǐ]-DBR(,(nT@=֩r(22u*c(WƱ:q~e_9K\'$L#Y+ɕcD@DՇmZ6ޑ 7R8H3E •U^ay GCoclCѦnD=5BPH!%!VEy?f%Lod+WsUG&`W1)adq6a&MBoA ն Q.J}) *{q`'<'̲V~fZdVA)I}2 k@nDK,6 )KEp6](AlRQܻ\TXFҚY\M\E9fQ)ٜxRERzR9)`8@u["p$P01rŕTTᆓ1pnӈ/b߫wI'iҙ71@D>3B6P:p܈#[t䢈z>L{'Wa#<*T)Bْp[- ۘ AIZ4 "1lُP< -M |Vm+L46 Yi[PɅlfjteMG±.R w.BsM7мSX9•Baӫʒ(PqսvK32F2 Vbf > !)*u iᡶ$EEăpNn8:dY (,"*2xTΑH1k ~}*jP94,+ړh8Q5ںϦy9F" br\5咬 U~aP?2G3W bjD!t4l@R%YW1hWȆ4KalĥL mXkF+⹌H4 b'huCR`;JvܾUek7ɨ+RZSsɤBg[d2HOWb҄1`ޛX|)@6j]cQ'a;Fn;aqJ%Dž' 9قe=<<9^yc]pIðc  I c X\7rN*VGQA?TWsA(.)En٠p G)!A jY}!&#J*eBYJr I]i6{ݶ)gUL`$+'l-!`*䍎cyA"CА$3fɯ,aSNxa1eRrX SNw1 \PGxHaXuUY!ܳ @rpO UH8ꝜT'CJ2ūTgA:,ўjj(I.K% r\ĶLu!6gW[O {\lSFؙ&U a*Ўs,qj8A\Yvcwt-Lvydw+3M-q[8~_#Ѫ(.)~ys_G?6;t0E*:aa& ,_f08׎|]$/>5ϼ(ߵhm*$,9f[դ.z2{jE4:*041)[9mK`2@șKR8!mbGD >$zAj6SmeNT l\Ur\pv̚~< i&0$t0C%Uyj~FNL {ʒ730TKU%U?KFя2g~tbp"[P\r:N?r2D'Y%\[s$Xĸ\l(ܬʤ qXFIκR0 i 9R0vztPKbTV$bNK@yenAzum1YhdHqŧM;n7_~C~pٗfM\&7 ~lcG~xdHZ yAヘ?z[ )-v{ \sgSŷ_J)Zj!G^p!߰?/vov|Y?:'o8ăѠacƎ/u茯wK=JI{ng^#O<3 k.)ǦҳxɇoQTTNvw!hаwüƏ3^x ZiS.u(g/Kw>8 |8jשN?cn|)˗SZAYצ?݋eK~V۴S+0xQ^L7Kخ87AbJ^0`ȠE={fM^} cO=Kıp)hѪ @ai xh4Oxxr 80ZwpJJQACm\p-⤳.Bm[f[E 1|YZ۸?mθ(& 6)ї'%4x+: `wFeg0- V3Ɂ7AqCRb]Ak):W Jܠ5| JÓ6~44 rxT4+; vQQHLzEF8CTt F=4/@tϦxjI%7\ءf2:Q 6AA~n"IxKiAxT/9BRoT]]8*#p #+Vl51 pCBt<)}9`ʴcbxo*y|y'ɞpv'A+kTS&Oc4$rprO%8R~ܪƮ[ND\{sc j6 P?a*,i#$!7`̚Yӟ;o.{C]yخm{CQG`{vb>69\\S~;tꊋ5Y Tzphm+\}hN㬡bw 2%piz|BlӢjԨ P`lVQTk]Ccb|pn(:u&&6EE۵m'G/3amhv:Q]v tw7xhgpiNWӞKt˭PQQOA O=\EEX|)V,ǝq`Ų%y[Atv68kqg41=u"'=: c;~qBp oYd1Ç'c98. L6Vk '!%I,9;n\ein=F|TU&˂IrYwDxr$P_6){B1|]dw@M H0M%b ABHaJ1 m y*m}^bEu+xOU@9e &Ey–Hp;\H?Y'PdG(SaޚpaS (/;|0@eAٝ0 ;SPD6D[R^T4pudp+"$sVtK(IJ=k䗉054@.-bU܊TCn"H驠h˨Z׎FnTA$F\ntKl$- ũOۂv i6kc `gZfO!+ĥH^S9prl-B pͭ-&5#AfGQI -|c<9jpͣqm{~#p!}̣rF%7h86o_YXt1~y͹tZ?^=zEEE9JKףt=}=7_u?u[_֭źV?/;kVaCOZ˖ {=jR\$z^N]t ~Z++(y~=vDHQ֩kS,O>|g8͗ B+|m̘}-g]z6KϊH(͓H?]hԘ'͵rWyy@fנ]4rnupyCrV4$OWqnRxGk=J7Qfn&MѢU|߁X ߮1$f-BXk-QPexfԨQw<8 9cuti`mZ`u7X13eoZ 5AP/N}Sz8N"4ͷsgDyyyr,lӢf5Br6V6|Xx{E.yoIDqq1<9':u.ݡKwn-?? 5kmm c grJLll[ݬ>|w.FgHvė.EK`VbSC@T2΂a G,Dpc6h j)ݘD$a^WiwҹO"%+qIg4U}E1a#~r{+PZVe(--˟S^e(-+GiY9+P^c!Gsm_!HչO[YĖϬ 3Lo MSWR T/ f-x8Qc LSVXd ݑ7,[K9 B<H8oJY*w:?\*3Nʫb̖߰g1D)61#%kl Lkdy!w.WƲ:nc3?VD¿0jBW ̺/ .)ZLS9BJ+R~arsQzfY^jxrӔڡt=n}}g]DppgмekԨ˖/`%8-UIQRRɏޛn}cbGה"~°q%3AGüfaGpYIϼ7\v6 :Xr9L{I,\v]V oxqغEKguӞ{"3yMk].xмek voySlU38Qt=mZB&MضUkԬY Ϝ[5kO- ?@;+B1pȑXxXG,N՗EYz4h;tꊋsޤ8ᨨ(W=ʛG֫/6]#.lݼ%λ 8bѫo1xrhx `wU܇ȑ,L;;v cdܣ7>=kyǮ՗C]DjԪ+oo^: uC0`Ixqӕg_Նw N{ozZkoǤcjRb% JcEWbp- ߟ'tu&u jPjrv]I,V9s md+w>&S= \Qf2szP<3ű_K5^q'(G`ѐHґ@Vb@bnE5n䫞ݐ %C $Pz&HDˠP*si5z,YYIԶ.U ha79l H'JxK*S`H6E)30$j06l)KhB>TOPQq$  vmHxj|-gs'\_aa;~bf62ڣ(]{'Ip;ɍ2j@R Lnv8fN}ȺK2\D>E+UlPMaOTm{O|0PQ"Cޤ!sTנ/RL{'>X5 mS al-)gvrg*rm{"Cc/ -g YBrwE"eavn׳WD(Dg?G$3tmIX*o9q!Y cP̞K~*Yc#eG Ic9QCNu="6DFVS;*lAKf-F؏+}c ZX55E9x~|#*a=/ ,+Tב܆vփK' ɲfN矜CQQvǝq!u׈mad8kaߙ_kmbHIB'cןBmpS5%)-+ǎifyم :Ѽ _?-9$$,uU&ji`*wU!|uq~؉j<0>[*sNY>[jHrkZڔ(rھ_i&T|W&[9`k A=4-_|cQoW ġQK0aƏ8uXԥ}ީ3Zf^k; AuK@?wbD>w\Tˆ, `bwU(dcB?FV\lK[IJ9 ЅI}%z d`fzjHC@nf:,7OAȺWLn2.؀l^[z01pjƫLHJ0lu2*8IE3$%49 TdBʽʍ ĦxӁhQ͗Ħ22tj܅gؗѬs<c= 7S!E+BEQ*߁$LkU.$EZ\ծ펥1tF?|C &Y9ս3N&4{02n&Usx d!`N9mm@VlԅDcKKڧZR8Tnň7xl=B$ΔVQTZ- e"ǿWо60QݢIdIAe`5% U2 6bQ5;i^@,P2Ȣp3JS+<`E#Ia0'%-!WOE8ɫ<C d*"pgAZy礌[.Cjm&)wD$`}w4oG)ʣk!^A^!%.9E:RB!gO4$z{Q4FmuzEVu3;5ԕ<\zzj 9V109L੄lsIU1.vRXUZ(J&]ۨ%]z蕆giqqKN"pḕhEk!,>%%{Tɓ#^reSc` y4u$274* t(ZI߫. D&amEʾ+cد`;K}@i8ҬR3 OolވCxWgPvJgq!*֖j1dSY?*&9RFNŁ6M. `7ab4p/r4e)G=嫒@"4Sj?&,oZ ꬺڞsdg>,; |*DP! ncUe{Ę aaw^(p[6M6m kgA P NA]sYjǗ**0I0Att|6a06(eGĞYL) y])dQ 2^=rR܈bO g$LŐ*X8ki|րhY! H159M7ErlGOd+CE _y'= @H%, \%U~EB,q2/SGvf2U4NBw%W8DG*#2NEybqvk]GTB0ۂQ{Mp+v7{f<%rY[ %h' ŢnVFU@YCU՛Vt3BEK"̩봕yn) tX6( ێTU[iFG<4>Zp(e)bV˾i|q4aӛsCs4W$N])PO,G?JT o)r}*5r Y$uqYy"ߊ&-BE,Vp<,u<L1tlPFy CгRo1j,uHR uY u7j-TO:Y4M=TbC m.;V&s#寞&{mSP$O9GĪYL[lqkL*m [3VEI648UVq ޵]Q5x粂/u)(sSى&iky)pp/p] LyLgf">½Zer7B+/5CRaq8b`l0Q=W)ݨ3#zԆ _\$f#~;mz}f,=DDl "iT(M{Q-%ljIXo}h;X'xr* aRaOg2J2T.#eŸNBF$+TM 2IΏ$ME`[tSnjsYؓ7/+qA(dF:1iENEΉ5:+!̀*Aavb]yzRيyN/`n -If%e) l4Ve3 IDATxҀ\D0,\:g]q 0 6 ,u]:RDW fVRS RtMo. $rm<+w#=kWބe$I'; GC1t8TS=+Jyohg=qc!V5Y.D^^A\2A&NZ5V|41t+pq`Q%J7ER .DHQ>U;Vĺ2>%ϕ}%@#e6YULV2]5d8Ts FyiYU;7BeWFӐhfT -Qx$tZ ?tHKdD:.Ӎv)izsh@ͼb6A(I!{`=8EQW6A8e"7 hM2j8aϳơ7J@ˠ]ЪI`#- % t!nB$J72Hm$ǸɄRĵTJAqY@Z 1Ɔ@yÑ*P'J $Iy1S r4u^"ŋ!z(xb+iXkgȗ'H X!2.WbiD(_) U,,ƿzsK?\<dct"f;Ι[0qfkcJ?QؿY11G7FF&<4`*Gt:吅RbQQF n%ҒU'ݓ4ʚ: 4HsLdz{ψ4JDA*t 1P O'd}*BP"Z)OKCXvHԯfQ t2h]:!7P0odށ]DX 7 Egz;vQ>"+[ 'uH i0"g"$ 93\7LmP~FnrDQpD\Ҭf HL HR@6thP̰ ,V?sG!I|eI<7Vp*pZBL:i0%YĪ UCh-"x]e"ViZfٯ,6SOM`K8qЪz-ܰ(@FCfB؅mpA2qK _sn02U9'_ Al(DzRPUI먚lI Y!voIń] @> :P.EnEl#j!+쬓u ->8ήPcKد=-oz-[@Pa$ 86Gqy>cEsm(*( d}5|!^ A̔[o&2H *ׁ⢪z&$zTʩGzDX=lKUZ<q0Y@'N޵6 Dp=uZ꿌%nJ ]@Ґ e3G)*XF=3~ XOibwz)tO($q.KDO6oy$i\Q 75h& 鴱[~ $pFSn\0_ܘSyڐTlw@ /eu I5%B__mNA\$CY !$${UD5D mdꬲ[2ȤwxlHOhrFueЂ t@wOiXQI//#*4m<<<<<<<<<<<<<<<<<J=OjhAP<~e?%Sw㓇GPN-p;0yeEtmz?!#& [BZ>%ۯU`-qqs^X; ?r|IM+}?M^NCq6,_|0{:TzGwI{~M* u.e44lI>`jm)|6v켳9Gf-[c e7`)wGz.z=*O+ Ƣmǝ # 4K{yr&DB4l:UA!?[66MVr(*|oJs D/2)L헟ԧq.Gv2O{샭L~h47jBLyxxxxxxxxxxxxxlTX 0A=O֯?@v:`(*.+qE+N=17QfM)]vλ&=ߗ1#_^7 ~n\\qXѷax-1tEح'5}1|xg}#܂yD[=#ݲʧ;0?rxGt(++ňKI_V 9 z$ɏ)Oukѭמ8fZXxQXt17lAG~v98> tٵNr/)*ӞSF5q K~~?/۶n3_[5SK94ZHL$nĦ`c⽷a;oV۠bGIXΛh:",k<31Oj}m:f,;,H۹(˰7ߋ:uŽ##PV-_uX;:b4 9[h[55wދeOXbYo_~͗ g_1;u Ofݹ|#}_,ZO>PR&Zm r qjԬ7RiKϢ.Es}pg7_զS,J0%5jb߅ E߁)<JsW`mZ߷.bXz_zL<x&~ufM{>zE )*]/Wa5Mqq(-])Q=<<<<<<<<<<<<<2n Ai Sw3 i0O{37 ;BM_x,[9|٬+<8|8۵mM7fLw{$!U_we?⋏?ރn͞S/2ȑأ6!hvGmZcOAͷλ w /wfD@v:uq¹Á`Շ砼 /Z=˖໯>GvQR{cQ6tStӞ~) S]#N>n>ىkQh>/qq* ԋfVOļٯAGja0w pbbCq;z֫DŽQノS`_ ữd,&JׯGGE7 << ۵moR|xq9)i-۴7jy}:u _YY]S/6OvL1|~R|eJ"ѼUktIӠQc~5rtضMͷ;_ɇlw+)cݚxL8m;tAEy9>_K=\9/?~z''`k"-O̭7%5jF4hB)ĖZFYi)r loI5PF K£cBнO?~m|*,;Lf9& )ҫ~F@p\y \(Xr'<<<<<<<<<<<<<Jb{`3pqI-CqxZܓjR%C꡼ + ?Zt <W!$#!נ!Z֮YiJԬ wqQQsMNҙA!an]*/+oCppZi6/LF:uѬvh۱ &֬\aӏcNƐb7eެ^}剮چ5Bԩ'a攧S^l|^YR+A& ׎bܹמعXG9s*^xb =)N+/?J)9r=9n-5hu?ԫ?]˓λ w=oxsxxxxxxxxxxxxxxT-(XL**.ƜW^-ONo!;vHV.7͙7gLUbhު5>zm^ zn똁9y=fNѭZMjv^Ƽ7^)O Ч D͔լU kV.oFyYsܺn{Gߎg"KJк]{|8!ha'|0 RwF3O*e.'澁^yYGjF.Cӭo>Y.? %[49NOV`7)Oa%8~xr= _6٤^ eeX<нdXd1~y 4n>?O{o[w %5jgbO0QT ",D"C8i:Cxv `c;%s}cbc>&}4n%F^} fvg`++]c]7cASѺc(};s8uǥ#BZ}u1z챷ON]QQ^v[Z֮zB1{<8QpJy;sg~KI-$'7l}< Kxy 5hO: W._=שL۽^831 Q\\CDӭ'w| ]p*^|a|iةv99tdS~hު ?h|xQQ^uvm1榫P\\/5j^9V,͗q\t#džyi7(ALN2k`uQV-L0&#"Qyaw_kꗊ7jXxo,s@<26| |˭7j |>yxxxxxxxxxxxxxl6ЭW_ >$>y̝5-۴г.FqIS O'61;<<<<<<<<<<<<<<<<<* Hj(J(QB_bf~u$LD'8lm7j%çssʂ/[/PçOrb?E~G Yv~( =<<<<<<<<<<<<<<<<6NBP !j!A~}Fɑh(8a2K|R|C{r~ ,_0{K.D?o5[n-PQ^}Jt쓏eoNSim1x^xQ֚0hr(νv/ŸXf xƸa;s'ێmpSjb]G<Z5lݺnvWTBãᨃam8-L'l/[8qUfƍ۶ħ-LO% @M՘<,f1>y8zDzOUÐÎoc71e,|aJrQz=4O28ŗ]Qs1ⶑQ=a F<zޚ=+aVxl8c1 v N>7'u(Wgbо}_8j Ce]݄ݏ/s&?pvG<>zYgW׷7O-{sY%?.1 F>b^qߨ;=<<<<*;<>c&O€={a^1!X48eQؽ[gu j ]ు`X%d&~~PEEfL{ٯ| Ew}3/`irqQ1x}VL2:t1cڋl4-x4;۶l[+pSq 'csq=saO?_}9G&?.Q,8q 75kp`!k?`]z`㓱zJ\rY}L}v۽JJJk31pΘ54e:||X]N:̙3ϻW]~i*+**PRRKq1h!~9^^q5թ[ٻLEDBDb%!!"$TQJ>Z~*ZZ/UEE*UXK-J-&d2?D& }5}e=s=YKP,9|$̛=;;;Oxf6;wN1nz1m߁'7zKTn-7 cokt:~o-_ '%&rߌ>-[ y,^8'g;u|ܾu3ߺtՇͻ1}943Tɕ@4XV{Zݗx>l 7ؾ߬X{{w2u,6GeGG'KKK._]N 0z^xA!lsԟ'HIIwBaF5ꋹ kG4aGw~#'g(y,S1;}v4i-*UE`;?Ɩ6!kϕKz8V@;cٮڽ'j{6Cc_ڶX"*TW/_w?׮^F\[PmB3>-Y&]rY?BkһcʰoG{o~ f~x׫@WhZ 5j{Z[@o{>2Zw IOOc}&c4kނZ%N#%%`ɔkRs BYO>Z yi@ȺwJq:7_C y} ,--;RYT*]/;lfa( *W}KfnBAǮ Ni)L1 nTW qׯ{}/.Uq숴zNԟ'شgB=o0MD؏+_.]zh~:vJ [[;*rERݣuH[z B@AtUnNچ/,.B!wgժSʚxE`Pe?={n3@6(kbރe>1F)>^Ch_fmeMx*W+}zycܱu3[FzzZt:[J:Xtvk+VRJ܍7/s2ߚn-ldg@n=>-zʎ3{{}된KV\D$|Ѣ=o(#cx J ӧXkfu2tօj_z R&;V?g h*O:V/?qy?xp?1$%&``Oox Iulln~ƨPZX0wc| !xr>OO^V%##-Z?DJRR5jUqrmo{πjyZ𒓓8'bi,YNps_{Rb":BÇIMM1HLx?))1חU8*Ub_WJe3Tqr"11 Lx89?UY/>gߗX#/zrwvvU`-]ax& ~ &{;9@mAU;3W'gzJMM%55m%~̜R94U{M}t9䗮Jⴴ4^d}Ea9;咥4hp|F-}'gٹHL)W}wQ~~_'de#sp %Q;-_~IMMտyO(~yc\ȠzӼe 7wBgg lۼΞ_և~ݣόd^3(Qdj_7DвU} ?Ca=SSٺ)\[O"ȡCA"6ny@` io}4TPH(OD@\ěͿUOǮ}iL*MH|]۵-amV:tQorraՊet ińqoŢowآryy9t)Zͫ^غ͸}&!-̬ѡuS>x.{*Ү=BۭmBaC|0w4tǍ8u`WY{:n4qّm[xSA%}s966xfO`@PGbL}^2sI;r8[ȳn3̘3祈!ʷn.fSUa!\rɵEB,0Lʥ`6mYk{#( D}T"c2c:!B!BaDE:d !B!Bay{.2giаQw:"=B!B!Ȍ B!B!“OpG%d?'Dkׄ=GN;e!\rɵEB,0L4YH?u0(݌xWeP!B!B%a` 3({;*&B!BQzddlvc)dw!B!BRK5|.|B!B!eJot_#dhE)!P*i臧Y_껹)kS\zH+rssNcTqr4y6[e(]I~e"3eƀ ʛ W^RMdժ fqn4Fʹ}j}i:@tΟDTvtkoҗv6OR}Q!ʻ.:"󏜯jD<3UhY.Kc)7C Hoo7LF0œԭt_B$?AY V!JkWoЦuQnxyycRZ60 SBb:+r%!ä5+[0$-kN%B00j#$ !(ɳSزk#L%uE!}Ɂ + ,{X$Bp$(3 A( ]kٳ'jJJBPSNשT*hݺ5*T@VV`޼yO^2B<0{9h۶-Νӯ5k...T\7|4}bRׯ/.Pj߮.$''}6tH͚ԫСwժUpwwݕZjЮ]v4~edc?F~}ouyrCl? 4~NcԨQGGG&N__=_~wW֖\˯\Bhh(666p'兗=_ܹCΝiԨs/݅ hݺ54lؐEEE퍍 :t֭[F!O*u+'E+;4 TIOO̙3TXt9|0[lѿQ{տ`ٲeܿa̙æM9BLj#Ջ_;vd:˗9y$K,1wK%)gӬ]N\mnSzu6nܤoggǶm[1b8 ݻO:t֙D~<իq\pc1lܹcs++_WVAt:f̘^dyҀhQC}beeEV"""GIXXO>$$$`^yy4hݺu#!!#G2|ٺu+'N&559s>%%P|||tC s{׌3x饗HKK#11h"a̘1&BTT6*?jȻJ::J$͝Q?5j @v8}4?#G;;;}֭[gNREc֬Ox 0ʕqvvfʔZ%׭[7~.g6ZjDGGe[9oߞ˗sm4 K.2h /_'xdLbb"g\t={ҿ '''RRR3 /^wެXB3WP5֤%[lO>K~4hPWurURSSTdǍ#[nէq^ȑoP6l@Ν>,\!C0l0fΜi0zaoҤ ,[ N… iҤ 4lؐ(6mqܹS?1srrUV̝;4V^̓hٲ%;vl۶2c z̻ !Di4}c^{Hu@ПɦsgK;v4흝Y`!*;ѳX| ֭ X[[aܳ[jj5Z777j5nݢk׮TZ777јAF!DIqݞ–\a*+B!mDp%Lլl6VŚ'QtwT(t$ I@H!J*7? oosg\8nMJ+FFaBQżVTlg_ER}7wY#l1Bu< h1&m#FIVY/ !DyWE !D)t5Z;gmw8bRZ60 SBIr|IkUQFA^=oBQJIge-FJBs[[Ɂ |[RIN+ JOOGPpZ3ϟOϞ=PըT*T* B?:u2NRD֭PjZ'11C憓,X8NU\vڅB̙3yM6jժGGGӢE x爌|ƹ-ׯ__\]]puuj*]]]HNN`mtґ5]WϋC YU+jՠ]vi3+65~>~q>}̇(?~<h4rNɓQL8Q"88{{{ի/W^-Ṗ_rPlllȑG&N^^^ <{;wܹ352zp[ښ ~(Cܺu1<\j7[ӡz0%0dA'==3gPbEIOOlٲEFݻWe˖q}߿φ 3g6m`( Ξ=F!""Yfyfs(}HHH`+6#,,{.ϺAѭ[79r$ <<[r oRSS3gNSRR K7d:wμw͘1^z48p -"!!ƌcBi !O%Ka#㈼d=@lȂcLJ}vN>MQF:t`3Tef͢G89fȑ|Gg̙ 8BAƍ8ץә5{o< reG2C&OBZZZ*=^V-qMСh4w]~#88zX }m۶چ ox"߷h.ٳgKx睷5j]tcv\vZUՃ:vlGt|!JiӦ1mڴ|6RO'99SNҽ{< sjZT1@JJ /ʵLӱx"RRR̖yciYݻZg믛;w6ܾ}ѣGx7w6m0}T*Km+4P| FN>}ٷ s|CIK{h04[?RʊϣRx:-k6oAN8Aݺu1bjբm۶Ohh(;wݻ<|͛7Ӿ}<˫Kw9j׮Tn]Ξ=˹s[~=\r !kxyuΎȏmc}dMYfTVM:ujCqQ֮]K׮];v,| ;we˖}،5 PbO nqF*U'|ҥKQ*(ǥII3mX>7oW6fȪPk֬fYegb͚TP؏ݕ3[yyyamm͝;ٵk'~~ԯuZ- &MP~-Zy֭Ǎq\~ 3`@|}RZ52s׭ڷo˹}6K"ˠAX|y1߿p%zI&((WWWHIIҽ{PչjBOY3!Kv耉?-E Q#hk˖-T^]o5x>|8>_߽{w}}1|p*T|P$"ۊ+>cĉ8993[yv{Ocٲe 80{P(XXXЦMJLLL_l?3{xڴiٸq#QQQ?NFZj޽h^yT*۷A߿\)L!%<<3S^x'udv(L4rvv&<<^jMFɕ$...bch9s] =R,,,9s&3g>|Ez͊+ *===sd˖-Gxׯ *Vn]^Jjj**Uƍ#[nէq<(}!(==:7"ld5(r,r|[ȑ";Ndah;trSLa1bcc uΞ("""s7o͛ԫW={Э[74~Spvv͛8;;3rH cǎ8ny&\4&MW_}ɷ~FA0cGF 7sg$22xzz;;ThNÝ-$-\xcL-|t,W}hcGΐWE1vkE}*Bkews1R}; (|@<ի6ʵUUBQޕй7h]W7!xn͚*g23B!B! ٦3V/V E+guBBWv+ [rm!01qw4}nZUTy޾X$N~slbB!G-7.^R.\J-ؖk# 0uK!( ._WAOILT\C dQ*,,ws1R5#\xUZ#.\Ghk6rm)nBw%9\MH ?V- aRMHَ_껹)g]շGp"ݷȭvɤrm [}Q!ʻ.ŦX[=ĄB!B!ijwo6Œ$s !B!BM2B!B!DiVV=2B!B!D9b{YB!B!`|B!B!D)VرB!B!D9"-B!B!J@V$/q`2ڢ؝FY( ,/Keis5w)]QWOC89\a֭t_B$?pHAW^CQN&j8R}7wY#\>5wN澴iIt:8QҭI_SF~O8BA8v(ׯ_cmL! 7ܹsL2+Vh̏_\t\Dž K.1bpNw-ŋOtB!% F_B!ʌY>3`@*Wvٙ)S>d)IRj#fsYUR%uΕ+~eAjՂ=s9^}u o^T|7ic|B}+Ch".:vFppy`۶m%4 AA Џ[nx"߷h.ٳgKx睷5j]tcv\v .ЫWرчGNX[[p~j6ܪJȢEi9J%ݺu'66-[6s5/PbEl [_eѢ ,;!BFZ !D9̩S'޽Gu 9mZ-*UYŋr-t,^?tׯ{b!˘1obcc3h"nݺ;cܸy鈊.]yciYݻZg믛;w6ܾ}ѣGx7w6m0} Rm۶W0iaa$**?&*j+>>M1>}oAÇ`>r^zW\<wk+UD|||} ##,Mjܹ4oL޽{5O>wf}&'N@͇Ng |2zoN*NܼyCu6[=zteJ*h4kX^[B!Rɨ@BQ>XYYKxx￟{9s>zR2`P9;;A^j&䊃RmvTZC8;;q~TRSS8_:s !X tqqښ#Gb{{IDAT7Pt!;;;O#jYN$!!֭PR%FOΝ;įQ#HII֖ *[/YX<]PB! "Ą4i _}%~ FÌ5̝gӓ(8{q`=^A D9{U~e@lll$99P*U&'6{MҖh,[11ǘ;v4:;;`B^{Uywѣga+[.bmmm4ri|}Ѿ}[^x'miѢYڶm믏W[דe˾eﱰ@Prj꫃]&/ЍضmŋQ,n !BAB &!\Ru$tm= ѣs^ N:#4aa?>s\u9wБ:~ذ 6ܤu!"SժUYdʔ ؼy:u6B!DeQ@0H!!! 3f,ҟCƅn9kք;+B!D#))jQJ󛖌$T1, (lYkŠAiJGCSN!OFZ5O34Bͽ:_Y)Ο{uҖk[o; R~]ϊZaBQjzQӣ|M$RXJ%wpk! 'n"Jَ_껹)g4$::s.I˂bsOCk#L$u,+|ȧeP cժ dhE;!P*ig㗥n4Fٸ*Nt֞?"::H-rssNnhRz6T[e(]I~=~TD˹=9;#^UyB!B!OoIX~~m MA tyBB!B!gC˄B!B!D)"S !B!B*!B!Bb'-B!B!  !B!B#B!B!eS`ГΞ9Ϟj&WBT*iH^f9~Y.Kc v綆c|ZWVÍf88\a֭t_B$? ؋ʕbwB?DY껹)gI@K?ZgHtt:{Hzfҗv6OR}Q!ʻ.ʇ"3Hʃxf*W3믢e,r6ZQ] H:k#L$u,+|P@BQ\tVFY۝V(-!8\pGIJL2)=YVMAlbBQJIge-FJBs;|Ɂ $:VyůH BQ2ח={vCVR_wyؾ}>SNRZbԮ]tjԨJ PPSN#4Ϋ,Y j|}s,p "EΜ9M/sҤ~Bn܈cڴQj:-<:ٳ?`u˜;w6wӨQc>|>u %%Yfiӯh4ҥ+'OښI>͝7|XU!0ĄDƎ}ͤ/o#J/ EVs^B!JkÉwQQ]ǿ HU1(]1(\D4%FS0jcJ6VLֻD|ĄҚDPnZ"7Z/ De,!2d2A}:k9gg`֞O<}߸H^2w<Νc'pss'*O=?Jꫯ`TUUUrE\\\YO2;]Wii);wf{³>ѴL``͚Nі̓QgQQqu꿑YG/[B! !'jkuT6ihh/ƍ7dY[[w[ָmݺ!^H<=G9;#G`3{8sNg/gGeee2eo= ggq h>|, _c7E<^GPO?82r(/s?NAAAmW__//l1))xz7\ N-?m\._; W[B!? !РA0)pRiQJƆl"#' @f8_fMW=5^ѣG~/h46mJ%..g˖{oƍx{5W `H??/4u%5u3L8Μ)j*x8xeDGt;';wfGTTo7JҥozɓÙ>}aax@x$^~9HO߉ ]>o%TRR[B!撜ABG={PBBBMM&3mM67ZMQ9z{;zZPIN~^1O/]b_)S"2% ,df8:t({|j`:KYx}$%%lr?@n~"#n7!BG  !}ҥ b޽ˎݛӡ!jࠁT;ux&h!蝺)?0m[sct(N&.EݻWB_~8:0=?5&Z=$@BFIi9n=JPRZpl{doz:GR[SϺZgB]qsw0C% J%պ8֡T*{{-#>?/O\,F4>~^f#FAV_(]o..8R*X}t[Gڹ}*bfEQTxܢ%ÈJ=Ĭro:۷E!zp?(r R̂v\B!B!w7Sǘ&f0I!B!B]3H!B!B<d0H!B!1@Za||kjPB!B!{ R.'B!B!z6sm$MIENDB`onedrive-2.5.5/docs/images/linux_shared_folder_view.png000066400000000000000000003153461476564400300233400ustar00rootroot00000000000000PNG  IHDRVVsRGBgAMA a pHYsodIDATx^$Eo`(PQČ PP$(HrǑs9l޽>oOl~k߮+t.]PQk]wǴ_Vu)P0+7{~e6/N_ή']倠FSpPP3TO1[Y"Em(1͢x<WgT.%JQ&¯g*|*vwլ*޻qpG7PIuBZEyxl_QEJUSUE~VX |C6~gH{d*zx3hhԿ2_ JrK>U)ץCg<7S_[t_%^ΕOXJ>S)wvğJ}OJ)q$]i$`ЂpK=?5S痳v N RjQKzE ii VJSmQt]""l@m\I`m tU)m v3/T >`U:kj_CmmEh\RvM Ail6u{¿]H(>s"yWɞ}gsAԼͫT=mKx-SmA>(m A}xqO>lM쪝.vFSݠRzՃnQѧ p?c-( z`_GݒnNinq櫖]|@WS;]nvO04懾)|vod.K{4:N:K .JuSq*/c?0Z|;|PA Cn6M=?5S痳A@7@^@7%5 m @76@ngݒ}O<>oii?m:pu[L=vUC.)?f;W?#)=B?JiqDVͻ4*d#(c@[buuҖ ZPr\o.||qrv@7 jS5z_%Ar|iWc& %||TD+r]nYUuNxf[Tzdc~Yq$y5_y6OV6˦~Ti1D)d~ NIEƺgDJg(u>O} R vU zq%68F &Ӳ̌U@-9kvtU`/>"Nr1@[Hi?yدdϏ']㘨;2qҖ ZPtQ4.||qqFKh j)^q|OntQ|@7tlmRIH5@߄#UXMIA~hRS'^-o!<.BJ>uK1{4/+&`gJ[B~0hAuxEi\>lM*nPj j)>{r,m.`6ڤ5X rQ]T)AwW`ׁvQBhۤ2a6tU-32;+afL>MgJ^ĺ`3ٔ(T;٣%61մ}{kyX1I.Ȼ|PA SS<(2֔/N_ήFKݠTܞ:6lfTR2P)bZL[>+97 (?U"6!ƕM(y|s@@71fxz5f)OХ|DX tk+mLɟAn* QW\3{IAIiI^ney/8ܖbɤ趘`ЂTqGP>lM*nPj j.9[J.AakI%.J/*ZƁz -|.(\)7ַ1eYo7O-~dZE=T @Ib[hmZ [z3΄Ƕ袖]?LxW\[ϵ]t[ܸ8&p>)?\${}dZp颼 le")948kA!pwJ[B~0hAu:E'[C5S痳AƕM4iJ`7Rnr#ЍAn57jmW~X-DI`ES tiz-v{A)Š֧$p&ͥ463&!5oykcxyofv t#M.wm@~fܔJ?HX]ú1\#4|BZqKRKWRia^]R$۽lѢRee=:XJބt_ܞs\s‹偖ΤMHE7 I` *T1mm XS_qkHB<ؘ@vmB*.7mB$"d.R-Хr sJ Ή9H6PnJ{sn=*dfQl򝛋6}cI9`Mqe倹1$Tt_ Qy\>&_`0 _)kz41gIΖ-%HJt?W,=~KޥH41xM͢Ϸ[ߋ6iqS{7lѢA7-}-A[tZ\tQfMnLwMJ0Pm_ss"zej!*vx)؍zAM+<(_ʀS_Q|˧sk cV%o^͹fuNmRt#ZwȁS/AIҭs]SOlPW⬲Y?c eC)=>)9tc@,%?HAn"&s\h>nj] {:B(>IhL6/O@ KsWݠfWܞ"Fo퍾fɀbieXy lԿ5 tU9Nquۻ*ި+sQQ=voLz [uMFJ}(~M^N^-P%lZZ)A5\fH{ )qK+_Q~M\I~a*yzN稷Yj>m9n_mdŽ<[[tZ@r3@w?}<Ҟlѭ6@7l) *t.bZBoqjr)]Sllr@NO\Jjs!6 tSoM6q1)櫽Q/" H!%A)5>'}OjB~UE.PqtNfPAhJ붝_|AkW%ZϙZsЫ0l3)_J>}t;B~RaocBW^&%{JiåۡZ?$%Ymd}PIlMuFmJ( M;ޛR^Am h GqȍnG]IAURp! Nt)^qLB#g㟩ݘAZ.֊uZxGT#un*똴 b۰wTYTMI7|ͥz >YJ}-]-s$~n;)5*zoE3 /|U? ~[JM4E n_[/n<,26_x>˳ZE\;iT qULqkP/15Hѹ']'x5Jo/'%*u'MRYRYkZZ2>G@7n>+tJes<y ͩ]ץy.HE{k9+ nMwOrpv[%l+.Quo)݄|7jp˝7:`}z-?ՒG#?ys)#_kKC5o8fM.*ռe/rU:qTjVR:FŵζRUۏv?m۟|؞䋳_Vd_ѳ@w jWp!躖xi ):pEqT*wq58YNvݛE[Jtr[tQvwKtm\lF>E Z=Q`}r t߭Ǧd)M,xc0/=7%ˀFdpjJJ[\4LZq;wpώw?\OJIǥ[OI `k=UR29-yvɞ/]Gfetk#cT `Ue@{RݠRG]Ms\+o#-Aͨ=CnL T{jO(:lZT9Fίhj1M]Ҡb〺KJEq3F4,]캖nՄ|kuJ+ڕsл4̬m65U?'#ƕn.КT+oz9)5 3nӰBJϓnt;}\[Z)qE)s|T(%|Zn|YojԤ|?_U@ۘ4yI6UXwUԙWLl ^2$8v6š#*R8g%*'q%w]ƷB9uP*u1(ޮf>ij*UmL.IV S БJulT|ٳ@P{)7P@v[P{+,}[RIPrmcuwq;'pVE|}|[RsΖqtQ6 HAg]7x򬼤aw'EtX.\J)͎ebLhuWLTwU}k.j<)gy5&9qPe{/1[wۣ2@*U' U Zq>RRD-6YK|[L|f>ĩ⭿9֡2@3|68nI6h-Dyl.%[K:JX{][V|ڝMk1W=z\iThMz}@A#gXn=Gܵg,| -*Al.jm}+Dn[w=#555RYY)۶mrٲeVo.eeͫ;vkmk9\ jI8w$|$7BwlH;ʶxX"_fsw͸+U[[L5]0UVS˟&fs*ydQ m/ϪeRUH6Vo-3mߺC+9ܮֶM%ۚZx\\[5<-7klrlٺ^YyZ W{tpː rtQU3n}7T n1cu]7=d=|^v։B&]5RQQfbm߾YEE\vgҤط7/m53~3ړ|qoGzݓWʴ׆*/lTMjZ;v2 3lN3/|(ʝ_Q-vL,ZYUl1UjrۈTWj4*JH]U+0xɸ+DgaU6'|mV%6ljk޸q*Q-;*vÏ$Etk?Lhr#e wN=[sc*+hӦ-&32ކo:%G8;3ۓ|qKZݓGyT{lӜ]3GMwN\T)⼋J^W4xQ{ڙfsgNelGmVeIk[YmJi1hKr-- %%ǍZswWfϚoeLB9|kj45XvoG S½ۗ|=zJb&k_ro掣J~/IzݓWl~q/[ }Pts mD튈 m~L{ɽ`sϹΞFM)"ɭSs˖-ӛK08&r n:={ 24t`_a O# #&K~#}d?~L2I6oޘb?M<ю% f.BDFe}MÆ IDn,I?qp?^|yO?ja7NNj//\PQ>gAACZό:g6eL(?_ YfXMM WN4\iZ]c/]#[Oϕ_,OX|jj2Vs0W#AӖe+F腨@)"Kq Z+|0dg%q [[EOڵĺqb]ϥ[al}҂LƥK/vAd0:uWk j;q}au/p݋< XΉ iʉdz5-/#hupucEFKs/N\,|E8E[Pa0oekFۮQ%"V ]AʴVQV.jwFb[l6%gPz#o[! IQy8mlBY.> VT!uc&Zy?^q̼:-֮oLb69fN1ċkh}Xe(*['!\XIXϥ{\5V.Vl<~^?eǧ=8ݮ uuvB8n[wߔ8{H$[uY5mt] $m^΄XI\4cYx̜5e̙ }<9\sO?ȹ9\?.n-̻%*9g%88&ϋ ?AAAAAmAY嶫sL)^/pu.=\yQt5^ԉ7Dt.)T?]zqL |!|t1ۤRVWZMm[jd X^&qL(ʪeE-J+5A9sI%X>cVD[t,_^mT^Pduċ 嗢cfmE\C}qcXrdqc֛7sq0e݁ 3;s tg/wy1c}V sΜ9znq=CLiSq!8cYg+as</,9߁qk5gecsK3saą_ݠ'&SZn5PnS۪bҺV~}>I|loׄ_6|t*v{Ted;e e[e3s쨩2uFyoI9rt ( 2~. 9.X{YLufpV,S 3 {':0uPͽwP~w>~r9\ׅ#Ǐ&sI[Cntn2eRz?qfI8,]*r-:]s~t-fΞ!9z ͋e9lRWfv}>Oˮ~<Gx@%\s/ĕcs8}˳W'Jg(. 4dxӕ@w-|Bgͺղ|Pkd*2,^u̵P/ #%ݘS⒬[ZS[qX2>ñ˩jgr˭^e2:~sa6BViąz"bVYiP7[C=- eQ>_KxLt)A[5le/+iXb'67ttYj<5f3h<a؃~{?q80ܦ+TN~sy2{49{L6zix墅se<=wt[5sj:^Z~Yw-fO2mT5G7յruI <,]淹V[@:H/?9NA/ۭ҅ot{~H(y\?jKQ~S/-Wwb^ϒˮ\pWS>+~\y2q=&zOFoZ^2c⥋,cߊZhyXǏ} Sr e=, @6qN|yv<I7 Sni@>smg?\V3C3sVȟxU.}dYGgߞ\(x`\N_;S.yTln3(g`߁n1. qFUbf87cc|ܹXs] @2u:}-c=f~(hXgdX>.ixs}.l*I1*^Qz0qP7w^9G%909Woe5W`]y}q]1o8 ™_ɲ,Ln%K=E&뮍V!`\.^dn=,**bpr 6c\ws#w{w?\"'bquPr!L;mdr_'N|^L} rGA*{|_gR vk?Qg 첿ڵsɈuă8q.\ ukk&A=+ t3{Ǐuˆ AAAAOuOW?;/Q5f|gHcÎ%={%Gw|Z zJхTJ5]: 'KUZwjFgKԮ\XǏ}x4G݃KG*|l_f,^-ј_G" Mruc5keSݴtmӊ\6;k\p+W#w\߹rwOgkɸd{E^pu7JBc°y`\Z+kA9v1Y@ŵR:Eg8+>9fx?Crm߯qXi~?  /)P?;n8#>c}tmKVAT,5 OΗ^+S}urſ{pd D/\;ůDs=voXl)@ [hz뎃i$gɱdgx`/b- >os3񊃱k6Soi:߳c)ŋu~o&n~΋|&uԨQV繎ofݠ8򻾜O^1},ho-oq'ȯ~}qdo9ʴy[(VD/ͳJZP6t$ӓ>g;\?XZ߁XV_ݗGc*0~xRuTo9w/9rG-X} p~!>?ͥKG;vj9/Yk#EF.2eGK2\wv[3-_.zǵ-l"e[w,>q>j t,ޥA/u-7Ѕ=hė"X_@-N+`o.X0@rb9o}GfϏ%>#ƌӿE+}1kյR~gD_/%,%Zkn*HcIץOk8$A%/W +oݞ'k8֝ jK}ǕxB9愓׿X֬hCyjĖm UX;E./ ~|b,r/Y?Ȑ^$ŏ%uE˖ˡZ ~ 'ʧ9T\!5zB={XǏ}IQog{ >P7I5BAz|X;?߼8 ;XjgX8&PfX)nZ U۪euߐ_߻T.QȽw'ɵϒ+~S~vɏnxK&Z/;oԀ o ty3ƄacS'3g|gˑG-_9[-T&w.\@D܁Xy'|RzyG4V]#udSsȑ#宻oIt]Z/ZLl"W[3X25{k׳1K.5h ~%g`<*/_!WE7Wh_R_2~=[oMN__t3F?',gyw} P?g?{|u]crE.vhm z}|( o.Esl^S>yAZЁI&%\"_Sby׬E{w|ߖs[L65[b>|?#̎~YZ׿2O~lqey=e}?ɏߴ?'zȫbǹ.ZC>Ѝ̺u)3Ln &4s;' jrFݓGC=2z_?&j݈9?VxeKͥ mGh:Wz8zszڢ% e"zQ@>-zZ~ .>?MZwZ*klY^]#;*kdTTV[/ŧ&W(nN= \VQ.?OZQVY+|<;{?0@e* w6qux]+h,97))GO[_޿o*?~|2fKg43(g`cYodzSSq<0aiLbL sbr&` OfK- .'oZ=ҥY_:ƁlڰQ9bsia9EqO/0u0vI2k|9_€/Khf9V+/()^c xnGL/~LgOIӦ[ ~+kQ+ [|Gn.{bi}"^9rҵlb>_|y9옣Й3lϿW]-f{}~(Wj}3`& iǿe0;w"{o~+j=uy',qō8ҽ'؛ ~ Yg~G[NMr7h )5Lk/̛^s\z_0Ƈ ͷI'~3n8TLLӫ{k IkZ õ uYzlش1zR/ȃϒ 7nPPPP/%QYƉ\˂eQ"4,_ʤcN:ɾIh]JTF3bqQOG@ Zu4V/.`B:/ xO~W,.>5BABO}>Pzs7]ֹߵzW:MY 2M2mEEPnnis>2n߱٦;e|wL>S~t׽#]7Aq[+_/ ;y&Ѿ0r 7aM+ߴ4}_LxseTlWɌdH"2=8\@ 0}imdS̸V\#=]=`@'MM lieV{:nuFQ1佈Ҍs+V\@Lu⨋l,mcli>gosxz:º19-`(N\#TtaL+(]|,<.ݪi]W^+?A|}`ϡ#GocE˦ _f~ʝMζ]Xdv({䑇P7v6]f19a#[75Pe-o4sλ /k ȝ9w<;o˜dTqgQ{=磅;ew:SOYgLt:_ y8>_.\fΟ/3ٽtNbϮ\^N:_x--5 ~Í7[bGn3w]a9]^L̲zF p;ܤ;|À]Uw je+#)-[`/aկ؋!FECQ/A9zǧ}PB_J=*V#k?l a?/+jƐQO@8Xq AFlGZj/3^9]:Q .>5[tr{-۶Z/YR}Ï4`^ S5;e6AKqm&Э.Z93q|qk_W,^|OoY7#\| 2zb;oL30 ty+Ѻ1]ZhZݎ:D9_i˺uЊ2prI*~6DtUXvr)0ݹsc*trrM7[M7jRRVKZ [ڌdZftMFv葇q T,Y&'|PC+=8x)A]umTVWɖeRgi-oZ7zɟ.5*oa2o/XofLeƙ/o@LO:d OibžPAvнsrf?>.ݞImK5;hM=|@@ꩧ Hi@!6 D׍~q5g  .0>z tяfiG?|I/P9 rC[(Kp9A^GʐaC#;p|vH"~V^K^X[ljX7usv/ - ϑ9 Fq70tڟ5N}9 LוWI#B `BK w3+R.f euIWgzq[I"Z`ISȤ4,/f JQWtQH!={SԷu^Z70j"~QN֦[m9&.Ü@46eS]]auEGt t7oY'[7o ˷o$<0W.2Ygo/=0]~"=3圛ʸ)+ƶQw tݾaI +KẄ́o|+_OAJm<8ƕ HR%hR*&ٮZ&O>|wEBZ llċ He.|4Ku3f'M_&=B@Hfu䱌9J̛iܬs53I27-ndbX$ɰݻ4 m|'\..yG .~su-]0Fwshſu1#Gc$qÛQ0IqlLζ]`usKgo9+_1؜;o_ȺLi9<ܳ2y4rTߠ<̳ 8tα?I3Wc?MvE=-tq~b̋O>?JgݾKj^Řՙ]K,JZ..,4p3b? Ȅy^ wtlRt|dk_' AAAAOIPswT/2,w&ɉ'b.22Oa7^2Bk L[^ɇ+ސ3U~9royC. cǷ#?~|[^r7.gPSkǘr@DFƒ+sg~fuh΀C_:u倗\Zu{ G1ttTk 3BAf&@g)]: 8Ήu]e\.!I[ o=Wԕƅˢwҕҳױr)YWaם\2kF/*ٵȰ)ljB2E=-͏.F<ٻ]Z%L7Ⱥ"1>;M A'V,\lI 3;[iLEtYv)``72i3d^N:پk]}L:PƷϱ6+ p;]n!,AAGGM 7iXױ^ ܳf̔G'?fҬy f+OSc?3'˄ޒ^':<[b~q'ȏsC_ ~8;n؟{)BAGn}= ܸ]q8_e`mT7bs@;&FL j>N1уI'^ 7j00_-/Вj.nԋ\u1^t3QdOLVb?%T \חjqh1j1[nޗlj*~nTǏ^dLFuxOz'+nU[ϷC)?6oo:6UE7uuQ)?&W?>U_=Y.i|[dKOI9r}??, hl|:uY$V>WCFҭ\@M,:˭.O<񔆷0i r`wNS"A-;fx]Ή+q'ZF]^fU cdޒ%/IdN9If)`qM9O>d~( {"S_Zow'3ge4_r7ζ15]+g*ҕeACkXsmDf _..GtEqU+NaGwEX:[iLس8{8} ߴ.Ɵ=srO._Yr1)"P=cmr)YA-Z^{uǤa'9?r7U\-/"LF‹,o-9أegwUG?e >̴Vp2~;:_GO7~ -LL^nƸ /P #J/h%ױYuyyᬻN>Х</xرc9wAAAAOMw/m/=\+ x)>/ȟ/L6m؝<]EQ}3.k/ʼn eE6ŏ2̠Z_R;3,sSG}$Uh(QS`x1Zo};WVQNx9Hbr=_˹uuui%n_f|^:\-֫.ovk36wozG~Tu'׎_5H~tHC7i-߼v2ybE3(g`߁n1߼ 0ZlCDŕ%d4w:A:]ԃ²ʵ.C<0jRęɨhy ry1UCEe^\.3;G>j' =zA.6;'.pܽpiB*Ch1!Gi++:C^c-{G>Nzsa0 V\=Γ߮˶@>#Z; zocM*o{u1&9|nf=B{JjZvX ^+ h9]|{b0j!&@ׁm|ܱLҿ2k=>q;v<t(gХ2;Yt鮌4p?nxDz jԬ+)KB͙һ Fl&|\d|{?x&Hfp>|#~Ïz8j*e{6/ 0,u=Gst6yZo~H<) ~ORMůP85 n'$kJt t;W.?mw']3Z!w|qk^\}6--TN. V&,{6_  (Nf3g|{ةr|rdE5)M%u~M?A+Z1ޤF-:< b;^Rr}=6n1ec5Zdf >dX~rg;B:ˬCG1 ?^QULҙᖈ{ȄC,4@c*׵gl] Ufێek]Zzm|njKؐu9pqy#mJd.v6sh"QWfZyYKnT7 -.ӥLzNd^Thw"+g_`KqMryǵg_$sxR1s {Ƙ~VTo%-teҖT34i@Z>& 5T؜lx>\ É㒠kkm]؄n7F:?nPPPP3WסeM95bKh+l{-ly63 .LDu7epH QPj,~Mŏɥ <m/kGQGLE5be .>?vgEE]VA:1 @eU+t8CWwK]x3Q-\Ï+n3n> +D\.}a[ߔ|7L>"Z󅙔3(g`cYox $xX -H|y< ! D} M랔 lÅ̿2`0"+\2t| Ɣ Hf"Z#.xG=c߼YpYo뺋Gc*4~ 5"^ϝ́賊S-. +" 9 idrXߕ] FTX)߰fRΠ9n]YD68<E[AK@%GDzYKZHGAn{_gK$ j[qbu'y eڔhB kilkuи`Lzmܺ%]4&r7_XWנ^(b LbXoqA\gHuz [X, q5hE@+|ƴUVe~ݱZZtvٿҺ@7GЭZI ۫F*6 ZAjl:8cufR;sqBT,3@< ߭y>{Y@^҉xڹm!]zN-;H#n'nPPP#徫Du529ĵ'\:1ucWw+5趖`[Ӡ[\|.O?Т3n,['o;6fZCWJ-UXߦP>w6ZyEׁX΀wUm[؝1q*~AAAA Pq_ru)S߱/CPfgUU!~ٕk:]&JIh?)n]EmQc\.[ˤlVٱiT*tVm0e?q n\7̤vt,O8hŷ1#1,FKWiDfgƌ4u~25'}iO.~7Lm j谁*8yVnmʔIVϜ5\: tܨk:"(l-niCKWO*@7WPNFT [[&5 כ jV=V19+6yL*_echyX Qap~P|ϧmXH6%֠;m$˸Oo$ 'Or_tv_ ::SMrNlonq ZC_~ԉa}ƕK:PSV^ !)]`@7GЭڲUrٰelTZx36F0r2EB!0ug=n=4Fiv.MݺK[`c~X T!+@7(((h`B S64\݉:/)"I]y˖|^Uh[QVYEnD@Rzh.]\':e. l۶n&[`ZfOFF#B- .c܈#Ab% 9i"KwNUzy(ZR6m mPn2rݺ52j(KKCcq눰}GÆ 2(yOEڑ^}d 'P'춽|i>bᄑ{&e}͚52rHdPPPPP*jD^̦Ky_ޜ:K (ۓ&cCWSuugϷζqrg Vx?AAAAARVp'L|Cx51xV?*/e/{LgC[^u*KɁ]~B,E=t)`* )rQL[)o-f݌`'v[-oҥfl%o`~*7n8;Lwy;nko v-pYGyȺͰe5NZ 'u@LKOc -% P x| ^t喛A~xrwʋ/>o7׸v➸g;EKs vcPO*y{ZWx9rpyys{R?Gr_v5?ʮ+V\it{V6zn@}^-BmC**hnqqgT.n nG k5?=EsNkwѺK B`IkjM Э8HҀOx:q/M:Go09뮳"7lXg/A ?]a)9Zړx~xƳ|#҅Iwf3z"gIȧn/כKԫkR`򂗠#/+)kY'.ήlnڦuEԃn t)zJqaҺjp2AwV*iT.Q'Eƅ9s`:m8L C[)FUVܾoin{I7E=HtDc?a&aǯ+"Ho҄nŊ?]"9EFZFrԭ5m/x& =lk$ j: ꨎ/כKVlת XCqqt;;hb2- AHA׾˄TYkn# vRk32r/7ݢf+/<)$@ޖf֓~n}N<]p6C?52 6bwg|ݝg0"Bĵ5\]o9YbWTDi;'}ྸqܺ{vU0A/ݓĶ|D%< j]3T<PJ?nm tKdS[ԣZvcv.t}vPځ@5JEv61R]CvvPPMkbij[e!R]S.;Z . . S"5;tQ>C)]$!҂ݽn̴]hNza 65]JTݴy&b"rߔAAEqay>Կ_ٹVs(3. . .ⴊS"^e?ۢKqW@t`LNEˮ*Al.jE7 Э]M@FuѢ VV)O )NGVtiU-zumu!.P"\薔!nnvٲuSmW+ 1,(#{FT|!CهCnp\p\pըvV»{=O tSqOGV tUW=^{ -9\@]iQz1־y~uTmA/9-Ν;:tWcGUtUE%Kq)l;-{g[ҭ燒Z|ekߡ;jd֍}L9(# [vˊ oGAp\p\pu"W[jU{(wU[]>[l{PU-eQ^1@m. 6W5] 7R]벵E< h + ct[^|i)^КrРA $] . .:Q׺.3rLFեȍZHg^Vd⩮Xhäpkewo٥׾]\@oA˗ \p\pn ]nq^RQRl -x*X>x ]4T . .v W0tYrMvS[J>6#5<=6E]>#E>Ut۱||i\p\p9A7ť ]S׺0;%9u[LPe-qE>Ut۱||i\p\p@5SCz0]&:AD)~w upmseuf2d\y?s-MϓkJ/Օv-\w9.ı8A7o \]<.9\l&đm8wp.;Ɲz<>1A._fC tkUu-K-UuVjaY'5Z; Imm쬫ih I{i ůn娠! m>{iF?w!:^dunS*> պyngUF1[%v֦֗~ .S)l2*]zZwm/*TphhXb. ]. Etkj ˸| ] @c<`!/?׾"_կ)gy׿*_|Y~_Ȓ%:t Ҙ6n\o0I<6m`6# %8uq*;p~dsӆ e?wnv-(w4S+ ˎ 3]Y*Emu]5mv+˔ǂUpQLsU,Vt}Ŷs \ {^gy/]ZҗvHy*Vbuf, قf.{ϒ΀]"/&N˂tuYP۽ul|zkOL=U5е1nl*tIDց9 5=[-@K)L񍳬wv.1z)..z}O~…N8\ܖ?#E`uX8@/aXq 9%_>x ]4͔G,;ԹQEBKJv`[\gv ]?lRת.\VapKg=jf Fj ӥ^ *urpH^M %_kD*{\{ΜHK.3/?AĒ o**9B9NeWAo\U` ( {֭/^v_e/ t/^dkͦ޽2 +c99vQ⃀L&C`"~~govߥA>~ C[Pi#knzV[K!UlF"K-ufEl@Ls .GyȃLs*~L;1{K+?]#l&&&FwKpfG- &ZwiuҲ{s}LG5|.mN>UNFD:Fiݒi$(;%^+ Ç͹Es;'xL8wM:qۜ^!DG8;%{66z{mX̮~/_ t8߻EhY=9 ֭T=`1bAg~Iӯ v]Ip O4TG]ẔBn'cԢ2cjQ3VZ֘ä=+uF-twWgl-=j!ۚOz1.JʽT6CjI{WSȤ-ԆZ|nr v9&}aݒҽOE-=k\*q" hf+6'7| v˘]&Ÿ0PF|:믵9|%OԞ|}x={&,,9Ȟ2c4Yj<3yws55k|go4g?Fƍc]:krѽ6˟, nT܂/M3Ž-LRɊ\ZY5mՂ)PUZϴ ]ûMhFmvn]l95j5S~sEKwv[ z*tI^UmV["y`wbj ̴\CeCd {Wtʢ*jgѩ,sǙ?u_lbZv (n/??KpKZ1@ĉoˏC[:0 ]|4?Vw{zJ {xaZY97@&|oŏ(9s[f ߻=ݻZ 6.o~l=л8>[J[P~i Q)+hU;ݰfLzm ~!4!~11HٲvFCBtVpD*ukȰxm..M塑C'=&)7EqOpfuabygP!2v6Ϧw^-Wk<^TȆU}eHC㵹|6ר5qYa${X\VҢ]XMPQRh)z R٘9.\@{e(@ȥ2< ӵ̾\@u]^hqmd ٘:^{ؒ?ٵnje#ʹEHL]ȧ?) &=rp;8]Z cs5ή$'_fA7fTZN=^y[ 1nWºUE^p{9IٱaƣF*+j-~6J `hv nWKNeB;.ZIgb=W-5Ug؜5P!uhcy16.^m4ps>qٲqEb,zȋG$ƦqtFG~hס]湯# q}nNo3ͩ" ~X*6TQhڊL! NAN )mrR!&t}gwhZuSm1}\P6dH6_qh*ݪkjj~utgυ"ի^U wfSda9V{L{eRyƤNaѥ/EYS*^vVF;}oW[E_5+yA*oLo8e* ѝݑ}noy-Ӳ͡ɘ!OHUڞ:L:jj)s}u;+thݥU=2֍YaǾۦ JZKi7pr -eB_ptaa' j X\AcāpLs Nlq!/˖-C[ar]oZxNw>.3$S%O||i)wXvɨ]aiAf6P+1Z2鵁RWelM(h;㹠ŔWX6fSލ~1XCC6HшEbr}DN^A^{YT>iN.0d{1/Ku v)-(2ls( nC_YƼt+n򣗢p- }20i}ř:(: pSЖgl٘h=&UKn̩]( , v,0 ,  /|tkk3Z> н喛l|3]lс~R&Mz8axS %y޼9g\3y8_ZiuJe>DX u6ӝ1óg4\1ߓ)._fn; t @dsVe^t֊׵WQ *7N볕8h2VzKw7i-.8BjKu_o1 Gƾxu!F{7͡RYɢ!fu60`wv?{ t]?U&B+Zңz WG#Ht!eZx)vCҦ[]1we- z6Wآ,8p˗yk]i{2a?ci}Y~?A% 3,puv,qÏIMH_|C{ァ|#| =1x= f`|9>D~+ZL+sf_Ə"g 9-;Rڂ/M3},U@q}n/ߤ~F|oiZ2W3ZA UVE4NvR@؄^[cQ/^jhݽO9XRڥWYͷ.g V8C-O}ϻ t_-j#^Ex&P ^(Sȭ*i ᝁ%^t]rwK^|L}s\kŊZZra7کieUSbV]@6mu (8pp]A>sᅿ2Уg|f"m3;10 zN\]@xdcN&ZY5څsw:vas[#,heW܂/M3=qÁ {\kMе.Z}nUM ; vyLA>ɂ6Y#p:K?goLFq W 6 vW2ڛ [4~ȞƵײzpi]=:5LsoGF=wfj#WM+HϹ1 ]t<,jнW^C^e727p67F̑o^-)ҒGS}gmqPc8B l25\e@SXWa?|dsqk;BR}r-*]9}f&\q&\8c[G Wڂ/M3uE׍m ЍZruNkY"`ֽ/%K漦q+Ҭ\Gv P=sC.=xa\sK7>Y4k&='4ڳܰ\tE;Das-]Fx6{‹/^+R|oxK>㻿uhg/ctk&*kA1ڵ-WV|XhFxebsC)5w&jC C 1~x fJc\5c8kr#ǰmA,KXw}.鷗U4iTcqҢgcݬvw̝Tmg8.OKmmuhvy8Ƭˣ\e-cG& ƈO]!Y4 .l}z(2F{-Oc_:nd\kw,O5{l޲^cn{FhZ۽AOF\ZwU)en]]?BhA-)._fnTlE[E |fcfKWKI W@X}.l^ygCi쫹 v*Yהsыw=6ӜۚO/9|6lZ3=_#n}pM8m N{F*/<6Ӝ\\ l'JSR lU{,7< WN6+N]FOs 4 Rվj9zѫ鳙|䳩|4\\ lͧ%Eo.&>&/Gl^5+zUӤo-U1QU47nނr/M3YAW8C49Pmr_kkVFh|s3Vtr@?*]Jk]Jۯ$ՕY e3tsrՃ.@g)=oMћ&M^/~& T}o:j[֕չ&nN)DkዾLm%4qZ~wHG=@&}]z,._f.ҍ.Y20Sk%}hWEil-_I=qcuNBbp98zZJo_%.M巩Uc^M<{\jz#u;#x i@w6 /~ UeowN~]'P8& dI<{TW^BW@Igt|4~mf{|zkK-2a]2{ftYY~Aȵ1Y>x ]4TgaOGt#0E5oOޠX)5Z@ ɐ'm&eT>zSӪSiu6?N}IpHq%cR(+I~G6W |WXs{+Zhև%V9$NCMi5峉T>°3|]v`fIN]n s'vZ-(w4S tp>̔ G[V:Ƽݢ9~1Ut״m]֘iSh7F[[C TהDr;k,J}gLoS(Ӧ*:hx^75Vvc]t_z,@]o'?+igsqݪeMoq/EЧ NZ>x ]4TgݡO]'tnp@nJk3嶨4 # }VYzr\. 6K7|6ӜWSjNU56cVKe3tsrH>%_k߾rߦQC{Tc_8+!O^iWMGe]brn΍2%^l!=6|66W}|akB;esת=cI=%n;Mݢޮ`sUv,._f3( _לr@Ә(Иno*_)|j-RW['<vׄǮl>iNl&򷿦]-}rDu$e3 4vE;-C~0`;'.l 4u/|o'{Kpٜ݁\g]sǽHw}c3(y:h52dj x&msI@7n;ނr/M3@I'+I$ 1Qi-+f6 ~V+% Ijj˥Z0F7g]fvu|aOJqcg6賙F 7.M#>ܒQk] i]z>G^Gzgsy0)vl_'WЭJl|xmteuLؗ/]lAjNY峉bKӈV{NZ'e7ʠǮ N=.s`sT. FgJm >x U>TGݝZzjM2R+ZV|4B &ׅjSW#.CqsצEiȰge6~K!z:?3h_i.G;KZ^ D]ݨ &2/ҪٸF|U)*Lf2y>l9ۢT>TqU6>gFђkiZ)!V#B/*LxNEß5_NoaO]T\G4˦ *Uts.4/h-ڌrv]|2c-{Gh9H|\\^i7drǰ$6wyV;z XJSL٠)wo.`$p]ژڄl y峩L˺hßVMh'Uȹv⺖ՓW$ ])O>wMrEwpm%,ҪngW{4F<&;4 @;fSG^z*MMTIY[f]ɑV)g=YhIC7~gWGU _2/ܖ.\C%Qt?v6hl*ыvWY 8awhУ\^T>7KZ߇/e/>T^9'@ſؾZYU T:2&R}VT!*@]Zr_~҃J,u^~(^|8^x٥lz^M/wy?[nJsTVה4%㞽lT>`E?fgsql..屹|6K\!=QCSϻT1dRSSm^|ew1GO@yϦrZb^$|j?;'峹^x屹|6cw_UJMVg!kY^)cG]>ʦ$}ߺ}}*uV^ᵹ|67Ogsqyl.=wm?OWKMvKDtӠ N8nݠ/Me3.o{<)eaJuRĿ}˵w,xk,8^VL{]VxӴrzcZ5-VLy-OU˦ͪȊCeatXY8cTnhvsF1?e37@#Ӟ^`܉1W>gsql..峹,2^M+e[WYrp Х'њ)W5!g j{gsql.d[\ ES&㤪lm:ߣu.ewt%A^m|a 2+SPS<)揕uszm..5׳j屹ⶶt[:œɲoSd՚QwyֵvZib 峩Lu6XaYn &Կ xHV-(e\TiH9UjwOut'm%yշCTK&|Dtr5&巹z-:0<6MX'/ub$5h90iiqfA^>T]Əjz2Mr[j߇./wZ6Y+U-2 AY|rvJc6P.D+哆˚ceėeٴ2oMGׄ$J.G!T8}*6賩|Z*hD7PkvM>gsql9lHed2<ՕR[HIId˗fQlj{oO{&lvb_5d٤z(Y5t^;' T\+˪<-sb teVK;+eȓ'or0 m%5ێܫqo~>Z3{,80F峹|6\6%쵚Ѻ;oh52̅坦qA^>TgݑZth ^h;@+}Lvyr7L ; H+ZbUSeՊ)C hɤe1 #dԗe>2G\L}^ȁ'Lڣ6*lA]izI].W5YY;s޻٢T>T>ZW˧ 0ߞΥg4YkF sfz9E=t+#(ߚ2ٱmTBC\Suul޼1 ,}4^rϦ2eG];W]W ry/Er@f5]./g${wA.cnog;hY5B[yGiZV^"g!j[}ʞ#HG鵵6FQcҰ^哛EjY+& ӄg3FSye/QO(W%y_)/?7yt4aX3|c4Z=PRX>Y}rԅԞg`CSv05OdFBTP_WG!G3co`k>c {cg/E'T^_;$^-xg&%.J"&zեkd"-`ܦOOT6#2Uמ nBTݪ ٴilݺY*+e˖MGKPPT:.V[!l1b}\j^/Z^)OyuЕe팠It t6Ň-}xߤ{m-oWȓw^O}J .~_hJ5% guf|ZxA>WW(8ksq5f/?p"x]*}oM҉:@7ӑvRVWZYY߿{/^sczl..gkup^mXw99 VO]+|)n;MkuҽK f{$E=t]Uz2qtZ{ulBI^(.]'.%T.--˹n T:*QUV&Io~/WKdJ?{M'K5~+3=inNZ`]ve JlpLj79]6u?nN|RRy L.ͿbvM.s.,Oekf0z.IEt]vm,-QTF-RUr+s_ߥ <2դ^|/<SG?fP zt-.^ݶq$5)k.]?,{)u}f/)w)}|wi:J=YեK.~)G* t| V{i(*Me 6RR*Gḙ7@ъ=ry yhP]N\Mtco1tsr55tRظ}c\"q׶ݓwȭ7MVUݻZp+WB¥]%l..oDw2[֢ - tuVW?%˝r K*6Ϳ[\qؚnXɣZv7;ҭfsU2 =@rpMH-G,=Qj8Lp-HZMiݒIɞKJ=>/ؔheu˄iau=9GYlQ]gy-4T5n;+bN0`nL2䪊QV/zxZE 1lGdxޢ[L]ŋʊ˼4^rϦ2QA(l5KߐU3˪cd팡 /ɪZHjARLV6,]E C3TTVՊb=nBV.|SVxEO.+fDn\5cϦvh9[f/T\u{t sf3kEYmmr_{x]7S~ͲbE+v(@Z8uxݤK%RiUo 5F˒ofTٕO|&ٛs_6%^Y~:YeUm`wmCg/rniwfZEztH57T/Si^#.]h)'T_ ]uWv3-~7s/AAgSꨠ͉]LJV7LS}EY7{u&*>.r}+ &TESYE&nnvYjeX;c,Z?]zm1 i p] ~v$9R[2y&j\)d4ͯo`Gˢ)/5'%󽴟 47KdM;}07tY!s&3ȊɚM"맍4`S4=qd@g/ t n eQkg) v&]<9ZVvW. %.ݖ*)!\'뺌ҟ9Bj(SQi”uZS6CmԚ[Rt#(P򲦿 8/iSK_ydɶ?ÌE\f\WO~\G>!'&cq  p\]]]ufr,>Z*v u@oFr֬]dٲ%v-+ nAɗ߿yScvRoiq\ Y5.B2T"3 1vm=cVhqWK'iu9*ʼlY2s>jyV ۔[8q~QtONJڛWgvWo 봻;M^;YY`R5j{M(ݓI4i }Vrj6!KփkQgit.K)]Tnq%n=M%757jm.%:]@RRAIp\`[n2(6wwN_qw޹(ʘ1䗿_sT gR,m_?u8+9 qoE.źmt|i)e#oSB].-it3⠋ͮy@qi.t=ٝK ۶ҸqpZsɧoqu-ͧ$ZoRmL榾 ro|gʷ ϐ@'ph}ڹt&L/mu-bcMsYr \^O|7g,M.2o;7|i)wYvTе#L+|v'eA[-(nN u;-gL{;ɗ&qmcgi@Yz>@n]_]k׀kŒZb}i9S=ZQ]-]6DZ 25?eޭqMZt_yewoĈal ֒ܵkuoKK?iqX\~]hvct`VP>x ]4TG݅SF7][5))n!NM%]l,ŕeeӆ͢"OzUl/i4ݝUѺiNI\|p鴁 &ۊ8n&K׿UK+.]ݟ| X 0,a0yخ?stTO>>=v^{a}dϱ|*~KzK+3$T\l'_>x ]4{2n瑯p+ntM@7!@0.7S}ZdQƝA]/M3,6K2 lkz?vL\A$FmdI+crxY;$ \ L<űLd5t`^V\n?֮ T; u)!s>={m^|i)l\t SMbܩ],Xr鶷 [=m-kioqZ믰v7Y+Tu{›N\\fw 1Y7gWÿಸeFF-}T p5Mew撲|aPStZ]?^6pi씪豴3z3.=ٝK4DLwkufw _QbעҸnQW4^MtK'NF\RO@ÍY~,9pUw}}717xgy晧EZG񱌿+2`<_L0jL2J|h<7qΙqqqc\v1GY 2P{YWg@npv-(w4St;2 ք46.ŕe6O]@osImܥAW# {wOv'Ҥ趭4nǠk%% %l 8~?-`?_jΥɣhmv;km& ma@DY<}z)a h~S*M8|h}%Yر회2d] @ZcfjV]ICoю-ݳi-;w1ʔނr/M3AwWӭ uٺ EkB@6Ngsq-9 )%@q@0Ҥ趭4n["Rڎ[t98Օ1"~@Pާz@A!uxe hcg6e k/~!YwM~ĵ_0]VZadqawdW܂/M3leΣգ8Ri6e5X4kAZL-3FA.۾{;gsI9]1Mq]t"g\{;ɗ& @m]ʮ5TUv Nujaup 8cFn2*w 1npJ8g=.,ǹK6 'K*nhgMϸ|dw/MZem# %][s:N\':0Rj;e?E` @!88:q߭n?Xwy Xe?dzu^nx,Ń0pMل]Ip O4͔_,;*覻MW&@D峷[V@7|6ig3pIY2g7=ݓI4ˁkփٞyp-Rv^Ah-X)dp<  `x+FT@G\pܱIK-88ܺnńb?g?:~˱xs8vy.~}M+ nAɗ⾸Aw׵h|jWNI%Awwf#@q-8 峹|tҹz3.=ٝK趱Ku{] xdS=i\]c)@ rk Z$뀇sq%0xAwass Š_uYxs.a;c+mA˗^pY<ItwHm\>{+nanq&͐⢜۸n4f7n.I\IЍ:n][~Dxi\]?||6 Qt-KG(D85*fYwV' %S,MC9S8dbƔaSF*& j/M#˧s'C`|9uޖjNrAKQ@wѤAfD}"8voW<DzžU_!-3SN*-}i^ ŜqknVOF*){O*E:A+)u8ZsKqpujl2St i#e⩖tsq A ?̓Jd>kЇ:Fn·d>ëg0R|@7C.rMŜqہnI5\]Nn w+2˲3UQRҭ@7(+Mf32 lZ ʮ8,G$@TtS/[ShiOAwPE;AeFK,CdbƇЍ;u"AA4 ]ryx%}趢4n1 zIQ}Nf[o*yLut]؈@Hڤ,AkL4 SН?l4VL;"JWOˁ Ⱥ%[ȭNzKS6zMt;/MrW6vmz\G٬E>UU@nqcu?3/C.tdJӠ[y9:  @w(MhR@wև:2ߴ7SF^Ldˌ|^hM&@7s@e@וoVtmE]q]y {O uW@7(l2St몵95)6rǨpi@7(+Mf3]`-7:=KPVXz]@7?@05"]t(n6G:m9mcg݂k@ۭw>%~{NgvO.3rj|kAn d]{+m=:˧]7V-q-'"nXtNta״fstm̠[tKeZr8\&% -mBtr&3QAת";]񡚗@>@whԑ^nc*x9;)#+c<@73 b @DA- {+{dNwz] ]R(tr&3Y@wUݼ].nN-]t;j8. [,na n5k [趱3n9eBtKBnɻNwfEKk.- 9$ jT>TGuf*|̸#2T7&];O g*nPW>TTt tt Ѳit q ZtiH-m̠[tYN'vme몜6讌-n۶ࡲ\6mڐ TQQmyq0[7;l?w.~Xޖ-l[V> j=IlՑEifaYL;}RX[9etAV#GlW,4|esTHZY^h:Nià  M) 6\`Y^5/+ )e4 yPRޢ)CN;YS4I,jd>7}j͔Ahٰr%Anp,(6z hߓ tKXٕ`=Jȧv Օix24_~)Љ l_UU|a<Οp.raF*]A/=TUt qt D*mX>@TG@7 ޴ jR.^6rnʌewrٕ@h5(7[*Y%@KXf~@}Vdx>K8m¨N_8adXͯ$x|$S؄ +f Ln覻@7_.݅twQ1ݴrQ:(Um9ݸjAݶpԅ!%{y=Ҳ{B Rg Ok-7O2ށڵ *he(u_qqrݝY\}9ȵ򶬒ԺݓLuTеJfV.]Ƭ5;b(lS@0qKxNfutkwGEAwݪMF@%u!-̲~{**zpX=ں,a7O[ukUxY:xO?O\jFMx\۝>UZ_{)lbgUSIQ؁Rs AI!MHEI]9c4eF]MߠZ;upt]~@%uˤQ,SUyʟ@ёR*٦Ԯ.2KbցӧL~ۋd[Z^PZx]7l\2_ZC&s, ҅_A'=TG]Z!RsK [P [4l>u-5lSk4RKzmyAwg_Hw_WȢE \εhd }? \xX-#||$StwO-u--Ltv53@)A~9ݴz&jVw2]Qӣ=&R8n2pH-R Dďi1Eo|;ȗE9oӀ1zH7϶o}MXG8t ɵ^qcr?OZe ESie큲e"o.%nۛ/QlBm,g峸wT=i3osfsZ;f[V/|G*,)Ug쟬]m\vr U?_ǵLa].T>ٜVO/ & iV7Z֑YA3 V,ESE[l3,+2&d)ewh|')i. t 0wuq fbc,=BLSXtS͸u; 5&.QIIf53KmQE%,))%b2*p|ڵD^ye{ş ~?O7К51cFɔ) zka\胯SC{iLow;]h}xe2wPY3m<^6L7_Z6[+Yb욥dZ3m:>iCe;eʨr(hvժ k(l^hy՜R~+4㢫 ϛ4^ ѭtݺe2K+{tG_tQ4wϜ\>G0SGW*ظr(@HrpJtyZ܅ b=ͬ7)i2IwRҞ/7ǵn0Y8 -/D)NZY"gC t~F1R}vNq'Nn'Z t:DK-@7au-e|.q @0K;0d[8r#iff5k{Cvr$_.|.33CL13^j4-'yT]]jluKʰ>?7T*$nm=4}z\؞|W1=LDӆ|0n"r𬣏>2;uS/7u^xOmݍ9<\`s_0m~\%~ܔYESOvQo=w!߻Gp_[/yVK<;%N:mks:לS{ O|nc%]/xa?v,T%>OtyiJJ4~Qy;==~" oa"aL{mzt;Fl:.WLKݒK:UMt@EH$dYp)2bkK-_Xe>KYIDK4- mm3g;qD1ems{=z;?I=YA)Dnj${wٶB[My|.4M+|%Y=}s7=t/݃{'n؍kw&N:mVkN῞ꡛ~׵4+Eo]{ۯꟹ+>tbY%y/-c.Go%mVkNswroܓ\ǾMvzWv/>p?w^#ߏO[_~g~=~z}HJ :)mO)=W{?sOwZu$Zƭ|wץ?q}ȍ\?C8{~sO4{NltbT%ՙNMk'֫~xvf'ѭ }n۱m\]Ǹݒ1>#?=roSjn|d8G)^zJN_u7pߺ~kw/~.ݟ~^s~k/zMtz͹Yoks:6soO?w%vRʋs+S7^\stfwߟ9ǝFwibY:<~-c.?槽&tbmN'ӹ?w?w5kݫ+J1}~sx"O}~ߝyL/%zUsρ%tbmN'v̥s?r =a'%rm/2ʰ Qe]R4̣~k8aeK]Mixoy6>>xt-hJO/ʌ3d/٧;؏v̥KR9¾25t% l>wbM+=ǎ=fg{R/f5}Uz5$֫àmJrOnmAjC/.;ʋfi .ri% (0Rr6P>Y015fڤ&clʳ}Y[mmmurz&I]1RjqSf]^.KU=;|AuM^19u ܧcw?c/=+qXD)̈ͽ:rnHDތ.CCA"*#DehU7=b׺U᧝N<5jprcDt +- }idܖ!3Jo-$.u[+u0mmPYn6zEQ?X].=>mʓ|)K) 'f]Q9/xgwUW/n.ʼyMۀ ۣ [oͷ8plj?VW9=ގޖ8_u.ݎys쎡7Qe6KN%^]D7!Jt|)K=)O=EKJOҞYr:03 m1l6φb5J^n7XFt6m^_C.Y{l eHF%Zzk;SC\6=H5"|gկ~Us>5OO6,-f<}\OC0gwpbh+B >o2:7C֦¹'vCV'f;xۍ{sIL>%JHL%^[7-}m QzYN4m\6nf]cvAX*̷2& ~=aח{[?]']wzbu0ߓz_IQ?Jר^xZvi!.C!Ç/ arCM>!KY?0n+<^{UmK5zF2Xߌ# sH{^±uGfm V>&D7c~|~vGѥ5D77)$E7,pb.<]j0ʥa6K,􇘕Mϳr-lRbuX4I˯Q!6guVmKk[iJ٨1_2Ew(f Ӌѡ4H%XGt7W^_\pA^CzLD93! j%sDt!ۣ;յe)}DwɗRޓKte&&M&iLN֋Wz>aظ #G/H!u3/-z`O'Mz^{̟ /Pֶt]|o^֣ v{ݔ2O}/ᩧ }+Jהn?j'xb'JE/nkg^黆b3};Em?6N;K]bIy%tXa@1L˙A=aE[tv.ka8lذл`'}2vLOhRkz," }mG)V[i3fK mM_bȼg}eY3τynX]6lFؾ CF/Qz|RqOC K ޒ;"̟dL/v'婗?dҽHWCJ/MOJ#@ZVҘ;6\^ǼX=AGmCKHiZI&ز|a{[ӯkb8jԨУ8I?rG-YZn[57:xŋK-~F^{3?#эVDw1=v=DW 9gV%E7n.Opm%NL%^<=z;9n8wG'|^9s港|+1ɄkJmxD@ZtM~p^j=#F w96?}lgE\ZV[`fm殿52F۷:8~+C'~iHts{h@WZJt;s<S$MRb<] ٠G@"`@뮻η ^Q)c2a %2!H<czmi^ƁmK/i.aiG򗿄i@pi26qE_^|Ű>j>g]zMtiev39Gy>׿~h̿ti{^f~^<'gὋ=VC4G:ݛ?ޏVDA_Q =yԋO]CTts榹LxOLZ)}Rz]kRh#h{[ʙS{Ǻ|ARoE|H9`˗~ז!rW_]hmBt"vXQǙ3,|,G usI0=@YD뙈##Ͷ?lpc˺a[;.^`9mc=״)kgKwM6Or2_{Y=_x]_t/nJWXwc+CF r^G=oG4 *mzsduHLZ)}Rzn)_`EHK7YEy5LL //#?O _.(3}½@o+|'tɇ>C mR~=جo<.N?M8QIC}Ak b6 ;_HtDwh$&>)O=E3gNdy^D#hc p%K RȺ#J+cb1wͥ I1OC6p8KGs R"T"y$[Lט'LC0٥3Cz,r?*=\rGo0n ?DoQ2: gv.E59gHa{L?.)%"7l "o" I13S 3m*)F]H,eRXG. Y{Mxmn\Z)L,^3l&)ҽ<@JJF_I\o|wg}v7OlM_~Ox9B;U?$NHt#r"Z]D8>;y,/+G=˶◖R^6i!l$bvg4u[PжO&lCG~fk5ȸI2Q/۠^0 J3}nFM_.D=B4/ЙW*XNtL'"ooKKeN93Q'$]8xbwow. y E-^Đug~24)4"H!C^ DĆ2mP5uQKv)|1emd vk7b0&n.,oTe;DY>>>_+˰{twk^{DW!B!ѭe2rĤ#],\}:zSYd纟SO=m{G;67q ޞv)npˍ'߇e=mAۯA> N? 1:~Xӟ$\Rvt4rO<瞻w__uSp5}n B>:hӔ2;{晧w鮼~_+%~ܔ=ÞDW(B!ns{>O DL^~slګC*=3fLs_SO=_o喿.F~iKuweG;_ozѡWi^+IW]uEM/|s)Ԍv+C//eQFi?=ӈ2do5i࢝,qSB!h $!,{fO)7o;iDY~zGw믿6\4.C)$#CA y1ONfܤulUr"C=8<{ \bCӧWnn ҋC{Mz\y/1#.Ʋ_Q?Lx )GYdˋ8SK_bxLh/Ó{`,ER"B!-D9DKuD^WzeyHa,E=VNg{aE$B$OK+?[S]x22Bӏ?m7vrY)DS?Ev饥חzhӳ>>ð'p.='\[=wp22mӞj[rg$>.c&;JtB!ĐEk"H%ҳjw!em32Db:,~q.?6djۦM.r̐m:k+.iS6NoqkďҰl?JtB!ĐE[e=$e<`.>%J6dDOHJů' !bt[ QDz{Y {'3{ːy\{7lYXJ%_+%O#B!Pa\dW_D .'UiL$z)cR?J.eYi}J_+%O#B!P֮O$! HtwnS^>S8R{LD_+%O#B!Pa5]+ܚ5DW"ѭ"r˸ I,qb9嵞]ڽ@VJlG+BK\}}Ht#bVYCncyG6^URb<]!B Ƃ&H#ѭèڴH02i]ƌ2]Z.)VKlM+BB"""Ht'"Le2&,Gӿ $1kIy$B!Jt :#D nFEUbdy$B!%6]E&vLG+B!ZDW  !B@+UBbdy$B!%Jt1YB!h $]e($vLG+B!ZDW  !B@+UBbdy$B!%Jt1Y[] tu0BQE|JtfO,DWpt-wtBQ5$]e($vLG+t/j̚5|IG!DՐJt1YV套cQ!]2;&#1z .>]WɄB1P$]e($vLG+b BԞ.$7.B!@Jt1YQ$Q5Ǝ^]B*!ѕ*C!c<]uY.ܯ+D3իAB*!ѭKnŊenٲ4/^槓y NW_˗grQ~Web}YLncmMې8CS?Sڜk<^x9__˨\ܗ+VK)M=-'͋~Qk?X?BQ-!͋E ކ]b[7>:NALRD2lo iieHYDq12L[N=Vχ, iڱzP?uS0R>ӥ㔣N[rm<.%{JtE']!D:kbIen;C]nm.˸l͟ŪIDAT?sQL"!Y0{z5_+%~ܔ}(eHtE= ! $]ddDWx1SRN{ݿuӟ: W^yyخI `"'ma2,30x-cZ,GNmQF /aB]j7P+ HtE= ! $]2DUVKO:餲fݧ>ɢy]z>]x3g:uk=zoMDgv#F s;gߍ7}r06\}a? {'?pYĉݢE󶂘mz sσ"Ҭ7i^]|m&ryO~a>R"Q$HtB .!;EҳLBb0H)~[wI|[ ‹~_v[lYp~.?uם m fk*.^m6YBf/<[oC=t^Z!zpѣ/KaDWvZ>OuG`KȲ>}azp7v7xp)2oB]\:<=H3e"*9f̨Mzhڰ Ed.90~9^;/ӛKyDuè`z Lx=qk/=P+-_ƅ5 jyB?T$U]DGtqwNcdKiJ_LZ)4/]QDWBAHt p%4dz1{nzcz @qI4"ؚ"ӓS)ȌSmD|yHԏ~w\zuUF4c;QcM'p\x?{Ƕy"<)^^W/䛇bCz~F-f|{s9ILZ)KO]QO$B!"ѭ"]K*O(~Ė's>z{y1r("VrYntG1ǖ!?r0M6hzegZ/\Jӌlml3N IY!=8azOiZܻ˶)O2v/qI6㔧m1eq:h3ueh#rj|Y:buC[d䯕R|o]aHtE= !D xq/'T"&&c\Ҙ8243 4c6nz6m[- 6:LSކ}vMFMLMB]&V&]^Kg]kqkCغqkďҰlJt!D+bP**n\>V;e#BԘɱq!DWiĎHtEz!B1DWiĎHtEz!B1DWiĎHtEz!B1DWiĎHtEz!B1DWiĎHtEz!B1DWiĎHtE֭JD+JDWiĎHtE B1(DWiĎHtE=BԖpʱ!Ht%J'vLG+b$DWBAHt%J'vLG+b$DWBAHt%J'vLG+b$DWBAHt%J'vLG+b$DWBAHt%J'vLG+b$DWBj#ѕ*C!c<]C+DW!DJt1Y!uG+ эmid'/VOL%G+bەLQK1=B!AhYD79[t;vl2l%w3wsU!7k$ |)K=)DWĐ芺"B1.G7%׋%覄6;|{?LJ{ -ͰĖ ݆ML%G+bKtEǜDW!D*]K][vc/y![%>&AfM饷(~YHE۰ɗRޓHtE qᘓ !>]nݮ &.B-I]||߫Mq:N~jD۰ɗRޓHtE +]!AP)GRs f>Ls߮ [$柾-MǶʼn@$ |)K=)DW芺9B*>T9;,( R;O}NX3byDa/v']Q71'BQeGKBs}uۻ ]v.!q|p{&Ng7h;-OTfEye>/}e^x_O/w+Ve^.} #˖1+/:|AnѢ}a/^9d;aCt Iiviȹo)<ֆw6wwYP?h/^}ezʰ[g?iuYu_jEkeIJKmʏXe(ݬN #%BԖ-:!WmkLً.;ˬOf2e>i?$Ӗ Z&%HLL+0dɁAdpA|"/aK/*NnذwGMY5j m[@/\Sk#RkC^)χ˙1c Bi[i64]G+DW!DInvqG೵ˌf3zGdhpq{j*8Ƀ$|zoY ~zu]eyZFbk=ܙg:;5\D>= IdqKyq,~{-i6If ӌS?Q8._J?nJc"CF+DW!D*&OMp U>ܣO ,}~9Ӆx1&0u]D9@V#Ҁ@yw߽Z15Id=ӣKo/R!) 2/w]6k?э=s]5;=\tK/w͛֡n'xӟzhH_ua=w-X0M8>\vM/v78]Qo$B!LJts\c3Bm6ʎFl}B"}*Kg'cUS75A0I@=y%̣CC0Fe%\[.%,\ԧ>=뮻Po>O~ҿ6w]wGy(_wo}nM7os{{Gvo>~XwM7N>DvۄKI?ӡkaqRjqS#F+ʔn{V.;|sQp$ ҙ-Mrs(S]tK.E H.·fͪ0͐OBo'"Rb",O.e=v =qӝsΏr/O<_C[a;gXkwUѽ[v//\}嗆RQ'KM詓nmďHtE7]!U&|$?/T]ԋm'?+EfFҝQMDA #ҋ=3kvg^w䑇pi2"#]vIX~Ǻwmajtx=zugϞ>OiRe"/m|M4mb&Mp}ﻃRe^ѣ{%ˢMd[^zt!41n$HtBTRtݣ;ltGlJWܞdkʸx׆fy2=ܛ=A~E\BУ8_n=ww#G"2ڃ.|7bİ0enDu Y/v7}r$x L !t]y.7Nmvs\vĖBwّ;Sz?-V?,9$;grH.̛ x^t_-/\~$p/IaM/~0?aYe]ns, (!3-ܣ6[ϲ ۥ\l^zq(K񍯹 6X$zb W&OXhǸqc¥ܻeܭ#嵱(gRm˗RďҤ0ֺU}#/BԐGQk8t 1tHά^oÿݯvv^fyҲ~8r;r;7eMW#&GOMtR]dQ #}-Ҋ"CtE,7lzi{=u9DXX!ѳkMS To {zof,+. B>{}晧B]\OHRRjc&('/᭗ڢ3']!. 㧅f~O ۛ Az{JL^+He?=nѝE*.EcOgO~|{X}/O9Fӟ¥iewtߟLycRM?hy͚0a\ G?D6s.YBcƌrnC ]}az3zMwcǎv;찝[viCwc.L,]G+8sO!BbAt׬g׵{s7O [eHI䦓u͈&Hg2h<"~zBy@=H#bK/0SiS⑖bJp /`> e!ˬ͔avcu23)͌^ݘ|)K) DW#Cg!UDu]YD8K}Sy 'zjoT 7x}g0R!uN EG: =tS7ee}12,ӻlBL32g37._J?fJ1"HtEG!DUh6ѵn" GMjZO( H =-rS(C}϶Yq1f YfvO[hbjrkZstghm`K]Mix$3?B!>B|JtF]>ƑqD^Mg. EYFL>:&ӶBʺʹ"24Q]vY.KfY3QɎɗR3HtEn BԒG! !Lܣ}]0/% &H!RLADO [f24Bi{ ۤNM`:lM̸֓ѶM=iyelɵe>&N\%~ܔ&KtE7]Q?gB3|2M&ޣ|)K=)DWFW@pyGB Mt=Ƥ6n/v'{/ꁎ;Qot 1DO٧hLjc81Rj{RW:!XgMtؒJ'7 nXˌhLjc81Rj{R.ޯ C!EB4mpy]fʁGcRDƉɗRޓHtEDryDWB3&8;~/>>P1E[K]bIy$"N!DUh4MFn4crɋ\\n1E[K]bIy$"NWo:!0(6e^v..7fW$\e'}4&Htk|)K=)DWɋ.)V#b4ZR*#HnǸ]v#]v.3~_ј"ѭ01yR'<]!DryGBT*÷rۆ$w$[XزKZq{^zG\fAr3^~%<)͓{ZQѹG!Ā+u2"I${!Zb4Obiy$BFsf ?q%B &ԵsI=^rGr/Dw'y{O#B4W/#y$B!֙F]e=v۩ӕyTfn^pw//IiHt !Dnx/\$Ы.WevM̜Dw'y{O#B4PaTB!֙FݔɝNYzr3ܽCnmsCD;hɓ< ! t9} BQ1ҧ-%ГzN^2wuّ;z|y%&OJ$G+hD'W!L=D7-yaJd7/tPΆe^/ .}%W_ӡ|(/+C93/y,啗 2nۤ̊B931&=qyR'cOcDw{FMڢTB Yj.̰~s2;q?1Ы˃"aex_?/Ɩ5DJ L ah2H7aHXm֭\^1fG"~|>.;m84y=~>ɬf}__zOÊn"t*='CH'CXXΐԅ"L%,~ a۵ظ&<)͓qYk]!D#\CMm~B Ij,A\Z/3I~+w^.3ҋ/<|*ys䲬D Cq}{?Px"dɁwA0\奬 +%ڥψ~߾6ޞ dܖ[n~˟/~ܗ-[ۤ,\ mIĻTJ1Dw/yèCZwryTnQ\FQ/WTA7"#A=`w=|A=Tx4j|_wn{C=N~rw]w~M6]|ag=C~#;昣 7&OJX-ݡV43:ԇ5_1ԨN]:f:2v쮮cϨ]\vI&"M%{{r"22m6zVZo70R6gy[FeD:zr(uLr&̳mG}lrg=zeRiB9iM=RqS*K1So]!B1d֭t^svn7ngD舅~:nt~kSO=. KܙgYaRty1InDwt'?Q(3vhw嗆ܯwm7i>{Wo.{2莎\h_h 4">z wFp?hu#F sf>}]8c? &^9&oJS|B! XtwtّI^zes)gSG.W CItVDSO"4dgvgqw߽SaLmO,.Duo6n.6]{aviAE.{SX'x,eєD[{:;CĘD˧7lO}2ԋS' Ϲn]qea=ː[t\ޔ)kS]!B1dFl5HiKB2=y͎rR˔rNK/8"o~SNr7|S @w'ѥyfYzp/K5\~mS7Kʏ}ARbKA@-,"= _{[_wC/1eXzm7)>fz Z+B!<_~yԔ W"y]$w.ң˓y 2K^zWe#CzP$ItΝXzYv=\RèJZ BxZ9y drYi7nFa۔fDvX^dLs4u^ߘ1B2z,l6Q˛3=/B!C^H&=W^yyזoPI-.\LUDg{ܓq<䓏u7ɓ'ǻmt AP_b>ʴo͛t;Cx`bJ;emzg^7cƴЛS6"{g`mDb7)?&ca_KtB!ĐZn[nkkktl2#_/#v#wّ;0/^B'g}O(~'7L9J.Q)M"z;8"{l˙M)""}ԁ "\* 14Z(KO2!Vb;l{T)CYP}`CS,V~ZyL۲|f;\Vu(vYNɛďҰ%B!bӗ߼ЫrcN۵OYH}]fn-Nn+.Bg4Z%ːWND̄z194ԁTRRz)geYF=,gX e>Cۆ>^ QC=,mIouRYei}f(B!CDwؖ^n휶&2(H|ZItY!&&"ʲd!2hF3|zrÄ!m֧ Кdr˄+(6(rg34 -ic}^ lec)OS=a(B!C>{t}ܑ^rvž'7H}J1jI%AҒZ2:&&lq%4Gl7Y>+o2յ:3:m>孍.k8RK.)G;oz~1˛ďҤB!%r(/J.Wˌ3<]97ja*iɍI뺤IDzYMd!rWkU6Xrg=BHAPQI2LRz|^ap-8=vpo-eXF[麭mL[{#eX%)[*nJe޷}P+B!,}.O[.Hgn>c*=ɥwvKDtYaK)2HXƐygR4% aC[dPYQu1muXmt XN(ϼ)O[]!B1CtSxᜰN;q/k͍ː].] EPŵ4"1._FZ9D V ZVcQLSl!e}g̷R곲Ð2iֵg#:Ltӱ^_/ҦT[=e(B!CDw̎nؤ=]ǔ}1N}B9|0)`/#}r)sV&ݸ|(J;&#B!M _Q}ֺnɼ"nڵulEuK/ ]fI'8懅RIOEHt%1YB!hJ I.ˌe{ˌC2,-KL>V !BdDwMDtgKtOnvBK(_0.U:&vLG+B!u]JuuX/|F2| M$WJL>V !Bd{r\ nnv$+11PZ%vLG+B!uk͍0 n2ö,n]n|oDr%1PZ%vLG+B!u5n[G3w\}.ꉎ?Qs8|$];&#V^Uٮ;YDžbZs[ItKD7$eόT&˗'e.}ŭZ½A(V\^y奐e^eL6(K/+P晔P?u2q,ccu^gX6z oC|B9^ͷٶ)kc~ݽ'&oJۧI]!;'_ZvM?L%U]CHZ&MǴC#&H"LY4bk4ApO](/|Reӡ-什]!':Oz!M?L%U]ĎAd.540ɇ$8RCN8.SzJYw=wwo}a{ =֛˲{˿6r:i lumd9c=a-Mb?\sϸYs\rQlqeOMb6l&0PֶoNY)DYI^/Tܔۧ 1p$<-DW40VAtӢu{ݢE;WwWᇺ@ Yd<`>}m^L BiI&4fēv}.isw߿ tn ːi> ^{ F6Fb" MH}۽o 6N;PY6]yapoznܸ1nΜY}C='ss:u>}v\.D yG]wb3O|,C eN9$7y-zٶpI5O_w6)OlG+Duz]TT[Eb38-~Rezq]z_XlWD}[='w_|aO.S,˲׿ƶ*8um@,gK;:rꫯ, zw/w֧m!b$.;1yS>-DW#DiQ/$D =.{ ܚ3NY򨣎(,;l>Oy-DW DiQ/$D `(DA5%&̣Ǘa_M\* ("䓏zNY=.2~uׄ\..=W\z_i"-M6Rk9sDr&_LŞ^Z7p,Ht8]QOtB+ Jt zrHdLzk_zA鵇P1==Asi/rB;c\b] =SL O]>$zwikL~^.}~mæh3ѓ˶'2 ܳSg̘Fdӟz~;6xÂt[~˛iy$BT':Oz!M?L%U]z8Dz1R]EjY erIǿio."O~ c |2MYo=3"~ҽ= =ܣ3N^˙g~zZ1{ėuK.'5a]k {}1yS>-DW DiQ/$D ЭY ԁ!y&~,g>HeK #\BL=ں&.L[y%i!v,Q?2߄qG .֣-öOk;iiS*OlʎDWE芦*n\>Vk*_kL@+Ӣʏ pjYV5kWn_*J?;&#Q:]zDWE#  FDWR(|WɿzDWE#  Fb+]E&vLG+bVDWE#  FDWR(|(+kC ':OF@+N|Jt1Y>HtE=yZ4 GB%>]e$vLG+bw2$<-h885Ht%ʐJ,DW(]}o]QOtDW4}XtErbdy$"F]eEC+Ӣ芆SDW ĎHtE nHtE=yZ4]ppjJt!1Y!ѭ':OF@+.וݵkDW ĎHtE nHtE=yZ4]xHt*C)c<]C[9]QOtDW4]DW ĎHtE nHtE=yZ4]xtnWME4sbdy$"Dr$<-h<$Ht1Y!ѭ':OF@+!{r:%k6'-iٕ* .8/J0 T둼|azDWE# Gè!)-RAr)( o֤_F(UDWIqn%HtE=yZ4]xDDw*jmkXಣL$w>[͍^ah.]Joi-[zaYzL\DWIqn%HtE=yZ4]ppjAt׬]|{͌6neh++^z!W_}9,6ԍhJiHtEVDWE#  F I-/\]]jE22:+qR#]&y% DiHtEG[eezs_yc8e3χX͕@)HJzh$#ѭқrne2eHX͕@)HJzh$#ѭr_.'3nK^Ė^Y*DWIqn%HtE=yZ4]ppjK"[ .]tSaזi5-^z;ԓ29LXY9ryc@)H#ѭ':OF@+N> EYDW^;Ѓ"v2K-+Ar^ƙgeHsQel ~t>{G>nGy=a>jfͪK,MPf9ۢ $3=?rGL)϶b4O$"Dr$<-h885Ht+]=dzG4",c$aGIebe&ҥK]wݵ=:)o8RnLDTYF=6Yѽ Cy~umx':1yR']F[9]QOtDW4}$."ZcK/-" MG)PڥŴ$jIX2_}<ʲ3]rEnspyϻܬYLmg?;'l܆۳nSdFmvۭo7 oLDWVDWE#  F:<bCJCD$qs)3beLǹ'1##Y/$-'}} ˺GMNzKwEK)3l+_ }mswCn-ʎ7Ɲqi'xp1byLDWVDWE#  F:\lZ'ilBPܗuM_L:$}H&eGg>KO;}'̣W1 ⊸0ui!fM7ܰa[^>1d,ӟ׿U7~BٮADWVDWE#  F/mdf4i3ٽ|s }H& DvCdq!G1<i XDRfs3e] gڀ]w5˗'N_k[%رYg l15N\hc4O$"M翰]QOtDW4}$U]Dݥ)#,CdYƥ"CcrRQ%,c].fO1Lz|=أ˨Zz{ o=S^]zte~; d-Mo܄ B~× 4;.ѭ':OF@+N>.Iz6!=ȫ-{r\^.!ƈR!eI"2d>`k=:Lȹ+<3q!RH=n-!g̙3 ~_$qnȁɓ<4;.ѭ':OF@+N>nOD֞@ңK?42~Kז#2)eu0D,u?O=- OQ/_{_}gYs~}o[MGm0u!kD#iw\[ ]QOtDW4}$]dIDJT륵ˌB"CHD̙zPg}dzyKXF} ztty!zg_~ ^ݫ"{&O_[I"LeL-O\] wSL hK4G$"MKt+A+Ӣ芆SDw`"}v1N";2zsfu*<^PN~裏 #"< {xN8Νwޟ 2pR#L##uhs0ӌ{ 㴁IiHtE)YAvEHtE=yZ4]ppjVedaD\IOSuLx)ФɦIt](9"Ht+C+Ӣ芆SDW;JiHtE)ʐzh$#ѕ0qR#]QD2$<-h885Ht%L\DW"ѭ ':OF@+N>mܲ/y~vά4c ~F!?/P0EHtE=yZ4]ppj"HeݲK}׃(JzMdo!_p/PDR$<-h885HtfEեח=`oxAxe?4K(02<‡}PS+#ѭ':_F@+N>ݶ>}fz]^5e˖DAQ1i /2VDWE#  F..]rYO =o,!rK_ =.b(͚%6._ՊHt+E+DW4 n^t/]E]`5f7צMtWnHtE=F@+n^tfL lnZݥ?Gwk\ʜ\(͚e̯R^tEVF[)]QOtDW4}Z^t}|&ǟx={q?{{}z!w*Jw]҂!|8!E=1q(]F+UZ.]#]~ߊUуDC>#ѕ*-·E {HtECiG+UZ0]#m[&EHtE=ѱ'ǞDW4$}Xt7)›\ RBdf>ސ*X#B!-AޕnmˮytzˎeFnDnE>M`w-Xߣ܂Z*IL>kX#B!-J۵vEAt;Fl2ötm6u=ّ9݋p|ۻfHt%&LM !B ݣ:Y[Fn峍yner0>aw:F\H鲊6ne|JE lٲ4(JwJų։<]!Bm:F/.nw$\Rz.gNߏ[= {t'X.U1YTB!h WZ.]$^Z|yQHf]]fn>{tgܞ%{֖ݠ;/Ą2̔)*%&LM !B vwٱ;y\2N+$;iߐ]';rPHnڒ^L9(mm]iߴ$t'E8d|dBzxY#1ebm*DW!Br.3|}7|.w'㳟k($e67@>O?UM[c ןulVʹo!]e`g-kSy$B!%֭*v;f'e2vO2~\f^uIdQ|o/[y5^ލtxm_?\zTk+|^en4b2/eW_E_R+/2Q䓏2gRFLѣG?:iGWj υrgCʰmcz͚UAY0.$ᆱ'>mk`] nJ˨M&mY[C?t1ڵkkf6Yegmey\>kqY^DW1TXjU~lY67oqYteaE {T!E|;1τ YD)CAFJ3gV~e=ݽ]l#;#C:;;۽؜P?a#u1Nyznކ2iMy^'AtFº&fJg=^?u0Mr^e)<ֳ}zaa[:)OV!l:X'dmcrm1g-S|,kb(B '.ۦzQil姌=Wt!IӁ.vK&b&e&WV,͛oC*+D*7lX{gBrw߽C='w~x C.= hOk6^mf(B V+LŠ+c !h\EtIC"ӈ"c4r&3M2Rc=[yr%B򫯾2p2eȹSN9]w5nd;ꨣݝwrK|ƿ&z;$a}e]?ays晧puawn^Zʞ|nmrv{٧ݯ~ eȷׂ#>u5 3>áSL uvD1~1eGp6q+/6<.LSb(B j}2M B}g뛆]MM7=E"ǑD.]!{9^[EH$"u]wl=SK)Z纟a.s4eaTȲ= =Lӳ1_}ݽKzBf#6va;6oq0ҋC{;o3;ĝ}e]oiOB.pZkgZmP^ڍvA|)Cl^˶_;ȮG(ǶYtP+J< ._B}gƿt9|9;~nfB2k!,&6D01.FrcTme_M/'Bѣ? oC%p/"jm:_dҖS_ L/4u< )3z,3AY74̼VL9^+z02ey&G>aEbyH6Yu~#2NO"Zf]]6('\!h<ص^moՎuv.;j̨mn?`RĦ Қ_|n$1eٸѻ!H+ܶnEl>r0qiAԘ]D@sSH/ro*Q"-rԋk#~~d;#71oKY}+6m"[oeis2= :YEd2gzi EMtcik R?#ƴk,g-S~ YtS!DH#k}ɴBީ薊lah=_6?̄}]f^r]nQ+ht.UH]w9<{ey"1=HOUH3s-I^x~7Dڊ(&=H/S1B^VbI~2YiutқCvs\zTy2eMo:0+ڋ bbE9!i)ԃڽ֣K]rEo0o+lSw;nĽobڐ]{Yď𺆒 !DKuɠB4n--3ɝLZ7;`1pױޑ}Ѯ}1+&247T֩$ԉ|O/r7 "]<r_~>X QG.H%D>NRœ/myyeYK Ei,WgĔ:DiӦKbduyB3},-sWxu?(3B\fk"zb`-~we +Myg-?nJ3TDJz@O!DQ[qPG^r'&=i˔s:ܒܬ75"dғu?eĄzLe>mq#j2geXN2D Y$SLy32cr,6i0ʙ ˦ ˙GQk(G;nuJ^-YN̳60G=z,eYď^.L!05M!DQ/ѵ{tS'ԃ\#\vQ.;X]cE BLLEԛ7=h2gܰa[&a>C+r_.hCi#֣]&kqS^;a3nKzUB+E}.]&<ʇaT>S8f&(}\/& -"&iՁ#[0fCY7cd¸)Q&m(C<۱6Q<.G֡nY=9e:.mrk3m=̳gӔV/֖|24A_2upO !DcRSѥזr\f^r˓ulG/]6J' %Ĩ|~ѢndD {RgRƺHȩcu.vǺHie%V.azmCY+v,YYeI/}L=yFs6(xL[<Ӕcӆvywz~㥧fwCAt֤qM&E-E2Ћ%w"Ƀs6 '|Oэ+J'&LM !BJE;rk?]^tr[\/ARłk^\zp[Inǹ)uTG'<*].c>I4jbYTB!T"1;$D7߻;KG72v]<}k_0E d6U{kOXNq]ǂ$J&&LM !BJec]I ovlKo҃c2M?M9zx-Lgg%۾cCҰg-kSy$B!bRѥˣv*J m\1m]?N\f.;Dr) ˕m:-I??5.* |26G+B!*ݒk"~<npLKC.KyYM%-{q%8>=6<68 I.;Ҩg-kSy$B!b.=}=xpyzKkؔI.[${tfc3]n~͎* |26G+B!*n=} qxpn3@=XzfD71I']pmxjxYuDWi䳖<]!B1TDts܎!h<5qu8$7/VcBome~BOr Nqys m`эɇ*c<]!BL /εr#sHޑ~|䶉A^psStz%udGb.a^tMvš8/wr7.i}^pDWQ !B!0{#rEw6^te{ɝg}^n'[t+w N{Jқ+U$vLG+BQ=V^5!YU e%w.3%T97=yTLO!˙D;Y/'~B(rezy%ҟĎHtB!dQɋ fLe2ɦ$'+Gwt4e] ]EObdy$B!GߟUaTr;qwM.; ؆ʧ_;'=]ƻ/c))z)+U~%vLG+BQ=trn]\fǝAKK!G$Ktg#t"^pYw( sYLwBz%ҏĎHtB!DztGozq3SNAtr4zv-J?DtonnΉr'K.ؗ =]EGbdy$B!A= h~1fs>i?vb/.Jdѝ }.a^wrs{r"sZ$ׯ?HzU~&vLG+B!ZzrUx Ne'ڧxe7ߣ$7rrz{vKdprdD[pk \l==G(H,DW!BtWM$\v>OV>|OnSF(H,DW!BýaJr}]/3yqAl܎ e ܦ#U~$vLG+B!Z|DwsN r~J{s9|=eb[#c<]!B Xtg#> k]pr)CO.a8*Ht !BV`PDwR'wSfB~.mm:ݒ+U;&#B!@UDwO]N"Or n-ݛ;G(UJ,DW!BG7p t ݿ?K-Ҋ3_Xj(AވJBqb(͑7Ep([B!hDԵn4Wݎ^p2Ã]C\C; uEF$ NbZI$RHtB!DQMEr^r{ \p_.Cэk%AQB$B!٨"E6%er ?]9*D(UDW!B4!]/˔C::wv,8);ЩE(UDW!B4]._Fr ˥H.o=㻟~OT\+DWQB!h6KtѝeO{X(B!h&!Ez!s .#yZ!ʑJtE+)HFa D"7:O !D9]HtEk%/\X BQDW(]Q ?x D`.OyZ!ʑJtE+m B JDWQ$ջ.E?֮cy}B#ѕ*DWQ$|QC8/$|-YF7DWQ$a\Z8v/tBrMt5Ee%RHtE'yZ!ʑJtE+z"ѭDWQ$'yZ!ʑJtE+z"ѭDWQ$'yZ!ʑJtE+z"ѭDWQ$'yZ!ʑJtE+H@o [vMX}B̢K$RHtE:;nyZ!ʑJtE+Hq#57*DWiHtE:; :O !D9]HtE;.u$wB#ѕ*DW]QGq'/:O !D9]HtE;.u$wB#ѕ*DW]QGq'/:O !D9]HtE]yH"DYn8C>yZ!Iy"ѕ*-H#E[):O !D9]HtE]Q_$B>AVϓnDWb$"DWn<-Ht%"EHtE}VBQDW(]QDWn<-Ht%"EHtE}VBQDW(]QDWn<-Ht%"E$x^r:+/_<-Ht%"E$DWne<-Ht%"E$DWne<-Ht%"E$DWne<-Ht%"E$DWne<-Ht%"E$DWԑk±OyZ!ʑJtE+y!~Q$B#ѕ*DW!uG[:O !D9]HtEk ]!j =:o]tK#U*D+J芺)GtBJtDW"uSD<-}#ѕ*-(E+nyZ!F+UZ0]QDW N9>BDW`$r|$Gi!DWiHtE)]Q78HtB7]҂R$npo$]#ńXQF do$]#HtEVB7]҂b$~Ht+Gi!DWiHtE1]Q?$BDW`$yZ!F+UZ0]QDWn<-}#ѕ*-(F+DrtBJtDW#C[9:O !DHt%J F+$*n8E !Dk)_ne]҂4ZDW_EVDJtHtEkA5# UJtHtEm_z#B!̈́DW(}F+B! DWQDW!B4C^t[XzDp}:tl2I2sܫ^q-{-}u[ƫ^һ׾aIf :AK=Dx&=ȮDWiHt/5k&B 墻LeL] ArsXwoz$SH7\OJvzx]KݼeWR$B$W=B!zOKЋ r{p>^pgzEdgsޔd}?8O=| Җl$öd;,nKoݐC2~ח$U|$B4^!bp(Ix].Oy}b;hvٹ^P}rr NO XpZHnӾ齦c3ҖE3bd#tgB6,M\&;WL$B.[^:8׾/'Hm::?RQ4Y̨\<;H!&Ɍ;b,Cdw],C/K{*-GkB!УA 3q%ˉfgs\Bnv^Izv;}sEiˌv"i2/y-˖Jt!˖&B ND7}.ǧrnx^ѝޣk*J=-D'} DWiHt/oM!<§lWaT]x9!~B(\/3 קcΛzMczmXn=Hݯ}2ikܣƋ5/o,_ꖾ񒗂eYPfDWzB1xo֣;vSyLϵOᧅpvnv֑!&1MS)N%RHt's?e9o*J4q-DW!B-}ǶLt\";tЋnMS"/EiHtB!D+0[ܴBϮDWQB!h#^jKӛ%]E@$B!&bRK%K%2HtB!D+0[zn-ؾƒ撓%2HtB!D+Ps-=95S|yDwމ}]EH$B!"6n\t $]!B ]݉[̌\fbL̋E[YGT[67&;)'.9rŵo $]!B %I.ۓ%7/InE >Ar78õok ›n:wvam+XʽS^x_q˼We'sbRDW!Bq=|2-̣Lt%_-Lulxf^xׯ]Kۖ mav𲻾{7^^{KAAzBѸZ7HJωIm,]!B O ^pz͋nron^nS٠uӕyTǂ\F^t78eR Yzavrm-|c6u+Vu[˽^lN55RbmVzNLjc !BV_yUAn{9'Klr;l3vnp-89YVMt[H?ܵr[U5/[okRY4pbHY9}DW!BvkHfE[rSAt&ۓGtFH.=Ar}r$7[5͍յul27s÷t#WxݭX[p soY!+7m*래"B!@\tKo.^Rm{R!=!XpIO.ATy\- [ayʵeg~'+n5եz믭h,}uYj&tbmVzIDjc !BVߢ;7wE׏=6~˭wjv̩F- kG~j(9״e\iuv9fncӝ];6^i+%O)zqιI/ k2N[nkkCFa}zt}#fzB9oOg ˘g4nh9;g+{\vBچq bF{bd3aac?/)aΌ`oӬúO[=mVQ$On?+Fd m2c6dGnFm$;jSˎen۰ywFoh^uNfVIFo$Nse#6zƳc7.$7:T?/8동tu.7;x?>ΗecC}?GmYQ~5N4:!o$=NgBr#Ne'2Sri{;`C]v6['-C ,$˒&I6<|j)}3\ L$w)ݗ5R5m,# wwq >U_oܸ? A4 *5L3I#H)}Jt22|q?hFtVF^NnFspK:eGnq1me˴a,t/$=/C+W ym_ל_$W3cnh^_z|چ^}.,0Ŵ|/хleru{OtL#BBb*4.G_eu_CXf1ы幑aD};x.8岆ɍ߮qnafO\貓vvɻ$/鰬Zi똼SK}(Ɍ]C2cx ~G֩$FNƷ13 ֔ݽtQT6&L8?$$IGI'&ZX 4ɞ!SNf|//}*zq=* yex\n %9/niRK- ?<,IR/I\n?*@)I pu'ٝX+}Jd;f.~?zslmGldAhjC9Asv br#{Olf&J{R-NM-,Oki$uSLb;eLB[R;H.= $wMُ;u{i!E*n/>LpQe{cГfᒨ&IlRb$mK'=!Khb=.s}J)_ ÖJe ^sR3IK`$͊¾L>3n !A^ iIz#+,BOd/PKxvǩP}!͔V:}T5P1+N]r^rg,2i^9~:m =NGt84>2e$7x=4\uDH]ܘIn&n:},|Dw ImI_fKltIiac \VՌ%^Ԑ?J%&7ʟOcPLT?7;fNALl$\L]K`QBĒVB;)۶k Ҟ ab4/2.eM'Y&Bd='Vg#%tb뤒CcH72Vf5uR)z&pt BnT9XtPt ]/$7rwrsD& AtcOJpS[v_]٠|O..KNݏT_tԥz#6T/I={D۬4L6˱`"Y.1R#y 7ZbǞҔInL)1zd{$'3ηk.7~%a%}$ZV/ld JATRnAJ='^g}dmI:^ؒ*Y7[6߿69$|>ے/ cR6{DֱnRl >9֋0a|<8yr86Ct 񲕞#{A$"zPM2ma8/穤%'.Zl]k޹QaXٱ$D98GK @ \HDk]﮽۳;S3{ F|gm(fbQA0T$MŲ+'!13"1Bd dH.KnJ.M4'wȱ- =ۚ~qؾJ2wI7m7יq˽~h< [FHnsDv|À?`ucVJm\V2Y`,š{jE{RtA/ݑeL j538cHt=Yr{4E|1m CfVf!Yr]cPtWَ1cr[`+sMaО}6+kggKPQF ׀F~*ruMt`װ: D{Lz.%w <&5-"pcm,0&D MnV͆'/Uр8DU;`raǬJ 0Atح;ɡ D.Ÿ%՝9x# WҲfu{rElumOi#,8r=B\rgVQIENDB`onedrive-2.5.5/docs/images/linux_view_shared_file_link.png000066400000000000000000003044241476564400300240140ustar00rootroot00000000000000PNG  IHDRq sRGBgAMA a pHYsodIDATx^`׵-l}e alJI@ӦiҴI_64 :쐙1J2[3[dٳ;ZVZI+=˰w9\nY!mnݎjGfVѱа=2 nYh;t@> |8dxǢIt_2> 8EcR#֡+1FW'9{CoENSn);%+i puYI8;M4L}9qds~a?px'"g8ck |!ڿX/滿,YH5zW碄i隃P. 2zg|qe|2\s$J)}CWHu?ōOn|o 7uN~)1eΑT[/ң=@{RxRѦH&IHeqde}ʥK@$%!u0zGV!L¶'!bi3$~M4!DFD޲>?¯c&mA*yA#]_!(+TL& aYv>'jh|7$?c~%~$9mtd$;AW$ݺ_IG@ׯbPBhxRfhTƈeH4 g1!YY[SdX妙`6ѐD>M e[JCc3 wː HjVSbL*%)hG% ȥ3&A4w6,%A%G2 d;R T‡ ;ҏD24B~Whw{/wŹDz|WK@rϥ1uqgNw n>'9Y1"CTF(DLJhRnAfMV=Tz9J'pJ)$"2BU,~5dpTqLLJC}bzᰓ;6{ ;rIJ>8: gs݌![ `4k-0"1d<}$Qly~th\D/mw}>%1\y#dfdw ؁b=ܘ L}sb0Ư Q&a:(B B$2ғʎF6 [[W'mHeb D,?h%hE+dYQH.A uÐr㤲SZ Ͱ8I)|@f}$$$&ђ4b*\6&rvMH3PSK`!= &$// JJcBBLu gy.}Kz|;ߑ?uW wCOKOI $S&f 5q+[_4ɤ!HAӁ""atnB Lh"KZpuD="d' A> G)#\߮6H60!0hGf-; O#zioYBTDTp c*pd|hrZNk XkCX`4O|8‘8qPHoKLzGGCS Rifm 8=בX9foI'|ǕDwrG i^.93nLұBEDwr1ރ1飱>-qBP;bٻt) R٣;oڱ)=z0{KdfG#OETx6&=vH^v: D7y+Aa- Y9+=?z~u8Y<%Ҁx>tƑdݎp$HG G98U>9"ȓػ}xǩ%sݧ9{;9|P?4Ǔ²w=z)n,n?(~qdwLw@3E* N \ *OƵ{uL58:'C ܜNo o TU׊+Fj]ݫE)'_(dtߑJf2KzgT)ݳW_yӍ""v޽{ʙ;PFo$~uG"xx t(VDU.ET!kZ ֙D5cw[jG&!MÈJ~g-=;Z:U0ۼm wlx;zUڽf|:FOT4PUSv٦훤F_6#?w,ud#X:vfTw \+;wcʎdkpkWb݇4~GaN bC;wTr!rL!*MzSan@Ttg[bmQzJӉ` cFA߳koNkػxak@]wܽRy9 %V3ݳg޽{;UU54~GfA64Yh&1F=ˆJFTDTAE["H7=U)f`ˣ{8 ]%#*Mnw]A5R!6bD:{!UU+"lZcCd4s^d#۔L:{wާ~q=󰓜,_*+[lru0jfU{=hTJ|4֖'љ*z(OQiQuJM.KQyIB": *϶B2;uMˆJlKDYƮ){UGrmٱS%;ck_kl.}Gm:%7o HΝ:ufWn$KHϞ| "^Jg|6f&H%7펱Fa@EMBhCD Dh>eE$G{"@yx5) %uτ=Ut{oCڳ3}^ %/mQm֞6O 9Ty@Tm ^G_صT\7]ڽՙA^{:}/ :KYW;g4Ӱ_1Ld4XuoVٴydi3#z*#%Q1ǐυ*:`<@M#fg1 `2m=zd1f(>|7FF.FPde[=(Idz2M>]Fȅ q#cƎP5'Q2a8a谁yǏaÆh:t{t,g70}qϤː!Cdԩxbwwfٱ{xxxx?طe5=/^z'ςgy[BA93}TykjY I|R5ޱfG ]'%;JV3X#Q2D2CTEx@.BtilPk38Q)S&[3 ,Iwm88aTTK]P@i'nkǼ7(k֓#=dI3K`ȍGǀkµa} }/{0E3! \<=<<<<hr3-0 m + b:14RՏg.nLO*#qJ:#Ja}*w6KI$茔e7oTر]]:dD4b?mq}mC&N 1m.doxhdy4%^@Z/- 16KqzRd7=!d.XRk5JxvѾ` *q]jQ:.13mV/& |R<7qʝ=Bc4I'$6U_X6 FLnCjm{*1ʷy%[r#\H6h>KPn]1f,xt zm "{k`oK%r<<<<<|ʁݔ e30ޖ,\3} `/5 ӓHz?M$)yIYJtNLeA7nOư63|ZB`!VR'>n+A(JLTB*óffժU:R ^id6J9^ 0ݔY6lb)tn}/#z)wר+s /lCye2G9!DCv^6@: ^6N7ᷘ>C4+o7~ꄎ>IR"V.c{ɷ͖JKΎ~+v^FDXgvЀXGJ]RW[-l)dU2rN_,#foV[ߖ!VȼRsr?Fv;,*(ŰnT;K*й8n 3F[z#. A^tHbqiy\RI\BlA&әb=nܸQo a?yT&a]Z(mߤk#UQQ+6Hنi2[ \J,5 NzMS\nPܴ 0x+ HpmycRRN F )2(i ߴySC^ovh J u2Re yIb)XⲥZ.KIqwA$ikܔI|Kuql |u߁`_8(Iv 6m&KSKYEbyry`Dys<>;5l ELJ: %ؕP8To::6rBp43Jm14ҩht>V@i4 [V:*H}UM1So^%߸j<}lܼI۶ ]G ӷ{. >+V[Hir^3 <%ebp`J} W^4 x gם;Nq3S $-qH]\jzhTq̀I#f(LJrG&/7EZq @Gޘs6f!蔳r•UAJbyl%@3a!guĤ}hzCv'aQڙEމv FP&-o`6F*_>FvbTZ:L'bãs1HQX70= xsӭE\*|+򍫾%Mp"}DF\h0CT Zo>eIf'-+!|me5.WeKvWږZ;ѧ_l%,W#Q$2v=H%A}^%Gkķ03ãs1Bb:!2g9#Gg)g%;D9OO9U/C\z@cMS0`&R7 d2]]J1t0üG!_Y)?z)p '(A|,JRL+?,=(>JmgT&a„ l|kƠ, )O?C ?oEṉ9y-X/:t/ #k֭Ӂ?<ԓ.@Nɐ#u R %>l0% p)?|1--2/)6l%.e'OqR"TH Cq[#ϗo~j%y_?\+\>EN??xHhP?HJV)afF̈́:Bzz G?䗿VuS`ӻJ 㳖a,#yfgɧ!.io~l 9i8<'f#:} Xָ*^^-AI*FL~w#k7O?>J~ .{dr]SeAٶ=kOtQ/vSҔ}:J*pR P.Z3;(ߺjG) 5Tp%.%^[MfxsLw2څoܱ˕YWgϜ!?qnꔪp[GA 0|9_7u #GܸA&L(_ʅJ4m4xK[R%K G^G?ǞzZ\cfD!/~+T 05d 2+\[1!(L)FTX}`ݚx Đ]ahG!w>1RiĒf %ǝt^wċ%L_=,^XɢES9#QVa]h>K.RLrӑS2.3/[HnPB4Wqy8;߆I]A oi, y©3qm|LL.~+eoN;L%>.u#Ŗfs%Y,HUͿ@tnTIIGIf3]咿t\y5䒯So:nР5ut+J8JK/$=⋪P8 w#`YCJ;]#[mm|#ָ;Agˏ =G~vɸ6,O|*]~ah%,?ϕT2ScO>:K.?w!_9 ̞3OKys޹J7ϓ?OJW|]/O>Q/_U䎻!OPTԃ##G UR Jڊ\k0X,f͑SC~Wr9bڃ257O֗&#oGWG=rWo|ms \W.X~?y5/f !}㛲(/_ >Lg1i\Ǚ.-w.?O]*=tvo}*7v86@N? YDO|A~_klIҶ,5H}xrͨ㞧 THE*x,;$GӦMӱ{3SOTzxxxt>036c7RdD;txި#! җQ/SǖKٽp]/tM!):5U?]L{>أJ*!7_=5߻NMT8GҁBY^UD*9S3q$<+0S!$>-u!W%lCu_a{#߸:˶q6]z&W;r@gQًb^~ +3oX?EbG6/"TbTB #FRRA-^! 7WI'}u?f~7fxysS_|ک(7MA/"{c%Ͽ XImɼ>.}?+Wia߷*7&9=~_(~`sҬMAr#{$/3n˂,a/ThT܃wWQϜ9S&xYܞTzxxxtNؘ3b:! {Σ@*yt| I'.k giS|g2)j~#3R =ՑƓ:ߑss_d5{v_lٱ[쭭qzٞ෹tʐI (5ä~̒bYU1D/ R8׸mR(\fT9Ri$At-.J9[^+߼ՅvR¶xQyT2c5yA|օrCG͗ݙ+W޽H}|g;Kf-(]u^GB..dR 2nܘz%VY3,W^]}+ÛH |(Jr- -뤬nL%@Ɲ;-{r#/Ï>YN<;eIiS{}zPvĒc-c[*6I7VN;]u3 BS9e܀_N9 Xx p?p"z,guKMSu&导TXrs&uׯX*+dr.k ~z 7lF 7>I)uY-c'Nr,pdl)ب3Dڤ\?K>"Irdq%̴~Y|T>X_%[jqvKSy/ҾO^F|a}a2bFmUkH؂tf /nđBY2_^|%r5ˋ8?/xzz+WK_:CQ1jt)w©r{~%2 H%y!òq{qn%ƊD[6mɓJ`f<y` I A??eY4]H甩s?/_:ȡ<5?;웕Qq>򨳀RSW5MJG:oC1^LVb&?ߚ =LPtKڱ͖Jte~݈%^oIp\ʥ*ٳ+:tpPez~|ruIݻfw*?g\w\w|{ɥ̑o= 0SJ>`JGS\tNB6 ̨M,őXQYrѣl:gsRwͺ7o 6)7^K~(t:ҧ"u6 %-^%#({dOnd'jؓN n/|lLmSN(%sWR[FV,`?<‹.v%DK.tĨryݧɟw{i)=W c~2z}U,Ud"0!~?oeO/?D{YT![nRewsH%ĔFDs㔔W評gy=tEQD! 8!K?!4@3̒J?)/ƕK["-_ 93,%Hmsʯ`X@gNp7 ;&;O9x%d dx(,)mufcg>JN}'IacK2/2hH]"1ed)e#_u"u41Ē]h귿Ѳ!1<0X9q6kκA; Ծu).=56F*PR_ԓ*={}ŋ/CN{ z?]ˆ|oigOٗ c)':LI)&'B8<]ʱ{ktkoX2,'/obʂ2+?'ԲVͷto*˙vۿow 1 ~AܷOڏWbadY}q'"}3JצkSHE*m !!}~T_Ls}2y4%j{IGCx,6RL/95_+W)gpOOg4}Y3  3,@l&:5U?#L˜ϜfLW>i%u| D%]0~+_G;@%<: :Xp rO*#t ?oT6[B* H#Q^^ׯJzy\ŗ]ԥߩQ]!3rk"cχwo͗_=+?ؑ2{alSm{W7*Ie", t: &N뻛 eq\ԓO;Sg.X1@|Y>QL:g:3 BKHp2Sɬ[b`GMLJN~eiSugonWf,i lߢ8`易pdlQ!\ rqt 48u.}g4= q!̋zJ,ZP<7ikպL^r {~*_Ǐ嚧~XR}f3ۣ W7 3Siok3?_~_)qg u?"I)`:AxMd Гȣdkcr#\q]W" ::=N٥-f̜e/C8ᎇкvvB*`&ogΜ/}u~#~ҴzX;]J|VƖڃ(xɰZ̴!nw՚r--| ӈc*"G[l߇GxHA6cMݧ-q3iȓJΈXg韀~6J&{zPgX+oz 'Sc2Rduk~N1 1ugW(=WwV "?N>>կxfd*)N@(!xg̚J2Ɍ M͓fNXёR:Ӊ! /ɽ//kl6v0EܩH]Ͱ[5TR.ف=&( j@b uē+wTC R 4`?$N巰χ\\}͵: o8|ӓ8CO#~ȍ'ŅIl6gio(_29S?,[IBP965apgf})®v‰'>K̆BN%#./ڃo䃟qh[-#liT6$] %B*xL2MeC}IGgi|63LC`U챼;{O9M?X{|=N=Cq7u+@ҙ#eH[cO8=t}T3+<^t%/ ç8RɋaR?ζ`f,nзy҆$X_IM݀R?L˃K:UH=}(G,yF 64kOGc(\Û2tr ֘w:K=9眳-KCcV6Czb/=񸾭Ÿ H%Y`q /PzRX:vr!$,e8seJbyh0{野y9D$= J`2bά`b=[W]-gu.ՅJ%C^"_ I[BD O9N?ʪe/\?Q>/Ð=S]zQ-;x%!tԕps+J\pL<<ߞaeƾ ǤC$@L^gul 䙍n t@OFkEǮ_MTrJԜr,޽Dsygwo"߹c\}Ly2wQlݵWv,vD.]\Q2pԎAM(܍ &߸@}xkƾs.c -71˜j1si1`_ T: o)JM#s[od/9YXkwp(K2`Nlx;73K;sV< [aO:ȮuDKA%.?3)ÀНA=׮+4,dmݹC={ۡZ't\=/6lp`Lޚ?,d߰2h/73rӕKᶈRF(YJ O٠id*7fR_%JhJyq›ΥȠC +ݧw,Æ,tܥK}%yuPgijn}Wߺ`oJ\yaERF]le˴myiO&|յ!cG̿)"@r=/[jWGf`BB駞TzxxxtNX~`lPM0yVOfx3xf9$==wx!L|lHqO.'9(yKnN!C<7~'Ƴwk Ŭߥ{*5+㤞:Ygul $>dU7&t&54N`k;w~P]{*e"93͗?O~zf{Ȃ soZ:2(pnT(Hp76/wTZnսUz20@.Pp-:uN|+s 7Aβ7~k[رԔߧMά!{&R(MXr˗SrtmU5R =5dz- 8  ``A| ql.85#\Z&2f4IϵAȹXC C<[F mԏ{ Q1!(p1)KCunpXSĈߙd98mrvbԑKG\تA }"G5$Ruu,"~ԅJ.]ib,׉x3 ] f/-pekI]@)joבm[uT{p:ӀE٤ ֖k {xΚ}0ʪ;dj~#n='8KIe2 ux2vX3g,]Q,X8W;%e9sg)e抸2)l߾-#쵵R^>~Z öӦM4a ]4[n:`{eÆ 0a\dy4s͔j?+XR )eĭdӣà򮉝7v-zm#txW\\,su^,Μ9]_6E=蝜0}d-e'/ߪ*5fO?Ot"$R$Sҭg[c'J7f-{:qdu;FCTBa  ۷O6m$'NgyF{_FCcG{<ӏɛoSb9rȌbذ!2~X}2t`1pY#Oؾa}d!0(kĈanܸ1j;v?<'xL 5Ƥ =/O{G3`ذa.Q2uT}3apVƱ*ƍ[dҤ ~QĽ%=urψɲe&V]ׯ 4U?L#pG':0SFrXq]ۭ82de@LJ3^[lQ;J][ۭ$ ބd 55(w^3~$Y3~#n`;3!0Aoc n呧]DG ڙkB6BO/?9nK`cJ2O?h2<<<<<c2htQ G? óN7ovψ@kͩ_Pֻު IewG(s>ʬ^'Kb!&J3.oȈatH(`.:X9)?Ӱ6|[.u0/ i =*?μD}1_xt qmfvXKsYYVc϶{IXyԃ gv Im>_&TcaK:3/T8R }sdRy:Y{xxxx/=A2gFP6f=;8T)dCR=ݏ'`OH2lH,NrUdƍRQQ`r#x_[2uX~"\.L2W_ƍڃV/LA0KFe%kYI:5l33ܿ1|}=\65buϤxY9xxxxx?sйL2];TT;L( 8qY~b|:++JdmOIeNg/~v&\Ha+zvd˖JN+VuM8 pf%?l|ϯ= תo bǴzd of̵ki}3*=mW-i+P&u 2v̎\Ghq]B>QL:"Lܔmzxxxx/LY <[]|j\)0ׯ14U?{rmfgq8bq%F*3=N2%}Y b'!HC}8C}ǟ+w}<#}GtGM{xxxxxxxxx0xq:|LN{wDe,q9XǑg=I]yxؽgTU >DsL>IH~e:qVkG8B#>)=s2Sʬ^Hg(=JqbY][,{Cn8/^xŋ/^-Z8[w;|4F*!4؆={+~Y3#hqh)qɁfŋ/^xP#={}Xz.IH%8A.=,#*كOL8Qɤ'^xŋ/^M~>Iu3?gBiPvwabYNKwkGA=<@&uf3fjTzŋ/^x94VCS:S VϊplT*t$3˴Ň=<MdrzyRŋ/^xP[LԓC>){#9=?H$1L*+`jTFpq(zl۾Ij*5jZ^xŋ/^JR2;13'E>tHqqb#XTC*e;@͎onGզ oѣG/^xŋ/לtĐT~ƑO);G`Ws'ܹs{0#TGa 7kj_r~]a>4O*xŋ/^t-i%4{{J#GLLH 'P̮d4'^xŋ/^TvW46%~۶- m߾Uq"$զ xRŋ/^xkIH%'fJX&L5801wVN{cR?Z}ߧQ6 .r Ey9ź~w%z:\XL#,`r( }RFgA $:&1ZxҹsK.E!tWiӦ J֬Y%^{z6lSVXV*]]%`1KVC(#Mz.}C!v,y7u&.~ԏ2HK}q gu$>y& u$mWiȋ0#&H#MhCTb$jYө> ?,S=8 ڀ炗:_0J| 9R:@H3M[e>QnR$syuuA?@hg)PJ|{Pq鄤@0mL3NSW_}Y>u)++ե{{|ΆZ\ڰBG?ay;ֽ˗/B5|[j5-%#<\gY;9찞r l).vMMՋw_-&H#M8IetAv)%%%U~̋h2Yƽ5Zm߾])6b#%P^I}FUBX^mQuwJu ծo=SЮ,ء?WaR吖صW :. 5F*LDF[KS I%d{)!UFCfΜKSj:EaV=6sHȱDC谳Ri ҥyrꩽ82#Hܯ|+rW"/{;Cg9[UݵkKg#!zgB^,ַJ^Ql0AHQmN*o׭c_su {;=g@@.9U{>Hl?Aqr6N`u,rS3jWNx2:o@*Ϟ&/Ժ>u{t}^'*ǜGә<7pX[ۙ Y~cpua?$ JCC9G0ATZ8djѢJ1y=T2) f&+%J^Af9B1uXrRUFQBo69]UI7wl7uΤNɒrU`I]w9EwϞ=!C{En6[J;e`}vF2IHQmN*#Y,y-((wf侪uyĒj782D$=l?cHjY_TM&>+c>.3G(?*LDJL'6QV=ˡ+;t̟1Df}Y {JxR }!O>FA RQxwCbЗR~̟9\|V&}R&yY?>|QxA_co9rY'jU#X/҉g*A$1A,/_n#> 5SaY2J:4?ʁ42Sݨizާ3E1b Wdee9ys1! 'OV"j$z/14Gegi V)?~֑:# 7E&'v#eQCj9h4@*ykC@ 1D3gLۼX7JCy{0?>5~9.1Va)1uT0!;}lX7]@ܵݳC-#)S)G? 3-٧AT c[ȒyCef^b#]@ˢ#eě}d>2gYFƶCceʠ>gԣ2upٶUrkK*3a2v-:KLAidI403BiRy]w't.},#Gé3(QI&H^=4_#xF2f̘3ȃ(I#-u6m6o})Vfj9h4>Iʵk*)TL $2L0L^άRZHj0j~n҆Ã2PfV3No}ӦMJKJqzKm1o%sԴ*E!ʼ1˄!}d~RRK{T}j_,ؙ - Cj NB+^R.}ur#w3s" 3cJy&1ot_]7j&=ƕ/,Q Kit]p_j &W9#Av?qs&bW zT  N 3 J? #].vÑL DCSH8t1Ϭ/dxX鄤fP p;v|K_ЙGA)Dy%fPd1!{,efs䉪CI$O'>%8ܳuO%qp9\I$0A$NLi]z.5k։4V>}5Ȳ*#GuN3̴6ϒ26ڸR$#=Di]T&H`bFI{n F'vL>0Z F@)$B.Yڋὖ{%=⚹#Iad7ܬUۙ\r;1:WL"WZr=1wIizѤrd3|.iuzSs^,zKgV&S^UFS;T2fzFfN.@ Y*D>؄{nQұ IBh0[K%pŒ0Ap1C dfsQb>NfS>i©yyK%D[JWPVVu 4O'//C CFf%Wx9Kq?|JJJdŊe/dqǣjCW#v#MiK*3a76\bHe8{0kY_.K/m D}?V&]Bv(Lp̞%TJΜ]0}%=^:T:!Dc v^I3k5L xKkD4EӖRDϓăf )4d}c#wcXN˒YH/uHwjOV_#L-42G 32S҄<,hb?Kg$n O}\Қ a$_~B,gI,+t$6‘ g=Q9~tD9PVW5>Jo,48>e4.JR @җ0,xG#Fg *KH%n( # ?$V7!ʠS3EzeGdڰ`Rٻs֏^r y/'-I Ir_ihR9yȽ2{#2G:?G* {XxN-#u5N_:}k_Დe/#I嬑 y@zX L_&X t}ܱJ6>Ӗ~}ʍ/-T9@#kwCL 4Ҙt3Y>Hk$/ح6rj#'v [o$Hkd򀥵|HcuIoe'5rDi_H%)}̒mYN3fV3//O-[&+WTeժU.buҥK]pB ;w,Y$4͛K.lY| H*ň*~J09 ۷;Xj S:Tt.{="L† f fˁ|YjWW}^$`4їfzѤrT7I?x1u=2{2iR,_vm]wWZw}pf&A[=ٹ-yK#I1ˬw #qUskoƞg5V{yH'$stA6hďX![2b?S-m8ˆG>a΃4Ǵ0[ȋ:Qg ؍@Za'\2y2fc2j2`ٸ~qTxt!nr~wOlGk>dvGtаυ9vЃ2~`0Xʵ\pNJ*=2d4@*<d1 2ܯ L^v@ 4f Qy ˟2Y?~屄U˓rC-_ZQQ\,SCPtf9e{B";)vuwC-a%X;#s<̀TT:S ꚷ' isF>^o<2Nh7X[GD5LSOs6L~Nq~ȽtxRL¤rGdk [O* IGzj:dv RƒuԚ3g̜9].͓cu*Kg͚%W'ɓeԩܔ,=7n̛7O$-,Gd%vҰr„ h"y5/vʙ6m.|B2R?)6زY[Nz*"߼)N@auJ2͵G}{{2GđJ}t=+]͐01=Qgޫ "ͻ2Tm ocb7onc㴑`w~Qנ+,~T& cYx_=!}OOұIeD2IHQm@WS&&II[d#GIL$Nf kd=c${Ljy "q?ei.ipCO^NYF(M6R饍%T221sSPZ~ck4 oQT6-J*]zn%=>+"<4ط Qi@*rw{RØG;O 쫤q0Zt5LttTJ=K']$#=Di]T 0H&nL(e٩)}%;y[8H$(O{Q}*D=`oSS!|;VWJ={ԓʶ#zcr6gHޝ 諤=F*ē.d4N*!~L#1k HB!s?ғ$p#n0$0f7B HIE3&Si!5b륍%D*G~΂Lv NV JQ1Y0q;#3zPo][*YJWbrw9.1S`Vէا°:OxL'hOw'm ټH͇+S<'Q}*]wg) Cr HVdxh "$y6MJbؓxO?Vƌ0k g=|R}3fO9c O2E?;2bݓLdG?σP~${(g 3L4I,XF!a@AJ/m,IrUaE)Xb 0PAΌѯ%jkTzR< ʩY~}w0[էAS`u[wķ\c)Gr'n,צ̢ϴAKs_ΈS 8wlZ}VxRē.d4N*!dTƧ8ϒvaaYCfH>6h g)v'.i(e>;쟤ny%v;8 TW@e@ SQā|0cwg*=L_H唁wByS`޸3n]J&!,}ҭ~LlwoL7& G&]iwQ_Έqt}d̀۵M~nSHd%=]4TQujOD)D|Di]u+$ rRm?#qo ~enQ?NZ:X87@, ~7q WHTw5~N Na)E7V mk^Q䱯Salb̫1ֵO2xqMɝ纬kKV?/vSo寓ܩ}>[Q4$wߝ2՛mNa3?qީէm)1]at-jjLzs}6%٘޸A,KLGBx}ox7mz 'yLZTJQ}.>FT c|\s>2ʘ~=u6LvmXaLnٮn/O*[:'h>4N*!WFtWNKv*'24nذAßJ[(Wh/t7CTs LNv}HgWc nr|Sۜ32>;dˆga {x_Î^1=oku51iM:xИ2q_b#l}eQ*_IFr=I3m/R"?euucvѼU=mrjk=wҎ!RǿvO*[:'h>4?SFB hm仒[d$#$ߏď$WXWG+IzImJIKII#G) g%{1JUΞ=[f%n^K@YzA4!(CVO*ABrث8^]IJ)o& -2)o,r QA/-%>=sS0+>H?Ejw f&uԁJ-%Sߺ5Pr)Q}* ̒RYNS5OA35"T:=vJ>曏(R*:\Շ,tÌv;d˄`}M[B91ݪ/2{H-2ջy[dJ}.>0AO,=o7nd돈n_Uo4 iS{"N *O#MS N!!/a8mb2U"3I|V­>ql+ᤃ%SIRtNJ/*yRxɨ(X0PJgɚjWe\a_L yI!D!-B 9JyT CX Q_]_x"/{y^eeYRlzJD0DT #υA+0{i5 seSRٳ;f tz/#S]`d\;e TchNuL.-ƌ MS υˆsa&^)uq˗9䎐C% Ybl,Εm\+!\vbS_/O*[:'h>4H*9҅r13f~DxF(q#Fī%JLJ%S#w\Fa)?R."+rX'{nnU5CTzfM25N6E es" VGCT{ȳ(c-%H-~N1.͝Q}.>FDiKl^ZY27Vo28Q{6/ }^SF馷W|WmͅQ/"^I|2{e#ytSbѧũgEcvX()[\V-r4tp?f|t& ı&% 1IIe]>#{=WnQ/WaS@J{Pp z_iU).Z2^/NeM<))1 DX:F:*o]4fu=ܟ;]2艙#^950/4zoCu 8%HAT #υwˆJSA?Aär$Yxݘ s_dSr ]>'+R*25VJw=2 T&z|@'-v_sn̊l}28t'υ1} }qȍNUֽ=zwyc^˃,#X2\.Y ]]x?ܗ|Z~O)ß~BͧGGd|fܸƷO3WL|dƽʥ\σ b^4N{39 ^l{iqnݭ#Bx󱿹C' Yd+JufϑJ#AܑnmIrO{RٞvG[c'KvY9xxN#Ns8#n+|wل\F+B3Öf!k;D! mj/Pd{:>4ks(}T%JbB ;vJz$D}ciXn q<! F=M|ғn&N۶mSa=6𻘭7zRYA3w39)/[yțO8?DG _yɄ7UeB PFP:r!T:k}|Ec ZFW3 = $PԆܘ7ʫrGy?-󂄥63 ޓԂ+xI&/>r<{o>Y^{<>M/<'y[iRkCLb?^jxRٱʏK^92a'pR@,dyY1C`wCO YkG,9b BɌ$ҖlT0ȨI^A,¢Ԟ0"i6шףj4T%J bI0?H7ˆ݄t|΃m$b$OXoDbC]4ZfOTfVl%Rr.c#VMNLNqicf~)S@(qhP)4d%`')% ӱR渥 p Rmٰv[8RX/+E:`D ^ +ֵ3e˪)fΠOCg(zd#M_y^fyPÓ`ku<=n=0!#gTz3ʀXF+JfPޙ,)) *?G~#Po ?#i ,n@&Ԉףj*4bf9s~FOt};aӦMS̚5KKX^GΘ1CL0AD?eY,{ 'M$'O'j,}eS%ԑ2{ԩג|҄$n&'R(8(Hy4I6-.k8|7WHw} (RKf0%2{r0ʔMro/p'VKy)r{a}g=QIRsצ`>[_T?Ό\R '͑󀔯˓ Gx),}܍W $Gt3>Lb)4?G(ya}kGLPdB|󤲝%⠞ LqlG,{IPv?Q8ɶ#(B X?ok%a? aRin ̈bQujO@lm@hk@zwz4Qm'̠Jw} Bl#7a$>#qCވ=:ҰO ;qF͗tccg3IDKO*3+ ԟ=8G % ] ']Ed~|;.vZEx0RI`YDĖ oBpHHAHOD"ڰN KIVV7YvKRG:j:K|g6KFLȗt7njy 0L[k寸=̬!RŒey#dR)=(Cw"PSp+#F(Tڀ>}'1O%K-&K1_2K!/Ҿ[2gsPE v١wI)m%oLʱ0wc?'F9%f*>}?+?!_ew@4wj][]m#,^Xxr^iR\\Σq GjߙP'v+$ 1"@ F̈́āb2IL#lO\Ǭ%q#Y<(7\`~5;y VO*3+auGf bB>H_AL-)EfN\ V]]\w)u{eL bce6h>_ Swr4R mv/A{gkIjuR_D$Gz k&7ho/!f*)t$3f(sz !%6-Di7Rsu&Yʯ|KJ,.a!>vL1eH'8ÔPNL!IG^,4>n:+3-noW>9Ȉ$đ0LbRPfgwuN XZ[F y6M~ЕIeh=G#Fȑ#>eرcu${CɞI=:݇t_$--=q?K}~I;{.m%g?~f%3),U7Ko!bٓJL( T6O*J;Ѧ͇¥x IeBL*u1==҃'-THfPv]HJ1(_ryPG?c()8ƍ7>&x)_`y,er>JϞSz?MIeBhO*3O*;Z:) EtL6 Dxe v%!}BCMǤ+gɏ@HH 7E I|'W!e:)B0YRke߰aCM>Dc$s}}N׭+,1cIxJ'/L0AHQmס+J|3hɫ>p>73vJ+vz$eOx; .eة6koIef8wIeax,UyRYO\xR9p1KH%c9NO*B{'xRpmIeGJ'$*9n#O+@B,P>nL Z>L 1#o3b6f(/w(ݻyF%x,e-# yN2Iя~XB&ORq3K O董~!aV!{:uR 9-(XyX8P=Z6MJ3>1cG-DھFI~ȥKj\UGZ\VbF\M9|p t%{!)4%iȏ=+W=۷.)Ͼ[I]K}pwM2b2H3PxRB\DJ_nSHer{Rو IԞi"=½dT:J$;d m)"J;\B~_ %mBH~7 #~A ٣Ɍ%f*i82*ܸQ"HO8^ˣAtBKZYv-fVO!f7 H&I!M ČeFа#@ml#nҚi\#- d2<̬䍏JN}2}xRB\xR9xR=ĵG$T,^:SԓʎNH*!^F4,L'?TB$m+`YwsG'=D4F(-͌#DŒlTR.qO?)'t7 ٛ7ovXO;v6ғ'a,%D~+9iӦ,[$2$~-LO*3+ySJ,G,=LT6.TfT6)TfTvr߾ZʈtRac?o)0n^|@↨RFƃiK`1Ig2?O@dž< p&//n!a,w%/`'zG2IHQm׈kgF*Yܶo$@Z'T$jKp92>,ÚO*Tru )tf & B*v[j$$զ pʤH>1GJHn'e3) TO*NiO*VΌlO4IeIeIeGAH*!P' 7vÍiqPx΍YoDxćf͏< 1bgzזf2p&#!F0|!F,-Os70AHQm]wsw5R&aBnn3 ;]f̘!'O+W'Bi8fϞ<3#&Mtso\>ux|C &?[gHȏaܹMI0wUwAJ=̬T̓c<,*yNI]2FJrH܈G0jݢѲaUT&6t.%#(}b{}O/1/Jeb)ZWkAҖ TzxD2 ?S˪*V0K6ofO1T|lݺUA,roذA$q' yVWW?aR.i٣qwduBTqF(Ief%5 +Q2Tzדƥt(D86yTvxR&0:ll?#%.ș,lL,Jpt߾Iefr!R9\I%K9KyR  =l(QRőʲ5m *##}Oxcwm2s m܋//œJ4ѐHᗿH3&8#aH&vaB'&ǏFV--@M|ҙ?Bzf+VP43rH?~$))GiȟH' *xRo*t'$ɤ ϵRǺ6䀨(DGQ Ry&IGhH$3n1Y 4đ8"4mfgG #|2- 8Qv%Iefœ“TB;xR9xRZD*IeG'i! Ó/P&bi,I1r?iC4I87W<̬(\21XjIe Ie*TuzRٶ2͓LʎO*=Cb[ Y!,ie -Keٓ2W'Zg&'M=l w cah-%4c ߽dJZG*z Y=Nl_R 3č"lh$|6c7H! L#QztǣuMbF*412fbn0?L_?YO8v*]X,ēղRdTM2xR٤cAY( 12J=loi%u+ B6B. I -[6i|hn2L2!aq0r1& tuRb$LsDݔӒ! Óʖk"Iz.s*Gi Rʆ8צInarKyA|O"}6VyRJls}/F*M1v! 'mڐY_ԓʎVNkRYWWߔ~"K7Q2h䐮%v҇I#n"sYr]wh|ܐHfD //\t IG"$Ie}5b7Z+X< 2Ie+#Ie'ݧQЮ-Tfcb8'dϔ;]+ē4Sާ> y~uj_$E5I0kXT0 ӕ#TfVTTzR}\ww/J&L}6m|{jws7F$a(fF4Qiq TJd$ǣ}uM2,a%%.JRUt i{RY 'E"y{䢋.ҏٳtӿ#<\gP,Y$?wCc=+c~G{UBIEmdƣcаφqJȔYt۶mҸ$$}{Z)\}] Q.4퉨:>,}@)cS?Prt,Mʞ9ByߣOyAےJr#*{?Ot$+_AlKNjT^v%o^!/4_k曤)|0}KEE|W_}9NtO R_Z%q)M*ϐ RGlӥlFQ7CU0jDtfmDYd,}uUAnB3XS $ ŋu*_6S].^2E Mlo/t듌7DoN0ol\^MN_Wu J% h'}L,Er& D]0D{"NaDjhRW7޾: %#RTf8Qa{):]'CN\Ox@?Uo|=>{2UNp% =R//so(AT"&Ɋ!ՒM&OKcISOhg- R&|ݺ&&M|?RI~f[G!uR&suOT:H2D3,cejY`_@ʖNs)yԿ5([>CUtg+&S`tPV lҁwu)eq75~x`TV:t.m~(EqRr|ж$$_FTATal.kekq1hؽzd@*9[MlX%EKnp/ wG NArzM[l~ ^2(MJG&{$_!{KgI#9rx#u%G 9R I QG@?W!A}>kC{ .~77?%+ߓ2?L+nI1b9lp8FZ HMt<}pJf껊t˖-J/F$1nK b-,ې?T/"% GȆsbi@D)%дJQdeҺWgi(R_/ENQ'ycz,ץ<).,zވS:sal"R_S[ٵT}=ig8ƻF{[\ވSKnʷɶr7Ukص~/Tf<16Kydwwp33PfWPfuH%$͈>aTAYڊ;ɲX}bĔ|$b7??L*[!G:fA=M8^LN^aR^O>d4=TRʡCTnX@82mh~hgðeCq;3q.Tcƍn ^$%KHKr_b_52Y5} )3ߒhN(;Fo p[)QkbK;M\T=Q\󮾫C~̨sl+zkno%%o;ӵ5Ɉ~SQ}. M(˥X߃Dpvu e_mX? KV܉i3awdX͙l(]5/2/M^'9q)Hλ Ydyd~fےJXAP!u1ܐJ @HcQ#9ʢ\‰ˬoB8udu`rx>1y٧5=3T 7xuRVŒ$KdYRףG3gN~:7u&:~k8I XUUv5]e%_L¤BNVQ&9##q/Ȓ /OɢO7'<PD)D0~Z JL yc﨤d9=cu0RiXƾ7y{S'd'\?37iu6#ꚅ'Qu #7)CJYZh ]^6E/\%kp>&mε!O'Foj aRZBi{Rl/Ԥ_TrPOSgKw3Yp8t:?)cF† jٲVRX[ 2фAlfRj\`D9Cv.ӻIxB%#H|og92vh 9͕YGu9krI'?2c4 #il5x0ښP#obb :ucI7.e˖i0/͓I#lyqdldLeWeeĀ'eT~)1`T oO2DnAdh$=x.6ℕ=]]q޿mlO)cFʘo?7^oa4u:m:v,7]Hw4wVx  *$\7M30h瑱6zkGz퉨:D;ol7$o]}8|lIM*u%~duʣRg(_mJ*Q9 :#u0X,%X^&XZvKң"C=p̏|iZ!S#}{ٽLV\P_Ǐ;3ԉ xK_9AN'>&>ouFCt512 @J3xغuka,{e*Nh012/Yrag6|o, frYj)(vX'k Wة *$?I5RRZ!l5N VU=E4C$tvnMvJ YfY[J =υaש=kQhߒt*{J]kg:33r{Zۄ%»fSraZmKSֶF]mGWd=URDsf@/&Heαmʬ|}n0Cӑ͖d%==wѓ]$}6PO\țKZTGC"FW aI& >y&Q0 ex۾} =/cK8H>`/:nذAݬ`%L̎I|^X`{k.2+++&[;en,`|m@ڝvMn 6O OCͼ\f65GjiCXB;^6BTwEp?ʩ#Y=?HCct3Pflq:ҹ{vҡ)C* (BQm38ŏãO"AʤK)m;~$l7i A0D?~(P* TF%-n8_pa[\Zo2‚r #G[a|;~[}TZy# e5 CYڵm]#}~]Uhp[`hH˨{rƭ,ͽdF8 NIe)ݏ f(>+Qg3JG(;cH%bQmb@[bo=ãhH$jڇ~'?w- P(cǏ03@ S>ŸbcǏ0n?ųZ gC*IXfb<,X 30Io!ḇNm#5hCwa/M{QmQ܇HXya1$ttd}=YkԒ&ަTG,^y^RV,*r%/u}W\j\FzէP.g*QO&C^g*!A"Yat}RKKd:J!>zL~^6m&Dϳ>L=v<ʛ@C,Rlve݄ǎ{~kXYV&.kt[cgv[>c||;g2gqY Z'jk(: >7T w]Ws ,8He}-!񳔈&K@*a݆rn\ 2dڰXz+Yrқ],XKd:JљI ;bQ0a{\7uNJre=zaۨaAM77n}'ô^.\Azsg |{>b-r-nԩZN-k/IC]ך~E춲5JI4Kw-_첋۷[o>qgq{gZ* 3l?;2FzO``1`?D|nkoFh־3f[o5[ͼ] P5.6c?bv 1ߣG򵋷v\-Oyeo=i}w`mS+|߸4mo; q_xzKk1}u]]n_#.oܿ0k㫯ݛoy!|mõKߑ̧0n0vmƟ'nAԆ׽z +*wyeY%& XX2îece?#=Ph{Ş.=Vu\a}IT%S_կꫯ^0_yfMb|sØ_/yUn2m>ޯ:n.nHzVW\GcʤZ[Jr/+^ ovBܶX( ڹP.>>Rɵ늷/.7+VrLYkܬ>ے>2,ƶ1Ye3e[V[VTwIn:8nW]uU{T fs=WIĊ}VM/u1z7H#@HSZ׾ҧ/^|fbL ;M֓M:w6N" +yA\CR(';UJna榉y[ׯ_&bM~l9'--I!M_޽˽mM]\/u՟n>nc({w: ^s!>N|>?`Űto %Hsqgv n]'׀uqiuXAk~g?uYbv}gKxz7Jk*u{^3M`ΡO>Xv^|6BY],rӧ,iXο 8 Lӽ(,+"wǻ;, ndJ%u=?"׋ .L$@"e8g*%T%S׿0qD C^PP7twt+/~%Ucv 7 e~1 4=yUWݻe]֝~饭.= ٳ|C\>M.첰6Pm9dp~ Agzxtn(U,7ԱrcX">&=?l3@"~##U"K/|XaX$dq%*+KM6$|=KB*c:pGy$wSN ׁ|Ol!C??qAa_= ;ı/c묳N*3, 90fm֬GuT.Fk_~yux<|39scws<n|G9|s`k~|*gwervqG㏇m@yR|\ X ЧvZXFrdЄŦ\CϽٵC\C> h 7 [/*RɹB|C8;&/%[朾kC" :sCH!`ׄA`aLy^ /0#?wvp8rg\;ys/8\Wo[lE|>{?ck?aA~g7M8Ok?o|}.__җ8~ Ϯz=ac}w_5?p<;k՜gG*Gկ>|!DRYDRٸkWlXOR(-';UJn;NleFz*mh'77I|dY%X7tSzCLS@(ϱas4`ۿaI9^&ztؗr~H,;rau Ȝ]?M>ql Xvaʳ1]@TjeDׄmqOr(Fz[wG?,s iӦ?'M171i ڵs$={ (صA` >L;f gf?|ڵ}?4g?Ҁ[VK@n3gr~s9a<Śh?؟1D8^|iGBRA@Ie%z -A*EKgd]JTrh7 k#:1^FăyzdavhSq |7!>Ao e77Hm38.rzn|P?Y9|8F8W, IL*F#&@8&[o5\C[ƃ>8ԇgfuHRd \pA詴!F[|Yon } y<3eib˄e¤҈m EΟiۮA{ؗ! '/\'&H"Fo|c`m11ex7!r|@{kdKT?ڟ9}`בK Tr>sc2uTvT$KBRY [\CKqe/ky\#IJ%+YIuM*iT2op-7L7(s=BFEa_? o0 z#m=C%[3fLyX2$ƛX B,vmumb'2#1҆pv=*L(˶xY|}TlAZЀxi.nLl0\yaj@`.9PĐ `q|Η1Fy .)o~8lذkNy' 4ϾÆ}Og sۮFT4SẶxg2?%p&HrtGS}V-)Y5jTfHH*Ie>#H*Hgd]JC*& *gE=6Dg" vCi7@OecD#p!@Dy'-[/_Af;7&J?s`uٱy^ny7R7H!7V?ϟq ӮW>Ώs;6l5~a CbdC9]ɞR/|7χdsK|q>-̡; eE<òa 8$D`>#nׅafT _|.@(/z2k͐h1wg|= Y7Sd3dWnKi쭝1./aaWE7&K'i7!t?7<0Ǐ~@H$vM/Iᆙ:5ΏHAo϶nd!sd7, oxlgh.۹ YAי/Hb%빮u71vp m: [&7vLSO-$n9&yGԾ?i7Oޱ!|^ s-F5ErlڳBpt^bZkE;L GMzDiCyueʟ!L:L{kw ^&wLwl5?ōɅ}΀ sO&a΋]Eāk:lvS<zdsLd*<|Oh+6H}_hyv i9c٦vlIbLc61oqhSg(>F\=@]6vq}cgK"n;̰a`k7]3v :XgkGek1zf>nu붶Z+nͧxz^gbLjG9eŵkѱvFRH*;6Je!)Yפ詴[g72vŔTnF2&4f#ޟl{&k-I'>_z7s8}@`e%(kiS\6f j9^[ [5K&p nM[w1}]X0XgƼXo,h`Nj5⺁}})y,.khgKSň3`P>9 }Q62d8}CtH*IeFR,D><%TҞTr3D6[gF l7 ݜR&}cudoegrYma=zǤoǎo!ޞun뉏>IW, -K[q&p`ּ]#ǦmQxs-g}2w3]'׵zl}zgcqYgK{ Sş+Pƶu\dub϶1l`G7J}xΈrTvlR*?mig3>֬C3? {'2G (~o|2rlV3gVvϤ-a#oe[.6qml-hV'u;<33lge}&JsL\dyq=S)DG@Bz$y.@\Ǝ<)þV#|ԏD-OIi7=HVcٵ"̳yp,5e*?ׁ63Oy~C;)c2ܛu6zE?\;e(F΋kbs^L^t^Ms#Kld}^pK_8'/b aR8Xd[Jg퉜GRٱK/~vm;vnvpt{=ݸqc=w&UȒe79EMNY/D',eLMM*XN$ &cJ]6iH"uP$DGG}äe֛ocY=l6}>|Z$p,y!e,I1ːX&5Yro2kSdg%9>X܀ml>>.2G}vLy۱1:lu1_?gK=@vޠ%HYbQhlgN^^K9r;D"o ;8n]vrcǎ7&VL$C"tV[mɔ+/~`7}á2G9NyBM&(cH V2}R)ۉ /I&q 1ϔkAd3爨[r)\%?i.p%"Bb Fz;^<QNʙ褏 _i_\mq=Lm[u6ןn|,f՗UY!Ȣ.DA"H/GmsoLn7u'k曯_|xA*^"Go"usLdK6u䘈+bzB|~Y:!PR瞻y4CָyG؆T'NEIO%E114D:b3w)'!c;SrAnM6 eA$Pc.>2aez G>ʱ<^ &G9z@)29{t:Lۭˮߎ+"eIL& ZXRLNr,R&iHQ,n3ekgَ m0ugׁ[;ӂ[<q{y-66c%>W!%R*72";z1c0^D2C9'&³moOeFydu=ztswuGd'?[q]߾}³=6,^H}#2sB3X,a-Mb;߸VX7{=w#< |2d;о Ŝ:z]ٟU(Tr]Ϳ/nW _nSO i璟T2+Jgr<#0CvbaX2~6v*ίԥI9enmꂸ!noXq cNV3B!bR*X"9&d ABX^D'6vtӍ>aÆK/wv_vkq X:$!o޻[oF!b'x,ԏ^|nW-Wk_j?1袿^zѝ}Yk׆0<EyƔew5V3׈wܴiS݌/뭷N]{&95'w&zg63WKd:vmYR):3IyXrxވ尥6ϱҲZ)kTw\_VܮN+Ǻu:bM;}!¨K'Dސ"AC%-06F^B hz"gl;Shѻw-SvAmC=cPI%=lC:;k6S[Fw͓BXwa_rwX-ݻgxɱx{-qB8Ƣ?~\2Ͽ4ywܱ+C=ꤍaE*?3W*nٲR!BtfR*}DM=~H #d?;z^coUe;7"K# =Qa:!싘1[o8Kt(PUG Ʌ^RvS 76a/m8zy#=˱Tsfm6qn,-*SLDŽ6SΤ73WE2R!B䅺J8oYC>yubDYcCXfTRE#rvmӂ!rτye7HƾT>ḬJD8&Ǚ|Z$H*B!DK7[H!DM"CL))g=y(u!#:<36$W^3A9$ eO?:c)[Xs&$qcTғ<0e+2erI#vRH/>u͡}Lr=FxvxAIRgT2+|>\G[T !BL]J% y2BZ!%76b=Ȟ )L%3 =!uԇ ݲ H~d /.~)䍪8gv]wMxoze?0V^C)T{ H]uwBoI)Խ{ O6:?#F,3׿y~~Ke.IHI*B!D^KD(zГdn(C9ɰN#B ?6ʚln6i.DZvQPPގC]V7=31؇zZY;똲8,s F9YYizI$_i.ueIB!Wd)t?O&ճسȓT[M?Ⅰ!_&k&]H'O9,#m[GbbЙ,[92OLfu[uLi~D[l"[o+̊},B!{/oE&HdJ/&3sTf_YT&J5DR)B!D]J!q4>Y[,l?|%T"B!y.!ܴ3,ӆPr3zYf},&sOI%J!B'R*iG M<"'ʹ&v,l?|%T"B!y.08I%oUe+r yF_Yd.I53I%9m/IoHh8usK%~B!CP=| P[JYH2e,i7ʢ&sOI%I*9^9sJFi7ELD1Eqߜ6k"ى7E6!B%O]JI$=dd}LV~_Q?)6iy/Yפ$ ;!Mb7ǯKaeh†b:C̞=7IIB!DGN{*eIZ$t&]3y=ˤ2Y3fH*B!:JE9"s 0 v'~:;<~X"ay%nņ.L?["M=S)BT*Jͩ8I*17_}wh7mn繩7\stns)2azM*INʽ˾ 7wsóe !B%RQjNHTR9C]cݘ+tx{ןnlw?oem7irYaquӦv|ol7~3g)B!H*Td\u?߹ue)\cԙq?|?45&Z//+k޹(IWNfiyѓ B!:?JE9"GRvqww|wBx2k=sŚ\hI 퍶4B!H*TdIe^{w]SXYlkˋ9w{8ca89d*$) BT*Jͩ8ʶ˴kvw gcso7[$}%wl1UƙߞM^L !"/H*TdIeqwS>~]cu==zxߪmsܬ3$/sf{6} !Bt~$RsE2r߹[o8vݟݣ榍ys\/r- {4²l4<)BѹT*Jͩ8ʶ Bysܝcntw?;ƞ&^w;7kv~r7; ??BŕY a_4$/ !BIԜj#lL\w|ѿuwv_PX~r6T=^6E uzMKNnҍ~i> P!#TS-q:T„Osѻ፬2Z?tL{&榎]#B!JE9"IN RDjaD3ǡ5;F~'B!DnT*Jͩ8M*o7o9u鞻ʻnd(_ݔQgѾ?z? oB!H*Td(w_߻icr^ 9Δr_;/v7 w^ MG!"ԥT~~”C=|n̙ᆞuaN>SgmvOY>>཰a2gkB[ٱ{7gά0O9Yfʾ.u OY+OV}΍61oPff+uk)vlH-^L*B0ۮ?{%jqv ]{ RII !"/ԥTrÎ%q?C%2z" A2cSDzw3~[o+pB!w߽cMwv(GGٗe S[ay@y)uvrرC}v\ߎЄO۹r )~~՟I\"tڳT2wn5,K~%jq1;pL*on g$}Yn_B!u)&b&w?*g7HoY".D~ĉeD ` '͘{ܜN_IDATܡZ;qlԱ/ǡ>壌 %ey C;l>sbʱ,bJ;>|Z$t&lb4^nnʍuS:݁yѼ-RD-Й3+rzneH*BR*!.$^>~~N؆Tqc!Cȣ $H!OM6ȝz)y䫱{X1cr+0`i瞻/mpB7^s} rL<\4ODv=_j;n+'Rp\ñ:.~0Gnذ!.\~h "saZh+?/vkkh(!C:3(vL@f3I%DMa[r֫ٻdžfmC']M&\z|ũngc"k/"ee9L7fҥǻI{L7KOp|/LL7whl/L3TB!Dg_ԃp!BI'~$G)zČ)Bd~LRO3V[wA@(%"1bYw%uvؗܺ.曯~+AM~_ݰ7z\ 7rv-Hy6lІ޽{}[_|^An)ǹ \_?muO=x.s59쳽 7z)|ǟS[݃~gH}y9 "N65]Zg.IsLkԙKw_spokNsӮ<|oݴΪYL1+|N ̓q9ٍn%'IuokF՜z. O= ^4Ov]|Sܼ?W/鮜I2/!P/Ao~e=m7d EHr<„Q~M7v'tBP!H}f =LA:Ȋ+.?ܲL&b~} Lzz3tM<1m%aaz nj[&T˹-pw 3z%9[{{]׮]óoSӜp./X"\Z$_i.prKvkxvTww=r2Z:rzܨ䮿$7?)ڋ~Hz~|_1^,Dw~>s_xw_m R&)B!Dg.r]wvᆱ_ :sqϡN;d aD7dC:z*8㴲H`UWv_|aQDfI]{[KkIA[o =\sU؏!ȝI%=qnCYE=&M0/"~9qO21xe m[Bg_u7k׆꣗^O΃uwZe}&J}h)|uJ;TNOR9īOsS.xdM47_ Ry2W^yt{n]V꣓Z+'^fI~7Goνq3_E_Mz.B!ꟺc=:H%PMDG(N!}07Ib6H%"~,#TlGCLD2x'Acx*vkd#Ǧ]} -"RZ"\rlDp,yVIٍIn?wWs7߹WD"cNfpCՒ曱I>$R9ʽ##yL^4?2=}7gz9\iHB!DS=ܴ#I,!;䐃W]Ez!Gz,YnV'E&Iy9/3oadzʳ\^± uP6R}#)'5k)\W-wd[zi_!x{ĺy3ݥF wE?qkwtqv7'c/>эxwt7^+7/מ /'{{щ{K%Bc$B/3N 6ktNB!D.Q א9#8>|fk庑ʦyn<~?,ul̝۟+w9?q{!_| ǭ /1(w_~_zZ/z)g?:P2`^R)BQԥT"Ffcȑ7gm&evzMl;bel?6&eud}YϔmM𨗩c,v;68ۨu&;aFBVv\Q+3WE2Ͼ3CKexa ,D;Ͽ.g].:Ggz>]t!̟B&Go "iey[H*BR*eɦZ$ԗTbMA.C(~{s/=v{qnq{ |ez{ˏo2~nq|e0;_x`ly+^JTOB!D} TS-qI*JRz)_f<2ٽǦ)|d/$V䥇&S~'n >/V2 c dfY^Ó%B!H*Td9nn ' Mqc'1&~O/aH{ ېJ띴*d3TJ*B$RsE2N}  XqO3ν ƻro<% e8tVCðY䊟` _gyt_IdT=IB!RQjNHƩᯄ^nvDDIYp _~[\o!B΂RQjNH,Rz3DIYH*B$RsE2R 3=\SIB!:+JE9"GRX$B!JE9"3Hs"lk(B!DgGR(5Z$tͥ,xZJB! H*TdIRʗ#\SIB!:+JE9"GR#B!DT*Jͩ8&MMsߊ4vӰ2S)!/۱aas*Z V5?[tKgR ߋsT7J!B&77:].%rU/k.k&RYŲ>kbu]c\C$ fJ 72{mH߻͞=3,!lC.7%H"9ۙ7d;ec/s.(Sk{IKҾL*T*IB!=" kz*JoiwD&122d̳y Xfyc<"Jl[aބIz9F,lk-|^؏i;Sڲ &IKҾL* R!IGR)B|0;4t4$2 }m\ڝx\O7د$evrɍSYdeQS9Cф:nJ;Tw*=J!B7C]Jedk3iSYXCIoes=LEC!cS$u 953YDoֳPoӛM9Z;NKa[o}ݲ蒬}'Tb[RHΜDxX4۽CnC7,B\K|}M(!BN@Smdk]na^,{춖ڥg)7p=6 eҮR!ur\2Fʲ7CHfRj@='ȧ$uZbh-eDX]FY2L*{x/FT !"Dox )~!%ҸvsR]7 iJĎxh*'"j^|c"~6G(>6k`cH=o-GY{ԎU:iQ7YI%}eIe~5|5-/T !$o-v[>ړ! u]/{ ?º }xrS_~f!Si^{]v.n}d}rsa,n!Rzڂq>HH2|M]=(leXdOac,X(K&YI%|E*_{DYH*B({=qmn.ko \Om]#.!]VI rQ +Kf|nMLڬWu$nA(A lc҆˔^F$[>}zwߵIY\g'x,nSj^?#ʔn z.9.gjow˖Q*!I墇*~pM%B!wZًm\c]a$#K1a.S[H6J YcRŲ=I9qDeb޶S/7I׿~[jۦM6sQkw)3A[vkuRm.ھleɼɍdRRDeJ!B9^+kW赶k軩n =7ޛ%鳹Iz ٶ<ߦRI) [d7c!L֬gB6#a>nMb<7uvۄwԨB/O>ϡ{GSO={lz(C]]w_( /pkkh(W_՝t a=ӷo7mPw/? <0Nd;H*˫Nr8=\SH*Bz*\5BJT!A4yҧ>m>r ʲY#JG9K3,vw,kv,nv(˧}(OO&VZyiT~t;창K]}J"|H#<$i|'qz'lP~ n W $ %2f~ZeY*i䷅ޞ{)M-3OtxƉPk$$B!/^Kw}ݿZ7.eCoVL{06P&IǶW!Aw:wG^Hz*M{vG ?1rd~ZO?5>[pTz/iӛoϡKφy^C[?׭J,C\96\#ջw&ҁz8cg~Ʃ{$lH*B愷H'E+/ yCOzm$mSvHqǴ~CO w ׅ6"\z><3kfXϱ[M4!~5\zE vEGiTY,Rt^6-KʂER)B|08WD*!^P{*K2Yzkg*]*MC9G~񋟅aPˍ;rRX!U\,a)cr|`xwʰ} Y? ;R'%Rqg}ؗq7a,Yg:dg<v đdxXk,#(DZiO(l>+IB$B!x+7vsP[xdl֫Gș"Lه 2ov!֓q TR,Ah'˔3ezz鍤n΅mc;eNk-:J%{k|NrG@oL,_y޽ȸLQR<\^~.pY`OХy!BJ%"qs!{&n<{hqzʱ226Y:cQ7YF Ya NYs\M6)d_@~ُzXd~ZlYRRIR!:de@z5v[g=gJ$ER)B| ä%Gid}&teT !"H*0iQ7YI%$M9^)8~żxqcZM>홬6ڧy3y[GpMfl 2)BِTaҒo>JM*JJsr{ݫ^{s[y[+o]jSsjkR 7ssfV [B!Ie&-9J&3L<5͚>|i7 ǻמP˖W${I$Mt/>2!\KJJzdA!TaҒo>JK*zAng+MAFC嵇o aVm5Ա$զM|m,Owo+f1#B T.%Ed}ԓTGR6Kb4Ͻ/s/u{+Cq夗=u) +v +O׶ RO=B!,H*@ҒW>JD*9aE_|ŧݭvM{` 郓.m)Lh&MqΩp}YMڽ2I7wyMH5OB!Dg@RVRWRY6z_z==nW^]}{k. a~l[%6Ւuh~}fM--g5LzB !B;%(ϴzJz&<˟|:˽[g_|=3galy:Y'.vm^x97c ^t󎿖B_k?WB! H*@ҒW>JI*h9k,?ռ|PN|}>p-1ߺD!s \IKR_L+7L sq3gt}JoKzB5={R!IHZRJgZIK% AztexIR!IHZRJgZI=MK&BZd]R!&{H"7,ٳg'jeRmnzbbhbge 8.elQQ'8],6jP!BE} l̳aCr6m&wH۹Gdmvo7&C]!kJ]v>\,mL}N5NgJ"jGM!(&FL)Q A>w|W^rÇs'O ۭ~-ɤ{7sHf㙖H*jz,^BŽD#OZd}<>0oPB!'P*Fn &m1vm^{N;r]vr;CcB؎\2O%bDR7GH!uZ.n/Keذ!A8RzB9sqdž6n%[TIg,L@F[NFl}4u8BKy#ΎaB!Yt@T&2T;T;J%B#9 /e=Go "_a塰'XO#RG9 qT.rwn y{tSo@g>mp;vtyH~H%IYO]k~|ݽzr{٧(k_jˇz oNˡs[h+L2NZڒuM+3cy'|r-Ut„ a:u>yI S[qt9Kz9ޟmSLi<B!;*Td C&D"鵼ºI&CzK$#?Ƿ1yQOcc1gSnz򲞆{C!{I$uB멤.V_}P7Tˍ=,v~;3oO;;^ىI=Y״>IPs9tB!ygѤFw;TZ#sE&m/az+yqbI!1͎2a>d_BOc~ 7#'-J8y+-1]O,[O$fL2 ۷OR_%I-Y״Ie>T͆>C+"B'M*=/Ay /Aq;ox. WdzMܹV^y +! "8rc9}á #JyK-_' eSk7.zR!H]W5d4'g.XړuM+T3B!D>Yg*l KmQ*VO;#X}x{~QR.OBOkA 4aueߩdC9mg뮻& se}!%26$x"H>#nGmcvX$O^n7u'tBXF Iݻg*)h!ؼOChۋ/>n7`aI]$d]J$R!drna~ de@^HCckA&jwlŴ7iIRjK5DRH*BOZb\z! 6H{nrW?ՆRMI2ԓ!dyAK}$)%Vw/oR;?٢H*BOS3, IM\֮a]ɢOϷTҫT",,#CPʋpao-Vr20x9߉I=Y״ykʺXL,HJɺH*;n$:~~K_ZJgB!yul轱+Jz(la.[_noҦRirH$SdFny>2k8cR>c=ԕO$d]J$B!"_,zʾ[A;,>'FeM]cͼ+T"=&<>i7J]q|&$d]J$B!"_Jz) w/J6"H[e*= 2<2ɍ:rOIbΝ$)%V"B!b>=Km uS"K!!xyO/}XToR-q$B!"W̛溦 z=qB]a2}{m\*yIBToR-q$B!"_4y>].#kg+/^˥J/N"A,Tn)Tj#B!bǏuiXup#]a˾Njc2I"!J%8J!B+L*sb]ץq;[x|zn eTzT*KHƑT !B\1)+!6s]lq;Dne57)M(S6"GR)B!rIeU\VA*o =ɺ^$7S"A*3"Trj#B!z쳚+x䍯ť6qn>=r^፯7eϦTR+"GR)B!rE<7OTn dWO{m =D J%8J!B/ᯅ.WsYz"Pذ$qo%=zReE2R!B IԘj#B!BR(5Z$H*B!DT*J8J!B/$RcE2R!B IԘj#B!BR(5Z$H*B!DX"RY}#gG}PrN> ):vzgϞ>>gwg}{`ͳG؏c>>>Dzu~V/uP)sdY>듺%lg.B!D*{춚+^z{,(&Z&{&]s7&p-ef c|^YYfIcDѶ#~s S?ֳ嬝V?ۚ愶PrC6,lgHb(oǶRc[T !BMJ3)cz@rlGXf$})7zGZ74mm&&vq<ʰݎ<NJͼO\Yfw}|l$mLi/O(>K[T !BMJz 67{睷"iv$u֣~g8uQqCֶvkw2$m#Gn{_Rپ&&o_7~$\jaz)O,ú'|56SO=Q>udڀTcMv'٢__sL>dYR)B!:7P*MrŔKկ~% 1SJyP>edj2H#lZO!r~;MgX,8fϞݭ䏡%7`ҡk_?.g Ae9樰R믿~܈˺v=M?q[c]ŭ SN%Gido-J!B/:TrCx1o=w7{{8Ēv;};%eX&H"NH6?V^y ]wM}{t'p~n s{{S]+9r;w衇3^pO<[{5 ':T"_nq߰757p2A6_xP95, zm|Ғo_ӑT !B|sZE+_9,H=C$YD"LYgV 3eَ<zEY,7kd"H(S߽{W7m H?ϱKgC]Ȣ0,RDzv?G ۩{-:J%,[J%B!(ܔ#yܘ#Y֓6zE+Ȕu"W]uEx)=ÞgGBqD7Qd*Xҋ7*GE~;nkֶ.?a]VylQ/8|nJ!B:T"\2Ex*@y,"<2uG$%H&yGy!Ye=pԍ6 >,8"~khj-B%m 9&o<<k#x , d;? ˅~΄g=M٢_;qI*B!D~RΛ77Ȝ=oH/!EXoCC=hGo$I$G/&?-_2#yP`"x]Oz0dmm=zt bHLz$*˰\~v?> 6$ꫯ?pl~aߥD2VnVnǔ!'OmGi4#B!>S!eDBXrc,2Ɣ>mD?Jbb:{:iCP~MMsgu>z\&VNֳq,u! bQ?8|v|f,B!(&(K&S#mM=a|%#&yC8>tr<Hk3?iu"ې`[R7f~ZseIB!,Lߤ+J=Z$H*B!DT*J8J!B/$RcE2R!B IԘj#B!BR(5Z$H*B!DT*J8J!B/$RcE2R!B IԘj#B!BR(5Z$H*B!DT*J8J!B/$RcE2R!B IԘj#B!BR(5Z$H*B!DT*J8J!B/$RcE2R!B IԘj#B!JJiI* A*okf}' 7f}>ð3݇)zn)UIKҾL*T !B|QR #7}Ai7LJL7XpOgRR!B:9sf)2ɼV>xD87}T"B!14Dy[%Xlc)T&YI%J!B/P*m+C`g,Sz&nH!Ϥ%Gid}&H*B!DCDiGYIyRLdugҒo>J$B!"_ԡT;oD,y,7&Y/&IXUg}%-9J&3DR)B!EJ)ݗ|qǑnp{챛{=N;wugwG n 7J,%B!"?t@DƘ!|ܤ3dPeD-zYopϾ=,,3toi8v!VvR4I6帴rT3*>V?(훬ϤIB!P*G i=XRzGBjeK:z )d_ayNf?u~rS6hףG7; 4ݍ˂PR^V_}0vVpgqZ8ۗZwx>uh9(ퟬϤIB!P*D9gyʹsg!D/!SxYJ|uǔ^MLdSOqmM}h=cǎ.vSRosx gC\xʍuCXǶ .8 LzbQ?YI%J!B/:T"{&Z&bHRi:$S`?zPdV/3o[cZ(ʐI&n~vJW wq[dn}ݱn^N:o>ͮEvbQ?YI%H*B!DJ%7Hݘ̔aL"j% EʐMD!:IѷZ eN9SgD2G ]=oX(t ]Rr_~9r Frn0E9ڋTfV'T)B!J#g,W!rP9A26ϊV[u8?63 LyJ)ǐ[o%,[7.t/bioE)oV%;(ퟬϤ;C$B!sCƜt7z$y~)c=oOَt";82,IpSiAHI׮ nʔɡ{UW,yk+/я~ϱ[ݫ쮾J曯~u뭷Nm^{uJF*YU[ L*T !B|As!dW{++-6\֦ [=Bϥ _Z /vۭ[dMHd*eh'IKv6;dȠd]Ҋ@nFAH}J2TOe=$3ϓؖ%B!sӁrcFL"HMF$J{N|Ŏ[[i.lz uQy+k=e L*-K*B!DJ%…`1x~+H^ðW ,# Wk&̧I)e@k"H,SRPk#u}(c=v|a}lj(훬ϤmYR)B!:7",#\c9<[H"#r:MpӸǰp\1@g= "hSc0ˎ~'Xq[}T"B!JrcACfyD Q'cنrG蘲Lʘ1%2v[\ڐ\ zdɥĂ>J$B!"_t@T旴(훬ϤIB!:LZrMgRR!B Ie&-9J&3DR)B!򅤲}T"B!BRYIKҾL*T !B|!ä%Gid}&H*B!DTaҒo>J$B!"_H*0iQ7YI%J!B/$Ob9~Gm޼ya*B!DBR(m> VZS)B!:7JEiSi&MMMB!% q/t|A&g:aMɟ5!B!:%K@*yLQ:{L0Ǎ͙3'D!B΅z*TN8>&B!Dd Hs=(6<{g /<^y%wuׄ)B!GQ< e+zR!Btn$ҦA$J[O!B΍RQ4J!B/$ҦT !B|!T6R!B Ii$B!"_H*M#B!BR(mIB!JEiH*B!DXRY ^$ ^$\2OEId}'3YmDR)B!E.]zg}W챩+ZF^ 쳕+,'Lz*/t.J-hdIB!JRٸmU{fRkf=Ioe;IJiq+2D*mk&T+yJw=զJ$B!"Wx'n.Ws]x챙ʵ]&kBKk㐆ޛ4J-],WʢʂRYd}'3YmDR)B!r<)KRs /Bw/=6r_qs<&,kNgTR!B B]q)/=馉4g(K黵϶o$.]f) eae_`%?Cv&T+yJw=զJ$B!"Wxkz9uk跃kXfSWc9 w )ޭ*]4ᚥa5UJa~5W(z,.o7鵦F^S홬6U"B!K?)]om K%r30`ȝ\㠝K5xlGHס{{tu-g)0 rzH+ +F;}^ko<%;ўjS%J!B+v$.e \a~*d{Wq2Ե6^6 vӝ HL*x2lo7鵅vn̙>胰? 빡'O7,3oه}Y,e?߸-ǭ:{睷>de{Ə>P7u2e;eำf}SuSvǺmʝrIeRܹu2o왡vVL?p=Ʊg:8Q~ lv\: ˴)XϱY2aj(;ѾdVs%J!BѹssKye=}'b-B K-)ZJKys˒ ^{c7=ʷz#HۑlcJnY6 cu]]r?3<.R[|W{P 56رQvYLL8&'ReiB;h˩Rj@k= 6Xs]믺8p7hwGav0vnurzBp7B=[nyzJ؇s?>ƭrGnAaCڰ.;'x,1+"?='۵km]±8? m?OqF=Ca5bZkMΝ/t+eZkє8I}ߋKE "B!vD)| G4'•l%YoX"He>iRi2cunmZ~Zt;=3nGm:lNډtsgPVD 8q|N{n0ծc?#=}aWr>|XorAAt{lz~q6ӎ>߷<'?qh;2Zr˔p\Da;>_nȐA:FBf9ƫgV鵥LCCo Cop;g ,g}'7"GR)B!EEpHs_^~mCos("ES[裏Drl?ahe'lO xԃ n bϡWmV?ۨcu֙A<CG/1q\XYfg*O>P' ZH{h#n)obz^:J ҅P BLqNzsG?}6=PW]uEʱy֒J/_2ܓ!NN4!pB}x3N+-Q1Vd㳍6$erMAڈz-KRɵ@L[*1NR?Cqm6ZQ븎H*OsX>DDcmeIB!iO%A6a&7&b" cY}B}jRm%Kia}m&A,9!mbH)"JL6휐. E=3:K_0gCZa_r.?h؇zWcX*pWD^ălSb_ 3![۞O>x;[~!)e\/EI^U fbM9Vw=H*B!DCddpKyx fS qA 줓N$"K?Hq, pAuW eŤn )1e'qoCd^QR:^˳˴BT9R!eL-[~.AMExS+/bXFm~sBh?2sd_ވ@/tpeʾo|-/WSy ˣFDYL=:/Vnr~ڹs|uY߉MbmeIB!ԡT"O\$w&,KgZ e)y.uH!C؇&a=/XG[m_A8}&SV?>ĘKvX[!vaǴ:Y~gꢼekeHiK8.261>k 孝LQwOw}KKݴՖ%B!sSRX#VKo!79B"$mۙ7 , c=S51"&a?0ώ6(S'v&*cjlͳp< 1i"1~+jK(CXO~n,Se;:`RXfJQsHi}h4>CIB!ԩTpl &NI # DjyʳLLg2cj؇2[~Q:MJXǼΓm7ekŲKPr\g=eLBZ8e橏)p a; /f l}Tt%B!sSR &ir|S}cv`m&C@nu񂟸)1c:cbXYc;噧 e9>9SuG(:bǴر]&ԍ1%C>˖ٟhysC]>e;:~;knCyYgd}'7ͿOeIB!ԡTrnb7&L|KA7D!B\Ll.1L,6֫Q.D(km'?e3m>lsXkBKY3 *mˎ6`ݮ͎OWue|,E9v^3dC͖%B!sSR9Y߉LV*T !B|1dgӒPn/imB$J;&;ўjS%J!B/#,/OV>KĒH*vLw=զJ$B!"_̯^D*Hdd8lHdRid}'3YmDR)B!łHe" ^()oRRg홬6U"B!b~R4&/A"aB DT*훬D{&MH*B!DTHe?zm vrH*Lw=զJ$B!"_,TcILdr]q.8toWRid}'3YmDR)B!rToz' GloT%j#B! lbϕXn.!}|tC^&E&HRR,"GR)B!r9~R쵢 ]CKx6p~[B]a]qЮq^(Tj#B!KMSE2R!B䊹 ^"W<5xXHq$ +Tn2J/ݽL _Nߤ+J=D#B!"5.k^&v\_q/WJHUpŕp |KeύcCW칾˵\ze5$J'HHƑT !B\I%=vva'N/pW/d/ʐIIe/=uJ\hoR-q$B!"W&0hg*$<[_le9<؋=ScS/Rr=I҉R-q$B!"Wsny4ҋ~k+ E=e'DF;0ɈRO^TvgzǺ؃o7RO8J!B+TRaJr7H"E $J%G8J!B+$RkE2R!B IԚj#B!BR(Z$H*B!DT*J8J!B+$RkE2R!B IԚj#B!bHeύ||BŲ$F\Q:K$B!"W@A^"IJⅲ"I4R!B Im$B!"WH*m#B!BR(mIB!JEiH*B!DT*JFR)B!rRQ6J!B+J*6i)HTLr5\+>|ɇ5YldYq>Ə597gv:B΋o%khd5O3fB]a6]Ð]нTerR܁P`yc ߧiV~fϞ>#7qfjZ̙:Y EiW`}s֟No1}:JƟbeZK))Y(d^8S+u+ \~?WHJ/4>h5|bM1;RڬO g$L7nhl;oVsM9CotdWdՐy9EN_1`W\/~۹n+]eHe2g*c\;O"Ȩ}jٖ:f6+?|Ə͝;;b23Y EQEQZܜgjzel- `Z辞:xOmY^* F6lr/&A*Tx2%n>(6oOܜY??}RQEQE!YP,x7}kan0x'W\f? \7JJBTO妮ث4YA'ജO>}|w[s'Jv裷'{pU6ɋzb(v'!k%鞵2&=4of28Lݻ5 ukr/eJͥr/"ͤ7|j>a"$kHjce>Vf~g77y͎aϝ$@[&= AW60eɥ-3Im63SUc<)Oɺ%x~),+ F r YsVX:8YmR'>]b]J~~t9mU/?CJPI*Lߕ~Ǫ!Ş^0_EJC5擵擬}Iﵛ:RV)yzmjz/k\4 %Qzű0hW/TzL2K2HeJKx҂TY߇r',k,tb[zc&똵%.YK 皮g zq IN5̗tJݧ{]M^^IgR7sf-ߦ7K{VRh)uKo -rථ̖IXGl٧8`//a͖IaK)7uPY盺Uڧv3e|mCwJ;#mM]d~6$!yd_$7<[$/ɲTXf߬]Ғ<]z K.Ymj()?Fvtz,UWBIenTG2cJN@Y4Yg^2"Y컁+m\%im,,gͳג cߴFBNZ ()i),1ˋe)f!1d/ܺ R#Aa|23R×q eȒH2dG?" eE*y1OKeXX2\J\[&C$dITz+w<㦭=Sii)ϦqSX(\%Mg{̔ er--~˄?Ca+4-f=mX0*nDrsװ42Ǎ{ Β#>D,mTDx{^u>2V R(>^"C;Ǥ˧'IH$2[&CvKb=i, {m#LeRIj&tBldgI%}J?xIixISĠş{)f}0uI,|L"3vDmHbYƚV&L oͿ_FXji0a|UggJڴ5k$jk8#J³eeuYu)ȬrL6J/A* eߥ$񳔭J5 VĂY%-ZjJ(Y#3YǬ)P:fffJ^P?bҍZ kxAVkoz>T֕o{[nd3IA|O,DneKVJz"JKbL;'ZKr} ή8`dپr3Xz qJ=1uu,siу!?Cp,I_V*/)]p@<7ia=$" >K^/Ѵ`.\..4;׬dIDpPϝϿF7 I Z;ZJ$+JLWzЖ5ؿ=l.LFYBiT贖ն 7kHFg?5VkIDT%CZEo,xK"ISD|2EIY ž[!L6 5j>3]$!ődR%f'bC,7^ ZX 8f:c]x<'ڴmMM{Tow04}a6&N)ƿ4ޛ/VX*7)/ndR2,T:L}^ɺb- 4s -iLRXrUdeG΀*כ拒X- " *R0ؖrz]o|Iz})Kn7~[8v$ YŤ-KeaSZ8BYИr{&YJq\Vo+4Y8lI+iB墌($ +Kg)KLҦR9TT[$-K:YmT!PTn2A[ivӘXT)K i,oKz2EoI$*%;t4JbcT*i.-\I~.KbZe!sM}/vUggJ9ג $J/Hm ^HTW* %iM(T/XKO-dQ$ێ"64Rq۞1 i)Iäg>ۭ\C?/H~i]qή!-X2;|}qЮ!5g HI 7ܠ"GImi*Ddm|݄#rd׉bס&-Ns{Y 7<}?̔1:KKڧ2JʲT~K^#l&&_v +T΄䱥L@.@2֗|mKV2i)Ke_;lʽJ@u痖f/PJ*5hm@f$>$ "^jJrQ%ͶdoPROdn[J1H:T1yO_d'rRcL)~Y$AϾ_ON%-̒^" 9 JrEH*n-dh[&\ˬ6+'ʞOdކ}3 &{IY}X&CXc_ƍpJwR8.I֛X"QN",K,"2@!R{$RY-Y /Q^Dq HDiק-p8N+I  /y) xFL:BqʴuucX (4&EGUJ?ii&!4J>K RIj멜_=NjK|-dH) oσ)˚ݼuИ(4ˤ>X@Z_N]S:^i)|~+)+}eD;"SeR+\z虌$rM2tf2ca9N,L|;ұhn7KVJ9iIGYLm2|J)}6d"QZ1$c] RI/eKO|.1YmV:nJ[I*⿄2+iYQ:v.|vिJJdeYD9{lBz&yLA/N$靬re&[) iaquDRIE\0|Je&22ITI*˜Ku$90N{IK φ,T,0 M Y8Y禴cZϦ#HK'6RUx)[K/ņ|6.Srz+L١z&R,X"yiIpB寧Y P-6,іC* F$&*,ص^ؤEuH=+ya1Z֯i&+fRΒ'ȌZ-G/T/u5uR_KA(7d(K>uv ֛S:l%2?;X.IR򳑥mU׀/bZKNg瓬z12@OŰrkL3l)v Iñ[ֲdltkq&KZJI -+}g>dTFۢuʲR4eװҗJRɺD6-}+|A핐{;IENDB`onedrive-2.5.5/docs/images/nginx_verify_install.png000066400000000000000000006505501476564400300225220ustar00rootroot00000000000000PNG  IHDRV><sBIT|dtEXtSoftwaregnome-screenshot>&tEXtCreation TimeMon 26 Aug 2024 19:34:55hf. IDATxy| dMDHWs(uT)Jh HQG)/JDBD$H6͎=fnvGDgggf?= Z!Af:#5Z9ЍXﻸ[kF˻`to-/ٺ_S,P`dDMh-d/zG۳ۉ釔\|ӨP)_i% QsKmZ RO`:dIZVMjTTds|> Pg8iՋsHowú@X {JS *YBjܮo5ڎqb`hsӯ A-C Qb̸/X.x5.Z|қ; g:@ޭ|/Vsc=ve.Td(;|p[Ϡ\SȜRzE2%}G8-h4!HZ (2ת`yru]'iZup&+Kx[3-f97{Z$)r;L5^y1ȉԭA%vpAVepZ ̓5 3Ӡk#L&V&u=^ g|0vIh4Zh4Z8qҊAS"S|-{@ Og@B>w:ro˖hղ0!TP0? zy3A/%ILO<7 _pR8tZh{%|Z [hEDz j)YЈ#CE\:R[F? SHG,8M~iR9D uMRL6 jjӷK/!** qqqx>f͚,Wti,_>}ׯcVo#F;wd~ׯ -#Gıcp=<~Ǐǫ*ΨQVaMDTؚHnTI;ry_מmA+QF]v!66HOOǭ[{nh(QB)cA/EJ;wD%U\o*UdkҍAdd$Lh#|e f2j- r;~=Ͷp{!7^+Y51W_F?tR%?4(gϞmhٲ%_VW^yBPP\pp0~7{HMMŮ]3ԬYӪt+VZ@elقɓ'#!!'ODp Ԯ][ry///DFF"00gΜѺukݻ3Y\r/7DDd/φ.Yb.j̓F}{;3BWO>\2+bŊ!88{7,GLL OB!S4FK ֮];!<_FPqyUW_a(]2K1o<J69;v %%?)))ؽ{lס+~IFʤϽP|y>R7F󤂠֖m@`4oX0U^7Ɗ+=ziۻ>F*USNX":%JGrfBJ0~xkBCC (Q=իÇK.S~}tOFЧOL6 ŋDŽ ={аaCԭ[}EHH.] RN:l#<<YYYv)"V!!g|lȥrNL놷L28pΝҥKɓ;v 4eˢlٲhР>Ǝ'OtҘ;w~)SlZvT?[&ӏtg9 R8xⲽV- <кukxx෇6m b@)*{ӧ z6Fv0uTL::tk>3ER†HacW?`p3nqUn܂z5D1+KsUK69̀QQQ8x ʔ)mۊ˔)ZSN"""Vŀ?VZw|駒\nCVRرc@n k׮aŊj4)55.֭?PrO?$N[bT*u[.oߎݻw#88p r܋+W w ̙3-֗,4̆G${>=Fԩ߿.]:}Xb%\?ǏqʟXb%ڷo.]xWqOG2uw܁Vɓ%͛߇V\rq?\dnWtAӧOF}01… vuyZhhӦ 7onxpaf2oweda.j|m۶\rVsN1}5j ''/"<<<?Ν֭> ?ƎkرZooo,]ׯٳgDD[!%7cm{UnY[cO 8233qy4m?BTbp<}O>ٳgT*M̔(Qu馮X+V\FԥOMLLx7]Ѫ*mG V &w G~u9OU.]dhBn.-͛7lj'Ww#up&nXOqTt6!qn$!TR4J 6DmZSBnJ7ݤp8BZШQ#l޼9998tj5z'J&*{GVc"22Ҫ:4nC\\~޽zB6mp!4m @%3g !!Ȃ'M۠A'jNNPti`ڴ(_<>|p@DT0䵑jd:qOƱq~T{ȵ盫kvډ/Woi֬5k=zݻ:+ʟrAU}Gr5!7+VL~tf!>WYjusFxEȀA FZrK40s0IjA(W)O^2šzyS?ǥ ҥ Ů]vZdffB hٲUeԩSm gggyTM6&LZ}>x6>|z>y f+'OCv]QT5|u;r+AUcQQQhڴ)bbb0|p^z1b8bccѼysDEEɬ;|tK~fhnv.œ_:$;b.mnN.ǢIn0&15/\,D5b| FVv@R f=JLuu5xr=0{^zC{mS`J%ӧ6m;wDڵ⼄@RRϞ=ùse˖i[r.^:TRh֬>U͛___;w'O999qFOW^'͛DF.Zs2jF琺rT6Zɗ%QyLzyyҥKhժ6߀k׮իX~ZjK.^{۷o5_}U˗ŋrȯlfz,>'O?y)jIII ?~HOOO?$6r*p\esΖ5#Wy}*?:ʵ {JM7>N^ASp1w(kSˇN:hРOOOZj^thZ]v[zvލo5yqx饗u5kgϞ֭[ޱ&>s(JYPjU4nZB 다$j?+V\)|w@~Zr%^ꐺl|֛ˑiMz$5yVK{սM"44>kxǣkYfzk&3\ l@؟7wa{2yʱ_QFX`t%J8I8x p233q Tu 99&M̝;۷oo>4lϐc 4mڴ{`ٲex >#( \~q4i ܹsXv-ʕ+:t(T*PZ5\t W^_2eʠEP(0o|zԮ]oΝ;qYy"4sՑC>ƽdڮyndJ^6 UVRJ+W!88Xoyeq~! b&<} V\ jԨf~2Fϻ7oƛoRJ?J\"**C)菲vtgRAf99t F}},FU^0Vk/846 Z}wq9TV ŋDŽ  7w^q39ڶm =8p !Cصkk׮wDXXXKڴiѣG_~ =Z|lRpyԬYo&__Jٶmv~i_Ct!!!8<~{I@,V0Z3/ {ג ʵ_ss}ڪAۑlٲhӦ Nٴ^?y$=z^x U Hn\EkaأFroqڝj$m5I !4W7|a {xyy!;;[gϞ̳G-9T[˚=w'cи36;jU*/9rJ8`p}{֭0aN:e-[X=KcUM2kFܩ*{:HkÙy]*V% = ^Q ގXusǝ=#FٳgQ^=oGA.]\]5""*p6sUWPt Z$cUp{wDRbWsM+sIW_}!CL2&%%a˖->}23cUkd rS 0a.0n^6~/`r#7:j(U ȳ~a٨^:~̞=O>uuՈ@e(gr=lY-`ˉ&׸vԱaWCoRikea?#|a : RVU|q*QZ9749 AhgW^.ZV%X] TZ"""r!GW%/R~^I`zxf֐YDw{>ǵ,ܶEVsU<]TX:_ A P8ow煩PAGBk[kXL+tk-ou-TկLDDDM@2ߍ6R,{ EǶ׬8mn-l>ڵo5t?Zނ@\c`R@Q<ȭ,ZLzpm -Rnx!K [&F*FU^3DDDޡL9G48h̸~ؑP[= ./QfG?x a/Ym+Б;php4`*KG7֚arM֕녪W)~oW Ý Rc5h C!"""X YFj^^ZU47TL ؀*62HK ÿZ!ǪEQA󡈞l f-w[?_3;R:cR!s:$>_WX_1kH !ܥT3}w>f̖;ٰ!`7IXbU1ݩj%o=L(_=Vu @/")ncuȭ/-:5`ibx`r4~]$t"""eMJj|@/^DhhnG&o"""""""?9h4*QQ=<< ~U"""""""Xh4ɁZZF  b0B QQU j* YYYPTFNNAoV".* T* R OOOW+՜T*ddd իW7?[nJ(r׭[;w?wHy ĉ'Ə#G &M$ݵkW̛7Og}֭[#%%{ſ/,_52Ch"̘1Cvv6o+l裏PF 7nzjMGV#;; """"""r{DQX1={ ǎCnncQ^>=Kɥ/.LK,jzGŒ%K58B0̙3}Ett4CHHʖ-//^+VLr}3f bbbwߙVߏ7|/8B zEy7{lL2b䘘L:1|1a,==ӧOzUWD؇3`~11R]eX}~mڴue$&&A:u**U8s V\X)S;vĸq0yd\x.]Bxx8>3:uݻƲepI<|˗ k׮!""7n@Ŋtq}vv6/_#G ++ ڵäILzΝ;QQQ~ժU͛hi&߿hذ!N_|兤$?~o{=M6aݺu@n>޽{cڴiwbԩPxq >ܠw+Wxb?>_hԨMjǛ]7)) ˖-˗={_rS .\r(U hHKKA޽{HKKJ,ҥKChZh4( @j!WAUG """"""cԩ4i-ZdY,-- 'OGȅ 0fB؇3!n@|nWesa]VYfML:6mz}aú-ZPL6 O*# AAAѽ{w|b.)) cƌAǎsN7G5(+""Xn6l؀wbҥ&1x`8}46o Xz59yaƍP*;v7^eзo_rÇSN8}Tv¨Q0|{>q >3f{d[FF={f_Cu޽{1cΜ9#h4 'N{L=zͯYfh׮{Xv-.^5kbǎhܸ1^z% !gQQQ8wz%Y^=$=Tp9۷&t *`,Y/^[n!++ 兜ԫWO\_PxxxZj}6q=P(HMMV͛7T*oAXXk0ɹ_T`ڝ;wxb[6KDDDDDDD+ `ڴi4irrrPLUx3/TMsQ: 2X';ĉe/]ߏӧiӦ&!K!#бcG j+{~+XJ{0zx< T*Lէ3nƜ *` _|Yf/zZjٳgѬY3ܹs5j1ZNxyyY,'&&$s5?~~~&jԨ/^֭[#"""""""y~~~ꫯePUއ?˗l>ƪeKl.X^:N@jk| o&[n`` |}}q)u{@!Nh4zZ}իW/bz~~~VUBP@Pzꈏǝ;wl*bŊBrr2Q\9^ | ժUW_}+WʈȝaŊXbܸyj%_[RKF5q3&Ʈ2|`wÇsNdee!==]|Pjj*ߏ 33+RJ'N ==6olٲHKKݻCw}Xz5qeܵ:t(-[sA 11W^jѣϟ??… -ZX:ʗ/ógϬZO>HOOܹslɇ?{X`^{5xzz^zhܸ1̙h4ܺu IIIfUVŹs@-rTX HKKVENNl ]լYw^ """""""roO¦o[VVRM߮uZtX*QFXhV\o>>>h۶-5k4=z˖-CZZ*WsBTי5kz職~tj*,_͚531ZB,^زe j֬*U>|8|||D`ذa[Uu Ò%K0}tj4o_AYKvӧOchܸ4|@!j׮u늁날jժƚ5k@t C 3gVZ3gBRr9r$ʔ)cjժ!::+U///ܽ{ERJ @DDDDDDDTIjHKKLÇZ[VǪ?tRdeeaÆX|9sin.,, aaabdS1x@VA`tA"""""""*X%"#"""""""G zxx@P@պ*DEVBDDDDDDDTB\]"#++ ^^^ RDBB{V(J(  """""""@V===l 33V"'jDLL OOOxxKxXE7o";;999jNU1RAU"""""""@V=<<)~J%4 4 D zxx盧A~UW(+U]GK "w2O45>{e6 ^u 2Jo"""""""Lf$|7X%"""""""""DDDDDDDDDD6*"*lrrrRV]]"""""" R Bë\,''*`*"STY{7"2*q?c`F وU""""""""""1JDDDDDDDDDd#V J(1#F@TTPIDDDDD$Cĉ8y$ZCTx˪jDGGᅦBohЫW/t yyfܽ{dZlw}y3EEE!** ЫW/ˮr\3"ۨj޽.\@ll,߿ *J*hڴ)z퐘O 4[nDDD{=6l@n /8xeff">>jm,_ܮ%VǏcٲejʕ+cĉhݺSuL4 =BVP^=TRn¥K/RJXp!ʗ/Ժk۶-"""мys/2e ~|+8%h"l߾>>>x7 %$$?DVV4hoQF6k׮Xx1,Youc8<^y 8J2T*ѷo_t۷oҥKqYL:m&%%j9'-Z0( sEY+VČ3>q}Uoܸ ,pj`5..FBV???q^6m)))/1rH^N1J%Vm0`[lpp0v:< 6킪Pzudff| &$$`Ř4i $Lrr2N>Pc֬Yhذe0flsŝ;wPR%… ضmz-ֻy&֮]k׮dɒ>s4jSL'كYfaժU ‚ L.ҧN֭[Ѹqcm޼{ɓ /`۶m8q"V\ҥKKcL0͚5Hdffbذa&˫T*ݻ[nEpp0233m'DDDDDHKKCZZM6K???x⮮2O?;XcرNJĉѤIrG?ϛ7/^Dhh(f̘ra?~<°qhPdI۷S| Z֑) X7ΑE;TzhpM 7oFe푨Ŝy+IG whV/"ݻ'}!@hh(fϞk"--MpB^ݵj?9sAAA(SIy'O4H/==0k,>k,طo~m:FEE^)S jԨFK_~&w?_~+:CLXz5222ixwſ{聏>~8GLFE۾}бcG10G{Nݻ!Dn~e˖A̞=}ő#GЩS'iٲeӧڷoUk׮ S6n(GGGcɒ%NG_6aU]PunTS#z >2_~%KՁ +hԨUiCпkW\sbSe( .]ْ߸qW^G}$9̙3F ѱcG?~\6z1O n/H?^ѩR @jj#"""""8vZhN:x~`_^՚ѣ ~v’%KO>Qx| ;v ݺuC%i׮]3XFˣC8~* ۷ի ٳ)p5V/_۷tիZsM\rT*qMxzznݺNϓ'O> /!..jLL ?=}9r={M6KLLDŊ (VŃL]ooo/_<smׯ/_|Qv9B%JDz*.\#Fk]} g/X|F˗C`رCxx84MGq(:u=Vukڴ)6n:/^cǎ:w'}o:jtt4p͚5VVJ%4_zsMR*xW.FLL W:u֡jժSSRRpQ|W?~X2gR% JeB 55jZr%K"%%CDDDDDѾ}{(J̙3#FX%رcXnƌު$*]44K.(W,Y"HJJ}v#<{O>1ɱ/00>tZ6o,ܺu+fΜHmϒc522FZ3{mr8q88KV-)T`P&䍩j\t Gη9ɓ'-{޿?ͦ(Q/A$9CתU+)SCjj*z%\TTۇ?5jz /"W}xzzU_~AzVRJʕ+ ʮc*UiuP~0==gvڶ'\R%Z 0j(ڸ+jvvaV-V'R9sⲧNB:m;z*͛ɓ'K>KVc޽ӧrʕ+jwA%Q(([CtLLL]\VZhذI{^߭[РAUD^pAܽ{W닍7o5޽{\:1 IDATٳSfMm-Z0i۶-""".\@ZVw… pveSpXƂ\ȷ*r<ܺu ۷o͛~9NTaȐ!غu-edd? ٮ_3gbԨQxW$9y$-iٲ%233 Zǎ3n۶m?uMB@ӦMz]DDDDD?Oի+Wk`h۶-ׯ &>=zǣ^zh۶/СCx jժeiHGVVZx-+[ocb„ '|mYcf8lI 5 Fš5kܮWW~Ulٲ1c T*[&z֮]M6aܹ(WEKΝqDFF",,$%@vv6/^ڵkW_uY=6m^{5n=-[V={Э[7(O믿.7sLa̙}"::oD,Zy,Z}4mqxvLL /^ A0~xԨQÓ'Op]qH3W_}'b5j:t耒%K8qVXҥK}#t7n1w\O?5Q[B|' ŤI]:NoФIm:ǪJ*aŊ=z4N:v=/߿VoAU=z޽{oB x!.^G"==ln:ALVVA@,^˗/ǔ)SP|y;ѣZJBNNC/&&e˖E2ewaΝ>C߾}VDɒ%flذ;w.hp( :g䕗"##1rH|g2e jGFFN7noBpj܉r Z+ӧO $;;^^^.Ǐe\v >}eˢwӧAo<*>}jXp!bbb0x``֭S&O زe .Wre 47vvʕ+3gʔ)t uի:vhq{?:Dd[O?a˖-CE۶m!SHDDDDDTМ>]vŋ\GG|~82e V\i2]wMUTy-|8~l޼v† 0uTlܸrwa3ќs;wn:>}6l^cOHU_>}`?o)SR {irdaa S_:u}5k3g" @vOilذAL$i ***˗#>>W1---ѣG@Uhu2}6Ν=z`֬YpttD\\ݻSS.Fn?_> d/RC\c-BnPRR(,Y4iRG}ɪh/^k׮aڴixQVV{I>4$_~nnn;.9bbccWWWcŊuIIIeVz<+++l߾]*-[ٳgռ<\~^ hѢJ3g΄/>s[nnn>|8`ȑ8r~7|3g͛m۶AKKvGg>7s011A&M#F`Ϟ=/#YA`j2:466?~[~&SSS,X!wEpp0?~˗/evjCNxb?>{ijjZ^/R}꾱UTT/ 0j(rGGGtڵQTi?^g\c{s^#9s6v1cϞ=066,jW:::8q"-Z@VT.)ƍKW\?8xzzbҥ벾5AUubHMMΝ *?ѣ(,,Dnݤy<6n܈4m.:tx"~'t j*]ϟGZZ1ydOnǎ۷cܸq/mu6n8,YDkװe@,C7oΩvV\ ,ȜA&M0m4Iz쉵k/^Į]ЬY3;w%%%ӧ>̜9bpss޽{ uoMSט`I}1"99Y3۷e֋i3Rt}մqF[ϰ,ʞ'N`={> Ν6m(|{q^9%D"BCCUzX7k,L6 666;#-ұ 1zh49sFRNN|Mt o~zΛ7 Bhh(kwߡM67od {8<~X Y0lذ| /̟?_q0i$aaa/xw1qD矘|_}~'>>Xj&7xn{֭[طoF h6YY!1T"y r>o-eՊ___S]|2ϟC}K݋O:駟Ơ*u ;;wFdd$`p %;w;@MÇǍ7$0wƠA8p5jЫW/L8?Pݺu)<<<Iobr}ggg_o߾}G@@ ѫW/n/_ڵ 'ND@@1|z{Zn T[prr&mbb"Sm۶ŭ[sٳT=a0227.  8d [h޼9acc͛7nj3vPVSlݺcǎEaii4mڴ勰~AS9߽{7^uӧOGbb"QZZ}ᣏ>m6?ɓѱcG۷O9,Y֭[Kz >\^O0NNNs8q"+}vL0A*g֭[[[tcƌ7%%%JϩW^󃭭-BBB"ٿ+W [v! 8p $)k#^RNȅqQQQx!,X{{{O?ѣG%nڴi8p <==a``~III_~@} eڔڏ2Em_AA>Svvv6mԏx-B@@<<< S6U#ڵC׮]aaa#F7oɓ'̬>XXX`ذa)j sJ&6m>#3,ڳCT3FcVMY)11J?7,--1addd~M{{1,uJP_ajj_۷oܹs?66VN:aƍh֬1zh^Cǎ*K>}pI7.]܉fz聝;wbذaJj7n`Ȑ!eْ=YIatRA/yÊknh'ObAB>|(U5/)) @!|}}eMcǎHKK,رcRUzsuј={uqqqؽ{ ZZZ$&&B,[} "۷/͛'s}iiƂ,,,0`tgΜspB-Z ҥK8rگebb'''2חJ RRRX ô/ìYoc077;wHzmR<--z_(J{6LV3֚`֬Y2? r@N=* aʔ)իL̚5KN4 }1~x_8{b֬Y{xb*.\"L<M6Ν;$Y_\\e˖᭷R;j}WFk#]?Wlذ044Tz^P~TB^255ENNBBWW"HnW="e֭ :=zrEuGLUyuZgiӦݻ76o,IQg3?vKUVVV|r/oG"""W7YׯGff&RRRzjlذkuLKKK <G4me7oޔ"ؾ}{,\7nĹs琖&Y!~~~8}4.\ IX/^}U|w5kٓk ׯ_WBii)ѱcGdgg}kӦ йsgN)<==c95 IDATp5W79O,ݻ2V;<0d888iF|ڵK2*eqvv۷SuY_EO0%)(zHcEU=2[MJsuAy޽{pww >}*2Zۉ'0|pR0^u*yT^^gwށLMM-fÆ 0111cT.,ڈ]}8777DGG#//O,!!EEEa)_YabbrPj܏){//5p Hdd$A Ym-۷/tuu~z> +eׅZCޛ4i„  Ϯ3F]}^JYUmرعsTmWQy%"":VMm۶ k]vIoݺU)S`ڵr06 wMlܸQEhh(P\\az9$򄄄`ӦMpwwݻ . //999sܴDJJdСCQXXK"''eeetdȑ#g8q8~$m]GE^^ڵk߂m.\Hm"++ GSo t .Ǐ!-,`Сu6n܈,ܺu 'Nodd㫯;wo o <<ׯ_Gzz:6mڤ9?~>^uY_EiQzvq+>^Մ'رcؿ?JJJPPP jdd`ٲe}6eF޽{qIGXXdYHAuKbŊRϚ?z(/ttt`nn}!==QQQ޽{طo>z mˮm\.]`oo?x /^#G*YZQjrmȐ!(,,h"447o*qn:\rb{e011G}P|ᇸr RSSqu޽EEEu46m˸urrrpAu#''aaar EwCߛ4ؼy3gRJS(k]_3-Z ~">>GgmWe^;v %%% PTx8;;cȑ%C`]\\$㑙00Y9ڷo/w֮]v킋 >UC)O:5k֠nnnXtdoO> „ +WʝZvv6mۆG~~~裏dnۯ_?\xG?VXSSSlٲ+V!퍖-[CNN֭[e˖K.PeرcK,Y{ѥK6[nū |駰C~z_5VZ3f //M49s\VݻѼys ̙5k`޼y(//GΝ+5p@DDD`Μ9011+4m666ᅬ 62bcҥ̄3}hkk+.#>-2on”XR Cf~mÆ n:{\|~!;̙3:::8p'+((( _?t{aƍX~=:uVDQoFT1~x8::bwd}s+T}BU*˗ch޼9zTΝ;QVV&Wرc*6B]j?GGG_ *_YիW믿Ƹq`ffW_}U꽭MYjUC]{l޼WƜ9sP\\ ooo| ݓ'M}}}X¸qвeKؽ{wڵ 6l_T_.пDGGcCppžʸbʔ)Xp!LLL?nWS~+ue„ :u*nݺ__z?cԅg Y(/___|K^sW/|°b 8pB1cN:]].iPSȲw^M^gggݻGDDp]xzzYfС $7G%5>y=UU\kbI5չ9۷o [ƍkb?DEEmۆaÆ1JDDDDD?H=Vܺu +VÇagg_Gnb?Dtt4̞=$"""""*DDDDDDDDDD4 U""""""""""51JDDDDDDDDD&VԤe """""""""G lB0X%"""""""""RDDDDDDDDDDjb`HM U""""""""""51JDDDDDDDDD&V*X%"""""""""RDDDDDDDDDDjiQp1O ߶Hj2Qx:aem DžV&""""u Ճ!}֫[xi9-iХ`EQ(w->sɲk1k _k+;|=`لe09]`9c՗6;\wQ{? c# #NIDDDN ѿdjPkt}-AUhi sc=/ ҿ܀_n9*C}[=_ġA_70*+J Nw-B`#./5,cCL-am!""X%""ҰEHL?rqd+,õyU7KؚLϠ%gفנ6 ss 1Jr1ƒwQYDDD*uymvuE=ܝLaardt;t[nVFaR$bX%!УZZTJd֣,w?"fk%Iz,>g# m`ga/Ab.Amjgg#8AGK IH(DDL~ GYEԧ#U?ڶb IzjTdlDKWsx8AOW i$]0GSx8v-,EZv1f,$yi| ,aeqe%sJp7>[- A;#qZvpFp;Gx֣,|MZ}]mɶ.<Ʒn=>|m-d;+pQF/kBa OC.v4-gCm^ f0F^AR~?/ɼk26.hf+C8X@y%+yvE܍d敼sVn ǯ=OOW mVFp2ĕH,Brf!*aP&nwG3 wx\KXOj9=&ꔈT*KCSrsc=9aq|bݩ70돟%-ѷ3~9Hry:a_6ZgcnYqwnGBWGXNw'SzSW\$&ʭJЩ^m=`aQiiг0["u 7mv&RT6g3E"`HLu"惁](.PJ\!XL=m*8$ ]Z."#WqowJǂ9⯿|A]]]|Lovr˯HZv1Ҳ{@p5=D?ɑۣ_g:?z19uW:凁]TN@Ki/-z^.~y7y++FnP6=]- vG{/ kMg qW4WDDDN^EDDN*VP\׻u"Vj.l1Vp xJwgl'̠jQI9n=½ {<icdhw^"j@2 q^B/'RXuup\[w%=T{/kX<{m:a^{=kMQOr$kW"6)Q2{UsBwtw4Űnm`׉X:x;?dIXx`x>L%JJhlTYpsQ-bapi>v|~}xUPtѪjmjƶݿsp5: Sڧ%eia5?O> ^zX8Ҡ:4UDDD>X%""jdZ"f m%=L՗Sh5o,$+r Xu L u1{vpVZC}IXa(?՘@oVinf^ /~Fc036 z89n)pd0@kG^z>UH;'}F`H?I=]-tmeSUCDZXd/vEJR3hk0'&4VH{H.ƤK 76|`ނɺ K1kRHd ]}pxH$5~(8|1sFJmf&K^&  O|>ebt!"Z"|(yna.yB4D6R:b3~4v+. W\ +S}8Y sc+9=hkͥ;O09ee(:Oy=·uoݗ5/-OzN1J*lkRM)U""" cz)™Y4@;/wE kc=t|7F=B/'X`Y~Q&hY#WemNfRǻx++,)(gۮch<piV^)/='uꁸpucIOr&Vs Jq=&m$jNF%B'#PXR+6j;Oki4UdbpA/ q%>>jm޶83ֵ&ђٻ?~(mH{aʄ-ȜTz1^;w{'Ƕ#12 OZzՇHv2pRJW_6p1Z^V. zS9J~ b3eT)kP_]Z pa&tA}֓4iAX>mؚ-+{_gV[GȬJ`oiOme/1ӆ_3+\,5JH{'mP+)yg,S"""RDDDJ IYOSK^N9Ԯ㲇++d,/\ͥW!ϧaJuuRnGv̠j5Y֦02Ё"ɺd͞3ɂjɨ:x VeEUf`k+[jL]Jf @zr'T}2VXEUڇ7Spw~D&#:!R(9_汛ICha;Z F7n9 z OH}3~dv\6bι6*т ˢsU:F-sJj.M0*E@Aq9Sq=&"!|vq$/T-u**̍p*o0I8,}j sc=D2TtMrZ$ Rh| ˨:%"""1JDDȬjz=  6)WOdcn vO:dz>FMw]S7UF[nF`_pڞmp,< RP^,@tV*+*% uЩ-n?Jp&JUS.OnlQv?q)h`"YgfσϮדp\$Mt}ߪIb䢬B,/>V͍'%AG[$ ܾZC_"(fTam=:׺)ilnhf!8~-KDI>?/b :aPWWƆ}aL?OҤ{&*=&딈*=.Bb/ę5fR]gꞜ5'|BzzK( MjRAM)(Eă 7kS [JHOF$I_j͡mQX\KiUMջV`X8|Og3x:aH77L"2JԚM +6)Sy)&1wgU!惺ĵ$h ȧWX&%P^T|U;=GnZD"ognߘK+#$wLܲN~O^](4YDDD>V4d;|Wf gF.:%pHU5<MA\:'=AӲgvw4ŭ,7s>cT.'8H_APaR 1Or͏ӑɂ6f me?ZәdA`=J˅畖],[QS.T0*j]?Ixm.nf0ГsFא%]/wE'9 {:z .6RDLm+ 64y?TVo"]\}+п :"ˠ&XŕTO ] r9ZS~Qra+c4@1~:E%_mS"""RO<DxtoCdAHz3ɫK[\Z"LB[da16ԁ IDATpR"*6STA'Q) 0"LT6o g~#Q;zgpdžH͇uu5:[+.#4DaܒS# TaLsRթɂ"0}H |5%*C *sH5 %";;7I_T.Y^3ߩ-:K 2-8=)\?DAtoJ}]m=K3SP*wrmab +CߚV\Zt^7_{YTmdw =fkc ?_c!6M)$HDD?IZ"d&> yeu`ȸm g#2LKGtB<Ŀ Cw$1>ryWMʕ:^w? ?ޓLbjGV3)@L {:kjk._L= EdഌwydJ|+RsXKK1$Umfn ?8{#ERϕz&eKvlo#+R&%Y'NJ_n Z"L| #z4Ì՗(?J*ו'<_;W1P^Oc'{>"h+#I:w5&,ҿ9ZYཡpQ&2 U2ؘ@[N^•@\j>ZJEm<D~6]/(*ǂMQZRd)Y3pMeKz9+#1j{KٓXKuJDDDa`%׻X?;@('sK&wLa kL48$",=<&uٳtnhkLgUaI96ޫ{URט!PTR{05+1Ia:,7WҽQDX)=OT?/ ä<LVv-IAsc=7 _TL$g!%b8XogwgK?z~\wGGo[~YŰ4:bڷaxs/ [Rv9d_K$r/3xf[cIKW ImzZ ~$'6)Wk,UT Q#r{+,;;\̠%)Z~QuXiM)^ 9X0ssc=tie\0$}skQ8<&1;=:xXAUĊ}elmf)T-)[kx鯵fv1w;W`g07D s4Ź<,wݹ;"]]m-x71sAS8"{Tp3Iꢛ=5{CZbpvLUV!ƶ1 K1ǫ)(yZ㕎hi-BocniUÖ5qj Zfn .Le)Cy^#5UK+O76y_i[bc,9^Nx+[BKKt20gF!&TX%""zI O5$]zxrmebl3m> նc R/rS[zW)V-GMٹ]uj+P13DfRIP꽶D+\Z&啕@Yjg೭R(%k[өd,J۪׋2~)Aaߗ LLʟ21v{Gh-Ybr1c%ܭՎm$MIӰ3mؤ\^RbӟшMʕn3rKp~:>Tr &ꔈTDDD/LYv@7_{4>[P\N$y3e9iZ )G&֣,PY̓\EwFhjK}++W[pzn? WMC1PTZ|abr{^M#&9J)ƍ{*J,3W~&M{M#Z+d =mf!"&}9F7N?D7gp2!- Q\ZEH,…8q=Inivf, m<`gac]!-c2pZ+Źd &?WVK/[RJf> <Ы#xXzW"O p~:\MDzI>r -]-`ka[ ؙB_OeH,BtBF <:]n;%g1(][Vx޾(<=l&v!%gRD,&‘17փ}G@K$ǮNťHɐ[:%"""gjv 57&kӱCgP uSWa+/DDD$ { :xd⻜RacfEZ₈^<V^!n01ԕZ.krn>y9ыɫ^=]-٬N>I/[՚4DDDDDD/X\Gc[>8/HˁW@ZZ"qB6p2, `cn 1p4<]d"""9V^ 2 BDDDTDDDDDDDDDDjb`HM U""""""""""51JDDDDDDDDD&V*X%"""""""""RDDDDDDDDDDjb`HM U""""""""""51JDDDDDDDDD&V*X%"""""""""RDDDDDDDDDDjb`HM U""""""""""51JDDDDDDDDD&V*X%"""""""""RDDDDDDDDDDjb`HM U""""""""""51JDDDDDDDDD&V*X%"""""""""RDDDDDDDDDDjb`HM U""""""""""51JDDDDDDDDD&V_S [l*} C5v%17Bic9Xbg)ÿU=- s. Uon@3.ڴD8 We%kfvoulbe/z=E@o0z V6YXLDT_ 8:7{,{shG$~9%VNr7A疶=HC}mjk6Z0ӁH!ꌞ0C= uucY@ʰE$³@ŶlTS==miC[o{gc3Q}".J`83>)],mc&ebDZ=d(K,o:g<̰tjؚ7j9R"Kv`S_՗_SM]ާjV!@V^FLc;뜉ꋁU"15EFn (D2UV6v 7}$ %RUc]+yADD;V J>G/>BoƐ@7 [x%e2Fo$wGS /3Zar"b2 ӷ3>{9ҿWۜ*.~D >vt6@Lb.(a; n0b\ޗ*s|ٻﰨ:C{PT`%)$F6l6ٸMdSh6n(^2: ̥h~y]ҭ]uV좚^3Mtu)p) _LFz7w *]Tި9O񆯳;6=_/#F f{μ $80_ WK{(Fc`{x:e8_Qhmx9>teNeظ-ImA&X??l`j:D])3 Ϻzkpᥭqjbm*+/miB~n}1zb2 q&;s$Db0ylLYy=$<{2vw?ٴ65 -x?uu֎ _+_=GHϯHj7nKBva VC56HVǢ 8\C}TXBbdȪ:_ڿIM1#s ^=|MuԕX$Œp7; ĐJfv 㽳qMǬk&V"JX0CJԃ{>@EXၽʇxj`\ͮĞ3(7 "<0m3^9Ax(ۛaol.JafQvC# |q0 c 3*Fx9O0*+7EE=@9 oq$bD: o\ޅX>G p2 6;+cl~lL t6=RA&7o _#9f/GQ#*z< ֆݟظf8jZp\rkdc.0z,$2 J!-uϏdz^@n=;Uv,%U 670_<,(ІzbAPG=]901Ø {lZ_Iϟn' q+PRՈC -;W&`GTr7db!HECsXcC16%y+Hc\=˃n,䅑X,Z3?+E~I=mMl NJԂBB{g0ħx ҒZZcQ|13Fc"m;c3淉(Wnjp',.$0x@ C}n~ |],ޞ?C1>}z{"Sr2ǃ|q5Vz š~c?Q-?H\XJ o k # ,05CTRQ2aVǩB|?mºy: }v bֆ ?BVa Ljx TFM9xc0ؚj I0:|z^YηV905GD>v<>}UǮNnxy\J/O'WGWBQvxoHD_.{֦gcͻ1:]ڿ=Zq~O!ב&ӼtDBw{3H-;S4O׺xoMDt'1JD3*jPQӄqhmmǵܪn/7tR!NPk #xuӋ,l=2rv'F°nKN#|znIl-h}R, . [$|b1=AXA_I²Pt ҉^XY. X$wU=7VÆbզh#[v%nqxkpƆzx*e>_\C}Sk'kS5Qq\ft_ Crukge k%}=gr`[]-~~ 6HRgq"AO4W刻zwlcAXn98~I_^@QEVnV{4?6kfa/w\Z^mD<:/~>jQ5v҆kU[n!f:]/!9R%x*Exni~<^<97jw=+8xhHʬaRJpCHɅ%mhaN{F57s _ad-&9ݧf"J,_' ʭQ'd8p.>=~N6SGRƭ.aL46jW-IBWwʵ(95HH(HOlu[bQPh|Oē #>Lm/-;r. ] !{M1&B"NĶ%q:ڳR7uK$FDD ogs Q۶tN$PQ1 9˃ijϟnC意^82SYŐVU;eʪ!tڔc~<b[\=1jAUWKD:)j#xd2ƴpgG Tn3VCl)Ϋ~jno|񴴵Sr4A75ևh0X*nN T ru;")Ycholbl?$gWrΨfDJPӮUjTE/WM3KЋUH_/~u\!JH}} Ф+ji@0JDԍkUS%=U^sPk8|-篖j#V n }f M9kWzb$&*0HC W6 n71S}ې)Q)S Rr[v>~,.ȐUA[{K5O-j}Zc#=(L+3^L\y1Ep5Dd6iRySԧpY"F-1 8Z@S(uSg:Ey+S;}9n*Z\zҗ~@*:^ζZ:1#{E먊  [>Exc0苑QPSI8 vNM.lљ7nR&Fzk5thh 1VY~ݾ^9¦54]k64LYqE",pO8ۘ<R5LrjG%X;7C|amn>~]ֆVXMɵ*apgl? Lp$@l-pgpc igRr}fIUZZ|7K!Jqr!N&*K5O' x~=wk4 8p>O7uGwݚ'+"SYhlnŬ1hkWrf9N$Bk+!}z=z {]ziq[OѮX;7u8{ qZr@>>P&"O tR!XEduXbp-QOsK;p>hdcHr tVPVK!Lu rmM헑/W hUо`U4 s^9*B<u#ÉL$U7*?߼^o](MieO+Da>6٣]r/<H_8mLP@<Z՘< bHk..S[QQӄ]9՞ﴞΣk7ǽ|7{?'bj)joǥE\.C;Ӆ2_OAz~5JT[m!zڟuh|`LX[!|gڼcgS,N6&j;[@_Oc=-;S9C1fN‡;bWLN] ^=5}3vJ/V}R[ ^ĽA7SpUh_syIHV|u(_JxB p\)_VѭTS]=1w3O=QYۄ.#Sl=-Ӕܭ`mnbp~ièt*cZ"0.ؾ}%&3_JGbF(CCNiyr!.愯ZLhݦTX7//-% j'I^(CpCԯc{aN=6 2St1Su'zvֹ_?TmM]9noW@^׌+;HΪ..]&oD|Zf`F ǩnKYf 2T=hs9i͍摮oVpjgt6&uD W.Zթ]ӱl{X&yzi񇅚Q_]Bz!syq~O]#MD" [sobnzsEN`*""q,^ BBW(GE5 Q&o9Fx /mS ȶ=Wו?őԷ #< < H2~3^=1H˓CbQv8Q Gvn \ΪHXһ%Nľ\!f{ƹRHAXwD/p8~:Ց'{+vnW؊lkۗ#Y)rdc#]`aj_NecF]L-CK[;Z8;s`gi jAzix8Hp$5(l!ft1W_fTo^Ğ3HVH09 %ru [!SV' K/NPGݖ--Ow_ `2U4VP8#VBc$/s swN=}RP<~oN%X9Ws*1vpϣ{v&-E~iꃪf@ѱ7oLW*]T ӐST sSL7KT֨9>RÜ0; +1&_JB02B/㹥!05dž/,؞ث^6 dTd$/? q5#_#m1w9e@v xp"z߱+:x(L!> 5-v2QhjiN231OAYu#N&"f8٘bO".TwDoz] ݛM1&BFacQe-Ek[;\-I8vI6FyUN``HG;1o՚lϙ\\ΪiX< F.yhcd(7a _<<.rf΁ʚf<9K&za|=uHrMN'zaQ'7ŕ HgjmJ]UT7aX,q9 WT -œÚY~xc0'GLr1bS>RzVnó1} \$iB"|{vӹux<2ֆLބ?+_ax `XٕX,2eMx5Jh'wG]c+nTc8:7u 'd8 1\$6# 05j\Q}1;mLWZ<96nKBqh8}1zF9Ӆ>6i7uCCsRUh5XK`]W3܆S ͻۓ6<' s#dJVw)뛑]T _Ւ۵)76iiW(q[N$0;VNjO#=.4v6ͱw;|q8wQRՈؔbt*K%OwDoz] ݛM1&B./ޜW; ZZ!+WұlUOu#zr ]GIUfrE%y.\,|7^-՜JBb`A? I8toL]-}H=DD+K+}SacaLY Gi҆g>L wƤ0'IPЂzluc}9ѻ+X%"""""""""9Vb`H Vb`H Vb`ydN=]#B筩o;墾oͅ}X,B̿bQǀ}G_݉z;KhEGb疆`ߦ?gbc`caxcߦNrxǰo!"3]"""@O C}Ew(? &pn%‌jU4X R. %;ۏe"CQ]C1 GNJ* Gp1-pK$G1:nEyX>ntꈃө,I.Ƨ׎AZRwHDDD )wDDt'"HSTq; DDDL@DDfضa"n>q<4ލFFm$o,{+vV*Ar|4m*dNpƒ)ޘ9mxtsleq[kSYz W'Fc;.(X,BԖ9ظ- W*vnFBb:D])3ڮ"0{p .Rv1_xϵnYE=K#P/k뉐QP3)%vէ]Rb357 5 @ay^eX3BaajZ\ɪ7oE X1#dTC騨iz=ḻ|i>{,@SKvx`4<\ukfv3]0{Ʋ:Z`S8ZExvIr{̿*S((W M|mz/]-!]\sp"Q!`/+x8A%,Ħ?L,$(h@HQm>yz,u`g 4F3*`*4W׾a)1,/, /mI?mfhWLW 7"=ۏg Ǥ0'忧wV5b'M%bVdWcYs!^|7,ab<9_DZKm, dBj)~86k/oip7CmC nT˃H=偞X%0..QP19\I^l_g 6 CVs4?mC#;O5:Cj}ok,g X'dd 0?<:TKjiSp*VB'4 9p)9$""[X%"?Z1}̑#\,Um.8W .Sjv%ENXiÝ8E"ƪamc p-rOWaL|y(Mcuy[o@}S+BaL? ӟW@_׏B5]Khlj\8#m!4e߄xr`N*翦 !n^ws^Dmj ?4m8p.ŵp1Ōpg|r$J*T+)]0eud6+l=0e3^[38oa q&`×HΪTX>G p2 ͭeϙ\?Ýq\vX+8b+7/G?BI6X:TT {`|^r[IUN1eNoqb ň uAo K(j@"ʁ&s¦x+*u8*Ex56̒`y7= 31{FHbsQRw Lv<>}cr9Of塨i;M-m7Hs`bсvش6J*7db!HECsXcC16%oiIvE砤N$/L>Au1k BZ #/g?`!_? fRcT|4CYgI{Bblvדc,|wisi?n~ 2|* UMp7Qԧ~Ar<6;ՋDV?1JDD(G pT|JVV.ρT{W; :/^Ŀ!%VTR!~9/Gs\0o~2D ??\Տb kKhh+AJN%JVîs}sxxggC,a;*_ፕðᾡX)Z-tpǑpeFN* ^d0븫#*[XcrcC{1hm\ኚ&T4a\Z[ua哽Xd9)^X}q|a$Cm|f8'+uO$eToTsRT2]û?%\ m<4[v(_!^:fGzb`;j~G x'\8ie!#g]c+u͂ϩ!frC ﶼ)GD'f͑X??w`Yr38 çόC\+~01_ùk%sxZ)?Ϗc.caExOL.xa3rCZR/峓5?'#8lDl_NAե! AAUHΪT v: Cay=z[+_3cD/p۟Tt4 ȜkCrvJPST_NeY>>ؑ]*ft{u븥?ʆ9,L =6WKL s{iLsө,ejaNk*NRMh)޸YTsIBtח 9AV⠵^rG""]c`P*jV]c3]p8.5͸x3]TOwYR[&YmKʬ豜+gϋ|sXx+YڿCZR7{>^=ץUk-0)&YsNmW m' jU4hV$fk KNkΔU^>7 46!SV Rr;1SѼrp5 K&zb>q:GϺ]wǚ%AaE. IDAT:4ͮLn΀à[bKC1Ɔz˗]T[R 7;3kq1DW*4a>nilý-*I)rd%ok- 8Zfr@_gQ(l #-nP(ؔ ͪ}cS QkBۜ"QG7>M{ګ=ԗ =~=nTDDs$./ )hniG Y+sv1//vkLnh 1 +_.j[F\v~~ +Yd6HH=hr.Zaif5/Af5),(_Zpֱ~\lMUop[AYY=orzx{zѴPIn9F;_WT֒U!haPGzvLH0ehŪPm0W]TV􅏳ʴkMeT[1k+>|| Y 8}PBe]uM%qV*__Х͸u|զю mL[R ;Kcճ\w{ Tծ#MM3A24- ة34>bk y]s}!dm\pۜPb1 C1j֗ =6:ԫ NLr1^S(G"邘"5tV/B@x{FwI(nTֳy)R=b+ |;ryͱ=W\;} zC6:qfjKjz4 s >x % VuΓ^Y>958رbuibBUrwvr`ڈ[iY) Ř; "<+&GmA9ۘ| -EeM3OV{ {*jaef/ۻs@_=48k:F T 뚵SiW(t,Qvjc$/VT7!Jm+ Jg`R<|CpCK$Ez,*U09sGr"=_Ixj`-U3;_SIxrAƅuϯRt* ٕh?n)O\,,5 \$,Bui^-ZǺcdL|0+ "?rT5Ńq|Z0+%E xfq0'fTWԢI(7 #<܆ 7iX??ٕ8ӇڔrS4h0^_ N+p$.: {6>4'd^6 dȪ~ mxk[|r$~=+EnqlL0#+s̑BT6 {@,/vVHkKk;>s]9 X+@M} |],0'S֎YCp$K'zөi6X>ժ~3|U7dB!rk!k)LDnq-`gSJ0yuGaE=C=f[p2Q2ԷGd*uxj &9a@ĥbː*~Խ{brKs&*ct&9sRt+'ǻቅAʼnD*j+AD|͑UXLPԊے֚D?+ENQ- kÜɾ>/;SQ <9$&hCxr-ݷ;&8YwevVLŚY~x@嶔J tTwɹYQ݄cd'cHW+_v_ï礂ݓ٘7 {ѧi&eF<9<:/@X>1K2ʛj/Sc}d`ӏWp9Bc`5Gcpɛ]h-œÚY~xc0'GLr1bSzk pmC {ǻ#s%UM)O[o\ <2FzH,X_~6mI8 qX9Cdʪq0pԉ?W |b$".0o->|b4V&eLܻV Fc;Q-Uv,C%Eo a/Ʈ7菌9VXEjK@O!+#D"`L?Ħk A  ;S!P=8Tun?|SySOp`(WL+EȐUC^[SgSKjfIcC=|7K<Y". M3~M5߽2 QPzYc| wG2'"" 8Xo7,1a#M +Gz|]@\Z  #djg424$ m3D]e`Ѹ7X0Q&oB^I< X%"""""""""9Vb`H Vb`H Vb`H Vb`H Vb`H Vb`H Vb`H Vb`H Vb`H Vb`H Vb`H Vb`H Vb`H Vb`H Vb`H Vb`H Vb`H Vb`H Vb`H Vb`H Vb`H ]""""""qxdNPi8p.na`~px{Օ>p; "]Ak$4Mݔͦn_M/n6齛1M{WH/a`eQ<ܙ{}=:aVYX[?&S9EOּv -U?e֜.Ҁkm?8XH&D}c3G2+X#T4Ѳ6ĕ-d jۙ4V<|]Vj_=~_Bq.bPLXv%ws͏api#=gB5lVq!)eh9k碱iuKZ?7eƊ7JyE;_w{.mOM: ܝ 8;P]~l Ù'B!ę U!Y'ω맇)udhCꉪA-z Xy?/U1 8,WVGc#Yywp6l:XDNz#1ድM~/qv$|5[B!{XBuFynmbk6p6PmJ5EB\l4{9G聣<.BqV[IYfrb;+6KioNd\ O8 ZIɲgLVGfA ~KVuDF{~wxrqzN*Lf  r`zTŚtʫY0#p8$[/ʈpw<^jCd3k2 k8UҩkygB3^8YSP^OvQ-_x^3M{ݸ;i;aQA.k9]wHΩDrA-}"j*uK-nǤX>[#E|q$QAZ]ɺ`|7 g5+Zm9w]13yCpQ'-  eN|WN &ߙ!ގW4QP.^7]ٴ+&0%lC&E',;)ևkƊZV:'Lf "]q#j\A^i]KWG 7]Al;a~T4_kCAݞ1}}>w(#<uJ_JqdoâYąBcv+[kv0r0A.=Ll?G/WΓѸPwyh`zy=p$]ҝ]'x{jyzɧX3O.g :vklĤX_=pAGAY=;|KV&NcWF`6+i$ZYaF8FLC>[edԋ?6.o~.8 9?`m.&+^3}Y03@B}(j$_v`OJ)֛:֚y6_{\4Uk)h`/J M yDf'>>ɃH1{MN"B & !x1fF{_;Gi>mNpfKWN 5rjJ[s3¹(4V31և+&/;?N]]v Cx$u~p݇&g ȳ7'(GxA.t\iz3+Sr]F}bl}@5`ԑ3F3e [Dy֏,ۘi~,a61և>9k6pۜ(T&InCƸh/`_ۺhVD7{&2{LK>=@}cnKwGWlmx8%x&׉_'f&3:DemSaZ w66!ƛ foN\;cX4V<`$ 0>l+Ær@_\XZFhgM#c}9U弧{L=jg̖uOǟ٩녉}xbH<\l':fý;!~e!LU2U6kMHWy4*X]5%S`&ՊЇhxcpw֘MXjG|WM ]GNܢu_u<>t.5^doyGa@NU/_Vg&w ~NĄaoE{+? 8VUosV+ &Ǒ Gv/uˮ+zSxÓ%@B'X!>ңӱ p&<^ZUD!l|F!z+f+0^vf7A.Ćmҳj .}c`$Vʠ;do3۰+D)C^i{RJ)U޷XqXkJ V`Mz6,=yT}etnKWGwc 6Iϯ6$8܇'w׶hVBSGMo~T5a_j+w -Zyo~ yr~<{SYPjhlԌ8oSo7;%k @mrF+*-cBMzbxQf?6O {d,i#t1?.sakR޴jlQЌ5^L(˙1ʟޜ o7;ë3PoՎo6d55VÞHv+f>8ĶEÕՍەlho\5;/8z7ΎK5zA?n^77?F}c3 ؛Z_Zho^k,6V-G`a,ېؒƶOW۶X:?22܃('#446cVˣY8%K|b $-lNc(Ӟxuܕ/eŚtMzoƪ57#GZ-d0y}4 >7*[0eWw]6 G{key,;{0[ύcWZ_$97' ܔ\ rP6 #{iԵ|yIJj OW_ZGe]fÀӑ *.oC-mPK6$r߼;kRq`>]JՒ--٪} ?|*Ù'vcRx8^;=[kyS#unU51UL?S_Z꺿gia6*DVq(^\/;sddd5|;1sT߬Ta]C3;Jpa&_MUʽh|7qVj7^}y(}IOꘌFYY"ڻfd)|c1X~b6xU,N"B 6XB5YQ>J,(K;n8;PS붬㣽zj!N٣;]wJRy` -!ͮ0{4*im_۝_ݩuvq-qa-Ne֘,׼:Sw;du#EJ#\v=ۑq4,Ck3J eBB>S|_eZe6RjiyMc79eW:F p1|] 2G;u6?mّJKɱl=Rd\ePϸ:jvZ( w'[M}Hε:NW{`$a6,kbRl_2 ٗZ 3Zg$պ fYpqa fDvKO똖u+uFGy*]T8qwvR7;B1$*8,ߜ_nhC`q/ZM&]8U$kqAڴ51)Jŋwabl{&hTz6SKWSS2bұ9K\NhVFkv2qfQyOe~Wݧ,-$SF쨯ғTOA4nSCGU2 .Go]zz%?4ܼt0q F#f=V_:k+]'mʪl:Xd"^55U:wG$:zIÇN1 p FFT*IꊥH[`u6/ 1Jwnv}O>iQؚ7'w}Gwh׻iy WO U7i)#|2—OWѯ{S}/B 6 !+ks8;ho G(Ë+~XqѲ]58YPcfhUZpiIB7]4ʨJPYo>˾2%hbgYW d F#J7gOɽ*)k5w]6LyѯY1S d;/3S]owi,H #ϻ;kΧ~KGwU)͒QeT58|P *eYaMk*߱ %:or0Dz+1TzIC:wOS5 F%12|KXşQJ=i\GFe(X1lHįUy⃽b4ҩX׹՝{/Pe_ FϧZB|6ĕf)K,P-&fW?]X:L_5ĕgkf|:x{B1ФU!yEۤgɠڛ-,([eQfOk^NI <\35Z릇19ηW7hVr%hwhovN#8f#w^ib X@ب胡?{ L Ô.6nca_8Ɋ9(gGpC,Ί0?g4VLǽI)F.:_`3^vJzJr/1m=e8ZnϧM#&x;_ڣeK|KO8lZrne~O:ZC[G;khX zfb^Crn%Ærɏ}VnI:ѹ+ t旷X쒢7olfDZb.o=ݟFI1lf^6qüXd&OT7wQ`N̎n{Z}e4yqj QR?Z[4Օjhq92Ѕe̠QXv-;rNB!DoHƪStGͦݟo)so@%u<~%OW5fcVڲzPIi/x?ܒ=WFbש <~egx:0>ڛP?'%Ig8eH{+SÆGxt9`myj'w~nֶcҴ֚aC\͂A{RJyηN1, vŖK' ]'L=7' < vSIs7~/nMY$TRLay=ێ9S|$(1Ǵ>گGT*¸a^&ߖ{YOa' Mڦ]M5ӴU lثU* FxvUuM7jpU{kc&>G;ks9 .ǟf=S}ɹfBCҟ#驜7&}@ r`l̟¼Jkmg?M>vS&&Y^;33!;Y_7 !%\S^/3xE3É r!יF$|s>>[αJ͊ {iլޛOFA5<2We'̟% vN#-g2sQP^ϟ1LGW4UXîRV<%m"f댕jϧ^ s9[nw{sE~D3:=lH,9ۢmYWL &߉@/Ge+9nK肫fW`Sky#Cs"׉:m5S72˒&CRja&&]0Ht_j7릇22܃P?'ܜ4ԑYXï8atS9 E7<͊s`B_#}g&s@B|?Qŗ2 ughGkSS㮥۸HG{nOqE'x3 skL+~FCV[\G^i]hj JkhVtӽd;|;c|) Y03@BRUXCFA l6uL~Y=䂿juW4/[s̚w;;/"!7{ZBۤ?f!TS4BC[ö goNPIL+ !G'0u/:nic2Y7=Ź%ۑzUuMZ_oGB! U!y>7j<\ln(a9C泵 : k 9 8XUq\Gl59+}_W5Ƭ.B!XBY0#+lteOW[b!0@MN6wJݑA(/#=;ʸcxCԷ)B!XOXd !Yog[mW,b0!8]5M- hhcVR+m51x;U5mΚچfyCeB!!} !B!B!D/B!B!BqB!B!BVB!B!$*B!B!dj . nPZoq,r%B!_"U!ĠX%۶܂z{7nvDD3:=il8PȚ}f*6R@g\AH/]yjm▋#KZnsI9=t[(+嘽oa   N)elڳkn(3)n䥯B!y8rՔ tŎ*-eu3͇wPOOah ζdհPɹ]ߓ_kb}lM:[qEDbokERN%fK6>ڛ3É rJ"5a[ε tݞuN.5ih]wӡB$Qy֚y6_{\4Uk)h`gR kP۠+ t0[WDӨӣ7IСÙ'y=zPFSmQ3w\b}Á8V-Yf] tQ$~f`"[]1ZsÅas'k55VŃ^<{s!N_sB!MXB ?͏!"l=c}xL ~Hc5y( u"׉jC< Q^<`$~m!,;] ކ~eB5cVMr ˁL*T*x8M AjN\;=jH?ϊڪ}!0q~Nn$lCƸh/`_yr gi桁. tQ~6O]zCv3@ i\35m}6 J`Ք->8IgŖOv+]!ۨh @'x3^W㣽y||HO"=!Xs+3#& yv]ɥ_!B_=B &2 5gL+cdmAբ )BjfG2ci-ÞncT-l0֚ޔV~B8qR "8Ur&&0r{P`0S\KieˀQ2$UM4Z3-oT8X>?.JP2Ҩ-/׏Puf'ӎ\v&Pm`0ݦ^O)cSOV7W@NOz~5{S(jÞGLS)OmYPc`S uqPO;|B!8S,ۘܒ(/;r9_y;^/1V >tm[Ѭ[ZY_?Mdwk&@q>[FemeZ6.bH?tzN-B-ͼS2z Ty\3.dCkp+eG-`J/eYFhwVǨol撱<(e!ʪ;bcղ~lnv;ٚAlɹ=[OǺx_ WpW ͜(~Bqn!8,܋ Ɩtƪ4_(w\ulR[.[e:.!cD;o7l𨶖jlr6dس{v]ٌ57Lms oV)fhe_>]GUhhj6bև`D7AT=7=;jg,Y!XB "=fyJw' ~YY7F<]l5#ɔÆx)':M3^lU c06ʫS櫿5fiAg6ȊBJtb^'kZ2D'ᵻu'mkc7fg& 4__S-ڬ7T>8QAD8̔X_[/k 淔ej`k*Azh/9ŵ-Yk_caI;c]٩.X吏ܟ2XlKz} !&} !>\֬BӚb4̓9%WN &>U'{aζ=Z-5{ s恫+)[`QIoŘh $=5/ZBn/b>[! vT?qR Lcq*a$A^˙VET+c}1ɹu7Hw(R+?pUiR'e.!V RvuMz!֭Rdveg{z?rXKf3xXx^!B$cUqVpϊJKvv䐖_^Xր5"&=6V̔1hgͿ:tz!>NJV ??F'.;'5vּq i iM-Ix?hmx%CY'X !De*?-ZEIe3.<:[WB`_trkXu9ˣ7̋5WSp*U*-E-dѨ+A%$SId 7EL;E?NýWĐ|aA\35yRkꆏ=o^0<:{[kl}->ث3#p`xS|Y@m!d !]ieK-QAfA)KWeo`v`4 v'e0?+o6dQAy=|hP`Tmhl/R\Щ]t }ު-\ͳ7%0g\ɹ]ηbklJ"Y js]c[[Wx82>ڛB7)X*6yDeF=R5sho˦dK]VX83nYT5ew) И`7<]lthB}DJ[3k- :^Yv#,NzaB!8Iƪb}!&O !ߙFj(`N70oR0 $ N*\A\;C],-E26ʋ`GJI/濛:W{RKYf\ΈpwqRVl!:_ʢ4ƊcR!_SɝKqC vICVQ l~X2|׾;†̟BD3ζײ3OY-\4& =~+wr:5_)oGlԑWڳf5:6n(üuU2e7?ɹ1'.:i(h0wr˛Y0#a-uRNTn]Tub;|zB!8Z}rT*(.FMC pu*FcKzߞͷOIء^5gRJX(Sb}yV$.juoݒh4+lcX/-Gv+b qGc9K2ŕ ڗһ&pK̖1~77Ό]x޺o"ISOonA e꣫2ypFExV(j-U4E 9jxϓ|83[罗Ǡ7yu(n]Uݚuc٣xd՜1QZ΍+/іZ>]fqew!sYY{*SO7P_'tp7Oץa0{j|>זm]s1< [+Wɚ4gIçOŖץh|b}:W梱VDE ޟϲ͙JvZɡHŕ l9Z6eD {5w{;s<rCruWv3"h'YXQ;iHέ͟,Q=w͉bD^.(l8ThVG>Þ^-;6č cX+5 :6*ٖ9c̶\f0y}i4ʔNt+_~`^KFA >nvLцC'y}mqJkRz:pQĆcZ;uSG1-֏GazNT6462\:nL ŎF~ܙϻrV'gMԛdsY4a~<^|Thouw֚?ƤjyUc2ni{51j  ;Orj7; kOITVN8mylӓw9zjvo7aZ#ܹzk0B|olf۱b>^}N=TTO7}OI/=7]] s0vɮທ6(ٮ2~ؑûO͂WO m9>ɝ ct'OH^YRkˏШᶥ[)ҞnI|VV*<5ćymc+#-% sH./([>Δ* VQhN,ާOm*~8rkmju 乛FR'ɚ4|uМ1A܃g'CH/֊?^wh~47t,{OMwƲ͙H*;ƱMfzqs۩ţxfߵMˤtBȟɟ ]Ouoۓ75Oǘヵ TpH>[޴2`͙rAwen9ZĄholՠ;nc|nt'r*6{N?VÀČr8ݗEePMG@Zߑ~_;>@nY8[w;?)}I!ų"xw s ifdߏ]~ݗh$UHQ֧ŠzR0Ohoͤ>=oq}}L\5\3%O^ygO7b>fGi˴wIʭe0*l7S*8QŚZiP1;)aiGҫL<fT q(dRgU׶q"zld2B}L*i<9QB&enW]lֲikp<;G=9ɫ5Ρ?;ύ [ `ORVm"J+o~mS%8ai R17WR@gV ӽv#G[MmJ>ٖ0w]x۳F!&z.9pw&2Ogkl~ro]<18ٻ(ieQg(FHF rhjm|͇L{WRp_5}0zb]FsJ׌Vo'_JKPQNja|\ ȹjP"-Ӄ ;*͑]h2V53R|(h6;O7v4*Ƕ%׵19%ƛK4+GE8s$ăXfyd;^'wXhvH˫ 5x-^\ϙZ4Z-Nv IH !{+K @ӏT=u#qHp IDAT]l?cYaFvrbdTm꠶o9 Q!l:o0<x3ǮDARhP1Ӟ¶qϢK-翁WĜ8?R rkSCc)_7;_9M ÂJqi!l;q9C0p؟nS+Pyhy, $1v0#/{sgv u_Ǖc<'jqXʅz$unVyF f6оfiVfi4CݑHta-XCRT]( K,X\uCw@UC;Nք;abošJ۔Fud]ڟ>1+CL>h  @ aiTꝃܳ(/L*!w~HwOx^:ΤR \ +K&!w 5iNf6uppjUɧ3-k2+7 ykJWd0)͖ܦ$!%jOe|R0/4g;+ lW6 XTwMwOf)=osǂb]v%}g8Q\wkLcl<,Zwvf3>ƒގT6D _1_5F@R͡J>9}ی} UpY5_γ9N*`=(jaY93aδv{c vÂ\. ~o s^A]sӫ~$]o`,yѸ;YSCl9\hnR 024vPIYmk^ßH$2Kxv)>w!]mSwMEQ<'v'kx$!Oyz xd'u` ni<=7ck5K붿ZoʩX5?9fx^X:X9uCw,F{7mh/rֹ||eY)j Iʵ,yњ `JU8o)7Gk>}`2׼Jy! ]//J.O(O®2v@֮G!rG50~\y] .$b cU .A|TilQfi4, $\o<|E,. VRc¨;b@p!@ U@  %ɨ2kEQ;ZL*,@ HHࣟә냯r)=SƆ}[z;^lqv;&5#|\WR#yX\5c$y`i4WMzŸ9 .`7̈`*@ b oRb*,돭)5%ӥ:=8\X+dtn;ݎ~8Rȧ7eGyJ)lCy]umJ۸{)8?z1C\ilQr&$}" 7ܵ0Ci״0-k+|s:yMF'UQ~|==gqeFh oe3&6ؕ[#ǁ6(a\4&FzrP|hPq:aX~x&Dx~o qm'JX' cJ֤T6`o#LɆN2JX#S +[򳷖ST'2Iʫ5uLJl+WsI>kW3"؍e.O{?2%à绲sct;$Ψdi43PL!{;PQΖ#Uo Þ1avDqa"ijSLn/]/-J.Or4j@U q0 |T*a?D˃Cxxy,U#}0ڭiU4% r˛9W+ҊyTr˛ y9j^Ç`gkMǸ.jf_2eVLa`(nep(G['1-?w;Tj 9eMۙS 뽃F30}z/`gH1 t)|+NEeh4ZdR sY4.@/{ldW~ԿG^FCk'ʊi!SZʧ3ٟ\nK3øl5wb4x7֩bra4k>>9`@,gj:|`y~hBݱKAS[>PoGV_Pj 3tZU&eD7gz67TR x>>ҋgs~lw!Iz j~AvHH3iW뒉CKsA$*5vd-͎{E1| -*eVTI:Og< W$H)Ҩ7?|zW'2I+W<K&al6rR yoketh~:^M3 r^wȤVL ahT5v🝙N7}^GzqpE>;ԕyyͳqUh9[?SXCھgN cק`bpSp4w~H?7;5gjd{}h nˣh|sP_'v$7u@#> #iT'}lؗˉjõ<"%D߿8:v.x[tOgr2[}`2vr:UەJ#o0*tCka$r-Ě1h]ڈqLSnH{PZpSpa,VT"a0O^i,7Z׌br7mJR pwapo r8Jk[뽃4+9ԝ'Wg/aþ\^tڥg~T"am,e14)pUTz.fL+kW#HP4t(5\7=d<>1P]oʝ "hiWb?i|po(dFẶ=˼ˆ{spoc:U w{&K)m PZeyGyV{y;ڟFc"ωooƺ7S` vw^x{W8s:L~.#|Q4d40ۑ3ˆ qz+c<$X93 _W[ڰs+bI$l>|O^;P!><hx  (dR_=p'4Z-uMr˜pmcԟJUgQT7 cbO?Equ vx:L$K&!2nIϮF.r`޸}&uB&!KDְx^u׽v[+C<x')kJcWŲj^oWsIL[DZ╽lY;?Sֈ-/8ƶNMȿ eΨ%D8m[X_m`dOů8S;Dm҅;u۳xSFs{/]ټ1 0o?;[:`+ٮ,-B*0-`u!yl=Zڟ|l ;O]sER9]psMg)i!ӞǯMϬ5KcqhjS!HL#G2x`i4Ndu pו+,>ʑ|#W=Bƚ>BkQn$ݢ{;ck%E8qz[>/f:gQNa}h'2xkL=H$ =0׽oN|Nv .6%Z-έ( 9Sf%XN9#I{ÑB.vONJΫ>5Zxn)=V_T"!%d ;:>1]md97%,zf_Aj)Jۇ{)D[EuǘOm}3~hMbZ%ΪwhhD!2-x cS)jA!2coe7\.8>g\3Fتd5ʬ~RkyXm/ /Nja'[1mvmHCyWN&߉6%7/r2Ԋ?;j{1>ftŮ_ZQ8I8[ 3lWDzju!86˥?'{shWQk|q\OioN_eL˲[+9nQ^3شVgxZdt}`Kwpܜ̉j4Z--*ڜBeQ۝맇T)TiP4xd3Bs\:Gq3L򵷑dm 4-{ΔUx9[ÖÅ7.`G_Krx:W)э O tx21ғ7&Ԧ J63FM };>Փ_ ީfþ\:.hgR)S+Pkvז⣼tfd ЩҠRkٝT ׵RXn4{9^8Y2%d{fwhO.gB' `?0;ot;)+w-JܜLsVlw׍aN=[nU l2}j^ ɋ_'1'{;&~[䈓uo~ɯhfיR@}k'ydy,Q.4t5M|K+: l9R-shML+%ݶ}}|@ wB!-2߸}Qt7%s:O'B\ ,EQ8[nGraMD;Mc8[PGFq;Obn>‘*4hϪ慕cRH{>ɯ3G_m nV㎥Clء)H6\ڕ i H'BjttG&N yȯg=uZ*1fir[V{(FH"t+gr G;ᅨgU-xT}<7Ƅ<ڔ*K8SXݶ$I;ڟ>x:^^|lMu7iy3|< [ l~bI!.l?};2Fqac%;'veѡRY@bz%K'kdol?YB~e3H ?61Er< ޺}!W$DAe}]mKyM;q<4o ϑaNelu#_׾؍ZUف>,F\; bT*Z.Faso_ZCFiQsc2͕a&'8+hlU넍:r)>:cgjQC\HH0գ7bqPz%y3Ow8F\oPSW"ҋ ۫ +MML}`?eU[&2Jt^Hs9M9r2\K [+9o1l T*1xڟhiW넭:~#b.fr0&c܂>\`jPQ܉-eD^°^w>7mz˅uU,jԢ^eiWIʫe\i\ dVfI4r:U?uwG2룙U<{} <<6NdWeѣmc%Ֆ5BFSa@| - q/umF.R-CZI/BKFڜLe^['DwR׽NQlN5Xc8WMʬGzb˽}іa&Wt*.'[sSCXݷ5vZ'JШI]> hZPxEw=Kn[RL |TƂT*ʲ6f-mv_tڔ9XpG2LcmnL4vW۽ݞ/f3 L1IZTτOz z)y:`c%cŴ#ªf[f#JJ%<ػqٳ̳e0sf}b W6/7kYk.o2q/qՖcL2TzB7`HyząWKBj x8ِZaR~:ك|ĴJm-۟Q\ɯEd R+p"$k_e _X绳9R޷Yl W뷎'&QV*a0^e{;hmGm}rA-FlK|,eP7gWY`0x9みѵ.2|N N/nt^M:nH$0a0zSG#LO4ZTo>]ؔ'rKo-֧?#M剏"S99"qaPGȫh6]t{9#< XQV2}n!>dOg$i1kifqF2t y1.ܓ6ChK{rͬ"6Og,an?^Ψ4|XthFq<(l68$oϰ]< 9Tj-KY:1Y# t[ˆ O9.'R-)Cdž{3y.:kNsaLejOu&鱾ܿ8맇Kv Sc=tgQF=2ʆ68j03-ևcPȥy95zvj-i!Xqu";K3[C|yhZf <#׉JmscPqM uEIʫLsGs[:y,韖+@SRZۊ_=jjŕcxqcY9ZEٕTj8'Y4+ە#}WGhJڕjZ;4zK3z~o=0 ㇣F1ь'(Y3w}떮.UkJF+;Og, (K;k5Kbv5ԝ7sSZJZQ=jN:j:Tۺ/v*Ɇ6뷎]EbZVrK'p@ Vê@ hd5r2#|iTs"~NPXMoyh Mz6kK 3ˑf,}9某Z]ku0|qUp6䞗Q0=և@{2Jk[vc9έs˰ST_&)o`NS83 _7;ZX7g.odŴ<h-t\cygaU + sC޳e@L+zϽT eu\1){|WoT"1a)ŞlR GdKs,Đ-)ȤBQ5&Je}7;E;og[r˛ؗ\f64Ձ\ܝl9—#|9[Py\=V[)F@R͡J>%}@/u,OeRͼ5GXNOƙaL=uml;Y§;t,S'ha]Y}rdٕEuc;0w'kj:|-Ϫf#iP')kd{(xh2&1WzN԰a{MmJ[|sO\3ʐS[Rxh4Zr$o`ཻQȥd6g,sXZ%|s׏FŎSSqu8V#>fm*]we8w̋Jrܽ6{EqpZ:TȮ᥍IFdžN\k3Ð ~?ѬYbo;wLVR!7OHOg[,&B/ļVo%|~=hhǣ}:ݡ[:ydy,ޮ4(}SƱ=k`!ܹ +giH%sM&j^o>ռMwXϔem[ͳ v@Յt{Y~41ҺVY;Nfrar -*6?-*b ~{.LBMc_5UڅL*)!<™v^E]  @ @ @ Ԃ4@ @ @ d @ @ Ǫ@ @ @  aX@ yQ>7gk߿G1멫`\.@ .°*@ ӳHϘ{e8)?C.[ ,Cpq2"؍vlNv ^Ũ7R)9卼sU-%r<8F㋗ yMc*etVr(/ % cJOaj˹fPx:mc">gj;| uJ.elMmJn}#yv8w=ht].3XAj; rehPZwdҮTd7IF9?Nu; ōI\3y(A\ulQ q LpQDqE"z96XwFZgwkp9Xc3x#U7@ aU @pɰt)o`dXCeC;sFVgcSbyclw[!JJ`H?VL ṯNQXՂkDsׂH9 5KbQȸC4) rqH$Wu^͖εjl7FZ?8Ams#ʱ״UHOyz/fld,DKW}j$[V/cp$n~; rUiՖnOim+SX57Q!<$E-x:phΠ(Ix{D~9QϢhɛM{O,al9RQ5,0Wou]F"nGAU3@sQCyX)9 T“=k{43^<-+Glç;2yrILBd l:cÙܿ8g2J1$~8^&S95Z˸s^$2>>o1/aþ\^v:{yYo]86]9G3|W6/~Z7ڟWO▷P 5Kbxgk*9eʱ4u|:Kbz<5oil@  B@ .%*ڨi 2pm(S{#Hę*F5yj4Z-3V͍0UZ;yqcsFao#ٖmMmJ+yqc6`y7g ަIyt>f{;ZTO~e3j6ϥChy5l9RRAҰH;NpP{%~u נOy][1&{+M /Ny[9f6&RkjHf_)C袠rhWQk|m鍭7OMV rkbo6V_l2md)!<뢤֟dy|0wYdt}`<Ἱ%hZZUC2V2.򶨜$Vl۵'Oeϙ2?T@JCJQWONAc0_'l2 PȤ8r48V G2 p!!¢r8_.|;zW5vpF%ύ0TkpZq|K`@b @ \8$ɽy4fe%̉dn sF붲w%Zoy̏ `QzsgI{͛}{Yܮdor9 Ձ\O bӡ|SbLcg-Se\O~۠Wߣ^;jQ[:Fcڧ{eQ9@ @@ \j4)-obDFL)⎧ vVU4]ZUl:$r?wS}V }n)Ϋhw2B^Zx{7K Ǒ=w'>t/2Kt[G%c&9Fu5Fs B6W䰔& Dңdž{茐Avy#^.\v$K=]; A8)8̋ITk-ob|Qz&{V⭨o$.-l_OeRZ'_ aU @p)t 7 fݶ_Z*r$'5ToS 4ɔ>Ȥl䬘= @tݮL^a N9ZqL#eYm+S HnLFSkvA~sGzh\ L_ an?X<-+]l9CmswgMc9 [+9μph꺅8_KnyO^3`x vû/3k䰔t(<$.hi/KhlU1!Cޠj׍aSb> ZwxT[{J JRDĮ`*^v~vz- b`A@Q^C{/Ͷ l Dw~ϓ33ygΜ6V:(WDU'Ldkr{3o0!;/VǾLcSux9+T*Ai%u3Z;secKu?72;`tI `F/_<=psPk lgmOtx@4D cU @_Ǿ Yǫkw,two /Zg;k޺u Vhzҋym]*9v9#̌Ϭ>Sy18ZQQyv$7*O'9,`?= H$Kxf ޿c܀϶PƳQAmu Xt=]ͪТؗ-o5kzs8WӫNp(HDֲ/>=b{jgG{n4EM'KX4s{kP,W:CaJ8qñ;O}K1t֮͟x$z+7exqs_>6`ڄM {H&ky@  |~$$z7/,29[c0-_W[ڙWd0o62??G"="@ @ uP 6-K~J62RZOl+{#CKqf٥Cb @ "@ @ |7%y18YӮՓr?=VgTtb IDATdw')ƒ_76-vZXG$"@ ^G>YbVcJņ&ߘf=a+H>S?MlY<ӻ'rX6g(>6+9Sx+>,:gjxc&g;ora0o^^{ +: r+qe |5N[GVTM"臵\]ǎSe|5Wc\͗D2q7v 9E|%3H}`*v֌ wgٜ̮dTSsczb(nv5t{;NGIM+G""kPa8Uƪ=ycvּ}Xnxc2k&23;F5m`QJ.1*[z5V^37W MjۛǖR3ޚoz?W[/M5ͭF.rŸ`4e+zo3+xi(Y|8 KkSzXߘK?qa<|Y,1bG[*m,sg[F5+ǧrxûwv?Z#7v6-ҍȯlbf% KyxPuao#y1K:xu#iT3=wE^T6pt-jmz0,-#Su,~e;K(K{ys3i&'%}QֶrhnW&V<}u"3+; ]Oc]iQ۬&>ؕnIqM 9=clNLz\x:P٠23̌Sc}ؖR }cPZɸkT=yD$ *Mi(5o\:ܟwc[knK;q?jcKeP/^Z2WgqJ8l~4:=yQ\NvLv|ǟ O{[5vᕥIֵ;ZٓVΥ#LY'%<8!FqK'9cq7맄`NϪ=y61&beVhUky4D. ZfQ\^o@o0<]lM}ݤ0&PVG 9CZ.u44 $RTӂVg`́3hzڵzݘA|+J뾗YXY)r!NŇ[2̧냽I/Le3m:VCa:*>۞cڴ:鍁Q}ٙZNS^UCk4֊SiRi0d~ קrלޫ%H%5)6H>S/Gkr)ux_VCiy<Η-'KtaL/I6H6 p(tB,Ia&9=^9]@dH_'dT ZzW.m: iV`\6oInJOzQ=ꦼ^ņEsP9k1AHx^G h^9c Zڴs* +x @ cU q&f5MD8rgYoy;%qf}\li`flZXgEGZ%-W !kۗLBNCK;C|Ge*I07֞.oa,nÃ,j6m(0vG=Fb x:p0eVrbϵZIo Dwt5]KycyT5!Wt3zx=]Ϳ\JVgzK8]hߙ7@ ூ0 ?}, Ъ}zq^HZ{م]3 Nw>w7fbS(#t^$KW&VUkT#]jKՒ4Սm> kcc-cWZ9lkY)EuQ0R?8sGp([pOϾʹP!ֳKE-:(dԴҤҰT},oNv[磃W:άؔh/+)myɳߞdٜG7Gtξ>к(? bj|{`C\p`eW1%ƛԷc%Z4 W[vm^@  @ 8]H= .քz9S3dwR j͖ L68%rt:5 )*;e406c"EY]+nwq{ r1A0zNz|(lNrÔ^wOO/gtm#9Slh)d }gNIM+La{FYMjۯ (OJz]닣(k#G/m<Y"_@V%sgF8<,mԭ%<BDɨJG+_G]l|lHɫh-/H2:c+Ulh@ ொ0 ?* ׎EŖg\ݕo SQZɐI%[: W5  >uնↃ RtKDשWZ1ݖ׏ ੫0<07 0QLݹH$pPS jhtu+vi}Ae3oγ Va"K} u=_aަ !n,1:X2` ?nH$>z2aNd~R R[Gf<@ ூ t|=6f8n jԬ;TÅ6n{owʗNF&_[ͮy4^]^o`,6/jfShQkyc .]iUkycs02)M* dY͝+KtZ-j-rkxd:".uj YQX˥5zvP ^-w+aJ8;SX>uzT۬f=1M>ޚM^yS?Qn$$=th IMucu*4]Ɩr.6,!>ؕZ9]ܿ oB]Vn"!36@_'jxz nœڵ˨ώ%ڵ<[4EM%?\Ч@ @W@2 @ &pի;{C6@ ]C @_-Z7Ю_@ 8"@ Avs0eW͞?Z$@ Odc"@ @ @  ˷@ @ ê@ @ @  aX@ ,)W-@ 9U@ 8BX%8Za28Z^7wd6'f|X'bʒ$Xý_+~+~L/>1<E(@ .hLim++6gҨj(]16FR jhn5Սm\ k'/(}r>z(kЗUƑ͕BH@bC‹P(4ؐWHסU_yXR97;f'?L>@ \8Ǫ@ )\79 CpFѱP普w0N5do^^{:B<X6g(!htzf>=+@3pL ImT5CBNU϶g msPB\ˤ7 /ZN֬~` s aLfWZ9WM!ݞ5naGj)_b@״zoqAT5|J%|{Tm38]RM{9фz;xvLJ㱯ݗFfȯh⣭ddz˘=OG%ŵ-|5y5&/Qx8*1*ywc͗DjGMeV42wDI6Kf:`0Uʩa\YqxVld~&ac-磭YcJ+~zt:V7H^Ywʢ~?&GsTZXu|z s v_q{ihns0>3W`AR #ݱSI/_3(kpی(hQk9]ɊM\16O=>Xگlrn!^Ȥr˛x _KkS̮ds} rep"} d6l:t%:OCk;/=Œ)$v/^fI8%/R *v}bjhW7Feʰ@^|G/G!pH ݍW6`c͗D2:wGU-xNj0Y\9.'%Y% ,_ ;wBv.d՞\y` FGaU 1ebpVs'Lk: ^}mJSʹiZzᨤ>ݞCvi.Ibt_͙\7);f a_fyMLΚz%&SYmFQ~N448UىBl'ixBoB?:_6]__KK߶%1qL*NTT}p2pwk`BKqqnUI~/єS /9Lp`9-T8}w"ɩafJb4\w`Ӷftzeب9QE896SY8z",e:H\tE%)1k2=C 8a/ÌS!$RF1"! Hfᴷ[Pi񬡶ށɑAo-2OYLPauOfxBlt'R"i&yFdxn |xxz3#W`e̍-G_ݭo,Zĭ#]=>J+o#X8:5Θ7o+6e0g!ݞ[f!ۑgkJ)33?7[kZy4Ҋs+۲9Yw`xsCZ!Jc<No )܃n짺 `WdR O:Nm`W^n$ŵ-tnEBO>NQu J9wAtYo0xv2YlN.,W +̏_-oņIN%];spThoz}r`NqghiӒxdF!#Þ'=Ny k9,Q&dRZk9WJ5B(JZMe6թH!VG3T /8k_IFD,EAU36 n<{p23v_2%1< kZspRlVcV/0k^iY4:=+0ˤ LZ.ΚVQ=M* Um8Z['!nJ/Pkt#=T5]V1gM/Ě nEim+w|6 n<8צp,GVݏoI)͛GѶΗ)1T61#Η9xܲ~nsߢ`s1 Ɛ{ K.esr ^3ߝ֚GQku}x6-Mmx+ҷwSo9z7rkWʹsPƗNuoIzuIM^4 ubpT>$Η'"@JL(OFGzG8i4pkG 7B*_DsGB5'%#=L1oT lI)!îJk#=LeE:r>\6?W[U*uøe 1cx+J\vJ9wB]Njr\w_F'+alI_ LEs:L-YMb/Ra4q. IDAT؅]KL 6DFG-sA_cxcұ֒֝#k;*GnbgfѼ=T ao鵿zT$(1ij7Fr 7Fc JE;:v+|kmRL-ƫʅyT͵VJ7}x׳a+iaLxau 1FiI;~>ĩdrC CA/Jܰ&C9FG{!}bxs?]yDF!Cxe(ࠞ!."@ ׳zoM* *6+bDu *>ۑc2]?9wpt5jOmy`O{ҋ9S @[U{Pktx:0&ƒL6qHK"KAe3G3`0*ݗUݧ)6H>S/NJmzpf _uXƿ?rp dR۔iW^íYLR=;7GOz;RLhyTTrYS*w@K~ˠN]7{fYCxu)V7Pkd{6ŵvMw,թޝKFNo`jD80. k2ާ'kri~d6V\5>W%<8 X[K..ȕ25gu?-ԓ_;Fg:X,_y:uN)gl'[K剣Y=̯aϩ5{tAEM!~Nusߟ`P/BN#)s;e^\^oZ2}4 _DZzo7jB4Z=u ؒ\u,.Ɨ#}Qu)|뽿~5a7rW:EcG{gϩλsf}S J&1H$澰/nf -5G*p4;3z3*T35,y{7w}x/o_iUkt2hdR `s/me㿠j7z=&> nR~2->{s'm:yȎu (z+j,^+^Ω:rܹ֠r?k^y^K%aѯq,y{7Mim ӟ'VGo0$!e"HH-cK[쥭뫣%d{(OJ=P*=w/}cx<%H O+7[W;WIqsm$h\_0gJE;^< <~0P*:r<ǟ7V,@|ѻtꄓ6_/;^+\E{'{8PXU[մYD‹j Ҫ$a ˣU7坏.㙗PZFP@ ]<_=rϾr)xy^T'.潏鷱?rxy8NR]Lcx|UTҪ$%=ۋLc^|Z^{o1_}?{;\Ч^ש~>w_Bg ᵟzv!ac-gLꋬ~:RHR/K16,@c?o>=!~Nxt/#ܩhPKs?1'~N5[ڴ!Q^z @Dœ sǵAIf%'DaD8rIf >ζD4Xfq!^\?)}j2- rF!̽8$@Rp/G;-c̮WZfUiӘ[XL|qH^Ew6y=sYFg&JQuKq0[ڴ7X~=^Dfqkr(29Uٖ`@w0+ogF7IhpXƧHNi#s*Mf.K b\'Jb\p&Fn_"h re׳8?lO?Vͨ {6% ([SJ r&=qr#B)kdgZY؉b0@^e{*#)-me1;($W; %ж/zbq(!Oڙ\cC1\pǤ0Ӳl9Iٶ;٩pqnF2uIBpW!tv&$!ƣR]DsH4v~myH4%edd1vT:n s3{3+0 c3ߝ27wD!* -?߫17ڸ}>kcc-cWz9l΢`[ٞZvxl3ޫNg0}I Ȥ{?d[bThu&tO2;en|-5ѝ&>,iXSj]ƶ>v?7ؙVa>lܚEu9~cv=ѣQ XFz26ғS#XS9}<+Lf VXucY&Ký67޹4#){ $2~265u$?67w#~cV㹼v둶窞Jaʄdbu<´I'H%"|˵hkU)~q\ k̗f0vd:!z021ZG^{ ɡsR[govȉ(fO;g-Qa$Ğ`pX4zT U8Mk_Z7eq!s ,3|Eӛst|,h1䏫P/=dgaI?3^ ."@ $ۉZX%ۅfqz:p +s1qV ;T.k$Vu3_Ҧe3ܹr?^NE{c"=zFSX݂F'.ȕ.m0Vg Cz}v<`Qtg:՝Fr#t`O{\͗>Y` `fh=8Qx?@Hqc3&*crK`0z.Ĩ0T&}zq}Ιf'My}+~ݼ lw5?֕&&\$Ηm^pↇ'[kӒ%c''w'8_flgDz;tr4:=yFOľT-! Vu%6ȥGXU6>:,T"A&pp+QQ2Ks~LBAU3~zAjKш`xChRiޱ%:6foΔש؛Q*9]eG ;6VlI.a;{&h|c,H_GF#JnRXelKlڨg0yZ*+Bc(E㈭BBqTRK&ņz{|6Xr$E%(; "pJ㹤\p~`_A]idý@3SҌFToZ=kQ&#jltJe;*%v`XuWLZf0vmVQ-ıP*:+E%dz*<-"4ny UKfvlaUQ3DNY#v\5>:-$$R)6p&fH%hLQ~NfFZKpbL-:UUB9.ȕb`wz9.Ib_fu+x`~ sN>]c~s``Ԓ11H-#L-E}xK~c<)if cNgf7r-''Abs!׵\TQ6nl#>ؕ&$1ԕ-/ ?_4Zc''pf'#HS ~`;IN(nw% +M* g:^wS۬6Q~=^delIZ u|G2;џhg>Z6lPZpA]Y6{(WD]`^ZQ]FCU4dkkKGzeR ޘyM5:U5M#.hv|rD]Yѓy97Fg5Ubd0sJ&ư"%D1oBJJǮqk,穇DN9=>hF$dW?GZ`oʆcL[TAm.2ꌆPiA:ΕWgǧp=Lkk-v*J=x{B3/r| QW4"wSULm=NXYPTT9skwGyH7#"OzkpwkD*S`GFvP2*̝> %90ƧkPm彽]{ek9_cIؗ 3^ .°*.:_y1|sTrK L֮qל|ydd2ou-j QX˥5zv~mӣx4ʩ2-'Zc庉a+H+oOέcLc^b+_֔ҋosr m,N#jSu|-۬OYB~ENDBuwz9lϡ#֧Fg"nY$ҐI%$QZ h/]njZڝۧr W{A{SƏ n4gǜD:QΆL5yZɤ=\aۡ(keӮV` Vp[arNL-cv?1.6uDc<l 7w0JjZ@3u_{^Ū4|~$$-ύBϏ`sp+xhgᆪd{/ CM +o]ZvU@PqUdEEQ:JB/ %'i? d sΜ̼w}s}^Uv$wLpvwD eש2r&Ans_6r)C+yu VH$)4Ε4X ¯*pmYjG!tb( ik17! W8OY]3$U=as 7s˛{/wH&eXrkhiv7`~Nd/]B/V)~D&x:tz=H%dW9 ->ƿ\7}_?<%e#̹$R&UA <NAAFGZA-+1 2;[=ߔ=zVG)/L*Ia<ԕVx拴ˠxo6 IDATOх1)dZ59]e;UFgW:{܊F<)jb͎ ÃX?157lmW6LYy{om`Mmrqx8ڒUi%&X @AAAAAAAAA_1*    `%1*    `%1*p:gADݕhIR(/4,+ѰG2{ABeٴ%TZ-T"a?_~I eb`U"+mfs([/wT~nL aD!ẙ2y9˜A ?&fOpV.p8 8=۠iW Wh#k|B׳z{J.Ƅ{xЋ~kGDu烇X`ыݯ^m B E֪_!<׃Z@ա)vL2"}-qyф:8]ͪPjGW,I %~;Z=X3a^2%?T>9M 䶩 rB!Ymg(VAQ?1/o(}̅#↪UÑj ˒15ڏs,I r6ކ\*%\ɻ?S[R 6ˎ~?4Fyry:RlcEE*9(#9Yߞ n;ώ$ˉ6 ULZZi:hzbX1?kmI9SƆˮߓއL*a0&h+JG{39Ye*Wtz=*X3vQdT$)w{r*ytX>7a^x:ے_jHA,3oW;z8Y;?֑~.<0!U6vg& !,aQT||b ύ]3Gwkgښ=3;{je+kvfp(l^wγKqsPIhuz䇓~7O #ӑ:Ub2pˈ\26 [9t _}jngKWWfKM_bH&F H(m A0!o;*[h![c G xQƖ|s~\]0^h]5sŒ9yqpvoh1Cqsdn#Sy鋓d+l&Grs!>N<2?0_g:=N,t}` W1*%턲Y͊hlQfkΖcZ_)SPku,3>[kr@,Áǫ_r4TʍI!~x~?/fͩ9*xQ<4_W{^*jQqU7q:$db(/4y@c.AF_6`^u 5*ʔ_YFl>ϻXĪ&r~No뮯<$.]y_R+C vX 4ɋ7B!??˚vcpo^tjAOɥU (oha"Fy(VZ5Zz=_)!n&ýmZeq$=ؐKmS+"kGZ sСVƚ]̊PT +m#L3nw,-^=SrswQat>FnSl9ZZCŽ ±t;{^*f ߁<4:=zPvSiR(H ߅OA;[n3KyQkiUCOS}wUߴ3#cRixȘk2~y$ͱB kA;O2+߰>OEt8U={TCYϯj/Swp x~\_z [Rȥ([гT)E5*4Z=hxt=TmݞNyC HE 8tebt%LY>˫%I38Y[ت&3ߟ(H-tQC>˄ۖ4jUm(ELۖ_*#H:cBvppuΒ6ȒtEe2̕+di[5lr!n/N ϏSwשxdN`Mxz1k;>/kٍǹ~B~D]?-k|-&9^Os7;CRrruTp^q|iI\ّ C7I^NR.6)C[~<6uiyp);_u:/bGZ)O0v )UE|y8ms۟~?3O6}`=jF^1^!5N}I۝ɬtaQ><8w|IÄLNg KGT(z8{ ? xx^4C<9S]3L|kOL5gniu81u#Os|-3%eȢ+Qy~%ᅄo)KR&||-=hѳ}kSkiQkG|l׀dir7س?ojUL9 G \C+ FGI[Of-2[@Gn{ރu̐&5mx8ZcR/YeJ򫛰t4>8]v*CԪ'~WihnFYfHFqށ-XDn)?XZ>ZZ]_pwAKmp X`d'?,1 ^?Ov܆ۏ{kkhPu=:fVfu2 xVGnE"~Ύf0wDavJj rcF?;~WH!O"Je\{ͩ&0гZ5l>ST4|Kˁ%B d|WB{u_4(#ɺ4PFs=SkϷ2s%0W2J?IeT^Ҏf+ v2ץX]wݱQAs$1U,/$|k=OKgSHs8z8jձz8WRqs3~@FW8=lIrCw|y8kboÄa>\/ F]ÞeLcza]6/ @}Sy AF>kjgF?_-$lj O (n"?7{~L+aΈjmq߻REM,}KJp/ס2ÀZZ&|گ2 •B pU62-+O|n?[OmOEq D"H'3,З۫2 ~nnO牫bz|am6{KwwTtb( k7򲪱Og[w>0[>.8ΕT*A.p0"9L)euLC*k8_Od!|Mӟs&1Skb\ ̕ Z~(';$F( U߁o2`k=OKgCFq pwFRc)Kc:F1%TBβIaȬUq8 HHÇg)k&1g{}Y02zUȥ,fь.>5}KG::~n*ܿ \)R \rÓԩ(Rfg#C<.(v-Nfh4dR yA{Omx-E.آfCrN h> Uc95U*/MzwL୻'boCE} %RԆT*AզGbF?}z:L[OGryTIUV,&>@/4-Mi]3;tgEW"-3*X}KfmSV[PwD?^3~sh/Pjuh}hM _1z=)@L9WÆV?05f5mO[4:=n ޾{6:󯭧^y8tz=kvddqBmK+c3Sq03L嵩wk}j{ϖ-%(#/oN嚱ܼ" G[9,?:U{x}婥q1Wfʐ^+{gGH$2xa ޻a͢J KGlKeC++taje+/ymS[BT]B@2^;LJ-qWPX},Ll2%0FX{ӣ=}Vm=cw49ltTm9J\q^O@9u)j`>/4l=IFIu WiUk(?}Y|Tjke <yQ*6pAb?EZXDtUVN0&p1ӑϓsɯjٖ~8|;'qa{жm:~%IkܞbwA_厀 •4;pY5j9-3&̋9ZxQk#="6g/~q9U%^y7N!TIZaὁ ANg Ε6 hەWdzf9-j{^yz扗Lmg/y\5  8UXw ]Zˏ%V@T V-nw&^h} rG٬&[qH=[6mD~_tXe3Ole$ ,WapUA. wML6 Gٞ?n;ϊ$ˉv ULZ;gvMcj),I ͞F۞N~uD1: O'[R jy3T7h+)LAAK-G 7_KIm3ϮPd˴|\ȫlmg.Whuz,}Ytz1C r-Fpv-2?{fE21DBFYo}J Lr6$"JX6)#qSG{39Ueq,H q!xQ7 zDwMX?灥ymmZn[GbGqu39Wcn<׌ʘ0/-??SV eS'GX>7v l<!0>/g;jrh5xTnJ %~;K܃7n_i!(RƄ{آU?s qBq4U;ϡj5=kx۰|N4#C=KT(yw{:ժ~.υG0F$hsPz:QjýY9c 9 {f gJrjX;Ԃ3(Moz/J9("9U[OY$P'[X_a6Ly}=,`syQ;s8AT)[D̵#I$pKD!Үeϙ2@^}w; #ˉ69ZWG ПS#tM't ]Fs)ִgkݙQnBGGXb*/m>Ivlvmk 9(X}$:=d^eJ"xPl)UyJ.#zPl5\uquPcY/<SK)|Юc꭯=b78]t~0&ȍY@DQlݭñWY;𞓝 a6K1<5ڏ޼^׸0w /XswT꾉|78 F%{܍U*'j(^m,}t*1^?9LSYֱU5Q{;#S%LJvB٬fhlQfkS"h!MAձxPqXny{Aw+EX:=сn>EqYr*cP|Z vX o~ZՌ ťPe;74||8ld4 vH$Ȭ(6üyq Uy8 #)j2̮eR8健ymmZB8UXǓBժ!1ԓɫ[R9SmzwpUV7:Umr[ _a><5*= x;۱nOXyusyS&y?8׌JT&Ͱ_>Wr4TʍBx<6's$u,NͿC~L1v.}Yl=^T*aZw !Ȥxڦ6=xekUdoKim3MUMB'0Wq,z3WoL$%|(LJXvL0O}~ck &ʱgG2*ԋ#ݖ2dS}o~[x15P/.|poNQ\b#O]@aA cyhNvÒ;k۳pݸ^׍=_I. Xnà{tV$#=yfqjT`/' ^!F?,z1υgw|ЯcM gGZ ~xf9[\->T6tሳ sVgTar/.28_D_u<% ~X}ZJyFcT2gD6ʷ.Mﴤ^Zւ U \rNgR)@ѡXa/̏_ߝXn5:U7?I} )R45:iըhl>Z]${`0<[\)4vή(oa"FyjϡUEjڈ r3} ߟ(F94}gȤ9JC<ߝZb4D~ ).ކWJ3 3a7nI5i~U/ʽ3iboҤPx0+VCknY*WZ浵yki#uz=ry}ie>Zh[#['q형rN 7%ctYIm38׏1z@=gL&[s=6%8S^j ɹ62aXa1qT5yhzz8]Ɣm鶝 7gV˕iFըRצRl͞& F16 '[<8WZ~?IWj{Ž ?wȯ<0;OVXk|XFYC[Fqv tPv:FɭB`*0+5}WyTڱaiBAuMBҼXʮ2>pN/,IF.E9@VedUrᆼ-9~W4[j#a.ͭ*iEuP,#]`tn o %, Bto=hW';}MCNE#+E+i0mPk6`9;s8M1IΨկӈ r3YOVכFҮ#~h-“ۍZE\;6U;p`:\cF&%ԻrlocmG9WS^±jV)(kV;>99s7{V^kw;.Vl=%ַgZNxP|=͵W~.V6JV5}^d+S1.›CUȤK5uLZazS%K5¹nO+ z\VXZ[#}`2fhK )ny$uz}rP 張}JtEHUl=QdUii;>}GHv--j-am˞Ɔ{YKҞm9ZL䣽;g\3eȤX1UP>׏Xbz'kWc#uLSrw)Q<W(mEͮesOd2+c*^ØUT]ciݞe}`2nOgf~L+aN| jhXBwԪ@ NkA+X @K.a>ޟ=VGne#o# _!Zbqw;[m8tŃ.EE} v֌Gx9& tw ám̥yvo']Tǔ(_ld_֨Z5l>u)n*ZxlA%i,ut= "Qic<[Jʩ:fs͘|s𣚥y\n;'VG~US帋5Hwi\.^$O :mPld+Kx8Lne#z\I=ӣnS@ڳ25cʀcN_SYw7˝lr/Ζ1?7{:?;J@ 6ƌvw>˒k:X<2:2Oyi-jr+vX7PVB=vAA^eaӒziiZ RUA.Os5cb#b#tX˨PO$k:=06䌊 G^e#Ӣ k 亱nw`0:(zjZ$-NjCJ$ȸwp<{|Q.96/Ig:>9ϧ]ˇ2y s/ݐȎu̥Eץ Oww{'7ݪtwpmx8:VI dx-]hCzP>=ͺʙ/-FY8fCH{؟[R:|8%C;"N޷4-v9߅emt*Ψ?qU:ִ#]$na066֢t-j!@^Z:ΎtωbJ2{e ZSwmڞ}y8LJ0oD []kJY}3SJ$1Mj>)/CB 9u(Tmz_\&5b]gRf3.hiSf$Mmw>̒k:ocj#C:Ά'šHɨOg[`g#Q1>m6vZxvw^(s}ziiZ RAjX.GEs״a4˭-<ca5<όiiאY>=b-tO^gLNFA53=kI$Tm~$.vh:[gGZ?ʒhiOIMcO=違Y|d$ e ;pvpu<([>Sjђ4h_UV^qζT*[X!9r.V^[W $K74:=n ޾s6:N AN,JrFɇXOra$lըx˓\CH~g<^\2-+gSZ#PN`#r<`%y|lmIȤd+7i}HN5/oI;Ple[j1e Dtj`E;ҥ # c tݞLr+-:G ՞Ԃ&(kC to Uf(*FGja-֦XZwm[۵jώT8N.֤oOlO$pƣ?Rrhhw'~fAB mfn?M3zb]V[PwܟQ[OmoM OrmzJV~qR+Z۵ u:52vx@}ZrF2q^?=I%44\iI$A~)$*VA񅱔6`^ۭx*/}y>nTR-HOo_qrG粚O=#tZF.I\.vkIuyɖuN榷eJA   \$6r)Q,JMSo~P ~?²>[17ƇֹƭB.+d%-62)O,bPUAK  ElǓWdKVWYׯߺ=4/w46i}\ktֲ#htov rw baI.  ڈAAAAA$AAAAA(    &f     XI bY2>tP q'gj0`dƌA9ίSuwS[6-ڏWnmv}3" fٷxT"aã=H1~Z慁ua;j`۟nѨ!箤K'A_;厀 uͬyek; 9O.U?SЂrGq2˔Zot,Tm+x=5U>tz=wepWx9qˤ0e^p7 "݁o^#|]xlhuzZZZҴ\(5ΡlQ[}T+[/ B1*phUk1𿗳--תPku1vW wjot,s=^m*{ϖ R_a>Xe^p7;vV ?BɢvDNE/i9%җV[(oX ] E3ύ& VjV'@7d2 NfWarZSdwNF69VWG ML*a0VNer0lz+,vnN ciR(lLE5*kĺCtwbNJ9qG)T4t\HtƳ-Y!½mr8?h23bk0mj,UmB.%L?FEL+FF᭹o{Ζ!|9(]I>{RQ0/J^aG;9O]@Pwl8S?W:9k IDAT1 p U6vw&d+1?!;kUۛC bxءdVt~=ӇHMS{ΔSdQx̰H_-M?Y̦yzBM5:Q8ώmSӆM0<S,t||A,%l:gL۰|v4#C<ˤT(yw{:lDYa><{HZsW溶|8~T^$J\|ω&N϶|/(}-+aIjMHiÙ7"\JK=gX'WU£b pEӓY]v twÉ⎽BF53tfgp_dR 9#߿I#B Vֿ,SW'q|Cv=}ds(y>ؖj4Jn+LOb/H+埒M rwRЂL\3nnT;^V_gG;^4U2xnV'5O玏`0pP^j/+ e 'Or{[~\4v(Q)޾cJ:[5O6}źy,Etuu62one6}@Rp#γyQ|8GOݛpÿwPk%۹ϵ-s"J]4og38J u;29]bU ޓMuc [tI]4,=G.=d7ZT{ihn}*k# I'`0i5y4ْRJ Սz>ؚ(?><J}{eC=Uˤ^\$+%q^[䆿V6.̛ft\=-mK!1ɌdlJ.Nw3EV4B{/eVTbllqsbƞ:k^l _`2ls}Sh!30-4ycq&E)uyIE#Y-Ll5yZ -T)6/ ;ҤuͭFڝ0/)Gʱ`sgPXmeNTknwMNJ(IfI[Rm?dxIV)Il8R(|Nl y|/قOV.&N1+TN%SFv}@ѪYmVq|5Yѧ޶g_se{Z4WԷ@WD;N%:* -^`UHNnk~9Z'URP`ETxeC2,H+ )x9k:W>ٜRzp6'9?ꎼJ_΢`d`Uzb<WW7X^U\g6s9#Oq'mcV۷'=̲ äy?:QƶԒ-,\kR5I}~:zZkHcNo)hT 8nqP)YNeL_t;ۦr!Zw *ݟ.U 9l}~@٣yV <6+`ZQq!Cn[wfs@0Ū@ 80ߕ6_Ǟr Ϭ9GM'OYe20o<^0\8Qd}x &3pi[!;h w>ݑ%ܾrn3O5ٝ^NneH#N % NQRE}Ϭb%s 'R1$ bѨ!9 Ш8kT.q,g_h{ p.9v9h^5\VE}yqvf_+Q~ܾjdY}FF .ډs%:ȝ^?溳-lJZ%/=mǻW>%s`2KT餏?|bv%.C9Vk^0-]b}'ˉ tcwFN%'&)B|u @7" 6u|7 >]F*b]sEj<'Rxt4WHU_'$fwzX~ϾzU 9~.*-[,ֹ(:ȝ]v8nzt(){g왧m쩋G pP>fݹlBk>겯3gftݓYΊVeg_kWh18a}@0t53J]l6.yh0x8RZ /^{TR$!a^RXdjg4>(;\7-e؄`K~ͭTv,WzÛj{\ٓY.Ⱥ ߾OA6UqtPvec ͒`l Z?Jyo%_7NMid޹R=TX`Z& Y=qlJ)d w ⦅!ܱ `/'+p[d9]Kw!<}Ye<yɒ\Y,P0t(r)DsVҩs>X؞V̘6- aozقZ/g yIMnlAQ6:yGojekjI=哶{$υ.#_'vo27z[yi &̝lF.RzC {OY2&[FRG Zcc'fR8ٌZ6BvZ#}{n眾jk{t8dVJ2'TrC,ndqѸƞ6ns6耣F%Fײ[,W=$g]Fv8nWٻyƞ!1wVLx8k:gu9;u< =s_kuقB.{;@ +@pI)f#U+ybh^{yU6[Fx"iFV$2 'mN}].k|NyAy8vwo!CP>]-&f-P^ߌ=QRҹ?]lBAֲoN)N%oJ.b^l :lIkj wfx'[,m A@7`.K^JY]3,I>]ýɯa0IbN%%uu;%FzEv8#6@ pm{=)y#Cbv|~\ Bo0=%XBMims6zn$N,@Ja K_³r+r:#|(Zb́\I wZZՑ`Տ=ӉN{ž(k&Q\46Zw+!̳@}r/%~ZdY=!NۮT =5~@ Xgo0̋kER˘<ܗ7:lq,RKHʫ)ʩh`f=/BC'&ض pcXG]7+[ &>#/pW<5<,ARx80e\O eA\H׿ܕ6J!G$!q|껭hf V.)h,~'boқhl1j4SZی1VN%-Eiѿpiko ev$Ս^*m|<\6)Tj4QRcqX7|$ qf-SPo;ܳp$ pqPȃKpժl{{Sϋbz? Zɕøs~>3KqwRs0i+Bxv˯&Ǘa.bjEG~9Z(ĞU&s1XA?J磢/g ]qP)pv!):jZL=ZrLf/dmΏhmrNEd(2.Fdǁ Jjx1x;K~m3>fʇ|;yq]朄/;Br[ ~*dmݿ8;goߘTȔ~\;-O=ݗsJJwGt,kJ8QcmC hlx{S-el7r 5m~*ٜE#3*KN%.Eo4Id/%M$xYZ)k=v:r= }=ao]=˥0>;t]v\M3"d |BFNy- 뭧Lf^{׮HFi_ű|u,jUאO^\syxkjX`V.Iw|yxu"+f 烛VRPw?}b&GvɎL*Zx1xhnCb;P~6В8<4TlI:WksH_:OV^#U<̎e 4ٝQƃ_:"1ĜJV5 c"Ưk1pPvI1@<}M;Eͫ o=UgޜƣlO+ =_z,?&0~WN QDqw|GjZ}lY1sZdI B R_k@ 8Ȧ=,¸ Рo/ Z2[qAE#)n9g$Fa0Y=/Q;?Vc<~}<sp @ @ D@ @ ~"@ @ @Ou@ @ ? a*Clyt;? >:+U}>ʧ8+y/ƒ`ud{ 4OY4jk:o였s?3yᲱ\&c?0,ʽ獴YYyb*!vDB?YyLΤ9y w./_1O' kKO[ Ey3 @@bN% -v?adsKGr;)kFR,bXxok:'JuVT!!ċގB.xd7gs:Sq͆A7ƅzȏk'v^1)Z}7GVr⚦@ ؃X@ f!khjƖ\_d{Z uM:*ܴ$Wjŀl9Y=v >~P p$:н?Cg^ܵȮlPkU IDAT\Dt }!,VYef?]:ZjtL'ϕWICy w[YZO & _, ߳0BٚZ“kXr%5Mx;brzucuollx8E=Yng+f`b.tMc^z"üIʯaXxxIؖNi]sˀF ᢱ! v`2s 7~=NC[iev$hUT4\/vVWwʷux~X~K.bljR|\s^4#x 4;ˉW1!܇ksgzâQCd0|\(k|@xt0 Ձ=?ʳlyjY*ؘTh+ͫ6wGML;:ٕ\c>fphULW'Qe\99qA8iT4dGK٧Gt0@V=r y'όHd|b(Z4r+}^cy!)=.Ye2…c؛Yۛhj4¸ Pd|slܝԌ Y*_RMiqHBiٕQGdAIa\6)O' =O 3"ٝQ8Q٨rVH4J p{I>u/{`2[ؖZUSˆsEb=YrjU>7!(rygsU:&Grn0Jo-ծI\ޖc~BapBp>h5('qӪ/ӹ17WLf  dG4 f pQ϶Yc?gtӪO2zݗͦ>۠1üix8Q†^ڴ3^:a^r&Gb`;풵mzJ8ZK;;yQL k}]|k~ a.=H?#$g:2s1{: rp{J'.NI{+b2[XG+csFÙem>#6-yՆ뤯9/9HFr/g 3us 7Kّ`LFgYcn$?F]sʟ@ .B**{3Dﮥogɟ؜bu@~=VȤ_Ƈy3=ҏIe2XOJAĸPoJ/#̎dNL8/YCPOUQ21‡Vf*n4J>yA7Ȯh$2G/g3lNGUS(nbIB0Ywµ"pRRیF`+B&aiS`pco6l XB.l޿8VFPnP/'^Xl\+bNbA\"|Tw6rQ,KDžPfA\b?>ej! ׅw:H]s+;KY<*eK}\Mk}!lI_oka|8IiX0}8#|xft tGWHrA O`R/O=Ba Gp>.#cz8yw>#6#f]8[V"B$E8(^Z qɥ d2fXG/oHFTrH:?Ѐ֟#! Cbd^|<9 (ߛw!]cxQ~jCybaJkѪ<$[fGonڶ~.k+O-K`Of"yPO\?&q$ ZsǕ}׾},+sۥa.Z)uG)n%}4qGil10*ċ/͋?PN%ϯOIͳe iy$ tD((2޼v ydLf }xgUpy#Jf)ky8cY7Fq֙n\7=lHZ' ^.X=dyTwq7\hX<4*WN/#)=mwΏh2s[0-D`WT60>̛G/%Q9e %*Н~>5+k=)7hXOy} lF`M]o=ܑ\99L ݴjV.msxgsZeNyq|+ ɘl&omeyn*:mn _1fnx7-FFx8tomޙCc{s{nZs'{0\8f(/_1C_rAM:زї,`ȟ@  @ UZfv(`v0ve'mJOg sc1[,lM-aSJM!x=R0y5L2vb6}R.CV299m̈G-Vȱj.xu}WLUKu4+RRS K^lg?~"u~g;׿Һf2Z~E/SgLfxQ-^+m/ӋGKN;U+kA |u7á<\*V%LtT}z.zm3YTd枅18t5S9X̚\<.O]<6`Xzj8RKPG ؔR̵"z7\*., un jpժ|RE5MzyLjQ-LޛhUˤp^)I@έhI4V&6/fwF&V#oz}Qe[t  R)a4YXs0Lۛ56ߛ:w6QZkjn5lƆz sׂ^5C9-tz#oLAR0uߠ%芫V ?*_Rhh1`UmvSJ%Q[ә;27~=NA[?QRCyL69#ɫl9, ٗB1[,KcpƇ`nǮ2j uGqZ߾?q!iZw 򭴶3+!bύ R_hn5?W~I*vkGuW6'6꧍-^%~.%#k=V^LU7) ]w~Vʱ~Xr{2;RRjȈ IcMO_Q墑RfՂLj<K{cC4έvH!}ͱɹ=UrO-&u Π/ΖlHC :#F@p|E0;&{72 d9#|OzIZVQŽ̛q)lj_\{wFyowslb(JyJ/5ԓ&()rh2ߙ5s}'˹xl)55y,Z)Ycc5;lp|n9/g Qd7ؤ3}ի&tGt[kC=.oF_6`3v`:8Ny;YZONτpgURYtgfpNs?W* atQosZ'~$aQݗiof9º?Dq>.\35V4Hy uAV5m,2.ENoW-%M[e荶j-F -iɫ*BX4j24JUiBN l` ٕQcxs@wgUtJt,jVㅵ6o)$s%:Н^7溳FE}۶ G X},ޜNo$jrX7I/Zi\jԓ]@ b@`2鰞*4rB9uw#@H̩/s"bwm*ОiV.#k=a{rsc9QR*NjjI/ct's'Jm!W:ky.aT }]:ttz#Y <8_ 8Q\{ݵy گu؟uH_slorn)]"wG{C=qd7F@B*:ٕZtgf?C<8W˜ r6 !{2˙fFb0I|{l&evLNSME rgvtNV /vgͬhƆzSQҭFjZD:O >.q@wK+\)iռ|xɭAlRط?JeK-FtdR/i:*oImcYL8YV0g=E;N=;pRBfֹ&.,BNEA]\6 ͞7s뛥_Rt$1:i[靊mn0ɮhb+&VԵ ,ŵL MnEc%~}*&r4J&E):g f p0_=vf eLj;#{7 3N-H+oM93A.܊@ Ygkh1]@PO 1üquQMNECxEr>y_1_\sW r6F?ѻJzժ5ׅ/S$j{t= `}Xr/zp.Xxd ca<{6q O_2oW F23ʟZ\+b%ca<7}d04jo~V#]l ktU^BcQ)-G QJZޗ{EcRQ),If~l > 2h5ٟUE#qP+P(bH nҬo1[>`/ sb=Nݿte\ v(1{ <]4ͼVTΩl`\7^.;GGO!C 2xgs+dL289(wQ,zݙe:7ed9-??g FxrXޒ4zC+&ǗƳM|]xwTodYVO#ij5?gU/N dȳǰ.1ցSp_RgHUȃUڷ'e2rr4֚Pk*.loMű6tXJsQX:}a"n1Vjl@v]8Nj{.{-i>7Q(2%WNﻝ{8lR(nNjADmsȬgD#s7{cqѪe [UI⅋VJ%-{۰Kλ gcI.?=('~'~ϟp  k(m]2/Ԧ"mX;j4#EChhHSk'5J@xü=zAGU:G^bN%U:*Zȫjlp{fJ#DeC Gbw*gQ#v0E, K]!3e/{*]8r]'M y\W{z|~WF}3F炋ٟ- h>HW~I& %ۙs*yZ &t\N2;!88^T˚9ey#*' vZ/w:[?͝vj2zOظ xcܽ oMKyUqc>ٙIEC O/~:/Y~3ʆi_ݨCyoSTvkSx8<4Tl^T Ӈk5r(~Le ݆pPnZ)Go4=umṂϏMK}`Vz_8 G8̲1pM"7{mWkK+I̩d2ۻ_[cn4%ulI->@ YbU uf3&Ɍl>Ӱ(>ZɎAUΊcpsT?Nj t"&ȝׯȶ^5ڦAKW0X_D@ sP ddn,x:iX׹.|c3- lM+ɵPe<5J.zc3 -&pp"|]ij5r4SZl_:/tz#+xw kw,==ԋV~;Vė{iY{*yU f,[ r晑 UĜJ?˝sѨΔ5[r_dt)o;M;2!\4&aLfdWo%wrˬH=qqPSs%-`@ 9ͯ٨7NԝV:KF3ύKw: yNEq+"B.c!gQMȽ_;1 l_6!'wf(ON%b\?.E\#ovs%@ U@p֩ll ]@Nϡ*Ƈz3}"ɘ_T Z &ƅzfWF - aX,zU, bR/7|)݅qA-:paP=YNf!Uq,ł\&cB/^6߁dfA\SGQ^B]s+C4LF^U#N%w(Jkxhx 񠲱g~ u<(\9K'݁}ǣvDξD )E5ڑ vjZJj( sq(d2ŁVC㰴)0 RRȱ$>' m}XZfB}\mNCxgv#7:kK6C )$!@#br6Dz+yyw*v]D^!${-?vdMz|>|f=9 Ax8'uuݻ'Q[IB'~5ty -7k0# 0q [X=_騔 nuցU[G_}})oeM V؍J瞷|]|͍m`oB!EB3=i] 1:܇q;V0/]pS1>ʏ-%4[7QଡM^U#vxp=t":L~u#brm|'YkݢICjD(z=B$q˄H #Z^\{ m '|9S2wYqy:h{p?vy\ FQFi]3V4)m࿷okQ驋 CydeDV)Y}8/vK{wOFۊj-1wNŞ@n!To: MmVm㱕c{m%!j(sY_!DM=:܇ FUI\Gy<4{(UZ?nva}*^-5,zg;6^u b{Fiܛ)|rnA1ZIFIol>I3a.!x8ie65*>o|nvDr$LOqL Ҏrݑ{p׻;kj,ÿ֧b_FPC3@.u<>>l 77SK7V WH3 A,}+vB^U#wL^ݻ't͡oΥ =x}m(U_e`xhPhMm\}3KuTz$/9F#mS}H v2^;Iуؒ^c+ %*B$*$h[u2Z`:1=.g{5ҮӋ8]z4Y?ɸ? :Bj6?:ܦ.]M;/8;~8Zث,hx\?z0L2=.gq]d~YUˤTjov3> MeVkqQCl;Nb0Ѷ/DUbKێšJK!\72J F# -x:۟ c<8[EەYFLZJ)1ܵs*-^$o`z8⠦q~(*^gSvڨmj#G;v*xFW*,[تSnة7o}+xzpMuhA\Og̀^!VM'鱸9cj!Mml(cnb̏w,Rj sߥ7I^͡Jό9Tk2ȍ拰}ge2?CuN7)ͽb.w&DUZOsGAޜ h~53zՖ5s;P;{<έhdP%՘.0E F#}И{ozPJK>1Ȯ@۪c|ޓ%ݦogZ n)c^~nL `Kz UIlJ+f"0&—Ɔs8*/E5Ve}}ץ#o (nwtybqse3 T*:j<6u>&[ Un(eg6:*ϭ:Y#|H+ `W526wG-`vB U C*MYg>vP;QT>;p뺤o7msuJ!NBK2:e œ(2;^۔C 0͏:!>| ԦŞ^̸(_V1̡\9"mG>-#:9?_3js׎h4v+%1ؓտŴ>k[mg{5Ms|w$FkGRhzyJQR^ݐ;'1,ī[^>ݓMa22r7j2NDP05֟i<8+5)4QQQh itx9[͓QZ ̏۩FtcV:zKoԮ'F${ܦELڔ5jFy[zx9c4%8^T" Z8i4H+ၙqx8imjcnBPgcv| )T'KX:=g{5M:&dȼaM:%b(#%?- s ur a0ww4%9~> _1An*GPhN䊤֤3?ESkjaɫb\`@'FG-B\(j)T l,e^b0 -ɯ̭Fۆ'K2;ʸwnO&j+vپl*ә]70{RJo9I`5]cዃ<8+(?74S:M@Vz |u0>3I+aվU1 JPotz,dbr =lh!wNy?SINEΈegV>CsICk;.@RkR X+ nO\+?RZkC3^sRe|$ȭl?Nx2n݊]C+y?^ħ{{.MVJ;?TNײ`D(a>.4s n8NNO~;=ScHZ:ƴ&G`2QaMFI6?RykDut#!j)}J\=L~U#|{6^ݘK7hvOUckXo[@[2xct9ù^5 ;/= Xag umGK nǎR=Xy4Zhhl?NMQFץ>v]?[kvZکh4V kڗ͛wND۪#Gl&&NZVI]3άڗMnU#ޮW7>ؙŲYC4hhnѹ ;jШ7c*g_6ZoIhJ #ϡ)hǝSSz%[z#<:7fҦ3G˷:e%:H<!B\ IϭBJd)6{pyhJ[_zm !~^6;؝B!%C!įɒ:l?|5dSJ#S8_}*7B!%GB+w0Mc#=hJ[9^XCS7\ !B!o2B!B!B bg@!B!B!~i$*B!B!$U!B!B! !B!B!Q\,!B!B!/XB!B!BB!B!B VB!B!b$*B!B!$U!B!B! !B!B!I`U!B!B!HB!B!B1@XB!B!BRYB!B!B_*B!B!$U!B!B! !B!.^\=" /gsU'punYB!"U!į&35gOw~ReϞng G񖿟?Ĝt6Y+<8wGMog탳X~D¼] }us!0&X~n3w]\V5(?-t=wB!B}3 ߴr4< ~ʹ/JJ*Z.v6NC۪״׏Lfi= B6|I.X1:ܗ歭 ۺʥxf2Jl.c G e`=.%bgY6s(7=2'Jɗr9]^sAm,eN|{N,9طCc0_#=;{O)z+Y94jy ׍H~Po͈cɊ]Ɵ-OklbyxNDu͔5_lXI- A7si/KKNj.XтjlTk G %É_u@Uck1y A8jl,V_ۜkt 'WS^y[u,{N^v#ükr4T4XAi~;;&8sR4sШ4rvfjt<4Ò)1.{,aD7&!ۅڦVKyt\=`+Xj]~Do=ɱj>v^5Z:y`/!ȓ%SbvMά2ۙiœy}?=(?7#R `WV ẑa:PʷG,^=ahTJGwt>*ٞ19SNpS eY,pUpNdx8ixIoiyl&ȫj1OgS3%ڟ cBۿr\j%@awwdr8e`K'G䉣ʆڞ 4-u/mY$:fG>U;r7[}cIŞsC IG,9w F#Nֶ K kZ}XtzߙeUG >?#pV|IKOώuؕUl,el"I/偏`/E SfLMyDxq]*wLhA5De QV*(kfxdN -\fĶKڮn">珗'ao\<6SـE IDAT'E36{|}8u 0vY7epèp|](8qXbݩon۔|V=moKUC pU'Jsr[=%;&Dq0GV|z,yf~J/^͝09_Wj(]Yd c{O֓2.{ŲT ̈ ICzq- =X2%wZr/{.K,7my͏khjqqL&'YKU$1k!|?Ïǭeͬڟóy3FrZG՟3ٞaOj"`O\؟S7(<0#@O H+՛P/g^q, lp_nCqܩwMkR>y0^: <߷Q]n,h7';u4_cͼ< F2jjN6|zk;;jt4n^vߞ6Nj؞YzR#Scyi]*Nj'U!%if\ 7/!ZGsX^brFy7)%3=_~4ŵM.[I +8[i ܗm9;\`/γWd{}Ђ=y$Ine#v*fSGP/aJqԨDLMi6?z"H^NjNadr*lsYb0Sq;ihiՁ) Fg`r6uڵllw QT k2vqN1K/2pԨX:5/C\ĹثydvwwkGQƗsm. QVԭ?^o`x(X֭n|x7zW 奅[[=DFmS/p?]9%+vҦ+pϩ^e} ޲CoʵɃI-UжSڣTR72Jyi]*:=I!^J_O\Ř @4c0(`MFavK GT*ȩh!~nDqX|]kyʤƴ"B<{Abl%hwV^ۋ&D}K;ٕˇ1r$ea,}"v*TJ}bѶ.FlP2JS)`sÙ4d-!xk[NJQ5шR y/0[Fs O^u?xr~kR-&|sSƎR.ªgn:0ύtH6쓽VAk!)QϮ#K_Եڣ̎AԷ(5MSTijkRL/72/g|nv8 RP+8*cKF -:=(ݝb# w'M7ׅ%V5ZtzVϦU6M[)o4t_6Ƀ{\cʫĀv8RT4\mbk([wG ϮIlZ-O}{kF)^Ұ:}9Ԫ m:8v*&dQf7 \7V0&}m||4-:=zoQmehGUW5GfsØm:+3(LZA!M6mk?#*yeq;2!Yha5NjjP}`FxW΋?㽧T75j%/?Ny=7SPEg0\ ^ےNRe^g{59* ihm۩)~o6uB+vgq  VϦZ¸H3ϯ;FI]Ӏ\:3oX0vj%_P." OKZR`n%nNp_TJ)Uvd׼|vfU#BAvj%MJ^51ŵq&ԢnI ]&Mc#swo_`4`GUc+ o76['54X9};;^cX):-_bʽvrz^ً1yF|] zJE5,&~m`ǭ0!*6FΜ`ڮ#鿋xElݟzOW_^Gm3 Ӡ&'Vs&dm|TjAkP`>G}}*ϊA9 #FmsCj!N7ijMiCRnɖMɣ```ͱ6(Q}l9wXswyC3U[;{h~уP*MqWfcWh%oSx[G|z'\wW 0T^ki;_>|~ƏXB\44˫o=ɦby|nbyTDV*ȩl6v^3wx/[od4j%m:Su2ÉGF.>,#Ǖ[ET9 ޵ͼ`P*ثUٜN$ժ7ؗSVDJ9WiC/]ƖҺnTk[ɮl וU=;Pv*%=Avqvi0m/??/JIK^pbq7y3j{OusH-m2 fPPM ( 2q}4]uZ7fj0?eGͺ F2?Ѩ=P=hש(*!=;OyXu2'ʾ]GH;5_8^TJ^a :M'L;6jsnmڦ6|])kbh+vwf`rsVΝg7(bf\ 'KqqNEZq-u p^ b[fI猶^t9'FQ UVO1ˮNdn|wyAR^* !.W~yhn/0~| [όeV[ږto=i ~|}~Yw1?)gm-[ e 7`$ Fn֡se@/#n;iT}.2{ܰ:e p˾ںyHb3IQAP[϶>(-Ξ<[{R?t}ڞ1ku:WJo6]J56\L ycƘK}\I]uP{Ƽ`jXv+8ZXMR'32=H3OŽǢV[=!_36t'ټN7 uO4L|{GK "1ؓ/OO<ݑnu,u/nA?~}™H?Gh^pR&ouP)FgV9 mpkHd8lןYNGKiX0cDcQ*{͙0.,vfɊV:?=g3{ۅr>d 1׷3#.'L=ק1{h=4׶⶞6_q|ϗrv`eW0-ƟjjLC؏!(1BZӣ=8]@+^V{93ZƆv5؇FKoծ=.׼ϮIqQz<R}0f;Ny9jyHi}A]z;j(+/zٕ =Azq1Ty=}\-O' >d]q5w+ǎk=17MiWi-U[ CV@?_NcKOuGLT0j4:^XVV߽QNFoNjsK]`Ѱh *'DQ;^ӡZ ߮ǬW~d;h p"|L)`x}r8#¼ihigP#8ȼ?*ơ53}z/_O&v'Xc=385&fܓ5zT(el$v*K'wwdr M?{91!~n8i4&ғu 0Aݾ/S7&\nvQh=w1qUZeǹviQm?¯,740-ortNvEÂO^+ꍯNr*-3[錏#tojmٖQ?7v`KB%!ږt! 5jna&ߛ*m+O_9i^=oYN؞QʕI!$rw%vП%Gf[og'Ъӟc-|~ Ǫn;|u(x–LX r1VW ٞOfש2KX:5A+BWwu6lmæ?ruu9 8۫Q~|n"˷ywʠۺU60{h JR#nkM_r)p`su\$\YbJގG$-i0́T(pSqhKj⍫^hJ>;ÈPon RH?u$){Y)|qWNKBW`UoY_L `PTJTJ av| =@ oiF3){ 9w:<Cknh]o`NO^~ 1>ҏI!tL eN|;=ۢ3Ys$V)^wBPBKRVY=}xTNXyg{&kqwԐ_Ȋݧ<1jY ' ;&QSP5)>4xC<<3JICK;+wtj V%KίbӼyDZکon-؝Eec Y0o{|{$^Uak,š&hY˃[cTU^K?S7Nl8ElW)UiaLilmg_vQ Y7;<5'a0}ǾoIM8CgJ Ul8Q<ކs+9[{mׇ8]Qϒx`F5y\wj6(EsUo vfZ#ҋwgx7RۙEve6V?kS*TFí㢘&J1z8T(8[cP4xy)>bAu+6]M=G7I]2ꉗ^e !݉6vׇ$*6H`U!mY^B#TdW4L^U#omPv*m'Oj%l(ae/ 4Pio2m:;"Ctk@W_Ǯ I:e q vY`?>BPB!B!B1@ !B!B!7΀B!B!BH`U!B!B!HB1>|sL5;+ +Lcl/ڊSs(?7V,￝yy IDATh&.s b8wOhKj/>٥xLWࣻ2=&bg眸;jX_ù$35/war850_JBH`U!zD쬾+kb7]|+9bg/M#, ᪤^1=u?CTWdEɓYګ R+'Gg'},`r+/v.sKvVݷ9B CBуCaVY}Үg}ZƋsQ1>bgWANx9֌LTSjȮh(y=o{u&zK |\)oFK@ck%p.sTu+P_ !7:8ZIs-%/aф!,hjϖNgɇrgٌx:r*xgg&I^0:/'{X:GGARc"34g{5 |;V/pX"}(oh9lJ/溑D5Mgsiŵ.ɈPo4*%|hhig6]x ϮITy=1^( *[`6Oɒ1z;mձ7N3MB0/t]Q;r7[}bIٞsc-Vyᇣ,L'1ȖڞArAV;V<1F@Uc+ߦ|ٞ1:S?Ty}Ǣ׫jmo ?~Xzߕc=~gGF1< Jz^ߚN~ֲw':VD5#¸"1_W k+yU;jt4n~k -If"g\hnA?gzL& HJAol;#{OQMXx ]#䅫s+O'KN<0T~MmzV >k-5:_xo{oU+O#Džv,~m,&ZQK-YcW#B{<6(׾M'F䉣ڞ 3' dr {yene5ZN4:cmGi*P/g^Y8onZƘp_nCBANjjO#!ŵSU?ۅg %׵Dz+]ϥ\3B\F`r^ےNS[?lU7Ea51`Og*ZX8OvùvaZLG.%B !~vw9s@fn|o젶tQ̕#x{g)8۫wj,//òO`frZ5<2+{tK7d"K]swMd\8*gd;H-aᛛY,~N0[BP)<0VxQj*3]-o^}j-.<<+0oKɷ=?ICK;L<<3yGY%^H!RHH:(XQlXOO='!Ꝟz H @ ,d݄|y˼<̼̌A~дzZ;YuDV8b S{Rk*9ryόJ_CP76D*<2=׷fPTJ3+f|'Zoکbf///-0zeRYH n}ODpӪ5Z"}\t1ym\^NJMi)Z΃S#Y<6&6ų|SGjUȸra 9htCoҩbF\?fݝ{HӢxzn,O|}H^=;X) ;/.Hkri׵̫7?#1ē~8JI}+~.v}n,6|7ǐp؏Ra~l ^?[?i8WşfW/=ꝶoޒ^rF`^i|)Xy>w8jH9#>{a|5<m]*]yfn+6\PÞS̍ <=6 tbTiFҥՏ{ORPۂhT X~u,_c-#[mǏGhL ,+Ŏnˊi IaP^q} l`-Ѽ$Mx;4ut$7Z%jnؕSS&6 Gq{nUyxVұ`\ﯯhl2k,QK9꫼>;pu57&&`-Ѽtұl2]xT*۱U1,M _OdvoR8 n<>PO^C{~9aZ,ic,IWNv 9O'!ȝLVkZ҇Y2ܐ0fXWb)@pѹm\(;YZRacz1[O$1p&VKs¼XnhYY)YQ[vT'Oh!VFemrvHJ?Oi%u^aM \\3U[:OǰW}+Ӊ4Y;XZ.⃌_0R ` ]tF?x9B_ߖBNٚ\D(R%&},Z˺j ]* o$+iy[OrVK]k'fF/oz6B, I듓7 G(]SƳRY0H k[P.J5jRIi\ŔLSQihCլM1nʶ2#{cZGδv*y}k-I-嵭<0%u,B힅qAVgT/o[R?C8Y@Am Ů9GJm-}+Qkuxc Ƈx"ru{b([NX!]* *Yl*7j{_ܞH.A (ΣĐ+ 6}KUY;jhlL&J7ZRfbv0:2JK-0g,QK9əVt4?ZNU+wv_VۜNX:;CkwvfRѨLnRah~q~ْ^zZU^2\fx;džޒ{3woumlvw&‚C}9XѬk2ݙTnczqEL7~}DĪ@ D@iTv>Uk6{ꯋvHa >ζ<9;WZ6V2B=H/0{?;nL= B&Z>㧫zEյ\ É^J5GjL7̎#E5T7:tMJ<(E4V76/&ZoB=UM㌎K$T{S5T;h[ɭnKu:`狔K C)k%h1_ {OW{:ÊZnn7:֩RSҁ-zgi=U5V&^H<(mXZ;ɯnf)vQju{j0aND-F8X656m/U1߇{EGί㱒I9UD}k'c=8_LU#ӽ}i[P?Y;r{b(NWQP<{NmRޅ-mDxE1AgoM$_6_}E#ýktَaG} 6}KP5Fmw?v\FKP~uu`s`½ N 7CXc@lpgsԓʫm2WV\{aX;n̉D k̵͜PGkMuveߛk5]Np)Kln޿c.pA5ߙ?53 3@ \ $zO#w_sV+^Ğit ߭y{g&+wgc=0L߶.U/Z-R$jIdy;!y#J8 m]l<^̺Z̩`H4E A;Z`H籯rI{R k`o.g.ZytA娿SF^Fc T >W}ܝE0/5qly9N?}{%ƿݦT.NMi|Z5YjlY~z&0J}'n-tݟ7w=UL?\~;ev% PA5Sý9V\GC{Wk[R?5> Ņ ZؕSƎXOOPHTY; i^6nT=ء#}GVc}MTb"=eX^ 5T5w|saçeę|9;h h9YlM=Xɤmp{Lb5[Vh[ݏa& ~2d[fsߪO?M=?7'ytT wk$`r*Y Nk4;e^nh8>ϑUMlgtڊQ}QV?sݻOXFnwG Xl<;mT.+SΣӣtvsɯi?;.g^FeWNO}µʺ m{ZC^M3cv7HG/怫!v B]ӫT6bxϥ㌲zy}4AvEi{aXk=zNam!{WN9üveQP#~xO6{?K#t0 FDYSLL+2ܧ_\6n2J~~O}Kƾ\ 6ץ@ZebXcOܗTGqSZfr/諼yi[tb0c֐ShXq3hr qwU׮ք{9yTk(m1]NffbX\ƃJfc]jxSavSJݫr8@p9 š\E 6++y1̊C )F\#YѨZ駒3D4TB1!<<5W4`mEBn-{k9Ǒ6Ix6&y#HrN%8+FBN3\@}[͎#oaERMZ7̫ne^)ita..`7{ky:ac%NARsXp<@ܼx& b-(R mhc5 yݕXgx{g&ǑfT;RG+Bxgg&ABN<63Nڠ{%_ qXZp=W8^V cJ$Xɸgpܺ%uU78Ëk#+ohù̌5#pCX[x|f4ֺ :n<9+"dU-ӋR9;-L 1.5-*TC$xpx8b[3lkmJ>n,I JL*a|'o^;/aIffE"HK%<:Ͱp_S-k i?eJáj1 gS{>S^ u@^^4f@ mӉkn苼lyqa_4whQ]ܸ~76+'fE3 Kp%9axDi=#]qӍGhn#E̊-`CvpV .Gᩑ,I C"e <~.\?Q4=gƢjٟ[ş=lXTU0N#P5'|~[e#K4w(YKۻ2ձ,I cgN9mt$QlKnu38͜~}:Z;s09G|/f5ߦ^&JV[7'hkRdyo]ӡT' ch'%|w.2;/'[:l*3x.YsӨ`4l>)5ְީvxzfH$vXvM6w&7!:V\˳?tR8ˮSɡjGkyǣ,M ywؗ[_%z-'NQ p ِVt^7WhRu<2=©mdKf) rGboޜM|gXFrav8RXKx~H>6roݜ e ms1R{mX!Kxꧾ#?)\JJlݦ1 BSɽeiR8i*5)59-^{9r1x_3]?]S$4V?}@?X7%jn^9'gEiԷur0-="9 XL>Y: e,W~IiQ]:ZvC$G:,o_[(-s:1б^jQ-ksYuD;4u(ywW&̍P^>ؓ͢,_8 g[Eu->p}ӡTm5,ߔIQr* ;4Masc؁<+h!)XuD~>QG nOc ՠO/cxH"h_ό ؖUƚy/ɤW7^@`Ne[[.(UOd^K2ܛkbd ,W|x$n~GQο5{$1?9pE~@ %R@pxxj$' 5LRȥ>.[+ٝz{팕LDOi{+g;쭱Iy|;Ztj祅v6Lqw䡩Y@pq@ ~"6%?㭛qUХ֐^RǓ&-o czhT&9 &M\ӌʯùZ BI}+2xxj$~.Ȥj[;*9{?@ K@ @ K@ @ #"@ @ X@ EA.03—`K-@ p F*ީ@/ $+y1\$ۅO7KsߤAq%;'yq{l`#[z7&~ oM 楅.W^LV6 7޼)y1Z4\z aW`p!w<7/]?nZ3'ڟuO系0-܇& 㙹q@ _j෎Fel*/(IRji0`ícCxk>9:ӌ=%8)o iv. E@p IDAT1C=pw"JhR\Z.hFrَҋ .bٕ}|Y1'yb]HI}+J/8 a ,N!ܵzXeֶ`o-\DX/@ )"fhlۋB~c8r -5tt_j.9B>ζuYvRd?;"])\Ų+}=_RܟÁ*>>89ve?Z'+I\+r&ٕIQ]o{b3w 57C_}d7$ %Gk+V4u8ǩDڥ"->#(o=\N=]?nt|Pnʣk%qSONj{gNm<:-W 5-|}$eF Jüٗ[ɍ}5y9Н_?5y)1K kY6jDY=۲,~F|wF m^G NN '՞.X; Fkt>́.{OW:jۖa:Uͣ @}['8Ŏl KIs?+׍aӉbvT>BIÙL"!wve8^%7lxp&=+!?]~H+귿.@wr)wONR7rZ`_i%u,m=+7q`bp;җܐg_ѽOy|7s=g8 mϪ&l'J L-cBw拾 ml(L`_n%yLe̊~fõ#|\XN3Jvdjo#P.dª&0?Q5r)+~N~ r]E|v(Va,Nc{d7̎c8ۻs\>qP/my&u\J+f|ךV p eೃYw͍੫F ?=pu篊c=09Lmյ̊F3>-ôv׍wp=}u,L`mԷu@S:@so'[uX}alH+5Q5ȫpG; cYs)5eRnHk7/jYp4bb S{hzuqAׇhR3sXsɅ:33́#H8We^l2]xvc*ZU1,ΛMMqu?yqg{iPh0oƜUJ*ye`y49pnxֿ9*(V2L3T›7'9׶G26؃o_7;5ߍ]*xuq:Ujb]Y`wc W^rkg௳c Gb`mx)xTW&#yꪑ2geL;Mi-N!pI.<,0W'ODp;PkDzdy}”ތB.兟>Ԙ{%v;[2KkXGSqC=؛[ uCrN|:gFoN3Y\^`0sigζ HNU@pW mcCّ] RiRiX*gίO+D֠Tkxe,fHJYٕVء%%-'K)kaJjِVdxB,嶱*%feMl<^̨@wt Zp.J5jRI>.O\XV?jMru$x`o-Vˎr )Z-N]Sjym[Lі`VfOAm 7qKkwveH+U|Ǩ@;s* rT7whr^Rr>ؗ4ZEyKam _GѢW6%GߴaSF1OUЩҽxԑQV}$e/FK[7~=OR ~[fQ1hPS-yVz6X28RTFS&ZN/N<ؔQZɔY`4'+^ڜnpZ\sv'OU0nV2PJF on?H_u n(kCζ ^afI}+pp2֑}x[rN:K:: FgSe`m%cb!+/N].:3yŲL F.-iP2pTz꘥9Ƈxdck[3hzXן<<5Zsm'Ѿ{'%lN,2|\t5NUKEul0qDH%ϾK:3e?jp!.@7zhJ$RznS$ve# ~5A"@?zc4w1.؃NN!#}:-l@9v63җ_ʮCD N~]0cf̯މgwU5T;[ɭiKu蘩A*{J$̉gL; Y=gJo';PG{:1#踏<ٟ[Enu3OΊaSF1YFzMq(נpA56+PGlrs8k$@#RlhQK$P.udtuʳY\KBw:jJ3ԝUL b_n%y|l+ǓeR<#<8C]k'y5̈́z8-1r9r0}q1Dn1XTBvpAM/ϫiY_#DžCսlJa ? \j_͵XF"dzrrAM:`)]* 3JhEA|v6 G9=~vS\? W]J$ tg{r+9Q@i#ꗅٔQء3n_Yp4.v r*Y;dZ\~`0K;*ʏgB8V%܋%/>*uJ14i'1-܇l؛c -sIEq}z&0O'qmO;Lnuj?;{x5n)t@^ʼO NjYZ`dv刅erV1RhRi͍u6SGϗ|??(Y$ bAl ̍TU#_PnM8,y%v'[N2+ҏǓ僽9hZ$i;ˉs- m?|׸ |gn42s2q::fie }M,[-5V2y y |?!քy:_a\m:?u\ٞIu3}xRi7nrKmFB.W-l; rs@Pp]@ )'J?;Y^%f'{6o:s cq4׍$ $u~>YJUKἧmt-w2]*+5T6茨>N 0ߙߘֶhȯm!v֐SՈZ=[V܎~í(_2(-\W^t%ˆqHNsuR?,1KQVO0/kƺct)̀u4t ,x9=NUK;.v l>uJPPۢ`uԉ-NU}g+Re0(]0ozV7'<lȭٮ-̎c|'Р@3CF] qw$yPz1J^`}13}'tpIa^:I1Cp6:VۆgIufstdE{')mK1ԉ%md&eN=ʷFy 6ҋAA;SY}Z3}2]]3 ;k9 1D++};'fFhCsu">b߶g 9` Fˁ**_ dz+pl,5-,n/O~w|x{g&ÖfT;RG+5ٕ3Fc3Tٗ[i6p6^6 Wr&xH<9,nXfG򶤿*ol#> G+\Q{ȫSɟg`mDiϟgF6z,QKD@pCP3LB~(olc0oI|}ߝ'hRjGK呫xxJy[J&mj^wvi}?2=\~` uGNEU >KNc{'G ~ Dd\n15<VUQ,BFVE#oX[ù<1#/J}k'u7e_ٝSI%4֎4Z\luS"6V(N67oR󱱒֍;P?>fw ih/br]2ÔFsZ\˚\V:N%MJݕ3sN=oW<25&SɖR6յura uu@ 4w(p_y5iI~,f6 s2 K򏟎rǸ0޼1'[+*29:sdKZ^鿼%'gFiԷur0-e3G]['~?NhRq6s*3SK:LbPI-' $ְ|sK'lGuKOSHD"ڥb8<mP571`NمؒY۷_Ҍo*c* ,}Ɋ6(ᩫF@c{?7t+ٍ,βqt*9_W)y-tb8^O{}y Rml(7'k+NƣD8ߦ`mnLʩ&^roW)5ְvxzr{oS{0޿M1Pq}+/n>FZIu@m,es:0"ǦGss6BL~U,Kƅ3#\پ(ke'Y2.δuؙS#re(y:W̻{2RixkI6{$Nxi=- mgȮh:@0P$ Y-K 07㴉@p91=_g;>GR v 9?>83* |$]nX3ۅo9~K@pe#"V@ ue}ZC  T?O)]bgDdp~3̎Y1<13v4-.vH% k~NUa ෉p @p^ +F˧Nd\(LJFk9upRt%d1y7>vX˥Զ35g-&b)@ @ @  ߆S @ @ ~Ǫ@ @ @  9K-@ @ @ \YU@ HǦ`3Yx"AZ cGpcBpH%=qC=.\cƟt-ˉߊ.n~x66sJ#ۅO$ͽӹob]iu.@  @ ~oA~TB{сCzѥo@y8 )5Z:Ujڕjn`EiiZV&qr3a8em.%INnJNUuN/ĺ_;?C#ɮl^ץPqId8] wn[;O4/6|gN+=@  X-iuu \r,&潻s t)}*FJAGCIz頒5ɕXWZ@/ZJ1y]ES;MY˓b_Yy9V}l9WhhuJl@ B8VEFӢwE*PGؚ vsɑ;TkH)a,Z;u)ü;JnLf YhY7Y!<;';?ۃ&E\*%wveRTj%Ie}2&Eޥb^OÝpBrwwgQ \ -O/4Ds8/@72)=iPRj86:ȝ%7N֏w9`L ͑O6%Im}2.>6!(Rr*xk -P/5-*6(|4ZywLa޸;ڐ_̛;Nrp;&cn2'Qi kс,*^5âUǏOѫ|c2t#䎽BɊ+6rnʤP/m+լO+#yg ..1YSRWGJuKߤ3QjL1e7WEG-3sZR '){>ؗCZiQ>.<<90z{NdGN9kRΖ4%wŮS<*@W:)v</IPRG, IDAT$aN5Z6(ᓃ z#Sp 4b\iT0v|stXwVdR G0;{N>9j;.?>CUȨiཽ*6jk%ILD*]ț;O3x8HF A" +vb|x\|t{j황`I ‘AO/}aN<:5G9xNWȥ|}tj?ez{p/F1,YuX-BhRSF1\j@ U@pyxj$*>܁Z%OG;^[4[H)A.rCP^[4op`B&(olَ'сCd;V\*|~=ZeP^aolg-)QڗEVEi.<}U,KKAm 6V2 Uø0=yԷb߯lc* F+[ :v I^lԅ[JIa(dRm4dž9ܮ1|/lyq^w~va*3}Y<:6gI;C&5G0?&W֏wҡwu?=IMkM]hf4Z-7~築g^ -kuWWDzxo\<;?M}[An4+yx;x;R,N6R\ߊ 6 W~8s[߹Ttԡ4+9]赖+ Pkҡ$ߍesXK:GjpіvsmgX6hXWdR ~LX?W^Z0VNU5a 6r[N[݄-/MSG9pV,ꃧؘQT"a0oTj _c^%\74JessH$ȫ"ύ]*xuq:Ujb]Y>w}${KK7^&Lg~mZ7:B9Y`2Rs'+(mCfMJ*5N6V<*g~ڪgLeQPRfʩ*ژӆϊcKq2+tkZ٦tbӘዽw/sPQk|VHm['Q>.&OOzR k\^rD,RAְ>6& |u$fŌ t}uA<CquK| {zDϔE9;rZs?>'+^5%Z}kO`[v3_JCm73—Yg6r/nM+㧌b&氵rohˊvߟT}[fompliZvsoǭ-ؒfЁ=D1OW2zF`BV2r;Z\gO=̻ml(;rY^hύNjٚUƒaKsK`qI|$7['jģͧ׈^6' dS 2Ia^9k5;|(>}B@ڟgFɼs1/ّ~A:׎ΐּ*%ڢp V2)2U'6S;L@*)=y<= FjDjg兣y։x?Zp 2:(@R/K`3A^gnqJ$$E Wwxo_5X?W+fhIS#SXx"Np a2ަ@ Xɹ;2Ӊ_y4? 5JȹpǸ0T9Q.C&ÉHo޺1+h9YЧY9rPVQPku-ԶOk$aNX\NJxjHlhP23җ_NRΟgF¼x!>!(d:B&ZnpG+>ζ<93 6V2BݝOe(k;j{OO%yvg~w'R9#䎫1`C~<ȫi5Ch2T@k {}H)FKْYss ѩSy7I%̌9x;"HNW7.o18s,A"P\׊-*rkyrf 2ɪl47E#Vrs8nD r68Ӄ{og\XQ.>tU;1lyX:z{esxgw&eƍI|qX? CѲ_QkF/xiQcm%O"͑%7x9rg{IIy_6ar/c|pǧQ58/I^)x1<>?Nuܔl1CxtJZYM kK's/sbgbq-}7{kmLE*}3XwvۛScYH?WGWDz!2I9#~ ) }C 6$2w-<@ Ǫ@ ܓER Fqj<Vخ䛣~)Z:{Oe>\PcFH^M3 cxjC 7]ٞ]k ֞+U:#jZ:>E ZuU{AII$}VSL;<*{j&^ j zwEŶU՟H1@ T{od2LLX=rι{|ϝϺ#$yr# 9x:Q܅ZN.Wڶ<؞oQӣ{ՎV)۔uG''ֶt;ku`4sIR('O,Kϋ̮Ȥ: 8[x6-rGKe#BFu{7Z~)+Sy'ֳU<ŖcV3Ƣ/u_fYlȾ&YHUBzlUe4tٖc˓9O^51n1ʏs!ۇKe/1ȓ׷Y]40:`rۊ0htzJmoeWXu˫jScs Wث!9RO3۪Ӧ`4Ջm=}RPì:/;dPe]Xw4{)^czɭi%1Ѓ# p&T vEu}|uOA*[bw<79\ʔ//aӹ5̉E!;^m{7YoOv%I7vᄇyId?w%Σ. 62ЈF:a0mC^5bXzs@7ӒM5`16˒hVFuֶt\ '5gmG,8[^Aer?';%]x|4Z?̾9Zƌ0!q!^5wѧ3_%ܹ=}TudP_ZCeܶ:Kʠ盂{F@>(H^INR'_*2i`-jvm93̇ڑg6a_k">drܲz'7cԠ*Tˆ_8)ʡ>3BWm ktֳ% pSߘ6-'\7IAIVJ1f9$haCC;lm  dh!^%^u^)]t&`)AJ!C&˗ͰT":̲=N[ھbg$;(O`DT HL3t th|~?>79 _*ork;I^J$Pԁ+D7SC*oivOJ K%m91̉C&`bjwǂ )+ERF?##7x[_ݤ IspmK ἄ`r) '$6O t>үI\@Wyᢩ$y`_O?=x8^cø*mKE 8H$N=(׶YTXL&}}^fGҭmkͦ:):(Ab YQ݋|]P)d@N{ y)Lq x8ĹɤEHs'cJ%eA;KVQ#ǘυ!y^B0choziiIs'&[{8ZqR*L|.BLCA-gsQ-6V'#JPȥx 9m x9մ0?$jOmA7|zw{ydI"Jrn~_XvogqnշϿ[%^%^%^%^'A8$˓vR`4ҫճjNGק#J(m-Gm*KeGI=C=hSr(^t&. ;zt_ -r[ !ܒR&E3oMN*IdGZ÷9.h_y*X}<}:~?ǚL7԰<.˥YZCkwݖe}:[{MB.٫ecDٮf͉ dWXry8qEr8;[kkꋃ<}n "j:]cus<ڹ , bH2۹LKO|z{:sX= h⍝5:=}:R=g$oN4:=˓?„xh'hmޖGmGM<.KvRQͭ)\_ܴɢUL*aŔ⨔إBv5Zwqι[ĆMgW6Y \&̛ٛđV6F On1P'z4|o1ߟ?fLY@o0rlϧ[ý'QKy=QʤL WMn/Ce|v|L 9.kŌ\˾ <,7%o^5; 9ʲOzWM ͑n .bsQ[j7̜-kvZQ.ٍYz'-ݚ!mH  ruQˏG*m941/']=\+F_0X9ggseJ8 t'0LFku7 l+0V?  JVA8]x7fp[bEӹ]Cc]c=0JǦ,g >?PjIDQAAV" ݵӣ M%t23rcy\Z25K,=Tw+ h)gc~5L8.*W`Umjn]5#ϖqgf7o̶*k !u"LdSA [Cn{l۱:fyn*_7^rE1R=9RFNoS_b=!ڎ/?P\UJ<8ZFYKZ=XOy5I% 9hvu ?dnpތe\jU%a|y IDAT]61oM͹l`4ҥRF.\T _u{7xKpWئM5,Go0ݧGH A.5=yil.gks+٘_iQ6OG;֙gF6Rh Hm 8lh|Gle?%1; wsԞy-h:{x?h`ংN<~E_` jjL 31Ye|v+S-+R>Śf{;]8VZzqQBճP/F c   ČUANH/g&z0:j,m`4χx9ִmNrv5 ٖYM3't^. ~#ϫ4{w  DBd [#{z[Efi#|YROVY#͟BNVǢˌ<[}ȌN"ɯk'әkE1aHX_KqS%ӯ@Ìl9o?b pQ)J.vYB&%ә=@aQQ.tѬJ;¾&;(Ӈ= ?v5psj=6\cM<0T_nI=Rw C>ˈt!c٪}p>CPhΧ9q;tjKo͝90b a wTx;,K7U$&pv5XD%@+˧=rX濙\Vn^/g.,2[r;-l9o\49a>l*v?@YsCg Kf%3AAIe8Z68˫klK5:.2 ;{;=#_Ƽϭ-h^J!6dߑl1{"usDm4O^hmGuid[ʜH_KJȤ{Ym{s45n1ELwQʼn|u   g p}v`OVNB%!JH K/C}`Aw̦tj@)=oI.~,hю >%nJzz]ȳOa v<<4s?wE1\by$y7z] <$_ vRqYRef} Ьزd́@W9/oicr&^}z4:WMr'v <Pʥ6dvt37D͟4uc˒TP3{"1*o~<A18;w An3NCexNOcDq6)1|زX}䊔pK>>m{0'TBΊ)1'1 d7r߼I1g:{<>ʘNm{%m2[mc}^fEӢf95-4wز$K0>ә7mWnFIAdh2Fm6FXw{a'qt@PtAAL3 'Wg՚1'KG&Pɫ[sAZ~>KϠd7f4&S"qPȯo3iрdFu/O [An]WM`4UuN9m=}<0_{:zd֐Yhp9'fN@ݧcE3mɌvPPkZoSa cohlHL3Uœ qwW$^ L(eR4:[k&ܺlb_EoOǣksc캌d@]-=G'rz[ky1#?yevOKsG&ݐͪY:˺U6b5ΚԽ<}itk6W7I]-|($Y}I[o:7{%^:gѺ6K;x=ܖ &Ȯn_ێ9&{aSw͍ӫsiV籠/xddLc lR>'oW-;eذ#1aK&u/u=h ;Jyxdqت~>uӢxҙwSdԺܝǟ&b4,mob>c]^# q>/lnM(QAA6מl 噟Q^0O"eH sE\*ŵtq߼xz n#^?VEoob!G'b'p̉\k(mNM3'2#/';*Z|]κ7wpEr8N* yqa:+hW:4}|S=_!9hUšR6郛S~z*,V-'8}s"vRGAC;eQ?/ΌPo>w7wsHnΒzJ;Y01w{%ym(%McMscլh}\h\Tb, TvF\inwᇝaВUœZ !-0^8;@NqBLj`C$M(͹&/JwdR^ .|y.YGb-XAJtr7Ϝʏ2;fs9FS@Nͩݦˤ4s"s"pTʩl]dWX[줮DzM!qr㐶a^VWzn6wB,GqUJAnvkx/h:'.KCrRѬmN9VXry2Ye˫Wn~.978?7}:7z};oMx:ؑ叏N^r*h}I\ķ:W辰wh];X65.@6uy|i2nJ޼r6zwv=-H/[ l?ecUkw{-->F҂#Gr UdX3]AdpbO,KFtj($RR}CeOy*Ϟ79X;W~4R ͝tiLv!әícvR1#۲`$ƂjGJ!cFX}\`Ovr"@W:z4w#';,uuU)yxaFs{ƕQe(dRbhѰ.+¹knZv2)X_%} eIitikQEZ/o*?ώev/D”`/yth+=ZvY*RހFg)`H!q1:ev9m jFŖCl͞( ^F{3;:X0|a+du nB| Rdc}̈#)joarT)F=yq8H2 0mSku#&pFnw+L- p4jڻti$z$ߔʦ!o=ϟ{X{DBz:RN|}zq?-HU@WYwv\,w{%o\9x, O&&E n"!i\zuz`>U@Z%+"Pʥ:(i ÉNFuu,)õm>AeIofompm+w7/w:ׇx4*D7X_W9v,IA)8'}s"p& 6,1rjZl[A8b|X}vS٪`}A5u=4t pq,n}i*OI5lϟ7_0?̜0vۚnD76SJ$Lv%tjLy"wsd%k+e>w ʐqu&ط67L@kǶrlݯˑ#ä3坝isW+l\n1?.L͹FVy^`_eN(e'뢺ODZN@;a)nUUvYByKא<-J;rBuqc0Gi|^څOUᄋtZjW(:9l)v_Vy{* pO9e\299ql.W?cIo0r^UA8 N*nO%um-w2 pwQoRsx6;)Rzs#YZIl;V%6;@2)ǚ:x}G_,GS[O]?7{%SX^ܜkզQsO-IOOlև pbrjZˡ9V:f$ <Ƈ09(/Q n<8'6`/0 mkvM k yo'`#@91 xc>\j wWMj cIp'1ЃƲ|z yy2gs#kqyR8 'Cj7LsGy6(__ThgpT#hfƤ<}t:`'J?]{/Aeh2ǑYBbJs AknW@H׀hD&**y׻Yi;wRe/Oo0yn+g02nQλb=hSȖ:ңYw O^zB=vv,DZ 'rF{q BgϝXlEL3VA8\UJ޼b695-\p -y?TnaŇ[[W^i)A)qU7ߝyz!V(A.%3)md[ )祋fN`gi=n;g b!3.l=% J;-fL@* JnZ =/\*{b?shtz]m Z,) O:5Z2Km|Y/vn:oF$DVdz4viu6{e=SΗ"H,7k7>2 /37 dd߃N/eoD{L ;sz#$< 2 F#w}kF1'җ WGԽn}Fv6`GaN4{:w ݿÝsbJkOkT`Be֨B*̋#;2GElcH&zҭձeA[ ћr% $"_V=*:Kai{?.LqӰ6s&itB4{4W.<.䂴LcںhA)qX$3e'zryR8VPp?qPʩlŬCdWO2 }Y* qMLW/6^fhSDhGKosʭ 9$`Geܽ[m_drgZ,7̘Of͓ۘ˒Uҧ jXduiVkh2t$䔣7ץx:Qͳ_yhvȢDVNbKq0p(!5̇q{cIl'I{erHߗ" O-yrsj4Jɚ VG4:GovHit2)A'͑65RnZ{*y=gZ9~)˜O6׍.KE!r׻9P|9.NjzzP*l,IE]rOL4A\FIs'AnvVfS QRϛ̍;.׎:l LdzՎy6T |5 : M2R"xǕ^ݣA=A8#TܑK;ym(M!K3J_۞G9xR`uӢX0!>_–ZnsRp;܅B>l=MDj+>byǼ(n9o AAA$UXA8;.{JAAA~g  普ĒAAAA9VANTU<̕^X/rO   o*p F~.}/ۿ$LWIAJ$n3BOq#x)#9S}0A|u>&Qg:'Ud-9%eEx:M qU)OIygsN!={yb8OI<3}6] %"*  'hjć F#oflrYb:)Ϊф;ѭձw lvn[qeR8!Nhx/ŵ<< O2)Shiv==-p'*x#39ɖ?W (nUs,nK zuyUqVr'N$ LJacn;Be~_:74c"ܱWhR֮J$|v|L bhv7!}ٱLwGkywyy9\fosʫ#");~eGWOWcG>{TXݘ}ȋ2ʘ͵S#ܛ>|Iɭm&o'ώ%0Σum->aNܙG}5V]UJ\7omiLEr(<)/{ qA|N*FUk;,m$œC`RnvvE O0c"An4v!ܛ>n)uKw%ɸ+y F**>~\AmI%H`IL J9]Sa=^nCVc׉܏2^ƺA"*  $z$ߔf6+)|@X̳9XՌB-bӸݖt$[wn [̳Xi3l=~~Z3C}xT tuEx;x'5K,-C>:Ή fvo']xlǨV$GM0K}g?-؋G%ZA)hR[94i0mk[ "nb']V{Ч7Qe.:5Zs\= &3*C>+6|{8kԼGaoͪN8^D=V7aX0[,|t@<[dȞt|[*z{7fj il)d]v)cl<^cFN: 7O 9kTsOn8uS#N5Y%\϶kϱry^5mĠIώm2m2h)6hTJ^5|Z&#U㬍6LޔҸ`\ wyeW.U-==; F?XDZq#?O#<맿92[͛lNm]^ܚM{2B ap?~m&W4zc&̚4dZ59NNUOao9|.Az !BF$ǝ} [ -%>Ե a(kj#yc}ܙI6ۃ]k&h÷d<=@g tHB\Ѫ{_hUJܴ.']lm198O\z>Ev;/?pUM_JڨoEq}+c-4vt13܏hTJMt(+aCW#h=RyiTy8ujѾ۴OMRIMkH/םAڡI/#{t*-:e=;k5lQQ>(A_0-T7YdV4y>BT!B2Eoc~`plAPٮV'+|N³ {?20c9h1ۆ݇Z_˻ryuw; }kOl_-MUo3 >=4Ҥ﵆ O'{6§Y%\75e, HEMn4*]ii1. }ukk|fOZ9Pn.OtMtLb~x^]#Ga} ޹{rkX{yQ)==ٛg{nwSO?c_w,xSN/BqB!E`2ST0?+Ya~ueq=l?B<]ؔ_amTuժqd`Iב @JkXUt6ؓ&9M 0q_u-Dxd}(w%*y)[# W4M @y͘zwP Zq]ʿdmt l@}`JxQJ>-{ʉbR-.B!E]>R3Ә @ O/OcMVɐshX9'V*mIne<0'G2sSNkíueBX1ápFSF/徹YJYf(?hx LǵQ/ݙ8v8nd<`_Bm~)E6`z۞( oTL/W ɿlCrbO~mD: /;1BEXB!#yCKSh6ֺSìǓKi0Gqdܾn# -x̏[Q׷ҎeZ_g P+vpFؓ#=?lE|tb;SR7+lIf1O]ʺ/MlsL{?=-r F/v$o?A].OՉ.eyva}~ۧ,g7gS&OۏY%7N[u(єĉ>;0dV4𗝃C'p!0.-zKS纩" O6_m-Q|v Xwץz<'gbJxGmDB;U{_s?]x޺wH?VJ{q!.C} p;*v(jb~򯭨 : F;\HYcxG`uԅP;_աuԵuYѢ7Iiz몣V-gOi-f|FC:qR(mhP]W9B g!ݜ79%⻷F[;BxGnA+pc4"hT̉?0V~0S|Yl'ңwQʟwPPb=E懳&1/2BAa}+d)<%X99#{?e]TY.:MgO7o^}k3)ybXU\Go0 GpGZ >Nu2$dcXP*\?.]tzz9zSt׷|rkF 5ϭz:pRʔ(. U#C9xV*)o]5:hUJҪ7p;]j~$ i5+96?7Ho7 f3xuwXRѓgtZrxމ)jI2{a9ypA"/̡@wg~|:-n>*;댆.>*3=R}x~m&O9ͻ HK۞Qq=2JEr7[>휨mxh? hJxʖ˯=;ZʵVM W_27ulmlw oMfwA,v+[Ć6{H_dS;!.8\Dqm*6U+y~knؘ_Ays;F5Y%LfMf^ޕKr7:Cj5j^ݓGa}5m8Sgt̼p^bl<0/ey <%,3ϫmIb^2Ө,W |[HM?}EeKQ~{ry6LDSwsnKKMv)#{# ͳ+6gؙ..ٿ.Ɍdf}N9+=-=x9k61ǿ:Hy=ֶyfjռ89M-DӢ47bHU!ĸX>y" losb8tz{-F:YVw3 CHo=TNI3eW5 VF g'z,om-_Т7Nlay6 *٧ӨpEIҩUDxUOBZ'}ڻַHz_敓Wl] &gzDQWͲ9^2T( >msf"Gk$ۻ4ssh_w&y$6 bk(9'iٗR`ŻUG^uT1>6 5Ҥ y5Dzs[j4Jj(ihuxґo0u-o˛کmhMO3C,6d09oqv{[ζcKR3yLo2^P2`YSvgup Z3zk/Gޛ˳-#}ua󀽺Mf6䪄0^{­?hNH1R]7uv㡳{ ǝVis.fJɲ~A6% na~8+H{zP:`đhⰃ41 ^.<B%iXBwJDV L  =]iy=f1Ho4f3Z[3ޮK gUG2XuLtLCRy?(od_c=?7P/W|N5P,,$ǝG%4)oj yn;ϡk5E-wZ9@oYVmsKj( Y~( wRFܳ 6f wV0߃l?VT*x'OQ>0;̟MvJ`z_NNSej70h6m4{ JiQ*<0( ܝ4ywe:*V^M*䡅";(mj{g.ɷ]o/Kʼn6=)gğw31[,}qjJΎCRe4g񐋉=u7MdWqu0qnojVO\B[e7jfO.FȮj]̆eoiV nZ vu-<(Z>l_{Ѿ7G2ՑLWhx9P^$r~sN9^嗗$s{j [*ya[g]ߑzxwMei;SӮXU#|h{纤V/OSpRnOVows]<0'[?7bP. wڌQ3&La}+o(`za4ͿZ5qu1i'EXO ݕ6ޔ+e2%ʡ !BxG@!BETp+uqp9G%͵oodj_CX~r˩u-i1mgqf1fŝX !B4 !BJ -JC/r8|ӢX|!}yʖ\4jNOdD_: F>.bk*/ߺmssV03ԏ̟BDA]5,J[<)^4tta4[Ȫlʼn^3[ߊP*|z%݃YP?{[f zݝ%8UhJ9XJVN)tuSI{zΓNER@zT "Lyx?7 ;30[,~s5M<9pLgjVj.|{6B!XU!BqQybI %Yf>8\ryoe:w|Ldp;b2[p~MF: ϱۜIGfa^l$ׇȪliѤ">/g- goY@CG%pT= ^ԵXU[ eMmLiSӦ/7pժyl)$bOӢ3 ύgoy o( !WjFM68k>m_|O7o^`pS4uvsPSuB!Ĺ" B!UOMtC`}x%#So.<ϏuR&{T(pEGьȪl`gI5WN `' 0[,w9\QorʹbDgAD NT 7J5h*V=suB:Ed/6*&߳g'{cM4tt`4[VT +g~dO|uh4Ax9kyCoCB!xU!BqQ6omfs 2!׃ç:`x1-<,f3f8=hķO޲Z~2o 5C}yavow ŀ4onN.F.Y>y"'۩l$&ySrJ5G*zl~o8A`sΩl䎴nJ"1pMmmu-bQ+E+kK)nhez/fű *6N6SXZɓ5Y0TrՔ0~: O`2UH}{,J)(հ6.fG%,n"%hTJvT[m1ܔI;ztXKPpXzt@ޛSXu\uI07ŸX_Ovpmb8wϘP_pS*m.4Yύֿ*. ⥫gsg7UcK[ؗ{9kuZ4/]=Yk مRB!CVb.ed'ai18mܓ|U3$p5GKxLeլ8lkX)Dc2[ؐwe [ps&=*%LnRt^Z^~&7cшvDza09P^ǫ{lzpILt]僆uPO UG}GKwK'JmфyU)ɯkᥝj2ؐw`n ۳I daT Jqj$ۋyxF}P)LI!jԶw|VqXFUS?si<ؗē8U5JF.m;͛od: Fcw3 IDATھv:y9#k悞ʱ3&0gv={e( ;pPw͘IJ*%[ +y}q6W[SNKĺc6?h>.NG念?̡Oo]mУ}i|EUcB!B|Hê7#ԗe&RסCv{VTbLA[ʔ(xvASgOrѪ)'{6cFrۊxnQ&Y}yw}vii]>y" ';i2cadFU n W̤æ1>?Vk30\5%箘-oY͎*isawB!3YJqQK+g,.[E3:'hwpЪqEf}7dil0Z~(Ga} UcqE9|N:pbTsWe<q[j o<:,lE4tfOL`IL b}_VG;l{%Ze;i.:(o%׃'`t tᆤHB\Ѫz~Ԫ9jV*:8:M+kpf8#<-grvQ*L boY/G¦{)NO`2[ȩnr(lRI +SpR+ۓrPUj858?OY\}bGyzNhzUpf^ Әcjc,%ԴY9KSFHɞ.o">{gA{^ ~5sᮈeZqˮlA'}z#[Vˢ@jpB!b%S!.jeQ3d6g㥍m<%禳ny5M6Cza]QޅӲX3t6flWl+k![ Z9Q2h!.l:QamTuժqajNo|77n-YTiThYJ5ɩnbVJ=>9'ƱjYύFzs#Ome(?^vU#ؒŵoӛp{Z ^ 㧬N!1 + ݙ"~@!BFҰ*u<~k׷k{Qsʹ:!f7g2 "Q)8kԬL⁹CUpJ5xzcaT J^2}Y̴`nOAVR*ϟ:3osy(= .Lh2qf1,Kc+%zYk 97ޞ}Z5-LD=Fg&z74b}=vqK1?"z{7ux$=w' J`o޼ynCɃ*dqLW'Q*(e`=TpcU6yf*nhe` j9lʱ׻lR Mn&L(*nh%=*F &8F9*eR*Z:gh73r%ܞ֢J^ؖ=.8Mo0]<0/wWR*(nh>a/cG z6$3'kԲT|\hbݱR>?V6; F ?[Z 4smv)Mn]D3-]6TQZc=/p׌X^~.Z5m, {֬5x`n<Š {ڌ1]9y, d{Q՘恳Y:U3˧15Ltz}I.Ńg{rֿߠJG.f嶽:˙뎽BFF˫r9VVT"ۮq쉖>yNQРIsD5-Mm{7oZc_yDz`4ٝS[i}ճT+,{IJy;=Z'L",c+jlBݝx`~ C`j'M LJ^gRޢ:_]7m)y+j{ݔP]r8ӲM!K:{FNj㍛oRiqK|8LQ)^<{Jy8a3y᠔]VwX硄?SʗYe,DAO;㤢-(hPY pyфy87H+o`kr.è[u;w͞H]FJ務H/8WG@qcMqaxKxchpSph=K6y9)̋& MUx-W HPȤ, 7e45ypϭ'R2/ė]Y EeBuɖ>2) Dbٶm}d޽eZgK[g]{6;>6?^5*u'*/AAzUA^BܝPkQtzYT;8XTæ='X3)(o7q弿-JRi݈qPT2x!5ZֿhS`;i<fXֵs!Z:Nvr]K(lTls+gu>ͫdidU`uل@Z=9\R =uUxJr&ykSX}ȶL cywX?%gO㎏Y:wdpclhBx|w6:|- a~(eR\UJK=݉6v-[4\fSNԵw,O#  Q @>:UdoWɮry]~)`Q &43}X:-|YhBo2+}U>-rB֣96vgִuڱ\f{vӡˈqcLN7-Г̚K D!rؖGv] &E9;X-4u&TjZ /j;2ڗWwݯ G ͫɞ9!lޟnS%<ίfMjXY=_fu?7/2ؗ_ɭ"tᛜ +u-PTc=Xβu>9*弞KVmK.v n cӞZѪGȨnNpq*ZWvĞ89Ꙗhxd!ZggJi66-ZހЛL|x-sB/2ÿxE&M=x;8*x:ڱ;zo PϒJٞ^d3>>Ŭ0yW{F_e2HUņs؜xVm 5{"RL=s_X 9sۯt?ͣ cjHΔN.h2[2"J,_eg ᎙yX.&=^nٿj^z[5]*k!`2WsN\aj .Y[IϹ?ZAn"*ˍC*Ÿd@ϋKklb:=لƏJZ}h )ٛWɲ $ԱtBd6sa/f;;\?&e59ǒW2Ky}]R#Å~d (aQ?sʙɿ{qPuюv4]Z:(oX\@ܳgfR0]O~}+,"ܶ ŪS09C3^So6dF:UCfd2lYr/Y11F3͑^OɳzkvXPݧ$mJgѨuzw/F+^cs:[? V~ewcc]n,{}GrE  \Q @OͭzɎKP_f@6Dt'}Y)LsIz޼JK\lkEWYJ9gX߮%% ˬAl9YޤU4X{{/Ȟ6],mn%d6CQ &kǎR^>jQY5 Ԅz8ٓ~~^필{8{ggj:rHM&ۘ5:CT&' Xe`GF Lb;ԶwPBe,rtgjYk)џܺm?~J #s)w:X1  Fd RƢp?W/ex;N. 'k//G5m}k77,ok2.Ȳ ܹPh2sǗYmȭoQźɡb5آF݉R@ONT6⨔X# v8SqW=6AJɆpKz/\&y35gWLoP`ǃc>j1 OVnjcON3_5H9T\KELS.W;XǧKvTQiXٞ^s锷hwq7)35wh%q(vUbٰۧEb&y!.G8YDSOۦL1c<.+y.1]3uDrqSnJ7ŅYjW_\UJBܝ:v){b,]jAB[\ꋎ${tUTA#I. 'ܰ?=dpX߬q<,Wrc&3 tFR(dWF6kKgOwޗA[w f`B8. 'P* 0Aa Y9u C'pX?%GQ39QC.N.!߁b܉5/&7{%/KؑQ2ÁQ *d}^ʬѨsjk UA'Qc cp#^c=$A.x0!BF&*i fR5kX̽s|Q3X /D{ުnX5WGw񨡂C+ Ȉ UO0A%˒A&3< MmPFmH?,™f*[;.Y`.4[ufrѮ:9  t   y1(dzQzAUW[zHK"\8[I R_Hi;/f*{uzny#Iy`^%|F3kb?2  \*  {:3/gcj|>\s+᪒QłP_"/ک7Xff1}=^AcDch FNTj8QMg> F+_;D5UG-r 8 ( ЏLbͩ޶ȥ0#ȋ*&Wsɩk+q|]1[4WsPc=A7Юa*u[sPkFtSypE鏟ԴkO錴8aev3͌ Gv =ON7\ky`5|V}4jyj42 z^*{9/Fo4#HQU }+< ~YKb.hu9܂{kAADƪ B?9ZZh- r):6VjcJ`GGKkz(cbA/vr GNzJk0'׽{17dB7!LtW@.6փ&MWJWj\7Lr'ӎz-osu B$p?Tn壍<8ϗ_ڽx J^[ƇD$_'Nazc:gnyF-Y# Vr ^9  #2VA3ߝ'N^`4ٖGp?Բ~JjTƱvr^*;t|~*(llI0?\T ҫ8)wn41nȤϯ\&S=|S=QʤEN/HYᇗ6΢Am9w(hPRPB H%4:/Oߑ^.<8?q 6^O#zCH<h2Mv.d6[AA_'{ J G.=5>TJa)k~x{?LsG"̚^Nʦj>ve>{.swQ7LJɤn 8*ktg}<}:˜έ'/aП ;70gu5qM*1>n,㻏ʼsqWںq껳=z<~u*A\ƊA8hհ-%RѭYm3on4~dնp&M:o禸0<%<1[_Gu|qgiݔP]?K?m:= M ~ v$\CQW3j:(i (W, f\=ˇNiFA)XY;-Z#ϯ˱W"\omn;YAm~L@OI%Qޝq;/ `i UꠠQGsl{ 6[Y~3KT=O)KXA.XA8OFu3M;Gt(܏I(eR~1#9><$]8TlKɳlO/ןF7/J.c9<$nJ]1>c۴k&?>I/IM& U3dc4qǩC7i.G*(CONR֢Ugn/'e[ڸ%>}y(ؐ>bCx|wzVހ\*e]\(/^;v&Yg=|m..D¦='hsfPѪ!Yr-k;f`4Ϳ=;hhy.;i<fXֵsY~tM^ۣ bQeQtzBݝxrT$ GK,mnQ,,2M-#!̗yH0?2)*%DNO}z%fƼsٜK#h_; (mnnJ(5<14]zץlNL'at{͉ǟ'ѩ"JjfrlsGF 7Ɔ ^?SQX=_!NtEs}ńk).p@%/ވl_QҬ#Uɯfz(ܙj2 ֿ~8˴mv]ݻ'yֻ>9~/"=ߦ c4Iecߦ ]6DбD=zx[u{~ջԑDߦ$ 'ڻhC15Ugt=͑D_G1?g̦[wﶽ7BwSw6V$r ު{.gzNg୺W7^^Tq QG={zO麫eBߟ]}P~*{Q7:g IW6U8Դƌ?էC_X_gϞ,cc_PExB׏żX}% Ѹǝ>YHB{(I Vȿ|M1~ww5^8NsBm`4M<8!?UTw5V*,㗉Pc <ꗣ׏ ]zh:]烡_G1v3v7B׃:nSǟ naߦ c]:tvB*lڃPc|cPּX>9k·`K([wTz|pM`yNԝ#_|ֻZVsm%vCOgM6BQuhL~-O<64x ON?tc秾`:39b~JXB׏5]]hUhP^6U`Imb4Y1 _oS1 vMoG>__yƛ.Y2>z*t=9a6Uz} nqߦ c~L*h2V,˻OEaB׃M*y0 MiL9\!d)UC>oS1?-o\y|/k)wj|C;Uaj#iSڥ S>qg0.7˜;@wP WS uXz{W֫` P( !46U>7vU`uWC,1_]ݣxö|KϷc?iUXs\7M5 cl#?"9tIºKSkR|<{߄ ]g脺M n~{^}Ϗ:2w*L]qrȗ[Suk(o<|I~EϘH]}cW3U7Z.z;uق3mIP7'x[u6Tw]|]>^VݫT75fuGwօ=FQ/ZѬ5>?Tսz3t5^;zAU;>¦zWMn}\n˻'739Ї=G oz>nzÿL9zcyՊikVU~(Wj߿~ȥoG.bqooxw~݊R<zK/Wx;/eE{d;/gwog~^o0z>*e1ߴo8џ0\zҟ*+{ԿD]3Z.~to)R| ]6ϑDߦ =C {g|+_l)M|%&޻=S|5Mq߬ O~Y~l<9Am`4Ժx0ºM| *,Pc LCV"t}zL|z鏞??~*z"}^y?}~a_+fo ^A~yeۇ+vaݗҫޥx%j.=h 8~\D;/0S?`[,d|b71%[Do5k9П̅?jڟgjx&9T9dLCr'V+ S%Hj˜䐮0Ucwk c|IS]ߦ ]gMte/hTj0nP( M}OEnSPã:mC.N?_vӺOwqpǝzZ-;,_M}&˺}&Ng_ݧ;$ξ/S>'}yg>t)ڝ}VW}m¾?+̳RMR<C+Ue9:gnj٧oξ;tw ~w>ٗuE/tIww٧p;2Jww<>'}ҟ2Owe_.fitʗns؜e}96Uzyfw{.uz8k yWc\y?%u$ѷB׏wOۮ6_ƯCI~T2wꐏF~zm޴AvZ,?; .9`X~MrBOF{hoϫ}[.dMs{{esB}5GCK~z?6m]{W󺻏7e_SxoS/ctGg9V?)asVت_ _4IY jq_Fu?}%>S֏ijGVtOodC~-Vj|5\9\u[vVt:k٧ξNw_ۗ'}ξKU̬U>>ݗwu>MOgvi:}*ڰaX"%3曧t•㦣qϏ;5|PH˜C*m0⏹`u!\Ehя}O>Ov׸k ctt}k5Uugh͚53h]2(O啷a9hr֋*,y~A yv`>?]ЛTa'NX۷LeNM4 p~7:خ ]_8p-i.ug c>뭟~N2[_XcFdVa]2V~fCOaxhӡ-;荧̫z޾,+v9}Vg_>o[vgoYy }vg3tqg}YgSuetyv:<|z6>=ȡ_oS1?-a{C'-F;|ί ~Sq!w5 e<'!9m厹8Nk7qɧ7/^Huk^tqBsG߶sz25АXy~w~1x4 ^~;OlǾeDZ05t[M/{ׅ2b=… p[kΝ3P\Puپܮ޿CP(:e¦ç?ǎ c 9 ߶BOZsPu;갏;twtYe}9:1ξO3a5gۧ>3a_'}yا;:}vgݧ}Ygtqg_W[yjO?0{k5VW:5gt=G}* Х *y//$_Qo^3?߁S yA{@|y>Q䃡y~ۮ^ ?6&2E[+K߼M_}*0V{1v lX/f?۲|ېӍէ2vĈ*hn}YփӇr|PaY6GJLC*t=N@:B)Vï9$3sٝ}٧/5o_/ }vg_w+rg:}Yיw)}>Nwwe}/}&{d>'s >+s?0Km` rWla୺=?~RHoSep}?}l}uɯ~Cj紗@TBwiέ[7W V|*t狞|sz_˽-j[vp@/;>.z;uׂ3mޭ_>Nr=g" s>^Vݫ /ezy}}y\O~_n.oս`}syNBƬUѺuԳǨ=E]_~~z߿R/{Q7p=v:PKou>M S?V9Wbw^잧MwwvЯ{:y;~Ӛ3gS{Wݗu٧ξμ}/+v)Z}7sٝ}f޾>7g_LoQn6(mehx Tãs$T=" V~y}*tÿ8< &d\veowS'7 ۏ Ƿc5}݉>?pyWBcM’B^WJhNp}*tc{ ejӕWO<O%զVpg ~ c _ cLg|&v>wQãa'B/dܯe/=_Dy>Chuɭ<>/OwY'>O?3o_٧ٷ8ogwY}VYw_uu gYűL]ߦ c~R?߸Э]5V;|>Qu'W?yT؀ x1ߐ ]? ɡ]/\Ze~㧓%C}{cAWܸ=;X~䉼Sux>`yaӁ}^} 6˜o/lގ@ξx*썦zWOQo/C&HOujNM}حL7]֭cw*TL'ΏP*9O{=a8<ם{Mg_1A:>>:>{ξWetٝ}}vg_}/}gۗuu:}>+νkV%Qaj=s\WD_ByR^`)n}d=_q4pR^R+P~20擨~y}: cN9b\s÷5Vkho=.u>&U'WZxU/o;;0oB'L(dK7."vd nUݑc5ch96kz;ZE;}& :^>3%iKiGLU/X}j JGu%ZWǷIguV_~9=nw@_xFhYm{l?!?TXփSMhE}>;gJwwe}EOwsirgξ٧L}2o_g> .jJP+t}96U` LK2Vz}u ~ ƭu |,ߩoit ?c?sW,6SIGUCЁG޿{Tv|.yz5q:5yȯ\~O=r%*Amg ]vl[Pk9}^SxjLvݯ9 |A1yf{?jqy^ {&~6愸]yٍl9 ϡ={(uǟ˛ ^!?v@~Ye>T8Yajק~|?h9t4x uC WC翱2*tte~{.ГN*94yO;O3gOwuw<}O:m<;:^cwIwvIw_>ݗN:G7wT< -O\K*t=[O3Aw9G.:, fwg#޷'H[?}}?_g˽$ziw.d=ֺ~B{!G?.T9 Ђ 9 Y'V~%6UF:po &#my_v{(=ôkx/_ڻ<<\;a9hrU8ٯ޺y{@/w*t}俺}SK_&/c-}B_m m* yC?#/c-ݷ/u}_םF\B"w W˘eu19?3tȚǼwٗ1H*A>U?Ae=8y:<؛egRϧPãaW wLϹB^:tw LgtYg1=[+V'}>D ,/:4ξ[v=uK6w_t> |};twʳ޼}>^|ϲ"~>ߦ ]?-o\y|/k)wjt͡q 6߱oz\0C=-/Y`OƝQǏ֒?Vvૃ ]?!^=u:Ezz\O?3k?uk~U}.Wǜ3=rY[lΖKܻ߽+3?6?6xbwF}F#%|~ݢol9?x/{ чؓ{aӷ>&o'3-g8)=DǾMǾ|@zGMWǝ~rWtwS9ꭁϧlմ:nVn^6Xw9pӳ /ӧ?w;>Ё4| =~N: |^nE/}JơT(w5VBUACT8^;cM=vU1= s/{T)ǟJecO.;?羮= .=髕' ~Қï}:zξNwgwqg_6o oݴh=T:83}>+˂>ݗu?ξ<>g}o0}>+;;$3zaOG~{o;yo-D_ó/O(Sǿzm<6atwЏWA+'kf ܺa/7Oޯ|_&/j!>/Of,ijn.Cיp*΅STh~xexY_-D_o?6 ο~~ &߲8,;t!}۔}/q+Kϼh^ڿWCʻ2%CgڧngU\:^78Zn3?~}J}n|+Jn]q{O9NY{' }t6~gULܢ;vtW(}٧LgS>'>s6vG0=D{8}y']~2o'ӷO;twteξ7Hg3oٗ~!WӿK#jܦz/Ml>SúG/kU =xM/n]E@ӝ=Um`|oWN<7\=' ?H_f:;h.<ns+7/ATxBz@0HKN_}ԥ@qмksXi{:v߫fG?'wDZeGg#8z;7}|>}8&l.{ϘOU?'zz<_u0czUﲻޥC'fu9gnY/ӏzߪG#]~}?A/ x^Aþ'?ݓ|4qt{2 \zu2jrBs]z3ml~A?捴{C+~{4e8t#>{?z([yGfWߋźKf}ڗ]˫}@lWFvgW/L|C/QWZ23? e3^_Ϊ\MS/U/T}4ۡ/Gwc_b/=wtNsyޞ9avTmuOI}']~Է岌wSҔ/uGzeg3@ l}|Wouma (?|%Un`}zXXھW#l_*:׫ Qu+Q/i]w| ~ruC'H;іckB] S/{K 3~, E ~#n,YD W;-VݰX?s@%9zӭc*~8>MOgOwgVK;h!^/U/ܳtf~:f =6tQy=ōCtC/襗ٻ亮g}7Fh${N;Ϻ~Fy-OY8q^`/ ~=Eo͗e1qXN0{~+`5=CS͡oC@0Q|`Nv^:Zr2G/:~GGǽszgӂS_hŗn4e*㇧e!0k3WeWzoi`|,zϘFGl{ U˃{4_n;h~/YxXH;v>IW|h:3Gfߖ,jm_nL7@i׎=ǝechN=5kb3g9zF9Bj~9(A_;N ߼nXc#ћysBZo:[޹gO7yΩ'>Wޯk< ss?H?{b~1ں?cQ^l{o?'Oke5q0~u~|>l>QwRVݻiy)}5RG:=+u|/}5L$c~]t/[Pg>˸>PՎd/ Zҩ>C 5t)Gv~{ʍvٮ/2xxl޾VWg٧պᮾ^z:\0ovݯ^~j ݲH9Nj=4x{^}\me9T^z^)0uK9wk^}/tWf&?5뎿OH$]~ 9x z߂5 }3y^Y?̵7c;?O$6:pUswUeƅ>oz3h/^سGjuoY r gV5= .=Z|#ޡo1yݼf ^BǪ }%?_9z]=Otsu?.gms=w|sR2>OXC E3vώN?  _]rrSe)?U)k|<vmLm?|ZޯM-_|o/f1Rvgᅣ>/\En|!41o:_0ʗ/n!z?'yϒ?w{Ee~3; AǺ.Pik?rŕSrɫ|XZ硪Bosm.T.1t6Tz)<)4~aZ XB et}T܂Y"%M^\3浉e/#ޖ :[Sb_`:83czd~*gO2;^> ry-~{2_0OwPOwɘvSoGٗu;V6:{Gy^{Izټ}Ku} ipKZ}Y4gi֯4S=~|UcOXobUaZqߚ| uKyΜ|\B a[|vAµ'm?g8 [YfJ^gQ >o7Ʋ>yme:} yǗ҇ b_gYO%h$t9sI6sNމ%>gGGL-|zڏWc#OGٵtʸIT!m'{2fc;nzv7_o_~_6`ަxV~r7Hgptp:d0aQqy^֝nU\k7J})UB*7n$rK3yνaݸ_$ mc:ms;sqȧEj,p x\zަk?/3᧶ٙĝsWR;cL91rfzsiNji>`/m,@(wϓ:vHFSS?giuֹ9u?:s0ݘw'<:b*g{Sq~ﺕ; yۦMsVϋ_3TsYgGյ]Ysh7>~? o]r?2w:.?YV Cm: -ᗿ;0=@:Lu\,b~>fπ^ٴ|~J~JYu=ͱϧɠ²tъ~3gOn}٧L[ȝ}/-<z AOSl}R}gx~Ww;̜}p3n&÷Jg 6?ygxMdna! (SG~v7t/m%6+ O~ת^rT9.ޞ|$xeOfg.s}eT߱VY:8~|~Nи^G8S~w?_'e=cw-n'#?ZM:6ZI-{VYA]8u7u;Y_˺LG_9\ۯVn“ߧ_>[noiޯ2=ߪb.g6f-&N{[>2;?_ayϻ|q?b>oi>o3TemUB3%'[hhۋt73ct];LJb-q8ӧwqqtpv ܹ1k~PM+%оj;c~@n73wp5Ï9aO?P~ O:F6?[T6>Hswyg/r:9tϧ{ۦvś}<1B7VwhaL]ac'Cg;!5sF󮽓~b=ignB:%L\uLv7]|Ӿۍ"26Hm'iTy*~~15aky??Hn2\`njdϷ\áo陾^_M`\mD'B~"Tb:qO}jOe՜4 d?!`˙ï+cLY] y=ϊg)eϏ./s#_tӬtI^}]|臯a߯}*}^Wjv:Lw_wg;7|oo|B>gtϘ2c[h_Ds1O!8A}ߢ׼[tJsͧ`:4a⛲ kuw}7Jw.vGu}gO>;V:)ǎNhҠM+S*vұWEߥwT_V׷)>˜)u'^BKti= /Ϡ/<_/HLvv,g|qaۿȷ+Xyjt^ _̶W#.G؇{^nsj^{gIש)~<Ɣ OvmUAsS]s=tFKΖ勓ʖQF;wSҡ{C*4|otsfƑ=2zB/~{|>[x +Z>}'+*o iq?~N3^~3O{vXu[Nf-$37}l+m޸-&etWwگ(8{nW߇t4~,塃w/]9[Ï'U},NF >3ikz88G>Sk?Vw$Tz^D3o+@:mIl>BCo/{ixKV]g,뢓yv+jg7՟ʻLx8~,ϰeq”BdO?g+gawi_e?M^zKz}:cVm˥|PߣKx#‡z/W/M3 zXh2r˜nr>c9t=ρ]-nˮwX2~ҽơ\W~]y=;ό%ߝ{W~~g?~޺z;YaYWgiK5 <.YM{EګLK3s~nXz?{~?>7/?~3 џߥk'Kn2VʒiOY'1+py*MjtZ?tȧ:ߦ c~ZC߸Э:nK(wn|ί./{ҁ'H i: sitlʟvR \rXS~2wBYIZAi̖S4skWw޹4e/A,ےZs|s3_(wsɡ]#~@b$wuܶhS3t#.I=rXY6M[_~Kz6m;9~_s2Ynvq ͽb=f]qڑ_9S?<ڽkіOҽW̧AoO:=*! ]/XPfw 6zKOş;9ޡz~~;>7e;sɼ_@7M7ϗOv QfSxw |~̜˽ <3>yyVe3ͭmy>sԱ͡GEk=_N[}<)[}\S^cg!; A뵙afBߐzʿht7:~ݴi_ !jî csu LK+GCꩮLg_RUgOCSzUjZ)]աА~J/ߌ O /ox=B׻tgBiç~Y :L N43f©kةG7+]k1u+˗׷7{_~v鸓Nt1m~O-[h=fXsօ7=<ݽS>g>ͣOξ'tΡ%Ƿk¾W| yеgo\L{綾號?L7-=yCݶFL.H{n "<[D|qwߜKфOx;NY^~pU>өݴBxt/gB¸箸*h{t4PVO/tgxܕ]t:_9-VY1k}>:/~>ݹ Gg;]HAozi{t߶|Y f;e#:Ǚ"a\F z#i΂5~@G͠>Y\^3y3ӭzهr[>ЯcatOL7 //݂|}OgrP? 鹷ӡ?.Z Jse~[wZt5}T?a9g{nFO?_OQ<6ަxV_;}:ۼ#gNXu ]q|GgxMu2?!ӛ;?~|>ݒ=wg|~ݢfe}=w>~{n=`6~r myfQfSJ6~s},0 \λk=@݉~ꓐo?cӭlVl3'CwЙ/s~1e>lj!?x>{k3 -ݞ˞~M2]&;Fc÷ѩ?Yn>NA݁ߞNd?״6Y./!9!=yOW|exY_Ox}Υý_֏5?^:mLP}:Se] q*L<ò_Q1NƎB~ſ>W f.hs%+]tY8m09rW+θb.^kesݨoȷTcz;O:L}Oϧj.xX;:s%tv^BO{N:û,t|[9Aw9zn?l9-zϦzHKYK}z#iZOUʳޣr;7}ޗ_q|nWD2zdi?d`?~UﲻuŘo &#my_9*w5Rz^ZNǻ}mM?~mz-'{?_勞1nmo{N>vuw:z!ցK:m~= yN>o;e>Nؾ95|o3:aS_]X߫ucxޥӨs+[G9۱Z8lZ9Y#s7Qw$ԋ}F^5U,/D'AB}x: LGw}_] o.P/{Q7Z:v+弢=wܪg*?>uaj=c.];?S7t5fw1V,+~:PIPwn>}fuq{v=~gOۗu^}gmw_jKLg_Ы^LNſ74)݅+Ǽ~ݧþW Og2Ϧ?KSτp)s5Ucu)meh\yB׃s$ѷB+rh!~vn~Va8 :{2u=T)kd/}X]qWpKKD_OξW"w9}VYw'_Owg?{t3te}>E}Ygߟξbxkn1i[V$98!=*wfή|syf~޺UG^R뻸n=;fC8WUumt >7@L\srvwҚ5ktǓ>W>߄~گx//ЋѷƼOk9[e_lx*}^L&~K h/c'C'aϏ7Owi5;tw}:>ξBwSO}g¾|>tawqЧɝ}>E}g8|CO:}Yw Atߥ]UQaj9]rXt4k(o<<9t鴯R򸫲jPQ}::k^G|˹Xkr!_ꗝ׷\Zd}iI_*7nx M]U7B׏7[oڟZ/:4CMMS֋}m)Q6X m0C\׫O_y^W,/g ¦_ zz<6*t=gU*t=fzE*t0y?Xw]ל}/?}yw_Wg_yξ٧>Ng컈^Ô>>{>}2g_F}&{d>'s9Ya\9R?x hKC8oSho]bx಻kM\+>}3]qבoReD8 Tcv}Y^wYoSZ͓CSA Kis?_ryAMہx ڼiKjϻcKg},t@SF3s ]5'¤}ǔݤqsBTK䘇v+c򮚟o.0u>}RoWUys}/sϥzSߩ]Ӊxix 量BzϏ]5.tTKO^3g˺<}yξߴξ¼}vg;Ow;}ξBw٧þ>3oO:KwE鄋 Ѹǝ>B׃9Ta{ȡGgPJm0/$s+O\Ф:g*=t;ćUnus޺<ϗWކ.AEp=5kշ/mCC  7˜/nB߼-_)ʸoR/Cj?)ueꓑQoS147g?eb4|ToW4}/>*A ~_ ]SC1?L|=kRLRE=ONg}>+;OO?3o_٧>OSξ?t[yf}٧λq)f 6UzPhsd{ZʝZ)N?uU#^k]P9KUU; ~J?}|yξòxk'8savロ:K_W/\*yOE/,e)*mjXδn3t)u Ps_A<սu*?=n.t^ lMZ>`;d_,mN K<*>;_MMէO=?atϷcuTJkяů۲>T :qg}>;>nue}ξVٝ}Yw}z 톺OwIg_qg;:esu>>w޾"M'^ߤ ]&y=6Ͽ\Yxze0t@ܤ ]_-;s4M<W>-&o÷Tc\~VaY XUQ1/SCL=qϯbauTz06Tza9l?e+9y:}yw>}}٧<}vwwe}˺vgξ٧/#ۧξO?7ksg߆wY;Km` rXla୺=?SDG~qhgNn4\LPЩ_|ir= w[qS>lUU7'm.9,ԻtF"?nݫ usA)_ lT?o;vv^on:kE^py}ﷳ|a9B-A\dCuWcum]D]]/W}uW񨇎O8FkC{x']wϔ\zH=t~\ۭkZuCz[.cO,5S,tiJg_vOtiz:x޾yٗNg{ngO}}Kk)mz>l+MGwk cϑOC1R|?oQoS1_f8ʵg?PLWvw Oӷ|~ڵsrM9wzy?R% m""v;[>?pyWBcmOͷLqo&iw3ϯ_K?sMpǷ ÷\m0$w=j? S/Mvͯ_K?v>ϯ{[`_²AAe=8qL=lCl|&t7*t=i_^ܫ4_nv>3oe>p+OEso)}>ɼ}Vg_~+bg ;[y\N7Q(fߦ c~R?߸Э]5LŷܙUCugWƩ 8o0'!9K[й_zWg$?'߶_~Muȯ-pQںev|z>.y?R  ]9R6gq>D~Ω}ط@D1UwL~ p0Y?uMzW;sk?|A/~h5Vj>_]ߦQay]r)h'hPVO/yy|*t}r҄g|/7oS1&|*t=p*t=gIߤ ]?g̢d[uO N"L3jCu_xޥK|୺W_^TRu!-C\ǸXbUQN⺦Uv']Q~bKM9ͪݙ|cx`愰۷B׃i.t6U3b"U d~Pch*t=X6Tz0 ks ]ȡXTXW")M$q[oshsT4k)wb;|0^$z*t$ tM S-ZOV(TnʁTã݌й'~Mne//ڋZ n|j.Yi<<jk[O}>0߭eqoǓѻ[x`:CMEk=&IU}(+>ߦ% SaZqߚ| uS|GHoSO .M~VaWemӮ*`T0{[E1v3tؤ c,oS'pO cAle ]CLJB׃:kC ]hB9dm Ь O*,.ŷB׃s43宰2^qW֐w5VϕMSRGMqp}P ߘ_^_Cy[)B׃]-oR){P9w5V愱]yhj.t_OFW}rzs.]EzMwn}ixhڞ4M< O_wX=W' ~B׏5]]iU|k:{(/*y>GMƼz~Va]_QoS1 cpn}|3s|ymrS 'wY:mRiN}*t=XMoS1?n?*yApz2T1x a*t=fOg?{!Yg|#TaOKrprC'-n{(wn|E՝` ɫBO}ĩֺլ.񐟄.O_1Ϗ;5|PO>ߤ ]vSh]ߤ cf=+qsNjzXoSSB:\-戮rwR mR?tjxhx O _*t=f ~h=q7l˷|:Vq]qUјh^zy|* & z//$ߦ ]&|*t=?ut0nN**y<oS'pO cAle c8C3tֆ ]фsm0+C*, 6U` L+ŇMg\PyϏqz:TK*h_T2B׃`T'?_tWWu9zBt|icN9ak^M_x;.E?^n.Zu6TEzMNB-6P716*-{uԅ2X w4wNẦUv)_p'8FKcz=O{~nxUkzss|OU,Uw0b~)meh\yTãs$T=# F~x}*J4sMw5V9|I55ݾ]'Nn̻:!vӜu&Uz<nߦ cl ˜A&XPaY&T`u/P0_zs ]!b)UX7B S|:yoU3+Twv5W Uڥ S6qg0.7B׃ݔY7B׃qNTۧ 0 $ao4֫aSsAL]AMx(kӟ/A~B'gi.]֭c <3v^hRo~{L RNR|:[Saj9]rXt4k(o<9|;ՅWN9\ FUB׏훧t•㦣qϏ;5|Ps$GGhݴsNzAAAAAAb9\Ƽ~VaW"~)M$ Ѣˌ;R|E՝` ^}t_^~e~kO7l˷coUZ=wUF]j%/n)`w_* ~oSo~Va{9TM宺=zx[uaaX@ gDSxtD{^糯$b7U/tX̻ۧt)@Ϗ;=|eϑTG91!Y*F!Ϸ˜`7.tzq u|-κܭ}~ao|: 5`K([ ]&?9T+C*U^rT9Z%gw{*uz8k yWMTTW]X>3.M9˦ cO~ ]!d)UX7B|I$q[/9z|=Twy|ERų"1_N\z5zmo.<Ν[G}4o?ll׿NޒLW? nRu,' {KFcޣ$酮DQy?^WL{˿o> C*5M<SKa`W,k-@uT_oJԧN4- lpLt{jۖӬ3h Y:x>4 ~=Ot֛%g}^oL9H1;f7@m:D06/Y3hgWdA1|,E*~1 \͛ix@#~P;ich{2?~S?6Uzy%gw{.빚zO{T睊Och]|ۈ鬟Om1м[6e59^<ޢBZ^ٗ>lᕋivv}D"oѝ,|g.[ێ}Xp[rЊ2h{!?\:;9Kn9sɵY",Tݹ(dz=>?^wu~@hs$1opÃxY7%ܸ<͠yKj7Yg FnM-=1}uv~--O=.Q1|7@3~S~Pubt*-}` Ig 9g峷XoM址Bh`(bڟ1gϠKi0}v^v~o_Ͽ};{iN<'ګ~@(5cknn6:8-[W_Cf͢??/,j c~Zhse{Z;|En)d;>oaC'geߺˏP#hP60NuryBqyQ=Ӱ{n3g=H/0m?-.Y4ù0f1iw{h`:ս{*k^ pnhBfs~/Źz>_ -ZD˖-_Wو} _|0Z8ZP[9Sch)amӅΦ}Y\:us CJ`ƍOnݚtwӗ%ڻ0]yCtκz߻y;ﳮywN!ow ~!?wBO[oL?v' S箸&_cu,| m] xYDM$nxFhMKh#óG(^^`CP!m f3h5_y}g|0a~iŗvy^8޻8:Ӿsq }lBr^|ef?By+tEtV;Hff *QA*$G? |c00|]t,f?Gh&Lw?H<0Qj?~I~uUzy|@.Ňu^0^V]k9??};&56Y>sYM+=~``l/Г&-^่ظT;=Ď3-YyC .Cv/%_, 7C4x`Opc`sm >M6%ShE9M^?Q:\?nʁ[ʵ*>˜34g_@]2z΅ϳh#4ҙj꺓`:; 沯-UKU|z&>9K wҲ},8,Xr5 ]Mgσ7sݾZۛGKo.hޞ͙wYZ_\hsD#r=uul iphwz=Ӣ^DǟWpc^O?]RiH7 aߴD(y<=6=B7Π#6fN΢ЏA}#At8+tGn|f s)t^}fЬS='݄[LipG'z'DߤOII|BޖwՐC-{3ߨJ'+*w*:-?u_u[|Wм}t:/tH?uI뮟Gxx4i;^GY<3~EdmK{-9 yZZrlV s 7\D6r8-􁓗`%x.Rڷ">ftsܾ$}9r1ҵ}_^Fnwo"$[d tM:kؔCz^z30>G{\}o??wC"z9ߋhV*8{'i۶ |ܺ}m4 w>@gݛo]=3 ?<xnY~~gM^;gq.t'X򺥼`mN&߁HGu u:`(t01(F<S9ǓzZz"  zػmymD? 0!&987ҿ|_ . TJg'L9yKUzyFC36q>4{n6;}>Z;Aw}xC)yu-[wg6E7.tT:\_Q{)?~Gg䣙"zK.yW=u|zD C7ؙO8ւ ]6ޞ>{^GՑ48oGGoxo{!BWJ__od|sOcfhξ Sx"wKd}(?~BO|egB>u'7};`$M$q[osh>f>Ӑ9R|E05uΥFw 'c?a*lޛ+[DCyZl:~o|#}ѝn,4UIPTã9rW\s0˸׫I^kȻE7s x{뭷?'2R*}N7{(/*y>1:n6Oz)rH&~RaY_|*t$q[/9JPs}E҄)?~7lʻ'*,)M IRј(G)zzã#ï ]_5Iecߤ cl)wUr+6@u$7.:U/tX̻ۧt)<>o)R| ]6ϒs'L㐬'ȡ`oS`pN7c~Tؼ.ߚ| u{zãTGs ]_U˜Cߦ ]ho]px಻kMu~lms}-UU~i2V/O+[SvR|eS|PSr{nAAAAA+w0cC*,!~m0%98lsE_Kc.+j=WѩGa mՏ_{̏w1wl}~,{dsG .Kǟ*s@c-:~.ݴ5@# sKoS& 㞻x1D_Cy[=`mN/(:{_S¶"/4֛h..L*:_sa~@ժ~RaYX˜s4]r)>{ZS= zIA~|p̔,5彖FtViǃET9.9C|}SKQRu c~,]S:b~”)>˜}1)'K}=Lcv>kc뇔oS}k*L7K+ޘ| m˜S:2"kv{.׫/qH|ssÃBPN + 'tz84)~7׬/Y_3ʮUX{!ϷB׃s43.ޞ|<ɫ^+/C\V8 *~99i¶-;Ku}ujw9tƇt7YOcNöX=N+M9؉x~;*t=<\}!߷_`)woIt~:沀Lπwi]pߦ c~ZqZՇ y~P4 Juŵ{?]{ٜ}7]s e~eLgCpKO{}U|䮴?a[ʵTã307_"rf?Hg6]];pWG |%rT98 z9ϡU4z-9'j2"s@k :zw9 ѵ'RYt۝DoS}ߪ0{KFcUO}4k(o|*y.@u~c)^Qƽ#^wOt Sr|RdlT?94K˜DS|*t 9CU9'.C@u9 4V'`Ա06K.ŷ_Byݞ^`}J~˜@R|?0.ŷB׃9s=9ǻխ{zZSyTϊ𫫊B׏5])pe}kANkU|P3Ա9\O*+C?oR1?-a~92}-:⢾kZ@ux]ߦQas\o4=:k(oۣt~PnHcߦ cр(w ުkm|@uL~U/tX̻ۧt)7eS|P1g#iEPL?XO JP0ŷBO r08qC){<[UC갳D(?~-))>ߦ%TZos=Vy1 1t|@Srpwn%^s4}{%0hfrhքH^rm` L˭2Ok˦xN[/ߡU%K.m۶e# Ь38>Z~ Zث.tXet);Ϗ;5|GgɎ:?ݼy3]|хZGï9$K{ N<9:!'\@n|#TaOKrp؜3}^K<׷ rwхL@}x7͟?Ni}n944ROV\|M Uaj=%ׄkt4K([gt=ACBg}6iôcth3g,vŚ _?P[S,zeΊ V y<$C3suJ:3i]nUrBxܚ/!x[W~5竿xzg4[kf\FCdv1XoSs4g]oU|Xe^kE/ 7mҡ߆ abhg3ȬhL+N-_Vv8s;Lwښy vPk8i/=B|40~A^C3θ n*CO#?3m ԍ*Z~OΦ׬7B]ҡ?X3{ML3ժQ:qSjϏ;5|P1t|K/ w^/$[id~O+03\(;L!SpVցߩЁYe}Q?u~Iuܝ3LPq2zOCC{mƅn-!Rxg*Q8W^vEWmlӠ;lsoa疘ff7']< tA]rnT(G苃2/MS{NK=[Y[O˲Dzz+۷:Ϟ7ގe(z]YFC:oR1hz{.qW֐w5VϕM>EWixfY 8+oY+)·"(t={.[uZf-1wyw|;=Vcַrѧ/ tIN0E;+z{xc>;s\<~h5V7MQx~:@Tg:>z:s?fky.=NZNsz=kgcr]s?C:ȡ_*0{KFcUO}4k(ou1ݠ?ggge9_v+z|usy{@v~~5Ҫc^/tDz]r/8bbTy݋T Q3?knП21>+=7GM-a_[z,1?-~$ k? 3wxx\хu#*t^+owӭ וm{ȷ;Ɇ~}_'~ ~6w^?たzɰ eJNJg B9@R|kp12w_|96U`}=wݞx]ƽ^=NZCpkUOW '^\^xao?_b "umu R="!r}= 1xs[w>E˞GwYg^}qpW{ᗴ}q:ghw6R7p?ͻi]GEw aZw:krô|ZsD+̷fAZ$o :p0zZ޸s !YmB?-UH{`{>t |"1Cw7G^op \_{{??y׫U}~'UK{9zX7˜k7O鄋qSr|)>˜S:b%E_yỰ//@ +]oYE]g2=XEss^s<lt]H'Eø~pYᘦN:7ܫ紻n0 ۲n^,WձoS1?V׻듻R|k:{(/*t=Xq-t ubGMcym+1le*~!?cĻ$,}Kϲ`Й%dU|?D|M~Zc:IYфercy']vXsά~r:L;N:z=k/S*cz~ %jU|?rlT`}|@-Ňu^0^V]kaS~P7eܫn=ⅮyzN0-ZOV;׷BGљÏC3y c9LmS NV5xs}?*?~|:=wcEр^`yJg^WT~P9-ǡZgƼ¥6Uz>G>Sb9ǓzZ)"hn?W* ]?ִwtƕhkU|PS:\ߦ GQzHȡ_*iIEϡkiU:b~"Tgw5VOT\R|:Fq]rU_cu=}F׃-P:\:s*9b~"UXk|*y>E:Ru#u/\N>rziϞɗb/tX̻ۧt)5>CyTã)u1ߦ ];G4VaȧU?|FʏҎ +i箢^/ȡ`oS`p<BF5p5Q~T~P:t5yWѥOǟ1Ӽ;h~Nߚ%c/_Ao1qҖ%TZos=Vy1 1t|EEa:o}=jlS%۽{7     ;Ï<4>wsCYtBwq|twt},u;]Ss)pehx T؉WK? /So2}t]q!{__-vQaOIrPrC$iZuuD~"TO#gҜ9ӫ^ѥ~R@EL_8:+t]9#:#jُGrh/б1},|Yl%c_nN0=r:ǟN7?㼽o/z;fkᗽ*L瞻~Օ| u{16@s}EEZ_*?t郇@A…?!;9t'NGpl;+/]IO ҥ'rg:32l3S4h*s1ߏ92p=LW_@{_I'[sn"Ruhw,?~tܩqwy,8]a'!Y=g0KoC[u1=Q-xgyY'w(U^K' +h/n)/t=XґT{turhGO]O*ޭgz7ԭt5ÿY?ɺ:=&V{-Gi/?AQ{y,Jkf@}OgޫNlyw?6Uz>G>sp{z䵆zl:c]E_?tOǻ˽0HqXXUxμCLGg]kiHǯ8VϽqھevƠپ^tyޠZ~{;w+?&噛e_g,L鄋 рk)>YS:\ߦ =TGsmU"=czϿK:k齞+ϣM7ٜyGv~-=2s]zJ@g::s:vuEg޲ К[sn3!4#OW;ӂƅn)g*ӊfo]>QcE|oqGˍ_Nݽ]QkC>yk9dz7N η<:=. mm?􏘐A?qoyywo~|W\߽Ifc喞~ms]r)h̻PVO/y<3/+*?9J7L|*zn@;GaJ:KW{v~|9Odl;˜r,ѷB׃~~>SbkT1{ryϏqz)?sݴ;_:rz|t<ˋ*˜砤J[]ߚr03^'`}JGT=D񊓿&O*VȡϷBOJrf=8PLCfmQP\Y?q*#xEW^[AKoS~TZO&J{:{(/[˃;|!E +7GU˜O"r>ߦ cѢ.5{s0˸׫^5ջ](?:QUcm)p1?a:W^6U`yJG]̷B+No -:JixlK``:804Y1_6UiA}B^sE߭z:ܺ|EGN߷_CK\L۶mF@whN^̷|:Vs\7 :5 c,Ő:?ݼy3]|хZ/+C*y/}*t=SbKaݣ-UڼzEYþ.Q}琢uceS:b~t4qg`{:7˜_u%{g0L>6@_Fg ̢e]Eթ Ayp2%5V2mxm^Ƃl2mw?^`_SCr˲,=gլej 5Y2:o @`F₇7N}޸iz?q>E~˜o˜`7.tzq>SjT]Gw͛6oÆ H0A`` @,ڜPNNŁ& ,^zy+0bLz=Aڝ-Џho,Tȁ'wqрl0򔎺oSWDW~8=&AUZpي V;u_?9 o2Z-5VR/[@^>vh1ybﺭn{)Ot˜DR|*t$q[/9ѢYG[WT阋~T~ZCbC7+X@Mr3x:pޮ6꠱BB۶U \?ko}a\;i`a+4x:xޤze-:~\T|>Ef5UXmd@#} a;TMcFxd.ݔAs.^EC \ dY(l)y^_/uѢ (LK45AKapP?dk"@@9}OUㅮ+ǟo)R| ]֧tйMz]?]?tJu0]u`έz]ue9΅3g<~̽eٞ}.U ~*(L;?G>yk:n6 !sjdtþ;}jl[_^Adjմd^g_:x4"SXtd`TTQ^oۦB+ٴ䑽Y% Vv^ݨ;+|)CusU{ ,s;x'Fcw}\d.{_@~˜o|*II'sd|uuD_vW0]. 8]Vokt9BuGuU :;{h]?ӽ?ŋ[iՃg#|>ȟgVlP@aƋ#ch`Z%zi-;a 48BÏ,Y3oU+Њ~hl_K#;yqHsJ@A, l[U>p<0f]wn;L3O;y#Y7p0RA;w0|w\W_.nUա{b!uk~><. {9z~50;*+)I SaR|k:%1'w|K? EϘ} p=/mG|a)|>R3ȗ"Wyq\t3/>FAn:s⟍wr3ba0^+QxCa  Q ->-4Cx,Zs~ mK_7zU;(ww]~ |54\} ~cߦ c~wקt|k:{(/*t=XQ6UzWg.x U|qU]mw]҈] s9=- bg}gT_?vne] ~~Y F j1~F9sʜyv|ϙhP {RZetL'PviS(&=BFwK z~_I։ y<v{~=5t²96UiIEϡkiU:b~່\˘6#n0| `t_Tbx.Y ^JyNu;p_~~\(gR~֣ wY,<!\PK;acy7+WtA4㨳Ρa9Ln_BgPn/ZByhŗVywbZ=4H"AS_KWb:_x9<;M] _wXwy1*Qל̹:9'z&|667Ǎ710:w:.]w߿gO]g=Goymʣ5}zi k_ԕwKw\3%߭iW~h#<3s:Wp^|shДOhK1B9uB[ƞ/wr 9~}!iq^uIRj_5jϠ<^TbKẨz]]*~Ac>Poꀺ ,k}>~e;^.L"~w^-旬ު2(2t>(k/:BQ[[ ʿVe)yKLm a7S7{D[ǻ_s52~NR'T%_, SS5xj! Keo^kV鈋FuOt c:?Y>k~9?~SǿtZOCR]~Z~m^} 85=P۞Wt۪פֿ~*s5zG%v)~J7~6~m ? դ~ԝ}A}?.y꭯{Z֩?k']~ٶ׼_>ݩxyRm]@j^IUed;>h5r=^/t5{ .I>{v*!*t UU6!Ze2<S,\ eQf{zۋw{w;v:.6[?;s0Cw}Nu'^NuG?>^uϷU>nol{՞>>gw.;.S_wQk~ݦ.l?p:Kd[\}nz~-ds]+cRuN:\urtGln}̸Qujz4LGF1t̬؛t.`'!e8A{K=JP7'}3_G룷K^}zλv^^Km6gd馫k-<˽%X]oґ>p}XX y%x9F~~?!:^|Zg- ?W$8KE-}u|ZtE}b5rm?Ogj%kl<\Q;mԏQ M95ud_mexw ~g0#z_oݫdjkM3=p͹eDtjx $˻Pºry.ָ u]rK1q:VG5J*]l)>\^k^^׽/ztG^o+f#=kXYS*:W ʬ{:_4U̒5_lq tG]x#׏+kiٝzJ/Ӓ@NWk>t.fTz-ʾ)>Xb阋!3"~5~Y4PIUn+;G]V,Ʒ@7 yY*;J_tݙ}tyɤOozf55kY5վ_i`pޭ$O|Ql5߱jT~IPuwۗ {ԧ>V|Js8Hŷn:}<3ϼD]b+pMkUxcM_1>s[ԝz#:ڀE5 |̟KVxouST{u y*!*@w_>y餗רw~ uI}9.;]\?ۻe:Q)M/I!Q{DН~\۫߫>gN5j;IB>ݽNIOUO#nZ?;'0Ik1PcU?d{5J/t56Qrl/IT 3~i~Q[8FGkKWj/^..;ӯWCoz Jx2nm(*c]|E5~ZtE}b5pmV'Nz_*Ylj~j:KW\/;QoQVhzIY2OНI_[I/e5{%Y\(UԚv^'y]Cޭq⇨~ݙ~ ?OU35~ܷwtµ3jz4L'뇬F15cUE(W$8m7ry |Qm]kC])O<B!B!B\'0Ik1PcUgVK\W׻56UjF1K:R@?~O]rHB?('!|QK/Lqur~*@w_ïR3yo㘟L۴~:{y=>V#ףe:\?d5rG}sQ[ߋ$4!뗥$m7r"%_ԐkQmZt(u~Ik[%x!8VR}hx:J Uʣ$v>g5x?L:bЬըOt>g5y4FU/]h`2>n{}uMn-=Q 3~fjhh߿L'\/YUe,Q̣2s1?d5r}&Z/Vb$KCV#ׯ Ipn䎷Bz7Ԣ;V˱t.i-[%!8^R|^cޭQOQeFo󨽤.Y2OНɼKnIżWD?dEiE.zur!*@wkfj5xe:Q>M/9>Qg~xu5A$Y\,%Ao;ޯ.!^kѱj:F'No=zzI'zzͻ>9/7M̪QxT8Qoo+oz4燬FOН| Iި64ֿS=z:^GWoR7lگjIP燬F1k ^T˚7Zk{<Ϣt'oǏO;>&k{b>_.gVGPeF1tAݻ?NG-f̤OB~jU>VnW^wS>78k, R>}I/-޴]odEFkۊFx;/׏Zt|j:b~9]',\;$×#G[ci>u9|DmZؠ -86^XU :\ZJkGrKRzo]#Us>0>Wt%jog|nIߪOQhw% uSTM/!QWk']W؇~X]u0nd|.S~G Vyߓy4Pͼ}yo[t:}skߦ.9\V3kۻkW\רJZEoe{[u=X C 鎾͡X`7ƚ)9˽ԆLDu }'}~I_ǻuwMBY|Kn)ϓתscƕktZu~t|z\Cm^^^O7jz4L뇬F[5 &ַUlɣ< Wt@89&ּeuXs? 8qӿXq #t+, P{G՞]{,uIU,רw<_󄼑yS0y]9tIWyvum6&ᣞCBbPߦZw6^lϜ3}hw-5ǖJh_/$m7r:C~Zt|b5o1?(u~q{!=Ŗ~u5~<:P=W(4t}_%u]6(5j +uG(,~m+Gt/|AWyˏKw$T]neVN< 2?.K8ܳ;ꊀMoBS|ڷ]zLYo2v둮|sQrlT#ף%u>?d5r}&z>+=!=a2y}cs"w|t,OFG[P>/uցy_W'!,/5$!ZU:޴]= Յ/{z{wCjax~VW]髁kYHv3~V'k|tO?{\tY݀u֨K%,׫;kuUMKQ 9?y-u|\U-Aϑ B;5zkH}C_Wnד!S_}!'Cz8}Յ\Zԍ=USP׾{wO}[dcqN:;2/~os7!θc+sժ'KskeN gw|C}S|.ǞΕ]&k5U 'zu˼گ? JyꧪEZw#.c]T3^$ɾrL.+M j[ 5XA]$\>GCWl vT8vn\֯Q<\ ٣&gwk<[5ulܙm_oܧN~]oLG_ T~2[l[6^7Ն{?r){߲\׿wߋ'C'k,K#[!.b ;{{Btܙt`屚._g1Xeg/wC7r?1hcϗk;[_/U;q5yyO%)\t|Wzӿ9o$9&ū ȫ<>ad"\$sϱySs& V/;6fj}ޭkD:{5vNI_@O8уjûR;b?=&8{K]N_yצv8k=$nL]r[}VoD7KP@ o{%Y\(UR|.}++׵/a?D]遟/Kp_> PH덍~nj@?_WoH;cꎯ>.~zo;d}Q;P?DnGh~KMt-Vߋkf*?9Mb;ϸ>v:-_O{#3>Q[?zeW]L[/YŽlohzN9Y<>F_țJW Cw@"7(*>友H8/H]4ȿk-n4wVw#9*e95ourS=`c~^cW|]=]PoPo}yH}' %=y?P_m[|u:d S>9稗^t:jM <|7j45;ב3xckƍ-`oy~=gݺɵߔ ~mjk]9zyj$YGz:ަU!_I/{ejqFfE|hN.xN{>Q/KI0t^B'{{ZO-~5~:|\oׯ?#+wկ_v=f£xC4`hɨl07q7Ѹo~M ~MpX[vDnV~+)^=O~m+&@w[+οCܻh1ɿ}Ni<׫׫+>{5jwWG~ڰvAһOuA-:u^fn ${Gc_`loߤ^Fm/zZXԧI'/Un3F寋!kLwi682߰`_}wMkk(/Z~`SJ9_>G s3ru:B>w ׶J⇬〟Y5K\EcQ̣N?d5r}?KoTwwՏ+cϿoC??ܠTܼݸ+\9FHq=aܛ+_ư;BjƥO0oF?V%Vw}ꎑekHϯ_Yx qץ/V k߯UBnrYGz 칏ސmun?uqK05ڿN~y{Y拝_Fr f=۲w]ߍ7;m7vWle/{}=%{3#o "y ? R?d5r=^f/]hܞ^kWo_?uS~V~[ww|I]o t7W2D>dLЀ{STDN:jH:9wk_Qo/ k~MJGyGwjMmͯlP٦ 2sb-(!Aw=4 jͱus!@*'*so5O.d$?9G(Jo>O|s?N>O7:}ײH|(Ce:b~fu UMjz4L뇬F(j ^_U{zީ6_}o~/ʾ)y*ohXtZ_{9KURWʣ?׾[COwJuE=]~YT.{uPz2%E~ow=6ڿc߼|[ߕ"=GBf7\KwBy{g6;ܧ4f9ogx+Ij^b$K}V#ׯJIpأURf?U-:֢c5q1j lI?3߻Geo}2f,%fIfC~̾S \"7.oz@Ymվ9[nuվa[|-|}:C'-m8 ^޽~umѿߧʂZU?܇?-Qu}ԧ< ޿vA-Anol RuRGSPokCpevF!!~;d!{RCWlk |5}(x`lTn>6ecq\3ߤvx>SC.5 j{ :lZJCqjK\k{(OQeFoz4nj)1/Kyzz_ t 762ib4Qėu]& kb>}3.3.Mn7>=߫PNg7vMN3|kM7`舺|ݡeDnoDK}9otMy/Uϑ mazns\3%߭/< [|m\Gvfs~Õ5sVgJ.Kչo. 9Ce3Ecݶ ߰oP~o7=ǝkaU|fjs-> g>>OcASI{qquuXKz-U\X ]r'y $TK\bޫqYbMQtp]z{y]n!j d#GG76wg\!X}R˘r qQ47rL[/YUe,'W#ףe:\?d5r}}Ƈj R%gTvGeſ:k"k_:1-Y+HP۬Q" }~jR .֢ck5o1?ը_߳5IIa?Awi? ``={r0~V VR|uKV%btҥ!3w[į |ɳ>.= #G~yF1p)~jz4F_TBk\ۓ|uUMKQWzbo9geQ[ =l<.Y0ʿ6+`{/Dvxݞr;W!/̐ Q̏t`uhz9YbޣК~F~1h۱7 ]pM~OB:ςeP'^sM ~m?tI6oը$ |~jU) {P*짪EZ'V/J~pYᘖN:7紻7ӊmE7^#;[u}36i-[%!8m5jk.5=u觨2Wr=AYCV#_ߥ>|Mʜ}R :技mO/G2DsB΄S?ҳk$Zj{5>Q̣%@.z_׵/CztG^o+f#=kXYS*:W ʬ{:_4U̒r qQ47rbtQ[UR|?d5x?$JB.~Q[5!Q̣%JWZz_׵/CT?k:U^6w^-旬UMjLg\Y[P%~T(;IB?d5rn%}j|PtV;eߴU~ɪQ^mU_VG?EQ̣N:#?$'Zj^I⇬h8҅%,{}:nUMKQ (zW2qm`uz} ?FGK:|~j3"ΤOBFNF1*%aw/S/jSբcx/ǺR'x!B!B!NatU?dFq/]s]Ƴ: Um  !3Н5\I/e5k~jF^]}.}׵/~JНr qQ}&[7r8]Ƈ|Q7T7Q̣2q1g5rG5U?GzF1I !W$wk. _T㧪EGZ'Vo5j SvM[Օg/R'F妃Ÿo:NGTWN(+}_ܢ^{Ejס||{]v׎7j9%gor: )\tt'y ? ՚C* ^qٛMݡ.>cZXX)Q>EԑO׬Wk֮W;p>NB)j%D4ۍ\(UКv^'y]Cޭq◢t'7`-:κR4oH-,Qw2/|~ܷwUv7p]ŜތVGq/'[`5r=^CVZ/ 3mZt}zHG9E-E=֫>7=7_|yuRwcW6f^qSIuo!E~#ն7?sDuv#qoޢxQ۾x~~{,9W=}teU:丣#@6ۮ: /;{|F7s$Rsz9[MeߴC&w>'?~@:vD}򷲱uwgsd58joV,䓀cdrծC<DzJ*|b5j%w?~ uvv'-/=R#U[wWgj>ڬv-z1uՎCTgg^c([T9rM!KCV#g"΀kRWTfu.{ڲqNv̫P'<lSwf ]'V}L: wwZxu#7gsu=ԝ9#ש7gs}Cuۍ*|Lgsw_=}8~\lξT9Xtοl?sJWnSywHߡsxG!_EC5j҅fo+5SKk^^ۍϦt\o\TWj&v5/ytg~l;1ejt}Q[P}Q|6Y jgz̿Jڅ7OS]z$?Mj93j#̛C[g_u?S u‰Hmٓ=kԩ85ЊxjGԑCO'Ք/;5u#CݻՕ"_o}ۭ6$3dٹ;R=[m{u#Qβ|\&;;v\NႛԱl<ɓ_rrW/vDߢʱN:żG~ ?뱖'F&~ݢxr˅瞢yNAY~lz{Dݼiz<"taz+Xoqu.P~yMy=jTT/Ymy)DoQWsߧl?Q~jL;mOIDAT-&YOϪf]_ܢ=7yW;˴#벱b-bQUΪ^$kȻ5<.c]RT?;?U4nqUl<. 7կ_N-=_}X: xXFGK:|~jzBkj n۽[m?kZ}b&XL:$d[jw|>?d5y}H7ukg%Ao;KjtUXZM[[|Q}|~nz'  ȑ#ŖYOmZؠrTm]ԆU6g7+Ɩ[ @&0)~:j:nR[ߑj¦l *=旲^I(CV#ף5 JZ_V 2*ڿvc㳩~1JlO:\o\ƎL7r8]tkgVGu/'ۧjz4Lg뇬F(u;P%k} o[|e핰U(Mȃ0}2gy|oߤٞ]l mP\g Ӝca}xXNbM'sQUwym{v9E?턛U5j;IBg5rFP>*JE jёj:ڦKWR=_{>tH~>`p ?+p2A^%ªvz?a$< P+>;,l & ynW.¹IXΟeߟo%(+WsXU~@mx]/3w3eߴU8VRǻ< x:J UM/żG~/]}#W{є0()k ?-J|_Ǿ>q1!<ޏ}uݎ=+I,8Xs=Xc?s}%5$D^I(CV#ף5 JYK07۽>{u yꗢGszCj:]}Je$~2tҹ kJ꟝=W~<k~W-Y(Lg}zH=Q0뵃/SӸ}{W+YUe,'W#ףN?d5r}К}?z  <,ƎvDכ#ew? ;sÿ~{)ؐaux䟊ɒVE(W$8m7ry |QoQXM[/jT_T3^$ɾrL $a.y 2t!9Kp,څR y*KwC >e0gújڄuz~ٖ!8vnγqgŇ\Ok[x=|6c-` ߣmB¾=x=ƞm?%Nqw{[|b>{H[o,Ctkq*L^ޭ񬎦2W7y^I⇬ ZO^r MaSfgט̥~B>y{,ȃLyS4tpXS^3?7l'8C[hQY}wkFfX"Vsj6+7ۆ CzrC| j|8hՉiO `ۓ| tfH>ۀpj 7i5\IS5yƉ~jz4F_TB*=Sex]n|}|)J|^oDŽ? `)7FsN߬k6l&7ey7}7}9F!tl %~_m3&WC7q̻/Ϭ^^OOhzI'Ybޣ5Bϯt7tM&|u V!ZM2瑗$scZos:tc{ڰwCה1{mjwX$.tgwt绢Wwjg1 ~ؚ7Z<70tV;9D/>Ic_Àa_gobw&|o@:{~+[Q9V3t{O( U>T-SƲm|3?g0t ?i}'I(F_ϷKho2n|QC~ZtE}iUto>^ןׯ|MPYU^p sӫv*Tn 8{#ʋ;a:μ Y{ɺVyg;\ ={׵yK[{ϟߌo߈c9sZyxz+T߇S74êg/'s\'ʟ=a.*m[K`~ϧv>ZHk4&sR~ϻTygK;uJ3t'Qxw\!\GʾSyԟs燬F1j vzHޟݥ~7o~;zϕw1{_Q١|'an^5ݼ774cjxnB$#ps+#ה>] `F#`@_;~;B՛. j]"|y}c_ښ= /jfMֿ߁;^OY||ߕw7}%{T_=ge\ޏFeXIs~+ Op ? ѺFz$!Ut^k5ջxWu5~p=T_s1{Vuܓt7 uprEf{Ρ мk}m)T 0/ߝw8C`vw\R8|^,_}z{{$$_X> 97!,k_q#׏bqN~fuVF1K:RBkj\տUwjzPm=_‘xC0_LnɍifR͢Imo8"VV-7P 0wQ|nBߕnX^ﱏު|sj|&kP~g8`hf_|/燬F1"%o;K56PXM[/jT_Mߚ 7ǟTW~Au{l[A}cCC??%dV熛Ԫ \}y 6/NM"fױM:YQ4}'m ufrt{4ٿ;a'p 4 7a;2'ߺn}ϟQ9j{{QKaPcw!D¥Xw%=h.1ܱk U]j[+ C~T|f5V>s3̨ZJ〟j:^zzzG?EQ̣Ng5rG)1/Kyzz_ t&}c*0^|کo ,fT=_F톓ܔ [m7fWWoVd7ߑj>膹ow/qc}s4<}޹N&|F<[U_u@]ѻ+~sϠa ka}25gԾU&;Ɍߕ;Aݱ{@͵k_ٿzMqs;G"tMݿ[9RlJB.~Q{5NCV#ף5 JZEoe{[uRz}Gom~)θxCL|p`2Vo_u@]wQ fM| JwQ̏cUl|zNi`uz} FGK:|j]ϭ-K/Q١ ʪ=jkM@_ifKZ9,n*@ `E25$`~YJ?v#w_/!]BʸE jёj:F~~ZYo$9&;  *Pγsr(>ngG@ "!/` U`(u{zOPWzbo9\GB`HwÕeM~yPf_`_QEPS .hŞ'Wq+aY׏7P^j#u3A@3/ ~VΝY_ïLMF]2p1duVF1K:RA5b~b?"]h2J?`/|V^7g񳰽s˼wa[}6UX P0,`~jHB>~UHCv#wPGU_Zt|j:b~V8_ I3  }ǻU.iC0JW[_ڝ+9ꟋF+(7A>:˵8]&Y?(u%n~*s5zG%t)jzVkw'|p#2gn"cJB8+ 2yquə\crWP9~\!>"+ R/VLxdX & u @lִ4q5u~oٹߞmϸ?k,`WC7q^mZ?XŽloᓫN8|isj :Z_Yʃ<3JB'{Vutŏ7Ԃ*EYE`ʽz{=Z!Ynܶ 7!]5ssr?d5y3YOB3zըOt>g5y^f/]f2>n{}uMn-=t'_|'/ KVGoUKj󨽤.YZ/Y`zf_|/燬F_зo%QZXM[/jT'r-NW%z&.uoTثTu>^<3K*ۼ*3~/?U*dvʸ7+fW:yR'x!B!B!tkq:^TnXvAꢫv>CmS՚u{vc~?רuY]nI/۩lXJy$ԃ;/:jԝ;5n'Ցzuk '-=jˎ5ޯ#ȡ.RWη{h(]Ͻ#5o{uUdmCu.tɥ|BUjF1K:|~jzd $KnVuz<"sazSkNT}!'7S'Ju];~~?uzc0:dۭO;y˰r]=\_7dVt=T;l_<[]=N~UNc{: yf"@6vU{zcSW|ƊӨJZEoe{[u=엢t_cj<oޢ^PW~X9nq^ӭYŽlo𝫑Q{IY<>&_[~o j" }~je) }ۍ~v ZO-{?~m->?d'QxT8Qoo+oz4s燬FO@w&kIhf^O㗲InjF^u/Ae}x^{>\e?;5d3>оN_:W7Q̣Ng5rG5\~ݙ~j$_~UHCv#wPGU?U-:Ңc5o1|Q};s0>?dVK\k'QOQeFoz4燬F;5bP-s5yƉ~ȊӨJZEoe{[u=엢t_cjF1?t3ד sEI:|~j5P%L3j{>g5e) }ۍ~v ZO-{?t_O~f(uA~ݙ'!y=_j'IBQ̣5{2׽>{]}ʾ)~+@wk cg|}.?: *`5y^ F(fC;3]/Vb$KCV#ׯ Ipn䎷JyꧪEGZw-o5A7s9G Ծ†9Ԇ'\m}%~n燬〟j:^zKy: )\t|(=ZQOQOGrIw d $TK\bޫqQs.zur{/E]g~.+\,o> @} ƚոb~tkgVGq/'|tCk?Oפ翪wԡl|}?rԦy7[˅ j|o_%bpz3 Ê`ltUmAddru}<>N_^w3mx,xs_Χ*I-9sm/>?Xt ?Ih}V_`зKdoAkёVtVtO{_|R~G=Q7TS?xNq>0a L8U *Z-r5瘣_$,ӝo>VI_gcvV9odbj 9>Ιy\`} ? Z|~:Ut/]r~q{oQ?FGK:|~jo_C竧|zߘJLJ *Zy5ϱnfS]K4֏k-]gGz?/rmb] X`2YOB4󺍟j'IBQ̣5{2׽>{]}ʾ)~Jgyjo, *:ᖄaIg2t!'rC;q_>*뫆zШߋ:z3nkMwPoB}go~E+1rܙU;Q߾IZY|C}S|3 ~z.smbg߻N=kiSIС_1}$_~UHCv#wPGU?U-:Ңc5o1|Q}|~wz_q p㛬` B9*:J梃F|'bmys =jjD[-CpXhgl6::{m,2m[ @8Gۆ5V} ?Jw/ ׼[@Go']wݥna2uoݭ3SRK/]r)>1PG?EN:żG+>LϬV,5ϣ-}}GUJV9nڴOø\̷v?=Wy`sxڸ~i\{wV}_Z l._dުoOo<ڹ } Cz@{3<8|V'!|߯N5O{ڽ_d $dkHN_xEkqFu ' Zr /H}M[B4]MQtp]VW5k^^~)_Wxe&/5QZ+i70N>iÆ<.s/](D_A]_Aoj,o2P_ïf :||މx&]VviW_}*5錛UŽlo+OCV#uw{zjoեyPb4țgvZCo<̍iVQ=m`;Gkh {X6|uU2<@TkϿS;qP{ּIyy۷i|qvpgS:c Bm bv$uI'rKOIe_Syp\%o;ޯ֢#hF~~{^~[J\s`5`nׂ.rnNǝbϙ %4=d^+gC0_G0_5'ݧqYoc{oQ?FGK:|~j+/UO}A=ғ|nr1am߼Nl`0iy1rMfg;X廥wW°yU \r8Aɯ!/%_[GdXyLͼnM~W~pmL/zgW\?~vt֍TIXF^u/e^q^kkWoǺ'gz* Mg}ZnNgk277>t`1BQ\SA=#N;C0g[mn{$sMK?}'@^WU?gz_s'H@暼}GBXy@~?u?~7zύR ;.T]&/OcD5J:U-ꮸ: *`5y^ FȬ[~U}TQ0?Lntu-(hI򜕐`q|!\ZpVyJtG<]ٛ^\J~GaE;/Qw]rwk:<\nзo%QOUXM[/jT_^ :{%bVͺYo^7\rzмrX ᚎ[ތzcj&, Nw2fMv'M?!\5ȳix ߑ>u#|9`} ??~*s5z#ף%t>g5yV|w/>Iр}c_nd|.nƋ;BUunv{Æp7^;}}0LL;!wTCvSf ~==S ,hzy y:묄xʾZa_L5O8Suͻ?i?Cpl%~hJ>P -Ň޷q]KQW|_TOݘiF!!aB%@} ƚ)0iWY=g{;߯3u`~e睩7s1߹$o>?d5К|1ZWz]VG_r#\^_Q[ouk/9<i*ɠkyWNo׼5El$o;ޯ֢#hF~~^ŸTOϼ8k?S4bVRvk̃.5Oy=S}-8|q8:3iE]C0_t_O7~m\oYz~Uk|*ǦI5r=^9CV#'h~˙e9y~U#'!yƛ{TXbt¥>=2k|/pBp̕Qz|BsX9 }7[?+ByjPWgEok ]mz^#sqf臬Fo.& }ۍv^B)UTHtFqrI3  }ǻU.iC0JW[_ڝ+9ꟋF+(7A>sܩ~^2td:F㥗.טO\hzI'Yb#5u{d+P_AP`۸s\K΄` sp !H϶5bP-%Q/KI0n%tHj:b~~Qx'N5$h!8gVRӽtɥʼnפֿEcSM/!DН~mI ]bרK%,׫;kuUMX S_oL; tQ7T7Q̣Ng5rGf-U?t X5N/Y\*$o;K(iQ߱_ը>NНkU~nQxK.5(২2W7r=^I}V;5bP-s5yƉjz4F҅uQ[^ָ{y]n~ݩX3wQ̏c~2nm(d{\Q7y|hMO~ݙ~Fm}/F1,%o;ޯ֢#hF;燬〟Y5JO%': UMjz4s燬FO@w&kIf^T$IH7t5y^f/]f>^|g׵W7c%N} A;2~O4_ҥgVGPeF1K:R|b>T 35b(;IB?d5rFx;/׏~ZtE}j:b~VJ x B!B!Bh꾆[%!8^R|^c>~*s5z#ף%t>g5yd $TK\bޫqQs.zur{/E%N} ƚa|c/?K}:z#ǎnJ6W1DL[[?:{y=;Wԟ燬FZU?K_UR)c|WJBCFx^BE{P^_T{EGZ'V[R=@w'A;uбz9]?j:@+/N4R_ڲe:-jcSO=M/h~mMݼb=APT:1hzIY\ ?Lͼ呜𮋎wuu.uC&SE}mQ'IBQ'S7\}..|N;4vZu 'Tqf5{2׽>{p}SBy}Qi5XMG[o5JO~龆-?3NTiwPOԳ~']wݥN>do~&@hjIY\ ?LPͼCwł~gnPNzEzW֍7z$45F^uxYFz]S}ʾ)~+@wk|z-_oʻ~A=zŧc~^}BMkmnY y+c96yڧ69Vwg{û.:_DiO{N;4ve]VF&{y]B(/֢#hF/+ꟾ6zCoh_PG*_ҕ[7^"3^Am?2+>+ī~vwd`+} n'K5$h薯r~T7\Lpeu ?ug0~Us¯7_)W~}F[|輢~9m=^Czf69j}{ӟ3Z[/\t(=ZS߾H=C竧xCPOX_Lx؁6)NA]Oٸ *]Րw<jejuW?嫺;O)Rv+a=jdcaL:$j9_nYIg5yGwuw*Ƶۼsxt^xYFz]S}ʾ)~?K=MKޱ=yWﱜߦM'/0j_OU35O4_#'~(^eF1K:R|}T}S ï!`K q9 l_JokQbICV#ׯ Ipn䎷Jy{EZw-o5Oj%xT;} nesnƊ}=jjD[-CpXhgl6::{m,2m[ /tUgVK\k̻3> )bt;ta}_HP'̬{&(B@/' jg,5;N;ڻbJ`gz~[5+_dުV;Y`pa(@sF d $TK\bޫqMQUR|.}++׵/ǗWez*,b; t'2mP9^j] `+k5*5w{hF1?tMד-|r5r=^ 7t5К|m}o+f*X"yPwmP8m`;Gkh {X6|u]wGw˚9*)95unN1R~Fm}/燬F_ зKhoj|H@5mmTtrqXB |톼8L}ǝbϙ !4=פu 9g;,W{lu; {yW>Fמ'6>E׶JCq<&_c2ϧuv; w(߫7F`f4~)Ѵ+ |jF^u/Ae|lZk{Rbps%*FGK:|~jV=sϴ٣QV[^k?&;=1V$'HOuKY^jF^u/Ae|lZk{V5j`f~i㾽x\.旬ު2(Q{I'\>Ө7u~??f_|/燬F_зo%JoEZw-o5'EM.&̃c1k v{ksyN}~NCP9\?;s0Ctkq:^T+꡷V?_?[IydhVK\k̻3> )btҥ>="_*~n$ t5u? .ٿ ]z9O('!|Q -Ň޷q]n|}|)*R $1-tn)iw n0 ۊnGvkfD| Jx]tw^;QqbtMtMoP7d~AJuZFGK:|j]ϭ~K-k+>  :NH϶`F~uҋDkd3$LV7ܰKp.umT7nTzz ^?N;=iO\O>9/]xE=%cuwM$Ab.!TeTH@5mmTtO7Y[' Ț?=W鮾ZPf=Swx u`~m/W?{vu߾@Oruu-qm뾽y$:ug@O7DC͒tgף$s>?d5yY4PIUn+;G]V,Ʒ@g'Qx>JU}9޹}*ǶFGK:|jt5j{%!Y\(*]gU/d|lu5.~)*@w~〟fj7ro*6鄛VGoUhzIgY\Qh 'ΤOBFm}'ICV_зo%JE-}u|Zt|b5omFq?&0)~:eS^([T9rM!KCV#g"NkRVޫqYbרKL_,{y]n7>6>JНr qQ;2y#4zWe:b~fuV.>Xb錋>=J]/T vͪ$`~YH?v#w|(\J'OUOíjtJ}q_jԿ.9{[՗Qrl+oz4F1@wz_oQ{%!\MQt5{ .^N򺆼[2/E%NoSLMF1?ֻe:Q)M/!Qg ~ݙtIhZI ]b~UJ^B$_WǧEZ'V6_;'Ikq*L~T.>Ƴ: Um utҥ!3t'y ?WuKYZ6nܨ6)|OxFTz.$8^B(/jSբcx/ǺRO}w@t{,hi : 8 ஫?oҁlp h=OW/x *:g~^*^veoP:m|5Cp  !3T}S ï!`K q9 l_J~$l/*ƪt*4zֳU ]~ķ'_奔]}I[aIgQ/tuẨz]~a?D]遟)Kƻ^v7!5/=l{f[uplײuojF7̶Tǫm>Vs2)z;Ѷ~k j7QqG>WC !ye:b~jL5wN4 䑙podY?3Am?E:tX89 Rqxڸ~i\{wV}7kޭcl4=vnlYߡ5O_y?~uy<#=S:bܹWowe:y\5KI\ߵPtV~~w*+gV`7e&{S۷~#_w텺h X_ߨ՘o2SvMY}gGϿQ]}bk57OT'x!oЃhtM+K}V#{}`7nt;2scZw7p)`on7 7BV a+w;'s+߯b;5oҵxo`ouqvpgS:c BI^O¶& l|T_QӿQl 3< tE5 JZ_lu׵W7/E]kyzL8,!-r/{q8taj7yv=\Jh~3՛sHvw=<-^~{ϟcM |XNwЂ3OsW}2~O4w:/T㏫_jў5y :?U'rCW(ъK3I qXzMn_0&7aZojOJxX߄|c' v;ՎbE_ jW5tsq 'oJ~kk>h{+IjT_pgg6=^﨟hm]OٚTXj:XW|%3 Mg}ZnNgk277 ?8߶z_Oge_{[X`p3pmyWP|dw)r`৏5,_k \9HH~+ mq lQ}խ3!;JR~IgY<~~jϬ0c.׵fzi?X%dH=GAϳﰅ^}kߋz  E97 ¥gyDG<ْxQ?GY~(Ek׮U]!a(tp]z{y]f /`ػln ܨ+DgM74 y9N7-LXjA]w2fMv'M?!\5ȳix ߑ>u#|9`Qg:b>P}]W)~(E 錋!33>TW|w/i`7FvƾoyS/Ty&7ݹx8hc|Mر{'ggz y*پЌCowO!9K9o}wCއ:+!^5zЏ uҵ஫X! M`,!UPtV~~Eԍ y!!aB%@7ew]ÿ{XFԫFmΤ?L`x7k 5҅%-'Շ◢o_?)Wgy|V*Pγsry)gϢY>G8mȣk ??vߨ.?CL4_S.DPeFoܥ;E9xN># ?>Q{Zh~˙e9y~U#tI6m}#ݝ'uHc% o$Y\*%a7oה?d'FtͥƼUjh8I JH t8gwY8W(VUgIV`X!x9rܜ-ߕmټVa5+C@"y $TK\bޫqYbMQtp]z{y]n!*R $Ht`TKnh\* 4!s\H-/{Ε~vEuՎ Gz_r qQ47rL[/YUe,'W#ףe:\?d5r}}Ƈ*R ph( r|(pm9.%gB0sɁ_A98scxg HP۬Q" }~jR .֢ck5o1?ը'_j!WSי&HMB/ 0ޭihk:XA]5,? |!9s=bqX.kY|QvM[%XICRWtɥ([T9\e;~xh߿Lg\Ϭʾ)>X\s3"ΤOByF1I>W$8K(_TXtr~9AMZV `D?ըטQ\hzIY<;k$Zj^~jz4FҥuQ[^ָ{y]n!*@wkf~^-旬^^O7jz4L뇬F[5C?36jHBáQ/KI0t^B'{{ZO-;U?d̪Q^R`u-'hzIgY\;kI|$!Q̣5{B׽zu=>{]}ʾ)~)*@w;kd{#׏KR((Q{ι#Ij^b$KCU- }ۍv^B)UTXtr~9AMZSzõ;]={+ZIco|> lqj:^zKy: )\t(~;̻䦑}?uYNJ2 dYVjFGk\K-Ň޷q]n|}|R'x!B!B!Pu2IENDB`onedrive-2.5.5/docs/images/online_shared_file_link.png000066400000000000000000001016511476564400300231040ustar00rootroot00000000000000PNG  IHDR-HۅsRGBgAMA a pHYsod>IDATx^ y^۬miIkH1J vu/Zi,`"e 6ɪmFJqubpjeT6^@FŠg癙3Ws>yޕ| ^s.9ғ!({ኹOr/zʳ̕+%L֓,Zծ~x.ubE:ubEƧ'*1hY+Tf袺Gu_$E:/V~H܈ %#b^\U;y$uN]6yF e{{E.uQ(l͗tQ(l#JE:'6̏TdyCo[oI8V88nCx܏#W= ?׋#-j${mUCvQU{,]3j-۸(QQ@S%w"Q}ɂ}Hg4{% \AYT|]^QXAtΨ%U .j޾%/fz>4 洴#⬇rCGUuQzZ1{=TP^Fc<7ވloOȥ. emr.ekܧtFeO\|O|]xլ]]e]gqW=^},Esv{Vz;U=1jܧBlJJ6JO2(XU]Hg=r.՗ r.I|PFSՑLx=kͺ(T7#$*;Bylp;1ɉg9ronWʱezv*y)}bE::keUro+)nmE 7_E:lc͗y2K.Y׳]=ko[/w{S%v( UG஋R';k_:^w/y^x+W=|}#-WKr. elcJNmܧBy.#畵ΡX%wIz-kۃ/wPFa%bE:,]hvEvQ۬#4d}?epQ݌GeCpQ(/r#-snGrCG5"O}Urm.eyeM] R;kOǍ.rcȕr 55YC岍7_E:/x ԣw~Q6z.;UXI}U~˥;퉎<;'в|U;''\v^S=ƴR=RuumL=;Y{z!t|EW9:-;-,oPa\-}HgCvQ܋ћt΍̕X.]W9}KJ<[U'AZ^.CӕċO2j$gPF#좶!45}?/B-ίfP9-8:WxD]8. C#鼀*yq[Hgm"QXַtFeOTvQ(wru]_?.ωWHIz}ٳPǗūnΉ9YmsZ"6ErQ(WRz,]3(Xu.]3 Hv(,9؇rSmDZ@؋ϾϜ4Y6-FȧEN,?hכN}'i)Ւ{׹E:/l#JGoP'[R2*6|K²5_E˞4wvz]$˳g,/{޶ܓpWIS<=Tzq^v>E/Gzힷ|)ųl֞zsih55iZOmZS\s*y.j6dotFe?ܥtFa/w"QXr>;tQ(ST evQیА-y\T7QY(]H#J;qZ.w1Y'#->sWg~{(S:'Zs]wnn$rin2~dNK%O$u.uQz_Y+T6*,sT궱s٩'rúHT.YnOdԽQ]>A哬_a=9iϖS)kTG.>#dz=Qݻz=Ώsa|̽<+} i}5]g],ϙkҦe2-o9O~|ļɮ+2Sǵk|LS25ww7d^̓;?9n.ki~9Ys֯e9({QU}()CqY{>s*rnBܯ&.ɶS-kmLtQ(JNy.-B!REpQ(*ttQ(Z9-zbniԿ7' C{_6׽EߎojTyiyf/fO_|u}\󫯏~үy>A;ټWֹ\7_kT_ǷWtQ(/lIGoP'k. eTmUm\ʨ(X.]ʨ쉈/wP^'[_?-ωe,:I=TLJmǻ\pn/Cj[o'FZFt{F>(M˨)xtqbn9.eǮ'^2ene8e2ͶKz7ey uR=>QFmdѲevuE:u1wPF한4s. e4<%MyH.jB+5-f4U=BoԹ#d=29"?\YX2Y~, eTm˫#et]Q6ܺU.Dn?Yyg_=QqSzGuXOyzr%{}FuȐ5z#uZQvԓfՠs{*ԇ}NσkʎAA9oY/;ٌ\U#-,ddWeyٜ?~|9}3Oo^/;ݮesv{Vz}lNVZun&;7e,l㨋]3jImyE>좶!D]_$Eu3E:[HܜlGrU򺭹7^dΌ/Z{Hn^U杻mLv;->V>9-+)ќ+rٺ(=*y.;QU}({Qh$+wvNE_1u[sI.Zծn?ܥBùK+9->]hxJϢ^v~]N=uy?#-WK,]GS%Eus'6JOr#%s)?kgvNE_mȗuQ0l@ղb]$uw+9؆s.Wr[}(G!nF!4폇ࢺOU'pu"#C뢪ybz^E*ynBޞ,+(QA]Hgm"QXDJE:w/Lu닗'9Y(˺Փpݎ#'kux|ArN1'*"f9jIE@qT%EmWцX)PF%# 6|Be?ܥtFa/wPF?)-}(\TCrQ(#вO\T7|n.ҹoe=|Ni.wnZ1I.޲V^1Y%RvVY(nr.ekUߓCu,rn\Y=T×tD7=9 >ȋrѬ˙َ#%]'k>|z=_HՊO6fCvQ(whYHHDWWs;/wIzE˖Bl3ȗtΨ`]Hg^Ij1PFWrtQ݌Bh>jBOU'pQ(#Zx<"B^\Uf%u (x"̣77Oqn\PFƛ/w"QXַtFa* j())K޶^Ie8UG^Eo]3pB!Euࢺ Gewp}+i)sWv] RUBdGoJǚEƛ/w"OG<E2m\vꉫ\.$˥;íKzGuVOyzrR-ekPGUQh=[fGNTV:u \E|źsp_XA&Qݻz5s.di!({Q1y,]s%%s%KvN*KNܭ'NmUu{]ʕ\O+9 ->]h:e. eB˩ǃ!Hg4?U=E:kG6{}UgѼs.rg񼹅Ǫ'.doܥtFEF/=;n]簴s,G7w{ѷ'wIi1#%E/fN.g;ZlYm<ԫ3y1?ϫΏvHՒܧB#۸좶RE܋B#u}^s*r}ۚ[OvѢevu.]3*\}O+9,>]hJ.zy. eBTa. e4;U=E:ku>e(/%/7UGfec\U}~ysZj>] dIU]6{mr.m r.%*BeUr22thJ.jy. eBݨW\h~*;?tnlNeKRsTvlPW+>]Թ6*;plBղ򲌊S_E:JN"O2^UGEBuɳpQ݌S.\dNx]mp]A^-ɱrC6yF e{{E.uQ(l͗tΨ(x.]3 ˞r.ҹ'ex62 zCYƇEaHnGguqϼEO|q|v.bn/b*և1jIE:7mUCvQUU}(GȪ"_nPFIǗtΨ({tΨ'OWrQtQ(#ZKf4;_E:i(wNHO>e} 4iY24,84e2%i=aiKhCgsZ?Ж✖:َ%-$@s&sZj*dw{!HKpb8tAqNs\ڿFZN4-+&sZsTN>i ]ɜ4-+sZ#(Y;sZ@4iɜ:Mˌ#o5.Yo֯W_?e0e  ۸d#K5gmNn$yFS6Ra9ɩӴ7-gָ<`}YdsŧWeniZsm 0OdۍcYv_wrRh#qd·MϞ׶>rW.j>P |0cF]ݻ)DҜ^>?grS=l>7Yu6sԺ³}o3d]9껒]-lNhsϮ6'9oN(=iZ eMӸtNO=Ϛi6<eG6Wֻl.n.>dsΊgGGW玚ݛωftZK3#ݮҦegw{9''_{itHodG_;{tͻuMKs~"v ;m̦sM˶!7G\k{e漵盭+z 沟lmY7Qن9[v씲eOzIbio+CmZ>k.8Ki:u9c ;.3kϺ\1QS@'7AtJMi I\yۦ9\vfך~(ɔn宏0&#-a9-7-Y 꽟1!ݴ\5oœS~|sN;7_l^iG-5G\e߼s^reg'_{ٗO'iXn?ذ|n\~|ҍ{6hK?#_m߲ά=@Z(8lW{s[֩4߼>PHoX Mў0%zmy1sM;1߽s8żuvG^é2UQsq.c'ė?rIt~,:b+}KGORmW߿vfmW['W^pSfvxi n_&mn2'1@x$Ss`NsEt%r¤ѳgٔN5`vOIU\;:/&퇲"#nO2J?ueGtζo ɇ~15am T6(ޏ L,-mڱ՜e; V M9-SѴiy yyts\$$c媭ffuf%G>K$}o7Wۯu9ңםϒS9Q03;='΃qmҞ ?nvlH~OGqR *۸-!' rQQ>~g{6[#8z>=Kڴ5|˖tI^|GBľ.~v},ꧯwh\͛S@t.wAv7D%:_;f]Wfy=<}É={怳]$M˓?>w\k֥JѶ۴EѾ1w{7޻xx[w'`3[m{[m:qtyol5w?|wh;9jEmuW}וvgq?x|M^;u9jswr9-;eQ4W؋x~rp(=ȍH$3[N0/ɯ/O#GғiiɖWmRҰXժlxX?QdÒ{Qe?͝}w}^~kLP }l]ϙ?1Gz}T|ȚҰlH.sZ޽󶟌C< hnͫQ6mFi7ܶ_N~=ivpc}%0lKGn6Wvڢwy~2:NFJ2~ w.C{~}[yQ]o9bzL9yrK5oG_W_:%~sZv5-6Jȷws/;Ϝ0H`Pvx}䛖MIKhX,U|i?#N:8>L~fs)٬ի<_EfѰlH\"!~x$KzuQQuI5m!]_MxNO -l_7\ṃuWfsfjaIh,{湼۴tq%}g['n"mEk21FooBimC¤!:&@`<;24?Ӵi-Zv%v0 گ 'e:o7+K\z 4-->,b;4C΋}ɧFT'j)#OK>>sN~sus+վ`<_ ?t2ږtд{DUy1Ke=Wun[H.ϹiN (6~~?HD9sI [JY?ϟ+ږgiY\RitGzFoG :XzxϷNF+}L߲& 7-ۘ3b;n(y0\#-,q‚5--'CIwoN潹fu2MӦeTGto1h*t״EO8J=q0]!#Q̾~Yצttwf[wWx>1#ݮF1֝}9Yv)9Q׹mhtmKo+ڶ4lx %m@k䅦̇J}Ƥߞ̍<ݷm2[=_EsonL9cMܜ4CM˺ZMC;}o3Wq*7=ts#`;弫Ԝ*;mm精dsߖ&| fw_K?h_9X< sZ6q6-#4Ns/n2;wLt6Դ'_njkbu; MK_'盭ٷ_\Nn_srEf;7K7D'ǹǨ\7~+ӓv6&6W K s}է+_?7W|Wy]&9{?;;5}9or K;1PӲV2"ޞQ`ޓnS欇"S8"ݵ\vvM?KG0\nm|ۧ|`ki ]0(Ԝzj%tE2ҲiZ@Wl4-+R{6eԹiZ@W0% <|0B!B!B2òNf%tE~NˆNbŎ-%tE9-ee*jNrL"?d2i ]1i雫Ӵh6MK 55eL"?eCi ]bGK Nڜ2$ӴPsZei b27WeUc}}{z/QmNK-ijM˧?RoL>=`jN#j.,|NlZz{Jc+ؾ_=Y*=ٹsg%ٳ' l13_)o9+̱GH?Y*]tYYYTk֬1֭ .07nu #֬6>Ʀo_ˤ^&*swc嗽2eiD~y^giZK%p/6|.Fд+4-1}=NӁLw={dѲcs={w&IQ>vN5gM)'aNˆ"/?cS;SW<3-W3kn'ޞϛm6M*̾vfdm_. DבǗ^mG2[p_賯7z]E>du'8?cMNjG_:Uږ #?״-\e_J'_r/dcܯ;#S΅ʨ|49gP/pr_:K_rAr8Or(bҰPw?{tr_HM}HqFs0[n'4-d-_b6~rotP-٦ٮ,;.Ƅi/^׳mso{ٲe8Sߖcx=v>iOX~ijMD_NL~7,r{/oQےerOzzHLh`7*sMBCN7>ݝN_<6PhI30yYs0}6sϛ?yP?Q4,ǮJrr9B"=w/ғy@.F.h~{]@8f'!}]Rv^P%}>ѽvy&c 9^״<дFӴT?(dt%y{Y)M6Gt{aۤ asύZl[7:ۡ{l^-v؆פϡ1Tngx[v?M=k`QsZY>ʦbݿ0]-ʵ/2׾|̃Dϝi~DN2ۻojEFNpZ!nyK~nbx_Zu|Ë/Kz^k)m_ {R_P?;ŅH/& uq^PLMB3>sј*LJ#"կ~Dnݫ)/~9U̚5k!FѴ#)kX|Uiv9uO4;w,hϞ=bcc OCR--p= -6m6˽9ێg>ɺX֓-M=.߆׷:rޙzv){T\e~~ >lm0sjk_o=|0J{soZfM7 7r-6u3,mqðn?2~,yms>twPp(HINE}rW{}- 汏m\s&tg*5^"QeWb^sz:6?7n]L?i>Efo<|*g FZOi(n3f;gj %]1Yq6<=U^޴f׍Wc|ķl uM},?9-e$Όpfs4|ji/K߾svgֿ|ũA2"n]W69EXcוҴ55qx'?Zv&H #(^Hҋ{1턾ߦe(q_ko ~P|ЯI~P;ϩ֟Y_pyewqW\zmBSxP_ݠHޟeo;*,0\?<o>w0^kP >bx`/>  ~D_P$gxa\E\O/Xy3z7nQϩ^z /<3%7=׾¼6o9ms_7|3_:RC5"{pܰC4-Z@Ӳ\gyf4| iл.yXx;?WcӴ2rF|Ҝ?]3MKд\1{w4?$;<#o>=馛Cȏiෟ2glSoKhZaLM˺#'N;>_~yڹs9tP皗4-ɜ2r2}NӲ9n#mcSiJGi Pۄ͸ FZY&BlO<ќuYom9Uɭޚ.Üs(ʌ`NK"wDei ]Ӳ}NӴ`NKԹiZ@W<œ0Si)di "0e5i ]HKi "?RY}M?6 DpN*s\ig_mRه7' ={;wolܸ1֥^j֭[Ӛ5kJevi^pǶei)Q6-=izwc9#Kࡇ/ۈ K㎸ix3߅`W=hI߾}+sR͛sܦnrVӾ̱Gi_n&\tE ;^%O1PPUTu}8o=hJaN&>Ʀ{9 g 5-6̼ ͛l(B4-e׍ym/-NwiįU60@wдӴ|ؐ7l/{Pqpzt{|ٰ~KD󑖢(rW:S5n'۰ :.i /bmڅԴ,4>4g>f/{alsmͥ~gٲm$tQX6Ӳ!`jBM2y.2ʦ}[#4-g:&e~t[MK(mIID~QYGٴ23oBM2I__'#|_Ŕe+m hs&Db|_ y9ceI=w31ؔޞn+q.v{:cgcEaNP(_~S,z߹ hZNH{vwT\w\8/z4&9m}u}#_:A7U;??]z 8Ef͋O1\ iul41FǡyhiA_?(羹$cHq.kAx܈Hܶm'7f㦹]>>HHlZ>/Ҍ CkZ$g46ַ_齏Ow\8":Vٿtn5ӭޚŚH^lyK.[}<6-/vqgmBw}ݯ:S ˺=#-_v?>>{)kN0?ez}G)?QMʈ'~4-䎃wlqcº.@0eCsv^Mx__ܺw?0eG sI/}i2 3r=H5(}Z0? Ŕ{'J/fL.5-ƀ|~} JiV/Ǩ79+_J:}jMKLڴ,kDɇFGaǀE8P6Svl_^ 9n =zlZ~霙 UFZ?fN8}9۰'LWwqG~vdkMl!`V^&$47-&{}>>%3A;~^Q3xZsʋטTMK5J/}Lg4hZN?ʦݯMr|=^Pدj `_M/rrj}O2j"d/ Ǜ޴gCh+2+47tw=e;6~`>*Դ GKce00lBqY2rˍrVrܜҴmC9-<ʦHb>>s4-}K :*%@3^9_wL]%[;sFbr1/o/\I}Kt=fNH./82 ]Brj_xXo"}H؇MgeHMK `HoZʶi]ܸEmkqOӲ!TMiY&i 0V|0hZ[[rET\yC.ESN/N;4Pe_32ғ<ʑ^`=){n MKXDFٴo #:p4mRGѴ2ۆ`4-`HԘRṬlZ~DzϹ)4-`~!d}S.bu܋1#z|[ϥsE54i< aeWдFѴ}M=?Y?x!3 w}U6R94-#zSFCD5)#ru\ZT춐;3߱]{^ }RgFٴ"nf/)"N^`y/\p5jZ&d @|e4864-Q7-2ajӲ%jU}R?r`Bq[˲ƢNۡ}{6ܺ60Ҳ!\G2={ҟҋ@DteDp/G7~&nN;4o/{yk^/:D`~Qw?5믽м,d_,=jſ_HQ hд`[aAoyMI\7qPmÅmc̑sZ:>ʦݯEOԛsa_7_7/=o{/2YX./֦_x[ 9iG5MoZFϳ| ShBі'.:O677?xw>/y9K/oN?5>c 5-jfQrҦeXY džP\rc74-et!e9fp&vZWOXXdѴX.^9_wL]%[;sFbr1/o/\I}Kt=fNH./82 m+ؚ}H؇MgeHMK `HoZʶi]ܸEmkqOsZzd i%x1GfMj|MKsZyMϽjbGG`,i9_Ѵ.h>(ʣlZ+%4-z"n+0KhZW4- 9v3vMKXfhZq[.k5z8e(GbNKcMXkv͏<>k7дe6ͤfygyر5\cqfΝٳ' FZbnYt뭷? n)>v oz'x7$YЀ?i 8GzKpFZFTziZ@WTR4-+EqNK#1% FZ>vQiZ@W0EqNH9,i)*QYi ]Q}NK'Ӵ`Nˊ>|!B!B!4ybNKAQ}NK(Ӵ`% xehJYጴ,ӴٲJTRs$P_ VdsZ8I$i9E9И0Lih0Lie{l7􉏯mW̿  UÄW̑#ZHq[VzѴDE xehʲ,N2mbQiZ3RT6R׳L2Sۦu۰6.Y1_}qI0Nz!sX7tٸqc6ogϞg/Ϲi&o{I`2ML]4-qoש$O㒦%* `yF4%fܥ^j֭[g֬YcVVVZ㏏ .46Iismk o_t'z_g_z ^0y/|/RSXmz>oK<_T?ݹb>+hI<\tE& FVڵ"?p[:6E˷/gMk]|A(=a,0֯7[KT7;677l<]6>߾qW64--۴\y-w-DdJޟnjvD(-Tۃ-E|`vbGK欔\clZ4KΚVetZe nZ0Eۖ M5A4--۴\іX _ {s?K@8pFZ16-}M*۸i_i9]4-fZv/ M:7'9[Zv9;'nvi;n߁y{̅^묳2yk[sI'?ջ:WheEk"2l31nQDk1U?cKz{{-;fqGoyr#טi&W/? 뒚ﱴj>6R5ܦeS+ו7su&ulvYW$/=oݼ;?oإ*M˨{N|O |H/y|a.k x/?sMk۵=%F~m_!םuYJ_&g RMxYMtl{=#k;7Ӓn{ڶ}d;HH+oK}9.EcoZ{+ۊ7WwX1I]+,q9gnm"`4ܬ~ne}J[4g~׊9sJFZW'e{,!H>-_c`+iy}1s`ѹ~ץ֍yB~_S}m#(E麅Pu0V&bM4":\lE"^ۋhG7]&\_,.:# Dbs+^?Dc4-G4-->~K_S[=g~_&kTw@?d>|Ǐ2}-˛qέsZFyM >b^1}|G+Vh?~~]ci?imPlm6!MmZzG VlvԑG@Jl4-a<4 "-Gzqw_Nm]e ܥ FDiחA}ݟwQk.&׫я]&qsx;hZ.hZZǮ͠B3*^)uu{tھ÷7-m÷-HmC?Ů9FZ6ۈ<b~v\V%i\>dRs-۸œSi_:nm CoQ>mv~PQ˭٦'Uϥoen9-\3}L[iZ(J|)۔ x}xu=3Xf̿ZsZǿYñп$EKn)k{)kN05uPr/mt뭷kKouߎ\c)הGSLolev?&Wu2ܘ*wlz{dŎ Y)޴z䯮Xt.ys+斛Ẇ~/[LΑ:MKqge"JuulϽai% TnxLו=GSGZLIM5y\-U&{MkߓkM֢e4-avQ?6D bð"Q?^9\9L{R+\0f܅Yuݿ=H!\5G=GJGIvs=78o馸avϝfNKV\py_7^r2/kwsJ( _jhQmZH"L;}B>W8Qj*0q9-d~[nV̚wP_o@C≦eߤsFQe!i9Ees\ش#+Gߜ.\1ﻵ8e?xżʤq)hZq MMK #-;VݿkDhҰ^hhZ.hZ@].d6-9eÊHiZ"≦%ԅMo36/3[F!<]7-ݿ7!~:K U+vdhJeN2M7#G5F>bvn8ܦi)<;w6 o馸Vz(}3L^146iZ@]e/~1mYU7x<\u캾B-q66l3m^y啓F,dԩ[oM:ji)#͵c{5WZ1GB-i~7}e@VO=b>(Jonysic 9.E8h!! .|Y]]MƃфPWQ>Hen<!B!B!T>v TuCrQܧtFA6.y:\۸H1PIǬ%K(]݊tSE,jZL>˩{սEsl5r^Eurђիc9uwhe珡*uQ.Gϡɺ,E܉_7Pnf6|[U&.ɶ U-k*./cm6r]ʨ[KtF*"WǷr^Tye/;«zxM@xP*eW}Hg)Y&x.}̲'_n"(ېE:Q`ڵtbnPn]U6ҤKEoHg#xӜwQuǗozu=sv{Ym!t"Ǭ.%ØrbWuB*36,onfQ(@Qm\eudnP]qY{^1]18Vz_ҫ|BڽT1Oe-~-m\3ݻHgT_ACrQܵBE:*_"Ǭs.yT~~/?dmϷ9-T7Ems#EToE:Ezb_Y{^휌mS*-kMf/qcmFu"({lPbn"(|*. 1*4"tF2⪘6|EuagJB6.y*;t"Ǭ.\HKQjIm֞E,PEͣ %E:PQm\dsUr<Ɯ/Wd`iQPmΗ'r{]zx.ZobFFvBRȞ6. eTmU}(Q< UE:/ʏ^voE:w}ezOCu:ٜ!@$EmW7wPFAƓ/qcm@Um|̲,_,{06.yJNjtbn"Ǩ'ݸMn3닓7'#K*Ǘ٫EU닚*;>t"Ǥ.\eׇ1Ւܧܒ:WvQY+T:nLv{USܮ[+y]g_̽%'rźHivda.S\/9mέKzź1.|H u{w뒽^.$GRhuGTڭKzźHO-a]FDĊ WdD^zGRQ/EurIݩ{]fQ|``wivf}2ߥY%zJ(-E:M3rQ_Ez>׿^_9g.e*;>u"$}~:"]HKQZ1{=T/q (xц֚ReuE:QQm\ew]He[};{;qڧEY=16n?}q{^=۾+=lOG岍*O2Z,E:LzڏqγW_d9oɲ(_{xlNKQMAxP*eW}Hg)Y&x6.y]eVm]c=xruǭ'*{}Uɉ9=wG.y*;?j"ǦaiZ;Hn^UZz{FeܙG7p{ZY#mr<\dnP=ӢdzF/wvX6|':EUd{"f9ecxйKsE:lǗgPFU9 錪lօt9t"ǧRg,ҹv9-Cy.;QK>]s/>X+kիї۸mJYvѢeuیE:Am\eE:QIL1qcT'm\U6tF2tm*/ONrXYm_?p(SUE:YeW]HEW Ւ{۬=dyYl㱋ܛGJ(ۓ5rtFqΗ۸H16|KlΗy2BP/e#)EnuNDkJl]TONNݑms&ˊ [e#wt6}>]3ZlE:Le.\<@MH=T6q}~: y Sy.+)E *Y]eYE:Q`ڵtn]w"Ǩ&.Zn.J%k3OFNsn5x~_"i}9/}|(Ozqftpc=_:2ΥsT}eP1Enц5޷V-y߅S걜WQݻ\u}~Wק?=5\T7w}HJJZ,їx~]T>|$&T|L%_,{P.}̒OHun"ǨO۸H1ldI2%#Bi٦oHt`zCϙ'P=*{v",}| H1| rFZՊzt^c<36 ov֚ReuE:Qq׵tlΗ۸Hegۨ%k^O>,1=16n?}1zrms"ElorYgP[F8BGIE:qlE:Le.\s{].;?r3IveٜT%EuW/RE:BrMƓ/qcmDu" ۠6.yO_n"Ǡ$۸H1(96.y #LHe/}E>1U5k^$'#~꿿EŜݞG U <&u"]ÞӲKMrI]B&36,oH66>fF/WdnP=ӢdzF/q;bЗQ׵':EUd{"xW9ecS6;fQȨ,w"g/-խ>O12qYr9=ĬVe.\󘥏OX<&yX1RTZcou,/m<63hC e{{|J"G(ۘeuܥYQܓz(I)Z=T/w۸Nȗ۸ԹJֻIt1g#g{;˙t} i"ms.[eG.e*vU/dݝio[_1?eB+v_XIENDB`onedrive-2.5.5/docs/images/personal-files-on-demand.png000066400000000000000000000424061476564400300230430ustar00rootroot00000000000000PNG  IHDR܈sRGBgAMA a pHYsttfxDIDATx^ݫOv{fDB"bH0߅٘z1`҄F a`8h@MH4d53І8'7?k˪UUye]{>gZ֪zOgǾBhh4m|h4ڔ h)MhS6FѦl7FMِoF!46eCi4mʆ|h4ڔ h)=c?K ;i??rChk)'ƛm~}0Fb sj C!s=G~{?jҶ͗>o]_g0IMpv s8{\g﯆[[Dт@o+C+mwgo;E?Ħ%)o|Kh[,3@=n.?Gi3jLOCQ۴:Oh^[8^~mֶ,"Ð8{ |&r=h:v|ȯ~oǨ?ڟԿ?_H t*1b!&~y?m|gk9E^CeR/iQ"3QQeK-̐:w"vܦmv^?'~~p*x+1ȐjShQ"oV9f*e d,xZ~5$yjQX+ZԱ_[災E"%,4F;zKm%;/Ǐ?o?'Z sEUbZ2F-+TKMUci仮fov]rEfLDюގ#?g~O۶[C*:jl e+FNLz݂YR6N!=)̗oMȷ+h㶤]m/O>'ۿwc+?{!l| i;6+cփw着K/G/uk]LD4툭(v?ʟ~o~?!+5_˷/ KRzp$q K[Ck vBw!D4X{+.o_ΟݵcmW'Ft v}NhTINfQ`ᛣ-6ʷ6M XЈi%O/ib;\Vq'hGi+2}?IC_4zݡ|n!`eIz-x2`2hb;4s9uv]Fh[;Iӿnhci4v h)MhS6FѦl7FMِoF!46eCi4mʆ|h4ڔ h)MhS6FѦl7FMٲ|L 0%7 S|L 0%7 S&߿>-Η-ф p |@"?p}Ny=Yt>S[g[ {|S^t-t3ݭ@)/rg|:V^[o7 vzn?&SS^t-t3ݭ|}U͚B-S'X?|r<-v>_<=h7ndgͽS Й!>vT)ƫWݦ_|_8}d'sa!w7Z =b"[TQfw^k?βWN}{*۵$ؚeJi^c U@b}G[LdK%J{Bz>pkk+|DJ ER.=8g-UqѮGVr]<ҙM\Ύ9 se*Kv2Kj x57+Vh4Y[?x& om`K!HlK vU);!&H?+M:ޑmil{-΢VVgREU4A> faIǽ#[湉G?_܇?z5o2ǧ(e\ʌ ˊSZ]$,P;:9jdJr«/:?%K͝fܬ,MC' 6鳰kHt#b9˳u,΁GFsQJڞ3|R648G6p}3cf'vVgs+:qǥƹ>px=;%>%t͎E͘zgM׎$^Zஆll鳰|NDl}dgq%pKUΪ"B&Vh;;Y"ŌN/\T0Y9\ChxTΆW NKTu2mm!+>[-~@;],Ӳg˄mxҖ jveoMhJ=YttLqdH9Lw+a/pE"Og[ {|S^t-t3ݭ@)/rg|:V^|Y=P`Jo)A`Jo)A`Jo)A=E&q|~K+ST$9|x/ihUEݣnyꃯ(FLU8չVnbo'ǖ$boL,S"+l -Vl#=2 Afdv1i˷.w:NX=ѡ68mNZUT%6ن; TEd.8Bb)C} I1| 0%7 S|L 0%7 S|L 0%7 S2|rOr2|gko8{o)Aa n7Ϯ=Q{P.Dé2ߌQ[{}?mV8za'Ŗw =J廖xऻ1,߬[&UNtdjx8 r#ؾGm1>j=;brWxD!|HY}=>vݛ}J'Sۊ3SLL ǮRYMT1&:b\ۜ:]3W3n*5O kJZр7o[F*K8׸ZAB!4)&4vP'OJ݌:Gvl^UU܆ {^}~^;S4-S+\۷=k{ i![` +j%1 t8PjeUsZ+R9e^ox͆bn,DJЉi/%Yd3 2E;SL$O:Ntdcj`IZkA)1Uk"Ӱ5#7Ŗ  k\|hv@NYA``-c6t*n@VvHZ!6:Y eՖSY]1 vps[XpSo@-)a᳎h-Od4% RcVt6qLU:8;[C)ڱL]߄[%&w%he~YDZZv^e`%H VFRV?$D)vlYR˂lJ5ڱ&pn9B i0oWŖ+vUSc#!!āV.N2GLpmu&YR!^dϒ+ձkX[Gd4+ŷ֍xg M1Čl2 xA K<#iD`9ENv ɮ#5,tr^@f[`Cl7-L+zjqe?W7Yq썫^d/2?7@"ɶO7[UKLujWIiDXwT1'ǎӽg޵2XaKxPOpO l*;ɱ:}LЭJt6xXls5Me;)e {acs7"JlBm.dRdd 19ˤجskgNd"єLs7E1)%7+"hND@aZUPox3^48_yO`Y,'ڍ*;Ѷ.^p%7Sr~}2N|[>#MOv,be3j:Ɏ9'lH)f.v__h\50XGkVUU;`We4tNmenl 0WZYR-j|M{Thqy:̟,5 5KkvX̌KMXdW7uKJQ?f&ӝ Lonn?ݻ ;loI6`7kTVrW u^2[VU/[hw*ǔZy4@ IE6,] J 6eXRaD -OêJ5.lZBSđ,;MmC]HqOL&www/^Pw;ɷl%oj6N; c /RFbvilH&E ]UUmrp?}HN-bu/a)@m f_ D(eHį`),,VG(Q*h`,/`Ip6!nRt &E#; KW|MۨݲmlPYjsl5Ey]&vX>s(؅nCc=B[8}ɽ)E6,e_7Ws9%e -[NKɵ8Yf> *ŹZ Vlp$:!UkjRN6aJC,6`>$_@#aO-Սw@|;p-&+NHҟ&Lb_YD\JtV}^i8)nfUՎ"Ůc"iWWT}Kg+L2ÿR3 K@ѹdݓ ݗ\jtbL[fS6#)52^<VkLw~B@P7o΀ {,.7)zV$g]6lU&ocf>>wWft%FU/'.Q{;̾I5NlLxkWHv!__&̉av.7R2[M%AzK5 HhSx vh":U+xN@Ǿ9Gde Ƀ@d؟|`V8$c&ދvI F|L 0%7 S|L 0%7 S|L 0%7 S2|43e& p S|>N0 OUu`Nj`x{v¾Sh$a|}UzpJq\ÐbYU-"VJj\ЉnSPZpX;Eꗹݲ%g_kMgIpe4Sei)V6mYVu5 K1TܫsKNnW+?޽K݁^.6[3[;nh+CY)2 ^eL/^`dƔZy4@ 1Zp\ٰv1+54۔aIa0;ECq+4춼:lHpa XPl&rk@im|^Q3 ͤC ]IL^x jwocK|jwob)zj:,k^ Ɩ،n[ХhZZՖ,X! Ӈ R!V"6`ːJOĈr_iD Bfv8ϊT4B@C?ր#N6>cvK|S6@ L&U/_h;߅v펫mqg e[l5Ey]&vX>s(؅nCc=B[8}ɽ)E6,e_7Ws9%e -[NKɵYG;6wk/`B pc obY2w )vmAnXy{gƈR8ko. =&Lb_YD\JtV}^)*NJ[YUvHHՕ;|Jp6 T1uL;PFt.hdBee_];)`X~U|)@}mEĴ [D둈ՃWkOn33|y&wluKe(B+cNO]6lU&ocf9P-wWft%FU/'.Q{;̾I5NlLxkWHv!__&̋3T*tv̞6=V(f=G-޶m ngV^g9Sa;Ibe s!ߨ73|EN;S̵|i,7 S|L 0%7 S|L 0%7 Sr O. S|L 0%7 S|L 0%7 S|L 0%7 S|L 0%7/o_}L+߷FwCwE"ޛι#nA98|MCч. s|~ul|%R^kmH.HqM`LWz(8~>$.f=ZuFo_mxB SS09@<|wߒT4çȟ 52VEs%j|&l9,- FZ몸Jܶ\m:Q,ͦjhCj{NU]Ekrn2xeD恪V#5VP;ʡoq0)nZu {!ԯG`"NrYI̅F$'H&zšBTZO,f: 8| 0%7 S|L 0%7 S|L 0%7 S|L 0%7 ߻syvu}l? Qwl;YI]ډtS.AÞVo ׇ7vE\r!;֥v'.*nC߈ ﲧ|{} Gz&Մ]˸]Mtqa"k,`NLj uS__3囕eWta6:GEڪhAJٖX-E*ҢdKTxX*Xi.6-)VmK#%rVY,n,ub,1`q͎bSsJ`+B.O^ǭ/n +vu"+qg L1mڮS כvf;u15^X9:I|U:m YLf ]n_M7=X+u f\d+ᚽm.y[HblL`j[X^-h舳ݢ;%3YR9G֊V22|rOrޱM⋳:jÔ.7RCՎ:HrD69bnA/ Yn2юNIuή`Svt4<k[j.ʶ]VU}ڱZ܂SsG]{LZgT{\ԁIwez)&3ٲ-v9bzn*qrշehCrB4kSW,-d68:&f(ujd i IvԖ]m᳥_US8FvV e-Ak4,_ԁWhǖjq 6'VsGh.1@:&޳"vT`ISg6rLC_-,u\GlNa:kUuO7b-*馫ӭcX?!j22Kmd`\i[* V޺Yv-X3M=-bF,+£N:g%777wwwbIlmLl6KwlncBHB.v!.+NbX^k-B-E*&K-]Nթ]nv2W{[K] ]uC^AycbgYc./##~FkީUp><ػh*m9r;<0N }Dv{FGo>~}2NGz?7,rd}ssw%g`ԙL^x jwg2| g|H ݁^˿ 2_e(͔ ͛h,|*R#07/{!o}T}s@8EfiBԝ|W+yXe1>DF|/Ƶ#sIs>[j?:'o5\|s5A]>a'%7|L 0%7 S|L 0%7 S|L 0%7 S|L 0%7L#LN8Sfl g 0%n/4>_|iE<8wRG\ z`G"؛`YX?pvTmmr{-'0:"T4E Z%69Vh 0&XQg02M7bs[ot5jꨵ^޽KWSΤ2THc7kz K$9wY!]VNlQ]\ו-YglL^x jwo9w(鑴^i|n?`!>kY XȊ>{R,04`bkYm4c;Q:z>$\+1|/iw`Q3.bY-h+$f9- Pon!DBjqUg=jb۪핬D o8^; {b̼ZZ]5NdjtbJ[ZJ4R[7r|"+Xˀ#sV}sssww E05GW:ɻ?WSvʇv-OvapukpdG_~b nΪtM˹'нlȓn-X=Γ^/3g6;؛pxwJ3zғ/vswR׉׽),BФX.>y2So1د$uh(Ρ`^MkoK`gV_w^Ab̖o#5 F:){iyaZ'C;jTKm8ǽ]I`rLnV֕IhUX.~ǘds@g%ƥjwU봥aҵ-"z ]jƘ,CZ nsdM՚1uf&&f2yw30jwPn i9tS>b7ggvl}aR5tskq6xYj+p FBYL,ŷm-ʖ6&f2{*݁Nq 4vCbϖAmgHTƻPIJK즆yH)xY6%_^˹m᳥_t>['\]^SIyL/_t;w:ŷw %>簣ˊ~lc~.6ƳIRջRD/Op܂&X;Kmnf[p\_rNh7ElL|H ݁Aʼn.0n7X PN:-f f~ܬ~,gjaIG;+fыXlL15@YJYvsp &Bv%7Q (%ҸJ[j\$L)߁o޼ɝQ9ľ>U6lSnPb) c; Y]`[m̂DZա E$:R꜅3] jnm%9&Kk Wa>fu  prهz,3|EN ѿo e 0%7 S|L 0%7 S|L 0%7 S|L4C;Δ;[;|ك|L 7 wgNn `kajx}9}x^%ׯCLP`}(掞yJq|7N3g}}{}ߟV)/byR6jP;S\or4Җ/CXs)PBڵ̽}rf6'Ҭ9Ol$&kqb*9uظl㪹68߽{Q|˧ER:Z5HvxE$qVa5Hey: -;q[D.-uNud}ww UpW|3m:}W&4w;|4J!釤kz&1-#pd  2fir]o`mHcKlFrM7Q:p8&*˗/],w47@}G:sq)ng'ln+JkOVҼ"KJ9,w]'LgHYMn/OI;(3w.& qiF,@7ǁ! ]U]F i +!} v2g 9S4n( Hf}:g69tt{ ARA߼y;>lԀ Ϧ&gF؅j!o&۽RbR̨x"& 31fCd`ZywM7g Y{GW3 .-*b ?I|å0|EN{/ggqi7\ 7Xo)A`Jo)A`Jo)A$A\)@`Jo)A`Jo)A`Jo)A`Jo)9e<ܹ<.` ǕP-W*[կ>eS=hÂ|Gz~EV9ާ::" ʷ:h3]7dk|˾ʹ ct)&Dq KH)QSY:I[^sQO"U]Ve:anaZsU&}KKfS*F#HXcD;u+ z2NQ즾 Λ/pΖCPu#|d3~ռLl2φ8RA%xs@] l'`pUG)ۖ֯e`, "߾8 qo_mļ0>F ]@c!фłŶIm7Kk$\"!.d,\ތʺx-Xͦ8G%r >7 SrZ{h4ډ±Ai4~-?plo_ˏ+F h±9i^8*< [0tkRa:N5-m n@h/!!EMŰ{FVNmzCeZ c\O)qn ToQXfAn»wbd=mZzH,k)+M[nۀ>gks^jϱ`[8.Fc@тf HjVJgv,)_a[Vʃ G޾(ӫSĢT9Ŏ@96.t,!V:qz:k۝ίcfm#-n 5reWU]Hڞ7rūغ`uu1XO$TWj2RZu]L-t΃2[6oSp oSg6s^@Ɵ0yx ә༴:!,Vj/ LJtֽDW w=m|xK8){c}Sopܩ+S& 6kJڕ}sAӪ\AϚWL9(a~z ?g?Ac㧲`FX`&7l4A` < [~ToKUtǔmTrdOr7p@k^9#ӼPa۽2{nDxop=*PuϣV'KlIلj{ïUlCANwFTiXSs@Nf6nnwq8i)߁(B#3`H*{Eӟ.w YǍ'G|4=7I|@F '7т|w;oD p y#<-7I|@F 'AKny#<-7IiA.-7 S|L 0%7 S|L 0%7dh4dǾ:]"IENDB`onedrive-2.5.5/docs/images/shared_folder_added.png000066400000000000000000000755041476564400300222070ustar00rootroot00000000000000PNG  IHDRUA&sRGBgAMA a pHYsodzIDATx^}%}SV)/uʤ$cE%6S,֬-`a k yYKJX.nĸj,$he!93% Ybb"@/C.^y`FL>s9z|<ӷ<[?w̠C([ȼ. (1]3ryǶ׷#s2hdE:z֓th|C(zƱ,]oqwQ!Q1\3jc7|0lmgNmot"{}wm` ~mpΨ&t>]3j.s8?Uэڻnc1yH62(G"3ާS:_S?tS;_TvdE:'>]ʃ(yD!]ʃLUa|ۓ},zElh%:N۞<(؊SvQTfs1]3,ۘ]\ʨ.{}Hg\A̐.좦tQ(#ufdsQl=6#$j:~]c[YznS9_.9R=vuY'[kPJ.՝S]fEVSwo]+Tw̤5u̥zzSoSfr=4_k[CoW<3k#.rdi>)(QfC([)[sY{>5;crMܯ&d{k?.oeV6!]3^EK=PFؙd좮)[&iF.yjngnyΜ/wr;0&tbDb vdvzP^Fh2Olo/w^2 6]O²?WE:沃'WE]c[YznS9_^ɥz*GER_dUV{ouA+TwzOu_/ל{{좤^ɥWTf%1gUOk]Dz~z5Sfꥬ{w~_ƨ7=JIݹz=CWE=4~mOE:;$ͩPAyHJZ~4ܮ}zrq][_M>I+.kER/oC˕ "yH2"tQ(SۙZSvQ׌ھ7EW. ubme3A(tHOg$'=H5mzr0pZLn"QP18tFa.u, P_'~]_\sY{ 8i5y뛞8){>OpTMZ];ufmOE:mdPRq1]3r(1mTr.eOhCHg :ByHGézQcHgyʚF:hP _]W_5hsEoM<9w;qy>V^w]z۬zg"30;vd{X=EfϷ3)!]Ȟl,ke}<9x8s.d1Be1Z>M"jvкg]~<'EE8cgER'}Sp_ǎp;ޟLGEf۰Lth]|\oO[SoybeshvRS[;dIm]ʃ(y3D!](j:sUyk|9lo]iY+Tʞ\O錚+T PFݕ]$.ά5=]OM+WE}G=ٞpF O2"th;Sj. eKM?ExMtQ(OI5Us^0t喞X\3UnWiyfqoe>jRS5㣿54ח?:7]_>ߎ_\I?3Hp]4w]3USw}os% ^[fc/گ;͜[&\ۿj~*}I}[jIvzP^Fh2Olo/7. eTm .. eTmr. e\v0}(WI_M럫rܗw8>gMPwfC[}E\ބյMwTFyz}o7\n&D3DwT?#Vvyڜlg~@9ܳjuݮg>jn>>]~O!['fcg󼞦tcgPl4&"*/q{NY(mr.elBwb.ȇ2>g<μR'"gMC_?y*YUxx_m}3Um26:ʛ'mLUMc?Xk^iwTM^SUO}XXzs[~*}Fr=ˢMZ95tFaezE+. e4=eMyJ.B/=f4ŎpQ(R5Uz3]9KZV_6O_T-)Ug]f'RZy5Uٰk=#4ͬfm3^6SvQ(;Y. eYQ}HgT=a.. e 2tE:A틬5ޡ]uNYFUz^"ӮY?S=zEz)SI׵Mgm{u=>^N=uZk=TV3U,liWeoMԟɗ a^/gju^S5s9`/F}߾ܙo}\皟IXmkyמ8#iDCHVKWWvΘܧۦ+jQOMvYvlP}Hg] tQ()k\4#Ц*x.MG1\CjL(Og$٦]4. o{r:"P1&OƓ73brtFAƠ+"Q]tΨ`ǕtQ(r/WylޭIS8=k*t8>'o~u>}9S81kuwjMN.ݾv/dcxVy. VJ,VL^HgTm r.eڝff]nG<'ꡌ٠=W顙1zfh:ٕY͢z_wsU}Uxx}qjMըij?*u]i|&. elc)&OEMs/Jv]3j.;q8OEzIg]~]c[YznS9F`.9R=vuRuj%{G=u;8*%;|.YkNݽA=+Tw@lK{fL"UzӺ&PϚ6Y;3^ʉ|uz'[Oՠ.כ%齿Jz5ZίuIݹzKܙ۞5=]OM,WEi䣞lTqY{.zy*;s.uW6Ȯ!]h|@<%5!Ƒx<5huO"ֱ3=v;ð3#t][o#CH6FcGoC]'N=g:ffbMU=yHr VLf7CTEf6\BeSܧtFa+PFU4. e4yJ. e5=If4scHU*~M<9w;qy>VΜzw{6y#|;2Ee`Y+T.8Ƀyr0K޷S2JܧtFu_Lv{vg.E>n?<'⡌٠;ؙ+墱ꛙBlgɛE}߿듵]ǞW >󙩺tΉl㨏뒝S]>і틬5ޠ \;{JTEԇ4B=k*jNNݵ]|JuQw^ԫS]{;׶Po*ԝ۩gްoL<YSy. A=m9r.꫾|4϶Ivy;+;.5W6H!]h|YB!f*|0V.ҹ(-=zGH5mWɛٙKnk19E:lcЕtΨ.t>]ʨ.;X}H%tԴ~:̋UVwDg|]ʭyHfm\rE것WE:!]3Zp^B!P?jz<錦#p ɹٍ|,.۝9Sez1y#}ۓmE6NȽywn+&/\36]O겍@Wv{vgӺHjٌ=Tov3'ڡ٠LutXE}lg#kfjw9fQg:Y{yx` |sgn{. elc)&OE]s+%;sTE]c[YrnUnYIz=u;)ozȺ|b-YkNݽA=4%^;{fITEԻKꡞ]쬹jNI]$z5nyEv *EǟR^Ku@j{GzN=͙CHgl(&OE]s/Jv^gEJZCqܮ}mj.&̶i?.oeT=r.5W6ȭ!]ʨάYB!CM?pQӌSxrYiS:S1 t]CCBy e1yeYC^l. eTm r.eܧtFa+"|퓮_B9bfEջL_ƧEkNngq/uO|}7OuQ=1k5hۓt-d.ꚝJvV^w]w3u{\oҷ#.ZdO]BӘ<'o};eQ(P1(Q]1qtktg޵w;q>]>e}/Jo~<{9rySO=B!B!BegaVgvp;0&tFcDϚϦ}{楗^2'O4m5U>{4 Н5UvvhL8-mR=u U/rmT* U~ m.f6u!sKLcW CQk?J0$5UsAcQ9̩7v+C13U=r|{˿-aز<3I; չYq^z)acf_/ЄT͕]Z6]f:r˳C>3Ug0iSu1342C4[u]NS`cT cXSLo^ǢݙQ9u_֞iLUTU)eƨ-ݾ6:MՂ1;/\|һ>h[{@XSux|;͟~' ݶzU4V:XS\.lИc*uɩ74U<ݝɶOU4VZ>3U;MUaySnqe'mav8\6ᠹnsg3*U5e9xiY%nK</|3m귽6}.{<|C/whz>W\}OhmDi*)St>q(K 6mx9~wfO~d%' ݎC0wN|jsOqqsfOT$]2CTHJ.*eVX- OUy)7U_45yPnR8.Om>=zb%#>`n<3)9f>mNtuvo0OwiD3*o^_|ڵwVOkVtO47i9{g=DSFRrHq_u}sMծ!7']a{e=#ؾFsٯ\aʾ˨lC knX?1A%b+_LMy^.wīxO;8owqe(ͨظa~7U'rKz^4z*l&rm!f^sَ̍2{޲ÜY9}_`,T=۹1g4'9ج=TM:MŠgmnOjm~ s!rظ5w]e\~ǒ{(W{wevyy HO]od?d^W%KOwǭh.^sK1 vz L ^oo_ s/r =2=y.qͻ۔Wz5.K0w_Ϣt~ G/Mӏ%glŋ?RU.B s3W/Qc9/ٱYy?e _xv6}LWKoE#꾻uR"}ۍ7"wkq v|s[%b}(SY_3\}As]"JJfQZ'ۖEwݾ=/WZ mmh T}29-qy2z\De'btrsmfϥv@& 'KNT'y9=`OPDrAwv/#]מ|#i{mC7vf?X,Pn(̍rў#as=V}fϗ%_wXS遛O48du~uf]j[5muѹx׸O:웳KHtk8\|vި+,^J陼NZڶLg,Mrs /$%ޮyo-fUq2Wߑ7KݽekW2vz2]Ü'ɾwTM}Iɱ1Y9l|.s^ypy[o4g) '!?=4cn3Oo>dy`njy2,_V^G=}~;;Ϛ:t;4&.MՄ#w+ES;rOtӡ3{7^m)LgХuLK징>I}vJ!9Uב~MmEg t7$狠hHC33ݯsz_<,.6\e-gԚҟ ny6 N37ܗ妪mħX_6.13_6%A^O4{>p~6M[djGq2S!Oyc{yI]Jrkr_7/X̄utؗ+]XkXSuXݒ _ hqBzi<se~%5DXd;ECz]զj[fRk@x;t'kC}*ذlJ1IL/~Uidd/R^ZCd4Oh[W}!]_-xI/6_o1ͬWw9y#>VañT-ߧt,q"l>i+Z٠HzMdfdf oRD_P޿%75UlengdˌJ.Soi TM0vڥTNHٯY\bnٶ9QdqZTg˽J/-ɢWXu_:AgɌtKهG̉q(18\tyvrTǶ#ky}hڎth.=gX^|cf~gYj YlI/Yqx>g'Y_̚`Wߩ>W })M&MljylvuX,T"9FZm{kQpS!=v+͸8ZFsо/<֓Tqš5U-gRaIrB:+[w4ۋ56Uz:#6_Ǽ$* U: N+ůl؁}ͮlS;'Ws8o:q`(sȓН_|023ݯF O2dy֛6ɯUB$U 'V/~dJGO>|e'[;N 5Ue=7?xUjھ{y'_gas3י=`w{KJekRʚ塦jӱQS5Pʮ|o3Wii#8Ɠ6ksj| +՚ >g2?쿎}]+4s;^M 56o~оγɹ?PY,_SUfҘi kTM8qiv;Ջ$zkOCMU{Puk_pCS`~8W|<@ӓKڷ=X<QݮMgRM__N "|023ݯFBќ!50U5Wϝo/o8Wzl$y,5g_c75W\x^~ 5U- >GrFs}o&f~OR2Y\l _ϣ0\𧻓\|kkTOO'5UH9Son4UC0Rc*BS^SUH_6Inslomc {_21F%3Usͱz2q_KiMC≊,3Ne{.Kl:ol'_7Ir `soY-_/_9׉miFuG1wuW/ڿ{MUg6C'9#[Ʊ,^Bnzt?nvUѻnszugWSO]4[[[+^ sYg-t9瘫j뮻nyg&X5Uef3z>Qװ6gxǖyUCOlKvlmܑϸۯ6_lvߛu^9ES`pܔ\ݴ;"fb??4UǓm΃;JMҴycܜ?ț&j¾9]zey L䃉henf<磟25s|[{JFkT=x} UMUA.ɠꇦxOSNc-Xʉ̡_zf_#뒯 57㥳WK+?GS5) Ǻ--`/<3Niq6hF;,/:_UyM_o5.}mUK^o3,|!:S@Sul>lO>|KU:|fqnmr^3؇uu}^}깨ފm8qEOR:X<^{6Ǯ~M6ykeS?ZK-sͥ|ǖgMIn-s_]'M>iay2ɋj򵋆dhmRǾr2}l[4+/;}ҍdi W_6WK}}{lgqA_,H酋K/dJXY\ +wEafW~mfpɅl__U륯{^MuKsk.g?hWO}frl #ɋ_wzX.sMQD QͮU VżVϖ\p5W6=yǩ*Q?v [uk`ޔT 8-mR=]lgT3Iܛ_en--mwo~54U66JMZӯTsG5!fYzmSu}}}\3o6mꟗU嵕+`Xf@bǍ^Ycdi/E`\pgr,?o}7"5# E/_l5˦}/{~t,cHfzo.󥥿֯8%qu{[%{{^l|^Zps}n[,ߧJM?T׵ 0oVjO>˦=—/nSrMB 4'ZWn چc,J;2-qtfTlL7}}!eSU?oR|ϥ"cU֟BfZrqX.^ꏑ_,.|ʼn@*.Rfp.ϕ=~R([z=ҦlBYO>F9ߟVSW5UO=ٲX*j|a.#O}a_Ԯ/5wTϲɧbyI9y=גnF{xY}Jߋ^y\\wuqwu~&[SU{h8lgC=x햹lo}q|:~ߖyTM妠m0_=Zn`fnPfzpTMK^[Bk^[ `."j "%".`'6a9s͋>w>pkg0VorV_F?Nј^p~xT]lSSkňR}x99+;GS\C9ҏ~]fkLTɻKJ\[o<ްe[;~TwjTM6myd\4C}?~]LTi\&keR=%9kYU9n֏ۖ\RُM?,;~)WΫ~6w \6-;s&^CUtf#u{\gTrjws#_pF_yyj>\**=gܖ(W-j]ϲQirLH[/l_SU>˦ \,ͮ-~jw9ʺ-]T6P?X-zT6 0 /~q'/?Ip};qY6lMdfi>˦̍2?2/~lƪ6*nfc.TY:e院֮i B/KfL MU鮩%3SuLտ;?d_M9=UE|ߡe6k `D&jרjj mtꩧ:~n? lYյS}.3NK+TOlo۫yM;Q2?ެ*l) bUW]7\. #5Tz*r Cg s9gшu56W)ٚjϗ-U}I7J-vnd* 9Y";[Wc[o_ @{< eTmrn]e6.egy.D]j)67&j{oyպUr*0_ƛpiSVSu}ؼKycc^Kje&j{,yڞ}{˓{0i.`LgMՐ:?,N?2w|cWWr0_,Z>.W3MUX_\M˩ `7SUf@NS6WCs /ٚjϗ-U8MUfX/YSUfi,m#ز]4U` *kϦy NS`ukʌS[:MU%krY{Q4U` _SU灜*AeMՐP^T1XݚZ2THcS}C^&ޞtaɚz.T=l%Ck/K2ST!]ȳl:C6's?V`'{^sYgofhLU;MU^ϙ_0'Mc49;gg}6m]2zqL4U`([SUfrKcSw+GiN$_EoOn,w=qq}f;>v. T:$r7ɨbrYc:5Ud1T CWSohy+_GG_w4??0gyf7̎7kw.VkZr.-kgoikq9`v_>n{Gq| >rʫY6U`TAc_ڴ!l޻l-YUSE]mZ8DS| 3Qi0ޚ! T#}MկmyEb6bFC6r1ܧ|q7.%okv6Yt|g|㥿|p4Fsg1K 4UK5p}=uɀ!3t}ǨV#rX~novv,_{k*й 4U`]q]fdv fEΛ ׄkJ_^J3 MMCz*}mgM.:qkI|ʌG}!On*jsq]U?SUf.9,3'M4*+bRfd/ {(8>TUz궸OF/4zjq}lT!x O:h~+v_+RV;ύ_k wVEeMUИY6ULrzhRo,d{LQ~prAت5.'>\].i@4UKhmҦjX#>t7ח?Q1`]3V4>ˍhr\s0 >˦WUm b2Фt69M GdyS5kr;5$=Av4Q5>.c>,?fh._q<ɠ*ꢹ<>9 &{M|hw{ߘgTsRp 4U`]_L.o  MuGS5(C_f>Nl/! /pqa?gm~ٜy7}#?ߛ9WҼxڴqDzXBMմx}yC}&W#o֟NVw^:ɟTox_WSN93;vX歝 㭩%3Nunl~g Y=P0r`!X\ESL),3 րt=ċK Th@7SUfܓϲ˥CepCSuz_otglWű;~k:{e @_*3EuE<Ǧ*Lyh }jM>f4U`jd5~՛tkYwuB'm_~k\973[%~MU[:MU;=Тk׮EC6MWTd__F|/, /bM.K4UڳERܠm뮻"u#~ yHMUVkE:MUկړT1Tg|fڛm*A5UuE:' cfk*TչT1T}>Hmj|6NSƠ}( ct\TT1nMU;ԕ94U=B!B!Z5UК܇3SƠ:"T1ZSu};MUTՒ:4U` 5Uf|f3RbzT1T:"G~LSt]TT1nMU;ԕr0ٚC2s4p03UefCHgh8T]LӮNS`5URWiXS}igLPtΉ d05UzcS}C^&ޞt5Uј<ϱzDŇ0JhK5U.3HCyeS8]5;̜<K?VyC=Yw}Va*3CuEMU^ϙ_}9oˏ#VAW]uU7欳JWlmmSN9eV\p5vڵhH)Ynz뭋uEU^}Ĺ*fv96UO~T&wo>|9;LM3TCm#ym8 ?Fߺ~o9SiԖwvu=O0$eg@>˦/LS@f~K/DMfnJz(z`ZWB`=>֩Z'k4!45smZwǿ`FA\}9QTFUr,a#sS~{o繢:Ucڞ=v]?6*oMU;c4&$ϲ2@SMUuA]U/􊦃\(_5.9oV/\+Wm{)j@ӔZƯezF5U3jLJU"c\ruӯDF /+BTԪϻ±PpAC,U]Z*z`/&eFLbrYL(MUEnYnB4U?:)ffu}lT!x O:h~+v_+RV;ύ_k wVyHClˉѼ 4U TXꍅb|)/Q.[5UE T| < X MkMUMXTkDه.F2cRz<*? `F}_ۊg1cU4W+xn皪igTOt 4U TX&Ynb8 ˛Y܌(| ' _/U__3^xsg:h|myW򳸏 9?ˏ- F`2}Jh&>}qߚZv+lRR.lRS^=|[e0}`BSTMF59-o&ϳ?[ gh#<lsuAkfnjq`1*TMI[G{cD0mB}[6CuM_켹>.Lǚ]lseS؜D#yna u3Mҋүf*."}i@굆Eљ5^fRߝ\YP]V/-\:wim} 7ǠwILMUٗt>;?-vE}o-9b >]C,_^x>Y癫4U`=p]T8o8TyM/bggT8o1C(ɳlϔӃ> 0z;1Mom?WSN̹^SW~ƼU2gqF7_^k8TˌѦyeS׍?k?T}ސ6B7W.lۭśk] >u@կ~9s̮]j0Rt};៩*3BuE5U9v[ͷ?|kyu0g퇡x+ҦݿldˌASuzP皪c,T]gnJ>YC9C7k{9qDm Tmm?oyTչO#Ϛ_|1DZjlo%CSO'O@TE(/?Q}t;%oEuwNXiC(}hk@8TZ35WvUǫys /zrQB!B!Њ^SU!}ڮjzyy!B!B!FiSu*3K(nz׬=dmS[h8ۋ r=tzrO^b=]dzʓt~E:#-}~+ҹCwa߮ۯCH6yJ.jJvgE1W"(s..y6W"({w.. 9*"r.. eTSm:d UdڻddƧnUkw|E CH9+4V5Ug{.^_KߎC'/\[*ki'ognke3m$"OJJ_m*'uH3We]$B*{G=>mu;ò\\`]rꥺ#O5^/Q uNd"zM3 Q%Nd"zMkl#cdTEVּ *9k5Qd+tkSOU;ԝEVB6TI5<^_SuH94Dm*]5RtQzޱ,]*kՇmO)uQl}|ԓm,.k>]sm~reO}H9It9wq?ӢBg"Qf\4kꛮ?upΨ?CHgT7~E:YѹrmON]\6F}βCWӇIv3}y]>Ay 깋1y^))Z3 ևoؐ.!9ppw̼kȸo؇ƿcz5U=yH!#E]SY+1mrV>l]y.ncZW>\3*o"Q!;T \Ȟl,kemrkOv};e3m$r)/Wb좤/ͳ]BVE:#6G.y~?ΟKr"kwиxoֶ'Pn!SvQ(G)9yH26\Ol̕96\O>&wqsT6.. &wӑ_Fmdۋ\ץ뛒Wf: Cg:ortSnCȑ%SrQ܋ԨT^5?crOlyֹByͬӑ_E}Hg]LzE]3Z/E:;"Q!= H97~ϛdzF][o=9u[('(#Ϋ GHg6vsm$"(HE:/铮Õ3eպ|b=sng=Svr]oBZZ:ӫt!$/pcϯo\Eo|هCzۑyH!k䢦9JCHY*AV1ַtlcoθILIΫuQl h %3ZBtFa. et~EpSx'ۻd}Y{^_SU!]CPE3PD>T횠>Fr=zESZYE<f+wqQ(Qd޷tA\.y ͜3,ڸHg9ԧtFUW1]h_. e^{.opsVh\6U7gȑ#s]X1W%uQ(Pq}eܧYqqͨ K8\OןٳA^=wq;kW09bfKloobgv"BH;^"'Q_}6Ys"kWek=Blc,&OET3D!]3 6\tl̕H96\tޕ9+s e)ܧtFD^׷oJ޵^x6S5@Y{'y _}뚫W~nP|>\סqxkvݟҷ=yHNu(>Tn.Hɽy;s4ByDW"me-[\ʑuRu;3=m$뒝Y3.%?5뒝YOe{zu]4zIu;Kvz˺H^͠.3XD\;=.틬5QO.N.틬5QwoQwoQwo5޿mzRݩ.Yw ?SN%uvц}ڸQA]TI74~n_:tN$ͭPBy%QOJZ~Py5n]Ǘqt^}_˾o]sEEƛ}}xۑyH!SvQ(R38s. Y*A+s.. 96\tl̕H9ʞ]tAZ=wqQ(QEG=ȯЌ>|uMvQ:Ο3+k_^]477ϙ¹=Ŭ+}|ǛHgTHOB&.ycݱy#|ۓtce1y0Ov}cE:P]\eڠzdm#ѕast; q>=~][&gzvUXE:lc•tQ(#6W./pΛ/7>6Ys"kkܧB!ۘSvQ(?יtΣH\U횠cd.׳,ZWecE:Q9]\eOޮE:Q HgQE:#L>]3 :c("5BwE:YN.93MESs鼆Ҙ2OvdByDW"(Ht..yͰELTѺdÕ|2=sng LE~rvQ̕.r!]w+T,fj "e}}z_MG.ϿKğû_ơM7>T!]-dc1y. e!*"QP1ַtlo} ͌3(ڸHgԧfҍ"BޯtQ(#!ti"Q!<'}T=5:W'-]myؾ$vm$r?q"g?knG>]s XLBap,(mr]gqQ(/4pm539rEEve;[B_YUd~{Alex5252 2020:06:24 08:21:222020:06:24 08:21:22Alex http://ns.adobe.com/xap/1.0/ 2020-06-24T08:21:22.523Alex C   '!%"."%()+,+ /3/*2'*+*C  ***************************************************G" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ? (0(((((((((((((V=3_M꟭YZ:f?Z 5GC|U'8Q; )ԕ7x'N5/4x4xO4UF_U9W&W&Z(n}Vo_CG?`?_CG?`hUAZ=37  h7  kVhk4x4x>[U9W&t_ My5f3xy ТX,F8>hI>7~}oTj:+rOM꟭f?ZzG٦O֣$4ީiS >7~}oTj:(OM꟭f?ZzG٦O֣$4ީiS >7~}oTj:(OM꟭f?ZzG٦O֣$4ީiS >7~}oTj:(OM꟭f?ZzG٦O֣$4ީiS >7~}oTj:(OM꟭f?ZzG٦O֣$4ީiS >7~}oTj:(OM꟭f?ZzG٦O֣$4ީiS >7~}oTj:(OM꟭f?ZzG٦O֣$4ީiS >7~}oTj:(OM꟭f?ZzG٦O֣$4ީiS >7~}oTj:(OM꟭f?ZzG٦O֣$4ީiS >7~}oTj:(OM꟭f?ZzG٦O֣$4ީiS >7~}oTj:(OM꟭f?ZzG٦O֣$4ީiS >7~}oTj:(OM꟭f?ZzG٦O֣$4ީiS >7~}oTj:(OM꟭f?ZzG٦O֣$4ީiS >7~}oTj:(OM꟭f?ZzG٦O֣$4ީiS >7~}oTj:(OM꟭f?ZzG٦O֣$4ީiS >7~}oTj:(OM꟭f?ZzG٦O֣$4ީiS >7~}oTj:(OM꟭f?ZzG٦O֣$4ީiS >7~}oTj:(OM꟭f?Zw٦O֊E1+:Αme"W^Isg-E nV$6m İ_3*%Pv,FXzE.?̲oMdoM~@;`y5cu%ޑ}K$O4fepCcˑF늋;}^mG{K{d{ xvdE;[zSZgڣ֡^a߷y$ g} hVg.-̕m\8'*][UDNm݇I#{}+FNK{cHP41 {pA3y.5HURt RwLGj/tNcJ6ww`u߹c>zSZ6G+Oim"fD;Xg$A88&];I-.Mv|T"a?1R7Iј5t{㩼Guz8M1/_{i3v-[Nԥ-en&S"9*GQQciڛLm@e,3<_J4OVVڀ&[rbFea|u<Dž-E kkHbh^hP4D@,KvI 訢((((((( u6?kb ()Iu{-_˞aI6piIjr]֥VA4%aA#>mFK-{by>1-pvF0=-io-֙\ >c+J,Ӯ/H30BPp6dZlk9Z丞WYTð)"1OuWk[] aM:s2[O4O^}FHvG+ּ1mk,-mr 魶4lrc\Ɂ1irX\M\,+ycq>~U/0{}6]J]>-Ba][,dA%s9eYyFg@nsM[?SFV.ˋ;I o<2 s[(N qH_è\ح弶gIUnqZ-.!-l<>zσMkW"kuyT$*G\8^ mܪsrwTʅ<д!&Dbnz4m cjоմ2HSR-m·,fC t; [CjI0vX&A@}~-Flwc&jr1TiHH ~la-d Xۤd8'nsAZ /5-V(l0]&66UI)(C y ɮpjQʐ^52Vg6T'̒72c7ukm.n6\HJ$0 Pă e>lu f}EKZΛe<] ]*Np8Mua\mФ{yWyef=6zxm'.-..Eo:'HOzm4WvwY.H; ]j _x-i$j;),wtp5sMnhD"e{~l\o?.8s*׿2\[z%#ȋP/\p{vt-ԭ$"IԴ u,3k=Q%9 ٳ [wBF*=c[^rPoR 9p͒|oǧ7SqE+ibMfG$EoMEjْl/:N9vm:e ,@/%ƸH5.FD^IcFFhrte-=i_Ď;9㸂A%+PGZUsIe$ VF s;='5%W w%V|fqYxjml*ͻr.p~懸t:+-JQ7:} J`]AFA+"S}ŵw2F߇A:L.sdɄ+Ѱx< s^?Iy,Ksk%ȹR#g+ԑ6-;ZGe]+ApDa[;$(+mpqBΆ(aEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPTo97#aNGcӷTl>9$QFCWaӿ'yCJyP<`C;g;p-G~A5h_6ǔIp?O|v=O~FM/P(QE!Q@Q@ASAk w|0]qS@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@cVAN~LAEP)?7jP@(P *BEP@(P *BEP@(P *BEP@(P *BEP@(P *BEP@(P *BEP@(P *BEP@(P *BEP@(P *BEP@(P *BEP@(P *BEP@(P *BEP@(P *BEP@(P *BEP@(P *BEP@(P *BEP@(P *BEP@(P *BEP@(P t*(PzeC! Vlqu"VO#R]W6Y")7B sԏjEiԌjZl8VzX wM#VEIoq`O#8${H> m.$Y[i#i^==~O 2DcxOcO+?=?1tEt ][Hq>``G-\ z+j>&"C{gfC j8,A>#𭴭sa{owqM#86rw`mlL +Okغ_M|E.[y92ݭeh H919AioD]NP](r Hb˕(ZVW5|7O\D,EcB;Qaz Erמ(t%. w5J M :sa5e- V[F2:`pQ-Wi3q-;Cgx4?4hj+ hN~o4Es+}&vκyTE;QZ( ( ( ( ( ( ( (: oJ=bp*(|;3 2ڠ{VĶK4kxb~ 4-vOEQ@Q@VV+M#XtۘiY^(5T;AQ@v^g56u HjmodCsd;Y<'"-0kEg*<9k^((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((~co[jgp$ rF&j9aOJ2y{I>xOy5.c-ϑU> m.m .d Q1#x|RѢh!I6 K7ې.zg4{W!utMM-xr;JV[~քײ/?<Dɬj^l0nVs`OլƵ_PבCdY[;XsqPxZ-u{Yfq.t OK~d_1jω>s:3or-C(V =i-Y/+5?ƫM/TKa#pǐ'ׁ_ޯ!;'P{3sb?=@1Q<>x}Gϖ]9,/ .JL8; jhվk^+ N}(Zi??/0]x/^B+8 {Plu$v^xTֵ [E>`FQ":u G~fC4_gyݞ14O[k[|-JϠ$)_N#foL+^83`^[ :ʭxG4GWWp;]%Eʨe#8=}u!in<h/ZRԀI{V}{2jpv rX'qbekGCEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPGأoΏGߝXM4v$QF݀U$E([QY00M3c*GkBQԢi,neFO(pêc/أoΏGߝX+?V~Պ(أoΏGߝX+?V~Պ(أoΏGߝX+?V~Պ(أoΏGߝX+?V~Պ(أoΏGߝX+?V~Պ(أoΏGߝX+?V~Պ(@,(@S_SQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEBYl^""EFS!FrHJ#TB)sZgCwo1@f_nB30: iƵ\]ܞdV5AM(Q2O]kKse>}v]&EtE(b\NhSX.}Yd_-P  d tГB|l WR Xx޿/N_+~?<7}Yi>IglGQ . 9l8!@UG\dz=b.4tw[G@EEu~cߏz|1i3G%?*IEf!: 5fF͹5٢ ry$m]h P}WA5IWtS5~+[Dv榩QEQEQEQEQEQEv?kl)QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEP) ((((((((((((( u6?kb ( ( ( ( (/.|ʡ%(@}hz+6XkH|۝688+y=_he*rEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEB^!PijtR䂣bi:g_nK]ki[uW)x;y9'd g+g:ޕswa/?nEa+/R݆?cuG34dq |ی[ ?tCRy7}񌎻q?9gWvkvr/W+,rp:ZsO 2ZJdabIBOQ_x!S0=֭c1989ߡydRՄV4lEܱn$g'$;$( uFb//mM`HۇD䌶$Nj9<[o5[^<|AXh|7z` ۚ7$Q!vK2qgoM7jxl^s׷Kj˷oKx{oK4g6Lw`gRǃqWm_e}mw|"y.`UYF- A9Pqޱ&gl˧a<Xu 5nRE`:`Q=qZ3ִuk q"9"Cc=o|fvՉ/:(aEPEPEPEPEPEPկS_SQEQEQEQEU c<!ѫWꦥo-͗o$v 㧥& $r9jc(I=I?Kgmt."[yCˀ* w҅QE9A]i,Ԯ'I4ָDg3Db짱5]|_6QIo՞g-P&P 9˺g ⺉49R$2 \N$B~(ϓ,=AQ*-Ԫǹmr`O OE|UOtZ萲Muqio$wI 8%C eq5Ay`kMyƓI$mSOʜnZE- E GĒ.ztm*t_*l8h)9Z6y$R{ T25|2-Jxۭ\euotZfiQp2{Nf((((((((((((((ҭEѵxo乻+&q!1 Fw ?.:dSomIkIqgtbQ- H  OBGStD..a1 (qҙo eXd),/nRK]R'@B/p8s:k0ͣK4cKƶ[V&p0$FqڴuvmKEv"K@#ᘷxgM[nS4*/S(Oa{qJSѬoѦswRĒ:㎌hB0rR+@js[ .t?3 G mAsQZYK遞c&Iv\U0}r|ѝ৞n68rz:U+?1Ȑ[DHɚYHDmʠ@}jbo{͓Wqk,xq* %[Ե-cC-y,wO% -+jmmXjD\Et60]IF 0g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy g|_FSy Q"7iPEP #zwKwQ&*WY\W?Qk?)ƏH jYkSQ^GD# "m'Lw J61}Y/FFzu5u ?h6һ{o.VtL 330A@Eqljum?Pm>Zڤu:\/O>^_tz7co2ue+4ǻCV O|GMEpx9ͷ.љCk~$d3Mfu!GZ)\Ҍ h$ b\xx͆y _*uFl.CU@  lu_&ihrm>TFCy?(ٴ\|%m^5OobB5}~n<֖R&`p3ØU=@QA!jV͵*Rs1bRDEpwNH5Q`(((((()կS((((+#V׿~w$zJ׬r8< c72Vʔ3 ʥݵŲ.Y?v P-A8#o=oR$.Ѳ*sW_VQSmF،mA{ſt[wZ܉d8 :oTeGmlEgWx6sǘ.U#:VG|[kWOdkxbxٮ"5|E}#[MF$W@+ݔCwWs<3&C2G=QEQEQEQEdoOws!P[ - B#OcZu·hП|n|m>V:s:6j7la6eh.P S ЊԮ 7o+`y^S ) ((Xm|Rdl5-qwv +{u.7XNS >߸X9F m9֥ۋ{be%jÅ琧X uB9빛3.p/g6m$RQWTb+\ʎw c%_Pz't;:+#bJq?t>31͝ m5`[}4-(q 9M. Š|\/ tBn-0)XY[I1j}n%k$6{D ȹt3sеߦC*nСW3ka{kA"ϩ:Ǩ!eG_lFA8a3 muyTYIui } 2nmoc,sb\+%NpAPk/1jWԡ! {p6s׌sYSYYMofڔFSt;XScxɍwz:_bX5ݿFsE4R`Avbٛ岂=Ug`^+1(>tGsEpK5ꏭnK䵡J.sf3uM_U>䧟c~u.Y(QEQEQEQEW_~~nl5f{yZ&e`$d|^6K:5Q >|ݝCT)L;w18hF/urq>P=Ue;|3~N8t5 7Eֶ?(r%d_ ]*w+x-~r>٠ yRqtPٺ#h/, 7?knnUf8J+z(3'uzo4a@Nk2FAwJqTa:fT'K{> -FI@a5QEQEQEQEQEQEQEq_~' nuk+Am*TsZj  X?cLZ>/.j  X*(j  X/.R>/.j  X*(j  X/.R>/.ti6VbuGKp'#ifۜn1뽎sQ=1ElVd\B<rnF{˘ KxgBP,)N)M][YE,SQo/vj: jPfYoHw.j ү#r.˼cgT$T%2Vi5a+RʹxAeLh}MYX!i9U!}qpå/=Κqܕ2p@9隺-/Rk;P.5=pzV{_swXg"YM 2 Eq+tbXB'i"i,#Ԭ?YLM~%Ri-_@s:x$e1vD|qGݳ՛e^EfhO)H(s8fYNZ7Mט L,Y~]ܶ6[~q?/)1nrwm|cn=9; QE!Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@+"~/W6nxIP2?GtO @y.Ϳ4RP?2 H>-4cceolXa1*gVh&:mɅb b!4W# 9\3>]uO2j۸F[`lZ4P#(kL6rs2-Ԫc9d6c<&'֜|=iSW&hF],@:( <-j lR+]J1H\noYZhCl v0zxPLҧ8)42]}aO 1} N)mL&aR#h\t (.Oi^\s(@Z6\ lIԢ(((((( u6?kb ( ( ( ( ( ( (> ~ Ao^׬\H͵A$O⌻xV#c}E| 7g?ܰ5R.]~֠eӥ^=yZ?9fo}-{|L~ZH+[{"?"_ZwE^SFscc=g{gp?(+gφ?,/09#2PtWÿxjO7Ck0CH1-O^֢zo#€?<诶?utOSĶ2mLEcG0x$ ϫHH ri7V wSX Dhnƾ Eg!As2{օQEQEQEQE6ڜ?=eJl[98:tP^[irf{zt8t( ( ( ( ( ( ( ( ( ( }èƩp}HOQ?CVⴶ5(*"( ( ( ( ( (1YY=GB⾄Ҡg9+Ƣ>3xRuHS׾@ o_E>WQ@q'hMQ=CXG ڊcMCֳ75A*/ބ碊(((((((qAkio8xݞ/&R,9\o3G?0z>!bKMPSk>F9a`?,: (d0WL df\HÊEQEQEQEQEQEQEQEQEQEQEQEQEQEBuvx23bPX G`kJ_+ZQE\U݃>??ih~(e*XGPGJD:w䶲9cO0 ^QFo +5jL*XYAbz>iƕNXIY_x"}Z8?$GOQOZaqYxPyd*%`c\hi> xm{Ke{l \F60'?Bz}cMROQ[ٗ|v:y.rG4rMɧCI}ڬeAS9Gn$Z i&JOݡOhT.u" ˽V [y3pdmbpx犩OIukXa[+/X66%'j&Esmo.fݨkxu 0= 4d;qp$˹U>Vm@QlQEQEQEQEQEQEv?kl)QE&}IziPvVFE7`oѰziPvVFQMwong&# ;ΦRU4vCVFtz2NjCvVFE0zh=[4(FտNzh=[4(FտNzh=[4(FտNzh=[4(FտNzh=[4(FտNzh=[4(FտNzh=[4(FտNzh=[4(FտNzh=[4(FտNzh=[4(FտNzh=[4(FտNzh=[4(FտNzh=[4(FտNzh=[4(FտNzh=[4(FտNzh=[4(FտNzh=[4(FտNzh=[4(FտNzh=[4(FտNzh=[4(FտNzh=[4(FտNzh=[4(FտNzh=[4(FտNzh=[4(FտNzh=[4(FտNzh=[4(FտNzh=[4(FտNzh=[4(FտNzh=[4(FտNzh=[4(FտNzh=[4(FտNzh=[4(FտNzh=[4(FտNzh=[4(FտNzh=[4(F`ѥ ((WKE{_봬EKKIcXYA;NV$U6\tg՝F.ߺB O_JVan?VF7|-CJOϨxf<䄄LO\}Vk<&%XKN@vbռYkZVyM|)o-X$lǾ{i6^$Aaky:ndY~Upx5Q\B!md6,G;nYGz'k;A/ڵ0Tǚ -=qE G z]b7vq[#mEFUVoI$;V' 5>3Dc!tΧ'pik>o{ky 1ƒLYRUBv|.*ޡjr&*Zd{+y#p>@5ϭ_]kzJͬńUh PG5C&0[ϋ&[qcXCrzFrOOEQEQEQEQEQEQE]Z: u1Q@Q@Q@Q@s PF" >Rs]Ra#4d.G>ybh{8ta*u9="($vEnW6=3GE4<[/(b~f_[ӣg,G>V*d:p+<1!^H3`~u992ZɏNDYϓpH$y&.ƈ7@ (%۰ʆM5w`M`$eQ' dҰX`K.cWjT1 cz+tt,EV% # #0fgi5ӯ|5c\slo mYF}D @NR0 [:~k٭ՌH;VV0At oMtsXKwF8 7ɍkJ@s}Fkf]iA,tl22pr'~s0xK%`>bb 0o63ھw eqi,4fEʺycv333zX[,6(luTqN*HKաDk][ 'yn9y;=j!־0$nw߭U}H]$q#2汸A$\v@ӵ;523d"H 6 $dgsZ|\KqgP1 !0xhA2\r E3n%c/~3ҺapT-K;o+o$0gbAv {Wq4&᥇KD\퍲Nsh|7 *\Iд ^M(F f;x*kP0TxrAq$.q܌ҶSL-K=*+ؕ!mOa<YH9 >mWPu^:\YY2Z,L$96㓷:ck iW\ShDw2Pp<['&e-2BZIa$cl9;[y<Mn__^oz",MU9CЂ/cxmm²[;9YlVF aI?I`:`;)ض'Rs$6ZClcpq PrvŨϖsԬEt֍c{ br6drq/oR[ory1;Jp3WJ=Ag ۏ.?gRJzTšD `[%M67"c|cRmZ,V֟L{Tk;YZFX4R$@M9oJ:sRk: pXOp/-3URU~<kzB.U0dG$.l zTSOhZT1BAu(wÕٳh8ډBiO"q+,|2CmoG鎲ckHY oqֻ{k kM6; hZx\cҪtC$1@]h$jy9>_$Zoخ'[qkm(Y3ǯ뺦h9 (2JQ0`׏úb[]@aUPJf,'֍Gún3{dʔGq$k*s=AhS3BU&o˳uXٞfpnCtsfڵ`񸐏qx$cYk1jn.txH1,32yZ4})mV敐aK8TTՓCF(B(( _ScVAN ((((/5(/Q,,~X'W.+-?i$֕'V\nXv[ƲKݕ+R퐙6lmsFC`o X+(Kvp/4 H8R5f 2F3Mk-!K~ yF9 UKNek4g0ܻmP0y<⤼O{]/ }f̂]X sjբ6{efQҡoh~ł"7H#+ghUSUkﭞFZfS W9MWmh'-bCP('P*֕wjvqJqp7<8=Q펕fun%hH!yZ}8\=hEa%L7i#G iہr:fgkjWVJmb  qqr9h/Ye@l3*U.p2ON5梶7Ϊ\n_8679@˴Vmguh {ayU@1>ߊKH|5KFQ:۹K;a @kUF`o/mGkBIJV#?ns{+*6Vz{Z’k$8-ym qp| zSuM#D9':P?bF[&.HV M^u,=Oio5H򴂊(D9H57þ"Ss,"I"mWlUdsUoa<[Ho$mirIyT8&. 2G9PO+;Y-p[\m>l巺),a_$XGIy%7cmU|9+0yxmujMR}Z4+֍f,CtogM?mJOSiգ,AoE1q q85VVqڬ<ΪI<+NOğà8+jQE!Q@Q@Q@Q@Q@Q@Q@5I#t`AQF`h%݅ŧECڵؓ2sӄܓCy240b^vИ'@m01޺Z)ekt%Ww$-p#.H /SIҵ;85i 3 MPV޺)t̍fRDriOI_L7=-渟LCqpBlyHAisx$3I C9Ⱦ4; `+…! \k (nJERQEQEQEQEQEQEQEQEVncsqY\ZDC$ܩ ˀrF:gV>—{=7 E \8*|T7˞@9KM?VN bXɴ쬤arxLnQUv+jwWO B<R܀oQwQ]h-+Kڋp;*> sI騤3qi׌H;xE|`HǿP$rzR[;X,>-lCkonܱ,oϖ0X룠CƩywo"eʝ<o㎘"*)[5sj>&Yckh.DcF$ 'y$t[K9-,,4M9`H*6ִ(ܥag%֡,J Si<^}XQ$VsIm$l5EsjR>Ǩ KuD*2l%]=3UXۧYI܎Wjm?(G%Ajޢ9R(Y7H.)V/!0n8n.t9nL$HTi\E CjOu͑avvm?ƣ.yTjwd3Lkdy@ʲKs.G]QEQEQEQEQEQEQEQEQEQEQEQEP) ((((((((((((( u6?kb (z?:O1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ώ1?:o_Ηz?:Z((QHaEPEPEPEPEPEPEPEPEPEPEPEPEPկS((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((onedrive-2.5.5/docs/images/windows_view_shared_file_link.png000066400000000000000000006762721476564400300243640ustar00rootroot00000000000000PNG  IHDR[lsRGBgAMA a pHYsodIDATx^Ug;sg νssΎb," D d$dQDE9ghMۧ}i<~sTg= E4[6{P;} {S"Ო ?u))Kk(]dgAQO-lW$m('ӶT3mצNaGJ ߕ%Im7$a]% ($ɲID:;oփui(a IxkKxl>~ՔlEA6"c76 Oy#_(x}cj^KWWTpe)+^وe_^/ʾ{qXB4 q/H;兵uy~ r-{Nҥ3MG>6OאԪP!JM}rE<yBybY8 -CޣSԙ+I}H>D K䵗v#K,il!AI/?P4-4S"Ah'ܿHt/EOE}",H[/CNmEk]'y}-䶨M[d7F:%-$GUJP\Z-5/Iٮ $s}\ ug̾eyMrS_ v]v" Ͼ-MnennɺsIs}GprnJ-"fqܶlx Sking!Y!76J3.Ⱥ~㛚g "@53- #94d_M$.ktm YW]+0ϼrZ2vyɲ^yWM gaa*{i0.L^]WFOJ6˦x\>lg]!a0I#ysX/u~ eJ]}'5H3.RZi I&#뒣#3or y$vȸhᄛ-L)i0j-5"ł {PQS=eNDMf:) w24R[t͖$A¦|ifUn=tt#EASħ^$m;y0#!B67 aHSѣ4X<_`LAKcpۊoE_}H+\(_L1fKsLfs\\YSɲ:g4gHT c!lɥB)iBC%I-D 4Q%h!/C.͖GL!0'W1Z-ܦ9B,KlmN5#E3[|Åq9.JIv=fK1Z$~AmL\I۶6 4>3Z um$?!Nk`rZ}"EmrZ|cn>FnRӬyb}%i8)Y)a.-w~;?GY;h۲^oEs Yw˜*9wXf˝s{29͓I[3%i 1Ɖs!fo"zA#BAm-N$Vb"G)ْay #BAhw(@Ewp sa!,zygz)xCcGzH݄Fȱ`̐(T-B)sǛl7'RAd^txmczƘBptgJ3Dg؆1E,j?&f8ɗp- da&4XjQwFJnIgɪ?ڐA\kf @.yaF{\IJP!7- K1Gy<@j& 2|G<388"E}ȺΑpG\j% s,I\$N!FHO2mc$9EN+)#$_o<3KǦT7ŧf r eemE{ +Vݲ}f(;%=cg#ۘ) 9r8SEl!I$bLOO˜Ɗ-9,Kr6ISQ9$pI]ba,jAH)dBĊYA#`x3Nf#Yɾ@^%2nMRsGònLJ  Ml#x񶉚-0sƛ_7.-6K~l 9WK;g ڶ¯ZEi\-ǀXY_^j]9WLCl^+%r,wF:h~4"?v6a6~\2/7L-r8do KeOP 3Si(x凧1zr.KkȖ]Iދ${i80X Cȶ tB60z'L hpV܅.Q4 "`ȇ<,5#{o }c' z84.hPgxy$ 2W٤ eێ+am#ER^ѻǛm(Qx'2?ꡫEX|S^W ]jSxtnab"RiSZwAV \3@ (|Cز,@(n ٟzHi;]0~| X #I#)xq]m^!.9#:5ISD=DxKI'up).5JMJ~YvXܧ'/qI)4?!/;!}l1r]{6I(!Lym>9m#&_#R$><ٖvE#;-#yG3X4Vr%%5IN/l Ͼ|egȑ{-'&n)+f=l>T&ϝRRfHli9Ⱦc6oٷɺoYqsOuk 9)!S8c ?̢bA#^Rgr6IM/FhLd(kYfT2xI y@3.d1Feْ} }< %labG|m L뤍Bd! #摡%W`ȕ0uc$3^Ø)RnH Y47hx$ X1ftvIoPm5X2..taFeVd^*Ȓ ϹR>/5LxhĠe2L6iSA#9cD1LIb" Iof?oY#~3Cuje,!bL)ΰ &!BF͖$1F$C0i.1 RO8o#Bc4[􍖤RbfvNc$G 8iP#9;G" 1F fox2L8fAwd4A$oMl 43`JhvڔcY5)exG~(xCx[wq eB2|CGutۄGkҮWS:"2,^ISu #kjx侸:I y'M[!$+69ta!uY&BMSYZ/9O-xRCfyLZ̒ tA"<04[Hg,qo$ h  3IҦ&Mg` lr_ v7V4ekdɒ4Z2dA.ӷ$ KGxr(rl!0Ϭ3LóI,Wh9rHnԘ*4\|4̘1w˶O]$jW 7XjfHig#Y>\Y4M[?EƭY2/-R͞YFҀiRl͓::2oZ`!302H",cX$deaOYB1&u3Ḥ<4@0%])R(go#7X$   JWzp;i,1{&AQq3&^oC;.)ȸ\ L1>wd:^//Ú 7BN4^䕲vYMbfh'"\4GkF̣9ZbőH:K 02_8YN0o;a귌c4[efKmăKDž|%iY#̌'/IscGw f Oi5 tݶX4al9Jht,K!V0IHZt2O%o๱tAQKh̔bHo IX|7yRG֫Kv {MxuMj^zě4NH_/KdU*侸ixa%_8L^/sddzjm'&YIΤ WY4hJ%ii yj9rZv,n'! Y>a]"H#ȂR4yxECqAEg6OH]OLӎ|0aIaն;^Z\nEJrZh#ln!i G%5dfv熜{RL1[>7]uqw x Ϻ|v wJx*tJ1Yn rldрl  3f4@<F$i4g0_xjg-אO̒IŘ/Sy^9 WOA͹Z貞} mSQS!̔8qN_+r2 g \f\f#hvF Mc1ʸ|:ò<~KMEq" iG$Jr;Jl4fd6 8fN$K%>%,wκh,κpY7za![2ɓy7!RlQ.hZb=:G͖bA7Xl%9c{803G0$5] -sl- M`\x{)ctkwU>l0 ,GgiaFJ:俽qt"4\$_*ޔtlz(|C46hQd^#ke}uF]T4Yt cp&2Ql#%Nu҄fb@MED iWG޳ˍɢl{F4aJ̖R_)7O'ȓKaquXG,69텇 Wi(^x}=lZ)'up'I%lA2Y7 vHyJ0@chQ0\-4Sh#OhM%d5Fgƍ`̌&LKmL'Kjp~MkLκh<κ0 2h.c0YXUG4J<.l3ÛZFCxFؐ@yfI͖0EQ2ZcAhڼ >=zt2H= )(xWQ>V֠ hc)xe=i#/FKo g^ c#_ ׇ1?Rf/ԦYr4$Ml3~.&,<` ID>+20fJ2I=yOʲ ^2B3EҦ hwcl 4arAC%hTpFKs{Ƚ)sk}ȺGa-zrLduvxgF͔p1YyNYi0YJEM63Z.anf2#I-vgE )'1GnR"iɍ>y ˓c￸dȰ7;$NcvXq}#e,d^/,DBM4.'S̼V̲vv2\깆&'ȹf*rhp1'$ϕGܶaM2Θ)elehH>h1)pl|  @3u gٹnZ~ZC~>!"m2t}/I'L'pg|k~+άŚLe4:&& "q$-E16$Y$'9DX, amЙ1Ơ1gxS7< (aaesXd21f ̖6Y|hї=,=wX64VoxWلn2>yG']e4 >ΰゔ2 }’dj(ƅ#R_o֦0L4+Z@$2NA2xGkWl$ (xe5 _Le9K$mka%,> 41x/, hXԩGcr4fHZHn ͏&a^=-K{.v$O'5 S'j{p= 4J=$х·)y|#aY><ߐ{h9&yP!'v'_r|A{ޗln< n'Ӷ9AZ6!*es ĵ"%9ͅf?㽀v.rKRQG,̣@ Γ~%A^C]% k1ÐuW wJulиl3#ŃfIͣ<59w~Z}{@έR}4d*[2ݐ}l{q7>MiY0sS 6fGzd 6G֍^>Y7\OdLAq-#@r A`e Ar }2u L)3zp ˸ZWLg]%WIY R^U `̔+'ZG[$Iȑ0,>s;RF*X8F+ ~Zi|~xqy")͖Kd8#8d;r/-Jk~*X9ME#<^,c&&eL> Ҟ i2KBK4E5d YKZ"'h 2.!"δYy9hp./\xI3g]0{LT{܇ei!0 H|ò O&1F:̖,Y-4XJָ%A"y19(DѴVzɾE "in2Y{7ɠ %~KXM2F g:I짿n 97I00 7H!xq~L A3v\-$<ʵɸf|'OV~uAR1Rk dJyWKdx5Ae Y~x㮒t1C"aWDp ϸR>䊺dI+d/c YCMr˥mM@ReRn r.&5o:ب@ !i.!^|$N"g}sUٲͰH7ϛ̋sX)ȺcCh42,]E!g](] mSAv?<|)ט Lcge2.`;<2YL '&Η YRve̎@f?2%/Js fog$%wY]"}!uJzt)'es߉`祱 OVӑ f! Jnu$21:Î ZcŴarߐZ.0 ;ucD^kuɦkd?#JXZM;_~Kd2Gt}L!",>GS/R< 9>V/S\d?Pv#ܟ>Y҂F}dr|a[q#5&eiZȠ ֕2xo9.Ru >//3/.@#uO³.tBlR.IM%2B# (0K\q~[dIM!YIh\ e\ϖ?% 3fE= aA.]0<%v 'H0M4geKEe=yMřj͸,OdzV~4&g)OȒgz,( $,OMkcC}Qԋ2De E؀n|(aY#F^UoЙo2P=Hǎ7Aq Rs_E4M mL[_hS n`!X8&^NMK2`=^|#xNu>+L"y A/yF%E|0?-tx@˰x%Ni(L!7 . 2^sSQ(s0\3 zn?"G\ }T- 2rK䴗cMKR-HZ.:6>0#ٵi>JM["i ldVjzVqBNIhG1:tCfsG o%d7 '06%Y#i m2ۆ%E^O$^ש䆹s2I8뎉i\^C2':UL2oHLfk]`|gH$5qY7fdP] ?<$.(k$ R^He0dxuH#z.qmqMfƇ1< Ghp5YcJ")#!yl$ix4M)YWRȸjT(Wˠ'*u +ˇL.4W)[ e 3/i8r)=~we#vYBu 0 %K̋?™ v2 A,3.Rp%Rũ/RtDžqօCjsqR 4g^({_>Gl Bf]( $_ S6I 8`i 6LXrL_8S#!rgy?ɸ ɓmr&J>L+C|)P$NgQ?2]v0'=eaml񍖢~B>d7M(W,yn[Qywh0Ag>tb`-ӘWdTLә~cy^ͩx@'~gePgҠ 蟖e*"DAX!&㑹f44lH,S!&XC;"d2LI &^ӑ& Z=dl)T 6-m&O IM]2 Sqg1>MhnN 7ɀT q ʯx 2oF#ȼF jIc t˪kN+F5g !R9t. K\>?l~pG5p[ϼ"$.KhvH"I{aqAm,a7ʠN &M}h@8_doLsLX? q` / 6~w>\0Ɲi~/e.5g g5gv~MfpFKl) E=wlE;KpNh6m;UX kڽA*򓔲4I$4(w kG|%ቄuʾ~1EIn9MĚrnRʚm'M)owMٙR6MEږay PHMABKǑª0MX޺nZvfvtVZEH0.=@l2%Mƥnu XL[Ss$cłۚuJFM`}$zޯ }8 ړ7[G6މnۑm5 w#dD #,$a7a8RT)\NNm> ,7r8"KRi&r8mi7HX)B}v <|Q\j$am]"C'\LA}<I$HQi(r{ =,Mc J' 6aMM {"h(i>Q{[Qm 6Mxp~ĢqģraCmm'% as4ydhpL}9Aqe ߯ڄ #,D>7?O  ;KX1mzvI:i]n:w}}'v lg-4Z<3Z DB( V[pvM8筍h?xTDN>NƓt F:,4Ϳ㤁щD 4ތ ;RV< "kMXMX;kaұVG㉞0ʕ"[a|gD 7 s5i#/ &,ϩC𜬟ڄ^dqt]h̖EfF.ׇzJxC(֠8xx\!KlJ B"8i3M88'M)=n' T:80SEҧ"zaawMX[ݱTti FHPBHGah*IYaG:4V̫> ,p: s⠦Sq]/_a;"Mhט-^;QسD؇>*w_kv.TGR-WriEit;~aHnaMVV@!y<1mJik*IgD; ۧ _V~=wN6R;AlJ 9N^!O~v8ЅPѐa&A l98;=-Ecpٳ j8Q~h7mUHD$7.=4ݰ^K0M9H8;p|c3rZ|Ǜv4ONj?NX ^#'x_X'v Q;g/,x] !Z Fu#H~;N̿{I}U3i>O͔b3>o$ㄚݗ(|eIdh52u>ӾX>JDZ Uf_$b?^^15:+RGBMo5lhHn1K/*mjd­c5)k~k=Z>|AiYv1klO6_`Yr]FStume9F&D5s8$wڴncj}?`cQW^7y219^q:\jX(xgoUjm''N oJxk<߆iN#;NCaɂ)3B-̖!fl)|-I,V 6 Ju^7OI i% A47& UID5"2تR"CFId"(Ee.TOAq3%$8R< KFldTR)#.OOcPXqcBq Sxe2= S1ۚfz]2,&(1(Saae}qA%⩧СChѢd (muJ0>776 ?^x!Ta;wwǎBi]NNNNNNNNNNNNo3[j̖]^;P@f c20R|}nU6͓2 Z&(1ƾÕD*R@KTJfC"Vi m wۓtDe=z>S̚5+6m:Nig`)6#x4f|֭߆H!Li042*~k[t}8Ei5mط9l+A7V(Gg{0Kcc `ⲪJ># ۵k9̧Pbmuv8]g:;4N嘼>aZekYaJeǯkOŸgr-xgs%M5D4GOh ]z饦M4hZw|NVٲ5[zBA(xo%itYK1lလZB9J$Z⨒qߩ[rnڇ)K࣯`%d.uGPQEuDKIJ=$'ƛ}At`3gbԨQqJ`˖͘2e1Ԩ!OClCtCd0AHMotiifw\j9U-:^=&6O3X4 V=NvX&? U0璨umEeoOj1Jkjfi0Uc͖@?0hŋG Xz S8'''''''''''BMQ=fK󾖼""feD:/@FnHPu#h&?yl&rBN+YN~7x?gXco.+>3uFJ rA_ӧ; W6m 㨬p ME+0w8Jxr)IHxr,%A684e#Q2Åa#aG@ċ{zw7/N#^.%)ZZ^U0\ eoTf̘u}5=Q%eRcvZ֥4zL42( c0={c(cBgTH;HIQhBxH"3̣d0X@~ba"J)@ʠ"AW-TAx5+(g0fiBh,..\͛?)~/ >:tȤZ4\a*n3b8#5oꫯ63x.ei>oذ?J4:u{;VUp7q8V٢$M4̖~4[4[-S|Ōe@*73X(uVIDJx kD1hA Y+S_>A^qi1#$dï7&JٴYd.e0`ߴBK.W_}UkZ̖ǚ ,{WUUJ [uވeٟcJ85)ض"4[d]r;ը%-jĞ|"Q_x_ }d7{"Krس.ī,)^KA0{ 4ݶ J̛7O;mp81]fO>۷/nF 2$eqB1hx'|+/ݻwǞ={(-f/=.++3Q]Z/ti3[3[k5lᒮۛRs zlA|9z俽y /lL䠕S {LV%8Ze{>g?g?5?A^)h> & dYh1|-UCˠ^"G@us_ O%w?~\d%g*@$#(?{lJC9qTǼwppI3]xf6J5~Fb $J=<7Zz#^]=eiF:j92?m1 }ٶtGt]mRZ5-ڣ14\gضm}kؾ㢋.2/b|N-ۚeR5 vXhf?? p]+BB͖}x~g-]!SfKOr gsT*=V {r!r[,+P(ᅏC^` @Q_ބ6Sb}X7207gjk|E# 9Lj(+qG)Y۱X pDc9 Q9.|/]i&A84VރD >64<:('˾ވpxػ!)?-e8ǣ֡}&<~}XqI:%K6MNKn/ưaPRRLuV\{3f 8`O^v͙3;w4uMbi+<+//7?/Ç7Q3h:VXɓ'%>òe̱~>Elӏ;ִr 5(?ݦ,P#2]P׽@D(>Jd|A-]"#ٲl~LC|OCˑr>e(xn~q5~ o;;oϺ.GoKp j9Hׁ.as5+ &A PZ CUu=%(&ryNA8Z-RwpePΖf:cRBEYh{Fy|hg/YwQ_qX\91i 'f:8yYyy(PQh@豥]?r1/~ߛw](*s{5ɦM)O<ϸN  vs !ҙ-,3[(jڵkѱcGw_WS6W_56k#G4Ql7Z6F .S7{M*S1p0#!WBn拓Sc1=؍>7|c(i-€8"Rz͖ V%㷾" @*ڙ>K5r\3` Z&,E+k ,+?;yo+en7d/ O=G]6ǝ7YWʍfw(f ZZ֯[1TPQAKl@n8Qٷ*7Œի1pZ #=Rgp~s _fwdR ޻ZH$ɲ]Tmyh!3jyyX (=qH6 `z[z̘&J˴irؿO8ɩ1؍B9X++++ 3[ ^3Zb2.E Qr^[l??Q܅!lO/߂؀fiv j hk=(=p*k1HF鸷q`pTn]Tv| Z}떡K׭@A;[iC\ 6[' 4Zl;#;b:bã(Ո9kF꣣c-ǜJ-lŎE @Sjnh|pvg\ve&-ǁ}Q<~sYK/dfp -_bGLb '|1-2|f˺uL:ㅳhXN2ehL0&կ~eʴ _ΔO~GWSr_Ù-NNNNNNNNNNNG+uq׹ܵkIrB91f; -!F30# XEY{yn|^7\܃_-߆nyvmOoTo~} TcA##s#HYiOhoDGϣ⫩??>TnkKZ6(0{_ %3!4 /4?aCqp @a:>Ù$qBiY|$l s9Kk1pV>*YI| ~YfӇq =a-n:_TV_ ۅ_ p ??-ڍ?)Ţ|,gexD$Ƶ1fTG*px :ށWbs7c\-B X|%z+A2Me, r5Z͖wnHkA7lI{ao3c|oPzr1Acoo_1+)q8{ep3GhX,4(x0[6>6DύFc=܄1g7ZgY—Ta؂ вeKt"loaBY&4[V}/GF-L^Δa8\lPm?NXl9~8ɩ1tQܶv8-[Q=r-qzugQ)n^jnDŽ0_/6Ge.}gcv7Q[kE;dGA^1rfKV"-GeuJ7gơwڠ||.4zG*Kš%kЯg$4CD}VK>=?;lha]gBq}h6 FqsެӀKE_)5kW`ȰAU(*Gu+PTZ5*HYGKq(R,]~?=R>X9.lıjT,=ctEbGlY| maK9Ik*%նm[s Ncq=Euœb<M rw_4Aq gĠq2׻wodO,sFLVAr4ǃXꪫ̋u\)F2/q:i4}&%l+W0Ù1#}Ya}> B Gm2** ''''''''''=ӱr\ ؐ:1fK4N3/8CB$)GU<"aS:0KXQLrLy.B88ʥdKz=\l,6$[~3>7^P*DJ Ge^UCҎriF\PQ^U+֠4#^-d'R-w!FFKwC-egg$vm(]JbK-CH<4_O$1qsgp'4FPgӲ}Q%vb>-CkYaⶾeh><;PquQUf8!B? 2p@s͛7Јk(Sqᘒy6'lYky)Bffp6iDG2V"Sm .iׅIY4ᶔ@U U3 ?K`qTkWGCuH)WEP-VⰬ40ʥ-W,Dw{#a**{ e2[-h&XF-;ȊػnTxf_ء-]-1Nw,EQ'xM'y0j[k=DONqL:˰ĥǥ֥z$QPZ6-?<*|j*1.^t̘0CTfʟ'ƛß'z\Dc8{xExfsJ$xb2h\i .)q%6qΊ&e3^c3'#;R:ӸtDg9dbՕGQ&WD#@"ZP-+8aѪxgog|ej-ffKZFK-],:{816Dl3x H5N4fvMgZ=&6u%f$54[/+1fKR4[zDQ5lyo YwV mͅh7u#>]-' 8aMK")%ILAnqظL> uJNeaU0t9r=#EPg-,pIe3m@;+'''''''''''''^gpX5FdPeH~8Tr~Ts9pًi%2HlI-#Fg89999999999999: Qs"x͖-ùƹ99.cq^|gFlq3[tz+l9{K /C-j ܋s~qNbw'zo7U뼲;['''''''''''''LR?̖$|A2,- ڇs8w~PmEa5 yo.rf3[N[afC5?lSc1ZzmPQ~1BQ8罭(֠R<8cg׈hB徏f&. ݗƤeŘhr˳⡉sq~qj;?ۀz|߶MY-G -MQm> ٭F$dv1l1'8-hdRlYp ’6l_: {423 ʬ񸤕&DRAlVKD_e{ƫX>]M'K`s1X*yxZ$/ǃ|¸|rcg-gwY,y81FIe04eX͖:FKb~z~ ؃QؿBa(g؂kPm- ,Ã37%ZM܊wa̤xt7xxB~g~L+sp^ovQ_GF"X䵜6vrEV˱ȖSv3YqViҎZ4ْPVFhƖoZa}(x⛱rGx迹@cH:& c&=岾ՉJ(GdB4h$xBȬ ٦I"eB%Q!O#Q)GՒ&FӍi_BM/NCyi_b|>}~Ob1sz*߆5_ 3܇`OKfQ"xW#fA5JDd~MAAmL\ʉHٴNb;=ȅHD l!ϱhy(ŮI!&/ڎ%O1sq_b6~:&.(Ɓ`EDUtXqzcU'<c"A,P}h!f }"x1X=w;9͙?ȏGbgla9e90cP1 /MGFeb'̘3'bgْbT*$j,0cFLjYJR!gPIuH#Z~=n!gY:`3KŽgcUy%Շɘ1:e&&/ۆJw/VǦyt2, :r.Ċ_c]r.|F<%NKtz8-;P{^u5^ט-4B Fv ZA0nt?9~'ō÷bغh$s|پF~>t[q7cXz==gЌcjr[LDvˉE-x T0cȰضx(^=xq L+_(,cG/{=wH5입Iݞãߍ{j6>g'JlļgA.ŝO_F'nŭ-ݯ~2XB MwG{ƭm__ez1I!1vK%P)mDT1c.RԕCR]F|3CUY2vUHbXP<ѡhPZu9b%%JQ] s}[UNxeمDlO7k^wG-liފ|oX]qT1g'Dž4딆xx"49bwD/Fu *wk7w kbGnC8u " wivoǺR|v<Ye[xx{~*;ЯwWѷ)q6 ?98vt-z?m%ݴM6rVȻo6s#Q/@IA_c pӈb\O4z.ڹF|kt\׿<#g/sKدp! ䷞6ӐzlBF=dr27elÖϯ K?7oi>[˱:ÞKs#QG>넋Kף8?| O\O\=Fⱛ#ݧcʯ0gh7?ǪpXԫz caxws ; &)4OTJ+腠\֚n&"d@>e8qf'享X0+܉u3?'\ݽuc17XU(ލ%{{b|2u:.oap4bVXawꃱۤ(#^_wܘ]8⌒e;w1|U)KwЬn+͇o`"74x0v5N}<˱Gq߫1lZnZÞ_[?-eX65t Gi|6-Y=tzI͖RmC™-Q!9h=u>^/>tOqv1k=ZqܖoG1?=_jjͧ i1i9OFv 4d&4[ 1ﺐ>Xt,g}>Z[@ރ8w ^>Yx-B*K>Y6ˁ=/a#(OCCl,/,>ZȷhϿ-.˯peݜ}zH cVf q:. : M8EI >\z BUi ۗ]n~ v'6bŲ˱< /c]5Ո/mo/-~ qUў%>CZX_j:+'~oW>*,"Q-&,tAT~3 ?O_ҝ0KXW-gCfo`*/G/v???_y3^"}f/ǐj駘t1mZbR.L}*pƛ-l9-|%g%t4 -'oˆSn<2[H=-G"T1l>^Wg_6_}7z1.~s\w5fm? />/?4mfm!ah=3QBQaȊy1^vcے1x-­(^BOχZb Z?oƜ}|!T6GQb*6aAx1p[5FbmJ0x,X V-džTUqM}|t3[dM;@dv(C{#ADƕ3ևi67^ D0gGۮ{QPy]: LÈ^ }#PV=ok>}1bX3 =S̞K/;B{?-s( 56.]ۼ[c_>KQ_>ݏ7:_-%˱hZl;$6 y5ß.EV0AxS7`VU򳉡yϊ^g&LjNGa~9@zf5%nfK9q ds|& [ퟞ`~9'9mf-4}Cߑ_GVbԦC.~n 2[TBI?p\)H:P*:L|t97oy%`j쩊,´`>^v/WxxyC@U+{xoL,}==f/jw%H1NH _+0%VW)-g_oG~ uZ1|hwt xaDVbH|Qޣ? $xK>~=n^=.BmA|ط#1ڀR8" ?EZpi30jDfK?}Anْe:-1[1*fM݌6}"Ig-cv.nm/O@^ooA%l?7Xb^Tqݫ6j FvK)ddL)aiBi8K"GQhо0f(`i 8gv=/](v/Aトg^x{FC>2^E;aC>7ߏX6y ^! Cq> o9vTʀ<^-($-1Ljd~9"$3UBI,QfLD$Xl^>E}зtx)5kmχKǀ UWzu+u{px<;;-th6ljTo׽GG[]i/iOfP<*>#ۜq"ڪiHh@^3 S>{n@9͹ 9vŐ^x1fK`ɨ׺> èc#+lKΏߌ[_@&tSZY(g5r\NjMw;99999999999b:-Pd#kqVeF^ْ̖e̖ID4-)3 \<`x ~t2Z@RG)c=1lCݾMF3|m v`ܢhmj; yRGn>3ohBF )C&NA}LƜy-?ghAuc h9$߇⥟c܋cDV,d(z=i<Kfw`D|q5v?|%֕Eaa?*ϡx'X[_UTK8k7LhcH_uG`ЩxЮx9DER7|7|L˦Řc1_>xvx읮x}$>_X6L~{&~ 9+OaTO{?gy>}GhkXpDNjFϟz~#<CƸcgS0(%ORL6^y<qVő2#W[9s+7_|ȉ"?kP0899999999999oqGe --|ARl)FQ](E|An yo-G[- Hj5Y-ǝƌGFnji6ǚ涛U B9$wrrrrrrrrrrrrrr:u9CPnFRRk' mC{PfX_i(k(ZL23N2ڌ9}i-Z~GhVj468 >jp8p8㔦_#c|@J E|gK{{FAO6`=i1Ye%Si1YzZLLIxqX]RjY3NNNNNNNNNNNNNNB,6Y<0[Ԙ-5/݉>P{{DAk Zvvb֚}o;m"M&i6O2Ym%/cDx`qrrrrrrrrrrrrrr:U׈,RccY-Ae; {mDA(o.i0{U [Dnl1iM d sYvA[l-NNNNNNNNNNNNNNhf^ly?hjeZ7ʹMb7r6|i,gi Rhə- r3ZDAX*NNNNNNNNNNNNNNN0/ aClQ(䬖w "^c h5u &ڇ{'#Y-٭&|hdf"Td9ac> V +Tp8p8ǩEAe' h@{+p;kQ*侱-'mU{wOĹ9Ɵ< <6>:>4S{*}xlVX$Xp8p4kD .-:ezm9=6W nJ?sn%+`:|tiF[sƢeb,_߮YOv6ٲ6mr8p8q Sl1`b=Bd̖AQ49m$!ځ^Pf]W!ruZv6!DYY5+PYY)TTTFP^Q GPYUU1Y/G!ޱQ^.@w8p/iF}i0Kgp8ByOrt͖^P@fuY!,C[Kpԭ[<lө`_k_h4j`9NGZ/a^trr:rfw&fr7<'''''''SSڷe펶,.ƔE6[lSSș-N'Tn Oqrrrrrrrr:>a};]+9SU}y^vqrrrjj9JIm[ s8p_};;hyV4EϨtļh=vT8鄊'踮 srrrrrrrr:ľ\ؗh1'2U0MMYNNNNș-N߉x ބ''''''''SS lXDיf``,'''t={`عs've6;v0p8'?vnغu+lb‹T0žmے{5fI0iHL[]],sjє8鄊75ti8p77L.ɾ} p88о͛ K7d>n/Is1š5kLYZfNh*lI2˃(t7\ {P~ ,v܂6!*u]NKΘ-qZrKߑV4`\}鎇j*4SS.nM ;#7H4"H9RJ Ȓ&\XgUl!^> 1h /|`^u*XeVvjWǥqկ✜Nfw_e0^աBت/bˠRRR8eڵfY_;pSmlgéǘT>k;ґ !,- k_}yU4 ?ڲj+U=A鹮vPv<Vח6LvʲnNM,8d.ݲEKP~qқ[t]wV"h7f|Xxr@_׋U+}hZž趶Y^~)ƌ/}Yr-lӟdT,#H.]0{lSW*/^l]˾r=xh:*;Nܶ˵a]{_(R-Z:^[.}`>t5J˱3?\f>{t݇L[,?Z&nקLBiKg[v*&--/6RIA4nsնhM,%GMO1 5k[r9c L>l3g}Ow}Slœ>t=( !6X_CiH<*`nn;Q5XOCڸs߇DBBE$J)P\x l/7%X{K{|yT6$xe#/vhgPx=cYYY]Lu{a~140]j$ˮ]cAi~˥m 'Z+-K *2)k>UVވ͔z"/pg}NNNNNN'^+YNq/ŵ㔖I齛v=\{>1b,m}%:Ķh;:|}0/۬,v^A85-ͫh>J˱q&_j>WTGø~JGk9} |6q)Mq\p Eeb4b,W㺝댳6mڔ4HƎkKM0X\_{HOZ+*Z@Ax(Xu]Ҧ+òIE 4hO< VG0c4p-.}K3?Õ]a~O5*cT%"'=-W}$r׾ si#>h^HjqRNq4z˰nSZkux <$L v^ݶÙGˠt)m3f$1J=??Co :t2H\}vl桴%* +/dyvt~O:f?~8k>;?a> ].vv iyzPq1 ׮Oˡ4bckS_N麶'B.Kuxi8.);eh <~:e=9pB|f*>N'I4A4_}yStXhya'=7^׭[#F^CΝ1uT͂etyݷhҶD͖JTƪ1m~6p%~~6MFn)n5Y-g 9xdX0ʣބqQR~8P}$G#۾l2xI6mкukT^}v\gXv,s/o{Gжm[Sy9ny/Z۵~zo9tgOiݚ&(i^n"i)4xuQ !|p7@''''Aڗ/촜B4}cҤI˼ &%HCee:U:f o~1tPwad`57p[)nui^]׸~7b]v9Lgs}xĉ1rH 63?4n]Oρs8-[KM˥o4 :'X-Jvh+v^cot]a\:שc0!<:EgϞ޽;,YjMd uNQ4jiy۔H:E^;vh=1\< ovLmH;wMrY [KK;q렕sP'ȿgM@ni59-f!lƔMvf8)B<o}rr~qvoV.}4*//75ow}7Zj|@f^Uq;oV?яp73s=x0wd9\j!v*Ulq ,>0^D؅4 {P\ \״& ~ ;u_||i4dZ`O-hIJ5[؟CCF„_h2} )]p?8gcg֭[o/hNhQm c~F͡!C35j"L*y4qܦ4PZ۸|r8p><q#[^\>4vtd(%k0?bE5/а\noW^yG9z-L6|[i9Ai܎;o^DI㧻] W(|b3P,!D䴜fpvO+KHDhT )73DxVE3.l'\kf} , 5)B3oȷ+a6H̙nn蜥G+4]Z2bD!ƫu0m}x.Y3[/t(Pi:'l0B_O}֭AMEg 3%љ-vY}Tf d}3~ׯ_O}A33}{MҲ8]r9`ߓf=+nE3w1xr6nhfpepZn4}_1MweW_}ed|Z& SNKeA4/,K`ߊ9}X1|\i).Xjy\uxYLpq뮻xOcD6]J^yD^ZU+X gp tѴKśb^4(>ab<26nXrovj~A7@>2 )gBi'-}a{Pb^[xcK {z5YdI݄s_r [OA}''@I{!|U\R+&zyLx,h^p>u4⍙3Nhxе:o^+WςǏ 6l`:X3 mIS6lv88 yr:Мc|KKTӝ)W d c*3دƱ~ q2}MP~-Kϴ1}q]7*ui=35~K1-]qbq\cƱFX~6<^c43g_Q+٧ md?c#;d=;`۫m2.}elgAռ>(lѾ&g8psϖul{\aZPiJr&zX!>lXG\2q~NvɿG=c)5ExT ,{gfp|$M-ɲU ㅍZNEG՘6E:c_~%"? !`=z(1޽{Mlxip //|ȸq]߷X>zr<g?%ˑa -m}f-}nח77څsh3n#3_[-7KqYW7/ܼhFUJ/ɋ)/F_c9o cȎ 7܀-ZaG  fʔ)~s ]n7›:r_{YW~|.3o&袋 |~< x aVΌLaNNo{.cnnK ~̖ oYYY8s8 5g89999 b}X}( w`s0QE).9YdK1.p͙fWfc`:/p>3VCA~=߱5:t0q+x̛1þ(bc]*ǃ& )yؗcf_.?fF =<.\gߒ }Ge910&Bǘh'|Ҵ_q_,1cvΤg/nr d<?d9l#Nz豠lp}cWzf ě-^8˂Z^^zhpnsM-"/bpGސh\0ۼ4Pq_(.uRI/"\FCf>Ë/tx/RTlnk*^y!}饗7K iBD7"{{!T%"l>5ϭEѓ>yϮDqkOgvgwzoO݂wۂ؍uY_^@?w2T~`Y› MCGsܹo7QLjF~uȹk~ 73^ع_4ف7!ϔey[nyuOe%q3[4o rrrrrr/Wa}RK9쟫¾(/q9g4{|Y73q#!EۦeӰ`,w?cm4#ؿg?>s|t40L_H8^eߍ qzhj1?M]Y:k{ 0_?묇}=%rF5;/y0˖.3MDŽc*~Vld^—ϘǏy?e:~|$?w=)ϝ8aLKtuݦxr:!G:wgbNjUIiD8c/ :oొeƋ,]^Xxmb /z<61- ^x1@/hxҋ.nᅛ ^hR0 /<<,eMc7]^@W6b=Hy?' r~<,ٯ,{n?ہh7~1(/? ܎ ?? z>T J"*uE#lK7} o/%OKG1. GAAkDQ| +Oq~k*X+Ӳƾl7+4 f}dy\xc_}h-qǏeOhPuoGÉ_1=\gfQM/% g[tnǕ~6rǖςNg}L hp,id8X侰q el>P~ϛ{eh8XdC-gy MgDG;-pc)c/~ty 0M^Tx//FPlu›J,I^/R\gCgc5^x)p,h 6ā_ij v"~ !4_]=Nt䜜N/hp65 c>\~ "𳟤)n հ >92[m!&쇱 |fNL`4.V//ip/ž4WxX'ujߎǖ&oPi.?q4Xa^~~b?DtfXKAE9na_P2 gp?e~Lza21Ͷta?Ǐϙ: QDV|(^sM3>hvŧ) 3<1`~o>aZ=Cp4-~x>0 _͛;3<=)ulajϴ4i8^0L Ӱ_:+bh&t}{دaglٯ@} }a?0Фa&:v.YjbƂOiF,!E6Ӱ]j$8ncm:P1=4M h*8L]la<ӳ_cvxlh*0yilOǾgz@13_ьpf 0F' #~F%0~ voL p&-l?G;4Lj_C_2N u؟5>0.cDC]cDCu?>^BLw?JMKGn_Ueub8/}UMO"]1J5e4AۦeH7T^0v:m#Sh$e1[-6ףW◃̕_ %yw#vZ2?>_)v PTv &76M#7oH~f@ x%4FhЩgv7BN#D4Axf& in?7LFCӅJZnmQalj^~rs7駟6ra),8H6aoa?Yq79''''M#Cs}>ue\Oh_c})u8g>J|3ipw~#1㘁fxEOڻgٶ9`h\݄P~Y'/mWw0-M al _?2}P|sd~x:Y=<\(~yϗx03qFs#~%ڧgx9~('NGO$z:P?.q͙8;=GM-EiX" ;/p0n[cļZ*(ŘUjMU#8]io;;2Q% g’* (o?.FGo?7$~č؍ߍcǛq҄!%#fO~.yvqob~CI_A.ޘXo xG3[DKhx5r\_Wx3v|tBxbx+ѱ|FW_mf0b{\a}ΥOy R.֎nrNNNNN'P8@)aK$qbߍMn7F- Ѵ!9C}:NƳ=ox3Pb9Ӆgp 7cqqbZ$g1sF kX~>̶qg9ӰBcY 阆&gþ*/l cH+Al/f Mcώ9^GcDuRI&>P<9; ge%$["~37?KlzK9^͹Ү];So~S71;?|߭ȟ早Z^X}=v/u1‹zj:Gx]œ.-/.htŲ5=,CԶvZu免0E i*i>7*ƞHxuqiwe,2ɔVaבrW`wYŤxC[B/֎nrNNNNN'x~9싰񺤘~ᨲa߃l~AEp>ȲO/ke]_1[(ž_0r?z Ƴ1gیg>Xi_uiyL<,1ƳO&uǂ0ǃgiFN7A%M8+à1^Xs:6rf?*@4t`׃hJ('ߎ68,)]?׵N eQƥFeU6Zvfm S4\+τK mҾ?0]v>.⺽\aXD*;^׉}4T%늦SRR\6Dy@$TS͎n>]o^+6*Ta:E?v^;i5\999999Jޯ.]?.)/p3=/W)|Ol5o{&mR-Zk!vW1 s](L DuiCiۨ`6Vi`ipJӫtNo4m|l ;omEyTZ^peHuGÜ1[r-TOkoj4Tg}GWL\?&t{]pO&*ِMR?RY0Rn3]Ղܐ%iOoR틊z Oj2(~+6& /Jm%e>? |=#Hpo h}Ɏz0]-a eiNvi 4=8B]ps~hEFN}fB]l掶,f boLYN_䗁Qp'̖lqJK9ss~N=gؿ>8#v%2hPm_j/t/G99jrT8ovf+5[k+ GcPvP#ٟz?Y-wJlIVAu'tu1RlF3[Pכ%ovt\2p8S}kך$fߏ>oӴ#۸qy,=]EEI euq~зo_z#HrdQh;[yfKa3[;[NNr!r~7Go99}7Ju~{8JuT%%%9JupA1-̖fKkDjf Tҋ}1ՙ-N'tF˄ xꩧ_m0-׎ \)(=_n뒰S{nԴ;e{oСC&黔}KBCvΝTG 7aũi>$/ _;[ήΖΖ"r\l齭y1rzwb:95.F_p:Y!?:t9s4Е+W^ç~BB:4N牞+NGzu7;9}9i6;ũ:o޵k'quo߿߬:ڿFd 7kD;Q{ zfK벤.''c}QqZ{]7[nhSA.p#cEUr<$/ղ-EHUPŁUܿoU'*#ˑrr$e>iAUI)9W9g'A5C0/" 5!x L:,>- P$Yf)H}[~>~"[Cl7/A/7Ha; OGտXs1qe/ޡ֒ėMwlzZf l_GYEߧJWV}H QvČw oTj a?PAc3xH.O6 wN Nג-62 LFR`M6Ղ`)-v@Jֈg/oy/Sd+|0 H)53˴a(k%a|EzĎFEB5`TT`4R& AČ3&Xt-ZeWT;h&d(țd׌O$E~ _G8 ,^8y?;oRɷ"!*ғ(ϊ Q cc>x[q0ynlFe 11 :kս[\.%9ۇ|, Y~z殬@1?D3<4#N%OҐhScF0e\{b1krF"͜2DyI94BlG^e Wqm,#]ayrdB55G#C]X;k%~b?ozz ڜ=48X?+{Ջ>)-O!ɕTS [<2dM?]-ίآ#TJ&dH¬aIF1JA FzP9;vIOz. "RATN5Z'OF,F^ (LELMar:tj$v #*JG NOibcM6ZJoӔv,j ®v۔ˑ[S/T-5l6ܙtz#AԢ{4C#(A#qDqGCWȿb[0ŷ8ɐ}kz5c?ǮGV[`,PYNB`\踄.s(Eyid?%cCI'||MдRSǔ5~ʗP[|'+p2r^keXʩL/pyv`㛑QrQrSuKWݮʑ)뤮B[:)ԯxLՑnhhbܳ> g&0w[_45I/%aa`Lj(o[Fad8n UmĐ%æ׏oy VCJhͮy{yͥʗ#(a;^4v`}@r/>¸hwTy7?aJ~7:ػ&sybd-[l&^ ZL1%_ 1G#cl\;-JHN;b0䤆H1;N7TeEGfHG FB@\1%#*XAH3RL&lIK}S APGZOCV$DJ1{_p\ #4MޘԬq _%!a)FݕKxv<NvCWG1NZE :cd&@ixtE9iM%4r ۟܁U9%Ge1*j/W߅G_h[ʛponW4kpv4M^ⓥ8_] XWgV[`7d\2dTƝh-{6!ޅ@F|ȁK](AsUXI!|h\6a֢XEE2ťWz%X 3$ c .\B`;.VՠDSx;0p[#ؙ(iCSFiP9q,JqilEׄ&yz K.TuJNb][Ӌ8X߅gaWaqjk:t1<&td ;؂J9s #BOK]=3X;cvw϶Q}P&jA\ITFڛG_h~!r1L]Ch 5 ^nĢ8;O;h(|17z؜g`X e 1c < uN݋3=k_¬HpbL`9$='av^ٕmvmOĖȺt;Ư~wZ.\g.Ub4dDR};X혽`=YxCNBn5!7 f?nƚb,xp vw[,+?Lc\@+I*0tIWEヨ=s ى(۸ k Pb7֎c6ڃX*Rr^;3vU o0`#dم0wZ|2J֛}WV[|l#o80 I!9{1uC8'۷@Yk?xdf1}\v;*"o3 6b3- θǘqQ6c~ntlr5G ݎk-/gM6 ZL1%_EѓG-H_]Up`"?\wls"4f {`n+FބNO5>w?#ՃptHnP"aﶝ$.cH;S#hk,+A*2N;16dӍMRʬiDb3PS[4= B<9-څ^.G4чUi؛~5nyF#Z"'LI3#d+Z>@Ke,p-2 %Q p﮳Ƚ22UM-]g[BfM-##4ĥ<6'M2nڪ0:arڊ]n {P'[ʷYӂe۱1e}N8{[Pu ta@jb:p\,(~/۱ wN }㘳jFOi_sfa9iՅ8<.0,^͹ը0:?s Rڇ1n@I>n;NuX_F_Sc;pB6RMɞ0\*/یme(bgchXA-;w'cMI'e AgiQd o7\T})5[h* v-;ES.]BA}/W~m8IQqxf<:#s0 4X9bۙ.:pkVŞ'Fx4`sa+7h: K~eG7S`yB40Ν'asuhb{i} *"GS+Qc][j#>Z*署! 2 \rX: O~StÀ-HCMu\ݛ|u^<ܸi$*E-}l`-f" l9?6dIW )($/ЪL$JWщ ZP︂m# D4b.ӳ7'w-SO}MAjI7'TP\}Kf"w [q@6űAG;?4?|b;LoÈFK#HЊ$N4p^\\dM7:]K)3`訹ֺ ]˃"@"i%BAxF[ppf=;*6КHIB1ҜVݻF<2fz4BGl{z]FPUP-qωP1=*BA3ZÌu0]Lilh#[ k1P>>1kY$ƞ5+xY܂_޿?1n}p-v0jؚTS:!$`>2=,x( ?ցnwv5 q6 )M# D^#oHāؖUtpuaӺ jUg;)5l3XJGv!zZ.`}[pG 'wf")Nu؀=86=Kضxs3+'V[ϢO/SR҉|F,)r OozulH}[^ .g3q$iƮU-m@|-UرW(XI;JQ:BcX>4 K-(J}wOYM{{~{+Ǚ@6t~<_^fKv~Ofjh[Kh흀dxj.fiO#n fa֓^AQǪPD=ls=ɂN%|04Y F%[``1[lUSlI$Z4BD _ś|(G_}r.6lkrrz .=U.DWP|~|724-(8sdx @&vV5k׼>*rጲ>thQy#z0L&ސt-̬2:B,0J]̌Hj;`'YK9Zݙ;3;0U#".i8_iH=[+Qq ۶QGށԒV4]n=CF̶\<~ Fxz[P]zaS(&o%~ k`PN#HFfA8Oc=[pٍM kCKY)>e(F|87;3KQ҄3ʱm:d70B9S]'QG3X6<-hh~4 AcGRǘJ-M Is`$GGad9j?U-P 9`ǾwA48uacQ .t]AёZxY kGSs707"ΓM]L-ڎ~ưF˒/ڸamu"ow ߌ[P׎G4Fcv)]&? FgMu*gi@}K;:` _c劼b)ϴэNג-70C5L'z6\^-{=6bM]-ίآصA"~vbrn%8CXez`3Vdh8=< oga(qwaIym8':.a[pkD?NEscsXu ֢vk,q?"<VF^k0mcMoDRfyX[?6&Ю\}$r]zګy HoÀgGVgaw/Ulp.699J-ͧO s#~{3 rmdcgKp!wNؚvM<6ڃ=}hu̱# i00g>Y֠QmlG7S`K47Ʀ2HE5QG!tS84`z<ن+8 Q23ҍH)D_l}m%XXw!j#vΓpk3c][oZZ[:E dFW )͓W#cNlϺ )|Jw+ R0UxzL-م8t]pzcu :TXQWҐ76X?N=kx‰p g3lߟ8r<1)f^$lzҵ2˳e\[mG#ñ8}Yyqša8C8z6(i Bda <'FǨ-XMwi1+vGfa6VK3KOc:6ԄEe=BNӘrXu̽UlQ.8j7'ht\c~ )BV~1\T0ۇ8B~++81bYl[yUnDюX|i9eH?|;c6=:P/8 p1J9YV'(JE-nXwVd^G?Ѡ?w,š=/EmaVmMâ]엹8tP{nۍÑ#?)pFPp2M10oR}%x?GКO>;n4 "[2-ـGqH)vޅ%ϣkݍسs7ߘlr&?fn^7V[f/~a11E=:S%Ƞl9o69'.1 y[P{gma+vq9‹(ksc~4@eMea |Fwt-b{`M6ft bJ:z-T8[tmKEK)j{rTw̞ݒRN`B^B/G@yFcoCNJ1.9ǂGp<.6H#D$Њ X ץ`yr.֞;|DF|ES_dM/FRʦi+N)-a_bQL~;tUHv~\Ck0PuT:ݕ#q`:oڊ4;P  `1lOƂ6!w| ˶)C1fՠA 6oc+bCؓw T ndYGV"؈lI|hž t]V!j= e ]/)nW=~ Poݘ=(֮݇ewc,<فQLјfl+@ٰnIZjQx5.He^\Dh4vCx~ x g ܆TQ*l܋9y`GqA>k6ec[U27硬յhә@Kr$Q}7hmlC{NzLKZFJ>l>5d<Ҭ]ݶkVnĪX&\ð;žrre2*qm|#&k)%q}Q萇I"X XeNje[ vҼa8=.cC\R5A]ZW`Ɋ]1[ {nl>,\ O G}7eM!L0=#Z f{ljA0Ŕt~Ul!KQ" ;R%X-JڱC GiM#=Z@!#TZN rmRd4 Ja(D_40ka2-t)pƭCyWIUL lɦ7]K)3ӈFF #` Y5*o4]/E^1QpBs0)Q{?`c1TaB!M6uab1* 7i0>Y`xo*{ҵ^`~+6PFx6.DDKab#_k{BM܌ڲAiڌ#MSl_#2yo]<(axV>|'dT.\TRl~ kyD,+^sE=۝yH)l>cd9Yg Oe.~LY$7*{'e1hi-֙W甬U 'ɗ* d3SM/lQ}YZEg7Bݤ0SO8 v b@2ER,MV*͉uK Yw %e Hݠd<ս`(e#u[2E|m!s10#M߲ yA'  x4y͆wt-b-6bM]-ί آtFؙEhyQ6-MQ!0zH`O0l)͊a(uS$1˓[&G lSEhꄃFK1qŤxZ*M6F$)BZ Wv'}Y|A9"yF'2djk$1̗}|7N#(.CWF e [y2 (y6ҰKKLÌB2|B3~ahzf^ת)- Zna q+2F -+Y:d{>O!pDãp (6ێiă>Q,"p%<+J(n8вjFb"RjG3G"*Y= 0^ERy0}@:,[ B!5 H>Xefo>u(7}-b-6bM]-ίB+"H!!AjJO ?NaꬭVTz QH032LJiR3X; rL%(,ځqExqIzGa~Jej&lzѵTTLcWOi3w]A-~dM/i ZL`1J73". )}ȫ [gLWIˊkսXX!Eе'S44(KXI1> Tq 1q2lΤ6M:c s2k `m#Fv3K(][4$LQ8s-ʦbƃƔX~U㻚5!0%#'̺q4y׀.U3|jz({0/&& ї2_"$o8q#c/բoNy0JV{~cnFk(iD;ӐNP?FPyn;SK٦׆S`E'"@d"dY|Z>S2+aǦ ݩ{V9Kx2.rhSߏIJC10=S' +o6d_}L_,;2̻GSǀ/F6Yˤ]YK#& n)R@KoK$ٰr뚮w"clX`z :=C[E W]),3 9(G$izR;$F"o-^A\qjæם~300@n^Ӿ|?ֺau;3ᡶ/%G?wL{c=3?nhcT{< l`ˤWK[WWZ~nզ+xu]`P8c$C)zQpwEP?FW8DGUM;jlA(ьhNZExQ G5{|Y $eBTFC2-&PL0a"OĴ0cr)~w b!/9|(fy( Y^ 1j fD ) ֛w9 `=IaXW܂OciE52ʟ}}j$^M1dM6]g$~7u< g= l1mzei{S pxA46\AG<5(<ϙK8C8Cpb5Df֩ k_ n|ָ`DrU(a#ƽF^w..'2zP%:(􂊓,EˢnTSqZ} &C̷~jF멳 dMo:^L鵦N镤)3_6<<<}ئ7/.Fl啧bߥ9_/^`M{'-79D4Hcqc.o 8G9fG('^"pЇ5ğ[?]ϯyA ?|/#lD /3# єu<-ۦ:X{i~yqG;F#x?}QM/xx͊(c"p8)1̱ jaSSTlz9t5#[lɦ7]NFo{ʤzZ!{)ögMGgMפ7Phr͖<镥)Y2ES=[nT%* -Ɇuq׏qlit`1|@;5_Hi:H PZ_ߺ?wtw`CCȝe|瑧e3&P"0bv> s'OLpAcxm%F5SE/ʷ?1F%GG vPQ +ߓ#qgd>L/k Q2 b.p"p f0mz3>˦֖!aMֆby: NW3"]Om&lz3 yoyJٍLS}BHl^xf3lz!-r# A]u 5E^"hiJ叠…!U U?Ҟ41!Vw'am]0^.13-Hk\`Ogwyx0;ޞm (u,ohD ስ6e 8I$&x 龠 Y`F=x/>eci,KY^(82a:J?l"͢>DTG^/ \ %&=XV^1ME2+fӐ7飐gL ̑ C4-$ 1>xF^564Aҝp"XeGC=(F[.<_ c*u#[N7@%J3 :<x b !wxqa ㅫ"N,x=V-AEoyX;OQ9 #^wɍO3أ0!F0RO?;yN{ ~/Yuf'iǦcT5p5~ocڣ؜S ˦EW3)F dMo}M,XOݻ&^Oza_|6bdׇ^XW;)MDqmf3?Vgc1|ЏgzT>օjBJy7FYkИk:P A'GޟKO&c]q/^>ZDS4&AyXnfGM&_’{%r#~k</`~rQ6ԇMx ;5'Cݸ?G~;PG06 yzjOo=QPC ~ OD`/N%-}>1GcGV"nd/}/d5!|D}Iؙ{ m'P`EȪø7lpΤ>x>F1Җ.%1l&lHj^xo^=za]nJ6~uk7`42K-la n`"pFp8|ʼn?Gţ.|279{RٝUXw}>],E&W$QĢ#H_>ϝhD@勅XN~鋦yXz%h`}32HJwô߁+܅X[lh@ƅG 1iZREf#h+ߋ;>V|4]e[:y5RߨHm=|32.rId?=w}kv̬E[?JG\Gu e{'}}?ၤ|,Ǐ>w =W1&g1~OGb#\Bf\^M1kL}ȿ9M6dAW+aS|ѦW^Xvt}6d׏nT`7G:>^^'޾goOSG#xۮA-eG?ޖ#  ޺o][z4+HT/AˏCA/é1|? MſO[V /#vQj#BQ $w/37Rp|!7a-/A_ᖏ5Z5A%݂O~mlzY>-fm#0z+m?nzi5m3wymBdM6dM6dM6[O%fY`#/-cŢp}Ò{F7>wKu{n|PՌ# #ֻ̯Ÿ\=V!xb>H@??|3Sx4xpyĜ*E&0Zx*G"a>8c1?~y2P:܁0˟Z#ւZvOٗtޭxd&4<.'ELymHŲsQi+~/baZ%jFp:uǘ7h< F;P|kɞ6 {YC5Ǘ(nNE#vW E ՚6>B(yYnQ:Qn+eOM+-NyAދwyKm~bM@ږ߄ɬdM6dM6dM6M]IDAT/ .xV>;Ttj 4: GMX?1qZtEh>-s1_{ v~[nC| h3Z}_ ߿_#5_v,[3#^L ՠ<}n7px" :q67a/eapϽOaAY s>QKggO|G>W("j&\9͏'M?LcӸ8kggpn혛|c<+> r~_5p=~G!ukEяn-f&|-K5آ6J, 8}$nݙl{.PʲM6dM6dM6d~/{ѫМ2:H2 Ұ㱂F|hy|w3V6'8Qwo綝Gn Z2Z'df³<ţpwUX8w-~ -Ăi˱ug.;5|)b ME㌵f{6ΞiF/xe^8+,Ē Qu^@ʽ(@k!Oލ=Ga㱰Y6qtW!{B,ZKS";y/J{ !Е3ȜXO֢eLB&쿄xXhRVu :1[K|<>[! Ue`?…8RъVt|[p6-܋n8o[/Է_[E* OYP?B6:7GNCˣE-ƣ|?&"l&l&lɦ7߃-_EFBR-r;{un r1@8+q+`-p(ׂ 4.49#5uMxϬ_e"aD|cp Fo^FF~kfZ0@j(!ߡ֑5k+(D~gDA<a8CL@Z7fa+&$"+ o.|ijXXvYQy !8qx (| Ȉ0=M 2A2w"̼dM)?L 2my13 N &hÜ)z/E<7;8i^+<` G̩uRc剕mk F6 m놜E?p HCb4 ]H &!1;xF?-.yM6dM6dM6d,l|p[޾{[~4LcXELD$D܇(&!4@%Kht"D6(5;w@ N`MxX^iJQx,`S>}2>Vy7LLy5\|=X7{2њ0Dq(Ç[ )4=ȐWg.^ ?+ozh7uOe` Q&&B7/1uL93Ӝf&1w1k!6iҳilaĈɛ)7}HD̵4iFb(dĆ5(H nFm.UX}cdYg} $}C֚P`ʯO^G<yOo©^ g2YcpaÈ]¹ << dyLPGE2>\mbq0)~C-o6a 5m` -9eAUnMT+ve\f[pU&|$@&` d?jGtgf =sjg< Ek  (nD_ fA<P1<ن~2#hjGy~zf#PN:\&?gELdgEAk*(ߏ8|zı5l̢|}h9cg1J~/qA֍cٍ|ddd H?<BB5I̖\ꇼ3gK2Ϡ|]ZS8Cpes5̈#aIFtY#qŵd_3q|ƂFBG kxĜ{Nق$ݲ2cMO%0| 7*0GV@C6)ш ŵ)tښ"Zpn"bygYCP^Via%"ČA8 )A%(kj--5⥴"TRTfhY6qOS3_M4b)Bj>wc"l#GPXt-QFPC~ 3Q\Dfapfwʊ8O\^?\{r!YSq?d\̻%Ɍc VmZ]7#"HeB6GуV1r,'}}=ĘG @ Q8ͼebM9 DKg/U6`:hD؀ξB<`ZMHbRS,o<| qö0iu\#+nCw =2؆Kȝf|YDE*:gW;3r 1[jTƇXmB_^1(Y8TAUHC6dӛ$$ӡ^jJb,&-BR$XC9ae^=As;j*8qxEjd\@wH>%8uyAe4ð̤ڃj~4 F^yn^8##_ê=D2ͭTgL*$LJqNEmRXx4h]H쟛սiH߶mgr kaJ`i#\ܸ~c`HơrԻ:аulEӄ 9c9r:f-yZz2k?'oɺ> Gh [KQS\Zou;DŽ/BfWbmmM6b9n2 dN%آ5c[.`ߏ=w[2)|eY :0:҅l܈ɳ4 9PǨPIFxђ9nBɱdU?[ő TǓ?)WpӇy8T?L%8Dž{[;inOWkzK"Db<}=VĹZ44ta4k<_ٞ2s/YSn/ܬQGf/wkl-BY߅Ǹiա/Tm?%# 'pbvk؝O`yF9*. >_J4OPJuPe<R-܍_~~$h&>8g.+a5 }q'*s3qt%Z$4E$(؂B?% O~M2u0';֌˱*=c ,mA-(@c1#T'X{Fh]nNW ͕Eذ}~LV,%F(w¡ d,{cUx|4 Ӱb\Bi83"MΡf\,؉+6`k?aCӨw/Aֱbq%T}}ZTw Eر|J=Jހf4;BEzz 6FD=ކt4 "_KϤrKƺcyX߀-K[=O/_˗`M8|ف1ݗiQrD惛P0."; N]E[/ld\7xЇB/Z}٧Pd.xp|l$Cec;Qrd%Bh݁I<^+A ^9CcÙn4u , ׬¦\Tw9YK-]m<ʊHf\DؒSU-F a?fgՓM6tmzq%g;u`M6]Z-$l|ݣw?(}kp}޸։R\6'åOPkPGvF9j[cZZBz-@Vn6[ !0с֒XNMߡ ,;\|Fae v(B58t N_ՋRN> рB".埸'p 6T:ۇec;ځN7A y0~*h,ErzJ`,3x+}Ge7^_;`w]U'`JƢ;bmxeKS=pȚ8ZPW]-&%4 ՍJ Xs lٵ/jM_Eqa9./j 8Q7q<7gVm߃]H桼yqւ<5^ 9',u`?':F0Rr&#ڄ]Rg7+]c\u5T Iشq5l݄UZ\Ae>Z+~{v؇lӷ26bM3]lt1lɦ^KEFXT0_:n;])c٫' p?i R$)ĩPfJ_mT^bh؁Gtv =8f1$whJ+r!yi Pfؘ{\k$&ӋxR9Y˖-](:CSw]+ގo6]@Pά^9>;>'}7s?Ax4|̳݁hB߼@ uiFujڍȫ#فmLQ\.49y8]ߊ";[K97#ދ+:;U%8Sը0̃`tl,@ oێEKQVCaӜf 0-Pa*G} j4-U`݄A֋`Yx2x_мCÀ{Sc 2^WzZs}6I=c8|0l\FA]?\!t]<=h>O;2p$N+A-߂#k6#ТVD!?!.rJ`K[d٭)X|6D;ΤCޓ`| Wf8 VlGZ1Cva!֢{`xRi| 8EPk#̾P1,gٗ=H,rTdnCn8<}Wh9[* ,Q)QrP12WGVZ8t I6u/ ZE}:we̦IdMפlyb-6tk (~*Zլ/bv]_}?y^ØyQs?O.+O,QY8UGS9 uLT\T2i@w"呃gV,^JhZ*qן߂ufU.\,iݡ݇|pœw`gu,-#8FگD ?V)J6IO}ٯ̱x_gqyJm-x䦯ac0'OaƒBTSa?]<هVO52gLWE_[Ċ8;m3rO8bίy$`7Kӷ~2.Jf(xfw'Ѷ .L,|]ĚZz Ga7Ng(.!ϖ!a p,[k JM6i#&+0S]Ȩog07yO6\)@nN6NvmAT^pI<%LhwJjlA8epܰg1V:aN^ŽQ9肋I&mx0{}.GI>}cs ]SN䂔(ߙԴTڍ-ub,s.7>+1ʼ %@8rM̳EuZ"=g{"u^TYSWc>?e^MM zPo r_BV1FV!wZl!m^Nڍds,%? očSE+3(hFZ%QNH]آ;7:" ח[Rp8.a/эgK vL_SCp7^lI:aMd#oñҀR`CͧtvM`Lr?@8'75!gv:Qֱ.\,uyvy(/tv}g\BғQp>e߉ #{F䟾NK䁦]Y'jg/[&l_x rزq-Jt7}VI-,$F m WVyŔ%)ac3ғVLV2/]b`Ͽd]XOֿ1d.U뽩p2Ҭ(b*s5uooB(?&/^̦׌&ߴoW˳Ej|<{~KXt \t"{>1|b7rd ̟雦c{nس)xn3菆bm\Ŭ|DFXz~~_!{ط(XӾ8:cRYH `3q˗']#!oƍ(ӡe7V$|N=}\ʺj݀K#M0}_rT;}ʩqL<b:$U'Ocb]#a_?.|!>4uI7/[,Eάxsi;]< =RQ|pñ^y{<Oa?J¶Z=&ocQ Nd%~~/棨E:v@቟.Bu-,b-1lO6dӛ+5o/Dnܩvth@Ϊ(:^^ф*.}q?j43*w:tyt ׬ڋ-hi>RϭƤ<k@٢ظj1ooc.CAi#:}d$_,p}Ib7vSdkB뷀; lyG;ML.=́RqӮl P2dF) }1(Ba/* eAgLyG; sEHLjCr3^2% , 3B!(5@K$w y @~AeSLK`RCdZNTHt3S:$hLzq4fÌϼQv)eD !}'Z7ִ]KUok ܌_Andg2u/ᎍb"6[|o_bƌ= ^[Gn߅aQ1O9. ;Jq{uc }?z/>>{+@|>{ ,SѶla|mjy'b43jk][jI#E9RUi}UXq?| g]DouEOyD0~i~+_ן{3,a0X{?-8v(>5r.vRa4Տ 󩭪3&޴d Vp|?1 R>'/c0s#x{&UGM:a84z#iNBm"Mس|36uAEw#=h;Z}L2=Hc8{OӟxO 3V@ ۹w#x≇ %-!#; kf='iMǃO>бjB)RaV`gJ6JI3ߒ\ %9 2at4 9L08Fy^ )_=p(Ox>0UzZ(&<.T݊#P6L^L )';/Ǝ(][ w4Bzb` TD-\aAIًcԼ}Ǻyi/26Hy]8g无:QZ|,0cYiH::lG^AxfSxއ0硇7 Q>dHKIéuQw=y<6cyjGCSuڴ!4@т`]l߲ӎaDV;#m,=u[2.]A8[z RT/̇{&M7r r#-b$@ 4+|xE1Ȳs X~ᕧUe":WWƼi((byk.n/pC9%yd̚Li/8 qϗ8ACgQrX&]\LL+DdZMh$۴↘aՇPzZ|UkDL:Vc_y:g!)*Sd 4=XU: pdžuL]~6n-n}_>9|9T9~A/x[Ż*)t4ލX!9 ?Zʫ c]Pq7M`)ZZX?6\a爛AŮbyYgdMyoߗ roo::Mu >b1R cnř׍A`s=A^xG1CT~qL 2|tuM^8C~#mG[S3z1paCa}(o/4B~{u14v*R{0@Zى!1&1ZbXkϴ;Z71F{d566t6q?^'|n,+=-dMofߒfU y[$08F{06:w#Q wCh0yQ ;6 >F{Lt-@>/9Ckݽpw`1i M1i8~,'xl;^f-.0u˜ӽIKSb7ܽ^xȧu6{̻cl#cNꁃzo>V1C1bOES:ǍNס5256]p?zL] %[zP;8ܔa`q|u>sd'Έꐇ_SW<&o| cc#l!8. o܉Ojǀ#x0>ɼQJXu91ohp|N/)= : :aM6]^1L#[޾ko9HQ,R`QL8Ωv:Fx y$ ɣxmoc(or%<|L͸L$Oxbgo|7o~㻸c^t(o=>}T?R59+ 5%1Rq/\`\*?+=4>ʈmB#]SdR ~QcK]VJDl6?iBJ A_%6~~·u  ~(RiK?ӰO ̟{,[Y3 ̨o}),/kt!EQ*WCe"9aQ![i_. __W0*?RYLW|̷6ay-"w(UkU h$%Uy +,g|Ȳ=Pa 3n-p)92_).)AuTlIAS:M-F^iD_|I#Z9LC1BOexC")h#c\0d_H2&Ȳ\LC[8냲&`75V\:!&9$0JX|{=2ZHT%_ݸII*J0mҙR晵Xxy-Ivkꏒc+'8ɝ'} "r@Ys7gJD_c9{;.܏۱r3hpg7DP}mJ!ÎhF=gWYS~ h@'0;0m^:r*"BɎض9_[m%hEL~7P˯ITʼ|>*..!*l;)0eP}2@RYQv DҨTu&qAZk6cXf!0fF̗2KiW`yzw,8-El|Ӵ5 "EP qTr"R46vQ}~nM6gGH!K W4|/![|xYk x X(cާ,  IWI=>_cFfA(&}JrS`f=(?OUj -0:KNY)zOev<*Qg~TǦ)hƒA󀊩ɶ>726NeM B}**Zy? `%x%Wm ˾PfUG-ro0Ya{IT|AUPSY}D͔'տ}''> 2mS;eݘ>0*,w(I$5pDy6ySP~ڜwlɦkpӋ-o=|/K$XAm Ӑb.cY|h:Ň2c(>c1|?ӣxZa;x/7wGiQYLpn;"p5Ё pϭ7JGD!5"7Uy0&.G 2tyXB9HZ.XY( kʰwc(P)al2DIjD)Bf/n!/F,&OasTV4->mˣ/Jr 'PVY){)9 9GQU{NӼb 'u[^>c{t﹣^\3|>5O,ƚe8V6r&BgF8[Ѵ#M0і8~8cytxW_ոb7R֎8Gz lB#T:<*S5ʤ8bB#xLOQPAV;fa)ܬ{)02חg)BRtT>oSJ>B91t4VYQ(5dR1FFEB[TG R# 38ڭNJѪOXF-M^\;g8-&4q0M_e,drך/ʋtjtʀL"o6dӛ,L< 7Xx9/ŻZ oߓ !PUaLΠ˕.ߥޤ<)=I<"|xZ cx-.i%7tjW%H67Ϫ@ّ|*[AY򓜞q0;f^ˋ"`25  y +JIf#BZ}\KwTW( ${<Fz㔖bKC1iY޶0oSM*_jg&l6"`ˍ3 aJ1$_bޗ\!%Gbp`z ߤ~+5wo}xρ0> xH" Ŭh2:Qsp5f~XD@%_5ih^@qq160mը)?(?_Zgqm4cxZQw WPWӿUOa]E\5Zt'Ple-Z2X ar#:Q|mn8Tp$%mzRFńNjʪjH7KFa466>ࢳߌD2$ DP%F+, sB JIӷm/(P;`蛰< B/]O,%5VkHǐfX|($:1էU~@!+w^-%^_ F xX?L3n7ٓY[q+G#>ʡ:P;ZJ BSvOI1<3^m!fP2Uٕ_'^P2~%bx +ӌJ)aړ4_(exUXj^E?/Z^h c=O\t^8*Y-cOGhGuϢ3#dQyW&lz$ W,` CU2lɅ'^ K߉YU Ť'.4rl$8)#`4 <3I 7•i&,)K9ҕlS|cd"JNHF@9a@K2,MCO%QF)XOUR3<\CO(apɧ5ߚtX&呁wxCo 4=ԯLKtL^P[+䷦[0ۗxcX_io4*ñoSGUAԼ~Qo<7QzYq+ndZ ie&l6`˞[[0:^3w0!@Cʥ!mA%Hx|<>a.=o4m("x?w1=œ"[#[Y72s>~=Oǽo[p+Lۅex+~wưF3'}g^DB4LmzehJ#gU(㧰--w=ؾ{v`ՙ}~V FDckF^Ta 3@c˾?a=Mv1iR0(墭4P݋i4S 3Rh83vb0'RV]̗8֕oJ%3VȇAc%\<hpbÌ׬ccd+rH2JrFehY8bF! (% F'x(NFh\|(*o>3yfViP Ƨ)+GT=!))m2) IeP _q٥aF)a ll#OH lnC2KL*֡p<6СK\)E;;Zsܟ,T5hPRa}.O}x`_SϚQOƣtx_oE0JTO'S`O1eBM[VsC@,[d@ ;EWM6tmzUjw}z(iƘ(@iUx3{؁0>G33"޳GG51cV όk0=ɘ6dkOɠE,ڊoǪ(&hI#K0ggKizKwctͽh)Xނǘev>'r_NJIز~xΉQ\DZÏnqx]1g?kQ4弓$ۉݖdq8v8nKlY,RT%)N$$W{ zXD;l'c{Xmc5Zg \ʕ:ی[nc1Pwߎ[tM `~gwǧѾFSlQHbh.RRq9;Mw&f`l?;݆!+\LS@|H ֬["_+,e '2i}_ j: >X晟@3(Adrb\1IG <'g\ 3=!b7y;ύI@FpޘVN07AF߬J*KY2fOiiqXyX^|<:G@GdQzUw)1 I &.-g-1mjOPAzcb2W"SvFi%% N3V7 N7_EtybZz soΔ*&nHO3U`)Q뮨Ea Vɟ&M>NA0)PC/b#k@9x1(-ʁM^}/7Mgd[,IJ1L[iZMXKu{A˲,U7CC}=aHuc֔1no6aRb/&-^[iح>YȰr7oʥR<7jTw^nUnw=JW{Y@ 3Q ƚJz?7dG͗gޱiV},xxu,QۼWWK#~ad#rj+tLuHZE>2BZaVdM-oKND]^˲hn-b dL\ Y02- xKT/ޡqޞğeid/x&D|gRdy ޥp`̷4I0(m`kMiFќ{Ur3GwGw~#߃c+x {Gg)0rt=[n}Y-W|*՛Xp_2t`]8Ҿy6*E_eh/O1FӤ{~M}8/%˩T9hŚC[q@k,N|&1+7Mߓht<5 M38?33=G#~Г,ffW, FJ݆ p8ՈS^ ܦOcpa *-19bȩaKq̝82PZ&n2TnG#Uβb Ŕ8ʈe-*୲Z@q]T+0af$)չ>6Zmc^3T &n ʇl͆z?Ѭdm_5p+ P ??L~U.LdM7.'3 YI7}?.o3&Ro)*<3x+K-~bv(>7?5CIU/'g'6U<͎A&^*j5#ȏ6ݠurWޕg߫{ /f2Vi7LgxxY(.+AZufʟ%&PǮ4&y,*2ʔK]6mϊϕOHA*b_jo*FA QVSuL[k0I*?eTIƌ59s1!NiF߼'/V;wF?a=;}$%4 e`Qzja&-ݐbi}GG4iIWX>Q[W6d[`[?Om{M٢;Uw!HLYz)#(L ~wk q΃L݇"#?IZ'ǼӴ<#gW/ׇe!өI:;1@t-a qt+>k"xi's8{iNGew;_ wqy e7͏A.| cWUFąbPSk8/ƦGNbQ`Aҿx*Uc'¦_ĸVĜ)/=܌Y8CRXXY3(cN-onD曕@Y{S͂bရ߶Jkd1dK3aɃu,Z1Y+F;3 o< I H!O\W1k3=s /激‘7Ot?J\Bu\N-eF̏,\bl/' 8*f Qe@?-&3gʥg&"9lF$JSaf`- gf9ԯ$>vS5Bf]) ~|Q) 䁚njbp)YVyEk40Q aOB7 O9bq= L7d l5a%|Kc}7}x,(&4NY J)!?FP,JGFx3SuiPZ4a2 n({ⷬXGgTcF`5 ) x;S{V[2lR !\+p.ERSֻQ%S1&VԴֽҵچ{2 _ڴރJwhW3+`otXy_e'lɦ_ޔr݈oKXg)[J'zu"VF%1Ū4}6 $y~ H/Sf?K ΅ZinDaYx ޙsqs>܇g {ف) | 2֌x/!?Id>7`4o-~q P42-\& ++eh)Uʏ$`ƚFyazRA"KBД~USK|$jQv?0,* Kr~IjcݱX|OڡOeWSɷީ(3(Aַ YzGjUDmFN5dwg?۩$15;c=+d1mQx̏kޣT?jy0 ٛÂdM׌e˿l ,[˕-.c'gFC,Fte\{V~ ?xpv@#8|D4DqſU`g"2ӏޏA, Ll0K1I2G9  C𵶶6d -H:4 m)S0׆3e2z}n:}?;#8zqtxG~7ojM6dӯ-/aI(NoZ ] hF%8(斄J)' yF-dK("~d^OIA\r> E%ϘʼHAw=†Rh3')띘ZEdXr3_3ջ[¼R(n)QJaR@i:8ּC,K; 3Y\GuZR0T5:RzUn&,%ڬ^ .87*VG&,3M֢uƸƚR-^0atϊCOyU{aX~%E?R$ү,m)M6t-Y '~~e鹬\SRNGk Ŵy`'#C96ނ;/xgcSx+tǦq3'f'|v 3x_ 2υ>?-U15łZs 2Ʋ߅w~Ep_>VDfE 8w~z{[M/b+M~7ן?޳&q=Q9L?};h#.~¹x|S]ću;􍗑6O~:~x{z׻pӇ>`xYL"6rtr% )Gn7Z[[:]XZwJp>X\X\:%Ԝ^<,NjF)D^:' 2Y^8xDfN3 C7b&lזȈJ@%+YAP02A ?")dњTdkiNͳ_#܈|)4$o"3bƲްFT{RR$<{tXEEei8E"ǃvUb1 ULN1^߆brNRhtzdh ̕u@.+#ˊv1|3Uȼ!?=2>x>EɄ$lC%ȫRKʹLQ=3/*#ҼK»S%Fg>FVRDlD;Z ت6c KbdNJֱ>T;lgl+Ư{6Ox1UO,`]Fk1=4)bPPEJ(DfڇھL0/IyԵ&lF,+l SQ/d:/LLkTb{;ߺg?Wyo_݋؍߽wWG؆t:>xL6tsZŖ pѭ en @$s4 _jmaSp${Yg\UG.QIF x:b$ˆļ%<ӟTlwC<3pyw 0̮hYL~qS4qnERR:j4Msn}j il\+e!{AsKGtBYp/Ep)ţ-|Ftf?ciM6dMDdK*b --@"=CǤh Ÿ5HJkdh 38E(EFRSR<d#l&v%2dh/J a,Acڵ%!AWHFb]7*rQ k81[ۘ 09ɻubAbX)vәwFI)k̺3~ЋpeHdb0M)܃y,$-X(X|}z_UJB(ddiC(3HӺL11Y<{TuYc ѯjgs+MNh7Ҧ3k0ok>mEEޓd/ۮ1( HGJ&lvt -ڑWͲ1TUr=Hnf~)_R=UyyD"(Ex@dug!> uKW(࣠J䭖ŋ,AU gi\Yx#bƂebt}ʓgT:N_'% <,oxd]LL-%b ~' 1:oК9G浞3Q !ҮgXt֞(:$;XdRhHAur.6AjuF%yTf"c3(nӾ+l#֩rĦÚ0;$c&ghKʏr0J/ߙ\mɦkIo4 eϚFdoQcǨf]cٟG|_ąE.ca Pc1 ,xp %(y,*fP< UvYVQ?; Ͳ kA9_v5/Y _.v %j;Yub~#?2D6  @FQ-G@z{GeMyΝ S!28xmbM6kOnB)K N$Ll#XXDWs `WRЂIa"ekRI8 +݋#Tz SFxќu'O'Qъ9n"& j*.B?.un ÕH>#GtN5.) ע4,=c(dy_8l1}H:ҎeEE%]iK+FR^$O7k˜Kk"19 GDrn-Yg>YFmv\Ds.܆{tUV|.8F;єCȱC(•U/|b:xN΅^taRG\ ::ӄ#Gq`Ӵ!/x?W )y(ԇ)/uLlkK)b P_)8Gw#H\/K Q]݈a:\(<64/pRMl ]W,\El_ƅ,%v*鑗_m.dvɒ&Cu4 c#EOy,]AO} Jj1*[lZ[@k Ɣ-,eoIJH˜UڞY+(vVR 8AmOb)DaiPRN~bW`V/b⚗i:DX<_ày4Y}_ϘVWmFl4%(7#1ZN4yO\fZ-^Y#dAe^y `>}e"D O|.V`ģ\,'U+62)BL=^b 3P'cǷL٢$^lKEX" .'")DxpGh0³qg),u?iEhM6dӯ-@d' A/ae娪(F~i2 |Nbq`B?2ta} E(/.GeY:O! c~_7r9'g.,OdT{%* P> |+QYQ yHJGjn=>`)gyHFbJ֝]WAA6eƑ2tqb{Xqbx8may-yU(PH?]q, MGhvbOv#v<ZB@vU8gSƪOؐsh;<G3зΈcR4XD'p`2/U^VXXG :0t9׎EAwݔ Tg) Z;8 Pe0k=|l[G_юQրfEcn&sѺh[dӵ7E3~u˿S+[h0 Ë,ǰFY"R;]vRUg[ϴ[ L[u)…(yTG+6º0<ŵL Yײā,pd*e e/1Tf#x̱U GP=2Zc6kAt0yHuaC'y)ӂ*wŀ{,-օL5^w EgLYSĞX9]嗇W)v/v;./Ĵ3b) :iD[8IwCDxn|=?[2>>*oܱ&l$%ī,₡ao !. 5+> P]Pݘ]^gR@'gБܔ|94lj\UWґz!b"7'A :X7WxZ]hw!\y735(;y80e3/nGjN=z`&2Lx\2յdY?'܎ܽG]م~PeZSGfH,@ê bM6]+O([x۹-g^SX[?KloJ2ɤkHX2r-IIS3MM㑚!,߈v??9bN{tv-J5LH2%0SVRjEQtt薬SmZikOkeTV'̣措Ӷ*s3Z+d̝lbDj#ghdd#/;c 4x?DRr?()[6nv-ҽkla2f)pl!Zq94?X؋~fKtnG9SX~ڔ47'lٲnFcmj[{9(fpi?͍:&lɦ_A2`Bb[>ZD>5 QS>px)ʚP۳ ?Q$~Q*ʜ h]w1~G g ^4"toOףM3fb`}V9׌…1{O<Ɔ;4Yx|&׈y̏ oNJ)9,=pbabbyh?tM 5CȬB{ :1=܎av:9[SaBk(Ƈ`fn,,ؖlɦMt7~!RW9:ޖMBK kݖYN4~$~0~~`~{_'('=2 ]S@{[z*gZ@KvlH)[8AQ/|l+7 qǽ>k'q|ޅ=N&"23^< ڱ,HDxvCr_OŽ/SZzō_#B7?﷭jl&,[^2[Z// "DQZsMXqѿ"^ WBiC'Ƃ BM ;(v.20E<" {]Xp9WW3mdZ7c>/튰,8那,A~80[ 7$b~(,7vzx` )Ah{ԟ sݕؿ;.m5M$&}="[F0؏ŕNm2]Ir2\ysUF̴pRrZPܴ@ρU'\]Pyx "aQ!AY/Ul.ꗜ"iO.]Zo' ?Wڐz 5]fHb3m8w >-I:SHBL!+6Uu<t,bsm=Oݦ"L :WW\nACϢw B Ch-Hij<Ǟ| ?}ӘdZ^@_׺p܃ч‹Σg 8lzGxǰX zfY4ڈW܅!)܌)z/ )SOgS?W|`G[W1;uљDD kSP0gY8#nVQ_~iyMXxgN|K/]Xey0*АsՍgX;ߏǓUTh+I:[!v78^)EC(;M>݌7?nFJ =:r.±TO?Y̸]\Ũ۹Wo.ҫ{ѮA(,dzs!g.5gvT?N^s h:eҚ6Fc{ub ɉ++nL7޽؛Xu,{QA^:'P%ϽSih|lklZ϶K-lV)6k3,@R`ZȆ =t!i1~- lZ*[aHxQ4 mY U5 _. >|'N? pj7,~ 8p7P'B8)GRRRjWTTd*r_ VdM6 D>@@URD GCAi琘S~MpOQ RF9S>MN9=/Tb,VWguSck+Ea9̪BC^h ϷoxN=ۈ" h<\hYg^" PE]")ƏP9g!;/aaM}rgB=jt\lg@L!n3'Q4B юNt\CIu1*g15Uok=ŨM`O]D W9 1T Ǩ+@ܪi+)r\,.BV AB*95u53ւ'OlpP[zM;DS^dT,bݍSW~EO-< ! $Dm$EHLEa1$vEùT']D˵H~x7 1փ#Sزsv==WvpGbv$ڇ=_уQe$0"l}0ENLU() (8v/ c⎖탛rJՉɉ8{Zfְ>Y?݊bؙӄv"Cf$bIIֈ'RBdMoL )Pc6G fH0{pʏF&kd(]xm,ZfVҵTxcKUOO񩂿]o7k@hvX.VbD@ Qgyu lɦ< Oɲ¥/(ԛ]X$(<.,O/ehĜk8J0/,/?[0Fc9(E]u0  fWsț#_PNG1Jet棬{`z'K^<3尬:YJ'bR1ʋP1y$1bh-'a6 'lJ'0oy`>N`rY[/i%hҬǴǪPUW`N~ u([7~51dVai|pxv<( O"%?^r󐗛S8%daй8>tc)E){qe>w ~EI'Xr ^8ۃ i ڙ34ǑUUۛ,ãt.gX|勈K(r7.9)5+.)s ˲JV^…9$W,anGxa{cXT.qaI\4) 5ȾӨ^$n@I|<';['_Fڥ^w;w-DȬĐMHryR2iz6dϦ_Nٲ, @DJ`OnYʄYO`@Ԕ"uHd[FR6 CぼhG`ǽ((FX. 3Q4Q~~n=ܙx ?Nھg{b艶44ۤ,n kleM6d(\fN4/e ^SPڎ&twpWfƱԕ`ng 諌C Pߌvc3pԣM輄gn< gf ^6M,a4zk"?jtvt0A>R2Iub {]ȲxX~D֗[CGRPpbYCU\gi_.KD q9(ikE[w6RaSW&ׂvtw (ǬG8V?G)X8##~xpqde棬 qn,p9 v=N't#Iğ@nNFrIjljfQ)EX'gQ tێFǥs8Su8pĴݗ3qLFOg62y߿g(ڐNq7 *:0K=SjQ؉ֶBe -Xepᕣl]^w TsSr:Ywu݃k'v\TU }#\b#e>d\SZmG@[[Mހ6d/l l0MQD f74f}m#j4(ڽMo-]Se8lqwcpm Î^|.@ֲS߇Ϝ cZ؇~G>{G,(4]kg+[l&$Q5a+ऀBoIlyr/[=`ױsȬȅx\ b&!Ek1Vla^x)e+vFfk\8%&< KBӤN&2wsB.F\d!cx=xyvsxeid^Z%"^lzE?1Yx1Y#`ʳ`-xiv쬾% й:L9`- L܄g6=Rpu XB:,u`8v| .Mc`8sCMطy ؾzE?:"}ħװWJN rO$]DQ~/waqvDkCxPYP>#2sPT، ^Xf)avwXf.7MX h>* \C (oA8“xE\%4 ضPy" u }q]H^ĩ{̋/b֭8^y5t5 PQ; m5^x^~G3rr 3FhDαc<-rJQGiS/f̏䟐!+5X6dϦlX"Y ebvcbkOȠVX:2]ُVLiOVB&h4$scٷgGމ;s;2߉Rn ax_Dhn+V;54VX QYYE߹x{oz]e[leM6dӛ@ZCf΅ؒM\68(8р5"u_!a-sL 0V(|cn3eF("NES>^$̚^uSok,:<#kK*EdSjkiD̃{o¼kMbB"niDw:\0%3HЁ AJ,eD~iګOg ^BXȺ,,Xg45hfY]xNӵ'9O INu0̸ >}%vp2K`0Y*tz]ol;׬H׬º! yN{hc(3ષ=ZmM{`m6Yg|=CeK085N_l,+@]HW+h+hPmE:Ntvƙvmw,+~ êm+_dU&lzczCe--f햴_we GqhF-J|>ރo, Ũc7i.FUgMo!]KebpL,fq >uӸ5Dқqޗ01cD!8k}1tߌ% +@^ć޵󿿮rrRhעD6dM6IQb%s#DIϞB B萝NR\_X'U$AmP ╵O4(\:G!U+kC~-9J!CQɯ/y,W9OkVlaAI?aR*FM'k0a!L:_~s4uW _,*oЏdZqq#3.JTkO P%FJ55q^}4>? -ʼTaAUIA5=(\ϤvzB咢BJ~۫,ϗF}kUCrM)e))slz(5Ϭ>zUjS}|as~jR}7gCSoMQ i ,#8[9SvQQ6d([7 ٪3NDqy"nW|KC7ꧭ'EF&C3bdέb]pUl ahuK wԇT]ȭW|κ{|VK_⥻r/F:g ?vv(ƃi[r`ڙ`nn۶{]s7|3/-=tU@l&Dbm;oyv֓{0~x4ݾ.N6؝P%n<_q=nnb(6 JcB!B3tz0T&FEͺcrjYCyLʿ&gR13RVT^R|4IW7=7B[g4<' s*/'_I*&'Wc˜(60&lyʖj-ڑT5T] sŽPΆ茂h+"y j'IogS `/dKR8 tdwEIQ8Fqi}ƏYBf0{*CD h@D#aȄO[L˜0O}@ A}%ʀa3ap~8,Ji$>ŷFW*Q)R(ҟFJ$j'e̛F82Fj=!_c*r<qwy1@HqƤ5ҵ,}=y}RY~ZrEo&+O9D[ߎ'S%]:762ҏl6kT.L`ⵏ]2E>̔&l&l&l߰vz_ke 2$+)^\ě)DYD4;vS K>{|m{xMF!HT| \ىoLec_yO_ cMG  j)J!$!Bt[!eZYC,e3k*rpaĩ~ي ~+ @kl/C-x]Amkc}AYGȟJA|yCVa=ND}OySLb%+tÉ 8_T)xR݆8t/6Vcp&4I A^Jai杳j2BP4\ A6u#[e[%RۑFj}Vl&l&l&U5Riœ&@TYnhjP@9JQ 5LmpW<g,߭{Tր%zHAz'Ѭ"a#. L- I8ę/_6odY"TRy0 )ʪDmH覠FIAT^ʊ PR*Y}i)hLKDc7æBs˸4G8?kڑByڴQ̹jܓŵj.xO?m'Ѽf\G/Ӗ,a#AFW*j8GPSbhb?.˪1>5EʉIR4CoFJFf'圬PweO,}W 8g@_޾ۄsEa> ofm&l&l&j1-JQ >ʅFk IzMڥL,nہ;Vp~b9("KAӊjt?걄Icq"83ቷGXE5Q3?|EDj6YHY}އ!m+vh 1NY,j' Gfmo&zj@S{(47@=x޻XzY  0/Z^V fC:Y- ey-Ҕ*?"գu5`C.uau&%KH:5oz#e6jqGRzNXR $C΅" oRݫYJ"N#Rv(aֈjSҳohL~-KY{YEBJ TaˑM=5.bL1&l&l&lzS"CTX0,Q=~)]= `>~cr;R&<_O_ [.e} }޻p'mw|zб(Gw#$RhU:;<ڦ6)ѤN8c$$fX‹dz-WO/zsq\Eߐ~l۲F<el(et> */*-)mdϠ4vyQ|<&l&l&lNTh,JObv1K%&P %GpZ7;gq9“ك%jڠ$]`.! LKXђW8j-ZbmYw<(7ȿ=G_<#g? iCKނ_َMC`i=ÿ+C՘&;GQ7 O }s+rgZ(F28c{懿{KD3PzqG?߶NQ!loQ>߁ IB#;sj-cpO+Bb׋.,Bz~!2ؙ J<'D9>,..+:*9)[5[ݐm, ax&;וp_Q; xpLýO}kU%㳟"l&l&lɦ_iA-œ~ "'Eݓ8V݇)$O"nOdt{qܑ0;pYo89~.!5L!z yM]a$Ú]y|Z!AS^|`Dk^utgƮ"ks³MߖE?ByL9ЋKIxqnIn^Fh)ߗ1@}<~vNc8| ѳA5xs !8|:P}wm“}xۗӃ\7'Q69s)ҭ<|ΞZ9Jucdđ5i+[l&l&lɦnHe!-@DDཾuogM{_j‡5mew't7_'qG n?z84[+/Щ˨2 hJH偔-p;}!{K(ֻxG{$T`@4(:g{p{'Z]ӻNR˓o_ۇ8sz< s.Dxh^4FI\1e=34GpcZC%T}N=1̏ ځ}_p>z;n~b+q Z'2<:wVkp|>V1¯}\C_Y)Ub C!L,,#GΤaB2&xr&R pefj}C #%KJe4fAi\L BAtgXJB.{,sG\:g1]/0:MS\tź m([B#HGLu0dqE~cύYϼzܔ/smlLzsgɊr:V8Cl&~)Ri:{ՙպgz?+[_S _Oӏ[Yk܊_'Iqz|;2Vqˉ57[yO.923mg3>ӄ KY R h؁84Rk pR6?o~a/k͜ă^>V5Hɬ-N9~^<:{ЋH9,ܳ\ш Q.^~DZ@ CFg1Ҙo?}5ZOݳӘ=QL1RCJXfH¼l?-/>3`&Vb °ʇ(&vџ^'o o:@|MS'Qa1/)+|8vQU}ps]^TSΚ|W0۔mPBu!|*).cWɫ'`嫱5ڡUQ0# u2m1zöP5mIZLd\ئ*i_QN6*V,5 fM6ݘvLI /{Q,:x.ێmqS0>?XGgc3ܶ:^5̶wɡ$ı#CuKm{9|jÂ[0'f/ n˿a|o⥼+t^)Ap.,S]:_g|{r͡8vn;^ڂh^'/H^h,1fPЈ+.a2cلwƾKYaNx]lǮWvT%\#I~_;x c_.LG>{e>ҁeH 7TăxDFQ"el)d~^z Fz<$Re gv]e? .,9ĺ *:S? ^zH+F:\$ B]DM&m߆g^؆Cţ[?.ݎW^z /,N@~OՅ Aw-{ZOBxxneY6cOV/ď5B;fx1'# /e ߛ;Ŋ) A/iVv}vZ(X9]h j;rwF4"NE|l *iXC5:2Q1 ߀&j7#60*Ρn.m" ?ɥDg[RK1$L8dbaY+Iae,"Lv',40Y9K#cM6]eˉ_-e:cg: #fɘQ"Ul-gt#{p[(npE|d0ӂ`v3cdW&PTJXi0ـNŹ du n;E/ٌ=κR䧤#\.\ƠCfNo}ͨ*kưMٗ9'[wC:8KXW0.`po`#M-hi?_@SaӑN\qK1&+QB֠\CCo,Adbc+hAfZ:3qKdLҰGQ$y)]bnyyϟZC04qߑiG,P(lHs+!4r< fPd8"Y8>Dg 2!r3dlzF/Q=] AFve*,ˬ\`H߭^ÀRn4*HPzMYgaCl o,1kI1* y0&lţB7' 0 6֣<߁u!oD0<W8\͸TӎԷ##% 9ѷ~w5hmiAkMrc$j⁼eP64bjډ13Ӊ*v .%|y9ZKQB|=U[،&֡G0L#QV0]3;ᑽ%!P+jNVՑqj*(mG~9dg֢ J(hꕦn!;^ LI{P<4g;Z܆f.'P2\ tNF'hO!Nߍ;N~b ĒA/^HN l#cX>C~ro/4*ŶGj3{=t ORHA,(DBs(N/Ža4\b9"b`"+h*@IQ#=Nxׯ`x.珥qށ\ ŕix*jQ}rW[+nY.`2Cd4.tמxnO]5|cױ>]/%3C(>sL_fZWCB` UXҠAtm,iE,|,SK]:rmb]GCXă .t^L"T'BJ,FC$< ;<]Zr!bہOt __$jsF 6#50'Qvr*1F?_9Ե Mkv.ģ/}wvA%ﶾ zK~t#s^Y N6XZ%AJ|~ދ't+-#9_Qfc˲&0Di9}sA=gyd&l+[^o7")[([z\عk)IB>LZ \vͮv|x n3vnTͰd FaF&~h:)>7&xdF9Թɜyh)Ris ̪4N'a\ )Hd;8̜P)'#LBHCba:ӡҟ|i/e adŹĘ데R@i^pBfs -2 )\#nZ$h BVS |ZG)HtsMHkXX;U-"R~T u[cd6<01c^9Dgg ,w=mFjZd6{%ouNSo6dM7,/GIHcgo$/ #n( :CNb bYFWقKA 0jGўsJ0 $ydͅHOkXiƻBR}㵨xmJmjH(^@U9к _S^@ c^92G۝QKp{w\€ /尦e /ֱ6y{|e+!Fk Pq,j,"t3Xb8ETVxB;1ZwN_eև)C-H?>,둕a`"r2{y3Gp83nbC*2Zyl¾sA޿:DF¶8dաӡrdƑE8ZxK@#/}ĶdYzݘ|8pQ1yM;@CE464+. ׀&FZqbvO@i&WeE_S;tm_(JZٶ$ó>˗r m8: NirIoh ýhiiD}#y$tbo=/)Eվ]oԚM6S]Hۯ*[%24&^,Vv)>8螞IU /ft㶗[q׎V<܉y t݈!5'VSTB)5 ֜`Gی?:uY2vϪ>FY# dL3Yj+Y(JMFKaô%/E_oO LeV DA9y)X5itc"pd&Hѯ;I")[ng:7J EK//ǎot/4FR|-7 ߧ| .~CCrL!.CdS{|+}OqhF]@Ek?}uד-6d O {HS dAዺzydbV ʋڰL iɏS(kCϊ0юiZĿގʴ m(EO:)u棴v!s;1y<6C›nM"$Rcm8ce.{ n Qg}xX.(zPt5юz_h!^_5*'eguM'bPF̻h]%hP1**ԶjUlp2r;p`&pMK8 W0XI.D˾@3 0~b7qvYuahQa\'^:d?Kt_ N hX| O:BU1$AQG&0>1exxϝE|4b}'يܾyzpMu4܎m۶aGQ=),~/Gwos/w&5*0n~MهS}Ftv7t]p?]įݱhH k~cl&l_# kl*6^Hf0@VraFMQl;a, Qi& 5a<֋w`уYoO"(8ye[0kJs_^!3UKBZ),XF=8pWئjysȬ@ V:WW{ېs'N$ihNb?#HzwBĥʳM6([fɝ&J*[9S{#e.D@-F"WkPPs6" ymhg@:d;|ᩔ8Ř:sau<1 E#R(/F+KJ5kdHC8\bR)P~X+<7B:T&2: 7IG`XM #|/VY N`T4zhbݣ.G ,vcPLIs?(ʱdO1Cw-a7NwowKO6|T ˹kb/"{݋`p_VZűЖFY`Ldu_Wq=9[bM6$,Ğ,L^Aj") 56/WEN&f`Hk΁$ZEp<G͔0Ba"VX8I,@ 2>#]+<Μ#<xi%? /8~q #rHFnb}*p.bdzx^,Zqp,52Kصr9α,.yCDY87֡ sPI#s i!6ˉUux$TOu#;(VpwTq9dLˬDU*"a'q(ZX9G~fѐ BՉTd֣gMט5yTa(',f[Գ^CrZ!F jSz +>k‘Gws (O?m?g~6?g9V9#@HmFS܍as_|W1v>u=X.Cm^2TxvDNy+G0T59XSO#x.ض\%ػy|<<+؞3Dބ̞FU45Ȃmlɦ7_BRT'v.lY.n([,eK ~'~srcܘ|uW,gVȺbkTL1- 37YGs`C&V ]3ds56'bq)=)aL0ʂNm=kƓ V SVjHGy=d˔%#ER<鎉Gg:pcYױߍD6d۸ϨV@T[w]ܧ;ۊ <|/.`Nܜ^sL$ :]p<G˷!t=lvCEF-þ5[^Wq=9[bM6$ȣQɬ )S+XwQ%OGsdqAaO;5WWp\.sz>)( X2.e"'#.t9~ 8<j= Yq9'-ֻ#,Ww;3;p";'itT3X\0mNܴ/|{mŮ^O>/ahɴ5UY]=-x p. X+c TRf8Ǒ\xN$ _ƚc)5Z"g.[ʖyLcÉ[^CE(Jg@Jvqlp=sl,ٸz̩m1orvH)<)Uu<0~ `|alA](Đ[[M[eQk,ikؒlɦ7_LUUg-)EV-##Fòze/D\,rGJhMLɦkIv;e 0 ~ ^>*/BlzNު/#ICt"{ wpvXj׀ Ѳ| K򏷾zr&lIcOAM!LKJ k=: +/Fl/: g 2V&/!Hz8d /itTp|>kt_[ R28l$CG7yxrQzև+x1:ƖֿF4zѱرzgYTW%OFa2KkѺّj$> ţ.,X*EՙC,u1AT8ܬZt_ӈ҄2cMYd[VZs=kv 6%igev#:WP3g's(w"6BæO cN䔵aA,\p%v? EЯouE(OBͲ h+.r J1ߒP9ºB$B4̻]lǟCbu'Y- D _ՍavS JQ'u8l_H6cQ$X 煵ɦ7b0v;5ӈNPGFqޓxG@Wqv0 piR{yGߍ[?fv#ze\cnjmZNy8q? lF'' kb,XCI<*pb9/VR 1vyX^%߉#p$DqYVSYQvO߀܋Aey:2ӑs ptAzϵ0YiģRvU娪,ťFt_YE=<_BNe-:j>]q8qUU.CiC Hb)yLu_Ğ=1fó$vauORW"++hܟy\mLrU&sQ11V"b JPYsyhŴjjVkMH9wf{D2r^T$g"gcUHxj+GVO! :"zo2Ƥu[$laZQbK߇DʭݤL(Ғ~+g>zV1Qc؟pՅd[iԒ([d e,C3Hٽ9e_%n'[Z\<tc6vIܚ!]?j*&ƭy; 7pmChZʖXo!ھiji AY" h`0}Nߍ|McٯjmQk+B !!ɮ2|b>ĀfGbvf-Fy0/x빵>_c.bx '.xQ Q8MǾFų̈́؂xA1PiE(I(%ِҽQ'0E{re"Q)qG DȻ`S>Vւt.MR^}dtP0UGϼA6m$,J I*ֺ/ħ'z./P̻}Qg,NUʰ@T,!+YC8SU]dka)/ӈFWվ4}vL[[giܚ{!Kհ0 9۸.%HB9ðzl'O-&pBX4~39_4-Kyż[ Gj"T9r1-6 b;f'O܁(dn┲EWfG/mɦMlP\le/O~UȦƮE꧃8r O[ܖ.Qۋ~ޗ^c,.:"z,ڧSk"\xy˺w7DY35B+Ʋ8C/[VTZG߄+E?ޭQKQ !! k g4MS4j;v  ,I S_aǺk&ވ^SVKBpLɦ,pgǎoeba'7I;O+gnނ:!|-x!\<۾bL73.}5_Ż\w)Yޔ_ec d>JKӁΔWr|⇂&SXsckX~B 5 E*O׼j2N[mLsްT%jܬ d4SPZf3@@ @PHE26 Y8Ѽp)[^OSJkd`lr>Hq0N5_Ng3$ DB8&`\.:͹33K>&lQRU$PSR$hR61hMϐuVkW,DY/랦uey9y;nv"AsdbJ56aXHc0~ _ʄ1,g _S\⋖jxQH!4L$C|M%J">c8e|̉yR%0mUQ`)c1 m3,a[uJzMpťxavВew( DzKqjm3z& C]<޴sUy}*7ޱbjoN[mv0z dJc=i8 XŢQ>ygZE8ǤWSTJLUcX0zRR`sgbZ`+(bTN}'aI]Ieae!eP+ۑǴmeM6bʖצ*lҒTW_DuU*Q^YJUٙ8{.ĺ*VbZrݚu_~I/gmt\FH橄Y#;GCSWi6(O3S3Oz0y3Ϻ2?Q>J_Gy z(N="xM)h"'uTq3>x|+-]-G ̘/ h>-*(PbR˜Q8w2tZ-(4@Q ̟ <4¤OID0Lth@̳ͅn5? u[V;x^ͷ˸*'bF |K|T,j|LU'փkKJMDh`ujM6t L0;$IuZ%jMGʼn?-.;(%4CR[D/7Z^FbmiX %X<:H )oPoMROqˢ3 q7 raBɧdbD8ᬵdxmYH?ʾa oȿH/qxQ1?%yCրIJ1]Hބj Na Xf 臭l"r3.KWzL*e5bӋbݫ~SӟՔw MJ3Uʼn/c;𳝘)[S`s#'I)ymh+6E HSaFT/U74]>\7liJx`X(5uCtij/_O_cdy8Mo\qb՞T߃@ tW+[ޖ*lQ037~aj.0:z:QTZNdB@?vFȴ'jcӉ& ;6t P`sF@&^68gS2h-! n7:s <ҎSQ'(FmO^Q L_bLwA@\vsUma7L`7#/ȼ 11W[ #:: +A92D] i" P0ړt~ɇj8+;{ }3Bh[REXw]LaMMy5]% šIOm0v16SS6IʯodcE}I֗Ջ̛ӄQݘr_c7V!DB<*+AlɦN\:-k$zOFwO6~0^UȤX`+"8ɓ40$Xlb]^K(&J4SWMUHKQ+CRų58W|r̃ה`<8ls%`DM3Q\W[RΥawgYurCNlLL-. V\Sy\Iǟ1Vï"5)ʳޟ4e${/Oe}HI]H1 ߦ=Kժiȋl#N% 'fr6uZ*WcԿʯb+[^(\~-[l9Yx(T.-勓(YâmmnDei)ggQөi5KnXDYj)^IF Ϸ υu252C/!f1h Ly#׼]2mSu4/@`-on '}t u -2Se>1pjHxmk,)o4p GTŨg3^gތ>.& `)H1&* ]ޮ?O ޭǦw-R:Z(;  ̳Z`z-I[UƬ$J̼$_ҼlɦkA5As<Ӻ[c1'b@O<Z B>0>-fP8j{M3sVA"_^a.Û| mhY~{?ylPu~YI˙Agqz(چ&-Lѽo~3H&תyl-¿n?1h7{L Q%(e7l7)[U߃aQt ,3luԚ9^YK]s3 ~CӖ,F5i7YWg~[B`M6tmHr⏐"n0; -T=)ci:}ka:tFgpmkp<#BOiH e!OJLNiWi%ħq 1n)$\fAy0;鉉1]#zQgʲW-MkY(3lȷj0YU#ό_0t" AfPʬ Gf1a 5dpރކ,"Y T4)+~y(KR=Twr2u# WN f]Ec~~H%|̺/|jmj[NFţnqu]dAM eCZ0-hjlŎN QΞMQ{oCX|,NNƙT|޻xeXQ vɗVlyB .M(Py9AvP#_4V* asީн⾀s^كNP1aU(kԗUP`emO("/,gن5BH:aQL}c,gΧXx㡎 hN:GyqLBC904q4 Xb3ʟOA҉l1Th$>IkV2*9\6zכIl*PV(v10=d [p7ut4 `1΋ڠ]tW?qO׭34tB)U.-xWKT~߳`=%E;&I~+;*hi,>B29 "peQj ,yQb {e^"#ar[8}3'ɞܷ}8<'BX± A5w"/Fd +U-!aBJ3F0  C]AtP~UcS=f )n:q]T;&-4HqZalVGaqMAu|(HT2ebFdl-ࢆĄ~]x <0 "O `aӀ%lAˆE/_/Hi* ^6{]| YRʇ?r # (&Bayr+^2{򝰮:@":GYM`'i0Am%H!/R{Jǹ\VNz82$To^XNS=Yz&PڧӢUJ z2$AT54#")) 1 WE/\\H:vԌz v4e9p246 b^Ϳ6$#n[O ߏ9(`> GB (L#hƹ _@£PSS#3.e&[ea1!hvmqbi(43n/sq[vܖWJ\U'[b0;{FѢM=WvqRgˤ|H(|-Gh[,d{kb?G;Hv0Jy )SфOf!+}`!^!J/ ,t%!!qY{+G8N/O,uW1R?~qx:g!En_wf_$x/@rHwrNB`RNNGd.+^W:OJM)s(>qE|!f#Euf[&ʏMc ;Ob\fNo ]"Bp޼Rϳ^̕cHN)q aL`Ndebe 8Wf N~f| nѶܸ &%Ppj޷dT#.5n˸]1)_~²Z4$BgߍK:SEAƹf^OlXO-=΅8Ytg_HGV-.ECP)6sHn7#c tpŌt m#(˜Cî/e΂(ނE7N6 fx<_ɇyr"XW)ƴ>'VFlwǼ!.0均L'pnwM_|3"hۋxp'/.Xu&”> X~)]i´sS^ y <:,xsuw~_!Va)0~Cx{?⛸m\ uܤ57!iN:H88Lѧ&A{<ECdp\1ܜ;;qg0nͭm9͸-?Ϭ-9yQ ~Q~w$Max0 !L퓿$QnQfEBa6faB[!}zH,I \>lHpfϿTް#E^-ѓ~LN9_!u8)^Zs5yΆ_)lq^&E]~>[BOFt{#9_n :qV|wO%˭14vvRcKvI+:56O۵DČ`G D"VDgĂ|'sc沓k'ь̍x_ηڂ3A 5z Z$n_OCdz<&?܁>4ČN*탣$̻yx3 V=xxse2ܝh.9ϱ&%h܏`^t5LW/@dI'\\.~2h7vE҆kѝ|+g*1>b72GvFFpp( a;X4vfa=r ;G4)s'n`8MN|nLϳ[ eB n+/,<\'\AL݈KKms 6J?B x3us`f `U ãԾ>vW†3>w cتFu|'Ra} B/â"g}_tmh/Ɔ0J  17UGau10a[, 42 Ɇ1 fy)}1%= 0_43cwB $3)_i8h :XENz VzJˢ1= , uabǻ?Wӥ*R^-GDkI!';q'\.aEF-`6m#fz'`S[*ntIz.pk@3nǣO=mn;6 㾋uiLN}>dd Ga Վޅ:Yߢlqژpl._|%02u80:cԦcxLøYи NvA;)V; g75JDŽc8thnPϾQ(Zy:,BQM3>⎏AV*I<s57*~h+?iR=C$)w $$$._7,pIr^55~;F{r`,m@@9ԶĴ."?YRsGf"̚ V.9;{206/"38YK?Zڕ`eX{,H$C5Ė;dRl;3%= K?{v¢%e$Q,f~y[O1 c *iv*L ݊% ǢhXp%,Y%yG1J̫Ÿؔ4,p7G1Q`8Z#| Y1ioNb 1Q۰'.j~L:Wʈ|;II:3@e`k>ՅDZsvDDE #'°oA\꘨a3a"@4n$E '77i2ebw2Y#DTXq."+_ŋb(2c\G3F -jIgEF Y/tWS%<^_O84[~~;0DPߕ=[=:Q~۱iZ|`>/[ ڀiL?z.i|X&"[&,i5&| ׄqu \ՁlɖKl @Ow[LՋV2FT8l(ݷM@XadG3x9dVݞFFoÒ_f'FHIcݵ 3gpvY.K՘Y39}1Rj< :CH( Hb% ϿFrO 0Ay@Aƞ Xt^^SޠPؐ %(̻ªj ty쌪GD$O=J`obof% {. DZ7Sډb? ,ۋ/p1A#]| mS-ĉx;'I`s9 2}۾|ӈLIp=kG%sS {G sWJZp?>\J|:wxi+^kYis,4(8L<#wy?BqYzI ¦ދ^'.#UMq>IC:w(XpH}gs+ .\|7h/q [TXg% U9Is%EPAyMj.8<* /t0?&"Dy͈sࡐ\&Ȗ&[,d 80At]dKPqtcpxmBOo/.7"/c:FD&n;@J}#T?!!/uRQʫ].6!R!EX,+ =~[,^~PEKQ f|..hA qapěh ._rꬨwW`/'qfmEx?!X5Hݳs.&hX*cټc8Վx_~i'kP;@C)`bIEaI;jQgoQnxn(kX2iC2))l% XmicO RMA|ީ4D$$#$4XX4 *awѰ̓/x2mS'OZ ׭o7q÷PǭGHT~W%gpoi~WOխKm1s61SOe6}=Ƿaɮȥ'd QMa[p# ; Gkٟ *LXdagͨEƾL;fr*Em OU:FMA@8¥c,4T,p4f:$&[ٔ}bNNeK2jؼMu3!p6OuF4ϛ岲g!'IE.:Uܻ^=UeAXGgBIu;)&Y8|ҔXBB§ oDD,gKFņ! tVV =8aY΅9 5aS5M~3$%d"C+9?CVJs14C5D5/l *5gSQՏ8=qF[DUAYʧh+OG=a({zsPx.Da, Ry`_U0"?Ƴ)8!Bܧ*chnCNA2BD|+.9qW(f+D(/KM: .RY]N \ށ 9R*> aᄑmC(*X9G)wGXj lB|'> .D>כֿnnUL30|܋ l).*3(3Ēag6 :ϫjGaM+Y7a'LIOlÂ2T7"5G-$òlϛspr&$! iI;_zG5m+Τ} [sId ρlT-[ }khCSc;[sp`񃘵{3_j2KaU,طwZ۸nS: UoC^.rݯ p:l\ Ǝ`nSaX`E(Z mlܸ {Vo@T^ GЛ~g>m5n<@FSa}؝߅n; < 8g=3'T ,Ê9HHY0y}_6nĺa8QRhL;TI1Z$:u W0WPgZ]cMX> ,|6 $g !dtjjPTUө8N(V"|~dxs)+gLfK9YoǷn7wn?#;qwb Ίå{xfl;;8X&ra'W7FpR i`A~T?{{^p6D>Jqp#o6bӖ-E19&kM7&1'oq#mK< ޼ N *e H:7n*C4Hu:# ":2 ;wB cφXq?vB2'i5 `0ܼi¶˰8E c?T gFvj',598sx36FV -9OAvbX[ D,%$$$$x\1bEdu܊7 IQgpt,<6R|imMřj7[ŗy8I"3) 0(%ٜ7Ј&e r15IIHBM_`I|ZML*Daz,)(_^̠( gk:)**8;P]$áC0ʾHNJ%[,x#S)rIIoŨs )BF`CZB+iCkmwM7^/JxK$l58|6X>A"l_P6JMqWfrZ'Χcﺽ>W=;#RN#(I/BSs3SVFԥFcИ}Qb܌ 8O]FR>j:0^TT8$#6գeVCm%I8WZ+w:цHK9Q\Q~k?τc{4Ũ pO1 @e8J^\_~O.$D"?ꑕDm(995跸).u8.)q fڠj7jPZ iHK=ǰe2NVtA1>ԎLğKFfi7c@g+J IOнvSU=jY ~ <ؼ4 [(?EO.kz@K3FPY(h \)aҰ^  =1IvF nG 4[1XcPmb,ٓ-AA4%(**GyyܪcX|DFB8\fݻ-m )o+PB})šӯRxE AYcbi$ ۗ`3g۳vL/OY2|^hGu1|9T|m-Z`ܞT>+"L3OgiG*)qAs mo)=g^5e++W؉^+SB]^> yy$lA}S3ZnlA|۹PϏlat<>֟m??;q~t엸=iXU[CWrBxd-,O⭆mW4nxn~ Z[_%Yw#h .;/|/<*f<HVY7n1nBN{ӏD#҄AV~{-I `{?]FRSMwc٘xp84`Q,xxGAzׅ^o|w~$ws=/?+.?v Rd}$xit:<:^k!B$x;  <8{q,fص1 7Gbx1kxl&-Ǻ vw&zꑹf^X^؄x*!!!A='@$¸B Ӽ4_r&LUQq vÌ`5$,&l1X㐜YfcAuQW?MQL*{H,Dh+3vbKr(} 2+b꒿3ablԡ|tQw1/B^i)KRq@:zt+G%:2MrE[6䶍cPIOJFVW Fr2$ Ga'Vb UKıFTic=nRϞABb,#p"|k8MU5{q ~'Zނ;'Ƅ2D>jSihOϓwޭK,,ْZNB&mc8p8ĦEf.Ef v4dD<}IHC c",݇Gp4Ź((;#˶s8pvF:]J ӄ l 7> 2Qlv |+DマoADON`%Gqo# n&J՘8AB8^=fU. pp6 n1Ojw {ϔcSLbO~Nc?~ k zܮ1 t*Ygv/݃ hQ}Mk~ +#FHª{Ō9p<H[qI繿bsFa7~aqc(6(~uz'=&]L6 j J욵)u&P_BA e.35Y) 8nzAx\Jm=4\M,þRP߫9);PnbXi9A![JOC݇J&BbRN>)HKÙFmfo>k>1Wlur* LhW ژ}&j"I: uq-wԓ 2-Eq\R69)#a*ȥ#r9鬷"KZM㰐0NO FqY^6D4 JTRsL{5F".v9G/;9LH&xp/ y-unf m_7ά_ᜟ7i?yx/m:nLK]' Cйtn KS0MB.C¢sk~=RNͯޏ=&:/3l^$rv#??/'Ԏ5jtO8[# $`btuJrJykv"d?t!󣙈:U_Oa: 1ih&-$ ]u"'`5ekE4~lŞI{*QMKAu.F"v@jBg6/Aq^f`Tr"}R3XhI3gTTUX$zcG+a#҄ qH9[,{i4fZU׫z8ݼ@CfݘXe=0ʢ <7C )}Y:ft5<:~MHĔ Y.sl%1,_ueR&Tg"d_8,kCήC01k,]sGj/C̼-HÐdBj E8>C#6 /$bB͂sؽ=̟ ybFW B6nzt@ڈh cgy8݇vŇ"d,\PdEA\rꆚQ eQXw( ٧‘B˷|~;c ڈAojU(:Yh$sfxR bF"g?<9q!g0jyFV| k):q.́4PM]1%P]4 ؿ%P?m+8e'3 $]131;D: rv\u W^R-*=8ma6`6nmL#45[nDSsh+ϥrU"+0+Q 5NRw. e'3"uĠ+Kde;I/Htzc ʀm1E D,F|Vʊ!v)t) Cunp ՗=Jyp9l}-Yh`AIUL_@q, zls`w4ZxZb{oL#&ȃ ƯCě%bmT~]*} 8ڴFsX\W⅂9Y<;7|8êVjg ,x.0Շ ;Ǐct[~}h;vv븀wc;p yqW^Τt67CgGvis4P0Pq#>Kב@Z#܊> FBVgԈՉX c( XP6GH(t1(޺Q*"1{^GrK1ʲ`N*|zqb{.PBXVT75-61u_I}./߷ Ȧ!h~xbGDy> -KlGmy6MYY,H{F/~ak~#` * ? #V'gQ/dvy~$7QFёDmUD-HF?cHgx,gOQ$6؅( FanDV!$5d#d UXq`{GEX %AH-EmTvp4!lZlXbtbE!fyL8£CR 22=$c&,}rr:UP<2)S:v 8ʢ"<-Ž$K8je.d؍~bڵXl, "FZ*Ī2 1(D*)1 Fpj?$[D!ٴͲ"=`~,XsdKRJ(FHo٭=Amzv g-ȋqjfA}uA0Rۻ:`p C1l#Ƒŋq=Nj͞~$!la7i;-m( GF1֚Wلz0z(@ >=ྊeCCECY%Q~sCxδ}@%Q26M8nӊI./@(BKsi-)Fѱs&Z*U@+z4yŧ &G|2_L/_|ӆmgCpgbfr{64-8by2r?? >1DI(oD:T#&)]H./UυSXc_& ;zytnOn{G`'T,ޟ1-,4*De!aZ٪ګc Az{ v*ǚ#$َqo|]%%#.߃hDEXr[aսY^=D))\]z$l݇cGSP<Ё#x[bmƜ0bqffĢM ՝l=h??x;{5V5308?D)[yv7c08݉Qꗨ>[ycwOJh?6' m$ė&D 2< ٤Zph6l4VKKG ދVtZ2n-T4wT$4JĆlS>~̬ذ.E]#ǛєSCql MFNKX!C}fEwe *PUS$da8d5 cͮ}U! (# N\6kp\!ڇl.r]q#\*wRM򖃧w3KPg%E[Apl2kJQU[*kmu!"F24<Aڐ RG+[F \\$KF1N-cL1!$!oAБK,9%3 ;`3Z0/Z;*2 |w, hT#bnɮ&y 3qb*˩G-Hqԥa5W#x+DIm-%pg=h#2GSQ`c4.DP2;w&EWY~Wl؍&gWUn3.I{l@ )NJuyGcgL zQ1oHһq~?$atqdVu1.(=CRۨn2D؃Smhr?3k$ ,n.ԝ Cc导tBWP!™hS#a0Q <N "`$zW#bħ [@?=hHéPC'| z dJ՘1jB8D#"Lkqng3hwQ= Sc:vUd/t] rW4vVUWҲb(.*EaI1_+B^A2_ XLS@L`Rk/arNAi&*hPF_=3ocO0&daee~;mC4_~bV25&=Myn\wH`"/_E|ҤlY biYiJdt/]/! VFe/Rd ;6zOilF hߏ׾{A5ipˎ $y>ޚ:-ƂMlh{3#Çn#sWa^TzFQ)yxI4Қ1uz-"!VaŘ;*kfcPe+~ev&cx噗1cbY0~nl@K`skK}CAxXH/߈E1eŊpLz;|n̜gFX ̈́ ~s1w pl,6 Vxgme'px%]]<s,C@v9RbۼX<:J0eG$2ZIPh,}$hb\#Bs ) I@Lt0B %m, 4-zAvŮڈ-S݁!d&&3,+ Cv 2BuhI8Ajl\ NEEtuv:G!2*aM@\vG1 qdbchĥGF8T4b &=$g+!㤀6!qIo@/ulɩTps'oW%SRλ<꩏[Z\8_t\ao&eF${Bݝ~Ȩ/#>r)q2> .DjGUc5( A YգUԞHH@+-,wyeE&[z2¥ 4Ji?quLL#2TeBȾ(\b9wyt$)y(mnd3ع/!!v:7R;ZՏ~Vw8 NI8sLQ<nW!jላFbR6jI v?e(iî 7 p5>GHtOöf\H GD@<ވ=GR F$~zda֝_y n'iBʖ8Vz*Dž0rGtrjz0`sږHngB ?Q7M HݱBr}(iCyfP;lH's]Yr>֫1s!PNmY|"198qj^ @@4ё 3Ev1yi lD/|Dl Q\b-A>_-*V\yW^z"܅MDH*X~Ejx ̈́ t13-:aAjpcōs|Q>Lf|qzy>](SLNpIC9s|4'&4XAśPELq 5 t7@]Oa!NddC;%5("{$pt䢺 "]F ߱ɖo|#[8vl`[~<[} H~Ml ƃq඄Τ'A˟3ͧw)ho"mlnׂo S11/3ܞ̒*vvL4ɪa meM1؆FlT?N}\ U)$dr+0sQGljDžBejQ50EН:R8'砰Ns ;C/IDau;ZF&zN(N(@= *t)-BI^%],~"|$$> |d+ Rj1+r6 N}ܝcܓ| n{OOƵߎ^[#q`S> #qGBBBB G*&VgᴓNC9eg~ߜ܂~_K|}mgg[OMc$ M±Orʄ!!!!!uGsӶ&0~LĴk+HEB ϓl-hl:`XEB{:&íwށ+WՋr~|0B4A!}98\ q'KHHHHH|d>ӷL,[H<7Ŝz WҘ1{\TvM/;A6Dr0⡫t*'{;G}񾄄S+E;tgbfe8"ceb̸8|IcK8HHHHHH|VdB$aӷlQK:xpq!{^(K9 Z_y*qzWe/|ucBL {K|ӽXqJBBBBB3$[$$ mgԥr.-&U8n T~]tw)qW~t$$$$$`ak\NAkٗ'OJHHHHH|d,d~nd sp(܆ )/v^MJ<ߞ[T),L>Yt#@}1|$[$$$$.h[C~<x6 D{∈%/|Ov$$$$$$>HEB ϛl j+Id(.:©C>YY t/IװJqUD:+;%3Y;"!!!!ŇN*3d-<6*ǻo08S(\`GBBBBBeI\|1 ޭFMWI#mBxk7)n垫)2; (}cAľ-D|C,J%<fVpI8\AÊ-2M/:BEpQ4urZ ~ G}ӯR@đkkg8vqT,x%53"̩?3 G :QX~7D&N1LĒl hq$ekƒ)oא>dee… FNN.rs dgұ d|^6R/ι FFv&ꡰU" OРn҅uRLpn_o4&4(4AlTF~ŒtU e{qKuZQ))\_*Ay᠔/㫃#*pRAt &L.:wFn>1!&NqU5U;qqe%$$$$$$$$$$$$>{\ӈ.VRҶ6:u 55C}T~֡5Bee9סUCaer.xEȲXl/YEKwDr!Ɯp+0dHŰYHr `_S sP0x0t :yT7]iT /yvJI*hTe.O?󔾦 qxX;?/ |m,؜2ڄFM}:0fcK)sDmxHOBBBBBBBBBBBBsmr~ގd(V45(hPЬ>kՃnVBN Dqq!J9V! %Z6}6^r:avaiKƆk[ށ>)*+ljP9(,.FQaGg*c넛 pKà˃ XL:nR VaJ 7iҔ t'bOOۦHhqhL(~ 7]GIT}+]؅Bl c…9`hLʐBOm^t` J{(#BO;:K(m@e}3bѓMNC}s+I =r/g<ؕ¶l -2%SŪ3|+ FX +NX(**JQQR* } Q/bkDr`&Q <(#}0G,nAԨ3Lf Ms:`U#uz};]91'8w&|@:>6 Mա`lie;aȰ&,:6a,ڠ94A(a"&[Ϣh,v'p4$G#q80/]ŋg˒l)T@y-[RSSI5fGfo4Pl2p@XyvD(2[#YCp .8DLa)愇dg ӯ/Ry0Mu:5DEsq,?qW㺫6n5|~x7!blgH,isT@ Q{10o Lnz3 zõ|>( \9y wc<1S]ħAP[)@&(jPV[Ԭ< 0 Pa2bp)Dϖ ҅oΝmaX푡8rIGݨeztTQ6ʢ"o SCyi%JK+NAyZQs ^؍UXO]u76V<6-$2r4Ԥw Obq\.k1hűsHU)O<9X㗄FdXgxlK*Jm)ejMGι$d왎(:{Ǒ nPT?w*nwlAqgxH4-A+.CWO?"h83-n{"e eOeo6olXW]bM"e.v;PmC݀M*.e(*/~R,Na+;0ZP wixw`ʃC7a?Ӯ [*PY+޳pyn*3ҮC 8B :eX6u7 $b`4|޽QHC[2?NY#O?ط|:~j-:eǻ<|;ѕs\,,X?}'8b FYL8DtlqQ:tmq%\b [Fpu0DR˿ iDtෳ^m=;yzO,~olZe):c"iKAdf9r:Q=SAqY B$2$aRBcgF#"6ͯߊ/y0?"30`8C `Ŏ ՝Fx=pRqU^1Uuln6`啅<O݆!.8܈>Qdj@usˋhR1yÃ?-X%c*׉ U"?0v\{w`=H+P(E|ڊI~2/ph48s2AeoN_zҲEBBBBBBBBBBBǗs/ pqx.1^k#)XpM !C&xHXLfŧ^컅l9{,l|{&nye+⾩ogy3ںExo6,?!GZy-v=`&/(&GƂb_ :?ҏA5`:E_["Qۖ51A7xif'%Zxq]ǘJǫ6o*>m*)p(n@nFW> 1=t@< =#.aCɁWqϿtF>n_!^Sx1?#FDd.rܾ|r:g˒laRbԧ2|9\*[vC39~xu19Ŷ+DdFJkqmȪljK?/{pS~(üž6rVb/Q`ƘbԌ*jV{ޮX엊ӱ/8]*e" 1+KJQ\X-nEw XsmY圃‘_\e fd!$2fRA-4".T^~uhCۀ-6wČQ Ga7cjň݉Q &cNXqeeBo?[0gG[q3Q)"~$[$$$$$$$$$$$$&.iD,attըA}Mj*Q_]Z 55Ut 5ըF-D%@5$g:dPu:hD 5d [p`E[CJ5) ]Wa%S 7ԉ2 _/SJNm=PB9QܔCәر.;kfYlLе/+TS]-@q8O01Td) .[<[5le2T14 WZ+cehPLx&SPE6SmNת._G&xg6#NhGo1x|l)-c-#l929}0";CBe%k“~x`Ru&_Q-YxJAt_ck2OsѶKlb%u8TJ6,I?" [6T:q6T+Hyѥ31A(:qY(HOwVv. E|xz+lbFy)ՖH- vȿlMM~G1޷d/]µ"|\drMM-D3 d5a&\%[-Zq\qW`VFWqj$鬂DTV0qbӻ?{O|/&6'="_ />޺(geSc^xOBK  Ƀ/yOl  %K\}M-W2r![VPuLq>yH% Wĕ{B-O#dDd"*TOa )Wf'/+?$"!!!!!!!!!!!1y ɖ 2ēXa:S8Ui_-l<^d n'ZvZӾA'&bKH  -Y *'밆Z^ǀ %V7$$ΐdwß%\.CtW^奓=0S"C-YHp eU+ǪM'#-[$& $"!!!!!!!!!!!1y>[r:wy-N +y4oХc^oH%$d>"X.2rձV\yW vA;Q5]>k qz.vk"lОN7p))|WwWQ8->Źug~e؉/wH8 CG:V;*kQILnHEBBBBBBBBBBBb`l QD`ȖV\vǚ e,$W EVnŷx=KABJC`׹KbEfm]O-'{->e^"d LT +N 5'Te ;5;G( *T]xe#/{]ue(NHQv{t T zM62(} ?<dh*nݣR S94sr킮aBkqz-N(.ce&XGG)8®{tƫKL^HEBBBBBBBBBBBb#[>&[f%hW }\uWnj/x6:7qMF8-pT-HɵmXP*1}~f,̟3N#{ RD.ޏ!( ݌4DmysǜdB%UيGwGFHLfHEBBBBBBBBBBBbo&[-Q&"][K^Ynhۡ:umRЅwG37to`*n[)::1Q s {G;*c҉Ƽ Lp6bZbSrid4(~/kߤc)GJDw`ǴwYcÑBS8ؑ֊F8+{!;ߛ{A%htC!]TVZҝ$[$$$$$$$$$$$$&v%>AO-mQc .CET\6 tky8;ЁN;3u^t1ͧ 1;1qYAbW$EVJ<=# X{KX (ۅߛ fCXJD6b$}U8ь!/B׆7|vm[c3AN+û̴qQ9U*j]*]:ȝd-^E.YpU@q;x Wr:61tCkAh(NםlCBT5ah*pB:Doı 'hf9TZ(?ԊS1`[(.JQKbKyȲ`աw AxĦ47FZ5^'i) >01S$$[$$$$$$$$$$$$&G?"V$X(dDliǕG[q %W- NcrM:+[o[6q51Qqā͸n7]5A#WH\&[4*ԚQ5kp@7uëy M53҂؃ݧet:?]Ft;c Z0Xw8ӄ֦8:c1fR#\4O)I$[$$$$$$$$$$$$&7r' Y&?C|$&[<:OxpÎz^w[Qf7x=ǁf^S²g⸨ /ӬU,z` 5U ?_>.4aPڞDlyܚlƇnŪ,CÔV]8V2yz N:Qy6Zk1AHmBg-ay\P~:mST"v:94Kl<+ȖO-ʫ[3paV|X³yngWDD8,N';P&K;T8 (=&` {0q͎1Հ>X`[aݯq1`n@_/WOţw7ێ~gļצ^{+ -'Wf4t6BЦwo^oN_á hp -2Ta(K:Ƚ  Ƀ/]_ג-g F4VWMT-:dp{7}u㛁 94qX*رxcvX٬*B *;5*A̮`GUx^(ąr2Ӛ 0:wC@q4XV/NJGoorP38 [Y"6_W\6 f*BCi ;4+V`٪~C*2S̔7YX" IHHHHHHHHHHHHL|x H70-AdK%GQ w%;q]k\w ϖ!7谪:1h 7O% :pA~Q4#XWZxD%`eP&^TQzLn1US4űS: ЯCU g1 H)C.蔖N WsPl6Rt(/+:a5l<(iC떿lD>ֆ+K?_dbxWO~!TpׅVݭo@V)KgŗI H0a@m7K^CWV~(yPXY)Shۣi8b 7Mvli?])ML(|\^(.¥ Dyӡs`[brC-jD> _dE*uۡ: ⛡v\߂_D Cjb,NRnU栺QE3A ã KD!it-r8p`BDTNťPzf`\NAlBFp2:8EWQn)5+Je}C-?F`Vzkla}u :'bjn!<}jy.؝<Ճ݀ˣi>LP:2Ph) 81 am괯U miGۀpRBbV, ]$ +.zc·A _"ft-[&Z+$1! Ƀn L04]+*ƐiFR)T pm i8.aiy+9JOgbud:{׋*H?JLm%0T'ta+*29.tLw0٣1#pFʸ"!>/)Ol}cs._DW^sR:LHLvHEBBBBBBBBBBBboXA\:khᕈ/mūʫ-@>^Uؕ,)\:1Zu^ޤꮸT37>(n ;e*· 49Os;!Ed"Ozϸ0AĽM bqm OoD UQ$&7$"!!!!!!!!!!!1yF$A.l6|KL-W_dĥIHHHHHHHHHHHHLȖ WBR$[$$$$$$$$$$$$&>YHBR$[$$$$$$$$$$$$&$"!$[$$$$$$$$$$$$&>S~\}WoÕG[qIHH|dD{ɖ#l,  ɃR+ OlNÃ0 t:~hfŘ7Ąb,7E|oqDjq'? ?*XlM=TbrC)IHHHHHHHHHHHHL d5h1\D˨hp [>Z誀V\І+q-} R)^Zٌ?Ɵ|?M7~7x3~ß?!nэMaҹXbr2Uje]XqA;.)nn::14YhNJE[RonNiNJQ}A((o; ǰQ~' yCwA1pz03WqJILjHEBBBBBBBBBBBb#[?&[.Ft-֯GGS+^{")鳅[.Qe͚5;> n~ZXwrB<zd!zp} }VþQ]wSlb!KCs" 0;ݣړĔ(|kCּ!":c(NAȰ5!qn8Pax@u|pSc*<Հء{(w[l1ˬѳ;(AHLjHEBBBBBBBBBBBbS$[KlYv-2ϥgWӘ4^0 s7c՜)X3N|0v, f-_nqhA£b qPW** ɭ]5tEJGXdΥڄ| B^ FA4'y502@-*rDt]aBiasru=hh+: JJtVi +.;[*QQ ȬFר1NA҃x߾ \%t9[L-܃n!n~uK­V kp/b}qx^1J,1! ILd˪իiVopdc*>Yŏ߿߾Z|w޻Ws;~bl޴^BX" ;5tX2c =܂;^Îq ݏ ըܮY{+XЇa*_O,  Xǰ&]=cx mQ{#MExk)} \~*0׋$IIHHHHHHHHHHHHLHe$[֯_nŔū߉' ?[oo~;nOoJ.Сø_$<`W§b@ @T#~ؽ{yYh0ÎjvCxoA* X۷0kytT:j wyF䱷S{؟ׂf7!({?Vf0R>*7y)j*ǷFbB-ϖ?G\y/"[z'S޾XAӈ֯GbL$~1-+g{^y/}&|z8vlێ܂(T?ţ@0un܃證ǂ;p?saA;tGcR\[𫯼5 ::N"d IAp&āX},ݰ q- #ɵL~|M~%"!!!!!!!!!!!%[t]ʩ/\vwwՈOO>;^؄^ pܷ N9o>w݁G~|Q :,{vE~~>40BA'% (8\N+u#uo'p~8Ҍ1^Yc%s슨@]obv,W6 ʫ1V?N" [o<εↇ1(hnVuL>ލX;UTpS4$l{SqzVY7ߙ{L7ziB$B$xBw׊{yUJu׻~BLi\ʪ۶+*ݏ&:h?f1w??؇-Ͼ7-W߂O?]>5k#P g0~Bs"oW)B)Ƚ|s@FRGPn@$"υ]G9R|D1(#BOB%*,%0RWco(yiI*R6a<_աD~([!B!$u8oETR5 XUlر#֯ۄkfo}㖠Өxr|ԯ>m#?;ގFkoe1w]J\ݤ6"iWO B_} %p<85()D/SجI|N<"Pӽ_P[솼P@t*YH*m3 @t͸&UPl!B!lfV>;v :uS5qc u/DwěAn+ 5W߀IfݱfF^"x2PEB&JȽ-IfD̒0hJfRX,P0e"T1q=.\)oq2=rZO"'Y/6%PUE|vbF!.EKmi\YE7<-B!:Pl) LW%77ӧOIxxUk{h^h^G-mھmjvoƸS1y8;~U.Spe;HWC%f䛴'=HO7̀qlK`OACWu6e\J卯 9P>X!ďZiH>~,RV'fHF%跄unI&3Z_w?mV3VtPmOD&&_TQ__"峴$TUOM!%ǯA#4F^b !B![Pl!B!ԁb !-B!:Pl!@B!BR-T(B!BH@ B!BI(RB!B!B*[!B!$uBHb !B![Pl!B!J|(B!BI.4׌b !pPl!B!ԁb !-B!:Pl!@B!BR-T(B!BH@ B!BI(RB!B!B*[!B!$uBHb !B!Pl6G#?Xqƈ%Kh0A֒ lZV4z|ߴ#RE;NB eAm_UZI^%/ ")[^myoZg-8<8]ѦƓ-g~WWD C3iR&&|-9$>7KZNKZѿa|{ܛ0f٧Z:>ٞ(,dێ?I'){'fKuB#I:ؿ'B!BI *ؒm :\] w0︔Ut }u|xB먆<֊(Gi3 sdXGqɠ('?>HQK |7L(}I^B M)yW&+H7z)>I;[L~k$+R|W2΁dOon(>Tte #⓴.RN߬p UF>_)A+_^K?#Jڎxad}2չz2RN3;2 ̇{ot>__&^tNdLX$2#GU("s)c m{2goFw iݷ<ʃ#cĴO̹̐A%wQ #K-B!:T8E$UEBf н"$Xw%֘^bg| o^qCyX{v}L> \}vQ/Yx_$l -o5bE c5UX\Cy(cB+h}/q6EkG0< ˂y"P_ yUK)XO*uL#M"q3ɱI{MR̝+]$a,ZiiiXi`ɢH[%i~,oӖbEb H/ ~[l.]"\l̕煋b"yx=_1-#nRS6yT:T:\W:\pUT իWGQ^kX5вEsm:vh/ѡ}{t ./^K!M+ct1\,]*cۺuoAB!BR'hf hq2!T‡$xMTn׵]z_M+ fhKZӬ' 3Q xҧb !#K1Ƹqsqm7+` NDb"Ar8#&?(cHY$p|DjQ|qnv=h1:RF3W"W /c L\]3-2?Il]QE"O|C׊La>!l\5 N:g*hm5j(CB!BR (]GlfKkB&H %pAqmŸ<%W_Ŀ:6{?_/E^IBZla$-q4gDsL\`tVx6xH3D}XnI3Rׄp/9m7t~F3aJHRnz?Z mcĈ6 W{/}0Kv;RRf#oFt]i7pB .Q* u. Th1X!~;]"*jծwV1FB!B*oϖ0@L $wwfl8Y\L4q n 9YyWt3/Os\MX3S{4E,K)=#=FQ k֯@+7O)aoEvtZ_BWF8LIڰ{1]+%R! =ҟ}x%Y.Bn.z/q;7u= T\D !sjfO{-J$̀ѽd\|~z$>ڽSe>̯:VŖ(B!BHŢ- tH7'PY1CCś֛QE&o ^y:4\f%:Nۃy%(quTtJLI.5*YuG`Ro7DUQ&(\/bYŧN`)عk-ϝI&bX Qر& s s0{.dJD֓uݡX(ҷ"HFs黱v"4fgl]33{D(AAnkf!s\a1X3&Ib=8)sK4G3ht)Uz[B!B! (Dfx Ad7ww> n,Em"\z%~z.yc ~"\d9z:+`|M.Av Q$%u8<-yK.3"t3;~GOxjkTysJ$HNDE\7 yw^u :S'PFۃ0/KՓ{QxV4`sg~Z?uĝUBcExt!F?Ge!l즻Ξ%E[\d*h\Mz5|x087/BfĤ|=@nB!B!'$)E,N }O7[$`QhxKs ?>-nį/tٯq韮/̕d]~w(g:V=Q(6C`YlItT̄ƴ6m30hJ?%m`H(ȁ9: /^'ĥ=~e%oĉ8|ܮ-^z+ʫ*, g^,}^+KpCxΛpͥ{A{_7T /6niGmбxபVw>7G۟DO>3wVǽOFCz5q?WxWU*>W//CB!BR *pa\l9[[!L(;\"[|ؒa hV ,x)<nʻvq˨P; 4Z4nG2wha7#%^ cpl\7>^yp::YT)@!zci\}#[⽁pTޙ00syc|4vlot;R<ڏMa *5z!w(A·+( ?^yy>\+Y͞G`l%#եU1u+ć{Đ-nW4.>6 sOAS1pL'JPhVRDY6B!B! 'h{V^ߨ `7u'ky?^\ ^XQGbұ1xom2ϳmluMǵQLT}7 >cq ױFړ||u]]td~MC0/ ^;K ;G?UgˎP,ahrK= =?9m^6kowŀ1p( Zz'CuЬG_ zwFگ߬ؓ7DZu4Aw1m+|Ų Ѷ p|=eh'f}um>cqJ-8|+h5Ŗl[!B!bQĖ@}iW^)*C$h)) sGb񥨵>*>&\ګ.nkGU q\TL8Ǧ@%ȌaG?{(dҏXi=[]q_{ؚS(bdnqۏ"3!0ato1{rWaTKGpg ip'p0-~V{ƽ5E88"s#gc1x \_|;Co=Zeێh\==)nVQ.X=<{=ҋ];~c{B)+sSlB!BXT<ŕpH 1$ДnB?kQQdd#;wNϾUVF7pǬxlf>n^[“==&i&Ȋ-5m69<~-R'&^AuůJ$vd "M]xJh9ڮ/#cB~$HP!4.b*K/=9 p}NJ'{QH.:#}q]%\OiHIJ 1ۮ<ߊ-|cQl!B!E[<&tܤ8҇ƚ[7O զVFQYW꒫qʸkC躲!J2y "G7h0cUqUIHƸҡIEk@,9\ܘU_}=YPQ&y 5$9RϏLN,||G'sw>')[m~ۣcǎֆ[!B!$uxbě*~fHeF,H a\lމ/S^DwvF݈G'5C=:tlg$C?mM˩*2 :؎DŽb !B![a*_2M̊-e˔xRl\'}%˳b?آ(VP%z KlegR,O}ZQ—:ZfX BLj~Kp(.K7"Usoڂ'78+~iZO}RQEu%E @Z'.& ZQ dICz]‹|yYS2B;F}FBE"%G2neEhY"Aݶ`P?q?iS:K룘nAXDRTW┶*n򫂘GґwڴZ>D7յӿRCJ_Υm:gyka|qJdu1r:^i6SJ@-#dWvB!B![ϬpI .qTRVl.bfbˏ~ilT\I5i"%tSܰ,%\AB!BR-߁vi"7MZ5s}=G]MPHCȹb !B![ ]2]v)veQ٧UB![!B!$u&AݗC}<Ƈ'tZE&\BB!BR-߁o:]2J@&ɫ֣fvcYB[!B!$uhݦ*I325 zmKٱ˶(!-B!:PloY+N!vO\ Q*ߊFsB!BI(|+T+a^ S,V$s^."}.!-B!:Pl"t(2vCюz(VۓVJ4w/"{^-DA(cMJȹb !B!T*$}%)Ÿxr!.XJNx\.dQha\8~1x~o;`d16QG.}|VQX/9>HdvwLx7kZnFiKfJ~M"\W#Ug 5ꔴFٮ:carD1iC jⷶԉK9mOh\:h= D$\MHI#_5e$ ܥ+4:6=)DLZj]$_O>Tl*22~t_u=I'$y EC#|y_)PO9 ֦&D%%uΒ>X\hZl]|l(Ws 6Yо(-B!:\PiFgM/8*MSE"\<Т6\lE_~%b2eHPҟ&XeHHx4 .a G?Q !(gF0k<M/9YT$TЀ=]Ճ Po<)+q ՠ^| <+rHcf#C4>\mԓw2 ^Ca& t#_1D1)!]'WŤ-2t>1IuG7d\K][ta"axsmGWbt 1җ,%]\cbҮ+:Ew'rd2ViCfK ;#ZώU#:*(أ7W*+Qi-B!:bU(AAfth!QN0oxd=&/,*™d _c<;X %ЏT"~J*9R6 xV!%J qHRTl1AHH܋⯄,Ɋ:*ț [mOkykpԇ]Ge27̜O=0ny3>T`?]@dtIt seed|ڨn3u Tr} US1G3so-f?Iݟ-B!:bWi $>LGAŻ 8 QDC5 Q`D媦f|#9ai3)9,|GE%<7@BTłPe!~ :CRG\q6l6E2A:82ߑo*XTq~ُzuwB͢")Iz-SVW!4.*b[RG)k4%%:Dh/]$Øׯ^^:tV Yvo$ӑT4s#Lqp"fX1p\ EҎ%[nl9uW' wchZ P"U"Oܐ5_|bҦJ[R;Q?u\߭)yU؉җ‘.wҜi!o:2k&,3 S*R"]}F)ig>9|}5J9 nBX-#M u5]U{/|VRK|1b ǸFS~!"_I1 H21iX3{fŨmE7ڽ5-f9}b !B!Km^J4DIՍW} Rcp79)LeO@"s,N QF0ݐy$ H' QxHؽTTq2 Î71`Dԯv'^usarpՕUpŵWfc遣> l a4E3Kv g,Fxt*TѽX2o×[q͕7ƫok}bችX>cubԾVT*lƬNoc_[[4#;25'i7?X}^6No7]u#.Z=R;04^s-z :#ߺ3w$W_՛vŐu9[xqWW㙖c`.^>OyJN`xBƣEUzDKޅo&d>o\=m>XfL9-Xy-B!:-d]v̌F΁3.&3%74^Q &oȮqjWwd"9^3"cc8kڇ. ϢZt6z"&tm?5G0~HL vw+tO~[L$1=Ӏ?l)=PNQ#}: Zu@V3\ȢwZfv?[!B!$u8oB4$=SlэQKRك>K/C)IUPṷx|l tú9a}rhe tZi^4I%w^kN\lP ? ]7unh}a}:, O?3[#N >|L<]2${='u3Wd!^CǑی/7#}VÍD_]BjSݳj5KCB!BRTl)8[DTL4p1Y;}wM,.-/~Uފcҍ8k1&|{ 1jeH{ڏgOҾ n+'~z=(pcRf Zq1q^Z?%rkl| >ܝ{{K1mr%sto?㌓)9 SWz;|$o9C%tghYPHh6i?njEY-^dz ੺aB@Zuj^zNtnlZ|0l-ȄVP9آe%Pl!B!ܳņI'srZUGqqϸ;ꚇkq+Њ1d/͸}e'<tG|mA1wlߖfc1zTr${eGxϣq#&L~{3er#v1z:kInko]n&~85,DA\z#]~[}㱛G;ǔrhZ> pJOFho5GŽ0lQA> c"gKƧ#cGd6O^R#K3ldNCO}8'jTirp Wag-QֆtTO'*ar_]%~Dn4YSۦ~'.ls8l}l[zO>Kj/=+T'N<-^i!}e?<gvp=:vd.Z\Z Y''5B!B!"a.IhJK\_A5wϯ C +]WV>gPV<Ǡb0Gs lܴ(D[l=ay?(BɉR7˟aC=]mƭQsA ёXqCKqɯ);q-v{fdO4t0-z;0;x__^_Uy wWyoc[: _ >U( ;u[guA_'W> 'lGB?][|=dzRPr}G[\qeU1` b*6a!:^> [s|8{aQ/~u9=6{`~i.ҷ~:7^?]r1~᏷Fϗ#V:- 4'uB!B!ygKrWRZ Tǂqpߚ;⧑v|!=#Gqq?| G>ħ_'Ű o|AP`lƆ8}WDnkbBdߍ};dBE]Va߱ldJ'wcvPj:zHD:&݄Ǝ#Ƴwh!m|-8SҨY}f/J"svu@|=Qۗa5Xf+o؆ +VVoÑS(v|A.EXt߂Nj]&%KbNlݹ6Dz. E2W*M|앾8~qe-bCI_Q߉C'QPtt3,KW`ؽv#m?n*@r",+bS#9(c{R-B!:bKYrc"͘]zjPz%⛖|/'}7[u1j( [=6BQbMz0i#H?Ja{1˾q8A2HU}bfq%rL\L*I?"yGbҎ/tߛg}K[ρTHU,c )R?_lwC_EFZx9؁\AR~Ulҥ_+!-Җ̣7z XW~#U| LfҖ }NGa 1|~tYBB!BRvoI.y#$ wOk^ʓM*3~*G\Ɠ?īuG?]uyY4rwo`a2L\eJ*FƱV Ӑ$VőPɳ _ =) d\%PZlɆ28K mo4o-F-f2mF")gT@ jMpx!ҷ V.MJ `'ceY;'#loLW>tKb[T̐c8U[VZifhFsl!:}4\ۯfy*zOCrKt#`;R6b|JM)+/XPy&̿Kz(ϓzPl!B!<[oMm]k $7@Ú[zf<5x3jLL׍ wϏ '`0C[ !}89x_lgJ3 W]>"ϱ{xfӒb'_$܇yM!._̓PVIF/mTs!(Q_2'g21[LBU6Ƈn.#-j'|]Y=HE1+);a B B!BISEhy[|ҥ5q EG̍M8p5Dӄ%'IB] cIj2>i+I U_0V)+2(LS1rbK{: (PF*I(HGLԬQ]%]d>4*Fw=G!Vd'om-U&t@ҷVMoX%vCs-k*H~fd"CNH9 L7Ẻ #Ѻb5&_@ƣ"9 OK'!*v~0l)\*[!B!$u*ؒDRT뿯~?)HKlEUK0`T.X4WqBd.1wxPdyl';JPQ ^M~Ē-ȍ?:d%{WB,Yf[|[>iNL4%ֱS{}lڻr+m?&G#WZ>3I>!YO~O^n[:V(H~ݙ_R -B!:b InuQ36[F*Dأ$~B9b !B!iD?Y5~M'g5H.gJ3e(s B!BI(|Vf*""]䢛ro%rB!B!Ŗ|2{jk\Q@z|$&St\}O9b !B!T்bKHѣwTht'\=Xhkwg&s B!BI.doŖkO$#KFE;'c_B[!B!$uf[Bc0m@Ld1Fr?D>t7B%[!B!$u.#*#d}b|?2XC(ɜ vX;0~dcrB!B!Ylst,)RV7$h8v5Bzo!~ ޤho"W5An:w6r//}$GD72̀6oƞ'-f}нb:[!B!$uY.f3՜_Z,JSJa IUP%ʣBa`Ŗ; QXDy#ar2/œArdG֦p6۶(ŋ=:!W&Lx!pL7O|UkXWu(B!BHp^-2E5(5U0ĎW Q5Qh9CLl+!sK_[GȉPj\u\׾q$B^d{?y NI%-i+I-,Pl!B![*裘+%wWѫ뗠h]9#(wpN Gkj 76'1IJOF9Qp+D9b{o>ܠbFiDZcWڳB" $(.Gd7Riu~ JnH}-lPl!B!<]F$hd<ʢ&~y;;&s10Ee?5h2{}آ#&tړ~Q"G مs}hۡ xYNT^EQ{Xtl޽9B!B!y+Hhju[|p1"?TU&(Aю3>0{Ls!7w0}eI ϟUl9t(fbb1wyq .o[ztpȱAnl{64-/[!B!$u̖--g[&S%2&8T&Be?C>{ mj.];f^ZKc=>NZ"Jd/x?@榧P nk7s1߃:>@cŖ-B!:rer1.\'}ؒ[…2[D8%2]: >0.*ձ:wh 0k{,=ln03|;j;> D,$KmSl!-B!:=[Jl=[e`ز[0yW\EIjx`迧/朘 0$Y |%LWS64]mU?F4 ~Cm-^s ^~9qn 8A4p>,YKZ>|8:vxڦMV6BrAB!BRg-_۳Eo[N/#J:Uu =y!ܫ8d8x9Gע6|pkQuwM'{ L 8/~ ;J!EZZwl:c1"]v7!:0a L`KLG  JN҄ B!BIZl6;Ŗ¤r r6=2{hϡ:Ll v:V<<2J\1 <oȌqh81gc-I$2?@pg$OAt7#}x9+Fp[n~ F &#K.!Dc?H+FJ->:g<)C_CB!BRIlTR.|OfKrQ*-ɌрTو%X oM{Շހ+ނ? UF݌W'܃) w]m| E+kdu/ Juϖȷm:*Nj\+xFދ;~O,s”eDR'4chp&[!B!$ubڷ-SJĖ=[Ml rSh+Xq-pŏ@7ɵKxJ3 wOgcE=y93Q5yg΁+Kᗜ/}hf40 ?Xn)XgBH߾Jqr !&lB!BI_lQ;b-l!8c=y) Hߑ .rcT`UŌrAÓ/6DPh=1+FjTbkSRȕ7FUiӪڭȻf.ҿC֏B=J$h}W[LM(B!BHgL4]J=[-bL\8Rl9_ ރ؁&OZBs6R{h5@d\KR8ߚbB![!B!$ul9/Ė3L~iɷf*jYyˇlF\uy!-B!:Mb=<[r1]'i8)a yW2&AB!BR-Kδj-D-_?[l{MǡD;KB8#=u( _8sB!BIZl9rAHTJgl[.\\!1ʷ].kʗ B&I3!B%[!B!$uAb/վGltR-g| D^Tk 1͑#HdE 9Pl!B!{+|Slvv -Jl4 Le{De\܅H bbsB!BIbjI-)L`bKd (sJwl[z_,o ~!\BB!BR[N -b~mb?jfKY`M"@Pyx^ Vl/dKidny9߁wj4.߷Aps7_[-,m|ͯ-u/-B!:rZp94eˈ4tom F TkA㱁%lBU~ e~HY .clqF۴mk{җV0"B!BIOEQMmѣսU< V5,Mn{^F apO 1&k ¬ؒ[27b_K?v’%yFDOdb5DE!<;/V@p\v)WիGС}{Ǩ_bEشi=Е6QiHiXqzkoRѥG#co6{F|SōvR"W|_!Ŗb !B!"id4$ɢ"]1SfQM(k Lq &#(c0œ>b}{? &27زQ(˚)['@FNݺ_[+ 5=ۢC5w.-[IgEC(! cp8qW?|K$$T˚4FE]$ML+K-m"wzpMžA3[d~ɣ?eDE6--B!:|rݙ-jˈtY1kZB=wV܋GٚzË; 39OCe|Dox:#c#pt@dC(Aa"Vu>vVA"ŖsB!BIUldVRhb+,e _4@ `l9*\v;Ǭ1rO̜GWc/ǣKo{܂[^GdWG"kM/ >a+?̙H0x*HRVFwgO2aPl!B!,~rھOlQIlX4T%FjFf=.ܿv2 \l'][^.mCѲ 8 WđI„t!F<'`\mk}V!uc6b !B!g[b[4[B DLl #?Mmco=s 3_ģ1W♑wcHduOI%_{b[#?k䪙-+txVquàB!B!,\g"H-F!U㱡Ƅ*iize: >ϸ}\eTx~'~DO'!~-V>86v#q'Ϛ= cڷok+W>pŖB!BI.d[~iDbA-A  A~hp$ .7Lת2z<<^E;[tHl}-͑1h /4c2(_0]_ŖPQ%|1-ouktV|7-? -B!:M%RJSN .lvK^2e\-*_[cWfޝ/hZmn=#Cv(9[ZԞ3$r" tHYa' Ť(TGLw\ԩWbn[~[!B!$u8bXR[T|K=#fxYcSWEnr|{Qk\+HARmOql_&GȭԊTPn qݵQ㮻PQzuFK$Kv >%Voeug(B!BHŖ2eF"i*LbK.u<_ {&FP>DpP< vpqHqZ@a~ >'ޖc2O,k&dH1o('R(6q_|&W^ϗRҗ:&S*RƕkLFkiOK _#/5\elbĤl$sXb !B!PlXQ#>Tg xgi[Qw{sy1Z.+FhVFKpq/[Pgxjz(zvg#qUEbܱ⇱"IRa#EpLR7ckV "J3Ma\xQLIHp#$"O-#e#iXƭO`57IfDW!E8+eTr2OVGe;Ia(B!BHP3[@~i=-Fo3:l5hݍo7h֠W{aSC<( }<4wt]M2TB86 |M<}>$4=h:61=c]n n[$ `nN4'\e .QBOd8{@tx&~i<`Ct[K<R&!uyƦxx8Pp _|bۯ RD~\(B!BHP]c;Hʸҗ| ק|Ç[]g*9vVIjCB!BR)Fbޫ^l4n lvic7&ŖwhG>^]٥!C << ăs}<4m]*e,1:񤫸y+xZl͍K)DV<GnQ$ҦjƎKI$B!B!C[DoJ(]ȯ0R~@A}xA-7h)@Mh:+ K 5yoԘ%(KoR%#}h=薳ف}>[Dߣ*(5iT :4/ºiXd19D8Cؾx9{7`%H[k6|E#;ڇC>B^Aϣ 01Fh瘝KKql/\7L& NXx7* IjCB!BR (T0dO gɀ?q/n f{r}w[r\5xaB<0}bqp|懨6'D  &cgfAء3B1t=Xug 8zЧn}|%^}WW,,W򁟢iqԃS ^{+\_'zOXA)=\KvFb1Ё_^~_6Q]'F| JSvto |Im(B!BHP1{ʏf~PPgġ͇,+Sm7h{>@!.q,qϬ5d.Zž[B>D P{9?=BTڬpǜf>!7Ҭ*:JW՝ȃ\SC"O9(fb-4~C^%٣wTi 0p_PzU<LQ܋StRX`6xO*rOQXY.7\vGfϡ\~"=_eDzaO'HCB!BR--$b Gb.ӈ<=o{ų6㹑QĨmgTw܆;n[pM:TUM}KPX'RE@b!vau.h}wB7Uc%n֍`N/[ ^-aC"_EtNW7bك5?76C!P e~J.l)xꕷ]y*D\z^lxR "dc2NZPl!B!ᇉ-2S[Tt̖&?J|IDATTbK MВt9v3Y0f>=[_.F/]˽g[ݗ YbsJo f@^\@7JF gvcE.hugK :".e#ŀڍI|x\ezjVN$s ͝IӦbA6hЋ[AG8úM0@Fjƍ=Q_[“M:m?e M ӆ`Qqdxpd, $tNR-B!:|R&[Ԓ-fQ[lIŖ(hƇQP~8I~(@n~>r ĊcQ݂u 9E%;UDB3XiiSZe(Y!XaG{qvZckMtZ}₄%m{F8yƆ0`1P瑪pOVqXpJUq`aSG3D1JBQyS_͛.BkouV 7=[_[O x{ܔʸty^JDR-B!:|آ˙vƷreffPi7bDJn+^<1{\|N$٥1 7 ݇OC'Lj=z6z_F sm4l.ӈ-63D)ƕ7ma_SĹ0Ilʂ?; +Yg.ǔy1q* e۰ m B" ݳETPE`-A⏭qtڞj$qiK(mxp#u9G$GO7Pē*)S&ڨY=_`Jṁ.Urd}bQ>0.ҽ[TAtؒPl!B!bܳ+[BT)-e @O]z# VnIJu0oZق[k6c߉vcѺz+ڊkwaQzj/IDwt("V8rat^y m{QqClK )f¨$[+uu*>t)2RtMABn#~i9O)eO|q/*he+b !B!^*tb\ʾ/#NLpeD?#) Tljdֲg$m2'kW7\KkZd4F.ȱ?z]#?mSu ]cOTP[y^k Lܖcwp%>mPW2!t^i1THQ?m3J|IYJ=^fvs_Db !B!g[ʭ[m{L+jߖo-_fB[FbB!B!B*[!B!$uH-\b !Pl!B!O#B?[!B!$uBHb !B![Pl!B!bKB((B!BHp%s}-CB!BR-T(B!BHpŖ1*JlM-Aҋ;ڷrjVZD\ByPe 6B~Z(B!BH[\δElJlPeO+HXR+D~YM~]JJY0 8ZLe-B!:ĖeDCbœ(S)%)|e<&k껦ZPl!B!܋-_fآ|@Hs]M-eKi=BKBO B!BIS@_JʿYY[ \#߄L(Uz$Pl!B![SS&/˂V ?#%-<7Pl!B!-bcp裸p[T T1^mZ.XEVEE*j5B~j(B!BHpnŖ *dآhjʊ\58ՕAb1)\"[PyVV'䧆b !B!آ=k jRkIœ`]ÑB,;tNdHA +*$CB!BR[[ebK鷊-eDٸhL2܋-{}%RhK>'$P͉'p0Ob偓xr|:gm;9%({R^Mŗ . []f$ 3wjڧozol=}gi>k91hMz [-l_hRSiXk۰@j_RȦh6+B!Br+3ߝ]ĖYRTb˸l\b˘2eй[tG5n $\ #D*/RQ!td%ؖu'%3Krf4x.ۄ{ұDrN7Үk0P%w1LJxRFL q& ܫArRֵ^k=i@KӑwmGD  ^}jRO !}zr y3C]+. uG-Xv#lC(ƥ4Lk"AQL]rc#fƗg↴B g.˂VU1FRWO bzgC{+϶#)I[fHmS>=?ui6LrH CB!BR 'ӊ䢽h< jK;cȦLܒVSwq;԰Ux k\vaSXy<aQBC\8X^&#u,v') B!BI*7 fa ʽ+#?^a_K6c辡o-Mdzw;ԈUx_9/ s7`}F ӷAn•vbW•v D'ZOW{D $Q,Š:&03]CS&h󢋋=o^x zMڊ=ENw?9:9HA@1iʋ :+5V.21Q mIړG&U>FСv7L9axBM@1%B!B!Cl1Su4C-) lwsƼxxVgqf.4O ^,}3q'Qh<`߉hnar鐘^a(VAg >yڿ v>´R֓`_uu!\]X1a"ƍ`m؛ 5?=j7]mիpxRdP} W`#c:E!$S`#m="'.Fy<kǚ 0jrlJðFoS^@AA5m% B!BI*-p"f}Sӻ΃TYxt7\_G0C845[ w|nq4h뉎7\~w#nhmۿk͌S2w>d-4{ sDOpr6ϡڵ*㎫](bȏQU/;aѨ :Uw7F`ܒb !B!OlрHOx 8}=o &vm> .Zo1h>D.r/-/xav:5: zyמHʕ~?K`7s0=n.`eqZ2]"#}Cf=15$Nn<}v(Aɦј0+]hT&>OI0}hT<z٬t߂]3_+1ѓxVNG^qkNmov"YZ:T e"1Q7 hb0V82Ҡ3ZWAaS09Z\SP0ڨ1^x|ϥ.*$0[!B!$upbn:~-*QlCq7;h/[ U!- Qkqx`^{yEGxh:0ËB1[(.}y.Bk჎,/!l4yf|:q ޫ:4YЌqzmLƸ% p",YODv8^d'w0|T= \Ô0ӡc[6upx|[xq">2'pj8|*fߏC@ oAnˆFs\?XVd"N77|E~:(B!BHP'ɭG A7īSf7h%4_o`K< @!^m]wX0L:0)H-*Me{]t4 %5.!lR=[/ZbjIX>/uؖG5D\3m!t; Jm&76œ-'#mQl1;}Gڀ.y[y4Szo1y #Oq>(V 酦C߃>b0Z1ФK/4{U|$Y2NS{6o-gIB!B!C[G"2pJ7>w%yq=+B^.{ǔ[ uƃO@_w=f*&m>œmP*4oc`\Onƺ/Kु۰%E{`GPh8p{~T^}:a ahtm?\ʼFnni+ k:M;ዱx.\’.)*SVRn'b !B!oQ'Ҿ. $$m2x.L܃7ׅhΠ+ ^<8 pۜ1n4yx4{ѥD>aL1j|*,E(GNOᬹkoM9XQ"B$M/<|?~ ޘ nAyk?釡ih|SژXQu<@ȅ[ŃkG#Wg!="!X4=^33 xszn8Sy1V?,rDO-s[$ɡB!B!C[FjF(}\x{}4xfN1jL.D)EsJ1nurV>kMwM“x .F|7XTbAT*E&`B܈}I9)IH{!bFn s$$XN1ӓ'r%{ #:>G`Zږ ;}О$5G;r_,}hq)BWұHچ$@zO֗ W|6K +#?:[!B!$5bSݴ ]=\%o wꏻZýza0j$h3w+;ZtǤ[Jꭶ*.+h*+J/bI+j qGG+ iXKO%9Пa䧃b !B!Pl1V}B#YAD@4$T\6`4|9}1/\E+aV0Ef*\pHJR fΪJz{t!#qY,AJyiOI $E-+hw+'1T1:H\Q8TPoB]IDrIo G1""_Sl-B!:T r#>Bѡ.]H"$$LJi.lضkNl۽ +x׊5xhrUq|O8tɎ#5Ck@w|)M RFŕ !>ĤB  U:~uϖ0P=fhV5*hfxo#̞1ptڶIHҧa ;:4{4FwQuI~G_-gCߗB!B!C۳E1l:GbTG˓|׫-ĤW"*MB-B!:b%jSpɔ\2b !/(B!BHp%s=+o&k 8*̲̖͊eˈ4E֥Dg-#Tlٍ+)cBB!BR .|/iߴ߰9͎ҬT[.Z_{&E}t ~x=ޯu=s(J$AP $<3H0 (((9Oꞹ^ga7s?ϏZ^z3Zɰpa*[  a [h la -?a la Zb-[-AC۪[-@Cא*-?x-"-?-J-5[ [ п!n+@F@[ @@#a .a#hA@ރ|3-? l,U[–kT*E lI8e85SVS*ved -A[ @@#a [h la -? l-4@F<–Hb-+2-Ga Y"} ZtE9(Ӆ-x9*{b~CMa [#DY-3cp@C"cDaVp3AC/m&d[^ /ȍ!lঀװEq5 o#eeGr6L r a˲dRb2lwʎQn[ hlm&t-Aa(KSTLV-eǩŦ$a끋[R*aK Tn[ XUa!lGe l惰&bJТpU_m-D-%QΖ2ygtB@NAkR֡,ZزZ [ԷyAa *Ljp{-4@F@[ ha [h la -? l-4@F4@F@[ @@#a MIS-^ la -?4I VD@^-4@F@[ @@#a [h la -?܂,cp@[来TTvbAMa [h la -? l-4@F@[ @@#a  [*tU9N%˯R22,D%/PsT'(60N>@ka !l ' l!ܠzز {J^xY*^(-sNPl-[![ G"l @C l _@@e]ZOD@^&f"Z[!-?ha,Fֶ& \7 lIu`%Fࢇ.[@5Xq[@P+[+@PEg˪r:[.Ps[ la -?~Q:aza r,wC-a Zbf [V--l@ФtmD췆-mDΰ"n1[ 0[mزZR*aˊBpACÖ5[@FTi,שt5*]vJ^d*^x知ǩh ͐u~J- rL-$lJKPK*l)Yx睥TTvbI6--? l-4@F@[ @@#a [h lI2DӰ>NEp@C`- QT4wbO@F@[ @@#a [h la -? l-4@Фٺ8>a/ViK3 [VWPr*]yJ_f˯RT",@ŋS3T4>N釨pk A lIˆD&H f˩٪lu*.D.Rɒ T</8KEQќ-jeTl=li-Ke9*w枠جr|e+nJDq+[V^G,anebSlrec ha \Jj)]-AE"a ZΛ;[1)6ٺ/  lɿo!Zk;Ö5TJW] rK_"A9*^ rg WgKmC@1"%*]#DtJV^|ѡb?aˤY[-? _zТ^[mj)Y!aU*YzDKk+Z$hYpIrʎi_l [[[ hl]5e,zвJWY–eΰEVQaK?(Vvb~شp~1"--?4)$UJ X1zز25l+[}-sNSl֯y DNN&C@@Cdr|됮,+Ջq]6Ök2[–#N9H ӗ+[h0[  [#Cj-%[ hb)^ [G7]Ծr\K2'Na 9a م-+t7|6Tm؂S@H%lo"r- ro#J a a [["aTTvb!l&?FFwO-꫟pAC^x}n:[ [b \g-t@P-wdxhbSY[j%l4#`"mDB.pkhcD%w W-ɯ~6–.[ [d-[֑wXWcD Ljde n\G|;[[":ٲ,#AE[R U䫞ڭa˪r*]yJ{[Rix^[9 L%lLXQ[V\R[rJ l,҇-%]R-EQl-hd-ll)';l1-ۈ^v[#lVc/[O`MEg [W?li;[E5*Kd }${Jݲj|稑BXf$OO5j5VYWg鯭RC~"&i iv\/ˠ.8Caۊ[a-gx*kyApz-O$<:]OTSMMH*v8=v\*Jop9qME:|={V&&*TPem5U6^SM25 TS< 9O5%?DT]F醄1|]UkF>'_g*>/U|n /U|_ 8@~yeK-3HAլ 'Q :jTyҋw2X}UT]s_?V҇eTQ}qJPSɓ'x8UUUJ*S*qL|k<3`+yT-%*,:!ODW%mmwj.25TYQŵ GVĩRVTIhRCq;.1 k+[rsW6=T]㹪{@~d  +JD$)^j^?+_>04Z/tUh!D|T[s}z=}q:N+ Zr&QSEk$orIT5#CqյUx-UpUJnE'!M<$z6Q^ X⼕`y\5oU}m #/MʎPlo [eg"NIJ)?E?NǭM;RKictRq*UtYF57ЮҝmGO;ޞ==}NV'Si:_'[Z~\箢1QOpD b*ISQ+>#u[Vu[朦ج#-%l9Y%UTU(qWЩ;iy4h!*?:ZI?EdHBթRT!i fnD|{Ө=td*>|$u8>4}} m=<ҏEzCD]JOGGz7gC'깣~RzQ4pD3b&M}^U1j*k_U;[@3h2l}Q"AU3W^8F[./F/ݧ.PKsStF [*j2MF\>E\C=H3әZ~M^L@3ыcӒ}i㼙ƒ}[PU*:ޜE㿻BtUY@G#SjT%JF#~ZٗKGx=u [~aKaVU&Ts %䫓߸ƌCB'Υ,I׮S~7\zEfϢޯgj)./ԭOS}NHTSyh䲟駋5K'wߠӋ ]gkqbKRݪTUWrdZ#andE9{J_Wd*Y|]xY*'f" [fJSti RϐEվVy>F?~G?lZDOuOV/ߠD%{*kwӨiФo髋gi>4d.T2%'K#o JPM)'T~4T9ūw&7LRU]IۼCcV}t#JzgZ9Tv6{.kT%F7Y YA p{Pe%lYf-g [fJr '踽 [OdECQ}rYw_B˾;F+)^uW6W%.iݙ+t|Vzy7tjf4x=r_sw u6̙KouLsNWS羱f7XIUPm|+}PolAڲ|޻?'õ+kUꫢaK<ya p[DcD{kI)wP奲<7[(~j/kOϩU-*M?bD8}b'}7:v9I7-af?]ḀSiԪchڽõD qv~FUWWQm:pNyB'̠Ҥѻ[>y:.+tnPM _c OՋr, lnnbr;[NY^k~azxH-լXq\2UsCEV$暼an$ר竩 I$x\ R__e->oյHHI+dMʕjS7^#ZۨZ<WZ{U|Ng؂iR,< WPL [_,:p[䑛<"d|rpc"ATqKh"TM5唈WR|2cHUmHOH#Mzfi iT'<*/.]C"^˵R**^C| U$G=6Tsǩ%`E$l)*;JYǴei)o#2G2 [Q$tpBgS;rHOZQंL0Wx5ծ2JHʚj> +^KյtӍ) s j$Cᚚ5\g+yr熼 :5ϒTVAȝ$A^ܢfشCT8qAJeP)4 \TآVHJFqsjaq Ȏ2TA<|TYsI=%q*j+y ˲y\)gH@'{Zuzq6NFS~jQTURPj/@#tl#[DKࢅ-Wd*Y.%pH SsT2"ιL~yScT40u~R!VV8U? u sȩ۸rN]篯nt쳐<=Y{9Iqƨύrm=~?r||R[G/z{2[/m3c~{/,fp\~_Wʵ'SڪcLKFnex΍Qr~wn9ríthznu7"_K]$8/r]TuϹuI?,,dɉ%RR,`m-QmXw%ﯦ>ޖYE%Wg%׬2T[̵E}Vdy OKL-r],~e.OU+~}7ťkE=<]sZJzg]M;-z0yXbbbE]ąu,<:y9b-ʱM9 ;.mEk"1svHZǪhӹ{m,-l?h2/Ͳk8<[} _r,fR9qF-lnjvEJL35FdT*|r\z&)#OLl2dݍ>&rݣ7[GtH40e̫ci3([eqd~v} ¢㨸1P7 S(lm?hR}c^Kg{EoQN]ck/v|Zn–Ryh%*^x'h *zJfRߨh^MJ&L/~?PtMQ߳Qt.ÿMe_ڣCЯk: ! idvsNߥFmuTڪmF!gv?[&f|)dnJi [?i/?_7G3_?npBp|{p=Esd_Wz_F ٠4ڌck2gFS F ] n5}VS=oc.] ǹ,}[&{֞r%z5%,bWn<_W7|XUI;$v;.RmÎR }mm:, mo+nIZ<؎}iUrj5-mN_YXO/,|?܂m*aCi>y)v}3sSC>Or_9צv6Z%} XO>s;wR4EȻ[끋-SΗø6k^(Pޯv>zʭax6 /sPx@=(c=aӭlfv) 7 #92,1tc5iH& 1[nJY_)6{{8\k([ 2Pm8KCy: c7ڤ&Zڷrg8\R\K)C\EF6|: v3\m+2\)%Sq(b}CkPr*r A%ܦ;CUbmy`K 3{qASCiu/Hs<*pQڕmݜk a 6>ϸS?ZUS6S?%j}OykmAY޶r ic3l<:]c#qMGg8=xIJ}j;'sfE;%׶By?M9;q丠_+kDw;ӂ{'/ g;^qݼ8mzx;$fSM5||g^&.m: wsS[?>?Ƴoo"g񎿎AزZ2pY.zR";O ҿ'h1R/˝{fJ~CT8'*z ‰?Rt^/x_Vݣ)#cpϿ[ױJIq?͓96C=Eg_:ϑ|]FR>G=dTjS%ߗӑ^gecv8+Svz;6aY9Tz5~id0vP9oPjAY ̑Sd~ihI<Dܦ8x>9~FM_wypn Kr24dN1{yVPU]1c6;,_^T%k"\x\FycKbW/Ei9 Bo^v,kqѫTfjNf ڰsd9OevmeNf ئ\o!<>ju̹qU/~PB}ގ׃n,k}FrXn{ȨSsdѹ12zE>8Jܝt:638_Nqx]J@0CӨ2ve<90:\3֥]%gTj>:n:)Vi%^\e3uO-YIo}B'XmTu\j>E3|<Σ+hV]>0Q?'>--:t-}rdB`!培5l+4] e/1 mu0ZO'=L ,oYR#8C\O7O}~!E~.hW+cј:j[Ьshvő"訽T8^x"9p^ !a{8?$c[!{Cq>Qm cXZ3i-sw?MS ]'Sj35᏿#cUNRj_|i(?kxُHoc \v5;l~gۼz|s^[(=)#:=LZ uߪ ʾ] }Y춙e%Mff1%Pg`gpšԄ:eF V_]ƪ6AV8|eMzeMj]Ua[MVŕI_Xڞmge5gyFf%o8Sٸ$yr1,^f[mU7}XI\SV8 8 X-}n~B&-*LZΦe6M5泴 K$qQ=3)p, &}~q+ PtU\k:ܙTx{&? 5:MZN \R5baz_k!#*~3X}FS95Iq?EG[qNHaQ%_}F]i ݧc|rMK>6qodE :On^Jn=tpMneǫ-?cy}k{)a6jQ#>4"J($O,p_[}7>T߫w+E>sM- ڡ 놤MjS칝brg;(؝Si7|c!vF(UVlP7])}l@W꜆^< ~4Z/;s[()i@k6%z] 16`:_@/ۯZ;q2ummk) P ϭ@lڬtj?JJS+rvr7}Z'ޚxߩܾ|^>K) >fv}Lx|Fe>򾟷xN6eTZdY߬3{v Z.P[q |g|X+oҧ9_mͪZ/SѼǡѮGBM?p\޲|,{y{xy^F.6&-|3-zR,/Q3T4O*=MEsNi ]cw4&)Cy| jNgg(dž_r4>gJ(חo r7)vCSDT8E˪N{- "׻i>~Jp@9܃{93O~W뤬Qy9<Py>/SFx<_FW{)2 Fadϧk9e1@ڳPܦBqؘMQ•<U~?s#}+]4 ]H zK|5Ą_+AJ/*|Pۃo4]#X1Mv `zr{EkƐ _$L<^Tz7,J8k7΀u-q*vB.EW05]k+5fU cFB`,}E%d/yk a2|[#dcl|tXيGx^%0*KӨA-є&]ht}."؏}%6N~C ]Q߂m.ZnI"A_ڕqd^>=sMUHc럧-a-) [,.Ua(aE{"?YRO:MqvP侴0+[׷` c:jN*J_(5Nm|U3)f(MYK@6xxa;0eHcSM>[O5CϮ3k2j{J_-cij56< Oy鴄-FxbMȪjL+O,l%tIxZK|\*l <%0ƮAFMAJآ{ ޿UQfEi󙁋tEBQr o}&Ztձ~$PVplͶ{^B=L. +Za ^v}H}uE{HKf"GT"/͕GU.E*xd&t\gqYRGt 3;9E/qBV3MǦɊ-oX|\82Ak]&:8tQ*bep$u=i/bc1?k:ڣeg.!׎<,?o|h {gյ2Tu qϡo r}`62f]Rcc=|qpB]ُ|QBcAPFfॐMUݏ#K{(ߡrQ+]vS=ڱp_ ]rl]FzoqC\qDxOWt lFm=kBfBoos[.S96ȵ^{=uU EzFԕ}VްT4܍,Ǯv젠.vM4CqnTc5PWfV vPW4l|y3{h/LҠ3Tz%FnY dEoCom5xFP&UaEE aҩd pt/)rFb~!=E]X9]+hP-ݠU}>Y[YMcUcY YTrbe^kx/Ƚmo|+-p#pC큋@ \* ]^NeZZP *Ξ⹩j+6 NZNPlfn,RLiLcx|Y4MS;]k2)5ιݴQm.N{r&lu;7ɍR2?ڜ+_2:氧֦9^ku}vH>뼵e'HB &Pg%1vN-K&UpVO(AFH6 ,7+e_l c< IbU]Ķ0hz dc[1&!ŧב6}.6 fOk]!`iݧVRVF(ጻD ]`.Kh _4/!?k)bul*m- [d߯&%Thɾ>-Wm߬Տ`EK(Ȇ/Uʱ ^HMfQ]kbtY/L#TRV#Xj;VJQ[X*[L>gS0'\7?\8 L.phv.qrk-r|,Clsy9ϵ~~$+Xnssm2ׯsKR:F8Jv!p_mtǔsR9gpHKad{@ \\pũ-~\'%qVx_{gLN췗}q5gԸ{7Esz_CȖ#|\e-.{J9iF<Rg6[ [ uz%`1Ƶ] .a!sR'!`g c;I(ck_f/[)6o;|IAWx[/L6)6+ʱџy >/AfM)jzm79 \,>A3דC >!S~|LBGu͑Y#+]=݀v,WOjx2 XJKԴb-)~y}|rhs6m>m?؂6Kn&V>J7d%F.@ŹF.JY҈od2{ Ɋ3#E/n!t6?,uNL9| O UPVPUA\)፷EM3-x>/'cT8> U%1+msݱ<_C48ঁj̯Ŏf#fT~XsE&}–"_o\/uyA 3~ aKO$x IX #r}Kʼž=Oa>vr,FMnN\p(+%q7M}M-/UCzc՜O afN Nf(K *Jԭi);qc /ll%yߩ)m%-}^1.Җ/nNo}nXw۲*l9tpi]OoS.^OgpƯVͤ*5A7ZO!'R:oFh50Њ>cuOeq S7U]v[],|jv$ O-ptv,s`Lyq)䓞M=g&ǩp]-qMcT{?Op,oU2c5eߢ樣ufț_-!;kJa( q0"_(:58<*= <Vz7Y"aKK `Z [DkآfYrJ J"[b*hfԸ,Q[*F#hJ"vy_p)yY"Fn/+njU.<=Vcܯ/Ksyo,e6߇Ӣ,TfY::#y9rmJ@#7ATYr54}nካRkgQc֟_N_HX'(68dԩz}uhLkG-C%s{p\0 ^xVTcc(AƤ[cDǍjYc: UY)ckC?"Dȟ(ȇ\kWb?]kGZKє6 pVl{?߳ciD|ݧ.~b!{)*am+a{ƨ@ƺɞrFB5% jYo|穬Q+eA3t v*lqڱ/cWyہWvRЩrqkw2+ݪ-J|Mmʗu2◦ |. Sۦ ]XnSǺ*lynY>36-<* w7%Fjn)\a@.tiPVHߡi2:F] uT:=V@P25lє}|nу$O[q=o֐U[3}96EmMSW}4\be.\#agnaU%r*Yy=T*.4":0S#Tnܖ,KT$i6XuTa^Ʃ%ɬڂ xW:S,U'meǜh^ʼxT[mc?<Ȗ>6Q|o#fª6.r/Ck=0CNJTd%$걣#T`mk{<)2%1U2KpG|5cS-!۔:>Ha™% ZxsHbg-y|^Jp{-q b!=x]F-YIþ)+a$Z˱ czerz=aa;׹CSj"|;Ekh#JH|þB&JZb7,vh ^Nŵmh>hgm6+OEǰ(^x^Ӎb#@'$hcF8c-jҎϺj򈲊Fi-A|-ɐ%~ Ox_hYmJiz\z袎-_{k T[iSĴW lFIENDB`onedrive-2.5.5/docs/images/windows_view_shared_folders.png000066400000000000000000007034771476564400300240640ustar00rootroot00000000000000PNG  IHDR*\psRGBgAMA a pHYsodIDATx^Gno.h䙽[ٝ1[-[dffYe13333333KMGfdEegw,{<PYe壈̿8Rs"%U˒*]ȱ 1T%WQTdƟQUߣctG*&]r i;W 6`iDk}(ےee.Ӡ-m?%ɏQ{OQ;7˼fG4ܒd~9-v_Hk\/4 jL>>sԁiŧ&lCǥɹ@?"[K+)PpkjRad6'Ցw7! 9e:Wol֥$ ~t{c-_G 0|^݀g,HX#F,$IZd" 4!y,j&} *DBVeDJE D@VцD4@6iʡʗ>.OARԭ']+tԩLʅK!K<ҡZQ疩duTlW) yq"EIH֐r!Vyn:]/QD^| -e6k̗l9َBy"u%}VڥA<-μDn? JZ~ty,䘞rJȒvFY<9[zOdG=>'f!*e YgZ Sa?@2 /GszAyxGe'? ?4 %f}UTU1(?r$mS"*#B|mLI`%}TmImn.So BMVx&#t$UQɋr,Wig=,K2'iyq/ci+J.]"(Knce{ݲewODD/*}>u,u¬_>RGm39is[\5; K ҎO3?̺mLXmd>΃iݏS/o:hDn'R %Tu BRW׻MU"ii7M1 *;W-c[xMnu<"2T} B7 Ww)?'.HT*9ʴ9*pyz"HU2Q"EICQ{m!GnmIB?mS uڒmX/=rrÏ  e*X/5#,+?CS&*AE-rw΋`xDuq,v|>R7%AϷ#mLeCi$)I|H E䖔|X"PR,)|OJ:$Jw6 9oG[JK#)a9&MO+O{lBQiy~.P7yhs|n{$Y>&ۘ$ʇ $^fۇ)(f} tRrpZFa 0%v> 2շʿWGW5O++6.d(轔ue%{B`ڟ'UpHrds|A Ċ/').|;g$d긒ɸQ&kƑk\t{c3J 4В/KTR~eGVIBTf7(SV*QteJ4eƕYW!xGQrIMч'%=Vpl‘IrD\L.sd2d+(,%-8Br'㗔ڋe(5"mJT,+!^\''kPJ9"%d y.(*"Rc;WpD%N%<%$|-))Dz98RIȑ҆Ҕa F-Iٶ76(LQ'g&E<+)NUA~b#*%?m?yb'?º>| Gzl,5MsMWRODRB-xTШt)+WR6F\GW㠤#e92ыwO@JX:D#K(79NIGY>ptBR_JJ8ȸ} 2n-Ag>~$!EAX9$y"&([个,z(T%_K?HÓzc(1MmPNJ”[]i QipeeRe !YgEAx#!5AL+GUvQ%IIu(/) Ee{'MG\FK*yg#yTi7TNVR?S=nEwsNY<%.+B( dg2-.2 Sc JWxmi5-^H)Z,M)IN)5br3#+5̧TPʟ'X5QIEz+*#$IIA\d=I){֑J0z#)*ˇS#M()sS( rqFL필 FI''f$ G$J#!Z2j=cZRvFT:mBaE%('ˇ1@YicSJFM1R 0`&nsta5-b()z\ f]5ΨI?Iki$=sģ@umLe gZF+[VLA5ih֍,s9C(G-)2S#xW91E<^R{mB$"ܥnGmi)#@ir+ OIYQȸu42e]m2I2&yZjn/ٟ ׃媎ێ3ї#2oi$*UdeHfԣ)t1`ҟG"?Jmyus4E[#t4O%&Ee(@yYgR9~y(!(-&PӞaDdC *wpqSEON(<(vFJXJ]Fd*I9JN;]D%,P(Si:X̸i"&G L\d Bu:n_nRqŤ•~ai/ITjILt\ee:RqLs iLMLYFiY$$aUIWUPh@ybsT_ȾԴ݇*n/p?΃\>I䣒{GVKp$RR{A]ܒtܥʼtH#oqPHI7dGN_oM櫭rK913ن]4GLP(R"4ŤfJY,_J(ϗw7 @ JY_ Ql,2(/ I60$e)Q2%_ZkhLLYILW4&Ǖ~eR2$eK)*_^&K ()dJTXheBLhIDe4\/aZ=Cr"RD%cv,MB^jQq˲<ѩ걎QSݑ|QeVE@&ɼqE'+}iJTsPbQsc@^:_Tyl?Ig'D%'+ e$?-&j:O:"2*sz$)=tje % BO@I7⹐ԇC82n:+Γ$iȴG;DW"2tD}CF LCST*i|i%+ݲ$)ʼDm覓a jQGwEۇjۗ-Wu'J0BG,4t+'&jJu@,vI#.CA iS\dĕZ8&5+Q)(+KJ]ǭWJTA)Y.Z*jmr^Z"PNy\49g+HT12YVʹ'De(I- C>T8/"J}JL+a7RPl웇&` H(^LԾ"`?|D'( %}Ujw9 +rAe )$ A {CŊ"!)I~*1))pŤ 1']QIYVr1>-[T5/υ>JYy&?X6 ˀ6KR/|B(WRϐ4墉') ӐIϓ 5ddyDe ~ŤST@QYjTRô1a{HW*RG)6YJ\D%9JH~H2$>)i5Bm]۹3'7qєZ)ɪCYY>Q$hX^YZ2R K6n7~~"1f#Jt3/}f",py} J#*63R.JBrtd66^_ʽL;fوrBDI}1Edl`9y[r'0~C &*FIHw`hQ%#*?G8:q@aڥ^KI[ ? vmnJ]JHA/IWTzyAo~pGA.e Ǵ١X4k%(3;H(V[_N:lVp5c K%#i+)X##MMV)E\RLnH=QY> !HT:#(e}~')#.DRd2]%%JJ%59Ik'KHHʕi51!)Jʬd7K dYe)!*HʆRH*#u$_24LOR.AVd=10*؇h]YHJJʅdY$Zx4qFI)*kC8%t$#*CAY<~f&R2iY/3 EN9JHF5Ds,~|f^7*%b<5[/DIGL?f)}8rB1<\%{<1UcSlԒRd8,Np()CLRCfZR:Y:BPKIܔZzR1)ef6KugZ0jXZ*~BW0oE\>ٖ1`$=yXd-nG+${U}yuh!K%%/Jd[\jaELEYš?1H#&::m9q뺂V]3e8ewB,CR$cuIDڑPJ<&)hǰ)K:z@-jpoqM"QןU6θyu~q(y7&KnR(_ Kݖe;a6t};uwi)' OV*QyLbGnD_$JL3)Mޝv9夬WtP8}pjq)(4|]J} yM Ee:ܖHYmvE"/ sɩ.?NƟf @,v,R)2mJ AӖMX&ӔUΑ_k$]A/.oM϶"$M>W-d+#KD+ sH4eIzڶZOHGM[z|6a;wݓI @pYw+ߑvgdZ6$|fd4mNNJaEټ*W}pڵ9?,s1R3zhtYW8䛨z.Z2fd7IEtY>"HO# Q"Ltv,FV}z<#ZWHAV]in [-)ؤ2Ys%Y)p#$m(%/)R'I"JJ2BXG:FFgT6QɋJۨCOR6e}/ }8 '*1k?%>4Y~BOHBTM)wy9 2)!I-*Ab)?24|ȺE~P}PpTe(A,)m? jNST>(a]^NIeO#i0s/^?ћ,ugKry|v,c[%Z]6I^W:C8H$/tXC}DwDW2?BlsJu.繌)%K}ُNZg)pqlC&#LCSgtbY1.}|wV(yh)RQ)’*߭m(uyZR6aKr7GSMQr(o,tQiJeY:NaDXq3:,D^$+&I(tHܾUH;o]ᔳ_n%P> q"5CIT|kQga"rMs$FJQy q525IU~*]d CRVr8AgrH8w/C>:\ve`.^Y0ݦ"`LWw#]wYV@HU~BV%]8, }qn|Ls$yFS&Knmib,]IP}|ܢ_1]ȶcWQ R;0M>ۚ \cf>d21]>ތM|{NcөӪPuG q)dvrVR DKrxwm,Y/V{8ԨHd`GP[?Y-G֫Rbb #dBN*'ͤ .ux9A%4Ya~yD | We#.,B Mt]Y: YY(dՕ*-lӬk/DvӵJTpGIzv )3PJ8FkAV͹ 3?"HG_2`䫲҇"ҁNYӹK<'}?'}qEe@i<3eJN>+딞H,S BQ3LIF!Iy Hi@ in;ӷlGC tcOAj~,& %@)ܖ+(Lwz&{8ɣDF%*)tlپ;-yNt#(B> E #jR_˳'(%yJ™#-BH\+IbSzfo$H1dyKn_ORRRGqKKayisEHH9t<կ[ 3 ˃QSy(Hj#qۖQ##MoR=_OF H)web}A>Q MwH=$=1!ְ?C<)#n%M.y{2J$$]h%;PT|5٩BitsT"ZV(IѓF?F ȼe8BR/I'2 ,cS>Iʸy©[e7 5pYN1P-Ks23?tX&HGDfH9qFsLε'F`1R7;'!-+VTM cJʂbE(*cIeHJ5z䀰ϕ{HK|/ F%@d$ZDRID.NSbY -sQ\ 6'i4C * iKZ^9n=Zl9g(+ؖ%%.M~-}8B!RY|&r_oI^*o3?,5ٟmB,5^=QTY9OR dCΟ{d()i:YeyE{fݜE "H$f *-(⹳LopxS# D_zD=?Amyu,ME8r,\IICR2ґh䗅S75QGUY#(W.:2.0Yp$,# ')+"iuQ,Py^~m vUky"BX1W3OGjJ#šƗ%f;Ⱥɑ3BE)YC.|Me$dgT%%dS]>a GN F}\td>5M3~ jV+Ǹ>Yi¼G'"CK%(hGw]IJG.הHEz6lǘ~M̴l2]9Ǔ C{@nw`:DAGS%]!=ڝFQRS>t@mbe7MhC5=ڝ)Ӝzg>FOrӖy7ѕOr]OZj+}P;8Qdy9SeC*mԺ>-uz:AدR=JsW>!NZOyبJJȼXL`JBcJFG.ʱiޕ:|#f;GP2OJUnӔhH&䠁]?8_v]Qe%5fTLF$|2şuqP'eћՆR59r LOg|I/ԕjt-SiA/LDT9lJ%):PN&Mv%'(8ъI"p'w96Pyf_]}M4paSqŋABT#o+ % \ -=Z{|i爖BI)R$K"F br(> eܮΗu'_oy mHͧ]rU|u4>ZV Eki>Yުf"uߓzT]_}ͻ)*ޖ}!oFUyso A^*^Y+Kʅp46[jP^_R⑔n=dQ#A 6L2%U_j䣓t~}<~Rٽd 2.31vTRQ潣|3/|cYJF!mGt 4tq-X=7`Z=D7󔈤84ecLs)-~d#qpd>\+d=H^\6B gzleZTj\$((;ҍZ4L)XZL&ev .Qyd"St؇~Z:r҃ !xF,JTx.1'Pv&qGڎ%t.a^RLZO{Y|Ցl s#)σ 9n>?Zsi;1 *)KJA J-9(+ R*ҕJs*it}Ru|T1PT*IXQd _R]V+A~We:AbOZ;o*vQ/7W8ѓ( =G)R,K+KIl d՜Rp=t*k򲤓Xh$Y.Kٸ05"Ehi?x#!fF&*)&~_brZa^mvtTW#)5"jd MI)sّ遄jLHdT1aD 3Uc,` ?'FFF<35 FBi݉iKF @ NYz'MqHt>ɓ~T>4^?Bxk\I1蓔N=1!OУ~l mdZ#؜NF<9Q䢴d2QRzXtɤ HYBTFY֕tqN/>F GJJM9QíOI$$e')M\aIdT$#4ud)(8:Ru 0/5!JGȾa}](2GI)m(y|d62oNI&|bDtpFZ<I9<ןJBD82y,nKwH%mU}e $#o4\ދv'yQYaR @-:$(.ȤARE!)Tn={w2mwKnw % r撜_IVRVodw1"s٬H9iNK\iD4QlJ4z!%_z \Q/ K\ 2\=Qa >^ κ϶'N=W^J H8&$O8&0Ebٰn"/Zd#ş'{,ߑ:RJDO`,)~kߔe༐&R"R/3"_DS+SmNԧd|%**_P,Fv&4,T h~D zi8_ߣԗ|!4X5YI/=ѐu`.v)*-} N1eKedX1aJI I'ZRFjFV9z!IDjLGũԘya "OKGF:1-"LJ4utHZLM?Q#).yF]>9#=M%'z~|(l+%R<.gJ?9Kn8e;QWJJewG*B:L{W$%eazXn6et˗zKb~'p!Gov)5QcJ whчBYw(0a} A%ы;Q(圊ͩ]IE`;)zvyJn*y)d[ҕhaHD\&K#,T[_8 GHۆ#=MTb;Uu,%ץ$˵!sOM$t=*&'r.4_ZIǏ`ά *W(wҀRA٦G5)׫RĴ|%JA÷Vz0ϩ/ޖ*4|sY7$[%Y;pݟW.՘RaqJ,JJ¬flPu6Ѧz|/G>˄,G#2Ou4Y Sq2`. 6Sz]d?>X.HY ՞h-SF֋֐#6Pӷ5,3 4~ 4i3dE$_?'i,FB:P4&_'MO"TD~Ɉ>55%&"$$E)vB"]>QIK.y$D]a색.!Eq\%c\xd"a2e#r(mط,XߐzHn6e="m#:_2) RB$Dcєtdɻo GeS!}}9o&|Le'n:F;n2N#3K#9zP{tF݂,K"1کf?lT.Qqw G*:w G BBNK\A;!;Xרg>놕}s#ҷ.DȠ0nwz3E~4m.aieaӏt&"wДo-Q:|%,[6L6NZWrGOPL8vD]c2Ni6BTMyPnHeNyrB3GƍTGznop5YAH3om \'aH^܏|$ėKk ޓc'#AX!K|.8d]o/O"m [:L`{R--%u9 !u}>#¢rPX&6'I.4_Hhsf]!4AeYsM4IMVDAQ0^2..Mو4uDA`"RFt,>4uDHp`".Q+jiϚ$(/J_5*4E)`ZIIJ K?LE+E?g"NpxfBό`ZEj䢴!J nZg( Mt]+#OO4+A.~\Izb BH'tU{!SKGyl3-ԇ}tQ/DAC2,aLJ|Hn]2! uGH?rSlKIH!2Z'BJ˒ȍEɲDȍ ם!K+IG./CP >X\ed{x .:Kw9XE2Q? ]6Yf 'ˬ$ AAD_MMUٗW$\d W/H3dA؅둗S~y6"MJn< (}g̴,p}EIA(QO ֋3&;5AיRYDkyHp j6pMg5J8: "c0O4A֙r',D%Ǥo!/7LʲRNA(PfJJ:BJq헋Ƈ9QzDn( &ϣ,|p82 2FAt =D%~x"Fh'( a#!7?^K"y@t2\t=OHiwBv4{Q@{d$?|2%K ߕ]CAeR1 *Oʼu;2 ’o6$|Em҇E*K5)S vr?6 %Tu2n/%ungt6CT, zn]W?Qҟ÷\v$ud_Ka4-tNyIe汞s~:>p]>#YVy}u JCG:{J^ $]ɀ̛z+2torAME ( )\8D H@Ty]wYpU6f?T#.g\ov5p%*GxgYbRM%#/+NT8..Qz+I)t܏*6o}|;-ipa=[svWNޅȲBvy|,|iIu*nOPْS'A|r'~hydb'üd?={*z(g"oMgTi++7#*dE $sU2ӗ7a)f+3'/K^eT"|GTj.mSm."d> "D.Fe9h2EփDFRפdO4 R!Rlu&SojeB4Ų+IXZ˦ y4e})dҊ SOfIc1,g i!.Izf iY'Ò,= R}rT?.HvFO($ A1W.R!) ?&7Nɶi=%q33B +BY6RF9teafH9F(H=% )=YX"Y  $ zd? }½P%^seɼ+a`Tg"^>\?UG(z2?M ҝ}PAL]/2eyd;wB\D}Ce:npɔt9R7|~n63Otܚn-r/[>A'#HejrJ|Rp]+2MӅiRu zl+Hȸ25R_e)t%ͼY˄y I4%$?t:vՒed~ J "2Ar+( =1RG$` ݥOOtڡ݄.Fݯj/u z{컐^OӵDVɃiBʮ`(2377BA{qfqSTRRjIDRv;YtС#9+q*w/mNV;Q\f P)QGQr+~HdԼS0y775;sy{vj"RR[y]xxUCEl%*ezn:rdjz4&R\"xJJ$h&6ehrjLB~ԛNLHMm"uSQG-A# M/G(5!Ƙx~TjKgG & IT wD^ 2* II1<2 " dR Ml)| ݾJ=?W7$821to: wIW/,( ~ C^Aܖ&Amϗϋ[=B) )$)T|[{'sA[&7oꞀi?Aj{\qPM֫> DzJn FT|$"H nHg+~]ATRȱ!R2P뒚k;GP[WH\#}]ui?ղ~|?L*~5峛 >ZOՈҕm7@j c:3QyJ*J=~; \z%jN܍GX6-#~KF-C-- j@ t@?}eQZ:OAZ_)ҭw1)k8w<Z!HWi| +s_ѿAu߁ADyMrfx'x-a";.ox,&/Bb8+< )Ek"Ȭ6e0|n%%I<Ҋ$QNDoM߹mw"^TnU~؆Eq,xPj 5L$?AmTm˟?1rTi),_s'I ~/JJ* 9oJ6.H "$hjYw(A]D1?KZ,nepJ.hG~^\Oq\^(01&cqdA` W=q}qCdVUe; ')9%܊R!* QEJ;owr zTb !Y+)PTRiTA'\6_l, cE>o,% RsSS.D"v6>}?wiA.RfN B <|my]\(j?_a"W'5\07ڟR]K`&0"R|xgc}qvƯ+wFU*QyGU:2I/ҙmEeܟ)*rT9?CN~LڇB gY2ֳGt/e!feB]m&q97N_E}~?kɏߏv.7*2x܂P(H/A ,/YAm/&A`&: +;Rmύkb~Y.V}_(~DAmnRU;9™3:#.t[\qSd(QY錬YR$JbE+*v9)RVqGS*Qei9߭WSs_w!^XoUAױyt'Wlz_2^Y~^𿟩jw ڏ%ˀυKȹ jo9OPs'?v;Jy?̂˅WX_(/*Ej{ ڏE/<+8b1c_Ѝ}yC_%+9r0Xeʛ,-*Bδo>{P.TnM؀ܖR-֡񄽈q~~@ {M *hx}ɵxK~ :l릢5O*~'-OgI " h_L\T䟠?A_]?5Ay9p \8'{NE1w)(*/҉c,.¿>WuBn}PjxƍY9#R}('*3n"ΨPT E/(/WkPo4Ӿwr۝f+r[ovFT\Wp۸\*crsB)+̓:LEWLϘK~/.j{>pzRw~!AQW9_${ .`;OH(櫢:]8$0ޘf{Jt]דO/%K}\ /RPƹ+.q뺪_Z}2X_oGϥn2؎up axX|jFY[w2j#]' G۟^q?D:}+'O6Fbc,sיoRFL'xL,篶CV6l #MY7a6~1E:BArL߫57lr]Q`-߳Sz}3=rݥY_9~:託ʒ'VSPo\qud^#.}Iiy#_<#*o-Q1\TRQ+UǗD *9rJRz2N݆V[Zd\쯗9BJÊ+% ErїJ ElrK41n8NCA>ou9}>||u;3zˠ|0uݎ|FPCߖ˲y,\fK^Q,g1}eYv4,3ia}0l0 c˗6lذaÆ 6lذa|C߇^'͟u12;3Y]GF3((~?~uUd\k#ȼB7rx?s*"Aȸq0* 7S(iiLjcfl~69K҇ڣym7d^}GTFƍCJTzPXR /+De-*rCEe}mmw ;d/fӉI8ء^(VTv!JuGoqr;'*ʹ/Eԏ._D?œ.SgB: dه쓟ݞK3~=t] n1C_3:t}jiji<\yDQ|JqSO>P|0ydpxbufmt;.˘Ӻ_.'G(o߾XQiÆ 6lذaÆ ^6mڄ͛7{pVۆ TƍUZ:uJ1=gΜ֭[ 2K>އݻTi]f:s ٖ wVʾUmQӿ邌(+9'*ov~79SgTyqDO@K2Deaz>e+24]>'|r!AeEVRGIi%)w {,߿6Y "ZWǡj }.d{jȦw.>^\pZ`&Mt] mۂQFEV^^+,E8+ cll"ٳ6b ғP[(Ylj8JDɁBG尿Pxo ?"c?+pZtV({^,0_9JOKJ36a .T:s-|dRf] v:>y,g;/z]7MP2GET^,(@"[nS2ϧin̘1xdvǏ6lذaÆ 6lذQ1{3ޏ?_)/Tٲe˼{>}w9Ƹqט7oehMsдiSu<\s 9'bEӿ+](%+J-+vB$*gT2szF%~+Q-V"˄ǥ>0 Dv,a\EE(Ϡ,m' Pdӫ#]o,u&!\{2kFDdן߿9-ƭŞ"HOWIO(L/ל9s.Q[`ĈaN^XXg'aI٭u^:`H8˩݂#d?QW#.VTڰaÆ 6lذaFq4nXwCj*urJŊ++B},{/HQ1cpqkx+x1uM!iyT?ZTƯjJWG&E5]'*C|QNVjcbECxU@LCTv̗PT\QR\9ߜ+r):ybl8CQ{6q&]{G#$dHIn8|3lU/))<.}SQe;rRx6|L/#GW#'\/((Te9y>XmƬi0goGH >[ (*e]Z*A뜋BPO(9U"띅%ڡd,uBph H_ =|Nwd,5KgBuΝΧƔL'֫Y&^~etQN:Ǿ}b^} ґẓ(kM4Qұ_~Oqu8t׎(p1?cu4zv`;fTq9ܱ҆ 6lذaÆ 6*&xOO>Qehߡaۑ$*cعs'{<8p 9.S9wQ==Nܢ҃!* OlJWE5 Q^s#*7*oBtGEEJMWTvpFTۅ6;#_9߮Fv'?x9ssqK"d U\/@Ӑh__+IQ 􏧮uڵ ]wÇ<_qwvz?uf|u؇^u]ͼYQ+*mذaÆ 6lذaO>qE5%W)*=,OjALh"ԩSG ndb܎S(%*{WW\tDk(YIQq]/d^(*#*QO]Wv? E 1x!OD՞!+DϨM6QEwӢQ]Q\tY*oD;kRdY-5,_Ɠd2rOF>X;lC cl<\3|N1<'"%Øt/:-*9m(|1+q> 슼ñr|faI P"9!19/|쭜&I|ck_De%zwSӽKwe'YvD|_k=pjG8W@&y/6q Gի{VP&M+.zC旙a%|&KxwA/P6l;`?^M kƌGF`p#Ծٳgg\0`9lRno4eXpfΜ=} +e*>\GZ24C<>5HYXQiÆ 6lذaÆ TJ}ٵ1|$Y Ա[xԽ=>.y\:ջDҎWTqpEeOVJvF;D%GSVH2GTI Y⎨Q&|>e~#rZp7E҄K=2iq&B5_'ڄjT~ rYwW!uz5ϖ}&[?li~5_|O婷]w="/@ٳg[l!C`/t|0Cqv88]=z Qr:eLM?/d)od)D/đuAyR());RM^m#|KHJ0./: 9" i^{-{96 7?\~emӧO+w7gZُ:XK/m۔7xC~~H(y))9[7GLrڷQɾ8wߍ.]7}Wϫ)!9>SA1[%)Yvmms{oچ}~Ps.'''PY4k ݻwW͆ 6lذaÆ 6.fͼ=GP,Xg=^Scdy}r?Cխ[W$ϭ%Fp\@$WTrgQT#*)*(QYntEeXMvD~>34G.:ы&*/xݎ7[܏*eʭ~rZW2ŮQX1e.5,oV"Rr Mmw_t߅67m߶܊ ;DnfchydʴXM1hH gฐWPʳ>y ZXӛߩ1ƊѽG_PLÖxe(.e-5gSRPƵγ)IQmS[gM)]$p{9`p]jLo>gKGQ};zTx4l yْGr"_>I8E9>mzFTRr7GA2;v]wݥ^}|V9Brй=/῰p)/[hFf__0zho8vIyF__oo|@a%CllذaÆ 6lذaRw}!@p8_ J8E\GQGTzߙYTopCVV{x\J<4өC9)sV8OuïW\+3*3o'$4eeቩ߁2O JMo7 `QI9Wє%QϳO݃l@{+RTb5wǿڇ#Ŀۋf [peu8J(jLS_:nİ|9 %goql$WYۍHȦU8S֠S<"هǣ/_T>2Nt()MQ%{[dOkk-z(9Eq֔\9/_:/*,3ӥ𷡠l b]iqҐ 9{Quꫯ7lE*UpW?V#2Nofaq)IBr߹|f&EMT=NMo(MREr?>u#4^IR/?8i9sdÆ 6lذaÆ *x}yu.&iT$|/Cv5ρdUf9QAtt m۶__q@>\ǬσߛҖ})*۫gT#*Sj|/IyTғ{I~\|QɏSDNK#^}&TbAv?¿ډyݛ'SzE%R/H ۶`8áɽpǑ͟PС1mX&| f* EŅ("Kv I)sڣd+)=-qv8.N,3[z pJE#zoW[nE5>7Ä\G9 \(l`HG <4n>?C):n(DJΝ;S*a tq> S?`9G*Ljomi\8ڑbKZlCq9rHԛ,M}JR7>O6McbԨQ#PYXQiÆ 6lذaÆKN{`F.c;}_se:8Gƿu݁ދOcÿ2}_Nbb< L_d 2s iuf <Ey8{n_=܏?3_C8uΞ9|[:t/샜N{WI%*e:KKm]Q %{[ԆWppы89-j]Xٷ|ATK/(__߯ed}j<<\>E>>rSlJ;NToJe!?7J^{ c=PtlJO T^QT7^|j_(IqJJ+oڵk aO1j?J>^}? 揽҆ 6lذaÆ :{:޻16t30,W3*>~.~18hxW^yE TkpJ7GޟNqf0O׭L6lذaÆ 6lظ}-o&>\u}s_Ssv#iM y \nN|#>]?~N`AOMʷ:TӟyR'*mo>2PTV;Q>gAsW&u U<1茶Lɘa)p듐pAr[JQOd]."9/EPP'sk9&3Oԣh0N?F3O)L?Nٴ58q5gaȊH]thBCm]'e::^ıaCg "v6Ê 8n.N31.<Eg_ +7sמrC/mq$?ZJTy~J]Kk8r;%)$(#*4šE5Qsr7NoSTS֡ )Rr7eۧ0忾Mz"ט}YQ+*mذaÆ 6lذq)ün:7 }|]Os9{x?uZ?:M7|Y_xβdp_11ӟ+9syFey2D`rEe{WjAUhKW`z.{4dfq2].TT*_TqNEϚ# 8zPrcER, Ep`RHLJ/QTrgiIw#vJ"}8'05ϑq:,9J#%yJHݎKK?]cG } `Z}vffC )%6lذaÆ 6lظԡ)} 4ޘu/0WC :y?:ʥpݬ#fB`eJC2|`ef5AJt('Q70ĥ7ҕ|E܈+(*9R>,~xqș%űX|JPb@%%2ʮ c JΠ@<(:}EivN!M7ۄ>}l8UPBXY䝒c8x2B)}Yf):%jTWT2`HJ-*w5qxY G=;Գ)e{KE9̥d/C/^yvZP[`:0K=.tpIt}p׃Mu}C|4^}0y{j[~xlذaÆ 6lذaRy/`ڼyya]n?L,z]GP/z݌tMYa_#U_Aj;v|9e&Ų`}O=T|(?K,/<׏ 6lذaÆ 6l\a޳t9FyAM|O/z\QIc%/*3JuEuݐq}$QɗPTF;SrB+P4(HI))wJPu'STEN'yUz8T%u<+{LHIWD*hQP'*RTʎlGQYY)*7ʘ?Eʗ/|y3fPe| ӄ|˔ȕ@,LQy|ċ$7>8A? NԶ8C;F WNy8*B8۰aÆ 6lذaÆ 6lبJYWߕnk:;2JR t"2%3(ʾy,%+۩> yDeBR^鎨^BIw͓Ld"_ ֣QgGӝjpڰyԔҖӌ< ιK~2E%(7Jb(,d)vξذaÆ 6lذaÆ 6lب蠣)=|F%~+*()#\})*(HR#U1G]:` ij/#!+%MIHSHg_zeIQi~Deesw_xQk1HuD/JTSR R,JeS"9Ew|hϢx, V|V(taÆ 6lذaÆ 6lذqLP階DQ)%*JOTє yQ q,3MYK%+MQJs%I7ZݦqEe Lt|ϨT/3:(͗p$-6ԝqIߜ5Dȗ,bzPɑ3Qj+RWaÆ 6lذaÆ 6lذq1&@T>[;tʴD *藆6pz).Kg9OJ7r2\Xꕅ;uiI?~Qi KNĥ'/oi~G$tsiJOTp^gZ&IJ[)*tn- XZTiJX,XFQ =N]ȾĊ$mm~۰aÆ 6lذaÆ 6l\'JYg%*ۢ[}MgTe Q=|F,*9t夒KyȤt>ԋwΖa *s4%9**+Qy{Qn\rZFؙL]S/)b}j\GOY6B^#9ǨE\²ʢb%9C#%SrP$Zdr& L$6lذaÆ 6lذaÆ 6OK\mTʳu]NʙqM'gDݐq}τr 2 RhHQ2F~_8ʄLG^$m95HqNnyz>椒IS]z=QY^Ti;ʶPa ^bCȬ3 uG#!r s#"NmذaÆ 6lذaÆ 6l\()P(d8ŐH3_ Z 8U HBRKJFyί]Iym'3"tnLrCe:#|HDF->$1m),]Yi}ZQ)rwTz lǑ(*J-aVTڰaÆ 6lذaÆ 6l\QqDdq!S ?_gQ/PbS_T둋%SXJYXHTyNO%*92tC?o()otD%}8훢шV+qZTԔ|4HZTj(,igEAJX ~YiRrrpDeDO҆ 6lذaÆ 6lذa㲏>HjI1)iX!1%PpSV:)bl,<Q)뱂"5;NjݕЍ}JRՔ!MIiߎ<2R4ҕԣ+ffi'+JJ^|1e:oʽޙFT#*˘]'~XQiÆ 6lذaÆ 6lذq=b(*,DIg$s>k5ZW[ @/zvAh7p"f,ZR" Q_z3*]QMVDeT$E:CvE~#*GIh]gTVAR \QH~gUGPU PVrde{muG[QiE 6lذaÆ 6lذae+.Bq,;zgS]? |l=ӗř|+{ǕݏJJwTޞϨJ;ҊJ6lذaÆ 6lذaÆ5JPR\G帺Ak\|{Tm29/BW>X|!Oⷒx&"uF^船w4GVo{/ӡt3E_Qtn!#ՈPLt#L{ WvGTjQ0zWv>+;GN{Q.Tm ?Cv˵ϨҊJ6lذaÆ 6lذaÆ$zX1[;z DOEW[#0,;. NwoAt_*Oœ -+*R(^"Y燻+Qd^9-Q~ST*F|Fe}N2IBRG` |~,9W8t'npe~Til@w2+*aÆ 6lذaÆ 6lظϨܼs/>< 6ן+iL4 #7u'1k9\>Okf3;koC&ƑGQ+V#*c1#N]ٛ}oV_Q)-/W.ё}Lɴȓ6&>N$$eGQa9Je?tއ*vʶ;QFڀܖ+펨[)p(":^ucVǸe0R,MǍgٸ| &-x\|cL|Wd;`Q( 9 ܡ;mjiP͛4GEX(v'q&&ueK 7sIkn[%gx@fY ȏϓl)K[)s>K<"ÿY˰PL:|(_Kƀ'ceQR"r>C.sd!^-b)[pN#{%ϑ9gMgBJ䜔fٰaÆ 6lذaÆ O=5:3gdZ4녅j$cA0zjqD_fiX~Xƃ]~0iI,U}w7 RorM@mXibt/bٗXhmyU[i4Qa B!Qɷ~W-T؈4PT!}&20$$%qDeGQ!Tt;]r}y/t܍mvJ m['h28d4uGĔ0lJ ^z?2t:i39 9ޣ>xe"5#8dH1pH^V 5,ۑ !GERG؏$*XTr2aQH#uuk/۱vZ|u˛,4|J|JQY:xAaI>( %H+/U,OYH^X茐%'E?)/*I?#!? 1ߏʅ"~ӡ)S)dby۰at|uѮzun=`wv6wjNx쟜xYj>{,֬YݻwӞmNJ>(*u_n$*;E|FvTn?K $Dd՘#7cȲ5hHjDf=H᪦Q奵8F֋w3RufIԛhPITt$|P/k}/vo>@G1Qߏu^Aq%Gk8ᳰix+9}Wc1;g썱vc97Iꏙ)ʶNv,IId?xL ?m.9C4+Qq#<'SXA O,~?ӟc J/P5^gBsoP :g.ê]\A^!l1 È1v,S/ً gcɔј0s.f؏wEA pkSb[Zmˆ1vQlXwݘ()2#T{8@VNADx|]EG7LƤpd0cZW^On'QPs{ukw_AsoD>A7Sg pL(Ʃ]1xAS<4:Cl'^,5d?[e_)+(qyr85YmlC~dsŧbgb!6+I]*-ޏ 3;]FͦQQzZ(9³0uc|Ф֪[>phJuvzhQG8xĎ-ySx/ؗG9'1r3+ئ<=E+1}4-Da8#n>lļZ|9>_#I, ^q -=3;~fWqm|(9˻sQr)q%?~8RkÆ 6lذaÆ 6lZLjdgTo\y V;W|wґ?QI_TlUZmENNh 'n^SѢ2^?d7'DZh&[Y'ǠMf㛽~|w.@DJ݆S7 Qw{BMϊӃ ISo3*t)-*t%EeV[E%GT֭ GE1&lU[SwOo_!<5x#Flڌ>Y{9)&L(*ę'q@!ΞcDkŕubk4|x&IJճ|aqlv,~}VĎpq;/jap`=6~h4Ow}c8|'m.Vق:'CM1.Z^BկwGvmh<vx| ˾cf^|3~ '|>Gw5?A1tL.8uŬ-+1we &p> "^s@96׆ 6lذaÆ 6lHz$%CJ.՛\h’S"r"'=&h4mB_][܌ֳN_.C"xB rµ#ds-0| _5ﰐ};&*3n$*o\_b2_)H[T<#tʋ *'!Zk>݆Kף[#"PmNAȮ=߂㽏'"tq,ǝoݝcej6nl1mIȭ?YϬ? #G@GCTʊzƟk0||c\K`լx:.' V_w!qD ;1-o#cͱ ݫ )8yk>نc{G?B]8[^/ǧE/-YK֭ƖQ/?-(34|BNqAQCY -՛9H~afC|{4߅s⽻B(Cd ?6n%a&1Ob38S8=g`qǞ ÷CcX|l݃g)bsRNCZ˞峉-Su+Y9?:5>ݧ;}? ͧQBJ/.Š_ݷ mѦ2= s[ޝ--8kT3Q"׮elذaÆ 6lذaÆ\o֣ u8[efVlK o^? Y sP|Tv5}Rlɹی9}?°y(8{+>x~Z?]Tٞ\-_j:9qb;xqml=םDIik:V.y6lذaÆ 6lذgJuݠuNpԾfL~{dHyjdN+ tX(č_CFSU{&"Zw:r끫DŽEpqאc=E̫; tGT߾gTR_<'PEM.-*p4!*+݊#ZpD$GTNpɸ*6t|u2kD%FR6ڬV"Rs^M[Edwv. ]#GV3CnٲZr!4O G>Y[$Kk0Y Ga3E+ej$a 2=qx|&o=qZQ0.t C/#w6|!1 ~9 _팑b٣غl.zQgX }ӦofOa㙣.(B!G;r  oYw~O*$%#*%:g7`լhUܬ 7oJؿz6| ߏUcXX(I~B߬1^{|c7 h=ӏp~fƤ4-<+M-iB;&x.EV: )NJqF. %}c~8 jP(mbVb\oMzOWNb1|:y%kۯ?ėa!Pg_*RuS6lذaÆ 6lذ F GONhYn?u;QZ=;Y|`)Bj-:Nۀ/&?}Mf!\g "5G#n-E}S#^8+-*]^DyDe')JQI( MIi] +/-,STe:;܏*J=—tނ7#f1PT;,J;J>`F"XD)kCpRcni`R,6lذaÆ 6lذaÆ ?Ex^1&ډ'>}HQכFQ bD,D,d4U^#uOə1XQ P7pUQ Qy]/d!t@JTS Ea(Y$c2?, PGT-*+ߋVvݎ6r>M\Y ~Ȯ=Ft̬/'J=0d郜FQdHȮ;[DkoX,bX,bp時"ʼn"_/kDW<5"w|4K};r_u ^x֓C:LL7GTvHQY_CQ-!*/ًw![ܖ&~' G`42ɭ&Dj@hc?Aʬzðy?~ P6lذaÆ 6lذaÆ 6~()$c:c'q![lyk0jJLYK6lŶ}pL_͠qoD)*S=2gT?**QTvN9 AYT)r7*ߊ܎Qz|ZXFwaںȩ=Yu NES{B!T/39e)[J6lذaÆ 6lذaÆ.E P"K<@Qq1)/Œ vKb)|>e _cSvEeE=7@JN~/չi$2K-*+-=Sq#*;_T w#zTn rYm^|غ#PJF󚃐Y#*Ǩdz ` µ/ӱaÆ 6lذaÆ 6lɢ8%%c\, KvJb1@M.AH;BHb,TC1)*}ROVNJTf^S0Ee5GTo, T/!7J;)]3*Tk[ya5>F|MrggϮ¹vXc'bŊ+VXbŊ+VXQ yQ#D#!CVb!D8 #NҐ&ȝ. ɵH2N/8$ΰkS/FCT. QykTݯ~wC$eWdeDmS*ىeY27:-rp -v qx!/OTa38r #pl֞mFuUlY%Ν;gŊ+VXbŊ+VXb*HqYrTסl)*pDAAi!NUVdq JJQ!RY*"aΕ%P^^s%(*%A5 c!)͆:c}6Qi[HT}rN+nHMTvL­S*pR279FM֟C,F8C0 ]F =^CAxB19/~(NY\K׭XbŊ+V}-jϩ[Ig~鮧Hw݊+V\ O@(_^#g6CE||lC$L%mWWЇ"·gwp}i; 3s6n3JCT޻ [nL \A_Sr}Jґ)$+ͺF:%*oR[HTN,MEN঑ĺrg>'۝[pϐED-.3zq@ӥ0?-,,,,w;mGat֨~;s;$Y;f&ʹiJnD@CTs-QiJD]EvnmyX( 'ǩyھh{=HDg_bvtM|MI$؛bJ^B^baaaaaaaaѿ@NItvObQO,,Q m#*R;G Q)$*?';S~ejר̺`'*^D,QiBu[ݬXbŊ+VO]ۭ'v_j3owƣӰbŊ+VMvCz{$*Y'Uٱ< ;ߖt%*-z ) r>@_G0猳H >'zŵl탺7 *2Da=XҢ~\¢nv=!q1Ó"5|jzŵlH=>}a؋7|u5rJ%t[?oH:r7TVV555-UUUFRݭXbŊ+V}qwuuu(//GYYq/&q68oll4t7w95^+VXb¶*/mSUe-p>Q5* w;Lrb2U,Qi|iliiAss16KNs+VXbŊ+O+--5B7ڊ %IB!={ĥqiŊ+Vh;>/!o47Qɯ$*H瞅tϢ+QIqx?tboJL9-IQͬm.)ernt79ƜM Q<,cTOS3z3W]K?qOWxaXEu /y$ i3vX|F\5#yú,$s'+\JI<; 2r?ΞzM>v;/DjZM._OY\]Cp_u}Miaaaaaї]$tz5;t7:s'xqX__t8vnqC~DTB5݁IFg!_"=]: HӕEt&5n\,TXESN+ҹw7:ˍkaq-@{w`.oB⒳ 8D% *}tgDݙt| ;De< \GWmrMV#{SHTU S;n^qW// Yܩa* 2&xp^(Ecs#\Hƛ",јȹOD0,j˳8k"(79090ħIô) bc;^5)k6,GT30nv{ME)xx ^czi+G+3ۍ~NLY~vc8Uԟcwǯ~nRӏ[ATMN_u\%]~RկnD:wuӰzgB9sڡ u`_uw*4FT&}H ߙO|eʇDerr8;4rT@Ě6 ;1ǽ̓tS|)ҹuQQXTI4F4GB52{W=ehkmDHM6?JL/G( /qCD XHr&I$iR0o*&ylŜN/Ӑa)n)nKPw]IM编 ;}}Տ;LjZú=p<4c=_ʩS:HGa9s!I">#l|\5a,,,,,,"m+Sil՝$%G(~;^͝a#*5>ijnh?=&xHq8X#qWuU\P?pǯ0r? - bE%qɃ$,'y) 6$QM?]zw@{f~ûkue0tWMM5NnZ6q㎃zswY;E`xK?J6r\|gau;LkxE_j;n 1sn㞪B$txvJSy)=vL$Rin'RaXj87gxnQ Ǽn%5^WQhu1`;xwi_O?~{2??yj\n4 3^n<{S#3|d>-y9bP{CCBwgsK^#*S-QyI҇~kE HT04UWi)f:c(B0n ;%Rq:?ƥnܹsX|9V^m hlm#'X^B" y#Xo?ރ;܄{mYGQF0AH^$u)Wc3"}|c{${V ]N 5D~Sn5^qH='Rk~T^Ww CIO/4(:jQ?ٳg_0㣏>A6Rxܓ/׊X¢/B>V#Gq,jwBc 73`|pn[}E]ùSv3NCSqݢƗN:Q}*g4Bq-ũv'XLk{u%Ei\'uo)GV6m2߇~#??M+%L¢?CB*T(ԏ ҅OEoM>on]~]oZQ$9ya,[̬S`!TF*J: Qub`8- /ݱIBq-K BdggwŨQn:E[_wPZ݀Ʀ:ģ>D%oD 06dzO݂~F zh#nyd#ϋܬ8S?QRb2Xn"w-7,Er R?8qL iCž}qnCŵ(:*zc4(|2,E`0x`Zrӏ]Q4LSGۊ-Zd%~g%ysNUwnjO^knj6v73-w<.6|EErssxbdeeaÆ fʼCp!ȸ?`,۷3՟s׮]25[U_ 3>_"eCvŵ>P]Dx|!8d%GP?PY]#/uՀ/@Qc+cjh)+pۓkq/_§݈O= fxo9TsxH^h6rRIĥ~qTew^E}YcSO=e%Dnpk^YGhɟ ~7¯/v5>+|N_7x2Hw=\n̯%*{_//v1s =P_ 6v\^06ᅬ~|pݿ_7y40H# G[IN?z̿Txm񺆧{yy9{=1Ğڋ 3?$I.X%K̈>%y pezsB)<s)$x̲PM?!|AMҒD"wt+P-/#F(??bǎ_e{46xLt(^W\uΠk+'PkH5F1Ǵ١S7 A#XhO͛7ƍ33wHV,4ն <#*'ӣ͸k.әUՌm~?74.޳{0 /}ڞ$*cSh G"DرcOIIʾޏ_4BirmGvI8v޽{MdN$4tSOKᎃaԸԯXZu[Wٳǐ)S 33&M2ԩSM_>5\:\:Q)ZQDz02eJ3;-qQu<[(r$% 76BبiHmzt ȬJn)z/c|2 yFL 1O6B"pC::87,IKnZ+49͙FGUTޓٽнf3?*xoԥW/MM79|qc𩇖GşU`6 $Rt#tY&, S{H__yL_YςG9sB3Ӳ^zfh`q8pYH?'McʫҚc!2a8_qgJ\G'c} K(hgЮP{KVBۃp5\ikrf;k'?2sMf C>S=#*XC¢/mXdHtq Dtc%*/*vikn=~P&YI{dGR@{;^x\Ì J>(Nv1;]N;ӡ_}Us2d$hr;Raw Ɨ_~ل-G{Nio&f$#B-w i[?0sLc?>NmGڲiɎ8&H7 c<˘H*rPK/deq1-3yN>C|9s2˲eeϓ3v,g:F۲`jjYgqZˊ#ܸ¢?:%8+S/uwW }CRt"7~ JCHJ.쐏$X>X6l9 ~7K!ɊFw^o%+)(4xLÂI?3QF;|HFpKGF rJ<x"5nk Shyw Ǎ1Fw4bŁ˝q/k҆P"% Q!|Sͣܰ|aF:ߌj|aj%||t\pbd|ejقKJƳ!H'Y4X>$XzRg~sώ`qۿaҥ,hTu]ƨH?|V4R9m?1VXa;QFRwoσi1 ַ}q~f%; _WI_i*h2fM!sYfcMOa0YN_;ʉ%*-,,,, mj*JG#F\J$)i7u%EҊ%ij{$Q;'m myԼiҦE{m8vjYHѾL2a??8"-kڋ$g-m7Nffxvi2 f-25u>L}I)gXs~T?)N?a11˄}*>+U|2gGܹ=9g(2L~5}})޻X s>3KTv-w^$D[Rw?ξ-y+JTr:Q]ʹ;w _w{`=Y"gnHQ9PdG>;)Q#*P`C]Ą䙮 _ vt1(wu`\4PE2O=(9n4ї'y_ 64;'Gp G?4VX,O8iԤJCt5^=(ACs"6IN3CNGşN._ͨͯKbV-ϼ*|av%>?:>4jR~ QQꀨ &\'x4tMQ,7ʈaIFQF0~7a ~#SWyF;G?RTDsh<9挃\"bo~cpu{wґ8;:Nšce=% {8]'qJCiT燀'&mG-Qy} $iC&`[$H.!F#h.=K}y$ԮE{&Q~\G$'K;5bD%c*_vm3ڎ7'Q$Qe{'{L2G[64Jx6ldu~-0.l)lK-Ȋ% 08z'#NF$~FA _9slI ?^12=^Wq}UZjto?z-)_N=][Ѐ_$AV~kY ŅE) ::1br4wDN{r|{ TXVd3, ƸA95 @;1Y4?ϛ$(4tiಮ~*Q_NJ,snA#J89u_I3AaLww:aQ zEinj6_95B#uϘߧ5x\X¢/&?XTXM?ƢAQD)y"NRig!IJNvihRWDگ )hgQ9$h;?B"h c^i?-Eۍ6:7cȲb|,O^c˖l紷9qˁH"_D0,m=ci'r 8iB{釣=ysd!2%Ed&Ct15ϙwY~_,;t+K‘~Y$"8eB?$7I2--c嬤t6O-2u"z:JmEETp/IJ"9xU#*#+'}~hBBTBh8mԽ76Ýli }'y45 Fe`3;5+:;WzO4(HtԢyan7uc8ǯףOo7e5ubVGQKjlJ5(ǔ/Tsxq} b33;" NhQp! ]1ϋ2x' 9GYt@T\C:jD%׬)5$$ +a>iHdc0fM\LD%eb4d=GB7k+3P')Jҕ}nZ&tT_g?YZcʥ5-,,,,h#]C{G RHfѮ!IE0iw@7 ND$hVrJM~{QmRB[?Xb޴څ0 Au$dY}%*yili˛D%ZGBn KR6m5O.Dw~&G3JmH;F'V>#cyk:G[y}0 |6&Hb^8RJϑu*ñ8DPk:p?kmTs-yvs2|E_[`[DH}:lذ_r .QAPQ=QҤe * V62zLaEA~cb4/ A\0 N ӨA mh Q^cTI͛I_ipÝz]zD#qbq6*=K%^ܞՊȬ5#&U#R/-?dWƐԃ,O7`^qBfG$)nzqg\T:Lc$ _}nMC#G~n4hRH*bAÚpTH~o4`iܓd9G)84#݇zȤzwCJL5ihd{_ys;s$Yj9s$bKf*YXXXXX5Fv %EILQԖ`x`IhKQh@J#5Ƨ#* 0]n,#qHB$I8NUHw"iδy&G;uk@N]Fv#G=~h4ȸXf3ȣNצƲGnN)olh42g> 捤Madh+KB`Ehl84#*|uI!捣IY<\ *$Hj<j+0"N%H@ўX^;~Fw/!*h ZB6/Ƽufh8cEvjYne~ˈi_4g8 5Wc'Qg-wCɏ$w>+AH%Li2@{`8_ˀyR|v܍归$!ϟOd,3>;^t?+ I1 ]ay i:@ϵL9Gw?yZJbV 7G.iAwu k[s1XR7>DT+ +G*A'Fˆ_՟a ڕ|1=惆+2_y0V<*5=-85 (H/~r1h8Kkj܆DD%X>1CQG-됚t{hp>+"/}$W^Cfg>|TX4e_wahlҀG1uѨhϕf0niHuǼ3,Ӡ0(O c,藿GL0}YO SBh'AckE{B shK~WArHt$8#hv=pL'-ޝLy%۴ (z>cڀNp^S[ii|03>TRO1ú8x?LKdxø ͛<1˜q0oLnڻƧyxLy̼Нnaejax{i_ͬt.nPA=@ď^8!Up돮`KkԊhlIL/+'8u<NHC]ǩx$S;$?5bD%t¼ knzDjSQwW^#:E5Z2.^S!XTw7a4u;[n =篦;mussuSQOϭsvE_|g:2 9 D?(xr1\M)?$*'*Qx~kFpRV0]M[ SES[n۝Pw"5~1ڜn}-hxw~*\//iQyI9dbTD(q * p H  kڼ (1g=?u;$ y6N̎%rn{gq1RPa!^w_S w)S$Ennl׺?x+-Q]3u/:aJ khq]0݄'D% iGҞq=4/Qɵ0uu /6S~ۿViI«%$VyCZXX8Pj[C7m$+IDߝ_anqDE/ EEE旆">_QVXbŊ+VІK (Fݧ׻S G񕔔Q,%)]`d<M~4m+Vp*E,T7aDۿMK2^L R„ &tybzԶLcTQQla.QZߞ\L$ҬOI~@TJW$dwDeCTrKTZ\k_1VXbŊ+Vn}pG}>dTF&ڞRcŊ+VOamۢJ3`$Y˕n(QQ,Qiq>RJ=Ou̞[#"ݢn݁;N8}B^el#B~KTZR_tX迠=zvWmD6#wxbaaaaa Fj;GWDK$*?QQW˙}1DE//eAhC k u߱srv:ZORt kN}LMS{蜨';;{BT6I7tH$rtzCCTζDE_p[ڀy $"êS{,,,,,mHmL mC:%*a@7 v&I5gYHr$\MIGTkmZq[f n,҈f̪íuf\['yL!*\"*{|-,,. BƷE_Ej䱻w7nw zBtWrײD"}֛Բ'Qd6.2шk;rWgpW!*fmJ3;ITuE~\KW&*WIeuDy#* kaѫp7lmcn_!c%#i6ZXt{:y }+:RFmhv$ynaѿ 8-Q9CY"s0૙p| 7ܷH]=klmsDNQIrFurDe9nv귅pkWh'TczZw6Lj鱊ՅWw걅E_[?PH[Xt[oXڎhܶRYoS/|g6>7ʁw-0D tܿ2IVZ!*3ے$%רlrʌz:g}IrCT5*-,>hCNFܢ/%#iŸoii#G;Huu-,[?c`P[X\MU7x -,R֛+2M=3 Έ95*IT޽ {,q{uL$*ܿ McoI ؛4);6irFSf;ӾSEWڀ;JYX>*Iihhƍ1|p8p2i1X-,кp$˚g=kǓ<:X=e{6ɶ/n\p-z[o.V>3 Q&**QyҤ,78eSHR2Q,oH:r7%-QQyJT&*aJ ނ6%*-t$U3 ##/2cH 7)IE*P\)7ޅ w]f,,&uKnjj۾X\ - -cw/ipqrWg`]Ef;$p@R^L'-x)\Md\FeY6NZq>Q9(PQ1R-kI؝W- ^Λ7*mf:u .n1PҒ`n;DqŕX9J I%"vzC&WZc=fsla"*yD0p ;S~r= 1"DAjKTvl۽NǮոejn$*ǝMGT:rnFF>W4,g͚qXG9СC>}:֬YcubJ&W,w-k]MiJt:v-, J.2`+{$mH Rw/FTtYdaNȊ$*4$%*sqۜFCV&*gDe%*-,zTvƵQca)۷o7#hkcf'.<]] X/R;Z,Qye~Z|oI%nhbGTZtQyeUnGTZ/H"u+r@rD倻31"[,ԑC71D僫RJ.[|IKtĢJKQy"KTZX\!P٥$*Qٛ;,vC"?r@q<% qg0G1,c "5y<dL}mDEcyqD{HT.{0$Q[HTrIky"D-KtGMTr귙mJ M6<_4z7$FW,4CpԢ |qCT,ӗw\DQΒOv 1c1ʤ8HH(wq20X+\X-i$ 9C\iIgd/Y<TŒ(] QQ(Μ! \kDE3b]dK Y¢H2qa~hT$^~1iP,,zOҖ$GOr:na7Uٛ5*AzlK)J9OP$}XA}Ke?$~n0).~6!|ҖHфyV/*>N !!&&O;N_SʝsмɧCF.48c$a9#i@J2ʴHJēV8LG:dr!IO/ ){`NI]1uδ,E'R K?lY\}@(]]bB:6.kV7C$ bfݒΧXBR\|ō#,?tz-z!HAvM,[|Dt7AFGɳKj("K\cy.6_?_X'F%^QE>/|^tqi"/ F ( KTZ/H]B/@Ors1yt / QyʯĠ ʓ\ Deo%*-,4F6DhX$ӱCvߜ# o18f;b0!,ƔLA1H7hWthm'nNG(pv%c9o#(3$mr7FD,q!hr qG%rK* k]uḧʆzMNG2$âd" MRI*DBEKb'$ء$y)n^$D$1r]t`"$*D=rW~e%%lM~_XO_]]!*[Z~ GzLKYK~ &HF6z;$"^85#@t&ٖ,F6"D%]|ȑ~SPB&(}Lvi`*[;(׽D&;t&dYb94ĤO񙁖iqD Jg ,D%?G1gr -I4r!bF+I򱰰UY^#I!a$QP&rM'(}(z)(rC:B"$Ipt G>EF;"!&Q,*0uVh1QlqD4#׌"$IbPN(*{CTr'"(gB!ܛtͥ!a_!͐#-Ga&-R6C Q6P/ہPm!%% i7^D0#Y%¸C.;PlBTuD9ivL6~$$!v,pI@J[ˠD N&|rd t$=f$(OڥV 9{䚜I~r5dܗCS,(XH/+OZiIS%N5Q9#(&4FHϒOʟ(YY RITkJ":g .:o;?Z@ $6â7b]bUCWcD%GF{-O""u"P+Gjyǩ'!]vqGDEʃ]wJ+u%J! jG$RĘe6X~ =%,QiѿwUt4_MnD 1>]!*r}:Q$/G*Ek"HT<~<(cMZXX\6F6D% #;2l1oY żj\ 4xC:;5b$!a* DX 5Hc9SX8}oA~),CC+Jw8>X f I!k, k ]uh "AH<ЂH1,><zy*[DoCQÌO=ix;G{Ey~cg6!1&JN3W"Gc˩Ո[{H$}ro{앨$L_8#jKOb̫#ţoo^'&¼SYugp".HڙhQ+ ռ<MsIDAT(  Iy?hՀ!*#K[BIGfьVzXwJJ>$$r+iyD3$<8o&TĪEK1m6nx<툟ۉ7O.­{m6`5`ˑv2qGqE\G?.+5RarT|32 ",M2xNJTWcH]P(]vaĈx7^>3 ,, ~t%8Ie32p GUrthaH@Ӓ!I^sd?\tCWF 6_/-f6mYyl^ ֜hC&fVcgmXp5fݍ5,I7rL2LKO53ZڦP]DO-+)vec*,Qiѿ /,uTcJ%*f:\Z"*;JCnnⱧDJTJ#laaqpQڠJ=":4GLǫόŻ[1#rKʱqr̘ނO3ʊQXVP;ڛNbٻ0ln,WFԩSըQ1 JZ\2[r) ܠ(pH -'!{Yz$lWM>Yق%ᵧ30~Yl=x'J[:>ob$GǒD\TnPb:RşYR+s_y'z(Q"Z|\‚m (!wFLZ p-ҾZ5|񶢼UNTlSH\+<1C"L]'S-+ } 4$=OŴ+~۳Ҍw%ʻrPCr!*of:/KK%*[⣀ۈ(+@TJ̢j#GN"{nEe0(VC֬L<=bIJx$^)َ(|^20>XG;#NyЭo)| )iey$Ĥ2Ph,JSş+ː\:d*zDe-Ch\0nB.G5خ8Dk@gmgMe'DX&^R^ ĹFQTT'bN]ea*,Qiѿ@e~pD wƀr= 1EtbD_| ~G Y"z-0K%*XJmDin"*",=l@f.̘uҍ'v2L ;ĐPurڇ[bbjc/6\-<"8`|m,69u[`J4p dfcV;pGX5F.t2^D`jXL't-,,5PKQYN铮̐! Eb>LW:Q[0)xmRLV}~sYS(zm>Qi_>U40ٱxgz2wƉjU|6Nd8шb>VvՉJ6m[r uOL]CTJy: xXt8;$d-.܅QMD6Ta=j1XWCc4ֲbskDt>_Nl&!s0nl,;-#wl[/;lL[@0'FQM1݈P҆ƺZ=p|$?BQx[eO jHnEkQ^p{6vvvĘfcή2ٰj2^zu [+6v^L|6/; m:Y8};GT&gmXXX\SS u fŮhBsĢd /Na9m2BAY9}!gj4<3Ȅ8#OK~>Uaᗆ٩}8*aCL+Cqe%ؽ.3Fac*o 2gLbEX[NZU+XOvϹ>^~87pKˡY~M.؎Šb*y&#&kF/]5a&^:_\X/>; ?l H[ Psff!30drn)I[拵" F>: rqk 1naY /7Ͻ1OO؉5[gi@xx}>F^w9m!obL Knj7G?}u-:=*qx6x)+D; f,{Wƪtz'VM__c&a-xɘ27S cbD!"Et t]؎, 1fMT>k*Fը}(j(I\Loe7s``m5gb6x:/~&m;RO>O_EWFJ<#~l"D˻B[Ǯ(@/׹MbüL; P1Ϗ'GL/_3 E,>|^yg2S3w`G{dٌLL[Hk'mDEΉJpÝ.Nc9ԃDrE Qe!ӑ"\.Q)w#3;JJ Q9kuE6A7 J^,HYga8ڈC.jCk,ǜy;p=HPr$oK'ftΦnAn-#Xym%ص2ߔkQAh'Ol¡x;j[0Iq'ZC۶⽭gQ#_v;a/̜Bc96)iaaѿA/|VэJ#sJ/bf͸c+s1楷̐٘k!&*~&gv#yG$͏r_G,3s4dn*DS>((9ᏏvԆi`jL Z?ލUg .W=#gUٛQ2k?QpD:-;cYE- p';x 6~*Qtzf f:\r1sz쒎YSaBlAKE!nKPͩ1VJ *Wc诇{¦*/ˎ wDU>xWv#ger9jV4ݍ/IXMD,%_OFFqؚsnƾcܯG#P*\Τ'/GkYTF[ ޛwbW(ٽ@ikVL7 ,Gas8-w\-/wÑ'>E' Z31X*ykNV*wY6OǢ Šs31|U>jvW`R65=r>整^VLZWB,}eqUQk cfuG"6'8u;/߳#~?~a,&hGcrl\ Ӧo渼aӴXsg ؅aykQ\y vZi($PB+WX?+j>SXҢm-m ʏ}yrr醨s.$*u~wy\qJCR\?lv(,\ ytT!Yy1I%/?G[~?'IӐ^v|f~K2 Qt%QycxbCTJ?  QWd2>QXb\rVfrAN6Ee[ۿ/>?z |7C _n}:s5*Z9:Y$`!v/ZwcmM#We~i?>&iuV9J'IKJPeZuTsAg+9r/ IQN P՗bƁ(Zl|ddāVgfDH1A/VN >d?7Ñ6m()ُO¼6EqpL͕-n{/'foĚR9<,u[}gìm8V\mK3o|yyCafIvˇGMQ)?GsyB9ӶpۖR=O>22k-K1k&n[^ b=EAkQur9k<1?yl0|~9&&^井kmDE꒴D$3~{H3{Y"n3 #֐C 0x%E k2)JT=#*oDۈ(+82(2 ㎈6tۋW`Z1E9t *3s7cKsl,8X B#lŨ SMgv,Zak]cκ#YPp|UJnRNH^K9ұ&A/fʆzp0m6Ӊwܘ DaWtg+ayeH[?{Febcud)1p,2^[ q'N e{0>A7͘r4G Gi1k3֕n}5.YUw#Gƌ^Nx;h2iGmS[`6 ~evxƎ=0X߆zigvf.왛#[lځޞqK`әř*G(敼H{QO&݁Of ̏&WbO:L|-66J|$>3a_}lkņڤ۱zY!޸Fmz"RQzn??h a<}+8&=4sգ/u4VS{7b31·֚=BLUgpL)NI;Zs6"{c!|+01og! JgX-Q_fYd vڵX51y(YWÚmGoF ^W Q"g"X=|O"7LXgqe^ KKp$N)ÙZ{Z!i}IT> )HC %.9oJgz-y){,Qiaч6A7 %',L]r5.FYۖaxI[kkpnf5^׀Vo%/Y뱵tW`\x JĄITUؾp2>ȓ8joڃUp됓q?"";erVKf:Ycaq-:O_*b rm="]7A[u%JkDGTˋ`eV=X9j),} "B5˱XZ{rbX_,:ޘmg.Ȉu1a}ЃtF`MR\zAT&8}f6yUq4(ގa䘥Ț5J ]RUfaĔ<nҲ_y*Q^kWw21-IsK&~p3z(Q;&R բLږvUOä́qxzqמŦY6i6EcۄugZF5h+pQ>o>ό{t2ُUbJy7K *o toݗA/9rr xl3\ Qu3$%EnJ > QWI #5 ӖƁVv:GȴbMt7%5u9fۈً3c e\I#31l~;Al1jz,ێ}PX ˏ\6j:Cc/݂i#aC]HjMᨴm#`8(SWw g虫Y+c-Xa\8CgXr-fgAva 8w Qَ#)X| ~qf&a |!/[dblaڍ>҈&_5_)S`aZ>n<2Yoi =⺧Q1K q:De<^mVW5nظf%wഔ9G Gc5ț:C٫d6^1ۇ D[Pvj/͚'aԻ]w'ZM~(Q6cµ'#ma[Pm"[ږEY׆`&z q<̜[P}{l$Y9;t!}mDzܝX6JG0H\ 9{wtնXҢAG%*S;_gcKq!*\aJgd"5*uRJ ~ :܈JaӪظ풎Ǎk?c|G45`Ǽ <dl9됷0r*hBt&TGNn J'C^! J=D$Pl;f`UqGo:|:%lH:4:H Gc't"9e.5dž7f?g}բ5dw [:oހ9 `,82Vbʂٴ#l_i3NV:݄3kޔx{L5aZ[zZ<ME /uAnK G6&Br=[0y<5v6ޚr2V[wbӚ8rǗƫ&` ;b6ޞ[  {+Y$$=Ӷ] CK=`tنp|FufҌJrJɻ]¢OmDi+F3K8Fp5;4ʨp͸1BΝqI&]h=7's]@"d fGRp-(s9g'oLCb ep1'qs^ӄe'̌1w`aaq]_/:"(A3lQT1ћYh]1B"_haRGq}2q7 kJQ~cv9m!E rXL:Y1EMZ~m~$> :'Q&er_cx$ҘKR^뜚`\ۑd&D'5dF O%NӎI[vŴ_i$0$NX>/F~Ϡ e/zvǏ~QnT#+.͚8H% ϐZwq/)%3 @YOIt_V™{:-,ļd[,yfOǝ=RO'+'ZQ6aB=3p!zBTR\#oDl-+b;ǍTXROgrXt{F!0CH]sv2|LߕߝQtUo]7m~(*R.2*w逎R,i;EbRTHҾ@5r&faZ.k0R?|h"9ͪO/+qy]Q9fLW ]%*~Q#K-19;z.%䚸m,?䯤=&|//~Hhˎ0qw6' rT%f9j[,Qiѿ /'uIZr2>y4 |r!ܛ7,\@JIn#Jt^K-""ast xH.p4<49"%Up2>#g̷?-׌!TXHh/U1#Ay` qQ W5gaaq-C_*t3~LfF/?p4/2Čj.Q'$HSqKX$l<.aH@r=э$ 9<&G Ba :daFGm%IcȐ*{%*MنYTY"&KÝD7Mwؖ y<}NO/A: -N҈Myƣ-_Fgl`<7S9ҍ[1InXVϑɣsC ޓ3xJg r.#%0%ݘ8*ijK'^hE곳)Glr&;AsoB]Ι)/Hr(ϯuF8AÇp}6͚ߙvbg]3";F:L39`8*P9f Ka2$.+~|W⪡z1RtUTt gS9E \g[amXy1͏ 2l[탄tf_i8h AmvFTrw3SܛQyR ӿ2ogJGLT.y莜OB'pQڠN7;$9]ӰCb,ŐngL $2ʹG1B&MT&䟝zKPOFٹŽt`ϝ%I\4q&' ofBv7RU$$3> K[]vm~%S$lH 9Đ3i2ø#2'jNun2|& !GL, >7:BnuOl(gE<>RP74N6$GҬSBЅ+;4*hL: h'/cPʡTD ͕11:GxqaL+mIqi!rÄvC/J $qdҕ{3;Zuj59Ҿ)~Ql +Ţ4_? /t<64H _6#;'9W!i,z :܈JguuFVk;|VWaiH#Vݜ0NEX9$$=S&劈7 k |ݢnԋ5׉%@@=!"Ź&aEҴt_/򟺌vՕ.5?ƻiu=:qu'NkT9;Ij)]$g3!\wpvFi`TƝeE!TI;–)'>G>;˖I F/Yf"=:"߸$TLzg'FDCߒdg* wRI&@_1)%}%qɉs& c ?&+u͉׉0&<ȓZG)z:D% ']55mVs"ё6v%`?t%nsnSr!|& /bqqzS]]-jݼ÷ϚN%s?OK<α"gRLNxj{1;ǹfN5UMXҢy_ Q)XIT~;L iADFCT:$1ҙ=Ssy%tq]I MIOT&GSf8DMY3ҐոuznV[& pؓD% M~qG V =lmRY=!l LgPڂ8L!0lHPQJbPύ>8Z"*ɵ-rqr3ΑY8S 鄤2Gc >N'!iJ#E?a C)"4np!YKgH/5uNG4 8NdsNGYR4ĤܸΎ@#q)RNlEMqz$oYս F"+qrɢІ[} w*v޸[.UMCQiaqZ? k Qiq;!*IZ&쵼 j[ҢA0r$Q9529D> z0IIRרtH?!*%ޒ Htr! 隨LQJ#pb5!i TSÊSdofĊ3p4p},g @>IH hۊ`؛_ږvDVn11ƒ,,,N Hb'}8n4݌[?GO~:CAC6" $\(F8H.x3?K1 $iT\;#(޼O|0pQwN$]/9Ԍ4#roqpx\7# $8zԌK޴ΐ1./sKHHbzM+#Q&<90#1$M*~ OeI=ZzèW,gwgR˞I;Ң/!UGPQi%՛P(\k mKz`vDEa$QzLo5ˬQ;↯N-2% x96 B;r!N.NpQrǾ ?2N7cF8ф/*?./}XyL` FՁrtjH{W⍣1oϟOĎU pt_D 90gA8XF4z8i#j'|R5 ׵!W]Ą]ͨ:lDEԼH;s`=!rci3m2ӳ%@Oe(ID"a&A#h/ig,fvWW|IB1nZ"a,WRsq%*-,,'B=Jۡvkg#_,, N,,RaՁ9̢@0n#*ֈJ=%@.$!#=%*oYHVQҬ H] :߄hGPZxp^>rNJ~C מh]3S9% ѓx9Xsm!/=nsq*_m1gb4 O"pDNH qѝ=yHԡp<<{1egj}3d"Ga2я8(0I%}RFs9P"9r2)AI[r4BQRrҠpDfP’K)rr$b}RqDtB}tUƩڸw~ʏ[o*{]!Qla?w8){o^"*?rt:umfm&*Ե㍼Sxrc+Ԉ6UѵR|e~̉g[ 2<[JĚ(oNZ(p#n90?`4C%"h54f@b'11v!Z#aX}~Ja/aQpE^QQb'<s`WUvWL߄JO!Νޝ22*D8b^#9X0q‹ҽX7LΣh;UsZl-QiaaqNu܎x2`^Waj=:"77WeܖEU4D'1𮙖쩤'* YyQ9f0a4f/~ db bf >_ʬq o6'T# G#⮉"ͦ7fa,nkG}~x\\b֐ rx0oW`_s-sx'S<{cþb>u VgSLƴ]'4x'WcܳËO1k<8Kƿg}<-iL=^4Ʀ /K㙧³ϼPu= a˿/z? xeT&GY}=;<̳p+.@Qw O=,~ܳEn%*-,,Pߩ#,z]Wu1ݹE:zsuZw[XtFTr^'*?JI%/U.$!#QґH͡0K qF<ҏ]ø}m wo{#?^vP#,qpknXSu$ڌ?cTE9ӡ ǾWDzoE2m$1. ޏo ;k+q2o"^~2"gqphw1kj$~?x4~?uN.zC@%aCꆫbW?|P)ui3DC.t[\9-w #,.\=˚ RkT߽FgӐy"]TOT߮$}>.&߮殉I]Q i<ZqXqwa%kp 8w:Q㚐!'MB"X6B }RZsQ#Ά|ԜS+ %^8rf% ԾC^b/U̞aEn< RĒ &$mGx?cr=x.{?Nl_om6=g-jfb7ºCx[GH; Ean_ŸɟO/gz MC_%z6V9&44hP wCcˡSij72X j,~tD򟅅ՀC]z"m[U릭݅7W]ߐ+uVJ_]9SiE8`**æJwJG0J4WܨgB[pۼfܖل9uv nͨ­3EWiEeY<$nsX.K w\eFp͏e KݥE V4/9Vmv0ZȤ3;*8ų /Mx јu;^k3y }oU֢"+o?@@NN znĐoXEu| bݷ%Idp<} ~Ĭ-g0}#QEӰ%\O>ϻ1i^: ^{)@O tǼoK؊ n^.C;Mͨ  7j7v= | AWW퟿4'Q)YQ|fsty1¼ G^Ny'~`=:A-`uATfwt%Q|D%:!Hb1DD|(>463 ߀ۂ?T,fz'DKC h$i$hlCnO1qb'/፬hp 8QA8^ nʯ1؇z՟GO_\P0"an~]rP+ ]~6=s7Kh8Ęxw7p2\ )_=p羅xnq | 0| -ɜQxŏ2 'y[oN? <1ddkx|5`׬cvQja˄⩱1@ .X??왊I6*|'NHR;ArSc2 ,Q٥ʮJWQIMe{[+) P_[x{|!4?[TG'f⎹XyQtToux~dFsL^0״a?}_0d!mmCCMz?CDxwvv|?? ޜʚzn765[ϏkQn~?o|ѱy??u#%k .A$nJ0IIpd% M' GTNJNJW]WWFsq̚S g0j[{m8= u)A'* IIQM5 G^M7[C_!KǔY8Tׂv5lEeX8s> 9 j"hF\,5Cǰ%X>s>)GU0h gw`C0d!X 8-U;- 22VQ)P9] -6L]`6ޒSdXխ(ݹoX e٫u%*9JR7JB(\;5NyϹGRrDIi5 ~kaS%%\g$/*nR䒉}W&t91F3w~(VAS{Ќ 8܈_.;-9]gD̚tᚏ iapQxCx1Cr/QO$CXDK~MpMӽL]<%!ɯ^Z(7dSL$tKPI~!UDG)90!fYHѸM"+"pK7!fonJ /n~yiO|ZGGT8d#קd KnZ@xe#lމ`6Z غ 5{NZX2N k ET:$E.$!%ґ'*?3!QAT6+IV^RI' (E% zq%Դ0rA7Eˆrk$dHx,(XA^_T #k1Q" I :GMF# Vd "D&wg=G!9Ť'Q8ZyJrJD$|0㗈L7H8oDu!F┄i Ao ј!L""ypA!|q:<Ňy$\c(WH3^\Hx܂,{#ςS >JMrL}g^ x!OS?'a|-9!4y'E=orm'R k$p.$I 7>p= fTenpՉt`EP@I,/@H* C pc#|r-7[Q K|Q9$k1x!q2 Y27!YHJ;(qH`2~I&c"B$%ϐ\2юL(Z"AHhLH$ %^ޛ)3i[\m$G~y# 9io#q.y5+BKqdyRIX/[ \Xo_$9VU o\$2 x D#Z7 H̔߱גZXXXXXXXXXXXXX\kD!+iJoN%*ӂ)2HUS#yf^`5Dg:Yw2%FRmP8˹\/,ν3G#2>Xt7ns#`$G.2nKS ?s$*9yE'>u:ypwD(M ޫ gsN<7$ŋMY;rnGg3g1aEA@!S < He"Ԙp$jg"0:ND$$j&!P&ZNY8u8GfIW^xf8n0ōL=_ $Ѿ\KIz7yo#G3x2+F_/q/\y|=;kwBg~%HăDl>RL&k]nM򥉇=='^I'rb*jmA|,Cma,x}** sy&9NY`8y\Ö*QF?8zLTVY[ Y5Q?u;B x%,% d,[6JGĴ8}$v4oC[K%̚=qN9gtp cX~1/5[hq^|5-DXySc\,Gwc펓h}BT7B#-e;l,^$qd-<,eeiNHvŤVңHb oBϱE}Z9KwU\7jژ\&qkpz,=Vaf☛,hEK\e*4-܏Y;E:[r`ǩrT Ц]a~ҏar#X<_wc\,Z˖̗ NJ6U̮88A|ػl=T$^yX TCr4;w yn1Rz߲ Pr,}M6M(шcpbO>*9mL.&py3@@]5hK):#Τ.eUzK}y2>y4 k&r{21toݗA$*H`!()+qrq?JU"*;DeǮi귅5Lpx5?D}b-uHH,j#T?HTsTdqoh:{ӁK>IqY0;=bӍ>ш&ȑQ1cr=đb)шisz1%,=SoD88r:õfIfTr1ɻH`114c6KBRfr qGLs|?*AvRXl7|Ẽ$H:"o <8Ƽ~H9H+FaϿu<7~/JHTHqѧM89GּՠYH;l굛(*CEXd -Gc09 $-4Xk!XNar0K̹j^D}8" sEVvY: \۞eYXd22Pg{!AXl,#&, g|yެCrhwػ< Kf@ 놴lť笨Ӵlc*&tX\"81r,,,,,aIOTN'A.eaRܘD?}HT~JKTZX5|D HfӤڵ| $3h)SHx <qoP/肀߆H#%ƺtL99&Jܬ*87wq#'$ȏ<Ea8hͰYgVvqNrM:`c(IQx+xOû3vD A1%G |d5劄bƼߌK? /.\C,} tV" t 9Ig@856/c ǥstA鰳#vHBkO~cʽJ*R"N'E:QD*ˆ0KY$"_EV#|̷t<B8O}[x? 侂oa\x_yWDʔe"acLJ,,,O=6h_t?GP_"mIćXi%{P$z|X"9(Ƕ5Xε N=- Uf.lݍU>C WĺPҀ@q,} K ؿ|1gv`ܰ8XQX_֊p!𣩍t*P?KG8%y֙ǻx4ĢfYr988i@cԳ&X5N>2igOuq^ Qbǡk5{J%*sbm>clӥ-Q)oC8m)&LYiX|M^ZlQxl<3GQQ<_fL&<62YQlI0ٗKnM~9vtܳ~,e9ɳ`*añV4nǂg>TIIF[ aiVJ"$ŮKCw`>]`Cz\~AQ3% X=bCTKbS1/f4Şl̟b00,i~$dbI$;[!,SJRXZLѽ~sD%Jgo%*4#*\S#*uW_Dg)dȂ6KTZX|D%g (>Oz~UcPY3å9hdĪ'"Z> ʑ7@)ƻD' &G<oCkIZQT TUբ!F5%(*Eumsdt0kG(㟤(!&b'~i! XBPBSm]xϾӷbgq*+qMv&Ino@cM%$oU(hmEģhkDUY_shIՒN JϡM:)q᭭FeIkqr9 i{x6nEƓ7 BM:1yV/UrʊPVY2; c$iEKi%*ϡ0vMS6l@t\HR5Je-J^"U5R5f4Jg*{eH̐'*JNr\F"i0d :%@09Xb?*1E aɦCv $HA$"g+w#ڏ¸l@+b۱*{qÿ?[j @!RZPĎ5[nKVeU˒lr-*,ٖU;0@xz杹}3ϽeGzgdP0Nڎ[wrTj%Fe~-83+Y}U(HHAg#AY{[ '!)ח`(pp}rlr~o d[5D<0/E|\ը8PE9XS)lEq  d~na.]h}vg g7`& kCI_tջBl}ԣς) OīpBdni(YĂϺuɾ*OOTn@=ht-Cq#N0\;EH?ւfpP/{p `ddCNxl;աwA3/<=%CM^?1G8N cht#.x=iAmt0>PhC##Ǩ4fmEZb!8.p;ߠͶN8G#q J|T^b-zR,FoԸk5"*ϿFe:HT^~k>.r^D9&*m#o%QiYpjt">{Soķ;AKTt׼9pDhxb+ ?bgBnEW"nUv8%_O|Opއ/y7>~$<_G^GE0{CwpAo..,,m¥⃟?_ . w/ſx ;/vSﯹ?Е m<̽v|KpťƧ8 Q'Yf_;/?iϒe_iqqpڑU&ڹ"=m/+>eܿC+_߅^r).2|#27h5rf<=ݬ *@ZLk%y3=êCntz@ֽC|KK>3z]8M??xU!aC1YKq2Yb)RO,^L֔€M^rnفMز: {ՠǃh(ٽqbpw9tpjI+ґw8 6" kdCH;Fvy]BStC}uݝsMah$ҍƊش2'hʎbصW+k@!YK45@=Դeyl e`ʬb4Ao}QPC= 2a#('v jBl)A~U 'YkPfӋș#4{&?{<>oEq4_H F:4Lh?y &Ll^u8?vZx V[K7` ߏQOfge]ȺSBֶb-oX叉O/eQ7ӱo6DtDTw1ri l[V],!a0V%)?]÷ooK^Dm8Y+2?] 4G8:A+}d#zWg8)S{p]\ TĤ?G] n}@0x8߃߬CSqŦ,M(bLXWRp mq5*'}#Mu7N>t:lAcA=Yև~5#__1Yuu lڟ3f!iW*ga-A?MdOS*4!! 'ao_^-g޻+{أ30Wt#g~rW#xާ݃E<69 E8e~=XWFҦ4$lGT8rgE #0yU17²|cw { wBbèOh8ӌXިV& ]d<5z'EMn:JU~tb*ϽZƞaֲ.CAm'D`M!e'4ɕQ"MeH/G6څҥ~g JˑU|CBDtl{\[&4  AX"FuZlV65Q) 1 r K3x3,rދ 0eB̝?KDαA yO@J22KQp=XGW=n+Gv|&v@sYY6ÑA'QC%H2Rw}:B?޴y6n+l-^8AωeaXa/Vl_ e8z l#H3cgIq ɪR'[l[^H[OTޔCTjnͣxD-]&*c(l<ܘ~ ]t=|gpW5xwp_ѷq{E]HHH"+n7>+g`-eQe~f:;07O!no= ⻗7#{:c ~q4dpڂ_53փtzbhȐw%geqX{0_CmA^Ț/Ɂ_ \wp=7MWށval[{]N04N#}P7U!iJ_W]8ԁc-NF3 @5(VXD| y W* kӐ0*ڴO8oIMe8pcxZuN͎ǫbMHNHBܪWRR7u"ڼ.\%k[vҥjގua{K?|9{ڈE+]^#8]Y%[Q/؞6ry~*/_|W,5+$by =;lf]jm&*m[^)Qy!*2rlTg|EGTdMTbMJR/\afoϷTp[gpOw#sfNU_{g~xjB$mJFŶpAdlw =p 5Pɔ 1Ե@E"8YO]~7VHz/ <M7>=y(؆uxjV8T_)AGr!EWx̛7+Rx;1? jGd7҆]F[cڣac vwu[&w@gS<&=K7F]lޑ,<#qh`6A5Ն5C}q2l$Tk'ןerP4~rzj?c*|k&y|9ȨhưQ7uEg|Oq 3&cx>(ʑ1qLJi.=¾ޘ?b-6zG쏴!Y"f兦dCENa$f`wwa7 @7NiS0^ [s5f 8ZCؿCۏ (8Ԍ!ǒ0aGYԟUg+:'bNVZ؋uAݎ,)m[QQը>̃˧aT؆­H]!}_-]^ۢ!bhzԷ@׵6QX7(,-F.w? )هy/+GRyf"q< nؘRzX:zp*{ ֭XIy(.GN^VW P/ڎcӌu9u;Q {C\Tk[ ێSSg ? \Ry!*Դyk,<؛euax qbWM7Q !%.)AZwk >]k1:d.^]Yl;^6 8RF"! 5GEZ7 #n@+ئpb'!nuF""UvT %9p` !5eζxsdo-Z=lQ=qa3,li8[lŖ!ԍBT~?uhQy-U}9kTocr6Qi-o'yKJ6Gy*9ܑu 6;C7"} ` Ɨm&FZZ&*Kؙ 5V(R'=CV'7aMY9,Yb-ײBPF_~}&r aN#?7e-NN"{]<*O I}J}`HED"lWu.ہ#ݣ2@J

N4/E:|$VRDS+C :6n:B BX ݸ>L؀^AjzRWq;D"0Xr0GL=!d!0ԕ !*#'ajv\{aKYۋ&ٌsPvbI7}hw:-ˏMc}mډO.FI(h?\M3JԈ܅u{=N mXQ޲ y80䀧c'OZm[[9&$Q/zd^='amG#κ0kˊ42Iue-b됿5*_#* QymJ{+-*^#*'*7 ^@DsB"gQ [@KLoK2FQQC$:=kcd%cҩ_l ĺk^bĜXE@aE99qkc_i<;#oe2V湾QX͞;3ocqty]o_AA!\I|-j\nL,ڏ3/@ زa,7V?i,Xa݈/R>^2T~'Nڀ_X].h=-~O2) c}x m¤碬?͸f:ٌU?1NkAe]57/NJ90).__M#z;qbg<ƉʵxЬdbOݗ}vAR'< ?-]n q݁d f$Gꎣ}>lwȯiqz<ߵ 曭-"~E 1ӰiX96>mXWc؝O9[l"DKHI- 'zOΞhDCؙ܃8ߍS;$8du6^v䯛}K}`vb68a}g@-h9w]-7? EZ { sX=Ü9(;T3mc:ΜA*EK6x_3t>tȔ./ymTrcJ֞)tЁ4<4Mݽ6жmH\}] 8s0o@N]+Zgиo2墲][ Q© zs!Za2/!j!`tt.$m=NxB<4a9-Fzԟ7b5+iqt3 YMYV'R*k,ڂqރ=lҒ#8b3SQ|ԕ ~l,:i@Oא^b ]5pxB/ 8b;>eEOF,r#f סՋb$.^%GNI}H{uV/a7<=+wagM3Zc`R6ḁض8Z=|4N+O(ے(_؏[l- ?$Rkt[DmJr9] ۋ{{3xoWdxf'dר|o{SFA'yW$ኄsʷ%Q)uK5f&D% t߾HN]*0AueSL lT&Rz$L/ZL5/ҋyN1WUϖ7&jwMC?xO̹.9 [SuZ%]&!x8H .f|!KU/ `4ҋ{S>k?ogٷp[q#k3/a 1 ;YoK L ~u/~!Yux&_q*K۟AiWmEsb{&ղ. 1pl^ ٓʧ?w\ស%*DYi37v<8Cĺ!Q8gΕ 2Neu>qi@e$DMsbBfהZ.†(:R"Y'OU" p`SfFyϺxy^<e6۔raD{s-(u8+YEh`dk_X=r@՞-x~scxqU.7g e2hHO10 v<L<>l)DWcضZQZ5}X VoAfrGgq "՘DgaSob-rҧ-f:x'Y]d]r|Hw}1$+K]_ *5 >r~\\pZN |f(i|J1 |1AHeE1kn6PAFԘuW4>?ޘ%_K:> Nba@01xoɣÔ}-wu#RRS4Гs}JY ܘv]S^|7nn+eu|93R'pӿ[~%@{rDMoP;ŨOmρڶCm=qwEߙ&xY~/C=##PSp63F2 hڼFಣ Z[|! Ds(|N`wk zjr흃3~}p| R/4oÉD#/~ uFsc;3@S'zx^w(6ohE({jGp=onCFC̳w VPk]Ŵ[NLgz18!q=p '[V;yFO"C8sE2摕yA:wv`Cd̴zGtZ괨oV w04z[c?; wyYS_co:"tP,k$QI=".6Ui0a>FCx~x}wo;W- u]F>i3Q=~(\zbQ}a |ٗ0H|j#}>xq9'uϒqqwzjLr-g0F]|V:XTZo4zT\Q!Y罃G+HHuH%Ԯ|dt04gދXޕY ɩ#F=r}p0۷{{Bd2 4xp+5(^d~v <ƚF7d}֗eA]"S.HYT_K_Ɠ6o{ou'~|ld9nWчC~Ǭ/eJ\ZMu-눘4R=`=;-}P^/=d$bCl6DJqiWC[.Nb?8Qi,oo%YLGH3W/Cv K=>^Z # }f ];^v'GU/QViocL{ԇ6t.Rk]M ΰ]=qW3+S7tG3$t9g}S9>VqJ/ec/?Vެ_HP-g7_A>3/W˟$fIz!3,XKxԚߦ(tS?/х X!VeP43a S癵YxYJwßGUjW'һ(ۤf:TS HgYݰpDuhSn Q'2DFL᪴>%WeD0>7_I!?1'?{4FP p()Tl]}ZxXd yeX ~kA{Ԭ3JB"0Ebxar1}^&v ȉ [P0;fjXM_s7ELA^G;C[ސAژ;躱eK뽃HܒM8\ۀa˺{fpo(z"|C۶wLoW_y|yp[ ~3@y$t~Ïgxehno%0f{bQښ/G6Dc`fE00m6~FDLxrF*r+z8;F߆ v٫Xt%j,]qk+^k1GqTfITa]y'<z/buec1泟 ٌz 4x+k2 p4 cUXl ,^5KqT/g6VKkzcydj0G.B+"̷9~EIHej5uH(1 Ў*')AOHP)\rb)M !D"Ctך3} iSW@> ?c{J OIoOdOofOѢ:S/T0կJ/я!dՇcIÄ́QnNATVO:CE$ G1`Vq1!5L=Gc7ec9W0A*zaIyRU.yb,枩ձw53X:RsʔW%#M-=;xFW!/d1zv* !⩰fnhۣC}ڡ_zUYOlcdc֬A V6$2^$ T8q6x f_xD٦~kzf'7 Џ kn0 15ۚW(&ozt3[l[^dM#*oɥ;kkTCT-dy ;/Q!*ߓLN_,ŅF H;>ŋOfɌJ StWGi$I テ`2'a?9.>g $`!9x~ G3Wc'~ !8݂}[fw=HO;˾ɊY5qX 쨩AMُ7/®QWhW+~LJ`D/LKVe"P@Wh Jǖ7&g=yEGY֮ãWYU2.E='0u*{DGdaes8Y{31ut2Cq: *W"'# +0rj1q%FcqJsr34@6wQJNdڪ /aQ<YL֓F{KYƀUq!|q!n+.c&V}6rd-c'䏃pTwhtf3""Uv5. ne &03[l"6'oYaN}9 T߭ f"","QF"5CPL~ԗ+]1$4opB k,y4rT_Q GhSQY& YroժU VqBV%Uojw"TD򙏛k>C)W{6z\qĤ㕬*Y' |oڮҷڽ_mA Wq .8R|,񚝥PAV&M'k-Gna=|C7?[l[^e~UA}OTjc\\zk>}.Q9FV^q֝cw^B Qiv>KT^~`<#Xe9yUHN 3iCV~"5+7ޗ`JHCiA|4˛t^#BbUE 0ZQ>1C0>t gxa 'Zc׮](*?Cgv! 6WfwmCը /ŢÌ={öͨ۷n?sXZpGL"Sf!w`݁Z"PދcpMPvcw>Bf\ Z[޸9Pk r1rj~S&ɥ2=+Ǐ?8YkoR8Wz]4Y%z~eT4ѻ3[/Qz',˻s;2LKC@q9SduuCMmΰ)XJ[`k^b0A=˸D6̆Y" <DT9TzGe))gzƚ:`@!LyUvWSbZ߯A52~נW %tyҟ3Bub깨dY O|꺛̔ՒڊEt.a@U喱#KW-#aGuϢ3^>:ƣ>ò-rxɎJVcssO u B U" Os03IH;Et˜F1n$8g4&6ò~[٧eW!`;UN1$Sև+,Y>\N Ĭ>\)sc,1=lJeRِ\,vRYTserQuve ==\ = =k)Ja#`U)m.JlҷϣWwM0 |}c7(%lEoVlO[L:_$ڜydnl[ly=o5Qy+^w^rtʮ-QIo@`Y(pA^{V%>1֔Ht>9$kwJ i!|ovʔ >ÄOmSF@8ڄyxc0F?{Ɵbl¡ʫI3?ϿnM"qr{?+x/<ч>YE"G0ɿŗ?xʰTB/~lysdLwjk(rKwbmr:V%nMX IC]]谀%f$eP},}0O;K~=*"YfiG 8W֣5զ^]zO3:}p*T+W '=>e'Q:fJIr'\A|:]֕a|"=MrA|hs?ߟH>g3"FL(h`ڦȘ+P*gXx&Q oƋ}va٭̗SQ GA4&OwWu䶗17(F1rƣ|,N,-~gK`dtG)}xHi=#3>1}^=@pw;ihU SW`֝^?QK7 s5ݒyi066soΔ*[l8E:͐;~2Τ>-9ah83gӟad~lb>T ]t,-$ur`FWߨKM4KCy<ui+M+ PS^,-׍nAPP_r>R6҇;MAn4(eʤu+lu]QTįY<2crTʃvտZ0U?*⻐\ Y"oqv h>^Usϴ1) uݪש*YǚP}w}%!iVLV:AJ)i 8?!2sWg[ٌE5  ļL[p8}9~{կ-Q5 5CUrzgXyoF|?WFU~9LEc?><9tl6s{%qQ'u>k@H,AKѺu٦8|q(ה7Y6>rӴ2kp:^:fH,ffJ,M[$QfAx3p>O0)H*Q""ti`'K3ӯDE>Eۊʇ!U7ڌ!ge[>teh掃K#\I0wGtvTzBqTiA0N[}YSyA2FFH`Aud}*BXVkZgK`ր[GVkc N,Y+*)jDk?o,ՠIϥ:S;_ezb+I #aEV?j,Lcn3~{,F³5ӛIWX+>H鸊 bf nP%'ڐѳP9&^?UԖ׌hi\uֹʩ_yW&1gϩ7Y:PD"bNY+?KR\2oO uO2I¾7DXDoSV|*FLO*bn_b=r)XAUze~ ,&<+QqxNÜd'%#yK66GJ~6fL{'Z<|˭W=_&V2@V{?=oǫhΚQ7KPtX_c3s@I!?28+Ay+DԹ@u!;V!.m+ǧ`ebӲQsG`b<.?w|w6H7+g0h & xnϼo@@,Zb.cf}13`:\g,/X;37wXl\[ϫ*;2˖Y9c~EL1+LGkꏭGP,BЯ!tT]izHax]wf9Fp!晉M5{,֤= VTc3ϚvT]虩=-9l"Սەd~̆z m}USֳQZi1&V֢QlwGyoIM9Vې!y]mC T1)cu?e-bT~׹!*M;~}ßݘn:E1kJǢv^ט{ˉʳ~wERH[GVTt]_[X+y~W𩴠Lci|xKڔ.vcF;!\sv]Ո' T""b0zG3a'~u \;А ~OM+BK hwK[P6$6zf$am7[M(|Xx xOǣ ^FvDݱP2q=1 <6u3o@Ç'v$a]wcIX{s &}vej^8[ސʺQ Rlj{ 9XY8xE 5~8ћ-XL_j;Ά;4xz2ɂg+=puۿG>},F[l[Y),l'ok0b,DB7FDiI0%/Ac\Df2O"r.&oƣ ca>XD4MVn C"Q4h ML/$"PhY*zZ"g 55W"Vd^aGET>~?xu Ȱһ"zE@ԚZ~E~,RQcАƊ doXU" үj`y`/?ӟ2Yޔ~F~Uw]bpEYw,0S[eWS6ԳUBo}gUY䲉ԍj8llM A<3ֳ)O&62sf~0]E=2O|Glzg b-b[,_%*)"*?5NT^De7,\~JkJˍZdZvDZT D\ Wx m:҃wϬdz4[$eN A/)o [BHZO3A|!=1'/>M(8LKnLFE <{ĺ,$e!3m Lx /L]F'?s)OӘJ"3ӐNvTct)[1% 'Os@pjVxǏ6 )$5 0 _ $|AIxp 0\8'p6F\Hh>㛢0z0_=ջHaa^h[JJ"!TZ/Gey%Lı桨k̳,RotƊ4$,{5<{Ւڀ5ԡc4xիXu6ajzs1l<:2ahmpǘYjoaepDy10N׉8WM>[B_#~eM!l[lyoLe7ZDMYcVSfJM6y=~oM:JszkE [kx;H'K->|jvUg:_kuІn|>2^d{B6?-혱* 8c~}|K P>A02_5;ؿq=W s/mGkKx~_;a+%j]GұcgG3}_ab}=k`^7qgW>s~9H#X Ě>o~3 >bb~7Y.[gsqrqXx<8z(ҳXF\n :G~v8]4 X\t758@D6H1F[Fl0xD :ۈXf6"ҿy9h,V[l[iE$`Le}G/q$Z7j-,Rb,̂6!vE<#GP6XVu}ioiE<&f]XS 7>6Qb1\5fN3^)b9A ')Mz4֡ft.JVn!dr ;RL^ i)2_8 փf9*Cuh@H0Y%1x\f2yQOP 0_" 1!O\DlD;[ݨ6DdԄENI'ul=ۊkꞵ)P X?X PnѴְg; ݡCb[頴 jjijZdy1cQb-o!*ϱ'*>f:I)J_LmOT <Iůi.m.8\^|~b"exO//yp1>|t:päu(oË;NaZ16CuZ`Bրp70b>!Ojz6!`.6`W0,YC-cUzݺNĸA%PA6p)&0#DT+zy%ًu=)OnB%0-|2>-4ly"@i@嘜kU)Ӄ$ǁ8Ɛ."*ܣN\#0{Ć  OfXX]?{|9*ĺ4 ϛl[l'%HWkh0;? .^38DzL$exl#E}+ 5͇݈C^6:PzAxJA8LI$U YҗnnEID3  d+ rÝW6pƊMeW*_hHu۹13&R1q\@'>0 `!MUt8aE*z! 306(7"::g0':ʰjaƛLjB҂"4-ڪ ST`g*,S" |'6덲-^U֫]i`5^3z' e8=DZڡoB08bH|='i|l рqURb-""]ˮOe7l2DLg|ocQi~7JTj﷛E%#"CEJ|z^\1l>5!L}bc6BwJ!kj1?XU(זV cVq=AπD%Y,X h"3-] LQU X"I0(E=]Ӡb1>ʹ).^Qkak"+Q h#0xx%S/k$ĢNnЂm DԖc|0vMbET*)ʀm7X#֓`:=mB/ލoF7 u2ַ1Xb-44kGd)MO.ApX0kIt$-='>Nk}ڥ?4NXzP$ִG`}ĕcqy/Bfid\- rϲRa7䉉$;`EŌ"E'N<s1֫7[e!>NxY EK׏ STaV" !.1I,a V5Qi֖aX[MtDd ɢR۴/ۈuf- .RTSeDkY~2]7}P|zcg&c[l[z+!ߘr~\~n+N}JA mJAMy˨YM(l6"渭qۚQc GAl(95tM#'1'bnh@QB`YV Pj&<7]Bak ) nሮ`/v8-.kPL{-ԗ 2a5 d:!G}FVfJ-oXIs)5(ӵ֠MϘ??ÈJJOm7_#"k#ܟߚX_"C{{E /YTb-B&2@O"6~bJERr,[EG!Bd憘jT9:rqį^NIPÐE^o&#.n V߀̝Gq{BZ>ب 'h<~7w% es7-uL嫱zJ83#> us"k]ql4cdmI?qa(Hw%v^z8NS}ovh#:}ė-BL'e5񷢢קbպlXV#Q}v>fflې6Ԋap"&Lz*"nDB"ףpVKچ]N?ͅ ->\AO#vo-DUu3"E3 lg0*tZFdj-b[+Du[sǦ}gm?$*ϻNk+-?DT8GCET Lߛ5I\U_?1USi՟8]6S="D-N& k=ן(Y [aeq)uh Зrkd-f='kv=]zq@+=& klK,60YP!TW^"WYYj*E:Q=WVrUVly#'M=ƉG #-ǯ˩ƯaD8;d,1D{6"6@LUqW$ U5D@H>9B4ࣲDu?:"lFTa9sݺ$BZ\CaXEb*\ G}QSX7GȰ[֝jZ_Iϰe85q#{ o9YiPzdf!Q!UYdN?Qeה+W. AHxHD ƉqBrycu9~y8+ǏRkܥ7Ĵ3v8?Xw }Ɉ' GGў@dV{9 5D,  xW||#&*m[^1OiZ;cG{'/i1^Dݍ]T{34PDQ'EQpj: ~ԫޚh 1?1_7ى pPգk^Gwb\s#:󰧱 'kdk]#vFhi:WVk.f@-Xh9-*A$˷` יJ{~%*}غ9 ZYcL=]۱qszD.T'FJCȃV壨:,(n%շ;ЀݹX! ~ Dw!,X>iqzHp$11(wcÊ9|$m=c ۢIMU8Y8^q[MFmܽX{ kx3d(5( FDžYوm#- HM'<1xA5u MnI4Mfg"3FT[l[Z"!a_t.]6ӹܐ-k~fQ6}ֽeDWl9QDEe3ss>i\9$:̂^svV[R,,wvڒid gEtzMѢXhi(QT~7pB< ]U$6V%( #VvQYG#cT[ùr߲$Pek2y3j"5ّ3ax]y4Q22֑(X¬+d7EHZIqr"k/Kt"*82fY}=p/PU?Yh:9CK]pbCNz#ֳ𝜂M9LsjrVJڰ4cX>9ؙkRɏkDl[lMVtz{c\S14bd #gEDGmۑ ^S8UR]98p3~G g89qTmGZr5N OX,UݻaWe^.$D=hٗĭ89 z:GVj4nIJN<29:(]Bp^tۋQW hΈ>ׅFj߇ P4FN.Ŏ<{NKܺ{gc^hsŨ/ge"2JoxS}G{N Ԏ˜ŒM8}M=ksNӉpMߋ*ҷŠ8gfۋ=˰(? gEN;YyyG|pxرaΥX4At qOVOpZOhio0xC_[)f,Dbj4b> 'r |f !俺Y;>jfTd CEow;N {IZu {wtmIoڙ-b-M>,Q~,QZ\1}c$ET^5*o/4] sr7ߥqEsQ'E : =x.;^uOuRI% ^'t 1{HO ]bbl6dBd g[zPT%܍/*޷ /)w`ˁױňu$ zeĺW!20R;,2/% Dcxao{3JYyj˚+b-H-Iw/Z<6F SQbN'QV-vӿ>F}h޽ա?d,ע<{ш`=d8caꨈύ~ #QV`2S}{,ԴfЅZPWB4S2@c=4LIOl v/dHk7`޽ܵlQ}fcMA S  طd6JF@BvZ5:F8+aK,TQTO9GPR9` ߉ p/W 1D  :Ѽ6bЅh6$-ށg0 N?g|#v=.Fp T!{\iރ-˓YF*DRz FU&;X4J␝^ :pϴv4n*E0\CGtE0p)-JS0kr*4$6ÖY0)6 lލMf`O'bLeb(y%pJVfa_ܴMX4iE?0Ẋe̜ļ<36nCN;k V.[9cKn HMۜ'b-5aI=lJ*ET^ݍ/Ŀ~u .Q9fQ~FT;ҳS߮Qt7ޛv.YOT 4Cӻseau+1o~!7x!!0u%3ʹg)%GÉ xbtZ] $ WaT#=Q/ l9f>AlzfvuP*>c}sp#m{ kB$dn݅ U4:S!v78^.Oƴ|4܄bix~L{jRucӀ][ZC#pƪgH=/م%8k}5gcuUW^EJ4 n],[K>o|wn}5z?v?Io[pbDW"|z5чj^a?7 Ol[lh8JA}qE =!pd$Di˺BBT6 kQ_aa \.Te$ p'1W@|۱-k ⶖK]9HK)kPO<7ӯfٜ~_\jsW>`ZZ(GqzG6^Wb+3 `#tuwVV`_ \3+E; .5v 붡uݟ%DӃm(l5S~";죽, nT6ԁg"oZAVM5fe<4aOI1F $7MXS [zL]uA88نB@i Ded%ؚW&E.LJ@R%RZsz$$bV477Yգ{Er!#i 5)ݣ ػu5>33WƲwrGK0hW`gZX`/NcSm@qz7QجsZ d֠* nM<~x8N '%H(X3i^>-E/cXS"0N5b%'Y#7tėZ[lŖ#$\~F_"*5")mҸnfC P]!5F~E\@FocIiD-Xy+JoPkjr3ܷNB-عnҳwA¶v|D"P9IqxdΡLEo'`DdVT#8o*wwt )L$2CaiZG.ۊWބCW0q8cOh(b:4Bce)&B}X$*RW}bؔW}g>=R.D}Ȝ [i7U`3?3Af[lJQ/_Y5FMTM5rJҐ>jX̫Y>H k֨"ˣKkіD-3q]|oPҽO[7#x?nIbg^E{- <(5<`MwFYXh3yI D-rԒ,(J-I$JE]ncPlYGNwc(G(GۍX. 5=p4?2BP=^:,612N>y+QN+{k¦A{^}hf,*w zP0=҉SU۰zi2v WuBi|w%.Dgb*nn3:v!A8NX)+ұh jj*Q(lǙ6 +3wS/d MhܵO3̡*BZo@kO7<'*ۏY9(I+CCMy/:A۴Ot{(_ 8ZW 38 ץ {jvagQr7桦XEQW[XC}A+ՃXt?Nyj@1"\8ȼ*Wj3.݁"Qh:rPZ}H]_C<ӎ#Pch.ELmBW84cִ\$!iU51֬hAAl݃~B@5%kUbЁ ) z3*Z #=(߈ܝ5wadnڎ!Cȭ@Wz5vG>9e ]k?{v֢ͣY~ 6`粙O݇pze"=0^= &#k. Ҽ i`XCd}HiX0EL1/2 wSsĈ_7z˰rt: R}LgZY8/NYR0M[vc%8ekѼw=ΙY _]81J=SX:wϘM": _#)Crng|&q Qp:a+qbzv?vQkjQY9(Va ^%Xf7wavgXUhc]>MvNQ_rtxiEޜЍN!oZaK1g6NٶC1tb׆$8ԈnۅP>P.gcܹؐQ>Q{7n}'$tMԗü9&3`[T9&j76Jܳ9jܚz^*-B획p\=y"]a"LG&L(Sg<3^J[lZOgfԒR "j;~)Q>08"frbF'ieAuQf,M:ƒHXQ|yaEq vxKv6K܈[! 2c.%5, EF=DoL_2S 9d=ЌifO~fs,٨10uȲ|bZscvi7b_?^dzf4O]AymU0<90. DVe45p1[d0YȲtzAk k֚g֣dS"<Xu7g ؍6j[ nړ nO1bn6&CS% h~Ddgj3)Yӳ M|zjL;631֡reK?3Yc-b)"*ggxl8VHT*ىJ揀LR OexhAx߈7+ʰp?@w @Mѐ$`ŖDb { |s. =˸yIѼ uCg/ u !cà3҉hd _$ۿ,YbU-b&GX+i]oGC"=D aCQD #k>UJӯkca\+"K8,"Xd @qʯ̀xFG14WOkET2Ҥw+19& ^o@}d#?B?s~Ed`FW̲XrK?⃤ ĸu>J 1OA]qY!eVɗE^?鳼5H|>G?OX L#YEE5 e)rAE%O"?[UmuC e KU9զyͼjf{hSyXfʺi>za8Ԧbgc"O|&D};g^Sߨ^3DX=Å(o8+9Sv]QQb-a>#*E(?&߸;'??].h|"X܄h"B?@ơ61o7^߂[W5խyu;n^x2kzHlx,kJBTJQ-ƭ7S}ޤOFCse,J.pZJ!P!|&)@0 h 4 tYG@&\ ZCI ao ko Vt m*`aO(ךԝ@IPal1:Sq1ES%+%(RNV*S#b)~Vܺ+/ۉ_[[S \hqqD l7llo<,6WX׌UT(qrܩ<_A5"ȯٌ9UȬM@Us z;;S[mˀ# Ϥݑljl|| D1~L>Ɖ+,٨$,Ϻ&-b-b-b-9`8!GTD\r&\zC .5R7CTޚo冨&*/xRԡDƉ|Ӣ:4M!UuWpk\7}sc'Ѝۖczv=(,!W:Zģ9v4MDoQ&& as2!!!Qd85#Ob"#t̏,65*&DЯ+5Stwe,23pg MV^5ph_ :p11LPiHTc ӟ0S=̫O,r[P4!EpiJI87q'? //N'_9i]ם@03/R DL[?ښf;jLHtZ_h ~dS;f'r 4hy,SCLɁ?Xw~.8::jb-b-b-bp,_ */zHTޔYyvH<\~8Q|s!$L\DȒ1'"IYnfwP1WvWk͍uM;^ߎֵ0i)Dx3jGkAuƩlQ/"؟zko2SL˸0%~NttCѢCވBHSr!Y2*nhCDb4LL3̗!RF̊8gM6J&uBuH|qLy`. V~okπ7b)Hbf%cĵ6u76VtQl<aC82"rMz]*"&%+[SՆ.d"xNugsе Л+[T0VJӴ&Ͷ*߬ŖQt-I5D/ےڎKQz#rb-b-b-b˅#"W^#*5=뭩ߗjʛ[q9ݸ51Q6LJÄĎ(}@"xd1вʡvyyCv~ܚЇq>]Sh)u$CZQ<EfԍH)6Hz<#. W@guopߙLENQYZ.@<Nwa,[;v,I)b\o6IΈPTgk5;۲~34ipk_K ¼c^b-b-b-b[#ؾFTjlD~_vc.a.Q֔ ֭ttAi),]6QO@Tʄ+,E("[ĒȊZP;xoӑ\$ aܴ7uᦥx ,|R\UVe1EKG56d.s+Z|pFGn߆MS_wҘCхnck7>~ƩÌSIb ļ,#QY aɹVt($X@OgYE|hɀ,'yюr*e%|ĴjeK1#u8:dB,i]N^%\gCh8݆KNڤTN؂b477RݫY񊨔{S~+"&1˺VlkY"oHJe2Ö#Zĸf ei-b-b-b-plJT4{=.>ߔoԎ4Vg~]DOTi]GT3ܟ!Qd饓ef5+9pnXp }~E;nMNŬdcA?+~t֤WazId.O N0! j޴x 1.$~rM[?W*BEp2w222Qr2Hd2$hiG}U&f' jE/ qtLnF_ɜN:E* gCKB, G:f?t?}˷pMODrC^J(ӟÔnm=iH,z+ywlr~17i8Ѹg#^}V|ǭ0-"Xt F4xi쫬Ɩ"IJŪ6q V%!%-555ƪrܼ7Ƹ^|=~_]z O1K mBb!+"`HP3rG2^S1QuQu>^ĢÌ|w)<:V}~m|586o<ܞ^KG`+vdL{cvwD{ې22a Vm†- '000`I:*9onX߈|WGMGYo:xZ}fzn`n%~ˣz;Gк? so% /==soÙ`u=xo᥍QЋW_( "MV4"qrQqrQu%<>X5iHʣpS%ǏGd+>ۭڳbۭo D6""{#bZZ"|Eg Poye14{i T^cڵ^poEDe)*3>ۼ٤\Tـojŕ31lv#oNקe9%(;)dY1<"6je}Tpĩ"q aGh-⫸O A$r֎ðy=RUhЁMzqyѐr?89QqoS_O0uvs:To~5Dl'Rn4Q7p뻱A2*J'^S9Աesw{_Fs,S{%nio!ZGၩ8,D!:]f#\?e w"bӋƗ3'1uSU9YZUرg?q$H:tqwG$+g8֔w#hq:: 8t-߶@LD# jh7¹_^:U^ X|8+Tp2,ι|r`׏ *zݒ. I!G޺($+s yQf{F݋ȺymPEQ{]*ߜ~PN%>X{|Sp&ϒulǙ%e7#Ja&M]|OS\ KI|{_DŽK\ $MX OLPϋi{D$yqH>q@vSNIY6I+QIHgYqsMx ,/.>l0_sȒ/XrB9z!G.GNœȥ=pM)K }6O[Eя"\c6/xRo$!y|G[ ~怞D=a܀~b*߲91?*?MQEyKu)_\H G.ŐYNgXbٚ+?$TʃH<@L<sy{0j|Z\3Q㛳[p\=WL;k-aW}"<ĥ_wGD~6澈8?+ڗ >> 9R>;ƒk>%|߿F/.;!q"Q[މ_|.LF8$Us{& ^\Z_x Œ (߯Eykb–c]GF8djT7W}KQTNq>̽/ \7Iymdz/ _ F.! ߄hYjjknñ&7b-Xy9bS&Nsc~~Ok>Z*JEбVy[c|?S?PPSV˜WŜBg--{-hd9dzOϣabltCb˼8U}m ,݂&yP"[zRuJwl¼u YH(^z SSrp;:AU#sds=;-#2Rk,Zc/bE,tsRha/Xg"L5/rmFpbN} /<&ec}e7܁+yfZ?yGʫ_ZT6uX_k/>^xs6WP\g8g&$דׂ"رZ*s\r>,yf4THkLD+xeZEvd#{,dFu<.keJolUhZJR 819Y8<xuXPԅRrPr;NBtыp=ZΎ) dZ9%EONF5*FxuJ6Tˑ$Œ}J7ӗ:a8d<+>{tST"-% [RIww|wِ⫉%LEQ-D3FLMr F:2/!T@~ʤ!f} O$TXB%+Jas(y̖W,C&*;j|o~\6 Y+63pV\z5F/Swc=$JqإG,+~1vߎK,d/YmCJjۺ+d"7m)*AeOTMVtMM8zp' v:AܒBGQvVh\`B4`o8܂0Bؒ-QԔ,ێڋ˰"3+yE(%~(Da^2Ɗu(ݸz\ůq;phZ2\l*ǶN=Djv6M:닋_l?𥭯%ˤcشy3pFcܧzړi$N. ;ܥ[x֠Nυ9tgoeo qpIqgĒl&SK}P Q Hp=6bXlb/~- %w#>G;cQXP$ybמ=(!gWfdo{A[8r?rw?fMG]س{oAQ ]Mh#CiۆizfK}Sxr_< Q/bJw#B 2aknT^䎟 {QfOOҧHq4yX:wQ`_`&c/)T31#O&Kz|)j*(o wywKrs0 *cTrDEB#Twv Ycɰ'ß,ﭬZg25N$8!%,/͎ao}^]yW'ƕ񍩍0r^< EMMO?%mhWmEEX-g#Hڏ󑚺mntd݅ GZ,>9Ұp7Nȵ\cjS jޏ}21ul7Kv6v/5Řw+39޶Uma%>oEk-1S]cJ嘨fRfǚq(= qK*^_aBm0Ocݞ&r8AVnI|{+irdO5e8xt<Ľn'_-y{{`>;~/*CR癗qX`[^-Ƕ!f;Iؼ93JpYgq),'(w(X߮Pt1* `NîtN->",of}⿑4߁P߬*y [&ieIэnrx ^ۃo݃oNhq̈́?Ĺ Gqy[y1x9nMi=EV鄘_n<!)[)-U4t]g3T8@q Y').2=o͚aᒹdx(M"9q'f rHK%e'K\3IJJFR nƔK ┳+QbzhQ\ #%t8·D5ֻu'\[[dd%'L.߭[RǎׯB[z?hxXM3%m"5G0ElAǍSo|z?.u ^+4QEQ΀%yFQ/{ L|y8wj ]9Xr kA/Se\kD#Oua'VEM؞%GѰwCyCD|C9+ Xb&mzDxyw6:>ożWf"/g+𮜿q(Tҏ*@ 9r7`HjkN-ރPv,x%یfm$0mŽ;sG;ˆ_3`ס,_~gaݮٗ{=(ݶKKG)`gDʵlt%u.݈vEEbwN,_isSK 1 {QVv=?x=lWʃU{G[ -&?^5EQM n:0Nxa@G[\LB㔳u$ؓ ݒ$"V#t~4FlMF q.9N%[[J8" ak88[ A IQɛjtp?`iDYIf^ىJz]<_KOcwR\̅8H<#42o6")pqؾutkx%=:Ga$ʾu1yF)a变 r2Nr=|7GW:'e:½p}Ub_}?\Lt;^{lL8 މT>K` ~\yߓϞ9v3#? TTEyCg}+99l"f|p@8M9k~n3u P tGo'2?qF{ 3V"gԊoX|l'Po֭ǪgX$%Qc'v7";#!ɳ4S+6.ݝrTArf*}-fh@ݱ*ݺ )kvM|8OH<Cf>QуxfNY5tZ\SEÕHy=KG zaً x1_-98΂M?7=OvTh ,LÚu9#G'"pJRhJ:ݾs|K61C>XǼW^yc<Gq"3'ў8BM!.[y/#3o zLkny ,ǭhLCP)~^1'9X0sWb՜E(TFٖGP2,3͈DZѕ?P[}JjlǃYJzܒgmCj-L8d/ջU8fExnEfXy1f{>IƆ8ݝK0iV>Ҍ}˰ibd8j|EXeˑ։C83O=r?"'aKTʳ֖()Xhob۬\tG:8?zk 1OOcs]fakۆuj:*c4D>FSogE.7]0Μ}?qdz`ɖ.}AV]]n+A^*aۚ4,0({"ez$}|BC>Z䎙8H<>;ZP 5 1{to߀U/Nƺ=CŮ%XRн/c|twHڌ;CՁ{qpV/c`(jk/̻ ߇W+~)ˬ(5'-+PٷPݛ-*EeBIFam gVzކPyPkB%tNRRQ蹙 #NK e1&NqgOl2iHB1op&d B#ǢJ/~ rI:Bʚ(S8ӛ7"b4*e'YP[ soѹ-aD##[bZ̗떣92riP-qu13UCL`x.f qa:u`OقU&Q@XB*%\I1ig)y, vH`5E1,r|)Fn,Fn7mW ǫ7::~x?Hɭu=ZFõ9N;ȷ ҍ"% ;阕( pm>ȆbiAܗ0%3m-9p*"oQ `ftI2-K #jDӮ|Lym91Wqe/܁#1mRk@kk,Ǫ4m7<;+bdl2;XyۑW&!twtk*̙0 w~6lC6"{ QH Ɨ'aFv!uBhچ9ϏAAU;cyr(>}kHьXf#/{tc=^1 ؼ`$nE3NHm:օH- asf:S Gbڟ`oًSO?'x UM (K.N,}iQ#Zм?cnt:Q`sdVJ,Vpեdz1=5ؑTVv-&<}I</Ww/Ŋ_ em mf\L$i(Ys>[Lb 1*Sd:Y t(P&}v}i <%R&0.IᒴPنKRZ{ʦP9=N*1*u3XV$W1.^RT=xr=NKS8\7~t !B3tL&Re-iq)1e?#WbS0 dsɴ3Dn|H,;%>hmSf7&ŃmǤ5ØxzZ_z7aJyĪܰx|oq͚O+>?1\.}a,DO:u$)s*/^ A \Я08Ѝ((ܐxk˳Ɍg_3]@S!lO_e8ta:UbXQڌc=qyť6lƮ hOj 릌 c^)/c/GY[Pu"$ysH(T4/@mG+v 20m^c}éێ-kWaΆ#fh1 7aá($ؑ5KaW'p*aCq%saҟ_Ń'a”)1e2.<,+*G[ЅdCh G4DXʐ_y LU|WH_`,d-݌=rkP%ՃPk j‰=X3 TƁ}qX="Q?*X}Oͨ?1Vsh;Tm9ۅcf`rhľ2sXl_KbRYI%E[Qr Bii,җbu>TE\׋NcrȪD[(rcB8{ߧ**?Py30yf|A5e,S1w23Έ<1 )Pr"1hJ ZyYg;;B֔f%g+T5e >6*P g+QOc+X(%z\O.RKྼ'122Pq64%?w0l\+\y [O|" nĪE#.' /W6` gb_!p ( a_`d 3vŎġ|̛C={|%iӎ[9m A#)1X=h[sWpk3[q8XS *%8[;8\yiX4 9)+ &a=]ހ 򱷮ʹx1t [W]~;N/DEXc됚 ֡âo9VTDشE˰,}.~~,6FC7':sܭj 25UbեXZur:{m LYz^/u۰FΩr[z (Θ՛9?״ǜyXQQTYʹ'!cfhGcF+ȯ նвOSR0q~ĂY`if])rӋ?'PBv!7(b˛%󑑒oZTr^êhT̙uuU9ऒ̇''dxG5z vG|! Յ+XSqMQ oE5X5w!XK]:T ^&q cjDLۛÈP˺Ip`E-`Zȕg$[*(oy4wJ EORM=%T^*)R;)TRPicSX! GQR-֌0~rtw㠋BX(aB#}jnc}=k7v&df#s}>JpR=:(ĊM(ZW¢L-Æy޲;}X+7p<ץ#5{5Es]=2tF٪E>iɳۋQ)9P:>{ Dž7G-u7Wv9kע(c!2S֠#P[R hv|-D(.RVmQ#T6`ɸq+؉#]lBO[9֧NŢX/: kbūS.VK2^چ[iB,4s6zl͘)sӑ|96gnlkFsh/m㓙t)̚rmEQ#TP9芹BU-*{~+T%})Wr 3ݴs)Tf~ʷL!2MK }+ʻA%דwEW8|]'Ŕ)cVZX=+c!= ҋGfb4u:&i`~lhAFΘc)ÈwTbظ=G̓g܌0if> Scn]iiۍ-i<=h)Ĥis1gB,^KsWxF?+^KaԱ>e=_ 3w_ZCK?b*}ĵ0~Q-EQļ؛ )*{[T:]#TwPy >Z_1FaȌۺ2))ʻI.&דwG4?˗,?7e݄Qc䪯3/jgU_İ%_w_kn4~j_Cvv݆ѧаD&xl@5Y 7ԯ8MJEQ^\l#%Zdó,l݇a \G('@oc{|~)$+O'v=IWKO'BOc9VL܄Bg&&.$'$%ZL< 7/#F`q)yfA,AJO~o?#iX>?*&v-!{ԄBvH˳P#e21ٙgpF GIx̎Z8R&N(WEMpmٖsKwdOk5sATEA)KrE;o1kC)BI=0:'QےzgYqS5dG]k#8ebr-IOj$.q9pɳlDlr/(*Ⱥh 'w?뗔=1-;{R%CN $u3sRܰc9GҖv&Jnq%}\*(oؙB}s*>G3aY<[\ywa{JEYbR & _M;5ʼ3~#Si4{"o[8z6Nc^p+[җVq[Ŷrr_^8_Ap (>8s|W]b(. ',q=C8-GDLJ%bP2ډذ%[܀L@)Qd?Ot<٦Eo!/*e墸i*7$~ȶTbA5<HzOJa[R8ۏ–sdiD/3A3G3ݥ,wqNv™2EG)g(Q /喇wB`ȉQ|1IG[d²~ ϶xd$^%ȆmBvCܖSXi;yY?9ǝFnhe {,?"1גs' ːE/גT Ȓ2^w1O>'pȱ} *):zi$M)+Rnr>& %W_az,???([y6Be߮ߜLg5bYb9X_29F@L' g kR8Rw ֱċQb=|WZT2/I ɋYP RoW'_̹_ |wx>o86ͻM7&M[~⟡e/о&qfkűN:[pyzto؀~d*T*"o'~Ho!?"{bl)Qpd=*)xIgAH |ų>ӧ%mLQCK9;I`Z|AXw D^ 8LwpyKxLX |'֡@&;#9E1yq?xQ=Iߘgj{j$ېlr[[tTQ "ҳܖn|GXn c6 `)K B(."v7"͗&ZAJPĵ O΍-feN'naw(%]LFKfx$9/['ut|dd9M9ցěܸ=i09}I‹XEQEyCj)xc|<(PcwSnw(Zz )Ptgښ3]MALXlq]q&(ybȳJ:/93E8gKK4" OQ%M79FBX@Iq&dbM\cZ|.&3&TaeY)3E!a%= bU$s.o\kD% ̼ /sK9&?^{1$><4 u7/uǤi>R%3;i|+#Fd<9NrۓƛoDZ-s*ȄUTEy;ߝb}(SB`#T*_1*3CNs Jl)lOf'O*OuOc|N^N&֭]7acQ  `m؀lzݿt܉@&ibtb;%c}#0* #:q8 t&짳mIH" ef1˂q=$) p-OF'3:Բd>:Loa&'~j,YJI?r^sr|WJ'r)BF%[VJ1ݜ_v a9fK-md$Uo"M~F俩b2g|2`&]Də7:s0&KCm^#̥fU9dlodSʜhYbd%ABck Y禢(yc)ߝj_bb'a8~gx_le(<_YL'S ۜyȂ~@юaYx_l'L:1>AmF58}<4z# gUƷ1&iN%`DD#ŘKc.10&[<1yz2/pcl3Mʫl%gd=m6̊bu4)`KT3Ve >B%^)BIE9qhYe{!wy>Pw8(qy֋sgiH8~w8g4/r,-wYg8!/bDzԜ92&" _eSjDgCM 5?:Zq-K\W]7rҗּHf@J+$`%).)_k㎔ҡ~$ :b/%g<@)of0/GߠpDy_}3i^%'*EQEQEQEy ̻|B\:\siQ9,۴L<9N%[UW~Pٟ!TޢB T75;chkK \;Pn46ޭXچFPRdW@N:sliDBϊ!#oG4Fs ,%I+Iڀ]p(NJb'Rv1-9'ߘ@|9E7K/<%Iw㉮Ef.YT#{%=ˆ{ጏl9'l˱)Z:A=f\)8JX-Lx)zDU2D~t)~+0r͒(FFBtⓔ/= (n5Ec (((wv*ގPI3be_ .Vd.DZz#5b Wv!nlĆ(vۅfc;eN aQ;ᘮ$zp;MoR.ŧ?W9Ӷ#AMHB!v`fd 2rSu9IϨbF$hSJ!t\))Nry`ZjbQ NreҤl[<`>-9)1ϱR>yhq3%#ypAoVdsʄ($L(CA&l^.$M>p_߉EQEQEQEy0*TF0F<oBnEqN?0vDQǏGuTةQS[YrFēD=0&0vLXb5Vwߎ1<ёQ97|$B2[ёP6#$K:݀m>HM%GL܂FW0B&{%鰛8Q+w3h]FHJЛA{Oa%c ,5]C1ؒ_Ԥ7q{HZT&Ĥ~_MS [r6^9)شc7Z!3a}I o((((U\ʳ*)L&dk~o )iKp1vܷKm@E}d} 1QwNΎ(AV;uDk;k 0㰍;8kƏn}~L@aR*1QF̋FtrK'e#Hb"O1nQ+1L̸%H a ;,IYp3g# qV 聙=LBlDkN~)t&ʈ\p=}n16tHz7Y-8C4o%Tcr((scM߸EY*KؼmOI\ \QEQEQEQs%T)U4_!*ӗdQ:Aξ(6alIGZ{ {:0q_ŠfD- DݑjlD"&3SLG/9ZpnO~EvUb%X4}"O2XN6B{h>YS1uL̞6>Ԅd6Tvp܈[(z"a4Whf.vځ̙4髊QVs'c 9 Ǿ8\N qi2_xN ^]ŅH̩ӱ sV$ -xtjY+\+`̩>c2cw7*c,C[GQy8oE阾` f,\)saIV.O})UTEQEQEQ* bW{#TUR'[S> BEKQW_mXWׁuXuu]Xs55fb6*Ku{ qX4Qƨ}_B^J<{yq㳱X#2q۠{OK/Qg#Z ԧ+~$^핋/G~#m Upa")RZJo߁%Z3e`4L3ePYYiɾ'ӠcT*(((*X*ga IK&7Fe ԟ“Y3v3*3ҩG 5׉{[.̛Gp۫j ؙ9w^z=s`0C(Ϝ-^QT휋[x.w7v<ɘ:o"ڏ ;c⯧"cQď,G3_|q)~p3T: ZlDzx=昘r ę-*Be$Œ,\S0ua:dc4L98\s1Kg<[Z2MeuEQEQEQEQCT|B#T&P0#G* E#졵EO8c#C.#qfu(Q|PtpbrZȘ[/ڰ|ή݆ys1;0+9pcAJ:='` ăY< ~w2l9)q#=Fލr}0㧷?.Ī-[\⨔a95L/̃c~_"Ϗ .q: ^J?q8n9v-/ y|>uM۞1||l>kIQLϖkQ^{rW`|%8ҲpX|ǣLW?c~*(((ʀBʷi'[Maoe2B$,%i8݈/n Py9Pxq:N?ʪjlTMm,Y1{S7BmOJ6笡B JVpeYgnU1۞[v3Obe#ޏmq.#e(b!ĸ;@= ҧpUucԆqݍxX}/8arN| {Dp2ۆ) O>8zuxqy[}~T4AW}.<2Pg8?̉gn" 0GดӶ ݥX?^G}Ť`\q%eG沽IsS0uAY_W\?4]EQEQEQ}ୄ˦c姏Qyd:Bᧄ!#Qk0ڵ[>@B1#VF1+3{t ]BXR\+T;oE ĉhnmGMQ=z ' &ԒVﳱ2N+)SF*!E9?cJkPY?s;fW9hw*l^<Fqclnô_݁PU<?qۂrh8G'Ac\Tp(]2w_z#XhK07ѧSPY)f-᚝]{>$EKn} R }jK07- vE$7qK ˊ(((ǜPyso=*G% U|'Bڢ3px-74P-*ס a;WM=^bJjY݅IOBmM-5[ETځpZl|  ??gzc𘄗{s^⟟#>;Ltݨ6S#x饗1u=0>sۋxG/<K[rёPr #Vap߰?`1sK1w{`u};ڏŒۿxK/is7ZNdAWa TذlM/#zK no X\\i ^{e<~MxiH=6NR\LZr%Ŗmފ(W kE>e(l Tdɥ (((>qBeE"Ӛ/J#+UL;*1*m,Z4GՠQl۹;vݻ`91O|/3p%N먮 #S]>w>_?_A[\]qyAFywn-zv3,~osm?cr˱ŁP??~'c]/"}ZliCvß[݀6B4alicg }N>[݄3^+JѶ~rE}HmA<̺in #QFs~TJ:'ހ_:`\J j؇C@R\2ii===زu+XUD`jm)AOu|iJEQEQEQEpPy\ z)*9>'96 }C&Tr\9gF$jlkGs[:*]h +Ǝnuu'ԉ3ϊHʆ9QFwa.,9;. uD*slȎ-K1ϒ4v2$wL|Xr $ai)~A@=4WҐr;qGOrłlQxy?.$@h=8XT]6I30Lk [QRhLZkk+.]CKcʵ8r {UTEQEQEQy=Oh"g#TB(*>t-*̟cP{/r-U˰bRZs 2XL7oq1e$TTJ[FoPRA'cF (8E(Me Gr^N#& ߉ GDTV@`HxWd⒀dH|S:7WrԈ B|Lt((l/a؀E7=9'X9WG1Nc:[BEMXa")(rzM<)@RvxľP((((zBcPy:\4?ZT:5{ꎜ@4n!  #Agg¡n{e;N^Be!DY=9rܗ})>F&δe|7xߴ4:'#焘i|m9_e>[S՟K-yPkpi89낄aYO?𼓢br2QtL㒂ceb4\O.UTEQEQEQ z.bo.TrJ 'Oq0g<-Y\dge###Ӭgf#3;Yd}d”HMM˯JD| KYhq K· #%Z+ZJi4aa1mX.8(R/%ہ% IڒH|6i6A GdKXvy.r I)~)϶R._0]!=6e q)=?[J:ZS(,$+NrϏMr[VL\}xzrB(((Osy_{t{x6d:"#Tfsho*4yP>UlW)v@ؾm;JwaRdfc[c"l-َm[&[9Z\D:g$1v`(:bVRIT8>#R*7EBv w,ڎYw%VIVizQXA qOIY$ux,YVV%}I3%>[tR $~L%P5$sLvQx&y.stlb QKh SH! )$rid 6ް8Ir_r=TRQEQEQEQ'빼PyspU *ec<$g>ZT'8/vR.T.%i8-!T.J Fͨǧ&6SE5α":MfLN}liH1!:&F:lhAS=e#eBFcxNK>ޘ#M%X檱H!6%P&14ib#[OʆIeIiD0'Uݶ#2z֗ccf{3QddVfC"Qe HFpu IrnvC},Pٻp (((20l$&|#Tef\AF\"òrTzR̉⯶\Z*#.PكK2qIz*/Ik%)xq3.^Ԅ6'pj||v >6(l0EQRQEQEQEQ'J6Tc#As?J?\6l*]H,N`&S,o TPB (TTEQEQEQy= x}JQ*ߤX9\/Ad`< ~Xvzo*UT (((2y+Tvc?LʑgQٟv?8B XTTEQEQEQFR<"}ZT*{ǨdoCI;)T#'krNӯx.MJEP((((PyZoqicTz1\!Ö`0NsCFeV`Uzm *T*DJEQEQEQE`SPL )gbI2 C-a2'גq^`"ppTFI Q ,ddKI@[ٌFV$EIG{4$'A\d@ X#yM% Ţ8^])ll .K_q=eۖ,Q%Fy;$ٛ&XߠfכuV$ozm ͻ(((({ysZʟ#OfS~: ^A;9ؠ^K=x Y_hQ9r*8re)V~ZT&ɤuЀ7b˖-(,܄\  Qd-FaMRuSٲś7pS:٨(, s%_ .$oGT*ko0Q_R pp(Px%HQI+,eOhzL@ lK֩SӋ~cA5' RVϖ:r| R8KA6>wSXτC|:l̒O]]V^ qǖC.V:Vb['۲pm5Vz͇(>C66P ۱cN#,5%[]i/ ?a p~3bjQtZ]z9!,١P&£ Oy=܏<0<_yr^Mb>w> 오%}QIb*SiƌhHDM{N>])TRnFZV5d+$]EQEQEQ}l+zk(T.IB%gN CG YB<)VZ IQIjjjl2XT{q2=<94xk;ey"ϷGY7x^ZwNnje l1ɘ.9fsвEhq\y޽h\[Gi."1:X)83c]CJt~fKKEQEQEQEQށPwo#T\vj"#V&ƨ2rXBr*y?ᆗF݌ Cf(ӿnCiU{`Vx.N&^s+ƥ4\qWF/y[M/4>q^=v.| ~SK.?~9PjӳR9'R"muv@e%v@ƭ$aŘ մLY]v}agoTEQEQEQ=otB%*iJ Q+XҒGu`9FE 1&#׬Ī={vջuÞF{=j֦:c.vEY#L:^SDA vf} f8Dѱea "v5x1g*:{pp<~w(,ބ 7b*ǂGounƆho[㿟eFó_ Zcu||,/(R}yz۳VpS6XS'MC(ܲF|I[/HtVw}6Jv] yX鹘LK9`k.;р&-Y)RZ՛((((oS|1*?8B%͈~qkQ(ВfEO}^3I

>vŽݥf\HNRñ"9$NJje~vxmOOCʞzmnLmA2,{)0eKM7+iwbfdd%x mrs>AOk'ʿN+ ]Be;.Ni%P$\goQE܀o}\{~>| nzQ<6w ^Z30a ]S(8ǛQkvVE3y6=+eѯa@F>41x^ Zq/=X<[~:1)Ù}ƽ羌z/fVq o=u4=|^ ;d+ĜJnX6^nxOxq#Rص;JgbӏbXma3b[V ;h ffy< f/AuhhiC7JLO[T*(((DRAW+I^r271FeXٯPt]{.JO朲*Z3beDOduv|bI.Ni%[Lʁ,T&cUrBڵkF[W~k|_#o]߅wOf )YXTX`GYv!y0\x+Ubw`^}]P.f!W7Bפ`a'!#b%YO Vt..N,{׸{sqz5I()BuA`l{HKn(:Fc1?T<\ 7cnZ6椤cwA,K1)3nrB(((OPMMWW-GNWbPC2#R VbČ*T_/I2)UWW#//QO_ÈGp#) { <<%K8K7b}-ڎ ;ҝe(ݾRVȅōy؜= uHD÷GNa)rvvP3?7Go1X~,#%S/(.aJߝ|" ]Îؼ<Ga'Na#XO  X,5__|jO<HU7ֻHXiWСCX%ՍN@JF6Z;Lķ)qP((((w TE`ӢP9xXnB%EJ*Mi,Iq~|iAMS7jd [;QމƎ4wv#1tDhDwڍ]e<}8g?(ǾmhioƉJ޽tw\״qئ5}I IkiiAFFiمԛKXKGn/7~Ki1I˥ (((>a^X(R.lM-}fʡV:SL w+BPגF(";;6mBblݸ 7mDqFl+T6`Cl"7oFM%%(*V\˖t훱m81 ʒcUZp}r5\#+FTN֘b#(lJZԨ|8,:88$/H<~8$IZ7e"$[K,ׅoG# ")m:Zf@`>q`K 4h.U{ⰎT7o.PRQEQEQEQWwtj(o(T^nKL!ó0tDb2NcdkPyHB;@uZGGߏr,߃C{qp^+/߇߳oq@۷{b]7l &QIr2 >)V>7J*? B%[}q$)QGȕ# Dxv (V$˜Agq-8[#2[7}Lq8p$-qCOio("@\<&+!x(`+NPP%/[KKgfLf4&lGlʰl)i'gZܫ (,)u4):rG[wK.UTEQEQEQ zBgDrʩBGJW/k8NE%E G-X+T*g7niBewYsJbD$f8D(R9!ŢM8d Jv6Jt_]:f4e=nDB6]Nn8QI݊>vN٢딢ztqɗ1~5Ǩl~!mOR r4nl!$gf!²Ǥ.2$MJ1W%9[١x}\,[Ҟ1 1~r=TRQEQEQEQ' 97EʄP=tR|tr|`kJ )Xj5P%EǷkuV'1?|3y*/ n\މK:PyIZ.II/lJskỸؔrSXOEni7p eH)#e癢XbWJrV_ZcdJSk}$BS/tb;y4AB:sOS *EQEQEQe+)T~'LgiQ9芹ES & #‘1OwW|-)Jgco%T.;C\*/[kʏSzlmH͇-#=-""pC~B((( 0D7*9NB(?Be?ުP4]=NҜL'qXG*RQgTTEQEQEQ9*9FFۚ\Xy S/؜͛)Jr217f&qd[Je B((( 0B|Eb7*H .fv4fNpv{CJEQEQEQE`Py< 0 ;:P ؂җrR-T*Ε#"iJeB((( 0޶PBe2P E `Id߁l)wX+l(TTEQEQEQ*\sP9dx- q#fogoZ%T@[T**EQEQEQeq 92K<δ&yƾӎ}ʘZ9w’qoYD`EyQRQEQEQEQPI\Yy%ɾޙPy||v >6 >6Pj2\L۰eY1s.c< m97)Ӥ}Lqu%8rx1alƒ20-bn3 $4i$c6y02[:<ʸ,3;CP(((( =&՞ N*?r b.]_1*)R.Őe{%ZmDʡ׮=WB3 sj9}(r `|T9m#!{I4mlj|Cxș}_HuPh `!w᳥%1*0>9Fe:br,*~\R8W9ǵaۮH:r̈R.&|@Pt\V zEK߂čycqR, $%EPRQEQEQEQ|=PiSB~+T^6\>P/ GQ\A{ʑ=j%ZcD^.B__ΥPILrA}EeRdDGs.<zp|Xa'!"cK4 jD>q q3'Ox^B58vkp+[t||{Nt}I?0aDM @*BGSg[u]'$, %%EV[^XԈ]|p_FL`EJEQEQEQE`PybӚr0B gZTR4>>6y#TtKKy-P5qf\'=\3vw!'ʖ.E`!ؒ ^[r!˅oGYkG/G#A1ݰm80{SmEI'Rd]IaKpt !ۙ<,یQ9W`)rhTPyPPyqj֔qaf9qO4W,]&nb [=:F4qو{pZl_^5|;?8cn۶߈O_kP4rr c&;$5jl%p Gk4-I☔>)ٱŧ>|̄"9$2QRQEQEQEQB޿P9d#RF~iJ#Tޢ|v^M7 c9ENyM092QR#f;J!G*OP,8ҭŎ EW㷿o2k'vHA??u7wHMDKqxTvaG6]?_129.e ^ 8%ǵ|[> Ksͨ쏲u%q8qR6*T*(((,Jvߧ~g̮ۢ}JӢ_!|s%TtfJ d:Fm}v&ON9 pƧRqڣ݈-D 7ˉK+~/\_Y=ݒ}.s7cV`Sa{ \ՊIEߍ%<~4!t ^wnq=ċa4V.pp{W!gŸpw߅vقˇdzaB܋q+ (((28B%gNg_iZT}HXكOd!T|m*p`#;89xR=aVwլ|rJx72H홮ٕ(/sll<5>vwq36f<ܙŸǭDIgrO_9\t;%zxO#{ rHy=ǫি&Fmalxa<;Dq lxA\Hy9Sv;RvLgB((( 0ΡP9tgHGո[Qa27J͎ {po"T'J#VlUi&әW3d:`ya OmAZUnbwo jqKԢmw}s؈-άW܌.? P?/.FYW./%-5([:b<[TqI _"RU>7cxk;Cs"wWը:w?nXRP>MG)su|[>GlBR0*T*(((*e/)TxSCGH'[T .xoʤ;VBwSݿPYϮg TQ lҒ ֑ffa%SN "Q<|{\/giʟ:*]t+k,o7`ҡ8:#xW^,!o3^v<7z9۫'gן`BuO*d=zH?݋ ݁Y.LF4cWVrf0aIC'0prDn6)*T*(((üz?BG. WÐkRN faYbbb}g=jk=*{ַef^-ʓBe+ +Bpo ]H?{(ipcqCm$a Eݾ-Ӣrζ^z^ñѼa:[ns#Krzux֎go lt*iU,37sXQ+Vm3ϿVU: 7÷06]X;aᗷn})ǥK߫Ŷ06nLJ׼wa #Br} ?ӯ䢸-B?|[7_ހ>|+_টA^E#z6.ވo oqSv0িcTPKooxgmAUŠSQ)#:y (((28KrpP9䚄P9B]*'&S[qϴSco`ouMmZ*{ KmRy1\DC\h-H;܃.<]ڂBƳ:5PVv (lAH~'PW O=Ǟy O2`/[ݛev֤ lTTEQEQEQ W/kRX9Bp ''9%R cTR-si Xy-˳*5 P(7wzu LE6>9_|-X\%,8`Yp~Fra)k|VNcƁK蒥 ήנHMԝ>+sl'Q-ӽl7*a""iزt<A> md78XlLzzVE`yXr&]d̥8(Јr~lFYR .[C%u%b~[΢ k+,c86tQLdEcKܖL[I)CnjHŤ,gXJ@JJNĸ_Iy8dB@GJEQEQEQE`c2-S-*d:yBVg9* '-B%Hx\2O9;?YڎG\@9#.])>:);բ-00?bD ɶ71et-!& QGQ"P()rhnd>_υm4II咥#=7ǎlvJQ p,s@(@FJEQEQEQE`R4etG2!N&J+)+{PI\頠vbUMkj jMKC {qMIuڗJ($=^blH_*mKv$1[m?cQzcǛt$}"1W>/nCԥit;! _STq#Fʒ3"<ơʓwc%OeB((( 0ކP9 : I#XɖCG-b֘{ʤHWſT"e=vxFsb}V\B3~/<ʄ拖[rl%iY\d7C&VJbLTWOT$1\Nq$X4,x3 H`9)`H\v3 g dzᘚn D} FJEQEQEQE`*/8)TrH?B#1*Xos3wR{af&/~K^;Naߑ)+RĒX+T^*|AJEQEQEQE`ݏPA×-Ǡ0dd*ObcMKPYOEJE9GP((((*_1/ƨ<{x)L3bػ,T,-!T&:7ifR*T*9FJEQEQEQE`mR ‘ K C(T;NN̔OYUT (((28 o-*;lQ9B77-/ɾZ1{ZY*/*/I#T.:Ǩֆֶv;~uuH-jꎠV֏HX6 X,QMQ<+*0p}jՄO7yƔLjcA> fH Ldֻ/Nbb(21uJJEQEQEQE8*ϘLgǨmQi}ǨzbJ3iQNC;=\ueމY[?B%F ᒌn\N#!R]lʦ~_4S8MP@EnIqgϽ?w)>Os_4~s,_}>|φ터iIXpn\D$`Kh%^TɇSN[tr-%SI+e B((( 0 lQ9%2,Cgc\#R&ʤ hQ9tԚPyN'+ÝWBcTUy*T>sHpuc?w܉޳?xd;~z\?z?4 ' 0}LzoY0[B:.nu{c1ym\ "5td≌_?Gp" n91#fգVb+6-ے?>b z 七s㰂@rغ3B&,+Fth hTTEQEQEQ}ƨ|ߧ2B%g>~NB176O*L<*Qn ~ W+'7⾗{s^'nSy>aJ7oG9۴۲-;`kwh'6ŌqJtח m"l=،X`ǰCJJ%xǷGb}Sw(<|_m#:|lKv%liCC!3&+q#^:TRϒ4H i%>YFJEQEQEQE`Pyr)Tb5t1dxe21* ƌQ uSr@X?B'" d:o(Tέ3y T><6o݈WG1su ^IKm/¸/b Wb/ }g1+0aOcY^!GTvlq_*+g]˦>7amӞ”ǟ* f#(d`Wp;؊t7yd!6Pt\m != H)lJ4W!D|dž[R& %drσ%w):>[j6DJEQEQEQE`hQg2=,˴<<׈~jQ1*\#VPi>I',+>~̈́J=Py |gi qn}ǯe{ga̘?cK_Cq=sx_02vlD-xVsJ]>Rlg AOXlw!?><ۂ<궖)Mq]Mwk1lEx?NAQ ۵ C()ތ-;QQw]jFΝ\ 6o@= W<س}3 h~U?|wU,nkN`ƀ9G0(( 䜆!$0sH?snO0 Eig]]]u_yd?*#v'vDT   Bq2-f^ ΦHJHQUZJbցxꦫ /Å^z!l0u?FuMy.2rǓbR~29XmDT   Bq~+Q2RLרWվWJr}RSET^7ߠ-1ƦN7ቷ[ڷOMwo뮿=?}7G$Hq`B8X29C 8>,7 Q?oQ?| ozc/y:{ð.?m4$ghLN/`O`&޺"twf8}9f}7"]>&8?\Her1JAAAA1NBTFרdTNUU9PjSF$elu\1g/HK/_kO֖c_ <{_<vqښ6*NB#RAAAbuUk6-fEDe8BkHE|+PVK<XT#^DPs(UkTHT^<,3jJޮ.w/3pCmq/_cߠ3_GO⃧{[D ܌)c@ظe 4>֠,*AAz*p/bixO1h 8CtžYrA |?#??|4Y|U|Qva״pCx` >FƘ˱t&d "Ƨk*3sDL z,ѐ' +$`°/'o>aBcz&#ԨLG_ET   <^/BQi;Ѩ.wrrrT⁇ڡ_D6[Ъ.x|#д1^4y>pu5u͛7KJ €f"D$SCyn,%}A~e(/󢄎 -$8a3} kQ6[?׻N җ~{. =~!`6cQnv }W_b,YsbDF<]^DC%{ 0-g܊AAA QY;QI֙,ta^}}R! <3w|xs/zL<ݸm&ΝD%OWYbao "h`LhFpz!}j XzAc*{ |*V08[Ӧ}A:Gh;H}B(ׂ@eBy x x J xu,hA )8"*AAA!?OJT&)*[TU!ZD%9QAɰb:? ݅NKUOF7C;'o@koyzd Hi:,=HQa.AY6Xαa3J+°4g#DDžxMaWн|E}irwKY~1݂A4+>mS4ؠQlVԷ$,"*AAA!?OBT5MD,)!OxJS, k; 5_dXD =,*{m[w駞E`ȴlZt~=>|? _^>c趸Xn= -h#Hϥuu^+YYikeCemҸB%4T6& FapZ4n*Ual}Yrqگ,, fVIchMm$KSb   c'!*9ʨV-rD/2l_ԩNeVV>3,^<>r/zq<#<w+g޸/=s3:~-f̚C7n={*AIht&'(=Aa"YrFitΚϾ>u]?BzÏ;‡=?]{o{1m\̞= n:A[?CDDdTTrfU1_Mg, Y7*4Bu>qy*7}ƙ!=ijԷfӦmaT|nHgiXl^c,M.89}Q)   g\WfYY7gTZETֈ_(,t'*{5D΀dӯ,~#\;pl2%u[͟C^eDjpf#o1Y i\G,yKFu&äp{{q=վȋN|=U ڲU-{>wU͗j;/z7ۖj{Ŧ`@5뀺6;Ә5jWG~S`:fJ/R8%DT   B@TrFeݦ۝\sn wىE{p Rrvmlf!va2bڀ--[aFUи ѹa Ӥv8#Qcɓ顅J?vƫEniq|m`wcۯn>n͍&m Θh,jU=aiIcܻs~OŚ=0LCuzBȹa%H / N8y>y%|2p.VzJ6mjεLh&<[7UAMaM,*90Cx 舏OĉC0~Te( 0cQ0|6`<&6,v }K^;3L~y;}DETtNcLey{ڿHTQ)   1FTTFF1QQy~*)*۬EQ^g7!*uMSe,?TjP\ƤqB&Nnnz㢎qpa!.xy>.xs 9 ea !j4Y ¬ʈ 0`m˜g}FbFXW}4nİŧa]e)J`9HK߂Kc֌Xj#eр`>R7l%X,:Cj㰢 o= #oM…ݷpA]g .$ u݄ _] _Y چي?~HÀC Ea^FoӣæYCe ;m> u6# ?{s3sc6n{Z+[[>RQfG NBFku״@0';2}F<">: TW7`[j#tb48ص[ށzl=wQ߀ݜG:.l9{]}΍?*t?h]gt=x v)_FC 4&>{ش| ,-!T =nK2ˠsУ"+Ԡ$;s61%+0qjf* .?}/Z @I z"!͋ſ;Z>a|r/޲z||;h9viglsfoޅ\ǂiu™CD    '-*#*#GqVZeo@TMiGudIc`땨-12}E=hB?K'>ЃFIwb1n_Fo=.mxM͂sjI#ӱ.TaUϠ}t,%+bsJB!v]iCD    ut%nqnQ818Ql -"<%)I#רYQ-ǘ}ٕDegl bS?cݸzI(E b8~*Ť\8knO)uL&ZN΃wOHȱ -c~ :Dϑ1j`>|[\ (B^Z"^]S[A͹zTotj8gaIxNxk1&L赹(\Ó.aȡ>7=&/r3=h$iN 80nx0%_DQ8_"u|<|F%Bz? >F`icʃ\|GСjG8sAAAXĢn﨤YP8BLV CQuLm+66JzLz Mr*ź<;'7* 3p\7=MŵUMٸfAڷOB<8?=PPIaS&BR;>kdVYyFmвU ;wAbe,Fo`ZJ& 1 7R 1gw<~4kIm}w~}[0xysiw5o;{ B0l~ cbzff@ v dLݶQ; ;UIwܳ㺽B@˖pߐm+ UήJ!RAAAbFeTTGHJkTk\ʳ@TVƣƱW$*m9gQI۴ΤSk^YPV rn>Z-@yj U^i%?EڥmsXZt,$-.+kwZtgr&Iilld0"8YF%G4F>B9];"*AAA!֠uv(uxZT_2*)NQ#eXyEe|J Wo~ W"*Ub<[cHF#g(~1Qpx?وjqaڶuGnךXZehcOQ>:'YZ,I+zXqF$ %Jۊ =N6EP)]I;3^#\e:Z&u%;4V~ Te+V!Y: ʔv:4RaIӀ|j< BZ%t"*AAA!`Bl¢rJu*PCQrZ-Vu*cNTF%aYYqǍY`IY}< %*dʈ]f*Qy˨&-yFDUEu sls T[MбLcE#ruGM!F!@ S5,-l0\9y?o4AjZ#rѨ:'"^zUYZ'kU5EW Vڤc#yg%}E4F9ppQ)   }+Q.N7qUI8Hh4>uLHDݦSPEek^%*)Z~fTz+dVFde%)R%*sfeTTrV[DD    ͉i|J֕\kM  8I"k2uNCf3|6\ķSZ/B6K(6+TQ)1JAAAA1+*E& 8cʪ*Y)2"*!fQ)   1qD9FۣS#Nt7QYm귈BD ,"*AAA!8)8Hi<uQl263#2FHED    GN1d6eQٔUEMQ9JTrVBk͢rDTXCD    ')*Ȩl6Jj2R{Z/?ri?A}DD    5D]ST6CSkQl&jYQjOT*:MQ#DT B"RAAAbDjh1âDU#q)Qj "*!Q)   1qDF 8I1D|S "R"%Y?/ţ"r,)i%e.Sfe3l>\6N-¥ q|\:1Kp'.Nkr`A8].,pXAAADJK%)QiJT8i4 uo:Pd5JElUٔV zKO]:]L S9XDy,*YRrpKJQQYKP$FEJT&Ӛl苲'O&*(2+JMAAAB)y6LܲhdDT6S_l9u!bQy~\<·8ptZDiqATL)6sɌ֨K"kT^<Yrm.*+*Eet>uEm<8N|Xh nqЫ^:c||N:G*J}(^?\tSWڥ}.j~\9<TJ}:q.:Q.8Z>^onI}-TYpӘ:<VF$vnugɯ|,]PRcv:]ugt]'oӽqyA>'p:}o^t{OvT mc6] O7^_G]4'=gӣwC47ōt<d9l:/C+ozu}˺48\;<'2],94<:hɠ@Dѱn]`aS[_ GIt[zƛ g&Ņ7>R"ЩY^"Isq[.3q~\t .BϯD'K5nfn7]?R_+I.?3]z~{|Y44g.|tkzrbrL1eB"'a”DL0 Sb򔉘:i $LAbb|ĩ>1'ұS89D:o훚0uqO%N)aqt$LL>0m4jڥ}'c*N8@O'#OȟM&"atLNNΛDG1eRΧ'Lq鼉t- 82^g`)5!z?1qSzkdQoMԶ     JTZ(XT~(񐨬h,ks$Ɠq^l|h5*DeTqҢR;Q~QvqF_ dQdQ?t FÏLx9Hwwcm|'&IU۰!Շ49ы 3 .dr ra[Z2b Ϯ$JƏɛ5i=vq^_*5 |p!=3 Gv:I>dgS7|N/r\(LIFnfSҰ;i vlö])ؒL$SLSb5%'پywޭUIZAAAcɇ֨)*6泪D<,*YRm}=JTq89!RS;mQ~QpKϓ,w&SE+ݩXl?Y :a5u5Dž/EؒG'tE+דӇtGK } tk8{Utn<ı_'ëǸEf#gyKC' ~^S櫬D7 xH? =|~73 5=tHE+b5ؠ2 y G?Ypu8<4ҐF}?\Yniٮ$3\pҘ2i _KS磶hn_I*]CVMBD    %PTTN4u:߂EQQITE]^2:[E,+OJjvO dY֌߂v 焓12uF4|"'-/]Wŝח c c).j?w|w5]َy?/Nfag' ._NŪ ^#GctHi^/K.O Rtp6']W;鞆|~|ip8aGi7E9jӡ i>+ڛyʅcHWקDo;ISJf>ds'd2Euպ,&>.C{R};uUIZAAAMXT)k>-f^#EeDRΩz7J^6Yx&Fv17 *sSTL>^\Diݸ$\u#n\f\J\y)/GVKqW1j]Hd ÝLOnާw 2pw">~@GΆsWz?"|ғ޵>s'D߯1G3J-1Ч[/_ <${&;ě\eK»}"k i[wcǪY[k`:l*_9zc$O*.d^ '1˰Rz?uҼ9t}Ht,u'=ŒG6+͎"*OGAAAʆPHԽ}4EewmLFmR ʜ,}%&JbuZB\s.d.|. {LEq{kqA5m&4n':aVEdԮ/[Ф@:/7{?\ؽ=sƾ8 ܌틾“op'kp?カoY/ EPfNXT&#ghvvp5mdyH[B9jAftcoKF=`bԛC` G?=> +wQ_O{}a2i px6GlH_.ҝ;s!+{RWիl25/Xp{ZśĩJAAAA8K8EQ-Q)   11DSTi: q͎񭹀N4lUk?)ihqDXT:ҵ߭ x^8pxqGzzcܷF4^~%ڬ*u CxD+集_AN1ӹL[>k1xH?#x^p>-\Ya֏ۑ은OhN qoܞ]ر~<^n(9KphLJmE6q7:;?DSJ݀]3G໯⛏Fo^xnCEXl.\?UdUQ)   #DuHTNo;RTƱlʢraQ$%â+Qy'%*kHȓITz]>8 7wwu!;̦,}ICbis@ O[ǡO։ẙ{q]uSqD?nSrfri7bgS]^;ot_E׾0on}A7ὄXϳz{  7.<.7&mwvXk `w.~4^t θ nspͬa4=>pcv8Ԯ,BS|Nduy^?|}=yQ;~1lIB3ƹIGŰe;-͏{VNoN$2u>uO?(yEG}:wog{FF`d #A<xyز;ÃnOnO">DDOGygx!RAAAbSM&Q$nU'8D? Gn^473#5ns5Z(sSw=Zο./OW}ƓqwaZWg[ O NJDۋ"dfdHA? *t&#秱cW w&ڝpd#𻓐L7Í NBNZ2r89p{-It܎ nq#3%H%A8/ ylx}(U[~$'#tP{~w9tpAuHsR},&{t{8[3޳5*jAAA *7i>FKTF(8|+ No6Lm3 p3d7Rn$e!rH"KrȗLN™Nۮ<'~$Rkl=ؙÇlZ~dPN\<,E>+Nm;t_2S@:S WNdp/͡;st/3Yf!td#5',wtUtݜQoBD    M&**S֭->y,xp8cWZ9lddk^hp/θ͖_܈;]Ɖס贠+֤ýtx>3t~OHKdw&d2p#y~ڦB'wߗ W~/xAǜ͢ =S9pfD ,OZ/V~<[F7c )^Xs)L8|^,9('\ډ̌n=HzEF`72y,pt<:ÝL.8(nBdHw+AN:=-ӏ,^NLӕB,n'=pѵz3Ґw#Õ/gYR;n_dw:gG\UK3B^:6)Zt/֚)nt:ti_ S6NcoH]=FmJp9Hx 7/ K%bJ^wR3pv#E1h,n$SΦ6kHWחF}{5=G0p+[.R8Iɰt1CS۴m'O=w;ōD̘< S'cHL4}]:wAo#v鈶mۢah 6@x.Z`,JAAAA8[9uQl[@|٨WlsXTƷ^|vGmSEfdq2 K7{JzY@2r!Em9=IpӐAgt\:>?،7Ƙɟ__Z2NE,ԧ ~>7gWg8eLO"ҩ OLbdMl'RRIc~].kv9^d!HFj%끋pwDzE$QIE%g )w*A}fXil^%D龲TJҹOy"FK&ܪvSh4.WAזnn3,Nu`D=tAΝq}mѨAC\u5_2NVT}u(RAAA4GJXt$ncDe4r1EQy:JVή?ĢrrDT^:1cxD*:WSR`V3XRVC^>ܝ /23Y(W{gO,[ >CΝ^{ 48aqG[[ 6ֱ+DT   BPMTҏM۵ʦ >%#~h1߇D2'/* p<\%d <{D%g"xt=\;nw2u#"Dv9B7J2QQi^.   /ɈtӉT>,*Qyꢒ~'҄ %*Òiudxz)+}N$~!Er}pЫ׎f́ϓGIV,&N'ΧJNrAAAĢcPJT:FOdBqUҴóΡx° l :iGnvH$ 3'tAAALqUkϨQyrr.I%|JT^2΍KFGӉ5*[-Y[_rgT^2_-2RKJɂ%DUQ,ugtQZvڡzcgP,yO)mtE7D;9ՠ}m5+N| a|rr#IDATaVKaOeܟFP bm:&A٦yv-c;<^1AAAA~i"آr4>uO@]݄*YTN;$*<5 hDDe6KDT٥lv .u,QY<{|UF4tJ%SΌT}[,)!QkE]Wb9aOg"` hzCh,kX#qLp@rJdّͽBGۀ$ L㬈bMɥz0gGR6kcySvʩ_%Qm+1iXFPR{C% ?v9pڣ\Jr>XLbccQcz26Â̾ecDlr6'Yˠk-N؍|w^`*c!XgVw!McQj~ڦg밳=}_E{Ϗ%b2Ǥ>ZS}jz3JAAAA5t{r 6xHTm2uFE%W>-Cf1"*GUʵgXTrZK'ˢGW$QwEkZ%:^3va *`Q<Fo` @G*١$>Xq&%5)12m> TIJYNc-qC㱲\szlCؑ,G0djG`=)[t5pP Q3,5* Qcjaq:!}E7gn*Qi4o' &gr"KGS&JzYta$ax!QI}s j"[> =4v1 qڵ4;" ΛOk/G='*9T17c+aa!ddZ; XJ0]np)O=t*h _   k٫(P;BTNVnNuQyXVV[ZYYxEVsNNsSӼ1F*{m珄c,}FUQIR"Ҵ" TekZ,+2^Xa !GdMHy,,JM1LBYbf#dy<g.& rsc>ch*J 4 pI /a[A%TYURTX8GͿL4n jI:fHA:2aR?d%=9ҤXj<@ ?c}iVDJڛ0K޳2#HOF=cYA??ǣ`>."xaTRԆ숮/ ,>O;y4P?,nClxuY 3 _PrT4d#p_JtҔ(-sPGrtoa{{*`N ]{39mj.݃}Ey-GX0/h-6xH^יTR/ixv{ֺJQNZ6EsQWB*,+7kڢϐUزT>-wn:Lz8 4zhܡRd"K;҄UYr=(*ڏB7ʥp<'mAAAXN?%Q-#TQMT_T)SQ%+Kqٜâ>O.*TU;RhChRNXuuf9o|x,=S.i&Ιp Sa-O!x`5Fmуg:m\% by?o \qx`=o.>eNvL/:>^-5%F8i(>C1¬C#b8sq z7\t ѯ`a '}Wݍ ?S< dѽ0PY'n>m(+__<Mo{/u]4{ӆ?.-rR2m/ <ոK'ƣtՄs*@{*:իK7u "&E:Q?ˆ}tkǷVc~%>?5\sEh]9Ǻw_bÚe2y4i>i`om(f!'vW}6Cūnth<ݜŠѰ`l޸ ۶+sU)S<}#0kFitz;y-~o^{t6oxC? .<}Eú4[ =7PX>7]-Ĵe۰jx4j/Ι63pE.[4AcLd,Bx9lZ1{/KVk+@v-wߍC`m75<٬y{PٟsѳE|>t.[Fƒ^og_H?@D    ''*tXT7fHE\y*MJc9eUjcJ.KkTr枒j(p!R;Sw <45Zl ټh:yI(ݎlB$mu I ?kDjHpR^_zu+5ÁgF?݃.+wFx= J*DX/? ]ni`s.>78K܍q .Â϶jE(܊5-K+TaӋu'J5腋Koׇc^,c z:^xAyC <@O$Dt˭96fU`wqqO^K4C>+c!՘ڻ#|:B:}a٪nҵD+ga̲ -_# ._WŗK{jlɡ{6/ 46dNWO?cnFr7]ƭ݅wZp|xs ɉ}T)\`'".9ԳtQ)   l6ͦ!*Q9Kʺn˹U CFmlj7QY#Rԩ&*qg^TRƣAC0i,xyJ~?mG<;5BU΍u7M̆Gq[|`-X 1aH,JhÚQP26W=z0iaӁM3D+Ő, BݘToL1\1{54zq)F=:>ڶ]_gcȍ}UcVU"Yf`hn1 wq_؀0l莝vekxѱ;xG=15 /m~7!u `OVq Dwе{wNG?½n8 ہ5{cB6,V9]weDk{)z^z[-Bc>.[|ZԿl0&|m Xi/V,^x~*\t݁7|L,;wi&F[|4` ݍpB|V3~3 pGd]UDT   BQCTDp>ZFeuQ9+RLz#~QY]E2*W<}ʨH,% Fe.E#Si}r~Y_bCpU0mې# >ߌfn';`^X>.): ye;,NxvAJZ. ma薦 povaOcXaX.~|^վ4U'XFY6.ƄOGa07ԦNpњe?E)KPiK?GW1˕/c_oCx1F~$e!_sbwb{+㝿ދ \HǮ_[:>WISK=l;oxv@yjO:FfPUҹe!l/@+ç6`vJH]\F"O !..ob r`yBmb=o1DU ^~?/.=J0;9kv@r`)JTt%Hv8HKǺϟ{bfFlI2*Q:ڈo|O9Ȟmcm;Ǣk/v •Ko锵VQ_9oͺOü[@&2P蜂j1fnH_ݧ M( E2MawkZ:p)ʻńY 3: \e[vc׏1)dW7^sz \> o|;^M1nm_s  /w>_÷h'?23:]}mǮ\=vQ)   J}vJ6QviҼEC2̱7½!;9E1,56*jyf͸|-2 }?`70;ؾs-*!,vK,.c= E6zk@(o}re6 _4ÆD^Ŋ: B*ڬ{0rUX)#6w<-ЯFq91kc2/ڌoWs}hrcHB|i ,U7(TVߦ!hsх8k, ,63\}E={+5 JU)6:t.*a[7c~֒_uߣA^{{xx%݁z$SsŸsx`KP73Qx ?_*sI(?/1JAAAA5DTN,)JUHQYjkTƨdaHJ7h &xI:cEwniZb} cl84 ,7Oq=ѵחݭ#&z䥰8L݂nbfȂ4|,Te0r /{tol> W}<+u`QK)qϕUҦ0+ە)ǛBA "+Ãet&Y J*PE+#$TFT<))CeRU/{I˓w7\L1.ˇ'/Y؛,) `{Rȡ~Uܰk3=|~=.ˁB[PG"RfJ6LkqN1jt}O{7'V ƸQ)   >j5D媘W.EjtPpS-llS^moE7^@-q㖩W}oo}O|)b͌yawL=,7yy<ҡI\44ZơYjWa 080Ps^m؝J@}xx)C|6CgYLâd{[Կ^ɂˆfi 4 m8mӤ1И5O:|cB<51aTv94Nk\R`ijQo F>^xn;;:>D߷NM#Dat.eAu=:etC""*AAA!`#ZD8N *Wc]T2ܿ M -Ed`h=N̻7Ͻ͹'noܜp3>^6 cٌo(9z`팷1el/lټZJΦYJ rl=2\%,zopCkBݾsz>^ Ǿ=+ԅW7w4o0LYF&Fj3& jEfhl,.YUt> *,hܚN.& CX^dj9]t0ۃ5" <ڢK,+}~_}19Gt톺_,|̪شd%h^i/ ͌H>1Jt>ASxJ:4R]>_qܦUF_F8HQF$K0ŗR=g)254i'JvhuO #Nפ,>7OfAi}RcSX>눶Ԙ<&.z %AAAXHQɰ`k8 un:M&༦PdUrѢEe (Q,F|Eo ʓ=mGE!Y+' !kˠ$),8fS)T |AAAXQDTWCT5A1b6ZpE] Uj*IyDeLoTT k *s_ޫ.Ym(xN AAAXNs&ͦLm>*N9ےelq-UDo'5/XLDo^Zѐ+c;0BV8Q)   ˣʪ5*m<6I@Ee\=*fu4IY'F/QQYc5ZVê*cG`+^ ψJAAAA5jJ;=ZLAgUFEyQQU~G3*ywuQQODoGTF?lTm .vsi5+HsY?    k)*QQYU %9 ˝ ,͆ipYRyQ)   F-R;RTm<q92"*4UtZF|KU-#kT_:*XT֯!*/_Y%*j{cZTV%C K1j/ˢǬDI~Pf/2>F(Н^?EKn@A(󽇢7> Àpɝ>-4%A= ꡥcmz5s2&kS;<.jBDD    '+JGP ?YTF"c*+0m`ڥUWʕ8u%x{aKC `B-AAAXĢר<Ʉ*~s)*OP;E%HzHB=.= +'Mha`DX%.h=á).|LQn1LXfid\ ֮Ɗ5?`Xj ֬Y`Xz֬\kW`X>_k賕kb_.ÄAT4tf^T#lY 3Dv! %Q)   ƉEemUFEeY)N9rʬ< De8"O&j=8D%Ǥ̸賶?Zk{߈^k']s6˯=ncnng/ G/J2GƇQ^A(o>5i6)/͂2 ð_NcXKqU`d6tzAJAAAA5N^T$5MD<{Ѣ5e|ί`g;چ:6 YvsI}hܾz,ÊJKAj6gG3ʥ)tA}kJzu#Dc P-%+2U DD    &*7%eTTƵ\@tDTzD'qۤFtA+؞~؄-ڍ]wpoGp6oA.zʹ r4DQwmy=J@ !ιK 0y=K݂j-MSԎ#.J@D    ?MT5?RTƫB:sU︖8x1'sLmQ >g!łTr~ϯG̽l\:Nͩ߼&%1i5&D|lTG5c"m:=pa:_4z-ch?Se%ɺ]a>Υ镻~ tE ՘Գ"RAAAb%ߊJU[eTQyF~Uڎ#sN|gUVE}%,+qymc>%*/YQyrOC23z,ȃHHs!RAAAb$*ωJQ,)^LtCD# * lޯPeRRHngFD    JfQ}EeMx!?GljhTMV ωJAAAA5NөbZLQ5dY;+t"tiBO~NDT   BqDe`Y9q-Nw1CDϑ9&YɢTD \AAAX ETD% a3'5(URU+)*`[a\ ωJAAAA5N>[V@<Qyre@3uu%! ~Li; c)RyQ)   ?CDόmB/FCy:A`^:]aM~E ?/"*AAA!9Do7BHԟ<5Q9)DeD+W;h\<63Rk(M{eWP2J_x%8H<+3w8t/*X0U @}fGz9h36#eKѝJyw[j_=Bl#RAAAb9l8vQ٬u)SeTƷZ|~-De\'+* q \ǺbBT1aðM|עѾJH-w(}`{Xi;"o}c)F,x8(5uX0իr(`ZAu4qHƥi387gl ſtLЏR6ӃKNuҦtMm1qC4ڶQ|oXFD    w:^CTpN8Hs8x"m2 5:Mn|[A=%')QɯXTr6̨<[W(*>`:e*%ǂ3*){TX&3  m/Ѱ*!*- a#lr]IAڄi~zhC{y9ԄfaԤAp{\tmX(t¤jLCAkGh@%*F> JڦM#l+GD    _TQ)W *YpLa9Іng}?Q*Ga,ʂ"tMK}, )LfHmae L?aуH0Bi| [f?ԾI*I-+iuYl3ZYVjVHe,A 6ΫTgV% eᄈAAAX#j pn8Ik:MPL;l)SuҦ`ÏQIoBva x2ӡ]F.N1,*wF{gU{; ̼wŞwztLw;*CvfD@#YL-HUE9 "DrN>(K_xjZk~be3b5+>ǔ1qF)LSairLS`ʌ(Nԩ0m$L>3Mzrd9?}tVυL1u L6eI]r~5}?Cb|rҦf`4]!5_7/_.N (* !B!qrQyanĢRL#sT槊J NPC4*l[Z1=3B{zOD"̤ v} ' QĔ>c(*upjm(((зM(* !B!q Q8\[ӹ0GEt#*u1GEehɾn)*ssTja6ro5:Y3-J\0}?~X>l{.vv3v-RUZ >?V] oJB!B!$j.*"*u~GVXdFwWTNJu˖fطE t-]Q }!=`wt®E ~hw]I%]V] oJB!B!$j@TXQsTT窨v}ʱQfCCeP/mxa\:\#-=ƇEWܝ]`GgJU`ti?%7*+AQAQI!B!DIQ; D%WnJ&aK;tط9x|:<#Xw)_1MѴO>Z+7NK 3*W -7U}WۃB!B'j5R)ʟ~VQidDRVLTE#a㨊J߅Y'"g.͸v ~[^'f6GnKGqTR;mol}+l_xl<ǁ !]ґ=*KAQAQI!B!DcE;8Q1C}ʌ> NJʱDfIDEGDHJϳSq߇R:ΜaŹxreoSs:^w kįq|\6;>5^~',\%⻧umX^ !pU}ۃB!BMz =먨3UV"*8> NfUhV ˓gTxxE>,9q50qxvͳDɶOf)Ʋ0ioz9W-ϧhIs>@c_΅e-9(O ~{plm|1.V~W^ykE߾}M9B!BH8 Qy$(*s#=U30QSGm};GDF(+UT6(FQuQ)C:q#mQbq뙃gfa޾uFlۋ]otدpӌKo5Dg-{l~{_gkXTm /pmP Mwk*̎6B!BH89*eŅ**áG,42#qdT&&kFmP9)璲 Р *P= ϑi|WOU=5 g#Vg=E_"1v~`n~ e3w( bl.\sZE[6{B0*xJ{fGFJB!B!$j|=Q\;#XQ+Q0in%0YgkJiD8ckSy؇>e"OUbI?F*tj'Ky~Utp:]N<ԣHrܒcpm)υ#{.&"m]džkIxc$PTB!B!QDQ;\b)L-S.Z%}7NJ(*zu{q8& QťBُq}wS_Goopشػ=\5j}g_> o&sޞ+k:Uj8t}We-[ f:qi%̈́=I97ǵr@ &}GNkjdB<:ik}rH3pʌaFV.Lɦ,2QYr\IX9瓟)* 4B!BH8רɪiUS2*LdTPR֥JT *k^W|3E%!B!B'*52u̜;By#H#)뾨NMD瓟)* 4B!BHz2#g2sH 3? yaL .+CDB:~sg[TRQ騨Oa(z̗DfPTB!B!QXQiNWQyk\аkBT]L'C}a<ͨW:+dcIW2.Y]v/׻⎴6.mL?@9/ǒ{,9>lǓ}èG q ͑I݁k+P-mM-‘QmJOm#|yg E"E%!B!5=**@>HĪ߹CdU&VN̂ȼB26Og:p\7GK ~$I],xfh5s<:ţ;cs|%n W9vF\Uhd[rvҐ)QBe}* 5sR_6l,pW;T ejEm\64S]fW-Iհ*Kʵ*k\cvF_ڲoބ"B!B(*yiQ醢RMUk2*[^7WxxsI"n6K|^-h>}qTH:ōYG`CpT&B2&,7%?^ 63wxty!h^6a5˲,T˓UJǶ<*^eǓ ~wnK~s;Ze7Y%k :@+K]FcCi&owYe* '(Gu(* !B!qR~YL'wWE+IC TTZRp}>G[[,4Xxah=AyZHy;q$-EncDpT.*[ҁQ.^~n7u:t&.4 -;c̞X(mm gwceX83lS)VqlȐichת'wP.V7Мj#W-9꜔rf< */1׻x}N+;f 0Eq˰`mò˱|.Z5 6B!BH8 Qٸ.lt1R3*uwFZD%3*뎨46JC3=F>9ZWxe>n-ē}<_fk&{ak&;(䣠Õ+pipfnzcd6jԬ/07lS1T{h;;u6 Pn쭨 +0t1V`yu~)&A1fL%uԫ]Ŭ^ofto6?8=w۾{6wYWy0}W=}*]cqw-_S렪>B!BH8gDerIEeJ;g+j NJ]4XQge xpZC;mxvIBRn:yޫK\=.+&:ț!Cr\R!VVeB||tm~#niz).o<.L8to`c%~-/}}a흀<_.i|cUhDqy5N3/<~=fur}TB!Bg *s 3xQQ YyIg#jLJ/ ?75ڀtp6]^y9)] ~XWsp=gTIR/L%jvFZ]躺_ދ<{nX4z 3܉="*%8Pn p1y]a\?RgLP~hCQI!B!D(*52oQQW,JQ02ʢPH;=&kfg{GV~dLq$Opqy=dks5CV ~\@yPU\W%՘[x*) 2*_F?ߋz}7QHN\G\Wl]0m%k<ai>3gK t7CDZKCuxa4SGΐߋQqtl9Ta}TaIvXq`f\N+2C}0~%eO3?zC+0ǛxQKR&`dԵ3|Yaxqv`Q_bV叡W8zqܙWVbkoݏ>'^;wENSO1uV-aIyouⰽkh(H$B!B4YNJ̜L I\H'7EeEeCR3+ !\\:f :+icؼ}'ڱv/cŘx6ڇMc(6Yfjʎ/׹4t6ߓ2[qw`a*TN S톾]J0,.V",Cf/bȄ%PG'>-w3݉koZR%vڅ`ͽ=X:q$>zfD:G\T>/>?^+ntK3> URӯ2yuuՒBQI!B!DYkFTTEȼB%8ԿR2ڢҬ-jJnTT[UVcP|:=CgT)~c{4,7m1C-)ʍ' 1ғNgsnLɾk#|TUy^xُ *3RYFnZP37aK 9fĨԣr1!zܫSR\/!}U P-Q')#=~3jDJB!B!$jybQ3t(*SP b8AnuYJC]\F6qs >HReFʥxALjUZzW󈘴K[[_ԥj i%׫TՅr`AmJ[rܒ6]-7I=RfX|h3ے*R!j/B<*ZuBRLH6N?$PTB!B!QCN׿O"*ӛDZVSJ]DXQ96 QmQD#UB7,;UcW{jX+jXR[r|VZ/Vkr&ۺ wJ JM T/꜑Rv:uFn:\b& lXIu+eT!. /5US% =&O!Q8-_+fs\lx.1URB^2ҼuϷ[W)1oy}ͼԯIH$B!BBTPL-#;ꨬ4THDj9';QE+ŪQqZXZM%0xafR2UBO1d쏚c`_2SE(*+LGt4Kǫ}, ťkR.լGOũxT1F Y*K+=r>.j9:d=S P-urvCȃkw5eRW=_{~O VkFJa9I$B!B=%*3 #gR#rGIN"w(+QtfUptkzLbƭXl/[Vcgx:]>_#VkP_Wlļ1W_K?߀@ee)L=pl:dԫ1v4[ 6=rwّ*W%R lغ9-@3=džU~iYG7R.0$o{1خ-krԥsXDwIy#vISL $B!BLTf'*YaFe8?8F}JñUl$3.a G58*2: ڈFFe0;RJuLRfXJE^ktOR奛rf i}rIfPHYN~N,/ QnѫuI2/B[`洔Ky^3>9iΒݰ3"RTB!B! uBzRT Qy[?\ب5^̨Q3s!*S33OwΣ.JBPTB!B!Q#ET:7v19*SEtɦ,N 9*x j5yPTR$B!B]XWT Hޓeՠ$NAQI!B!Do *sGH"3oh&*uA.*SϥuV:E%!B!5N-*DZ2wJQ=**S~oQyܹ坵$NAQI!B!D=ִ2/UTSIp1.*/Vx0׆2y숨)VPTR$B!BͨqRQ_|DDԑ';_[gIQIHd$B!BƙʡGD稌Lnk^S[gRQTY(* !B!qRy*3TVJ<ʉrm2Rnk+L#YG1JB" E%!B!5nVЕO *TVL$Rd ;2YdRl0.J _Cz =-lzm#%C/&BQI!B!D ;]^WQ)**o vEFݏʴ5ك"*y% ͦ,(' wujoHT +!D?c&%C~xzӭI% )*It$B!BF5De#r@BT9.2GDJdFST֐4PT6H#UTEkʾ|.{GucaRͥt! Ҷ5avEB!BCڢ29;gH=v*Q9, #*AG}{:Os)U넪?CWQY4RΫԎYHJKB3B!BH)EeG~3GeRT挤2!+k7Kl E**w&fQ%y&|vjRI)?_/T <B B!BHԐ-sCQy$jʋ%TVVAQ D~x^xhJ$^RD89 J| ک o;CB!BΞ̧<')R穌<"bRť1T>S;J'J2gGv6B7B!BHPtTC)*SJT&EuF<#"**hVRڣMܪurA%n26|IB!B)*[tTR&Eeʂ:.*R˱ʋ5FQpJٗx➏C1 ;c(]܆uqLjzhfK=@B!Bg**R%e^lkʂ)*6.&5Nt}J1j4[%ʢoͨyTQOEj<1܉J-UgջH֤Ash󰳪Š1]xd6^/a}˰"G:TK2Tzzf(Uq=K9ʝ*<2CٛY=Z욶J:t̹a r^meSuMr8,SRmS(_/pCTuJB!B!$jqAGE/k]$֌Jm[cu1>FmNUQZƸ*4[šha\4\rQQ7{gقic(!]%uctNB'bgUۏKO'MB b >ۺ嫬:xUZzը8AoÕl#?nazm}G{d_ťҳOGmK嚄'9Rx\'gv5|l=7nK}G HAIUhY']]\Jvm1)V)K{:m(* !B!qQy[_\بlxQ2Gef~8GeErXb,2겨K: JUtK;@sCr1 2i[lFfWn}mc:AɃ4" ťIWVVVȳr`I\ ֲ>mnVg=WʊC(;Tj+RF@4*Sj\ oPL"[$ZkQߤb&͚ԡ,V6}y[#r|ٷl6 ,uUɩeG*t>vmƳ0ko\J մDJB!B!$jIDezHkY}UGu@TFyuDT/`K'P ś9*#t @:4\XoCWlEϥq~<;s+UOXfmُC ۫ʥé`ThwOJT9(g|LE -A] Qq&"<8~gljOxtpСەAX#\n0Icr5:O3.js0#x(٬&Tȳ/\C"?JB!B!$JMQY[F8S^] 4і=DY:|Lq2ЃX{8&-dzB%̝ iJo.n:u*şヱ hW5F-{cZ491GP!߃o ~\bt;3_[m(sZR˅I)*X%b؍m{5B˱-k9/u 9ü}[TKyh:["T\}WYٕr--3,Ofb1-dxZՒCQI!B!DiYj/SPF~߹ZLFuuDTyaҚ)ai>[A3aЬe6s _A~3z<0`>n67}47tk+; uzCiBGe`mĽjRw=hxk7l?K:;=vOB~k>鄎v;9Xyg\Um~mzOEq89 ]-lX1f3lWC}}5 ow^%‘v/栴@ ƻ1wD|ڼ5^m E+qsLxCt=vN\U(* !B!qzJ3GQQY?wD!2F"3w(2][y5#ym]yUPU}O>zSm =]&Oⶏ&Sp[CNSpI8\qEN~#y׶y7?k70߯4l;t>~FN.ȷ{UfES=Pt0Wcix;pͷΫVm‡lnumw_S9pw?܍t 1`یWp]",j=NNO_u;nvuS#Ptan7q4=Ow1+OfeXZȽ w> 6 -,B!BH8Nzvr1dT^J Jks<RQ^h1f%\< }]y,T; \DY>=엺1q=vj -7l3;ۣ<'C6 cOwcNm;Ł2bkcKt6h=E&^.,B!BH8Q9C2=9GbZI-̵uDTZj*U!ju=}\7FT+|ml<2Ńs|9Lh\o 152 8~D.㖣*w^o7swKl/GbRx鉁z6>F~w݂+NGRcfau !7Ze(}d`4{ھZ+'u}g+**3I GlͬLmb 2r%")*53ʫ-:"*u][WVAiVb)[:xuƻZ]Do.P^^uuqDs5 ,NEg} 쁒MeSaƺXf5~%6Zݻ2|0l)V]0~=dL<{k(2>苏۽zk:b29oZ?; 1}1xzN߁/߸{W!fWoI~̯ѯKw;w}C$B!BFfŭ}˻^PV&3*C;LBW֌Eet3*5j o_GD%|mIH: lxd[॥^\ᙅ.Z-<wMsq$OpW~"r{^xb]R-H'3BTÔgqT3ڿWc9YcqxpE V]0zNl /Ьl|.x{8O!%%Waʉfv _m^ 7VMF^Ð 6|0=Eh]xw ,;s{uFKGl[B j(Z]|)^|n=Vl!o4CM娲5x5o6wĐkpD0m\5AxMK]4I(y+Mf J'p51e%GNeF cFjrav߬>` ,rae(EgD#>_;ޜVY> /<xw0 W;^Cъ[_KK׻cAyNOlcOŘaw-ϰkߋ=0e_9,ۗ{Œ0xძ0x큘zi'XG.A;B!BH8QiY}?~g+*u廊 )F_P,@QeQ /4`G)-؎eDe˞pwYxp67v'T  ۉ+DMex.47`5sg[TEv6|NXefq3zJL;, /.6&TK[uls,R+;J=|Lv/*JS725.R'Q-WZ-m˃f",C٢r[qM N }B!BH8tғib:9**3ߡGʔLH:_=)_qH}/_߿ny/n|?~/|iyem}e0F㶃qߺhFJkz*]DGϫbfqW%o˔tfS)mRN̜>ǫ$l]HoR\t5M*5*9ow-ZܠBTtNN}fy*i!1cZVR] R>W娐Î*\uU`9)8LPTB!B!QDezaf QYCR1QJ&.CC³Tǰ{*e[QU%Er.쩈cOy81tPzT(5P{逞*F͂`, )Y-ѩmθ\+f1r_\|FO嫫B2&[qy6ˎʶ-1Ӟ@c`'jϓ;4Ӗ<9sgRHJ'Js)4[Y*R#AQI!B!D3ɌoaEQ\<QJ'IhVKf+[K]ضL1SfI[8Jx ̜A?BNMqBRJJ)ەlUAr x\:hJb)sg0p7'-մKGʌk֤TkPqmiÑP-gT !, K-@, 9C}yx_E< (š zW"+%V߄n3UIه+UZT&Q3#W3-Fo-3?VA35cRvrBPe=:S\)W{X&KRفs-ff2kNtta!9rO(-JQn5R}Mr9{F5$B!BHk礢RQQ+~R"H⼈JGym QYјR D)_7:xsE%!B!5N&*=d5(CQ)~ArҺN^"RGDJ+!+cx\..ѬEECCG6RTr$B!BFv1 7鉴H sTfAz0#)=(?(PEeRDFޣR6QyNȃ| PTB!B!Q#ET?wz-2]Ee#*ub::Geznw=2ED~0R$R'EFVRTr$B!BԌʌQipʤ/:Hd>ʹ#U: ǖaHJ]QG~7H]LGsT%E%!gJB!B!$j.*u[_\а+4Q"*&~G,UH##"**ucDJƯOԗ#fo3? ׬NRT#wG9c(* !B!qjQSgCFr 2*JH-BZ^1ցJ9f5 E%!B!5jQy42SEenb!(Qy+2Yc|$B!BFLY/6솴=Pi5b:**3UT"3o+*CuwEFԠ$E%!B!5N *oq'&ƽQI_5鏴Haf2*Q$E%!B!5N&*;˻BMjLQ3iy͐s**L< Qkٓj-(* !B!qrQfT@ڑ=ȬmfU&D8WrRkGͲYcʧ$$jPTB!B!QRԡfb:Y.2\PG(kbdDUTJ*VnJBE%!B!5A [!CAUTION] > Distribution packages may be of an older release when compared to the latest release that is [available](https://github.com/abraunegg/onedrive/releases). If any package version indicator below is 'red' for your distribution, it is recommended that you build from source. Do not install the software from the available distribution package. If a package is out of date, please contact the package maintainer for resolution. | Distribution | Package Name & Package Link |   PKG_Version   |  i686  | x86_64 | ARMHF | AARCH64 | Extra Details | |---------------------------------|----------------------------------------------------------------------------------------------------------|:---------------:|:----:|:------:|:-----:|:-------:|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Alpine Linux | [onedrive](https://pkgs.alpinelinux.org/packages?name=onedrive&branch=edge) |Alpine Linux Edge package|❌|✔|❌|✔ | | | Arch Linux

Manjaro Linux | [onedrive-abraunegg](https://aur.archlinux.org/packages/onedrive-abraunegg/) |AUR package|✔|✔|✔|✔ | Install via: `pamac build onedrive-abraunegg` from the Arch Linux User Repository (AUR)

**Note:** You must first install 'base-devel' as this is a pre-requisite for using the AUR

**Note:** If asked regarding a provider for 'd-runtime' and 'd-compiler', select 'liblphobos' and 'ldc'

**Note:** System must have at least 1GB of memory & 1GB swap space | CentOS 8 | [onedrive](https://koji.fedoraproject.org/koji/packageinfo?packageID=26044) |CentOS 8 package|❌|✔|❌|✔| **Note:** You must install the EPEL Repository first | | CentOS 9 | [onedrive](https://koji.fedoraproject.org/koji/packageinfo?packageID=26044) |CentOS 9 package|❌|✔|❌|✔| **Note:** You must install the EPEL Repository first | | Debian 11 | [onedrive](https://packages.debian.org/bullseye/source/onedrive) |Debian 11 package|✔|✔|✔|✔| **Note:** Do not install from Debian Package Repositories as the package is obsolete and is not supported

For a supported application version, it is recommended that for Debian 11 that you install from OpenSuSE Build Service using the Debian Package Install [Instructions](ubuntu-package-install.md) | | Debian 12 | [onedrive](https://packages.debian.org/bookworm/source/onedrive) |Debian 12 package|✔|✔|✔|✔| **Note:** Do not install from Debian Package Repositories as the package is obsolete and is not supported

For a supported application version, it is recommended that for Debian 12 that you install from OpenSuSE Build Service using the Debian Package Install [Instructions](ubuntu-package-install.md) | | Debian Sid | [onedrive](https://packages.debian.org/sid/onedrive) |Debian Sid package|✔|✔|✔|✔| | | Fedora | [onedrive](https://koji.fedoraproject.org/koji/packageinfo?packageID=26044) |Fedora Rawhide package|✔|✔|✔|✔| | | FreeBSD | [onedrive](https://www.freshports.org/net/onedrive) |FreeBSD package|❌|✔|❌|❌| | | Gentoo | [onedrive](https://gpo.zugaina.org/net-misc/onedrive) | No API Available |✔|✔|❌|❌| | | Homebrew | [onedrive](https://formulae.brew.sh/formula/onedrive) |Homebrew package |❌|✔|❌|❌| | | Linux Mint 20.x | [onedrive](https://community.linuxmint.com/software/view/onedrive) |Ubuntu 20.04 package |❌|✔|✔|✔| **Note:** Do not install from Linux Mint Repositories as the package is obsolete and is not supported

For a supported application version, it is recommended that for Linux Mint that you install from OpenSuSE Build Service using the Ubuntu Package Install [Instructions](ubuntu-package-install.md) | | Linux Mint 21.x | [onedrive](https://community.linuxmint.com/software/view/onedrive) |Ubuntu 22.04 package |❌|✔|✔|✔| **Note:** Do not install from Linux Mint Repositories as the package is obsolete and is not supported

For a supported application version, it is recommended that for Linux Mint that you install from OpenSuSE Build Service using the Ubuntu Package Install [Instructions](ubuntu-package-install.md) | | Linux Mint 22.x | [onedrive](https://community.linuxmint.com/software/view/onedrive) |Ubuntu 24.04 package |❌|✔|✔|✔| **Note:** Do not install from Linux Mint Repositories as the package is obsolete and is not supported

For a supported application version, it is recommended that for Linux Mint that you install from OpenSuSE Build Service using the Ubuntu Package Install [Instructions](ubuntu-package-install.md) | | NixOS | [onedrive](https://search.nixos.org/packages?channel=20.09&from=0&size=50&sort=relevance&query=onedrive) |nixpkgs unstable package|❌|✔|❌|❌| Use package `onedrive` either by adding it to `configuration.nix` or by using the command `nix-env -iA .onedrive`. This does not install a service. To install a service, use unstable channel (will stabilize in 20.09) and add `services.onedrive.enable=true` in `configuration.nix`. You can also add a custom package using the `services.onedrive.package` option (recommended since package lags upstream). Enabling the service installs a default package too (based on the channel). You can also add multiple onedrive accounts trivially, see [documentation](https://github.com/NixOS/nixpkgs/pull/77734#issuecomment-575874225). | | OpenSuSE | [onedrive](https://software.opensuse.org/package/onedrive) |openSUSE Tumbleweed package|✔|✔|❌|❌| | | OpenSuSE Build Service | [onedrive](https://build.opensuse.org/package/show/home:npreining:debian-ubuntu-onedrive/onedrive) | No API Available |✔|✔|✔|✔| Package Build Service for Debian and Ubuntu | | Raspbian | [onedrive](https://archive.raspbian.org/raspbian/pool/main/o/onedrive/) |Raspbian Stable package |❌|❌|✔|✔| **Note:** Do not install from Raspbian Package Repositories as the package is obsolete and is not supported

For a supported application version, it is recommended that for Raspbian that you install from OpenSuSE Build Service using the Debian Package Install [Instructions](ubuntu-package-install.md) | | Slackware | [onedrive](https://slackbuilds.org/result/?search=onedrive&sv=) |SlackBuilds package|✔|✔|❌|❌| | | Solus | [onedrive](https://packages.getsol.us/shannon/o/onedrive/?sort=time&order=desc) |Solus package|❌|✔|❌|❌| | | Ubuntu 20.04 LTS | [onedrive](https://packages.ubuntu.com/focal/onedrive) |Ubuntu 20.04 package |❌|✔|✔|✔| **Note:** Do not install from Ubuntu Universe as the package is obsolete and is not supported

For a supported application version, it is recommended that for Ubuntu that you install from OpenSuSE Build Service using the Ubuntu Package Install [Instructions](ubuntu-package-install.md) | | Ubuntu 22.04 LTS | [onedrive](https://packages.ubuntu.com/jammy/onedrive) |Ubuntu 22.04 package |❌|✔|✔|✔| **Note:** Do not install from Ubuntu Universe as the package is obsolete and is not supported

For a supported application version, it is recommended that for Ubuntu that you install from OpenSuSE Build Service using the Ubuntu Package Install [Instructions](ubuntu-package-install.md) | | Ubuntu 24.04 LTS | [onedrive](https://packages.ubuntu.com/noble/onedrive) |Ubuntu 24.04 package |❌|✔|✔|✔| **Note:** Do not install from Ubuntu Universe as the package is obsolete and is not supported

For a supported application version, it is recommended that for Ubuntu that you install from OpenSuSE Build Service using the Ubuntu Package Install [Instructions](ubuntu-package-install.md) | | Void Linux | [onedrive](https://voidlinux.org/packages/?arch=x86_64&q=onedrive) |Void Linux x86_64 package|✔|✔|❌|❌| | ## Building from Source - High Level Requirements * For successful compilation of this application, it's crucial that the build environment is equipped with a minimum of 1GB of memory and an additional 1GB of swap space. * Install the required distribution package dependencies covering the required development tools and development libraries for curl and sqlite * Install the [Digital Mars D Compiler (DMD)](https://dlang.org/download.html) or [LDC – the LLVM-based D Compiler](https://github.com/ldc-developers/ldc) > [!IMPORTANT] > To compile this application successfully, it is essential to use either DMD version **2.088.0** or higher, or LDC version **1.18.0** or higher. Ensuring compatibility and optimal performance necessitates the use of these specific versions or their more recent updates. ### Example for installing DMD Compiler ```text curl -fsS https://dlang.org/install.sh | bash -s dmd ``` ### Example for installing LDC Compiler ```text curl -fsS https://dlang.org/install.sh | bash -s ldc ``` ## Distribution Package Dependencies ### Dependencies: Arch Linux & Manjaro Linux ```text sudo pacman -S git make pkg-config curl sqlite ldc ``` For GUI notifications the following is also necessary: ```text sudo pacman -S libnotify ``` ### Dependencies: CentOS 6.x / RHEL 6.x CentOS 6.x and RHEL 6.x reached End of Life status on November 30th 2020 and is no longer supported. ### Dependencies: CentOS 7.x / RHEL 7.x CentOS 7.x and RHEL 7.x reached End of Life status on June 30th 2024 and is no longer supported. ### Dependencies: Fedora > Version 18 / CentOS 8.x / CentOS 9.x / RHEL 8.x / RHEL 9.x ```text sudo dnf groupinstall 'Development Tools' sudo dnf install libcurl-devel sqlite-devel curl -fsS https://dlang.org/install.sh | bash -s dmd ``` For GUI notifications the following is also necessary: ```text sudo dnf install libnotify-devel ``` ### Dependencies: FreeBSD ```text pkg install bash bash-completion gmake pkgconf autoconf automake logrotate libinotify git sqlite3 ldc ``` For GUI notifications the following is also necessary: ```text pkg install libnotify ``` > [!NOTE] > Install the required FreeBSD packages as 'root' unless you have installed 'sudo' ### Dependencies: Gentoo ```text sudo emerge app-portage/layman sudo layman -a dlang ``` Add ebuild from contrib/gentoo to a local overlay to use. For GUI notifications the following is also necessary: ```text sudo emerge x11-libs/libnotify ``` ### Dependencies: Ubuntu 16.x Ubuntu Linux 16.x LTS reached the end of its five-year LTS window on April 30th 2021 and is no longer supported. ### Dependencies: Ubuntu 18.x / Lubuntu 18.x Ubuntu Linux 18.x LTS reached the end of its five-year LTS window on May 31th 2023 and is no longer supported. ### Dependencies: Debian 9 Debian 9 reached the end of its five-year support window on June 30th 2022 and is no longer supported. ### Dependencies: Debian 10 -> Debian 12 / Ubuntu 20.x -> Ubuntu 24.x - x86_64 These dependencies are also applicable for all Ubuntu based distributions such as: * Lubuntu * Linux Mint * POP OS * Peppermint OS ```text sudo apt install build-essential sudo apt install libcurl4-openssl-dev libsqlite3-dev pkg-config git curl systemd-dev curl -fsS https://dlang.org/install.sh | bash -s dmd ``` For GUI notifications the following is also necessary: ```text sudo apt install libnotify-dev ``` ### Dependencies: Debian 11 and Raspbian (ARMHF) / Debian 12 / Raspbian / Ubuntu 22.x (ARM64) > [!CAUTION] > The minimum LDC compiler version required to compile this application is 1.18.0, which is not available for Debian Buster or distributions based on Debian Buster. You are advised to first upgrade your platform distribution to one that is based on Debian Bullseye (Debian 11) or later. These instructions were validated using: * `Linux raspberrypi 5.10.92-v8+ #1514 SMP PREEMPT Mon Jan 17 17:39:38 GMT 2022 aarch64` (2022-01-28-raspios-bullseye-armhf-lite) using Raspberry Pi 3B (revision 1.2) * `Linux raspberrypi 5.10.92-v8+ #1514 SMP PREEMPT Mon Jan 17 17:39:38 GMT 2022 aarch64` (2022-01-28-raspios-bullseye-arm64-lite) using Raspberry Pi 3B (revision 1.2) * `Linux ubuntu 5.15.0-1005-raspi #5-Ubuntu SMP PREEMPT Mon Apr 4 12:21:48 UTC 2022 aarch64 aarch64 aarch64 GNU/Linux` (ubuntu-22.04-preinstalled-server-arm64+raspi) using Raspberry Pi 3B (revision 1.2) > [!IMPORTANT] > For successful compilation of this application, it's crucial that the build environment is equipped with a minimum of 1GB of memory and an additional 1GB of swap space. To verify your system's swap space availability, you can use the `swapon` command. Ensuring these requirements are met is vital for the application's compilation process. ```text sudo apt install build-essential sudo apt install libcurl4-openssl-dev libsqlite3-dev pkg-config git curl ldc systemd-dev ``` For GUI notifications the following is also necessary: ```text sudo apt install libnotify-dev ``` ### Dependencies: OpenSuSE Leap 15.0 ```text sudo zypper addrepo https://download.opensuse.org/repositories/devel:languages:D/openSUSE_Leap_15.0/devel:languages:D.repo sudo zypper refresh sudo zypper install gcc git libcurl-devel sqlite3-devel dmd phobos-devel phobos-devel-static ``` For GUI notifications the following is also necessary: ```text sudo zypper install libnotify-devel ``` ### Dependencies: OpenSuSE Leap 15.1 ```text sudo zypper addrepo https://download.opensuse.org/repositories/devel:languages:D/openSUSE_Leap_15.1/devel:languages:D.repo sudo zypper refresh sudo zypper install gcc git libcurl-devel sqlite3-devel dmd phobos-devel phobos-devel-static ``` For GUI notifications the following is also necessary: ```text sudo zypper install libnotify-devel ``` ### Dependencies: OpenSuSE Leap 15.2 ```text sudo zypper refresh sudo zypper install gcc git libcurl-devel sqlite3-devel dmd phobos-devel phobos-devel-static ``` For GUI notifications the following is also necessary: ```text sudo zypper install libnotify-devel ``` ## Compilation & Installation ### High Level Steps 1. Install the platform dependencies for your platform 2. Activate your DMD or LDC compiler if required 3. Clone the GitHub repository, 4. Run the 'configure' command then build the application and install it 5. Deactivate your DMD or LDC compiler if required ### Linux: Building the application using the DMD Reference Compiler Before cloning and compiling, if you have installed DMD via curl for your OS, you will need to activate DMD as per example below: ```text Run `source ~/dlang/dmd-2.088.0/activate` in your shell to use dmd-2.088.0. This will setup PATH, LIBRARY_PATH, LD_LIBRARY_PATH, DMD, DC, and PS1. Run `deactivate` later on to restore your environment. ``` Without performing this step, the compilation process will fail. > [!NOTE] > Depending on your DMD version, substitute `2.088.0` above with your DMD version that is installed. ```text git clone https://github.com/abraunegg/onedrive.git cd onedrive ./configure make clean; make; sudo make install ``` ### FreeBSD: Building the application using FreeBSD version of LDC ```text git clone https://github.com/abraunegg/onedrive.git cd onedrive ./configure gmake clean; gmake; gmake install ``` > [!NOTE] > Install the application as 'root' unless you have installed 'sudo' ### Build options #### GUI Notification Support GUI notification support can be enabled using the `configure` switch `--enable-notifications`. #### systemd service directory customisation support Systemd service files are installed in the appropriate directories on the system, as provided by `pkg-config systemd` settings. If the need for overriding the deduced path are necessary, the two options `--with-systemdsystemunitdir` (for the Systemd system unit location), and `--with-systemduserunitdir` (for the Systemd user unit location) can be specified. Passing in `no` to one of these options disabled service file installation. #### Additional Compiler Debug By passing `--enable-debug` to the `configure` call, `onedrive` gets built with additional debug information, useful (for example) to get `perf`-issued figures. #### Shell Completion Support By passing `--enable-completions` to the `configure` call, shell completion functions are installed for `bash`, `zsh` and `fish`. The installation directories are determined as far as possible automatically, but can be overridden by passing `--with-bash-completion-dir=

`, `--with-zsh-completion-dir=`, and `--with-fish-completion-dir=` to `configure`. ### Building using a different compiler (for example [LDC](https://wiki.dlang.org/LDC)) #### ARMHF Architecture (Raspbian) and ARM64 Architecture (Ubuntu 22.x / Debian 11 / Debian 12 / Raspbian) > [!CAUTION] > The minimum LDC compiler version required to compile this application is 1.18.0, which is not available for Debian Buster or distributions based on Debian Buster. You are advised to first upgrade your platform distribution to one that is based on Debian Bullseye (Debian 11) or later. > [!IMPORTANT] > For successful compilation of this application, it's crucial that the build environment is equipped with a minimum of 1GB of memory and an additional 1GB of swap space. To verify your system's swap space availability, you can use the `swapon` command. Ensuring these requirements are met is vital for the application's compilation process. > [!NOTE] > The 'configure' step will detect the correct version of LDC to be used when compiling the client under ARMHF and ARM64 CPU architectures. ```text git clone https://github.com/abraunegg/onedrive.git cd onedrive ./configure; make clean; make; sudo make install ``` ## Upgrading the client If you have installed the client from a distribution package, the client will be updated when the distribution package is updated by the package maintainer and will be updated to the new application version when you perform your package update. If you have built the client from source, to upgrade your client, it is recommended that you first uninstall your existing 'onedrive' binary (see below), then re-install the client by re-cloning, re-compiling and re-installing the client again to install the new version. > [!NOTE] > Following the uninstall process will remove all client components including *all* systemd files, including any custom files created for specific access such as SharePoint Libraries. You can optionally choose to not perform this uninstallation step, and simply re-install the client by re-cloning, re-compiling and re-installing the client again - however the risk here is that you end up with two onedrive client binaries on your system, and depending on your system search path preferences, this will determine which binary is used. > [!CAUTION] > Before performing any upgrade, it is highly recommended for you to stop any running systemd service if applicable to ensure that these services are restarted using the updated client version. Post re-install, to confirm that you have the new version of the client installed, use `onedrive --version` to determine the client version that is now installed. ## Uninstalling the client ### Uninstalling the client if installed from distribution package Follow your distribution documentation to uninstall the package that you installed ### Uninstalling the client if installed and built from source From within your GitHub repository clone, perform the following to remove the 'onedrive' binary: ```text sudo make uninstall ``` If you are not upgrading your client, to remove your application state and configuration, perform the following additional step: ``` rm -rf ~/.config/onedrive ``` > [!IMPORTANT] > If you are using the `--confdir option`, substitute `~/.config/onedrive` for the correct directory storing your client configuration. If you want to just delete the application key, but keep the items database: ```text rm -f ~/.config/onedrive/refresh_token ``` onedrive-2.5.5/docs/known-issues.md000066400000000000000000000104741476564400300172740ustar00rootroot00000000000000# List of Identified Known Issues The following points detail known issues associated with this client: ## Renaming or Moving Files in Standalone Mode causes online deletion and re-upload to occur **Issue Tracker:** [#876](https://github.com/abraunegg/onedrive/issues/876), [#2579](https://github.com/abraunegg/onedrive/issues/2579) **Summary:** Renaming or moving files and/or folders while using the standalone sync option `--sync` this results in unnecessary data deletion online and subsequent re-upload. **Detailed Description:** In standalone mode (`--sync`), the renaming or moving folders locally that have already been synchronized leads to the data being deleted online and then re-uploaded in the next synchronization process. **Technical Explanation:** This behavior is expected from the client under these specific conditions. Renaming or moving files is interpreted as deleting them from their original location and creating them in a new location. In standalone sync mode, the client lacks the capability to track file system changes (including renames and moves) that occur when it is not running. This limitation is the root cause of the observed 'deletion and re-upload' cycle. **Recommended Workaround:** For effective tracking of file and folder renames or moves to new local directories, it is recommended to run the client in service mode (`--monitor`) rather than in standalone mode. This approach allows the client to immediately process these changes, enabling the data to be updated (renamed or moved) in the new location on OneDrive without undergoing deletion and re-upload. ## Application 'stops' running without any visible reason **Issue Tracker:** [#494](https://github.com/abraunegg/onedrive/issues/494), [#753](https://github.com/abraunegg/onedrive/issues/753), [#792](https://github.com/abraunegg/onedrive/issues/792), [#884](https://github.com/abraunegg/onedrive/issues/884), [#1162](https://github.com/abraunegg/onedrive/issues/1162), [#1408](https://github.com/abraunegg/onedrive/issues/1408), [#1520](https://github.com/abraunegg/onedrive/issues/1520), [#1526](https://github.com/abraunegg/onedrive/issues/1526) **Summary:** Users experience sudden shutdowns in a client application during file transfers with Microsoft's Europe Data Centers, likely due to unstable internet or HTTPS inspection issues. This problem, often signaled by an error code of 141, is related to the application's reliance on Curl and OpenSSL. Resolution steps include system updates, seeking support from OS vendors, ISPs, OpenSSL/Curl teams, and providing detailed debug logs to Microsoft for analysis. **Detailed Description:** The application unexpectedly stops functioning during upload or download operations when using the client. This issue occurs without any apparent reason. Running `echo $?` after the unexpected exit may return an error code of 141. This problem predominantly arises when the client interacts with Microsoft's Europe Data Centers. **Technical Explanation:** The client heavily relies on Curl and OpenSSL for operations with the Microsoft OneDrive service. A common observation during this error is an entry in the HTTPS Debug Log stating: ``` OpenSSL SSL_read: SSL_ERROR_SYSCALL, errno 104 ``` To confirm this as the root cause, a detailed HTTPS debug log can be generated with these commands: ``` --verbose --verbose --debug-https ``` This error typically suggests one of the following issues: * An unstable internet connection between the user and the OneDrive service. * An issue with HTTPS transparent inspection services that monitor the traffic en route to the OneDrive service. **Recommended Resolution Steps:** Recommended steps to address this issue include: * Updating your operating system to the latest version. * Configure the application to only use HTTP/1.1 * Configure the application to use IPv4 only. * Upgrade your 'curl' application to the latest available from the curl developers. * Seeking assistance from your OS vendor. * Contacting your Internet Service Provider (ISP) or your IT Help Desk. * Reporting the issue to the OpenSSL and/or Curl teams for improved handling of such connection failures. * Creating a HTTPS Debug Log during the issue and submitting a support request to Microsoft with the log for their analysis. For more in-depth SSL troubleshooting, please read: https://maulwuff.de/research/ssl-debugging.htmlonedrive-2.5.5/docs/national-cloud-deployments.md000066400000000000000000000170551476564400300221030ustar00rootroot00000000000000# How to configure access to specific Microsoft Azure deployments > [!CAUTION] > Before reading this document, please ensure you are running application version [![Version](https://img.shields.io/github/v/release/abraunegg/onedrive)](https://github.com/abraunegg/onedrive/releases) or greater. Use `onedrive --version` to determine what application version you are using and upgrade your client if required. ## Process Overview In some cases it is a requirement to utilise specific Microsoft Azure cloud deployments to conform with data and security requirements that requires data to reside within the geographic borders of that country. Current national clouds that are supported are: * Microsoft Cloud for US Government * Microsoft Cloud Germany * Azure and Office365 operated by VNET in China In order to successfully use these specific Microsoft Azure deployments, the following steps are required: 1. Register an application with the Microsoft identity platform using the Azure portal 2. Configure the new application with the appropriate authentication scopes 3. Validate that the authentication / redirect URI is correct for your application registration 4. Configure the onedrive client to use the new application id as provided during application registration 5. Configure the onedrive client to use the right Microsoft Azure deployment region that your application was registered with 6. Authenticate the client ## Step 1: Register a new application with Microsoft Azure 1. Log into your applicable Microsoft Azure Portal with your applicable Office365 identity: | National Cloud Environment | Microsoft Azure Portal | |---|---| | Microsoft Cloud for US Government | https://portal.azure.com/ | | Microsoft Cloud Germany | https://portal.azure.com/ | | Azure and Office365 operated by VNET | https://portal.azure.cn/ | 2. Select 'Azure Active Directory' as the service you wish to configure 3. Under 'Manage', select 'App registrations' to register a new application 4. Click 'New registration' 5. Type in the appropriate details required as per below: ![application_registration](./images/application_registration.jpg) 6. To save the application registration, click 'Register' and something similar to the following will be displayed: ![application_registration_done](./images/application_registration_done.jpg) > [!NOTE] > The Application (client) ID UUID as displayed after client registration, is what is required as the 'application_id' for Step 4 below. ## Step 2: Configure application authentication scopes Configure the API permissions as per the following: | API / Permissions name | Type | Description | Admin consent required | |---|---|---|---| | Files.ReadWrite | Delegated | Have full access to user files | No | | Files.ReadWrite.All | Delegated | Have full access to all files user can access | No | | Sites.ReadWrite.All | Delegated | Have full access to all items in all site collections | No | | offline_access | Delegated | Maintain access to data you have given it access to | No | ![authentication_scopes](./images/authentication_scopes.jpg) ## Step 3: Validate that the authentication / redirect URI is correct Add the appropriate redirect URI for your Azure deployment: ![authentication_response_uri](./images/authentication_response_uri.jpg) A valid entry for the response URI should be one of: * https://login.microsoftonline.us/common/oauth2/nativeclient (Microsoft Cloud for US Government) * https://login.microsoftonline.de/common/oauth2/nativeclient (Microsoft Cloud Germany) * https://login.chinacloudapi.cn/common/oauth2/nativeclient (Azure and Office365 operated by VNET in China) For a single-tenant application, it may be necessary to use your specific tenant id instead of "common": * https://login.microsoftonline.us/example.onmicrosoft.us/oauth2/nativeclient (Microsoft Cloud for US Government) * https://login.microsoftonline.de/example.onmicrosoft.de/oauth2/nativeclient (Microsoft Cloud Germany) * https://login.chinacloudapi.cn/example.onmicrosoft.cn/oauth2/nativeclient (Azure and Office365 operated by VNET in China) ## Step 4: Configure the onedrive client to use new application registration Update to your 'onedrive' configuration file (`~/.config/onedrive/config`) the following: ```text application_id = "insert valid entry here" ``` This will reconfigure the client to use the new application registration you have created. **Example:** ```text application_id = "22c49a0d-d21c-4792-aed1-8f163c982546" ``` ## Step 5: Configure the onedrive client to use the specific Microsoft Azure deployment Update to your 'onedrive' configuration file (`~/.config/onedrive/config`) the following: ```text azure_ad_endpoint = "insert valid entry here" ``` Valid entries are: * USL4 (Microsoft Cloud for US Government) * USL5 (Microsoft Cloud for US Government - DOD) * DE (Microsoft Cloud Germany) * CN (Azure and Office365 operated by VNET in China) This will configure your client to use the correct Azure AD and Graph endpoints as per [https://docs.microsoft.com/en-us/graph/deployments](https://docs.microsoft.com/en-us/graph/deployments) **Example:** ```text azure_ad_endpoint = "USL4" ``` If the Microsoft Azure deployment does not support multi-tenant applications, update to your 'onedrive' configuration file (`~/.config/onedrive/config`) the following: ```text azure_tenant_id = "insert valid entry here" ``` This will configure your client to use the specified tenant id in its Azure AD and Graph endpoint URIs, instead of "common". The tenant id may be the GUID Directory ID (formatted "00000000-0000-0000-0000-000000000000"), or the fully qualified tenant name (e.g. "example.onmicrosoft.us"). The GUID Directory ID may be located in the Azure administration page as per [https://docs.microsoft.com/en-us/onedrive/find-your-office-365-tenant-id](https://docs.microsoft.com/en-us/onedrive/find-your-office-365-tenant-id). Note that you may need to go to your national-deployment-specific administration page, rather than following the links within that document. The tenant name may be obtained by following the PowerShell instructions on [https://docs.microsoft.com/en-us/onedrive/find-your-office-365-tenant-id](https://docs.microsoft.com/en-us/onedrive/find-your-office-365-tenant-id); it is shown as the "TenantDomain" upon completion of the "Connect-AzureAD" command. **Example:** ```text azure_tenant_id = "example.onmicrosoft.us" # or azure_tenant_id = "0c4be462-a1ab-499b-99e0-da08ce52a2cc" ``` ## Step 6: Authenticate the client Run the application without any additional command switches. You will be asked to open a specific URL by using your web browser where you will have to login into your Microsoft Account and give the application the permission to access your files. After giving permission to the application, you will be redirected to a blank page. Copy the URI of the blank page into the application. ```text [user@hostname ~]$ onedrive Authorize this app visiting: https://..... Enter the response uri: ``` **Example:** ``` [user@hostname ~]$ onedrive Authorize this app visiting: https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=22c49a0d-d21c-4792-aed1-8f163c982546&scope=Files.ReadWrite%20Files.ReadWrite.all%20Sites.ReadWrite.All%20offline_access&response_type=code&redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient Enter the response uri: https://login.microsoftonline.com/common/oauth2/nativeclient?code= Application has been successfully authorised, however no additional command switches were provided. Please use --help for further assistance in regards to running this application. ``` onedrive-2.5.5/docs/podman.md000066400000000000000000000460141476564400300161040ustar00rootroot00000000000000# Run the OneDrive Client for Linux under Podman This client can be run as a Podman container, with 3 available container base options for you to choose from: | Container Base | Docker Tag | Description | i686 | x86_64 | ARMHF | AARCH64 | |----------------|-------------|----------------------------------------------------------------|:------:|:------:|:-----:|:-------:| | Alpine Linux | edge-alpine | Podman container based on Alpine 3.20 using 'master' |❌|✔|❌|✔| | Alpine Linux | alpine | Podman container based on Alpine 3.20 using latest release |❌|✔|❌|✔| | Debian | debian | Podman container based on Debian Stable using latest release |✔|✔|✔|✔| | Debian | edge | Podman container based on Debian Stable using 'master' |✔|✔|✔|✔| | Debian | edge-debian | Podman container based on Debian Stable using 'master' |✔|✔|✔|✔| | Debian | latest | Podman container based on Debian Stable using latest release |✔|✔|✔|✔| | Fedora | edge-fedora | Podman container based on Fedora 40 using 'master' |❌|✔|❌|✔| | Fedora | fedora | Podman container based on Fedora 40 using latest release |❌|✔|❌|✔| These containers offer a simple monitoring-mode service for the OneDrive Client for Linux. The instructions below have been validated on: * Fedora 40 The instructions below will utilise the 'edge' tag, however this can be substituted for any of the other docker tags such as 'latest' from the table above if desired. The 'edge' Docker Container will align closer to all documentation and features, where as 'latest' is the release version from a static point in time. The 'latest' tag however may contain bugs and/or issues that will have been fixed, and those fixes are contained in 'edge'. Additionally there are specific version release tags for each release. Refer to https://hub.docker.com/r/driveone/onedrive/tags for any other Docker tags you may be interested in. > [!NOTE] > The below instructions for podman has been tested and validated when logging into the system as an unprivileged user (non 'root' user). ## High Level Configuration Steps 1. Install 'podman' as per your distribution platform's instructions if not already installed. 2. Disable 'SELinux' as per your distribution platform's instructions 3. Test 'podman' by running a test container 4. Prepare the required podman volumes to store the configuration and data 5. Run the 'onedrive' container and perform authorisation 6. Running the 'onedrive' container under 'podman' ## Configuration Steps ### 1. Install 'podman' on your platform Install 'podman' as per your distribution platform's instructions if not already installed. ### 2. Disable SELinux on your platform In order to run the Docker container under 'podman', SELinux must be disabled. Without doing this, when the application is authenticated in the steps below, the following error will be presented: ```text ERROR: The local file system returned an error with the following message: Error Message: /onedrive/conf/refresh_token: Permission denied The database cannot be opened. Please check the permissions of ~/.config/onedrive/items.sqlite3 ``` The only known work-around for the above problem at present is to disable SELinux. Please refer to your distribution platform's instructions on how to perform this step. * Fedora: https://docs.fedoraproject.org/en-US/quick-docs/selinux-changing-states-and-modes/#_disabling_selinux * Red Hat Enterprise Linux: https://access.redhat.com/solutions/3176 Post disabling SELinux and reboot your system, confirm that `getenforce` returns `Disabled`: ```text $ getenforce Disabled ``` If you are still experiencing permission issues despite disabling SELinux, please read https://www.redhat.com/sysadmin/container-permission-denied-errors ### 3. Test 'podman' on your platform Test that 'podman' is operational for your 'non-root' user, as per below: ```bash [alex@fedora40-podman ~]$ podman pull fedora Resolved "fedora" as an alias (/etc/containers/registries.conf.d/000-shortnames.conf) Trying to pull registry.fedoraproject.org/fedora:latest... Getting image source signatures Copying blob b30887322388 done | Copying config a1cd3cbf8a done | Writing manifest to image destination a1cd3cbf8adaa422629f2fcdc629fd9297138910a467b11c66e5ddb2c2753dff [alex@fedora40-podman ~]$ podman run fedora /bin/echo "Welcome to the Podman World" Welcome to the Podman World [alex@fedora40-podman ~]$ ``` ### 4. Configure the required podman volumes The 'onedrive' Docker container requires 2 podman volumes to operate: * Config Volume * Data Volume The first volume is the configuration volume that stores all the applicable application configuration + current runtime state. In a non-containerised environment, this normally resides in `~/.config/onedrive` - in a containerised environment this is stored in the volume tagged as `/onedrive/conf` The second volume is the data volume, where all your data from Microsoft OneDrive is stored locally. This volume is mapped to an actual directory point on your local filesystem and this is stored in the volume tagged as `/onedrive/data` #### 4.1 Prepare the 'config' volume Create the 'config' volume with the following command: ```bash podman volume create onedrive_conf ``` This will create a podman volume labeled `onedrive_conf`, where all configuration of your onedrive account will be stored. You can add a custom config file in this location at a later point in time if required. #### 4.2 Prepare the 'data' volume Create the 'data' volume with the following command: ```bash podman volume create onedrive_data ``` This will create a podman volume labeled `onedrive_data` and will map to a path on your local filesystem. This is where your data from Microsoft OneDrive will be stored. Keep in mind that: * The owner of this specified folder must not be root * Podman will attempt to change the permissions of the volume to the user the container is configured to run as > [!IMPORTANT] > Issues occur when this target folder is a mounted folder of an external system (NAS, SMB mount, USB Drive etc) as the 'mount' itself is owed by 'root'. If this is your use case, you *must* ensure your normal user can mount your desired target without having the target mounted by 'root'. If you do not fix this, your Podman container will fail to start with the following error message: > ```bash > ROOT level privileges prohibited! > ``` ### 5. First run of Docker container under podman and performing authorisation The 'onedrive' client within the container first needs to be authorised with your Microsoft account. This is achieved by initially running podman in interactive mode. Run the podman image with the commands below and make sure to change the value of `ONEDRIVE_DATA_DIR` to the actual onedrive data directory on your filesystem that you wish to use (e.g. `export ONEDRIVE_DATA_DIR="/home/abraunegg/OneDrive"`). > [!IMPORTANT] > The 'target' folder of `ONEDRIVE_DATA_DIR` must exist before running the podman container. The script below will create 'ONEDRIVE_DATA_DIR' so that it exists locally for the podman volume mapping to occur. It is also a requirement that the container be run using a non-root uid and gid, you must insert a non-root UID and GID (e.g.` export ONEDRIVE_UID=1000` and export `ONEDRIVE_GID=1000`). The script below will use `id` to evaluate your system environment to use the correct values. ```bash export ONEDRIVE_DATA_DIR="${HOME}/OneDrive" export ONEDRIVE_UID=`id -u` export ONEDRIVE_GID=`id -g` mkdir -p ${ONEDRIVE_DATA_DIR} podman run -it --name onedrive --user "${ONEDRIVE_UID}:${ONEDRIVE_GID}" \ -v onedrive_conf:/onedrive/conf:U,Z \ -v "${ONEDRIVE_DATA_DIR}:/onedrive/data:U,Z" \ driveone/onedrive:edge ``` > [!IMPORTANT] > In some scenarios, 'podman' sets the configuration and data directories to a different UID & GID as specified. To resolve this situation, you must run 'podman' with the `--userns=keep-id` flag to ensure 'podman' uses the UID and GID as specified. The updated script example when using `--userns=keep-id` is below: ```bash export ONEDRIVE_DATA_DIR="${HOME}/OneDrive" export ONEDRIVE_UID=`id -u` export ONEDRIVE_GID=`id -g` mkdir -p ${ONEDRIVE_DATA_DIR} podman run -it --name onedrive --user "${ONEDRIVE_UID}:${ONEDRIVE_GID}" \ --userns=keep-id \ -v onedrive_conf:/onedrive/conf:U,Z \ -v "${ONEDRIVE_DATA_DIR}:/onedrive/data:U,Z" \ driveone/onedrive:edge ``` > [!IMPORTANT] > If you plan to use the 'podman' built in auto-updating of container images described in 'Systemd Service & Auto Updating' below, you must pass an additional argument to set a label during the first run. The updated script example to support auto-updating of container images is below: ```bash export ONEDRIVE_DATA_DIR="${HOME}/OneDrive" export ONEDRIVE_UID=`id -u` export ONEDRIVE_GID=`id -g` mkdir -p ${ONEDRIVE_DATA_DIR} podman run -it --name onedrive --user "${ONEDRIVE_UID}:${ONEDRIVE_GID}" \ --userns=keep-id \ -v onedrive_conf:/onedrive/conf:U,Z \ -v "${ONEDRIVE_DATA_DIR}:/onedrive/data:U,Z" \ -e PODMAN=1 \ --label "io.containers.autoupdate=image" \ driveone/onedrive:edge ``` When the Podman container successfully starts: * You will be asked to open a specific link using your web browser * Login to your Microsoft Account and give the application the permission * After giving the permission, you will be redirected to a blank page * Copy the URI of the blank page into the application prompt to authorise the application Once the 'onedrive' application is authorised, the client will automatically start monitoring your `ONEDRIVE_DATA_DIR` for data changes to be uploaded to OneDrive. Files stored on OneDrive will be downloaded to this location. If the client is working as expected, you can detach from the container with Ctrl+p, Ctrl+q. ### 6. Running the 'onedrive' container under 'podman' #### 6.1 Check if the monitor service is running ```bash podman ps -f name=onedrive ``` #### 6.2 Show 'onedrive' runtime logs ```bash podman logs onedrive ``` #### 6.3 Stop running 'onedrive' container ```bash podman stop onedrive ``` #### 6.4 Start 'onedrive' container ```bash podman start onedrive ``` #### 6.5 Remove 'onedrive' container ```bash podman rm -f onedrive ``` ## Advanced Usage ### Systemd Service & Auto Updating Podman supports running containers as a systemd service and also auto updating of the container images. Using the existing running container you can generate a systemd unit file to be installed by the **root** user. To have your container image auto-update with podman, it must first be created with the label `"io.containers.autoupdate=image"` mentioned in step 5 above. ``` cd /tmp podman generate systemd --new --restart-policy on-failure --name -f onedrive /tmp/container-onedrive.service # copy the generated systemd unit file to the systemd path and reload the daemon cp -Z ~/container-onedrive.service /usr/lib/systemd/system systemctl daemon-reload #optionally enable it to startup on boot systemctl enable container-onedrive.service #check status systemctl status container-onedrive #start/stop/restart container as a systemd service systemctl stop container-onedrive systemctl start container-onedrive ``` To update the image using podman (Ad-hoc) ``` podman auto-update ``` To update the image using systemd (Automatic/Scheduled) ``` # Enable the podman-auto-update.timer service at system start: systemctl enable podman-auto-update.timer # Start the service systemctl start podman-auto-update.timer # Containers with the autoupdate label will be updated on the next scheduled timer systemctl list-timers --all ``` ### Editing the running configuration and using a 'config' file The 'onedrive' client should run in default configuration, however you can change this default configuration by placing a custom config file in the `onedrive_conf` podman volume. First download the default config from [here](https://raw.githubusercontent.com/abraunegg/onedrive/master/config) Then put it into your onedrive_conf volume path, which can be found with: ```bash podman volume inspect onedrive_conf ``` Or you can map your own config folder to the config volume. Make sure to copy all files from the volume into your mapped folder first. The detailed document for the config can be found here: [Application Configuration Options for the OneDrive Client for Linux](https://github.com/abraunegg/onedrive/blob/master/docs/application-config-options.md) ### Syncing multiple accounts There are many ways to do this, the easiest is probably to do the following: 1. Create a second podman config volume (replace `work` with your desired name): `podman volume create onedrive_conf_work` 2. And start a second podman monitor container (again replace `work` with your desired name): ```bash export ONEDRIVE_DATA_DIR_WORK="/home/abraunegg/OneDriveWork" export ONEDRIVE_UID=`id -u` export ONEDRIVE_GID=`id -g` mkdir -p ${ONEDRIVE_DATA_DIR_WORK} podman run -it --name onedrive_work --user "${ONEDRIVE_UID}:${ONEDRIVE_GID}" \ --userns=keep-id \ -v onedrive_conf_work:/onedrive/conf:U,Z \ -v "${ONEDRIVE_DATA_DIR_WORK}:/onedrive/data:U,Z" \ -e PODMAN=1 \ --label "io.containers.autoupdate=image" \ driveone/onedrive:edge ``` ## Supported Podman Environment Variables | Variable | Purpose | Sample Value | | ---------------- | --------------------------------------------------- |:-------------:| | ONEDRIVE_UID | UserID (UID) to run as | 1000 | | ONEDRIVE_GID | GroupID (GID) to run as | 1000 | | ONEDRIVE_VERBOSE | Controls "--verbose" switch on onedrive sync. Default is 0 | 1 | | ONEDRIVE_DEBUG | Controls "--verbose --verbose" switch on onedrive sync. Default is 0 | 1 | | ONEDRIVE_DEBUG_HTTPS | Controls "--debug-https" switch on onedrive sync. Default is 0 | 1 | | ONEDRIVE_RESYNC | Controls "--resync" switch on onedrive sync. Default is 0 | 1 | | ONEDRIVE_DOWNLOADONLY | Controls "--download-only" switch on onedrive sync. Default is 0 | 1 | | ONEDRIVE_CLEANUPLOCAL | Controls "--cleanup-local-files" to cleanup local files and folders if they are removed online. Default is 0 | 1 | | ONEDRIVE_UPLOADONLY | Controls "--upload-only" switch on onedrive sync. Default is 0 | 1 | | ONEDRIVE_NOREMOTEDELETE | Controls "--no-remote-delete" switch on onedrive sync. Default is 0 | 1 | | ONEDRIVE_LOGOUT | Controls "--logout" switch. Default is 0 | 1 | | ONEDRIVE_REAUTH | Controls "--reauth" switch. Default is 0 | 1 | | ONEDRIVE_AUTHFILES | Controls "--auth-files" option. Default is "" | Please read [CLI Option: --auth-files](./application-config-options.md#cli-option---auth-files) | | ONEDRIVE_AUTHRESPONSE | Controls "--auth-response" option. Default is "" | Please read [CLI Option: --auth-response](./application-config-options.md#cli-option---auth-response) | | ONEDRIVE_DISPLAY_CONFIG | Controls "--display-running-config" switch on onedrive sync. Default is 0 | 1 | | ONEDRIVE_SINGLE_DIRECTORY | Controls "--single-directory" option. Default = "" | "mydir" | | ONEDRIVE_DRYRUN | Controls "--dry-run" option. Default is 0 | 1 | | ONEDRIVE_DISABLE_DOWNLOAD_VALIDATION | Controls "--disable-download-validation" option. Default is 0 | 1 | | ONEDRIVE_DISABLE_UPLOAD_VALIDATION | Controls "--disable-upload-validation" option. Default is 0 | 1 | | ONEDRIVE_SYNC_SHARED_FILES | Controls "--sync-shared-files" option. Default is 0 | 1 | | ONEDRIVE_RUNAS_ROOT | Controls if the Docker container should be run as the 'root' user instead of 'onedrive' user. Default is 0 | 1 | | ONEDRIVE_SYNC_ONCE | Controls if the Docker container should be run in Standalone Mode. It will use Monitor Mode otherwise. Default is 0 | 1 | ### Environment Variables Usage Examples **Verbose Output:** ```bash podman run -e ONEDRIVE_VERBOSE=1 -v onedrive_conf:/onedrive/conf:U,Z -v "${ONEDRIVE_DATA_DIR}:/onedrive/data:U,Z" --user "${ONEDRIVE_UID}:${ONEDRIVE_GID}" driveone/onedrive:edge ``` **Debug Output:** ```bash podman run -e ONEDRIVE_DEBUG=1 -v onedrive_conf:/onedrive/conf:U,Z -v "${ONEDRIVE_DATA_DIR}:/onedrive/data:U,Z" --user "${ONEDRIVE_UID}:${ONEDRIVE_GID}" driveone/onedrive:edge ``` **Perform a --resync:** ```bash podman run -e ONEDRIVE_RESYNC=1 -v onedrive_conf:/onedrive/conf:U,Z -v "${ONEDRIVE_DATA_DIR}:/onedrive/data:U,Z" --user "${ONEDRIVE_UID}:${ONEDRIVE_GID}" driveone/onedrive:edge ``` **Perform a --resync and --verbose:** ```bash podman run -e ONEDRIVE_RESYNC=1 -e ONEDRIVE_VERBOSE=1 -v onedrive_conf:/onedrive/conf:U,Z -v "${ONEDRIVE_DATA_DIR}:/onedrive/data:U,Z" --user "${ONEDRIVE_UID}:${ONEDRIVE_GID}" driveone/onedrive:edge ``` **Perform a --logout:** ```bash podman run -it -e ONEDRIVE_LOGOUT=1 -v onedrive_conf:/onedrive/conf:U,Z -v "${ONEDRIVE_DATA_DIR}:/onedrive/data:U,Z" --user "${ONEDRIVE_UID}:${ONEDRIVE_GID}" driveone/onedrive:edge ``` **Perform a --logout and re-authenticate:** ```bash podman run -it -e ONEDRIVE_REAUTH=1 -v onedrive_conf:/onedrive/conf:U,Z -v "${ONEDRIVE_DATA_DIR}:/onedrive/data:U,Z" --user "${ONEDRIVE_UID}:${ONEDRIVE_GID}" driveone/onedrive:edge ``` ## Building a custom Podman image You can also build your own image instead of pulling the one from [hub.docker.com](https://hub.docker.com/r/driveone/onedrive): ```bash git clone https://github.com/abraunegg/onedrive cd onedrive podman build . -t local-onedrive -f contrib/docker/Dockerfile ``` There are alternate, smaller images available by building Dockerfile-debian or Dockerfile-alpine. These [multi-stage builder pattern](https://docs.docker.com/develop/develop-images/multistage-build/) Dockerfiles require Docker version at least 17.05. ### How to build and run a custom Podman image based on Debian ``` bash podman build . -t local-onedrive-debian -f contrib/docker/Dockerfile-debian podman run -v onedrive_conf:/onedrive/conf:U,Z -v "${ONEDRIVE_DATA_DIR}:/onedrive/data:U,Z" --user "${ONEDRIVE_UID}:${ONEDRIVE_GID}" --userns=keep-id local-onedrive-debian:latest ``` ### How to build and run a custom Podman image based on Alpine Linux ``` bash podman build . -t local-onedrive-alpine -f contrib/docker/Dockerfile-alpine podman run -v onedrive_conf:/onedrive/conf:U,Z -v "${ONEDRIVE_DATA_DIR}:/onedrive/data:U,Z" --user "${ONEDRIVE_UID}:${ONEDRIVE_GID}" --userns=keep-id local-onedrive-alpine:latest ``` ### How to build and run a custom Podman image for ARMHF (Raspberry Pi) Compatible with: * Raspberry Pi * Raspberry Pi 2 * Raspberry Pi Zero * Raspberry Pi 3 * Raspberry Pi 4 ``` bash podman build . -t local-onedrive-armhf -f contrib/docker/Dockerfile-debian podman run -v onedrive_conf:/onedrive/conf:U,Z -v "${ONEDRIVE_DATA_DIR}:/onedrive/data:U,Z" --user "${ONEDRIVE_UID}:${ONEDRIVE_GID}" --userns=keep-id local-onedrive-armhf:latest ``` ### How to build and run a custom Podman image for AARCH64 Platforms ``` bash podman build . -t local-onedrive-aarch64 -f contrib/docker/Dockerfile-debian podman run -v onedrive_conf:/onedrive/conf:U,Z -v "${ONEDRIVE_DATA_DIR}:/onedrive/data:U,Z" --user "${ONEDRIVE_UID}:${ONEDRIVE_GID}" --userns=keep-id local-onedrive-aarch64:latest ``` onedrive-2.5.5/docs/privacy-policy.md000066400000000000000000000114451476564400300176000ustar00rootroot00000000000000# Privacy Policy Effective Date: May 16 2018 ## Introduction This Privacy Policy outlines how OneDrive Client for Linux ("we," "our," or "us") collects, uses, and protects information when you use our software ("OneDrive Client for Linux"). We respect your privacy and are committed to ensuring the confidentiality and security of any information you provide while using the Software. ## Information We Do Not Collect We want to be transparent about the fact that we do not collect any personal data, usage data, or tracking data through the Software. This means: 1. **No Personal Data**: We do not collect any information that can be used to personally identify you, such as your name, email address, phone number, or physical address. 2. **No Usage Data**: We do not collect data about how you use the Software, such as the features you use, the duration of your sessions, or any interactions within the Software. 3. **No Tracking Data**: We do not use cookies or similar tracking technologies to monitor your online behavior or track your activities across websites or apps. ## How We Use Your Information Since we do not collect any personal, usage, or tracking data, there is no information for us to use for any purpose. ## Third-Party Services The Software may include links to third-party websites or services, but we do not have control over the privacy practices or content of these third-party services. We encourage you to review the privacy policies of any third-party services you access through the Software. ## Children's Privacy Since we do not collect any personal, usage, or tracking data, there is no restriction on the use of this application by anyone under the age of 18. ## Information You Choose to Share While we do not collect personal data, usage data, or tracking data through the Software, there may be instances where you voluntarily choose to share information with us, particularly when submitting bug reports. These bug reports may contain sensitive information such as account details, file names, and directory names. It's important to note that these details are included in the logs and debug logs solely for the purpose of diagnosing and resolving technical issues with the Software. We want to emphasize that, even in these cases, we do not have access to your actual data. The logs and debug logs provided in bug reports are used exclusively for technical troubleshooting and debugging purposes. We take measures to treat this information with the utmost care, and it is only accessible to our technical support and development teams. We do not use this information for any other purpose, and we have strict security measures in place to protect it. ## Protecting Your Sensitive Data We are committed to safeguarding your sensitive data and maintaining its confidentiality. To ensure its protection: 1. **Limited Access**: Only authorized personnel within our technical support and development teams have access to the logs and debug logs containing sensitive data, and they are trained in handling this information securely. 2. **Data Encryption**: We use industry-standard encryption protocols to protect the transmission and storage of sensitive data. 3. **Data Retention**: We retain bug report data for a limited time necessary for resolving the reported issue. Once the issue is resolved, we promptly delete or anonymize the data. 4. **Security Measures**: We employ robust security measures to prevent unauthorized access, disclosure, or alteration of sensitive data. By submitting a bug report, you acknowledge and consent to the inclusion of sensitive information in logs and debug logs for the sole purpose of addressing technical issues with the Software. ## Your Responsibilities While we take measures to protect your sensitive data, it is essential for you to exercise caution when submitting bug reports. Please refrain from including any sensitive or personally identifiable information that is not directly related to the technical issue you are reporting. You have the option to redact or obfuscate sensitive details in bug reports to further protect your data. ## Changes to this Privacy Policy We may update this Privacy Policy from time to time to reflect changes in our practices or for other operational, legal, or regulatory reasons. We will notify you of any material changes by posting the updated Privacy Policy on our website or through the Software. We encourage you to review this Privacy Policy periodically. ## Contact Us If you have any questions or concerns about this Privacy Policy or our privacy practices, please contact us at support@mynas.com.au or via GitHub (https://github.com/abraunegg/onedrive) ## Conclusion By using the Software, you agree to the terms outlined in this Privacy Policy. If you do not agree with any part of this policy, please discontinue the use of the Software. onedrive-2.5.5/docs/puml/000077500000000000000000000000001476564400300152545ustar00rootroot00000000000000onedrive-2.5.5/docs/puml/applyPotentiallyChangedItem.png000066400000000000000000002250431476564400300234330ustar00rootroot00000000000000PNG  IHDR*tEXtcopyleftGenerated by https://plantuml.comviTXtplantumlxT]O0}}i/P+!Z4NY4M@M{ߏsr]K 衂th4A͸@{bٸTA%( B\:%uiQ9 SbC S]BiWƣt˄px&lSe@`z't1\6kmR5]7-ו"4m ׺nMOsȕCcE`b9h=TN⌭ch x-&_4;[6w  $rk5s3:*#jdW|>TnD>]{rF>q)G9{t&% $⏖xhe: 9mȵ+Y]4v)n2JZ7-ah'ч}q@L +<:g n+J.dErV% `7]boHфV&`^p/pv gWivz 1 >NqFS:jIffwC3+)eNo6*R`&т)9:P),#*_؄Y㒀acK،>ѻˏIDATx^y|že.@"e""" ">x*K$(ǃ,*j@6E"pIؔE ,! R65S$^?򨩮T$EEz l | | | | | | | |֮];v؞={k׮I&]tyg͚7!Efe˖֭[O2ٳz7"d6ŋ'&&Aσ>xq32ي+7n3}!@rrrZj'Ǝp0d=yiڴ]@ѐÇ[vFECf>C=SN>(2@i,h҇E@f G}pl AfH 3Hd6 |?d6VZ,c"#X"ߐӧ?pxD=y?w@fe˖OOinzʔ)gϞջ!]vر={l׮]&MtϚ5+??_o B0sWUؐfl$2 Df!Pl3d6J `@I"̐(Id62%0Cf$ydaO?4i|(6*P4Ǐ9M:_d6 a유2?’|s- 0lz(I& .-*_ @yfl)9{M0+!N)??_?PR  2!nhdlr饗w} ap̙3sb߾}3[3_d6!uC se3gnK.Q<`ݑf_|Eǎ+T0ydmGFرƬzѩS'xFϛ7lٲ?g}^z˗oڴ3Ϫפ[ȑ#Ϟ=ճ|\s\cm`(Y-=zX ͢xO˙3g\VJ*u]gϾȖfI&|Ȑ!XdP} .[]vMҡCrPfeeYة"%@ڐeŊRsUWެqiii{>}g;}I[vh!mԡE]|ŧOk׮8G}$o~=ztf>3y:g!,222nzw5P <3ZfsңGuIRapc.YD߾/:ԩSGMԩS%Y8cU}eB%ɩJlSofR: 2!nhC.\Xre\aÚ7oU7裏ZmڷoFxrs/Ǐ/_^YRN:#={v͚5UY_rz8wr/!7ge?3ZfJ%H3ugh]|>L݇~hJJJR`%\b~ښ5|y_m=Ӷ~(g: 2!nh3bV^}7׮]J*+VC'NݻdQ? 樺OW7|SWm*%l߾s!'ذn:kE]ɓ'RCeU3<3Jlg>7S'`֮]{ӄ k3//4hZj~@!B^74MVCgee{ͺgGՍ/">HKKTSU߱cѣGqL3j/?_[?7wsf3g ػ>~qTf)z"eaGS,UPl Mf[f~=%%EYbu>|ۏZ7#F馛[6 ~nu__dY8ydŊg͚eib C{ΌfJoԢy.J̙/8u(,ߍCu|. 0P!@{d-[Vw޽pxfI&/oݹnwYLr}Wƿ?^NO?TF1Aw^>|;f̘5kJ۳gϏ?(eر,8sf3feeծ]ofȑnoa'`{g}gD3gΔaY4h_./̉Ls9Xa l@"B^74MH܊u7]vwARd^{M$Q{馛$Ywjc_y智u>}?/',"O:UV\rIʕ:,ZN>8fߝTH.W$qNQ޽7nܨ8`N@r̙'|RBTJKE~wY矟z.,&&gϞnӌ6y?$@{ݐe67 &Q͚5KNNkQX] /pW:2!䙹+>|7ވ>?0ZÕnڴ>رcݻO^r nl@8 B+24U̙3Q'T2[%+WqG8-&lg Mf d6B3sydO?TPzM` W3ۤI. d6B!ZJPzlп<.4d6B3sflg"̐!Efx͵?2v\d6t=IS𐟟ߴiS+Ymɒ%͛76<3 w *̶xz 4Ho Lxf.2FFlllÆ %IfVnݸL)0ᙹl\uU5j$_%կ_nCʫw9;w6m\d6@Pָqc>}\d6@z۵^˧VFFQ[6mB3s'#g" ꫒B3sx8|ٳ ѣ%W.<&{W Ef*##cԨQYx^ նmf̘! Ϟ=/" xf.2'O=z 4p9bĈ g"egg'%%W z\d69q<^ bŊS+ g"8Ϙ1c HKK3s)##aPt/ұc%3siԨQǏoPNNNJJxf.2?>|xʔ)ʄ N>/4?fa*k֬Ytxf.2?$''\P'NEf^zI ˖-ךaƍժUS1cT^RJC5k֬^>x!UݧOVD>ku߾+F=TTY*UtM7\nӦM5j믥m۶.lɒ%8q .+PO!RSSUKkܱcGݥe&Mm\aWZxֳԮ]^z˗/ONN<G~֭[JoաW_}UEoo9]ܹGd4u]u]"k]v9ME{޽;Z<U3:7O[Ko9L]~hl EfMnse5صkV/RO;vL}۷w߽vmVطomjuo*5JUt?5kۭ['xgDn/buG+#i~^v[o{eZ:u[;sdzU2dy$':t*_V_E5!.wڵ>|:h" 6GgT/Zzٻwo5r^ʾ0ls{dmێ1j\ q2[}38Ήe'2 7\d66m$z۸{JyժUUTl\r[nUm⋄_*)S~ M1c )rsܲeK\}A$^r%oZ;T{fӮQE&z yvg$#ey UZjlQ7oެΛ7aÆ7\f͚>;u][lQe 3[llܬ;wjs& <-o6'nl8n@}#@0<3 g6Q`ZvKn=%Ij]c5o~K*/ɓ'kǛW;S̙#GAno:u_Zר]KFFFng,Z٢&z(^reU+[**V_*v]bz]۹s:7un*n#xNfo9qko8n:V/od6g"C+{6+XoGۿ2eWWS·~h?do`/hܞN8p~ݳgi#G7.>>^6lCuFYúheT2  KYdeW/JG~$#6nat]viss6Dnx 8-ݾm'od6g"gfOk֬9sL˗/>7~׻woIJR޾}{ZZ;.[bFꫯj׮-j`.=w}z!y^m裏>,wʕW^+#7YͷگGCfsVG>u{Ǜcuݷo_T^{a|I}zz\U/JGvӬ;wvQ*W|.n`|_Y֮]$.PU/tlsfp$YOY<23s3Kj\1;nqr;8``Ҳq/{Ϳ2BzY# tZ.\*V{K͞=ځ4h0}tk͛7_wuUTi۶zGU6k,_cVVVnݤ{ӦM'NXR%Uow]zٺ=ͱjl}$Rz)9%BǏ:_d߹@Tץ}n}.m-|./G}ѣ]O2Z}38ΉeOA<23s!7sQ`V2 G 3*o23sfjڵ֭–-[ZnO-MIv#`$yRRR3s!99YB@˖-KHHQFsssŦ$oRf0Up♹l4o޼ M:uǎp♹lt#F7_`"++Oyf.2_jPh6lӧ3sOb F H`KJJ՗;Ef`ڴi-oݏ?8l0\d6.\/epGN:u„ J$g"pf '?~Ν g"p̙Km W\q^ -?"\d6lz(,Ef!B0Cf <3 `@yf.2 \d62!䙹l3d6B3srsșF<3 !555==zhlR/Gg"<7mԊmVfKKKk޼)0䙹lo}􉏏WMe6 l4h\d6؆ Jl&nݺqqqzS`3sA%Il4hp 7!EfeҤIqqqW#mڴiz#`3sAϏW-66I&|E癹l`Ge:#g" M6|!ᙹlHB3s&M$O T<3 "F~~ȑ#_*UG&_%KN@\d6 ؆ +m/֫JСC}6@\d6 <3  <3 -m߾}իW9rdׯ_om۶ݺu;x(i &M4)ϙ3kUϟ/{wyf.2)[cd+={j׮-rR޼ysBB %3s L6V.?a'@`۱cGǎ}e̘1aCO?ܹseToɓ'4[J޽{[55kִ5 yj1^Fկ_?U @i\d6/jժ鵆Қ5kOǏժUZj%#t%++Kj{w*9^Xreƍ۴i##l><3 …g`2sni<}t }y/~(%d6Z>f#B*TP^(v*)E#GYX3ۡC*Vذam۶˗7j(>>^ݷoQʃ R$m=fҾ~I˱c;v-ZHMMޫ*% ^ves=j|.W+H5BRRgf#J /`l0 < 4P{YfuZc&:wM=z4&&FF>쳱۷gT۷ߖjӦ|?g(/S͛ǫ+V$ٶml6@8 @mpC` 2D? >lpAۂF`2\(|Ķ -  #  .8>b lB#((cǎPdҫJܸqlEf&} ^陹l3F 3sf!.@F{g" & 8Psssԩs))9ro߾Zjٲe^rrrT&M\}R/}q&d6@iCyyy_~ɓ'>כp#Jڊ+bccOd233\lRЬYFhB! l:v7d6@5j@ܮ(кuiӦ P*T` 6GpDf={֤I>}7d6@XbQ[˖-ܐF=>@d6@QD§ ߙ3g.]2acرW@ON:#,??_Hrr5k|aizUXvĉǏsN}N(Yd6Z .|rrrD=zޚ0aӧ :M:uѢEz qذaP"lOb =|:Dl 2Du͘1C(mÆ $%QٳIIIz@(dee3@1#@T7o^FF6"o֎;I8 =ZG>Pl=K▒ (Id63gܶm2Rk׮MKKӧbCf葜' >Pl=^z%=^D7VVM5Q7a}(6d63˖-FL2$ z|%rd6@I"@(Df۽{wYj՚5k80//Oر{*UjҤɤI*Tp~?FCk׮W˓'eqcq8=أ>j5lZde˖fAdcRUVx!q(Id6l={{rss%mvĈ[o{~׮]:urH2رcIIIv_j8?~)K//g6駟CSCs>g;pm6x` J ifɹ7oެΝ;aÆV֭[U 3,{rzz*WZ>\[oIO>_͞԰VR5g+W_$$$6O>02$ zfoV0##rʎ2*[ jp+t] <СCU/`caӦMT;GX56' Pl=L3y-pq\ܹbŊ2u֩^K*ٰa5thlDfLf;dm;77WrTv풒T=zfݝ;wO2EݻvzW[d6رC+ ?;;[۷oOKK7VeǓ(Id6-YD^JK/G=zjխ[MN8RJTq\L:UN`ܸqV|y䑪UVVmՒ Pvʕ+7n_|d6@I"@ mΜ9 4kKO Pl=B֮]l˖-[~'%+'%%Ez Grr/ kٲe 1115j߿nnޢdL8Qz ǬY{=a ӗ.]O=ņSLCBj„ O֧bCf2jԨǏ9!J JFFƂ y饗;O:ʼnf̘1꿐Fh-\Pn ͉'́"XrԩS eggKlmv}(d6N'O=zG}1b I5j믿fd۶m3f̐;g @ "@;|~/'''fdҫJUr?''GGJ P!.@F{g"C\JEf1ڇ핞0cp2+=3 `hdWzf.2>h\d6} ^陹l>d6@JMMMOO!C; P7on6kZf?2u]WMCq)<2v۬Y3mI`+233\lR A͛7}~:u7d6@=z@M4@۶mM7d6@ϯ[ laÆ|lѫW/ؚ6mʧ()>ȧ(5͚5}O 2ԨO"G;#g%Pۘ1cdy4 I>|}aizUy}6@ @4w` g6@ @؊d6N>[(QG` bt lFl"2Dxt`ۺuk͚5ڀ$LرcǎP bT JlBe6RZ56hE^ qJ6@#@4V\ٸq6mb lB/CZn?*O>]"˕+w^U/{g6Y̙#w}Wr^RSSoF{,i:>}c9:uj.\(T•\*8"J "^RRñ#$??_k׮UMGٲe:'?~SNݺu?~_|s ]RXjժʳgϖ$yLRb:uܺkT^cZYcbb+zW4-[\9}Q/EfR bذaJݻ?egg/_z_O4K.s ]Z377WRӪUn:U-i9Ҷm[۬iZd "WҗŨg"@ۚ7o*$+ uu@|g͚5R8ps?[|y%_UP!//'ATcw_FSs{8x~̺uF |EGU PhFb3sm+VhԨQ۶mleݻww}͚5KLLǏ۷_n6m|Ǿ_;lРgqgyҿ3HQxγf͒ɓ;v(y-9G`Q\d6(M b ("e1噹lP|ĶA`Q\d6(}>b[!a,F<3 ‚:6@/Q/Efp# BFb3s mEC`Q\d6/>b[a!g,F<3 Žf(Fb3s I3fKaI!* 59e1噹l3FpQ/Ef1ڇhzyf.2>@D+ܮg3sf!"Zv=^0c߇&L0p@UͭSΩS|ȑ}jժe˖zQm4irWK </Q/EfCyyy_~ɓ'4`)S/YYYGe˖m}~z葒nd,F<3 `Fۇ;d ,0j˗/i{3Ebƍ|nݺ=aŒQ\d6}k׮[hUӽ{QFIB3g6m$'O۷O5XfMÆ ']/F<3 `JMMMHH_G߿EZ)))Ry:$&&Jev-[%]/F<3 `0`@rrV @a3sf;wO?ٚ%җŨg"<jvhb,F<3 !55}H(c,F<3 !??I&_zhCNM.Fb3s_n]>4mڴz 4Ho @1J_^𖙙 M! l0..NDe1噹liFv mU[-t7 /Q/Efeʔ)*)r˴iFDe1噹l[->>iӦ| /Q/Ef*uޝO\җŨg"٠Aهڷoϧ.Fb3s'# Q\d6)S\QYQ\d6_>oȑ/QF>$_P0J_^aJO? 3/֫JLѐ!Cm`,F<3 ‘/\[x"Q\d6( & 8Psssԩs))9ro߾Zjٲe^$T6M4ꫥ^Zlrׯ_/T|M۶muv;.tFb3sH$$\~'Oqo%KrjjĶ [r;vL[;6lIyΜ9^{6|)>>++Ul\]@nG)))@" $UXQSv "_Uʛ7oNHHw(}Yzyf.2w}'lTM~~~O>}~Ķ7ݺu{|Ppmw#e]; (}Yzyf.2@׮]իh"{F&3glڴI 'OYaÆV{-tccfw#?c)կ_?=/Q/EfHMMMHHP?Q=ڿ-ZjJH:t !A*۵kl20رcǎ(2fpX#ٺuk͚5s]j#Y̝;W Qvל9sJvZ Hje{Bjj7;I\rnz2;;\rV*8^u6@Q/Q/EfCĻd۷oC&QlٲWX@Xǥ?TTIrZKU:ܳgħ|)]lgn߾}-$թS؇W[KN:um_|۲e>@7?%po@^̆gŠĉ\=d1egg/_5=VǑ[ݻ?əĬZJ֭Sg6ǖ_O4K.s$ 6L (E{\d6D<n|A6+ٳB yyyk׮ÇW?R;rȚ5kU9TW𻑩V ' AT$l-H5>}Z8p`?W[|y}.WN½zyf.2"[C>fT _޽k֬Ybb|?~8mŊ5j۶>lllly~ s۷ߖmڴ}.WN½zyf.2"[O>S6Dv: h3s@B(N½zyf.2"[g>b[(}RT7Q/EfC3zKaGl+;P 4噹lxFo mE`CbJQހF<3 -D Q(E{\d6D?Q Bm&l2t@0J_^̆ȓn=%^Z#Ne1噹l<763-ohBpCƍ7ed/kڴŋCkgJQ\d6D={ǫئÆ KHH4hũئv:)KM'e1噹lH+WwBÆ %IA[lllݺu333D $&QMv:*eaJQ\d6Df͚ɛQFWѣG.]FD;mNJYjFQ\d6DQFӦMd_kӦI(yW/Q/EfCϏSX׮]5kdk 62递g,F<3 gϞjΧ?ޤI+> e1噹l`+WT?jܹ3 >lRv:N e1噹llH@رt|PZҗŨg"!O"oJ8v:/Q/Efx饗^|lU?䥑wݻw@1v;Del>jNMM=tCT0J_^vAXzs=7yu/^WEm۶͘1cȑ;{r@(~,5ȲD'ѷEŒ-yCo6D8e1噹lQĉ,X/-IIIׯ_W(Ç";FnFuI]e,F<3-޽{Сr2@aM:UV}"r땔$_"Q\d%K<}!AZbc12eʗ_~2BJn$8qB"/Q/EfZ#G'l$--m… &,X' b acƌ߄@Fb3s٢ի蓜|1+ bG?Ge1噹l;~r ߿?%%E 8WFnFiҗŨg"E K^dx饗5@1{7>!QҗŨg"E?*6 >ҥK^>UVk b&dgߐ(Fb3s٢Ћ//?ĉL0A_PyoHDe1噹lQ^W _lٲz 6nXZ5U3fL+UtAU|#L` 8q^~e}A)Aoq/bgzŽ3/q|5k֨()> b,F<3- ٖ-[ֵkWYd=kf̘QH ~ia9Mv @!$''u r}=858ow)ELyoHDe1噹lQ3}駗^z;sȑ>(b?ƍ ~hs#(2[ts j qR2dHg,F<3- yf֭[kZZ&MTVe˖>C5k֬^>x!UݧOVD>ku߾+F=TTTM7@ ]UV9uµk׮WeMGdi#)j ƍw[ }{mg>;wѣ\f&M^}U @!.޽;Z,_S;v޽Zdt\7^y {[>rαeϰT믿^n*Uo;rmf6g9E.0@/w/VNN^fc۷ .S'e^*+%={;77W:l pM֨{G(Y۶m;bU뭷{R/IN.q=t\{ `mkE$S=zTƗ;r>rαeNJ47DknZcϰmpMM8(syOm4msyEܐ"Q\d(8mڴIVFܪw^)ZJ*I*W֭[U/"!!Weʔl3&111++~^86p; .e9+UZjlK.y7d(n nR;wnf|.383n زe7o yfPV Z֚`m}s[^[gnʕ5kִw.sX˾cv>Hc +ej6cn3l6iSscS>tq8o9HdHg,F<3- lk{nq'#YFR?%]vɓC§qlv'-Ʋ@x2NNd=k~??p==r3v Z㌌ 3GfPlkTʕ.6niՖY ¾ͽ**VROL]۰˾cS/:66cn65|:>J~gU$Gs"Λw+-/Q/EfB3?\9꣓ϯ_L5իԩ ~Æ Vsp>^i5ͭ^, w<Ǚq_ϟl MɒSUos6k}s[-׾8 ڻ{n\dEU:vw(a::.JEҥKHsGa2Q 4P ;Θ ۹ ss.0p/0ygѾFҗŨg"E!駟֬Ys̙s#/_}n}Uݻw/o߾=--MqwuYyyy+VP_}UڵeW-- ƪ߱c|Y7vŽ[X裏>,+rWj-4sHLLC%nMMΝ;;vl !vF6Yi׮]RRjܣG_}Ν.q=t\{{b2LOO* N8v,GR;#'y=XFNRYq^;l0Uog6mjv9EnWJe՞md7dHg,F<3- yf6_?_B\l<º 0@2lܸR׿UF^z~T/kY1kժpBPq\< ^ZիW; ;n V͛7_wuĴm?Zh &o7xC=t;O5yMz#2B&]t%KM.K⥗^裏Mjխ[7vɢd^0k,ľ>SWnǏoUw|lE k;V0>>^^{>HϰmpE4]:h]y"nl(}Yzyf.2[ &l"KѯC2Bl6g7`ʞJXljEIz"E:e1噹lQVLxO-Y'N (Yvu|ԭ[~'N)fp@C"/Q/EfB{1cf *ꫧN_r"KGh ovNNDD1J_^F|yyy/b@pƏo}!1vXHc,F<3-:͝;7##C_6Oc[o+ bGoEDe1噹lٳsW^yEĄ @xgL"/Q/EfZ֭ڢÇ%>}Z,#Æ ;t萾w}wÆ (}Yzyf.2[4>}+%F۳>`N$bP~LFb3s٢ܴi-Z/$v풻+%eذa?%KL>]!b/Q/Ef~|ɋ/~}EAx˓ĉJ$%%%eڴi|$BJNN[/-Hf,F<3킐/&L駟N81 qI& ;w_KYdW_}uÆ 'OdwLɻF;NZvmJJnMNC3J_^v9sҥK'FyKUnѢEJ,8+ItGnFKw!*/Q/EfC3zKqRT7Q/EfC3zKqRT7Q/EfC3zKqRT7Q/EfC3zKqRT7Q/EfC3zKqRT7Q/EfC3zKqRT7Q/EfC3zKqRT7Q/EfC͵?Q";>җŨg"!򤦦[o C";>җŨg"!7n̬ɓ[h!Gl @iӦ-RNj(IFb3szbzKDV2hr\d6D19?SRRdHtؑeeeɕNJDV2hr\d6D7u'>U:t+w"E+}Z9f.2 UrJDV2hr\d6 J1Bk}"\$4|> g͚jw!1cݻ җAc"\$}|_XH^/#@ebŊCN4iӦM(+Wn:mڴÇGO3J_V*C` mPyȧa͛gFf*%ej嘹loܸqW?y:tW^W]uU˖-ov NNNNfͮJ)F?6m޽IIIÇk5jdU7ߴi&++K.EEE7h۵k|X?QL+c=@壕 Z3-9r.;~=z[nYpZ͕vwVZD+6Abd6G ,ٳgkNKZ;w 667:z|Ujҥ@?%zIV2hr\dзo߷z{Æ o~GG.«Ϲ9sSNݺu?~|^^2@6lժUSwWٲ,-o޼9--k|u,X駟Z9Ų mu+lJ_V׿J$7o1URZZzE:u?ض~~K.?Y[pMl~饗%/_EcXl91Jn,lJ_V/:wܠA%]v1b$4Y駟6n(+ǏP*\2##èfΖl'|"+r y>@СC=jDF4GTvQ'FcV2hr\dxQ>ܻw̫JU۷,!m۶]xQ?H`۱c 7+5jr0zl:uXKϱĶ@{'5eg1LBl'Dd681۴җAc"ŋ}XKC$IU4l>83gΌ-Zd=!#.DfdC=Yisׯ7'} Z;`(&BlFH{q^Y۶9^^ALh=Xf;VBo zu~ls7j嘹l:*Qی,{/?;w3@=ڸL8A ^{ TaAAߟ$~aK^zz2$?ɓSSS{1:fv;k֬)ח7ި[\/^';|A77|_>U!2ss;v*55k&Ϫfe#/DG=p[-fYek[͚5.4rz~KkԨ޲eo6|.o=E&ՓjJ¢ =۟׵ի'j?id6N$0$$T -Y&Oyɶv'MML:OIl~h[hic匭ާOO U ͚5/;NڲRڤɣ:mSJw}-UsM+t5YK7cyig2[evmO^9r7j嘹l:*4C=Ըqciryս=1cX*ٶm&Lu nw}W.1_MũyZ1b/K6iҤM6[xlgBl~r-ýz2~uG{JjZd 0rP d$yxΝr5Sa s.7=kwBĶy17}WKfQ߷]п]z(=F:{u-P˜O#%\|UĕeĈ}PU=tGI;}mr q|ZlَV+EfQRrӧK/d-xqǙsl2UFBEz ˖-S'*U|wgU5jTVV snl7oV}QFFZ7nB _ ̙π,_<11QLj߹W͛يVjW_kwպս{?\4dL_ᇳ߼zrÆ_c,K}PLh=hv?jhU/\%ɣJn4Zwߛ*Z'gmKÅ_N[~3}dgvʝv̮ZB?zZlHD/_Ķ[fffp r>W'3@%ٌg3R{7%%Q78'sM"=Hᥗ^:i$smf37_jU5&,F@[ lDD7mܸs!YԬYv&Lw}ɌqㆾZqC>۶/m;. KüUʊ5s[_rIRZrT:?=Ğ/8BĶyFmW:ܹ46OA -fC%%TdjԗfHo۾Z2KR_o䍲-bm -i拞qә2 =~-))WcRp£GʄG%WRKQ=X~H2GRh"Ν۸qs&,߳_ƪɶuiUMURw7>]}6{y߳Y~nP}CSq~\ta۳=[N֮o$P5&}d cNF|Kk;ݚ{첺FEūyHLML#կ߃Z7Dz)َV+EfQv>b[ٙ =I0`@6m|e -<իL㎻K.sGYt{Փ#NLL>}4ٳ9Ioڵk7h խ&D>6VL>w!;msѭ[zk;v쨛ل޽{ kZ/©I|э2WOvXӺu_ykUP϶cH_.!%,oګT`dF۷Vu~"7oUS?m=ƍϮ_@>?IsfٜZCq3eU52wի'$x@Y8@[eq:~n1sټuTb';TNsLuM7=ٞ{1cDƽ[fͤ$ciD"Pݺu?SYy,׎;Bf V2hr\d6rss"0>U`;˙RzWmiii kݻwqqN8Bf V2hr\d6JKK6mjL^2iҤLyN>jG=!^kM`;˙R9s;׈R-אX\,lqooذsϥ 0Z!*)SXsO:e=!#XlB92y&''[13@%4tУGZ}$Y2G 5y̭zM7Y+13@eb gurrrJJJGTBf##GTsV:us8TBÇWQd~Xj"Xlq455UM[trWp|ѣG_|#GXc*K;o„1,, -~jOr|Ю]$m[TVzG+}Z9f.2[y["Gcǎ܃3@tرaÆm\5׮]k=x@ Z3~ ܃3@b In&MU[n6m?ӧO[PYi/V+Ef`@PΚ52D3Euf̘{nA*=ej嘹*6gep#F"Z'z"5fTr*..6? @˳h [UQmŊCI/E1'?oO3?zsPZ?唛g<4@)gAh [}fԯYzVc@hMSiiiffی(%R7=vGEܢ81f@3##C65e====o5l3eoS PѼFkƌ%QM۸qc) i [ef>|8ߐě |S <FkN8! {iժ ;Z5l3gmR|;vlII'c@8iMx__e=3С7˳6tУGZ?~'c@8iMxbʕ*)M6 #ְu\d)SX2;ufLk+*]s5}D˛6sLV<_hfLk+cƌQꫯ#ְu\dW_}:#@<9qĄ E80+fZ?^Qw"uEk:f.o2+bڵk/bkתUZr6z0;vXy?JLw]ö~.BKay;fܨQW^TTd^?eE}c@iMxH݉D#ְu\lO\}ޥr6Iq0wb~<wl߾}uY+qP~LkCN$ZwQc"Q1I1 /ȿ2oSϟ/sÇK2 E``5j۶m2)kA~ׁQFJ%KWZ}Q5m p -c$ٮ;Fmvʶ/;lo%1nݺU†d U(D oyvm|پRKO<U RͲ ; ?/=ggg/믿hHy,0Ӛ1sE&۷O7oV̙a|޼y=Ug˖-G}UZhW_ʾU=e~r&@{˗'&&e˖թSļE9vk`fj~߾曾ݻ+|RJ68|_cDB3ѭQʁޱ@ﰅ/ӧOӧ5\#ʛ,w[jޱVڱ@3RKBfssf;Jyyy.Q׬YSUo꿙m:th 6uOl'@v… =zT Uh<Ћ1Y]e骸8))IfիWWjTbPg l5 ~2կ-Zg7qƪaw,;l!nkٲ/{7--M>j|MؾQjݿO۷,Ў:zWj93Cfپv-Ag3oZ3W2ݻXҶmlU[n=ԑXǎmgVΒ:a޽km۶m"3?2{}e$KMTҏ˓z mvEvkfTUۚ}JޢN:;.&9rdҥ08T+eۭmC__xӧNH2۵k7h Un; _F*Ԯ][gD7}}-lw,4}^N(Ռ=743ٲDy,0Ӛ1s)]p ʜG2KK'FUy]tIHHh޼L)W~~g?Qvo@U|W:۷^z2lڴk&7odCRM6_*?5k֬U|`;c3}礭Lnjc7ݕE`T5 բEL2E= ԕ^yIII<* | ,O+?@x8f @Vv_|ȑ#9bҥKyyPѼFk[V+efvi7ߖăieg@p",К1syıcdž &ŰkZ8UZ?r7Z9f.3b yO43n:mڴÇGOrOq1 BiMxj嘹**)fz+1M(#8cƌݻw[qE'z"5fToZ3WfuTa-AV+EfQbg>@Z3ZG|njr\d6oh fp@a D1sټuTa-AV+EfQbg>@Z3ZG|njr\d6oh zZ|˳O0"j嘹l:*@3|)g0"j嘹l:*@*--4bqK˳TxiAV+EfQZϞ=322TlSg0Z@Z3ZGj/rJJJVVD59ƍKI~~* L3r7Z9f.27 N<) nժ;vxiAV+EfQ]=0`e:t0uTk%Gfnjr\d6oh ڭZJ5iӦ}* @V Ā-[ֶm[>iAV+EfQb/2W_GB1"j嘹l:*@ Pw"3@EcD1sټuTؠDG1"j嘹l:*ɓ'ϟ?!=sr'͢E~'AJiAV+EfQA<۹s_}ݺuǏE/ZUH~?~7~ rbD1sټuTN:%w9|5:(..ϬG"iAV+EfQA<0hР~P|@D1"j嘹l:*7ز8`޳E@Z3ZGqԩS "E3f[z,5M /Xk߾}iAV+EfQA\7nۭСCO>m=:&LsN^6haD1sټuT?v?њ ?cAfc]Oz- Zfnjr\d6ohď1cpK/Y2"M%A @V 8qqYs{W)5vQ :fnjr\d6ohĉ^ښ=n:k,q v12bB4 wPc"yC N%[ h=!#.v؆0"j嘹l:*5xmڵ^x4FTz˗}[~}Z^3cV\z+ϻ$z\C̶fgq)K}SP3o>QKf;ClCfnjr\d6ohĉk8d _ԨQSNRbTyHEIϺu|![BdCy6!z\l߾O>K8BMhWiAV+EfQA%YСCO?5\s~I#ao!V y D(i3nܫ|m{{g*&%c,iKE:v؆fnjr\d6oh &k''NؠAkvÆ OJJj?{/==}msp⋍'ONMMMLL|$%:fv;k֬YN9rw7xnݺ)))/˓Ûo /cG5&U;vtUj6kLU/G^ã>zsl3VH,2@5wݭf^Z{F9v|SO=T5jToٲ7>·ӢEII5UaQ~Zjo42'G7&$T -Y&Oyɶv'MML:OIl!A -ݾ;mlz6ߟ9YFEZ|֩S[P8T4yWgT㮯eݶv)be&ۿ~8X~,/M2[w~94 wPc"yC Nhe6 np>ܫWnݺe+iɒ%$4idӦMn͙MƲ;wmv&mֳgOi(M6C ^()):Uh޴d?PŭȞ:td6_ތBy?v}F[[*'Y@귿 MEūTvԮ`JYò=+deϔiأoܥ F]ed;}RIDAT;f^Ml_$%ՒYgʾJ@wқE0x_,%nd~)",#F<-Z};JJ7ߩ}oco O˄ c% @V 8JfUz_-[VN_ojUR3?ȨQ$W usfۼy*裏222ԺvR_h8gPugY_|ybb* lĿs'l?Ʋt ΄&]`;f^&^UO~}֗\T եOo e;fބ[UN:wn/oͲSBKYPEII5";Yŧ<~۶̒ԗrf3yl˃X!9Bf-@Z3ZGqB+~-))WcRp£GTdUT1RT+V_~ [-Zd=ܹs7n|݄{6 :X<2nݺ MԺ߳Jʿ&>["(:m9}fA ՟>Ou%KsmӅml:Yn@̛Փ*E:9K/XhtkY"252I?SQ[!C~'\zBO,o ԏuDž[L3r7Z9f.27 D(sLuM7=ٞ{1cDƽ[fͤGyD`@uOe}RaÆ4e7O>iل֣Giu%O2/l'VZ|sf۾}{.]i޼ }Sٷo_ɷ5jhڴ=!Yƍ\M~3e;֬YN?bf߱۫nKoݶ9ի#%]c. tHN\~np[?}@,HmUlݶD&$TkӦ1HBn}#kԨ#woh~G}oQ_˝%k5922[y2`4 wPc"yC N7Κ"'62ŋj׮ݻwbk Aאʳ3@Z3ZGq/\|5 zlڴOb! @V 8qԩcZso}Aq Bf-@Z3ZG#''g޽(hp#FX2[L3r7Z9f.27 GII/lMq~~ Xl4 wPc"yC ̛7>TnFKMd6 fnjr\d6ohězkɒ%Xȑ#ǎHM&7 2z&L3r7Z9f.27 ;3m4k8@SPP MB@Z3ZG)??5%Ҙ7oȑ#?n=xQL3r7Z9f.27 ?pӦMۺu51 B6m4i$9.yyyc @V 8w̙ݻwO>2D39ECcǎ8`=NPi0"j嘹l:*@0 wPc"yC13€[ @V  |>lr7Z9f.27 38 "j嘹l:*@0 wPc"yCC>/;;[>0 wPc"yC+^|~)|njr\d6ohxB}ma-AV+EfQABl|>lr7Z9f.27 ?"3€[ @V b[q@a D1sټuT/h`Smęa-AV+EfQR\\[ZZj}"_Mэm'?'3€[ @V C^^^ffիW[;/dusg>@Z3ZGfofZZZjjM74uTۯ|M d+o1%%%==g>@Z3ZGC INN͛[v llS_eddkNb&Mla-AV+EfQ?m{oQ}VPP:)*I?ꋵ+UVj[0 wPc"yCeTߚ4Z;w5>lr7Z9f.2*ƍ1 55533sر;v|&m bŊ{G56OjՊ @V̆36Uwqʕ+g::;l^+RmG6@5-wPc"!$25h 99Y}f{Hw6lؠvfdd @V̆HԴiSktc[fP_̴ٓ&njr\d6DLqqqnnkBm63TZ?r7Z9f.2I(M7PiMxj嘹l2c {&njr\d6D@IZ?r7Z9f.2l#b @V̆hem6@ Ӛ1sTl۲e ô&njr\d6D7m2$l5-wPc"!i W: @V̆5$:\r7Z9f.2֐ p"j嘹lzZCÕ wPc"!i W: @V̆5$f@8q"j嘹lzZC@yHT4hжmۈm@8q"j嘹lzZCk*۷OVm@p"j嘹lzZC;ۀJD1s 't@Z3 QOkH()6 njr\d6D=!@/h`Sm@EJD1sϊsssKKKO !)MAD;\r7Z9f.2֐\^^^ffիW[;/dusJD1s9s4l055n:un>mVNRRRҖ/_n 0t@Z3 QOkH^rru]ףG͛[v llS_5mT[ƍ l \r7Z9f.2֐&çE={nbXkݺt~yW: @V̆[s3gZ;P>֋pq7Z9f.2ģ͛7h@M5k6t]v=Mھ 6mzҌg.] h/V+Ef[jj\QRRRudْױM6٦Oޮ];d.Zˠ1s HdJOOkbb`3ڵK}햜AlD ej嘹lG7lz5[-H`3>}mgD ej嘹l/sssmXzl 1ddOdOPh/V+EfJl lDej嘹l`6 i/V+Ef8l Z3 ?@+}Z9f.2 $F` ej嘹lPضe Nh/V+Efh&! Nh/V+EfѺ]Z9f.2@u1szCD5wW=VУu ir\d6QUOc"h]jzZ3 G:@Tswj嘹l懖YbV2hr\d6ܼ<:$ Z3 ࠴433ӈmuHJ\*E+}Z9f.2ٽޛbzzzSO=e @ J_Vpg%''lR\f͚II~~*1D+}Z9f.2 $ڵʒPSSS塵E+}Z9f.2 $+P.] Hl:uE+}Z9f.2 $LNNFq@J_VUΝU`kذa~OsҗAc"Bgּys>Zˠ1s222:Gq"U\\l~hiey1sԝH Nrss_1Ṷ\5s\d6;wu(VZZzWh_fffkc"B 4k֬lY> @ 1]6jH6j޼y 60`3 '+EEE6@<=͙3'99EۤTǻv9f.29)6@<=ƍ7m433SZI`k۶M7d1s '2~;2P嘹l`6mZM.*5k֬y>8f.2 _1L+oQe;#c"Blnl+-- F t3ۜ9sTfk߾GEfؔcիgff(nf;[v'iGEfX4$N:SN)))iii˗/RrԝHB☹l\6?/֚6m*q6@tqj(B☹l_ k[OQEf;1sek4sL֋eZ9f.2%%%?|yg/¦M4ץK,n=d6@*OlSg}ӧkN6Y! P6fصk-999##md6@ӍmAo6> zl 1JKKsssOl0 %6b >c d6@Xm6llBfD9 ۶lB` 2 b$u@d6@$i]CZJEfѺУu i]+3 G:@ҺV:f.2@u8ut\d6qHZ阹l=Z!ֵ1s-!˳ܼ<:$%¬433ӈmuHJ\* {PM]d===V g%''lR\f͚II~~*qveeeuy橩Z 1p@uEmРĶSZ+2 "<|95#""s*5lذ_~֧@2 2>3ٚ7oG q "F݉ bɓ'ϟ?!JKrz嗭OTV-駟o:1bΝ~&NnݺǏ_|a-;&j~~q$qK@ؐ :ujSN=|5j;> * [qqwa ` ,7a@f(&mȐ! Ųe˦Mf=*xԩS&E{キn:;d6V&La7dȐӧO[!@Tڱc;cMUV͝;z +]"+#GZ!@9y 3mڴ{Z^ @Y`5kuz 㭡c=Nxчk׮ ;z ϸq㬉|$$$ȧZJNtCK 0dݗOfT2D̦H⋭e9b-ZB"*!2DwMI&5lذsΣGѣl~z)㡐+5wuw֬YN:OeO>R?))G=pQ7ި[nJJŋU'6h !!kݰafz?999.d]݄dɩ=ءCS[4\  d6>3|?lm6+$|׿4nٳgqq;ڴi3dSݟY^z}/,,޽j/BII{uשϗXxaiխ[7?/5%8jWl7a:ewٶmlhv?!* Oy2䷿o)+s̹+r`_jjKEEEUVï*--ͨgY_|ybbiٲeuԱ˓u ^fM_MN=裏ܼEKf d6@!@q,{x㍲ҳgϑ#G_Æ ٌVZUF bƍ_IYk׶T3GISeUhfR9О;nv끐ǫV\\$1z7Qu*KDolЮ]p…G*i^ 6wƍlm=2 xD޽:ud);vm'w~}}~u~en۶m ,T3Pm4юǓVTTآEM6Y!al999l}-ZrJkh@H / :uf)S8pz ҸqkG>z RIIɫjMyY!@Oϟo #GZ !@dk@9rdǎCf65LH`۳g)2D5kdgg[S*̼y~qkd6O3giӦmݺ՚0M6M4iذat6d6)w1cرc_rUV[t* IZ!ֵ1szC!kc"h]CZJEfѺУu i]+3 G:@ҺV:f.2@u8ut\d67\gXag<4_\5d6@fff͸I˳Td6@s=7VM]d===V '''lR\ߦMJI~~*q&Mm6++KC͚5KII묕 =\n&WW\1uTk%@fDġC/?q}[d6@d|*5lذ_~֧@2 2>sٮ >@ d6@4iDC} lQw"#Af=zxW^z%3Dhdu[l$2 4h߾}> kQȊ}6@L#\; l7| )6@l#@7n\zqqqO2R~̙N:umyyy֎*k`Slcdjժݻw׫WlYhC7oޜfn@Bf׿U"ټy$ҋ.ԩSWĶ׿]tyO*q`ScHcK/=KfD2ąΝ;7h`FI׮]G! M駍7%ب +W0*}`S,6-O>DVMxGT! BnnnZZfI9|p޽333*wRXXXؾ}{3Rضmŋʾ_Nkixc[=Hтqo߾999BlRV@H(Ef;8q\|e˖5mڴu>ljժ3\s5=zx'T~^z#GuܛI6m,X`nn d6ql˖-}^z%klXZ鎙 fi}\0wm֭7pCQQ98c fh};f.2qŶF`lbe̫l3bGc"YZ6-XZ鎙 vZ>.-ƧlXUd6g<4\JڨQ#5 V.%Rn TwqGFFMOO0`* z UV%''hB&q)ʺH*P e5JIIϷVD2ӪU&Mdffǥ+RbޝGU DYI$@TAaDPA ggCըH]%qe0nY U~ ,a  ?z9>tG͹z|yÉ'ڴisu(+-33o߾z?@B!@$?|֭;u$R^eYZN@q7XӟԪB 45̳P]"++  ";,뫁zK.jՋ`ʕ6y#Ufy xzI_f ̞y)<}B=?NjT&L R^ Ep~A[r?G}G:G]ׯvlbbO?ԩS׬Ys@[`ޔ^|2扂#V̤IJJJ;߉ث+v{g _@Mxĉ^{ }Nd}~ Qk{gݫ_PYYKdj_=2/mݺUġZ`LZ(,,,../tپ}ȑ#&`329)ځ7)3fo'jD%KK<Olqĉ<[z7֮]TXZz$9$ Af-[5$'|~+&WO‰~Yl۶mԩD[n6m>SCB)--={~k>`ʕeE_]~! xJ7N?sG,/ږ5d2 kM4Qˑo_--ZHu7&#萻p @MV3?T;vTTTJQݮ{ŋ7)L;tagMm۶NSJٚ4-Yu W:|{ݷoߠA~a~~.1,`Y;ΐ{Wvذa9ŗ/2!j&&GU{c-^z!ǤF܅[ jj$K6oᄈ}Ƥӽnnu6d6)2$}RJJJ<n&;VVV.Y/e˖gv67|3gi)ԩS322w'oO4$9s攖Q[|y_ /UBdxCf}>SC(++;v~SaiԨQP?7o~Q#̴i/^PUVV9ѣ%yyyr=KĖ.]kW `KӧOgm$ɫ~/Q#;wƷmʌj5X"Xii#U={1c;EѣGz$6?5kGf5j7oެo6lx饗\RyȊ+$uN&G5kO?_@?s7xcĉ&>)zS{8p@a򢢢'P#VN7lr #|ê3 oYK V%ݘl|˪\qLj߰*Ef[V;F,UI7f.2߲*@1b7J1sU aUҍ%wX n\d6Q^^Q+Z Wd6].]:?#UUU8`\J_@b F,$42u}kNMUeiv+P\۷wX lCջ ZrejjjNd,R^eYZ] 2>tF֭[]t-33sR.UE?C6m?tUFlvڷo߷o_ [Ζr),-z'޸[dw}I`+,,; "JKKS_!<ٿ/gfgg3b ё;t&nݺze2\2==]ʥ*]|<}|f >DҪuDfR.yz O NjL2_&L R^ nJ I8 X#xcO?ԩSW^ Mpر҂1AMB EN8!Ӭ^{B!q߿???>opRbTC4d6R^^uV}څmN22ȑ#8϶ȤVϘ1CIQ]$P @ 8qbLm}7Xv~˓Iڡnd65TPPe}x'~'JΡnd65uiӦs+Hiiٳk) "hljb„ Ũϒj(@02k|<^ߧ\ EFf`mҤI|*֬YӰaC rB?3SɽoOlT~[iӦzka(@dd6ԩή"ɫewZnn !kqTr=zFnJ8 EqvDUVVMy;of:Xp3Q67dcxPlg6SSҗ^z)--sϵm6%%gϞׯWf͚5m{qިΘ1###CZNnQ^~6m4nCԪ]Hs-)**7>-ZhݺEܛ rp;vxM4i޼C={˖-6>^9| Nϖ-[ʪ?\,˻dY+.(G-[f0ꐙ|Ķm .ܲ lA?!`4-!M2EKcjjĉO}GT9Ǎw 78_%P/X3n&ռX&2{Sݮ{n.^XQMje١C 68TԖ%镗DK.1bZ $[;|f^{-"yq9xNڵݻo߾A=N}<9l09rHL>\-_ve]!w>o]]K/8C_wcN}]>}/m!`4-dĞvi*}e%%%[E_KeVDF%2k٭&TfYtieaϞ= 4ɟ{ˎ;K.[lqr:Ȗ7nܨ~5kVO.3c]ve7n~Kr2uv݅ӫv߿QF|/HOOwE8)8=e-rHjI&PGi&.8eozɓUcMwg΍={vwT;h8TVVvg+*\3"G}Ԯ]@a(2k٭&(i_|Qf{uYҲ~zɹ(wK/RTǕ+Ws9j9xЖg!;h.rOe|jc<nG|ʚ(Gun S\pg M[ >@u@ׯlO>N}y'6u=3|@ HwKlg6Ԧ;v?#GHqP=۠A+˗/?{=*? ڵkSҐSG z-i'.own!Bt/ۅ\S$D85SV߳O;hn!qСǧQ"T$C9_gWVCCX3nmMy[)%%%uzp 7tL*++,Y~_~ٲeK:QTg6وwyyy0ضm4it#\A9s%92pݴrZ7g_!O!dOm9.G~JwIqTGwc;%xw8p@_|-h';BlΜ9"AnԩڻE 0ݺusZBH6lg6 >#)))2k0aSvv[&M5kvjoעEy9q:8ύF< oڴrz7Y_/o~B;[^|jQoN\C y9deen_{9.'c(ر#s35)e˖J])S [pw0[|H7nի ]i޽g}%]^;-!O$Pd6֌:r2 O$DF￯.j y"#z;rS1իVRٵiӦ.oIl"ŎkTFESRR==ڧ~l2}Jٰa̙3S,"#vĉ:ݴi<~bTgI5  @ML4i >rȑ1c7SEАÇy}n;wnii~}Q]?%P @ ͙3磏>ҧWq< 0뛤FfPsӦM[x>B9rѣGGEpJaaVYY̒UɄQ]0AfP[V1bDii>B={1c;è/"x|G5}͛7/o6lxGrJ&1Fuc(@Hd6^ڹso1qg}GʥޔNϜ9?~A EQ*@1b7J1sU aUҍ%wX n\d6eU.coXtc"-r #|ê3 oYK V%ݘl|˪\qLj߰*Ef[V{$I1sU^QQQII{Ju~$I1sU^UUUNNۜb iv$I1sU??ڷob˗/OOO:t8&!Ef[V)SvEbX l[.--ջ$Ęl|˪\q??mڴ꫻v*#V[$I1sU[ozwvW jb\d6eU.xY~Jk"555;;@3 oYK .BzG&!Ef[V)Sֽ{w>>`5 1f.2߲*@'Ȉ#Vc"-r ėz O3 oYK X 7n:1l0&LWxjb\d6eU.D{lϞ=`)f80r bjb\d6eU.@Glر3 oYKH F&!Ef[VPIbĂ$Ęl|˪\$_`Sm9I1sUIbxjb\d6eU.Ibxjb\d6eU.Ibxjb\d6eU.۶mz- aرZp\{w}wVV 7loiڴ/ 6$Ęl|˪\"9"6X۱cGJJÇݍ9ZbԞ$Ęl|˪\" lN4hеkK.D~n.]ty,/X ܖŸq:t蠶6~S;yHNNNKKKݫ+oQ&otxCd5 1f.2߲*HB6mڤSLA+믿.YHw}Wy睔>@z-ܜ 5jh׮]Yp8-EEE;u)hCі-[Z%}|@-XUc"-r$TQQ裏F=;[6l.T:ȑ#_}٪,hxAߧOj„ _|M̜9gϞ999;w>󵵑3[CLޕH(eUU%1@7c~O7opw__?W\q,[LW^y;GP#H ׬1sUDҊۜs3oYUc"-r ĝ{ݺuvyDr[VUŘl|˪\qo;&ǏU5\'墢"mPUUըQÇvgѣ\UU1f.2߲*@i#vȐ!?ei  6nդ۷'O\RRD-b\d6eU.Fodgϖ0ZN?'N)֭[7mڴO& ޲*Ef[Vۿm;- =z$4Y+Y8vݻU+Vo(_| b\d6eU. EEETTT <8'' /ɓ'K}..]H%\hѢ_7_| b\d6eU. C  jê3 oYK #v޽]t˿{W 1sU^yyGmjkS ޲*Ef[V{EEEGGZG1sU^UUUǎ> ''G1F-b\d6eU.k222TlS#W_MKK:tD-b\d6eU.X` Tm,H`KMMmӦMiiD-b\d6eU.xСC۶m%Ɉv_qz'1sUG֪-RXXwBQ.x˪3 oYK ^***RSSU`;uG1sU誫R-771sUH=DӇ1sUC2byHP.x˪3 oYK ԓHxHP.x˪3 oYKƮ]|ѣGˈ3fё7iҤ3g5j7o@,X@o 6m-r[VUŘl|˪\իG)3c} s;vc1sU_}4OqyՇo( ޲*Ef[Vћ6mŋ 2PTVVJl;z>P.x˪3 oYKDiΜ9v=n8}KyyG\hk$Ęl|˪\"~gI1P͝;7$EEE%%%Ώr!jjb\d6eU.I&޽[ۑ#Gƌ 檪N:}G\H_=I1sUщ'0Mw׿iF6U.dYZ] &!Ef[VF~e06l0sLmHITr!,-lYMB% 08dc_.R)Ϋ$Ęl|˪\hҤI,86֬YӰaCFZn]ӦMV0^d+<521/999Nei;=I1sUQ4&{NIIiܸq.]{=K~ު]^x57~Iffd$Ęl|˪\Ș:Բeɓ'WTT>|xҥ .;k :E[s ~~IFF y#b5 1f.2߲*02f+͛7o֬}wi$Ϯ~{־cǎoI&އzRqMs=k۶mJJJϞ=ׯ_moQQz>;e#,nZC2etN8QۅZ;_nӦMƍ?J$Vsh{ wƝB]Cm/Wl-dS-rT yl!2m!xnG82[iiiN4啧$Ęl|˪\Ș***dJ}׿۶msͽݻo߾A=Á0Jkkor`q~}ݻwŪXnڦFw0ϻaÆ>|xȑr"ÇW˗]v,N;4yxJJJ]Xݩܷo~%1٠%ܵrws_y{L6%zr:;B[7jQ-\@A'3 oYK3?133SbL޽5oԨ7||遠0p#ٸqjڷo4h_t͛ݨ]vɲ.Y&MŸEYYg+HxPܛո,Tg͚ͽp͸S0wovٲe7v ?otߑ[t w;" \3 oYKE[n[W"_}|4=Ilv|2wW\y9;^|E uY0p7'|gj"P'-}m-`:Hkfit!|:[7: nnG82[UU >CVc"-r #JD}{gst֮]9]޽{4h+;vw}'G/-9ÝСCǏOKK ぅݚSg333ղýp͸ӐK+npw$d7!B5b\d6eU.adl[n;v}UnK,T&|8ឣ4HNȑ#U 7pM7ɔrɒ%JJJ<Frȳ̙3Gv*9gԩSwa<[S٤w}.//Oqh0ܵrwP_pu yl!|GBv %b\d6eU.adlw[RSSSRR=믿~ӦMjLև Ҳes9'++<@&M6m|rg;9Le;7o~BLn6k{USݧBNup)8SZ9; S|m-䱅|s;" KVUŘl|˪\Ș!wP'lUU1f.2߲*02~<'X. ޲*Ef[VFdDf @]*Ef[VFūV'@"ؼy;CioYUc"-r Ǐsa ̘1cϞ=v\UU1f.2߲*Ƅ FƍG(eUU%uiӦa~+--={>OE-b\d6eU.I&mٲEO|֭[92Po|Dž r[VUŘl|˪\։'&M'Iٷoĉ?#}ԆG-b\d6eU.Q3۷o0a”)SsۈG\rҤI2 1sUDm?~xrԔX QcO… }Fr[VUŘl|˪\qLj#.>oYUc"-r #6eUU%w8UU1f.2߲*@1b㈋[VUŘl|˪\qLj#.>oYUc"-r #6eUU%w8UU1f.2߲*@+//wXm-br[VUŘl|˪\u=b]:?"(eUU%P:v6gΜ93''G1F-b\d6eU.k222TlS#^HKK:tD-b\d6eU.OefggKl lmڴ)--ջ"(eUU%/YYY:v(#V[߾} br[VUŘl|˪\2|pm۶mUn+,,;!(eUU%/*ѣSN<}Q.x˪3 oYK j> ޲*Ef[VO?T}֧O> ޲*Ef[V,<}$^(eUU%_I$<}$^(eUU%_۶m[VV@\UU1f.2߲*@UVV>oV^^^ W#(eUU%/*ٳGҚr[VUŘl|˪\ql -.(eUU%P-^(eUU%P6V(eUU%P"6V(eUU%Pg lml****//Wr[VUŘl|˪\u#Df͚um䔔oYUc"-r Dm>`vRSS۷oO`1oYUc"-r Zf͚U/k׮ڠ\UU1f.2߲*@L&)*mݺU}Қ lG-b\d6eU.z6mڤ0K9@-XUc"-r Ĕ$.m|ȑeeezرcZZ m2߳@b5 1f.2߲*@jT` ~mժU<@v$iӆ$Ęl|˪\u PUUUkԌ bVc"-r ԍelVZ׿$Ęl|˪\u&ul29] @ݲ3 oYK.m PXMB%Pc $Ęl|˪\u/& YMB%>`5 1f.2߲*@c jb\d6eU.8Ro%?XMB%_d&!Ef[V;F,UI7f.2߲*@1b7J1sU aUҍ%wX n\d6eU.coXtc"-r(UUUxX`oXtc"-rhH`6lئMNl#|ê3 oYKgIkJl#|ê3 oYKDl sX n\d6eU.Ap`##|ê3 oYK.)61b7J1sUDHBl #|ê3 oYdP^^^TT$1L_F4Mme99}ErcoXtc"-r$JJJ:vxmN}`Sm-srrHuI aUҍe򐰔ڵk{.nM>%$Ĉ߰*Ef[V2HdС\IPm۶վvY`ScۢEjّ.'' . È߰*Ef[V28MIKKS_ܹƁMQm׮]kjݻw'Eƈ߰*Ef[N zKa~m}>ɪ3 ܹs[nݪG۷GZXXXVV6||϶o߾#Fvdk{ U i߾=߳ P+*eee_^?sժUZ]5m*\&[~333/"ɇ6@2 jn޼ym۶̦X @b[p`s^ntblԹsg틵-B`sS_FfDyyyQQQ/B Dۢ l999}@fԑ@6{d6@ Dm6u*&k@8d6@c lPm6" F|'!g1sv>HBVEfر Y}V3 `s$dYi\d6!g1sv>HBVEfر Y}V3 `P^^Q2N8??]:?_l:VUUձcG'9Cܹ+ ^nnnZZmshܹm۶:tGfԵLm9$u5i׻l8֭|o^^%uСo߾z'@f / QI+,,;2 .T`KMM#DfGnnl}#CfGiiizz|uޝ 7I$<}lys 7;wϡ2}8aÆ[ÇjP#iM^mCf5w`SmCfԩFl 2 l `d6@b2.D&[.***//W d6@Eؔ(cۢEnƜ}~AfĖm`S"ĶÇeeefffFfP-ZtWKTO .=2 Vjv|֪Z l$AfĊM69orIK Ϙl_I>|xmg۷o߈# {}dggO=c"NElS-}UV=]tQjj* 1s@b[p`sTUUZnNla\d6@!l8-ul29$c" Dlp3f.2 @F`0m6jϘlF`EfDlxŘlhضyf^1f.2DVZ1sv$MCfCEf!!c"1svlxȘl;d6}ѣg͚O?73f.2xرcfϞo#G\z~G%c"d/z @^MČ1s3GVYY$%Kk 63f ߰Ν`bl+W;l~2qÇ3 1jԨ#G$ݻwO߇/L2Exʘl'(YaÆz ֭[״iSdgy_iĚ1sdE_=zL>%-ٳGz]V_RRRS@- SDef+((o62f.2x>7?:TUU駟s=-oaݺuQt2L Q kEf3E] /h*<-ZhݺET͛7k;pj/++;iӦ-3fȐ:8ߘ]y 6=\۶mek={\~jܶmۀdr:!P>]nhTe/!SCfĚ1s3}.QD{Çk^T{nn7߼w} 4VW۽{weeŋTfҡCgW:"|nwPxǎ.?ÝwY^^.اOY(<]nkۥK/4qjlX3f.2x rfꫯN;4dٵk,/[qƁ5j7ߨ>_|Ezzz4h_6Ilر]tٲe{{AnwJpfs,]ys0{හ;wsvg;#ӦM}֬Y5c""g6 RKwءs$I'5isϕ?y4wy/J1fpS3ۋ/ؾ}:@Ё\28 ;EȞ5c""g@ICFGy=k w lܸݨ|?srwp/hN2N'9rD> }[wջB"dp wFl~aq#b͘lcf㏛7ooF~s#oG[j ؽ{)?ܔ)S?lzgݫgWZAA$Pnj1QUU5yI&իNOo߾]Ğ1s ~ᇅ N2er_hժޔȭ)..gȘl;&PSEf!!c"1svlxȘl;d6O${EM駟~A}E5-,[, K![l3ÝȮ]l3۾}F{6HEf|m1bđ#GdСC+VpVimݒn*CuOvv̙3eaԨQ!Ce]$r!{1܉tYCJIb!ύKT6lO0)ر㮻R &8mO>ڳg'x?}Ywn_ 0cǎ]v=zt$p=C1܉,YCl~Ee 3_ b[-~k=ueLb6HEf|_b̙ӵk׬=z[N_oT6lO0@,c[B# @20f.2l-@l B`KT6lO0ͅ$l1s`f ĶjAe $rF[ $}l# @2f.2JJJ3iΏ@6Q$!c"cǎƙȜ&''G#eܸqg6SmҥGIS1sـv5dddئf6=\ZZСC |6mڨئ*,K]$ ʄ&;;[b,H`KMMMiiT0)eiiiդɫ,K _3 HlYYY[رcj+NP.R)h՜Wi;_3 Hlyyy2iӦl~{aa Ա.]&dnj$ÇnZMkz٩S'~G@:d6Y1c" ojZsfddHeW>ߌ$ ̦O>>jֹsglJeoEf ++Kf6<}'g\d6ԓH}~"5 3 ~sȑѣGOL2cƌ+|Mn;?~DYܕ-@d,B+U =uuD"x@Q#EwB"P"@@k)`{sL'w;g?r&sΝ;w&d̆ O_ cd5a„3fl߾]d\d6Y7owыr\hĶL駟fΜ# V/I1|/&O裏.^X a\d68"eZEEEaaayy>fϞ=z={OF ݌̆A`-b[L6^GbHG ̆àdĈjSNٻwlرcСgyfN.RY:&Lh׮]׮]\۱b.8mڴI&?OI }̆@zcǎ{TE]RmQ]]]~ݻwr{F`$s쭷ޒ3gSKZ{ek7ޘ3g>Kcƌ@3 -sO-[\z3#!]kH=<eeeZ;6bdc9FmoذO>X?+W;wݻw7N_7|c\d6DٳgKS%GuԾ}jW_ۖ-[6eʔ .ছn lx6o#O< -?..Ęd=zĘl8l?-Z̝;. }QIh?XB6#GUaѢEj;8]O>V"_Nr ֶm.n믿nܸ^/'p^Z39gm`Ϳ|7eCfu]g׏}M0AV)S!C̆æ4??_ѡܹnرg)_x )ܲeK޽ey-g}ySRPr֯_{ngazvB> -e6>{キ`}L 3fM ̆[n0a^BfGK/^z]t'~ ez  ʅguV֭zO%R;Wgh3f駟Z;vlָq  モ]TdGMg]ոZ5jH\Tm۶[nw};%kwzj5Z cn1bzUBtLg~ ! @ꌙ̆@,___3 ,G߿믿M&y@Ցyꫯ+7k,~饗d*+++ׯqFUno;:[Ϯj܇P2sLٞ>}&UA/,#Gt$_{ 2D|-[[r婧k.u]j{YhmyL%Kg?K$e#vܽ몓! 0`Cd6ɓ7oްaoQ.nݺ4hР]vO=`m yyy=zX|E; j,X`֭ÇݤI믿~۶m_I7p bĉ͚5¦M?^RVV&;YOj^O?',=ǎ\,ٖȶ~q@^<IZl) 4ׯ5j$M1B=nI'$7o>zxU!CH9ӻw~=;cWpjU{4O*Oҍ{V;ʶ;v;e˖V>{ȝ`50C|8r:~'eo߈9OI{~gɯ˿oEwroXH\?=wq 8<pgswq߉ !C̆ItmېT`;dcfj2+oe?֭UZUU%sԨQj/kQLҧO`o~YYe/UvZ)oJUߝkܳ)xVvx )O>٪U+c=WN~zV\Z>g>XiՈgm.No(yϹjw=z^ ݵmr<|N< b\d6DNhoIM&mܸQV9'|ۢE ݻw[VT >T:Y]xyyy۷w~h۽+-.. oF5nx…ƺ[Աڶm_~'ݸB)+\W^RMʻw>{5v?_}Uwef(܇x6;5'Y8PvΝ;-W;N~}Gya<#⮾jM4Qm_m.7(5W !u7>Vݹy\gZ>'̆ 1f.2"'eEld'[n x.ܞ<,Q[]AfC3 _UU[m"O-]-ap=EyXܷBCHc"+---++u.\^5mqh`;ɖJJJap=EyXܷBCHc"𫮮n߾շ2ZJ\^o:f̘Ā\h+v0ݓ-'͝;B_@ZW^@:3 p7o\2ZD@jl[[m^|͛7M ̆H(//oڴi˖-e,h*R"zU 5L6bʘ1c%&EfCT[вzJ^ H&[5kL2E_@Ϟ=[\ib\d6DԩS;w|!-%z% lF%%%W,W=g%&EfCTTWWi^F6 a۷oȑ۶mӗ-?ӧO_tǘloUV| 2fTUUUXXHls~G$VEfCwA@F16jԨ5kK~{ڴiH7c"!ZԇC&[+))y$Ie˖ϙ3G,`\d6DԩSeB l['u ={L8?uF3-%6l>}xѣe-_ &̘1c μ(#lٻwܹse-rE@L$c"%㧟~9s#< /ZJ R;EH_|1yG}t΀rD|e$M/c\dUTT+.={ѣ٣I,\5 ll6m /:H;G2$W ,280f.2[L+)ڵk׃>?<5#D @3-7xcΜ9 *++njOpdAگqd6q`\dPv=n8}qof;iIU ?i&}qѣIU fo߾ && 'L2%{$rՀ: f{-X@_49/1c>=eij@@fEf3+))WL@Oqd_W llf?\ʌe˖5nX/%Kg?K}8yǚ4iҠA[֮:yG XZZIKۉ.3\ =֤x_5N c"PGVl2J6lجYWTTR܂rͲ\tV2KKgrGOZNtwOP{DR#\~BRlX!Rj@@fEf3Kh=aÆƍ׮e˖y#S\_&י`)vIXZZIKۉ?{.IhĂOH+dBW llfIGA[>NhҤ_m6U^YY9d)wZYn݀4hЮ]z^PfSw ?TX`/FI#Fصk}}ً/XU((( jYf .T{O:$)7oU=r-ZIcЯc6ɓ7oްaoqǎ%'N(¦M?^x_}O$;&\#v6.Sҍ+u,i' d6q`\d6$֣СCn˖-fU>ƦMd[۹K\UU%+޽{ Jlv#^)Z誫]{>xvsBWZ%UYBYJᒚG޽[rM1w\?;w]ٳc6u8ƪg]XX^r7(G<#>Cٖ"WsUx_}O$gfF~#}KtN]YU280f.2Yŋ{Vׯ_>(??ߪcz}νvUY}W^S J֜FܽR@+WTΚ5Uku&`_i~ÇuYrg}V֡ƍe{ 6Tl6N8:ݠ h裏ʺ~ot<X!5GlkfqpwɜnY/Y>kRj@@fEf3Kb=j϶b Ƈ4j㎓˗˒ѹhG{UZPd7,wHEM'ww!Cƍnڴiw}_aÆOjݹj;qgyF ks1ni9|VjΓ۷-1stwutAp~I}oG-[V^=gIyyy^^ܣ0SdNI7W<+[^,L5_5N c"ףtt]wgS(_N/M_]-(ZsqJYt]@@gu%\ҩSZʯ|EON륂$^椳9-iӦӠmǎcǎmٲ?aw:Y!|=wxlڴIYԵF~#9ڬdNI7Y<+L_5N c"ף%ƍ?7j3p 7߼[.+dk׮O>Dk/kԟ^f3fk5>v}-[v+;aÆw|v6L2۷o:tŴ"u۞={5iP7ސYa?䓭ZR誎t<ɳB>{ տj~L&\ϻu>5|FsYwɜkܳ)xVdV>;~Հ: ff\[C?W_xbUYr-'|Ęm,Τ&M\wu{- /0//}kfl[o6nx…`8p㏿vܩLz0L!s]m{ g_{t@v:fS-7|p \W^RMʻw~cnA#Y d6q`\d6{6e/͛I ,KU280f.2[(Şucƌѧ{RG)]W >280f.2[(k֬2enٳg=)#YƫD @3-իW'.{衇߯dqdGzqd6q`\d7rm۶ (n>}ҥK,HU" ̖BrkOHFeQFfEfK,IGf}1oiɝ&#ѫD @3-a+))y<9[l?~9siV#镝D @3-I֭+..~'?3}D̞={%GM8Z͙=rՀ!c"d޽sΕH !k?>3/Hd'j@tā1s9F0ـ&EfCYd"@3 Ohd 8nRq`\d6D?5L6 Iā1s9F0ـ&EfC: ;w5jeY d"@3 ODI`+b[l@qc"!r 6o,iMd"@3 OM!%D7)80f.2"!-QL6 Iā1s9/)Ķl@qc"!r mb[L6 Iā1s9q ]UUUZZZ]]#L`SmG!wmu7)80f.2"'?ڶm;tЊ 6%dl>}A:v(=_i1l@M ̆ȉO hѢiӦ]v_[M m6l馛q l:ـ:@3 ۟Zj%߬Y-[jo%wl>}zϞ=Oqt%`'PWpc"!rڎmJ~~zmݺuI6EŶ/RfbȐ! l=ـ:@3 c 8KzK+7 @qc"2a$:tpUWs9:ujee}!0i$l|YCf8pbܸqW\q9#MʶH҇pԘ4iҊ+n:|*R^:n Mlo;vl͛ J)@lnniϞ=k.RGjJ}biGfx;vİLjJ}K-\?pΜ9 СCAAAN TQQ e/ "j9pö+O/3θ{^/$ @TTT׿v 4HKkN::+˾o d6@-ƍ^d6@-߳]wuzJWwmH/2s#,Yҹsg=Ҿ}>}d Pg+**֭j%\裏:+^d6@-8py?guV=TZ;3$Wjהd_i.)"t .߿?,\d=ӧOldO"ԗd_g!H aر7t=`RS^ 5d6`7|İ={诹H)g< Mرc67yUHM@fYpojL4iŊ[nB*R^o2ƍ+>DDH2Ef"@t l]d6.2D SJ"#IENDB`onedrive-2.5.5/docs/puml/applyPotentiallyChangedItem.puml000066400000000000000000000024541476564400300236230ustar00rootroot00000000000000@startuml start partition "applyPotentiallyChangedItem" { :Check if existing item path differs from changed item path; if (itemWasMoved) then (yes) :Log moving item; if (destination exists) then (yes) if (item in database) then (yes) :Check if item is synced; if (item is synced) then (yes) :Log destination is in sync; else (no) :Log destination occupied with a different item; :Backup conflicting file; note right: Local data loss prevention endif else (no) :Log destination occupied by an un-synced file; :Backup conflicting file; note right: Local data loss prevention endif endif :Try to rename path; if (dry run) then (yes) :Track as faked id item; :Track path not renamed; else (no) :Rename item; :Flag item as moved; if (item is a file) then (yes) :Set local timestamp to match online; endif endif else (no) endif :Check if eTag changed; if (eTag changed) then (yes) if (item is a file and not moved) then (yes) :Decide if to download based on hash; else (no) :Update database; endif else (no) :Update database if timestamp differs or in specific operational mode; endif } stop @enduml onedrive-2.5.5/docs/puml/applyPotentiallyNewLocalItem.png000066400000000000000000004326401476564400300236110ustar00rootroot00000000000000PNG  IHDR _Q<*tEXtcopyleftGenerated by https://plantuml.comv iTXtplantumlxVo6_qbqt鰹NZNDIZ:ۜ)R%)°}GR$4{`"y}eG\I<僲(-gBŬG 5̮qc[:m`W(_1)z^ )7k`B#KJK?95`lO@p>@sPVt$8'kGrٕӝZ8^[YH%nŨ$)>riȋeY[N. QT-?ȩjL9{"ovqD#o͛Ouc oRLznʱywPLNstxBϹƍUZN9:ZDTۤҪGU[ޛ qzo2bY{/R'KĴL`;M;\q|~9ͺ }tQ!țLJo.2[iqLiER(XTi 2JݫhLLӴ6%B)xl4V0hYncz(lS{MT&鵱v5X)TCRmxkIDu6=ϭ DFT xG+nƕ^Zps|.Foi x?gqkhzDw۶m⋣a4}YpadFÇGcYcɒ%)hJD_ƍs:d6P~^zEӋqD4ԣn![C=jjjs46P_>Gs46P_6nM,뮋N@ @}YbE4.,:Mh#E:mԁh#O>q-[,:J}$+d$!@Fd$!@Fd$!@Fd$!@F1ڧH<#SL ~F;$@m۶hfs1fPJJJM4ӟm ^Aq"RRRm 2eJ9·#?@TRRm &hcxM6E;|V:&Em`EH64M6cBz\PSS&<6e?\~'|zwi~fts=f u =-EBi ^9m|Wׯ__QQѧO>;:(Muu*VZ͛7/{(bzZh#@frDXq̙'xx!1"V =zj˖-uvwD&ݻiӦq' >0 Zzueee/VJN*Փr<`HW-$g}/e˖ԗ1jԨ+r h#@ m=GY^^jժɼZ8*ybz0wo裏JKK/XM6D"yV^y<9yɳRuK?89>K==}O&=N1ǏׯSO=Ζ{/>蠃^{vܹs̘1]vu]͑ x٢u oDˤ+"h~FKi c`6>ReeeLW_}uONMخ3<%\H$rrg}YʷoުUٳg'[REK*u|IO}ǝww衇G8[-Z9|k=z|_0aB8 3A:l}//sG64Ma 0Ƶk6ozw͛׵kHgϞ .{Zj5cƌIr&sloߢEgy&lIߏ8Gy$9e˖{o{g7I;v:w{w|x`ܸqaW*S%>ߧÿΖcA*יÙ/s<ѣG t-_|̙1HٌO/ǽDB2GQh#@ m s=Z:묳fΜɴ=c jٲe׮]_E&ݛ[<3{|i1I}Ν;pz>}zj7,xNO6N:N8:SN9%yJ*]Ts?_(N/nL]F0˗/_~mڴ×۷o?n騣j޼y0 ui{mk%41Ed"r;c'NmmΝF(jMSl4aEkjj֭[oܸ1׸4;?rPDhbÊ6?3gF;sDF)6Xh#Mr864MĘh#<m= èQqmhh)SA#h#@ӴFHCt(NM{m=Fi&6o64MDF F)6(^aÆԗhc*6(^2k֬hc&_ЈFEKjkk{hѢe2X^^ޫWChb=Fҥ… ?שS[o5:+6(^U[[ۣGdk׮AKtWl4QmԨQ'|rm<4jDF~_XQQQ&6 2㏏vFEMVRRr뭷F;hb5k~N8"1f̘g}WSS}|DF"s|n1cʕ+Ee…ѦoŊwyg+**o&6˗_{oܹcƌپ}{] Nl4Q(ݳK4p^TUUu5? 9FEԩS,YڱmٲkyFE?`hȎ}jر7 bd֭7|s4^>5w܊[@DFbRVVV]]ֱOm۶nUdMm(;v4iR4XGpwD02&6ŋ?T +fϞ}$6(P4&O`L81Il4QhEtY|yͣ_D /bvcǶo߾M6ׯ}T]^ I]ga|7 LbEcҤI<]&>gѮ]6mڜtI3ft`3TWW#?G;='mSl4Qhm|G={wӦMG'P3yA&6|'x~Hc؛2eީSG}4l_~ȑ#;tо}/xƍa{UUÃvڍ1"9C{=yAK2(//oݺufڴisg;;wܶm6m n.]'8^ ZJJJIfʸTo_>C}xʕvآERoduKKKo]tiDMm(ƪf͚ۑ˗?϶nC _z|dpVxaqP>i+**:o޽{]ZjN]pSe<_6gΜO?=?x'x"қq@y&6hcOL2nCkɯW 3GyC3gܴiSmm?>bĈl;K. x ַoWWWoٲeɒ%Y Guݠ& ׶z1*ۍ\|{np0r)?CEm_#L~݌rmSl4Qhm ,^83tЀl 6\ve;v F[n ۫3o. Jΰdɒ?|޼yP>Ơ;ܹs۶mGiӦ?i֭< &$ X@vfϞq2}ݗu}ӧOO.W_=S߿c=NC=q@6y&6< Sc!hh#@(++銇h#hh#@ml&N}$6(P4,Xlٲh`ʕ3gΌadMm(|AQqc#6cƌ@&DFbr7n޼9c_3fL hh#@1YzԩS:9sD*&6UVEu;^{Ν;YFE̎;FqhŽ}aƌ˗/IdMm(>6lk%KL>=Sl4Q(mذaof4m2iӦEFEՎ;&M4u͛7Gcwԧni޼yѷ痕M*%%%Ѧ,x'O^x~}|DFhM@l4Q}Ci&6o64MDF F)6(!4FE7Dhh#h#@Mm`/ٰaCH1 @cMm`/5kVyyyej1hz/hb%{^`A2m,//իW4^DFQFu%L7]><:F*6(SYY٩SΝ;/X{ѡ4RDF!C}ѝ;w.))޽{o߾A4^DFiӦ 2s:u2eJtWl4QgϞak׮AKtWl4QmԨQ ÇvШFE*++WRRҭ[h7Zl4Q}`Ȑ!%%%}vFEMVRR2eʔh]l4Q$1c\ςK ]ћ`߉&6wD⪫NԿ FAp/W^yt#@Mm3kܛhh#_Cҍ Gl4QL4?qxaÆ#8>7mtp w9S]]8qbϞ=&IJ4\c(wq͚5۷xQG=a_gСׯ$"6(P|l/}i㯸⊰-ZϚ5s jkk[huְ=yhPt5k5k`8>}<1"&6(P.첻>ڵU>ݕPl޼K' 2dذaeeeyM&ʖn\fMVw}cǎh?_}nݺ@FEwܹÆ [jkk8;v>O^|_CK&k eL7:y|a}*P?b3ҥ-_oO>$8_~`k֬ <ݻw9s{;6<Ni׮uzSO=5ںKE-Y䤓N:z`h.ƌOwA~88E]@FEլYu~`h͗\rɗN8!YVV4[nРA}  裏~kLSk׮ӧ׾IkL,{aˬYN?ȰYf<@p椓N Sy>`x<}aÆSN8pyz-9sUUU-R/<쳝:uz'×H7}DF"VP'k_wsΊ+o_#8"mlժUx+~O͛>${/֭[?3۶m)SN;.(Hoвrʃ>89 ꫮ*8>&,h\l4Q9u#ݘm\foǣF n˖-l9%c1pg޶m[bW]z饕;wM3l }&LgϞܸqcp|wz[lIo;ӯ_~8rьW&iQsDF"f D\kL`FE3M2( =Mm(bvK4t\#h\l4Q9h2FF`)hh#@s %ҍr@A(hh#@s (ѨӍr@(hh#@1ٰaC9 E%iQ 4Mm(&f*//OL9Ao%@$]QC 4Mm(&zJn; ;PJ$ǏX7N 4Mm(2ÇڵkywK.Fh +6(Pd***JKKwlsmܹsN*++C(Š&6}{` G}!C(&6)StԩsyڴiA @FEOmmm׮]msiiiϞ= GbEiyРAFvPh 4Mm(Jݺu vvPh 4Mm(V} vC vP?h "6(PL윧M~(Dl4Qwk֬9sĉoƌ윃LYjjjehJx2RHyb@}ٹs9sn3f\2A=XpaBXbŝwyWTTD?Ш)RHyBb@X|^[YY@Q;w1co@cÞS^Mm oƌsOtũk ~F?и(P(+@hh#P`SN]dItl˖-ߏ~P⡰Wb@!= ,n\UUU;6FAMm f֭7|stܹs+**{(rJ< hh#P0eee 4۶m룟{(rJ< hh#P;v4iRtw]SS@R⡾)FEXx?ݩ@bŊٳgG?Pxo+@FDF0&Oݦ@c4q{ .6(FYYYtҰ-_yB(Liٲep/k.|JWrK.r~|O?}RI .[$z~u .6(ƤI{voΖ ҦM֭[ҥvI]Caד33UI|JWrs{#OT4&u(RK<yI)W&6Q}ђoݺ+ׯ_]RP*L*]ƳliB\paeTOT4&u(K< GɑR{%+@hh#Puy>;t8[z׿6msʔ)H-ug} n.]nz/RRRR~c`֬Y)~xN}є?qyr3޾}vڍ1"lL_pƇx뭷 >0y`mT^1{=yA}&ƒ;k׮gqFв~#G_ql$,x7?я/C/r!\[hQO}.]> Ƥ%_UR2 R'XH\"S] Ux;Ssm۶iӦ+c'RSȸ^ H%G$Y]wo|#н{{.<%cǎ='N ii)b@aa8쳿lذakۃM%'?ON:xt~UN~u)2g2./ԫd!]֬Y,' s/8U0yL[o 4(~{婽cYbn3c]ۿ[MMM} Ɯ{k׮]n]=+MȲ7x#WUU%vqVg 'e~:/O}Ƥ%)@JJbAي/kp=Ç/=`:XljoUeSלZ b&Ȳڵ]re.]:u6o>hLYI(` \sM0UW]+_I .6(FVW_ _޽{_ΝO1GK]pdSOu! NyKm6DŞJW^I=+U'Ӿk9sD6뮻 gwر}YjU=wlׯoѢE=z'uٟINm:oOz8{~&U>;.D~g?{o|J<*Lm۶鵛Bz%%[ Et?\O^\: ^I)d[U{L=1c'c%9,w$-/ }{wȑ'tRpz>Lƕd6u q >8 .6(F>^ vɗNoOGO~.Z _"}bOL6C=)ҧMO6aopavw$ύȱJ^~b:CMm]_jp|g̘11W <AoOqO?{o:xR#ulłE%~H/k2V*=FkX]4xl >ſN>'?mvx CWmڌkg&6Q}o?ENвhѢm۶ gqKqǕmy'&daڵ=qH>`6m?~|׮]=Gq ^@d` 8I6lо}6mDH"U֭;ꨣ.w}7g?{o:xulłԪJ@R%eݿ1G^I)d[U{XʸEW̶Į3;v;wS, }%}]r%UUU ,6I"˂C$}2dH%Uƫ.s=789rd$qg?{oyaW:NjM!eU1c*㚳_rM2^1ey_|J89䐶mnݺ5MJN[绎g&6Ͼ-Zlo[C=?͛V:th֭{5y6m>YTdO`'LP?8۵k7{쌻Tmۖ: `+~7XI/袰1}+k{=|>zkd𫯾z) K ,Y7o^ʔ_$_ve;v 6=z[MȲŋ]wݕlI*w_׭[l˧O)oC=q@hL)Dj@JJbAEe $;;w 9rM®ludM˸llElW̸З#F ~+)u:uq6+@hh#P:{RFk?j`{И_Hf6XEW4ɟ .6(FYYYt-[ /$vmN<ğ'4_~\mmhL [i(XtE)b@av裏v֭uևr%\aÆ۶mۻw+VD'F?P [i"XE)b@a,X`ٲem 4.+W9sfEKdMm >hf̘QUU@R⡾)FE7oݬ@#2f̘Jy(6(իNݬ@cQQQ1gΜGy&6(RYY٪U[hڝ;wF?Px'+@6DFv1z7Fw-P˗G?((PWb@mذk1YdӣuhDx(, hh#PxwoftEhӦM~QPWXDF^رcҤISNݼyst+Ebڵ7tӼyohxC+@b@=z뭷&L_rٲem 4`?-RVVV[[X@cC(_Hl4Qw|vDAD3tɋ/ehJxQP^ 6(PĂs @bE`/SsDF"f )hh#@sh\l4Q9e 4{.6(P2=Mm(bv{ &6 6#9&6Yf'_7P(hh#@1իWr9ϛ7wA߇P 4Mm(2~׮]ss]t5jTt(@PXDF"SYYYZZzG`l;GP 4Mm(>} ݻw~{`Ȑ!A @FEmlK>wiM6-:Q(hh#@񩭭ڵkm.--ٳg@(Pl4Q(SO5jTBS(hh#@Q֭[s߿ph %6(P윇 ~(Dl4QXvmyڴi @AFE믿`̘19e$w꣬SO&6|+N4 .6#3ꪫ@Pe((MMl4Q _nl:J-@MmK1n>[n`+޲R tFExb/(MDl4QwUTAHD0 !\%p+H)m(>VT! J UD@ !@A[H$a>>UyW3e޳=2?x&k^{Yk̏ n_=cDxc\Xr::yi\Xr::>^K’8*:AASOB^^^=k Kn< 6޽{޽ŗO~!Njj򩍞A?πxL$h"FkX.QF'OT˵ q'Zn} /0qD51Z|~y[oKk%Vڵ'pԨQjMc_~3gT#4W+//Olb4hЌ3o۶-&&fZ/,jXGDj*u{ũjOm yd|cXF6\#==zZJŋLII>}~(U3ݛ+TTTif߾}JukM6g}-[T+4jH]v*+dcqq^RRrM7S۔X'$$,[L{mee֮]WTTȽ@p{p#:$SnW%VSj'e%MzkK:m۶9s ch"FKʩSήZ.NMbnplm7zS'XF6sIlm75E5z'XF6sIlm75E5z'XF6sIlm75E5z'XF6~={'[/Gݳgnݺ=NRdggq=zZ;X8F,D\L?/]$M3ϨÇ߰aFUVV?NFhQh"FO<mȑ#DQoΫzT~wߝSPPo 'vvP#\4Pèwe4h#Is=zOMMUK7n|e}5շ~w g?wb؎njjN&mp1zȐ!V2lذoVlW~ظtRiiZaǎ;v꣞3tA*--mذ7pz뭳g4H"##aܫB:Uėv5<2HŌ+ܸoF+p¸qvڳgOoNN(sLw DgM$bƔO<-K'@y+P~Gݳgnݺ=Nҿ@"[if*"Zjxh)f[ƩmT'OܧOo޼Y+/_5jO?}Ic5a߾}III)))eee_8qصkW v-|Ʌ {YF6>vi14h?O]x Fv?H}Yh7lؠn>Z#cdyٲe_ߥn<}]`ɓ'~odM>I&_~W̒IfG֭[A֭yyybԫree!C?&KǪUƤIƌ#6=/6^zۥKb_e4h#K)&'mlg+\1$Ć(7xQMJVn(-[Q,6]bArZᦛn[m14in7nܸyv֭Om4&Q^^خ/]$M3h/b:uJ6KѴiSĉumŊZ}c5q}hԨ7|#8VСC׿6]vq/h"F7-((оҥDث} ҟt ,XUUոq˗/6jrH?ŸVsEbtM׿vW dY'?hm۶G S/q] >:\P(Lj1}Q%L/ O tJuxZlg?Ո{YF6X<'$$hF-cQ.~WE]FaذaYYY~ꫯ>SK+ho+\h#???55U+"}ˈZ?qCPL~_?sjWk֬b1vXn׮Ŷx~ƍF\2Heܷ}jQCرm۶/"6^paܸq]vٳ7''Gm裏vq;|]w͙3GݮhҤv4&!Cƾ{Bn>;zСCŐsѣG_x񊏸0##{5Vۗ$LII)++S O>ݠA+WwyGl9sF}i#.eM$2b_PP bޮ]B*h#`;nԪU yIII=z9r]]ˢ}ݗ Z1c(IKKԩS~%ܸ8OTp N&mp:Co>&&fr%x 'vv<^ 'x";;[.zD3e1{;%%%-ZHb؎n'Փ}Apzr=h"FjӦk:v(JJ(NxC]82Hx5إK_nx 'vv< .\O`M$Jb#Љnx 'vv< .\O`M$Vb=p@y<`;n`'N&mp)S9E4N@_Jݤ܂ .\Oe4h#p/wߝ]mV:uC^'uNNƍ+C@rss /Dث} EXF6:vشi^~]v)jrQرcGvvvNNNUU9!Dp]|W7n|ZL>=**jڴi'/ [tZZ m1!Fn 0`xxDfϞ-={xr޽{#""%Z_R#̜9SCBu*''g֬Y;w/q~z+?cS oXhqIAЮqpqsqIP-~뉩 D,A&$jk&M_QxxxXXXRRTSgjz5jyh#`w}^:}|r… \|Y>p?qZXh:b...!<~#fXF6FgyW^ m D/1FQEufѢE|uьrL U\|> = !_E-Djm<~~͛hbĉjyqqa9ssr¶mnf+ƆO>%QQQ; %-[lڴI|ETtt̙3o[6mڈ# Ǐ0aZj%ŶxVk֦Mf͚?=ڂS.NIIɨQ"###""ƌ#?+}K.m׮ݞjDm@]Yd֭[opgwQ8ℊ*i/qyy?!$642H@ȑ#̩8))I̢{G-srZO?trrRڎ?~…ѣGꫩbVVVk?n5YXZZZPP TKϴÇ(Շݲeˏ>H=233cbb{9u{rG%~8ǎݻwFF.?LcNQQMx͛7KLV9sti6ch#={,]TO8{lzzzMd$!qũ$׈yq{B aM3,D "xԩnNǎCDŌ(Z֭[[hV'O۷oo֬%jrIIM74o޼ӧOKuСCΝTʵG% -~v͵ mʕi) 6ԚFƗԽ/bbb#GmF _IGɑOq Ԅԋ ܷ2u8Ͱ&m.PhASƏ?.J6lpE1_R[VhӦMk۶P^###ł$<<\9:*}Pb=ӡCO<}tÆ FƗZnzŊBm.\1c|(W_[sfXF60xV{ˏ;ֻwLrjjc=&?~|РA~‚*c bTPPpܹѣG-B54i*W\xCv秅q%&&}ꗾJ6w??o߾ji@͉ = !_pZ h"FZS' # m)CR9q`={ʖ>Q[M d+j̉Kk%쁼c LϚ){JwTc:vF/_[D6nXPP O3_|܇|ZLQ(++KKKogϞie,3,W|uaÆ}Rc5Yjj۶m ᣿o:<X]5JXDD4ORȁ(%Qjڴi#FЎm&L05_6&M{ioTZo46.KyҥڵSXSq Ghp{ڴiӬYϟW+|7%1^+vU|Hg?ie4h#pSNɳ!Z/^ʒ{mgT:tXp޽{5jtQˏ;ֿ큷rqG)'%%wiQUUUTTFFFB 4xg+++ſS=$6{.\=ztjjVLQ?O>s}3ԴO6{#<"?[1jDe\M?s>i>RVM2GT+--ؼyZ(ᰰ0q^qlٲG}`hch޴F%!)16.3Z9stij*>3=7QFׄ޽{gdd}}S/_Fm'p h"Fb ;w{m,nڴI&b;11^N D\ռy[n~El,7[ɓ'Eg;~T.>i>iӮ[a\4f05zƣZt]w%6F9e.KDC_>7ˍCRwކ K ^l^}U%5S뙀07 oJxh۾xjraM$d…[nD@Ggff~rpCyyy[WlР;0[jW7AjkΝ{wСC?c{WǏ6lpſ?ϋ7j wU2sF_Y-' wyezқꓦG1>}tÆ rlzj}arI48 Ν;Yj׽ꍁ IIiixy:(>FZNJ+ƚRL@O?*Om5}1=?ߔeyTOXN3,D,&OJJJ233ſrw"pҥKojO=u}=#ǎ0`WAZ*B{̼꘶{曵5kVYYxl/4m Ν;7zY_u.\ iٲe#Gj/o=nܸM?mTYfP?mML鑫LarI4/n׽א_?\lի~;Ǩ ?VZ2JgzҨT mOuQ\7oo߾._ct:_ߔX]aog'p h"F>bʓ#իWO2ҥKrG(pСC 7lݺuEEEqqaºt+hϿ}?o޼yDD|ر#F4k[o?~ ھo}̘1v^mB_m׿Ю]ӧU^l۶mvvVX^^OjoܹYjQ@ a_ȑ#)))j|ïmƍ1rEݻeˌI_}thׁ˗T2KJJz!b(;V-4RdU~ayI4Tq͛'["wrqƩ#"666##OP_e˖-[]ִ{+5 HR1 7M6OVV|w}_~ב4˗bu.u+z42H2O5kVvv%y]v5qWXq9CpLg0uݣ999r' ڵkJuX;|n|G$luCRn6ie4h#r At8*;;[~. \h/,8q ¦Mnqƕ5ʚ5k~y_e_a6!i:܈6b9Ͱ&mp1Rn!G-_vΎ;6n(8qg9q}!N daM$bB':wYf]|Ypq(Ps/.rC=42HH N:6y/-gϖ=C>q Ԅԋ ܷ2u8Ͱ&mp1Rn!ܹ3??_~F oy饗*++sOD5!./- ~9N3,D\[L2D~L Xnݚ5k'QJyq{B D.F-qP⋌ a5o˖- .O9IJqBs XwqzK!42HH N:FIIIFF1K'nSLiw3!$642HH N:̥Kpݻw˧'N8g0tqaw!42HH N:XAAɓ_}ϱQ-YD+W~مW+N8t.[0 o(//) mi/,D@Q%33̙3W_^.rSN+ o-((оmQ.j_@0Upe4h#rC1@,q Ν;o۶MRK.MHH{ 5T D $(݀tc X8iiimڴQӍ^`AllSO=%W1Upe4h#>\{n (,,۶m ,_Ĉr*,D)݆t,q ^zA'֭JPL`M$L!N`؎n̟?_5RSS-Z$W`h"FY F7# 'vvJ6.]S'XF6oR5FS,q 𘴴458|zJ 5T DroFrrr {nF:tP-R#""R3ZȻ o<-[~n:wn:.=ҍF,q :ݧO-bh"F5J`'''_n޼yVJ(0-m߾]^:>>^KBQ իڃw@1Upe4h#%\жm۷˥2cƌ]&%%*Wcy$..j_Ws$&&&$$j5c֭[;w,VO?iqԩ:uR[VWw=zUVddd۷o/7G5 &LrOgϞȑ#ZMpÇNjO4I-TΝ{|ɪ*=z|עҍz,q 狡h"yS'XF6O>я'N4iD.vIu#77{QcybF\ZKUŋƖ?Ob3FKJJ5jPl#oݺ|&N\=˗~;,,woV޽ bRF?`׮]w5jZmׯ̙3m`T?.uxAf̘|۶m1117oJ|9x`WȽon@QQQ1iҤAC[+塀ɓ'+|ژ*82HxR;uꔜh>}t5!!uJu,iӦ>e˖׼BƍՏ!o}7v*Zز8F/ܵk1xŁL>?T ^yoӦ;}G(*+GԎVTпQllbxwU,))馛۩mJM\0,,L|˖-^[YY)kj%o[qv=@m7o~c'@߶ 8g}V!0 N&m^nl۶,zjPb@W())iܸs箭bɓ'Gtm̙3xcNJķ~;%%E:6Fl4o<--Қ***i7/ر nF{nBS'XF6RH7}=|֭[7lPZZڸqGzFaȐ!g(?~ǎbôeR抍LcgΜ|AAĬXB꓅bR86oO+Z+|GMW?Hm<PLƏ@X긧~x,xŎeO-p RiiiddɓӾ}͛7KJJJLLLII)++EרϹFF 1Upe4h#&%t%Kr{ Ç߳g(|矯]>h||XO[lҩS'q<ÇZ4իW5k֨/U?pQ}Rp„ CW۷oٳhG7%9lذ.]UVVZ <ڨtС OCfkbDz'HA֭yyy}UWZ%6&M4fKQEYL`M$DjZy궥LOm4KN&mp1Ю) n!t-SUQ/|q0hР+V U.\߳g؞4ihcyyynnF?,D\9ʂonp]oСC 6޽{Ϟ=GuVmm\xqll'cbb60@{/KN&mp1Ю) n!t-?M:U.uFKx D.CFFFN۷߼yZo߾Ĕ2>=@m78\k׮ (++9\c ,D\L6ۺuv^^^߾}UVI&3F"eO-p kYRK7^DП`M$bRiӦ'Zj%6***5j7߈iq] n!t@(J7k ^Bve4h#IHmnB ~| $vCZR\n$X#x D.fmR׬Y#6Ǝu'Hеt#ƚKN&mp@KJJJLLLII)++~| $v]_J]K RӍA1Upe4h#1vOY-p v-((оw-Q.j_”R/ӍD1Upe4h#1vOY-p vܹm/t҄AQiӦ\!?<,D܄Ow=@m7(--M6jKZ ,}ꩧ䪀UL`M$&| nFqqq۶m]k˘Q.W<bh"F7ҥnk֬]!eO-p իTqqqnݺ -_ `竹.UjjEJGIL`M$2'wС@̡W^SOw=@m7ثJv.]g:$ N&mp=zsĿ111:uJ_ `455|pB T D3w6mڨkB{"eO-p +,,T?O>%?,Dܧm۶j1::;"eO-p NիZw!ah"FW=zm뮻$ ~|ZuIDAT $vϟ/v ah"FW*,,s褤$>E_ j,77w̙/ %Wыx'N|rWs }v]N(ch"FѣC}c/{on*((4iҫ~X~\%KL<9//_lEKS'XF6ܹs$ ~| $vtҥɓ'>w0GTL`M$ꋒ7|s&6e1;Cvvsɝ nw9M8`ɒ%/{apNh*82H\g_ʕ+'O,fEEE~I5^{M;w}N n_dddTTT[l޼_bmч:;ch"Fp=޽;33Pwʺt)'HGVVu{YFBFHq@,Du<JIIIFFWN ~| $v󥠠 ??_ ԉ^zR5DFe@T DXp-[6P***233 :&eO-p͗ɓ'_xQ ԉٳg˝AH*82H\k֬Yn<)S}1/{onΞ=;o<*Pf͚uek>؁S'XF6VYY9sLy~ \?;w{3_ L߿_ ԡ;vlܸQ#d9v`T D͞=T_ŋ'O,Tg nΖou.''G#3v`T DN]|y֬Yϟ9: eO-p͔Ӟ={4i"6!FxLa`*82Hԩ7رCY/_.W n $iӦ!CDDD'''/YDQQx̬k "z/$&!/de#03 0:XHD)Bxq[ Aj U! "$)E!!p Bn.U_mi:ysn}G~yN<ݸ$)9[o?蠃N9]vq7ndFI4qn喰j6~nǥ%00f̘7n\wwy睷qpd۶mѡWgܰ- jV_{J EȌ&6#jaFK=Kݪ <{oرc``O:Aq8`͚5}-q7cMUMmܸ}/8dɒs&vϟ??1 /~駃˗wwwG'+U>8t!ʕ+ӿHp#W_}5\p'^~guWx֭;w683}CvL>vyoٲeٕIf^zi׮]?҅$_4o޼6o| '\y啕7;vltfs;y䑤X?YfEEW]O~}7^^댟?/??>'xE*z~:ijᪧꅎɟɟGˣ:'gѡիWy񪦪MJ lZoT%cSOnɓ'?酵ST5xuNlVwPhh#0{1.|``+?ФQs Y+Eȕmeح_z׻¥oql}O&)O:a„wva7mۢcxꩧުĨ9~_c郉׿u~I&ۤˏ|7o3mڴdyS_|e}#yW%gu֝wޙަ~7͗_~9YX?z_$8/O[muUijSWB˓_gժUt,|tPjSğ_|1裏7iIvWzꩧ(~+'<Ԫ`:~_~y/+'tPzgjr] ҂]y+hzi'˰[Uca?>,-ݶm[cojg֭իW޽?y<_"ٷ֎i$pw_}#/pɬi`ӪU0衇_ qe _$:===ч+YZGHjgկÕօן0PY^zV]UW=m6eW'֭?}{^UST5xUOl՛ͼUwPhh#0m 9'ԿwljUkaL_ 3᧿7<]ZgǸiT */wrfCU4N:a">ao{ǎ?O>OWwy^xa<1c\pAmuSO=5oS/x|p6a?+Vb ۳aÆިӧwuu]6h:oqWZ*`ٰaâE:L+!Phh#ٳg…Jw=lڴ)vgΜ9]]]===Q 3f8cNn ˰:c=vUW~O>d8 3}W_} ]KôwT(Bf4Q?JFkF]viݞ+VrF^uUF {3m߾}ʕ7tӍ7xuEU+\ľZ{z뭰F˽4Mm >sa F߿bŊ^lݞI&Hwww$aA}LEȌ&6Ӟ={n喻kΝa /x?Y<ݞK.dq WÕgPd?D}.Bf4Qlrz뭏?xAy{{{oe˖)t{6lkڵj` ALEȌ&6śoy/_| "jjך5k_5|#(ggΜ9Q SN W9ZT-:O;QMm(1myVXpUW+rA-LEȌ&66tdQ mЮrA-LEȌ&66tdK(E'Si's2% ]:/Ya7 jډ\hh#@iCNK `؍Zt2v>!3(PbХ/v LEȌ&66tdK(E'Si's2% ]:/Ya7 jډ\hh#@iCNK `؍Zt2v>!3(PbХ/vU?U+X mFi*!3(PbХ/vUV\ۛZhm#6Phh#@iCNK `؍V1cFJV$Z}gSh;?mLSDF21/v9眞8W/pSh;?JSDF21/v֮]5}ިjEtwwoذ!ڎOT(Bf4QL_v=KhSs13f̈VOOOWWל9sM%M"dFEJsϭĉ7e_0Fk]uUQ:c?0_bE)Phh#@<]]]Q0ʔ)3BnOa7Zk``;uE&MKtPhh#@L<N9sfԆ>cYw{ rguV;w;3EnOa70uԨj͙3'\@h*!3(PJvZk0aՌnO:Nr(UW]U-]3M"dFEJӦM3hrt'ZM~la6o˖-eXdITɲ!k֬z*ci*!3(PV'Oгf W0 ̘1#I7&ݞhI|oq3Vj[l/_qQ.{챥K.[lԴh2 :B2eue}Le0'Ƹ}9sM-gحs-y;w GԵm۶nG?QxN)z6GESDF xC97pEzS/ذaCWWה)Sz{{K{FK|p8@v+ >saڈ&PamquM"dFE۲e_뭷nڴ_gxr-˗/oEf>cn̙3O8ɓ'5F Zΰ[/^W^ piڵk><CmWNPhh#0{Ylي+LO'o{M7? 2}sꩧF^veFq3V.{Yp|X}ݏ?xxi5ڞB2@-Zy|@OOյsp8@v+e˖=X-9,^x޽ቦ۶>jo EȌ&6+2H{n'LGO;put ͛p|֯_Qo;i*!3(4aϞ=-kJsϦM{&eÆ ӦM>`a:NrJ߹sg8DKn^{mxiCژB2@/_n|+YfEݞɓ'+-gح,|e˖wqZA6Phh#(SC}ׯ;'eŊQ. W4C Zΰ[Y<7n gigyvxisژB2@nSC}_4000u{(\ 'h9neaB-]4<㴂zQGJSDF!oö9^ ݞ%neq-ò??r!馛3N+mڴ.--QT(Bf4Qh<㏇ms`_'̟?/>l߾.:=|3JM,FjN2ۣ>Zuxn#'?ҥK[}|pT%\RܪҗtVzG o3lm}d˖-gyf1_/Ǐ|dɒ%sMv~}!.ja+]?z}YGTPhh#АNWe?1Xc~i #ӗhvwYe~5' @[YdbΝsy֭\pg-կ~5vؗ_~9k~E >ODߞuY_W ;nz_}ŋû+'tRo,(cؼyN+S?ګW>#U??NoY6æަ}g;/f˖-gN2ї~s{WJ&Mfd<y䑠`$ӉY`AtѿfڧtQB2@C2;]<Ғ.Gy[-4jcN.GA2 RuNQ˚ۓ `ح,2Z?_UOOϻ9sݥ=ÇvXN;?>~駟7O:a„wd9=\kMC_|1}޶m[7N 6~_?&MΦC*ׯ_Aū:dHGQy*Ƿ6Mo:G7n{Fh*!3(4$UԚp~gqFo;̓wLod:'x׻ޕJVicuf6æ&?*I2A}{N9üyxaH.U´uQISDF!F` Oϣ?'\مԚ?nmO>dzᦡGudzٟx≋-C- jI_$ԵvLboʊg{+gKK=U<ȡvfw6x}UfcQ]~ϖϝf͚bPhh#АNWeքdfUVUv!*U?ү}k3g|g-#4Z;Y1N_GV_ui}}}~oD 2ZץVEzp~xJT=iڠ^5~5' @[Yd׾)]tEO1cz{{_} . =}g|#?,9.?G;>ɪX2ȼyOUwL`SVD,(J& 8/^/x|p< AzՎ;/p oW]mMwm'm3gΜ b":3g'RΝ;Ǎo}+:cͫ,hB2@C2;] +Wv!H}iTo= 9xm<9:˃[9>Fj -pӪ~iʷ V{Ae9ʃC??~O>?i/u^5~5' @[Yd⥗^7o^WWرcΝ;7_W'Nwuט1c,Y,#8⠃2e7ߜǖ?|rȺuXu :.p˖-ѯ0nܸ}_|Ν;7 <0ڦU_Kd߽CǷ66z( ygDԩS'I4f͚77,UrwzǏ_tieU}D#4Mm9ԚpK᏿4#xmr$qg#9:qF;ٱ3Q *˩:vbc j^uU*_> VKգq=K"Q|aSoxꩧƎ[Q}Ԯ4Mm` Ԛp3ԧ>muSO=N`.Vk$#qVJ/r\pAt?> _^'6a';|&:2{U_PH~ٽ{:qdUZ|u]k?s]֮]/LTRzո]ݞ%nelٲpL֩?Ͱy\z.GJSDF!Dkյ&gO?x˗Wd9Pkt.mڵ~~ vwy1\tE;v\8-ɿVT-~_Ϛ5+:6'p??%:s284deEz}}}'3衇_e0QKգJ/Lh@.knOaX|y8&K,]4<㴂z;<۷o7nӟ|p(>jW EȌ&6 Ɍ6-5)v( ZEJǰ[YY7|~>jc EȌ&6 iT7n|opVa<v( ZEJǰ[Yٳ:߽>jc EȌ&6 iT=Є K_xP+Xky96PJ /v+e˖u{k&<״z GMSDF!k֬y: /v+^{n ggժUׯ5vQ{T(Bf4QhȞ={n氅+E=KG?<2\}}}_³L}4Mmu-Ka;xݻWnOaҹ뮻֮]Ҽ]v-^_O1Poەh*!3(4^æ:UVwξw{ =CZ׷xR:B2@~a^{=S!' @[Imڴ+ܰaC8bKVZu׾i`m;quM"dFEy?f;t]v-Z /v+{w}\sw3τTx'mذ!<GHSDFiw Щ-Zw{ ^z;!7Q ?љjxOFgmQU[}Ա4Mm_xaժU\sM/_0FAT-:O;QMmi޽?jSӁ|o=QWnOa7 jډ\hh#׋/onE,~wW^y%ۓ `؍Zt2v>!3(PbХ/v LEȌ&66tdK(E'Si's2% ]:/Ya7 jG/ W0Rډ\hh#@iCNK `؍ZW\O/ZHqQi's2% ]:/Ya7 jqq۶mч_~YqQi's2% ]:/Ya7 jt1&ݸDFӆ., @QFXeQq?Ri's2% ]:/Ya7 jZƘtSi's2% ]:/Ya7 jƘtSi's2% ]:/Ya7 j Oʕ+5 6k5n!:x4@EȌ&66tdK(5lӧOqpݾ5L7F~ƌёhO;QMm(1m QU+5kyݧz+N8d1V'}K]Ї&L טO;QMm(1m QU+8xQG͚5k$5*Ӎ4G}ԩSL"טO;QMm(1m QU+8!qO7\c,N7F4N:uѷ5O;QMm(1m Q8GAq&E%R->!3(PbХ/vQ裏Nr===?{/<6F.\믟>}zwwwӦM1cY4M"dFEJLt_%n0:%O03ƍwv15F֭[7wד'On4Mm(1m ({QGuuu4 [ 7XkLDO}G-4Mm(1m h;u`ƪM75[B2% ]:/Ya7UW\YuƪO76kLDIt< h*!3(PbХ/vk$l Phh#@iCNK ` @t\#B2e hC{!藿ۓ ` Ct\#B2ert:ZM~dt_0m2(䧩Phh#@ ̘1#I7&mhI=KݠƧzJh M"dFEJf]]]{loooԆ2eJdÆ ᦌ>=Kݠ޽;T(Bf4Q|Lrq͜93jCǹٳg1*/vZDFY`Az>餓g̘bŊp#Fݞ%nЖ@x!3(P>;wzzzw{ A[rkyRhh#@)~qqĉW3Z/vZDFRZzu6U W0 /vZDFQmݺuv믿^iӦ|+'|IN9s$A}Woz!AnOa7hKnmUCɒ{^uU'xY& 9c?|\sMzfhhߨd AnOa7hKnmU0ϸ??򗿜lhۓ ` ڒ[hϓ"dFEF9s'?0㾦L2{dhߨGFݞ%nЖ@x!3(0zM>8eʔ0XaڴiѾQ ɏ=Kݠ-V<)Bf4Q` fm1cFd}(ެPnOa7hKnmU'x"cXdӿ/%GF%$?2/vZDFn;W^=y0Ϙrqmڴ)>7*! ' @%6*'EȌ&6^7n?yyƷ͘1c…鍣}Kw{ A[rkyRhh#[o͝;Jۿg]9wiӾ/ݻ72+7*!Yhۓ ` ڒ[hϓ"dFEFu֝vizzÏ~ӦM'k'>~^Ѿ酌=Kݠ-V<)Bf4Q`[dg?E[FG{+w{ A[rkyRhh#hw}sgxp]hhhƣݞ%nЖ@x!3(P{]diCR6іrVnOa7hKnmUSL9fΜ\ٳÍhS:Nr݀xh@q!3(P> ,Z'tRoww3VXnDq34C:DFٹsgWW300nDq34C:DFR:\ĉϟ}8@v]_hh#@)^:qڴi6lWӾt MЀN/Bf4QLgϞ8@v]_hh#@Y-X jCX"\A[q34C:DFںukԆ Wt MЀN/Bf4Q.\/>i_:Nr݀xh@q!3(P>qq۶mч۷K7v'h9n@S<4Ӹ닐Mm(t1&Qt MЀN/Bf4QL*sҍF Zΰ 4"dFEJV1&!t MЀN/Bf4Qcҍ@ Zΰ 4"dFEJ`\ctʕ+V:Nr݀xh@q!3(0 6k5nܸq3ft MЀN/Bf4Q`Tl2nXb)=a„{,܂M Zΰ 4"dFEFacxɓ'Ϛ5먣:H Zΰ 4"dFEFXn닧i?xq}`\cy8@v]_hh#(p§z*+6,#$+@>݀xh@q!3(0Jk_~yY},Xs}_Iq't̙3{{{ïH @}Fh? EȌ&6^yҍq1jժSO=+jywqҍ1y+W~hDˣɏ@[T(Bf4Q`T^2ט'qnW1cFW-GkhG EȌ&6vͦVZgg~8@gW2͛7/3"~hD'N/|!h; EȌ&6@s+W+:i]]]{loooЈ2eJdÆ @T(Bf4QI76kW3eʔ;n̙C#5Ξ=;hG EȌ&6Ft\#+X`A8餓g̘bŊp#i*!3(P&ҍr&4bΝ]]]x[OO@Ў4Mm(t\#@ @N?88q燫6Phh#@Ӎr9+ Zzu*Cl4Q Z+POTꦛno~3vXY Z!6(P|HN0׸z꠱n:Fh=|Tȇhh#@ 9V Vf1$ Mm(`>C$ ԭvQZ!6(P|HNC\cHZ!6(P|HN%Ug1$- Mm(`>C$ DUUkjj;rH#jh1XC`=@B>FE ɩBQQQ1`~?|tR5n =XC>Q!bghZUyGzYRRr'gTs:ҍYsy{=Q!bghZϒ;SO=蠃"?jT1T;L8dȐ={rPp|Tȇhh#@ 9Vhtc?>}zG}t#5tc0O3~A G|&60 #G_Ѹb7N &60_~7z1`;c_}_m {~}M碎9昁.U&60F sa3x{7otck f̘q '4ht#@(6(PDwݻw&:ڹƴG[Fhh#@m>83Y54XG1ӌ30@hbLOgƴ` JDwDF&?I764Mm(`yUwQqbL r-6(PDvFF$b*n$ @SL75$Mm($ӧOH͌6]Vn|뭷&6Ӎhc{1V]]ݽ{-[DwDFs97L7ƠݧO::ghhh#@5kVqqZQQ|sn,\0:&\l4QoРA 9ơCFFE 7|g>cےGFE ϦMo߾555Ah#@rDF4bĈ0اO|;h#@rDF4k֬pnC hh#@ׯ_yСh#@rDFBu 7ߜˣ;Fb@ۺuOpӟ4mڴk D&6d۶mcƌk_-bҲ[mfwk׾}k6$Mm Iii ƍO/(+sߖlv^wݷv/Fb@Xt}g,X#D_7 !hk[i^nؽD&6;شiS4n!Dm|+-Ap+{6$Mm ֭[ʢA6ٴiVZ}ԛhoaqt#n%\l4Q( 3g|A6ْ%K~_G_=&hk[:ڸ]`mH.6(h 4iRSofeFK7&FEP(ŋw)3?ܹs׭[u@ӪyWOh~ߡCv]-{{a//u=?v;eބ[o߾cǎ%%%?=#l^:Xދ/fݥw}& s:Oo_Úv O[wyHzX=_1lnN:tȣ{1y2/nxIky]%jzX6m,|37nu$[hvF]N hh#PmLVXqmui…;ICѮ|Zxqk[DYƻtwx_.]ܹ(h>|;߹88C綾}W?MM hh#Pm5kV=^xgYfڵvu]W\cCW\qņ ֯_޽7o쳹f;c`ܹs3"6&2FIp{zo:u[_/v]nau)[R>x_=kGg=rذ+WW)ڂmĈc~>zPxC5e~ٹsQfOd˼ u?/\vI»CH/{РCƌ*= Jn_'y \l4Q( 6.XC?{a9sz91ǡe˖ݕ+W튊ǧVEEEsf7o^ǎ8K\ŴrL +++kSY&2&TÌ5W)Sz n۶oylƎ/1jEOzqY:w.Jo7[o?QQL{6l|M6KޙޝAc?_y"t\sfǮY xlyz"b弰`pw=30doǰ?ꪋ'[RGfL=~0'Uʵ|衇 ^|z$+f̭z.;$Ȳ)ݻwK=y7F]C hh#PmLj+|}7rH}D;Κ;LuLYgȔu@.#N+_ 9̙kȜ33فn~_Asι[j/i&쬏ڸwxwuw~λԎg"u s'8$+OSFS}/vYo,3C;vl챃ȵ¬wmꊳ:w`y'y^\k{gX.YuppWxk[&k6$Mm C#^{瞛 3CrL5{ٮ;,ΐ)g]pgܸq zٙʱȬsfVUUչs o7Lm͚5m۶ Gw( enSN9sN-.o/&몞: _ iamыOd4לY moz[Sԝ{ZW7zr-ׯ6unݺ<=7K >42C方\Kc#~~sֽ6FEРhʕ+'NXTTTQQF ̙3ҥK:?uL$5{َe=K3dJ7tY)=8GݰaÖ-[&OܧO\#s͟ق8pذa\yguիy晰3lƈ%k't݀>,*34ޟVV>L۹sQ0'=vpf{A#7똳^'%K2uwsiٸUΚ-\vCg~L_:}X[#=QC[ߺ`̘ SO=ܒ2yiв#W8#M/K_: L^M`mH.6(D-۷رcIIy睷`ު+[n:tׯ]w.**ԩs=uL$_+{nf%S2e7hY)=72dHp%۵k7x?F?S͞=;x֦N5[eeesΗ^ziؙK=H:}͵ba¬C#s^T7 M{r`W^ v|ǎ> t{''f۫}vC~45\?PǜtBn;s9EkF u=OQw.}wEw7N^<`McڬW)ڂr# NzatʭI^yuV_ptٷf+"K4tٙW8ص(xjUcw۵ l6$Mm ClB[ok׮N(+v\̖|}=woַ.HLW|dɭM`mH.6(hw^tEdDv-mB[6F]C hh#Pʢ6 ٺu:vx!}L4)7|lݻ=1.[nh#@rDF0̞={_zm&k6$Mm ömh vlÆ WO6 zm5D&6^jU4ٲe-}4h7F]C hh#P06o3cƌ D_7 !h+M`mH.6({l̙DRzk@Dv Fb@3Dsmncƌ/*+`;i@ hh#PxMZUVV3&P@ hh#P^xᅱc.\0s1c?K ?D&6O?}oiӦ-Y$yE{ٯ2I hh#PV\_&27hWK<˓&Mz֯_})h#@rDF3.h&60ߜv1@\l4Q b7g]?$Mm(`9b hh#@`4FE oH.6(PH2F9GD&6ӧWTTf~sh#@rDFBRSS3`t19  @6$Mm(0sN߾}tc9hꫯ6$Mm(0g...1xA… ChjFE O~;C=4@6$Mm(<7xc裏n= @ hh#@ᩮ.))}DFE 'sWGwFE ٳo·r… Fb_~7CFw7FE Ս7|s.// oD&64X*0aB!|mEwZ2 F-h#@rDFIRcǎ]vmx']իo馔t#:6$MmhTs56ҍ@!\l4QRrI7h#@rDFe暰]UU~mݺ5hoܸ /4hСz駯^:3iҤ:cԝn\jUΝo/ܹs^zi1bĺuv>mH.6(L_?à=a„뮻.?Sf͚O~=sak:ҍV >XΜ93h?G}t?pGy$h|͗\rI!͐h#@rDFź+}'zzw#{wo>lذ#GVTTDI56DtUgbŊnݺmGu=_|޽{g 6$MmZ^{1cȑ#Þ}s۶mym߾}>bĈM.k16H hh#В >gϞOh3FB'\l4Qh֬Y3pN8ᣏ>G1sTG?сC0aBa~z۶m;찣:*{!#G 3#gΜ}/h'̜6sqM7 <8#+ \q{[nUUUy7O9|9|?<}7+FmH.6(ژHw ndaee{rʰ?݈ jI'#ȑ<߶k ڿ@d̙|_#+V~a|?5\=ϐ!CasIIܹsûux뭷h)V@U hhc"ޫEJR7xի[l9Fqm͙3'װhc0{qgg} oѾ}p|ڵ33GYGf]_G/R*<{W0nQ?xfgV#kjjW(| #@*\l4Q1URň*nٲ=s'^zYe>aÆ̑.]ne9=rԩanOe[aEEߎ1"2a}r!{Up /K=N:;{}9 r6$Mml)Sޫ۠DD~;;|+_)//hD&66FMMM^} h=A-ѫtA #@"\l4Q.t?hGw\ "ѣZ4Fbp޽{UmЎh&OF06FE/}6E_ңGݻ6,mH.6(xSL ޫ޽{yyyt@K'\l4F?ٳg6cv[^Fw4'5|'nPEa$ F]?~{']ϢE~~˖-^n`W)G}Da$ PFW;V^1ئMKKKm@)z4s*'@6$MlFw]LZtرcWEBrԇh#@rm,//;wnL_=E%\l4YD_xiӦE ǎ+@)z"nFwO>;vlL-],dMGѣpumH.6> c_tiGAS9rmH.6۷o?~|r.Q]]=qS4EBr"\l4q7GWX1mڴh]e[n>+@b- h#@rm/dɒh٘]e3gΌ>+@b- h#@rm8qbf.GE 1E@J hn6qњq[xqN|@"MB(tFuqѢE{Gق۽;}7bDi =vr$$\l4Q(n{^ !Fbm\|7񍢢.]\s5aҥKO>tAsOt*~<;̛7oݺu_~y0yΝ/קv֭gϞO?IvIIIẓv|ꩧ"#CeeevyAτ 8􀫮;N*++/`:uKRi Zpѣvu"#gLRl*'@s#\l4_9眪K5k֬]N1u7o쳹f;c`ܹs3zȇZZȺ1?lTNfH hbEW^ݦM7|3Cp~;1cF֭s=̙3w:\2hWTTa()ЇG͛7cǎ^{M:u͚5_WL1[m۾6=CK-zdNd]yR9 h#@r6mڴI]`A7+gwȘE(ȗ_b w~|+;9s愝/?q9z gެYm۶  -VyL*'@mH.6XjjjN;?jٲeGuرc#G⋃1˗/?ꮆ2K. c_GUYYy睙3gf:*SpG}tÆ [l}һe 8pذa\yguիy晰 Zv#ld]yi*'@mH.6ܣYf-[3ر?jӦMw}wĈڵ;(zN:=sUUUW^yen:tЯ_+2U\Gez7 <`5{!O:5k;/XΝ/Ұ Zv#lZyÞ@6$MlF{-4oVvj@o*'"\l4q7G'N-7? /vԵ8otDk/ho =M]D&hSO=ջwvᆪF(֭ر!G5>HA#TNK hn6w :,\pgHLѣP9mH.6֭:ujl̮RZZm۶$Y6$M7߼e˖h[fMiiiGAS9rmH.6ϟ1cFxLM4ih"ME h6Ə_YYOO>?}&&QTN:6$MlT*uM7UWWGGw}ѧhj)ErM hb6V\yM7];D ?= K hbs6>[neƌъ2Md…ƍ{'Erԓh#@rfm UTT|{믿-0(K,6myO?4z]BѣyR9J hb6֯_UѮ׿U QQr4h#@rfm,ޫVKahTE&6& hFIU hhc"ޫVKahTE&6& hFIU hhc"ޫVKahTE&6& hFIU hhc"ޫVKahTE&66LUUU{Ud/@K0ZU!6(0ӧOH| -DBl4Qajjj .߫?-Ks=0FE."~^?:e98ȼyz=zPJ hhc-\ޫ۠ѡ-O~0XRR0FEcРAz߿^':Ozy'vaݻwݻw~ r6$Mml)Scޫ۠>vIDATDD\pA:; ><:mH.6(555z DDoF*RRRrA)h#@rDF袋E rqaUN=ztt7@&\l4Q.\اO* -O~0xG*h#@rD4hP^Fwh|IϞ=w>lذ>N hhcM2%x n;Z .{-h#@rF?ٳgL0!x n; _|OjA-Z4yo}ٲe'vVRj(,-0R@p`mH.6h?~w^:Zgmڴt۶mgv h#@rĦ6w}U ҥKnjSUU} ϔp`mH.6sFKׯ2 &\l4)/´iӢTZ׏;_5`PjQmH.6dO>d̘12*-{WVV})5@j8?F,.X ZC%.]}@Rjp OD&6MqǏVOi'N}@Qjp OD&6MqŊӦMVOi&OuR4 5FbMm/dɒh锖k3gΌ(5@P|mH.64Ɖ'F릴h}QYYYuMDh#@rĦ6qѺiZh{-/ԩS)o}OK آEcho4T-FpY'عsϟ??Iik/M;[}@>6$M5F_{\,YRRRR ;s N<۷رcp?>+СCaÆ=;8Sw!SO >SN#}zz=a{ڵkIISO=+++ (..;άӆ3{スz>|xd%W>KM[n#}'M%s=#F}O#)?uGqOho*|o|EEEk& ~׮@Qr 2e~κ@ee\|+O4s=ܛiŊ{oݑ+\>uL>Q*G("bJN:+Vϋ@O^s[:[}{ rU5hrFw1 .ZlQG5f̘p׾??:th:O,_|ӦM^xȑ#I2>5k֬]N.]o͛7C :.]ڦMjժ: g+6lذ~ڏ4yإs^Ǝ[RRr7c9&}lǒudڬYzի۴i[okA?O]tI| N;?yzRYNW{q͙3i3g}v}kU3=2_m nOݥ+Wߩ/_]OHG}Y5ۋr(RqT/҇Za?-mvŊO?xɒ%˕QdkL>QDwsQg>:*ݮ֮\M h#@r]m\jU6m2zo߾QGy$AX]|{1k֬-[k ;_|t-880h{ /QDmܸq„ zJ6s+ɔVZUy,agdm^{[{׳>۹sAY8#L^s}kN(Pd-DDG)}HYm۶P}z?X]]}'tAF c/WGQGF(]8JO9l"g>.ףH:[2{0rU75hrFwE1}zk/}iܸq` /*{GtM!v7͠ٲe{lX}w6l ;.]t@?jԨp̙3Ó'Pw9G ߲eɓGe6sTgYWiX2eXrĉ+ٵqѣGO=tuOSyT_=K>}z+6'~\<1=25 Gc#!gĿ]ws|[e.]jk8PtK&hڵknjs1~ᕕ7xS_ƽ{?#8bر[l6Yjհaڶm{'ۿ[إKSwaŵƍ^뤓N;B3gƨs/_~g ҿ_WQfܓtcf]͹W崆KWޤi׮]xk;wַ/VEkZo4K >aZ=pgq\pA~_+nݚ?kN/Gr^Ⱦ}5 ?S&90Xn]ӱc.,jl̫G}0l_tEo***fΜ'{ۜ{q yFN/7CNeGy]w){|[e.]jk8PtK&hc>ٗԛßxx+pYK M.Ph#@ʼn6vm񋦍z뭵VjΝ.||i \ PhbD)?.||i\TApD~z+~USSƏ((\ Phbq6m{N)]3gܵkW8"q5hK&'L8qǎ񫧔7Μ93~@Qr h#@E6.Yd޼y ӧo߾=~@Qr h#@E6'O^n]*e/{h.5@GpbF?o۶-~%Rٳg<\jq h#@Ō6k׮?~T(I?+'R+p6.1XhcsI&͛7/~UV?kc@s5?D M,~1R]]=qYf[񋬴+Vx'Om(hh㾩ٳg"~*/]JK #-ra(K&61ct%}W]J߭[ #/RUU5\ PD Mmg555:u:䓫wUx ˡ%ǻᄏSN{.DΝ;0F%FE_~rJ a9;ݻwWVV}٧zjEEEUUUPD MmlYfUVVVUUEWrhw(EFۿۊ 2{x'%PhhcSEHX-NN_9wPVD MmlѣG/jտj&Phhct%|Wǰ_ P(8`Fr#Phhc/|W {WJh#@M7k֬]+JݨQ***fϞ_PD Mm7{Yti.ACKhke#Jz3+**L_QŸ5qo\6.1(ڸ/^{͚57ܴiӯCK*oT*5~ 6Ƃ M%*Y7%%Phhc޽{ʔ)C Ypa|]6 =C:t/Xn4F%FE޽;_s,OK7@H5F "Phhc)S\~协**͘1ꫯkkk=O>$,oٲeԨQ}իȑ#7l>}zöqRrt;v8iҤO?k׮/B߿wCݴiSGpD/2dꫯ^wu ֭[x ˡ%C f6_m۶wq0eʔ0ڇ3Ds9rdXkӦۣBZJ4n\~0?%I5"טʓ[hQ{^reԸ|Çr)a&NZM6`Э555<92ā;D 7xck׮,Xm /ףG#FMꍻW6={v>WzN7fDz?oL̝;O>-٤܈6.1(ؐد6/dFU@z㹶,9È={ϰp 7\r%QgqGk׮M?s9sRyFľp+|D!1(ؐo|oF1cI,O6~ @pQ$R믿 ڸf͚6mՅKVVVuz۶m裏~={ѣDZ5Z13waر#,h׮]!,}ѩ?.x{s ;1șl߾/=m@$)PLIPhhcCMv=+=z3O6~ @pQTn#eGkkk۶mhѢ+uQ\3c|9`̶u'xbԨQUUUcǎMi OOg*;*șX},rK&66dҥ7r5t=i+öaʄ%,nFٳ9s7o;j4hPH~C9 Ok֬җ" =ܰѯnٲeɒ%Q{cVVV^|ŏ=֭[|7{챿oSyo̎e>1gLELjkk]j\#@3 '"@ ٳg\ptݻfCKhk=Va0B@iK5n|w<Ο;sBE۳g\2sذa'|򩧞:y:u:6m F}'8pb||??r){SN3=Cb{5}m1;ITcZ3όsk(&wP(\b4Q1ŋ /^zu-<В![(.jTFJ\#@0 '"@ɦLrg"cB?l_@pQ˕\#@0 '"@v|/߹sg|]' P&RҍGBpDFٽ{)S p amz595GJ5;rR(.1(ڸ/^{͚57ܴiSh a9DBf5JJ<5!;rR(.1(ڸoٳtiӦ]xguVncX-=o@y|[ @II7:F2ar@c6.1(E0gΜ̋=M?TK\#@01D Mm"ٳgXFh a_PRɓo?\#@01D Mm3fL.]`EZB{+-D Mm⨩ԩ'\]]]QQrh T&w$m(\b4Q_~rJEEEx ˡ% 6.1(E3k֬ʪCK- m(\b4Q. ˡ% 6.1(4zݯ_ @k`r@D MmbҥKEEEx &w4@pDF(~UTT Z;m(\b4Ql֬Y1|D MmR[nejʔ)1b >gP0?Zn0h [l5jT߾}{5r 6D}O޽{>}mzL)7ؒ4pkj;v4i駟޵k^x!j߻wCnڴF@yIvDD Mm`ڶmqsΰ|3<-ϙ3gȑaM6۷o Rn5N[Sׯ5ܹsD{~'ĉ/M2*?DD Mm`7n}?NXrg{|>gоgϞ}k#F9sfuuu| 7Q[Sׯ?â5ks1MisX^|yUUU&@HvSH"K&6ӧϼyFr!vٳgϲeC^~噫Rn[SH^DR]-{ @DpDF{neeO?n6lɓ?7|3,ܹsQ%Kt-?[SnM6~?~ꩧB\veQ#P>R]dM!6.1(`Μ9UUUяFn:f̘={7<Μ934nܸ_j޽C.\uN5xwСCpAza0\ /{RV:3IGuTsanxkNы/8`^ztIϏΐyk*__߿܆iӦ(iW4ѣGTWW{,qVٿiwsǦM!6.1(`ܸqӧO6Nl[A ,ܹ… ҵ Gsޅʔء -ZbŊOr)5j+ 崯}OxS@r66)$@!D Mm`{}gkT Thcp~_lIe?~|rjůN:NkiӦ 0gϞaÚ2r'LCZwçV nֿ={1eʔj7#F7̝;O>-95dK˴+x;u-眪Do0ڵkj}aڵ`hEE??x+WfU9GΔ?6kG¶aٳÔ6s3T,_|᧜rJxщ'rsL D MmhvoR_Oe˖-C;wܰC 0 3ѣn!m\vm0gΜs9'9;zS{\n]6m!Cx≨1bDK;w_og[re(p7~ڕo-眪DӟGy$,IP۶m{챰_"-={LO.hYUΑ3e~đ;v,\+ҥ˅^k7oɞr3θۣh=9ZsGSH(7>M Phh#@SѶm۾7Co-[2%Ua58K֬YӦMttG={٣Gc=69;dޫ;v8묳:u>{Eo߾/=mXg)?94Ӯ;u>R*T+LmR{ZڵIObSh~97r|_1#g ;9o޼]6;|_֭;CÇ3{&=Z(ʍ@9.1(Եŕx-OV}wyebڵkm۶]hQX~W̡uȾŕgjo^DvO4jԨ^ΩJVz9g0s|#gk3rd֭O+6l''ON >GSH(7fCE Phh#@S4nRsh<Ӯ2g %lT MmhvZ6>qS 39#Ӯe lT MmhvZf6[#)Q2*CP̆IpDFfhnn%rS M}M`UVL!T *@N] 72iLʄ)$0!"rR(\b4Qjkk3ZmܔP@v=l !PDNK&64On1"؏BMJz|5@m۶wq;w SLÇ?39sFܦMvϞ/@9.1(ЌjjjzN:=s~jPgоgϞ}k#F9sfuuul/@9.1(м ݻW]C} \XӧϼyIK]]!k׮={,[:t__ eh2'U%FEٳC]{M7W]C} \rޱ>s+++~t˰a&OgϞ駟aaΝׯ:,Y[n_4IpDFQ>g};9s Ŕ)SB]{뭷WР{=ӟ|IpBJWf%T>Uz|Ei 0} 0}9s?[n:f̘={7<иqƯ~{\p_"<4NK&6 fΜywTWWh ěhKu]SN]zuBʭPt+^$.L„.Lxqrޱ7n-^sM Phh#А_vۇ~E֭[̘1c׮]c U(BʭxF 0 4Bubݻg?1U("'U%FEfϞOo?~|mmmH`߅*WĊW]0 Sqd]}ݱvm hդpIpDF x_ /G}4aW  PV*^oQ/LGPߜ9sO3X6Ց("'U%FE^}՟`|GǏ?S4Y(BA%XD/LO~;֡% H*@@ݻo0h1V\9s @R*T{a?, W]u;a9xWVE T Mm_~9~ Z'V$ ET($L¤/~djjj:uTYY/VTTǰZB{+@"PDNK&6q7|s0[nя~?vHPJū+Lŏ AUTTp H*@@=֭{79s'|?/OU0 Sd={v޽+>CK@k#PDNK&6?_bE"*^WQ~¤/LOLGrhwhmpIpDFӧzAK5s @~~0@}W]uU׮]+**cXhpIpDFo=~˫5{W>xk*u-tر]vK,СC*Ϝ-[mUZm'mYƷz5{a?>jjjzQQQr|5@+$PDNK&64myɢ㗾xsÆ ^lٳ4u]G}ڶm[|u{ ߮]#8s_|q_aWa߾=h،3G0)^QWܪ*^ f-E1 TQQ+Z')"rR(\b4Q˖-nٳN.tг>[WWoxš5kNڡCzEfڲe˿˿ 0 ޘ{)F}$ڸ?ZN6@c̞="(sTwygΝØ:u}ѡ… /qwyQnݺ7- /^o%f͚u '{&ik֬9CyX{Bk7ْ{<}T4L`4-ژ&ofcEEE -Z@9c ?sӧOˡ 9G.sثi`iE,Eh5k͛jI*@@=Lٷւ?kkkWZտn)j׿o;guV⭵}֯_m۶^x!j|?[5jĈnٲeʔ)'pBfc*뵢j%?W^ [VUUU8 o3ial۶mذaݻw3fL[//73[Ry];_6&D䭢kSNgִmCwZH9-@> /怽mvM7}h%pIpDFD?PWWwy]|ŵW8p #F=zt}7bi 7lذm۶_|1|U]]yQF5Ngrfcx1cƬ[.w;<wq]wu%_h9!{e˖~'xbj-#<#ؾ}{[|iCKP[l>}zXڵkoۇ^<3]Zf v5K`42ȚV tfMXfv.bBiFrz'+B5u뭷("'U%FEz3Ϭ^ .8#/_y[n:\rСm۶=v,n_nݷw.ҥԩSs3s9vܸqs~I'qm#˗/?3߿~|=^㎻KN;mذaU9}FE]Ȟ9sff!|/QΝ'/g2]ZWEkZoL`4&UW\qEC/N,3k|mEG-ц-m ۧM΀,O=TMMMZ)"rR(\b4Qhc͝;7%B("F}ml2#@;c Ȳcǎo9~@9.1(ShҥK_y啰ok̜93~_׻ oDٵk_߿yaxR8E Phh#PO`.\m۶Gy1cjkk=0wyg P>+([0^hQ x뭷}axR8E Phh#P#<[oůTA˳dɒg}6~_(B(?a~6cƌ4(3hpIpDF͛7w}T̘1c׮]#B$@&}IbF[lY|#OᘉF@'PDNK&6q&MڱcGJ$6l9sf I(B)('a&}#7}wF}omh/})zzk׮m۶}o~ߠak4 3hpIpDF _7o^J$~.IBJxuE9 ӽ0/< ^WWw 7'A14-4yVg۶mPڤpIpDF [neݺuU2̟?4N(BA(a{cmoo꫷mZjذaڵ޽\hchѢ0`iӦcdž;v}Qsc|·Ν;hÜ;VQQ6d^%{՘ W#wygӶmN:eG}tp¨=>{w^ԡ[nÆ/ηUf:=\F(mR8E Phh#~l߾=|un msbo!ac*-=`ؽ<0>_:gC38#jϹ:tPWWbŊʰQcǎCcwy|gD5("'U%FE֭[7aHӟPV*^oQS{5!ڸaÆ;S]v~}F7n1sᇟtIwqGsa+ڷoߡCŋD;K/4Z>ӆ ^s+F(+fCE Phh#@S@ˡ>l&;wn#Fh̆IpDFfC}ٲK.}Woݯ_k6ރ/3h̆IpDFfC}f̘O`…UUUm۶=#njS[[AaxfCE Phh#@S@ˡ>lO?ҥK5cŊ?p0Z?HiOaÆaxfCE Phh#@S@ˡ>Nux r[ *@N] -rzx ̛7/~@9.1(Եr 3f\2d~޽;~@9.1(Եr ]vp }Q<{衇^{fCE Phh#@S@ˡ>N0A~?4@a6PDNK&64;u-shXmm{x~ Ъ *@N] -ڵkƌ?OnQ~6mtW@Z!"rR(\b4Q٩kP@#^zԩwu+QvYSS3cƌ;S.~|@9.1(Եr`|'O?w9,!TgOj *@N] -h<PJIpDFfC}4(%iE Phh#@S@ˡ>O4"rR(\b4Q٩kPrJs@9.1(ԵrS99 T MmhvZh9@RPDNK&64;u-sT@)qN("'U%FE6ih2wRPDNK&6ߜ9sO3֦M}4;@)qN("'U%FEGs΍%Kt=KW <P]}4k8*@k|G?]dIhK]fPGƨ>zh u{+@wRPDNK&64Ν;wG u%KtxW:WYY9k֬x'`xyCP;@)qN("'U%FEK]]]ݏ?iUUUhwP<*Cju9k8*@k&N} (wRPDNK&6-ݻ{I&=+VH ěZzk֬YT]]PZBw 7lذ!^fT?~Fh>X9 T MmZW^ye„ 555uT͛ [l5jT߾}{5r 6D}O޽{>}mz\ckpq;v4i駟޵k0_{С6m=zӍ;֡% H*@쩧?~ʺune۶mwΝ;)S}߿SN5X` HNNfsmѢdkĎ9Ҳeˎ;~BQLvY>j.]{NMM͛Ce"իWߺuk5c R*͝4im'qqqڵp8;;_*R8F*tFm?|JO_>_+AG^kh"4@իy5E?Hj*Vմi|*O>}ܸqqzbbbԚ· ߺubccClKKK VjMBQ3b*Gֆv\XY*uVV^vmHH+w֍Y6C,?m@\\i3:uZWWÇ 5n0Drrr-/f)#Mp:6DFF=t yJI` BiAalmm]]] vIniذ+B5.m6T?7c!҉'N< [dIqq۹sXcKKKjFezuwwg-7nإK???__ߦMRMYYYϞ=ϟ!,qBQ3;6`bz*rss鱿SjMv矧tU,%oРkyGGGV.))CsNRk׮uԉ v#Mp:6m j>++kijܹ3~xN"FW Sܔ)S ggqI3gθrRRLLLPPXST։T>uꔐJ,++KHHXti޽CCCeknݒFe{ol1 =dgg[YYfٱv&M֫@5Uj*737U&"&ShcAAAXXXƍ{۷oS%=; t=|pj`oo?zhޖ.]z[,YnmmݥKϫ%niAs܂O&wC:zSdYlG߾} E0;$33sРAvvv~ܸqVi!!!>>>tLmfZt#mիi&旔cccu5>$@v#Mp:6m[SQQQjwZZZ :tӓ5‹W\ynݺ|jhkk+H3Ga *m+V=@['N4lؐv899ocm222KM=Ts]zܼysՒ>Yk׮[lAU;j]F-Ν+B5mv՟k|Rcjڗkn`DNg4F0}P yG;;Fqϟ?s=س ,M|w͛7hР7WiԚ_>}gϞkF͑bJ${(JqRܧqm0j~T]tcJJ_``x)K ka\jP!hFn;T*% !MDLr\Izq^^𭄌L*۷nmܹsg޼yg*uQ?ipm6IPpڸe[>ڸuVA `\F9Lk|b 9n8VVTM6}ܹ3r۷if kӮ];e~Đk4;Zҍ9993gҥKf>ϝ;D.5TlllRRRN4Wx 7C 3h#L墍dذacƌΦ7vMt_ [o۹DOpȑ%׿6۷Reee~tAx{nݺpB;;;Z)7Ν;O$ܡ  **##s}LilĈY``qm0.CF<0Dc*^w͛_۷cccHRKK˒V/Đk4SJƜz޽qqq]ve[nŒ3F-^j,$$$s?Ksl 7C 3h#L*jرNNN 6lٲŋYG}dgggooرwy^:88+m$_~eիݺue~W邬FY`mll+m'Yln޽FXPhM"CA222|M_㏋r}jjɒ%҃ALFsc\ӍUcǮ\HMM}׿~jA导JHHHTT{55tcNNN X9++&kiiPիE&3fK7k׺M"ftFmmh#qR5ҍ'&P .]j׮ݶmBBBXMiiÇm_gϞ>,ki PnҤDYrr ] ܾvZzJ|S`8DDd7!` J%~ɽCͭjj́r5j@%}׮]BMppY˩ѣ .P޽{999Ǜ7o.גk~E۶m۴iӪU-[-mRƍڧh/̙UV!^*===00:wԉB˖-w~K7Fh~zǎTq*,tؑ===鯋_޽FPA6Ng4F0Dk4@EƊQ)\eP#X*U(W{zz/cƌ׾}{EݻwΝ;8p5֒kTkrk.oog盀hx WRRWUv[JB۷nݚ-"N7*Eϝ;@!((Y%+Vxxx8?տ5km0h"`2w>u.~?P>>>BbLxtÆ ~~~4ϦՅ͛5)rO4cUVb/J5To 2vJOsgy111]vee?d崴4'''J':u %=$''ʹshтZ5bGiٲeǎ?s!(yk&;w,@LvYg}F5K.MLLd ޽{JWq͡C իoݺ5͚1cx)QN4ӓƸvډkL =l$xb0 D 3h#̃l[N}"OOOnd.\j&駟t#T@M߫W3ר UX}VVVӦM<}qƱՋĉ ߺubccClKKK VjMBQ3b*Gֆv\XY*uVV^v-=rn-ZʴeiLѩS'֒$$$>|XQt#@-6fm|W_Fh#tFmSZ`AQQٳg'*-[LJJ… ]\\ܒ&EcCh_*S *J5vг!҉'N< [dIqq1=ޜ;w5fT>y$kZnܸK.~~~M6={͟?ݑ,D=sCi1 J+++VɭWh "77~mPk?<zf) X+W8::rII ;w:ݸq?UP)ۛ>(H`8DDW^Ϳfĉ[nO0T+eׯw|#G,Hoեz>[---[I3gг +'%% A=JemmHSN IJKQhhl֭[hl m &Bl++BR``QR씔;;;V)//Pnl3VӱcG_ƂG&"&ʿfԩ?.T =\9;;h׬Y7hdmۖ TBeUVuhcDDD^TM65Kɓ'WɱJKKrxx8 Sc5k&ae/yʔ)ٞ^x)m &. F~vo߾4w9~8ѣKW!_)ڨRXK-*X>< i 3h#Ç'O|m40ׯ?}4jJJJ\c=Zn]ZZ7hd6o\6RcUׯ[XX_[ x0wڵKYc{{O>߿SNBTq...]t6m ݸqN4ر;5Z-Z|BO35jwΝd76&&&oߞ6w-^EffKy ?>kCjՊݬYKW!_)ژZTgIUЍ>< i 3h#J:u*ҍfȑ#֭O*&$$e5Ƨ~McR ը?[՞nԂoΝ+B5mN՜k|Rc7U&"5JPEEEL2GzD1Nl` FѺukgggOOO77_~oPȤI*X4zFPi `8DDk\XFTTÇFo̗FL00n}7͚5sҥ˚5kF5LlKoU VnMD?|e>/2_zD1N⁁t#͍H-oPȤ)U VnMD… DiXK㖟 U VnMDł D! )֛/={vpp0y~Ax٫W/B:tn߾=P&a+Bm۶]6l_fI `kk:lذ'OD{$*>]!JTzn!6i@ߴi\3g3THރ{Jȶԟ!^`_&ŵYYoΞӰͿ^Ϊ$;0nC9;;s ?QJcF,?>0"T 3h#@s- +A.O믿ٕPƍM6^/_ܠA-ZO⮨PZZ:qvڱJ ''4Rmm@lRgϞdzYG&++k϶q$䫞*>G6L^ 7$$Cmگ;wh***ȑ:{lEwB Äv%{|d[Qj;~9U]PuFEh6#o׬YahXO+7U&"P\ B{J69h ;;ƍ7Nإ,]Tdwܱ>z(7l0z_~Yx-,8mڴ̘1^wʉ1Bg}W_hlj*VևȄ:,YN{ץKS =oy$b˗ھte˖yxxWp.uQ{l-ÇJ{{{:܂ʮm hGGGWWb!!!lH.sCѲt\\\-Z*GaK)m-*oŋt"BRRׯZyJM-UJǃ_봆P)@-QjmPm+;>B}Fz?O}ǪZyE빖E>JVw4M6_///Gx###kD@3gMP5hl J2jS`pS0h"Uϵ`.G"2DRL:տk#F7oSX^.\Hc._|„ PիϞ=kii)|"۰/*_䔟宴4ʢΝ;׸qcRzdʻv,**9rdHHiͯ]>fFFFΝ'O,ᇅ,%{.u*G<`/տC܂Zhc뒒ۭ[7X6lۋݻsz{UtrKhJVΩҺ:GZnr[ Fv3B;2rRTNu!vv 4i>FݽwOF5mڤaC6mZ;*W~?׷-_ܼml۶mf Fi'ePZMW̕m/^^|dKԹKQ&KJ۵yy†U~iV/껗>ƍRPet'q7 G,۞uu YڤN/^Y~]9 9PZZhѢcǎT{UPN<5o<6]_`V0"|Z`DNg4F*Z0ڣWQnns=wUr͛7_mۦOlʔ)_Ԛ/M, {y.Ҟuu+M/KJ;O&C6zPп'N,((/>0"T 3h#@s- +i5**jܹ b5ڵB;;;_Ǐo͛7ŕ ꫯ\fVf-oȌ?~СT̤Y+++f9sFh/@߁uVoooV/^ҹPYe?yyy²xAa]Jkdҽ1Bmdܹ3o<y@vSi*Đ닓H]|tV1&۹x`k55uǎ/}džUh;*ݿ^&F / Ӷ+w6ǩz\iQNdW!,"] I߾i#ܨV98ر6AFGGơ14}ţR{#(z-;+^%>r1 wSxۓ'OV"1/`S 7ޠ'X~T>0"T 3h#@s- + -RZZ:`aÆTΝ;O25 5jիnݺ999-^裏%^_QFW^M}gϞ\_~3fW߿AKQ܀lֻܸm۶~Au… 쒒7?\XX8rHOzz:Ul1Yto޼ٵkI&z.%{."`̘1jͪwz뭷~;778>>[P.ٵpI2^5 1-ȝZ }^^^jQMmhFگ9s0myXX+V>⳯&۹Zn<kK܉R3*P aNfIF:+nŽP/la$Kǩnn/m۾24?~W{{H"w@tR?OC:uԺu'q}c0a6 ӍpS0h"Uϵ`.GXbIl߾}o/ƩAAA֭Zy3y_~%u^nذ^e/_}?[k4m499Ym'*v%(nl޼K. 6ٳgll,?{,vdh8;va.// 裏쨫cǎ6\|ƾ$OR:uTc:99ѮlRHfggfS3Pn_~EXZ$t/Ԛdpp0K,NիWuFR_-%=8ݔ݆ 1$l2d m^F _0teWtrƃx񎎎;w2Im<<<""" 1qo#J- j^\Pghϩ?ȲneklzT8tnرĉcY!C^۷;⥦},=7[}tmz޽#ḳTr=f݋[rR¬S?mN>C{{H"w@tR?r5ǏP20/s_-#ڈIv^T|t#>0"T 3h#@s- ѫJ"-I6bHبJz!9deFT lgװqFƅoII}ر#_iӦ {rQ$'Tӵkg9iM9ǃml۶mfJP=X4Wv/Rn޽#UZ[7h~{ZRu6 }+T(V]2]u#5{m%(> :wVK?OryfqUVL2GzD1NQ}\1<݈O+7U&"P\ ˆѫ'O:u ׮]Сη֠9{!a##^_5Yz94JLcݙ rl֬Y5juQQ7|_0~m$; i `8DDOR_rϵ\c…GsuOOOӺcƌG V j5#X ƚ/;;{ݺu5QQQp}/O=d'Fp7U&"`|7nd/cǎ?$YYYn%K,1 xp}/O=*1K1""`@$**lˬrm$;I)+nD pS0h"U"<<=&&cǎQMhh((//5k .䯙e ?ϡ1a9KqѢE5[{{{O~ԕꙖ_>_\D؆ /3gMAA?OGČ؛RW9~8cJ-dȩh>_TJOLgiKgUh}D)#Mp:6TdWWWz=v옗z)@My'Nyk׮MOO/ e ?Cc¤}FA֛/={vpp0y~Ax٫W/B:Wu aaaB"a/0۶mڵkÆ _~常8aÆ ;y$HĸGRJ ;aJJ!&o~}~-001 $M6-ͥm>s ?CAE=7ٮdlKUԧر52rAd  ӳh>MUwJM7"`DNg4FһwVZsСy)SoZ*ח1ʩW&-R\_+))7<<<6m*W^JJ [ 4hѢO?$ 'Nl׮prrJKKc/F$..q˖-{?~<%K|dϟoool?H5GW=UuUzA}vE)mRnHIȷ __wܡUTTD#ȑ#=uيlKރ{Jȶԟ>^2.^<'2rknKE(\~=ײhG)JNUwH4͘o ,//p7U&"PU֬YF5ϩS֭[:PWN2ÇR)̈N}TlL*:hcffA7ntPV^d;].]Ο?O54Hh X˗/wss}X ˖-۷Z\p*(( ۷ZNtccc] :ю`222BBBؐ]N˄" E~@RJe@ }mJ__/^.|wN~+P:hjIR:ćw^^^5JjY@RmZnk]13B=gI)*}j;M4 #޻'6mҰM6-ϝC+/Hsn^Rp66m۶whҲ +6Wi/]2%Z*n׮[E&,l[hf?oE{iܸm!5 5[|N~Bo7yʲY[MHΝ5St"~)@ pS0h"UݝE===oP#]6>>),,6mJ/2 Uqj+iC +=== `ԩ^1b߼ygϞzpB*;v&L^ ͼW^}YKKK نԏPyu''|.wAVVVz诰Fs5nܘ}]233F"m/m vZkÇѹsɓ' ~!X0K\pU <8//vy_|S#''Çe|%%%[n-8l0ݻwNs, {y.Ҟuu+M/KJ;O&DL7U&"P;wLϵ͚52?f۹s… >0Ŋ׮]e+U3UéMG".(dӤI,ggg&D/-,,.]ڄU={= ڨ3ĝ(5B &dϚ4iRVK/9 Nܼ$nq~Ҷ+CCwW$rD*íNh#I `8DD֚5kv ѣGkDA5'I vz Tq}X5թGX0Hii R222:w?{~F]z5={s٦~͘1C\Ie-FqqFZmJJJh/֭[ .KJJ7nܨW GIOOJm Ƃb4͛7v:i$V%dυx]3&;;[YݻY[oos %.I& f#FA+ھ};oKpp9 J) "oӓ+--mΜ9,G[++@v=ʮ*B2D 9r -9'Y֭¿}MsꃂpS;8q,k?dk}vgQԴ#gF=cTu+O4Bݻwd:y W/Lܿx{q󷃇~~(iQS6mT)vĈ7ݿzǭMo&*tFmdƌ ٳ鹖3جYj5`V) UI{%ۗoھqQQkdmmݪUhg;_R _WW_ AӦM٦8߯*v%(nl޼K. 6ٳgll,?{,vdh8;va.// 裏쨫cǎ6\|ƾ$OR:uTc:99ѮlRHfggfS3Pn_~EXZ$t/Ԛdpp0K,NիWuFR_-%=8ݔ݆ 'ߦD3dڼFA`R *@頩:WˍwIexxxDDDh?AbdGZnr[+JѢ^H:FFNRk,&6zrMnР`;77.}K"M껗ƎM6-_K"&995]ۯ~~=AA66m۶wYcN~yyZq{+)7wޑ*a=-[)vE^^n ڄ}nul*S_QV.H>ek$Νҏ&*tFm:d„ ݻz_FswH\`MڣW'EZГlr L6A]_5Mm&}&iL6nMD SMFswH\`MuSNQڵk:t3SQQQe7#^_5YmzD+=!ڨ<nMD:kdn0kx*@dՁ<==O3FR-@?5(fFoFj:b 96VzBQxn0"T 3h#,ƍceJմiPΝ;#Gl߾}6mDDDk׎~-Ɯ3gvҥYffΝ  *((xv!>x*@/\|'4_6ze <6bĄh#I `8DDf)..~饗ݻGy}c؁RҲ :kdҍ999t߽{7vnJ3f=ZT'Cjt'N>0V•+WoCFddÇFo̗FLm0 T 3h#8cǎ]rᑚJ5j~jA导JHHHTTTRRr"Ɯ rVVM?իW===ŋ@u;ڵkgc0*@5k֬2m:////**`*ח92#ڈ&*tFmҥKڵ۶m[HH)--_]Ϟ=}Y5rFBI&OmIAl Xoν0S!̑FLm0 T 3h#D}uwwߵkP6> ;hPV ?n4.Ulܹ ٳsNR\_(FLm0 T 3h#D1coߞ޽?UvcK)!hքHLLLII[nͷm\KzaF >0 x@cݻ7gΜ;vY!99yڴigΜ cUU0:<nMDs{]ƿ$GKAeeeTr劍 k@eGGG}_i={͟?СCBόRYYYT>yhƍt۴iSeIIΝ;KRRRcWW@͆1JK ihKh{u*0ɳg^b˗-@m~z۶m{1I:O=pS0h"\]{RՓ&MK-FXPζ*,,|.]ڻwP,nݺ%6ڲϟg*:11ʧN i);;;V֩`ʔ)j?`8IIIZz7,sƸ8Zm ?nXM6EFF.^8ݍ*25bbb)Rz7U&"P'*~JGI߾}'O̾Ν;ǏB~~>a <جY3V. ull,LVQTTdccsi*Ϙ1UXYYQ9<<\mda\:y9TRR'z7o8V4hI7#L...5JaX0\}F*tFmZt!w}uwTy kTٱc;v٥H|||- a_]]]v:m4r...R遁E5>5nnncr4׳gOg zhyeaX0\}F*tFmZtcM#NOVk|R/0_xQ"N7*/q̬taƴ45ҫW/X p7s `*7U&"P'*:XsOfJՁ~'Q~gS?|n cTpnMDOTjsK7V6qqq͛ݺuݻ5k OLPozj߾}@@yyy;vo}X0\}F*tFmDFƧp9,ءC_N<)U'goѢE=<<wq};3q! T>}BT=h"F qE6*@XxqӦMmKJJ-O?WβuGnfH7_}\}۷oW(7U 9F6Q+*[6_g >񤧧nh?rV7q1Hl|BӹFu-!I7*PY@qRDhS\\5\QV) ڌ:[u &ζM%oY #'.רn -U 9F6&;;[Rm f٪`T5 3 jGHxMFtcqO'Up1H6%%%m۶}լvE%5R N6mڬ*@^&ߴ *7HM=MQ!*@e!Is&m7ߜҍJFM}\# ۪K^FtcHpO'Up1HBIII)))o\Q_76g55gggoF%tAFdDZ T>}BT=h"F :u-111%%ELKٵtŋ6Wn$Jjxnᆼ<㲟kTL7ve2|8VçB*M$D:$!RclT}IMMU_`n8bT%| @7oz 7iK^oӍ*(Zo1cd+-ҍ 9F6"gggϘ1aԩSJ\?YXXhuԥKD) %TE;n,3n$ !!Nc4h#*}VXl2Srss瞍7"HP(*3*F^t#FQ @qRDQirrrHn!j̜9o1~Ҏi\6R(JJ0{gהn$ :!!Nc4h#*ͽ[ZZ}7DΚ5xرc<GGsBmܐ\>^QV췧v^B҉ ؼ( hU?2RYEM|hJ¬zsyg kwلuOoOcb>n8ҍD 5!Is&mD8t  ͚5ԩSc=HSL~D+WtXvsGw<_vZu0ϧ6ї`@ W=8ƮA ?s{s?!,C**Ϋ^znkŲóBr6u֊EUk &ܬk.vYvt#{gt5kB*M$ڈsmݺ"Ynn[oe<փn:ҎA}>u䶇W/~[j%PUd(k`+iO6nz۵,mܡ] &ܬk.vYvG8@^\9x5kB*M$ڈC1@;qٳz0xJ;"ڡC\>2wkF w&&&fοɛZn0=ɘ1Cׯ_{!'Nn3 @[\'+DZRn]~+22Zt^۷:uغm&,2..V61x凊rUc%94NJj2kdCv+ِa0*>U|x[v wv8"yꞜ3ڙ؟=s۴i_sZ=˱ IkM4Կ1u p`@^ݦ y[>oUEOHMMdWo߳}{N85S~^erlhH9v|8q9y >޼ ( p9ڕIoׯ3@rM8@qRDQ9-ڸiӦ:uk#A~~~56j,0ó]n]r8~hcQQOiGsXvsuZJ+/_ذa=ߙTЈ)F ֭Vo6[J꾃]V=ɥ c0-\W5>|,=zHjju f/&,ƊEŇ7&T?w7`+زС݊~6dYd/?~bkQq^i'AD޽k vPLϞ+?5oNk޳ݐc]1k Xn]6~hMf\[yure7?VY9T俒_HhiӦNS-}6N_,wQZ/&A(MW9}k^b87W,@t{g8DiBT=h"FTӧ1:!Ly&;<=-&&Fm5- ]@/g_gՁrƍJ˖z?7A#ҹ|,9r>~bҳ~i}g?*t%_>Z> dWQ Mꋹ7Uoa[~XfhjMg(}!&L<^Ks_RZ`nf.ڊnHCEoٗ^HD%s3v5oK+= 2(]jVÁawٽM[ˆmmi̼ kO?3Ի%>L O*{SNdH{ubRk>M5Z|\Ұo~C-Rm"q:7W,@t{g8DiBT=h"FTJ6j$*plT/ójӦMJ˖zǏ7Ap)P.n66] q%}ͫ5Z6xVD t,n@t{g8DiBT=h"FTrD7pڵkׯ_o>z߳gO>}bcc۴iOg4ڢ~5j;̜9SdZ5.**=z #>>~ԨQVgϖf1113f:WEï~͛7K\U;swɏ>pذa:u\wun6l(]^ZQ}gRSSk,U'KNNW_;R{{k̇n٫W/hU?pqh-0pN DzRڵk9# ~>dNٵ,*Ϋe߸\߉ݺv۲ ދ/=RlúR?u4w6dX]M^^]3Y I߭ݘ:Է1ZMkE.U7ZUؤIC[QGf۷o;;{ӿ;:E |/6M2Zm]Pݵoފ~Zd͹| Ǖh9 B*M$ڈQhc~ R\\gϞN:M2-Pӵk߹瞫mwm֬_̟?Zz=]~j-mWHs9g۶mn?裸8R_r׮]ի-jŒ%K{a!Ni=y{@riѪ 7A`?n6vQ0aXwײ}t꣏N6o~=zt[UW޺m'@v-eԻ妛7lXOo۶cuoyz-y63fNhԨߵkO=w~&3cڵbbjva@jlȲ*8l~ ~w1۷}rT{հuY˱lnrvkܴݻl8Fkjb3RSkՊ9rЉ۴/]ƍ#+W=#kd[v|ub8i_M8VA=IFF+} r+ڴq%s5m+фs'Up1H#͛Wͮ_VZ$o7amԧߴ6tAA|(Qvu6K,KG8,̙Ӳe˸j֬iXbn-IAs*^]D[]{/@)mEsc˷[i 3MP >[{w-ubx ۷j GL5-+ح6!MSbτc2>_}tM݅BbTr| Ǖh9 B*M$ڈlp?N В!cѷѦ\ -}WӦMkڴޱ}IUVGOUnܸQ[s-4n^RvȮ>wm|WZh@)mErX74Fkz&(kngyE<ӳ:1?r-[ԩSdΝ)))EU/vkM;ȑ#lF{]wk3X} 2R(X6Vbu\ g ;5h9 B*M$ڈHNIII߾}-3i$/1bٷo߯ksR~yBqg}oĎ;^}#G>cƱgJ>nG:>|ܳgTj 4G}w'X4jhҥTU|k׮&Lwn7$wJmO+-_oPPk<1EjTLj~ȳhAbRR҇~}ѝ;wϟ߯_??kM}Okv OYF % ƪ\\*;;[./YR/KY~M8@qRDQ9S\~nժU{߿\\\z?|ת]zӶmٳg3k.7tSFjժպuGyD*? /P'#ԩok;SSS|Aj׮]N¡Cl||ȑ#ݾڍ4^Æ -[yEvQG|TEmH~EۼVMߕ00h-_oP?Wc-mP5!y&64yVe'~V6loqѢEv_#F:Gz^}9[CP(J)nX5rȔnT;?R3n8cSPg X@d!!Is&mD J^^ކ db\pmfl 0eԱc(nչgnթis:[qӦMjRyAy-u֍oe~E}e?ݲ7SPIMU+S;̋"."Cxݬk.~z{h0ail.>fڊeq9r\ DԜ^fTmGb-TC B*M$ڈ0zf͚ԭ[/..6@HYƹԉ'~.Ois7 /su~YvYI&]wرc>}Ek_Q?ݲ7%OAIG-UYFvig}qVTWzܼi첄pB1{JU(t$$$UMLLK/Ty%%%ZQZj^)DR8BT=h"FT;w4&bHiӦ7xxܢǾ[*G\FK:>dCh;?]/n56nz\oe bA5.*PqüyRRRݻwVVO?5y*ݨ~izcS(p 8{D_|O?mL?yXXQWhc.>k_ *.΋)jJ7sz$'7IIMf6%,xVqqPQ\\it]jb+22Zt~'o.{}ameN2n[Vd̘ו=mAO>ɺʓsna+KˑE;RhͫgkEo׮]A]껝ͱ[7x 1zk IZ}i~ڝ~Vh7Oyٖed3RSmz^ܜVRRҬY3kLIIiӦ w]֯_xAʴH)DR8BT=h"FT7ec(XG}ᇍz0xJ;46V[vFCˮ>w|GIMMEuQ\}uA.>zw_~֢⼮];n#),5jpn"tn]Iu6qTo~Nw^0GH|-[g˷s Vcͫ~ ^[7Uo-z5V6t(W64~XÆ(UܜH7rHmٳq㌋h߾}VAʴ@!!Is&mDYx덹 2-ZhϞ=ƣ<<Qc]FZ_F/)-073־SS&,W i/?h8f=Ljcw~Fկz ֓CEoKnmZ6a(k dg= ޲++z9 d|\Ұ8Y_o>BlR?[cլ~62gԤ&s_Uv۲W% FTe͛7OHHҥLO$%%kNLKDR8BT=h"FTI&s1@ڳgc=f<ăS.2={fvi^47ՋS'NɊ?'Ah2E+e">*25+qǛWH[U{6aE3xˮ̯z2:Ԙڴ68զoeKwau#bͫ4i;o*eW:!Jd./ظ@iӦ~B" )!Nc4h#*ӆ r!9rdҤIN2A)t'ژq>eFԎSܠA]CKu_zRۍ&VcBs og}QK-/JmkYP& m(U67o^BBBVVq 6L6ʴq1D R8BT=h"FTE{ƀ !9r=ryyJ;"Dz6~Um~;>w껝e?~t֤ƍZ|?jsi+ZVVk?_ӽ{'Ճ]<;`@#|}tt.M6y}OV|xCffG^`>kEnݵWԱcwߤ rX۵{F1Fbs@8-e} 1^Gl///o ֭[BBizYj\য়֯_n(eڸ")!Nc4h#*_VVˍ ݻwҤI5~aĉ`2~2}#Cr/ffgk23;֮]+&fN K1sBFbvU.rEJ)jѢill,ZO0[rM6'ch۶c%~wmm3UV{-_al7x=T;p`فוx}uzsJ͘ #܊e@0qڽ߮~:L~#2oᚲwm˲^uɵjŎ9mQ"?c%''Ν[PPPTTvZ+RIj?Ծ}k\'Up1Haaٲe?a ,=ztѢEfrjҎHt<mP(Pb-DQuM6W^W6.ӑFZJ{2P= ׸")!Nc4h#EIIɬYy?~€6mzǧOw^A"YYYk֬1nWcى6R(Y6F_!ڈ*lرcƌx2i#-=FWXX  2 9F6"|wof!XrQOsuRe@%ڰac=CD) z>Wj~iժU˟k۶ge-i)=h5 r Fсߌ 8{D@*iOi5k֣pG>*C=NBPN!ڈwWkh5j.}cYWz*Ŝk$ j'Up1Hr\Q>ah#BBQofWXa_矟umT)vFt#HǗBT=h"F qEh89 rv FDn޼Y۷1siiiƍ˺҃6 \B@DKr!Nc4h#帢4|) rV FDݻk0I׮]*@Q 6XRR>—BT=h"F qEh8Tiii6ۺukcѤ]vZ{YWzf@U 8רn˻[=qT$ 9F6Q+*@*0ܵ}$ϵi&33Sk]jA?ƒ/8))iӦT$ 9F6Q+*@*8p͛믿f+n˺҃6 \FŜnTilӦͥ^ڮ]իW/ 8{D@ q O>oUA^^ޕW^{:wlL5lqUWM2EX֕5 ZM8qǎƸb}S!8{D@㊪y &P|oTVVv}uҥGZqȐ!]tє)SdR֒uD8pW +=ݻ;lժj׮]zz1nт_=h"F qEUs۷o?~k+Iz0vaB PD uIIIr,ӿկ.\XXX8qă;p]w%deeѣ}*;kXBs&mg*טزeo1//O[z˝nTFzoP.]I7_` Ep1Hx}γG115ԉ!מ2q_Wyr֢6.뭸-Y(GoPD榥umbb[oe\ $s8u֙1s·v{C9@lF9bv; F[q[4PG*nkXBs&m)׵[6.9VeUjվ>PRq;z-!=ɘ1Cׯ_[Oܦ?hܸAZ?޼Bֽo115eZ5iP3e+K秥:wOT;q~ NQ?iEm42CwmٍrAٳg0/ą"M$O>hРnݺui0@0Gl͍7k/7lӦ-2rR>sϭanbbji(Wr/[oѮ/yeA? רo`-=ڬ?xh}5λn[b6mwHJ׮~ffwK||m:T'kcPh#"c4h#XNNU͝;hڵWF-6`mʗ<]v|grlOlU9żVd \({D͛wfiժ՛oumhェLKk_{=}\Ӿ}'LU8><INnS3)ɬٓKJ dZ֪+e'c _lt!'NnQhՆ==#55Y,1_0C4=Qƾ/>4--. pWedw~'o.{}\z)cҬ`*iM}e|ySΝ)ݪ6ǎoG4n@&x l䏾=;>ɣ&M_h#"c4h#wWk&Na41tP}cYWzر6b=.G޽|[v`}ĻkR- ^}uA.cFoz?ՏVuu56r;tH?~VZyf'+.֯+?G ~o>Ԏѣ&_w@mH'5jTz/{}C>1b7%QwI=t-rLڽva|6{RV=05X6 p9F6[>WjvBZ2d,ZZamԗ־SS&VVoKl]O6p?7hPW7.;m6//ӤICmiN笁Lεua~>Hmi Zj_>sYUwmT֊䏾=gܳ͞g+am,))IKKҍ/R#MPmD 4Ǝ;f̘ǏHi)Bdmܳjժ?d]rI38̎^ I}P{vI5ܩvY7n ]}Y=Yʀ}FM9<-lgQ'J3m邧}РA=zhٲizYj\ 0 ''71ׇIYdnBBoO0/ qV 4k,)))!!Aʴ #9F6@pmM4|yp(7HHh,,7p.am,))i֬Y2-5Fˆ6{D1\F)-amG֢2m\ h#M$h#%ZKEsss[h eڸUFp1H"FJp6;&$$_TyD=h"FD)Z0ڸ`k\*h#M$c(,HHHlڴi^N6{D ?޳kʕƪWTT4i$/F0Fs&m᫲rt#@#9F60Us FpFs&mҍah#M$N5*Fp1Hrҍah#M$޽IU/ ki@t H6*|%]/JݠUEQnƕX1o,[6BAtqT:r*eʉMsgu=9}zx 5ƨF^b(mN׾֯_=k[ >|… Oͼw}[na㥗^:c?>}L6-> FL6&JLhkGe-.f͚`O~_+ni#@zi(tMhcԨQs!2mڴa~~^8`~ :4r-vX4>v#/裏4n{ma; ƍ͌׿># GR7d 4QX+&Q`aءC{,l?裝:uz/c=6|i=~CK^K,yE ##G6Y]]}ꩧfJEEŋ/i %FĮwy'Bڸ>444zsф}˗_€-x8d~ /i>hsi9mկ~uq߿_~_Wb3׭[ףG~:z˵c@)X&J[n]_m,淗.]{ZCjhhxWSN9K.)QK,i6.XK.фyEuuu:u5kV~ךFᨮ]FۉVX1jԨ #,KKL@鵲n12dȨQ_v\zr0'r!v^ѣѣXfMΝΝohpٲe{|暦ic]]]tDk Di# Ӥ/wկ~u[omwoa裏~ꩧyE ^z;l]w]%7ؽ{*«]pA!2ò 4QdEkƬ)'w]cDCde @ziȐwuڨkaY^b(m]GXHCde @ziuc %%F yݨklJCde @ziȨv[7aY^b(m֍h,H/1M6֮F]c 4@FX&J[nݘ1ch[\.>r-h,H/1M6uD,H/1M6uD,H/1M6uD,H/1M6uD,H/1M6c}]w}{ߋ|ga{Ç?#?o|˗/?o߾GqDaY^b(m("vzڵaÆ=v뮻.?3OmWWW{׺uih,H/1M6UW]ӟ{=/E߼y'#k֬G &+Wx {̙3Uh{%%F"ޱꪫƏMDڞe @zi+V 0_~Z8NaY^bؗ0IDAT(m\]]]^ڞDi#窫kjjO X7aY^b(m\}}}UUs==߱#a<:RaY^b(m#FTTTDuct:l0JaY^b(mڊ=z>g̘1\.C31(CK.=ztxˋaY^b(mrӟ_bP֮];zǿMh,H/1M6@٘:u3<Oà-]___&4@FX&J<[nGa[x'kkk_mi,H/1M6@y?~eEƌoKCde @ziƍxx`ժU~ %%F(9k֬x W h,H/1M6@뮻!vZ&Ν{GK'̔W_- WDi#+mV/:[>MEN:tmcs,\.ľN-WDi#Ҧ4\?׎uװvKݕe @zi/>vz|{Gi .<3:wܷo{ipVEk4zh#砃ٳ?6lWTTh|嗇o.O>$8qb֩Sݻq5wEᄏ0&^m.,q7o޼̢o W8sh?i/,]vxݺu0Y]]/E&MԫW!C4?]#Ny(i#P,KKLPv,mYTS5}ObFb/m7EP?]яqOzF,X&J @)S_mlZnW r,jcǎիWlcOD/??:tg 3f̈/^N2}􆆆MK܊+c_iKBwStE?$u֓6e 4Qei'>aÆ-Zc=zt4yС,^_z+Np]vҥK{7mڴ>䓫VjhhO~һwhr]|[4hPBk֬ҥ/~po}[`8C555apMƢcso?i^ї _|ڵk_z0p03jL _K4w=yME?]я8]'o=i#P,KKLPZ6v-Ztvq͚5OԩSUUĉ;wɊK6]uUtоۧO;3 'pBp%o~M.T-]g;Bw~׫W?3{}뭷6Mypv^so?i/,].ڵkK.$Hn{}'i}OW#l[IDi#Ĵq׿>Cehz_}uuu|}H餍@Y,H/1M6@*N1gΜ^{-l;Gu>dO~ %%F(wuWKa̙|pN..>d+#YӅy_@Cde @zi>;gΜx <~ %%F(}FȚ˗/ h,H/1M6@y[׬Y`wq-ĿaY^b(mp`[}'_mi,H/1M6@ٸ뮻xoܴiS- %%F(7n>x~_&4@FX&JՍ=Zn~ǿh,H/1M6@>x#f?_fh,H/1M6@ٸq |5kc1(uuuǏg_i,H/1M6@Zhmvw6lc1[g͙3g„ 'N[!2² 4Q>{g'r;Ì3?ƿǭ!2² 4QP;Y2² 4QP;Y2² 4QP;Y2² 4QP;Y2² 4QP;Y2² 4QP;Y2² 4QP;Y2² 4Q𹺺§;ֱ= %%FUWWޱao)%!2² 4QiӦEOwH{N4@FX&JbĈQݱa$ǧ4@FX&JGӦMr1l0JaY^b(mjQݘ墮1'Q  #,KKL[MKjڴi|CFxػu*@6&Jd7|kM6駟%5bĈnFx|*@[6&Jf͚:ujcwtw[. (ڊ=zL6-˅ǰFx|*@[6&J7xcCCCm a$> IKL@KnxnO?8qb{PR#Fr1lw-i#@zihɸqN5w/ot->33lYKkkk c؎h[FDi#Вƹs瞱J[Y`AEEEo3'NuKwСs]t aÆ͙3'+w}O>0Ed-m c|@6&JKx^ve]֮]x NKGzknjkkcV^}s1qɹ\./o.O>䋗mǏv37^̻k.¦~ߵr>c|@6&JI;tonѢE{Q9sΰaâO<16>쳋/^fÇa .Xbʕ+>뮻.pa0 ~ׯ[.

䓦4 -G?QѣGWTT92>餓FZ~/EgM>G]]?gY8 aY^b(mZ2m|)STVV˗wwɏ7Rp@kw}7z /|iN9ѫ/Y$lϚ5K.acҥ_җ+VD66 -ƛxk.jjjvkD]v&^{WwhW8aAfˤ-,H/1M6-i9m|7c‘N:5n:vv͋{キOޛo]Ѯkn+_x/j-%婧/8/ZhРAQ{555V GE . a.첥KFMh޽ -d8CCO~QEOcW֚K-zUK K,ۻv]Wkq͒6 4Q$1m\de]vA={5jԺu=zt/_zut… 8N:UUU}V?Aݻޚ꫻vڭ[ٳg]uUk; &L-o '.&D666n{%iZby-BsT;wo⢋.zWcrJ"-,H/1M6-IL4 E<'x">J FX&Jd?m[KZ`Y^b(mZr۷iO?6 4QG?~<cU[[;cƌ 4@FX&J\o&Lq 4@FX&J7xcCCCcw| &Ŀ٠!2² 4Q$={ԩSǯ[.  %%F 7߼txeڴiO?t %%F YCCȑ#׮]]K>` %%FU/^WWWW4v:!2² 4QꚚ;aW__߿|ݘcFxػu*!2² 4Q^z顇ՍFx|*mNCde @zi/jjjwޯ_c#jJECde @zi`TUU?˅ǰF( %%F&Nؽ{}k\.<0D)h,H/1M6lU__߳gvO4@FX&JqEưMh,H/1M6l&) %%FrAh,H/1M6M81˅JGCde @zis7o3gθq?N:)˅ǰFx?!2² 4Qٳg-&Mo~/rx a$ i,H/1M6ڦMƎ{i͜93@愙a~|mBCde @zih6mtW^q6lk" 3|ucIh,H/1M6رc֧af`aY^b(mکٳgviׯ/;wu]771l a~8*[8HaY^b(mڣ͛7{3g̏lڴi̘1 =H/epT86!?BaY^b(mڣ9s|,3fgѷom0Ndž3i,H/1M6Ѹqӹs񱨱P[dž34@FX&JΛ7o^^{QG{ao g? h,H/1M6ѠAO9xD g? h,H/1M6Q~?7^26cOi #,KKL@{Ɩoԑ0'?߯6= %%F=:͛|'^26cOi #,KKL@{4nܸ/W_nj0'??ΐJaY^b(mڣ9s|,w{aœ-x[89P8®!2² 4QG7o>sgΜٴiÛ֍a$pl8C~6!2² 4QSg>֯__8O2$SǰF 'pl m@Cde @zihƎ{W"c0?!2² 4Q_6m+ 65愙a~SHv" %%F]۴iرcO;3g{Ü0SX* #,KKL={bҤIoc#Ѯ0'~mHCde @zis7o3gθq?WVVǰFx?!2² 4Q@aY^b(m h,H/1M6P4@FX&J( #,KKL %%FʀDi#e@Cde @zi2!2² 4Q@aY^b(m h,H/1M6P4@FX&J( #,KKL %%FʀDi#e@Cde @zi2!2² 4Q@aY^b(m h,H/1M6P4@FX&J( #,KKL %%FʀDi#e@Cde @zi2!2² 4Q@aY^b(m h,H/1M6@{#`Y^b(m(c ܠH/1M61rhcn&Jʘ917hKLe̿ژ4%F2_@jll=ztx67hKLe̿2u 7ܠnʋ4%F2_@9ƺ|ru#P^ܠH/1M61rNaQ7 Di#@/g4Ս@q 4QP(#uu#P.ܠH/1M61rEc]cD7hKLe̿hFc+֍•3w2n&Jʘ9RSSӿ#F̙3'o[#W$`Wr 4QP(W^y8'O\GkP7W u)ѣW^?x|. @zi3PZQاO!C؏86PiZ7F?XUUu <{<@~/@q 4QP(n <~c~qҥ;5F1'A]|Gydx-]#PBn&JX }x~_O {6l4Q0`Yg׫WK.˗sV ?\h 7ЧOݻ_UU^7~)4QƓN:)9x`qڵך1cFc=ԍ+1M6#jjj*++wL|ucӮ1oݺuя8r޽{Qb(m`s)Ƣnlk,4cƌ3J@:{QUU pI'UVV'ЎIhS'O۷o^rYg$1i#mC}{VO6֮={Fi) @&mVUUEiSO= @&m?\.WYY@'mw*ao:~(7H@ G lDD@@+Si&лT#{qW >ގh#d8zvh4Qzt h#ߡ!V(F$IժjN'ˈnd{h#Jl|ɘn(3F jGc2c+4D(Jۭjooob???_[[T*db>b7[L:鬒%@!ɿz{{;ݸ!=Pnܤ~_Vaƌt>v@y6P^W׳pnv8vlmpoo餕$I'''Jess=؍ !@QnV{{{wvv>?811zww؊lm@Vl&Io7( F $Q 11QHD(V$VVu:|/#1r%'wv?~~~~yyb<=*ɯP>|pvX]]T*D=JI!mFh#0BD"F`6#D!-F4"IENDB`onedrive-2.5.5/docs/puml/applyPotentiallyNewLocalItem.puml000066400000000000000000000051431476564400300237740ustar00rootroot00000000000000@startuml start partition "applyPotentiallyNewLocalItem" { :Check if path exists; if (Path exists?) then (yes) :Log "Path on local disk already exists"; if (Is symbolic link?) then (yes) :Log "Path is a symbolic link"; if (Can read symbolic link?) then (no) :Log "Reading symbolic link failed"; :Log "Skipping item - invalid symbolic link"; stop endif endif :Determine if item is in-sync; note right: Execute 'isItemSynced()' function if (Is item in-sync?) then (yes) :Log "Item in-sync"; :Update/Insert item in DB; stop else (no) :Log "Item not in-sync"; :Compare local & remote modification times; if (Local time > Remote time?) then (yes) if (ID in database?) then (yes) :Log "Local file is newer & ID in DB"; :Fetch latest DB record; if (Times equal?) then (yes) :Log "Times match, keeping local file"; else (no) :Log "Local time newer, keeping file"; note right: Online item has an 'older' modified timestamp wise than the local file\nIt is assumed that the local file is the file to keep endif stop else (no) :Log "Local item not in DB"; if (Bypass data protection?) then (yes) :Log "WARNING: Data protection disabled"; else (no) :Safe backup local file; note right: Local data loss prevention endif stop endif else (no) if (Remote time > Local time?) then (yes) :Log "Remote item is newer"; if (Bypass data protection?) then (yes) :Log "WARNING: Data protection disabled"; else (no) :Safe backup local file; note right: Local data loss prevention endif endif if (Times equal?) then (yes) note left: Specific handling if timestamp was\nadjusted by isItemSynced() :Log "Times equal, no action required"; :Update/Insert item in DB; stop endif endif endif else (no) :Handle as potentially new item; switch (Item type) case (File) :Add to download queue; case (Directory) :Log "Creating local directory"; if (Dry run?) then (no) :Create directory & set attributes; :Save item to DB; else :Log "Dry run, faking directory creation"; :Save item to dry-run DB; endif case (Unknown) :Log "Unknown type, no action"; endswitch endif } stop @enduml onedrive-2.5.5/docs/puml/client_side_filtering_processing_order.png000066400000000000000000001366741476564400300257570ustar00rootroot00000000000000PNG  IHDRpT+*tEXtcopyleftGenerated by https://plantuml.comviTXtplantumlxMO0sLUâ%JVJI&aTglRWBHw̝uҸR-Ō,iA|:D^5sR q Tt^=T@`۲ξ^!rX$9 b AO*A(붹v)_0-(D!:<RĻ!g_'av<is.q-x;;*E<y#=wս_{}ɂ֠)Agj BdYbglcrN8>8Ɍ;m$dZMNWף$ZKZhj*@"k3\t,r/w͎*L+"nd4WޟX<>wx')Vgd:x/nRrYҳŽ\sHk?Id7 IDATx^wTT7p"z 1*VjAkEc$=QƛH%,K bl(c\)V$ MADT` iُ;=33=Sٮ9 Ij%VDb0Q`D!B$H & L"1(Db0QMݻ!!!k׮pw?&)$&&oٲ%%%Eh1n޼|!7P8|EZK.-]499Y5DFET? TTTP.--c@ vܹ/R4)DƲb \Ct#GhcH(a0\!!!eee 7˗9@燅kHŐ`PBCC+++7qA$nx111a0tIIIO?[N6l?H^RR"/46;Q*kAH}m@ r "q EuW\8qӨQΞ=óU1cgڴi… BأqG/TjC#C;4Ǧc->EMvFt@$HtG[[[/Z(===55522ĉlBmѶJtt>fJiii7o/'VӃ=x_wTHp“Owtxl%- Aދ: Fė/_nժս{pssI&͞=}͜9s̘1o߾͚-Yo}wL C[nk׮5x L-[L<9 |w}G?~|DD 9mK9vX:EsXp9Ͳw~l7+?E%wCCCcbb8o{/Y}*:e޼yB%'$ׯS㢢"%IV}EEE23Y{d/dsswJOOO/uG2fVǦm "~)л;vR=zJzDb+ }6BM6yyyNWPP=]O+Db0z HP:z(%cDz!,^j(Ws)/j ZAN $Ƀnݚw޽zc-i_koerh߾}Y`` :sV3YR{#RcӶY Xxtnnn[o-G}kQaoاX#xxe-"q33{졬wܹdqRRBPI̙3|_XӸGuvt)*'OfRp3Hm "~ә'L$fsssuO%aiӆyƌ,k[Qc$>}̙oKo "1=D↧;gddİoRa\]]i+?2 _WV4Fb*rlmmPgWfe!I.͛RzIYYcҶGݻ:uJ}m۶eRp3z$*X[[GbI=q֧ٓ0\\\}t>)HmE/yTٵkWiwW"q)GYXX׏.ٵkem۶;VPۗ {JBtٲeڵNNN۷EI3%1jFciiZj Q}:tcǎ?iwGRp3Mfk7w4|ɓ'YrtxdGzrJR[Jطo?Fm(NHmE8qFߝn`H\zUC9ʙ7okVZZO?edd 4JII6j|_"\l?T75iQQ"j(EjM,mذApD↷n:1[ ]p$Z Db0z ؘlܸqŊw... C$nx111Ą`PN>-~ "q+..V@a ?Q,_\ "??SH(؏2c `tˊ+rssŨ?~ȑ#!7?PXΝ/O3B$nD999q E90^č˗/tҥKO0QC$n [lCr˗0DGG'&&HL"1RS1TCA$ꫯ:88T"1mmm̙#6"1(KKKJ鯓\tIl Db0$vuuHܣGB)6"1zq/a~wǃxyyGDDUzϺ<==y򤯯/1cJeUUՏ6)l 8YŅ?^/ڸq#E/ is׊*aBVuOjjA(.ZH\y$~O.D8::/00-MRwPYUUWRR^JzaZZZZAA{bmff&mMh^ZZ*T-9VyMFFmv!v$_TVV ڶ&?7oJllffFHRNZ1o&3itxJ;1 yx޽ݻw>|%U |ugf111NNN/u+ԩSxpӸ-[ЦJ{pB~~|||^|Eooojjժz5}vNmM]v9;;iKP$)=5;vKvBftt4~.]Hjmm6x ++RmP]lWt [nQ9**%66v=Ԙ+Wڶ]^pMlNXh**ЂrŋŴWIB%L#۴iCe 666=վ}Wj[mBiS/Q*߾{KK)SDDD_{rJ:~`dZ5t.\( :``@3B$$=R(>>{=jFy+wj[w#FEhV"q޽{NJ]>s̠A9BԷȐ!C_.V<\ЌA+T|- Wgހ̌.][ܹ#\wɒ%?)6VS߾ӹaZҒGYWWW77۷-oMI-ꐇ"1"ՔYYY :)'8p5b ///ո-[X[[<… {챷-6mj׮hhܚJ/^u j ՔJ7 Q\LOOΖW깮>ap{u᧕gϞ-ѦV<\ЌfRM1&U~nrrr~mB&.zfH z<Q]DDHoEEEGa7n@ %5y*nᐇ&4#Db )ᆂ Ș#qQQ0$bØ#qtttbb")yS=-/V$N -1GbR'|M5TOKj D?$-_y>000HL͛gkk&lMe'M5k1y$tB Lny_KKP)4 .0d׊իЀ4&#ƆR1ͼ]]]njceeUYY)&Dc>}h`ήo߾b#Ff8""ӳJ`` ]nժU4Hmllm&6hdJիWFдh`ZYYQٽ{w}zOhpBqA FNx$IW? L+.h4=oLU]"R MJJzAEEdĪN)X:ti'BӠއ~XPP ~"S¥KJ5⪪sEGGkEll)u$>zڵkxlT .$FJbRsfO*^xѣCBB?~,.Siݺ/"]$8v8GN`b( UVV-***++[˴V^^kd2yѝyzz&''XRRB#1-͕W>ZZD;v;wNZBSO] JqZ2dǏuVRٱcG֌6V>rHϞ=G٣G%#Xft bɰ(..fey$&۷{?,)u5335(--卥ZለfΝJRԓ(//?qĎ;]=v״Y\z*ɓrƌB$èz&B@ Ts$sWh999b?iXf {|ƍ)Q~;w.V||=9OOOV)2ڗPC*TLw/ʕ+ݻw:ڥ:;;4EO GG 8kB$>p~"Z#ݻo޼)PwPF1eE*//iii<#qii)-++jh Rݻwoݺ%UOOO///g/i/mvAql_|HLiuvೳٿ`it-ԯ3oB=CvA/9X5Gk׊3Ghn!!!b?A#0_f,S|:uj@@Sy.ӦMڸq#ߔ\XX-63gx˖-ݻw:tӅ > toy߾}iIy!&&V|饗<(i9xf׮]uR1#~*NFh2==]U9[Mqq6=/ȯ]V' jFy.66v/RL M9sٲeFT}vN?ƴΝ;gggK"1UZXXQQQ...1-W})_xHL̤7nPަ4<9~ݵkx6t) `P _reĉNNNF:{,ҥsmllص[l;++3g.67NkPd0)ןR\pa~~xiii9eʔ~=B͛y3!m?Jcǎeej"qttK/*)ҿ=վ}Wj0t߷^hQzzzjjjdd'"69yKBgSFFmS"v;vpFr׊>޽{NjggǾLw#F1X[$9r*iEb6Ĕ)~%s]I!C\~A>0tė/_ٰƧH\\\tI*S!-- K,;k֬W_}ºexxIfϞM^nٲeϟi4u~wh`*_?[bE`` I~w?6CYf̈́ h)))KHHxGM wŗرCҾ}^^{W^ye֭k׮ɼFrזTˆ dI}>ɔ-Y"_|AΝ;8McA`_Uضm;t 322h^;wHZ^Rm?''5B>0tm۶x7ȿ'=4C7oۡii>}( (={ו qǎCCC)a޸qݻwlOi n߾M67lpa[[۝;wV۴$ JZ_~{`?rHEhgZ($wԉgϞY> 0Z~,kԩS===}.y aR^:R1 F{{{e4X%4///ZQZÆ cuY[[S3fEvttw;888;;Ēk྾0`%Q<\g}`ə3ghF3ϴkn̙Z@R]hnjtuiSWRe_dc-X7*r׍TL׈朜vYBaHj?T[".G=xCzQgggAʁ?y䑘'oCfHLuִ޽{Ϗ &&&+sΟ@˵mۖv_ ftlYlٔ)SKIuͽ׭[Ǟm۶ÇK+/f-5n.o[2K,u0)3VZ,~8:[[zC`ٳg?VBddpS<;vU&$$6H|*WkTӼ(7)7v*NNNKYx IuҒ3go_rruueא,,,(oSTNMMem4n믿J/VH,Iy}4H*}:W[CWHCjIuihƌ>>>lˁ/_fޑG &L8Q+%I{TD_awHr"iVZ_jwf̮]>|";9sl-Iw/_KZOO#eem0)IyA`o$NOOwww777wvvׯߕ+W"';;;#HL1W^ RJ<ڵʊB){2<0tLAAիWRbnn.jqRRR4c255U[F-))/<o@1%KT_G⢢"KaR.,=N*F>025Gk׊3ʆ p,Ξ=w^~JKKׯ_gtBV uO$:::11OʩP+ LE5kܾ}[5p%%%}Ν;CHHO3I9P=-)OˑL͑x޽)))b&C%2e=I9mmmy)4ӧ>;;3gMZ#qaamX͇ݻb?SNYZZӇ4)={K.MA}w4صb6zM5IIIbSHLN`uuC&ɩ...4)ի5@#qGR1>KUl`/^~ uw}'IZx1M}}}oK40KKK777 .60zEb|\1AӢ.TDSvvvx@ӠfmmG F(}#1%KPS4:'ž1aÇg3rƳf@yWx$2e@ɝ;w(ZqN'@E3r'''4ӧOIhh$^Z5 w8]Kx[#XO7n?oK.{)l*8]{jJUUUΝb- DbЅ&v؁ CBB=*Y0NTxѣG0|LuֿX &***fVV8Z:qĎ; &°YeeN/_YeeeT?xKycL>3S1UOOd~zKJJ m䑘 CH Ѭ=((XB?>22R<`(jjȐ!?r``֭[JeǎY3J\Xȑ#={9rd=ږ<,#/X߿?U NїaV:t"} "1hPYYISLᆵk׮+WH8&&5F{u޽999666'%a5:R1k׮2wzӦMYEGN4հKDb`Æ ogPoAAAcѣG1S0]Tnݺ0R<ō7uԨQEb bڴiQQQGk 0 0cq !))&Db^OR+?|Exoeh_EBIc*8551++ Çg wG:c}FDk׮ʕ+3F¢员o>// `k׮DXe$iΝJRmL|wh?@F=H|I___V9c !ӱUUU=QhB`LG}g4Nddd~~xxYŅ=^Khƍ(͝;?^+>>ޞj K$r?#׽{vhRSS D1uѢEb!HL 8kB$>p~"}F9~? 1ܼys޽yST*)1 UUUyyy%%%쥤wHKKcc!QVV&EEE:oyy9_$l֭[&==oGb}ff&GV#i9ZIv4v$U6633ۨSV̰o c Gy`| 6+ՏV ru@aFPM4yO[9N0`jLŹWҒr#K4(F"s_|AΝ;8AV}v޲"Th5@TTT߀@V"1TSc$.((h۶-%oa&>>>cY#ii>}( мyϞ=ugРAs̉+,,x4O???Jڻw޼x| ^عs'9bʽNZxqx$9r$m"Eff&mcǎ7nh;Qi;Seem+Нi@gݝ>,yˋ(<^kذaVVVGM6kNĒh53>吇5h899sf< ͌gΜyVOՃRl#֭[Yexx8.ˍ=P=_~f|"zj4ܹ3bsrr98"˗۴ie̘1AAAڍt̂ |MVXtbʖɔ,x Ϟ=[^x4`4H  Guvv;v,ϛ7OLyo;t o&iILauִ޽{ﯕUڶmK[f}E,߿_ .˖-2eTH`,Y2uԧ XN_T?TԔ~m)K - M: C5GbfϞ=gT)-ǎc vvv 13g=899QJgSNъ|ɓY$>}%3g-ZEbo~H Xm$_Q]DD_#GܺuKo uh1gdd(JIueiƌ>>>l.˗ٓ{x„ *'N?јBy޽{iӦZ޽۩SxIm۶eIH233vzaI z_|StLcccYYDb}` h  @DbHnnnlaaѯ_+WE?}`y$ܫW/GGAegg˶4Pyŋ...NNNSKKp.Cˋ)Tj#+++%%ESHb9C~RYE-SSSk̮5&((hɒ%՛hHQQQ0`yX;&!C56l' _CnTgϞݻwXT.]ZXX(֪YnxɓDR>`r*F @P Esφf͚۷o&iy'OJ3K5TOKj 2/_w-!k?DbMp-הĈTf͚egglR_Na4 6Db244TqpË*.]իi+ZTs)tY;}ᣉF(6DbWU< ӷo_;;޽{Ӏټ+F={pqqD{=:v'P_qA v_U<u%IիY5h㡣!CꖕE7^r*!!Aj>|ҥKaaaP_?\}⧭Nj4ݽ{wҥRMܹsѕ2?Sh&`г|BZDDŹ-4u=zT 0: KTxѣG<~X\Һu_~Eh&`гQ6Y۷)-4;v]F°YeeN/_teeeOT`-eZ+//5Fjyѝzzz&''7^RRRXX(o#Ĵ(77W %H =}ѽ{,4ܹS0 [j5d__ǏS900{֭Jcǎ:|ȑ={9G mKFjyё,Xs֬YEaÆcV:t"}@B$TVVҬy޽ʕ+b033W2)İHL{{999666WA%ÌTLyڵk޴ieVfѣ&Mb5쒲>g !C nݺ%N`9,[L~,!?z5Fb С Rֆ *3R<ō7uԨQEb bڴiQQQO8K zV_~8ufrҥÇFAļe9V~!"dyјy$NMMuttʢrBBY]b:W pppxHg><_EYjI`,,,YYIp v횙R-pDDPsNR)T)22m<==y򤯯/1ciwUUUOT?ڤP(u%ƃH ==z(,,LB/v5k899kmܸ2skS,`R-0} 5Y BXAFRSS DIuѢERNH<ӿH>}8߯_@HYhT`гǿ{q ͛QQQbWS**JJJKIk۶m'S*吇q!Cr5F₂mRoWy>>>cpԠO>cbb Ş={ kLcǎKoܸA(@޽zߟ}6mPLݰaÇmmmw)l >%%SN4G?{,Mi_ʥ1Ņv}~4կ_?:ڽ{ZXXE5"B9$$A$tbh4i(JYWXE+ 6lښ͘1CHL~6r5B  z13g }X>,6[:{>5"`7|S^tӑ)8hlNNzCaa0y0.Z Db0xYNHPL=z3V!~޼y6B$oC1Slݺ5w޽zT[KLLdm^y2?s|]mNYYTN>ۛ&{LmۆNS.^g/#1_+88]HRݿJ;*P#XҤ4J]guq!CrGbfϞ=$Uދ"cX9!!Ύ7c4F3gjTUEaIauܸq{ɉСCAAA*[XXPlE7`ePf~5AOqqq򗹹oT'z|=! @ H =322bbbJ4c i|5{<'LP2qDîޘb*U޻w֖Z T/M6mժULoD:|Y̙ӽ{wvg/꧈?66TT4 B$jr$ˁ?`!S իAe[_]vm6zyE'''ڗexx8ZaU.>>ˋ;tWŵkX?ЪU+賗Fb)yfNԟ-@$T<\-"1<,'}E1AJIIHl&mmm̙#603f̰P"1<,WYYYm߾X*#EŅf40鯓\tIl `2h (ݻ7T ЌgBCCoA+//N2jvvv>>>40{P(<<Gg #%:p3/^x4?~,.{uֿX `Jg,Y)ΚMfcEa̬zzX(++[˴V^^k0Sc*7陜@IIIaa<Ӣ\1s "1޽{|TDFFg ҥK[j5d__ǏS900{֭Jcǎ%: ~|ȑ={9G mKF2աC; #`ftbƍ:j(Eb bڴiQQQO8WF z([t/B&III 8!zuXÇ,{xx|+C*aF[*f8551++jΖc C%t><_Z+V 8 bVGbҾ}<*8pE®]fffT<!ܹST :8qbǎt|]UgqSS1'Oe53f"1 Evvs`dgU= 'Š]dd$5k899kmܸskS `R-0} 5Y B ,xW\YTTץ\TggAQR]h)T"1s1p@ooo//ӧ  d4+H =kЭ8v/ΈA7oٳG<`J%%L*//iii<߲2V+1ϳHLJOOg"ŤիSO {PP0]+s`gU\j"G0ay8??>>>/7dSN L+OtڴiVVV7nfϞmnn޷oߙ3gJ$ 111NNN/ڵY^N[*u`гF ݪ׋a9==]u'rTXi2AC).66v/RI!yٲeFT}vN]/i-kkLqj~vvv׮]{}oM`gUD+WL8iԨQgϞ.]$%% -mll.\ T{Y(Hdee gڵy4( KT.\0??_P(2eJDDLmͼ6i۷7tDb 4{}W^e- @oD;/zwz0V`гF ݪ#-Zy IK$Qm䑸 E=Cn 'ӧz޽SNc_;#F-98::W2we- ru FB$5JV}/_LSsH\\\tI*S!-- K,P:k֬W_}º j6vؐsH,ȷ~N2ERx̖-[&Opy֘Tk+[nT'M4{l%%%k֬0aLII}WwرTH t\K%KQgeeuܹ7Ngdd wG߹sſp0V`гF ݪ#qAAA۶mpY$ϼytLKCa8&&FPٳȍ1S/^ܡCi#nnnN>L/) Q޽{7u.KSNK~)P}ǎCCCƍ#G+V`_6Nڵ+ F:R1 {{{'U'NӇˋVTְaìRHL8p|-!7*>cH =kЭ13g VTbdD+RG-ZruJ+CqYyW^]V-fb֬Y-ZpӧOJ˒]F 9zf͒ׯ6gىéijgvaÆ(0aBn|,JB2qb_)d"lE%HJ+oHVL퇢mvjavvv||tR[n XOZ|^Yb:S-[,۷߯w+-kז^f-[8#Ji ,p[PPp222T}82}0ZXqJm۶3ge_}sξrÆ 4P;p %zէO.ρ֭pB_8>sBW6js5gPl,ꫯԃ-Z9J";Q?_^=k>J+- 6LKKs/ JSV1ax$k%b )+qnnnVj֬Rv֭[oܸ窔y|| /;J NNNNLLl߾}_e7o^:uڴiڣGЕWYeRRCLLĉeUM62eJO0aɒ%-[0`7JviԍJpҊÑlE%HJSVbe߾}|︄ gAA4j}Ç?Пeٱc͛yyy!TRRe#Ylijc\%.,,tM m-*Qbט}@5A%HJ0+qY߃feRRR{9}] cƌ1eee8ߺ,ηt܊ *1GV"pR%9rѪhȐ!'OV]y;˝!Kd9k2Ç}Hk>x$k%b ҥKW^w>y3fޗ!7n?A@Y&sM}2>}SƢx$k%b ǎ>7a„֮],ej͛˒w}W@zd{jL%2+MJ 㑬5L<̞={@JJJ!.F5kLVZ5mTlR>2J 㑬5LG5j^ݟcW_}aÆ 25:t _Go≠K.D&&&L8qQa<o-NVPP k3GqqX> k2'3XJ 㑬L^}Օ+W-'9rdРAǎ({vޔ_}j>jx$k%b-L ݻwCfk6nXD{ァn䃵Pa<Qٳfffؼy/<|ps?O+W_}B>X C`-JTbdD?ڵkԩ$/1cdeeRuŋ?[aO? |W~|>SO=_啪g_*/Ǩ0ZXMGʎ;F_c;e/*;97Y{9C?YYYǏוz7KN.AݻW+[̾7x`,*1GV"VӑԀO4ި!疑!6p=z3וQƿ/}i"Շ#&V|5TP Y+y۷oF|)Q0?СChmܕXV8Oٳe@|a唭XO?~"UѣG4SRzx$k%b5]uNPޘ:(&g;dZ1*#<_m۶w|{e+UVR}6w%\~iii]toVӦMwޤIln|a%t+NOO?Ӻvze}wO>:u?~|:u6~e00ZXMWm<~[5+rR0 jTß~z/(mY=VxѢE7|Z?uذaCgvƦ>nŧ~32RgΜ)+qC TgTbdD >{[C|Ǐ_P.J`믿cǎ-ZBUq3f8v4F?VZrYRjЇXVp0ZXMW=ܾ}{FF( eT-[$&&6֭]biݻwBBٳ/.|kը+ZVIi걌 . =0ZXMW=5jT4t8~ieTwy.S .?Ѧصk֫W/??_ms@Ó'OK#90ϧ$77W׮]СCSy睷gy0gUbHD%HJjj~sof 4~xC@E8X./SN;v;J,-11kiݺu>}~*ٸqc٬C\rC_>,Y'K}@IKKs-[RRRڷo/u'o V66.ׯZB%HJjjoa֭[xb?ٳ?;w8KEEEߩ8zzp__˶m۶nZ\J,?Nd{ytTB7;|O?]ڿ<_i7򒝧h[q0r9mpPa<&a5n8UFD^zCvu+ԩӓO>?Iھ}{ݺu }Av2} v֭UVgv朆zoYv+Kԁ)S@oʄJ 㑬t0o`óuV@ qѣG)IJ3/BL2YYY? |͞={2qv)ۨvOjNmVN>LmlriX;|=-bo駟$Z= *1GV"VUé7nիWBBBRRW_rJY(?#m˸_[?񏘘6mdffXQV 'HEH|Gtgu] /zkFFnmĉ%%%Ksrrf;|W: FڵV^@BL4J 㑬t0SVÇ7h`[ld_J,m9/+j㎑#G'RV5 N+vWbgg>}.p۷뭷䫳NW}~]vݴi{?p90&*1GV"VUOY7l oߝ;СCC}w\>L4J 㑬t0SV}s9}E*oܹC==o͛72\nL4J 㑬t0SVb|.]q޵kܹs7Rįz>L ]/_.;e0PV 'HV\XX[o}S)((dF@8&D%HJjj)+mfΜ)op}Ν;N. 4hkJ| 7իW>}Ng+5jl~S=Jj8A*]%(DL4J 㑬t0SVVZլY3%%vڭ[޸qӟy|| /R۷osN^OJ,֬Y$8q܀^ bpea!LTbdD +OkHj)رc͛>C%to Ek`|0ZXMW  s9bĕ.+++''=AduEYԊDCPa<&;vL[F}v}ƍ#Y+HpLoQFpULǎeZ:u$B80ZXMG~3gΈ#2332Z|fϞ/edd\kSB80ZXMGnv:u1cFW \|~G\ȿe˖=[=<Ӓ#Zx~G=SSj :T_Q1rrpTbdD#A7.sܸqk׮okE6:vq~ԨQ;vJ2x}Px18;U+}ݬǏJDwx$k%b5 zIJ]hرc wÇ322Ə! ]s5U87};c̘1}T5KQE0ZXMGi`'M=D۷K),,ԯD7)ç~GѣG*gKcy֞={BZϴ>n2:tX~3EEEwoIJ:T"*1GV"VӑGɓ'ZJjC Eiֵk.モ}ԩ+R\\\NT8iz[oմiݻ7i$;;}JV#׿n۶}'^Y+hժT_K./OKKҥZСrQa<=bnܸqԩLTҊf9ӝRgΜqÆ ؽ{w\\ O}X ъ/eXUE|jСQa<=b۾Co-Q?Ł(UbSVbiz5BZK3+Z/Xرc-jPUb)wqnj3;SC/Pa<=bΟ?ڵJT?Jr)rRoFU ^r%]d-}X ؊Je˖DqݺuS8K,8o޼޽{'$$|wx$k%b5 zĂ}'w>Y4l?}˖-)))۷:`}֊J,W_ܩS;yZ%3gNbb5\Ӻu>}Ux$k%b5 z1cF^^^~m*<-STTׇm۶uV.9rD=8z{<%7^RRrEUke\;w~Z~c 5c9ݽP:xJ 㑬t$gEz?~~F0޽{۵k׹s+SNjć{l5wqe]V~޵ܧzYڝ_j޼yj̙IIIΝgʔ))))ηkňrTbdD#A>[n7mʴq9:t(o'>LtJa8]?ٛoٳgOع,WaÆ9[+ĉ֭ܧteرҙږ4h ??_~Rս~;>>O?uʣ~݀J t$VzOq^z%$$$%%]}+WT/>m}qqq0gcyc}u^ЗJ)ӫs&OI{-a.]Zظq>޽"-4&&[op'KmٲK/]}ʿfro Ow%޽Wڟ" w'|"۵k'.CQrIx$k%b5 z ]>ܠAyCe˖%KU+?lו϶mdO)ɔyi¡OI{-/v~N5*q%irM>oW,ꪫuVݻWb_}:O5ji&eVV߿8p@w&?EERU0ZXMG1}`CW 6\_S:4twyG˃[ʃAIn&yS=WQO8f͚7|>(w}/r-{^jXvf͚x뮛={vQQ3<|?ާOXsϞ=?Fv2rnAyf_mUdܯ.ݲ?CY޻ϝ;wʔ) L4|WUdnoW^q+N%vNwD"WݻG%DCV\PP|Q-T.\KUA\_{5ycǎ?S=/(m?5HڵK=1c}'.*A%HJj:۷sΑn:J,n;wCNU͛725jH]zryn֭eoӧO]\sM@ҟey~~ɸ_]:`׭[W޵˛ &nRGMMMU خn:i5ٳG Zx3ƽ+TJdĿ!Zƍ|lժLLi8;vTO>^+_Oλrd4.vکe1dEyTbdD#A>+X|y.]87w}sFޒΝ;Wz4(gcwitmN8y#lM6}Οoyꩧԛf١sYm?c=漫v;ݿ,]Q=~mT={^{СC}~?l~u;t[lYBBBAA{T_VX!wgy;w'Zy{׽CV,MUf3SNIݻw;7u%%%!fƠ>9IQa<=b+Q-Zruש%=܇zȽ4:elյjroRkԨ!j֬Yrrr|;QxNOu;sÇWp2lذ[oF:ܯnR0Κ5kvIFX}( u+MwriJJ+7gcpr5rK_+wOk.8՛?ф_eܜ }ᇝ x7lPVJ^_m7t/G%DP)؇2" *1GV"VӑGLЕ877UV5kLII]v֭7nܨV9]?W]''''&&o>/:O2~RJ'L ߮Y&555))I3qD_+… ;vbɒ%-[IN-J,GU :r?N;gijժU2tM6Zo%xDխ[7燻Q+>ڊ ?yD#Y+H#lJ۷O>q>X+Hׅgǎ7o.VJJJ:m3l˖-lPTTwe˖IG>;)`# :tРAR?T(,,tZƵbר0ZXMG1}`yeep*+W>}9rD:%%EJzz~Ə_%[VVVNNY.koAج>E0ZXMG1}`=#Gܾ}+**2dɓT⊐&Y"e/F=9Çz#FӇ"#Y+H#oyf?l2*AYx qFoOS||v4nn70ZXMG1}`:> <eĤJɑkbbbllڵkMQ] Irrs4o\XBߴx$k%b5 zĂ}Ǣy{ᳵ*֭[-߇$Ý:uҷC֦M J+$55.jҤQ5F%HJj:v ѣ=_(KekLLL֭322P"KСC1*1GV"VӑG'x@oHN/^pB@l߾B>3 $5j\$\$nTbdD#A1ǎKOO?rޓP=\^/ T@vTiҤ 瞘u髫7*1GV"VӑG؂i+233'M__]UT>X ɅpaOZ*1GV"VӑGlد?ޙ`)yG>x*L}Ȗk!6m\XjJ 㑬t$6''gĈ/2Vyyyٳ{ @%Qk!W_}B>X+*1GV"VӑGl؃Μ9s̘1Q_d)tԩv#:-[u &O?0%)zo]x~VʋD$;=Pa<=ځDqqo7nڵkcǎw /2_GOHޱcǨQ?Xm"qH^7nKzB%HJj:krE3[i`߾}G\+ǥuO4O7Ç322Ə/1G%HJj:kr0:K,;4/◥ wءl+iŃ '0ZXMGa`@? 2D Ip۾}wX/?~*1GV"VӑGX;cd̟?>#|ܹsHI]:d2*1GV"VӑGX;cd<c.ѣGÇ~i=Qa<=ځ#`Ϟ=cBݵkzؾ{N),5~xI\\0ZXMGa`@0mڴ<1l!N:UO=loZn$_.TbdD#A0v ;vv=zz؞}ٯZ#C0ZXMGa`@0fMqxnzamannnQQ0GS<{8tW_}/={w Vcǎ{ C,UuS#fw)}ƍ_.TbdD#A0v 8e%޸qc9;gIDAT^+W\pTŽ`Ĵi&33yb /KO(&Nc}iUB+.]/3j(=I%w;"{ҥ\<@?ne]n_׿U_zgyF\0ZXMGa`@>|A ݲe_dZKa޷o03#GT''l۶-66fL磑=&!ŋ;u/u%NII߿;'/_e%2dk̙R:{LHJe]nkӑ J\V$k%b5 z9F@JaÆN; W%>tСC cyuVy0h y}tMYYYsŋ/?_~DwO())yo޽{Zzgggѣh߾}߾}9[?g+oҤlO:U.5k֜ K/#{zL*R2ja9 6!WK/tРA/E>;3w^JshֺukU?_N=vv1\u_f͚P˥S~lEV"VӑGX;c+n{ѢE@:DC7sիkժL XҷkԨ!Gl֬YrrrO dҟz;NƎ|u̘1Cv oF4{lY5l0S90p@٧tr귲ezf͒r]wչsgJ)! 4xBw7%z|[z<8x`F [CwqǓO>+=OZ~{G=V{'ʵ4w\\v׫WObjnVZ%[*6mi_d0Wg}*0!ζm۶ozLTB 6T{[͚5mӃxǁ=DjprrrbbbT灯3RSS111'Nt{;jպ!O#FQFz cSL_ &ȷK,mٲÀ͂-ws())dK9Cy Ϳ?[nr˰ag''灻EI%ynӿv:пկ<_mڴ=z+/9HkiӦ҄ ??X:Y `F!`k޼yuԑrt*qϏHJj:krЕXٷo'||V8TU(((P[;vؼys?>,]B*$EEERp곯bd[lӓ/ m۶'ukg;tPQRy]*?$Wl8l2)dz#1\uɷ}g}D%ZXMGa`@N%.gYr7n\\s5;[zz !=ZO=l^Tb[IRRR{9}]-T[t$cǎW#Gn߾]_Sټy?O:СCrGFQQѐ!C&O\S!ly؊dD#A0v 9sF%n???"Y+H# 1:)GƄ 86/*֭[+g}=^T[t$#FDTX]T'رC_\~ϼR/94ۄJ3c+=ځ##''5k"JpPgV$k%b5 z9F?^PP;$#FI)+ĉk֬y7?}25rnA5u ,n馬,_[zjժ?=zx6m4w)S8ke&M ??gϞ?\\\,nW^y뮻n̘1;0ZXMGa`@sرK$J'蔕x͚5u7n4/B\s5ݻwwڵkkOJܼys)3gΌUVWJiZZk.=;e%6l8묳٣k˦қڵ۴ig]XX>SRVyBe˖%$$hקT_UWX!\z,6xɃ>3ܹs`]~}͚58K;0ZXMGa`@_1+67|cǎKY+ٳSRRUÆ [oWUܹskԨ!Ol֬YrrrN~w)eSNcǎ-**~vn|w2{<3g} vYfh٬O>Tb#Y+H# ȱJH"/EPD' K"*yZVe˖8{;*RY/^ggg/_\vrFA߿޼yRYڵkKed@_mdY~b7'A.]|{-PTbdD#A0v *$e#++k̘1#וpU?L8X ̙3c9iꇙիC 7PRW^}9x`F^z%|iӦ۶m?*ݰaC]倕X)E9W_}֭pB_P>s*1GV"VӑGX;#up*)Sׯ/w„ %Kbcc[lYv[VVɉ۷W߻f͚Ԥ֭[L8Q{tZj]T*%%O?U?N;m֬YΖ+XjiӦ;w ~yթSM6GTb#Y+H# ^ _8_IIɖ-[ЊITbdD#A0v GxJmVVVqq"_}X)k+s3=+eER0ZXMGa`@MUNNNJJJ~֭[;/>يrt99}lH*#Y+H# ^캒.ڰaK/4###Mc_QX]v_~\\ܔ)S- gERY0ZXMGa`@}ו℄i_||gok֬bL+V0ZXMGa`@)"Qo߮oQV߇񅥷i#Y+H# M]믿}[8 _[q>n7i҄V\Pa<=ځ !|a0CADG_Pa<=ځ x$k%b5 z9Ӈ#Y+H# /H+#4*1GV"VӑGX;#VL)Qa<=ځ[1}x$k%b5 z9Rx֭aJ 㑬t$xMZL40A%HJj:kr"0Qa<=ځ`!LTbdD#A0v G h#Y+H# &D%HJj:kr"0Qa<=ځ嫾Q0Qa<=ځ"MxРAyyy0Qa<=ځ }ia!LTbdD#A0v GSB+6 ax$k%b5 z9ӊDC0ZXMGa`@/XVhF`!LTbdD#A0v ( هZqc!LTbdD#A0v H*,,*..W +emrr&r> x0Qa<=ځ#,'''%%_~֭םvVlr\9a!LTbdD#A0v [dI\\\lll׮]3224+!ZEեK9}ժU ax$k%b5 z9V H5MMMMKKkҤvW>bu[8))IxBB}J0&*1GV"VӑGX;cUQXƿQFR/buӸ}XQXn nݺSNW&D%HJj:kՔUM2>}ST5}Pa<=ܨQ#՗bbb4i2dȐ]v޽z 8xi,XҊD%HJj: ?ܽ{wmڊCa ћ5kF+#Y+H# ,PVYYYo ~+;dr&r> UJ 㑬t$pZqY0E%HJj: DFVLlB%HJj: DLVL,C%HJj: D+x$k%b5 z"݊À0ZXMGa`Sx֭aJTbdD#A0@V,> XJ 㑬t$*>VTbdD#A0@Uax$k%b5 z #Y+H# ,PU}0ZXMGa`lE%HJj:k.ϗ._0#Y+H# <8//OҊ lE%HJj:k"Շ'+P>VTbdD#A0qaVl(f`+*1GV"VӑGXaZ}0ZXMGa` +b0[Qa<=+b0[Qa<=V¬b}E0RV, g"磯}0ZXMGa`JNNNJJJ~֭[;/>يrt99}"؊J 㑬t$ X"...66k׮o؇X"ǒ#qrf`+*1GV"VӑGتAM6+ƾraſr9Q+Gw"}0ZXMGa`j|۶mn+~maW\>\}0ZXMGQlڴiz-#كSD}B#Y+HRԤG}tΝ߻w^s&ϕ=~doOgM6m֬x$k%b5 Js嗿;ڒrbՇeޖ.]zWQX b#Y+HF/U>|Ey[vQMc9V J 㑬t$Hm֬Yݷ*k+чݖ.]*G^1T:*1GV"Vӑ lRXX~+;L|x$k%b5 @%HJj:Bb0D *1GV"Vӑ SVLA%HJj:VLB%HJj:VLhC%HJj:T+޺u+} #Y+HHyAhC%HJj:1/ Qa< BTbdD#A#Y+HǼ(D%HJj:1/ Qa<KOO TD!*1GV"Vӑ`4&Њ#Y+H AJY[eļ(D%HJj:999)))[ndf+Lu(;D!*1GV"Vӑ`eYxq\\\lll׮]3224+!ZEեK9>з@0/ Qa<+󥚶h}&MƾraſIII҇7nܰaCp%b^@x$k%b5 V.ՊeTw~ÊjŲu[.СÅÕyQJ 㑬tS!M2>}SxF*F%HJ 4m4..Nժ&M 6l׮]]by׸qcm۶y|zTbdDC ȼMKK[rܭXaw͛׹sgՊSSSiQa<4FɌP~tZv111V #Y++SiNNNݷ*k+ч͛'Go֬`+*1GV"V0+++moaaYDG_0ZXr`7*1GV"V|Bb0*1GV"V܂b00ZXoax$k%b*݊C%HJ Tj[n #Y++P)l #Y++PYM@%HJ Tf!Pa<@TbdD@ea6#Y++PYM@%HJ|>_zz:eM@%HJ <8//OҊ 0ZXVM@%HJ}XG9f!Pa<F'>L+~&Bx$k%bBB+Z&Bx$k%b6}XG'f!Pa<z0++X_/>9ș+PyM@%HJ굜~[N_w2_}X qrr&:T*f!Pa<F?񏸸خ]fddi+cVBb9K(ǕXB@TbdDt'''w֭I&Mc_ߊmᤤ+BՠA˗;kf!Pa<Fj2QmV4.(((wVT+mڴ˜cч#@Tbdt!i[F} 'Pa<*"''YfM4qSRRR+w^M+{رcǣ>_ǧj(A%H(7Շ7n,(&&+\lܭXaك/\"ǒL+рJ ,P>H㥣Gѷ(o+Î"uX+GE%H(u{nٿh Qa<ʪ0+++"(Vfv9ș+"J ,a*G%HЭ> LD%H`> E%H$VLx$ DӇѨ0Z֭[hTbd*!MXf}J ,PU}tTbdx$ Tf0#Y00ULG%H\>/==]+`f0#YCI`:*1G ևZq}tTbdM هZY}tTbdDaaaVVVqq"_}X)k+s3W{>`:*1GU%'''%%_~֭םvVlr\9x$[bccv횑𦱯}X ъ(r.]q"LG%Hj[RM7o֤I\}XopRR4DpcQa])>!B%PQ999#4i$==}׮]߻w^s&ϕ=/IHHpjo͛Z *1 Q}8..NXllUW]|rgmqqq[%%%.\إKrf͚ъPATb'TV~tZvL+UJ  hLL{wSUpR..>0$AH[?-*-eY3VC-*jKCyMJtӦup="M1s}w{Ϲ^{rι{$Fa*_`*iWuE} =e y&I`s)))fͪ]w+~V+d%>?G$19rd||#"fT$F*I$Io"Hb}G#,$FX"ID;a$w$1I HbVZk{$1ƒrƷ$D#<\x1%%Ũb#el?C#l%&&*VI,G=sLPIQ^^*%,I,_njp8> ~ N233$[ Ip2c l*=Q\\!NΟ?/%D>X @3=| fUZ$FIHH$惵I>dG$qrJUU-GH;p=۽{__@!裏jkk/]AرC\|Hژ@#ի!~iGG)@Н;wtVVVW*5H imm;wnSS%@⯲H`~BKJJ$q]zuܹ0Bkkkkt$q-_ĉz!f޼y~$'O]V TWWoٲEFVaa!/pQPP_@D#ʕ+˗/׳U%%%---u D.8jjjBUcccYY~$"9жtR:"I@˖-Ӄ9Mj f5aG_{ݐeܐA NSE?I/f;6|ͷ~[ٳ'''gѩ2VGwߙիݎ9O۷c?ݲ-i/Ldxcmv^qf[v·~_@"ȟ$NII/`˲-]. WX /]F8I={pXvWGGLj#fϞpђ۷{g\őgd6^qfY|~.f$1 I@~&qqqqܹ?Ì3yVٕt:g͚ill$ycggz`AWWʕ+L/8pxW4hг>ٳ8RĴ7zLxlm7cV,G:tHN3}ʹsxeEEE6m/]DyyyYYm17o:ujNNɓ'770rmٲYkK{{{aaɓeW]]:*$qK,;sĉaþkuc=&k׮9svmZ߿_ԩS_b޹sL$-߫#g]Ç8ܺucJ _cI&=YsSS*$q#_Nӝ17nTH S ty=33S/b-fZorpkk+%%[㗓|C:ujrc:exsFV=&ɺn|c9;$~㺺qMM͐!C,_sNbgiD|gΜQ?} UH3U2ԅ +_?~qӵN۱cGLLr-[nEfONN7n\|||vv:rvIȼ2`nn4}n\7F2߈6'ʚӍ_}U$~ļyyy[oM6Mm3g,RVn^=2eI _o&..8x'M>|;8w:@FV=e 0^zIdz,x5Ϝ9^؜$1 I@76ϟ?o۶MK> <ػ$Fm<[dz%q[[b WcǏi&]lgg\VV4}nX&ymv#(G'Hzk3+O;V}+#,77liiidzB "rdKSS]wݥj} UH3cLe̖-[~JA=Sޝ&i`C'%%95k֨]Z{?IƑ&Nzju})j*Kihh4hȰad#GxmWen杝Ir%8x=#g裏N2E^U㹊Ȍeۮ\y/&L xC IOOc$%}_@ FzbѢE/_/V :AK466E]]ʕ+.\i ʑԩSNC \./I Z$1 J(EI R$1 J(EI RAJ`IENDB`onedrive-2.5.5/docs/puml/client_side_filtering_processing_order.puml000066400000000000000000000015211476564400300261260ustar00rootroot00000000000000@startuml |Decision Tree| :Start Client Side Filtering Evaluation; if (check_nosync?) then (true) :Skip item (no sync); else (false) if (skip_dotfiles?) then (true) :Skip file (dotfile); else (false) if (skip_symlinks?) then (true) :Skip item (symlink); else (false) if (skip_dir?) then (true) :Skip directory; else (false) if (skip_file?) then (true) :Skip file; else (false) if (in sync_list?) then (false) :Skip item (not in sync list); else (true) if (skip_size?) then (true) :Skip file (size too large); else (false) :File or Directory flagged\nto be synced; endif endif endif endif endif endif endif :End Client Side Filtering Evaluation; @enduml onedrive-2.5.5/docs/puml/client_side_filtering_rules.png000066400000000000000000002711071476564400300235310ustar00rootroot00000000000000PNG  IHDR 6k*tEXtcopyleftGenerated by https://plantuml.comvGiTXtplantumlxo0W֍B $DI.vزi!$@M&w~:n\X ]pB+-->\(zRrq C#Tz?@ ]O$xhaM6ºMp TPI rˌKd^U,:P6WQ&G['46:S w 0)p9Qt.b,)KMMl+)"R9@*m'Jh,IIǼ'=|3Ut3+t;Ab'^7ߐi>\_ al44',`]7<Լ]fW?ǘ)/zh9>QV8dNI}blztbecɕAmMc`shA}i;M{ෛ8ՙ:#晿o8L2 o agՊnq<ڊvZBsI8<&<\#d2~dž\O TG7397m^O@t;IDATx^{\eJK1eZ@<HMҲLukRRQD;4L< ).Su}+50s32/gX`or773 }}}}}}}}}7n|Ww̍&&ͤEC}Y׮]|E}~mƌ|hezAAo >誙O.(#_=G_.x >~6nO#?@ࡏѣGSȏx#G|;ci޼9_>eX>~GkgZG~}ZG~,}SU7}3  >a `r#E:uYB,G0}A,G0}A,G0}A,G0 /[ ? %ݔR&!G0hԨHovll,-sU#`>}DDDD>5*22rȑCH_~%e1**Il~Uݡ`h:n{NHHW02e +#3Ϥ+}9fq|t̀I`>}>>xg}}aaa.]0LKwfTG0. ޙSA!PΟ?lٲn"G_Pmɒ%N@k#h/##^;ww}TlÆ ,rrr-Z4y>ڵkh}-Q&Mzj>i=~={4sɱc En>6.]Dq,..륋۷/X&uGɓ?sKOO_v-Y* ݾnj3JJJ(/4k5+ݝ={v֬Yj?~޼y| B}M(Zti~17oo"@J|7M(JNN+e˗//ԩSHHHժUVXA3[B~Uߔ>VGP/^XvYfرcβ+_}>~7tSaa!7N:4[FFٳ֭Km۶=pF@ΝѵkWn}n49>ZHaaa}]renn4;\nɓ'iAՋ@A6l珠?Gk nN:OUNYFO8ZZAA-a%ǏڷooUխrʞ+?RGQZ5طo;7^t/T#CA+#忏t8eʔCt^^СC>ͥ#GѣGi:++pРA^(_?>Zx̙q}=|0[ӹd333_~eZ^zӦMGn}|ZAA-}ZAA-S_BA-} +7BA-}\+40i$3|¹s !4iJIIsG>}mPƎ[\\wK۷o_`Mꠏ'OR"?\o j#h˓&M{Ǐ߻w/Z@A{Tɹs"srrqɫVvA!PΟ?lٲn ԩS偍dɒSN5̂0f>٠`# f>٠`# f>٠`# f>p8eMׯ3g/St}; #? K>|7(?/ϲ&:^pXkG?ЊmȎ_&#f>΄}Ӧl/ EѼ!$z>.9~ h.:+o#[yO=Taa!Ri.a! z _/PƓ3g<zkڵ={E;pZjUPᮻJHH>r#zJQ3FrY ahz7KСCk׮z_4Ǐ`^zvr-S~}Wyyy0aB>}wsu֥ݞ}_ews(%f._̭)a_YM}4=J[o͜9aÆt:uTj}Y֭omHӤIKxxɓ?4??cZ*((UV)/B^z;gJ? [F74>=z4:URG.tm ={6Yz^zI5y8iwz!lK4qIG{l_wٲe7B1>zoM7vq6mDRS^w}4=JiW_}jWXA-{"WQ:x9ұcǮ]Eztt4}9%֭[ׯ?qD%+8tޝ.ߜN'\n7L Kn߾m֬:k֬=zl(JKW̋/H)t>}߿{ȷRn̘1ԩS+}=7֤شVmm!m@JJ3/#s}`*nL蹅.iٲ%L\hϑ ?/= hzҦTJ*-Y_j;v`7?裊+O?deeIk5kt).l۴iCtA-b_O_k~Uy`v&tl>e8(&?~.o ?<H[ .oOW4{Zx>My!tG-{}+v}`>~8]JF'{fӬMtn:qi]Y3<7|}BHyn4Yzj6l6mZ=腘3g/=++_##sy'{9i}vsLƍ 7 6p<lq\u{w>裞+0廾Bpwn{$/^kcw}d+f4 W> =l{W&N%{ Ν;of͒vk...kT3f|'ӲeKGEʕ+q&Lgqo;:]>6gϞKx}8?/VyΧ~*}X]_/[LZmرlX_S>zn},ݤmKs\հW>,*&ҙ2e eҥNJF»rlxx8{8Ot/|7|3yd?#B* 4hК5khztʎty= 4ȢEhanL蹅.{9ϮK ^_HW(ӧOXP 6m|5_Ud-Zo} .,hǏ-{CCC/_Et|S})9qMpcԨQtUR/ :czÇw֍$CK=7!:l0iTsy8_/b7 8<"66Nik4h@E/L]HHr?1c =gWy]5W>'99^z\(_h6q(aDzX:ͥh:%sWr1y}u > $m۶c.l5jDG{W3=_z҄}C,LGO &or%/>8hi_#:`?1c!թ#A7:į*(?V,G:aG0 t&f>΄}C0\ }ä4J7A7 p84i"%RUbccd ¾`=zԫW%q#GWКo#iEvIl~U >6lXnh#űs J {Wu ʯ¾`":mdqlٲ%HЇo#[n ;3aG0{Ѝo#BÆ xg$ޥ;3'aǠSPP|򔔔fH}|ơ]9s߃`¾A$++ksa~rrr-ZD^zk '\Be(=q}{L7N>=nܸ<2Zx_,aG|򫯾Z\\P.tDM$ }D9jkÆ k׮w4Xo裝ܹs ~w>٤I.]ܠg͚naGۺp¼y#4B}"h[˖-39Cvv͛"h[ӧOiΏ?o;,E7Ѷt޽{+T-C߿zNjS1\>Zom%''ǴիWkotyʕ2 k~9HC}Uu^WE{HHHV2225>Zom H5U֪U֯__IWA|/ySBIIѣ[lɯ>9aG1..]paС5k֬QƳ>[PP@3ԩ#e9y#bo^~/;v t f▲ ?*ݽCYYY7w'4MgUV+}Cm=)*UDݼ; 5w׮]–/y)U*WV@9ϥeT?9v >ږ>T:ݣRпo>8eeWZ ]ҽ6nx%2cY@9ϥ6^N $h[>~ᇵk^zuIIXx2៣GDnn.}1HyYzM?qD6mƏGo߾F+ANyw'Fc }-aomۖc|||ZZO>|8Փ5jh̙l?ϾM) "/ eo߾t{w EEEl>֨Qcl/R^iӦ^7=){F˗/Gom)#>Zomue}CmK߿N6 h[K,k"h[NZhXFz뭟~`)¾uD-O>`5¾vr]v7p\~w>ٯ:n8u?wfA7كBj`d.{ }TOlj'I71(,XO?w(q!v",֮]n@#D)));wٳ筷?q ~o >Wn޼7ߤVa&ugvumL7̂$aG0 t&f>΄}C,GЙo#: }@Ag¾`#L7 G&o#&---++K)#ͧM@ }8&MHbŊXZ FٳgzX"Yy睈#GhM7i&bƍ)4Aq WКo#QFDЦ>Rト~% }3Hu{T~% }i#cV C7׽{wǧ~̀n}Cx6mbxgt#ШQ##ޙ= >)wi I719fBC}|ơM;aǠC<^7l2"QN$Ҿ}CӬq4'$ބ}Cq,;$Ƅ}Cq,/$Ү}CqT%aGs"Z@"G7月vH }389r[ni֬Y-m6~%vN2MӶU^ԩ?M۷ooݺuӦM5j^z? v"h[Nqtz4hÆ aaa[l+hG?Ck֬. i¾A{4ł+ccc;f=lՋF`NڰaC6?))dأ⋴Τ$''MUV4O>|M6^*-z6ucKiz„  p~ܦTyʕ͛7nzB"A7ц i*ҥKibرO<٭[?M/\Dyyy+V<}4/M0(%l2{rlM#݋:EK,r^7,Xm۶k׮d=7s'|TXH }q>|?=xXn]8uupݻL*B F/]߽{iӦmݺU>(C?*Fロ)cʕ4 ֪UMK|m O|W4r;Jsl 9>teZw}wN}[oP~C]s̡yc~~>.izϞ=5kdkz#Mlŋ骜$]G6 999ժUnAvS#hZ¾TH. 6mx}&ѕ/MO>fvڕnRb ܹ&Ο?wg4T?s 4=rH?rJٽccȋ/seggә 0i4M?n) }41aGry{v!-ɓ'zhZTyQvL3[l'H8o^{-448qD)t]ܠAi}1˗/4=wN:QѼnN? $)S]裉 >ڙSHpgVW8G }9щSGhb¾4:P8G } N$Rrх>ocp"&.Ą}C,;qt&&\HdY }41aǠD"$.Ą}Cgl0?Pх>o#];be¾`v] >Yص#v}^6 f!ș3gjԨ1ym֯_۶ml7|ӪUݻ_pAZG }H_x4rvڱTƏ?&&M3HhZ¾`\+W̦O:Uvm(..XoFӇ73Ѵ}C,<˅>ք}C,}t?HLLo#i…M6DRG(u W,Ѵ}CHׯ_Ŗ-[QG(ʄ~=+CMK7 6tP*HΝwjj*%X K6i60i >ڵkسgO;3à%[p!c۶m >o#KC;3 hZ¾` ]3à%>O~)06aԩSbe֬Y7oz*}4-aG"777)):{n^6l ;wrѴ}C1">*sܹӧ924>o#… cƌ9~8[[n]@>o#8nܸA`ǎ-FG }e59g߾}~hZ¾3g;voF0aµk]hZ¾2Ο?EٵkիhZ¾JJJ w2e wTCMK71Otqw"X-Zٳ>R}4-a`駟C&tR~>oc#vAMK71ؙSLQF*U.\/s:[B~ c } v)))|!J)$$E%!yΞ=K۷_pH;Gꠏ%p;y2eUqq1?XGT} ¾NyIFFFj4]:f͚tϲ߽M&ݑT4ܹs#""vnΙ3VZaaa[la3YYYt͙֭={vݺuif۶m8 7iZ:c 6v>a`t/w҅4hPϝ;w޽{=ZG6lXaa!3fLII ۾}{;=\nɓ'{%_!77oi̙3Vv<}C>@oV: ;t}RVb#GunVV'NHӧӪU;v8e+vmͣJN?!ο;MTR~`o+ӿu۷OZo//͛izƍ.]LzĤ獷=1x>2tmҤI41p!CU-M=z4==& }٢":[xqaaL}GhAq҃YQ=zz{yN?!e˖O>|xڵoFI?ge,]fff;5jԠS9z}$/rHHoڴi܀j߾=Vhժg}k;}Aoc"gocC9c } vo_F;Gꠏ%6oޜG"X}+V:i >;|'{WXX#uG }/Ϝ9ç"\t T}CURR"|'Q }4-aGaݺu|0 AO:/Z@MK7{m۶Fp(..8q˗Ѵ}COx9RQ^\` aG(eϞ=&L+bS|ɔ)S\Ҳ>|Z*c >ڵkVJLLsɜ(w͛7_2I&9)|Z`(aG.Yج.4,/ޙ1!aG0 1;;;66wfLH7~}tߥwfI7–}LMMwfI7–}t8Ό9 >Yز.>/ }kGl@7®aG0 vĮ}C,>/ }kGl@7®aG0L|N hZ¾` Ѵ}C0qR",^8666s>o#dd6m9r$e%Fںu+QFH8鯁%Klj"%[ }4-aG0ĉ aaaO>hZ¾`0!1>>>::60i >z׿N0i >nJܹޙaG }Shܸ1uf0i >)wil >o#wNNN~GiCi)kBMK7s݋_`i >~233ҥK&M6m:hР{W^^$hZ¾ׯW=GAe@0aE;i >NvK7x>7ifɲ{4|U%:INN~woڵk'ЦM AMK7t#8p@IX<0j(i}/ ݴѴ}CA':u$aÆ "֢E >W~BG }/M>.t_M AMK7t?#o#c>}"P¾댌 ?w_BB޿ }p?H066OcTT||O<(_? }x̵kƍwlْ]vC G#~M7"zѝ:u>vܙנaG~o# C7&!ϟ= >a_/>hZ¾>o#@`%Xi >hZ¾>o#@`%Xi >hZ¾>o#@`%Xi >hZ¾>o#@`%Xi >hZ¾>o#@`%Xi >hZ¾`u`¾`L7} >Y3aG0 t&f>΄}C,GЙo#: }or}hN7 %ݔR&@ >aGƍDJ}\xqll,-sU }#=,ӦM9#GWКo#i֭FQ"iͯ 5aG0]bS)P{ J 8q"e1,,'LMMWaG0wf@¾`^z>;3aG0֭[CCC;w;3aG0ƍS I7LKwf@O¾***4itCG_)S8N~M >;+rY6lW_}ՉDaǠf8<}C $}C }Ci{¾AqT7a8ic¾AD%KnݺYf=zسgϑ#GWίʻ6mڴQFH } 8vs۶m6lP8w |׬YC%eH[ } 87ЪUH$~xbbbzEٳ'M4Icaaa_z%! .(=-ZMHH8v9uԆ Nr6l[tڵk<6\yMH }?%q}4Mt%99QK.c>4WbE@w}k6b?r&배0:_f7@"mF7(.rl￯Uӝ *4!mV\\,{HH-^X&Q ݻ֭K.]޽iӶn2tٳgㆇ7pB:5 yN6g8G>DZ]O:х>jD718mȜ:s .˴`8G>8挣 }Ԉoc0r" 6.Q#¾AʉDe8G>/'B5"ԜHх>jD719HKх>jD7~OԩSgy~GaG0 a_iB7ǼrW }1&}C,o1b6wO4}AhѢiӦ}9{,['%%q͛7t_iB5!f!?拋+WtRRѣ={nܸMQ"ipTX͗&l }Ԅo#w>w_#"";r^B{ohF_~}k֬YYYYA }Ԅo#w?^y⭷?Wc(?~ݟ{9~ }YxÇOIIf۾r }Ν.]㏲U&}C01-9Q¾`4ȏyOKA5!q8111R"c~֭U4Q¾`=z4hЀ%#GWQ¾`e˖"%ycxxxXXXvv6*Ƞ }իWشiS:){oBB>jB7 6|p:[lYǭO>JP aG0ؙ3gXIÆ 4iwfGM>:u%GM>-[ؾ}{3 aG0z1wfB5!ޥ;3  }Wnڴ7ޘ9sfektO2_`%O aG s8tXN>=##i}6lgYݻg͚t  aGZfʹiΞ=`{N'~&GM>Z׮]`&ǏWWGM>Zl۶?| &LD }]v-Z?(t6 aG+_ƍ`nǎ{7RQ¾Vb :?0\T}Ԅoeo'O_;\xŋǦMySANqq1?|>)}4aG{׿d9sjժF'l>]:f͚t yyyO<ͤPʫ4[׫W氛$+++$$H茵[n4ɓS = v>Z>N?ͧC3m߾=?hР;w{=z4̙3![R(1 6nꡇ0`@~~~nnnV&L֡6lXaaT74m-&=J]]tR+_z>S9G_;l}C-S9}CF72zR*UrcJ+W.]ҽh]wEW"Fv۹u|񺩾$ViPl߹sgS_ݩlXOk>oeGv$YϿKbtV%1l̻{ŊE4wG G_J.^nex5(UTa:g*翏v>ZׯYoν-Ձ 8pȐ![uGMOOg裏ґ_\\}vv/6W_}UvիW5%\zMSjM6Ǐ\+_u@]fMaaKzzI󊋋L{>oeH6o.9"Rxb_u >zGk6jh̙l>?8@gafFԪU\b}Zw /y#?}-Tҩ_ifHHHV>v޼yP^|r>ځ~f#hJͲ^~Ƅ}C-zk4 >Z~Ѕ ;&&4 cB¾hi&$hօnN¾Vϟ(|9 >Z >Њ%h1kk_o{oL8~|%T }Ԅo%͟?%0Ǐәc B5"hUk֬6m~Kw7Z>jB7̙3O`ݻwϚ5+))ĉ)Q¾wM6 2gY|z >S Q¾`8þ҄o#y尯4!fc^9+M>YWJ¾`8þ҄o#y尯4!fc^9+M>a߽f= }äeeeI7<ͧMࠏ }8)1g?WGM>zѠAHv̯]6""bȑ >jB7"%yc[vv6*Ƞ }կ_?::IY%GM>>C:%GM>)ԯ_y3 aG0. ޙQ}Ԅo#t:LJM4y2L}Ԅo#J1vX>3vÆ 'KOiD }?8 HQ¾;gƑ\"GM>BǑV"GM>;'x>jB719ҬHQ¾ˉ8zcD } RNK駟nԨQ~*9rf͚\7'}Ԅoc0rZ*'O )))㧏N'}Ԅocq%իꫭZLOOg322Zhp16sԩ 6Ir6l[tڵkСC={O4$Νnf͚рNodK@;vSі-[5ON$ a,Kʕ+izɒ%[f)OK.uG{'h"//bŊOf+ĩS^{mĈ~l߾3 F?GDN'}ԄocpÇ*Sre6תU&(|BAӻw[.M\t)>>{ӦMۺu+СCgϞ]\\7zmjl>G?]GBN/L\ >g=5RNw"9sЕiNVV]//[g@}ɩZ*[ki6L?ocq%^+t_䦥ĸq|IhZ-ׯϦtb v׀;vT~}=tQQQ*UKӓ&MG}8oc0r۩S'xIrѶm:alٲ'|\p!S66iGM4i֬Ybb")-?Opv&N迏۷ooذaVFK\ >)(Z:tԩS8ocr8999tG.\XҢnGADZqtiQ7#KA`D"o'.-~p)HtK\ >H=ҢnGg&R8o#|?.-~p)8 kBw_#"";rQ Ϳ~}׫WYfeee1'uS? }?իWS:O[gj %r޽s=/6uS? }---22~ 2$66}]Pw!..fif˖-aJϴԏ.}C,<Çp3m󙖕o#&??_~S~̟;wNt?V uS? }f1OitfMR7 p8$QJtܹ?WrϹc)($Fׯ_TTK$;)#GWr1hϵJ7oAN<ű~aaa6R>v@ >~޽{7k֌yc >~={)G]zP¾`q8+K%oX 7b%c<СC0\-<(aG0]9e˖pX>v@ >ؕ#;!Q s }S`WArvP¾t͛76 ^y:M/0W׭[>1εJ7n3gʺ|:6l2+ڱ={vrr'@DM]AvP¾vӧ;w?!0.\H!y澨#{̨'h ,>S+#*bp)ht}vN8Qa"MR7ٳh"}Q",RɅ\ >Z޵kV0Bnn[oſBMR7>]vG*d…TIE*M}ԏ.}C뉉1 )..>}::nGv)|lfϞGMR7ږ,Y`/\ >Z]G'|7JF}ԏ.}C-99?:g޽*U˗GK }6}0UP;5})h>W%>Z=X\\ϺLO8_*r%>Z[9xGyZj5k1bԅ=zTRqsZٳg׭[7$$m۶pxZVZaaa[lak8qW^l4hs΍ڵ+͹pСCijԨL }VqKKK\n_#/[Mth ¾V>>C ϧ jj„ l>O~!>>޳hdݺuעAQ1cƔп۷gkң 8FPvs4vaÆИ?w{=zhY=n;}l%>Z[YxٛoСCG}%?rzjϢرΧ7siȨZhf+Zs4vG'Ih+V6`֭l5%}=r}s|_h ¾V>E1Kk׮o|Ϣ~zZխrNMQ2m7wq#k@E쾾6?}C}N#]Yh7nt}axIj w{m݅nҿU~o>io/^Hӛ7o>u|_h ¾֦2ww۴i#^z=ӴuK.e9z(}1dee43Oѣfңts4vV 2$//~t]#zP":o]x1m<7鵏zcC-A7ڄ}4:`ۗBpw cǺwҤI:TRz߽˴BzM'yػl4?_PL^vmoԨ̙3j633wީQFDDDJJp7)p6xa~ hmXn+WlР?C-A74ݻwٳ&>|5_r h ¾֦alIWwqǐ!C袒_ߧ aGk[tw}`͛7/\ >Zۅ ͛`(aoM4ҥK1 9wJS_7#KAG\z5ARRRJJJ4uS? }ɓ'^c_~ڵA}ԏ.}C퀎W_}Uۿ`ecǎ 7~p)hND,( \ݸJ }˗/O4 ߋYvv ˿2iiiYYYMyh>-n*>jB7nvI;w.~.2rrr-Zjժk׮/Ci#::ZJTXZʠ }'dn-y~);//ѣ~,n˗/9r$&}C,}̯Y ; chhhxx8]*`}aG0 QQQ 4g־}~%el!fac.96nܸ[~RSS҇o#fqkr3~_C7"yf}0`@ޙaa_@7"y. رcޙaa_@7"H(z~g }h¾`Ar̳wi $*Є}C,/((gZwf W&f<Ǽg~p)f<Ǽg~p)f<Ǽg~p)f<Ǽg~p)f<Ǽg~p)f<Ǽg~p)f<Ǽg~p)SYB}ԏ.}C0*Uo#FOe uS? }#i,VnG`$m?*MR7 ᧲X\ >4TP_7#KAG0b~p)SYB}ԏ.}CxZ}*Uo#&b~p)ɧX\ >a_{G}CiͧB}ԏ.}Cq;w/\ci[DwuS? }]v-))[n[lRZ֤enG+ݰaÞ{+W<:&oDo#)y{Gkt/~o#'33./_,m۶7o<**UV/޽{+t/;}/R}ԏ.}CA'ׯ_۷{tn7j(J&&&u։sL6ho#d?|%O^F￟)_K#X\ >NyfZZ:v(ЦMKS_7#KAG#N7m԰aC>74mڴsx#KAGЉ?:4M45~\ >N̋/#c.]:vߟOR7"zذamڴ8vak!#KAG~K\ >]ZMR7nG` ϟԏ.}C0/J\ >M}ԏ.}CnGzS_7#KAGo#MR7@o~p)7uS? }Л\ >M}ԏ.}CnGzS_7#KAGo#MR7@o~p)7uS? }Л\ >M}ԏ.}CnGzS_7#KAGo#MR7̢N0|\ >YWJ¾`8þ҄o#y尯4!fc^9+M>YWJ¾`8þ҄o#y尯4!ϗyn)ȡ }äeeeI7<ͧMࠏ }8h)1KK\JC5!FѣGY"1|򈈈#G aG0Қ5kPgg4Aq WQ¾` 1־}~%( }Ԅo#.hoܸq~+Ai&}C`tMKxgF}Ԅo#.Y wf@5!cҐ;%GM>)DEE1wfB5!ޥ;3  }S8vyyy}Ԅo#|0n8/&}C`,gϞ2;wT}Ԅo#IGT}Ԅo#3HB&}C  }#D>jB7$LYp8҂GM>ǑQ;v 4(66V 6>jB7S82~I''OnҤIhhh $.Q#¾őL$0;,,JqO]F}CAjȰDI'u:ulqt }=3|hٲeP}Ԃo#ҥK*/\0~x:|w[lY~}ƍ#hB7t&,Nw=xȈ^H$o#|pPs }]5~(G?X"srrGГo#%N:#I7st&f>΄}C,GЙo#: }@Ag¾`#L7} >a?D M7 &s>|Z*aG0ሎ)1===66*@>z!}n #""Fɯ 5aG0Қ5k(,&(Z }EEEh>o>!!_ }C`t)Melܸq~+o#YcccZ̀>}CxtA8`3 }wiHǎ F7L!**wf@O¾` ]3z }/JJJgh__HSN'3aGQϤdž Yvܹq9`%G L7lqdȠ%82Hdp }s$2 >$2>;Q *¾AlҾ}Mҿ|G f͚hL"|5jԯ_?<7Ch[BCo@"ocNj/Rg7Ϝ9sAg>ɓ!!!VP(###''&֬Y/A"ocGr+RR K/:ta^|Ÿ8:S/ٱclrԨQR\-ZMHH8vӧ-ҬY3ZN]tRR>==(٫W/qNڰaC6f+W6o\ }C0ѣVڭ[7jƍL&g.]9lZ-[Fcǎ8pl?P.]t (|'uZ>?f .DD^^͗&배m۶~ '<:b?^YBd @!B"*\"={Y#Ѱx%*APpA8G.@@d I $AaJNTtg#TOOf&L8x W>|?|BBBN*9DwK2Jg)h:'NP`~2Y-NTYZj _pGIII3gܼy3N/ M6]z5BϾx͆G71!whY?*H HN1թSgԨQ>hii)GvniXF gijqt>bϧPTT4eɖG~" LϞ=bcǎ}KJJ΢hʢ4ݙ;eZx\Ҽ8*DH]vIC7ވܴiS%I I?/I*8p`dd$ݮnz?TYL}|f˖--[ԩ|Fr~EqqqB??~'_]t6)cǎ_|H6~4 ǠƳ"tb9:G ~ vLTd/MB7H_MB7\#9:G ~̊/9:G ~7 NEI?J"Q$~O(OMB79}BsϪbؗ' v0V v0V v0V v0V rϙ3gܸq,]\\ܨQ˗/Sd۷o۶ XvQ>Օ TGS ~vA9KKK7n|%J~_H(//^zYY˗ h B.ps>%%eѢEGxx#G(GjժuA;'_QQѫWs( TGS ~v?#/;;rhXF+W(1H{}wF8GS ~vA=֬Y#7--lHW߿t~iV`Νr@E=V ~]P?S9ȑ#cbb1 5ev-6626nxE=V ~]P .8\G39Zo"/GS ~>nsi 8GS ~>"9cʧ7 | mH6m1~x(P?o#%ofHHHll,)>x&Nxb h B8phڴ)dFh BtؑdF )?o2?vOf?o#=) [?+GS ~= \zuÆ ͛;w2qD AXf -;?ocR^^N:عsd֭[gݻw꯿ʏmMA71YzY x7?n%ו~{/}\qɓ'sfGS ~ .hM79{/h7E D|wK,D"Lbm~?n/̛7S?ocjժ]vh&33ѣ~4~=k~'NG68h BAetw޵k GS ~ Ɛ@?M ̚5ؽ{wj\=[YݺuYzիUVQQQR7o6>=r;wdv/^ Ǡ@7nܘH!tyɒ%FTBAAܳg2B?*M]r H?B?~WwuҥKKJJ˿맞zʠJ$=2M]r H?B?v72J,XРAPZ`|5jT駟>{,?u԰a(Wo֬M+SfM4ZGY_iWTTe??~SdܸqI`DnSKNc !x#ycǸ|R O0vڕ>|Ȑ!gΜ),,߿jj*Ӥ*_޲H-[駟n.1`{D٭[7ُ?tP?zhNN*UneZD<?BAg?߿[nsoɓ޾}{ڵ%皫zbe6o!9[gEh鱱GQRP&8Y槟~8?Hɍi9D<?BAg?**dJހnoN߷oZT2 rjmqpڵuCwH>r݉x~ $~~?[\K?֥TZ? :kaǎ5Zrr2?3GZW={NT儻 H?B?~WM6qϯe?=ȑ#O:EÇ]<&㔖nٲb-| 6f%eRӿؽ{w//7eٳ%'1 ~ ~$6lлwoͤUҥKBZ={6'-+<݉x~ $~-~Z Ǡm .] Ǡ~4|?E!,_\l 6 GS ~ y~GS ~ Mv~a)? ;wTh'##V?oc> nݺիW;GS ~ ".^8i$=`@;4 /^̏OMA718y$)HG_?ocqҥ_~E }Ï MA71H!K.\_d+B/!׮] Ԝ={vŊ|P|ᇧOGf v0V v0V v0V v0V v0V v0V v0V |Fqqr^~4G3rrrM圧|+o~(//)m۶Q>YT~4GK ~1~x(P?o#%ovHHH۶mI4Iᡡ|Q~4GK]֣GXrlݺuBB_T~4Gcx tIRRݾۆ |L~~>3#ѴiӨ(<?o#=:ub~ݻ7h~4G{~mǸ8<h B44dF#)?[ɌFGS ~#IҌ3ɓiΧ;C]} ,GS ~^B4iRAA^n:>+K1) ?o#l9K+~4Grd"GS ~AV$h B@ȑMA7hE792R)?MH)GF)~4G Fg92L)?H[.R~}>tӧ˛O>dV|ݻK^Q ,E ]?CG I" #En۶mt#9?ӢE8?ODGGA/ڳ|a݌?MHisl%Gv3ph""&Gv3pht "m(Gv3ph!")Gv3pht#"m+Gv3phA",Gv3phD"m.Gv3phIМ糼dg9:̰CG`g?S-.Ϝ7~[ ~]9oL?%%%## g-.(3gh=o)f7b&7繽qo84 ~>#++Kq圧|+of7(Ybbbh͢v ڵv"|IrrrDDS$3~xhv]?_i&ZIN cHHHӦMsss~ ڵv"| MPI>}B~tZJ ~>fʔ)4ȈÆ [x1_(Ώ֮R~)++x}EGGݢw~tZJ ~_~l92cG{Αni3cG` =c-zGG𭵫G` ؝c-c8++u'iii4_y~`:uwm#Uoc3mڴ O_n:>/Yg]v 1ǠZkW)Bŋ_~l~*nrN{nzǠʨ<1ocpĉI&?}%*n0G-Ocii)?klݺ㯍+x oc @X9ځjQan[ ~;w=GQVV_CGgڴi.\)gΜav3ph;>xR)0n7-G}ws xrss7l_*f7ѿy |o6o$22R?tVM֭[i=%Pɓ')}ڵkK7Z;x +駟[cɞlhիW;y戈VLv\Gڏʺu3_ #]vq.F#z-im'~)ZZ~7;3+X\;.(qנzcGF#촯ΚZ[~ (0z­?3uk\ߨ m_EٳG.?P4PzÆ p.l}w ?7ZxVAyyywL '''?T<6Ç)rrrΝ;7|p>w3(ݻwWv7jȑ싈Xk׮OLϟuҥKCUwtGe\]Kn:/ ~o~ kРA$kر$Vȑ#III5klݺ5MZjUn:/h̙֬3zR= gyx~!4lؐn[j5{l3]-ر߮W^xxxFFܦK?JWXAݾcGƳfժU-ZsfG@7ѿ1яyyy}%<ءC/qE/ ~oL7FDDw9rHKm)Gfٲe>e޽k׮/v3phߜ8qbɒ%>7޸|2CG"--wkuqo84 ~{VZk.~yQ"UƸ݌?=׮]? | n+¸݌?}ޅ9Ν*Ͽ0n7-mٲ*H/bqq1aph B{キf~₪_eeețJQ>75?oc@z^{̙3 UCii)܅?x]^^ަMYh~ޛE?ocAٳi6^x h`3Yf⑿HNNnѢS$E5?oc`r 6s2O9g6k֬!pZfmۖIgJr j~4G`{WTTjcǎt$GreBB_N=V!BQF9ٳ^x1_H?V Bcǎ13lݺOf?V B0{w_~=aXYo# 0333tݓF0? s=3 #~]9Ϟxd$cU?$s= #Hƪ ~v!x35phBygj7؅x o# 3獟CG`g?S-.Ϝ7~[ ~]9oL?a f7 _0n7-g,qo84 ~_ex o#%*`n[ ~/1WYv3ph|/CGcLU݌?c֯ f7~_0n7-,qo84 ~_ex o#l˛5k?ނ(M9O{ qo84 ~aǎ:YpnJ)M9lf7X͵kqF~Ke$9f7X n̘1GtOT>in[ ~zwTS-~?cn[ ~uرn/^ܽ{wJJJ\\\dddnRSS)GYS@z/Ҹ݌?4h=GZ7M6 zݽ{4jQ݀ymn[ ~EoSoժR|,Luebn[ ~E̚5ߖ7krd+o. o5f7XC=o>y3))be|^.Ouyӯ1n7-"wI¨(ވ#US]_cn[ ~Ei7yhD.Ou~qo84 ~=ҥ ֏0ph,{gϞ+3h -"}ow}x~-"ԟM^7HHH=`7XyGZl4ctttΝ31ph_oڴ{H:uzgB?K8̰CG`)Gv3phnn[ ~oP eg4b7 #22 `71n7-Xqo84 ~jx o'oIDAT#Vcn[ ~v3phCGƸ݌?`5f71n7-Xqo84 ~jx o#Vcn[ ~v3phCGƸ݌?`5f71n7-Xqo84 ~jx o#Vcn[ ~v3phB`?yo84 ~vs^;+S ~vs^;+S ~vs^;+S ~vs^;+S ~vs^;+S ~vs^;+S ~vs^;+S ~>Xyn/P?o#YYY999rS>7h BgiFV<)bbbh͢2)?_ܢE H67o1~x(P?o#%!!!m۶%EҜ'9E~/bǎccciΓɕ |9P~5j={6qBw܋/ |̱cǘ ZKnOf |Ͻد_?<h Bdff2?vOf?o#=) y<h B4x2~-hΟ:u\?o#=.\GM2E$~7P?o#1LdƢ"(R )?_#h BgEj~4GɑEz~4G<ˑEz~4G`5ZЫ򬬬`a~h#C"(!MA7X^92<(-رcHHH̓D$~Ex'GZ`|#""Ɍ߱ GS ~V`D SNc-8iٲe?o# &رč%[n'3~{'32~d~ҥ  ~=!? G` S<V"BӧSN%?Θ1a t D7\0i$P[Ϻu,ӗ C7B> B~ rl?5X(2 ~ ^$Pd H{ A71 GPd0 tH"9nܸk׮mڴi۶-)СCuU;zhݕ9UχVwU͖-[:wL'تUk@ocp!XRRR~}lI-ŶoߞO?<::}(2 ~ "$'OV^9wܐ!C{gE.1cƌ-[RZ;3f /4lذ3ydjy洬׿E鈈q\NSNT]-//: wlժUڵ7@oc i##55v}yׯ_2tȞf͒sXd唞8qc=h:N"R]I'N4jԨ6_zqI7Yb5k~'8V@GBeH^eW`m۶~7lPd",кwÇJ/BHHԩS%tHXZt)+هh St .#))i̙7ofF?~iiiӦM{N)?תUt XU(++C^mz}? 4'Le$"9SNڣ>JRc9(9 E $$$1rrrrn։-VFnׯϥ]Q]EUU4: G~^"iPdqqqvvJϞ=&"ˌ;,))QJ$++ҴJ6l}oJnڴy,Mt{rJRvʴZ.tϞ x$R$Yckݺ~RSS})% ݭw%&& t/`d Oɍi]VQwNAb@~ !tA3fsm([QQ@~ L@2U$(_?Ӑ@D"o9#0 ܊T dW^yuAgY8 0DW ?+p( ~vAWd]Q$삮%H7edϙ3gܸq,]\\ܨQ˗/Sd۷o۶ XvQ>Օed6nҥKNOOOMMe[~=Kgee")Q^^^z2/'@?씔EGq8?TZ7h+**zꕜ>~S@b&^?!++KqedS>7VZɊ#wߍ7  gd=iҤE[R@GFF")Ar iڴinn._ nٲe' |!?"--O.^/6GP29&&&rOf 0`1cd~֭[2>>OfG4x2 ?=`K.}fG48zVVٳg6~^￯\_^dI~~d3֭[g~i…4b~ްk׮ɓ'Φ-0`gG{֌z8uԔ)S/?N@-oSZZJx"?6@tc_x OΏ2 #JYY٫Oq`?b-#JFFӧ q…4~=&\{9~ā &۷3O?#l41g~ZGhbݻUA޽{֭ӧOW^Z*23ʃq`G ~Nbƍ$2TΝ,YjPPP@%0XLq`G -~ꫯ뮥KO=^S4XL~'#Є?v72),XРAPZ`|5jTv駟>{,?u԰a(Wo֬M+SfM 0ZGY7o  ;g{۰aðM6щSRZRtKp'(3hOG ktq;(„ eeek׮,C 9sLaaaSSSY 'O.--e(Բe˟~FvѣGo&8jLJÇ.ے_n~-o)Sw'NdOru,'(3hOG Osoɓ޾}{ڵ%zbe6o!9@[gEf鱱GQRP&8H?;[oEO>$::Z[(K,]NV~'#ЄЏ$Ǐ΢m&9eJހswR}ԒZyw/\P;Kͧ~ڻwo:P=H,|{IC+*c"3hOG %l9ĥ>؃f4SB;Z?8p@ZرcGFV\ܥ,L$===<vZ?x~YZZeV6l0;;q*%t#矟;w… oFf]իt[(3hOG -~$6lnc㎸Kԇ\;dVϟ1}QjVv#F`Ș 4Xz5dTZvڵv5kԩ_yubwaz:Ი=&48x IS}o1=&B's-_hOG~"nӦÉ藺'#Ě5ki ???M\|96/((Ghe̙ϟ'70̌3~Z9z~rc~X{?-&y饗]4#+W&M$0ȲeÏ2 #Gqq)SHDxv~!E*_~g<3336~@7s]x1h녖_~%?~{~W_}u{tŋyIa|r^^̛7J`KG`k֬g3(,_aÆWl ] KtEo# ""G`tE6.EB.l\+~]DW ?+p( ~vAWd]Q$|Fqqrln/ZA#o*#i FG jJV ./A0ppH/BDDh~tdd$)$ǐMEh&NxڀA@+032zŋrfrLJJ$A1p@gyOfGhPRd 0OfAG4x2 ?sܹiӦ24l0iW$@?_Br׿UPPa֭[g9qD G3NUT7Pd?rd@AȀ"XJ`ȑE<#@# lG`xСuU|Gvޝ}"X9JMdE.#r%%%K Gt2j ?H)]t%aǎ e1w{333'.eL貊2355/..fe:]~[$4Ϙ1ϵ /e:]~'$Oݻ,rtNFW-G @2[>r#tNFW-G F EB.%4!"!GCdt ~ZV?t2j ?H~HOe:]~>$R$2ZB@7yfCg}e:]~] .t ~vAWdǻxUK7] AW-G`=gΜqƱtqqqF._L钒Ço߾m۶,((`e222ڵkGTWn/L'o# .--mܸK(~z"ERzeee,_NFdt ~v씔EGq8WV 9^z%''ϝ;7''G_tNFW-G`I$>C5j\rEYAܻwロ4zh~7CtNFW-G`ԑf9o߾iiidCJ_zuӧO;wEZUK7udgeeEDDrGӾ}{K7ԔYXXح[Xʌ۸q&ߢ-%:SRR222L$A j ?3gz0>>~SA.%삮w񠫖o# "<ŃZB.lxj ?+A]<%b ƻxUK7GfZB<PEx7u ~YMn|ke:]~K_KWX.%x[abbbdEm6ʧ7Bdt ~N###"҉GDD?/ ]UK7:- i۶-)Ɨ:<<<44477/ ]UK7:' }G/re:]~⩧ׯ_'ŋBe:]~̌DXXXTTCtNFW-Ǜtܙ111Of-L'oM-Z'3]UK7&) /stNFW-J4x2e:]~/\0}9vb4B$>NjƌcАF2ZB̏$ &H6cݺu|)..2eA/C3i$CCJ+2ZBƏm 0EJcUe:]~!GEJce:]~ڏ@hH]UK7K9ǯ)A׊e:]~ΏY"%"uNFW-,#h.>W$u ++KKSE2ZBYGȱ*"srrڴiyyyHGE2ZBUqٲe;w{w%}5k;l:=}v69`+cEFDD4mڔ.bdd$DrTc=X.iR.%[Qp|o>&&&!!ȑ#ҍȠ ܩS'RyŻ[i홞 ͛7lْ& /r͘1 R f͚E~nn.P1cX_~aÆ#up{^;wnȐ!=܁XRa:4m*YjUv9."8ѣkNQRMxُ ;vGaÛmݴ)=uԡCr-se,lb7\ k|Nͩ_ 9GNlg0~H]UK*#̉'n6> ʹ>'N6l t㖜es@nݺuVV%^z%uLԭ^ɓ'&KțTwޒ{5*,,7]]^+JǓh4{1MxZM9P~P^5)Q}>}|g,Id{K.WL&eewmrC}D2lX+V4]qO(MCDf~TGN CNUo XPK*ciiow~|9Hkaaa32nvVV 4`@WV1Mk[Z$%%͜9V,? F^U75j\wWWFzE\t)+$p4j# '' Bg3m۶3gC ͻロ%ԗZV=7u"}'i[)%uذƩlVZ:=GN"Y-%cA-ߪЏm;vpi]я3%wt>bcw*4i >Mwu|{Wuԡ 裏ҀH'4|h-6#MNfFI|QӧOרQ ۬Y3Iue!RaQ&%%%tJݐWE6.E,Ieʴ.R9`|"GNgXPK跪C".\x"ܺu%5HX9hxdo#HηYg Gri\Z0{~-T$9.ZTWV"4Vl!FsNώ=Jf@nA}Q؍Ksaq9*,SN:%cA-ߪ܏ \d 5oݒ3:Hj瞣0e2cv+M/;v/$[E4)_TNLZ+ޫUjj߿o>P? HkСK$"i@zM#k9~O^}(c]>?YvƻXыo, j f+?iVh]w^~6i]@EEE>3Ώ>wH~HTa ѡY2:?:,Wd@rdH~H?rtt~ԏ(~-GEZM, j fPYGΒo>Oȑ!AU, j ?:H=Y5MΒo>éȗ_~uAgM\?tRiiiKhH-%cA-|G{k|A΂ZB< n,%ȣk|A΂ZB< n,%ȣ9s75jteJ >}m۶8p`AA+ծ];}6r; @`x.gXPK7G97tSSSY~~֯_YYY4(Q^^^z2/'@`]Βo#7)))-?Ï9p~*Zjn WTT+99yܹ¯~kt9KƂZB<,5jԸr劲f޽{}ݤѣG?]Βo#z|֬Y#7--W^ݿ?%nӬΝ;### @`xz`A-GfeeEDDrGþ([nqƛM5ӂ~y㛒e`5ӂ~y{zُE@0]Βo#k: j ?_< 0ƻo#k: j ?_< 0ƻo#k: j ?_< 0ƻo#k: j ?^~*&7^< 0Bd,%xĔߪ_ʧ&*fY2 ~+Lw111fQL 0Bd,%x}6oޜ6~ixx @`.gXPK7:!!!mڴ@KM6勂`a %cA-ǿhѢE˖-龉Ɨ@.]B @`Gd,%=,84hŋB @`Gd,%t'@k'x8fY2 ~I||<=ހ2 r~o駟iЭ[7d, j ?VE4xp 0Y2 ~{3ot9KƂZBUϝ;rٳgg8yL6W^w 64˗//((ǽ*kj `ztr~*?ڵ+--駟$bݺu|O}52^SS0Kd,%~tҀ ܗ^ziϞ=0\ `tr~3ُN2e X²e˖.]_cѥY2L?^xqɥ ec\SĻ, j f ;~իW+pMѥY24?)0{첲2"Dotr~3͏ӦMp?G͝;H:5.]%cA-cqq;Ï")ͻr 4k <+t9KƂZB˗gL^^ކ K\S]ѥY2?_sez/fpMtE.gXPK7sk|ͥKG\S]ѥY2?Κ5?{n \.^=xwMiUA޽{֭ӧOW^Z*#zؚbF1(ڣKd,%xzN:7nG۷/G,Xh Yf.]'8oРAhh(kX_599Fԭ . lרQWtO={2iij9ʒOGv.׎ȨbryF{|M:\wSN 62iJz[`{f͚Qd999l . #J}(sƌώ;]Jcq n;ߟ###i4cʹr(- BHHsڣKd,%8`C;u4uT<ر=zFYÇ'ICP?'LPVVFvJQ{1jD٭[7uk1cΝ;š2dș3g )SSSY1q}M<=x53]#.݉ 'O7߰;Zl\e\|eD?Lv(M'ظqcg+Q@ ~ZIg,Li.Gr˦M(M^~u, j ?8pOP?tVM֭[uR'Ok׮-h~nU${MZT^]͛#""X1laiv\G WWݾyF{|MOc9qw t뭷t uӧO=rr25QF+~}?ė_~ټys}J}v9PsKm۶-]V;w*$-JB/x?CM1kA{tr~F9vuwWx뭷vrKhf=o}ʝwsy,ekPu1h`5)Ml|].\2nv1Cp&5F~7KzjĉWˁ;1l04?233{'_}UI5Qhѻwo?ݷ啻\=t9KƂZBƏ ]G/\@SI6ng[_EٳG.F34&%%%ްaܦ%.w1h`5|e(KΧ*lѢ"U)Z^ӊˣЙ~4"Akf͚ݡ=t9KƂZBYWz7h wuرcI #Gj֬ٺukjժu^x*@p̙j=)%{ZZCBqCQتUٳggyn=h@/ovz56##CnS%r+Vn_r1h`5)쎌DuB+ jwĈAWf 1.,(z(i%l_.JxƍgDǎ>QhٵkWHЩSrywh.]ΒoVkVZ^'5UtMA`+t9KƂZBGW7//s:<| ]!k ]ѥY2?ҍU~~>?^qƈZy#G;ƞ={֮]_*͘xMA+t9KƂZBǏ/YBSx˗/J3Kd,%9~x!t_'=w.]ΒoXr]>bG/NpMKF.gXPK7O<H ~{"t9KƂZBG"77XgϒԴvgpMKd,%~${=&'PN:05 Kd,%~$?jgҤI^\S`$t9KƂZBG/xW=>0*oΜ9zo|k.]ΒoUG|ٯ:ZcPE͛7o̙+ 40+t9KƂZBUW^ݰa'_>˖Q]f隂?^SS0sKd,%[5 x@`xwCgA-G] x7t ~5 x@`xwCgA-G] x7t ~5 x@`xwCgA-G] x7t ~5 x@`xwCgA-G/SzU @`.gXPK7ё#o*Ǘi Yr~\tt<]n]LL Y , j ?^o߾͛7g3>~x(&Y2 ~NvvvHHH6mh& mڴinn._ S, j ?E-"##i&{B @`Gd,%tD#ۺu&Nxb>, j ?Eqq1I9@h 3, j ?ޤW^l 2oAt9KƂZB7f@a]Βoc%ZhA7%cA-J7<@`Ad,%訨˛5k?ܭ[7vEiʡ|W"I+Oy)0| :: ?:.ghe~ v?رA' .ܿQQ֭[/)2|5`d'we׭[gY  䇊h:ok׮gƍ>PI*Ud9?UFqhU^^7yoAGݘ1cF}%~ *C%<i% TFqh5xHSYfc ~ )O$ZP5H{NMD{Xoy桡c ~ܱc2_xQIg:tv QNƍ{ne*O^HRv)^ʕ+M68p=CH-Zի_NoANJA)saZj &&siii5&բx]H6HMDW!CPykXB)s؛.ILL$E* S]jALD/e:]Ƞ'3 ߂Ώfz͏?e˖o@w﮼ѦԂ LD(CztզMZ%Pu7CL>=J~}ܔ.DKP.صkWoALJzh߾}f||}Kgff¸'Od޽{s{7n\ZZJ9s1pپ~Yj*J/[,- )r!3~Gs4 &iΊk:S-OfBcTToPhbXL֨Q#n2M\r%%:t_>Φr膒:f031!7aQQ;NߨqHqNRi*iJ'x ) 07>ʲ7Y{~?gu]fjcv|ꫯOukԨAaܹѿۿ ׯxQ`VM Fwi3#}>J? 8? *Zt*j?.^qtog%N]ƍYt;铗װaÓ'Ozϯ糏U~q篏H cBSІQl"˖- 5t~x:<ÇH^tK.t*rVRRn:޽.>\btUW觥x=诏y'?T9.[NN]0`! Xn21FMh(oG4hDҩ7K>:$++K0q߾}>`ƍ̗K/I; tJNƍ"A:HԿ?#Ћmۦm9wQl1bĝwI9} ޟ7D6&cڄ6bq}g1--}3vpoDZdbPІQl"~5D?Z8k׎wm?iF0Xn21FMh(or )&cڄ6b}t3 ^M& m-BXn$ҿƱdbPІQl"F|xH_r1BmBF}>W:$$$nA"+!&cڄ6b>yȋc6 ؾvT'Ns-"ŋo6gх?TbPІQlGC>^}o&PZZZz?ty۶mqqqTbPІQlGCϯ7xc9ڄ6b>/ <h1#&a7#}3>Mh(o#!^HP^k1x[77Kk(2# N}EZ ޖ }͒L%5nX{εQK.MIIk/ؾтe\׮]k|"S1 4Ϲ>\Uno#_{NHH/5k_Tb0>annnTTT&M9Q蘘|yW?V~嚚*#X5J&lذ!?I詾:w,䟩b>oկ_-B ☔ԩS'yb*1Fizly'L7 xܽ+k.]L-Pd*1FųMG76^S }䧟~k^^FL-Pd*1F޷o_?5u[lGF-rرL%'}fkؾbjr Ec„ 6QޙL7!ޥc„]-o#OK\C0aqq1RQb>Ξ=|W^ye/{^ff|SJOE~Wy7TbPІQlGZn',]Tb6mzWc*1FMh(o#g…'O>t萼!8?MC:sTbPІQlG;_+oϞ=??f*1FMh(o#؁bky]=:n84#&a7nƍs̑,؋9f#'ڦcڄ6b>Bpotr'/Vpݻ_y;TbPІQlG_^^ٳgٳG&Uf*1FMh(o#3225 )--2e}TbPІQlG'nꫯqS1BmBF}C!{by6o޼xb[c*1FMh(o#ɫ/~TbPІQlGɓ'˫}6mtUW[שSGAjժ[mW>Z,'Q`>BGKKKMzXGA}s=jժSΰaô.ٳ'==f͚59sϢ͘1~5jhӦ͖-[<o[ntt+Ğ?s=l4{61p֬Y]t-GB)W^+.. lrرb]w5`ھw:x,ZzR<7A/QF8q۶m['Ji6 evg r1Qo߾EEEٳ#nfyKHHcc7l:tem۶M|駟&$$hw!zMot<幘5k\{m߾]`@ѪW݁+Wʼn݌Q~X` } 2G:)ri__kݻh^^[ꫯi٤uVZ]D_ے񹏞 cݱGAd#?~3kmٲe'O׭wHǏ}l}!_~Z,%%%tyw@gJc}> lG"#}MGI{~;(}C!Q6l(oGAd῿ްaƍ3{1OFo#}\bE\\_ Jy0B } Gy/_.tL% m } :ro!/Ppqئcڄ6b>Bp?ɓ>|(*3#&a7kݺu2dee8qB&Uf*1FMh(o#]FFepҥK.\({Jj0AwԩѣG[ ,ZS1BmBF}CD()V+뿔#]T46b>MN>=a,fƍNNNN^^>1վTcYF}CVƏ?k,^dP̙3'##t9PYYYYrrH-146%%S08zG}55t“7=|ݿxHÇ˻gYF}C 1BΒ%KH@qsyWԞ:F}C 1BQbb"06nܘ)űC;wwR0`FIβUׯb=u6b>bjᅢ:lqlѢHVAa7S /DuUq3#=u6b>bj(. IKKAa7S /t%&&#Aa7S /twizgFP{l }0 ]%%%HzgFP{l }0 i?R m }0 i?R m }0 i?R m }0 i?R m }0 i?R m }0 i?R m }0 i?R m }-؟Zcڄ6b>Oeq-S1BmBF}C| 6 ؾ~SY\TbPІQlG,e*1FMh(o#@ T2#&a7 }*kJj0H>ŵL% m }`SY\TbPІQlGF>ŵL% m }SY\TbPІQlG^0>ŵL% m }6l\Bǃ<㽣=i%_L% m }ڵkԩS-իWӦM7oNG6m@ӨpY6 ؾ?O>9={cbbbNRRR-&L?ƤQ46l6#&a7 6P [w/^222;XA%tJj0pɓ_u9sQ]vmK3h_4S1BmBF}C.{l٢}&޽{?34fо icڄ6b>\@A$cGo-[!4VeH3#&a7&MC/)))4D۟ icڄ6b>\ ?6jH.bewq RІQlG ?xZ4h~hڄ6b>\ rʤ$9z;vMh(o#޿8tPgٷr˽ދ4NmBF}C.3Ν3T@211W^gSІQlGt߷v1--mĈfMh(o#%=&cڄ6b>\Xn21FMh(o#@%cڄ6b>3?FmBF}C|~a*>Pڄ6b>@%cڄ6b>@%cڄ6b>@%cڄ6b>@%cڄ6b>@%cڄ6b>@%cڄ6b>@%cڄ6b>@%cڄ6b>@%cڄ6b>@%cڄ6b>@%cڄ6b>@%cڄ6b>@%cڄ6b>@%cڄ6b>@%cڄ6b>@%cڄ6b>@%cڄ6b>@%cڄ6b>@%cڄ6b>@%cڄ6b>@%cڄ6b>R/j&a7S Ԟ:F}C 1@Oa7S Ԟ:F}C 1@Oa7S Ԟ:F}C 1@Oa7S Ԟ:F}C|+..)-/ֹXxٱÇwʪԙ*(o#_K,&𨨨|yWOiLb>HG=7G+C;ww_ԙ*(o#@ #G%G*׏*>uJ15HII ޢE :2T3U:QlGF׮]"0`jOiLb>0[ $-- P^T4F}CxLSgtSؾ[zeCS{= }0ʆz05na aO`j7+ž(o#W6=׃QlGp Sl{jSؾK>•iLb>c,WJ15SJ \*(o#8RT4F}CIUWJ15⧔@2U:QlGpX?•iLb>ê)%LNcj7WO)petSؾ༪|J +SӘ }WPWJ15)%LNcj7\a.,,HetSؾg}>=z㑯dtSؾ0CQHLNcj7D`tSؾ8"1U:QlGp8 H$*(o#8 p$2™*(o#H,++3SӘ }[`0K.߿JJ *(o#l,))_8SӘ }QN$0vEavڥ"aT4F}CU w^qXt1,*(o#aԨQ۷og҇~e"22(o#ɓȪ?3wiݺuRRRLL -ƍBRU)qÆ ?81!!A| #.DzQSVV&'4hD!l"Q'x #ڌ'`53>B3Hq(G!<N$jG82‡w"G Š>#TF$rΝ#TaIl}d W7+'S"o#W6O^ElGp SlLؾ^>z}C-L|2*b>[zedU }HLzeϔDNN_ٴվ0}0QVV%R{e榤еv0}𑞞/)^ٱÇw0}dzAH@qϗw0}(>^:tY ȑ#5Mg*׏"A!ac-@̀2M׮]E wf*G7]wf*GC;3PE#!. ޙ*B! ޽^مfnJKK}>`. Š뢢"$}𡏣DBU&DBQ@"A !/p$ڌQ0Ȳ|0C$C!`0K.߿JJ QȒQQQ #(x't"0ZvRSSG(G!U%H޽{cGРzF}vy&}Z_|}C'ODVx̘1N֭bbbh17nǏ6J"E=?nذ8&$$B",o#G-qԔɨ  ` 7le6GO<~La>݌'`53` 7`$f`78#؀H }'y'q۰}CaD"`'o#8O$rΝ#؉@Wvb>[U }@flGp l }@flGp l }@flGp l }HL#L7s>vV ؾd-ZsssSRRK7/);|pyW}CIK,,H@qϗw7(>HqСCΝ9r$βUׯE;7VRRB"-ZI3`>v*8`3a>Ļ4$-- ̀mؾ G3vb>+wi ؉x<Ρ[>v;O4lGGCtRyӓA"# 71yG7@"# 71yʐȈ }\$2r}C#qlHA9Hd$`>Fh ؾqŊm۶mҤIӦM|mܱcu]mϞ=iii-F<UR`9{>jiG;caa!Uرc!BAA=.y/q#h ocHdqqqnn( 9sؚ'O>ӟJJJWHNN]Tq",OØ1c Sq~M;`t҅&xjݺub;=^q-Eؾ%zѻw:MNNի?ܚ#Fy[lg(3VJLLlٲ%5G}|ƍӝKb_3raG}C#KYr"lLj4aGƱ}71y,MdXrsG}C#q,G- }\$DZ}71yB"Eؾ΃D^*q,G- }$c9ho#\@]4iˎ5/o=Pc9ho#ּqx, }7ϕ%ؾXṲ7B_~aÆ7|JKJJؼyMСCbF5k֌Xmp>Zn_󥥥N>M333G!w}ٲerNN%.U^ĉbv!`>[Hk~Сook׮7٫Ufh;uԣGӧ' W%ؾҚMͥ-txW9sF@ܼy[oխ[{L:젏`>[x.]ԯ_ѢEږ !]>{֭[Z{Ghwڵ^ly' }7{nQFqwfX%ؾ֭[>v >Z{E۴iwf@- }wihGK}CĻ4xg lG_ӧ?Ok~ҤIᅾ+,_ٳ0lG7ڳgOff& 6x"ҥKMkݺuKY>Z.gΜe[o?~\^RvLBg 뀃>Z"Gyv-/#k/6WlG8>GWDիWϙ3G~M>Z tZ=j(1}}7Mk.y@7nܹs/%ؾΣ2[Zȓ+>|A- }t^ff&ޭaĉlG/J R_%^GK}C_F/C`͓_%^GK}C6m4y@d*>Z0>nڴZjV3@_uUta]w|&NXv5k[N܄;YE 7g>چ谬,y}bŊ.]:vjjΜ9 +YbvAr!?}~;YE6ߜO_%^GK}CfK,Ν[RRRVVJ6;C>ʛ8:?fd|s>a>:H[hC(V̙3֭Mb#G\N:Q}GSXXF:j3k}x"=zF͘1CQ(.̚5+66o{jժEwfذaըQ^lt[]vnBg֭_~xo\l's]^yzFEEM:U3CN>چ0[w^i;:>jԨ'N۶m+8o߾EEEٳ#>ϧk@R|9~hwA3wЁCOho>z*~jU'Vڵұvh#֮]{7'>3G`L^}tA ڿ'sB~z嗔˗KQ홙!F2*lG%Kԩ+k߿?UΝ;/^,}{/UVQbo榛n{siiiucƌM>>h߾}y<'Ǐsx~3?}27pÓO>)G!l=4=|a#H6lp9yi;$HdUؾ!؆0$mؾ7 $WlG-_|ڵHO?}' h oΜ9c-ocWlGM6Bs_|Q~}>Z'NLȜ ҁpGGBh o裻lܸq̘10_L8K! lG9w܂ 222X@^O~7x_4}7ѽ>ʚh˛}CM'=GGK}C-se o#ּqx, }7ϕ%ؾXṲ7k8[`lGp y\Y)..)yZC- }i_Z)++kҤHm-%%+T>ZNѣGÆ E"Ś_re\\]A}7մiSJ$ycjC@- }'?>66UV)tfݹsgy? }76h ZzwNNPh o#8lϞ=% }7צMt3ch o#8NE)xglGpx^O!lGp. ޙ1}71ZB.!D`>!&}71 vr*%ؾaq#D- } SO$h oc@es"GK}CA8ر/墎;{Gc:y/g:u~mY衇JJJٓ&_}ѪUZjմiSyٙHlǐ uXo*QƉ'+Y.|7ֱ-%ؾ-p=~iӦ~.;_~bmۺwN HMM?~6V?nWC[og6ݻ4iңG?qO= 51*33sȐ!bݻwtMŕygOEΚ7o.]<'=zt˖-8[?+W&&&}9rt?ϟ߬Y3o$}71qx_4HlwڴipBj6C۶mN*.8p#%f޼yt6v>gϞM Wڇ~(v߿?]ؿ7|a / 6L_*vCQFm߾]^W䡇oF|YXXxWj;#ūzeeetyÆ b#ZjZS+YP~}}3f̠{W>K7$ɯj5׭[Wl~ƍIt'KΝ;#SQzlF>&-3Xϻ5؂kVlܲey'&+;M"Y_t\٭[J*xg}>3PV-qE'?r71ǧzϧâDؾ}{ǏYMTN']t:й֭ fG)؁bcǎtb'\[GϞkmxRQF1]0av?Ŝ,q,B71N?$G}DeGf͚&hd-##C_nmܸqZ5VZHǡ#F6۷o_z;kmIgϦ@qLMh_!Hc*UȜ@c> u4i5hի򕁘cy2<ؾa",YPP@tvP4))c7o,_B˫9偠 } aȐ*dNy }C i?8W!sAnH;U%UȜ@c>!$Ucy2<ؾ DZ SzlǰD%q,B71Q"322Z&)?)gNy }C-"p+?d偠 }57"׼CVzlGp\Yy }C-k_6l\\\|7td͛7oڴi޽:$jԨQfh;q9)=o#~͗8}4]1b޽{e˖999HPVVVz'NSΜ@c>[Hk~Сook.xUfh;uԣGӧ'q9)=o#Srss)|b '^qgΜ&P"7o[ouv+)=o#ҥK-ZmIOOȠgnݺ.i֭KHHw9l@c>[x?ږǏ4(%%E|PÇk.556nzŊp7l@c>[xCfeeIÉC6Hy }C-k;v/v 7ʙSzlGpLqqKiK׆%)=o#8&''G85OZp9偠 }ǔ%%%ioB^5LNٳg\\HXƍ-Çw Gc``>VZEK=119]8FEE˻#}ıv0}Ca"*t޽sNax#X;ؾ &2 >`vvS2?o#8 EȱqƑsh} lGp9G٢>Fv0}CytH;vQgf }Wgvh| lGpqig }c``>#GL:uʔ)CAFFy|SJOy7Iz lpvϚ5􄔥Kʛ\`Μ9tػ`sc } N2KNrnji&aZh ޙ:ocؿѣ x] Ah}RKKKU 6Zz;#oRΜ@c> & /^pBr71]?st'N$?3<ؾ!o'O)8H|ʙSzlv7xC^(3go/ʙSzl6o޼=ǰ|r[r71M2E^_~W^U(gNy }CCɓ>6mꪫԩScH`>}0UVMj`>!£~C0߄F>ochS}ZjթSgذaZٓ^fF͜9gf̘Q~5jif˖-7A׭[7::zŊbϟGb6=8k֬.]Ж#G I1Kr\sE# W_}OS&ݷ[+믗vy[<>7Ub;ؾlG:3~Hg4۲eNoch3ǣ:eee={~2غu1cƈ{C>;zeΝb;v݊i&JZZl{5h /^x׈nTt:w\җ>Ps~;ؾ(X}p O>$EAk׮nݺըQ#99w͚5+Ovȑ#i^z;Owl޿o:nSiӦO<ҹڵk_ڵkfeeyK}&裏= } lϟ߰aCy+>och6lظq#]ؾ}{-yy00T}CCbŊqqqtF|4N*=}P } m^AA@Qŋo/ʙSzlж9s 5cƌ_UV9偠 } y甖N2E&9偠 } y~e ={={oʙSzlǐw9wY?󫯾*SΜ@c>7⧐;v's ʙSzl0AuV,؅ /ߘʤ̱5%ؾ#;;{ѢE…j:r4R[DΚ/,,e$ ߙ" *o#EDۋ>;3BD=W }5?o<ǶmۚzgF*xؾ!}gF*HؾŻ4fߙ" o#Ey.wfH{nk^!+=o#Ey凬<ؾ|omIDAT@c>[DW~Ank^!+=o#Ey凬<ؾ@c>c0Х9偠 }Xa,K9sAXBr7dɇ.)=o#8ɒc ]ʙSzlGpX?%t)gNy }CaU0Х9偠 }UXBr7Wc ]ʙSzlGp^?%t)gNy }Ca,K9sAPc ]ʙSzlGp7l0y{wάiͷoߞ.NŽr7v?U5k֭[9zj/]-*G^3<ؾ`seffvuŊu:t-C{uB9sA 2;}|ڇ5ʙSzlGRi|EXPΜ@c>}֮]ĶNoܰaã>ڲe˄ngٴi~ڟF"3<ؾ`G3G:66mڴm6##CIhl9偠 }qG|011QG!))鮻Dw4~KPΜ@c>M&Ok_~>(Ae'4fо ʙSzlG=ܳe.]Qy7l<Í5җYfmڴ Ly }C>>5k мysc֭RzlG~r7l9偠 }k3<ؾ ϟ7:$$$F(=o#c3<ؾQΜ@c>8F9sA)=o#c3<ؾQΜ@c>8F9sA)=o#c3<ؾQΜ@c>8F9sA)=o#c3<ؾQΜ@c>8F9sA)=o#c3<ؾQΜ@c>8F9sA)=o#c3<ؾ",<ؾXṲ7k8[`lGp y\Yn5o+K}C-se o#ּqx, }뿔ּt-衏`>crrr/kӵڗ A- }ǔ5iDKW\B^*C- }'7lP$RK>\tGK}CI͋EJ$ycLLLttt~~+蠏`>V5Oql޼yΝ坠2lGpؐ!Chjժ^^zegg;Ae%ؾBQFwfX%ؾۋ>>ZΛ7oc۶mΌ%ؾ ΌA%ؾ ]3ch o#¶mhW/%ؾ༓'O>3f㑯/%ؾ0CQ>D>ZNQ@"@- }x4}7/h o#8 p$2lG8 fYVV ̀>Z2G`"7l0h w$h o#lƙ3g6k,**J;}7lG;tؿGGG7j(rlGCU(DƸz6miq,G- };5jrLEho#ءǏEEE?<?N:cbbD&ZjEv`>MHG֭E%{!`7죖H8'5X2qԣɇ~8ߨ`>݌'`5glGpD#YlGpFD"`o#8_"G7Hl }8ؾ<;v `'o#%^z#؉n6c>[`3o#6c>[`3o#6c>[`3o#6c>c?D& 9[}i;]}  lGpLYYY&MDj}\reJJ ]{iW `>ӵύ}\till] }'͛7/**J|n / `5o#8,>>^|@+ؼyΝ;;76d*cVUիWvv@}Ca$!!!99̀=ؾۋ>̀mؾy>m̀mؾ G3vb>+wi ؉%GӨtRy->F49ؾ ql ;v_֩SGwhvϞ=iiiaN8Q\^jUV6mx;^DFoc$ GO1>k֬)(( yƍ+Wؾ'p=^qҤIM4d޽wңGYF!Cv}M7=zt˖-#m۶uޝBFǏiҋ{-/[Zn ?~fʹ/}B"71qcaaa8 ]vĖٳgS"/|͔^6lVF͢۶m;uTmG[>okF0 },Fھ}܀ʼx7jkuK/\SsjժrQjm= xGt ݅ j[۹sk"kD|鳏}fϯo诺*999'ĝq,G- }Dl"lrw4n#JOif6mh#z_|mܷo߃>(?)4QV֯__rzzzrr2gddz}y=PÆ 3~Hϵq,G- }Pl"m&MX>ZJdAAܑ#Gq,G- }hN%R k ƍw2DZ}71҅V"-$X>ZHB%EؾpBq,G- }Er"C+EؾpId&2X>ZP"kBk^d kn%ؾXṲ7k8[`lGp y\Yn_/ao_%%%l޼yӦM{}!OVVVF5kFi6OB- }Я?O˙#Fۻwl2q9''Y^{O8!kh o#oov-Zj-.jV?SN=z>}z^^~p>Zn!ߔ\ BljW\qř3g ͛7[ݺu{h o#ҥK-ZmIOOȠgnݺ.i֭KHHW(`>[x?ږǏ4(%%y_:n׮]jj*mlݺ+.M+P }^Cʒ6B }C1/k;v/vߡ`>cYv~vV$%ؾ2:HyBKBe%ؾ{7!!A$Ry?|pyWA- }'M2%***55Hk۠A|yWA- }'/հO>r R;u$`>ZOZ۰a(wfX%ؾTn >ZΛ2e㭷ފwf@- }wihGK}CĻ4xg lG7:{_$#G5M"|-ZH|hlGw)++>}i֯_SNy"ҥKMaݼy3&O?˯h o裋|SL)**ٳgϿh o[dgg z 2!1c?h o+ĪUEرc/D`>:oƍs̑ DJ$E9F- }tعshI "՞={^}UU}7a~UlTIRh o222tʔ) 2lG'߿?yo3f%ؾNz o͋/_.:%ؾN(yex<+"\tGK}C4ydyeiӦjժ[ͨ .]wuڀ$#}\bE.]hM֬YUVsQXՒ` wn 7sա6`>:%Knᆹs疔|ᇫ>% )U>ڀ$-Z?!mz̙u֍Lȑ#SNڵ}ѣG텅<mPʫ6wߍ-K OFQQQSN-}vx'z)wFiwCoZ@/š֬YC޺u_傂oqٲe_wW]u}g{۷{U݁aÆj;x?!S>ڀ}LO޽{iQN8Am۶>p}>|gϞ#FT8x -௿Z 9-ܿ۳ge]W_eGvI˛wh7zΓ'I!k7...֭O?-6z%; .>wW^[=[l9vXsG}CtDJzq8p.Ե^8^;>+W '/'ڵK@+x 鏭y睔駟R<~'믏@jڴ)=cǎy%; .>34!}wm& v~B|Nho裓=t'm)uIc-[xGFF:5ktfwqӡCJH'~;]_|Qo>}! ϧkE=~;= O;#M~kF`G}C?j+\s mV:ns1ڵkoO>D$33366V|I璵kצԬYSQo>$oGv  ~?@sQ|r-jhCGʟvh3lG'}\dI:u}W_}%_"ܹsbw}z.--eRo馛rssŞ:iɓj||vvI|xߨ7>={ЋP;}Ѿ}҅vǏsMӡx>ٓ?nݺ;L>S>ڀ$q^Foi \CRhϤiӦ픏￟fCGyDfRԭ[w…KڶmKɨQF˖-iWo!wgoԛwO}hn'J;hOϩCm }t>i< 6`>:)O?[ROFm }tRhȑ#tؤIߗd o= Oao裓͛{˗/_.:%ؾN:|Ff? }7aǏ?y> QJe%ؾ[vB$:qB }7y&L x //%ؾΣU1zhױ ^wޑ_`>)8dV_~`>ũSƏEFcnڴI~M>Z.֭JΚ5  ̙3a„ ;wN~)>ZFGə:uHBk^8p@>Zn5o+K}C-se o#ּqx, }7ϕ%ؾXṲ7k8[`lGpLqqKiKׂh o#8&'''//ORi;]} lGpLYYYJJHmN^*C- }'} "bÇ˻h o#8)+++***55Hk۠A|yWA- }'9s&&&w޷r ycÆ ;u$`>KW☝-`>þ;QF!)) ̰GK}Cy"m۶;3F`>󲲲DoV3ch o#8OKS⇏u h o#x>Z&<ϤI_z%БI%ؾဖs=w!BҥKM!رccƌؒHlǐ 8>Z<ؓHlA]wQwTDђ@p΂`F,P /ZЖ&*r"B ܂-QArxZ9`, z y~U56ٙ;sfL6dCH1(ؾnE>7ѕUk"Ǡ`>qtK$lG"RMD&^84^kQg9XBIYl߾nYe˖=cP}Cgڵ+::z߾}t{ҤI߷o_mڴ2e~'m۶N:t0vXmqCVGMkCw Ll5}#GG*mV(5k&nz&bǏرcllvf'}?G@z3Ν;kZ*11QOp>7(\e+__6L,tvZjVخ]Κ5K>y:HȴW^|򉸽dJ` RXB x'7|s=9sn+G:uT͚5驊t&b,|r/}↞Qpϖ-[ 7R#*c }tϿU8Zvo.[nT`-F%Rx</ғQ6 ֣4iD,>|oIϪq[i… =zݻ36o,7vmb-57l52#JGіEg^G75D#mH4s=c/m ֭+ٳGԍNɣԪU.>cnn.+)I6EkONN~g6wkdcU^= 8Oe ؾnbH}~7bĈ<:9x[<뒒:uڵnO2E}E7&N-LII0aE[Q;v hzJ[D~Ooh{-mⳏ^+nh|D:5[\@q,Wk`}C]$+|@gXΝ .޽4O>-Za,XsOJ_XgG:t}'ޟ1I Mq,Wk`}C$n7|^{M^j LX>1b>R%tٳ:?I(U-jS,o[_"C߅ 'OYS8Oe ؾ.D:8Oe ؾD:8Oe ؾD:8Oe ؾtı\}*c } )Sf4EꫯZcZTN9/1b>S` *Ae ؾqP,o#8漠rTNs=z]TTt=\|m!Ck׮u/,,c2227oHi[m?n>1b>S|ii~K.ӧ}ݸqEf͚eeebvåTڧ2XlGp iΏ5w޹zjLLÇ+>FoH@˯_޳g~JTN!󟔿իWS:OU֕+WJW_}{ĈjWQi`}C)s>%%I&֭Ӗ'==jH{ oŀ;vixTNaYYY?dذa ڵ?邚9s[nmڴ;wi.d<F*c }0QFeddH Þ87B?O>MIII}nHDPi`}C6EEEҜF7lqVQTڧ2XlGiٲHm gp]ؾ`>}{"b/[,&&f̘1Ј᳏8׮lGӺuhHAqjܸq^^<4b#ε7lK<ű{Hⳏ8׮lGYZZQAeffʃ">\;ؾ`b"4#j_qllG_JJSO=E>\;ؾ`?qHW&}Ĺvp}Cʑ<MXsb>#+G\-s}Ĺv}C]\2,7:u*W_}U^<>&Xsa>\j͢EklذA^`K.^ڵk`Gk 71|\t)==<:Eɓw-U.h ޙ&N<9a„SNBbٲeT r1b>/RKKKY !D%(׼*S,oc8j9: ֮]+<Ri`}C]/77stٳg/R Tڧ2XlGכ2eʅ i 69sT*Si`}Cٳo}ԩSyY7|I&kҥ˞={77l0:::;;[|8=?ΝXBVVh͕h;M̡ }t*G$B٭[7Ć#G,..š}4gΜIMMMKK2Kڏ磑v"mkܿ3>7caaM7ݴ~q>Ӗ[}b*vwΝ~ƢzzZmIm޽7;|,i?>ۡq3>7tHgvteM{۸q /1OZSO>ĸ7&t ݻwkM6tg;O lGwS9Ǔ}gs'N7tPCLJJ2СC!77xȐ!<{D"Iҽ{wތg5l0֯_x׈T[-[FO`' Gm}牙C]nlOetG0`஻z) bÇ{]v-ZSN /@6m:c c=_B15jwMg˟{9kۜEկ_?&&&##>>q^O lGw3cZ2ؾ>t۷;v<\x}to裻_gggw9l0Gk`> ꫯ֯_/TTڧ2XlGwo.]*OPռy._,TTڧ2XlGw~ԩS )))y)*S,oZjΝ4dff=zT~>1b>իW'L OSÑ#G3#Oe ؾ ??߅ݹs*+%Oe ؾa.l"OYIF>D>{>LP=Jg>C"+++77Wo-] lǰv?g^{<-Zrrrh9i cP}C M9sdddرC<_~y_cǎɯA')ڷ}1cC+A } OMf{М9uY!pɂ ڴiC '>7"իW4iܶm[:={ncDlGpO=6)!U}C)"|۷OЕu}3#D o#8|E3#X7s~:uΌclGp y. wf`>S`Ηx*(ؾ7ޥ1ygF o#8漠rTN9/1b>S` *Ae ؾqP,o#8漠rTN9/1b>S` *Ae ؾ`*|J$Pi`}C6UܕH>1b>m+@}*c };+@}*c };+@}*c };+@}*c }+@}*c }+@}*c }J$Pi`}C~ꟻ Tڧ2XlGD7AsW"JT~z~~̙3إK<ݦ%D7쑓 .ܻwٳgmFmZ"Vy>1b>B]vmzΖZC#i.Oe ؾR#G1ҥK:C#i|D%R}*c }AJzh$K}*c }ɡK/ڵuףG4Z@i^JT!rHƍKLLlժU\֭[?IIIsLڊwUڧ2XlGG}Td-[eԩ?Nmi%J}*c }9sEWyroڵkϞ=ڴ-ATڧ2XlG{lϞ=݇~XbeoƎmi0>1b>Bt]-ZXsDO17BUV?vWΡA֭imBH_/\P.be]v7B=(G*c }1O>r_y*c }W\ٻwo_ ۶m;|pky`}C!{X*S,o#~#K}*c }P7>1b>=?C 3jS,o#F=zB7YTڧ2XlGgQi`}CE}*c }p7YTڧ2XlGgQi`}CE}*c }p7YTڧ2XlGgQi`}CE}*c }p7YTڧ2XlGgQi`}CE}*c }p7YTڧ2XlGgQi`}CE}*c }p7YTڧ2XlGpFp|h TN9*(ؾpN9*(ؾpN9*(ؾpN9*(ؾ`"]iKkA} o#&+++77WjwA>7lxDjs>''ڟBecP}CNs~cƌlGӂ ڴiC9/'1(ؾ`W6i$99m۶4)={AecP}CfO=6)Ǡ`>'Hʺyxg>7׾}{Ǥ$3} o#o:u;3*Ǡ`>Ļ44Ό"1(ؾ]3} o#8B~~>SN+1(ؾ`?;nܸ+VL8n˫} o#Lın;wT>7줏D@1T>7/i} o#kG4>7BͫG!Dz 7B[8 D ?jժq-Z8A }PZ y)qزeK:aL4i$X> 7BxyZbE#A>8o#Bii/h?~C{921pmF#7BJ"E?\{"㽐H7Bj4Qsiq:}6"o#T4N>lG5D*Q96PIdq7aHBHBv2&qa>D!ؾ`? JlGpJdFG%o#8~7}c>SblGp BN>B}C)G1o#8! }HL#~MT7o#&++Kl}V Pؾ`ǣZsrrh9i(@5`>}ncccnj#6o#iQQQmڴDRE };]zI&Z={7lSOQبBJJJff<}Cfe$teݼys3 }o^1)) @Ȱ}C~ ,}ԩޙa>Ļ4G3 }G%ocDzx嗩ӧOW| N`>F. N>}6l /:,^$2}C#aqt$2B}C#q q$2}C#q ؾċ8 clHE WlLjEA"71y|N:m۶e˖}/>ѣGwUĜlݺ֭[ӓY~$2}CÜW-5@ݲeˆ ,|'NԮ]L|j}vYmW aμjq$111 i;v[?~oӦMVGۗCLŮxРA=IOtuH}=k֬Y31F#GB}Z*11Q 'lpF3gV$|ͭ*/T oW^|򉸽d ݵkYf'OIII3g5죿Gќ:uf͚qW{DJ3=zzzkϿt-߿ } g.\x饗 )^Io6q{߾} 65jS3ҦVrKii~s3e˖i kMQѣG޽g̘yf8wMI7^v?:h71)&srrR4h`Z輬P?}Wßx rmI=::?~rr3<#H}.MYPP@OU^j8}CßJ".\x:tHݶmƍ}Е;n?~ǎb<`a?/~ )V }͙3g_|q7:u*]YK5Ϣ,H#ű}TtҎ;҅']w׮]xĉ~ZC6O>-Z7OKKNw`>{ ռK. 4C~~s{]M`X>Z }*r2ocA"q,G-c>F$2 Nc9h71 r2oc$B"YΏc9h71B!&\r2ocB"}rKGؾ*>hˋ@q,G-c>S` G"o#8f{p,b>S` G"o#8f{p,b>Sg9sF-ns=/_ϟ2dHvZnݿB1&##y扉Gؾ^ZZҥKt{iiiby߾}7n(ngeeQ"YfYYX{ElGp i5w޹zjLLÇ+>NFoH@˯_޳g~7"o#84RV^MK}өt߻w/ݠoV رcG\\6>NaYYY?dذa ڵ?邚9s[nmڴ;wiax lGp l5jTFF4#a>Sgӧ|0)) JGؾ`"]iKk}8~rZGؾ`ӢE -l_reBBi(>Z };۷iӦ"b/X &&f̘1P0@-b>6mDyHAqjܸq^^< Gؾ`x "]hl8&W/ElGi7iҤQ'|233S}Cf";wnٲ%ޙQ>Z }G׿5ޙQ>Z }mڴ)**f{=Ό:"o#8B||Z }t%K9< N:Eџj h7}-[ܣtҤI/^_Z Gؾ.@h}:}vy}+W_*A-b>:ڜ9sG/JW h7fϞ--5vQ4*{6mZԩsYym;vq^5W_]Uw>RU~}h|NII)JԩҥKCaa! ؽ{B +Aq[zYWJGؾǿ/wuײeΟ?x>_׊:i2 )--9!`9(n>:7Tؾ}: FGG XNÇoР]>ΝO:SOB: j{kMq֮]nk^hɉ'{zG-j(vp˜:XTΝK_***j֬Y{#S熛oyĉ^v0&M|mzhI_1yc }to裣} tqi9XZ>nܸ2k׮b!C t3gΤ*|59O]h֬_}c&+=#.**:zhǎ'M$9rdqqT"$y?U =M7$ EϜ5<+aÆ wy; 5>UKt0)ŗ_~Y~|nb}to裣}ܻw/5B^zcJ:7>z+[oI }Ni8ѣ/:{?ԯGʖv}T}:cbbwK˩tuT٭<:Wꡏ }t4>/iРG_|!O>aʔn:thb/ˁR8JKKn*{vw^ZHKMMtΝś!USe5k/\0o޼Mz+WU>}[5>ǣG|ѮAh*}$6mtUKmZl)M5jFk?Nx'hgV 6\v+H,0nݺwu?_RRbSj׮]k׮ݱcǿ{vvzzWa'`H{9:tNblGGS# lGG3@/to裣S%~ `>:ںunVPPb "o裣]|a/,,_*A-b>:݌3{]ElG;rȻ+O2pիW˯qU}C].}hpɓ'_vM~ }W\y嗍 ew_] Gؾp & ESiҥj h75,ȑ#_d }&t=gΜŋm8s̬Y>3 "o;vlƌsŏ;ŋwIx0HGؾnuu{#\l 6m-GؾN(1ؾN(1ؾN(1ؾ)**ߕfGؾ`\~rZGؾ`Ӽys-l_bEBBB>Z };ׯiӦ"b111cƌh7씝M<>>٨_GEE5n8//O ElG]bGGGӟ*$''˃"o#l„ 4Q'̔/ElGYYY?8vڵe˖xgFh7׷o_g}̨C-b>h?xgFh7A?wf>Z }G}C]dʔ)tMz,GؾnEq| s 6ȋ\^{9ElGW 8$}>[Tw"Gؾ.8K&}&U_"Gؾ8W5%}UG"Gؾ.`ǃ|moXxѣGw.Vqʴ} 4hHF!~ȑlݺSN[_~} tH"oәkAUa/>V~۷Ѝ5kִlٲҸʂH"o裣kkK?㏷iӦUVUM^{Yfb?#4..>*9f׮]ۓ&Mx +~|]ٿq޹sgmVJLLD}Cp}MW9Oz'K,h{N:Uf͓'OtX⤤tqݧ{K.k׮i_`׮]g͚%ngos˩/,h x+j׮G?OM4ߩ[l/)XD-b>:׸q8 ϭʌ8i6֨QCkh Mȅ zѻw3fl޼Yxsω*43tP7qBv-jmo7|C)IE_Xэnt[N1n7l&BYYYƍsnFkcXlGYDd>RzjժEgaF:?~rr3<#H}d;vH[; 4ӹ^ץx_A71>Gziw͝;wnĉ^G7Ѽ\"UHRRR&L NM;voBΜ9sw{WԩSZJO1FG\>-묬,A93~]`R&… w.*CاO-ZCrd֭[5kFgiii"gƯKJފow}?!P˹74\^$rk }t /ZUc9W@b>t*DZ+Z`}C]ƋD8s4_ ,oxHrk }t%/xX|-ؾnE",(q, hXlGVr7vyk_$`3/Z`}C)0ۍ̏Z`}C)0ۍ̏Z`}C)0ۍ̏Z`}C)}Μ9G˗/2]v[߿aaѼyDZNjq;7B?KKK]tnO>=--M,۷ƍ,J$x<5k,++˵ak }fQy睫W>|5j!-~zϞ=owv4_ ,o#84RV^MKH1?󘘘1cC#T@q7?1**izl86n8::://O>;ؾ`Mm۶u4)He#θ6o~CoT!55533S}w}CfNe${o-p1g }uM;yu>;Xؾ`?fS<|g }GkFNc9θ q(Gq71<}9sf+B^W8>L|$Nrq71x<fھ}U6l /rsΘ1رck<&}ulǰflQRRxb:֙QKwf`>k׮g0T#G?:~=I˹b> -[ rܹI&=4_ ,oc8[ty)L5jԐ>K}B]nRy } &ۡʶGW`>[xĉ{^z 4=zօGӧN:͛7?ϢM4]v.]4aÆbc'F[&6\paLLLJJ -9{Yկ_g=w-:/GkGcܡo'f}to裻U<(;v4iXWǏѣhdݺuג!CPACƍWVVFvUGy'ionݺ&69rdqq(sРAO>sLjjjZZo_~=i[~9ؾh o}Q\\bիE۶mOyo$ɓt{uہ?ظ7!Sܥs5kjO`ͱbJ|>z>53>7#]S;w}.7[Ӻn6Mzn{iw@wy4cI9FĶ9ؾhG:S?+kƍ/\@1yҚ"?~'ƽIύ6{nm?xyi&~Ҷ>C]n*}7>쏂5` ]wSÇݻv-Z]N;^x4mtƌ<{$ {3yZb:jԨロ.gϞ-?stUK׶999-_~LLLFF Hw}ѫ| = ؾf*[j}'/53n }t /n8p}cǎGk`>۬YYUٱtE|w6.*+}CmŊ_S` [nCѬY3J$ݠ8FEE5nܘ.䥑8b}C)05-ZlTo߾ 1ؾ횩S2 O?ϟNٮx<"tزeKG"o#8f^jjo[;3ElGp v[FEE1IJJ23#Y }lwi|3#Y }lwi|3#Y }lx<:&>ߙp,b>S`b>S`b>S`b>S`b>S`b>S`b>S`b>m?MEZ hXlGM@ hXlGM@ hXlGgD&7Y+ɼk })~Jd2/Z`}Cf hXlGgD&7Y+ɼk }|Jd2/Z`}CZL4_ ,o#8Y+ɼk }\~=??̙|hӟtrZ+oy hXlGGNN/*,\p޽gϞݶmIiXEc"yvڵӧ+;;[^Cki a^@b>BHQF9bĈK. h H7B)y꽣4WD7B'''./^-͛׭[͛%$$`qT &1i+66/Z`}C!DGR|QСԩSu[Ӷye$0/Z`}C!DfΜh"nFFʺtk.m rk!7B 1 4_ ,o#~:7aZϟ|-ؾ`#..|-ؾe^@b>8y\4_ ,o#s|-ؾe^@b>8y\4_ ,o#s|-ؾe^@b>8y\4_ ,o#s|-ؾe^@b>8y\4_ ,o#s|-ؾe^@b>8y\4_ ,o#s|-ؾe^@b>S4_äcXlGp @Y }lElGp @Y }lElGp @Y }lElGp @Y }J]Z FElGMVVVnnvW?i9OElG㉏ŋ'$$ڟ/ElGSjjjllH'O%cƌh7uV͚5D cTTTƍ`>Z }hB$Q}&''˃"o#lԩO?)_Gؾ`3-Hg-[;3Gؾ`TxgFh7u֨(IIIxgFh7AKwf>Z }G}C=JԩS };/?i^W^ ~}Cf"TFT>Z };( Gؾ`cȀ}C( *Gؾ`8 H$ }G!Dz<`"o#zDB(,-  }PQ"?O8ԩS> ]\ihx/!8lGD8zuog\\\.]r*`>BxHc5GNFGG7m}C!&$zt2~,b>ByGM6*$28&q`>m~8Bh}CND"2lG>#7'yB@YlԨ }o[c>SblGp BN>B}C)G1o#8! }@!ؾ`I}į) }deejw7lxDj}\xqBBi(@5`>RSScccE"E'OLKƌ#6o#i֭ cTTTƍ }hB$Q}&''˃7l6uTQF駟̔To#DGG8ҙc˖- 7엚*@Ȱ}C~[n>&%%o#8x@(}CĻ4xgB]~oY|fVt#)*̚5>8w5Ca>ի~!-]@dayQ.\H_Ν;"7*˛0a)%L^W_t| }C'3 wN8q")wlGwyg˖-r<"Cii)%ŋA }>LF$iӦ7?Fؾnr!fڴiׯSٳgu^]jԨ!/| L}C#]FF\(@wTvv_ yNkUXXH;ܽ{↠?"Ab>F:>y<_~911QPJKKE_}zDc`>F:>۷׫WOܦÇ7hЀ.~}sFMmH;[-\0&&&%%Eܝ?~Æ x!77NZ{EK|&M.]ٳGۡ?w\ ޔdȐA>}̙3iiiZoȑ#)UqϮ]pݺu'N())Gׯ~ѣGo/nVi}lHG;pwԪU8)YŘ͛7Ɗ*}ңsssiC vHfZE7o^ӦMV> 1r}C#]}\bEF}EEEFox̟{9ٜEկ_NQ kצ͘1CvJE;v_*6F:|*Foc類SDoc[n]~~HUPPb Ab>F˗/R7g̘QRR"""kс }G.^XNE[f|t |}C_r0"+r5@b>¿\re c˗/߽{|\ }Cߊ&N|D ܱ}C'H:L> ؾñc?̛7oݗ.]f\|9//;w㑿xlG:nݺҨQ#yQٴi/aؾGy@ub>SblGp BN>B}C)G1o#8! }@!ؾ#7}c>Sblߔ8 Pͨ"De BB(/~U @HPEՏ*'!?Ӡ#8 N>Ӡ#8 N>Ӡ#8 )**ߕ(=lZ.-G㉏NHH'GScbbD"EǍ;fy(@ȡ`m۶Q(tոq㼼ҟtrZ+oZ##''.\w޳gn۶۴D1f!>B]vmzΖZC#i $G)ȑ#Gq%y4[Rt>HS񴕼:999t|Em /##kժՠAux ߋC!D_>`Ϟ=Ke3JsLڊ;b#>ݫWо})S.mJ>B̜9sѢEr+ܹ]-mB۳gv'zhرxږB{$l֬\imHg'_V_}G)pB \}P@ϟ)gpl=zџBp77777777?IENDB`onedrive-2.5.5/docs/puml/client_side_filtering_rules.puml000066400000000000000000000025771476564400300237250ustar00rootroot00000000000000@startuml start :Start; partition "checkPathAgainstClientSideFiltering" { :Get localFilePath; if (Does path exist?) then (no) :Return false; stop endif if (Check .nosync?) then (yes) :Check for .nosync file; if (.nosync found) then (yes) :Log and return true; stop endif endif if (Skip dotfiles?) then (yes) :Check if dotfile; if (Is dotfile) then (yes) :Log and return true; stop endif endif if (Skip symlinks?) then (yes) :Check if symlink; if (Is symlink) then (yes) if (Config says skip?) then (yes) :Log and return true; stop elseif (Unexisting symlink?) then (yes) :Check if relative link works; if (Relative link ok) then (no) :Log and return true; stop endif endif endif endif if (Skip dir or file?) then (yes) :Check dir or file exclusion; if (Excluded by config?) then (yes) :Log and return true; stop endif endif if (Use sync_list?) then (yes) :Check sync_list exclusions; if (Excluded by sync_list?) then (yes) :Log and return true; stop endif endif if (Check file size?) then (yes) :Check for file size limit; if (File size exceeds limit?) then (yes) :Log and return true; stop endif endif :Return false; } stop @enduml onedrive-2.5.5/docs/puml/client_use_of_libcurl.png000066400000000000000000001467671476564400300223410ustar00rootroot00000000000000PNG  IHDREO2*tEXtcopyleftGenerated by https://plantuml.comv]iTXtplantumlx}TKS0WpJPa:>0@2II\dyI4Ȓ)z8](>ثo7Rm])H応J g4Ƃ[4̸t΀PE.xΜ&Xז"/VW:$wsδ2ٽwZ$PלV.p/Jqi$hn.{Dݻ 'V6Z 'HT1VE[V6f9;y8F.X, Ԝ^&/'4 %vh,PeGR|Z5Cİc*H-Ł\d'X=U%SQ+p *O%6clp ~:oQ 5~c3gr/,[ZO9ɅV-L^(U5*K'js$1 zQHz>md?:s6/Qvi3*J 7ό&{.k of>;L //.9Zz^%9V\M%IoQN.]U<Кɀ zyV5J~<`R6.jx;|NXT.+Qst+@yۊ IDATx^ x @HB K0%B((Ak[-5[*UQD_(RD5 Ċ)Hm\"  ۜ_Ό3s޹7I2>Q֭R`M*(fCeCE*u̜޴5k֤J_%VX lذcР{6lvEfI#Fݽ9_&f"$$$*^yݻsJ۷W_8p,rrvgd.!!n*667dj!g_Ts7̆Ղ"ȼ2qyn?"gi&eRkb,Ͳ؀o-RFxfnڠAL.zk9[-` TJd۷DEhflv(R x۳ >>ᦛnWQl|Sd9y WR)LϞo1M_}i+G|X|S[vm۶Z׬$34tU I?OO|Sp-i=gx@ϝ;_9YY~nWF7%irvPOUL#9JllP-(bɳK"%?=4FY?~'NIJjAnFMƍ/G@u̘GiLi~WE{oA6ndҤgˮ99lEE՛7=Z1)BED Xˬ&A42Ŝ))mz K.2mSlW_剟.XfCe)*i>]Rg_ye԰g|Og|c]}F~c^ q v>Zj,Ib|2e((Ac5iS6}5xΜyT2iZ32I _h)kZx2mSum3,`mmQm|}Bf @ |<1OِN#;)h5Maaa4 Sşȳ^y7dž)^;bc8[osEFFS_Qꦤ/$U %ۯ1.,nFbwػ>~yv>D[u@ 2mRi3JC+V|?/ Ouz=e8s-bUI` _YoYYԭ[WR/ЗަLɤ#""shDK# [}Bf @x&7wGі,YMuuԮ]Rfgn_Of:DiZwu/W) cpU#c~!`6\FX.(݌~||ƍyRO`Ær!fu}yޣ{^}NQQh_;ge-f){KUՊ_TI` Fݳx'k~ce={'(,;<> _1 q@s]Nc|y ڴi',_|fD)S2Av mŋWP=>h߶dgHH'~50VASuƯf;{.?tϬYA_7BS;,NM_33==7yM<7k? ok^+ k6OL󗄅ljĈ2sEcciӟ|_)i v1MtkyVol\ȤIϘęvPs=gAt/0_̶U ~?ng<[aui|~F/;w>|W3~SO@MIxsqmbm[>qĿ4mz%]k߾11߹~ZԂ_$km#oPZ^YY$$$^mZSܷokMH%̛[FfU}BfWpLz=\vJgȾr-4=]&?=efNoݺWԪ[n> }O322b$)b) '0=詺hhmbnlZ U7Q&gCB!@8拕>*'?ѣ)`99^ӽ{HJ: oHcoU7bk1Ka mwIc~uW@A4/Τj׮s h{m͛;x5baP=Yn|}Bf+pp\h|xVWO7\Cw/a8eh. א8拡a0i~˅w3Al~4p5$y{bqL_rnL_= @p Iށ^`z\h|@<WO7\Cw/a8eh.#̜޴镖o YWO7zWWުeRϊ[ex#<1`]ʶ ˷kmۋSXPFc*<5Orm8|x?gm߯3gToq*Y@iTCRzoW@_>{!ٝ񻄄İ+bc~~KNgNWN:ɭduoz[ɜSPv|:s6Z{zmQ[zTv tOÆūG5eR7]{msfl>UeVT bb?IT6]qEki Dk>ak[W_l{n5Zî]кuuRW^ye;̐o7Ne`xS޻7z+_s2###իsD-fg]/=»AUqx_ZFwcǷf.̯n@5$UJXu/PeۥK?5j_gm&uѢDž 9Sgf\V={ :o \!-fڛ 3~UlW/_W=S^{vW11䁋aη^Ԯ]6d۷D<4={;EYT% l1yȞzؽꛔsʔ{o9ͤҜ1۩$mRi$3|ÁII-+_|XꦛnKKD o2c[ͼ˵S b*tA9Ҏ3^xY4͛oڔԹsR5>]$ضm{ښv\f\mwM?.Wv0s3\2̯n@5$???-Mg}'NIRFMV y}'w`սMF/o;y)S2i5i?=&g!ﱚ| ָqGyBS p׵vÿ_Aݺ(3P w4o(%sʷZ!V8%ҽ^ohJ8iҳ]^aj7r"?xcs0#G>,S *޼yQNs7!!!{3S&L$S?-w?̰eCDwJ^xx7jj|fPyK 5a.k!OӬ|ᗿM׮7KFlsJ?dD_#/A%M2'̒\Lt.RZ zhu-W=Ǘգ<=eXG7W"4vՋʰhzwN7kMwgQ}!66O!ϠxY"h6mڽ vHptqL "׬ONŎp(,^ ug˖3P~%_= ܀jH=\ܼU]76nGko1՘s5ߝ8~ǯ%M99iяO1yc0Zw!,jQ {_m+fګdP*iFKǔN{P(>ov~Tu= TۻPͽnw?CQx !<_ h.+>8^|q&tiC:)tg2F4?%0Öϰa#h(-3s:95U^j si˦#<+l.?:1vf͚fӛ,[ܿ;`*7_7ޕ᷹bz+Gs2j׮繞Ҁb%[׵\d_fW&s|w}Shki=peƝ A<Qҟ R@ '3XddY|]HgϥCDfIPXO{Cd~4p!ipi4%{Jq4G}'w`{WQ6KwGyyGԩx'2+~SC^Jr^e={ ֭ۋD_hT &r->_ l~:_QBwyW\QL0bŗrWX9zmut_ԳioAa]o_ڋ 1iiyDGEQ/˔wYL)rLФI^2g9zg{1[3*g.RT 94^ylgUTٛ״Pp~ p@U x9į| +=^pr/_*3;is h>[OK4 7ҥs9o O٬W 2端%LOoիst C3c;wСãzm@.T}-=^&Y+Qx !f+)e,EzZ:hEԮ]քߛ-[Vf[3g̗6~fK-0[ChSe' /ˡhsi4O4WLoh.}4M%&6F϶l1ժ#>~e7|F*἗Zgå=#4[n5SCo }CY27-.]`~2̯n@5$U.[z޼4^|q&mX{Wh­_Y? 3w`ս]Ull|_3-ck?t/lذcݺ-tSV?lذiyݫUɑ+{Ă322N) zAo"xMh*E~ dnmf>BC! -ltgO]444T?ixvޓVtNu$eiާy\N3zՐTy8&Ǧi)S2Y4'ӔD 7?PfXu/d4s}3@ n/Ec;y[nݺ:\kxrȓhXSg2$g?W@ z`nAS ZǮ]ot{PTF'RV^ThBjf<0FգMbbzxwNi+$!I+U6ݚ ׇ6HԩsM׬pKl;NߟyE4Yر㯹555'wo34e^oL(WH6L-V=s9*UMH3wrO@Uni>,?UWx2z׽| miw,dW?nkA:\d2<}4 b󫧁!;pp  7nbwaXlڶm_5غáZ UIV-G; 6z׽|1;j4  UIw3Al~4p5$y{bqL_rnL_= @p Iށ^`z\h|@<WO7\Cw/a8eh. א8拡a0i~˅w3Al~4p5$y{bqL_rnB̤IϘ/;tȯn ѽ|18 & /Csl;@̗v+[ŋ D04  Btg^xᥩS3~9XUڳf~'X/,04  B T( vKcFJ0=zD~kHTм@@<#RAjN= J `{8 30R*4/F~4#*\*|#RAjN= A<V3` 0R*4/F~4#*4iĘWpXU/x&F~4#*h*lڴF?Q[l())1 Bn,*O@@Ccʧ"ԩSyTzRN@ f_>jN=J֮]׿ɓ .|Ie{7Ťwݨ@jN=Jڵk3Sˠfذa111C={lMaOV;u}voV˖-kժO?͙3΍l 8zhaa!E"J5k43˖-Svch|w^ ͕_̙3#F_R/-/رc"ѣGв2@?x 0=zD ^:t63 e߿6}3flqԩ!Ct6wMΝ%Kݻ[Mlwq̦v²43gtгgODxB)tŜ߲X=z|ѹXU ,[DAqN:u.^h=4pҥK (Odd$>Jzx: )ܹs@ϘoNE9"]K ȏB6l@>>?UF|~,*@UxFW%mڴ1?rHhh+D駟7oެϦ*Mx@Z&~s}v:ĉ"G}dy ދ̯*V|?m6[@b~fg̕ P Uի###h={l-۷oFFhWrJ/%}z߿v%JӳtҚ5kRp? `.=eʔ .xRQQHJNN6mFܹ9s#GJU 9(Z0$R2j(:[hQn.]deeY^ϯ*Ӳ,/-/~EEEPS 0=zD ^=cW`}0=zD ^=cW`}0=zD ^=cW`}0=zD ^=cWNPiGT U?x*A<ʖ-[""";vU  ^_{KMM}9+ˈAD)T[neΝڍ^Jh𪠸~%%%/^x`ڵL Ο?oL pذaڵSn,*@UxFW۶mɓF@R=tгgzze˖j՚1cF)5kܿ>8s̈#5j*\92g/"44Tu^_|'F~4#*h ~|#GRzzz(ӧϘ1cewMΝk׮-BbҤI{D\1`=zc^S~駖-[7NJt#??. wjN= (Z4hP||jN=V1 p*O@@& 55u„ TgϞ U2=##ԩS%%%C [eU-zA ٳx}k4G˦PUƝX [U@Ο?f>b :ʪ_s缦xu4ªǏ֭[E%F34)Tq'!U` ߀ g4,Ux{>YhQ.]4hP^HJp/_?U QAnn.Klܸ/а5 =}.r֩SŋBN1\qÆ `hxꩧ-RsJs6UU-(GޅėFS5l %X [U / pQxFRos[vYf#GBCC}]O?Ti?H?g.\yfV>>|X|pK oV VZ ھ};~ >*$PUX_U~f۶mZ^& Q520^ ȫ0=zD Kհ}GNw߾}t|rJ?p@^;'RJݺusrrE{9w ݺu ܿ?ڵKC7##ɓXeʕROݻwzzxbCǏeK.Y&G㏩ fSL8 „Vg8p`ZZ3>6˦PUF|tcA«y`{8 3a[TT԰aC̤;w3g~76mXE;oڗo FUȑ#ŏfϞ-1|p(grriU$S(ׯ_TTTLL I,I!E D ]FE~a-]tʲXVɯ*Ӳ,//IJ)ʸ˱ U` ߀ @`0=zD ^<}4ɫy`{8 3Xv-~ v7#yNjiGT ~z  6.~,*o@^3 {[U@<~,*o@^3Xla~ɘc TkU`1cW-|* Pׯ޽{xxxzRSS}cPo喝;wΪ<6oܯ_غu&%%=ĉS/|UWɏ3f̠|xܹڵk/]Tf0,ѷ-WZZڎ;X*o@^ bpg4,gff\xk׮5fRpycR0o ֮];YĚ5k"##)(** &,_`JoV|hM6t]qʕW\qd>H[nmS" ^ ȫ0=zD Ku۶mӧ)T:tٳgeݷze˖j՚1cF)5kܿ~[|̙#F4j("""%%%77WU2!PYfΜi[9sjՊۀ=_F۶myc-oVdXp!]+<8r^JOO߿QQ53fos}k! 1iҤ޽{lb[L^g;rH:կ~hѢB $ yEɻw7l[f͚|LNᐪ( x @:4h%t@13s̙TakWa U` ߀ @`0=zD ^%:Կ,ز}-Q^:ul ˗\j.r u ꫯ(wh999$7n4icMuҥAT12]pA_5%xFu_xEE6l(B_@-3bWDnn's=T (쉉v"N6/7TnݺYkWaUST~,*o@^ bpg4xUnݺ5kAAAm%I}>RR9*nVψ'Lf|A:2m4w]\\|1*cȑ)))tA޽Ϟ=+?~HĻEC1P2ϟx/بQ#Tlr,Hx7  KAA?ȟQ ><...22299Yl-| %iFю_Cc,ew 6+x ;P_{Ze̤R:wfŀ4#*h* lR5춿P>}"C={V>|o[n۶mg̘A%OGJgʙ6mZbbbxxxBBK/Df͚%I̙ӪUH*gGgΜ1bDF謔\U5aU=v****66vϟo޼yӧ{5.\HIwԉN_7_iGT U?x7m}3Fݛ6;wg,ٿ?eXjm6yی%Kݻ[9P9wqHtgǣJ&zUeN0AmŊ/-k"2gdd:udȐ!]v@@<~,*o@KUl### ))B:qDHHH^^lٲ8:8z(ܽ{HxFUΑ#GBCCgΜYXX($y*ѳzp:8~8uVjWM,jh^y啄7 UU"}ݺuaaa80=zD ^=cW-|ZrS~%mN4IH PGP^:u\x177ҋE 6ʳhѢ4 uf}}D!(.]4h@… |YcMTCUBָqqsU5Q<10=zD ^=cW-|Z tVVm9B$ɯk$goN'N}HW#)))?ŋ/bFeOڵK\HϾ}HZ|98pW^RDx L&zUݻl:?ޫkCnzر.]*jr93pxFWpXU` ߀]NDDhqqqӦMy~8uSdY3(h* lW ܗ7٧N2 h* lW ˞׷jj=m U?x7 3nB֭[?s|]0=zD ^=cW-|*<$''7mڔ)11o߾ pQxFWpXU` ߀ -"i֬YR(a0=zD ^=cW-|=(Be˖W6j& \ vN=y[֮]{Wm۶ڵ^u ` vN=y$&&^wuW]uըQrrrxU6@ՃQxFWpXU` ߀ 3M6MHH!cw0^gxFWpXU` ߀ g_W U?x7 `IAAAvv2xUp&g4x^ ȫTx  ^=cW-|*w0^nO; 3 {[U*` 4#*h* lW *p=; 3 {[U*` vN=y ;ݳ@@<~,*o@^ƫmsQxFWpXU` ߀ @W U?x7 ^flڴF?Q` ygԩ̛7(T9>*>fw0^gxFWpXU` ߀pŤwݨU9>*>fw0^gxFWpXU` ߀ʰlٲ;wرC9sfĈ5"5%%%77WxaÆDGG:ٳiӦ%&&'$$ȷX& )Rf͚Eǎ0`@TTTllѣϟ?y<}͛O>ݫȼpVZQ:uS;ϝzD ^=cW-|*CL={ASѣG={<*1== )**pO>cƌSj*:&i۶mD=/^@hqqqAAAjj VXA7xbٲ&"sFFƩSJJJ ҵkWY>x iGT U?x7 8|pXXEtU~s)غu>e'BBBe˖#GBCCgΜYXX(3[&13Gwϟ/nSd{W6n(TUMDu-c;*p=; 3 {[UO=T|| ?Sܹs阂2իG\xEEFFvm͚5"e꯸aXdƍ7NL~'_WiGT U?x7 Z"PVebb L,((ÇL<9666ؼy3̂ m[hO UU3w0^ngQxFWpXU` ߀jҥKk֬I!2>c&eb߾}322N<-@V\Itpy^|Flv%Sz~cǎua^]rС֭[;ҥK^EMx <}4#*h* lW-?'nݺ5[ÇEFFf,dbrri(1//K.QQQ;v?-L40rHٳg{KÒ~)111$x?~)F ♀;3A<~,*o@^%N:]PP`x  ^=cW-|2g}uׯ7j`^;  ^=cW-|SNMf((,,t T"J5k%;vlQQQG>ۛ7o>}t&"… [jEwԉN_\fcW-|*w0^gxFW?x`:ٳ' 2=zFQgeeկ_ܹst|qnݪlxĉOSRR2yXDb3 ,)bJoѢœO>)TUMϸ ~,*o@^ƫm`4#*h%K.Y&Gi/8p`ZZ2}8y4Yr%)jz5jJ#ٵkLݻwzzٳg;֡C{u!ʡCZn=vK.y5A<6y ;ݳ@@<CbnF-}W"##SRRc3EEE2199yڴiץK;Y&9rٳaI~蔘JJJ?Q?NE#Y[U*` >wg4x8uTvvvAAQ lW *83q!CnzF f,xT` ߀ @W 0N6mZ۶m[h`Xëy ;3A `%3*Sh{1m8''aҤICqҼ|Ĉf4Nٝh'?yghڵkn (ڤK>}:,,,??͵ށ9XPQ~l t;b07L0_rE@Ĵk׮Iܹ9srٺu222:vHMEEEQW C9zfr>cx#,J!:rH_!A| j*۷o{Q6֬YCI[jа\xv~?i>ʔRRR(-~& .>P,X/׹s8 QޟF~@7#fϨ̢.\'O?~̌njs9Z֭[E &dee3|M>}yk׮±cǨ–-[?6l0ĉ*++ 4{l0+1۝ÇQɍ ><;;^nܹ Kô.&}EEEJԴYVVF卍MMM-JLLԹs2 QޟF~@7#fϨi:k֬g23iӦ%$$DEE;//OԡH)۷FΜ9O… ;uD 2dٲeb/bC;ftT򏀙kʧO.lҥr#o;R8#>a.W_}E;S.46KKK322ht4os2f,[N0= 3*>o4<Ǐ> `%3*>orcX ŬsXVB>> `%3*> X-|l t;bj|'ZWUϼA |A>=OA |'0= 3*>@ko l t;b0?p@eYYYhheҢbccǏ_QQnaш/)4ydѠYjqÇo߾_~H劈fK,k׮Rh%%%K?)ŋzDK߾}N*:::>>~̙-TX~=0:mXPQޟF~@7#fϨ ϟ[Ŭ⋔Qlܸ믿v7OG1zh2#y(0`s=ӉaÆ'?F*!T0Qɩnhh4im3Ѡ̊֏=Z^^N+tFTNRZEΛ7}Gy!%H)=c{W8 ñ-|l t;b,J)3szy)8C[/VB>2nٲ%!!oݶm[llK ֭[ѱcǘ(*z::QgϞ=!!!n]!˕+!fYbV?e2rAs`fg% Q+!QE)HII$77g?(T&O [j۷fu4Ysn]&4F|fڵdANJUU(ߴi02B3LgX(Ϩ ־}"QL֏;F+[l'NP54ݬhdĉuuu ={ܸ}K.Q6)7B?N+ *}饗(O+//״? &dee?S?3|lpܹn0| ~ VB>b_}MND/\SNQQQC Yl4ݰhDXdddnn..r!%!!!׮]+O>=ҥKtbÆ ݻwv222VX.\0mڴ:}TTT;6:::..Ad@,r f,[N0= 3*> WVZeC۽{wZZڌ3O ]`#t?`{3gT|g9"9s@K-[֥Kc6B1|FG}|f:v8exhIII я~ԫW/5X-?opG QQ`"R!5?o l t;b60`w;v츭-w@y_n€5|FGΝ;ov-[n\d >p,Gl|FGtҥk׮=?㝏JfRRRd4G @w> `#3*> v}O?((Gl|FG*((0@F |w@?(X p~,Q~l2GfϨ(sc6Bx` X3gT|9G[xQ3*> X-?@?(X p~,Q 6Bx` X3gT|9G @w> `#3*> X;p~,QP㝏8?((Gl|FG |۷M6/_w@?(X p~,0m۶}ߏСØ1c9a7?g`~Oǻ(X p~,E?䓨+W׿+;v믵ب-j dgO0.Q3*> X0׿KrurZp”)Sbcc'OLRaʕzj۶m~~f)G ?uPLH +e˖ѹPnFǏyyy)))ʛY#2͆555Nl„ nj-Y"=> 5|FG ѓ'OرcrܩS'7dgg7#F̚5K0qD*t&_4ٖőV 7n+UVVRѣimh69xlֈpC3;5nh O`z2ǻ0` QQǂa4 6WUUQRQy愄ѣG:u~3ZzjrrڵkلoĬƎ;"""haaa555JԓF 7/N 2NfxWQ3*> X0~74.++ ?㸸8D\dAĴk׮IS/h}} [%$h6ie u222:vHQQQT(QE%^jP;5n(6[wp~,E~%'NyWUUM6墕SN[uw!==w;v / MZl٬cժU rJ P {1$o~hvfvKww-ǻGl|FG fQf-\0&&ohFK/]z<33S3&''ܹs^QQuV.9-[ֿ[n嫯&-lXرcTa˖-~ĉaÆC-++ MMM-JLLd"3P|âN5..ex(Ϩ(scn۶{ ҥ 6t޽}+VPiӦ%$$P.Իw\-El|FG |T8pM׬Y ܄k׮T 6BsݻwgƌEEEr9x` X3gT|9G]p7ߧ]o&ϱnڵkGfϨ(scHbb"wÆ @ɶlْrw1W^3f|,k8?(x O`#t?) =ջv:|pZ?vfϨ(sc6RwqW^;vtocg QQo`#t?? (={={w=_5|FG |Hf(zRRRVV?(Ϩ(scd[n0O{> `%3*> X(IKK{7n(#|FG |*((=p~,Q 6B?k8?(x 6B?k8?(x O`#t?!sfϨ(sc6B?k8?(x O`#t?!sfϨ(scNȐ9 X3gT|9G @!|FG |PgT|9G @!|FG |۷M6/_|/lhhW^ژyx7 (3*> XࣆDr,&&fȑV.O\"m&A~4g3*> XࣆIyMM͔)Sﯭ]9_Q566jtA~4g3*> Xࣆ4]v) .P;yz&ȲeҢ|>jH;w{hvvqjkk)1bĬY4V 7n3{*=z4?~6ܶm&d"3999 &M:txIPFQWWr 0o<ĉi_.]/ySkJ>cnݺ OGG%wÃdg3*> Xࣆ$;666**VhB) )--/7oޜlBw ;v숈ӧO(QO3'N;w ԡ Z?r^Z\Qѣ&Jk9TN^ur:f wÃdg3*> XࣆYk/ЁD#Ĵk׮IĬmnݺ;RȔ^*ʳ$33S ӈBhR+WuhD={PʤcRyix:K@_/?k8?!${ŊN>}am+&f0V?^}vĝ,X@|/2o'$k֬?!g f~u{G>@> 8?!$׽z3gx9f̘sι[j61p1eZ?qİaĽ*466655-Z(11ً| ><;;rРAs1|gNG|<;PgT|9G i&ދr7kڴi QQQ{lbX,\SNT8dȐe˖{iiiFFFtt0_4kDq Ecǎ}M>]|bakCԲ3Kʭyw3 3Ϩ(scꂂ˥ C> 8?̢~k6a„>}޽[~'dg3*> XGSRRzd+MȐ9 X3gT|9D2ѣd[x À5|FG U> y'?@|aA>у+3~ҥKQK⁋u}9ֱn  ˱t O  Ϩ(sc˗/{߯_?ֳgO|>sNZ|kZfϨ(scA͘1GC IIIAJ<3*> XGkSSS"]Gl|FG LT|\ 1wwKQ!QQu7\SPPrc㝏8?(x Q3*> X-&d㝏0` QQo l2GfϨ(sc6Bx` X3gT|9G[xQ3*> X-|l2GfϨ(sc@|FgT|9G @w> `#3*>צM˗/k>_DFF644h+QBm<<Ï> Q!QQC" l;jԨGj+O\"mg:X |x6B>⣆sss5u5%va?_:*=zX-|l2GfϨ!w׮]Jʕ+{նm+WTVV?>:::>>~̙ŋO=Tbbbddd߾}… SL 5|FG ݳg2D)8qbmmKdĈ4s\ 7oh モ'OѣGi%;;{ܸq-eYmhB4+İqD +7~==m~f(4LNNNuuuCCäI#^^C <5JRSS{1+Z3< >c6Bx` X3gT|Ԑ6KLLIc6Bx` X3gT|ԐfkV^\\L/={tV(Q(r$Ӯ]& [.+++***33S/BHWUp7(##cǎTHjW^`QhM qnnؚnN/ڻŏ> d㝏0` QQC3f\6G z ,oЭ;ƕjf0V?^}v]>Fd3fpsf~u{G>^;F~ |,k0!Çή4hܹsE _̘1crrrΝ;noݺVhiѢEf26.W3p1eZ?qİa yϸM憭/NG|<;D㝏g f4;vlttt\\ӕ… ӦMKHH۷.MmmRػw\<*,--Ƞt2,İqapN:Q!C-[&&f`ֈkhakCԲ3Kʭyw3;%.K.X;4桇ӧݻ1ÌwKQP㝏4NKIDATJ=yiRSSzdB?d|w@?(X -((:tm͆ ֯_?$3yM; `A>d̀Ӵ8REp6Bx` X3gT޽,]:num-qC~ |,k˗/HKK֭πS`#t?8M0` QѢzR>}$''#'} l2ϧI` X3gTqMRRRjj*RV=g X1!wKQ!QQw5.KpD1~"Gl|FG _@)\^ `A>)\^ `A>O@O(X p,<@} l2~"GfϨ(cS`#t?!> 5|FG _@)\^ `A>AQqLIIɌ:h!..Eا\Y|6w?~5M64&&w'sn`qyt|۰a) T%|pyF~ !G믿|F| 73 @O@g,|,+ 1v8S_Bo%=a͐G}r!?OÓh+m E4Kn5 )\^0`3n8YjUII(N%aaaz+r &< GFFR;?ljjJII;w./]԰ hm۶*j_\|Wxxxrrٳ/]$B teݺuO>$ETyӦM1[/۩S?P_a޼y]v;Tr-ڵ/ ZgGyرcBifUUӧ#zZZRj*(/jnOS =o֟x =Cʟ]/H+ݻw:,"h[n:+V{Tcs˖- E**+ȟȭxѴ_@)\^0`K)SDV4OK>*ڙ>}:ڵK\~. 5z5P!)/}Qzo?~]ҥ a# &Й3ghۦgEHFӕ+Wk׮iꎷu֕m oKrdQM)!y[}'?4 ^{Mh^r-%zEExIق"%q{9P!ڈQ?\s֭۷o?xիW 9}]D{31ŧyd:7B_l0k+"tA6ּ:YK?4<"D+mtϘEE;|FNf^TqHb}>~Y&44K." kj߾}vڽ ~ 0@ğ`?|ԩSEHnfSSS4'ԲYuqx/^? g+g,oƖ-[Zd/DX|F|2yyy&Є^L׮][3d@G(^ZOhfffd455СC~N:uG/_~璓<))__iibZzubbbTTܹsE$*Trw_' Yl٠A/ +3Q /p=mv;w6Yd@GXUUU__M^F/ ./dD0`3ELI}&3n/86ҿ ÿ#HQ|4q{9P^_kc`cǎ)EO@O(XVpz>m̸1PLS`#t?!> n$6mچ Ldƍ `PQp#L0ԞS`#t?!> g^}U=s C1~"G,]>G?g(gT| !> `}|FG?g(gT|DQPϨ(8ԩS/^-5_DFF644hG˚ZMy~R> 6B?` XA Q1;6>>}3gάV!5auMCʕ+EDDiIk MPƎ5ѣݻws=T?&&f~;Fmi߅IZtĉZm3x./dDR,+33*haaaTT]1yl٢wC MΪ0%rSϟMOO:,\ZR~ӟ Җ>C~XԧpyF~ 3 |,+33*h~^ymiGGGϜ9Stڵiii.))RRR099:eMMǒ%KĶ+WիW۶mݫ(ŋO=Tbb"mطo?\cT7l|ڵ+44V  at.^ :rxzjтVy0ٹsgxx8,555kK)Q9sVDae}(V%0$&&Ě*--:ujnݨw}aAsvUUU!!!xy愄¾̮Y;\pIהJ۔6C=DYYYbP(M4}7HNN޻w/ݻwSI)N|2 07E(++b Չ{Y[7UulrBKjJdYʮ +!J ME7n\5mY()VLLLvh­oi>!Gׯ_R8vnV)M8fu,Ν;ϙ3G|3!>CA>2W_Ֆ5kn6$ ,w7r򲤤^*i&є|FBӔL?İ\ߔ;w4ۺ\6g̮Y; ˮ]֩S'3dK̘1^s7LNNQthxa5}ʻw_BDΗQj.G,PϨ̢.\'O?~nGvvv}}}eeAΝ6V\iѢEP4K-ƾKW^-//Tf&L驘Ԋh$[_bXoJ?A3f}I&}ݚ:mnj#~EJ.A\، q0lǓ 1uT!"u|giNMDoߗ>}<3tMΗ>x3 =G,PϨi:k֬gϺcǎl'..nV4VԜ*9==] -]T?߰aC۷oOۮXBRr5mڴ}M)-ub>s̙z())cǎtY߶V9޽{rʋh~}30lǓ .Jȿ%=rHJD'[anمF}0O#SN 1~ӟpv\pK> 6B?` XA QQLp۽{w޽~颢"m> 6B?` XA QQ ֵiӦfC]|'S`#t?!>  ҤP߿[PQPϨ(@0QRҳg>}0 > 6B?` XA Q3w;vtxg(gT| ]699Y$'߿ޫ|>Ï> Q33*> 4D22dȐ=z̘1C~,QP㝏XF QQ@ e2hc@|2 ?o`#t?w>  t. ~,Q~l2G>CA>> d㝏0`}|FG |'; ` 8?(x O`#t?w>  p~,Q 6Bx` XA QQ?(e p~,QP㝏XF QQ?(e p~,QP㝏XF QQo`#t?w>  p~,Q~l2G>CA>>/swUJG%bccGuQ߭3rC,fx O`#t?w>  p~,0Ѧԁs=J?>777==]SnXfʔ)+X [ޟF~ c` XA QQݼysddCBBBKQI v/7D/\@N\\\llɓESx=pMS%3奤DDD$''%T|^dQu l2n)  p~,0q?sLQ(gϞ C є+s=G=ٴZJHF1k,Q>|չ\.j|ưǏSm۶:_SS#BV>g]g(gT|9`կ~tUwW_ڵk;Srڵk{EJ^DbbbhMMMT^WW'ٳgڡ:֭ʢT*33P>@3{ |2 8? ڶmCSrM+(8}4\. )(>)))*Qi&Qn֎aJ   > `}|FG O>$44f}0oqRBJ?z5gr̘1999Νs71[nYYY"oFbcc_z饫WgffrvhrE%&&bGAg(gT|9` a„ BJ!f̘auY{)>f6mZBBBTTT޽D'O>\fTaÆݻS9%9+VP )--j_-]T.|l2ǻ\.WAAAuu6 XA QQ=m_7F~ |{'|H3+33*> XࣶC>9t?w> ޢk׮IIIw}=Vg(gT|9Gm|s~ |n4ݺu {wJ` 8?(x O`#t?w> 7FIiN q PϨ(scACA>> QJ2ӥKo||433*> X;oQ2C9 %325AL QQ?(xE3ל1}|FG |l2Gs. PϨ(sc`#t?w> _s> p~,Q&`#t?w> _s> p~,Q&`#t?w> _s> p~,Q&`#t?w> _s> p~,Q 6Bx 5AL QQ?(hg(gT|9G @w> _s> p~,QP㝏/ל1}|FG |x 5AL QQo `#t?w> _s> p~,Q/LOw}W)ٷoD65jѣG˗/rLLȑ#:$WD\;_9 b 8?()55usR(-ϟMOOהV2eJ J;_9 b 8?(c͑ /E&oٵkWhhܬrXXpe8qqq'Oyyy)))b&y뭷(JǏ9sfccRڵiiiKJJw!L(F~ |||433*> X࣌qO(SgR!ChrsqlEmm-#F5k?~6ٶmSJ;?p]]]EEŀ{9mH \.*7oR?''aҤIC5kH`B 6Bx 5AL QQ5sԩ>WXѡCK. )$A)7gfTTR"TUURZZ*^n޼9!!VN>_SSiȑ#իIQncX.8qBܹmҸّF~ |||433*> Xf~_%%%]z՗vڽ;nŬ\)v޽{_|E:pESS֭[E)PfffaaN]]vϞ=Jqqa`+Wgw> _s> p~,QCڶmp'[yYpŊ*>}].k,X@)[̚5kܺg\,r- |||433*> Xࣆ>W\駟㥤0oqRB(z5gr̘1999Νs7g&[n2ZilljѢEJ;'N4hٳE#ÇήsWg 7;??(hg(gT|9GIuuuAARJh~?a̜1ca֥f^dddUUӦMKHHݻw^^fddDGGGDDo&~ߌ6U~ҏcRӧ7443Mw> _s> p~,0Ѣ'|O>w>/ 6Bxw_9 b 8?7|JJJ֭'3n?g`#t?2> _s> p~,ѢSѣk׮wq'3n3ס?X|433*> X*&P %d->Ec5AL QQEuvwQ>tRѺ!auA_u[>ar;sfr~[[[oۑ p~,Pj={T#G; F~ k-ka 8?M0##) @`#}|FG q %6=z@Jw>CA>&iӦ~cg(gT|9GMAAw> `}|FG |w@?(XVg(gT|9G[9l2G>CA>> d㝏0`}|FG |'; ` 8?(x O`#t?w>  p~,Q 6Bx` XA QQ?(e p~,QP㝏XF QQ?(e p~,QP㝏XF QQCK,66vԨQGV4X-|l2G>CA>⣆D>seZ?|nnnzzNcc.s$G[M; ` k׮P|ʕzj۶+W*++Ǐ?sL%xSO=ٷob*p”)Sbcc'O\__/*奤DDD$''+}ڰP?۾[QQQTY9S׮]F <ĬqiG[a `t?w>  5$3gϞ2dR>qK.QɈ#(s\ 7oh モ'OѣGi%;;{ܸq-iYfQǩm۶:CA>⣆ >Ybb"Mi+ߥd^9rD\zי3glYUUUHHHiixy愄Z9}tXXX~~~MMRٰP9tD'N;wM7;X-?@?(XVg(gT|Ԑʋe]]xg e2r EDbbbڵkDueeeEEEeff ͑xոVfٗxyQ̑G[~'d㝏0`}|FG yh>Xf\.:uJ.544,X >>BHj\ vΝ3xG6?(e 5a>C]___YY9hРs &deei1c;wݜ Q33*>J \.Ri>v踸ӧ744 .L6-!!!**o߾4Ja޽򨰴4##ZHOOeX(獋mEFF*?Afx:fan#> Q33*&ZTTD>}޽[ ^\ ;>CA>G/_>tTG%3n3 2> ;F~ |,+33*9ZTTOw޽iɌy>R7F~ |,+33**ӧgϞTdő-'; ` ݻwRSS.]JE7:փ~` t?w>  EoWm'Nׯ>'ԧpyF~ kq-ka JC )͈#Ҁ > 6B$PϨQ㚤=z `(e }s0!wKQ33*>nh \.6>CA>AQ3qƽ, QT>-փ 6BI~V|xϔ.E~7~xm\^0Ms2Sg|x./dD0` QQԧpyF~ 'B|,k8AbQ3*> bD1~"Gl|FG?6rV>Ux#ŧpyF~@7#fg 6B1|ZS`#t?`{3g)> 6B1|_FOopG8:v̚5k^yv+7|o v8,,X( W .o?\~yy 7o~ccʐʹ`dǂEY0Zpxr5n*Y'ǁł˵-d8' 8,ʂՂśqS/X,ZH{KkH8' 8,ʂՂgxF` Iֹ`dǂEY0Zp7]L/^ip|sɂ;`8xr9po^5$Xœw epjrt 3xu kH8' 8,ʂՂgxN֐;p` Oq,Xˁ 5՝'!w:,,X( W .ky>fͦ6m8pLjEޑe;N Iʷw`s+Ϯ@9Λ_|qp]JGt[}/ޠOV?Z V?NY]r?8r[bo={|G;u~K׮O=̑#'ռZe%!!!1~u7rؙ#99B'o6v"$ߩhСxu ̆YofR7#׿~="""^JvzNS*mjm兿J­-[vrcw\uTTTiiRcG/E^}5o߾ӑye?SЇ f=B.{ ?/5j\\\|XXXǎq\V_톗=>\}ۻ=pΆ EToV,Ŭcc>T\2ō] ~zݠhoO?L9G}9?P߾ʞ/ⴷoKOڟw֭r]4KYY} /D'='1c?Qxxҕ˽fCҬBof?#B]Sr-:u剴pڶQ~[yg֭{v}5p7vYSL)|ə))]bx7Po(%|QI8f34Gaw!ý jeII#?xl[Һ_nl8Д75 v:@S?tΜÇ?lxŰaS1-?֯ڻw59d\7EX|5|?KNvS;}lR@A  )1ԣo?'o%W_zEWP]PwV+,\@JSP?(/tRʜIY&8g.BTYfCҬ?q]wM3ozWU?ϟ=4&&FJK1?:ٹH_b?뮌e%R? /3z5Rɗןfzyv߾!j\622RÉ{lQ{VM_|Ul,7vE˴{DIYY=ϟ}v^=qe RǓSnrL} K EGǼIs,e2}[f"Ӭo~ZLL, e` smT$QݻK4_WLN1r ,uOS[iG=-iшև4ˍ].f==<Mz2bMh|wJJW.NTX(gRڼ]_ʂf|NJs \;'{|I5M(;|LUDDҥ{TqRtG5"4\7ˊ 48,7b[޳o~{z-[=v{Û6`8TR6<p=uڊ.s"xfwA^37sI4>>A,=|iD#/C4//vS "Y{\,7pǕ.zk?/ n5}O԰*u<>foVܨK=47xjY+j8 epp+f/E3ZzE>&LA2ǩ,t4Cx^ATgv_n`8L&44T?8[r''roוf˥dOϻ|fc>X5Z?mtp45t\8O"D zsPiiEv>)[[:t놧MMJlxQQG=*I)#eo"(!%{6[Cܤ*RjӼg޴r7P{LSK]l޵GUeDh# <-Dy ̨A ȨDHWBa !"H(̵jCY/wٻO7OT9ZkW}֯a~bx \ThN:`d/<]Ig^bc[(>dXa55O,gaTuiu'2gyǏK=-/D>7畋Q9|xo58/oxaIl؉ԫ̭,r˖0T'r8m?Efee#h۶?1Ȏ;hK^0|sC*DoC Fc; g|#7e Ú_Y=2qupn:Mo?k9PgjkcWQvӬ^LZmuu~V…cτ;aƌჹENݠl͕?L/M"S'\Uu,,,,ЙOUbTM@fKӧ/w`5q·k~jAjDmHN),"%ߺuij\LVj-z}nC0X{c; \ hvaays!(D @fqǎٳWtTC2Nc;|Cy^/&&W5xaٮPUv2*s+f!eZ-kcЉlm)maR`> f懖vkT% 772.dkjÐ.^x bgn_.^+]>t$`ؼ&hРT2TXg8yf yns5%?a7@q~3u0%fn^ktj[t!fC1XpL"g`w+ Al_&DlO.ϔH[Ti6`۱L[[lg< vdȐ73<˗1hAjK'^qqXȔΚ#hQn'α1ٳg,ضm+7? ω26ԩSjD .p`n4+գX(]B0=}T uWY ̝td9OZ4s"G7dJ͕`y7JgzH4M覤}Ϲ$~Ç%׬ln>'Dj3qÆ|w Ξ=_=vIl`[Rg~~FknwղߑyWE&u2 ӸX3{ j٭TK>|bgS_;>ʕǎUq?=peel/|B./?%,~}m? ܺM .ĒRt87 f.XĮ^n0Kh|CO>[$Ν5lʇ'>sI"l78%׬E:RZz!<%EhwŴ7mw?*7I<>cRah:5[~j MI}Ͻ{'~وx]Åh'#ocGϔ)Ϝ\Tgn}N@f֍%f'hD WN "YIC H6۬i۶RT wSS?D`ݢјzfz,k矟յk7)I105#~_Wmn2:膯+/ulekOν{Kh|VEByHI/XD|Y@ٹ<6m 2T;<>͸vȑ) ժG`ݺ%D)jZM\X'4OO9tRmHjTWY+Vӹshb4!:dw  ͯ|X ?Gy99r4"7[PـucBl&38azՂm` dԩ3J˽CAO87r(de3(tf L㺚EMetv֣nRg8 3aSF~(%XYf[ن̧;2C 롴1X :Чlud>gKu_H,_^$. vdT6,KVZa3biB$]Sl ID"'MHj$R>sd@K4MkJ ]c!:!iHI\Dg y闦 wMIk,D'$4"+iVg\L4MkJ ]c!:!iHI\H%#MJa>?藦 &Aـ5W˗ߗڔtil4 ٛFBBWҬt .gΟC&WW_p[s͛?f_&DfJfvXNR_v%ֽC|3m ] .f>˲e+#GNlͅRA7$D 23ywh,D' (`ժW$cE㬫!)ǫ.GnGvq]"/# v<qtA3#c5?Ə֠ m1Rdz[0_dB$dVN-,gΜÿHރ~t .gNa%‘s_x:}yiUՕu6?ϟ?`U7Gֹii:l߾c}#k8T\(bXJJgK7ސ,**SM%x⍌Uwܑ -c!:7M@׮҃K}.knn^BB[?m\hU_OvGH m۶E3g.*p7#RkJ~ʶ?B'\$6|h^~e}tsh433?ѣj4GprN+D\ψzBh ,ZZrrXQUݴ4KKS6mmCK/-޽'2={ H4{CBdbϞ=8޽+w]+BtB'o/ }__ϝ&C7oȾ~G&U>0G"PXF$#{G=U^~U\Gv,Ǒ7!dеk7ڟkޢEK1./XDX94Sms|d23dt=^ttgX8 yV$N<kC Z F>!@ MP݇3H~bbbx?b#r]<p" 6lzՐψ?_N` O_Oٿx]\E?tsh|d_T6)ADx3j"VUoKKnݺ5bاn*o<7$a (>SYy}e!H-z4 ڋgC]XIO8DiovP%?fjX>Ȩ3iH>C } ~mɢ߳2++ r'OڋQIސ@ &KLI>~$=}a?]V}!ݒNq1~dʔ~|^r¥x)iK#:p @ gyoVz?=>ÇOq"8q7oر̙EE{{לci곘A!l4%?7_+*;t䓿-/?sgA|[Kbccbժ @HU|MKHGbY/voҭ''ywv9H$n3ߩS/P9ӉbzNYH,'g9 膦mENb7$a (Ĉ"Ep!|$oH PEBAIސ@ (# 3''GV @ PEBqL(oެQPP @  (!Ep!|ر/ԉJll|bEƝ;wd;A!N ee֮&617m|]YfP@!F)A3@ \|@ @ @ @ VOnqwIENDB`onedrive-2.5.5/docs/puml/client_use_of_libcurl.puml000066400000000000000000000020451476564400300225060ustar00rootroot00000000000000@startuml participant "OneDrive Client\nfor Linux" as od participant "libcurl" as lc participant "Client Web Browser" as browser participant "Microsoft Authentication Service\n(OAuth 2.0 Endpoint)" as oauth participant "GitHub API" as github participant "Microsoft Graph API" as graph activate od activate lc od->od: Generate Authentication\nService URL activate browser od->browser: Navigate to Authentication\nService URL via Client Web Browser browser->oauth: Request access token activate oauth oauth-->browser: Access token browser-->od: Access token deactivate oauth deactivate browser od->lc: Check application version\nvia api.github.com activate github lc->github: Query release status activate github github-->lc: Release information deactivate github lc-->od: Process release information deactivate lc loop API Communication od->lc: Construct HTTPS request (with token) activate lc lc->graph: API Request activate graph graph-->lc: API Response deactivate graph lc-->od: Process response deactivate lc end @enduml onedrive-2.5.5/docs/puml/code_functional_component_relationships.png000066400000000000000000003554071476564400300261620ustar00rootroot00000000000000PNG  IHDR*tEXtcopyleftGenerated by https://plantuml.comv,iTXtplantumlxT0WLD زU v mWPJ=U&1Rb:Nu=4oy3/3+*rx-d~7ѷkcrEpbAǁRl灹𑴒yw iR, IV'Oϖ`'̙YF,WT2TΔ>kyޞ_QdCJzy[+Jk=d'E5*Q+@qlI;,}+'4=9ݜJo%"pϩI2=í$>aU7d듮ib *i ڡ8 ':׵~sNk6=BioxҌ"؉ͻ;_~(Ϛn]vLcm}t+r+Wsz5%'t~pI78ɧp:7c`qdJ1clW,hM4HQ bk&/qCEE(stxTy %UTdZP~Ị4p/4<\ CPeG^9IDATx^y" (C "2 "(C  R/^\~u֢"ԃ2脆=zHJ={ݝ}  :7ndswB/}  :(C@;2 #eΠY(C@P(C0 M2y9 $(C!Pa@@tĿ Aƌ!@HP1/CpY M2ysC  :b^_S@e2 „2yeh!DGRMR(C@P(C0!DehP!C@tPveA9!DehP6)CDDD|7=bB24 ei2DAAAff۷oـ  C@tڤ !J(C@PhuN<:3f011122W{;vlQ )aaa|>~PWWOMMVUUorr2LJO?1i$znWQQtDc4z:t'ɽcǚԱޓ-[m۶:ǎCO{?_zž EP~A!44c___ʖ׬YC5K0R^zQݣG.7n,--+^nnn˗/iӦ?^GG2N:~M聢(Wܿd]N:NwQPP055eׅ n޼IE 帣!S𖒣GһzLyf*aeeŽFOyyudeeѣGׯer 2{B32H.MHH~7Zw>JJJvܙKw,X@V^MwpY.e䒒Z!¨ejjj@KI̙3.\H鶱q]8uH~˗/SH 67n\8)J3()u۰a% ~9Z۷i/_d(m̌ F=_f e'N )ѣ}}}qҸ \ܹKﶇyWwZJ޽ }4...>>>G\ZZ~-O2pi}Xv-W =!@(Y֭[qTؾ};vM8KY+8TASS)D.]J) 8i999#5-ʕ+AAA61BQQsKHH߳gRܹsuuuv5suQ۷oѬ)*r?533JNNt 񥥵W|]VV}j.y{{^;/CCCmmm &M .H*++w5%%-WU!;vo\ ;YpScՋ;agΜ9Ύ;1Z<؛7orR]]>)2rJ޽KeeyyyQƵtE ~jĞ4J5鶾>[XXPFIu ;^t)~.ouܹ~ZZZ;wVSSCO3,,,))@pTzV3f >?}uQ紴{/^t#==k'&e߿NЬY(WQQOiܹ>>>ǏqF OzKjK3G6l(Z/AQh\OX???ZwzKY0*++{(C{RdJ{BBB~axuuuJvJСCqԁ`qZ؋@MMÇ(spp055gۥKJfΜzꀀi:WZZJϙpBz]ݺu^\rL# w3s;w,eЧIڵk8;;[ZZrۗPgt̙To-G>ADGGw1vvvzkq:OYHoɓE#/)^fr^^^BVV٥|ʔv@yR>} R6NXfffUU{ߣt4,,q={+V JKK}eVe//Ç/]D5\NޯnUUU## ҇N}=z$Mzņ *wp'1Lw7E JZԃ2dxq}m$-RzЩSnݺ JMfQGoݺE `- ^~}UJxU5O>JJJǏwppؽ{ŋ|NYY^^^fRTTDtΜ9vqhq2ɹ~:}<==.]:a ;wر ڶm}w]&U9xӏ &Z7306A` lMvZnD*̚@ JKKi Ni htNw~Ĕ)Si{ݡ"ܹVVV[.?]'R~-))IMM rrr255x{Ա)?RS 4z^zѻ|6-QW^=tkkkZa|>ZZZ6.[ԩSwy{g;U+[&شih nD۷;mJ&S˗SY9BTVVƕÞ==='M4b $mCCC333 rGDGGsסlϞ=nnnٽ{w}}}JB.\#{ɓ'WZ׫W/Jl<<ٷoڵkgϞMVϞ=y<}yi KNN~5{gI"OxaTnbΉ'rg(++sgs5 nHwMwxb\ݕDDWh$!ݣ!ѣGO׭[7J]bpڴirsswtWSSzI&)))[~SNeffwhodeeg͚㓒"Vlj !e^xqƍ#Gз{޼y >|ܹxСׯ7_]'55;I-n]n>N ===W0.wn7SYdee沏„2QA+WhbccCcs|jYpӶmۨs縋S]b{UQQq֭={Q>Э[7z---/\PXXޡM]~=oL:ݻl?2D{͛䰰08xm(N8VZZZ[nnǏbbbhuV4d,++K^zxa /[L0'mdcp @@/JLMMhT4dȐSuvIQꟑ vqPOuu5eYAAA˗/{zzzvvv{MJJjyE R1cP@IDZ2DEoNJJ mm\]]-,,tuuuirJJw)O~*6mhM[xnn .wqL"bmLփy72Hr ^v-""܀r̙_]t.Qw^[rrrddd;9zݻ:fɭ2#۷oh <%:: CmTLL)ϧӶZKMM }j;uTpC Κ5\w7ϨQ&NHQ''' }̜9300PR_|}wܡLu۶msҥ)ť6<<<--MB/>yPgϞ(ӦM7n;t ##M![A;2n L,ewϞ=KLL 4o+fTTTJ'>J<](@AD4񐗗תUhөS'9Qr;ydTѰ)00ܹsyPsEq>>l.-޾} L_pk׮oAUUս{N:i&JMiڻwoCCÕ+W8p6eee}@<{+U=z&ȯ:{l\E99:(((p"͙3+U߿ɓt'OI jvرj*ss#FtԉcǎZn h]. ^wmaanmm(C݇Nb+;w=LWQQaooJċ/&>vؕ+W/6,,%KKKʎ q֘6K,֭wܨ/_w!777===...$$Q3f~ڵ/B7FiffF7Qg9*P&QHJJӧw4{Ok k׮oDDDrr2jO7oܱcedd dggGi3FZ>=(E|>ʔە{ ,OaÆ L{uЁmj T ϟ?~Kpp0b=<<ݥcv U]]MghhB)Sпᙙ*))yiBB™3gnjoooiiibbGzrW^f۹%o߾-=H!(aʺ|27_ 5&L0`.]x<ٳgSk׮ӧO )(v}iK__>ĈW^][[~~%?˖-4WVVNJJbrP6~6 sα1h#/^ڱc}4mӧ噫W>~8pG>~?:: ?:::)N>>>|-DhPh߿O}Xaܹ#Gx]t)%| |2MaR[nQ1y޽{aċٮB렠ҺtŪ*SտV"2y]]]STmȠAN#6m(66{Μ9BL8v `$4U^^ޝ;wh XjռyLLLhٳ'w+###sse˖СCϟOJJzic6& Ph/rÅ ֮]+(7tޝ)SS:tqx?HgM^r%E~ٳ4ĤG_`=4TMMPU(j5#R6mڄUB}6&&ș3g祪:{;wjB4 yi\\܉'C[qNX~/w5,!͔ "2zyBBÇ7nHWn033[|܀LNNΑ#G.\dɒPW^Mܘ1c:TRR:s? {T-2ć ̙C9C#\yyy |2MQϞ=hB$h&8q>zAk w٭[Ϗ44졵1P(CHwFFF҈pԩ]tرc7nq" UViiix<=y'd_޳gqǏ=wM0Rq 2ć#; ƾ… .((`c ƪRSS)v7***s̡=xի[KwY^^6D/޼yiD+g7M (CH޾}ĉgݭR^:k,ggg233EՀ8Aخ]hջwo۷OJ&i(##ceeu%U_>|ʕ+Esє!8۷\[a͑P999ᎎÆ ѤI1pl͘1PUUuĈB=rŋ޽vA L(CǏ{zz.\PQQv4³ܰa_ڡ3gI]]_~t#22z(,,ܱcwyy!}fxСC؀Ј A=J&iCgll 4H:F]pSFFF3f 걱eeelWLo޼yŦM޽7!ŦMhSkv)b(CLܹ֖ΝK#GܸqG,swƍקO05fay󦕕K.u/8p&!HDDRbb"={(**ư *++rʖ-[LLLhE[m۶ 'nx56++V`www;;Swޝb7n޽{l?D;G{zzO>}ذaݺu*-rssۿ ٥@=(CKvvvTT,Y2rH]QbsγgfffG?`ȑ#4Ra ?0ÇW\)''7{K.ahªݻ _!HBBǣ @ZX+**tppz;c֭K,4iV;f+++77dff^(C4GNNιs缼(cɣڵkigZ\\]\\hCll5MԩS#G8p 0}KQi2wϧ #hÇ+**ڵ ѐzz뵵UUU-[F.Ҫ֭[gΜa#x<m"""hH;@* u޽qF@@%FFF22246mڵk)sN۞ΝaǶmۚtMa+++۽{qdd$ҿzm.N<Dm$++b>}Jĉ?@J=~Ąs $5.^feeE@~v][[{vvv۷oOJJlb P PڰaÆ3g{ȑ%$$ YϜ9cmmMk^9/_A"i|Æ}NQP7ʊхGDߔaÆPvڵ=avP4˖->|x݇jmmk׮8qJKK---f8W[f lE/^cmAL֭[4>aS^^Nk~DFFr3g:u G|Νwrr1c w544&MdgguI&_h+(C@PUUuΝцX__{MII)++c,%%%4D633;|؞,(@Ѻ^zE~ )CDw6hv=wHH<ٯ]Oxƍǎ۶m 7SՂ 6nH_7obT"2HO?~iݻw4hmpw}m1H ZΞ=;w\ӧ154 , @ah0ڎX!>]VQQlў={6jԨ3gw<ܱciMUVVI_˗jhhݛeׯ {O@Gyy9nf͚ūC7<==ccc9'l2yyyOFt dѫWfmJ˴NIIaVYYIﳺzbb"v֭[vvv};wnKu?|… w撚ӵ飧gaaL_<$ hwggQFn>}:'' z222hL)Wnn.C̼CAA (@ ڦLbkkښ!UTTn8%%%>ojjrݻw_xQC$Ë/lmm555y<_jjA} 4噴[5kNZ*G!UUU،=״*7fd[n9reԩ***;Ν; :]=K˗/AV.\(##3wX#""~^Vٳg/[ %I)CHMM֚eƍZZZv11 z͕+Wlmm)ƫVڿJJJII {(Cq;411V޿4l0JNhw%ѩӧOOK*Aee33Mxlɓ'5J{ADDĦMf͚տAYZZܹ3&&Gɉ !@_nmm-;#ggg!]x K{***n߾_+ыVUUgJ%ݼy3hx+W㶫l$\QQQ||ߢE;c 773gPf[z~U\\k׮}?T+--]bz\\r ϧ[(//;thO󭬬p\hC%%%;wUԔFlXٳƸgϞe***tttـ29q}^LNN/]||<455568eee/_޼ymxxW~eK.3}hwyzzR`eeu]6,EJJJ͛Ň˔)S+Q 7nd[[,..62)))ls(sXbEuu5h Ѯpk֬)++cI#!CC=--3gΰ1Q)**ںuMVV.vvv=IHHPRRz@RS>46bQQQʒ~ LqqɓONڰ?Kbܸq7nܐr'VPPh_͛77npҥaZGK1RS 4>ưjjjyyyls/^lll *@{eXXmd 4|Q I¡ k׮L:ɓ'l @$ 7l //lٲ0Iee%Rmmm+nWUbISL0a޽lkk6lXii)ZV > "o7pӦM7nܨa;I !Nn556 /_\fCnn.Fs)))acЦ|۷oـĒ2yx Snn.LMM===߿W(CSQQQbg[;99al;-H$44tlk+y)}Ocbb:r6 |4 ;w> ſ*2DSVVfkkr666a-[@<̚5klbLB&Q[[;|SNVjк"##Pwq QFyzzPN|hմ4mmŋc&cǎڴio߾8q3[/^Ғk _v h/_G4t߳/VRRuҰ0ssscccoo|GBB:ر#%%iSTT geFFFs"Y-Je3pLJ q%i:jʔ)Bx'FHsa;wrZZh Ň0aى'***me)ݻwkXAAv0xhs7o7nٳgXpU%%C'ӧOg[%q=JWڵ-UQ"ʆ /4UDDl<{l۶mÇ_|y>2200{5>qƪz+33ܼo߾Gfɓ'bInj :tӧ~t#**s;v/&U͙3Ϗmm=>lCS?~F_vu[[ۑ#G( ҆v{4ⴰQVV}/!_d ߾brWPUUMOOg f6l epN_~QQQ133c;I;wMڱjii{ 4($$VG1P\\LQF-[ݻlXP*EEErrr4Ӝ9sF)}U^^IumU[[lALJ ogR^NNSNQ b I`kƌ?fcIMM i"@)fkkʶJnݺuӷoߡC=ERRR~zayyӧ߿RYiRPP驧S\\̆[#11QVV;vܹMNrgѢE\bȐ!fRĉlkJJJRQQAy{СRAC9M}xʕϞ=cíe)c%%%йڼy{-}B]tPk@__6l _~˸qhKuuu߾}}= ڙ2$,, |MmmլY@]ti̘1>GBdggxznnn;%$$ :ȈV66.wPKKk۶mlS׮]˶J AA m߾}ɒ%lkkҋ/TVVNfƍyS9sflDFF2U~v(;;6Al@e` q-ee3gΰlk# !2+C;;;oooU=QK/_622b[LEEݻl^~=`& (^jl(CH1cp7 YYY<|܍cǎOjjjoxbܸq,\۷ԇ`9;w*++ w}A ݻw̜9wAiϣG?<~K?>}rx<իWv >2 (C4RD-)C0HFF͛7l@8fϞͶ4Ń@橭]v󫫫 !JQhhc{nĽӧO7$??_YYy,M+%7n988p?~رc3p666~GRRR>κ Khh4 İ(..Xmm !%{!Ȍ3?ζ m4|>m%@SƪIbŊUV2i(C999'N>|1el+kP{zz:~ȑ:Կ#OCs̙7oݨԩӱcǸvoV0 3g~gI-Zҭ[%| CFDD0 YhJLLdM2D#|O2ľ}/^̶ YV&9rdEE(4:u={@=(CHerrrnnn )C0eQRRµ'$$|,Ѝ7k|Ύm44p(1҆%g ! f̘!8 ⫾Z`掆HLLgDUUu߾} gXܹshhiӸC*>|^T?⡩;X# mm"!{!(O]'w&@K}b+..fuPNMz(C+JJJK._-Cm]]Ǘx/[?~L7\?133SX٫W>!?l/4ӹ*Aʾ{ 4MҒ=Qvvv ںh'''GϜ 4 y<ޭ[qsssvvf[ !}QV4dc_Ә2cƌʪC"vpĿC 0h矩 IKSTTx J= yKɵcǎKM2D{ߖ!>lmmͶ =ܮ]VׯȮ $4.]BB*ݻwoСӦM{%=z8x RCC/nAz5`d6t(CLߧ*Lihh4b_~zSSSN k1PgϞ= ?ѣG;t 8MTTTL8֖ ĺt钞,(CLȼ~ ӨQΝ;Ƕ4]uu5D}:! ,!۷ogΜ9rȬ,6&fvɓl@(o;wy4ld d"2ȴ R.\`[)$$dl+@жHMM-66 H2(C ezO/~ 4~x\]|RVVK4*>>~:2(CL!D>{:tPSS BBdZ A@333UȬ˶4ז-[0IH1c0(C eIǝ1z蜜6_@jhhիW׮];tлwo!DUO<ׯ*d/^/ \cǎbٳg4fQhK|ͣG@BB&x.]RVV> ˶6N||\^:_͝-S!jjjzQ"@I#mO dsZ$qӈ2D[*((|mǑӧON"gbb lm~ Q xgϞjz k=z\^&z$SǎvuuuڃvŋFMq͛7~VԆZ AJqUh*lg߿NDC)eq2īWئIUTT,]tȐ!E B8UUU|߿g F1rHeeewwիW[̶;_Yׯ~Ǐg[,99yl+@8::ZZZe!]XXSSS)o/ 5^ԩ޽{{yy1 WQQӟDJLL}/Y|;vLMMP !qrss ͛WZZ(UdrҊSLa['006Zuzf ٹscǎԲrJyy}444[͛7k֬ѣmi_N 8XYY?駟 :x6i&1޾ukU~V[[Kؓ'O@ Vhذaubbe!Ec/)|[utt?#G޽}~ۻB n޼IK%q *))эώAG#6h˗/xח @޿OlԩS?Huc7y|||Q_޽{iQ>>>\W]ٳeǎ(C|v +Vӧϱc8q1[[ۏ Z A VwϏmh***b8(C4 q)ӿׯ_?{]w"ϸBN>o>wŧ'e4fܠu !A|}}|~\\Λ7ښ x⅜\YYxyyy;;L62DllP9w6쪪//rssᇝ;wrqws=:uti999sٻw`!K,i 駨(Ko˵V"))iȑl={vҤIl+@wɂ2Ds>'Oٳhq]CBB˗_ZKpKe,6##Cm-(CHJ^(?ac57oaw=H%____#lࣦ!hQjjjMMM/8mСCH<ЎÃk"&&p}.^۷>]sIOO,۹| }ϟ BU-**|eee}yhI&m߾ 1!ST 6^م|:kj⯠¢?BXXX H===a%25-88!+9r䧟~ڿj/-۹ܻwOرc 옾g j7oBUҥKt͛78J޾} c 28A!Cm,Դ`++"#"tQ9iii6668q]ݻwHH*’%K~GU(~~~QJKK544?ƍGQ9?G ɓ'(!(![|@ i[f??QFUUUGCC݆ p:9\N#8^=zŅḹ%&&VWWC4 oC@el2%D haaPj׆LDDN```qq+޿';\PΚǍWSSsDLPN:.V7x'(]]]{왕M48R o#G9ǎ6ą qٸ~ !Ȕ!xɥlwء.''g/ 544O4爘OgwZXX`T<|^ u@@ xNII I +A}Zx^%|ӦM;ydmm-zLۆ(,,QBdf֬Y B!S6Dccݻ|3/ ZjUn(ؽ{A҂ Fdeʔ) 8GĔruTSll=5͝;WWW떁X{ѣGkii_v?ӧ4QFQBd&..N%DYYYcǎRŋ8&:pӧ[⨂dȑɌOI Lĝ=<ĉY%55U__?441)) Go# 0]\\= 2DOO/88SRR&M6"p߾};"30o>u/^.~tB ڡCӚLilltss:u* $UD@u7w\Cp<< ~=^ZZ3yyy\.7??'r [[[Vϟ?_ny߾}Å'/ ?ٳG ;w>!ȝ!utt`,:thΜ98ӳg p`⢫aÆw4GSSL]]] Teeex>~̘1#))I OZYY7{l֭[nj ɓ'MLL&OsLa666^^^uuOAмy,Y"w*Նv횹˗/q6mo8]TTE???--֛455xҤIԃ`Deee~ 6`p8_}Z8.{Aڈ# p8(cccuuuKJJp p`O?xYy<"c00 !HCEPC-׷o3g년g09Qʕ+utt<=={֭]hQ߾}o޼s̱777Ҍܹcgggmmꕋ/nW^]޽[0n̙tSa0ZXXȨ J!P6ԷPu:tѣoܸgϞrU&L111Tik.SSӲ2 }ĉ8ALzz!CvC+Ù3gιs}0`LG'Or/&&&L}u1 `qvvNJJƳ~0%D.w܉"/Jن̴tss+..9%daaF ̤/_JѣG3g^lڔ3GX|}}qd`b ;OJJ {Njiiٺu+8Ǵ=f !2jst>ʘx,Y"}g_<&$$#x]/ӦMQBӧ: !DQ QWW|r>J_{ .JtPFDDR),,\t)f͚cV3.\ 8A$24D#Lllȑ#tttlž?bgg'棱qVV2;v앗ϟ?j]vIs>oQAA LMMͣÇ(!"Ԇ Sfz\tGPee%Lӕ+WpZ=ܻw޹s'w[!(m m=d>~&۷oCL˃8!O<?~<$^cg222555Ξ=+Ç={(!i8!Ȟr!ZZZbccs:?tqvkUUU^6!>~4_g)(a W PEq7olٲeĈs΅Bu9vԇ"UWWCjbb";2}QF1~ ѝ;wY#~G5550) E W".NL"{JІ# v:}_ddd਒;all:N{ss]vի똘[[[. ˗71}||كr=(}}}a9ijjӓI oݺ'䨡aӦMڡ>|NTTThiirrr-[ƌ(-`GzwDa&F!!Dކb&Uk֬Gv*))8{!!!ׯQpӧC]&) a#N)\~] FT۫W Ş+W8K樨(xIII8'c/_:t(L~~> ?xp$=x(MIfWdhhh8uĉ.\Oѥ3g9rG #G"cmC=((o߾ݣ}y~p.^Ct沋/Ԅ2C3ZC1 .]D%iӦ!CΟ?ի]t=zdjjr{{c{6싟ܺu 6C~qd|⑑8P[[ёk**D5774̙38A!6Dqqb/~-S]]ݫWP-ׯ5==Vxx8l̒K:3c Z[|rƍ666|>? ڵk/=pB'Np͛7T__%ʋR0a† pTm߿?<;vnSאG1mmlKHHQB…  `C?>؆y@ i:|{|[[[mg/ \\߻wO><+**pH0aΎx zzziiiǏW>|={vxӖ1cB//e288XGG'66">_Le07 Y!Yc8J!2ú6uuuդ0Gv֭[vvv۵k0;)N)b0ׯRm…ׯ_\SSӫW/y.֭[{ΚG||<ő#GsV[XXQ'N@Aill<}# ~bP{n>}DBǢ6L༽Urʸqp Unڵ8b0#:]l,(SaBϒ3d"##݇ŋ߸qCCK.Af>='UVᨢ}Eۛb>֭[m%ϟ? rNNN=vQO"liC@fmm=guk^paĉ8NCCL1߽{;pB)o.uuu0ݸq#NرcĈt2zٺu,--]eʕk׮QϷm( 8*G|>?33'&,, 8Сv}}}bb"n*\jO<%D`{/Fa V!>}jhh-d777ɓO:꤬lժU<oڴiwi65k 8AX,''gڵy&~lmm۟{/SG166'dҤIǏQ_y 6668-w3g\`0MCܹsW^"oC\vMWWW'Eyq#۷oQSSSc##ѣG+pE.lٲ&v^566:t޽8AX);;{ڵ ӧϒ%Kdz?ի'd8  'Ϙ1cٲe8 :::0P+m[NCC,99YY aׯau !r6ѣGaBp PǏk<}}}UWMMM'Oj|r@(==NDDɓq̓'O֬Y{ŋrCO fϘÒ>B"Yqqq}9rd˖-NNNFFF3KZ$Dm-X`ʕ8J!LSd"228NN81c D~=z--- S>{+))Wͽ{#fC?033344 }6~,Xbݺu8ʴƾt)ѣϝ; Ą ft>č7޿СeggGݙ3g\\\>}f͚D TXX]VV(Ŵ!ZZZ( RS3'ON,^xӦM8J>//㣥&{{{9Cꦾرc8A-++kզ} UԊ-Ç)uyyy666Zڰ :::Yo`` ӧݻw ئǏak_ WA:u;zTjҥ8J!R@e޼yvvvUUU8~~ŋG7˗/H8-c0urr{bŊ)S(QعLׯ|:9Gd0ľ}\.k%ټy3RqT*++ /_,`bbٳ-++={O'񣆆 -A䬤D[['!9nC^_>KT~~"\paر8ʐr77-\Zw ˗ vCϞ=ۗSQ'w6K=uMMMwλh"%ȵ 4sqJJJrwwM4 ~GI; >dzr~~bVIOLL "G 366611 ~BFFF(RSS{׮] 'Op&i8J!LGbƍVVV>| "qo~zP%"[TTϟydF0j  D :$ճgtuuqB 6DccѣE:ݻ7~ L)nD׮] 3f77jܹsĈPƠ8ĉiܐFKK.\(֯_z݇VgώQ9f{{7ozU kÆ &LQА{{{ݹsRKMMD:lyxxb$3!-[6yd:P.9s@=90aݵkW~ZOm}UN'r"---00POO=PTĞ񢢢$ڻw/ W~ƍÇN:˜vرpBeիWG9`'N܌H޽{J/!oC$8A:;(njiP>}毦{쁙}! 0b@ -)j6k׮@9dccqF%٤`YkX]]pz\.7??'d ol)S sKIIqrrQF8ԩS8Gȗ311G !DR !`>|s0 ;Mzz:~H%-8J:t2dӦM/.99yҤI8ڹ;w̛7O.oJ`8}GӞKEg$XZ.\:tA`'9B:[BlCn ҥ?xGE0gΜ۷(7o褦B 5~2rM}}* m455]r6?y +  v޷H>2ER'|w;w Axqkkk C1jY;|qϥKH?Ht6VѣG8Ϝ9Ja֭8!GQhjjZf 2$8 88vy 8N<9`v\ӄY///%1ӆ7oqfݺu8*k׮(aT``q9993:: V{~qFU? /^ލ?ѣ]̃oܸahhTWWsJ mmmusgϞtuu|2Qzz:lK,ڵk(فQرcwppuNC:::tm !Dz !222`&QVVD48*4jRrm@ z[dzf͂RaҥTI"gΜ177kboOII.ڧ2KTPPPߢ;ŋqN%lٲE;޽(гeggNjmm-zʕ+l7400pss{)N|%1Iۆ./ԫv+ٳg6f䑑8JYD?~'e˖ikkϜ9SN :롦o߾׮]kTΝ3g=z; ;;7n`^^ސ!C\\\޾}R*FvqơK "]|vݵkܹsJnw+((8 0՝={6]7Gz XҶ!(舣DݓŠ[n(aB||?J׆ qH'88RRRƌc``>ofVQQ5O<SaFHs߳gOii)~gϞ-955U__ˆiVT3%`/8p?}>3ٳ|>]=<<$ P> x<͛UoW". nC!$oC~ZCC˗8A'-M rڃ<==Zreߊ_~Ga9y$ Ź"묭=}0v}љ _dЙ{ 5#//F +ŋP/]5;x~qwjL֬Yzje\wwwAJ$QGEلiHކEpHdԨQR֜p0x~n߾Gի ---??l<]>>>Qq|ĉ8J$5|݆ tuu]\\گ?k,IKh0gm@%qttܿ,Π!` ;ΡCpNuA }["==@[ZZvM[lB ;o޽{mhh3ةCܹs<o޽(ɓ k]reذaNMM9֭[쵫"ab޼yVQ")qTPmdaa!+aoff6tФ`f(ZP^/jummcڻn۶ |Ԝ8qbM0Ai%%%vvvPXŀiiiPkllٳΝ;`r}>;5~B=ǧ kSNё92MMMZ"I{V%+11 G4o޼M6(pTZZZ` UI\\\mm-544`V*5; cgPC1MMM ,r"ۦ;6m4\pgϞsss9U"??С0>t֭gϞ_YKKKU* 7|>8=xW^ao]WY466޽gB& 6X%H҆Mܾ}{ذa8* %::dWn`c2e NjX~=LIa/PNukIO?ܹ?&L uB# ~111>3iҤCї?ڂ ` QfkCCʗvGQ\.wϞ=Apuum}*GGGv>#}v]]K. ]I\sժU>|i3Os'!D'nD|>_+"z-L4qT|fff_-9BLLԩSqT޽;Lͅ^z)~',===֮hUPP`ii)(w8Ç >9YU ԓZΜ9cooiӦ /駟#ıuVx޽{Cy,sM}}C9~(**!t׮]uu ̠A\%b!N>=p@%RRD燣D40 x _6X:ì 06%Dhhh@u%=;/zzzFFFw^-BF:z(}͛7orm=TV1cF UXzz:χ}p8'nݺ#Ͱ婸xȑ...Ret&L055_qT`%ȟmɓ'ڵ G`!}Y^^I׫KfʕR~&7oΜ9{JC=z~;&x ȀWzBu33X[D,cƌMk4*Dd͂HFĎ^lɓ''&&⨂+ m phUP'NymܸQGGvv:Ά-'!s!Fy%LȰQXYY]|GIJKK%ʕ+ܓpYݹsyUWWgP*//p8U1ɗGxxxݻU?MzѢE\.7::VT1III8J! E9d+zb,}M2GIBBB$;!HeڵkfAԴn:>smXXXdeeڳg*!$ Y"222,,q ÏPf縹ꕪ$11q„ 8J! wޙ3g(a˗/qT|*yLQϷsttt=zU|ΝQš6$kC@}"9>>}j^:h -0\WpB:!Fѣ8JXxRx\6m4{l!T ]jCH )0s`=QUPPêڶ!$kCoFF@ jUΝ[ZZD^G !!%xhÆ 8*j Nct%%Lm 9p@ǏO>GU͛ϟ mi0ckhذaԴy*utthҢԞ>}ԄQSoի#GQI988|癩Ά gW χO G !$$$xxxਤ|~nn.N`/ĚojCtmϞ=9䘚⨊sNq԰ ! qPOOGۀٳ_~s˗}=u{ ׄiԆRmРA8Jd8*H%8*>禄~"~Ԇ]jj@  '(Bzu( aBA6p88{۷oW^ ru7.;;+ 0 ##G !DjC禄sJs7!:T__AZZΉ)222<<GUQSSqԳ ! M.==Gہ m۶ bΝ<xWq͛ 0BԆX~=#ży\,\PMfb6l#w87%,'Hm/z䉥G%5Wbb"˗/Gj؆fp ޽{q#V"***`<҂ӄ5T{k$HO6 ۇD6N>=qD‹/\nuu5Nwq8io% Nѵ]jC 0>|'$eee&7e unCJӆضm[PPvJYfY[[yT vvvÆ cbD&M&cBDjC%''( ݻ'jҥKƍQI'QCԆhURRdkksjiiѣ:4Ra*.. vP64iCqj8ڥX==k׮9z@ -++9'OV/I6)oݺDfFR444drJ!!Μ9׬Ym QUtiqNy6iCH_vMOO0*44n۶Bikk6/_Q"3ׯ_lJgҤI"^eMvI!!jjjdѰz"8Am Hӆ{u7oX[[Ϛ5*)//ʕ+8Gj6lQBHm]]]:M233ҹ{!4xD AԼ 339sTUUݫÆp*ů9 1|pn|X[[mcc&_G9sdڴi^9 o߿?BoDjC|nzA^^NHg{Q;!ڶ!`ڴicܢ-[l8r:4eejCH@6o||}Ʌ pݚ5k܀ !*L6CDnݺeaaRsvv޵k'?㨤jC>}z9Y>W^hdw1lP6ɓ'y<ަMqiO>QRRR+Y؆ztb!e"%%e„ 8*AISLQۗ\^^@ `vbB ADZZZ |߿O>3PqqJJm{F733DaçjO*[_|.tLUUUś8'0Y^Ko((] Cm⫮>|xQQΩ57n@hС2w !!!8*59?C޽QIɢ !󒒒nݺ <… OÇ mEϞ=~;l0C|fiFj ~>}@ퟹ÷[1"..NCi͚5|>̙38'KP?GUˋ/ey!`07o˰~iӦTle@SC?[;~R!5]p߿'Tٳgf̘>'Gii&̻pw "YYY8*5WW۷:矙:(ۆ;w>~J-[jρ{'ON!^~ {_j~ϝ]]N_"F(__%%%][L ËO|۷8`c͛8Gdȑ#nnn8JQ{; Ԇ`"--- m?44GG/^ (L9Ҿ/]j?),,l}سg>L[n_l ((hÅ+}pjsZ#S6i0e L^WU!>411QV !&]]Nw޿/𳵵ɓ+40B‰'qTjͫW644-T]RRR>}̙SVVsD>|p*/BHw .SNݷoJDGGfo8.]iB8?sLkm/;+vFuxbv^L֟۾Bq+ Uto%$$|J! jI!!!W!6Dgl@S'DG;..ٳg_5_s!eii 9uS櫫CCC\]pav%jC˗a"LQp1_ !Ew؁Ygx999~~~8?Ӟccc[#|Z^L?>g𚅧DΝ.1E 0Alwj~NNNm%Q Z>8T G ѭ[7a)ҷ!jkk{!L=zddd8zÆ S^'?~|ҤI8JQo; Ԇ`SSSY\9\SSӧO;wx#7c$fD+".QpBxrH޽mll?ǿ/x<ɦM>/UgWWX)w̭+|SNெwRٳgvss+//9ZVU!ׯ_QYmCӧ<|0}0_}qѩ!:u믿i}L#oC###8f(s`gpN :tHWWwɒ%UWW`C:NBX!Xg֭gQ&pȑ8`bȗ!0tҟ~cn݂.7ro300j+=pp0>3;; rMMM?u͛7۷3gBEn)i߅^3}+ʼҀ?gС111_vZ8ST gxeqd>j,qㆹ9o޼9"5j:::(!D0P-Z$[\z_~0! Uw9!X.---66޽{>BBBm/ ؒ6@tRUgΜQ6iCݻGe͛0]r;c ttt`illi"ZMMMuG,! ; Ԇ`Bmm튊 `3#(WEEE8!&jC\ZZw}_khh0 !ꂃ _s,hѢ;vਪ1c{!$Hի4ETPP`mm˶ܼxJ=ϒ///!2վ@m Q&rbPuҟJmmǏ8ӳݺuMMM82;Jx-é րvܹ}Ο?ٳx'_Tbڴi Ç ` uظehZRJw"!DFw j2eGUڇ`bk!R!͛gll,eee 4GUΝ;gΜ,m6GG޽{7.&&&''5KkCH6h6l؀RZZ:b6իWNNNVVV9"tq8* jCZYY}N0<OU/ LpTdԆ oCܿG&ϟ4i;; .ਂ@ahlll``0o޼!6jCJ,P-OEDDՉ9SJv` JIIQBia6rpwwWaaaSLQxe˖RSS9WZZCcélw{xiZC(S$"C25hPTԣIs4(**FMBIȐJ"%so:bY=\?:7Yp]zAUUdeT{'CCBNNNpaA& I8{jjj~~~Ν ;XIC*'>>֭[< 9uVF3gvx---_xcd;'+ A˕466>v0o***yzxdĈ8,6n8dȐ~ 6Yo߾|CHlHt-[F5 >}UvZxF*:ʎ92uTckk&R{ŏDXfȑ#ka6m4p@~83AAA8 ֯_I|ٳϞ=U>o=q}EEE)))ܹS+PR',@2R ﷳӈ낂*pqƪP|ɘ->󤧧s| p^rh{l-ϭc---= 9sEHDD… yyZ cǎ3gb[R\J0pG0N #Xa e׉'߿D֭[mllp|E]]8ϟ]f4O8^i3yz*JKK]]]p@> ViΝ;w̰*,qqq3fp8&Lضm3v==<脎No8'66… ***bZ69sFUUut&V6nܸ~z6!Xa e: Mhmm r)0B =z4? """;t'3'N@;p-X9K.ݽ{7Vwٰa?|c7nܰꏘ`+]f``@T?%==}ҤIXxx<222pe*@`RojӧO䨳,iooݓ1A S4cǎikk{jkkgϞ IGpU:IpTUU8qbܹ͛7߻wOj)}:uOYprry&V~!rW^A UPYYinnGHIIg$-q@  )C qO͈344djZZZ?-,,z3'{NAA}p>1@ddd]jն?sb\r ;E7nFFNN `ff QVV{5qqqXM6E*ӣcׯ_OSS dܹ5558b._U+ AKĴMTmmfJJ 0wq\ |}}-[VRRť4@eaaAFvӧO=<<\ *mznݺښ̚5ѣzQ/>_zYfϞ=Xw 윚U70c"=Դn: 8Vv Xa e2eʔ7bUDdeenb39qĸq/![[[ϟ?okkA_ڵkoߎվdccCСC/-'>>~ڴiH;ŝ TTTF NFFK=<<6lj={\L++hJIfS, zͭs02]y?@nmm ={444.^ѽ!!!a֬YX;-KURqƱaVwuuMJJ]TTfkkp9r ֭[XÇC՟acc#6RaݽB?~c`7(++:tLꌉYp!V ;02v횎F{xxaTUU-]t3g,,,aB7]LSō76l:ӧOJO/^ Q__?888--~JFFFmMJJ;w.VF^^\z$̙U =%,izo666'Nߪ^hD@!R7Ǫ$ٳ8 JKK!Uزe {OkkСC?Bm^^^lMMMAAAZZZO o_9Ub޽[___FF/_#66ݐ 'G|EKNNVMCC\GGGG<Ѣ(++۷}J X`!Mcc1cķRQQ>j޼y6mo]Ν;177ƪ477O}41X6FEE9;;s8H*%O*)--L,>s}}}ȂJ=Ymjj:oߎ9mϞ=,\֭[X%,@@Lfff3+ y5[[QFuquuuɞR%K`YXX] Zׯ_ wބ J=MKK D]]ҥKcǎ+WvVNRÏ`w444hhh"Cק"ׯ_WVV&;#tIyy|8R說QFA~L˗/K,c$G.ZR%##c͚5G$a…III?]&V ?ׯ߻w~z4Ypq/^8..hժUl^*++NjaaQ\\c ,*@`:R`E"7͛7.\rm[[[0@BO<ŴbYY#A<ׇVTЗ]v[GΜ9 &l۶-//=!!!tقR$'' 7W\SS[ZZbbl߾7U]]9sȶq)p0,< QlF@"..T|D+"ALL<Ҽ9; GRRVEԘk,ܐ;wL{DDV%E[[ݻwCCCX.qܹsXpɠKj,ҥK8@% ;* inn3g\Z#|rڴicǎ]M J `!}}}*:x<\迧NuUغumwYlٲeÆ X=[ի8@C+**p,XɓX3Ր͛7ONNbƍB, iJrpp}6VBJJqu4X'W3f -vKOVQQ^#((h׮]X%F@L8 :N8SYYRSUU5::ZT7ئ3f|'Upf7oZg*=gʔ)_ܿÁo߾ ޽{JjƹclmmJ={&$**J]] /iӦsŹzɓJ `!ݝ;wp@t=ztMM H ___.؈СC8@W^aUܽ{rx!ֱ>}!PJt&&&BѺu233?T3͊TRh E;>*"˗/Ç*9<<8@ĩS,1uϟ?9R䆍@ P )C0!3͛7txаe0LjoRUU+W7$PJKKr䳳utt-[+}}%VMQQQXX-Ù6mZll_tiҤIX%իWc"V)[O!++ ӧO=13qDoUC D`!˞={Ə/օmll\QWW~ |Z)-%j+99YUUɓ'8@IZ[[lBjX=ӟm.\Xx>4P`s={ѣGJ葺:EEaaaX;wե;g###Ff%%%8@cǎ)++Én ʲ}"Xa e&3wEaUt˗/8&N---srrpLlMdoHHHPWWsRZZ kUU~yȐ!X#TٳёḸ8p@rrrFGvՔcbb*BÂUjtjj*V۷o"rz͛7Ǐg^nhh kkk ˡCJbccit[[|FKKK ,uLwٿrtt45 BR`III޽f###V @_X~رc)KƏ/MRzOKKڵk!I&K˗/O>)//lڴi6,,$m۷xbz$99[gϞ`UX!w^\2uT2Peee8@&L>>ǏǪYl|{xx=rԩfff[}%&&R)F֭[X%􈍍wUTTlUigiiU~ ǏA]vA;y  \.W|[)J!Xa e1;߿}̙3AHHȸq |U3$nnnZȑ#`cccq ߾}{qOOOcee`?S555f,'1Erj?ڄ;H455\!5%cb333qw^^^->!Xa edccn:5)))***½*--}q3 LNUUUNNS\vdӦM`#"aĈ]%d{477bܸqCYY8@芖Pw{{{+**YZZI~$*/,,*{544Do5ݖdSANN˅ݐk``M+Fhxb&"Xa eQVV. /۪ߡ*t?%662҇ˀ+JF58Fl߾Ɔ̘1#..StҤѣGc#fڿ?V^sss344ɊKH ttAAASpE+ F WQQ{.%K17oVTTt^uVMM\`4VVVoƪ())QUUzxAAܫ)Qׯ_C.pw)̈́HJ_ ::zXϟwpp8{9s>ZZZ<=="uxꕍͤI(I*@`R |koo\  AMKKSSS۸q#dNNNPGW\QQQaA,YUQXXr{ׯ_٣cl[nx B:$¢ Æ *偦211nxwp@$$$x{{cU@?֓'Op@O< '!b pvZ? "eJ `!O^^^X 5khhht~Oݻ!Kxbc˗/|||X&&&R޽{}ڞbҤIt\@$TVV=ztƌg„ ۷o>H@jrJmjjjY8`msUiӦ J±cb/_|ܸq>|1π{hܹ> !к@`R 'O 9h+]:QF-^R=hill\pI_W#X67oބt7??8Ν;EPP333EEEoo.Y xJmϟUB7u(cmmUPr*//*YnqUU~FSSSppFzz:IwwK.a@ 0 )C-QcteH<.\c "66VYYY{Q ClWx3ӛjR]]})///999 M6eggKSXXHUL0ƍX%tXB|՟VEѣG*q|2|p0hY;lܼySCCP.ֶ@ H )C~ĉYhkk[~V^^u$?cƌǼ-<;x'~F!ۗ"IIIjjj/^orssFe+++3gΜǏ}'~233Ja^|r)Xe& {MEEƍƍê4HH)tuuI%B8̝;Ȩ$NVVtIX% B@L}}MҧA---۶mSVVg477kiiݹsǘ¬Y(cǎikkwmmm;vxBoAjkk}||߹~LfW^:u*V))X-nժUX)k`Ut|\*RSScp?8p@GG k"""$6K.-02 >lmm &zAvvƖ-[Ҟ>}jkkkccSRRcLƍ)!$Rkb2߿___p9;;33pOK7*144>V ]QUU(#FǪصkעE*q(m9x𠎎8@);88Hw*@` R tM]]e_e;vst{{ÇCCC?Ìz֬Y,{9iF\rTrjcxjΟ?><8iGiP>b7gM0!''߈&ئZCk. Xa eB񲶶 MPTA}v…7={[)ДÇB"H"pc :4;;̓'Ovp\]]|U~J4:+ 1c /OU+ Aϟ?O8qѢE=rvv$򮫠͛7qxzz=IoPB޽ VuŊK.LԆWgYlYJJ ]>Txx JIDUce>S\t"7n|o,XŅ+t@>pܻw/ ;v*%IOO*+^xDmm- /kdd$u/^|18|0ФOrÆ ]}Bp˗/_s{a@g+ A9MMMg쐲\Ǒ.r޷o۠>mA/_Jm&z&..c`#>}Cݻ}E+++;hkkzƌ⫠x SSSW\ٗX7WdRO°*6$8p@˩Z*** f QSS3mڴq 벟+ 7nˉ'=ydȑdZIVH+ZZZ [>sp9INaׯgd1zwssI& vիW ;v[ĉdmg :(++>>=O;"_>|0.<<DOaz466m,ΣGx<$c)f={[lٶmV   CA 糳 HN'O| +9hhh 02Λ7f;22: q %o޼Yl Whkky͚5DI@롣mbb-(ֹ={Ǐ;d] )1H0dn 144n…III̘__SNazwuu*+<<<$7bU ٳb޽+V ïD1ӧcǎ1cFg]L9y$ZZZ7nζ.oSNŇ:#Xa eB+͗,Ybll,m~ 555Ԏ=JV===YYYȜ Y,bٳg;Z UV N@E[YY3=ccc}ٳ/\U1m4IKnn.$~G|rtF;Q^R8i:::5*̠GO;F]-%!4C QPP:u*Xuuu1118@g+ A:ѣG8& aR1+&>} ׬#F@bbb"##_C]]p]Lﷸ533;w g n'$$0%)))XЪIr}qvvUZ{yƪx;vlzz:V%B||<ODEEU"͛Ы@{Ç6L$2TRRp84VH +x >BJGF@;Ӝ[nA@jnn~y%%̞=$5)=8 @;TH[|2i$p|߿555۶mrSL;+1WZ+++qo߾ŒdSVV4h !999&66;***tttF.bvvMƍ'''uI6?~|nn.V) lHvgΜϾ5VC}}Tׯc;֮]kiig '|z[޽{PQ^^߉0@9R -'L9pu nܸrʕNJ);fbbbjj &a۶m䇆v7RLJgr\hIDUϴµη/ p8A^WKuuD{{{Ccff.++[8EEEXp'K ?G< y왁VҥK%i&M*566 |冡;E@L{{ٳXyyyǏ S^^k.333H5CBBrrrǏQQQzzzJJJoÇWPP=*@a̙przxݻw;::r8###EEŬ,|AXy8p V)x}Wxxx$''cUHxJHX%􃊊 0*gΜ)Ct)CA@Lpp}?gAvZkҢdcƌQUUOII穐wޝ?>Cw\v weÓ'O!ŭ|fɒ%`Ouuuhhh};}}}$++UjW\^^^gghJ???W%POJ=r8z\ } )C~ ""ĤNLm>0U C3ݹ'O1bРAgvL^RRw\kH"##?s;v055m@V%&&cЉ+WQdիWK~t=kІTUUx 4\X%L.[\\;Db$VHDԥKS3g΄>,,,//OU3 ]z}]!,XMСC]]]!ymm퀀W>%Ǐ@t\ Rwwwh J}}4;vJzsN*;Νf+ԇֆ;Db$"F@OVV-))~s1UUUxV#xƍUV)((̝;ѣ8JBkW FΟ2xC:99ݻӧdH``=3z"޽FNN)61m4J˗cU5J2%VDDTTt뵵8VhmF! Xa eQVV&JA~~ݻqTVV}8WK ۷o̙ڃVTT¢s֬YzGYYV)$w@о`UaU"DGGϛ7aX"voo$`?F! Xa e]/~~~8  g[n٫Wp)8prTUUUMMM5,((sZ Olnnχr*SSS֧kJfH6x򥞞V͋/x<.uGaa!l>}cmmUmp@=9h D NNN+WAk"7OVHlݺƦO X?pႆիWqq@Js%K 6lAAA}]455t4]Eg+ Jqٲe8@Ϟ=300088߰êO__Wc }ϐӧO_ٳX CwH02IMMxѣGoذ=o޽9g]]]99ɓ'^EEE.+++00~֮S:뭭׮]c###JfRn{aU444 6 PPPm ؀`!XJ[[Î;p@khhܽ{XFSS .rrrVVV/޻wիW_|+233eeeFFF& p7ϊ=9B[XX`UGIOb#GvY~ w">#V bŋZZZUUU8`gj$#Xa eyɓ'SaxmZZקiǏ;rȪU 2hWIMM6)##3`8` v6OYrssǏUrKKKq}eeoe,//Ǫx|}}=hJ'F566 `aq VHܺuKMMM⠢յf899y޽ZZZ5jӢEmv3g 2dYYqGUH=zJCC#!!]q}in%)))666X%|gX?IwDZcǰ*:jkkJ3>>>^^^Xe,7 !#A 0 )ChI!YJOOװ0MM[n{ZZZ?WѣAg_3tPOOϠ ܣ ɸO|m^3aJoo}KJJ$ ;88ܾ}իb-cHqNlee{n`: %1S02Xp˱J 3۴i-/SvZ2 |=d0ʸSe sEEE</-- ?r=m}^NN8z>.ό3.]U  ZII rMMMOee%<lkIK$f$F"Xa evq5]]/_eӼy޼ycI&q\ H|||.^vZܣ I, t";;:eSNM6 oVZ%E¢E:'7Xϟ?'۸H hx<^11RI yVHE֪gff8q℆ٳgq=>6`YYY]] 6yXB!##7޽kkkU)LZAIÇ8@= ߻w/V% pZZZp@Pp[Vkbb"UH) CwHH C@,gʕX*ϟ?6l4"))IAA򺘘22dwŋ/^Āy<^qq15qDJJyy&`7666t%,,,44^LKPmIWtROOO2! $;ׯ; y#A 0 )C4==0---!!!ԟ,]?~ psmpѣGwmII (Emm->T H=;wNMM8@Ν;vvvX-*9v؄ 貏rLLL`` V%ɓ'OUQUC2~0t;W )C4B@Ok׮HKKSWW߼yskk+zB\t ,«Wp@"H=8qBKK),:iYFFVMMM Ǔ}"11q޼yX8_|f7npuu*A@KENN0~R VHڵ UWW[ZZ>~CWǐaÆfaŊ|K,8p #GG>̙3ӟC.?&L8xDjbG[`7ovttĪᯨW`7ү]:Yŋ#""o=<{'N.{>@WWڽ{ !|~w:滖3gBsiw-Dtî]~}6FCYfݺuXe7W^oll  Vݻwnj~U4 usscvRйC@ѣg2߿ςavZNNX5550)))oo?2û4'"AF@;R`>sݼy3Vɛ7o\\\Ȱ!諫S ]xDD?FFFv(~~~-_} :tΕR^^3::#h"&!}w[s玽=V% $`v铦۷q<\__RH?-]lV Ҡr``}5 u?!ovZrǟ?ÇoH|Ÿc ̉\ǷGF@5R`8^E "zO_]ō7G!SRR~iP޽{Wx?.Y\\N&!`WeT)#==l\wɒ%X<555)`UJ@ߴtR;w_)QYY*buaY"55u/ (#PZZ[0U&@#A  )C0H{J"ƍGd OokDu"!Rv ɉ dggCUw^,uu:immUQQQ v\rEOOӧO8j~V/_?o7 A P )C0k׮3xѣGɰ^WWݤ =`&}I111ŋ3 ,yM:Xƽ{[hhh2F`+6C4P''tqqIJJj? |c0'''p/ˁ9t?)#**.\Pjg\ܰ ݅dbb"aܡ?|xyX|98Ȣ<oзF>u`?"1bؽ{_;y2|ttD;@`<R`,IIIY9\Ea!իu茡ذsΝ?҃ Q]]=o<(_---c)!koo1֐+*V $\.8hllTPP6ӦMr V ***8@s6l؀;џz`&~ӆ`?o0=8mMMgΜP Έ`Y֭[ ??ܸqXXe+[lqww*ݘ9sŋ*=*,&&&X%P_@.ԦÇǏC@`R`&Щw~ŋϟ?c,ed? ><$$=H ] (,,;v,V%yJJ VYI^^ǫѣG*= ̓TxDˋ/TTTJKKqb$$$𷎆~vސ6Cva}2>|rJhh3ן?~LLLQQ!Xa ei``fUHܨ $]E܃$y08t ŪDx1%|t̨؆pHغukPPV222d99x𠝝6&{s~WGGGȄ<==wힷaaG;vlcƌ9r͛oܸ `!ӏ9UiR%%;vrW n޼ydXׯ*+$1SBCC*UJKKER𪭭*J>|$ &.]&E^^^CCcƌ;wLMM=,7 !*#ի^9::>lŊIIIdi02xF`+o16rW!* &^|ŋX?_~k`7oބSOГC-[ X#ϟ?BTTT$<ݻw/_޼yiӔM>}۶mkXnCF5//o߾}Tuuu}}}v‡4G@L|ؖ-[z\=jc,wpɓ'</99ׯutt*~222 >JKKr///JÇϛ7}$;;kΜ9X)?~LII UMMMAA-44Kga  p`V}VUU6ȑ#O>4D@W"&аn:T)X*$Oaa!eRfX?~~~\b ҙTJHsdӧO*z477v[n޼1k,BBB.\+|`a  'NX`6xs:t8&VHQ$$$aЉǏO8*// ]Eff/#++;w&qݻaU4665/744?~@{U 2a)yH} B難PVVq7ZZZ :|ٲeqqqEEEmmm$ CwP *`b'N8bĈI&A;%(`!D +͛M [RR;UvP[[wq\H}}}Ԏ=JBΟ?g~eX'?i:sN;;;*9"> 57dcL6X%Pc….Çǎfaa1tP33EEGGS Cwt jR__Օ+W=Z^^366V|B Xa e栯Ov[[[S@cQSSnRuַ""X@{{{SSt|ʤɓ'555Akk*N?sL{x^99:J?ann^? -V QMMm˖-&L6l󣢢1,T3 THPJpqsr]Z{'-!IDAT}^kwkgwZgI TLNJGIR4Q!$JC**t rP6u3ό癹>}<3}]}nݺիRY}FR½{JCZUUE4`rrr04>m?i x;N۷o Eٳf͢۶XÇe:7ׯ$硌7:TeW^fdd)e;!)z}A%^xAxӧ7l0uTyyy <Jz=`Ao߾BB >"ķ iYEd !ݻw]TeeeMMMkkϓjgDHFH؂l6ZfC{RRRHB RE(F---JD0`B8z(={hʔ) 6D 3b稨(UU@nݿ¥K !|!i <==M&$dazl&;~O:EhR ;w⾘E*" ݿ?dÆ cX6666m:spˎ8!S#YYYvvvX!쉉zB9,CHIIIPħO;Ι3Ν;wwHRj!wmQ۷.^' bĈSn˗,YOR #UP^^rXppmHׯs8#G2dׯJ%E ))i pUc !I?+WtݽpCS`PqU[[9^bb"i޽?~/i`Ljg744 NNN\TJ3 1|WQf``4+FFF/$ƍt[ZZ455i1SXti\\"4SNZ`Ś?~ZZ(D+ X`<:::y3M⢡FpYmg7TiƳgTUU/ҥKfLRUUuYapppسgONN]=%TTT$z҄%L288<Ν;},P+ X`6666eeejjjv-+44TEN5k444bbbHTWW/\^26ﮧE{ll,RʭEAAzouH~XXXjgDFF\Te􀀀s窪…=o޼z52eʹsHiI7EEE})++&''| !MXX%#9W6ر.ϟ}o?~eii)ij&f`\<}_[[{ܸqGd !;Gg*++ %gΝDаb HHTTTؘ6qRZZ ͅ Hپ}]H"16?Sl.KIΈ Ç;va/88X˼CKF7o%T]]fIU4!K'U)I.^8}tR+aX,V Ӝ\AAaڴi)))="[#켋HgϞYFIIi˖-M !̨Q߿Ot 6}-iӧO,XF(ݻw0r8ݣj*+++f y<7HUZصkmЄ[n3Ti XR|Zpp0wŋlii㓐@mt'O*ƒ)#F$FuuΝ;qe\_a2+#TQSSﯮnggF .]daaA˗O.vyyy _~^7o'Ui҇X'H0&H42RRYf'no[:|rƎf͚'NRECDƍ&L Ummm ,0'O@0}$:B!,C06 )"KkkkBB$gIߊ?~TUU$ ǧO> cI:s挮Kuu5i8'''{{{q#(Yc:ՙ _"ҘFOkii=} \RR׆GTTTqq1B[}*"c|)==}ܹʫWfNp t1'dyÇwtt|i'BVCz R ''hRe8/^TWW}6i@q&=yDCCcӧO]]]Ǐ?}t N`߾}/ ۗT?0ԔT >~x!+;x ,C0)ꠠ H ll-[vAR>|عs2/I(---v7 FYHfIıdǫW a$ ΌZ~~>i@e̘1ʺ6Yffkjj.Z… +[7ؐ*t 0h>|Y~ { off'q.|3bv}UU IIIR\]]㩚U__oddB$ӧIDEE9;;*IKKSWWL>ږhhh8|PPޔ)SΝ; hCi[7$''/\TD?S~e=tCMMݻ fnnI۷ot{_U+ׯM"06lth2B_'''k/_~ULKr8R+mmmǎ1꺺:L) ...,SIظqIANHEСC)< `xzz9rT% ľl8쬯///?e+**֟kࢹ+k^xqԩd Dp hWXiZZiCz ~e=Nmmmhhn@@5ə0aқׯ_CR 6yC``ECR9|JJ i~~~C6mڴuVRe&{1bӧOI"0p1i$U1ʛ7o|ۢOz!͆ u0~놙3gfff*+WtuuWZ]~e==ǏxӦM0f*s!_fsˤC:99=z L^^|ڂ NΟ??k,R (뭬ϟOឬLJJʂ HU(2 .@=sLq'eeee#FHMM%m} dR88ޞf+++e痖&+Gס_~ɤ:0~ Qj#٪őD<=,C0)Dz666⻹eeeu.;w!Uаzj8 ܭgHs߿а4PpwO81##TiÇ.\;҆Px*HCBB  ֦MΜ9C=ۺj*ҀHuEyy"xxȑ#n݊p$2Bz/^DDDXZZxxx/ԤfٳϞ=K(0;v@ܣaqq1<4Л$55HҀ@SS9 C ߿m14a…0ߺ"?? REheeh"F/w! һTVVM4b-]4--Mk7ŋ*-L2ۤRjjjVX~a_~ĉb]d̘1n"/gR1חF$ p777ccc999 uro1bÇɧў'$$nr0~늌9s*WWWf{} a`O!4RHkkkp¤$ WJ>}ԄwIȑ#ϝ;Gں~q&nybmm-i+<011(GO⨨+Wə{yy6n8&)^UU舛H=uE\\ҥKIA=zD{X`0Shhh8qĬYX,ܹsccc_zE6!mBIa]@秬sNq,G:u \\\7=J伧IÁg*]QSS4 ]W]ii)85k֌;VNNdp~qꬭmNNқ ]]]xp!ҌHuw[ a Ǐgeee~e=H!tttdXӧO?z(ϙ3ɓ*TVV.^FAZź"w ђ%KHy-Z$֮]N]>r|@?Kݻ "?pXZZ<rߓԩSJo >Cǟ_,p7"%UG'\ǏSRR|}}jWPP),,ʕ+__+**TͲeˎ9B\9_,"7^xի:cDIq `qF^^ƍ===\qA,| &r###uBĺkjjv?ҒCÇyw($ !f;::=zW͛gjjQ<<--m۶mvvvٓ-[[޽T 8{q/\n0~6=d6}}Ҁ2BJEEž} 9qq넫ZiӦ=_~mnn+xӧccco;|⅒һwx v؁ ~KzzΝ;Ν gf޼y'uuudk_i&R1pa+++wL5Zlٟ Ro}lggg;@cҀ 2BNccc||%$d\~2dR !K̊9w3Ȉ$$''ף2::ٳg^reʔ)QQQ͛7W^ W'ok׮ḍG༙qۑ'wﶷRRRs痖&nkLꘄINN3gyy9wJDZpEaaa  \-9b`` Kv{X`0Sښnݺ#F˗C\Ç t˗ݻÇ:/սI8]Mɓ'***322z= dlan߾!<]xq^^ޟZpPbO@pe8 @7)((X[[HJcpxMMMqOBzLLL a Ç  CA$ƍM)a`O!RɣG>a„>}暙9< gѡ][Q"MMM=S!m}QJEEŜ9sSYY& tqA7|DMM :tܹ}WӧqG @hѢ,6=`6ȦNN7iH5 En޼Oֆ@>O6] *̿{Ʈ]"Gzzz7GgDVٳC*]ILL?>!>|H chh|y8X&Fxfw,S2Bdӧ?^^^n߾} ONoFڨӧOQQQꐎ644t2==l0Ӂ~j Ѐ}ߋ5ǏC8xJLܹvqA`bb^TT$஺͛7 JW<<<"""H2ûp8\'hiiA@#33SWWw2=,C0)Dրݪ*l^@YYy…G}џ#|=䓤"޾}{==_633f -0?'8tzG7s1`l1662eX+><11gҤIC o+MMMdk #&nw%Ϸ#q_A` O>!tcݺu... 2BdUVڵT?otRuuuWXYVmm-Nziii4RCCY5 rKN%99{knܸ̭A~ݳt ɓԍ7Z[[X,===GGK.uUb4p]u x9d\.qիW*"`)fffpf`h m477#"" DuuuZZ̙3e4.J?㝜H3ܭaN`ɤAݻwddc,{X`0Sls8H!3<9!!Ax]TUUWX3Ĺ:::/_ eXuuudnyɐ!CAǏ _ ^^^***pp>I3ȀO;o{,U\T2"f?~L0yK.*|hb6rDHF@@g^#_ɓo߂߾ui@2BdB ߏtvvYhQxxxIII[[ٔ444lٲEYYãG @dD\pZx ĵk׾|--ʕ+aaaNNN#FPPP:uӧzotRRҌ{u͛;] a:4d*%#%'0Ǐ[a`O!2{hh( Ç! YrQmmm!aT|{滺OaHK|||,Y[N߿p8/622SQQy&BMֲYQ2RZ!k' "vznn.D{2BdQF]zT)߻wK=QݻwÙ?~II iСCS K,!Gr"`ɥ2VXwKvv={ VTTyKUUm޾}+//OaҥG!.ˌ"=[<-MgwѣG… g֭[ӦM͛70‚B{\u u x׃όJofcc޽3믿駟`\v-} ħO`bbÆ AAAt2BH&UTT*++Ishnn}vtt4 ꖖ,KSSs֬YqqqŒ=뮮]$bϟ's ݿeA:dȐ@x尰0''Ç+((@ Q#pr|0;~G3Үϟ?oBBBxWFgϞ :m,7t.oq8;:C;qbN :^[ 8СCh)R_x"Dnt2BGDeeeFFƾ}-Zdjj ̎AAA@ a` 4Aޔn ݯ_Ďkjjnkhhpu%$$9h0UUI{n߾ vMUUo0A>&>:vU0p=1bDvvW?য়~﹣9c= +׏[t+L{:Wgի;^@PSS P55phӧO;|go}aÆ}wyVpp0|M>}m_^^o۷НNڊ2BB]]ߴO>ݽ{aۏ9r ,رc襥T-0/yoߒ.Cl_1455k^C\l2ǎv'Nyhoo'aH@м~zR \sDy& Bgo߆㻙e.6l_1`&>{\#FMb<ٵp #K333x52W2RRRܹ-!Ătߘ.QxϞ=}ݾ}yVqq_[RRO6^jɓ'!|JNN_=<<&.Bw:?vvv養 `ˌ3HUziiiFVHG=`{{M6ƍziq)-,ѣ>}5b!&"mm|o߾zzz9rBzNGf* |KNΝ;733TBG&3SU?7X#Fp?F궶ᶟ4i̙3suu5Н6RRU:U 6455;^2DBBW0"pرoܥ%xStX-[g/Zkjj~{H#""~k!BJ~ { A\paر*K@ t} g`*--}=شi]7 =pD4h iiiD8f̘A΅.RE 7^H%x+JFziV %;xW0ß!Q> x/''?#B{\+H/pn ׎W qɎ2DzzQ󼏎'eC2>>/_vzmm~6**{Cnϟw(Np!OOORE{X`0S¥]__ƍAɱc6mdoo?zhH555'OgϞb/>}4m4EEEەNZRR; ٭[ \Xhn~ vӧ89s^ \\\x ={6~3nK hYCaFڻvr7!n_N]x)DngC >}?Μ988%6:jժÇjo.ħa٤0oX`(7r׆8[rt^,׆.Q`x[>袣F?~ns¹7et@><tyywޑ*g! =T/q3g4556]]A,2qʕ)SjQ]]' UUU0o.C SSS?>wb>}wJIcN:wYkkooouj;byT? `REӻL P[[[VVƝJyleYѮ_|IyƟR {wĸ <4hA;dȐ%K0vqǮ~yN[1c ! B[[ȑ#HC[6lذw^---++ԏ?H&&&*)Y,VCCi7nXXX*(hZ6+zFJdDAN߽{+H~e= rĉޝ-0 ]ǏVVV!!!&UI1uTRq FCMֲY3Rwȗ[ZZl{H5~ { AUGG4 =A ,[byxxܹsׄH ޽2dJ+V>>="9rdׯ_Dq 777RE7YfSLdDA2ħOFA/ `G455ihhܽ{4 ZVVFLիW!TWWo۶MMMg]2 &ܼyTϰaķHAAرcIa4߶l>tRJyy9!O32]{:z5" `8pThii[%ŋɓ'/]DO4779r4&&W@Dg̙*fJJJFA!Cf̅[NNN\\FCCC)IVZCy ֭[e~$ ՞޽{fI"~ { Az ZZZn" `@6CޫN^~=ډ'\."ubgjjh! G LJ Fsb6g{FQq8q۷aTԲL!ud養(BWSS˃ 4u #а[[[(*!kZggi$/nnݺrFR b+UUnoߪ(b!k pmm-"D+WnذF =lC0Bcdd$'/566a #退*ʚ:u*MXXĮPVVQ:P]]p`!ʐVss3L!{؆`0)0qDqI HjZUU1?&$ȑ#zzz։wI]AA$o?e=c0@$钓`!ttt$%%:mÑBHd8qĄ@R"V\\fx+WXXX8qB27DԄQ񨩩QRRޣaŊQQQ0@T`!رFѰ=lC0BBJII122M ݹsfdݽ{YUULR?¨x?~\O뇉Td޼yxJD/]F! G !Qy5YE|&Y[[oݺ&pmMM͈4'ǃQ:{ߖ-[8zuLf`!iKOO'F@B{؆`0)DQvD%֚V__?~0Ǐ]\\<88@"sQ9sHQQQ+VQD=:UTThhh(BVWWGL@ X `8RYq״*mmmq1.ҥK]1 L` ݎ;CJBKK L_VV֔)S`QOܵr񮮮0TΝ;&=lC0BaaaAjYs87ne^|닧. )44tӦM0*:0*555l6F$PhaÆEH ҒYmÑBH:;;njC2L25-//OUUyf˖-\. ={v0*:NNNR,?3^8uԲ{."$=dbPQQHt! G !z&P*$VlvAALP~Ϟ=dxO4-;[IAAA8qb^^"IQ$BR:vx@"%X `8RX$G#ɚveuu/^f-ZTRR󲲲N "͛D)G69990p\///E&X `8RCaaBMM L 1pM=z7o`b>|8tvww';*L8;;¨ڵk0&ZGu(BRray#X `8Rڵk1x'LL9K~~>L_ԄQQhnnVPPC0&ZG?Fb=}{؆`0)O< $NRi6m& {^{{{xxǏa}O?(x<'''L4 FݤRhKV}0555mÑBH|"""MHRȴxŊ0=iw}'uvv^AAFݤRhPRR2zhEH\]]׮] Hl! G !244LLL $6ҪidFB>9 kǏϟ?/[ԟGYYFŤUĉb}z.Bta q=lC0Bb6f̘@!ŚF"f9 jxx8Yhѳg`ZB|'OͅQD1):<{,"$Y@YYY H! G !qsuuݻw/"nMkmm2e/L0 E8l2=T,--ݻ¡ ŋ(tkihh(BTSSH! G !qTQQdH566N0A6b\.3{씔=Wd{ٹs'"II9"$A햖AAA0OapΝ;FP `׭[<F@3;Z'yΝ[h"$A/ƝPZ! G ! {UJ%5RWW7** &ݻw~~~JJJ۶mi9}1%Wdd Jj-_0|x0:\ΝsuuQRRRz"ZQU$LKK FUU mÑBHbjkkY,ӧOam5-??f߼y&`ܹ:::111===0-sΜ9b Yf]zFBAC[cǎ5~2vz# =lC0BtQ[[^@"BaM۷aB&]~dKBBatXTTT:::`B<<

| Ç, H! G !iYv-Y(#jٳg=gϞ3ŋ04***޽ѡ={6R$??FQD%11Fϟ㟘Hap&MM;wSjɓ'uuu_~ 2*''j0BbkkKXϿr "*1։ˍQĠO> u #%''=O4!մH2dŋWvvvf1,,,NEEWnݺE =`Νy>]]]!!!gŝ;wE.**իW}>~̌|PRR6;n>|P[[.p#Z]|F}ayIEHh7n`GapMxxT9giӦNeff1ٳg> K jjj^xIIIP***TTT6Ouuu[[[Ǎgll Çd? , h?~̠KH:::"ۼyĉDu #mz{{a}FLL )e YYY믿.--2JPPرckjj`B1% ;;77ooo~Auƍ.Zt)ڵ nIBB`ǎ,TXH䗅QD]~}ԩ0{ɤbmÑBBdprssa}?Jj<{l5~uu5Lȟ(**[qqqd"ebbB^?7|3rȯsWdǎ#?ڵkweii$ IG~N) d~uEhzzz/_ncc`.mÑBNIIIzzzx۷jfkkߓF:zׯ_Ä\tss!/ovĈp;w ٯ444~رcFm~GEEI&}7S}}}q_B"*g˃QDٞuvv*++cJWWעE}+mÑBZk֬Q42g%+D&FFFp &}%Lȫ4wrr" f2?Wk׮`_uXXܢ///4uwwp"-55F2Cpvv7o^GG!F{؆`0)njjz)@quu Ξ= w1/^"ʠ eeofĈcƌ:b}dopE̙3p m۶-88Fed{reDYf=D1`6H!D2UU|@jkk*8Lq)---Ky,[ n-R/^w&L_311T***0M=1)3rBoebJ4$F[[ۥK|c`4xu #.\w_Y?ٻw/LȊhuu"u'_ }H2믣`l6y,--zϘ1Fe-󷤤$ZQ׷3 =lC0B[v… a U:!/^p8= QUUׯ\rڴiJJJ#F9rXoQQQ1jԨ/66l (//ՅQDf/+++#u0Lap_GGa!w],YC⑑OIIJJbٲimm]xiEE!9#X `8RɆ^(vH( Rmjii !&nC$%%999(l>L$_[[+ $ =lC0B2ZGG'66&244ܺu+LPb26ĝ;wlmmaQCoϞ=p8tIQmm3ƥ{؆`0)dӧO.%%&^d m]6 QPP`bb20[j( uuݻw{u #LzźwLH!63f455`BL݆x&"j0}VWWow>ݤ^ihhܺu HapU7op80!q؆MOOφ `NJ !&nC4775 F5>۽{5k`+r0qppx-!`6H!$xگ_QlٿquPd}tҿo_}Exx8Y|_?O'NɪU?F:p@w//455IRB6/46D}}ȑ#ɰ^n]Oq=A 15?jhhܻw")OOOjee |@ܹG";5kD 1iҤ(@[[? 8ydE򤷷Y,;AȊUKK hC3blH#$"ؘp`BچXh?肂`[ǓZrrrMMҜ9s_%ӟt!~dי#3m_>>KZFb{´Ç7 tN?wƍ\cS...cccɎw_hC 縸/1~E@6̙3i` "\YYF‚"`ɍj{{{kk늊 C޽Kv 윐>K.={,"dQ@@~R"˖-[9sFYUUE"^K,W}YQ6JSNJjm?O!!! m{`/|77r ǏWɎwѾobŊ/!CMMW_}"##׿}*dV4o<E(??? F^476͛7ɂ/ۿ Q[[?WJJ ?B>KȖV}YQ6/O%3!lC;y<~foW^w¾ov|XxSSSg69*;F}:::X, !K&|{m-{}Wɏ 1B6_tt4;uL/_/ `Zo˸/_}}}0mooǏ('O&LKLL433S!Pjjχ`A700=z>\.7<<FB+ h=;cHmE(EvuuujjjvvvW*n<(Fjb٧O jsedff߃6w9`_ >D_ tر-W\9PUU?ȑ#}AiCNśQ^~&Lknn&  >}+W`sm'O.YF} ^[[&B+88X___2RX`?Ϙ[-_?hǎǹs ɗޢr奐d dz l"o qg_zر|3fdA¸BHHG!K#!/^_}Y;=9ߴiSvrܾv߿ .>EEDCCNF# Ϗ9O!Y x؆`55,!n߾'O߿/HiC>|:0(0uT]i3n8|W\{,Fs Qww}`/!jkk***`͛عDH$%%„`B9%sۆ_$v>B$iC9rd0(Ƞg(9<2s QggΝ;a׾Ԇ 'M&aH{{v@ɴ\2{8{,LmjUxⅱ1%C q)o^|ღ{`ɐW^͚5k„ b=ɏ9shC/^ttt\ddBHcim2)Sœp !&´!Ν;HɤFik.OOOE+,,b8p|  ݫW,--`b ݆ BCCݻa'N L{+D;6,%ǏaN؆ad2Hv-̰JׯUTT`Ʉ\SSS{{{ 6DFHMM%;AAAL dڇg͚sÅmٓ`b !&¬W((6KLLQ*yxxsa\.WCCŋ0аtvvܹnHo A>}eee!BNݫW633a6L***3f̆ D!D6DrrܹsaIUGGǨQplNNVkk+L ;{,'OL<944hm_>N0NV#nnn8!D"!d#899Y[[ ߱6ӆHOOQ$Un"hOooOB4((( 0аtttڵWO>AZ ͛7aQ… &L>#$oRRRTUUϜ9CmFo߆6ӆȘ9s&" fL%&&&ʊ@ݻիW쨨(V$* 'N|xL "+Ç+++[br|r߿Nٳvvv DT33'Ob!4w\R߽{sm62w#{ixx8UXUUs@𯌄C^w´!Ұ A/_X,a’dE O&EEE0а:|rrp߆ ^xakkLUEDkk+9dffBHF.%Bb2`耿|!saLXs͚52ىx IJ\\0Yf( Rf&Θ1C[[ҥK0аTWW F|+{hh)SP$]UUU+V={'O`!$\DUU5::&,vr+ ?X(-Sߚ|||`Bo~9W7o="155ʂQٳga16FڳgO[[L#4t^Z~ŵk`N8¶!jkk/_ T(..vtt $'JJJ,Ys}RZZjllOY;qℹ9OqƜ9s`IӧOՇ4; ;u^\^ҥK`'Kӧ ̉h|SNݻw/ߘ";;LS<<< a!$sHqr0'd?Yj6H̝;wΝ0JdGGGER#Imm˗/a16ӦM͚5ٙaNtDنEQQQ'O>y$=rqq7o8I p႒R``ట%LMMMS1q~:9(V 9rF000HJJ9ԁI&´ 9eʔ&(޶tR;;D9t"$&L`=ɉ'VVVœYYYnnnn݂9D'NL2e7oī1dX0P?_w5klr k(S;k"YR`Ѥ#44C~~>L#4ֳgxp it 6XYY9rȱjƍx!B2uzb̛7OE.d2􉏏Q$Y/_Q:tvvZXXmKH9uꔆƴi޽ ߟ9sޞz-Bӆkmm={ݢE+11qidyup8+WBddr srqq1LP̯/_H^jjjJFzzzx<ޘ1c333a/9sɓJŋp :H ѧdӦM&M ;G0ȇΝkcc^]] @1Ѓ <==aOXGG򲣣&hr1oooE֦K֭[dFFFrE 'ON><4wPцtbbbV߿?uG^[ ̡7lؠ ---k׮ҒmӦM0JPBٶ{nwwwCMMzFFL >ٵk`(**ڷoMHH݇>!HSNuuut>N>={lb ,,,[ #33SWW4C_ Ekjj5`IDEE vuuڒE L $g}}}y&L#$3o޸qyΜ9ym>Biaadɒd[ Zx eggwwwÍBkmmݺu+>{,!Iss5k`NFeffjhh&sν{(9s(ȔlܹޱB޴EDDJ2{l?=ѯ̙3nnn+VHHHCuOAA_ڵkp D+2XɫV266eb!9oaa1mڴ2C蓌 :-b֭0J-[:tFE„ |ի 0$188f>y;''gǎ'NlC31 чLwM%&&,AAASL&<BV===***sh WE:u &(n:|~byE*TUU=zHtҥ0GꪧA>.}^xqa;;3f1Y<== /^L^Z ٙrsrr`O紈gϞeӧOaB?** F8Iad{y@D溾{$911qڵƍ:ujPPle6DӧOϙ3rYYY6yM6m„ :߸qÇp#M^j*j/GR'?Eվmsŋ(+++ EvN;vϟs\%%%??7o4cmmmd%khh}ڵ,b|O{{;lذ\\\ȴ{CJJԩSw]&v!&RoUUU=JQ̮WZ%ۧE̟?.`T._ HlȌf~&(b 21RJEvv6X={䈏nH 6mژ1c-[SUUuӆ诼̌g͚5q-[dddtttŪcccɁs3g B)))!P##[nBOLLL9Y{ڞxuEQ[[A 302e#ːc;vя3@"Avww?qℜ[6}RSS}}}\988===p;D/_FGG/]rŖBIHHsuuŋ]ܹsgܸqNNNt], 9}LEbmggk.>WHһwLUMMƍ0Ikk+ä8L6M[[{ǎ+..+oC+W̛7/<<nV^^~%K鬭m@@)}/BA&^;wķ}Ѐ:;;N̷ezinn.6ϬY(|+%%ۭD\.WAAaժUԔ[߿OLLܼyɓ')1 ߋ/-ZdhhH%GII ѭo|7AHȫrŚ0G@{`:;;(y~g`Zxx1ol2ccc{{[n Byyy ,ן i+))!/+WW^mٲEEE5'',?QCCc&7EmϪ"{ۚ5kfaaA/^$unVSSyfkkk]]]R58.co!D!;rYlO4 0a̙3?sE4dŧfSxAYYՕ8{l2l#+ ggg555\~=YVTTMѰ`bPȎgϞYfFHHHvvv[[Q,?~|˗;3<<<77!1inn޽{MyY3:LR?@$Ȁ I!E"__|&\[[;::&*++wI}ʔ)<IeBddҥKǍG^Mnnnaaa999LlC GQQQTTʕ+MMM1Rh IKKKVVÇ]]]hrǏ?z0۷o}||X,VPP^UUU͟?0@}~ׯ`˗/ܹs0!m%%%d;}4L !tww_vQYYyÆ [XXMr檪3gOLL$55lC͛NNN3fz*9PM޽{w}9;;蘚._<"""77ÇpkЕ/ZHMM-<<}Hkkk{zzVWWHd4~xEB{9?~&0 2а:wdӧOy<5"In "VUU}v{{{vؑ7fߧ߿EWWȈ̌233Aϟ?>6#ЀZ[[qSIIi߾}~ڑߙKN0SRRN999l6;)) &ܸqٙ Bbޢ8___[[[R LEp/@m*++;<6zy\znАh"###MMMh^r͎0r80/^ 5VWW1a91"55F}:s/>uy%wwwEum6M0!m|1LAkoopٳ׮]K&p De˖q$ 6={fww7D lCHSee%9ٳydr9E^TpkD+WߟL"5BVM'$$o`# +(([đE0lյr &]Tݻe@FLAW^%m2PQQ={6dw`lCPL–-[F-d;w\???R $^ Rss?N666dcjjIVYɯ__+..^p={D'Zd nʸ.ٙ\YyxdIάcoٲe0*8h޼ycmmsRu;x=== d@bեŹgCCC2R{!3dl6#͛7ɓ'khh`cի ,055UVV633ؿBBBqq1 #kΙ3g4Wdk.9~dW\Q48< 5g޽0'U'N`ٙ0euuu2=>vC,#c߿O 6L>]EE  'O3 !dMuuuJJ \ܜ'MLַEEEfk׮xyyYXXio!Bx<,br0wiF0ӧy~ݭ[` BTT*9„T ȔЯ޽@WW7 @/79䨽eGGG---EEEu;v,''gr͛7鑑ׯ'36R͛G!f2iiiG(3ڮ^:44K{8DJJٵkml#iyԩSqdijjOOOyyydEMpdQs蓲{.>xnĬɓ'۷o??Hʝ7N؆GdbdZommrʃ^zOb2yz l&LPPP S⢢kkk!LO>]n… sssaɥD"ߩWeI~~>\.UOm=|*ٽa}TQQL=6lɁ[ hll>an޼yܹ?ӤI,YrС$YN^?rVE!K}RIX,V||}u?##۷k:lC!&R%@v%ogooOVNLL,,,aTAAիW999ޑ#GKKKLxDXRyH [ yBLO9ۤxFGGÄ IQ˗/ƍ# .YDLg0 7kjj?-E~iRRRHHȪUY,@=!6 !Rׯ_hhhH%B # ݂fň>]]]eee7o9~8-U˭+VhkkœAɉsrr7nܷ~R|:e\ty֬Y";v=;:LqBGmffikk;m4yDz*++kP񪩩AơC&NXVV&]qnڶ-ʶ섄PDh Q388xÆ Ot4?~\QQ۶m"""Fտ={8p}YddΝ;?';r޽8;;; Çh…AAAW|PSP<T˥K4i p%(XT`#]~~ǎ>>>jkk4k,gϞ=˟[;`/^ףDD]m-6l(QL6m111 P(wݼy̙3) :u**v(uuuqZq6,YJOII24&evUVoذt PAtttG{𺪪G.\Q/ƍW\\,__ZZ?W޼y511AT=}vʌw Ɩ7|(--Usq#޲eҥK݇ګW~;v…7np.iF]t)>>][[yڵR2bccQ:t*u̼VX'C~~~UUktDiv"segg'''GFF}w+N=np]G?*++Co۶m4"J8 ,իW=|hd***%lA__TR'I"~7s3" Gjjj:cƌ.߱c?J=++KBL=z{BBFCC7ѠApid9p牨u ٨wFEE͛7Q@myڴi"E"SL+SPP}v1qhcdd$N ˜%K|WEEEW\&e0+p£Gv_Ix?55񬭭PSSٳ'^R k׮ a0<5<=?eJΦwܹ8fffxo߾8CSSJ[X2޸D1=n8aw...q򤴈6!ݻw?~\7~x TUX^^^ ۶m;pBZ6AypPݻ71cCCCEsRSӊsk׮XGQ^^X%#vء.#QT/R***FFF]aRPP0zӉ (MLL@+]yTTT$''`¾ϙ3gΝ׮]?^SSs1ܤ '''CCcٳ-L7u,!:ԽP9rD)/_.: XZZE. `777"Эb31ifffBBBXX8} R 7 44TL"[TT "ϝrz}ppǝ;wk=]xqfff۶m^^^SsΕсeeeIuf(>|xvv\x,ϟ?_WWwɒ%Jux\ +++?SyyyNNNRRҊ+|||P122-caa1aT{P/sI Pƌ%ƍF~ۋT  HY- Au8#^>-)! B].** 墾ױ޾}{/'```޽{٩Eٿ)S|wz~~>"^\6m\QL:u I>|tՎ#Mׯ `iiŋccc333ϝ;w&H1 AMݻwʕ+(7o矋 F9p@555UUUѤbĉXrJBnn.jׯ_͛7}wyyyiii8)yyy|?kL.tݻwۭ=~Bqeooo-[~jLk^ 7nܐ.޽`dd4k,mԴu!C>xL߿l2GGG\!FB'OHWUl*((HMM]fM`` ~xxxJJ ?tD91 ADLtp·~86!!ALJ*BOOOTlllDLR;kll?"ŅIS p;xʕ"OqĉrvmTΞ=kmm)S+OXEQYY߷o_kO>ѣ?cGEE!=,YweܹҷZɓiWs>>>ׯ_. ϟ_`N̙3ʤ FF<޽p$q&%%hn~ڢݻw0ɓ'[ZZjhhhjj+FaT\\Y;Dј V}vUU 55ՈE ʵ 8pܸq*D蛗'ռ{t>޽{w'NDZJ)lmmMLLz:t( \l={ P g_JqT qrgW]]7|`Hooo---I[[n=lߏEiO|XRR"FrijjKCN"22ž==3gIwׯ_{yy[)WZZ*]C ܾ};77AfϞu֋/Sk1 aӦM˗/1c9oʕ[l9tPYY;bEcc#7gϞ3&&&"]pa`` "^j똛[YYMDȨ!6+XiW 555W/TyqZRYx1ٹsgVV8AJ52@Q+xvƁƍb~'''7 [;={gϞ1cꫯ~G[bѣwޭT޽{\dd]AAA>T_|?YwޘO>OuY9rohhh|||ff&Jmo߾n1 ADo&==UV!vssC0g-ihhൃa???+ڵk߾}ÇҿA#~\tI4HHH/pM6j(MMMәΜ9sDxt?sε@֭8&{!HRQQݻw``xK!>"[RR"YF\  `cc ֭xQQQxYj1788ť!8xVϜ9ekbbbVVyO"t .ѣG۷!.jҟE;l0b@9rϟE#Ca ҆D:nڴI777WWW pUŐ!C-_Q'=b +}6.ŋ#}6lkG!֬Ya'CY̙3ߍݻ111mmm!Ո{ddAի5ݻw=== /t 999)))8b  5551ʻ[>|+s&"R !?Խnܸqio۶ +Vǰ@%q~&LD⫯ʐFP5)#3! qqRE?;;;sss޽{t >ot,_Y Ā/6lً_lقYKm<'O>Q{dd5)6w4ę3gTUU[)ywqN0!--c{7nts5܃jjjC wXn]NN| 1%-xIgҤI***x"5 wҥKt׸߫9:/!˳g-;v@ 5OfɈmll6OKѣȈ# )֯_/*z(`*=|P8qۖ-[pfϞ=yƖ>}5xcǺုXkdݻZ ]!~*((i(zl\WҵI3D#1cƴIk=<<_ȑ#8n'$g333''iӦ];VQQq(DD\)0 AQ E}!İyb&1x@eXzÆ "y;6X^^~SF11d:fBṯECmbb2P+gʔ) FD 1Hld |||p%.[lΝ/^vN3N>Yᴸ_ϟo``q7nfÇJȍ/Rc˥"SwܩS6,, G[B  1ˬHEEE[yQVMMMNN,kkkUUUjuV3ZOCܹgϞ^F=u]v566Jw=| D"`֬Y[n|r+gb ;ﯩ"Z֘9s5k]VWW#^ujLC"^ 7'{f43lycv%b~U3ޱ+`yͳ"(,,ۯǟmܹr۷8[,{4\YYYtkxB$rssw"׀FϞ=UdMLLNd4"4uer@?EbE7@"Z^hjj /;c:9ƺuю233E"Cީco?v u ̦_67`2exdzM-ı>}:bQ1-5}}}0ޡC***U)vAϟWUUа0b_ƸNϘ1ٳg]XMMM=2ݝ?. /e=Fݯ_?lW^:::h/EMMy֬Y7n,,,lQ$1 ADD;w׋Ñ#GD!))I$2-[&"O1tPF#fܲ@޸Aeo;| _lA|L5yP'#E0uܸqvڕF~?wĿA6'Bũu,&w 8_z%ˊ \`ӧO0`SPPPJJJyy_@ .Z`3f;7 nX+++kkkKKK}===q{ͽ|rkV!"649Y 4iHU&eͿE:;0j(ҥKŦSx@&C g+}/_R޻gϞY hkkc7bqľGEEajbN7ʩڐ}})S\xQXFw"_6ϔK,OS]]}֭{v_O:j*\!8===-[W_(Jpss1bqʈ; K+W^e3̞=;"""999??1l "RVLC)>&UUU"pIy!&&F!ϋ|̙"U"!~+ޔ1yJ@؜$rǎ8p@LrJsf+ϟ#R=rHJJ O#GᥙyL.\zxx`/_&&&bOSX4m4Ůan=vψ4s1#b삂:= e`` Hdq8vXjj*ې!Cppqd fggbZppp=z3p"k|Uf\Kyyy.\hn0 ADDԹzJD(--򼆘"֮]+)OXP@(_Y *YN={<vC~Z w 6,ZQdG]]]}||-[!krwhFZϟ//1 }2;gΜyh3~7GryBEE//۷oGX.VicNJ8rSN=p@ttg}6yd;;;쯺:]UUUejj-|@@"__ѣG6 k1ZDDDbb~#DD] DDD>|(OBqq<א&A n&ӝL@߾}[/1b#Vɦ}4ib1c b>>=zطo"DDDi"""֪MLLlll.hne۷h۶mE ,`DDDDicǎEee|Ѿ}΃Ģ+WH1 ADDJi""""Eh4DUU| ""e4"|tBt _#55Uh…LC2cH>: ?qʕM6ITQQٿ?%$$>u֭lu "64"& $366VZCCC``1b͛En՘ "64"|h\RUUUn0 ADDmi""""Eh4ӧ/_cҕZi""jCLC)B!ի=z$]u "64"SB "64"0 ADD+DDDD4ѯLC)DDD2 ADDDLC4b0 ADD+DDDD4ѯLC)FMCDGGKwc1 ADDDk֬知!ҫMJJ bH+[MMMHH˗/;CDD """RGu*^Q+0 ADDDDDDD 4)DDDDDDD LC?)IENDB`onedrive-2.5.5/docs/puml/code_functional_component_relationships.puml000066400000000000000000000022071476564400300263360ustar00rootroot00000000000000@startuml !define DATABASE_ENTITY(x) entity x component main { } component config { } component log { } component curlEngine { } component util { } component onedrive { } component syncEngine { } component itemdb { } component clientSideFiltering { } component monitor { } component sqlite { } component qxor { } DATABASE_ENTITY("Database") main --> config main --> log main --> curlEngine main --> util main --> onedrive main --> syncEngine main --> itemdb main --> clientSideFiltering main --> monitor config --> log config --> util clientSideFiltering --> config clientSideFiltering --> util clientSideFiltering --> log syncEngine --> config syncEngine --> log syncEngine --> util syncEngine --> onedrive syncEngine --> itemdb syncEngine --> clientSideFiltering util --> log util --> config util --> qxor util --> curlEngine sqlite --> log sqlite -> "Database" : uses onedrive --> config onedrive --> log onedrive --> util onedrive --> curlEngine monitor --> config monitor --> util monitor --> log monitor --> clientSideFiltering monitor .> syncEngine : inotify event itemdb --> sqlite itemdb --> util itemdb --> log curlEngine --> log @enduml onedrive-2.5.5/docs/puml/conflict_handling_default.png000066400000000000000000001534561476564400300231510ustar00rootroot00000000000000PNG  IHDRxQ*tEXtcopyleftGenerated by https://plantuml.comviTXtplantumlxmTMs@ -!@;aKiLCJBƖk)<ÏGvIrJ'i}.4U"ːbNä-\s0`™x[*ѠkaA8Mڠ Jvd !䕦 1me<Z7g@Jx<7.1P hιT}ō-`apkM*[:{pUl|eJOJcq -QSF ~۾.6ҞC;MKb]H#L/6-+:wbMP7c҇ES})R@)!﵇%و} E m!ޱ)`+ç`nôh?c`pә(HA]ڸ ֲ.*%FO #IG'~cqNg˦Gim(Wcd''[NB*5($,쌬pxSLS qE%:L:Z G!/- q&rqB![嚺|Mĺ4I4a'0m> Lm\ZZ99y+9}wz>#VMƑ~]Ρ{D9Y7q'Fjwnqۓ>8Z6H& jnl\6~Tsyl$u)\+ u] IDATx \uqEMT(zd@[jV[els-TL$! Vb7%EARZw~;9\E|=1gw=393475 01a c1 cP!qqQ3ebc_rG_ ~q&k }mj80T̸wϖ|[yرc))){9u@-g- .|w?ß a c5RRRz{IOOcc@cXXo;.[O ahjРMZMݘc5VXm7c[ncPX' X iaz؉'+;|2'@ c9_=Xw4If|_Ѭ_vߏKd߿sES7dO%M0Vy,44ԻΝ mڸO>SGYKYX/]wXa˵Mƪ%,<TC{#"bd7G?hwVxQ[~dxOOwȟs ?ְaCyy?ģ-[6>s2*!yo[vZˌT ,]׭[f͜靝\~0:i~߾wKmn]GV||<>8R- 颢W^c,z^^/ߺKمjcdI۶d=-7Eݜ;/O-K{ʹj'i߾KYz;kGzxO3czӸ䓃cbdU?۶mSnw{Έ4:t)c1Ƕ/2>V>T.d&⽷q xvKr?~Z,:Ky? ǎ}Ws/et.[)Ç<YןK0T iz|H=r)3fsU'd=k7~ؘ3HH y/..z/mK-))QψO?a쫯#a P0֨Q#R/lӸq#up{o|N>>ʼOޑ4elfN?ءKsww3ce|nN Ȑ]/9ɱB/$!zeGF۳u1_ͩW^c,z^Y.I$YEI,^^2jԱc"$/@*GѡC;ܹzFj11S~C=3c)))| ڶme|eLj2T! OvťRLq57W^׾}s]A[XͩW^c,z^]c+W͓+>P[nURo6V}}=7m^,\.kWk}!kgڻo9LKL\v̙?alݺu| dl}s=0^ϑ1w6 ,;__eUkЕ1z1z7Rgϕ1rrVuBEi6Rrߖ{oo6VĿ./BCn~Sf$•s7|f\TLii[^c#a P0w$ukYOSG:t~ 垱4{NTPsnk/YeOd\~'xTm6j0_wPoz{Lyx@Wq[j'H^'hN5WLsEHauIZ9=y射۪W'tzݮ|?퐐{3gg.]qLL󲲲4ψ%ݻ: *dnӣ~{3d0+|GӼ::ˈYo2i鮻z[9NY׮M40IyG_8;2ڵk-̨_c_0u?Yo/2f'hN5c~:-QTSʗ zwrrݻ?&˓?TR3n/~$ɎTGa-s̈x'?_0 zحgjz2d@9łʏ~Ur\\HKK5={T48q aLIBt]ԧ4g"=BCC+Νˇ 0T&9T<=ݿݻ67*Guϋ_3/cPKa.OUcի\' 0T=aLkư>;; aØĉyll[||+Ƙj6$1 1107q~M1 01@ c01@a c01K/_6c@Xpa~~fLII0_aaa@@#G,Xzzz" 1rHOOO%)aL;Ca ԔTɤ1 c:v(3YYYt@Mv횿/cSIb?=1PG˖-[0jVaaƼxtpPc?\r9r$Ν; cQ8SMLO \jjj%u҅Gwa 1LL52͌{n̩@a0TaL~`!::Zc1& cJJJVZ갰0 ctE]ի @cbX-}Æ g>xemtBݗ>c  @cbYXK KKKc TO?466ڵk pX wZמjoص׳v-X 居 ~njuI&0rc@ cp.j(X~JrA]aŋn@ŒǸ>ƀ c ܐ+c,BfXXMtBw\WbggZ/ c՘ǒRRR"&//o̙ PWƪq@O#Յ~07M {}?V-++>}:c-Zy1ƄuܬS>s(/zyyl rG:x|q-p} fcuhxX[?,r5ﭷ^%ڵ޾knEm[~dxOOwȟs~:ݭaÆռi~Mߥ=Y>j Jon-lYѤ'g_UNި"F@-+++@n@6졂ep޴q}iQ}k>Ç< #"}O*$c39ei4OiԩoV6o\;^Q?Z],:˸^x6a^3GwVٿin1oA+t"B+//eX 3g111+?:99Zرy' wU{o|vWKsww95/fܹzMC;Pl[Q۠~~>yGҔⳙ:uPWbt&4'͝51`csbØfgVtJ[fMff&cbرcGZZC0v3ۿaذZrܴyuMy2/3]T ֨Q#-lUByi[Q2|7򛃃oSWLJwzМ4w=fA]ОM[ТU捱?U=uU c3f`@ @ !P˜2vc"76WK9__AZ4|k-p@]cf1F4FEٿ߮] ~Hxbz՚O]~ML;cU;Z4;[U+cݢf;;0V/[5-11d2Z3jaC7K ca )?^9,j#Av}edO\;v|O<.oUM4[e1GrTe~һᄀކNknEΞ./edڡC܊^1j'k&4'͝뱾}|oڸslj˜`g|6ٙbbb/&%%vmsժU6N5߮WeP.7nܾ}Çܹ;vܹsUXcuܲeKӚL7E1cC0ԭ0傞={uuu(uMuݤIcypvnafl6}?yn+1|vZ$3]s+o~~>͚9uWu4WLw_oXvW}j;/]wVf-zm3+:M9iiʊ߫=eddnZ2XQQիe T#ڵ*6k^sVVd?&%{)ie{ePcǎlRO4)$$$9 HMM={S^|EJZl<_S;JB 6损8qfw3XE cz[ͅO٣G=ڸB.+CV}%%%{9r$a 1&&F6'5mT+~Ybƺto>e>??cǎj\e^Jny/^lsj=2VeHds5*7nZ߰aÔ+-z裏qeKwﶧsl~d2Y/_d߯K"Zt.j>d;Øye_43U4,0V~9BiVץze$V)6:ƚWa 1&&&%^#{=w7ټBծ];0&F9884lP~Jg___Vv-nZIJ͛NfA`e^cII2/37Ң۷o rvvVv~O?T_pzQ/ c7zb|kގ1RKK Vرc>>>k֬!11{vP'"DcR=߯Sm3c__O[&gn*).nzLLXxWV1 U QGUrrrlf[y[ly WFDD 2ĸ#GFEEEFF>ƕΟ?߲ecǎWƾ[e~Ϟ=7v,O>9qą NQolݲ2Ib...RqUׯvΝ՛*WIɻ(PɊw쩚ℌKKK;}(E5jӫyPaU*l:uHEiVץze{=ٰAVRR"ǭa 7, ZXM:ՑΩ-Zqj^EB-cͧ5{W%˜Xr_FdGd^]{[yj\BBSbcct"ݻP~Ŋ&IlI&>}Ѭ mOiwnNN'VtY\n5sӧwvW{j^ZK[r~e/GFtwqi?[0wߵj￯aS:wu믿~`{{Cc@ c22d>S,z|H=Ƕ)3fs@lM.^;W^yڠ戈Hͧv)5eDԾ}5y[yfZϰa.ixOm1XK)#D'pe5lڳzN2_={W7Ύ52~4Z>j'@FnOY111&M6/?"66!@j5y{w=7e>;+esN5 jk,cbSǎ΋8Yb7%W(?:990fώ?29_ISfvx(^z޻ њk4/\GZ턱EX @-;x5kB1VØc^9ˌ^N]Tof30lCZ9znڼ8\0#kiV.{qڳcKcwkk=c-|kH0v̙3&ns疕1c@2&_ &%KΗѲ .34+Qn-IEnnmz{*Cs3{4iV2] ~H7ۿ{W^{Bot\xm1wi5Dxx8cbdƀcz;e P˵\'˟xQ0>Gۖ2zi O?y ۷ok<Ӿ}|oڸsl:Ae~z9ᲊoJڳzo^z޻ њ^eF cr-i٣p5*--bƀ cu ڵIfԯ{'O~M/ S7}ֵw&=h~̚5+::*a̶3g,^8..n&0777:..%%@cLL1 0Fcb"01ظnGa @1 c@1 0c@ a 8/_6c@Xpa~~fLII0_aaanݎ9bvս{rcF1ScJ$o9@5%55d2)yL˜$N:LVV1PS]'KTXpp0=1P|||ٲet @*,,RØ 0bɤ|MGwa ԒT c;w@D}UQQQxt=)))1}p 0yr_ P80mڴ{^%'' rDJpTc2I5AB380?\]]Mt £Gh׮]̓͟fmO7_h#m㏄10j*)3ƍ{U0vEEE֭ RgddnzժU|2k.u|u* @5Ƅ$f͚͛g26l(/KKKnjӮ]mʌTM0]~,x)S<<} :rRrNHHHu8q<<<_~YYYG=zP6}ɧ~zرr77/d޽#G4XhBU^({ݳgOʒ`I&q۷kvA>N:/\܄^3RSSϞ={ԩW_}UѬDa a ...>uynnZ333S߽{2-/-ҥ˾};*:t5O#z -Zh+%H>|dذa5+Ud{8Ɋ/djѢœO>&ɦM덎*[,,, c4H6mkcc܀0\ PRRˌ!̗]аaC,߱c]]]}||֬YcР%zW%^Ki޼dF@/3aLuرc+c%/zo^3oDkifCaޕ1IPף|}}srr(*))]v6jD DsȑQQQO>*~2g;Ø8}z'zM99uݳgʾ]l,ON8q…'O˕W,F@wyGU)(((44TY>u~}wwjHa +m6p@eA曒]nnn ZWyZֹ~zI_;wVo[e„  Pz@ 1u۷\u֫W.**;wZ… %jnBcKKK.M͠a a :$,kmrϟM&ٳg0ҥS޽W\,_|yea```jjBU^u^t:zHy]\\<==~~+..;Ԍ,/% I9tZL*kҤ\jnJcNͷFɒ*Zwwwi\-[,F@Źs$˿Fc0Vw5nܸG;vgO? V3g#0*l֬Y$ TΎ;8{&''*P9ӧOz*@oM:P=$&& JJHH&Z>も 0*ʕ+2W3Jny[nUKN2}m۶H2tvvk֬1hڵkEcÆ ;uH_7ʘfSy=XdI %1!);v ɓK%꺲nݺ%%%Uk#G\`Aii- 9j &H-[ִiS?sT+ټys+sӦM]t1dgZͦFp+**W?Ê+ûv׿U/zjI)vcR!%%%2syf{9zh̭[Nwmo0Tc0znegϞ*++ē0?ޢE k̟=hDY$9rĞPdΎ{z)&iժUN>m*j6s碣9ch2{XPPкuN:UXX8mڴnݺ)SRRSSSϜ9i&%Qy撚wa c/ҁΟ?e\]]ْƌS0Ta hUc%%%NNN냃4>={bO&M 4c̐!CNXX1'|IggN:-]*al޼y^^^QI[j%ɓ+4J  ysݻ7Hb1؛7nF"_Py $1 c$1 c _@#_0u  Fy}t҅{_;TFFq1@-Q#2,q1@-Q# jr9 @uŨ &h 22׬Y]AZVZZzꘘ[N>Rf̘e˖WܺIlԩŗSǏc57n={//jHzzzlllTTTAAa I 1;pİ4N:5O?9s@C$?s @m˛4iRII a I 7qkРA||M'N;BBBNH cHbyLXxxxyyyRRpȐ!aaaĉ ƉM)))_n3gp1 cW\_AY,\pŅsʦOΙZhy*OB%JLL4LV:Gz_fխ0&? cUTT@UVVBIus̩Re˖JjUzQaL3i!CL2 kV^^(@]0sL?FP)ӧO_kܹu0988\t&RPգ cyL3~;5kdff2Pر#--0Hbݻ+󹹹L^WhGm@˓ԴiSDڰaN:]:mڴK.)KJKK .ϻG͘1#660HbaP2/eEEEƍ88x|y{dʘf̛oyYQBNI,?O>Yc cxɺ~W5omk~˺)f#k @u |0ֲeKy)3'OtvvV5j,;vlϞ=G-&dDf.\`Pa$HϷPsl1Ҽu$vm捱)i0 l.]߳gnj"&&ƞQRRrO;5n0V-2%Ur7]uV̫ƪqwjl{̘10w٨2?ͿF F #GXW-{穧3JY\Ҭ0fsuvjs @Xa̼ǬØdZj$ӧO dɒիW2[Ν.//簯BhݺEEErXʼA&U [[ȾKu<թԩ0vcP"U9{̙ 6? 0~xKwO]wÇ{饗8p-[HD%*jc6W.P\\_~灁:B=2 )ZDqƙMuӦMRl̘1al}ݱcI0v՗qqq#Fu;$$$999}ݻwV;eggQFߠ3L 6>4ڿСClѢbyW|͚5ӫ_<==45)/'xvڶm+3HuS>'L.[W2%j W]7w} ?IzۢyUJޛnpn^=z۵nuJY޾}{騕+WFDDH:t%[1)M6x}NNN~5kf8'N<ܩSK* T@wttxh0I20׬0fsu<̙3J<j^hMߤI؂ Zj%ɓ']J?LC]c2:|blһ26x`9d&y>}rggg1BCCz#=_2|7ĉm=4_W?#(++ѣʣ#;-92⋒ ˜cN`u繝hOVtkdMK+**?;('2߫W/{0ֽ>FR$F?Ibƚ6m^dP?Z1*we߾}|~~~ǎ򹹹zWͪɀd2R/qӬSFʼTe3Iy-Jl=4_W??7,lgԵաCXn3ߐ^{ ip*={XWUt?'mמ0f'N(a/P'(X`I 71͗l]аaC=R@V+olT" 1cz5YԼZl<=4_W?I;g۱c]]]}||֬YS0f!S@5XŸgAM78Y,`N<a y 7u3a6ذa,>|2oq^ɩON8q…'OڌRRoUT"U1)#]+9ͳ)$V%%%k׮Ba̞-Ξ]SW XrU}lڸR^k{̨_ 3YiܟϟM&6R{dglw˗/޽, LMMtkrݱ*iIK2b瓊075$$d7n1b+̙3{F܂aLcaaa|_@mZ~e"4rMOOѣGPP%]WZZZXX~#W_a Ikt#n0Wyj3gf̘bga n$5}Y//$e… }||֮]K7 c={DDD|=hܿ|||tt_#=r_^xᅜz:sŋgΜ AU:6GJ\\\JJJyyy%>cpӌ\Gt,Yna XE=@ pUc…  08VAثGjSG遁J:Xa `}ݧ1e4_{xxL0\c1@M6mZ{%yLFe&++\c1@Mrd뮻$h@XϞ=yz pU5k2߿u=вe08VAԬL` p0 ݺuSyt jɴiӔ0ֳgO*cZ]!9sy2339_ @58~xtttTT֭[.Lrr2`tݺu$s08VAʐt\\eM@M:uǎG pUbΞ=z1r*mѢE7olb p0*..4i$N֬YvZ)*c`O?WQ]fΜYPPXa lۉFeeeY pUȡC-ZD~@/wXa tz.18VA] e pU@_|Ml@Mظq#g\c10c 2jNll,g\c1s+G '&&L&6TIIe pUa,99{ݺu[dIL 6eb,YXa *222ڴiXTT{Çƪe.]Jc` U0>aÆ͞=zyii1cڵk׶m[~ʔ)۷_\2""ťC7oV {zzO:wf*P^60,xe>??cǎ)%77*j͚d6,>>b+2M&ZѣGyIANNNU cvF١CÇS^4+!18VAczWKJJy"o\ IZhtCÆ fJ1Kof͛7?~KY} gggJos75_5rǎvuuYfq%z Ь0Xa ? 6lܹ2f3o|ʼԠ(ٳѣG}_*__ߜ =Bo^#GFEEEFF>i'|rĉ .}ZB׈#zWPPPhhalʊ}&O\\\=p@|֭eD۶m͔ۤZL%tYyLGbbbiiw}7tPa̢JR oJJJ$GɰƸhVBc p0Ƅ 3tRZdF=a,>>SN...GVoHww+KHH0E˖-3ۥK)ٻw+WL)zجҥKש_zLNNx֤IwwwYfhak} SSS+kf%1*cP#a^#f̘YXa 4X0YXa c .8VadԄ}[. h(++5k5Ar2*c-""B}!P}/*cPo:Urʬ,/*c`$!!a۶mT'OFGGsf18VAΝ{AR,,, ]]6cƌ/,8}I 8 @lڴi<:e18VAʸz꧟~{%''0`ӱc/^Xa s̉fd4@'1cg}VXX)Xa h*@F4 ! U0ic1pH  *cF8VAԢ˗/,~ 08VAT k233SRR"08VAT=zyL E` p0wǔ_1a: pU5eڴi۷ի1 Hsww,: pU5ʕ+&鮻<&Ib={|G08VAԬ8n=C˖-[Xa P233חGw. 6tM c` p0%ӦMSXϞ=yt j 08VA*1< pUιz-[f͚;y뭷d4׳n޼y pU&s訨[]뮕]622RRji*cP]r%..NF2_^^ԩSwXa cǎaEm޼#. $6qDIt֬YvZs*cPO|5~9sfAAG;\c1C>XEFFr38VAСC-" ˝;wr38VA:!<<?8Xa aqqqD[G|||yy9G>\c1l"ʭ###cƍ pU1cVˑXa nJ4hP`P-HLL4Lފ5RI pUcݻwwtt֭ے%Kj35q۷>|Ν;-~%nozuOO-[Ty櫫MØ pU&cmڴILL,**ڽ{$ZchٲezzźҰ^zYpҥJ7Osu @maÆ͞=[sПtwKNS_xqʔ)ΣF*..V ۵ko$bo޺=4hΜ9yUM!/KKKnj#[o۶KؼyL&SÆ V2W^αΝ{W\\\f͚Ec p1$IrӌɑD1y>}(ˣO-!{5^]NHHHu8qZFޑ#GWcz{jQy;Q]KBH׮]}]'"6jH>|]v(ӧF,TRRkNfԯU:APҗ_~YbKc p1'>1cG>\c1 XTuqJMd@&hZ21͵RP[Wiq!x'm)kE&Y*IR++J8a\]m?vvÜa~=gΜ9ށc#y9q,[`b  cZ7:u>i=;}vF>\ b YEEŪUHC^ng 0VA@㋋+//RZx<\ b јB6ma3 Xnݾ}hM;!!`_?y$R]reΜ9\ b e˖}GtKsE)B9\ b ]v%&&r$33s|&`ƍӟ.\e˖b:w;w!F5\ b 'O&'''ݴ%@{Rtҷzh42UcfC`0C`0`0cc  41M^UUـw&c9RSS 3228E` 0VAh4FDD= `28E` 0VAă>2\ b P}}}#=&)1Y .X1h(ׯ_d6 %& #F̀ .X1hXGyC=G}tÆ 0 رc~ƥ;`^WblԨQ\Lp*1$&&*1֧O.&cExlKw .X1p)2\Lp*1hL&SbbҦaƌ2o jժjF c %hѢ&c˖-MHΝ;'J .1-Ěz LpX110 M+WQVEFF~C p .X1%y={*+̙#}Wnjc 0Vb %F9$Ʈ_. ׿ܔW^+Wxyy1U@5橹2==]YKdj1&c Pb%6CdãO>۷ooR1{n% .11 oV0`Cj_מ=zR/Co^Ӄ С6ԋjxyyi]T|~:'11vXS&`4HVS&`c11%LpAc1 b 1J @c1 b 1J @z 10b \c-ZdlIBB%b vf1 i0b X1`6$C1h}Tq㆟_AAAyyyTTw|7##C׻;==b `}6+ׯꩧd'LKK6۵k`P}~!//O64 u ߿'^Yٹs6?k׮̙37u={pAUc ܹ'Px{{_x x~b `k6p?~\YMFQɓ'={C8 *1@}gRb?eEESO=yfeejjjHH^߶mX1l~xgN>͙1c TUU5hӦM8K b RSS 3228E b F`P{L Hz)1c bj))N11h(ӦM۷Ăe!''b @ŋ}}}'N(=&)_c1b а $3ftذa@4͛7w3ʥ;@1+*%ֿ.b @\dڴiJz.b @\Dt11p)2\@'>x`rr 瞓@bb"¶5kȠ1c`EUƯ/^gϞk׮U6[l$&k|| 2k@ؾ}{BB… 4.99955OɈ1 b 7nݺTk>}:66 vk׮ݷoW2s̡Lj1 b @땕[oh1 b @k$ุ81 b @M%''s}Eb `кO|,p}+V~$*1H^^ƍ4xr ")))EEEg}vAޒX1HLL$\re͚5%1 b @k:M6+-͡t'.m?k׮gcX1lٲvww뮻Қ~޽" ]i=ms-;v/[nȏ?`0ׯUppcd!//O]~~,9r$$$D=`cnk… GY[}{c=}b1c1[$Z$j2dHM=ЫN_x5ktaʲ lQ̙~kҤIJ9SN]pa„ ӧO"5c=5Ӓjs0`탔{WVVvqމjʔ)ɲj*_]Y~mּKNNNΝ-uE6'ƈ11[oU?le9+++((H+ʗ{/~ ;~It:u3gΨ S>Mݻww4ٳgeilٲ\vuV%]>cƌG}Tǎa;j=6frKOo6'ƈ11'ceeeʲ,f5,?x`OOOڵkno`2ow[۶m忎Ƙ{0R %iF"ezIHӖF.V4'c]t< jOc1b 1Vߌ)>g'cFz?كzGYXvmQQѕ+W.\"={<}G`A*$7oc<#СCeÇ[l\}b,...**淤`>1FUc1[WSڵE'xBY+5xYfcÆ S(L˿hKJJǕ+--ݷo?h9/rȑyYYԈ 쯑Kv1>>^/^,˖-ؾK.?***rrrkdLN`hħOccba+Θ^w02eJכdAC{blʕ=zBx?H۲eKϞ=۷o/AcW^yxxwӦMƘ{0R 7n(Hv(mi8qB>|X^Y>yd+tɡvL5B;Vٿߌ0@>1FUc1qֿ ck׈1b `Њ$&&ch"yKcc"%% @<[ yyy7n㫫yKcc⧟~иF#0FUcZ۷ݻ@#Rv7#10VAhuf͚{_eddl߾!10VAhJJJ͛G߿qFރX1:q~u#*1]IIɬY1@vv… KwX1}7銊dh{=1c ěP plSҚ5kdh"b  i @Ucf ~01 b l_cf1F;@ b l H;TUU؃w~4fhaԂ{ଂ1hMOF`PGϲd2qVA4 2s5g;wdf:=QAJ,((襗^│1hB2HKK 9NOm{=Ȩ=HN)1c&$%%E&UJL1sEVWWKIdA{9 @Jlw#F̠NO_yo>}俽{tOc M΄ d 6pZ|F߿Ϻw@1hrssi` c>=6l:{9N&1&J.1~x.݁0=ݶm[ݕHc1Mrѯ_?fhS2.b IS.!.݁3=U.;@4ueQVEFF~C p1c@=&1k26oܳgOeeTTԜ9s䎯1c@J N1ׯ¿/777eիWeʕ+^^^^b @%o.W^stN7_ncƘ#b ?H̷lF1.޽s>Ț=f5Ƣϟϙ3_S$b k 1rsscVco۸1c eyd6Ќ6))#Ƙ @ @1_f`8cc1 Mr6o߾^zʗ7n+(((//;"""fddzww޽{sAUc"##}eyO=,Ý;w>oo/ּcaaarr\'NvӐ!C6li1c /KٓKw1cWS? b Ef͚XDD1c .\Cf\@r.b @h@7nسgOrrrg/8PXb͚5@lٲs/_ޱcŋnܸA1u!+Wʌ)&:t1c h¢E>؃w1 [r%SF A~L&N.SSS !;;;## .1{ԩSL4CܹӹKh0S %&m\c14!K.e,[˨[1eRbAAA/\c14III;QkӦMܳsu:ŷl4j6k4_f=0.]m|}}1ك^& b hQ1֠emlİqA޽YSma1&2"ƪ%d _{LpA@cͭ%}bX}|2bL~4W^GۻwoN@-?˧NӵkWY/oW\Ym̨k֭[׫W/?++KYhUǎ ޽{z<=='NXRRb)k=ӊz9JlZm ;& yyym۶ϗ#G8?`0 ڴiS\\o׮]OȥK^x8((hժUHj=M5Ν;ߺ||yyGz&k{Vɡi+kRs{qqF9n?޽{.@/d6dȐzW_}UY?k֬Ç4tP{>А/GuiΝ;wz^YZZNƮillArrrΞ=W֌Zrʔ)ɲ +/#!ʎ=:n8G%T\\f͚:}>}>!g6__Y}ZOSko.xւ 5۶m1bţ[=;:CҡWƛVCO1& s=@"嬬 ue,SLwtu1{ S>Sݻw+g~J`Ul;j=ȭ[*sh3f<裲}ΧLe [cCs٣G[BBBNjnٲgϞi K>߽{z0,,ão߾6m1z/_W^mc'dÇCdɓdƍw}`0dff!ƴN4cZOSko.xߗA|tgֱg6,Z#);D%ƚb H$-Ɍ3f!gb @-̓;z饗Ξ={̙iӦ1G3߆iٲe@KJJtwyIj^hڴiV?gS&b ۸qS|hǎ۾};1c XQQQѪj*ۗ:$*1USfpŋ7b b ƀFc45pM6c1b -k׮ݻw/sGr…fRLpAȒ2꣢bٶZ [uuuRRRFFHQ\\,%VXX\R .1̄.vnv9|{>#*1_e˖&juܹ~{jv)@MW^lsIIIׯ741`610VA  b l 'b _8 @1`6b! /1b /1c i~1`6Vp.10VA 9RSS 쌌 b `0F:dd"*14wJ)IJGSNmɐ?~b Uc2e!'' PCCCd8It7րdH1Z̛7O,00PoMUcfhXFQd _fFؔ)Sq@2_fhE233{!I[Kwc/@aeYΝhmfi= _߮[ssv?dU\nѣ)xXrʹÆ=ppĉcf|^VSKb]%.5csZ+3fx7$6z6j}ZVۊ+o֯_dh11f+Ƃ/ӟ(ˡ=lRY7{]6fVֳ /eunս>guofzQKJ{ ?*.uWbuceʲg<t@/Noеk=عͬ>#gqc ٳgOvmcnM9u3 'tssS?XRdM?T퍵Kk3ڞ[Xbb"PW\qo*cc1 w]qxV\9U_%u(Uc9yrڙ[;k]V=r٭O曅~s€}i{c8|2F5KHHh Mf tNg->n=cx.x]p\rL$!*Z~͛7MCΟ?MY!C;o$]>ڶ?TU1ӥj:t(((HBYrawҥ^xAv%w_ji#;~'wq5Cc_6N՝|^Zг pVWc1b#Ƹ56?Ɗf͚u*_###O:u… &L>]~ԨQOܹs ~ɓ'Mn/5ktA,ӧfYݳo:tL wC=ꫯ*ccc sc]5AtCPY={Gf;vPΤ={ZQF̙cP-v5ź. k:;wCBs^YYѣGǍgc%1c ƈ1b[Si3fLfȌYYv1e{2Ue~~fNrKөK)Z3l900Prvvl,(YYYAAArppQUhi.1uLǎ3fi5h=Gu/RI;ۘo]պ9,-[k~/+11Fqָmm;O<5p;?j͏G9f;^t>q1#7nŋ-Z_d͑#GOO,c%WZNk'Kk3'7o7i+11clР2>zӞdrJcM֠x:cbɲ4d۷=`zzzyyW_}{WԿ6llg͚_`ApȞcN.]szjNw!IΞ`ZQfС,i9:f,XrKuvb6sY9?/++@=ԿQsȇvfc;ޮ^hOZyd0[_m۶s=ʕ-[ѷoM6ٞXoٲE}rG'Ƙ5}YOO=z$&&Cx)S$ o|]|N]\zlk.[tԩC5u7UڛeԋRq5E9ϲtoZݹ3rtXݡʗ^iyimгظqw-'`0dffXI1bLYtxl6sMc"IN^i}9cÇ%Є14Y@2O/Z<30˫sύf:nwڡǀ}OTYWz{{vtر,tLo,0ٳ^β=op7~饉ېOY?W]3dgNreAWhh~MZppΝ=MnfڧyݺEO^ Yo}wЍ髭>k~8jg 2b쥗^:{3g"##MƬ]F1˩OߓΕs {|+'&N3sc /Ld6U >ۛ|}2gg^xaȑ'';n~ʗm?\\y->lso=7 ) YÓ"e bL9NBc> bcz9l,XBdk=o-[O>eY[HVkР1III:;4iRii)kb uL1d-ce}^!y{=+RY6I+nR׺Ù$ru1=;M0R$ywey;>Nu(<}rv>Ou'G@:9Zս)q_}BGȉAclժU%%%LCV{!b ?tl^+ؼlÑۇI[looùʲ,!ƴ޾v: @' ;3?@?Y5ޡ3?N9l;vYݧQ7z-Nf}Nc;1;w3]oȉAcѣ[la& r)1ch1܊.|ޭ۝3 H{w:[qVh}pۯsPbLk!Ca+ߏ}^^=3_EʞG1]ɗdu~2Zh|RnN2HZc5r`h,XLh.]t&1 Řƍ{͵:9GtBmя؞C˄[o;Wzz#F Vn1gے??S^_-i髎ƘzCo>xf\3>__60 T77o7cC?Bdk=я?s%|}cZc5r`h֭;uQz7 11f}N翤W;'^B:tネ;ٞCg~Vxxp䎎Ƙ, ƛcZ{68[ڕҲ#|2'?]uԧ N:`@_٠ww7b}H3>>]& jZ'>'_2>z{z{x{g{19w04h]~]$Üub `Ofqk[/HhmJJJ̙㬿#*1n1VK.=y$sS(//7o^YYYS\c b1ֺb:99y߾}Pgvzcc7nX[.%%*RUTT;˗/7LMsrI1nZc ѸpիW9si+ВțZkʓKb @qJcLQQQehd6Ihٔ'1ƭU _cܸq#lC_+qV1f /x D @ b lC_b _cf /@ @1hZl, c1b Vg"c1b |FQ׫=vi0L&@4=bbb89 b $ ""BzLt:,pr@1@C ـG1c k„ 2vStt 8- b av`  b t11p2_~\@r pc1b Re1FVZZhѢ:XyyyBBB~~~lllhhhPP2 ((($1b SJd2)|1 @c%fh4*t:^O1b Qb*僲ѣG z 1c=%fh49@Ա-11b P1puQb@(1 @\cc J 1cW(1 @ ?~b b l cl_cf_f~0~4*1cHMM-((:1b |FQ׫=vi0L&1cѿǔ@ZZZ@@@LL '1c"1Ytb TWWJlw#F1b а&L n7EGGoذ1@3ƥ;1p>}(16~x.1(@r p @\Jb &˛7o^tiRR8-Zĩha +oe˖޽ƍ(1c̅ ^ɓUhlIh8|B~1M݉'$Dh***֯_/UV]]͏81ohy̙SVV:1IKKۻw/sV*--11iٺuG}lh-_x1b @SQQQd@k[oq=1Xxqqq1T5@L&˙b Ʒul@ٳ~1b @#[t)sSYl?b bڴipIOOtrgϞ|:רQΝi51c 4${&v[n;v_|b,00p8|󍷷w~~~ ;T;9{q;vPټys޽_ew_sXɋh4[|9zK @h1&|ɓ':utcnnnyI&OK궷~_.(((..޾}ҥ| ?CY>|zaÆ;v|ĉ51y @h61Zpȑ#StUKY|1僈m*&9r$$$DۺuzqgeeY<}ǏGGG{yyu1**PYҥ^xAZ4\zu2Y/))y"##322/?6 ׯ_od\200wcSRRt:I]iZϰ-9!N:tw`رL/WXOǒ>}Y|ϕsiӦ8yveaСCk>V}^PHشF֯Κ/g@Тb,''srLL̐!Cnz衇^}UY9eʔdYI3xi*nHIQacJi"HRgnu8heJ&IK=rN%MrDt^=߱gV?I|ޯZ{Yzg>߮*oR dx!CENѷo_rW}95͒ʦL2w\uve^c俯}YuuS^y͖X6Hy7PufIgDdذarϞ=kW@Å ԗg ڊ+gc%"--M"e}ݻww?777 @V222ikttyƌ'Q,ImEEEjUnnnvGѬӎLM&.l#(ɺc 嚚mwJJmqlǏk?wfUzUۮhtf@J3d:wꫯ'To)$Ԭ|˥0f0Vʱlץ%7oСj{ڦ.u&5~@a @ cyyyݺuSez]]]ˊ2ۖi䄪*wwwjwUKufY8rHDDԩ?YaLBo߾]v1/s{xxI(lf ?f;c"..wa`ϸP6w3'!cT[tw3G^lᜨ(YvrWlذ֭[eeejy~I^S=XpppAA DFFٳG}̞wMwzm0Uk`sf@4vڵN:)/g&Mds 9Ν3>kvsf>3֐LޥI)%4irfϞYQP̙3RouuF:ܡCO>}qƌ׬YS0vm0$$m;wtCfƍپ}{xxӿŢ4E???i̙3OU`V7P cuzJY߷o_dd$1j11,,LFIӟy:ܹ3h f6tRg c9!'^ޥVָq:ϑcZw"66Ve_͛7% :յ\Nŋ[55@a aO<ѯ_Gr6[ 99@a C&s2fƄ1@ۓu{a 1-Ųe˪@a @ZLU6lÆ yyy\cZK.%$$ǀȑ#\cZhg޽[n*@r,[ymƵkג2331iii|4z:ujժUfoc he*++SRRFlAh655533K@a l1 pcl1 ֮^ 1`64_[lY~~c |Iݻ/_&/@f@&1y \~0; $11`6<$F_0UVVZ,{5b_333eGN 0`6Gs7Vs)M>_~'Nt/cfxdwavڥy$Vg ?g?1{ȊwbƤӧXC߳gOppG:Mۧ2YF-lҥKŔ)Jۤf _T߾}{xxs-۪Y`d$Y|c=־}{{ ca H 1cNi!4OzOh]ge60xaqMf~-7=;a0F@>^Y/_|EF >Ͽ{G[J4Էo_rWL6Ms4//>ԩS4YҘ5Ø^:i\0hSaL".s_777qHHӧb___e_&'l=w,ǧFg̘eF:zk^ *YjUϞ=>9>/鎲.]sl*l ɤ9tuwJJʅ  РwgMoHa̙24.Fc0vI?о}vɿ;V]]m;wvĉR288{ärEKjV"dzcKknԫDIGpwwWZO:7tƃv{xxeddhӠwg͙!=PMb10@cʧT2/((мwg̙j tС^{ͱr74_PW9*ltÆ n*++sIuxv9NNoHɓ'Ǐ,M#1~֭:V#F-ׯ_?|رcQFZ:txw}q/ǚZhPYҥK-R?pz1J4$iǎ7nܐ&L0cƃ6nܸZ˜;;!]hkew)9zeiBb10@cfK.OSLII qss8pΝ;տ@>}ߊ+:tR?7LRc#kjj,X ?PfZh\ġүٳgx@VԿs)U٤={H21A۾}{xx B-;;!}_suӮzca Vƚ±c$͛w 3 w)**:w\dd9s\ի{.,,$0h/99@1o/LݻO6~*پ}{[5.a ƴyfh:{lFF 0@cLKKc Xvjrhdqqq̶ \(/77W}9;YYYBh˖-b p[ ЬjjjbbbV+3oֆ D1ХKcȑ#\M(&&W޽{ne LVkBBڵkə\ׯH9}իf3*x*++SSSSRRь$233kkkc%aA a 0a c1 ݻw ˜O186mT\\N8a 4p5aرc!h/<1Ib>>> ,`p0b1LAAA$I󓕼<cܿ?,,Lҗ1WIb @JLLTO>۶mcX0Vyyy@@dGwa 4_&I3E1L,K>}$ݛGwa 41<cY%&&J@M7oܵkWRRJ0F~_:t=!a Y,KYٳw"ٳAhrrrRRRKJJ1raYYY̧Z|Ijj1?f 4˜j0a +VZ/~I/~ivv6SgUUUr va,?StylϞ=_|f2ca-ޯ~j]beI<0XXZe(%&&VTT0Wj]|9s0XXZek}j6zԩSeev9֢E|||<==W^lo'LеkΝ;K%%%$uƍaaannnuŪ4ݾ}{ɒ%zrww:ujUUc!BCC7o\ZZOmW\+xtI޳gOΝ;.]*>pqnܸo^=zy[ndj׮] ^Oڣy 4{(oXvEY?ydPP]#o޼9k,id@@~fYzh0$U7xCoW@rvu0ƀFc[zޝ: 20࠲(NHӵkWVT s?M<ýKL5wW^Swo?d|ͭc>X9t>Y%3 "7׻|߱㓲Cy_|睩RZqu43,0y]Y6zu~Ww7˫g7YQoꍤP{ ĥ_J߲&))~ժII iik,Ν;g4mgذafϞ}YqqqÇ+**9so߾W^}7MNvǏ_PP sŋ2xfsddd~~~YYٔ)SΝXg}V]]}ԩW^yEH㕟~/f1ᤘL+**$J{$uĈ|0V\\iҤLj#L#vwFo$ cǏ= WnNgOc@#ʪz:*aaA~~ޮ1Wk0hrw>>^alヘSIw YկjAm=׮[ZV/CG՗zG/U]=#7 :h8ԣ_rІe:]S 荤P{ 䛤)Smѯ=iĈ9s8y˗/ϝ;wˎ;VWWە9rHDDr;7;$<<@۵k':p{xxIP6N81==Ν;B5IptG%+R-2Wӱ͞K]v-zr֮_jN6^AF0w+k^ ca h0R&(w5Z˓9k.,ؿ{A^&Y/tesLel7_rOÁ-`{?pAY11 ØQ ?zu캦It8&i/:uڲ.mټ4)iK'vZNuI2w6lPZZz֭2"#I(((pFv驼4hСC^{5bv;\Ø1:z=5KG^lȑ#zttf#OGyՏo9s y^[p=fjߟ~flȡK1Ihйz:|4 [l^v0o߾ȧzJ2 =Ⱦ L&3k֬Q6ٳ'88C>>>2wo0vm0$$m;wtaREK;bfK.uaLٳ{< +߶L1v=u&̙_ǎoF|^nƌ]vZ^YTlwI[VqF)IgΜiLKک}@#2w ?wFWØ35Zݽs顡:<ѫI}0˙xdzIY?g1-?}:?gs:j/xF$p KOS k 9 (u*):֩`ӳ,IH EC`6p?0WX yS4Р0ڿYRW'%-;xpQ cW^ݻwaa!=CcL?@c!E~}a|o^NjzJ{ HIIa a0_6oެ~&uٌ a KeeeZZd]j2ca01KABBs0XXc-77W1QVVց{10B+˖-b p[ Ɩcaai.7jPSScZ1MaÆ yyyL<c@IOO"0c111"ЈݻuV./a :Xք4ܵkג3331u}@=>}zfocGee-[RSSSxyy1-Z*33k@m1c01@Cc@1 0c@+w5cv?@شiSqqf;qDff&C1{E0#kkk" &1yd%)aLς  bXL&$I<cܿ?,,Lҗ1WIb#F`d0l}n۶ahZ偁j @a&IIb<cX,}J @D}hVfYhW~###% ӧO_~˗ wɓ'GEEIɑuK.egg'&&>sΟ?@a 4qʕ{5(m۶g}O>a0!͞={wޭpUUĉ? cA,Y2k,dN%mذ쨨(W zW\-PUU1 nj#yvKBBBppp_ n۶m!rrr&OleْHBOmcs=\k;xOVhhիW򉉉ׯg\}%嫯X٬/\n:ؤIƌ0 F•: 8P-_UUs1cӧϽ{ԗ>lalذaj˗/GGG3c}Yzm$-\P-3c?]lՀ 3ѱ9990 a @01XǟHIENDB`onedrive-2.5.5/docs/puml/conflict_handling_default.puml000066400000000000000000000021671476564400300233320ustar00rootroot00000000000000@startuml start note left: Operational Mode 'onedrive --sync' :Query OneDrive /delta API for online changes; note left: This data is considered the 'source-of-truth'\nLocal data should be a 'replica' of this data :Process received JSON data; if (JSON item is a file) then (yes) if (Does the file exist locally) then (yes) :Compute relevant file hashes; :Check DB for file record; if (DB record found) then (yes) :Compare file hash with DB hash; if (Is the hash different) then (yes) :Log that the local file was modified locally since last sync; :Renaming local file to avoid potential local data loss; note left: Local data loss prevention\nRenamed file will be uploaded as new file else (no) endif else (no) endif else (no) endif :Download file (as per online JSON item) as required; else (no) :Other handling for directories | root objects | deleted items; endif :Performing a database consistency and\nintegrity check on locally stored data; :Scan file system for any new data to upload; note left: The file that was renamed will be uploaded here stop @endumlonedrive-2.5.5/docs/puml/conflict_handling_default_resync.png000066400000000000000000002057241476564400300245300ustar00rootroot00000000000000PNG  IHDR,+u*tEXtcopyleftGenerated by https://plantuml.comvHiTXtplantumlxmTr6+&4$|IbrqN@RD.™||@I4"žvTH]POL봠6My0uz^vIG} / nl9^A>76QƿR軠yy ]j&lJ|l| mMjBcj^sX3x|lMj]Qm2д8rs,w6&rD}<ՈfQ|!3~i^@T"$K"ȢrkDfs/$ GwF+A2& ,ލh Y%ΪXgzuPumFiwC#H 2^,[/؁T+*@95DtP9'։нJb>*GH,YH4@i/|'pk׀y].6zR{o `,Mc#G& KK :W,s^e@||ЇJǪOl)[NkY <<6(+W>X8ʯX@p)yQ#5,5Vk'Q!++W=;E"mע)1c楤/,G>zDZ+>CKu#o fAD^)Iw+sh(z7/n8W;*7dӬ̪ 5N ve7̪+W)o֗?S6xiCol[A_%תs 7skGYKǷ'3&ɌIDATx \UqK\ (QdVi>Kc")Rf.- ZZn+"$ Ͻ3s ~ޯ5w8s̙{y5CPH[Ji @i"m(M-4&DPH[UȰّ(JVa-@i_APJ7b̘w\ m0IPJ̎p)`.wMLLܱcG\\܁nܸ4i -JUK[2\N:5z6mx_|1::lH[(*ݻSNP׷o߳g @i RuҖܺuW^Сm۶OҖXjԨQIԝi\{=ڷoyH[ m_i )L٧-x+_9i %mIL3]~(bx~Gbbe^:-˾^LˑRfӖ \޽ۭ[7;p-ڒI9!s2 XY%ӖvݸD^Æ/J)-۷*GO[I2<|FF|菓çvwwoo$,bYxyrpkذ~=.LEѱX/ WDnUmI[k-k~֩S[yhj-]C^<=~.WYzqMqΘxy%HWʝk6m$Iwmmm|}VFk|Uϗ+=SkԎ;iܜ 7y}ZڢV>װ^!^Ν[^Q:uuic=:i 埶kK1ϔ'AAgE,M~_Z9'G'22w!F*7(~饿/;n>!&F:lKsW劽d'Jn&MzWЯSgvI)ȉbw:rUbYATMqΘw^1V̌c> =7)NNM׭_x0%%u۠A}Ě?~%P0tҾԎhсŻb΁[!ܧ~Z}џwe=aSW_}=+"bЈ""&GFXfΝ$''aÆyXիPiKڵxͭ'ԩ-O2O$-'%&-x.-\o޼\=7WywI[݉ b+-J)V+\<=88H,SEnի{߾}@BE7]%2rf ?ϟo߾δeժUP9 R^iӷo_휥M6[n EwZ'Pcƌ :_N:`0̟??..֭[Y@B` @UR(2)I[ m@%@H[PT -*i m@BH[PT -*ƍ/MP)<-Y$==]~iGGG3DJ)++O\%&&߿!T:-(g={)mvuu;v,H[PbccZln0BBBH[Pܹi0Zl)~:;;~TR-(ǏgXaTR-(YYYRҬY3ooo H[@U$Gaa!g`gϞ-<@UȰ7B܏GKcnnnG mP(+#';  mP(֤-' L?`0|\i P(+Ӗ \n޼13ن ~wH[( Ar)SDFF:t*u1ܾ}j{jH[Bi \ W\'O5@-5j(݉h7X}:^=V޷;bl܏Eǎ{Y'\6i Qb~̇+BPERҹsUqz¨QbԪ)bgN>΋9,3C=&}{vsBzx"#sWգC=za߾AkWSk֊s C1bji1*=ҿka1FA(ܹs˖-c pJ-mSbiVI5WW eUk-OrbV]\ l3^^jCacW܋L=]Zι߼y3y3gU3:ϝ.jZra|gAgtZ{X;?mQL+JIҖK>}I/P.bbb㹄(E{.z~j}yva, ޽}CkVܣZ5^3j=/]q/oՎWύRV-y{VMY+Ν.wLQ,h7zK@&2kS%""/P^>3.!"mn[f04QDʿoFHJoG^?rnZգziƛ+VFg݋uojyfΫUSkָ ZvQLVmll,,-_9nQ< :{eŽ-âgYi̙3q~KX~rvv.n}Hu={6@etۅ)b6TAO?|~Q:,,J=k^&MXAb,vfۋ2aHQ ݻw8q+EΝ*^CsVMYb2tjWۅbQ~i i-DG}mmmZ#Jȼv_,*oV<֭<AdX+S!!#\\ NNMEg/E UԎ]q/oՎtzl۵{$&v^Ԫ5_:B(ڈ%&m!@4|EZOς^}A3믋 {7z&>|cӦMłx8\&QDdK=ǎ": mTRX|MQ_DOƍE#ZH[H[(XL[d2%%E~)7oL>k׮Ν{饗F%Wݻɓ'[o5i$i ~bKJJې:Xܯq 52\GfiMllc=ѸӧO7lʕ+b? LۓO>gp{wҥϋA{joo/80E(ś>W;ָƍS.c(,ŴXXhhhΝřMKK:tvjCV̘1O=ɿu4 }]nnCH-@BTbʳwib/h--[<|*WHNNE &|ͯ~mllvĤZZsf[[[5nYlRN.5ׯtZ.\طo_iEogp,~%;;|~ۭ[7i900p&w+!!Agnyq,eMObRjV#RlPmHC;t萴,l֬٬YB m :/)b};|ŽQ;mUV͚5OAW_5br[{j4kroKƍ$ʪ_~ĂxѸEI666ҲXIڵK.vvv7oϞ=.]*-/YDQH-t8R{&ڏQ;p#wÔ_ Z}Ѹ9{y<==ׯ_O-P=-cGK3%iG,ozoow{,.Usq[1c_uBBBQQm grJ&B,NwѶmm۶ <ؼ|SoXXX>}8pԩS_xeW\iذӧ{[<(-8p@'M"/Ϟ={sΙ%u1~x)V$mѸQEunbEbqOOO{[_ Z}{[;vk$i i i}2pXT\ixk1mٻwoƍ׬Y-fbyǎү:tʳЈ^tiΝWjs;vYz_dBRk7??_@XXh\X1ָLl׮sLz!=Dlpđ1 QQQyyyǏ6CqrM!C̘1$iG}$?K.cƌĊ &Mo$1D&mQlPmHC"ΩŎ[rss`0i ~rfR-dpy1I7^)n"|>>>k"?.ŋ1lٲU@@\}ժU΢~֭+~^= 0"7OjM]~og1b„ ܹs8w 6_[[ | 6olFlӦMIҖܷ~Ă%E&V-G^?%qhk76jűvvv͛71cPc+W/Vői TEVe޼Y+s=v|pJoAU~AH[tҖ¢`hX,7#y'-4t H+Oy^v ߿g-qrĈ!/:?@{_eZ5W[{իgۡC@bV#U jb%FF|El"{/=|hww{o1fAzԸWKpq1ԬYS}]OPjKxV9OnҖǏ7jԩS2<Wjj*sxTp#FHKKKNNڵȑ#Kٳgs -P iiu:On:L~сO8s`J_vDݺ}Q^6%-/,وZ5t SRR Gn_2Y4y:>Riځl%E,M~9!=z<+!C=|[=Gjܫ}/n_?GWs`5mQ<_r5ų\ﴊ"1&j2~.PaEDD8;;7i^3Y%?o<.!J!mpM>񓴜MZj.eX8bhY4eܲbW3.NkrsHmmmbڢ@JNRr>>'K97o=QW;R9G~־Z̏X'ZXE|r,m;wnNNi\ڵk\Bi BbciY,rPLϹ߲\Gg#jՌ#{Q#;oo[NOԎbڢv &[)6nkkcՕZjoHwtK}tuI|nyr,m9zub}iKm3[[o]y%ȝiWj;aQDn0UDA$e^uzS2]H\V|1-b؋xVgvkOѩ+։.{[LNŴE,; aLz7sLJ'm0aЍ;N8RZ?mOg2~¢T~^iKh訧"?:DH&Yq7M S<ɩ]H;uj;y7 22wځEӳK *ph|[=GvWD+UD[L[r2mYlYbb"S_}癙\?i Xy::6E,S!!#\\ b~Ţʸ_jQn77 ´1}}睗<<\E rF"5X͸DG}mmmZ#J{vI[;tWkHR;IԢ6|`ݽ.:)o9RlB}]NbQXLNŴE,;,ӖzV.Pm>}zܹ\<i XPO)yd@)Eزeˆ e#''?-@-ҖfJHH` ZBBBrssr mTΝ; ,صka~?&j mPE"Ydg}vef@ofϞ]PP5i R!++kʔ);q$(!Q;wɓSSSZ mPc"Ϗ`00@q-@BT``-@BPH[@i Q((Di P H[Ui *@@ P!mT-@H[Ui *@@ P<ݸqIb[*%K/ӖhP鐶.R#m@ ?+*;Tҳry>.#m\vڈ CxxxO~駢">@+MG@>t{~?fϞMdOi H[B5C7"""i c֬Yzg}611iii?ׯߵkXڵk}}}xS,oۮ]vb̙3i H |AǎoiKn}옘.]H׸qիWk֬ѣ7|C m"i]^=y k֬)^ >ѱiӦbAOرc]\\į̙#vĉ #߸qmV-[Ҥ=QkX=m۷Eׯ_ֈwwmraÆۋ>SE ŋ+_obGAZ79,7hРO>7.iW#mܗEC޽{c|#t>vI77Xn޼񶯽ڌ34n]tohGj-@L[$zJHH'''~EҲxif˖->,-J͚5]2Vи'j'RٿiM~N6l<i7tvvnР / GE>|[tǐ=M"j-@L[߲lllrsse geVV5k={;@USN%p;wꫯi ,.Ǐ+EжaÆ˗y蒟?uT Eׯ-( 1@8tPddi tůzٳ7Pߢ Pi P H[PT -*i m@BH[PT -*ƍ/MP)<-Y$==]~iGGG3DJ)++ur"-111}f4~x^6{?@j`ʔ)9997*uU̎>}:<<i @uQ i @@B(3-Ν;}||ݻ',**j֬Yzzz^^^>}ׯ#K󳱱"j)Fs1 ...7oV={k׮?p``sxi ʚZJZ^l/, mٲ_ݻTQ5ja_vO>Ǐێ7_~em׮][w_痜,V6jԨ]Ts?3|۷TEBpZjI+]&^joo;H[P׬YiӦ^Zp󚙙{'LԢ_TTs5W"p-/iI-(w~Gtrieee_z뭷Ĝ߾}9VHHHӧOƍׯߵkXŋ֭+~.Yx`B^?У[sww߶m[Iz(-b'Nᅬ?oi M.]}Ye~~/looߢEkJ+,YiccaQq#v*9;;oܸ… 6mb/'(ݴժU%y࢘9sk׮=O?W_MJJ*:@$ 4HNN6ЩS(ʕ+yۅ Tk2##gyv!ל8qSӦM^BBBkt[iժղeԎf͚͛7s"_wŮo-*~)j1fΜioo߳gQFmܸQ V;wN~)yk׮nzڵ&-Z~z???رcEWX/eooofEglrCfSs-[ԾƼ]%pLT.\(PΜ9jժVZ_RK[]fߵk-$W W^5^+(6;jԨG}tС1#KڢU J BJ?ma-77w̙W^q 4Ij*c!/П۟VX!mꅴTCYYYrҢE P= 0YZx>.7^H[@'y>.7^H[@$?+ @Bӧk!2@R̎ P(Ry @n޼1dZ(<dԐ!/GN2%22СC7}֭[ @}0޽{I[H[H[(qȹtPʕ+ (ᙙ-- %C81cƜ={ 2?{ 6P(-87o3FL{(\nJBBdggg3P^={P*kQPi i 3X&{ɓH[H[(21!m)PM8irwԩ>y{fY5k6hc7ܼC&*8:6?:괅Py9sfٲeLTӦM#m!m!m!m~ c&{/=?ֹs-.-JjŧOf"شiS||_8ż'PJjV%cG(_{ttl,K;$ڻ語ashJ\1W'hȐ~qB-je^}U*3f0PA\zUD-.m3ńy:ujk^QnQ^6%+Җ}/^F ㉌]W)CNNM׭_x0%%u۠A}Ě?~%RC_1VS l~zŜÇ6.ԶRڮF )>Qu0iһzmQܵFW smESj`hb~o8eƽݵkR&22Jd5M)fK(ggj>{U)b''(((ОPTEW[~㸋=Kԓ͛7]]/;xnB9Kfjx{\wZ'(<'~==-語ashJV(yn:w $m!m(Zv< ~eZeL[JeG۶muKMMldlR>:--X|2C1ݻwsZir[vmǚ7k_bm<[U=ը-_K+7,nݴA_mohLwZ'v6J&ǮU ^-Kv=ڊ#.y&L)LB m1o߾ƍ^:;;{͚5byU&m)B TT<--K[<=ݾ]y%ȝiWZVMZxj^Ĭ~S2k#⥿Ϙ1twwmoow{[wZ'v6Jj!=$uvU{؋uz*Ɂ+>W+BBViK̙c<z祙L-^ߤ׮]8qݐ!Crrr ,pvvl<Ŧ9looߠA>}dffj׿|aD-Z|o8?MA>|cӦMłxi`tx[;vػtϑqF1VZRVjMH?ǘơmd<aѨ`1vkam b>,,L߬Y-[%,,J=k^%O[ t"姂[7I :rt”MqD~e3-M00s[wZ'v6Jj'L)?{'4m&ZgWժ9V4U+z'( աCi i Ŝ\/rmy璒4,$$Cz]&&&;w^5j\w'OT)6wҥϿ[v?8000oO>vڢ˜1cz꩓޽7/1?7q,No6;wNHHHKK:t9=y+}+.77СC,Vb#hS퍪^,?^1OAk|XtVЮ_=izذaٟ}YzSZ~i iiKZ[a%O[Kx֫gۮ#1?,zQ_[[ֿ숒]w,THS/M5.RڮFI”w}ѱ(bA PsBXlJgWժ9V4Ur5k>p=qƎ}RA`~z'i i ܃>(& A`ccy|%[[[ie˖]]]jTŦv-ZK˿vڢ؂"Jˢ)ioG۪|KT_gլYYfpGOQD^J2VI謠]Mmz]={V`,t%ҖJ\j^e? J/-TX(ZjլYS+=D]vuN~@v}\iY,h-j-aq j5٣q2vz:ix6:gٳspp\~}GjoTM*xZ=j']â~٦q5e=DBR){_.ަMK@S(--L[gܖK& Q , NZh_={իΝk"m){[V'SLe09G:gI_vZGGG+ߍu?Fϡ+*rIqP;?,jL:VMm٦ŴE(T".PH[H[(n믿6nx͚5b$+J,37{DDD``yҥ;w>V-bw`Yرc{!?Ê壏>" s1t2f=I4iRΝ?gs׻woo qduڢ&)Gyxwnv-~X*tL~;MiQ"m!mP(--ꕶOݺuիW?5aÆk׮͚5e˖5Ӗuy{{6-? ͝;׊EL_u;;͛Ϙ1CZoobAֆx^r?tvv%s{ʕ+۴i#VYGmiJD:t**xZ=j'EIԪW:۴%ҖK7U5ozoo {i U/md3k`FT^ZNơb"m!m!m!m)ҹsUCiK3bĈ]9)î]CRܼyBO}S 8TA RI[H[(/3f`-""ٹI&ڥK*2i i sΜ&x*={l߾-b"d C͚5>u}7x I睴QnnCNVbcc `f'G`pyZzk1f֭ճ! 1i䩟 %oذ~=.Vܸ(b;5hõQ##_,j*)Zjl\t=LZ:ijxJ2--СC֭c"1cFQQi iK}/$sNHOddʿztȐ~GO6:0}s >XZ)fAA[&fX{R5yt SRR GO ѯSgvI)tVZˎ[O\0jԫÆdhGq&}Q6}ԓ(Zjl\t/+ƻӨ)Vƞ=6ƥS|p )#s=>j qyx{WSG5)+hO>رc &KERӖ/W϶]GbbKTHhw}ѱ(bAIE󲃃Vm}& /;fsBXL=6ժGݺuܜE&QkGg)-Z>|~Ӹ$&m!@TZ"R{|Td|Y0jKSV m!mTIwܙ7oΝ;(3Ψ- P,^xyyyLW|ٳ "m!mPH[H[&O޵rĤ<"'n1nW.:K܌CBi ~]J(5V~ھcүw4i9'bQ|:A+ 00i cڢ+"[W϶CĤJo)V6oSçvwwoo$i}3^3iһC[zX˖-N'$2Z?E_iR{ϯkf̓0SRi h-6Z5W xx6jd7r+E/Y:PfM(_78:6 T;iܜůD;ŕj-1M<D 3a6I[Z-iK~Oe-&ғ|ЩS[4f䜐=ܕ!F3t}޿]{ :O4^#^ M[رsUv)=6Z?E޽}6 :Kzٵk{]895]~”mQ|~QB}e_/ %~u~(ݺu4]iiۋ]90|`QJޓz+uCqx}~%Nߵ F:lKo=mpH[P91? n%ڔJڢئgrΥ͛i4uQ>kc󠇇GSեvڷ SS:uj 6?~iZ?E K0w4Aع_I>qtן9G&BU\I(vFqZ bݰb~{M0i *nRzb ԪUK:KOJמݻiFm?5M[0W>_5v߼kŚr$ll?WWf(vFqZ &P(TkAtw^]pS<_jQn77gшŷ6I[Z-@iK(WJ (-\ap-T菓{Τ-0i H[Jԭ[m6z%mpH[@BpH[@ڢyEqXOwj풞Jk趗Jwi Bi H[H[H[H[{wř}qx@KPl%D3G1oL:c\0^17kdIF x x:`bc4 q$(Cvywj{j}?|TUOU=t./4c(,nڢqH[H[ai H[H[H[H[ai =m9?/tzxj.+f{_-n+x?gʍ|eҜ }e-[ <[<3\Fj{m;/ۿտ~r) jXYؖ?G{voQl჻vu ^{YU 7 "m@_-%8xЗSo_͝;zUt_j_x|F_vsύ(p̌,eyy 7>XE,/dɻZm4 G|^)q ß.ȥʓsL\oV  TKo7Ϟ-qҲREVՀyH[Zյk,0L~ @@Sͩѣus%}us|.yV_m?UKk^).]֛UB,,j{zϤIsUdU (.ܜ7U%%%)0 CFFgN@߿-kԞں3|ht~.S,_qyN:Y{o*jyޖ5MT,,lK.V칼jK?ww_r~h"j@qA-,88X\`8sV-bOo*CLƌ K1o7'?o7ݗG{YU 7 "mim#G_{{{GEEq-i l@E'.5=Ot:W?GmF|H>hn]|rhM_#۵\]{ʿͳz߾1aUj{voE~ݽ8xjVYֿ~QQ9?|7>XP׮.Çp*nD"##=<<^TT$zR"&rss9mMI[O[^>|p 3f L@~H[}/<<|֭3i @~!miw<- mҖf ol mi ?4Wdd :-C\ҳrE-C2g|\@e)?Ѵ!>>~}3ga\Ύ+!!Ν;\i H[jhhX|yee5nUZZj*@ j mi -@ByNNk׮},//lifIIIXXX| mH[Z''ŋ7:--- @-V\pI @Q .NNN7o?4յ^LչY mh \̧EKi @8995aTuVT9"B*uX{[*++loF']|+W޽;;;;44T4.PEĻ[UUeV}}(THHg}&ٰaԩS}Ϟ=Q-i H[NakkkKpuuft:Ν;K3Νc͚5kZIT]]-&ꜝ-=mitu TTTdR5(֘)^/|A19lcW^ :y7rŊ 4{` @}HĿrvv9BVZO+ڟb|/bӖFWW\믿~>K:[1?"iq-;v쨮0?y"$$d;w jM-`K4tҞ={;vȊ ߋyg_y7Μ9sʕoGyyyrb-n@eeePP?6rGd|j$2Hegg̙c!mٿ^9rdNNQ 0hr m[ \t/@燄t%%%SNuuu޲e4311%(((55|=z0yƭbӖFW7_^裏߮X T<"P7+,,5j< /q޽{nТE,-ȑ#_x` @|kv ` @.h.7n$jM-@VG }H[D-Ɂ\@` @.D-Q mC_&eٲeð'jM-zpaMa 49%= 0&$&䀖D8rȠA޽+u떧gQQQMMMDDD=j0fddzT49pFXXضmۤiӦ^{mͷnڿpp[Nwر۷oFC_ H[q{G5` RH[q{sp7oL& s׮]0- }A\RRRQQb`0dddPE-\YY^@&,>lPPH=oo﨨(*49őKNN 1.&rssC_GΝ;>z1cP3o!>>>uV495Co !!!<49xꩧe̙<49+<<49+z<49Uq }AZi 4׍7>QQQw]]f͝;wh]@se˖UVV^6?C\\ 0hr m&Q /hri +ڵkE+;;[YRRֽ{RZ,0hr mh \/^А ͌.\pҤI4Z` @D-899ݼySLLWWz1QWWFM-@8995aTuմ.bai9mC_&%999((e7onġiMJݴE-pQL["""bcc_7/@i jԒ陕uҥ'N훶8;;_zJL[Ŵŋaaa=OM-`Q0r۷:u̔\o߾...;vlzÇ-$ȰdLLN{򴯕(S+DlBlHlNlrßM-QVZZ@9nnٲ߿{[2eƍkjjvڥ5#OhY=22|ƍӾ̙3]ޤBƎ+oj\/@i ذr-QK[z>Թsg9uuu΍f% t:XiĄW^֮eay ٳ9-_b` mMkkk1bǎ=<<|QNEEji77sYV7O[ڨbURUU@[2i H[Œ?>ݻNgΜCQ2M4TZ-=;wݻwNWQQa F1}tRX XqرC>}Z32KCg>O?tqNG5Zj6Pk0BCCǍ'6moZZ43))EgffR2M4TZ-=۷o ] }a i 8nCz͛@{ b&*`-s׮]0-maDKL3 /l"mK/ŨQ/nݺj /l"m|\@G2M4TZ-(zL0: /l"mG/-Æ 2e hH[QH= 8X X\ 8X XwYnի?͟?_0 DqJ'ήH[@CH[Gt51:ZlفjkkIOO`YMM8,_|՜i $##cŊ%%% u}-i lM w}i H[}'fm,\i H[n}W qW m۷,Y@ݴia- mH[{_ F}+EH[@vݻG mx@;ZbOo@qII =СC999 mD||<#=vÆ  mʕ+Aprrj2SSS=<^޳gOppp׮]ӧOO8Qسgψ .4oXwݺu7}*MѩSQڨ(///+Q!j`has6m4hhO=ԉ',/ޭ[K.//+'OWM m@*@8t"F 6<ӒRUdeN<)QhTcս-hUi֨qs4zTEEEn&m&ꧠ@ޖn-H[@?~O>۶mc91p^~Chhhdd 瞓V=zgyfѢEyyyƍQj*]1G9"4瘘#F!buuWL/Y$R1!@KvZooo7771.qĉzJ=6m2~i^ իŒÇ߾}_r%&&O766ht^|pJJآo>ŭ<^^^J6RoXgnTiL[~W+Mw$)-1m5n+)/ݻKӻw /~'~bbg $+ݱc]z'P<6h-H[@,-.PPtիW?vv\7n m@ m! mA;/~1tМ;;- mH[O@NH[@vbÆ ÇpB@ΝKIIaݹs2-Xl= mi @؛ݻw>|!v9 mi @؛ 0Ʋ$ -i `ccchK999 mw}xbp66oޜĹi H[UWW/\ȑ#b /g]- mH[Gw˗3,RJJJĉ߱ci hi >?p}DJ"N&o&N,] alÅ `H[vBK-@vpa}!%@]b0i .1\@_van.L/D0hK &]"m;w5 = ƃX X\RRRQQb`0dddPEv2M4TZ-`ʆ*.r㫯 nhh8e hH[9R\ƁEw6Pkwww6lXQQa8pKLR92M4TZ-`n޼O6L0Ę1c@b&*`-p/[5}/֭[@b&*`-p~m0`@`` tA,C_DCEBKi˄ x>. b&*`-pR2l0 X X\3e hH[H3e hH[:7n܈6͟?_0ltׯ_!- mZ.]ZYYyf_xqժU.@Z@ b-i hi 8xrȑAݽ{Wzy-OOϢ=z :`0H...AAAs m  m%,,l۶mtrriky[n߿?88XN;v۷ 4u m  mѣ{{z}AAٻwoܹ 9s:t--@X.;wܻwɓ9:-pBBBˆ#ONPhϨxF@ڢ%p9vСCCCCO>-͙~_MMMYYٳg?~:~Ni hi [iiUb7N~Y[[;m4777__ߴ4ifRR^̤i hi g"3Dll{>}RRR-Z*^ڵKZxرݻwÇK3ŒAAAb?pd9:uԸ-@LLN{ԶxtJLLՎ˜bĞ{e|\j gܾ}_ϧ1i @i @b!&QQQbݭ[vEL_tILH L2eƍ555vL77_|ԩSbp>;;;00P ,7nm3̙3-1Ǝ+Oi>h|h͛7ӘɁ:{ܪ/gbxZ t:_ƑfΝ;5kݻ>ZnK,++^zYؖ1mIEY8 c Si>hٳg-i K.MK[,/v9VZO+{xxرBmkt[is]]gjE1-i 4+pѣ0 +={V"oq̙+WH0 ͥr-5%m4u@ 3f>|a /P3A:i 8Pѷ?~BBBnJZpJi-2330`-H[QHjFq->%m4u@ p͓Җ!C|\@AI[ MЂ\A:i 8Y<CP5 IDAT8HS mܹs''''!!a [o%zTٰa01-M-:k׮}G+V8tիW_~ (==J?7nܨӟ$>k֬Ɂ@;Zreii)V]t)!!!))\49h7)))vb ؓŋ744p@@}Gah ؟h-M-ԉ'>3*++[x1_)@@ҥKv$xH[H[TX{ "-M-Zݽ{p.]Zf =-M-ZWaaaJJ PA-M-ZWbbbII CPA|99949hE?QWWaN}H[H[+Wv7XZjjɯ0;l۟Xҍ7xt &휶jBamܙ6H[|||R??oVL=p=ݎ/ԩSSLP-bK)-FkUIXkɒ%o333njc|NBBXn,M?+ pƫ-VZU%-M-l)m1 '|}}b*M4-bx)Mo׮]W)//ƫ(n])- iK``tÅPTT裏ZHxK. Gj|{zgX-bbO<)WZUlu1o~ƿݵk</aa늇-jհuvi @i [J[\\\i1!^ŗG uuuIѹsFU˷pFKnBҵk9;;wIkUby+Z 79psrr&LweZ*OQۺXL5֪[&{饗>3UUUƿ/}||*++&ĜKm]T7^]m1v]i &=miν-3>MD~iIII]]]ii>|'8x7_jGӿ/? T;|wSm1ţӸ. mhr m`Ki{9|-=GbZSSeĉ|ՍX!,Y$$$D` ZJlWBUUՑ#GƍM[%)*慏?suuuZZXWۺXLѣGhU>>w^{'WX!W^my늇T[L4K-H[H[tϣ;>1a-7x;>>0/MlW^D ^^^bl,RՍX!\rw_rMH[E ]v>|۵WQin8@SRR "J޷oj19|KH4 Yf5XK}m&f3g_>~,o]T᛼j)uvi @i 458<6nϟ?z-H[H[Raaao٢UӖqԳv7n m@@uǷ@nժU<̙3MF H[ZdggkRJHHɁ@+JLL,//g 8/2''S&0%%!( ܹéi @i Vt޽K2AYYm@@-dee>|(`4q@@-DFF0XFFFVV;-M-HeeeLL Q^=z4%%s&m̙3111؟?|ӦM49h<aٲe'OɁ@{ڽ{ҥKSRRD|y۶mg-M-:smذ>_ {$zTX^<Ɂ@`d-@ԁD0 mMhI-@ԁD0 mMhI-@ԁD0 mMhI-`]fa[C6./lZKJJ***Ra ߤ--\YYYpp= 744PE&mMhQ-`ƌ#.R`0DEEQ9A:i ؿyyyy >H0 ͥr-5%m4u@ 3f>|a /P3A:i 8Pѷ?~BBBnJZpJi-2330`-H[QHjFq->%m4u@ p͓Җ!C|\@AI[ MЂ\A:i 8Y<CP8HS mhhhXj[[oF||mvBB;whu@9jfmn/\j* mH[@D- p mi #G 4ݻ[nyzzDDDcСAmFF^wqq JMM%jiڵ啝-,)) ޽{xxxii) mM mcm۶IӦM͛oݺ`:رco.,, hxbfH3#""ź .4i mM m;z޽{=^_PP f:w,-`|Μ9"jE7o?4յ^LչsL~̵@sν{NF'''i9m@ AK0 -@@;6tӧOKsWSSSVV&ihϟ駟? jIMMprr2iKM).iKDDDllף&T\IEVN`q m@:n8emmi|}}ҤIII...z>33S{"}#IZ1!!A{sApq;qŴŋaaa=O9RN8п_ mi to~]Q]-R.PQQ{n__ߏ?Xc|& -YhCxwwaÆ8%&rss[|--1qyVZLr!CH!!!=W#FxoKqqرcw>ݕ+W#==x~N3339)))AAA...LNNFW7ٓXiӧXxѢE] ûtb\GGmzMOO'|rذa///11f̘X mi 8 k(++sqqǎ`O0i˔)S6nXSSk.^zTTTee-[U__(tssۿ˗O:%FWWܓ[vEL_tILX+g[NQAA-`KbQFI_WVZ mi 86L&-z/DiiiN33ՊWWW:gggifVV^ך;wc=6k֬ݻw[^]q\^+QKf-]ƍ.Mo#jym8[\ԾI$-&-nnnΝӸ]ycǎ Z駟~W,QݓFѴE3\ZMHKɄ ?ik-AA.Rpҥ={?%wر ,h7^ٳj1tbs̑ggΜr,QխJ[GyyytDDMI%m!jv4x)76l'6m-%H[9EM.]Lt~~~HHHnhRRR2uTWWWoo-[h\}ƍ{E3}}}]\\RSS-QխJ[*::GSrG(\q[QUOmZgKs Q  IOIM[ l p\@p7!驷>ik-AA Zp&Դhl p\\Zݠ %@A Q  mhr mA# ..D-n0%mi @ڂF$%%) CFFU\4uV(++ ;m9Ht|.bm'jv mi lҘ1cNt5(*`%05u@ yyyy >\t9HtDnn.n0m:i Guu3DM{v!h+6-H[𿄆uV`J%05u@ / 0g A[hAS#F=v!h6-H[`j޼yRmȐ!ؐ[n:t. >S-S i˵k>+VO}}ׯS vF|T\">+z՜tp.\V\y1Lε9nܸQWWgϞ 6ܺui h@J[Dk6`***@$F֭ךNhq_-[g4uC-)))v ؇؆N: Ѫ>G:i˧~z.̀=)--&pСq^6玴4uҖ'N|g\ \/^Wt QQQ|{mi͚5.\G:i-]1`򒒒8!hw~!_ B]b>ԁvH[RSS c%$$Wgr#-EffC'䷤- mZ8mw7vҥKk֬-Y2څcޒTTT85 - mZ1m),,LII ؽ8֮]S1˺ueeeAAAr"An܄ !mi `EڒXRR5{ӟrrr8-h)))yyy^?w^1bR" Acccl-pд%>> 06li@9}^?z_|yJwX1KuӖ+WɩLMMW#dhwڽqn^VZաΖWxh]Ҵ͙Jfwoooo}m9e JOO2d7ozm=9xtvhھl%E˞Xׯ_'mБӖ_ٛvi־psuԉjQ;f"b)-֭[I[@rGy$55ĉ/-W^5P6//N[-?-lޖVD5D#tOŋo /B```L%ӖI&_|~MM͜9sۧO1!^DqNԩΘ1RZ6**K('#T_M6 4k׮O=ԉ'jk@TX(Sm._[o[μCؓ7|S#Dc=b'qm߾}ҥOO[={=8p`rrVN>=qDQ`Ϟ=#"".\qbq pt:o-jP+|kC*چe,׉׹ޒExVpy|5j? [ҵ,-oN޽{˿rxY\\l|P6MDi.*#igkqI[@|".uř:<m4iܴ[B_kv~~yib?0wwJmjkν-je(ooОhE[IKK۷V}%%%uuu7*&GxoZ;شqvϤ? ->mQF+\4-ZگP&tBMN[VEÇx≃N>]boӄN}- =Fŋ%mi im۶ qٞmQF+\4-ZگP&tBMN[VE>쳢{*>N{ߦ (kZz--;%mi Вi zC qqq[lo}w'&J-W^صkÇo߾]~~;I[^;I&Rۨi9i2O CӞ=y7\]]x _(RRR{-f۷Oq+bD!V-k׮#jCViI[*چd^)NI+~@H[_beԮъgE-'i+)ݪPӖUq۶m5?"6MDYײc$m!mMi _mGkh%ׯ_'mБt(ׯ?=ч~4uo~%mAsܸq@{]60`I m!ma @SOZj#<2sL@k%$$pZ@ڂFk׮Zz5CPԁK[Xv/]q*F{9uTff&CPԁK[ Ek0`ܹi@{%`C֭[?~-ui˽{.]5oeee<@Zd_+ i"dee>|0`Ǥ'4qNЎ8!m۶-77 i h@[-Bdd$ث,Nƍ8>}-퓶TVVp=ѣGSRR8 ֯_wqrFۨ}xb i h@-™3gbbb'M88ܹdddpFk+//_p ܑg"TUUEFFr/` òeN#ٻ;oJSU@$Ī6k]Q#ڿ-je7^jvmA(mwUK8F`$J4>˶/γOf&s͕dr>Ι^g<þ?ӧO_|yCC8Y[o=ӦMLڂ+m .[璏1H'm?c+?+w}c oݺCoڂcCh?[0tq$/I[p  mPl޻Aڂ$m1wCIڂcxi :d  u&i A-M '555ynW?i *96{w,[b.O-t Nڂc䤺xbf]D'I[P@ Ucǎ.{ /0xપ*CI[P@ UW_}*++kkk{wCI[P@ UsNeeexz9r%64i;; [;si Av2mUgqrχǩ>[oر'Ծ; /^qW'+*3K߼檫K/LZ~<6j[j+Xwц WIdf501?go{Vwx'ns6iy%o~ooWojNT3Gmw޸Ϳn9䓏Kmniv<$mٸq%KTPfϞ?woi ApR^>po=^gC Moi94ryllI(V X3:UIllu-Zx[Vg;ꨡ|:nOdr| I5[-Jy;ÇyEI[ ogիgG~z=G~ L"͓-$%q{O 89fwmsT)IhsK-Y̜9sO?tcccӖUN}=3_{\,;.++{ꩧZЮvIjݡ]Jڲ c>! "mi!4ʷ 0x㕥EGfGOɃ#Ft' *΂Ӗ>om߾jλn6gK)ڊ?g?^zVVgyw 20W|5{g?Ն;ꨡWS_ڜ9UJҢ7RiK<6ؼS ̙5o޼×LҖh}UU__R#ikhnu9I7L[ږ0䐶D->Dd۶mիW4O:uРA{4iRCCC| sk֬W'NاOC9sټys|Nqqqy3fy ӟu"I[޹s_^UVVv]w/W\qEXG&¯sJKKßB#ms+?%Uwj!ͪ&j?!CO /j-w}K/ M}ݩ1tNho߾7KҬj-jטiDv}KHJvmO8)>:mIrXqƽiuرc7u)\wuӦM5jTMMͦM.69},: H]i_Hg?PfG3\2Tz'ΐjn!hc&'mtܮOu6jvmE8Zx;^~ . ͓P m1iţ—_/{#3oK$?#_yhvCM2~-LWPܹ3qST"ieeecX;扦׬Y3dȐh<~LR-ֹiKJZ60~y]B33?Լexmݻ_OOn0~MZIm^mX]2l$lڊիW}}:t裏>Ii t%/0yG/;¦og^esCJvݷ7Mm%2d}ݷe˖{o֭I+}K[rmK(G[_:g+сk[R\0{Yf}_K/n%6 ymK-jiv.U#-+lڊmz-[I|R*-8!mմeԨϯ2DH[CC{I[s=wab޼ycǎ E۟}ٳ:+}Kܹ7ޘ8qbӖk&~ߖO?=}fb^{mG-s+/^YDpʕÇ6lX.ZK!H=YRmc|N;-~ӐTآ/oX]R5bR֮0a¯~;v,[,yR*-8QiK>{„S9r_Ŀٵ-t`gL[{~~Gyd^*++yCAf1m ׾޽{Uw졇&JwWˉRe7v =;B?.p%mw̴xyRgT]ەjvmŒ%K=бVJT:H[p Ӗh]NvՉ'V\9%'Җ;S#^xaMt4w%}0䐶D-^zFY]VVڧϡ_6?o^yСB;įqIj9-ͿG?;]Æ n'{ [7mg_+*:~a"~MG}3,/<:|fؒ$)ʤGY Җ6]y啛6mZ~1c&Or^~ 9-8Q@iˮWN[Uu駟5ڤI}ۗ?qPbG6l|+_9'V'HIW&>`дeܹyP(Λ7袋o߮rϟ/mCi A*PFuЍo>M7l < >; /FX&ҢY?S-.񢆵~iKWĭ?ov-\jgiK63L EܟO`ֺ_O#!>`дeL(p/V^-mCi A~om}&\B#}4ˏvډ}O|"-~M=?Ǐ6DabҖT-|nР09Ut_Lx]iKzpӖmgh9RT?jYy;ÇeIW&>`д7\dR -ܲgi rH[p Җe믊>1bDy:LC˿޹=Re~Mo6j91v ?тSTSG7$?ix1qAIlږT"UgOZVӟzraPe>RL}qiK0c &~{?#T: m1K[ e7N;㌓C8ErCs_ڛZܴiWyK!mIryU΂azwͻ-?-T2Y'w)6 Jf|noi8u"UgOZֹkO OzӖT+few0th?3 N(X-`!m1K[[Be{QC>??}'Q~A4$iKŅ2۾}{އ%Uˉ^Ď/;^ ӯ׬jorwg20W|5@,}Yfg;[|'Q63 /I/uiof\hr "jBYlђ$m1r̤IJJJ裏nq˾E-bC9rРAÆ n `hI^gҤIE{_|qEEEtKcc>G-ݻY>;.ZwcuD .%i )A@.Zre֍ mCi (C^̙39jihh5kŋG/bʔ)mޝB.A-PLH[rLbRRRrg'?n(jinnڶmɓʢeM4Iz J_rnC&-$ 80%Dر<Z-[,ԥBb J_rnC&-9#%.iRq \YlAK uȄ 7;=PҋYG- fZxQSLdUKW'p権m۶M<, \&M$p Eҗ^|oK%ѲeˢK]JKK+**.dUKZK]ƍ'p Eҗ^yIҢB J_rbB{I[V&K{n%'* \D-ty%'* ϥ \D-t"VKN Th/i @kZ&Eҗ^Z>Eҗ^B.O%'*Յ3 Q ݧUK -΅Kgeo1ޘp.D^8.{Ƅs!t[7& -0b1\$mp] B%i 3 買oL8"/I[\SSS3N.b@ڤgXlŊ.,b@乺x?Èbf]@J_rbB{I[߸qKtʪt][*}ɉ %m'O.-- gXpŐ msQ J_ 9 :pŐ#m(/t(i @zQ J_ 9PRWW0D-(}1CI[a7& 9&i 3 Ƅ!$mpޘ0 -0dxcl8oLrMg CI皚Ҝa+(}1`I[\uuummm3X,b ]CK***K #盛uJ_ 9.i @7n\G- fZxQSL@N9-ye(jinnڶmɓʢeҤI I[|oK%ѲeˢK]JKK+**.@9-yK%.eܸq wI[yIҢB i C2 \4i C.H[<*pHB琶ց(L: $.`I[B.`I[RWW0D-@9-0 A6I[a8l8p.$mp\I ds!&i 3 BM<Ԕ _򛴅!msյI0b؊+tP8-ti @.3X,onnE@ᐶ9-oܸq%:ÈbeeeUUU:((: M<6ab0QSSs"msH[߶mۊ'MTYY0ƏgB#msH[ ¨Q¹Ŕ)S:餓-Z[B#msH[ ²eˊ 6q$msH[ EYYYp &i CP(&O-sIB琶^ q%msH[ Ht\ !m>㧟~zw3sꊼpX,8-@wy[ouΜ9/w4/_χ~Z*/\?vXi ˄{wsNU+ڙ3g^ -H[.cǎ뮻RB~Q-H[ΎZnᆆu){wi 477_wu>=ڼy#H[n"{gvi mܸTB|wi cM>Q - mAtƻK nnnvi ,YdڵO(?O>-@GPhnvG? m:ʼyIwt\;K.-))i.?srV箳>o,-ms_vm~ mAter^'?_9=S-fh7̃ <+]odӦM7ӿOcƌYre|e˖}3_a:k]`ƍa'}4&mi  Җs׮]3=_򗹕cĹs^tE}þvꩧ~[ߪG9H~~^x!O??A54iwږ-@ק-q7| ;w^q?#D5}r!s͛}K/4:,XPRRzES:NH:g&o~wZp'~՝w/~1ڰ0⛟ږ/}_狋C<#3f;h?O/9Z/kvhJnFiO%NO mr#m9â骪cnSN“_~~lϞ=D7>uօBo9rd$m38cVZ}~._ir'Ο3)I 9s3fڵ[n -ﭷފje{ŋw/_pI$anHa &$Sj'ΙI9[0hР 6 ӃN|m费s'ե^Z__pƒ>8hs\~xeMA-($mٟ- mAF*O~bhz͚5C >hT7nʔ)_+kCBk6m7իWKIf .)) CrGFD[6y׮]I'qe p' $/b|RCIJQmpD93x#a/8!/9 |S_n]ӳgF-GwQiWڒl-+q:IA~w&TjfO mr#mׯ_4;vDa"*C^VVлwPK'<3hР32is=7zfO|I߇W^ثGx`ږPM2W>;tG}4$ݺTo}ǙI~L:u?[W]uըQ2%6m??7mIWxk[g&Tjf}i H[̘1#Eȩ.83fΜ SO ƍ˰Lfl{mݺ5>x^_-i׭[ƌ|V&e˖?6lf|RC9͵-d!IWl۶m|p4}y絸o۷o:tɤRMoL-(ٳgBF>jo muXSSyWUk&~ѣG_{=Cn0={0}gj3ԺK.ݹso1q~;C:!0}g:묤Iww_S̄ Bر#;6񛭤Z4es%m޼N>я~T__z#Lǿ;mb$]Ttȑ7xcCCڵkm/mi -g+3 _~{g^{h>Lf˗>*-- sw]|})++{!myCGyd^*++y-lٲo߾M2K,9cC;VJD+|%ljҔͩ:jӖ6;//+W3&گ_'7^xQG^ ?&w>9szm^iK}a'nݺg&jR5kwm/mi -DnGYepO̹5oknsi @ڒ' m'?W7- mi D( m~~ mAt%K]V _~@ڂ(_ 佻ᄏ-H[4}7yoَ{ mAt[Ο?_ `ҥ555{ mAt}{YrM7 mo˗/ohhPBz뭷xiӦ׿vdi Oz饗s!O3 ¿˖-f mA3 Bi 3 BMg΅ -0 A6I[a8l8.ϟg]8"I[at ͳgްaüy.s!r@׋[ \B4i 3 .D.s!r@Wj\B:i 3 .*j΅ig]#}"p -0wYjD-WX^h΅*gٱjժ3g֦3]KMMMUUUXX,fw΅BgYsC<.Z \B-?~|X_x;p.Dג8Ȧ3g;3<[\oQK%eذaF*//΅&-0l̙E{UVV~_餓eZBt;B+Wl?wnB-X,6bĈDMqqqyy??3glll稥!sϝ} b"&Ll9-dSDQHt{,}\9j꣏> }Q$p5XlEEE,;vh=Ͼ.- .u ***.t9i ņzi%^̒T{4QK\tرc.t9i Y;hll|gϞ}wرq@5?ܹs~m8i۶msOuu\-Ydɒmx7N mws=̆Ȗ-[z `=Z.SN"i @br̙Jkhw#<Ր=[-s7!;Á@K,QQCfri @-[(!zjzjiHoΝ .tteҥ%%%p@ڊ|qi @Җ[߾}:묵k׶-++{ꩧgfby߄LVowd-m&6ohߟ:uAz=iҤ ,())9H+߿G&¯_Z...s>#3fӧπ~ӟX0quQz:׬Y_^UVVv]w%_}Չ'y9s9gͭyw/0ϐ!Cx;I_bc,"*[߾}o'8bĈx }'jll*-- =T{!ig&]z|?*Iblra{c׮]wTHg-NM[k[F:gΜ1cƬ]v֭^xUW]„ of&cǎݸ)ruץzI.\x_ve>ignݺPxukǍ^vZZcYj~.<_} I J3[0q 7h;vx/hڴiFٴi%\^HڙIms$]b1}诏=ؙgLT{V m褴%ҧO/| ?#_yhvׯ_ҘX,MYfȐ!^})Ν;?ON[BAMJW^?Kl/))i|XNNk,lf_|x?߰aC ?QyyyjLBLkC%[ڵkKKKw/z(ýjYi Ӗχ{ ?'ި%={ܱcG4&¯m$t J =ѣ{eIaCZbzm-i?g}v߾}裏T^HڙI_*v_>s|0N} f&H5B-N[Bѻnݺ6ږT/ٷ%k[-[{[nM5K/%>OFisa"lXx?_ݻw/[?СCumKbڢ;T3{YgY_s5e=k%,(~ߖO?=&ҥ;w|7&Nt Vzm~5|DN;-~TO0WՎ;-[VTTM>}ԨQw/$]zx'|2j@Еi#<4lHLn{'>OΙ3Cms;º/RͿdɒc=6sEEŪU]v]}%%%EEEam/$]z}f,XPRRr-[;͛7Gڵ+.q%JKKjN&~_ϛ7E+K80~M}ꫯN81!r9{&K3'x"ވ#x{3%T]^zaU rw' JR͓t3[aސH[r#m9׭[joqȑI53gΘ1c֮]u /𪫮kǍ^vZb&LO@.>}z4cy-VrڴiFٴi%\~SaFG{[OUUرc7u)\wu闞oS5t9UVm߾=sE%6;h;vx/{3%T]z'I3%L4qYD|673li @-ѝ;Banݺ׀%fy;WںC&,%鯩0U#7n,//5kV{mYg=5r駷O5jԓO>z%OoJSaF:>|ƌ+~ۑѣG_{j$*ҥ;w O81Mڒ&L_jǎ˖-+**Jn]. :zy!i''fXTI[-9̙3CMNPy䑽z|G׾ֻwϝ;jW?\RRov_-ZlڴiĈ7tSEˏ+L?Ӯ%U#IWiÇXZZ6$}ڒӖ,YrdžNXjUnpRuiQO I-0li @M[: /0x*SLiBui ;-W^yM֯_?f̘ɓ'oa6nܨi];pte޼y%%%~E]+rGwu_u;\H[2r+!zjlڴiɒ%iHc֬Y{qdoVNC*[lqi @<<[O i @TUUܹS] -,_sێ;ND<ŋ-_:u+\ w} cǎ*pb3fY1@+W曗,YR__plٲ% 3f,]=H[7\p᭷:w9t"Й C}atN@7"m H[F-i ݈< m-t#nD@Еm+i ]6kbVX9R]]]EEEg:ڵk@EuxϟDEc?"&p`DZj6V-Li!̒HDl"Q"GHDFM$+m}331sy޹9s͛C?&Ok׮g tFCCÆ H[@:e˖ݾ} m}QVV֑#GXUWWd mЇNI]IP;wtݻAII֭[Y],[i "%5@P,^k\r%KbKR m@BP.t]v" m@BP+mvQJj )))|- źҖ\Y `ժUܽH[P(K[,dddձ AaaѣG-H[w}^%~n//7:`gKJO4BwKvzjied@hnnNOO mbrҤ'>)ku* OiKϦ- \'mqqqSm1:Fknn}1mϘN.&{NkegV.tk[zebݴ?ӣ>w.Z񺹹ZmiK^ݙ[@ϥ- 8,:%m!m!m1X$pi7mQoVMMMCCC~~~PPPxx7oԵ]q #"m!m m;Rxb}@3V*ic^3 ]ۂBgW9""l?4sӥ9lş>LnF.ך(s'||Fdl0:uソUu+ ЬQhU#šG:Ym뼼O?WY<}7-;(ݕC 0wn8gY!SkSpTNN B*O]@L|-reѢ_~AILM]s_Ξ=dʕ+_䔖_Μ9ѱ_~?ŋUhVV֨Qzcǎ.S%FmqF||a 0{F.۷O>4hԇk׮.\ЫfxtÉ bG} ;dȐ(7zņx(Uۼyx1`dʍT?FOёzxx]]S{X_f̙j/> |6-rbju(φi(ķ;s۔*,_>OmŊgRn*:sQA@F۔*,[6WoKHsgRn*:3u_$/__)پcFvӖ/ _bŤI+8994773gδ`ڢhL"Vb}{ΰӧgMMMJ?ӢWcIfu8mYht1M[:0z#5%m;ӯ_իW$n:fcccN* EjVz,WMSfRW@@ҥKňtNmHHsI[iKҖՈQO>9&oi)--0_Cx{{<Џ smT?уM'+O(~c{{cKQNS'7@sG3r7~>pCaozW2,90VT}Go)Uιc [?r3)7:sO/N|W`5ͪW\Q~h7mٻwopp#<"bYQQ!… ]\\T*Ն ;?//zU*-ܸqC48rH{{c~G-dgg ~~~]v~YiKSSSLL ' LHzIDATMI[Fz?/. g믿&^Rrכgjzw,}L:vSO=٩,$>|9stJq~ H[\Pvb]wh:tHBL[z ?>cLICZJJ ?i BbeFMz沲#FQB)i i B%55;mSZZZ_Hm:m~ƍ-H[(J/O[222Xݠѣ|- -,njժwH[P(^,VWWM[i B+i˾}X ]j_>{i  'aͬ.{={H[ kRS(u|7hjjgI tÇܹO@ӧOs `Y[lH[@=\(--MHH8v-@}%&&fgg744`UWW'|rrrg m䵰J*IBO&==i lJbV zX@/@+BH[`EH[i i  m@OuCE -I^MMP7m)--c6=O \T}6S9-aSN3f6pѦ-Ç_p!E-aYYY555*tĈb"w:'WTڨeʔ) FEFFTɓ'=z;"m@ϫuvvV?www ]- Ǐ i- YYYڴˋli ^ Zhq=<---222((HR=3QQQLǔDDD괴۷WUU%%%M81**􀶶䠠|:;v0aόli [[[[LLLTTԭ[ڭȼli [|||ttt[[[ZZ333:M m@***R---;###'OTWW[q„ `H[}Štdff3&$$D7cƌѣGgdd>wǎQQQ!GDDYhѸq< 3&88877W6qD~`H[}m&=,))?nܸ˗/K.x VUUIg͚KKKe"#m@ս?ȑ#h~UUUXXr->z oll8q"r->>>>C__vZ-կ eVGVPXXrd~qqqdd$r-> ,Q]vyzz*-F4i .N WZt+r`H[}Z[[N8! oo+VI5#""C#m@ ׍Qw}w„ oEDD>`CH[c򂂂"##ssskkk[[[q/ڶm[DDDXXXQQ--I\`Z +..-"m$K"m$K"m$K"m$K"m"8 IENDB`onedrive-2.5.5/docs/puml/conflict_handling_default_resync.puml000066400000000000000000000026621476564400300247150ustar00rootroot00000000000000@startuml start note left: Operational Mode 'onedrive -sync --resync' :Query OneDrive /delta API for online changes; note left: This data is considered the 'source-of-truth'\nLocal data should be a 'replica' of this data :Process received JSON data; if (JSON item is a file) then (yes) if (Does the file exist locally) then (yes) note left: In a --resync scenario there are no DB\nrecords that can be used or referenced\nuntil the JSON item is processed and\nadded to the local database cache if (Can the file be read) then (yes) :Compute UTC timestamp data from local file and JSON data; if (timestamps are equal) then (yes) else (no) :Log that a local file time discrepancy was detected; if (Do file hashes match) then (yes) :Correct the offending timestamp as hashes match; else (no) :Local file is technically different; :Renaming local file to avoid potential local data loss; note left: Local data loss prevention\nRenamed file will be uploaded as new file endif endif else (no) endif else (no) endif :Download file (as per online JSON item) as required; else (no) :Other handling for directories | root objects | deleted items; endif :Performing a database consistency and\nintegrity check on locally stored data; :Scan file system for any new data to upload; note left: The file that was renamed will be uploaded here stop @endumlonedrive-2.5.5/docs/puml/conflict_handling_local-first_default.png000066400000000000000000002464311476564400300254440ustar00rootroot00000000000000PNG  IHDR.Z-*tEXtcopyleftGenerated by https://plantuml.comvpiTXtplantumlxUM7W(J`(6aYN{gz<Mˑ46SŏI3kTr-OmcUE&U\^GD[q% 桓+CŖ}|cdOJNI0!i)H739v\ɇڄK KNZ_UXOe#v-i4|rb "Zl+8b]ԴzM %L黐 #8XmPe,ϒд0C4lTܰD#QiB$<-{7VDn(Gu@7+Me]-:0(u2=XѥJMQQ`hgdu1LLեi54k@J,Jt$#4j$Sg,:k LIE~oӷ.:1:\ zXD{OOϺ)v#Xϝm ]HNjxF[mW$R˫7̣9,GseV<*3IU?QYin(~w`5{|Ku9 ھGJrֆgU*C?L6:W4J4^PlѢƱiP3:\dES{BH%i .|?-6`c.RDVbx sevP;LJk9-<3Yӑ}I_g~a a΅c^j00抩^z5ݘkz?!8د*2UE[fM$ܹ_@@DA!5a,"O߫-Z1汼T}WVVoB ^a,#s$3_els4뗅:)u̷oTTmS9^[Ӧw_}oƜǢ*nѢE&jrrwl0o҅C2͛26'K>ݸTrV*ƜǾ^S[Fƍ4P^ΤZcig*ZnL?EbZ'= oM*WJ r52/Kn]ª r^'8e*kU+fMP;;j?;rI pĉC %?@(ۣ] 50uV蔰Zיȕ1G&ɓgu(m㖔+VŽ17a)qqo%$|Gvfeeal2@6l(L%2㎆3(Y*fLU ڶm&o憮}e6{d|ednW7'dָ̗B[.\,fGdOe1l]f,:AsUWKx5Ɉ_(e*reih4I x \Ri%7أU }PVh0c.Gyqq_} R0/C1.aUWԨ&3ߩZ 4P[>!MzuhgƤ~w~˅׮gn*iB|֭:AsUW=~Xf֬]so6ouJeMBCn3d+\\?G㧜 :Ss]yޑ[,7=)iՅ ~JM6a Ka=cCRG_%zQBgcRJJxҗʭ82Y^~0|[;sʘ Y:W4W^ctYAؗ[*rI"ʖjVg N%d{eL a[Ngm۾zn̸Y;_~SRR*ƾK~ @u c'S[7Mi俿PG}D*=Xwv 6o@_7olذŋM&[ţFjժKURs֭ڵkMѦMm۶Y5If-[֮];77xr_͛9>vLӦM  QӶŋG)eϟ֣lB Hm_d+7o.|rTZZ:a,=)[T;G(hv}}׶ N-Z5;//yjoӧO;r46wd ca a'=5}lnYkݺ pĉC pڴy۹.3|%}̘?fincf׳烹v](84jPei4OiԩV6o¼I{?tK_ĸq# zU[c ǻԠ˜>jGZŲ1mz1X.]111=z8{>;f|~Nܶĉ-֣Ն Lttqyoo>ȑ#6KSLHOO1bݣٙ[W]m-1uTݍ7 #Kc@-c2bV˿rsklUǧK@߬gdn^)l;,f j^PJ]s+j]쓩|AaZ۶m֣۫W̑ntmBsY 6yl684;r ˜!<$$ѣ|nnZ>++Kš$뭒\ñ;mØוyl%L&Bkc:tHmڴ?qe^ r(hv=5~EfK3%%%2/\ G,a 0U6ouJ 7k'yQW<|dS^]=<)Ɔ jnQf*K}܊@ 4PWSVU[c Isg+cVy 낎lVlảԹkd\&OMb7nܸHyiwyR;ِ.޽;22]m`[ƪj=zZmȠf?߻wo===7l`zw(hv|wl=hР+Vy,&@\S&nIYR'ߥ̬- }aBI?^R]cf1F4FE9Ѐ3xbzZNV]~MLV;cʿR/;[4ĕ1nkY˜333wʘ* c\/]4//ҥKgϞ+saZFo]7ZiMn:///$BW ֍ߵꇊ*޹s}ݷcǎC:~46wd ca alРG}k'd޺u+͛|jy3Z0ylnkY˜۳gOڵo߾vǏWoԉ0&Rݻ.HrHJJ*..ꫯYf„ zsZFo-ZC|b2V۫W/-۷o_QQooo4uԈ򞱊ͭk=U4-v*˷lۄޑ%1NO߻nn;wϝI_ty)[W4jtiipwojf6ǟ=i+f imW_W dF޾knE}Wo%W߮]`&nig4WLw_oXF.]:K}*A%GV9x6;ؙO}˜JK ~W˗/LJuyڵv _~V7Ɍ=11^pwwo۶mlllFlۿ~`yl6ˎhS,X[7&&Yfv)OSI6oT_zuǎSRRRII믿n2$&I+w4nU?TTlXf,?z;10Ԫ0jy֡gDMr<9jH)C'iرeӄ10Ƙjq{Wrrrz1zhLMwܹl@cb ٬15ԲeÇ>|5LTVS f'6n8kd@sΓ ncU!zy"\6ݢӛT7^8mظrׯ{>erVԬ#R=Oc@ cU1%U0GDܟ{tӗVXa&7@jm?&2W۪Q5:UΩM}zvEB :**n0F#  c9;[L7B!S^Bae;؁5=-Xx+c2lٲvڹ=Tb/_}ڲ&L0^^^c4lBJ={nڶdF%jJfu;իDIǎ8pdӦM e:i7o慆._\hQҋ/9R?|WFs7m[1006ti^\2uÆ=F?\СCP&n]tܮ6?;c8??G?yL6\+ t }j'SO=.˛5ݓO>V|!A1):wָ}I |zwK/2^vjߙӼ*w#{j٪cfoԯ۽FΔ)c|}M3q4]cmZrgQvf0gҟߊgffP{ҤI]tѼR!ɰGgϞ}gnj,zGOԫW/j' .KSNU7nاOFN2%""BhNNΈ#BJ'|ҢE5k={fԽ{7xx}WfRRR ϝ;'=3|pͮi}QQQё#Gy4 uĉ-W czeq1@taq]K aO9ّ;6O̬(GrCU_կ۽ j^\X0f|6jei$9\RGÛ4hP~},oܸ%"կ\bAXBJ0\4H}iBJ$7N,גyiuͅzh6iݑU3{300pÆ 4;ˣHZtMeq1@tkժ1=-%mиj uG6ã[s[z,6?ģ͛mݶҸV푗ގXY#ȞZn_O/kUIvٶP(LaLkH%L+HzWvΝwߎ;j[ ͋Z/ZWIvvv@@3*ze0WfݥK]tٳ=wB2պu뼼4R)aeСCzW4U Pؐ!l:tXhhoR}>_ %nSzKurl' '[Rۍzpd(_}21ڊZFV`G9{W^{lN핱 5WƬݳQ(ߖ3z-ZieڳgOYRXXk׮}*Ǐ3ֻwV+veFN:UꫯloYH%oL6vӲ_-OQQQczh6ISRRRqq,8pA3~۷H˜f;wz]:a9R@U PX-<S}jy3eGl]oGy}St#{]k͊ygeӖUIzՁ{6jei94ki!!!nnn;w^v ^xݽm۶5Pb͚5&ImdII믿.JX`qz-4DPhhoiiٯ_~M2~BaL&_^tl1cz;J'hv{ץДkv^ݴj^1@t)?y]MdLJvϿ.Z<]&MӦϖi>d}= fl]Ш&D]0o{S,O߻nn;wϝIm˫L2~-#s{.eGڷpU}DQt#{wkӿ'Mzl#{W&uZV٨y]|0V8 kH}رe\nΜ9c@ cLuy{T+գGѣGWhsegg 4@@c"0&CLWh%L-[>~_lР_AX||<@c"193-_\_X4?~|Æ caəa… K,a XhQii)@cbbrfSLa 1c@cbbr~;t rV۷ 1&&&11cƌ݀3g 1&& c7`iٲec@uc n䄇n̜9<ݻrJ1\nnntt4W͛WZů0Μ9?f,: >>>99_a WKKK6m;3uիW_Ӆ V\0M'XM|$ @1 @ a 01@)++3cVc9sss5XZZZrr2]1|:tȰ cyXXXyy9]1P%~i___%)aLX6mƏOa Tɤ1 cf̤9@Uƍ۷%yL~Jl֫W/zcjt?UV-@U+??? @ c2ϣ;0\駟Vn @..a,44Gwa 1R111xtK}Zn-1, ,,$0$f.^\ Sbh߾=I a @ɓ۲[zG1P͞=ҥKNbf:ydLLLN"oRܾt2@<$r7n 2$44 0U\( $0sASsssd@}S0FN:GhJKK7n OcT W^$&#uJDm6N{@8qI)uNJ+0gOiӦ]~0m|MIt%Kp7||rI4k,>10lٲSNK͛7) 1nX2Iu%Hc111dQ^:SW^MHHS@cT0V?<==훑QgØwqG֭|_t]wEFF>|Fw}G#1oSfΜ93vخ]0&?KJJ?>mڴf͚ٳjO2{);E#1{SF&M/6LחţFjժKXii f5olذynnn35Zҫرchڴ$L4Ivgٲeڵ =mz~پu…ƍknczl^Zyő#G.ϟ?0FcԆ0\ҥ\ɓ'z}Sx e)S"""srrF,###ٳ>1c2裏92x`j KII),,lﬓ6tev{Lٚ^:%y⋒d]a Pj13a @mpUa p{ƒI겅 ) 1nI}) 1nG_XR7^~Oa pSN%A/^#@c6;s ?a p\vg*5xta pmݺ599RGFGGsa P-̙3pɓ8 ca jƍ-ڵkqv;1::$FcT;˖-[dIqq1t҇~P^^yNcTGӧO_p_wcjWq)4;;ӛ0JKKn[{йr8&1Qa 0t8` (0:Q 0tg@ (:3 j`l.@c9sss5iiitca 8_~~~222ΟyXXXyy9]@UgU2t$֦M &9 1PURRR$z)yLΒf̤9 1PUnܸѡC4KL&S޽Z111:uΝ;Zna @3ϴmVΡ<10\$%%{Ua Q;@cbbbḍ;@ctҌ3bT'N 7oލ7ca ĦNZPPPg[~=PN:%1ca @yڵ]vmڴ-..0`~N:)&''5nܸ}III10I |ѣǚ5k˗?s23dȐ>@ٶmÕw===?fggK@$T>޽C˯% 7o^?6l7nQFVO.a 1Hb@ c={Ol?QY~(Μ9p‡zhС10@Hbիں_NJL&[ڜf۳gON"##;,zSqqq~~ė^z)''租~ڿ5@Ie Е5n8,,l…V˅۽޻iӦjL*E9wUPV]۱cA*$W}YZZsyxx[NY7n$1\\~gɰxѢEV~-[ʘv6 \r7g~?OSLa Z,ǾWn߾}ƍCCC/_yQK2L8>};,,lΝ#;چ?0((Hsŋׯ={yjYYYwu5o_3";cɪ[\|YyXV<~f=ڴiSnݔqss2{1yJ͝Lȼm۶m7nܨlaU餙ǔGtHMdc$f9ضmŋ92x`S&L Kv TZ 6ڸ|"&M_]ZwJV_}UItZ27nǎ+jV%e4cwfXJΊ+w,YOc*ًMJd"d.\%()7U-ٙz6( ڶ ca q3f=3bĈ>̑ɌD ( ===- ^0^XVfΞ=k,Q֒UIVQ^Oxi%۪$kN2:tHݴ$>|X?O?˜qSšiۘM6v_ad+;;ۻ!1@]$#Yf:~3OFFF\\܃>38~兣'O:-ͯ)7777in O0fY0x9rSmU>O>d~ԨQd>44tȑGvyƻٙ1L~~c9ØJII$$1yԑ /|2ݱcmKw`0~ӧO8q[|.\ؼy<$ cے 3vX)FEEI*7n-,cvaLm۶um޽ŋqgϖXj{(hۙܺuLx+aT͓a cv:Aƍo>))ɶdtt `9Om?{wih.]߿nVvvv5ǰhҤ 10lݺu .<~xY&Ynۻwܹsg͚ua P$}I KMM-tŊnܸA#1o.N<]TTD#1>صk %c10@Uٰaç~J؀#ΝK#1t 8hŊuy10@9spyf"|ܹ THBBBc10%''.P!KMM%O@%ԩx7gr*Abz^z酱~IeLLL=222Ξ=ώ3FYRXXxܹ^zir~GEEEG}pe0^aOdWKP楀^R_=zT]&IoӦM||'ilK2/)x l%)cmlp@@e:GzF[=mɤ TH]{ŭݻHSQa vAI}]۶m#9(88XIb|<1[oUPP@ŜV\&1zRncu%%%'N,--%iҥK?NsЅ  z PGjSIb~~~&Lc0kΝ;K3AX@@̤sF1@CU)**jݺa$əpc=a U+22R΁czj*(0cZ7nTGwpF0'&z[ytgc Q:vx+a 3*@y (:bx;8@`茊Qqa 3nۙCg 0:0a 10j2ջ p\͡sZZZrr2]D#1|:uR:t$&˝@c 1e,Igرta0@UIII1L!!!d,I, @fa rƍP4KJ޽;=C#1Pf̘V[c10V~~eyA#1  Rl 1")))ʝc<10\D}a 1c ytca PoܸqMq&M$C3g5ܲeKBc3f̘7oޑ#GP[_N5/P a j3g&%%1j7g̙310d<7a„<@ RZZi&@HdTȶoNcgҤIϟgD \Kcd߾}}cY{뭷_NccɌbZ ''gɒ%10˗3j@ Nb )))iii10bcc˗Hc"&&+Pk'$$ֆzվQlܩڽ-OJJ2L#͵9i۽z*a jIߜ8Bunm>&ܖ3~~~;v~ݮ0[.a jO-#Ta-oР+W.85Lc˜cѴi9sFY^\\/r??yTb{˓'Ouww6lXAAZ^nkڵӦM۴im6yj{F%+jJff{`OOϿ//^4n#ŋL4t„ fYޒzyp77Ph.ԫA7rH=k#!GV.CQz-ɛze\ջҤ=zddd={g3fZLϟ_dI&Mdsk2\ٳgMݻw74WQLttq{YWO:SLHOO1bveG9rd j8qe34ޑܐ#+Zu(kK􃺰BN^|GN?@0=c26Lm02XW*BBB=kwy0&f(\E-s!^{YWgeemF@@Somڴ?qeaͅz5v-aqGVsCh]ƇRZnn,FsuB%˜#a jm3X{Hwww%5lPY޸qc{\EEE cnnn ojРA[}= =Rs2zqd]Yny;^3,퇽{300pÆ  jjfY 9a9HGY8J?TIv;ȃӏ0u1/]4//ҥKgϞ1~*Upppfff%FtÇ6eȺ^`c{ux֭[ePٮCivޑ5غ#+:x( i8J?mJ1GN?@0&񤤤⯾jǫ۸-Zcb׳gOYRXXk׮}V"Y)Qol4U)ӫW/ 8U1uԈCۯ_}IB&LV hvޑܐ#+V(u^ˍح&i;NDs#1@] cׯnԨlW`^pwwo۶mll0DЬY3IU!!!nnn;w^vm%˜UҞ_~M2c2`U_k#ZM%%%d z;z5X>R6فzGVsCX0Qz-7cvy:ȃӏ03ݺHJ_u.pU@ U4(|Wrrrz1zhW\Yp!a jy9od+!..d2lrᅅ1ݛJcׯg ׯ_'1@ /L:!,P Ź&D@ .]@y3g\LEU ,pY"0sl޼999,PCL8n1@TSg>~8Z&*6iҤ"W(c8͍7,XsN@ rԩ7x$Fckҥ .,..f Ts+W///w}"0O:u7D@` @1Cg 0Cpp&pF1@a La R V0Fc9sss5iiita0wIcY,///c10ĠA< %.:oR>P \ROZ2zr׶6KT(J  k>mFjz[qQ-73pΙ|ޯy:@OÌ!ӦMcp1b L1:KEDDBQQCc1ܥ166V&cRbfd1b {DGG[ 1Fp/bc[wc@O7nRbu1r,221cCԷ;@𨔔: b xTIIL c1%F1p}J1c=b @3͞=egb ttϯmvUVV44401%tQb@O%tQb@O%tQb@O%tQb@O%tUTTPb@OX`` %411@11 @11 1 1R__oc6 kdeejXaaann.Cp=ҷo_ c-ƍcRb!!!ӦMcpo2bbb$Ƥ"""db Kcccllԗ {DGG[ c,u2oq)%[w1<$??_yXdd$om2eJ@@@=dA>T6MHHZdIIIO>>k9C[PPbÆ ~~~={ܶm޹]p駟×.]jU'+wz{k:6{6اK3 ( fgyFf>|xرrݻw3fT#Fؿ0`~ԨQ%%%ʇR\Æ ;yg̘3gΠAN:5qDetС=ԩSׯ>x Vڜ%WX!8i$e;4>3gZo;y.라5E}ڽ4GCp0ϕD v_UUu9 &8>b@СcLCIV?nf6 }+>cbb:,(={\hщ'7\iseee33]v5>7Y^`靼1}ڽ4GCp0ݿ/sIc1@1֯VeA>l^s5^^^;w?G| Vꝡ#& Ƙ͑ۧKsjp \{!C*U/c11̧\$>3}1#JvW:X,ό8p@};yiB;b́yJJY>uh(;q1YXzuYY٥K1b 饗 2ds4ѣ֯Ȑe".]v9RY?jԨO?Z"00`SŢwn w"(cLyz!{iv@zCamfͪ,..3 $8K%m0vXט1b 쬗?ZKf͚U]]w(@n566.[lΝZӧOϜ91:իWgffPjkk׭[hѢ+b b$''/]X;vL=O<7#@fgg/f! 8Krrr CbAb c@b 1c@b 1b 1@1Ę1YYY1VXX1\bũ=޽{1GPzL1)^xb K~~d "1.R_cRbfdizab b1j2oGyD)P޺1!z;oT1޺1[444-HLLKIIa(ŋ cؼy*++-7ndϧb @b @b J u]vS>zjϞ=KKKkjjƌsM7۷Pܸ8oo޽{СC~meyڵ?,Wz7Ͷm'|ɓ?A(1h~޽oǏ[nne^x'?ɔ)SvjcJd妝L&rZ ]K[yms9ذa?mݺ??5ϟo3ٳw}k~btS/w\\̇V۷l]>>>wy͛xW-'Wkoƽg1cz7tСCm֬YեK3++sǎEv$5{O>۷!C>5555EI&:uoݳg?A:J˄/< Hϕ2;XM;wTVfgg[Rk׮58>O<9lذ]Z,w9,,zB ͳuڭc7ߌ˗lXSXM/MslvU\\ٚsbYԣG|ʇڌU9 YS4GFmVKf|ޫb5c7dM7{rrrrdM?]2IGGzo~cǎcEʲ_xᅊiӦ=ZY9~UVȼ_&J??m۶]p!4) ${l>|ALLR ֑kvMMHϝ0aL 4GfWeR[oI_~)`S)9(ig {UXz_,^h7 TxSnٲE@VʲQ?WFow.ֲfSޢ711@3{lr9VAr7ˇʄWYofhJ/q'N~e!4)+b2U74#4SKKK4h*bp96޽wղp%evY˫A5?EsőQ<{߫zbB͜!⋷~{Μ9cOӟ$"yQݑ@qqqcA b >DP*֩U=u&*3Q hN222_Ę>M&;#pyF01uEeeݧh4ec#`+Gbx:XV"!$ޗɑQ2"mlWƘK 6sT9sFn Կi{fʕǘݑ@|644Ecu}[l~ 7d /kdvx |#G\xq/Mb}3fڴiֿfSLiIi#n̘LΛ54GfW4/4n;11S"""|M|'&&:cyhܡkt^;/1 ) ysI5͛7oWAppp~~ܴr,kl>>hskI ZXb1=?ڵۻ;vlРAݺu~_aaa2WV._\Jۻw.tϓ'O[ڽFUcb9c7| :?#Fؿʕ+### ! hxZvGRs1b @h.^ꫯׯ%FcZo1..nǎĈ1cz Cc \#@1%FcpJ1:1b @cCc dF"e,X#@0Q@&1D1Dc1:DSNK,|lС? V^^θ13gNCCû3fLbbb}}3~a 17|# ?/_K.1n @>ƚ.1&c @x(ƌW_%&&k @x(ƾ⋡CG? <@` 1mD}}Dob kdeejN sss"c1\bǫ=Nd}CCCb [?ݻcDYJl6?s 1'=&e)1i3Y(**bp@pP1(Kߟ1cM&lrxxغuc1bH 441cx5&MĀ1 yyybu1Lyc1<*11Q&ʼu1QcccAAAff+&))I&iii aŊB~bc1hz`l߾qFjjjgĐ@iӦM/++c )˖-[fM %#@:(**JJJj?CGt8Vڹs'SI-T^^>sf1b бݻwڵL"ǒ1@&''3}BG"@`$;;#l+c w}7w\f\/&@ɓCjj#@:˗p>1tҥK+V @!--:N:u ڳ[NNd+uj^oKM_%h7ƕ+W}1b @yLݹ. Ęl޾}{(kmgN1Vc^^^uuu  Ѽ^#ƈ1cb̹)]MM͔)Sz! 3K,ik֬YӫW/۷OYcʮw>f̘gj~z3|夤P__'x6!!!88XJ5lݓ8t,Hڽ4Oɩ7K.٩ !b yذaN^3x3f(O~\3|pGАG}1Κ5ksM4i„ xfLsrC-...//ǦNj++3gΠAN:5qfʦ1f$'O) *ޯqׯ__]]}kTPEEŊ+u&,y2sLMHz.Soo) W?iӦ|#i{O󔜺!|4^󒝺b 1\XX,۷/<<\]/s;eY6p0d>,ǧHLfĘcbbDiiiHH4""B}@s61'{)shiӦ=C#IϞ=-Zt 1T],Xw0.Soo) F/^姞zjݺu69;Ѽ4Oɩҩ7K5/٩ !b VeA>T׫$&1w=d___nfLP&7\չsg`WWW9u1f$ef,RYY)X,~~~&44T48ѣGGFFJ9xV8.Soo ƍ{ꢣQ9z/<W^]VVvҥruͧ7c29ުγ f{2Aɩ9zرc6ތ=gdd˖UUUv9r%]ܹs $fsnF9r .޽e955U-Zd|&FOe .fĘހ$$$W\Y/8cz7|5p@֭[]s$Ƙ)9uC:5fѻl.٩ !b yg6M<5`KbO>oXXXzzz.]Mw7۝nܸ1::Z,a֟ތ=_|Yvӯ_ 6}ŋ/dYeˌϹ1I9rD߻w,ٳG?33ӧ3>>>??17 o)`]<5>o-.'#i<%nH,zw%;u!@\c# IVYӦM[諯"@{N:uC>LF;w.**ɓ\r 544c Ж‰WFFd&L=6SN^^^s\#2331chX|yEEoc PRR͔;̟?1d\c K^^Ν;8p ȟ1$$$0w*6mڼys3~c KUUURRG.'41b 9rDzY?1c*!!׏hyɟ-ADt\[ly嗳8L~bύwy7'@`ԩS222Vd U?|1cL=c2b 0Qb 0Q@&1&1D1z@*--՜(2D b bW{L(K@9rd޽S&RbaaaSLap@p81(KBQQb 466FEEJDYJd2u] 15{l 0,={J[a1c,Xd1c|A5&Ò1t,/\8o\.???,,L&'ob c,wӟorxD1c xxea2Q;@1}*~!1c <:ԩKqqyp$cRb3gάd t7o&b 1~db̵=А&+rY]lc6 c@1M]c8v(u1XFFtL))).{VVViif21c1fbL0e -׮]˔bW{L1)1Yb웫''o UCe̙jk%Gy䁚 |%=lu͝tb|,+һv޽s٭\rDD-> Qg1=88s?+MI}l÷W{鈺+Yӣ->|kGu3`@cj+f?䛨%߃sO9wJ)1&%6e&? ƀ6cR\#Fc9G;`)S0l]w}Yʔ_++{}3_~h%w冿61+닋ܞ={n766FEEJIIL1Ƙwׯ9,˂|t8ppnV~np|6Do3+һC<m`:6ܛf<ֿO奬χnxg,k^t׻Ryx>.\4Xh?{l 0,1ֳgO u11g$T>3&ƛ/nKGANb=S))koRݭz[g78'M?yc~0`@GoQ63cX+2є{ɑy_mwĮ9{Kc….Ob , ƀ6cg?iII+秽8l]~͘VsчclΜ>8D}啺Nn_M 4H*ƍ!97'z٭z<0uY֛-˜#snƎU\\˓onQ6aJ1>͛lǎ19R|ȑ#m <0#7۷o^;hkvg˶X3N4p准c'Ofb h31fCU7C_Ƿ5@ɕ^PĆqIG=!u,^2׷4C*r_u}|9+bxx;ZoJzg?9oWYL_ް^Dٌ+%\5YKfd9fƘ_$$$;hMZrVdT+vX0)11mT(˗/_NJJ }'*++׬YӫW/ٲS>|xر~~~ݻw3fٳge?AAA6lHNNmzm۶2i橧2K,1SLFIDATcCCefpp9=UgKojٳ'<<\BYaٻw]p駟]ɧ/]Toepd<|6".8u`4wUlٲ%>>^066V5Wz8Ɣ;@1ƃ6_mTM>?aZZСC{챩Sۏ=رc2՞5kրqqqUUUΝ4i҄ e"[QQbŊnݺ_)wygܳ_rÍg26lkU8{b. ;̩Y~}uuǏo1 ob ƈ1<6m^1fy_3&dF%3fe}LL̡CҐu{*2TI&e2%oܵkצag,sMexF.6}Õ刈6M6l A靪3\x#<|ᇕg{TXow6Wwbwͻgy͘_}:;iSWѳgE8q4Wz>,;@q>|IҒ֭[UTtի?3VWWg 80;;[Y/3ܹi<޽{!C|}} a4{Fo/kUWWecFX>l^9;xzezj e{M7FC鍌5=͝7iɽ/9͜ѣGGFF{+=cBb 1S{mvU1'7sձOxn37#e k>󀀀rtt,#z겲K.kNc.yfLfMqy%k8>w67ό\ͧ;u/yK7tz;.͜ %|]IclР徿ʑdrIc֛jaqeff*SaÆj׮]#G4X\0''ѣcǎuaKk>㹬lfȐ!ӧOWϝ;wРArn/rQQQfb$=`zרn3|pKz;"gOw^^ovbs]z9uFO| Vc1؏15*;jԽ;q<όusͰ,s۴iwܡ3ޢEbbb|||aƍ%t,O>.0 x=7.^/L&5.[sv-nfjM BQVQoooӧ))8f-ֿs+rt%O~No'6ץSWݧO|@Řp9sدŘgk1֯|5Бk[+PVuZp!1c 4_xqJ fsͿ+ |kGu3`@c*Kѣ>{7=5ۿ2Os\\tZ_]On|(y'dji2Y}ȞexEY늊 sڼ[n}# if}|sd`mʂBo0=~n/zQ94ZF<㯑 oŘL<;uLJ[DEc1bvpRwk޹d}}KGx^nȾ\r1s{-{{4Y}HoHiȂBNOT1 ̙*崕;w_ l+(^ۿּͫ4Ok›s]M&m6a„*&1Zc*@XY߫WɒreUaXXOu/++ ?>f?n}zw8D}،۳؜F‚nc,d9cy;g9mncT7u`dr_ ~;k›}1]\\4:yfb @ wߟ>Udrli^^^pݬ-|w8o_sBYfĘޞWWdoحk',kdS1f}r84n`38z_ l-?|-FG?ض<㯑 oXmm/Ȗ.]1SeަXll,#s7\|p?O^:.zNvyv1gǰawʯhʟoZ}}k{<I*G.Oό}-%w,!/$77|\{3)Drrx:T1?_zw3b=2N(rUj3bLo֏#mkrUY(kCTLƆ؇ zz6t:on,R̖ ޱr_u}|9ǘ|\{3/իwɔϟc ƀ6cZc"33>cb t(3gtň1c1ƃ#hll\pann.S8ٳg=zb @ ƈ1;y{ۺukRR#@Ę^?199yƍY֭o={x1s‘#G-[Lv/##c…b1c1ƃ10Q@({ ƀc_Y<~7?IDc1(c ({ epep` 1L1L=c7(-@pR͉raaann.Cb Y,xԉohh`@p#G)=Lfɓc1%///((o߾c2Qݻ,18 b KcccdddTTLBBB~12 b ׌3d+dX@p/Y;@#F16qD@O [w1!xD1c;@ 233/^ \gΜ92QNKKc(\H+V+-߼1ڪzݦرҥKp72.wEc徕G1ژM61Gue˖eeeugɈ1c1ׯIGMJJ1b dժU;wdv<11c1b 6c߾}k׮ecIII1c ~mrr2wW"b (''Y;ګ̎1ڀ~ܹю?~@кfk֬իOg|h^$gaÆd٦gϞ۶mS6;l< W2vOU#靼5mٲ%>>^166vڵW9Ԛ8t,\^YqE@жclw}dFuu)sUgw ([Rg/!CL>]Y/sG;%U Rrj׮]#G4.Cۍ19uz"C:h 9%׌9r[;ܨQ>SK;… w>|YNMME㊈1mŋ"""ϤZwS PP1 ~=76nݥK`[SO=%{3˖-s$._,{ׯ߆  AvcL@Nހ &IвOt;ykzӧ|b|||~~1w#G6ӭ{>WDm;ٿ ƀʕ+@кc2331Y|yEE3uoqAA1c"%%%LѾ͟?1Zd&hΝ;ס^0F۹s'SvW #15>}￿yMͨJJJbvO>1ڒ#GHڍu֭Yc~;c hcxںy߿~/c h^~l1-eeerow7'@Юo[bł j2Qf\k5囗1Dc1({ e{ ep` p` 1L1c7(-@pR͉raaann.Cb Y,xԉohh`@pѣG)=Lfs=1.yyyAAA}X޽e1c]###d,%ҿF@JLL4L2Q[nb {Y,  b x<ؤIc1QG \c%R(:t(Ϗ \c5Œ%  cΖc-1J 11J 1 J 1 &1 11 1C1 11 _ bopR+,,egXScLJL7440D@=zt\\cJI{b K^^^PPP߾}$Ƥz- EEE c]###ǤBBB1+11d2JK[ab b Ahh(ox@I&1 @O U;oD1޺1ڤƂŋҦ̙3Gb,--m+dectD)));vtR}qƶx.\1OMM񯮮V@Ёlڴieee~Ο?lٲ5k,b am9vѣIIISi1ڹUVܹjU1־}֮]KKJJcC~mrr2Ӛ_1++1@{SXXHf"b v￟;w._qܮ1ڏ'OfggS;_jj*@~,_UM裏 cctNpҥ+Vp h'\ :ujUqp999&y,6\ctkvsmŘ`6o޼Cknf_W_}E 3c^^^uuu.f̘g*eO?_tfʤ e={n۶z;W:Y˗/BCC}}}xJOUovL ذaN^# 6$$$ y1wPXFFl,UUUk׮#GT@@/JEDDx{{˟III7qD} ސ~e {Ё222ctXǎ<1vUV18jkk͟?_~2,,͛k6mActF?dk ?1pG0F'a 0c@ a c01a cƍV˜o1:,cUUUyL cd~CC]1&ƌ1%I{7 H0&I,88X& 0x`c$ 4hĈ @ۊtAAA%]nh[UUU<LJGwa g}V cSNC0C^^rYGwa 1xt@0ƣ;0n޼gϞEp0qqqƒ Gx+VqcDyy… ,X~p';;NpLW\KLLTvM'a H혚*Ed}}=U5p+Ξ=_PP `Cmmmtts(ֲf͚O? @Z={vMM 3кrrrrss91,hhh櫉@YhQyy90dȩ oN>zjeM}'p'.. !!@$R%@>k vkÆ %%%T@;(,,ܱc0JIIDڍ^_ɎSffft.]4yygxDzlIII778p`޽#""ڨ ۽{IQn}[c-v10#ʪsssۨ vrr~;xDi `o/]d>`0DGG{{{8P}$ѣGǎ֧OrH]jСC]\\Fq!*Veصkbcc}||\]]_~嚚&m߾=44Tvzꊊ~mr劼<b{;Y@{zzQoڴ)>>^k׮]֏~րdB^-_\ud w֑[`H#Gę3gagϞ/" /_~פK,Q;bYJOZ EIƪ믿.7Y@kZ:|@O<1mڴǏ̏ +...--2e23$$$??… SN}Wbw̘1'OZΜ9#G^@I&͘1| 6l/'L s sU~m۶{bd1)WX!1UL?C֏Hkdd&O>YeFLGjo#w<--M&$`8;;/[LOΞ=Cx{JI;ØLĘ,qvj>a  al:O>'NTc}g~˘YK-v%)RѺ47*j& 4| ^^^zW\i9kZ\̼|ڝ,VQQ\B1ٳ#+**ReN:e]ٙԤ1GuV%>3o/K?~|NNf@b1i=hI;ØÇ7nN{0haS;wnƌaaaKgg纺:e|[n֋cØnMv*?ͷPPP0fww_Wk֬~!C$h-f$Yl=Gd]2!/e+kng=KjjjdUUUnnn2|&ǥnk-fZ=igzo\ZOcf/^iI2WW\YQQqJ" I8y=45p@޽{~ݻwKV3eKkw-cViHs;SHu/ϟSO)qgyb# ZYƛbLVd$57wZوWƴکu10@+ܪ?OO<2ܹaaa'N0gLMfff}};vl+1ijddRd۷oԨQ[=zg}VWW')CcISwaeUݵ8;Eҥ3gδZЙ&GjO:Ҕ>},X@eZכo6::駟VR7x~Z̘*Z=ilȑs̩)))#2:3ܳqvj>a  a,///""W^RKW_{u:䙥K*3 ңGooo[1]vM62|M6o!##cذa@hhh~~:ƍNقŒksw-cM6LmR3MԞ0uǎe<(Ӆ2m~ɓ'0P7x~Za*ƫhC1By`UL+ 2e3-o\ZOcBJ~79R۟}cpϜ9Ñⶇ1^i )˜NNNߤH c10C())F#Grss91`Z >,Yi O3DN8a SUUF M6sc+Wݻrh# ,T --p ٳ[ `YcccrrrNN3Њ%scXVcǎX1wߥgggPOuܹ5k̝;S @َ=t<<<ǔzꪪ*!a t@ a ct a 0@ a ܸqJ3-0ZGzzzYY0VTTCa P5aLohh0ĨQ<1IbӧOs0J~~gHH1 cĂdh+<&LN{G J0%lڵt @۪/2ͣ;0s=iӦ!@!??W@Dy11Ю}] c<c͛{YtEpHƒ $ŋXS @ .X`߾}Xvv6._(͛aH*%c}}=54кΞ=_PP ojkkϝ;G 5k|駜pfϞ]SSC \N;a WvhѢrN>a %''D= DN>a Щ>}z@;O 91@"!!S@tR/^LM  3ڰaCII 51p[ر@tF)))m91@gL5>tr LNnh.GJJ '"0 i 뮻ۭ͛N5ޯLJQ.w9~?JkqタFRjq~~~wntvcD1Z/z˖-2m%̴4&=xW\9~x|||߾}8F͞3gcٳ|oc=feNNNׯ_wK11@gcǏOMMU_.^XX,v՗׮]quu}kjj/_vگ_?I|I /YjСC]\\Fq!5yFm$奄@5Ȅ@xw!/_~dkK,se=ztر>}DEEfW_㒨bL{wkO>} yiYYe>3 uhmPKcoٲZKjȑs̩)))5jZf###%HT۷oJ”'^L>Qh_P\\,IM6n}SyyyRj\]ż߸qNGYy~oIe<~xGkm>Moim\o7[t#o߾l ޲yܹx6lح1 ӦMD&oZ cv҂0f˒o%yoPKcyW]]]}}}%ʊ!/3CCC ca 1՘1c[,Io n$ɫ.NDa I!جĉ*e |J|87xԩSwzNDa @kW]tqrr2~Dఒu:ݽ+?0hJm8rHnn.'"0 צeɒ% W-) :4bmڴS@tj+WT3vPYY`N>a vqJd ٳgs@566P(mŋ91O~~~RRȎ;bcc&͛555T;wڵkΝ[XXI `ñcǖ.]W^]UUY C1@ a.1 c@1 7nX c&@HOO/++Ɗrrr" WUU15I t@5jTppǔ0&Iwt@[ <&aLXPPL9@mq>>>$It>(=1ж}] `~~~Ƽ$]nh[UUU>c\FFFQQ͵tR0@ s%W.^xb>Da ӧ322hDc%/__Cٳ@ͶpBZի+Vs@%%%(B.]:C3[ncMXqww5jTII Q[Pg78p`޽#""e.)>#menH ca US&|{^~,++ W2˖-2}Auݧ~/ a a Ƅ$[|rN׵kWyY__?}0@&䥲`_*3]/(ozj+3MZh7%MEׯ_WȄD#U._kZK,Ƥ/]dն)9^P=tP@@JjѣGǎ+%cc0\9r:gϞU^JΉ<'5k2?...,,tʔ)LiDDDIIIeeIf̘ذaC]]ݗ_~9a+3MZh7%Q?9yyy=UfϞmO<1mڴǏ, K5^W^QbqZ ϯpԩSe;7a a nnn/%̨O:.WTTL:t_69L 4HiDkI [fD(3Ə7nrJk9ddɓ't>}L8QM={T/**UXUUiFuu# p˜|+|B2C]]2-j0rqqɩk׮S_PP0fww[Zi%ZoVKӻwMdB@l_&1չsf̘f1i5Zؿxxa a  c/I25dȐ'OjQUVVmδ7%6'L0qD!Ç cŋx75稗ukkkXN:ueb3dʕ++**^ZYYWn3^b114;J3gTϝ;7,,ĉwj%''’$W۷oԨQѣGguuu<<<̴7%6$} P >|ӦMaÆ|+3Dkjׯ4Q ɓ'W^jƋ$|WbCC~n޼aJc~aoV3%Uf|}a a ch/_lJ7|Cc0渺wtjhh 1Z&--@ͶbŊjZf޽|hgfdd*2 ,hlls@K*00@ݻhz' 0Z.::tm۶-77666;8p 33Ǹ>򗿬Z n֬Y?-EEE  oAQQQ!a7oAc?und 2P #aN a 0Fa c1 ͍71cuY cEEE999t@뫪 U$cM3&88XcJ$;c :c{zzH0&I,((H& 8x`c$t#G3@m+..O˜L[nh[UUU>>>"<c=3o߮. 3%c {ڵ?++Y_S0a‡~X__/AKB@tt4x={K.!ClVk׮˗/嗲YÔvܹ300%;Zl*y a `:f|_oܸ1...((OVsww7^w˖-R$d TWO XܬD|pʔ)7͕u]?YluOf@L C q>}_SfA%ʜ>0&ٳ"DIIIrr>_|INyfv/^)Zl ,hhh`rc@50{…*YWx{{_tiΝ2-sL&G޽{KjRD"ӧ,ǎrݻOswwfK>}z ˜Ŧ1n)չ(yyyzH_b|A@@@=~h1DEEY:111ǔ'Ntuuo%-_988A#*ip~0gΜ1M%/c<]reٲeÇ>:80y{!!!{!J15$N @#/@@_T  J5`[c$1pa :,VEEE999tc$1pa BCCՂ@<(a͛O800J5p__h:80h+:nRH5 AdN JCCTRH @HHs=G_mkԨQR=ڣ/~-1@˽1@;W|{/cvG._J{/c฾ۅ ;3fH5+--Avv={,]Tt/ZݢEVXQTTDЩؼyjjjnt8*++ch7ϟ_pabb޽{  ~._+gIe7o$ <jXjjTH|g@CgW[[}9>n͚5k>S<֥KToo;w*3+**"""z))$6{l_NNζmcHb󘄱!C(3bbbdYf7,a3sLvYhQyy9a I wj0w?'''ekdիnnn|`(62 1$6$ikefft:u[)9"ؿ5yLV7VØ$jԂnO>?'DIKgg琐+-.~~~wnqniX,1a,***66ok(..p6/tkbJż<e˖9`srr~z[[j11a믿{x,xbJ@ 550S$1?''gذaS¤_}2?|^BBBݫvvv 2S#ˤt:σZܬ-2eG}v͚5xGdhΜ9#gϞ&k׮۶m3c;w v2^_eÆ %%%Aaa;c:E3 cUUU@iI/oVuuo9fe >C[n`tss۵k˗KY敱hi`e66W7_ʕ+yyy|-+c{L&?֚%YK ]JDDeΊ+&Nجc@Ppz0#Y߾3c}JtwwW/uM9cƌ|pʔ)mlBdիNNNV6kΟ?/YhСeee&xD6Øx\ wuqcLJr4L׃>l}<}:6=WHo*?F_EnN->gюӺ#f#8RRRcxϟo|)B~ٳ)))zGmg2eqv1[\6Qh#?yrm͒H/^h~,DXXغule߭ X=\Ʋ.)ZܝZeGrز]6[Vl@놱V<{V=DYrtۍϜ9S[o~MΟ?i{cǮ\"][#bYØO>_\S7h񈌏Bk=&Q74Ν;eӧ[) wOc=rVٲeL mcgΜy'{.O9sXzIb>,I Ə\x /pҳmdbժUCuqq1bġC׮]O/*'NueqSG;v[>}˭/^{M%K|P__?ydقq?ӧO8pdB^::[eSCCC,>Ui|+S~h&ͻlq?hV>,V0ovk`) |OOOM6ve/tcp4$F$noKպ26f̘'OJ6gΜ#G*󓒒"""JJJ*++'M4c uѣG[֦BBBe_paԩgϞyɓO>i=Y̙3y晳Mz)_Rq֬Y6֞F՟qqqaaaťsb#;/6lzz0f#Xi@:_&bbbt0fq ~~~RA*Ӳ)aL7ޣ4Hu3O||Sҟ-7ʇ/_{˜]WTT('i9uK11Hb1G2fnMv>LUкJkSwuu5yDBLXcZ[PZkGu4x]4ގVO<]AA1cnڬ0f#Xi@MJt+;lg;iI]a y $1,7n=cǏWMn3C 9yd)+WVTT\zf H X+^:Xi?5cb6N@t~a1 c:E7o{TuD$1a6JNNt{+R[[Ktr=,Y~ `C:###8rHnn.a \N1 c6Zdo0t!>_n X?_0t!]UUl2@צM c*} :T^޼y˫>**w<@QQۜggLV\w^jAKeeb1*׈7*ӫW~eⷿu$ڵ+44T3gt#:[iiiǏ" ٳyn0a u?O?:uJfׯ˿tMY~6}={Їa1%%%''@{x$r{T1*-[ر^P渻y|Eg --饗Ft0عs… y=!@4 cp'UxÏ=yI}}}UUĩSFt0n裏㳳kjj(sέ]V3nNP1*WIbFR_ _|?++Km69;`tlSJJJʚ5kZp^"T~NJc1h?Bnӧg͚UWW`0vYv޽{u555g.Xa l!Ez>Yc1`0,\ֵzr>_c1ДP]]Mx@?#11.Xa ,khh$E(p*c`֭[ h {SF 0VA RRR h;<ƃ`01 trLЬ&J۴d) Æ svv׭[a"S;1+۽{b1PU{̬>t c ''ׯJ?SF 0VAq-]|~}}8`jq)ڴiS||׮]RSS_˗/[L.F׮]555u:]׮]beAհ$=ztرrD}􉊊*//W7jժC1BRWWmF`q#1 \ ~Ӻ2\WWL˄1L1I ݚ899uU~ZL)ƷcinUUU{>D&ܿxx CYY?y_j5`̘1[nXa`0ƍlٲf]7 eZ(V.--UWR 2ɓz(*bZ L0!111!!aĉiʕ++**^ZYYoka/5p@(p*cap7nx񢄮^xA;嘆~Ϝ90O++>Sf#G3gNMMMIIɨQ﯆7In%mUŴ˓L2x`1'N;v0fxX S}ѣGguuuXa`0Ƅ oN6m@PyhOKMMuss2ezC$#F([j,kA^(K>|ӦM6S*6yu&%[VLoka/!l0444??F`q#1 \ I`01@ *@0V&z>ec1`ߘZǷnʧ`0\tiŊe˖ >ec1,..؀`0>l駟 5 555D>Yc1`0O"ZŪUdQUDn]AAڵkLQU^eee111|_bׯD 0VA1 [l!Tjkkz}NN# \ PQQQ||_VVFƀ|ͷ~{ر%K$%%W(p*c .]vŋ/&999 |d(p*c! *@! 0VAP i0VP i j i j`HUT`HUƍVc1@HOO/++X E Z_UUUppj` t(p*c6)yLbcc}}}PUmeÆ R(dBNb:c1@[ill&ỵsMPUm_:K c&MZ~= \ m}Wų>ȣ;@ 0VA{L c:.Xa N6l)#<£;@ 0VA1; PUGy.Xa O?T[[y{oaG-;ޡ[׿͛d \ I1,//oܹK.=~+;;݁RRRΟ?Ϩ ;rİ={@`0z}cc#Û`0jժU|9}Yc1p8k׮ݻw/={6(p*cXrrrH,^gSUQ U:>1O 0VAP]]MP$HLLdSUkhhcѢE1 \ ~[n-**"tسg#`0YJJ 1c1X.]n=Ft-ދۺmݥ3)p*cp%''Sggg6_n]{.Mw9~?W{ /W۽{wgT(p*ca{̬>t$vcʕ+Ǐ۷L֕=C;99]~ͳMgSUcƍ[tŢժUCuqq1b4ekbcc}||\]]_~嚚LMM5I<==c-͛7zh_]tU5>}}2!/Ŗ/_vjeu+cZGj7sl˗_{5777%K(p*@A˜$3gXcƌ9y$9s9RQRRRYY9iҤ3f(ŠKKKLbllҿ7ڼ#W/^cY_]Ndd&O>Ye$={aLHM`fu ={b1 \ t0ֳgOI̐Lֺ(ӁGQ Lw}3,^s߾}|NvԽ{w+WsOAAKKeN:esaLHM`fu Ƌ(p*@A˜+c_J*ɩk׮S\WWgow@WВ¬nP(hWf=i7ެα&(p*@Aظq-[f2dɓ'͗xeL}ݗ`O2UMMziW쉂aLHYcge$Ç(p*@AXaa6nxE/0&یxP[[o߾QF)Νv {Ξ=+)EV1 Ųg}f~eLvj= ;U3gμ0uVX:GGG?blx}駌y \ 8Jh 1 111ҁ3)p*cXJKKcUAAڵkc1pDeee111|_پ}c1p\!11q˖-V0)p*cp(**7o|PVV7i,544;vlɒ%IIIU1 \ a.]vMuDR tZ/999c1  4a 4Xa @50Xc@50*c! *@w +Հo \ uYrrr"PU*88Xcj5~Іc1@xǃ>v;wNb*y `1Iѐ@ *@.]JsNefEEEDDD^"##+++rc I󘄱!C(3bbbdYf7QG 0VI󘄱N&899)3]]]]&W^usscQU0$Fk<&a|Z c0F 0V$tcXTTTll7|)p*@@?sHHHZZ|C:T۽{wK+Y c_uDD=<(p0VXqż<e˖Ēu{Ɯ_na݃U0LGNNΰa{X)\䎗Pھ馀x)۾},?Q""x#MX+#IMu0/$"(jϷf`nc>Ù3g|>9c|AhhۮXǧ}ٳg_~;wT9ydLḼުn ߯ꦎ;vzzz ~%wiUǗE WWөS'(ߚ0a4y 6H)W5^So%6uԲ)SwnJ֙>}#kZ $+6h5H^x_ c`9s\p_̳1AYDS:(Wv淒k[V7չsgKڠWx_{ ` b >$9x Ib)r77:?aq)utbLN1 Jk)_b@2r֬Y1V}QbLOcS~[o9_g>B <ܱcGuAuI|I5<<>>^^^1V}QbLOc(=:QbLOc@Qb b c21c@Qb *1(1c1c@Qb *1(1c1b 1Pb h1pV,n6d6L$--1F;@x1`6$Cvf10VA3صkW>}~'7nTVVq/,,Tڷoߜ#1 b P@ttG},^駟zj͚5f۶mPgϞ~ɓX1l`wuO?#<AAAׯW quu ߸q#X1hl~xg92 b Ik֬ᰀ1c֙ X| b 1JJJ 8D b  CSg;w0L"c1b (د_?ǔ@^^BB@4Kz{{}c2񑅢"11h,555~~~c2{y 11h\Ǐy=㦑#GfggsX@1@:vXׯW^@8CDDc=1c NtR%"##9u11p42 b xpc1b d2mܸqMi5uT $''s(t@JOO߲e 8!b Xw ,]/mrss9ؿ -`1c L䜜mƍ2Ν;10VAhdZPZZJ'9F%K6mO10VAhL&L)8-?Ab `F͞=0@xˏ!10VAhs۷yfMh7n -s!дN>rJ~1 b @7߬^@KII !YYYgΜ $*1mEjj*ڵk"10VAhCRRR4&iɒ%Hccbi׮_kF݈6'ƈ1 b 1ZfM۷[$A1=en[Ǎ?[n:x`y10VA ewygNNNYYفdfcť!mЬ_ɿW\9zhbbb.]cqsyBΝ{=cLO11Ǝ|WVVN4˫{ _S9sx{{˷֭['%m6uKzzzw|rÇG-+w966ܹs+Vi߾rپ{\\\yy7V_s ʙ}qssBw>{:$ 'OGw)Y>x`HHǖ9.^T#b bLDBbbbN4x3fS_|lʕ:u8q#+7Yf)ׇ_to[M0A]_BB QEGG;vO>ɓSvٸQFK͞={;)?/?~FT/RFF,,[L7T_~e_Slu[.\H8p`S=|b1c1=z SR3eَz/B97uǏիn())ٳ1fN>,K{勞ozz'Ȇ t6lؔ)SF!ƍӏ:&-Rnm޽M1b @ ΘkEE, oY !zݻwGEE+СCu}ɜM...۷1{`NJҌ53$$DzIHӖ \#\V?UnݺY|KEA5'ƈ113cʯXQOPVZUZZzkHXXXqq{`N*$ׯ_e{,אKLL-@[XkOccbLlݻw裏.\ c)O6MWTTmzHDa3TVV~WG֊4_ɹK.ڵG7݂Z;9r}UTTHYhQΝ.\(ɲn~nO@o{Ƣ"*{Cф#*1Ęa+g,<<>P^~,yhK-]4 @ ^P?ֱcG??? v|W^nnn֭7݂Z;vZ9DAi|kȑ#r}AAҽ|gҥ]vL5B7Nپgdo  wڄ#*1Ę8o1b `@cp ~$1 b @[tRݻwǎHcc\jj7$*1myQhZiii<cceժUǎЄ~sHUcږ?:{I$*1mg}GgΜɧň1 b @onpGfϞ]QQ 10VAhjjj/_sN q̙3fPbX1wy'##T@1^zzd⇎  y-]S,dP-[,11ɓc1b 0k׮MOO_hQ"E7pA%C/b @ ɐ/1b `01 cl *1c/ b vf1 b 8uf! b e*)):(,,eMO b ZN^ـL[z!B˚ʐ߿!-3A4#111j)Zt̘1CZJgϞSLᐂ1hF}}}###e*d8=ի2Ă b +=&)ÇsdB555{JɿJ < b 2dzaA˝.X 00 b [V__Ps==5 1& ic1THH2m}~'7nTVVq/,,Tڷoߜ#1 b P@ttG},^駟zj͚5f۶mPgϞ~ɓX1l`wuO?#<AAAׯW quu ߸q#X1hl~xg92 b Ik֬ᰀ1c֙ X| b 1JJJ 8D b  ]wݥ:X~}DDd1cFW1e61c %33S&c %- EEEc1b Xjjjz/=&)Gy#b @o[iܸqc1b иΜ9j0`@^8u11pO8Nb @D9* b IxlSw1cRN;@1˟|IZZd6`hѢ۷߸q1cj˖-˗/?zh5r`aϞ=Ν#b 8rd؎;\hx⚚b @̢?0'O9sfEE1c ߹s'H W^^.=fcX1uyyy 8ŋc1b јc^yccЦ%''1qXFq…@`dJOOg1,^Xcr)قw1ڰaCaa!SFa޽;vp2++,// .1ZE1_x;4 j[(((ׯdb b b&ڵk[v^YZNNŷh4j66s<=0cbܸqJ)[:u*\c1iiiM[&n\g}b,00p-6Vo+pZh'!!!c)Y(**b b bck...UUU bX[1yq䲦wr[1W)31c@돱I&yyyu]K#00pҥoΌzMfff>}(>|xѲΝ;ƞ;wk׮͙3=..\!k=Rј'ߒGV.:w288СC7nO:%Ξ|gr@d"zj\[׺ue#۶m? /_~e堠e˖Y<"ǫ0HJKKv~ʕ+ٳgꑬsY%ϬK]ę1ϛC~ zLHvv6\c1yXLLɛdc`01&8u\c1yzBNYl1)2tss}zĘ-KyOISgSZ488X}:Y? 6(sh)S1BǍ''N9M˷zm15'xfuWUv HY~v=X~~~>}d ;u\c1k;c2ݴ%dV*--z-n^-DƇHe [}Gٲ?pRRСCeyȐ!}zz[n<[g'Ĝʯj7ǖ!˚.]ڵkף>y $f}Gٲ-ܹ… e999Ydȑ)G1u@Wl15'Hn.;Yޭ:Ǟ]k@hli3 z .1{ՏgXK2cʔ)<jk&[1cE+O>~xttʄ-Է~zIr-Z1cy/ϝw9a„ZvڹyȶHOO'b +V^!pGnذ1cxʕ+2h oh$b Ν˔@cX`AO)& b h2z6mh;vl۶1c$%%3w̷ň1 b ?F̙3_bժUEEE@PӧO/Xp{yw[JJ11c@Yfb-(1Y0 ,|ŋ-kY)@_|[o;tҥKSRR10VAŋ{ŋfd6AJ^",Yg2ZhJ11cl 'b `01 b l 'b p@x1`6b! ~^b /1c iclm^uup.10VA 1JJJ¼ېѵk2%ڬ˗;XE댱7NqW7Y/9=wd[o')W?OO.]7nxC:[éW^CBuigF>"G?͵oе9uج<]o偨g1ϯG˿T yj`GߛwazDݔ\ӽ{W6?^r]: ysGGju;\rYdI>y1ڲ .4eh1&/!C̛r}©11uIQ?;sUM/>-[VlY]ƍhfi= o'NnX+-6\d!qnjyBN֗~_=[Z+.nԩ/ȕ/i Rߘ>ehlsa#y6?iʃghHåIbĉk׮e2 qɎ10V:c,8,+<$_Y w+?:[Mo:unjV֣ 6+-6n[)ݫ֧OS;K ciŠMlSrWY6~i=Rpi[bU ;K1quoO(˲ _־^rС=<(AסC۷cFVšß;kWӹbV&3j&3gwfLӍ8)?;w#DRȝwz G(ƌyXroOHwy{w׹b>sY-Ϙ+w*lƒG~XVxx؛+uQGGJ9cS#|G9 3۷[`6Ihis8mKzs1c 5Ak=M. ?eWzHdܱ->1&O{z-hmd\jrh=O߹nnw+kM1(켄 WO%c}t_ߴ9SVS]\\Ok\Y+,Fk_nZ2?--6%Ɣo&!!x稪!۬ތj1j#ƈ1c\_\fX_)׮]3g{\\\yy~fff>}d=zGΝcccϝ;/Zn]bbm۶2Yree?/ \t\V4iWeAT#'ߒԹZ*ߪΘڂή RPhmM˗/⋲)e˴٠~;ή~Dv7Wl^:ˮGgEDDݻիu$b 1Fqi.mf{N>SFGG;vO>ɓGU\\,Sٳg0@><<Wk~:\:~h9{xw,\Yaʑښz3g4_A1¬Ywb#_SxO֡q#Z(d>VTT|Ǐ׹1c1ƥl5=;p?3&2cVիסC咒={ˤVYN9-u}˷zk@k*˅3rYYQ8,oYeƘ҉1]seJ=n8ʱc*hmMS|Ǩ_s3|fz[]]7==ĉ淲z%1c ƈ1.\}pmYd^ZܔywYTtƍ63VUUuс!Zfnrqqi߾?޽{wTT tjhmUe ,+˗Wٲ6҉ ]s wq&Y/uudcG7f2k:7]b޽F ٰaΕ@km1fd'&qf/{"t`,~_uY S% xAAAV*--zNcygLfemyeNwƴp ,x'l|J9xcTבΘ#wXܮ̧^imqifףP?"~z!Jb @4ycK29|i`o}5$Ę1c222d!---&&F栗.]ڵkף>?`NNNeeW_}5zhشiό=CsYYYMTTӕ͛7h 7ٲ62׭[7uNpR>Ǵfa$9zСC,im;f,nnXrSu6bVQ9r߾}]2ӹ1cuǘ]GbsX 3Rlƍnxzrss\n:77Wcǎ~~~rCƘ5{9wwT G!+Ko7\#]\|lk.[tҥ]5UUU7UښiԓR9gYMCn;f,nnXrSu6bVQ]_~r`#""u$b ĘM1,\rxFsNc-"ikY $ь1cVsssU䩁~]?o*V {]wv6`@ϕO|{.w7!u_^>y@W['Nn׿?z8lHlUN&-,f +44_S|]oemw?ͬMu7NqW7YPz.f}##'oе9˭>j?G X^yӧO?~<::W_ebM^&1cS+sovN:~_=xH\ةS_P;vعoDv~ҔO>"߾VU4y/ȑCJqmG?|n.1?~o 2d@{ԞkmYHoHiȂ܅,KIc~E{׏1TW;w\/y^.fC}y{w߮~X@3ϑCXjjj+Gy&Ltkb A17d-c>}BNڡ,_* U׿Xlq4KUW~~=/\d+W 2S۫y?oRr<W/a~GVY;cZυl׺/"/$3:ϑcCŘHLLd& eeee10VbL.U o{?(sqƒG~X-n_qݏ?>Ӻs'?HzĘ֖/o%z"Q3ޜ#m {cL߲|IS7=[V0#TWxWό :pΜW ِu_c<|Ȗ]?!1ȱQcl;wd> YoK1sӚ{ W;'>}B:ur/oڜ?n;#77ƴNf/{pp?\ڲ-ҡKYTqPcoSvuҤgrNJ?0 RV7%gSMuI^{˫\dABB`6kWާ}w_77Ȼgg13:ϑcCƘHHHPf6eƍ6mjVKb 1ƥe,.M{ix]tiΜ9JfϞ=999mrI1.X1qQ1ڎ?33N.1c1ƅk[1&***f̘ǀVpo\c b 6c-[̟?ڵ1)--IJJu"{b `…sӧO[iiio Փ~O.1c1ƅK101_c\p!lCcspҐ 1f O Df1 b lC01_cf  @Ucf ^~b jـwb @#++l0//Cb @`0DDD=z!1c "&&F1e6PPP1c %>>722RzLfJBQQ@4+=&)Çsd@1@2dza1c k֭=~ʩ;@13(16x`Nb @$>>^~qc1b $iS TUUq(@1L̴11pRWWW z @811pvc b iJ1c MSb11h_ɖdcc%fWM6-"" b `41`vvvLL$)1cc1%cEEEAAA>>>{@UcGyUUU)oIח10VA5kVYYYudfflFUcJ%%%zXyyyrr}ƌN"##KJJ8hnS& b Z|)%f2M]~=555<<\Xz X1ppYC)oz X1pL锘JylĈ1 b  ,!X1g[b10VAc11pvQb b =F1cg%b @c/%b @c2@10 `01_cf`@2h1`6cc i cl *1c/ MZg6`]1c*)):(,,1c3 j)1d2q@1@Q{L $$$pp@1@cـRbPTT1c h4z{{Jl@J,44tc1b и "󀸸7EEEeggsX@1@ںukrc1b !!!J >>{`8{Jy+,,,LJL&Ucf͚UVVV0=}c1MLJ,))h4ֻ˓7f___uYRRAb =dR6upbcc1 @c%fСCe"""b 1=Sb*卲#Fc@8l)1s!++KnAb =fo1c%1{b =F1b 1pvɿc 1 Pb@0^~b x1`60 b lx1`6 b Ԫufc URRbu6PXX!b  CDDcl@JL7L" @ELLcl !!1@cـRbPTTb FooX1 H>#1@2dzab qmݺ|}}CCC9uc 9uc NX~8uc NCf1cRN;1ٹqƎ;22258qRRR8ϒ%KV\YXX@œ;w.55u…w6hrss9Օ+W6o,?Re7ne @hdҶlٲz, /?޽{y}1c3g0Z{?U @h.]4sr@駟nڴ:1y1L j"к-Yܹs1HZZzF199W<18qĻ<h 6nܸ~^b faܹh;,X@$Ö.]h;Gd2c 4?رcLOc[l @hb-bn 51M,--۵kx8sؖ;vnF5{H|-Zī@bMrq? ܾ} v7|yԩV0AwȡV7r;7oެ~}e/|ɓ$ V_@zbLrѣGtgϞc...UUU &wK귵!Ck%%%eee6mR?ԭ[?XOd@C=۸o󸸸ŋΘc S͟?ȑreeIw. \|!午+&}X܋mÇ=ãsαΝS|/(-[4\vmΜ92Y///}/v>ֻwիW[=K. ~';+|||HZlJ@yiQ7ҩS/^aܸq'\dc=VKƃux\-dݺun۶ͺC־}zΝ;/\Pe9==TڦLsrr*++ѣG졇R?U 7Lw裏Z=o[K9r kRAgVucueM6 s|Az9ݺuOhȲG7++'jixIT<cuߴFfϞ]^^~1Yc1b @1?ƍSOPBK/&YPQȑ#re˲z:SAkaaa;vyI˗#Ʈ]&ի[ddujoSf;v~v"""Φ ; /Yցjxy@$nvey$d_&7ӧ%?tTUUw}ZJJJ.]ܽ:cL;cϟ}_ yBͿ7Qz_9[cfffϑ'b cP̚5KfǾ˗\ٳg1b @ӓ9զf w-޽<b bY8vHOO @hbk׮=vc8tЦMx1ch4?8Voٲe&W?1͛7O=V/99=1Y0 P`ݺuEEE1ŪUv<hΟ?pB^b eeƙ3gi11٩IKKc K]Բb` ,\]htn;mvSZDEf(D$YM-]w|1zY5?3qNy^~/^X@!ƀԭ[N}t̙QSN8x<ǰ•۷o_cvPcX~A @KLa1`7_cvkJLa1`7{ͭ[FFFĦc{zz7zS"bرcϞ=˫>|o߾{v`E`7_իEEEOƍ{{{'<(˷&ugϞH$X~c 7جYʪ.\8lj%O=9 +--...VbX~c f"$YyyyS.C(lѢE˗/ϼ ͛s9̿A1D"MgΜYZZzǏ?~S.T*uѧOΙ3'~yѫWZ*sː2=1-s4{1Sq%+Lf/^1u%6ܣ c(AY4u>1/͛ ~Ǿr% .ob `=o11% Bc@{L1=P{c@{lƌJ @b$1@1@1S1111  I߈q_@… Ϟ=0zzzL_2X6B>}2Eb )֭[xLeb,ܹswirܸqc̙BD">49b YFGGKKKB %6{+W1\X<ؼyBuvv1\d2،)..v1B}}}6ƶmfBP7n(..u $scn b!ܺ@ѣիWoii9{АcojjF!D+W Ԅ*{䉉c㵵]UUuE3 i(֖t:=T*xQ rС[$á:::L/ʽ888Ǜkjj^|; JUUU11LXCCC+-*"$Y$9{lvwwC@-H455^9p@uuuDVXqڵl ~[,>ϞMիWmmmlٲ`oXEEE{{{v|__1-Y$ .,̦MC544F@'ոZlYv|*1O$>\|1V]]?44TWWg1h4{;͛7Oc;vȎO$ͦcٳgO޽[^^?~=a1;w;ھ}{ii%VVV{hѣGc~Cw}}&wޱq WOOOcccnee.Y&dK.ݰa@t:*b `vž|(x%)-﨔Q.e`XSɕlL$\$kl$U 3Qܱ_k"9z*gjS6ؒY}!g)gZ y.Oy c^k⩫7>][k!Ԯ1%-5z) }̅x>]ˀP9yV{ѴWR Shۚ8kQtlFjOvU +]iEDY+Ex.&7&ሕ7Zy\ A *.RչԜ4:aOW42 i);HiҪI {`"heX#d( Ty]A tLۇ:gǩ/ťX/i:3a9v Z)pS kt;CX]rGz/ƿ ;*{po魯:PR#+'kBri ktm$QE>G2Ϗ NȌotpЮMhqm 8nXE9-{ǃs/Ȫ0 G :H9IUKz c0̤8vӦcW\9~xBB©Snܸwcb S1F@ݽ{w…?޽g}#-c0މc kv3c0މc111Zxxxlڴ o1X k3594lO4ʋc[l* c@E1VԅiX]]畭Z=5v=xo1@=cixۋ/ZQbھ#9?unWXݍuƨ%R)I+pcUǦO5+V- b,'`f6KBn"[t6:)ڷy51VXWcbJأGx cG16lXpx➰Ç`g"Zg[[ߩ5[{)ںkXV;? ƪiӗuYa C^,Ț4}fv`[4`֭--U~~i~e7>ԩ롩mqi77UcyI, 79˒svv֥w3gys[Q⹎%ok}s2ҥÖ+؀߻ʋ'9\\( tQ)WINh%!yԏ]D</ՔaI1Ƴ3ꭷEGS{߬?t軌sŅJdҥKx@7!qbUrn FIB쒐=^w*{RnSdッ >!`Ǘ0QH%Jdݻ,d wUs#&3Ap~IuC[M=W'*$AgyIxɼ?f g0g,>yɀ;V=.:0thٷ$(€#&ŋ(rYtja/uuIQ;c:_M?JIܓ~虋cH7_:Q\\dJ}]1wbaÆT{mԨ!2.}/3m//d]t+el pQvcZ3[.-}[P})eTF/\H޽ƭR:5lÕjKL:>(СC+-. ;1ּj԰xAhS'9/~B3;:Z[7c4r wWd9:6߲>fpu~6I364?::">~͛7?J;w]1wb<{Ɔ fuޥmJd\mJPw/7ƖjySv$c§5j>dg%!=c?K;cN&9Ûg_v+ wJx.6{1N/}|OPQ4FINh%!# Ʈs +|Mò6/|$%%UZ?1@cYvvKfOSٗ`u^]{znY<40G)闉TS~+WԇY3(G͞=qΜ'o~y1:%$H&9ÛM+i01sI=.$؜oz%ź;TB͚5 o1@=ck`Ub߲yC{Xoh"99٨=SN<<< c@AsEmۊ={V2|O?AD%L>(1b ?cc0 F}N.e(ӧ*?o1 U#***-Yޜb 1 1&p`I;拉m `c?bbb,Ye˖"!c0 VbAb 1 Atb,&6v! Z @1c1@@b b c1˃dĘO@jXvmnnKIIIHH@b UOAAA6muؑ#Gڶm[VVk4A bZ6mc.=Fb35RSS@uoooR__Au1T/QQQ^^^_޴ic^ Z-cxt@ pqzowcǎÇ#""6oތ0 n?|b jW\>}z~~>󔖖$&&'FJ6@zEo>c<5kVaa!o_ѣ; pȑ]v"IxxxEE1x̙3[?S5c`j]n:"P?> 105qqqW\A!ݻwo"`T!PϹ{.> DEE K4h󙬄owrr\ԸeVVVb j۶mѿ_Ɋ~rhԨj-Z͛oV}uj[tL5!@惎3!4́ Ux^L115N?~Ύ4Xaa7|Cm1S{ĉgJJJ.\޴i#GTTζd )//o;v{C߿bXr6115N 8pɒ%#_ ޻woΜ9...VVVÇ/**bV\dfffkkKD'D/^*_W6O\\JСɓ' +yܹ}wIxI"ՕUh4y$F!Y!Ν;dmmݤI,sΨQh}ήLz.p}?V{n ӓ=`ZI%itIg h0}?u۷o;.͛S^ $gy+yKv򜑉W?N,!yDkb @@mcTdffnٲ|Bsv---C 0a@*H(7?裰0?11wFۯ_tKBBB +955Kl+BEгk׮W_}Ufrz*¬?JvƘÒE[6mnݺu {''|/,ڥK9CCC;wL9R2v^2PNѣ/_N +Vcƌљ cRc֬Y3g ذauTeP!STL4jH%6R7`y/w.ڸq=c$C:99GꚒ"O<)9V<щ]I:pN܎;gϞ`)o~iBBδJ2 06Sh-Zȇ)#$3$- MX1&4=^^%;.*S22N,!y<__jKkb @@T4i'fff/ )%88xAͳɬ3fgg'0ॗ^'Ԡ2ի_|!׮]˶G]v(5ibM 9c3c%1%AkQQ-Mt,U2sai=z_~666nnn$E>L0xiQkbEP7Xzy23󼕼 v,jt8)Ҽ&!&1VPPXeWkkÆ ̶߫pO5xy[3rss5j$m^Rbu͚5w޽~Ɵ>}ϺTOt^<; DB vݩݳgOI%3siSRRؖo6?ydGuʼnEdVGq˖-NNN4^fƍӿ7y#o]KT 8P|Tw*|wzٶm[cIRjV4XQ h4˖-dԩj r6mj0J|?~.\|ndz.%Klْfbyf:ORRȄYEɯ'/Fޯ,}ǓFAy|||Hyzz1 `0űox1 `0XMc/X} **cb  1fb=V\\_/\p0!| ”u @1$Ę k׮e˖]p09۷oG GYhѼyPQ1@ TmXpxL[R}M\+)cժΞ=K2,99u9]6&&ɓ'+bҵiu5PDAe;Ovu*ƪIQ矣OQZ13R<:&!L)t:eZw4y" cQ(}1VzlÆ D jvƪ yb ضmP|CVVVLL 1qSl*??ߴ}:Ò׷Je9~f7e؁ٗ47mn9A'3Ag~{:g"Iާ{{;j ^쒫0x>=w^FmmtĈفkXV; M+)?o )ɘ$:1J%^֏]>-bg$}V̚&x6m#} @uy@P-bly׎Pq<7brNt9:6߾c̬Cן0vUxܲvѭ-[e)Is[B$O؈c< R|Z(cɬE,b,&&72,^?U/KճvC ZX|xQQrJ'''333[Go;v}ͩA/^̎4r֭-ZػwKԈRT:t8yO3tFYdd}ܹ ӤIsΝQFWWץKy$ Vf @^o666~!"?~>>>u/tƌj2I+΂d2%Wg1T?2:nڲl˫W*9%xgb 1 `0X^FEEu---C 0a߷ol}-G֟te̙CP&駟裏/뗞N}HHm={RM6IIInݺq-{Oyx,$3MKYfɏwppꫯϜ93x` ܹsjjjNNȑ# dJ"KErE})aaaO{lY1 `51kk>}P,jٳB;77ٙȐXh4I'Otuu//z]hS!RB 6h&OG|B:شNbyoѢELLLffx/bZ- Rr$):T/&!VKJJlذA-B1 ASCum?177733xMb-,,65C$ 43-$ÇvjeežPlޱ: oѣGgcccWr$)ŸTxOuAJT b )ƪr&{f]U-ěGףX3O\]OG-bjtS3FY&??ݻׯ_9}4bcx \lZrA%!۶mϿHFɜGc/I<خ] 6L-;c@PXuc5JΝ'|j 1|O[:oMnڵӫIB잱=zH@!>>/^ 3c :c%0ji][}=vXqq1  ܹ3&gس Ou`"6ѱcG߳g[wf!ZXZ7⋖dԸUb$ǨÛ4y"دEWs˖ONYZ?ؽ{bbbZjR|}}njPYQ>zB 9*c4oeeղe 4nX۷{xxЏj5"˖-ccxFEE5m0iLb}7~m۶<$%%_LIIԩSH&; T'^*n[lNM-;c@Pb,'`f6KBn"[t6N110ceR.\)c'Nyj?&O\b ʋaÂ?M>|+F7nmR盖6ȧ΋Ѩ1aYf7e؜ٗ47mn9+,Đc?|ZvLIwphfooG i DVEhζVǿC(OH^bZ`ff&3?~{:ʼn2CȻ$dbO(A(yM|%mVظqrrr222u6~x(΍7ݳ c1*sK*Y;`@ϼkGv1Sv2bLrd쒐=^wyxS```' nݏG/ݻ:a»F ,y|U33 _g9#CTs0s')OH^|B~~?EΟ]7N;̨sdTbevFɳl+.i:,(4''f͚{ =577g[ 1I5lؐWq՚5d(UBQJe!#$Gzyee' [)-[%6jq?>kb=bLI IM$QJ"/t-({ɛ~t-fT\RX1&5Je_i_e& 5l1+ƚ7gT1?enn LR{΀MٗH&vC >C/fQ+IH 7?**gLCɳl+Z7/ݷ=6vAt􂨨 6VTT{ >dfrS+jC1@С6,Ę6rRmΈ7[RzӜһYppaFdO:RC|٣ }Θd :1 \aFo~?ё3%cwtNQ,?+/7gybWc1^_oc@Pb,+;zi2U%Ɩ- D0ɘӉAAݕݶĎz5YW4ŏ2xT;:6y֦s;uj77bòy)yՅ7$QJ"I<55ZZ|ϘQ.;JD%ϲZ*!ƞW111uA^O%RsW rTo]q3m+Ցؤ{zj7nD%!VVM [·_y[mxy8]{{;2j@7nՏ]]iIF%R)ݯ] V;Ρ*/tNQ,JilBYvƨR:tpI! wޜ9s\\\^TT$iɒ%iW\dffƖOokk'/))W^;YZZ:c Zmoo 鰌IƑ#G3Fq;i޼95ؗV-oIΝ;DlҤIYf)Uݻw{֭<2щ/w5jtRI#l@1@ clu㫴`<_Ji+뗞NvHHN)nݺ]~}Ȑ!&LO޳g? O۷o_裏„vbbb޽u ܹ3ќ#G;7 7mٲE??$K.3gΔ_]2I$]jӦMRRҭ[nܸAy$Sg0i_}Uqq3g,y6e5^J?b7` b 1Sb$ЦWRIǭZ:{uvv~"6##CvT꒒j6lqRo=yț$66СC(Mʯ.[$.),,trrLh"&&&33SC2щ/4^|S2F2Lac@1)1TG?177733~ !b78mppi4E쥌 2e /?⣨A/WM"ÇvjeeŞK|Ҏ=گ_?77;vH))I, S~c@`/ƄTK vƔL{v8p`ذaԒ|7IVVV7o;coIk֬Ͽ{3I Mm6{{{ɴ+Ri})Θ^:A1XةL7jZ J{bΎBO=n:tЛo)O63֣Gc%:vعs={;F?x^3<|&4nyڵ+!xHD)>>$#ַocǎsppLx)1ckp ^:A1XcQQQM6?M1&&UV*w֭hU˖-,Xиqc%lD;YRR2uT)e˖P~CgY=zt?ĝQb7K۷o'eH TI͛۶mKIIJJLx)?BLo /Ly 1kKjJ1p-1`1S^-ƪ'N$3('O i&gѢE1b b11ƍ֭:ƍYYYHc1V^5i!|#Kƥr1Bdw— ϋѨ1aYzI9;;һӼ-fbZ`ff&"ߍb[vT;%g[ů V}>͙}APӦ/ KKc_:Z"?>|}[T)Q |'YHHE.l]&ɿ~ѱoOrBC'8gqI/} OkVӔ-i?''f͚{9c@P-bN7u6:BKyKl%ߒ=^wLIcƼ0z' ,݃z];B܈ɝ:W ʙYlSt7 Oѱ}|~<%*y'LxwԨ!K %FI㩶޾c̬CW?/vYHHȾ uGC9osmt^"O-ʡEƎfs3.6eĘx~!Sg[blݺu컰\paǎ1bLZ3KKB K * 5Yjo}w^K //dᐢ[)-[U..N=v\] (cT4 G*,6E'YpQv0($ti3 Ibeϋݨ&yeb Gjc4rbL|ypwoɶx.S_FW|V:U+n޼駟@+Vb jxqyЦ/IYQ=${Gm~jPe)uom+e늷:yW*q_ {ɛYl:ѱcOں)Pɵd'Mfs ikknb7j!0eH}4g*wIe@s8o*%bL< Z1F@yb c ׾f+ԦM\ӽ#џs_vK ??s)"|ɛYggRmacˍ%-{,F 6-e%7^0*I:84S?/vR$/hN?h~J1K^HI3ۏYtʠ|2]`HoSNtۇ1K͞=݊qΜB-Z I|EZheC*yxM9Ȧmƃ`Y3VOԵ2noݻ+KB%UW4Y7l^xッI>.$\؍ZHlӭd'g+V:?e`~T(4=] =qIߩS_;L(c /oVV#͛WTT}"##Q]1@51*'N|ގ[/iԨa3ԾU|RcIqyyYZڷy86/K,S. jbqC1c޶j?_ɎMnqF..N4 Y7Lx3L}f'RQ[Ub~^122oA--n݊IHH@Qc0tbL %%%<<|ժU(A}_~y.]ob 1 |Ę͛77l$IBBBYYj `Y1$  c1"16ql( WFx11@@ b b 1c11@m2bL bc`J>o#kJ c* M6zȐcm2=)1/++Cb 1mqE9AAA)1ggI&!91 V]b$%%9::jՊ1RbZH@1U$(1ZGwcKުkNx, Ν'|:V'X%))Cg 1*/H1Ԋ&M^,Ȫ1 {c<2bnn1dȑ^1o<cxt@P]bl֭--U~~it%ok}s2 Gd\^hBswo)sENhMG,-j0^/.bUeIr٪jw*~͖'9\\сbxym۶"ڴ8z XX@ffWdM>MbooG G>7Ɗ/UrF/0hP:iӗuY%ɐ 藫:~gKKKohѢhP !1T(7~_/^\QQ@P؀=rynN stl}ǪT_fBTSN_z/~I6$Bd܈]ңW=?|)SFE >!?'$ת,I;(M In<=ě}wVi1$ʪC.9@x<@^8v9$dXsT"$@2rwȨ>H>6:Oؒ[mܸ 6V\ Zc .D @~z<1j`gc/,,D @RZZJk(b 11//^Bb 1ؿHHHHIIA 8vXrr2   <1 `y[dß=gc-B} ɈA=c\P;OvurJ$YXtttͩS㝜t;u^.$#`m,Zce1V)A5i"{}3v˗/NN>PZZZb,''K/ԭ[]vUSAh8SԚAU:&L&7c@A]UYcjyXq*%1nmLTTHttrmDu}ĉ;wڵk5^;-ձdDcc1jINh'f`_;icooG A6nmR盖Od9EQ[[71bòt׷Je9~? ƪiӗuY%;yn? f7|sssY`ʔF.)ƨ(߾c̬CG/Q>a»F 9P1F !wdX !d;`NC rJn۷$nI /GU͔N:=… :;wNMM9r٦M[nݸq㣏>zXۯ_tCBB h}1խ[ׯ2d„ 3888|WgΜ 1_?jTt*=cx*^NC r ѡx9-:DX%\׿c|S&Mz-&ZR)t+vI mhU*bUV^ -Zd=1juII ) 6H/yѰ|a E~#h4+(olLFF`08#y $#ݱc S{9y>}P{ :R $ՈJb95L*cSNς~* b 1jxqyL'5d̫Ie?bnnncΞ=`@O[[+w{7<}fg@@Gk¢ 69PϠObL&qE2Q͛JRĝq'OsOreJ橽6-=7*j RSS+**z+W&Lйsg᥅EqqΘÇwʊX#Ob633g8zh~lllHNׯ0xI:$"qA/}e&S?R%D2R:dIQQ^PP`mmM=...ŦU}5iy7L!J6H#1ݘ#N&Q3>c>16?jkW4{xv+N@@9s˫flX Nly(N=e2RN'us<.$\$)oPN G@N>Ch޽; 1W>5ҽzuUiH^!X?[^I?bBz.bk׮;w,((N: aaa;wx16oߦ*c䪿?Td:t7ԟo߾ǎ+..&;vHٳGfe6mR:}tbԉTEhѢ&M̟?ڑԎџvƌ=z`waiy7L!L )**JKKĩcޒ߹'?9O^c@Pد] V;|u$ĉۑQ}bXY5?Mqp//7KKU/gc]+*oLnqF..N<}-cƼmcc:S 1WxCWWgZy~Rx mgߞcHTUٲW%خ]u/RMLeK#NDzfٲeB=<<7nVvB1v=UV*w֭3l޼m۶4'))oٲdEEE5mrcFO>&SW":*cHϟ}'NPp|`mmh2`3oTć2)ɓ:t4Ŗ-[#G?R~r!1X=Nn߲'~*ik)T%O<}mEb 1Ę{{m[I1FaoܸHsc111b 1f[F}Κgc֭c;ŷ9NjD 1WpŽ;P1 VwY?rJ˗ _E)=?CSU#̙*b1 L*L/:wnyEsc'N|2! INN޻w/ S֭ݟhEǸKZZ7⋖dԸYI꯾zŷ@& 1F̝;2b Z&ڷy5Řxi7rr6kf$f)%KgK|E=srϻvJV;w̜93ի/\J1j}ZT3"kҤQe_aР>66VM4p`w 4øqéeUJKͶO;ujz|mL {:ʼn&O~@v033Y1|}[Tɸ׀Æ, 8|M[[Z(}"h4jk#F &S}x6qul<4onK=%`'ƈyAP:th(N7 OQ#|$TSdݻ}|~<{S'LxwԨ!B? aBa1:xt|;1q{EgVcvEAhx%bL!k⢂ѻw׸Q: wlI|Wtvv\2zOJS(6lHNC Q@BQJe!ܲvѭ-[ˍ7 SL'9¶0QRR2w\<gp…b1 bIjXX3z$>3 uSy$9$+c%)rtle9>.(p?x+={VnP5on+3&ReO?Q6$dx֯WRco$bLɓ~i>hŋϟ?*5]({!okKɸ1j|1ܓ9wϳ&L/R;*3&|ʮQswbtΘIIZ16th{Ɔ cZrH3;g; 7ڝ;G~ Ę͛7׭[hѢhPppp@j PVV@P XNFL~X~5pPPXUgW.7D͞=1qΜB?ioהWd]zxР>{؄tk̆ 6%=F}w_$WgVibd=%`cb J1V'A1b b c1 1 b 1 1 b 1 b @c0 b @c01 `0111cȉ؅0,0c 1c11@@ b b `rY\\l3%:thCCM0F.1u`%)S"IbsΥqcUf`PL$O h1pƈ IJ3f -0Fe4'Æ ۾};0FeXBCC0£;cN(S1p??~~~={4iRii@y5k֨oܸq>>>#GqDXX^;f1zM'};ܹ3!!A( ''0cxg1zs=cW70ailK.=sgVGFF檪_1c~ϟW'NXTT$d…FҬ69sfzz,][n֭S뵃޾kVXo1E^}򌌌ݻK ca071{53;n|rÕ{P_~H0 rhh/)_, GšhV`SN'xbΜ9O=,?&I/)fYaLd2fr׮] ca071ݺuVeA~U7~f9K.;wC*aO6 846jV@ҚJb5AAASX}׬5+5fz˄10F˜;cz 8~+++vڥK48UWUbbرcey̘1O8(Vo5+wkn;a Ν~o*&&f޼yMy/yɉ'<8~xe}޽jjjɓ'%,Y$::ZV|gaLVٳe9))ISRR;5M6a paz̙nyb@]]p#FܹSY=p;00P6PKz[oe0dvaLNm+Y>z,>}AzYas\sx6Ys@#a އ0cVZc%L:~0F-mڴI/A>}za0@[ t(֭c1ŋ'ʲe8a wqQzacxe˖UVVT-0Fjkk'YŻmܸ  ϟ_ly̋nٲC <޽{oAcxڤ>5***RRRL&7@#y?ƍk׮5ba0@{RQQe˖շx#y~If28a 0%CLcyc)F`J<1#a 0%a #a xzS"a0ZGfffII(//d2Dc1,Kdddqq͔(77wС 4a ԩS>^^;)Sp1СCC FFF9sFVw}~r]w)[>Y8@<&]]tEY[WW' ׮]4c1\b}ye˗,--MOO//:IHh3kǤj10FUrss y񶚚Ţ~"_??pQIk'1É-40VZ}4f4iҢEnܸa $6~x^x/$$d׮]̰nݺEFF~N&171O=Zo]cao[ll~3c/tӉjرcGDDĹAmڴ~3hݻ`P/׿{$~zeSO-۷o^b ._wޭ;w߿Q>o1YX~숔WsIڵ.9s&::Zґ<{}uuuYyiwcx#:n$fIrrr\rɩS6yg,..N߶m[XXR^Jؐ$IɏjJ-dB#v>s=p5o޼7|S3͘1C2L87ިl )_J̙#[+!miӦ1$x8ٳxW^ye޽|LZ]֥Kec c.zY={HjٳbƔ:Ͻ{%֐*%6JJ.1 4HzYbEmmmTXX_gu;c֊ W5^ G}$|e ck0&?Ø[n'MG/_a0`"yL N:uK ߲Ggox~EiYa?$6k, )zSX>̙c1E].cDFF>裇&1~'/3_>$$[nYYY[KQ`a_BYYٴi|}}m6e]櫚}OɅ jn~xe.£;t41118eA#l6;Vdgg+ߛ>|8 n<ƃGwc`pp<0F($cR;c1hn޼[iٲe+Hjjjcc#*wd2ɩtEHb1i2q+++L\mmmJJʧ~JOwʯn ())4iR= d2EFFv-"""++$֡XN֬YןRfϱsѣ/] hhh7oL9V{x:LMx[nl&CyTTWܹsIC1 c/y]//?)S8rsN2ʡC ?9{qݺuAE߿/yL3M4I&7n܈chM61]\*))0N_kkk_x??]v)+333”d&5+˗/ݻWzݺuslt\tL3o~<2a\p)"'|:Q ;v숈87h N&iС}֯_o0:w,^x׿=#/T9wd]*dE~=sLttyYb5556,Mhݻ>oʷ/Θf-֬+eʕLWzR0.n?c֭.Jb/''ʕ+'O:u0fX$3fHP~B}]Lwt8qoQUUYD7||Μ9}{6oެ شiTR40'mm۶0͓K!aL|4Øf-FCXbE[g^Vhܸq#550wpb<+4g˜W~ꣲ{t^W׾?|uuuׯ_8~Jꕦ9kk׮uEYٳgOSW}Qa~3skWF:VZ E~ȹ<\Z 7˜ @\h46Z_* _<-~~~ƙ0&?5rsl֭IWR4I>dJaLf-8&aESXo~1X۬^09/t&wNz%mYQQgY?Cm/^o=J3g{!!/_~75?X(rrr"##}Ç;im۶xzSt\1I0}a,??2SRe}MM/,냃׬Yc_6SnѢEAAA/']Ʈ(oЦRskvuf͚ջw^zɂj;/lܸqR#G;vLMeeew'WW2~~۹sgN8~Hh}Jܹ3!!Av- @|G^;yQ7,\W_}Uyڵk c1}^dS/ׯGn""">ԵkWG\ud0m4_R.=w߭n,HkQQQttt$O69A,>2 rehz,>}A-&ØMz-oرc#GTqF)f뵿Bc1 c$1N:~s2Ip{j9s0zfoh`\+y $1N:XϜ9__L]0`sN=7SSS c:̒q-//d2y.Hbtp`ʕ.: 3f?{׶K.k֬a8ެKOO',СC mƵ*22[?&yL&)`2!ON:9I׎ZJrŋפ1Y ;v` x$g'OO:%1tVHfffJJJcc#T0)a hŕ1u,O?ǟÆ woϟߺOQc ͛7OjL#{n_5ޤR1)L `ӦM2 | RRRI9c@9w^@Ǒ Ͼ]r%R. 08h"& @R\\ORR佘mb. 0*++۴iiYrCCիe~'5eff^pݻ7// $#[nnԂ0/lիW)))BGj*. 0h[b^;uԾf^a 2 bmX}kgJkwlFqFjj*aQmG1@ZΟ?ݻGy:-hMMuh߿t; cw7k0&stg*=tnݺ 2d֭<=̥1ƀ1cƼ%%%{쉉[Z}.]_X ^ѣGgeesر~˜gs]V\ɥ1ܡgJja{'O׳gI&ʴ)$$D W_-Z(((^`pIDAT/ʕ+&%]heas^qqq{^fBMMMͬYd^zɂjw쑲իŋf`zz8gEEE 46%۴=JWs΄I@@@NN}8(dƍ9R&z3FhnWgÙp5 iVK}|]oyT8Ӟ.M2%--M%05}t={أ>:sӧO۬4UUU}믿>c Y4Q`޽ÇXxҥ{n`ѣGӜL0XUŋGGG?W^ќULϟoGK,Qƍszfi·dO<9uTmJTʲ_62qĢ"-\pԨQt[:s`4Yzu<< gBl+ <*ivd/m NWQm{@C ~o~CC c%%%/`ٳiԩ5WPM2EwZ-_~%>v̙3ze3[[>>t[:s`4Yzu<< gYhn{qzkδg crLtlڙ~1m\chE֭se!c1Յ fϞzС___5wuX,=zx,ȯR.w֥KΝ;O»uV]],˂QzeZ7ѱy/U-~l޼Yș/'+W1:h|'N ۽{waz˛Bfrth͕zu<< gBlYa5 gӛ93 8/arKa hĈJJLL$1qݻN{]vҥKuzԩIII˖-6mF& EEEKN8hrZWMY 3`lٗ_~C߿_tz^̷vջw&Xs0ds`J́Ѳ0Wgãɾp qbj<*iu5 }w_rj30ZUxxxxa bbbcXJKK?>zfee|7'OV{K/%''=Z1UUU?~q;3MN \dIttT:sUJ!{7o<#<"۷ U̦zg0a„#GTWW˴[Ơf1g0ds`J́Ѳ0Wgãɾp qbǎ~Go{ͣ™lOSիׇ~xeI`xa $\s=2]ٳg:}ԏ>>>#Fعs#^~e??ഴ&ze^z2 ra3{ZLagΜ6YP?c?ё)%|*fSֳ_رcСC(ܬ0̞6ƜlL90Zyx4^!5k/%]G3˜?c۶mkրdxնg.a 1ǎ+;0axja#Ҝ9shxv(gb֭3 fshh,xc<c ݩnCjj*a10<$.y, AV\ɜFo]tQ_`եB/t1JR7ZJwcQQQVYYhrss8І͛ \jAC lٲEMb6n0o/WJf @GbŊgŊ#[zzzmm-Zв˄~+a XBB3Co,Ynga ;a m/,,dtNiii>yyy<ԁrrrȂ0ȭ[x"q\x1--CƟ$ꭖ-[LA'0u$ xCjkk']e{b.`Rp2XMMo]]]QOIIIRRy̛n޼ +08qݺubx/.XӒwػwm۸I9c@mܸ1##5jkk?^cdRRL_V2L\L1,Kbbbzzٳg[횜iiiK.=w\y睒qF~~ڵkF#~'u:+++55u5k4W89زeW?wҥf2<+~'`\88#Ip~'`\8p0q 'y0q yD08@p=8#1k]}}x0q @,))>ٴi_!/0dw^\*!!֭[n5,ZW+..t/0lӦMDARRgx0q _.\ ' ݛI.^;a /+W$$=^' 1kZb;uXgJkwtQ8vƍTN:p c޽{#66=]l2*8%͜J)ƚ2nzjN:p cؘ1cx㍒={Ĵ0 ;a $u޽~}~~ɓz9iҤңG\~]@꫺EK]rW_ҤkתQA󵝬UFg}3h HZdƍGy1̚5w޽zUY_[[(Zfue4 iVH}_{5yk3=K/0- c>̙3O>m>22l6WUU}w3d< vyÇXxҥ{n`ѣGSkmrfe4Ø,|Auuɓ'N8M8H2… G`cI\?~ϟW3-d!6Sxk3=K/0- c%%%/`ٳiӊ)// d!55駟VVN2E_Ͼ^fYYY555|ɓY޽{e>`cɣGUUǏo.qqq?&4jpP &9rZB8Ν~KZl޼y%KDGGK3dmiBoرcǪ_r^ݜYx0q hap{=,_fgϞUgggKSRRY׃nSXWW'1bΝ"_~/888--i5{^ev1tP)-**l6V2s^ɂ W[ANR/'kL|ˬYRDzk3=K/0- cx0\@CI.^;a /+W$!=9ŋ~'`\%--7=p'x1kggg+Vu'x1kJHH 'jjj9 1ky I p 6rc׀s-xbZZx0q ' |[ \@hKII)(( 9տ*oWWWscmׯ?t  1k@6nܘqDmm֭[W^i.^ `\bXϞ={ r'6r,]ܹsM5jkkRo[ ;r6E9x8 1k8#1k'#0q 'y0Ip~'`\Ip0FzRSSoݺESpG c$XXXl2'y0q Kb.\ qG c$ qG c$F8@0my<NIb1N:;a VQQa6OM$1)d2 N:;a Z a,**꥗^ڷo_k%f'Ni2h|LAC'QF«zA-cJkhhPjll4ӧOxFA;a :n߿$1VD/mZl5Ţ( ?;a :V yAS7$~'@PQQe[awǜIb,KffN;a <$LAwHb~'ܝHb~'ܝHb~'ܝHb~'ܝHb~' $I9w 0~'$` NpA@\1;a DApAqիW4A $PXXDc wpAܗ*++/\@c wpAܚ10~'$I<@1M#1;a mc` N.H@***f[Z1,544L&y!@\!H:ts=_VkV_:eʔaÆ` a  +Q$,%%EFYsXy!!!$100"""X`3X(kYc_Ǎ׷o_yHApABc$M’rի-Nbyʕ+ʭ0% $100h$u>;a.Ņ0F_|(9f̘Gy8{ʕ+]$VUU|b8lذ۔ tPPPaa!0:t 7n܀^}ՃnqSXCCRTccl>}AzP86VD/mZl5Ţ( # %///<6m\X|9@}6nx&g}ǘp+V0 ĵk"@}F#pԩ d97oLMMeLcxJvɽ˛,͝aL}/maƍ10g1etΜ9<aZVa aLw]]~zйsgf֬Y{ի,ȯfqqq5k(+-ZK/UVVOP4hG͕65_{+W(5Ɋ7nqDXXfg5ke}߾}vܙ qH0p֢^?6Y^K  Yv-a ^ØrglԨQ &(@H=z{+/^]PPpW^yEY)oXxҥ{n>}|'O:u65TZWfRWlĉEEE].\hi6_}ƌØfᚕ9sfzz,H֭ۺuN~L>J\!1[,>ƷU͝ cnW^/Xz{@1SO=%J]u༼>>#Fعs~ǎCQQQfJ)O=*.{OԬ^^y=p;00P lYӫ̩SdJ=*˧OvPl QsƷy#6vر#G*OhܸqZ^Jx嗥`9ca vƼ   cm͛10XIOcx7fdd0SvڶmRSS8b$&&={ƍL5n޼)Gr;w jkkRo[ ӧj,Gۜa _$@1 h@ a 01@wl ֑YRRL&M1,ː!C m_|@a ij>1%I q0\l6KR1IbPPP@a Jcc!C$}I ?Na h6lX1b4 kY,05 wxg/IlРA<cMfÕ0ƣ;0D}p+(aGwa Uqq:cMn޼aÆ~pSpy cILApw#a M#a M#a MkYkhh0LBa IYy/ˤI ٻ|J , D7UD%Q:yKoٌr,oBTTrF$֘GQ.ц]Yk ߵ߽}^c JbV˗۷oB@pLc ҡCa$11G&1` a _ϟz'y?~<111**jV&^42ApdSXmmRT]]]nns=ץKaÆuԉ<01e %I?&&@#g͕1BI |a0[n2y<&J3 ecwzj*a0,,,L cܺa0\g )TDD@#ٳƸua zn0F.(S(n0F\9$`2JHH)l!Uvv?߿?>>~…Ivlk.9}ݝ;wr1Wx͚5DLVVuNc4S'O.))!jjjRRR իWy p_~ӦM\ h^O^^^NrJBa fdǎ999$ׯsU0FY5knK.a0@w+W>͛Dž a oŊ'O$}lܸ1//k1M\RR/IEc4}hޢEƞ44a0L~nuX-ի .a0@sc-X Js͛7l`ׁqYc  3귽˗RrS c0a ˜ԹzҤI~~~ڵQxɒ%e˖&OtRlll``/XQQ?pQ|||ڶm;rbe}MMMLLL@@%55U-Ŋ]tzwޭy}pp Ο?o;N]?]]Ыy{b^!`==Z^W_}Uև,Z0@ØĤԯ_iӦ>O 0tر'OVGFFVVV9s^{嗕g۷o~~'Ə?bĈÇK 9sf޽5X=6Y1cc8viٞ+Ø!Yx*kcl@u<}t ca x Swn|r:w~ecǎ۔ e944/)_,~///VmϞ=Y8f[So׶|g̸^c4Iz1i6W_ʲ10l c+===eYՍͿrM-[۷o߿d]u Yylٖ]4իaL,4^em cJp[^JY 1P0fL>l^|򒒒/%i3fKۻw,i1ޮM׫W=˜!39 [޼TYY,8qx h1*z={{wo?::j[`>7q… pa0FYHOO/(( ,[1µkךm<>}[w0Fyꫯ n3fm1c4;DϟJa0@SWWo߾`+..1cI ЬXbҥ$ŋ?쳅 r1G^zرcrəVXXEddd,=@P4-J.9ǸS($1 = a )z` 0@@OBLГS('!Gr劅)o0F^TT9ʢcʺuVPP`2o"##kkki"1pg}600PcJLL 0FΒ+Kc2$ 4a 8K]]]nd$yL$f0L s%&&v޽?kժU4 a 8WYYYXXBCCua ³>$Sn0F.۳gO%q1p6ܺa0\*11QPܺa0@Ss9s$ӧ*!!!ɍq.1#UTT\qcWɓ'%a 4$Xa f۶m]t___TTT]]=r6mt=//OmVVVddg׮]322HbnZh*3€JYYRR2`;sa0,\矗{O?liӦ(巾;v駟 e[1 cgϮ]n]xxR3ӦM=z4g;@#Foޭ[_~ƍGw}w۔-NzO4i˖-$1wcJ]vM~geKLJ p/ӟ_7Q={|ⴴ>} $1c̗0&a cǎݻʚn.++S?k8qǮ]$8Iqˌ `?g{ԩSՏ~g3f̰1EƬ֎$-*K:z<&%Kxzzv5##~ag LeÇ۷u7h0,G#nY~C˜ڑEe c@GؑX$10lQY1@#=@# [T0t $-*K:zSf6mR~cǎ~P6 <$͞=vݺuʑ#GΘ1C+cѣ|8'|uV:ꆐia ۷w_~qFdd#Gdwn6e˩S&Mڲe I Q?{xx(+/]\&>>>t>aݺuYYYty,%%0Fnӟ7__߳gϚoY\\֧O^x$1 cj cF:%:gG裏dC#εcǎݻʚn.++S?k8qǮ]$mo.ska0L~e4f9rdll?0c >̙S^^Nr s%$6l0Z{}||BBB֭[LOO 􌌌ζ=)wTټXw>=??_sO6m nbŊN:jJ7~Qf\x c7on+k4ӧ;gMII!A9{wua p~饗^:|pn=1ej{YUNTT̙3O:U^^a zIaƍt_~,_ c <˗/7|w-~S'%1mVV}ݧ,9ro߾wq{Qed'|;Tof͚]zzzFDD\R7cbboJjkL`42~{T޶m[ٻ,,,B4F(0V=O?T)JVN4I]izƬbȑSL1aLe2/PƬ$10Ʉ1YHMM .t„ ϟ7٠Z:U??vɂ?pQdtn۶tz)W_U̼h"NxŊ]tzwޭ~<66600_T~3-[Tl޽&o}` c1$4ܷo֭[ߌg1((>SV.YDrOOϮ]B5c t˚<1{FGY0`w)#֐!C٣nӪU+wٲe|n!eC R9sҸ47+Y݅ JYn@͝jLjC=˜ӆ$>406p›dAm bbb ם6m^3gμk/~u#F]ؽ{VK*# dcǎ 1{ 1 88X` ,oN8,KSsʛ`cǎ~zC C2eOIتU+{))PﶥfYmr#~Yal֬Y}h$+qqqjV}Zxx:wח}-h"? L6Q5a N+҄ 2UϟErIbŎzEcpիWwANx/[kƍ^9993gt݄1`o0>/_YQQAвɓ+W0WPח}ݛ6q#f?~zzzII^˜Hn̔, fbbbTKHH)L -'xݸqrڵi`A_|׮]l0x_p}x'W$UAӮ]ShΝ@/-^رcGrrSNыc rw}w͚5,h,9蚁b߾}ölB^555)))uuut1X};%%%t(p`,rvv61VXkhcǎM6N0F? IL#poڴp[|֭[4\EEy0F\6 p+W2=[ лcڱcGNN}*>>Ȁ[IJJX}Qqq1},a0fݍ7f͚Eg;~ҥK2g>jjjΝKK#YW\\rJz yQ[[B{=>C#YbŊ'Oe6nܘG %IvرyfzZa -x"TGrr261˜unxh tF=:A,z… ˜;삎=##`0tˮi|6KШO di c'N_6m .Zzlae\~ cp ;0ظc޼ysH6mbagMN'a00裏[EEE6l߿# c1wc0c/_nJoXf}yg91di c֭[;w|FӶmۑ#Gڵ+$$Dpe!88ᄏtRlll``/XQQa^_}U)MJXhډh>a7nyI㧬XK.>{n WWWO4ϯ]v ?*kjjbbbW6--Y]-##/ d/l7[^Y˜^w' ƍC:^̟hAu6o(ٖ^њSV244tPXXزeǏ޽{MLۡCڵkM6YnqJf}W ^#Y())_]pA~˗/+jL%N4ɮҮWŢya0FsEgϞmݺW˗//))xbiiz3fܹsyg5ra>|rR޽{7j+}5Fsfݺu~~~ cz9'= Y=Z[ZF[F-V?85l7[^Y;cz]^7kcVm\б[c!C t}dyС c,1jH]غu͛7 z/IKZ33SBWVd P˗/ޤVҥKAΝzvZ߆kw[|]wݥnw0 k֬\G1&N&YPpom0Ԕzx^!LjjjPP,"5͖W040frg6N^yoooX1/ͤfmQ- :va̖>.VVVwLhܹ1˜MnJRH Ҵ4:(OK#*::deemذpC˖-8PII oc5ka۶mk֬/֢E? QSS3m4-F#x /_pguuuʢĊW cz?Uyyyݻ酁Faƍ@̜9cΎ_f  PRR"'Bh\~˗effVTTСɓ+WF/J#9ƍ7 ,Y-r䄑@?ݻ755!}<ϟ??==0F}%qpcT¡1N5.*K}%qpPY`p,aS > Ƹhc.8`pn* cqKJe7l@}S*;j(`p\d۶mݻwʆuc ƍ FFF&$$uctUUU!!!sM61+.t=zagϞM61+.ܺ# @5 $oAq#۶m7HeW\)yLm<c\W0\8ndܸq$<֡Cws co4IcUUUXXČ++y,22ƃ0u뤧xʢf>iO7ЬlM1p]PeeewԱo˖-QQQ4'ΝPYuz!5)c_VVV```LL 0ilV%qR:tѣ1$)Iϧq&1p,uuu_t$< 0N57zuÆ [j0i$S :tPҥKΝu0N5\{J=z44켷0^v={rI#a j0ͺOcƌ7ǎC)񐱏[wL cT#C!ɹ=18rn0i$S0ƃ0&2fgg$''/@6eΝKS4U7_r&T0Ʃ ccsIMMݷo44Bsk׮EIL,aS 1< cNc2K` 4mr3 aHe cj1-Z8dg[˜L˦MVRR<hjjjRSS7lB:/aa|8GFF,KFzսzuڵӚV+U xoRu}GI[l)?KќS||\=|=\<%kڵ[֘o٪ݺujګw^l 5,0x/RCX@sVTTtR&$L cT#Y c y.y}zʕLFf.11 F8cXhh#G,:,,PY)+|BRqɚX-|3բc//-4Wv7jitNu.] oQ+*eA^`u'wCB:*OX)S⮲\//OaLz%a,==ɓDf.777//9 F8ci_;,˂h^}ݷaР}|R>Awm^mY#Z?qwզO,4)V4Yˍfx?OᡬkD>_Ϗ^[WSyܒ04K"F8cygL;cO?[x@'j.KG%uB/bYݗ])IP_l^+b#b}2,""O~,/-oLZÖwcmHO¤جYoA478𡓧gL˯]/ٓ+uBW@6Ӭ^-zjġ_;&CvVc3^ffNk<7{Ԩ!"#_/^2i 5%\ON3ga0L~6삗M炗ի1&1p5A7|^G~̙oᲹܯ>mW Ø_;4nWԙmn9jŲ=zyy[3,4bHHG9x w,4_t koY*$oՍBIkX)aᏄش-[8<נ?~Ƙ \0#޼yvCۛlK! 1i$S0ƃţ>GH/11v9g3[˜p阘}<<<._ܐ՛Qnƚ;c1&T0+a̾0&Ξ=~?66600_P_bE.]d|p5ǧm۶#G,..Vr:tvxӦM2i\]]=n8yVpppjjloF>poۿ3&sW:vn/ZeY;i$n u{y/~F`k*yyyg䲱l,޽;$$DY Ujdr1ʔ駟VV=ZyG4ux:ٳG-j;glΘ^z[j!U cǎ?Ks%aI#%q0ƃgmz{ ,HHL'_~w._|С~x͚5z1vG˖-_۷[s|2f+SVUUl,ۨˏU\†ޡZ=²6mڜIG 鵌^f;gr.k: ̮ZܹsĈaaaׯ0Ƥ8˜gw}><(,In1tf&51y]+_z+Ĥ^zU5kw}ҰQQQVƘ4RYl c fϞý]sM:"50W!S(aq$*\1WƘ4FӘ^SY9sߍZ{XU uԺW޽ ??o6O?X|8:Kɑ;'H ]~p]o(ņJ9jiF&Շ|蟕?| )Ȯ) 7Sƽh#2 ]/_i^?{dA}GHkz7}m^ɋ޵k55ky"iȁ'a̪7xĉG0`oĚ0{]|9--9 F8ckz@l=ߤ3u}NoxGO:^~ŧwD݄) șzV0땬>$oHҐم$RY$Ǯ06;v,_sセ%٘nWSomܻ0YAhhGQ7{-!;$o>Oh`5r0wLf@3d7NA#S3ˍ'߉N&9rrػoàAܥۛ;O~혲, cz% 4B_~Y5ޮ0f|r6r,Sq ^ lHkGn6}b{5kac& 4g ,`B¤0N5˜䵤U>Kf:,,^9P󉚋ޏzb׻Ccz%?|Z|DS"{q9gM*[.#=;,;cz^c6<$KvI`,F= |P\\̄I#a j191O.[>O2KNۛ=jshp“Ov7{~oaLd1~/]~\@@ cq>X?[G~n”Oo wYYo47-zِSO 9p˿_;&aCv19dpj=ɓ/f6¤0N5˜?z}dۥKX^<}C sܯ>mWyaLow2}}CC;~ln=˜^Əկn'˕U{eP~a?H (:i m{{%tU Mh^DzyȂBB1zbY]<{׭19dpj7nbV 47ӧObL cT#hFw!hbn 4͘1F8c<c Ƹhc.8`p,aS ʕ+>`H^TT9eeeDF8pݻyLvU[[KL cT)1eocbbhI#a j8KBBBz)yL>IbOL cTY]f0xc2Iѣ?NL cTF!^~4tUV,F8pέ;&1p ݺuSذaøu0N5\$!!A c=zF8p62qI#a jrn0i$S 7o^hQJJJ2}ͣ)%%Kq2i1p@#vԩĹsnݺ -33Fp9sH*~:2F*K4&׮][pҥKe^Taaa\\Ν;4RYq>y$y4+Wܴi5F*KiӦɿLd_>;;I#% իWh"{kI#% t"9sp3i1p];:zʕ+/q3i1̞=ףi1&T0+p;.\dM[jjjmm-;F*Ka ի i۵kח_~Τ@7LAJJ ;F*Ka Ȃ 1mѢ[J؊ha0L~I5JNNzgHe c @# c_b˜Xpp͛M6ha̱5SI#%01˜wq&ЀNaI#%0M*Bjjjpp Ο?oAuuIڵk' Fi۶ȑ#R«*CBB-Zl,Yb0Zl)?^t)66600_P7[bE.]|ݻw[޵l/tAvڵ񲍿M vd1[n4C5/_>ƍ#""4$^Sk444tPXX(ey޽aaaaI#% r8p`M0c $`oڴi3gμk/~Ç+x8`ұcN|~+1mcz;MNNn۶ydyܹƘ4RYq .H ccckjj#B7:cĉ$ '233[j q 7NJ ^x-aҥKRBΝzvZ AoVØގ:fF~  Rjct;xcz[f}'O͵=v]dСCΨaI#% wkb Srr2;F*Ka ܈P chRRRޙ4RYc@c`H4ok֬)((`m6lzgHe c Yhu4mrr3i1ǫwAyq3i1픕-Y:ks3i1Xb۶m%&&r3i1p]z5.x]xqƌ|[I#% Z]]]JJ_| Mٳg%su3i1p]@#iӦ$nv.77766Ę4RY1~G}fffVTT0G#r?vZfHe cСCiii nFp ,MNNʸx4RY8`p,aS > `C}%q]0FT]zuuuu4pPY%G.X<0\8T0uuILcO&}%q]piS> Ƹ:> Ƹܚ$F.*Kpky `p,a uΝͽ~XXmmmVV<`p,aS h$w} EEEJbvɓ'GEEr}%qHZZZ =z 4(7Yc}< !}%q7>&o/w1 @I.*KT{k< Ly… Nby[a^vJ.*Kk"gɹ0˗/'aOcи}7.(((22GK.-((3gNMMMXeey?o e/ٳ@>1M?I+l۶Msǔ$V[[UWW?o߾!c %hI`0H:R 2ߦ~y$+++S(޽;y @>1K^^^hh[acJylQQQ14>ͷlIb剼( %@=I %@CI @>1pu#'T0c$1y$ PY:={$ PY:IGOO,a `"= [T0t-*K:z,a  [T0t@sV[[x⺺:`آ1`T\̙SPPD,a %+WlŊuuu1lTb555W^ci)11l]bbSbbSbbSbbSbbSbbSbbSbbMhѢSXXc*66V5*++-[V[[ac=F1lcc=F1lcc=F1lcc=F1lcc=F1lcc=B1lMb c@11 1 11c @摚Z\\l4󳲲8E@jGcRbShaaaCUzL1)ϙ3h)999ccRb>>>PPPb :___F#=& /b hz.<==7nib ,VO̥;0a4 b HNNΐ!C$ƼtcFxp EGGKq ʃtt|:%NË@: .19l믿>~8/A1踢~=*xs dʕ+"99"tD[l),,dN }Bc(11 1Њx!1@Gl6:utFcð)Fbb"/D1 Lꩧ;vl8rѵkWWWɓ'?~ﹹ>>>cM>!Cx{{>p<~x#FӖK'"9 @1_k߿4XyyΝ;eL4i'gR~֭s-Yo߾ǎk^po[ǘt_Cdܹ/{ny@:zM<999YqʕdWΝ;/3712Ztwww9uؘڡSjj{y,l/#AvnWw@СC-#~@1dyEGY0cqqq!!!eee9s ._,˳f͊Vggg꫍߰".\p̘1W~n3|ZYgϞQFٹWWR?Kƍg1z9997n͘1~$rvv!we-#6it3s,1֨_ ^re̙whꔚ~?O/{饗,*]\\lRUUuSc1@c{^1wexРAϟWe t;W_Y_{{{u?2VeJ`~,7ڵ![7i$S;駟?W'OZsr,~\߸qO~eYhӦM T~&beyzz<WL/h5rYMRSCR1ZU%%%b hJC5p@1&.:w,+I/52}wowo-Zm޽ՓM8qʲcUU, d=zhppUrC}W e955U}TYWf&g[uX?eeeU\\ܵkW/}ɏqb\7iBY}Ŋc}[1+o҄xEtt $9E 3ftN塩ߩ0a *...@۷o6lX.]d*'O>U6lؠ$???ѣG71o۶Mf[nof*W4̌'O|S{zL\fѢE_rS/j*OU߾}333% $ a@@ȏ#Fxxԓfbʛ4!,ym?PƒlߡSjj{y,oTmݺUο !b ǘ*,,lɒ%,ɼyܹ5킷+)y"11"tD2lׯ_f~>>>.]b67͕+WΟ?bn$^b @TN/Y 駟1c?1ѶnZXXThgϞݽ{7/D1t:]JJ sbUȳ"tPK,QA[K@:.VvZŀm߾ th6l8rcfZm\\/>1YܹsL} @%%%}LUQQ!%VRR@8{b1tPǔIc).\uVM|r9c)֬Yÿ*&;tPnn.#b M֭[ @Q-SN0ݻrJG1?: I&A]yʕ8p`޽CBB٣nSOɟG=H<#%b T) %%%s}xc'?_\\\^^{`e}^^^w!w)'NPo/ ctS.eޣGuڵk5MΝٳg8p ?)pwwJNNVV޹sgŃvtt>}zee~޽CMKK37HdE^^^wUȂܼywurr[19ׯ_7`)3e'O;2Qٳ'N} fb ֌1坱1cƨ'LpeGKƍ *((r̙3r!!!eee9s...[l:}ԩSͬlpGbj:Q?3ʚ={5M>c3`>^|{ܹs 6ƻx,{xxvƌ5Wޅɹq?k,ُѝڌ@ 1prr~&1?g~~|I///eyȐ!c}9sFY...4h斔_#V68B#1F('OVL4IyM!1&7|w4M>}Md7oܣV5cFyyܵŏ)oF h3^fReAM*.:w,*sssœwef#1Fyz},( de|^:gΜ +SLjLѣG7zH6#c;cRPGG)Z\iHLQGsԩ˗/6mCoSNYcB&9[smoܸ!3z?/--}vYY^͍nF h1G_U ?>:::((VBBl, ]qȑ+'L7TUUIwYiHLQGsϞ=R_>>>L$""_V΀l`>޽[Ֆw{Hsry|qÇ*qh.Ltlzzzuu'.x31Ƙ{7,uև~hVZ^0))a۷oWoݺuĈ2000''J3Gbj:uw\O$H;NNNVYY٫W/BBBGi!i . ֭[7sǎFs=gWc0233*erz}11fqMiݻw1@]]v9rdnnn;{\@P*iV^ @[@>|877@F|֭[ 4M\\\]]#MtRMPQQc4޽{>LZOA1.22vTUU-^rssy hQQQ?6mڔS1@ݻwoŊmϢE$bccۤUV1~c%dɒʚ6)33_@ @{+@J@fuȑaÆ=zHnnn{9rd~~YYYXXN]\\߯,-- իWhhhYY1@%m۶)iiio,7n6;p@``ǎ{KdJ{Lb,**6##WY)`addI1ڨG>ѣG?C@@ee~:S.]-͛Ξ=СCX[1ܿ_)+ܹ# ovrrb hBCCwܹo߾)S(k+** ,))Yzرcz-JzFIece5Ob 1@vر#G={VYVjV~"q֬YW\'f}QPP޿޽{ٳGfÆ >>>ݺu?SSSo;qDlTXԌ{?~N{7222HV~u.\ڵkw;U4޽{+**'˲F[0cݻ zh6={1ںW***jmtZ}9/bzz֭[ezO?TGSG~{z%xauŋ0@EQ~ܵkÖ-Bf1vCH?0wƌ*׻@[[7PWG||+2gΜ{ʲ2GY5mܹ3|F}LqԩׯВQ7޼ysekfr0yӧe̿egxF@qAW^^ި/_IHl۶-**jСLŘmw)"c}/nPUU% o_V70[Igyf̙_rӷ{n9zJ1jŋ{A '#~zLLNk•0]֧OcRh4\Ϭ1 ˗/[Ep ?_nhv!iWQQaqWGkP͸qFlllmm-#_ftXppݻ{VlٲÇ+볲srrdoeY_%ͧN޽_ԓx¦o?֭[T??Y[JlM1J_cfxrppPҫW/I;uꔺg}ݭ[7sF3&<<|,X@zLtڴi6mz[vFTr… cF/ u֚5kFͿ(1 b lX׮]:DJQblc/1cx1`6z x1`6z/1mL-1z/ #55l ???++Sz/ OՎ9RǏ2wa-[,dz 4/e6?iXuԨQ2!ـLeﻹG? Ld* *gx1hY?e0n8z?6oi^~AZ֙3g\Ϗ/ 8 b `#+VPfF/1(#^~AlJ9^~A@u޽+V$/sΕ@lll;{\Vc1Cؒ%K*++kڝ׿&$$cWʬ:&&:/h ˖-*{1=?Od$%wZŋsss11tt0Wt`3_|Ł1ԩSrr!!!z -++Y3%&Pbx{Lb,**6##WY`̳'M^ϟGb %'$߿/ ?씕wܑ۷o;99\Q|:@+t111JI-4Vm6L1c'1.\\@:~81XzWn\iZxzz|XYuV{{C~ҩǸj45[3{hpont3g~~ Qc0zK.Iu޽Av#;;0weMuusY([l),,d -۷o1CXjR ʲ˼yΝ:u%v%atrr:p͛7O>-X|g,""BjӦMfvk5u 7uVHHȞ={|G}D3zd߿qF[Ik)eAII"*k֭[7mڴF?4Jbb"?mGRR1K4w1ַo_QD]tQVΙ3g9sŻ媪*Y}Zcontk׮I 6>"1fJQݻwK>SAIɁIݽ{ԩS/^tAբR8jG[{F3{DOψ#11Ļ~zLL/NNN/_6Iaa~2etVƘśĉOS?/?c1&ӵ;vHVTT>u!((hƍ;w ߭7nƚ=\m$222z)SMk;ҿP[+Ʀټ_mhkƇrʲ(c:J)޽{/k͟?_a޼y~Qڵk/^T?ouLD${ w7RfZconAeeL(?ʛu>"GacgLRjܹStvv޿l6{l3⋹11׿;vܹSO8nbYPyG6um*Zwc:P)޽{KCrӦMsttشirڵRq7w`)_6[c 7x7>o/__RF0zlFإKƍ׭[7uO .4^yJ m!&Or)S,lذaذa?ɓ'wYx8}t묲<4MΝ \Fwuى':99'<<7o|we{y^yޙCuu;#{`u@gg_r&St3O3_~wuu}%K8`Y"t,չ %Fnŋꏲaꝱ"-\p̘1²_}Μ9&L0IcS ɑϚ5kƌCCC/7n3?\綾^2.!w{`9Hۚ:QQQAAAW\Qk;[/[l2u&31Gqfn X9M>~f,Vn`~3SkfZ|ݺu=z'y3OOOA*˲+1&ߣkA2dipVz+S斔_ǘ:3iD])]=ly0K7dr󛙺_kb]*/aY"cx2PbX[~g6]uYl`Vvu`GGH5tP! c߃mpjZs5u>cLZyxwaaa޻vjTߑ1s&St3O+7AYV╺9Kz 1#&M;c'OV|T5v矗޾}bJ~Hb3`?HSf4Yyxw ɪ6*Ƭ9S qgCSOl`|6<[|ڠ/+i1Ƭ9Kz 1#ܹ\R|qdYؙ˽ʖ292~&ǘӫ'ZL_~YPb죏>R3&[e'+ 3#+n„ 20dH(mr:S<kv%ޗ^zgs>|L->YLmLmf~c֜%b =J Ęq۷o6lXn;vkgܹ0zh[c,33W]ijժ&Ę ~m劬+V6{7, 3`?H֭[~Fa#1;,n:bY3u<7x8J KS_egσ_' Lݯrc̚D@9'NP/Z8]vٳ:9Dnj1"##"[޴i/S>~׮]ֿFc$\;6rg*kުW]]jO$Κ5ʕ+818qŋ׮]˿ %'U1b u7trrPVz{{dggscLq 2V9 H^RjkkD4s}~" 1'1mKMM@NuqFUcfZ\\lt6)\ b Zm``cl@JL7#\ b `٫2􌈈 .X1h)|ѣd6 %6dY((( .X1h)UUUӧOz gLp*1@ yܹs]m޼&ceegg` 0VAlA;v,\ b `#|c#F` 0VAlDtUcR.;`C^z%wߕ+8u|a 0VA@3());v؝;wj'33w۷+U&Ӈ .X1M!锔Քʕ+K.y`Ʃz*]&ꫯ?lb 0VA@#JlSvvݻyN1 ڈ>rʒYLp*1 11O't˗b 0VA9/^/4x~1 hNG<\ b L KNN&RRRjkky1 [ h yyyY`#i$eLp*10"!!#BNZ5Mch!5IY`Xff#qvL613wyc` 0VA ?O?tzzzyyɓ''OL5]ݽ{qǘ 0eLp*1hbM4iժU뫫gϞ=pȂN/^*}%K8998p@ 99׿͛7Vc'=;w8}JvZFӹsg371OIP///5dAĉgϞ8q<>}ڰaðayVeΘNܻwo``l}zԩwbc 0VA@Xw|)??_Y>y򤗗:/--U:_7^~e/e1c,\pWM)#GȖ+MLmgic}ĉ-X>ƚ0M 曪*(֘߉0b .X1cBʿ3i&ހz~КKNNprr9s4)^L߆ /eѷo_ &%%ɖG޾}J1u{z333%Ϻu.7c kT4u[nߣ0000''NLѝcLp*1h{u/Lp*10N1&c chWx1 [ h OY`#t:e 4ZeLp*10.::Z!Ќ/_ .X1&iիWSh^۶m+((`s֯_aͥ466g\ b ,[jչs<>NɷŘUc`,Z\J\ b a߾}qqq\MwE\ b 6lYYYI`WEEE?~g\ b ?p̙W2$苏/x0 414X1`60X1`60*10*1! 0VA ! 0VAllLp*1@HMM-..6: .X1h~Z600P1u6 %&kkk9E` 0VAZDHHcl@J3""&c|nnnGـؐ!Cd&cTUUKl`HW^y3&ce<`.6oi\ b в]p0 |OL;Kw .X1>@#Fp0 6\Cf\Lp*1M)` 0VA@C^zʕ;e6z֭g 3 SRR/rر۷o״SqݻwOݻ766VL!`N&)))﫫k_t\6\ b ڮȫW0W_}?`6Zb ,$]ڥz݌s&c-|4}[reII .X1mHbb"Nlt:Lp*1h+.^_*Avvv^^c .X1mBttN#T:<\ b ZdXrr2q2UcʶnZXXHtyyyc3 %&&'MRR# .X1,!!sN:=~4NHOOh4M7ocnS*g 0VAc#F>|ƍmcuuǏoWgϞ~===<ӿzUc`{O<)Ed?oݺuܹ%Kرc n+5j(Ûݽ{ɇgOz/`' &Mj* 6 6矗NS߹sgŃvtt>}zeeGDD8PZX -.]:aÿ~wcuuٳ  ڵk5MΝ1SNwިc߼ywurrJII!ƘUNbLҥKF3#,,Hb…cƌQŅsQGEE\re̙aso۶L={yN`3b .X1$&Mfc׷p{oooI 2$&&ƚ[dIxx_UVVo51kR0L=R3ߨczJN"ƘUNb,//o۶m$2e}Jܸqȑ#ǏWGGG}w|Tt7|cΘi``裏Vϟ?1c#5c:9VpDD/l&cLp*@h'1&222g, `ӦMcΝ;III~~~G޾}֭[~F gTo%2tE"b.]V'O>qƌ#h{dAh_c#5c:9Vpuu;#)%ƘU~b 3\ b Zˉb Lp*1 ⒒Lp*1he[n-,,O:ӧOggg3Uct:]rr2q2Ucby&c ZvTJGm۶<\ b ڊ?ôJVZZhg 0VA@۲jժsQ,N\ b ڜ,)//,))a3 ɉb޽{-Z{bLp*1h߿aÆJb uմǏ3UcΜ9@|\⋲2F2\ b l`HUcf`HUcfCUfC`0`0cc ͫ13h\ b ` QF޽MŘms_zh ٳ'`Ic Pb󣢢bϞ=^^^k֬i%7>m*޽kz` *@@X1bamڴm׮]h:wk֫WÇ+\t)44{ϟ?ԳgO… FwUXX:;;/Z0:1C( qqq;TV/z LpX1hŋWTT4 Vkooo<=z0@3fH(?N:uջvSVY(6o޼sݕl3|F4H+MhEDDTVVJ^z{{[y_ ` b )$~bbbt:ĘX\\\PPM~ҥo߾eee5ʭd]GQcvh4ƪdvvvVޗ> K޻w1 .1c?oX999]|6fbL4NM1JkIq)1c{>(1Lph1hS{5u@1z׮]xԩSsm1WTT̛7 lc w8dȐM6I>-X|Y/J)\0b ǔOv̥njg1JKKM!!^cܸqݺuS7.** ѣmprr2uQ FwcF3p+V1E1=e F;@@10b 1J @zĘ2 @1(10=e F;@=F @qA1Imȑ,_1F;@x1`6$Cvf10VA ɐ/1Olȑ#Æ {܊{=r|o9 *1@g!!!۶mS|MYx76n(mv@o;K.Fcc鳁G>ѣG?C@@ee~:S.]-͛Ξ=СCCccqg;wܷoߔ)S57,))Yzرcz-N#1 b Xcǎ9288ٳʚUWWkZfͺr<5N#1 b )ǫ?t7|+##CYmooi10VAk6_WEEEc1b `ـr7rZ@1@313h1c #55l ???++Sb @V>|cl ###00Sb @Zďcǔի1c -eÆ 2 )1WWWY(((1cRWW7hРKl@J,$$W_̀1c_R.&OyfN 11hYW^U>(ƌǥ;@1-Kw1cQ.!ʸt1122 b xpc1b AmmmVVVBBB<,@LL Œz{'@?/[lʕOu2339 ˓%<׈11Z%oݺf@Kʊ)))yGUc:k׮EDD N'dgg#*1׽{{U7~\6p7E4/D1e'xoMٶ][>T ^ ĻndRjdy/0\trv390>׼|3gΜ1> s1qDSp[d_~%?X1JHH(,,$ P'V\{n~ 1 b @#͛7CIIIn⇑ 4-SN%P-]Fb `Є\xqŊٳy$*1Mϝ;G }999Hcc"%% @}p5~S 4!ɍoZa05kf}͗ZV7÷X,G ckÆ ݻw{WZUc,((h۶m6IbLklN:9믿}z;b ck޽sOFFFaaef_cã&mP^ɿW^=vXbbb֭mn.OizU>1FUc5bĈ Vt:th߾,ȗ|ԩ:uo[NJM6[nUW?~PPϋ/x#G >\Vnժհa.\dмysoog}B /Ue˗{yy=R;e˖HYk׮)6 9|,>}Zݙ3gd<G6[9bcc+˞u1b @ HH*T>>>>&&m>I) /PXXtҖ-[KʲP#""[՘1c%$P}-((=z Sr9ǏKM2w;)5k֔:thԨQQ7nѢE`ŋ+/)oMFc۶mmu%>}'ƈ11s]wz|.+''GY޿pp:W:^+71f6FrٽɡÇOccbL̰3Ga8nܸɂyi6l ;$Hb.]xyyEEE[sv _jڵkmGSW;V#[vώF(Ӕ#G*۷̘m޽NcX1sW-4>10VA ƈ1ۍ71b `Єc'-Zď$10VAh*,XPTTD޽{HccСC6lPRRRnݺŏ$10VAh*~ӧS[W\IMM 4-˗/ͥP{ .HUc7oJ2Swcc&*+++33*%$$i1b `t;v6;%%%ccbѢE;vϟAAAJC7䐂1G CTTd/ F8=ʒ!C@@eXd#=&FDD 2#:=J)CZ1b P :To8pի9,h䰰?`H1٣N[dyР&)$$D ic1k]b`zO 8u1+11Q={r4iVVVDDl!,,! b RN!N݁1=UO;@wi<8499! b 4Q7nHIIIk &L 6^hQEEÌ$[ b @S,cÆ ho RSS1Lph1h%cLOc@c *11=͚͟?_&:~~~_|re~~~߾}_@J .1iӦY,ׇ)W6,!!An8iҤ#F0t11\cc7oޔ~Cpڵ6m0z11pۭw+gdd z1هjɿ1c qAbf1u!((h۶m]K'+6lԩS~M1cK)Ęã;crٍ/?9' b 4kvӳk׮+VJ299CA[ϟկ~_"""bǎv7ogU/}||~mwƚim֒%Kdtֱ-s{Lph1X1֦M[^rСCFxG}\)zKKK7n(bw o~_|ѥKѹBziĉoF_Szgn cƌ_8= .1h%$$H8ku&Lٳ?yfG~MD]\㣾/ԢE [|M6Ig?ݝ{skZ2L :cvw[MSy]SNuD"*1Rb3f0͎)77755|̘{kgΜqSg?tTŘݶ~ؽųg϶X, Ԇ;$ @@zL{ѣW^ݶmG5n $yϟ:uʑ_tTt/Ə3UƘk)~MnlueDcLp @Uؒ%K===u떑Qÿ́) |SOy{{~G߶m[/)SwSN!V:76ۯru(Qb\c豪_n@cLpAz Kb `z1c % @1@1$ @=F1c@#Ƙz,))靆Cf4(101 b lC0É @Ν;G[n啖6ѣGNN̈OOnݺeddpAUc?YY^b3<# WZ%muH>>>ӧe#1 b P]_WDDĉ'ʶm6I-5x_Ǐ߾};X1l &&O>ϟx K.U -ZC70 5 dgg#::ȑ#5d2K/={޽{8 *1@MgRbCU4}(1/rc1b &k֬ԩ~Nb @D9r11p4@P[N x ̘1CD~Wn"b f͛Opcǎ׆ 8=gΜ3g?1cÒa۷ogr Yf9===--1c|{ %8uԤIJJJ11!+WܱcH5WTT4ydG#*1[~}ff&3H.|,--1c攔\>p1c h9sfaa!Ge6g͚E1Y,4fjܹsX5&Y-| .1ꯍ70ePoezzz^^-ȫYff&\c14 saƣKq-Hub01c1f͚-vn-##`0|F?voM`-=G)hC^d\>5jTPPcz-& b h0RSSLݸ. 7ĘL%m J%mc6og ڈ,i{d Rb`41#ƪؚGYYYczD}3bL^d\>֭VzLUJ_~LpAct:th߾,ȗcǎmӦ̐ϟ_ͬ,_<<<ؿr#G.jժհa.\`קN|XN>ݼy3gCCCud˖-r@vb \SN֭[(ݺur / +/Xi=^57X;qFe-?cp10H]رc}ݷm۶7ZO͑2SҩgVS1ƣڧ` b ƀco)'׫0`9tvylO&|mn^-C5w9tPHO#ȾYfFkc9sZj5{lY5k,Ill={JJJd .QFixyƕecLajm ϑ}GWwGKN HY`z }BO@ l&sqƵMI LII;+oM&O[rںaÆ0ق4)o^-_~]6إK//u}իW|M i\p>W#ɣG{cdڵݻwmFFFfeeU#ƴ48cZSkknx\v=U=Ԁta 6٩R'1f2d >u\c1Irxy5Tm @1+={ĉ}}WУo;w|i21c 'v%ug01cT> 4kã?gCvDZZ1c G\رc7n$b ;._tRjÒ%Kf31c 7m4j̙3J11c@9pݳi@M/11@̙3;pYf5b b ƀd6_\"==h4c1b @Ξ=;k,z @޽{ʕ %1%$$jb˖-Wn@)@l5k'|„233VJ11c@={}wL1hr $''U1b `˗/\rmslżdffJ5Дb b NX1`6b Ny f1 b lC01_cf  rd]b `0kN999X1`63L{onnpꫯ""", 10VA V<J)IJ7>>N$C0@% ')1???Y0X1`6RQQq@ Àj@2/@4=zիWccjd Uc,$$ /@iO?t`` ]Vx1`68-++W^JUx1`68M=GOA_f@u$''p;1 cl$éڧ /@C0É @0É?/cдl c1b yyyvg999"c1b z&Gj:22%Kb `hzSf_}U@@@||<@ԖN:KzLfRb~~~`498 b ܼyـ, 2#b @j?.G}m=ի9, b v>|O:wܥKNb @!""B8Nb bl\pѿy&&p%zũ;@`>υ KB«GriY)4˗c1b[_{С,ȗ8;mڄ|P<>c>>ޭ[=r+u|3{3un˳[k3\iYy!!mzo偨קױy?͜FP_6Q7,ǯk|)}rMm~z޽vnҫw_=U>RRei]|s~Y&''))d-\ЅW$*gIq ۽rׯP5\t`(WFF׎2 {:[NL-8>"GѩS SNo\iYe" I?K/͟2`C/zo\ϼn*坉{m1lɳ?}vFTk;\}7o^>G¹sc1bL/BBO{es@wWʿ먳eٔSF*fi= N$+m6nԭw>]Y.*  ?+*.+7M]e^^1HåNbɓk׮e2 4qfr'Lj1cw)eY׫6ߧMoеh-8>"GqȖ#m֕:WluN]M =<<>^" ^g믬]Zk=RI-^*`w&b ĘIT~gLyWG*~8kvTԕOu6|r""y֊Juf}^kא +'Jz:rs"h81Vb,%%i(ټtRb @iN~U3H:Uo~M6aȐhW:Z5tVS]:׹f粬W[hܩ'r~6|@[a$w\Y+lFW1&2j{k[߆{pԻйqFM^I1c6_u~סC;Ȃ+poSQrg)Wf}a׮!wyG@AbC?Ƥ^~قF͟ݪ&֣=yyyFE_;2t|6`y Q3ZÏyc-[zMJ 㫿re9U>Rb3.Z`f1 2ѣ 0# ڶm No֝ևl6.glc1b Ku.7,'l^>/9yZj,̑S.^oocwkeee5zfT=F1FUc̹.]R_>uԀoog}H]<~#G >M6Z6l؅ e;:uСúue__߭[V~KKKǎ+ ?\V?~WeAT?0#'ߒTZ*ߪΘڂήZ{ VP۷Okk]r^M,Xu}||^|EήV~DN7W^:˩Ge˖H9]v]bΕ@#ƈ1.'w}ʗrþ}=z„ qqqǏ)Sz\U\\߾KcƌQחlaaҥK[l)R{U9nYjРAgn߿\111o{G'M\?mڴGyh4={矯rr?h.w?v՚=ٳ•6oެIw:yd,$$$F3ZXzCFVsQ|n͚5%%%5jΕ@#Ƹp6lӊ'֧Oo?3&2cVҥÇ弼<u}*2TI&e0s뗖Z/u]d,eeYGY޿ppf>;cZK'ƴv=)ȑ#+Gۣ5N΁ԍTG>3R{O#ʍT^ͩG뛖v)[ٽ1c.\|`kKJM_ju08[_51/ZHRSScbbdZ\\sΡCOe.QZZ7 >܅1[o0`\VVV?r=qDӧ?#ordye]v^gWi͛|:w~xLkkF~L1_mcN%w>ZNk#6Kk5Ell={JJJdns%1c ƪ13*mzjp1֪/Կ;cM쳞={*gKKKҥWTTԺu'6l<;.1k>sށ)))r:BV7n\dAWApB:\6hݺujMknSO5'#s6E9βtoݸ#rvܩΧ^imqiԣXvmFFFfee\I1bLYrȴijp1:"DS)T1ְ۷OR~q1c S˿T1o7ڴiߏa9y۹eK޽r\o}||[{WݻȖ#"vd‚<=:Mn}W͆v*ϪO@g7^d'NEYӪʂ<Ν5?;#$Ŀm[W_춃^ywdw 7o~:vCv#\hj|5QQʓޭ[ >j?G. X^y啳gϞ8qo߾*kb *++[h1c <ԩۿI);7W1bЅ2Mߧ\_;2ˌ&F;S֟w|RAKLp۽}o- ) Yݓ"e bLOBbk޻~ݦ´izmLB`kWN7l|7OIo>j?G. cϯTp=351geggo߾1co&Z&ᡧlWs}/P-?f})+ϯnuwo1eiѣce{I E~:vtbL'vc6BBsu0kr߿%u~p0jS\h̚|zG~$:ujxisP1SΝ;pz5$@+ƾ˪_2B9L|C[6/ןCg}a׮!wyG@Anliݝ_~ޟUڲ︣EI!Y..9(njYߝ/:~o:rǿ;JV֭GٜM6$uuN.Jsu0kr+{up<ƴvF9r`[ldV 45EEE&Mrէň1Fc\֟RuwyرcM&Q6$@biXEEł v h"8i$10VAq!ƈjz-ZTZZThfʕ+̙cX1c\4ӧO?>XۨIDATm?}t}\c b &c ټv9qS~Z~1c\4x1ƅ b @1 P+1ȅ \101Ɛ/@ub l 'b 01_cf  @01Ɛ/1b `01 cPl c1b yyyvg999"c1b z&)22R1u6o>bp@1@V{L dggsp@1@mILL4 QQQc21c bHHl@J,""bȐ!c1b P*؎ 8pc1b P'aaa@Cxxc b Mٳ'1c nCf@rNb @j۶m2(++P1c7qFRRҲeRSS+**8 b M%VTTT^^^PP@1c1c .1z @Mc b nJ1c uSb1Œ)7  ,1zh4k999dccF%VeY,UV[fAAA1 b 1+w֍10VA\Yb=VVV`J X1/!!f-[p10VA[Jlƌf%V\\<{'O;6 @pFEEqQߦLpA4SJb(XjՃ>h0dC .1vE2Hz X1pM锘Jy,..10VA\c5ɔ.7  1gK *1@M{1c 1J @(1c1b @1{ҥK11pwl1c dHcl ^~A ɐ @Ucf`@2/@ @Եrـwb @\#===//l '''33Cb @\d2EFF='[,11j)x11-!**JzLfRb`498 b X,1 HEDD 2#b @jСCe^b @jמ={:$ ,,Sw1cwWbl@Ibbc={ b MxlSw1cRN;@1[m۶Mfeee c1b &7nHJJZlYjjjEE@Ċ 1c1b Sc b %F1c)1z @Mc b nJz=fX233dcc%T^{-222'' b `ԨĪ1Ųjժ޽{L"((10VA\SbZ=f4_y啀@nݺQb *1+K̺ʔ‚:L %$$̲e:n@@'%6c \+..={ɓ'ǎNccc8` 0VA\cJY,eSVz !bbb10 .1vE2Hz Lp*1kzLTeqqqUc#%fd2 9` 0VAcΖ`Դ(10 1J Lp*1{\ b `.]D .X1pwl\ b l`H cl`HUcfCUfC`0`0ccl 0 gw6!\ b z&)22R1u6o>bp`Ԋhǔ@vvv```||<Lp*1@mILL4 QQQc2\ b P[,XLL""" ‘\ b P*؎ 8p0 kϞ=Ʃ;`Cxxc` 0VA$11Q={r0 nCfLp*1[)` 0VAj۶m2(++P .X1p7n$%%-[,55&cM%VTTT^^^PP@ .X1pk)10 .1z Lp*1@ݔ=&cnJ\ b P7%VX,rC2UcjS=f4_{Ȝ2UcX=fXVZջwoIQb` 0VA\SbZ=f4_y啀@nݺQb` 0VA\Yb=VVV`J Lp*1} 5lٲ@>1:&%6c \+..={ɓ'ǎNvccc8@ǔX,ʦ***VZ Cc@lJŋ7ʤ"##1 @\c:%R(1pA9RbL&Szzܐ 1@5{c 51J 1cw%1{b =F1b t%1{Lfc _f^~A /10^~b x1`6Եrـw1kٝ dffr1p=:طo\oX8D@Ԋhǔ@vvv```||<1cڒh0d6 %/ F1@mX,c111c22dG1c5tPvmW1b P'aaa1cwWbl1c7ILLTbgϞ1c7QN!N1[)@Pܺuk۶m .LCc4a $''s(s.]4''119g͚c\FjÆ ƪtӦM3gΔ*u/k1b @}'yMfrLgFӉw @'Nx9@#bŊ[*c GEEE'Oƍ?3^b ƍ'NWmܹ.\ @GRSSD33g @/N ӟྐྵpϟ}ٹs1c 4S%%%*˥ǏСCeA+CBB>Ѽysݤ*[[|yxx<~{MqȑÇiӦUVÆ pr+W^x>88xͬ}SN zQQQзoLKٲeKdd<]mr yeA_d`#i)U^#DH˖-/_\y#G#U7oOΒlٳꦼl6m$`0(˲Ո.](o)u[|Uzg|}}Ҭ#{{i탬 :PUak:r@ԍ cǎ#ЪUzJ͆K}NyOʖOp9c:*??_/eW\;OoZԑ11QŘhl׮,뒒eYٶLeݦM& @udiwb׮]ѲM7ZhawjĘDH<<<7o.}gLZ;{8PR{bi탬_nʑܹs&LxG|gLL6??1c3V' 7QȓUc1b @D Zo 8pƌʇs'˃ rpMYXlY~~k 5U#Ž?aKQy:t'T>̪ua9r@إKZl,13c#GDΉ'59r>3V'ԑ}QPKYs111Ęl6I٣|뭷R?=qDjjٲ̪Tc̑,wi&t…|ᇕ8kO>),,!EzJY۪Z'QjD}{2eJQQQnn\s11ǘ[#G'(W~!jܸqo=*7Qܻw,'x2aÆ;Og_;v$bPP… cׯ_ v+**jݺu#3]Yvme;YYYZgS ~OUiάZ@<ƪ< /͛7Wd9y򤺚0<<\}:>g-99uU^1U'K}5._c1b @Î1(Ϭ'N\rEzO> n2eJ;`mΜ91uLdTVwѣGݻwl6a 9ͫ@@1 À1k29ÇoڴW?1 0=X,1u/11Q=Foټ1dZx13T)Xnhu @/-[sN@VPPb1b @xb2h|fsBB1cꝊ9s|YҥK'Opu1b @=_p{1ʚ:u*1ݭ[> 61sέ\211q޽1 ѣG-Z) p%3g·~h2x)1cf^~A / _f^~A /1:gX.\XQQ/1̙3sssSRR1 clp_)ܹsx1`6x1`61 cl/@nJ/1]|9++֭[.,l933Snȓ^~A IѣGbbb^^J̩3oCNNOx1`6&dAF`\\ܦMQlUcիWPbhJEDDټQV1孰`/!!!x1`6&cAAAoܹshhFlvYXYYVĞrGx1~N+{Èu&''s4000<<|Μ9'O={lv͘1̙3 ]v֭1I 9rΝwKJJcJY,eS7od~$Ʋ>裿o;v5kC=$UvIc5~wy'::zӦM:^8b@ظq{*W.**zǓ8n@:u /ڻ0?J3B4ABR B.&9m" AC487W==ox]9.x'LoO&noo@? *zs86󗗗xr-՛D#Tü*u CZd;o9tB^<@HVh% ?\)6w%"V< .y-MT*M\|ed;??&1IDATx^ XV5AEqfRff)ZݴTE3EDRyATDӔA)sV<9<z|Y{{n3*-xMU 4Th*:򚪴tѢE ,+ Z$kŋ_p4cT*զM30j4?|}}30T@%Kga"ThhҒ[L˃t.]^4㋝v&HT?lq~<ӥEi<^KcՀZHTW|Քvx%dTݳ\)Thqk+p`TGqF1[ Y%ThqkO?%ATAEU.|" 4iĚJkL~WF$رEgϞ Thj^z^ *}g>_t{I=}=C'{ط]oq rMEEE999,?j@Sƪv9HzO]S.߳gTh*,US}1Of})%ųH}RMax!Uxe[[VZ 0 ###11_~K I,Thj!/YɟSJv/ %3۔o;rTW^>XSEFFvjjjN%,ͭ{*ĉ۶mvq9booOjȐ!$خP 4VMիO9XIlNFu>ӗO0bM%ؘ$ 777g,>66[vcЮ~;4ij'YtQo(Ψ?'}*99<&"ȓ/S;wN{![ 4VMիoou&uTDqFuy>èT䉏OR4[ 4VMPTS|8aĺ(..N|722 _T49kQ?= $WHY%vl8r#EYYYtA)Qss󐐐i]a GcT{+瞶O2jղeKG; {ESqۻK.)xh*4X5Uy3NܿIƃA 5j@#XS#G;F$N3cf͚'r@SƭJJJy 4_|h}wb h*4Ҹ5Uy5kր (f[.4hEBPi4i7t_zqK@:*Ma?EqsxفJ @S&+gwwW_}gҥ K!?WHTƁ@#T@h*4M@#T@h*4"""fڵkЈY4@Sh*4@Sh*4ڱcG-VzYڨo Th4UQQQNNNYY_j$v-)#%%~ȑBBɓ'k$@#yTSS< 22SSSO DII qDHNN愍yշYÜԁ̜9'%%EgHsl;x(MFtTقGwӉ=/_nӦMxxxnn~ll,9) >>^6RqujDҽ*N$Cb}| l2@Sm5/44T8;;kĈ-d䇑QXXPEP]}).]ԣGI&WLJI=zo؁@#jwiYYY+V^Sm߾]__gϞ>h LOOVWnuޥ^TTDENNNyyy,r͚5<(.(@23eh*4r޾}{ccW_}U.u5Ć%y/{&bbbbgg:!#::zΝzzzT*=zx-ٜbHۋRF4ASXzk OH44U-rԩqޚ:/k @ShZgݺ5ڭ^/k@Sh ''gw_;t]4p4U}rk,XAS4i|`ћ:a^ė5<Ј(''G m4ï.rۏ9RSXXhcc3yd9,$ŧYYY|YC 蠩ԠV׍TSoII spw@@#UTBQMi*{HkT*GGG;;; 19# =y.\-{G=MFؽBFVT.]ѣǤI+_gnnlbż9sgEG: TESWмSN7oZb i<."*rrrck֬!EwCwlݛ9Vxx|-$X@S*j+W|۷766~WRWsʼn!#::zΝzzzT*=zmqqع| |_XAS4czލ3&ThpʕeD,R̝}9>v@#TCMK+""@#T !~)Gbܦ>|Q4U#--mZV54j\<(bށ9sfmGThsQ7i3㓓7yx̬eH#TjV"JII)--9rfs6@ㄆϫX@#XS< 224Ojj):$8*JOO766 dyLҵkׂ9,YY3w6*MFtTLlݺg544ׯߑ#GXQppp>}ٶmq*[lmժՀ222hOuiMKKXZZ2 QٳbҥK233cbbٜ e͹q*:t\:MFtT{III8p!CXQHHHlllvv6k*@꒸Z$خtss޽J:qĶm۬]\\IQQ_lСrvvr6|N:1meGz]:XAS5ڵk*߰a ʳ*$XѺuh7..Nإ %%%Ƥ$l;))y#D8رc-[֭˗~ly]ͅ0ThDgM+޽pmڵ¢uFFFSr9.Crr2X|YÜ9$3g$!G JٜOOYE|4YS w 0550aBBBBZZڪU5TMLLxA0JKKYzd4i-}}}Jhgg'}vK6gcH߅^=_phXASTw aqy骩󍌌˜_WdMuҥ=zL4z#1roo+>cɓ' H h*9;;?~|Æ VVVj*vww'mª<."*rrrfw'k֬144' Ԁ.]988WAS{&ycbbbgg Đ?sNRT*=zXflgq۵{}gN_Ɗ*0ThDM11[~ZRNjse4UŋyƆ*+?:ukJ4U3%.nlEGHYIՑ#Gh*4M|ϟ?-!$ _hKh*4Mٳg/c5u֭ @S v-\8wӦ$$nuIh*4Ҡ5/4g}-++{P<.(Wx{JJJiiȑ#<666'O֘s紋yHVU.c5ҤܹßUT,[MtUD,hÆ bjaHIjj)=na⒒*JOO766 dyLҵkׂ9˗/~rv*3kÇ4*6m" ZZZ2= oٲֶUV HLLׯҿiii,_M) ZbT"7駟 Ě*88O>m۶7n6]-WnZ#93^tYfffLL^||< Ǵse*JR,TAS\ pss޽;'Nl۶E7oO=:k*?lؘtnnn.& v׭[Gqqq®~IIɅ  H0?1m4-l[i$T% ebb"NJJnK s;ve˖ݺu|ϐY̙9=^vH/YNO1:k'OdN'??_=wxUbb"mdee1?E&!AFISQvEFF3ڵ¢u֬V^^PKcW7g9$3g$!G ٜ5Ȓ%Kܹjj;ws2Vk/_I@DgM%ܨP||`n/ꪩoӳo߾۷ogSS &$$$ZJȬSWֈN{HkT*GGG;;;[9k]Ne{zN?y*]/84wݻ>khT]S=Z /Jѣێk!dTK]vSNe ޽6YytTֈQ.]DaҤI/377bds ޽zUܿ`Uզ9sD'͎k*ݝ233#""<<4PDMؼ9rk׳}'',|w7nChZ@SETDAA3ۧ.X3cϞ=|(Mh*Nu&???UžMsx4 tTnoQR򔔔R{{#G y mll&O1'".͙3+y~r0,cFS KFFFR7RSSOq疔PQzzq`` 3eʔ]h ۷ou/+?*(S9?{y:r @cFMWr<<<~iq~Z@l۶mРAZz/^,󳱱100tuueGd%w.]jffTDiiɼyv/*QX 4)tT8q$JRR 8r߿J֭ƍHY\Eu]RQ]...H:*W?k ~СrvvrEg޽}<גtAVeT(>FJVx1=DL6_~lTڣG'psssM y# 4aii)8vX˖-I]|Ygl\rk׮&L -M?EtT VSvxz>}QQ]M?iӆdRIII.];qpVVE'''sȈƀ*..ǹs83^|QFZG/??(,,L( YFS]tڝ4iRy}||ٜ@ӧ'88W%={F~Aڵ{ܹ h6:h*O :sX=zG߹sd*'ܼysvvv`` +VRwww6!!!:SqR/**"'''kk뼼<fCCÃJiy"TӧOF4hPLL _ׯ_[dަ+ŲY~] 4ݥKRP-^_$$0ujO߻wo7&&&vvv⊲W99IQ8xѶłG6'PT ҥ/Ҕ)SJKK28|xy䩽^^n|4 4ݻw;wأQzMEܼyLJ={2y G.*MW/\WQ]Mu̙KTMq;;e˖Z~kڹeUIQ_9*!Q]ME©C ".]t-L;n߾bŢK;-VVr'WaP]M0k*ƍ~믿^^233<=+U䄮^bE͛7 7T@4QQQի|ܹsgժ填޹#VV.HJJ+P@SEtT 77_ڵk|.:9D"ߵq\,c5zՃ.\u)))#GLTzyMOKɪ⒌ DGGT '<<ZȒ 6x񢁁5kӧ uH#s;ve˖ݺu|ϐ̩"3p#22>7nC:s _ZF[M)### 1d׮][fUi#;;[UB!~?D8`̙$ )))b?C6g3pqqquu /Wˡ?q!X 5j*(--bMMM'Lj*8qBTTKiCS?ZԊ$XBfNh?/T͛_,NY]xxl j|##0/F*Kv-5o<p!rƌKiCSVS]tG&M*|џ#S# 3pׯգ.Sqlg܏|3X:@[ME LOO'E!ʒ'O9;;?~|Æ VVVB~裏T*R\;w"Z{{zQQ999Y[[5k+5JJJz5Giyw^zslzԧׯI)}}nݺB4jVS/5kiv7ofIiTY8X4hԸ***ٳgII _PӔznʪ[sBBu_ ݁Ը"f̘QK/w9sf\.:"UV84{޽{:M_ (Rɓo jW|l ʪXm7B4-/`|ҥKBB? (R?~mxomo_ Je_ײnu밌 C,nV>rũRRRJKKG)),,zoiC NӍY)Stڵ@cNPK꯿zꩧHÿz{Ow@:ܫ8Ï-+2V&4 4 YU7蠩h}mvܸqBiQQSH5*<<\ZW نٳbҥK233cbbٜ4g}NywN:])>`ܭ[1M5CcNASM6]vk֬޻w/%q, x*ի?9***+++00SNB]\!ʆ:x`+++gggX!'=MEѣG:$=J vx{{W 4 MTU*//UVAAA_@,ٰa ɛ/6O.MJJ޽< 1KKKqcZl٭[˗/ ٜ4۷zb}V K;-nY껕+P{@S`ĚVS!Ցdeɮ],,,ZnmddDi#;;[UB!LNNN(q̙3)))b?C6'=MELhw˽:zlV_i Ȫڤ5mN0UVN8!DT*%!ti-}}}jΎBlNPꫯxo}pUKCW/sWt_ SS)Y4UžC 03kݮ]7KjgU=JYҢc;--;߼uRZTv۷++?*-5Uj ȪZC[MU{vBt=j޼y,… B34҆p=&MT^?sss.F6'UMug}/'N:|Tt!WeݻOTw\FU|ssTk-&yj?:E3{ϖaMd5UdU*|G BZɓzzzǏ߰a>*RQΝ;Si=OŽKI5k>}zP~zόg<6y۷oG'MK"G:żÇ@)=y:u,ߙbτ o MkeՙDVS1ga!,Ѻ ժP8 Ugٝ:> 1X.jy蠩JKK}||HRʒ@ΌH; v>3Rh#G "5W 1'GttΝ;T*=zmqq j[S̙?UCŋOHg90W#F wY%mGIi'scE1EK_ye믿"ĿΨ4OXhaX曯=U>0M5luϴx {HickMĢΝ-Q0XOB,,̅&HW=K#'TG!?~*$-񏋏 u ly 4U1{lʼ7c wnڠso>%Ȋ?O?,51yIiF*h’?Q__OĤ F_~$V˪_.:Ҳe˓0k5AR !MWeWݻ̬HiX &1&v?nHJĘNJhy4Ėqy~߷o5Ci^s~4վ}BCC322?`bb2w\>7c~yo\/iiۥ}wxyy|Mr`0X33wY%m8 V {{[smژ5Ѷ3`dJi/t)"qrR\/4Hu3g]b3R8Z qשd3u*j{ܨ7Mկ_?Rݺu#AUZZlw޵j(`ӫV- ]e Lׯ6Jszzz?ƽ=̯+&Ts;vģv _(MBS{&N|wFӹɭZ+93!,6T8Z q{;瞧fx/Ki=knԏ:$yo#/CGΝ?믿55!`&fy=*h4G=6vETT}^9=m ޸RZQyIPR礹[oZfB9s=տ'̬q t[J2^4YQwJ @C~lׯwqq gmJ:01ꉥK}CitI"݀Ջ}ѸR}%Ú4YQ+JEEE999eeebgmBrۏ9RSXXhcc3yd9*uc_ǎ{6[ns$?{yy;v)4P?Դ֭M~)5$RJLLg^J09[Lu aOv rՁKJJ(==8002eJ׮] 4l 9sNtbv ,Ѱtlɶm986:Dzg xi! ?Tog'bbF6h߾݅{#eU&25j9ɎiiM2i:8We咓q&$#C@$rW}cf:&v5LcΊT\]HV(HY)uU!mk/.- 5yx|XQQ t``׬YS 80!!ȑ# BZ:88HbXSiͭ{*ĉ47vqq&EVUVV6t[YY9;; 9*uc{饗N<{ ͛k[~۷ܹs|eY7͏<򈬟4?Wmv_|^~.kZ69߲eKYjݹCrޫ>>'q==?0?p0Z64$Nį"Y_ҽ Z\8tPٽt3myD#m&M׵̊*=hСI`u3-7`fnu64tTlwݺu'볋HC4*,,466&%$ 777gIII$50aii)8vM7uve!R7?>`׮]Q{O}]R.ݺ,t#]I8́WS?`pTtxc]/3oJ9i㥗 KMrfIH=J߸ŪOF׮Cأ6>`j"%0RJ5ӂCRg#ufJ2L)Cu94:]8K. QFgM%5ǩ j*i#>>^?R!4vC i-}}}Jhgg'}vK6gSn~l ,罍{̙Aw-7Q?8ݡCٳ䊄7;̬]qT֣vsviYQ5'v ETcNNluX.g?p_J AzS4"9cz"G:IRCr-T0%aoFl7W35mlpSSnW|ssT4V]DqSz޿qt!^.:]Uot@)uO0S4l`4U6˪ZTqqq-Dם:i|##0!N5ҥK=z4iRy}||ٜM̞=6Z^-޲5Dϫ',񍌌\ȝ+R? H  dڵ#8Nahhй3}}>RdO춲>hܬY 4Uz뭑G۲ev?spg xc8ͥVTJ56WmH\ڢRE#^j w0ܜj6sqIڍ'hV7Z};>LOpd[5q]'ܦiV5:w\G-Mya =h.7oGVQls X z{_KYU+ԵL߻wo7&&&vvv0+N YÜ;wӣRqѣmmmlΦJHsKlڼR+UcQ[֭M.Q^H8icGpi矿/($H;&-pqϞO?C۷S[ME2ߣ41O2pr<ܻwwqZ'TԤGKe"j%{شHSەepuazӰWxQӆhO_]qc@.¯RY꪿U23k'1R/*ꆸKba aZ~-2~A};\3h_K[ϵ6cS4lqi4UֲJMuc1bm*>żii HdS#]DK˾azpsDOk SCaWH!'ZԷMijhhK*\:sVu*ֽ1c^{d&iq'1^ͼ={>=:,~SF&T>cҩ8LA!}T_Sw ^$tB:*0)M/Դ֭MS)5Mmc V7rs ̭AS(ʞ=466߿c;Y_+YN6l2O+ԨeKܹVTj*5<Fd5שsP\4.--9rT5l\͏֜~"3ϸ\ڸt+ Ǐn_~rKbBW+N殘ʗA^*Ž6m^A݅=K9]kP(Y7TMMϝmYQ٘*6b`9Rl=zxOOX}Zlgd(a&;;nܸK.'))ğ>}Zly)sN !/ ~T܍͛/TSo ߾#e+vyӒK2&Oo߾O&GiKzmiĉRdF*k%j4ʠo([Lj5q~i^]R4]]HV(tCV}iiJ]m e_{ec|ڨwr~+oI?O>Ӈ4 5ӝqJpvm|ic:չԁ0+m`0X3c_tiI#lCxϿl](Lxq`̤)[8I?im۶ 4UV=ŋ>}mvܸq*6m" ZZZ2"oݺgT;r+j0xH}:tIchhh MJ@VlҥK233cbb[ds6.K-/{Ih{{{ss6mژRݻw9Q@FFFݓNdKgG({|rLg„7ȤU~m3곉4%Wε̮lPu`<_|p"w>h@N:_&{kbKh 5448pp kFg}Z{Gy?W( lC\cW>)yѷFNjh̘qj A>mxpM-hСʧzԯ}~>쉪-rU?JldsT`^6teׯp-N{aV:`fn8C77 þ_J+|jujUA/lO_R\5鬩uqƬRV+Wd!!!֫W/vPo߾A $[Ie8q8f{y Š>x&{NڱcG7W-H\AUVV6t[YY9;; 9ӥ MUQQ!pשHwܙ6if?޼yvPH* i7//ANTpS5+ZȮYI3>CrEM6\&i8{GVSqῂĭH3WTƏO]▿P5AIʏ^%X\֪!Ȭ.um{]m`oi(晞..Bb믿" V{4v[vq6_ -NTlcF #͓/PWD;i-Vgìtɤ1Đ款)ZmT֔zRԥG)5MmcW8JשVPXNM<3:}|СHIΚ6mZ=D!aRjaE4566NHH6 ޾};]v>]PVŋ}WB|RR85FMrcǎlْD˗~lƅt%޻wOa_u%[[ӧ̙3䏋ra+rx7&L@'ߕkggQT;mY/[o]vAjFQeP>rr[Ì[ܝMMMh&ye#\ϫdVE!Ar?’011&i'9g'K[lu/-,"15s:I"ǍW_MEd} {J ]BYĤ FBa !Gc{|Nvrj 5ƝFF&:c0hгeuԵ^~uS џje_NVgìtufU N iڳ*4!X5rtjԸ~jm'W#U٣͎_+U(V'^ڠ38.Z֩SMztT4fk7Z/sR;wrseTJȎlwg\[OѴ좐=BE=(gBRsbb4G܊ϫSU~~|c'*R2曯 tIV{l6is k4 {eUA ljLCJJM8ԈU5v[n8GUh`jV?yj*/;;[&Ljժ ݱ(--eD4oZJNZ*Nx$L@6gBzVVVw{IfO=m,g7ygJ4c7;)e{="h0 jer.+SXVV'M^Ժ&Є|R#;]u (bJ⹊BM;[T146bң'W'F>ǟ&`ң <½wn!4k󍌌x5*..ϝ;'YYFS]t>I+_gnnl\HOZ.@Sqm17coWBvڜIV+2cG"V6PfV}BN|'H)MۧZ(>7k֧®=\n3{'PqEن-x[^ӧH3{ɿ3.l鏰Kٻ_{ش6و/Ioi)W!?w%%߿p47K~_HLRXE̠A* ]-ΆY5d4}Ł9tXB5 ;&  W6 Ce35S/O;1eehVO0YS=䓛7o&HhŊTtI===1Ǐ߰aU ME LOO7!h;(( M`SLԩ"WB ꪉ%]SjH XP,ĸ\VSv& o:0|?y{=of9ˍ$++祛 :I.2՗_~)AJܹω#@{#F8>'4ttʪb;uNFGGu׈H'jvSO=F[zHGWPT._?/QI' pCفpN:v7dvpѢBR_o&͙_dXbBE_sMLҷs[ڿfthڄ8wT*fxc1qsnEhfڟY6A/V:^7_(}E٢9([޲YX̹sKǁ՛.U7耛9m+Wyy"=zg#GL6mZXޔ"##U.JL8rsXXXjj*WTd4İalѱrJwwwN7{le $XHaaaII HZ+l,|leP@/ĵC#WˠR\CDew{SZ2HJ(dOQPP:1+e梢9([lܲYX̼Aa{q`}M!ptTW}J#PGLL#} }y$ "O/ jfm ϩ._9zBQ}RcA*ܲYT̼KyWJ~ru(ңLoO#WfHQP,-V3+kֹ߽qZTy_>\T4|fa1"XKU+WG\y!S"e[)_Jp9OWy&T_>!6bPz܇*l3/^:(m6p? \*{A^ߟGH e/_ R_+W~!S"e_ [zH/">zTHv AAAӟĵCrPPxڟYڳo]0C4Ud*V3D l2,s *݄jl!`;(((CXϬu2 CTgL~n b4̙kZZZ,Y"he.(}~~+M6L 2 G@o6d3u SN󆈁TP(:.==-ݹsV:;;X3S]uO5/\+IWWI} =&d*saEs*1™=HXn7pEi&^խ)EQhiaAdj*:ˏdD1YEh4nٲ"MYyM2ƍlmrrkmmmQQ]ii)7gڵz: HKKX6??_ܽ{IG5|vC;v۶m 92uT$$$ҥK[uAN:7!#/ˆϩTfW.9;}6\NYQ\ݨ뿯6WwJNN}0c4WG7yXS~F~RrD-aQ?8}(JdzbX8훟}>tEڛziaAZ|W^^^]]]JJ +K2՚5kFU__1۳fzgz4uwwGEEEDDPdҥl>Ç߹s'[9#^6=ljlC^!}[:^ld?]]ewѣF.^;T1b }G=~Дsvb2)>+J vvvH㳇 ^ڽBs|缱p vRKZ:>j}=vЮA2kO;_D:a…!hbt̅GUt6*[Z.v‘gNcNs Dy4yR(0/{de8[4UgN`㎎sCJKk|?>Ҹ?b|Xa'ӡ{R S*v}CT?K'^v7D(->|L%ݜcm(E,[NGo1''+˿~nt>,u>ZVs^/|vٷ洏fS`NۜbӼ!²LU__k2Uqq17Y^}U77gϊRҰ/(( ƜL%=\\\.\XVVVUUg>e˖=zܹ]YidK&t8>~>sq-i?vvvio/ʶK ;S%tbԠ@ /!{vHg~T 6v]/g^{m>{6Ww]ႝcxЉ)BT+ru:'n~1[Gyc1·fz/?f#8sML\S=5S^a)#&Agg0VcfR:|A^E^S~6k#j(qQiJEȳt2wW^|7uxfm*/KLt|#=ocirgf f*ݿL^os*'%%z -h~$$$bUkkkPPŋM=lG?v\JQywbdětwSvϩd{08KGTp&¢tec%dzһ4PHi\36g!yGs>QTW}j007Szn=f̘Cեyzzʷ{xx+555~~~toZC ;;[_wwwOت+VRcYYYΟ?-[啟O|&(i ͳΎ""?77LJWTTl߾buu z={ЖJDRj:U3;ݻZ'ZbjN6/NrY1\:ʶuTL޿=&y~Q{^'ln}ɥpSQU> ٳg뿾0v옽~.ʓO1}x3(yݔ߽.JڃqVj/ZTRQ:ñE 0>>ߢ`C}y׌4hC>/?[>MXs>EgECh d+WRtgRUG(Ef֬Y^?Fcbb"%z R>y7YŪjIkGLLLpp0MHHROMM͔3==)22x̙3]\\iӦQ^J؏t*E(jF~a_jܸqZ?L3tV/(*b1ce wPxb'nx=%vm'unhFFEt9m(sS^(xySӧP'Dc'=jМkO=軎Js45ws||0.H$l&ȫ^_oO҃qVi/Z0;eo/{e㋔Wiq@a?j*LmGoE^70436Vg<>Zxa:yC2(/[WWŵj߽IW Dy}d_J\܂]^d'&H,]{oJ5uNd*Pd_搐q-y{{KSPPO:yCG3<!33՘1eJWoˇoo"-T`&d*Pd_WΞ=[\ gg* 4ty_X>zWĵPPPD:yCTO=… o޼ꥷ[]paܹFbwN/,,nttH$Inlzl4̙iiiZdfd_Kk SuN{>?YWG߶UΝܲe zjI5­TS=pgUVV.ZU]]Y?qqq~~~wӚFoq-d*QiaAڱcرcm*Yl8rԩSCBB.]V޽{ҤIT9jԨˆ mmm&H3c TҭJi& 'Prr+eˢ"Iiild_6:IIIZ?T(((bӼLUSSC筷ޢ p̙qDdd'E sn߾;Aky8A(5Ѷ̦+y|֭[k6ӧ{QDv}MG:7 Q֭t41 gd&dYf׋kf*ёlٲѣGϝ;UcqqqYp!%={!88HQMZZZjjj _U&x,޳ ҹ Qֲ/(( юӚ/xHXHcɓ'E}}=ߺu+F_?~~ۥ-[z;)..~m$Qgξ+m,xbSύhwEmd&+d7nt*E KJJ(Za㘘`=d&+d*;w<U&%%Т^_~=;?ÇO6!!!HVUUl֮]KS'UZZzA %00^6$%%ˋwE^~hڜiGVZAi6̴TlPrrkmmmQQ]iilL7sLB gɓ' :s 6PbrڵG&$$O>ҥKf̘A-FFFK.캺TJ\;wdDJAE!6>stt,,,d-)߿d˗/Tv+id'&$Է񉏏V"e**9555'NxLE-P۷Mظ-}WW׭[222X=Yf Z TB(&Qe{{;owװMΝ333]]][[["pGV^'_QQA;o{+zիWmmmڄl13S矋݋r)Sm ^в8S544ӧO7qpۛ7o )PӋ:VO>L5,$HS92UAAͽa#FEER쎘i{+iս(D [`0>-bNOczDEE}G}B(^08SPii)?gF/Z,};v,{-m AGo>C63Gd_Jh„W1}ZD=S&NH7_!!!|xCi޽Iݿ 6OLjGҥKj~ /z=KMfȾG邯7?lV:J/6-11QFOd*`5}Ѽyiԣ$"".^{"7nyJDٛ6mme˼҄fvjll]in>^UUEDRرcO=#ƌs!6툧'Jt/u%+VGoeeeQ;tsA5f*S?'%U_sssgϞs1HJJ ^ sXXXjjlcLٙ{`6o=l0y毫kHH&tD5FfB5ΝmLZ#;::V\(K.WD6*,,,))111'nii~<%4o޼'OZj/>}łLC0S)}<%m۶||;wīETg*>{L(S' `(@Eo9O ͛7O)Ud*WPP0n8oo_|qٲe?Osы/޺uK*G2(zw(>Q(Ebs熆R֢(n:tH)U*dotn{ )ŭo۸ A, :=_OhSUy,l{/R&W >gOKKK@@%K4QT*+q#JxbNb/\pSN?~FSSLuׁqee%1ݻ\WWt鬟8??;wh)L0tXI|AAAhhC~~>U&%%Т^_~=;?ÇO6!!!HVUUc֮]KS'UZZozAyɱ׻w4iQϟLvȑ#SNh/.]VNU8UVyxxPډ̴TlS l$99յBiil"T,T'OtNg6l @rڵG&$$O>Gxx83fPbҥK===RSS)qܹ}*51Wm[[(..6&LnbNO:e0hh*٩ |r///)))xQOG&UwwwTTTDDO||xB"T,T bKKN۷Mؘb [ɡǏE{{[n988Pad͚5iky9AR[rΔ:L}کcǎ^ J+i&MMM\z5o_QQAoT:wW^HRl"T,T lӢsz'''innoܸ!͛E TՓÇS ~se'BBB*++ن'N1b|) Z6P$ꑆS&\݋Bu( S @řٌšR~NFic<_4S3(Ō˗/|+e…eeeUUU{NI*l2U}}=__Jt %al"T{oobSSAAAmn,;y0lݺUSg5# 'v1\xFnnn6} Sh{"7nuJDٛ6mme˼)󤥥 QQWWGvȻ2PΎbLMMMnnR2Yҿp/d*l7f̘CQKMOOO\O%z{{;Zb/r#ʢDwy*p_L8NٝRSSed΄~/uyfooaÆnBԏSddݻU2E;%}GGʕ+u:ٳRla"%%% iqLLLpp[} Sh SPL !ShBET@2&d*PLid*sns"Tu4ܔܜ+L .7 F1<<|ΜAbIDAT9%Kh9!Sh{>h3S'8pWVVSogvuuѪjNsf2& 2 T@z~,c~appçMvZXKfk׮MKK#3^}PUVyxxPډ>cWlS l$99յήT@A @řj 6RʺvѣG}}}O^VVvҥp W3f̠X###Y˥KzzzեRڹs'[%lUjb˽X)))xQOG&UwwwTTTDDO||xB2&3Ң(2sss6...f999xqhoou- VO֬YCs(vռ B%Hz^իmmmzFA @ř->}Z17qpۛ7o )PӋ:VO>L5,Hs9R$ۭF:. T%ln:{4 gd䐩4YX*--1ƢD-Tf2U}}[3#t\^?Zaaa/NA @S3Uss޽{m 33USSAAAmn}/337?LJs^xFnnn6}rTƍ)?P"޴ilcLE-[啟___&GE]]5۵kWcc#Aƌs!ꄺ#T{Ӫ+V޾}ʢDwy*BT+S'Rpvv KMMm:;;y͛ f#9vtt\]͞=[x/u?RΎ  % LɂLC 2&d*PL !ShBET@2&d*PLiPg*?.͹@-`R!`0s,YDρLJbQ=pmUYYkM4dEu:]zz:'..Ν;}d*Md* BCC2))) zY2&3ɓS0ذaC`` k׮Q,MHH6>}zYY٥K(P1c*Z.]3//.55Ν;*2RePF<%E HΝe'2z:2MW*m/^Adq[ZZt:E&`߾}nnnl1''?nݺɚ5k(ư>(#݊ $S!!!5k,@PPIk,Jh4Ox#; V, ^jkkKMX9p4YӧEɉjy7nypUyy9cT4BH"9r$$ rK4X%\݋Bu(@AX9p4YX*--1Ƣ0-T2ҭL=WmQˣ$l\__[ 3 ie-{{谰0[}d*M}TNNN{7Tmc_#Ǐ|eQ ̙3ΝWƔx 9cJjmmq/^lџ[bbl @S3ٸq#S"޴ilcLE-[啟___&ED]]5۵kWcc#JePF)x\reذa%%%5~':DHOO=2\O%z{{;Zb۷Yˬ,GGK7W!ShLERRR&NHgaaaU2UgggBB:ټy7e!ԕeT̙3)> kX#GL6✏ORRXGCHvRb6 >LρLɂL ܲeF3 eTJjllLNNtV 2 d*MC%SQpؽ{J24!S"d*M}Tfރ߿\gff6T܄Q Eׯ_Ջ(m+0)`09s~ZZZ,Y LTf!m+++yCzhUuuNKOOgݹsGA @sժU333)))) Aׯ_ ӽ٦ow4iQϟ/z.4ICڑ#GNJ=\tIڬm޼ySLH]lSZ[[[TTdggWZZ*j LɂL|r// W<$lذ!00ڵkGMHH`[  ##ZN0!66V4S:u`0L>}ƌfoߞ5k3<$AlQߪ;***""'>>^ZA @#^zjZZZt:]YY_o>777ZTrEnn3VJ)H;az*ѽiՊ+|}}WIVVK7WtBdAXr%N7{lѽSRR&NHY9,,,55Ջb͛ f{/tqrrܽ{(477!S'jԨ%m&?5.NC6FINNvuu-**JKKE d|4(ھ[ Μ9G׮]xPQQPjTp9SoBEa`0L>}ƌٚ5kFU__O]Q4J͐pXeFFFqq1uBcM0!66VR)i{)UwwwTTTDDExj>,d*Mf*|׮]^())IӍ1駟^jWi\ggg-&JJi(% 'W: zaWRokk3}>XTTBCCG."ʖ~X=K7n.޺u c":o>?~mV/7N8I1ɉ沍2Ni+O>]݋Bu(4T Bdn"˖-=zܹsY%RyyyXRQTBo%he…eeeUUU{ E).. dwJt^?Zaaa&烅LɂLKK [|gΜI?!oT'rA[[[5Ǐ7okO,d*Mf۷S,^`^g/+W 6޾WTˮ(Tdgg QqĉСCwuuu9%FgCC]|||MMMnn_ڸW^ƅ~~~NT{дjŊ쫉$++U>d*Mf/Μ9aڴi^ z'5*h4&&&RƠPD17ߤwO>9|p¿IH6oMͦ^TILH9BQ:5kVFFkS*81UPI{Jkcbb79BdnReq-<䐩4ojllLNNt@tÃ]d*ML !ShLyA @fŸ{<Ƥtݮ.ZU]]Y?qqq~~~w4Yvcgg7vm۶Jwk|ۤ^~z!$\/&''lKKKE d)d*Mf[[۷zʙ3gX0}"z66l ,((vѣG}}}˒?[uwwGEEū)d*Mf*QCΝ;'7qS[ZZt:]YY_o>777J?Y@ \z[[4:;;CCCGÿ'T類ӧO򏓓477MTVI=W%ln: r4`3}2&s3_lѣΝ*E&U|rjPZZc4yG(kDGGIݒ4Y$---{͍7ƍǯb+4lQTAAA/6\斘(j#'H!Sh27SUTTl߾`0TWW/X@׳gR O? Gƍ)dddٛ6mz*ѽiՊ+|}}o߾Zfee9::?^ʠ Sh27S]xq̙...ӦM,ꅙFo2qD7aaaeV)RXXXRRbggGkcbb;;;yl LLC2&d*PL !ShBET@2&d*PL !ShRT C4)f*g0 ShBET@2&d*PL m߾]{ggSlJ~gffp/Lu̙Cϲaȸ~  ;%HAAA U{/ PT  ٙzIENDB`onedrive-2.5.5/docs/puml/database_schema.puml000066400000000000000000000013361476564400300212420ustar00rootroot00000000000000@startuml class item { driveId: TEXT id: TEXT name: TEXT remoteName: TEXT type: TEXT eTag: TEXT cTag: TEXT mtime: TEXT parentId: TEXT quickXorHash: TEXT sha256Hash: TEXT remoteDriveId: TEXT remoteParentId: TEXT remoteId: TEXT remoteType: TEXT deltaLink: TEXT syncStatus: TEXT size: TEXT } note right of item::driveId PRIMARY KEY (driveId, id) FOREIGN KEY (driveId, parentId) REFERENCES item end note item --|> item : parentId note "Indexes" as N1 note left of N1 name_idx ON item (name) remote_idx ON item (remoteDriveId, remoteId) item_children_idx ON item (driveId, parentId) selectByPath_idx ON item (name, driveId, parentId) end note @endumlonedrive-2.5.5/docs/puml/downloadFile.png000066400000000000000000002451431476564400300204020ustar00rootroot00000000000000PNG  IHDROmk*tEXtcopyleftGenerated by https://plantuml.comviTXtplantumlxUKs0WR8$ICS 1fB"A,y R }9w[ c%}K/ 1%z p% K J=&JgBCڭ om3ZkKp&"ׇ#2Ad $@ 2LbID&P1L$"D&B T "!* Dd@H2D;rӧW}˜D>8ɓ'zoL}p""o@@Fh$2! M>"u7W} '2F3"S%r>دdGBd '2Fb"S]\rIڵoSNި,|Q{LYy?{lvv9=߉Lp"-=$2{~O>i׮]f͎;VxRVEWd[xsɎDmw"6D&HDO%L{bZl?I]_^yF]z-ZHLLT5jԐ.;yg*]w]?}]vYǎwޭدU>ছnmԩӯ_?+#e^z骫VZ޽͛WDdz}\o1ȑ4l^;w\-/R[Ȅ 'RuS)O R^+V:thٗ_~ܹseٳ`0zʕ{OPgҙS"r/j.jyU>|X&ye˖EEEY:kYjաCdz<~uJbD"(ocD"رcϏ{#7N"S+Œ<>裬,`eD"$%%^9s~۽j*}(,޽;11Qz{Νӟ72H$2Qǎ+ `:f̘;vO c$([9rdFF7sΝ3g4@0F"2tԩ#F8qB/ 79HAD&رc&J`ժU˖-ӟOH$2QVRRR;LX|||^^@E3F"ꫯTR=|zM:5777//oƍjի?P7''G(jJ*Ɇ" DH$2Q&ٶmw}W-=`u֮]'y=ִիWˣ)%}'[YۏJhL`cD"eŸt"~ _~CDdc$(Dfq\{衯(KN"21Lɓ'32wѡC;vR~~> lHdLw֣(+V-T(c$(LLLԣ(iӦ>}ZnB#D8kGɓ'O,T4c$(+ ,غuݫ?PьHd/#GCzz:Hd (G9̙3 AD&VBB|=ztNNL@p0F"2{}gzL;rH  #DyXl٤I &$$L:ߒrH$2QN\.ה)Sl٢ضmۦM۷O #D:{lrrS[0˗/?}D@2F" T L}zH$2AdHd#* '2F" T "NdD"D&D&P1L81L/j1L$%%X)ֺ-c$@9q\-[/E+2eDeS`eD"(?>lÆ UgȔeq}*Hd'555222::ZR"Sʲȸ>JH$2rթS'KiK뫌`eD"(W Z,ˈ> VH$2rr4ibE,?pc$@y{g_#,H$2򖚚ڲeKLGYHd@}1F" TL>cD"Ž4ƍȔ'v||'|r1@0F" 9w… _{ݻwiժUs|3g;v,S nHdam6j(i{P,Y2~|q@2F" '={vbb:PGPdD"_^O'NHg:uJrHd.]|r=n4233'L?l9H$2yyy&MҳAfҥ|@3F" ԛAɓƍ<c$vgΜӃAi֬YD&ݚ5k6mڤ ҷ~;|!@1F"`7e=eBc$vz`+WGTMsZjA7_/!@1F"`d]k׮oժUk߾}bb>@qmڤE(vO*Bc$vDʕ+⊹s?~rY_~M&iڕN<D&?yM7QF;v݅)##]vZTjquk;_FشiSvv֭+}'=6X{gC5yՓ/ސ Ȳ}o7ߔq97xvK͜9sfÆ kԨSOIUEߨ$u W]uڵmH$2쌑)(+˗8p 77o߾={t233O8~zm,zBzÇ9rW^Æ V)|69r^z饼QFI >\-vmֶb޽\r*OE)))ٷo_FV}Ԇ/\|ԩOLfD"v풾G ۸qcݺuݶ|ʪTw}gfLEvvv*U~Guqݺu& Cɲt,a5kɊe];H-jWhѢMFُaӦM5j԰omH$2쌑)#+R5 TZm˧3MY+GR":uUkCǣ-[N6&&F2U[+o֭[WQ" #D3Fo2ANիW|RJOWmu;v,++K}φؓƷI槟~ڤI71Bd6c$vDʕ+֭O/O?ɳ7%%رc}~|Pĉ6l5j̝;W6yᇵMp޽2h룏>ڿ 5Ŋjb͟ }g~,Y"&8mڴƍ׺/FvNNo#FQ" #D'2?xT,Zz:H( /!5qDȔ{Gdڵk?jp̙r1**Js~ZvRV͛7K <^zr͚5Ws,7xwI Y]v}⮬O0`Eߨ?1Bd6c$v~F&f2Ed6c$v? D [d?!H$2RAdT#D[|yZZ2Jw7o #D;})SAP3gNVV #ȄL8177W &B1L8@zzYAIMM]x #Ȅ3ٳG1cƜ;wNZHdΜ93|Ge0w;vB1L8Fvvȑ# $'D@(2F" '>|xzz^98+WLHH*(c$p3gǿ|l;rogB1L8Ҿ}&No2vԩ[Jr!D&˗/:u_>roK[&''={V<HdC"S1Lbp"c$@ 2DH$2AdHd#* '2F" T "NdD"('999ZdjkdD"('III)))E{dʸ.AD&PN\.W˖-W2"ש@2F" g}aÆ"SeD@P2F" Ȩ(yMIdWYקAD&PnVh뫌`eD"(W [,ˈ> VH$2rr4ibE,?pc$@y{g7n,)_81LlR"S?pc$@PGqHd !!A"#Dscǎ}#7N"S+*Kr_w`cD"!N_rUVCM%t&|3F"P Zt&fD"!Ma "#DP2eʐ!CrNNW_}iY>~x߾}o6mw}?jN\\\omXfڵkk;vkׯ_]vm۶޽{vvv"Hd"(8qkϗaÆ{j9))I:S\.W*UԸ`qS+3%2崰b Y^`-ܢ%/?SYxWg!D&/QQQ{PЊ+W o={N:5%%Eۏ,%:S"jժjջPReᅬoaD"o\xԣq\^z3g O_ҙ;w5kVh)R3կZW^y" l#DڵkF/_ncر|]vB~~e˖Mew?֪UKwΝ; jră ڷoߦMf͚XB_]3F慂_]t,COXJHd"$%%EGG)oݺ7(_N*G[%idCk׮`*LwkHF^^>T`ӦMw%KlR_}֙"H$2Dm*LwZ0au]'9ԪUXJ*]ұrq}QSYGپ}kvŊ/,K*Zʾ[?'O4IF$RSSd᭷4h,lܸQ^ڔ?ov>s2>}toY` 7`x<D&Çt?L~VkUT9tiڏu駟ٳgK9ey޼y .?P?IJJ;V0͛¸q:u~Yfv\￯ƽW_5h`ֈWt&.DfGP$`ɓ'cbbw>qufLʕ+_QjNժUewUVM͗嫮x?ϟ߱c֭[jꫯV[YѨQ#( z]ʭ*bK/r}7i6ll2uhr]0@x3F"Y< B?#F'ꫯO~w<^gddH;v>]x,׭[sbi&Y޶moկ_?96mȲ,7nݻk֬z#n~f81VprљVt9rNj_\{^NzH$2ҬY3yբE y%on;CD_^~eyU@>}I*8_Hd%77722Rj׮]˖- !Dwo=|IxeD"_k֬Qç_yWH$2Yf~O&8OH$2A}~ LpXyя?8..0vXy5n8}Ƀfff":t>_Q!*NzFrV?cfIJ̔W_}uƌ~;ZJrݻw'&&ʛŋ;wNkd…j䵣BW(̙3ߠPc$~d073f̎;@m۶5J^)!jɒ%ǏogS1bDFFAoNٳ B|#=z|՟=c$fd:uJ ĉj8<|`7k֬/v&)cǎgNjժe˖-,Y|rEp9a@x3FbE-[;?@WIҥK 쌑Xjꫯ&s1}ݮ]iӦGz)Ks=/Ƴ" {?#U)oF2o ~饗^˓.djnSs/_.orss+qޑț9˵{nI\ \A}zc$zÇ9rDuذaWr}Q!xzuCnE^233}D83Fġ4/,'%کS'5YċZ*nU\yֲ7׺N :T\%\QO\qTʪUԩeΚ,j9}`90k¡Cd'ru>GGG/n?֫WOg; @q#b"3++Kޣ_[ .lڴ5.ŋ=],[5kj7nXn]u 䫯;wڷo/yyVh߬dggWR:u֩w-vE |͑}VTS㊟z=gܵk'M6ըQ]k׋zm۶ݳg}}Zu.ZvͭekסI`_verڔb=;v+~0~u|SO=դI+:w~Ez>@[n^۶y~v"0FbD9_{dM/wZ5 TZU8{z_uuERuԱ*nmWkL%D83FAفq'(?_V2(y6c mb WQ#"סj-Ztwژ)F5=#gݍ`ԲާOO~) iii5JIIz-vD&c$VLdj.WL?eys#V^}I9~5hJ2{mӦ\\#<"o;6g3O?U;5Õ*U4ş }LшL3cd PN&ڠצ͛jjԲsYעZ|WǏrޭK|4Țn=F-{=_6m~o oޫW_|XX~yru;t>B9{]tO?1;vo߾sի7i]֧N:5jg[E\ީdddMVXa_ T\*7sqzv޽rV]{#~=r'Nذa:"gDʕ+֭O/O|6=_z-^Xʹhg$RϳE;BϮu9,YDN'OkܸlӣGuRw`5'==}*e yGO?''G?gSL_;wo:tرc,SBX:޽[ ǒq+Vf l,_<--Ma \7y•1K'2<[cM6 yLX̙?KWH,<@'NLPLgE03f̘sO˜1K-2ŶmLG;zQΜ9?@XuT6Νc@x3FbiFHHHX~]N nj?@9r$;`bb ##S{}m޽No69r7(`2Lr[l٢Gpؾ}i^}J8qo|Nںuk||<-LEH,TΞ=,߰%8C$<(˗/?}(j3 Eq+z4(߰ۖŒXz>#,o+Df~ @X4H$2[ '=1F"Y<V81IhHdNz@cD" ''~Q{fD" )))%%źh%ֺ!(1LբE -~k[NfD"nܸz˥o[QQQCէqE0F"i,o7o.odAlEFF6l055U ID_5k&oZh!I*8_HdeĈNQF < $/H$2rĉ 7[:thٲ%~ q#W=~B'=1L%''GFF>@2F"Y ͚5[|0Ix2F"Y 0 adD"iii&Mzo6y%_eYFd\@IFپ}kvŊ/,GGGZʚ&k9ݮ#FkNv%U{Xn̗ u#,D_.MM65__; {ݺuOٳ =(UT9t萺( zر.];V]4S >cY7o^DD… e?С=GKv`Y~*՜-Z$%%+RtdLʋ1Bd=+ 뮻I&?9g|XǒK/رciӧqO*CZ3e+V`WnSgBQ [x)na^ 21L͛uv)۟{9f 6[BV}|qә~Xd_߬YܹS_]¼@dc$^8SʢL/[+77"#BqРAҍ:2Gf|7YR%. D&1F"clllnIIa(7Y|"#,͛/0cƌ]veggˠ|eQ;Lrә`a^ 21LϧM4Q#2"|,R3xa^ 21L n:ӤT  @`Hd@|0a”`"=Us/ D&1F" )${,$oD&챐Qc$BBFPnHd|ؔ)S srrӧO{7iRs7o~ 7ȸlkD&'{8qkϗaÆ{j9))I:S\.W*UԸ LaD"c/DEEٳB'V\n(p?{9uԔN* @ Hd|zo\xԣq\^z3g̝;wΚ5{WW"@#c]vmԨ˭=z;VRϞ=k.YTlҴiSk~QH$2>yXRRRttok$77[ԩSeȑ#zk۶meCk׮uF#cF#dÇm۶K.?mD&W999Zik H$2JJJ#qYk]t."@#rjL6nغukYT"21L@!w}w&MTg[vmTTСCDdc$B̙٦ML1)F5h 55ULD&0F" EGGxҙcRm۶;IEdc$n瞄}cD&@wAq-ZQLaD"ENTd3d>G!21LsQy-G(D&0F" .::Zz,>G!21LwB#"@#;}g}6eʔR5f &+ 9ug7D&8޽{ccc%e`ժUPiزeK\\v|h- @ Hd#9sF:m֬Yz9Ç'Og鷭D&8Ovv/w-_ H$2a92bĈGX6lHLLog!21Lp/r(2gΜ۷뷶lD&8I||={D G>w~ @ HdcH^Κ5KPuŋ뷹 D&8Flls?KǏos 21LpӧOYZ[^ڈLaD">KKKӳ,޽>oyi#21Lp1"7S奍 H$2Bdc$ qqqz}ʕ+N#7S奍 H$202O8QE_Fd䌑Hd3,28ԬYnݺC o޽=zVZͧO~egg0@6]O>yQk1cFTTT׮]n9" gD"dy>999Rڵ=z{~cbbFf߾}zÇ9rW^Æ s\|_4hбc$;n9" gD"u%|… 6mjj|Ş]Jkκu뢣sq߾}j\iE 2rH$2J_$uq֭իW:vj֬YNǵh=|!2rH$2J-ZO2eMjEGk<6_L1Lp?#իף>o߾:5JMٳ?.s8ХKa322d~Zb#=5zN #g'2Sիz]F+gUӽ{-ZժU+%8xzU^Yfn\G9" gD"A5^YX`A&M "7S奍 H$2J72Ҷm& ?M73* @3F" 0e=v興:u?''GQAf근D&8Crr͛& -}'|Fdc$ gΜ1~ӽǎoyi#21Lp)Sdffe*N<9~x6"@#˛rhbڵ2"שD&8^ffG}FӇ '9eʔO>?L!C4lPuLYק"#2ڠAՙrTaH(PbD"SDܹsdddFFWY}01F" )"3!!o,ˈ> #Jr6mjE,?1LO >뮓%_JD&6mȍ|%cD"SE慂GPbH$2>^d&$$ȍ#(1c$B/2].(>3F" )"B(ʍ1LO!c!y(7H$2>d#=7 rcD"SHXH(ʍ1LO!c!y(7H$2>d#*''~Q1mCD&WIII)))E{ɸ.: @ Hd~r7onucgnݺucD&^zEGGT=6vX:t>ՙLaD"PȆ $î:LYŒlذajj>ՙLaD"kѢef뮻}cD&@ꫯJIg۷oBB>ɱLaD"s\ 4P٥K-[G(D&0F" {Q9p H$2^lذAl.]B#"@#xעE P H$2ީ QLaD"ϧM4ۤ,ˈZ}"21Lټyf̘k׮7WYJ9 @ Hd.;w.66[nk׮Z#3e9LaD"A sdwngD&XFQf|J_D&0F" amݺu;u5ro7o޴i6m\R";mߒ2_c H$2 edd <)ed̗a H$2 I42Dٳ>?2GfGi!26LvfFFFnݬOy 8H$2aÆ_9s eP˶0LD"Pŋyyy'O߿q.2"x-YD&F" .D&F" .D&F" .D&F" .D&F" .D&F" .D&F" .D&F" .D&F" .D&F" .D&F" .D&F" .D&F" .D&F" .D&F" .D&F" .D&F" .D&F" .D&F" .D&F" .D&F" .D&F" uSR6L-z (Hdl@D"` m$[D&Ph#"2BD& H$2LF" (x7T(D"P,;;;''ǼiLMHdy&MiF[o"{D"PB>}4"sܸq2NGD&kJ[6jH:S60cccׯNGD&@,aٸq㛊u]iii$HdT/t R'D"zqqqFavܹiӦ\Hdݻ?D"ڵk.۹sg.LH$2%''Kdr`D"q.Hd/^PN>=~x= @F" Ҝ?^R*ٳ?SOH$2D˗_^1<_}Շ~SOH$2DLg|R}Hd4ӦMS/&LSN7nXvmuw9زeKժU2 p3PXX ۶mOޖ;D"@:u~ +Wڵԅ4R۶m̙(UAA֭[)J pE<Kޖ;D"@)Kd~g_owy˗?cRm۶`;y:H+}x H$2DD-k>WVu6(^}n!..nʕ999111cջufCƛ4i"\s5 ٶΜ93>>k׮2RXXO]שS'8vq㯼J !o/4g̘!sd066ָܮv뿂Iy|>us:|#<"wZvm3KYGyzLWZ%lÓms믿^~5k;cA'GyTW')' H$2DmdJȟl|x#ئMѣGrB?쳧N:t0hnGdN:!C?nt݀#G=zO>Ç71|$bɒ%B'Nz+jݟgFKrx |>us[Dl[f1X:呏5J3ovs$xvڍ9XQy"v\C2D"@Fۥ%Ѣ_2;w7?裤$oe{5k4#Xa׮] )EdVV7mTF }"G 'WUˤ:s!:OO"^{1(uw::ut;_ʺ?u 吀ױ x/Г<*MS҇dD m$"62}E) *Y eض$Yj+W0tv>eO>-h+#wow~+XYW+S8rH*UC eYZt֗I'|ҰaC͓<*i=+}HvL@F" Ҕ%2?쳺u/1.j*3}y5j&'AsW^,{YtuNudA#0OeD_oǯ}w};yڵkA:Gn}9I7~1bQy!!2CD&HS}h5jH9HʠTD߾}%>P{v=&&&99yƌիW7_:u_ 3tzCjܸiӬ{}~wjGI&;wСik)߀+X)_}衇%g7XrW}'ͫ?9ʣ2oڝ=$;D& bh#i`_Dy+fC H$2D+R^^͛e믿[~iuAG-w*ID&H_V\su 43PB?2mSOH$2D%K?gN }$$$iD1cd$==] 6L@ k׮lԨtlHa֯_?77W 6L*99̛3--M6LjرF^~_eee@TF" Py޸80Қ6m%AD& >}_K6L@k׮ܹ3&m$p`D"q.Hd@9} /07N"S;*<$H$2 HP,((e˖CN yBLB6L!!U th#PAa Dh#@ajљm$P(23tH$2QWri#Da:@gPHd@)0wUvmͺuZk(wQqEЙTm$P9J/L_%rdV :JD"*0}~hɓ'm6%%M6ĉ5jڬY #FȜKӾ|И֫W/pc`,5rHDYgmYvٲeu>0(Gzj/(L*6Lhe)L_QUReRhfq}Fvvw)V9.r>|;Hu'otfÍ | ~LLG}$nv F7?b̀099YK6#GgPᴑHd@E9ra)J$?l߾}JJ7(#RY:u޽IV^m̿k;v|EIVjU3\S:$W  KHd@ȑȘ$ԡ 'OEy%H$2")"\DH$2")"\DH$2")"\DH$2a9l0cxΝnZj޼=SPP`̙:uj&MZl)rNe!2p6L-k} =znKMMvڭ\xJ.m$[a6tЩS*a\H$2avȑΝ;)aH$2<fBd m$bqf2.{͛aH$2ż^of4lŊ)))xj!2p6L@ =zhذљF-X >>>==]VL\D"P¼ybcc3%̤0S +HdTM4III0l۶mZZ:)BD&@SOI5o"/IH$2X04i֗1BD& ;È//c 2p6L@3"n KL\D"XbbY\@d m$D% D&F" aޛ6mTW;Vl„ ȃ̜?(gD&F" … |̙{n_9Xl:;v̚5K"ۓH$2 ,mٲeijEM8ٳ"Wh#3gΜs ƌsa$Fd m$f|͵kתN:%yT]Ed m$N.\tR_AAAFFz"2p6LN2egbѢEz "Wh#Y8} sv +Hd@x8ԩS2,ofD&F" aŊ6lP,رc޼yꙻH$2 =//OmqF)dy]zO̔\ i#؊ȼTt9.@HdlELd+r.\i#؊zr.\i#؊ȼY@%F" IaI@%F" IaI@%F" IaI@%F" IaI@%F" IaI@%F" IaI@%F" (x70S"Wh#1oZLey3BD&MII1; 3q[<5BD&'%%il|԰Bd m$L*m)a&rssթaH$2%_۷o-$6l(٥Ku^!2p6L_~don*rwdee +HdTyyyF^7n֗1BD& T0op䏁H$2L2ň֭[% D&F" Y\@d m$D% D&F" (vżɓ'߿m۶f~lˈ^Ad m$ڰa/̜9s텅_||m1v0Ad m$K.\֭ʕ+}WL yD&F" NqȐ!>{Ϗ̑2?:H$2 eddH7=e̗H$2 mذ[ngΜnٲewqGRR|mNrTx~& +Hd@xb߾}y…1cƴinK*ҵkקz*--mܸqO;(967Kd m$^3<ӢE #/HgZ'˱u$BD&Dɓ'͍76mTz葖f{r` qD&F" ѫ_~_~yZ% 6駟6˱y3BD&D;z<frr%s=r9_ qD&F" ѫYf̛jSiѢbΗc7C +Hd@R>lԨ%lْO2rH$2 z)پ}{5+K֭ @F" KԩSլ,I*崑Hd@R .vmjY^ֺucZ i#už}\9~xLII9rT9Sces$BD&D 6t̙3戔/ܡC&MH^6kT>rk qD&F" .##cO)K'3emD&F" NqȐ!ҍgϞU92S=JC +Hd3322ufLW̰+KD&.F" 6̙3o^XX(]e^iEd m$b/^˛H$2%_۷o-$26l(٥KuJH$2~I^7wܑNQID&@g䥡q\HdHMM5 [o?D")Slݺ5&m$Hdr`D"q.Hd@|3_|QVUB}ySH$2}R5=\AA/-[L 7Ǐ5j%H$2eH)̈Ag"m$&LܢD"\0C +Hd;|fȣ36Lp t&AF" Qa H$2 (> 3 љ8D"ZvRJ-Zjڷo5k֨0pƍw}رk׮syy0a:҃1)}k׶m۶y,]TmAg6LpWjarhٲeqqq+WLqcbbN:e,ʉݝڍ_2Fw-~iӦM%љ8D" (x޽{ےCZJIIIKKۻw17<͚5ի`O8QFxFF1(9"#2ִ 8g˖-R;vѣG?++{1Iv,k8$YܹgϞ҄ / #'On۶LkӦMnnusCqȐ! W1nlҼiJi#'0k5YJm۶կ_ؖz|E#ݺuOzKzO6>\Zo77 {/1͚ 9۷ohѢƍEm`x:tx饗mcAs;:t7=zTaÆuI/'K4eD"y+$S"A@Bzg jUVmqYj?}tNw>iҤիW["nM 80&&oq\Qg-zW1صkW)+ 7ѣGw]EJ9<116>5vsz֯_x JkQ>|0ou|}R"St9..n޼y4q¼DdD"RJg'L soKi$ܳgwGeu .4!28>ǎ3gvQ\98p`Æ ?2G\7W;1cJ̷zKtP?b¼DdD"UJg='NTGKEa^"2AF" .3+ݻSRR:vXXXGaLcH$2tf0MD&1m$:3QVD&1m$&:34Q "6Lpj(LD&1m$>:3tPǴHd@|&9Iu(܌?GdF" ED*^YcH$2HH+ pLD&)xei#"E",1m$[6lxns]*рZjռy{3u&MlRXs"6L-kB+ @i#O섄~9qĠARRRZj%ߧO.GRSSe]v+W,^!HdlСCN "6L-k9r$55se  pLD&TRDًEdF" (mG)"׼FdF" (z6mjv"/NIISΈLcH$2%#11L#E}tu* pLD&EIgʆflllsssթ[D&1m$URRRbbbf$E0o4u pLD&@??!5HIgD&1m$cccLMMmڴ)0D&1m$N#2z!.yLcH$2;v?8D"XRRHD&1m$pɟDdF" ??wߝ:uK!lܸq"ǏWw yM6o޼ǏO1JEdF" … ۜ9sv y˖-SBW_}oH eW IDATGdF" dͣFR# .Yp Ξ=>CdF" aNѣG>|X}P pLD&7|s͚5j|=~xeD&1m$P^Ο??uT~PQ|ML;D&1m$P^VXa5}PQv1o|7寶vvMF<د_Zjc6l:3gΌڵu.$uOMׯ_g^z 4Xj<-όl[Gd!2i#LC3oQƍϞ:uJw0`ȑGgx"Rwk֬1V0*NڬQF_}?2?+$~:wwxJJJ2:02L>999-hlתU˘lȴCdF" ENgqkz5\+*Ru]'R*G3g* Y'z<k*wa v'PvLcH$2h#W;ӟYe\ȑ#UT1?H4+lذo7ouuud֩Sx^̏?$S>ʶ" pLD&Dg}Vnݿ/eWZ\]֚R?A>,{Yt1~}_ɓk׮52VDWނ &]|SLUVNNKUzzz6m._J@G9p@vF?ܵ~WdݳAd8D"KY"SX;ƫQomURqCh76NzHVS?ne y 7,ZȸiRɔ7mڤ2mw2۷Lߞ8quo~ykްaC{6`ǴHd@y)cdvLcH$24K*(BdF" ȬtD"6L(//Se pLD&seff郊2w܂UA"6L(GƥVQ&L8D"޽{gϞߦM,X8D"״i$5B93f̅ ǴHd@:>{15PnΝuV pLD&‘#Gҙ/3g$"6Lҙ=ܾ}$.]>CdF" ӦM={6כ-G2eŋǴHd@ڿFFN#7nzO7lǴHd@%8wŋ2330Iu(dS7Ȋ+~)FLcH$2HH+ pLD&)xei#"E",1m$[H8D"`TǴHdl"W6L-R$RF" (x7Q"|ǴHdegg7)"׼FdF" (z6mjv" .LIISΈLcH$2%#11L#E~tu* pLD&%KHgʆflllsssթ[D&1m$URRRBBtfǎIgD&1m$%B6lxS?++KpFdF" P?~<66(̖-[6mڔKD"6L@]v5"G?8D"qѱcG.yLcH$2%%%IpɟDdF" ̸HD&1m$B|>_ cǎ #CSD&1m$*$}ق_Xl:TIi'Gg8D"B/ 3Й."2i#0ˆt pLD&T2y%LWǴHd@eQW  pLD&T$"6L> 38tf0LcH$2J-]vUREZJMM۷5k]͛7ONNK9kVG&cK!g:auwvڶmi6nxҥ^ pLD&T4_e˖ŭ\Rٵdɒ L9/pw-~iӦM&:"6LP>]aEoѻwoe뭷Z9#FhӦMBB1ɓ۶m"㹹ĉ5jڬY cMƄ#GR2gm,1l{]vrsΝ={t}Jl:3 2i#+CaEmׯo2jMnx7 2G6wyGzo;S6>\Z5s\65>_Q|G tGuᥗ^2'r֭3:JǴHd@9r_٠Ae}dML_ќkޱco^Yf7x>}SNݻw4iիK9dL իW7K#ÖOnW:38Q[HWD-m$Pi|tS\HWD-m$P|t敋¼(=Rh#J3y/";;;''Ǽi]Aeyi#3ˆ48Lכlv‡~"{P*m$|tirgϞ7|љ Z|||zz:{H$2 TL{\|ؤILِŒ_~nn:{H$2 HDM8qZȐQ*<-YdƍKU&'' R]tIKKS'P*m$[csɱ 4C=NTH$2 ʕɓFam۶iӦ\pHdl9`^ˈ{KF" L̠,_0SN\6L-kשSgر۷OLL\f1M6ݻw/,,4.#+p3H$2Ȕ#,]Tϟ뭷㒗|l =9'4.%h#R"k5:T^KEVO?$;wLHH0 \6L-.kn?DdV #+6L-md^* .q=|1QKD&VY" T1QKD&)]_Hdy<M%E,'b+6L@윜5Ed\7QOWD-m$b^7%%L3E֭['!"D ~@F" (O>IIIFg)"/HHHHOOWZH$2%̚5+66yҙ"?\u**I D"PO?$UymJvӦMy<'b+6LٳME뮬,u*O D"ڳgAM4?!%D ~@F" m۶FdvڕK1QKD& Yfٮ];.jOWD-m$Hpɟ|" ji#f\K1QKD&0?XbzLmȑ"x.Yܹs{lOWD-m$’֧NqFͲe!d//x|" ji#~-Z$??ĉO~yn/D ~@F" O/VTGxԷ1QKD&pf't ;vle1QKD&>c,_|;@}'//oΜ9@9rd7ħeLS+"6L81}//kܸyʩ3G[,1ct);;;''ǼiMD2"2i#ȄD&_qڷ:s6mR$"NVVMII1;Luɸ-Z6D&1m$p+(μxرcƁHtĉ)S \Իw襤$3D/թe@dF" 'L~o ;СC6&/|9忂ٳg6o\:SQ 3>>>...77WYD&1m$p+ng;wV"Զmۖ.]WO?IUv!55UQ6mL+#"6L8AdKycؙSLQ "ٳg^8뮻n*ңG,uRǴHd‰[˛_2??,1_Y}~+cՙ'OV y[lk|EJծ]0aB:uWqFs뚕9SO ƟרQy35{Rfj֬^N-9han)+cZH~O_{z{L̵2AK^cbi;_y~_qq>ct)ʲoy˽?]7IīJw柃?TuevȴOs*U|sop' ސԶm? ўluSXv_FޞOϘ5 >ܸo|sW^8̃ׯVZu6l7ٻ𨪻-rGHP7@jVWTׇV}jBI/U@"  D@h&BH\[׬٬=+ dϞg͞5{f`edׯ_ZڶmK/i;;\ըQf͚}'+jɿ_|E50++f͚?+{ðO>c:thիwAܻ$))i2"Un.222<Q4lP|͚5j/BvS|駵kז#Gr`<ѣG=_}ոnݺ/葨ݻ%%%&L;vھ>N!Cd\jGưohۃI[N ._\hذaAn`ԣ3f{j瀬x.]ȶ]wu#G 8'T{ʴ?cǤܴg>ȑob|z-2sHLOaH7k׮ǏWWO Y(ܺu/>?NSdtBD&BQȔ]?V.zIFqؿOXMbZ5k8S՟qfn(L&_\zbuϦƥ[zWU}a+СFƿh޼/_M{P/#:s!&K̝7 7hdk.uoժ5g5.5mٖ[uL޽[LUjvܩmذAtcuQ;vWFXiAAAխ'jڵ֞RJ C ~$zrss/첹sJ.jW)SaXhFgL?!BdBfD"(gd~/2x/Ŭϖ9lH^ǭ%KqݕW֯WONk><3ldsº}G&ݻܼg{AigӎP=EU_r5j\~_媿~[|vݻrzi1)OT 6U_Y_>\2s3.YLW42o.Gn]ԏLNoO|rkBmf+H)SF _nnӣ ܱcf+_,ϑh>I[nEW^j0tv_wC 1>Xy t% f̘2sFQ9vɭw2/^\<|pjլٔoV]jՉ'dVlݺN( ʈzN~_5ktAdlAiէ֮]k?YOQv-_xsվsbb['`׏8i_f|nƌe9UVI 4HG}T۷o͚5۵k'j:rUkڧ7mi{n]y9>\ >S2dմiӬ}Gdzn`G}!@-RDF|]v6m.DZv%G+?VOv/No}ȑ#O|5ߣdgÆ -[&2j? p-c$dU/oJJ.YΌ͜9SGf-ZT\1cddd裸6 p-c$W4GfZZڼy~ 6^?UNN.w/~O{*;gLD{.X`ʔ)K,9}G<1QD&vر{/ \"¦ľ; ?ßHd" eD"q_gD-c$G;? j#8"E)%QD&)N.ZH$2Hw u @2F" pDSK31L9ZhעZH$2ddddeeY)"ruU(D @2F" 84!!L+E6n(r]QuOgD-c$ 0UV3Ulذ!>>~"'b31LyϟӡCLI)f͚fgg뻢ZH$29sL\\\RRRbblooC ?ßHdtÇӧO2KOOwB ?ßHdtWyl׮*'b31L@]vUٷo_>mOgD-c$ϟ"3))q1QD& ?"| ZH$2#\(D @2F" /կ@U ?ßHd8qO?;L0WJZH$2:UtۄZH$2BgM eD"paҙ.~"? j#Sa*t{ZH$20:%OgD-c$rR,--(,,ԯ@OgD-c$_J9;3''gԨQ YYYuO eD"ZE S ҙҥKLLL-( !D @2F" +T;3''硇j޼e||5\Ca^ 'b31LR:377WuٺuXL ?ßHd@3fݻpw} rVP31LR%%%ayd^[oUDg 1LjuaZ76[jEg 1Lv izCd 1L@:i)--(,,ԯ@"2!3F" tfE EdBfD";"2c$s:"2c$w&=D& dH$2:{gRDdBfD"={PDdBfD"tID& dH$2HD&)U#8"EW2c$GWBfD"*^Y@ȌHd"^+ 1L#Rīxe!3F" 8~QKZD."2c$s222qֺFdBfD"pNiiiBBՙVlܸQs"#gZRRdÆ GwE"2!3F" 8cbb:t ))"٬Yl}WD,"2c$9s&...)))11QRD۷o7!#.ҧO&e뗞HFdBfD"ۿKNfv!2!3F" k׮*2Gx 1L@WGx 1L@$EO"2!3F" L}ID& dH$2}"|D& dH$28q駟~w&LL@ȌHdt0%/ L!2!3F" 80:{L@ȌHd/L:ӓL@ȌHdé0:KL@ȌHd-xa*tg#0vfiiiFFFaa~ 1Lv/L3jԨ,:T5"2c$*ZJ,--?~.]bbbZhAa 1L^ߙ999=Pͥ-㯹 ӵL@ȌHd@ 0ՙ֭[JHdR.GdBfD"Ԙ1cvޭcM?1LR%%%ay(H$2tUEf߾}`1F" `*2`1F" @}D&쌑HdSG;c$P9~ᇏ>h֬Y3gLKdN:U"ɋ2իGfD"u \Rp7o 4H$2 tNby׊>|裏W#;vz BX8Hd@(0ǏQ=\Q6lذ`ΌHd@:uj̘17[n_cD&TXZZھ}XAzgN> c$P1LA$^dJ@Hd@ŤYޓ c$P?̙3@A{71F" G聂ȷwwyGc$Piiiz+_oD&T@ȑe˖K.D ʯ_~ll۶^zgg* 2(c$Pzf͚޽{KzժUK. ,3w(LeEfp%_oD&T@y"/˕W^[o?~~kg0P~噊 :# (Od z饗6lf5^PP0r ԯ_8z:t ֫WO՚A]o6o\FEž÷~{w֭[Wf~Njո_l֬Y͚5o߮Me_߿VZm۶[ȴ<{lyt2ULĽ3kVsiڴi:u|A)vs|ȑ#|'2yyy֭҇S3ZRmݺ_~yv/˻ᄏ;w8qV,_\HFRN2dL+٣G-2~'-we鰝n%Ot]?^>:!2(c$Pܱc$>z:$ۙuVz={}֮]/ժU۹smZrrrbb}Ww\a׮]joժnÆ 49Dv|Ee]6w\:Vv<]CR:ڀ ]O>>]֊L1dȐ#FW_}b 5~w ++رcÆ sFE{մ={v޳gҥKe)^xykVrTrZTTO=T͚5M<28Я_?ٹ]v.k׮ݻ˝;w;ȭO9rGsx\Nϭ"0F" ` T ʽ_oD&Tyq1L3gi J"S^\~Hd@^zӦMz ܹs… # 8uT9?eǎ_oD&TLZZZ^^(d'N2eJ@Hd@ŔL>]D?0;;[@ H$2¤I/_ "S^^ԩS80F" ?u^A)..8qw}1Lт2z rIa/-pfD"BO0!;;[އ~|IEA#>}>H$2IOOСtDffbcc]@1F" 8ϙ3gt钘()p7dD"1b䥄e2v[zzJH$2qqqڵ#bD"@RR~?bD"@zzLM>XHdPӤ73@3F" L}쌑Hd@9qDrrr[M0A"sԩ\HH$2jHa>>[r>&R>:D&T(̈@gp#¬\t&1Lp4s\m6n~Ǐ6SN:t%>m۶رmy( !Hgկ_gMJJjѢźu_|ѹsľ}ϿpdD"_}'O픔'|RvmVRҙQZZZz5nmQSgJd7+VEu%//^,&M7A#`F5o޼qqqɘK.gu,S?,yM7֬YYYYD3%2kԨ<بQA]7A#`KC.YDQ^z饧N:ܶmkַo߇z~yqwqYk H$2w͚5[|5ү_ɓ'KR?c8y4aͭZR{W>Zqw_6mzZ&UACHNNG/wҥCVX_}֙#eC^ÇA`dD" 222ջ^JQQш#:u$5k 9rG.2صk5k֨"oYfII]*hCw^Xt5\_mcL1L05jTjj>Z> g61n8)iZ띺SnZZ})))̓ >|XVڵ^v޵km&m&SM4~7|_ZE*5~jrhu[K|dK.jN1}t(e.fggA|dYhQǎ#nT c$HM:oX#S['~d#77zR;ˆd' ݽ{3fmm>},^Xmu>.q}]8v!CXW[nEmks8$O?]n5B0F" ZDݻws>{d֨QC ܹaÆ!ګW}N6mڵv9#S".+..V/kJ,v? xӬY3{wо}ƍ!M.[9JIQ5H$2W+IULzۧ~K/|Ç߻wo:uU۷o?2K/رcUj4qš5kfff?6C֭:8|}XHd9V3vfC|cccu3ϔ?2e_~ڵk'Olo/;iӦ e˖|W(Lo`w2F" X3=ި,NH$2W33*2)L/aw2F" X^myQdD"p5α<μp(Law2F" XCg^';#\s,O3+U,NH$2w),,_αkBaz Dc$ddddeeYX2.ZQD&. 6mRs,9j߾\{nWD>ɓ'@NJaz,qYi-$.aD"pѣGũTXrղeAB'˝Lb(K,*bD"p͛˩c0cbbV^ " ,wLYePDY]UD&F7pCӦMJαڶmۦM}'"YdѓO@Ye1%Q PuHdn޽{&eƍw/Z,$;1LJKK[nNDlllIIx,wY ,|1LFm~WwɢV?Ypc$KeggwQαF}, ?6H$2d2F" Wzzz>@TRG #Ȅ7㏫W5k2iiԩr_9ɗWᣏ> |JYdYeԯ8DJ?P@HdkJKKjRSS7o|+Wԇ"PNNٳSRRk!}̘1*Z92~x D&_f3#Ȅw/_\?߿ر \\ D&<7X~~NW:zљ@9s?aƍկL?~|ذa:uС.IMMm۶mǎe\nkcs}a*A:3//~>lRRR-֭[ƿΝ;'&&۷@2F" /زe˂ tرcY+..O<))))O>VZ3223*-իqk!8uDXB-Zԭ[75.yxb٘4io D-c$xO8q~۷oӻ".QF͛7g\\M,/_ձ9s榛n߿Y"0)YF }FT׿d{׮]@2F"|gI"8.ҐK,zT#^zi.ܶmkַo߇z~/ SLUW] Hd"M2E?}@(**zW׻wf͚-_ׯɓ%)*w!'OR;l޼UV>ܳgOjծN:%&&4hݺuNtҡC6mڬXB,3FOe?.ʆ<9ÇW匑Hd"ɩf͚>tŔޣSF e(2(%֣G -ڵ5k>Efz+Wvm$w^Xt5\_mcL1LD?Od*''G}iԨQh/2ܹs `]K. zoԨQaaVLb) 4v7nt4E:vhs#c$lr2 Ҩ7L\NLL{r (2mִiSuW۲}ƍK~mgȔ$&e[vn駟ƖtL(c$l3f.9INB\~ZjXۛ7o2%޻ p!޽[k|٬Y3uե^ZZZG/K~)2kԨwٰaC]RR".[L]4ڻw J/@3F"<2lr%U-??_j֭g@CdV:NhpH=}'s޼y*+++11w۷ﹽϲ,XN}vZ;Asݺuն*q7.\>w.**Ui=iҤYXX0q&{Hd"~wYn]9yNj''1m۶}饗.own6!4lgϖ5k֌Qf?ٖ9dk%wԽ{wkO>sssZ~}9 F)/;<GvЎJEfftsiڴi:u|Ǐ_|E9Go߮pl\h;s?aa*-2x Y^v~e֭3<<2e=ٳuC'&.dx1LD"w]XX(g!;w8q0`+|M^G3fLIIWZWvN\~aÇ9rdO>>>磕I믻v> C,_\⼨Hj }Y8E3oȑSNG/<  k2D&"[E#S*$vڥ.Z٣Ɨ,YbCA6kB]vܹsor0ժU۹s׮-((^uk׮W;38`=9|zr6lؠ+xNY8fgݻ7!!gϞt]`&.&dx1LDF%&}YڵLtU-Rn^zI_90,hرCV;K+Kh>9W^SF}ct:J .0;JPX=H$2*;֛u}'?,ǏOIIo7Hfի/ÇUfvm~<[:Eb?-翓x-[~U&YjՉ'dMP3U3X.;rHez)SZtڴis8J .>w&š 1F"<2Ch"QŞ>i{EF@ܙ&.&dx1LDTT"T999l޽O>f͚W\qň# =Ȍ\rf߾}{:k \`m3$99Ga"rZ7nܘ 0F"{}z rl޼y늊{Zhi l+8p`˖-kT˚&ݛ5aD"^ mŊ~Z" M6Ug3M%>|Zccc۶m˚ 0F" /&LP\\w jB"lwq:i޼9UgϞjMx1LxDnnt&gF7/!*Cffz3}|T?*2tš /1F" 8ydrr2f-vĉ[l_7PÚ 1F" o:v… R̈XI(˫;/RW^=k֬eҼeԩF_䕒죏>W@IJ/_{Oxg=&]/_βHd&ߙ!T#Jn޼YCV\yENNٳSRRkQdAJ1cƦM5wvY6mrT1F" Y-[6}|;$"MQQN:",= gȲϜ9e9J#\Ȭ,˗/׿+"߿ر ϟGdY7nr40F" Y)x =:~xNhn:/3B',{1LՈmٲeAxЌ;"BNN DzmH$2W#2t'-9sp_`x,˳gCHdFd>~8p@ɢEXGzz:˲#\ Ӕ)S{𢢢{Nƙ3g&OՅwɲ<}tD&jDf81k,Ap<Ȃm^xe2F" ?O{տrrrVX!o6 rٺu+˲W#\ Gjj6k,wx*c$1c/$[lKъy.r}4}76oQ"رc}ΉNt!V0I x`2֭[a?`UH$2W#2ar~,J9XRD&Z/F[ӟFLam?swGdz1LՈp/bfj֬}v{͛7.((9rd ׯ=z>_߿VZm۶o~wYn]?jmܹT;jJvP۱6mRK/5lPF֬Y; |fQ3ϙ3iӦuy?nqqq{9onnСCɩW߯ٳg˃^Ak'2yY'M4`5"9ꪫ̀N6jHֽO>$55Ue%mk~G2uHڸrg紺{,_=>}XbypDW#\ G,q˗˷aÆcmONߺu/R);u]>r䈜%9*c$0FoΒo6lhРS&'''&&۷O][PP A{Q׮]oVϗLڽ{oj|׮]joժږ3O?9rd.]Ν+g|!̬STiӁ٩&}Ԅj\QԸxjvܩ'-77.G*.#22F5kd!˓mYlbb_'d["]n]k+12;;5m]?ֆsUH$2W#2à|SF={UW]ꫯZرިrrW>L}S]:t7y7x≗_~{|~T`96N=oSݴq%ȓ&gv;;5m]Nm.sUH$2W#2Bd~Weee;vlذaw76jhɒ%j!C1"77WpŊc{={Zeݘ]N0Ao۶vG_qu)))}׎i΀H,TLn"|7ݺu7nמ+ˣ(..^~ ɩҥK^h޼`pD&Z#7ߔNziYz=zd!R?i9,&IʦMnƝ;;鶮ZD9*c$!2SO=UfMIiӦGXjذe|e}ԨQr~ЦM4qȑOtr"r=ԭ[~ÇW49޽ ӹs?i08"py뭷JRG?6nܸGe^z6mo\LmM'W#N˝䶮Zm`<8"ӫHdFd#xd"8t?"pyDfϞQ'#ܥ~QLZCEp-iU.Ww^eD"p,=2e\.¨q}.ϲ G#G,{1Lnf͚5o\"3!!M6m۶wB9H/]Tϙ1cFIIp,祦,{1LӻwޤDw„ N(d/GëVX/<2e r4XrebD"p֭[}w@x_uUJ ?$bD"pѣGwQEmݦ_͕{ϛe {[oŲ Hd."'OLNN3=CL8q˖-+ dAɷ@9;w.\[ڻw E\xӧ@9sX#ZLdaD"pt>B8v… RcE>y%{wW@ĒS +<&ϰ- 3F" ˙3grrrO>x믿^3e[Fd\o?6&{Hd.iӦUW_ݱcGAA 俲-#*Gp>Nh=X=H$2W8}tJJJ>}֬Y_g#>' 8`M#?Ccڿ(˭L k2D&PΜ93h a>}zݺule??w;Vr[>o8`M#RNNί~+f>}yik~w 'NhMX=H$24}9sXlңG=.mڵke/ k2D&P۷[x޽{ey[me"pB 1F" T={Z6mYyk`]Nh=X=H$2Ծ}kFoJ?[ E@܃5cD"J;!SW%d" Nh=X=H$2;ݺuӳ|~?8`M#j}}p˖-F$/;vx=dffwVr[ pB 1F" Tz.ep/үpB 1F" T=Ƈ~Xɓu~dS/"pB 1F" cJJJ>}쿟O}dO ApB 1F" ȦMWW_}uǎ2(mQW{0܃5cD"p3gL>}zjժWeD,Y'4P 5YDH$2kLBYYYE,ruPH$2kLB۷:Z:!!A=+HdPزeKՙjMްaC\\ѣ]dD"p˜LY05k D c$5D&T-Z$&&vA丸mtMN@d2F" ^Cd@{d5ڵk2}IOOw"1L"ܑ#GbbbTa6mڴm۶|<D&x nЫW/7t/1F" ^Cd,\PEf.]x1L"\EL #!2%?c$5D&|SNM2>CHdFcܸqrJ}䅖Ggz1L"0mH$2kL0aH$2kL.0UH$2kLn0Agz1L"@Q8c$5D&0q>:cHd/haٳZj^{mN n:}0GMnޠA}"M#={;wlذT;{IJ}IX6nOȔ(K=+u}Wf͍7`]vYqqP0`d|,][Sd^zj'Nիo߾ӦM[vڳiӦg F^&ޥ"c$5Q. y 8E饗:t+{kNsn aVXX(A)۟ɡCGdձcǴqqΝ;˪kQlN7?2][<D]Ȑ{j0===..|Px Ceg˓ۿl= ۫='L_kw2,,7oެ{19M)Jqk?{~:BR_~J%𴑢ZjmٲE.N4=r`kػ0)(&l30l ⒈qAq $B.Ѱdpݢ(7*.D0 (v)ׅyӳ0]u\էNWUw󡙞͝Ε ܚ L0%@9'L>wolذA/l7CSVbcᆲ]{n0[dI˖-;w띥A,)ڐHXRM00Xpo靨奥7=%سڐH2[l ʹi:䙭XȽX]7M L0 !>|ػQ%%%3gx<ުU+7g%'ϗu56$24L SOyB`ƌG٫Yf*g|}Iˈ#<= L0 !s0pZb;㙳˖-jѢL)0g&M== L0 !iӦyȑ#fN|)3ϩзo>}x;D `B&iSN.l>G?8Ci͚5 6}9jP5ua5r1_iӦx 4 ڐH2H2ewa@?*!/ON'zխ[cǎ*HPVԯF..F.&S^?M6U .h۶_hC"!LC"L-_{:I GСGdVVVmҩ *yjb9o[`,2_?6$24L Ҫ2y䑦M֭[k׮|G=3/^znٲekݺ2;tN;QFh޷ojOgڵ7e{Æ ~Ed{Ν_lذ7ߜ2|&Mԯ_]G"m6Y7h@3|p=YfMy_jەl,]TmOSLTܧN'S"5{BcGr~Hv*.Oۮ${9=4C?:t/g%ϹkDIva$'yx!빙)/ET_cW(ڐH2HӆdJEs '|O>̵k&F 4n3o<٫;r;ST?dK`g)tEɹz)Ľ{={ 0ӟt5\s'rBI=4q8={;馛|Aw%W61}ϿbBoZ!3K 6+mH$di@U9dn۶Mv-ZòĞO^{5Og Su͛2$++kҲ{N8IIvI\^-dc l*ǞrI&5k,#NիudxmFIx'ˑ9P}V~zirrrd$yڧ+p$D\ڋQ?LBx빙)/E,kC"!LcILU吹qFY߿)BmٲG΃ ~PpB՞(Y_W_}l :?j8pl*lɒ%)QEȔ~%[ncƌq*~RUHTy, veܸqHzsssiǏ$d撻ӮМ?z)-Oiƍׯ!'>]GuD |ňtM۫W/#n)2L0%0UC뮻֭+itS}{tnӦM*|ذagyVZ=C=Q`_~sϞ=fϞT>u]נAFr-).vYIem&GhذK/^+Rzz꩷~݃/)sTzJ$穞;wIrmwyGO=] |9$M9/ԩ]m>d'% <ҹM6Ow֬Y/#n)2L0%0UiJkĨQbF[N"wBk޼y3ې d `K*`SzWY ~xV$rUV9λ;=2/5kw&kC"!LcIL5m42̽{֯_sϼŋsrr֭۸q!Cz{dȑ#GiDB&ƒ jܹ VX;x'mKJ6$24T8T{M02kƌGN`mH$di,pƏa@F֟rkJ6$24T8`˖-?w Mv!t>KJ6$24T8l&LPe@,\7NTKJ6$24T8l3fLYYw -Yg `mH$di,po.93̚S;?kC"!LcIl_?2xܸqV,)ڐHXR{,[LO<j ̙3a„W^yoNȔ,)ڐHXRٳgܹS+LAt{ a?}v L%%X `K*DVֆDB&ƒ {U,y! @$lK^nmH$di,p '[Œ[ `K*DVֆDB&ƒ {U,y! @$lK^nmH$di,pN7=ɞ0%%X `K*S,+**ro&'Kuo<`mH$di,pNxɔ=ZW_}uvvTnI&I `B&ԚL0cyӧp҆DB& j9ӓ0ZJ}GDhC"!LCZVٜ"ad"l! !d@K?g0X,VZZd6$24LȈtrfe&BڐH2 SRL&̠ L0 !2(Y$aڐH2 9 hC"!LCK̙$LF `B&ʙ֭#a0ڐH2 $$g{2 цDB& {2̣ L0 ޓamH$diX@x hC"!LƒdG `4'<ڐHӰ=цDB& 06$24,h JKKozޓ={(҆DB& Ŋܛ.{ݛ@DiC"!LC ntߓEew]h҆DB& u5婜ޓe;77wĈޮ@iC"!LCzᇳ $[{lٲevvvqq+AڐH2 I&?ڷo/ɒ0^D6$24LȸAɻzNnݺz;Ѥ L0 !2nڵ*^*-[+` mH$di;vT I! !d@<*dvЁI! !d@dцDB& !a! !d q&M4bF|л&2d$xL `B&0\1f̘no27nC46$24L0imH$di2!a‡imH$di4cƌÇҳ:/ ӧOoݺu]'CD)r]5j4a„]6oWqΝ w  L0 !@>#GIF/EX,&9S6x:u:ݍD )%˙2%t,\P͛׭[7.^|͉wAiC"!LCaÆ=_uf6mT^Oۚ'etRR٨VP*u1\˶mVnCywf\t d_o۶w,"A `B&4޽W^H֭[Zs=w+.|ЪUN:ynTH>nӧϦM[UYjV_:\sΝsrrl?2e/rbx1E`DZl8iҤKJ̥%\"Mz?>%5xNi9_8驞j3,--U7ży:tLEڐH29'֭[ ߷]v".^{MmϞ=[ґ{_|Q6F.~n_/|I֭e xٖvZ9l%H?/e&vء6bE]/&٣ڹsg:uܳF`pMv l )-/m߾ڳglKL>|}% ށ)YmmH$di,x@DEz8p'rO< T Ex\W\biӦl%.Zo߾5kTOV}qэLG{嗻v*Sd*Ou1E`Dٳg~&O{9I2eee-ɮ KB:t#<"ФI?X5:tHnfjr͒~)\eڐHYtBN:iǻwϱkߒ<^@ 'zꕝn$sxS3 a1@ `; QfL93͐m۶oQ}əǏ?eNF `B&rÇUT5ae;//v>7pLXn޼mj(mH$di@geenZ2agAA̝;@ ۮ];w mVZdj{J `B&~ݻwΖ)}]tQnnjժU׮] d K9̥ L0 !v5iD&W_-?  F-Ӷo߾y-Zxg̥ L0 !x<.sڵ @Xegg777גQ! 9 Dw! SBѣw7pO~oVV׻hڐH2H(..>s""G}ߖ-[?6$24L *w."UV2E `B&?@d+mH$diW_}; |P&ĉ;g_~jBT&ɓe DjhmH$diY<ӌ3e u'~moS|4^G2eʖ-[O:PU~52! !dZo]U@%1a33Z `B&4{/5͛7;ic†Gf6$24LXhΜ9h.p۷{Ԫp1aæ3Z `B&lz{λhq&ұcVLpԌֆDB& |7{wZiӦYfy_ 9&l?! !d*_bwn޼I0aC. L0 !Vy$ԢN2I0aC. L0 !())3gw5k֬Uw؃  hmH$diNj/a kʕ .66ҙڐH2aӧ{GȄ3gz_  L0 !5G}t'ƚ5k6l]G?:Ҽ3fx_'le9+NxQFۻww_Te!<3Z `B&1m4(O~1c$guVYYYb]^iZWM5/II´KR\7akd&H +))#^Wk|m8p#|GVK8=j^i@nȈM&͑\)5rf͚4\kIX%.ڶl2`znZ.wy'Kz.]wޡCvi5/~o>uDm۶J\>|pSO=ueyyyAmggg/[L]>zgHŋ?c:AI~a)h@ndCwWTF~(M6)VTT$_xdo;OH;! !d׬B\C Qۏ=X dQxIh $aUViݻwwg)HW_{={uȑ.WK5\#ܹ=#~iÆ d)\(Ii˂mԨQ\p{ c:ADuKR\7aCf{饗p Ҿu֞={d#9𘡚bdo;ig6$24L#U,^X_eu5uTO>Nsm%%%?֭[zřN uY~{wQ\W^S۲{9I񩧞SqvYرC.]Z~}՘@rH KM$u2 ]NGiNPo8FtN秝ڐH2akVEj\v\V+|NТO>ř{+Vwr.Y>5,Kz|kw_z(T )?@HW_u?_ޮ]Ku]#4GNvR.Ir݄dC dFk%. Wo;O;! !d׬g}vI'egg eƍ)**ڿ-e]~k˖-=zP=yfitW 2dΝ;U DW/Pueܸq}͚5rJ-cƍׯ!GNv:^X %)PjK0`M7$}$+p&Ɏ)x_%{ўO;! !d׬.Y6jHhn]wUnɓ'XomڴIVFm֠A .[LsÆ ;3eڪUzHI+eu{ꩧ~trg|jSNrw]';fIva)h@n~T{-Zlnڴ_~jΚ5^z?J6 K<tdo;ig6$24L#F$ʏۄ7o5E;! !dӧO. %)P^vʕVu֝wywyA;! !d5fEu̜9>58a/^Snƍ2ՠڐH2a^z>P-[;x_  hmH$di~i 롇:z|ΌֆDB& L0Ujˮ]!ֆDB& Xbޅj˔)S:}U$!ֆDB& -X7@JLJFkC"!LCȄm⋱cǖyWL8gyb:LpԌֆDB& رC|@0a33Z `B&PZZŦM6u)&2S+<۷oo5Pm~52! !d„") L0(&,ivNamH$di,x@D1aHs kC"!Lcg" DSX `; QLX ڐHYbfֆDB&΂D4;6$24v< *JKKoz&g/jC"!Lcg"7'^&jC"!Lcg"9ӝ"BΚ L0Ç檜&l]{ޮΚ L0⬬VZI +? _vnv\mH$di,x@t=;;u2a۷o߫W͛{;=;k6$24v< Z $gʄ+ 쬹ڐHYh-Z8'ݻ5W `; 9orssUٳw7(jC"!Lcg"sU!"Κ L0ݻ˄+貳jC"!Lcg2;k6$24v< 8&MV>L߻#4ٓ8Κ L0t4nܸR'~moS3!gIYs! Nf$3쬹ڐHYD F3d쬹ڐHY]3f>|ږXxYg}嗲}wر]vW\qg[С}q:gڵQF&Lڵk}wܹ_~{Ys! !>#GIF/EX,&9S6x:u: Cd}3<ԩG?󜜜ĻfjC"!LcgC 9|I%tIG~%9s͚5_mwH"0g.n~LΚ L0Q׷oߦM.Xm߿ĉ%RW_}vZ8rdayyyj!a" ]7ސ y)o?` ;k6$24v<|pnnE|g 6͛7ÿN4$}=j;dɒ?]vZZpw1,d?05W `; nذaӧO%L?k׮ͷ~E5| RKnذA6^mzw' XΚ L0ѵ{^zqHbzUO0AmO2ܹsqq|ЪUN:yn+8ڷoߥKĻKrSgӦM%}srrwq<9r'Zl)==I&lݺkU- p̛7C@LΚ L0qNlܸ}vmeC2m݈c."'IȔ/ѣݐ⾞mɄݯzWXd矗ܨzTs{ 4HOs:uo_|kZfϞ-93j?~֭[}I2;4bӆDB&΂`3jԨoYtg}Сn/ܵkW #:,'(dn߾]X<+W!3}SܽiӦj'z<3ԶK:={ׯɓ{yN<T)YjҤɛon ʼ/L8~ڐHYalN9S+ҽ{^xA֭[WlZJGܱc?dov{!3NEV}>}r-;wtIaÆ x$acZv?mH$di,xȈ 6'CuO?pjٵkD_=bĈd!өﲱX̩P-'I=??_}ر7pgo)k0^?tϞ=w}y۷G}ݑ<˗/SeySO,8~ڐHYlN9s۶m?/++k׮{odɒ-[vyȑn WtM-ZHd!aOqFh7xT<om۶(?'Ox_'?Yq 9! !#B5؜4rf$P [,8m~ڐHYalA93!3 <|VsiC"!LcgCFp9̰ g,8=~ڐHYl98m,8%~ڐHYl9F9axv?mH$di,x7=ͳ7rf aŒ8쬹ڐHYP;bXQQ{3qIuoCά&CΚ L0#{wIﺆĤ'NAU?>l <jC"!LcgC>|xnnZ߫&yyy^{+p0v\mH$di,x5YYYZ5 6YPP -/+p0v\mH$di,xMݻwnݺ իyNq8DYs! 6^ەW^)?ovo'8c" 쬹ڐHYPx-I{no'8c" αjC"!LcgC-o~V={jggՆDB&΂ZV\\|=_La"쬹ڐHYPw.ZAf1Yv\mH$di,x}U+4!2Κ L0/wyg|AlwA,X_z_aSl5쬹ڐHYB%2nƌ Po GYz#<"1f˖-ޗ3lj65W `; ^x[ӦMۻwwe:x`aa̙3=}k 쬹ڐHYBb_K9acǎ---ưAm ;k6$24v03gM ٷo=Sq 2x l{Ys! eի{9 Ƒرck2l5;bgՆDB&΂Y|ͽ]P6m9swTRS6v\mH$di,x5+Vxl0׳>yf8$ ¦Fm쬹ڐHY2.`>qPI M lYs! ePIIɜ9s5n̙s S5쬹ڐHY2_ܰawӭ\r…ѐ6 ©Bv\mH$di,x4}t2 vη0lZjC"!Lcg GG?ƌ3!m6x≲f͚ zwWIvJy5jT^{z%½l;k 5W `; ^M6ͻFKP^{EIԤjd-^~A\ҳKr){;@u6+ZIIm>5{^wlԭ[cǎ6ȨgOHu쬹ڐHY2(i! =uDKo߾} 6,..:p~?߽c2Ysh[Hjk֬Ih韷ۤ<=C,5W `; ^U!-l۶jРi6|pwy׫Wu֏>hj~֬YuRNB l Ys! eP_~5הJ<ܹ=ܣ/nAڷnڳgOZ?ju׮]t,ŕbw޽gϞ.lȑNs6&~Y`Hܕ+K.cǎU}4hj޽p,X JQI<~H俞EIUo~K.$gks("όl:tV/}4xr5F%$?/yRοۭ޺~C%{L2wuW^T{AԹ~˃7ndѣGꎁF(,F3-dgՆDB&΂AM %%%>suW^sׯ_ϟO ;wSOŨp ru%kܻwo:u{999Ns6&u$꫉OºuvQ$_ϊ+N9KasgRZ63[oռys^SnT9߱cS aUIPo& Y3Tc㤓N Hmhg z%Qsns l Ys! ePeӂYe7ݜo杊UE]$ ֞={ʒ2ս .@(}$oSO>dF5k6}tw-+뮻Nr[R4zwذagy)ҪUzIrF_Td9Y#7mtرr-o:tRՓ]wu4JgҤIn) L)U{E^z%W,;8A Z\I2$ss{<${PہφϒsF=_u쬹ڐHY2(uZyA Fb(ZaBƦ5W `; ^M>ݻF+WZnywwz{ɰAhUg`[Κ L0/j0-,^8''nݺ72dHii)3g􎆴1lZjC"!Lcgˠ^z>.`˗;ѐ6 ©Bv\mH$di,x~ڻRz衣GzGC6jl Ys! eք >]\v1l652mcgՆDB&΂Y+V?wsM:СCqPI M lYs! e߯9`7x;am;k6$24v/Ǝ[VV],3<}aفm;k6$24v0عs>2؜9sfϞ}᫇a;v\mH$di,x!qȑ'v)..(j*K^6Ȕ:-agՆDB&΂*EEEǏ'CQaÆ9sL0a޼y_Q ԚƳjC"!Lcg X,6mڴm2v?ۿ[{0ljed`Κ L0`C0Av?mH$di,x€q siC"!LcgCF0CdO `; 20`"~ڐHY 6dӆDB&΂`U8ƍ !2 L0`07n8frfu0Av?mH$di,x[YZZ*%%%`"~ڐHY IL 9: ;6$24vM4i֬Y, dӆDB&΂`W͛ٳe˖6*%Lş3Gmڴ8p`ݳ~iw ;6$24vl:LJ)gs]xٹ$0s kC"!Lcg" ~>`VVV޽6mx>t0T]lٲ[nM6#aSX `; QLHyNΝ;|={Tlܹ SQ9S>PA vNamH$di,x@D8zΝ=(";- L0<7T4i$??ԩ[lIwѫWDhC"!LC+--b]J?g05ȕxw L0 !2"Yل 6$24LȔ9 3hC"!LC J3I06$24L,$a$ڐH2 s& цDB& aruH06$24L əL„a! !d@x hC"!LƒdG `4'<ڐHӰ=цDB& 06$24,h`qƍwr'<ڐHӰAI3fƍə006$24,hi*aFii)9Q{2̣ L0 DWbTș:ޓamH$diX  hC"!LÂQ,a*LD06$24,h9Nʄ3Q'<ڐHӰAfIbxܻ# 'T6g5ȕȽ;Z{2̣ L0 d\QQQ~~oە+Wz}vT̙r^9\\wPxOy! aA0x饗5k֤I>}~T2a*)rEջw윜e˖y{dG `4~:++?UW]զMNϙ˖-[SeI ޓamH$diX ~&2l{{t:˗/~BfYzigNP"!Z1L $ϟ߽{on}8gsz dB+FѧOǏ.fs6wP !Z?~T?x,kL5CRЊ'O\\\TG5cT9;T[( gw^3S.L5CRЊƓ̇63TdP(!Zx'۷L 3ռw2( h|]oʚt3S,2's[?JȄVf2Nwwwsf*[MTUJ!dB[`0|ŋOgs:@s*gE( -{{{-ϩf !dBd< Jb~t3I  Ѕx귣s:y{!:2&h4yi5TR-YV , , , , tB-= IENDB`onedrive-2.5.5/docs/puml/downloadFile.puml000066400000000000000000000030751476564400300205670ustar00rootroot00000000000000@startuml start partition "Download File" { :Get item specifics from JSON; :Calculate item's path; if (Is item malware?) then (yes) :Log malware detected; stop else (no) :Check for file size in JSON; if (File size missing) then (yes) :Log error; stop endif :Configure hashes for comparison; if (Hashes missing) then (yes) :Log error; stop endif if (Does file exist locally?) then (yes) :Check DB for item; if (DB hash match?) then (no) :Log modification; Perform safe backup; note left: Local data loss prevention endif endif :Check local disk space; if (Insufficient space?) then (yes) :Log insufficient space; stop else (no) if (Dry run?) then (yes) :Fake download process; else (no) :Attempt to download file; if (Download exception occurs?) then (yes) :Handle exceptions; Retry download or log error; endif if (File downloaded successfully?) then (yes) :Validate download; if (Validation passes?) then (yes) :Log success; Update DB; else (no) :Log validation failure; Remove file; endif else (no) :Log download failed; endif endif endif endif } stop @enduml onedrive-2.5.5/docs/puml/high_level_operational_process.png000066400000000000000000002413421476564400300242310ustar00rootroot00000000000000PNG  IHDR@~&!*tEXtcopyleftGenerated by https://plantuml.comviiTXtplantumlxUmoG~b/TvT%WI!_ws2{}f0`X= 1;/<3B\u(/}œJ`)~{7}Ν sw]]@љZ쀋Q8S ;ϑJj+W"nUwܡ-;- (OLM+Lds NiӨ[sa |Ÿ!ZSDĎ"ֈ e:-)zRB XH4<`'U &U, G>(1 ƋiJi;5y>yQ~z2|(4HVjVּ=T?P˯;< 0kc1ځ#&`4GozK(8 `As@p4 + `As@p4 + `As@p4/?4$$d({| /,c̘W,RDχW 7lᄄ2E+TH]kyt"BCCcbJ|s~~׮#ڔؿ?>t6±y.]MN.G/}=/_}\ڎQ~񐎎޽0]h?8^~3I׫f|/ԒĠuZ򇸸֭,Yo1{ڵtS_~9M7u͚Mt֭~k֬SR-[iӑhذaK||u꣏>ߴig{≧X{_ylr `p)`򗑑)=6\ ugmj. XiM~(M§|{VS^Vw"K/Rzz)[6iРgM%Ğ=UV޽`̏腅O@d3f<ɏ(Ԫ5uJȕ8ȖۦӨpr-J ¥OMm̖SH<,ByÇRIJO=5\.G~w84))NA{ ^GVL_|j}Z#(}]Na~ʦi/]Y>|r*p-.]*+4jԔ ye߼y׼yKi( Ko9A͚.Rd*=f-4"##'On^8S4 8̤p0el7/z2t+\  +aL:bJ ,~ƖǦM;?4K۴xKNDuo9RnܸIɨJ%{J_GO>YMב.bFO\4v9.nxx8}Xb4&LxdRֽtbHUu׽:tfSc5QFntD|@_Rhviiw :B"ͮ]GHWzkQ9߹3&ǿ+򑏦\k9IzQi-as}SlQ|Jck5KHTm}ŋ7-[])`^8:[ntD ؜9'%%Ӽf @C]Yl hdTxSΘ, 11%ȕgfLҋ}!`P +` `pG( 7h 슀 Q<5vIf`܅yz1||rT;gɒoڴs%uԓgƒ%Kv0i$^Z?PM]`9TVo'Ej/-ѯX~РguaM.!R`倀MVTLq =8. `'LR(q/zVַuPٲI,3FI J_S zߡ8k-U-cǾ#' qq_tt &j-HӮQA{Г@#{ڊJ 7? ` X0>? ` X0;_oң] 9 `Mz,FBeV:v@_d_8^.ָ%￿ɟgnl4Ë+LwuyͲҥW!00H&G.hxvk1DzJH(3dsl_눈23g(U "lϞܘ3f,lڴc W@{XXXv)mT0<&;ɑ2^Zc$`={1%A_Z좔`MN.w`y'>WCCCXavRގ>69ڢK+`{L=VBҕRYw{_0Wv[Ywzп^sQ}@l;pgCymQ0=F)R N^y>-hUNu뾗&m3@FZ … ɭ.22 )"h_t]Mvr,nذ姟F&m"`c4b+S +[ѪSjܮ]Ǒ#_+Q"Vw#>_0WegPFZ8j֬m|DP,Ǎ}ҿ+Vly'۷pmf-JI,ZEk(|-&~@_{Õʗ*jdL뀀vh>Fc~;F9s>&"lܸ9Mپpg4) {Q[ !`Zݬ񫯶8&G"$*0Wȁȇ X0>? ` X0>? ` X0>? ` XB^|q88r, /7 9^}uzw}F ؘ1oܙ<|8>%Kcf-ԎUuጌq&`gϞ}7_ye̿<^}ŭ @AG4z &`_8 ہYߐa;0`,<Nv `Xx@,8 ہYca;0  @@@$0{,#4lfx@,8 ہYcp"#4lf|x@$0  @@. Yn:0f=H:uF-r:Yzȓ4)F `0ɉbWhݻw)ve˖¢&C׭ԩS_~-*VxkӪ07yv `0&6n8X"; 9vٍ7&M Mfb'޽{ٲeXLlNt[ˋMBR^PNEkСNHH/L̹L=^zVV(Sp%KRʗ^z-|IS/nݺ%J(RHLLL&M>SN)7LS*zHNN~w(hksB.D 7p ]#FQ DUI&q޽{=FcǎQ^2֮]gϞd)|-믔N[jE۴iUٍz6~xOo߾}(@dX')Ov6s.v%))J=Œ[oN;w̧f"ѣG>}QTdd={lȐ!&֭+Gr\(w sT"SE+`_}˗ӌ⍞{ǎE'O٧LBPRrdJ@3 3fڵ>ug4ejr!NS|ʹX jԨk.$NR&%QaÆ xժU7o永Ē%KHO:ͱ3g30xQxy=6*M)D ݹPtY@F/psCٲeyQZi@o!@ /SsJE Bo~}LFpJS 8g|ʹ#F\moMaOE^><ɓ'W 430 7.seRR@6s첂@F/Dq.K*&>LYbO5yw!۶mIUnn.KbŊxJ9zhBĂ;W&=u]ݼr].9 ebbbȵyAG~rFa 0 O33)$N0AԞ73PnvY?!wP 8,Sx.s!%8iӦ {@^>'\XFzݺukڏ=F[n=z[iRuaRdI*wԉ5jgp/2228VZ`O_uY˥77YYJH+O/_o> 8q;FN?mР%p-Dǎ۷oYJ6p@%-z5kva ݻ7''Ȩ[ˁ1W0EOFFp׺tBK.u] ]"#,Xoڴ ,(Y DڵkiJ0|P`׮]r…Y[O:{n>K.-r":y$uV9VhT˱c&NHSCXhҵC}tm)%0r20yIϞ=Y€}ũJ*QDtt4ܿK+`[l-87|#y NcB)+f0Eq^Q5cbԭJnٲ%٨Q,Nkm5 0HGl߾< ?>/_f \M/Je/K4S]ɖe-Tȑ#]10K$  `Cʕ+7nXONvŊ>tP--;kt'Ny 4*0ܹSծ].]omVQQQtڡCI|5֒M˗/Ex72F10%=0+K1cl3fLBBBttt 233Miii%v%Sɓ΅})]4^zFF ƨsΉ7t5ֲ{nz񯅺F% `XGi"X 8 ہYcp"#4lfEGi"|5BErD[/kG7 00xm7f2@f3;v,11qٜ-[T?e>)ʟL2f`?Bt7iѢ?Ȩ+`8 L5Q FԊ ԩSgذar+W-[vɒ%X%&L@,=hfȑժUŚ5k"#fɈ>0HE.XJ$0aMxqKXMFNѼys^v GLdn\bgQ.J_Dd4s}ꩧD^]AioYDw,l2Bu:tHJJz٨Zǵ(0H3Ei%{Mkth>CyFʕ+1cZbt}M@~47@,b4u7!^zo֭}„pD'ťQw? N:E36ڸagQ]5hIKKԭ[gqH}?r]'|ʕ+\ #,Xd&:$Z;v4׿jԨAb&\ц#>)i B7n\hh(;zݍK{u] OY(gri^HEٓDQ%&i@,cϟϿw,صkwڵk0@M,ظqcʕJ*, ` H `{ѣ/NMw({Rx9: YFwE,\iK(Ѻuktބ͛7⎀Ux*Hu `9s&==vj" ?> Q>SA4=~XT$䫯 {,YDl)RL.a֬YժU ˡM 5FDPWFُ=ڪU+. u `o=zhܸCΝ;S .ݦo#K;3)G1&47J y @ioYD{,~U򞒒"Y?D.YTkKwk.#Ӗc#?.\| @47@DH•+W6m/rr.%lKMO0K78rcdb^Qz>(ڴii0a**&y#ӛԥFR9F1ͨ}zΜ9j@8\: )0+WL2cǎ7xƍhp9 `Ec۽{ *$%%URw倀h[n;UO7)Kѵk|P/ `YvЭAm2 `9}ԩS5jtM7UVD%99χEﱛ7oׯ_ʕofh@,E+bń4,%%: %0Eߧ8L".ǢO'@iOYcp<Nv ` H `XGi"X 8 ہYcp"#4lfEGi"5[N}mNSA ;FRF@M M40?<@@,)0XBg|-`vE|"@>k(@,ƫ&NFqժU)Bc5:@ϫf]n&7 `"{>}TXhѢ%Kۗ,Y&Ga|[;VT)őuz,P*WLͫY&_~af#['/rJz){ٲe˔)C7;;8СC[hquQ !!,ӧO=\OT+++,7tvΞ=qL6MW`ԩʯsB:e;trʗ/w(\\@( F 66VFDDT^/K@\c4#Kyw_N[JTɓ'II<}]&e8,i⋺uҠ-RHLLL&M>@øq(c||<ƈ(.S\4~ph0ƚk(~ˠ^XoZ;vXժU)\v={-믿\4)&< YҸoN333ENK.˖-O>v]4iҤwT r3ӵo߾CՊ\, mV6mű<s]kv7oj$%%8s{@C ! (GZfMw>!+ҠAΝ;s@b٭[7aQy"Y%5kV=hIcDFFٳj+^>(iA(@U({\1 -|, R;#,7xʔ)yF!NGZ|ծ];Q].۶mۼy3қѣGE祺xf@0%J2 Ӆd” <[o%%hl)@rRs}'ەd$4J3X0!['gZBHrK}̕iQo<`yW y}pU<8D3P.جY3 X"M+Ux7i&$AЋ7n\DHua. _м\N{EѬwժUS1G<#;v,9k~7&TO~˱W.&C70z \y[Q=[dY<X~آ$:E]I0GRR~`x6`kg`e*ԽڂFH>%bT~5\|Y5m۶+W|;%%onܸ0ʥu֭^z,5jkeˊrܹ8<#|"YU1q:ubI<(,_~_~8zhVjժ;S]nnndd3M4233_$̔8~?ر#44tlTzW_)RDkOÇ)߿_6~' ˖cǎ۷?{,Iw1p@sdpKrÇu]ZhrJ $K 4p:yy J}(*&Xh޼(믿Rjb @,=v֭I0d7|C3e ;u%ؽ{7iSti1zz)_%''/ZHD :Td?4O … ݩNWd֮]KS4텇pN6@ZlDQ-Nx"+b9BN9;;[6 ,..ΡqBX%J(Vu4/[ĀTJD ~mr˖-Yŋ*UGGGS,)e^~夤$9sʜ>}Ck`EioYh,֨Qcر[n;wv8V˦ ?SV\ F0p՛1cvA˖-iE{cǎ͟?ҥKtf.G7"""\fcv\@b4e `1 q޼y q̘14k)5x^z%4jH(Avho9³j*̬]vѢE#ۙ_x;uؑq BY݁(sN.dtb -Zhz]HRW/(in/ 1r20իW7i$&&|z'lٲʕ+GEEL6M(ٳgStiիgdd8 \s( )lL>{NKKӮBtY]ɓ')pBB%nР)VH;t [R'xBĘ8 ` EuVҀ?P+WΝ;Wpr.V/͝3gӧՈkqi4}]ma -[?Gi;v76nXZ|j@"11kٲejYrerrrRRRÆ NrB@V|yzˬQFJ0!50 aDŊoL|"<@z'NTE"e޼y,]r7ܬY3i"V/r5nܸjժO< Nv `XȐz+WLLp"@qnAr)i"zH ` Yca;0`E,#4lfEGi"ӰE0a;0Xw߅\xg7nYD:u|"ի6mSdvڵKޠA+WȖ?S'֜0?Hƌsҥ_~%Ϣ̙CtѣG*Uj߾}"{ҥgΜ)h"`/_VM@!`?~75QF^{Mtq⸚}ҤIʕ#mE8k֬jժEDDPsbccyKdddPQ4 LNN78{wB:JyH8qya||X9ETNmfT8~KX"wBBB۶m.]z1Ô`R&'|B.씷^z/ XΝϞ={tر}tJqw 8dW0E> ?^N~$`d%aDzӧOl{vQ́+B({^zUT, 6ܹs'lB$ro&44TΞEjg"`{SNQvO?sCΰ'9sF+-Xlٲ 1ӵsC}=j ﱲ 9Bڵk; dk׮&&jٺu+^DŊ)j-[nԨ+ThZMem Ú&@,b;9"Epn;vۭ[zqm}]wu&Mr)`tzQ9̥KF>2e843030]c=j A5LYXTͷx̙ݻ7lؐ,[&"Ucƌ)Y={rС43s)`DvtkFHVZEQ4z7EΝ;dĉu2dֱcGj۟ym]Bt %֜0fkc;3N<٩SRJφ+Wlܸ1IW۶mw!gjqلwRӇԎʬ^zFF.555&&&22^zO x4 Io>.. m]Bt wx kN˱x9suEDDC+~[p4@,b=vӦMݻw7n(믾}6o\60|րM222jժTreE~Ǩ  PM:uT|yիk^lf1PBY'z@ڵk+Uĺ0|4{o[.bЂE"eYnFh@ "ڱ(&d^X Y׮] c45 `q9u'@ť<Nv ` H `XGi"X 8 ہYcp"#4lfEGi" 8 ہ3F/[Nf[  _5j:0`kڴ0|F&M `0` <| 0"{, D O `)$+b!` ($VHF~H!0@5 `)$c"d,B@>րY|5EVTiРAǎS1'޽{t钐Sf#FĻwӧOŊ%KLKKOD,MQ27|)SDB"`i'0`Kkٲe߾};uTX1:رU]ӪU+:֭[իG㨟\rNv Nuڕdĉy.;g,Zh.].ҭZaUHԡ{ӧ˱rʕ+^tJF>e(y)S8|e߾}9 |rJPD 3fڵ&1Fv.Jh'|\p=p^O;#R8 f8LE5i҄"pfrf(k]4&ͽCy.;|(Lņ8#Gey 3|A9&8pرcmۦľ[Rʀ0R ϧf /E)dff9i B ۷Į d!rѣG7hЀc-ZD2eʄ\;۱c'Xx\_|AN: "ñtz!6QGY2[ƍȧ0sʷ~Z>(ݺ5a@/A׳pVby*@o|J i1,,CI)>77%bŊ_ J#?Sͱ|v֍BUp&l;g:p9yѩ.s &30)%ey'D|ae ɟtCA7CCCsB:X=5jԻw֭[1ѣ,l"=z[IX6J,٦MJL3֌ &FRRJz╁?3Eܹ]4yEQׯɅ+(ԩW~ZbŊo`4o`]n"`uYk9dG!q "d,+`Dfffݺuy9w'ٳI/N/*G}499QQQu0`M(1%OIIY]f Hц'x^zycbb7oN_~!a+_}PO 6 ۷yk#,[z^ ƍy  ` i@f_u'NF7 +b8 @ =%Feˆq޽&/yZ4[liԨQDDDddd5>CwӦM#P`ʕ"K8p|*b#GrwG`َٓ 3{:m۶\?YLBf\|Y5ف;WNSfM ?s3gŋC-W\xxxrr!C.\ bccM_Ü?~РAIIIjժ'OfG}RhbŊldK VBӧ;V1zRJ?Z[KuQxW\4i҄9._NiƧ\B%T]QI8+VH2rÇ/^vb oQ8201{20z[0o߾CS2rJX.k׮P(Lĉ"ׯOsp ũxU{&]w{ѿٳE|i(IOOX24;vHŋ}ڵ4333/sz N6-##cܹUеT\i8|wi\\?v]QK.'i_e.רTNg̘1a„.]8wvWX}#0ހEcdz)`+W0bAF>5rOa{ҥK9FCϯi-O'OR qTF zr8eHN9rD.Ӝ:u?@hh+ӧiT~}~Y/0aĈr2lk PC=4jԨoF~ܹgR **.5Fal=fO?I[E:-Z(r9t)|q>2e څer5+P{pF_hn$%K%6,#ŧ;w8O+V>,D"~inˊ>9sfiT_|k: +,u7jyٓ饀gϞE ( r56mڄ^?hԩӦM\}%{ҥ>v'|R.+Ǐ?o<˗6l̙3z)2'yGRR;C[2e8(@Ŋs"ƨ?ɓgϞݢE J!br!n^a9[Qc{̞L/'U9/8y,CQ|PT/_Z.^Hoaaa$T@N,sҥÇWT111s! y晔Qe E3fp{po"ؽ{wzY7o. e4!.u+r5[&&)fΝi$ E\a9[Qc{̞L/̤Z^gH:r9>lƍ#""ԩ~z)Wx^~ԩSϟW5k4k֌@]?jVCwoT ,'ӂ{ _=-RC?d,FE{СCjZ!3,'3."qڿo>5k:8<f;O>A^mQ/f,90q-`]ЌE0f;fꫯ8y <\TEc&`#`@pq<8lf@YF?/^ҥKj'(=:..,Yammf]芑{)Ydݺu uE5/n޼Y#7nlܸqddd%ԩ#zM%j׮ݮ] 4[B?y=® cWEL]_u=z$&&߿_M*a!~#Ә1ch/4}Ytt9sHΟ?ORJK.=sL@{¥[".LСCzΝKOO}Gȃ9s<.dҤIDfͪVZDDĦM>馛nm۶-]VZT`zv!rQ8hѢ5jP St,YDL2F~' oJjdK' +WN"`EȠԤdQ..xĉxf ko]%0ہYDwk#O wر}gϞ%͸;$YΝ~>%iˣ OJJJ׮]i&M9@RqiJн{nͤ: DOBBB۶mIP;&>|(O>g/ ؁(zj2RI5d2ʥ# ufu6lԵt"ہى۸qc"E(pԩݻw?eLȧb3 6ѣGixxH&L_~='0NS0ثWJ*8޹s'lB$ro gʢ9%4M874AtǏSme˖5tzNt=mݺbcRDbŊ'"9tOiU ȑ#4] ,;;[N8;Mh6t-^e˖э52Y4LQFv~%p]OױcLJ~\"M(iI9u)`Fx#`l 70vX9[nPξo߾뮻nҤIrm-.]9rd||,L?ԝ r8Xى(im۶Devt«]N-d3I@ZARN] à:Oo&ͷx̙ݻ7lؐ,[&Lӹ q̘1%KܳgS:ffrfr˗/ӥ~7:dmNKK j'֭3Ϙص7ELq߁|;{l>}/^zFFϿM<( |ꎀV'㑀|ɓ:uJJJ*U/lrƍS]$]m۶Ւ"(Z wJI{TԼNVQ޸8Ϣ Xa;0hmuEDDV,E ` W߾}7oF@lf̏?հaC{ @f;0@EtqXa;0?EYUEV&@ #2qFzͳ\=FH `7p~^CѣZU;#Lw96vdzu%n]@!{ `1Za.iii"o nw" n2z2,q"y]p! v'BB\&`Q2zTH d.|0ey6/ v'L @dF򩂑$SQr @pEth?wh&d݉(({m"*up@,+`80e˖"ojjp۝e&na\QC@lfEq)`&ӮBD.S7C Q2zȕS8 q 8X@=0(`@,bt"ہYh.nBOi6OFG)++Ks/Kpg 46>XFo;0 ?_2m۶vzmiB,dF憆n޼Yp %in;@lf>FĖ&Y(BkcA).F %(`Bf;0XrEFF&''Bt"1K{RRR)=YτrEjԨAo۶M%\?*uwlMFE\}X.à%^&33:EyvJqm'֩SGb߾}aaaƝ6mN&O%KYL]6*h(%(wPwlp &wV=FwSE޷oDj-[5áP͟Etq(.@ki]O?qݽH|?y~ZصHRҥӧ/]Խ{nM%GWX_ddYt\bt)-6˗/'yp8?e_r~-<|2fܬQnvYF.hvAI;͢S͚5#9޽{srr}(t&=mѢ?׍dv `vC%^vz;FFOsh/#4M[HR_^j+5*/hŨJF] pz}m֮]Kodzz)_ѻ9(]Җ,#r9ܫQֺ,`4~t$ӎ ;d1ɓ'ɾuV3F}(tF=*>#n#`f'ʳk_xq˖-5j뾌"Q pi.7mȡWIbj+v,0%QߍjTN.QK7LjjjRj>KMF!J@˖-KLL#-Y7ýﲌT]vO72FwSbSQf8 ]ݨJ{WbXSktȑ#{ɛ'm۶^9駟*\(i+(b7{a1F-Q7"5k*^z3f̸}YθY XזfX񣔠A6@]Di漣Ũ<?3FQnS! l=0;1z8=;;{ժU/_w7xެ9^$rFQ0vܙ^xӖ!C;K/D999 nFO"GWn22,}7Q95(߿NWXAChB.V왙k.Z={d;fQn6ckthJs rQJPn Y͢SC4btPE&=m޼?|q8 p"^ԯ__ikZjjjLL /?\ۋ|dW/\/֢*u&V-[rQQQT˴iAҭi*MF}XL.nKp̘1 Tu H0b'{9JFQ6ʸSd\F47,m  w;+ct7Yt{p^1QM7tPE&=M fhh+W8 q"m[xb>υȹϝ;W_cww̟?_9eH.B~^ cҝS`ϻi/vT8Pv)@lf?o"11dٲej |qFTRJHv `)@"0ہYDwEHقFm X@ ЁX@ ЁX@ ЁE v 8lf]F 2zہY@,xv `q)`O3gNnna0@,btSN7xƍ8p`@l}IIIIJJR  ДnEjz9 `>prt<@>@=0]VV6Pz"_ӧNZFJ*+Wz0Xa;0+b7oܷo_Ұo199ЂeĄ,)))%%"ZЄ_~>;0@EL8 `Zv `Pz 8X@=0(`f,xq". "0G@lǺm۶`W_!Z_Swx^.ԩSϟW@An 5,^( fUY abpق"2QdqDE(A #E@TŦFd]@ !Hd&eSNo^UN{>]+\!`uNK?dJP@ )Q/,طoXK#ց: 4.S^boTFVV^X@m` lPNM,PP^aG,6B+ǾM>= 9(/P`S}u=0 L90@n30@lM7lP \\g\<4_M7ŋŊj Ċa-["""*** o@T]D7뮻+)৖ 2! 9 LZ75yC0Yii;u$TVV %4I ]@o@lbCHd_~ezx ڶm۰aCJ?~)2AXzf ,h۶mÆ 7nZ^\B[ 5 My8S(--knNyxŊ޽{}ݚ!dž̞0`-\(ل xѣG{wwˮaN:Ek#FP[2 իW1CoP N!ˢ޴iӾ}.[رc\߄#I8HlƠǴ_pfِ!C&FdizxyC0pcY%{\p«ZVvZz83ff YyIIIhhݻ[]Ѻ\]C17hЀHׯOQZmd۶mdsY)Ȇ,:3jԨ-[MvvΝ;5]6EvbƋ-bS$Ap2Z33ۻw޹ŵ2dzizxyC0XdÕ+W~'PJX$ʗ,YҥKHj~%YcCfR?ϒd٨Q˗/Z+Z/- țcٳ'5֭ۺuxq~9pXat ,*oЪb%EFFT 60M5 j::cs>jkԉѧKqqqL%YA)>%%>ÇGX!{&-((`;׬Y#;v=2?n޼Y@QQ=z"sE!ŋ[w1iVwQéSiU{ e3'==}bY YhU˗zi:W,=q8^^PY\ e=p7G\g\<4O8SLѣGxxMXɞgZ:rAϰI>Lϭ@֐f`t17l0UxCNo֡CbǮ=&MZBQ0>s̄>`Si=**jgΜѪ6 d~'YEK/D}ӧG]999MJԙƠV1gY\ MrqNdK`zxMy8*f{;w{믿^B1cFӦM###`Z"٣8|hLRرcW7sժޖ=:>>,۵k!sED mTzǤI4ҡN:oeZ"=ӬN04tȲ4y'NXTT ֮] /oItJW'??Z]s5:uIͥɡ%KPwItFZ1{:?x*?NDc=ֵ6y晱c>C,ۥ Z4ýayy*5\xBCCҥKUѢEi5uT+V%Hɬ-Q~} o*~JR{`bQyxx89s&)N `ʱJ4k]kCh1r9sK8UG "ATZ=FV.;߿?ӿB2e-BzCjcccCfiժUM41?6 cFuiN@bG'SU_Z֨Q#jBqA{/X,ѿkWW7rX^ R-Mx֬Y!UKO>U=#et/>c2cZhbW\J[;Λ7oؘ1c4|>;4v ʏ&@lb^/`Aw}Bm^^^JJ -gώ3piii[nۓ4nxǏ,Xжmۆ =zO>wŒؾ};L+V` ޽{}Ր!C穤W^ԜVEEEYYY iڴi߾}-[v1n@]^?v%ƎPTq0@l⽀֫WO_KQ~ٳg>}8qjmsKk׮EVDӲN˪E>ݻw5e˖TsN*5 [mذ!44T|ݺu#2*?NM0 ,YҳgnݺP IQ]MU@'S&Ʋ@D7뮻+5TUXXؽ{̥KrիW|QQQM4ׯ߮]ͳ\/1&;v$''WVV &Z#H6m6nܨ/񾭯|\ YEEE6mzCH 3TeU~L90XÇԩ`S{ KxΝ#q1c=_|?sfGEFF.\_yy9=۷7z駅B6śk,r)1luۚPq1d~7F^vz{VTq0@lbC_~ezx ڶm۰aC)M\\y<{1ch uV*!U8o@T²@*hAaLһロH'Nm۶]%%%wf1ʏgϦ76-Ч*r;jԨ-[RIvvΝ;FtJIPjÆ uITV]th´F*vZZCh0-b y\I&?j$!o&)t0x`*ٳ'\ׯoР/`,<"*kOf=dve^"CUPǁxh~!@]_`LF dʆݺu#9azL;##C^8p@o XszؿMLXMz]tB]t̳ȍy7{}w*kk,gW2eСCi{wƒ_s5AAO#/l:!F{搝rzS7 `*1?rIӯB.Nbԩqqq  i/_|E}>tfftri "3g7HMM* ޯ=B6SeL-.7 U@ J(15mڴo߾˖-;v옾jŊӪww}7^s5,Q?|^qNN΀( =A v)Z1k׮2c:&oiXTT9ydfCrˑ#Gx޽$a瞳y{!$O>iaOOWc=z`SeL-.7 U@ bv=jԨ-[𳳳w)Zhڵk {o=\r .㒒Prl>AC7hj۟I`׮]wMLL'NPmۮjv5̀BxӻPTq .߀D?CwFF;]dI.]bbb###ir.]D+WtO?ѢqϟBʌdиjҨQ˗/ ƕ mhGa6lܪU9aj3K'fƀegϞTҭ[uiͧO3`j,>TeU~6q(fo3xzeAAAf蘲aJJ eÇ?#aQQ=zTLĞ {O_1daGdN6oެw"3V`pf,ASNӼOfXv!}8lW&S9{/r#Fdggi6hELJѣ>N2J7m]oذal?iêUxÞiUorrrˋ;v8i$f3h Z<^60sϴja2'eUf2d-wX'N?pUee%-qfΜ{O͗052TeU~L90 =<8))(&&OYՌ36mSg:vTaԤ]vyyyZF ;v,S3g ӰeFMh܅hʩwܹsef/ 1""֩| ݻI;itaaa:ub_!j^̧ [\ozw2*?NML PTq0@lq ʏ&@QTq .߀ԣ*@\<4GUPǁxh~fĺF '*@\<4In *C906zp82*?N  TeU~L90@@UP @  TeU~L90`8lP, *@\<4-N:p¢"B0pTeU~7 `rJ~~A:tPXX(V+ ʏqL{>|x͓Zn]{AQTq .߀لeKΝ;;HOOU `8lP&/_r1"F `8lW&ƍU'$TeU~L90нxԩyu---m۶$*]wV`8U@'S&{qӦMcǎmժ 7 @STq0@lb84,55U@'Mʁ-jp82*?CڢV' ʏq U@>o<иxh~v X8S[vῺE< 3+/2$)@  ?<S&0 L90'rܔgC950l;W,, 6,X'X u`_ @lM&@lm6z@lL9 M8pXr @- VL8Q,9IÕ!7 p,0  @@@lHʁ $ @l{Hʁ"'4 EO i(fr ` HB֭[7U5>Xrj/i|gb B"n oy2n r.Ai r `Ws?0+@܏m={XQ /UbuEmҥK۴iS^=W_ՠf%c@܏ލ%`ǟ{_Jyk愠x㳳MVTT$I5_UR5Җ-[w;vh$TzjjՊf''VB?~|j K;q}T1hР|{,,yE_Üsѷv :ԩÇES3|W{UaR~|jOX̌ `Ǟ͛7>lTAtN2٠A-Z̟? ?䓎;FGGDEEtM~!G-[_g$ O?}뭷6lذ]v֭cw1bDbb"7k,77ܛ!௾I&T2vXVbk&3 &̾K-[ֹsgRε^{rd 9>S=z-dްI6 Y>fvSµȑ#Ó)B}+Vdƙlv Vwvj:Q?Q +dsˎ1!oM6zT*; ,x3yW>$ U{7@vΜ9a.]z^z=3T`D=IUdgϞRyGDJJ U=bE>eaB6,))oF*?vX6m4##yXՏ?(tZjĉej5g+cfOgA[b>}_Ɩ`{%'!1ZZW"Ws-uoXd: r_ iq#\kۀӷ+&fFcBBtLdsˎv7i$*ԡC:g0 D"Ík쓀WNN;[EӉ'|@ 1)0ov#}׿UFSyxƌ!U4:~C>Z b êFY`f&=ڋĺ=1qڵG))))5L=4WAOƽa(,Y[}4^ktJ{:/k셙0[H@hNM'h\.[v,̌xJڵycǎM6Q-/= dX$7!x'1f_~A$?r^g#yRO+0 0 SDꑝ{A㏇-3mn݄+U )%VdмysVB~zݷoag &⋔AZ~0l!¢ӥZ1LXjv셙0[HU a:QFRObnٱ03z)V!Uijʔ)t@L} >DWfiac#*ZV?*ZhoD[H@ҟ Uj֗%[1G(4vG|GTH,O?T_+#ؿB ܈M-[ƞ7> vVЭ[zn{gu!Uv1ۚq믿>jbNNΨQzAϿ§, S:>z(K ÇGG#GM5UOC1-Zu]oc;0$w`(!׿<`cץTRkްtkM?fԨQbȩA)NzAP@$0 &_}Xr4K/$$ @l{Hʁ"'4 EO i(fr ` H `6 _uHHȅ  [Q薹xX# CAK& 'X"hܸ]wݵw^ѨNQz(tQK3,0M&@lbz/\ii;u$TVV %Dm"V덣- 8Ӥj&z m۶aÆO7~xjgϞ3fLBBҶnJgΜ͍Uȑ#˙q^^^JJJXXXrr2ǴЧ۷oI1PǏ xԘ]x1@:wcf, =GӧOxxxzzY8}4^4ϙ3LoJU5&܊j&>㏴C )++;<ݛݹs犊233'O>;V~ٳg2ڧ֮]K ,k>z05fn:Nׯ_ߠAM>zq-Z !p3t p{ MŘ& P `*eC|֭tJtÆ 򸾜 (э5|2U-YgϞݺu[n7-H|ArۥK ͥKLSc-;% q\& 8 3tYp!=0iJx{"*cؿ?Zr%:tGnLeIBqڈ^Vܹ B06]'N]ox L%n,տرcDzwjѣ###ˡ2^خ]<*ܽ{7eg֩S'%i}$>0cƌMRIVVV~~>Sc 3 #G6w!Ν+3c1]ÇMe›^ |IC90`G,'4 EO i(f܋@P&>Mj}lwOxt'O-[DDD-c1{ӦMN}"TBaC&o7(щGƛE fxWjçVkn-^QQQhhMr{"tGlSďG' |oQ7N|EOO(t%o|}L{P#)9d?YGȜ,C[lꫯNS,N[M"^?nk:i>esh-)7)-AsQ۷~=/a4rf/=|#aF!81OЅG4haf:v{ԉ&z `*1&qOQ{"2'̧~HyVѭ&&z3Nѧl{q8);j)_bSL;ر"䳧GR [ t>=Z3u"z `61}+|e?YGX8y虔 *UdFb4lYgp5GK.ќӪW"Dv)Qw+ V*OGk3M=q IML>ƟQ{"N5kO9Yn5f6l ;15㵚\Q 9Be_˺#hEh|򄄄~WqLcum1{z͍Mإ4F1614Bv*O/3Յ{OD3b4@M^~Cx?YGX8[j +0?"{9M~e&![t\Tpԩ?rl8]PlCR^}٩l>&^{2'8Ӥj&'K(= '?C{+W0cYFqޯlҌE6zСCϟ?_RRBFZ_{ї3d1 t>=onz)eWaڋit>=zcޓ9x0M&@l"8.=J~O =@ѭ&cbn9 (3~-&M)Cʘ,W^y%44$-;jUE> ;5b:.X#470E4B~j:1ӏ3u"!K60})((0~VZ;bH k.322Ə/ZxbѢE${lr `BZ?_Z@|rP;@$0 &u`r `60Mf!`u4\ &u{/B86iM^pmp%0XG& W H ` H `-N:p¢"B0gƍ#FСCaaX@lb|KK_~9===))UV^ Ę4@ D#\999^{-IWvjuŀp`r `6{/7oB:zi0r `67nB l `ʁلfΜv2Q+0@S&Xd2־}{h6q( .D0t >?9 h?qH `6XO i(fr `6 $ @l{Hʁ"'4 |IC90Im ؎;ƹӧ@.%`C_* ]jW;==z4bP3 HFP,_Fn:qxx $W^y?, ;b>ƾ9={XQ@@Ҁ6F<%&&R޽{nf@'oqI:u*;!G4ia[ vAUچ R믿NgϞ{Ĵ#bQ-c/N M}:A47 '|ұczEEEtM~!bه);s]_|8gyfر,ov;C6^!QZ S3g81*r(sK[[\xBCC=~,*} v :Jcc` 7 =w,gΜ9Nc[4C>`ӧ812aYZU {-gRl*`2'I4.]*\v,@ P'Y5jDMH<?w߱^{ziDi}* 'BE_ǎ͛7|9fۖc>f_!?,{gho-55N馷l3@fkݺ55iժUnnnvX1-Zu]}zDzP<ػwoFF-#&MƉbʔ)-[lРATTT.].\hR̶]\{l7 'ۓO>Lޒh&KZQw`Dz dI#xpZ믧GJ;ﰌc*`^XU^jwIJJJJ:PЅX%V^^d2A?dA4Xzi{oaطoX"GdI#x0=zrv^E i A>}zHOAs\ `6Qu/~4Q.\+|xXiӦM7o[QQaSdSkXfYGd˖-l& K|Ox]|6tF\ `61yiBM6 M67n4u" ) 6+:={֣0MAr]^4~k&D7|ce;vHNNr助OY`TVVEd9_#i|ţsz4I#P 05~y`oFjjjddȑ#IqqƏϳy~~~Uƍѣ} OOO5k1}aaaTѬY3ޗ3gƒsJOzG'$NOY`} ,h۶mÆ !={v̘1 {ZZ֭[ŋiN(Ν;_4( #G83i\0tN6=5xoӧO󻂘3g;" 4iu!1sQb|'&{=ɓ+V}ݬ2Yz 4''geee xN:}駂MFi_!Cr-:|06lةS***FѵkWfl:Q=z{)=ZǩGISȭtN6=5x=$-Ar]DHv}D}R5Sk+-2Y $44t?gLJjҤO?$8)LkH)9qUm۶M_)L7l@IA}njj@,YK.111ёdv%=G씣2JWC5jDђ1];zX6:1q ,0Y_BÚpW Ӽn:Q2{!΍7ZN,:rY+ڐ2de}y4`#XbL%ǎ ={jO^{IS-<kH>ׯ_PP6(Y}e씣|Ŧ D.@@^)))IIIXrP 0},SXXxuY/4& ` EO i(Mp/|IC.@l{Hufr]u(\ `r]c>\ `6X lQQQ!{󫞲ŋ 4 i(Ml܋,ET}m}dOQ2&6E}:}tnnnFFh. `4\r]ƽ(d/~̙3$i79rdyy9+KII KNNf2?oFjjjdd$s?~|ee%_xqOΝw!s#,Ym۶#M6ʒr4뱨t*ce]Rr,Z(+Dn[_~Æ ;yV%EVAee%=Ϝ93!!2d=wqĉI^rrrYIm\eհ0Z7g?81k6,ӗOe"$YFjڣtʕt|С=z洺嗃R\f[NZp!=!oNDDDIIVb񑑑ڵˣݻwS⋊b_!]|| %}llرcû,_:$H\B}|YG }7w\Y$.j}zh?5Q36cƌMMVVV~~>o~z0B{(Q 0XݴiӸq:tPXX(֩(c4^%׼ywLkZR/ @`bLr]D/Ғkر[ܹsJJu]W{AL `uf+==}III^\ `6={6-H <d(}v5dȐkߝ (XMxY:v47 4 (2,))u0У\ `a A߁@ \ `6GxLG.@l@P 0^r]"'4&>\ `6XO i(\ `r]u(M>ϨO8 2W/^+|DaH i(Md;b y|TElH$JK.Kn `; ""rqQ8hT2#dGf& !,,}ޮβ2<^}~^\MZ _SaҨQ8 0Mdc;z͛weVذa}e{իW3ŋ\ZZJG}w`&;5! }fݳg;\tiӦM@ NX>7mTvmC\63+ o"s>y$ڵfgޞ /#eɜXkٲe!է٧~M[dI.@|OX;tW\Ase_v-傟>FGGEr5Wޕ+W-Kфٳ'uڵk׍7ݺu+'*^PXoj P֯D WβdNed짠pO̟kq]^V-ڧ%KynD)cĈ<kX\\Lǎ3b_nP3gBgDy2'l9^k?s3Mps^bEΜ9ץK*8pf6[T>|p=bTӠAm۶PePjOaO&BEE}^5kV||0! ξ7o!׌y 2s~۵k.P g>{F:&lNi.D,s(tSBʼq\ `XYN.@4 ;b!`T=4q]ik@I&X@p\ ``-Iq]iX&u@ H `q]$0@.@ p\ `z, ÿI:.@40&Ŀk@ߤQp\ `]eee/...@6i=LZ5k֠Aڶme! `Qsp\ `درt+)))11ES/@bO5 uVYؖ+--~Q٧e@Ӈot<@ L 6p @q]SVV`ݻwpte˖Uq]ibm۶ &h"333)) 8&;b,111990cO5 ub- O8I.@4q}8ۤ8 0M^.V J2ydkA E$9r5zH )0 LHwIq ``-Iq ``-Iq ``-Iq `z,+4 H ` Iq `XW i8L`]7n|jg}f]N& ?$X"%>uu84L`]LniݻC'@4 L(O J ֤G `: `ǁ뀀~<: mΜ9Vs+C|=0!0MzB|8 8֭n>zܷo_nnnƍ"##۷oOX|y}A X& ?$X ٳ'ˡ˭N>zݻ7uԨQ#FeddlX``M~IE=[`eҺ.@f_nݺEDDԮ]Yf .ʏ?8===**VZ޽|yh2KiӦ{&M<Ț^SN#Ӎ7_67b>aܵV蚹=434i7|_|S 4 q:y$PNj8c ^wTO 7olnFAM*_0`A;d =տ ! ! : hyn'|-iM7jH~_߿i+⚄ӧS. wkV~רL!ӛ[oʴ3,\}'3fd WXqSmkdj[XX_y*8qJbU""dO@lFYtiفs=ܟzꅸ>Sj?i.s/Oeo{֊mY/ڵk5jr҇~He*!k('~:;$]Gk?_"<>]V¤pKJJI_=+D *_Xp{G;_}f[.}]T^PPZֵk￿O>-Zt.???55sM|1GA{ѣGɭaÆwujW4uּ fUVcǎe]}^RѣG|! Z$K+= RnSL q}KhT7qǁp+`f[aaazz: [2_WJ}۷]6wyD {MLL4hбclj' -2, #99y3%ĕFy %Kz/BLw!ʺ6В6a„hvKNAnT+TJaRцd5!!a޼yDSfUסŁh( ĉKWbLr+<IF̘16[4ڀڵ| ֤G `:wO@u|gP h]\ @@@4XW i8L $ ǁi $ ǁi $ ǁi $ ǁi@p H `& $ ǁi;b^80M꫐~jBmƌ , g&l-nٲ[n׏ر{gpgUD844t۶mJTAT=i"\?cÆ gΜI;+WP"ϭNNlEn9;Ht@#L2@4^):uj0Ϗ7.>>><<]v;w6mDDDDGG8ĉƵb 2NSNEEEh_RRBm###5j4qĊ mXjNԼI&|ܹX6r .III<11i͛GQ|aaa}̙3_bܹH/_NgנAΝ;D^:--"ddd|7G0isJ7)?~l"ێ=J}9r k֬TnDW^}5ܲ2r-P܇5+..&6mG\/={M4*:D֭2HYYӧq2gx1C>}&-99y{< 9޽{Gݼysʭ]tٽ{7U;w4G޺u+zr=rƺWޕ+WX/={]vݸq#F/M?ze<%7@0!yUqm?ľǒ%Kp,Ҿ{w;09j+..1xP/999)))IOO:u*0ׯ_nn.iO?PQQAYff,,,- 7߲30ƾ}e;ROp#¤*L>߷ i8L\x@$0 1߀W}Rܞ#10GTÓ銇l/F 0G(KEH+WRSOQPvBQQQIp )?>W'::vX޽4h6{l.`gL6-¤*LYҴl2:a>C 9{ŋͅR>Rd'Oϧ?b<< {>􄑑'X"g>!33 pSj,im `֢9G˞u|gn`&Td!(曔iBSeϞ=_{5`F-`^=1\PaÆ{~?e˘`1xȒl-Y'͊Rf=Rp=rذaW;Ibccϟ?_n]?Lы'i$mݞA},im `֢b#ŸubO)bҤI &ts%&&Z\L  | Lc}8ۤ x@4Zx@4Zx@4Zx@4XW i8 @@@$VTT4& /9r*6rrzUlTV1cЙN854$ 5 j~:yL7nh>}Hj^y@ICMu09s  Py4@|P?ggJ<iӦTo߾_̕ɷ+ `>A(`jؙ.YԩSjϟOϟ]EEE ◸Ev0 j0[`AFFF]TC<#YYYdMOO/LMfر͛S ǒ@!UO>hRRRڵ~/|h… a˖-͛/_| 7ԫWk׮;v`ӦM馛7n[o׺1cFLL U{yꩧhf(9/r6mHtnJÆ 35VJaܙ$**o\r%:4|nP ${W\V 4 tHSmeqHNٍʔKJJxGRɓ'[_ !?{栗;7ޠT5jy=?oڴa瓕f^*S 6k_XXHTte…q [rB]0d>|!S(..f i<_E7|͏pr]_R駟BJJ0> $tCeSUC?~\|6jybٲeqqq$;w]:CCCbW\ K8@'vT~嗙 tx 7Cš')꥾ˆaBא kH5ؕX^߰aCA!`lzK;ӧvmC$i< ңАwaTI=֨Q !tV/lH"Q|ͻ+&Cl}%` RqQ={_|衇XG+W_} e|d={vk'TPP&<1G3믿vZV#PVPP' H6U`Scj+S^=~ 즛nr[2ܭE6BuO?4eʔİrLyy9o7oNTdee-^.]1ud-r78}.d|dؽ{7s۾}6zٲe< M5۷g5. ee!u$`Daa!}fR +cu֭N:;vܴiiD wk… V%44l6:i.`Nғ/~Oz"a̠jAE4Rx^փ[m8pO_Q' 3fUwVC%`VP8,zR{B\yx8K Fg Mi@P)0MpG,+4hh LEW i&X@P)0M]_}MO?d5x ;v///~*]̥K,#]jd#)s mҨihh L۷߿QF 4HNN8qbii#oWσxX(bѫe˖nݺկ_?**cǎ\s0n=_lQ82mE@fƍ4-gϞÇO6mڵ]=g`8/w"?ذaÙ3gVʕ+[Lh_rVQQa)m js3fXk]WXѦMީS' ܹs#Gƍڵ۹s=O͛7oꫯ.kS!>ϝ;W _|9ŧJΝ)ի,(BFF7|c(,, PvXf '%%%o߾G{/ecyJo}f:D֭2 _CȔbfR Nqw}7}8~877! j$6|c0#~:v7ad6dxvdV )\ ڵvNVõÇMj׮Mн{>(.. 'O$ x-y)}5D&2͛7! ?dTaLWQ[Xo}fH̙Cܞ}Q@qFM;#wm)ڳgs^l"̰ נmT,2sOE! n>4 l Mi"Ӝ~7}o۶C=tU !E1*!TvZuw_=>lذ/R̬^6iYQQAg͚o\.b>/!n_s^͌b} v̙3yyy]t1E/hNNDR>u1Rx- ɋ> OnZüۗ@Y MiX)ސ1<<<99yҤI?"5={v̘1qqq̇/o׮ݮ]Ah1j(wMfdzd͛'E1*!g̙78܇4QGhh(WۤI)œ!7H~̝z;3+2x1͡1Rx- k>?%% pBK ["iCKS ``-YdIӦM ( Mivþ:tP&Nh ^ʃFCS `1l¾)ϷB hh  &Mu@hh ,  Mi0&ꏆ@40&ꏆ@4Zp7iT44" ÿI)0M!`M Mi0&ꏆ@|EYYŋ' M9϶mƎ۶m-[Xms r͟?s ͛7z0@С)0-רQSRRnz0@С)0M-WVHThUe@@p Mi2a7^@4XZZ#lْJ޽۷o_;0@S)0Mk?ٳgBBiX.]a;05ľE0{f44z,m p(04+|8 )0GCS `@4XW i&#Hj44 "+4hh LEW i&X@P) /ɓ'[@ȑ#VpRSJ(#S }/2cƌ8qk*ǀ!0ҀӁOgqFjB `* `jP[3gcICj*3ec#x ~ȭM6 }̅'ɱꑵkj8QsP*D} ؒ%Kp nSYϟOϟ 3 e91Oe6&°ͳz] K5jB߇4?NOOUVddd?fb))+.7ol==ZdC+͘W ~%$$Ԯ]e˖3;fo\rرcÓh?}tС ._<99nW^׮]w?5k,,,SR6B_NՅRVJY'f6s+V~ꩧڸq{ӜtؑX9#K5jB߇4lѢE$ZwFVV5_~{z7n;ٳ8@R/'O Zdc[|0`M~NNIxlK姟~ )))<>qA;3oذ^BJu`SSS򗿼빹Jfjek٣G?ϛ6m4h27o.:wa4sdIV UPݙVzǏ4o}gh9j2c<<֙63zhYZZz%^z*BCCܹsgϞB X=Abb"abСC}٭[**e#:[`,뚕?g52:>]4'txƅϑ%Z5V!Cug6j(̝;M,VyxhsTTT?fͰ.BLr-~Æ y[J}2eJVrղJ5XvgYvOs4SUCm>Tw!`գ&$'N?҄W,v믿~]{z>`LvZVþKOO_`… i1n8KCY _!u]>.._|Ňzvذa_ڣG,((UF(t`^cMC\;C\Sp jB߇4E!k62RSSY Xf>ˏ;k`T/Öԇ,?TӾ}{VS^^>}͛׮];222++kŖVOZnXti|||DDԩSy>}ժUeC MR6Bh50daeڇ=TIX 9jP[՝iا~JɈ);҄P,YBꯋR *^ƵVZZzi&^~=FGze1{V UPݙoKv=ek?d e׮])))!)&&fС%%%~ *,iԪ ; ,V [m 9p}6!K5jB|+`cƌy.{^"K4Ԫ  ު$ jB|%`/BQ=EW iUCm+S[\xZV!0UZV!0UZV!0UZV!0UZV!0MW_DOV8.]|̌3bcc,X@5Έ(z!%ZQUc6VoGΧN;5n_oGAV=N%E j&;boK2v CCCmfZl_ XteUSgmpuFB j&µhU!n;^* bAӫVِuTv^M vnaҨQuAmD:Nf7x#999""bȑܤd58q"OmڴVp Vر޽{7h --mXׯ_SNĹsccc)& +Wnݚ9FB j&µh{ױc)SXՋޙP\\LӦMcA֬Y50uɧo߾r ̴<77FB j&뱖wibϞ=~ҥM657!U:Xذa}ٲe‚3A"6mTvm*ݻGqqqL7ߤlB0x`ٳ4=*z!5lMlYb ;͜6[03w)t0\eeeDEEEDD˗!"C נգAӇ z;3Ell֭{nsd9=*zAA'vgd3 Ő͘!ydq~q27sG.k%:osa Yp:ޑe-~G j?~<а}Bw}]oMduHon}WVP*0z߿?*߿cǎ=q5dXRRzaWaԩ!vZ[=;dUG{SNßH ~Aɓ'S9//__ᴱ1$`/Kp  h,{Z$1>cWAOo/^$-ڵzf3Y "՛N'Ō0!9_СCf+ ZV!0qƔ5k㏛4bX%񋌌?~<̙3)`DDDfffaa!g7L>{1c(ljjjAAoէOWR?&o61Aa/oo~aC2 fY p9ޤ]vvR̘UPQNg㤷Chh(R8g׳ꑿ)))5 … y+yҝ\uHz3]!/.++uAmLZCYZ,YDxKN L [li۶ ?6ij]P[@4X_Wh={o:ԡC'Z=`:Af͚%%%~ ,oȼMZV!0M]}$g[n ,\|ʔ)S3lذTˆۤ|uAmk`4 ڇ=[o2$ .B `5 7o_ lCf#5 .B `z,Ζ-[Zh,11M6=4Ժ 3+))޼yթU 릛n"[.GG jTRVZa˥F jT 'Q* IC j&I+4Ժ i $ .B ``-IC j&X@P* IC j@ j@ j@ jp;v[=ymҥKV5.B `h\e),ETTT>}SpaQ,OsU70I#P* u̙:X 4FULhP_|EXX;w$-666::zȑ.\`IIIOLLd8oFrrrDD9%%%lԨĉ+**+ڴiCq:uTTT$ nGYre֭YmhgI9X\\LCm:Ν G֐ /_]v 4ܹ3\zuZZEȰos=ob>yVXXHgD8q5?vX޽ <{kf*,P4FULhSNw}ݺu֜={f^&MD&֭2k{)uqʔ),5LG$T?m4[VVV^^w-ȂFb>|lT~kԢٲ5$cnC=}4$!]>|8?޽/]xDѣy`k֬ٿAnllrdff\0aA4 .B `hE4}%ML{e}Q\\?NR1gngvtҦMRLX6m]! .Jo&ev:* <*{^qFbFV60K͛7!X:uᶉGw}6la{9-[" {.I#t>IDATP*X^_6j駟f]vU^+WHHvqF>׳[nlذիWy睔cccϟ?_nݻwrZrl`.> 4feeĐtre{{[A4 .B `U%C-\"m BL;< iq-h6iİ}zL냻 c ^~e:LKK{}v{׊},meSTw!ᇲ-c%K[ifXsLaXf Q*VuX&%֭[?#_~N2\R駟RTf͊q BKJJ~a$;;;''… ~ԩ~ye΀]90!>}]z~!|# }7o%%%==BԺ 9ߐ%''CZV!0a2 .B `w.B `z,+4Ժ itIC j&X@P* "+4Ժ i $ .B `z,+4Ժ  Ժ  Ժ ?cǎrAt6sԺ 9̖-[uV~;{V%JULaÆ3gΤŕ+W(W~V'PQQa "g9¤QP*دԩSVai&"""::z'N9s|j:tϝ;K#GpwcDhѢ֭[שSҥK&III)LLLdfmxd9^RRBËlԨĉ42+V)PN:ɂ:UD;ϟ7n\|||xxxvviH0i(Ժ i"\7n|w^fӚ5koRp^KӧO׭[e[駟f圜={Ā'Mt-ҿ`xȐ!sEYCr[nDs=eޱc)SԐr=SiӸnnnYYm.nYpHXYlHGPmvQ*۷ȑ#d!`_FB j&wѣG7oޜNK.wzƆ h{C=DW~hT.-- P磏>_DLɚʚV̙3{aK.e;'13ÇM6ծ]ېČEpgNJZV!0M|T a_(ij]P[@4X@GI#P*0?CBx(!;ֻw ͞= }gΜbܹB7㚀 "7tnԺ 9:3>^3J ZO&}kp>{E@pUIzЙ" ~O=QsP*$W^%}bɐ2x0QF'NL.C1nŊmڴ!N:v9s`0K(O&^UI&1fIi_~9prC6aG!yc֬Yկ!ZV!0'9rH szW^!^;v6m>K1.77M=K 0i(Ժ ib1zh~)˜#CӧN=E#̉, ;?e9UVp'`.#WovHqRښϔcT0`){s;SE4jj]P[@4EdX`+ߟǏc>K10''`0"P8TEGÝ \-`$N.DZfnڄ+?NC85|+#K5.B `("cBdj5立QCP* bаgߡC:t0qDNUL\ lْҠA|݆8ZV!0UZV!0MzU, .B `  puAmĿc!`M.B `XFu@ j&]0&ZV!0M!`M.B `w-B84j]P[@4q{=l 0I#Q*<۶m=zt۶mlb9 puAms r'$$4owe@ZV!0-}IW6m|^ uAmӇoRRR'lٲe j]P[@43g-az8Ժ iK/'hݺuBB[o}U^ j&浸~;#114,##S*}-7d)))0{ҨiuAmDiC6tP p(F A j&n?qI#Q**P*ķ6`WW^!Ep2 `F2_ ؑ#G^*.Ԛ"Wz,+4hh L $ 5kHj44 "+4hhJe(?p@kAP^He p*0(Yd7 0@$0 LH?x@4Zx@4Zx@4Zx@4XW i8 @@@$0 L\x@4qg@>@4Zk'ց g@4Z&`z4t$ ǁi!L(ǝ 0 i8L\XM HVVVEC\d̙3jG^~ zLƘ>\[^ ww 7ueƌV? Ȟ{cu#>x*854 ٓ|r6Lm7Qcƌӧfdd9r*Ba'NX*e*Iq ``-zZJ`6ɓuVNԍ72?8===**VZݻw,XLaÆT3~xV6MdG}Guefv޽{6mZv&M>~Ĉw߿תUѣ{MKKk߾=+?)>vM7yu 7q*;(t,5F ᾅ8yqMbb48I\1q//.A⸑1q *9*(Fp0.F\5\ouUw;]ݮj  ضm {zzg_bZ. VY@@@g͚%LXbժU^ad1Z;vhڴ)i-V܁,} 7 &yyyEDD]zUιqF\\\-BbY'Of|RQYYYO  T0`_ 8fJ `B?7p ~h%0gfo4 E+U `a80`.Z 28 ÁsJue p]h|!`0Np?JL8 Ásr@c-?)pJ `Np?`p @t 8 Ás`p "&4 ܏@8%0N  S@t0ND,&4 pss{a<Çkժu=9c%&&ߟ̔pNi8>0h۷w׷o߾yyyr ӤI{! #b}kR۷Okp@0M6y{{'%%Tܹsv~~~gΜ u?/N$`FD*lFEi@tv?y汱bȐRL **۷G_NÇ˗/oڴij,X%Q8w|'GK.˫uͳ:ԬYjpBJLL"a |?vfHk׎dULi*F͝;W,*--7 88B\BҎ6 `ܸqL|-RJژ4 j꫆ RGQv *3jNf$.\ //_>00ФxРA/))!sύ?'u?uɬѣGɋ srrMڵkɓ,|u N>$ȑ#y:xahh(ox󾾾׿ȍRv~.55՚ HFFmq@e^v@a <==yStY$''4bp3Oi6V[$Բe;f$/^$/VPP ߤyLڵkԨ#bܹD*ţ8_7GqԔt]C~~~ۛbIAyI]'}1,^ ZFPm$( 0i޼y||2tJo+}qFҥKb.W2"##.]گ_ɓ'᜞={fˤػ<(?zxxZ=cVټ1SiܸG}$Jg" ~)  vxӉXʕ+>G'N(ti?sLھvԉ>۷/mnܸa*u۶m3) 4qBYYٳg 77Olb*~GRiڢEw}WzZ'yYmvҤI4"RZvV ]&s„ rfPQs@70h<}.]TR~ǎQ"9/,))y뭷֭K׬Y2n߾MiH@Β%Kz?^p)DՍ3}h"H2HHAy9nY0 z*m(|MM4vԟ049ʔ6Y-)DmهKBb9I4L'ţGMII#W{XB(=T9LJEvvv-Ǝ|k N{٧-P@4hֽ{ŋ+7dV: `=0T\ "JOO#@Eի׭[6kLڐUpm `:XҰRڶmkuܙo4 6G@ӆhC!P6 ``jԨ(`ZO@ف1pWI׀rrrD8 ``^$Z$]r0~,3ho4 < `?qT0`.lNp `:\@t 8 Ác6a80N  S@8%0~,&4 ~"6&&߿VZrTyՇ)nF2(㰲.}Q^Ө( `:1UJ:uz}i9QESTT.F"@kxbٝ;wYfڵ#""l]v۷o^^=**_~C񡆟=1[*;Np@NtE%ݼysСRK!̡Cl`_ IѣGk.fӦMIIIܹC;??3gu]lX5>s̖2E/η:sQrI~m*Ux˛6mZZ5Z`W\0`]7/џ~iԨQAAAkٲ#G(#F]ɇ aaac 5IKK(mbppYeI Y 5k4oޜڵkwqV٦*O0jUKʔ&Hr;F7nܐSLTrll2'*}rd5ZHr˗/pBttW֭͛ړ4[i1UҥKzڦ=.E?eceyuځEEEܽ{B{9rLt_TT1eVv֍>} ԿKk2?Ϟ=Ken߾)ZHm?l6ybگ 6cǎ,lS/b+Ԫ?P44FC100O>֭y J/d2_v])/mMC>T9-߰aᑺS_xz=zx嗩yI1C4☚ eKˁR YTim `[uuJkmZr,x8FGINN WRѣGS\)׮]sww?y$yf:zxx,X֭[<@ $`Ο?>feeyzzԛ*&jUH(S5FB<5JV~hv.D D]v5=zDQk׮ٳwNbfE4ܓҏ(}\jMUzWU!98@Jcm/^pXAA@ځxxfGVAС5>|^ý{ZI"Vdju)ǗM\ԆG@+Wt٘"t(ҥKbȽ{O`1!6tL,jƍfF}Ԛ-&PBR5FBma/_WbСC_yӓϜ9Sz ͎ZiժUi;vHL&֬YceM"R!Yxe,TVvS b7-ڶm;i$>p@4f ۷!CbmF'u觟~(!6̙3qɏ, JM*d^lS/b+ԪP:8&HΝ3gGu֭aÆoߞIOOʕ+>{)1;K9qDڙ(GVte <|=sq_}ջw$A4I"V6vput||i[fq╱| 8;0\uj?f۷o[clْ5)))͚5KHH0ްJ%ԬY322}d6PBj yƍ{yyQ/Ҭ$ۛEixRMU&b+ԪP:8&HT)m^|zQWQį nڹsg*O>N5(GV夑TJLL4;(bz9I?ݝ.lX$bEjc 7[I IW+NhӉ'b<ܹZjr3j*5"Kbi[Dm.ٳآEl98>5jTg#//}{q)NsСC  }駡^NÇڷoϿt^h*F!=8@@t"rn:~1 3N÷\LѣGCfCIFFF&MHD d?L?ŋ/ҥKxxxDDj؁@93};}ׇ@93 ! mذ!4 L'cن h8 N,>4@t 8 Ás`p "&4 ܏@8%0N  S@t0ND,&4 EMiN ͛11OM3cF̮]YxPlۖ9my2V4e_8eOt͚pԒBVKJ-`vo;-99-[76;vqڹk*Q9LLVPp]v>H'z{s7m9vѿe^Xl !`(`t񗐐~{\XX}kiiSc]Vr=kמhc>f OR7[ZZDDdfx̸~.gBBgoD8uIÆ,[.X(%B={>46sLif=W5N"K,`ǎжzV :uFXXv׿nݶEuԕEM!e߷/ou4ƍ@%rO-&h*<5u)%BWU4{/_R$YEKkNfMM4 脿k+/b_e5p(, Mk}B/NzF թK;{s7n6?ߓi?Zs,i~tJF%PVZBsN"K:fW֯NH$SQx~ǽOsEQzzzҕɓkԨAqk|}رAq3UE5rS;)nfLpNQ_\0¢ѢgsO8y+}Hss i31w<)h: Ѣ[k煋N"[pj_ fyP9͢G[6dЁ]Q]DdҠ*=p+fejb{˯сvNlEXpNjˆ 8XR7Ҿv ĩӤ!8[bʥKS)̙+$mڴM<1q)L#=/\8 u*m5M )\g k_VaΝcV 4颃&]%'>j5rvj k 8M:u%m0jڒ%ÇU/mJ,W;eh.U^#ڵ576J`޼/e*s>PK <4:3 vuF 3[k tf:>~2h0PX#`tܠ]W|]0`3)( bcY\. +efo{sWH-Uی~4rr .=-ݵEIZ t=ҴiQi4jj ߲%&}^<(\ iө# 8V mnx$`B ֋ه8EA@iQٳ2h'33Ӻ8𕐐zLfKafq%+Fg144Qzv3^qƠEVUVjdn?BQ2 ])n(MW_}V ޽o޼KYTZڝ0ihGp(0eRhN22&NH}P*X<__:wMᑑEz^j 7+%}]e0X9 8s.n0P@\ `0 801e ACsi~I0p( `.c0Lݦ&TƾJ  ͛w^)?Nԩۯߋ?rRfʗXokr\fz1lذmڴ猌}b8s#Wmk֬9=.a `ajчM 59bQjK g .'LoݺZꉉK*a0P:US{Ԯ]{81Jr=<< ) `<oaH|MXR =jF_2-46mKCd0P͝Ehhعs7I0}b&m fRQh}(RN/)ߣ!`do9A$] 66z{ 8$`;va_7G=LtRe2ʩ fIRa (RN/)ߣ-`T;h*UyR3p(\F{xxp'C/gԢWDDΝ(q:f:Lm*L)TtUj֬I~g0PGZR޽, 2v>rXQ8A 3IyRFlɢ,|8ުk[w鱵k1">{ 8!`?pm9Z W{hQe0LB+6X,ݲe7snZ#`W);k6l[Ekat||quKN 7p(\Cj:uFOj8(2a0oQ@,VĉK'%}CEzٹswM4)KɀSLSnAC֫u{V ۣn)T4 `0 8!`B   fw2 `A`v7p( `.c0   fw2 `U 6-FyK `9W.bfˣk7*L*OVMZZ<$TV%/[0'.$$|.ݨ0+))?O򴘘ݻw@Esg'RXXLLg%onTe)pJ `~7}IENDB`onedrive-2.5.5/docs/puml/high_level_operational_process.puml000066400000000000000000000035761476564400300244270ustar00rootroot00000000000000@startuml participant "OneDrive Client\nfor Linux" as Client participant "Microsoft OneDrive\nAPI" as API == Access Token Validation == Client -> Client: Validate access and\nexisting access token\nRefresh if needed == Query Microsoft OneDrive /delta API == Client -> API: Query /delta API API -> Client: JSON responses == Process JSON Responses == loop for each JSON response Client -> Client: Determine if JSON is 'root'\nor 'deleted' item\nElse, push into temporary array for further processing alt if 'root' or 'deleted' Client -> Client: Process 'root' or 'deleted' items else Client -> Client: Evaluate against 'Client Side Filtering' rules alt if unwanted Client -> Client: Discard JSON else Client -> Client: Process JSON (create dir/download file) Client -> Client: Save in local database cache end end end == Local Cache Database Processing for Data Integrity == Client -> Client: Process local cache database\nto check local data integrity and for differences alt if difference found Client -> API: Upload file/folder change including deletion API -> Client: Response with item metadata Client -> Client: Save response to local cache database end == Local Filesystem Scanning == Client -> Client: Scan local filesystem\nfor new files/folders loop for each new item Client -> Client: Check item against 'Client Side Filtering' rules alt if item passes filtering Client -> API: Upload new file/folder change including deletion API -> Client: Response with item metadata Client -> Client: Save response in local\ncache database else Client -> Client: Discard item\n(Does not meet filtering criteria) end end == Final Data True-Up == Client -> API: Query /delta link for true-up API -> Client: Process further online JSON changes if required @enduml onedrive-2.5.5/docs/puml/is_item_in_sync.png000066400000000000000000003315011476564400300211400ustar00rootroot00000000000000PNG  IHDRi~q |1*tEXtcopyleftGenerated by https://plantuml.comvmiTXtplantumlxUMo1B"ABARZ5P^{6kصW7e&MS^ڬ=o޼ATed5A[S:`ڀo `xR :ZNHY`lHw; #qrQz#lM?ߢgC4J3*4AmB:dݤ.WJx RN6hrnKtp`] &/lS*ҿ7Zݭwu+ǁBAi(#j أ$l80?cXBFXj"$UY2Ԩ jSj=/“kbƽtC#1Rɷ˫k#<.6t<7^ _ (euOt{(?ZW\"ˢutoZ\s[͊a-lG;[[UU{Qmhdf(hX  &P՝^:6ҷ]~2WƔTLkY;JYW(]u.[܊FbnG5f9%i+n =ojtuZ$WͱuIQk 7,KGJŝN Ғ Ki%{RkSU>mmZ<+gp6q(cm.ܘb*ePj_'{Gxg0r|Ν^Ep/p.'׃m'8=kp5+n՟0:f5u>;'#bä!:|^#Xp5>`.2iC\y#Mvb)kn'=JIDATx^y(,hn*ш *фcI4fN̉"zlH .G(H0Wap4 * ۰9 _}ԵmzzO[o]gvD1S Oi'N>|J;v)H^{>餓N; /pҤIѩ@;b?OjУG#Gnٲ%z@Vi'@";LvsgZ*z2@h'@[h1$583Kdv4Z]}UD;ظqmJJJ dv4>㏏:~] !͛7/ZEG[l.ϴ!V:]`i'@C:th4٬Y3hH 4 А. i!W<4 А>?>F?At-lN6nܸhB٠ g~mVW_}ut,NhѢc9&ZHjpgVUUEH޽{G;n>UVEOH˗'?֒Ocȑ[lU ,Zqqg}I'vi^xI***Sv$W*3H.=H.=H.=H.=H.=H.=H.=H.ȽvRYY7gƌǏR@6lܸ1E>N?@RTt 'E>QvȆ~@ݥR?~|I-G3x~'@Ŷzl'֭7=Nm{N(N|ᇫW޾}ONNʪgL&vd]CLi'ɧNvrwo߾I&m۶7n.93J~׭[:[nٶm[湳fݻ!rI'-YO>noI|p/ 'p,^8zBu[oٳg~?Qѣ;w`@h'@օg{pȑ#ӧz.'x"~뭷VN2dH?/_|ٝ:u馛2=KJJ/^ܯ_޽{~饥g~g ,x7#}4id…uú~P^^,|$WvuY~WvRVV. 5ٽddͮ-[4k֬$=#i&ϙ3'ܝ:uj;wnӦM?RI{>(s~MƏ~p{} kZsӦMr"\o ԍvd]]}G}t'n/:u#srz;a=`dk֬\jÆ 7nL9"*z`_W0RPPp]w5p`cٲex-`yh'@օ_gg̘쳟9眳i;^22IiiiX9ww_-s<kZ!WG護޺ݬZ^ҥK9p/{ 'w ¯Nxl:餓.\yLw_x`zĉ;>۷=SM<BP׮]3GjY?:uԤIÇN`i'@օ_g4*ݻw5jTtt '/ bۇvRky͚5[fM؞N`i'@ŶK>#",S ;Ⱥ B;.}Ĵ3fuv5' uv2~ϰ Ⱥ}m'~w͛}Y}l'ȺɥY>  bۇv$HEEEnD 2eʔnf; ƃ]> A*++u'v2gΜݻG? Wbۇv$_ޮ]0dΜ9?ϢS\lNd)---((ԩSIII*3gNvڶmGӫWT*uQG۷o۶m~JlNĹ뮻:v '0iҤ$> q*++3IQQ> .0t],m DEEETgϞdQlN .@vŶH+JK<]C;Faڵ<ѣG5r?1dȐT*wH@+--O _Cw~6@m M6 4hÆ ɹ9s}Oh8C;OSCHlN ߔIvm|ABlN a„ -Z׭[7n\v+#T*\$0eʔj/>S/W_=Νl/^s3oj@ĶJdٲe{x$Er!U;CK0'Zly㯼&8+>wwDN2]`an~EEE-v:Cəg酄}hnjSNׯٳ#GP; }h'Wjo'U;wi+V.SV\y  W???2'dnD{쨣 6{`3oj@ĶJl; ̜93KSvX޽レCEEUW]uG3t2zpnB֭/ s9#|p7TvO0E]vٺuC/5kֱcÇꫯ.UV?p#njժ:{I_믿޷oJի_p>NvI>+ui'TSS' $AlN 5*~11cD?0 bۇvy%g; C;.^8= piӦE?0 bۇvyeŊ'O~mO;v˖- ȹ@:thk{>7? _Ӡ%KD?*!ĶͶmn4~] 9bۇvy{~OCXf_u4@~gO=mݶrԧ}ݣG bۇvyrرG.))~O-\xK.~@2Ķ?p̙cǎHRѡ iӦmٲ%I> RTt`ĶH.ȺɥY>  bۇv$vd]lNNm \ uC;"s7N"GBlN2eJIIIz7.މm ݺu{t;={v݃N+C;/o׮]Ov2{Ÿgѩ{.}h'@t^HRgn׮]۶mT=> qzJ:u JKKRw(}h'@Bwwx bۇv$]wݕJ%Ȯ@SUU5tБ7dȐT*'m 4 UI=3?i]QQ~˖-u.;c׿,5jT׮]=`<87bۇvJ8ɕZɆ ?caîp_3<nO2_zQYYٴi:.@Nr|rUW裏ڷoo4i䋟8v`|_җ 0vؒB>m *!ԔOc} TVVtA[nu}_~:3a`oŶVUIСC7|UVQ0M6\ɼ콶dɒSN9%:SהON?vM6-=|e۷o?W_}5ؼy /BQQQz>bۇv*Kj^=Խ~dʔ):t#ׯK>;.x;vl0jժN:gϞ`޽g͚m 䡪:={<p|ĈGݫW`_/|wY:L>}EwztrHymN{w};Np=z0`@nΝ;Æ C5]g^v U5 M6M_Fz#T~- y'NӧϓO>٥K L/Uղ~t>YreϞ=O= ȡ@4hŋ_7lڴieeepvڅ}>=z|gfC=4~׎8p;\6l'VXѤI/|Nƍwg >|ٻ^T~Mm ]x͚5{"_UM7HMAlN TUU 4hѯwUm;h֬ټy_|1,vL-[VvR^^~A]v7Ν;wv_y vF-ϻnݺ^zRR2\UMGX 6nȍ@C>ys2eJ} 7jt;l_s52q!lذ!yg;unjϼj7+(---,, !t_U-=N Wbۇv*.U`{wAAA>}nt :+TN̙ӹs^z]{+߽{ÇoVGÂ?OSz媚I/;My~`w͚5wq)lذ!t^UM(N Wbۇvy|!Ⱥ= ';ȕ@>9h'g/r%}h'j'ԇ ';ȕ@Orer%}h'(T'o_r%}h'XT'ir%}h'ЈT'cr%}h'иT'ٖpC;\m 4:UUUÆ ?HRѡ[=N Wbۇv$W @m \*'4 (}h'@r5Ш^,4ɕ/_޺u[nO>:u_իWϞ=8իWwȍi'OOl?'xb8޳g{,ظ/v> "C #<2ذaCӦM?`_СCz~G;܈m \ͮ> bۇv$Wl;ٱovO 6|wȍUv^m \*'4 (}h'@r5Ш^,4 RQQ yF;܈m SL)))If`<8? FlNѣG:sB0G?wȍ}SNa> sBݡC::5h'C;[nF* @nĶH={o߾{Ts'wȍS\\\PPЩSN]v4iRtRN 7bۇv$NeeevpԩS~%>@nĶH~a8i׮]%>@nĶH'ݺućȍ ճgTK|H;܈m PũT1%>@nĶ[l6mرcnj3z뭩T*xȶh̙~av>N.]:| VO?7/¨QƌSYY}9@nĶu1cL8qwVX1bĈ'|2.?r#}h'@m***;D#C^{ꩧ&N}/v>F7x5km_k'C;uց6pzov'42 ڶm[ FlNj,Ydĉђ,Xcݩ FlNj1bђ( <8r#}h'@Ԗ-[  jeeeцX-^G@;܈m U\\iӦhChF}v> jرz0CmݺuW^=VUUVV֤I>ۢQ=N 7bۇvDŶ ͛7o֬q!:#CKƊ+_zOd'C;N8QYYy{dlذ!:_~B;|> $0o޼-[۫WڴiӺuK.d͚5`* 2eJ`C9$wѾ}O?=?~GѶmYfC%%%͚5 ߿02nܸv}yW 󋋋E#G Zh'ObۇvDսlذᗿ婧_p{ʕ+WZuYg]{wN?׮]f͚pǾ}"?+6m{キ~ 9aɒ%p>l/_$<묅v$}h'@T]I?VZtAmڴy׫viӦof8g:t'ץ,]4=.[jZZhEIs=\LUƄ>;\rezZMY IlN0NZjկW_}5 *-[~ᙓw]`Ms2E~EEE-v:C#N/9~͞=묅v$}h'@TIW^i޼ᄏbŊ`0xunK/#/ƺu3g{;ygyfƍ_]0jMGV#k-Z֮]oԱG$x`񒒒` . `O jOC=J֯__QQqUWy䑇vX.]FN[l٪UO0u۷5jԾ/f͚uq_}h"ЫW/)5]gM'C;bIc@>m U\\ȑ#Q=N 7bۇvDo޼91IlN3g.X EM2%r#}h'@֭[$;\fM= FlNj;vʕьlܸ[n;C;܈m P>`ԨQђ<%%%w~h'C;O>јИ2$r#}h'@&N  6 4hӦM7h'C;jsw}Ѱ ѣG5RP=^Ky䑵kF_p=kڷo?q`sΉN$}h'@rKKK۶m[PP0qŖϏN$}h'@r}; vi'pBAAAb;w<s1I@ĶHN&Mtg292: ȞIeeenݺvvڹK<ԫɎwַw> I;)--=餓[XX.Pbۇv$W#i';v1>.C;I&K<FlNj<2x9> O;^,4ըrBzЀbۇv$W @m \*'4 (}h'@r5Ш^,4ըrBzЀbۇv$W @m 9gȍ 2eʔnfNƃ@nĶHnݺ͝;7M3ft=8Լ@nĶH+0'aN1cF0r5Dr#}h'@taܹT* 'm۶ ƣSv> qzJ:t<kSON; FlNę8qbQQQ}4iRtRN 7bۇv$NeeeNs}v> ʶmۦRc=6N 7bۇv$Qiii.]vwi'C;*c|cK|H;܈m P'NLR.!r#}h'@l߾}…#F8sN>T*<H0GȍI1w;^}իW?sc9v>m۶mذa5kVXh0'̏i'C;ضm~^q7oM0'Ͽ|@nĶh`Æ +Bv>!͟?6m5kYgգG>[ּy2'' FlN}o~8ٶmۅ^عs]u?ioSs#;r#}h'@Yp7̑K/4RMҺu_"srpnB~M;܈m `F1a„ܹsvm&9昲`N;܈m `W^I~;߉֒]uꫯN VHȍ SNH~ _֒ݜp v>wzK.Tnݺ+ww FlNh*M>} C;L~'W]uU?=ӧ/~|;BlN3bĈ &wjI߾}9 v>,\F7ܷoh3)*ܹWgN Vٯi'C;ۿoΚ5+=m۶[nO> I.]կ|Ƀgg+Gw FlN4oڴ)sk=Slg@0?8+87sp@nĶh`Æ +2SR`f0?8+z`?@nĶh`۶mxWl޼9zl7`f0e@nĶhx۶m6lX3}h0'dv> )ϟNwqǫz`0x P$v> Ao߾p#Fs9 l#xp4zBN 7bۇv FlNA;܈m @"h'C;Hr#}h'@nĶ ȍ$v>DN 7bۇv FlNA;܈m @"h'C;Hr#}h'@nĶ ȍ$v>DN 7bۇv FlNA;܈m @"h'C;Hr#}h'@nĶHT#}@=m \ruC;K;.}h'@ri'@ŶH.ȺɥY>  bۇv$vd]lN܍Q> ALRRRl'xp4 wbۇv$HeeenΝɌ3wt*^m ,W^yeaaaOv2cƌ`kNsC;CsMRa8i۶m0 bۇv$N޽STvڝzI{%}h'@L8(}N4): `ĶHN:IΝ%ȖItWm6J{dQlN$*--ҥKN%Ȣ 1]m P'NLRdWlN*//Ft[ 2$J iSLYfMm-}h'l۶mԩrɓ/^\`O?tt(I-Ztw|%%%wH@RVV6hРh`h|ӧG'}$m >`ȑѯ?[@ŶرcWXqƛo9>s[n3fL ~ӝwYQQ$dm 乙3g.X >iѢE=P!}h'犋7mvz6r'$ClN GׯC9$:'^~VZCmݺuW^묨̳_v[!}h'Ǝ^WeeeM4m;v+V/Rdv./`-H@KB;ywUR" B;Ċm =j'K.0`@ͻv:nܸt;Yze]֦M֭[_r%k֬I8~#8m۶f ST;M2%PRRҬYpÆ 7ɓWG?ޭʸ̥͛W5yڵ{gGllg.Y?6a„-Z\~֭ 'TN bۇvyn׾;bҥ'tR\p{ʕ+WZuYg]{> x۷odv[oլY`{Æ Gq?嬪ZWUp  j۶_n|sCv@޽op~^ +}h'NVXqԩSvzMٳ;taٲeyZhZvy晷~{?{PgJJJU-[ '^F믿Nށ $VlN ս+p@z|a;yWD˖-?pr]ɽƷ!CC՞GN`B8êj~Zh'XC;=rbU,Y$ODjj'`={~K_ 'g.U'N,Xe .ށw}O8p`xgv>s{N,Y|Yfݺu7n\:?TTT\uUGya֥KѣGGNlW_}u˖-[jNfΜ_; eNN/5=jIծ Vvj&L(,,lѢe]nݺP^ +}h'bIx75kG>D.cƳ״H@+..~?ϿGGs.r 4C;s[lo~v4ye˖E? bۇvoĈׯ~OhyԃAm۶->hnz 7Y&M?Y5y䲲$IlN馛3gΜ{' >hD***n%KDgM6mҤIwH@uֱcN4iѯ++W۞z{ $RlN1Ztqƕmڴ)Z}[`ѣǎ[YY}m 4^[l6mZNc)JE!|fΜFV bۇv$W*ɥY>  bۇv$vd]lNNm \ uC;K;.}h'@TTTdFI(^m ?J E@! 几T)-" ?.G^@/CHUhkPP<^CH^`$@Bz|Sd}>șgf?c;$WPP`>f'2_5@h<Q*==O>Q֭Q{'sy)3 ;] ۶m'$''{)5d}:}INNNKKڵKMM8pB}:/+ϻ,Y7xfd'NUUUǎK.%@m>NDI&$''ٓE hTXXإK=zpx3 ;K<}R/NNN.jgAvTUU͘1Z5k֬d?YO07 nxfd'UUUOgM󩧞2Ox}K'ug bpq3 ; N >yfd'BB|!@Fp3 ;('@\>NOpR\\<`&={tjC; ~xfd''8/[l Cw x'fqm5nꫯݻ<_ffffdd 6L~tqڵ_%iiiֳI&oӦMyyH'jˢ >gAvgpbXDrJ5k 6LoL/[,!!wޑ7|S%.V:tW=zTNM}tV&-;)((\lِ!Cm=z4++NQ 焠xfd'A 7W|b&K*++Aeee?19q͛մDP 80%%V]yyy2-=zh?N1 焠xfd'A ? 5{;u\r{[nsΜ9ZcШyI{+qnN 焠xfd'A IL0g1j+'CsBP<'^㓽{v}eee0@,l`9!( |Pp#F'(XqNgAv @l`9!( |Pp#pF''FsBP<FPON 焠xfd'A`16V}7B`T|Bph(XqNgAvaٹ@֓P>(be+ A>N &P>(bV>|/cǎ 8Tt2 c޼y\\P< nىj};nC#+*3 ; nʶ޽{?*;13 5SOy^233 RVV槱6ĉcƌѣUW]ummfΜ)m:waÆiӦtǎ7moq={ׯ(b*ۮꪕ+WIF/_dff;v9g|˻6LZYf_ezŊ*9Dߝ8 p| gAv1Aʶ&M_wg']tzJ{UZZa\x=ϻjӴi^:ԼysZnӦ9%ى[Wxfd'! _7~+Nʶ'ONK8$:ꢋ.:{o6-]1=33 ;+Yfڍ@)?V^-f 0;9WԫfΜ9nܸsC?1vܩqN9-K.?8 pd'A>NӧO7iDݴs_'@4PI vڧO)SX_QQ1~ݻK5(?sss6nى}b„ W^y'c'ǃQ͜1c(:vasL={vя=7|cov}W'O,EEE5ںuLŋ<hӦڶ6 1N5jvZ^|y߾}LiyA=nʕ+ebcǎwM&}̛7OӋ:B |N'@ ;`';iڴy!Ƨf&$$\uUofEB&7:CnD'>g' Js‘#GZh߿_?~jڜٲex{Qrl斝ﯾs5"'Nt$:yfd'?;9m< +9'h޼_~)̙?;G9r|slv&YzLqƩ˖-СC=$>L;B |xf' RW_}]vg<53++[nٿ9{ŋ#G4jhŊ2;ѣG ;B |% &P>(be+ A>N &P>(be+ A>N &P>(be+ A>N &P ޕ[j, e+ >N]^^^AAZp|y|zDsB8<xWUUս{w6 n#PN:[26yd)ꏔgRierRM !>NpnÆ m۶Ȑ:[ nUmMP<"M֋*ۤBN9)89!d ~^{RpwYPߤHRM 6)ۤxSU@?o~#W_\GK,I&T&@?JIIQwr͟pkA($EsEJ86 qNgAv[T-7wZRJӟg8'3 ;;'\TSqNgAvK]lRqGh `yfd'cNBw)SPGKn۶Mr fQAC8!Q  t4DiӦUWW& 0Q-NpyZ>NBT\\xb@Cٲeˊ+bQ'N}w]EE^!*͚5K~ Q ^<P|zihtҒ}+B? 'xZ>NBvڢ"4CڻwG}+B? 'xZ>NB8Ŝ}+B 'i3 ;Eȕm.B}n0<{ZlgϞԬY2Vj3W~ͪ[c޼yV1Q`VK.ĨE.꽸 !vy B` ӂgAvE4Hjʗ'K. Ri}v`Xᬀ ݋x#{q \F<УG?XoaQgwhgk0 pꚟӂgAv J]x'NTUUرcï > qY?E3 `jee>˯`H{q`rr*x駯EPg ;[֍[l0G+P<PxVgϞT /кu디׫eee&LhժURR}w15d2e˖A=wA樇jPPP~j-j׮]bb … ۷o//ӧώ;dNrrD9ɺn ,6U۶m`mLʛZw17~f(S80l0Yn׮]c/RjjA`>Ӄq-ZNދ+'!ljqtf'֣9*&{kU3eŬN#jG\8Vn~V~r<g?}³RzNꨯZ/ǟz꩓'O~cƌ;9rÇ?jtÆ UKyWڵ|}՞Ue,//wYYY5kH[QQ!#jl-Wd6... >C/((ZN˲ҖkGn#O bX]`[o뮻J^wuDVfҤIǏWvCCsAFjkڴi꫄٬FonO>1{ދ+'s'8p:~PQ`9?0մZ{ˊY~#0WGۻv| 8vO/~9-xfd'w)>|75hQ&MK棏>JKK3j_vƍ KAgϞo>S Zݻw+VܹocӦMZ2l}R^|/Z;sYVrw6b~)@LL޻ ={CYIAcՂxwhho_TmufwDWiﴞs:r36ӈcI}}y29.n+`{ }³R7jݶ+TfOqȑƍ£⊷~uBV\٩S'J>ԩSRu۾}co}'NNMM4\e-K{Gn#懟 ._H.j j oJEu_W˓lҬY3C;꽸yBN;v]믿viw9ZխD֭[ExnGҿۊYF;avTZ9kI;Sv9^sZ>NBYZ(EaUUՇ~8vXOu]ǏWZv9Rʍ7W>6mڬZJ4}5{̘1R}N2ŨY/((8~<֭Xf?w[y@/~?ߡC㲬eߑۈS4@V>|w߭n0`ىZߔQ {:uԿ333ӟj A۝<}teo)F -;q; r;8vqVo?*޵ VϙJ;g?}³RW֭[yRDJ9VIWqi٥K{N͗W4n8*[n慎˧9ѢEڵ"{„ wO< 9su{eZl[o9\ݻ'Koz4\eF̍ .˝C6 .NC80bY.졇07j6hF^~em^\=s> ^\~i3 ;ł ,vW}+Bo322vڥ?z/ `9-xfd'Xn͛ JJzKߊ Px <P={C^yǏ[ 0Ao8!ӂgAvڽȩSfΜo?(z/>qB D'O7o^!Zj˖-1 'yZ>NBzk5IIIٳ-b4{rZ>N²x7CeeԩSO>o6(`'h<-xfd'z^H,? b = O  @-ضm۴i dժUf:s挾@CA` FvTWW\r̙K.ݻw^!"vڵh"  8D7Vh>|7əY ƟǏ4t^&F˶Q!@yBy<[CsBP< m8'3 ; n@sBP< m8'3 ; n@sBP< m8'3 ; n@sBP<xW^^n}ڳ/m8'3 ;wyyyCk-Y!e+ >N]UUU͚,e̗gl CsB8<9rdNTͭ nNMMgUWWO4iĉgΜџ6Ryfd' 8 d'OVW\qŷ~+'N3fL=nM:Qmrrrvz5|yEuQ 'O|rᤤ3fӧcǎ6lP?^zeff2E@Ξ8qbY*}DApPyyyvLTUU5iɓj9bTWImsO>ܨQkgff\R&O>vXK8?xӧO[gn۶MN}]w]Ν8zGeWqgAvQI'<+355u߾}j /y55dӟtذazG b%` M6UӇjӦ͹0I&׿dziii֗L`#F㤺z̙߿u֣G9"Jxfd'c->kfժUÆ Ss.袳g6|a~2dĉ V%`%{|g9}嗟#;?Ι9s*33'6J9q3 ;6' j߾5k9CW;wY|3W 6o,_* v_ 'uOOs5kj 7N0wEmۦO[҃ @$Wٳg/e˖(..0`>7 ֥{ҥn;rOԯ(*Ǐ޽{=gnnG'Jff^^+=}-#D}A}ᇥO~"sݻիPvm7{^5ey6--mڵ>~}gJ?###;;ϧ~~UoܷotkӪB eZYW믿/Voذa1\^5p> 0NN3ٴiS.]{Le'Z_Zzx*ݻwg?֭<5}tׄnG1i$۴iS^^!>B ?zTb#gVz03 ;[SNRw-[̨]w|˻馛TK3ʫ/_.仫o24i͜09#ߐ?54zhj=;}䕯T~'oڴ/ZnMfSN]C 3gG}dofGWI"me_U+mD /֗K|Iy_<. bH}}lI-!R~4iAVZfN."[URRrWVV}Z߈[Mhv+&LpB]vn\ɚHlߝמOɯk9׈#D}9#;yyyyBB§~*[nվ ۧ@ /p77έُdqUWۜo֗Ap. bH=~ly'[9.o񟝘KqBN5aAAAffeˆ btSɹzӀ0͝;wѢEm۶]{zZbѥKM6҃ @$^ɗ5 //Ϩ?gGyČ4.6G;vLMt\]Z9^J4h,ԩS2}ĉ͛7$@\Đo MFFZSN ٥|PV 80%%߶δ\}i@8~[7ѣw}Yk8}D7>Q? o۶m>}HYl~U{;udރ]lW_ _{Wvl~5,KWMja2{GݡT~Ι3|I4!'>:pYƍ^z=K :4==ꫯ9s[kB[)۷7zNE1b9YYYr8kI.]Ə/Ϛ-UZ r v'A Dǖ[|B &<3\hNEǞ,??Oܶm㯹Ν;gffy|i/Zg3 ;(%>;'`qiCc1>No޽ݻw0`@YYyQ= YvvĉS⟴*}DpiCc1>A8'89M{I&M8̙3s6FZJ}$' DI-\i@hl<+m%#~'!8 1$>OjE'oOB/Ν;di'~xfd' >Bp. Yyy=[/O%Ii@Ⱦs9ܹi#Y>NA|$Ld'hWPP`>~l|y|XOB%ɹDgAvP+YE >Ϫ'3f 8  UUUuݼm~l/ش^I5"5^Է= @yfd'M r'ONMMUǖLwiȑzS i<{D”N:ǖh۶ƍ@yfd'M ~ h۶mNc{]vMOOacO>N ,YҳgW]u>}{gAv NJJ1cF>}:va5?իWff!C*d'WUUչsguE[pn4>Dkgff\R&O>vX= &L<+TW? 4V |hIӦMCڴi#M4׿%ӻwNKK3#bBaaJܻu= gAvZvdN_~Nb V 0@>w7{gAvɹٵzj9sq*d'XdɒdݍǞ}G d'Z߮Y&77w~Ζ-?Q{rk[a]!p:*3 ;.7lJ@80gΜ ugՁ͛7VUUﶶEri\$J3 ;.7lJ@Ξ=;W_}BJKKΝo4B]gAv˭ ڳ!d'ȧ)S߯_lX֬Yd͇-NF utT<xWPP`>^pS;vLmܸ^Ӈ q5z@]p}Ļ]竇z+##;N:{)SҥKnݪDHpPG%gAvsƍk߾O?O2Gћ"vo>BfC7uj},ԅ:*?<+,,l۶m󓓓?Ô7E ;x%8e˖+V=.Q }={LNNСl۶mnnFb (***Ka֬Yp)G  ~hѢ'w7/YDoBvܷ~_K.-))%`q>z@](BG%3 ;7Ne' . pDNN>(cQ x>No;vLNNݻ7woN?~2̛7O}g-[ֆyIII͚5ۼyv۶m^xEP~n=K.>ӱq]0Wv7eĄsT<H17 d'y^߶m[F5kУG?XoaQWkCp2z7u]J*--u۾}ufP+%`Fjc!5wہ9*O ~hѢd0~k[XuRea P8WiZvvR>3Qؑ[mf:6 +` vwہ9*O OyyyR?0k֬d? CGN`N ~ڢE 5]VV6a„VZ%%%w}ǎHEHgмZz{饗RSS  [NIIY~jowV5^u\1?¹J;v>UPP պ <ڿZƋ-j׮]bb sF6~oms@f#F0zUsTnjsw9rѣÇǭFM4ǎSz꩓'O~& #խ:jԨ^zM6MͿ[n__ח|POPO:5%%駟V7_^p*\9|TZKf͚VTTr =KXg:ZѣG8pwYYYj}pp卨\ꮻR+yu71k 1:6lvb.q|W %%%Fͩu֟|كpJgAv"̙33fXj^pWXX8uԭ[G;@ߨQ-[^tEZڽ{QM4/U>(--M5_(W|y75Ԓ更Nw[1?¹JiKKK/5wܹ9\s5RF:2-KTKJJ._~ȑ#ZM6t% Rkn؊+Ԉ9W߈uLm_z՞={TYIs{# 8ṷBƍjb}~ڿ_|Eզ[n@sT<Q$++K`-W.N ѣG OΝ;UhK/6O;^vlc֡)Z#iǎ\pp˖-͛7wo p.mdmbŊnI?#߆?x;wXiӦڳ[Мp[[+i8b>&q!YUs `x \q[֏b}}AQ^nA&F5k,S9*O ӧOgee3@ 6lꫯG@cǎf͚}ץ2S5jn:Kێףjum*mPhoA8wb%pjasvvvjjuQski:u?ߕ:Zms+WvpCFm9.F!+8o$AgqGiܸ9})//OJJڲe4=sT<9f/NYffCv\W<}t뮻ƏjjڵF]%5j7xF -;vֱe*mw̢j})z=uTxذa{9x1:J 㰛Ͽ/2§Nz;t蠵ef̘1~N[g:նA:Ww۷)S @ q 8k*uyՀ 8.+8Bo#GVVVnܸѰffOSmpJgAv"DJ|qԖg}ɓa-[&4<@6m7oޥK{N5x[hѲeE%%%د5k/z-⬽WM8WiF>1"11.{衇d0U} 2$!!!==}͚5Lg=;P;xUի7x':̙3OWiLǵRe[kNd„ ? :f'omL6}q6… ݆x5Ά 8В_W2fǍfZGq|5#/Q x>N@̘1ԩSz9 $?~";Uں˗Į={$$$3pJgAv"رc^ s=wY` ;.77W3\+**ںuQsŹgϞ=)>=ܣ @8G%3 ;[oڵKa֭6 @,Xę}PV~K/tz F%&&fdde0}HW/-77W?,N[n]AAI7v[o8=.yT< kHZlϭ u׳yʺ[DoY+٭?3זٳg'%%5kljSNٳuwӎ+?~\u!̣}Hd՛By.NW>uƃ;rqT׊R|֙Ktہ]< (!̹8u̙3R܎Pj>N@$žV,}Tuؑ[ָV|g=;";Ԣ'OUVmٲE uVJ?DoW!B- 8dl[GU^^^\\ܫWiӦrw-믿5k1f̘;ȑ#G>|?HO1{0ۏ=Zp޽|a㸩z)vٯ_?m/qTވ>V7z0EGvu/޸q@I#eӧQU\Pjxfd' NJKK/ݻwNΝ_~j*p5벲&M|Giii?|Qw2oLhB56\|/#G6v6mjժ6S={+VpN8Ӹq/BWom`7/6ݺu3+ُُ0qܬO?MLL4Źqa7U[=m? vNuN#ԩS'ԅ::*G `;v\p-[4oqj~ZܹShK/ژi+n&裏|_ܹsbMjϺ/Ǖ7_G־ 5M o5jԬYO5=1޹>n=_{vaͪ)\d':"XӦM+,,?{UVIs׆?z@]ӣ>N@$hg`5@8w}vs)ҏL)rM}ĉTL^>8uWXrm9Ҹqcs;'%%mٲYfZFM{k wI&|uːu֩wa]Gqsn8Qr{#n#lYm@Pwq̙K.ݻw!vڵh"ySU rxfd' NYTUU >ܼSE޽N6{68pƼT]\\,H%IDATWꫯԍ=/k&Nz;t蠵ef̘1_uy ./n#GVVVqǁ/̟|| Exo߾SLoQFwq6j;.ڃZ}MU{/MOd'8|oSc^-}V?RdF= P< 󼲓F>1"11.{衇***T} 2$!!!==}͚5LgThѢe˖<@6m7oޥKڵ ixUի7x':̙3ޕQ0 :T zf'o_W6ƍfZ߸7jC/7η_hQv &?2S*߅wj]qsm8-ڃZy#n#ljž)Yd'-D{3 ;?; XB]سgOBBBT u``Oxfd' j1;)**ںuQsegϞ=G{&f@ c @>N@$}C~K/tz ,111##c׮]s&d'-D{3 ;oݻW [n]v~YpiCBdP< ^t~@,X1-D{3 ;2k,/PUTT̝;W?|qiCBdP<!+Wܲe~@H/^\\\f4!|l!2( iӦo߾} ,Џ1. b[ 4gAv"g~@0;uY `Oxfd' .]i&b0;vlڴiK"= }H[dڵkKgee `Oxfd'{;z~m%KL\>iԏ ̟?̙3O֯qO۷/\w݁ 1-D{3 ;[n݂}R5k|. b[ 4gAv|l0ؔ"= }' 6% `Oxfd'M !|l!2( |}`Sb[ 4gAv|l0ؔ"= }' 6% `Oxfd'M !|l!2( |}`SYyy= = #>`),//|hؒ{G |p`SYUUUFFƛo[]vgl = #>`)QGi߾oqV~~~v>)4v |p`S\aaaJJJ۶mx OKK2_o = gAv\po0ؔw7fddmV>:t ?Z64>`)oɒ%CM>/55_acO>N SSS4ݍ@} 6% &<#VWw7{+>`)1wթS'ݍÞ3 ;.7lJ@w7{gAv\po0ؔXdݨkiL |p`S_TT4wܑ#G_>L//@U^^7o޼gެYdOu@Co@t>N Q㥗^ڹsgYY٦ML_`weD￯Ϫ3{]t3/_^]]3 ;wևwY@N]^^^AAz]˳C@4iĉϜ9?g#m'>i`曬=ah.]kxfd'񮪪*##ÌO R|yǦ)d'h=qi)UYOʪԃaÆW_}U3 ;QFuA'ꂻT۷Gd'?xӧO[g~駿/uֹs]tM˖-6*}`̚5+j͚5W@yfd'ZF۶mvZPP,_jjjJJJaa=#FXqR]]#w[-UZ%fďg}ɓ,?ꪫڵkھ}nAoBvEEE?ϭsz)&Fam,sf̘q)=I??>:"3 ;͛ٻ(+>p01{ybDB681 N稓O+#`PDqf 0s֦ih\%FTia8g{`Rr]} Dv>uud2]vw.Z(>E;p̘1cه {^bEv~6!h޽?X͞=ȑ#8ۇvjkk$ݻķt o޴iSaS+WmC!-ђ%KlojڵVG>>0z( 4][܇v[ gu]_k&J˳öaهDf͊VRR3(}h'|K.Q;qhɒ%eeeه$هp?$O>a۰CZ3gA!y;tЦM={UTTӣٕ>>4`L&.-Tmmm$N|G8b;?߷ox-i+hTTTs9mڴiݺW^//9>k׮Í7W?cvpv%Eww[oѣGOvNvꏰpvwp Zww}wv@b;Dmm JFMMM|W^y3$ή]vf}~L&~W|k .N:۷,|L[n;wG8f̘`ÊaÆ 80LN4hP {>%:vڵswwرCwIN%`ɒ% N.ϟNF=|'>OիWG#ᬩuGF]!Clڴ)hܹsNpҕ}Qyy ]C;(P˗/6mSO=u/[>>ٲe~] 0K.d2ݺu #luׯsGNo|+g^ ׹öa#8'Njjj|ÇGcƌ[v;p ƍ˝x9N9{g߾}{G?ڿyFb_+YbŎ;ó5*w¶m=ayΝeee&yy ]C;(D7n\^^+}ه~СC7Sᵮ]fk6κGtMWΎO: _·GdĈC 2eJX ۆ=dGh9Ӿ} .cǎj/sj^ݻw&L;پ}{v<<|m۶xVdY&L]΄??~{촦y ]C;(8O=oWi~v4/ V@'NM6i歷ڵkW ?έ۸qcv뿞s9nX^j鷓;v^x/wAx/[nalvgWbN ???SbRYY$pJKKtps=cǎ=?愙aɷ ɷ`ȑ=PXp_+W9m?߷o߭ޚ7uR; v^VVv8f̘Ó׿8ӟGVyy ]C;(oW"~u 0 ɸK<~#G̽Icamf 'nv g2UUU{綾^zgф]v۷/--]`Au6k֬o'>غu=z<#^{m۶mÄ?sIS ήS*8p`ԩwS1](Lv7o޼gϞ0~0ri!ή#G_Q'|r߾}SQ[[d%uϘ1cÆ իWHki'1%%%8ۇvPVZUVVXmٲgGL&8Kf͚?+n ]C;(s΍:UoS֭[|jÆ +WG>PRRi'O; Ν;/^?+Vs=|p=Π¿?ܡC6mٳ'Ϗ3g#NviӦRuu3pf% $pb֦M֭[_y<->#^2vvq>gNNve˖[.~W|-Zm۶YC;('N8Q[[;~Ag8QSSjW^i~'C;9} PP'O?+2oܹso p% |; ^~vE{;vءC|;{ L&Jdɒa'^xavo֭ۈ#O<ħ>Ν;^:)++kݺuÑ#Gk׮apȐ!6m0~ ; k;u4sh0q6C;9} Ph6nSOF88q#G p% |;y3[nٽ{;s 77.wrl; ysϾ}¯pG?h~Vk%+VرcGuuuxQFNضm۹6,ܹ,$q6C;9} P/^Mp=y䪪; C;('N‰Y/;uǿѪU^{-/v=|2d߮;mFYvf͚p0u9*++g?ݻӚ:fh'O; ӢE)~j۶m8qp#}h'dI'yQF? ˛7oJ]v\rI˹+ljNyիq]tQlҥK/6l؋/Xq6C;9} P{plgS]]hѢ JbN ɷ`ӦMmڴy뭷vφs6nܘw ˫V:vcǎ /8p .aqݺu Mg3ӧvܹ4<?#l:3fl߾=⁳-}h'I0rȇz(,vmwuWeeeX~W\YwoodUWWm?}nּIx󲲲1cv׿uXu=z{NNv{oժUsϔpĬX Sm'//~UUU{綾^zgф]v۷/--]`Au6k֬o'>غu=z<#^{m۶mÄ?sISi'1NHbN Bb;)6si Il @A())׃6k֬{)ri Il @AXrXmݺ駟G"b ÇIŋ+++ȥ'H@$}h'Gg4uési Il @ضm=Ϻu.]w8u. 8A"C;( sy71L4>p\qDۇvP@92~{{BXx  K1NHbN KUUĉ3w[hQri Il @ٳg|x[H{n…@$ >Btȑٳgmuuu<2ή]} b m۶M>d 3gΜ3gNmmms\qDۇvP>s͙3gٳ>y-9ǭZދ`>&. 8A"C;O Il }25|1NHbNhqDۇv@~L %@$ >dj(b Ѐ'SG  $>>J'H@$}h'4QTUU> #}h'4{j(,YRVV}{C$\pO %@mmm׬Y=̞ ]veaSb>p=5|ߵk(D'Heee=z[S>p=5|AyyyΝuf͚pTVVַoN:ƧE#}h'4{j("Çv\8Aׯ_Ν Ѐ ,ZhСt~Ξ=;> (&C;SG ݻwN]K<Ѐ Ⱥʨr-@Il >Jt]Ѐ 5|p.h >J\-ʸKJȕq߼y={֬Y~0r(NC;(vUUUcckiAhO>}ȑWֆ9af_ZbNݒ%Kʲs/6E;S}}=3vC5愙a|E%}h'Ů/0Ңh'@q>}رcOa~*Hp[oٳgO a{Si99wܫW~wys'a+>>7ԩSL&~;ǧrh'@9zM7ݔ{z賟l߾}{˾~7%l {Ȏ)>קOA 0 ɄN: :4>E;oܑ)S 2$d/wr6} ۇvƏd~vi-OENb3cƌ dVTT\uUfo߾/rv~6!H𾚚N:eNٳķt PlnM6e>ѣGgmKl W=z|ߍNbsuUUUe^{TȀöaه@%Dwd2wNbc?#.F铝 {>R,}h'|(9~ד Z (6w Plkגof G_~ƌG?dϰFxX߀B;M8]`AaEEŐ!C$G~rVm6!H?;n7o޳gϚ5kϰFUaN|3Z(6ׯsGN:lذx39_~?r'mrGJl @>}ȑ#W^_# s0?¦ѣ7tSm8:uUW]5pp2`q↭¶u E"}h'(x=;СCu9af/, PJKKGyꪫ0;![5ӧ;[H懭+(` PۇvPtƍwu _BXt P|H>o@?CW]uUG xÆ :uri'@ro?yC;(.ׯsG&M}.Ι._ԩSs'mrG(X PJKKo޼yϞ=a0 a$Z{PۇvP\f̘`ÊƳ rxW6!B=zt vÆ gX#a7h%|͛6m>;믿ۆ=dRȴۇvP\+"^KӧСCöaه2 Ga>}ⵤ.,;?l}H!NJl @q}/F|;iAۇvP\b;׿O% ?Av @^C;(.3fX`Ao~޽{ǃI+";?l}H!NJl @qY~7ޘ;o~3LN0` r'mrG(X @^C;(.G馛V^[.{̰U6!;B!NJl @)--9rs{??߿uo~U6wB>b4}c~yafi'y%s=cǎ=tP|]#aN|kh'y%HO>}ȑ>i, sLNJl @Q+--ϟy={3,h{P @^C;(vG]~3F=lذ^za96-vWbN ۇv餝>H' @:i'y%I;+}h'N @^C;tNJl vWbN ۇv餝>H' @:i'y%I;+}h'N @^C;tNJl vWbN ۇv餝>H' @:i'y%I;+}h'4!E.'>ptKl h'[bNh@; ۇv@ >NHЀv@%-}h'Ů*a@K>bdɒvCH/]O+ -ۇv;ƻK<)>Sp+V̝;$OdyT{ܪU{G @Hl Iپ}>ӟ.՞P&UUUM4Jl @۶m[pa:nݺe˖FC;cƌڴiH>|xܹ+ŋw@$ nʕ6l_Y'n_"Il @\III:a֬Y NbNⴓ /lw\٧مy91 @1Hl @ɴk_ZCiڵ7n^]޵kWXSdIN-[ֳgϺ;{ܯzd+$:;v^x0:N±5N:ݻwwyHާ $ .}̞=ѥ$LX`A.]ڶm{wќܝG#Ƿo~M7 ꯪsg y3x[nݣGGy${۶mx~ISSYY;tpwF󶓦^NSb>vr |rb>5D;9ôb>sƯ%6k֬R'}h'qVZvm:ie˖gy&Il @ܑ#Go*N6l4iRyyy;-˧NzСG@% Y}}ҥKLx[ƯӢlٲe,++ʧ~zfe2P̚5gٻwoh$2L|R$}h'4nC;tKl h'[bNh@; ۇv@ >NH܇v[ -]bNݒ%KʲsIk ۇvPjkk/l>ɶ0@˗>r-={INrݿŧ@ >ZSNѷO2Lٻw0R^^ -\bNx_>}8`L&ӻwsСI%M0!\}ՙhѢ$hۇv߹s(={txR)}h'|믏I=%Jl d.UbNP>}2@%EwwxR,}h'gG]]O~9&R&>}z|)c7N]bN΂ &ڵ<;L4N>%L+pR>C;8ꄓ pۇvp '|2gΜrUUէ?Çw}w̘1W^y?3k֬}4(m ۇvp 'IMM/СCayƍO^,Y=,ֶjjxvKl P'|'{O>֭ogOt\?zQF@%W'|A/_>jԨh .8rHѣG_y啿ۿ7mvuׅ^{}yMnUPKرc|/0hO,Yҽ{̎TWWu]_~W^~СCիWgnC;%;vnz"|bvd;kOYf5(CC;D%ݻwλ+O᭷79p0sԨQaU]4wæeF4rݻw_revvUW=M3„/xl;i<3z~aaUSzՍEd֭aÇ{ۇv0a5bIq;9reˢ?ǨQVZxv!6-Zζp_*,?`xMn_dɗhwƏm'gFO_2ymE'ںuk挈4C;D?l{'uFm?+Nx\湓80lذ/ˏ<ȋ/Դh9N.h>h ._!C.ӟ#o֪UhgI)rgv5O\3̇c @*$LO>^g%NR!}h'g|q9[v C;8sw1 ۇvpF'솓c @*$LO>Ni'>@>9UNi'>vʔ)3 O&iӦprL;HЀ< o@ $fxsR }h'4 4ÛC;yHl 恝;wvaʔ)C g>.? 8v^\C;yHl @bN]UUUX-r @ $-Y,076 ۇvPjkk'.ǧ7 ۇv dz~vܹw_㓊vC;}ҥK/}K-O*z @ $v- ':u۷7@bN1cv/|]NR }h'|{LfvC;C%) @ $͛7/ɸK|SHl ڷoϚ5kfzM6-Ʉ)OWVV?ݓ@bN[nʔ) ,زeK]=tٺuŋt, ۇvМCM:u |&MڰaCSovC;hReeĉEwRdqϾ @ $ N4&~y饗.\vC;oڴiqR}=+ٳg9r$"vC;{glNXvU rh')>YfůS4ΝC;H͜93~A z;tЦMk׶o>TTTy@8ƅ^MRRRC;H5N>0-{׮]ƍ)MXSgX]kv @ $ ,W^y?g5u`MЩN+}h'qرon׮]ǎg/o۶oӦM߾}xs/뗕n:}#G>Q3ٳҡC|;{m*++o0}k׮aC ٴiS݆݉ԧ>չsիW75X d2/Ob' ,ҥK۶m/wߍV%u6bĈf6i۷5*zۇv׾[oj۶mwyɘ1cnݻw;7pøqftΝ;kjj~E+VرcGuuuèQ6G?ڿy65X VDa'~{}zĉѪ㏶{e6im=СCƴKl @ܩ]v{jzע˗'^oٳUVݻw?“wycYk֬رc݉Jo_~m۶M 6h'K.Gn߾=zx2d_Qtoxc @% TɦM=u]|y/7N6oڟЮ]K.$wFO5o޼^z=.k\*6D/[ y&_QL|NR/}h'qNb_~cIaX?s7ٽ{y睗=Ȏ;†/ ^3$`S/pƍM,[gϞu'wMN&i'>i'{sp l\}Փ&M&5//œ;v >3ԕ۷o̘1dAm۶lʊIxF/\s̈́ Uk'uMl?3871 ۇvNi^ؾ}M7Զm??ho/u;wn6m,.TUU{w^z_ܧOٳggͪ?vu;|p=zxG$y^}}Haɂ t6}hU7n'Mm9_}? % vWtL;H}d6l >>? @ $ n֬Y իw޺uK.䮻Ϡh'>~z֭kׯ\2"vC;۹sŋ)s=|pD ۇvǴiIG}4O! ۇvҥK׭[N-\p۶m i')><'ONz%>@bN򫨨xꩧI{N0ȑ#h')>&-^^_h'];y䪪ǟvC;h΢EVXNZ&L8prL;H$x{ݻwǯӒUWW/\pΜ9'󧺲Hl @ڒٳgbWUU0bkvC;(vK,)++>a<>D;H~eI6,_aSvC;wѵk(Dy`ٲeaO-n @ $c:ug>SVVd-[0ZܴHl +3LϞ=Ν;_b|RNR }h'o޼y]tɜ0bĈE'= ۇvjkkuΝ;]NR }h'|;%>RUU0Nbkhۇv{d*w,Y,06"}h'|(cg/OdgZye2wuwt5'Q;YlYgZʧ~zq}¦Md[x-fzgSNg›lٲf-TbN T}}ҥKLx[֝)?||e˖ט{O)={;wӧ?kr%mذaҤIڐR˗/:uCoDa7o^.]2'1Ÿ5hۇvPp iWYY9q3vnݺEs}ux+}h'e…/R<,'B{,׭[w @txwhۇvP(̙sxI(>2eJ)d2% 9rdP|}ߣ6ɸK<@K>jժkBڲe3< @&$ ̝;7۬YQNR }h'$^ ?ܡC6mٳ'Ϗ3gߣ@bN œ9spb֦M֭[_y<->#^2vvq>gN$ L;Dmm JFMMM|W^y3 OHbNw'QT/ m$"(0D+:E@":̀PA" 0 d ! w{ʩ>?yO:uZىsP}̙ziӦ>ٳgER W_}1ŋ###(͚5غu/ $Z-ZԺukثWbl˖-d< ;TmAv}zذaO<񄹳uNįy?sΞ=+>ϟ?#lڴɓC5wȨSG}$SRR.tPM W$;5hРiӦG-*0G?sg۶mmڴ=N233vԩSEej %;1رCL!++몫zWrssnNtPM W$;ӧ:e}AH76wnfltc;tRaÆJkzb2߶m[< ;TmAv}BBBD?P4O'd'j>N6; 4d'j>NBBB6@5fd'^D^#/@v~@}x{B:vo^#/@v~@}x˗/'&&BZbEvvz mAv-ϟiΜ9d' Xl#u֩W;fd'^$!!j`fΜYRR^@v~@}xiӦ={Vk^Av~@}x3gf|cǎ+VWěfd'^'//o'NPiӦ˗ːfd'ި8!!aٲejwrssw}W އ6 ;^= /ŋ ,HHH(,,TO+fd'˛6mJLLL.Kmelߪ'NhC<6 ;@9npqhC<6 ;@9npqhC<6 ;@9npqhC<6 ;tyyyJ<<Nh@b<4][o%Z&Mv ld' lٲ]vwv\oVDѮv ld' ~p 7\vډQQQ P;<?Xxq֭]?4hN6 ; ۴i#VZEEE-Vd' eر2;0`o6 ;mrٓoEv~@}'x'd' ~xbŷ;!;?>N]ii={ϟ?bĈnrb[vC#;?>Nڮ]*x9sfǎ-)G-Pfd'dܹ ںuxV=ECv~@}~x„ .]R}DOџ6 ;DsΝ0aY)' mAvpv5hР/7nxmEGGwСK.{oZZ/ >!;?>NKiiwm#FPu]7ydgS^b_1hNhgϞ;2j(%51tyڴib_1%fd'eK,1~Ou===/# mAvXov5-)/::7}@Cv~@}~;w%{6}@Cv~@}Î;Qu]g6 ;,Nz5*)cǎ}5 k @`QdܸqjZRސ!C~ k @`?%K]vUuԩ_~_+F0 ٳ;4̜9GjlR5'wqٳ͝žbsK@!;?>NKiiw߽uV駟ٳg^dj}wYK+F0Z mAvpv5hР/ӟx~uᗿ6.A{}͍6 ;DsΝ0a3%K}"fd'0a¥K,DSNOfd'NhSd' ) mAvx6 ;NOfd'NhSd' ) mAvx6 ;NP iCvo吝6 ;@9d'>N]^^( @KJJJII1.5 @+,,ڽ{|hd'k֬ߧ>Npeر[Nz-2i$+>N}JZZZ˖-۵k{n[oEjW|6 ;nծ];3"""**jj'|6 ;/^ܺuk׏ |rO}mڴIVx_fd'cd|K<_iKZZZ۶m].WϞ=x_EQE'/ `>N|KIL+IQEQfM[ ~xbŷ(ʨħ.">@K}tԩ+W=*5g%~Og+jժgϪFvBQEI|6 ;dڵfZbűcNJP 6oެ6*>|xMIIQ쟁섢(2Jf''h3;cƌ4uM)ׯ={K[RN((줔 +ʨЀoʚ>}GvBQEeNJOp>N}vueӧOxzWP_Y^F(TWMJ ~wh\Me;JT OznMe_V[r^~;M:8_U)I)  v6lHNNVߗ5g Q`YAA C;O<ٓ>WՎf.ms Tsvn5YgԩS'mS^[U{v4s)#W߁U2;e}=!Bw~gn>~{6j8l!G?м{ݿ/0yOjOh줔;x?3/֯_a,Yn}y2'}<\N#ۮ:uv>v*Ѩ}+4%LUeє\V77K?!df"NOi$D_ݛ5kDnSq3mvRJ|6 ;^m…z3/.\0k,Jd'ֿ={v=򩢋Np_ӦCzh7Lٰ&_]׶m`1\]:1K^yzм=>׮[ҥKo^Liˮ!&#[31ÿ-G#|r=јwvȑCCC]sMI˕esܞ2d/4 azJN'Q3<}&i} /^dkRӶ/EOju?"ڄęQ{@o91*UW53e?3fN#{8]]p{[zͷLmkTn&c8ps}fѭcBٰ8/6 [jn;v(cwql[ىӬWoRZZ>N*..^` K;wN=Vd yM1{6bm]W㭣11ѣgҺw2aܔv72{WOĔzhT۶x/y(F9tYٻ;Nebp.Lq.ӧ?j\gػww azJN^>^e4}M7iDmYZ/z_v֞<)\nvQfnd7]]p{[z4u&ΩL|qml=79ì>9xxRR׋ͻࠬݲEyO?MjzŭEvA}.3(>>^=Vĺf*+,,aëK?aTJn/JR 8OŝŘ'pPNgg.aoşoO4}iN9skzvʫfsOnt Nr7Ovwed4[syxtcLݺQ[dvݺuuu_0q#bcRv?񗫯j+ ?~ŭEvA}pBu{ݨQ_o)pj*ȃ5I;[mb2999b37z:lsDl>d˓OIh۟)8&7K;G.' z>ߕ?__~9NDzӔlZkԨ_W]_h1xp{#\NX|. a=Ӆ*u8;sedU?N־Kയr9M۩/X'&h9W.JOn=lQr 8 n b=_dKcd7iƘFurG7v?gʯ)pGs)yyHH/a<5ny>f͚0ӬW";>N$;y5kvڂ2?v2qZ wj*aM igP[Wfu4X+IG6lXp3 >m]3ʼh<]yPthf͚wEOYGSqMn"mV7GdM۷ ۷W7c䟩ye\cYNiJUtSc.]:(ggHhhM9U:yYٻ':\}߿}zذaOUUï8 mAv6;9p@:u Ϝ9S~?\>m۶6mYў#Su)sΐ'gk׮uo;l4NA OAI*4RRRĶ8 -^٠%2S54;эćN(r5~kmOQ?^ppP>7K_q x/mvb܉ |Ĉ999۷o/*VrܹXk"b4‹ r_O/NQ_~e޽M&CBB%s5j쬽 J۫j; eIg8] cvN(w:;(e'5jϕv׽q.eۿ DXznMCj복^uY(*`6 ;˓줨0zըQ'%%'6o\<ձc GyD~-ʮ]A ♙w}wHHH&M}|ٞu}}Ǎ''OԶmyY3=+C ;uh"뚾u|ٸdɒVZx衇gM(&o WvʶN/qƶ휝FSj*ۣNl?E)Ff!nʶ <}P;w4Z.^:ܨQu5VgԩS'mφRNmo|sF >gZxR@P=zt>Q1k*y8V&-dDy'vnEk`fd'{y@a^܇O$;QQ…cZ6Vl7e笃WyvҩS={7ʖ?Lv_UxyO6Nm~7~ss4i~"/֞2軒n?~dddUYg*m۶4͓)8ɸq#bb(PEvA}1d'>ى!11κ:&jEG"$n?rgd jڠ&$Tkʯk۶FWsbTTj+ mt5'M//y:e~׺u~yoܔ!C~t~#Z( 5mXLXG Ns~eݣn'2"'RxᠸjWĥOЧNS1WC SWÿ-ҥ':|7lqvXʙ*|TsرEx^_XbԨ_ee͚51vh?G3t8Zϸq#-1+d/:[Hi* x{̀9vosN̵}Ǫb#tjz/yAkהx]FQϤv5f̝K3ڶmf"cgʎk.hԩs싏e.ڝˇ,?ṣGED\n+}S;ÿ un|wٳs'Ol4g'gϥ7[^^ۗϩyJʶRxO4gQ{nC}sȦ-Z\#feWNng;[o7úI N2tϯ큔ϝ^p;(E=;wr˖-^xZgS;E}`fd'͟????_]lȜ9s"loşoOF>g_sMceGl|5/_{m3XkV> uxV׮gq#g>̻;}Lb@|Ę.Zp=CM{D\TM]w3=sj87`<+?w4/,禳yXL [gϯm,q~󛻭Ϛ9NG1j ^wYדXv2n)6 ;^-##cٲeb3/RSS׭[addU?N־r;˿~+„-Z\(הK;G.'n^k<߽mԉS3g?q࠴=kطo91>|c/-ʔ3UjSU_SOk8L^^s1C~0a}bwlG3Zkխ[7}&-[$^XRT mAv]bb%g/̜9D+6;?y!!{w_yr#4QPPØ.Nqm۶j(xܸ\>bךOI!bM?8Y_.:w +w=xp M۬Y1Nڿ)Sepe]T 8p`_.ꕥsmWO(%2`~VLKnk{y^>)X7yԉgxhsbÇk_VS;E}`fd'O>ٳ3^{t rkߗfoESEQVd' yyy3g$>?ٱcNJ+{Nך]PEl`fd'7M>ĉ 4/_BvZj)y), ℄˗Kр͍{w6;(N0hc233{^x!==ŋ4.]`BN((6 ;>˛6mJLLL@Us\j~2[lo[g#;(<, \. ^섢(N0hCv[N((6 ;@9d'%11!11(<ET吝6 ;@9d'>NP iCvo吝6 ;@9d'>NP i@g~d'ʳ:mAv蒒RRRDgmAv ;ud'Fv-+O}ʘ1c"##e|"֭[O4I fd'ڲe:\VZEDD]q?֭С٪U0@fd'/р/_vi022R'-[[~I}_ƌ#}->N/mڴq\=z[J}'xfd'ɋ/rxfd'dgg\2>>>7g%~O[zsԛM %%%k׮={+;Vڼy>|xɒ%>LMMUQwfd'Ϝ93--M]j̙s%f6mAv|ފ+^{5umYYY3f?[P m˖-۾}h x3f\xQqG}aÆdu&YYY)11Q][||zj6 ;>)!!A]:W_}ZzqիsZ=P I/;IOOu]VpVqɓD.N:[njg^wfRUUJd'% Ig'Ν;CCC3gz衦M?gϞ홙C Zh\7b^13f!,,N#\.($Gx7o޺u>(>>^lGDDmvd9ŋ###fD1rPPP˖-dɓ'qD{L/ƹ^鸺IDATsq?8f͚oݺUv6G/ q:k(NS=6 ;>줠`7|l9rdnnӧ Oo}yyy}u]&;;[ F hF2!~ݚ2eg̘1uT}M7Ξ,yϝ;']222ԩ##18QFsbbbfΜ)t؞|Iq^g>}dgy"A̜N^/ۣ8Mp$OkLXXX 6mzѢ%ԯ_?}m֦MSN>L]6!sݺu9"%l9SN픔-Ɨ2,v̔vʺꪫ^y\c_yk֬СCQ7Csΐ`i3q:k(Su=l x mAv|'ى\?}СCL"<()44qƢu1MMMMdYr6o=y#eڵzh߿mۊ,#νQFEڞv(miDr:TSN $ϳ/2''G4v),Xn\?t]믿[lu5> 9lo>k`Xodd%;"hܹEO`]V܉E=PrۚGsĖr"gzvN< 7NKh* zѣǏ%daذaB~_ܹsF2V̇>bĈ۷F 7+G}ӌe̻[7iGWI"Xœl"hvDlϺ=Tmoi8!;/>NOf'!;/>NOJHHPWFv^B}Dvڠ>NOJNN޳g8 xcǎ\RA}te>zbŊ,>mAv|s=.Q^iΜ9 %e˖KԀIMM]vzj6 ;>,!!B5efΜYRR޾Z>N+..6mٳgյjkk =3f;m߾W_UZ@fd'M6ĉ5P6mڴ|r~6mAvAqq… -[.`5.77߸qz6 ;#33s޼y{Q{ $$$7(;ho._iӦ2 Qq.Kme˖oV)D}˥6G !;7mAvrNM}fd'(ߴ !;7mAvrNM}1w}7::ZNp%--e˖۷OIIq\7n(#ծ8mAvtruA P; ~K/EDD~t-,_\fd'Aaaadd NZlŷ6 ;_~|K<_iKZZZ6m\.WLL W?Ϸ6 ;O^z%ŷ6 ;p֬Yqsq\EEE Y)Szl޼YmBԩSOViKNOi3NOviP.\c-Z\|Ylױݻwҥwܑ#GEEuM}qNO'fիWv>c'tugΜ) z^{Kܹsx ~mr;));Rԯ_0#8qOobݻlڵuO?wT6 ;bĉK."##?.ZիwÏ :thBBBJJ: m|ݰaCW_5oJYY~^l=zM6]>N@Ou~Cʖ ]A(mvrovmذAl7Ѹqd# .IIImڴR^?~|Νw.~&$$ӧO۷k׮oܺu&8)$44K.gΜSRRuVх{ɓ'Ο?on2Tc yK`O+6 ;eĉjg'Ew)Ø[nѣvu]׵kק~Z̟?gϞ[LLLZZy!b wݼy5twѱc=zβ$77ܹs/^1aС)$e^k.99v6m6o,;; ;xEΙǻD}۵kכooQ68)*ݹs?bcԩcƌ}􉋋ۧN2~ IIIzke)|Ϟ=Cpu]~c vhѼpoP n:ꫯcmP{ڪUofoXfÇ,//S-r8v΁(OfhP \:}\W_կ_Plٳub#++몫*((0z^zu9::EA={\266vJ' 9993reRU ^pAl9r$88Xv͚5+r>Vղ=_CkK ;vL}3Xo'@iP ܯ?wbnN򂂂v)>o޼&MlذSbJ 48wl7%Y7obl;K%n29K8}Ӌ~{ mAvjvH;w$6O~ƛn74hp pҤINKó>{yѲqƌN)#+|׻wvJ}󱪐1ꫯ6*CkKIprŃ@K}ZoļxmEÐ!C:utϞ=[N@M3?Tg(">hpRUA}b<4vP>ɕ*}$mAvjBaaaN%`cwߍEEE>B̝;Bɕ~x})ƍ=i$+;P !iii-[l߾}JJڸqcDѮv`{6 ;5[n.CgDD0` BԜ^z)""[nej'w iPs ###o˖-kw iPbcco~kswhPڴirbbbkswhP^M@E*>N@M{饗\.t TܹsWNsq\Drʬ, jޮn5ީn7xmAv~:k֬%K>|m޼Ymjǎ[bٳ׮][RR0F խ;Ս&Z>Nt.]={N6stu7mAv@@ʚ1c.je {P ŋ3f(((P2ۗ-[^@}M5L}̙h*?99yÆ ꅀ {P *5550".. {Pc j֬Y.\P/@NNNBBz9Gu7mAv@ :wҥKՕˀpb/xo@jժÇ˖c[lQ/ B۾&>NDeILLT/ ?{wUu}M "!$5F VmEބ[]"Z^A $RI kC PhA oy! 6g]=32s|?dٳ>LΙg29!c874mAvXBnul=z4""Bm${$874mAv=/ڵ ߿-LDVZ zj*EAHۍxt5nou0;  vrUUܹs맶0qeeZW,?gCvhXIDMs@3fd'ؑk߾,N>C/bYYt8K*Bff1+{[fMLL̘1c÷zcǎQQQ;w *fdV7gCǶ?q.W^y ƍ'ø8qzǪ*DN:EGGڵ+%%E.rAسg&&&ʵI6HKK}u &n#""Lb,bDnk֮]ۥK'Ov#bEC 9v,\{rrO?-~ӟ+M@}`G~/WVV#G'N|gKJJ.]SO͞=سlK/t岲2^r9tPޠt,ٿdžՍPm}s\\`w /s΍13;z˃vΜ9?~TTܹseyذa?Gت^ٳ=MmѢ cN\ZF~\|}]60a\d[n=|EEX011hy{/ +,,t׾tl &f>N#DDDiӦC'Otz֭O:%޽;66V6>esvrY^3j'f (_{7څKOOw׾='M@}`G~/_t)11qΜ9|qH۷͍=˞eW3_z>%auc~6Tyl;֢E >~^Rsٟ#͝gee=Z,>bĈݻw{.%)kAHOOѣGxmƞu† qQx,Y>s@3fd'ؑǎk׮ݹs犋E z#G׿MŽ;s@3fd'Q.\( ?i \w}hAN1WTT/_~\9J?#q"TWW^k׮r>~ԨQFM874mAv3l<|GEE9sfN^z\R6x۷okFFFĤxN*f'nS6mR}=wa J{l;m߾ٳO?txxPϜ93vذ޽{kξy:e؂qɵtɡCm 'ȥ MԩS͋]6W.]DӧO_^}U].]6ڥ;v6j he;`~6T5ѱyf?Bg҈>0&f>N&_TEAHhcСCvN0"4iv|ҤIj6 ;RRR Ka~6T5ⱽsذڴi.KmB(;)-- ӧω'|  vqӧOsqС\EAHmu6 ;222iKHKK~ $ض:MC}`SK,Q-7P_Vǹ hl*++ [@}9Bl{l[&f>N ̙3܉:ض:MI}`_6lP0CWYYټynܸ9v;s@3fd'ZFFƞ={ԉPTVV`˥Q9s@fd'nݪNg/r޼yLڍm㏗/_^RRkZ_EEźuVZſ>s@i𵪪ԕ+W:NuӚ>z7xٳNBض:M>N7;vjJCjN@sp\,_N hNxhY!:@izm'G}ԧO7MFv >N@34iRLLOoNNNttYԦ| ;fd'uܹ{NpDjS>@i|p8w.~FEEsԨQj#@i|ӻtmׯWN}SUU#8 46 ;jҤI2;yǹK}^{5wSRbYуQ@v >N@;t}s /&eN h|˖-[vO>ƉـbYуN}رcjZr}?7ڋeECh8mAv].Pn>^,+z00@i^zQzh/=N h|NըN={|nj|N}ƧdjZr_F{wBv >N@[lڵk?7l0h/=N h|oIJJ߿'w/^ln,=kHd'p47o>;w4jjjj~_};0`LMzO>|YXJ,+z0jN hOHHz2??Æ ѣE^,%5W0@iTg̘aNIDK^,>6h8mAvJMMK/4cƌk׮ymDK!h8mAvPMMMrrrBB'ijhIpԍN}&f͚ǏJSE|{ ;fd'9ܼyСC˖-{gFѣGSEϪ N}Av >N ;fd'e@i2N @pSazfd'Ц>Np@hfd'vrDyfd'vt:DԋgmAv`wUUU{6#;裏#)֧>NpkҤI1112>INNNttYԦX6 ;Ν;wt:Z^m i|{gTTTϞ=G6 ޥKmG^~fd'ZUUULL NK< $iˤIdvsx@fd'X1p@Uߐw.>Nt]!L}ܺ||,Yp8O ٸqcaazO}[;xE֮]{ 7jm۶MӧOgdd#++F=t>NM]vm9999p>|X=֤>N&%%49CF-HX6 ;sդJuvӞ={֭[Ofd'v,Y7Npwrssl٢RKfd'^<=N˗/rz`C}{YhQuu:hժUmAvl:ʕ+oܸ^fd'F6mt u"رC=>N]IKKS/Eh`#+VPߪU+>==z4""B_vڕݚի']۠^fd'FNv9f̘v 4(##P\\,Z9rğzu׳.nfmf.K}f'oUUUӟL}{8zז^]ƞ  mv2`R*[oձcǨ;wӧw!22_,++&LSL1znذk׮F>d&F_ڵKHH0Ux7EC 9v올LKKmDeΝkqyc<7s8r_Ljwc-clyNSvJIIe(˥YK}7+,,#s)^r9tPY?qg}ҥKO=ٳeӵ*++?Sكz뭞={8qv51] s1ڋ[?B(j Zh!sN x]kKYڭvw|sw*̙#V1saRy";fd'FN?ޢE ŋEy߾}o*nԩSݻcccݵ7&iٲg}f_i믿S悢Ƙy {v]!s=ovIIvqcc.n晝ښibyNSH۷oouN /ze{u-qm۷Ec<|֬Y<%y!_cu/=zj۶=zgĈwcqf^E~m^;^Wa.: X6 ;6Rwv⮽IzzRu򽸸XTo0lɓJ|1?en`.( FƘy݋ϋ۷WWW'''Zs͋9rs_d+ZUZK}f';tq򪪪]vM2??mڴBQ/rsse~gyrϞ=r)ßN:Ȗ8Dlgc_c9˗/O8QV:u꣏>5իWڵ͌^7+((+#WkZ'.mAvlD;v NS]!;fd'F6nxiuC檇" u"ׯ" %K@}TTTmAv%;;t8u:  Μ9] 6NYYټynܸRKfd'222٣N-Xrj oݺU /͛GpA}㏗/_^RRΔUTT[.55!C}[JMM]rz:q;|իx㍳gϪ ʴ jǎiiiHMu8ja֭ׯ_WipUmAv;B6 ;NM}d'Ц>Np@hfd' i܁ڴ ݹ\.C%;Q eff:N9;Y!!@}]UUU޽N#)֧>NpkҤI2>IVV5kfd'׹snݺ9NÑUKԫM8mAvpt]իרQFX6 ;ӻtm̘1ׯW`}_ITTT\\w$mAv4iNF]J}_vp8 ]J}%´ p8K< ip݋/^d,Yp8OZp!74mAvn{ܹm6* 5g@fd'I">4mAvЬ'w( OVzWdr=ׯ_'?~|qqlׯ_?Q/51 N(22rѢEC ֭ۧ~* 80>>~رw.mAvSee巿k׮rrrٳeO>}vY?~(TUUnʕ+('H]sssEy͏> N2ż @]fΜ;9s̭UVnWK߼ysԨQNSMpx|'EEEm۶ .tVmֺu|X"fd'u֯_DYSUUզM7nk7oN4ƌuVfܸq/y(WǏk׮ѣ, NN:GlAAZb^uY<<ɭٵeQS>N4233ccciӦ۷gjjtc=/*s[ݰ!˚i>NW<L}h̜93%%E[og8p`lllnn_l٠A+d3>>O>ɾ5eYsǷlGϝ;H.({#8qf byfQ?h YEY=z(nڨ_sYdk ٲ.11Q ^WM6{7x`X. ^ X>}Zpԩmg}ֱcGY ҷo>},+ݵ[O<ԩS.kfn,뫙> ۴isecUx ~ˬx޼y=F8mAv`En?YBQQQ6mKQ5k}]vufFe.\pVVVe1c$%%8p {zˌ4F8mAv`Qn]|+Kݹs!CW_|WwlkYsYɓwn+^V<Iz!уtou{z@pX#6 ;k`f..>!8B#6 ;k`f47Ic#8B#6 ;k`f:7I!8B#6 ;k`fn@pF8mAv' CpF8mAv2'w=p2?TfgXN@}@t:ĈX>N@}@ݻ1`,dggGN xuܹ[nNpdeeEjS`fd'p8w.~FEEkԨQj#Kafd'ӻtm̘1ׯWX #6 ;VUU#⸇*:F8,mAvnҤIrfaȑCF8 mAv.//k׮cРACF8 mAv =T@(a@fd'`{P>Nܽ/nܸqEhJK,q88SRR6mTVV7+,7a2 zZhQFFӧh۶mS4N8fq;N X}`fC>NÇϟ^D!'''g׮]SOb`]@=dRUXX$~',0T(mAv_o٣^/2))ի)@00  H}˖-[rss+E ,Y`Pr +W,_\F$''@c`*X6 ;jժ"E͎Ypb`-ƍ7V\^;\|Y=ChF ̂C fd'4vq'NlڴI=ChF ̂C fd'4BRRR3f,1T mAv@#55U(lFZRkZ{׎=ޭ-XB=ChF)Ā{uGFFk׮\cɦaXg`ƪUԋ;5 S!{3.yEۭA0h܍l޼jU(G]V`f*Ţ#GrS𵧾u_3 V>Nh7pq_j4:j5F6no^5*XE`f*G5n.F2u_3 V>Nh%o߾Cq1\PP0nܸvŽ[ןʥ|(Zv1**jΝٛo6dȐcǎʴ4FTvY\ֱhЩS']RRRDY4es]C7o߾ &DFFFDDL2ܭ*<{px ek֬3fP4(-->}XD/Zy/233/זkquVk={ѮN6Xvm.]򓟔L_`fٳg"<`s:sY~X:('$$}|9:>Q7{cx➯/ B}иK}{=\`w /s΍1B{):Bڕ+WOq.nz' xw-yQQzcql0g` yܹ^CK/t岲2h 6g-))tSO=5{l_0Q_\z`_MRd &9{ ;0svO{Lϑ=F^Cfd'4X\{aԟ:uJh/ŋEy߾}J{v]{Ƴ (+uwYoٲg}fn]|gzaiii֭Wxݱn:ui{`_MS$Kݫ W5++KcBXE>sP|s$u|⹧f =֍Uhwq~1q]m<x7 ̼6:r0kX7 V>Nhy^fRUUSO {ϗ'O,ڈ+ؑ#Gj/G^~t^|yĉR\GF\^k׮7ՠuJc?3<#+++Sw^/={JEMVXX\Y ^r/^^7oӼ k^cG}t޼yV`fEjGYlxs$#^6:r:*X6 ;%O?o}?iEEl|̙cdž;--]vwvuꫯN%ҥKeɓ'*('ԱW%۷ؿ,FFFN:U.ۏ?G={= .k̙:uzrJYu^lڴ딇\^;_vߋvuڵkt":>}zyyl'&D؁67n\7T>b('#_恁уƞ# _UhKy${233Z/s P* T:taQ _Bm810 r B}hK;wƆӦMs\j 5<;IIIQ P* \͝>}S`PB ׯdddg͈Y0c`!޲e***ԫCf,Y4;fAhz֭S;9x`vvzn' ֢>N%55̙35"` ,QO Yb`-_nܸ1o޼22 6g,0TmAv_.k\nݛP zWIII_~z[_^= ̂Cfd'ƍBvBHII?c h00 >N܍gϾWW#+zW\ZUUf͉@hfd'׷nݚp8*4Z;vꫯfсF8 B6 ;kp8j1@@hf@afd'` ,>NYB}50B#6 ;k`fF8mAv=p2?Tfg,6 ; t:Q/5X#mAvA*..1y>}gi `p  ɓ'GGG9YfM,N}@ܹsn8p8>0**JԫM,N}@P0`֭O,K}@P[fMtt㶄׫,K}@P ]t  ɓ'˙'x{>N up 4{>NT*%p(,`͚5{P>N>۽xVd!~OX… \n@xƍWX~7fᬨiӦ2u`K@ss-..v[ܶm*K9sjjj-Zqi4ĉk֬t:h*Ih > 8>|xyyywYxk>N,7I!> dR?C]aaaRRmAv (ZW^e^~]'Nؿ~xŲMJJJ\\\~DX&8 Vu'EEE-2dHn>SYecǎ---s!p~ٳGʤW/ l@} ɳgϖO>e933sPUUպukcsMpONnn(o޼G٢p)Swg˖-3W%K l@}3g|wĜ9sVmҪU%o޼9jԨT[} NW|RTTԶm[YpBNnFk[?)'O5/^| m?999INXbڴi}߿**/]cNjܹ.8q=k'^+˹s*}KCOJ|+;^4;v8p@T'NlڴI}Ҵ 2s̔n]p3طo cx3gʓ'O>=P||… EͲe $ 80//O0~-[>#6BRR[n/E966v۶mrsG?ӧObbX~keSׯ/))QXyK>g Mko&~ A`]@P())9rw9أo߾]p„ rС+V/?ѣG}d'Js ?7m$~XX؇~( 6lY~(f;w|嗻v3ψ-|FI5za&53Sjjad^vڕϹZRk@ !M}Ɓ9}zpB֭DСCѢPXXx=TVV ҷo>}L\ܱcGwmZ -ֿzFlpNNNn_afͳk^Ѽ75@ZjI||q%(>/. &dȱđ#G'nk5؍6 ; 3".wϟoꛝ\}Ç;t`W`bmڴ'N>?ky]fԻf(..7o(]4VNd8!Lsׯ¤^I*^=zF؍6 ; 3#>1733S^xY9l0***jӦ͗_~)Κ5… {?`ƌ#6@~-[Oӵu6Фf 8 ko׾}{Y-18/bYY"K*b N@m͚5111b [;vڹsl/9ΰ0aBBy7pHT2رcF}ZZD<۹sgc,u;@v`7h4<[g+^47n\޽yŋ_"\\W#;&Oܽ{wg} ϟ4i;tRYorw205$HvRYYꫯ9RO8g-))tSO=5{lscϲʗ^zeeekvsСAZ֭[ТB]DhPPPТE]vrQQ锋x:؍6 ; 3'6'w0q%ѦM:`;؊ىpرvڝ;wN||Jζ#G׿KrQޱcGóϋ۫ݵWKNNq3ngNF}&> n'Aw0ze'BBB… E矟6mZaa(ڛF?***}˗/?s^ze'b-s):8qҡXG}$^zu׮]R^d'v>NFcPq+;J}_|RTTT\3gvիʕ+e_~}_vmdddLLLJJJóW_ ڵҥKO<9tp`|\vBv`7h4<7I!8 Zh!;mAv=e~<*" '$v~N heff:NyQԋg 7Ip 8 6(RRR!M}w*..Θ|4f7nطo_7MPz8999%~n`"ޣz{nx5w0mV:}{ァFihXș+VYfM g?ҥO;(Q6ׯ]eddB6 ;d׮]W^NSVXѹs.]M Ȉw*{n&~ `ٲej`KK,Q_:mAv4T\\\TT騕O ( 6L'K'FmE֭Sc9x`vv i,Xp8dv2a„׫ (GyD} ;GZZڙ3g0f`F}i PW\1aÆ=CcU=zى(qƍ25O 6端 l@} 11QN;N>gݻw`'wq\ ,g|wތ =hڵsEkW \/RBZnn.δ 8K<+6lOƍׯPCsҥ+V|;fd'@wXx p7xcW^U3vW\ͽ>N-ZR/^p8,Y>ċ,^jreff٨?ew%ˍ7.X׷nݚV+ ٱcW_}0lI} TWWϙ3XSm6*Y',\p͚5'NPi4ӧ322/^USSZ4=mAvbĢO`!W^]hQNNzA,//o Evfd'"'A|Kp¼y # @vfd'hLEEE-2dHn>SYecǎ---s!8 UV+r|ׯryyĉ?^t"ۤOԋe~&yUVV.`{]nzp푝  SQQ8`rssEy͏> N2żDppu']&ɳgϖO>e933sPUUպu+Wz4ĢE B? Fvfd'hLEEEm۶ .tV{֭ɓ'ccc͋"8 u'3g|wĜ9sVZj5~D͛7Gt:Վۿ?8AHII!T36 ;Ac(?t IPoׯ_NNNbbjӦ͍7l7o=z7رcg̘> … 15G9 B}1yNnϮ-[ŋNj?89uTڻٻ(㋈@$"ȑp "rx˱o|E x3_ + DWeaW !\&!W8‘ a8E6TwO$d'OOMuUuOw dDojJ9RL*>޽{W\{u:b/^/liӦFi͝7oz,))4xDv a}ϊw?8q(pE)'S=R' 4ߺukU&S:u$^kQپ}k s|]kgk֬QuVd' *Ɇ 7n|mZqFXߵk1bl/Xq݊?gϖAرc۶m۠AUV9M/ 'On׮{eddJr=zbbp:uƍ[Ŷ>m:,zzEqqqzzS'6m5Ô)Sԫs.]JNNVuVd' *1ch@||V裏&M'v7ۍ75w}]vkco±cFjjjnd]ѣu=u~W{9}o?ۋ/ Yd~۷o/uXLG˖- ,իܶ''OlѢ]wu%ʘe'۶m\Z["]رuɋo׶2qJoF]x4d'ىg =z/,,[lRY~}Xl>uw}޽111jժݻk׮-+´>o[1;1°af͚UPPP^J.?%[*뵮L5'*WŮcIL@}zM\l ,pXJcoǏRȑ#qM'gvtᮻhLwW:,ڽ{wFٳ6m=8ohl6m̞=[u89ү_ի[瞓r5|ܹ[#Anna.C 9}-''G▔ٌJE|9&̚5~!!!~{ffҕF?e7߬S䫯6m[ĶВE31nTTԩS!ġYMpAJJJzd=GmEr=zQj׮-z^v7=V/ݫW/QߤIq,2;Q^ľ}j<<>ňڎRKb h?*mAv oֶm۾KZ`/{=~E4W__ym\Q#חoQluXLȑ#?xfZh!N4I@p \g'999f?|q80//СC^?~x[1bę3gN><  4`q :uw-v񂂂Zve 6z98zV\)n|1qDC(kZv"dΝKHH3fܹllzhbb*UOzz2bb }Qq۷R7ˢgcǎZu(n#ștIK;/"$$D\NⷡԮ]o5F뮻䎦ىT] ; g@>NXp#> xÆ ӟVN~ e'YYY*URk?BCٳG>\dIFW׸e0771[Aޮݻwzj^ioz6lڽ>;9v옣/m1ܖ:na޼yæ:dؗ.]*]O?ylܸ1,,a}V:+9s8l֬&<`ۑ#G^ſy~\Avm[ٳ111kpۮ}ɲe6l(53ZNL EDDl޼944LQ;wC߲fco h?*mAv IE AmvzZj{gϞ-,,ꫯ䧯['$$8 +tСC^߿ժU߿' ֯_(W4>M0M%zKOO?s̠A+~^mzhO>D z3g:vbm+ND{qСرceF1=V/c=&wxq~ZhqwˇJ=zkaaa-'dSu4 6 ;cc^pk A6;qSժUk߾EDevvv߾}j֬O; '|N:ƍO>]W.]Dv .ى?),,lڴuC Q#ųiGbbbʘ8Ɏ@ N~ $SמcǎUV: ~6 ;3>lz1ClQ67H9z… յg͜9AI&iii򡖝|Gqqq ʇmAvg222ׯn:c*EW$8 &kπ*((2ez@k֬ɓ'ggg1*|J<ʟmQI^^^jjbSU'N?f(y{999]Yċ*p\fMr$xe*,W\ye\+%%w@>*;ٴik6w]v /*xɾ}.\8a„K׷Wqn*ϛ"22R 8-[L<YۢFԋg2`}x?;p믿|ru CǏ߶mz{7 Q@d'r@sfeenذA|ۢF>%ڨf}x9;9zرcsrr6SLK~,n T,@***JLLѣڵktijh\Plof'cر6lo}I9Y{S‘NQQш#~E9FOPvlof':Dz+V}p 0x@#;ÇK}kIZZۀ7mڴsΩWYHrSʈo=.\\ll֬YF뗞o ڋ| ʈm^;'O&%%WYHrSʂ۷3N~gQs뭷6VӦM_|E{S^A6Nv;o);iӦٳZȑ#^zZ{9'Νӽ{wh;l0KDDĐ!CN>-{yGEexx\ng-lܸQW_*@of:uׯW_M6MlGGGmɢYHHHTTԩS!гf}~{ff;))) Y9s(:]pڵZqCi>L1==]<=z{6$dggKԋ9%طo_m~g/=>ňڎR[t Y&EL`ڛ`ug5s3rS: ,(w2tP5-}/j<Ç'yyyj۶v#Μ9sih0hРmsrrnyVaÆZj9N6ҥKs}e7n ڮi33v0lM;+3_. {3gf͚imëT"NTy)Su4{X7=5oByQWauhx3'o:fuV: ț>PlҧO}+Ү]&W5o0aW6(رc#G(e͛7WVaX4Tfee?z5jpzB㊧߶Q٭[7Qsw[NM3gy XժUÉ 9wi31Aϴgwfglw޹;'Nosԩ^z=ZxZfo9氻57^WE۶:4G݌3Zublo%?)#NetwڵZMQQ믿޾}{5-5c'L Zľ<GG8߀S~ZUnePy(oqM0//'SqӧO999?"""b򩧞5iiiBWWP{#h#ZLfd'Ok+ /㏫e… Y&fG~z3MAv(iii;wnVVVnn_ŶO'(هw)Ski(5?]-g]vϕK.f_7| |8-[L<W-jDxV(هww}w߾}rvرj*p7 O)o ;ى0a)S7'Y`JSGa}x';,YyfuQ , ,8tz{+7| هײuQ  9sz{[7| هײa֭|ՙ3g\^%͂ݛÛى`׫ l;s+^V7* ه^r섄X#f*ىbŊ)S*;~5k$KEFFU(5y\]Y[*@9#;6(eu.n#2 xfd'~u.n#2 xfd'~u.n#2 xfd'~u.n#2 xfd'~u.n#2 xfd'~u.?vXá> x[ď;?ݻwp6 ;cs8qp'> ىcGp6 ;csU8cpB|;Apa}1ֹ*Up"T8n#2 xfd'~u :8O*7no4i׮]de˖' x[ďUDoC.qp"dǎ&C !8) Ge@>NyRSSNQ{5ne(  T Na; 5<_|6 ;G` xfd'װsܹ1c8 KmAvx +(#Ν=z޽{_~ea}^#ʂ NN8p8Wa}^#NH'(#|6 ;Gx18!>A; 5<‹^w0NaRSS ',H'bb&b>@0>NaVӛ6m|sr?8܌O>>}NjmAvx +p!==A"KLL4}">}gn喨 a}^#\KOOoҤIttTիקOPJH?;"vm-Z 8k|6 ;G'ؐoC9rHI'o4 wy' w0}:vصk;4;MHH֭[mV.ƦSmAvD'[4ioٲE{p8>Cy'gڹs瘘@c}@yXtiLLLtt|Iaaڢ18шQPĸ 4 >~u# ;2ĔEp'߆" T7rhL|'`b}qxlE Pb (E|BpmAvd}O''@fd'd|o>l#22(=CddZs +ld'Wfd'N>N?^a} ;6 ;@vxmAv>*//OPNg6 ;=g'^<=>|Taaa&MC-;裏iji m ׯ__'2;Yx9r{l]QQQiii/E{limڴ_6mڵkWfd'RRRׯyUϞ=ϟ66|Zaaa dpR~&M)@ifd',ݻ's B>6 ;_q-DFFoޯ?%>9yl BP>6 ;? ?1?%BP(AR_=I|,@JJJddJ< BP$%*>6 ;:z;u)eoĉM-ziKBP(ARdv$>|mAv)**Zd믿p}9g}V]v͝;^KOOWN( $EN'O>N[nMHHPӆ| &\xQ=%BvBP( )I|ܵ/q;vL=#;P(J%;q>6 ;`!87… IPg';[]Z迮 yZjO׻Yn7W0٢|d|֩;:Sn ("_~>`|ʣYT(\ى% OW\F $''gĉyUv)}w ֺu'ئ%7oKJ2|j|RIYw(}_y!ʴ8; 7R4;q>6 ;Ν2e&˗Mvը>u]藃[جeU\!-}vr}JYJЧޔα`J)MyƽX2;QΒ'͗%)U2a‹OyjV/(y.^u*7uѣGȧzw== CeCMb rˠVƽƪ xT ѷ= 1jBl]!'0~UnkE6~>㴘~JVDM>|fC ܹ2cooƺ;L^6b:}<3J1՜"Tr6m1&Mnۓ?QtctOI*/h).?[n۸qO zw606~O{x]A=htZ^HʡU}1 {4ӛεd\mzS(fd'ʚ5k6mڤfj׮]~zfoãR_ӱc{t^]ܳۢ9uƙ+U$vvbm}fgˠۦ{)eCBw%fӏDG߼l(|Wֆ֊қ͕+WF,մj2Vyf\msVJ3cgDDu; NFTP/ĩ \n\"8=t}Q,+/$дhgU_L/H^(f,^KCX롍9Q!;*mAv$''Bp6mzfh۪U3ʋvɧ7/TLGDT%<j"zbJ]U`ڹ2eҕrpFRBݺ7NLm5PNt_7όXMj+?T K/4\lMk`;פ=9vaaM69UYV߼y̬VƽDicԩ%j:th_&ɵQbуhٽ{'d1Z?14Dp ƈҩmLQ`ڹ2zj!!U۶m+i[}1՜UM8>Gڳ{zMP9!.x1ZjW_4UD9g.Bjٲ9Mӳ_ڬJ?=}1Kp\ g:y4tQ( PlpDظqcvnnajժ1dȐӧOIEHMMvxZos΍޽|o֮];::zڵ";ѣ5kVEߞu('''NijQQQSNt?aΘ1Oj Ÿ硆 cR?|%] Plp줠`ԨQwu4hЀN^N g}+d'dxxx*UjժgG9~AYn] dcwl^<u5i5:fd'JRRd'#Y>bQjvTJ`zaaÆ kpl]`@SBBD3QRII魢YnȑCrll<ѕط];B9.ND9g.!-[6=g\յ=,&Mnۯ>Ru(b;c˧aZ'Vozt pZ5'NOΆs$*˗Mk;-NOåRĩ'}bb+8ۢmN 'N{ʋeU/gܺ*\LӉ_wb+L/rlM ي2s֫,9)y4A?篅,;tZF6iq5/vo:NT\oQsp*4+aoK?1p+e5Gki}JPN g}+d'iӦm.;xpѵkǑ#J#uҮS[Ǎ=¶^ uron8&6V^p?ūisG߾w׏.61\n1ܸqOk xsÐ!:wn+Պm'tG\[LFCvbzZW\豍b[ޤ-}{o)eܰzx;v:.|/zh a|\c}Dc)U zwbbÆ yg{eN+D=h#vZXVV'pq2M'nL/rlM >hb[Y;?M/$COk׸jZ7ޠ% & /MP]kj[}uU0Wt\w'ٰ6r8V{z[ͤdp jժ[Bڷo￯#Ye''N_}_'/RF~|/e}<'2m~1mb#33ZYÐeTndh!7~,BܶYoײ~鲔\0!tz;;vFDT?|޽̞qC߉*U&j׮颽qIp灃$N2o7.%=4S)dXVV'pd:/](vblѹxaum(ES+W<7YڳV!{.kawњN^)>{y(6YN=?ѧ~\l\t :e)UlneA;=:O__έfRBvT8\|rrr! ?~\=GnN8**E^,ӷO鶚5#BE1x]jۛDߜegݻwSjոpq׍7ް{Ͽ E,c<:9겈(^_Z7a< nꔚjbtZ\!VV'\{h!INxJ߉8Is"7x*f+w$jti9/$COЊi.zŚwk׮O}XxlٺB<{׭{8qVոߝ16˪;=9]̤p ؘU.m۾Ҹ7ᔷ[-Zu"&ݯ/[>Ov"A<e˦}|xZbubby[~{߆V_87TjNL&f:O+ VV֣CZMxJ߉cvgk6f('NnᴾEBb|WEջy'o _i}ƞEi۶%wرO)E?Wj\NɘneՃzts%~}ݹJVN g}Cj|6o޼l2x4;wmW͐ew?=;B.c=XvzmGΫ[VuIv?(/ּ+jح['X&|7g%Oq\q_/g$%ԭ{B:]]z+eg񆆆dlT4=@ ୿LjժY*?ع^qfߝ6pLG`zժΫ[VO>9vaa͛7d괱u5:~/b{+Qɩ܌~^ZZ5D+VnY"ܽWH[l:*U!(巿nݛĎ8JJ'Vu"c6MΜ~c{Xpni1=3GmفQ_"PÆ1i|ʪG@vT8ܒ7~O6lذpBxĴk?|ӪU#F ~ wʒs##xNCN g}ƍw!5[hV?z.JĴlJ_ްaLhhH͚Ç?|.c(Q/sH[:+W7>G@vT8NG6mTOdŊqqq46|נAbbbd|"+Vԯ_ȑjS>NweddDEE5l0===22rŊD{liZlԨ-6nfd'fϞyU׮]ϟ66|ZaaaLL N4i§a}4hNt§d}hРAddd۶mxlxlgώSҳ>NG9-[L<;w_ŶYun>N=PlܹYYY6l_ŶO6nfd'[{vZ9h#Zsfd'CF1|/6hO|6 ;8|pR{O`}HKKѣDž _~5o޼QF-[4hжm D{}6 ;t:Az뭍/ߛ"Vl [lӧfРAJjiٲeBBW`6 ;0y䔔_~ٸqc53iӦw}ECVl GMKռy^xAk/=hX>N't%//O{%:tڋ}ECVl qqq?쒚5kfd'hBJu뭷vIkN7fd';y'ԴZݺu{| &|ɓSRR۶mk޼ouӆ b R63d HaB€쁗^,y(Y-G-RzӨ߀s`f8N&9x#pJ}h'Pl6kOVh4:&j^n<=~+>N0N.//'zf܊8%>("˲aX`pqq\.?b?n8%>l6?؏[ tHUU؉xkC;薪6Me>y/Nc'6+tQQ^vǧkɯ#ql @Gu]vN ^}LbHdNIdNIdNIdNIdNI<Ͽ<+|;?~E$ @h' 4vN @'i=@&QIENDB`onedrive-2.5.5/docs/puml/is_item_in_sync.puml000066400000000000000000000037071476564400300213350ustar00rootroot00000000000000@startuml start partition "Is item in sync" { :Check if path exists; if (path does not exist) then (no) :Return false; stop else (yes) endif :Identify item type; switch (item type) case (file) :Check if path is a file; if (path is not a file) then (no) :Log "item is a directory but should be a file"; :Return false; stop else (yes) endif :Attempt to read local file; if (file is unreadable) then (no) :Log "file cannot be read"; :Return false; stop else (yes) endif :Get local and input item modified time; note right: The 'input item' could be a database reference object, or the online JSON object\nas provided by the Microsoft OneDrive API :Reduce time resolution to seconds; if (localModifiedTime == itemModifiedTime) then (yes) :Return true; stop else (no) :Log time discrepancy; endif :Check if file hash is the same; if (hash is the same) then (yes) :Log "hash match, correcting timestamp"; if (local time > item time) then (yes) if (download only mode) then (no) :Correct timestamp online if not dryRun; else (yes) :Correct local timestamp if not dryRun; endif else (no) :Correct local timestamp if not dryRun; endif :Return false; note right: Specifically return false here as we performed a time correction\nApplication logic will then perform additional handling based on this very specific response. stop else (no) :Log "different hash"; :Return false; stop endif case (dir or remote) :Check if path is a directory; if (path is a directory) then (yes) :Return true; stop else (no) :Log "item is a file but should be a directory"; :Return false; stop endif case (unknown) :Return true but do not sync; stop endswitch } @enduml onedrive-2.5.5/docs/puml/main_activity_flows.png000066400000000000000000004204511476564400300220420ustar00rootroot00000000000000PNG  IHDR dD*tEXtcopyleftGenerated by https://plantuml.comvEiTXtplantumlxVMs6W"ɤtIkVZORB  Jt߻ irEþpud JE@R L ]̲y̡_ NNSA7XrP})鬷YkSrHƷ(]ίe(9@rRv51v- oRaY(䪛ip~m7b@+|hM6gm, EP62I擝#Y+3h[ H:wɫձ+d[oeU+0fȪSȢ4}ͬNR)/f6EjW&aVUS13}bvUtu-Ko=Ka &~e0\Wn.O=)/{6C^<g1 @*˗w}MHI}cl^f\CjqQbsİ/HzjS+Cjs%L:PO۔n 0n[=+1h0h_quJ.ѶH>LQ'7 ֤EF_>~z#U<t VhP9_7Wm$Yz,Rurwr$a7ɟt/#0K !-sw~{rEG*ŒbIh o[U!IDATx{|U՝7~Rc(1 ;^KFE@iK :jm+[Z+A6"cb*\Q`@DB#`0 6-tP3_I$~W^ksg7o !5PHLBHPBHPBHPBHPBHPBHPBHPBHPBv1c|{nG}?S o61|[Zb*PSbΜ9۷os,'Nt!5BX̙ӦƍbT[~}U),mٲe@5 NJSKݏ9tT[vmpB:ؘ1cN,,4hPBS{މ7TbݻwosTb ?S NnĒkFa!)裏XXHa!)믟XX8o<:ا~o}x.]:tHa!bŊ ̙nT .L8IC=bT .|ƍbRM>a!@YlY+yN_ ˑ#G.\8hРo|ьkя~4o޼C5HX""PKqMX@5a!GXׄa!@\P{qMX@5a!GXׄa!@|9tP2aa@u ̙3sssaavv… ",/wرca9sRSSKJJ",;7pC6mya4,2eJV$,;-D"m۶ SL ƦM$,;Gmݺu4/ ^x]vݻP衇ҿ9s( 5KXvݲehRڡCef  7 便)5NX-Z_M',SGmݺu$ݻjP롇D"sQ j>g}v҉C D"#GTS.7n+ra@ ,oLdɒÇOw__=XxߤIs=nˋug}`d֭'MW8CR80lذVZ5nxk1iii-[l޼ĉ+uK&%%uq֬YtVe=E͘1SNW]u՚5k*/Qinx^~޽{|u/oFkۻwpmqU\B Nw`̵^xaFFF%+Ua!Po  3,;?nv?à}e-[0~vw y䑞={믿>? Q=zٵkםwy_gnݺmڴisO%Dy晢~tVe=oCk+/1zN:߿855.] gHcmַ}opmV2LKK+]!C7kl޼y$V}@!,s̰00pѣGGۣF{ˏONNn:;;;^fMiS3DuaÆ vnnE]TKOզMwU,-ZܺukvVe=m߾=.,,LJJzb;~7v_e X:+ +\[JJ`xEPAa&L W\y̕Ī7p;-[qhkֽ{ƍGO6j(|a9CTRR_86l-XS>uXgYjU~6mڶm^xΪ'VPDUYܹs;v4a^:+ c-XkYdᰔz*+Ua!Po  T%, 逸9sM7vnz;wOvU3‘֭׮][6lX۷߼ys+5U۶mVagg9x7o~*% u6u0`@g)2=ڪrgaտ5VrgߦMJV",=a!$*jjjW˗͛7;w޽{}W+ĸ={fh֬Y222•+Wӧ cM5|nݺWag7(**?~$*8*Qi_n߾'|R\\|嗿[pe{̵=3 EVxk .!|fa\ j8bĈW",=a!$*k)ݳ`۟y-[̬$,ܻw4i2y? w^{:tHJJܹ>[~yڿoH$Xgʺ˃e˖Uy\ũbJpݺu3gNt/ܫWV2=ڊ~7n/;vlpQ_2\ A.B4};>cDX{BH8U ![o]|PB@Rw۷lңG_ ",%a!$a!U|}݅ ",%a!$a! pj#,;wnrrr 9qPS'x'NUiӦ{~w*ꫯ@.SS'",Nh?LKK+ik޼={vû c/:$? aaoݺuksX-a!$ 7|)SFFFFϞ=׭[WXXr>}3,uH߾}x㍢G"ZI^zEA 8<#,[BH8'.Z+СCRRRΝ}cʺ˃e˖0JbݻwM4III}+dHd/tyΜ9@ģ7x#RJ6mJJJ%,S:u&;vLKKSj N1" 7mڤ 8a!@*))INND"zR j ~'̙3G) Bȑ#˗/2eʄxD"cժM=P D7nܸ__œ bO.]nڴiG@:rɓ|ɽ{}#G\j@%,TQQ#7_ B dffW\\>|.sa?O~Xgm\4uڈo.?GoqGqB⓰H U o։'&Lz* 6UV70`@AAA8`ԩ 6W߿?̩tK/}{M4iݺIr~{7tS2~9rĈ?O*L(>ꄅǛ#(3|bE5PJJ&MOnkp̧ƶl ^믿j޳ݹ /Me&|~\{m WD꿎n[JD_O*`7o43;_Ke͝ppXn os&\ziN (g+Ǭ[-=[ vݬYO =\')l}|aƑlM6n۶Փ]L>nN0y}[w߆ ҲY_eA㪫R Cʂ|q%aalݾB T%,lժ֭[ݠ}W#''g׮]wy۷mۂ~ÇG-ZԻw2ݱcy睷#/\U9Q+˖-[VXXG _޽{4i=WӦM ,k+,f:Xfk޼Y{H ?V;Cn{ گӗ\.{{7.#mjSV ]r^zʇc~PϞk`/~qWEo~l7pῊ_x ^xn}Wc[-JNQz}p|r`c1⁠CU^ לw{ウº}=8{|Иtf߾7TX¢u͚590ʷ1CX$guVEC6lsss/p-[휜-[FΞ=y|ɛo9nݺuvvvf͚I^~~~rrr?O7pCݳg+&M|;ٴiS|swX2駟[Ky:}>/nppUWF`iѢy߶Tg͠ѻw3+Lڵ8?OצE[3.Ok\ŰºqNJjt̺J /3:xj_GGolX(|m5|-.t8۲e|?vMu ?Vf=o}MX@= ,ȉYؼy¤ӿpi5l08x`8oo߾yvFQU^{{7~SO=mϜ931V(X; KJ>~mfd<2yW? +CE敇IIJePpg/Z=U /+'pGf%u+gaP /3xAc޳96)Jo=a~Ya2k^~7vmQ?2:xO٢uE{ @9g?oal־}͛73W[bE.]^}ջwy;v,\n]vڪaÆyOsO>d׮]s38t. ˔ؿ~[$)サarӱc_gHM4xOSRZOpq8w5'RۧTxgaUn ҲYaW; pn*_򰰊uJj*ĚO]v>w^ן ,|o-^)N0ϟm⷏~w_QVs0V((,N *aի5kϟ??hX"kvС999} c={[pʕbj]v֭K/TҥW^ye֫Ws~`ah޼ܹs߿z$ ;vl͆e@vl֬I}\6؂FӦߟ.#G=ג{o)׽?J%{ݢ菈V<ԠY+ *.={^ϯyfaxc湛o[_Z6+]/s~0H+%*S`7(f1KS޽VV}e量F3# B+a!@}N:~ 4a5kꪤ;Θ1#8١C_;w)͛7/999_{J?(qM4III#; ]l(,Z%,Hff橊4hpiM8P}7~~.))-׭_, HLB dee֬_~ѢE'f@)..4iHL8_ aMX'H,#FػwT=:&,LX$ݻwՆsnڴ:&,LX$ӧXBWv9f̘j4BN2a!LqF _M)..2dHuV5a l}K!,ѣG322.\(竾<+:GX$e˖Nؒ%KZ{ 8%@B;|3`_رc֬YÆ [zw@%,?|Æ &M;vlzD"qDƍ{ꩧvP Z$Qj =B&, ⚰#,kBj =B&, ˡCJ CX_fΜ .\Da!@|ٽ{wjjjaavvv_RRDa!@ݻwF씔4 ΨQZhѹsH$ݮ]i& ÇoΝ;G"vڵhѢW^*@ģ~E"D}Μ9@ģH)mڴ)))Qj N]zѤ.KKKSj N5*viӦM @ĩÇ'''G"^zAXD̙a!PW9rdSLP=HdرM pJ '//oܸq鯿k ,W׿xҥMvooIX%GwC/|ȑ#WZ}p :hȐ!;v쐫cg~WNa!P7=zo…/8 @PRR2d?=8x≼<|&,L>PnjPۄ@{gϞ-?K4/իjw#F(..%ѣG{*a!׊'M$6KL'N,)))=B eeeӛoK/a!233efl>GXĵ YFFO@q*aaѴi޽{;5V۷o߼y/=zXdI8f\rɗo.}W^YPkU ?0--ꫯ+\ 7_*777??ݻw׬Y{.[o۫W~:qa!׎+, ٳ')))>paZjոqDZlټy'V>87lݺ=A#%%孷ފuHk;?np &?sx5kڶmo߾J sԩ 6B.]cǎf \NӦMӟ'ڻw/~>\pAv3fԩS0UW],ɏk%BSBXĵ w9x.]Dw{葓k׮;?ݺu۴iG'+.]Yd׾Jpm]wݠA6nXf@Vn/w}رc+ m۶/$<3EEEׯc{ƣ>OKK !Cy悂C^{L~\+B 3 Hd:lذ!ͽ袋6mڄąb s&L[=rK^XT|qawYgEokԨQc3޽p˖-Ǽ-Zdff&fggGk׮mݺu ۷o Û8+V",8%@\ܸqc׮]IIINkذa7ߨQ2|_b'Rvqw֭wFo|* ßHdUVׯ_ӦM۶m /džaNYD, ܭpZqxY{5o|Ϟ=A}]m۶~cƌ=zwQ!+}eS Y[XXy˖-ª\H% ]n]]; +9uɏk%BSBXĵ 7|)SFFFFϞ=׭[WXXr>}D >[nngNdɒ۷k.|xaC*\[/^{7]w]7h֬??z5s;*aaU78WQQQpH$۫W oل?p`<,pZqE]qA:tHJJܹ>ɑHdX< o~:$TPPp9Yc=f͚ v3 >s^Wp.]T%,/}+gcΝcƌmBn2eƍhxȐ!Vp ѣ .oiiiyyy'K-[#T,Y2tP4B9|3`[=cǎYf=W8@oذaҤIcH$R/pܸqO=Ԯ]N>a!@\D"@-5a!GXׄa!@\P{qMX@5a!GXׄa!@|9tP2aa@u ̙3sssaavv… ",/wNMM 0,KJJ",;={ hX8 a!@5jTrrrΝsss#HvvvvƦM$,;NNNٳgΝ#HvZhѫW/f Q~"H߾}#_׿>gef Qvvv6mڔ( 5KX.hRxe)5NXF ;ui& Ç#H^T ,_D"sQ jc=jժ)SL0D"2rڴi'3:O3f_r(,X .sK\TTpr av:DgϞɓ'Ϙ1]'^xAZxw VRRP@>}ʕ+gf׮]<V f͚?񏒳 Hj_G9r,tƌ> DXįyegg ɓ|j_#FgϞ'x6 8m۶,Q1cxr!@mqjڴi;wX|U|(jSƍiӦP8a!Ǝ+$#4a ',1iӦ}ɉ|+X[}͛7ܣG%Kcϟ%|K_ ^y{~wkXOP8a!Fyyy>`׮]6,~_/^{7|Yf=\z+}}}7ϟD*p%y/^{7]w]?8Yf?|~~~0I^zu9swq̰0p7O2x/$W^x c.VXXǵa!I&,TMEEE A#]MNND"'Ov8p 33CIII;w~gYYY_~yЙlٲJ:CsNdɒ=z͚5߿{ NѩS3<3sUxҥKUE]q{!$'N⋛4ir=QUXaaJ'S پ}233vZ'V+v+B ,T 8㌯~Vֈ/&LqB N7-)SP8a!M/#|UVP8a!mۖ%'#0f̘GP8a!F)'c׮]XPK@ZdɊ+e .===P@\KKKۻw,a-\p>DXĵaÆʕ+| jwΰa_hfϞ=}tZ%,´4/L#G\vw>@muƒ%KF/Qv#F7oѣGNa!P|ӦM7nD24xYmp2 Z$Qj =B&, ⚰#,kBj =B&, ⚰s+x饗&OU H$TO0, nDxתN25{'('f5Vr<ď5'Y ;f',݅3BN؂ 1]X@=#,pX>j߾a׉B]K/@jrȑc8[ly^:v7Z:3=СCJOrWg\yרm۶C )))IzFX 08r;ӳgN>|x%8Ya~;s֭]q?Oo#;pΝ;Yl0]w5}c< ټys޽%]X@=#,VXG5oꫯ?wOy_~>C֯_~7q{z="ޥK>:u}R  ' /9rdѷzk C.]vݰaCGػwW^yJcxF}C=k][h[AܹsGrիWzc馛n;DZx H1BT~iQQ7?Gwy'+XbEs=wWFo,[_=zO%\Ynٲe[K*Ə=D p'ңGvС!k6mڌ9zDiڴiyyyYf ]qSkH~"`*^oVҋ.(ql5BRz;aay睗s]w>駟F;fdd:t(z7,rfͪcA6m׸qF CaGo{]Wq%E333c 4X4]Kp={ >_jdX!C=zt~v?!ҞdXlذ!Xm۶---=AT0ʡ^8Q=0Wh@&,Nא ׇHф),6m4A;̞={Ĉas̩=ꭷފ~@-r`/,4h]wݵo߾O>$z'"I~ i}_p<6m [2eʉ rCՏ-;.­#,XXxW\2zwŊ/4hг>[} a~֧{ /0M5  9\pY^n]{C Bh:ujuڵ4ڵ뢋.裏=տ4arⰰ ) 3>s:u ?pS /|G[l1bD}ffŋ?òk^}Ց ~__~c7ߌ ҥKxFyӭ[~f߾}̙?7M}k_ok׮cǎܹo#UTTr-ş}YO~65?8l2o~zr`Z_aa"$6a!v/Z999sN]paC˗/?'|2vtW_p_zHkUVmڴ>|ݻG;]vҸhѢ.]{\rI{;:4iiy뭷wyui/hԨQdyÆ 5kv饗N0!Ç:ر~G?Iރ6dɒpk2r0VcRR@Po'9,lp&M4v3x|_ܠϡ ')  t sss ޏ;6eʔC6s % a!au~k_۱cG~ggX() } 7a!)HR@ZPo'>y8;=ZR@ZPo'Ν;+pVԛOj^y6iEX@BMX@ 6a!&,h؄ԛ9zh*aaG a!@pذp$ h^ /__QQaH)((ѣG$//ԩӸq 4<˖-k׮]nnnqqqXXxqBQQ yBK.YYYڵk߾_~P'B;h׮]vvvDX Dž={v޽°P'Bj7n ղe˲ڵkweJXPUVVvҥ]vw ,hv=z饢bڴi3Sń ڵk7eʔx:g},8m@{8p4,[,e{7uTy!i#,EJ&G^p:  )PH}By!5)NR(/ a!$B)+qRdɒ09cZE_|yHHM~K.NUkX]hذamڴiٲeAAU<#\rɹn.\K`2ɼpӦM#G+,,t$IXd}qG9{׮][r#+VZzZ};tK/اᵞ={v޽۵k׹sgI!@ TLR֭[ pm۶Eי?~VVVFtߵk׵^ۢE_~9vaΝ;Gӻ۷4Ϗvaam۶ [^lYUVpW.Y$z'-Ѿػ5n?y\6sss6mڣGEE;s=ۼys1U5n3H&nڴﳳ۵kv6+)+a!RI i^{1cww:47߼k׮}G=yyy>|^޽{nwq7.c=fΜ|gѫWCKo8^z-]4AXj|.ak׮^Bرc(9iҤ=zg?3τ#9|p0y\F77?Ob(11܆x?Y  iSt̙_Xncx}322vQ1++駟hĿ_Xc]Zj'|2В%Khhk|.͛g͚}{^2aa]x{сN֟>}z>} o|#2ԩSظ:qB $Vұc߿̘1ߥ۵k׻F^x!ldԨQѾÆ ;| iaW\?{իW}:tXfCKC{2aaz?w]wFG)kH]tmָIaEEEU #p7kҥ BXd@k˖- h޼yݶm[a={FٺuΝ;?cƇ~. S;wW]uչmI#o֬YdyժU-Zȸk?~gNNNl qÆ K&,̟?k׮M6]dI=_~7oNr5nƽך*))|аCݺu }g?۷yx S*ARa~~~qqHgBO 9眼_|QXX0VII… CG?WC|!N RXK^()H+BB^()HB䅒B4!,PR5J R iӦN!ar2e̙B$,0 M LHSB?Ҕ 'abֳg?} #._vڵk#fff[O?ݾ}{X0 a!@N¬W^_|uxE}寚4iYs̘15jԋ/h R }' zgÇGZ233_ݻ͛wW?0)CXugofǕDqn۹sg}aÆL2i=8pwo馌].]4ҸpœMX0 a!@N>[l12IX01kҤŋ @&)a!iJX`@&)a!iJXʎ=`rUQҍ -\Uaa @:h^\ C΄)ꫯ慑UaaaΝǍgpҜ ͘1#++o߾arUXXح[PTTdpҜ }'ك ۷o\u-ܽk BwwaZ5dȐv7, Ro.FNNNEEa@X"Ia>}ƍg@RX&f̘ sss _ '|&W\s BX..LqC@H_ǎ{͛ܟƌ&WӧOOgq ,(,,t$ ,ݻgΜ9}u:thXlY<ӏ>?\jմi,Xp1=@uB ;vlΜ9կʎv1y;it, ۿ b tQZZ:a„WXZibzݮa!>>JP^^>eW@H}۷oGdDg| WHqO<͛cڰaի]B ͞=[6Fuw@X/m%Kdee hYf:@K&,qsNoW_}5.":)nK.sh۶m˖- VZ]gҥw^ ˱}/Z9'rFvjݬKa! ~oO< /\n Oƍ9r$_}wqGqq}V\9pOi駟H擅WϢG-Jf*++5jT/8,jld)aaQQE]Y>}zAA͛5vIڐ!Cv{뭷۷o͛7"˗]vYlС[l9pw=oW^yUVԩӻ;wo1cF2aa---}GUq 4hqW]uB/@ʫGXxs9'ܽ{7x#\\\ܱc$Xm֭;ڳgOq~oΝi֬Y/, [[Zj5rhP'I6m4od%%%ٳgώ cեKƍv횸]XPB mڴ,7k֬q7nԨQKb50Wrx;_X{=zI~04iְpCyg cմirXڅ ,R\=ɓ'6,|饗nٲzE[-^W0ގN0, ߼y__7 o*},mpcǎM^$CX$ I&aq 7kч><{ݻ7k֬o߾O=T}^xa4ߊI (-Z,Z mӦu]m۶ja={={7 C!,RXׯ7B/@",GX8a!Bx@;aa h )¹ E{=B%,R LXXVV6jԨm^|a!܍FkjԨQ>[322v:gΜhÇ'NةS֭[?Y }aLzɓ'egg]VXJ@K&,7nܠAwUW?> 2dǎ u]}9 6o޼wGպBN;Lp z},Xмyn-|e RHqɄ]t),,,oܸk׮l֭cFӸݻ;Vj]!j[01ٳ'?_XJ@K&,lڴiiiid9,ȑ#uM5kƍ7j(Vj]!j[0c,,HB ŝ' kV޴iSdC˥^e˖]>b7qXƅiBXdŸ8pرcƍo}+7l$e֬YM<fAx+i&&X-~In\X&@K&,,--/>.,D3l[nҥܹs]><{ݻ7k֬o߾O=Tm[a^xao0ɍ ҄HqɄYf:@Rpb:@={` a!@@[dɖ-[dczWX)|Μ91b=a!&O\VV&!#jʔ) /@:())?%K.i7MسgԩS]B ]̛7ߖB(a!.*++˗Ӿ}Ə{n@H/k֬>}͛giesT!,αc.\8ye˖8p@{EM4W_uT',כo9wܙ3gHar&4fZhQIIS a!iJX`@&)a!iJX`@ѣ &WU  R… k\._3a!@*+))ϏUaaah0DLXoE䪰sƍ38iNXᆲ}UaaanBQQHsBWYY=hР}Un:vo ,H}ÇӪ!C;k_?nX~]  -'wܸq/iEEE/i2;;;Lo "bar "DEEŬY??s\M>9sVVV:[a!WQQ1mڴ=,[:]v͜9S^p N6)<;  a!$)4 B }IϞ=?cǎegg 6e˖}),,<|򼼼M.Y J O]^>`v^4ٳE ڻwS@Z+((x'#ˋ-馛7޸xcǎ]6???hffu>۷oJRxJ04iRK^z饑aÆM0!?~_|+a!̤W^_|uxE}寚4iYs̘15jԋ/X};SO> _7nilݺÇ¡C222u%,ݠAy~ᑖW_sͻ+~ĶK OC^՗aaԃHw֭ӧ|H++++))~mݶs>lÆ )yZ=;6lĉ}ф | )@= 8pwo馌].]4ҸpœMX"ҘLRx1[NJhWFN$,QEWNЫz^XcX? .4h޽{u%,ݧ~x˖-ug  ~!'HkarҤIŋ׵c>͟??++QF֭[ pm۶E֩7Uew5͛뗙y=Dwu׶h"//_ҥU5~G I$,3yР߿w{׎3f߾}wyСC4xǎ:ѽ1.++{gCGP㎒eg@CaSN-//SXXRR{Fݻu kM]ZjY'F`&MCGPjuɓ'7p6LڅfW׼JHMm01Nex;vwB=BI!8&BP3_)/Escƌ-[ܼys&ZW2\l;n׮]#lGcjܑ@4IT|^X%$۲eˀ7onmۖq„ -[L+Lnȸ{ٳgȑ[ܹc=Vek5>{5P$ aMڅfB$ MڅffBI! HI%/Ԟ A @"BI! HIB^()@:i 9zhӻʣ4\BI!G N xz._ y@AXOIII~~~lޡ` lw(sssBQQRÚ5kB-|z6l`pl#,Ӫ2''K.ӧO8Cܱc PDP;,YHXۄ BܣGpzwڵcǎ>aR{왝 /]w΀UVu)¢"5k;>۷߰a΀ʜpzhZ§W^FZB83&LNG}Pj6mrͻL4)ޡr6= ,U8\b$pر_|Q=!" "G %ǔ)S~:t(IXlAH|/+c.7ؽ{̙3OrShYV:u ;o@rXbڴi{QqqJ۷oܹɓ'8y睅 ٳ}(\{?/iHXH==)άy敖}Qa8/_+#n_|_Bx% V&O\^^n2 !a!}'xB`ԩ~ }ϙ34Nx?  ٳG]ibɒ%[l1 SdÆ V  3f8K:thJ4q)5k,jҊ>}W p{}~ }̞=lxsI[o5##k׮s̉+T?xۉ,L~a޾}0&O=ɓÎ׮]',H[ 7lfG ]ty|뮻.LG[j5lذݻw'wk8Vc-á(#<ҳg0߿ƍM2xϝ;7P-A P=T~ֵV5ZtTVv^xGUԩa!QO~_2dǎƍ4h㮺G'M4`;w'?4l޼y޽Gi'o_}Ĉ j2=okt]w.h$BN61~߾} ,h޼mY M?Y77W,Z*2[f?y7ߜdXXkSX65vo;7СClr;Z8NԩS )5R_y啷~oou֩j}!⽲Tu*D 0##oo[n֥KƍvY֭[?]Fu7ވ,w1={w}7vj2=:A](7x W: aϞ=Q|zfa4F?믏~Iwdee%18M:_,y ;w,-]m۶6W:}־aM6E?DZWrVck aa0bĈSN2eȑyo~={:th޽գM7n8|x{LHPA ʕ+KJJv+ҷR=z8ɼ^dNx'dONE  Bo? 0wHÇgϞݽ{f͚?{k֬Ix"= Ņ^7[nҥKhxQ U8jx xaa/ 7 #t\{-[v饗{:t[lܸajڣGGy$cSX6 lݺuΝg̘VHeT5NhKM6{1 AQ%-((hѢE6mm۶U[Zaa= d^xl2kփS%,H7BְN>HX> Skֹsg!,H7BCX٣BX>*,Osέ[?ɿW5k?5iEXH}̘1Cuc޼yJ4qDw֬YYYY_Wo{e BcSq6x饗֯_HK,ٺui0"s=O @ZR;vx'Q MVYYHs5 S  ɓ'+8?Bt_dN)S#nӪU^~eugV3]id&p-YG  qA=ʕ+]iG )1D{:u?/iHXHSkqٳ']{ꩧ*++]qD;v__&Olٲ A 4 A 4 A 4 A 4 A 4 A 4 =z4]Qp Ypaqqqwaa N'a!>%%%Ѳ9zz9WTT"@NB8)#w(tӟaȑ={RtG?NUVo߾O>lw(sssBQQRÚ5kB-|z6l`pl#,Ӫ2''K.ӧO8Cܱc PDP;,YHXۄ BܣGpzwڵcǎ>aR{왝 /]w΀UVu)¢"5k;>۷߰a΀ʜpzhZ§W^FZB83&LNG}Pj1cjPQQ1sOI&;Tbͫ:E2N"a!P{8z-[WRR2k,4Pk)4eʔ^ 2Na!T-OORx: ZKS:B8Y_K @PJ p i¹Գg?} #._$,/iܸquև p@B =)L*s BA=3?#-ﯾݻ͛wW?^OR / fhnUbPj$ Be$CXHnݺ>} 87ߌาvm;w6lJ*Y5VÆ 8qG}4aT 5Ф0AT=/T@24l<l#wqz۷;N)4 7=XNNNh jժÇg̘ѤI3gȑ#^x{6Rew5;nHIV:۶mZj C]"{-//O>,,))޽ #wۺuֱzEmXʦ2336-|B>uԊ gkA{LRk)/,,-- jܸqgϞ6m V}饗 yhʕ΋\jHjwZj-tBGx:UhnɇUz% 322vQ}; %e&qƇ  ?яFճgϰܣG[o"eee=ӥO:B0N;Д($V)G#߁*1c~ iƖ-[Ft0אVHc!qvzwGi6l؝w5ZRZomj uca„ ss93g 3f/ /QF%HlRc[7% %`w^a)QI2/Rnٲe͛7۶mKJP&fkֿ{222ٳgȑ#[nݹsPGw𪫮:s%޵е % ~鬬mΘ1#qηq??)R%,oڴ)?|E?w}w/5;, Q(B)pyD)_xFidmMRZom@_PH 8=BSt͚5:uе ڀ(eBI!rWXhJ %e\)J 8BSTyе ڀt^()S4 MR\ 煒B2xkiyL9+,}¬(<0 EEETp-2v}F"I6@5 AXxɹeIDATK¬(<0رcZ3ȑ#۷6lX߾/{wWE?~<5o`.&*Y7M\R4oײ^$ Qr%L~ffn`%]M,@W$ߝ33g9|ޯy3ggپ|9ťk׮ݺu6l5,,fruu޽XqOOOYXq 2D\&NO֭Zw@Aݝp8ht_|]ܹ/64d:tM<}XʏR!ilBcwww韧gc 1c ٮW^\w G1g3 t钋 6w7#B d%5kXq~X 2D\֭[GU3ghH,JIIYl=ŊGEEYQcccE=B TVVFGG/9x,|rwh,߾ˍri#4ׯٳǺڗK,Vjj*94e,|;wﮨlUlb].++,j[yii)?~IIe[duDzNO^p!,!mK&.X@ܷ~wV׾48;"##ccc_QM& o.¸< pV"=l;Sh@#%4ׯ/]466{@4'OHII6}Ը;VYY1 d o 5Iܫ:u+/?/$M & W\w^.??̙4 q͛ׯw9'',$$M6z_pm6;;;??)l"Bq_t8ڵKԺu`q[BMEBS\\5MӶm9@ak^+4℅ԑ Z~/ &ǯ]_|E@@Ӿ}~׬,16|8퇇-yf___i`HHȬYĴo֣>ķ!_BCCy(ŋrB&¦7n܈ZnQ;Ǒf{w޼y۷ohBssϔ)S٣xE Ŏq5o5o\pEsGGG. %Xh> TTTDFFrB&¦񩩩\-MLLLii)m޴iSRRرc!NNNΝ3377Wlx駟6N iߴ_N$ P`9N83?߿?h l'Yx͹sr@Ν[d mطo_^ pai*+++((8:y7nI3 SopAt*& CBB®\2k,^C ķ^QQ;)daVVV||3՝P3M-Yh'j~!Zo 5 >KOO ߟl$ ,Xb]p!66ډEk׮>*'wsF{qrr3g<?p֭k4鷷P\qAٻo]=~]\Lrʲ[\[V$1sڴiF/O>b=EӾkIV!=ş~阦KƎ7Nm>ѐ({8[b%[ha4soFP\vIc;88'#)MAϱf,,--h޼阦k׮=z\xҥK`?x|Lˣ߾zPtƷz˘1c5|P`K$ }.gΜIKn%")))>>>6,ؽ{dee(5V,&i% iӦ׏d0p^{-''011qwaƍbMDi'TXa_Wt %q%'xBGGGӛߪPd3YĚ% ͎lZk׮Goљ,lL$ myyѣG#""ĉz߾}jd/߅x>ٳ_}UH6oҥK5Xĉ/^=8.89kBACZXյYfɮ)S8;;wQhnnn⫥KJ/^ .;wܚ@~=ERnf-8LG3fRرciGooӂc}ғ/{ȑ#wvttԘDz;s ğN>} -߶iFPdHF!Tq!!!𵟆YPb hǴ<)d!SVg#G}*F=z:mVsssCSN4Il2BM-VۗBm6JU:*F [jRR܄>}$%%iL~g<1+:$vժU=ziT }C-3}Uk͛ Dt,۷<|ȑ3=" Ӄ>[oIҲ_|EiX|ɩSJE?Rß[SVT=ER=E/ѣGFĉ'䏢k׮> ,e6_(K"|A_BBƮ%{Gs̑'7n~駟\YfiFGq) qW&?~\^PL.f"'$-[G3,d͒j+NCUV7-Ee YT7Y("}*Fb}ɓ'ˏ 3g4 AR' -}.:t8uQq*Dm5"%MnH-J11,XvڕkLEĈe˖{O嗥5*##C#Ϟ=p7ԪZїbFq$ M7 ."yxff;SSSSH֭[7?k.]Ν;/Z0RVT=ER^d( /ڶmqm&gOO"1VZ_鵊a_~ǯ?~񦧸jiL$ ؜& E[o5J1Ul P141a\E .Bm_j 9JU:jD zȐZbcX>GўVq;nݺUJ2dڴib7הfggᙽQ1 V,ԨϷlْd!hKExTZZ*9Z2.j޼yf_ixJJʨQE1d8ZB٩SN̤?A m||_'+WZ{lN , EJ1UlE>`T\f5V!#''Gqe*Dm5"=UdH-J11,ѣ{aicII.((pttCDJ?aTrbjUQtǓ Bj#"0P[?6olv`͊6\~zukܹs}Q׏3hZj̤džzBЖXHlNuۈůBPV}Q^^ޅ ~[v!:Pd/58((h˖-Gŕ2Pm"-*ҎRv/SUVm;>C͓~oȐ!+ntyUEu/%jY,4o0`3s B2o@]ߊiZZZDD6T1uvvNHH(++G, a_)}nѢ83bnٲW~nnn"UK bZ8P`0YXK s!srrX) Q-1jR\Oռ+s1;vko 5VMO d֬YϭJ&Klٳgw6das뭷+%%!ZYYI N#Q>~֭T,Nzzzd!]' BP+/BB" Jk>CLLL]>AY$_XBWVV__j_{Tlv;BT@ YHHdn޼9w\Q`\dIw?>aڿ$ʺ5XN/$Ld!!ʊ'2219oʅͼUf͚m|뭗J~}것]`Q׮gUkYY S!d!! XbE^^-?|MJJJm҄ޔ Qw% ?<{fLuoxg!"Lp5d!!%Z`mW.\.X!lIQQQ-ͫ,'>*v3nFS)p]?gaO-NV흞iQMU^qCNJ GqĐۋ!%7]i U٢N]E`^ Vhb"U_yNNڵ3fhǿW^(vw4^C6/3iғGDCZh' ; C$YH 9@=]yIY"B''#F|hzʹyxx޽{sw*ʕ+iDM=|7d7~iVGﬓ{Ԯ];Mev^^]2-g|.PT|PwiQu"k덪lk'~#>joة-ᇃ6l\!zoxo„5+F˰̊vPqPxA9:ֺEB9ȯ`5BT@KO ZXյYfcYYٔ)S;v(zGiP777ҥK/^ swwwpp8qbqq4|Νݻw_z@Cϟ4ie-E{ViQjժ=zBw}?i!5]iWb>[o%  LKK~bAAAO>ԩSb}g??1̙3 -Xm)G0bQFeddϞ=["CjW{*?99ٳ'O~gkRj4iRaaalllVᄏqo6lؑ#Gf Y4BݼEEE_~0*jFکߙ$Ϲ:i1=ŷTj#q2ΜW4_Xثj{+"ķfGV[Q Qo?(XN*.BHjKϷtzc 7 :l؀D'VBQoY,$L$ ,LᙙTL֭?,tEܹEN8a8@CbYjK?Gu#dggK%%%iժXC5HjTہeeev[' 3g΄mBW6pQsVBFG 5G\ZIh֬Y֭/4trI!n漷"GVキWUF",4k'^{g=;0$ Ֆ%Y 䯾^F l#gť+#k"BEhIm)ɻ>ݫe[]M] Y(V|yzvYlk$ S!+/BB"KTא 7LٕJG|4.o͛7kL5{֭ -Z.R`QmQX0`gj,S-j,Ν;''k5ާXjUmA ^Yɚn\=T[WVMqU]F`]Fk߾\ ӧf{PT+;HGWO]ڱ]]HBPW^D6B=Oy{{fdd-ҥK7ovvv6;P~СCR傩-ү0VB=' +V|G]p!??_@5[o,+>cƌ>}}kc5^U5{ez 8kHMqU]F3`ӂJ;wۇ 1+-h ;Hg9ݴi<2xBT@KU7ߔ 0c iܹs;f؀-F>tPIIرG! 9r?PZZyf5ES[:t g W[G=B_b:;;'$$G֙,4Z_fBCLT;6%_U[PcWV1{j3*j% MqU7nT0ݴ`nnn^XfƬnA#<һ?d,[֩cS$YH 9@}\yIYJK$z7db,_\~#Eufoo߻w 6H{)$''k 4TVV /8::zxxEwQq)fGuҮ];:It&ժ-[lMd7|ҥKGׯajY=/qUW[PcWV1{j3*j% ϭfS0I #|bGVIIŕSS[Vt[cYyVtؑ,$LpՃd!!%2jʕ+ Ǐwvv>w~W~6{edz 8V{F뙛ΪӳP=BłݻO>w~駵gAiCu2N},d!a*!u%YHHdHUVV6XPz,&&FT ?ybڂ뽲I2=T[WVqsYuz'YV~M=+-Xm-(wHjd!a*6qr} fdn&ndPbbbjV7Y}KT ?ybڂ뽲I2=T[WVqsYuz'YV׋"*P{V[WkȄS$ kP<N䪑,$L+...''G1HMMݶmU, ĭXlϷ~R]_Ѹ/_>m4z7%sH,$YH ` @!Gjj^YYI`!7$ -t4q'O'3$ шΞ=㓕Uzj$ ]?gG|ߨ*z}N/v3n;tp+t^ 1YyOV}NR6̮|H1b/BԮ]N2rnIZADD)ԓZ \4qm޼ҥKcy7daxԇ_pv;wkӃ9]Q)Svo¿s⥴S4I9GDCli8:g6K-[?ϵ'v1hRE'z"3Vf=oZ}k욪͇d!a*drv]Q9X>B 0رc޽{I >,\r~G7ݼ^^]2-g|.t~:cnn4,fe8gqt45R[.]\[_/éV[FW3y)h=zxg#vYz*[z:ӳ4<JδN|Hqww!G]]]~j'YhBhhhYY-[ybbb-wά,ޔ SwOk'~#>C?%ѱ-Z蟳<ΙFjkᝏ>:}{__]_h4[Ź7?%foog͛K~8hg&L=ZRPmښO0Ȝ9s\]]=<B 0JJJhGڷo_BBBw7–2R' }}=L,߮tIy߲+.Ql#?ϓQCfU戮Wwtفl咋>g8Z^gNk-z}HjI>d!BT=톻{T uOB7 $G BԉO>dժUus&&&\ g8ksDOΙx {U>?O7 ;u]%?pdaxaȿ'6;pSt)Z^f_BZaޅ:cǎޔ x7Wׯ__$ HHФTVVFFFsߖ-[N>pB'YH+ ػPgN<+#bߦ3)Iґ,$ S'YH+ ػmB:lh"|!nI .}Ed!'[,w}ףG7oJ_޹s眜6m+55Uv۶mvvv~~~ 3$S r-K.ۮ]yyyAAA[l웍H .}Bd!'[,SPPիWO0A?~ڵׯ_/o믿feeLgE/[yf___i`HHȬYĴo֣>ʩFVwۨ%KY^G-|wy͛7wL1}W-1O~=L2eϞ=!Sڵk~k޼4ŋ… }m$ i{ lY6mJJJ;v4ܹsc<O?p2|-b/' _;Y$ i{ lYի׀>, yOeeeG'O}ƍ;99ɓS[n!0V'U]۫sS*& CBB®\2k,^C EVw[6`#F+**&Lyfi`\\ۥz2?c2%tСM6AAA;vG^jO˖-_v3=z<ٲ:4H* gΜIFVw믿>3՝P3, ={ӧ   ߾}Ν;ϝ;$yZ77oZzda~,]6p6r--ZXvmu'Q9ggg׽{իW&>FEE9;;nٲEN=í[߻wda۶m333M߿BBQM  ?}Ш_tQ,n֭[WĴDE~ǥYYYvQVYf]v5̰Y紦lEfd!0`B5\zu7nܠ*ط-ux1@zJ/8O?㏛}H.44O?V\YVVuV= ]pXСCNsNy10??_(yڋ/y睛7o@hhݺuv/**ڳ54y?f6mڨQ^+))1JTѮ]u릑,9hNsX>BZa^I}8`qWQQ?Y8uԻ_4@WiipBͥNNNt-Z3gά_><<{ZPpM6_xBraִ9k׮@ yaE fuNk:Z-P\\~ 6 w5ׯ_OfBKFaNXȢ/Bg@(Y(<䓢dݻwѣG[ w5dc8p@{Lj FճgT66p.F ABzz8!rᒅ'N_ݺuWJwٌΝ;x'f ŝo#I}8`# SHl ''D7\Pd(ehh(/(E>hXLZ0))i...bY^^^d ٷ-F`t1_m _26s8M;vI4Yb )Yx= w5}^^^IEzаƙB|(D̟L!6p@x1M۶m/B9,[Lj; O4YXUU&7l0ZaݻPWvYˀ?8lOAAA@@z%_8 [Ed޼y 1|ky `c.euС"\3I& '|RoݺuhD_} ]v>|xǏ… 5EП}ƎkvB VѥvQBEjd ҃0d :T\F)A/:l2^zqBB YfI6ZaUTTfG *((QモS׷ir ^^^~BBy̞I6Za'_XL!Ⱥu&?J)--b)Shx}ׯkLK XV#_H@ @m̞=[.Q!3f֬Y#g n1-†C+ `d ԒruuaÆQ3Ɗ4n5B X/|!Ba*ubСbn:p4>B 0,aL!0WUUUJJJLLŋʽ+bEUX5q0ƊS 8y<ޛ}K)@c|ŋy睯2l–-[PVV{nqxTAs6pF9X>BZaTıL@ bˣd˗/_jU= ,2KX>V+Vnm,2KX+Wݻ^͜9n 9>BZap,5!Dy7ys׮]#WFaaauC%D>ttVZ,2\Ԛ6i::f͞:q/qFDD-:vX\\gfG"j::I c.m4m5\'C/OMMyj111 8"&YH+ իW/^| c.m$ Z[›7oΝ;]ڝ;wnɒ% 8"&YH+ 'SIc.m$ `F} O8O.`"##kKi 9"&YH+ 'SX\\,N"_ȱ 6ttME+VˣU{IIIlHDMGg5BZahB Be,NyY\~cYu/\`MmpBll,gG"j+M-&YH+ 3SHc.m$ Zӆٲ,MxWZJYu/IlSXȺ_wM^ZIl0#GlZ,)$_ȱ 6J_;9o-W{*8;wx1k;YǎЦIe& 멱Fqnm۶v=e:M;ӈ#-;;;rvvnӦMPPЎ;q6oE{֪o΍da.^ 8m YhcQQї_~dɻQQ3-6>ꪨZwhMV4~|!2\ڬ+Y(/?>_ش!-<0n܈ F[c/:;wXdV^.~߆a,l}[4$_'= N6_~8pkSXX8`i;tqF1|ӦMOl' I&Hr6H' -xёQQaY~4KD $ ǏYAКd!06qKNN~zf k/ܶm± Ot{{;?ÛO-NV흞9zpbELyZ{&{^Ow*;_S'%ѳ/y;::":z^tt%nڴ4L%VY}G) Yzz-ixxxx```ZZZvv/(  JOO'N* '>L履~z5*/ѣGFpww?qQwpg}vfVҖ={vb|eSШrՉYfiWQ *S&n322ٳ 7b( ;w4dÆ 3Z-V*?cƌ!CӠA䲩U̙3 P;YX}luyfKcM4066UVbRwmQOr6Ǝt*Slѳl2;;{O嗛X eFҷ{[ス~Ѵ|啉|+(WE{տju|\#Bk)BZaP|aΝŖ󋌌}l7$Sȱ FI\\:;7wPwfKug"oFWfK=\zig΢;SD'}"-=KӹI]Y'Fݵkg5Uճ?N5?s%*/*D_cQ3ryjEmШsE?.ECq>>]M[Ym&{Kvw$%x{oѣCJᙙhnK͗%?q"֭ӥK_0.ZȰ)_mb˯ /ڶmqɩnM~*Ez`pZ z^LWXX(lj_UVF%TL1Zvv_RRboo j԰V^^.EmYhZ-V*/䧟~EeS@CAaե'9}հ_ɍ,lP㳁jU,֭[!CM&v)O_6|<>HHw?vw'܈-$}ͿϚ5hB&X#E;p'FV0:Q >5BZa`AB???www;$$AÚe IIIvqq?Be5VP윽n|ٻG?SqA9:NZh!O%'DmfK=WuU[!רּ0jرO?RZ4\L=7;͛7Y h~fK4(j@n"W 4ۢQݚ`Lt-?#%%%FkPbrG,EV Fu9>ڕg۩7kH9Tl`cG)&(..-((pttCĶИSSŽ5;:z򘹻|V^q6FD_Os * wwWӫWC4h8p2dH<k;$pq"|t٠78VH/?}5ENOkDhMܹr|T Y@І{W~4_yӮVf~jִѩf[ܽ:c 7ɰa_R:AP.s^h s*TƠA^q܀"1b41D-.VV{M&k%[1^|l_h> PԳ<\çʛC`:Zdu[4OΏz#8~Q9?K+uDG"--^oR 1c4|ܹǎ3}h1CJJJ#FHG?n޼Y<5*D,711 77_߿4\LޡCM6_k7n8uvvNHH(+++2zhyeSШnРAOsU(rШL|jEUlvׯجIII cvFEȿY(*MV r݊3ڇu\l;kLr6P+-jU+۶mHѿh"-$6Z(ճώ5.Yz<1潹nn}KZD1ԿF$  W_aN]v>|Ǐ/\… 5˹;=p("eSd%$ z=KڷoV|>]fMɻ>ݫe[]L`g''qijyn򾯯ǍleϵsщM8Kvph+OQ={ohrGceĝ1C۴=0]_.%}50ySD /]6GL]_~y^^]>\i:Zdu[4Ozٳ_..E7R3?uL̻ &=PZZK/u_W^^o˥/^\hQn{aix|||Ϟ=@qC1PV\\ܺukǎAAAcF-1"zѲeKwƍkqҥ>}(-[|}}S\GlҥK /hM*]v*S)hrGׯT,tuZa.裨{ATׂ Ąڕ *^! 9>ڕgYcƎt媕ȑӁDEѣG95r+bQ[lDm29B9t-7GӒMRQ'5"&YXVPOFI&\V\k/2ҬDp6~xe/$ +Y(mŐ c,dEj5ZګJܫ_~eɒ{1|h--ׯek,_|ڴi[ȿ6ɕ+W5Yـ# v<QQ5bGw{uwwҥ(aii85e H?~ H,T~Ѫ=뽿mܵ6gR~_KE6[{ꕒB!={V\gee5_y̠W_}&,G:#55Bڭ[7G U7_)9Bjzx)Ѫow-DŽuرT3in S ,IBJۼyK6V]]]g}HLL gt89H,m***Zf⣄*((QBNѢGlllaa!M oSRR86p<Qё,$YT#_XL!Btt$ >V]̟?`#5B|!B@d!]-BDD+,,婀`9#5B|!B@d!]$ wرw^vp˵Dtt$ Id Btttu,BCCh:㑈d!BLL! Yhɖ,kBqF /`ۗPWq%6ttVZ,-_XTTD,:wѰ0('|j*lfd!PgJKKM4P~ ? |T7~%nU(BR֧X*HukۮYtm k@D [zVETB \4\Ji;f0 IN&~+s~:IJD"'N z7=1 @Xz޻dɒ{Zݻws„ ?Ϫ@G 'BYH1;vxM.F: d00p&=#}a! P`8C\ Mߐ0bi,,#pH7da! 3@! A=(`ňfTYY_]]+ gZC\h.&Mںu//4hoBP p1YD’C;BV,,#^ͤ0J^h8 ߐ0bD gZC\hJBy @+|CzPM&qR(/4hmoBP p1~+((Jrd/_'piߐ0bD\guք |RؠpA"pߐ0bD"˗/ #F\ 1/ c]zy{/{" g YX!@.F{葕ugׯ֍K [ {}9dffSRh8|oBP p1Ȗ/_͍hXQQ褰f^x譄?=pߐ0ňTjժCG硇ҍm4 gm"Hẅ́7y7|sҤIN KJJ&L}}{z =ENNkS'Bm(`TM 3331a~~ZZZ0VVVFU]]bŊ/8fgg Y'Bm(`Dz٭[譄eeeui\^X+)8zap &Bm(`/>歄jh^ )hxeuYBhɾB~ ۷oߢE꽕^$5/X EO 4d_X ? $64)o/,Іh煒BhU}a!60@[//@k !ڰyZd_X ? ж %:'Bm(`6/ݻWRs/,ІHA!)9Bh@(9pBP %WX _@4 A (`W^a!(| +,/Pr@^y0JH+:t(AQ@}a!60@[`zKH$|r]d/,Іh  c%G$ WVV"h%}a!60@vUW 0 FKH$ҳgnM@ !ڰnݺ 8((9"HBg/,Іhê훝=p߿ff=j/,Іh~fff%Gݳ-Z[UMPm[qqqvvv׿ +++u ɾB~ ͻ+ba-ܢCMPm^AAAϞ=#ZXXCMPm^uuu߾}czZd_X ? ƍ/ 'Bm(`u^n]~~ӧqƎSLG/xMΝ;7x}RC& !СCӦM0ao~󛊊C4e˖鄦R^^3L8qeee-G9Bh@ʊ+&M{nq)a޽f͚? 8ɾB~ h={1w VVV4n/,ІZ=؉{1c hd_X ? /… N={;#}a!60?Ǐ5 ,0Re(` tK,D"r&چ2a!P@>{GD{͘1@X10~{ɒ%&ڒI&υ !5޽[D[Ϯ[0O~ga!60ODs&,,І5eʔs1ZL^=|[Rc(` \G N:]uU[lIȭqqWChB5a ,~ W2aate׮]wqy睗dH&,“}a!60޽{O<x 33]vv[.]N;`%6[EEѣ͜93cǎС 7PRRݾzAedd… 6A 3t_kF6SC̟?Ov>s7lPwm7|sǎ{5k֬zk+۶m :ƍtxGOwڕmɴG/,&Bm(` \ pÆ &UѣG_tE>qyƍ wqM7E7':t-[s׏92=(?eeeu]`cMN0=X뮻j=xͨ͵r#f`6|[}ݱi̘15Bw}hrٳYfo~Μ9[o530pw};ηmK¿x=4d_X ? +Yرc/˅oflH$]߰aC^{{/]~z]޽i~;׻xZ+ܞVuV v]/--Ȩsж!>z*O^~wqG^{.1kٻwofff%x_oBj/,Іՠ?CZkc߶o߾,q'=v_׭[7|N:穧JxZo{fZpV10KbJKKsrrJJJ:tP\\ܱc`Kvvv532$8f4%qےi/,&Bm(` \G&s{_>}ޥuxg٥K#n<❅7n;Խ0^3ms_3&FAbM}Wxe]6a„K.$X⋃//r~`}IlٲN8!+++ږL7lpԿ !p1,lc9 [a!BHp[XC׶Ja!BHpw}icYym;, ̞=0O~ga!60sݻWE[sϭ[0O~ga!60۷/YDD[2yjd?P@Ə/^ػwX_*@[js='dm5BH`r&R݊+V\iDWa!P@kPZZ:vXQ)^Xt @#&Bm(`ؼyرc_HzGϟo иɾB~ h=ND&N|5hd_X ? 6k֬{,Yw^Aݻ.]:a„gF.G3Bh@ӎ;|i"AɡJя~TTTd$}a!60+,/Pr@^y0JH+@izP(9 MBP %WX _@4 A (`ءC~ 8Bh@U ,(**D"˗/EJ&Bm(`xРA0VrD"`{ee.V2Bh@aÇ0`@4/HgϞ#G9z&Bm(`6[n,** JH$ҿ`P@ !ڰ}fgg80(99x`=j/,Іhƍ׽{Vzxbj/,ІhۊM^YY[UMPmޕW^ ovm/,Іh z999:Zd_X ? UWW7(97NP`ܸqAɱxb]p/,ІhUUU>l~~;6(9LVO>wH$~]Bm(`f;L2eҤI=\EEš,[PW\tܹs60ُBh@43gΝ;5l߾}֭"d?JX ? JKKGsN`<[l5׬Y @N6@-*06qֿTnnN5!4y|ذC L?7,>MpCTⰰi©Smڴi.d_X>k0ТvXx_kР3Ѯ]Ogq睷raa?Vm;3{3_vƶ 17c_]yO` Vˣ釖yE<6cĈˎYH>>>;Cs6U^LX?[n^{K/raas=}\Hɾ|(`E% cw_oefv/VXoZzAl+[B k:A3v=w꩝f̼{_+2soGy~E}yB#nZ=?zX o\LX$yaaa>ؼyO9^x!8t gmZ2nn&ǪmkStDoA'ͷ~c3z<63xbkc_rNN9kREWil&mpb+qIWn=<|_VVv%hp[О3엑~u/|ѣrssڷTL'?Ķ{>ms &~LÞsFЪ.Yۏn韾2~r=ߍN=z> Ν;V?ri  ^z\ۈgތnG%*=x9Jw]nXo_7v?k77K50{6lXtnҥi{k+۶m ^۷o7nاOϟgdd{6luzxGѱcǓO>ꫯ޵kWt7{5k֬ºw8p`ر:tJJJ#, hQɇص멱x/~w}\|{noC`I'?l фk./z;bLD3X :w-,9|[{_`n3cݗ^wv=_ 7\t#|+} v6⢝kG:y1ⲝ<v/{(77˂o[`=R͓&|_+O jXn-{G=*H.]:ۂ%6:įZ3~ѱwKOnFFliju7:j>xc$q:R!8E7?WS+k ydѴ1gܓ7./o|^ބ3ϗ]ܛoYVV{wmDXXXX|&>z.h'.;3x-̞=;X5kV̙][G>|֭[KJJ:K 8wwo}+}̘15?pʔ)Cݲe˞={#G 0ُGX>k0Т ߟp޽{nڲWћuVÂFzj=,w+y%+>vzYk9x荬+7",<>۶?]/)Gbkp{6i^,O +2zܷoحW mX<0~OK57H&:>5|[\}z>Ìg3\##O2On&,4GXot|!77\HzvgYXc_ufl+V~Ĝo~sDOn^=j{䟅 C:Fc*~w{h kgO˻w׸; ;w]o߾}YYYt=X F999%%%:t(..رc%;;;Zhuzx 3֙4wfdd֮]૰x6@-A\t~l|}zż/dQSl`[oo}׍ ?_H,{FuۜS4,|k dEջ|WO_I|W]7;|}Ӛܯ}ؿKYy'⾒W>;:Iš=iИJwENׇ7m@x_3&Mzgbﮍ ǏW' .0a%\_|_^&v̛7o߳gOl`ƍ민ф[n=4a!< #573K,`5_.伱c5ݻw`}{M3H 1v=Xm;_̮bȻ{_[o&x+|20^&,=2y/ AW4+_,x*?k nKQzjXRkٶΝ;Μà=no?x=U' ^u1c[yrd$,?;Õ[|pNj0ރjwT|g6%K$#|oƟ<`^ބsg|XXQQd;v|?~C 5jTtԩSO>ɓ'&M ֧MdX]tYtiyyo1bĈK/фAoGظqciiڵk*a!& gmZT0Ю]O~v)-G|?]t`%G6m^sǕ ΜvԩC=FqQpOsY+WoPXϿ۳vvZ԰O8x*32ڟs{nib̻;t89^# ŗ<`!g;:xc$qS)ΰmÏ:p׫z,]d߾}] 3(ڗ_~9[nO+?iwet7oN2,we˖p YYYӦM_^^~7v1'''??hGׯ_FF9O 0ُGX>k0ТⅅK:,SUW]a ը?4C%ԩS]Hɾ|(`E -%ޒ?sCM:UhӦMs m'BY%,X,xKwWa7 (ab4GXdɒ-[^[r 4i; (ab4GXXQQ1k,_#VYY @N6@-JXhX,Ə_^^.kɓ':Γ}a! P@Z,K3s5ObĈ;|W_}]֯_߫WFwVrrr^~;6;;C7pCIIIh*X?駟qnذ!v[.]N;`%6bYYYfΜ RovѬYb{cj8⹂ҩSoYzA]ѿ F7&ӟ zwD$sva!LP@J&,pw};η`g?իWGwXjgL2eС[lٳg_?ra÷nZRRrw<8ΣG袋} /;Ǎwر㦛nJxa1cj9OI[]wݕxy^}뮻.1lPԻs'" e/,ІRT2aaQQэ7ޘy'k_۾}{}̘1knk^G;vDKKK322윓D6lիWtwޒXP5\>G322ıۮ]k9/}eee`%z'Ƀ$حݺuÇԩS>}zdA=Pd., ɾB~ HQ މ'n޼yw޿={bau]7iҤ'~k_nݺuk#7.,wga>}*,ܸqc>Ү]$;lXO>d.]@;{"9P&Bm(` E%2dʕŻv??K.K.-//7F VZ۷o?/ rEmܸtڵW]uUS?b0hQ . hXbї^zi wΝ;RK.x6_,++{'j'lPԻs'" e/,ІRT2aUOs#Fx뭷ۗ-[{ 'deeM6-&~fffP!'np(//;v옓!OL2SN9n̙3{n Βx%KyA 4 lPԻs'" e/,ІRTC )4ԩS 4Zf/,ІRԩSY4iӦhd_X ? (a!Bd_X ? %KlٲEE3yV\i2}a!60***f͚%ӢJ !5~raɆ-6Bh@xΜ9b-O묳"@k A (`4RPPн{`K_Zre764)ݻCRJU]y0@)((ׯ_OѣW^ +** <O8p`DBPr+,/ @MW>ttz!ԥ'B~aD" իW{ޚ2eJEEE’8۷onjӨ>}{@3Bj ?PD` ӽ{gya4)<|pPÇٳg=2E"nݺEo%,++OZIaM [^JX1ZX$1bD[ м0AR3ΐ@Bj ?o߾E{+a I k*..^`AhBH%64)Rc~BI!BBH兒Bh U %j !5@ J 5Bj ? +)LX1RNYYY0@k&,~&>@E̙L| Mva!3@B0g04-څ` `iZ vj]Ј]XÇ{of^^E̙HIѤСCv`4h93fR%/hD.,sfRLݤP^и]XxIE̙HBy!&>_` @Jطo_AAAUUU'6./\|yL| ݊va!3:k„ EEEL>)lP^XXXxw 4(x:04,څ` @.\\A暕+W{aC#慕/>KRm.,sfB {}߿֍K 充#GٳgNNN=  )ҹh93_?߿荆|Ašy+z`ER(څ֪UyF M^ѣGݻ3Ϝ3g֭['NXQQ褰t۷osrrb/}ٟgkSa!@*%{ٳ 7X4:/&CUWWg? wnBGX2^xݻgffFo%,++OZIaM uW^Ж RC$ݻw[ м0ARK_ҠAm ۷oѢEJXdš,X' M M hcmVPR-J ͫJ }5BI!1B {=I!1BtQRRWBb?Ҕ@@(HSBiJX M )a!4%,P?Ҕ ?IXҔҔҔҔҔҔҔҔҔT8a!MHQ @Є4 h4a!@jhB&,фMX@ RF6a!&,HmBMXڄ4 h ]'?? BҵkWh|XxO׮]uG Y@jBU|#,hՄ4a!@&, Z5a!GX)kIW]v 5k_um0@',vcHW]v WR@4,={󤟮]GF6 ??_X a!hX3owW] 7@@ c:owB\ZR@s|bŊ!pPHs&,LymR՚kCX 7@ @ 1O}*77wر}QCٻwׇS)hDDV*;f3$O*,pfBHuJUUզM.o}N8 V[oz3<3##㬳z{ /p׮]ѢEAc{?WN:r)^{MƚWnkw^ v>n>&srr궹n=xfffUUUPܹsEEE6gҥ_3,LxGaÆPW Ύ:hs=G{}uY&5_o7xYgܹr;j>AAI'> Z K13[+!uW^ =Hxq%ϖb-ڨeȑy_|E 7@k&,P7,>}3?s}c`=[ӟtqo`Kߪ~[:u:p@̓v۽{??zчS5oQ>x?=A_?^uc_jUk׮!CmpFFF?LZocaa'8/[sc^}q F1 b `nsh|5aDQ4x Ġh\P#N9ǭe[3==03tQ߷zͪ.vڵ6kVu;e sWr,Ab3ve2uWUw?[#:F믿>> τٿ4UvR'Q*{XFYjmXy$c'NܵkW]]ݾ}2ouOY^LdZ1"X[~ᆻz8StToMfipڂsOhw?XIec=?N.?:/KIbiw%zWm5raaz{ bя¨wG!sm۶o߾59^o,,RUtiwJr?,:ֹ6$7@>@wԩҥKw]]]OqiJ}_gϞ^>o&m(** V>| ?+7M݌u^YݻڵkkkkraaA;mڴL[nOֳgEKJJz}G&d*vYzfwݫWd{~ +裏;Lg*dذaԧtKaa7Z;ٳϊ+ZPP}S~wg'pWm۶LA=9^o,,RUtڤJr?kg+|Zڐ  9Z/ A?oaFR@sl}_| ?[nСsiI'|K$"c[tI't1 4h?F8c]fMDXaIR O  #8o'H3^{Ûa!D… N#)&,"DXBOsc&,"DX`1f!  9ƙ rLX`h9BsW CX`1V,YR^^M KKKKJJt  9Ν;aXXZZEn",07cƌ dXXZZZTTT\\s HX`1nnnݺ >yٲe мc4O> мOL&C )..!nf',0Ov[2,4hPYY0p4;a!D9xڷo_aaa"8묳%  9رcIJet%  xαnڵ~sέr+7@@4uWՌ3v!|{-\pɒ%27@@4iq%%%7oƔ)Sjkk ?9}Aa ;vL8Q^n!,qqݺuw<b VUUAn}Oх +${\֭ۜ9sziW ԆTv*,,6jlْ\,((I=-l ]n]]ʧ=a!Ќ7@@4א[wmvBPAAh۶m6mk׮;vlN#dYMjC'#GСC2 xTC fL{ms<,vWiOD.GX4 ?-T_߾}>f׿ 6d޽{W\٥KFWda6zwo߾>رcGXImR i g wE^z)|06S"ˑvWiOD.GX4 ?? oȑ&LH[Foٳ/Reee;/窪V\H$Lے&K.+V֏?>{XؤfTguVᶙp ')`# =3ßxT>ȥ f"ª+'-w0H,\0rϞ=s0`@AA|˗tIʡCY&ʴ-iRVZտ>{AcM!mLaaHU]]}饗vر(06Sf̘Ѿ}F իWPe]%{'"#,qB a!?7@@ AXN7@@ AXN7@@ AXN7@@OX4*i4c|+V6W6'ݗh   >,l&&a4Pfk&giUTTSOq߼6pa!D0az񐇅m۶ݻwo"ݗh   2,<"ErMuuUW]եKΝ; ۆ[w=(6ŋ4SO}3eʔ={v/^Irgڵٳ3CڙW_}u;v<Ǎm۶,{`LJv`}ez$a53SG}饗*** 6Ei&]j۷og>mۭ[62K=љ-Ŗ k|.W,rڋ-S%=СCx +',h' G~7poyĈeee[lzƎaÆɓ'vi3f5jwk^I>x,δ2d͚5W\q%d 'NZsX&J2zuf &s9?qgV{+| iж[n%裏3^#o6gL[3X񹜚+KbTI"/~QUU/_tEB0p  =,,***--M.⋽{nI>}‡RٲeKr <`W^y%\^^ޣG̛7k׮qeomv6ծ] AU5-h% e vZ:3a /rr9hsj7nׯ޽ݻK/g?Y͹[Lg^s95:޾K{i/LtmΜ9o! a!D@ڵJ. ۆTOOm۶M6p 6S;Vȑ#;t^ʠ1Yrz5er$SLaa.a흩M R/S\p}߿eeuZ3hX k|.Ѱ0SWbr]vر:u۷#<","z ˓}MP`ڷ߰aClڴO>ӦM;' Vw߽}>`ǎ³K/\^n]X&S%:0S&ԇY,̥gy _SO=o}+m?iLj]4fzʱX6Ŗ{\K.B0p  >,<&0Y#GN0&rˈ#x㍆?70P={vPaʠw^l޼SNdjgjtbŊ`㳇guV`aLLŚ֫3SfaVt?ް9vZHL[3%,̴FLXo_9 ߦ2Ur?sUUU+WL$B0p  >,1cF+`,v0H,\0{gϞ9s 0`>`.lٲe7tS)S;Vjժ}ݻw=,K;vXTTTITIaa:3apw;tЫWD;mRx 諠̱·-SX f 3Ѱ0SW·i/L,_N V:t͚5B0p  >, /ի[-\{yHX"@XsW_}-[6n8jԨkI۾۴in7}Ѽy 7@@4:8k,alمg/7 .mΟ?_={~oρ   s;v%}y׮]"9͛7/_\2}:7 ?9z;vB '9^g@$̘1#q" 9j1 乒_Wl`B+++L"||/_n}o>l=`h^Bs1t$??SO=uٲe мcW^I۷ommn0p4/a!D9x2dH2)>W\\C NX`1fΜ \VVC NX`1׭[D"qg 7@K@s . H,[LWZ"ck={p 7$3fCc.\XWW 0pa!D9VS[[üjժ@sl9– u߾}_m&WvaϞ=|бcG`Bs-DRByap6\௰0p a!D9ǖФ0zkVXQXX\|ȵPc2U6/L7nʔ)}ѤI| )`BsͮQYPTTSO5\>Z1Ydžyaڰw5jԱ;z;v7@>@sl^ mݻ!Bb/0p3a!D9f{R~O>SN7tS' }_> 3τ[-ZM6Y̘1K.AUV%Wnڴiџԧ65KmƍGq|͆_q5{É"c32e{w˳t_'Lk׮nא]t]wU]]# 2$,p%lݺ5{⊊￿o߾ɕcǎRʹfz08!\AU†{OYlذ!p0p1!,0،*++O^SShuܹ3Xv)|# $^*Xڶm\پ};v*`~J:t=,l=IEEԩS?cp!,0ؼr ,,ر͛_eaam aXضm> X!J Ìa!D9fK^{I_C}b֭oE]6,lLkHnkHCO>EEŤINq ,CX`α%4_OرM7ݔ{X}}k:tիߟ6,lLiӦO?裏פ6SXaÆ#Fs17|3*,,ҥ̙3Nq ,CX`αH X!,0r䅒B@|  9(ya!Bs-M^()H%,CX`α %!a!BsC^()H!,0j䅒B 8@slMz̼H$%ӧO1!,CX`1rֿ@s3a!@n BLX{a!D98Dk"DX`1 24h_w֭zܸq?ϕ&dȐ!ڵ##<2Yn8䓯ꪧ~Z̽ ބބѣ~_jrMN{fn۶oҗo}K7̽ Ƅބ>}nȑjrͷ>Q]]sG-[?ߩS'p0^7p@s#G<·5557;vػw+W&W.Yo߾ڵ2dȣ>kWBsqVow 6VØ"cNXGtRj^7px@s?0m z @K!,0+K,)//ߦNX"&,CX`1Vvy'ya8arʡC"&,CX`1nF1pd^^ti=u@+!,07=P"顇*,,L$Æ +++!FXć"c<%1c $,CX`1D"l2]К@|  Zzαd'fO&MH$M+JIYz+a!Bs|oyVZ?|pGGXć"%kkkM|rnm۶mTp8!,h9ǭ[o߾]fܹ>6@|  wαFO>yyfϛ7_ܷo߁}@|  wqɻv@֮]{йn߷zyyƫ ,/!,h9{nBh!SNݿ|H^^;]^B/f  @4ל^qӧTXռaB >5x=۲hQW.--=ϩ˫ @|  kq֬Yhiw>o"zyDXԼPXć"gΜHаgŊI{L[C\rEy?G;i3AW\=UySkk _txgIya!D@ SV fSTTSO+׮]ۯ_z5  !5y0sCurqJ/:aX{^(,CX۰m۶{==N/rIaam,L4^駟~7&2d͚5W\q%$O81|ڔ(uehgƌFZ~;_{ovw]L- djR?͛77l7h͠|oAaio(*ޱc>ݐٸ=PoWpMИ-$f?%=ѦM,{|w>Ăv[baAÖ[L/kn庋/0{fƆ6 y:|ZX>Xӹg5F_2b @IXXZZ\^n]޽(** aTv*,,L.R@X8`W^y%\^^ޣGwy4!S 2iGA7>}4|,(e˖reeeAAA.T>8_~94vf:i+1[ktw% x r҅65,<xw +Xz+pw|:nC3+7-\~דNd?aQbUW}+"m A /aafc׮W=r֦q da.'I]M$>{3ucm\jm,sYsgǴH[CP˵XPЮњӖ֦A w槓zuktҽM^vٻwd\Δ BV /o~K/>w߽}>`ǎϢ[oذ&wn߾o}`OmReMEYK4^v6)vUVV&lh{K~c2' Ν;6mɒ%3fL={g>ûk,,<ꅎV۶mL%sv>U/ ֤-˿:/v> Ӗf=ϲ1,3ٟ,L31퓅Ao=̰<   ,={vK/TYYty|N9唴5djaMJQo#F9/{2$Iaavi'OX~}P]wǒK'pB>r XXXw^\W)*f*o!ܟ, ?owͫuRka`}Xᆍ9' /j\=E2S9֜Og;f?_;' ߢyQGٰ߫u=oo?0zG75o}ݛf᷿Kz|s?SB\  Z-,ܽ{M7ԧOvLRSS$իWǎ/#प+`!:UV裏޽9sA ^ziP[QQ…  ,ܳgOP +֐6)펲FAO?(,,L$AU3L]|~:tNGpNï&[/zA\xq]wǒK3fh߾}ö5zLXXOS}n~Fz?8?31Tƌ(M7]jݙg~qʔkѻOtnf 3U?=5^=E2I9֜7 /p|뷵Xңo g;ajhdӆu=^pٯ{2N6m~:_pSО,o7wٻ7S7AS_[X%2 ES:5)a!@.yo߾=܇ /˱FzREwLkjoosN9s:x K۝ݣSW]5ìO޺trB /\}9z,ɢaAm}]ӳ*~䫻wOt9$"m kNmWώ6M=昂zqio,,3w} ~?"{:QWO{c=&x35>虩_ڦM;&hjqU/5Z[Zr̶mK5)a!@.B꫷lٲqQF]s5%̛7?s#y*{޽{臸IBs5k0{~\rIeec+{s*,l*ֽ}Yg}Ku0a!D@s9.XB-g}s*,lo=ѹgYǍ:DXJXć"_}UVrE͘1c9=KXp88uTYYḟTX%,hMBfs\d%:Bm۶̇TX%,hMBfs̙ܿ3%:nݺpƒ IXмsO~zaa #a!D!s|V^-@"VN~%KoSҒh @9)S͛7q?;w<ü0 W\9th @9۷{「#bh?#F8p`2/LK.ѣGqqqn&@9{ߖCW.--u x衇˃Kv5X(++ $BC;8k,{nD QWWWTTTXX8xD"ѵkaÆ3&r7@  ̙3FQ\'}Gs Dn8-[Iyui̘1BzKwe6_[1 !Vv& 0`@mmmn&@X\xw¼C5)bܹnA+#GL^xa0Eqha!D@T{WPP\޳gϔ)Sz١C/"{]t?~A?|޽ݛ\, 6׶Ei&^{챡C-8pn4/WNW]uUΝ; ۰ŋ sꩧY*o}yWM6߼ysK/7#RdoCp, Xߵk|Svر[nO>dʳNyWǏl{Ǐ7n۶mY˃%`sVXVZDbذaeeeQDXp&L|;cƌQF_~ǎ_ׯoyĈeee[l˲NVKYzaòlRm~2i˲I"/~QUU/_tEᶣG`aҤIO?o ˏ;vÆ 'O>ӲTh㯼o=XX`Av~$gҴmkH/|׮]wq1sW$ga g?ѡ!CYw *K'NVX1cDtha!D@T~0H$6lؐ\?`q=z5|-Sd3o޼|+5^xayLkƍKn̙[o۶4nݺ޽{'[lI.WVVOX?#dsι=`9脒aa6f9۷oO>RO}*m g?iڵ0vj*| T]w]0-[,7@  "d޽{_/~˗/O/((8m۶mӦM7]vUUU*T8YΝ;?OoDͲI_ek׎;SN}}Gmv A?u}L[y,**СCp;v 3=,l2md_n@^>rؓ!tP>m[e*cB UpM$DX,|7t{2Lշo߆Of*V~EM>}ڴi_ײo%-˾޽{W\?^J.da]׫<Ɵ}ٷvۙg,q9So1XXXoFOAR1A}۷ovnOʺuRק-cBD"݁I \pAfϞ=z^z28;/Y[n1bo ^ 0&Yږi?瞫Zre"=묳?wgM0ټ#GN0!{P\я~,O>=X3gNO80ܼ8<)A=LsVX#,CXG?,ٳgΜ9 (((>|>,{Db…ɕ ݻ'¯ʹIed'tRrСk֬ ?~^:vxeUWW~yWvD~+f.m4^ *y>X~U>cƌٷm4KXi/jժ}ݻwʇ냳p饗g(x-cBa!B< W$IOB >Ba!ʼy܂ @|  YӞ={nv !a!BC;` 1>nACB >vW_]j܈1c݂ @|  ԩSFMuuY @|  %K֯_/="Vm۶@< @9Ϝ9SzD|lݺu…n>[B >0O򗿔!'NkgB >y2_$qTɓy ΄@|  dα'?IyN8QR @՜ŋ-ZT]]-XpRSS|ܹ9a!D@9ܹ[o]po)d"6n\%i&w IXć" ?kjjVX1o޼sΡ$ BK78gTB > 'h{a!D98Dk"DX`1΄@s3a!@n BLX{a!D98Dk"DX`1΄@s?0m z @K!,0+K,)//ߦNX"&,CX`1Vv9dȐ0/ 'KJJZ[[Za!D9Ǹ5jyarzҥ={,..9@Xć"cܬZ*H <iȑɰ /,..!FXć"cj^:uO?-:"jjj,Y2gΜ:7"a!BC2x=?[ou7VUUA| @ҥKy)$UTTL8B ><#򗿔A ̙v1!,CXКs555f͒ ACs϶mܑ @|  Zsqv AC555 @Xć"kkkΝ+L̙~7%8 {U} @] UJe~#U[O튀}xVm]a{Ч>xjZ\ERQU"`_u=u-bP 7A"O'O{7d2wsνw>n$@  26XQQQYY)A*7o~g] 牅@  26X^^Az .tQ'C, s^f޺b.zb! C,؜… ,%UUU_W tꩧN%'$T-nj3&-^W| i6K:\{*B !@dg, -[l߾}gƌͻ;#Gڵ+[O9vؚ5k xm˕daCy{N>w;ÇoQTc4CҍS T{$8PMyMu-0+M^ھXF,XUpҤI֭ٻw . ~РAW0}DY~ȑ#G ,2eʶm>K.X)^z/XWWf͚X,x?߱G?Q⦛nJ='?pDJsܤMܹs'Nm?"j̓P{HqjϘ%ޔ`sS@1LJש{*B !@dU,\~ɓO9唁N>7ވ_vȑ#O:餡C'̱cNJ>G 65jTAA9㏷ S=G?~|tӦM.Z3߿5\S__5]w 2UKO;~R7ASm|ȑXlɒ%kb!D@9VWWr-~)[v!"C,9džyWOZpaEEb!D@9VVVvmXUU^{mѢEohhp <$C,Ȇ9C=C?P9 BĿ瞊F[b!?Bs#NXk@Xk@Xk@Xk@Xk@Xk@Xk@Ǜ?l1atb!D9Ǽ|֕@  9敚q%zabºt5b!D9|3yѣG{a|zʕEEEeee B ڵkcرc+W<8Xرcb!D9|T\\\XX8vX,6x|3SN52!C,0瘇nX,6dȐ؇.UV !@sC555R8f̘QF566 !@sO&M/}Keee cB ڵk cgc1b!?Bs8M:hdX"c޺bتU @&@  97|3;vPdX"c~:~mv}u]MMM cB P { 2I,X`14/qz!@&@  9֥P/0b!D9$C,0'җB cB A{JazacccEEED Nb!?Bs90T/\nWJ ~b!?Bs-l)l644|{6lX, U  !@sa+zuLй;aB sG){a}}}⣄q_|Rab!?Bs[n9pfٲe1:@  9檺;Så~oVYgu9甔b!C;  X`1}^/]555mڴ+>|__~p'@sm-Jas5552d̘1z!s^^&?hկ~T/ )"c>h/lO)lf 2-D{zaRiG^ йBsy%U/T tb!D9|Ӻ*~pt"cj B?X`1?{ammR7@  9歺X, EBs,?X`1~pt"c> :b!D9|& uBsL,눅X7@  93n#@sgb!]G,0B?X`1~pt" >dɒ%b1-l "  OX DXDXpر#_b1Eo0@ Y@ X@ X@ X@ X@ X@ X@ Ў=ƍ,Yk( X,fq1YE,B׏)X 9Nd- .]oF=;:  ,YD,Bb!Z<c9B7 t-=:qg)ߞ t3@h'z:%?b.=Љ'뮳:O>C1c~ @C B 쉅xVN|߼+|?O裏~wXa@C B e˖ >N ]|y/_ӟt޽Gzvi_}4OuФ, wg|Æ 7o^SSS|}uuO}Sr>M6޽ĉ~lhhhcw}Ǐ/((0aK/jժ#G/½{~9=;6l={v~&Mx)))ٲeKWځa<묳"8k_kϗ&8["Rg)B' \ @h팅7SO%>\pA|sڵkCM/| JΈ#˧zjŞ6f̘/'O޾}{[^[޽;{xد_;pW\q~^zR}Kuuu'6 7nܘ҆ BDb!d^a/444~ʾ}.O}S׫W4o' C={ѣG=|W~СC|3Y~}wI&߸ř9\hI&}EG 8k֬s=_0N5Vi6;w;h ^B_B \ @h / I+ה)Sn455OJU޽{7o{-(//1cFp‹/|'N,̝;׿u[Iõ2ä{qu0RmZ`7xc2 K,rl א[.𩧞J)S<O|>/o޼bap￿׵wN;-tgnٲ%xn~;h}X};+5rB(B W!b!Z;cO>YTTl6lHaȐ!o]v͘1#sYxW_}uر _e+W#o']}՝5 7nXRR /4667V # \ @hIca ?._N***>O>yر=X|s=ꯊΊl/O|6__/w_~޽{OyO9Ç'֤8x-%%%zJ1: l|eȐ!#G[?v0vV, ,[3<餓FCGW  \ @hy2ѳqo~x饗ƌ_J!tp @ aǏwtUWڵ/\ZZZ^^Bg t-'zWO|bӦM[r3f̘ŋ{S {HXf|:X눅@h7n>tVf͚B d'mɒ%ԔB\b!B$d!i@ d'D@&{HXf B=$]G,B3Ib!#RǏoE,lU|b!.|汰!,b!.USS3~D/L{!,b!6qq{a<>3gqFYYCЉB 4=]mŊXl„ 3<3t`aǎp @' Lt#FL0! :tԨQSN52{H:Xf obѣG>4eʔUV=$K,B3{S .\Ygώb͋7n ߓ=$@ LYnǎ[h+r<֮]e˖;ߟPdD͛zd'|2x{C%B 4=@vڻwٳۧe{gݺuc=$@,B3dٳg744qQ??t;D~8p@z^ !^b! w[~9p n$O~-?o`p @7 LYe+V";= !Fb! ܳgQ6lm !.b! }ݒ[t544Mn$]w%E=6tDU'Ʈի [|syb!@h&zf,W:hРSO=uׯn,,..~g[lХ0%?ocp @w LYXxEp XnݤI {yرL<p B 4=@Vi3СC744 :tРA-|צO޿}^ve{M4x`̘1w֭[t믿>駟,;vOH£GzEEEꪫjkk7lPZZѣWXo߾O~9r$xoج" X?x=v[ O{Hl&J .obO~'رk7nܦM>;|߾1lڴiv3g矟&M2e.ٳgw?:Ο?;wܿW^9k֬X,#ս+3f;wnO=ԩSn4R"ګIDATk=p>}/}b! @h&zf,9sfaaa߾}⊷z+~ذaI? pYVUU_>|pAAAW\\\YY_QRRұ#FzW3Έ/27L|O<_ww>cFyT8THzbb!@h&zX8x`>}Ço9e˖۷ۿ מ꓅퉅#GܵkW~ر5k 4(?~HY=5!B=$@D@h&zf,4iҺujjj .;wĉ?4  Zzu}}}~?я08M7я؞X`۶m;|ppK/_|n͚5X,}slܸ1f vxJC{H LYX~ɓ'r)>}o_ȑXlɒ%k׮9rI'4tº뮻 _=Q؞Xx`F*((8sG}tM6%c'<+f?mk~VJ\ /1/V,C JgϞǎ˶X:̙~/V,C JXf_AN?`!x{-,,ѣG=<cƌ)((8n_[o_~W]uUmmmrذaj{`WoVm۶Ç9֟,L JKK3=z+;?q»{߿dm(:p !b! +++2e]xᅳgNls饗^O6m׮]s9ϟ?y;w߿+5kV[tix޽{g?/ҜUXtX,#ս+3f̈ܳgivȑ7|6bXDUB֭[KJJۼ멞^UU_>|pAAA|yԨQ8`'g/~7x=X\QQ*&y`Ȑ!of]?߅z`M XYZZiӦ`w{3g߿qq%KZ^^C "+E#'={H,$ȤX,ftD@&{HXf B=$]G,B3Ib!#$p @ LdX눅@h&z2I,CuB 4=]- B,B3Х/_^]]x<VVVVTT"=$E,B3Хjjj&L腉Xϗ666"=$E,B3&N8nܸx/gy!Db!rX,|a3 :4XرcCЉB 4=]hĈ&LbC;vԩS %Ȁ3gbѣG>tZʰ!\b! x),**9rdccaCйB 4=~6²2Ӊ@h&z2cʕXXZZcӉ@h&z2(M:h! b!9sf,[jCB 4=@ill뮻do9͛7/ mѢEMMM&rIccwQ[[{<[]6;OlϞ=wy^4D3f9p ub! 7(z!DBp @N) ĉ@h&zHS B=$ b! җիW g1KzavTTTO !.b! Laqq>֍y^cǎ|;ƍ !Fb! G{yر|]z40xV\yb38C)t;D9)k&xO8Do{-,,ѣG{)7o`3<󩧞ٽ{)SN>?A=]6鹵ޠ׭[wĿTPP,kIa%_[|?sׯ_' % N&X,6l0C B 4=@?8p78mڴW_}u<µ0cƌ￿'7n\/=z7ͨQp n~ܲ ~Ç'=>bp} w]w>tP%Ҥ/!qUV|r`yȑ|apG cR!b!ZL2a'iVSS,߿_~ +[0`@Szz\nݸq`>[.XxzZocƌٶm[po=ĉ7 /'6H!tb!ZgB# dF]]: T߿[obeaa~&_xe]v75Ix~+X/fzΝ;c0^ ԴiӦ+bذagu^Q'M,=y۵kĉoYwW3|+O~oΜ9޽ /<餓~0nz'_o,o۶-oSO=50; [jjj4,,, !B\"@DaiJaB⃆&L !B\"@t^ Y|yDߙ-b!@. B ,)@tD,B B 7rb!@v B J!9 ;@hb!B䍜XB 4zRdFN,Nb!XP)2s#'d'M,rL]]ݼyd'6|XKB 4 \0ܸ.I,B 2ȍXB 4 \0ܸ.I,B 2ȍXB 4 ;f̘?'N2dHuuu}}e]vꩧN02Պq{رW6@W\2 @ BLj~|x޽ǍSOFKKӉ@hb!@&`c׮]FȒR&!tb!XI/W^+W4,@\2| @ BLrr$d'M,$L 7Kb!@v BLrr$d'M,$L 7Kb!@v BLrr$d'M,$L 7Kb!@v BLrr$d'M,RǏOslU rX&t˗WWW'`VVVVTT"{D,B TMMرc0q\reiiicc!XKB 4M$ @hb!D/OLz7gϞ]WWD rX&B??@Ξ= =b!@. Bk>䓚 t ]1b!@. B[_r޽H @hb!Dży8 @jhh;\%b!@. BƟ: d'N.3B\" " d͛7?쳮K  @hb!DByyBc…K  @hb!D… ;\5=tNPV['L:BrXKB 4"pڵǏݻYgrLٜ:vniKÒUUU_W tꩧNtv] .3B\" 퉅[lԧ>zlݺ_r|0>؞Xuf]t 7P]]tݺu&Mķ&Ն'}ݮK  @hb!DB{bᗾ%K$m$<1c ;M[=z[o-**ׯUW]U[[_PVV6tA-ZEhټy,XĴ,Z]Gyo2駟n^{ӧ.ۻwo|}}}̙3&j+lذ4X9z+V$}EISof@ZRСCi^{8,^sh3x9{キGy#R*þ}>iJ/>| Y؞ G3k֬'xJ:4رcmgMta߂XH> rX&B$og?k,9r]Zo?|,ܽ{a0wR%T,,))Ylپ}{'IT۷٨;f͚A%}E6=)聃ӧ' m_~<y#~޽{>|8\UU /B\" 퉅[l9{챃nݺ/Oc,X0eʔm۶>|8\r%sΝ8q9rmݖ& 80%T,4hի냳>}zbGl>>m6K/_[fMpeK:6m{SJsI&[f޽/-++ %7 yxFzg ?9sܹ38I}| M,HhO, Yf{7n?=z|ԨQs?_ȑ%KxVUUѣ 6?S8}7xsgΜٿl: ;vHζ'nݺ N F}$}BXKB 4"0KgyqfƂ \b!@. B0|;UUUɓX X&B$GMaa>; .t]"gD,B !|* ׮]D rX&B$:tH`ҥ K  @hb!DܹsE~+D,%b!XQQYY_Js=ӮH @hb!DȼyjkkFwqk9F,%b!Xp- 6--[c"rXKB 4; !6o {B\"9sH!֯_j*rXKB 4;-@W-//p!WD,B !*++Vts}/^<rXKB 4СC==S `wχ***]dyb!@. BLrr$d'M,$L 7Kb!@v BLrr$d'M,$L 7Kb!@v BLrr$d'M,$L 7Kb!@v BLrr$d'M,RǏOslU rX&t˗WWW'`VVVVTT"{D,B TMMرc0q\jUiiicc!XKB 4]tE#FsҥEEEeeevb!@. BSOQFUWW K.|888ء_N srr 8WۯtBaɓ,Y/Z!4j`ԨQ&Ll>lPPPdddnn2;;;::?>>~޽N ج3ꫯVQI/5 }]bbOѣGWTTha! a! xڟ^xeL^N:k-"u:BBO~ҹs>eJiO/^_ \dIhhh^vڕ(~ܳgj/^PfLgMy={E;w뮻իWrLLjϟ_UUoN0Wa_|EllcPPЁ]v _+iI'&] QϜ9ŋM7zcǎ'xB>jwY$/tj@TUUVpp]ΝVU,ܸq~Y.PmeeXCZg}{f͚o>ys+iɖJG-H ԩ{/_8-**b2j^6GX@5Bx-`ZWZeZ)}Ӱ`0|'aW\ٳ?~_XUJ ,l6Ն׮]3 LWpQ ,m",a!Iq'/3{Gr_qŢ"=ՆϿrǐbܹs^|ӧO_~ rit?,Tu͆Ǐ_`8~ٰ\|y8 @ FX`Ś LkѢEscyyO?cǎ/ 6lm޼{9%%Ejƍqit?,Tu͆O2d8z?)I!(09B;$h 7?m}t5?0I!(09B;$h%>ܹs||7|CR. G!mj& Z]/I!(09B;$h$ a!ha! LBBpQj& y!I!(݇j7o$"/$)BBCKKKNb2"y!I!(MPRYYy;L4x` )/|,䢐;\,c티jSKy4IL&SXXXrr2f鑑CEb^sa! a! 9f`0DDDIb2ďb(744T\ĵbDEEqQBBs􈈈J͋BBBCCCEAJ Fϴ;BB£V\),yGvIo2O?|Q0 7o[a! a! Qc744-K}rQBBӦN*),99_6}t /!Z@XzBX@5Bxl0`$tEaܸqc\4jƸ81IƏOoqQ6l'T#,\RL;w(. 7o+4j kkk?5k֬FpB1I/_NWtPk/?qTqQjZ fsjjoˣ:C]6--'n'TfXx鴴 56-;;;==S(|* LXzBX@5 ۶mxCв䚚΢M\30a! a!մرC4NNNn-dfū^>[~襓Ktsn͍a!ha!4ٳg޽t[[ȋW[|唔y TAOg6PM;afKOO?c.++k>a!}zCy!* ܚ9B&Bi',4UUUt,6---5<,z(/R[s#GXDX@5 YYYtDk׮uV yy",D^ȥ ;Fj M&ɓ'y:_ϟ6 TAg6PM#aaff&ך5kZOXSaa\3pkn @ wg4U;rrr U5i nQ7}[#mvg턅+4U_쉉:mzRk p hsT@aaw?x׈a4믿߿C -nCBOrzkgw^>z?9!ӿnr>iU^JҥKhhԩS[ȣ(֜[s#GXDX@5B%^:uoARRR^uxǟ,@ǫWK;ˍ'FH [P2yX8;>׽ݷn_p~_$}w7 ghM ~~~bCܿ`k =YVyׯ秦СC:}Սa!ha!_~ڵkjkkΝҫW/ ~֟:u*)))(({'Ot钴^0{l>22rݺuJOilݺuÇ?znɒ% 1cFuuXuIp??b|規H>rɃ>;ԠtrJG#Q`ƍACl䰰0;1{IʋKeQ;t8\実Sfwt,Z0.. 8 w4ߊt0==h/JaC ޷?[^{5E3JJp}Y)WWֺG;O3 KPX?oxٛ':j +..n۶m.ND.Nb7YN.*b}hhhݻSSSE}9paaCa!t=baѢE> f˯̙3 ./I SRRF!7gϞxg͛'VΙ3gÆ baݺuꫯ8̦юʅ=z\~ݡ(ҎD'JaK9d֬Y{Iϟ?ܸqw3v*+uXuNX`\,柆RռU:T׉_{_.v( v߾ػ}!!=ł_=:u|zQk룣{OY\aOs7kFH i,Yt^~Q84guXXo???{<z@wwѼ(h瞛Ok-͌O ۱3w lXbaAA=P4`k?Lu۩=l7NŚ^kvvԛ*qb{~%.j=qĴi\\*On^IcUUU6m֭ػ)Nj2TN+)S4A2JG#QܹsN(**gJJŁ8qBZ nJiJ-w)u]S:{-7 5yڰ~xԴ:XtX_yx=؈e~#OH?̛HSSIP*ΓJp?D|{8O?N,5M•=geZ}lNxՕ#+oUTt'.擅!!=Ec׈{O] iߐ̬cx?m7NϘԛoRQy7TW],_fo]h3,>]tqU>}222=8Uڟݼf9mFyy]wE8GX؆6GX@vE|QS׺v*iZeX㴀8QFJtiyo:N:#""ž*++Ě~UN+6oܴWfw$ (}!}UR+˟N;U~[VMrgCǢԙnP*dٲeC>?qA/,9rP *\lrW3aaeQ;a;6ٲ՘XxQ["-.?gw_7Ǿ}CݘZQ;s3,OwYj1iKD_(>(-WX(]8=Lpх3Dz22TFi}gϞ'Mg'"JSko9i(-4j(,:rÇ/wyqQ-, [l)//qFEECcrǎS6-/_|̘1bƍs]mvV~=zBGt.&v|^:y9}Ob?-3ݒfdaEwLdp lјfÆ̃?n喅]vqx*O:u$Ir%rl̘zJh8^9 ?E7oeO=5{cb"8ua7^udӞ tSjd )/wQ,n>z SSS'OW!!!.NDn*U] =w hsT@aŋycǎYVX^lرck3fLo{s5j!!!999gΜIJJ"7B0==]lrѽ&L֯]{V+WjB߿rZҁ7{JGmb1#G=~Ty@ nB-1bDJJJuuٳgEv]^E@Ϟ=,?ԭ~͑{""<|ܹbmZbqZ./7)z-xP.]:˟4,pd{oey;O:kM禸 .}wȝD.S]o]BSaf+((HMM o&N(VkYnn8yTur8s',$,=$Pׯ__xqTTwɒ%6M~2+++<<<((h֬Ygxpw7ψAbf,'%}g6Bŋ_o̘,y]Z߻}w˭.~w ,.v쇩: W=gUYhLyts6q=u:B=-/ ~,p߰TT,Jj_@Mo_4BO6񊏏ywcS^ 턅|2uT忣]v 4H&$$f'"7OYgw9Bu}5+ȑ#K~>;ܴiPǿmܣ=%>ڑi'Ⱦs>'`3Vo~BHHO r0..k.ݘ*5곢l^4J .Z/ZXX^N7Wڣ_7?Cr\^gxQ?i7zQf돥W -@ g??&JxWYݩS5JHXa! Qk%%%ΝKLL|9MYfM333Za.l/BzFj 333 Zzz`fΜYSSñ{",  peeetD~i yBX@5W^x֯_ڵ,,Ka… 'O̙M999ɤIRRR PM#aۂ@G$x[OXKOaw}\\\,^B.]BCCNj/כ}EDD|WŚ.h< .TYYIXpa!մZ,\rc9xln>a!/=W9sCt=z:t^N6SgΘ1ىGXGX@5턅ªUjjjH_y y+,LLL4LJ{8q\[[;wܐ^z񣴾nɒ% 1cFuu~ qqq۶msWmݺuÏ=bG?*цPݻSSSs׍tž~6Piƍ C=J}K/r 6-999,,Lt(bGoSD{w>yK.3f a!}TTXh/^,%:͛7?33xy뵢s՝gϞeeeJaaAA+-'''=> F111ٳ<̼yR['NL6i7iҤꔔ#FޑįfϞ]UUiӦnݺ+!C\7Ri}xaCO8_]ǍW|ǘ1c6oҥ#GCVRR2k֬fwԚvǛ暚˗/޶8#,j RH^h8o|ᇜE; truF#"",|Hi966ɓrPԷo_iO>EEE.RyGw:+//_뮻\7Ri}[.?wzĉҲ͋DYiGi* xڵ]GX@5s'/\x1G h޽{w)>' UœoZe ~:ѩS'???믿4iRppptt={\n?*HmgHm6s_E8m}ΎԶS܆5*00P,S!OZj-{͛?:lF1//HК+W]d2qq; /^Rjjɓ]?SXXT}}}nnnHHaҎkYXHj%/8lTұf,vd[N1([l)//qFEE}1a!ZFOvvvii{LRQUZZ͛/^~8uԺu NXz! 2AAA|~[*QF͟?_Z.?~F\'L 8qjˮAҎz)ҲPJK^pP~a; ǎ4,\lȑ#Ϝ9N+le;CBBrrrjkk싉锑AXpa!ZFOee?sG=&2p6lիWoߞy;$:.d2_7б_pANOS9rD.iZ̙ e]]]FFFlll@@СCw-ߵkנAʄ~X#أG9YaR#֫ݗడRjB /b```xxի~)z- n+le;bbbD3Ćz1t ,j-~g鑑C{Lo߾3f̠WT~%x;ah"F}G fDJIIqyvF6Po}L޽{[,(PPP@)Ba!:^{ܹsz>5kִ5j-~1!!!>>>44wRR8tPN233t#==`0w}3gά!,=$ ֚7zF/+44ߧKa!.''lД'Nٳ5jkʛ!,Zk詬8p`xx544Хp@Gg֯_O:M[37m$K,=$s 7zO>yd),1c %t -- !d+gСC5:..!,Z+1cǎ+,((?TVVnܸ k׮SN>0!!A__y BF=Cҙp>lݺU\:˗/_S(:t:_;dȐz(..6GX@5Bxܺuk֭˖-˫&ǂ甕m۶mɒ%j;>}bkl,&sjjC88B a!ha!|3,lhhXbŮ]H?&)55",p6PK.%''*Al6ڵkM&8DX ,m", ,lI~/8 B a!ha!|-,\xqUU1|Dvv6_a@ u0MToݿ?|ҥKo߾͹`8 @ SaҥKk}]u4PAXDX@5 +++?C#˗s):B&BNXm۶2r#{Z,NwPAXDX@5 jkk7nv`8 @ FX sm[hajYFO>}k!C]Jvد٭Z4PAXDX@5¦yw}y핱 ݱvZNwPAXDX@5By̕+W庺%K/00pƌz͖庰ÑqMڶqFb/OHH-۶mhWpp/|5{Ν+߫W/ ~oݺuÇ=zE6>**ɓb… brĎ64ۺ>44TݻSSSs.t/Msԩ$m'O|%iٳgbJ[uy%,Q`8 @ FX4,,//?>(h4Ϟ=[QQ3̛7OZtґ#G̚5uax`Қ} 2&m8qD lZO81m4yѣG_C,,ZHZ,G,X 4iRaaauuuJJʈ#\Tl̙an:w}WZ~W] 61.`UUU6m֭+"-Kдrׅ],>>l6\|YT2sLi… ;_J( u0MT#,l8Bi}lllPZZڷo_O)ž̩SJkz)yDMvܹfҧO"m-|رHi9""B^/D^/ʗH555N+o{쑒qƽO>X`2\NNXb˥G*..z2 Ҳmw}y7%,Q`8 @ FX>?ûvtSN~~~_ijuDTyee=s ~tC/u_=iҤ={ Mo^Vlkjj"""5 >m]f EYF.ТΗR*f: B a!ha! fBϟ rX2BrӦM[r+~iכH\oR__+/o{qibd];TN˗3F,?cbyܸqKزafMB84F-[oܸQQQ!o.˃r1N˻Y-a!PAXDX@5Bhʔ)җ=555&L Xlȑ#Ϝ9cJӿ 6q6M&N7X\1(cǎo̘1w5j:kv}ժUbyʕb9##á={ڰafMB84&$$$''V̓$ydyPD=znVKX ,p6PP)4ڻw< 222bcc{nz- Jr?VTimSd׮] +fmVVVxxxPPЬYjkkO3gN;ĂJAiӧE%Gˇ=zmv$,TڋCcbbbv&(KbD"""i( u0MT#,=w$a!M!,p6P ## ,p6PwtB&Ͷ~zNwPAXDX@5 7lP]]Mn$<v`8 @ ;aa~~~^^|ЪUn߾v`8 @ ;a|rr#իWs):B&BTXm۶gϒ{.]\@S u0MTGeee֭D@k u0MTP8p޽{ɐ ,X B a!ha!|-,y3gΐ$A_Uh"Y`8 @ aaccƍ:D H haj>J{ׯ,AOl6ۿ˿ddd444p~Y:B&BlX(TVV._|Æ EEE=9:sέ_>--… hajJl6[NNNc$Lsp6Pf6o. w:B&Bp;} a!ha! +VdΝt;} a!ha! O>YfjYp$˗//us ,m",avԝ<ԡC֮]VVV|j8 @ FXONKK;xMt@6-;;;== 9p6Pm۶> r芊kjj/ a!ha! ׎;Cuuurr2w0MT#,lG{ٻw/Ξ/LOOgnwB&B͖N?eeepAAXDX@5b4f1;}a!ha! ECCCVV^]֭[sAAXDX@5va2N|ѣG]Tf]촰Rl6[rrrXX8.&,AAXDX@5vNX|Μ9fsMM˗_y啙3g<2dX0gϞxg͛4B8qbqq&b?czĉiӦێ=baѢEdy>`I SRRFr7ftґ#G̚5Eo@+q砃 ,m",aap',,--}饗 Cݟ~i9ϳWUU% ̩SJ+z)ؓ'Oʵiv9GM铑QTT䰭b;)-GDD=*KJJ嚚x5;-Լ($6mBh%t0MT#,ly9RQ ֨QrGΝ{bA(VtSN~~~_.6'Mgy[*-iY,8]?:ƻJٯ9mBh%t0MT#,lBʕ+ݺu###lR^^~ƍ 96mʕ+WXOKkbbb ]zޤ>777$$DҲbqBv\UUJ͋ndRow>>MT#,lF*++/]?ϥ!!!999gΜIJJS}_ѣG?~F  68q7|cZssspێ;V1c_o_ (b#=jPy˖-9rU,TmBLJjp߾}w}wϞ=Ο?/ˋڵkXXXFFZCXѺ:Q 666 `Сwn6,Td׮] +fmVVVxxxPPЬYjkk3gN;ĂٞJU5^UXԼׯ[A׻mBLJjBǐjS/#,=MT#,l˸sp6P]/AAXDX@5va!ha! ۅw6|򯭪 ;}a!ha! ۅB7EXAAXDX@5vAXHX; a!ha! ۅ;afKNN ʒV:u*)))(({'Ot钋bdn:`Ç=zTZ_WWdɒ~Θ1ޯ]6{lu!Ďvy/؋OHH폋۶m',-AAXDX@5vNXtґ#G̚5KZo6kjj._+̜9Ea1&M*,,NII1bh4&&&={g7o^ӽ/\pxGP (&W+-Z亼Zj=qĴio?a!hw:B&B w¨(Y@ bdKJJ嚚i966ɓriii߾}ViXeQӏou(&W{1铑QTTd_q砃 ,m",aap',Z+H5*00PΝ;(?tSN~~~_{ J pؗbՊ._O4)888::zϞ=; a!ha! ۅ;aatttӇ###lR^^~ƍ 9sZX),),,twǏˏ*5ϝvZ"?YT^R__~ @˸sp6P].[lȑg̐Z>))I՜V G}1&Ltcǎ\PV={S@Ă\1c,Xĉ՚+',-AAXDX@5vNXxz`0]~2///&&k׮aaar洰RXXWW' :tݻ^ *5h4ѣbb!+++<<\/,..%$)hj8 @ FX }l-[Ѝ\5un{H@#,a!aÆPq2d??f̘ÇoڴVl-N ]&=Y(4hЈ#.d%66 @O FXHIabb>;{lq"kZ[JIaCCTUccl>}zLLȑ# xa! a! };ww(jmZeyCRhRz0,,la! a! M飄N ]$2AÄBmjWn߾飄Nګ2(a! a! C & @O :@`Uh|2"AU[~@)iuuUZVeXRTOX"+[mQE|@,=Ιdd&N͝;όsLB̩*@6 HXQzRd 9 V*@ HX{Rd9 # B ͉XL,h4v &L*b!@s")  hXB eb!@c2ac^ XL,hL&LyKb!@v Ʉ 4yI,Nb!2101/I,R&4&&<% ;@BdǼ$d'HXИL@B$)  hXB eb!@cdz @ډ@B=$i') hXL,ICvb!2&y @ډ@B=$i') hXL,ICvb!2&y @ډ@B=$i') hXL,ICvb!tB=dXB@c !H$C9b!2 =I, sB ezXx @戅@,4& YhLb!=$#)ИB{H2G,Rf1dXB@c !H$C9b!Bٳ_ "AݣXB eX@D"h6Bl#)ۿ4H$bf#x'4@V , sB&9b!@V  dXB2G,jb!#d5  I{gϞ<N$B"hB^8\D $/4X!$!4\Hs ͞=[,  КxJ<,#dX!xpLsL3.L@B*kտ+ ]/ nw'BN@cazCFb!$31#@$ ?bSO߿M6z5\gϞ6m4r1bG}Tm&mܹs-^tWN8k׮{Ç*O>[ngX951#@#uY??W_}UW]U>}/_׿ucǎ=ztXrepܲe%\2eʔ$ʃҥK0 $??m-%BNM@~ga[oŮk׮O>8psU[;Z;Sэ/x͚5|gq%K 4 !&F~Bz|>W۶mws)D+[m&mo[D7:tpZl٢E;^<}۷vƎ/7g_,d&F~BzD ǍcǎÇ駵\ƫ'Sg}ӪU?08Oܿ;c' of싅ԏX!X/}O>nX"]O޵kWo;`?ϓyws=>=V3EEEUn£b!$71#@1s9f:xoѳgt«ꫯ+++/_~Yg~׾}{lϞ=w~g\,\ݺu?~ AO_lY՛e)<*Br#P?b!@c[o}ϟ3gNb={SN9M6{ꩧ|_җڵk7dȐ,zɒ%XZZڧO֭[~>hk)<*Br#P?b!5f\ `bL  M w)<*2I,&~4A,lX&F$Bxr $@<sرcs !B,={1T X b!OV/ȳ/BBH Bx. #@Xeb! sBkL,#dX!`M<8p bXX`b4b!52o޼-[.իW"01@s{녱XӧE,&kzXXVV_\\lpi$@X5HW^[l 6򂍵k01@sÇt钟߫WH$׷oߡC01@zsИ1c"HAAAs_|  Bk9OSO +++  Bk⹩Xx%01@ډsӯ~N:E"}]րN,&>ܥKH$2tP&FBxD,X`( @xeeo~oڴiSswD&O?xSO= B\['OuTыٳgOCXm0^(@Z{M?iUt4\#KKKr2Y/KIII_{ SBB, -ZԳg?>'hK/q u_ {FȖMqbaXXXd/  -BJx;v,\0^reㄨ &|M ~d˖-߼c3R}}e˖(Y-Zt҉_Yw1f5iԤq>: b!X!pĈ3gΌ$Yƚo߾'mk0gΜ-Z|[n}]pc>s~`㣏>=ztvN?Yf%sGc|aÆsI']y啱=4sѣG֭?UVU9`Ŋ:u6mZۉݻwx;v7_{oS%%ssbF#@x,<b~B.S{s:3x-[N;6ln[sxr>ãۧ~ի۫VJK;vK2n޼9s֭[t)xwD BII/T?X;^{-vu†T׷=dV|R{n0>}z~yoڱci r$rJJ7db~* ]t㷮۷zC|'ؘtmB,Ž;ޜZn}Zl٢E79IDAT7|G}4SXXs*;lذVZڵ+l$sG5a$m6IDz|`\qLjOzGccԴGTI{9_x~u0,v [~ ӹs$XP~X2O^э=v]|~pûn?ݺ勅mBYX;  `ȷyXs*,,\~}k{s=UܳgO6mc,|cxK|`p;=жm> {ョ]Nj9,X8`ws'a.vw^|\W5s_x ~xg7}\쾆-~g%|E,l#@1W\qEٳgr-w_>@㪥\;Vlrb3ݻwsΤ:caE'ž}{hcMtS%3'm{Rj,\~Y~|{< Jb=ٖf{~rob6m$xO?}7RI2M, cy睓O>yӦMiIS|g{tፅ%%%~(~k!U,"@R-[9sDXo߾￿ޯ=P,B&>k֬ zXb?_מX(b!@^7/^{QSL9tP_{baib!@^~]w^jSNmkO, Bٯϛ7oݺu)ynڐX(b!@_OL~ٳg7'X! k}zee9b!X!kk۷og͚~ze&M{ ى2D,5{Θ1cH$=x@2^ L b!5\D  CBkL,#dX!`M<`b  2L9b!5\&2G,&B01@戅sX&FBxN9p@*oh(!Bk9e޼y[l]W.++3D`bt  ۷; cgӧOee!#X!`M<3 ,??H,&kJKK#H^llk׮58`b4  Çw%??W^H$//o߾C52`b  1cD" `&FH/Bxھ}{OvZXXXYYiX%@XM .؀N,&JKK;uDvZ&FH;Bxn:|p.]"СC Bk9k̘1HdL b!@+++w}ӦMJ?>L1Չ)WVVNK/gbj'@$&rʥK0 &M:t-#P'B 5' 07n~frI,sM|۶m<!щHX!P?\^^@,]tM@2B:ħN*@ٳ_"$C,sM?iIiii^^^0} n5]ԣh$/KII-s# B}]vٺu벭}Znݽ{Gy?=D0l\%۹s#?ժUg/8$,Ldj|V?`Ĉg!|;խ[>dU|aÆkN+nZp]N LT&`LF駟>k֬Dl틅M51  ~_~7n^,.. 7;#;8pڵk7o| 7DwN2eРA֭{;6?Wڵkך5kֲ3Qά%ğmîWTTL0!g[hEy)0`̘1ov?.]tu]{IԘj|Bs^y晝;w~?яo}Ȑ!?o}+v)`R+UDw4nܸ5Xؐ jb!@; ۵kw饗]6Æ _zUN?v׮]c93x-[N;Wӧ׬D; ϶a7onܹuֵg[5C1/t25>믿>//邏NU'pBÎOj*[޾}{Vر#8ڏFo͚5`c' &XT&yZS }&F b!@z i_תU]vEXӺuc?ײe-ZFKW\qEuߵG,?ڿ0v1yVx!T*//;vda?=5>9xm6ڛc-3:{$_ƉӚR,l틅M51  2 D]nݪpq-رc;IVv޼ysS%DYK˩哅 Z-'?9M|g#\7Y۰aCV蜃Cm۶?~|>YXL0Wf@^k_7k5¦d oؗ~wuy6mZp믿s //_޵kעE"H-;I~&LPQQnݺf5:L-*yrCcaட|۷oݺ d.\cǎ`<+WVy}{߫a&:;޽;8~ذau>`bYxEؓm$2NtGDص|5¦d w5f̘S?l={=///̞=;s߾}ӧO?3[n}9׿޽{;3Բ3љZ޽ܹs5)SiӦvDx:w|1QFm۶K.+3b=^0 h_}v GV޾XT# B᱐P裏O޿f^})fdb^dbapgKa 7t͛7l0hРoY,LHX!PԩS6ǴiN9뮻nΝbaw271  \5kVEEF!bŊHX!PoxbBdʔ)2enb!@Թ&R_ ,v=uT[F'F b!@2k[nnݺX!̚~:e _yyٳl01u  \gʔfƍm61  _~m= 'Lk.ZcN@-B> d_7nR#P B 5sΙ3g d{Ο?镕&X߾}w={ 6H5T֯_?k֬I&{&Bzݻtƌ H$bB*O/0$U#O,&"A#dX!`M<`b  2L9b!5\&2G,&B01@戅sX&FBx. #@Xeb! sBkL,#dX!`M<`b  kg~D TLb!4X!H$b H,ؿH$b `zh B2G,jb!#d5 X@戅YM, sB&9b!@v9p@*o!B2o޼-[.իW"E,.۷o?묳b0 -ZԧOJC@Ygݻwh,|N;b@Y'D"={ܲeKcu)Xv Bsႂ={F"N:}_:t BlϑH+_JsC Y`a Bl}h)ڵkaaaeea B,5`h,ˊ i'dN:E"}]րvb!@:|p.]"СC d1cD"  2A,:֭9s^;hРH$r%5(//78XE^{#G> Ґ!C6m8pcE]4s˗/H7)UGiӦK/0P?b!@Sڵku bWTT_MgϞ]ݻwo߾C _^^R"4.?C+?~|+V\{ ҭ޺pwݻwOT {ѧOw}7vFG,hJ˖-nz)ݻСCn:@MСCEEEk֬9xW_ݫWX&,,,K;93g9r$vO>9rHc@Ml0?IҡCzoV(**z &4oyȑQFM:b!@;xȑ#Ǐ_=z1c9j!d|Ç/_>x'*4XEʊڅ :tqӦM9r䥗^|r@ZСC˖-[ܳgϮ]{Ȑ!Ǐ_bFb!(rX9J,%@  GBQb! |ku%vIENDB`onedrive-2.5.5/docs/puml/main_activity_flows.puml000066400000000000000000000055341476564400300222340ustar00rootroot00000000000000@startuml start :Validate access and existing access token\nRefresh if needed; :Query /delta API; note right: Query Microsoft OneDrive /delta API :Receive JSON responses; :Process JSON Responses; partition "Process /delta JSON Responses" { while (for each JSON response) is (yes) :Determine if JSON is 'root'\nor 'deleted' item; if ('root' or 'deleted') then (yes) :Process 'root' or 'deleted' items; if ('root' object) then (yes) :Process 'root' JSON; else (no) if (Is 'deleted' object in sync) then (yes) :Process deletion of local item; else (no) :Rename local file as it is not in sync; note right: Deletion event conflict handling\nLocal data loss prevention endif endif else (no) :Evaluate against 'Client Side Filtering' rules; if (unwanted) then (yes) :Discard JSON; else (no) :Process JSON (create dir/download file); if (Is the 'JSON' item in the local cache) then (yes) :Process JSON as a potentially changed local item; note left: Run 'applyPotentiallyChangedItem' function else (no) :Process JSON as potentially new local item; note right: Run 'applyPotentiallyNewLocalItem' function endif :Process objects in download queue; :Download File; note left: Download file from Microsoft OneDrive (Multi Threaded Download) :Save in local database cache; endif endif endwhile } partition "Perform data integrity check based on local cache database" { :Process local cache database\nto check local data integrity and for differences; if (difference found) then (yes) :Upload file/folder change including deletion; note right: Upload local change to Microsoft OneDrive :Receive response with item metadata; :Save response to local cache database; else (no) endif } partition "Local Filesystem Scanning" { :Scan local filesystem\nfor new files/folders; while (for each new item) is (yes) :Check item against 'Client Side Filtering' rules; if (item passes filtering) then (yes) :Upload new file/folder change including deletion; note right: Upload to Microsoft OneDrive :Receive response with item metadata; :Save response in local\ncache database; else (no) :Discard item\n(Does not meet filtering criteria); endif endwhile } partition "Final True-Up" { :Query /delta link for true-up; note right: Final Data True-Up :Process further online JSON changes if required; } stop @endumlonedrive-2.5.5/docs/puml/onedrive_linux_authentication.png000066400000000000000000002703121476564400300241200ustar00rootroot00000000000000PNG  IHDRl]5*tEXtcopyleftGenerated by https://plantuml.comviTXtplantumlxVKo0 W,`-aak] :ڌ#TW,$.sqk#L U<4~Hz栊 7p'H)u&TkQh%VQsL,Oage0<d&up"׊k04pYm gڈXv ^'.&ޣ ":IDATx^ \U\.*/Iw]TBWyilMۦ?SLQJ\J4ֻkBi bZxlRTT3JI / l~:si^bW}w!!mڬvU볇`Isc5k|„*pFg:tnS{y$<<ԩKҽ֭ی5VZA:|4٠Av49Ԣ} ܙK%Kd4o"3s{SS{ sﬓ}~Z)/)|?GEŖYwmvK69'KJzEIJ+HǁriR˗f5IX.^TEgB` jQȑx!L<$&x7X =K/Y懫#G^^ W HZHQͭ[Q;R^" JH('D?pW&T^^^}||֮'keE!GLpAeb 8qDEgB` jWed|{ I) uBNa5?{gLJIk.[JN@bsn$iҖjkB析:$=~}-IhЇJ;ǫ BNϾ+'[tAp- v?FE5/wTq- K.=z\PPۭ/mV\Jad͚-;v|/foVӦwRZAZBC vI@zyyefVZ49`,eee))sRg͘1{;wCj!0+h}? G(T2F+-m%mTwJp|D.ZÆ> }V~䩧Q >21q@r򜈈H&մ[tAc={#VyefYr%T9)u!WXXrŏ7Zo; ѣDž6^4jBuϘvv;v:Ln:,'0=)<cQkI?xmN]~@MY+>=BH2'uV8'_}m|^^^mڴ?y^GEg0iޒٳeg!0.r!G2oʔi͚5$ o), aN`Μ<+kOϞ- wuplRTT\^}~:si^b7 䋎nX$-/<51qv(c#wLm11m)J7/ 09)ڤGþ$ pٲUݗ@ ^.܆i:tܠwN]ro$iZ˫Axx /v%{izuS&Ӥ6iNsV&vہ 3uHSI^%1o/duɍ1Tg׮< |ѧ4UG m̷ Xڇѐ!904v-Pxd%tLzK35tl4|ڴYنX"؃O,(Zc5?ݐ$oszr3{woiihpD79dH2p j ?},al݆*"|i/}]>н{oumz[nC3={ҬYIIK֫{!((85wٰ!<Qf:*\j=:tNO= sxΜ4yEv-'琻ԩV?rо}G09ٱc?ͫo2;xR^S520RƮ6ٹ3J,h޼Efz\/G]yttFVߪs%R;t贇]BVxf'1SGp'6׿!:y|C$ B` 6Ѱ=zZ+nlU:=Ri%͢* ///4>>>k\F{iB:z,MJҭrKOϏg#G^^ h7̏b?B?6dz&l>/$0a>?%ĉ^S20RƯG"1q!T ï><@3ܺsiΑHmݺuϞR_% MRg!?[n鑢"=!]Y"DÆ>Tr9[^>lK^}CCHPhg?pٲUT"e~ Fm-[&ݗ>}l*3{NhDg_q3+mW20RƯ"""w>Ҡ7I yҸ^x~i9;w :txZ2Gyuܾ})KᠤB;inNӸGXtשS/'!LBm O>LNY`ZZ׿ĐL&=js/ҽֳ hJDV( gffjHJuDg͚-4!?jeշX,=ڵ}}LaGZё:4d(.Y^^^4v5_!C ,7=4~-PlGE5 yѸ^uتv(,,]4sqAAp~'6^/뿟kniih/Ѯ/OYHj<=4ajΟC̡};&$SMOf0ZrӦͬ?vK, ,U( ///QNsf):xv-_::%eӂ^z=8uؗ<-<@VhDnɛ{?yvzǑQE.]I~{g[c$4hݺMblA`N6mXxnӞc=j9Mz-9Ph5"ugx;;[}xx uBg)f!}1]tΝ{]ÆݝD#xf˖)QMmЇTSONӾGȝHMc]Cf73K %8Ljs ,~U|_o4/ٳ m~?5i매o׮}~~~]t3g[y۔)hJATWh/՗k^LJz\ψN 0ٷȑO}=4bᮻǎ}ͽfttnw~\Ak4diϱ OX1{PbFr!v偬=={X,TNBnBgɳ`T'sO;J&qRearC2L=1m,r06g^*ANծi'LnC'}BK4 5| 1s0ĴO@ȩ~-m9o֬7i9 s/PhW^!OGW=h[Mc5w;?_e:tfXYʹF|a3K/dkMFQVs1# EMYMtB50v :fihc19cO6nܔ㟗;I{@@`(=zV^[A}ogܳ/22*44gYl4(&}1f͚wqjK^A~B9R5ZN;edgVբNt|ִ&flr֭c4zOSUɿU6g <…,*;pŒ_ ѐ1)O?=W޴+*=N\1c ̡@iFLB^}U =NNNN}w!!aPSTk9'p!ˣT)މ"#K>Ռ 0NFK=X&b* VDP ՜U&!Y 験~f'XL'I,>v\f'Lx3DI4ԩkv^^999UIهGkN]%b_ҺuJ })es}LKJ|a٠AvRⲍni߄~9;U@Aj; d׮ٹ3vUI>ԩ-MTi镥q,gKpOOϦMçM 5W\EDۃưGOH2ԆnWkE>|]fD1>92ʉ<*ռitFͩ5qjn.b%VgYNDd%tD/8MF) pٲU4ɱʆn8MiKX``А!OO>$"E>Mgx7}yJM'68ˊQ?SbԺѐ1L8jhQ''̴OKB\Z}Vm9D5.Z|5IqFv-rc?4ҾI5k 6}>zа-3'9 (1"iCaRU\¾l#Jt]Oڴi#ܜyF1'=z.uD9;rtu6͝քI(2^BM*휭W{!((85wٰ!R;]h^}:U#TyPoU%4E>zvGR# 6\vX+r~-2')ρ<8ѐGM3;osjMk[/dyL=w"rհͰdɒ :љٹuw+ߧ +}ҖQh,jxȇn8S@%n ٢E+T<A yβZշ*Dnj]hHXk `G1t ?7m,rtUP 4pT^PPM4^^^}||2{-\app#x^T nwG.<X,'NhMIox-T8rJ  VieI*5ʏHUa_WO?=IEG I͙ is;(0vKxhѣgh ^'>*kgEs>Z]z3_q4c_X4nTsokΥI@^ǃGO{$SԆK_WkĂLv>QO!j=BTHMћ`X%9qRjgG-7 ;# aS 'fl=z߀* a($+OO=`5eÖ.OOuj1 U= k Ցwӡ67v ?7m,*rsOMlk,;tÆ>Tdyݻ8*݅2?{4!O/-hM/]9 {Fl**9y,+aTr );xxxPRsjŜ@7K ]i#&5!'OЮP3/|GݺBo8iu- n-4c`ܹ UaHj2 Wx^>s7b|JG@ZBCxGC )zL6ݴ=NShv!{',"". +m-SoIA1c2<==y41×:7|/foVӦn S hO^^^?f[VVj;WaYф zLaWG$DZQZNC*Uv^___Ҙ ΥK49 Q ]%q',UG<ʽiAҥG 0aG!/A`FOmT&5rW嫆gڵ[%}ߏNh"ܛ`ۜvӊ8O斛څl읈jcjq+{=+@* a('+kūǞ\wj /5횊CrogYG!/Lz3&ބW;Fi}5}:䄟6s 9KR5 }VSϨUJ8 9yNDD$;s-JwP!\mHn¥~!{V~1Ap%ߪxDM:/''^QСæO9R5bN`|缄GF_,Z7iėyO#7UIą{8PҮ/? yZA`FOmT&5rWӦ2^x|8E͸7KosM[qRjgǮ[fjw"HVXxqtʶm־}Gw\ a('+kp'Ԝ?4 /5횊CrogN0iHϘXk `F1 ~[͝:9g}tB.;{oF!>?kԩkjZ&bpp# C$)3 a_#h5R53s=I6mڎ<ѢԨ0Hük~@\cH2]th&FxLo/o7oɍ^툤:xv-_::%~"#h.} j@kW#G>MiaӦyh&EWY[RpV)Jk}bx_:^YjBݘ1o) vS/U%-xjF*YmU妝Os{MOώNi<*y]6ѴU'rS *\M(zO,NgJᭉBg"ۻ' oRэR8-[/[̝JNQAˤ':dpc4Z3>b:!G7)n(VHGE?L}@jrv4ҥ9 *>@B'@í.n?Zb'D޴+yCBjεa?5iWVXx1)镊{Cܕc4HzFg(ه VjF"uH5e4yV^*Q 4҈݂~z<]__~{d4SqbM)oPҼ_W_}CћUXbPy׮v}yߘӂ]6zXu2ZWU'5|_Z|}wL ˋnXG>ډ ixvwSY՛[nw"VֿC{/*xnBgbF .diz pm KNN{i\&kjeW/3yU  0J]tV''V{'X(`0K4j9Bl߾[Iڛgi￿u6YܴZ|t!bF{(sSr}BK49嗧\ölzvݵ+oΜ~~~ҿ ֡C紴ernD#>a: rZŌt( IVrf k69 3/PhnԩSWyyyhъ&FjoZ`̴ fH \ MD ͙̅ i`k b!`ihc9R5 rvM; >arC2L=1m,r06g^*ANծi'LnC'}BK4 5| 1s0ĴOԚ6-`̙2g^*ANբ'O`C'fDK Y9b? >dڵk. rn>A|C2L=1'XjM]|e֬Yӧ9jɳv!Fpe`ez &5 !Csք7|# 9 ҀC ``8rř?:9 ҀC ``8r2p4A0yA" 8p !g^ cH@ș 2p4A0rb@!.y+ cH@ș|d i!0`09p8Dp  Bμ A8Á3/w~1s@@p ^ 8Dp  B\ 9`^/~7n' <ÇJND֚9s&YlAMséFp9C*3\2f̘;]S^^ݹs={d(n޼): ̋+===֫W =zYsסC _UQ5QyEEEFFF;$i?!\h&eӦMbM_xQW^ݶm[ԭ[<˅  d|6mx{{޽B2ө0--<[,:LV\\,?"iW5U$D1c`84A0yQHTW\t驧>6lHoҤI<11ȵkJJJbcc 9E?N _|-Ӧ4_#GB>}HBGq7n5GlQNL>?+-++QFןʝ(>hT8Ō ҀC ``8rE1ܙl X,}9=??nڴ),,ѣGYU4T9gjj+W&yyymݺӃI_d{i "cfQfffxx8 Ҿ?~/ڍ; Q" 8p !g^Ý۷o޽;44tڴilӁ*4#00ǧ<77ʯ]ƪh&*WCu֬Y@2>>>;;[D`ƌnݲUG~.]j-GDS>_xb֭ϟVmרQ#:F&jNj=(f 8Á3/.ȆtRGϟ咒qԞBii)+߰a+WùqIC}>sݴ///CojI8+WJ)~޽{O0mU>`\b۷oCB磘10Dp  Bμ(* mڴyGq%[~۶m+OHH6l/\ǔ9 JNNuj DOaa!-""6w&M*_;rkHƍ=<oB(r( \z\BWBμP~1s@@p > 2p4A0ycJ"kgX4-F?r910Dp  Bμ{^^^dd͛7yɾ} TZZZ}b׃bb%}TY9vٳo```N>3(** )++7ZXi!0`09H#/ZXi!0`09J۷4im6^ҡCbJW3&$$$((hׯ_5W^ݶm[__nݺ]rE ]|y6mI)6J;w?SX_g'FIm!Cs?¼y󢢢Cddvr9x u%qO??N u{ͥ .PIIILв?hדlSܹs~ M%c`84A0aZI b@w7Bnذa.++#Я_I&#F|7FգGMGÇV~gJDbb"k׮r!X_g'ׯ?yBVG}TZԩS_P!ERyPUƍy?Mt{>s -8qN-Ps:u4e[eӏ?8W^y9Q=44eǎ'r2C[r9*oڴM}8;$$U@Bμ:vX#;;; 55իz̙SnٲE.hG>Az B5b!c]^SQȰaî_~…8֨Z} h}Eo߾>PXXHo޼Y^^>w&M*Ӥ؋pW\5jo[iСt LwueɶJÇv+饗CV!1`)9 10Dp  Bμ}GQQQߑ#%=iҤ/* qƅY,yd҅k$Z0aŋysvy5FmڏuvRZ-%%q$666--MrIvڕpqմʖ9?c=AmԨѠAJI{}9cTPSA4j%ȑ#)Yɭ9PQ " 8p !g^4}رx SC]Ebccߘ,1N!AJJ om۶:th=d<!r0裏L8Q#@ș `,^x" 8p !g^Vȵk#8d i!0`09pddd(>@8Á3/w~1s@@p d i!0`09WBA8ÁB\ 9p1 ŀ3/x%d i!0`09b߿ƍB޽{~W`/}C8yK`64A0yBAAA?'JQ591sLkٲer5} TZZ*VRj#6UTDp  BμT!ܥ9rd׮]:7oJLDyy9Ν;S&@!W壮Ndgg[,{VLyf UyPB ҀC ``8r .Ã/_M6$.\0dȐФ$sRSSIbqoSnϝ;׿??:,XˉW3&$$$((hׯ_~lڴbŋn-ZW^ݶm[ԭ[<G{n tӧO´4L_rẺFB[o(U{A *H@ș**$Fˇ^VV?SI~H-\vSNSLU ա^B>}HBE!GnGq7n5GlG-xcdee5j~'O*9|Gŧ*vC Ej>>gƌnݢ+WPKdrj^QW/^غuUb͚5qqq5[,L]UsPlgϞBi!Cͭ9!W);+WOHDEE9rawB%%%p9V̉wJhw 60퓋"FR;l͛{0aD?sŊO l߾]5*9[;rf mꧦ:#ru BVհaî_~….]Lڵ۵k P/" 8p !g^^ZZ/n:""UVPq|XDH3gRe˖7ח#!֫D`3H@șpJ+W3cǎb%'bWꔗGGGwܹgϞڃu_7܉ݣ<(f 8Á3/.Hӓoz*IѣG_~;w~~~:tX`0+Q3o޼(__H)I飭EjR6mD>~!^O??I&{U+B9r!w…!C&%%ݼyWcCעET*^m۶ԁnݺ.?5F` 4A0yQ wtSO=%}5lذׯߤIXybb"k׮r~N:E ZM{i<"?#G>}@ԟ޽{9sO8aZ wFP#|:u4e^n֭k׮e*17n5jT=jŌ ҀC ``8rEP, -DGG;wm*--uwwg6m byQVj*m!󞞞W\a^jBE*ӃgZhH++:BE!' Qff&6^m…wf[պ*;vбebpi!0`09 ۷o 6mDiVviRl` ͋"_ϝ;@ aZiLoiGI }! Gq7n5GlS~h}ڵN:M2>|xYY?,xcdee5j~'O*9|GlUQNrhLNAAPPtK$&&X.c FJHz4iDiE!\Ga]~l*jSh/Bje~|H3/L&?/ټ֭[i9===88MjBرBqq1=zgff'N.Ǐ ;/ڍ;)t025BUM6eE4Ԝ|9vIQGG@g',{zz^rEk.S^^.53H@șŏʟ͘1#""֭[LaKdi&rssiڵk<''fjŋ[n-5k5jhX2uU͉C r={ 5r J4Bis\r)zej:::;)]T!!!6ggg36G0aŋզ涊AH'.졊b}J >>>ӅbJJJƍ-KlllZZ>oT.lT$44:=iҤ/T*i<7ƍF=7oz]VvM+ 9E(+IA]^MQ#j+Spy?Zt/ 2ƂH@șWBǎ³>f3<˗/dhd 8ÁSk׮'ٳG իWGEEEFFkɒ%zn!.i9r͚5mժ9p cx@[ QQQoLLLVV^"8Á3/H+@ƍr#Zjծ]=z'r9 ҀC ``8rpסC-[>s;r9 ҀC ``8r`>@8Á3/w cH@ș ())P|'" 8p !.rb@!g^J(@?9 ҀC ``8ri8Dp  Bμ A8Á3/w~1s@@p\^^ĺ¬Yu5 " 9 ҀC `P}VTi 9?nj{O* Gyd+3n8:;vAAA7XO?4::A>>>:6mbر'l< ^^^[ʝ:uݥ&7Hi!0`r˗/'SO|I|?)ƏVW :2{ZycD0luʕ+߅ Jey֭[i}AA-hpRe$|M_uYYYtҨaÆ]v:W-Go)՚j*&0lի>>>,ÖUBʍ |I*N:E?}reɿ+-+Vd[*~)uB*++???i9[ܨ*Iu8_ s@@:8C=Q -ZL Seg :i$Zx:B}D/+L:UZ6)WBBBB*$_pp0/gu*pSr?  r+W&/HCup]HDo6Jߋݭ7woQq6EEEgϦ:B}bɒ%T2dȐ\ãYf컗RؾNԜ9sHLVw6u҅>vJ̍3\Vh-[3&&&)E!׼ysZ}Ycߑ*/WիlٲǻURR߰}9wrfff&M,ɓ:ӟ>vrƍSh+ ...##VϏ}v r$TڷoOR̔[b8OOOV~jP}g0:p:|66q] Qq6S2!:.Rqɓ'mN_ -(( nZ^HCu7n:þa3H+@?9 ҀC `P(\:lw# cHCupX͚5vHJJ6=;w{ A8ఐS8 ;@?9 ҀC `Puy+ cHCuPer`^uE]!EQA@}g7n7K_~Eܠ@M ^ G1cWKFڵ%?u W5gΜI[-[K۷oРA~~~III19 # 50` Bμ#hg &HH)l7oE"g=oY 3Z 9ET^^N¬s={UmXUVVFEEESLټy[E 4Ў40` Bμ c˗/oӦݻ\9sLbb_,XR+z1cBBBF}udfΜJz-BnӦMvww?t萸K+]zu۶m}}}uQ!?!Cy}AigF̅Ў40` Bμ Bneee?T' uq…=zʇ 6x`C_~&Mo?*7:u*<<_ӧ5kgBZ9r$-Ӈ@)Ν} 17n5j۴~'OG!q1B;@:(29pG;c( 'NHWIB~~>+O^j奥|ӦMaaa!/?1ckƻ*߿?ۋD֭[i9===88XЇāhߟ~I(U6ZTTVwAo|}}mJBNH1B;@:(29WBB?⫹p5V~rI{3uW``Oyy_ ߔ9tлȑ#Hz/""֭[|jeҥBgҾBMxhY&..QFyBԊ\附E ߾}[lܸqaaa%&&f޼yrR7i 9R>C$&N(* 4iŋFJJJƍ󱱱iiiB7;9RgF)))ȸ| PerYbExxX^p۵kWvHٳGVQercT8z#GhԩS;vW+j∥5k,**O>K,Q|@gTzp AQAș|2Ʈ]ZlרQ1c.5\t(ûwiFΨH .3/wٻ;@&!D @KtDL)mST"/j6U+e`P{)ÈWB24 $(ɹɚnYgsNZkί>Ee9!AnرtTB@Bae9O}Z=#裏 `Q 02{Qe:^~e! 4!\Fs/ ?h:ťG=#G!  4!\FMRT-8 ge9_ywz߂p\FfWZZZXX-8p'c.#{sAνx$b <4F1˜rŗW P0(PsAν(w㊁  c.#ȹ\1TB@Bae9+ƒJC@(hC( ^< \1TB@Bae9p/c.#{sAМ>ӘZ[,|WV_xDiYž 7)b%$$_탚O@AN ӧn|S`]RTTG>o1cL<֘3g[?JKK#""oz/W^wĘr#!?M4iРA1uuu䲲]H0 g ?w}}>芊 ?Q˜5 :O>J^)J͌F1˜rŗ_W [ؽ{wuڵk333$8?>...))iܹ*̬XBɓSӞ;wܹs~^~e.]4mڴĄSy5?/S-999/u͛cbb=*ȑ#ƤwmbtO<1tPm\۷O}ӦM 5֭uU/_5kVJJtꫯ;66V&YRRRYYg+WNbuS+ܹs[n킂.] j۷v0_ _Mo_pA6:}GAiWN>w%EmGb1܋r 򲺺ZܻwJ/o.J!c$vߩSz5LѓH0awgق%復^vM+++eZ}fܸqӧO437|-Wo\۸qcvv-"3YɽϞ:~>y,4(hC( ^< A\1|![9!O&MӟԶ;rq9n']t{t $**[H_vOZ1;99uGIΟ?ߡCzKi;vrNq1 rATnf 4!\F1G[SSSVV6p ꮧzܹ ~999zl9ƌwEOcPܶm{O>Dsڷo/ɖ-[d@Tի- 3f' '&L2zFĉJӧ天r)ϼ'7vbge9)TTTc2K;vl\\\bbٳ 7[={V҈VVUU͜93999666+++??_=W &" ܜqaެۗ.]W=//vUZ ꫯ!T2UOq19hی m%gϞ=>z\Fbw222HqR\Fs/ ?C݂ۇ ~w\ `Q 02{%'W } Hqh^ۄP0(PsAν(wS$mB@(hC( ^;O?0a5?ׯwЌ `Q 02{Quuu˖-ܘ1crhFۄP0(PsAνx$]ZZNC4b1-S7$wh 7R{p7c.#{sA˘r#A!  4!\Fs/b <4F1˜rEW P0(PsAν(w㊁  c.#ȹ\1TB@Bae9P㊁  c.#{sA? \z1^ڵ+22Rm[ѥ]RltEBl_pAv?tuײϟ?ߡC+VTVVF#[+))ǎS/׭['M{Wۧz}-s @4F1˜rD #))iɒ%KxJ|||NW0R{RSS]i|fOfOb-]7nɑ:lذ;wFo{ڵ3g 6LcJq-|A%(4ٲe S_2a„pFO<)uuu-[{#Gͭ)++8p… 0y_|E>}ϟ$ rbA  c.#ȵ),!|͘uj̙ɱYYYjٳo ZJ^{zܹsvvvAAAANҎ3kd8gO?W=gl4.X=h [F#K;v웘('VSo… 2%%9ee9p/c.#{sA˘r#A!  4!\Fs/b <4F1˜rtϘ1c5. 111_&y]_暳N;@@4F1˜r\;g r1Q||ѣ=j`TZZ~{G ]jm"::Z/U5gN>}ZjUU@4F1˜r\?O>5(ANŞiӦ:O>1@g@˸~o/Qb  c.#ȹW644l۶M꫽{MHH?~|II)lذA\TTƟ;wܹs~^~e d=wСtDD9ԩSkjjqӦMXn k׮̌{t7--Ma޼\YYsi}G2[Ϟ=oHW开e˖}/p JC@(hC( צ9sF~dt?ӘaF{H^^^EEEmmɓƏ9RNuuuii!CŋL2|pݛ;nܸ*IAryy略'N(î\kSNɰ۷˶t>|Xk\7 'w7`Ea[v]bl\l"OBge6EҎ._lhGEGG{ӧU]"##eDڏ;~m_ANJllldddX^^Q\\^n޼999ٺ5?~ywaŊKr#[uIl^y啴}^_r^={^!22\}GnƍٷrK||^׮]*%''G=l0]V^zߝD5=[n ,Z#fge6EU׮]3r[oU[[+/wءR49oAA$9)}=FjAa/EtRI} rw~z9iիŋUU9/ॗ^@Ȍ ^=:c 8q?COw}*u8!555eee1ɶfff{Ycƌ˻x18H|uI٨_lYJJ9O 8p…z_g=>V开|?%%+(* `Q 02{%NOO_/v566vȐ!uٳg%7ߌ)//4~+̙3YYY{y`ܫXd\\\ttABJOc;v̙8{lu: XӧOh\Μ9#Z)+(* `Q 02{]+** KKKm3fP=?{+.׽b͂JC@(hC( ^~}ٳgM4#* `Q 02{y{ii~={~8h TB@Bae9>~|;{#!rP0(PsA-8Ę1cHqØrGKG8p`˜rVQQzowum&n֬,7 c.#ȵO~ҳgOItd9&ae9~$TߠKMM м@K  c.#ȹ×߿֬Y9hFTB@Bae9nWTT;|{P0(PsAν(w㊁  c.#ȹ\1TB@Bae9P㊁  c.#{sA˘r^\F22{H(q@xPib1ܫmI맟~S[[kha/BbbzΝ>Qm@۾b=4F1˜re,?X~(1G}Q0RzO>˲liꫯ,ç4""bǕ+۷o.]3رc?25-Z$+k>'.gb͎JC@(hC( ^r~ڮ6mZӿwo@\]])X|^|X(Kyn?>]vСCSLIII9qmdsSh|B~+4F1˜re,wۧݻwwA^tI]bbbBBԩS%s;wׯ/m*RE,_秧GGG$Iv;+WTì>sU-999~YfIҐI{Aջv̨(_VV6~$PgےTqMjX+{{kK^#OڰaI\TTϛ1 M0u~SWXqw'Ol߾S<>b|Dꫯ% rJJJ?Ja 6+4F1˜re|$ŋSL>|7n\UU|5jԼyTȑ#xuuuii!CY9<"de[>2Q]Wݿ[}tY۽{Y>~3gTĉ@W\vY~  Xhǒ| K9Yt_ɞ%UTTNKf\Fkempo߾%K.B3ߩSz[,@1Glܸ1''G2aԷ>:|]&Lpw~gANΖlz<%9 3ȩ;r'O6ތݑͦ^~2/ܾ}LL:en[nEvWk׼2ӽc.#ȵ2O9Ưd.}NuGH6U￯}ͣ.]T2l8p;3(zK.sn#+Ҷzi{ ׯ7[uX뮻~_[[&Md};:o둏Xgff.X@3fL^^ŋ=yf۶m=''G>ԔeggO2IBB³>+̙3Æ ӟԍS.O>Q -Na> ˜re|$;111ZUU̙3%dee1gϞlcJOc\իK+((y$ɰA?={lk~VR-Ru-]tIo߾Wرc艉rDuג=^Kre֭Ç%I^1z975@U79w-RU7lDGyo~STGr>\ڵkW}Ȑ!^w2^1fG!  4!\FsVkf  c.#ȹW ;AhZPib1ܫʝ I-tl4F1˜rEW P0(PsAνx$b <4F1˜rҌ3/_nom28m1Z:k?/?7xxC4n%}\ rONLLw1ZG}'gdd}ÇX~~Js9Phc 444l۶ڸy昘GFDD9rD_|y֬Y2^z{A_.]6mZbbbBBԩSkjj iiiUc7 WVV6~s窛 WTTԳgO%6lݻ,`ٲewK 02{ =sPΝ;gm7nܤIdcĈt{ٳ}qWcnnLRUU%iԨQ͓SNɱo.ua_FANfWWW0`ѢEz[v}ӦMjqIjp^^^EEEmmɓ߲e>@  c.#ȹf'\|YH$]trl_pAF:tHX^^Q\\^n޼999Y6Ο?ߡC+VTVVF#[+))ǎS/׭['M{Wۧz}-I >}jߵkw-#y@  c.#ȹܽ=s׮]m W:uZflKZzscBxDš76lΝjћ-Q 02{QAzG.==;{^{5uGF1˜rEW +3f̰=#G@Bae9+ƒJz ΆF1˜r#AAQG b1B-8-r^\F221܋GB+ƒJC@(hC( ^|I+q@xPib1܋r?* `Q 02{QA!  4!\Fs/b <4F1˜˂rEEEsn&կ1{1{A!  4gΜFhe96W2P_u3gΔw:wmܸq˘B rsֽ{Nwi?2e%]A@qN^ްvVXap>p@xPibp朼 "ȩw*/^l}ieNj[t}~DFF>#==I='\uFxPibp朼Nn;h9Ꝿ[EEE&][C`=8r}Zy]]111;v|'֮]W]f'Oh{A!  4s2:}5V^=hРdC^Z;y,X-ܽ{efVho5_zURMzzzdddZZ?~w}7**J2OIIIMMTwZ:gIHHڵChjLy7ߔ'Je{ٲezSO=%K{q ~ZtѣGK\#۹s&뺥^m^x[nź~_ﷶvѢE=zС_f3fm0DxPibp朼Nn;h!˗vǍ7aِҨF:y$?QVVԌ~=;R+}Qiye{6m71tPY4N6MOGloܸQs%YN6zi=hW+oFgϞ zFCYgnw}Ok׮-[Yfyjc۶mj&P/_lk^o ??_vt{ӡ TB@9'/cӧ[ZAW^-U.sGl޽[|wTo3 .y|'xiple[Z%s=:ũvmƌzpyyW_}%ʚgj̶&'NKdwK.UUUFΝm럟߿llŋv*[eB9z}ޓΘn  "GxPibp朼Nn;h!Ho~dC}W/{<<.))Q/_{5\Nq]AG;vܺuuCo3Zg>CT"\hiYըcSʚ5k俿]7xC~{ur3-qN^^O6}t%UPRwrnWZC5q҃SRRwi^@f}kN$'[/H0 J]]]jj4FEE{6LةS'| P]f~ySZ|;ΘmBmӭq- nIxl|5ҹW#y@?#ףGf|F.ۯ^_"--C_vr@v}ꩧz]XXh(vmҥ ?BF|駯e'MsvMߞbt]vۭ[Vf~՗zN]>&9y{>wB rW_8p^Jِj'|rQQQ ]Cz4%͐@z=S4iBpoeutkA "5%]8ޗ[Gv -7; 8n7 P0(gָ\s8?/i ݉'-\1TB@9'/coAn̙ r?!P_,)PA!  4s2W8 \1TB@9'/coAN}?ܹsw F+ƒJC@(h3e >9"t< \1TB@9g+c/An$le%䜭9 7s2܋GB?cW;nO?4&&+{* `Q Μ ^/i "<oa&Ѳ$>Io֙PW|߷jy zKNH>}\70Њ@ 9[{ re,@M}}}FFw=|p{ߍ-@u5Vuuu&o_wT\\,?ۿ}(7hvTB@9g+c/Aνn"iiib111G8rn|YRRRo߾xҥiӦ%&&&$$L:F`<ћw+++?~|\\\RRܹsU(٢={._cIj z- ~3g|5$r(F7o4nl4r2t-nݺ{6mRKR***jkk'O$#K Qu$aJZZھ}T%OVv55&駟ֽK.̌۱cu 'W 6|o_;//4;* `Q Μ X)mܸ1'''66vذa;wx{k׮ɶN:YF%$:$G76zZw^CzXn,X$6!;rƸ5bݢZrzB_k~ֵ}ɓ%n6KkeHmmҥK,I&ݚDFF6T7lhl,--sYG76ZR7W^/Vb5z &H}U^z) Ө۽9ߑ;pUfVF''On۶NȲeRRRl#?KH(ieW}$9p1c]xH䠲a<;e9277l .~}?~CCǒB r>F8q=;y|Mȉo0;;ۺ6(i|$7'le%ȵ2:9K=h cD!8lذ9sxq̙ɱ}UtcVVV~~4nl4NYdƎ+&&&Ξ=[=-fI.\9O.)Ѹ$ z_wx2xҤIo49ٳg%2?/le%ȹp9 ={{Μ9Yp@xPibp札9KZo8\bշzkZZڈ#dx+ƒJC@(h3le%ȹ~VJMM۷<}+ƒJC@(h3le%ȹJeݻvmoYoq@xPibp札9+裏A!  4s2܋GBXn6nAW P0(gKV@M2K,@+✭9p;Iq驩[ppV^ڞ={NnІ9g+c/Aܫ[pmKs/ ?* `Q Μ ^|I+q@xPibp札9+ƒJC@(h3le%ȹ\1TB@9g+c/Aν(w㊁ 9[{ r#A!  4s2FrV^HRAhN[_ 4gΜF蘭-$4񕞜e_f3gΔw1wu#<&+ƒJC@(høqғs2l,ޓwsN b <4F18 z rNrZ * `Q )5Zb\1TB@S2ȅ-ŋ/xQÚܿ~vK.5z9^}`5p@!  49=Iڸ{zH^D >^wztq:{j !G'>@8 u$mAs "mٲe۷w._%7mY?Z~'x"===222--rmտ穩2;Xju3<еkՇԘ&2՛o)-'Neȧz*&&F{q ~ZnZ>`jɒ%$%І9 z>IwМ{rk׮6eOٲ{tt?}=͚5Kə>Q3~&Gy͑Z裏J˃>V+ӧO޴i7Cbqڴi8n89󹹹qF=x„ dgϞ֣(jٳ#""/AH6z! v6]6c 9=IڸD]oV 9wݟ~%]}Zr埼e :y|vT2=ztq֭uSYg>CT"\hiYըcSۊ 'SUVVhSN;hνAN:.JJJ?C}з>[DD|W3 %y|vuz׿k[.%%%::zǎҨ~qW.((fͲ}kNr&_ַ_W+ea_kW+!rP0(D'isoAo]z5mڴ,An6yyw'?igBLqĻի/:t萚*}K/tΝ;ڵٳgddd\\\vvvaamfX۵K?lIwМ{r۶m7)I@Nv>[o%)Nj,!=yfH{_nH[9?a8pS!W ܄4F18 z>IwМ{r7V8ޗࡸ!oomVAp%ܐ+nBTB@SN;hν+5W>>s{_j8qB%{`+܆JC@(hsz e̙3s=E͒W P0(Do…)@᜞m _JzΝ?W9=RAιn(^,|D/Aνx$b <4F18 z rŗW P0(|D/Aν(w㊁ %ȹ}?W;q@xPibpGAXLѣG=z>(2dHCC嫯P=2oa;s$8= P+4F18 z re|$ѿrڴi #(Y[7gdd}Ç8~SxP0(|D/A}߽{wtK$%&&&$$L:F;wܹs~^~e1lSYyӣ^z%I[O+WTìJf!6nܘvgd'd{\yf9ѣG#""9/_yRSS]i|bP&YfT跬84Wf}5z9D5=[n ,Z圏%ȹPۧy.--.}NuGH6U￯}ͣ.]T2l8p+R?s c+W!v!fΜ9oe~~_5}ITTT& 7ۯ>VX߬s[I{^/^z}- g+4F18 z re. ۧy3&//ŋm6՞[SSSVVL^v̙3Æ 8ɓ'eNԲeRRREEEiiiuuu(>>~Gॴ4""bDtt^kϜ8}$ժ*{v(^{z$G}'ն5TVVN6_m'Ò (t%+quP0(|D/Aν_Аm6{vݡC=ҥKNZSS#6m7ǺufXvmfffTT-^"???==]MKKSkSnذw>xࢢ"}t WVV6~sZ1w$SqU Xlw]hیW QibpGAXgΜ{9{.^8eʔÇƍ4jԨyy略'N(î\kSNɰ۷˶t>|XWQQQ[[;yCx9\\uuuii-Zڶnڽ{wjqU زe>hیW QibpGAX^|YTIFJllldddW^^Q\\^n޼999ٺ5?~ywaŊK{ir׮]JJJcuIl^y啴}^_r^={^m4;* `Q (^{=IIIK,Q]jטN:T/g:qƜIÆ Sui򲺺ZܻwD5=[n ,Z#4;* `Q (^{ ֵkW_H9)Y"CsK]t$F׾9[I{^/^z}yr@fb͎JC@(hs> ̘1C=NJ2a2331c]x6N<)uuu˖-KII7 'F[SSSVV6p a2_|ѧOKvX|A9Kk}>ʛoS^^iV3g&''fee1AθWqqqvvv\\\ttABJOc;v̙8{Z=Ly9%=W尀3gȴ'9Kkf̘&g>hmJKK +**UqGAswG?ڿ(^{H(eݷzkjjj/_nAA!  49Ks/&Y.##{iii=zAA!  49Ks/rmfAA!  49Ks/q o>#DxPibpGA܋r`/֧OܡC1bܑC8QibpGA܋GBh*}3fLVV֜9slq@xPibpGAV\٣Gt-8Њ8 z rj{ӧ-8Њ8 z r^܂sGAe9 z r#A!  49Ks/b <4F18 z rEW P0(|D/Aν(w㊁ %ȹ\1TB@Q9P㊁ %@rGA?\z9KkST0i?zG}]e 5!C-_}矗ix {Gة3/B5iWqGAk06mZ탾.,QWWgo2Q3'''w4oȸnq|A|F(^{H-޽CҥKNZSS#azhʕt Qk׮̌ڷoܰaC޽eY9X...))iܹ* [϶={._cIj9,+* `Q (^{9_ŋSLާ7n\UUQF͛7϶:M8QvrzWQQQ[[;yCY2hР%KXgX&Ӳ-J{<<m&2ɤIdcĈt{ٳ}3gj4ԩS۷˶t>|Wwե Xh&g{֭ݻwߴil\?7 @4F18 z rD q9U^^Q\\^n޼999YgcmWKڵ+22Rmkz;wvYⓞbĐ!C^u0.d?4kIޝWRltEBl_pAt!`c?^"+*++`c-ȕcǎ֭ئ+iiiS9,+* `Q (^{Q:;544H0HJJR„vOԩS}}1 r^z?~c=bdSO=#;=˗/w>N(Yf+ٸqHÆ ,N޻wD5=[n ,Z?7 @4F18 z rD>H8lJA8pKQQ,//W/})9TX|V\Zرc9s7/f5'QQQݚDFF6T7lhltXR[[tRm[I{^/^z}-ɟsYW TB@Q9 Pg}3&//ŋhm6Os_~원^v̙3Æ Si"CmO<iH*q=c=y>h߾ђ&[lWL0!''G!8Ѹ'OF]]e˖j4>!#Gͭ)++8p… 09E_|E>}ϟTqIŝbA %ȵ)7ߌQwتfΜ)i*666+++??_={-UV{W^;w.((Pi"Cmk׮zňѣGKFRFҚ fFIs4~񣞿o߾8cq%$U4HH;v웘('\Nх dӧKJ4.ɟswrGAZR{7qGAZ?a>}c2(^\kR^^q~;)hQ9>H~޽{vv7MR+"GxPibpGA륗^ҷ7=z4)7A!  49Ks9sSG8p?* `Q (^{IWTTksϝwޙ.222.HP0(|D/Aν3fH{A`?* `Q (^{y?oХٓ,@b-JC@(hs> *߿'? GhQVMQ9hY(^,|D/Aνx$b <4F18 z[6ȉ_~饗l6l}퀶xc;l߄zY_wB?kA!  49 ׮ŋ/|;2ۻᆱ[ $-۶mztҥKնqb <4F1DS0ѫ[oUTT$/jۅulo իec孷Z___c]e1G>\1TB@H )ZwЂfۯ^OGFF=W\ ~w_RRRSS<55UqV~gvٻ*;ㄤ@~c$`jP вZ*銲 -EEjaZk˲%Z-RZmA_@DX`D[ / $$@ $ٜ0rI^?x9̙7׿ևPccctRUU%?d dee}K_ ÇU80>>~…~۴ H(q@xPipF1a)Zw< 3<~/+iӦY'YFհa>s,((qqzj=y„ 夑m=r}PQQQccL'5yfy ˗/;w0J圏<"W W(hs>0J\|I+q@xPip%l1V 9y%E.@b <4`>$))nۻw}.ϟ>}]v͝?~yy}. G@~8#EW W5TUU 0>bբ߳gϔ)SJKKS-N} T s>0J\;q@xPip%PBζmۢӧ%ڥ$''O:F^lllx?Zaʕ999:uڱc^w\l٠A!<رcCoed&L{U{}MHHƏ_VViۼ@~8##A@cFN2eʈ#h~~q㪫%b=zٶ]ĉeڹs笣?Ν;޽[pQF9֬Yoq Yᮻ2e~*9y% TINNNHHFVVֱcPEEETTԾ}ڵkU^2[7e4{3g4ždb;v(7oK~8#99vѭ[ ={$%%uҥ7.s֭['!/ظq ;wpjYիW]uU{Qy(Aae 9EEE?.r7]vm3)̔45y|;a|My}ICN;::zժUlڴI&_pvYD眏<Vh$y&''gܹjsر&M:uꔴ6l C.|qqwerK\\Ν;u|źxCCCIIIAAAZZz୴TF׭['Ç*N8дms>0J\< x\1T\ T0^} iWWW&$$gɒ%j̙3/@ A.LtqWGN>hРyYO?ݽ{w}С˗/ zډ@~8#ŗW W(6Omqqqee}<" x\1T\`ڃ۷wر}̂bpGF rr<* P0dtItO<<" x\1T\`r_}^z葙o~vbpGF rGB+ƒJ+L\VVV&ts>0J@PnSټy8|a  T[lQZ)29#9!Q)N 6k}uK#9IٳgFF8##A =PG(|a V㊁ 敗s bpGF rr<* P0(|a (w㊁ bpGF rr<* P0(|a x$@b <4B@Q.|a 9y%@rGF rкQ\P㊁ bpGF r/i<* x+>@>F7IJJkt1!'صk痗]|a (w㊁']CpwXW߳gϔ)SJKKS-0J@"IkǏvyy tڮ]fEE~1yrw4䬢WZU[[+6mR+'4˒hQ"-;Iə;w;vIN:% 6HСC'|Ⱦ?я92| r  o2n:i>|[nQ+'дs>0J@ ꫯǫ;lՅ }Yd3sL/l[{+**Rɘ܂ r 7o=ݻw:t 4 s>0J\< x\1T\dTVV_k d1y(A.r%Aٹs۷o۷9C;ᜏ<" x\1T\/ʥK0 ==k!ŵ8#EW Ws{W^MrrrHq W|a (w㊁w5hBkz8##Ae˖IfS W|a RYYbŊaÆkU鳲mf i(AnΝw{=`!|a 07{ICᜏ<nGs>0J\< x\1T\dTVV\<|a V㊁ bpGF rr<* P0(|a (w㊁ bpGF rr<* P0(|a x$@b <4B@Q.|a 9y%@rGF r?0>>O… ~V(A.ryx$T%&7or\9}n]v͝?~yy}^ϟ+W(hs>0J\{>ɓ'<6㙇 έ~Ϟ=SLIKK+--O Ap< bpGF rCb϶m:vW\өS'^'N?~|bbbnf͚ݲe W;x~!ǎ[ng}VӧO$''O:F#ل ^^|y߾}d9y4@x@ <"r{;f f͚T~8 ]we8 +W(hs>0J\]Ş&iii:reee駟^{G}=̙3GYYYzwISx ***ۧ׮]=C͛7i<\14B@Q\Hhc߽{l={VmT{ݺu⋍7JT; gϞR$%%uҥAMSf#z꼼JvOHH{Qp< bpGF rm1nL K'Owc=ri;vL-??ǣWZMʻvheI 8#6FUSSsĉy r-qqq;wT=Gw9jر&M:uꔯ)(nذA/HCCCIIIAAAZZz୴TF׭['Ç :$O>D-h_D|a צ@Ĕ3gZg| 7GՅ }Ydݺ;rɃ h;rO?teC._\,~c^pD,|a w,..py9#H'Oܯ_?iKW QipF18#%/߿zzz޽Iq4+4B@Q\䲖dٳG},G`@"<4B@Q\r׷T "@"<4B@Q\zU~<"{WYY7xc^Q| d9 ✏<$Z \AQiǎ?+s>0JUVVD|a x$@b <4B@ rƍx5~x{A  h9✞\V;rȿ"4Ǐw@\1T\`Q ${pj✭gyp@xPipF18sVQ\P㊁ bp札9q s2  sj hm?  b?N=ȡ%A  c.#E.@b <4B@BaeEW W(hC( (w㊁ b1"{gb <4B@Bae9\\v 'Q6_ڴiӦM6mڴiӦζ 9\ ˘\{1 VʢETGo}&̞=ۺyEM:sCݾ}}Fh?@ }I`C* P0(PsuƼCUVMF6׬Y{l?X?GGGKϛ766\o'8Tr2f͒FNN}Fh+|uuu>3vR/||3…J+ 4!\fݰ1pqP}=zl5믷Qf555А) m޼y2 /ԾIII:t"111?s{iz5M"˩~9}{^jl\'''<… ^ܭ  b9Y7l;\*>+s=gښ 3ׯ[ZZ*<?ěQj/ʿҞ6mҷS*x i* dsܸqzj3f OL[n/~uVDxPipF1œˬ6.jӉ]tIJJujoh&M:>~СHO^SSJ cYIɓҖ;4ݸ7mԨg~y !!J:4jo]'#GԦQr@"<4B@Bae  9{li̙366_HPUڂ s5$GyD%7kr|^'N()S~etT%]UUe]9**J?P[ (PET\`Q 02농yC<|O~66_XBzƏ믿ޱcǞ={エuź1cƼKwqGZ K=ܺut9|ff},,,vnnaQQ,8c :YYYҖw)_TgZ@;ae Ad k~;ÇwhKˬeWӛy䑌zHى~,oݩY7i9Rڵ ,ΎILL+..5} ɀq뾿w﮾\( `{/02농y&'ۛ0g*yADs.nؘwh x{L9s.nw_qk?Z{WZZp0X9ƒJ+ 4!\:!lV㊁ b1" x\1T\`Q 02\+ƒJ+ 4!\F\;q@xPipF1˜rGB+ƒJ+ 4!\Fee9 >@8}c.#ȵ)*x7IJJkt0d'|R+N3"66VQ>Y1k𨪪*((0`}gZ';t ^zȑYe5v»wߗ]}s: xbPipF1˜rC۲DQQdǏK\ :m׮]]JJJdBmwSiĘRCN:],&&FfGcgjkk-Z$Vݑ{׭w䤿wޏ= tJ'@p< b1"GBmYBRDNN4vI&:u]6lk~O>QsdGG>|J#ƔZ~wQbYwyG0aȑ#Uҏ;}AiI\\xqZZZN#5jT~~~MM͉'rss͛7gΜF_S =Z+W(hC( צزxWԄ>},YD͙9s-^xA6z޽{yrv$Y{$|ŏd3vO{߾}rzVEi4?acw}웒"oHmm^ɓ'eiӦIJ4R0 1 rsA"1 rsA.rH(q@xPipF1˜r/i<* P0(PsA.ry.ӧ/]^~㔔x/$ ]KRo;+ W(hC( {IIIFFF]]}=cSMn{Z'GEEܹ>R0Kէ*f0>,I>+W(hC( <裏{=qr*TUU 0:1, BZ .oW -* P0(PsA.ryx$1--mÆ g}MHHHNN?~|YY92;dȐ5رczk\\܍7![ٶm[tt=}D9ԩSkjjs͚5W_VXreNNNNd}^bɒ%oFF'Nțح[Yfi}EZvv IY9ŋM <\14B@Bae6ȑ##0{~2ѣ._s4iReeemm?l05ԨQuΞ=[^^>tP wԩ)S1B7ZRn{ĉeڹsu!qFiG}5? 'W7pirnׯѣN5xV';Hӛ@茹 צHڑٙ3gM6okN#V[nFYY駪7$$$$H#++KƊ}͵kצZ;ue˖UUU!قսk{.##cǎj4Y9eA!22\GnyyyW]uURR^XݻwKٳ߷!dٷQbOn.\ӡ))r.]T9rw]:sicuzܹz@g|ܑ@32\"{9uSkժUi&:}$ȁ|M_M999EIb*^믿n#'{~hr>gyF_f!32\Hգ_T~֭5}9-ܢRC_SSsĉIW+}My5SRRfΜnTZ>>~ȡlbmAжT\`Q 02\2| ?&IIIv޽{HC=gΜӖ .X&OyyyTTΝ;}glܸk_Zbbb׮]ǎ駟gGRϗ3k9ގx7xZW(hC( n]UUUPP0`0rߪW^MA}g.~'$$˩?ꪫ8`3eʔR̖z A3^1G  c.#E.#O۶m֣Oh{o!٫G_|gϞ;w?M7tQi߿ȑ#jtĉrsI~ ?/)OwLdv귿mjjyG uafر[n .TC:4e<%))K. X"c Yzȑ#%C>\}ç=$Qj„ ]w'|lɶKPg:%A9uGN7lwluɜ/bƍ3(k*;r]uU2Nӣc.#]als}M_(C\II4***T~;hvѢE!k.̠S]v򗿬;ߊ6m/_7)9L.pOj,!00+4B@BaeeVꫯǫkՅBd5ѣmlZk+{~IwEEE߸$1;x`gΜ~^P=N[;}>\㿯$["GTq~#F)I;3F/MMPM5h WӛenP(VGKw.:tmoHiL+4B@BaeJn ڌVb6T\`Q 02\jr'mR+]1* P0(PsA.rR6 W(hC( x$@b <4B@Bae9\\F"M>}ҥ+TzӖ@32ܕ$##OW^v^bcc;4} b׬ÇSRR̘rWxG444dee 4hĈ+oUi 'or@cee|$1--mÆ εkݻ7**?gΜ1c̗޽;Pӧ RRRNZSSVXdIffflllFF!c? wĉ'&&vm֬Yꦢ5ȕdggߐ4oWN`Ȑ!^_&Ю@ b1"1#9rD~(ǎv7nҸ% ~IM7tѣG~7Pg~~,R]]-iѳgϖCɱ6n(m裏u9YY~Ο?_O ~=zYFM6_~s̑ pJAoOɡ tZW(hC( w裏233W>Jڑ`f0||ŏ W;ug>},Y"KLLBT^^^\\\YYi: wE>}:lxX{/h۷oׯ߃>sNfe9y={fddtM+Vz /$+;;_^ :sA.rH(q@xPiQYGCZoQ0(PsA.rk"$++K$(PsA.rQAf_}޽UKOOӧ… 9  c.#E.@b <4Xw5H3f͛(hC( (w㊁ҠIζ݂`Q 02\P㊁ҠH۷wigC@Bae9P^^^\\l5sA"1 rsA"1"W W(hC( V㊁ b1" x\1T\`Q 02\+ƒJ+ 4!\F\;q@xPipF1˜˼ۓz_롇w@\1T\`Q 8r=t5S.$t@˘B rwySm]aaYf+q9$/c. 5I{뭷nٲ@Ȝq wi99yG:4Yl} ] {ٻ * P0(g8J4ANRcY7 rZ.>~Eg\'|r=Ĥw}?}E5"_ bp朼Nn;h9ꕮZD6W5¾5lq)μek׮Ν;_rO~ykoh{HW(h3eutkA =ȭXbHC6F:s˓m۶Yvma[+}][:C{j233cbb222~s&ԩd}{2k}&''w׿>T4^}U8qΙ3Gڋ/3,X K~ ?.G\hm&*n߾]wfzKA5j?񏯺*zիWtt"1k?І?* P0(g8ָb[t;v8nܸ &HC6StUH;p/'N~C=d{M<~/+iӦY'YK_Ұad@:m$%i^ZOJ4GQMj巿miϜ93**FccYDcƌ`mr-/n;3|͵a5SX(gΜ׫N,Y"T/b{t󡀶DxPipF18sN^QO\޽-Ր.jyT#C۶mSoh |RR}Zqqq~k pԩݻKVA^}g1oAvbcc}^3/֙ZPWyIVXqү_@ bp札e,w٦!++kРA#F]>Aޒrb/w`}Ѿ}@~R@ЌW QipF18sVQ\25,Y$333666##8Y]6>>~޽QQQ?š3dwy邂SԨG7vr'N?~|bbbnf͚BŖdgg/]7|o߾rC ]J wqyտկԦ9rz5_ j_j\2''SN&;v뭷x>Ν;[{WZW(h3le%E.cq!ilܸQ:?#&ƍl^233@䜭9h۷oׯhÜq x$@yyyqqq0b <4B@9g+(A.r%A 9[G rr<* P0(g8J\;q@xPipF18sVQ\+ƒJ+ 4s2"W W(h3le%䜭9q s2V+))yxꩧBs{pj/8ZANO@@9[G[7yO] U̚5_/ۻ * P0(q9'lem 'Y+[o%b˖- ㊁ bpNOF סɲe JA <^A.lCS&{1릕}?jZ h"KQ;{+'* P0(a铴qyUVf_&{G}ƥ::{W!Go}Z8 bpNOF>IwМG=wy'777))cǎ_Wߩ!_%7նh9{Mϟ;w6mN PVVVSS}/==]_{/uݻ׿ևԜfԫ*='N9sH{z ﷮`m?rDus/й.\h]A/769=yu$mAsV\)mʔ)yyy{ll/r3fP#GľPw#zw//0un^S< =~/(J{ڴik֬җ4l09,((q;/իW&L,'lQ5GSgΜo|Q,l"1cXWo_[n>7]6}t 9=yu$mAsD-T/?]Jf+Q/ᡇ6G콦$9y_iKu/gΜQ_t'WTT\pAʚjΆfy믿^322dwӧOWWWK#..om[as;XV@@䜞<:}69zrӦMSm5xKqĿ_9AW^;w^~ujcx֕/ס*gV7T[ tn*RUUUmIDAT*a铴qyCҥ~o}.N'(|.ڿxNqĿ_u3fK/twth'kiii6mN+V(**3f̰kNN?җ|/3VʉYWo+΍_†T\`Q è'i WWK޽ >5\#]w߷3r8_|Gy$###:::==]hgyfqqq[n]`AvvvLLLbbb^^^qqmeXEg??,O?Νg>Wse9y_@(ZgW(hsz0Iڸ0J\|I+q@xPipF18#EW W(hs>0J\{&ϟq@xPipF18#e,w?IRRmݶw^0r"jhС֞ .\<1TO>,+N\Dll1 f8* P0(|a Z?WUU 0>)JMM/ii А5hР#F. _ûeb-J+ 49y%]almGO>-.%%%99yԩ555رczk\\܍7Ϫa[ʚ=dɒ،gyF?YNEpC^:''GP&J{ΝIII\vZ9޽{>c̙3fhwx&>?ȝ8qb񉉉ݺu5kV]]~%%%K.8%57۷!Cd>"s>0JX?:ujʔ);Qƍ$0zٳgQFI8{lyyСCUpru:$6n(m裏^{JjHB.\{QYY)6l羦_뮻.Z,2ydi|tޛnѣ޿#GubFANV?Ο?_Owo=zXfl<%5yҤI.ɜQF}ONNNHHFVVֱcPEEETTԾ}ڵkSSSQVV&3?So(Z㒲-[VUU^ANl'钇3_z%0o}K:?{g^Em &e@#°MEGGed$ ۰)j ,aQ!A IMj^tw9$}Wum]tz']v7 Jկ_ʕ+>s h޽beLhf& #dx9sDEEر Uf.ضm;K#GJaݸqaANcٳ/]Īm߾y =#OYWSԩ3q<())O]L};učf?yyyv93gFDDU6L1 &wQV|}}z)=dȐt*fOOOr*X~=~_~t435Gy]Vn%hӦĉy5:E?sfƍT`S@01R9)wjك└Kfbu~'2_UVR>4ÇZ`` m/5} p,*.G䑘!FM0!CLIvte?o޼9{5N3Ss$eӧ 3F~Wc̙3c=F.QsH0r@ӁҀ) B)}qah@L GJa싋#@E3 4` p ?P #g_\J(5(Ҁ) B)?P #E,UKF1{le^y啐O/:DY}V,B)\#'''**ڵkeǿ=z8p@@EAAGvv$FLˇJ(U`O#?~jqq#GJa+#G|XZ=ϟ>|xVnŷ~i94)eGVL)AAo@i @ rdFξhH7"""6n6端/\@.$$$88xذa/_+Vc 6d=,^8..nݺd4[f͊QQQll˛6mJڵ{W6rONJJ 3f֤I IQ0soPќ1p:P08Y(/rϧ{ )ڞg:sμrrrr߾}u}رV,=`vVǎj6m4۷4hPQQQIIɐ!C:v@ms2r.]*((hݺIx5ۆ 5jDU|ׯ'7h8( A,M{Eɜ ٕJpWXXqavڰ0h9"ouI//s?Ǐg۶m FԩSy!AW3gNTTԎ;XިʢR@ Fs@i @ rdFξh]܍7N:ݻNc0W6r˙V]v%ةS'[gn"={Kɪj 6?~bL6c)#x"tRN2J>/y65S@01RLj#de*'C&..?ݻAΞ=[Vn4ퟢVyyyvZii̙3#""mM9[nɗ/_>}t6m&NȫQ?sfƍG޵LgTJ?P #Wطo_tt7~aaaYϚ5Ց9VNLL m۶mZYVC}5Wc}9sc{dԭFUY(1Ϛ9DQPP^TT,@B,ʊ5jTvv ?P #g_J(jժr:w`t1{Ҁ) B)}8墣###bcc0cS@01R9DkTN|||t1{Ҁ) B)}a5#**Q۲e > {Ҁ) B)}"+V`΍hڴi˖-D( A, 0Gko=z9=@i @ rdFwq_# 0r`kߑS?@5B,})((HOO#8#GJaR9WBL GJa ~`=@i @ rdFξ@`J`b# 0rr3pP08Y(/;81{Ҁ) B)}+`J`b# 0rZB)?P #@sNM^,5 ?P #W`=z8p@Yf\%X:tq㆘o\Enyח_BYם|`+B)}Jh Ο??|VZ)+݌/q5e簰_q+--IHHܹ0xN>zX1L GJa싅iUˋ^p]HHHppa._LfO|v4oCt#CGASf׮]5DGG%D鴴ɤ3ghG{+kfɓd)Ν{y^Y3S;um:tmfddm̙cV7$#f,XJ`b# 0rłܹwqP( S1|||JKK5sXm$%%OC5'O)SR!96caP08Y(Q(ʏO{z=hРgϖ[7UşwQӧ_~=??SN-hPԙ0aBXX9rO?M!CS5kxzz=U ڵ+Cu8L͑QڵkdgΜt-99˧OnӦĉy5:E?sfƍT搌s`OB)\5(==(ć~Ϟ 5k3j(-UbccҘ[trSC{Cу<4֨ "vȘ)2y=y͛7gifjt*۶m˾Eӧ N~Wc̙3c=F.QsHF9'rdFZM+f͚eee)6C,U'ϟ٤I8rdFξfgg {Ҁ) B)}Q*>p3J`b# 0rՒ[>;rT B)\5(==@YrdF\Y("GJa ^ 3pP08Y(u#׿&o4H#4UgҦH´d@2k`J`b# 0rr3pP08Y(uS)SM&>SӶm[ٸqPK8iӦf5 ;81{Ҁ) 4BZh6X(effɒ%999O5aVY`?m:t67n\ZZzsśǠ7l^gPq0cS@01h#RlPgfW^0aBttwTT3rH֭N*+jcTf-4p,uȑ#)gϞ.K{Lb W;v2KMNNtff&ܯ_?rhҤ(--ƍT=+ڲe Bjj*5W^۶mY>VիSRRF!+jcTf-4p,u~PP9s/)GL.}/^d,CVW.,,(Qa*{KJ$&&%KDEE>X>T(}…b1_Q PG 2kـcT̨#Vcnzϝ;wc5rHVhHT_WT3~`=@i @ cTf-4p,23SXXxe1fCK #p)1{Ҁ) 4BZh6X(efN%f 4` p MıP* 8J/=zT̯WILĠ8Jj)J(81{Ҁ) B)?P #E,k# 0rZB)}+`J`b# 0r? 0f 4` f%%%`&"GJa 0f 4` =ܹJ7Ú\zUYzhׯߦMI&(L؞艡 GJa 0f 4` =&ÇoժH|ѵkהYNE;tЈ\eUɀk'zb=R9( BO0 W_yyy . 6l˗֩x7o<ŋ֭cޡ9eΝ;7!!"//رczE4-Y~}QNMMmڴi@@t)5xP{# 0r( BO05:{СC;wK[\\L{cǎU4 jW\K5;w^z{aͧNڭ[7D4܊+XzGӧ~XfڃjrdFfr(s VTXXqavZ4ܑ#GM*kN<#ƍč7heQ[2"++Salٲ׷LW?P # 79v:uTVw^AAA>>>FNל[Lگi&*rdw[}1'r 4̚^~]J/5?P #܊䤥}:y$ ?ڵKӹ)6NItt4?,SRde_8p %h^^^K,a`T~S X%R9VֈL\\f޽ tYJ:ujƍ8v59xffsɓt痝3%b祥999Ç`/Ru(}q94ŀw7#GJa ^ 3pP0`ϸ>CBJϚ55j%FN9?P-Z2i}9{BBĉſ#7cƌpjޡCTKP{# 0r? 0f 4` C6==HYb# 0rr3pP0SjڴiJJU5Y(/;81{Ҁ) Zy[o522C ,|@1R9( =mԨQ\\\&M ?P #g_J(81{Ҁ) ZrDƍщ ?P #\[ӃZΝw޹e8jrdFPYbcc۴i\-D,*wq3tPXBB):ȼEFF?P #g_J(81{Ҁ) ;rA rdFξGZLx Ӎ<B)}L GJa 0f 4` p ?P #g_ wq0cS@01R9WBL GJaR9p-rdF\Y("GJa ^ 3pP08Y(/Vq0cS@01R9( š`vIHrzqeaM^,4|0oӦͤI nu㩾XCMB,`=@i#>UVJ7c8]vM;tЈ\eU XCMB,`=@ipA_}/pYaÆ]|̞oxS67Xxq\\\ݺuwA˗/oڴ)Ю]#’GeTj@IJJ:uz<kP# 0r( š`Dt١Cvܙ&''۷]ǎhɍ܀+WAJJJ ұcG#ȭX",,W^}QJ>}kҬV&Y(6JĜ8qzxx>|m]*Mg9"?~mn۶ۛ9jKFdeeyzz9-[jV!GJa.ƍ;v :u*+ڻw/x RMgVcu `~f5^ jrdF JKK#vIJP@ٵk$''6 g}s`䒓H ג%KJJJhsͬx ]Y(6Bō?m{РAgϞS6nHcǎQ:688xׯ_ԩS\ii)YÇGDDrsstݺu>~x.]XUn@-@, 0f 4` )**JOOWY6C?ߟ=a+..NII 5k3j(}UbccҘqtnF;ryBBĉϘ1#<<ӡCTރb3@i @ rdFξXV ~衇9ifҤIz7Z]zUY 3Ҁ) B)} w;wnm۶UԹv"2;tЈ\eU9 baP08Y(/䮰=_}'_xq\\\ݺuz>}:)))00044t̘1͝;7!!G͏;&v{ĉϯe˖o7r.\>|xHHHppa._ahZ~=,ڴiӀvԩ֩x8oݦM'ɓ'w/;;ͻv&&&rջwA={(nܸw4''g셷\*]n?N`=;vdUnrdFSTT.)6a'էOQF~-Z9b~LW+SRRg͚ś#@Q3éyRSSy4HLLd^k׮rT#B)\u?Rj|п9rdFSTT^PP, ?P #g_J(81{Ҁ) Bk\߾}0cS@013rL`ݓRW#))I:`J`b`KRrJUFT7xC:`J`b#V0r( A[i@U"V0rPȽfieT F@;/9W3(@=@i @ F0 9j#`J`b Fξ@`J`b Fξ@`J`b Fξ@`J`b Fξ|,3pP08Cee0r`_4}c#GMiFiFiFiw-9<Bӗ}e\ L74iRZZJׯ_4TgL6w,ZMVVG6F׿u=C 6sc+Fΰ#Rp'_Y(3Ҁ) */7h7(gɒ%999|Xr%mX(3yW޹s* :,-f+v"lgF͞=;""6E/ޣ:Pf +PAՑ\&92I= 3J`b ھLP4ی޽;mvta/_h__߉'Ry"m?㘘[nǧSNw+ꩧ;9ҺukSE^:a*zg\ŠX/gPw}7]m^6rW^yA| Y}Hΰzb#fb"2Lqxe4@> {Ҁ) */7h7y16z-wҥ9s:,a 6PGͥDbb"PD-[hH/bzڶm+VVN}$''S:33{E锔#FEFI={|w_J?c5ҥ?m۶ܮr[Bކ6)mK꧈SmU&wzaP68*/pD&OcL($pP08Cee7/ }||(-áR;sرVZQ}r4ےLjbEuʿ:(VVi`2)yqq1gEp(̙3)M95ϿAr#W:<~YZo3M#J,(.1{zY( 2h2qCv|sر7n>AƉɓ'uHBs%P~}^A,[`>fh0ϟQYE[1e:N쫕֭ck֬#|LP{01ڼ~sXoTK%Luӫ `%r4` p ʠ  n^LǏ>`IJJZQҥK===7n~RDݖ ^{LގtJJ ۴iCMKK1bO$<<<ۄ ȩ9fOf&5m_&n(nPqB$#j!ھLPݠp֢  On=zTNrrsser0cS@01TM_fi3pP08Cee0rr3pP08Cee0rr3pP08Cee0rr3pP08Cee0r( 2h29/ F싦/#F̞=[[عs'ݏW^ݽ{IIf~M\UT9(tPI4}\WY5k֌\xv%ɉv횲8qpءbڵO>~~~111cƌ),,TV2d/r_ 27I|嗩tѢE {Ҁ) *//WB:rt~;1pP08Cee0rՒPe0r՘e`h2Fo߾P#bT-&\~~#))I:`J`b@ T@0$_h'ReFn˖Y~yBäI_vM),Pm02@1`jIΥʌԩ/?^>طȂ  * P40L- ڹT{ŗ@3g*-UDA B;9D5QMsjC>@u@3SԒ_h#[ |}" LSKB~ ;oj0' 6 P40L- v.0rjUڐ''B T@0$ڹ!qVsjC>@u@3SԒ_h#[ |}" LSKB~ n,]Y:uU97g3fn6OOOڗឣw\mYg]v8{C>V. ^g}!)iUlyTf{]m/N^N0v8jJa8/s۾}z{r1O<1СLHG``PBB;D9tguMg8cvA_TT4Thhg-HPwz;pฺ7|G>ki_ CO? [ɜ`8|ܲ%M>rB}:kDҐ'TρF.P/ Wa8 ܺV0ry'xk=;8 #b-ԗψI͚喺jfuʄFҋ ߧO={EW5`I)v.3r_|mHHhX|AfVm7o'Օ7K}EMLEδ_~'44qp͚G߭2:94"8-p\ޜ`5+gc#y8wQ߳o9:>sKc<٫xᅗڷV$a!. A=ZX왖wOODÂfñν՗ϡi}u\ Ҽ'Ǐܭ[OuJ ]wE\1>} kS \lg=tmwߝhQa ڲe-mݺ֭;yg~5|;+o~(8 1| dƌdxW}^=Am.nqlqu.9 unFS $c'DFFюhw>WV+YU Uіt7|Y _׭[/5ub,yW7'4JLr ~IL?!|*ӻВ=ɿZri_[1Ɵ5,gF]ٳߦ+}iN`ut+u+40uQ43.ZO#iS=)0] uW~54]slTsrS+{K=zZ]d* FJfN/sMt*)Q~n @eEt~~9|y9sޭ_Kk^WTnOGIϞdw)AwN/ [4{UY-y*|"gpXmϰZ]˧#FN1:-Uk+̀X/R?񶢦xcSOn]/=kE/C3POKFš`j$jB;{9{jGujz+MݧU~ŻCԫ0`pn=O-22]9wă[O;; Ir_]IYM_QЛJ5H,uSӧ࣢e˄޽M{5((Xs>b[9!ꓽ8ξ]#&ïpo+/lۼFN|ݻPrU=9l.^.اv˂#G+yBFn3c.ސ.ƍyhJS^%<'IG KyP/ wVSMQswx?ԢjK)EÚ`j$jB;[w6hX_t֭ۊFw]D_lddFRH^׌d>8^GG1hay4t.9 d{(M0Dن K _0s/ kKuA;"qﹹgBBBcVVcbbhF4]tn;p5zsKC'0'W//sJ5_"jժ/֭MݯߣE1pxȉ!OٳCwyه>a׻ВU=zHvsi_-sϽىDby$?{'/Úfapΰ+7щmwdBr/4;%'?J0ŀwˋHhU8;-ZJc9s.-/(*Q5W="aM05U5sJ~_#ݐVGGcſ(?ܬY[nצM^{KVVHUmCּO=*4:pPHdzڵ_wة^0q_""yzzW͖˘1ϖ߿'+Rw'$G$==o4t|/,ٲ5tO!I[ɜS(TF2&LGڲ%sP>iի m_C'R|>'4<{y~$z:_`g7Nu.˻Ad*RO"u]{)Z}:x_w> SQ5Cs<_}8zEݏS^}`x8qﺫ}`k|.)uH&Q>]#-Co͡:9 kc DM_hbG#@ w*`N@Tm賒o_ 7oޡ.m;S?TI% Z \`8yW9Q!O:߶ѲeBj"u>hJFa_h#[ |}"TI!l \`8yW9Q!OO:`jI/sCTp\ DՆ|>>f%!FQÝ p5540Z|b[}"0,kg_HJzDp>ƌ7Mn-_x ˂AT_h#[W k~A'4C^Kno=,}Æ Ў>lVCs5^*NPGDFFw_O?Du뾜>}Ɯ9k#/u}Bы,"~i/Rw Ob5A#qL0dN30Ԇp838*3rӦ"O a]`RXڢ?PhѲ{?5_f ܲe-mݺ֭;yodinNo[5|셍M<=&&h4~dVg֬[RoAAIIi4v2c쯸VG>N+Wn:YY9|aRBCy)E>￿)?|Ɏ5>p7ѽ6y_&Gx2$$׷gχyA fMG$Xn/:߅\Z~ڶݗ_MԄ6/V)gEh// Mw)Ϧ;f:*ّs_> 0X_djC8K?ߖ#s'_vM),Pm[Xٳߦe}l. +d#;Pw}a]^}D3[~Ӯ]NaE<w2%;-7m̘g셍' ?w{;v쳭tcu22VQekY~ v8tH:g^{-Z|<}~ZzjÓТE+0umnZDd~ ZΟהdGN  Y1|iiLww'${QZ|b|F%;M-ZF9G4ǩעEK'Lr=]i2mګi$=4 \<ȟJ_#XBsN#@ϋcA^ݞ$AIf͜cY^_2LHhԯFIM9ّsBG+dӁExɧԫrjiٲ5u#kzEܤ:?3s=F1~񜤕ý-5~@@@W_|mC#kqJg_u$@A;ڜ9Ҫۛ'×)5p.s{"ߪb>+NJ[A'!4{k1=M>.\1lXJpp=rX_}E ȎJ?,0r[yy7|i7nѢE+^;[H>}fYާwߝW筷r/XP2o;wT;WĻ~DZ: , ܭ[_6w 89#G+&R߼yFA$Xٳ=((V|BZQN6ӘdS53u 63ZPLG `,*LĴV$CTWLSc_<͙=,W=/=ٻ禧:RtKSE\7VNj0 5Փ<R^^׿ `pfl~mfn昘_|߾;ƍOM]ڳ)Sᶶ>滺n> ?33sBC?ɃVc r[ !X*MskVw=/mYUȑE^>R ƽQ=> `n9sQYZ_GmY]C3:gs@ ᅴb;kFxRgΝFkg10iҔ+ӯ\x8LԻ(76LX\dsZnfzyNZ*r/$'%X$ls}Jo˖B~mnx0Rq=+_6>ݑ2/$$ bۘ3KTy2&9r:a#E "Ӻɱ46H'j zUT쏍S<  09%-z hh50Ŝnh8SRRpv=T^^-sa#E FF.11f:rq^ #$LF` <\@X\0A:a#X@OWXcsI(=klxե | j==QaaILdmfI;+s%$$UTԈ\XcsQi&`:0rK [ ͟=xw늍'][={,\~k_`G=h-X\ds4lI=6l=g<9x#gA3V]_i.]9O#؃ 50ٜ [= I͸F;﬍bp-**aj_2}ܸ \Vw2́ZF%&&75umg̘9jT`r򜖖O)~^l-SLEdejcjj꒒fѡ*/gx1Qq<-[ (""r[i%.MBz,[\ FK^67pŋ7٣J0rK`h\=bhon 9.;p߾} [<@uA:T\s„0?=ǎY?}_pȢ)(<KΝcƄO>iڱ\?]`Y_Z[{hrr6Q@Y< s_gV9"ۑ̉#qc1132ְC:ɆmS=6m*C.N&[,F`0rK#=]-`Fz[,t#X9 Fo#yykkݾ}Fo#{{] @N瑣G/J,۶~ >EEF| 91`ǀi&1GqIENDB`onedrive-2.5.5/docs/puml/onedrive_linux_authentication.puml000066400000000000000000000044011476564400300243030ustar00rootroot00000000000000@startuml participant "OneDrive Client for Linux" participant "Microsoft OneDrive\nAuthentication Service\n(login.microsoftonline.com)" as AuthServer participant "User's Device (for MFA)" as UserDevice participant "Microsoft Graph API\n(graph.microsoft.com)" as GraphAPI participant "Microsoft OneDrive" "OneDrive Client for Linux" -> AuthServer: Request Authorization\n(Client Credentials, Scopes) AuthServer -> "OneDrive Client for Linux": Provide Authorization Code "OneDrive Client for Linux" -> AuthServer: Request Access Token\n(Authorization Code, Client Credentials) alt MFA Enabled AuthServer -> UserDevice: Trigger MFA Challenge UserDevice -> AuthServer: Provide MFA Verification AuthServer -> "OneDrive Client for Linux": Return Access Token\n(and Refresh Token) "OneDrive Client for Linux" -> GraphAPI: Request Microsoft OneDrive Data\n(Access Token) loop Token Expiry Check "OneDrive Client for Linux" -> AuthServer: Is Access Token Expired? alt Token Expired "OneDrive Client for Linux" -> AuthServer: Request New Access Token\n(Refresh Token) AuthServer -> "OneDrive Client for Linux": Return New Access Token else Token Valid GraphAPI -> "Microsoft OneDrive": Retrieve Data "Microsoft OneDrive" -> GraphAPI: Return Data GraphAPI -> "OneDrive Client for Linux": Provide Data end end else MFA Not Required AuthServer -> "OneDrive Client for Linux": Return Access Token\n(and Refresh Token) "OneDrive Client for Linux" -> GraphAPI: Request Microsoft OneDrive Data\n(Access Token) loop Token Expiry Check "OneDrive Client for Linux" -> AuthServer: Is Access Token Expired? alt Token Expired "OneDrive Client for Linux" -> AuthServer: Request New Access Token\n(Refresh Token) AuthServer -> "OneDrive Client for Linux": Return New Access Token else Token Valid GraphAPI -> "Microsoft OneDrive": Retrieve Data "Microsoft OneDrive" -> GraphAPI: Return Data GraphAPI -> "OneDrive Client for Linux": Provide Data end end else MFA Failed or Other Auth Error AuthServer -> "OneDrive Client for Linux": Error Message (e.g., Invalid Credentials, MFA Failure) end @endumlonedrive-2.5.5/docs/puml/onedrive_windows_ad_authentication.puml000066400000000000000000000063521476564400300253110ustar00rootroot00000000000000@startuml participant "Microsoft Windows OneDrive Client" participant "Azure Active Directory\n(Active Directory)\n(login.microsoftonline.com)" as AzureAD participant "Microsoft OneDrive\nAuthentication Service\n(login.microsoftonline.com)" as AuthServer participant "User's Device (for MFA)" as UserDevice participant "Microsoft Graph API\n(graph.microsoft.com)" as GraphAPI participant "Microsoft OneDrive" "Microsoft Windows OneDrive Client" -> AzureAD: Request Authorization\n(Client Credentials, Scopes) AzureAD -> AuthServer: Validate Credentials\n(Forward Request) AuthServer -> AzureAD: Provide Authorization Code AzureAD -> "Microsoft Windows OneDrive Client": Provide Authorization Code (via AzureAD) "Microsoft Windows OneDrive Client" -> AzureAD: Request Access Token\n(Authorization Code, Client Credentials) AzureAD -> AuthServer: Request Access Token\n(Authorization Code, Forwarded Credentials) AuthServer -> AzureAD: Return Access Token\n(and Refresh Token) AzureAD -> "Microsoft Windows OneDrive Client": Return Access Token\n(and Refresh Token) (via AzureAD) alt MFA Enabled AzureAD -> UserDevice: Trigger MFA Challenge UserDevice -> AzureAD: Provide MFA Verification AzureAD -> "Microsoft Windows OneDrive Client": Return Access Token\n(and Refresh Token) (Post MFA) "Microsoft Windows OneDrive Client" -> GraphAPI: Request Microsoft OneDrive Data\n(Access Token) loop Token Expiry Check "Microsoft Windows OneDrive Client" -> AzureAD: Is Access Token Expired? AzureAD -> AuthServer: Validate Token Expiry alt Token Expired "Microsoft Windows OneDrive Client" -> AzureAD: Request New Access Token\n(Refresh Token) AzureAD -> AuthServer: Request New Access Token\n(Refresh Token) AuthServer -> AzureAD: Return New Access Token AzureAD -> "Microsoft Windows OneDrive Client": Return New Access Token (via AzureAD) else Token Valid GraphAPI -> "Microsoft OneDrive": Retrieve Data "Microsoft OneDrive" -> GraphAPI: Return Data GraphAPI -> "Microsoft Windows OneDrive Client": Provide Data end end else MFA Not Required AzureAD -> "Microsoft Windows OneDrive Client": Return Access Token\n(and Refresh Token) (Direct) "Microsoft Windows OneDrive Client" -> GraphAPI: Request Microsoft OneDrive Data\n(Access Token) loop Token Expiry Check "Microsoft Windows OneDrive Client" -> AzureAD: Is Access Token Expired? AzureAD -> AuthServer: Validate Token Expiry alt Token Expired "Microsoft Windows OneDrive Client" -> AzureAD: Request New Access Token\n(Refresh Token) AzureAD -> AuthServer: Request New Access Token\n(Refresh Token) AuthServer -> AzureAD: Return New Access Token AzureAD -> "Microsoft Windows OneDrive Client": Return New Access Token (via AzureAD) else Token Valid GraphAPI -> "Microsoft OneDrive": Retrieve Data "Microsoft OneDrive" -> GraphAPI: Return Data GraphAPI -> "Microsoft Windows OneDrive Client": Provide Data end end else MFA Failed or Other Auth Error AzureAD -> "Microsoft Windows OneDrive Client": Error Message (e.g., Invalid Credentials, MFA Failure) end @enduml onedrive-2.5.5/docs/puml/onedrive_windows_authentication.png000066400000000000000000002717731476564400300244670ustar00rootroot00000000000000PNG  IHDRlB*tEXtcopyleftGenerated by https://plantuml.comviTXtplantumlxVMO1WriV TU9$P QzN6;f6!"Dzl>W%-sh~ F,:Rr]OuVy; ,DBM3Z*D+)=-81h>XOͩ61Vvuax>%|^#`l32m kڈA*tť=as[l{oBXNߪNF}`H zILSgn^FFdBgƥDaY{mVX14br/@%]a&aRL Y{*Zۻ q/3Dj{̅y"m0(hF̋UN߷iey#bE6}Ay{6qViQZ\Rp)ĪFm&?7%y-~"n үZ>_O9tnb3FG]MR-s/ FWuJ?.3uut5^tWɩ|pbNk0t*R E/8 ࿚8aZy!Vs]M k7̵ ΟN K1&14=T gvUV6 :نEfr(IDATx^ |Ws4WW`cٯ ` uUVZ/`C>O Js̘1KfbP5F@VPhUA4h9 akn- uFXD#[ F@VPhUA4h9jh\rǾ}ՇffoڴD}&[M=P$b?33wys`!?_^Tw}7$gΜ{j٬yzsܨQz3vӇRgsXgPPpN]_xoɭ{ϸA4h9Q4FEEϛ|21:CU&D7Pc8S?:44fͧ99{ouS#gԙw۶?lOrڵؼԙfCUD#[ F@E3ϼۃ5zI\)`y''Ĵ׸qOVy;uJ.M'4a‹Q{Y+}7Hz{{7i1e̊zu{z{|sif=vY/}Nv%+k7Ϝ4@-9q₲~[ԲeaF)3(5k>u,wEֿc}>(ѝPFg{۾=R,YެYuZ񩧞 i;_şOߋ/xwz~KѣEin- u˗sz|IniƵkFNA=?waL)scΑ0ԩkPP0ɧ4;LI~*DcnQCҮeF(oNlhT yw7z y4F:GƓ'nwhjJ. .M1bLHH~r_-)Jc?O%B穼!k՛*G]٫h䦟*3(StH-/\U֊s׫ѝL'+if&%M>YRR"P^H7o.Ow\,>|JmR))iTo1#c#rF>~} R>ե>|^!efGFF1Y(ѝ%##%￿c.?'埥s$sI'+|$ɍ? rCBJz:֭ێ4#ѝ̥Z4XM6L2t ujY`E2]-IRN4ifޤ^|=?_ݴ2#1)'"11-~K,}99?hOlDnɶm;I6m>"""y'.8,%&ONm8U, S=y 8q8s^&HRkYɍLgǎ04S{?t~zMz,||]Orzڴ9ܡim;w4C 0F@͟h9E7pvlE~==_~p3uW,ivw;s/lٚ8vKӦ_pXJR kSO= 9_;wZ>?m3JO_KV}1h`M=={^}vr99y?4a'kׁLNoN/^生"S 9`..il8Ј_o\JYdyfȐ e֛ vȣ1S)y ̃^~ִNQځ'h JS7Xoc97]mF5fz )EǎS2nd@4./{IfX+e*a?O%C)?ߚ5 ӠACy)y-ԪÇ?XyTm;%ShʹyܹrQ(mmtt׮txcuzѡg0-m(\Q]ii+iz====N}tGZQ=<_JN_^)04>/(22jCROV!( HPu5((xР))iyeD=Gh~%I)g 'oiMrlC3^ǎ]>lD# Th9s6 㖒_qSqyiO=,Խ+*^thժO’EO})w)[Q^VJ;yI)-)by?YothdNh GgT'Q0]ZP+땜"Szڵ&'/V:h6;#c"7ƇwttӸ{QO#!vSAAAҥ#F iЯC uN$RH<)vǦtf9$ ^jݺs/PTl L7#3Thܺ5K=a0؞=/k1 7j嗧nْí]7f>++9)i_OddlmZ(?t~  )YYHz3onf;%;{f4;22K>/JKK}v"Mꂰi˗-4aa?TOCFcX@6pKIIR~[$ϯ+4'"?B%9)@5mBDiCr$S`.&L^ɶo߷K, 0Xj#I#hڴMqǏ6`w[,#&_OmZ~{Ab_yYz e)ʌܒ7g-v>=v;d-]4ΙPvrM>9-[5 iݺ- OtxL1z.999h2151P<)I}yZ_ ԛ ֮6mڜhd H/4D&.]~Kx]YiBŸ<==Irdc9[hIm]Lkۄډ$I.Ù\M4t`0fs 0Xj=bGGح"-zE"D8Gxyyyh Ϛ5;իй]g/0RJ^ -'MBkZ0_ayhaDhmtҺиÂ/+|H!VW{:V~C=Fn-fԨ"N`FgqW7"SL>ݴnjsjѨ7d~]=zX,N4<nH0;h>yx࡟|ҧzI39LJ_o&ԍ&9$7y`5&D{1 foO3e.˶5L16F&Dc%zM~G|Ԇ.hd_hshjA4V)_ªy'Lm2FXM66F&Dc%zM~G|Ԇ9e:b m.0XM4*KX6 1s0u\ ahU 5m Sb!`/ܒ&|֬M6|es4\͵rR*E~|ᅿ 8ۮU#&qk߾#&X9UfYŌUm^GfUYi!Tup9*cBK+-K~Yˣ&_Do(LgΜ{jՑf{7l6ڴi3dg+Vu 0rr<؟5jرO+kOxzzwԕnr͵ҳ]eC]J ^)*Er;aee}v+?W>fVSfsֲ-V~׋Ou4V-savh&^ba e3Jy≱< S]w "fp9Hp!ds(`D-z9jU~ͥFZϛ|21:hXAyu1)`=ttMl7#c#lzXʬYo?zH]֠`}סa5k>9{;t|9lXmI+9_ulvE{Ao kD'EWvC|?{3x ov]^bgCZ4`[mۼ^|{ĮPM(5nx1JQv3"rذQ%/-ĕ"f0n.ONRՙ4u8\* #U娕W m.5T4>K=XJAA1-}v0&ONiSq矟L.M4a‹Q{Y+}7hxx{{7i1e̊zu{܌f`ƶ>~]))i,aʳc4#o߉Dfǎ]vl<4}p=*G˖exe6{:]^﹧?HtӦƍ{JL4D |e)=oZlM7WeEf͚O]YD^褸DƧs._CRnU~Il]YQOدC 5_IC0|}wsTv_[XCI{HO LaS\kD4xPuVGy"MY{CJz$&m%3`s+0 gQVѝnQcƪ I6ЪXpp[|Lr"Ĵisx4Hl/ʚ.kT6J*2'֪ g#\k娕W m.5T4X_ߏ ,YY< i@~Cgayڵj'99(g/(y/<<'{X# jFm=6Cr8~dbz]ad;D؃SՁ% ?jyqLF*2'*3~ ]-|e&RsEEFFy ]f`]vmXg#l2gZ*J!GѩSנ`l4P΍+ٗ_ ‘Zg?`b۹Gh O5 oSO=۫WԳ]W9[1#tZbby?Y)yx㍷Ef 4yN.Ջ 7UVG<,)5HEFZë0zvg]՗V._/\H[ `uX_$ c_J nd+H!=~ٳc2dDXX#oEpS?$i9{I-a}!͛^h﹧7ml-X*8,exxzKJƕFy}/$ spE:j,TUl;دh-7?4~kƏҹMNSha͝xS޿@8y?hʻ*bF Ghy@hǎ< h[hI3wn2%-%äOt`q6α&\х6-`P4i ͹4Q'fj?_<>23wѓM/n'q///OQ!B#3엢}o$yH3`Mz_(ʿbF 3R4I}$fSxRdR &4sJ ņzWY/L= mkÊIV._\jh`V*Frܿn]۶T2n/*MV7lZ6/OA3e ٪(f]d^oU0[\ ahU~ :]b6}1۷ؑ7{ʰ:hwJIIS3 3h. 굪eW2YA4B&DhRL_:c.ivZo{`jMOX-0bƔ Y&DhRpUoO30 SGL~X~ ^0!f`NB D# V JV&#>ajC2L16F&Dc%zM~G|Ԇ9e:b m.&L>s FvT DcUoO30 SGL~ͥD̙sO}Aƚ5k1VT< {?9O JO`S#BKƋ/͜99u  -)ifvv8`lݚ5utu`UjII͙zII7qC2L1DM4ŗ_~)&1{@@t A0f i)0`0F`C `= ҀS ``:81{@@t  -q0cHN@4;x]`= ҀS ``:th D#] uaq0cHN@4;0181{@@t  -q0cHN@4;Z`8Ӂhv0" 8p uaq0cHN@4_ٽ{?,0޽{KKK`L`:*b&7bDM>򤥥Tތ87j@4JIsҥ#GvAFJN:C}o߾۷_`y_4K̛7/::?**_'JEX6%6lC}&LI\r[nE'NP[6ڿ?/%y.G~Jz"L2dŋKKK ֽ{wvZϨcǎ&ML駟4ٰۖa}u;f+WGN4vJ*l$nCo\i)0`0F`Goh1bXh#&&Pqqg~~>ݰaCxx8m={r>|ZJ(]ϙ3g/]RzyfNMMmРi9\4lfSQzzzDDQeqeێ9BBZpHLTRu#p'z3HN@4;zCk7nܹ3,,lʔ)о}<$#88ϯ,77ү\²HۤMozzL8+W*ivZϞ=Ǎ*X9m۶T " 8p .,H'[~>:dȐ .ʵ-[Xz޽^zܹXjIHHHRR׭Vk\\W;~ hs6nVCXEJ>///`g+شie&ŨWMuyĉzСC|A̓qq:qF.,,{x " 8p dxS1cƄ[,6m̛79u 메v-ZP:)T'??w҅<7n!ŋY A)Vz1ƄRP3?ۥ ???3fPY5jDح[m0^)D#: AU- D#Z hU D#4^3p4A0D#Dp  LؑQF)H&{ dY'-@@t ˋvOٳgObbb+zЬWd EF 8cǎ=zw>sВHf L8Ӂhv$Ck/ʲX,o&S(&MqFGA^eSE+W4h0k֬Ҳ2r矋<@=/:dDi)0`0F`Gu7n4nx˖-<}O puI/_9rdhhhHHȈ#^sfddm߿k׮yyy.]mJ,gѢEy˖-kݺ/@JӧO7 \-XF<ٔLʖ 8ٳ0o޼h%O.\ ~DZcEy睹x91(((,,,11gV;CSUm-;w}wЛ10Dp  LȰZ$}L02dŋKKK ֽ{w@Z~'ND||<)+Wu֍F]nݱcl<# 'N[R"EyЄT\F~?̙3Ct ={|Zʣ\47H6gff+3NNNt钐YjҨQ7oN:t{ڥPN-===""VQ:]%-ܱc*++@]4~w[PPps.;jȔ^H~~~ ="'lezR+WXV^~T^:66aÆbPׅ {CqqqYYỸC СC[`O4mtSO2@ƍ75j$8sLE_QƢ"TT۳g2]alWR'z 6gcVXƶmX~ SN )UZČ" 8p `h/_hQ3p4A0D#0f i)0`0F`C `= ҀS ``:^3p4A0D#] @F@4;x]`= ҀS ``:N0޽{KKKݻw{xx/Bz; 0IqsσO1@-8Ӂhv\ZLKC=zTdIӧS4 A%VR4FUgϞ$&&tpA-ƵgA@t ׆RCҥڵkB˸ ]H uԩG1&FϺ2JʲX,ofII N4iƍb>\yPqmYi)0`0F`ǵ%(/ˋ/[u־$6Ν;7pDIqoTĉJO۷o@@@,X˗G2bĈWr?J6lxAOODMqxy-bm۶C]vcE4OG8;wZ,sK9N:SRR3i9:vK.i9TH%{3fStktσڍk3΂HN@4;.4$ڭ[7>x~RCʕ+EEE;v4idsss)S n)$\$$$ 0!&LM7CyUѫW/RD,QS4[!C\xtذaݻwg4OG8k#33aÆu;vV.#<h>TH?sq!Vf 8ӁhKH97&9q o<{,>|GDD駟7n,_'$\j.Ş,}Æ l[ %͛7vjjj fzfgg7gGvV)6U)*U7R iT? 6sdD#p.Lͥ+Wݜl{ƍ:nݺ ta*U"88ϯeL6-22}%ʳtRyrѨΦn;|-[?>%V^۰aCjbT='NU*|wQbAA2VyPhcP4 V\ɟ4L&4tпBq]EEEqi o |||O78v={Z靎f~{ڵ={7"Μ9b 4m&oS VN̙3DLZD#pV\BBիWϝ;׹s'C'O{vR{ rE裏2… re'xyy;[M8)FDj1'NC1=y:.I|=d?~R8lJբ1+++(((99˴{)v[[%z2 ^VCtR':n8_o)svx8R5ox3&<8p`ӦM#""۴iP1p4A0D#_GL.P HN@4;Z"#! @4_x%KzѾ}m۲afͲŬ:D#ٵk׸qn]FEEA7Ph6m @ؑ.<48A@t &xˋS@@t  -q0cHN@4;Z`8Ӂhv0" 8p uaq0cHN@4.th D#ׅkw00cHN@4;z&f2&b&7bDVM>򤥥7ߟw#!PЛ10Dp  LZJsҥ#GvAFʪN:Ci-Z+^ {h2eBu9أ>:dȐ .ʅЖ-[Xz޽^zܹX&fIHHHRR׭Vk\\9~ hڵkΝ۸qc[kb)OHU`ӦM}fРA*&닚Z.ܹĉy6;xoܸaiD#p@~`` {`XRR2f̘pҦMyH5D#Su CP%4A0D#Su&hQu3Ji)0`0F`D#1PHN@4;Z`8Ӂhv0" 8p _5jTP)Ⱦ528ViiiDM42d^T]{߿XXX@@@LLLbb"c$FdҬgЃfb&c)0aaahhhIIx@4=^RZԩS=隘(jT*!++bLl4iƍ|:,lUWLU1~\y#yȹqFƍl٢LܰaC``===85i#E􌌌mҡ]"Ν8p`PPPXXXbb",eZnsN^ en //SRbJJ y&aBΞ={%60'J5}3fStK>}o߾Tv\2]|yȑ#F\z&w*槦|GDDGUWJ#GT)6U)+TH%QWZź3??oذΈmk"sw8_&Jk:Μ9흜|%!;(OYY4ћ10Dp  L0Ii"##_N۴[tMpgzQ-776\srrHEϟoٲ򻗫WmذappbT='NU*zwQbAA2V21RzU^$U =z(+1R/ի0H {CqqqYYỸ ODo\i)0`0F`GohݸqQFF __&_ ѺHsl}g=ߥl3+W*ivZϞ=Ǎ9Ҋ+__ضm NU*F[ ~3gmiܭs"oU[7띣Wez:ɳ SN )4HL] Go\i)0`0F`G2F_Oh~M6r#CBB_nZzEv!AOvKHHzs:w {8~8e,,,{xjј|e=uɓCl:n %<=66}G r[*]vzUʂQ+򜚕ڜg((( ׮]+++;wnƍ_~SƑ" 8p H?ie?h !ڵk[h@b 55qƱ.^XOʥBIRH3?ۥ ??? M}s֬Y5X,ݺuKIIq㕪ERQbbb&Lpy[["=vhIIɘ1cémڴ7o˟ѥKnBjMѨ\rʾRT+zy6JmNF*iϐOIߟ:իT`6G~7ԺGQQ/^(P!10 Dp  Lرm۶cƌٵkx uFtm٭[%Ky D# آE֭[7o:D#MzăG찥!iժUff&>0" 8p %;vhٲ%׊-ZܹsJJ {҈Dp  LpblVÅw1cHN@4;Z)FG1{@@t  -4- `8Ӂhv0h嚏0cHN@4.th D#ׅ" 8p `8Ӂhv0" 8p `hDp  LG&fΜgbDp  *jU Ch$.o*cϼ3f Ebb0`+t2k׮O7x6,ٯcU D#fFiHD#k+͛i% SNe| Ky~u+/)S3ض:1ji)0` Fam[bE^^%>c]f O38_~eJ_>Mu eca͆fPc){@@2C4nڴs^^^AAAw}!@JdJnb*۷oOO$aM6}7ecbbիw^)SX,:=zTζ3<ӲeKܱcG___*/n= ҀS `P!-[FBq$bcc8oEرcn޽ǖ#:2zjygs"%`wʕ… y6tʹ?A*,˶ Dѣ,sff&]J_~.]NƔC>2m9 @q@۫WtQ޼QF) 1p4A0TwFb͚5ƍ7zh:}TGvM%˗i[moT"%ķ~ĉ:t#GpͲ,_~ OOOyŊQQQG_NU%%%Lgx^A*)3mϘ1P8n= ҀS `P!x_ER®鸬m*w'L@O?GOgɓ'+0!Ir;44ԣH^6h@SRh$2L4~駗.]R8GEH={;T#W󼩼ITF1TbkGO,YR+Wzyy5mڔ}U ++|Gyxx8mϞ=:y{uYvsԶQF;VT:Pqh۩H-F٦M:4EcfhUVO>4VR1TbIU'3}ݔi&Q3TE.;,GN2NOOoܸb8qbLL{``އpJKK'Oܼys˗?CE{x~8H]v$KH=Luh+Vbf7yW ĿeZg R!SMW^ N;A<rhb$b;& ?~w2l*8h3fʋ)nDp  *V2`hDp  *Ӣq̙T 111Ƴ}v0" 8p iѨYJ0" 8p ASB4;x]`= ҀS `P45 D#;M hVXZZ*-޽?x@قhj@F` 7cVO+s#Lq<&Q/Žvyyy}L0A8tĉ'Olڴ]FeСѫW/.8>}?~\D*иym6?j-úc z:c81f@B#%;g~t\kjjf̘߫W]QRք -Bbٳm(SRR222Ν??/ܹsgY!Cx 2;N3uƾYk֬wn Hh֮]۵kW{s;gl޽{~{èFuak+Ʊcǎ=*NׯNprӝ?{gĈ+W4xlJ퀂F1Ø ׊W{fffv;:u~S;܌gLrc=z|箻؊b1G @bصِ!ChH!( 4! Hh-S?CFFw-y6!( 4! Hh-㊁ֲw{'33S%nݺkɒ%iD(hC8. p\1*Tb;%.z<~? c$4HسgOۭE7Θ $1fgg?#[o1@䔗Y[pc2f@B#ǘ A!( 4! Hh&8 * A`Q 0f@B#|hW DP0(p3 >|+"JCP(hC8>ZA!( 4! Hh_8 * A`Q 0f@B#ǘ c$4nb~i\\\}}?}W ҲrJܶ%%%?<~}P *4{-.]u+W^xAzz-b5f̘ɓ'sssgϞmk DyyyTTԁSʕ+3 >|]@BbXI& 46Bc^KHh Z >|͛cccu ~vB>܀B4ܶ(hC8L p]1lqeϞ=m۶WʊVQQ1~9sb fz'ONN{|cǎ}}WthxԩSLRWWQ$/N [l;r>ݜ*NYny駇  oڴIg{]tҥK3gLKK>}Io/,++p႞AkI] ذaL%<8*B4ܶ(hC8>ZÚOgҤIC 'N|5JKmmmyyy.\m9پ}{Ē%KFiVZ2yyyƍ%sӚZI$v/ݻ8pQ7"6?|~Զ$Ν;ZJ4.vBGwږw=bĈ3gKKKemN2>jxՀɓ'Un[ 4! Hh- 튡GR4Kt~cYY}Z޽[ދڎ* - c$4‡vŰ"yY[[^۷O$V};v𦢗6͑OTKKLLСCccI$,M0>z-4\enݮ^*.\i-DqM6|/TߐtZSsV7mܸqСwq/cdN=uڍhB4ܶ(hC8. p]1vkNdtB&M;NweٳHO":ut}FШӥEttӯn>~֯>:-̘\Bxa$Νk׮ڵk՟عsz/4NtڍXh pۢ`Q 0f@B# >rȼ.X@w-ZرsssCf̘1UUUP}v=$|QиymJ+kuVf|NJfi.Q'L ]O'NH| -O˩SdѣOq1 3f@B#UWWYh^vOcǎMHHHNN5kO{n~I>OO1cFjjj|||^ B=jѸ0cN/y飾/wYZ o!TB믿nxڍ!4-̘ VsIM{޻wWuuʕ+~ӣG7c$4‡ n-fee}߿{}X t A`Q 0f@B#|x01CZ2d;Sk׮$F.ۄP0(p3 >|NZxvۄP0(p3 >|Ns?Ӈߠu&F1Ø  p+o#FtMrcVV6!( 4! Hh_8+(Q܈V_i c$4ZoѣGggg3 ʪ׬YS^^n76c$4|1f@B#|0q@dPi b1Ã+"JCP(hC8>ZA!( 4! Hh-㊁Ƞ c$4‡q@dPi b1ׅ+"JCP(hC81f@B#pw{ HhDTȉk8z#G}]r / x뭷Ό'Jǵ\3  .L:_~A_B.jhh79kll0`}+{ׅprVd̀Fua[ٳgOvtŋ%F&'''%%M2NkfA+zꬬ 6dgg ...ڲe|ȑ>L_ti̙iiiۧO"Fe G??4VTT?>!!!%%eΜ9*[φ={._$58sЮ@4F1Ø q <.!7n\MMdQF͝;׶K'N(_|Yϯu,`ǎ-]vj42 .ll۶k׮6mRK .+,* A`Q 0f@B#|Bhܒ/gϞU]QQQ%%%-[RSS.F rӧOwVVrti%ۅ:u)ϟ:dlltZsڵkbŊ .F#[h,++ǎS/׭['Q{W=UӒ<'P0(p3 >}tkjjd%aLI%11CƔ9LԞnݺ]zK9U-vi_tmܸ177WqNNή]xc?ۂ޷oB=K.:-)s\]1`Qi b1G˖[ %Ϝ;wN˥KxD#pbbbVn*~}l~ҥhe[Nw=33sŪiI: JCP(hC8о.l-X]1cWUUyc۽- Hh3 >|]@b 24F1Ø A!( 4! HhO8ӧ/_}/Pk1`mPϸ~A>O|#BbA c$4' 755m߾]IIIǏ/++Ĥ 6HWllg>;v۷+lkϞ=ڵӽ/^,2eJ]]4nڴIҥaYYY1112q/QPP.z<uqY?XQQ!'!!!!%%eΜ9*ZߑֳgO[U/`ٲeK bBb c$4"tR!t?ۜF裏z[O~~~uuu}}ɓ Ə9RbUmmmyy!CCcUU?>|pݛ7nܸI\rs略'N(._שSd؎;d[>5ܟh߿… 0Y۶mۺv*W 6}[n_a2f@B#B'J*ҥKf~all%>}Z޽;::Z6ʤرcwq e###Cgʨr˖-ֽ{;w]v+VpmnݺuW_}߿_:}{FØ ƍ7:;HLLT1ի組"٨U.`ٷI"VJJʒ%KTסC4IEءCC>^\YvNNzKuez_U/;h] HhD$uYQݬ[vm}}ܹsJ8N';zBISr o#R|=j.e/EtRI^} w~zFi\xuZYRG1f@B#|BWĉ'`>oxTqI>y***lJoVVG7f̘*osH%4:uIhhhhll\lYZZAFo768p 0/ݻy${{V往z?ՈoDW (TB@àF`Ç?e/w9>>~Ȑ!o5C3g$YToUVVzX3cƌT9b^ WIIքA7vX399y֬Yu˱M&Iո*ʴo` P0(p3 >hUWWY|]O' D5@ c$4G3&;;{޽>%+Њ4F1Ø hUWW?u떑Ab_1JCP(hC8ׅ?]vѣ=Cb`TB@àFx6 1c$4?ʒؽ{w n HhğTWW\n ~4ѣny Hh݁fϞ}]w 6G3r#p;0f@B#|.ohUWWYs+*4F1Ø  p\1TB@àF8 * A`Q 0f@B#|0q@dPi b1> Hh3 c̀F1ׅ+"JCP(hC8[ğ~i\\\}}:{лvӂ- JCP(hC8,5,11qG +W;Zz[.]$˖Ư20rˣ8u]cǎ~ :u3f̱c#T\p>vkȜ@ c$4e… SNׯ}P$ իWR-o 64644؛B'պ\l޼9>>~͚5Ժ_|;`=իW:㏧8q6iGr?!9]1E!( 4! HhG={ڵk{/^(12999))iʔ)vTٳg|;W^Q6z<鱱GV(U۴|0/œ9sTKnn/_ti̙jd>}իWgee'$$Ȅ:_ٖ7mڤW+rYGkK~3oڰaI\\\ϛ1M0oot~WWX1`=ɓm۶=u(VGo,^R 'O?:!n(NW uQi b1uakzLJ{ƍWSS#bԨQsU#Gkkkˇ *4"v!ua꒽v_w8tYۈ#Μ9#Ǐ/--U'N]|Ye-…^%9 KFYtwKMM(~Æ Scx I;Ճ?o^dGTƣXQWwM%] >q/0ܘ@ c$4>)))>>^6222Ξ=*++JJJ-[PVV&#Wy6~k N;w]v+Vp{~><>=l:t_ -[NaINFj%ڸo>9-ޖO>wu>ݻm۶WSG͓&^f(VGccc{iz"ɘ p %K.]m󤒘ءCF[54:#c6nܘ+y5''G=}%i.m&L=l 6[PkZCxIkNcx ۝Fl| c;vȄꎫQ,sTw :;pUt/" pcɾP2ùs͏.}QsX\\,WNhK.*'^ԩ}ݧ4,^Cź$i]V._֖I&YΛ6c=k}) -==]XQݞ]v;wT뷝a)p3 nly@~ʚ?z9f̘*osvھ}j͕PWWWQQ1tPӿLsI8)--ѩ8ɓ'eqٲeiiiޖo=zTʺO>DFh6?EV;3rH`Ò{${/>>߿j~z/,@#yuJV:u,U}]8~hѢxcǎτ(ƣXĉӧe~~ q3f@B#|.쟦~8uðfƌxzUPPƜ9sFrh)$ uqI}2,!!!66vРAwAŬYԯJE_nƋ/СCJ:;v=99Yn9-8$+۶m>|,I#<1湜75@D9ր$꿚hM*/^R1B2pΝe!Cz4 7 +к4F1Ø ~&w4F1Ø ~-B#p~W JCP(hC8"4w4F1Ø  p\1TB@àFua㊁Ƞ c$4ڦO|r{A 3 P\\x/[oemFĵmO>\SScnZ Hh5<<󌵥1##cÇ|NkFOn% Hh۷[lwȑ>L_ti̙2^zSTTxũS&'''%%M2NPPPx7CcEERRR̙nZCcqqqϞ=ooذ!;;[0x`EϿlٲ}{%p3f@B#|.\ZZ*5pYkq&M$1. mĈgΜǏ˾Nyyy2IMMDQF͝;WO:%ڱclKÇC,G-//߿… 0 ۶mڵM`ɓ'6LϿuVI%ppbJCP(hC8;xҤ.][$@FGGKN:]|Yϟ?/#:G:5VVVFEE[lIMMsεknŊ.\ЃFXVV&/;^[N"ꫯz<^%OVw޽{Q +к4F1Ø qhi|uvUٖ סCUVɶ$CY[[G7&HLLITڸqcnnn|||NNή]xc?[h,**z߾} NȲIDAT.]̟?_$ mN#n[NW uQi b1jjjܹNzNLLL76MEۯmAԦ~ҥ)))lrӸ~zFi\xuZ{h&iϣۆh]TB@àF|O (6oܶmۃغuz<̄ $D西hl3fL~~~UU9ݩPzIhhhhlٲ4F#492//b ,$~{7ocÒCC=ğj"* A`Q 0f@B#|\.|tQIVmrrrfϞm~3RSS飾hlэz*((ƒC&$$4H&?4J;v웜} 4! Hh\<Ox1f@B#|hW D6mR)QdggWi`Q 0f@B#|hW DoϞ=F c$4‡q@dPi҉ 4! Hh_8 * ;[6 4! Hh|͚5\ Hh3 c̀F1ׅ+"JCP(hC8L p\1TB@àF8 * A`Q 0f@B#|hW DP0(p3 >|+"JCP(hC80X\\~ҥKuW&V;[qGiiiݺu{T{3{M{TO@ JCP(h;guIڸFhtڵk][`=tW-744؛L<׾}~zد_?*oy[#+&* A`Q S'iZqʕ jL6uKgCށٳDzk+SOO裏Z~IP穧|m~Weeeuuu?Ou&_:g?YRRRΝChjLe ߖ'Gk޼yl2=rѢEǏ`~gK.=zT!y޽r?y7-jC/q; ѣ]v2~ժUzϘ@?ƈ * A`Q S'iZqݶmqM0A64j?%HvFFFEE>P+{'WjVSO<&& ,%mI) ZZ_aҤIj79n=)q-ZTUrOoj֖@. 7[^t@dPi bpn?IwnZQ觶|ҭ`'N {PݪW h4F1sOypC3ދ8; *fG @b 24F1sOypCM^>ZA!( 4{3s2̙G턏q@dPi bp򌽡F^\0q@dPi bp㌽F]8c/n9Khۅ{3v㌽FuaKI_rMOQ뫯w⊁Ƞ ={ qz0q9^]z-{Gŵ%q-BxԄGqʕs޽{fpbJCP(h;g%4ljll0`}ߜof'jhh79}ؖ???ȿ}(+к4F1sq^B#|>ZSPPxoٲ%..ȑ#QQQ}nt̙3ҤO>EEEN/^:ujrrrRRҔ)S ƣƊ'$$̙3G0-..ٳ˽KR7lؐ- >^վzzꬬyGƵg>;v۷+⟙?} p.7Qi bp㌽F8}t\9ulرCkjj>llܸq&M_˜n6bĈ3gKKKd9ġQF͝;WG76FY^[[[^^޿ afmֵkM6%'O6l_q b퉉ǎ[jU.]ʼ-Z&-]Ɖ'b._uX9r~C }i Ih]TB@ܹ8c/>N_qܹsڵ[bŅ Z={6::ZlvI$66VVVFEE[lIMM6ߚ?%K lu$"aٿuZ|iվ{nyj[Sc,}YݻtҬ;wZ;-LF jlowޱ}1v~!?YpbJCP(h;g%4^ƍssssrrve>ݺuzlKСêUd[LR[[kҨ#VbbLu8џ-4Yo>^zX.]ϟuZ.FFosu0Nkֵ}ɓ%n+9KhS__tҔ(IA]ZDGGT7m>jl,//ƳgZG76ZF7=33sŪiIaF9]G0a7|S509?4:NV[{3F'On߾Aβel#7oܶm[ $e-n*H:UF}8f̘*osʆF#D7rȼ.X@7_{޼yMMM^%{99Dmm?'!!A=4H8q=S?4z&78tPd+ 8c/ntJ)))!&66vРA *EB1''gΘ1#555>>O>꫌ƚثWi4h$5;VMNN5kv5?^6m$Rֿ8x`i߹sȑ#jl jzz 4iIh\8scS?S/4_yy5k㌽Fu&"l޽ٳg>pς+"JCP(h;g%4‡wx\x+"JCP(h;g%4‡֍Ј;3==}Ȑ!o|+"JCP(h;g%4‡q@dPi bp㌽Fua㊁Ƞ ={ ppq^B#.szٷ^z_QZZji5{WhMpJj9{}C#W UPi bpOj!nAsCڵk][`=l?b{\ * A`Q I-^ڍ;h!ƭ[8011m۶ w}եJjk*ԏ>%[MW\yӣ=SO=uew111+++O~ҭ[7]w?ϒ:wVBSc*[ToL8Qz͛'˖-#-Z'=~u>+GT7-&_Zdu=~ U'z~j7W^-A6tP=66gΜ^lf'7WjmVSOZA!( 4=Kh,+W; q@dPi bpb!R1#Ybbѣ9bA.Gu 2W_}}`^x޲wDwIǵp9{@P@ B%4֘q…S>(\bJMMo~cmiؘ1`Ç9S.* A`Q Y,^B#bƞ={ڵk{/^(12999))iʔ)uuuٳ>`ǎ+[$&M_˜n;bĈ3gKKK+1Ych?A.\۶m[׮]7mڤY)ۓ'OӤ7on۶ֲ[na&L ZE/Ecq%7kl4Ot#G\`&/ݻyԟ'1.NW uQi bpb!Ѳ%oũ5553f̐l߫W5̙3ZlOO{/33S%Iv<%%%2,!!ABݠA/gΚ5K"믿ZReΝ;B=z1$.vXIhk;{loH}Q_e46Wb|F#D'oرorr1z:Eϟ9M&Ը$B#\8]1E!( 4=Khh6[bVTB@ܳXF\\+`E!( 4=Kh0\+`E!( 4=Kh[{ ,B/6}˗[/&''Ź<֚3S ,B/P\\x-+Ybbѣ9bOyyyTTԁA $ੵXTa"9qiI555 ܳXF|]'xgԶ5b]paԩhO>T \P/5W\wB%4"t*5KLL=z#G샾5!C-_}^iz-{Gĩ3/bcc?kqN>nY,^B#Bg !.\:uj~샾.`o2Q37nhlll0`}ߜi'7,B/>}]BӮ];{EIIISL`i'kfGm^:+++&&frÆ 2ʜr,l^bqƍ2g? >ql8p 11јl"+?rHTTg}/]4s̴4ӧOQQSq%@V.G~Ɗ'$$̙3Goٖسg˗{sܶBb B%4%z-//oܸq555IF5w\.N/uh8q~e2??~Æ +gРAK,uX&)Ӳ-Jկ~mG}kI&M$1. mĈgΜǏ:5Wr)Y;d[>hef9zmmmyyy.\޶m[׮]7mڤ9m++,* A`Q Y,^B#|Bh\/gϞU]QQQ%%%-[RSS.F ZvݻնڵkWǎ%ٜ# oJ^w!W%ҕw'Ǖ%ۅ:u@+ϟ:t:贒sI|]bŅ `c-4cǎ֭giIsܶBb B%4'iMMMBRRR->!aMsT;thLNK1Ǐ9-FF.Z{w~aZɗ.]j߾ѣG+?|nݮ^mLj*o˛vi4dƍV%~HU㍍l'z}I,út2|Ӓ9mvE!( 4=KhOh-[(,,q9.//.}Q;xubyYYY^*{%kjbΝ;%D͞=۷sc.-ol_56D_tk6ZNNw=33sŪiIsܶBb B%4' r$,}k̘1UUU}vo}<7))瞻zjiiiNNJ&`lc~T≠osMLLT'ɓ'iR͛7mVBoY[0&LUK}h\ɓ'eAeҜOȑ#***`=LN_|ѻwy?Ob\R Ю@4F1gz -W~;..N91c$^z1fR {/33cǎC-,,TĘXmszbѣ%$PvJ@[ٳg{=8!)r"I@B5(p]Vl+l0[Ev{e]E.uK,Ai FrBY $|;pfN8Bz;w39Ӽ;9h4IYY$ITj4 }C$3FMMM7\ޢǏ˚SLDjF [1W,c}ѣ*g1FY.,pܸqݺuڵkVVV^HxJ' 4=%4"詧ҷU\ 1ţT<`Q Y(A3fɑ#8T<`Q Y(AN+ v7Ghxumݺ>@#:4xB@ܳQB#;v̘1_AȍlcDO(h{1JhDPׅnݺhW r bpb>F u㑿\ܳQB#RUUU\\\QQap%qb>F rg1Fh9ܳQB#0q@tPiF1g174)ҾO=6mڴiomO}k~ڴ[a;iuƸ(45㊁ bpb>F4Y`u:(iaW"gҳqF,;98_hj]!hW DO(h1i>FbqǨ NV*--UMt7R~XBVַ6?c֭[CC/b=s>C퓮|+J' 44n1Ƹc)8;wnVVV\\\ff#u]?+)SX'YFܐ!C>3,((αcqzj=ym=r}PQQх dW mٲE^e8PeCۣFvaaԩSipc-w|:d9~_iKu$[nӪ3))IkK#&&a~J#//OZ*33Sfhcǎ>uTuu6 .c|QگBCh޽{%Y/]tcIʡGQ$O~_ W[Ejj^SNƥm%1>~YXX(+V(**ӦMzԨQJZZtg?={R6 ebQcA1Bϝ;裏ffffdd<áٵkWJJJǎnZ[[;츸bʡG?ѿ뿶+MΝۣGR޽{'$iW . |\1T<`Q ,u14*8UVVX[6yȃ+J' 44n1ƸcT'`k˶h"cW >ZA `bQcA1Ro>k˶~cW >ZA `bѨF\hW DO(h{1JhD_>* P0(,c-{1Jh=%4@|pb>F A |ă+J'Q+?!!>+FԊ|G @b :4xT0P(99;ܳg}.Ν\~l:uꔛ;o޼ }'|er*=%4"q@tPiĩ`ɓOKw5+w=yۧZpT {1JhD-㊁SնmbccS$F<555,4ݜ| +Wi׮KҥK qmF Aư'NLF hTJIIILLF=9*++cbbڵkU63ƽ{Z7eiwo{Ҹp8|8'JJJڶmkQlpKzg1FX:PI~۾}{Ν.\vޭ򤒜ܡCch t][Ni&:{Բc);W˻kdwC2NӣhܳQB#Z,[***vQiWTTȐܹӘmNa%mҤIwT|-4vllUؼyL>턝YD |bbd9sѣGO8ĉ.//߸q4<(|Gݕ~;vܱctXohh(---((HOOW_Pܿ[NڇUӴ?-{1JhD_>* 8L轻_~9!!RՅiiizZd3}tK˖-s N?2n=Z\F9Νk;O>d.]d/_\A Lk%pb>F A 5\\\\UUe|G @b :4xB%%%7päIkC1g1F>* P0֭[222rss-[zbpb>F  |\1T<`Zɍ]w]׮]w. ۍG=%4" W DO(FF%;;[xܳQB#Z:_իז-[ʸg1FW {//o+V~űrb>F &*1ԨQ9993fpyj+|\5/_ޭ[Ln-:qb>F A ())ݻE=%4"W DO(֠),cЈ >ZA |G @b :4xB@ܳQB#hW DO(h{1JhD_>* P0(,c-{1Jh=%4@|pb>F A |ă+J' ޓY%''y{OܹsIJJԩSnny***.vZ,cЈ >ZAcM\'O,((ׯ}|:{uݻwO<9==}>ΧW -{1JhD-㊁%m۶SNILMMMIIyjjj$XǷi9sVPϟW+Wkn[V4hPii>u_[?~ޫ˗/|ƍW^^z>i[Вg1F>* +kN;qɓ Gǎ[]]-qȑfͲ⴩C dgϪ͉'VUUC k k֬IKKSzk߾}8v옜=c8UW -{1JhD_>* +RRRѣG#Gʘ2vZތ))4ݻ!uָ8B())i۶GٲeK|||a/MOkUCK|Ju…۷wy…jh*O*:thhh0Dxij8Gi\zu^^5\#'Rn}}}^iz{1Jh@+eK\EEE =*튊 7;wZw)--Jo4hL>Bc~~}' 9UV͛ qfY-{1Jh@+eirrr̙6G=q'NH|ƍ8xG9oJJ~Ç:YBcCCтtu}Сo]`;i> Z,cV*/VWW%&&kɒ%j.[L6|͞={v1//H4cJ '4(0`ֿܹOvENf˗/+iZ,cЈ . |\1T< `1 -R8вg1F`b㊁{lڴ{ӧOII} -{1Y(A|+J'Ƃzsrr222zIbl%Ъg1F>* fӦM_233v:hРnzppb>F  |\1T* P0(,cЈ . |\1T<`Q Y(Z,c-{1Jh=%4'$${=)hY(A.R\Bo|{Oj>B>N:Λ7>bjsXb^QiF1g1F{05b}g&M8pmN]]7Q[}}ݻ'O~T B#W +* P0(,cЈ -[ڶm[۶muʕ+srrڵk'1رcƍKJJܹ̙3U\tj>Mӽ{8ͺ :b^QiF1g1FhYM4i„ gϞ#GJ:sLEEE͛h9پ}]v.\8b۲ңwuh;v/Q5k֟Ω14Y&--Mz}ICp=2NZ9W +* P0(,cЈ -RK;xo,//͏?Xm+]vUo۳gϖƅ zQ\\wf^S2&&L]VGA-4u/f˖-4= h]14xB@ܳQB#}])bw%gΜQD>^n?|ӦMi=z466vժU{l޼YsN^N,KW,c?b l555ǎ͝;w?ޱc;vÇ;6z'8q"J7nܨ ] ==]}Aq2n:i:tHN@|Ai|Gji> pqb>F /4:K3fLRRRjjՍ;L馛tuO>DgS {duwSRR  Iw|.]^|^YNOe\W&,cЈTUU[C+{1JhDׅwQPPлwV4#* P0(,cЈWUUA222wNb^1ˁJ' 4=%4"R]~_&1T<`Q Y(Aҷ̮HB?ƈ* P0(,cЈ3f䨠p5|֊|-r 7dee/w* P0(,c;vL:Gɍ=%4LxU=%4ԍGN#pUpb>F KUUUqqqEE}=%4" W DO(h{1zyCرcWqٻW DO(hK&5O74=y\Ǹq]+J' 4ADfisqrF\]z){8 bp㌣Fua㊁ bp㌣Fh-sq{3Fp9" {khIy{8 b??4%A  c$4"q@tPiF1D˜  |\1T<`Q 0f@B#hW DO(hC$Јw.pA  c$4ҡQ|ߗM6mڴiӦM6mڴiԶȝFh 2f@ϡM 솆٬W-Z?!ԄYfY7/bZGo~%%% <U賵OjV|]@b :4xB@"a΀ VZUZZ7ůk\fW>C+>kZ"j id>S̜9S999QٻKr9͂W DO(hC$кac6{5rH1bč7hڕMjjj!++KܹsetٲezA+orrw]Ν;'H\\\ff#ZA  snؘw0gyF}Wg}:G56l {w Z]qR6Ѓ>(矗=e5o9S`_{5i+ dsرzj5j NNo/uրLhW DO(hC$кacgN:ԡCdi[&_ׯW}%Ih\`uY9?.mM IێzF*YYY~ꗗ`ߤcǎm_cuu귮Nj3(zy9>* P0(H3uƼC5k4fϞmc/$ǫ~m9ƣ>R 4FɓqÆ &LɓUZsҥ:m8'}I111ҮϬNmQE_>* P0(H3uƼC9tOS66_XBzƍѫڶmnݺZ};t`]JzQ^xᅻᄏM:Eg}vݺuS;w.++KlJ;77WΰH6mZG{!myUQEٙ3uƼCy:+__^ :M㯰ZfYUw2G}4333666##?SRRdM7ݤ;5:*ÇKvqqqIIIyyyŁGׯcǎO(ɜ6,XJD9?̓hкacA?e߾}~\o}Yua㊁ b1zhx01q@tPiF1D˜  |\1T<`Q 0f@B#hW DO(hC$Ј >ZA  c$4" W DO(hC$2f@B#pxw9 Hh*$4JNN;ct(Gr^z>uyM.!ɓO\TWWgrУG 6>Qxsfd̀F-l۶-66V:uJbdjjjJJzhllҥKO<';l\6?c+HDӞ}۷QS =.W +* P0(H3 A>Z:]pABNΝ.\$<$''wСB%M?aFFF}}};r_|1t2gΜNv =mZz%:wQ󍝡l'k.}]zڵ^;g)+O(hC$Ј -[n)**>~7QF',o̘1ojj!zzǏ5L"xJ'' Hh3 d̀F@1ׅ+J' 4! HhD&>* P0(H3 A|NO{k?NMMMHHpy%>6r͵fs\1QiF1D˜ *--̬xg 3Qrrw޹g;v< 'sT}^p ǡC$WWWJ' 4! HhDփ>c{}q *blffըY@II644  HhwW^w5$''W__opKڵk4Μ9}]kN}/\ s .TCwnӘ'9bT .{>\N{СꙫNg6mlNbvΙ3GtV'F4/c$4? r]tiT7VZU[[+7oV )Q[TT$iJh|Dj#]R-Z$4h{uNsjO੧@dЈ _:uؿ̺uoW %Hϯ9vX^^^h$+999ѣ'Nxĉ@cH%4:ui544,^8===t_O1MrssΝɚ~i>}fϞ-;pV'p]w}< b1|AVVSO>d.]|KO>DUOO/rBBBeee5iiir^z-Y$t{IjMJJ8p`俞h~cƌ5SSSOnZ<~kʔ)TgreJ' 4! HhD%?ZUUUƛ|P>^ *.yO(hC$Ј ֎; {]RRb*\1fD  c$4"(UUUbŊgddtޝ@ bO(hC$Ј ׅխ윜]ٓ :4xB@"àF(Y[n]Ø ڲe 7ܠ"_A@KàFIUUsss{4 Hhݎ;&O|]#r#z3 A_7322$=hW r b1`bu㑿@sb͈J' 4! HhD%?ZUUU%@ b1G @b :4xB@"àF>* P0(H3 A|]@b :4xB@"àF@1A Hh3 d̀Fua㊁ b1ă+J' 4! HhDPh ?qjjwޱ]WJ' 4! HhDGޓHh|wٳ>);w>4ԧOsi9m<ebTTTر#zʦM*))SNG3£rss͛'gb|odNW yQiF1D˜ rhY?ydAAA~쓢%i^zy饗dkhw^˙7r555?񏯹}zݻwO<9==}J?+h^T<`Q 0f@B#.l~۶mzԩS#SSSSRRxI;ȑ#wqGǎogyF6RjS9u,Y)~IntSӬ G?9s>|У>}zڴijd}ڵK\2'']v2رcƍKJJܹ,Tq5kʱ\\?gb@ze,2hРR5^=_K0@?p@۶m<p8% FSN.+h^T<`Q 0f@B#X'&OvucǎУ˹z뭟|{a5:a9ٳg_NCϛ7/pJN)]N ˽iii%@VUUC Qs@`W'g۷/\P#(VGok*ZV{{ 7'Diȑ#j2&&Lm]Vrk mKhtZѣK.=y{_?|Qǥ{}%@MKxW$Nir (QM$sZ;}]y[M;:tHoݺ5...TRRҶm[=_uٳgK… 8|+#lٲ%>>>గ 7'{ ۷oܹ… Ր6yRINNСCCC-Ii֑9W>|աC$ =$m7pG}l ͶlKP-`:%aFuN۝7lwmuɜ?|ӦMt5Fy]{QD1}QQdG *CƣtTVNh-*;wEԩ-ܢ;C4,6m/W_7)L.ro~f4i[} c_1BGв$ɡ NGr?=j*=6oެ8M, Θ r-999sQG8q'iƍjjj;~ERRR~H89|СCu*0si544,^8===>HzoT@3Z U~{F_Bnnܹs49p)x7?|S|Iɽ! + 8or T~;vT&D1uIСC:4χ4/* P0(H3 AN&MS/rBBaX]]]XX&W^K,Qs>QQϞ=_dQQ7#O%%%8P.>}Ue˖%Tu-4:uJo߾wWј1c詩rDu锌!tʆ  &$q{Ƙ &ʱ  !VDjzSbltMjS1B2p.]d/_\ q+h^T<`Q 0f@B#.Gr XQiF1D˜ |-B#\+`E  c$4"}@sO(hC$Ј . |\1T<`Q 0f@B# Ș S>ޫSRiI 3 PZZYWWgя~$K/Y; M4>IsCVWW1q >c=fihhѣǀ fbĝ k_$ HhDׅ/\qFkڵkӧOO6Mh߾}wyԩԔxFdɒL+cgxرq%%%uy̙f54fgg25[N`РA^_W&z8]1E  c$4")>|Xjȑ#αcN4Iv1/ [oOw^ש3??_6rYfIX6m }NFQV9s$4nذk׮k֬Q&O82d^0&z8]1E  c$4"%!MjӺGd\\$.iuٳ>~ܽ{YYYSVV6׮]&G.]ɓze?VDD=gܾ}u:%5СC֭U[hCCZ +м4xB@"àF9}B4?Ȩ:Җd(3Ϝ9gw4INNETZz;Pиk.}]zڵ^;g)پi;wj9]1E  c$4"u….]4Gk&qqq76MEۯ;+**lAԦvѢE;wdW_}zQ{`5tJQ4I NW yQiF1D˜ rԩSշo~mܹHaƏ/!JE/Ecѣ'Nxĉ@cSҨhxtN41"??رcs$~}={)ƻ?Ո4#* P0(H3 deeGJh0t3f@ZXXطo_UFcguuիג%K,///)))>>~7QFQߘ1cdӧi*?~\֜2e$R)FI%i "TQQQ\\\UUep3 6uTn<ˠO>3fرc} @2f@B#t˗/֭[VVmݶb n<1pxꩧvz-|#p93 A|]@b :4RQHh3fL^^H@"àF/[n{1f@B#hW DP۶mѣN߿3* 6:1feeu~F1D˜  |\1T$1JVзm(hC$Ј . |\1TnvkцF1D˜ rUTTZp93 d̀F@1A HhD_>* P0(H3 A<@b :4xB@"àF>* P0(H3 A|+J' 4! HhD-㊁ b13Z'r=.pA Ç^6e@QrT)3`OZBy3g;%3`Q2g-ݛo)wޱpUqOyQB )8ئҥKWٻW DO(h;g%4^Ш^X`uʾO0Yh jG{}ѷOGrF0@4xB@ܹu]?+)SX'YK_Ґ!Cd@:m:ʤzj=Y+ɍζEQs4+aÆ|_p႞YHF`m~/nݺ~zٜ6mZ66nܨfjAӶ;^ubYTTdIz; R1J' 4{3$mA04S Ԑҩ6G:ӧOmk5ڌ%ƀsJ ONNǏK[X'K[tJJJRԩSKC`Ldy̙oQ333ewSNUWWKcǎ~kjS/11ĉ]t0%Cc;^}駡1o "<`A =G~6EOOjH}>֑ӓvyy/~FpN)*f ݻwo߾ =OFuУA%.i<ՍڸFV^|E7!g?)WX_z%cжu wL[&{3$mA04{SLQChriTsY4F)%_zQ^xᅻᄏMӝ4=,===>>~́_]bEQQLVj]9(~p.\ '//>s=SzuжRWW!ڵSNmv)C=ؿA﯆Vvz}SOZ3guIڸahT7%Hd5}T#C{c9;wǦ% }ή]RRR$nmݺvqqqIIIyyyŶCb-Z\<>m/g>|N"Ц:~mϞ=ַt"k{UNW=@[nA8Z3guIڸah˗/UGlZ'u{aÆk׮ ,F$hJ) :{I&Ɏ*] ^vEj&޿oaWKbJC =G# oF =} +͒F|+J' 4{3F[V|xA =GF̙3g"քq@tPiF1sOyQ}W. |\1T<`Q sq{3pqQB#98Jh=G ޓ>w}'$$£^_\1T<`Q sqЈ Q?яd^z>u M}^/֝ZPW|ߴOqŊ7x!}ifpb͋J' 4{3z1`aÆǾ8ajb/w9|ؖ\K٧qb͋J' 4{3Ѳƞ%Kdeegff:_vmBBž={bbb>CiӦh߾}wyԩԔxF`<3Thh|x}}KKLL~^G5V\Ӯ];yEsG;:vx7?3ڷooqO(h;g%4"飥i&鬮S;vҤIҸ$~Ihz'|"{>|ة3??_CH9rYxtcQhhgΜ߿y4y6lڵ5kd)'N WBظqcrr/^{偦ַĤGNG:4N0ANٳs#F/p/!]8]1E =G r+G]tɓ'퓚9r$..N:u꤂eݻw[';+++cbbڵkBn4Fc)Mm+g}633sjC[kWmMIx'Errr6oltb:4JV_{5cƌ+O@ bp㌣F+[z;ا?322-AC/%"gΜNv+99Yihh8we=+KOk̙u:%]D!;h׮]nMN'C^B_'PhUsq7vѢE;w Rtm8ꦢGykN+[݈{W7gϞ ,PNah;?~^xAuPrb6tnwwtVhsq7:8p`ƍuuuv/^nm۶Hʛ_^Ut4|poG8q'G* эFnĈ555ǎ͝;w&/O?ӧٳ/\p8C~9ę3g;))I=4H͟0a{Gthh 87V`l|z/F=G WLDSEEEqqqUU} W DO(h;g%4"_ •zc\1T<`Q sqЈ >ZWB#3229sTVVgp@PiF1sqQB#h$7GBc׮]%=>v+J' 4{3G ʍ7xc&\1T<`Q sqЈ s!e0 bp㌣Fu&)QWz-vm+V٪J㌣FETbG3cƌ;v'{3qݻgffrk=G ?QϿ"-{3ׅTTTsk+J' 4{3ă+J' 4{3G @b :4xB@ܹ8(A|+J' 4{3G @b :4xB@ܹ8(A|]@b :4xB@ܹ8(Z g%4@k㌣Fh-sqr-O~Ç!?8 NI=G/oh7Yaa3gKK7n8{8 b;vKRsq%w}v5{7U;l+J' 4=:BcFK.\fFA |^1jGhT&,X`ݴ"im-Zdpɏ^A> W YPiF1'5n?wG[yh\jUiilj}j7֣?6w]􋒣7> W YPiF1'5n?wG}&''m6))+_oTP)Q.ZY^Ssfeeeff>#gϞM~7ڵkׯ_򚚚z(##C_˖-NҥK_WSDz嗥g„ 2:{li/^XϜ?~BBw޽^pIDAT'xBnZ:޿Yj…"z@pOj>F~j7+W8y¼<=>>giӦÇOkd_|;ߑ??_ګW֓Ǐ/Q֣(jJ{111_/\g6ƨQ+X۷~/˭[MvWM:պmA=uݸ>#4 I?Ŀۿ}D~\sҖ-=ɒoӧOΤ$կIӓ+++ϟ?/ IMG35gcK3gnF̔ݭ3O:U]]-;Zm_ u:7ٽe6F2sOj>F~j7SLQA,$%6/1BUhTiw޾} 6X˫O}mҺrQ\4W@Fm\Cj+NB,uI^{R1Sq}GhСT~QĘ|g/ڿN,گ5^4ݝӓ7o,EsssWXQTT$Mf[9(~p.\ wg?ٗ޽{zT91 mTE* P0(cv/YvٳgAAA^T0뮻N6oᆿk4FY(ܹs>hffflllFF1A8vJIIر֭[kkkϟW\\l[9(~mѢE?ϥ#O>)ޝGU' Y0 $H@Av,O@"Ee"ȣ"R*T֭GYj6 @ "PD dUBiN/w9{ ayםsνsܙƎg(pʶolDܸqpT|`Q `u< 4.Zm۶:$=gϞ-Q?m+SXR/Ǫ1R`%##'wbȠ b0'w+hxb,|7qDX58yd}c#24B@I-] y %m۶ǪH>o߮} W dT|`Q `RYׅ+"J/ 4Vmh1bě^#aNjF64-H9裏QV{|}dNjF*4GUׅ+"J/ 4W D_(h9%4"q@dPiF1XQB#BxjWgcO\1T|`Q ,`Ј J%%%{ƍId=jk׮N|O~X6K/"NO+c8z/^W rQiF1XQB#B.l6lX"{PZZkO;^r%3'Gj8z/^W rQiF1XQB#Ll1c山zСC#SRRzտ{*>>]v<3MYsvLSOIj~6m45ͺAUʽylR۷r^^^RRk[`ƍcbb>_q2zE]֫uOJ ۷obbbjjQN8SaÆf͚=%&ϝ;uֲ]tUPmXQB#L1C ~ֿ>}IիѣUϞ=%>|k׮*Bvv)}Y\?+SCx:wqD5KFx]1E `bF [rвeK9_?hРE=zȑ#QaC6dzzzI\?ޛf<]w%˷r6j;Ԯ][R2l_lX6lXO棏>r f*W x|mꫯQC@% b0gFx0S-Zn"cccCILIIINN:t#GsCs=Wm_nٲeݺu%Թ%L%fffSΝ;uߥK 6{q}711155uԨQN#5kL^w'+M y]1E `bF zjyܽ{iX2d%\'߿O>EEEz5zhZjy2ѣ^kܹS,Cׯ4rKn8Cl\BÇ :t0~x=MkܸD\5u;J7(u*_(h9%4"%iM_n$(!!AuܿLL̖-[ Yײƭ[ڳgOllԩS|_#5I^jٲ4hЁJJCkԴmk;v‰'|t纾Bٳg9o߾N:7NOm~mڴ3f25\?3ΜQgee9kTyW4h_1bDZZZBBBVLCZ[lILLܹs<4p ͔;z_~)u뭷JRu+fs 0JhDghfvԼ8YŜbŊ֭[6,//>,`Ј. @xyeddts+"J/ 4$77n֬YӦM * P0(s 0JhD?L |\1T|`Q ,`ЈZA `bF  |\1T|`Q ,`ЈZA `bF A `bF =Y,(9%45իYy19%4"8BJJJݻƍNWuEmk׮N|O~X6K/"NyO+SuՇ9%4"$ׅ!Æ k߾}'Nػܨ-Tnh,..ر%\b;s|>*J/ 4쇉m!d山zСC#SRRz ĦMfۂ?j_nٲeݺuWZ%7ΝۺukB.]6lؠȺl3++K˺y_wܑ.]tڵk:]DL2E\]ff>Nиo߾}&&&5JoіجY~cd1GeFQiF1XQB#B=!C ~ֿ>}I&իѣmxԡqѣGA?~[n֭#x:w>>''g֬Y*&s͑{oԨ3wޒT|d(J@[ܑ#GE]'[l( s/Q];]9dnAMII.XOS/mz뭒H]w)cjȜQ;u+VfY,( gΜѤI48+XQB#BP}جY.qƲ@b`5A `bF yꩧG[ظ?eȠ b0gF9e˖*(/ƈ * P0(s 0JhDgСCvvz_y7lx1FdPiF1XQB#BOwyYYYF6#24B@Y,(!/.%76mڔ@s^1@ `bF (G^^ޠAw9%4",Չ9%4@0gF,`Ј. |\1T|`Q ,`jC/)7,W姞zʵeYfe2M,Z]Yf.|ֲްq]A(5㊁Ƞ b0gF> * P0(s 0Xĉ7]9-L^zl/ܹ,Z2κ^'O]U<+"J/ 45iFM1u- Ngްaܜ]F |Wj73g~7oM4)..>}iz'|}Rׅ+"J/ 45iFM1u-Wpr;v{ʪSNffw}Q/um߾޽{9,##Chb<@rrrF^u}JQQQ\\\<(7 &?wdB?coa֭[;tРAI&Ymf1-)Ƹ NnMzw_Y[ϟ/Y[n_}t6L:#ۿY7oܯ_?ɍЬY3(Yf:uJ&~,Y"aƌzz:wf^{,1b~4"YL 0j1+hF?))IzKYʲX'Kb⋿kՙ5mz[Y)zd/d!''G͞=;33So366VxY>tPQQ6 kb ZQWQ^Gh<ի'y:ԩʨMZ8++K~U6luSe%16m/ܲe˺uZJoйtN:cǎ.vQv;wzͷr3Zn [۷޽{;:ͺU 59%4" W D_ 80dȐK.D߿O>EEEz5zh*dѣGW[vZ}ҤI={4ܝkh?~ZZZ~뭷m& -\wukN90gFD-d!;;{jh111[lQ7,Xkhܺuz.n1c©SsssKĊ+j׮mQ,YWⱖ0gFD-$Z*55uҤIjhݺu*O*III/..v Λ^˜ J o}:zf]K~8o޼s9GVW1ɓε\QD=s 0Jh@ԲYfIT۳g,ȐQ[fkJZ7l$  * P05ʍK/?xY,(@XYYYVZ-Y^59%48˼;*%?M6ݺusc dbF &:1k׮y?O ?ZXQB#$̌ >ZbbF A PNEW D_( 77܏)s 0JhDO-㊁Ƞ b0gF> * P0(s 0JhDO-㊁Ƞ b0gFua㊁Ƞ b0gF,`Ü zXQB#Ds 0JhD_> * P0(s 0JhD?L |\1T|`Q ,`ЈZA`zjyڠTRRR޽7nht:ʱcw&11aÆ:u?~|AA}n^!XQB#BxjW D_5q"D`MY,(!<+"J/ Ɩ/_G:$12%%%99yСG`WiӦٶn~j_nٲeݺuWZ%7ΝۺukB.]6lؠȺ-oV3f̐dw^Nn&V Ĝׅ+"J/ ƚ80dȐK.D߿O>EEE{5zh*^7uh0`~QusРAǏ[uȺ-4Ο??--M-[۶m}\wukNQC41gFP*q%'''$$Bvvݻcbbl٢n.X@7הnjߵklٲ:ueĊ+j׮mQ,YWⱖF1gFP:q:ujժU&MRC֭SyRIJJ_~qqkJ NS7](O͛s9Ψ{IZ(js 0Jh@ eK\f͒dgY.((!f*6lW7~m]SbؿʂUllٳ?.7/^`iM"XQB#j([Npղe˱cǪ_A8 {]h,ܹSVٴi#&''?'O޽{bÆ KOOW_Pܾ}.\PwuW-k59%4" W D_)\|pW^iР䰨hĈiii Z2eswY>}|77o3k,\Sb8Q;;7κ=XFdgv:c xM9)fbF ᇉ+"J/˓צM+VuPXQB#BxjW D_\ ҥKFFFfH5k1(,`ЈZA[ 6,;;[bӦM/BcÜS @b 24 FآEƍKh$14\=Y,(!|]@b 24"dƍQØXQB#āƍwdddpWmۖOQXQB#… {ꥢw]r#js 0Jh@t~xQCXQB#… o&F,`Ј~l@b 24N])fbF ᇉ+"J/ 4S @b 24B@Y,(!<+"J/ 4S @b 24B@Y,(!|]@b 24B@Y,(9%4@0gF,`ÜR.7hիWKQ}~VbT|`Q ,`Ј`?LR\R?njTy|Fo 6ԩ N:v}E+_(h9%4"$Sw6ĉFo'O\nݐ!CӷonjAhE `bF  ԲE˗׮][-[[ļ}711155uԨQ*IN:cǎzk;vwiݻv=3:4:thذa)))C=rގw73fn:!!A wރ*tڴiӬj`W /* P0(s 0JhDH5i5!whUfĉff͊ɓ'eYu+'OV?Uύ7M;u8w~8׿~{ߋ+% utr)kOC8GXS_Wjf>M6c"@dPiF1T{ްq_ٳ7lؠo7xCnΟ?_+|?66^z_}UO㵮+5G-[w ~ۜ8q#vڍ7OMkntޣ'|( kꇉqRs ɹ{,۔-3"2GdPiF1T{ްq_7fVz%7{yZe9"YYYqqqƍӧ Z?9;;;Nw[';eguiѢ[vСA&Mη:vؽ+*fff}GUCj_.R]`[o}{ZWv>#sNF^ujNKJZjYNpc;MӺ{Wueg/ƈ * P0(pπ6+]|g俯gQ˚ ,|۷oA+u,Y"4cƌx^z;wNv.6L#iذ`l]VDWԷ1Uh9x+#՜6By… wySKVX߹|%͓'O~Wjfq׵@ * P0(pπ6+]ܵkׯkmm9sw~W_}vM4Qj\WI?.뎬#FN:Κ5kwqG?O/Zex웦~'##gx~Gg}VK^oΖQ9^Y~G~9v,z}߸v:'O}]ӽ{Zh\W̙3'===!!aܸq*~;=~ 5kVNĜܒi߾}||R=ܓ+ RO#QmO<,[h۶nк0_QF^F++կYf{x];T haB)#@HI*7lW(3qDk?Pu&O >g@ ϱl۶ڏZ5uD\5b jPiF1Tkab㊁Ƞ b HhDO-㊁Ƞ b HhDO-㊁Ƞ b HhDO-㊁Ƞ b HhD_> * P0(p̀F@k$4B\3 BÇgիWرcq ?:oWʺGk8GW-]PЈ;?я*6m螯ZRDٰaCff'UӂR qn͚57pCjjj|||vvQoý6~+t>A$̇~XF_z%ce^KGj׮])))EEE\3 !.;T[d 3A٦;jժ?#of81v}g _ݠW[tiBB\7Tސ1~ ZY%Rr|\mKر%\b+eXZ垅E PЈ`?LU-[[U7}훘:j(ն5'~X&=z<#:N:thذa)))C=r䈚6eʔL];g̘ѺukI;ٷ{?gϞڵ{gʽGԩS-=R}wB-̝;W<.]ON*E߱cGڵwYmTgyW]uydHNz{qx<(˵>6mQoG2WD,X@vlƍ111|}eyZKԻYO>W^oZ PY4B@*5V0ѣ2W^~:t?~s-%4no׿դI<>X&߿O>=Z:eLxeY֯_)zm۶ɂZu]⊛nIIKnh/%rW)r A ?~-vdW_}U^kת&Mt]kUڵsJۭz{qx]Si~^\^O!dpi嵮y j-uSR8gA$M`W /* P0(p̀F{j7/t֭[ͽ{͛7s̑7iV7x㡇lٲEM^`AZZ,ٳ'66vԩԛrYdI\\\Iٮ-6ߣ _uwrgK vR˖-SZ8:f̘OAsss~a;qs֭[g}Vu@kuMM |TfLW]uZK{'˳fjذ8\z]+;L.{+Vbݣb~QiF1Tk$4"$S델V}sڵrʕ+msٯ_ .`ӦM} *uRRR;y#!!{K.U[9sdu i gRnZ J$o}T\sܽ/B:waTl;u@kuW7͏WMpV+sezdˌ SYh*_Z׼.yU5'84B@*5^k]߰꛶^}U'έYDÆ /b) j6Ǐ]iA%]deeIT7v=裶: ^5YD[v-se:YR8u=L:uԟZ[ 6Ȃڷ~u"]14B@*5ZgsZϞ=ﯾשSqƹNӬC}U,_ :p@IiPQmǎp yO{un߾]6~s׮]W\q_>pGݻfJ8xݝrkȄ AҥKNzСҟ;}wu@uU_Mܴi_x]+yGNge;Ԯ][22rpd_p]ܝ "99ڦM6mz7lx1FdPiF1Tk$4"Z|u]c&Mȍlx1FdPiF1Tk$4"uacFFFvv6bUJ/ 4"\3 X|y>}w KaaannnAA}@p̀F@k,?4h'/Hh>Bc~~f۷ I5jQ05{+ &=kWhΜ9A6m/3GI5jQ05{+ ѕHUƯ~U):9f~>iF|5 4\F:6Bcvfe95C('riUKڙmyl W`jH3EhѪc#4V)^/ig_OQ34_!|+FXxf~>iF|5 4\F:6Bcvfe95C('rW߮Uֺu۝Ck=t&Mk׮-{H Z7rEk v4EN Uھ^F .nUݪ _m}Vu3x=?&}vv{~`Ty ֪ǵUiTG]mKVG|+W P&$$lٲWw.YW E۷G[v*tSG^I~y׿m\\춺) oժlDzQy*\ɍ7l:usyw1fϝ|5"&&&11c.zyΙ;P^-/oA/33KTjj~p[o-QC֧w43EhR| ֢ZT^_V#~snr[% v&$$.]uY؁[8Od}p[oCO^s9-?-Zzjé9O_8iS]woӦm||wSif?7\YYun\vu([[⒈ʗ*\44{)SfQYYMʭC۱^ ;'=by瀞s.K(<խ[oa6Tb?JIIuʍ;]tQO?~Sx*y":m;4kv>DV'֦6%gvժM#y |vma)Bc2^V]ʋ[~^_y-*:oh~OP,𛭴y"[j72\Y[^y#nݲS`5Sn h|tr+_Dvv~̙˗K믿;v섞=qNίzxzkƢVZ_yeoP۷%/U]ihɹDر_uxX=&LxP. u|'=\,Oz9rF7##SnO<_􈍍=Ɠ&=Zvkεds,;ٷM_3cKn{jԲkQBf.]sXqqqW]uO?_x/mРWO~2L K/"PXzX?Iwyigi>Xrw<`]zY쳂-Z:zB*=TT~cbGk#n<#z?6n8)2;ՍX Uעׄnytգ\{?^4lͰ3u׆N{m֤&LYVo|8u~ȶ&G)11饗^gFP 0عV~К)}8^y Gص {JW[KT$BQII&Oznzz A5u}6é9#'9~ \0ZRe>ѕF 3sfnӦy[?KNn(~ժMoT5mʕ!CnKMM\ǟgSOM+Vl2M K{?Tѵt%eɇ#r>ԣS/xcҥҥΖÆ!vONң>>^{.O:uȦdYe)k~۽N:_C&H9+P-pDeIDKDWjPG}3V 5LڎM%3VO*WI Tfއ r'xV~r غ@yٲy-Z!)Z?^֟Z'JԷcǿ7o\̺{>Zy;wKCTҚ5nٲW&;z(/y_&H_}ik^ׯ?_Z> Qˮ:y"6k}b]k~O?|BBg=tsY6o !CFhR˪kQyqzׄ4˼W}:>;xP7׭ٚsg-'|׮gǺM7oV vuV8gѣ<-]Z[K7/_ykkKr[p^jr]zŵN.Is!ugzEfOLǾ޿Q͹Mé>2SsW \0QYQRe>ѕ322W$yFNk낾;z*̗^zMz$IAt%11INی/ /Vi~^&4)Gy\e}іGM\\~Z^vuHHvOv?]_Sdd#raBӟPz(-gszkI_D%J=snVP*rmРvWtOd|y=(;{l(UzYuMO4]ܼ>'Γ\"/^<&츞ku@\fkΝɷ ]6߼Y](OleγpȲ\US)Bbccʛ`y֍q-^r[p^jr]r5k>7g߽+RSӤ8U]o}s^ḑ^J:&&CN \0QYQRe>ѕ뫬&ݻO=>sPBqSN}G?ꫂuSjmBߋzLk_~yߒ%y 4hBcۡ6l|߹j{W&'ڹj<ȹ6vݬG\n$˳]$=S:eC#4V)e5]tqkBeޫ>OQ?Byk<ܚO]ώu^oެnƿ#c+p^GٶOzǖ]R~?i xu|eo;}=ֶaN9/A!LHHƁU*緿}6˵Zv|Z>`\T+zR-pDeIDKDWeY}V}Z~,O͟}.mƌj;KSMN6hm?񠜜K?3; 445=?O}^%C=Woʞ} 6kvB>}G8$vLW3̲muor[π{T+=Ze]L7}~]ψy^!?R'4V1e5kUD-* U/^ǟپs[6?^tm^խ'|ngǺM/w4Oo[a,:2wwF)[hnm۶ϤI4p΋m;aݪԻ彍-~>(5Nq'6KW8w5?SZ0[֒֗*\:4ʳ}]ΧPÆL{/ZOС#$/ԾzbHƍ3TIyͷj+Wnyڬ]>'IOڲ/Zҫ ⛖8Ⱥ^,oZZlAZv/55M\p\6Ϛ<^xP7Y['pg'7oV7ٙ{eka,p=2pȺwli„_V}xc][ \3xC:^5߶3{]?)VH| T7Ĩ϶ëW*W Ϭ,o޼}{ɩIm;];n!s \0ZRe>ѕZFgXǏ$O1a+]]7[[/j=Q.+U_6w#G9cݻa.޺&|_\@L{U_ߡ']}$1c0mO$rzbX ?ni1qhc7ӘAunذA=gq~$2r:eǎ5{E9L8 O^J/y_UW,b\ݷGu댿>.5#YVJXT.O"1r+:g{xI_/N~ؓң(Eg#X4~#dr_s$rM.:tXqq/R^JIۃ(( 2ujTmm ĉOY,AA˖%_u%i՚6n6 (!brr ~mذo5'FN+Dcg1~rUU}C1 r. Tu?%s#/im~Խ2 e׌dMf4,9;&fn~NO":"ʪȎ9fLhzz6oBLWFVN2ꉊzBH/Y֯2=00(%%JيqX"J`B];s`,f !jols$rM)Tw-V.1SN< :zyړӧ? ٮ䰰I57wP:vNzlp-[t/j)Gd1tF6[ 3ccwO#k\VVe97nuOƳ N!<| u*;M*:۶HK2D|bӧ/B&P^^}(Z:: Es,\Kqq9 %$9'<ӕd,rJLmΚ2R>mk-G 4,ɩF5PA+hKDiuUҐ1!;vخ ge0~3DeeF޽{W=[h%X.gͰ[ $Ql&BſyyyQ#w%ozE %%+6mScǟ}||ؽ! ϟ[,h]3R9Riidx{R OH11sy[;Wx^rKTꪞq%R),2?{ ҟ5QBxm/aytj.+7)8pQE%%G[^Jwh֬GycmQ# ˩ʕ0T*.ޓz)%JM]Oz;u U+9}yCi>C٠KfhSyfrQRN":0xicx%9C]@6y0IEkMc4okTf7DN|>LGKdڊɗ[EEE>/&fx,!=cb'<,0ӡ`ۣG&'~M#Wxݧe{ c&t󺧧W^ukuWx7ᶫnZsOv,ҩ jLZZVpp|NO_ސ!B>EkMci,+bȭ[w>JP5$-.Tn:bm刋y yxC!=cBJIp;L#th%X.,b9s[Yh w9ry44޽wܸ |*?p.'<=>3JVZM |_;2F챥EE60;;_|~rw7~]XXJH5 &u&M7:^ "sX$kZxԤO…JJ*蚧 ~@7P)YX a4C xzzGJfzMQ* 0RgڊrDHOϮ=I;67AA컚Zb!=cBՒx`80!IVs⋯ ?q^ӿ)'9Ʉ(Wc@@v񙙹'{x 6-:++OQVzz\BCʜW(Jgzvw^t9U%; *xJjz1'-Yq&:x% "sX$kպ.`usx{{y4nk,giLKHf]#%jM[Zѱc3gKt bߘ䋡]N3&wKK+Ja0 cigt;:zIEJJd̘'rD{=zm߾;,loc0 ci ya67=++j]#"L㠈Ȃb 0F'XB>EFq(e]x,0L/,2p`CA}!sXA*a0H9`,X Áiy|ޚ^. W_ׯGc4Jʳ{T"""o/˙ci EL#tHCkkOp{uNud'Ō H9~6|Z瀱H4Ȝ" `:$ ֪Uѿ䛽7o.+g{xIƁ%j:E!ƲnSXd4L4FDD*@4->8188svv>#dIȑ11swr$`IFZ2ujTmmYZZ9eʴ!CB&{DÞ;VkڸqA VeLIIEdtUTdCBƸdBIۃ8, qdZZ&5A /[t]Ji Ӯ| ڑٲԩ{CEL#thƎ dQyڵx.++Lƍ[Y…uӮ mgTU}S[{r^xa IMv!BOY/=[6|ߖ-jj:99ʘ)_66`sihhHNN0X ZRSbɰt$&Cnn(+ trK%rU=RjcNja?ǯo>|o]]*w4LiiT~-`; 7m^MG*7y k7n-zopNųKdFOPe塺JM3i ]7I# 0ӡewʪx 7 Tŧ?иu.룏iiԺRMr.J{//]!ς}`?;kQrZuHguOO!-i ]7SR2F?`C4ç&%}7&aѢGٚggTRRArr Ftsv%dhX$pTFApxmvsng֞;`KZbH稄X[ިIOA x΂F?U5ZZW7eUi0iiZL7&ҥG_ba]]7/kN~ pHɁ==wVL NLL\6;4kӦ~A}E104i L#M`h՞tfIENDB`onedrive-2.5.5/docs/puml/onedrive_windows_authentication.puml000066400000000000000000000046111476564400300246410ustar00rootroot00000000000000@startuml participant "Microsoft Windows OneDrive Client" participant "Microsoft OneDrive\nAuthentication Service\n(login.microsoftonline.com)" as AuthServer participant "User's Device (for MFA)" as UserDevice participant "Microsoft Graph API\n(graph.microsoft.com)" as GraphAPI participant "Microsoft OneDrive" "Microsoft Windows OneDrive Client" -> AuthServer: Request Authorization\n(Client Credentials, Scopes) AuthServer -> "Microsoft Windows OneDrive Client": Provide Authorization Code "Microsoft Windows OneDrive Client" -> AuthServer: Request Access Token\n(Authorization Code, Client Credentials) alt MFA Enabled AuthServer -> UserDevice: Trigger MFA Challenge UserDevice -> AuthServer: Provide MFA Verification AuthServer -> "Microsoft Windows OneDrive Client": Return Access Token\n(and Refresh Token) "Microsoft Windows OneDrive Client" -> GraphAPI: Request Microsoft OneDrive Data\n(Access Token) loop Token Expiry Check "Microsoft Windows OneDrive Client" -> AuthServer: Is Access Token Expired? alt Token Expired "Microsoft Windows OneDrive Client" -> AuthServer: Request New Access Token\n(Refresh Token) AuthServer -> "Microsoft Windows OneDrive Client": Return New Access Token else Token Valid GraphAPI -> "Microsoft OneDrive": Retrieve Data "Microsoft OneDrive" -> GraphAPI: Return Data GraphAPI -> "Microsoft Windows OneDrive Client": Provide Data end end else MFA Not Required AuthServer -> "Microsoft Windows OneDrive Client": Return Access Token\n(and Refresh Token) "Microsoft Windows OneDrive Client" -> GraphAPI: Request Microsoft OneDrive Data\n(Access Token) loop Token Expiry Check "Microsoft Windows OneDrive Client" -> AuthServer: Is Access Token Expired? alt Token Expired "Microsoft Windows OneDrive Client" -> AuthServer: Request New Access Token\n(Refresh Token) AuthServer -> "Microsoft Windows OneDrive Client": Return New Access Token else Token Valid GraphAPI -> "Microsoft OneDrive": Retrieve Data "Microsoft OneDrive" -> GraphAPI: Return Data GraphAPI -> "Microsoft Windows OneDrive Client": Provide Data end end else MFA Failed or Other Auth Error AuthServer -> "Microsoft Windows OneDrive Client": Error Message (e.g., Invalid Credentials, MFA Failure) end @endumlonedrive-2.5.5/docs/puml/uploadFile.png000066400000000000000000002771341476564400300200640ustar00rootroot00000000000000PNG  IHDRҠ*tEXtcopyleftGenerated by https://plantuml.comviTXtplantumlxUn@}WxD$Mi4@Eck텆Vή8\j\x̵6Lg Z",;a @Ρy"Ka]R^ f͘FHrЏI O]"+o0) h/QwH=N1V|! `^e{‘b|ÌBљ] ;u$xo,xR|hdIJlή&j6|B*^jFLrb'ؕv.ex X^]%/_!gye؂͜m"j}iQHbexUXo2L2.c hjw^[cgzA/TGb,.Ṯ.ӛ0 Q:i'̥ k?^lB8 x&BZp75xWb5㍲#~7QX.]0 J}& ~?K&&}sG`{Nw9, }+FlZ1׬(+*_Qa"%pv5WtʣՌE3-" rMgnQƄ@=;=_~hOR w8K8^t%L'=pNо @K"Q|f݌tGbXx419Nɰ%܊WRa?x'7à_N2d"">7jIqT\,<}_oIDATx^ixU aU쬬!.^D@&"20#qق(nс)&( $,WEIfI:M}@bxM9TuyOZtA7r#GT#BPY* @e?,G`^?ƍǏ߿;viږ-[̇LjӦM?pkyTy&CSTT(,\Xi̜9S-\PilܸQNejӦ͎;0#0k׮=CrbThs?Ynm6y_LLdܸqrYĉ}0#0}e;,۷o/EtM??L ?#0ue;۶mD]CqQ%..H?sٸqԦM;v{`G`:3gΔ+g .`G`:EEErh,… ԦM~a8#0#0yƍǏ߿ǎ;u4`^{m˖-| ` G` 6M`zG` G菀5+?k?VD@XX"##`Et:wb΍7.ZHWls(((M(60&0M^?]+ujl6y@U[hcWu?_ )/+*DۛWx`gm@ա?&u9r˗/:gm@ա?&u3Gmy۶m{[ndd+Զ~o^Nv-]T駟w}wqǝw9rH0M^{{~C YbEѽ-ǥ9_93 :ujQQ-[ޤm@ա?&u3ǶmnذK,[eˌ3mǍ׬YR-_|;v5j6?y&}=8}课*''g۶m-[|7m5V;T#`BZ7_\|vĉ۷7ԩSGԭ[A.;0n믗>}Z+Wv޴iSN#Uާټyzq۴2޽Gi֬YÆ ի'/_^&}Ntu}\bZЭR;baa[!>>^ڵk 0!c?,U9={rnnv˖-bז?:F3n?~gO˿d׮]z/Ǐ3Oi0!_GQFJ_;gΜP+Vkr6Mzg\`6y„  իgI%GL/0tИ999֭kڴ̙336mڔ׫WOnkc͛oܸQL9sLZ&L%vG{Wbf͚~3foaN:qʕ+!>`G|].K/Լyڵkh"118믿իWݺu#""HѪx㍰:uknɒ%?8$$^z<ȧ~GƟW_{+V? }QΝ x2GLc`6GX #&DLH2#DLH}Ӣ?&쇊qF@||<Ӣ?&6E#ELvc]L5RC##`B~H&p8/(P%L*%%n/QKUENgLLڵkzڵk=@QC#0e˖ݸv{-y,[fi| _}<@UPC#0}+$$fEGGmV(!ZrrO>駟#<"Pu.-->7G`}l| `6~H7&G˗ϝ;wM>f ?p|@GkN>}ҥǎsYM!/^gee]tIa\\>yذaz|#|菹5j8rrժU[Ǐ=]VΝ]m۶EFFTozjiO4ڱcGӦM]7'ڸ؛K0~H U?:tF˽{6hРqe+?>|8((M5;~t^}fee 7o.((عsvޔo_BDQC#xt4HGݺu4i69..gs peGAebܰaM(x:{lС'O~߯kȠ#FhG'N(uLĆv… bs4d_ɽ{V} F>ocЭ6oޜ9dȐ u]/ҥKǏ0`@pppvĞׯdp8Ǝ۬Y mv޼ymN>=zhm' jz̘1ix-Z5k~ko_k`@S [r̦ef@DQC#/))r }~{WfB*|IIIr [FFFy督Gv8 31[|뭷X|6l8p\wǎOX|W\YpwK.=u|{+SC#[o3ʴiӊ{X222?ڻwʕ+X.N8!W1KBBX.^py9-]t߾}-OT#c$H?KMMMNNo@p8222H>o(@s |G|#v͝={ue??PMeffOZp}r۳gϞsΟ?t,~HZron6M2Jnٲڵk5l6y@USC##`B~H@LH菀 )!X0!e??k?&G` GLp_JQZ J(!TJJn_kI9ΘիWk/h۵k'{**G`^K,q?-ZSTe??JKK YdfQQQba˖-TUAlvڅmV(!ZrrСCm%"""ω'ʓTe??Ss:;vdhhh^^< @QC#0aÆiqjUGzmBBBT,o߾6oF)\p?7oܹsm6)@ |T2e??PҦMx6m$رcK.:uի ~HΘ1/Kp&Mo>*ٓ'O?zҥK,Y"?v)!*pɓ'?P~RC#U`̙|~~/R~QC#_ʡgϗBGM>@D@ə7o>G… |4wܫW" ~H>NC@eڽ{-[ge??WϗPă'?/(!7o\ZjɣQ=u֕GVU،34iR~sW./Dܹs2f֭kܸqvt &IyPvTM9'rssQ8 0\ N*C#*+eܸq]wݵlٲ/:ίjȑM:J^^\ 6nxȑq-ĈRW~~[b9<<<88{#6-((~]vOxⰰ wx񢧽 III}9shޝni_dee=57Nڙ3\'龟RO;%]q')))G)DU1;;[&~Ii|b_?{쩍1'\F yfo:}X޵kWÆ ]%]ѣG9۶mtWĚ5k~z5cƌǏWiV?rcǎM<5qGZn}T;;s挾ӻ32vWZQ;L}+]NRڏ+Ny\=?JG)DU7,i\jOZ<|FybСCSl%w}WZڻwy֭[7,Q^=[P+u x*Io?x >m\ߝ4_;*IJtz)OP((e?(!*?J;# ڛo?or9sf͚4vݼyoUk֬ю%6ܼysAAΝ;S~OUV&.^EYBOUΓJSeT0ԣJGJ7nشiO?ToFkco6lѣ?am:thnnn^^۵=h֬ڵkF zgEػwo(bAAAv… #F322ĠǥNʚx)v=zL8d/B,\Exzw: en&M>Ǩ\'龟RO;e\F:NJ=J菨$~HQزe 4֭۲e<&1vf͚m۶7o6FI&FҶ}{gݺuK]FF۵kgW_}U hb֬Y/(߸qݻwO1covv|ȑ={ibK._6ӻIM033sȐ!b?wuK/t%9F:Izzr{( wgz2QI_yIyGTe??WCUg+?O@7noѢţ>*OP+---,,,$$>lv=**J,|TUAۻw֮](y*G`jO>Dxx OPELtFGGwYK$UD_WjUGzmBBBT,o߾6oFn\3f1ӧl6S^a&SN˓/.Д@ur&N2M6C&#*Lg:t>غukS,1.MTk.G{Sw=|sĠ)ňJ̑7n\JYTTؿ[ Z1Gu@E)!jE|4e'Ȝ&ML:{-[LMMu%66vڧQTT /<󅅅:7b) +~H:r+Y 2'''((hÆ byʕ=zccc׬Y#L2rH&eHLL|b/WG/<%Ȝzi˧Nj֬Xj׮/#GDFF7d|qp:uԺu똘k'b+$|BT/. ֗n1"""%%t72?|M<ifРA׮]gĶ|#6nF\Ušg̘!qGmܸe>l۶mqymmڴXؾ}{׮];t ay^ YjQ_~X6mڨQv{LLo~/B(==׿qDlسgO)>h,{0G W|y3oaٌ{P-+++88\g ?ڵرcb/$%HOgv{֭CBBZjymfً/_߿GrtҥKb[%P1~HZpy/vUgϞ-^t%--M?A/ZɓO?ت}qqqچqPUdd׬Ycǎݺu36CΜ9#G*Ʌ;w{۷]n5Svcǎm6SrN:G$nפw6-$$䡇?O:tH)jrqՃ>ۊ=/QC#E|tt+V & 6L?}3?mk֬і,YYrX3v rX a$.&&f宒C >\`쏥}344455U)m&An>ٳgOáOݻ&[mV//QC#oҤI?=zvNS,kycbbڷo߼ysmf:ujxSl mNz=|s6?zhc1t?qS?V\aaa֭^~O|7](**J.n?D T|.kҤICa*5w%g em:u\p;*)y.Fxر j:T!ӧO5j--77_ Qڵ'l"""rϞ={%}O>rnV~HZpy qJJXNHHB9''N:'NNׯ_PP /^gi^N:Unݼ<}[}6CIg .]_)Sߝq۷6WRT,mǰ0e˖/瞓 ?'@uR%H ̛oҽ{7xýйJfmttP5k4X}VZy3v:<ݿFڣGRߝq۷ooӦM.]Əfdd[[vc->t:kݿO>xSTT1 e??PLR*>^'b+qe??PHQ?X61S[+ QC#ՔYio^x/,,׹sL1^ MT_.d%g|%&& ݉bI|)!jE)GݻOw}ΝX#*#|NTw.Ta|Ϟ={СψbYqAdbb|sl̘1 #P%fC5RC##`B~H 7N[v8͛7rXx#:wܡCjsΝթS'1.v(!XB8~xmW͛唔Yv|m\_pǎ_~=""7JȻVZԩ/..~衇,X`ۥ0e??kp~mN֮]8:u\z։|yy5 QC#(/<<|MV\\,]vaPXXMسgO֭n5SRR"##}ҥKGܹ`1x^zƊnݺmݺ߻p8vعsʣEx̙>}OqG`RRZUBRRRvŸXPULtܹS{G޾}{V (!W|||DDDjjꍛnjG ~H敖jv{BBB>Sy*G`j}l;wnѢ< @QC#0J<{y*G`jN3***""BKBvv< @QC#0kW^jUGҺuGy0e?? ۷fgQC#d<\.Wbb<+9sf{74cƌrHTS.kҤIΝsYĦM!}H@uZ|$ e??P~ADuGLmƍӖGͯ\"/^8bĈΝ;waڜsFEEuIm\G?*;A4idԩݻwoٲejj6K. w֍sQC#wkկ6oެ- tѣcbb:w,~.X@ ={W^b[n[nQ2GmAiӦPqZN+(v:vX/7I Sg`!~HƎ;w\y .;ڶm{+ZԊ]N>]oSL7nIVVɓO?tlllĶ}/OQx}Q洉'v%22RD]ڷoǏk֭-pVڞjٲG,i&}/,6Vƣ$&& !N8ѬY3átʕ:u_s$`i~HΜ9ۧO~I^.> ۷_|X0a>mYG{Gm޿5khK,ӖueǃG\7rJ,Ρk׮xLL~Çfzdbg>+V^jX_LmX*9ʰaôA(Nj޼ٳgoYghh/I$~Hl*ԩSkv:b9==]u&ᆵURjժ5ep㈫dZz{q5JsAAdGG3foK/׭[O+? *7LIXXٕr%$$9sF_:}tQ/n!CVZi/;;N:.\+M4hqvt=U{6.{ۏ]bŀcǎ5jHY 3(G,LY\%ۧJ^p8g}vʔ)7vOEˑ/^ܳgJ?~W|ͿT cbb{g\%QnݺڟKJ=+O{6Ǡ (_yO>GA5h[͛xË; 0o*ˋ}6mteeGa5k6:::66V5kJctYL2dȎ;ڴR+>lVܿԳgcl3.,Y$<<\&##wRyoxwg3PC#ery b'cƌ9s?)!+q:111v^v{Z}7\.̙3צMVxwe??`17nV*ުUG}T KŠŤl6ޡCO?T KŠ7:::<>Zիŝ(!KJKK֭V~@(!۷fIwG*99",,DՕ+W֯_`ϻ=3gδloɈSڲe˵k3 >ʇwRUeSC#~9k֬tlڴI w3gN|WRV~l e??P^:>ҥKr1?O_~6Zߺ챁@r8'N|x&M7ns\/KNU߉6{B *L ,//W^ڵ}.\u{쩧r8]t3gΞ=;hРǗ=.v>l0^zRHG+ .u5,n&xF.iJ~K.qqq(͛´UƇJSr|{$zzXA)!&߸իnF233 5j8rrժU[k֬r|lvG^n۶-22qm?6z@ I\|O1;;;x6t<]ROۺfǎM6urǽ{6h@[e|3x?P,oٲˋS`G|b 8rС5j/$k>LQFwyg;Yq$---88X[^z?,Ճ>m6hzVN֭W4G*˅ 8P,9r„ ./N{,T=esoF҈իWk,仰igc޽ڜG}$6LLLG=zbG|ϛf- IwB *L^RRRaac쭷ޒߤYŷ. =60e??{III?clΜ94eG|o˖-iiirnŊ4eG|ի ,{e{C~f`ߺؠ”@X`ٳg$cASLߞŷ.+>6e??P)͛'W Zvnߞŷ.+>6e??PY֭[i&9XJvv7fp˺ *@Td˵g,"///!!K_|cRC#kٲe|iL/;;;!!AߏX[W`<6(e??P8oɵƬ֮];mڴBX.uc)!(**Zvӗ.]z1ܘw}xiӦݻW~Vfo]K~Hrss/_>o޼sιm6Mq2> .g@|{+l& ʦG,fCYX0*U*QwVG,J e??`aT@ŝU(! >rM4@^ϝ)! &Jǎ{תI&w@XXuTZ|<{Xͭ& :Ye??`a_QSMdY e??`a]cI}gHp5 "(! J*3>j;AEQC#fJp8RRRNQS)A8yX7G,*nOOOu|x q?3::ZΔrge??`aTve˖!!!w_!]匏28Jrrr>}BCCVZ%0+ YTs~H¬Uv{[l9j(!]}M6<8\hh YTg~H,W٦M[-[jήp|h nvr,-e??`aZ_ ˗/b9wjeLIv{z%3gfgg'$$9sFn^>xԩ={FFFjG ߶m|*|AOرcG6xZUGwڤ-[&AGv{VBBBLq&|*Jt\^$GAwQǑ%!yHF=SQ*A¯X*H1 RHJ%!bB OlB͂Taiއل陝Gv$#' CG."HG9F_1_G1 c?` c?hn!# 䏀2@` a~H# CG?d?@!GOYVC%Th!#SE{EX=\ CGl6[||]C-X,qqqbU4YfEEEm߾+bMNNVh!#_۷oX,=zV CGF>`[oW^j%0?$~-''箻 o0l0O{ۼyZ%Kl6a~H@@tҒ%K/_^SS&X|寽3!#jΙ3SS+ 6cZ_z饪*5B0ڱcN @rҥs>(~aaa`?HO<TvsQTTFShݛvŋsk޼y/_V CGŋx 5B>}Z@s3 6m:pJ8qĪUn47}5BK믫hn!#!;;[<6mڨa;w,BCCϞ=ٳGV6lsLܥK/pn@߲8oYѣGzz( #޽۱SN~ZYxƄ7|322R4t!}S… 'M$ O>SO==tD?d? R1?uR~Q /ՉovY>eʔ￿r„ gϖرC Kr-G/5ieW[oտ:nܸgyƮk۶mXb׺|=Ltu0z創2ȳ_5ky睢#o߮uZAO{v\#wowXh!,,lڴir+;wvmÆ *Y揮Fi?Xceeell||ocǎ!!!?eS3gOs֭sZA?I&7xO>YSS#Ϙsω6{h"xZjl2G!asرZ 2 .\P)gyVKyn47߫ٳg;vwu3ja~H@`زeKaaH8rիn47pҥ,5Bl2ժv gϪZW_}U0uuuj4 ??b!#dÆ j:VZZv?1aW3*ڹs?^0<+W\jT!蔖 t_~P,SSS/\?1T?S~~~ZZڊ+N8WLGywϟw^%a~H@+//_zufffF ډO>V_c_ 0Ea~H# CG?d?@!G 0?~0?$C!#SVUPa~HTnnbGQ.j4)sNP-K\\XkU0?$k֬YQQQ[nJhXbccժa~HWaaaDDDϞ=nnX{GU4ѣG0 222<<|@fb?`Ȑ!ߙ3g45֯_hA j%0?$n֬Ywu | CG dȝgb?0zp<p<n/\0;,Xꫯ+LV__>%F CG`ݞRYYi͛7E)// 2{?|"H CGpMA>JD@c K,yj/^(ϝ;7eʔA 0`ĉNFFF߾}(ŶZ;{,,,l޼yIII111;vƍw٫7a~H@ R[[… by…gϖwuWAA\͝8qXlm۶ڂt&,++kժզMr^^ްadyBBڵk«:uT&@p3hYf̘QQQ'O!FlӦ+6/_5jTrrrVVbQڱi( ڷo/O>ݽ{RݶmGo7󓓓efk׮ݥK]|޸qq4-tM??e3hqƌqFdϿ|X>,.\PVV&+ٳwrn>>#}{.J?޹sg]8*//OKKSKعs!C  uӦM熿^~XqڴiZ} 8zRMM $wqGBB(:t֭[ |,)) z/tNw} nݺP"HW#Ae?̘1###C-(|kݺ޺pB-GNӟ j&''UW5#""K/M<~u ,[Vb'NG2tPǵھ=t-H"!!aȑΈ(|qرk׮|Arr\sUgIII6lӧOyy]riii۶mϜ9#kNʅ/2""bǎ+D+!#,,>,//oӦͭW$4onXG /C#F7nѢE۷oׯrVٳ C;2uBScvܹs+**djvUWW7QsܹDѣoG*!_~tѣGO6Mq(9qDN n?l6 01(0?$xSa t? cƌ={\W%%cdFd | V J&2^T?[@ILd?~PuILd?~iILd?~]I0(0?$xSf`P2a~H0j?TZ{PDAA{ CGrss-P?bA$cpcP`>/&68"`e,XϟO bAjҮ]`K- CGW̚5+**JFr/f111'NTjL0!66VFrPھ}`VEc(,,o>ѣ@ ޷n:1NjH J۷oÔԪh -FS}1@һwH J}=zZ d?%''gԨQ ^P+̚5K D رc0VB#ol}ѣꋅJUU6"ٳo߾y/5kw!gƍSWo=ZHb;Ϙ0?$xQaam&ȝg4uqȐ!y!#O@r~w 4a~H𮜜p<oȻp.^cɒ%ab% l?hD+EÑФhDomB73 /\0++k߾}yfȷt1^hQD_\UѲ5WDvvE[ڇ<`?@ptRff_SS5[^{mËn^ 3Be,YD}-j3gw}/aoذA̱SEWW{?`Dq&v) CG6bޘRUUN+qŎ;E'W=!D< CG*.]3gᣡ?p߾}آ{=h &"Jk9? GsO]j!# OG9l/t~(N:xcӚ"DhM׺t7j Nε֔6=G/<ݻwwI.={ڵkXXأ>ZUU%|ȐCо} sW^JJJ>#O?MKKK|AΝ;O:U:qlXD5c:iz:?"h4!tu?^^K.uukƛ|:gdggGDDѠG;ٳcǎ_Ν;\5oGEE3ͶN7Mma…&M'|򩧞R^><{nȑ|ʔ)EEEee fϞ-7n(f555<{ZbF}w8]iQyɢrQQQbbK/$'5(++GcY;~Y_BфEzw?CԩS#FpNotzum۶M,abȭ!ZzeSC;wrsO޽{֭[7ݹO4ڂ'OGAE*ʵ(??JWhË$]믿~ٲe 51DES *Ƌem]==o [oeoh|gCMII{G1ӮݹsLqAÓJfHyQdȑW+85ru։7ի" CCÆ 3grsոnkwt1l*',!!aԨQZhlz1ѝ1cF;tЧOLY sEifyW\Qޯ_?I:vx7>b_\?w),,lڴiqw{LqA0luWɓ'Ǎ'ЫSZ{:gF=zcʭ\!}{1Urm>=o mٲE˖-JZ#]gyVK=pyG/… F222S t7֭[CBBt2}tժrFWAٳ;v;r䈺=#[l),,TpKLsssS &љ1v$.]O@6ֲe˪S &љv"\Ԯv5(VVVGP__?o<$Ltiѱվ4xC1<222$\߻wz@fXDV:$mEd CG*6lؼy:Ҵ4/55Uto$Jڽ1 ٜ:NmmmJJs>ѱE\ěx+Q !#+VZJPAiiiJJW=k@:+W5Ӂ^~Bufٲ[.55… 䢫^k-Cq?e?@駟VXq uْ9rdٲek4KtxEzUW76!,[xP_yyի3d4pWO KG?Ԍ_BA x0?$xE|Ld?> d"uL%c0(0?$xS}~AD!#+ J&2^T_aP2a~H0j?TZ1GZ,~/Z! CGl6[||]CmoX_ϑ?0?$xŬYv󕩾b8qZ|D!#+ #"""##w)=zزeZ|D!#[Fջw﨨(1Տ[V#4a~H𖜜ѣG7߿_T+ϑ?0?$xf۷o=dQ[[V#4a~HYfMd?011QL Ah"]FS}<?0?$xWNNN8wOMd?@ b,X[iiib2z CGh)vܹs+**>yf˟y"HMd?@`oApD!#?{ %"HMd?YdO?-V7|E|ܹ)S 4h'N,//u222;p@Q.[p( ͛cY;111!!aܸqgϞz#D!#7.\ .={,뮻 rnnĉłfk۶m]],4>Jn"Ȳ21ݴiX6l,OHHXvXxWN@#4a~H~gƌ?uɟb6m b`Q~QF%''geeY,;ᣎ}rݻw!n۶?|h& CG;,Kl6[v.]tu|ƍkvGN#HM73#Md??3fLddƍϟ?b~8|XpBYYgϞ޽{e{ǏڵZj?#}{noٹsgΝ; 2`M #H ~z NiӴ CGGwRMM $wqGBB(:t֭[n𱮮N-$$$Di0ܽ{'ºuﯮB ]b&2͘1###C-ݳѣwuW^}UQx!C'&&گ}s%ю(OltSNO %''c®]sm=J(^кu[ou…Z]K8pD-H- CG/ #GuF입ߞ.Ϝ9+yOL U W b|8vصk> 99Y.;, /Qվ/2""bǎZSD@KFh" WZZz ?㤤oސo^ou릯ooЮ];foڴis d5ǖO>ݶm[}?W]]]Ϟ=7lp%w;>uf"06gtVkHHݻ_-A%t6۵kW]]}u-9sZGWN8ѩS'%4|= ]v}x8k֬&˜1cĮs#lo\eb_S6boLd?{a <~w뭷Ο?_+=zHJJz嗛?<6Erǖ;wr-gv߲վ.]!|g z CG|gD-ᣟcoLd?{d't@%4|ى u|f"0=;d@(ߠ0?$;GQAO3a~Hw5{  |f"YVCeW[dI%,Md?\Ţ=DX=[XI%,Md?" |Orת ݾ`!551JY?0?$]3gΌႜiӦUH=GGEĿ}%\ Eb?0?$uÆ 3XVЂ1JMd?𺜜~7|wJZ0F  CG^gbbbd DGGsO z1G|a̙2Y1JMd?BǕ111S#F  CG>"/=%(!4a~HGrrr|{JpQ>Ch"*--MĿ `ݮY%K2=;@(8U?qEE?!4a~H6oެʹsډ 8l6[fffzzݻľ}.\X\\`&2`c DY~kƵjjjK. CG >߰a}g9sXVC"4a~HΒ%K~i,7|sM2eРA 8qbyyѷo߁r֎ᣗ ͛cY;111!!aܸqgϞz#d˗/}OKUUU)))D~D!#;o.\ .\8{lY~]w܉'nm۶ڂW DiӦMb9//oذaAʿ֖o馟Cyyy{U{*ˋẂD!#kcƌܸqV2~_|Y, .\(++ӻwo~biiijic[8~xΝ^1W>|ZhuַzA&M:j[f,߹s!CDa>}6mk[WS"H ~z ^iӦB\b7oG jjj/^h&2_͍_Čt ?Ы;DСCn*+{#|S\3L [4JS7oMYqX{'NuʦN#HW#>}_~ hch^&2_1cFFFZQmO=TBB!CCYxb0>>>11P|ҥNӟ$KNN:s[EGGoN8Q~pС=7bBO=Ylw߭ZzaՄ* ?|oժU24Gڿ{ CGw***F߫<`>6~b_|d3gBnnwީ\j,;vڵk|ˢ!Cdo,6j?:BO?zXSs={U2񌏏֭￯_FDDh$믿Kggg+|D!#8v:۶mk}"##ew7˚ڵ5۴is dnݺBW珎wߝ0aDDD'NtI[^EEl,^X:d5%ۺu1c:d)1J rE/ CGaAo#\ӪU;Ԓlߎ3f(9{c=& {GdMǣgmXD}߱cjSb\ALd?|%7*R3CY?s='OZEEE/,z!Q~ԩ#FgZvƍKJJjjjL"^%DT~Ŀ~B#t\dh'ٟ/ &̞=[t<*{þx;Y4t;ϑ?%0}Uٌ!`3ګMab=*~駽{ʵ);&qÇMҫڵK~7YgΜ˻wرBWG\$Hqq|xٶmjOdѿnܗv#5k8>k'3DLd?|=W$LDh464ܻwo;&q3gδjժD]a[{@ާ^IdN DsÇ|ENt"W9tF! J&2`>7"H/!|4CY<tl6ۄ _<:thJJ#вIDAT:%%%#GtL}g]v&m۶S~ZX,SLq5:-aQQhVKH5v饥bY̦MQ%Rk}hwF! J&2`>7#"H>!,cO4cǎ7xO>YSS#+W-?ڝ CG4*㕵6"H>!K^u˚!///66V-wdgg+|D!#(77bhEX=oA, ^bbo߾Z,;vl>Z^pyG@h"Md|Ak׮xת \  j%!@nҥKӧ[V'&2tbbbd ?ڵ+**j֬YjU-̙3ŀ"rttiԪXnn#GL p˖- #4a~H۶m[=oXx>FDDɕZ@#1D!BۧOQq--[fZYYY.]R_W CG\}ʼnQQQ111FR+h &1Je8x`i޼yj@PYYͯ CG\ o߾ FVR_~r||wJh{Afff]]9?0?$5󨈈.7|JЈ!&&F""LVZZ[o6lP_K4GVwuL Ɲg(fΜ)G ?0?$wo6n+@!'111 &*--MII[aժU+WT_B4+G0G<)yNjl⹈'ŋs_oρO>F="ZPdXzuyy"?0?$66M|wޭ~Û6oެy;}.\XTT> 733ׯr m *..V_H\R!=D!#@PY~kV^^NQMM{wy%DE]AC D>H4Ć7lؠNw}/ZVte^AC Dsa 2|f" HرC UUU)))/_bߠ@dR!=D!#@0طo߇~Ά[13g@TXX¯ߠ@d R!=D!#@Ν΃['Ofeego\Aڑ 7i&2^^^޽{Ip|r||f" ͟?_T555zuyN D/V_lxTAO3a~HN>/egg_xQ=M_ V DׂTAO3a~HVZu uۂ}כ6mRO~TAO3a~HՉo&/jEP8<Q 7i&2/󋴴={t6mڨ)qY _^ApxdMF*ߠ0?$lÆ СwSk踚ᗗ ]Un^/VחlM{ 7ȇb{nukf1kCTAO3a~H ǼnݺYg}3Ϩt\oE W,KHH,;v)#""DS=z\oarB֭SRR5`ڏ@фԩ(~i-,**?~vtRǀU(|:t ofddB:U{ƌcwqIjbݺu֭[.;ӳgώ;/9w\xFrssRB%ΛImB z CG><}8'UW42e_QQQYY9a„ٳg]DJ/tښ^QQu]m6\VVfX.vy.]ٳA#P4!{&OlZK/N1ba@5^[[2D,oܸF\wɲCxj9$e^xN{w|AqC;w\#^NGJys4H4}x!1FDDtIޟٳm۶=~\}hM5믿~ٲebJ:7tӧ~jYGL(?+ѣLzkWJ~~'unpw0Nk׮]jՊeKRV;sX޽{wǎeDT֞Ś5k?*{*;m:]=Fa j2R!=D!#@`s?:~Q`>|XKN:uEVp]Xf͝w)֎1BL+))Ȑ=lV? {7XIx[{ؠ}\]NSYSS#+..m۶ ;=PDTCGȌ3|o2`lcE A1?$@TVVctttǎV,R749=;ԇ"Jޠ\?oz+< PDTCGcǎ7WGR(xJsc~HB7R(xJsc~H64MKMMIu|hެYS'?R(xJsc~H4M2eJII֭['7M4I#K}(T *E!#@B>| qD@cE A1?$u R(xJsc~H &eee[>>}#zѭ[!C}222:vؽ{w.5c<,..nڴ?ߧO͛7ߒ tĉ+w>Q*"MgΜ馛Ν;'ӟ}Y{]~3dQYYY~ 0hn AKߵk׊e˖vmz{RRҊ+s=7j(.Աԇ"Jޠ\?lܸqobz{~deeIk]Y\\ܰaC}ѣZ*P_o}x.Աԇ"Jޠ\??޽ʕ+SRR \pʎqŽ{Λ7oРAO>Ѯ>֦-Z@>Q*"}]f2x3g^xQlwΝ+..;ر]v>9CÆ +,,۷h9x`&M~ʌD-[ի[nYZv|wk0_ZJl=zUaE A1?$p_NNN||9]yy1cvڣG3++K4qIIIw7nA!|ܹ|%Ri?3+XCoPi.r7nܸ U>?~K.^{-ܒnĎoy衇Dϔq־}{8KvډKAQq{obb|=NګW/ڵkWlǯ[N /]&''狖={߿_lO>}c.[{Sj K}(T *E!#?Կ}N4Qg sϊ+ ۆ;vL)6ټy$ PDTCGpͨk_~YKJJիgd|IU̻ gϞׯߠAfϞGwUVV>O?~S嘂8B۶m?|q 6#_znٲ{ӧk׮]tiݺȑ#>cM6FKiG#DF(xJsc~H.:nin|AN2buO>W^뮻F-O?<`Ĉ\nرcFrr.?Wͥm?**j۶mb{FӧO'''5>):tq c(`F(xJsc~H.:i dQ8pԩS>}zǎv҃Mͻ?Fc_cǎ?7hb{ĉFgy'oӦrrr C0PDTCGpa@s #G&&?`R7492BFǐ2Q*"\uXh҈ @ʘ|Q"Jޠ\?keeeut/FY3LJXCoPi.r𵜜<:L{sdu> &bE A1?$*++vuVq&]t^iZjjjҬYC^$cDL>~R(xJsc~HM81..N 0OLL:t_RL>ziL>E]XCoPi.r=ab߹s-[]=bSMvĴO>b[v+"K}(T *E!# qqq:s ֫W/1k)ZN,R749~\m۶̙3Ns1"K}(T *E!#AtbbbSMBB1m&R(xJsc~Hcĉ{#_j/h1|LdcE A1?$߳gϛ>7|ڷo/&ol,R749sw@mҿox,R749࿲ogxnbg"K}(T *E!# B|w6l̜[%W/55U\P# OTիϟ?/(-Q2?.q#(PDTCG@ثK9sl߾]Cͬ[NnUڵkWVVVZZZaa\@;|(?1+R3#qxXCoPi.r-77CII4Tyyy222.\ W-P;9s'W$jxy;<>Q*",Xzjy9ÇO4ĉrn+--&JNBԾ5kdggC PDTCG@Zp͛U bN<9uT"HԪ)Sb^ٲeˢEXCoPi.raib'tɓ'QKI&>ֹŋٳGԇ"Jޠ\??m4yL7dddkuaJ!\R(xJsc~H?˗/߹sBۼy>,W3P3jCM+W ԇ"Jޠ\?ϬY5gϞ-W3P3|uHIMM ԇ"Jޠ\?LQQa*++rM%iܹrNwyP>Q*"f~C > ڵk͚5rMյzjQTrNw%KC*,R7490!𑕕%4P]|14q UXCoPi.rafΜ9RO={ի'^!{6iDsEvڵ&Aq߂ `8Ւ:VvXCoPi.raF%ܸqիעEj(EvڵG 6߿QFM6~w^\Q8111?.r<3~4o/%l-A\U :N@B}hIKK6lg?~|ͿȮ믿ަMF=O{Pf/xđZ=Eگ>rƍn&LS2KNN~-$ůwx>քbĉ=qk7p@8<~ppQǀAp_}ǎūɚI\_)bh|r۶mvghnP?f R(xJsc~H3Ǣ"7H{I***o]o1bă>xgۇV)..͛7GЗbھ}/ұ/[fXæկ8nҏfpx \qӧOp1A?wx>dmڴ1SO:uɓFOq x_OG}T)_~)[_)ҡЮݠ2/p!;ppvXCoPi.ra&xo߾kFnHJJJemjjjRRRAA.[6o\߾;ϟ/6V^xE?Sπ1۵k{=(GolfFHW7:@f}udwy 7h6۝cq;~ppQ5Gנnʕ+UGzirIVV(Me*y?Ckjx=dK}(T *E!# O9rDj7/J\s3/7nܬY3&{-Zk]W_}UUiذ/?h| 7hwjvAke(xUP3;Cc28Ο0ԡK}(T *E!# (Rc|D4WtմǏ_{Gؾ}{֭.]jKwagϞo믿K߃uݻzZvS+Vh۶y/ӧ4#d0[q̘1v#e¯k47hwjvAa=&O?8T39Ο{*'PDTCG@q?͛_oڴIks3HlWk׮ۇ 3gl٢?nժX= bw񦙗wԩ#F8p`݃.FqSev۔)S{i |8X )~p}n_6mvLlf͖,Y/آE q|̝;&Ӹqc` 4#Ǔ&bV RRRF)9r_f DjIP3FhwvS:ax?aE A1?$QذaoF}뭷)7n\VD:̝;Wo_M6=zq-[l277Wio0{l}4===HπvnƷT;vpo].׿U?4@O𫵪:Toqڕo 'mҤɐ!C\ rϞljbΝXoZ1#Tw%"   $^:uz饗kʃɤxUODonwv!pvXCoPi.raF%D5XW pb-[p!q UXCoPi.ra>Godee5 T]m_7#C&R(xJsc~H3oCjvZf\@u^Z\gյqf͚3Lqj!w%KC*,R7490STTh"y5 T('w?P%%%P K}(T *E!# ̚5K^!|r55nԡTyp K}(T *E!# ,_b \̛7r55SPP JK6޴rJyp K}(T *E!# |ӦM}@+((QK222D50}t%.aE A1?$ݻwW .N<9y .u o4i(3ŋٳGԇ"Jޠ\?… 7o,sɩS8qB`=SL!C[lYh<0K}(T *E!# -X`*|XE) w#^=H`E A1?$DnÆ b=JE\MP& OTիϟ?/(-Qa4?.q#(PDTCGpa0PDTCGpa0PDTCGpa0PDTCGpa0PDTCGpa0PDTCGpa0PDTCGpa0PDTCG||SZ"e(T *E!#g4_vqqj@?`R749Zeee׮]7oެ4t"j @?`R749ĉcccRKLL6lj@?`R749]~~~LLA밼N:EGG+~(CoPi.r HLLa;w j @?`R7497WILL?g͚%wZ"e(T *E!#ϷtA6m3"e(T *E!#'G " E0PDTCGzb2Q*"׀ne,Q"Jޠ\?ξoPX E A1?$h2RSSu)Qwf͚uYqX E A1?$nh6uǏkdݺurS*))~G D<~(CoPi.rZH~(CoPi.rFx "e(T *E!#dffN0A.++kݺӧGѣGnݺ 2DѱcݻvqFX-#M>}ILLܼy-999))iРA'Nr'!E0PDTCG9sM7t9{휜!CWTTƆA#| dqqbYv^lmݦ'%%XBlf̘]CwqGRRhݻƍ,xހf۶m;t:w,}A| .X E A1?$ٸq222VZ־}.]!3<#Zz%n^xqk׮zΝ;=hoz!h)))fC}'L{ȑ͛8q"`O#C4vݤGa%y.[{S@?`R749s񤤤sEEE?v~Sl$&&fFK@D@`2Q*"7?{(9{l~ 4{>H ׯ_YY)w|ӵk.]nZ.顡^z\TŸ`=pw/]Tl/]πcnGa<+**ڴi /tyvQ~(CoPi.r ^4?jU'|+u]GܕEEEm۶Ml޽yZQ4hԩSFUC Ck۶8%ѮQ9C5nXn dҤI|10F(xJsc~HȰ)D$7mڔ圜jGyDl7hb{ĉFhxwJzb/=@<};{ v*++kѢȑ##iӚ?u> G&SWpዩ VT0PDTCG'G#)ꫯt뭷ZJ?>::Zt1c{ܩS[ne̙ZշF2XQ~(CoPi.r ִ cd`*U2Q*" N#LjT+e(T *E!###FY- VT0PDTCG'GF*>F2XQ~(CoPi.r oJҽ#2TG,Q"Jޠ\?@H3n#E׸A* | Lep"e(T *E!#.]l߾]iGh^0iZZZZ͚51SASƍvTX E A1?$P_*66V H9rB۶mGT7qD+|E0PDTCGu ۷ob޾}{".]*&.Sb*͍T@?`R749N ~vA{!wЖةS],&&&99;X E A1?$0tshoӦͫ*w6~x1r-l??br;X E A1?$0PYYٶm[}.u Nii1uСSNLe0F(xJsc~HWUbbng~СCF(xJsc~H!??_i |]0tR=;`"e(T *E!# [haM(7< @?`R7496{$< B7@"e(T *E!#RRRdɒU2pT)~w #CLffeN:%W)QpsET ~(CoPi.r~ʕfZhѡC4XnrkbJ\bEO97LMMe~(CoPi.rԽ={̘1C,P]VJMM=w\m@SәWnnnZZb2Q*"@[\E^5V\\<}"h"PRR2c 柫"e(T *E!#`-[6%gΜ>}?4|ĈQQQ1c u,Q"Jޠ\?3k׮WlBϪU#LIIIzz<Ұ"e(T *E!#QQQ1gyԂUVuqbJ|EeQ"e(T *E!#URR"/ԀZpYf%bv*7a2Q*"@pܹsUPk͛w) Č j)ϟE0PDTCGu`Æ ;v쐗h@/{= ۷UHKʣ ~(CoPi.rԁ,y}ԲsʅL^Yˣ ~(CoPi.r^FF\@hQD]YYeԨQ5_sa޽{lW?<ׯ;9s^hٲ~N'|ٳg7i$??^-RK͑?"dEFhΓ.rk],Q"Jޠ\?*cϞ=?IbW^iٲeLLƍ'N;yM6}O<=#I&F2߻x℄Ѣ򢢢Os=ƽRq4aN,`}5kO>ۇjѢG~饗ģ'=g"2mf 9rƍ_=a={V͒_~esO8o8OjNsru>}wپmڴiԨN>w+:>'=7 Q@?`R749c(bo=U{wҤIX#Fx?^ZZz}=z*beyfZWf۷o_\:IKq^=`g1ꫯvܹlРA5ӑ M6mj|]?|pq|999yj8v_?x~_M6qzSNsQQQ8b[nIʀ!zcӟGb ׯc4fǬ4 Z?GidNl^*ٟ8#<"1{ԩz5k9r\ꔔz͹#T"e(T *E!#:?۷O!Kcǎm۶5jHXJ<裏\kݿ]&%%2w0oH 1 ֭8SNi#G?z7$Ng>"U?MڵlU>:;w4Ycwyի5AU嬷/+W^m(N,4?5u>!:G0˗ϕ֭[7oٜݹ#T"e(T *E!#:?;rpѻo>ѹ%7n֬fDc-^{5.y)p}p]ml؝Y>˖-M[ׯXK`&=!9~Ѻc>?2|,6F5ydA?"dU#{9[ۭhuӷKRunN>y:.I \iU9o׮]* 6lNԑ?`2Q*"@phdp QV+jǯZ;R#l߾uK.5e`ސ\Wv'cOiiibb1c=:ӧOʼnO?zf|V%z?lv{և_vͯaÆ+ݻwk!˹ > ` sfxV>3+Vh۶V4A֯_Yqū9Iڝ:G,Q"Jޠ\?*~ؼy~[M6Im^?cƌ)**_}ڵkaÆ=bx̙-[{G[j%zOCYud<>;v8st~NK_~9!!Acgbֳ$wA6mfyD 1͚5[d/b-i{h|1qƍ[ؽ{w% DROY1YHII9rsȑ|EnjrQFoh>|b5u2O}fvOU8bobvmSLѪ~h/5(Ij6禎Q@?`R749JU}Fot7pX]n Vƍkժ١Cb_BiӦG2 -[o.͍v'c{g|챴m۶ .4|o]׿zlҤXu`_:TǏ_^^Y]Z~~}2dȞKn{Ǩ_8NLL;wZc`czzA?"dJ, 4H:uz饗ʃcknyż'N^= >'|Q<+4m߾]olV?cǎ5虐0{l֓lM ~(CoPi.rԁLyq2YWf[lLnj:!XuXF(xJsc~H?{Y揻vL_~ٳg7r! ,Q"Jޠ\?k֬+dy}ԚC;r!a.N7nj֬٘1c>S,Y":,X E A1?$PΟ?G E˅1%H1ȣ ~(CoPi.rԍٳgߖԶYf%tH*7a2Q*"@8|yUP vܹ|rPRPPrJy@?`R749ܹsŒ[^n>}/b#:~(CoPi.rԙ .L4ɓr pϢEv-z[x={# ,Q"Jޠ\?K'N:u*mԒ͛7/\P.; TN2)12lٲeѢE@?`R749؉'&Mtayի,X JKK#5kEP,Q"Jޠ\?{.\Ș7o_ W roeJ _?"e(T *E!#PQXXk.y=پ}9s233+++ ÇҘȎ;222Đ1T@?`R749BW^9w XH Z2lذモ [bJ͍)12F(xJsc~HH VT0PDTCG'GLe*Q"Jޠ\?@8=@`*U2Q*" "S ?`R749Nx`EU E A1?$p{$T+e(T *E!##D2XQ~(CoPi.r oJҽ܇U2Q*"BZNNN^^q)ŽM\ÊF(xJsc~H!K.۷ooyyy;w^ a}XQ~(CoPi.r M0!66V ȼQF] p+e(T *E!#۷ȼۋ.wp> ?`R749脄={;@rVT0PDTCGݻwJtttllk&w0> ?`R749*++۵kB||<< |q+e(T *E!# &mVa}XQ~(CoPi.r p@||y}XQ~(CoPi.r?{+WLII[*++4hp…+;ŋ;o޼A=k5ݢE_rVT0PDTCGalk֬1Zܹ|9o܇U2Q*"@7n\FFܪ&xx.]\{rKzz֙co桇JJJ=SRR]W¾Ϟ={bbb/O>|p#o^K!Cgһwo뽗Ym۶]wUPP]:)S+ZQnC0,[{+s\ÊF(xJsc~H ,?~<))?u@К sϊ+ fv},XЧO:a~ǎ{ /ޫovd1yGyD:ɗ-[&W^z5v'l޼hC }XQ~(CoPi.rn}rHvcIIIzn$yw>#GƑϞ=ۯ_A͞=>2^G֯_Rlڵ+66VڽaÆzlRonABEEE6mrss:$p<}#1谢*Q"Jޠ\?"1N?XTTԠASNwsdQG O>yWѣGkk4oh0w :ԸqcD>ĠÊF(xJsc~HHl #0pShUy;.z]gy'oӦhTo6mڔx뮻̙3շ ]v>裚\U?29"|Dp>ĠÊF(xJsc~HHlG#G{Ν;'%%g65'OۯZ߾}Ϝ9cӧO׮]E[ouժUZn۶m޽kf3>P#1W+e(T *E!#"  QCP\C : ?`R749䏈d&PAYKڇtXQ~(CoPi.rɘMuP\C : ?`R749䏈d&PG"G\jbaEU E A1?$D$c6U!t#s1谢*Q"Jޠ\?"1jA#ڇtXQ~(CoPi.rQ7D\ÊF(xJsc~Hg4&]k Gc*Q"Jޠ\?"TVVve֭Mc6 Ν;{/w:{Y2,--l,d#d!.aEU E A1?$D8qbllA곉X&$$ >\ #O=T\\AshrWD".aEU E A1?$DϏح[D,h;v}v+Zm۶>Wm"守D\ÊF(xJsc~HԿظ81tԩM6ݺu;T޽,ݶm[h;!Bq+e(T *E!#"PvvwqsX3##CS ,h߾>W b[ȝ܇U2Q*"Ҽ Ԉ911ј6sp+e(T *E!#"ĉ{衯ixn@xꩧbbbD-~3> ?`R749䏈LzئMyB;t jo.aEU E A1?$D߿Me3~> ?`R749䏈X73,X jyo܇U2Q*"GN:tҹU2| 55U&|GúdɒyBbЙųl2v)?_p*Q"Jޠ\?B~~Yx/BuMСC-cr^'f+VYBbƐ':Rs|TerVT0PDTCw9ZjBD3f|rA={L>]@rssۨ< \ÊF(xJsc~HɊRSUUK*..1cxM*Q"Jޠ\?FӦM;s挼lݺou0-[TTT̘1C)j}XQ~(CoPi.r#Vjj*|7VZvZyjtiEp+e(T *E!cd%jΜ9rIW)77q*Q"Jޠ\?F3g={V^?JJJ*򤀫$RSSS'5> ?`R749ԩSoxeff^pAM3222|*?Ũ..aEU E A1?$@_|!oyyy6lk@D۷eK.bT*Q"Jޠ\?F >킀^z%VDLy"@ S~Q]\ÊF(xJsc~Hsj@k21TfuGq+e(T *E!crŀ7**QFIIIK.{x˕c ]9_^䏀ߨM6=u~}UVYYY[nݰa[,kj!Jj&G&F1/6lؾ}wyGo! E\ÊF(xJsc~HŘr1էO>L 63ό?^l8qbر͛7oڴ?~Iȑ#?7n,M0̙3ڥU+Ҳe˘7 {[j%˦MrQlmOyG<-Wٶm디ѡcǎRvV9Y5CG"FǸ#73{?bZhaNs_oӦMFxӧOKH8%)[;N@f?!t*Q"Jޠ\?FcyyX:tٲe֬Y#VnÈ#ĒL߿_֎?.+**n͛7mbϚ^VV&~KrrӵUI&~h4߫N8m4H'Ooyz1/_@OV~aĒ;0V*G3sW_)**bݲeˏ?ʣQ۶m`{֯_Ú%3OhvShW#Sh޽Nj=A:#t'xB,8暾}Z?u͛b8|zDbzAG}i*))?p~o׮vix1m۶FG5'řۍ7b6 }z˭+U̪\ ?O^}U}Ν;A?~?nܸQfb;))_,d >NLDpN?9tjvUE\ÊF(xJsc~HGÇz!ӅX5ҰaC_|Y=GoTDZc?-hVBνbq3 x@ǃMZyڙK\ÊF(xJsc~HGuS9"oׯ?{wc_RsZgFDO-vX?o_U{h޶;ȧ~j>)b =`gǣUʚ6m*_{䏀8bobi]EL)MǎI̼m7{袘B۶m]yp3OqÆ bv4j{ڴihni nGǃBBb,O:p'?Y|{}?OŕO7l۶M,qNkǎ6mj߾}] Eqg!S"3HF!E>?ѢE rTWWtIGwر?s\s(+H_{8),!#6`PDT`I(?UW]tRo'Mt1q믿ŕÇ޽]a͚5;wOP>رK/u~]vWزeKVCxF5?~W/^,yBHiYCF*l Q`Hhoj={ ğf ӿs=W,}]wc;wfddD^۷oϞ=sss:DK?p wn軻+z>3tQ쳸˕W^|򫮺J8999 "V>餓ܧH?PwL,!#6`PDT`I(? _J$q1cbXYYj*q?lӦMF%ŖO:GǷ;x'{~S\;w1UTT9ǔ.B 2Ra EDf4CJ❠ R.|sJ^^m.0`@޽{I'}vQ? W]uxqZfCщǢ~3q^Hp-Kԉ.$B 2Ra EDf4CP❄d/Z?#V8p`;I&%o2337ܹsۿl#6mw_W1n*j+ym*KHfL,!#6`PDT`I(? _j%IXAR|.բ iq0CT؀)CQ$M1|)x RT,KHNL,!#6`PDT`I(? _*&ޡ'ER3 1tH 2A4 ×w uH񻔍.dB 2Ra EDf4Cn*ȆIT.B 2Ra EDf4Ct*J.B 2Ra EDf4CTVVFKPA]i]I3? 1tH 2A4 C0H$x\**ȺHt.d:dL i!ѣxKrqqLVf˗,Y I<aB 2Ra EDf44#^(<$D4%g YCF*l Q`& jjj]޽{g͚$D4%g YCF*l Q`ceeٳEL),,|&1"B3`g~b萑 0e("*0iCc+FGp 3eʔ֭[7kl͚5Zr{C$f>Zgfpg!S"3HFaujLfyyXnݺ1L?5FT-k֬YFF\ СQ$B 2Ra EDf4C cr8_C5)Fzر E ,!#6`PDT`I(?ܹs_߲e6m{^QVV6t֭[j;p>}ݰakHD~ꫯV^s޽#F(]w۷/z W.,,̔]'<3jiժUuu֭[܅[ }{rC_bDE|/]`.G衇4ǎ^0`{ǘ6ƣgpg!S"3HFcyyy&M6m`Ν;˛6mgE>hӦkr_wOٳgWTTqeʮ]O̵hIYYYӟΝ;bĈ.H]l|Ȑ! >Dii<1#*^VZtI"`n2}$En';:6<03? 1tH 2A4 m7lФIǵk6o\^yꩧ:wܢ֩ڦo|njٲi}蕅 ^yb.:S1tP*x~>^1c<5)FtϞ= =z6o;6<03? 1tH 2A4 m}[pMz];ww_|yMMx5fDަo׸&~T-y_}ԩSsrr:c{!z!Ck˖-r-۷cuKޙkS 5k_}x^nwxFÙ:dL iTWW<[o,--z!w뮻n(//?p9izIDAT/|D"6lXGA<ʜ.[][y˖-oxĚCt{%TX~};u4vZ-<^1c<5Dꫯ0a0G_;v${09ƣgpg!S"3HF~h奥^{m-N?{][niٲe֭Nw>ѡCiӦ59rd۶m7o޵k|.oM6Ox>}{kL_M!03? 1tH 2A4 ӡ;w֭[-LYnݲeSA"B3`g~b萑 0e("*0iCs_|"ȑ# M!03? 1tH 2A4 ӡ}$!"*3 ,!#6`PDT`I(?LQx׮]v$D4%g YCF*l Q`&7|ȶo7u("ɏ<aB 2Ra EDf44b=ۗwQ$Mfg~b萑 0e("*0iO(̙3gʕ^`߾}Ǐ Dpqg!S"3HFaZ?do>n8ʚ!Ɇ<B 2Ra EDf4tŋ? VUU5gΜjC$A$:dL ii? 3fXzr@N6rh3T8CT؀)CQ$M0=G_bŊZIL$޿(/ҥK90x<1g qg!S"3HFa:#E]Zp0CT؀)CQ$M1|$)Ђ:dL i#HQDL,!#6`PDT`I(? G"`b!S"3HF!cHEthB 2Ra EDf4CTVVFKV y]3? 1tH 2A4 C0H$x\$ 1pg!S"3HF!c{fG/bX.n~U *H/˖-ٳ'P?YCF*l Q`H#Gfggxq],d;vt+H7ZNNΨQ,!#6`PDT`I(? Gqqqfff֬Y#/ܿ*Lz-XSk&rۮ]; 8CT؀)CQ$M14}YرX_ H>jfee+8CT؀)CQ$M14sڵ9ljb% 5J$[nnt"J3? 1tH 2A4 CS]]~%H ts˗&h ,!#6`PDT`I(? ȑ#۵k'/gB~M77h ,!#6`PDT`I(? SqqXş|}R-4€.B 2Ra EDf4CǐB7 tIDopYCF*l Q`H9s3H933? 1tH 2A4 ܿk?se"O %#3o޼rRݻ_y啙_OQ#VG3uݓNѱS m/((VP0QSr`%.bǨ B 2Ra EDf4C[pb>w)O}|z_#|1w/Ҥ{;:J &Ϸ/K1,4~U^nVo]&Lu˱c! g~b萑 0e("*0i?7=3ڵkѢ~w;w^-[lӦͽu(Ogy)o?ޚ;v8p|ŭ,>+/?'>t>i? ӟ7oy5~霜K.$t+.W]umۊy'Pq1S훵oN/#m](G.E{饗:tQ-oGAbu*}x}VoqP˖8Q{ڡo?37o֫W?o/viѢY-Ž-NOf?Fk>8U K\H|ۏ~x'~ӮbYYg*`o/8C1f#F>yݺu}Ə._tΝ; 6h o[oZkbk7xcEEŞ={x-I&n{H$⿏\[Kz+W:ǟ1c<(ׯxk?m&_|qey7޳g}s-6x=.ȉ:1߿o>+s^]ďGAz衇Ǝ^0`)WT:GswCڸ+r#>]tٸqcBzLnؐw?]v㍃eʒ .˻].Ǘ^#oʲzaswJk%\Fb}Kg?[^|'-7{?[۳Xlm軽/^GgXr̓o'GЧb~b၃>唓ݺV\&Mx1w7{ף[x˽$ێ3? 1tH 2A4 ܴi|…;wKyyy&M,X>hӦs|͛7ku޽{O<-[+{۷?~.++;gϞ]QQ_طM~g ݧk.q}ժU-ZՉ~5EY|"Ѣc=O, G1VD3޼xA$;[l LuYhnE#>)S=m۶E/'>$p}}CY\"|gowl_si+;vSO(ֿNywxϢbZ1R]w [so|WDڷ~qyC׿!؎ڽ:뻛r*wƉ'-xO6v/YCF*l Q`h\~}ӦMgdd8Հ[vZz=† b }4_ucy%$uYtgo}/~̍njW>묳^uw'|"6+V-c%)|ǤO)d=E1[>o#[xE?Dz»L38-p譝}roV+"7nkEo={7sϿ%^?^o{Al.3? 1tH 2A4 MNLw 7|~ڻwnذa`x%7]‹/ ?sqH$~qw:+XK/+ keeeN,[]!Coٲ孷Kjjj:%\wuE(//?pH;n*c) 4o߷oΝ;/roygQ'+z"i/Kщ:s^ 래Vu̝Q17Ċ۶mh"qEsw馛G?ԩt ꚏ篛g>ݻر#o韯ۗ[݊{/wnqwovWel?jxEo?qc>/;o`CguFfo_e}^n_Wu`ھUlֻiOsf%7 O6vG?rg!S"3HFq׮]Ço۶mͳ</1bץ^{O{>`FFFMAݻw~Weeȑ#]ڵk~~w_Л6mׯIO>n喖-[n;tV~5mv5׸ϷYfxϢNOW^~"N>{>i?||k~9wRuOT:Gsח;6Ċ\#gy%KuMNNNx٣-cţ?xudAd]r hwq"lټMNjyagyzk1s\۶mĒK.禹 \̹kRcnD,NMYl|eŒSS ͺݼy;!fu'~-z1OF O6v/YCF*l Q`h'fy7ai9tPrq#OQYa4]x{FK.YCF*l Q`>}7x5?W6o|5Ç@=2a]ř:dL izǹsnݺKzTo>##N>|xeetnݺe˖L~풒~Ôdġl޼y)*lȅZYCF*l Q`ܹs/=h|?#GL&b dK/K=.YCF*l Q`۶m{{#b-ϯ.Bș:dL io&// AYCF*l Q`(P]]]PP0cƌիWꨤpڴi;vL:u֬Y-Xf̙3?fqg!S"3HFaczŊ7|!_DrD~.]zRxRovA-3O]&q@8CT؀)CQ$MqG HQDL,!#6`PDT`I(? G"`b!S"3HF!cHEthB 2Ra EDf4Cx( @ &b萑 0e("*0i?#E]Zp0CT؀)CQ$M1|$2G_t}"-!#6`PDT`I(? G?~$~X.n~uZCF*l Q`H>TQ]]ݣGoSA 1tH 2A4 GBرcGt;o޼QFW5ZCF*l Q`H>xbXSʼy233۵kW\\_>h!S"3HF!cHh!S"3HF!cHh!S"3HF!cH|ڿk?seҤI"'O߀3DlDx͛W^^ b萑 0e("*0i?'kN8gٸqy"Dٺu/8yE}8A 1tH 2A4 GÇ'Mh"4Lqq?n:}B 2Ra EDf4Cdk׮2uh/K}J 2Ra EDf4C:t(//JQ@:dL i#4i|Β%K:dL i#D"0)?? }B 2Ra EDf4CpM0_f޽fb萑 0e("*0i?ćr~hdGb萑 0e("*0i?ćW_ݸqYqq+ql:dL i#!1cߑ#G ql:dL i#!>}j>N/#SN9ſ4Y)SnݺYfk֬iժ ާsaG:dL i#!zP`׵yC${[^^.^uE/B9t#}B 2Ra EDf4Chx˯'tb萑 0e("*0i?ć{3<Ӯ]-Zg?ꫯ|wٹs_߲e6m{^iOdgggddwÆ A5k֬[nbsO\uUb޽{G!6޺uk߾}jO>gw[m۶?3gŽu^1 H$"H: W_}D8Mg{(fff}gfϞ=x`wΝ;aW^^w=G:dL i#!jH(j :/eȐ!7tXaǎ}?~|ҥ;w6lؠAoVwk?G[7xcEEŞ={x3~w]sb͇z(++kرzC.ޖ,^&Mݻ#.~̇OZjU]]ul [n-6CxxCT؀)CQ$M1|$>D 7m._paΝR^^ޤIo +Dڴi_yrxRǽ{x[lq|ڷoﭶk.q}ժU-Z84zH$"pl҉{x}.++;gϞ]QQ_V̧Ჲ?͝;wĈ]t!Co>#}B 2Ra EDf4C%ׯ_ߴi%NmԤIoڵk7o.w; 6\Axꩧ:wܢ֩sčkٲi[Q~xkz=D]V.mGg+].{?n'" :Td /p}Qo.-OLJ>h!S"3HF!cH|wnҤg}-Y`AN)o|> sNkjj/ }Pl-qFfbp14=D]V.m9Z}|WSNɉ^H8!Cze1[n}[OGFxxCT؀)CQ$M1|$>DG?: qO?ݻ8a*++ŭ\rɸq^Q5x`?Iz!?wD"wj5m|饗&n\bÇ/++sj7l2j^Ac!:\9쉹[ly뭷ĒC{{Rn͛7wN;Eto4b萑 0e("*0i?ć(ܵk۶mۼy켼ѡCiӦyَ;(w]Fi'VXYY9rHwߺv횟[-4ޚc>SǽYr4y7mԯ_?}K'>;ý޻wo񼼛h!S"3HF!cH|Oo?!>LxxCT؀)CQ$M1|$>Dsݺuٺu-[#`+-!#6`PDT`I(? ї_~/!=Gb萑 0e("*0i?ćرc'NC@c>}?xCT؀)CQ$M1|$>\oڵkhv"`1-!#6`PDT`I(? 7|篈Ʊ}vy}B 2Ra EDf4CЕ@€}=zAnZCF*l Q`H> V\鯋}7~J>h!S"3HF!cH|x,Y/oߞG:dL i#cɒ%>hyy=ꫪjΜ9f]>h!S"3HF!cH|R.((1cիMP%%%ӦM+-- @-!#6`PDT`I(? OB_+f͚UPP8DtPKFgҥGg A 1tH 2A4 G③.-8XCF*l Q`H>EthB 2Ra EDf4Cx( @ &b萑 0e("*0i?#E]Zp0CT؀)CQ$M1|$)Ђ:dL i#qqo&DL,!#6`PDT`I(? o nyѣGSA6 @ &b萑 0e("*0i?k疏 ]Zp0CT؀)CQ$M1|$^EH.-8XCF*l Q`H>\>RA6 @ &b萑 0e("*0i?|tQAjGthB 2Ra EDf4Cx-壋 R/ @ &b萑 0e("*0i?*++ϟ_]]!G|tյ D]p0CT؀)CQ$M1|$>H$ҳgϛoyժU~Q.]dIIɝwٽ{w'PЂ:dL i#D";fffN6-!:xg}wq) `b!S"3HF!cH| t"^vk!z.t?ؾ}p:u|LЂ:dL i#dnn9܏Cջ|t]+Ė>pΦ38CT؀)CQ$M+o>33c○G)(! ,!#6`PDT`I(?Drvǘ:V h!sss"hTYCF*l Q`HdTYY9xQ G'b74B 2Ra EDf4CG Gk03? 1tH 2A4 >$#$',!#6`PDT`I(?DZqT8CT؀)CQ$MF ) qg!S"3HF!#PtII3? 1tH 2A4  r˖-8CT؀)CQ$Miqq4|$Ǚ:dL iHgM qCT؀)CQ$M錣 $?b萑 0e("*0i?"q4DZB 2Ra EDf4CG3&8V[CF*l Q`Htj 1tH 2A4 8 83nܸ @*Xm!S"3HF!#GqƎyѣGSA"pCT؀)CQ$M錣 ҆[>+O*Hb萑 0e("*0i?"q4Az.]TH'-!#6`PDT`I(?D:h4 TH3-!#6`PDT`I(?D:hT|tQA"=pCT؀)CQ$M錣 R|tQA" pCT؀)CQ$M錣 Gee󫫫7(VbĞc:dL iHgMT"Hnn-ܲfm?(. dذa=z{ j 1tH 2A4 8 D";fff+???!:xz .۾}{G$b萑 0e("*0i?"q4AD"=zξ?ԫ|tGXڵץKG$!b萑 0e("*0i?"q4Arr+Ȏ;S+;;8dYYYG[Aڵc܇8Ic:dL iHg^W^y)֑؂@:dL i`Z$ԩ[t177EEEeeeyyyNQYyyqv.;gϞ|]0pS"3HF!#問zWĻqzWn(x[[:w|k׎ @t2Ra EDf4CG0'dffx #8dVVVvv6$qS"3HF!#Drss}xɩc|~R_D \CF*l Q`H&TVVΟ??cr+H#A ` 0e("*0i?@r*Ⱥ 8݇T؀)CQ$M|8݇T؀)CQ$M|8݇T؀)CQ$M\AR>HiCF*l Q`H) |8݇T؀)CQ$MR[Anٲ@t2Ra EDf4CGHHG}H 2A4   p(T؀)CQ$MR 2H 2A4   p(T؀)CQ$MR 2H 2A4   p(T؀)CQ$MR hT72H 2A4  㖏7o=z4$2H 2A4  F▏ CdL iJxDc.]ThT #6`PDT`I(?T{$G*H46e 0e("*0i?@*=z+]Th$ #6`PDT`I(?T{$4r.*H4e 0e("*0i?@*=TVVΟ?_(tGW]+H?8AF*l Q`HH$Dzy-CrR KJJ޽{w'ۀ( #6`PDT`I(?T{$D":uѣǔ)Sb6NGW R,|#7;;{5PS"3HF!##d=DZ~ӟFҩW+HkС#Tp(T؀)CQ$MR PV9܏Cջ|t_~~sCPǡ 2Ra EDf4CGH%^ϼyb+u?zL iH$ҭ[,qR-_r%EEEeeeyyy{wnj##v_x]t['knٲe]5h EDf4CGҊW>vڵcǎ#G,))nu[>+Ė.]\z۷Poh EDf4CGG$СCffǘ_WԫGxYYYT_m Q`H@pv旿ecrXA&({ @0e("*0i?*++ϟ19b D_m Q`H #ڀ)CQ$M{9 +HGK 2A4 |`ڀ)CQ$M$#%L i$#%L i r֭@0e("*0i?pGQ>0_m Q`HyPpS"3HF!#aBL i{0e("*0i?q |l Q`HyPpS"3HF!#aq>6`PDT`I(?< S]]=z͛7SA~8؀)CQ$M|0ԃ[>;#D=pS"3HF!#aEzcۻ\-WeWw4M)R(wح*qBGҋm-[#,vA.lV~]ʐpYP Ҕ6noϏa83əəG9g99Od(Byi%G}~GIT?edPfZ Cx(^W1$A#.>O"*VBP0}| Iŧ)S$S0J(V:::;;;+PL| 4A{$8 :jLLL+~?{XJ#GKZ[[>*2Av{DQ\|2E2U(3balժUuuu555cǎ]xq!{C$`f5jTp܃:H|Z.>O"*VBP*jժ`_ w1 3'NX[[CO5SH aPl?=l 6,_u| -{cx#8B|ŧ)S$S0J( |ѦCK.1)S$S0J(&:0nܸO~'tRSSS[[۔)SvW_}uڴi~{1bD]]]pk0yOLL+~? cCC 'p_q1:A1=_veT[}dPfZ Cﮫ ox,י^%%A[}dPfZ Cwѣ#7<a&> ;iҤF *o)Byi%Gj\ǂ2E'"cVp]T>o)Byi%Gz,SDi|6o)Byi%Gz#mo)Byi%Gz)E|S$S0J(^~էH aPl?% R|>E2U(3b իG+S$S0J(@& ~xOLL+~?P~ALL+~?P~ALL+~?P~ALL+~?P~ALL+~?P~ALL+~?P~'V&:uIN.T<̴#xdg}vʔ)drH aPl?( Ӎ0>drH aPl?( ӕ KE2U(3b@i=Y\"(Byi%GJ*>$ȪALL+~?P~'"m| I%"*VBP4?utt477wvvFWt!SD| 4AIp>$KE2U(3b@i=~KӣG>syCE& .`ԨQDבx.T<̴#jNjjjƌp‚CfzC$(?;C +KE2U(3b@i=J#G<&zgU| 'pÇ+KE2U(3b@i=z >!zCa 8dȐALL+~?Pa'|ѦC;e^5S0J(@zt7L&_>lذcJP T!L+~?zkjj^$%v$ laPl?ѣGGnx,(M|L<̴# eNEǬ3 'T!L+~?}"SDi|JU0J(@xW%! nJPfZ C(A 銫aPl?~ttt477 3EPOdep&DW0JPfZ C(*Q:nll蠃jjjF~`10>Iŋ;68n]]C=݂U0J(@xW!;?ډ'U| '!C444 U0J(@xWyGv /8lذ`Qj*Ayi%G|¢thS졛o9SU}L+~?Pt:=rȺ}{uƍkjjjkk>}mEkoo2eJ`o'tGqG)vٲeS5c Cxa|=56:A1x[+"8Љ'8tP (9b@t:PSSX3J1+8Jx;dmmm]] XBP;tqҤI7<a&> o=ztp&u?Pl?RGGGsss  Y9gOt@%GJ)SDi|(-b@eM##"A@%GD~K()>ɡ?Pl?Ca\z$XBPoe2୾$XBPy$R C>>(.J%G@(Pl?sTBPy$R C>>.L6m֭s.J%G0>>sW]uɮsQ*~?gWqÆ v ]TBPyOǐɮsQ*~?էwII(Pl?s ]ǐɮpQ*~?է2ǐI(Pl?sW.d&3 '*TBPyO:nll<F}P*2A~k_1bDp&uT#}[}ݙ :蠚#FL6퐙P7 28~:*8n]]ʕ+[P\J(9o 1cy=ro*>dxcCCA^|$Eb@V0AtIC:>nݺÇՅGpQ*~?ݻ[MnN G|Q'|Gy7M>}ƍѦXW^y餓N;v'KFO(~?eC644\z饭ٵL 2۶m-W\1||$~?NZSS_"AǬ(퐵uuu$\l?>N'M{cA&ncvѣGg]~?} eNEǬ3 '~?)"A4>}!@eM#$Dl?t GH~?ɕ GH~? GH~?I&իW4P*@&IR#$Ml?ʐJG2菐@P* G2菐@P* G2菐@P* G :::rZ_CHt:6?˃oG :;;lLӣF ־)Ob$ɓ.]{1N:ltS?CHKRt:=r=PmG}tMMM*5jT}}'P3H4t뷿F@?#hF=zt kjj6o'Png}vO=jCH;.>y%@?~|*34P*@SSO#TM6򗿜?y?͚5+J͜93b ~/ڢ?lH~?@Ejii5kOӧ~:Sj7t̙3۷G$P oΞ=V+ӧ?IG$mmm3fX~}T1pݴSt*@2C*o1cƌ-[D{Ol?zO?tEQ}VZu}E~?@X`A4DQ 6GϏV]cѥ=pt5{8`޸qct]&O lb`޼y #yŖBQ`<%$Jl?ee˖ox"xd?P w߯~z)֭;3os9'`͚5cǎ1cF|֭ۼy^8a„w^c=6?;}ׯ_N3]ĸ;mٲfapg} ^}Չ'^y啙.UpaDe]iӦHpgǂ/9_WG%Pv?>3;cذaOioo}ݳ~V\yfg {mmmWӟtÆ مc\=svnܸqzo 2$ű .KjڵzeG&?;|ɑCt?(P }|'c%--- ^{v#<>:O dذa|1K~ӟoN8w.b\do zvm{G??VW _EdLEVL~v,#uuC$Qb~w=oʼw]^;3$׭[lpn۶}쏡_}Μ9?iz aÆ=#{PO_s=V 3]"cώ_r D#Tcf?4<묳^z#8"^xڣ>zԩl8qb_:uQӧO>Mg;c\n|{WUm۶?hhhn޼y}]dIs>3N}˖->`7 32;mŊ]ˆ/Xpdc{#GW^?>6m֭[3__r%vO?=X}_ַ͛ pf5kN;`ȑ# ~3|3]ˆ/9?fEVL~z(?(P t#GW8!:?C*Hւ Ol?,]tEimm]|yt~@#Tn)ڢ> ,x뭷Ol?2̞=;ڢ27ok3Ul?2,[G)ŋ_|̀~G ۷o1cFHQ5^x|MC*?<ڥW_oG~?@%馛V\S hڴi6n P ԴbŊhbzjĊ#T_]w݆ d͋/??&bs…OpEkmm]hќ9s֬YCC*;s}-i@JG|z+ÆD#PRTtbT(@e!bT(@e!bT(@e!b$TGGGY ~? ܜN`y6-_b$Tgggcceo1N92X@?#\'O_dɻt:0a„@#\---uuu555K,IRᝏ7MG ƏԤRQF >Oׯ?7nС<@Ǝ;f̘SO=uƍ|T~?@e˖ۿ}7s̹+gqss?Agg^{mjq-#>&A_~v[bEn;cƌ;\sE]bФI__xwwf=9l`;N: &,\0NGt ׯ_|$|/wg k7x3 2$)0C?Ovزe&L.Ї>opgǎO<ŋO=K/4<#>v` :POCa?~ /.;wnqpر---•+W>#76l=z{q衇Ι3=K//~18&LO/n;,wID]Pmb͆ ƌO}/Kt]L1umnW_}yElll\ti+OѣGg̿1rWc SN9; ۿۄ ]??kkkx쒂$H\l?<Ɩ^{uvv[[[/6p뭷7qџ'{{Scmݺ>ltD 0PCK-,L>}ƍAWw}O>dcGGǠAz>W^)rc=Ƕ}C6mz/;uu~ش SL D2Gc-P]b[Xy _s5<6mZֹ1 ._CЋ/<۫1JG.kaOv̡"Co#-,*N=sH~?ܜN涰`y6mLU&ȞwK:pPqٲeNG}"d29s,&gQ||cIG&O\__dɒwkata„ M)cN# |---uuu555K,IR]x7M)cN#T{555TjԨQ>|xt#JʘS bU3L4dȐ)SQRƜjG F:CVSSe˖F1Cɓ/-3Lt5}3CEKKqވSPØ3CSOA)'cG"MMM)R^Ɯ- 6m/9y}̚5+J_+J$8_mmm֯ۗ.]GCYEOzc*Z%NG vMl?曳gկ~Fe>h?|'8iUe˖͜93rފ#3f_hic?ϗ,Y=v &۴i^yACo1cƌ-[Dxg~GO6mZ0c=G7֯_Cw}wŊ6sACGKK]wBܹsn ?o޼²e| .?f͚m۶h֯_?(… _}B0 BCM/^ Bݼy~XH FI~ӟvttDz"n֧~:ZUVw}ѱ(`---Cҭ`r-ѡ# DSPu^xt,JdѢE_#PBOC/͞={7n]<s=KExJ*ѥT?뮻.:P~4*-ۃ=yJ{܂?FA=ߧJusFz"x"Vql]TGǢDEP~Ћnݺ3w9tttYfر3f\,饗N8"{[|u6o|N0!wlm.M6U+gaÆW_}uĉW^ye(]Il ^ ڵk:iӦ~yˏ=v3b֯_e˖x \[#O,'Æ w|#?rv?O=n{o~ٵ]OKZZZ z: c<+,3*#(z#?Fn+um۶#a1n` ?V;]R~ׯ*.o|i?yp( ?bgÆ {G{.eXq#GvQl?_9q?"<ꨣOn}|wtzӦM^xaWq%K9眂1_yŊk :묗^z)x?#f G=u)^3?>øf͚`a38gqFp>[ly3]cEP~w{wڵ~}_͛Í_xSO=uРA#G\h{E᷾`ㆆk8T&M?> >|k +\| O~ft$ ^xt,JdѢEoFxb2dȠA>я^|-dѱ(Eu]J~?@?Xtի)hkmm]|yt,JdŊ?xt+J~?@?hooA݂ zXHCҭn_%Dl?̞=;Z͛7_{Q(뮻.c̙;v숎#Dl?,[%ŋQ(5k455ELyn-:CP}|;&4@ }3-Z/FO!ӦMBC?^ ^~QM1z|M7裏Fz.Пn馕+WFkM6mƍWg:::f̘!AvhjjJl?555X"_|ꫯ.g| uttL6m͚5",:d[P_۰aCU͛7x/ϟ] p¦&>wܻ;:X bٹph7(-k׮]}eÿ˿?xG'ZM~y-X f1]GHwy[h… T*]T:i/_ފ~OpV wqYw/| Vt\b%JEnj9Xl?hagbE +?cG.ZXs~?@uϘ3CKL&3u-[DWT s(Fl?Tb >{UWUb1"CKŵ0> +n̡xPRY-,7>*1AV֘CCKX z*P]*uC +e̡bե"ZXYcG.oa͝]C=M9gOtE+CCKt:xX"2EP /1bDp&u}_#P]aÆ_M6Ey| u ;;;/^J }zGGG)}ʔ)~?>g>@555GLXWW7|CN456:A1x[+bĈvѣ%H#P!N744Ԅ7fI b$N~?I6AP*E!9b$W~!Qb$Zn!ib$] y&@d2TJ|#PRTtbT(@e!bT(@e!bT(@e!bT(@e!bT(_R"G?*AwT*Hm۶_HRE@2l۶-J?AJ?AJ?AJ?AJ?AJ? ёm?Fɤ? ܜN`y6-X#PwqGm?#GkH*Hɓ'755^L &Ln $$WKKK]]]MMMSSS*J#Fw}MDD?~_SSJFYWW7|F@R@555uY냯SNn$$ZggQ=0Anݺ5T#t'O>xiEW ?IrǧR<E*SO?'@%ڱcGkkܹs:;.J_`y6 yG VZvzꩍ7\2<mOFes9S```: 1G Ao~e]zoft]```{ KdΜ9^zi=12>xVt #V:Sx܅=׾?|ذarĉ g_L#;v8sۯ?# _;32xV\ ?/|!w̙3=k„  HHsp o{c9&?hɒ%{~ $$™gOf7~6Z?On<7C[ !G ?o=hqÇg<%-#GoG-y HHOxV܅@B@̙3K/ͽ{+dپ}e]v饗uym-@@l߾}Μ9rJ !G VZvzꩍ7 `I|ڱcGkkܹs:g`Iܧ]CE?}E+#WG@_gR!IENDB`onedrive-2.5.5/docs/puml/uploadFile.puml000066400000000000000000000035711476564400300202450ustar00rootroot00000000000000@startuml start partition "Upload File" { :Log "fileToUpload"; :Check database for parent path; if (parent path found?) then (yes) if (drive ID not empty?) then (yes) :Proceed; else (no) :Use defaultDriveId; endif else (no) stop endif :Check if file exists locally; if (file exists?) then (yes) :Read local file; if (can read file?) then (yes) if (parent path in DB?) then (yes) :Get file size; if (file size <= max?) then (yes) :Check available space on OneDrive; if (space available?) then (yes) :Check if file exists on OneDrive; if (file exists online?) then (yes) :Save online metadata only; if (if local file newer) then (yes) :Local file is newer; :Upload file as changed local file; else (no) :Remote file is newer; :Perform safe backup; note right: Local data loss prevention :Upload renamed file as new file; endif else (no) :Attempt upload; endif else (no) :Log "Insufficient space"; endif else (no) :Log "File too large"; endif else (no) :Log "Parent path issue"; endif else (no) :Log "Cannot read file"; endif else (no) :Log "File disappeared locally"; endif :Upload success or failure; if (upload failed?) then (yes) :Log failure; else (no) :Update cache; endif } stop @enduml onedrive-2.5.5/docs/puml/uploadModifiedFile.png000066400000000000000000002461151476564400300215200ustar00rootroot00000000000000PNG  IHDRAK*tEXtcopyleftGenerated by https://plantuml.comviTXtplantumlxUQo0~8 :ѮDںAi@C$bձ#abnBHm}qUdaJNh'RjB"TH<_ )p)~"|]`}lc601{ f2퐢e;)BG[jeC:at AT+)„;/rm UKr!*mzљ=4םz@ućh'y?z T&– 5tT6|_oޟa;?IYY/l@4g3ch1K44,6:x~Wt+m>Eht 1 ns2 ]’jʿCvw_N<,n6ZѪ=.Apݪ.?\~m-x\ f }B6VٜmŷTk*`[?`i¯IDATx^yxUEA$+;$P #3.⨠BAQYD6Af eA!@$MO":]!rtFN}v?2T;v Q@`Gj;vT;PnN>lٲ'|[n]w]Ϟ=Gܹslv|{:ujÝwޙ^D*!CEozav Ν;SOۂ CC5o<5mhoU" 0ׯz av p$/v pM DD#jr1޽{jj%F;d@P@iTP2T;8GP2T;8_@P@@͛7Or[-Zv@uܹzJms,XD!CyEozav|{:uRS;3++K TT;PnN>lٲ'|[n]w]Ϟ=Gܹslv (4lPjlP@P  T;AAj6v (Pؠځ@T;v`CS._V>}|Uw HC œ}Ps=뵜9sѣΝ\Yf>|XV?_|-%{:tӸqQFio8{y Rڃ@$3:-p.m6lXBBu\ɓ+WVItߚ/_^zsι/zg}־w>\?M8Ѽ6n-nsӧViZpaݺuoeT9T5z2./=(QD2WF_֬Y##EEEfeeL/nרQJ*2?oݺE|\#|!v\X^̽۶mm܋SN8j9T5*ɲkӦMHf]T͛e;77ܻvZ3%ͽN]f͚ ܳgomDѻGջr-[|||{eSųVq|b3 r{oe._\rL#HPD2J.첏>;yh {c[ٵiiiF-yС5>ds8kͣ3ͽNRʻk2z]F+=QD2J]wݕg:thu?~uf-V^-=ox\~Ewsܘ^֣y^qa_ 8wEG55H@Ɍ+jwݏ?ToJ5j4a 3?Ν;K Ks+q_Gc9sӤ$oQ+ >dȐjժ{.L4\͵xj?_Nm۶_^zN:Was|+{Y}_y=tPD2JmjM#=PD2muSad-T;L[T{x"C@$VwU;v"5վj*//DHKӧdET;RCΦMgj ]{yj ij@$V7Hn(yyyojWn(jqk~ žv|\˗ߚ.޲eKTM&99966?%5jtwS@XV7bR͛7 ~4ځԵkפ$ w-ZFVI ijSzzްXLL|?~: ;muS@yr\-[lԨ|n(g={4ODmuS@9ر${tt4?kD&muS@ڵT;C bij_zz:C ijG_p)SRSS'cJWuGГz999V7Վpk}wдzju(D޽{ܹ/^ٳsnԩScǎ]lڒ8iĈ۶mS$n!,''G2Q3T[~gI5Q6loOM[T;By=h\>P3v,&MTXX>smuSI)))'OT;СCSNU9vYf3eʔ"M#,\0t.{DټyڵkxV7Վ3e5xV7ՎГvQV-M۷oX[7 ^vnǾȶ4ҥKڵo*ZjUVm׮ݦM^A{%H2ez&O4~TҬY~1p]w+:h)]T;V7ՎgӧOW^ttu{6lh-222̴}饗bccVڡCƴkָPˆ#dѣ [nڵ~cǎFիѣovcz?z^kiӦɷr㣢Gz77my*'';RV|ZQC[T;B.z}BکS'e+V߿ĉٳV^]N͛7˶\>|#GL64id;vT޽{ݖ#l߾벿kٗ]v'|.[陙֋;C>8r֯_o yvnQiiiOV?A[T;BҸqBD9qĉgxn!iҥ[lQ;7V9vgώ9RD={9TnjSkAرcC-**R3vsnܸQmF+I#FM[T;B[zz+rD9ٻwСCIvV7ՎN4jB"N8oL27P\.WZZ$cff(c[n6mĉۧ>1?>Μ9vZ)H)aÆP(zŊDځ ծn T;L[T;v"v (PD2muS@Pdځ@ɴMAj ijG^^[ڕ ijGFFFffe\n(.+11qҥƷfKlR>;muS@INN]p .ߧOu*kځr%..޼ysԩ ijS׮][n-.ޢE HHHP'pn(OzjXh &@V7'ղeƍP@ځrl~w@V7;sD&muS@ڵT;C bij_zz:C ijG;to:9X;V];< .W_pɴM#l={vɒ%)))sݽ{;^Z >}k6f̘-[7o>rȬ,5zͭ=Ž;'( Z'oYYY9nKyeJСC۴i()KnaŸjk_yeӧ׫W/::zݺuѣG Pnڵk?ǎ3srr>t7`7o^FdVdffVZxo/Y|РAFmyWwn^eINN{]&LЧOs?O~i& ʠl_q V7Վ0vmO?)ƛF}BکS'c_~w}Ç9ҫW}:tHj{m֬w}wG޽{s=yyyڵ9r1Gnc=o`5k7%?3C!Ct:hlajRhjGV7|se◟kԨ.~RJv2|wWP￷wܸqIII{N0r3vi|7mԘ#7c߾}Lվe˖իKz3fpUV_Vʕ+׭[׸6ծ<,6v.v mK?+^Uߨ[C͚5ԩ#_g˥d𪫮zW]U(G&]ڕ](sرcܖGٳ3|p W  {] ƶZŋzv뮼ڵk/ժUSnO?}c~q\v-@ijGWZUnCO>Q5Oۿ=?ʕ+?wuwAA KGׯtRcIi^z%w׏1suk֬2NJJ馛qoy'NQƛo=Pv ]ZrnݤzW.,+O(.37o>ec\:v=q)[|Aie>}H(_y問?Dv-[%o&N>=m45's=xijGxǂKIIQ vt(QƶlOpB[T;ʴi٣v%҈#Ξ=> muS+EEEÇ?v옚(sݶm4M#9p 6̙3G}s$܇w^53QzVX1{l%nᩨ(--M*Srss_x˗:()muSg{_|m۶ 6oޜ*\.@[T;ӧWXVljjذ:L䡛6mڵkϜ9>ijT:"v (PD2muS@Pdځ@ɴMAj ij@$V7Hn(yyyojWn(jqk~ žv|\֬vI-[ߧpn(7ɱ ,8%?S@XV7訨( wvI͛NaM[T;Pvڶm[ wuIvHHHP'pn(O{nX,::Z>$MrlٲiӦF9T"v%''/G "veeeux C 2ij_׮]*K[T;P*L[T;vǍ7̌;V];JϘ1c ;v@C}ذazjuTgwv; ;AK[T;;@M#LL:uРAv^^^ N>-Ǐׯ_۶m;C՘ڢE6mȸ\<vnCծ]{̘1:thܸ]vIIIrѣG/T& SN lc͚5vFF#iJrLXw&&ܥG~ʕhѢ;K,ѣG?֋KncgϞ]bk/hSLϝ;wM73---33S9;+ܥګTbl8p~#_r~7޹sg||"ijG#QtRqcrU\K}ǎfͺ[}Qsn;d<@V7Վҽ{+V#v[JJ4l9soSNI6oܴiScmvU:AUPum۶MJJӧI~0о}Lu 6ȥ͛\R]3ܽVw|!C=dB[T;JFFF||ĉOLLiii2xȑΝ;K_nݺdw;vիWGGGUX/ieeDa=Ό3nᆋklڴiݲlٲVZ/PWiTʔv~p}qF?YfR &L50soOo߾O>=1a޽˳^z樨(eP֭k헰_ZW-ZԦMw`&>, ݥK_~Eݧ#ݖbɩTqsCfڱcGLLK~z->p@ 9"=ܠA1&[Iѣ2hv9ԹlW;GGGkߢCPM;ax3=O}z׮]z!_ z^P@L*C?ijw ==]NB| ^~eL🶺˶ڏ;簾N cǎ|Uw&y^~CO_paج0[0 4͟??''G}i=33sѯw}#WVBݻΝtҳgϪ"e˖1cƼkaNl4ŋY6"mu~:uJCRR 5jԨ/R}:Y'ߖEY6G}vVw)W .oO*R%+#X'bs i4d/((PW5yPvddg '6l={LC.jOIIգ0z˗.0vXr>@} muZo޼hB9pil:&O̲ RhԪ}'OT1#G1K3fp˲ Rhtѣ3gT0294՟4Sl(1muN/X \pZvL(w}u2m޼e@it}ҤI/L6M}Tjjcò ĴMo^bEu4:uP&OsޱcGZA(1muN aݺuݻwZj۷;w: m-T@n:G6WQjնmC5T;PKvرzT\h:~*%gڵ]tQFڵNqu1Wf}nݺUdqW=uTh;@վjժ+7<~裏|A 0/(wpСmڴQg(EjFWŋ ɲSO,l+땲Ⲽf=o3//ONrOwܩλPNYu'o2YYY%^5Ĵjk_yeX_}ոݻѣG Pnڵk?ǎ3fKUVС_-# 64_;ߴi |-ĞGΜ7o^FdTfMc媣ӹq^{-&&F|ȑ^z <ؘb 'NȄ={.ľ.h\ 6GCf̿UHR2K.Ƹ fgg_ve|l:t(338< w_^^޾}Çݻ=#rA99ҜN::ajJ}8p@~嫺ן__˚͒ڭ밟˲9xL5:jժxP in7VY&&&ض9`jPbD7ґU\ʏ=ZR]v~ƚnqƺu/^Xq].HKKS&(G7n\RRҞ={rr(?V/3gF9_/^ܴiSwC@s7ƍrFt_F #vW_m]'k֬i|ϯgl\_n~.^_Gk-[T^ݭ;usgy̫zr2'أQ -WƕQ4ְ3V:u]3f̐NQJ*em.h{=`W_}՜*9ҳggyݺuoQN66Ԣg;sۑmn (EZCײϒk\9˲?2i_k9ugy{=X{NG(1muoOT\VZ/9ȸ'Oc,_~yY_\>l/4h{3^tvj~'?>a„8w`y}ɒ%M4q{n4fY{3ځRd_"111==k]^oVV2k׮~.^suyn2=7|<+zx_S^x`jPbPZnݺ7'|z.}u<''GǕ+Wr233g2hQ.#3?/]Ԙ*nѣG}\Pnezd 'FG#$//OcǎÆ 3֫W/9~#F(hƼfأځR kNaao^~e{ĉ5jae}{vߞ:u;ߐ4iUW]e|jA 4gek֬i|)z/NG(1mu/oۨ^t9}W^ak4Oc^{ځRvwV:t`o1##k]?J1%Wey]v\\\jjU{]|ܶg͚5rdiwqq(ss:Ym/~5څ׿u˖-^S{u @i;p-eځRO#QJL[T{ j矩vQ --M}?.j4it_~jJQjjcC(1muN[wVW/;v\R}Y'֭[Y6K80w\uC({O>>JСCmڴi,JL[ݥSΝKIIQ0I&O3K3vX ĉ?ߴ]:.-ZdsZisVcfŬj,.KÇB޽{*PΞ=;rHGoϞ=|%VwUضm uǎ1bDQQ( ۷o7oP&aX6\"muf׫KB{F>Jܹs7lؠ!4ɲ9rHMN[ݥ\b~!egg>s+VPj;l0MB[ݥ_b/Bnn!(8qB"-- ̇~8iҤÇ?lΞ={ԩ,Jˤڅ LMMݼy!hl߾_|ۧ>ʘNN2%33SDڶm&vÙ3g֮];m493M 6TB<#X'Pd,Y0 ,ʔ˶Ï!7,?muSp?`M;IĂ V7 '! &O[T3O,?muSp?`M;IĂ V7 '!%//`*{VڝL[)V7 \-[n.˖-KHHO\L[T3T;xp7̥K&''SڝFVVVTTT&M$ed.&Tvgv׶m[Y*%zfn&ubڝތ3bbb^н{tubڝ劋3=::E|M;C<`TG> V7 ZYYY`k׎ϡ?M;C?Ϥ9T𓶺vgvnj3ds'muSPʝ;v&P cƌᓲvgvK}Сzju(3vgv:*wAB[T3T;& hjwjP.${Y";muSPM=@V7  ${ʑvgv&pP^M;CmڵB /ҥ:# 䶍7N@SV-e0;;nPg޶m&%%g$ [\hjwjndwhbB^Y'hlidc^:::zݺu))m vgv%GO?tǎknO>}ӦMm۶MLLڵ={2A9-O?{III ={92xw|_Ν;{ѪU+6zh8Æ k׮]||ʕ+W|͚54dȐu~ y޶3g~ / (r{B L[T3T;#c߿1([ol Iwy]|sI9rvv |K,1&̙3GݘimN:M<>xqEɷr-֋x+Zl!=?վcǎc[ؐ#t1s @ ijwjPF{n5!=x1Xre Me*U2nL|9u{r'O7o.+,R!.ZhpB3fgX+>@>>tjw^NNN6 0o es(`C[T38í ]\TOL^4'N45i\nkٲܘ]?iӦڵhcdk/?nܸsEEEuaԨQO${ V7 +q=,?|޽/${ V7 +@rG^/[u͛7o߾;ed/SCڝa` B5Ρ muSΰ<7^H V7 +r&CڝaP^܄{ s(`C[T38ʑp/m${ qlhjw@rd0Ρ muSΰ(wR&LdTI@ V7 + I6M;Ê~b muSΰX0`C[T38'9uAyyy 48}l?~_~m۶k\cNjjj-ڴi#rY8iCڝa?ŶԩS=a„=zXf!..RJƸP9nV`8__sTX stM={LKK̴!s(`C[T38'e#QtRqcrU\: cǎYfr->s(`C[T38'{+V0Gnit>s7|#N:t1aM65#y`V7 +sȈ̑'N?11m۶5--M9ҹs礤$׭[!s(`C[T38`㮻jܸƂ)OSy86M;Ê6|ժU+ wY0%5jNE V7 +ؓdoܸqBB,Fx$D$Ρ muSΰR٬YڷoNBD V7 +s\111FPa V7 +hO2UV|&Ρ muSΰ5eLJJs0qlhjwѪU+Y0*86M;Ê0>Pa9nV~bSΨv({r֮]{AA V7 +o߾ &Hnݺ]~V^_|1yd)xM V7 +`VTT4eʔYf8qB؈ /,_\}P86M;Ê h :t޽jF?pㅀ V7 +$>bĈcǎŊbׯ3g!86M;Ê  6d7wm۶s(`C[T38ԩS٣V*<1ٳÇ@ V7 +`o} olٲxbDplhjw@y1KIIQA PvgXqӧOe Ν>Ρ muSΰ*+W,Rٽ{9nVAK 55U}CڝaTN6i۾}W\alTXQ&O>Ρ muSΰ*j kKd.RwU'^^86M;Ê VO.((?YQs(`C[T38%VK/:|ӧ׫W/::zݺu}ٳZj-ZKyVѣG Pnڵk?ÞV[bqqqݻwW _RfM9AF6m4rG$ʕkQPs(`C[T38%V+$O8ѯ_?rc,>laa|ԩ1w}˓|ܹgÇ9rW^6M{%o{=fggknȑ]]v'|":\j//CڝaTvYkYԨQ27n[9i&,?1yJ=zRJv2&|ƶ|WƠ&`c~k\Ν;oMzNɹgΜ)3#9nVAşjy}ƌ5UREo믥/eڿk׬YN:|-Tի{Ns"ѭ[7oQ~gDΡ muSΰ*R]k֬9yoz1g*/YD,dC%ʷkk힇Ǐ0aB\\bT{y V7 +r)? ߯_?jzp ۿ+W4Mss\~믿~Ĉvڵl2Nj/بQ/ ^^86M;Ê \J!CTZU2wĉ!n}m-[z2_~՛7o>ecܴKak]bO>5jԸ+'NxN۹sgNdܼv}ǿ_plhjw@PV;Ρ muSΰ6iii('O s(`C[T38Maaɓ>7˖-RA PvgXq˗ZJMT\,''gܸqc V7 +4{7 F?s(`C[T38ּbjUvIv>d,Ρ muSΰf_~Qn`˖-7nܩS 9nVAٳK.;vܹswޭ6lfΜ)? Cڝa*rssy)SN.`CeOb=plhjwĂ $ڝa?`B? vgXqO,POnV &lhjwĂ $ڝa?`B? vgXqO,POnV%//`*{86M;ÊddddffZL淈LCڝa_\.WBB_ȸ}*"PvgXqwݸqc#܍S=..'T")ڝg}ժU+ wY0%㣣ԩi 🶺vgv .. fFbbbnFu"jѢ盦y`nM0A&M4,vK1z%Y4sHrr:lڝ劉1]4k֌T#ن ƛ$٣_oڝ@;4=!!ϡe˖Rƿ=ztUD xn@0sǏfȂ 7fܸq򰨏kÆ ;dt›M;CZҦÇ?|;8^Z*>p/o-[s(C|V7  8,ك  Ϥ)muSPd!˝s(C|V7 M:uРAv^^^ N>-Ǐׯ_۶m;C*ӘڢE6mȸ\:Tv1ctСqׯ7ƿv%%%r-GBps(`C[T38.QAAW_}ԩhex=֬YclgddH/~yRJƸarN؄TV\)ۋ-ر1.dɒoxSCڝapqqq{9_+V6dܹs7tSϞ=233Iv|T{*Uԯ_|X[o&;w^Nqlhjw#QtRqcrU\K}ǎfͺ[}QsMp7!cn_uU9nV{+V0Gnit>s7|#N4&l޼iӦƶ6wUB֭['$$o>33Sa! kEU?7ХKu6l{}54o|ʕbC> yvz!s>Js(`C[T38JEFFF||"ĉOLLl۶|MKK#GtYQu֝%b1c 7pBuG:vhov-˖-kժ%}U;F-ECڝaP*Kv,elO{hBBBϞ=e q[q~}}ynj ݻ~yyy /^BZ6++˼'xB,ZoZ3vXs{6m2~ڵ={28¢EڴisA/;PvgXq\" n.]/>8ǏߣGco^d=g hel˚[o4hp~ d9x𠱑ѭ[7R ,C4zv]捱n wy]|uwGGG_p$Ρ muSΰ(jOfGEEI:>ވ9cmqu*B\=4ib}?裸dujd V7 +?eeeEEE5kLbN*2"T3h<ג111< Ρ muSΰ_vpHvQ'!,4jԨu\s<גmڴڵ:)qlhjwf͚%1'.K|mQ'!,gcǎ +==]86M;Ê.KJ8!|61\Otƍ[lss(`C[T38yGjxܹQzs^qlhjw#YYYM6C6|;vsPvgXq8ծ];Y:j$0P} V7 +f͚%KCgR/Cڝaݙ3g>裩SN ǏC;<~ӧՇ,s=Zϵ2}96M;Ê#%U1yM6>T.Y/"ȦRl+[+_CZI% .%HAU BF-4rf99z>ν~{4qeŊVRR7cƌ{ʷm *u}P@AIf8@]ygΜYVV&G DLeegϞ}I~nk̊gnԉ?K.c>4iRyy|Wq[c_}P@AIf8@liQtȑ)SVtsp[E-}P@AIf8@/\P:&O\[,m/uCm&??"+ udyyyM2mGrCm&ou'??Ϟ=}2mSw9PЦnR:5|98NUVV>c2m_w9PЦnR:5WcГO>i5˻ (hS7 _|qΝrj@]裏/_.߭q[gnD?.GĀ'N|+,>s(Mݤv3t j{192Ŀ7֫WOZiӦMJ+c֗_~iⶊtkC fqw9PЦnR:5x'N 6LJJ֭hVxqUW#<$7n2lذyi6m94^d-+3_]6w9PЦnR:5a;7q+B*;vL^ ԾAikԌ_|1cƌM=;a¼u+Tj7Z/VcgnDMnݺuM4q>rX)~*YPP v|8}!Ce_Sl=8Ç"u]3Ν{^]jgϞnIl׮تy9sҒzyfyPmV׾}nVqվX,`=E޻wAjbj^d&qqDYqYf[yƍs=G <Ϛ3[zq1)}P@AIf8@ԄDx9r~C }{*?1bDyyޯ_pʕ+EFt-[&Zee8gLPmaVo;/_=zL2Y,`/-EG6l&{߾}{M 9w}W,^XXlG?9{aϳfL-=l>s(Mݤv3t jNtMwy-Zضmر{edd8.әJO=Xx뭷:twr;PYYٹ駟oN/ƍ{ UA=֒%K EEE5>q;j6M{[s^s(33],^"o߾hѢj^dw>+~s9Gt_(o;S..4L3 Mj7C&xwСA=Cby˖-ӤI͛{Kˎ^xk wqGvvw?xxڼyHHgp&N4tW*I1@]$?WkРAj6- |NP\xY^Z$.w_xKO8l {FLi4egnDMNd ~eeebOxP˛5k&(~ R{{R]tk8*o~W'6|yqҝ}tMn :ù,A_"/^8Z๹9pѣGOl~.nڻW)AKbsCm&QcnGyD, 6l̘1oڵk6IxAإK. Cq [nDT:vؚ5k!"2VaРAF:r䈈wW?M4q~Z8%\QQ1rHg_yf¹G<|>8ٳgVV3X{YPY-yqk&"%}D~'[ll!N޽{O4ɻWt3c^a 6w9PЦnR:5;k&YYY)ȸq.Fm'pWiӦz`@Yju>p @ׯ~СifFݕA wڷo/2SÆ .'6W$3f5tMnmʕ"_2D /w,`=Ev"S-ߐ uݚ۶mӧ wqCfرC&<ws>.sv |l>s(Mݤv3t jvm߾]dxAouZE(KbiyCm&Qˑ!b~?ƌ?_ᮼx@E'Nmx[c(ؤX{gZ}P@AIf8@8H;|pƍ;vuVy[XzuFFFRRRnjS^^.&mˢPL˻ (hS7 ^{- %%%VVظq3 Mj7C|877WOq3 Mj7Cpɒ%rv@y'd>s(Mݤv3t ʲʕ+.]*ߡƝZ̡6uq(/'O|19D D/ߞƗں̡6uq+--:K/mxCm&uĉ2t裏[R1>s(Mݤv3tM:g~GΝ;_xiӦH})N*nkgnԹ7xj9m΅}KI s[4!̡6uq L \@AIf8 .Mݤv3tӄPЦnR:@iB(hS7 t|!pPЦnR: o @AIf8&&$%m&@(ާRÔ{9PЦnR:RPPPXX>6L^lu9PЦnR::vwa]V[%P@AIf8pj N===}P|0 Mj7Cѓ۷o/hiFFFJJJqq<?̡6uq@SN:t SDkV%P@AIf86sL*[juy^z˃ (hS7 |>_jjم6m=T8Cm&֐!C޹sg s(Mݤv3tZvmJJh]v{p1 Mj7CptI4L /P@AIf8;|^̡6uq a cnZ~~W&O,bnjNd/++ $ (hS7 Ap9PЦnR: bnA;N3JMj7C@~edw (hS7 $0"4|rys(Mݤv3tacvԩPކ (hS7 \~Pww=99e˖D (hS7 EvG`p/))3fLFs(Mݤv3t8mNp/--u>\oݺ҈쉊9PЦnR:'Nܾ} +NRNp6uq@8~gbI&Λ7+pR]V:tgonDvٿ>bĈ/6u͐U]>=99UVwMj7Cj/ஈ^%%%FЦnRR;HaFvmTMj7Cj@w+Mj7CjjMj7CjPBw";hiS7 ;¡Mݤv3vPw";IIfH۷ LMj7Cjp.&¤Mݤv3v §Mݤv3LB&&OIf L4L6u0 @h>m&a00 |Mj7$aa@nIDiS7 R^^}*5Li+KIfHJAAAaa0z} hS7 B|;vt0׮]+֋ MIfHpj N===}P6u͐@Adۋ.xR\\,xhS7 :uꔞޡC0EdOMMkAiS7 fΜ)ZeV.֫W|ylMj7Cj5ϗDvM6|nZC q"{Ν*CIfHvڔ0vP Mj7CjptI4L aҦnRR;N*C0iS7 HH~?;;;gڴia 裏n$~Ჲ2?jՊ+Usɓ' @"ҦnRR;`Dv;n$?q$$m&! OdG"GIfH@b@ѦnRR;Dv$;Hn;? $ m&!qͯFj۶-ܲgϞkFٱcGӦMqJQm۶}viȐ!/EڰW,uZ\}͚6mVf{y_H6H Mj7Cj_ۗTUU]˫~SOq=#QJ- Z\bk(5˾BPA% @IfH@+#=;v|w+X'?s_z} nݺnݺC\wuw&/$0S z/6pwMj7Cj_؄na D> ߿^,xwqTTTԩӠu$ǏAWU˖-oQDgϞ :[nMNN>{'&*\W$o)_z% "qbaĉz۶m]vٱcٳGZ?ҩ{G* :^pK`dqM_Ha$ z/6pkMj7Cj⑈J۷o3ԩ]$z9 ]u~n3f{R'"v< U'%KZjAEjoԨ*Oիȩ"5^zkРO?_|!R8X.)) ւHoX+׿:+֗.) C_=]pI'qM_HaN+PؠD#FZMݤv3- TgY{iiy:2]vܹ]wѣ%HNb+H*++/^?m@gB7z@,dee "D^tEFr=;2hA/s!^]ԩs2Έ#!I :$W@bPhd?D6uжR& jޡC.]g̘!Vڵ{󟽻䥗^ /9~駽{G~t ߩS5kڴiӣG & cƌ9smQH];2/rnfЫxB}Z$AT#i? 򴩛n5upQ"i? 򴩛n;;It"i? 򴩛n$;C"i? 򴩛n$;]4#i? 򴩛n$ ;W#i? 򴩛n$;Q#i? 򴩛n$;KDL@iS7 m H<wċ짙ӦnRDpA S^;ӦM~} ?яV\,.|>_iMj7C0I sܸq=ܿݻw5~^zWqE5o0`Arss EPWHӦnRIj|K,qY;3'Oz9Dpߴi?77#HӦnR)a80--mٲeniԩ"寿z˖-bĉplذ!33x7.m&m@fAAAFF7|㮩3fLNu&+:Էo.]={\zw%PG&ڥMݤv3-S`7n\NNq!n]Mj7CP˽O :u߿? ? Ҵnx䋷abq4m&m@(>SNnpwX#֋ Ea"MIfh[0tL'; S,l׿<q4m&m_Ν;.]vbMqq<q4m&mZfff޽t"fSSSSy iMj7C &Vy7Dpϗ!0Mݤv3-PHNNVZ=D6uж@kNdOOO͈CL@iS7 m 8}|510Mݤv3-Gffh|5a0Mݤv3-T0HӦnR:O}Q0gΜ)o1J)_b`"MIfh[ꖈ&M*++Ǽ+VȫbÇw? Ҵn'{8HӦnR'G ]4m&m~"{՘HӦnR٣D6uжDEPHӦnRh٣iMj7C5~"{!b"MIfh[ï{kvѴiS-U5jT۶mo[?^%L@iS7 m @=j1U۷/))ʻ.<^L@iS7 m @È裏iӦK.;v>}֭֭[Nݻw+ѣG˗+a~w:4h{n׮]Vz޼y{XĄ%-_??Co{UW͝;a HӦnRHiur~<KKKׯ~gyW_ 'N>|z_|MK/ LÄna Dv]ހX^yvډ>}L6M,mv˖-vqz;w^! Դn Ҫz0?k?~x~n3f{Ұ/Bdz'KJJҜ}4h O/bge`j&Ϊ^z]RR0+Z._rʁywsYD~O^iS7 m @h7vڹs^wuG ڽEޕ a"VTT8RT/soA,;!. Oe_6uжD:СC;^۪UU؂0y#FCdxt0p@QJ =agKOOO/~!`.Dkc@MIfh[Fܹk׮^zhޥKݻF6}߾}?;t (1cޥM޹s^y/eqYhS{Eӵ1yWMݤv3-Ѥċkc@MIfh[M Oe_6uжD=:5˾5m&mHK~6&/ Դn#'#ژ+PӦnR:Dpkc@MIfh[v}"ژ+PӦnR:'#} ?яV\,.|>_j~@MIfh[&a7׿{n׫W3& 4hPnnnaaj~@MIfh[&a~'"/YDqg;N<}ӦM?7_B C}jMj7C06́-[]sM7M:Udt_oٲE,8q΀ 6dffQ3”}jMj7C06̂o]SYY9f̘N:uM+V:to߾]t+{zJF) Դna lƍɑV" ) Դna6̃vҥ?/--͉B۶mj+PӦnRh :ԉ={{~@MIfh[UXX.W^Pce_6uж ]v ˾5m&m@8D{1~@MIfh[jQEEoOJ,٢a ,ګz^6˾5m&mSN}gnOP+VW%;w.\Pܾŋ:uJ1~@MIfh[,8q";;ߖ kʔ)7nq챟+PӦnRYYY_9"nPM1~@MIfh[j/رcrC[fK'/ ԴnƦMƧjK.oy̰+PӦnR)**Zd@f͚UUU%`?yWMݤv3-53uǏA ,77W~@MIfh[j瞓SٳO<)`?yWMݤv3-5'epժU}jMj7CP999rC˓o +PӦnR5kbƍՓF{M65mTl¾B]_m+PӦnRPjWBwgϖo +PӦnR ' $fnjw뭷6iҤEsM75lذ]vsν /ڔ b_zjg̜9sҒzyfiuy =眥 Ocǎ5kvw9rU^<8+itg}v΀LeC駟NOO8pK(v5Mݤv3-55o;(//1GSLq?1bX/0KWUU_l╕#G4h$4qo:tH &|wjnqs9w(,, !>M|;w[DXwV.V9Ľ[QQfnLS{YY۶ms[;v8,YNj߿X^n]ƍ-ZpFM$hP]v뻯p]n?gIZ0=Hk׮}ǎ{UWEqb/{u֫Ԍ6uжԀij߼yӢF]MewSO=%wj 4Fz9a[l3K34iҼysg˻Ez1_~"I=Ç~_.X7oިQfΜ+ fnLS GS}ʕ+W?~\4ygw/Pgr`X:L+ſѣGO]7?oܹ :4##CLs{@hS7 m @ ڏx|6|޽={r4hԨQbL{}IA]Š#G:+{IIʁa=0A3fLii|/_w;Szũ1|ɖ-[,kiӦFn_ӛ7o޸q㪪*^ӵQ6uжԀ6l+WyȐ!"P^xw_ee3x7xcRRR6lxvկ~դIMA>(*sWPݶm[oW&m&!'=r7N]W4[=Ъ &t]̱c6md\.Zbz&۾B(uw޽k-R ¡Mݤv3Gj;W#NPG]w9{wN:%B ? k׮mԨX8|رc[hѬYȑ#ȧ~:==]TH 7[4i"eӹs^r%iiinNNXNIIߝyyyb8Prr'̙#+{yBLsRoWMݤv3g~"%ݺu<>gX9`@>_^,,?VG!?TTkʽ{wfü.A_b{m/lxee &~Tz^w?{}5w9~$+N7#V#SF\g9xQT6䎻{ }C܂uܩxǗ.]/O5ַo_>\~;v8{l wnH-++ {۶mӷz+333.Ų8XvObI&Wiiϊ-mr|-Z}&N9 l-Z䜭[\O?nu8mڴ.]޽ۻ]v$q̙#&/oWMݤv3~jߓwo_"T;l޲"XII [AߐQ }z';+՜g}яDܹ}ʿx:Ç+vO˖Qw~ok߬_>0zDSť˲ѣ1cSټYӱe˖͛7}yfq MEEEo(vqs%2_/N_~";+z)|WkР첡NKz`ťp*\tEO?weS+K>OL^6"@KIfH /%VVia1׽ǒ۷6kT䍆bHoaVfüpw <*"_sMϟ+դ3t.QfM8 .8gb_W$_CŅ ^b1!nfL7 v);KOG>}zzzE+W?~\ w:ygx֭[C?xv+ _zo?e_[_jUc)]j?]}jMj7CJxv7:vGO:5AeglhHaC}CE5׮tz]vz e ݿ:52ѷwCmh_l:j8ɞ7/wu"{r5H°aƌSZZ*wڵ| cwoWTT Ol_}2#G L'$i8?wޓ&MrnnM$pر5k8#oK,Y"+++O/;c)D6uж"o y|?;>-?a'N猿Iߢ_W$|ura.bwr߾ݝ8n y{U}Փ_yg>(ֈ?cƌ={9ݿ2vXWСC4iҬYѣG"_|K.ˢ=  n뮻n:>zMM4jѢv;>/?7n_ظ;r&]rI wn=?Ʌqy eߐ ZDGw~{+I9nWs2)A_ 27d5j8zm?}}OڽF ҥr LRyd޼[Խ"5R;HЦnRV yLߓٿ;ok+trssT("ɾ5m&m%|{e_6uжDW_}l2g'0U ڪUk-˾5m&m@h^WMݤv3- 3v Դnaa{a_6uж L4a/+PӦnR;}jMj7C00c@MIfh[&f찿iS7 m B)//>d?yWMݤv3-}mb>EO^iS7 m B|۷w0WXѱcG.˾5m&mӝ4LƏ/EO^iS7 m 333Ep SDԔby(~@MIfh[ֵkW*۴i#u !'/ ԴnjsMII A.˾5m&mKOOw"{rrr۶mj+PӦnRh >I}{~@MIfh[UTT|޽{w '/ ԴnpPce_6uж s ˾5m&m OSNY ٢a |W8 wc?yWMݤv3- M:gٺujŊ*Xعs… uѢENJ}jMj7CƉ',Y"# O2eƍ{:4˾5m&m4++K+" NPM~g`?yWMݤv3- |";vLA@\Yfw0}jMj7Ciax˾5m&m񮨨eG"5kVUUF?e_6uжx?rVYYYnnF?e_6uжVQQsɩsg>yv+PӦnR^u.;φ VZ%='/ Դnĵ9 !//O~{O^iS7 m kf͒Î7֫WO^kH 䵱*up*5mڴf͚5lpÆ M6j~@MIfh[@\S4zRڣĔg[+TYYXGyW"rfϞ-='/ Դnĵ8M1 zrAKmڴ)peБq!/;m&mqƩ3Ϥ6n{9z˾}n&Mhw9sҒzyfg޽{ ԰avډrO?>p@cǎś5kvw9r6w܋/8%%euG^r%bӻロ#^b+!a:>\Tnڴ]w uVGaa &#q7p;;2ԙ{_ ɁoP7};233EpŎׯwr$v iS7 m k+0/zSh"t۳gɓ'K|wqgϞ=zL2Yl2d+++G);xذaN}{oEE~:$ a?pUUO>uF>CbdVV'Ntjgw0ځDAS ʠ17ԙ{K9"NLiiP7>c۹s@\agHbTHH@ӦnR,Smۜ-RVV&;୷rx}-Z}vg4#<=|pw<}222a֭kܸ=wbYܤICy ֟{~w//yPg{?gXT-UN$k׾cǎꪫ1LU/Aj6uжN6mһ8))_Du5jY$͛7 B Ç~_.X7oިQfΜtˑځxMݤv3- SD~[on_/^]$-3f}+W<~N>9q2A.:ن?tcn3 z,ѣGO]8B0;wcڇ!Pi Dj6uжNɾ>s'tթ}ȑbk޽'Mw_ٳgVVXk.caaaEE b#\s"D1cƔ._\ʠ :RZz-"ǎ[f?tc?Kd~[Q'lٲWp6mjԨx͛7oܸqUUerځxMݤv3- iSEZKDJKK0o _nnnNNΆ |Ē3fݻW~O^iS7 m HH_UD$ ~6jh*Hne˖SJ }jMj7C00c@MIfh[&f찿iS7 m D̾5m&m@h^WMݤv3- 3v Դnaa{a_6uж L4a/+PӦnRR^^}*5Li+~@MIfh[JAAAaa0z}(+PӦnRڷowa._cǎbwC]}jMj7C#F;i˖-KKK?~<Qd?yWMݤv3-P(**JNN]4LSCE}jMj7C+BJſba De_6uж@m޼y)))qu˃]}jMj7C5ϗDv=:g?yWMݤv3-1bگj '/ ԴnZEEEaу˾5m&m@8=a?yWMݤv3-ǼyD{1~@MIfh[/B4Rye_6uж@=Cĉ~Qg?yWMݤv3-Ps"{YY_{,+PӦnR(x# '/ ԴnFv{+PӦnR*;u~@MIfh[H:d?yWMݤv3-]T.(((//7e_6uж+3v[N m0g?yWMݤv3-pFv"ڵkܺuk"{m+PӦnR8j4hPJJh:tSe?yWMݤv3-8mNp/--u?\Zn݈~@MIfh[ L4ir7ꫯ:IQ BCiS7 m 'NhYC&O\ZZWw)33ɗbc iMj7C";hݺu׮] 14m&m_]>=999##;HӦnRx "{9D6uж@;]QciMj7C@0idGLa"MIfh[_܉4m&m@(ȞHӦnR(w"{b`"MIfh[ DD6uж@ ;w$' ? Ҵna]4L"{`"MIfh[&f"nMݤv3- 3p7HӦnRH@iS7 m DL$M Ҵnaa&&iMj7C00 w4m&m@h D6uж rSaJ[_HӦnRRPPPXX>6L^lu"0Mݤv3-o n\|yǎ"0Mݤv3-P1bDzzܝlٲCWHӦnR(%''gff.)Պ塈+L@iS7 m Ԯ *EpȃoHӦnR͛7/%%3|y iMj7C5ϗDv=D6uж@kĈNjjb`"MIfh[UTT!f=jb`"MIfh[;|5a0Mݤv3-Ǽy;E P4+$aa1 <0.L 98ƒ!eT D!OrnߐTuޯ?T߾OL|cjԍ.e .]YPP? S7R-3f|Gqqq jԍ.e EEE3jԍ.e 63?Wnv9([#;Մ]!GAp;\\MlGvpp5aFjq&3RZZ?n S7R--#;dp?pSO=? ?Wnv9([*ΘwZٳtn0u#A`}p?p#|}}v[ .&LHrP~ΰ^PPH=z [.&LHrP̙3>KYR[+aFj@(s{ɏ=Xxxxpp0K4nv9HrEvEÇAAA>(;4Bԍ.@)#0u#AjR$IdbokxS7Rvtpw2Qnv9Hz]64*ԍ.b S7RvG@H wDvgS7Rvs$aFj Ăٳg$LHrAa &";]R;P0'LHrpp &].BNBp0u#EI(nv98 yԍ.!'`8O"$L S7R\)-- &(h S7RvGRRR233ջڂIz8ԍ.P53---,,+&LHrL7. wV0SSSNw aFj`"++'88;L5!LHruޝJ%w󣉁6aFj`#w 4(99 S7RvsUUU,tCnv9HBƍcP!LHr`FFF{nv9H`IP$LHr & $aFjuSPP&&&.kϟOn<=[lz*8G >7oަMΞ=4&w<vڹsfee@D =:k֬l>߁G۱c󫫫0u#Aj'mڴ4 t ]R;8cÆ 4&ܯ_#ԍ.vܙƇ8h| ,X#ԍ.UVV.[oXر_NgS7Rv0TTTg7h]6w\~S7Rv0q֭+V uw@H {:tOmи}| ?Vjnv9H`bʕ|dPD~&LHrDRR,)''M6|=ziӦ|$Z}Ƿ޵6g6iٲeXM  )QQmٲoLLÇNMtV ë~:0\ ;={T[\Ҷm[}O-Ͷn]R;p&ҥK/nӦMvvv^ʊ [% Yp-lAAAYYYҥR;X0u#AjR:u1ctIIɄ ڵk׶mʕ+He=O222 žC aw׬YӾ}{'&&Ҵ/M Cѣ3-=vZ???oo'q\x'lݺ5bʔ)+իW{yy#Fݧ N-yٯ kyϟ4MII1<SIvnv9H`}Ϟ=ntllO?}#GN6M?>T_|ūWR$ewg̘QYY@14..M?ce=J}5Y~G9vRJQQQ,FzgC^^^ddYX{jj*rj瘘~#2[SDS>sL233՞Gv 7Bԍ.L!geejJy?Yf8Y_b҇j5%H4Wf[n]gFFZ;¥ӧO>,88X; m=ܣvضm렕ޮ];N~Y]iisΝ"K.))PwG{`tf8H $LHrTj_t)L3~h"//>k5ꩧpMG 0u#Ajs Yfnv9H t֭3g^rOpȼ{GaFj(--5k{cv7#aFjN>s\>A#̏ DŽ]R;8֭[+Vذa~U|ҥKwŏSԍ.d?~ūV:rdff._<))"ԍ.͛I5V4T0&Eʕ+{mp0u#Ajp &].BNBp0u#EI(nv98 yԍ.!'`8O"$L S7R\ j(+jqhv*.%QzEQK]Hek=Zg޵kW\pwmT{ԍ.&Mȷ:AEvE[w…;wh"jp˜1cXˈ#:#߬Y3}6߫WN:7MOXɓ'Z-JmpG%Sf t9ָdLYczzzHHHϞ=_}U%֭[wmqpaFj&._Ls7nQ[PP@R8kg?Yƍ)4ߙwӦMh| Mo޼k۶m4GEEE!fcbb+Nl=.;v,kT@JJ4`P{jS7|{;0u#Aj[x}fÜ>>4qڵ:t_R56m5~GJӧO5kR;hтNӧNjٲ%z!ց^-PÇkgwf/]lٲw)wASM핕~~~vbwHKnv980P툢a|||qq1k+,,l޼yEEݱcG6MoYf͠A{ygOAVH 6l=2_aj/--Ƞ#G-dҧvzԺuk6m^{]S7R` 68{n:6+L8&h3w>2N:Đ!Ch򲲲CEA1wǎjb?QLtHIIQj6}RE;p{$\rxxq;/rssizԩֆ j#;k4nw`ԍ.'?})NǏ<2k׮]njCQU dHDDDϞ=wI/^_Jݨn/^-O<LBCCGw)wF۠8# С={ܹsddiFg~]S7R` 8ܭC$Dvπzv!LHrp-`ڝbl%;c]S7R` @Op<B[@ {C@d0`ԍ.'?P ]S7R` DApwDvzv!LHrp5jr{lGApw=Dvz6%LHrRRR233ջځJzlJApw%DvOz6%LHrBCC :PlNl2EV ,\cPڵ]zH-Ԏz%LHrޱcG@}7Nwh'OcCjX0u#Ajꫯh|v҅;MPdTvv6Qݣ@!4 S7Rv顡4P4pA~Q%R J]R;XY\\ QGj/INN;4TuƊ!iC8aFjVVQQ.QzѢvF=nv9H`q.Q>, ɓ;vHn=T>aFjW_]@cF50,,!ݢ S7Rv*N* LHrwR+`ԍ.]RMݺu+11p`ׯ*))yw R;]lB W_}7~˖- 6oyJoAݻ&]RMXm)o0tRDpƍ+W7؄0u#Aj˗,Xm۶-[,))s6m2=ڴiSi!wZ= <2ԣjvnv9H6%P;Ujꔼ"##8MQQ]';?P Ou"uw^ԁp-J&7r7dʔT{uN ~:paW@j/aFjnSΤvV+++MֳgO?4)++k߾}RRRyy9m[zz޽{Nu"uwvtmSgZvy_>>gv-eA_X ԁZ\W]ݾ]RM97|ӪU+())0aBvڶm_rEoˋIӣ/^|'[nMsM2]ش 2]f ]OGӾ46hW^ .uXvĉ ]`OC  Yz5m3޽{?~5?~ĈtHtBWPR;,ajor\)SMWOV*IFFU9Pt"L+YlF6y:@j/aFjnSΧv*j^h:66駟|rqqȑ#MzI/R]ZF3ϔEFFΚ5p;cƌʄ*qqqlSŔSѣGoݺ.u;v,m]KP쩖T|BRSSBEH@WMsLL [Z~WPR;,^PP@' v)KjU9`G=7),,t(fUu۷5;n_ԍ.ݦI4ԠMYfgΜa@'wuFQQ>}4m۶`Yn~~>Mӊhfdӭ[f}N<9qĠ Zxtt4{H]piU}Ul TKx5Yâޮ];N~WPR;,~ : V])st83jav#{}w(ͫK0lngddx{{:n_ԍ.ݦI[j'~}O]K]VV,u6;f^h|VNNN&M-^^^wtX[oEW&-Z0l~Lvy橝2"Մ/r\)3)S[;Ɍ*m6JSw(fGR} S7RvCjg?޷zjroϢT䌌 tS}۷igtZkNR[mۦ.G|B:ٳڵkTjr{҄@yjWj>NkJaxj;vL[* gԲrQpG-ZQ͎* vnv9H6UNbbbƏ_PP@?cZZ'wwȑ#}UaÊ`ViK&L0p@֙-D4vpB>}fΜhQF=S죮'Oٳc7<,"133ի4yܸqsttpd S_~ٮ]?}Y}4,v^^5&1c+vv?˃UVuQpUxH%LHrmnIڷoߪU]G~^zr}GUذ">3>>>^^^?-'.D4AopO9tqb}+++oxXixbu>l0j 5=R;,aj'{eSB￯J֦M_fԲNqTG}߾}i^5F6;&K ۔0{0~zBHI5K ۔9p^= =בK Ԋ+h ѓ?`jҥd ^sƍH%LHrm*55|y;{͛L}t_NNNZZ|MS7Rvyfc~݃{EEE .]i&t[j]lB ŋ՟pB_u޼y6WQQtRnv9H~zȁeggر [nO*74؇0u#Aj+V;ws`[gO?'Vnn.jwԍ.nݺwځ ǎcpڑ#Gv@׵1؊0u#AjxwDiv@Rrr lhg.--]aFjl/ܸq#@lذ/O3Dv LHr=í[/_We쥸xٲe0g®],YRTTğr`atJNNNJJc<0u#Aj$ϟ_xʕ+,YYYB*FgCg XѣGWZoU"΄]Ryfjj*Ab4P&{'ee{޾}^YFeH,+%$LHr0PC aFjlA=nv980PC aFjlA=nv980PC aFjlA=nv980PC aFjw@?lJ 5dffwQ.gC=nv9H`MUUUJt} G X=LOOgwzH-Ԏz%LHrޱcG@]zu@@ԩS_cCjX0u#Ajڻw/.]Pp >>>tx4{T(S=[C2aFjVH4׏ uZNV"LHrfΜICϏd@#@/""CBӨ`qԍ.ח]zݵkW| '~AAAjjiC8aFj7l0vz=Th^~;R=[|O ݻLJj+h̨Q=[C>aFjBC`IP]R;XN*w@B C]vm޼y\o4PN sʕnV?,,,X0u#Ajt;cƌ"-v7;eeffΙ3pO=gnڴi޼y۷o#LHrڡ _>w;v'-4Jٳf:z(?PJ]R; XM~~̙3 j,C  ;u)I?Q%==}Æ knv9HFw;w.e_~]q LHrI*3 СC,;%&&VVV0u#Ajg4 49s ȟ]|9))=Є]R;G3gδiF{]vk:2\856i; uK wI#oݺŏ!hPԍ.̙GvE E|ӆ ; 4?1.;H޻w/?A S7Rv0!.4O<9""W^t뜑ѣGA;w5.Y:Scdd$`֘ҳgW_}ϟNYF^mg-AppK"7nXr%?A S7RvpęȮB6ӹyf:Sn٣cǎel"%%el:,,Liڵ?~y=vZ???oooY ^VM߻wǏϏ1K.A.\?Y;y^zI:Ū;:VW^ZtXh>>>˖-s(e?C4}XgHV#LHrp}s ) wu̘1;QXۨ:tx̞=[ ܹsddi Ss$+L u8)Tgh:66駟sxȑT4XO⋔>)\rzԨQ*ajWWgc8t?vZ>z]_|ѩS'h b9'O8qbPP+::P{a(e1Vuj]O E>{FjGdoPCڹy 5.ePCYYY6&V4iDے7_!oY-Z0jժaÆij>b3f {ѡVj^< |Ϟ=l:%%;MTUU5k֬*Z͑#Gvpի֭[ nv9>-puҤI֭駟Ν;[4mwt[b8bĈLB *Zƍ85h\2{R~Ā5S7R\zI ;v8Z7onv :^ Uњ6l_ Dv+nv9>-!CSSSՖaÆ͛72:M߾}ĉ4Q]]]XX::t(88X`H?"vڵdɒ">A#S^^dN X0u# lA?PSRR{ATЩ_~o߾`D?:^|9~:3~իWϟX0u# lA?P'M5=`}޽{W\(zSSSlD/_0`74]"X(؅0u#}"X(؅0u#֔F@vzT PP0D.]N~055===<<+@BUnv98Ă; :u**a]S7R`Y|Oxx8w|}}U,CB+ ޽{nh?裃 ;+TE8 Q aFj^!گ_GjOJNN;+TE8 Q aFjҥK,N:{j`q`ԍ.'?X\߾}Yjӟ*a]S7R`q|K}P P0D.]N~@*"X(؅0u#Ǿ{*a]S7RwjEٳg@]p!q_ܼyyTE8 Q aFjߝ(W-[,##CwÇ\rϟG!*a]S7Rns7xOXQ^^aÆ+Vܺu"X(؅0u#]Tz3g,--%8! v!LHrp 8)wrʬYܝ! v!LHrpZvvM^Qg*a]S7R.Ν;rJ~"X(؅0u#R~iVVHrrr^^?L6TE8 Q aFju~׹s ᔗ/]P"X(؅0u#:/^'jVZdU*97DEs,aFjgϞC@:vXZZ?XAU&%%%33SNw,E:K.@CwR͡*TUU8pU(P;={W+nv9>Β%KbGDӦMO,_Zsiӆo ׯ_Gj7/Ά(MS ],C:]|>-; D?K m'p}|GzAU/yHP}||)[jX0u#uE䊊 8٭nw*..{\-G{Fj7cNCEvj;X0u#u&^W޽?_fC=o>#Fhٲe.]h.}j/))0aBvڶm_rL=vZ???oo'q\x'lݺ5bʔ)j`k6C *Aߍ*:$-[[bR]K#mlP CJJ~gMU/R9TE䈈Gij;X0u#u&R-//tkUVVm߾}YQFĔRׯ>~˗/9rڴi]e%lcǎeˏbU; m30;䭷ޢ]E\)>Yť'NhCp֭~?ӤImKvvRnZbl `]w6 krx=x`:ԫ}oaRg,..1bČ3OÝu4~q͡*eQq!J*X0u#u/^Hٳڵk4b ɽ}v3u e BN:l۶-((HYvZڮ._>f.8Z-a7rwmHYY٢Evm:Z)l… cǎibU9/=vs`e= Q|lA:uN?#̫WƚG2rqƱXGGG;Č?-9--k5ꩧXI'Oٳ |zO>3gdiOxGEE%$$(vuo} v̙?BU:v5 Ykqk̙8x:h/&;k8p͡*%''P]OSNOE l߰aèshhoP4iR[j|r֮OqT,G &Cbѣxॗ^*//g]Plњ ꏵJ&A1ӧK}h푑?6Kv-n7os6umڴObB/wΝ>ϟOCn,0gC# LHrp}rn | v*xP1cϬi| S7R\\gҥ|vS]]nUid)I:Cj<7oDj7 {#$LHrp}r͛7|hP޻w/?XAU!7F:_ʕ+oݺV@Ul Qnv9>Ԝ9s6E $%%jCU`VxS7R\\*33sǎ|h ˗/)Ԇ٭nv9>ٿhX{ٵk?@AU# #}֭[7裏򢣣V9sM6ڻڵ<^ Y©I&1`]Bpo ]Ov b=3E'*zad/++J [XXxĉ] | v@pxԍ.'7(((HHH;P>FvC7k֌r*\pa̘1aaa#FP,\sά}ѢEmѣGxxAΝ;3gΌ LKKQjg'OՋmȒ%K35겳YczzzHHHϞ=_}U'S;kNK?:fz(ݳ S7R\ܣ >nF_رcP=36m?NvϞ=j;l߾Moܸ;MPʧiBߢMe?c;vRh)lݺUy]MTvqJbbbu)5&RRR̦Յ0k?!3~xHk rAp`ԍ.'wʚ7oڵk;RgϞ},X@/g~ )TEOލvƏ?GM>g֬YJMlڴ\#jP;E:t_a/]DJ -Z:uꡇq>?lpi&"[lݻ7kJ:t4L7oޜͥ6\99&LHr>ԋW~˗/O@Z7oK>pB={ܹsEY}S=(?~|4w̘1;3~_-={v922rڴi 8Ss$+Lv$U!]h(6lIwFa@d) )LHr>@C@`TGwH%U!]h(6lLnw&U!]h(6lMApoPv'U!]h(6lOApo @B S7RP0Pm0=Tnv9RG``5 !{ )LHr>@C@`k<w@d$RRH} n֨P\hQ囬0"ǐԍ.G4 Tp 68 Qh@ROH} n! Hj S7RP0Pm00DI ?aFj#u * X7DWX1e6]ZZڡC7otYYYlll=uO>]t޽;Ӽr!U!]h(6l`qxᇫizѢEӦMcÇ߳gNIINUUU͚5d*0u#ˑ:  ,N?D'Mnݺ~) ܹs6NӦMpG믿8pĈIIIrτ0u#ˑ:  ,N?DO<@`IjSL F*Xi{Pr0u#ˑ:`/׮]?~͞= lhѢEOXCvv6QLܼy/Xb!.Fc꡽h1w۷ogE!]>cƌ"#޽o℄ݪ*{.\Ç 1Ц:D!΂!:(LHr>؅Evne8ߔT֯__^^ΏiƇK,ٵkx:(LHr>"![VUUU Znn.?"_*7 S7RkŊ7JKK;tpM.++ѣGnݞx ߬Obbb.]wN4-Dv0 m۶;wn޽;utFFFFDD :LPoP94VgΜy~}X;۷Ĝ9s}Y,PPk&{」G'ԍ.GC4iҺu~駀sQ M6kP:p#F$%%eff Bd'NElҥK۷5l_hӁY*j-_J?pYf㉤*0u#ˑ:PN|_XGҧ|M,^WUi kWvpܡk׮gf~@f$=zcT..''G_\^]=v#LHr>C)pO8166 (V^zQgndffR޼yСCYKiiWFFM9rD}G0ŲE9ZVaa!ew;v쨈l 0??n[ٳg[n5wxvQwGILLhmpyΩ.t{{{SO D#£ BZj|'\1z{AюU/IuHUHaFj#ǘώ0kdqqqh!C(^v:dP*,1|}}k!?O:U9嫟Qe^N^֭[Ǧ_yz8pq)P 뙐p]1Z/ׁ^<BDߜppv*&۷OJJ*//LOO߻w/ߩwDxԁZKV11눓|G"g!G{FjS7Rn̳cܹI}ŋ_vA/f?3((H6:R1tJg}||z={l'3hZǏ4LƌC[qbo*F[cΝ###Mfڵk'LR~\;P9[p!.Wӣ GԞJi<66I!IDAT}hͧ^{5z%L}e=GSZZJY_~NKx4)..9r$f*Z ѣGoݺ^'k2s=Wj(av3h;yNI/g͚:(yǎ=**J:돤~cX#mƋ/H/{(rwu3}5 n^w|9_ZHaFj#u͜v\Ϟ=]RR?.ڥ"o2Gq&>`?j엝222R%uJE?3.Si֬ٴɓ''NDKbu4oAA;٧.Q Of۶m f;Gkvu2W^GGRK}򱶏z6p #uZ4oޜ6ӭ} ;k>̤KAjnv9RGLqipI aƙ(*5֍w-Zp՞)Y[x.8qB4[n}rssnj޵55VZi;i}iҤ%;;KoLG+U~cO޽GU{ H@["r B<ŢS*V)X+!4‘^C""W+"DF/;Yew̬skfڳ^2ۨ>wiqk#1b\;mٰr#knjdG^ُWl1JKRݻu:tH.Zw^X]jY}cմٸ 57uquۛ/+ nٲiy Mݯ˾;WqxyѶ!S i86G=ߪU?0~뭷kіuD/ ]cLݤv;V$k!Eͪ Xa;vȥƍȑ#:&Ay;;v񵵵׮]v*--URM0aСꡈn۶gmC9_/mtolfDSnF%Vs.#)^tESNUWqxUgHy'6h"[m6z;2lذ/pj;qY~iHc&۱}$Y=5;onU?FXiIj'Oɑ3k֬MBåsϞ=#~[n}guVݽ?w1cɓmv+|?p\߷o_|1Z{3$A3K}kzkCCSn{8SNr766O,|c2W4(#鶹 Η I1qnGOhgGk_|aܺu6mFQ5Mjc5H*H<5"&~ p'RƱ!nj]tݻ+GGY>}o靐eaVtݻW&<:TZZBj4nRGr|r\ߡȑ#-ZwB*YGeee iLݤv;V䨭ULٓOHfE{F'CY͐Mjc5H/XQFC(TEr0+j.\ꫯ9@{>Ϝ e5CS7ݎ#i~ߩ|vEeHfEŋ/YDO+@֫^[[3j4nRG2uE8Ům޼yE=٪r̙ϖf5CS7ݎ#ԧ!ܨ$`VرcV1cŋo߮G ;lٲ',--!nj_|__u0{C?}%#[Y&UTΟ?rzb̊Fuuu˖-| ބPʕ+{٣!njM(StΜ9oxīyg͚E۵k̙3%VouT#_ìG"@VgLݤv;VU}YIuuuzR .,//?z>4IK [`lqgϮk(QȪnjE z:>v:uj}}>@ o߾)S XfMϬG"@VgLݤv;V%.\+ [}'ӧOOfp.Tꫯ.^X'{̊Hq(dU~Mjc5٠Z.znM|SL!y,Yyf,1+"QUS7ݎgO pxΝ>>^ 0oЇ,$/X@_1`CNիWeY)EϘIvF?o/G0{l}⪬oA˕eY)EϘIvF?޽wC%͛''r]]^a1+"QUS7ݎg7FwloڵkRB\ȉl2|cVDD 3nRl<~>ϤB;HI0ߘ(QȪnj3ٳ+ؼy󩧞Z'93֓}gv?oiFo515Nڑ(QȪnj3[.qD*UV999å%Ȼ1 yiZĥs+;Y\v"Scǎm}Z#nwfgpO=' Y)EϘIvF?S4Pӧ%ZO&wf0E!G(}GfjӦMuuɽ,pgGl7"q-g ?IԍԎ F"@VgLݤv;V|v믷nZ޿ ڵkwgp G=eT+**d{.+++=zzO~n8Rqzرcnn7xAmݻw_uUyIvl~N: 0wU_F!٣G[^ڣ|0|pknܸ}{}Fy]w;Rr'N.7 7E .;{4 #׻j%= $ iLݤv;Vԯ$sss?동9^^^6lйs眜mێ?^jzмcURNzƬTSQQQUUuˣ] ѬfHc&۱̶|fdRSS~z}Dyƍ*{[lYb^a1+"ՄB!WܝiGH0ҘIvF?G`|ѣʼn< .}lYGee̙3˨YdJtn|E]xm? oLݤv;\b9'{ܷo߾dɒg}رc$}ժUϧŋVe˖'|Rj5YL}&ϡ"BPQQ嶟7hS7'Xb^MQlϲe2~}sVo>R +WlGìTpB)Q>{!'CUn^8g$pTg_:ݑͼ#<޽{K_iLݤv;\9hpƐ xwiJw/ޔɑ SޠAd9TŘIv>s6ȞiaѢE2CbLݤv;\9hp=B!!|U1nRO@zM4F8o6uƑ#GǍw{W^y)sѣO>.:Vw޳>v+wyo߾{B3ҘIvFJ裃bO?'ueeewuj+֭[nWTTHp?Riبڝ0=%­]Vn\.RWZ%7q/!njPH-n'| vy)|zЧC1b󫪪 3B.3T?oWCo~;w^qg5CS7ݎAP4[OPzj% ~G=o/X/я~䴇$bpWqn_?NjO:ҘIvF qeui͚5N~r/xÇ%oFQQٷmv)wy{ȓo6Y]6m4ưaÆ{wҥzK|AVߜ1T ꫯʁ]޽ڵkx{~2=ܐG"X͐Mjc5@P(T$ Ν;WLǗwҸo߾/XR4_qSd}5Znllԛ 9x`v$P-'wI68Vk۷o>l^OЂ{ΧQj4nRB"i(@r-s[}"{) ///OI{#FnxW_jg=zĉՂvj߾}}}ܞ={<*X]]'?<<ֽ gϞN;̙3uʼ- xտoܹ3|bN*{!|1gƌGy$*V\٧OdW,ҘIvF ؒ?d9dȐ3 d+B6lتU_r[2o[G}طo/~qm>ՍK/Yj;vjt6z5wuWnn;׭[G\VR%r ˖- 7m޵^>͒kӱwȌ?YJv8*C~~+DDpO)V31uX> IC[)Iyyy\?੧MTgwj?C;a„~O?ر;C?cR%֫$1jjj:u~hڱcҥK'O,=}pe:4x/|֬Y/rg73Tݲe98=,Fg|ܷ=2x(VkLݤv;VBEPlkoqƅ^yhۢJR% _]nڵSKE̬ꩢ7.p1?GַbLV;-G+*[n܍y|Xc&۱} (*b P3?#}oz^veӦMtnonJgq{mAeȐ!+VPw+1z׮]r{ҤINjQQQ!8OqnիWm/u;ⲲSN^ڵŽ%%%]wuaOnajVnG!Eͪ%a5Mjc5@P(T$  ~ܵ8w\ݻw?իW޽YT믿4`:urnz{^^ހ^'ihuH+CϞ=G;Իt /|- QQh~_hllc"(ُ7`d5Mjc5@P(T$ f~e„ 3g[-[PKj`nj􁤩w U{#fd ۷o/))4ho-[VKj`nj􁤩r UQ._̊jܑ-[\Kj`nj B%%%Npw UZ]GW \\"x"Xc&۱} ꢢ"U.]L4I b2a{ڊWd?ZS7ݎ4gΜ޽{KXB]vϯֻìx ~ُǯXc&۱} >sw;<)BICqŬLyVZWKp!nj$ꪫD'Nءɐ!C-ZwY10=M<Op-a5CS7ݎIotޝϡ"јI∟;KKK"1jad?Zb5CS7ݎ׻wo/">$`VLn@ܹHZJҘIvFH9s~9T$by0C嶴pV31uX>|3R||a֭WZNRY͐Mjc5@ gR*Y1.\XXX--z'j)ѬfHc&۱} rJ |PK.NҒ|vC-% iLݤv;V$_***瞖`Ĭn)//O_APV31uX>d*[>pIj|v-A-% iLݤv;V$;+w$bP ZZJҘIvFHod'#9pB|>;j4nR#ZdWH49RYYY^^>o<'Dii ~|yJ 8_LҘIvFHpȮܑ8i7+ڵk̙rIOt /Mihƍϟo/ɤ_QKʴ!njD mpI^8Y͊\,XР!cgϮ[p<-LҘIvFHȮ 555~{IIIUU.}Lk.@*YfM"~!jveZ͐Mjc5@-##B!..]SZ̊ڦN'RϫxbGqѼʴ!nj 7++^\/**߿ǎuFd?+~SL!%Kl޼Y?G52fHc&۱} - ;sЄ[?+Ν;wΝi6}cǎGe[V31uX>ٷnݪO$***TLZNT"m iӦիWGqg[V31uX>ז.N:jر;vtĉ;<^kG&)++;CTii~8-qVinjiIpW=|}_~?_~wd#G̛7O?&/^,~P}#A**7S7ݎӼEv?";2?Ϗ餯۷/[L?q Vinj􁄲 1"C>fZӝAGLhLݤv;V$'ِۨ~6m ySO=Uo$Org'󟍼DheZFc&۱} wds'$5UV999\.-=r%4h(77w+V{Ļ_]krꛟhTTK~~رc[nl*+SO=w7inj܉Zs[(p{>SCNϟ?^[~) #ݼ4q-al+8>5kV6mOe;^P?'leZFc&۱} iw";Un{[nn߿„ ڵ;o˚2;/UTTȂ=XAAe]VVV6zhio֟']?Q=pQ.͙3'Z{v}UW^ȳvm*v<9#nذAuvxC#C?xǎsssoFjE"T<Ý:uM0`ᆱ򗿌1BGV5wD۝>`je0#nw q6iҤkF{(Z=>d̰v3T.whWUn4nR܉rs\'Onܸ}{}Fy]w;rq8q);v R[[JICC F+W~(⺤W^zInݻ*Z{F5f̘zٷoӧSLill<ͮ.^{4?m4m+k֬`$;.;( /rΝUg-K*:Loۏ>x{;?x㍅\ ^㌶.O_Wx C3DltJNyyf (**RdSn\gYz6ڝ|g猽R^{M6|l޴񟍼}mǣjwիco8xmӦMguh9NVgwPnuhWUn4nROwm߾}#F.{=whݺu۶mݝʒ%K.1f̘3fڵkQ/F[W)o]z2x` C-%Amo@|O9wKuuuNNN猶R!PPn3<3bghg#/GB;[刃ټ}="{)vӹcmYWl+*7S7ݎ.&#Y6j?FDz-wzЮg}\IqCm]9 5:[|Eֈg޽-[~0U+ݽ{ty! jklލF^VG_;~^k8x;]!!t.`x~ʴʍMjc5@P(T}n6l}'7Ǝ;~x6;v]6\ .{СZ{Ri'Lt.Y˳>{V=P.]5dȑλ=h"p{|jK^xmƍS袋W*;" VUUJzwt4hPčq󟍼|O\BP#F륏!Cx<.G|8ܳg< )Y{#xz}"nd}eZ]njPq߹͹|r9wZ~-o޽{=?%`VN~homN|_M69Qw;4-`gORvʴS7ݎAPIuL1Gb[ 1 (wW\vZr2eZ]njPD消zk:kI +G7 7E .;{+_Vcc&۱} (* mb߿?77X:kI 9A-GÆ ;wV?~|}}#54ej'ʴS7ݎAP~7[-[XB?q Viu96nRBѣƏ$"e-\AL˱1uX> Pݫ_u:TZZNK}ĝmeZ]njP^jpZ#l+rlLݤv;VB=2@6 GqԌʴS7ݎAP_W~FJjll{?3(6GqѼʴS7ݎAP͒&u)NQmm~ZjveZ]njP͛O^]]_*++gΜyaG֒ʴS7ݎAP;z3f,^xAزe˓O>YZZj!fJ\*rlLݤv;VBb[lټ&s5+Wqޔ[Vcc&۱} (*? b5Mjc5@P(T\5j`njPj$S7ݎAPqH5nRBxXc&۱} (*(̇US7ݎAP@a>DϘIvF k׻j(PVcc&۱} (*UQQQUUuχ.:wDS7ݎAPdP(T\\wg>iGH0˱1uX> @6 TpWܖi׻du96nRBfyyyEEEe>r[Z] $ؘIvF _fB _i; fu96nRBr .,,,pܖ`Vcc&۱} (*, tvP|Vcc&۱} (*|yyy2_>@X]njPP]]ݭ[7/CE .Mjc5@P(T8~3|AS7ݎAPxgRe>sؘIvF VC?$ؘIvF p^t@Y]njPuuurC˱1uX> @6sGv@X]njP7˱1uX> @v;rlLݤv;VB1#BpG2Y]njPMGdWl{(L.Mjc5@P(TY%;+>{MMM7T\\\UU?`u96nRBaȮPh…+(( ٬.Mjc5@P(TY"ܬȮxzqK.2Vcc&۱} (*lnAdWTpU/w-??C";ZrlLݤv;VB NuV=[Zt@"U1uX> @6Z;21u(TY"܂ڑRn0@fwodw21u(TY%lcDv7dLݤv;! @ >#FErS7aiB>mdƘIvCH *܉HeMjCBZPdpNdG3nR y;ϘIvCH *,Dvc&! !-P֭[H MjCBZPxSpȎ`Lݤv;! C c&GZP@a>D0nRN~ C c&GZP@a>D0nRN~ C c&GZPp8D0nRN~ >eʔ[N:`1"]S7'?&%b>D0nRN~ AqGv`1"]S7'?@x#;c>D0nRN~ -+w˜Iv8(T$Y8fdW!҅1up#-PhP(?EGdWllll!҅1up#-Ph^zz555c' .JJJdK̇HMjɏ@~w1//o-{2+1 d]t!.niBE\HpܹSqqq׮]͊73_ٓȎ`>D0nRN~ wѥK^zkkkyԋ Dv!҅1up#-$ҥK$nIAR )HIMj Z;$1udIcLݤv;vYd2nRR;l@S7@Q n^a6@V1nRR;D܉1u pNd#c&!@ Nd?n;|2nRR;uV";dLݤv;vCºLDvɘIvH&gLݤv;\'&LϘIvOL1up0?c&">1aMjEMv! 1aMjEq"m~ܑI0?c&"R?V"IpG&an;2 &gLݤv;\/NpGan"ZdW L1upB˅cFv S7!h+**B@a] %=@@0?c&"555c' .OڳgO1 8L1upBDv횗קO{2+1eѢE7;vK/=@1aMjEHp/...,, ]PPp׺_z7++^\u?&;uDdG bnbP{tY^[[ȮgzIIInZHYL1uZl--]TR s( c&qkykfLݤv MjZk-dLݤvEmTrMj-#Fv֌ 36Mj܉d!c&w";ɘI@@2nR;0ܷmFd kS7:ZMjRv d c&)@63nR;Hd3c&)@63nR;Hd3c&)@63nR;Hd3c&wR(lMjQQQQUUuviG S7F(*))y]'Kd/..Gd:c&4iRAAK/tDj޵k+R 21uځTWWwIvz[~d4c&A:th%Kj/...,,z'錩iѢE]vY&ԩSN S7R(ѣG~~ rQ21uځM4K.Q} S7Xuuu~$9T1uځ :T]dc&[hC S7h/b͛\g̘?ۼ&G7jjj֬YH Mj|;wMyyW 5S7nӦ͛O=p5dHYMj|3g]$m˙~=qR{"HYMj|1x=ODvR1u_ڝFuG9s7lؠ:<Ý:u0`]Du WO9KO0aBv>nO>Qc .є)SU5k޽aܸq#F> /жm7xCn">}9:{HYMj|1v9Sڜ0|N{ۯznnk֮];m-[_ן~p vڶmC/rΝݝ#"bLޗ#Fp}gۏ>hQQQn3<3"b݅ؼ{$nݺm۶NhH,c&=Ku:tHN4qv y$O8G?Ç<w@jL.=,? H76>s7n޼oׯ_QQ\poڴAR,Gj/=zǎ%S{W,%2d3R; 555n>dw^wtm„ β<dR; g~ǝ7o8p]w/:eYy.6v w}׹{w6L'we ېځd4hP}}sf=~^z9eYy.6v ׿:w%!ݣ[nNYV H^k-AjÇ9dFrvHw\|zNw޽k;drv "~_e]&ofwg,Gj!o0O>ޫW6*p#IqaÆ}g͛7g?.yo߾&Lw/KɲFmH@򔕕G?r2v y$O8QÇTv $ 6w/yTHO";8Njq6y{KҢAj_̞=ꫯ 0 and valid latest online data) then (yes) if (is online file newer) then (yes) :Log that online is newer; :Perform safe backup; note left: Local data loss prevention :Upload renamed local file as new file; endif endif :Determine Upload Method; if (Use Simple Upload?) then (yes) :Perform Simple Upload; if (Upload Error) then (yes) :Handle Upload Errors and Retries; if (Retryable Upload Error?) then (yes) :Retry Upload; detach else (no) :Log and Display Upload Error; endif endif else (no) :Create Upload Session; :Perform Upload via Session; if (Session Upload Error) then (yes) :Handle Session Upload Errors and Retries; if (Retryable Session Error?) then (yes) :Retry Session Upload; detach else (no) :Log and Display Session Error; endif endif endif endif :Finalize; } stop @enduml onedrive-2.5.5/docs/puml/webhooks.png000066400000000000000000001274561476564400300176220ustar00rootroot00000000000000PNG  IHDR0`x*tEXtcopyleftGenerated by https://plantuml.comv,iTXtplantumlxeSn@}߯ᒠ4BI IXfݽ;+Yڙ3g.gyi}h֊riR}_5^_?GKRV=ƊwqS;E &*U$]tP{!ZŃB;YVIhsӉka~^* !N]Nj]<^.V>Z;Q5,f`{Mn~fqK2ֶl\Ԫ+CN9Ar TځEs B$mUƫJey*~Tsc=t qǔӋ .NL =L5qR! =Y8Vٿ)VQU䨌P0j/%9Y[{/b͢NưEr:yyYOQоӍ.u6x "dVCNCV4W%G ~pWb6\v\ M2)5g8cI ̍$:}g_"QAͥ#IDATx^xE  {wH^D)JJWt!  pǸnrw>yvgo6{(((wPPEQEQe~)((*OQEQEQP}(("SEQEQT<ݠ?v4MNr)3/EQEٯX?_4MN2A(r=CFYzUxer\:mgx}?u{}2~򴽇kėn;}ڵۏh&SEQAv+öpP~dB0eǛ*S6ZL\n}$ֳvM;}(!"ӎ_vƝȧۏoˮ'_Q<&ލzGO_`~G܉E4$*" 3dȈ:0Lf>ޞG[?)2Dșg[s-X ܭV&jӾ#ڲ=}ڦ,Zڵۏ6܃E=~wvG&SEQAv@?s,h\Щk/ 1$I6a,"ٷv:+V 'NjS#afbu}XMD~&,!P8PڊCB()+W&{ԺmTtϕ#bVTjEJ)(O;1} >xzz˖7ACF2<|4w?AuƌE4d(QI[b{OGe^,͗͆~0tᣃ&N'o>,] 11+LD߯]W-@?Eʔ_ Zx ,r+rدV EQCDЧ'}DYP|)g@S8zNUw>hw"bJPZX9v͊m/b9U@%+xXg& aOME\ 5h>iҤ"2QnQ@2Rz b0>$VT5DYIVOQE9D}9[,A_<..^*@?N_1 V"K,{򧧧gf?)Ռ;An_>:HjUac,tSY$Ke˕[^.^/*/@?u4"a=tt&SEQAvJ:eU.{IVl/-e(%4첶I$ɐ!ckiQP->&*\ȇO}(!"ӎ}jb[vX#'U*<@35[;Q_-Yy/ݺm3ꇺn"iP~Cx$ol2 P AD>yJz %J$"->G)(O(>;?~!'-w*ދ|Hr|nc/\v_ǧh>EQ#ǖ+_XXj#дkMЧ(">O,բu3ڭ4r)(O4^&SEQAiڽLЧ(">MӴ{OQE9D}i2A(r{QcN12hq4pEQ ǎ|bŋWth:&SEQQ@qZa#jn>EQ M'3Ah4w)(Oӱ4Av EQCDЧXO;}(!"t,MЧd>EQi:&N2A(r4Ki'OQE9D,[vڴ9K|mm|Mm>|8*1kc+WӖ.Y2\Y Uyժ :hڴ[ch4fp1m+6MЧd>EQtg[\,Y|ӦMm>DiҤM8qT۴(46tFͻu .\)YL@@aOϤ7Z}Q7-vuРpaLrPn>^Fј*TֆDZne>$)(Ȭr/%^BȊ뵑ch%^|oٲ :uUI$KG7iՇ7A?Qv=£FMCn@?>O;}(!2+/^H%-ZB,YL0trW2eŋWw֯d˖"P˦Ǐ_L"yΜ9)KoժB+r^hUN=i'MAq*R,Tfz?nYɓafc1P4ib mŊU .ҬY}NPZۘUWhOdԨ '4mrM"<,&JYGm)o}I&SEQ)AS? aʕe˖Q '^ @2eAF"_o?haS /Z,#O_߬`o]$R=;Yf7SW%J4rz\\o7m \9s~R֯ υMKX!ybGuӧp5YJlL˛׫$&CI[˟>$S,'N[Oa_\-혠O;}(!2%oݺT^|+D jj za G(ShѦq,:lڻwtҋe?[K.'>_lg)u~{$m 2}Kt|_&MZX͞=G>Ū5٘ ۬cUʻpPTTjxk&N2A(rL ۷ϙDh*-VTl]m;#Nvo/SP@Yƌ2D>ʭJl?~bG4iM)G6A sݷ@(Enmժ]͚uIJ5٘ ۬'Ngʔ%QDq|:&N2A(rL J$TCM8qX "{+7.yoذ |JEMUYty&;gI{ըˍSZї_W,H.d{@۶rkǎ+U*ƴh@f%UߠAS-裣Wڨ훠o"߸$hc'5~2XGwy"d}yia>w֗%WM:˗YzW&M'&e?9vw>2eʺu?~~&Apfs7b5D|lZD}Nڵ 㓪b*h%Kxy% ,eW(4U5jBu޲|bk*27Nz.]ĉoiW>ܸ Abyƌy vMPYx!!a~ɓԩfMk^ t#:0[3ADFgi{Э&!]`0sWQ: 6OϤrtytM䑣#_c] &{%ip~|]۷rX;>|F䖳Μ@~{ ƎysHHxv|cLH9>&}Dw OnaLЧA}O 4&}Dw OnaLЧA}O 4&}Dw ٩SOLW#l^xF?m6>}M|tΜ%-;|2|׮#޽ǵZnvфj+W0cƼ~ڂaMCE2}Y:!'~eO[Q~nKώ=@_C1&8&c 6ӆ[mہ'h# E>[rU%^ߚq!ԩGӦ-<ݪի݆֮k…+b0P*˛Ù3pD|&*S6<^xܔ_lԨynq;t*џ1cfiKprSɒe'N+W}e`k̾FMeTD*'X'ag3 N=iSpѢ%p&3xn^7+9Mt֢87¯^}¾hĉ3eȀ1%]4*ų,L57 m+9|zQD)}%1kр>ԩ\޻ヌsD(<Ÿ>+WkkBO2r*^tWX9B0Ӟ1[|݂+ʔҵ%9ڝ7o~w C1Ax8콒F{MLȠkPF_(A_׸pc|ɀ+Ϛ@U^oڄ"~c8&dӥ6NJi0Sl;@ڵGh+寋yrʣ W_F-vеM?tZ L-0 25Q&iѢMe5_bwϞ `ca^(X͞=VMqÀyĬN<Oi/zIDz֢4Fbqk3;w);1Yѣ']4ryqѫMGi׮#@il XSA*Z} 8kO0CD<-/rb&,]i~B{($7)A_y7:D{UZNwUJ>.ٸ~Ոh%آ;^2 S֭pe%OQ W>H6]([ؠ7mBA|WWu[GvĔ`m"`$FaMzgu+٣ǧD8.4ⲲzyzzK.׽'r'uŻ)^i;,&jrvsht;(T(P$G[+ ](+•sGQaSnjժx`rCWnig@ׂ c`DDPT$"bTzР0  ݊׆[xC97E^m1Ublً94r/;|oP5kjkb!т$I'(%苙Sի6a'3eʂ7 A_dCN #7)A_y7:D{iw*U (\@?*Q1K qdu5l1xL -p;uv6AǎCha6MPdbbGWk"ǥ\> ]0&h4ڦGJGcQlQ&\>lzЯZz۶ܹKGKt<_ -]an405֭ێȧN]{ۈpIc`M }C7ȭZϟ5~ڵc١(B5R vSo_ hS:_A9s>#3k&-U4Ԙ1eȧ~]4mV:u4_~9\b`ɷ7 T"MЇ4ix? Swe ŷh@_J}||D廨]AYOѮGL,:?wRmqPޡ.l WܫW\;oj [l鉄&5"EO ZRU|}k*ZSNU&ܥJJn.]z@o*n7i,\!~eWӦ-ӧπ 6ZMOrq}O+}i:ޛo"w>;5s͛af6D.7AV~&_|1TZi-L7c oxg^ }MЧi0AD&Sx5Avi-L7 ;ri7A}O.7Avi-L7 ;ri>=zD-Piʴ=V15nx3A*vt4N?6h>)Mn?lZ-P񓦺 u7kY[~|ԩw{/]ӺuԽ4aw>1%?]VݵqP@r X#FtA[v$qIįGM{ OA em UO<2c'Deo>u/aM8e̘ݧMaҽ{>垚2#r8Af_дԣOXAh&6]e}}=EƢ\.B{E74*;#S)2rq+i6S>>{Ah4Me>EQi}(!"4M EQCDЧiv/)(ob;u@ ԣO$rq+;iW٩GArOˆAh&6]e}}=EƢ\.B{E74*;#S)2rq+;iW٩GArO(^MlHӴz)߃\.B{EЧiv/)(6P|;і]{=}1djB'r'OSe=?Ӧ}/ y65KhY=v脭>6Y梆Zc ׆K߉|LAѓ={־cG)"luNkoJ_}v%]k>EQk_ )SWCG9s/PP,wfx.EnEֳwҤM[n_"kd8xĩ3{|ilfZ˽e͗ ڮT_߬GBωTڜj.j2 >EʔWn=nFҧP80I$ڭ¿8 <4UnI6֘h6<>:3m%JƴĮ$kMЧ("~<+C5mP֦Ն+su>nĉٺKF|_mn6bEo>]l7hܴP`,D*/[bX~ۏ.\u79Uk7{iQ,SN-F)>~6|_9`;)6=z" 1z Wo?ܴ#Lx8U&wo޹K7k0VT,9 2BI9p\c[v={ ҩ'X)(ob<.Yq'Ծ M{yyy}3{ec/ Cm9kk>t܅3 = e裶ɒ%[ADP=*>YU~%jW Ye_e -?l3WJU'4I/3?gjճd!L?xRogg~7 _N"K*--$G€=iG!'OmPτi!&S)I\obpΔ)C@Ʒ!'ðܺM{z Gx)mVzi=p{UTS<-ڜ5T!CFqw;,60Vz-Trߑ2$mts_f^) DEgLfeޤI<f&,U,TCZE  P6(ͣ;2 ,P";wɃʋ,Y'U*Tf=>OeC W}[/WT-_\@?u4 HyLV(EB*7iE9_jA* >l~Bol?IO_ŔFgYfxIRk CG1϶Ihe$q[ĭzak3 6A_E<cV YMzG~ML;b>"cQ."z}GwZn?$ĹKKzzz_\n3-0zs`q9kkt>$Wٺ+Mڴftӫon+^rMWN)rγh@_kŠ63t;.cJ^U@="F=GUwYRD8z0V=>OXAh&^_8UtOXAh&Zv8_pĘᖻb\ bV})_/-Ê|D<s,Y|?; &9kk(}Y#?q#WC*Vqg[("Vk>EQ P+V˖-;3o>{Ah&6]e}}=/R." vz)r8Af_дԣOXAh&6]e}}=EƢ\.B{E7: MvGЧS|r8A?!zO{~ҧM_ m*_ic|t6\ o=î$9sF܉nVUC9n}GNԩP,/aź;[hˮ2bC5!F񓧩9[ane4OQE9D=>ΒwaQHs-ՆGmu6\JJb/^q&iU5^g~7_'^Z r\oV4WCGz;ՀQIڑsެ m6elUC/M'loh3үgҦK'+C5mf W}AOmݽ`cfˁQlF)(q' MYe_eX"S[~?g x'O>:hRgYU~%jWKDEYZ,Y|W^Ц}GD.^Բk(l3_N"K*Qosc~90p`6.fMT8xX-@S$I,\`tҽ6t&SEQAĎ뀭۴Sq;%ÍAt(e߲koҤIO)RoeZLY >܋9mḅ4$ȳ`@>z?P߱dT7ݣPj 1&Z-[ rr"5dΜE욪Ȫaf6AfljcmDJc3|t\.R'L2;g|;/QD!'â0HՄgpTޔ9GOG|eO]PiD[ /[w6h6AfljcmDY:)ʬ9 *Z\je Pd0[Td”r /q]ƌ;ñ>bb> )^MsQсkŠXXi{$O"Wq˧8EN=G@I&wΪCEo3Re~3{m-.nҋU=*DYڪj<+}>- +Hf a3MAM߯]wаQrᠯͦ'MUl9mg!G {E\obǢ/~8bp] O$HyMvexf.o뼽`\9|#1Uyafw?2Qz۬A#Qo0w*/&S)2rq+ I*!>OOό3K>UeʃNf͚-KaƩ@[9sɛ/y.Bate (]TV=O.   P*Iɰ2eʜ>}@mhede/ Iz P IhCj#'/PńHiUج^>zMgWen&§/^C9{ѢA`z_*)߃\.B{EЏ@H M&*7ibʌoNU[Cٌ&SEQA?^x=yT͖-JVҦcnwhtUݻO!p3OQE9Dxbq ?o|U/_>P=>tӎ`m8t}(!"~č|___Q>MZ)(obO1;o| yKQEQŗqAF_vBVcn|iG٩gBѧS!rq+-S*^`JiکvꙐOXAh&/ YΝONs {E\ob_N]4G}1ArO(^MhS˸4M;}q1ArO=H W}i2A(r4Me>EQi}(!"4M EQCD7: MvGЧS|r8Af_дԣOeC W}}AӮS>>"cQ." vz)r8Af_дԣOXAh&S_iN={AR."4M EQCDЧi|޴##kWkRٳG}(24MϠarFQEP}gUTʕ+)7"N}ePaP )^Ml`MЧ$eC W}}A'X)'o 2rq+; D7r8Af_ }I"E\obe\:m֧O֭[wmܸqTd9r,]Ty/^Dn=wٳgŝ зX,7n̶}H(8PlRJ$E*+"ꉟر*^,Ydn8UzUѢm E'3$EQɤ_R%)cƌj՚4i۷V?c >.6[nӦMXț7O?bŊ!C 6 oܸ9sf hB"裏&k˚0aBD;R"fҥڥKm۶a!UToc6?H~q"%jٲ*Dڲ"m=Lxb,'JҥKX=zYݻw!%i2+!($=z(A߸E@ӦM Z ss4P^xqĩ_~"W5Т(r1OQeLc 7vQݲeD~hhت}̏xּAXh&MTD:tɓ'c9y"R"3fBTo,-*N4Ν;J͓1Hyi˖-2+! wRk7.¸zb~~~w@"~ʕ#mи1l EQ;K|t(JKcOnDZ#"%PѢE=weVw6lHb |AGJž@XԩʹBϝ;}M6b޸qClҢ},E4נo\q" A_ž;+勗[hE*24j [C+>CQy|kj/k/FZkԨѦM6gdrc~~~;w/*A˫gϞ 6v ^1c͛ׯ_?!R4y֬Y lU(%- tҵl޾};{X-^A (e}**AÇWEW/ZL:d#GDbn&@_[2Cư5˸oQ." :Z[n-^RҦM@=@r"{%#n2"s 'O[T|yCזig 0@8+ڴǎ>VFeѢ!)cPD3H]yG_[*Cư5"E\obqk=Ї=z~ȑ/*?.t]С*o>"2<}RwAgϞUH2Ͳb7oE"lXXX%ʱU%?JJ\OӴl/-}DЧ(*>OӴljoٲeZ6lؠ@E'i6M ;OQT|AiO9I}&6_ƥ D7߃\.B{E7tʕ+IQA_O!rq+;SDз)2rq+;7Б.tjiIT{Q_(2 W}}AӮSQ(2rq+͗qiUvGЧS|r8AiڽLЧ(">MӴ{OQE9D}i2A(r4Me>EQMlHӴz)߃\.B{E74*;#S)~ِr8Af_дԣOXAh&6]e}}=EƢ\.B{E74*;#S)2rq+i6S>>{Ah4Me>EQi}(!"4M T<ӊ+vء Yh޽{_x17N&VMv}رСCmoa QQQ2d…VDQ2I&VӧOtPV-e]2?:qDÆ աZmݺ{]ͦ>駟^|);t{QqAiڽLЧj׮ W/_~Ĉwʕ+Wr׮]mw]Vx޽{ ӦM[~}`;lذ3g (,N i=<<,CJ.=ydEeHPP8 (L2W^}2.GլYuhlJׯ_ϒ% 7444M^z?_~iҤE8fh%J7%gMlHӴz)T߃}eH́penG;'Nk.\V i+V2eJykY 2}N"E\cǎ" *?}f [`q%rYzu__#s]F VL qzUҭ[7 ٳERÆ kѢ20-AYUTI›5k6h AHX^fMT"((HTH,ݻ7iҤW\1.% @LH^٪ /""BK:C#Gs"DA`E74*;#SX3x`ߗjnjӤIfl!eʔӧO b,X@T@Xe*!ٶkkпpRɇR@XX,"M/_F۷o*]f̘oԨj"78Ǐ#͛2};v\Eg6.E@I&ӦM{ezl:}4"zʒ%#nݺL2Ub=&6]e}}=kr;mڴ2^t鯿Rnڹsk ߳ghϟ?Ơw^eVR 6,Yȑ#gΜ#ߠJ@K*2P``ܹs!HD  :4UO!67">cŒ7)cǎ$Iȩњ5k|}}ߌb=&S_iN={*A:Ui7o^k) | oݺ%6?^-[ᅲ9#""V+W'N,ye}^^U ׭[wԨQBrhѢs̉a)lSh_\rH^D ڀ&nܸMgΜfB7X+>MӴ{O3iܱlذ!I$/ )A|*U*˗/Av,xUVUV7nܿCƠw}YfϞ=2ׯ_, (ϒ%}  R}Dg.Yn@R^٪VslnB&k׮_AQ[l߾iO4^&SLZ4w,?~x)SӧNZ!%S|u',,Xb3gΐ!"*W,B}Rzu KNL9PV */_}J7_J1k׮ʟ*n9s)RXpI)lS~MӴ{ON!!!mڴɟ?C(ʕ+]v;kMlHӴz)=ǏϞ=5kV???m}駟C&1{E74*;#S) -9r}}}sEނyM{E74*;#S)0}-R>DзW}}AӮS>>ݻw@哔OQoS}{E74*;#S)qG޼yUV-Uf˖ 셿+WTǦ('o&S_iN={J|NHHH޽ϟO!1{EЧiv/)H^Z(O4^&SS[O4^&S&k(4Me>EQMlHӴz)߃\.B{E74*;#S)~ِr8Af_дԣOXAh&6]e}}=EƢ\.B{E74*;#S)2rq+i6S>>{Ah4Me>EQiߞbȈ1mޡeo?ԡV=}K.jRo{~Μ9###ܤIUVc/ӊ+ƌ%C.\xaErT'N4lP[Ťn݊]^6|Çر㧟~~/_:޽[F("4M=|vbŻݺm4i֮[8_tEC9sf2e^C;wxxxvݻQ^VL6MiذaӦMŲRÆ kѢ\EV!a*U'O>i$#1zEK zJM>=O7oޮ]W 7(4 թSfϞ!*,X Wرc*j2{l4i"àzpӧOc*ҫW,YP@֭˔)""cQ." vc}[n?$ĹK !wF= |-]Ty6=%Ory6e˖?s @I>Rdɑ#GΜ9M6`J z,S*]ti޽ʴ׮] N) i///p}BX#ÖĪVAUd~СC v$]sNp/qF$A謰0puWvؑ$I۷o5k"cQ." vc}) pX=zꟻnzeʖ-+C?yL^n3Ofƌ{9}tu9rxe8qb+@_>g97Y۷}R.]`"Aߠh+Pj0AC}7"Ju5j\EVh@Zh9sH̻ZʕC%J={^:-l=sX5k"cQ."ر~8:G?YTN]{`Zʕ;8jiq~՛K@-V*mժUժU&UW)|||!s,=_.%K=ЇJ*շoW vqs[lyTj;nwJ)A,*U*2^1СC)ٳLɓ"RAAAʏ#ٳ'֭[ vD]/em82Yv&rnھ}:J|r8AiY pɰ2eʜ>}@}UE{xyyM}(,,Xb3gΐ!hRqVjċ͛7,Y"V|ɓ'/TP| _֬Yn7Cׯ Fel}qSJ %mwڅ |fܸq յkא GV蠜9s%1Zpm tTA?wPQ\m"b EA+'&v&+ƮƮk`EkIO8vY}s8wiwfYfv}|r]hQ[Z86쟖#bˁvR wHJ|Qқ[nKӓ{%''?\x.]w53MdӦMSLٲe̙3+6k,q $ fz=za'̠A@v3piTg-ߨ?OLL8p`vzyfh$gڿq) ѷa28 >kiXQJ$b7}kAG߻tiחɼ!Vo6Dto`_`-zWlY00X oQ#$\9___fB^^^ODfXՃN@m >xGޥKU0CmX= D߆øX ?wss/Ydf\h3xz >@ʅ# f DO~GA>h > b@@[@AoHd = 6 00X }}D7"V:azѷa/= 6Bto`_`-,zAmX= D߆ZX#  z  c*X# DtFMϜ9O5kφ1e: C[MGA +GMIs   ̜9Syh"6ӳgOZ^}?:l-[DAWӺuk>#͛i # L>]_JOOϥKprrS._,dd{ѷ,zAmA"V;EXbڼyhQF4ܣGq%&෿-e$ޢOS!=.a5uLVSZZmU6X.2H,XXBFlԈ81> GǏO7n⢟\~}ܹs.\ fƤWo>;WG*ɝ;wۗ)7wҩw;vSXT_Xl*դȢe}˔7iX.HPOp̟_Z%=pFS# @@F玅h!:*U03f]͛7(.:ίadLCj%L ѽGoȕ+t歇15@%;qƽIWX9/^-~bMA*V~͏J|oVMZa|߸hvt!٠! +-p{փ;eE #rF7lS "E27/ܒSƗǧ6NԮo@F玅h!:>lΜ96lXh7MW_Q\###}||h 0+q; 4B\iO>/{Tr`^^pK\\ Wn_wiFMoFYPNc] Is]{]dUFMh o޼lr^ޤYiY 6;%-4ܰqSZ$&lS$]/`ovڍMіcKة4nc55URe}0:w,D y'_~=**jĈ~iJh͛QY|~߾}i}*$CճϋMR3vvvbčB5K%\qs~5l#T>w4I"^l.CRFH#G.^]`2E6uY95!٥k%-BnMD|-dAH르7E[R?&jV>]eg}DAʢߦ۷es(~f/n{޽prs%coTK7-[>gΜESo?2TɐZu~1q_Ϡbjxe\52-mw뮘O>[oS2TRk~pǕ}fMffd}Yv]2.0kM.}>ǼiQYBr )BbM~4_l&lSҍ Z$KgPq} DAʢlٲi3fĈXYӧ )RCǏ%ÖoT}a[5_2?`CN;S!wǝO29%Uk޺#jĘ]s;xQX|dӄDž%hOZyQGH򌽜4mVDL~N6ܽGosݺ[wV弑vsppv̢M{MI7_$RڛOׄtQAׄT#R㬲# XY߿BrT'O !͢ J&7N>I v14R̰T8|da\VNըGOrntmdX! DNIQRY>͂E.l:`χ onhy6m8 jm5ltY˞^6nJKHڨFia\_n"ZHR_$z۽xl > HcegILLZ>|K/P qU%H/\!~!#qCZ*5Tr6%.』&W?3&"]`Ƌ|TZ_*Km"ZG^|ofQS7ѷQ>G?h!:}$oL~S }K`/-bskqw60ѧDft͆NS7囝[wnܶ[ZnF,,zAmF玅h!:={npدbi@}}Dѹc!Z;ED_ѧ-,zAmF玅h!:fpjO?^};H&uE`ѣh3:Bt ,Ys,}ADY3WDDAFmGA5s@@[@Aid\=}ƢTG?h!:f }_'"L.i퐔z_-[tIn4x<]D͚BM -7JZ@o,;"zIqGȯg;QXh1" >x\8 ?+E<~OhqtuPʚ?17/^."cG@mFlh!:f }vA+ӯS .ܸiǿ#*W &/G ߦ:iڬ*N']! 3YS=h(i-t17^R7pvv (;wna}۶}Gxp;;Q$TSԚhvtm@O'OU0"`B#L2c]5j̼R2vDftXN(k0$}>K]}~x.۵;tU8x_d<~z ?z7MuC\ޤ> ]=.^΋ Pf2$Ï:ׅF]3/gĝKi> ϦRф4+-QuUjJ}i֤ B{Q2c5kוdX~HY>o * r/}QOi;tڍ z(XPLcT^BLd1NPf`r);Lh3:w,D y'5s@m ?|%+iI^JUA^TP>g W))MhƘ&jTgЉRC f5̘e5xؗiv3#f}Xh1FM /'NM&+*U[%J{BhRޥـZ\]H*6ԔfEI 邇JBjqȗo\d6e|Н {襺FP.HrʵQQ9uy\$Sϙ3']eVv_ oޱ6B0W}&G;ʚz 6Lo`;hLSF?&a ʪ}Գ?9}IS/Iݸm7ߣ'm 5͂,_hxʵN n;hR}E]ނY4~MH=t}uez\RVA_(ќ[-3QQGHzE~Iz.p#qgiwAO Kg"}DsN(k +V=|Lz~ #<^OAӒf?4uƴ; gg9u&+j•*Rq'$^iRoI$֛Y6iXR*$ UiJJc&J@B#4i4o5 mRTڤ^bO:ՙ䫴>{7m+]H}vy˳> ";Hsw|XBB <}vvv|֓ɓGy1Ma{ݹ-SVY6iT$F'6i ";?z<~IWn}e_Ϝ+lFMNTSfuVe^UU>kElJ*M+z&@r!~9R*$ i*M QiV4Aϙ_2||4iw|}j\5mJ%=&O͆J)dzt \y&ct`rἲ> ";LǿSj04 ٓl2bd;9Ht)ʵ\\͛,Ϊ̫Yh~_+Dzo0}Rsьĝ5}F[x .J,*a*M QiV4g *MGϕ("zy?hg-X4Etͫ77+}S>? ]CG5M#&F 2Jy𣞴ixKس *b  4f y`a)oL5BbŊ;;&$^M8(o5v"Z#wv|OڸtBs!? լm4M_2ԭV~'2e )~Ӂ([o\UWZ$U"jV)!*͊Z JIORjD}Ǧ Qט߀)zIՐIIȩ3]ܙ sv[oaZ m^ٙ6;_Sꈪ.Q)KMy,X\R^{e>9HD y'5s@m}}X{{X:tRo%_--@JGB]8$RK7<,rbE2cSBݟˉ~VG%͸s)OkP#ܠ(я\їy 6o6DwBY3WD߆ Y̯\+.6n.r5#%HPXE/A}fh3:w,D y'5s@mᾈ>xÏz*UʷL2ed-Z3WIbXLG;ʚz 6 ~eK,ްh3:w,D y'5s@mguE$˃qFA"Z;o\x ݙ3+GA5s@ ;6mA[GA5s@~777ooo>DAFϞ>غ\[ H#k0}PqG_f#ڌΟDwBY3WD߆ZX#ڌοBt e\=}kaѣh3:w,D y'5s@m E>>ܱ-DPѷa/= 6sBwBY3WD߆,zAmFA"Z; H#k# Țz -  4f>h > "o(=_''O[GOd,@mFA"Z;o(틷6Xg~v+ZXu (}fh3:fCD y'5s@m}Aldډc>y{$n#7**\ʖOVrۏ\`뮘)~yvXxe^p׸mO칋7*/ G3 }Dѹc!Z;o( ?ܹs/a{yLo~!-ZlT-۴sus*UnN#V9bu*M>:QӋ,{^ÏzRI5̘Ҹ`+(}fh3:w,D y'5s@m}AQ>

G?h!:f~6ߺC:ު$a\\rX!bӨ[wӨI>GG>f퐸Ϟ7ۤY!#F{/c:XEK/6$> " ޣoktvv17ϦO٥0lҴYw~w Q>9/Q#IWo/J/mK# Țz Dž"Ϧzo6hݮn߲3'&;ʝ;7w7wlppp *W ޣ7 дmwY.VV]V GA5s@!"'vq):}<NJդYNNe .\RI]Hwi`oل=J8';V~Tb`Ŋwvv!OHKE_q% UH1Yxٵ;> "odq@R/a<|LՓgIy!?AfϿC|P%=ЫO/^VY̗ߢGDft͆N(k0?uWlWRUH?G;ʚz 6 kZU~oѣh3:w,D y'5s@m dM<<<误o-;-z&#ڌ Bt e\=}ƢsS~~~ʖ:b$DߢGDft$ʚz Y); o-f!  4f>Y# H#kʏE@Aid\=}?e\AD5s@m>PGDft$ʚz 6 ¢GDft͆N(k0X }}Dѹc!Z;o`_`-,zAmF玅h!:f }G;ʚz 6E`ѣh3:Bt e\=}}ADY3WDDAFmGA5s@,aWalӿuD$޺W&Du֫V*޽{Jzx'>>>44 Xb׮]±-}PR,{IDATdĉ5ދEnܸqݺu9r;X|w^a5AYd\=}ƢTP9n8b}uڭhb5k}_jB*W f"&D?<<Rܾ}P߿/aypƍɿckԨ_}2lGzbUT_~d n޼?C|򉫫Qf͚տ???6/2ʕ+ n9HD y'5s@m Gld:nھ}y쑸zXbi_qJF ?)ׯ_gw'O<~8&&޽{hvD- *55UT[gϞ%$$:u?du>}z >N  =MGr9r_+WD_JMDGGSZߗeٳE;KI%ڵ#( [u~Z\rٳWcBs߾}|٣o6DwBY3WD߆Z}B_s^ ę$2eBj)Z۩p/9:z\5snTRPݻwS.5ɃiRJݺuswwZeϞ=}}}&̙#|$ ]@]a-9>66ˬFY\CICpر?~|ԩlI>U !e6m*Qē'Ohadz::uR}JM>}JWMe]dlJK^4h <<$5PBoA/nџ?~Μ9ik׮y䡋I& 'u7:w,D y'5s@m GeH};Pɶ=%0H8jLK~~ TMx U0a>D;g(`0ӦM &g% DʕE2 :z rȑ C)%ISRRDJWMe]dlJK+%… KR5k -YdfDfJW2t@ƌ|]xPeZHmX +ƫ=:w,D y'5s@mE\qUwIør墱Bv6G.]—;Ux"CՄ2ڢE#Gr>}xzaR:u Dl֫9|0맦*>_ hΝ;4izj>tTEiIWYaustA 3('˗/g/׮]jsFA"Z;Dxe(>tm<"V>weѢMqqq}~ B c|K.7od/هׯO9wܗ'Lr~kE_)Yf۳odWMe]Է. KӦMǎ_]Eח[coƫ>5s@E=.׽_p7nߌiosnڪ߀Awozv!GEÃ9 ?ݛ;vXn]6jȑAAAO>=x`O:ʅ6hР}k*::LRjՁ"į"NNNf:;wnvG쪩Sj\vIX5p%ʩEwa/邐~׮]_mAWd\=}0'"ѿW30Vf-;9/TpJ'w? O}J#55 /\jU ĖrrѥDY3WD߆Qy`Q,zqEgha._ܣGq52hРqY KgŊ՛?:f }g^߿LL4[$6o6D QQQnnnիW ~N(k0X }}1WtXHքk@@@۶mCBBʚz 6 AɒT^EGGCf }чOmF玅dM>\Lc3f >Wѷa,8 @}}DsHY>uiӦ :f>h > : Y)#|DY3WDDA,ϯYfad\=}}At25s@@[@Aid\=֭[OYVS-IB@`ѣyh+YA4$kNHNn~GAAD 1 4m9u&-[}-ZyDiӦA6s+GMe{¢GGV7}kaѣh3p,A'Lo 6EE>>GI-0# f DDA1K -  YmGA c*X# Dto`_`-,zAmlX= D߆ZX#  z  }G8b7}kaѣh3p,A'Lo 6E`ѣh3xz >h > bdV/_=z\)臙sπjB䔥 zɓ,&**?l`'*襙m/Y`@WK|{<$&;,[uKݾg?_Z~v́V^>,MBBСß={&+(_ Vfǧ b^)5zk @Ih1#|e Η]~퉋Kl1c`e6O\\y쥙ѣ'H0v|T(63.B5D_0!RAV9YWQ&JWĻ6ɔoCh_GA V'{DMѷ! ѷ:}} h GՁ@@m>@Dh>oC@-ou @@}h!}ɷ-[?}K%%ݒVHٺ5ر#L2ڰ<\|͘d/r>SVm޽w6:-k"1cbNH-Y D߆P}v'&^%.ܤ+WH+0og!J}8(v3S0NmWIO,`])l% OBqҾ2W_~̙n޽G2as~/-6#gETa#1 v@"ŋF%\ .b.w߳6 *+W vaBEiMN˖z'-YiETPξyzQf~ؓUL?Ljc 4OKH$`"1CBjK3oC(>u9rdݺT"zGż(>CzqdÆ]ly)nB6nMor*СTZ~҅{Vzѧo7vԕ &D͝JVX5fv}˲|ژ~r~889 u\ʕrI}Շ25ѠA#dŊ&".]l_@ɒ^t+lv֬)w=z|rVr֭۳OζA}GZ;v-މ}BE .Bpt7/}=DEs̼uٿ?CMGZt߳ҋnEl{0B³-)Cq՟8qj>>eBC'N$ Q\`Bi?hZmۉڼtknLp-ڞ|c# :uMt4ŋH5@mѧ9:7k֊D?5шc*VRzƏzlz!}dO͛vԕ =J3KƂfH4ʕ5s9Q /]SdJY+.DVىnԨW)~|&jգ [Xio}u4UwЅQeCisR#B!T1`4VKRy,YKSapݻb%" /;靘T.Xi{5w߭6lt\VĪʟ??ySwppعAaS'S 6]]7[5? wfkaV͝서>}%gΜD3RJuk ] q޼\M7.kH;R"aæt>s*,Bd7ՅWZH:|x> _AbHu|t.YD߆P-[ }: ,Dzz{whJg6i!zF͋0=w3)WD/'SaiGOh`p7,\Zta!>]B/USdYJ+.DR,3fDOIi_E_'MEOXqmVڛ.uq)ֵǫVmj eO(N0"ZNN5LK=֚R:+]^}H)96%:GZ.oܾ}?ҟbִȩ+|cNSo߹Uw ۽_AE8ajlΤeSwIH2;!۷]#Wq7?bÇUx_j^zxx^JٍiBʞ0]Aթ&>ɩ Yd}B]iIԯ >]Y;2r&rޙD_3L}=DEsO†WE_xU9S>ѹs7{|8mnd5(W.Рi-Z SԎp$1j[[nOg'Di8g^ƕVߠz^=رJsc;v|?#Bd7AF tgǧwΜ93ucs^rP̀RoPIe;3ÔN CTDJja<߽ BrU4g=zuPP%!s hB¥~ժU7 _u~Q4GѰu/Rę&#6lJF(EY*+г޽m&MZKYߵ0maW$/WԊYPoH3_*˧AD=ASM6t@c٭guΝ?myuuuQzرwLF#'ʕ}S^W{潴KQO>d;5ذMë' y:,^ahذѢraƕVߠz^=v8H_#G9;{tgCO7#}Ҡ [DpnaIV1qNNz}{$;]]/ 8͚'vIaNZH,guV-Q/]**J z!T9! To{ΜJoKO?ni[l׬YIfe˦2;A=II4ob!f}!\K"ES˥KT;%{xy3f}CtTOQ:޳TV\]s'D]\ϕeEuoY$mG&׏.D_]T? Р,꽝jc9wl:|cNSg amrD{͟?ӦA;yT4Aemۉ/})ieU,w %]֬Y۶$": .YD!0JTiv"hԽ袓*Ү[/XJg^UV.^kǧ'MtjnALzfΜOG{(BcbsK4Fz}S.i誉D&$\$s]IW!ˀ>AG.\ /ۭT6:z<;};-D_1 :a(NNMvvk^px4 ]NWtn_|݃K{ҙ_M$OnYV iyoϞBEn 'J{h4!.z:m/Qot _[-jڧs,͋¯D˩۩&uJt2jХAp`(3 @ڵ]tS_N[ٝ=J5cǎ%Ro4tN9]^KGi $邎/OR!CD?3da#\k҅蛂{J[q}~8})~7Y9ӕg)J|ll@@m 'OM6iӦ aHeb ѷ }+BL/P@@m ޽Gk[647Qѷ:} yC[>oCdX mDܹkdDMѷ! mDvc{>&>Ra?'T1Ll-,,pX?~a011[2#G 9JTȵk׭1cFVYLPZZ!/[j̙qjK9rk}D [>>!'ϮV-DZP*&&-[zz\-ξڒ<[8Jjr)cggJдmt6~ *T[۵$[ӫTVȁ %Kz/ZV=gg?pኲeToC@-ou @؀yyyb J֔TXp֭1k}"u OaÑ3ƍ-{F-YV:jpw;gwfP]Z!tqP;D?FQ7() }[>>6 +Vltttd_ky^CgϦ1,X|a|ŋWVh޼䧯YJ_=WP>02rFp5j zYfڶ$ QBOHءCȑLw>ҺuUVi'NZOZeAa5 9{BuMEr+tIRk׏Wڲxm~y޼TYITntMB.-CB*Er}Z)Z$`ҤYd/ cDH^RSix)ŋFG-[ жe>ʐv-J\8 6,ߔFaɚm{g8p` Y!Þ={&+(11K(>d_|a^joРSR_߲'N5ݳ޹s7?ޫW(PpՆNťر$wn]6L:͵O|}X94x_mZBj<+LXUF1_nȑePD *BDʺ(M¦*[6ൗRuVRf:F̙СҩΟ7P{ZƍLBj"KK6tE_AT'Ot94ztGQ{r UGL>Yk;qEg[n)A3o޼?S?]֮]7ix݁`^j~WOKMC iƿr妠JjvvvsnD>7p02z+&'^6,_4% 5k׏\vS|TǎvQZŊUdȊ>鸿%"dŊWgDJ2 j򩔐3ItՔpI:֨}i! OR1cD(--.Rmڴom0kjWG^\[']]$>}A,׭V.˨RGϛ;dݻ2ddpp Va潤ST>|xTaN|~ ̛v8HgϦʕ}Bt:?k~3WכT##g_V9s\pUF-?Qk|'sIY{Q*.*HRBΩZn)}Сڌ},!!۶_Ғl.ՄHC'O^~C 8+Wn DDX> bXDvqEVP>496 m۶74:؊0!ւiJf^$DV/lHv͛Kr\W }ezaKjNK.w\#>>*4t*cb̿r 6?ŋװ {ԕCfOǗj셤6EVȪU[2\͠d۴̶k@Wǎ%}'۵Ī}_U_ GA~J}1!㿍ZF-˗ II:tbggG:::߈lI^N]*9;)֪U9'$\ M ,/_B[i;eʋ]-Z^=k7nΞ=xTj!4Zw2Wj$ҩD@WÿR4JxaC`9^ܯ_ aKaЬjv &||9cTV;GCgAƧO_a> 8a>}A,nh޼\$G98رDj[2 k )K&heilIo5!߳s,:꧟cϋ %[UV?zt6r>}A, GA>,DA1K d>}A,A B@A@ GA>,DA1K2%GO:a܄IRE @A$S?1 R ۳dɚkK GA̒L{?O:n¤#cEػ_g> bdJ> b@@[@A %}}A,IGN4 !}AAA>  d@AA$   0ziMIENDB`onedrive-2.5.5/docs/puml/webhooks.puml000066400000000000000000000014021476564400300177710ustar00rootroot00000000000000@startuml skinparam SequenceBoxBackgroundColor<> AliceBlue box "Linux System"<> participant ClientApp as "OneDrive Client for Linux\n(webhook listener 127.0.0.1:8888)" participant Nginx end box participant Firewall as "Firewall | Router" participant GraphAPI as "Microsoft Graph API" ClientApp -> GraphAPI: HTTPS POST /v1.0/subscriptions GraphAPI -> ClientApp: Subscription details response (HTTPS) == Subscription Notification == GraphAPI -> Firewall: HTTPS Notification (port 443) Firewall -> Nginx: Port forwarding to Nginx (port 443) alt Request for /webhooks/onedrive Nginx -> ClientApp: Proxy notification to http://127.0.0.1:8888 ClientApp -> Nginx: Response Nginx -> GraphAPI: Return proxied response (HTTPS) end @endumlonedrive-2.5.5/docs/sharepoint-libraries.md000066400000000000000000000262641476564400300207610ustar00rootroot00000000000000# How to configure OneDrive SharePoint Shared Library sync > [!CAUTION] > Before reading this document, please ensure you are running application version [![Version](https://img.shields.io/github/v/release/abraunegg/onedrive)](https://github.com/abraunegg/onedrive/releases) or greater. Use `onedrive --version` to determine what application version you are using and upgrade your client if required. > [!CAUTION] > Several users have reported files being overwritten causing data loss as a result of using this client with SharePoint Libraries when running as a systemd service. > > When this has been investigated, the following has been noted as potential root causes: > * File indexing application such as Baloo File Indexer or Tracker3 constantly indexing your OneDrive data > * The use of WPS Office and how it 'saves' files by deleting the existing item and replaces it with the saved data. Do not use WPS Office. > > Additionally there could be a yet unknown bug with the client, however all debugging and data provided previously shows that an 'external' process to the 'onedrive' application modifies the files triggering the undesirable upload to occur. > > **Possible Preventative Actions:** > * Disable all File Indexing for your SharePoint Library data. It is out of scope to detail on how you should do this. > * Disable using a systemd service for syncing your SharePoint Library data. > * Do not use WPS Office to edit your documents. Use OpenOffice or LibreOffice as these do not exhibit the same 'delete to save' action that WPS Office has. > > Additionally has been 100% re-written from v2.5.0 onwards, thus the mechanism for saving data to SharePoint has been critically overhauled to simplify actions to negate the impacts where SharePoint will *modify* your file post upload, breaking file integrity as the file you have locally, is not the file that is stored online. Please read https://github.com/OneDrive/onedrive-api-docs/issues/935 for relevant details. ## Process Overview Syncing a OneDrive SharePoint library requires additional configuration for your 'onedrive' client: 1. Login to OneDrive and under 'Shared Libraries' obtain the shared library name 2. Query that shared library name using the client to obtain the required configuration details 3. Create a unique local folder which will be the SharePoint Library 'root' 4. Configure the client's config file with the required 'drive_id' 5. Test the configuration using '--dry-run' 6. Sync the SharePoint Library as required > [!IMPORTANT] > The `--get-sharepoint-drive-id` process below requires a fully configured 'onedrive' configuration so that the applicable Drive ID for the given SharePoint Shared Library can be determined. It is highly recommended that you do not use the application 'default' configuration directory for any SharePoint Site, and configure separate items for each site you wish to use. ## 1. Listing available OneDrive SharePoint Libraries Login to the OneDrive web interface and determine which shared library you wish to configure the client for: ![shared_libraries](./images/SharedLibraries.jpg) ## 2. Query OneDrive API to obtain required configuration details Run the following command using the 'onedrive' client to query the OneDrive API to obtain the required 'drive_id' of the SharePoint Library that you wish to sync: ```text onedrive --get-sharepoint-drive-id '' ``` This will return something similar to the following: ```text Configuration file successfully loaded Configuring Global Azure AD Endpoints Initializing the Synchronization Engine ... Office 365 Library Name Query: ----------------------------------------------- Site Name: Library Name: drive_id: b!6H_y8B...xU5 Library URL: ----------------------------------------------- ``` If there are no matches to the site you are attempting to search, the following will be displayed: ```text Configuration file successfully loaded Configuring Global Azure AD Endpoints Initializing the Synchronization Engine ... Office 365 Library Name Query: blah ERROR: The requested SharePoint site could not be found. Please check it's name and your permissions to access the site. The following SharePoint site names were returned: * * ... * ``` This list of site names can be used as a basis to search for the correct site for which you are searching ## 3. Create a new configuration directory and sync location for this SharePoint Library Create a new configuration directory for this SharePoint Library in the following manner: ```text mkdir ~/.config/SharePoint_My_Library_Name ``` Create a new local folder to store the SharePoint Library data in: ```text mkdir ~/SharePoint_My_Library_Name ``` > [!TIP] > Do not use spaces in the directory name, use '_' as a replacement ## 4. Configure SharePoint Library config file with the required 'drive_id' & 'sync_dir' options Download a copy of the default configuration file by downloading this file from GitHub and saving this file in the directory created above: ```text wget https://raw.githubusercontent.com/abraunegg/onedrive/master/config -O ~/.config/SharePoint_My_Library_Name/config ``` Update your 'onedrive' configuration file (`~/.config/SharePoint_My_Library_Name/config`) with the local folder where you will store your data: ```text sync_dir = "~/SharePoint_My_Library_Name" ``` Update your 'onedrive' configuration file(`~/.config/SharePoint_My_Library_Name/config`) with the 'drive_id' value obtained in the steps above: ```text drive_id = "insert the drive_id value from above here" ``` The OneDrive client will now be configured to sync this SharePoint shared library to your local system and the location you have configured. > [!IMPORTANT] > After changing `drive_id`, you must perform a full re-synchronization by adding `--resync` to your existing command line. ## 5. Validate and Test the configuration Validate your new configuration using the `--display-config` option to validate you have configured the application correctly: ```text onedrive --confdir="~/.config/SharePoint_My_Library_Name" --display-config ``` Test your new configuration using the `--dry-run` option to validate the application configuration: ```text onedrive --confdir="~/.config/SharePoint_My_Library_Name" --synchronize --verbose --dry-run ``` > [!IMPORTANT] > As this is a *new* configuration, the application will be required to be re-authorised the first time this command is run with the new configuration. ## 6. Sync the SharePoint Library as required Sync the SharePoint Library to your system with either `--synchronize` or `--monitor` operations: ```text onedrive --confdir="~/.config/SharePoint_My_Library_Name" --synchronize --verbose ``` ```text onedrive --confdir="~/.config/SharePoint_My_Library_Name" --monitor --verbose ``` > [!IMPORTANT] > As this is a *new* configuration, the application will be required to be re-authorised the first time this command is run with the new configuration. ## 7. Enable custom systemd service for SharePoint Library Systemd can be used to automatically run this configuration in the background, however, a unique systemd service will need to be setup for this SharePoint Library instance In order to automatically start syncing each SharePoint Library, you will need to create a service file for each SharePoint Library. From the applicable 'systemd folder' where the applicable systemd service file exists: * RHEL / CentOS: `/usr/lib/systemd/system` * Others: `/usr/lib/systemd/user` and `/lib/systemd/system` ### Step1: Create a new systemd service file #### Red Hat Enterprise Linux, CentOS Linux Copy the required service file to a new name: ```text sudo cp /usr/lib/systemd/system/onedrive.service /usr/lib/systemd/system/onedrive-SharePoint_My_Library_Name.service ``` or ```text sudo cp /usr/lib/systemd/system/onedrive@.service /usr/lib/systemd/system/onedrive-SharePoint_My_Library_Name@.service ``` #### Others such as Arch, Ubuntu, Debian, OpenSuSE, Fedora Copy the required service file to a new name: ```text sudo cp /usr/lib/systemd/user/onedrive.service /usr/lib/systemd/user/onedrive-SharePoint_My_Library_Name.service ``` or ```text sudo cp /lib/systemd/system/onedrive@.service /lib/systemd/system/onedrive-SharePoint_My_Library_Name@.service ``` ### Step 2: Edit new systemd service file Edit the new systemd file, updating the line beginning with `ExecStart` so that the confdir mirrors the one you used above: ```text ExecStart=/usr/local/bin/onedrive --monitor --confdir="/full/path/to/config/dir" ``` Example: ```text ExecStart=/usr/local/bin/onedrive --monitor --confdir="/home/myusername/.config/SharePoint_My_Library_Name" ``` > [!IMPORTANT] > When running the client manually, `--confdir="~/.config/......` is acceptable. In a systemd configuration file, the full path must be used. The `~` must be manually expanded when editing your systemd file. ### Step 3: Enable the new systemd service Once the file is correctly edited, you can enable the new systemd service using the following commands. #### Red Hat Enterprise Linux, CentOS Linux ```text systemctl enable onedrive-SharePoint_My_Library_Name systemctl start onedrive-SharePoint_My_Library_Name ``` #### Others such as Arch, Ubuntu, Debian, OpenSuSE, Fedora ```text systemctl --user enable onedrive-SharePoint_My_Library_Name systemctl --user start onedrive-SharePoint_My_Library_Name ``` or ```text systemctl --user enable onedrive-SharePoint_My_Library_Name@myusername.service systemctl --user start onedrive-SharePoint_My_Library_Name@myusername.service ``` ### Step 4: Viewing systemd status and logs for the custom service #### Viewing systemd service status - Red Hat Enterprise Linux, CentOS Linux ```text systemctl status onedrive-SharePoint_My_Library_Name ``` #### Viewing systemd service status - Others such as Arch, Ubuntu, Debian, OpenSuSE, Fedora ```text systemctl --user status onedrive-SharePoint_My_Library_Name ``` #### Viewing journalctl systemd logs - Red Hat Enterprise Linux, CentOS Linux ```text journalctl --unit=onedrive-SharePoint_My_Library_Name -f ``` #### Viewing journalctl systemd logs - Others such as Arch, Ubuntu, Debian, OpenSuSE, Fedora ```text journalctl --user --unit=onedrive-SharePoint_My_Library_Name -f ``` ### Step 5: (Optional) Run custom systemd service at boot without user login In some cases it may be desirable for the systemd service to start without having to login as your 'user' All the systemd steps above that utilise the `--user` option, will run the systemd service as your particular user. As such, the systemd service will not start unless you actually login to your system. To avoid this issue, you need to reconfigure your 'user' account so that the systemd services you have created will startup without you having to login to your system: ```text loginctl enable-linger ``` Example: ```text alex@ubuntu-headless:~$ loginctl enable-linger alex ``` ## 8. Configuration for a SharePoint Library is complete The 'onedrive' client configuration for this particular SharePoint Library is now complete. # How to configure multiple OneDrive SharePoint Shared Library sync Create a new configuration as per the process above. Repeat these steps for each SharePoint Library that you wish to use. onedrive-2.5.5/docs/terms-of-service.md000066400000000000000000000070111476564400300200120ustar00rootroot00000000000000# OneDrive Client for Linux - Software Service Terms of Service ## 1. Introduction These Terms of Service ("Terms") govern your use of the OneDrive Client for Linux ("Application") software and related Microsoft OneDrive services ("Service") provided by Microsoft. By accessing or using the Service, you agree to comply with and be bound by these Terms. If you do not agree to these Terms, please do not use the Service. ## 2. License Compliance The OneDrive Client for Linux software is licensed under the GNU General Public License, version 3.0 (the "GPLv3"). Your use of the software must comply with the terms and conditions of the GPLv3. A copy of the GPLv3 can be found here: https://www.gnu.org/licenses/gpl-3.0.en.html ## 3. Use of the Service ### 3.1. Access and Accounts You may need to create an account or provide personal information to access certain features of the Service. You are responsible for maintaining the confidentiality of your account information and are solely responsible for all activities that occur under your account. ### 3.2. Prohibited Activities You agree not to: - Use the Service in any way that violates applicable laws or regulations. - Use the Service to engage in any unlawful, harmful, or fraudulent activity. - Use the Service in any manner that disrupts, damages, or impairs the Service. ## 4. Intellectual Property The OneDrive Client for Linux software is subject to the GPLv3, and you must respect all copyrights, trademarks, and other intellectual property rights associated with the software. Any contributions you make to the software must also comply with the GPLv3. ## 5. Disclaimer of Warranties The OneDrive Client for Linux software is provided "as is" without any warranties, either expressed or implied. We do not guarantee that the use of the Application will be error-free or uninterrupted. Microsoft is not responsible for OneDrive Client for Linux. Any issues or problems with OneDrive Client for Linux should be raised on GitHub at https://github.com/abraunegg/onedrive or email support@mynas.com.au OneDrive Client for Linux is not responsible for the Microsoft OneDrive Service or the Microsoft Graph API Service that this Application utilizes. Any issue with either Microsoft OneDrive or Microsoft Graph API should be raised with Microsoft via their support channel in your country. ## 6. Limitation of Liability To the fullest extent permitted by law, we shall not be liable for any direct, indirect, incidental, special, consequential, or punitive damages, or any loss of profits or revenues, whether incurred directly or indirectly, or any loss of data, use, goodwill, or other intangible losses, resulting from (a) your use or inability to use the Service, or (b) any other matter relating to the Service. This limitation of liability explicitly relates to the use of the OneDrive Client for Linux software and does not affect your rights under the GPLv3. ## 7. Changes to Terms We reserve the right to update or modify these Terms at any time without prior notice. Any changes will be effective immediately upon posting on GitHub. Your continued use of the Service after the posting of changes constitutes your acceptance of such changes. Changes can be reviewed on GitHub. ## 8. Governing Law These Terms shall be governed by and construed in accordance with the laws of Australia, without regard to its conflict of law principles. ## 9. Contact Us If you have any questions or concerns about these Terms, please contact us at https://github.com/abraunegg/onedrive or email support@mynas.com.au onedrive-2.5.5/docs/ubuntu-package-install.md000066400000000000000000000556121476564400300212110ustar00rootroot00000000000000# Installation of 'onedrive' package on Debian and Ubuntu This document outlines the steps for installing the 'onedrive' client on Debian, Ubuntu, and their derivatives using the OpenSuSE Build Service Packages. > [!CAUTION] > This information is specifically for the following platforms and distributions: > * Debian > * Deepin > * Elementary OS > * Kali Linux > * Lubuntu > * Linux Mint > * Pop!_OS > * Peppermint OS > * Raspbian | Raspberry Pi OS > * Ubuntu | Kubuntu | Xubuntu | Ubuntu Mate > * Zorin OS > > Although packages for the 'onedrive' client are available through distribution repositories, it is strongly advised against installing them. These distribution-provided packages are outdated, unsupported, and contain bugs and issues that have already been resolved in newer versions. They should not be used. ## Determine which instructions to use Ubuntu and its clones are based on various different releases, thus, you must use the correct instructions below, otherwise you may run into package dependency issues and will be unable to install the client. ### Step 1: Remove any configured PPA and associated 'onedrive' package and systemd service files #### Step 1a: Remove PPA if configured Many Internet 'help' pages provide inconsistent details on how to install the OneDrive Client for Linux. A number of these websites continue to point users to install the client via the yann1ck PPA repository however this PPA no longer exists and should not be used. If you have previously configured, or attempted to add this PPA, this needs to be removed. To remove the PPA repository and the older client, perform the following actions: ```text sudo apt remove onedrive sudo add-apt-repository --remove ppa:yann1ck/onedrive ``` #### Step 1b: Remove errant systemd service file installed by PPA or distribution package Additionally, the distribution packages have a bad habit of creating a 'default' systemd service file when installing the 'onedrive' package so that the client will automatically run the client post being authenticated: ``` Created symlink /etc/systemd/user/default.target.wants/onedrive.service → /usr/lib/systemd/user/onedrive.service. ``` This systemd entry is erroneous and needs to be removed. Without removing this erroneous systemd link, this increases your risk of getting the following error message: ``` Opening the item database ... ERROR: onedrive application is already running - check system process list for active application instances - Use 'sudo ps aufxw | grep onedrive' to potentially determine active running process Waiting for all internal threads to complete before exiting application ``` To remove this symbolic link, run the following command: ``` sudo rm /etc/systemd/user/default.target.wants/onedrive.service ``` ### Step 2: Ensure your system is up-to-date Use a script, similar to the following to ensure your system is updated correctly: ```text #!/bin/bash rm -rf /var/lib/dpkg/lock-frontend rm -rf /var/lib/dpkg/lock apt-get update apt-get upgrade -y apt-get dist-upgrade -y apt-get autoremove -y apt-get autoclean -y ``` Run this script as 'root' by using `su -` to elevate to 'root'. Example below: ```text Welcome to Ubuntu 20.04.1 LTS (GNU/Linux 5.4.0-48-generic x86_64) * Documentation: https://help.ubuntu.com * Management: https://landscape.canonical.com * Support: https://ubuntu.com/advantage 425 updates can be installed immediately. 208 of these updates are security updates. To see these additional updates run: apt list --upgradable Your Hardware Enablement Stack (HWE) is supported until April 2025. Last login: Thu Jan 20 14:21:48 2022 from my.ip.address alex@ubuntu-20-LTS:~$ su - Password: root@ubuntu-20-LTS:~# ls -la total 28 drwx------ 3 root root 4096 Oct 10 2020 . drwxr-xr-x 20 root root 4096 Oct 10 2020 .. -rw------- 1 root root 175 Jan 20 14:23 .bash_history -rw-r--r-- 1 root root 3106 Dec 6 2019 .bashrc drwx------ 2 root root 4096 Apr 23 2020 .cache -rw-r--r-- 1 root root 161 Dec 6 2019 .profile -rwxr-xr-x 1 root root 174 Oct 10 2020 update-os.sh root@ubuntu-20-LTS:~# cat update-os.sh #!/bin/bash rm -rf /var/lib/dpkg/lock-frontend rm -rf /var/lib/dpkg/lock apt-get update apt-get upgrade -y apt-get dist-upgrade -y apt-get autoremove -y apt-get autoclean -y root@ubuntu-20-LTS:~# ./update-os.sh Hit:1 http://au.archive.ubuntu.com/ubuntu focal InRelease Hit:2 http://au.archive.ubuntu.com/ubuntu focal-updates InRelease Hit:3 http://au.archive.ubuntu.com/ubuntu focal-backports InRelease Hit:4 http://security.ubuntu.com/ubuntu focal-security InRelease Reading package lists... 96% ... Sourcing file `/etc/default/grub' Sourcing file `/etc/default/grub.d/init-select.cfg' Generating grub configuration file ... Found linux image: /boot/vmlinuz-5.13.0-27-generic Found initrd image: /boot/initrd.img-5.13.0-27-generic Found linux image: /boot/vmlinuz-5.4.0-48-generic Found initrd image: /boot/initrd.img-5.4.0-48-generic Found memtest86+ image: /boot/memtest86+.elf Found memtest86+ image: /boot/memtest86+.bin done Removing linux-modules-5.4.0-26-generic (5.4.0-26.30) ... Processing triggers for libc-bin (2.31-0ubuntu9.2) ... Reading package lists... Done Building dependency tree Reading state information... Done root@ubuntu-20-LTS:~# ``` Reboot your system after running this process before continuing with Step 3. ```text reboot ``` ### Step 3: Determine what your OS is based on Determine what your OS is based on. To do this, run the following command: ```text lsb_release -a ``` **Example:** ```text alex@ubuntu-system:~$ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 22.04 LTS Release: 22.04 Codename: jammy ``` ### Step 4: Pick the correct instructions to use If required, review the table below based on your 'lsb_release' information to pick the appropriate instructions to use: | Release & Codename | Instructions to use | |--------------------|---------------------| | Linux Mint 19.x | This platform is End-of-Life (EOL) and no longer supported. You must upgrade to at least Linux Mint 20.x | | Linux Mint 20.x | Use [Ubuntu 20.04](#distribution-ubuntu-2004) instructions below | | Linux Mint 21.x | Use [Ubuntu 22.04](#distribution-ubuntu-2204) instructions below | | Linux Mint 22.x | Use [Ubuntu 24.04](#distribution-ubuntu-2404) instructions below | | Linux Mint Debian Edition (LMDE) 5 / Elsie | Use [Debian 11](#distribution-debian-11) instructions below | | Linux Mint Debian Edition (LMDE) 6 / Faye | Use [Debian 12](#distribution-debian-12) instructions below | | Debian 9 | This platform is End-of-Life (EOL) and no longer supported. You must upgrade to Debian 12 | | Debian 10 | You must build from source or upgrade your Operating System to Debian 12 | | Debian 11 | Use [Debian 11](#distribution-debian-11) instructions below | | Debian 12 | Use [Debian 12](#distribution-debian-12) instructions below | | Debian Sid | Refer to https://packages.debian.org/sid/onedrive for assistance | | Raspbian GNU/Linux 10 | You must build from source or upgrade your Operating System to Raspbian GNU/Linux 12 | | Raspbian GNU/Linux 11 | Use [Debian 11](#distribution-debian-11) instructions below | | Raspbian GNU/Linux 12 | Use [Debian 12](#distribution-debian-12) instructions below | | Ubuntu 18.04 / Bionic | This platform is End-of-Life (EOL) and no longer supported. You must upgrade to at least Ubuntu 20.04 | | Ubuntu 20.04 / Focal | Use [Ubuntu 20.04](#distribution-ubuntu-2004) instructions below | | Ubuntu 21.04 / Hirsute | Use [Ubuntu 21.04](#distribution-ubuntu-2104) instructions below | | Ubuntu 21.10 / Impish | Use [Ubuntu 21.10](#distribution-ubuntu-2110) instructions below | | Ubuntu 22.04 / Jammy | Use [Ubuntu 22.04](#distribution-ubuntu-2204) instructions below | | Ubuntu 22.10 / Kinetic | Use [Ubuntu 22.10](#distribution-ubuntu-2210) instructions below | | Ubuntu 23.04 / Lunar | Use [Ubuntu 23.04](#distribution-ubuntu-2304) instructions below | | Ubuntu 23.10 / Mantic | Use [Ubuntu 23.10](#distribution-ubuntu-2310) instructions below | | Ubuntu 24.04 / Noble | Use [Ubuntu 24.04](#distribution-ubuntu-2404) instructions below | | Ubuntu 24.10 / Oracular | Use [Ubuntu 24.10](#distribution-ubuntu-2410) instructions below | > [!IMPORTANT] > If your Linux distribution and release is not in the table above, you have 2 options: > > 1. Compile the application from source. Refer to install.md (Compilation & Installation) for assistance. > 2. Raise a support case with your Linux Distribution to provide you with an applicable package you can use. ## Distribution Package Install Instructions ### Distribution: Debian 11 The packages support the following platform architectures: |  i686  | x86_64 | ARMHF | AARCH64 | |:----:|:------:|:-----:|:-------:| |✔|✔|✔|✔| #### Step 1: Add the OpenSuSE Build Service repository release key Add the OpenSuSE Build Service repository release key using the following command: ```text wget -qO - https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/Debian_11/Release.key | gpg --dearmor | sudo tee /usr/share/keyrings/obs-onedrive.gpg > /dev/null ``` #### Step 2: Add the OpenSuSE Build Service repository Add the OpenSuSE Build Service repository using the following command: ```text echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/obs-onedrive.gpg] https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/Debian_11/ ./" | sudo tee /etc/apt/sources.list.d/onedrive.list ``` #### Step 3: Update your apt package cache Run: `sudo apt-get update` #### Step 4: Install 'onedrive' Run: `sudo apt install --no-install-recommends --no-install-suggests onedrive` #### Step 5: Read 'Known Issues' with these packages Read and understand the [known issues](#known-issues-with-installing-from-the-above-packages) with these packages below, taking any action that is needed. ### Distribution: Debian 12 The packages support the following platform architectures: |  i686  | x86_64 | ARMHF | AARCH64 | |:----:|:------:|:-----:|:-------:| |✔|✔|✔|✔| #### Step 1: Add the OpenSuSE Build Service repository release key Add the OpenSuSE Build Service repository release key using the following command: ```text wget -qO - https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/Debian_12/Release.key | gpg --dearmor | sudo tee /usr/share/keyrings/obs-onedrive.gpg > /dev/null ``` #### Step 2: Add the OpenSuSE Build Service repository Add the OpenSuSE Build Service repository using the following command: ```text echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/obs-onedrive.gpg] https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/Debian_12/ ./" | sudo tee /etc/apt/sources.list.d/onedrive.list ``` #### Step 3: Update your apt package cache Run: `sudo apt-get update` #### Step 4: Install 'onedrive' Run: `sudo apt install --no-install-recommends --no-install-suggests onedrive` #### Step 5: Read 'Known Issues' with these packages Read and understand the [known issues](#known-issues-with-installing-from-the-above-packages) with these packages below, taking any action that is needed. ### Distribution: Ubuntu 20.04 The packages support the following platform architectures: |  i686  | x86_64 | ARMHF | AARCH64 | |:----:|:------:|:-----:|:-------:| |❌|✔|✔|✔| #### Step 1: Add the OpenSuSE Build Service repository release key Add the OpenSuSE Build Service repository release key using the following command: ```text wget -qO - https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_20.04/Release.key | sudo apt-key add - ``` #### Step 2: Add the OpenSuSE Build Service repository Add the OpenSuSE Build Service repository using the following command: ```text echo 'deb https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_20.04/ ./' | sudo tee /etc/apt/sources.list.d/onedrive.list ``` #### Step 3: Update your apt package cache Run: `sudo apt-get update` #### Step 4: Install 'onedrive' Run: `sudo apt install --no-install-recommends --no-install-suggests onedrive` #### Step 5: Read 'Known Issues' with these packages Read and understand the [known issues](#known-issues-with-installing-from-the-above-packages) with these packages below, taking any action that is needed. ### Distribution: Ubuntu 21.04 The packages support the following platform architectures: |  i686  | x86_64 | ARMHF | AARCH64 | |:----:|:------:|:-----:|:-------:| |❌|✔|✔|✔| #### Step 1: Add the OpenSuSE Build Service repository release key Add the OpenSuSE Build Service repository release key using the following command: ```text wget -qO - https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_21.04/Release.key | gpg --dearmor | sudo tee /usr/share/keyrings/obs-onedrive.gpg > /dev/null ``` #### Step 2: Add the OpenSuSE Build Service repository Add the OpenSuSE Build Service repository using the following command: ```text echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/obs-onedrive.gpg] https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_21.04/ ./" | sudo tee /etc/apt/sources.list.d/onedrive.list ``` #### Step 3: Update your apt package cache Run: `sudo apt-get update` #### Step 4: Install 'onedrive' Run: `sudo apt install --no-install-recommends --no-install-suggests onedrive` #### Step 5: Read 'Known Issues' with these packages Read and understand the [known issues](#known-issues-with-installing-from-the-above-packages) with these packages below, taking any action that is needed. ### Distribution: Ubuntu 21.10 The packages support the following platform architectures: |  i686  | x86_64 | ARMHF | AARCH64 | |:----:|:------:|:-----:|:-------:| |❌|✔|✔|✔| #### Step 1: Add the OpenSuSE Build Service repository release key Add the OpenSuSE Build Service repository release key using the following command: ```text wget -qO - https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_21.10/Release.key | gpg --dearmor | sudo tee /usr/share/keyrings/obs-onedrive.gpg > /dev/null ``` #### Step 2: Add the OpenSuSE Build Service repository Add the OpenSuSE Build Service repository using the following command: ```text echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/obs-onedrive.gpg] https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_21.10/ ./" | sudo tee /etc/apt/sources.list.d/onedrive.list ``` #### Step 3: Update your apt package cache Run: `sudo apt-get update` #### Step 4: Install 'onedrive' Run: `sudo apt install --no-install-recommends --no-install-suggests onedrive` #### Step 5: Read 'Known Issues' with these packages Read and understand the [known issues](#known-issues-with-installing-from-the-above-packages) with these packages below, taking any action that is needed. ### Distribution: Ubuntu 22.04 The packages support the following platform architectures: |  i686  | x86_64 | ARMHF | AARCH64 | |:----:|:------:|:-----:|:-------:| |❌|✔|✔|✔| #### Step 1: Add the OpenSuSE Build Service repository release key Add the OpenSuSE Build Service repository release key using the following command: ```text wget -qO - https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_22.04/Release.key | gpg --dearmor | sudo tee /usr/share/keyrings/obs-onedrive.gpg > /dev/null ``` #### Step 2: Add the OpenSuSE Build Service repository Add the OpenSuSE Build Service repository using the following command: ```text echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/obs-onedrive.gpg] https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_22.04/ ./" | sudo tee /etc/apt/sources.list.d/onedrive.list ``` #### Step 3: Update your apt package cache Run: `sudo apt-get update` #### Step 4: Install 'onedrive' Run: `sudo apt install --no-install-recommends --no-install-suggests onedrive` #### Step 5: Read 'Known Issues' with these packages Read and understand the [known issues](#known-issues-with-installing-from-the-above-packages) with these packages below, taking any action that is needed. ### Distribution: Ubuntu 22.10 The packages support the following platform architectures: |  i686  | x86_64 | ARMHF | AARCH64 | |:----:|:------:|:-----:|:-------:| |❌|✔|✔|✔| #### Step 1: Add the OpenSuSE Build Service repository release key Add the OpenSuSE Build Service repository release key using the following command: ```text wget -qO - https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_22.10/Release.key | gpg --dearmor | sudo tee /usr/share/keyrings/obs-onedrive.gpg > /dev/null ``` #### Step 2: Add the OpenSuSE Build Service repository Add the OpenSuSE Build Service repository using the following command: ```text echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/obs-onedrive.gpg] https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_22.10/ ./" | sudo tee /etc/apt/sources.list.d/onedrive.list ``` #### Step 3: Update your apt package cache Run: `sudo apt-get update` #### Step 4: Install 'onedrive' Run: `sudo apt install --no-install-recommends --no-install-suggests onedrive` #### Step 5: Read 'Known Issues' with these packages Read and understand the [known issues](#known-issues-with-installing-from-the-above-packages) with these packages below, taking any action that is needed. ### Distribution: Ubuntu 23.04 The packages support the following platform architectures: |  i686  | x86_64 | ARMHF | AARCH64 | |:----:|:------:|:-----:|:-------:| |❌|✔|✔|✔| #### Step 1: Add the OpenSuSE Build Service repository release key Add the OpenSuSE Build Service repository release key using the following command: ```text wget -qO - https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_23.04/Release.key | gpg --dearmor | sudo tee /usr/share/keyrings/obs-onedrive.gpg > /dev/null ``` #### Step 2: Add the OpenSuSE Build Service repository Add the OpenSuSE Build Service repository using the following command: ```text echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/obs-onedrive.gpg] https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_23.04/ ./" | sudo tee /etc/apt/sources.list.d/onedrive.list ``` #### Step 3: Update your apt package cache Run: `sudo apt-get update` #### Step 4: Install 'onedrive' Run: `sudo apt install --no-install-recommends --no-install-suggests onedrive` #### Step 5: Read 'Known Issues' with these packages Read and understand the [known issues](#known-issues-with-installing-from-the-above-packages) with these packages below, taking any action that is needed. ### Distribution: Ubuntu 23.10 The packages support the following platform architectures: |  i686  | x86_64 | ARMHF | AARCH64 | |:----:|:------:|:-----:|:-------:| |❌|✔|❌|✔| #### Step 1: Add the OpenSuSE Build Service repository release key Add the OpenSuSE Build Service repository release key using the following command: ```text wget -qO - https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_23.10/Release.key | gpg --dearmor | sudo tee /usr/share/keyrings/obs-onedrive.gpg > /dev/null ``` #### Step 2: Add the OpenSuSE Build Service repository Add the OpenSuSE Build Service repository using the following command: ```text echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/obs-onedrive.gpg] https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_23.10/ ./" | sudo tee /etc/apt/sources.list.d/onedrive.list ``` #### Step 3: Update your apt package cache Run: `sudo apt-get update` #### Step 4: Install 'onedrive' Run: `sudo apt install --no-install-recommends --no-install-suggests onedrive` #### Step 5: Read 'Known Issues' with these packages Read and understand the [known issues](#known-issues-with-installing-from-the-above-packages) with these packages below, taking any action that is needed. ### Distribution: Ubuntu 24.04 The packages support the following platform architectures: |  i686  | x86_64 | ARMHF | AARCH64 | |:----:|:------:|:-----:|:-------:| |❌|✔|❌|✔| #### Step 1: Add the OpenSuSE Build Service repository release key Add the OpenSuSE Build Service repository release key using the following command: ```text wget -qO - https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_24.04/Release.key | gpg --dearmor | sudo tee /usr/share/keyrings/obs-onedrive.gpg > /dev/null ``` #### Step 2: Add the OpenSuSE Build Service repository Add the OpenSuSE Build Service repository using the following command: ```text echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/obs-onedrive.gpg] https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_24.04/ ./" | sudo tee /etc/apt/sources.list.d/onedrive.list ``` #### Step 3: Update your apt package cache Run: `sudo apt-get update` #### Step 4: Install 'onedrive' Run: `sudo apt install --no-install-recommends --no-install-suggests onedrive` #### Step 5: Read 'Known Issues' with these packages Read and understand the [known issues](#known-issues-with-installing-from-the-above-packages) with these packages below, taking any action that is needed. ### Distribution: Ubuntu 24.10 The packages support the following platform architectures: |  i686  | x86_64 | ARMHF | AARCH64 | |:----:|:------:|:-----:|:-------:| |❌|✔|❌|✔| #### Step 1: Add the OpenSuSE Build Service repository release key Add the OpenSuSE Build Service repository release key using the following command: ```text wget -qO - https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_24.10/Release.key | gpg --dearmor | sudo tee /usr/share/keyrings/obs-onedrive.gpg > /dev/null ``` #### Step 2: Add the OpenSuSE Build Service repository Add the OpenSuSE Build Service repository using the following command: ```text echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/obs-onedrive.gpg] https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_24.10/ ./" | sudo tee /etc/apt/sources.list.d/onedrive.list ``` #### Step 3: Update your apt package cache Run: `sudo apt-get update` #### Step 4: Install 'onedrive' Run: `sudo apt install --no-install-recommends --no-install-suggests onedrive` #### Step 5: Read 'Known Issues' with these packages Read and understand the [known issues](#known-issues-with-installing-from-the-above-packages) with these packages below, taking any action that is needed. ## Known Issues with Installing from the above packages There are currently no known issues when installing 'onedrive' from the OpenSuSE Build Service repository. onedrive-2.5.5/docs/usage.md000066400000000000000000002355711476564400300157420ustar00rootroot00000000000000# Using the OneDrive Client for Linux ## Application Version Before reading this document, please ensure you are running application version [![Version](https://img.shields.io/github/v/release/abraunegg/onedrive)](https://github.com/abraunegg/onedrive/releases) or greater. Use `onedrive --version` to determine what application version you are using and upgrade your client if required. ## Table of Contents - [Important Notes](#important-notes) - [Memory Usage](#memory-usage) - [Upgrading from the 'skilion' Client](#upgrading-from-the-skilion-client) - [Guidelines for Local File and Folder Naming in the Synchronisation Directory](#guidelines-for-local-file-and-folder-naming-in-the-synchronisation-directory) - [Support for Microsoft Azure Information Protected Files](#support-for-microsoft-azure-information-protected-files) - [Compatibility with curl](#compatibility-with-curl) - [First Steps](#first-steps) - [Authorise the Application with Your Microsoft OneDrive Account](#authorise-the-application-with-your-microsoft-onedrive-account) - [Display Your Applicable Runtime Configuration](#display-your-applicable-runtime-configuration) - [Understanding OneDrive Client for Linux Operational Modes](#understanding-onedrive-client-for-linux-operational-modes) - [Standalone Synchronisation Operational Mode (Standalone Mode)](#standalone-synchronisation-operational-mode-standalone-mode) - [Ongoing Synchronisation Operational Mode (Monitor Mode)](#ongoing-synchronisation-operational-mode-monitor-mode) - [Using the OneDrive Client for Linux to synchronise your data](#using-the-onedrive-client-for-linux-to-synchronise-your-data) - [Increasing application logging level](#increasing-application-logging-level) - [Using 'Client Side Filtering' rules to determine what should be synced with Microsoft OneDrive](#using-client-side-filtering-rules-to-determine-what-should-be-synced-with-microsoft-onedrive) - [Testing your configuration](#testing-your-configuration) - [Performing a sync with Microsoft OneDrive](#performing-a-sync-with-microsoft-onedrive) - [Performing a single directory synchronisation with Microsoft OneDrive](#performing-a-single-directory-synchronisation-with-microsoft-onedrive) - [Performing a 'one-way' download synchronisation with Microsoft OneDrive](#performing-a-one-way-download-synchronisation-with-microsoft-onedrive) - [Performing a 'one-way' upload synchronisation with Microsoft OneDrive](#performing-a-one-way-upload-synchronisation-with-microsoft-onedrive) - [Performing a selective synchronisation via 'sync_list' file](#performing-a-selective-synchronisation-via-sync_list-file) - [Performing a --resync](#performing-a---resync) - [Performing a --force-sync without a --resync or changing your configuration](#performing-a---force-sync-without-a---resync-or-changing-your-configuration) - [Enabling the Client Activity Log](#enabling-the-client-activity-log) - [Client Activity Log Example:](#client-activity-log-example) - [Client Activity Log Differences](#client-activity-log-differences) - [GUI Notifications](#gui-notifications) - [Handling a Microsoft OneDrive Account Password Change](#handling-a-microsoft-onedrive-account-password-change) - [Determining the synchronisation result](#determining-the-synchronisation-result) - [Frequently Asked Configuration Questions](#frequently-asked-configuration-questions) - [How to change the default configuration of the client?](#how-to-change-the-default-configuration-of-the-client) - [How to change where my data from Microsoft OneDrive is stored?](#how-to-change-where-my-data-from-microsoft-onedrive-is-stored) - [How to change what file and directory permissions are assigned to data that is downloaded from Microsoft OneDrive?](#how-to-change-what-file-and-directory-permissions-are-assigned-to-data-that-is-downloaded-from-microsoft-onedrive) - [How are uploads and downloads managed?](#how-are-uploads-and-downloads-managed) - [How to only sync a specific directory?](#how-to-only-sync-a-specific-directory) - [How to 'skip' files from syncing?](#how-to-skip-files-from-syncing) - [How to 'skip' directories from syncing?](#how-to-skip-directories-from-syncing) - [How to 'skip' .files and .folders from syncing?](#how-to-skip-files-and-folders-from-syncing) - [How to 'skip' files larger than a certain size from syncing?](#how-to-skip-files-larger-than-a-certain-size-from-syncing) - [How to 'rate limit' the application to control bandwidth consumed for upload & download operations?](#how-to-rate-limit-the-application-to-control-bandwidth-consumed-for-upload--download-operations) - [How can I prevent my local disk from filling up?](#how-can-i-prevent-my-local-disk-from-filling-up) - [How does the client handle symbolic links?](#how-does-the-client-handle-symbolic-links) - [How to synchronise OneDrive Personal Shared Folders?](#how-to-synchronise-onedrive-personal-shared-folders) - [How to synchronise OneDrive Business Shared Items (Files and Folders)?](#how-to-synchronise-onedrive-business-shared-items-files-and-folders) - [How to synchronise SharePoint / Office 365 Shared Libraries?](#how-to-synchronise-sharepoint--office-365-shared-libraries) - [How to Create a Shareable Link?](#how-to-create-a-shareable-link) - [How to Synchronise Both Personal and Business Accounts at once?](#how-to-synchronise-both-personal-and-business-accounts-at-once) - [How to Synchronise Multiple SharePoint Libraries simultaneously?](#how-to-synchronise-multiple-sharepoint-libraries-simultaneously) - [How to Receive Real-time Changes from Microsoft OneDrive Service, instead of waiting for the next sync period?](#how-to-receive-real-time-changes-from-microsoft-onedrive-service-instead-of-waiting-for-the-next-sync-period) - [How to initiate the client as a background service?](#how-to-initiate-the-client-as-a-background-service) - [OneDrive service running as root user via init.d](#onedrive-service-running-as-root-user-via-initd) - [OneDrive service running as root user via systemd (Arch, Ubuntu, Debian, OpenSuSE, Fedora)](#onedrive-service-running-as-root-user-via-systemd-arch-ubuntu-debian-opensuse-fedora) - [OneDrive service running as root user via systemd (Red Hat Enterprise Linux, CentOS Linux)](#onedrive-service-running-as-root-user-via-systemd-red-hat-enterprise-linux-centos-linux) - [OneDrive service running as a non-root user via systemd (All Linux Distributions)](#onedrive-service-running-as-a-non-root-user-via-systemd-all-linux-distributions) - [OneDrive service running as a non-root user via systemd (with notifications enabled) (Arch, Ubuntu, Debian, OpenSuSE, Fedora)](#onedrive-service-running-as-a-non-root-user-via-systemd-with-notifications-enabled-arch-ubuntu-debian-opensuse-fedora) - [OneDrive service running as a non-root user via runit (antiX, Devuan, Artix, Void)](#onedrive-service-running-as-a-non-root-user-via-runit-antix-devuan-artix-void) - [How to start a user systemd service at boot without user login?](#how-to-start-a-user-systemd-service-at-boot-without-user-login) - [How to access Microsoft OneDrive service through a proxy](#how-to-access-microsoft-onedrive-service-through-a-proxy) - [How to set up SELinux for a sync folder outside of the home folder](#how-to-set-up-selinux-for-a-sync-folder-outside-of-the-home-folder) - [Advanced Configuration of the OneDrive Client for Linux](#advanced-configuration-of-the-onedrive-client-for-linux) - [Overview of all OneDrive Client for Linux CLI Options](#overview-of-all-onedrive-client-for-linux-cli-options) ## Important Notes ### Memory Usage Starting with version 2.5.x, the application has been completely rewritten. It is crucial to understand the memory requirements to ensure the application runs smoothly on your system. During a `--resync` or full online scan, the OneDrive Client may use approximately 1GB of memory for every 100,000 objects stored online. This is because the client retrieves data for all objects via the OneDrive API before processing them locally. Once this process completes, the memory is freed. To avoid performance issues, ensure your system has sufficient available memory. If the system starts using swap space due to insufficient free memory, this can significantly slow down the application and impact overall performance. To avoid potential system instability or the client being terminated by your Out-Of-Memory (OOM) process monitors, please ensure your system has sufficient memory allocated or configure adequate swap space. ### Upgrading from the 'skilion' Client The 'skilion' version has a significant number of issues in how it manages the local sync state. When upgrading from the 'skilion' client to this client, it's recommended to stop any service or OneDrive process that may be running. Once all OneDrive services are stopped, make sure to remove any old client binaries from your system. ### Guidelines for Local File and Folder Naming in the Synchronisation Directory To ensure seamless synchronisation with Microsoft OneDrive, it's critical to adhere strictly to the prescribed naming conventions for your files and folders within the sync directory. The guidelines detailed below are designed to preempt potential sync failures by aligning with Microsoft Windows Naming Conventions, coupled with specific OneDrive restrictions. > [!WARNING] > Failure to comply will result in synchronisation being bypassed for the offending files or folders, necessitating a rename of the local item to establish sync compatibility. #### Key Restrictions and Limitations * Invalid Characters: * Avoid using the following characters in names of files and folders: `" * : < > ? / \ |` * Names should not start or end with spaces * Names should not end with a fullstop / period character `.` * Prohibited Names: * Certain names are reserved and cannot be used for files or folders: `.lock`, `CON`, `PRN`, `AUX`, `NUL`, `COM0 - COM9`, `LPT0 - LPT9`, `desktop.ini`, any filename starting with `~$` * The text sequence `_vti_` cannot appear anywhere in a file or directory name * A file and folder called `forms` is unsupported at the root level of a synchronisation directory * Path Length * All files and folders stored in your 'sync_dir' (typically `~/OneDrive`) must not have a path length greater than: * 400 characters for OneDrive Business & SharePoint * 430 characters for OneDrive Personal Should a file or folder infringe upon these naming conventions or restrictions, synchronisation will skip the item, indicating an invalid name according to Microsoft Naming Convention. The only remedy is to rename the offending item. This constraint is by design and remains firm. > [!TIP] > The UTF-16 character set provides a capability to use alternative characters to work around the restrictions and limitations imposed by Microsoft OneDrive. An example of some replacement characters are below: > | Standard Invalid Character | Potential UTF-16 Replacement Character | > |--------------------|------------------------------| > | . | ․ (One Dot Leader, `\u2024`) | > | : | ː (Modifier Letter Triangular Colon, `\u02D0`) | > | \| | │ (Box Drawings Light Vertical, `\u2502`) | > [!CAUTION] > The last critically important point is that Microsoft OneDrive does not adhere to POSIX standards, which fundamentally impacts naming conventions. In Unix environments (which are POSIX compliant), files and folders can exist simultaneously with identical names if their capitalisation differs. **This is not possible on Microsoft OneDrive.** If such a scenario occurs, the OneDrive Client for Linux will encounter a conflict, preventing the synchronisation of the conflicting file or folder. This constraint is a conscious design choice and is immutable. To avoid synchronisation issues, preemptive renaming of any conflicting local files or folders is advised. #### Further reading: The above guidelines are essential for maintaining synchronisation integrity with Microsoft OneDrive. Adhering to them ensures your files and folders sync without issue. For additional details, consult the following resources: * [Microsoft Windows Naming Conventions](https://docs.microsoft.com/windows/win32/fileio/naming-a-file) * [Restrictions and limitations in OneDrive and SharePoint](https://support.microsoft.com/en-us/office/restrictions-and-limitations-in-onedrive-and-sharepoint-64883a5d-228e-48f5-b3d2-eb39e07630fa) **Adherence to these guidelines is not optional but mandatory to avoid sync disruptions.** ### Support for Microsoft Azure Information Protected Files > [!CAUTION] > If you are using OneDrive Business Accounts and your organisation implements Azure Information Protection, these AIP files will report as one size & hash online, but when downloaded, will report a totally different size and hash. This is due to how the Microsoft Graph API handles AIP files and how Microsoft SharePoint (the technology behind Microsoft OneDrive for Business) serves these files via the API. > > By default these files will fail integrity checking and be deleted locally, meaning that AIP files will not reside on your platform. These AIP files will be flagged as a failed download during application operation. > > If you chose to enable `--disable-download-validation` , the AIP files will download to your platform, however, if there are any other genuine download failures where the size and hash are different, these too will be retained locally meaning you may experience data integrity loss. This is due to the Microsoft Graph API lacking any capability to identify up-front that a file utilises AIP, thus zero capability to differentiate between AIP and non-AIP files for failure detection. > > Please use the `--disable-download-validation` option with extreme caution and understand the risk if you enable it. ### Compatibility with curl If your system uses curl < 7.47.0, curl will default to HTTP/1.1 for HTTPS operations, and the client will follow suit, using HTTP/1.1. For systems running curl >= 7.47.0 and < 7.62.0, curl will prefer HTTP/2 for HTTPS, but it will still use HTTP/1.1 as the default for these operations. The client will employ HTTP/1.1 for HTTPS operations as well. However, if your system employs curl >= 7.62.0, curl will, by default, prioritise HTTP/2 over HTTP/1.1. In this case, the client will utilise HTTP/2 for most HTTPS operations and stick with HTTP/1.1 for others. Please note that this distinction is governed by the OneDrive platform, not our client. If you explicitly want to use HTTP/1.1, you can do so by using the `--force-http-11` flag or setting the configuration option `force_http_11 = "true"`. This will compel the application to exclusively use HTTP/1.1. Otherwise, all client operations will align with the curl default settings for your distribution. #### Known curl bugs that impact the use of this client | id | curl bug | fixed in curl version | |----|----------|-----------------------| | 1 | HTTP/2 support: Introduced HTTP/2 support, enabling multiplexed transfers over a single connection | 7.47.0 | | 2 | HTTP/2 issue: Resolved an issue where HTTP/2 connections were not properly reused, leading to unnecessary new connections. | 7.68.0 | | 3 | HTTP/2 issue: Addressed a race condition in HTTP/2 multiplexing that could lead to unexpected behavior. | 7.74.0 | | 4 | HTTP/2 issue: Improved handling of HTTP/2 priority frames to ensure proper stream prioritisation. | 7.81.0 | | 5 | HTTP/2 issue: Fixed a bug where HTTP/2 connections were prematurely closed, resulting in incomplete data transfers. | 7.88.1 | | 6 | HTTP/2 issue: Resolved a problem with HTTP/2 frame handling that could cause data corruption during transfers. | 8.2.1 | | 7 | HTTP/2 issue: Corrected an issue where HTTP/2 streams were not properly closed, leading to potential memory leaks. | 8.5.0 | | 8 | HTTP/2 issue: Addressed a bug where HTTP/2 connections could hang under specific conditions, improving reliability. | 8.8.0 | | 9 | HTTP/2 issue: Improved handling of HTTP/2 connections to prevent unexpected stream resets and enhance stability. | 8.9.0 | | 10 | SIGPIPE issue: Resolved a problem where SIGPIPE signals were not properly handled, leading to unexpected behavior. | 8.9.1 | | 11 | SIGPIPE issue: Addressed a SIGPIPE leak that occurred in certain cases starting with version 8.9.1 | 8.10.0 | | 12 | HTTP/2 issue: Stopped offering ALPN `http/1.1` for `http2-prior-knowledge` to ensure proper protocol negotiation. | 8.10.0 | | 13 | HTTP/2 issue: Improved handling of end-of-stream (EOS) and blocked states to prevent unexpected behavior.| 8.11.0 | #### Known curl versions with compatibility issues for this client | curl Version | distribution | curl bugs | |--------------|--------------|-----------| | 7.68.0 | Ubuntu 20.04 LTS (Focal Fossa) | 2,3,4,5,6,7,8,9,10,11,12,13 | | 7.74.0 | Debian 11 (Bullseye) | 4,5,6,7,8,9,10,11,12,13 | | 7.81.0 | Ubuntu 22.04 LTS (Jammy Jellyfish) | 5,6,7,8,9,10,11,12,13 | | 7.88.1 | Debian 12 (Bookworm) | 6,7,8,9,10,11,12,13 | | 8.2.1 | Alpine Linux 3.14 | 7,8,9,10,11,12,13 | | 8.5.0 | Alpine Linux 3.15, Ubuntu 24.04 LTS (Noble Numbat) | 8,9,10,11,12,13 | | 8.10.0 | Alpine Linux 3.17 | 13 | > [!IMPORTANT] > If your distribution provides one of these curl versions you must upgrade your curl version to the latest available, or get your distribution to provide a more modern version of curl. Refer to [curl releases](https://curl.se/docs/releases.html) for curl version information. > > If you are using one of the above curl versions, the following application message will be generated: > ```text > WARNING: Your curl/libcurl version (curl.version.number) has known HTTP/2 bugs that impact the use of this application. > Please report this to your distribution and request that they provide a newer curl version for your platform or upgrade this yourself. > Downgrading all application operations to use HTTP/1.1 to ensure maximum operational stability. > Please read https://github.com/abraunegg/onedrive/blob/master/docs/usage.md#compatibility-with-curl for more information. > ``` > > The WARNING line will be sent to the GUI for notification purposes if notifications have been enabled. To avoid this message and/or the GUI notification your only have 2 options: > 1. Upgrade your curl version on your platform > 2. Configure the client to always downgrade client operations to HTTP/1.1 and use IPv4 only > > If you are unable to upgrade your version of curl, to always downgrade client operations to HTTP/1.1 you must add the following to your config file: > ```text > force_http_11 = "true" > ip_protocol_version = "1" > ``` > When these two options are applied to your application configuration, the following application message will be generated: > ```text > WARNING: Your curl/libcurl version (curl.version.number) has known operational bugs that impact the use of this application. > Please report this to your distribution and request that they provide a newer curl version for your platform or upgrade this yourself. > ``` > > The WARNING line will be now only be written to application logging output, no longer sending a GUI notification message. > [!IMPORTANT] > Outside of the above known broken curl versions, there are significant HTTP/2 bugs in all curl versions < 8.6.x that can lead to HTTP/2 errors such as `Error in the HTTP2 framing layer on handle` or `Stream error in the HTTP/2 framing layer on handle` > > The only options to resolve this issue are the following: > 1. Upgrade your curl version to the latest available, or get your distribution to provide a more modern version of curl. Refer to [curl releases](https://curl.se/docs/releases.html) for curl version information. > 2. Configure the client to only use HTTP/1.1 via the config option `--force-http-11` flag or set the configuration file option `force_http_11 = "true"` > [!IMPORTANT] > Outside of the above known broken curl versions, it has also been evidenced that curl has an internal DNS resolution bug that at random times will skip using IPv4 for DNS resolution and only uses IPv6 DNS resolution when the host system is configured to use IPv4 and IPv6 addressing. > > As a result of this internal curl resolution bug, if your system does not have an IPv6 DNS resolver, and/or does not have a valid IPv6 network path to Microsoft OneDrive, you may encounter these errors: > > * `A libcurl timeout has been triggered - data transfer too slow, no DNS resolution response, no server response` > * `Could not connect to server on handle ABC12DEF3456` > > The only options to resolve this issue are the following: > 1. Implement and/or ensure that IPv6 DNS resolution is possible on your system; allow IPv6 network connectivity between your system and Microsoft OneDrive > 2. Configure the client to only use IPv4 DNS resolution via setting the configuration option `ip_protocol_version = "1"` > [!IMPORTANT] > If you are using Debian 12 or Linux Mint Debian Edition (LMDE) 6, you can install curl version 8.10.1 from the respective backports repositories to address the bugs present in the default Debian 12 curl version. > [!CAUTION] > If you continue to use a curl/libcurl version with known HTTP/2 bugs you will experience application runtime issues such as randomly exiting for zero reason or incomplete download/upload of your data. ## First Steps ### Authorise the Application with Your Microsoft OneDrive Account Once you've installed the application, you'll need to authorise it using your Microsoft OneDrive Account. This can be done by simply running the application without any additional command switches. Please be aware that some companies may require you to explicitly add this app to the [Microsoft MyApps portal](https://myapps.microsoft.com/). To add an approved app to your apps, click on the ellipsis in the top-right corner and select "Request new apps." On the next page, you can add this app. If it's not listed, you should make a request through your IT department. When you run the application for the first time, you'll be prompted to open a specific URL using your web browser, where you'll need to log in to your Microsoft Account and grant the application permission to access your files. After granting permission to the application, you'll be redirected to a blank page. Simply copy the URI from the blank page and paste it into the application. **Example:** ```text [user@hostname ~]$ onedrive Authorise this app by visiting: https://login.microsoftonline.com/common/oauth2/v2.0/authorise?client_id=22c49a0d-d21c-4792-aed1-8f163c982546&scope=Files.ReadWrite%20Files.ReadWrite.all%20Sites.ReadWrite.All%20offline_access&response_type=code&redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient Enter the response URI from your browser: https://login.microsoftonline.com/common/oauth2/nativeclient?code= The application has been successfully authorised, but no additional command switches were provided. Please use 'onedrive --help' for further assistance on how to run this application. ``` > [!IMPORTANT] > Without additional input or configuration, the OneDrive Client for Linux will automatically adhere to default application settings during synchronisation processes with Microsoft OneDrive. ### Display Your Applicable Runtime Configuration To verify the configuration that the application will use, use the following command: ```text onedrive --display-config ``` This command will display all the relevant runtime interpretations of the options and configurations you are using. An example output is as follows: ```text Reading configuration file: /home/user/.config/onedrive/config Configuration file successfully loaded onedrive version = vX.Y.Z-A-bcdefghi Config path = /home/user/.config/onedrive Config file found in config path = true Config option 'drive_id' = Config option 'sync_dir' = ~/OneDrive ... Config option 'webhook_enabled' = false ``` > [!IMPORTANT] > When using multiple OneDrive accounts, it's essential to always use the `--confdir` command followed by the appropriate configuration directory. This ensures that the specific configuration you intend to view is correctly displayed. ### Understanding OneDrive Client for Linux Operational Modes There are two modes of operation when using the client: 1. Standalone sync mode that performs a single sync action against Microsoft OneDrive. 2. Ongoing sync mode that continuously syncs your data with Microsoft OneDrive. > [!TIP] > To understand further the client operational modes and how the client operates, please review the [client architecture](client-architecture.md) documentation. > [!IMPORTANT] > The default setting for the OneDrive Client on Linux will sync all data from your Microsoft OneDrive account to your local device. To avoid this and select specific items for synchronisation, you should explore setting up 'Client Side Filtering' rules. This will help you manage and specify what exactly gets synced with your Microsoft OneDrive account. #### Standalone Synchronisation Operational Mode (Standalone Mode) This method of use can be employed by issuing the following option to the client: ```text onedrive --sync ``` For simplicity, this can be shortened to the following: ```text onedrive -s ``` #### Ongoing Synchronisation Operational Mode (Monitor Mode) This method of use can be utilised by issuing the following option to the client: ```text onedrive --monitor ``` For simplicity, this can be shortened to the following: ```text onedrive -m ``` > [!NOTE] > This method of use is used when enabling a systemd service to run the application in the background. Two common errors can occur when using monitor mode: * Initialisation failure * Unable to add a new inotify watch Both of these errors are local environment issues, where the following system variables need to be increased as the current system values are potentially too low: * `fs.file-max` * `fs.inotify.max_user_watches` To determine what the existing values are on your system, use the following commands: ```text sysctl fs.file-max sysctl fs.inotify.max_user_watches ``` Alternatively, when running the client with increased verbosity (see below), the client will display what the current configured system maximum values are: ```text ... All application operations will be performed in: /home/user/OneDrive OneDrive synchronisation interval (seconds): 300 Maximum allowed open files: 393370 <-- This is the current operating system fs.file-max value Maximum allowed inotify watches: 29374 <-- This is the current operating system fs.inotify.max_user_watches value Initialising filesystem inotify monitoring ... ... ``` To determine what value to change to, you need to count all the files and folders in your configured 'sync_dir': ```text cd /path/to/your/sync/dir ls -laR | wc -l ``` To make a change to these variables using your file and folder count, use the following process: ```text sudo sysctl fs.file-max= sudo sysctl fs.inotify.max_user_watches= ``` Once these values are changed, you will need to restart your client so that the new values are detected and used. To make these changes permanent on your system, refer to your OS reference documentation. ## Using the OneDrive Client for Linux to synchronise your data ### Increasing application logging level When running a sync (`--sync`) or using monitor mode (`--monitor`), it may be desirable to see additional information regarding the progress and operation of the client. For example, for a `--sync` command, this would be: ```text onedrive --sync --verbose ``` Furthermore, for simplicity, this can be simplified to the following: ``` onedrive -s -v ``` > [!IMPORTANT] > Adding `--verbose` twice will enable debug logging output. This is generally required when raising a bug report or needing to understand a problem. ### Using 'Client Side Filtering' rules to determine what should be synced with Microsoft OneDrive Client Side Filtering in the context of the OneDrive Client for Linux refers to user-configured rules that determine what files and directories the client should upload or download from Microsoft OneDrive. These rules are crucial for optimising synchronisation, especially when dealing with large numbers of files or specific file types. The OneDrive Client for Linux offers several configuration options to facilitate this: * **check_nosync:** This option allows you to create a `.nosync` file in local directories, to skip that directory from being included in sync operations. * **skip_dir:** This option allows the user to specify directories that should not be synchronised with OneDrive. It's particularly useful for omitting large or irrelevant directories from the sync process. * **skip_dotfiles:** Dotfiles, usually configuration files or scripts, can be excluded from the sync. This is useful for users who prefer to keep these files local. * **skip_file:** Specific files can be excluded from synchronisation using this option. It provides flexibility in selecting which files are essential for cloud storage. * **skip_size:** Skip files greater than this specific size (in MB) * **skip_symlinks:** Symlinks often point to files outside the OneDrive directory or to locations that are not relevant for cloud storage. This option prevents them from being included in the sync. Additionally, the OneDrive Client for Linux allows the implementation of Client Side Filtering rules through a 'sync_list' file. This file explicitly states which directories or files should be included in the synchronisation. By default, any item not listed in the 'sync_list' file is excluded. This method offers a more granular approach to synchronisation, ensuring that only the necessary data is transferred to and from Microsoft OneDrive. These configurable options and the 'sync_list' file provide users with the flexibility to tailor the synchronisation process to their specific needs, conserving bandwidth and storage space while ensuring that important files are always backed up and accessible. > [!IMPORTANT] > Client Side Filtering rules are generally processed in the following order: > 1. 'check_nosync' > 2. 'skip_dotfiles' > 3. 'skip_symlinks' > 4. 'skip_dir' > 5. 'skip_file' > 6. 'sync_list' > 7. 'skip_size' > > This can be best illustrated below: > > ![Client Side Filtering Processing Order](./puml/client_side_filtering_processing_order.png) > > For further details please review the [client architecture](client-architecture.md) documentation. > [!IMPORTANT] > After changing any Client Side Filtering rule, you must perform a full re-synchronisation by using `--resync`. ### Testing your configuration You can test your configuration by utilising the `--dry-run` CLI option. No files will be downloaded, uploaded, or removed; however, the application will display what 'would' have occurred. For example: ```text onedrive --sync --verbose --dry-run Reading configuration file: /home/user/.config/onedrive/config Configuration file successfully loaded Using 'user' Config Dir: /home/user/.config/onedrive DRY-RUN Configured. Output below shows what 'would' have occurred. DRY-RUN: Copying items.sqlite3 to items-dryrun.sqlite3 to use for dry run operations DRY RUN: Not creating backup config file as --dry-run has been used DRY RUN: Not updating hash files as --dry-run has been used Checking Application Version ... Attempting to initialise the OneDrive API ... Configuring Global Azure AD Endpoints The OneDrive API was initialised successfully Opening the item database ... Sync Engine Initialised with new Onedrive API instance Application version: vX.Y.Z-A-bcdefghi Account Type: Default Drive ID: Default Root ID: Remaining Free Space: 1058488129 KB All application operations will be performed in: /home/user/OneDrive Fetching items from the OneDrive API for Drive ID: .. ... Performing a database consistency and integrity check on locally stored data ... Processing DB entries for this Drive ID: Processing ~/OneDrive The directory has not changed ... Scanning local filesystem '~/OneDrive' for new data to upload ... ... Performing a final true-up scan of online data from Microsoft OneDrive Fetching items from the OneDrive API for Drive ID: .. Sync with Microsoft OneDrive is complete ``` ### Performing a sync with Microsoft OneDrive By default, all files are downloaded in `~/OneDrive`. This download location is controlled by the 'sync_dir' config option. After authorising the application, a sync of your data can be performed by running: ```text onedrive --sync ``` This will synchronise files from your Microsoft OneDrive account to your `~/OneDrive` local directory or to your specified 'sync_dir' location. > [!TIP] > If you prefer to use your local files as stored in `~/OneDrive` as your 'source of truth,' use the following sync command: > ```text > onedrive --sync --local-first > ``` ### Performing a single directory synchronisation with Microsoft OneDrive In some cases, it may be desirable to synchronise a single directory under ~/OneDrive without having to change your client configuration. To do this, use the following command: ```text onedrive --sync --single-directory '' ``` > [!TIP] > If the full path is `~/OneDrive/mydir`, the command would be `onedrive --sync --single-directory 'mydir'` ### Performing a 'one-way' download synchronisation with Microsoft OneDrive In some cases, it may be desirable to 'download only' from Microsoft OneDrive. To do this, use the following command: ```text onedrive --sync --download-only ``` This will download all the content from Microsoft OneDrive to your `~/OneDrive` location. Any files that are deleted online will remain locally and will not be removed. > [!IMPORTANT] > There is an application functionality change between v2.4.x and v.2.5x when using this option. > > In prior v2.4.x releases, online deletes were automatically processed, thus automatically deleting local files that were deleted online, however there was zero way to perform a `--download-only` operation to archive the online state. > > In v2.5.x and above, when using `--download-only` the default is that all files will remain locally as an archive of your online data rather than being deleted locally if deleted online. > [!TIP] > If you have the requirement to clean up local files that have been removed online, use the following command: > ```text > onedrive --sync --download-only --cleanup-local-files > ``` ### Performing a 'one-way' upload synchronisation with Microsoft OneDrive In certain scenarios, you might need to perform an 'upload only' operation to Microsoft OneDrive. This means that you'll be uploading data to OneDrive, but not synchronising any changes or additions made elsewhere. Use this command to initiate an upload-only synchronisation: ```text onedrive --sync --upload-only ``` > [!IMPORTANT] > - The 'upload only' mode operates independently of OneDrive's online content. It doesn't check or sync with what's already stored on OneDrive. It only uploads data from the local client. > - If a local file or folder that was previously synchronised with Microsoft OneDrive is now missing locally, it will be deleted from OneDrive during this operation. > [!TIP] > If you have the requirement to ensure that all data on Microsoft OneDrive remains intact (e.g., preventing deletion of items on OneDrive if they're deleted locally), use this command instead: > ```text > onedrive --sync --upload-only --no-remote-delete > ``` > [!IMPORTANT] > - `--upload-only`: This command will only upload local changes to OneDrive. These changes can include additions, modifications, moves, and deletions of files and folders. > - `--no-remote-delete`: Adding this command prevents the deletion of any items on OneDrive, even if they're deleted locally. This creates a one-way archive on OneDrive where files are only added and never removed. ### Performing a selective synchronisation via 'sync_list' file Selective synchronisation allows you to sync only specific files and directories. To enable selective synchronisation, create a file named `sync_list` in your application configuration directory (default is `~/.config/onedrive`). > [!IMPORTANT] > Important points to understand before using 'sync_list'. > * 'sync_list' excludes _everything_ by default on OneDrive. > * 'sync_list' follows an _"exclude overrides include"_ rule, and requires **explicit inclusion**. > * Order exclusions before inclusions, so that anything _specifically included_ is included. > * How and where you place your `/` matters for excludes and includes in subdirectories. Each line of the 'sync_list' file represents a relative path from your `sync_dir`. All files and directories not matching any line of the file will be skipped during all operations. Additionally, the use of `/` is critically important to determine how a rule is interpreted. It is very similar to `**` wildcards, for those that are familiar with globbing patterns. Here is an example of `sync_list`: ```text # sync_list supports comments # # The ordering of entries is highly recommended - exclusions before inclusions # # Exclude temp folder(s) or file(s) under Documents folder(s), anywhere in OneDrive !Documents/temp* # # Exclude secret data folder in root directory only !/Secret_data/* # # Include everything else in root directory # - Use 'sync_root_files' or --sync-root-files option # Do not use /* as this will include everything including items you are expecting to be excluded # # Include my Backup folder(s) or file(s) anywhere on OneDrive Backup # # Include my Backup folder in root /Backup/ # # Include Documents folder(s) anywhere in OneDrive Documents/ # # Include all PDF files in Documents folder(s), anywhere in OneDrive Documents/*.pdf # # Include this single document in Documents folder(s), anywhere in OneDrive Documents/latest_report.docx # # Include all Work/Project directories or files, inside 'Work' folder(s), anywhere in OneDrive Work/Project* # # Include the 'Blog' directory, but exclude 'Parent' and any other children of the parent # . # ├── Parent # │   ├── Blog # │   │   ├── random_files # │   │   │   ├── CZ9aZRM7U1j7pM21fH0MfP2gywlX7bqW # │   │   │   └── k4GptfTBE2z2meRFqjf54tnvSXcXe30Y # │   │   └── random_images # │   │   ├── cAuQMfX7qsMIOmzyQYdELikZwsXeCYsL # │   │   └── GqjZuo7UBB0qjYM2WUcZXOvToAhCQ29M # │   └── other_stuffs /Parent/Blog/* # # Include all "notes.txt" files, anywhere in OneDrive notes.txt # # Include /Blender in the ~OneDrive root but not if elsewhere in OneDrive /Blender # # Include these directories(or files) in 'Pictures' folder(s), that have a space in their name Pictures/Camera Roll Pictures/Saved Pictures # # Include these names if they match any file or folder Cinema Soc Codes Textbooks Year 2 ``` The following are supported for pattern matching and exclusion rules: * Use the `*` to wildcard select any characters to match for the item to be included * Use either `!` or `-` characters at the start of the line to exclude an otherwise included item > [!IMPORTANT] > After changing the sync_list, you must perform a full re-synchronisation by adding `--resync` to your existing command line - for example: `onedrive --sync --resync` > [!TIP] > When enabling the use of 'sync_list,' utilise the `--display-config` option to validate that your configuration will be used by the application, and test your configuration by adding `--dry-run` to ensure the client will operate as per your requirement. > [!TIP] > In some circumstances, it may be required to sync all the individual files within the 'sync_dir' root, but due to frequent name change / addition / deletion of these files, it is not desirable to constantly change the 'sync_list' file to include / exclude these files and force a resync. To assist with this, enable the following in your configuration file: > ```text > sync_root_files = "true" > ``` > This will tell the application to sync any file that it finds in your 'sync_dir' root by default, negating the need to constantly update your 'sync_list' file. ### Performing a --resync If you alter any of the subsequent configuration items, you will be required to execute a `--resync` to make sure your client is syncing your data with the updated configuration: * drive_id * sync_dir * skip_file * skip_dir * skip_dotfiles * skip_symlinks * sync_business_shared_items * Creating, Modifying or Deleting the 'sync_list' file Additionally, you might opt for a `--resync` if you think it's necessary to ensure your data remains in sync. If you're using this switch simply because you're unsure of the sync status, you can check the actual sync status using `--display-sync-status`. When you use `--resync`, you'll encounter the following warning and advice: ```text Using --resync will delete your local 'onedrive' client state, so there won't be a record of your current 'sync status.' This may potentially overwrite local versions of files with older versions downloaded from OneDrive, leading to local data loss. If in doubt, back up your local data before using --resync. Are you sure you want to proceed with --resync? [Y/N] ``` To proceed with `--resync`, you must type 'y' or 'Y' to allow the application to continue. > [!CAUTION] > It's highly recommended to use `--resync` only if the application prompts you to do so. Don't blindly set the application to start with `--resync` as your default option. > [!IMPORTANT] > In certain automated environments (assuming you know what you're doing due to automation), to avoid the 'proceed with acknowledgement' requirement, add `--resync-auth` to automatically acknowledge the prompt. ### Performing a --force-sync without a --resync or changing your configuration In some cases and situations, you may have configured the application to skip certain files and folders using 'skip_file' and 'skip_dir' configuration. You then may have a requirement to actually sync one of these items, but do not wish to modify your configuration, nor perform an entire `--resync` twice. The `--force-sync` option allows you to sync a specific directory, ignoring your 'skip_file' and 'skip_dir' configuration and negating the requirement to perform a `--resync`. To use this option, you must run the application manually in the following manner: ```text onedrive --sync --single-directory '' --force-sync ``` When using `--force-sync`, you'll encounter the following warning and advice: ```text WARNING: Overriding application configuration to use application defaults for skip_dir and skip_file due to --sync --single-directory --force-sync being used Using --force-sync will reconfigure the application to use defaults. This may have unknown future impacts. By proceeding with this option, you accept any impacts, including potential data loss resulting from using --force-sync. Are you sure you want to proceed with --force-sync [Y/N] ``` To proceed with `--force-sync`, you must type 'y' or 'Y' to allow the application to continue. ### Enabling the Client Activity Log When running onedrive, all actions can be logged to a separate log file. This can be enabled by using the `--enable-logging` flag or by adding `enable_logging = "true"` to your 'config' file. By default, log files will be written to `/var/log/onedrive/` and will be in the format of `%username%.onedrive.log`, where `%username%` represents the user who ran the client to allow easy sorting of user to client activity log. > [!NOTE] > You will need to ensure the existence of this directory and that your user has the applicable permissions to write to this directory; otherwise, the following error message will be printed: > ```text > ERROR: Unable to access /var/log/onedrive > ERROR: Please manually create '/var/log/onedrive' and set appropriate permissions to allow write access > ERROR: The requested client activity log will instead be located in your user's home directory > ``` On many systems, ensuring that the log directory exists can be achieved by performing the following: ```text sudo mkdir /var/log/onedrive sudo chown root:users /var/log/onedrive sudo chmod 0775 /var/log/onedrive ``` Additionally, you need to ensure that your user account is part of the 'users' group: ``` cat /etc/group | grep users ``` If your user is not part of this group, then you need to add your user to this group: ``` sudo usermod -a -G users ``` If you need to make a group modification, you will need to 'logout' of all sessions / SSH sessions to log in again to have the new group access applied. If the client is unable to write the client activity log, the following error message will be printed: ```text ERROR: Unable to write the activity log to /var/log/onedrive/%username%.onedrive.log ERROR: Please set appropriate permissions to allow write access to the logging directory for your user account ERROR: The requested client activity log will instead be located in your user's home directory ``` If you receive this error message, you will need to diagnose why your system cannot write to the specified file location. #### Client Activity Log Example: An example of a client activity log for the command `onedrive --sync --enable-logging` is below: ```text 2023-Sep-27 08:16:00.1128806 Configuring Global Azure AD Endpoints 2023-Sep-27 08:16:00.1160620 Sync Engine Initialised with new Onedrive API instance 2023-Sep-27 08:16:00.5227122 All application operations will be performed in: /home/user/OneDrive 2023-Sep-27 08:16:00.5227977 Fetching items from the OneDrive API for Drive ID: 2023-Sep-27 08:16:00.7780979 Processing changes and items received from Microsoft OneDrive ... 2023-Sep-27 08:16:00.7781548 Performing a database consistency and integrity check on locally stored data ... 2023-Sep-27 08:16:00.7785889 Scanning the local file system '~/OneDrive' for new data to upload ... 2023-Sep-27 08:16:00.7813710 Performing a final true-up scan of online data from Microsoft OneDrive 2023-Sep-27 08:16:00.7814668 Fetching items from the OneDrive API for Drive ID: 2023-Sep-27 08:16:01.0141776 Processing changes and items received from Microsoft OneDrive ... 2023-Sep-27 08:16:01.0142454 Sync with Microsoft OneDrive is complete ``` An example of a client activity log for the command `onedrive --sync --verbose --enable-logging` is below: ```text 2023-Sep-27 08:20:05.4600464 Checking Application Version ... 2023-Sep-27 08:20:05.5235017 Attempting to initialise the OneDrive API ... 2023-Sep-27 08:20:05.5237207 Configuring Global Azure AD Endpoints 2023-Sep-27 08:20:05.5238087 The OneDrive API was initialised successfully 2023-Sep-27 08:20:05.5238536 Opening the item database ... 2023-Sep-27 08:20:05.5270612 Sync Engine Initialised with new Onedrive API instance 2023-Sep-27 08:20:05.9226535 Application version: vX.Y.Z-A-bcdefghi 2023-Sep-27 08:20:05.9227079 Account Type: 2023-Sep-27 08:20:05.9227360 Default Drive ID: 2023-Sep-27 08:20:05.9227550 Default Root ID: 2023-Sep-27 08:20:05.9227862 Remaining Free Space: 2023-Sep-27 08:20:05.9228296 All application operations will be performed in: /home/user/OneDrive 2023-Sep-27 08:20:05.9228989 Fetching items from the OneDrive API for Drive ID: 2023-Sep-27 08:20:06.2076569 Performing a database consistency and integrity check on locally stored data ... 2023-Sep-27 08:20:06.2077121 Processing DB entries for this Drive ID: 2023-Sep-27 08:20:06.2078408 Processing ~/OneDrive 2023-Sep-27 08:20:06.2078739 The directory has not changed 2023-Sep-27 08:20:06.2079783 Processing Attachments 2023-Sep-27 08:20:06.2080071 The directory has not changed 2023-Sep-27 08:20:06.2081585 Processing Attachments/file.docx 2023-Sep-27 08:20:06.2082079 The file has not changed 2023-Sep-27 08:20:06.2082760 Processing Documents 2023-Sep-27 08:20:06.2083225 The directory has not changed 2023-Sep-27 08:20:06.2084284 Processing Documents/file.log 2023-Sep-27 08:20:06.2084886 The file has not changed 2023-Sep-27 08:20:06.2085150 Scanning the local file system '~/OneDrive' for new data to upload ... 2023-Sep-27 08:20:06.2087133 Skipping item - excluded by sync_list config: ./random_25k_files 2023-Sep-27 08:20:06.2116235 Performing a final true-up scan of online data from Microsoft OneDrive 2023-Sep-27 08:20:06.2117190 Fetching items from the OneDrive API for Drive ID: 2023-Sep-27 08:20:06.5049743 Sync with Microsoft OneDrive is complete ``` #### Client Activity Log Differences Despite application logging being enabled as early as possible, the following log entries will be missing from the client activity log when compared to console output: **No user configuration file:** ```text No user or system config file found, using application defaults Using 'user' configuration path for application state data: /home/user/.config/onedrive Using the following path to store the runtime application log: /var/log/onedrive ``` **User configuration file:** ```text Reading configuration file: /home/user/.config/onedrive/config Configuration file successfully loaded Using 'user' configuration path for application state data: /home/user/.config/onedrive Using the following path to store the runtime application log: /var/log/onedrive ``` ### GUI Notifications To enable GUI notifications, you must compile the application with GUI Notification Support. Refer to [GUI Notification Support](install.md#gui-notification-support) for details. Once compiled, GUI notifications will work by default in the display manager session under the following conditions: * A D-Bus message bus daemon must be running. * The environment variables XDG_RUNTIME_DIR and DBUS_SESSION_BUS_ADDRESS must be set. Without these conditions met, GUI notifications will not function even if the support is compiled in. Once these conditions have been met, the following application events will trigger a GUI notification within the display manager session by default: * Aborting a sync if .nosync file is found * Skipping a particular item due to an invalid name * Skipping a particular item due to an invalid symbolic link * Skipping a particular item due to an invalid UTF sequence * Skipping a particular item due to an invalid character encoding sequence * Cannot create remote directory * Cannot upload file changes (free space issue, breaches maximum allowed size, breaches maximum OneDrive Account path length) * Cannot delete remote file / folder * Cannot move remote file / folder * When a re-authentication is required * When a new client version is available * Files that fail to upload * Files that fail to download Additionally, GUI notifications can also be sent for the following activities: * Successful file download * Successful file upload * Successful deletion locally (files and folders) * Successful deletion online (files and folders) To enable these specific notifications, add the following to your 'config' file: ``` notify_file_actions = "true" ``` ### Handling a Microsoft OneDrive Account Password Change If you change your Microsoft OneDrive Account Password, the client will no longer be authorised to sync, and will generate the following error upon next application run: ```text AADSTS50173: The provided grant has expired due to it being revoked, a fresh auth token is needed. The user might have changed or reset their password. The grant was issued on '' and the TokensValidFrom date (before which tokens are not valid) for this user is ''. ERROR: You will need to issue a --reauth and re-authorise this client to obtain a fresh auth token. ``` To re-authorise the client, follow the steps below: 1. If running the client as a system service (init.d or systemd), stop the applicable system service 2. Run the command `onedrive --reauth`. This will clean up the previous authorisation, and will prompt you to re-authorise the client as per initial configuration. Please note, if you are using `--confdir` as part of your application runtime configuration, you must include this when telling the client to re-authenticate. 3. Restart the client if running as a system service or perform the standalone sync operation again The application will now sync with OneDrive with the new credentials. ### Determining the synchronisation result When the client has finished syncing without errors, the following will be displayed: ``` Sync with Microsoft OneDrive is complete ``` If any items failed to sync, the following will be displayed: ``` Sync with Microsoft OneDrive has completed, however there are items that failed to sync. ``` A file list of failed upload or download items will also be listed to allow you to determine your next steps. In order to fix the upload or download failures, you may need to: * Review the application output to determine what happened * Re-try your command utilising a resync to ensure your system is correctly synced with your Microsoft OneDrive Account ## Frequently Asked Configuration Questions ### How to change the default configuration of the client? Configuration is determined by three layers, and applied in the following order: * Application default values * Values that are set in the configuration file * Values that are passed in via the command line at application runtime. These values will override any configuration file set value. The default application values provide a reasonable operational default, and additional configuration is entirely optional. If you want to change the application defaults, you can download a copy of the config file into your application configuration directory. Valid default directories for the config file are: * `~/.config/onedrive` * `/etc/onedrive` > [!TIP] > To download a copy of the config file, use the following: > ```text > mkdir -p ~/.config/onedrive > wget https://raw.githubusercontent.com/abraunegg/onedrive/master/config -O ~/.config/onedrive/config > ``` For full configuration options and CLI switches, please refer to [application-config-options.md](application-config-options.md) ### How to change where my data from Microsoft OneDrive is stored? By default, the location where your Microsoft OneDrive data is stored, is within your Home Directory under a directory called 'OneDrive'. This replicates as close as possible where the Microsoft Windows OneDrive client stores data. To change this location, the application configuration option 'sync_dir' is used to specify a new local directory where your Microsoft OneDrive data should be stored. > [!IMPORTANT] > Please be aware that if you designate a network mount point (such as NFS, Windows Network Share, or Samba Network Share) as your `sync_dir`, this setup inherently lacks 'inotify' support. Support for 'inotify' is essential for real-time tracking of local file changes, which means that the client's 'Monitor Mode' cannot immediately detect changes in files located on these network shares. Instead, synchronisation between your local filesystem and Microsoft OneDrive will occur at intervals specified by the `monitor_interval` setting. This limitation regarding 'inotify' support on network mount points like NFS or Samba is beyond the control of this client. ### How to change what file and directory permissions are assigned to data that is downloaded from Microsoft OneDrive? The following are the application default permissions for any new directory or file that is created locally when downloaded from Microsoft OneDrive: * Directories: 700 - This provides the following permissions: `drwx------` * Files: 600 - This provides the following permissions: `-rw-------` These default permissions align to the security principal of 'least privilege' so that only you should have access to your data that you download from Microsoft OneDrive. To alter these default permissions, you can adjust the values of two configuration options as follows. You can also use the [Unix Permissions Calculator](https://chmod-calculator.com/) to help you determine the necessary new permissions. ```text sync_dir_permissions = "700" sync_file_permissions = "600" ``` > [!IMPORTANT] > Please note that special permission bits such as setuid, setgid, and the sticky bit are not supported. Valid permission values range from `000` to `777` only. > [!NOTE] > To prevent the application from modifying file or directory permissions and instead rely on the existing file system permission inheritance, add `disable_permission_set = "true"` to your configuration file. ### How are uploads and downloads managed? The system manages downloads and uploads using a multi-threaded approach. Specifically, the application utilises by default 8 threads (a maximum of 16 can be configured) for these processes. This thread count is preset and cannot be modified by users. This design ensures efficient handling of data transfers. ### How to only sync a specific directory? There are two methods to achieve this: * Employ the '--single-directory' option to only sync this specific path * Employ 'sync_list' as part of your 'config' file to configure what files and directories to sync, and what should be excluded ### How to 'skip' files from syncing? There are two methods to achieve this: * Employ 'skip_file' as part of your 'config' file to configure what files to skip * Employ 'sync_list' to configure what files and directories to sync, and what should be excluded ### How to 'skip' directories from syncing? There are three methods available to 'skip' a directory from the sync process: * Employ 'skip_dir' as part of your 'config' file to configure what directories to skip * Employ 'sync_list' to configure what files and directories to sync, and what should be excluded * Employ 'check_nosync' as part of your 'config' file and a '.nosync' empty file within the directory to exclude to skip that directory ### How to 'skip' .files and .folders from syncing? There are three methods to achieve this: * Employ 'skip_file' or 'skip_dir' to configure what files or folders to skip * Employ 'sync_list' to configure what files and directories to sync, and what should be excluded * Employ 'skip_dotfiles' as part of your 'config' file to skip any dot file (for example: `.Trash-1000` or `.xdg-volume-info`) from syncing to OneDrive ### How to 'skip' files larger than a certain size from syncing? Use `skip_size = "value"` as part of your 'config' file where files larger than this size (in MB) will be skipped. ### How to 'rate limit' the application to control bandwidth consumed for upload & download operations? To optimise Internet bandwidth usage during upload and download processes, include the 'rate_limit' setting in your configuration file. This setting controls the bandwidth allocated to each thread. By default, 'rate_limit' is set to '0', indicating that the application will utilise the maximum available bandwidth across all threads. To check the current 'rate_limit' value, use the `--display-config` command. > [!NOTE] > Since downloads and uploads are processed through multiple threads, the 'rate_limit' value applies to each thread separately. For instance, setting 'rate_limit' to 1048576 (1MB) means that during data transfers, the total bandwidth consumption might reach around 16MB, not just the 1MB configured due to the number of threads being used. ### How can I prevent my local disk from filling up? By default, the application will reserve 50MB of disk space to prevent your filesystem from running out of disk space. This default value can be modified by adding the 'space_reservation' configuration option and the applicable value as part of your 'config' file. You can review the value being used when using `--display-config`. ### How does the client handle symbolic links? Microsoft OneDrive has no concept or understanding of symbolic links, and attempting to upload a symbolic link to Microsoft OneDrive generates a platform API error. All data (files and folders) that are uploaded to OneDrive must be whole files or actual directories. As such, there are only two methods to support symbolic links with this client: 1. Follow the Linux symbolic link and upload whatever the local symbolic link is pointing to to Microsoft OneDrive. This is the default behaviour. 2. Skip symbolic links by configuring the application to do so. When skipping, no data, no link, no reference is uploaded to OneDrive. Use 'skip_symlinks' as part of your 'config' file to configure the skipping of all symbolic links while syncing. ### How to synchronise OneDrive Personal Shared Folders? Folders shared with you can be synchronised by adding them to your OneDrive online. To do that, open your OneDrive account online, go to the Shared files list, right-click on the folder you want to synchronise, and then click on "Add to my OneDrive". ### How to synchronise OneDrive Business Shared Items (Files and Folders)? Folders shared with you can be synchronised by adding them to your OneDrive online. To do that, open your OneDrive account online, go to the Shared files list, right-click on the folder you want to synchronise, and then click on "Add to my OneDrive". Files shared with you can be synchronised using two methods: 1. Add a shortcut link to the file to your OneDrive folder online 2. Sync the actual file locally using the configuration option to sync OneDrive Business Shared Files. Refer to [business-shared-items.md](business-shared-items.md) for further details. ### How to synchronise SharePoint / Office 365 Shared Libraries? There are two methods to achieve this: * SharePoint library can be directly added to your OneDrive online. To do that, open your OneDrive account online, go to the Shared files list, right-click on the SharePoint Library you want to synchronise, and then click on "Add to my OneDrive". * Configure a separate application instance to only synchronise that specific SharePoint Library. Refer to [sharepoint-libraries.md](sharepoint-libraries.md) for configuration assistance. ### How to Create a Shareable Link? In certain situations, you might want to generate a shareable file link and provide this link to other users for accessing a specific file. To accomplish this, employ the following command: ```text onedrive --create-share-link ``` > [!IMPORTANT] > By default, this access permissions for the file link will be read-only. To make the shareable link a read-write link, execute the following command: ```text onedrive --create-share-link --with-editing-perms ``` > [!IMPORTANT] > The order of the file path and option flag is crucial. ### How to Synchronise Both Personal and Business Accounts at once? You need to set up separate instances of the application configuration for each account. Refer to [advanced-usage.md](advanced-usage.md) for guidance on configuration. ### How to Synchronise Multiple SharePoint Libraries simultaneously? For each SharePoint Library, configure a separate instance of the application configuration. Refer to [advanced-usage.md](advanced-usage.md) for configuration instructions. ### How to Receive Real-time Changes from Microsoft OneDrive Service, instead of waiting for the next sync period? Refer to [webhooks.md](webhooks.md) for configuration instructions. ### How to initiate the client as a background service? There are a few ways to employ onedrive as a service: * via init.d * via systemd * via runit #### OneDrive service running as root user via init.d ```text chkconfig onedrive on service onedrive start ``` To view the logs, execute: ```text tail -f /var/log/onedrive/.onedrive.log ``` To alter the 'user' under which the client operates (typically root by default), manually modify the init.d service file and adjust `daemon --user root onedrive_service.sh` to match the correct user. #### OneDrive service running as root user via systemd (Arch, Ubuntu, Debian, OpenSuSE, Fedora) Initially, switch to the root user with `su - root`, then activate the systemd service: ```text systemctl --user enable onedrive systemctl --user start onedrive ``` > [!IMPORTANT] > This will execute the 'onedrive' process with a UID/GID of '0', which means any files or folders created will be owned by 'root'. > [!IMPORTANT] > The `systemctl --user` command is not applicable to Red Hat Enterprise Linux (RHEL) or CentOS Linux platforms - see below. To monitor the service's status, use the following: ```text systemctl --user status onedrive.service ``` To observe the systemd application logs, use: ```text journalctl --user-unit=onedrive -f ``` > [!TIP] > For systemd to function correctly, it requires the presence of XDG environment variables. If you encounter the following error while enabling the systemd service: > ```text > Failed to connect to bus: No such file or directory > ``` > The most likely cause is missing XDG environment variables. To resolve this, add the following lines to `.bashrc` or another file executed upon user login: > ```text > export XDG_RUNTIME_DIR="/run/user/$UID" > export DBUS_SESSION_BUS_ADDRESS="unix:path=${XDG_RUNTIME_DIR}/bus" > ``` > > To apply this change, you must log out of all user accounts where it has been made. > [!IMPORTANT] > On certain systems (e.g., Raspbian / Ubuntu / Debian on Raspberry Pi), the XDG fix above may not persist after system reboots. An alternative to starting the client via systemd as root is as follows: > 1. Create a symbolic link from `/home/root/.config/onedrive` to `/root/.config/onedrive/`. > 2. Establish a systemd service using the '@' service file: `systemctl enable onedrive@root.service`. > 3. Start the root@service: `systemctl start onedrive@root.service`. > > This ensures that the service correctly restarts upon system reboot. To examine the systemd application logs, run: ```text journalctl --unit=onedrive@ -f ``` #### OneDrive service running as root user via systemd (Red Hat Enterprise Linux, CentOS Linux) ```text systemctl enable onedrive systemctl start onedrive ``` > [!IMPORTANT] > This will execute the 'onedrive' process with a UID/GID of '0', meaning any files or folders created will be owned by 'root'. To view the systemd application logs, execute: ```text journalctl --unit=onedrive -f ``` #### OneDrive service running as a non-root user via systemd (All Linux Distributions) In some instances, it is preferable to run the OneDrive client as a service without the 'root' user. Follow the instructions below to configure the service for your regular user login. 1. As the user who will run the service, launch the application in standalone mode, authorize it for use, and verify that synchronization is functioning as expected: ```text onedrive --sync --verbose ``` 2. After validating the application for your user, switch to the 'root' user, where is your username from step 1 above. ```text systemctl enable onedrive@.service systemctl start onedrive@.service ``` 3. To check the service's status for the user, use the following: ```text systemctl status onedrive@.service ``` To observe the systemd application logs, use: ```text journalctl --unit=onedrive@ -f ``` #### OneDrive service running as a non-root user via systemd (with notifications enabled) (Arch, Ubuntu, Debian, OpenSuSE, Fedora) In some scenarios, you may want to receive GUI notifications when using the client as a non-root user. In this case, follow these steps: 1. Log in via the graphical UI as the user you want to enable the service for. 2. Disable any `onedrive@` service files for your username, e.g.: ```text sudo systemctl stop onedrive@alex.service sudo systemctl disable onedrive@alex.service ``` 3. Enable the service as follows: ```text systemctl --user enable onedrive systemctl --user start onedrive ``` To check the service's status for the user, use the following: ```text systemctl --user status onedrive.service ``` To view the systemd application logs, execute: ```text journalctl --user-unit=onedrive -f ``` > [!IMPORTANT] > The `systemctl --user` command is not applicable to Red Hat Enterprise Linux (RHEL) or CentOS Linux platforms. #### OneDrive service running as a non-root user via runit (antiX, Devuan, Artix, Void) 1. Create the following folder if it doesn't already exist: `/etc/sv/runsvdir-` - where `` is the `USER` targeted for the service - e.g., `# mkdir /etc/sv/runsvdir-nolan` 2. Create a file called `run` under the previously created folder with executable permissions - `# touch /etc/sv/runsvdir-/run` - `# chmod 0755 /etc/sv/runsvdir-/run` 3. Edit the `run` file with the following contents (permissions needed): ```sh #!/bin/sh export USER="" export HOME="/home/" groups="$(id -Gn "${USER}" | tr ' ' ':')" svdir="${HOME}/service" exec chpst -u "${USER}:${groups}" runsvdir "${svdir}" ``` - Ensure you replace `` with the `USER` set in step #1. 4. Enable the previously created folder as a service - `# ln -fs /etc/sv/runsvdir- /var/service/` 5. Create a subfolder in the `USER`'s `HOME` directory to store the services (or symlinks) - `$ mkdir ~/service` 6. Create a subfolder specifically for OneDrive - `$ mkdir ~/service/onedrive/` 7. Create a file called `run` under the previously created folder with executable permissions - `$ touch ~/service/onedrive/run` - `$ chmod 0755 ~/service/onedrive/run` 8. Append the following contents to the `run` file ```sh #!/usr/bin/env sh exec /usr/bin/onedrive --monitor ``` - In some scenarios, the path to the `onedrive` binary may vary. You can obtain it by running `$ command -v onedrive`. 9. Reboot to apply the changes 10. Check the status of user-defined services - `$ sv status ~/service/*` > [!NOTE] > For additional details, you can refer to Void's documentation on [Per-User Services](https://docs.voidlinux.org/config/services/user-services.html) ### How to start a user systemd service at boot without user login? In some situations, it may be necessary for the systemd service to start without requiring your 'user' to log in. To address this issue, you need to reconfigure your 'user' account so that the systemd services you've created launch without the need for you to log in to your system: ```text loginctl enable-linger ``` ### How to access Microsoft OneDrive service through a proxy If you have a requirement to run the client through a proxy, there are a couple of ways to achieve this: #### Option 1: Use '.bashrc' to specify the proxy server details Set proxy configuration in `~/.bashrc` to allow the 'onedrive' application to use a specific proxy server: ```text # Set the HTTP proxy export http_proxy="http://your.proxy.server:port" # Set the HTTPS proxy export https_proxy="http://your.proxy.server:port" ``` Once you've edited your `~/.bashrc` file, run the following command to apply the changes: ``` source ~/.bashrc ``` #### Option 2: Update the 'systemd' service file to include the proxy server details If running as a systemd service, edit the applicable systemd service file to include the proxy configuration information: ```text [Unit] Description=OneDrive Client for Linux Documentation=https://github.com/abraunegg/onedrive After=network-online.target Wants=network-online.target [Service] ........ Environment="HTTP_PROXY=http://your.proxy.server:port" Environment="HTTPS_PROXY=http://your.proxy.server:port" ExecStart=/usr/local/bin/onedrive --monitor ........ ``` > [!NOTE] > After modifying the service files, you will need to run `sudo systemctl daemon-reload` to ensure the service file changes are picked up. A restart of the OneDrive service will also be required to pick up the change to send the traffic via the proxy server ### How to set up SELinux for a sync folder outside of the home folder If SELinux is enforced and the sync folder is outside of the home folder, as long as there is no policy for cloud file service providers, label the file system folder to `user_home_t`. ```text sudo semanage fcontext -a -t user_home_t /path/to/onedriveSyncFolder sudo restorecon -R -v /path/to/onedriveSyncFolder ``` To remove this change from SELinux and restore the default behaviour: ```text sudo semanage fcontext -d /path/to/onedriveSyncFolder sudo restorecon -R -v /path/to/onedriveSyncFolder ``` ## Advanced Configuration of the OneDrive Client for Linux Refer to [advanced-usage.md](advanced-usage.md) for further details on the following topics: * Configuring the client to use multiple OneDrive accounts / configurations * Configuring the client to use multiple OneDrive accounts / configurations using Docker * Configuring the client for use in dual-boot (Windows / Linux) situations * Configuring the client for use when 'sync_dir' is a mounted directory * Upload data from the local ~/OneDrive folder to a specific location on OneDrive ## Overview of all OneDrive Client for Linux CLI Options Below is a comprehensive list of all available configuration options for the OneDrive Client for Linux, as shown by the output of `onedrive --help`. These commands provide a range of options for synchronising, monitoring, and managing files between your local system and Microsoft's OneDrive cloud service. The following configuration options are available: ```text onedrive - A client for the Microsoft OneDrive Cloud Service Usage: onedrive [options] --sync Do a one time synchronization onedrive [options] --monitor Monitor filesystem and sync regularly onedrive [options] --display-config Display the currently used configuration onedrive [options] --display-sync-status Query OneDrive service and report on pending changes onedrive -h | --help Show this help screen onedrive --version Show version Options: --auth-files ARG Perform authentication not via interactive dialog but via files read/writes to these files. --auth-response ARG Perform authentication not via interactive dialog but via providing the response url directly. --check-for-nomount Check for the presence of .nosync in the syncdir root. If found, do not perform sync. --check-for-nosync Check for the presence of .nosync in each directory. If found, skip directory from sync. --classify-as-big-delete ARG Number of children in a path that is locally removed which will be classified as a 'big data delete' --cleanup-local-files Cleanup additional local files when using --download-only. This will remove local data. --confdir ARG Set the directory used to store the configuration files --create-directory ARG Create a directory on OneDrive - no sync will be performed. --create-share-link ARG Create a shareable link for an existing file on OneDrive --debug-https Debug OneDrive HTTPS communication. --destination-directory ARG Destination directory for renamed or move on OneDrive - no sync will be performed. --disable-download-validation Disable download validation when downloading from OneDrive --disable-notifications Do not use desktop notifications in monitor mode. --disable-upload-validation Disable upload validation when uploading to OneDrive --display-config Display what options the client will use as currently configured - no sync will be performed. --display-quota Display the quota status of the client - no sync will be performed. --display-running-config Display what options the client has been configured to use on application startup. --display-sync-status Display the sync status of the client - no sync will be performed. --download-only Replicate the OneDrive online state locally, by only downloading changes from OneDrive. Do not upload local changes to OneDrive. --dry-run Perform a trial sync with no changes made --enable-logging Enable client activity to a separate log file --force Force the deletion of data when a 'big delete' is detected --force-http-11 Force the use of HTTP 1.1 for all operations --force-sync Force a synchronization of a specific folder, only when using --sync --single-directory and ignore all non-default skip_dir and skip_file rules --get-O365-drive-id ARG Query and return the Office 365 Drive ID for a given Office 365 SharePoint Shared Library (DEPRECATED) --get-file-link ARG Display the file link of a synced file --get-sharepoint-drive-id ARG Query and return the Office 365 Drive ID for a given Office 365 SharePoint Shared Library --help -h This help information. --list-shared-items List OneDrive Business Shared Items --local-first Synchronize from the local directory source first, before downloading changes from OneDrive. --log-dir ARG Directory where logging output is saved to, needs to end with a slash. --logout Logout the current user --modified-by ARG Display the last modified by details of a given path --monitor -m Keep monitoring for local and remote changes --monitor-fullscan-frequency ARG Number of sync runs before performing a full local scan of the synced directory --monitor-interval ARG Number of seconds by which each sync operation is undertaken when idle under monitor mode. --monitor-log-frequency ARG Frequency of logging in monitor mode --no-remote-delete Do not delete local file 'deletes' from OneDrive when using --upload-only --print-access-token Print the access token, useful for debugging --reauth Reauthenticate the client with OneDrive --remove-directory ARG Remove a directory on OneDrive - no sync will be performed. --remove-source-files Remove source file after successful transfer to OneDrive when using --upload-only --resync Forget the last saved state, perform a full sync --resync-auth Approve the use of performing a --resync action --single-directory ARG Specify a single local directory within the OneDrive root to sync. --skip-dir ARG Skip any directories that match this pattern from syncing --skip-dir-strict-match When matching skip_dir directories, only match explicit matches --skip-dot-files Skip dot files and folders from syncing --skip-file ARG Skip any files that match this pattern from syncing --skip-size ARG Skip new files larger than this size (in MB) --skip-symlinks Skip syncing of symlinks --source-directory ARG Source directory to rename or move on OneDrive - no sync will be performed. --space-reservation ARG The amount of disk space to reserve (in MB) to avoid 100% disk space utilisation --sync -s Perform a synchronisation with Microsoft OneDrive --sync-root-files Sync all files in sync_dir root when using sync_list. --sync-shared-files Sync OneDrive Business Shared Files to the local filesystem --syncdir ARG Specify the local directory used for synchronisation to OneDrive --synchronize Perform a synchronisation with Microsoft OneDrive (DEPRECATED) --upload-only Replicate the locally configured sync_dir state to OneDrive, by only uploading local changes to OneDrive. Do not download changes from OneDrive. --verbose -v+ Print more details, useful for debugging (repeat for extra debugging) --version Print the version and exit --with-editing-perms Create a read-write shareable link for an existing file on OneDrive when used with --create-share-link ``` Refer to [application-config-options.md](application-config-options.md) for in-depth details on all application options. onedrive-2.5.5/docs/webhooks.md000066400000000000000000000410241476564400300164430ustar00rootroot00000000000000# How to configure receiving real-time changes from Microsoft OneDrive using webhooks When operating in 'Monitor Mode,' receiving real-time updates to online data can significantly enhance synchronisation efficiency. This is achieved by enabling 'webhooks,' which allows the client to subscribe to remote updates and receive real-time notifications when certain events occur on Microsoft OneDrive. With this setup, any remote changes are promptly synchronised to your local file system, eliminating the need to wait for the next scheduled synchronisation cycle. > [!IMPORTANT] > In March 2023, Microsoft updated the webhook notification capability in Microsoft Graph to only allow valid HTTPS URLs as the destination for subscription updates. > > This change was part of Microsoft's ongoing efforts to enhance security and ensure that all webhooks used with Microsoft Graph comply with modern security standards. The enforcement of this requirement prevents the registration of subscriptions with non-secure (HTTP) endpoints, thereby improving the security of data transmission. > > Therefore, as a prerequisite, you must have a valid fully qualified domain name (FQDN) for your system that is externally resolvable, or configure Dynamic DNS (DDNS) using a provider such as: > * No-IP > * DynDNS > * DuckDNS > * Afraid.org > * Cloudflare > * Google Domains > * Dynu > * ChangeIP > > This FQDN will allow you to create a valid HTTPS certificate for your system, which can be used by Microsoft Graph for webhook functionality. > > Please note that it is beyond the scope of this document to provide guidance on setting up this requirement. Depending on your environment, a number of steps are required to configure this application functionality. At a very high level these configuration steps are: 1. Application configuration to enable 'webhooks' functionality 2. Install and configure 'nginx' as a reverse proxy for HTTPS traffic 3. Install and configure Let's Encrypt 'certbot' to provide a valid HTTPS certificate for your system using your FQDN 4. Configure your Firewall or Router to forward traffic to your system > [!NOTE] > The configuration steps below were validated on [Fedora 40 Workstation](https://fedoraproject.org/) > > The installation of required components (nginx, certbot) for your platform is beyond the scope of this document and it is assumed you know how to install these components. If you are unsure, please seek support from your Linux distribution support channels. ### Step 1: Application configuration #### Enable the 'webhook' application feature * In your 'config' file, set `webhook_enabled = "true"` to activate the webhook feature. #### Configure the public notification URL * In your 'config' file, set `webhook_public_url = "https:///webhooks/onedrive"` as the public URL that will receive subscription updates from the Microsoft Graph API platform. > [!NOTE] > This URL will utilise your FQDN and must be resolvable from the Internet. This FQDN will also be used within your 'nginx' configuration. #### Testing At this point, if you attempt to test 'webhooks', when they are attempted to be initialised, the following error *should* be generated: ``` ERROR: Microsoft OneDrive API returned an error with the following message: Error Message: HTTP request returned status code 400 (Bad Request) Error Reason: Subscription validation request timed out. Error Code: ValidationError Error Timestamp: YYYY-MM-DDThh:mm:ss API Request ID: eb196382-51d7-4411-984a-45a3fda90463 Will retry creating or renewing subscription in 1 minute ``` This error is 100% normal at this point. ### Step 2: Install and configure 'nginx' > [!NOTE] > Nginx is a web server that can also be used as a reverse proxy, load balancer, mail proxy and HTTP cache. #### Install and enable 'nginx' * Install 'nginx' and any other requirements to install 'nginx' on your platform. It is beyond the scope of this document to advise on how to install this. Enable and start the 'nginx' service. > [!TIP] > You may need to enable firewall rules to allow inbound http and https connections on your system: > ``` > sudo firewall-cmd --permanent --add-service=http > sudo firewall-cmd --permanent --add-service=https > sudo firewall-cmd --reload > ``` #### Verify your 'nginx' installation * From your local machine, attempt to access the local server now running, by using a web browser and pointing at http://127.0.0.1/ ![nginx_verify_install](./images/nginx_verify_install.png) #### Configure 'nginx' to receive the subscription update * Create a basic 'nginx' configuration file to support proxying traffic from Nginx to the local 'onedrive' process, which will, by default, have an HTTP listener running on TCP port 8888 ``` server { listen 80; server_name ; location /webhooks/onedrive { # Proxy Options proxy_http_version 1.1; proxy_pass http://127.0.0.1:8888; } } ``` The configuration above will: * Create an endpoint listener at `https:///webhooks/onedrive` * Proxy the received traffic at this listener to the local listener TCP port > [!TIP] > Save this file in the nginx configuration directory similar to the following path: `/etc/nginx/conf.d/onedrive_webhook.conf`. This will help keep all your configurations organised. * Test your 'nginx' configuration using `sudo nginx -t` to validate that there are no errors. If any are identified, please correct them. * Once tested, reload your 'nginx' configuration to activate the webhook reverse proxy configuration. ### Step 4: Initial Firewall/Router Configuration * Configure your firewall or router to forward all incoming HTTP and HTTPS traffic to the internal address of your system where 'nginx' is running. This is required for to allow the Let's Encrypt `certbot` tool to create a valid HTTPS certificate for your system. ![initial_firewall_config](./images/initial_firewall_config.png) * A valid configuration will be similar to the above illustration. ### Step 5: Use Let's Encrypt 'certbot' to create a SSL Certificate and deploy to your 'nginx' webhook configuration * Install the Let's Encrypt 'certbot' tool along with the associated python module 'python-certbot-nginx' for your platform * Run the 'certbot' tool on your platform to generate a valid HTTPS certificate for your `` by running `certbot --nginx`. This should *detect* your active `server_name` from your 'nginx' configuration and install the certificate in the correct manner. * The resulting 'nginx' configuration will look something like this: ``` server { server_name ; location /webhooks/onedrive { # Proxy Options proxy_http_version 1.1; proxy_pass http://127.0.0.1:8888; } listen 443 ssl; # managed by Certbot ssl_certificate /etc/letsencrypt/live//fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live//privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot } server { if ($host = ) { return 301 https://$host$request_uri; } # managed by Certbot listen 80; server_name ; return 404; # managed by Certbot } ``` * Test your 'nginx' configuration using `sudo nginx -t` to validate that there are no errors. If any are identified, please correct them. * Once tested, reload your 'nginx' configuration to activate the webhook reverse proxy configuration. > [!IMPORTANT] > It is strongly advised that post doing this step, you implement a method to automatically keep your SSL certificate in a healthy state, as if the SSL certificate expires, webhook functionality will stop working. It is also beyond the scope of this document on how to do this. ### Step 6: Update 'nginx' to only use TLS 1.2 and TLS 1.3 To ensure that you are configuring your 'nginx' configuration to use secure communication, it is advisable for you to add the following to your `onedrive_webhook.conf` within the `server {}` configuration section: ``` # Ensure only TLS 1.2 and TLS 1.3 are used ssl_protocols TLSv1.2 TLSv1.3; ``` The resulting 'nginx' configuration will look something like this: ``` server { server_name ; location /webhooks/onedrive { # Proxy Options proxy_http_version 1.1; proxy_pass http://127.0.0.1:8888; } listen 443 ssl; # managed by Certbot ssl_certificate /etc/letsencrypt/live//fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live//privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot # Ensure only TLS 1.2 and TLS 1.3 are used ssl_protocols TLSv1.2 TLSv1.3; } server { if ($host = ) { return 301 https://$host$request_uri; } # managed by Certbot listen 80; server_name ; return 404; # managed by Certbot } ``` * Test your 'nginx' configuration using `sudo nginx -t` to validate that there are no errors. If any are identified, please correct them. * Once tested, reload your 'nginx' configuration to activate the webhook reverse proxy configuration. To validate that the TLS configuration is working, perform the following tests from a different system that is able to resolve your FQDN externally: ``` curl -I -v --tlsv1.2 --tls-max 1.2 https:// curl -I -v --tlsv1.3 --tls-max 1.3 https:// ``` This should return valid TLS information similar to the following: ``` * Rebuilt URL to: https://your.fully.qualified.domain.name/ * Trying 123.123.123.123... * TCP_NODELAY set * Connected to your.fully.qualified.domain.name (123.123.123.123) port 443 (#0) * ALPN, offering h2 * ALPN, offering http/1.1 * successfully set certificate verify locations: * CAfile: /etc/pki/tls/certs/ca-bundle.crt CApath: none * TLSv1.2 (OUT), TLS handshake, Client hello (1): * TLSv1.2 (IN), TLS handshake, Server hello (2): * TLSv1.2 (IN), TLS handshake, Certificate (11): * TLSv1.2 (IN), TLS handshake, Server key exchange (12): * TLSv1.2 (IN), TLS handshake, Server finished (14): * TLSv1.2 (OUT), TLS handshake, Client key exchange (16): * TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1): * TLSv1.2 (OUT), TLS handshake, Finished (20): * TLSv1.2 (IN), TLS handshake, Finished (20): * SSL connection using TLSv1.2 / ECDHE-ECDSA-AES256-GCM-SHA384 * ALPN, server accepted to use http/1.1 * Server certificate: * subject: CN=your.fully.qualified.domain.name * start date: Aug 28 07:18:04 2024 GMT * expire date: Nov 26 07:18:03 2024 GMT * subjectAltName: host "your.fully.qualified.domain.name" matched cert's "your.fully.qualified.domain.name" * issuer: C=US; O=Let's Encrypt; CN=E6 * SSL certificate verify ok. > HEAD / HTTP/1.1 > Host: your.fully.qualified.domain.name > User-Agent: curl/7.61.1 > Accept: */* > < HTTP/1.1 200 OK HTTP/1.1 200 OK < Server: nginx/1.26.2 Server: nginx/1.26.2 < Date: Sat, 31 Aug 2024 22:36:01 GMT Date: Sat, 31 Aug 2024 22:36:01 GMT < Content-Type: text/html Content-Type: text/html < Content-Length: 8474 Content-Length: 8474 < Last-Modified: Mon, 20 Feb 2023 17:42:39 GMT Last-Modified: Mon, 20 Feb 2023 17:42:39 GMT < Connection: keep-alive Connection: keep-alive < ETag: "63f3b10f-211a" ETag: "63f3b10f-211a" < Accept-Ranges: bytes Accept-Ranges: bytes ``` Lastly, to validate that TLS 1.1 and below is being blocked, perform the following tests from a different system that is able to resolve your FQDN externally: ``` curl -I -v --tlsv1.1 --tls-max 1.1 https:// ``` The response should be similar to the following: ``` * Rebuilt URL to: https://your.fully.qualified.domain.name/ * Trying 123.123.123.123... * TCP_NODELAY set * Connected to your.fully.qualified.domain.name (123.123.123.123) port 443 (#0) * ALPN, offering h2 * ALPN, offering http/1.1 * successfully set certificate verify locations: * CAfile: /etc/pki/tls/certs/ca-bundle.crt CApath: none * TLSv1.3 (OUT), TLS alert, internal error (592): * error:141E70BF:SSL routines:tls_construct_client_hello:no protocols available curl: (35) error:141E70BF:SSL routines:tls_construct_client_hello:no protocols available ``` > [!IMPORTANT] > TLS 1.2 and TLS 1.3 support is provided by OpenSSL. > > To correctly support only using these TLS versions, you must be using 'nginx' version 1.15.0 or later combined with OpenSSL 1.1.1 or later. > > If your distribution does not provide these, then please raise this with your distribution or upgrade your distribution to one that does. > [!NOTE] > If you use a version of 'nginx' that supports TLS 1.3 but are using an older version of OpenSSL (e.g., OpenSSL 1.0.x), TLS 1.3 will not be supported even if your 'nginx' configuration requests it. > [!NOTE] > If using 'LetsEncrypt', TLS 1.2 and TLS 1.3 support will be automatically configured in the `/etc/letsencrypt/options-ssl-nginx.conf` include file when the SSL Certificate is added to your 'nginx' configuration. ### Step 7: Secure your 'nginx' configuration to only allow Microsoft 365 to connect Enhance your 'nginx' configuration to only allow the Microsoft 365 platform which includes the Microsoft Graph API to communicate with your configured webhooks endpoint. Review https://www.microsoft.com/en-us/download/details.aspx?id=56519 to assist you. Please note, it is beyond the scope of this document to tell you how to secure your system against unauthorised access of your endpoint listener. > [!IMPORTANT] > The IP address ranges that are part of the Microsoft 365 Common and Office Online services, which also cover Microsoft Graph API can be sourced from the above Microsoft URL. You should regularly update your configuration as Microsoft updates these ranges frequently. > It is recommended to automate these updates accordingly and is also beyond the scope of this document on how to do this. ### Step 8: Test your 'onedrive' application using this configuration * Run the 'onedrive' application using `--monitor --verbose` and the client should now create a new subscription and register itself: ``` ..... Performing initial synchronisation to ensure consistent local state ... Started webhook server Initializing subscription for updates ... Webhook: handled validation request Created new subscription a09ba1cf-3420-4d78-9117-b41373de33ff with expiration: 2024-08-28T08:42:00.637Z Attempting to contact Microsoft OneDrive Login Service Successfully reached Microsoft OneDrive Login Service Starting a sync with Microsoft OneDrive ..... ``` * Review the 'nginx' logs to validate that applicable communication is occurring: ``` 70.37.95.11 - - [28/Aug/2024:18:26:07 +1000] "POST /webhooks/onedrive?validationToken=Validation%3a+Testing+client+application+reachability+for+subscription+Request-Id%3a+25460109-0e8b-4521-8090-dd691b407ed8 HTTP/1.1" 200 128 "-" "-" "-" 137.135.11.116 - - [28/Aug/2024:18:32:02 +1000] "POST /webhooks/onedrive?validationToken=Validation%3a+Testing+client+application+reachability+for+subscription+Request-Id%3a+65e43e3c-cbab-4e74-87ec-0e8fafdef6d3 HTTP/1.1" 200 128 "-" "-" "-" ``` ## Troubleshooting In some circumstances, `SELinux` can provent 'nginx' from communicating with local system processes. When this occurs, the application will generate an error similar to the following: ``` ERROR: Microsoft OneDrive API returned an error with the following message: Error Message: HTTP request returned status code 400 (Bad Request) Error Reason: Subscription validation request failed. Notification endpoint must respond with 200 OK to validation request. Error Code: ValidationError Error Timestamp: 2024-08-28T08:22:34 API Request ID: 36684746-1458-4150-aeab-9871355a106c Calling Function: logSubscriptionError() ``` To correct this issue, use the `setsebool` tool to allow HTTPD processes (which includes 'nginx') to make network connections: ``` sudo setsebool -P httpd_can_network_connect 1 ``` After setting the boolean, restart 'nginx' to apply the SELinux configuration change. ## Resulting configuration When these steps are followed, your environment configuration will be similar to the following diagram: ![webhooks](./puml/webhooks.png) ## Additional Configuration Assistance Refer to [application-config-options.md](application-config-options.md) for further guidance on 'webhook' configuration options.onedrive-2.5.5/install-sh000077500000000000000000000360101476564400300153530ustar00rootroot00000000000000#!/bin/sh # install - install a program, script, or datafile scriptversion=2018-03-11.20; # UTC # This originates from X11R5 (mit/util/scripts/install.sh), which was # later released in X11R6 (xc/config/util/install.sh) with the # following copyright and license. # # Copyright (C) 1994 X Consortium # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to # deal in the Software without restriction, including without limitation the # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or # sell copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # X CONSORTIUM BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN # AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNEC- # TION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # # Except as contained in this notice, the name of the X Consortium shall not # be used in advertising or otherwise to promote the sale, use or other deal- # ings in this Software without prior written authorization from the X Consor- # tium. # # # FSF changes to this file are in the public domain. # # Calling this script install-sh is preferred over install.sh, to prevent # 'make' implicit rules from creating a file called install from it # when there is no Makefile. # # This script is compatible with the BSD install script, but was written # from scratch. tab=' ' nl=' ' IFS=" $tab$nl" # Set DOITPROG to "echo" to test this script. doit=${DOITPROG-} doit_exec=${doit:-exec} # Put in absolute file names if you don't have them in your path; # or use environment vars. chgrpprog=${CHGRPPROG-chgrp} chmodprog=${CHMODPROG-chmod} chownprog=${CHOWNPROG-chown} cmpprog=${CMPPROG-cmp} cpprog=${CPPROG-cp} mkdirprog=${MKDIRPROG-mkdir} mvprog=${MVPROG-mv} rmprog=${RMPROG-rm} stripprog=${STRIPPROG-strip} posix_mkdir= # Desired mode of installed file. mode=0755 chgrpcmd= chmodcmd=$chmodprog chowncmd= mvcmd=$mvprog rmcmd="$rmprog -f" stripcmd= src= dst= dir_arg= dst_arg= copy_on_change=false is_target_a_directory=possibly usage="\ Usage: $0 [OPTION]... [-T] SRCFILE DSTFILE or: $0 [OPTION]... SRCFILES... DIRECTORY or: $0 [OPTION]... -t DIRECTORY SRCFILES... or: $0 [OPTION]... -d DIRECTORIES... In the 1st form, copy SRCFILE to DSTFILE. In the 2nd and 3rd, copy all SRCFILES to DIRECTORY. In the 4th, create DIRECTORIES. Options: --help display this help and exit. --version display version info and exit. -c (ignored) -C install only if different (preserve the last data modification time) -d create directories instead of installing files. -g GROUP $chgrpprog installed files to GROUP. -m MODE $chmodprog installed files to MODE. -o USER $chownprog installed files to USER. -s $stripprog installed files. -t DIRECTORY install into DIRECTORY. -T report an error if DSTFILE is a directory. Environment variables override the default commands: CHGRPPROG CHMODPROG CHOWNPROG CMPPROG CPPROG MKDIRPROG MVPROG RMPROG STRIPPROG " while test $# -ne 0; do case $1 in -c) ;; -C) copy_on_change=true;; -d) dir_arg=true;; -g) chgrpcmd="$chgrpprog $2" shift;; --help) echo "$usage"; exit $?;; -m) mode=$2 case $mode in *' '* | *"$tab"* | *"$nl"* | *'*'* | *'?'* | *'['*) echo "$0: invalid mode: $mode" >&2 exit 1;; esac shift;; -o) chowncmd="$chownprog $2" shift;; -s) stripcmd=$stripprog;; -t) is_target_a_directory=always dst_arg=$2 # Protect names problematic for 'test' and other utilities. case $dst_arg in -* | [=\(\)!]) dst_arg=./$dst_arg;; esac shift;; -T) is_target_a_directory=never;; --version) echo "$0 $scriptversion"; exit $?;; --) shift break;; -*) echo "$0: invalid option: $1" >&2 exit 1;; *) break;; esac shift done # We allow the use of options -d and -T together, by making -d # take the precedence; this is for compatibility with GNU install. if test -n "$dir_arg"; then if test -n "$dst_arg"; then echo "$0: target directory not allowed when installing a directory." >&2 exit 1 fi fi if test $# -ne 0 && test -z "$dir_arg$dst_arg"; then # When -d is used, all remaining arguments are directories to create. # When -t is used, the destination is already specified. # Otherwise, the last argument is the destination. Remove it from $@. for arg do if test -n "$dst_arg"; then # $@ is not empty: it contains at least $arg. set fnord "$@" "$dst_arg" shift # fnord fi shift # arg dst_arg=$arg # Protect names problematic for 'test' and other utilities. case $dst_arg in -* | [=\(\)!]) dst_arg=./$dst_arg;; esac done fi if test $# -eq 0; then if test -z "$dir_arg"; then echo "$0: no input file specified." >&2 exit 1 fi # It's OK to call 'install-sh -d' without argument. # This can happen when creating conditional directories. exit 0 fi if test -z "$dir_arg"; then if test $# -gt 1 || test "$is_target_a_directory" = always; then if test ! -d "$dst_arg"; then echo "$0: $dst_arg: Is not a directory." >&2 exit 1 fi fi fi if test -z "$dir_arg"; then do_exit='(exit $ret); exit $ret' trap "ret=129; $do_exit" 1 trap "ret=130; $do_exit" 2 trap "ret=141; $do_exit" 13 trap "ret=143; $do_exit" 15 # Set umask so as not to create temps with too-generous modes. # However, 'strip' requires both read and write access to temps. case $mode in # Optimize common cases. *644) cp_umask=133;; *755) cp_umask=22;; *[0-7]) if test -z "$stripcmd"; then u_plus_rw= else u_plus_rw='% 200' fi cp_umask=`expr '(' 777 - $mode % 1000 ')' $u_plus_rw`;; *) if test -z "$stripcmd"; then u_plus_rw= else u_plus_rw=,u+rw fi cp_umask=$mode$u_plus_rw;; esac fi for src do # Protect names problematic for 'test' and other utilities. case $src in -* | [=\(\)!]) src=./$src;; esac if test -n "$dir_arg"; then dst=$src dstdir=$dst test -d "$dstdir" dstdir_status=$? else # Waiting for this to be detected by the "$cpprog $src $dsttmp" command # might cause directories to be created, which would be especially bad # if $src (and thus $dsttmp) contains '*'. if test ! -f "$src" && test ! -d "$src"; then echo "$0: $src does not exist." >&2 exit 1 fi if test -z "$dst_arg"; then echo "$0: no destination specified." >&2 exit 1 fi dst=$dst_arg # If destination is a directory, append the input filename. if test -d "$dst"; then if test "$is_target_a_directory" = never; then echo "$0: $dst_arg: Is a directory" >&2 exit 1 fi dstdir=$dst dstbase=`basename "$src"` case $dst in */) dst=$dst$dstbase;; *) dst=$dst/$dstbase;; esac dstdir_status=0 else dstdir=`dirname "$dst"` test -d "$dstdir" dstdir_status=$? fi fi case $dstdir in */) dstdirslash=$dstdir;; *) dstdirslash=$dstdir/;; esac obsolete_mkdir_used=false if test $dstdir_status != 0; then case $posix_mkdir in '') # Create intermediate dirs using mode 755 as modified by the umask. # This is like FreeBSD 'install' as of 1997-10-28. umask=`umask` case $stripcmd.$umask in # Optimize common cases. *[2367][2367]) mkdir_umask=$umask;; .*0[02][02] | .[02][02] | .[02]) mkdir_umask=22;; *[0-7]) mkdir_umask=`expr $umask + 22 \ - $umask % 100 % 40 + $umask % 20 \ - $umask % 10 % 4 + $umask % 2 `;; *) mkdir_umask=$umask,go-w;; esac # With -d, create the new directory with the user-specified mode. # Otherwise, rely on $mkdir_umask. if test -n "$dir_arg"; then mkdir_mode=-m$mode else mkdir_mode= fi posix_mkdir=false case $umask in *[123567][0-7][0-7]) # POSIX mkdir -p sets u+wx bits regardless of umask, which # is incompatible with FreeBSD 'install' when (umask & 300) != 0. ;; *) # Note that $RANDOM variable is not portable (e.g. dash); Use it # here however when possible just to lower collision chance. tmpdir=${TMPDIR-/tmp}/ins$RANDOM-$$ trap 'ret=$?; rmdir "$tmpdir/a/b" "$tmpdir/a" "$tmpdir" 2>/dev/null; exit $ret' 0 # Because "mkdir -p" follows existing symlinks and we likely work # directly in world-writeable /tmp, make sure that the '$tmpdir' # directory is successfully created first before we actually test # 'mkdir -p' feature. if (umask $mkdir_umask && $mkdirprog $mkdir_mode "$tmpdir" && exec $mkdirprog $mkdir_mode -p -- "$tmpdir/a/b") >/dev/null 2>&1 then if test -z "$dir_arg" || { # Check for POSIX incompatibilities with -m. # HP-UX 11.23 and IRIX 6.5 mkdir -m -p sets group- or # other-writable bit of parent directory when it shouldn't. # FreeBSD 6.1 mkdir -m -p sets mode of existing directory. test_tmpdir="$tmpdir/a" ls_ld_tmpdir=`ls -ld "$test_tmpdir"` case $ls_ld_tmpdir in d????-?r-*) different_mode=700;; d????-?--*) different_mode=755;; *) false;; esac && $mkdirprog -m$different_mode -p -- "$test_tmpdir" && { ls_ld_tmpdir_1=`ls -ld "$test_tmpdir"` test "$ls_ld_tmpdir" = "$ls_ld_tmpdir_1" } } then posix_mkdir=: fi rmdir "$tmpdir/a/b" "$tmpdir/a" "$tmpdir" else # Remove any dirs left behind by ancient mkdir implementations. rmdir ./$mkdir_mode ./-p ./-- "$tmpdir" 2>/dev/null fi trap '' 0;; esac;; esac if $posix_mkdir && ( umask $mkdir_umask && $doit_exec $mkdirprog $mkdir_mode -p -- "$dstdir" ) then : else # The umask is ridiculous, or mkdir does not conform to POSIX, # or it failed possibly due to a race condition. Create the # directory the slow way, step by step, checking for races as we go. case $dstdir in /*) prefix='/';; [-=\(\)!]*) prefix='./';; *) prefix='';; esac oIFS=$IFS IFS=/ set -f set fnord $dstdir shift set +f IFS=$oIFS prefixes= for d do test X"$d" = X && continue prefix=$prefix$d if test -d "$prefix"; then prefixes= else if $posix_mkdir; then (umask=$mkdir_umask && $doit_exec $mkdirprog $mkdir_mode -p -- "$dstdir") && break # Don't fail if two instances are running concurrently. test -d "$prefix" || exit 1 else case $prefix in *\'*) qprefix=`echo "$prefix" | sed "s/'/'\\\\\\\\''/g"`;; *) qprefix=$prefix;; esac prefixes="$prefixes '$qprefix'" fi fi prefix=$prefix/ done if test -n "$prefixes"; then # Don't fail if two instances are running concurrently. (umask $mkdir_umask && eval "\$doit_exec \$mkdirprog $prefixes") || test -d "$dstdir" || exit 1 obsolete_mkdir_used=true fi fi fi if test -n "$dir_arg"; then { test -z "$chowncmd" || $doit $chowncmd "$dst"; } && { test -z "$chgrpcmd" || $doit $chgrpcmd "$dst"; } && { test "$obsolete_mkdir_used$chowncmd$chgrpcmd" = false || test -z "$chmodcmd" || $doit $chmodcmd $mode "$dst"; } || exit 1 else # Make a couple of temp file names in the proper directory. dsttmp=${dstdirslash}_inst.$$_ rmtmp=${dstdirslash}_rm.$$_ # Trap to clean up those temp files at exit. trap 'ret=$?; rm -f "$dsttmp" "$rmtmp" && exit $ret' 0 # Copy the file name to the temp name. (umask $cp_umask && $doit_exec $cpprog "$src" "$dsttmp") && # and set any options; do chmod last to preserve setuid bits. # # If any of these fail, we abort the whole thing. If we want to # ignore errors from any of these, just make sure not to ignore # errors from the above "$doit $cpprog $src $dsttmp" command. # { test -z "$chowncmd" || $doit $chowncmd "$dsttmp"; } && { test -z "$chgrpcmd" || $doit $chgrpcmd "$dsttmp"; } && { test -z "$stripcmd" || $doit $stripcmd "$dsttmp"; } && { test -z "$chmodcmd" || $doit $chmodcmd $mode "$dsttmp"; } && # If -C, don't bother to copy if it wouldn't change the file. if $copy_on_change && old=`LC_ALL=C ls -dlL "$dst" 2>/dev/null` && new=`LC_ALL=C ls -dlL "$dsttmp" 2>/dev/null` && set -f && set X $old && old=:$2:$4:$5:$6 && set X $new && new=:$2:$4:$5:$6 && set +f && test "$old" = "$new" && $cmpprog "$dst" "$dsttmp" >/dev/null 2>&1 then rm -f "$dsttmp" else # Rename the file to the real destination. $doit $mvcmd -f "$dsttmp" "$dst" 2>/dev/null || # The rename failed, perhaps because mv can't rename something else # to itself, or perhaps because mv is so ancient that it does not # support -f. { # Now remove or move aside any old file at destination location. # We try this two ways since rm can't unlink itself on some # systems and the destination file might be busy for other # reasons. In this case, the final cleanup might fail but the new # file should still install successfully. { test ! -f "$dst" || $doit $rmcmd -f "$dst" 2>/dev/null || { $doit $mvcmd -f "$dst" "$rmtmp" 2>/dev/null && { $doit $rmcmd -f "$rmtmp" 2>/dev/null; :; } } || { echo "$0: cannot unlink or rename $dst" >&2 (exit 1); exit 1 } } && # Now rename the file to the real destination. $doit $mvcmd "$dsttmp" "$dst" } fi || exit 1 trap '' 0 fi done # Local variables: # eval: (add-hook 'before-save-hook 'time-stamp) # time-stamp-start: "scriptversion=" # time-stamp-format: "%:y-%02m-%02d.%02H" # time-stamp-time-zone: "UTC0" # time-stamp-end: "; # UTC" # End: onedrive-2.5.5/onedrive.1.in000066400000000000000000000314121476564400300156520ustar00rootroot00000000000000.TH ONEDRIVE "1" "@PACKAGE_DATE@" "@PACKAGE_VERSION@" "User Commands" .SH NAME onedrive \- A client for the Microsoft OneDrive Cloud Service .SH SYNOPSIS .B onedrive [\fI\,OPTION\/\fR] --sync .br .B onedrive [\fI\,OPTION\/\fR] --monitor .br .B onedrive [\fI\,OPTION\/\fR] --display-config .br .B onedrive [\fI\,OPTION\/\fR] --display-sync-status .br .B onedrive [\fI\,OPTION\/\fR] -h | --help .br .B onedrive --version .SH DESCRIPTION This is a free Microsoft OneDrive Client designed to work with OneDrive Personal, OneDrive for Business, Office365 OneDrive, and SharePoint Libraries. It's fully compatible with most major Linux distributions and FreeBSD, and can be containerised using Docker or Podman. The client offers secure one-way and two-way synchronisation capabilities, making it easy to connect to Microsoft OneDrive services across various platforms. .SH FEATURES .br * Compatible with OneDrive Personal, OneDrive for Business including accessing Microsoft SharePoint Libraries .br * Provides rules for client-side filtering to select data for syncing with Microsoft OneDrive accounts .br * Caches sync state for efficiency .br * Supports a dry-run option for safe configuration testing .br * Validates file transfers to ensure data integrity .br * Monitors local files in real-time using inotify .br * Supports interrupted uploads for completion at a later time .br * Capability to sync remote updates immediately via webhooks .br * Enhanced synchronisation speed with multi-threaded file transfers .br * Manages traffic bandwidth use with rate limiting .br * Supports seamless access to shared folders and files across both OneDrive Personal and OneDrive for Business accounts .br * Supports national cloud deployments including Microsoft Cloud for US Government, Microsoft Cloud Germany, and Azure and Office 365 operated by VNET in China .br * Supports sending desktop alerts using libnotify .br * Protects against significant data loss on OneDrive after configuration changes .br * Works with both single and multi-tenant applications .SH CONFIGURATION By default, the client will use a sensible set of default values to interact with the Microsoft OneDrive service. .TP Should you wish to change these defaults, you should copy the default config file into your home directory before making any applicable changes: .nf \fB mkdir\ \-p\ ~/.config/onedrive cp\ @DOCDIR@/config\ ~/.config/onedrive/config \fP .fi .TP Please refer to the online documentation file application-config-options.md for details on all configuration file options. .SH CLIENT SIDE FILTERING Client Side Filtering in the context of the OneDrive Client for Linux refers to user-configured rules that determine what files and directories the client should upload or download from Microsoft OneDrive. These rules are crucial for optimising synchronisation, especially when dealing with large numbers of files or specific file types. The OneDrive Client for Linux offers several configuration options to facilitate this: .TP .B skip_dir Specifies directories that should not be synchronised with OneDrive. Useful for omitting large or irrelevant directories from the sync process. .TP .B skip_dotfiles Excludes dotfiles, usually configuration files or scripts, from the sync. Ideal for users who prefer to keep these files local. .TP .B skip_file Allows specifying specific files to exclude from synchronisation. Offers flexibility in selecting essential files for cloud storage. .TP .B skip_symlinks Prevents symlinks, which often point to files outside the OneDrive directory or to irrelevant locations, from being included in the sync. .PP Additionally, the OneDrive Client for Linux allows the implementation of Client Side Filtering rules through a 'sync_list' file. This file explicitly states which directories or files should be included in the synchronisation. By default, any item not listed in the 'sync_list' file is excluded. This approach offers granular control over synchronisation, ensuring that only necessary data is transferred to and from Microsoft OneDrive. .PP These configurable options and the 'sync_list' file provide users with the flexibility to tailor the synchronisation process to their specific needs, conserving bandwidth and storage space while ensuring that important files are always backed up and accessible. .TP .B NOTE: After changing any Client Side Filtering rule, a full re-synchronisation must be performed using --resync .SH FIRST RUN Once you've installed the application, you'll need to authorise it using your Microsoft OneDrive Account. This can be done by simply running the application without any additional command switches. .TP Please be aware that some companies may require you to explicitly add this app to the Microsoft MyApps portal. To add an approved app to your apps, click on the ellipsis in the top-right corner and select "Request new apps." On the next page, you can add this app. If it's not listed, you should make a request through your IT department. .TP When you run the application for the first time, you'll be prompted to open a specific URL using your web browser, where you'll need to log in to your Microsoft Account and grant the application permission to access your files. After granting permission to the application, you'll be redirected to a blank page. Simply copy the URI from the blank page and paste it into the application. .TP This process authenticates your application with your account information, and it is now ready to use to sync your data between your local system and Microsoft OneDrive. .SH GUI NOTIFICATIONS If the client has been compiled with support for notifications, the client will send notifications about client activity via libnotify to the GUI via DBus when the client is being run in --monitor mode. .SH APPLICATION LOGGING When running onedrive all actions can be logged to a separate log file. This can be enabled by using the \fB--enable-logging\fP flag. By default, log files will be written to \fB/var/log/onedrive\fP. All logfiles will be in the format of \fB%username%.onedrive.log\fP, where \fB%username%\fP represents the user who ran the client. .SH ALL CLI OPTIONS The options below allow you to control the behavior of the onedrive client from the CLI. Without any specific option, if the client is already authenticated, the client will exit without any further action. .TP \fB\-\-sync\fR Do a one-time synchronisation with OneDrive. .TP \fB\-\-monitor\fR Monitor filesystem for changes and sync regularly. .TP \fB\-\-display-config\fR Display the currently used configuration for the onedrive client. .TP \fB\-\-display-sync-status\fR Query OneDrive service and report on pending changes. .TP \fB\-\-auth-files\fR \fIARG\fR Perform authentication not via interactive dialog but via files that are read/written when using this option. The two files are passed in as \fBARG\fP in the format \fBauthUrl:responseUrl\fP. The authorisation URL is written to the \fBauthUrl\fP file, then \fBonedrive\fP waits for the file \fBresponseUrl\fP to be present, and reads the response from that file. .br Always specify the full path when using this option, otherwise the application will default to using the default configuration path for these files (~/.config/onedrive/) .TP \fB\-\-auth-response\fR \fIARG\fR Perform authentication not via interactive dialog but via providing the response URL directly. .TP \fB\-\-check-for-nomount\fR Check for the presence of .nosync in the syncdir root. If found, do not perform sync. .TP \fB\-\-check-for-nosync\fR Check for the presence of .nosync in each directory. If found, skip directory from sync. .TP \fB\-\-classify-as-big-delete\fR \fIARG\fR Number of children in a path that is locally removed which will be classified as a 'big data delete'. .TP \fB\-\-cleanup-local-files\fR Cleanup additional local files when using --download-only. This will remove local data. .TP \fB\-\-confdir\fR \fIARG\fR Set the directory used to store the configuration files. .TP \fB\-\-create-directory\fR \fIARG\fR Create a directory on OneDrive - no sync will be performed. .TP \fB\-\-create-share-link\fR \fIARG\fR Create a shareable link for an existing file on OneDrive. .TP \fB\-\-debug-https\fR Debug OneDrive HTTPS communication. .TP \fB\-\-destination-directory\fR \fIARG\fR Destination directory for renamed or moved items on OneDrive - no sync will be performed. .TP \fB\-\-disable-download-validation\fR Disable download validation when downloading from OneDrive. .TP \fB\-\-disable-notifications\fR Do not use desktop notifications in monitor mode. .TP \fB\-\-disable-upload-validation\fR Disable upload validation when uploading to OneDrive. .TP \fB\-\-display-quota\fR Display the quota status of the client - no sync will be performed. .TP \fB\-\-display-running-config\fR Display what options the client has been configured to use on application startup. .TP \fB\-\-download-only\fR Replicate the OneDrive online state locally, by only downloading changes from OneDrive. Do not upload local changes to OneDrive. .TP \fB\-\-dry-run\fR Perform a trial sync with no changes made. .TP \fB\-\-enable-logging\fR Enable client activity to a separate log file. .TP \fB\-\-force\fR Force the deletion of data when a 'big delete' is detected. .TP \fB\-\-force-http-11\fR Force the use of HTTP 1.1 for all operations. .TP \fB\-\-force-sync\fR Force a synchronisation of a specific folder, only when using --sync --single-directory and ignore all non-default skip_dir and skip_file rules. .TP \fB\-\-get-O365-drive-id\fR \fIARG\fR Query and return the Office 365 Drive ID for a given Office 365 SharePoint Shared Library (DEPRECATED). .TP \fB\-\-get-file-link\fR \fIARG\fR Display the file link of a synced file. .TP \fB\-\-get-sharepoint-drive-id\fR Query and return the Office 365 Drive ID for a given Office 365 SharePoint Shared Library. .TP \fB\-\-help\fR, \fB\-h\fR Display application help. .TP \fB\-\-list-shared-items\fR List OneDrive Business Shared Items. .TP \fB\-\-local-first\fR Synchronise from the local directory source first, before downloading changes from OneDrive. .TP \fB\-\-log-dir\fR \fIARG\fR Directory where logging output is saved to, needs to end with a slash. .TP \fB\-\-logout\fR Logout the current user. .TP \fB\-\-modified-by\fR \fIARG\fR Display the last modified by details of a given path. .TP \fB\-\-monitor-interval\fR \fIARG\fR Number of seconds by which each sync operation is undertaken when idle under monitor mode. .TP \fB\-\-monitor-log-frequency\fR \fIARG\fR Frequency of logging in monitor mode. .TP \fB\-\-no-remote-delete\fR Do not delete local file 'deletes' from OneDrive when using --upload-only. .TP \fB\-\-print-access-token\fR Print the access token, useful for debugging. .TP \fB\-\-reauth\fR Reauthenticate the client with OneDrive. .TP \fB\-\-remove-directory\fR \fIARG\fR Remove a directory on OneDrive - no sync will be performed. .TP \fB\-\-remove-source-files\fR Remove source file after successful transfer to OneDrive when using --upload-only. .TP \fB\-\-resync\fR Forget the last saved state, perform a full sync. .TP \fB\-\-resync-auth\fR Approve the use of performing a --resync action. .TP \fB\-\-single-directory\fR \fIARG\fR Specify a single local directory within the OneDrive root to sync. .TP \fB\-\-skip-dir\fR \fIARG\fR Skip any directories that match this pattern from syncing. .TP \fB\-\-skip-dir-strict-match\fR When matching skip_dir directories, only match explicit matches. .TP \fB\-\-skip-dot-files\fR Skip dot files and folders from syncing. .TP \fB\-\-skip-file\fR \fIARG\fR Skip any files that match this pattern from syncing. .TP \fB\-\-skip-size\fR \fIARG\fR Skip new files larger than this size (in MB). .TP \fB\-\-skip-symlinks\fR Skip syncing of symlinks. .TP \fB\-\-source-directory\fR \fIARG\fR Source directory to rename or move on OneDrive - no sync will be performed. .TP \fB\-\-space-reservation\fR \fIARG\fR The amount of disk space to reserve (in MB) to avoid 100% disk space utilisation. .TP \fB\-\-sync-root-files\fR Sync all files in sync_dir root when using sync_list. .TP \fB\-\-sync-shared-files\fR Sync OneDrive Business Shared Files to the local filesystem. .TP \fB\-\-syncdir\fR \fIARG\fR Specify the local directory used for synchronisation to OneDrive. .TP \fB\-\-synchronize\fR Perform a synchronisation with Microsoft OneDrive (DEPRECATED). .TP \fB\-\-upload-only\fR Replicate the locally configured sync_dir state to OneDrive, by only uploading local changes to OneDrive. Do not download changes from OneDrive. .TP \fB\-\-verbose\fR, \fB\-v+\fR Print more details, useful for debugging (repeat for extra debugging). .TP \fB\-\-version\fR Print the version and exit. .TP \fB\-\-with-editing-perms\fR Create a read-write shareable link for an existing file on OneDrive when used with --create-share-link . .SH DOCUMENTATION All documentation is available on GitHub: https://github.com/abraunegg/onedrive/tree/master/docs/ .SH SEE ALSO .BR curl(1), onedrive-2.5.5/readme.md000066400000000000000000000201471476564400300151320ustar00rootroot00000000000000# OneDrive Client for Linux [![Version](https://img.shields.io/github/v/release/abraunegg/onedrive)](https://github.com/abraunegg/onedrive/releases) [![Release Date](https://img.shields.io/github/release-date/abraunegg/onedrive)](https://github.com/abraunegg/onedrive/releases) [![Test Build](https://github.com/abraunegg/onedrive/actions/workflows/testbuild.yaml/badge.svg)](https://github.com/abraunegg/onedrive/actions/workflows/testbuild.yaml) [![Build Docker Images](https://github.com/abraunegg/onedrive/actions/workflows/docker.yaml/badge.svg)](https://github.com/abraunegg/onedrive/actions/workflows/docker.yaml) [![Docker Pulls](https://img.shields.io/docker/pulls/driveone/onedrive)](https://hub.docker.com/r/driveone/onedrive) Introducing a free Microsoft OneDrive Client that seamlessly supports OneDrive Personal, OneDrive for Business, OneDrive for Office365, and SharePoint Libraries. This robust and highly customisable client is compatible with all major Linux distributions and FreeBSD, and can also be deployed as a container using Docker or Podman. It offers both one-way and two-way synchronisation capabilities while ensuring a secure connection to Microsoft OneDrive services. Originally derived as a 'fork' from the [skilion](https://github.com/skilion/onedrive) client, it's worth noting that the developer of the original client has explicitly stated they have no intention of maintaining or supporting their work ([reference](https://github.com/skilion/onedrive/issues/518#issuecomment-717604726)). This client represents a 100% re-imagining of the original work, addressing numerous notable bugs and issues while incorporating a significant array of new features. This client has been under active development since mid-2018. ## Features * Compatible with OneDrive Personal, OneDrive for Business including accessing Microsoft SharePoint Libraries * Provides rules for client-side filtering to select data for syncing with Microsoft OneDrive accounts * Caches sync state for efficiency * Supports a dry-run option for safe configuration testing * Validates file transfers to ensure data integrity * Monitors local files in real-time using inotify * Supports interrupted uploads for completion at a later time * Capability to sync remote updates immediately via webhooks * Enhanced synchronisation speed with multi-threaded file transfers * Manages traffic bandwidth use with rate limiting * Supports seamless access to shared folders and files across both OneDrive Personal and OneDrive for Business accounts * Supports national cloud deployments including Microsoft Cloud for US Government, Microsoft Cloud Germany and Azure and Office 365 operated by VNET in China * Supports sending desktop alerts using libnotify * Protects against significant data loss on OneDrive after configuration changes * Works with both single and multi-tenant applications ## What's missing * Ability to encrypt/decrypt files on-the-fly when uploading/downloading files from OneDrive * Support for Windows 'On-Demand' functionality so file is only downloaded when accessed locally ## External Enhancements * A GUI for configuration management: [OneDrive Client for Linux GUI](https://github.com/bpozdena/OneDriveGUI) * Colorful log output terminal modification: [OneDrive Client for Linux Colorful log Output](https://github.com/zzzdeb/dotfiles/blob/master/scripts/tools/onedrive_log) * System Tray Icon: [OneDrive Client for Linux System Tray Icon](https://github.com/DanielBorgesOliveira/onedrive_tray) ## Frequently Asked Questions Refer to [Frequently Asked Questions](https://github.com/abraunegg/onedrive/wiki/Frequently-Asked-Questions) ## Have a question If you have a question or need something clarified, please raise a new discussion post [here](https://github.com/abraunegg/onedrive/discussions) ## Supported Application Version Support is only provided for the current application release version or newer 'master' branch versions. The current release version is: [![Version](https://img.shields.io/github/v/release/abraunegg/onedrive)](https://github.com/abraunegg/onedrive/releases) To check your version, run: `onedrive --version`. Ensure you are using the current release or compile the latest version from the master branch if needed. If you are using an older version, you must upgrade to the current release or newer to receive support. ## Basic Troubleshooting Steps If you are encountering any issue running the application please follow these steps first: 1. Check the version of the application using `onedrive --version` and ensure that you are running the latest [release](https://github.com/abraunegg/onedrive/releases). If you are already using the latest release and are still experiencing an issue, you must manually build the client from 'master' to ensure you are running the latest code, which will include fixes for bugs found since the last release that may be impacting you. 2. Configure the application to only use IPv4 network connectivity, and then retest. 3. Configure the application to only use HTTP/1.1. operations with IPv4 network connectivity, and then retest. 4. If the above points do not resolve your issue, upgrade your 'curl' version to the latest available by the curl developers. Refer to https://curl.se/docs/releases.html for details. ## Reporting an Issue or Bug > [!IMPORTANT] > Please ensure that issues reported as bugs are indeed software bugs. For installation problems, distribution package/version issues, or package dependency concerns, please start a [Discussion](https://github.com/abraunegg/onedrive/discussions) instead of filing a bug report. If you encounter any bugs you can report them here on Github. Before filing an issue be sure to: 1. Follow the Basic Troubleshooting Steps 2. Fill in a new bug report using the [issue template](https://github.com/abraunegg/onedrive/issues/new?template=bug_report.md) after following the Basic Troubleshooting Steps. Fill in *all* the details as this helps re-create your environment to replicate your issue. 3. Generate a debug log for support using the following [process](https://github.com/abraunegg/onedrive/wiki/Generate-debug-log-for-support) * If you are in *any* way concerned regarding the sensitivity of the data contained with in the verbose debug log file, create a new OneDrive account, configure the client to use that, use *dummy* data to simulate your environment and then replicate your original issue * If you are still concerned, provide an NDA or confidentiality document to sign 4. Upload the debug log to [pastebin](https://pastebin.com/) or archive and email to support@mynas.com.au * If you are concerned regarding the sensitivity of your debug data, encrypt + password protect the archive file and provide the decryption password via an out-of-band (OOB) mechanism. Email support@mynas.com.au for an OOB method for the password to be sent. * If you are still concerned, provide an NDA or confidentiality document to sign ## Known issues Refer to [docs/known-issues.md](https://github.com/abraunegg/onedrive/blob/master/docs/known-issues.md) ## Documentation and Configuration Assistance ### Installing from Distribution Packages or Building the OneDrive Client for Linux from source Refer to [docs/install.md](https://github.com/abraunegg/onedrive/blob/master/docs/install.md) ### Configuration and Usage Refer to [docs/usage.md](https://github.com/abraunegg/onedrive/blob/master/docs/usage.md) ### Configure OneDrive Business Shared Items Refer to [docs/business-shared-items.md](https://github.com/abraunegg/onedrive/blob/master/docs/business-shared-items.md) ### Configure SharePoint / Office 365 Shared Libraries (Business or Education) Refer to [docs/sharepoint-libraries.md](https://github.com/abraunegg/onedrive/blob/master/docs/sharepoint-libraries.md) ### Configure National Cloud support Refer to [docs/national-cloud-deployments.md](https://github.com/abraunegg/onedrive/blob/master/docs/national-cloud-deployments.md) ### Docker support Refer to [docs/docker.md](https://github.com/abraunegg/onedrive/blob/master/docs/docker.md) ### Podman support Refer to [docs/podman.md](https://github.com/abraunegg/onedrive/blob/master/docs/podman.md) onedrive-2.5.5/src/000077500000000000000000000000001476564400300141365ustar00rootroot00000000000000onedrive-2.5.5/src/arsd/000077500000000000000000000000001476564400300150675ustar00rootroot00000000000000onedrive-2.5.5/src/arsd/README.md000066400000000000000000000005361476564400300163520ustar00rootroot00000000000000The files in this directory have been obtained form the following places: cgi.d https://github.com/adamdruppe/arsd/blob/a870179988b8881b04126856105f0fad2cc0018d/cgi.d License: Boost Software License - Version 1.0 Copyright 2008-2021, Adam D. Ruppe see https://github.com/adamdruppe/arsd/blob/a870179988b8881b04126856105f0fad2cc0018d/LICENSE onedrive-2.5.5/src/arsd/cgi.d000066400000000000000000012660201476564400300160050ustar00rootroot00000000000000// FIXME: if an exception is thrown, we shouldn't necessarily cache... // FIXME: there's some annoying duplication of code in the various versioned mains // add the Range header in there too. should return 206 // FIXME: cgi per-request arena allocator // i need to add a bunch of type templates for validations... mayne @NotNull or NotNull! // FIXME: I might make a cgi proxy class which can change things; the underlying one is still immutable // but the later one can edit and simplify the api. You'd have to use the subclass tho! /* void foo(int f, @("test") string s) {} void main() { static if(is(typeof(foo) Params == __parameters)) //pragma(msg, __traits(getAttributes, Params[0])); pragma(msg, __traits(getAttributes, Params[1..2])); else pragma(msg, "fail"); } */ // Note: spawn-fcgi can help with fastcgi on nginx // FIXME: to do: add openssl optionally // make sure embedded_httpd doesn't send two answers if one writes() then dies // future direction: websocket as a separate process that you can sendfile to for an async passoff of those long-lived connections /* Session manager process: it spawns a new process, passing a command line argument, to just be a little key/value store of some serializable struct. On Windows, it CreateProcess. On Linux, it can just fork or maybe fork/exec. The session key is in a cookie. Server-side event process: spawns an async manager. You can push stuff out to channel ids and the clients listen to it. websocket process: spawns an async handler. They can talk to each other or get info from a cgi request. Tempting to put web.d 2.0 in here. It would: * map urls and form generation to functions * have data presentation magic * do the skeleton stuff like 1.0 * auto-cache generated stuff in files (at least if pure?) * introspect functions in json for consumers https://linux.die.net/man/3/posix_spawn */ /++ Provides a uniform server-side API for CGI, FastCGI, SCGI, and HTTP web applications. Offers both lower- and higher- level api options among other common (optional) things like websocket and event source serving support, session management, and job scheduling. --- import arsd.cgi; // Instead of writing your own main(), you should write a function // that takes a Cgi param, and use mixin GenericMain // for maximum compatibility with different web servers. void hello(Cgi cgi) { cgi.setResponseContentType("text/plain"); if("name" in cgi.get) cgi.write("Hello, " ~ cgi.get["name"]); else cgi.write("Hello, world!"); } mixin GenericMain!hello; --- Or: --- import arsd.cgi; class MyApi : WebObject { @UrlName("") string hello(string name = null) { if(name is null) return "Hello, world!"; else return "Hello, " ~ name; } } mixin DispatcherMain!( "/".serveApi!MyApi ); --- $(NOTE Please note that using the higher-level api will add a dependency on arsd.dom and arsd.jsvar to your application. If you use `dmd -i` or `ldc2 -i` to build, it will just work, but with dub, you will have do `dub add arsd-official:jsvar` and `dub add arsd-official:dom` yourself. ) Test on console (works in any interface mode): $(CONSOLE $ ./cgi_hello GET / name=whatever ) If using http version (default on `dub` builds, or on custom builds when passing `-version=embedded_httpd` to dmd): $(CONSOLE $ ./cgi_hello --port 8080 # now you can go to http://localhost:8080/?name=whatever ) Please note: the default port for http is 8085 and for scgi is 4000. I recommend you set your own by the command line argument in a startup script instead of relying on any hard coded defaults. It is possible though to code your own with [RequestServer], however. Build_Configurations: cgi.d tries to be flexible to meet your needs. It is possible to configure it both at runtime (by writing your own `main` function and constructing a [RequestServer] object) or at compile time using the `version` switch to the compiler or a dub `subConfiguration`. If you are using `dub`, use: ```sdlang subConfiguration "arsd-official:cgi" "VALUE_HERE" ``` or to dub.json: ```json "subConfigurations": {"arsd-official:cgi": "VALUE_HERE"} ``` to change versions. The possible options for `VALUE_HERE` are: $(LIST * `embedded_httpd` for the embedded httpd version (built-in web server). This is the default for dub builds. You can run the program then connect directly to it from your browser. * `cgi` for traditional cgi binaries. These are run by an outside web server as-needed to handle requests. * `fastcgi` for FastCGI builds. FastCGI is managed from an outside helper, there's one built into Microsoft IIS, Apache httpd, and Lighttpd, and a generic program you can use with nginx called `spawn-fcgi`. If you don't already know how to use it, I suggest you use one of the other modes. * `scgi` for SCGI builds. SCGI is a simplified form of FastCGI, where you run the server as an application service which is proxied by your outside webserver. * `stdio_http` for speaking raw http over stdin and stdout. This is made for systemd services. See [RequestServer.serveSingleHttpConnectionOnStdio] for more information. ) With dmd, use: $(TABLE_ROWS * + Interfaces + (mutually exclusive) * - `-version=plain_cgi` - The default building the module alone without dub - a traditional, plain CGI executable will be generated. * - `-version=embedded_httpd` - A HTTP server will be embedded in the generated executable. This is default when building with dub. * - `-version=fastcgi` - A FastCGI executable will be generated. * - `-version=scgi` - A SCGI (SimpleCGI) executable will be generated. * - `-version=embedded_httpd_hybrid` - A HTTP server that uses a combination of processes, threads, and fibers to better handle large numbers of idle connections. Recommended if you are going to serve websockets in a non-local application. * - `-version=embedded_httpd_threads` - The embedded HTTP server will use a single process with a thread pool. (use instead of plain `embedded_httpd` if you want this specific implementation) * - `-version=embedded_httpd_processes` - The embedded HTTP server will use a prefork style process pool. (use instead of plain `embedded_httpd` if you want this specific implementation) * - `-version=embedded_httpd_processes_accept_after_fork` - It will call accept() in each child process, after forking. This is currently the only option, though I am experimenting with other ideas. You probably should NOT specify this right now. * - `-version=stdio_http` - The embedded HTTP server will be spoken over stdin and stdout. * + Tweaks + (can be used together with others) * - `-version=cgi_with_websocket` - The CGI class has websocket server support. (This is on by default now.) * - `-version=with_openssl` - not currently used * - `-version=cgi_embedded_sessions` - The session server will be embedded in the cgi.d server process * - `-version=cgi_session_server_process` - The session will be provided in a separate process, provided by cgi.d. ) For example, For CGI, `dmd yourfile.d cgi.d` then put the executable in your cgi-bin directory. For FastCGI: `dmd yourfile.d cgi.d -version=fastcgi` and run it. spawn-fcgi helps on nginx. You can put the file in the directory for Apache. On IIS, run it with a port on the command line (this causes it to call FCGX_OpenSocket, which can work on nginx too). For SCGI: `dmd yourfile.d cgi.d -version=scgi` and run the executable, providing a port number on the command line. For an embedded HTTP server, run `dmd yourfile.d cgi.d -version=embedded_httpd` and run the generated program. It listens on port 8085 by default. You can change this on the command line with the --port option when running your program. Simulating_requests: If you are using one of the [GenericMain] or [DispatcherMain] mixins, or main with your own call to [RequestServer.trySimulatedRequest], you can simulate requests from your command line shell. Call the program like this: $(CONSOLE ./yourprogram GET / name=adr ) And it will print the result to stdout instead of running a server, regardless of build more.. CGI_Setup_tips: On Apache, you may do `SetHandler cgi-script` in your `.htaccess` file to set a particular file to be run through the cgi program. Note that all "subdirectories" of it also run the program; if you configure `/foo` to be a cgi script, then going to `/foo/bar` will call your cgi handler function with `cgi.pathInfo == "/bar"`. Overview_Of_Basic_Concepts: cgi.d offers both lower-level handler apis as well as higher-level auto-dispatcher apis. For a lower-level handler function, you'll probably want to review the following functions: Input: [Cgi.get], [Cgi.post], [Cgi.request], [Cgi.files], [Cgi.cookies], [Cgi.pathInfo], [Cgi.requestMethod], and HTTP headers ([Cgi.headers], [Cgi.userAgent], [Cgi.referrer], [Cgi.accept], [Cgi.authorization], [Cgi.lastEventId]) Output: [Cgi.write], [Cgi.header], [Cgi.setResponseStatus], [Cgi.setResponseContentType], [Cgi.gzipResponse] Cookies: [Cgi.setCookie], [Cgi.clearCookie], [Cgi.cookie], [Cgi.cookies] Caching: [Cgi.setResponseExpires], [Cgi.updateResponseExpires], [Cgi.setCache] Redirections: [Cgi.setResponseLocation] Other Information: [Cgi.remoteAddress], [Cgi.https], [Cgi.port], [Cgi.scriptName], [Cgi.requestUri], [Cgi.getCurrentCompleteUri], [Cgi.onRequestBodyDataReceived] Websockets: [Websocket], [websocketRequested], [acceptWebsocket]. For websockets, use the `embedded_httpd_hybrid` build mode for best results, because it is optimized for handling large numbers of idle connections compared to the other build modes. Overriding behavior for special cases streaming input data: see the virtual functions [Cgi.handleIncomingDataChunk], [Cgi.prepareForIncomingDataChunks], [Cgi.cleanUpPostDataState] A basic program using the lower-level api might look like: --- import arsd.cgi; // you write a request handler which always takes a Cgi object void handler(Cgi cgi) { /+ when the user goes to your site, suppose you are being hosted at http://example.com/yourapp If the user goes to http://example.com/yourapp/test?name=value then the url will be parsed out into the following pieces: cgi.pathInfo == "/test". This is everything after yourapp's name. (If you are doing an embedded http server, your app's name is blank, so pathInfo will be the whole path of the url.) cgi.scriptName == "yourapp". With an embedded http server, this will be blank. cgi.host == "example.com" cgi.https == false cgi.queryString == "name=value" (there's also cgi.search, which will be "?name=value", including the ?) The query string is further parsed into the `get` and `getArray` members, so: cgi.get == ["name": "value"], meaning you can do `cgi.get["name"] == "value"` And cgi.getArray == ["name": ["value"]]. Why is there both `get` and `getArray`? The standard allows names to be repeated. This can be very useful, it is how http forms naturally pass multiple items like a set of checkboxes. So `getArray` is the complete data if you need it. But since so often you only care about one value, the `get` member provides more convenient access. We can use these members to process the request and build link urls. Other info from the request are in other members, we'll look at them later. +/ switch(cgi.pathInfo) { // the home page will be a small html form that can set a cookie. case "/": cgi.write(`

`, true); // the , true tells it that this is the one, complete response i want to send, allowing some optimizations. break; // POSTing to this will set a cookie with our submitted name case "/set-cookie": // HTTP has a number of request methods (also called "verbs") to tell // what you should do with the given resource. // The most common are GET and POST, the ones used in html forms. // You can check which one was used with the `cgi.requestMethod` property. if(cgi.requestMethod == Cgi.RequestMethod.POST) { // headers like redirections need to be set before we call `write` cgi.setResponseLocation("read-cookie"); // just like how url params go into cgi.get/getArray, form data submitted in a POST // body go to cgi.post/postArray. Please note that a POST request can also have get // params in addition to post params. // // There's also a convenience function `cgi.request("name")` which checks post first, // then get if it isn't found there, and then returns a default value if it is in neither. if("name" in cgi.post) { // we can set cookies with a method too // again, cookies need to be set before calling `cgi.write`, since they // are a kind of header. cgi.setCookie("name" , cgi.post["name"]); } // the user will probably never see this, since the response location // is an automatic redirect, but it is still best to say something anyway cgi.write("Redirecting you to see the cookie...", true); } else { // you can write out response codes and headers // as well as response bodies // // But always check the cgi docs before using the generic // `header` method - if there is a specific method for your // header, use it before resorting to the generic one to avoid // a header value from being sent twice. cgi.setResponseLocation("405 Method Not Allowed"); // there is no special accept member, so you can use the generic header function cgi.header("Accept: POST"); // but content type does have a method, so prefer to use it: cgi.setResponseContentType("text/plain"); // all the headers are buffered, and will be sent upon the first body // write. you can actually modify some of them before sending if need be. cgi.write("You must use the POST http verb on this resource.", true); } break; // and GETting this will read the cookie back out case "/read-cookie": // I did NOT pass `,true` here because this is writing a partial response. // It is possible to stream data to the user in chunks by writing partial // responses the calling `cgi.flush();` to send the partial response immediately. // normally, you'd only send partial chunks if you have to - it is better to build // a response as a whole and send it as a whole whenever possible - but here I want // to demo that you can. cgi.write("Hello, "); if("name" in cgi.cookies) { import arsd.dom; // dom.d provides a lot of helpers for html // since the cookie is set, we need to write it out properly to // avoid cross-site scripting attacks. // // Getting this stuff right automatically is a benefit of using the higher // level apis, but this demo is to show the fundamental building blocks, so // we're responsible to take care of it. cgi.write(htmlEntitiesEncode(cgi.cookies["name"])); } else { cgi.write("friend"); } // note that I never called cgi.setResponseContentType, since the default is text/html. // it doesn't hurt to do it explicitly though, just remember to do it before any cgi.write // calls. break; default: // no path matched cgi.setResponseStatus("404 Not Found"); cgi.write("Resource not found.", true); } } // and this adds the boilerplate to set up a server according to the // compile version configuration and call your handler as requests come in mixin GenericMain!handler; // the `handler` here is the name of your function --- Even if you plan to always use the higher-level apis, I still recommend you at least familiarize yourself with the lower level functions, since they provide the lightest weight, most flexible options to get down to business if you ever need them. In the lower-level api, the [Cgi] object represents your HTTP transaction. It has functions to describe the request and for you to send your response. It leaves the details of how you o it up to you. The general guideline though is to avoid depending any variables outside your handler function, since there's no guarantee they will survive to another handler. You can use global vars as a lazy initialized cache, but you should always be ready in case it is empty. (One exception: if you use `-version=embedded_httpd_threads -version=cgi_no_fork`, then you can rely on it more, but you should still really write things assuming your function won't have anything survive beyond its return for max scalability and compatibility.) A basic program using the higher-level apis might look like: --- /+ import arsd.cgi; struct LoginData { string currentUser; } class AppClass : WebObject { string foo() {} } mixin DispatcherMain!( "/assets/.serveStaticFileDirectory("assets/", true), // serve the files in the assets subdirectory "/".serveApi!AppClass, "/thing/".serveRestObject, ); +/ --- Guide_for_PHP_users: (Please note: I wrote this section in 2008. A lot of PHP hosts still ran 4.x back then, so it was common to avoid using classes - introduced in php 5 - to maintain compatibility! If you're coming from php more recently, this may not be relevant anymore, but still might help you.) If you are coming from old-style PHP, here's a quick guide to help you get started: $(SIDE_BY_SIDE $(COLUMN ```php ``` ) $(COLUMN --- import arsd.cgi; void app(Cgi cgi) { string foo = cgi.post["foo"]; string bar = cgi.get["bar"]; string baz = cgi.cookies["baz"]; string user_ip = cgi.remoteAddress; string host = cgi.host; string path = cgi.pathInfo; cgi.setCookie("baz", "some value"); cgi.write("hello!"); } mixin GenericMain!app --- ) ) $(H3 Array elements) In PHP, you can give a form element a name like `"something[]"`, and then `$_POST["something"]` gives an array. In D, you can use whatever name you want, and access an array of values with the `cgi.getArray["name"]` and `cgi.postArray["name"]` members. $(H3 Databases) PHP has a lot of stuff in its standard library. cgi.d doesn't include most of these, but the rest of my arsd repository has much of it. For example, to access a MySQL database, download `database.d` and `mysql.d` from my github repo, and try this code (assuming, of course, your database is set up): --- import arsd.cgi; import arsd.mysql; void app(Cgi cgi) { auto database = new MySql("localhost", "username", "password", "database_name"); foreach(row; mysql.query("SELECT count(id) FROM people")) cgi.write(row[0] ~ " people in database"); } mixin GenericMain!app; --- Similar modules are available for PostgreSQL, Microsoft SQL Server, and SQLite databases, implementing the same basic interface. See_Also: You may also want to see [arsd.dom], [arsd.webtemplate], and maybe some functions from my old [arsd.html] for more code for making web applications. dom and webtemplate are used by the higher-level api here in cgi.d. For working with json, try [arsd.jsvar]. [arsd.database], [arsd.mysql], [arsd.postgres], [arsd.mssql], and [arsd.sqlite] can help in accessing databases. If you are looking to access a web application via HTTP, try [arsd.http2]. Copyright: cgi.d copyright 2008-2023, Adam D. Ruppe. Provided under the Boost Software License. Yes, this file is old, and yes, it is still actively maintained and used. +/ module arsd.cgi; // FIXME: Nullable!T can be a checkbox that enables/disables the T on the automatic form // and a SumType!(T, R) can be a radio box to pick between T and R to disclose the extra boxes on the automatic form /++ This micro-example uses the [dispatcher] api to act as a simple http file server, serving files found in the current directory and its children. +/ unittest { import arsd.cgi; mixin DispatcherMain!( "/".serveStaticFileDirectory(null, true) ); } /++ Same as the previous example, but written out long-form without the use of [DispatcherMain] nor [GenericMain]. +/ unittest { import arsd.cgi; void requestHandler(Cgi cgi) { cgi.dispatcher!( "/".serveStaticFileDirectory(null, true) ); } // mixin GenericMain!requestHandler would add this function: void main(string[] args) { // this is all the content of [cgiMainImpl] which you can also call // cgi.d embeds a few add on functions like real time event forwarders // and session servers it can run in other processes. this spawns them, if needed. if(tryAddonServers(args)) return; // cgi.d allows you to easily simulate http requests from the command line, // without actually starting a server. this function will do that. if(trySimulatedRequest!(requestHandler, Cgi)(args)) return; RequestServer server; // you can change the default port here if you like // server.listeningPort = 9000; // then call this to let the command line args override your default server.configureFromCommandLine(args); // here is where you could print out the listeningPort to the user if you wanted // and serve the request(s) according to the compile configuration server.serve!(requestHandler)(); // or you could explicitly choose a serve mode like this: // server.serveEmbeddedHttp!requestHandler(); } } /++ cgi.d has built-in testing helpers too. These will provide mock requests and mock sessions that otherwise run through the rest of the internal mechanisms to call your functions without actually spinning up a server. +/ unittest { import arsd.cgi; void requestHandler(Cgi cgi) { } // D doesn't let me embed a unittest inside an example unittest // so this is a function, but you can do it however in your real program /* unittest */ void runTests() { auto tester = new CgiTester(&requestHandler); auto response = tester.GET("/"); assert(response.code == 200); } } static import std.file; // for a single thread, linear request thing, use: // -version=embedded_httpd_threads -version=cgi_no_threads version(Posix) { version(CRuntime_Musl) { } else version(minimal) { } else { version(GNU) { // GDC doesn't support static foreach so I had to cheat on it :( } else version(FreeBSD) { // I never implemented the fancy stuff there either } else version(OpenBSD) { // Fix issue #2977 - adopt same approach as FreeBSD above } else { version=with_breaking_cgi_features; version=with_sendfd; version=with_addon_servers; } } } version(Windows) { version(minimal) { } else { // not too concerned about gdc here since the mingw version is fairly new as well version=with_breaking_cgi_features; } } void cloexec(int fd) { version(Posix) { import core.sys.posix.fcntl; fcntl(fd, F_SETFD, FD_CLOEXEC); } } void cloexec(Socket s) { version(Posix) { import core.sys.posix.fcntl; fcntl(s.handle, F_SETFD, FD_CLOEXEC); } } version(embedded_httpd_hybrid) { version=embedded_httpd_threads; version(cgi_no_fork) {} else version(Posix) version=cgi_use_fork; version=cgi_use_fiber; } version(cgi_use_fork) enum cgi_use_fork_default = true; else enum cgi_use_fork_default = false; // the servers must know about the connections to talk to them; the interfaces are vital version(with_addon_servers) version=with_addon_servers_connections; version(embedded_httpd) { version(linux) version=embedded_httpd_processes; else { version=embedded_httpd_threads; } /* version(with_openssl) { pragma(lib, "crypto"); pragma(lib, "ssl"); } */ } version(embedded_httpd_processes) version=embedded_httpd_processes_accept_after_fork; // I am getting much better average performance on this, so just keeping it. But the other way MIGHT help keep the variation down so i wanna keep the code to play with later version(embedded_httpd_threads) { // unless the user overrides the default.. version(cgi_session_server_process) {} else version=cgi_embedded_sessions; } version(scgi) { // unless the user overrides the default.. version(cgi_session_server_process) {} else version=cgi_embedded_sessions; } // fall back if the other is not defined so we can cleanly version it below version(cgi_embedded_sessions) {} else version=cgi_session_server_process; version=cgi_with_websocket; enum long defaultMaxContentLength = 5_000_000; /* To do a file download offer in the browser: cgi.setResponseContentType("text/csv"); cgi.header("Content-Disposition: attachment; filename=\"customers.csv\""); */ // FIXME: the location header is supposed to be an absolute url I guess. // FIXME: would be cool to flush part of a dom document before complete // somehow in here and dom.d. // these are public so you can mixin GenericMain. // FIXME: use a function level import instead! public import std.string; public import std.stdio; public import std.conv; import std.concurrency; import std.uri; import std.uni; import std.algorithm.comparison; import std.algorithm.searching; import std.exception; import std.base64; static import std.algorithm; import std.datetime; import std.range; import std.process; import std.zlib; T[] consume(T)(T[] range, int count) { if(count > range.length) count = range.length; return range[count..$]; } int locationOf(T)(T[] data, string item) { const(ubyte[]) d = cast(const(ubyte[])) data; const(ubyte[]) i = cast(const(ubyte[])) item; // this is a vague sanity check to ensure we aren't getting insanely // sized input that will infinite loop below. it should never happen; // even huge file uploads ought to come in smaller individual pieces. if(d.length > (int.max/2)) throw new Exception("excessive block of input"); for(int a = 0; a < d.length; a++) { if(a + i.length > d.length) return -1; if(d[a..a+i.length] == i) return a; } return -1; } /// If you are doing a custom cgi class, mixing this in can take care of /// the required constructors for you mixin template ForwardCgiConstructors() { this(long maxContentLength = defaultMaxContentLength, string[string] env = null, const(ubyte)[] delegate() readdata = null, void delegate(const(ubyte)[]) _rawDataOutput = null, void delegate() _flush = null ) { super(maxContentLength, env, readdata, _rawDataOutput, _flush); } this(string[] args) { super(args); } this( BufferedInputRange inputData, string address, ushort _port, int pathInfoStarts = 0, bool _https = false, void delegate(const(ubyte)[]) _rawDataOutput = null, void delegate() _flush = null, // this pointer tells if the connection is supposed to be closed after we handle this bool* closeConnection = null) { super(inputData, address, _port, pathInfoStarts, _https, _rawDataOutput, _flush, closeConnection); } this(BufferedInputRange ir, bool* closeConnection) { super(ir, closeConnection); } } /// thrown when a connection is closed remotely while we waiting on data from it class ConnectionClosedException : Exception { this(string message, string file = __FILE__, size_t line = __LINE__, Throwable next = null) { super(message, file, line, next); } } version(Windows) { // FIXME: ugly hack to solve stdin exception problems on Windows: // reading stdin results in StdioException (Bad file descriptor) // this is probably due to https://issues.dlang.org/show_bug.cgi?id=3425 private struct stdin { struct ByChunk { // Replicates std.stdio.ByChunk private: ubyte[] chunk_; public: this(size_t size) in { assert(size, "size must be larger than 0"); } do { chunk_ = new ubyte[](size); popFront(); } @property bool empty() const { return !std.stdio.stdin.isOpen || std.stdio.stdin.eof; // Ugly, but seems to do the job } @property nothrow ubyte[] front() { return chunk_; } void popFront() { enforce(!empty, "Cannot call popFront on empty range"); chunk_ = stdin.rawRead(chunk_); } } import core.sys.windows.windows; static: T[] rawRead(T)(T[] buf) { uint bytesRead; auto result = ReadFile(GetStdHandle(STD_INPUT_HANDLE), buf.ptr, cast(int) (buf.length * T.sizeof), &bytesRead, null); if (!result) { auto err = GetLastError(); if (err == 38/*ERROR_HANDLE_EOF*/ || err == 109/*ERROR_BROKEN_PIPE*/) // 'good' errors meaning end of input return buf[0..0]; // Some other error, throw it char* buffer; scope(exit) LocalFree(buffer); // FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100 // FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000 FormatMessageA(0x1100, null, err, 0, cast(char*)&buffer, 256, null); throw new Exception(to!string(buffer)); } enforce(!(bytesRead % T.sizeof), "I/O error"); return buf[0..bytesRead / T.sizeof]; } auto byChunk(size_t sz) { return ByChunk(sz); } void close() { std.stdio.stdin.close; } } } /// The main interface with the web request class Cgi { public: /// the methods a request can be enum RequestMethod { GET, HEAD, POST, PUT, DELETE, // GET and POST are the ones that really work // these are defined in the standard, but idk if they are useful for anything OPTIONS, TRACE, CONNECT, // These seem new, I have only recently seen them PATCH, MERGE, // this is an extension for when the method is not specified and you want to assume CommandLine } /+ /++ Cgi provides a per-request memory pool +/ void[] allocateMemory(size_t nBytes) { } /// ditto void[] reallocateMemory(void[] old, size_t nBytes) { } /// ditto void freeMemory(void[] memory) { } +/ /* import core.runtime; auto args = Runtime.args(); we can call the app a few ways: 1) set up the environment variables and call the app (manually simulating CGI) 2) simulate a call automatically: ./app method 'uri' for example: ./app get /path?arg arg2=something Anything on the uri is treated as query string etc on get method, further args are appended to the query string (encoded automatically) on post method, further args are done as post @name means import from file "name". if name == -, it uses stdin (so info=@- means set info to the value of stdin) Other arguments include: --cookie name=value (these are all concated together) --header 'X-Something: cool' --referrer 'something' --port 80 --remote-address some.ip.address.here --https yes --user-agent 'something' --userpass 'user:pass' --authorization 'Basic base64encoded_user:pass' --accept 'content' // FIXME: better example --last-event-id 'something' --host 'something.com' Non-simulation arguments: --port xxx listening port for non-cgi things (valid for the cgi interfaces) --listening-host the ip address the application should listen on, or if you want to use unix domain sockets, it is here you can set them: `--listening-host unix:filename` or, on Linux, `--listening-host abstract:name`. */ /** Initializes it with command line arguments (for easy testing) */ this(string[] args, void delegate(const(ubyte)[]) _rawDataOutput = null) { rawDataOutput = _rawDataOutput; // these are all set locally so the loop works // without triggering errors in dmd 2.064 // we go ahead and set them at the end of it to the this version int port; string referrer; string remoteAddress; string userAgent; string authorization; string origin; string accept; string lastEventId; bool https; string host; RequestMethod requestMethod; string requestUri; string pathInfo; string queryString; bool lookingForMethod; bool lookingForUri; string nextArgIs; string _cookie; string _queryString; string[][string] _post; string[string] _headers; string[] breakUp(string s) { string k, v; auto idx = s.indexOf("="); if(idx == -1) { k = s; } else { k = s[0 .. idx]; v = s[idx + 1 .. $]; } return [k, v]; } lookingForMethod = true; scriptName = args[0]; scriptFileName = args[0]; environmentVariables = cast(const) environment.toAA; foreach(arg; args[1 .. $]) { if(arg.startsWith("--")) { nextArgIs = arg[2 .. $]; } else if(nextArgIs.length) { if (nextArgIs == "cookie") { auto info = breakUp(arg); if(_cookie.length) _cookie ~= "; "; _cookie ~= std.uri.encodeComponent(info[0]) ~ "=" ~ std.uri.encodeComponent(info[1]); } else if (nextArgIs == "port") { port = to!int(arg); } else if (nextArgIs == "referrer") { referrer = arg; } else if (nextArgIs == "remote-address") { remoteAddress = arg; } else if (nextArgIs == "user-agent") { userAgent = arg; } else if (nextArgIs == "authorization") { authorization = arg; } else if (nextArgIs == "userpass") { authorization = "Basic " ~ Base64.encode(cast(immutable(ubyte)[]) (arg)).idup; } else if (nextArgIs == "origin") { origin = arg; } else if (nextArgIs == "accept") { accept = arg; } else if (nextArgIs == "last-event-id") { lastEventId = arg; } else if (nextArgIs == "https") { if(arg == "yes") https = true; } else if (nextArgIs == "header") { string thing, other; auto idx = arg.indexOf(":"); if(idx == -1) throw new Exception("need a colon in a http header"); thing = arg[0 .. idx]; other = arg[idx + 1.. $]; _headers[thing.strip.toLower()] = other.strip; } else if (nextArgIs == "host") { host = arg; } // else // skip, we don't know it but that's ok, it might be used elsewhere so no error nextArgIs = null; } else if(lookingForMethod) { lookingForMethod = false; lookingForUri = true; if(arg.asLowerCase().equal("commandline")) requestMethod = RequestMethod.CommandLine; else requestMethod = to!RequestMethod(arg.toUpper()); } else if(lookingForUri) { lookingForUri = false; requestUri = arg; auto idx = arg.indexOf("?"); if(idx == -1) pathInfo = arg; else { pathInfo = arg[0 .. idx]; _queryString = arg[idx + 1 .. $]; } } else { // it is an argument of some sort if(requestMethod == Cgi.RequestMethod.POST || requestMethod == Cgi.RequestMethod.PATCH || requestMethod == Cgi.RequestMethod.PUT || requestMethod == Cgi.RequestMethod.CommandLine) { auto parts = breakUp(arg); _post[parts[0]] ~= parts[1]; allPostNamesInOrder ~= parts[0]; allPostValuesInOrder ~= parts[1]; } else { if(_queryString.length) _queryString ~= "&"; auto parts = breakUp(arg); _queryString ~= std.uri.encodeComponent(parts[0]) ~ "=" ~ std.uri.encodeComponent(parts[1]); } } } acceptsGzip = false; keepAliveRequested = false; requestHeaders = cast(immutable) _headers; cookie = _cookie; cookiesArray = getCookieArray(); cookies = keepLastOf(cookiesArray); queryString = _queryString; getArray = cast(immutable) decodeVariables(queryString, "&", &allGetNamesInOrder, &allGetValuesInOrder); get = keepLastOf(getArray); postArray = cast(immutable) _post; post = keepLastOf(_post); // FIXME filesArray = null; files = null; isCalledWithCommandLineArguments = true; this.port = port; this.referrer = referrer; this.remoteAddress = remoteAddress; this.userAgent = userAgent; this.authorization = authorization; this.origin = origin; this.accept = accept; this.lastEventId = lastEventId; this.https = https; this.host = host; this.requestMethod = requestMethod; this.requestUri = requestUri; this.pathInfo = pathInfo; this.queryString = queryString; this.postBody = null; } private { string[] allPostNamesInOrder; string[] allPostValuesInOrder; string[] allGetNamesInOrder; string[] allGetValuesInOrder; } CgiConnectionHandle getOutputFileHandle() { return _outputFileHandle; } CgiConnectionHandle _outputFileHandle = INVALID_CGI_CONNECTION_HANDLE; /** Initializes it using a CGI or CGI-like interface */ this(long maxContentLength = defaultMaxContentLength, // use this to override the environment variable listing in string[string] env = null, // and this should return a chunk of data. return empty when done const(ubyte)[] delegate() readdata = null, // finally, use this to do custom output if needed void delegate(const(ubyte)[]) _rawDataOutput = null, // to flush the custom output void delegate() _flush = null ) { // these are all set locally so the loop works // without triggering errors in dmd 2.064 // we go ahead and set them at the end of it to the this version int port; string referrer; string remoteAddress; string userAgent; string authorization; string origin; string accept; string lastEventId; bool https; string host; RequestMethod requestMethod; string requestUri; string pathInfo; string queryString; isCalledWithCommandLineArguments = false; rawDataOutput = _rawDataOutput; flushDelegate = _flush; auto getenv = delegate string(string var) { if(env is null) return std.process.environment.get(var); auto e = var in env; if(e is null) return null; return *e; }; environmentVariables = env is null ? cast(const) environment.toAA : env; // fetching all the request headers string[string] requestHeadersHere; foreach(k, v; env is null ? cast(const) environment.toAA() : env) { if(k.startsWith("HTTP_")) { requestHeadersHere[replace(k["HTTP_".length .. $].toLower(), "_", "-")] = v; } } this.requestHeaders = assumeUnique(requestHeadersHere); requestUri = getenv("REQUEST_URI"); cookie = getenv("HTTP_COOKIE"); cookiesArray = getCookieArray(); cookies = keepLastOf(cookiesArray); referrer = getenv("HTTP_REFERER"); userAgent = getenv("HTTP_USER_AGENT"); remoteAddress = getenv("REMOTE_ADDR"); host = getenv("HTTP_HOST"); pathInfo = getenv("PATH_INFO"); queryString = getenv("QUERY_STRING"); scriptName = getenv("SCRIPT_NAME"); { import core.runtime; auto sfn = getenv("SCRIPT_FILENAME"); scriptFileName = sfn.length ? sfn : (Runtime.args.length ? Runtime.args[0] : null); } bool iis = false; // Because IIS doesn't pass requestUri, we simulate it here if it's empty. if(requestUri.length == 0) { // IIS sometimes includes the script name as part of the path info - we don't want that if(pathInfo.length >= scriptName.length && (pathInfo[0 .. scriptName.length] == scriptName)) pathInfo = pathInfo[scriptName.length .. $]; requestUri = scriptName ~ pathInfo ~ (queryString.length ? ("?" ~ queryString) : ""); iis = true; // FIXME HACK - used in byChunk below - see bugzilla 6339 // FIXME: this works for apache and iis... but what about others? } auto ugh = decodeVariables(queryString, "&", &allGetNamesInOrder, &allGetValuesInOrder); getArray = assumeUnique(ugh); get = keepLastOf(getArray); // NOTE: on apache, you need to specifically forward this authorization = getenv("HTTP_AUTHORIZATION"); // this is a hack because Apache is a shitload of fuck and // refuses to send the real header to us. Compatible // programs should send both the standard and X- versions // NOTE: if you have access to .htaccess or httpd.conf, you can make this // unnecessary with mod_rewrite, so it is commented //if(authorization.length == 0) // if the std is there, use it // authorization = getenv("HTTP_X_AUTHORIZATION"); // the REDIRECT_HTTPS check is here because with an Apache hack, the port can become wrong if(getenv("SERVER_PORT").length && getenv("REDIRECT_HTTPS") != "on") port = to!int(getenv("SERVER_PORT")); else port = 0; // this was probably called from the command line auto ae = getenv("HTTP_ACCEPT_ENCODING"); if(ae.length && ae.indexOf("gzip") != -1) acceptsGzip = true; accept = getenv("HTTP_ACCEPT"); lastEventId = getenv("HTTP_LAST_EVENT_ID"); auto ka = getenv("HTTP_CONNECTION"); if(ka.length && ka.asLowerCase().canFind("keep-alive")) keepAliveRequested = true; auto or = getenv("HTTP_ORIGIN"); origin = or; auto rm = getenv("REQUEST_METHOD"); if(rm.length) requestMethod = to!RequestMethod(getenv("REQUEST_METHOD")); else requestMethod = RequestMethod.CommandLine; // FIXME: hack on REDIRECT_HTTPS; this is there because the work app uses mod_rewrite which loses the https flag! So I set it with [E=HTTPS=%HTTPS] or whatever but then it gets translated to here so i want it to still work. This is arguably wrong but meh. https = (getenv("HTTPS") == "on" || getenv("REDIRECT_HTTPS") == "on"); // FIXME: DOCUMENT_ROOT? // FIXME: what about PUT? if(requestMethod == RequestMethod.POST || requestMethod == Cgi.RequestMethod.PATCH || requestMethod == Cgi.RequestMethod.PUT || requestMethod == Cgi.RequestMethod.CommandLine) { version(preserveData) // a hack to make forwarding simpler immutable(ubyte)[] data; size_t amountReceived = 0; auto contentType = getenv("CONTENT_TYPE"); // FIXME: is this ever not going to be set? I guess it depends // on if the server de-chunks and buffers... seems like it has potential // to be slow if they did that. The spec says it is always there though. // And it has worked reliably for me all year in the live environment, // but some servers might be different. auto cls = getenv("CONTENT_LENGTH"); auto contentLength = to!size_t(cls.length ? cls : "0"); immutable originalContentLength = contentLength; if(contentLength) { if(maxContentLength > 0 && contentLength > maxContentLength) { setResponseStatus("413 Request entity too large"); write("You tried to upload a file that is too large."); close(); throw new Exception("POST too large"); } prepareForIncomingDataChunks(contentType, contentLength); int processChunk(in ubyte[] chunk) { if(chunk.length > contentLength) { handleIncomingDataChunk(chunk[0..contentLength]); amountReceived += contentLength; contentLength = 0; return 1; } else { handleIncomingDataChunk(chunk); contentLength -= chunk.length; amountReceived += chunk.length; } if(contentLength == 0) return 1; onRequestBodyDataReceived(amountReceived, originalContentLength); return 0; } if(readdata is null) { foreach(ubyte[] chunk; stdin.byChunk(iis ? contentLength : 4096)) if(processChunk(chunk)) break; } else { // we have a custom data source.. auto chunk = readdata(); while(chunk.length) { if(processChunk(chunk)) break; chunk = readdata(); } } onRequestBodyDataReceived(amountReceived, originalContentLength); postArray = assumeUnique(pps._post); filesArray = assumeUnique(pps._files); files = keepLastOf(filesArray); post = keepLastOf(postArray); this.postBody = pps.postBody; cleanUpPostDataState(); } version(preserveData) originalPostData = data; } // fixme: remote_user script name this.port = port; this.referrer = referrer; this.remoteAddress = remoteAddress; this.userAgent = userAgent; this.authorization = authorization; this.origin = origin; this.accept = accept; this.lastEventId = lastEventId; this.https = https; this.host = host; this.requestMethod = requestMethod; this.requestUri = requestUri; this.pathInfo = pathInfo; this.queryString = queryString; } /// Cleans up any temporary files. Do not use the object /// after calling this. /// /// NOTE: it is called automatically by GenericMain // FIXME: this should be called if the constructor fails too, if it has created some garbage... void dispose() { foreach(file; files) { if(!file.contentInMemory) if(std.file.exists(file.contentFilename)) std.file.remove(file.contentFilename); } } private { struct PostParserState { string contentType; string boundary; string localBoundary; // the ones used at the end or something lol bool isMultipart; bool needsSavedBody; ulong expectedLength; ulong contentConsumed; immutable(ubyte)[] buffer; // multipart parsing state int whatDoWeWant; bool weHaveAPart; string[] thisOnesHeaders; immutable(ubyte)[] thisOnesData; string postBody; UploadedFile piece; bool isFile = false; size_t memoryCommitted; // do NOT keep mutable references to these anywhere! // I assume they are unique in the constructor once we're all done getting data. string[][string] _post; UploadedFile[][string] _files; } PostParserState pps; } /// This represents a file the user uploaded via a POST request. static struct UploadedFile { /// If you want to create one of these structs for yourself from some data, /// use this function. static UploadedFile fromData(immutable(void)[] data, string name = null) { Cgi.UploadedFile f; f.filename = name; f.content = cast(immutable(ubyte)[]) data; f.contentInMemory = true; return f; } string name; /// The name of the form element. string filename; /// The filename the user set. string contentType; /// The MIME type the user's browser reported. (Not reliable.) /** For small files, cgi.d will buffer the uploaded file in memory, and make it directly accessible to you through the content member. I find this very convenient and somewhat efficient, since it can avoid hitting the disk entirely. (I often want to inspect and modify the file anyway!) I find the file is very large, it is undesirable to eat that much memory just for a file buffer. In those cases, if you pass a large enough value for maxContentLength to the constructor so they are accepted, cgi.d will write the content to a temporary file that you can re-read later. You can override this behavior by subclassing Cgi and overriding the protected handlePostChunk method. Note that the object is not initialized when you write that method - the http headers are available, but the cgi.post method is not. You may parse the file as it streams in using this method. Anyway, if the file is small enough to be in memory, contentInMemory will be set to true, and the content is available in the content member. If not, contentInMemory will be set to false, and the content saved in a file, whose name will be available in the contentFilename member. Tip: if you know you are always dealing with small files, and want the convenience of ignoring this member, construct Cgi with a small maxContentLength. Then, if a large file comes in, it simply throws an exception (and HTTP error response) instead of trying to handle it. The default value of maxContentLength in the constructor is for small files. */ bool contentInMemory = true; // the default ought to always be true immutable(ubyte)[] content; /// The actual content of the file, if contentInMemory == true string contentFilename; /// the file where we dumped the content, if contentInMemory == false. Note that if you want to keep it, you MUST move the file, since otherwise it is considered garbage when cgi is disposed. /// ulong fileSize() { if(contentInMemory) return content.length; import std.file; return std.file.getSize(contentFilename); } /// void writeToFile(string filenameToSaveTo) const { import std.file; if(contentInMemory) std.file.write(filenameToSaveTo, content); else std.file.rename(contentFilename, filenameToSaveTo); } } // given a content type and length, decide what we're going to do with the data.. protected void prepareForIncomingDataChunks(string contentType, ulong contentLength) { pps.expectedLength = contentLength; auto terminator = contentType.indexOf(";"); if(terminator == -1) terminator = contentType.length; pps.contentType = contentType[0 .. terminator]; auto b = contentType[terminator .. $]; if(b.length) { auto idx = b.indexOf("boundary="); if(idx != -1) { pps.boundary = b[idx + "boundary=".length .. $]; pps.localBoundary = "\r\n--" ~ pps.boundary; } } // while a content type SHOULD be sent according to the RFC, it is // not required. We're told we SHOULD guess by looking at the content // but it seems to me that this only happens when it is urlencoded. if(pps.contentType == "application/x-www-form-urlencoded" || pps.contentType == "") { pps.isMultipart = false; pps.needsSavedBody = false; } else if(pps.contentType == "multipart/form-data") { pps.isMultipart = true; enforce(pps.boundary.length, "no boundary"); } else if(pps.contentType == "text/xml") { // FIXME: could this be special and load the post params // save the body so the application can handle it pps.isMultipart = false; pps.needsSavedBody = true; } else if(pps.contentType == "application/json") { // FIXME: this could prolly try to load post params too // save the body so the application can handle it pps.needsSavedBody = true; pps.isMultipart = false; } else { // the rest is 100% handled by the application. just save the body and send it to them pps.needsSavedBody = true; pps.isMultipart = false; } } // handles streaming POST data. If you handle some other content type, you should // override this. If the data isn't the content type you want, you ought to call // super.handleIncomingDataChunk so regular forms and files still work. // FIXME: I do some copying in here that I'm pretty sure is unnecessary, and the // file stuff I'm sure is inefficient. But, my guess is the real bottleneck is network // input anyway, so I'm not going to get too worked up about it right now. protected void handleIncomingDataChunk(const(ubyte)[] chunk) { if(chunk.length == 0) return; assert(chunk.length <= 32 * 1024 * 1024); // we use chunk size as a memory constraint thing, so // if we're passed big chunks, it might throw unnecessarily. // just pass it smaller chunks at a time. if(pps.isMultipart) { // multipart/form-data // FIXME: this might want to be factored out and factorized // need to make sure the stream hooks actually work. void pieceHasNewContent() { // we just grew the piece's buffer. Do we have to switch to file backing? if(pps.piece.contentInMemory) { if(pps.piece.content.length <= 10 * 1024 * 1024) // meh, I'm ok with it. return; else { // this is too big. if(!pps.isFile) throw new Exception("Request entity too large"); // a variable this big is kinda ridiculous, just reject it. else { // a file this large is probably acceptable though... let's use a backing file. pps.piece.contentInMemory = false; // FIXME: say... how do we intend to delete these things? cgi.dispose perhaps. int count = 0; pps.piece.contentFilename = getTempDirectory() ~ "arsd_cgi_uploaded_file_" ~ to!string(getUtcTime()) ~ "-" ~ to!string(count); // odds are this loop will never be entered, but we want it just in case. while(std.file.exists(pps.piece.contentFilename)) { count++; pps.piece.contentFilename = getTempDirectory() ~ "arsd_cgi_uploaded_file_" ~ to!string(getUtcTime()) ~ "-" ~ to!string(count); } // I hope this creates the file pretty quickly, or the loop might be useless... // FIXME: maybe I should write some kind of custom transaction here. std.file.write(pps.piece.contentFilename, pps.piece.content); pps.piece.content = null; } } } else { // it's already in a file, so just append it to what we have if(pps.piece.content.length) { // FIXME: this is surely very inefficient... we'll be calling this by 4kb chunk... std.file.append(pps.piece.contentFilename, pps.piece.content); pps.piece.content = null; } } } void commitPart() { if(!pps.weHaveAPart) return; pieceHasNewContent(); // be sure the new content is handled every time if(pps.isFile) { // I'm not sure if other environments put files in post or not... // I used to not do it, but I think I should, since it is there... pps._post[pps.piece.name] ~= pps.piece.filename; pps._files[pps.piece.name] ~= pps.piece; allPostNamesInOrder ~= pps.piece.name; allPostValuesInOrder ~= pps.piece.filename; } else { pps._post[pps.piece.name] ~= cast(string) pps.piece.content; allPostNamesInOrder ~= pps.piece.name; allPostValuesInOrder ~= cast(string) pps.piece.content; } /* stderr.writeln("RECEIVED: ", pps.piece.name, "=", pps.piece.content.length < 1000 ? to!string(pps.piece.content) : "too long"); */ // FIXME: the limit here pps.memoryCommitted += pps.piece.content.length; pps.weHaveAPart = false; pps.whatDoWeWant = 1; pps.thisOnesHeaders = null; pps.thisOnesData = null; pps.piece = UploadedFile.init; pps.isFile = false; } void acceptChunk() { pps.buffer ~= chunk; chunk = null; // we've consumed it into the buffer, so keeping it just brings confusion } immutable(ubyte)[] consume(size_t howMuch) { pps.contentConsumed += howMuch; auto ret = pps.buffer[0 .. howMuch]; pps.buffer = pps.buffer[howMuch .. $]; return ret; } dataConsumptionLoop: do { switch(pps.whatDoWeWant) { default: assert(0); case 0: acceptChunk(); // the format begins with two extra leading dashes, then we should be at the boundary if(pps.buffer.length < 2) return; assert(pps.buffer[0] == '-', "no leading dash"); consume(1); assert(pps.buffer[0] == '-', "no second leading dash"); consume(1); pps.whatDoWeWant = 1; goto case 1; /* fallthrough */ case 1: // looking for headers // here, we should be lined up right at the boundary, which is followed by a \r\n // want to keep the buffer under control in case we're under attack //stderr.writeln("here once"); //if(pps.buffer.length + chunk.length > 70 * 1024) // they should be < 1 kb really.... // throw new Exception("wtf is up with the huge mime part headers"); acceptChunk(); if(pps.buffer.length < pps.boundary.length) return; // not enough data, since there should always be a boundary here at least if(pps.contentConsumed + pps.boundary.length + 6 == pps.expectedLength) { assert(pps.buffer.length == pps.boundary.length + 4 + 2); // --, --, and \r\n // we *should* be at the end here! assert(pps.buffer[0] == '-'); consume(1); assert(pps.buffer[0] == '-'); consume(1); // the message is terminated by --BOUNDARY--\r\n (after a \r\n leading to the boundary) assert(pps.buffer[0 .. pps.boundary.length] == cast(const(ubyte[])) pps.boundary, "not lined up on boundary " ~ pps.boundary); consume(pps.boundary.length); assert(pps.buffer[0] == '-'); consume(1); assert(pps.buffer[0] == '-'); consume(1); assert(pps.buffer[0] == '\r'); consume(1); assert(pps.buffer[0] == '\n'); consume(1); assert(pps.buffer.length == 0); assert(pps.contentConsumed == pps.expectedLength); break dataConsumptionLoop; // we're done! } else { // we're not done yet. We should be lined up on a boundary. // But, we want to ensure the headers are here before we consume anything! auto headerEndLocation = locationOf(pps.buffer, "\r\n\r\n"); if(headerEndLocation == -1) return; // they *should* all be here, so we can handle them all at once. assert(pps.buffer[0 .. pps.boundary.length] == cast(const(ubyte[])) pps.boundary, "not lined up on boundary " ~ pps.boundary); consume(pps.boundary.length); // the boundary is always followed by a \r\n assert(pps.buffer[0] == '\r'); consume(1); assert(pps.buffer[0] == '\n'); consume(1); } // re-running since by consuming the boundary, we invalidate the old index. auto headerEndLocation = locationOf(pps.buffer, "\r\n\r\n"); assert(headerEndLocation >= 0, "no header"); auto thisOnesHeaders = pps.buffer[0..headerEndLocation]; consume(headerEndLocation + 4); // The +4 is the \r\n\r\n that caps it off pps.thisOnesHeaders = split(cast(string) thisOnesHeaders, "\r\n"); // now we'll parse the headers foreach(h; pps.thisOnesHeaders) { auto p = h.indexOf(":"); assert(p != -1, "no colon in header, got " ~ to!string(pps.thisOnesHeaders)); string hn = h[0..p]; string hv = h[p+2..$]; switch(hn.toLower) { default: assert(0); case "content-disposition": auto info = hv.split("; "); foreach(i; info[1..$]) { // skipping the form-data auto o = i.split("="); // FIXME string pn = o[0]; string pv = o[1][1..$-1]; if(pn == "name") { pps.piece.name = pv; } else if (pn == "filename") { pps.piece.filename = pv; pps.isFile = true; } } break; case "content-type": pps.piece.contentType = hv; break; } } pps.whatDoWeWant++; // move to the next step - the data break; case 2: // when we get here, pps.buffer should contain our first chunk of data if(pps.buffer.length + chunk.length > 8 * 1024 * 1024) // we might buffer quite a bit but not much throw new Exception("wtf is up with the huge mime part buffer"); acceptChunk(); // so the trick is, we want to process all the data up to the boundary, // but what if the chunk's end cuts the boundary off? If we're unsure, we // want to wait for the next chunk. We start by looking for the whole boundary // in the buffer somewhere. auto boundaryLocation = locationOf(pps.buffer, pps.localBoundary); // assert(boundaryLocation != -1, "should have seen "~to!string(cast(ubyte[]) pps.localBoundary)~" in " ~ to!string(pps.buffer)); if(boundaryLocation != -1) { // this is easy - we can see it in it's entirety! pps.piece.content ~= consume(boundaryLocation); assert(pps.buffer[0] == '\r'); consume(1); assert(pps.buffer[0] == '\n'); consume(1); assert(pps.buffer[0] == '-'); consume(1); assert(pps.buffer[0] == '-'); consume(1); // the boundary here is always preceded by \r\n--, which is why we used localBoundary instead of boundary to locate it. Cut that off. pps.weHaveAPart = true; pps.whatDoWeWant = 1; // back to getting headers for the next part commitPart(); // we're done here } else { // we can't see the whole thing, but what if there's a partial boundary? enforce(pps.localBoundary.length < 128); // the boundary ought to be less than a line... assert(pps.localBoundary.length > 1); // should already be sane but just in case bool potentialBoundaryFound = false; boundaryCheck: for(int a = 1; a < pps.localBoundary.length; a++) { // we grow the boundary a bit each time. If we think it looks the // same, better pull another chunk to be sure it's not the end. // Starting small because exiting the loop early is desirable, since // we're not keeping any ambiguity and 1 / 256 chance of exiting is // the best we can do. if(a > pps.buffer.length) break; // FIXME: is this right? assert(a <= pps.buffer.length); assert(a > 0); if(std.algorithm.endsWith(pps.buffer, pps.localBoundary[0 .. a])) { // ok, there *might* be a boundary here, so let's // not treat the end as data yet. The rest is good to // use though, since if there was a boundary there, we'd // have handled it up above after locationOf. pps.piece.content ~= pps.buffer[0 .. $ - a]; consume(pps.buffer.length - a); pieceHasNewContent(); potentialBoundaryFound = true; break boundaryCheck; } } if(!potentialBoundaryFound) { // we can consume the whole thing pps.piece.content ~= pps.buffer; pieceHasNewContent(); consume(pps.buffer.length); } else { // we found a possible boundary, but there was // insufficient data to be sure. assert(pps.buffer == cast(const(ubyte[])) pps.localBoundary[0 .. pps.buffer.length]); return; // wait for the next chunk. } } } } while(pps.buffer.length); // btw all boundaries except the first should have a \r\n before them } else { // application/x-www-form-urlencoded and application/json // not using maxContentLength because that might be cranked up to allow // large file uploads. We can handle them, but a huge post[] isn't any good. if(pps.buffer.length + chunk.length > 8 * 1024 * 1024) // surely this is plenty big enough throw new Exception("wtf is up with such a gigantic form submission????"); pps.buffer ~= chunk; // simple handling, but it works... until someone bombs us with gigabytes of crap at least... if(pps.buffer.length == pps.expectedLength) { if(pps.needsSavedBody) pps.postBody = cast(string) pps.buffer; else pps._post = decodeVariables(cast(string) pps.buffer, "&", &allPostNamesInOrder, &allPostValuesInOrder); version(preserveData) originalPostData = pps.buffer; } else { // just for debugging } } } protected void cleanUpPostDataState() { pps = PostParserState.init; } /// you can override this function to somehow react /// to an upload in progress. /// /// Take note that parts of the CGI object is not yet /// initialized! Stuff from HTTP headers, including get[], is usable. /// But, none of post[] is usable, and you cannot write here. That's /// why this method is const - mutating the object won't do much anyway. /// /// My idea here was so you can output a progress bar or /// something to a cooperative client (see arsd.rtud for a potential helper) /// /// The default is to do nothing. Subclass cgi and use the /// CustomCgiMain mixin to do something here. void onRequestBodyDataReceived(size_t receivedSoFar, size_t totalExpected) const { // This space intentionally left blank. } /// Initializes the cgi from completely raw HTTP data. The ir must have a Socket source. /// *closeConnection will be set to true if you should close the connection after handling this request this(BufferedInputRange ir, bool* closeConnection) { isCalledWithCommandLineArguments = false; import al = std.algorithm; immutable(ubyte)[] data; void rdo(const(ubyte)[] d) { //import std.stdio; writeln(d); sendAll(ir.source, d); } auto ira = ir.source.remoteAddress(); auto irLocalAddress = ir.source.localAddress(); ushort port = 80; if(auto ia = cast(InternetAddress) irLocalAddress) { port = ia.port; } else if(auto ia = cast(Internet6Address) irLocalAddress) { port = ia.port; } // that check for UnixAddress is to work around a Phobos bug // see: https://github.com/dlang/phobos/pull/7383 // but this might be more useful anyway tbh for this case version(Posix) this(ir, ira is null ? null : cast(UnixAddress) ira ? "unix:" : ira.toString(), port, 0, false, &rdo, null, closeConnection); else this(ir, ira is null ? null : ira.toString(), port, 0, false, &rdo, null, closeConnection); } /** Initializes it from raw HTTP request data. GenericMain uses this when you compile with -version=embedded_httpd. NOTE: If you are behind a reverse proxy, the values here might not be what you expect.... it will use X-Forwarded-For for remote IP and X-Forwarded-Host for host Params: inputData = the incoming data, including headers and other raw http data. When the constructor exits, it will leave this range exactly at the start of the next request on the connection (if there is one). address = the IP address of the remote user _port = the port number of the connection pathInfoStarts = the offset into the path component of the http header where the SCRIPT_NAME ends and the PATH_INFO begins. _https = if this connection is encrypted (note that the input data must not actually be encrypted) _rawDataOutput = delegate to accept response data. It should write to the socket or whatever; Cgi does all the needed processing to speak http. _flush = if _rawDataOutput buffers, this delegate should flush the buffer down the wire closeConnection = if the request asks to close the connection, *closeConnection == true. */ this( BufferedInputRange inputData, // string[] headers, immutable(ubyte)[] data, string address, ushort _port, int pathInfoStarts = 0, // use this if you know the script name, like if this is in a folder in a bigger web environment bool _https = false, void delegate(const(ubyte)[]) _rawDataOutput = null, void delegate() _flush = null, // this pointer tells if the connection is supposed to be closed after we handle this bool* closeConnection = null) { // these are all set locally so the loop works // without triggering errors in dmd 2.064 // we go ahead and set them at the end of it to the this version int port; string referrer; string remoteAddress; string userAgent; string authorization; string origin; string accept; string lastEventId; bool https; string host; RequestMethod requestMethod; string requestUri; string pathInfo; string queryString; string scriptName; string[string] get; string[][string] getArray; bool keepAliveRequested; bool acceptsGzip; string cookie; environmentVariables = cast(const) environment.toAA; idlol = inputData; isCalledWithCommandLineArguments = false; https = _https; port = _port; rawDataOutput = _rawDataOutput; flushDelegate = _flush; nph = true; remoteAddress = address; // streaming parser import al = std.algorithm; // FIXME: tis cast is technically wrong, but Phobos deprecated al.indexOf... for some reason. auto idx = indexOf(cast(string) inputData.front(), "\r\n\r\n"); while(idx == -1) { inputData.popFront(0); idx = indexOf(cast(string) inputData.front(), "\r\n\r\n"); } assert(idx != -1); string contentType = ""; string[string] requestHeadersHere; size_t contentLength; bool isChunked; { import core.runtime; scriptFileName = Runtime.args.length ? Runtime.args[0] : null; } int headerNumber = 0; foreach(line; al.splitter(inputData.front()[0 .. idx], "\r\n")) if(line.length) { headerNumber++; auto header = cast(string) line.idup; if(headerNumber == 1) { // request line auto parts = al.splitter(header, " "); requestMethod = to!RequestMethod(parts.front); parts.popFront(); requestUri = parts.front; // FIXME: the requestUri could be an absolute path!!! should I rename it or something? scriptName = requestUri[0 .. pathInfoStarts]; auto question = requestUri.indexOf("?"); if(question == -1) { queryString = ""; // FIXME: double check, this might be wrong since it could be url encoded pathInfo = requestUri[pathInfoStarts..$]; } else { queryString = requestUri[question+1..$]; pathInfo = requestUri[pathInfoStarts..question]; } auto ugh = decodeVariables(queryString, "&", &allGetNamesInOrder, &allGetValuesInOrder); getArray = cast(string[][string]) assumeUnique(ugh); if(header.indexOf("HTTP/1.0") != -1) { http10 = true; autoBuffer = true; if(closeConnection) { // on http 1.0, close is assumed (unlike http/1.1 where we assume keep alive) *closeConnection = true; } } } else { // other header auto colon = header.indexOf(":"); if(colon == -1) throw new Exception("HTTP headers should have a colon!"); string name = header[0..colon].toLower; string value = header[colon+2..$]; // skip the colon and the space requestHeadersHere[name] = value; if (name == "accept") { accept = value; } else if (name == "origin") { origin = value; } else if (name == "connection") { if(value == "close" && closeConnection) *closeConnection = true; if(value.asLowerCase().canFind("keep-alive")) { keepAliveRequested = true; // on http 1.0, the connection is closed by default, // but not if they request keep-alive. then we don't close // anymore - undoing the set above if(http10 && closeConnection) { *closeConnection = false; } } } else if (name == "transfer-encoding") { if(value == "chunked") isChunked = true; } else if (name == "last-event-id") { lastEventId = value; } else if (name == "authorization") { authorization = value; } else if (name == "content-type") { contentType = value; } else if (name == "content-length") { contentLength = to!size_t(value); } else if (name == "x-forwarded-for") { remoteAddress = value; } else if (name == "x-forwarded-host" || name == "host") { if(name != "host" || host is null) host = value; } // FIXME: https://tools.ietf.org/html/rfc7239 else if (name == "accept-encoding") { if(value.indexOf("gzip") != -1) acceptsGzip = true; } else if (name == "user-agent") { userAgent = value; } else if (name == "referer") { referrer = value; } else if (name == "cookie") { cookie ~= value; } else if(name == "expect") { if(value == "100-continue") { // FIXME we should probably give user code a chance // to process and reject but that needs to be virtual, // perhaps part of the CGI redesign. // FIXME: if size is > max content length it should // also fail at this point. _rawDataOutput(cast(ubyte[]) "HTTP/1.1 100 Continue\r\n\r\n"); // FIXME: let the user write out 103 early hints too } } // else // ignore it } } inputData.consume(idx + 4); // done requestHeaders = assumeUnique(requestHeadersHere); ByChunkRange dataByChunk; // reading Content-Length type data // We need to read up the data we have, and write it out as a chunk. if(!isChunked) { dataByChunk = byChunk(inputData, contentLength); } else { // chunked requests happen, but not every day. Since we need to know // the content length (for now, maybe that should change), we'll buffer // the whole thing here instead of parse streaming. (I think this is what Apache does anyway in cgi modes) auto data = dechunk(inputData); // set the range here dataByChunk = byChunk(data); contentLength = data.length; } assert(dataByChunk !is null); if(contentLength) { prepareForIncomingDataChunks(contentType, contentLength); foreach(dataChunk; dataByChunk) { handleIncomingDataChunk(dataChunk); } postArray = assumeUnique(pps._post); filesArray = assumeUnique(pps._files); files = keepLastOf(filesArray); post = keepLastOf(postArray); postBody = pps.postBody; cleanUpPostDataState(); } this.port = port; this.referrer = referrer; this.remoteAddress = remoteAddress; this.userAgent = userAgent; this.authorization = authorization; this.origin = origin; this.accept = accept; this.lastEventId = lastEventId; this.https = https; this.host = host; this.requestMethod = requestMethod; this.requestUri = requestUri; this.pathInfo = pathInfo; this.queryString = queryString; this.scriptName = scriptName; this.get = keepLastOf(getArray); this.getArray = cast(immutable) getArray; this.keepAliveRequested = keepAliveRequested; this.acceptsGzip = acceptsGzip; this.cookie = cookie; cookiesArray = getCookieArray(); cookies = keepLastOf(cookiesArray); } BufferedInputRange idlol; private immutable(string[string]) keepLastOf(in string[][string] arr) { string[string] ca; foreach(k, v; arr) ca[k] = v[$-1]; return assumeUnique(ca); } // FIXME duplication private immutable(UploadedFile[string]) keepLastOf(in UploadedFile[][string] arr) { UploadedFile[string] ca; foreach(k, v; arr) ca[k] = v[$-1]; return assumeUnique(ca); } private immutable(string[][string]) getCookieArray() { auto forTheLoveOfGod = decodeVariables(cookie, "; "); return assumeUnique(forTheLoveOfGod); } /// Very simple method to require a basic auth username and password. /// If the http request doesn't include the required credentials, it throws a /// HTTP 401 error, and an exception. /// /// Note: basic auth does not provide great security, especially over unencrypted HTTP; /// the user's credentials are sent in plain text on every request. /// /// If you are using Apache, the HTTP_AUTHORIZATION variable may not be sent to the /// application. Either use Apache's built in methods for basic authentication, or add /// something along these lines to your server configuration: /// /// RewriteEngine On /// RewriteCond %{HTTP:Authorization} ^(.*) /// RewriteRule ^(.*) - [E=HTTP_AUTHORIZATION:%1] /// /// To ensure the necessary data is available to cgi.d. void requireBasicAuth(string user, string pass, string message = null) { if(authorization != "Basic " ~ Base64.encode(cast(immutable(ubyte)[]) (user ~ ":" ~ pass))) { setResponseStatus("401 Authorization Required"); header ("WWW-Authenticate: Basic realm=\""~message~"\""); close(); throw new Exception("Not authorized; got " ~ authorization); } } /// Very simple caching controls - setCache(false) means it will never be cached. Good for rapidly updated or sensitive sites. /// setCache(true) means it will always be cached for as long as possible. Best for static content. /// Use setResponseExpires and updateResponseExpires for more control void setCache(bool allowCaching) { noCache = !allowCaching; } /// Set to true and use cgi.write(data, true); to send a gzipped response to browsers /// who can accept it bool gzipResponse; immutable bool acceptsGzip; immutable bool keepAliveRequested; /// Set to true if and only if this was initialized with command line arguments immutable bool isCalledWithCommandLineArguments; /// This gets a full url for the current request, including port, protocol, host, path, and query string getCurrentCompleteUri() const { ushort defaultPort = https ? 443 : 80; string uri = "http"; if(https) uri ~= "s"; uri ~= "://"; uri ~= host; /+ // the host has the port so p sure this never needed, cgi on apache and embedded http all do the right thing now version(none) if(!(!port || port == defaultPort)) { uri ~= ":"; uri ~= to!string(port); } +/ uri ~= requestUri; return uri; } /// You can override this if your site base url isn't the same as the script name string logicalScriptName() const { return scriptName; } /++ Sets the HTTP status of the response. For example, "404 File Not Found" or "500 Internal Server Error". It assumes "200 OK", and automatically changes to "302 Found" if you call setResponseLocation(). Note setResponseStatus() must be called *before* you write() any data to the output. History: The `int` overload was added on January 11, 2021. +/ void setResponseStatus(string status) { assert(!outputtedResponseData); responseStatus = status; } /// ditto void setResponseStatus(int statusCode) { setResponseStatus(getHttpCodeText(statusCode)); } private string responseStatus = null; /// Returns true if it is still possible to output headers bool canOutputHeaders() { return !isClosed && !outputtedResponseData; } /// Sets the location header, which the browser will redirect the user to automatically. /// Note setResponseLocation() must be called *before* you write() any data to the output. /// The optional important argument is used if it's a default suggestion rather than something to insist upon. void setResponseLocation(string uri, bool important = true, string status = null) { if(!important && isCurrentResponseLocationImportant) return; // important redirects always override unimportant ones if(uri is null) { responseStatus = "200 OK"; responseLocation = null; isCurrentResponseLocationImportant = important; return; // this just cancels the redirect } assert(!outputtedResponseData); if(status is null) responseStatus = "302 Found"; else responseStatus = status; responseLocation = uri.strip; isCurrentResponseLocationImportant = important; } protected string responseLocation = null; private bool isCurrentResponseLocationImportant = false; /// Sets the Expires: http header. See also: updateResponseExpires, setPublicCaching /// The parameter is in unix_timestamp * 1000. Try setResponseExpires(getUTCtime() + SOME AMOUNT) for normal use. /// Note: the when parameter is different than setCookie's expire parameter. void setResponseExpires(long when, bool isPublic = false) { responseExpires = when; setCache(true); // need to enable caching so the date has meaning responseIsPublic = isPublic; responseExpiresRelative = false; } /// Sets a cache-control max-age header for whenFromNow, in seconds. void setResponseExpiresRelative(int whenFromNow, bool isPublic = false) { responseExpires = whenFromNow; setCache(true); // need to enable caching so the date has meaning responseIsPublic = isPublic; responseExpiresRelative = true; } private long responseExpires = long.min; private bool responseIsPublic = false; private bool responseExpiresRelative = false; /// This is like setResponseExpires, but it can be called multiple times. The setting most in the past is the one kept. /// If you have multiple functions, they all might call updateResponseExpires about their own return value. The program /// output as a whole is as cacheable as the least cacheable part in the chain. /// setCache(false) always overrides this - it is, by definition, the strictest anti-cache statement available. If your site outputs sensitive user data, you should probably call setCache(false) when you do, to ensure no other functions will cache the content, as it may be a privacy risk. /// Conversely, setting here overrides setCache(true), since any expiration date is in the past of infinity. void updateResponseExpires(long when, bool isPublic) { if(responseExpires == long.min) setResponseExpires(when, isPublic); else if(when < responseExpires) setResponseExpires(when, responseIsPublic && isPublic); // if any part of it is private, it all is } /* /// Set to true if you want the result to be cached publicly - that is, is the content shared? /// Should generally be false if the user is logged in. It assumes private cache only. /// setCache(true) also turns on public caching, and setCache(false) sets to private. void setPublicCaching(bool allowPublicCaches) { publicCaching = allowPublicCaches; } private bool publicCaching = false; */ /++ History: Added January 11, 2021 +/ enum SameSitePolicy { Lax, Strict, None } /++ Sets an HTTP cookie, automatically encoding the data to the correct string. expiresIn is how many milliseconds in the future the cookie will expire. TIP: to make a cookie accessible from subdomains, set the domain to .yourdomain.com. Note setCookie() must be called *before* you write() any data to the output. History: Parameter `sameSitePolicy` was added on January 11, 2021. +/ void setCookie(string name, string data, long expiresIn = 0, string path = null, string domain = null, bool httpOnly = false, bool secure = false, SameSitePolicy sameSitePolicy = SameSitePolicy.Lax) { assert(!outputtedResponseData); string cookie = std.uri.encodeComponent(name) ~ "="; cookie ~= std.uri.encodeComponent(data); if(path !is null) cookie ~= "; path=" ~ path; // FIXME: should I just be using max-age here? (also in cache below) if(expiresIn != 0) cookie ~= "; expires=" ~ printDate(cast(DateTime) Clock.currTime(UTC()) + dur!"msecs"(expiresIn)); if(domain !is null) cookie ~= "; domain=" ~ domain; if(secure == true) cookie ~= "; Secure"; if(httpOnly == true ) cookie ~= "; HttpOnly"; final switch(sameSitePolicy) { case SameSitePolicy.Lax: cookie ~= "; SameSite=Lax"; break; case SameSitePolicy.Strict: cookie ~= "; SameSite=Strict"; break; case SameSitePolicy.None: cookie ~= "; SameSite=None"; assert(secure); // cookie spec requires this now, see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite break; } if(auto idx = name in cookieIndexes) { responseCookies[*idx] = cookie; } else { cookieIndexes[name] = responseCookies.length; responseCookies ~= cookie; } } private string[] responseCookies; private size_t[string] cookieIndexes; /// Clears a previously set cookie with the given name, path, and domain. void clearCookie(string name, string path = null, string domain = null) { assert(!outputtedResponseData); setCookie(name, "", 1, path, domain); } /// Sets the content type of the response, for example "text/html" (the default) for HTML, or "image/png" for a PNG image void setResponseContentType(string ct) { assert(!outputtedResponseData); responseContentType = ct; } private string responseContentType = null; /// Adds a custom header. It should be the name: value, but without any line terminator. /// For example: header("X-My-Header: Some value"); /// Note you should use the specialized functions in this object if possible to avoid /// duplicates in the output. void header(string h) { customHeaders ~= h; } /++ I named the original function `header` after PHP, but this pattern more fits the rest of the Cgi object. Either name are allowed. History: Alias added June 17, 2022. +/ alias setResponseHeader = header; private string[] customHeaders; private bool websocketMode; void flushHeaders(const(void)[] t, bool isAll = false) { StackBuffer buffer = StackBuffer(0); prepHeaders(t, isAll, &buffer); if(rawDataOutput !is null) rawDataOutput(cast(const(ubyte)[]) buffer.get()); else { stdout.rawWrite(buffer.get()); } } private void prepHeaders(const(void)[] t, bool isAll, StackBuffer* buffer) { string terminator = "\n"; if(rawDataOutput !is null) terminator = "\r\n"; if(responseStatus !is null) { if(nph) { if(http10) buffer.add("HTTP/1.0 ", responseStatus, terminator); else buffer.add("HTTP/1.1 ", responseStatus, terminator); } else buffer.add("Status: ", responseStatus, terminator); } else if (nph) { if(http10) buffer.add("HTTP/1.0 200 OK", terminator); else buffer.add("HTTP/1.1 200 OK", terminator); } if(websocketMode) goto websocket; if(nph) { // we're responsible for setting the date too according to http 1.1 char[29] db = void; printDateToBuffer(cast(DateTime) Clock.currTime(UTC()), db[]); buffer.add("Date: ", db[], terminator); } // FIXME: what if the user wants to set his own content-length? // The custom header function can do it, so maybe that's best. // Or we could reuse the isAll param. if(responseLocation !is null) { buffer.add("Location: ", responseLocation, terminator); } if(!noCache && responseExpires != long.min) { // an explicit expiration date is set if(responseExpiresRelative) { buffer.add("Cache-Control: ", responseIsPublic ? "public" : "private", ", max-age="); buffer.add(responseExpires); buffer.add(", no-cache=\"set-cookie, set-cookie2\"", terminator); } else { auto expires = SysTime(unixTimeToStdTime(cast(int)(responseExpires / 1000)), UTC()); char[29] db = void; printDateToBuffer(cast(DateTime) expires, db[]); buffer.add("Expires: ", db[], terminator); // FIXME: assuming everything is private unless you use nocache - generally right for dynamic pages, but not necessarily buffer.add("Cache-Control: ", (responseIsPublic ? "public" : "private"), ", no-cache=\"set-cookie, set-cookie2\""); buffer.add(terminator); } } if(responseCookies !is null && responseCookies.length > 0) { foreach(c; responseCookies) buffer.add("Set-Cookie: ", c, terminator); } if(noCache) { // we specifically do not want caching (this is actually the default) buffer.add("Cache-Control: private, no-cache=\"set-cookie\"", terminator); buffer.add("Expires: 0", terminator); buffer.add("Pragma: no-cache", terminator); } else { if(responseExpires == long.min) { // caching was enabled, but without a date set - that means assume cache forever buffer.add("Cache-Control: public", terminator); buffer.add("Expires: Tue, 31 Dec 2030 14:00:00 GMT", terminator); // FIXME: should not be more than one year in the future } } if(responseContentType !is null) { buffer.add("Content-Type: ", responseContentType, terminator); } else buffer.add("Content-Type: text/html; charset=utf-8", terminator); if(gzipResponse && acceptsGzip && isAll) { // FIXME: isAll really shouldn't be necessary buffer.add("Content-Encoding: gzip", terminator); } if(!isAll) { if(nph && !http10) { buffer.add("Transfer-Encoding: chunked", terminator); responseChunked = true; } } else { buffer.add("Content-Length: "); buffer.add(t.length); buffer.add(terminator); if(nph && keepAliveRequested) { buffer.add("Connection: Keep-Alive", terminator); } } websocket: foreach(hd; customHeaders) buffer.add(hd, terminator); // FIXME: what about duplicated headers? // end of header indicator buffer.add(terminator); outputtedResponseData = true; } /// Writes the data to the output, flushing headers if they have not yet been sent. void write(const(void)[] t, bool isAll = false, bool maybeAutoClose = true) { assert(!closed, "Output has already been closed"); StackBuffer buffer = StackBuffer(0); if(gzipResponse && acceptsGzip && isAll) { // FIXME: isAll really shouldn't be necessary // actually gzip the data here auto c = new Compress(HeaderFormat.gzip); // want gzip auto data = c.compress(t); data ~= c.flush(); // std.file.write("/tmp/last-item", data); t = data; } if(!outputtedResponseData && (!autoBuffer || isAll)) { prepHeaders(t, isAll, &buffer); } if(requestMethod != RequestMethod.HEAD && t.length > 0) { if (autoBuffer && !isAll) { outputBuffer ~= cast(ubyte[]) t; } if(!autoBuffer || isAll) { if(rawDataOutput !is null) if(nph && responseChunked) { //rawDataOutput(makeChunk(cast(const(ubyte)[]) t)); // we're making the chunk here instead of in a function // to avoid unneeded gc pressure buffer.add(toHex(t.length)); buffer.add("\r\n"); buffer.add(cast(char[]) t, "\r\n"); } else { buffer.add(cast(char[]) t); } else buffer.add(cast(char[]) t); } } if(rawDataOutput !is null) rawDataOutput(cast(const(ubyte)[]) buffer.get()); else stdout.rawWrite(buffer.get()); if(maybeAutoClose && isAll) close(); // if you say it is all, that means we're definitely done // maybeAutoClose can be false though to avoid this (important if you call from inside close()! } /++ Convenience method to set content type to json and write the string as the complete response. History: Added January 16, 2020 +/ void writeJson(string json) { this.setResponseContentType("application/json"); this.write(json, true); } /// Flushes the pending buffer, leaving the connection open so you can send more. void flush() { if(rawDataOutput is null) stdout.flush(); else if(flushDelegate !is null) flushDelegate(); } version(autoBuffer) bool autoBuffer = true; else bool autoBuffer = false; ubyte[] outputBuffer; /// Flushes the buffers to the network, signifying that you are done. /// You should always call this explicitly when you are done outputting data. void close() { if(closed) return; // don't double close if(!outputtedResponseData) write("", true, false); // writing auto buffered data if(requestMethod != RequestMethod.HEAD && autoBuffer) { if(!nph) stdout.rawWrite(outputBuffer); else write(outputBuffer, true, false); // tell it this is everything } // closing the last chunk... if(nph && rawDataOutput !is null && responseChunked) rawDataOutput(cast(const(ubyte)[]) "0\r\n\r\n"); if(flushDelegate) flushDelegate(); closed = true; } // Closes without doing anything, shouldn't be used often void rawClose() { closed = true; } /++ Gets a request variable as a specific type, or the default value of it isn't there or isn't convertible to the request type. Checks both GET and POST variables, preferring the POST variable, if available. A nice trick is using the default value to choose the type: --- /* The return value will match the type of the default. Here, I gave 10 as a default, so the return value will be an int. If the user-supplied value cannot be converted to the requested type, you will get the default value back. */ int a = cgi.request("number", 10); if(cgi.get["number"] == "11") assert(a == 11); // conversion succeeds if("number" !in cgi.get) assert(a == 10); // no value means you can't convert - give the default if(cgi.get["number"] == "twelve") assert(a == 10); // conversion from string to int would fail, so we get the default --- You can use an enum as an easy whitelist, too: --- enum Operations { add, remove, query } auto op = cgi.request("op", Operations.query); if(cgi.get["op"] == "add") assert(op == Operations.add); if(cgi.get["op"] == "remove") assert(op == Operations.remove); if(cgi.get["op"] == "query") assert(op == Operations.query); if(cgi.get["op"] == "random string") assert(op == Operations.query); // the value can't be converted to the enum, so we get the default --- +/ T request(T = string)(in string name, in T def = T.init) const nothrow { try { return (name in post) ? to!T(post[name]) : (name in get) ? to!T(get[name]) : def; } catch(Exception e) { return def; } } /// Is the output already closed? bool isClosed() const { return closed; } /++ Gets a session object associated with the `cgi` request. You can use different type throughout your application. +/ Session!Data getSessionObject(Data)() { if(testInProcess !is null) { // test mode auto obj = testInProcess.getSessionOverride(typeid(typeof(return))); if(obj !is null) return cast(typeof(return)) obj; else { auto o = new MockSession!Data(); testInProcess.setSessionOverride(typeid(typeof(return)), o); return o; } } else { // normal operation return new BasicDataServerSession!Data(this); } } // if it is in test mode; triggers mock sessions. Used by CgiTester version(with_breaking_cgi_features) private CgiTester testInProcess; /* Hooks for redirecting input and output */ private void delegate(const(ubyte)[]) rawDataOutput = null; private void delegate() flushDelegate = null; /* This info is used when handling a more raw HTTP protocol */ private bool nph; private bool http10; private bool closed; private bool responseChunked = false; version(preserveData) // note: this can eat lots of memory; don't use unless you're sure you need it. immutable(ubyte)[] originalPostData; /++ This holds the posted body data if it has not been parsed into [post] and [postArray]. It is intended to be used for JSON and XML request content types, but also may be used for other content types your application can handle. But it will NOT be populated for content types application/x-www-form-urlencoded or multipart/form-data, since those are parsed into the post and postArray members. Remember that anything beyond your `maxContentLength` param when setting up [GenericMain], etc., will be discarded to the client with an error. This helps keep this array from being exploded in size and consuming all your server's memory (though it may still be possible to eat excess ram from a concurrent client in certain build modes.) History: Added January 5, 2021 Documented February 21, 2023 (dub v11.0) +/ public immutable string postBody; alias postJson = postBody; // old name /* Internal state flags */ private bool outputtedResponseData; private bool noCache = true; const(string[string]) environmentVariables; /** What follows is data gotten from the HTTP request. It is all fully immutable, partially because it logically is (your code doesn't change what the user requested...) and partially because I hate how bad programs in PHP change those superglobals to do all kinds of hard to follow ugliness. I don't want that to ever happen in D. For some of these, you'll want to refer to the http or cgi specs for more details. */ immutable(string[string]) requestHeaders; /// All the raw headers in the request as name/value pairs. The name is stored as all lower case, but otherwise the same as it is in HTTP; words separated by dashes. For example, "cookie" or "accept-encoding". Many HTTP headers have specialized variables below for more convenience and static name checking; you should generally try to use them. immutable(char[]) host; /// The hostname in the request. If one program serves multiple domains, you can use this to differentiate between them. immutable(char[]) origin; /// The origin header in the request, if present. Some HTML5 cross-domain apis set this and you should check it on those cross domain requests and websockets. immutable(char[]) userAgent; /// The browser's user-agent string. Can be used to identify the browser. immutable(char[]) pathInfo; /// This is any stuff sent after your program's name on the url, but before the query string. For example, suppose your program is named "app". If the user goes to site.com/app, pathInfo is empty. But, he can also go to site.com/app/some/sub/path; treating your program like a virtual folder. In this case, pathInfo == "/some/sub/path". immutable(char[]) scriptName; /// The full base path of your program, as seen by the user. If your program is located at site.com/programs/apps, scriptName == "/programs/apps". immutable(char[]) scriptFileName; /// The physical filename of your script immutable(char[]) authorization; /// The full authorization string from the header, undigested. Useful for implementing auth schemes such as OAuth 1.0. Note that some web servers do not forward this to the app without taking extra steps. See requireBasicAuth's comment for more info. immutable(char[]) accept; /// The HTTP accept header is the user agent telling what content types it is willing to accept. This is often */*; they accept everything, so it's not terribly useful. (The similar sounding Accept-Encoding header is handled automatically for chunking and gzipping. Simply set gzipResponse = true and cgi.d handles the details, zipping if the user's browser is willing to accept it.) immutable(char[]) lastEventId; /// The HTML 5 draft includes an EventSource() object that connects to the server, and remains open to take a stream of events. My arsd.rtud module can help with the server side part of that. The Last-Event-Id http header is defined in the draft to help handle loss of connection. When the browser reconnects to you, it sets this header to the last event id it saw, so you can catch it up. This member has the contents of that header. immutable(RequestMethod) requestMethod; /// The HTTP request verb: GET, POST, etc. It is represented as an enum in cgi.d (which, like many enums, you can convert back to string with std.conv.to()). A HTTP GET is supposed to, according to the spec, not have side effects; a user can GET something over and over again and always have the same result. On all requests, the get[] and getArray[] members may be filled in. The post[] and postArray[] members are only filled in on POST methods. immutable(char[]) queryString; /// The unparsed content of the request query string - the stuff after the ? in your URL. See get[] and getArray[] for a parse view of it. Sometimes, the unparsed string is useful though if you want a custom format of data up there (probably not a good idea, unless it is really simple, like "?username" perhaps.) immutable(char[]) cookie; /// The unparsed content of the Cookie: header in the request. See also the cookies[string] member for a parsed view of the data. /** The Referer header from the request. (It is misspelled in the HTTP spec, and thus the actual request and cgi specs too, but I spelled the word correctly here because that's sane. The spec's misspelling is an implementation detail.) It contains the site url that referred the user to your program; the site that linked to you, or if you're serving images, the site that has you as an image. Also, if you're in an iframe, the referrer is the site that is framing you. Important note: if the user copy/pastes your url, this is blank, and, just like with all other user data, their browsers can also lie to you. Don't rely on it for real security. */ immutable(char[]) referrer; immutable(char[]) requestUri; /// The full url if the current request, excluding the protocol and host. requestUri == scriptName ~ pathInfo ~ (queryString.length ? "?" ~ queryString : ""); immutable(char[]) remoteAddress; /// The IP address of the user, as we see it. (Might not match the IP of the user's computer due to things like proxies and NAT.) immutable bool https; /// Was the request encrypted via https? immutable int port; /// On what TCP port number did the server receive the request? /** Here come the parsed request variables - the things that come close to PHP's _GET, _POST, etc. superglobals in content. */ immutable(string[string]) get; /// The data from your query string in the url, only showing the last string of each name. If you want to handle multiple values with the same name, use getArray. This only works right if the query string is x-www-form-urlencoded; the default you see on the web with name=value pairs separated by the & character. immutable(string[string]) post; /// The data from the request's body, on POST requests. It parses application/x-www-form-urlencoded data (used by most web requests, including typical forms), and multipart/form-data requests (used by file uploads on web forms) into the same container, so you can always access them the same way. It makes no attempt to parse other content types. If you want to accept an XML Post body (for a web api perhaps), you'll need to handle the raw data yourself. immutable(string[string]) cookies; /// Separates out the cookie header into individual name/value pairs (which is how you set them!) /** Represents user uploaded files. When making a file upload form, be sure to follow the standard: set method="POST" and enctype="multipart/form-data" in your html
tag attributes. The key into this array is the name attribute on your input tag, just like with other post variables. See the comments on the UploadedFile struct for more information about the data inside, including important notes on max size and content location. */ immutable(UploadedFile[][string]) filesArray; immutable(UploadedFile[string]) files; /// Use these if you expect multiple items submitted with the same name. btw, assert(get[name] is getArray[name][$-1); should pass. Same for post and cookies. /// the order of the arrays is the order the data arrives immutable(string[][string]) getArray; /// like get, but an array of values per name immutable(string[][string]) postArray; /// ditto for post immutable(string[][string]) cookiesArray; /// ditto for cookies // convenience function for appending to a uri without extra ? // matches the name and effect of javascript's location.search property string search() const { if(queryString.length) return "?" ~ queryString; return ""; } // FIXME: what about multiple files with the same name? private: //RequestMethod _requestMethod; } /// use this for testing or other isolated things when you want it to be no-ops Cgi dummyCgi(Cgi.RequestMethod method = Cgi.RequestMethod.GET, string url = null, in ubyte[] data = null, void delegate(const(ubyte)[]) outputSink = null) { // we want to ignore, not use stdout if(outputSink is null) outputSink = delegate void(const(ubyte)[]) { }; string[string] env; env["REQUEST_METHOD"] = to!string(method); env["CONTENT_LENGTH"] = to!string(data.length); auto cgi = new Cgi( 0, env, { return data; }, outputSink, null); return cgi; } /++ A helper test class for request handler unittests. +/ version(with_breaking_cgi_features) class CgiTester { private { SessionObject[TypeInfo] mockSessions; SessionObject getSessionOverride(TypeInfo ti) { if(auto o = ti in mockSessions) return *o; else return null; } void setSessionOverride(TypeInfo ti, SessionObject so) { mockSessions[ti] = so; } } /++ Gets (and creates if necessary) a mock session object for this test. Note it will be the same one used for any test operations through this CgiTester instance. +/ Session!Data getSessionObject(Data)() { auto obj = getSessionOverride(typeid(typeof(return))); if(obj !is null) return cast(typeof(return)) obj; else { auto o = new MockSession!Data(); setSessionOverride(typeid(typeof(return)), o); return o; } } /++ Pass a reference to your request handler when creating the tester. +/ this(void function(Cgi) requestHandler) { this.requestHandler = requestHandler; } /++ You can check response information with these methods after you call the request handler. +/ struct Response { int code; string[string] headers; string responseText; ubyte[] responseBody; } /++ Executes a test request on your request handler, and returns the response. Params: url = The URL to test. Should be an absolute path, but excluding domain. e.g. `"/test"`. args = additional arguments. Same format as cgi's command line handler. +/ Response GET(string url, string[] args = null) { return executeTest("GET", url, args); } /// ditto Response POST(string url, string[] args = null) { return executeTest("POST", url, args); } /// ditto Response executeTest(string method, string url, string[] args) { ubyte[] outputtedRawData; void outputSink(const(ubyte)[] data) { outputtedRawData ~= data; } auto cgi = new Cgi(["test", method, url] ~ args, &outputSink); cgi.testInProcess = this; scope(exit) cgi.dispose(); requestHandler(cgi); cgi.close(); Response response; if(outputtedRawData.length) { enum LINE = "\r\n"; auto idx = outputtedRawData.locationOf(LINE ~ LINE); assert(idx != -1, to!string(outputtedRawData)); auto headers = cast(string) outputtedRawData[0 .. idx]; response.code = 200; while(headers.length) { auto i = headers.locationOf(LINE); if(i == -1) i = cast(int) headers.length; auto header = headers[0 .. i]; auto c = header.locationOf(":"); if(c != -1) { auto name = header[0 .. c]; auto value = header[c + 2 ..$]; if(name == "Status") response.code = value[0 .. value.locationOf(" ")].to!int; response.headers[name] = value; } else { assert(0); } if(i != headers.length) i += 2; headers = headers[i .. $]; } response.responseBody = outputtedRawData[idx + 4 .. $]; response.responseText = cast(string) response.responseBody; } return response; } private void function(Cgi) requestHandler; } // should this be a separate module? Probably, but that's a hassle. /// Makes a data:// uri that can be used as links in most newer browsers (IE8+). string makeDataUrl(string mimeType, in void[] data) { auto data64 = Base64.encode(cast(const(ubyte[])) data); return "data:" ~ mimeType ~ ";base64," ~ assumeUnique(data64); } // FIXME: I don't think this class correctly decodes/encodes the individual parts /// Represents a url that can be broken down or built up through properties struct Uri { alias toString this; // blargh idk a url really is a string, but should it be implicit? // scheme//userinfo@host:port/path?query#fragment string scheme; /// e.g. "http" in "http://example.com/" string userinfo; /// the username (and possibly a password) in the uri string host; /// the domain name int port; /// port number, if given. Will be zero if a port was not explicitly given string path; /// e.g. "/folder/file.html" in "http://example.com/folder/file.html" string query; /// the stuff after the ? in a uri string fragment; /// the stuff after the # in a uri. // idk if i want to keep these, since the functions they wrap are used many, many, many times in existing code, so this is either an unnecessary alias or a gratuitous break of compatibility // the decode ones need to keep different names anyway because we can't overload on return values... static string encode(string s) { return std.uri.encodeComponent(s); } static string encode(string[string] s) { return encodeVariables(s); } static string encode(string[][string] s) { return encodeVariables(s); } /// Breaks down a uri string to its components this(string uri) { reparse(uri); } private void reparse(string uri) { // from RFC 3986 // the ctRegex triples the compile time and makes ugly errors for no real benefit // it was a nice experiment but just not worth it. // enum ctr = ctRegex!r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?"; /* Captures: 0 = whole url 1 = scheme, with : 2 = scheme, no : 3 = authority, with // 4 = authority, no // 5 = path 6 = query string, with ? 7 = query string, no ? 8 = anchor, with # 9 = anchor, no # */ // Yikes, even regular, non-CT regex is also unacceptably slow to compile. 1.9s on my computer! // instead, I will DIY and cut that down to 0.6s on the same computer. /* Note that authority is user:password@domain:port where the user:password@ part is optional, and the :port is optional. Regex translation: Scheme cannot have :, /, ?, or # in it, and must have one or more chars and end in a :. It is optional, but must be first. Authority must start with //, but cannot have any other /, ?, or # in it. It is optional. Path cannot have any ? or # in it. It is optional. Query must start with ? and must not have # in it. It is optional. Anchor must start with # and can have anything else in it to end of string. It is optional. */ this = Uri.init; // reset all state // empty uri = nothing special if(uri.length == 0) { return; } size_t idx; scheme_loop: foreach(char c; uri[idx .. $]) { switch(c) { case ':': case '/': case '?': case '#': break scheme_loop; default: } idx++; } if(idx == 0 && uri[idx] == ':') { // this is actually a path! we skip way ahead goto path_loop; } if(idx == uri.length) { // the whole thing is a path, apparently path = uri; return; } if(idx > 0 && uri[idx] == ':') { scheme = uri[0 .. idx]; idx++; } else { // we need to rewind; it found a / but no :, so the whole thing is prolly a path... idx = 0; } if(idx + 2 < uri.length && uri[idx .. idx + 2] == "//") { // we have an authority.... idx += 2; auto authority_start = idx; authority_loop: foreach(char c; uri[idx .. $]) { switch(c) { case '/': case '?': case '#': break authority_loop; default: } idx++; } auto authority = uri[authority_start .. idx]; auto idx2 = authority.indexOf("@"); if(idx2 != -1) { userinfo = authority[0 .. idx2]; authority = authority[idx2 + 1 .. $]; } if(authority.length && authority[0] == '[') { // ipv6 address special casing idx2 = authority.indexOf(']'); if(idx2 != -1) { auto end = authority[idx2 + 1 .. $]; if(end.length && end[0] == ':') idx2 = idx2 + 1; else idx2 = -1; } } else { idx2 = authority.indexOf(":"); } if(idx2 == -1) { port = 0; // 0 means not specified; we should use the default for the scheme host = authority; } else { host = authority[0 .. idx2]; if(idx2 + 1 < authority.length) port = to!int(authority[idx2 + 1 .. $]); else port = 0; } } path_loop: auto path_start = idx; foreach(char c; uri[idx .. $]) { if(c == '?' || c == '#') break; idx++; } path = uri[path_start .. idx]; if(idx == uri.length) return; // nothing more to examine... if(uri[idx] == '?') { idx++; auto query_start = idx; foreach(char c; uri[idx .. $]) { if(c == '#') break; idx++; } query = uri[query_start .. idx]; } if(idx < uri.length && uri[idx] == '#') { idx++; fragment = uri[idx .. $]; } // uriInvalidated = false; } private string rebuildUri() const { string ret; if(scheme.length) ret ~= scheme ~ ":"; if(userinfo.length || host.length) ret ~= "//"; if(userinfo.length) ret ~= userinfo ~ "@"; if(host.length) ret ~= host; if(port) ret ~= ":" ~ to!string(port); ret ~= path; if(query.length) ret ~= "?" ~ query; if(fragment.length) ret ~= "#" ~ fragment; // uri = ret; // uriInvalidated = false; return ret; } /// Converts the broken down parts back into a complete string string toString() const { // if(uriInvalidated) return rebuildUri(); } /// Returns a new absolute Uri given a base. It treats this one as /// relative where possible, but absolute if not. (If protocol, domain, or /// other info is not set, the new one inherits it from the base.) /// /// Browsers use a function like this to figure out links in html. Uri basedOn(in Uri baseUrl) const { Uri n = this; // copies if(n.scheme == "data") return n; // n.uriInvalidated = true; // make sure we regenerate... // userinfo is not inherited... is this wrong? // if anything is given in the existing url, we don't use the base anymore. if(n.scheme.empty) { n.scheme = baseUrl.scheme; if(n.host.empty) { n.host = baseUrl.host; if(n.port == 0) { n.port = baseUrl.port; if(n.path.length > 0 && n.path[0] != '/') { auto b = baseUrl.path[0 .. baseUrl.path.lastIndexOf("/") + 1]; if(b.length == 0) b = "/"; n.path = b ~ n.path; } else if(n.path.length == 0) { n.path = baseUrl.path; } } } } n.removeDots(); return n; } void removeDots() { auto parts = this.path.split("/"); string[] toKeep; foreach(part; parts) { if(part == ".") { continue; } else if(part == "..") { //if(toKeep.length > 1) toKeep = toKeep[0 .. $-1]; //else //toKeep = [""]; continue; } else { //if(toKeep.length && toKeep[$-1].length == 0 && part.length == 0) //continue; // skip a `//` situation toKeep ~= part; } } auto path = toKeep.join("/"); if(path.length && path[0] != '/') path = "/" ~ path; this.path = path; } unittest { auto uri = Uri("test.html"); assert(uri.path == "test.html"); uri = Uri("path/1/lol"); assert(uri.path == "path/1/lol"); uri = Uri("http://me@example.com"); assert(uri.scheme == "http"); assert(uri.userinfo == "me"); assert(uri.host == "example.com"); uri = Uri("http://example.com/#a"); assert(uri.scheme == "http"); assert(uri.host == "example.com"); assert(uri.fragment == "a"); uri = Uri("#foo"); assert(uri.fragment == "foo"); uri = Uri("?lol"); assert(uri.query == "lol"); uri = Uri("#foo?lol"); assert(uri.fragment == "foo?lol"); uri = Uri("?lol#foo"); assert(uri.fragment == "foo"); assert(uri.query == "lol"); uri = Uri("http://127.0.0.1/"); assert(uri.host == "127.0.0.1"); assert(uri.port == 0); uri = Uri("http://127.0.0.1:123/"); assert(uri.host == "127.0.0.1"); assert(uri.port == 123); uri = Uri("http://[ff:ff::0]/"); assert(uri.host == "[ff:ff::0]"); uri = Uri("http://[ff:ff::0]:123/"); assert(uri.host == "[ff:ff::0]"); assert(uri.port == 123); } // This can sometimes be a big pain in the butt for me, so lots of copy/paste here to cover // the possibilities. unittest { auto url = Uri("cool.html"); // checking relative links assert(url.basedOn(Uri("http://test.com/what/test.html")) == "http://test.com/what/cool.html"); assert(url.basedOn(Uri("https://test.com/what/test.html")) == "https://test.com/what/cool.html"); assert(url.basedOn(Uri("http://test.com/what/")) == "http://test.com/what/cool.html"); assert(url.basedOn(Uri("http://test.com/")) == "http://test.com/cool.html"); assert(url.basedOn(Uri("http://test.com")) == "http://test.com/cool.html"); assert(url.basedOn(Uri("http://test.com/what/test.html?a=b")) == "http://test.com/what/cool.html"); assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d")) == "http://test.com/what/cool.html"); assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d#what")) == "http://test.com/what/cool.html"); assert(url.basedOn(Uri("http://test.com")) == "http://test.com/cool.html"); url = Uri("/something/cool.html"); // same server, different path assert(url.basedOn(Uri("http://test.com/what/test.html")) == "http://test.com/something/cool.html"); assert(url.basedOn(Uri("https://test.com/what/test.html")) == "https://test.com/something/cool.html"); assert(url.basedOn(Uri("http://test.com/what/")) == "http://test.com/something/cool.html"); assert(url.basedOn(Uri("http://test.com/")) == "http://test.com/something/cool.html"); assert(url.basedOn(Uri("http://test.com")) == "http://test.com/something/cool.html"); assert(url.basedOn(Uri("http://test.com/what/test.html?a=b")) == "http://test.com/something/cool.html"); assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d")) == "http://test.com/something/cool.html"); assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d#what")) == "http://test.com/something/cool.html"); assert(url.basedOn(Uri("http://test.com")) == "http://test.com/something/cool.html"); url = Uri("?query=answer"); // same path. server, protocol, and port, just different query string and fragment assert(url.basedOn(Uri("http://test.com/what/test.html")) == "http://test.com/what/test.html?query=answer"); assert(url.basedOn(Uri("https://test.com/what/test.html")) == "https://test.com/what/test.html?query=answer"); assert(url.basedOn(Uri("http://test.com/what/")) == "http://test.com/what/?query=answer"); assert(url.basedOn(Uri("http://test.com/")) == "http://test.com/?query=answer"); assert(url.basedOn(Uri("http://test.com")) == "http://test.com?query=answer"); assert(url.basedOn(Uri("http://test.com/what/test.html?a=b")) == "http://test.com/what/test.html?query=answer"); assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d")) == "http://test.com/what/test.html?query=answer"); assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d#what")) == "http://test.com/what/test.html?query=answer"); assert(url.basedOn(Uri("http://test.com")) == "http://test.com?query=answer"); url = Uri("/test/bar"); assert(Uri("./").basedOn(url) == "/test/", Uri("./").basedOn(url)); assert(Uri("../").basedOn(url) == "/"); url = Uri("http://example.com/"); assert(Uri("../foo").basedOn(url) == "http://example.com/foo"); //auto uriBefore = url; url = Uri("#anchor"); // everything should remain the same except the anchor //uriBefore.anchor = "anchor"); //assert(url == uriBefore); url = Uri("//example.com"); // same protocol, but different server. the path here should be blank. url = Uri("//example.com/example.html"); // same protocol, but different server and path url = Uri("http://example.com/test.html"); // completely absolute link should never be modified url = Uri("http://example.com"); // completely absolute link should never be modified, even if it has no path // FIXME: add something for port too } // these are like javascript's location.search and location.hash string search() const { return query.length ? ("?" ~ query) : ""; } string hash() const { return fragment.length ? ("#" ~ fragment) : ""; } } /* for session, see web.d */ /// breaks down a url encoded string string[][string] decodeVariables(string data, string separator = "&", string[]* namesInOrder = null, string[]* valuesInOrder = null) { auto vars = data.split(separator); string[][string] _get; foreach(var; vars) { auto equal = var.indexOf("="); string name; string value; if(equal == -1) { name = decodeComponent(var); value = ""; } else { //_get[decodeComponent(var[0..equal])] ~= decodeComponent(var[equal + 1 .. $].replace("+", " ")); // stupid + -> space conversion. name = decodeComponent(var[0..equal].replace("+", " ")); value = decodeComponent(var[equal + 1 .. $].replace("+", " ")); } _get[name] ~= value; if(namesInOrder) (*namesInOrder) ~= name; if(valuesInOrder) (*valuesInOrder) ~= value; } return _get; } /// breaks down a url encoded string, but only returns the last value of any array string[string] decodeVariablesSingle(string data) { string[string] va; auto varArray = decodeVariables(data); foreach(k, v; varArray) va[k] = v[$-1]; return va; } /// url encodes the whole string string encodeVariables(in string[string] data) { string ret; bool outputted = false; foreach(k, v; data) { if(outputted) ret ~= "&"; else outputted = true; ret ~= std.uri.encodeComponent(k) ~ "=" ~ std.uri.encodeComponent(v); } return ret; } /// url encodes a whole string string encodeVariables(in string[][string] data) { string ret; bool outputted = false; foreach(k, arr; data) { foreach(v; arr) { if(outputted) ret ~= "&"; else outputted = true; ret ~= std.uri.encodeComponent(k) ~ "=" ~ std.uri.encodeComponent(v); } } return ret; } /// Encodes all but the explicitly unreserved characters per rfc 3986 /// Alphanumeric and -_.~ are the only ones left unencoded /// name is borrowed from php string rawurlencode(in char[] data) { string ret; ret.reserve(data.length * 2); foreach(char c; data) { if( (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~') { ret ~= c; } else { ret ~= '%'; // since we iterate on char, this should give us the octets of the full utf8 string ret ~= toHexUpper(c); } } return ret; } // http helper functions // for chunked responses (which embedded http does whenever possible) version(none) // this is moved up above to avoid making a copy of the data const(ubyte)[] makeChunk(const(ubyte)[] data) { const(ubyte)[] ret; ret = cast(const(ubyte)[]) toHex(data.length); ret ~= cast(const(ubyte)[]) "\r\n"; ret ~= data; ret ~= cast(const(ubyte)[]) "\r\n"; return ret; } string toHex(long num) { string ret; while(num) { int v = num % 16; num /= 16; char d = cast(char) ((v < 10) ? v + '0' : (v-10) + 'a'); ret ~= d; } return to!string(array(ret.retro)); } string toHexUpper(long num) { string ret; while(num) { int v = num % 16; num /= 16; char d = cast(char) ((v < 10) ? v + '0' : (v-10) + 'A'); ret ~= d; } if(ret.length == 1) ret ~= "0"; // url encoding requires two digits and that's what this function is used for... return to!string(array(ret.retro)); } // the generic mixins /++ Use this instead of writing your own main It ultimately calls [cgiMainImpl] which creates a [RequestServer] for you. +/ mixin template GenericMain(alias fun, long maxContentLength = defaultMaxContentLength) { mixin CustomCgiMain!(Cgi, fun, maxContentLength); } /++ Boilerplate mixin for a main function that uses the [dispatcher] function. You can send `typeof(null)` as the `Presenter` argument to use a generic one. History: Added July 9, 2021 +/ mixin template DispatcherMain(Presenter, DispatcherArgs...) { /++ Handler to the generated presenter you can use from your objects, etc. +/ Presenter activePresenter; /++ Request handler that creates the presenter then forwards to the [dispatcher] function. Renders 404 if the dispatcher did not handle the request. Will automatically serve the presenter.style and presenter.script as "style.css" and "script.js" +/ void handler(Cgi cgi) { auto presenter = new Presenter; activePresenter = presenter; scope(exit) activePresenter = null; if(cgi.dispatcher!DispatcherArgs(presenter)) return; switch(cgi.pathInfo) { case "/style.css": cgi.setCache(true); cgi.setResponseContentType("text/css"); cgi.write(presenter.style(), true); break; case "/script.js": cgi.setCache(true); cgi.setResponseContentType("application/javascript"); cgi.write(presenter.script(), true); break; default: presenter.renderBasicError(cgi, 404); } } mixin GenericMain!handler; } mixin template DispatcherMain(DispatcherArgs...) if(!is(DispatcherArgs[0] : WebPresenter!T, T)) { class GenericPresenter : WebPresenter!GenericPresenter {} mixin DispatcherMain!(GenericPresenter, DispatcherArgs); } private string simpleHtmlEncode(string s) { return s.replace("&", "&").replace("<", "<").replace(">", ">").replace("\n", "
\n"); } string messageFromException(Throwable t) { string message; if(t !is null) { debug message = t.toString(); else message = "An unexpected error has occurred."; } else { message = "Unknown error"; } return message; } string plainHttpError(bool isCgi, string type, Throwable t) { auto message = messageFromException(t); message = simpleHtmlEncode(message); return format("%s %s\r\nContent-Length: %s\r\n\r\n%s", isCgi ? "Status:" : "HTTP/1.0", type, message.length, message); } // returns true if we were able to recover reasonably bool handleException(Cgi cgi, Throwable t) { if(cgi.isClosed) { // if the channel has been explicitly closed, we can't handle it here return true; } if(cgi.outputtedResponseData) { // the headers are sent, but the channel is open... since it closes if all was sent, we can append an error message here. return false; // but I don't want to, since I don't know what condition the output is in; I don't want to inject something (nor check the content-type for that matter. So we say it was not a clean handling. } else { // no headers are sent, we can send a full blown error and recover cgi.setCache(false); cgi.setResponseContentType("text/html"); cgi.setResponseLocation(null); // cancel the redirect cgi.setResponseStatus("500 Internal Server Error"); cgi.write(simpleHtmlEncode(messageFromException(t))); cgi.close(); return true; } } bool isCgiRequestMethod(string s) { s = s.toUpper(); if(s == "COMMANDLINE") return true; foreach(member; __traits(allMembers, Cgi.RequestMethod)) if(s == member) return true; return false; } /// If you want to use a subclass of Cgi with generic main, use this mixin. mixin template CustomCgiMain(CustomCgi, alias fun, long maxContentLength = defaultMaxContentLength) if(is(CustomCgi : Cgi)) { // kinda hacky - the T... is passed to Cgi's constructor in standard cgi mode, and ignored elsewhere void main(string[] args) { cgiMainImpl!(fun, CustomCgi, maxContentLength)(args); } } version(embedded_httpd_processes) __gshared int processPoolSize = 8; // Returns true if run. You should exit the program after that. bool tryAddonServers(string[] args) { if(args.length > 1) { // run the special separate processes if needed switch(args[1]) { case "--websocket-server": version(with_addon_servers) websocketServers[args[2]](args[3 .. $]); else printf("Add-on servers not compiled in.\n"); return true; case "--websocket-servers": import core.demangle; version(with_addon_servers_connections) foreach(k, v; websocketServers) writeln(k, "\t", demangle(k)); return true; case "--session-server": version(with_addon_servers) runSessionServer(); else printf("Add-on servers not compiled in.\n"); return true; case "--event-server": version(with_addon_servers) runEventServer(); else printf("Add-on servers not compiled in.\n"); return true; case "--timer-server": version(with_addon_servers) runTimerServer(); else printf("Add-on servers not compiled in.\n"); return true; case "--timed-jobs": import core.demangle; version(with_addon_servers_connections) foreach(k, v; scheduledJobHandlers) writeln(k, "\t", demangle(k)); return true; case "--timed-job": scheduledJobHandlers[args[2]](args[3 .. $]); return true; default: // intentionally blank - do nothing and carry on to run normally } } return false; } /// Tries to simulate a request from the command line. Returns true if it does, false if it didn't find the args. bool trySimulatedRequest(alias fun, CustomCgi = Cgi)(string[] args) if(is(CustomCgi : Cgi)) { // we support command line thing for easy testing everywhere // it needs to be called ./app method uri [other args...] if(args.length >= 3 && isCgiRequestMethod(args[1])) { Cgi cgi = new CustomCgi(args); scope(exit) cgi.dispose(); fun(cgi); cgi.close(); return true; } return false; } /++ A server control and configuration struct, as a potential alternative to calling [GenericMain] or [cgiMainImpl]. See the source of [cgiMainImpl] to an example of how you can use it. History: Added Sept 26, 2020 (release version 8.5). +/ struct RequestServer { /// string listeningHost = defaultListeningHost(); /// ushort listeningPort = defaultListeningPort(); /++ Uses a fork() call, if available, to provide additional crash resiliency and possibly improved performance. On the other hand, if you fork, you must not assume any memory is shared between requests (you shouldn't be anyway though! But if you have to, you probably want to set this to false and use an explicit threaded server with [serveEmbeddedHttp]) and [stop] may not work as well. History: Added August 12, 2022 (dub v10.9). Previously, this was only configurable through the `-version=cgi_no_fork` argument to dmd. That version still defines the value of `cgi_use_fork_default`, used to initialize this, for compatibility. +/ bool useFork = cgi_use_fork_default; /++ Determines the number of worker threads to spawn per process, for server modes that use worker threads. 0 will use a default based on the number of cpus modified by the server mode. History: Added August 12, 2022 (dub v10.9) +/ int numberOfThreads = 0; /// this(string defaultHost, ushort defaultPort) { this.listeningHost = defaultHost; this.listeningPort = defaultPort; } /// this(ushort defaultPort) { listeningPort = defaultPort; } /++ Reads the command line arguments into the values here. Possible arguments are `--listening-host`, `--listening-port` (or `--port`), `--uid`, and `--gid`. +/ void configureFromCommandLine(string[] args) { bool foundPort = false; bool foundHost = false; bool foundUid = false; bool foundGid = false; foreach(arg; args) { if(foundPort) { listeningPort = to!ushort(arg); foundPort = false; } if(foundHost) { listeningHost = arg; foundHost = false; } if(foundUid) { privilegesDropToUid = to!uid_t(arg); foundUid = false; } if(foundGid) { privilegesDropToGid = to!gid_t(arg); foundGid = false; } if(arg == "--listening-host" || arg == "-h" || arg == "/listening-host") foundHost = true; else if(arg == "--port" || arg == "-p" || arg == "/port" || arg == "--listening-port") foundPort = true; else if(arg == "--uid") foundUid = true; else if(arg == "--gid") foundGid = true; } } version(Windows) { private alias uid_t = int; private alias gid_t = int; } /// user (uid) to drop privileges to /// 0 … do nothing uid_t privilegesDropToUid = 0; /// group (gid) to drop privileges to /// 0 … do nothing gid_t privilegesDropToGid = 0; private void dropPrivileges() { version(Posix) { import core.sys.posix.unistd; if (privilegesDropToGid != 0 && setgid(privilegesDropToGid) != 0) throw new Exception("Dropping privileges via setgid() failed."); if (privilegesDropToUid != 0 && setuid(privilegesDropToUid) != 0) throw new Exception("Dropping privileges via setuid() failed."); } else { // FIXME: Windows? //pragma(msg, "Dropping privileges is not implemented for this platform"); } // done, set zero privilegesDropToGid = 0; privilegesDropToUid = 0; } /++ Serves a single HTTP request on this thread, with an embedded server, then stops. Designed for cases like embedded oauth responders History: Added Oct 10, 2020. Example: --- import arsd.cgi; void main() { RequestServer server = RequestServer("127.0.0.1", 6789); string oauthCode; string oauthScope; server.serveHttpOnce!((cgi) { oauthCode = cgi.request("code"); oauthScope = cgi.request("scope"); cgi.write("Thank you, please return to the application."); }); // use the code and scope given } --- +/ void serveHttpOnce(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() { import std.socket; bool tcp; void delegate() cleanup; auto socket = startListening(listeningHost, listeningPort, tcp, cleanup, 1, &dropPrivileges); auto connection = socket.accept(); doThreadHttpConnectionGuts!(CustomCgi, fun, true)(connection); if(cleanup) cleanup(); } /++ Starts serving requests according to the current configuration. +/ void serve(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() { version(netman_httpd) { // Obsolete! import arsd.httpd; // what about forwarding the other constructor args? // this probably needs a whole redoing... serveHttp!CustomCgi(&fun, listeningPort);//5005); return; } else version(embedded_httpd_processes) { serveEmbeddedHttpdProcesses!(fun, CustomCgi)(this); } else version(embedded_httpd_threads) { serveEmbeddedHttp!(fun, CustomCgi, maxContentLength)(); } else version(scgi) { serveScgi!(fun, CustomCgi, maxContentLength)(); } else version(fastcgi) { serveFastCgi!(fun, CustomCgi, maxContentLength)(this); } else version(stdio_http) { serveSingleHttpConnectionOnStdio!(fun, CustomCgi, maxContentLength)(); } else { //version=plain_cgi; handleCgiRequest!(fun, CustomCgi, maxContentLength)(); } } /++ Runs the embedded HTTP thread server specifically, regardless of which build configuration you have. If you want the forking worker process server, you do need to compile with the embedded_httpd_processes config though. +/ shared void serveEmbeddedHttp(alias fun, T, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)(shared T _this) { globalStopFlag = false; static if(__traits(isStaticFunction, fun)) void funToUse(CustomCgi cgi) { fun(_this, cgi); } else void funToUse(CustomCgi cgi) { static if(__VERSION__ > 2097) __traits(child, _inst_this, fun)(_inst_this, cgi); else static assert(0, "Not implemented in your compiler version!"); } auto manager = new ListeningConnectionManager(listeningHost, listeningPort, &doThreadHttpConnection!(CustomCgi, funToUse), null, useFork, numberOfThreads); manager.listen(); } /++ Runs the embedded SCGI server specifically, regardless of which build configuration you have. +/ void serveScgi(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() { globalStopFlag = false; auto manager = new ListeningConnectionManager(listeningHost, listeningPort, &doThreadScgiConnection!(CustomCgi, fun, maxContentLength), null, useFork, numberOfThreads); manager.listen(); } /++ Serves a single "connection", but the connection is spoken on stdin and stdout instead of on a socket. Intended for cases like working from systemd, like discussed here: [https://forum.dlang.org/post/avmkfdiitirnrenzljwc@forum.dlang.org] History: Added May 29, 2021 +/ void serveSingleHttpConnectionOnStdio(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() { doThreadHttpConnectionGuts!(CustomCgi, fun, true)(new FakeSocketForStdin()); } /++ The [stop] function sets a flag that request handlers can (and should) check periodically. If a handler doesn't respond to this flag, the library will force the issue. This determines when and how the issue will be forced. +/ enum ForceStop { /++ Stops accepting new requests, but lets ones already in the queue start and complete before exiting. +/ afterQueuedRequestsComplete, /++ Finishes requests already started their handlers, but drops any others in the queue. Streaming handlers should cooperate and exit gracefully, but if they don't, it will continue waiting for them. +/ afterCurrentRequestsComplete, /++ Partial response writes will throw an exception, cancelling any streaming response, but complete writes will continue to process. Request handlers that respect the stop token will also gracefully cancel. +/ cancelStreamingRequestsEarly, /++ All writes will throw. +/ cancelAllRequestsEarly, /++ Use OS facilities to forcibly kill running threads. The server process will be in an undefined state after this call (if this call ever returns). +/ forciblyTerminate, } version(embedded_httpd_processes) {} else /++ Stops serving after the current requests are completed. Bugs: Not implemented on version=embedded_httpd_processes, version=fastcgi on any system, or embedded_httpd on Windows (it does work on embedded_httpd_hybrid on Windows however). Only partially implemented on non-Linux posix systems. You might also try SIGINT perhaps. The stopPriority is not yet fully implemented. +/ static void stop(ForceStop stopPriority = ForceStop.afterCurrentRequestsComplete) { globalStopFlag = true; version(Posix) { if(cancelfd > 0) { ulong a = 1; core.sys.posix.unistd.write(cancelfd, &a, a.sizeof); } } version(Windows) { if(iocp) { foreach(i; 0 .. 16) // FIXME PostQueuedCompletionStatus(iocp, 0, cast(ULONG_PTR) null, null); } } } } private alias AliasSeq(T...) = T; version(with_breaking_cgi_features) mixin(q{ template ThisFor(alias t) { static if(__traits(isStaticFunction, t)) { alias ThisFor = AliasSeq!(); } else { alias ThisFor = __traits(parent, t); } } }); else alias ThisFor(alias t) = AliasSeq!(); private __gshared bool globalStopFlag = false; version(embedded_httpd_processes) void serveEmbeddedHttpdProcesses(alias fun, CustomCgi = Cgi)(RequestServer params) { import core.sys.posix.unistd; import core.sys.posix.sys.socket; import core.sys.posix.netinet.in_; //import std.c.linux.socket; int sock = socket(AF_INET, SOCK_STREAM, 0); if(sock == -1) throw new Exception("socket"); cloexec(sock); { sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(params.listeningPort); auto lh = params.listeningHost; if(lh.length) { if(inet_pton(AF_INET, lh.toStringz(), &addr.sin_addr.s_addr) != 1) throw new Exception("bad listening host given, please use an IP address.\nExample: --listening-host 127.0.0.1 means listen only on Localhost.\nExample: --listening-host 0.0.0.0 means listen on all interfaces.\nOr you can pass any other single numeric IPv4 address."); } else addr.sin_addr.s_addr = INADDR_ANY; // HACKISH int on = 1; setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &on, on.sizeof); // end hack if(bind(sock, cast(sockaddr*) &addr, addr.sizeof) == -1) { close(sock); throw new Exception("bind"); } // FIXME: if this queue is full, it will just ignore it // and wait for the client to retransmit it. This is an // obnoxious timeout condition there. if(sock.listen(128) == -1) { close(sock); throw new Exception("listen"); } params.dropPrivileges(); } version(embedded_httpd_processes_accept_after_fork) {} else { int pipeReadFd; int pipeWriteFd; { int[2] pipeFd; if(socketpair(AF_UNIX, SOCK_DGRAM, 0, pipeFd)) { import core.stdc.errno; throw new Exception("pipe failed " ~ to!string(errno)); } pipeReadFd = pipeFd[0]; pipeWriteFd = pipeFd[1]; } } int processCount; pid_t newPid; reopen: while(processCount < processPoolSize) { newPid = fork(); if(newPid == 0) { // start serving on the socket //ubyte[4096] backingBuffer; for(;;) { bool closeConnection; uint i; sockaddr addr; i = addr.sizeof; version(embedded_httpd_processes_accept_after_fork) { int s = accept(sock, &addr, &i); int opt = 1; import core.sys.posix.netinet.tcp; // the Cgi class does internal buffering, so disabling this // helps with latency in many cases... setsockopt(s, IPPROTO_TCP, TCP_NODELAY, &opt, opt.sizeof); cloexec(s); } else { int s; auto readret = read_fd(pipeReadFd, &s, s.sizeof, &s); if(readret != s.sizeof) { import core.stdc.errno; throw new Exception("pipe read failed " ~ to!string(errno)); } //writeln("process ", getpid(), " got socket ", s); } try { if(s == -1) throw new Exception("accept"); scope(failure) close(s); //ubyte[__traits(classInstanceSize, BufferedInputRange)] bufferedRangeContainer; auto ir = new BufferedInputRange(s); //auto ir = emplace!BufferedInputRange(bufferedRangeContainer, s, backingBuffer); while(!ir.empty) { //ubyte[__traits(classInstanceSize, CustomCgi)] cgiContainer; Cgi cgi; try { cgi = new CustomCgi(ir, &closeConnection); cgi._outputFileHandle = cast(CgiConnectionHandle) s; // if we have a single process and the browser tries to leave the connection open while concurrently requesting another, it will block everything an deadlock since there's no other server to accept it. By closing after each request in this situation, it tells the browser to serialize for us. if(processPoolSize <= 1) closeConnection = true; //cgi = emplace!CustomCgi(cgiContainer, ir, &closeConnection); } catch(Throwable t) { // a construction error is either bad code or bad request; bad request is what it should be since this is bug free :P // anyway let's kill the connection version(CRuntime_Musl) { // LockingTextWriter fails here // so working around it auto estr = t.toString(); stderr.rawWrite(estr); stderr.rawWrite("\n"); } else stderr.writeln(t.toString()); sendAll(ir.source, plainHttpError(false, "400 Bad Request", t)); closeConnection = true; break; } assert(cgi !is null); scope(exit) cgi.dispose(); try { fun(cgi); cgi.close(); if(cgi.websocketMode) closeConnection = true; } catch(ConnectionException ce) { closeConnection = true; } catch(Throwable t) { // a processing error can be recovered from version(CRuntime_Musl) { // LockingTextWriter fails here // so working around it auto estr = t.toString(); stderr.rawWrite(estr); } else { stderr.writeln(t.toString); } if(!handleException(cgi, t)) closeConnection = true; } if(closeConnection) { ir.source.close(); break; } else { if(!ir.empty) ir.popFront(); // get the next else if(ir.sourceClosed) { ir.source.close(); } } } ir.source.close(); } catch(Throwable t) { version(CRuntime_Musl) {} else debug writeln(t); // most likely cause is a timeout } } } else if(newPid < 0) { throw new Exception("fork failed"); } else { processCount++; } } // the parent should wait for its children... if(newPid) { import core.sys.posix.sys.wait; version(embedded_httpd_processes_accept_after_fork) {} else { import core.sys.posix.sys.select; int[] fdQueue; while(true) { // writeln("select call"); int nfds = pipeWriteFd; if(sock > pipeWriteFd) nfds = sock; nfds += 1; fd_set read_fds; fd_set write_fds; FD_ZERO(&read_fds); FD_ZERO(&write_fds); FD_SET(sock, &read_fds); if(fdQueue.length) FD_SET(pipeWriteFd, &write_fds); auto ret = select(nfds, &read_fds, &write_fds, null, null); if(ret == -1) { import core.stdc.errno; if(errno == EINTR) goto try_wait; else throw new Exception("wtf select"); } int s = -1; if(FD_ISSET(sock, &read_fds)) { uint i; sockaddr addr; i = addr.sizeof; s = accept(sock, &addr, &i); cloexec(s); import core.sys.posix.netinet.tcp; int opt = 1; setsockopt(s, IPPROTO_TCP, TCP_NODELAY, &opt, opt.sizeof); } if(FD_ISSET(pipeWriteFd, &write_fds)) { if(s == -1 && fdQueue.length) { s = fdQueue[0]; fdQueue = fdQueue[1 .. $]; // FIXME reuse buffer } write_fd(pipeWriteFd, &s, s.sizeof, s); close(s); // we are done with it, let the other process take ownership } else fdQueue ~= s; } } try_wait: int status; while(-1 != wait(&status)) { version(CRuntime_Musl) {} else { import std.stdio; writeln("Process died ", status); } processCount--; goto reopen; } close(sock); } } version(fastcgi) void serveFastCgi(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)(RequestServer params) { // SetHandler fcgid-script FCGX_Stream* input, output, error; FCGX_ParamArray env; const(ubyte)[] getFcgiChunk() { const(ubyte)[] ret; while(FCGX_HasSeenEOF(input) != -1) ret ~= cast(ubyte) FCGX_GetChar(input); return ret; } void writeFcgi(const(ubyte)[] data) { FCGX_PutStr(data.ptr, data.length, output); } void doARequest() { string[string] fcgienv; for(auto e = env; e !is null && *e !is null; e++) { string cur = to!string(*e); auto idx = cur.indexOf("="); string name, value; if(idx == -1) name = cur; else { name = cur[0 .. idx]; value = cur[idx + 1 .. $]; } fcgienv[name] = value; } void flushFcgi() { FCGX_FFlush(output); } Cgi cgi; try { cgi = new CustomCgi(maxContentLength, fcgienv, &getFcgiChunk, &writeFcgi, &flushFcgi); } catch(Throwable t) { FCGX_PutStr(cast(ubyte*) t.msg.ptr, t.msg.length, error); writeFcgi(cast(const(ubyte)[]) plainHttpError(true, "400 Bad Request", t)); return; //continue; } assert(cgi !is null); scope(exit) cgi.dispose(); try { fun(cgi); cgi.close(); } catch(Throwable t) { // log it to the error stream FCGX_PutStr(cast(ubyte*) t.msg.ptr, t.msg.length, error); // handle it for the user, if we can if(!handleException(cgi, t)) return; // continue; } } auto lp = params.listeningPort; auto host = params.listeningHost; FCGX_Request request; if(lp || !host.empty) { // if a listening port was specified on the command line, we want to spawn ourself // (needed for nginx without spawn-fcgi, e.g. on Windows) FCGX_Init(); int sock; if(host.startsWith("unix:")) { sock = FCGX_OpenSocket(toStringz(params.listeningHost["unix:".length .. $]), 12); } else if(host.startsWith("abstract:")) { sock = FCGX_OpenSocket(toStringz("\0" ~ params.listeningHost["abstract:".length .. $]), 12); } else { sock = FCGX_OpenSocket(toStringz(params.listeningHost ~ ":" ~ to!string(lp)), 12); } if(sock < 0) throw new Exception("Couldn't listen on the port"); FCGX_InitRequest(&request, sock, 0); while(FCGX_Accept_r(&request) >= 0) { input = request.inStream; output = request.outStream; error = request.errStream; env = request.envp; doARequest(); } } else { // otherwise, assume the httpd is doing it (the case for Apache, IIS, and Lighttpd) // using the version with a global variable since we are separate processes anyway while(FCGX_Accept(&input, &output, &error, &env) >= 0) { doARequest(); } } } /// Returns the default listening port for the current cgi configuration. 8085 for embedded httpd, 4000 for scgi, irrelevant for others. ushort defaultListeningPort() { version(netman_httpd) return 8080; else version(embedded_httpd_processes) return 8085; else version(embedded_httpd_threads) return 8085; else version(scgi) return 4000; else return 0; } /// Default host for listening. 127.0.0.1 for scgi, null (aka all interfaces) for all others. If you want the server directly accessible from other computers on the network, normally use null. If not, 127.0.0.1 is a bit better. Settable with default handlers with --listening-host command line argument. string defaultListeningHost() { version(netman_httpd) return null; else version(embedded_httpd_processes) return null; else version(embedded_httpd_threads) return null; else version(scgi) return "127.0.0.1"; else return null; } /++ This is the function [GenericMain] calls. View its source for some simple boilerplate you can copy/paste and modify, or you can call it yourself from your `main`. Please note that this may spawn other helper processes that will call `main` again. It does this currently for the timer server and event source server (and the quasi-deprecated web socket server). Params: fun = Your request handler CustomCgi = a subclass of Cgi, if you wise to customize it further maxContentLength = max POST size you want to allow args = command-line arguments History: Documented Sept 26, 2020. +/ void cgiMainImpl(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)(string[] args) if(is(CustomCgi : Cgi)) { if(tryAddonServers(args)) return; if(trySimulatedRequest!(fun, CustomCgi)(args)) return; RequestServer server; // you can change the port here if you like // server.listeningPort = 9000; // then call this to let the command line args override your default server.configureFromCommandLine(args); // and serve the request(s). server.serve!(fun, CustomCgi, maxContentLength)(); } //version(plain_cgi) void handleCgiRequest(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() { // standard CGI is the default version // Set stdin to binary mode if necessary to avoid mangled newlines // the fact that stdin is global means this could be trouble but standard cgi request // handling is one per process anyway so it shouldn't actually be threaded here or anything. version(Windows) { version(Win64) _setmode(std.stdio.stdin.fileno(), 0x8000); else setmode(std.stdio.stdin.fileno(), 0x8000); } Cgi cgi; try { cgi = new CustomCgi(maxContentLength); version(Posix) cgi._outputFileHandle = cast(CgiConnectionHandle) 1; // stdout else version(Windows) cgi._outputFileHandle = cast(CgiConnectionHandle) GetStdHandle(STD_OUTPUT_HANDLE); else static assert(0); } catch(Throwable t) { version(CRuntime_Musl) { // LockingTextWriter fails here // so working around it auto s = t.toString(); stderr.rawWrite(s); stdout.rawWrite(plainHttpError(true, "400 Bad Request", t)); } else { stderr.writeln(t.msg); // the real http server will probably handle this; // most likely, this is a bug in Cgi. But, oh well. stdout.write(plainHttpError(true, "400 Bad Request", t)); } return; } assert(cgi !is null); scope(exit) cgi.dispose(); try { fun(cgi); cgi.close(); } catch (Throwable t) { version(CRuntime_Musl) { // LockingTextWriter fails here // so working around it auto s = t.msg; stderr.rawWrite(s); } else { stderr.writeln(t.msg); } if(!handleException(cgi, t)) return; } } private __gshared int cancelfd = -1; /+ The event loop for embedded_httpd_threads will prolly fiber dispatch cgi constructors too, so slow posts will not monopolize a worker thread. May want to provide the worker task system just need to ensure all the fibers has a big enough stack for real work... would also ideally like to reuse them. So prolly bir would switch it to nonblocking. If it would block, it epoll registers one shot with this existing fiber to take it over. new connection comes in. it picks a fiber off the free list, or if there is none, it creates a new one. this fiber handles this connection the whole time. epoll triggers the fiber when something comes in. it is called by a random worker thread, it might change at any time. at least during the constructor. maybe into the main body it will stay tied to a thread just so TLS stuff doesn't randomly change in the middle. but I could specify if you yield all bets are off. when the request is finished, if there's more data buffered, it just keeps going. if there is no more data buffered, it epoll ctls to get triggered when more data comes in. all one shot. when a connection is closed, the fiber returns and is then reset and added to the free list. if the free list is full, the fiber is just freed, this means it will balloon to a certain size but not generally grow beyond that unless the activity keeps going. 256 KB stack i thnk per fiber. 4,000 active fibers per gigabyte of memory. So the fiber has its own magic methods to read and write. if they would block, it registers for epoll and yields. when it returns, it read/writes and then returns back normal control. basically you issue the command and it tells you when it is done it needs to DEL the epoll thing when it is closed. add it when opened. mod it when anther thing issued +/ /++ The stack size when a fiber is created. You can set this from your main or from a shared static constructor to optimize your memory use if you know you don't need this much space. Be careful though, some functions use more stack space than you realize and a recursive function (including ones like in dom.d) can easily grow fast! History: Added July 10, 2021. Previously, it used the druntime default of 16 KB. +/ version(cgi_use_fiber) __gshared size_t fiberStackSize = 4096 * 100; version(cgi_use_fiber) class CgiFiber : Fiber { private void function(Socket) f_handler; private void f_handler_dg(Socket s) { // to avoid extra allocation w/ function f_handler(s); } this(void function(Socket) handler) { this.f_handler = handler; this(&f_handler_dg); } this(void delegate(Socket) handler) { this.handler = handler; super(&run, fiberStackSize); } Socket connection; void delegate(Socket) handler; void run() { handler(connection); } void delegate() postYield; private void setPostYield(scope void delegate() py) @nogc { postYield = cast(void delegate()) py; } void proceed() { try { call(); auto py = postYield; postYield = null; if(py !is null) py(); } catch(Exception e) { if(connection) connection.close(); goto terminate; } if(state == State.TERM) { terminate: import core.memory; GC.removeRoot(cast(void*) this); } } } version(cgi_use_fiber) version(Windows) { extern(Windows) private { import core.sys.windows.mswsock; alias GROUP=uint; alias LPWSAPROTOCOL_INFOW = void*; SOCKET WSASocketW(int af, int type, int protocol, LPWSAPROTOCOL_INFOW lpProtocolInfo, GROUP g, DWORD dwFlags); int WSASend(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent, DWORD dwFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine); int WSARecv(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine); struct WSABUF { ULONG len; CHAR *buf; } alias LPWSABUF = WSABUF*; alias WSAOVERLAPPED = OVERLAPPED; alias LPWSAOVERLAPPED = LPOVERLAPPED; /+ alias LPFN_ACCEPTEX = BOOL function( SOCKET sListenSocket, SOCKET sAcceptSocket, //_Out_writes_bytes_(dwReceiveDataLength+dwLocalAddressLength+dwRemoteAddressLength) PVOID lpOutputBuffer, void* lpOutputBuffer, WORD dwReceiveDataLength, WORD dwLocalAddressLength, WORD dwRemoteAddressLength, LPDWORD lpdwBytesReceived, LPOVERLAPPED lpOverlapped ); enum WSAID_ACCEPTEX = GUID([0xb5367df1,0xcbac,0x11cf,[0x95,0xca,0x00,0x80,0x5f,0x48,0xa1,0x92]]); +/ enum WSAID_GETACCEPTEXSOCKADDRS = GUID(0xb5367df2,0xcbac,0x11cf,[0x95,0xca,0x00,0x80,0x5f,0x48,0xa1,0x92]); } private class PseudoblockingOverlappedSocket : Socket { SOCKET handle; CgiFiber fiber; this(AddressFamily af, SocketType st) { auto handle = WSASocketW(af, st, 0, null, 0, 1 /*WSA_FLAG_OVERLAPPED*/); if(!handle) throw new Exception("WSASocketW"); this.handle = handle; iocp = CreateIoCompletionPort(cast(HANDLE) handle, iocp, cast(ULONG_PTR) cast(void*) this, 0); if(iocp is null) { writeln(GetLastError()); throw new Exception("CreateIoCompletionPort"); } super(cast(socket_t) handle, af); } this() pure nothrow @trusted { assert(0); } override void blocking(bool) {} // meaningless to us, just ignore it. protected override Socket accepting() pure nothrow { assert(0); } bool addressesParsed; Address la; Address ra; private void populateAddresses() { if(addressesParsed) return; addressesParsed = true; int lalen, ralen; sockaddr_in* la; sockaddr_in* ra; lpfnGetAcceptExSockaddrs( scratchBuffer.ptr, 0, // same as in the AcceptEx call! sockaddr_in.sizeof + 16, sockaddr_in.sizeof + 16, cast(sockaddr**) &la, &lalen, cast(sockaddr**) &ra, &ralen ); if(la) this.la = new InternetAddress(*la); if(ra) this.ra = new InternetAddress(*ra); } override @property @trusted Address localAddress() { populateAddresses(); return la; } override @property @trusted Address remoteAddress() { populateAddresses(); return ra; } PseudoblockingOverlappedSocket accepted; __gshared static LPFN_ACCEPTEX lpfnAcceptEx; __gshared static typeof(&GetAcceptExSockaddrs) lpfnGetAcceptExSockaddrs; override Socket accept() @trusted { __gshared static LPFN_ACCEPTEX lpfnAcceptEx; if(lpfnAcceptEx is null) { DWORD dwBytes; GUID GuidAcceptEx = WSAID_ACCEPTEX; auto iResult = WSAIoctl(handle, 0xc8000006 /*SIO_GET_EXTENSION_FUNCTION_POINTER*/, &GuidAcceptEx, GuidAcceptEx.sizeof, &lpfnAcceptEx, lpfnAcceptEx.sizeof, &dwBytes, null, null); GuidAcceptEx = WSAID_GETACCEPTEXSOCKADDRS; iResult = WSAIoctl(handle, 0xc8000006 /*SIO_GET_EXTENSION_FUNCTION_POINTER*/, &GuidAcceptEx, GuidAcceptEx.sizeof, &lpfnGetAcceptExSockaddrs, lpfnGetAcceptExSockaddrs.sizeof, &dwBytes, null, null); } auto pfa = new PseudoblockingOverlappedSocket(AddressFamily.INET, SocketType.STREAM); accepted = pfa; SOCKET pendingForAccept = pfa.handle; DWORD ignored; auto ret = lpfnAcceptEx(handle, pendingForAccept, // buffer to receive up front pfa.scratchBuffer.ptr, 0, // size of local and remote addresses. normally + 16. sockaddr_in.sizeof + 16, sockaddr_in.sizeof + 16, &ignored, // bytes would be given through the iocp instead but im not even requesting the thing &overlapped ); return pfa; } override void connect(Address to) { assert(0); } DWORD lastAnswer; ubyte[1024] scratchBuffer; static assert(scratchBuffer.length > sockaddr_in.sizeof * 2 + 32); WSABUF[1] buffer; OVERLAPPED overlapped; override ptrdiff_t send(scope const(void)[] buf, SocketFlags flags) @trusted { overlapped = overlapped.init; buffer[0].len = cast(DWORD) buf.length; buffer[0].buf = cast(CHAR*) buf.ptr; fiber.setPostYield( () { if(!WSASend(handle, buffer.ptr, cast(DWORD) buffer.length, null, 0, &overlapped, null)) { if(GetLastError() != 997) { //throw new Exception("WSASend fail"); } } }); Fiber.yield(); return lastAnswer; } override ptrdiff_t receive(scope void[] buf, SocketFlags flags) @trusted { overlapped = overlapped.init; buffer[0].len = cast(DWORD) buf.length; buffer[0].buf = cast(CHAR*) buf.ptr; DWORD flags2 = 0; fiber.setPostYield(() { if(!WSARecv(handle, buffer.ptr, cast(DWORD) buffer.length, null, &flags2 /* flags */, &overlapped, null)) { if(GetLastError() != 997) { //writeln("WSARecv ", WSAGetLastError()); //throw new Exception("WSARecv fail"); } } }); Fiber.yield(); return lastAnswer; } // I might go back and implement these for udp things. override ptrdiff_t receiveFrom(scope void[] buf, SocketFlags flags, ref Address from) @trusted { assert(0); } override ptrdiff_t receiveFrom(scope void[] buf, SocketFlags flags) @trusted { assert(0); } override ptrdiff_t sendTo(scope const(void)[] buf, SocketFlags flags, Address to) @trusted { assert(0); } override ptrdiff_t sendTo(scope const(void)[] buf, SocketFlags flags) @trusted { assert(0); } // lol overload sets alias send = typeof(super).send; alias receive = typeof(super).receive; alias sendTo = typeof(super).sendTo; alias receiveFrom = typeof(super).receiveFrom; } } void doThreadHttpConnection(CustomCgi, alias fun)(Socket connection) { assert(connection !is null); version(cgi_use_fiber) { auto fiber = new CgiFiber(&doThreadHttpConnectionGuts!(CustomCgi, fun)); version(Windows) { (cast(PseudoblockingOverlappedSocket) connection).fiber = fiber; } import core.memory; GC.addRoot(cast(void*) fiber); fiber.connection = connection; fiber.proceed(); } else { doThreadHttpConnectionGuts!(CustomCgi, fun)(connection); } } void doThreadHttpConnectionGuts(CustomCgi, alias fun, bool alwaysCloseConnection = false)(Socket connection) { scope(failure) { // catch all for other errors try { sendAll(connection, plainHttpError(false, "500 Internal Server Error", null)); connection.close(); } catch(Exception e) {} // swallow it, we're aborting anyway. } bool closeConnection = alwaysCloseConnection; /+ ubyte[4096] inputBuffer = void; ubyte[__traits(classInstanceSize, BufferedInputRange)] birBuffer = void; ubyte[__traits(classInstanceSize, CustomCgi)] cgiBuffer = void; birBuffer[] = cast(ubyte[]) typeid(BufferedInputRange).initializer()[]; BufferedInputRange ir = cast(BufferedInputRange) cast(void*) birBuffer.ptr; ir.__ctor(connection, inputBuffer[], true); +/ auto ir = new BufferedInputRange(connection); while(!ir.empty) { if(ir.view.length == 0) { ir.popFront(); if(ir.sourceClosed) { connection.close(); closeConnection = true; break; } } Cgi cgi; try { cgi = new CustomCgi(ir, &closeConnection); // There's a bunch of these casts around because the type matches up with // the -version=.... specifiers, just you can also create a RequestServer // and instantiate the things where the types don't match up. It isn't exactly // correct but I also don't care rn. Might FIXME and either remove it later or something. cgi._outputFileHandle = cast(CgiConnectionHandle) connection.handle; } catch(ConnectionClosedException ce) { closeConnection = true; break; } catch(ConnectionException ce) { // broken pipe or something, just abort the connection closeConnection = true; break; } catch(Throwable t) { // a construction error is either bad code or bad request; bad request is what it should be since this is bug free :P // anyway let's kill the connection version(CRuntime_Musl) { stderr.rawWrite(t.toString()); stderr.rawWrite("\n"); } else { stderr.writeln(t.toString()); } sendAll(connection, plainHttpError(false, "400 Bad Request", t)); closeConnection = true; break; } assert(cgi !is null); scope(exit) cgi.dispose(); try { fun(cgi); cgi.close(); if(cgi.websocketMode) closeConnection = true; } catch(ConnectionException ce) { // broken pipe or something, just abort the connection closeConnection = true; } catch(ConnectionClosedException ce) { // broken pipe or something, just abort the connection closeConnection = true; } catch(Throwable t) { // a processing error can be recovered from version(CRuntime_Musl) {} else stderr.writeln(t.toString); if(!handleException(cgi, t)) closeConnection = true; } if(globalStopFlag) closeConnection = true; if(closeConnection || alwaysCloseConnection) { connection.shutdown(SocketShutdown.BOTH); connection.close(); ir.dispose(); closeConnection = false; // don't reclose after loop break; } else { if(ir.front.length) { ir.popFront(); // we can't just discard the buffer, so get the next bit and keep chugging along } else if(ir.sourceClosed) { ir.source.shutdown(SocketShutdown.BOTH); ir.source.close(); ir.dispose(); closeConnection = false; } else { continue; // break; // this was for a keepalive experiment } } } if(closeConnection) { connection.shutdown(SocketShutdown.BOTH); connection.close(); ir.dispose(); } // I am otherwise NOT closing it here because the parent thread might still be able to make use of the keep-alive connection! } void doThreadScgiConnection(CustomCgi, alias fun, long maxContentLength)(Socket connection) { // and now we can buffer scope(failure) connection.close(); import al = std.algorithm; size_t size; string[string] headers; auto range = new BufferedInputRange(connection); more_data: auto chunk = range.front(); // waiting for colon for header length auto idx = indexOf(cast(string) chunk, ':'); if(idx == -1) { try { range.popFront(); } catch(Exception e) { // it is just closed, no big deal connection.close(); return; } goto more_data; } size = to!size_t(cast(string) chunk[0 .. idx]); chunk = range.consume(idx + 1); // reading headers if(chunk.length < size) range.popFront(0, size + 1); // we are now guaranteed to have enough chunk = range.front(); assert(chunk.length > size); idx = 0; string key; string value; foreach(part; al.splitter(chunk, '\0')) { if(idx & 1) { // odd is value value = cast(string)(part.idup); headers[key] = value; // commit } else key = cast(string)(part.idup); idx++; } enforce(chunk[size] == ','); // the terminator range.consume(size + 1); // reading data // this will be done by Cgi const(ubyte)[] getScgiChunk() { // we are already primed auto data = range.front(); if(data.length == 0 && !range.sourceClosed) { range.popFront(0); data = range.front(); } else if (range.sourceClosed) range.source.close(); return data; } void writeScgi(const(ubyte)[] data) { sendAll(connection, data); } void flushScgi() { // I don't *think* I have to do anything.... } Cgi cgi; try { cgi = new CustomCgi(maxContentLength, headers, &getScgiChunk, &writeScgi, &flushScgi); cgi._outputFileHandle = cast(CgiConnectionHandle) connection.handle; } catch(Throwable t) { sendAll(connection, plainHttpError(true, "400 Bad Request", t)); connection.close(); return; // this connection is dead } assert(cgi !is null); scope(exit) cgi.dispose(); try { fun(cgi); cgi.close(); connection.close(); } catch(Throwable t) { // no std err if(!handleException(cgi, t)) { connection.close(); return; } else { connection.close(); return; } } } string printDate(DateTime date) { char[29] buffer = void; printDateToBuffer(date, buffer[]); return buffer.idup; } int printDateToBuffer(DateTime date, char[] buffer) @nogc { assert(buffer.length >= 29); // 29 static length ? static immutable daysOfWeek = [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ]; static immutable months = [ null, "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ]; buffer[0 .. 3] = daysOfWeek[date.dayOfWeek]; buffer[3 .. 5] = ", "; buffer[5] = date.day / 10 + '0'; buffer[6] = date.day % 10 + '0'; buffer[7] = ' '; buffer[8 .. 11] = months[date.month]; buffer[11] = ' '; auto y = date.year; buffer[12] = cast(char) (y / 1000 + '0'); y %= 1000; buffer[13] = cast(char) (y / 100 + '0'); y %= 100; buffer[14] = cast(char) (y / 10 + '0'); y %= 10; buffer[15] = cast(char) (y + '0'); buffer[16] = ' '; buffer[17] = date.hour / 10 + '0'; buffer[18] = date.hour % 10 + '0'; buffer[19] = ':'; buffer[20] = date.minute / 10 + '0'; buffer[21] = date.minute % 10 + '0'; buffer[22] = ':'; buffer[23] = date.second / 10 + '0'; buffer[24] = date.second % 10 + '0'; buffer[25 .. $] = " GMT"; return 29; } // Referencing this gigantic typeid seems to remind the compiler // to actually put the symbol in the object file. I guess the immutable // assoc array array isn't actually included in druntime void hackAroundLinkerError() { stdout.rawWrite(typeid(const(immutable(char)[][])[immutable(char)[]]).toString()); stdout.rawWrite(typeid(immutable(char)[][][immutable(char)[]]).toString()); stdout.rawWrite(typeid(Cgi.UploadedFile[immutable(char)[]]).toString()); stdout.rawWrite(typeid(Cgi.UploadedFile[][immutable(char)[]]).toString()); stdout.rawWrite(typeid(immutable(Cgi.UploadedFile)[immutable(char)[]]).toString()); stdout.rawWrite(typeid(immutable(Cgi.UploadedFile[])[immutable(char)[]]).toString()); stdout.rawWrite(typeid(immutable(char[])[immutable(char)[]]).toString()); // this is getting kinda ridiculous btw. Moving assoc arrays // to the library is the pain that keeps on coming. // eh this broke the build on the work server // stdout.rawWrite(typeid(immutable(char)[][immutable(string[])])); stdout.rawWrite(typeid(immutable(string[])[immutable(char)[]]).toString()); } version(fastcgi) { pragma(lib, "fcgi"); static if(size_t.sizeof == 8) // 64 bit alias long c_int; else alias int c_int; extern(C) { struct FCGX_Stream { ubyte* rdNext; ubyte* wrNext; ubyte* stop; ubyte* stopUnget; c_int isReader; c_int isClosed; c_int wasFCloseCalled; c_int FCGI_errno; void* function(FCGX_Stream* stream) fillBuffProc; void* function(FCGX_Stream* stream, c_int doClose) emptyBuffProc; void* data; } // note: this is meant to be opaque, so don't access it directly struct FCGX_Request { int requestId; int role; FCGX_Stream* inStream; FCGX_Stream* outStream; FCGX_Stream* errStream; char** envp; void* paramsPtr; int ipcFd; int isBeginProcessed; int keepConnection; int appStatus; int nWriters; int flags; int listen_sock; } int FCGX_InitRequest(FCGX_Request *request, int sock, int flags); void FCGX_Init(); int FCGX_Accept_r(FCGX_Request *request); alias char** FCGX_ParamArray; c_int FCGX_Accept(FCGX_Stream** stdin, FCGX_Stream** stdout, FCGX_Stream** stderr, FCGX_ParamArray* envp); c_int FCGX_GetChar(FCGX_Stream* stream); c_int FCGX_PutStr(const ubyte* str, c_int n, FCGX_Stream* stream); int FCGX_HasSeenEOF(FCGX_Stream* stream); c_int FCGX_FFlush(FCGX_Stream *stream); int FCGX_OpenSocket(in char*, int); } } /* This might go int a separate module eventually. It is a network input helper class. */ import std.socket; version(cgi_use_fiber) { import core.thread; version(linux) { import core.sys.linux.epoll; int epfd = -1; // thread local because EPOLLEXCLUSIVE works much better this way... weirdly. } else version(Windows) { // declaring the iocp thing below... } else static assert(0, "The hybrid fiber server is not implemented on your OS."); } version(Windows) __gshared HANDLE iocp; version(cgi_use_fiber) { version(linux) private enum WakeupEvent { Read = EPOLLIN, Write = EPOLLOUT } else version(Windows) private enum WakeupEvent { Read, Write } else static assert(0); } version(cgi_use_fiber) private void registerEventWakeup(bool* registered, Socket source, WakeupEvent e) @nogc { // static cast since I know what i have in here and don't want to pay for dynamic cast auto f = cast(CgiFiber) cast(void*) Fiber.getThis(); version(linux) { f.setPostYield = () { if(*registered) { // rearm epoll_event evt; evt.events = e | EPOLLONESHOT; evt.data.ptr = cast(void*) f; if(epoll_ctl(epfd, EPOLL_CTL_MOD, source.handle, &evt) == -1) throw new Exception("epoll_ctl"); } else { // initial registration *registered = true ; int fd = source.handle; epoll_event evt; evt.events = e | EPOLLONESHOT; evt.data.ptr = cast(void*) f; if(epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &evt) == -1) throw new Exception("epoll_ctl"); } }; Fiber.yield(); f.setPostYield(null); } else version(Windows) { Fiber.yield(); } else static assert(0); } version(cgi_use_fiber) void unregisterSource(Socket s) { version(linux) { epoll_event evt; epoll_ctl(epfd, EPOLL_CTL_DEL, s.handle(), &evt); } else version(Windows) { // intentionally blank } else static assert(0); } // it is a class primarily for reference semantics // I might change this interface /// This is NOT ACTUALLY an input range! It is too different. Historical mistake kinda. class BufferedInputRange { version(Posix) this(int source, ubyte[] buffer = null) { this(new Socket(cast(socket_t) source, AddressFamily.INET), buffer); } this(Socket source, ubyte[] buffer = null, bool allowGrowth = true) { // if they connect but never send stuff to us, we don't want it wasting the process // so setting a time out version(cgi_use_fiber) source.blocking = false; else source.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(3)); this.source = source; if(buffer is null) { underlyingBuffer = new ubyte[4096]; this.allowGrowth = true; } else { underlyingBuffer = buffer; this.allowGrowth = allowGrowth; } assert(underlyingBuffer.length); // we assume view.ptr is always inside underlyingBuffer view = underlyingBuffer[0 .. 0]; popFront(); // prime } version(cgi_use_fiber) { bool registered; } void dispose() { version(cgi_use_fiber) { if(registered) unregisterSource(source); } } /** A slight difference from regular ranges is you can give it the maximum number of bytes to consume. IMPORTANT NOTE: the default is to consume nothing, so if you don't call consume() yourself and use a regular foreach, it will infinitely loop! The default is to do what a normal range does, and consume the whole buffer and wait for additional input. You can also specify 0, to append to the buffer, or any other number to remove the front n bytes and wait for more. */ void popFront(size_t maxBytesToConsume = 0 /*size_t.max*/, size_t minBytesToSettleFor = 0, bool skipConsume = false) { if(sourceClosed) throw new ConnectionClosedException("can't get any more data from a closed source"); if(!skipConsume) consume(maxBytesToConsume); // we might have to grow the buffer if(minBytesToSettleFor > underlyingBuffer.length || view.length == underlyingBuffer.length) { if(allowGrowth) { //import std.stdio; writeln("growth"); auto viewStart = view.ptr - underlyingBuffer.ptr; size_t growth = 4096; // make sure we have enough for what we're being asked for if(minBytesToSettleFor > 0 && minBytesToSettleFor - underlyingBuffer.length > growth) growth = minBytesToSettleFor - underlyingBuffer.length; //import std.stdio; writeln(underlyingBuffer.length, " ", viewStart, " ", view.length, " ", growth, " ", minBytesToSettleFor, " ", minBytesToSettleFor - underlyingBuffer.length); underlyingBuffer.length += growth; view = underlyingBuffer[viewStart .. view.length]; } else throw new Exception("No room left in the buffer"); } do { auto freeSpace = underlyingBuffer[view.ptr - underlyingBuffer.ptr + view.length .. $]; try_again: auto ret = source.receive(freeSpace); if(ret == Socket.ERROR) { if(wouldHaveBlocked()) { version(cgi_use_fiber) { registerEventWakeup(®istered, source, WakeupEvent.Read); goto try_again; } else { // gonna treat a timeout here as a close sourceClosed = true; return; } } version(Posix) { import core.stdc.errno; if(errno == EINTR || errno == EAGAIN) { goto try_again; } if(errno == ECONNRESET) { sourceClosed = true; return; } } throw new Exception(lastSocketError); // FIXME } if(ret == 0) { sourceClosed = true; return; } //import std.stdio; writeln(view.ptr); writeln(underlyingBuffer.ptr); writeln(view.length, " ", ret, " = ", view.length + ret); view = underlyingBuffer[view.ptr - underlyingBuffer.ptr .. view.length + ret]; //import std.stdio; writeln(cast(string) view); } while(view.length < minBytesToSettleFor); } /// Removes n bytes from the front of the buffer, and returns the new buffer slice. /// You might want to idup the data you are consuming if you store it, since it may /// be overwritten on the new popFront. /// /// You do not need to call this if you always want to wait for more data when you /// consume some. ubyte[] consume(size_t bytes) { view = view[bytes > $ ? $ : bytes .. $]; if(view.length == 0) { view = underlyingBuffer[0 .. 0]; // go ahead and reuse the beginning /* writeln("HERE"); popFront(0, 0, true); // try to load more if we can, checks if the source is closed writeln(cast(string)front); writeln("DONE"); */ } return front; } bool empty() { return sourceClosed && view.length == 0; } ubyte[] front() { return view; } invariant() { assert(view.ptr >= underlyingBuffer.ptr); // it should never be equal, since if that happens view ought to be empty, and thus reusing the buffer assert(view.ptr < underlyingBuffer.ptr + underlyingBuffer.length); } ubyte[] underlyingBuffer; bool allowGrowth; ubyte[] view; Socket source; bool sourceClosed; } private class FakeSocketForStdin : Socket { import std.stdio; this() { } private bool closed; override ptrdiff_t receive(scope void[] buffer, std.socket.SocketFlags) @trusted { if(closed) throw new Exception("Closed"); return stdin.rawRead(buffer).length; } override ptrdiff_t send(const scope void[] buffer, std.socket.SocketFlags) @trusted { if(closed) throw new Exception("Closed"); stdout.rawWrite(buffer); return buffer.length; } override void close() @trusted scope { (cast(void delegate() @nogc nothrow) &realClose)(); } override void shutdown(SocketShutdown s) { // FIXME } override void setOption(SocketOptionLevel, SocketOption, scope void[]) {} override void setOption(SocketOptionLevel, SocketOption, Duration) {} override @property @trusted Address remoteAddress() { return null; } override @property @trusted Address localAddress() { return null; } void realClose() { closed = true; try { stdin.close(); stdout.close(); } catch(Exception e) { } } } import core.sync.semaphore; import core.atomic; /** To use this thing: --- void handler(Socket s) { do something... } auto manager = new ListeningConnectionManager("127.0.0.1", 80, &handler, &delegateThatDropsPrivileges); manager.listen(); --- The 4th parameter is optional. I suggest you use BufferedInputRange(connection) to handle the input. As a packet comes in, you will get control. You can just continue; though to fetch more. FIXME: should I offer an event based async thing like netman did too? Yeah, probably. */ class ListeningConnectionManager { Semaphore semaphore; Socket[256] queue; shared(ubyte) nextIndexFront; ubyte nextIndexBack; shared(int) queueLength; Socket acceptCancelable() { version(Posix) { import core.sys.posix.sys.select; fd_set read_fds; FD_ZERO(&read_fds); FD_SET(listener.handle, &read_fds); if(cancelfd != -1) FD_SET(cancelfd, &read_fds); auto max = listener.handle > cancelfd ? listener.handle : cancelfd; auto ret = select(max + 1, &read_fds, null, null, null); if(ret == -1) { import core.stdc.errno; if(errno == EINTR) return null; else throw new Exception("wtf select"); } if(cancelfd != -1 && FD_ISSET(cancelfd, &read_fds)) { return null; } if(FD_ISSET(listener.handle, &read_fds)) return listener.accept(); return null; } else { Socket socket = listener; auto check = new SocketSet(); keep_looping: check.reset(); check.add(socket); // just to check the stop flag on a kinda busy loop. i hate this FIXME auto got = Socket.select(check, null, null, 3.seconds); if(got > 0) return listener.accept(); if(globalStopFlag) return null; else goto keep_looping; } } int defaultNumberOfThreads() { import std.parallelism; version(cgi_use_fiber) { return totalCPUs * 1 + 1; } else { // I times 4 here because there's a good chance some will be blocked on i/o. return totalCPUs * 4; } } void listen() { shared(int) loopBroken; version(Posix) { import core.sys.posix.signal; signal(SIGPIPE, SIG_IGN); } version(linux) { if(cancelfd == -1) cancelfd = eventfd(0, 0); } version(cgi_no_threads) { // NEVER USE THIS // it exists only for debugging and other special occasions // the thread mode is faster and less likely to stall the whole // thing when a request is slow while(!loopBroken && !globalStopFlag) { auto sn = acceptCancelable(); if(sn is null) continue; cloexec(sn); try { handler(sn); } catch(Exception e) { // if a connection goes wrong, we want to just say no, but try to carry on unless it is an Error of some sort (in which case, we'll die. You might want an external helper program to revive the server when it dies) sn.close(); } } } else { if(useFork) { version(linux) { //asm { int 3; } fork(); } } version(cgi_use_fiber) { version(Windows) { listener.accept(); } WorkerThread[] threads = new WorkerThread[](numberOfThreads); foreach(i, ref thread; threads) { thread = new WorkerThread(this, handler, cast(int) i); thread.start(); } bool fiber_crash_check() { bool hasAnyRunning; foreach(thread; threads) { if(!thread.isRunning) { thread.join(); } else hasAnyRunning = true; } return (!hasAnyRunning); } while(!globalStopFlag) { Thread.sleep(1.seconds); if(fiber_crash_check()) break; } } else { semaphore = new Semaphore(); ConnectionThread[] threads = new ConnectionThread[](numberOfThreads); foreach(i, ref thread; threads) { thread = new ConnectionThread(this, handler, cast(int) i); thread.start(); } while(!loopBroken && !globalStopFlag) { Socket sn; bool crash_check() { bool hasAnyRunning; foreach(thread; threads) { if(!thread.isRunning) { thread.join(); } else hasAnyRunning = true; } return (!hasAnyRunning); } void accept_new_connection() { sn = acceptCancelable(); if(sn is null) return; cloexec(sn); if(tcp) { // disable Nagle's algorithm to avoid a 40ms delay when we send/recv // on the socket because we do some buffering internally. I think this helps, // certainly does for small requests, and I think it does for larger ones too sn.setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, 1); sn.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(10)); } } void existing_connection_new_data() { // wait until a slot opens up //int waited = 0; while(queueLength >= queue.length) { Thread.sleep(1.msecs); //waited ++; } //if(waited) {import std.stdio; writeln(waited);} synchronized(this) { queue[nextIndexBack] = sn; nextIndexBack++; atomicOp!"+="(queueLength, 1); } semaphore.notify(); } accept_new_connection(); if(sn !is null) existing_connection_new_data(); else if(sn is null && globalStopFlag) { foreach(thread; threads) { semaphore.notify(); } Thread.sleep(50.msecs); } if(crash_check()) break; } } // FIXME: i typically stop this with ctrl+c which never // actually gets here. i need to do a sigint handler. if(cleanup) cleanup(); } } //version(linux) //int epoll_fd; bool tcp; void delegate() cleanup; private void function(Socket) fhandler; private void dg_handler(Socket s) { fhandler(s); } this(string host, ushort port, void function(Socket) handler, void delegate() dropPrivs = null, bool useFork = cgi_use_fork_default, int numberOfThreads = 0) { fhandler = handler; this(host, port, &dg_handler, dropPrivs, useFork, numberOfThreads); } this(string host, ushort port, void delegate(Socket) handler, void delegate() dropPrivs = null, bool useFork = cgi_use_fork_default, int numberOfThreads = 0) { this.handler = handler; this.useFork = useFork; this.numberOfThreads = numberOfThreads ? numberOfThreads : defaultNumberOfThreads(); listener = startListening(host, port, tcp, cleanup, 128, dropPrivs); version(cgi_use_fiber) if(useFork) listener.blocking = false; // this is the UI control thread and thus gets more priority Thread.getThis.priority = Thread.PRIORITY_MAX; } Socket listener; void delegate(Socket) handler; immutable bool useFork; int numberOfThreads; } Socket startListening(string host, ushort port, ref bool tcp, ref void delegate() cleanup, int backQueue, void delegate() dropPrivs) { Socket listener; if(host.startsWith("unix:")) { version(Posix) { listener = new Socket(AddressFamily.UNIX, SocketType.STREAM); cloexec(listener); string filename = host["unix:".length .. $].idup; listener.bind(new UnixAddress(filename)); cleanup = delegate() { listener.close(); import std.file; remove(filename); }; tcp = false; } else { throw new Exception("unix sockets not supported on this system"); } } else if(host.startsWith("abstract:")) { version(linux) { listener = new Socket(AddressFamily.UNIX, SocketType.STREAM); cloexec(listener); string filename = "\0" ~ host["abstract:".length .. $]; import std.stdio; stderr.writeln("Listening to abstract unix domain socket: ", host["abstract:".length .. $]); listener.bind(new UnixAddress(filename)); tcp = false; } else { throw new Exception("abstract unix sockets not supported on this system"); } } else { version(cgi_use_fiber) { version(Windows) listener = new PseudoblockingOverlappedSocket(AddressFamily.INET, SocketType.STREAM); else listener = new TcpSocket(); } else { listener = new TcpSocket(); } cloexec(listener); listener.setOption(SocketOptionLevel.SOCKET, SocketOption.REUSEADDR, true); listener.bind(host.length ? parseAddress(host, port) : new InternetAddress(port)); cleanup = delegate() { listener.close(); }; tcp = true; } listener.listen(backQueue); if (dropPrivs !is null) // can be null, backwards compatibility dropPrivs(); return listener; } // helper function to send a lot to a socket. Since this blocks for the buffer (possibly several times), you should probably call it in a separate thread or something. void sendAll(Socket s, const(void)[] data, string file = __FILE__, size_t line = __LINE__) { if(data.length == 0) return; ptrdiff_t amount; //import std.stdio; writeln("***",cast(string) data,"///"); do { amount = s.send(data); if(amount == Socket.ERROR) { version(cgi_use_fiber) { if(wouldHaveBlocked()) { bool registered = true; registerEventWakeup(®istered, s, WakeupEvent.Write); continue; } } throw new ConnectionException(s, lastSocketError, file, line); } assert(amount > 0); data = data[amount .. $]; } while(data.length); } class ConnectionException : Exception { Socket socket; this(Socket s, string msg, string file = __FILE__, size_t line = __LINE__) { this.socket = s; super(msg, file, line); } } alias void delegate(Socket) CMT; import core.thread; /+ cgi.d now uses a hybrid of event i/o and threads at the top level. Top level thread is responsible for accepting sockets and selecting on them. It then indicates to a child that a request is pending, and any random worker thread that is free handles it. It goes into blocking mode and handles that http request to completion. At that point, it goes back into the waiting queue. This concept is only implemented on Linux. On all other systems, it still uses the worker threads and semaphores (which is perfectly fine for a lot of things! Just having a great number of keep-alive connections will break that.) So the algorithm is: select(accept, event, pending) if accept -> send socket to free thread, if any. if not, add socket to queue if event -> send the signaling thread a socket from the queue, if not, mark it free - event might block until it can be *written* to. it is a fifo sending socket fds! A worker only does one http request at a time, then signals its availability back to the boss. The socket the worker was just doing should be added to the one-off epoll read. If it is closed, great, we can get rid of it. Otherwise, it is considered `pending`. The *kernel* manages that; the actual FD will not be kept out here. So: queue = sockets we know are ready to read now, but no worker thread is available idle list = worker threads not doing anything else. they signal back and forth the workers all read off the event fd. This is the semaphore wait the boss waits on accept or other sockets read events (one off! and level triggered). If anything happens wrt ready read, it puts it in the queue and writes to the event fd. The child could put the socket back in the epoll thing itself. The child needs to be able to gracefully handle being given a socket that just closed with no work. +/ class ConnectionThread : Thread { this(ListeningConnectionManager lcm, CMT dg, int myThreadNumber) { this.lcm = lcm; this.dg = dg; this.myThreadNumber = myThreadNumber; super(&run); } void run() { while(true) { // so if there's a bunch of idle keep-alive connections, it can // consume all the worker threads... just sitting there. lcm.semaphore.wait(); if(globalStopFlag) return; Socket socket; synchronized(lcm) { auto idx = lcm.nextIndexFront; socket = lcm.queue[idx]; lcm.queue[idx] = null; atomicOp!"+="(lcm.nextIndexFront, 1); atomicOp!"-="(lcm.queueLength, 1); } try { //import std.stdio; writeln(myThreadNumber, " taking it"); dg(socket); /+ if(socket.isAlive) { // process it more later version(linux) { import core.sys.linux.epoll; epoll_event ev; ev.events = EPOLLIN | EPOLLONESHOT | EPOLLET; ev.data.fd = socket.handle; import std.stdio; writeln("adding"); if(epoll_ctl(lcm.epoll_fd, EPOLL_CTL_ADD, socket.handle, &ev) == -1) { if(errno == EEXIST) { ev.events = EPOLLIN | EPOLLONESHOT | EPOLLET; ev.data.fd = socket.handle; if(epoll_ctl(lcm.epoll_fd, EPOLL_CTL_MOD, socket.handle, &ev) == -1) throw new Exception("epoll_ctl " ~ to!string(errno)); } else throw new Exception("epoll_ctl " ~ to!string(errno)); } //import std.stdio; writeln("keep alive"); // writing to this private member is to prevent the GC from closing my precious socket when I'm trying to use it later __traits(getMember, socket, "sock") = cast(socket_t) -1; } else { continue; // hope it times out in a reasonable amount of time... } } +/ } catch(ConnectionClosedException e) { // can just ignore this, it is fairly normal socket.close(); } catch(Throwable e) { import std.stdio; stderr.rawWrite(e.toString); stderr.rawWrite("\n"); socket.close(); } } } ListeningConnectionManager lcm; CMT dg; int myThreadNumber; } version(cgi_use_fiber) class WorkerThread : Thread { this(ListeningConnectionManager lcm, CMT dg, int myThreadNumber) { this.lcm = lcm; this.dg = dg; this.myThreadNumber = myThreadNumber; super(&run); } version(Windows) void run() { auto timeout = INFINITE; PseudoblockingOverlappedSocket key; OVERLAPPED* overlapped; DWORD bytes; while(!globalStopFlag && GetQueuedCompletionStatus(iocp, &bytes, cast(PULONG_PTR) &key, &overlapped, timeout)) { if(key is null) continue; key.lastAnswer = bytes; if(key.fiber) { key.fiber.proceed(); } else { // we have a new connection, issue the first receive on it and issue the next accept auto sn = key.accepted; key.accept(); cloexec(sn); if(lcm.tcp) { // disable Nagle's algorithm to avoid a 40ms delay when we send/recv // on the socket because we do some buffering internally. I think this helps, // certainly does for small requests, and I think it does for larger ones too sn.setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, 1); sn.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(10)); } dg(sn); } } //SleepEx(INFINITE, TRUE); } version(linux) void run() { import core.sys.linux.epoll; epfd = epoll_create1(EPOLL_CLOEXEC); if(epfd == -1) throw new Exception("epoll_create1 " ~ to!string(errno)); scope(exit) { import core.sys.posix.unistd; close(epfd); } { epoll_event ev; ev.events = EPOLLIN; ev.data.fd = cancelfd; epoll_ctl(epfd, EPOLL_CTL_ADD, cancelfd, &ev); } epoll_event ev; ev.events = EPOLLIN | EPOLLEXCLUSIVE; // EPOLLEXCLUSIVE is only available on kernels since like 2017 but that's prolly good enough. ev.data.fd = lcm.listener.handle; if(epoll_ctl(epfd, EPOLL_CTL_ADD, lcm.listener.handle, &ev) == -1) throw new Exception("epoll_ctl " ~ to!string(errno)); while(!globalStopFlag) { Socket sn; epoll_event[64] events; auto nfds = epoll_wait(epfd, events.ptr, events.length, -1); if(nfds == -1) { if(errno == EINTR) continue; throw new Exception("epoll_wait " ~ to!string(errno)); } foreach(idx; 0 .. nfds) { auto flags = events[idx].events; if(cast(size_t) events[idx].data.ptr == cast(size_t) cancelfd) { globalStopFlag = true; //import std.stdio; writeln("exit heard"); break; } else if(cast(size_t) events[idx].data.ptr == cast(size_t) lcm.listener.handle) { //import std.stdio; writeln(myThreadNumber, " woken up ", flags); // this try/catch is because it is set to non-blocking mode // and Phobos' stupid api throws an exception instead of returning // if it would block. Why would it block? because a forked process // might have beat us to it, but the wakeup event thundered our herds. try sn = lcm.listener.accept(); // don't need to do the acceptCancelable here since the epoll checks it better catch(SocketAcceptException e) { continue; } cloexec(sn); if(lcm.tcp) { // disable Nagle's algorithm to avoid a 40ms delay when we send/recv // on the socket because we do some buffering internally. I think this helps, // certainly does for small requests, and I think it does for larger ones too sn.setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, 1); sn.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(10)); } dg(sn); } else { if(cast(size_t) events[idx].data.ptr < 1024) { throw new Exception("this doesn't look like a fiber pointer..."); } auto fiber = cast(CgiFiber) events[idx].data.ptr; fiber.proceed(); } } } } ListeningConnectionManager lcm; CMT dg; int myThreadNumber; } /* Done with network helper */ /* Helpers for doing temporary files. Used both here and in web.d */ version(Windows) { import core.sys.windows.windows; extern(Windows) DWORD GetTempPathW(DWORD, LPWSTR); alias GetTempPathW GetTempPath; } version(Posix) { static import linux = core.sys.posix.unistd; } string getTempDirectory() { string path; version(Windows) { wchar[1024] buffer; auto len = GetTempPath(1024, buffer.ptr); if(len == 0) throw new Exception("couldn't find a temporary path"); auto b = buffer[0 .. len]; path = to!string(b); } else path = "/tmp/"; return path; } // I like std.date. These functions help keep my old code and data working with phobos changing. long sysTimeToDTime(in SysTime sysTime) { return convert!("hnsecs", "msecs")(sysTime.stdTime - 621355968000000000L); } long dateTimeToDTime(in DateTime dt) { return sysTimeToDTime(cast(SysTime) dt); } long getUtcTime() { // renamed primarily to avoid conflict with std.date itself return sysTimeToDTime(Clock.currTime(UTC())); } // NOTE: new SimpleTimeZone(minutes); can perhaps work with the getTimezoneOffset() JS trick SysTime dTimeToSysTime(long dTime, immutable TimeZone tz = null) { immutable hnsecs = convert!("msecs", "hnsecs")(dTime) + 621355968000000000L; return SysTime(hnsecs, tz); } // this is a helper to read HTTP transfer-encoding: chunked responses immutable(ubyte[]) dechunk(BufferedInputRange ir) { immutable(ubyte)[] ret; another_chunk: // If here, we are at the beginning of a chunk. auto a = ir.front(); int chunkSize; int loc = locationOf(a, "\r\n"); while(loc == -1) { ir.popFront(); a = ir.front(); loc = locationOf(a, "\r\n"); } string hex; hex = ""; for(int i = 0; i < loc; i++) { char c = a[i]; if(c >= 'A' && c <= 'Z') c += 0x20; if((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z')) { hex ~= c; } else { break; } } assert(hex.length); int power = 1; int size = 0; foreach(cc1; retro(hex)) { dchar cc = cc1; if(cc >= 'a' && cc <= 'z') cc -= 0x20; int val = 0; if(cc >= '0' && cc <= '9') val = cc - '0'; else val = cc - 'A' + 10; size += power * val; power *= 16; } chunkSize = size; assert(size >= 0); if(loc + 2 > a.length) { ir.popFront(0, a.length + loc + 2); a = ir.front(); } a = ir.consume(loc + 2); if(chunkSize == 0) { // we're done with the response // if we got here, will change must be true.... more_footers: loc = locationOf(a, "\r\n"); if(loc == -1) { ir.popFront(); a = ir.front; goto more_footers; } else { assert(loc == 0); ir.consume(loc + 2); goto finish; } } else { // if we got here, will change must be true.... if(a.length < chunkSize + 2) { ir.popFront(0, chunkSize + 2); a = ir.front(); } ret ~= (a[0..chunkSize]); if(!(a.length > chunkSize + 2)) { ir.popFront(0, chunkSize + 2); a = ir.front(); } assert(a[chunkSize] == 13); assert(a[chunkSize+1] == 10); a = ir.consume(chunkSize + 2); chunkSize = 0; goto another_chunk; } finish: return ret; } // I want to be able to get data from multiple sources the same way... interface ByChunkRange { bool empty(); void popFront(); const(ubyte)[] front(); } ByChunkRange byChunk(const(ubyte)[] data) { return new class ByChunkRange { override bool empty() { return !data.length; } override void popFront() { if(data.length > 4096) data = data[4096 .. $]; else data = null; } override const(ubyte)[] front() { return data[0 .. $ > 4096 ? 4096 : $]; } }; } ByChunkRange byChunk(BufferedInputRange ir, size_t atMost) { const(ubyte)[] f; f = ir.front; if(f.length > atMost) f = f[0 .. atMost]; return new class ByChunkRange { override bool empty() { return atMost == 0; } override const(ubyte)[] front() { return f; } override void popFront() { ir.consume(f.length); atMost -= f.length; auto a = ir.front(); if(a.length <= atMost) { f = a; atMost -= a.length; a = ir.consume(a.length); if(atMost != 0) ir.popFront(); if(f.length == 0) { f = ir.front(); } } else { // we actually have *more* here than we need.... f = a[0..atMost]; atMost = 0; ir.consume(atMost); } } }; } version(cgi_with_websocket) { // https://tools.ietf.org/html/rfc6455 /** WEBSOCKET SUPPORT: Full example: --- import arsd.cgi; void websocketEcho(Cgi cgi) { if(cgi.websocketRequested()) { if(cgi.origin != "http://arsdnet.net") throw new Exception("bad origin"); auto websocket = cgi.acceptWebsocket(); websocket.send("hello"); websocket.send(" world!"); auto msg = websocket.recv(); while(msg.opcode != WebSocketOpcode.close) { if(msg.opcode == WebSocketOpcode.text) { websocket.send(msg.textData); } else if(msg.opcode == WebSocketOpcode.binary) { websocket.send(msg.data); } msg = websocket.recv(); } websocket.close(); } else assert(0, "i want a web socket!"); } mixin GenericMain!websocketEcho; --- */ class WebSocket { Cgi cgi; private this(Cgi cgi) { this.cgi = cgi; Socket socket = cgi.idlol.source; socket.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"minutes"(5)); } // returns true if data available, false if it timed out bool recvAvailable(Duration timeout = dur!"msecs"(0)) { if(!waitForNextMessageWouldBlock()) return true; if(isDataPending(timeout)) return true; // this is kinda a lie. return false; } public bool lowLevelReceive() { auto bfr = cgi.idlol; top: auto got = bfr.front; if(got.length) { if(receiveBuffer.length < receiveBufferUsedLength + got.length) receiveBuffer.length += receiveBufferUsedLength + got.length; receiveBuffer[receiveBufferUsedLength .. receiveBufferUsedLength + got.length] = got[]; receiveBufferUsedLength += got.length; bfr.consume(got.length); return true; } if(bfr.sourceClosed) return false; bfr.popFront(0); if(bfr.sourceClosed) return false; goto top; } bool isDataPending(Duration timeout = 0.seconds) { Socket socket = cgi.idlol.source; auto check = new SocketSet(); check.add(socket); auto got = Socket.select(check, null, null, timeout); if(got > 0) return true; return false; } // note: this blocks WebSocketFrame recv() { return waitForNextMessage(); } private void llclose() { cgi.close(); } private void llsend(ubyte[] data) { cgi.write(data); cgi.flush(); } void unregisterActiveSocket(WebSocket) {} /* copy/paste section { */ private int readyState_; private ubyte[] receiveBuffer; private size_t receiveBufferUsedLength; private Config config; enum CONNECTING = 0; /// Socket has been created. The connection is not yet open. enum OPEN = 1; /// The connection is open and ready to communicate. enum CLOSING = 2; /// The connection is in the process of closing. enum CLOSED = 3; /// The connection is closed or couldn't be opened. /++ +/ /// Group: foundational static struct Config { /++ These control the size of the receive buffer. It starts at the initial size, will temporarily balloon up to the maximum size, and will reuse a buffer up to the likely size. Anything larger than the maximum size will cause the connection to be aborted and an exception thrown. This is to protect you against a peer trying to exhaust your memory, while keeping the user-level processing simple. +/ size_t initialReceiveBufferSize = 4096; size_t likelyReceiveBufferSize = 4096; /// ditto size_t maximumReceiveBufferSize = 10 * 1024 * 1024; /// ditto /++ Maximum combined size of a message. +/ size_t maximumMessageSize = 10 * 1024 * 1024; string[string] cookies; /// Cookies to send with the initial request. cookies[name] = value; string origin; /// Origin URL to send with the handshake, if desired. string protocol; /// the protocol header, if desired. int pingFrequency = 5000; /// Amount of time (in msecs) of idleness after which to send an automatic ping } /++ Returns one of [CONNECTING], [OPEN], [CLOSING], or [CLOSED]. +/ int readyState() { return readyState_; } /++ Closes the connection, sending a graceful teardown message to the other side. +/ /// Group: foundational void close(int code = 0, string reason = null) //in (reason.length < 123) in { assert(reason.length < 123); } do { if(readyState_ != OPEN) return; // it cool, we done WebSocketFrame wss; wss.fin = true; wss.opcode = WebSocketOpcode.close; wss.data = cast(ubyte[]) reason.dup; wss.send(&llsend); readyState_ = CLOSING; llclose(); } /++ Sends a ping message to the server. This is done automatically by the library if you set a non-zero [Config.pingFrequency], but you can also send extra pings explicitly as well with this function. +/ /// Group: foundational void ping() { WebSocketFrame wss; wss.fin = true; wss.opcode = WebSocketOpcode.ping; wss.send(&llsend); } // automatically handled.... void pong() { WebSocketFrame wss; wss.fin = true; wss.opcode = WebSocketOpcode.pong; wss.send(&llsend); } /++ Sends a text message through the websocket. +/ /// Group: foundational void send(in char[] textData) { WebSocketFrame wss; wss.fin = true; wss.opcode = WebSocketOpcode.text; wss.data = cast(ubyte[]) textData.dup; wss.send(&llsend); } /++ Sends a binary message through the websocket. +/ /// Group: foundational void send(in ubyte[] binaryData) { WebSocketFrame wss; wss.fin = true; wss.opcode = WebSocketOpcode.binary; wss.data = cast(ubyte[]) binaryData.dup; wss.send(&llsend); } /++ Waits for and returns the next complete message on the socket. Note that the onmessage function is still called, right before this returns. +/ /// Group: blocking_api public WebSocketFrame waitForNextMessage() { do { auto m = processOnce(); if(m.populated) return m; } while(lowLevelReceive()); throw new ConnectionClosedException("Websocket receive timed out"); //return WebSocketFrame.init; // FIXME? maybe. } /++ Tells if [waitForNextMessage] would block. +/ /// Group: blocking_api public bool waitForNextMessageWouldBlock() { checkAgain: if(isMessageBuffered()) return false; if(!isDataPending()) return true; while(isDataPending()) lowLevelReceive(); goto checkAgain; } /++ Is there a message in the buffer already? If `true`, [waitForNextMessage] is guaranteed to return immediately. If `false`, check [isDataPending] as the next step. +/ /// Group: blocking_api public bool isMessageBuffered() { ubyte[] d = receiveBuffer[0 .. receiveBufferUsedLength]; auto s = d; if(d.length) { auto orig = d; auto m = WebSocketFrame.read(d); // that's how it indicates that it needs more data if(d !is orig) return true; } return false; } private ubyte continuingType; private ubyte[] continuingData; //private size_t continuingDataLength; private WebSocketFrame processOnce() { ubyte[] d = receiveBuffer[0 .. receiveBufferUsedLength]; auto s = d; // FIXME: handle continuation frames more efficiently. it should really just reuse the receive buffer. WebSocketFrame m; if(d.length) { auto orig = d; m = WebSocketFrame.read(d); // that's how it indicates that it needs more data if(d is orig) return WebSocketFrame.init; m.unmaskInPlace(); switch(m.opcode) { case WebSocketOpcode.continuation: if(continuingData.length + m.data.length > config.maximumMessageSize) throw new Exception("message size exceeded"); continuingData ~= m.data; if(m.fin) { if(ontextmessage) ontextmessage(cast(char[]) continuingData); if(onbinarymessage) onbinarymessage(continuingData); continuingData = null; } break; case WebSocketOpcode.text: if(m.fin) { if(ontextmessage) ontextmessage(m.textData); } else { continuingType = m.opcode; //continuingDataLength = 0; continuingData = null; continuingData ~= m.data; } break; case WebSocketOpcode.binary: if(m.fin) { if(onbinarymessage) onbinarymessage(m.data); } else { continuingType = m.opcode; //continuingDataLength = 0; continuingData = null; continuingData ~= m.data; } break; case WebSocketOpcode.close: readyState_ = CLOSED; if(onclose) onclose(); unregisterActiveSocket(this); break; case WebSocketOpcode.ping: pong(); break; case WebSocketOpcode.pong: // just really references it is still alive, nbd. break; default: // ignore though i could and perhaps should throw too } } // the recv thing can be invalidated so gotta copy it over ugh if(d.length) { m.data = m.data.dup(); } import core.stdc.string; memmove(receiveBuffer.ptr, d.ptr, d.length); receiveBufferUsedLength = d.length; return m; } private void autoprocess() { // FIXME do { processOnce(); } while(lowLevelReceive()); } void delegate() onclose; /// void delegate() onerror; /// void delegate(in char[]) ontextmessage; /// void delegate(in ubyte[]) onbinarymessage; /// void delegate() onopen; /// /++ +/ /// Group: browser_api void onmessage(void delegate(in char[]) dg) { ontextmessage = dg; } /// ditto void onmessage(void delegate(in ubyte[]) dg) { onbinarymessage = dg; } /* } end copy/paste */ } bool websocketRequested(Cgi cgi) { return "sec-websocket-key" in cgi.requestHeaders && "connection" in cgi.requestHeaders && cgi.requestHeaders["connection"].asLowerCase().canFind("upgrade") && "upgrade" in cgi.requestHeaders && cgi.requestHeaders["upgrade"].asLowerCase().equal("websocket") ; } WebSocket acceptWebsocket(Cgi cgi) { assert(!cgi.closed); assert(!cgi.outputtedResponseData); cgi.setResponseStatus("101 Switching Protocols"); cgi.header("Upgrade: WebSocket"); cgi.header("Connection: upgrade"); string key = cgi.requestHeaders["sec-websocket-key"]; key ~= "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // the defined guid from the websocket spec import std.digest.sha; auto hash = sha1Of(key); auto accept = Base64.encode(hash); cgi.header(("Sec-WebSocket-Accept: " ~ accept).idup); cgi.websocketMode = true; cgi.write(""); cgi.flush(); return new WebSocket(cgi); } // FIXME get websocket to work on other modes, not just embedded_httpd /* copy/paste in http2.d { */ enum WebSocketOpcode : ubyte { continuation = 0, text = 1, binary = 2, // 3, 4, 5, 6, 7 RESERVED close = 8, ping = 9, pong = 10, // 11,12,13,14,15 RESERVED } public struct WebSocketFrame { private bool populated; bool fin; bool rsv1; bool rsv2; bool rsv3; WebSocketOpcode opcode; // 4 bits bool masked; ubyte lengthIndicator; // don't set this when building one to send ulong realLength; // don't use when sending ubyte[4] maskingKey; // don't set this when sending ubyte[] data; static WebSocketFrame simpleMessage(WebSocketOpcode opcode, void[] data) { WebSocketFrame msg; msg.fin = true; msg.opcode = opcode; msg.data = cast(ubyte[]) data.dup; return msg; } private void send(scope void delegate(ubyte[]) llsend) { ubyte[64] headerScratch; int headerScratchPos = 0; realLength = data.length; { ubyte b1; b1 |= cast(ubyte) opcode; b1 |= rsv3 ? (1 << 4) : 0; b1 |= rsv2 ? (1 << 5) : 0; b1 |= rsv1 ? (1 << 6) : 0; b1 |= fin ? (1 << 7) : 0; headerScratch[0] = b1; headerScratchPos++; } { headerScratchPos++; // we'll set header[1] at the end of this auto rlc = realLength; ubyte b2; b2 |= masked ? (1 << 7) : 0; assert(headerScratchPos == 2); if(realLength > 65535) { // use 64 bit length b2 |= 0x7f; // FIXME: double check endinaness foreach(i; 0 .. 8) { headerScratch[2 + 7 - i] = rlc & 0x0ff; rlc >>>= 8; } headerScratchPos += 8; } else if(realLength > 125) { // use 16 bit length b2 |= 0x7e; // FIXME: double check endinaness foreach(i; 0 .. 2) { headerScratch[2 + 1 - i] = rlc & 0x0ff; rlc >>>= 8; } headerScratchPos += 2; } else { // use 7 bit length b2 |= realLength & 0b_0111_1111; } headerScratch[1] = b2; } //assert(!masked, "masking key not properly implemented"); if(masked) { // FIXME: randomize this headerScratch[headerScratchPos .. headerScratchPos + 4] = maskingKey[]; headerScratchPos += 4; // we'll just mask it in place... int keyIdx = 0; foreach(i; 0 .. data.length) { data[i] = data[i] ^ maskingKey[keyIdx]; if(keyIdx == 3) keyIdx = 0; else keyIdx++; } } //writeln("SENDING ", headerScratch[0 .. headerScratchPos], data); llsend(headerScratch[0 .. headerScratchPos]); llsend(data); } static WebSocketFrame read(ref ubyte[] d) { WebSocketFrame msg; auto orig = d; WebSocketFrame needsMoreData() { d = orig; return WebSocketFrame.init; } if(d.length < 2) return needsMoreData(); ubyte b = d[0]; msg.populated = true; msg.opcode = cast(WebSocketOpcode) (b & 0x0f); b >>= 4; msg.rsv3 = b & 0x01; b >>= 1; msg.rsv2 = b & 0x01; b >>= 1; msg.rsv1 = b & 0x01; b >>= 1; msg.fin = b & 0x01; b = d[1]; msg.masked = (b & 0b1000_0000) ? true : false; msg.lengthIndicator = b & 0b0111_1111; d = d[2 .. $]; if(msg.lengthIndicator == 0x7e) { // 16 bit length msg.realLength = 0; if(d.length < 2) return needsMoreData(); foreach(i; 0 .. 2) { msg.realLength |= d[0] << ((1-i) * 8); d = d[1 .. $]; } } else if(msg.lengthIndicator == 0x7f) { // 64 bit length msg.realLength = 0; if(d.length < 8) return needsMoreData(); foreach(i; 0 .. 8) { msg.realLength |= ulong(d[0]) << ((7-i) * 8); d = d[1 .. $]; } } else { // 7 bit length msg.realLength = msg.lengthIndicator; } if(msg.masked) { if(d.length < 4) return needsMoreData(); msg.maskingKey = d[0 .. 4]; d = d[4 .. $]; } if(msg.realLength > d.length) { return needsMoreData(); } msg.data = d[0 .. cast(size_t) msg.realLength]; d = d[cast(size_t) msg.realLength .. $]; return msg; } void unmaskInPlace() { if(this.masked) { int keyIdx = 0; foreach(i; 0 .. this.data.length) { this.data[i] = this.data[i] ^ this.maskingKey[keyIdx]; if(keyIdx == 3) keyIdx = 0; else keyIdx++; } } } char[] textData() { return cast(char[]) data; } } /* } */ } version(Windows) { version(CRuntime_DigitalMars) { extern(C) int setmode(int, int) nothrow @nogc; } else version(CRuntime_Microsoft) { extern(C) int _setmode(int, int) nothrow @nogc; alias setmode = _setmode; } else static assert(0); } version(Posix) { import core.sys.posix.unistd; version(CRuntime_Musl) {} else { private extern(C) int posix_spawn(pid_t*, const char*, void*, void*, const char**, const char**); } } // FIXME: these aren't quite public yet. //private: // template for laziness void startAddonServer()(string arg) { version(OSX) { assert(0, "Not implemented"); } else version(linux) { import core.sys.posix.unistd; pid_t pid; const(char)*[16] args; args[0] = "ARSD_CGI_ADDON_SERVER"; args[1] = arg.ptr; posix_spawn(&pid, "/proc/self/exe", null, null, args.ptr, null // env ); } else version(Windows) { wchar[2048] filename; auto len = GetModuleFileNameW(null, filename.ptr, cast(DWORD) filename.length); if(len == 0 || len == filename.length) throw new Exception("could not get process name to start helper server"); STARTUPINFOW startupInfo; startupInfo.cb = cast(DWORD) startupInfo.sizeof; PROCESS_INFORMATION processInfo; import std.utf; // I *MIGHT* need to run it as a new job or a service... auto ret = CreateProcessW( filename.ptr, toUTF16z(arg), null, // process attributes null, // thread attributes false, // inherit handles 0, // creation flags null, // environment null, // working directory &startupInfo, &processInfo ); if(!ret) throw new Exception("create process failed"); // when done with those, if we set them /* CloseHandle(hStdInput); CloseHandle(hStdOutput); CloseHandle(hStdError); */ } else static assert(0, "Websocket server not implemented on this system yet (email me, i can prolly do it if you need it)"); } // template for laziness /* The websocket server is a single-process, single-thread, event I/O thing. It is passed websockets from other CGI processes and is then responsible for handling their messages and responses. Note that the CGI process is responsible for websocket setup, including authentication, etc. It also gets data sent to it by other processes and is responsible for distributing that, as necessary. */ void runWebsocketServer()() { assert(0, "not implemented"); } void sendToWebsocketServer(WebSocket ws, string group) { assert(0, "not implemented"); } void sendToWebsocketServer(string content, string group) { assert(0, "not implemented"); } void runEventServer()() { runAddonServer("/tmp/arsd_cgi_event_server", new EventSourceServerImplementation()); } void runTimerServer()() { runAddonServer("/tmp/arsd_scheduled_job_server", new ScheduledJobServerImplementation()); } version(Posix) { alias LocalServerConnectionHandle = int; alias CgiConnectionHandle = int; alias SocketConnectionHandle = int; enum INVALID_CGI_CONNECTION_HANDLE = -1; } else version(Windows) { alias LocalServerConnectionHandle = HANDLE; version(embedded_httpd_threads) { alias CgiConnectionHandle = SOCKET; enum INVALID_CGI_CONNECTION_HANDLE = INVALID_SOCKET; } else version(fastcgi) { alias CgiConnectionHandle = void*; // Doesn't actually work! But I don't want compile to fail pointlessly at this point. enum INVALID_CGI_CONNECTION_HANDLE = null; } else version(scgi) { alias CgiConnectionHandle = SOCKET; enum INVALID_CGI_CONNECTION_HANDLE = INVALID_SOCKET; } else { /* version(plain_cgi) */ alias CgiConnectionHandle = HANDLE; enum INVALID_CGI_CONNECTION_HANDLE = null; } alias SocketConnectionHandle = SOCKET; } version(with_addon_servers_connections) LocalServerConnectionHandle openLocalServerConnection()(string name, string arg) { version(Posix) { import core.sys.posix.unistd; import core.sys.posix.sys.un; int sock = socket(AF_UNIX, SOCK_STREAM, 0); if(sock == -1) throw new Exception("socket " ~ to!string(errno)); scope(failure) close(sock); cloexec(sock); // add-on server processes are assumed to be local, and thus will // use unix domain sockets. Besides, I want to pass sockets to them, // so it basically must be local (except for the session server, but meh). sockaddr_un addr; addr.sun_family = AF_UNIX; version(linux) { // on linux, we will use the abstract namespace addr.sun_path[0] = 0; addr.sun_path[1 .. name.length + 1] = cast(typeof(addr.sun_path[])) name[]; } else { // but otherwise, just use a file cuz we must. addr.sun_path[0 .. name.length] = cast(typeof(addr.sun_path[])) name[]; } bool alreadyTried; try_again: if(connect(sock, cast(sockaddr*) &addr, addr.sizeof) == -1) { if(!alreadyTried && errno == ECONNREFUSED) { // try auto-spawning the server, then attempt connection again startAddonServer(arg); import core.thread; Thread.sleep(50.msecs); alreadyTried = true; goto try_again; } else throw new Exception("connect " ~ to!string(errno)); } return sock; } else version(Windows) { return null; // FIXME } } version(with_addon_servers_connections) void closeLocalServerConnection(LocalServerConnectionHandle handle) { version(Posix) { import core.sys.posix.unistd; close(handle); } else version(Windows) CloseHandle(handle); } void runSessionServer()() { runAddonServer("/tmp/arsd_session_server", new BasicDataServerImplementation()); } version(Posix) private void makeNonBlocking(int fd) { import core.sys.posix.fcntl; auto flags = fcntl(fd, F_GETFL, 0); if(flags == -1) throw new Exception("fcntl get"); flags |= O_NONBLOCK; auto s = fcntl(fd, F_SETFL, flags); if(s == -1) throw new Exception("fcntl set"); } import core.stdc.errno; struct IoOp { @disable this(); @disable this(this); /* So we want to be able to eventually handle generic sockets too. */ enum Read = 1; enum Write = 2; enum Accept = 3; enum ReadSocketHandle = 4; // Your handler may be called in a different thread than the one that initiated the IO request! // It is also possible to have multiple io requests being called simultaneously. Use proper thread safety caution. private bool delegate(IoOp*, int) handler; // returns true if you are done and want it to be closed private void delegate(IoOp*) closeHandler; private void delegate(IoOp*) completeHandler; private int internalFd; private int operation; private int bufferLengthAllocated; private int bufferLengthUsed; private ubyte[1] internalBuffer; // it can be overallocated! ubyte[] allocatedBuffer() return { return internalBuffer.ptr[0 .. bufferLengthAllocated]; } ubyte[] usedBuffer() return { return allocatedBuffer[0 .. bufferLengthUsed]; } void reset() { bufferLengthUsed = 0; } int fd() { return internalFd; } } IoOp* allocateIoOp(int fd, int operation, int bufferSize, bool delegate(IoOp*, int) handler) { import core.stdc.stdlib; auto ptr = calloc(IoOp.sizeof + bufferSize, 1); if(ptr is null) assert(0); // out of memory! auto op = cast(IoOp*) ptr; op.handler = handler; op.internalFd = fd; op.operation = operation; op.bufferLengthAllocated = bufferSize; op.bufferLengthUsed = 0; import core.memory; GC.addRoot(ptr); return op; } void freeIoOp(ref IoOp* ptr) { import core.memory; GC.removeRoot(ptr); import core.stdc.stdlib; free(ptr); ptr = null; } version(Posix) version(with_addon_servers_connections) void nonBlockingWrite(EventIoServer eis, int connection, const void[] data) { //import std.stdio : writeln; writeln(cast(string) data); import core.sys.posix.unistd; auto ret = write(connection, data.ptr, data.length); if(ret != data.length) { if(ret == 0 || (ret == -1 && (errno == EPIPE || errno == ETIMEDOUT))) { // the file is closed, remove it eis.fileClosed(connection); } else throw new Exception("alas " ~ to!string(ret) ~ " " ~ to!string(errno)); // FIXME } } version(Windows) version(with_addon_servers_connections) void nonBlockingWrite(EventIoServer eis, int connection, const void[] data) { // FIXME } bool isInvalidHandle(CgiConnectionHandle h) { return h == INVALID_CGI_CONNECTION_HANDLE; } /+ https://docs.microsoft.com/en-us/windows/desktop/api/winsock2/nf-winsock2-wsarecv https://support.microsoft.com/en-gb/help/181611/socket-overlapped-i-o-versus-blocking-nonblocking-mode https://stackoverflow.com/questions/18018489/should-i-use-iocps-or-overlapped-wsasend-receive https://docs.microsoft.com/en-us/windows/desktop/fileio/i-o-completion-ports https://docs.microsoft.com/en-us/windows/desktop/fileio/createiocompletionport https://docs.microsoft.com/en-us/windows/desktop/api/mswsock/nf-mswsock-acceptex https://docs.microsoft.com/en-us/windows/desktop/Sync/waitable-timer-objects https://docs.microsoft.com/en-us/windows/desktop/api/synchapi/nf-synchapi-setwaitabletimer https://docs.microsoft.com/en-us/windows/desktop/Sync/using-a-waitable-timer-with-an-asynchronous-procedure-call https://docs.microsoft.com/en-us/windows/desktop/api/winsock2/nf-winsock2-wsagetoverlappedresult +/ /++ You can customize your server by subclassing the appropriate server. Then, register your subclass at compile time with the [registerEventIoServer] template, or implement your own main function and call it yourself. $(TIP If you make your subclass a `final class`, there is a slight performance improvement.) +/ version(with_addon_servers_connections) interface EventIoServer { bool handleLocalConnectionData(IoOp* op, int receivedFd); void handleLocalConnectionClose(IoOp* op); void handleLocalConnectionComplete(IoOp* op); void wait_timeout(); void fileClosed(int fd); void epoll_fd(int fd); } // the sink should buffer it private void serialize(T)(scope void delegate(scope ubyte[]) sink, T t) { static if(is(T == struct)) { foreach(member; __traits(allMembers, T)) serialize(sink, __traits(getMember, t, member)); } else static if(is(T : int)) { // no need to think of endianness just because this is only used // for local, same-machine stuff anyway. thanks private lol sink((cast(ubyte*) &t)[0 .. t.sizeof]); } else static if(is(T == string) || is(T : const(ubyte)[])) { // these are common enough to optimize int len = cast(int) t.length; // want length consistent size tho, in case 32 bit program sends to 64 bit server, etc. sink((cast(ubyte*) &len)[0 .. int.sizeof]); sink(cast(ubyte[]) t[]); } else static if(is(T : A[], A)) { // generic array is less optimal but still prolly ok int len = cast(int) t.length; sink((cast(ubyte*) &len)[0 .. int.sizeof]); foreach(item; t) serialize(sink, item); } else static assert(0, T.stringof); } // all may be stack buffers, so use caution private void deserialize(T)(scope ubyte[] delegate(int sz) get, scope void delegate(T) dg) { static if(is(T == struct)) { T t; foreach(member; __traits(allMembers, T)) deserialize!(typeof(__traits(getMember, T, member)))(get, (mbr) { __traits(getMember, t, member) = mbr; }); dg(t); } else static if(is(T : int)) { // no need to think of endianness just because this is only used // for local, same-machine stuff anyway. thanks private lol T t; auto data = get(t.sizeof); t = (cast(T[]) data)[0]; dg(t); } else static if(is(T == string) || is(T : const(ubyte)[])) { // these are common enough to optimize int len; auto data = get(len.sizeof); len = (cast(int[]) data)[0]; /* typeof(T[0])[2000] stackBuffer; T buffer; if(len < stackBuffer.length) buffer = stackBuffer[0 .. len]; else buffer = new T(len); data = get(len * typeof(T[0]).sizeof); */ T t = cast(T) get(len * cast(int) typeof(T.init[0]).sizeof); dg(t); } else static if(is(T == E[], E)) { T t; int len; auto data = get(len.sizeof); len = (cast(int[]) data)[0]; t.length = len; foreach(ref e; t) { deserialize!E(get, (ele) { e = ele; }); } dg(t); } else static assert(0, T.stringof); } unittest { serialize((ubyte[] b) { deserialize!int( sz => b[0 .. sz], (t) { assert(t == 1); }); }, 1); serialize((ubyte[] b) { deserialize!int( sz => b[0 .. sz], (t) { assert(t == 56674); }); }, 56674); ubyte[1000] buffer; int bufferPoint; void add(ubyte[] b) { buffer[bufferPoint .. bufferPoint + b.length] = b[]; bufferPoint += b.length; } ubyte[] get(int sz) { auto b = buffer[bufferPoint .. bufferPoint + sz]; bufferPoint += sz; return b; } serialize(&add, "test here"); bufferPoint = 0; deserialize!string(&get, (t) { assert(t == "test here"); }); bufferPoint = 0; struct Foo { int a; ubyte c; string d; } serialize(&add, Foo(403, 37, "amazing")); bufferPoint = 0; deserialize!Foo(&get, (t) { assert(t.a == 403); assert(t.c == 37); assert(t.d == "amazing"); }); bufferPoint = 0; } /* Here's the way the RPC interface works: You define the interface that lists the functions you can call on the remote process. The interface may also have static methods for convenience. These forward to a singleton instance of an auto-generated class, which actually sends the args over the pipe. An impl class actually implements it. A receiving server deserializes down the pipe and calls methods on the class. I went with the interface to get some nice compiler checking and documentation stuff. I could have skipped the interface and just implemented it all from the server class definition itself, but then the usage may call the method instead of rpcing it; I just like having the user interface and the implementation separate so you aren't tempted to `new impl` to call the methods. I fiddled with newlines in the mixin string to ensure the assert line numbers matched up to the source code line number. Idk why dmd didn't do this automatically, but it was important to me. Realistically though the bodies would just be connection.call(this.mangleof, args...) sooooo. FIXME: overloads aren't supported */ /// Base for storing sessions in an array. Exists primarily for internal purposes and you should generally not use this. interface SessionObject {} private immutable void delegate(string[])[string] scheduledJobHandlers; private immutable void delegate(string[])[string] websocketServers; version(with_breaking_cgi_features) mixin(q{ mixin template ImplementRpcClientInterface(T, string serverPath, string cmdArg) { static import std.traits; // derivedMembers on an interface seems to give exactly what I want: the virtual functions we need to implement. so I am just going to use it directly without more filtering. static foreach(idx, member; __traits(derivedMembers, T)) { static if(__traits(isVirtualMethod, __traits(getMember, T, member))) mixin( q{ std.traits.ReturnType!(__traits(getMember, T, member)) } ~ member ~ q{(std.traits.Parameters!(__traits(getMember, T, member)) params) { SerializationBuffer buffer; auto i = cast(ushort) idx; serialize(&buffer.sink, i); serialize(&buffer.sink, __traits(getMember, T, member).mangleof); foreach(param; params) serialize(&buffer.sink, param); auto sendable = buffer.sendable; version(Posix) {{ auto ret = send(connectionHandle, sendable.ptr, sendable.length, 0); if(ret == -1) { throw new Exception("send returned -1, errno: " ~ to!string(errno)); } else if(ret == 0) { throw new Exception("Connection to addon server lost"); } if(ret < sendable.length) throw new Exception("Send failed to send all"); assert(ret == sendable.length); }} // FIXME Windows impl static if(!is(typeof(return) == void)) { // there is a return value; we need to wait for it too version(Posix) { ubyte[3000] revBuffer; auto ret = recv(connectionHandle, revBuffer.ptr, revBuffer.length, 0); auto got = revBuffer[0 .. ret]; int dataLocation; ubyte[] grab(int sz) { auto dataLocation1 = dataLocation; dataLocation += sz; return got[dataLocation1 .. dataLocation]; } typeof(return) retu; deserialize!(typeof(return))(&grab, (a) { retu = a; }); return retu; } else { // FIXME Windows impl return typeof(return).init; } } }}); } private static typeof(this) singletonInstance; private LocalServerConnectionHandle connectionHandle; static typeof(this) connection() { if(singletonInstance is null) { singletonInstance = new typeof(this)(); singletonInstance.connect(); } return singletonInstance; } void connect() { connectionHandle = openLocalServerConnection(serverPath, cmdArg); } void disconnect() { closeLocalServerConnection(connectionHandle); } } void dispatchRpcServer(Interface, Class)(Class this_, ubyte[] data, int fd) if(is(Class : Interface)) { ushort calledIdx; string calledFunction; int dataLocation; ubyte[] grab(int sz) { if(sz == 0) assert(0); auto d = data[dataLocation .. dataLocation + sz]; dataLocation += sz; return d; } again: deserialize!ushort(&grab, (a) { calledIdx = a; }); deserialize!string(&grab, (a) { calledFunction = a; }); import std.traits; sw: switch(calledIdx) { foreach(idx, memberName; __traits(derivedMembers, Interface)) static if(__traits(isVirtualMethod, __traits(getMember, Interface, memberName))) { case idx: assert(calledFunction == __traits(getMember, Interface, memberName).mangleof); Parameters!(__traits(getMember, Interface, memberName)) params; foreach(ref param; params) deserialize!(typeof(param))(&grab, (a) { param = a; }); static if(is(ReturnType!(__traits(getMember, Interface, memberName)) == void)) { __traits(getMember, this_, memberName)(params); } else { auto ret = __traits(getMember, this_, memberName)(params); SerializationBuffer buffer; serialize(&buffer.sink, ret); auto sendable = buffer.sendable; version(Posix) { auto r = send(fd, sendable.ptr, sendable.length, 0); if(r == -1) { throw new Exception("send returned -1, errno: " ~ to!string(errno)); } else if(r == 0) { throw new Exception("Connection to addon client lost"); } if(r < sendable.length) throw new Exception("Send failed to send all"); } // FIXME Windows impl } break sw; } default: assert(0); } if(dataLocation != data.length) goto again; } private struct SerializationBuffer { ubyte[2048] bufferBacking; int bufferLocation; void sink(scope ubyte[] data) { bufferBacking[bufferLocation .. bufferLocation + data.length] = data[]; bufferLocation += data.length; } ubyte[] sendable() return { return bufferBacking[0 .. bufferLocation]; } } /* FIXME: add a version command line arg version data in the library management gui as external program at server with event_fd for each run use .mangleof in the at function name i think the at server will have to: pipe args to the child collect child output for logging get child return value for logging on windows timers work differently. idk how to best combine with the io stuff. will have to have dump and restore too, so i can restart without losing stuff. */ /++ A convenience object for talking to the [BasicDataServer] from a higher level. See: [Cgi.getSessionObject]. You pass it a `Data` struct describing the data you want saved in the session. Then, this class will generate getter and setter properties that allow access to that data. Note that each load and store will be done as-accessed; it doesn't front-load mutable data nor does it batch updates out of fear of read-modify-write race conditions. (In fact, right now it does this for everything, but in the future, I might batch load `immutable` members of the Data struct.) At some point in the future, I might also let it do different backends, like a client-side cookie store too, but idk. Note that the plain-old-data members of your `Data` struct are wrapped by this interface via a static foreach to make property functions. See_Also: [MockSession] +/ interface Session(Data) : SessionObject { @property string sessionId() const; /++ Starts a new session. Note that a session is also implicitly started as soon as you write data to it, so if you need to alter these parameters from their defaults, be sure to explicitly call this BEFORE doing any writes to session data. Params: idleLifetime = How long, in seconds, the session should remain in memory when not being read from or written to. The default is one day. NOT IMPLEMENTED useExtendedLifetimeCookie = The session ID is always stored in a HTTP cookie, and by default, that cookie is discarded when the user closes their browser. But if you set this to true, it will use a non-perishable cookie for the given idleLifetime. NOT IMPLEMENTED +/ void start(int idleLifetime = 2600 * 24, bool useExtendedLifetimeCookie = false); /++ Regenerates the session ID and updates the associated cookie. This is also your chance to change immutable data (not yet implemented). +/ void regenerateId(); /++ Terminates this session, deleting all saved data. +/ void terminate(); /++ Plain-old-data members of your `Data` struct are wrapped here via the property getters and setters. If the member is a non-string array, it returns a magical array proxy object which allows for atomic appends and replaces via overloaded operators. You can slice this to get a range representing a $(B const) view of the array. This is to protect you against read-modify-write race conditions. +/ static foreach(memberName; __traits(allMembers, Data)) static if(is(typeof(__traits(getMember, Data, memberName)))) mixin(q{ @property inout(typeof(__traits(getMember, Data, memberName))) } ~ memberName ~ q{ () inout; @property typeof(__traits(getMember, Data, memberName)) } ~ memberName ~ q{ (typeof(__traits(getMember, Data, memberName)) value); }); } /++ An implementation of [Session] that works on real cgi connections utilizing the [BasicDataServer]. As opposed to a [MockSession] which is made for testing purposes. You will not construct one of these directly. See [Cgi.getSessionObject] instead. +/ class BasicDataServerSession(Data) : Session!Data { private Cgi cgi; private string sessionId_; public @property string sessionId() const { return sessionId_; } protected @property string sessionId(string s) { return this.sessionId_ = s; } private this(Cgi cgi) { this.cgi = cgi; if(auto ptr = "sessionId" in cgi.cookies) sessionId = (*ptr).length ? *ptr : null; } void start(int idleLifetime = 2600 * 24, bool useExtendedLifetimeCookie = false) { assert(sessionId is null); // FIXME: what if there is a session ID cookie, but no corresponding session on the server? import std.random, std.conv; sessionId = to!string(uniform(1, long.max)); BasicDataServer.connection.createSession(sessionId, idleLifetime); setCookie(); } protected void setCookie() { cgi.setCookie( "sessionId", sessionId, 0 /* expiration */, "/" /* path */, null /* domain */, true /* http only */, cgi.https /* if the session is started on https, keep it there, otherwise, be flexible */); } void regenerateId() { if(sessionId is null) { start(); return; } import std.random, std.conv; auto oldSessionId = sessionId; sessionId = to!string(uniform(1, long.max)); BasicDataServer.connection.renameSession(oldSessionId, sessionId); setCookie(); } void terminate() { BasicDataServer.connection.destroySession(sessionId); sessionId = null; setCookie(); } static foreach(memberName; __traits(allMembers, Data)) static if(is(typeof(__traits(getMember, Data, memberName)))) mixin(q{ @property inout(typeof(__traits(getMember, Data, memberName))) } ~ memberName ~ q{ () inout { if(sessionId is null) return typeof(return).init; import std.traits; auto v = BasicDataServer.connection.getSessionData(sessionId, fullyQualifiedName!Data ~ "." ~ memberName); if(v.length == 0) return typeof(return).init; import std.conv; // why this cast? to doesn't like being given an inout argument. so need to do it without that, then // we need to return it and that needed the cast. It should be fine since we basically respect constness.. // basically. Assuming the session is POD this should be fine. return cast(typeof(return)) to!(typeof(__traits(getMember, Data, memberName)))(v); } @property typeof(__traits(getMember, Data, memberName)) } ~ memberName ~ q{ (typeof(__traits(getMember, Data, memberName)) value) { if(sessionId is null) start(); import std.conv; import std.traits; BasicDataServer.connection.setSessionData(sessionId, fullyQualifiedName!Data ~ "." ~ memberName, to!string(value)); return value; } }); } /++ A mock object that works like the real session, but doesn't actually interact with any actual database or http connection. Simply stores the data in its instance members. +/ class MockSession(Data) : Session!Data { pure { @property string sessionId() const { return "mock"; } void start(int idleLifetime = 2600 * 24, bool useExtendedLifetimeCookie = false) {} void regenerateId() {} void terminate() {} private Data store_; static foreach(memberName; __traits(allMembers, Data)) static if(is(typeof(__traits(getMember, Data, memberName)))) mixin(q{ @property inout(typeof(__traits(getMember, Data, memberName))) } ~ memberName ~ q{ () inout { return __traits(getMember, store_, memberName); } @property typeof(__traits(getMember, Data, memberName)) } ~ memberName ~ q{ (typeof(__traits(getMember, Data, memberName)) value) { return __traits(getMember, store_, memberName) = value; } }); } } /++ Direct interface to the basic data add-on server. You can typically use [Cgi.getSessionObject] as a more convenient interface. +/ version(with_addon_servers_connections) interface BasicDataServer { /// void createSession(string sessionId, int lifetime); /// void renewSession(string sessionId, int lifetime); /// void destroySession(string sessionId); /// void renameSession(string oldSessionId, string newSessionId); /// void setSessionData(string sessionId, string dataKey, string dataValue); /// string getSessionData(string sessionId, string dataKey); /// static BasicDataServerConnection connection() { return BasicDataServerConnection.connection(); } } version(with_addon_servers_connections) class BasicDataServerConnection : BasicDataServer { mixin ImplementRpcClientInterface!(BasicDataServer, "/tmp/arsd_session_server", "--session-server"); } version(with_addon_servers) final class BasicDataServerImplementation : BasicDataServer, EventIoServer { void createSession(string sessionId, int lifetime) { sessions[sessionId.idup] = Session(lifetime); } void destroySession(string sessionId) { sessions.remove(sessionId); } void renewSession(string sessionId, int lifetime) { sessions[sessionId].lifetime = lifetime; } void renameSession(string oldSessionId, string newSessionId) { sessions[newSessionId.idup] = sessions[oldSessionId]; sessions.remove(oldSessionId); } void setSessionData(string sessionId, string dataKey, string dataValue) { if(sessionId !in sessions) createSession(sessionId, 3600); // FIXME? sessions[sessionId].values[dataKey.idup] = dataValue.idup; } string getSessionData(string sessionId, string dataKey) { if(auto session = sessionId in sessions) { if(auto data = dataKey in (*session).values) return *data; else return null; // no such data } else { return null; // no session } } protected: struct Session { int lifetime; string[string] values; } Session[string] sessions; bool handleLocalConnectionData(IoOp* op, int receivedFd) { auto data = op.usedBuffer; dispatchRpcServer!BasicDataServer(this, data, op.fd); return false; } void handleLocalConnectionClose(IoOp* op) {} // doesn't really matter, this is a fairly stateless go void handleLocalConnectionComplete(IoOp* op) {} // again, irrelevant void wait_timeout() {} void fileClosed(int fd) {} // stateless so irrelevant void epoll_fd(int fd) {} } /++ See [schedule] to make one of these. You then call one of the methods here to set it up: --- schedule!fn(args).at(DateTime(2019, 8, 7, 12, 00, 00)); // run the function at August 7, 2019, 12 noon UTC schedule!fn(args).delay(6.seconds); // run it after waiting 6 seconds schedule!fn(args).asap(); // run it in the background as soon as the event loop gets around to it --- +/ version(with_addon_servers_connections) struct ScheduledJobHelper { private string func; private string[] args; private bool consumed; private this(string func, string[] args) { this.func = func; this.args = args; } ~this() { assert(consumed); } /++ Schedules the job to be run at the given time. +/ void at(DateTime when, immutable TimeZone timezone = UTC()) { consumed = true; auto conn = ScheduledJobServerConnection.connection; import std.file; auto st = SysTime(when, timezone); auto jobId = conn.scheduleJob(1, cast(int) st.toUnixTime(), thisExePath, func, args); } /++ Schedules the job to run at least after the specified delay. +/ void delay(Duration delay) { consumed = true; auto conn = ScheduledJobServerConnection.connection; import std.file; auto jobId = conn.scheduleJob(0, cast(int) delay.total!"seconds", thisExePath, func, args); } /++ Runs the job in the background ASAP. $(NOTE It may run in a background thread. Don't segfault!) +/ void asap() { consumed = true; auto conn = ScheduledJobServerConnection.connection; import std.file; auto jobId = conn.scheduleJob(0, 1, thisExePath, func, args); } /+ /++ Schedules the job to recur on the given pattern. +/ void recur(string spec) { } +/ } /++ First step to schedule a job on the scheduled job server. The scheduled job needs to be a top-level function that doesn't read any variables from outside its arguments because it may be run in a new process, without any context existing later. You MUST set details on the returned object to actually do anything! +/ template schedule(alias fn, T...) if(is(typeof(fn) == function)) { /// ScheduledJobHelper schedule(T args) { // this isn't meant to ever be called, but instead just to // get the compiler to type check the arguments passed for us auto sample = delegate() { fn(args); }; string[] sargs; foreach(arg; args) sargs ~= to!string(arg); return ScheduledJobHelper(fn.mangleof, sargs); } shared static this() { scheduledJobHandlers[fn.mangleof] = delegate(string[] sargs) { import std.traits; Parameters!fn args; foreach(idx, ref arg; args) arg = to!(typeof(arg))(sargs[idx]); fn(args); }; } } /// interface ScheduledJobServer { /// Use the [schedule] function for a higher-level interface. int scheduleJob(int whenIs, int when, string executable, string func, string[] args); /// void cancelJob(int jobId); } version(with_addon_servers_connections) class ScheduledJobServerConnection : ScheduledJobServer { mixin ImplementRpcClientInterface!(ScheduledJobServer, "/tmp/arsd_scheduled_job_server", "--timer-server"); } version(with_addon_servers) final class ScheduledJobServerImplementation : ScheduledJobServer, EventIoServer { // FIXME: we need to handle SIGCHLD in this somehow // whenIs is 0 for relative, 1 for absolute protected int scheduleJob(int whenIs, int when, string executable, string func, string[] args) { auto nj = nextJobId; nextJobId++; version(linux) { import core.sys.linux.timerfd; import core.sys.linux.epoll; import core.sys.posix.unistd; auto fd = timerfd_create(CLOCK_REALTIME, TFD_NONBLOCK | TFD_CLOEXEC); if(fd == -1) throw new Exception("fd timer create failed"); foreach(ref arg; args) arg = arg.idup; auto job = Job(executable.idup, func.idup, .dup(args), fd, nj); itimerspec value; value.it_value.tv_sec = when; value.it_value.tv_nsec = 0; value.it_interval.tv_sec = 0; value.it_interval.tv_nsec = 0; if(timerfd_settime(fd, whenIs == 1 ? TFD_TIMER_ABSTIME : 0, &value, null) == -1) throw new Exception("couldn't set fd timer"); auto op = allocateIoOp(fd, IoOp.Read, 16, (IoOp* op, int fd) { jobs.remove(nj); epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, null); close(fd); spawnProcess([job.executable, "--timed-job", job.func] ~ job.args); return true; }); scope(failure) freeIoOp(op); epoll_event ev; ev.events = EPOLLIN | EPOLLET; ev.data.ptr = op; if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) throw new Exception("epoll_ctl " ~ to!string(errno)); jobs[nj] = job; return nj; } else assert(0); } protected void cancelJob(int jobId) { version(linux) { auto job = jobId in jobs; if(job is null) return; jobs.remove(jobId); version(linux) { import core.sys.linux.timerfd; import core.sys.linux.epoll; import core.sys.posix.unistd; epoll_ctl(epoll_fd, EPOLL_CTL_DEL, job.timerfd, null); close(job.timerfd); } } jobs.remove(jobId); } int nextJobId = 1; static struct Job { string executable; string func; string[] args; int timerfd; int id; } Job[int] jobs; // event io server methods below bool handleLocalConnectionData(IoOp* op, int receivedFd) { auto data = op.usedBuffer; dispatchRpcServer!ScheduledJobServer(this, data, op.fd); return false; } void handleLocalConnectionClose(IoOp* op) {} // doesn't really matter, this is a fairly stateless go void handleLocalConnectionComplete(IoOp* op) {} // again, irrelevant void wait_timeout() {} void fileClosed(int fd) {} // stateless so irrelevant int epoll_fd_; void epoll_fd(int fd) {this.epoll_fd_ = fd; } int epoll_fd() { return epoll_fd_; } } /// version(with_addon_servers_connections) interface EventSourceServer { /++ sends this cgi request to the event server so it will be fed events. You should not do anything else with the cgi object after this. $(WARNING This API is extremely unstable. I might change it or remove it without notice.) See_Also: [sendEvent] +/ public static void adoptConnection(Cgi cgi, in char[] eventUrl) { /* If lastEventId is missing or empty, you just get new events as they come. If it is set from something else, it sends all since then (that are still alive) down the pipe immediately. The reason it can come from the header is that's what the standard defines for browser reconnects. The reason it can come from a query string is just convenience in catching up in a user-defined manner. The reason the header overrides the query string is if the browser tries to reconnect, it will send the header AND the query (it reconnects to the same url), so we just want to do the restart thing. Note that if you ask for "0" as the lastEventId, it will get ALL still living events. */ string lastEventId = cgi.lastEventId; if(lastEventId.length == 0 && "lastEventId" in cgi.get) lastEventId = cgi.get["lastEventId"]; cgi.setResponseContentType("text/event-stream"); cgi.write(":\n", false); // to initialize the chunking and send headers before keeping the fd for later cgi.flush(); cgi.closed = true; auto s = openLocalServerConnection("/tmp/arsd_cgi_event_server", "--event-server"); scope(exit) closeLocalServerConnection(s); version(fastcgi) throw new Exception("sending fcgi connections not supported"); else { auto fd = cgi.getOutputFileHandle(); if(isInvalidHandle(fd)) throw new Exception("bad fd from cgi!"); EventSourceServerImplementation.SendableEventConnection sec; sec.populate(cgi.responseChunked, eventUrl, lastEventId); version(Posix) { auto res = write_fd(s, cast(void*) &sec, sec.sizeof, fd); assert(res == sec.sizeof); } else version(Windows) { // FIXME } } } /++ Sends an event to the event server, starting it if necessary. The event server will distribute it to any listening clients, and store it for `lifetime` seconds for any later listening clients to catch up later. $(WARNING This API is extremely unstable. I might change it or remove it without notice.) Params: url = A string identifying this event "bucket". Listening clients must also connect to this same string. I called it `url` because I envision it being just passed as the url of the request. event = the event type string, which is used in the Javascript addEventListener API on EventSource data = the event data. Available in JS as `event.data`. lifetime = the amount of time to keep this event for replaying on the event server. See_Also: [sendEventToEventServer] +/ public static void sendEvent(string url, string event, string data, int lifetime) { auto s = openLocalServerConnection("/tmp/arsd_cgi_event_server", "--event-server"); scope(exit) closeLocalServerConnection(s); EventSourceServerImplementation.SendableEvent sev; sev.populate(url, event, data, lifetime); version(Posix) { auto ret = send(s, &sev, sev.sizeof, 0); assert(ret == sev.sizeof); } else version(Windows) { // FIXME } } /++ Messages sent to `url` will also be sent to anyone listening on `forwardUrl`. See_Also: [disconnect] +/ void connect(string url, string forwardUrl); /++ Disconnects `forwardUrl` from `url` See_Also: [connect] +/ void disconnect(string url, string forwardUrl); } /// version(with_addon_servers) final class EventSourceServerImplementation : EventSourceServer, EventIoServer { protected: void connect(string url, string forwardUrl) { pipes[url] ~= forwardUrl; } void disconnect(string url, string forwardUrl) { auto t = url in pipes; if(t is null) return; foreach(idx, n; (*t)) if(n == forwardUrl) { (*t)[idx] = (*t)[$-1]; (*t) = (*t)[0 .. $-1]; break; } } bool handleLocalConnectionData(IoOp* op, int receivedFd) { if(receivedFd != -1) { //writeln("GOT FD ", receivedFd, " -- ", op.usedBuffer); //core.sys.posix.unistd.write(receivedFd, "hello".ptr, 5); SendableEventConnection* got = cast(SendableEventConnection*) op.usedBuffer.ptr; auto url = got.url.idup; eventConnectionsByUrl[url] ~= EventConnection(receivedFd, got.responseChunked > 0 ? true : false); // FIXME: catch up on past messages here } else { auto data = op.usedBuffer; auto event = cast(SendableEvent*) data.ptr; if(event.magic == 0xdeadbeef) { handleInputEvent(event); if(event.url in pipes) foreach(pipe; pipes[event.url]) { event.url = pipe; handleInputEvent(event); } } else { dispatchRpcServer!EventSourceServer(this, data, op.fd); } } return false; } void handleLocalConnectionClose(IoOp* op) { fileClosed(op.fd); } void handleLocalConnectionComplete(IoOp* op) {} void wait_timeout() { // just keeping alive foreach(url, connections; eventConnectionsByUrl) foreach(connection; connections) if(connection.needsChunking) nonBlockingWrite(this, connection.fd, "1b\r\nevent: keepalive\ndata: ok\n\n\r\n"); else nonBlockingWrite(this, connection.fd, "event: keepalive\ndata: ok\n\n\r\n"); } void fileClosed(int fd) { outer: foreach(url, ref connections; eventConnectionsByUrl) { foreach(idx, conn; connections) { if(fd == conn.fd) { connections[idx] = connections[$-1]; connections = connections[0 .. $ - 1]; continue outer; } } } } void epoll_fd(int fd) {} private: struct SendableEventConnection { ubyte responseChunked; int urlLength; char[256] urlBuffer = 0; int lastEventIdLength; char[32] lastEventIdBuffer = 0; char[] url() return { return urlBuffer[0 .. urlLength]; } void url(in char[] u) { urlBuffer[0 .. u.length] = u[]; urlLength = cast(int) u.length; } char[] lastEventId() return { return lastEventIdBuffer[0 .. lastEventIdLength]; } void populate(bool responseChunked, in char[] url, in char[] lastEventId) in { assert(url.length < this.urlBuffer.length); assert(lastEventId.length < this.lastEventIdBuffer.length); } do { this.responseChunked = responseChunked ? 1 : 0; this.urlLength = cast(int) url.length; this.lastEventIdLength = cast(int) lastEventId.length; this.urlBuffer[0 .. url.length] = url[]; this.lastEventIdBuffer[0 .. lastEventId.length] = lastEventId[]; } } struct SendableEvent { int magic = 0xdeadbeef; int urlLength; char[256] urlBuffer = 0; int typeLength; char[32] typeBuffer = 0; int messageLength; char[2048 * 4] messageBuffer = 0; // this is an arbitrary limit, it needs to fit comfortably in stack (including in a fiber) and be a single send on the kernel side cuz of the impl... i think this is ok for a unix socket. int _lifetime; char[] message() return { return messageBuffer[0 .. messageLength]; } char[] type() return { return typeBuffer[0 .. typeLength]; } char[] url() return { return urlBuffer[0 .. urlLength]; } void url(in char[] u) { urlBuffer[0 .. u.length] = u[]; urlLength = cast(int) u.length; } int lifetime() { return _lifetime; } /// void populate(string url, string type, string message, int lifetime) in { assert(url.length < this.urlBuffer.length); assert(type.length < this.typeBuffer.length); assert(message.length < this.messageBuffer.length); } do { this.urlLength = cast(int) url.length; this.typeLength = cast(int) type.length; this.messageLength = cast(int) message.length; this._lifetime = lifetime; this.urlBuffer[0 .. url.length] = url[]; this.typeBuffer[0 .. type.length] = type[]; this.messageBuffer[0 .. message.length] = message[]; } } struct EventConnection { int fd; bool needsChunking; } private EventConnection[][string] eventConnectionsByUrl; private string[][string] pipes; private void handleInputEvent(scope SendableEvent* event) { static int eventId; static struct StoredEvent { int id; string type; string message; int lifetimeRemaining; } StoredEvent[][string] byUrl; int thisId = ++eventId; if(event.lifetime) byUrl[event.url.idup] ~= StoredEvent(thisId, event.type.idup, event.message.idup, event.lifetime); auto connectionsPtr = event.url in eventConnectionsByUrl; EventConnection[] connections; if(connectionsPtr is null) return; else connections = *connectionsPtr; char[4096] buffer; char[] formattedMessage; void append(const char[] a) { // the 6's here are to leave room for a HTTP chunk header, if it proves necessary buffer[6 + formattedMessage.length .. 6 + formattedMessage.length + a.length] = a[]; formattedMessage = buffer[6 .. 6 + formattedMessage.length + a.length]; } import std.algorithm.iteration; if(connections.length) { append("id: "); append(to!string(thisId)); append("\n"); append("event: "); append(event.type); append("\n"); foreach(line; event.message.splitter("\n")) { append("data: "); append(line); append("\n"); } append("\n"); } // chunk it for HTTP! auto len = toHex(formattedMessage.length); buffer[4 .. 6] = "\r\n"[]; buffer[4 - len.length .. 4] = len[]; buffer[6 + formattedMessage.length] = '\r'; buffer[6 + formattedMessage.length + 1] = '\n'; auto chunkedMessage = buffer[4 - len.length .. 6 + formattedMessage.length +2]; // done // FIXME: send back requests when needed // FIXME: send a single ":\n" every 15 seconds to keep alive foreach(connection; connections) { if(connection.needsChunking) { nonBlockingWrite(this, connection.fd, chunkedMessage); } else { nonBlockingWrite(this, connection.fd, formattedMessage); } } } } void runAddonServer(EIS)(string localListenerName, EIS eis) if(is(EIS : EventIoServer)) { version(Posix) { import core.sys.posix.unistd; import core.sys.posix.fcntl; import core.sys.posix.sys.un; import core.sys.posix.signal; signal(SIGPIPE, SIG_IGN); static extern(C) void sigchldhandler(int) { int status; import w = core.sys.posix.sys.wait; w.wait(&status); } signal(SIGCHLD, &sigchldhandler); int sock = socket(AF_UNIX, SOCK_STREAM, 0); if(sock == -1) throw new Exception("socket " ~ to!string(errno)); scope(failure) close(sock); cloexec(sock); // add-on server processes are assumed to be local, and thus will // use unix domain sockets. Besides, I want to pass sockets to them, // so it basically must be local (except for the session server, but meh). sockaddr_un addr; addr.sun_family = AF_UNIX; version(linux) { // on linux, we will use the abstract namespace addr.sun_path[0] = 0; addr.sun_path[1 .. localListenerName.length + 1] = cast(typeof(addr.sun_path[])) localListenerName[]; } else { // but otherwise, just use a file cuz we must. addr.sun_path[0 .. localListenerName.length] = cast(typeof(addr.sun_path[])) localListenerName[]; } if(bind(sock, cast(sockaddr*) &addr, addr.sizeof) == -1) throw new Exception("bind " ~ to!string(errno)); if(listen(sock, 128) == -1) throw new Exception("listen " ~ to!string(errno)); makeNonBlocking(sock); version(linux) { import core.sys.linux.epoll; auto epoll_fd = epoll_create1(EPOLL_CLOEXEC); if(epoll_fd == -1) throw new Exception("epoll_create1 " ~ to!string(errno)); scope(failure) close(epoll_fd); } else { import core.sys.posix.poll; } version(linux) eis.epoll_fd = epoll_fd; auto acceptOp = allocateIoOp(sock, IoOp.Read, 0, null); scope(exit) freeIoOp(acceptOp); version(linux) { epoll_event ev; ev.events = EPOLLIN | EPOLLET; ev.data.ptr = acceptOp; if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock, &ev) == -1) throw new Exception("epoll_ctl " ~ to!string(errno)); epoll_event[64] events; } else { pollfd[] pollfds; IoOp*[int] ioops; pollfds ~= pollfd(sock, POLLIN); ioops[sock] = acceptOp; } import core.time : MonoTime, seconds; MonoTime timeout = MonoTime.currTime + 15.seconds; while(true) { // FIXME: it should actually do a timerfd that runs on any thing that hasn't been run recently int timeout_milliseconds = 0; // -1; // infinite timeout_milliseconds = cast(int) (timeout - MonoTime.currTime).total!"msecs"; if(timeout_milliseconds < 0) timeout_milliseconds = 0; //writeln("waiting for ", name); version(linux) { auto nfds = epoll_wait(epoll_fd, events.ptr, events.length, timeout_milliseconds); if(nfds == -1) { if(errno == EINTR) continue; throw new Exception("epoll_wait " ~ to!string(errno)); } } else { int nfds = poll(pollfds.ptr, cast(int) pollfds.length, timeout_milliseconds); size_t lastIdx = 0; } if(nfds == 0) { eis.wait_timeout(); timeout += 15.seconds; } foreach(idx; 0 .. nfds) { version(linux) { auto flags = events[idx].events; auto ioop = cast(IoOp*) events[idx].data.ptr; } else { IoOp* ioop; foreach(tidx, thing; pollfds[lastIdx .. $]) { if(thing.revents) { ioop = ioops[thing.fd]; lastIdx += tidx + 1; break; } } } //writeln(flags, " ", ioop.fd); void newConnection() { // on edge triggering, it is important that we get it all while(true) { version(Android) { auto size = cast(int) addr.sizeof; } else { auto size = cast(uint) addr.sizeof; } auto ns = accept(sock, cast(sockaddr*) &addr, &size); if(ns == -1) { if(errno == EAGAIN || errno == EWOULDBLOCK) { // all done, got it all break; } throw new Exception("accept " ~ to!string(errno)); } cloexec(ns); makeNonBlocking(ns); auto niop = allocateIoOp(ns, IoOp.ReadSocketHandle, 4096 * 4, &eis.handleLocalConnectionData); niop.closeHandler = &eis.handleLocalConnectionClose; niop.completeHandler = &eis.handleLocalConnectionComplete; scope(failure) freeIoOp(niop); version(linux) { epoll_event nev; nev.events = EPOLLIN | EPOLLET; nev.data.ptr = niop; if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, ns, &nev) == -1) throw new Exception("epoll_ctl " ~ to!string(errno)); } else { bool found = false; foreach(ref pfd; pollfds) { if(pfd.fd < 0) { pfd.fd = ns; found = true; } } if(!found) pollfds ~= pollfd(ns, POLLIN); ioops[ns] = niop; } } } bool newConnectionCondition() { version(linux) return ioop.fd == sock && (flags & EPOLLIN); else return pollfds[idx].fd == sock && (pollfds[idx].revents & POLLIN); } if(newConnectionCondition()) { newConnection(); } else if(ioop.operation == IoOp.ReadSocketHandle) { while(true) { int in_fd; auto got = read_fd(ioop.fd, ioop.allocatedBuffer.ptr, ioop.allocatedBuffer.length, &in_fd); if(got == -1) { if(errno == EAGAIN || errno == EWOULDBLOCK) { // all done, got it all if(ioop.completeHandler) ioop.completeHandler(ioop); break; } throw new Exception("recv " ~ to!string(errno)); } if(got == 0) { if(ioop.closeHandler) { ioop.closeHandler(ioop); version(linux) {} // nothing needed else { foreach(ref pfd; pollfds) { if(pfd.fd == ioop.fd) pfd.fd = -1; } } } close(ioop.fd); freeIoOp(ioop); break; } ioop.bufferLengthUsed = cast(int) got; ioop.handler(ioop, in_fd); } } else if(ioop.operation == IoOp.Read) { while(true) { auto got = read(ioop.fd, ioop.allocatedBuffer.ptr, ioop.allocatedBuffer.length); if(got == -1) { if(errno == EAGAIN || errno == EWOULDBLOCK) { // all done, got it all if(ioop.completeHandler) ioop.completeHandler(ioop); break; } throw new Exception("recv " ~ to!string(ioop.fd) ~ " errno " ~ to!string(errno)); } if(got == 0) { if(ioop.closeHandler) ioop.closeHandler(ioop); close(ioop.fd); freeIoOp(ioop); break; } ioop.bufferLengthUsed = cast(int) got; if(ioop.handler(ioop, ioop.fd)) { close(ioop.fd); freeIoOp(ioop); break; } } } // EPOLLHUP? } } } else version(Windows) { // set up a named pipe // https://msdn.microsoft.com/en-us/library/windows/desktop/ms724251(v=vs.85).aspx // https://docs.microsoft.com/en-us/windows/desktop/api/winsock2/nf-winsock2-wsaduplicatesocketw // https://docs.microsoft.com/en-us/windows/desktop/api/winbase/nf-winbase-getnamedpipeserverprocessid } else static assert(0); } version(with_sendfd) // copied from the web and ported from C // see https://stackoverflow.com/questions/2358684/can-i-share-a-file-descriptor-to-another-process-on-linux-or-are-they-local-to-t ssize_t write_fd(int fd, void *ptr, size_t nbytes, int sendfd) { msghdr msg; iovec[1] iov; version(OSX) { // removed } else version(Android) { } else { union ControlUnion { cmsghdr cm; char[CMSG_SPACE(int.sizeof)] control; } ControlUnion control_un; cmsghdr* cmptr; msg.msg_control = control_un.control.ptr; msg.msg_controllen = control_un.control.length; cmptr = CMSG_FIRSTHDR(&msg); cmptr.cmsg_len = CMSG_LEN(int.sizeof); cmptr.cmsg_level = SOL_SOCKET; cmptr.cmsg_type = SCM_RIGHTS; *(cast(int *) CMSG_DATA(cmptr)) = sendfd; } msg.msg_name = null; msg.msg_namelen = 0; iov[0].iov_base = ptr; iov[0].iov_len = nbytes; msg.msg_iov = iov.ptr; msg.msg_iovlen = 1; return sendmsg(fd, &msg, 0); } version(with_sendfd) // copied from the web and ported from C ssize_t read_fd(int fd, void *ptr, size_t nbytes, int *recvfd) { msghdr msg; iovec[1] iov; ssize_t n; int newfd; version(OSX) { //msg.msg_accrights = cast(cattr_t) recvfd; //msg.msg_accrightslen = int.sizeof; } else version(Android) { } else { union ControlUnion { cmsghdr cm; char[CMSG_SPACE(int.sizeof)] control; } ControlUnion control_un; cmsghdr* cmptr; msg.msg_control = control_un.control.ptr; msg.msg_controllen = control_un.control.length; } msg.msg_name = null; msg.msg_namelen = 0; iov[0].iov_base = ptr; iov[0].iov_len = nbytes; msg.msg_iov = iov.ptr; msg.msg_iovlen = 1; if ( (n = recvmsg(fd, &msg, 0)) <= 0) return n; version(OSX) { //if(msg.msg_accrightslen != int.sizeof) //*recvfd = -1; } else version(Android) { } else { if ( (cmptr = CMSG_FIRSTHDR(&msg)) != null && cmptr.cmsg_len == CMSG_LEN(int.sizeof)) { if (cmptr.cmsg_level != SOL_SOCKET) throw new Exception("control level != SOL_SOCKET"); if (cmptr.cmsg_type != SCM_RIGHTS) throw new Exception("control type != SCM_RIGHTS"); *recvfd = *(cast(int *) CMSG_DATA(cmptr)); } else *recvfd = -1; /* descriptor was not passed */ } return n; } /* end read_fd */ /* Event source stuff The api is: sendEvent(string url, string type, string data, int timeout = 60*10); attachEventListener(string url, int fd, lastId) It just sends to all attached listeners, and stores it until the timeout for replaying via lastEventId. */ /* Session process stuff it stores it all. the cgi object has a session object that can grab it session may be done in the same process if possible, there is a version switch to choose if you want to override. */ struct DispatcherDefinition(alias dispatchHandler, DispatcherDetails = typeof(null)) {// if(is(typeof(dispatchHandler("str", Cgi.init, void) == bool))) { // bool delegate(string urlPrefix, Cgi cgi) dispatchHandler; alias handler = dispatchHandler; string urlPrefix; bool rejectFurther; immutable(DispatcherDetails) details; } private string urlify(string name) pure { return beautify(name, '-', true); } private string beautify(string name, char space = ' ', bool allLowerCase = false) pure { if(name == "id") return allLowerCase ? name : "ID"; char[160] buffer; int bufferIndex = 0; bool shouldCap = true; bool shouldSpace; bool lastWasCap; foreach(idx, char ch; name) { if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important if((ch >= 'A' && ch <= 'Z') || ch == '_') { if(lastWasCap) { // two caps in a row, don't change. Prolly acronym. } else { if(idx) shouldSpace = true; // new word, add space } lastWasCap = true; } else { lastWasCap = false; } if(shouldSpace) { buffer[bufferIndex++] = space; if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important shouldSpace = false; } if(shouldCap) { if(ch >= 'a' && ch <= 'z') ch -= 32; shouldCap = false; } if(allLowerCase && ch >= 'A' && ch <= 'Z') ch += 32; buffer[bufferIndex++] = ch; } return buffer[0 .. bufferIndex].idup; } /* string urlFor(alias func)() { return __traits(identifier, func); } */ /++ UDA: The name displayed to the user in auto-generated HTML. Default is `beautify(identifier)`. +/ struct DisplayName { string name; } /++ UDA: The name used in the URL or web parameter. Default is `urlify(identifier)` for functions and `identifier` for parameters and data members. +/ struct UrlName { string name; } /++ UDA: default format to respond for this method +/ struct DefaultFormat { string value; } class MissingArgumentException : Exception { string functionName; string argumentName; string argumentType; this(string functionName, string argumentName, string argumentType, string file = __FILE__, size_t line = __LINE__, Throwable next = null) { this.functionName = functionName; this.argumentName = argumentName; this.argumentType = argumentType; super("Missing Argument: " ~ this.argumentName, file, line, next); } } /++ You can throw this from an api handler to indicate a 404 response. This is done by the presentExceptionAsHtml function in the presenter. History: Added December 15, 2021 (dub v10.5) +/ class ResourceNotFoundException : Exception { string resourceType; string resourceId; this(string resourceType, string resourceId, string file = __FILE__, size_t line = __LINE__, Throwable next = null) { this.resourceType = resourceType; this.resourceId = resourceId; super("Resource not found: " ~ resourceType ~ " " ~ resourceId, file, line, next); } } /++ This can be attached to any constructor or function called from the cgi system. If it is present, the function argument can NOT be set from web params, but instead is set to the return value of the given `func`. If `func` can take a parameter of type [Cgi], it will be passed the one representing the current request. Otherwise, it must take zero arguments. Any params in your function of type `Cgi` are automatically assumed to take the cgi object for the connection. Any of type [Session] (with an argument) is also assumed to come from the cgi object. const arguments are also supported. +/ struct ifCalledFromWeb(alias func) {} // it only looks at query params for GET requests, the rest must be in the body for a function argument. auto callFromCgi(alias method, T)(T dg, Cgi cgi) { // FIXME: any array of structs should also be settable or gettable from csv as well. // FIXME: think more about checkboxes and bools. import std.traits; Parameters!method params; alias idents = ParameterIdentifierTuple!method; alias defaults = ParameterDefaults!method; const(string)[] names; const(string)[] values; // first, check for missing arguments and initialize to defaults if necessary static if(is(typeof(method) P == __parameters)) foreach(idx, param; P) {{ // see: mustNotBeSetFromWebParams static if(is(param : Cgi)) { static assert(!is(param == immutable)); cast() params[idx] = cgi; } else static if(is(param == Session!D, D)) { static assert(!is(param == immutable)); cast() params[idx] = cgi.getSessionObject!D(); } else { bool populated; foreach(uda; __traits(getAttributes, P[idx .. idx + 1])) { static if(is(uda == ifCalledFromWeb!func, alias func)) { static if(is(typeof(func(cgi)))) params[idx] = func(cgi); else params[idx] = func(); populated = true; } } if(!populated) { static if(__traits(compiles, { params[idx] = param.getAutomaticallyForCgi(cgi); } )) { params[idx] = param.getAutomaticallyForCgi(cgi); populated = true; } } if(!populated) { auto ident = idents[idx]; if(cgi.requestMethod == Cgi.RequestMethod.GET) { if(ident !in cgi.get) { static if(is(defaults[idx] == void)) { static if(is(param == bool)) params[idx] = false; else throw new MissingArgumentException(__traits(identifier, method), ident, param.stringof); } else params[idx] = defaults[idx]; } } else { if(ident !in cgi.post) { static if(is(defaults[idx] == void)) { static if(is(param == bool)) params[idx] = false; else throw new MissingArgumentException(__traits(identifier, method), ident, param.stringof); } else params[idx] = defaults[idx]; } } } } }} // second, parse the arguments in order to build up arrays, etc. static bool setVariable(T)(string name, string paramName, T* what, string value) { static if(is(T == struct)) { if(name == paramName) { *what = T.init; return true; } else { // could be a child. gonna allow either obj.field OR obj[field] string afterName; if(name[paramName.length] == '[') { int count = 1; auto idx = paramName.length + 1; while(idx < name.length && count > 0) { if(name[idx] == '[') count++; else if(name[idx] == ']') { count--; if(count == 0) break; } idx++; } if(idx == name.length) return false; // malformed auto insideBrackets = name[paramName.length + 1 .. idx]; afterName = name[idx + 1 .. $]; name = name[0 .. paramName.length]; paramName = insideBrackets; } else if(name[paramName.length] == '.') { paramName = name[paramName.length + 1 .. $]; name = paramName; int p = 0; foreach(ch; paramName) { if(ch == '.' || ch == '[') break; p++; } afterName = paramName[p .. $]; paramName = paramName[0 .. p]; } else { return false; } if(paramName.length) // set the child member switch(paramName) { foreach(idx, memberName; __traits(allMembers, T)) static if(__traits(compiles, __traits(getMember, T, memberName).offsetof)) { // data member! case memberName: return setVariable(name ~ afterName, paramName, &(__traits(getMember, *what, memberName)), value); } default: // ok, not a member } } return false; } else static if(is(T == enum)) { *what = to!T(value); return true; } else static if(isSomeString!T || isIntegral!T || isFloatingPoint!T) { *what = to!T(value); return true; } else static if(is(T == bool)) { *what = value == "1" || value == "yes" || value == "t" || value == "true" || value == "on"; return true; } else static if(is(T == K[], K)) { K tmp; if(name == paramName) { // direct - set and append if(setVariable(name, paramName, &tmp, value)) { (*what) ~= tmp; return true; } else { return false; } } else { // child, append to last element // FIXME: what about range violations??? auto ptr = &(*what)[(*what).length - 1]; return setVariable(name, paramName, ptr, value); } } else static if(is(T == V[K], K, V)) { // assoc array, name[key] is valid if(name == paramName) { // no action necessary return true; } else if(name[paramName.length] == '[') { int count = 1; auto idx = paramName.length + 1; while(idx < name.length && count > 0) { if(name[idx] == '[') count++; else if(name[idx] == ']') { count--; if(count == 0) break; } idx++; } if(idx == name.length) return false; // malformed auto insideBrackets = name[paramName.length + 1 .. idx]; auto afterName = name[idx + 1 .. $]; auto k = to!K(insideBrackets); V v; if(auto ptr = k in *what) v = *ptr; name = name[0 .. paramName.length]; //writeln(name, afterName, " ", paramName); auto ret = setVariable(name ~ afterName, paramName, &v, value); if(ret) { (*what)[k] = v; return true; } } return false; } else { static assert(0, "unsupported type for cgi call " ~ T.stringof); } //return false; } void setArgument(string name, string value) { int p; foreach(ch; name) { if(ch == '.' || ch == '[') break; p++; } auto paramName = name[0 .. p]; sw: switch(paramName) { static if(is(typeof(method) P == __parameters)) foreach(idx, param; P) { static if(mustNotBeSetFromWebParams!(P[idx], __traits(getAttributes, P[idx .. idx + 1]))) { // cannot be set from the outside } else { case idents[idx]: static if(is(param == Cgi.UploadedFile)) { params[idx] = cgi.files[name]; } else { setVariable(name, paramName, ¶ms[idx], value); } break sw; } } default: // ignore; not relevant argument } } if(cgi.requestMethod == Cgi.RequestMethod.GET) { names = cgi.allGetNamesInOrder; values = cgi.allGetValuesInOrder; } else { names = cgi.allPostNamesInOrder; values = cgi.allPostValuesInOrder; } foreach(idx, name; names) { setArgument(name, values[idx]); } static if(is(ReturnType!method == void)) { typeof(null) ret; dg(params); } else { auto ret = dg(params); } // FIXME: format return values // options are: json, html, csv. // also may need to wrap in envelope format: none, html, or json. return ret; } private bool mustNotBeSetFromWebParams(T, attrs...)() { static if(is(T : const(Cgi))) { return true; } else static if(is(T : const(Session!D), D)) { return true; } else static if(__traits(compiles, T.getAutomaticallyForCgi(Cgi.init))) { return true; } else { foreach(uda; attrs) static if(is(uda == ifCalledFromWeb!func, alias func)) return true; return false; } } private bool hasIfCalledFromWeb(attrs...)() { foreach(uda; attrs) static if(is(uda == ifCalledFromWeb!func, alias func)) return true; return false; } /++ Implies POST path for the thing itself, then GET will get the automatic form. The given customizer, if present, will be called as a filter on the Form object. History: Added December 27, 2020 +/ template AutomaticForm(alias customizer) { } /++ This is meant to be returned by a function that takes a form POST submission. You want to set the url of the new resource it created, which is set as the http Location header for a "201 Created" result, and you can also set a separate destination for browser users, which it sets via a "Refresh" header. The `resourceRepresentation` should generally be the thing you just created, and it will be the body of the http response when formatted through the presenter. The exact thing is up to you - it could just return an id, or the whole object, or perhaps a partial object. Examples: --- class Test : WebObject { @(Cgi.RequestMethod.POST) CreatedResource!int makeThing(string value) { return CreatedResource!int(value.to!int, "/resources/id"); } } --- History: Added December 18, 2021 +/ struct CreatedResource(T) { static if(!is(T == void)) T resourceRepresentation; string resourceUrl; string refreshUrl; } /+ /++ This can be attached as a UDA to a handler to add a http Refresh header on a successful run. (It will not be attached if the function throws an exception.) This will refresh the browser the given number of seconds after the page loads, to the url returned by `urlFunc`, which can be either a static function or a member method of the current handler object. You might use this for a POST handler that is normally used from ajax, but you want it to degrade gracefully to a temporarily flashed message before reloading the main page. History: Added December 18, 2021 +/ struct Refresh(alias urlFunc) { int waitInSeconds; string url() { static if(__traits(isStaticFunction, urlFunc)) return urlFunc(); else static if(is(urlFunc : string)) return urlFunc; } } +/ /+ /++ Sets a filter to be run before A before function can do validations of params and log and stop the function from running. +/ template Before(alias b) {} template After(alias b) {} +/ /+ Argument conversions: for the most part, it is to!Thing(string). But arrays and structs are a bit different. Arrays come from the cgi array. Thus they are passed arr=foo&arr=bar <-- notice the same name. Structs are first declared with an empty thing, then have their members set individually, with dot notation. The members are not required, just the initial declaration. struct Foo { int a; string b; } void test(Foo foo){} foo&foo.a=5&foo.b=str <-- the first foo declares the arg, the others set the members Arrays of structs use this declaration. void test(Foo[] foo) {} foo&foo.a=5&foo.b=bar&foo&foo.a=9 You can use a hidden input field in HTML forms to achieve this. The value of the naked name declaration is ignored. Mind that order matters! The declaration MUST come first in the string. Arrays of struct members follow this rule recursively. struct Foo { int[] a; } foo&foo.a=1&foo.a=2&foo&foo.a=1 Associative arrays are formatted with brackets, after a declaration, like structs: foo&foo[key]=value&foo[other_key]=value Note: for maximum compatibility with outside code, keep your types simple. Some libraries do not support the strict ordering requirements to work with these struct protocols. FIXME: also perhaps accept application/json to better work with outside trash. Return values are also auto-formatted according to user-requested type: for json, it loops over and converts. for html, basic types are strings. Arrays are
    . Structs are
    . Arrays of structs are tables! +/ /++ A web presenter is responsible for rendering things to HTML to be usable in a web browser. They are passed as template arguments to the base classes of [WebObject] Responsible for displaying stuff as HTML. You can put this into your own aggregate and override it. Use forwarding and specialization to customize it. When you inherit from it, pass your own class as the CRTP argument. This lets the base class templates and your overridden templates work with each other. --- class MyPresenter : WebPresenter!(MyPresenter) { @Override void presentSuccessfulReturnAsHtml(T : CustomType)(Cgi cgi, T ret, typeof(null) meta) { // present the CustomType } @Override void presentSuccessfulReturnAsHtml(T)(Cgi cgi, T ret, typeof(null) meta) { // handle everything else via the super class, which will call // back to your class when appropriate super.presentSuccessfulReturnAsHtml(cgi, ret); } } --- The meta argument in there can be overridden by your own facility. +/ class WebPresenter(CRTP) { /// A UDA version of the built-in `override`, to be used for static template polymorphism /// If you override a plain method, use `override`. If a template, use `@Override`. enum Override; string script() { return ` `; } string style() { return ` :root { --mild-border: #ccc; --middle-border: #999; --accent-color: #f2f2f2; --sidebar-color: #fefefe; } ` ~ genericFormStyling() ~ genericSiteStyling(); } string genericFormStyling() { return q"css table.automatic-data-display { border-collapse: collapse; border: solid 1px var(--mild-border); } table.automatic-data-display td { vertical-align: top; border: solid 1px var(--mild-border); padding: 2px 4px; } table.automatic-data-display th { border: solid 1px var(--mild-border); border-bottom: solid 1px var(--middle-border); padding: 2px 4px; } ol.automatic-data-display { margin: 0px; list-style-position: inside; padding: 0px; } dl.automatic-data-display { } .automatic-form { max-width: 600px; } .form-field { margin: 0.5em; padding-left: 0.5em; } .label-text { display: block; font-weight: bold; margin-left: -0.5em; } .submit-button-holder { padding-left: 2em; } .add-array-button { } css"; } string genericSiteStyling() { return q"css * { box-sizing: border-box; } html, body { margin: 0px; } body { font-family: sans-serif; } header { background: var(--accent-color); height: 64px; } footer { background: var(--accent-color); height: 64px; } #site-container { display: flex; } main { flex: 1 1 auto; order: 2; min-height: calc(100vh - 64px - 64px); padding: 4px; padding-left: 1em; } #sidebar { flex: 0 0 16em; order: 1; background: var(--sidebar-color); } css"; } import arsd.dom; Element htmlContainer() { auto document = new Document(q"html D Application
    html", true, true); return document.requireSelector("main"); } /// Renders a response as an HTTP error void renderBasicError(Cgi cgi, int httpErrorCode) { cgi.setResponseStatus(getHttpCodeText(httpErrorCode)); auto c = htmlContainer(); c.innerText = getHttpCodeText(httpErrorCode); cgi.setResponseContentType("text/html; charset=utf-8"); cgi.write(c.parentDocument.toString(), true); } template methodMeta(alias method) { enum methodMeta = null; } void presentSuccessfulReturn(T, Meta)(Cgi cgi, T ret, Meta meta, string format) { switch(format) { case "html": (cast(CRTP) this).presentSuccessfulReturnAsHtml(cgi, ret, meta); break; case "json": import arsd.jsvar; static if(is(typeof(ret) == MultipleResponses!Types, Types...)) { var json; foreach(index, type; Types) { if(ret.contains == index) json = ret.payload[index]; } } else { var json = ret; } var envelope = json; // var.emptyObject; /* envelope.success = true; envelope.result = json; envelope.error = null; */ cgi.setResponseContentType("application/json"); cgi.write(envelope.toJson(), true); break; default: cgi.setResponseStatus("406 Not Acceptable"); // not exactly but sort of. } } /// typeof(null) (which is also used to represent functions returning `void`) do nothing /// in the default presenter - allowing the function to have full low-level control over the /// response. void presentSuccessfulReturn(T : typeof(null), Meta)(Cgi cgi, T ret, Meta meta, string format) { // nothing intentionally! } /// Redirections are forwarded to [Cgi.setResponseLocation] void presentSuccessfulReturn(T : Redirection, Meta)(Cgi cgi, T ret, Meta meta, string format) { cgi.setResponseLocation(ret.to, true, getHttpCodeText(ret.code)); } /// [CreatedResource]s send code 201 and will set the given urls, then present the given representation. void presentSuccessfulReturn(T : CreatedResource!R, Meta, R)(Cgi cgi, T ret, Meta meta, string format) { cgi.setResponseStatus(getHttpCodeText(201)); if(ret.resourceUrl.length) cgi.header("Location: " ~ ret.resourceUrl); if(ret.refreshUrl.length) cgi.header("Refresh: 0;" ~ ret.refreshUrl); static if(!is(R == void)) presentSuccessfulReturn(cgi, ret.resourceRepresentation, meta, format); } /// Multiple responses deconstruct the algebraic type and forward to the appropriate handler at runtime void presentSuccessfulReturn(T : MultipleResponses!Types, Meta, Types...)(Cgi cgi, T ret, Meta meta, string format) { bool outputted = false; foreach(index, type; Types) { if(ret.contains == index) { assert(!outputted); outputted = true; (cast(CRTP) this).presentSuccessfulReturn(cgi, ret.payload[index], meta, format); } } if(!outputted) assert(0); } /++ An instance of the [arsd.dom.FileResource] interface has its own content type; assume it is a download of some sort if the filename member is non-null of the FileResource interface. +/ void presentSuccessfulReturn(T : FileResource, Meta)(Cgi cgi, T ret, Meta meta, string format) { cgi.setCache(true); // not necessarily true but meh if(auto fn = ret.filename()) { cgi.header("Content-Disposition: attachment; filename="~fn~";"); } cgi.setResponseContentType(ret.contentType); cgi.write(ret.getData(), true); } /// And the default handler for HTML will call [formatReturnValueAsHtml] and place it inside the [htmlContainer]. void presentSuccessfulReturnAsHtml(T)(Cgi cgi, T ret, typeof(null) meta) { auto container = this.htmlContainer(); container.appendChild(formatReturnValueAsHtml(ret)); cgi.write(container.parentDocument.toString(), true); } /++ History: Added January 23, 2023 (dub v11.0) +/ void presentExceptionalReturn(Meta)(Cgi cgi, Throwable t, Meta meta, string format) { switch(format) { case "html": presentExceptionAsHtml(cgi, t, meta); break; default: } } /++ If you override this, you will need to cast the exception type `t` dynamically, but can then use the template arguments here to refer back to the function. `func` is an alias to the method itself, and `dg` is a callable delegate to the same method on the live object. You could, in theory, change arguments and retry, but I provide that information mostly with the expectation that you will use them to make useful forms or richer error messages for the user. History: BREAKING CHANGE on January 23, 2023 (v11.0 ): it previously took an `alias func` and `T dg` to call the function again. I removed this in favor of a `Meta` param. Before: `void presentExceptionAsHtml(alias func, T)(Cgi cgi, Throwable t, T dg)` After: `void presentExceptionAsHtml(Meta)(Cgi cgi, Throwable t, Meta meta)` If you used the func for something, move that something into your `methodMeta` template. What is the benefit of this change? Somewhat smaller executables and faster builds thanks to more reused functions, together with enabling an easier implementation of [presentExceptionalReturn]. +/ void presentExceptionAsHtml(Meta)(Cgi cgi, Throwable t, Meta meta) { Form af; /+ foreach(attr; __traits(getAttributes, func)) { static if(__traits(isSame, attr, AutomaticForm)) { af = createAutomaticFormForFunction!(func)(dg); } } +/ presentExceptionAsHtmlImpl(cgi, t, af); } void presentExceptionAsHtmlImpl(Cgi cgi, Throwable t, Form automaticForm) { if(auto e = cast(ResourceNotFoundException) t) { auto container = this.htmlContainer(); container.addChild("p", e.msg); if(!cgi.outputtedResponseData) cgi.setResponseStatus("404 Not Found"); cgi.write(container.parentDocument.toString(), true); } else if(auto mae = cast(MissingArgumentException) t) { if(automaticForm is null) goto generic; auto container = this.htmlContainer(); if(cgi.requestMethod == Cgi.RequestMethod.POST) container.appendChild(Element.make("p", "Argument `" ~ mae.argumentName ~ "` of type `" ~ mae.argumentType ~ "` is missing")); container.appendChild(automaticForm); cgi.write(container.parentDocument.toString(), true); } else { generic: auto container = this.htmlContainer(); // import std.stdio; writeln(t.toString()); container.appendChild(exceptionToElement(t)); container.addChild("h4", "GET"); foreach(k, v; cgi.get) { auto deets = container.addChild("details"); deets.addChild("summary", k); deets.addChild("div", v); } container.addChild("h4", "POST"); foreach(k, v; cgi.post) { auto deets = container.addChild("details"); deets.addChild("summary", k); deets.addChild("div", v); } if(!cgi.outputtedResponseData) cgi.setResponseStatus("500 Internal Server Error"); cgi.write(container.parentDocument.toString(), true); } } Element exceptionToElement(Throwable t) { auto div = Element.make("div"); div.addClass("exception-display"); div.addChild("p", t.msg); div.addChild("p", "Inner code origin: " ~ typeid(t).name ~ "@" ~ t.file ~ ":" ~ to!string(t.line)); auto pre = div.addChild("pre"); string s; s = t.toString(); Element currentBox; bool on = false; foreach(line; s.splitLines) { if(!on && line.startsWith("-----")) on = true; if(!on) continue; if(line.indexOf("arsd/") != -1) { if(currentBox is null) { currentBox = pre.addChild("details"); currentBox.addChild("summary", "Framework code"); } currentBox.addChild("span", line ~ "\n"); } else { pre.addChild("span", line ~ "\n"); currentBox = null; } } return div; } /++ Returns an element for a particular type +/ Element elementFor(T)(string displayName, string name, Element function() udaSuggestion) { import std.traits; auto div = Element.make("div"); div.addClass("form-field"); static if(is(T == Cgi.UploadedFile)) { Element lbl; if(displayName !is null) { lbl = div.addChild("label"); lbl.addChild("span", displayName, "label-text"); lbl.appendText(" "); } else { lbl = div; } auto i = lbl.addChild("input", name); i.attrs.name = name; i.attrs.type = "file"; } else static if(is(T == enum)) { Element lbl; if(displayName !is null) { lbl = div.addChild("label"); lbl.addChild("span", displayName, "label-text"); lbl.appendText(" "); } else { lbl = div; } auto i = lbl.addChild("select", name); i.attrs.name = name; foreach(memberName; __traits(allMembers, T)) i.addChild("option", memberName); } else static if(is(T == struct)) { if(displayName !is null) div.addChild("span", displayName, "label-text"); auto fieldset = div.addChild("fieldset"); fieldset.addChild("legend", beautify(T.stringof)); // FIXME fieldset.addChild("input", name); foreach(idx, memberName; __traits(allMembers, T)) static if(__traits(compiles, __traits(getMember, T, memberName).offsetof)) { fieldset.appendChild(elementFor!(typeof(__traits(getMember, T, memberName)))(beautify(memberName), name ~ "." ~ memberName, null /* FIXME: pull off the UDA */)); } } else static if(isSomeString!T || isIntegral!T || isFloatingPoint!T) { Element lbl; if(displayName !is null) { lbl = div.addChild("label"); lbl.addChild("span", displayName, "label-text"); lbl.appendText(" "); } else { lbl = div; } Element i; if(udaSuggestion) { i = udaSuggestion(); lbl.appendChild(i); } else { i = lbl.addChild("input", name); } i.attrs.name = name; static if(isSomeString!T) i.attrs.type = "text"; else i.attrs.type = "number"; if(i.tagName == "textarea") i.textContent = to!string(T.init); else i.attrs.value = to!string(T.init); } else static if(is(T == bool)) { Element lbl; if(displayName !is null) { lbl = div.addChild("label"); lbl.addChild("span", displayName, "label-text"); lbl.appendText(" "); } else { lbl = div; } auto i = lbl.addChild("input", name); i.attrs.type = "checkbox"; i.attrs.value = "true"; i.attrs.name = name; } else static if(is(T == K[], K)) { auto templ = div.addChild("template"); templ.appendChild(elementFor!(K)(null, name, null /* uda??*/)); if(displayName !is null) div.addChild("span", displayName, "label-text"); auto btn = div.addChild("button"); btn.addClass("add-array-button"); btn.attrs.type = "button"; btn.innerText = "Add"; btn.attrs.onclick = q{ var a = document.importNode(this.parentNode.firstChild.content, true); this.parentNode.insertBefore(a, this); }; } else static if(is(T == V[K], K, V)) { div.innerText = "assoc array not implemented for automatic form at this time"; } else { static assert(0, "unsupported type for cgi call " ~ T.stringof); } return div; } /// creates a form for gathering the function's arguments Form createAutomaticFormForFunction(alias method, T)(T dg) { auto form = cast(Form) Element.make("form"); form.method = "POST"; // FIXME form.addClass("automatic-form"); string formDisplayName = beautify(__traits(identifier, method)); foreach(attr; __traits(getAttributes, method)) static if(is(typeof(attr) == DisplayName)) formDisplayName = attr.name; form.addChild("h3", formDisplayName); import std.traits; //Parameters!method params; //alias idents = ParameterIdentifierTuple!method; //alias defaults = ParameterDefaults!method; static if(is(typeof(method) P == __parameters)) foreach(idx, _; P) {{ alias param = P[idx .. idx + 1]; static if(!mustNotBeSetFromWebParams!(param[0], __traits(getAttributes, param))) { string displayName = beautify(__traits(identifier, param)); Element function() element; foreach(attr; __traits(getAttributes, param)) { static if(is(typeof(attr) == DisplayName)) displayName = attr.name; else static if(is(typeof(attr) : typeof(element))) { element = attr; } } auto i = form.appendChild(elementFor!(param)(displayName, __traits(identifier, param), element)); if(i.querySelector("input[type=file]") !is null) form.setAttribute("enctype", "multipart/form-data"); } }} form.addChild("div", Html(``), "submit-button-holder"); return form; } /// creates a form for gathering object members (for the REST object thing right now) Form createAutomaticFormForObject(T)(T obj) { auto form = cast(Form) Element.make("form"); form.addClass("automatic-form"); form.addChild("h3", beautify(__traits(identifier, T))); import std.traits; //Parameters!method params; //alias idents = ParameterIdentifierTuple!method; //alias defaults = ParameterDefaults!method; foreach(idx, memberName; __traits(derivedMembers, T)) {{ static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) { string displayName = beautify(memberName); Element function() element; foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) static if(is(typeof(attr) == DisplayName)) displayName = attr.name; else static if(is(typeof(attr) : typeof(element))) element = attr; form.appendChild(elementFor!(typeof(__traits(getMember, T, memberName)))(displayName, memberName, element)); form.setValue(memberName, to!string(__traits(getMember, obj, memberName))); }}} form.addChild("div", Html(``), "submit-button-holder"); return form; } /// Element formatReturnValueAsHtml(T)(T t) { import std.traits; static if(is(T == typeof(null))) { return Element.make("span"); } else static if(is(T : Element)) { return t; } else static if(is(T == MultipleResponses!Types, Types...)) { foreach(index, type; Types) { if(t.contains == index) return formatReturnValueAsHtml(t.payload[index]); } assert(0); } else static if(is(T == Paginated!E, E)) { auto e = Element.make("div").addClass("paginated-result"); e.appendChild(formatReturnValueAsHtml(t.items)); if(t.nextPageUrl.length) e.appendChild(Element.make("a", "Next Page", t.nextPageUrl)); return e; } else static if(isIntegral!T || isSomeString!T || isFloatingPoint!T) { return Element.make("span", to!string(t), "automatic-data-display"); } else static if(is(T == V[K], K, V)) { auto dl = Element.make("dl"); dl.addClass("automatic-data-display associative-array"); foreach(k, v; t) { dl.addChild("dt", to!string(k)); dl.addChild("dd", formatReturnValueAsHtml(v)); } return dl; } else static if(is(T == struct)) { auto dl = Element.make("dl"); dl.addClass("automatic-data-display struct"); foreach(idx, memberName; __traits(allMembers, T)) static if(__traits(compiles, __traits(getMember, T, memberName).offsetof)) { dl.addChild("dt", beautify(memberName)); dl.addChild("dd", formatReturnValueAsHtml(__traits(getMember, t, memberName))); } return dl; } else static if(is(T == bool)) { return Element.make("span", t ? "true" : "false", "automatic-data-display"); } else static if(is(T == E[], E)) { static if(is(E : RestObject!Proxy, Proxy)) { // treat RestObject similar to struct auto table = cast(Table) Element.make("table"); table.addClass("automatic-data-display"); string[] names; foreach(idx, memberName; __traits(derivedMembers, E)) static if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) { names ~= beautify(memberName); } table.appendHeaderRow(names); foreach(l; t) { auto tr = table.appendRow(); foreach(idx, memberName; __traits(derivedMembers, E)) static if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) { static if(memberName == "id") { string val = to!string(__traits(getMember, l, memberName)); tr.addChild("td", Element.make("a", val, E.stringof.toLower ~ "s/" ~ val)); // FIXME } else { tr.addChild("td", formatReturnValueAsHtml(__traits(getMember, l, memberName))); } } } return table; } else static if(is(E == struct)) { // an array of structs is kinda special in that I like // having those formatted as tables. auto table = cast(Table) Element.make("table"); table.addClass("automatic-data-display"); string[] names; foreach(idx, memberName; __traits(allMembers, E)) static if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) { names ~= beautify(memberName); } table.appendHeaderRow(names); foreach(l; t) { auto tr = table.appendRow(); foreach(idx, memberName; __traits(allMembers, E)) static if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) { tr.addChild("td", formatReturnValueAsHtml(__traits(getMember, l, memberName))); } } return table; } else { // otherwise, I will just make a list. auto ol = Element.make("ol"); ol.addClass("automatic-data-display"); foreach(e; t) ol.addChild("li", formatReturnValueAsHtml(e)); return ol; } } else static if(is(T : Object)) { static if(is(typeof(t.toHtml()))) // FIXME: maybe i will make this an interface return Element.make("div", t.toHtml()); else return Element.make("div", t.toString()); } else static assert(0, "bad return value for cgi call " ~ T.stringof); assert(0); } } /++ The base class for the [dispatcher] function and object support. +/ class WebObject { //protected Cgi cgi; protected void initialize(Cgi cgi) { //this.cgi = cgi; } } /++ Can return one of the given types, decided at runtime. The syntax is to declare all the possible types in the return value, then you can `return typeof(return)(...value...)` to construct it. It has an auto-generated constructor for each value it can hold. --- MultipleResponses!(Redirection, string) getData(int how) { if(how & 1) return typeof(return)(Redirection("http://dpldocs.info/")); else return typeof(return)("hi there!"); } --- If you have lots of returns, you could, inside the function, `alias r = typeof(return);` to shorten it a little. +/ struct MultipleResponses(T...) { private size_t contains; private union { private T payload; } static foreach(index, type; T) public this(type t) { contains = index; payload[index] = t; } /++ This is primarily for testing. It is your way of getting to the response. Let's say you wanted to test that one holding a Redirection and a string actually holds a string, by name of "test": --- auto valueToTest = your_test_function(); valueToTest.visit( (Redirection r) { assert(0); }, // got a redirection instead of a string, fail the test (string s) { assert(s == "test"); } // right value, go ahead and test it. ); --- History: Was horribly broken until June 16, 2022. Ironically, I wrote it for tests but never actually tested it. It tried to use alias lambdas before, but runtime delegates work much better so I changed it. +/ void visit(Handlers...)(Handlers handlers) { template findHandler(type, int count, HandlersToCheck...) { static if(HandlersToCheck.length == 0) enum findHandler = -1; else { static if(is(typeof(HandlersToCheck[0].init(type.init)))) enum findHandler = count; else enum findHandler = findHandler!(type, count + 1, HandlersToCheck[1 .. $]); } } foreach(index, type; T) { enum handlerIndex = findHandler!(type, 0, Handlers); static if(handlerIndex == -1) static assert(0, "Type " ~ type.stringof ~ " was not handled by visitor"); else { if(index == this.contains) handlers[handlerIndex](this.payload[index]); } } } /+ auto toArsdJsvar()() { import arsd.jsvar; return var(null); } +/ } // FIXME: implement this somewhere maybe struct RawResponse { int code; string[] headers; const(ubyte)[] responseBody; } /++ You can return this from [WebObject] subclasses for redirections. (though note the static types means that class must ALWAYS redirect if you return this directly. You might want to return [MultipleResponses] if it can be conditional) +/ struct Redirection { string to; /// The URL to redirect to. int code = 303; /// The HTTP code to return. } /++ Serves a class' methods, as a kind of low-state RPC over the web. To be used with [dispatcher]. Usage of this function will add a dependency on [arsd.dom] and [arsd.jsvar] unless you have overridden the presenter in the dispatcher. FIXME: explain this better You can overload functions to a limited extent: you can provide a zero-arg and non-zero-arg function, and non-zero-arg functions can filter via UDAs for various http methods. Do not attempt other overloads, the runtime result of that is undefined. A method is assumed to allow any http method unless it lists some in UDAs, in which case it is limited to only those. (this might change, like maybe i will use pure as an indicator GET is ok. idk.) $(WARNING --- // legal in D, undefined runtime behavior with cgi.d, it may call either method // even if you put different URL udas on it, the current code ignores them. void foo(int a) {} void foo(string a) {} --- ) See_Also: [serveRestObject], [serveStaticFile] +/ auto serveApi(T)(string urlPrefix) { assert(urlPrefix[$ - 1] == '/'); return serveApiInternal!T(urlPrefix); } private string nextPieceFromSlash(ref string remainingUrl) { if(remainingUrl.length == 0) return remainingUrl; int slash = 0; while(slash < remainingUrl.length && remainingUrl[slash] != '/') // && remainingUrl[slash] != '.') slash++; // I am specifically passing `null` to differentiate it vs empty string // so in your ctor, `items` means new T(null) and `items/` means new T("") auto ident = remainingUrl.length == 0 ? null : remainingUrl[0 .. slash]; // so if it is the last item, the dot can be used to load an alternative view // otherwise tho the dot is considered part of the identifier // FIXME // again notice "" vs null here! if(slash == remainingUrl.length) remainingUrl = null; else remainingUrl = remainingUrl[slash + 1 .. $]; return ident; } /++ UDA used to indicate to the [dispatcher] that a trailing slash should always be added to or removed from the url. It will do it as a redirect header as-needed. +/ enum AddTrailingSlash; /// ditto enum RemoveTrailingSlash; private auto serveApiInternal(T)(string urlPrefix) { import arsd.dom; import arsd.jsvar; static bool internalHandler(Presenter)(string urlPrefix, Cgi cgi, Presenter presenter, immutable void* details) { string remainingUrl = cgi.pathInfo[urlPrefix.length .. $]; try { // see duplicated code below by searching subresource_ctor // also see mustNotBeSetFromWebParams static if(is(typeof(T.__ctor) P == __parameters)) { P params; foreach(pidx, param; P) { static if(is(param : Cgi)) { static assert(!is(param == immutable)); cast() params[pidx] = cgi; } else static if(is(param == Session!D, D)) { static assert(!is(param == immutable)); cast() params[pidx] = cgi.getSessionObject!D(); } else { static if(hasIfCalledFromWeb!(__traits(getAttributes, P[pidx .. pidx + 1]))) { foreach(uda; __traits(getAttributes, P[pidx .. pidx + 1])) { static if(is(uda == ifCalledFromWeb!func, alias func)) { static if(is(typeof(func(cgi)))) params[pidx] = func(cgi); else params[pidx] = func(); } } } else { static if(__traits(compiles, { params[pidx] = param.getAutomaticallyForCgi(cgi); } )) { params[pidx] = param.getAutomaticallyForCgi(cgi); } else static if(is(param == string)) { auto ident = nextPieceFromSlash(remainingUrl); params[pidx] = ident; } else static assert(0, "illegal type for subresource " ~ param.stringof); } } } auto obj = new T(params); } else { auto obj = new T(); } return internalHandlerWithObject(obj, remainingUrl, cgi, presenter); } catch(Throwable t) { switch(cgi.request("format", "html")) { case "html": static void dummy() {} presenter.presentExceptionAsHtml(cgi, t, null); return true; case "json": var envelope = var.emptyObject; envelope.success = false; envelope.result = null; envelope.error = t.toString(); cgi.setResponseContentType("application/json"); cgi.write(envelope.toJson(), true); return true; default: throw t; // return true; } // return true; } assert(0); } static bool internalHandlerWithObject(T, Presenter)(T obj, string remainingUrl, Cgi cgi, Presenter presenter) { obj.initialize(cgi); /+ Overload rules: Any unique combination of HTTP verb and url path can be dispatched to function overloads statically. Moreover, some args vs no args can be overloaded dynamically. +/ auto methodNameFromUrl = nextPieceFromSlash(remainingUrl); /+ auto orig = remainingUrl; assert(0, (orig is null ? "__null" : orig) ~ " .. " ~ (methodNameFromUrl is null ? "__null" : methodNameFromUrl)); +/ if(methodNameFromUrl is null) methodNameFromUrl = "__null"; string hack = to!string(cgi.requestMethod) ~ " " ~ methodNameFromUrl; if(remainingUrl.length) hack ~= "/"; switch(hack) { foreach(methodName; __traits(derivedMembers, T)) static if(methodName != "__ctor") foreach(idx, overload; __traits(getOverloads, T, methodName)) { static if(is(typeof(overload) P == __parameters)) static if(is(typeof(overload) R == return)) static if(__traits(getProtection, overload) == "public" || __traits(getProtection, overload) == "export") { static foreach(urlNameForMethod; urlNamesForMethod!(overload, urlify(methodName))) case urlNameForMethod: static if(is(R : WebObject)) { // if it returns a WebObject, it is considered a subresource. That means the url is dispatched like the ctor above. // the only argument it is allowed to take, outside of cgi, session, and set up thingies, is a single string // subresource_ctor // also see mustNotBeSetFromWebParams P params; string ident; foreach(pidx, param; P) { static if(is(param : Cgi)) { static assert(!is(param == immutable)); cast() params[pidx] = cgi; } else static if(is(param == typeof(presenter))) { cast() param[pidx] = presenter; } else static if(is(param == Session!D, D)) { static assert(!is(param == immutable)); cast() params[pidx] = cgi.getSessionObject!D(); } else { static if(hasIfCalledFromWeb!(__traits(getAttributes, P[pidx .. pidx + 1]))) { foreach(uda; __traits(getAttributes, P[pidx .. pidx + 1])) { static if(is(uda == ifCalledFromWeb!func, alias func)) { static if(is(typeof(func(cgi)))) params[pidx] = func(cgi); else params[pidx] = func(); } } } else { static if(__traits(compiles, { params[pidx] = param.getAutomaticallyForCgi(cgi); } )) { params[pidx] = param.getAutomaticallyForCgi(cgi); } else static if(is(param == string)) { ident = nextPieceFromSlash(remainingUrl); if(ident is null) { // trailing slash mandated on subresources cgi.setResponseLocation(cgi.pathInfo ~ "/"); return true; } else { params[pidx] = ident; } } else static assert(0, "illegal type for subresource " ~ param.stringof); } } } auto nobj = (__traits(getOverloads, obj, methodName)[idx])(ident); return internalHandlerWithObject!(typeof(nobj), Presenter)(nobj, remainingUrl, cgi, presenter); } else { // 404 it if any url left - not a subresource means we don't get to play with that! if(remainingUrl.length) return false; bool automaticForm; foreach(attr; __traits(getAttributes, overload)) static if(is(attr == AddTrailingSlash)) { if(remainingUrl is null) { cgi.setResponseLocation(cgi.pathInfo ~ "/"); return true; } } else static if(is(attr == RemoveTrailingSlash)) { if(remainingUrl !is null) { cgi.setResponseLocation(cgi.pathInfo[0 .. lastIndexOf(cgi.pathInfo, "/")]); return true; } } else static if(__traits(isSame, AutomaticForm, attr)) { automaticForm = true; } /+ int zeroArgOverload = -1; int overloadCount = cast(int) __traits(getOverloads, T, methodName).length; bool calledWithZeroArgs = true; foreach(k, v; cgi.get) if(k != "format") { calledWithZeroArgs = false; break; } foreach(k, v; cgi.post) if(k != "format") { calledWithZeroArgs = false; break; } // first, we need to go through and see if there is an empty one, since that // changes inside. But otherwise, all the stuff I care about can be done via // simple looping (other improper overloads might be flagged for runtime semantic check) // // an argument of type Cgi is ignored for these purposes static foreach(idx, overload; __traits(getOverloads, T, methodName)) {{ static if(is(typeof(overload) P == __parameters)) static if(P.length == 0) zeroArgOverload = cast(int) idx; else static if(P.length == 1 && is(P[0] : Cgi)) zeroArgOverload = cast(int) idx; }} // FIXME: static assert if there are multiple non-zero-arg overloads usable with a single http method. bool overloadHasBeenCalled = false; static foreach(idx, overload; __traits(getOverloads, T, methodName)) {{ bool callFunction = true; // there is a zero arg overload and this is NOT it, and we have zero args - don't call this if(overloadCount > 1 && zeroArgOverload != -1 && idx != zeroArgOverload && calledWithZeroArgs) callFunction = false; // if this is the zero-arg overload, obviously it cannot be called if we got any args. if(overloadCount > 1 && idx == zeroArgOverload && !calledWithZeroArgs) callFunction = false; // FIXME: so if you just add ?foo it will give the error below even when. this might not be a great idea. bool hadAnyMethodRestrictions = false; bool foundAcceptableMethod = false; foreach(attr; __traits(getAttributes, overload)) { static if(is(typeof(attr) == Cgi.RequestMethod)) { hadAnyMethodRestrictions = true; if(attr == cgi.requestMethod) foundAcceptableMethod = true; } } if(hadAnyMethodRestrictions && !foundAcceptableMethod) callFunction = false; /+ The overloads we really want to allow are the sane ones from the web perspective. Which is likely on HTTP verbs, for the most part, but might also be potentially based on some args vs zero args, or on argument names. Can't really do argument types very reliable through the web though; those should probably be different URLs. Even names I feel is better done inside the function, so I'm not going to support that here. But the HTTP verbs and zero vs some args makes sense - it lets you define custom forms pretty easily. Moreover, I'm of the opinion that empty overload really only makes sense on GET for this case. On a POST, it is just a missing argument exception and that should be handled by the presenter. But meh, I'll let the user define that, D only allows one empty arg thing anyway so the method UDAs are irrelevant. +/ if(callFunction) +/ auto format = cgi.request("format", defaultFormat!overload()); auto wantsFormFormat = format.startsWith("form-"); if(wantsFormFormat || (automaticForm && cgi.requestMethod == Cgi.RequestMethod.GET)) { // Should I still show the form on a json thing? idk... auto ret = presenter.createAutomaticFormForFunction!((__traits(getOverloads, obj, methodName)[idx]))(&(__traits(getOverloads, obj, methodName)[idx])); presenter.presentSuccessfulReturn(cgi, ret, presenter.methodMeta!(__traits(getOverloads, obj, methodName)[idx]), wantsFormFormat ? format["form_".length .. $] : "html"); return true; } try { // a void return (or typeof(null) lol) means you, the user, is doing it yourself. Gives full control. auto ret = callFromCgi!(__traits(getOverloads, obj, methodName)[idx])(&(__traits(getOverloads, obj, methodName)[idx]), cgi); presenter.presentSuccessfulReturn(cgi, ret, presenter.methodMeta!(__traits(getOverloads, obj, methodName)[idx]), format); } catch(Throwable t) { // presenter.presentExceptionAsHtml!(__traits(getOverloads, obj, methodName)[idx])(cgi, t, &(__traits(getOverloads, obj, methodName)[idx])); presenter.presentExceptionalReturn(cgi, t, presenter.methodMeta!(__traits(getOverloads, obj, methodName)[idx]), format); } return true; //}} //cgi.header("Accept: POST"); // FIXME list the real thing //cgi.setResponseStatus("405 Method Not Allowed"); // again, not exactly, but sort of. no overload matched our args, almost certainly due to http verb filtering. //return true; } } } case "GET script.js": cgi.setResponseContentType("text/javascript"); cgi.gzipResponse = true; cgi.write(presenter.script(), true); return true; case "GET style.css": cgi.setResponseContentType("text/css"); cgi.gzipResponse = true; cgi.write(presenter.style(), true); return true; default: return false; } assert(0); } return DispatcherDefinition!internalHandler(urlPrefix, false); } string defaultFormat(alias method)() { bool nonConstConditionForWorkingAroundASpuriousDmdWarning = true; foreach(attr; __traits(getAttributes, method)) { static if(is(typeof(attr) == DefaultFormat)) { if(nonConstConditionForWorkingAroundASpuriousDmdWarning) return attr.value; } } return "html"; } struct Paginated(T) { T[] items; string nextPageUrl; } template urlNamesForMethod(alias method, string default_) { string[] helper() { auto verb = Cgi.RequestMethod.GET; bool foundVerb = false; bool foundNoun = false; string def = default_; bool hasAutomaticForm = false; foreach(attr; __traits(getAttributes, method)) { static if(is(typeof(attr) == Cgi.RequestMethod)) { verb = attr; if(foundVerb) assert(0, "Multiple http verbs on one function is not currently supported"); foundVerb = true; } static if(is(typeof(attr) == UrlName)) { if(foundNoun) assert(0, "Multiple url names on one function is not currently supported"); foundNoun = true; def = attr.name; } static if(__traits(isSame, attr, AutomaticForm)) { hasAutomaticForm = true; } } if(def is null) def = "__null"; string[] ret; static if(is(typeof(method) R == return)) { static if(is(R : WebObject)) { def ~= "/"; foreach(v; __traits(allMembers, Cgi.RequestMethod)) ret ~= v ~ " " ~ def; } else { if(hasAutomaticForm) { ret ~= "GET " ~ def; ret ~= "POST " ~ def; } else { ret ~= to!string(verb) ~ " " ~ def; } } } else static assert(0); return ret; } enum urlNamesForMethod = helper(); } enum AccessCheck { allowed, denied, nonExistent, } enum Operation { show, create, replace, remove, update } enum UpdateResult { accessDenied, noSuchResource, success, failure, unnecessary } enum ValidationResult { valid, invalid } /++ The base of all REST objects, to be used with [serveRestObject] and [serveRestCollectionOf]. WARNING: this is not stable. +/ class RestObject(CRTP) : WebObject { import arsd.dom; import arsd.jsvar; /// Prepare the object to be shown. void show() {} /// ditto void show(string urlId) { load(urlId); show(); } /// Override this to provide access control to this object. AccessCheck accessCheck(string urlId, Operation operation) { return AccessCheck.allowed; } ValidationResult validate() { // FIXME return ValidationResult.valid; } string getUrlSlug() { import std.conv; static if(is(typeof(CRTP.id))) return to!string((cast(CRTP) this).id); else return null; } // The functions with more arguments are the low-level ones, // they forward to the ones with fewer arguments by default. // POST on a parent collection - this is called from a collection class after the members are updated /++ Given a populated object, this creates a new entry. Returns the url identifier of the new object. +/ string create(scope void delegate() applyChanges) { applyChanges(); save(); return getUrlSlug(); } void replace() { save(); } void replace(string urlId, scope void delegate() applyChanges) { load(urlId); applyChanges(); replace(); } void update(string[] fieldList) { save(); } void update(string urlId, scope void delegate() applyChanges, string[] fieldList) { load(urlId); applyChanges(); update(fieldList); } void remove() {} void remove(string urlId) { load(urlId); remove(); } abstract void load(string urlId); abstract void save(); Element toHtml(Presenter)(Presenter presenter) { import arsd.dom; import std.conv; auto obj = cast(CRTP) this; auto div = Element.make("div"); div.addClass("Dclass_" ~ CRTP.stringof); div.dataset.url = getUrlSlug(); bool first = true; foreach(idx, memberName; __traits(derivedMembers, CRTP)) static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) { if(!first) div.addChild("br"); else first = false; div.appendChild(presenter.formatReturnValueAsHtml(__traits(getMember, obj, memberName))); } return div; } var toJson() { import arsd.jsvar; var v = var.emptyObject(); auto obj = cast(CRTP) this; foreach(idx, memberName; __traits(derivedMembers, CRTP)) static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) { v[memberName] = __traits(getMember, obj, memberName); } return v; } /+ auto structOf(this This) { } +/ } // FIXME XSRF token, prolly can just put in a cookie and then it needs to be copied to header or form hidden value // https://use-the-index-luke.com/sql/partial-results/fetch-next-page /++ Base class for REST collections. +/ class CollectionOf(Obj) : RestObject!(CollectionOf) { /// You might subclass this and use the cgi object's query params /// to implement a search filter, for example. /// /// FIXME: design a way to auto-generate that form /// (other than using the WebObject thing above lol // it'll prolly just be some searchParams UDA or maybe an enum. // // pagination too perhaps. // // and sorting too IndexResult index() { return IndexResult.init; } string[] sortableFields() { return null; } string[] searchableFields() { return null; } struct IndexResult { Obj[] results; string[] sortableFields; string previousPageIdentifier; string nextPageIdentifier; string firstPageIdentifier; string lastPageIdentifier; int numberOfPages; } override string create(scope void delegate() applyChanges) { assert(0); } override void load(string urlId) { assert(0); } override void save() { assert(0); } override void show() { index(); } override void show(string urlId) { show(); } /// Proxy POST requests (create calls) to the child collection alias PostProxy = Obj; } /++ Serves a REST object, similar to a Ruby on Rails resource. You put data members in your class. cgi.d will automatically make something out of those. It will call your constructor with the ID from the URL. This may be null. It will then populate the data members from the request. It will then call a method, if present, telling what happened. You don't need to write these! It finally returns a reply. Your methods are passed a list of fields it actually set. The URL mapping - despite my general skepticism of the wisdom - matches up with what most REST APIs I have used seem to follow. (I REALLY want to put trailing slashes on it though. Works better with relative linking. But meh.) GET /items -> index. all values not set. GET /items/id -> get. only ID will be set, other params ignored. POST /items -> create. values set as given PUT /items/id -> replace. values set as given or POST /items/id with cgi.post["_method"] (thus urlencoded or multipart content-type) set to "PUT" to work around browser/html limitation a GET with cgi.get["_method"] (in the url) set to "PUT" will render a form. PATCH /items/id -> update. values set as given, list of changed fields passed or POST /items/id with cgi.post["_method"] == "PATCH" DELETE /items/id -> destroy. only ID guaranteed to be set or POST /items/id with cgi.post["_method"] == "DELETE" Following the stupid convention, there will never be a trailing slash here, and if it is there, it will redirect you away from it. API clients should set the `Accept` HTTP header to application/json or the cgi.get["_format"] = "json" var. I will also let you change the default, if you must. // One add-on is validation. You can issue a HTTP GET to a resource with _method = VALIDATE to check potential changes. You can define sub-resources on your object inside the object. These sub-resources are also REST objects that follow the same thing. They may be individual resources or collections themselves. Your class is expected to have at least the following methods: FIXME: i kinda wanna add a routes object to the initialize call create Create returns the new address on success, some code on failure. show index update remove You will want to be able to customize the HTTP, HTML, and JSON returns but generally shouldn't have to - the defaults should usually work. The returned JSON will include a field "href" on all returned objects along with "id". Or something like that. Usage of this function will add a dependency on [arsd.dom] and [arsd.jsvar]. NOT IMPLEMENTED Really, a collection is a resource with a bunch of subresources. GET /items index because it is GET on the top resource GET /items/foo item but different than items? class Items { } ... but meh, a collection can be automated. not worth making it a separate thing, let's look at a real example. Users has many items and a virtual one, /users/current. the individual users have properties and two sub-resources: session, which is just one, and comments, a collection. class User : RestObject!() { // no parent int id; string name; // the default implementations of the urlId ones is to call load(that_id) then call the arg-less one. // but you can override them to do it differently. // any member which is of type RestObject can be linked automatically via href btw. void show() {} void show(string urlId) {} // automated! GET of this specific thing void create() {} // POST on a parent collection - this is called from a collection class after the members are updated void replace(string urlId) {} // this is the PUT; really, it just updates all fields. void update(string urlId, string[] fieldList) {} // PATCH, it updates some fields. void remove(string urlId) {} // DELETE void load(string urlId) {} // the default implementation of show() populates the id, then this() {} mixin Subresource!Session; mixin Subresource!Comment; } class Session : RestObject!() { // the parent object may not be fully constructed/loaded this(User parent) {} } class Comment : CollectionOf!Comment { this(User parent) {} } class Users : CollectionOf!User { // but you don't strictly need ANYTHING on a collection; it will just... collect. Implement the subobjects. void index() {} // GET on this specific thing; just like show really, just different name for the different semantics. User create() {} // You MAY implement this, but the default is to create a new object, populate it from args, and then call create() on the child } +/ auto serveRestObject(T)(string urlPrefix) { assert(urlPrefix[0] == '/'); assert(urlPrefix[$ - 1] != '/', "Do NOT use a trailing slash on REST objects."); static bool internalHandler(Presenter)(string urlPrefix, Cgi cgi, Presenter presenter, immutable void* details) { string url = cgi.pathInfo[urlPrefix.length .. $]; if(url.length && url[$ - 1] == '/') { // remove the final slash... cgi.setResponseLocation(cgi.scriptName ~ cgi.pathInfo[0 .. $ - 1]); return true; } return restObjectServeHandler!T(cgi, presenter, url); } return DispatcherDefinition!internalHandler(urlPrefix, false); } /+ /// Convenience method for serving a collection. It will be named the same /// as type T, just with an s at the end. If you need any further, just /// write the class yourself. auto serveRestCollectionOf(T)(string urlPrefix) { assert(urlPrefix[0] == '/'); mixin(`static class `~T.stringof~`s : CollectionOf!(T) {}`); return serveRestObject!(mixin(T.stringof ~ "s"))(urlPrefix); } +/ bool restObjectServeHandler(T, Presenter)(Cgi cgi, Presenter presenter, string url) { string urlId = null; if(url.length && url[0] == '/') { // asking for a subobject urlId = url[1 .. $]; foreach(idx, ch; urlId) { if(ch == '/') { urlId = urlId[0 .. idx]; break; } } } // FIXME handle other subresources static if(is(T : CollectionOf!(C), C)) { if(urlId !is null) { return restObjectServeHandler!(C, Presenter)(cgi, presenter, url); // FIXME? urlId); } } // FIXME: support precondition failed, if-modified-since, expectation failed, etc. auto obj = new T(); obj.initialize(cgi); // FIXME: populate reflection info delegates // FIXME: I am not happy with this. switch(urlId) { case "script.js": cgi.setResponseContentType("text/javascript"); cgi.gzipResponse = true; cgi.write(presenter.script(), true); return true; case "style.css": cgi.setResponseContentType("text/css"); cgi.gzipResponse = true; cgi.write(presenter.style(), true); return true; default: // intentionally blank } static void applyChangesTemplate(Obj)(Cgi cgi, Obj obj) { foreach(idx, memberName; __traits(derivedMembers, Obj)) static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) { __traits(getMember, obj, memberName) = cgi.request(memberName, __traits(getMember, obj, memberName)); } } void applyChanges() { applyChangesTemplate(cgi, obj); } string[] modifiedList; void writeObject(bool addFormLinks) { if(cgi.request("format") == "json") { cgi.setResponseContentType("application/json"); cgi.write(obj.toJson().toString, true); } else { auto container = presenter.htmlContainer(); if(addFormLinks) { static if(is(T : CollectionOf!(C), C)) container.appendHtml(` `); else container.appendHtml(` Back
    `); } container.appendChild(obj.toHtml(presenter)); cgi.write(container.parentDocument.toString, true); } } // FIXME: I think I need a set type in here.... // it will be nice to pass sets of members. try switch(cgi.requestMethod) { case Cgi.RequestMethod.GET: // I could prolly use template this parameters in the implementation above for some reflection stuff. // sure, it doesn't automatically work in subclasses... but I instantiate here anyway... // automatic forms here for usable basic auto site from browser. // even if the format is json, it could actually send out the links and formats, but really there i'ma be meh. switch(cgi.request("_method", "GET")) { case "GET": static if(is(T : CollectionOf!(C), C)) { auto results = obj.index(); if(cgi.request("format", "html") == "html") { auto container = presenter.htmlContainer(); auto html = presenter.formatReturnValueAsHtml(results.results); container.appendHtml(`
    `); container.appendChild(html); cgi.write(container.parentDocument.toString, true); } else { cgi.setResponseContentType("application/json"); import arsd.jsvar; var json = var.emptyArray; foreach(r; results.results) { var o = var.emptyObject; foreach(idx, memberName; __traits(derivedMembers, typeof(r))) static if(__traits(compiles, __traits(getMember, r, memberName).offsetof)) { o[memberName] = __traits(getMember, r, memberName); } json ~= o; } cgi.write(json.toJson(), true); } } else { obj.show(urlId); writeObject(true); } break; case "PATCH": obj.load(urlId); goto case; case "PUT": case "POST": // an editing form for the object auto container = presenter.htmlContainer(); static if(__traits(compiles, () { auto o = new obj.PostProxy(); })) { auto form = (cgi.request("_method") == "POST") ? presenter.createAutomaticFormForObject(new obj.PostProxy()) : presenter.createAutomaticFormForObject(obj); } else { auto form = presenter.createAutomaticFormForObject(obj); } form.attrs.method = "POST"; form.setValue("_method", cgi.request("_method", "GET")); container.appendChild(form); cgi.write(container.parentDocument.toString(), true); break; case "DELETE": // FIXME: a delete form for the object (can be phrased "are you sure?") auto container = presenter.htmlContainer(); container.appendHtml(`
    Are you sure you want to delete this item?
    `); cgi.write(container.parentDocument.toString(), true); break; default: cgi.write("bad method\n", true); } break; case Cgi.RequestMethod.POST: // this is to allow compatibility with HTML forms switch(cgi.request("_method", "POST")) { case "PUT": goto PUT; case "PATCH": goto PATCH; case "DELETE": goto DELETE; case "POST": static if(__traits(compiles, () { auto o = new obj.PostProxy(); })) { auto p = new obj.PostProxy(); void specialApplyChanges() { applyChangesTemplate(cgi, p); } string n = p.create(&specialApplyChanges); } else { string n = obj.create(&applyChanges); } auto newUrl = cgi.scriptName ~ cgi.pathInfo ~ "/" ~ n; cgi.setResponseLocation(newUrl); cgi.setResponseStatus("201 Created"); cgi.write(`The object has been created.`); break; default: cgi.write("bad method\n", true); } // FIXME this should be valid on the collection, but not the child.... // 303 See Other break; case Cgi.RequestMethod.PUT: PUT: obj.replace(urlId, &applyChanges); writeObject(false); break; case Cgi.RequestMethod.PATCH: PATCH: obj.update(urlId, &applyChanges, modifiedList); writeObject(false); break; case Cgi.RequestMethod.DELETE: DELETE: obj.remove(urlId); cgi.setResponseStatus("204 No Content"); break; default: // FIXME: OPTIONS, HEAD } catch(Throwable t) { presenter.presentExceptionAsHtml(cgi, t); } return true; } /+ struct SetOfFields(T) { private void[0][string] storage; void set(string what) { //storage[what] = } void unset(string what) {} void setAll() {} void unsetAll() {} bool isPresent(string what) { return false; } } +/ /+ enum readonly; enum hideonindex; +/ /++ Returns true if I recommend gzipping content of this type. You might want to call it from your Presenter classes before calling cgi.write. --- cgi.setResponseContentType(yourContentType); cgi.gzipResponse = gzipRecommendedForContentType(yourContentType); cgi.write(yourData, true); --- This is used internally by [serveStaticFile], [serveStaticFileDirectory], [serveStaticData], and maybe others I forgot to update this doc about. The implementation considers text content to be recommended to gzip. This may change, but it seems reasonable enough for now. History: Added January 28, 2023 (dub v11.0) +/ bool gzipRecommendedForContentType(string contentType) { if(contentType.startsWith("text/")) return true; if(contentType.startsWith("application/javascript")) return true; return false; } /++ Serves a static file. To be used with [dispatcher]. See_Also: [serveApi], [serveRestObject], [dispatcher], [serveRedirect] +/ auto serveStaticFile(string urlPrefix, string filename = null, string contentType = null) { // https://baus.net/on-tcp_cork/ // man 2 sendfile assert(urlPrefix[0] == '/'); if(filename is null) filename = decodeComponent(urlPrefix[1 .. $]); // FIXME is this actually correct? if(contentType is null) { contentType = contentTypeFromFileExtension(filename); } static struct DispatcherDetails { string filename; string contentType; } static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) { if(details.contentType.indexOf("image/") == 0 || details.contentType.indexOf("audio/") == 0) cgi.setCache(true); cgi.setResponseContentType(details.contentType); cgi.gzipResponse = gzipRecommendedForContentType(details.contentType); cgi.write(std.file.read(details.filename), true); return true; } return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, true, DispatcherDetails(filename, contentType)); } /++ Serves static data. To be used with [dispatcher]. History: Added October 31, 2021 +/ auto serveStaticData(string urlPrefix, immutable(void)[] data, string contentType = null) { assert(urlPrefix[0] == '/'); if(contentType is null) { contentType = contentTypeFromFileExtension(urlPrefix); } static struct DispatcherDetails { immutable(void)[] data; string contentType; } static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) { cgi.setCache(true); cgi.setResponseContentType(details.contentType); cgi.write(details.data, true); return true; } return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, true, DispatcherDetails(data, contentType)); } string contentTypeFromFileExtension(string filename) { if(filename.endsWith(".png")) return "image/png"; if(filename.endsWith(".apng")) return "image/apng"; if(filename.endsWith(".svg")) return "image/svg+xml"; if(filename.endsWith(".jpg")) return "image/jpeg"; if(filename.endsWith(".html")) return "text/html"; if(filename.endsWith(".css")) return "text/css"; if(filename.endsWith(".js")) return "application/javascript"; if(filename.endsWith(".wasm")) return "application/wasm"; if(filename.endsWith(".mp3")) return "audio/mpeg"; if(filename.endsWith(".pdf")) return "application/pdf"; return null; } /// This serves a directory full of static files, figuring out the content-types from file extensions. /// It does not let you to descend into subdirectories (or ascend out of it, of course) auto serveStaticFileDirectory(string urlPrefix, string directory = null, bool recursive = false) { assert(urlPrefix[0] == '/'); assert(urlPrefix[$-1] == '/'); static struct DispatcherDetails { string directory; bool recursive; } if(directory is null) directory = urlPrefix[1 .. $]; if(directory.length == 0) directory = "./"; assert(directory[$-1] == '/'); static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) { auto file = decodeComponent(cgi.pathInfo[urlPrefix.length .. $]); // FIXME: is this actually correct if(details.recursive) { // never allow a backslash since it isn't in a typical url anyway and makes the following checks easier if(file.indexOf("\\") != -1) return false; import std.path; file = std.path.buildNormalizedPath(file); enum upOneDir = ".." ~ std.path.dirSeparator; // also no point doing any kind of up directory things since that makes it more likely to break out of the parent if(file == ".." || file.startsWith(upOneDir)) return false; if(std.path.isAbsolute(file)) return false; // FIXME: if it has slashes and stuff, should we redirect to the canonical resource? or what? // once it passes these filters it is probably ok. } else { if(file.indexOf("/") != -1 || file.indexOf("\\") != -1) return false; } auto contentType = contentTypeFromFileExtension(file); auto fn = details.directory ~ file; if(std.file.exists(fn)) { //if(contentType.indexOf("image/") == 0) //cgi.setCache(true); //else if(contentType.indexOf("audio/") == 0) cgi.setCache(true); cgi.setResponseContentType(contentType); cgi.gzipResponse = gzipRecommendedForContentType(contentType); cgi.write(std.file.read(fn), true); return true; } else { return false; } } return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, false, DispatcherDetails(directory, recursive)); } /++ Redirects one url to another See_Also: [dispatcher], [serveStaticFile] +/ auto serveRedirect(string urlPrefix, string redirectTo, int code = 303) { assert(urlPrefix[0] == '/'); static struct DispatcherDetails { string redirectTo; string code; } static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) { cgi.setResponseLocation(details.redirectTo, true, details.code); return true; } return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, true, DispatcherDetails(redirectTo, getHttpCodeText(code))); } /// Used exclusively with `dispatchTo` struct DispatcherData(Presenter) { Cgi cgi; /// You can use this cgi object. Presenter presenter; /// This is the presenter from top level, and will be forwarded to the sub-dispatcher. size_t pathInfoStart; /// This is forwarded to the sub-dispatcher. It may be marked private later, or at least read-only. } /++ Dispatches the URL to a specific function. +/ auto handleWith(alias handler)(string urlPrefix) { // cuz I'm too lazy to do it better right now static class Hack : WebObject { static import std.traits; @UrlName("") auto handle(std.traits.Parameters!handler args) { return handler(args); } } return urlPrefix.serveApiInternal!Hack; } /++ Dispatches the URL (and anything under it) to another dispatcher function. The function should look something like this: --- bool other(DD)(DD dd) { return dd.dispatcher!( "/whatever".serveRedirect("/success"), "/api/".serveApi!MyClass ); } --- The `DD` in there will be an instance of [DispatcherData] which you can inspect, or forward to another dispatcher here. It is a template to account for any Presenter type, so you can do compile-time analysis in your presenters. Or, of course, you could just use the exact type in your own code. You return true if you handle the given url, or false if not. Just returning the result of [dispatcher] will do a good job. +/ auto dispatchTo(alias handler)(string urlPrefix) { assert(urlPrefix[0] == '/'); assert(urlPrefix[$-1] != '/'); static bool internalHandler(Presenter)(string urlPrefix, Cgi cgi, Presenter presenter, const void* details) { return handler(DispatcherData!Presenter(cgi, presenter, urlPrefix.length)); } return DispatcherDefinition!(internalHandler)(urlPrefix, false); } /++ See [serveStaticFile] if you want to serve a file off disk. History: Added January 28, 2023 (dub v11.0) +/ auto serveStaticData(string urlPrefix, immutable(ubyte)[] data, string contentType, string filenameToSuggestAsDownload = null) { assert(urlPrefix[0] == '/'); static struct DispatcherDetails { immutable(ubyte)[] data; string contentType; string filenameToSuggestAsDownload; } static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) { cgi.setCache(true); cgi.setResponseContentType(details.contentType); if(details.filenameToSuggestAsDownload.length) cgi.header("Content-Disposition: attachment; filename=\""~details.filenameToSuggestAsDownload~"\""); cgi.gzipResponse = gzipRecommendedForContentType(details.contentType); cgi.write(details.data, true); return true; } return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, true, DispatcherDetails(data, contentType, filenameToSuggestAsDownload)); } /++ Placeholder for use with [dispatchSubsection]'s `NewPresenter` argument to indicate you want to keep the parent's presenter. History: Added January 28, 2023 (dub v11.0) +/ alias KeepExistingPresenter = typeof(null); /++ For use with [dispatchSubsection]. Calls your filter with the request and if your filter returns false, this issues the given errorCode and stops processing. --- bool hasAdminPermissions(Cgi cgi) { return true; } mixin DispatcherMain!( "/admin".dispatchSubsection!( passFilterOrIssueError!(hasAdminPermissions, 403), KeepExistingPresenter, "/".serveApi!AdminFunctions ) ); --- History: Added January 28, 2023 (dub v11.0) +/ template passFilterOrIssueError(alias filter, int errorCode) { bool passFilterOrIssueError(DispatcherDetails)(DispatcherDetails dd) { if(filter(dd.cgi)) return true; dd.presenter.renderBasicError(dd.cgi, errorCode); return false; } } /++ Allows for a subsection of your dispatched urls to be passed through other a pre-request filter, optionally pick up an new presenter class, and then be dispatched to their own handlers. --- /+ // a long-form filter function bool permissionCheck(DispatcherData)(DispatcherData dd) { // you are permitted to call mutable methods on the Cgi object // Note if you use a Cgi subclass, you can try dynamic casting it back to your custom type to attach per-request data // though much of the request is immutable so there's only so much you're allowed to do to modify it. if(checkPermissionOnRequest(dd.cgi)) { return true; // OK, allow processing to continue } else { dd.presenter.renderBasicError(dd.cgi, 403); // reply forbidden to the requester return false; // and stop further processing into this subsection } } +/ // but you can also do short-form filters: bool permissionCheck(Cgi cgi) { return ("ok" in cgi.get) !is null; } // handler for the subsection class AdminClass : WebObject { int foo() { return 5; } } // handler for the main site class TheMainSite : WebObject {} mixin DispatcherMain!( "/admin".dispatchSubsection!( // converts our short-form filter into a long-form filter passFilterOrIssueError!(permissionCheck, 403), // can use a new presenter if wanted for the subsection KeepExistingPresenter, // and then provide child route dispatchers "/".serveApi!AdminClass ), // and back to the top level "/".serveApi!TheMainSite ); --- Note you can encapsulate sections in files like this: --- auto adminDispatcher(string urlPrefix) { return urlPrefix.dispatchSubsection!( .... ); } mixin DispatcherMain!( "/admin".adminDispatcher, // and so on ) --- If you want no filter, you can pass `(cgi) => true` as the filter to approve all requests. If you want to keep the same presenter as the parent, use [KeepExistingPresenter] as the presenter argument. History: Added January 28, 2023 (dub v11.0) +/ auto dispatchSubsection(alias PreRequestFilter, NewPresenter, definitions...)(string urlPrefix) { assert(urlPrefix[0] == '/'); assert(urlPrefix[$-1] != '/'); static bool internalHandler(Presenter)(string urlPrefix, Cgi cgi, Presenter presenter, const void* details) { static if(!is(PreRequestFilter == typeof(null))) { if(!PreRequestFilter(DispatcherData!Presenter(cgi, presenter, urlPrefix.length))) return true; // we handled it by rejecting it } static if(is(NewPresenter == Presenter) || is(NewPresenter == typeof(null))) { return dispatcher!definitions(DispatcherData!Presenter(cgi, presenter, urlPrefix.length)); } else { auto newPresenter = new NewPresenter(); return dispatcher!(definitions(DispatcherData!NewPresenter(cgi, presenter, urlPrefix.length))); } } return DispatcherDefinition!(internalHandler)(urlPrefix, false); } /++ A URL dispatcher. --- if(cgi.dispatcher!( "/api/".serveApi!MyApiClass, "/objects/lol".serveRestObject!MyRestObject, "/file.js".serveStaticFile, "/admin/".dispatchTo!adminHandler )) return; --- You define a series of url prefixes followed by handlers. You may want to do different pre- and post- processing there, for example, an authorization check and different page layout. You can use different presenters and different function chains. See [dispatchSubsection] for details. [dispatchTo] will send the request to another function for handling. +/ template dispatcher(definitions...) { bool dispatcher(Presenter)(Cgi cgi, Presenter presenterArg = null) { static if(is(Presenter == typeof(null))) { static class GenericWebPresenter : WebPresenter!(GenericWebPresenter) {} auto presenter = new GenericWebPresenter(); } else alias presenter = presenterArg; return dispatcher(DispatcherData!(typeof(presenter))(cgi, presenter, 0)); } bool dispatcher(DispatcherData)(DispatcherData dispatcherData) if(!is(DispatcherData : Cgi)) { // I can prolly make this more efficient later but meh. foreach(definition; definitions) { if(definition.rejectFurther) { if(dispatcherData.cgi.pathInfo[dispatcherData.pathInfoStart .. $] == definition.urlPrefix) { auto ret = definition.handler( dispatcherData.cgi.pathInfo[0 .. dispatcherData.pathInfoStart + definition.urlPrefix.length], dispatcherData.cgi, dispatcherData.presenter, definition.details); if(ret) return true; } } else if( dispatcherData.cgi.pathInfo[dispatcherData.pathInfoStart .. $].startsWith(definition.urlPrefix) && // cgi.d dispatcher urls must be complete or have a /; // "foo" -> thing should NOT match "foobar", just "foo" or "foo/thing" (definition.urlPrefix[$-1] == '/' || (dispatcherData.pathInfoStart + definition.urlPrefix.length) == dispatcherData.cgi.pathInfo.length || dispatcherData.cgi.pathInfo[dispatcherData.pathInfoStart + definition.urlPrefix.length] == '/') ) { auto ret = definition.handler( dispatcherData.cgi.pathInfo[0 .. dispatcherData.pathInfoStart + definition.urlPrefix.length], dispatcherData.cgi, dispatcherData.presenter, definition.details); if(ret) return true; } } return false; } } }); private struct StackBuffer { char[1024] initial = void; char[] buffer; size_t position; this(int a) { buffer = initial[]; position = 0; } void add(in char[] what) { if(position + what.length > buffer.length) buffer.length = position + what.length + 1024; // reallocate with GC to handle special cases buffer[position .. position + what.length] = what[]; position += what.length; } void add(in char[] w1, in char[] w2, in char[] w3 = null) { add(w1); add(w2); add(w3); } void add(long v) { char[16] buffer = void; auto pos = buffer.length; bool negative; if(v < 0) { negative = true; v = -v; } do { buffer[--pos] = cast(char) (v % 10 + '0'); v /= 10; } while(v); if(negative) buffer[--pos] = '-'; auto res = buffer[pos .. $]; add(res[]); } char[] get() @nogc { return buffer[0 .. position]; } } // duplicated in http2.d private static string getHttpCodeText(int code) pure nothrow @nogc { switch(code) { case 200: return "200 OK"; case 201: return "201 Created"; case 202: return "202 Accepted"; case 203: return "203 Non-Authoritative Information"; case 204: return "204 No Content"; case 205: return "205 Reset Content"; case 206: return "206 Partial Content"; // case 300: return "300 Multiple Choices"; case 301: return "301 Moved Permanently"; case 302: return "302 Found"; case 303: return "303 See Other"; case 304: return "304 Not Modified"; case 305: return "305 Use Proxy"; case 307: return "307 Temporary Redirect"; case 308: return "308 Permanent Redirect"; // case 400: return "400 Bad Request"; case 401: return "401 Unauthorized"; case 402: return "402 Payment Required"; case 403: return "403 Forbidden"; case 404: return "404 Not Found"; case 405: return "405 Method Not Allowed"; case 406: return "406 Not Acceptable"; case 407: return "407 Proxy Authentication Required"; case 408: return "408 Request Timeout"; case 409: return "409 Conflict"; case 410: return "410 Gone"; case 411: return "411 Length Required"; case 412: return "412 Precondition Failed"; case 413: return "413 Payload Too Large"; case 414: return "414 URI Too Long"; case 415: return "415 Unsupported Media Type"; case 416: return "416 Range Not Satisfiable"; case 417: return "417 Expectation Failed"; case 418: return "418 I'm a teapot"; case 421: return "421 Misdirected Request"; case 422: return "422 Unprocessable Entity (WebDAV)"; case 423: return "423 Locked (WebDAV)"; case 424: return "424 Failed Dependency (WebDAV)"; case 425: return "425 Too Early"; case 426: return "426 Upgrade Required"; case 428: return "428 Precondition Required"; case 431: return "431 Request Header Fields Too Large"; case 451: return "451 Unavailable For Legal Reasons"; case 500: return "500 Internal Server Error"; case 501: return "501 Not Implemented"; case 502: return "502 Bad Gateway"; case 503: return "503 Service Unavailable"; case 504: return "504 Gateway Timeout"; case 505: return "505 HTTP Version Not Supported"; case 506: return "506 Variant Also Negotiates"; case 507: return "507 Insufficient Storage (WebDAV)"; case 508: return "508 Loop Detected (WebDAV)"; case 510: return "510 Not Extended"; case 511: return "511 Network Authentication Required"; // default: assert(0, "Unsupported http code"); } } /+ /++ This is the beginnings of my web.d 2.0 - it dispatches web requests to a class object. It relies on jsvar.d and dom.d. You can get javascript out of it to call. The generated functions need to look like function name(a,b,c,d,e) { return _call("name", {"realName":a,"sds":b}); } And _call returns an object you can call or set up or whatever. +/ bool apiDispatcher()(Cgi cgi) { import arsd.jsvar; import arsd.dom; } +/ version(linux) private extern(C) int eventfd (uint initval, int flags) nothrow @trusted @nogc; /* Copyright: Adam D. Ruppe, 2008 - 2023 License: [http://www.boost.org/LICENSE_1_0.txt|Boost License 1.0]. Authors: Adam D. Ruppe Copyright Adam D. Ruppe 2008 - 2023. Distributed under the Boost Software License, Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) */ onedrive-2.5.5/src/clientSideFiltering.d000066400000000000000000000713001476564400300202330ustar00rootroot00000000000000// What is this module called? module clientSideFiltering; // What does this module require to function? import std.algorithm; import std.array; import std.file; import std.path; import std.regex; import std.stdio; import std.string; import std.conv; // What other modules that we have created do we need to import? import config; import util; import log; class ClientSideFiltering { // Class variables ApplicationConfig appConfig; string[] syncListRules; Regex!char fileMask; Regex!char directoryMask; bool skipDirStrictMatch = false; bool skipDotfiles = false; this(ApplicationConfig appConfig) { // Configure the class variable to consume the application configuration this.appConfig = appConfig; } // Initialise the required items bool initialise() { // Log what is being done if (debugLogging) {addLogEntry("Configuring Client Side Filtering (Selective Sync)", ["debug"]);} // Load the sync_list file if it exists if (exists(appConfig.syncListFilePath)){ loadSyncList(appConfig.syncListFilePath); } // Configure skip_dir, skip_file, skip-dir-strict-match & skip_dotfiles from config entries // Handle skip_dir configuration in config file if (debugLogging) { addLogEntry("Configuring skip_dir ...", ["debug"]); addLogEntry("skip_dir: " ~ to!string(appConfig.getValueString("skip_dir")), ["debug"]); } setDirMask(appConfig.getValueString("skip_dir")); // Was --skip-dir-strict-match configured? if (debugLogging) { addLogEntry("Configuring skip_dir_strict_match ...", ["debug"]); addLogEntry("skip_dir_strict_match: " ~ to!string(appConfig.getValueBool("skip_dir_strict_match")), ["debug"]); } if (appConfig.getValueBool("skip_dir_strict_match")) { setSkipDirStrictMatch(); } // Was --skip-dot-files configured? if (debugLogging) { addLogEntry("Configuring skip_dotfiles ...", ["debug"]); addLogEntry("skip_dotfiles: " ~ to!string(appConfig.getValueBool("skip_dotfiles")), ["debug"]); } if (appConfig.getValueBool("skip_dotfiles")) { setSkipDotfiles(); } // Handle skip_file configuration in config file if (debugLogging) {addLogEntry("Configuring skip_file ...", ["debug"]);} // Validate skip_file to ensure that this does not contain an invalid configuration // Do not use a skip_file entry of .* as this will prevent correct searching of local changes to process. foreach(entry; appConfig.getValueString("skip_file").split("|")){ if (entry == ".*") { // invalid entry element detected addLogEntry("ERROR: Invalid skip_file entry '.*' detected"); return false; } } // All skip_file entries are valid if (debugLogging) {addLogEntry("skip_file: " ~ appConfig.getValueString("skip_file"), ["debug"]);} setFileMask(appConfig.getValueString("skip_file")); // All configured OK return true; } // Shutdown components void shutdown() { syncListRules = null; fileMask = regex(""); directoryMask = regex(""); } // Load sync_list file if it exists void loadSyncList(string filepath) { // open file as read only auto file = File(filepath, "r"); auto range = file.byLine(); foreach (line; range) { // Skip any line that is empty or just contains whitespace if (line.strip.length == 0) continue; // Skip comments in file if (line[0] == ';' || line[0] == '#') continue; // Is the rule a legacy 'include all root files' lazy rule? if ((strip(line) == "/*") || (strip(line) == "/")) { // yes ... string errorMessage = "ERROR: Invalid sync_list rule '" ~ to!string(strip(line)) ~ "' detected. Please use 'sync_root_files = \"true\"' or --sync-root-files option to sync files in the root path."; addLogEntry(); addLogEntry(errorMessage, ["info", "notify"]); addLogEntry(); // do not add this rule } else { // Ensure that 'sync_list' rules do not start with the sequence './' if ((line[0] == '.') && (line[1] == '/')) { // Display warning about 'sync_list' rule composition string errorMessage = "ERROR: Invalid sync_list rule '" ~ to!string(strip(line)) ~ "' detected. Rule should not start with './' - please fix your 'sync_list' rule."; addLogEntry(); addLogEntry(errorMessage, ["info", "notify"]); addLogEntry(); // this broken rule will be added, but will not work right until the user fixes it ... user action required } // add rule to list of rules syncListRules ~= buildNormalizedPath(line); } } // Close reading the 'sync_list' file file.close(); } // return true or false based on if we have loaded any valid sync_list rules bool validSyncListRules() { // If empty, will return true return syncListRules.empty; } // Configure the regex that will be used for 'skip_file' void setFileMask(const(char)[] mask) { fileMask = wild2regex(mask); if (debugLogging) {addLogEntry("Selective Sync File Mask: " ~ to!string(fileMask), ["debug"]);} } // Configure the regex that will be used for 'skip_dir' void setDirMask(const(char)[] dirmask) { directoryMask = wild2regex(dirmask); if (debugLogging) {addLogEntry("Selective Sync Directory Mask: " ~ to!string(directoryMask), ["debug"]);} } // Configure skipDirStrictMatch if function is called // By default, skipDirStrictMatch = false; void setSkipDirStrictMatch() { skipDirStrictMatch = true; } // Configure skipDotfiles if function is called // By default, skipDotfiles = false; void setSkipDotfiles() { skipDotfiles = true; } // return value of skipDotfiles bool getSkipDotfiles() { return skipDotfiles; } // Match against sync_list only bool isPathExcludedViaSyncList(string path) { // Debug output that we are performing a 'sync_list' inclusion / exclusion test return isPathExcluded(path); } // config file skip_dir parameter bool isDirNameExcluded(string name) { // Does the directory name match skip_dir config entry? // Returns true if the name matches a skip_dir config entry // Returns false if no match if (debugLogging) {addLogEntry("skip_dir evaluation for: " ~ name, ["debug"]);} // Ensure the path being passed in is cleaned up to remove the leading '.' if (startsWith(name, "./")) { name = name[1..$]; if (debugLogging) {addLogEntry("skip_dir evaluation for (updated): " ~ name, ["debug"]);} } // Try full path match first if (!name.matchFirst(directoryMask).empty) { if (debugLogging) {addLogEntry("skip_dir evaluation: '!name.matchFirst(directoryMask).empty' returned true = matched", ["debug"]);} return true; } // Test individual segments if not in strict match mode if (!skipDirStrictMatch) { if (debugLogging) {addLogEntry("No Strict Matching Enforced", ["debug"]);} string path = buildNormalizedPath(name); foreach_reverse(directory; pathSplitter(path)) { if (directory != "/") { if (directory.matchFirst(directoryMask)) { if (debugLogging) {addLogEntry("skip_dir evaluation: 'directory.matchFirst(directoryMask)' returned true = matched", ["debug"]);} return true; } } } } else { if (debugLogging) {addLogEntry("Strict Matching Enforced - No Match", ["debug"]);} } // No match return false; } // config file skip_file parameter bool isFileNameExcluded(string name) { // Does the file name match skip_file config entry? // Returns true if the name matches a skip_file config entry // Returns false if no match if (debugLogging) {addLogEntry("skip_file evaluation for: " ~ name, ["debug"]);} // Try full path match first if (!name.matchFirst(fileMask).empty) { return true; } else { // check just the file name string filename = baseName(name); if(!filename.matchFirst(fileMask).empty) { return true; } } // no match return false; } // test if the given path is not included in the allowed syncListRules // if there are no allowed syncListRules always return false private bool isPathExcluded(string path) { // function variables bool exclude = false; bool excludeExactMatch = false; // will get updated to true, if there is a pattern match to sync_list entry bool excludeParentMatched = false; // will get updated to true, if there is a pattern match to sync_list entry bool finalResult = true; // will get updated to false, if pattern match to sync_list entry bool anywhereRuleMatched = false; // will get updated if the 'anywhere' rule matches bool excludeAnywhereMatched = false; // will get updated if the 'anywhere' rule matches bool wildcardRuleMatched = false; // will get updated if the 'wildcard' rule matches bool excludeWildcardMatched = false; // will get updated if the 'wildcard' rule matches int offset; string wildcard = "*"; string globbing = "**"; // always allow the root if (path == ".") return false; // if there are no allowed syncListRules always return false, meaning path is not excluded if (syncListRules.empty) return false; // To ensure we are checking the 'right' path, build the path path = buildPath("/", buildNormalizedPath(path)); // Evaluation start point, in order of what is checked as well if (debugLogging) { addLogEntry("******************* SYNC LIST RULES EVALUATION START *******************", ["debug"]); addLogEntry("Evaluation against 'sync_list' rules for this input path: " ~ path, ["debug"]); addLogEntry("[S]excludeExactMatch = " ~ to!string(excludeExactMatch), ["debug"]); addLogEntry("[S]excludeParentMatched = " ~ to!string(excludeParentMatched), ["debug"]); addLogEntry("[S]excludeAnywhereMatched = " ~ to!string(excludeAnywhereMatched), ["debug"]); addLogEntry("[S]excludeWildcardMatched = " ~ to!string(excludeWildcardMatched), ["debug"]); } // Unless path is an exact match, entire sync_list entries need to be processed to ensure negative matches are also correctly detected foreach (syncListRuleEntry; syncListRules) { // There are several matches we need to think of here // Exclusions: // !foldername/* = As there is no preceding '/' (after the !) .. this is a rule that should exclude 'foldername' and all its children ANYWHERE // !*.extension = As there is no preceding '/' (after the !) .. this is a rule that should exclude any item that has the specified extension ANYWHERE // !/path/to/foldername/* = As there IS a preceding '/' (after the !) .. this is a rule that should exclude this specific path and all its children // !/path/to/foldername/*.extension = As there IS a preceding '/' (after the !) .. this is a rule that should exclude any item that has the specified extension in this path ONLY // !/path/to/foldername/*/specific_target/* = As there IS a preceding '/' (after the !) .. this excludes 'specific_target' in any subfolder of '/path/to/foldername/' // // Inclusions: // foldername/* = As there is no preceding '/' .. this is a rule that should INCLUDE 'foldername' and all its children ANYWHERE // *.extension = As there is no preceding '/' .. this is a rule that should INCLUDE any item that has the specified extension ANYWHERE // /path/to/foldername/* = As there IS a preceding '/' .. this is a rule that should INCLUDE this specific path and all its children // /path/to/foldername/*.extension = As there IS a preceding '/' .. this is a rule that should INCLUDE any item that has the specified extension in this path ONLY // /path/to/foldername/*/specific_target/* = As there IS a preceding '/' .. this INCLUDES 'specific_target' in any subfolder of '/path/to/foldername/' if (debugLogging) {addLogEntry("------------------------------ NEW RULE --------------------------------", ["debug"]);} // Is this rule an 'exclude' or 'include' rule? bool thisIsAnExcludeRule = false; // Switch based on first character of rule to determine rule type switch (syncListRuleEntry[0]) { case '-': // sync_list path starts with '-', this user wants to exclude this path exclude = true; // default exclude thisIsAnExcludeRule = true; // exclude rule offset = 1; // To negate the '-' in the rule entry break; case '!': // sync_list path starts with '!', this user wants to exclude this path exclude = true; // default exclude thisIsAnExcludeRule = true; // exclude rule offset = 1; // To negate the '!' in the rule entry break; case '/': // sync_list path starts with '/', this user wants to include this path // but a '/' at the start causes matching issues, so use the offset for comparison exclude = false; // DO NOT EXCLUDE thisIsAnExcludeRule = false; // INCLUDE rule offset = 0; break; default: // no negative pattern, default is to not exclude exclude = false; // DO NOT EXCLUDE thisIsAnExcludeRule = false; // INCLUDE rule offset = 0; } // Update syncListRuleEntry to remove the offset syncListRuleEntry = syncListRuleEntry[offset..$]; // What 'sync_list' rule are we comparing against? if (thisIsAnExcludeRule) { if (debugLogging) {addLogEntry("Evaluation against EXCLUSION 'sync_list' rule: !" ~ syncListRuleEntry, ["debug"]);} } else { if (debugLogging) {addLogEntry("Evaluation against INCLUSION 'sync_list' rule: " ~ syncListRuleEntry, ["debug"]);} } // Is path is an exact match of the 'sync_list' rule, or do the input path segments (directories) match the 'sync_list' rule? // wildcard (*) rules are below if we get there, if this rule does not contain a wildcard if ((to!string(syncListRuleEntry[0]) == "/") && (!canFind(syncListRuleEntry, wildcard))) { // attempt to perform an exact segment match // split both paths by '/' to create segment arrays string[] ruleSegments = syncListRuleEntry.strip.split("/").filter!(s => !s.empty).array; string[] pathSegments = path.strip.split("/").filter!(s => !s.empty).array; // Print rule and input segments for validation during debug if (debugLogging) { addLogEntry("Rule Segments: " ~ to!string(ruleSegments), ["debug"]); addLogEntry("Path Segments: " ~ to!string(pathSegments), ["debug"]); } if (exactMatchRuleSegmentsToPathSegments(ruleSegments, pathSegments)) { // EXACT PATH MATCH if (debugLogging) {addLogEntry("Exact path match with 'sync_list' rule entry", ["debug"]);} if (!thisIsAnExcludeRule) { // Include Rule if (debugLogging) {addLogEntry("Evaluation against 'sync_list' rule result: direct match", ["debug"]);} // final result finalResult = false; // direct match, break and search rules no more given include rule match break; } else { // Exclude rule if (debugLogging) {addLogEntry("Evaluation against 'sync_list' rule result: exclusion direct match - path to be excluded", ["debug"]);} // flag excludeExactMatch so that a 'wildcard match' will not override this exclude excludeExactMatch = true; exclude = true; // final result finalResult = true; // dont break here, finish checking other rules } } else { // NOT an EXACT MATCH, so check the very first path segment // - This is so that paths in 'sync_list' as specified as /some path/another path/ actually get included|excluded correctly if (matchFirstSegmentToPathFirstSegment(ruleSegments, pathSegments)) { // PARENT ROOT MATCH if (debugLogging) {addLogEntry("Parent root path match with 'sync_list' rule entry", ["debug"]);} // Does the 'rest' of the input path match? // We only need to do this step if the input path has more and 1 segment (the parent folder) if (count(pathSegments) > 1) { // More segments to check, so do a parental path match if (matchRuleSegmentsToPathSegments(ruleSegments, pathSegments)) { // PARENTAL PATH MATCH if (debugLogging) {addLogEntry("Parental path match with 'sync_list' rule entry", ["debug"]);} // What sort of rule was this? if (!thisIsAnExcludeRule) { // Include Rule if (debugLogging) {addLogEntry("Evaluation against 'sync_list' rule result: parental path match", ["debug"]);} // final result finalResult = false; // parental path match, break and search rules no more given include rule match break; } else { // Exclude rule if (debugLogging) {addLogEntry("Evaluation against 'sync_list' rule result: exclusion parental path match - path to be excluded", ["debug"]);} excludeParentMatched = true; exclude = true; // final result finalResult = true; // dont break here, finish checking other rules } } } else { // No more segments to check if (!thisIsAnExcludeRule) { // Include Rule if (debugLogging) {addLogEntry("Evaluation against 'sync_list' rule result: parent root path match to rule", ["debug"]);} // final result finalResult = false; // parental path match, break and search rules no more given include rule match break; } else { // Exclude rule {addLogEntry("Evaluation against 'sync_list' rule result: exclusion parent root path match to rul - path to be excluded", ["debug"]);} excludeParentMatched = true; exclude = true; // final result finalResult = true; // dont break here, finish checking other rules } } } } } // Is the 'sync_list' rule an 'anywhere' rule? // EXCLUSION // !foldername/* // !*.extension // INCLUSION // foldername/* // *.extension if (to!string(syncListRuleEntry[0]) != "/") { // reset anywhereRuleMatched anywhereRuleMatched = false; // what sort of rule if (thisIsAnExcludeRule) { if (debugLogging) {addLogEntry("anywhere 'sync_list' exclusion rule: !" ~ syncListRuleEntry, ["debug"]);} } else { if (debugLogging) {addLogEntry("anywhere 'sync_list' inclusion rule: " ~ syncListRuleEntry, ["debug"]);} } // this is an 'anywhere' rule string anywhereRuleStripped; // If this 'sync_list' rule end in '/*' - if yes, remove it to allow for easier comparison if (syncListRuleEntry.endsWith("/*")) { // strip '/*' from the end of the rule anywhereRuleStripped = syncListRuleEntry.stripRight("/*"); } else { // keep rule 'as-is' anywhereRuleStripped = syncListRuleEntry; } if (canFind(path, anywhereRuleStripped)) { // we matched the path to the rule if (debugLogging) {addLogEntry("anywhere rule 'canFind' MATCH", ["debug"]);} anywhereRuleMatched = true; } else { // no 'canFind' match, try via regex if (debugLogging) {addLogEntry("No anywhere rule 'canFind' MATCH .. trying a regex match", ["debug"]);} // create regex from 'syncListRuleEntry' auto allowedMask = regex(createRegexCompatiblePath(syncListRuleEntry)); // perform regex match attempt if (matchAll(path, allowedMask)) { // we regex matched the path to the rule if (debugLogging) {addLogEntry("anywhere rule 'matchAll via regex' MATCH", ["debug"]);} anywhereRuleMatched = true; } } // is this rule matched? if (anywhereRuleMatched) { // Is this an exclude rule? if (thisIsAnExcludeRule) { if (debugLogging) {addLogEntry("Evaluation against 'sync_list' rule result: anywhere rule matched and must be excluded", ["debug"]);} excludeAnywhereMatched = true; exclude = true; finalResult = true; // anywhere match, break and search rules no more break; } else { if (debugLogging) {addLogEntry("Evaluation against 'sync_list' rule result: anywhere rule matched and must be included", ["debug"]);} finalResult = false; excludeAnywhereMatched = false; // anywhere match, break and search rules no more break; } } } // Does the 'sync_list' rule contain a wildcard (*) or globbing (**) reference anywhere in the rule? // EXCLUSION // !/Programming/Projects/Android/**/build/* // INCLUSION // /Programming/Projects/Android/**/build/* if (canFind(syncListRuleEntry, wildcard)) { // reset the applicable flag wildcardRuleMatched = false; // Does this 'wildcard' rule even apply to this path? auto wildcardDepth = firstWildcardDepth(syncListRuleEntry); auto pathSegments = count(path.strip.split("/").filter!(s => !s.empty).array); // are there enough path segments for this wildcard rule to apply? if (pathSegments < wildcardDepth) { // there are not enough path segments up to the first wildcard character for this rule to even be applicable if (debugLogging) {addLogEntry("- This sync list wildcard rule should not be evaluated as the wildcard appears beyond the current input path", ["debug"]);} } else { // path segments are enough for this wildcard rule to potentially apply // sync_list rule contains some sort of wildcard sequence if (thisIsAnExcludeRule) { if (debugLogging) {addLogEntry("wildcard (* or **) exclusion rule: !" ~ syncListRuleEntry, ["debug"]);} } else { if (debugLogging) {addLogEntry("wildcard (* or **) inclusion rule: " ~ syncListRuleEntry, ["debug"]);} } // Is this a globbing rule (**) or just a single wildcard (*) entries if (canFind(syncListRuleEntry, globbing)) { // globbing (**) rule processing if (matchPathAgainstRule(path, syncListRuleEntry)) { // set the applicable flag wildcardRuleMatched = true; if (debugLogging) {addLogEntry("Evaluation against 'sync_list' rule result: globbing pattern match using segment matching", ["debug"]);} } } else { // wildcard (*) rule processing // create regex from 'syncListRuleEntry' auto allowedMask = regex(createRegexCompatiblePath(syncListRuleEntry)); if (matchAll(path, allowedMask)) { // set the applicable flag wildcardRuleMatched = true; if (debugLogging) {addLogEntry("Evaluation against 'sync_list' rule result: wildcard pattern match", ["debug"]);} } else { // matchAll no match ... try another way just to be sure if (matchPathAgainstRule(path, syncListRuleEntry)) { // set the applicable flag wildcardRuleMatched = true; if (debugLogging) {addLogEntry("Evaluation against 'sync_list' rule result: wildcard pattern match using segment matching", ["debug"]);} } } } // Was the rule matched? if (wildcardRuleMatched) { // Is this an exclude rule? if (thisIsAnExcludeRule) { // Yes exclude rule if (debugLogging) {addLogEntry("Evaluation against 'sync_list' rule result: wildcard|globbing rule matched and must be excluded", ["debug"]);} excludeWildcardMatched = true; exclude = true; finalResult = true; } else { // include rule if (debugLogging) {addLogEntry("Evaluation against 'sync_list' rule result: wildcard|globbing pattern matched and must be included", ["debug"]);} finalResult = false; excludeWildcardMatched = false; } } } } } // debug logging post 'sync_list' rule evaluations if (debugLogging) { // Rule evaluation complete addLogEntry("------------------------------------------------------------------------", ["debug"]); // Interim results after checking each 'sync_list' rule against the input path addLogEntry("[F]excludeExactMatch = " ~ to!string(excludeExactMatch), ["debug"]); addLogEntry("[F]excludeParentMatched = " ~ to!string(excludeParentMatched), ["debug"]); addLogEntry("[F]excludeAnywhereMatched = " ~ to!string(excludeAnywhereMatched), ["debug"]); addLogEntry("[F]excludeWildcardMatched = " ~ to!string(excludeWildcardMatched), ["debug"]); } // If any of these exclude match items is true, then finalResult has to be flagged as true if ((exclude) || (excludeExactMatch) || (excludeParentMatched) || (excludeAnywhereMatched) || (excludeWildcardMatched)) { finalResult = true; } // Final Result if (finalResult) { if (debugLogging) {addLogEntry("Evaluation against 'sync_list' final result: EXCLUDED as no rule included path", ["debug"]);} } else { if (debugLogging) {addLogEntry("Evaluation against 'sync_list' final result: included for sync", ["debug"]);} } if (debugLogging) {addLogEntry("******************* SYNC LIST RULES EVALUATION END *********************", ["debug"]);} return finalResult; } // Calculate wildcard character depth in path int firstWildcardDepth(string syncListRuleEntry) { int depth = 0; foreach (segment; pathSplitter(syncListRuleEntry)) { if (segment.canFind("*")) // Check for wildcard characters return depth; depth++; } return depth; // No wildcard found should be '0' } // Create a wildcard regex compatible string based on the sync list rule string createRegexCompatiblePath(string regexCompatiblePath) { regexCompatiblePath = regexCompatiblePath.replace(".", "\\."); // Escape the dot (.) if present regexCompatiblePath = regexCompatiblePath.replace(" ", "\\s"); // Escape spaces if present regexCompatiblePath = regexCompatiblePath.replace("*", ".*"); // Replace * with '.*' to be compatible with function and to match any characters return regexCompatiblePath; } // Create a regex compatible string to match a relevant segment bool matchSegment(string ruleSegment, string pathSegment) { ruleSegment = ruleSegment.replace("*", ".*"); // Replace * with '.*' to be compatible with function and to match any characters ruleSegment = ruleSegment.replace(" ", "\\s"); // Escape spaces if present auto pattern = regex("^" ~ ruleSegment ~ "$"); // Check if there's a match return !match(pathSegment, pattern).empty; } // Function to handle path matching when using globbing (**) bool matchPathAgainstRule(string path, string rule) { // Split both the path and rule into segments auto pathSegments = pathSplitter(path).filter!(s => !s.empty).array; auto ruleSegments = pathSplitter(rule).filter!(s => !s.empty).array; bool lastSegmentMatchesRule = false; size_t i = 0, j = 0; while (i < pathSegments.length && j < ruleSegments.length) { if (ruleSegments[j] == "**") { if (j == ruleSegments.length - 1) { return true; // '**' at the end matches everything } // Find next matching part after '**' while (i < pathSegments.length && !matchSegment(ruleSegments[j + 1], pathSegments[i])) { i++; } j++; // Move past the '**' in the rule } else { if (!matchSegment(ruleSegments[j], pathSegments[i])) { return false; } else { // increment to next set of values i++; j++; } } } // Ensure that we handle the last segments gracefully if (i >= pathSegments.length && j < ruleSegments.length) { if (j == ruleSegments.length - 1 && ruleSegments[j] == "*") { return true; } if (ruleSegments[j - 1] == pathSegments[i - 1]) { lastSegmentMatchesRule = true; } } return j == ruleSegments.length || (j == ruleSegments.length - 1 && ruleSegments[j] == "**") || lastSegmentMatchesRule; } bool exactMatchRuleSegmentsToPathSegments(string[] ruleSegments, string[] inputSegments) { if (debugLogging) {addLogEntry("Running exactMatchRuleSegmentsToPathSegments()", ["debug"]);} // If rule has more segments than input, or input has more segments than rule, no match is possible if ((ruleSegments.length > inputSegments.length) || ( inputSegments.length > ruleSegments.length)) { return false; } // Iterate over each segment and compare for (size_t i = 0; i < ruleSegments.length; ++i) { if (ruleSegments[i] != inputSegments[i]) { if (debugLogging) {addLogEntry("Mismatch at segment " ~ to!string(i) ~ ": Rule Segment = " ~ ruleSegments[i] ~ ", Input Segment = " ~ inputSegments[i], ["debug"]);} return false; // Return false if any segment doesn't match } } // If all segments match, return true if (debugLogging) {addLogEntry("All segments matched: Rule Segments = " ~ to!string(ruleSegments) ~ ", Input Segments = " ~ to!string(inputSegments), ["debug"]);} return true; } bool matchRuleSegmentsToPathSegments(string[] ruleSegments, string[] inputSegments) { if (debugLogging) {addLogEntry("Running matchRuleSegmentsToPathSegments()", ["debug"]);} // If rule has more segments than input, no match is possible if (ruleSegments.length > inputSegments.length) { return false; } // Compare segments up to the length of the rule path return equal(ruleSegments, inputSegments[0 .. ruleSegments.length]); } bool matchFirstSegmentToPathFirstSegment(string[] ruleSegments, string[] inputSegments) { if (debugLogging) {addLogEntry("Running matchFirstSegmentToPathFirstSegment()", ["debug"]);} // Check that both segments are not empty if (ruleSegments.length == 0 || inputSegments.length == 0) { return false; // Return false if either segment array is empty } // Compare the first segments only return equal(ruleSegments[0], inputSegments[0]); } }onedrive-2.5.5/src/config.d000066400000000000000000003503251476564400300155600ustar00rootroot00000000000000// What is this module called? module config; // What does this module require to function? import core.stdc.stdlib: EXIT_SUCCESS, EXIT_FAILURE, exit; import std.stdio; import std.process; import std.regex; import std.string; import std.algorithm.searching; import std.algorithm.sorting: sort; import std.file; import std.conv; import std.path; import std.getopt; import std.format; import std.ascii; import std.datetime; import std.exception; // What other modules that we have created do we need to import? import log; import util; class ApplicationConfig { // Application default values - these do not change // - Compile time regex immutable auto configRegex = ctRegex!(`^(\w+)\s*=\s*"(.*)"\s*$`); // - Default directory to store data immutable string defaultSyncDir = "~/OneDrive"; // - Default Directory Permissions immutable long defaultDirectoryPermissionMode = 700; // - Default File Permissions immutable long defaultFilePermissionMode = 600; // - Default types of files to skip // v2.0.x - 2.4.x: ~*|.~*|*.tmp // v2.5.x : ~*|.~*|*.tmp|*.swp|*.partial immutable string defaultSkipFile = "~*|.~*|*.tmp|*.swp|*.partial"; // - Default directories to skip (default is skip none) immutable string defaultSkipDir = ""; // - Default application logging directory immutable string defaultLogFileDir = "/var/log/onedrive"; // - Default configuration directory immutable string defaultConfigDirName = "~/.config/onedrive"; // - Default 'OneDrive Business Shared Files' Folder Name immutable string defaultBusinessSharedFilesDirectoryName = "Files Shared With Me"; // Microsoft Requirements // - Default Application ID (abraunegg) immutable string defaultApplicationId = "d50ca740-c83f-4d1b-b616-12c519384f0c"; // - Microsoft User Agent ISV Tag immutable string isvTag = "ISV"; // - Microsoft User Agent Company name immutable string companyName = "abraunegg"; // - Microsoft Application name as per Microsoft Azure application registration immutable string appTitle = "OneDrive Client for Linux"; // Comply with OneDrive traffic decoration requirements // https://docs.microsoft.com/en-us/sharepoint/dev/general-development/how-to-avoid-getting-throttled-or-blocked-in-sharepoint-online // - Identify as ISV and include Company Name, App Name separated by a pipe character and then adding Version number separated with a slash character immutable string defaultUserAgent = isvTag ~ "|" ~ companyName ~ "|" ~ appTitle ~ "/" ~ strip(import("version")); // HTTP Struct items, used for configuring HTTP() // Curl Timeout Handling // libcurl dns_cache_timeout timeout immutable int defaultDnsTimeout = 60; // in seconds // Connect timeout for HTTP|HTTPS connections // Controls CURLOPT_CONNECTTIMEOUT immutable int defaultConnectTimeout = 10; // in seconds // Default data timeout for HTTP operations // curl.d has a default of: _defaultDataTimeout = dur!"minutes"(2); immutable int defaultDataTimeout = 60; // in seconds // Maximum time any operation is allowed to take // This includes dns resolution, connecting, data transfer, etc. // Controls CURLOPT_TIMEOUT immutable int defaultOperationTimeout = 3600; // in seconds // Specify what IP protocol version should be used when communicating with OneDrive immutable int defaultIpProtocol = 0; // 0 = IPv4 + IPv6, 1 = IPv4 Only, 2 = IPv6 Only // Specify how many redirects should be allowed immutable int defaultMaxRedirects = 5; // Azure Active Directory & Graph Explorer Endpoints // - Global & Default immutable string globalAuthEndpoint = "https://login.microsoftonline.com"; immutable string globalGraphEndpoint = "https://graph.microsoft.com"; // - US Government L4 immutable string usl4AuthEndpoint = "https://login.microsoftonline.us"; immutable string usl4GraphEndpoint = "https://graph.microsoft.us"; // - US Government L5 immutable string usl5AuthEndpoint = "https://login.microsoftonline.us"; immutable string usl5GraphEndpoint = "https://dod-graph.microsoft.us"; // - Germany immutable string deAuthEndpoint = "https://login.microsoftonline.de"; immutable string deGraphEndpoint = "https://graph.microsoft.de"; // - China immutable string cnAuthEndpoint = "https://login.chinacloudapi.cn"; immutable string cnGraphEndpoint = "https://microsoftgraph.chinacloudapi.cn"; // Application Version immutable string applicationVersion = "onedrive " ~ strip(import("version")); // Application items that depend on application run-time environment, thus cannot be immutable // Public variables // Logging verbosity count long verbosityCount = 0; // Was the application just authorised - paste of response uri bool applicationAuthorizeResponseUri = false; // Store the refreshToken for use within the application const(char)[] refreshToken; // Store the current accessToken for use within the application const(char)[] accessToken; // Store the 'refresh_token' file path string refreshTokenFilePath = ""; // Store the accessTokenExpiration for use within the application SysTime accessTokenExpiration; // Store the 'session_upload.CRC32-HASH' file path string uploadSessionFilePath = ""; // API initialisation flags bool apiWasInitialised = false; bool syncEngineWasInitialised = false; // Important Account Details string accountType; string defaultDriveId; string defaultRootId; // Sync Operations bool fullScanTrueUpRequired = false; bool suppressLoggingOutput = false; // Number of concurrent threads when downloading and uploading data ulong defaultConcurrentThreads = 8; // All application run-time paths are formulated from this as a set of defaults // - What is the home path of the actual 'user' that is running the application string defaultHomePath = ""; // - What is the config path for the application. By default, this is ~/.config/onedrive but can be overridden by using --confdir string configDirName = defaultConfigDirName; // - In case we have to use a system config directory such as '/etc/onedrive' or similar, store that path in this variable private string systemConfigDirName = ""; // - Store the configured converted octal value for directory permissions private int configuredDirectoryPermissionMode; // - Store the configured converted octal value for file permissions private int configuredFilePermissionMode; // - Store the 'delta_link' file path private string deltaLinkFilePath = ""; // - Store the 'items.sqlite3' file path string databaseFilePath = ""; // - Store the 'items-dryrun.sqlite3' file path string databaseFilePathDryRun = ""; // - Store the user 'config' file path private string userConfigFilePath = ""; // - Store the system 'config' file path private string systemConfigFilePath = ""; // - What is the 'config' file path that will be used? private string applicableConfigFilePath = ""; // - Store the 'sync_list' file path string syncListFilePath = ""; // OneDrive Business Shared File handling - what directory will be used? string configuredBusinessSharedFilesDirectoryName = ""; // Hash files so that we can detect when the configuration has changed, in items that will require a --resync private string configHashFile = ""; private string configBackupFile = ""; private string syncListHashFile = ""; // Store the actual 'runtime' hash private string currentConfigHash = ""; private string currentSyncListHash = ""; // Store the previous config files hash values (file contents) private string previousConfigHash = ""; private string previousSyncListHash = ""; // Store items that come in from the 'config' file, otherwise these need to be set the defaults private string configFileSyncDir = defaultSyncDir; private string configFileSkipFile = ""; // Default for now, if post reading in any user configuration, if still empty, default will be used private string configFileSkipDir = ""; // Default here is no directories are skipped private string configFileDriveId = ""; // Default here is that no drive id is specified private bool configFileSkipDotfiles = false; private bool configFileSkipSymbolicLinks = false; private bool configFileSyncBusinessSharedItems = false; // File permission values (set via initialise function) private int convertedPermissionValue; // Array of values that are the actual application runtime configuration // The values stored in these array's are the actual application configuration which can then be accessed by getValue & setValue string[string] stringValues; long[string] longValues; bool[string] boolValues; bool shellEnvironmentSet = false; // GUI Notification Environment variables bool xdg_exists = false; bool dbus_exists = false; // Initialise the application configuration bool initialise(string confdirOption, bool helpRequested) { // Default runtime configuration - entries in config file ~/.config/onedrive/config or derived from variables above // An entry here means it can be set via the config file if there is a corresponding entry, read from config and set via update_from_args() // The below becomes the 'default' application configuration before config file and/or cli options are overlaid on top // - Set the required default values stringValues["application_id"] = defaultApplicationId; stringValues["log_dir"] = defaultLogFileDir; stringValues["skip_dir"] = defaultSkipDir; stringValues["skip_file"] = defaultSkipFile; stringValues["sync_dir"] = defaultSyncDir; stringValues["user_agent"] = defaultUserAgent; // - The 'drive_id' is used when we specify a specific OneDrive ID when attempting to sync Shared Folders and SharePoint items stringValues["drive_id"] = ""; // Support National Azure AD endpoints as per https://docs.microsoft.com/en-us/graph/deployments // By default, if empty, use standard Azure AD URL's // Will support the following options: // - USL4 // AD Endpoint: https://login.microsoftonline.us // Graph Endpoint: https://graph.microsoft.us // - USL5 // AD Endpoint: https://login.microsoftonline.us // Graph Endpoint: https://dod-graph.microsoft.us // - DE // AD Endpoint: https://portal.microsoftazure.de // Graph Endpoint: https://graph.microsoft.de // - CN // AD Endpoint: https://login.chinacloudapi.cn // Graph Endpoint: https://microsoftgraph.chinacloudapi.cn stringValues["azure_ad_endpoint"] = ""; // Support single-tenant applications that are not able to use the "common" multiplexer stringValues["azure_tenant_id"] = ""; // Support synchronising files based on user desire // - default = whatever order these came in as, processed essentially FIFO // - size_asc = file size ascending // - size_dsc = file size descending // - name_asc = file name ascending // - name_dsc = file name descending stringValues["transfer_order"] = "default"; // - Store how many times was --verbose added longValues["verbose"] = verbosityCount; // - The amount of time (seconds) between monitor sync loops longValues["monitor_interval"] = 300; // - What size of file should be skipped? longValues["skip_size"] = 0; // - How many 'loops' when using --monitor, before we print out high frequency recurring items? longValues["monitor_log_frequency"] = 12; // - Number of N sync runs before performing a full local scan of sync_dir // By default 12 which means every ~60 minutes a full disk scan of sync_dir will occur // 'monitor_interval' * 'monitor_fullscan_frequency' = 3600 = 1 hour longValues["monitor_fullscan_frequency"] = 12; // - Number of children in a path that is locally removed which will be classified as a 'big data delete' longValues["classify_as_big_delete"] = 1000; // - Configure the default folder permission attributes for newly created folders longValues["sync_dir_permissions"] = defaultDirectoryPermissionMode; // - Configure the default file permission attributes for newly created file longValues["sync_file_permissions"] = defaultFilePermissionMode; // - Configure download / upload rate limits longValues["rate_limit"] = 0; // - To ensure we do not fill up the load disk, how much disk space should be reserved by default longValues["space_reservation"] = 50 * 2^^20; // 50 MB as Bytes // HTTPS & CURL Operation Settings // - Maximum time an operation is allowed to take // This includes dns resolution, connecting, data transfer, etc - controls CURLOPT_TIMEOUT // CURLOPT_TIMEOUT: This option sets the maximum time in seconds that you allow the libcurl transfer operation to take. // This is useful for controlling how long a specific transfer should take before it is considered too slow and aborted. However, it does not directly control the keep-alive time of a socket. longValues["operation_timeout"] = defaultOperationTimeout; // libcurl dns_cache_timeout timeout longValues["dns_timeout"] = defaultDnsTimeout; // Timeout for HTTPS connections - controls CURLOPT_CONNECTTIMEOUT // CURLOPT_CONNECTTIMEOUT: This option sets the timeout, in seconds, for the connection phase. It is the maximum time allowed for the connection to be established. longValues["connect_timeout"] = defaultConnectTimeout; // Timeout for activity on a HTTPS connection longValues["data_timeout"] = defaultDataTimeout; // What IP protocol version should be used when communicating with OneDrive longValues["ip_protocol_version"] = defaultIpProtocol; // 0 = IPv4 + IPv6, 1 = IPv4 Only, 2 = IPv6 Only // What is the default age that a curl engine should be left idle for, before being destroyed longValues["max_curl_idle"] = 120; // Number of concurrent threads longValues["threads"] = defaultConcurrentThreads; // Default is 8, user can increase to max of 16 or decrease // - Do we wish to upload only? boolValues["upload_only"] = false; // - Do we need to check for the .nomount file on the mount point? boolValues["check_nomount"] = false; // - Do we need to check for the .nosync file anywhere? boolValues["check_nosync"] = false; // - Do we wish to download only? boolValues["download_only"] = false; // - Do we disable notifications? boolValues["disable_notifications"] = false; // - Do we bypass all the download validation? // This is critically important not to disable, but because of SharePoint 'feature' can be highly desirable to enable boolValues["disable_download_validation"] = false; // - Do we bypass all the upload validation? // This is critically important not to disable, but because of SharePoint 'feature' can be highly desirable to enable boolValues["disable_upload_validation"] = false; // - Do we enable logging? boolValues["enable_logging"] = false; // - Do we force HTTP 1.1 for connections to the OneDrive API // By default we use the curl library default, which should be HTTP2 for most operations governed by the OneDrive API boolValues["force_http_11"] = false; // - Do we treat the local file system as the source of truth for our data? boolValues["local_first"] = false; // - Do we ignore local file deletes, so that all files are retained online? boolValues["no_remote_delete"] = false; // - Do we skip symbolic links? boolValues["skip_symlinks"] = false; // - Do we enable debugging for all HTTPS flows. Critically important for debugging API issues. boolValues["debug_https"] = false; // - Do we skip .files and .folders? boolValues["skip_dotfiles"] = false; // - Do we perform a 'dry-run' with no local or remote changes actually being performed? boolValues["dry_run"] = false; // - Do we sync all the files in the 'sync_dir' root? boolValues["sync_root_files"] = false; // - Do we delete source after successful transfer? boolValues["remove_source_files"] = false; // - Do we perform strict matching for skip_dir? boolValues["skip_dir_strict_match"] = false; // - Do we perform a --resync? boolValues["resync"] = false; // - resync now needs to be acknowledged based on the 'risk' of using it boolValues["resync_auth"] = false; // - Ignore data safety checks and overwrite local data rather than preserve & rename // This is a config file option ONLY boolValues["bypass_data_preservation"] = false; // - Allow enable / disable of the syncing of OneDrive Business Shared items (files & folders) via configuration file boolValues["sync_business_shared_items"] = false; // - Log to application output running configuration values boolValues["display_running_config"] = false; // - Configure read-only authentication scope boolValues["read_only_auth_scope"] = false; // - Flag to cleanup local files when using --download-only boolValues["cleanup_local_files"] = false; // - Perform a permanentDelete on deletion activities boolValues["permanent_delete"] = false; // - Controls how the application handles the Microsoft SharePoint 'feature' of modifying all PDF, MS Office & HTML files with added XML content post upload // There are 2 ways to solve this: // 1. Download the modified file immediately after upload as per v2.4.x (default) // 2. Create a new online version of the file, which then contributes to the users 'quota' boolValues["create_new_file_version"] = false; // Webhook Feature Options boolValues["webhook_enabled"] = false; stringValues["webhook_public_url"] = ""; stringValues["webhook_listening_host"] = ""; longValues["webhook_listening_port"] = 8888; longValues["webhook_expiration_interval"] = 600; longValues["webhook_renewal_interval"] = 300; longValues["webhook_retry_interval"] = 60; // GUI File Transfer and Deletion Notifications boolValues["notify_file_actions"] = false; // Display file transfer metrics // - Enable the calculation of transfer metrics (duration,speed) for the transfer of a file boolValues["display_transfer_metrics"] = false; // Enable writing extended attributes about a file to xattr values // - file creator // - file last modifier boolValues["write_xattr_data"] = false; // Diable setting the permissions for directories and files, using the inherited permissions boolValues["disable_permission_set"] = false; // EXPAND USERS HOME DIRECTORY // Determine the users home directory. // Need to avoid using ~ here as expandTilde() below does not interpret correctly when running under init.d or systemd scripts // Check for HOME environment variable if (environment.get("HOME") != ""){ // Use HOME environment variable if (debugLogging) {addLogEntry("runtime_environment: HOME environment variable detected, expansion of '~' should be possible", ["debug"]);} defaultHomePath = environment.get("HOME"); shellEnvironmentSet = true; } else { if ((environment.get("SHELL") == "") && (environment.get("USER") == "")){ // No shell is set or username - observed case when running as systemd service under CentOS 7.x if (debugLogging) {addLogEntry("runtime_environment: No HOME, SHELL or USER environment variable configuration detected. Expansion of '~' not possible", ["debug"]);} defaultHomePath = "/root"; shellEnvironmentSet = false; } else { // A shell & valid user is set, but no HOME is set, use ~ which can be expanded if (debugLogging) {addLogEntry("runtime_environment: SHELL and USER environment variable detected, expansion of '~' should be possible", ["debug"]);} defaultHomePath = "~"; shellEnvironmentSet = true; } } // outcome of setting defaultHomePath if (debugLogging) {addLogEntry("runtime_environment: Calculated defaultHomePath: " ~ defaultHomePath, ["debug"]);} // DEVELOPER OPTIONS // display_memory = true | false // - It may be desirable to display the memory usage of the application to assist with diagnosing memory issues with the application // - This is especially beneficial when debugging or performing memory tests with Valgrind boolValues["display_memory"] = false; // monitor_max_loop = long value // - It may be desirable to, when running in monitor mode, force monitor mode to 'quit' after X number of loops // - This is especially beneficial when debugging or performing memory tests with Valgrind longValues["monitor_max_loop"] = 0; // display_sync_options = true | false // - It may be desirable to see what options are being passed into performSync() without enabling the full verbose debug logging boolValues["display_sync_options"] = false; // force_children_scan = true | false // - Force client to use /children rather than /delta to query changes on OneDrive // - This option flags nationalCloudDeployment as true, forcing the client to act like it is using a National Cloud Deployment model boolValues["force_children_scan"] = false; // display_processing_time = true | false // - Enabling this option will add function processing times to the console output // - This then enables tracking of where the application is spending most amount of time when processing data when users have questions re performance boolValues["display_processing_time"] = false; // Function variables string configDirBase; string systemConfigDirBase = "/etc"; bool configurationInitialised = false; // Initialise the application configuration, using the provided --confdir option was passed in if (!confdirOption.empty) { // A CLI 'confdir' was passed in // Clean up any stray " .. these should not be there for correct process handling of the configuration option confdirOption = strip(confdirOption,"\""); if (debugLogging) {addLogEntry("configDirName: CLI override to set configDirName to: " ~ confdirOption, ["debug"]);} // For the passed in --confdir option .. if (canFind(confdirOption,"~")) { // A ~ was found if (debugLogging) {addLogEntry("configDirName: A '~' was found in configDirName, using the calculated 'defaultHomePath' to replace '~'", ["debug"]);} configDirName = defaultHomePath ~ strip(confdirOption,"~","~"); } else { configDirName = confdirOption; } } else { // Determine the base directory relative to which user specific configuration files should be stored if (environment.get("XDG_CONFIG_HOME") != ""){ if (debugLogging) {addLogEntry("configDirBase: XDG_CONFIG_HOME environment variable set", ["debug"]);} configDirBase = environment.get("XDG_CONFIG_HOME"); } else { // XDG_CONFIG_HOME does not exist on systems where X11 is not present - ie - headless systems / servers if (debugLogging) {addLogEntry("configDirBase: WARNING - no XDG_CONFIG_HOME environment variable set", ["debug"]);} configDirBase = buildNormalizedPath(buildPath(defaultHomePath, ".config")); } // Output configDirBase calculation if (debugLogging) { addLogEntry("configDirBase: " ~ configDirBase, ["debug"]); // Set the calculated application configuration directory addLogEntry("configDirName: Configuring application to use calculated config path", ["debug"]); } // configDirBase contains the correct path so we do not need to check for presence of '~' configDirName = buildNormalizedPath(buildPath(configDirBase, "onedrive")); } // systemConfigDirBase contains the correct path, build the correct path for the system config file systemConfigDirName = buildNormalizedPath(buildPath(systemConfigDirBase, "onedrive")); // Configuration directory should now have been correctly identified if (!exists(configDirName)) { // create the directory mkdirRecurse(configDirName); // Configure the applicable permissions for the folder configDirName.setAttributes(returnRequiredDirectoryPermissions()); } else { // The config path exists // The path that exists must be a directory, not a file if (!isDir(configDirName)) { if (!confdirOption.empty) { // the configuration path was passed in by the user .. user error addLogEntry("ERROR: --confdir entered value is an existing file instead of an existing directory"); } else { // other error addLogEntry("ERROR: " ~ confdirOption ~ " is a file rather than a directory"); } // Must exit exit(EXIT_FAILURE); } } // Update application set variables based on configDirName // - What is the full path for the 'refresh_token' refreshTokenFilePath = buildNormalizedPath(buildPath(configDirName, "refresh_token")); // - What is the full path for the 'delta_link' deltaLinkFilePath = buildNormalizedPath(buildPath(configDirName, "delta_link")); // - What is the full path for the 'items.sqlite3' - the database cache file databaseFilePath = buildNormalizedPath(buildPath(configDirName, "items.sqlite3")); // - What is the full path for the 'items-dryrun.sqlite3' - the dry-run database cache file databaseFilePathDryRun = buildNormalizedPath(buildPath(configDirName, "items-dryrun.sqlite3")); // - What is the full path for the 'resume_upload' uploadSessionFilePath = buildNormalizedPath(buildPath(configDirName, "session_upload")); // - What is the full path for the 'sync_list' file syncListFilePath = buildNormalizedPath(buildPath(configDirName, "sync_list")); // - What is the full path for the 'config' - the user file to configure the application userConfigFilePath = buildNormalizedPath(buildPath(configDirName, "config")); // - What is the full path for the system 'config' file if it is required systemConfigFilePath = buildNormalizedPath(buildPath(systemConfigDirName, "config")); // To determine if any configuration items has changed, where a --resync would be required, we need to have a hash file for the following items // - 'config.backup' file // - applicable 'config' file // - 'sync_list' file // - 'business_shared_items' file configBackupFile = buildNormalizedPath(buildPath(configDirName, ".config.backup")); configHashFile = buildNormalizedPath(buildPath(configDirName, ".config.hash")); syncListHashFile = buildNormalizedPath(buildPath(configDirName, ".sync_list.hash")); // Debug Output for application set variables based on configDirName if (debugLogging) { addLogEntry("refreshTokenFilePath = " ~ refreshTokenFilePath, ["debug"]); addLogEntry("deltaLinkFilePath = " ~ deltaLinkFilePath, ["debug"]); addLogEntry("databaseFilePath = " ~ databaseFilePath, ["debug"]); addLogEntry("databaseFilePathDryRun = " ~ databaseFilePathDryRun, ["debug"]); addLogEntry("uploadSessionFilePath = " ~ uploadSessionFilePath, ["debug"]); addLogEntry("userConfigFilePath = " ~ userConfigFilePath, ["debug"]); addLogEntry("syncListFilePath = " ~ syncListFilePath, ["debug"]); addLogEntry("systemConfigFilePath = " ~ systemConfigFilePath, ["debug"]); addLogEntry("configBackupFile = " ~ configBackupFile, ["debug"]); addLogEntry("configHashFile = " ~ configHashFile, ["debug"]); addLogEntry("syncListHashFile = " ~ syncListHashFile, ["debug"]); } // Configure the Hash and Backup File Permission Value string valueToConvert = to!string(defaultFilePermissionMode); auto convertedValue = parse!long(valueToConvert, 8); convertedPermissionValue = to!int(convertedValue); // Do not try and load any user configuration file if --help was used if (helpRequested) { return true; } else { // Initialise the application using the configuration file if it exists if (!exists(userConfigFilePath)) { // 'user' configuration file does not exist .. but did the user specify a custom configuration directory via --confdir ? if (confdirOption.empty) { // No --confdir entry // Is there a system configuration file? if (!exists(systemConfigFilePath)) { // 'system' configuration file does not exist if (verboseLogging) {addLogEntry("No user or system config file found, using application defaults", ["verbose"]);} applicableConfigFilePath = userConfigFilePath; configurationInitialised = true; } else { // 'system' configuration file exists // can we load the configuration file without error? if (loadConfigFile(systemConfigFilePath)) { // configuration file loaded without error addLogEntry("System configuration file successfully loaded"); // Set 'applicableConfigFilePath' to equal the 'config' we loaded applicableConfigFilePath = systemConfigFilePath; // Update the configHashFile path value to ensure we are using the system 'config' file for the hash configHashFile = buildNormalizedPath(buildPath(systemConfigDirName, ".config.hash")); configurationInitialised = true; } else { // there was a problem loading the configuration file addLogEntry(); // used instead of an empty 'writeln();' to ensure the line break is correct in the buffered console output ordering addLogEntry("System configuration file has errors - please check your configuration"); } } } else { // Set 'applicableConfigFilePath' to equal the 'config' path specified via --confdir applicableConfigFilePath = userConfigFilePath; configurationInitialised = true; } } else { // 'user' configuration file exists in the specified path // can we load the configuration file without error? if (loadConfigFile(userConfigFilePath)) { // configuration file loaded without error addLogEntry("Configuration file successfully loaded"); // Set 'applicableConfigFilePath' to equal the 'config' we loaded applicableConfigFilePath = userConfigFilePath; configurationInitialised = true; } else { // there was a problem loading the configuration file addLogEntry(); // used instead of an empty 'writeln();' to ensure the line break is correct in the buffered console output ordering addLogEntry("Configuration file has errors - please check your configuration"); } } // Advise the user path that we will use for the application state data if (canFind(applicableConfigFilePath, configDirName)) { if (verboseLogging) {addLogEntry("Using 'user' configuration path for application config and state data: " ~ configDirName, ["verbose"]);} } else { if (canFind(applicableConfigFilePath, systemConfigDirName)) { if (verboseLogging) { addLogEntry("Using 'system' configuration path for application config data: " ~ systemConfigDirName, ["verbose"]); addLogEntry("Using 'user' configuration path for application state data: " ~ configDirName, ["verbose"]); } } } } // If the 'user' did not specify any 'skip_file' configuration in any loaded configuration file, load the defaults for 'skip_file' if (configFileSkipFile.empty) { configFileSkipFile = defaultSkipFile; } // return if the configuration was initialised return configurationInitialised; } // Create a backup of the 'config' file if it does not exist void createBackupConfigFile() { if (!getValueBool("dry_run")) { // Is there a backup of the config file if the config file exists? if (exists(applicableConfigFilePath)) { if (debugLogging) {addLogEntry("Creating a backup of the applicable config file", ["debug"]);} // create backup copy of current config file try { std.file.copy(applicableConfigFilePath, configBackupFile); // File Copy should only be readable by the user who created it - 0600 permissions needed configBackupFile.setAttributes(convertedPermissionValue); } catch (FileException e) { // filesystem error displayFileSystemErrorMessage(e.msg, getFunctionName!({})); } } } else { // --dry-run scenario ... technically we should not be making any local file changes ....... addLogEntry("DRY RUN: Not creating backup config file as --dry-run has been used"); } } // Return a given string value based on the provided key string getValueString(string key) { auto p = key in stringValues; if (p) { return *p; } else { throw new Exception("Missing config value: " ~ key); } } // Return a given long value based on the provided key long getValueLong(string key) { auto p = key in longValues; if (p) { return *p; } else { throw new Exception("Missing config value: " ~ key); } } // Return a given bool value based on the provided key bool getValueBool(string key) { auto p = key in boolValues; if (p) { return *p; } else { throw new Exception("Missing config value: " ~ key); } } // Set a given string value based on the provided key void setValueString(string key, string value) { stringValues[key] = value; } // Set a given long value based on the provided key void setValueLong(string key, long value) { longValues[key] = value; } // Set a given long value based on the provided key void setValueBool(string key, bool value) { boolValues[key] = value; } // Configure the directory octal permission value void configureRequiredDirectoryPermissions() { // return the directory permission mode required // - return octal!defaultDirectoryPermissionMode; ... cant be used .. which is odd // Error: variable defaultDirectoryPermissionMode cannot be read at compile time if (getValueLong("sync_dir_permissions") != defaultDirectoryPermissionMode) { // return user configured permissions as octal integer string valueToConvert = to!string(getValueLong("sync_dir_permissions")); auto convertedValue = parse!long(valueToConvert, 8); configuredDirectoryPermissionMode = to!int(convertedValue); } else { // return default as octal integer string valueToConvert = to!string(defaultDirectoryPermissionMode); auto convertedValue = parse!long(valueToConvert, 8); configuredDirectoryPermissionMode = to!int(convertedValue); } } // Configure the file octal permission value void configureRequiredFilePermissions() { // return the file permission mode required // - return octal!defaultFilePermissionMode; ... cant be used .. which is odd // Error: variable defaultFilePermissionMode cannot be read at compile time if (getValueLong("sync_file_permissions") != defaultFilePermissionMode) { // return user configured permissions as octal integer string valueToConvert = to!string(getValueLong("sync_file_permissions")); auto convertedValue = parse!long(valueToConvert, 8); configuredFilePermissionMode = to!int(convertedValue); } else { // return default as octal integer string valueToConvert = to!string(defaultFilePermissionMode); auto convertedValue = parse!long(valueToConvert, 8); configuredFilePermissionMode = to!int(convertedValue); } } // Read the configuredDirectoryPermissionMode and return int returnRequiredDirectoryPermissions() { if (configuredDirectoryPermissionMode == 0) { // the configured value is zero, this means that directories would get // values of d--------- configureRequiredDirectoryPermissions(); } return configuredDirectoryPermissionMode; } // Read the configuredFilePermissionMode and return int returnRequiredFilePermissions() { if (configuredFilePermissionMode == 0) { // the configured value is zero configureRequiredFilePermissions(); } return configuredFilePermissionMode; } // Load a configuration file from the provided filename private bool loadConfigFile(string filename) { try { addLogEntry("Reading configuration file: " ~ filename); readText(filename); } catch (std.file.FileException e) { addLogEntry("ERROR: Unable to access " ~ e.msg); return false; } auto file = File(filename, "r"); string lineBuffer; scope(exit) { file.close(); object.destroy(file); object.destroy(lineBuffer); } scope(failure) { file.close(); object.destroy(file); object.destroy(lineBuffer); } foreach (line; file.byLine()) { lineBuffer = stripLeft(line).to!string; if (lineBuffer.empty || lineBuffer[0] == ';' || lineBuffer[0] == '#') continue; auto c = lineBuffer.matchFirst(configRegex); if (c.empty) { addLogEntry("Malformed config line: " ~ lineBuffer); addLogEntry(); addLogEntry("Please review the documentation on how to correctly configure this application."); forceExit(); } c.popFront(); // skip the whole match string key = c.front.dup; c.popFront(); // Handle deprecated keys switch (key) { case "min_notify_changes": case "force_http_2": addLogEntry("The option '" ~ key ~ "' has been deprecated and will be ignored. Please read the updated documentation and update your client configuration to remove this option."); continue; case "sync_business_shared_folders": addLogEntry(); addLogEntry("The option 'sync_business_shared_folders' has been deprecated and the process for synchronising Microsoft OneDrive Business Shared Folders has changed."); addLogEntry("Please review the revised documentation on how to correctly configure this application feature."); addLogEntry("You must update your client configuration and make changes to your local filesystem and online data to use this capability."); return false; default: break; } // Process other keys if (key in boolValues) { // Only accept "true" as true value. setValueBool(key, c.front.dup == "true" ? true : false); if (key == "skip_dotfiles") configFileSkipDotfiles = true; if (key == "skip_symlinks") configFileSkipSymbolicLinks = true; if (key == "sync_business_shared_items") configFileSyncBusinessSharedItems = true; } else if (key in stringValues) { string value = c.front.dup; setValueString(key, value); if (key == "sync_dir") { if (!strip(value).empty) { configFileSyncDir = value; } else { addLogEntry(); addLogEntry("Invalid value for key in config file: " ~ key); addLogEntry("ERROR: sync_dir in config file cannot be empty - this is a fatal error and must be corrected"); addLogEntry(); forceExit(); } } else if (key == "skip_file") { // Handle multiple 'config' file entries of skip_file if (configFileSkipFile.empty) { // currently no entry exists configFileSkipFile = c.front.dup; } else { // add to existing entry configFileSkipFile = configFileSkipFile ~ "|" ~ to!string(c.front.dup); setValueString("skip_file", configFileSkipFile); } } else if (key == "skip_dir") { // Handle multiple entries of skip_dir if (configFileSkipDir.empty) { // currently no entry exists configFileSkipDir = c.front.dup; } else { // add to existing entry configFileSkipDir = configFileSkipDir ~ "|" ~ to!string(c.front.dup); setValueString("skip_dir", configFileSkipDir); } } else if (key == "single_directory") { // --single-directory Strip quotation marks from path // This is an issue when using ONEDRIVE_SINGLE_DIRECTORY with Docker string configFileSingleDirectory = strip(to!string(c.front.dup), "\""); setValueString("single_directory", configFileSingleDirectory); } else if (key == "azure_ad_endpoint") { switch (value) { case "": addLogEntry("Using default config option for Global Azure AD Endpoints"); break; case "USL4": addLogEntry("Using config option for Azure AD for US Government Endpoints"); break; case "USL5": addLogEntry("Using config option for Azure AD for US Government Endpoints (DOD)"); break; case "DE": addLogEntry("Using config option for Azure AD Germany"); break; case "CN": addLogEntry("Using config option for Azure AD China operated by VNET"); break; default: addLogEntry("Unknown Azure AD Endpoint - using Global Azure AD Endpoints"); } } else if (key == "transfer_order") { switch (value) { case "size_asc": addLogEntry("Files will be transferred sorted by ascending size (smallest first)"); break; case "size_dsc": addLogEntry("Files will be transferred sorted by descending size (largest first)"); break; case "name_asc": addLogEntry("Files will be transferred sorted by ascending name (A -> Z)"); break; case "name_dsc": addLogEntry("Files will be transferred sorted by descending name (Z -> A)"); break; default: addLogEntry("Files will be transferred in original order that they were received (FIFO)"); } } else if (key == "application_id") { string tempApplicationId = strip(value); if (tempApplicationId.empty) { addLogEntry("Invalid value for key in config file - using default value: " ~ key); if (debugLogging) {addLogEntry("application_id in config file cannot be empty - using default application_id", ["debug"]);} setValueString("application_id", defaultApplicationId); } } else if (key == "drive_id") { string tempDriveId = strip(value); if (tempDriveId.empty) { addLogEntry(); addLogEntry("Invalid value for key in config file: " ~ key); if (debugLogging) {addLogEntry("drive_id in config file cannot be empty - this is a fatal error and must be corrected by removing this entry from your config file.", ["debug"]);} addLogEntry(); forceExit(); } else { configFileDriveId = tempDriveId; } } else if (key == "log_dir") { string tempLogDir = strip(value); if (tempLogDir.empty) { addLogEntry("Invalid value for key in config file - using default value: " ~ key); if (debugLogging) {addLogEntry("log_dir in config file cannot be empty - using default log_dir", ["debug"]);} setValueString("log_dir", defaultLogFileDir); } } } else if (key in longValues) { ulong thisConfigValue; try { thisConfigValue = to!ulong(c.front.dup); } catch (std.conv.ConvException) { addLogEntry("Invalid value for key in config file: " ~ key); return false; } setValueLong(key, thisConfigValue); if (key == "monitor_interval") { // if key is 'monitor_interval' the value must be 300 or greater ulong tempValue = thisConfigValue; // the temp value needs to be 300 or greater if (tempValue < 300) { addLogEntry("Invalid value for key in config file - using default value: " ~ key); tempValue = 300; } setValueLong("monitor_interval", tempValue); } else if (key == "monitor_fullscan_frequency") { // if key is 'monitor_fullscan_frequency' the value must be 12 or greater ulong tempValue = thisConfigValue; // the temp value needs to be 12 or greater if (tempValue < 12) { // If this is not set to zero (0) then we are not disabling 'monitor_fullscan_frequency' if (tempValue != 0) { // invalid value addLogEntry("Invalid value for key in config file - using default value: " ~ key); tempValue = 12; } } setValueLong("monitor_fullscan_frequency", tempValue); } else if (key == "space_reservation") { // if key is 'space_reservation' we have to calculate MB -> bytes ulong tempValue = thisConfigValue; // a value of 0 needs to be made at least 1MB .. if (tempValue == 0) { addLogEntry("Invalid value for key in config file - using 1MB: " ~ key); tempValue = 1; } setValueLong("space_reservation", tempValue * 2^^20); } else if (key == "ip_protocol_version") { ulong tempValue = thisConfigValue; if (tempValue > 2) { addLogEntry("Invalid value for key in config file - using default value: " ~ key); tempValue = defaultIpProtocol; } setValueLong("ip_protocol_version", tempValue); } else if (key == "threads") { ulong tempValue = thisConfigValue; if (tempValue > 16) { addLogEntry("Invalid value for key in config file - using default value: " ~ key); tempValue = defaultConcurrentThreads; } setValueLong("threads", tempValue); } } else { addLogEntry("Unknown key in config file: " ~ key); return false; } } // Return that we were able to read in the config file and parse the options without issue return true; } // Update the application configuration based on CLI passed in parameters void updateFromArgs(string[] cliArgs) { // Add additional CLI options that are NOT configurable via config file stringValues["create_directory"] = ""; stringValues["create_share_link"] = ""; stringValues["destination_directory"] = ""; stringValues["get_file_link"] = ""; stringValues["modified_by"] = ""; stringValues["sharepoint_library_name"] = ""; stringValues["remove_directory"] = ""; stringValues["single_directory"] = ""; stringValues["source_directory"] = ""; stringValues["auth_files"] = ""; stringValues["auth_response"] = ""; stringValues["share_password"] = ""; boolValues["display_config"] = false; boolValues["display_sync_status"] = false; boolValues["display_quota"] = false; boolValues["print_token"] = false; boolValues["logout"] = false; boolValues["reauth"] = false; boolValues["monitor"] = false; boolValues["synchronize"] = false; boolValues["force"] = false; boolValues["list_business_shared_items"] = false; boolValues["sync_business_shared_files"] = false; boolValues["force_sync"] = false; boolValues["with_editing_perms"] = false; // Specific options for CLI input handling stringValues["sync_dir_cli"] = ""; // Application Startup option validation try { string tmpStr; bool tmpBol; long tmpVerb; // duplicated from main.d to get full help output! auto opt = getopt( cliArgs, std.getopt.config.bundling, std.getopt.config.caseSensitive, "auth-files", "Perform authentication not via interactive dialog but via files read/writes to these files.", &stringValues["auth_files"], "auth-response", "Perform authentication not via interactive dialog but via providing the response url directly.", &stringValues["auth_response"], "check-for-nomount", "Check for the presence of .nosync in the syncdir root. If found, do not perform sync.", &boolValues["check_nomount"], "check-for-nosync", "Check for the presence of .nosync in each directory. If found, skip directory from sync.", &boolValues["check_nosync"], "classify-as-big-delete", "Number of children in a path that is locally removed which will be classified as a 'big data delete'", &longValues["classify_as_big_delete"], "cleanup-local-files", "Cleanup additional local files when using --download-only. This will remove local data.", &boolValues["cleanup_local_files"], "create-directory", "Create a directory on OneDrive - no sync will be performed.", &stringValues["create_directory"], "create-share-link", "Create a shareable link for an existing file on OneDrive", &stringValues["create_share_link"], "debug-https", "Debug OneDrive HTTPS communication.", &boolValues["debug_https"], "destination-directory", "Destination directory for renamed or move on OneDrive - no sync will be performed.", &stringValues["destination_directory"], "disable-notifications", "Do not use desktop notifications in monitor mode.", &boolValues["disable_notifications"], "disable-download-validation", "Disable download validation when downloading from OneDrive", &boolValues["disable_download_validation"], "disable-upload-validation", "Disable upload validation when uploading to OneDrive", &boolValues["disable_upload_validation"], "display-config", "Display what options the client will use as currently configured - no sync will be performed.", &boolValues["display_config"], "display-running-config", "Display what options the client has been configured to use on application startup.", &boolValues["display_running_config"], "display-sync-status", "Display the sync status of the client - no sync will be performed.", &boolValues["display_sync_status"], "display-quota", "Display the quota status of the client - no sync will be performed.", &boolValues["display_quota"], "download-only", "Replicate the OneDrive online state locally, by only downloading changes from OneDrive. Do not upload local changes to OneDrive.", &boolValues["download_only"], "dry-run", "Perform a trial sync with no changes made", &boolValues["dry_run"], "enable-logging", "Enable client activity to a separate log file", &boolValues["enable_logging"], "force-http-11", "Force the use of HTTP 1.1 for all operations", &boolValues["force_http_11"], "force", "Force the deletion of data when a 'big delete' is detected", &boolValues["force"], "force-sync", "Force a synchronization of a specific folder, only when using --sync --single-directory and ignore all non-default skip_dir and skip_file rules", &boolValues["force_sync"], "get-file-link", "Display the file link of a synced file", &stringValues["get_file_link"], "get-sharepoint-drive-id", "Query and return the Office 365 Drive ID for a given Office 365 SharePoint Shared Library", &stringValues["sharepoint_library_name"], "get-O365-drive-id", "Query and return the Office 365 Drive ID for a given Office 365 SharePoint Shared Library (DEPRECATED)", &stringValues["sharepoint_library_name"], "list-shared-items", "List OneDrive Business Shared Items", &boolValues["list_business_shared_items"], "sync-shared-files", "Sync OneDrive Business Shared Files to the local filesystem", &boolValues["sync_business_shared_files"], "local-first", "Synchronize from the local directory source first, before downloading changes from OneDrive.", &boolValues["local_first"], "log-dir", "Directory where logging output is saved to, needs to end with a slash.", &stringValues["log_dir"], "logout", "Logout the current user", &boolValues["logout"], "modified-by", "Display the last modified by details of a given path", &stringValues["modified_by"], "monitor|m", "Keep monitoring for local and remote changes", &boolValues["monitor"], "monitor-interval", "Number of seconds by which each sync operation is undertaken when idle under monitor mode.", &longValues["monitor_interval"], "monitor-fullscan-frequency", "Number of sync runs before performing a full local scan of the synced directory", &longValues["monitor_fullscan_frequency"], "monitor-log-frequency", "Frequency of logging in monitor mode", &longValues["monitor_log_frequency"], "no-remote-delete", "Do not delete local file 'deletes' from OneDrive when using --upload-only", &boolValues["no_remote_delete"], "print-access-token", "Print the access token, useful for debugging", &boolValues["print_token"], "reauth", "Reauthenticate the client with OneDrive", &boolValues["reauth"], "resync", "Forget the last saved state, perform a full sync", &boolValues["resync"], "resync-auth", "Approve the use of performing a --resync action", &boolValues["resync_auth"], "remove-directory", "Remove a directory on OneDrive - no sync will be performed.", &stringValues["remove_directory"], "remove-source-files", "Remove source file after successful transfer to OneDrive when using --upload-only", &boolValues["remove_source_files"], "single-directory", "Specify a single local directory within the OneDrive root to sync.", &stringValues["single_directory"], "skip-dot-files", "Skip dot files and folders from syncing", &boolValues["skip_dotfiles"], "skip-file", "Skip any files that match this pattern from syncing", &stringValues["skip_file"], "skip-dir", "Skip any directories that match this pattern from syncing", &stringValues["skip_dir"], "skip-size", "Skip new files larger than this size (in MB)", &longValues["skip_size"], "skip-dir-strict-match", "When matching skip_dir directories, only match explicit matches", &boolValues["skip_dir_strict_match"], "skip-symlinks", "Skip syncing of symlinks", &boolValues["skip_symlinks"], "source-directory", "Source directory to rename or move on OneDrive - no sync will be performed.", &stringValues["source_directory"], "space-reservation", "The amount of disk space to reserve (in MB) to avoid 100% disk space utilisation", &longValues["space_reservation"], "syncdir", "Specify the local directory used for synchronisation to OneDrive", &stringValues["sync_dir_cli"], "share-password", "Require a password to access the shared link when used with --create-share-link ", &stringValues["share_password"], "sync|s", "Perform a synchronisation with Microsoft OneDrive", &boolValues["synchronize"], "synchronize", "Perform a synchronisation with Microsoft OneDrive (DEPRECATED)", &boolValues["synchronize"], "sync-root-files", "Sync all files in sync_dir root when using sync_list.", &boolValues["sync_root_files"], "upload-only", "Replicate the locally configured sync_dir state to OneDrive, by only uploading local changes to OneDrive. Do not download changes from OneDrive.", &boolValues["upload_only"], "confdir", "Set the directory used to store the configuration files", &tmpStr, "verbose|v+", "Print more details, useful for debugging (repeat for extra debugging)", &tmpVerb, "version", "Print the version and exit", &tmpBol, "with-editing-perms", "Create a read-write shareable link for an existing file on OneDrive when used with --create-share-link ", &boolValues["with_editing_perms"] ); // Was --syncdir used? if (!getValueString("sync_dir_cli").empty) { // Build the line we need to update and/or write out string newConfigOptionSyncDirLine = "sync_dir = \"" ~ getValueString("sync_dir_cli") ~ "\""; // Does a 'config' file exist? if (!exists(applicableConfigFilePath)) { // No existing 'config' file exists, create it, and write the 'sync_dir' configuration to it if (!getValueBool("dry_run")) { std.file.write(applicableConfigFilePath, newConfigOptionSyncDirLine); // Config file should only be readable by the user who created it - 0600 permissions needed applicableConfigFilePath.setAttributes(convertedPermissionValue); } } else { // an existing config file exists .. so this now becomes tricky // string replace 'sync_dir' if it exists, in the existing 'config' file, but only if 'sync_dir' (already read in) is different from 'sync_dir_cli' if ( (getValueString("sync_dir")) != (getValueString("sync_dir_cli")) ) { // values are different File applicableConfigFilePathFileHandle = File(applicableConfigFilePath, "r"); string lineBuffer; string[] newConfigFileEntries; // read applicableConfigFilePath line by line auto range = applicableConfigFilePathFileHandle.byLine(); // for each 'config' file line foreach (line; range) { lineBuffer = stripLeft(line).to!string; if (lineBuffer.length == 0 || lineBuffer[0] == ';' || lineBuffer[0] == '#') { newConfigFileEntries ~= [lineBuffer]; } else { auto c = lineBuffer.matchFirst(configRegex); if (!c.empty) { c.popFront(); // skip the whole match string key = c.front.dup; if (key == "sync_dir") { // lineBuffer is the line we want to keep newConfigFileEntries ~= [newConfigOptionSyncDirLine]; } else { newConfigFileEntries ~= [lineBuffer]; } } } } // close original 'config' file if still open if (applicableConfigFilePathFileHandle.isOpen()) { // close open file applicableConfigFilePathFileHandle.close(); } // free memory from file open object.destroy(applicableConfigFilePathFileHandle); // Update the existing item in the file line array if (!getValueBool("dry_run")) { // Open the file with write access using 'w' mode to overwrite existing content File applicableConfigFilePathFileHandleWrite = File(applicableConfigFilePath, "w"); // Write each line from the 'newConfigFileEntries' array to the file foreach (line; newConfigFileEntries) { applicableConfigFilePathFileHandleWrite.writeln(line); } // Is this a running as a container if (entrypointExists) { // write this to the config file so that when config options are checked again, this matches on next run applicableConfigFilePathFileHandleWrite.writeln(newConfigOptionSyncDirLine); } // Flush and close the file handle to ensure all data is written if (applicableConfigFilePathFileHandleWrite.isOpen()) { applicableConfigFilePathFileHandleWrite.flush(); applicableConfigFilePathFileHandleWrite.close(); } // free memory from file open object.destroy(applicableConfigFilePathFileHandleWrite); } } } // Final - configure sync_dir with the value of sync_dir_cli so that it can be used as part of the application configuration and detect change setValueString("sync_dir", getValueString("sync_dir_cli")); } // was --monitor-interval used and now set to a value below minimum requirement? if (getValueLong("monitor_interval") < 300 ) { addLogEntry("Invalid value for --monitor-interval - using default value: 300"); setValueLong("monitor_interval", 300); } // Was --auth-files used? if (!getValueString("auth_files").empty) { // --auth-files used, need to validate that '~' was not used as a path identifier, and if yes, perform the correct expansion string[] tempAuthFiles = getValueString("auth_files").split(":"); string tempAuthUrl = tempAuthFiles[0]; string tempResponseUrl = tempAuthFiles[1]; string newAuthFilesString; // shell expansion if required if (!shellEnvironmentSet){ // No shell environment is set, no automatic expansion of '~' if present is possible // Does the 'currently configured' tempAuthUrl include a ~ if (canFind(tempAuthUrl, "~")) { // A ~ was found in auth_files(authURL) if (debugLogging) {addLogEntry("auth_files: A '~' was found in 'auth_files(authURL)', using the calculated 'homePath' to replace '~' as no SHELL or USER environment variable set", ["debug"]);} tempAuthUrl = buildNormalizedPath(buildPath(defaultHomePath, strip(tempAuthUrl, "~"))); } // Does the 'currently configured' tempAuthUrl include a ~ if (canFind(tempResponseUrl, "~")) { // A ~ was found in auth_files(authURL) if (debugLogging) {addLogEntry("auth_files: A '~' was found in 'auth_files(tempResponseUrl)', using the calculated 'homePath' to replace '~' as no SHELL or USER environment variable set", ["debug"]);} tempResponseUrl = buildNormalizedPath(buildPath(defaultHomePath, strip(tempResponseUrl, "~"))); } } else { // Shell environment is set, automatic expansion of '~' if present is possible // Does the 'currently configured' tempAuthUrl include a ~ if (canFind(tempAuthUrl, "~")) { // A ~ was found in auth_files(authURL) if (debugLogging) {addLogEntry("auth_files: A '~' was found in the configured 'auth_files(authURL)', automatically expanding as SHELL and USER environment variable is set", ["debug"]);} tempAuthUrl = expandTilde(tempAuthUrl); } // Does the 'currently configured' tempAuthUrl include a ~ if (canFind(tempResponseUrl, "~")) { // A ~ was found in auth_files(authURL) if (debugLogging) {addLogEntry("auth_files: A '~' was found in the configured 'auth_files(tempResponseUrl)', automatically expanding as SHELL and USER environment variable is set", ["debug"]);} tempResponseUrl = expandTilde(tempResponseUrl); } } // Build new string newAuthFilesString = tempAuthUrl ~ ":" ~ tempResponseUrl; if (debugLogging) {addLogEntry("auth_files - updated value: " ~ newAuthFilesString, ["debug"]);} setValueString("auth_files", newAuthFilesString); } if (opt.helpWanted) { outputLongHelp(opt.options); // Shutdown logging, which also flushes all logging buffers shutdownLogging(); // Exit as successful exit(EXIT_SUCCESS); } } catch (GetOptException e) { // getOpt error - must use writeln() here writeln(e.msg); writeln("Try 'onedrive -h' for more information"); // Shutdown logging, which also flushes all logging buffers shutdownLogging(); // Exit as failure exit(EXIT_FAILURE); } catch (Exception e) { // general error - must use writeln() here writeln(e.msg); writeln("Try 'onedrive -h' for more information"); // Shutdown logging, which also flushes all logging buffers shutdownLogging(); // Exit as failure exit(EXIT_FAILURE); } } // Check the arguments passed in for any that will be deprecated void checkDeprecatedOptions(string[] cliArgs) { bool deprecatedCommandsFound = false; foreach (cliArg; cliArgs) { // Check each CLI arg for items that have been deprecated // --synchronize deprecated in v2.5.0, will be removed in future version if (cliArg == "--synchronize") { addLogEntry(); // used instead of an empty 'writeln();' to ensure the line break is correct in the buffered console output ordering addLogEntry("DEPRECIATION WARNING: --synchronize has been deprecated in favour of --sync or -s"); deprecatedCommandsFound = true; } // --get-O365-drive-id deprecated in v2.5.0, will be removed in future version if (cliArg == "--get-O365-drive-id") { addLogEntry(); // used instead of an empty 'writeln();' to ensure the line break is correct in the buffered console output ordering addLogEntry("DEPRECIATION WARNING: --get-O365-drive-id has been deprecated in favour of --get-sharepoint-drive-id"); deprecatedCommandsFound = true; } } if (deprecatedCommandsFound) { addLogEntry("DEPRECIATION WARNING: Deprecated commands will be removed in a future release."); addLogEntry(); // used instead of an empty 'writeln();' to ensure the line break is correct in the buffered console output ordering } } // Display the applicable application configuration void displayApplicationConfiguration() { if (getValueBool("display_running_config")) { addLogEntry("--------------- Application Runtime Configuration ---------------"); } // Display application version addLogEntry("Application version = " ~ applicationVersion); addLogEntry("Compiled with = " ~ compilerDetails()); addLogEntry("Curl version = " ~ getCurlVersionString()); // Display all of the pertinent configuration options addLogEntry("User Application Config path = " ~ configDirName); addLogEntry("System Application Config path = " ~ systemConfigDirName); // Does a config file exist or are we using application defaults addLogEntry("Applicable Application 'config' location = " ~ applicableConfigFilePath); string configFileStatusMessage; if (exists(applicableConfigFilePath)) { configFileStatusMessage = "true - using 'config' file values to override application defaults"; } else { configFileStatusMessage = "false - using application defaults"; } addLogEntry("Configuration file found in config location = " ~ configFileStatusMessage); // Display where various files should live // - items.sqlite3 // - sync_list // If using the 'system' directory, (/etc/onedrive) for the config file, these should always live in the 'users' home directory addLogEntry("Applicable 'sync_list' location = " ~ syncListFilePath); addLogEntry("Applicable 'items.sqlite3' location = " ~ databaseFilePath); // Is config option drive_id configured? addLogEntry("Config option 'drive_id' = " ~ getValueString("drive_id")); // Config Options as per 'config' file addLogEntry("Config option 'sync_dir' = " ~ getValueString("sync_dir")); // logging and notifications addLogEntry("Config option 'enable_logging' = " ~ to!string(getValueBool("enable_logging"))); addLogEntry("Config option 'log_dir' = " ~ getValueString("log_dir")); addLogEntry("Config option 'disable_notifications' = " ~ to!string(getValueBool("disable_notifications"))); // skip files and directory and 'matching' policy addLogEntry("Config option 'skip_dir' = " ~ getValueString("skip_dir")); addLogEntry("Config option 'skip_dir_strict_match' = " ~ to!string(getValueBool("skip_dir_strict_match"))); addLogEntry("Config option 'skip_file' = " ~ getValueString("skip_file")); addLogEntry("Config option 'skip_dotfiles' = " ~ to!string(getValueBool("skip_dotfiles"))); addLogEntry("Config option 'skip_symlinks' = " ~ to!string(getValueBool("skip_symlinks"))); // --monitor sync process options addLogEntry("Config option 'monitor_interval' = " ~ to!string(getValueLong("monitor_interval"))); addLogEntry("Config option 'monitor_log_frequency' = " ~ to!string(getValueLong("monitor_log_frequency"))); addLogEntry("Config option 'monitor_fullscan_frequency' = " ~ to!string(getValueLong("monitor_fullscan_frequency"))); // sync process and method addLogEntry("Config option 'read_only_auth_scope' = " ~ to!string(getValueBool("read_only_auth_scope"))); addLogEntry("Config option 'dry_run' = " ~ to!string(getValueBool("dry_run"))); addLogEntry("Config option 'upload_only' = " ~ to!string(getValueBool("upload_only"))); addLogEntry("Config option 'download_only' = " ~ to!string(getValueBool("download_only"))); addLogEntry("Config option 'local_first' = " ~ to!string(getValueBool("local_first"))); addLogEntry("Config option 'check_nosync' = " ~ to!string(getValueBool("check_nosync"))); addLogEntry("Config option 'check_nomount' = " ~ to!string(getValueBool("check_nomount"))); addLogEntry("Config option 'resync' = " ~ to!string(getValueBool("resync"))); addLogEntry("Config option 'resync_auth' = " ~ to!string(getValueBool("resync_auth"))); addLogEntry("Config option 'cleanup_local_files' = " ~ to!string(getValueBool("cleanup_local_files"))); addLogEntry("Config option 'disable_permission_set' = " ~ to!string(getValueBool("disable_permission_set"))); addLogEntry("Config option 'transfer_order' = " ~ getValueString("transfer_order")); // data integrity addLogEntry("Config option 'classify_as_big_delete' = " ~ to!string(getValueLong("classify_as_big_delete"))); addLogEntry("Config option 'disable_upload_validation' = " ~ to!string(getValueBool("disable_upload_validation"))); addLogEntry("Config option 'disable_download_validation' = " ~ to!string(getValueBool("disable_download_validation"))); addLogEntry("Config option 'bypass_data_preservation' = " ~ to!string(getValueBool("bypass_data_preservation"))); addLogEntry("Config option 'no_remote_delete' = " ~ to!string(getValueBool("no_remote_delete"))); addLogEntry("Config option 'remove_source_files' = " ~ to!string(getValueBool("remove_source_files"))); addLogEntry("Config option 'sync_dir_permissions' = " ~ to!string(getValueLong("sync_dir_permissions"))); addLogEntry("Config option 'sync_file_permissions' = " ~ to!string(getValueLong("sync_file_permissions"))); addLogEntry("Config option 'space_reservation' = " ~ to!string(getValueLong("space_reservation"))); addLogEntry("Config option 'permanent_delete' = " ~ to!string(getValueBool("permanent_delete"))); addLogEntry("Config option 'write_xattr_data' = " ~ to!string(getValueBool("write_xattr_data"))); // curl operations addLogEntry("Config option 'application_id' = " ~ getValueString("application_id")); addLogEntry("Config option 'azure_ad_endpoint' = " ~ getValueString("azure_ad_endpoint")); addLogEntry("Config option 'azure_tenant_id' = " ~ getValueString("azure_tenant_id")); addLogEntry("Config option 'user_agent' = " ~ getValueString("user_agent")); addLogEntry("Config option 'force_http_11' = " ~ to!string(getValueBool("force_http_11"))); addLogEntry("Config option 'debug_https' = " ~ to!string(getValueBool("debug_https"))); addLogEntry("Config option 'rate_limit' = " ~ to!string(getValueLong("rate_limit"))); addLogEntry("Config option 'operation_timeout' = " ~ to!string(getValueLong("operation_timeout"))); addLogEntry("Config option 'dns_timeout' = " ~ to!string(getValueLong("dns_timeout"))); addLogEntry("Config option 'connect_timeout' = " ~ to!string(getValueLong("connect_timeout"))); addLogEntry("Config option 'data_timeout' = " ~ to!string(getValueLong("data_timeout"))); addLogEntry("Config option 'ip_protocol_version' = " ~ to!string(getValueLong("ip_protocol_version"))); addLogEntry("Config option 'threads' = " ~ to!string(getValueLong("threads"))); addLogEntry("Config option 'max_curl_idle' = " ~ to!string(getValueLong("max_curl_idle"))); // GUI notifications version(Notifications) { addLogEntry("Environment var 'XDG_RUNTIME_DIR' = " ~ to!string(xdg_exists)); addLogEntry("Environment var 'DBUS_SESSION_BUS_ADDRESS' = " ~ to!string(dbus_exists)); addLogEntry("Config option 'notify_file_actions' = " ~ to!string(getValueBool("notify_file_actions"))); } else { addLogEntry("Compile time option --enable-notifications = false"); } // Is sync_list configured and contains entries? if (exists(syncListFilePath) && getSize(syncListFilePath) > 0) { addLogEntry(); // used instead of an empty 'writeln();' to ensure the line break is correct in the buffered console output ordering addLogEntry("Selective sync 'sync_list' configured = true"); addLogEntry("sync_list config option 'sync_root_files' = " ~ to!string(getValueBool("sync_root_files"))); addLogEntry("sync_list contents:"); // Output the sync_list contents auto syncListFile = File(syncListFilePath, "r"); auto range = syncListFile.byLine(); addLogEntry("------------------------------'sync_list'------------------------------"); foreach (line; range) { addLogEntry(to!string(line)); } addLogEntry("-----------------------------------------------------------------------"); // Close reading the 'sync_list' file syncListFile.close(); } else { // file does not exist or file size is not greater than 0 addLogEntry(); // used instead of an empty 'writeln();' to ensure the line break is correct in the buffered console output ordering if (exists(syncListFilePath) && getSize(syncListFilePath) == 0) { // 'sync_list' file exists, no entries addLogEntry("Selective sync 'sync_list' configured = file exists but contains zero data"); } else { // no 'sync_list' file addLogEntry("Selective sync 'sync_list' configured = false"); } } // Is sync_business_shared_items enabled and configured ? addLogEntry(); // used instead of an empty 'writeln();' to ensure the line break is correct in the buffered console output ordering addLogEntry("Config option 'sync_business_shared_items' = " ~ to!string(getValueBool("sync_business_shared_items"))); if (getValueBool("sync_business_shared_items")) { // display what the shared files directory will be addLogEntry("Config option 'Shared Files Directory' = " ~ configuredBusinessSharedFilesDirectoryName); } // Are webhooks enabled? addLogEntry(); // used instead of an empty 'writeln();' to ensure the line break is correct in the buffered console output ordering addLogEntry("Config option 'webhook_enabled' = " ~ to!string(getValueBool("webhook_enabled"))); if (getValueBool("webhook_enabled")) { addLogEntry("Config option 'webhook_public_url' = " ~ getValueString("webhook_public_url")); addLogEntry("Config option 'webhook_listening_host' = " ~ getValueString("webhook_listening_host")); addLogEntry("Config option 'webhook_listening_port' = " ~ to!string(getValueLong("webhook_listening_port"))); addLogEntry("Config option 'webhook_expiration_interval' = " ~ to!string(getValueLong("webhook_expiration_interval"))); addLogEntry("Config option 'webhook_renewal_interval' = " ~ to!string(getValueLong("webhook_renewal_interval"))); addLogEntry("Config option 'webhook_retry_interval' = " ~ to!string(getValueLong("webhook_retry_interval"))); } if (getValueBool("display_running_config")) { addLogEntry(); addLogEntry("--------------------DEVELOPER_OPTIONS----------------------------"); addLogEntry("Config option 'force_children_scan' = " ~ to!string(getValueBool("force_children_scan"))); addLogEntry(); } if (getValueBool("display_running_config")) { addLogEntry("-----------------------------------------------------------------"); } } // Prompt the user to accept the risk of using --resync bool displayResyncRiskForAcceptance() { // what is the user risk acceptance? bool userRiskAcceptance = false; // Did the user use --resync-auth or 'resync_auth' in the config file to negate presenting this message? if (!getValueBool("resync_auth")) { // need to prompt user char response; // --resync warning message addLogEntry("", ["consoleOnly"]); // new line, console only addLogEntry("The usage of --resync will delete your local 'onedrive' client state, thus no record of your current 'sync status' will exist.", ["consoleOnly"]); addLogEntry("This has the potential to overwrite local versions of files with perhaps older versions of documents downloaded from OneDrive, resulting in local data loss.", ["consoleOnly"]); addLogEntry("If in doubt, backup your local data before using --resync", ["consoleOnly"]); addLogEntry("", ["consoleOnly"]); // new line, console only addLogEntry("Are you sure you wish to proceed with --resync? [Y/N] ", ["consoleOnlyNoNewLine"]); try { // Attempt to read user response string input = readln().strip; if (input.length > 0) { response = std.ascii.toUpper(input[0]); } } catch (std.format.FormatException e) { userRiskAcceptance = false; // Caught an error return EXIT_FAILURE; } // What did the user enter? if (debugLogging) {addLogEntry("--resync warning User Response Entered: " ~ to!string(response), ["debug"]);} // Evaluate user response if ((to!string(response) == "y") || (to!string(response) == "Y")) { // User has accepted --resync risk to proceed userRiskAcceptance = true; // Are you sure you wish .. does not use writeln(); write("\n"); } } else { // resync_auth is true userRiskAcceptance = true; } // Return the --resync acceptance or not return userRiskAcceptance; } // Prompt the user to accept the risk of using --force-sync bool displayForceSyncRiskForAcceptance() { // what is the user risk acceptance? bool userRiskAcceptance = false; // need to prompt user char response; // --force-sync warning message addLogEntry("", ["consoleOnly"]); // new line, console only addLogEntry("The use of --force-sync will reconfigure the application to use defaults. This may have untold and unknown future impacts.", ["consoleOnly"]); addLogEntry("By proceeding in using this option you accept any impacts including any data loss that may occur as a result of using --force-sync.", ["consoleOnly"]); addLogEntry("", ["consoleOnly"]); // new line, console only addLogEntry("Are you sure you wish to proceed with --force-sync [Y/N] ", ["consoleOnlyNoNewLine"]); try { // Attempt to read user response string input = readln().strip; if (input.length > 0) { response = std.ascii.toUpper(input[0]); } } catch (std.format.FormatException e) { userRiskAcceptance = false; // Caught an error return EXIT_FAILURE; } // What did the user enter? if (debugLogging) {addLogEntry("--force-sync warning User Response Entered: " ~ to!string(response), ["debug"]);} // Evaluate user response if ((to!string(response) == "y") || (to!string(response) == "Y")) { // User has accepted --force-sync risk to proceed userRiskAcceptance = true; // Are you sure you wish .. does not use writeln(); write("\n"); } // Return the --resync acceptance or not return userRiskAcceptance; } // Check the application configuration for any changes that need to trigger a --resync // This function is only called if --resync is not present bool applicationChangeWhereResyncRequired() { // Default is that no resync is required bool resyncRequired = false; // Consolidate the flags for different configuration changes bool[9] configOptionsDifferent; // Handle multiple entries of skip_file string backupConfigFileSkipFile; // Handle multiple entries of skip_dir string backupConfigFileSkipDir; // Create and read the required initial hash files createRequiredInitialConfigurationHashFiles(); // Read in the existing hash file values readExistingConfigurationHashFiles(); // can we read the backup config file bool failedToReadBackupConfig = false; // Helper lambda for logging and setting the difference flag auto logAndSetDifference = (string message, size_t index) { if (debugLogging) {addLogEntry(message, ["debug"]);} configOptionsDifferent[index] = true; }; // Check for changes in the sync_list and business_shared_items files if (currentSyncListHash != previousSyncListHash) logAndSetDifference("sync_list file has been updated, --resync needed", 0); // Check for updates in the config file if (currentConfigHash != previousConfigHash) { addLogEntry("Application configuration file has been updated, checking if --resync needed"); if (debugLogging) {addLogEntry("Using this configBackupFile: " ~ configBackupFile, ["debug"]);} if (exists(configBackupFile)) { string[string] backupConfigStringValues; backupConfigStringValues["drive_id"] = ""; backupConfigStringValues["sync_dir"] = ""; backupConfigStringValues["skip_file"] = ""; backupConfigStringValues["skip_dir"] = ""; backupConfigStringValues["skip_dotfiles"] = ""; backupConfigStringValues["skip_symlinks"] = ""; backupConfigStringValues["sync_business_shared_items"] = ""; bool drive_id_present = false; bool sync_dir_present = false; bool skip_file_present = false; bool skip_dir_present = false; bool skip_dotfiles_present = false; bool skip_symlinks_present = false; bool sync_business_shared_items_present = false; string configOptionModifiedMessage = " was modified since the last time the application was successfully run, --resync required"; File configBackupFileHandle; try { configBackupFileHandle = File(configBackupFile, "r"); } catch (FileException e) { // filesystem error failedToReadBackupConfig = true; displayFileSystemErrorMessage(e.msg, getFunctionName!({})); } catch (std.exception.ErrnoException e) { // filesystem error failedToReadBackupConfig = true; displayFileSystemErrorMessage(e.msg, getFunctionName!({})); } scope(exit) { if (configBackupFileHandle.isOpen()) { configBackupFileHandle.close(); } } if (!failedToReadBackupConfig) { // backup config file was able to be read string lineBuffer; auto range = configBackupFileHandle.byLine(); foreach (line; range) { lineBuffer = stripLeft(line).to!string; if (lineBuffer.length == 0 || lineBuffer[0] == ';' || lineBuffer[0] == '#') continue; auto c = lineBuffer.matchFirst(configRegex); if (!c.empty) { c.popFront(); // skip the whole match string key = c.front.dup; if (debugLogging) {addLogEntry("Backup Config Key: " ~ key, ["debug"]);} auto p = key in backupConfigStringValues; if (p) { c.popFront(); string value = c.front.dup; // Compare each key value with current config if (key == "drive_id") { drive_id_present = true; if (value != getValueString("drive_id")) { logAndSetDifference(key ~ configOptionModifiedMessage, 2); } } if (key == "sync_dir") { sync_dir_present = true; if (value != getValueString("sync_dir")) { logAndSetDifference(key ~ configOptionModifiedMessage, 3); } } // skip_file handling if (key == "skip_file") { skip_file_present = true; // Handle multiple entries of skip_file if (backupConfigFileSkipFile.empty) { // currently no entry exists, include 'defaultSkipFile' entries backupConfigFileSkipFile = defaultSkipFile ~ "|" ~ to!string(c.front.dup); } else { // add to existing backupConfigFileSkipFile entry backupConfigFileSkipFile = backupConfigFileSkipFile ~ "|" ~ to!string(c.front.dup); } } // skip_dir handling if (key == "skip_dir") { skip_dir_present = true; // Handle multiple entries of skip_dir if (backupConfigFileSkipDir.empty) { // currently no entry exists backupConfigFileSkipDir = c.front.dup; } else { // add to existing backupConfigFileSkipDir entry backupConfigFileSkipDir = backupConfigFileSkipDir ~ "|" ~ to!string(c.front.dup); } } if (key == "skip_dotfiles") { skip_dotfiles_present = true; if (value != to!string(getValueBool("skip_dotfiles"))) { logAndSetDifference(key ~ configOptionModifiedMessage, 6); } } if (key == "skip_symlinks") { skip_symlinks_present = true; if (value != to!string(getValueBool("skip_symlinks"))) { logAndSetDifference(key ~ configOptionModifiedMessage, 7); } } if (key == "sync_business_shared_items") { sync_business_shared_items_present = true; if (value != to!string(getValueBool("sync_business_shared_items"))) { logAndSetDifference(key ~ configOptionModifiedMessage, 8); } } } } } // skip_file can be specified multiple times if (skip_file_present && backupConfigFileSkipFile != configFileSkipFile) logAndSetDifference("skip_file" ~ configOptionModifiedMessage, 4); // skip_dir can be specified multiple times if (skip_dir_present && backupConfigFileSkipDir != configFileSkipDir) logAndSetDifference("skip_dir" ~ configOptionModifiedMessage, 5); // Check for newly added configuration options if (!drive_id_present && configFileDriveId != "") logAndSetDifference("drive_id newly added ... --resync needed", 2); if (!sync_dir_present && configFileSyncDir != defaultSyncDir) logAndSetDifference("sync_dir newly added ... --resync needed", 3); if (!skip_file_present && configFileSkipFile != defaultSkipFile) logAndSetDifference("skip_file newly added ... --resync needed", 4); if (!skip_dir_present && configFileSkipDir != "") logAndSetDifference("skip_dir newly added ... --resync needed", 5); if (!skip_dotfiles_present && configFileSkipDotfiles) logAndSetDifference("skip_dotfiles newly added ... --resync needed", 6); if (!skip_symlinks_present && configFileSkipSymbolicLinks) logAndSetDifference("skip_symlinks newly added ... --resync needed", 7); if (!sync_business_shared_items_present && configFileSyncBusinessSharedItems) logAndSetDifference("sync_business_shared_items newly added ... --resync needed", 8); } else { // failed to read backup config file addLogEntry("WARNING: unable to read backup config, unable to validate if any changes made"); } } else { addLogEntry("WARNING: no backup config file was found, unable to validate if any changes made"); } } // config file set options can be changed via CLI input, specifically these will impact sync and a --resync will be needed: // --syncdir ARG // --skip-file ARG // --skip-dir ARG // --skip-dot-files // --skip-symlinks // Check CLI options if (exists(applicableConfigFilePath)) { if (configFileSyncDir != "" && configFileSyncDir != getValueString("sync_dir")) { // config file was set and CLI input changed this // Is this potentially running as a Docker container? if (entrypointExists) { // entrypoint.sh exists if (debugLogging) {addLogEntry("sync_dir: CLI override of config file option, however entrypoint.sh exists, thus most likely running as a container", ["debug"]);} } else { // Not a Docker container, raise that --resync needed due to configuration change logAndSetDifference("sync_dir: CLI override of config file option, --resync needed", 3); } } if (configFileSkipFile != "" && configFileSkipFile != getValueString("skip_file")) logAndSetDifference("skip_file: CLI override of config file option, --resync needed", 4); if (configFileSkipDir != "" && configFileSkipDir != getValueString("skip_dir")) logAndSetDifference("skip_dir: CLI override of config file option, --resync needed", 5); if (!configFileSkipDotfiles && getValueBool("skip_dotfiles")) logAndSetDifference("skip_dotfiles: CLI override of config file option, --resync needed", 6); if (!configFileSkipSymbolicLinks && getValueBool("skip_symlinks")) logAndSetDifference("skip_symlinks: CLI override of config file option, --resync needed", 7); } // Aggregate the result to determine if a resync is required if (!failedToReadBackupConfig) { foreach (optionDifferent; configOptionsDifferent) { if (optionDifferent) { resyncRequired = true; break; } } } // Final override // In certain situations, regardless of config 'resync' needed status, ignore this so that the application can display 'non-syncable' information // Options that should now be looked at are: // --list-shared-items if (getValueBool("list_business_shared_items")) resyncRequired = false; // Return the calculated boolean return resyncRequired; } // Cleanup hash files that require to be cleaned up when a --resync is issued void cleanupHashFilesDueToResync() { if (!getValueBool("dry_run")) { // cleanup hash files if (debugLogging) {addLogEntry("Cleaning up configuration hash files", ["debug"]);} safeRemove(configHashFile); safeRemove(syncListHashFile); } else { // --dry-run scenario ... technically we should not be making any local file changes ....... addLogEntry("DRY RUN: Not removing hash files as --dry-run has been used"); } } // For each of the config files, update the hash data in the hash files void updateHashContentsForConfigFiles() { // Are we in a --dry-run scenario? if (!getValueBool("dry_run")) { // Not a dry-run scenario, update the applicable files // Update applicable 'config' files if (exists(applicableConfigFilePath)) { // Update the hash of the applicable config file if (debugLogging) {addLogEntry("Updating applicable config file hash", ["debug"]);} try { std.file.write(configHashFile, computeQuickXorHash(applicableConfigFilePath)); // Hash file should only be readable by the user who created it - 0600 permissions needed configHashFile.setAttributes(convertedPermissionValue); } catch (FileException e) { // filesystem error displayFileSystemErrorMessage(e.msg, getFunctionName!({})); } } // Update 'sync_list' files if (exists(syncListFilePath)) { // update sync_list hash if (debugLogging) {addLogEntry("Updating sync_list hash", ["debug"]);} try { std.file.write(syncListHashFile, computeQuickXorHash(syncListFilePath)); // Hash file should only be readable by the user who created it - 0600 permissions needed syncListHashFile.setAttributes(convertedPermissionValue); } catch (FileException e) { // filesystem error displayFileSystemErrorMessage(e.msg, getFunctionName!({})); } } } else { // --dry-run scenario ... technically we should not be making any local file changes ....... addLogEntry("DRY RUN: Not updating hash files as --dry-run has been used"); } } // Create any required hash files for files that help us determine if the configuration has changed since last run void createRequiredInitialConfigurationHashFiles() { // Does a 'config' file exist with a valid hash file if (exists(applicableConfigFilePath)) { if (!exists(configHashFile)) { // no existing hash file exists try { std.file.write(configHashFile, "initial-hash"); // Hash file should only be readable by the user who created it - 0600 permissions needed configHashFile.setAttributes(convertedPermissionValue); } catch (FileException e) { // filesystem error displayFileSystemErrorMessage(e.msg, getFunctionName!({})); } } // Generate the runtime hash for the 'config' file currentConfigHash = computeQuickXorHash(applicableConfigFilePath); } // Does a 'sync_list' file exist with a valid hash file if (exists(syncListFilePath)) { if (!exists(syncListHashFile)) { // no existing hash file exists try { std.file.write(syncListHashFile, "initial-hash"); // Hash file should only be readable by the user who created it - 0600 permissions needed syncListHashFile.setAttributes(convertedPermissionValue); } catch (FileException e) { // filesystem error displayFileSystemErrorMessage(e.msg, getFunctionName!({})); } } // Generate the runtime hash for the 'sync_list' file currentSyncListHash = computeQuickXorHash(syncListFilePath); } } // Read in the text values of the previous configurations int readExistingConfigurationHashFiles() { if (exists(configHashFile)) { try { previousConfigHash = readText(configHashFile); } catch (std.file.FileException e) { // Unable to access required hash file addLogEntry("ERROR: Unable to access " ~ e.msg); // Use exit scopes to shutdown API return EXIT_FAILURE; } } if (exists(syncListHashFile)) { try { previousSyncListHash = readText(syncListHashFile); } catch (std.file.FileException e) { // Unable to access required hash file addLogEntry("ERROR: Unable to access " ~ e.msg); // Use exit scopes to shutdown API return EXIT_FAILURE; } } return 0; } // Check for basic option conflicts - flags that should not be used together and/or flag combinations that conflict with each other bool checkForBasicOptionConflicts() { bool operationalConflictDetected = false; // What are the permission that have been set for the application? // These are relevant for: // - The ~/OneDrive parent folder or 'sync_dir' configured item // - Any new folder created under ~/OneDrive or 'sync_dir' // - Any new file created under ~/OneDrive or 'sync_dir' // valid permissions are 000 -> 777 - anything else is invalid long syncDirPermissions = getValueLong("sync_dir_permissions"); long syncFilePermissions = getValueLong("sync_file_permissions"); bool invalidPermissions = false; // Check 'sync_dir_permissions' if (syncDirPermissions < 0 || syncDirPermissions > 777) { addLogEntry("ERROR: Invalid 'User|Group|Other' permissions set for 'sync_dir_permissions' within your config file. Please check your configuration"); invalidPermissions = true; } // Check 'sync_file_permissions' if (syncFilePermissions < 0 || syncFilePermissions > 777) { addLogEntry("ERROR: Invalid 'User|Group|Other' permissions set for 'sync_file_permissions' within your config file. Please check your configuration"); invalidPermissions = true; } // Invalid permissions detected? if (invalidPermissions) { operationalConflictDetected = true; } else { // Debug log output what permissions are being set to if (debugLogging) {addLogEntry("Configuring default new folder permissions as: " ~ to!string(getValueLong("sync_dir_permissions")), ["debug"]);} configureRequiredDirectoryPermissions(); if (debugLogging) {addLogEntry("Configuring default new file permissions as: " ~ to!string(getValueLong("sync_file_permissions")), ["debug"]);} configureRequiredFilePermissions(); } // --upload-only and --download-only cannot be used together if ((getValueBool("upload_only")) && (getValueBool("download_only"))) { addLogEntry("ERROR: --upload-only and --download-only cannot be used together. Use one, not both at the same time"); operationalConflictDetected = true; } // --sync and --monitor cannot be used together if ((getValueBool("synchronize")) && (getValueBool("monitor"))) { addLogEntry("ERROR: --sync and --monitor cannot be used together. Only use one of these options, not both at the same time"); operationalConflictDetected = true; } // --no-remote-delete can ONLY be enabled when --upload-only is used if ((getValueBool("no_remote_delete")) && (!getValueBool("upload_only"))) { addLogEntry("ERROR: --no-remote-delete can only be used with --upload-only"); operationalConflictDetected = true; } // --remove-source-files can ONLY be enabled when --upload-only is used if ((getValueBool("remove_source_files")) && (!getValueBool("upload_only"))) { addLogEntry("ERROR: --remove-source-files can only be used with --upload-only"); operationalConflictDetected = true; } // --cleanup-local-files can ONLY be enabled when --download-only is used if ((getValueBool("cleanup_local_files")) && (!getValueBool("download_only"))) { addLogEntry("ERROR: --cleanup-local-files can only be used with --download-only"); operationalConflictDetected = true; } // --list-shared-folders cannot be used with --resync and/or --resync-auth if ((getValueBool("list_business_shared_items")) && ((getValueBool("resync")) || (getValueBool("resync_auth")))) { addLogEntry("ERROR: --list-shared-items cannot be used with --resync or --resync-auth"); operationalConflictDetected = true; } // --list-shared-folders cannot be used with --sync or --monitor if ((getValueBool("list_business_shared_items")) && ((getValueBool("synchronize")) || (getValueBool("monitor")))) { addLogEntry("ERROR: --list-shared-items cannot be used with --sync or --monitor"); operationalConflictDetected = true; } // --sync-shared-files can ONLY be used with sync_business_shared_items if ((getValueBool("sync_business_shared_files")) && (!getValueBool("sync_business_shared_items"))) { addLogEntry("ERROR: The --sync-shared-files option can only be utilised if the 'sync_business_shared_items' configuration setting is enabled."); operationalConflictDetected = true; } // --display-sync-status cannot be used with --resync and/or --resync-auth if ((getValueBool("display_sync_status")) && ((getValueBool("resync")) || (getValueBool("resync_auth")))) { addLogEntry("ERROR: --display-sync-status cannot be used with --resync or --resync-auth"); operationalConflictDetected = true; } // --modified-by cannot be used with --resync and/or --resync-auth if ((!getValueString("modified_by").empty) && ((getValueBool("resync")) || (getValueBool("resync_auth")))) { addLogEntry("ERROR: --modified-by cannot be used with --resync or --resync-auth"); operationalConflictDetected = true; } // --get-file-link cannot be used with --resync and/or --resync-auth if ((!getValueString("get_file_link").empty) && ((getValueBool("resync")) || (getValueBool("resync_auth")))) { addLogEntry("ERROR: --get-file-link cannot be used with --resync or --resync-auth"); operationalConflictDetected = true; } // --create-share-link cannot be used with --resync and/or --resync-auth if ((!getValueString("create_share_link").empty) && ((getValueBool("resync")) || (getValueBool("resync_auth")))) { addLogEntry("ERROR: --create-share-link cannot be used with --resync or --resync-auth"); operationalConflictDetected = true; } // --get-sharepoint-drive-id cannot be used with --resync and/or --resync-auth if ((!getValueString("sharepoint_library_name").empty) && ((getValueBool("resync")) || (getValueBool("resync_auth")))) { addLogEntry("ERROR: --get-sharepoint-drive-id cannot be used with --resync or --resync-auth"); operationalConflictDetected = true; } // --monitor and --display-sync-status cannot be used together if ((getValueBool("monitor")) && (getValueBool("display_sync_status"))) { addLogEntry("ERROR: --monitor and --display-sync-status cannot be used together"); operationalConflictDetected = true; } // --sync and --display-sync-status cannot be used together if ((getValueBool("synchronize")) && (getValueBool("display_sync_status"))) { addLogEntry("ERROR: --sync and --display-sync-status cannot be used together"); operationalConflictDetected = true; } // --monitor and --display-quota cannot be used together if ((getValueBool("monitor")) && (getValueBool("display_quota"))) { addLogEntry("ERROR: --monitor and --display-quota cannot be used together"); operationalConflictDetected = true; } // --sync and --display-quota cannot be used together if ((getValueBool("synchronize")) && (getValueBool("display_quota"))) { addLogEntry("ERROR: --sync and --display-quota cannot be used together"); operationalConflictDetected = true; } // --force-sync can only be used when using --sync and --single-directory if (getValueBool("force_sync")) { bool conflict = false; // Should not be used with --monitor if (getValueBool("monitor")) conflict = true; // single_directory must not be empty if (getValueString("single_directory").empty) conflict = true; if (conflict) { addLogEntry("ERROR: --force-sync can only be used with --sync --single-directory"); operationalConflictDetected = true; } } // When using 'azure_ad_endpoint', 'azure_tenant_id' cannot be empty if ((!getValueString("azure_ad_endpoint").empty) && (getValueString("azure_tenant_id").empty)) { addLogEntry("ERROR: config option 'azure_tenant_id' cannot be empty when 'azure_ad_endpoint' is configured"); operationalConflictDetected = true; } // When using --enable-logging the 'log_dir' cannot be empty if ((getValueBool("enable_logging")) && (getValueString("log_dir").empty)) { addLogEntry("ERROR: config option 'log_dir' cannot be empty when 'enable_logging' is configured"); operationalConflictDetected = true; } // When using --syncdir, the value cannot be empty. if (strip(getValueString("sync_dir")).empty) { addLogEntry("ERROR: --syncdir value cannot be empty"); operationalConflictDetected = true; } // --monitor and --create-directory cannot be used together if ((getValueBool("monitor")) && (!getValueString("create_directory").empty)) { addLogEntry("ERROR: --monitor and --create-directory cannot be used together"); operationalConflictDetected = true; } // --sync and --create-directory cannot be used together if ((getValueBool("synchronize")) && (!getValueString("create_directory").empty)) { addLogEntry("ERROR: --sync and --create-directory cannot be used together"); operationalConflictDetected = true; } // --monitor and --remove-directory cannot be used together if ((getValueBool("monitor")) && (!getValueString("remove_directory").empty)) { addLogEntry("ERROR: --monitor and --remove-directory cannot be used together"); operationalConflictDetected = true; } // --sync and --remove-directory cannot be used together if ((getValueBool("synchronize")) && (!getValueString("remove_directory").empty)) { addLogEntry("ERROR: --sync and --remove-directory cannot be used together"); operationalConflictDetected = true; } // --monitor and --source-directory cannot be used together if ((getValueBool("monitor")) && (!getValueString("source_directory").empty)) { addLogEntry("ERROR: --monitor and --source-directory cannot be used together"); operationalConflictDetected = true; } // --sync and --source-directory cannot be used together if ((getValueBool("synchronize")) && (!getValueString("source_directory").empty)) { addLogEntry("ERROR: --sync and --source-directory cannot be used together"); operationalConflictDetected = true; } // --monitor and --destination-directory cannot be used together if ((getValueBool("monitor")) && (!getValueString("destination_directory").empty)) { addLogEntry("ERROR: --monitor and --destination-directory cannot be used together"); operationalConflictDetected = true; } // --sync and --destination-directory cannot be used together if ((getValueBool("synchronize")) && (!getValueString("destination_directory").empty)) { addLogEntry("ERROR: --sync and --destination-directory cannot be used together"); operationalConflictDetected = true; } // --download-only and --local-first cannot be used together if ((getValueBool("download_only")) && (getValueBool("local_first"))) { addLogEntry("ERROR: --download-only cannot be used with --local-first"); operationalConflictDetected = true; } // Test that '--modified-by ' has a valid argument and not another directive if (getValueString("modified_by") != "") { // Does the string start with '--' ? if (getValueString("modified_by").startsWith("--")) { addLogEntry("ERROR: --modified-by missing a valid entry"); operationalConflictDetected = true; } } // Test that '--get-file-link ' has a valid argument and not another directive if (getValueString("get_file_link") != "") { // Does the string start with '--' ? if (getValueString("get_file_link").startsWith("--")) { addLogEntry("ERROR: --get-file-link missing a valid entry"); operationalConflictDetected = true; } } // Test that '--create-share-link ' has a valid argument and not another directive if (getValueString("create_share_link") != "") { // Does the string start with '--' ? if (getValueString("create_share_link").startsWith("--")) { addLogEntry("ERROR: --create-share-link missing a valid entry"); operationalConflictDetected = true; } } // Test that '--create-directory ' has a valid argument and not another directive if (getValueString("create_directory") != "") { // Does the string start with '--' ? if (getValueString("create_directory").startsWith("--")) { addLogEntry("ERROR: --create-directory missing a valid entry"); operationalConflictDetected = true; } } // Test that '--remove-directory ' has a valid argument and not another directive if (getValueString("remove_directory") != "") { // Does the string start with '--' ? if (getValueString("remove_directory").startsWith("--")) { addLogEntry("ERROR: --remove-directory missing a valid entry"); operationalConflictDetected = true; } } // Test that '--source-directory ' has a valid argument and not another directive if (getValueString("source_directory") != "") { // Does the string start with '--' ? if (getValueString("source_directory").startsWith("--")) { addLogEntry("ERROR: --source-directory missing a valid entry"); operationalConflictDetected = true; } } // Test that '--destination-directory ' has a valid argument and not another directive if (getValueString("destination_directory") != "") { // Does the string start with '--' ? if (getValueString("destination_directory").startsWith("--")) { addLogEntry("ERROR: --destination-directory missing a valid entry"); operationalConflictDetected = true; } } // Return bool value indicating if we have an operational conflict return operationalConflictDetected; } // Reset skip_file and skip_dir to application defaults when --force-sync is used void resetSkipToDefaults() { // skip_file if (debugLogging) { addLogEntry("original skip_file: " ~ getValueString("skip_file"), ["debug"]); addLogEntry("resetting skip_file to application defaults", ["debug"]); } setValueString("skip_file", defaultSkipFile); if (debugLogging) {addLogEntry("reset skip_file: " ~ getValueString("skip_file"), ["debug"]);} // skip_dir if (debugLogging) { addLogEntry("original skip_dir: " ~ getValueString("skip_dir"), ["debug"]); addLogEntry("resetting skip_dir to application defaults", ["debug"]); } setValueString("skip_dir", defaultSkipDir); if (debugLogging) {addLogEntry("reset skip_dir: " ~ getValueString("skip_dir"), ["debug"]);} } // Initialise the correct 'sync_dir' expanding any '~' if present string initialiseRuntimeSyncDirectory() { string runtimeSyncDirectory; if (debugLogging) {addLogEntry("sync_dir: Setting runtimeSyncDirectory from config value 'sync_dir'", ["debug"]);} if (!shellEnvironmentSet){ if (debugLogging) {addLogEntry("sync_dir: No SHELL or USER environment variable configuration detected", ["debug"]);} // No shell or user set, so expandTilde() will fail - usually headless system running under init.d / systemd or potentially Docker // Does the 'currently configured' sync_dir include a ~ if (canFind(getValueString("sync_dir"), "~")) { // A ~ was found in sync_dir if (debugLogging) {addLogEntry("sync_dir: A '~' was found in 'sync_dir', using the calculated 'homePath' to replace '~' as no SHELL or USER environment variable set", ["debug"]);} runtimeSyncDirectory = buildNormalizedPath(buildPath(defaultHomePath, strip(getValueString("sync_dir"), "~"))); } else { // No ~ found in sync_dir, use as is if (debugLogging) {addLogEntry("sync_dir: Using configured 'sync_dir' path as-is as no SHELL or USER environment variable configuration detected", ["debug"]);} runtimeSyncDirectory = getValueString("sync_dir"); } } else { // A shell and user environment variable is set, expand any ~ as this will be expanded correctly if present if (canFind(getValueString("sync_dir"), "~")) { if (debugLogging) {addLogEntry("sync_dir: A '~' was found in the configured 'sync_dir', automatically expanding as SHELL and USER environment variable is set", ["debug"]);} runtimeSyncDirectory = expandTilde(getValueString("sync_dir")); } else { // No ~ found in sync_dir, does the path begin with a '/' ? if (debugLogging) {addLogEntry("sync_dir: Using configured 'sync_dir' path as-is as however SHELL or USER environment variable configuration detected - should be placed in USER home directory", ["debug"]);} if (!startsWith(getValueString("sync_dir"), "/")) { if (debugLogging) {addLogEntry("Configured 'sync_dir' does not start with a '/' or '~/' - adjusting configured 'sync_dir' to use User Home Directory as base for 'sync_dir' path", ["debug"]);} string updatedPathWithHome = "~/" ~ getValueString("sync_dir"); runtimeSyncDirectory = expandTilde(updatedPathWithHome); } else { if (debugLogging) {addLogEntry("use 'sync_dir' as is - no touch", ["debug"]);} runtimeSyncDirectory = getValueString("sync_dir"); } } } // What will runtimeSyncDirectory be actually set to? if (debugLogging) {addLogEntry("sync_dir: runtimeSyncDirectory set to: " ~ runtimeSyncDirectory, ["debug"]);} // Configure configuredBusinessSharedFilesDirectoryName configuredBusinessSharedFilesDirectoryName = buildNormalizedPath(buildPath(runtimeSyncDirectory, defaultBusinessSharedFilesDirectoryName)); return runtimeSyncDirectory; } // Initialise the correct 'log_dir' when application logging to a separate file is enabled with 'enable_logging' and expanding any '~' if present string calculateLogDirectory() { string configuredLogDirPath; if (debugLogging) {addLogEntry("log_dir: Setting runtime application log from config value 'log_dir'", ["debug"]);} if (getValueString("log_dir") != defaultLogFileDir) { // User modified 'log_dir' to be used with 'enable_logging' // if 'log_dir' contains a '~' this needs to be expanded correctly if (canFind(getValueString("log_dir"), "~")) { // ~ needs to be expanded correctly if (!shellEnvironmentSet) { // No shell or user environment variable set, so expandTilde() will fail - usually headless system running under init.d / systemd or potentially Docker if (debugLogging) {addLogEntry("log_dir: A '~' was found in log_dir, using the calculated 'homePath' to replace '~' as no SHELL or USER environment variable set", ["debug"]);} configuredLogDirPath = buildNormalizedPath(buildPath(defaultHomePath, strip(getValueString("log_dir"), "~"))); } else { // A shell and user environment variable is set, expand any ~ as this will be expanded correctly if present if (debugLogging) {addLogEntry("log_dir: A '~' was found in the configured 'log_dir', automatically expanding as SHELL and USER environment variable is set", ["debug"]);} configuredLogDirPath = expandTilde(getValueString("log_dir")); } } else { // '~' not found in log_dir entry, use as is configuredLogDirPath = getValueString("log_dir"); } } else { // Default 'log_dir' to be used with 'enable_logging' configuredLogDirPath = defaultLogFileDir; } // Attempt to create 'configuredLogDirPath' otherwise we need to fall back to the users home directory if (!exists(configuredLogDirPath)) { // 'configuredLogDirPath' path does not exist - try and create it try { mkdirRecurse(configuredLogDirPath); } catch (std.file.FileException e) { // We got an error when attempting to create the directory .. addLogEntry(); addLogEntry("ERROR: Unable to create " ~ configuredLogDirPath); addLogEntry("ERROR: Please manually create '" ~ configuredLogDirPath ~ "' and set appropriate permissions to allow write access for your user to this location."); addLogEntry("ERROR: The requested client activity log will instead be located in your users home directory"); addLogEntry(); // Reconfigure 'configuredLogDirPath' to use environment.get("HOME") value, which we have already calculated configuredLogDirPath = defaultHomePath; } } // Return the initialised application log path return configuredLogDirPath; } // What IP protocol is going to be used to access Microsoft OneDrive void displayIPProtocol() { if (getValueLong("ip_protocol_version") == 0) addLogEntry("Using IPv4 and IPv6 (if configured) for all network operations"); if (getValueLong("ip_protocol_version") == 1) addLogEntry("Forcing client to use IPv4 connections only"); if (getValueLong("ip_protocol_version") == 2) addLogEntry("Forcing client to use IPv6 connections only"); } // Has a 'no-sync' task been requested? bool hasNoSyncOperationBeenRequested() { // Are we performing some sort of 'no-sync' task? // - Are we performing a logout? // - Are we performing a reauth? // - Are we obtaining the Office 365 Drive ID for a given Office 365 SharePoint Shared Library? // - Are we displaying the sync status? // - Are we getting the URL for a file online? // - Are we listing who modified a file last online? // - Are we listing OneDrive Business Shared Items? // - Are we creating a shareable link for an existing file on OneDrive? // - Are we just creating a directory online, without any sync being performed? // - Are we just deleting a directory online, without any sync being performed? // - Are we renaming or moving a directory? // - Are we displaying the quota information? bool noSyncOperation = false; // Return a true|false if any of these have been set, so that we use the 'dry-run' DB copy, to execute these tasks, in case the client is currently operational // --logout if (getValueBool("logout")) { // flag that a no sync operation has been requested noSyncOperation = true; } // --reauth if (getValueBool("reauth")) { // flag that a no sync operation has been requested noSyncOperation = true; } // --get-sharepoint-drive-id - Get the SharePoint Library drive_id if (getValueString("sharepoint_library_name") != "") { // flag that a no sync operation has been requested noSyncOperation = true; } // --display-sync-status - Query the sync status if (getValueBool("display_sync_status")) { // flag that a no sync operation has been requested noSyncOperation = true; } // --get-file-link - Get the URL path for a synced file? if (getValueString("get_file_link") != "") { // flag that a no sync operation has been requested noSyncOperation = true; } // --modified-by - Are we listing the modified-by details of a provided path? if (getValueString("modified_by") != "") { // flag that a no sync operation has been requested noSyncOperation = true; } // --list-shared-items - Are we listing OneDrive Business Shared Items if (getValueBool("list_business_shared_items")) { // flag that a no sync operation has been requested noSyncOperation = true; } // --create-share-link - Are we creating a shareable link for an existing file on OneDrive? if (getValueString("create_share_link") != "") { // flag that a no sync operation has been requested noSyncOperation = true; } // --create-directory - Are we just creating a directory online, without any sync being performed? if ((getValueString("create_directory") != "")) { // flag that a no sync operation has been requested noSyncOperation = true; } // --remove-directory - Are we just deleting a directory online, without any sync being performed? if ((getValueString("remove_directory") != "")) { // flag that a no sync operation has been requested noSyncOperation = true; } // Are we renaming or moving a directory online? // onedrive --source-directory 'path/as/source/' --destination-directory 'path/as/destination' if ((getValueString("source_directory") != "") && (getValueString("destination_directory") != "")) { // flag that a no sync operation has been requested noSyncOperation = true; } // Are we displaying the quota information? if (getValueBool("display_quota")) { // flag that a no sync operation has been requested noSyncOperation = true; } // Return result return noSyncOperation; } // Are the required GUI logging environment variables for this user available? // Specifically these must be available: // - XDG_RUNTIME_DIR // - DBUS_SESSION_BUS_ADDRESS bool validateGUINotificationEnvironmentVariables() { bool variablesAvailable = false; string xdg_value; string dbus_value; version(Notifications) { // Check XDG_RUNTIME_DIR environment variable try { xdg_value = environment["XDG_RUNTIME_DIR"]; xdg_exists = true; } catch (Exception e) { xdg_exists = false; } // Check DBUS_SESSION_BUS_ADDRESS environment variable try { dbus_value = environment["DBUS_SESSION_BUS_ADDRESS"]; dbus_exists = true; } catch (Exception e) { dbus_exists = false; } // Output the result if (xdg_exists) { if (debugLogging) {addLogEntry("runtime_environment: XDG_RUNTIME_DIR exists with value: " ~ xdg_value , ["debug"]);} } else { if (debugLogging) {addLogEntry("runtime_environment: XDG_RUNTIME_DIR missing from runtime user environment", ["debug"]);} } if (dbus_exists) { if (debugLogging) {addLogEntry("runtime_environment: DBUS_SESSION_BUS_ADDRESS exists with value: " ~ dbus_value, ["debug"]);} } else { if (debugLogging) {addLogEntry("runtime_environment: DBUS_SESSION_BUS_ADDRESS missing from runtime user environment", ["debug"]);} } // Determine result if (xdg_exists && dbus_exists) { variablesAvailable = true; } else { addLogEntry("WARNING: Required environment variables required to enable GUI Notifications are not present"); variablesAvailable = false; } } // Return result return variablesAvailable; } } // Output the full application help when --help is passed in void outputLongHelp(Option[] opt) { auto argsNeedingOptions = [ "--auth-files", "--auth-response", "--confdir", "--create-directory", "--classify-as-big-delete", "--create-share-link", "--destination-directory", "--get-file-link", "--get-O365-drive-id", "--get-sharepoint-drive-id", "--log-dir", "--min-notify-changes", "--modified-by", "--monitor-interval", "--monitor-log-frequency", "--monitor-fullscan-frequency", "--remove-directory", "--single-directory", "--skip-dir", "--skip-file", "--skip-size", "--source-directory", "--space-reservation", "--syncdir", "--share-password", "--user-agent" ]; writeln(`onedrive - A client for the Microsoft OneDrive Cloud Service Usage: onedrive [options] --sync Do a one time synchronization onedrive [options] --monitor Monitor filesystem and sync regularly onedrive [options] --display-config Display the currently used configuration onedrive [options] --display-sync-status Query OneDrive service and report on pending changes onedrive -h | --help Show this help screen onedrive --version Show version Options: `); foreach (it; opt.sort!("a.optLong < b.optLong")) { writefln(" %s%s%s%s\n %s", it.optLong, it.optShort == "" ? "" : " " ~ it.optShort, argsNeedingOptions.canFind(it.optLong) ? " ARG" : "", it.required ? " (required)" : "", it.help); } } onedrive-2.5.5/src/curlEngine.d000066400000000000000000000572561476564400300164150ustar00rootroot00000000000000// What is this module called? module curlEngine; // What does this module require to function? import std.net.curl; import etc.c.curl; import std.datetime; import std.conv; import std.file; import std.format; import std.json; import std.stdio; import std.range; import core.memory; import core.sys.posix.signal; // What other modules that we have created do we need to import? import log; import util; // Shared pool of CurlEngine instances accessible across all threads __gshared CurlEngine[] curlEnginePool; // __gshared is used to declare a variable that is shared across all threads extern (C) void sigpipeHandler(int signum) { // Custom handler to ignore SIGPIPE signals addLogEntry("ERROR: Handling a cURL SIGPIPE signal despite CURLOPT_NOSIGNAL being set (cURL Operational Bug) ..."); } class CurlResponse { HTTP.Method method; const(char)[] url; const(char)[][const(char)[]] requestHeaders; const(char)[] postBody; bool hasResponse; string[string] responseHeaders; HTTP.StatusLine statusLine; char[] content; this() { reset(); } ~this() { reset(); } void reset() { method = HTTP.Method.undefined; url = ""; requestHeaders = null; postBody = []; hasResponse = false; responseHeaders = null; statusLine.reset(); content = []; } void addRequestHeader(const(char)[] name, const(char)[] value) { requestHeaders[to!string(name)] = to!string(value); } void connect(HTTP.Method method, const(char)[] url) { this.method = method; this.url = url; } const JSONValue json() { JSONValue json; try { json = content.parseJSON(); } catch (JSONException e) { // Log that a JSON Exception was caught, dont output the HTML response from OneDrive if (debugLogging) {addLogEntry("JSON Exception caught when performing HTTP operations - use --debug-https to diagnose further", ["debug"]);} } return json; }; void update(HTTP *http) { hasResponse = true; this.responseHeaders = http.responseHeaders(); this.statusLine = http.statusLine; // has 'microsoftDataCentre' been set yet? if (microsoftDataCentre.empty) { // Extract the 'x-ms-ags-diagnostic' header if it exists if ("x-ms-ags-diagnostic" in this.responseHeaders) { // try and extract the data centre details try { // attempt to extract the data centre location from the header auto diagHeaderData = parseJSON(this.responseHeaders["x-ms-ags-diagnostic"]); string dataCentre = diagHeaderData["ServerInfo"]["DataCenter"].str; // set the Microsoft Data Centre value microsoftDataCentre = dataCentre; } catch (Exception e) { // do nothing } } } // Output the response headers only if using debug mode + debugging https itself if ((debugLogging) && (debugHTTPSResponse)) { addLogEntry("HTTP Response Headers: " ~ to!string(this.responseHeaders), ["debug"]); addLogEntry("HTTP Status Line: " ~ to!string(this.statusLine), ["debug"]); } } @safe pure HTTP.StatusLine getStatus() { return this.statusLine; } // Return the current value of retryAfterValue int getRetryAfterValue() { int delayBeforeRetry; // Is 'retry-after' in the response headers if ("retry-after" in responseHeaders) { // Set the retry-after value if (debugLogging) { addLogEntry("curlEngine.http.perform() => Received a 'Retry-After' Header Response with the following value: " ~ to!string(responseHeaders["retry-after"]), ["debug"]); addLogEntry("curlEngine.http.perform() => Setting retryAfterValue to: " ~ responseHeaders["retry-after"], ["debug"]); } delayBeforeRetry = to!int(responseHeaders["retry-after"]); } else { // Use a 120 second delay as a default given header value was zero // This value is based on log files and data when determining correct process for 429 response handling delayBeforeRetry = 120; // Update that we are over-riding the provided value with a default if (debugLogging) {addLogEntry("HTTP Response Header retry-after value was missing - Using a preconfigured default of: " ~ to!string(delayBeforeRetry), ["debug"]);} } return delayBeforeRetry; } const string parseRequestHeaders(const(const(char)[][const(char)[]]) headers) { string requestHeadersStr = ""; // Ensure response headers is not null and iterate over keys safely. if (headers !is null) { foreach (string header; headers.byKey()) { if (header == "Authorization") { continue; } // Use the 'in' operator to safely check if the key exists in the associative array. if (auto val = header in headers) { requestHeadersStr ~= "< " ~ header ~ ": " ~ *val ~ "\n"; } } } return requestHeadersStr; } const string parseResponseHeaders(const(string[string]) headers) { string responseHeadersStr = ""; // Ensure response headers is not null and iterate over keys safely. if (headers !is null) { foreach (string header; headers.byKey()) { // Check if the key actually exists before accessing it to avoid RangeError. if (auto val = header in headers) { // 'in' checks for the key and returns a pointer to the value if found. responseHeadersStr ~= "> " ~ header ~ ": " ~ *val ~ "\n"; // Dereference pointer to get the value. } } } return responseHeadersStr; } const string dumpDebug() { import std.range; import std.format : format; string str = ""; str ~= format("< %s %s\n", method, url); if (!requestHeaders.empty) { str ~= parseRequestHeaders(requestHeaders); } if (!postBody.empty) { str ~= format("\n----\n%s\n----\n", postBody); } str ~= format("< %s\n", statusLine); if (!responseHeaders.empty) { str ~= parseResponseHeaders(responseHeaders); } return str; } const string dumpResponse() { import std.range; import std.format : format; string str = ""; if (!content.empty) { str ~= format("\n----\n%s\n----\n", content); } return str; } override string toString() const { string str = "Curl debugging: \n"; str ~= dumpDebug(); if (hasResponse) { str ~= "Curl response: \n"; str ~= dumpResponse(); } return str; } } class CurlEngine { HTTP http; File uploadFile; CurlResponse response; bool keepAlive; ulong dnsTimeout; string internalThreadId; SysTime releaseTimestamp; ulong maxIdleTime; this() { http = HTTP(); // Directly initializes HTTP using its default constructor response = null; // Initialize as null internalThreadId = generateAlphanumericString(); // Give this CurlEngine instance a unique ID if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("Created new CurlEngine instance id: " ~ to!string(internalThreadId), ["debug"]);} } // The destructor should only clean up resources owned directly by this CurlEngine instance ~this() { // Is the file still open? if (uploadFile.isOpen()) { uploadFile.close(); } // Is 'response' cleared? object.destroy(response); // Destroy, then set to null response = null; // Is the actual http instance is stopped? if (!http.isStopped) { http.shutdown(); } // Make sure this HTTP instance is destroyed object.destroy(http); // ThreadId needs to be set to null internalThreadId = null; } // We are releasing a curl instance back to the pool void releaseEngine() { // Set timestamp of release releaseTimestamp = Clock.currTime(UTC()); // Log that we are releasing this engine back to the pool if ((debugLogging) && (debugHTTPSResponse)) { addLogEntry("CurlEngine releaseEngine() called on instance id: " ~ to!string(internalThreadId), ["debug"]); addLogEntry("CurlEngine curlEnginePool size before release: " ~ to!string(curlEnginePool.length), ["debug"]); string engineReleaseMessage = format("Release Timestamp for CurlEngine %s: %s", to!string(internalThreadId), to!string(releaseTimestamp)); addLogEntry(engineReleaseMessage, ["debug"]); } // cleanup this curl instance before putting it back in the pool cleanup(true); // Cleanup instance by resetting values and flushing cookie cache synchronized (CurlEngine.classinfo) { curlEnginePool ~= this; if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("CurlEngine curlEnginePool size after release: " ~ to!string(curlEnginePool.length), ["debug"]);} } // Perform Garbage Collection GC.collect(); // Return free memory to the OS GC.minimize(); } // Setup a specific SIGPIPE Signal handler due to curl bugs that ignore CurlOption.nosignal void setupSIGPIPESignalHandler() { // Setup the signal handler sigaction_t curlAction; curlAction.sa_handler = &sigpipeHandler; // Direct function pointer assignment sigaction(SIGPIPE, &curlAction, null); // Broken Pipe signal from curl } // Initialise this curl instance void initialise(ulong dnsTimeout, ulong connectTimeout, ulong dataTimeout, ulong operationTimeout, int maxRedirects, bool httpsDebug, string userAgent, bool httpProtocol, ulong userRateLimit, ulong protocolVersion, ulong maxIdleTime, bool keepAlive=true) { // There are many broken curl versions being used, mainly provided by Ubuntu // Ignore SIGPIPE to prevent the application from exiting without reason with an exit code of 141 when bad curl version generate this signal despite being told not to (CurlOption.nosignal) below setupSIGPIPESignalHandler(); // Setting 'keepAlive' to false ensures that when we close the curl instance, any open sockets are closed - which we need to do when running // multiple threads and API instances at the same time otherwise we run out of local files | sockets pretty quickly this.keepAlive = keepAlive; // Curl DNS Timeout Handling this.dnsTimeout = dnsTimeout; // Curl Timeout Handling this.maxIdleTime = maxIdleTime; // libcurl dns_cache_timeout timeout // https://curl.se/libcurl/c/CURLOPT_DNS_CACHE_TIMEOUT.html // https://dlang.org/library/std/net/curl/http.dns_timeout.html http.dnsTimeout = (dur!"seconds"(dnsTimeout)); // Timeout for HTTPS connections // https://curl.se/libcurl/c/CURLOPT_CONNECTTIMEOUT.html // https://dlang.org/library/std/net/curl/http.connect_timeout.html http.connectTimeout = (dur!"seconds"(connectTimeout)); // Timeout for activity on connection // This is a DMD | DLANG specific item, not a libcurl item // https://dlang.org/library/std/net/curl/http.data_timeout.html // https://raw.githubusercontent.com/dlang/phobos/master/std/net/curl.d - private enum _defaultDataTimeout = dur!"minutes"(2); http.dataTimeout = (dur!"seconds"(dataTimeout)); // Maximum time any operation is allowed to take // This includes dns resolution, connecting, data transfer, etc. // https://curl.se/libcurl/c/CURLOPT_TIMEOUT_MS.html // https://dlang.org/library/std/net/curl/http.operation_timeout.html http.operationTimeout = (dur!"seconds"(operationTimeout)); // Specify how many redirects should be allowed http.maxRedirects(maxRedirects); // Debug HTTPS http.verbose = httpsDebug; // Use the configured 'user_agent' value http.setUserAgent = userAgent; // What IP protocol version should be used when using Curl - IPv4 & IPv6, IPv4 or IPv6 http.handle.set(CurlOption.ipresolve,protocolVersion); // 0 = IPv4 + IPv6, 1 = IPv4 Only, 2 = IPv6 Only // What version of HTTP protocol do we use? // Curl >= 7.62.0 defaults to http2 for a significant number of operations if (httpProtocol) { // Downgrade to HTTP 1.1 - yes version = 2 is HTTP 1.1 http.handle.set(CurlOption.http_version,2); } // Configure upload / download rate limits if configured // 131072 = 128 KB/s - minimum for basic application operations to prevent timeouts // A 0 value means rate is unlimited, and is the curl default if (userRateLimit > 0) { // set rate limit http.handle.set(CurlOption.max_send_speed_large,userRateLimit); http.handle.set(CurlOption.max_recv_speed_large,userRateLimit); } // Explicitly set libcurl options to avoid using signal handlers in a multi-threaded environment // See: https://curl.se/libcurl/c/CURLOPT_NOSIGNAL.html // The CURLOPT_NOSIGNAL option is intended for use in multi-threaded programs to ensure that libcurl does not use any signal handling. // Set CURLOPT_NOSIGNAL to 1 to prevent libcurl from using signal handlers, thus avoiding interference with the application's signal handling which could lead to issues such as unstable behavior or application crashes. http.handle.set(CurlOption.nosignal,1); // https://curl.se/libcurl/c/CURLOPT_TCP_NODELAY.html // Ensure that TCP_NODELAY is set to 0 to ensure that TCP NAGLE is enabled http.handle.set(CurlOption.tcp_nodelay,0); // https://curl.se/libcurl/c/CURLOPT_FORBID_REUSE.html // CURLOPT_FORBID_REUSE - make connection get closed at once after use // Setting this to 0 ensures that we ARE reusing connections (we did this in v2.4.xx) to ensure connections remained open and usable // Setting this to 1 ensures that when we close the curl instance, any open sockets are forced closed when the API curl instance is destroyed // The libcurl default is 0 as per the documentation (to REUSE connections) - ensure we are configuring to reuse sockets http.handle.set(CurlOption.forbid_reuse,0); if (httpsDebug) { // Output what options we are using so that in the debug log this can be tracked if ((debugLogging) && (debugHTTPSResponse)) { addLogEntry("http.dnsTimeout = " ~ to!string(dnsTimeout), ["debug"]); addLogEntry("http.connectTimeout = " ~ to!string(connectTimeout), ["debug"]); addLogEntry("http.dataTimeout = " ~ to!string(dataTimeout), ["debug"]); addLogEntry("http.operationTimeout = " ~ to!string(operationTimeout), ["debug"]); addLogEntry("http.maxRedirects = " ~ to!string(maxRedirects), ["debug"]); addLogEntry("http.CurlOption.ipresolve = " ~ to!string(protocolVersion), ["debug"]); addLogEntry("http.header.Connection.keepAlive = " ~ to!string(keepAlive), ["debug"]); } } } void setResponseHolder(CurlResponse response) { if (response is null) { // Create a response instance if it doesn't already exist if (this.response is null) this.response = new CurlResponse(); } else { this.response = response; } } void addRequestHeader(const(char)[] name, const(char)[] value) { setResponseHolder(null); http.addRequestHeader(name, value); response.addRequestHeader(name, value); } void connect(HTTP.Method method, const(char)[] url) { setResponseHolder(null); if (!keepAlive) addRequestHeader("Connection", "close"); http.method = method; http.url = url; response.connect(method, url); } void setContent(const(char)[] contentType, const(char)[] sendData) { setResponseHolder(null); addRequestHeader("Content-Type", contentType); if (sendData) { http.contentLength = sendData.length; http.onSend = (void[] buf) { import std.algorithm: min; size_t minLen = min(buf.length, sendData.length); if (minLen == 0) return 0; buf[0 .. minLen] = cast(void[]) sendData[0 .. minLen]; sendData = sendData[minLen .. $]; return minLen; }; response.postBody = sendData; } } void setFile(string filepath, string contentRange, ulong offset, ulong offsetSize) { setResponseHolder(null); // open file as read-only in binary mode uploadFile = File(filepath, "rb"); if (contentRange.empty) { offsetSize = uploadFile.size(); } else { addRequestHeader("Content-Range", contentRange); uploadFile.seek(offset); } // Setup progress bar to display http.onProgress = delegate int(size_t dltotal, size_t dlnow, size_t ultotal, size_t ulnow) { return 0; }; addRequestHeader("Content-Type", "application/octet-stream"); http.onSend = data => uploadFile.rawRead(data).length; http.contentLength = offsetSize; } CurlResponse execute() { scope(exit) { cleanup(); } setResponseHolder(null); http.onReceive = (ubyte[] data) { response.content ~= data; // HTTP Server Response Code Debugging if --https-debug is being used return data.length; }; http.perform(); response.update(&http); return response; } CurlResponse download(string originalFilename, string downloadFilename) { setResponseHolder(null); // open downloadFilename as write in binary mode auto file = File(downloadFilename, "wb"); // function scopes scope(exit) { cleanup(); if (file.isOpen()){ // close open file file.close(); } } http.onReceive = (ubyte[] data) { file.rawWrite(data); return data.length; }; http.perform(); // Rename downloaded file rename(downloadFilename, originalFilename); response.update(&http); return response; } // Cleanup this instance internal variables that may have been set void cleanup(bool flushCookies = false) { // Reset any values to defaults, freeing any set objects if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("CurlEngine cleanup() called on instance id: " ~ to!string(internalThreadId), ["debug"]);} // Is the instance is stopped? if (!http.isStopped) { // A stopped instance is not usable, these cannot be reset http.clearRequestHeaders(); http.onSend = null; http.onReceive = null; http.onReceiveHeader = null; http.onReceiveStatusLine = null; http.onProgress = delegate int(size_t dltotal, size_t dlnow, size_t ultotal, size_t ulnow) { return 0; }; http.contentLength = 0; // We only do this if we are pushing the curl engine back to the curl pool if (flushCookies) { // Flush the cookie cache as well http.flushCookieJar(); http.clearSessionCookies(); http.clearAllCookies(); } } // set the response to null response = null; // close file if open if (uploadFile.isOpen()){ // close open file uploadFile.close(); } } // Shut down the curl instance & close any open sockets void shutdownCurlHTTPInstance() { // Log that we are attempting to shutdown this curl instance if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("CurlEngine shutdownCurlHTTPInstance() called on instance id: " ~ to!string(internalThreadId), ["debug"]);} // Is this curl instance is stopped? if (!http.isStopped) { if ((debugLogging) && (debugHTTPSResponse)) { addLogEntry("HTTP instance still active: " ~ to!string(internalThreadId), ["debug"]); addLogEntry("HTTP instance isStopped state before http.shutdown(): " ~ to!string(http.isStopped), ["debug"]); } http.shutdown(); if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("HTTP instance isStopped state post http.shutdown(): " ~ to!string(http.isStopped), ["debug"]);} object.destroy(http); // Destroy, however we cant set to null if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("HTTP instance shutdown and destroyed: " ~ to!string(internalThreadId), ["debug"]);} } else { // Already stopped .. destroy it object.destroy(http); // Destroy, however we cant set to null if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("Stopped HTTP instance shutdown and destroyed: " ~ to!string(internalThreadId), ["debug"]);} } // Perform Garbage Collection GC.collect(); // Return free memory to the OS GC.minimize(); } } // Methods to control obtaining and releasing a CurlEngine instance from the curlEnginePool // Get a curl instance for the OneDrive API to use CurlEngine getCurlInstance() { if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("CurlEngine getCurlInstance() called", ["debug"]);} synchronized (CurlEngine.classinfo) { // What is the current pool size if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("CurlEngine curlEnginePool current size: " ~ to!string(curlEnginePool.length), ["debug"]);} if (curlEnginePool.empty) { if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("CurlEngine curlEnginePool is empty - constructing a new CurlEngine instance", ["debug"]);} return new CurlEngine; // Constructs a new CurlEngine with a fresh HTTP instance } else { CurlEngine curlEngine = curlEnginePool[$ - 1]; curlEnginePool.popBack(); // assumes a LIFO (last-in, first-out) usage pattern // Is this engine stopped? if (curlEngine.http.isStopped) { // return a new curl engine as a stopped one cannot be used if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("CurlEngine was in a stopped state (not usable) - constructing a new CurlEngine instance", ["debug"]);} return new CurlEngine; // Constructs a new CurlEngine with a fresh HTTP instance } else { // When was this engine last used? auto elapsedTime = Clock.currTime(UTC()) - curlEngine.releaseTimestamp; if ((debugLogging) && (debugHTTPSResponse)) { string engineIdleMessage = format("CurlEngine %s time since last use: %s", to!string(curlEngine.internalThreadId), to!string(elapsedTime)); addLogEntry(engineIdleMessage, ["debug"]); } // If greater than 120 seconds (default), the treat this as a stale engine, preventing: // * Too old connection (xxx seconds idle), disconnect it // * Connection 0 seems to be dead! // * Closing connection 0 if (elapsedTime > dur!"seconds"(curlEngine.maxIdleTime)) { // Too long idle engine, clean it up and create a new one if ((debugLogging) && (debugHTTPSResponse)) { string curlTooOldMessage = format("CurlEngine idle for > %d seconds .... destroying and returning a new curl engine instance", curlEngine.maxIdleTime); addLogEntry(curlTooOldMessage, ["debug"]); } curlEngine.cleanup(true); // Cleanup instance by resetting values and flushing cookie cache curlEngine.shutdownCurlHTTPInstance(); // Assume proper cleanup of any resources used by HTTP if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("Returning NEW curlEngine instance", ["debug"]);} return new CurlEngine; // Constructs a new CurlEngine with a fresh HTTP instance } else { // return an existing curl engine if ((debugLogging) && (debugHTTPSResponse)) { addLogEntry("CurlEngine was in a valid state - returning existing CurlEngine instance", ["debug"]); addLogEntry("Using CurlEngine instance ID: " ~ curlEngine.internalThreadId, ["debug"]); } // return the existing engine return curlEngine; } } } } } // Release all CurlEngine instances void releaseAllCurlInstances() { if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("CurlEngine releaseAllCurlInstances() called", ["debug"]);} synchronized (CurlEngine.classinfo) { // What is the current pool size if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("CurlEngine curlEnginePool size to release: " ~ to!string(curlEnginePool.length), ["debug"]);} if (curlEnginePool.length > 0) { // Safely iterate and clean up each CurlEngine instance foreach (curlEngineInstance; curlEnginePool) { try { curlEngineInstance.cleanup(true); // Cleanup instance by resetting values and flushing cookie cache curlEngineInstance.shutdownCurlHTTPInstance(); // Assume proper cleanup of any resources used by HTTP } catch (Exception e) { // Log the error or handle it appropriately // e.g., writeln("Error during cleanup/shutdown: ", e.toString()); } // It's safe to destroy the object here assuming no other references exist object.destroy(curlEngineInstance); // Destroy, then set to null curlEngineInstance = null; // Perform Garbage Collection on this destroyed curl engine GC.collect(); // Log release if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("CurlEngine destroyed", ["debug"]);} } // Clear the array after all instances have been handled curlEnginePool.length = 0; // More explicit than curlEnginePool = []; } } // Perform Garbage Collection on the destroyed curl engines GC.collect(); // Return free memory to the OS GC.minimize(); // Log that all curl engines have been released if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("CurlEngine releaseAllCurlInstances() completed", ["debug"]);} } // Return how many curl engines there are ulong curlEnginePoolLength() { return curlEnginePool.length; }onedrive-2.5.5/src/itemdb.d000066400000000000000000001230651476564400300155560ustar00rootroot00000000000000// What is this module called? module itemdb; // What does this module require to function? import std.datetime; import std.exception; import std.path; import std.string; import std.stdio; import std.algorithm.searching; import core.stdc.stdlib; import std.json; import std.conv; import core.sync.mutex; // What other modules that we have created do we need to import? import sqlite; import util; import log; enum ItemType { none, file, dir, remote, root, unknown } struct Item { string driveId; string id; string name; string remoteName; ItemType type; string eTag; string cTag; SysTime mtime; string parentId; string quickXorHash; string sha256Hash; string remoteDriveId; string remoteParentId; string remoteId; ItemType remoteType; string syncStatus; string size; } // Construct an Item DB struct from a JSON driveItem Item makeDatabaseItem(JSONValue driveItem) { Item item = { id: driveItem["id"].str, name: "name" in driveItem ? driveItem["name"].str : null, // name may be missing for deleted files in OneDrive Business eTag: "eTag" in driveItem ? driveItem["eTag"].str : null, // eTag is not returned for the root in OneDrive Business cTag: "cTag" in driveItem ? driveItem["cTag"].str : null, // cTag is missing in old files (and all folders in OneDrive Business) remoteName: "actualOnlineName" in driveItem ? driveItem["actualOnlineName"].str : null, // actualOnlineName is only used with OneDrive Business Shared Folders }; // OneDrive API Change: https://github.com/OneDrive/onedrive-api-docs/issues/834 // OneDrive no longer returns lastModifiedDateTime if the item is deleted by OneDrive if(isItemDeleted(driveItem)) { // Set mtime to SysTime(0) item.mtime = SysTime(0); } else { // Item is not in a deleted state string lastModifiedTimestamp; // Resolve 'Key not found: fileSystemInfo' when then item is a remote item // https://github.com/abraunegg/onedrive/issues/11 if (isItemRemote(driveItem)) { // remoteItem is a OneDrive object that exists on a 'different' OneDrive drive id, when compared to account default // Normally, the 'remoteItem' field will contain 'fileSystemInfo' however, if the user uses the 'Add Shortcut ..' option in OneDrive WebUI // to create a 'link', this object, whilst remote, does not have 'fileSystemInfo' in the expected place, thus leading to a application crash // See: https://github.com/abraunegg/onedrive/issues/1533 if ("fileSystemInfo" in driveItem["remoteItem"]) { // 'fileSystemInfo' is in 'remoteItem' which will be the majority of cases lastModifiedTimestamp = strip(driveItem["remoteItem"]["fileSystemInfo"]["lastModifiedDateTime"].str); // is lastModifiedTimestamp valid? if (isValidUTCDateTime(lastModifiedTimestamp)) { // string is a valid timestamp item.mtime = SysTime.fromISOExtString(lastModifiedTimestamp); } else { // invalid timestamp from JSON file addLogEntry("WARNING: Invalid timestamp provided by the Microsoft OneDrive API: " ~ lastModifiedTimestamp); // Set mtime to Clock.currTime(UTC()) to ensure we have a valid UTC value item.mtime = Clock.currTime(UTC()); } } else { // is a remote item, but 'fileSystemInfo' is missing from 'remoteItem' lastModifiedTimestamp = strip(driveItem["fileSystemInfo"]["lastModifiedDateTime"].str); // is lastModifiedTimestamp valid? if (isValidUTCDateTime(lastModifiedTimestamp)) { // string is a valid timestamp item.mtime = SysTime.fromISOExtString(lastModifiedTimestamp); } else { // invalid timestamp from JSON file addLogEntry("WARNING: Invalid timestamp provided by the Microsoft OneDrive API: " ~ lastModifiedTimestamp); // Set mtime to Clock.currTime(UTC()) to ensure we have a valid UTC value item.mtime = Clock.currTime(UTC()); } } } else { // Does fileSystemInfo exist at all ? if ("fileSystemInfo" in driveItem) { // fileSystemInfo exists lastModifiedTimestamp = strip(driveItem["fileSystemInfo"]["lastModifiedDateTime"].str); // is lastModifiedTimestamp valid? if (isValidUTCDateTime(lastModifiedTimestamp)) { // string is a valid timestamp item.mtime = SysTime.fromISOExtString(lastModifiedTimestamp); } else { // invalid timestamp from JSON file addLogEntry("WARNING: Invalid timestamp provided by the Microsoft OneDrive API: " ~ lastModifiedTimestamp); // Set mtime to Clock.currTime(UTC()) to ensure we have a valid UTC value item.mtime = Clock.currTime(UTC()); } } else { // no timestamp from JSON file addLogEntry("WARNING: No timestamp provided by the Microsoft OneDrive API - using current system time for item!"); // Set mtime to Clock.currTime(UTC()) to ensure we have a valid UTC value item.mtime = Clock.currTime(UTC()); } } } // Set this item object type bool typeSet = false; if (isItemFile(driveItem)) { // 'file' object exists in the JSON if (debugLogging) {addLogEntry("Flagging database item.type as a file", ["debug"]);} typeSet = true; item.type = ItemType.file; } if (isItemFolder(driveItem)) { // 'folder' object exists in the JSON if (debugLogging) {addLogEntry("Flagging database item.type as a directory", ["debug"]);} typeSet = true; item.type = ItemType.dir; } if (isItemRemote(driveItem)) { // 'remote' object exists in the JSON if (debugLogging) {addLogEntry("Flagging database item.type as a remote", ["debug"]);} typeSet = true; item.type = ItemType.remote; } // root and remote items do not have parentReference if (!isItemRoot(driveItem) && ("parentReference" in driveItem) != null) { item.driveId = driveItem["parentReference"]["driveId"].str; if (hasParentReferenceId(driveItem)) { item.parentId = driveItem["parentReference"]["id"].str; } } // extract the file hash and file size if (isItemFile(driveItem) && ("hashes" in driveItem["file"])) { // Get file size if (hasFileSize(driveItem)) { item.size = to!string(driveItem["size"].integer); // Get quickXorHash as default if ("quickXorHash" in driveItem["file"]["hashes"]) { item.quickXorHash = driveItem["file"]["hashes"]["quickXorHash"].str; } else { if (debugLogging) {addLogEntry("quickXorHash is missing from " ~ driveItem["id"].str, ["debug"]);} } // If quickXorHash is empty .. if (item.quickXorHash.empty) { // Is there a sha256Hash? if ("sha256Hash" in driveItem["file"]["hashes"]) { item.sha256Hash = driveItem["file"]["hashes"]["sha256Hash"].str; } else { if (debugLogging) {addLogEntry("sha256Hash is missing from " ~ driveItem["id"].str, ["debug"]);} } } } else { // So that we have at least a zero value here as the API provided no 'size' data for this file item item.size = "0"; } } // Is the object a remote drive item - living on another driveId ? if (isItemRemote(driveItem)) { // Check and assign remoteDriveId if ("parentReference" in driveItem["remoteItem"] && "driveId" in driveItem["remoteItem"]["parentReference"]) { item.remoteDriveId = driveItem["remoteItem"]["parentReference"]["driveId"].str; } // Check and assign remoteParentId if ("parentReference" in driveItem["remoteItem"] && "id" in driveItem["remoteItem"]["parentReference"]) { item.remoteParentId = driveItem["remoteItem"]["parentReference"]["id"].str; } // Check and assign remoteId if ("id" in driveItem["remoteItem"]) { item.remoteId = driveItem["remoteItem"]["id"].str; } // Check and assign remoteType if ("file" in driveItem["remoteItem"].object) { item.remoteType = ItemType.file; } else { item.remoteType = ItemType.dir; } } // We have 4 different operational modes where 'item.syncStatus' is used to flag if an item is synced or not: // - National Cloud Deployments do not support /delta as a query // - When using --single-directory // - When using --download-only --cleanup-local-files // - Are we scanning a Shared Folder // // Thus we need to track in the database that this item is in sync // As we are making an item, set the syncStatus to Y // ONLY when either of the three modes above are being used, all the existing DB entries will get set to N // so when processing /children, it can be identified what the 'deleted' difference is item.syncStatus = "Y"; // Return the created item return item; } final class ItemDatabase { // increment this for every change in the db schema immutable int itemDatabaseVersion = 16; Database db; string insertItemStmt; string updateItemStmt; string selectItemByIdStmt; string selectItemByRemoteIdStmt; string selectItemByRemoteDriveIdStmt; string selectItemByParentIdStmt; string selectRemoteTypeByNameStmt; string selectRemoteTypeByRemoteDriveIdStmt; string deleteItemByIdStmt; bool databaseInitialised = false; private Mutex databaseLock; this(string filename) { // Initialise the mutex databaseLock = new Mutex(); db = Database(filename); int dbVersion; try { dbVersion = db.getVersion(); } catch (SqliteException exception) { // An error was generated - what was the error? // - database is locked if (exception.msg == "database is locked" || exception.errorCode == 5) { addLogEntry(); addLogEntry("ERROR: The 'onedrive' application is already running - please check system process list for active application instances" , ["info", "notify"]); addLogEntry(" - Use 'sudo ps aufxw | grep onedrive' to potentially determine active running process"); addLogEntry(); } else { // A different error .. detail the message, detail the actual SQLite Error Code to assist with troubleshooting addLogEntry(); addLogEntry("ERROR: An internal database error occurred: " ~ exception.msg ~ " (SQLite Error Code: " ~ to!string(exception.errorCode) ~ ")"); addLogEntry(); // Give the user some additional information and pointers on this error // The below list is based on user issue / discussion reports since 2018 switch (exception.errorCode) { case 7: // SQLITE_NOMEM addLogEntry("The operation could not be completed due to insufficient memory. Please close unnecessary applications to free up memory and try again.", ["info", "notify"]); break; case 10: // SQLITE_IOERR addLogEntry("A disk I/O error occurred. This could be due to issues with the storage medium (e.g., disk full, hardware failure, filesystem corruption).\nPlease check your disk's health using a disk utility tool, ensure there is enough free space, and check the filesystem for errors.", ["info", "notify"]); break; case 11: // SQLITE_CORRUPT addLogEntry("The database file appears to be corrupt. This could be due to incomplete or failed writes, hardware issues, or unexpected interruptions during database operations.\nPlease perform a --resync operation.", ["info", "notify"]); break; case 14: // SQLITE_CANTOPEN addLogEntry("The database file could not be opened. Please check that the database file exists, has the correct permissions, and is not being blocked by another process or security software.", ["info", "notify"]); break; case 26: // SQLITE_NOTADB addLogEntry("The database file that attempted to be opened does not appear to be a valid SQLite database, or it may have been corrupted to a point where it's no longer recognisable.\nPlease check your application configuration directory and/or perform a --resync operation.", ["info", "notify"]); break; default: addLogEntry("An unexpected error occurred. Please consult the application documentation or request support to resolve this issue.", ["info", "notify"]); break; } // Blank line before exit addLogEntry(); } return; } if (dbVersion == 0) { createTable(); } else if (db.getVersion() != itemDatabaseVersion) { addLogEntry("The item database is incompatible, re-creating database table structures"); db.dropTableIfExists("item"); // Check and drop table if it exists createTable(); } // What is the threadsafe value auto threadsafeValue = db.getThreadsafeValue(); if (debugLogging) {addLogEntry("SQLite Threadsafe database value: " ~ to!string(threadsafeValue), ["debug"]);} try { // Set the enforcement of foreign key constraints. // https://www.sqlite.org/pragma.html#pragma_foreign_keys // PRAGMA foreign_keys = boolean; db.exec("PRAGMA foreign_keys = TRUE;"); // Set the recursive trigger capability // https://www.sqlite.org/pragma.html#pragma_recursive_triggers // PRAGMA recursive_triggers = boolean; db.exec("PRAGMA recursive_triggers = TRUE;"); // Set the journal mode for databases associated with the current connection // https://www.sqlite.org/pragma.html#pragma_journal_mode db.exec("PRAGMA journal_mode = WAL;"); // Automatic indexing is enabled by default as of version 3.7.17 // https://www.sqlite.org/pragma.html#pragma_automatic_index // PRAGMA automatic_index = boolean; db.exec("PRAGMA automatic_index = FALSE;"); // Tell SQLite to store temporary tables in memory. This will speed up many read operations that rely on temporary tables, indices, and views. // https://www.sqlite.org/pragma.html#pragma_temp_store db.exec("PRAGMA temp_store = MEMORY;"); // Tell SQlite to cleanup database table size // https://www.sqlite.org/pragma.html#pragma_auto_vacuum // PRAGMA schema.auto_vacuum = 0 | NONE | 1 | FULL | 2 | INCREMENTAL; db.exec("PRAGMA auto_vacuum = FULL;"); // This pragma sets or queries the database connection locking-mode. The locking-mode is either NORMAL or EXCLUSIVE. // https://www.sqlite.org/pragma.html#pragma_locking_mode // PRAGMA schema.locking_mode = NORMAL | EXCLUSIVE db.exec("PRAGMA locking_mode = EXCLUSIVE;"); // The synchronous setting determines how carefully SQLite writes data to disk, balancing between performance and data safety. // https://sqlite.org/pragma.html#pragma_synchronous // PRAGMA synchronous = 0 | OFF | 1 | NORMAL | 2 | FULL | 3 | EXTRA; db.exec("PRAGMA synchronous=FULL;"); // Leave this as FULL, this is the sqlite default, ensure this is set to FULL } catch (SqliteException exception) { detailSQLErrorMessage(exception); } insertItemStmt = " INSERT OR REPLACE INTO item (driveId, id, name, remoteName, type, eTag, cTag, mtime, parentId, quickXorHash, sha256Hash, remoteDriveId, remoteParentId, remoteId, remoteType, syncStatus, size) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17) "; updateItemStmt = " UPDATE item SET name = ?3, remoteName = ?4, type = ?5, eTag = ?6, cTag = ?7, mtime = ?8, parentId = ?9, quickXorHash = ?10, sha256Hash = ?11, remoteDriveId = ?12, remoteParentId = ?13, remoteId = ?14, remoteType = ?15, syncStatus = ?16, size = ?17 WHERE driveId = ?1 AND id = ?2 "; selectItemByIdStmt = " SELECT * FROM item WHERE driveId = ?1 AND id = ?2 "; selectItemByRemoteIdStmt = " SELECT * FROM item WHERE remoteDriveId = ?1 AND remoteId = ?2 "; selectItemByRemoteDriveIdStmt = " SELECT * FROM item WHERE remoteDriveId = ?1 "; selectRemoteTypeByNameStmt = " SELECT * FROM item WHERE type = 'remote' AND name = ?1 "; selectRemoteTypeByRemoteDriveIdStmt = " SELECT * FROM item WHERE type = 'remote' AND remoteDriveId = ?1 AND remoteId = ?2 "; selectItemByParentIdStmt = "SELECT * FROM item WHERE driveId = ? AND parentId = ?"; deleteItemByIdStmt = "DELETE FROM item WHERE driveId = ? AND id = ?"; // flag that the database is accessible and we have control databaseInitialised = true; } ~this() { closeDatabaseFile(); } bool isDatabaseInitialised() { return databaseInitialised; } void closeDatabaseFile() { if (databaseInitialised) { db.close(); } databaseInitialised = false; } void createTable() { db.exec("CREATE TABLE item ( driveId TEXT NOT NULL, id TEXT NOT NULL, name TEXT NOT NULL, remoteName TEXT, type TEXT NOT NULL, eTag TEXT, cTag TEXT, mtime TEXT NOT NULL, parentId TEXT, quickXorHash TEXT, sha256Hash TEXT, remoteDriveId TEXT, remoteParentId TEXT, remoteId TEXT, remoteType TEXT, deltaLink TEXT, syncStatus TEXT, size TEXT, PRIMARY KEY (driveId, id), FOREIGN KEY (driveId, parentId) REFERENCES item (driveId, id) ON DELETE CASCADE ON UPDATE RESTRICT )"); db.exec("CREATE INDEX name_idx ON item (name)"); db.exec("CREATE INDEX remote_idx ON item (remoteDriveId, remoteId)"); db.exec("CREATE INDEX item_children_idx ON item (driveId, parentId)"); db.exec("CREATE INDEX selectByPath_idx ON item (name, driveId, parentId)"); db.setVersion(itemDatabaseVersion); } void detailSQLErrorMessage(SqliteException exception) { addLogEntry(); addLogEntry("A database statement execution error occurred: " ~ exception.msg); addLogEntry(); switch (exception.errorCode) { case 7: // SQLITE_FULL case 10: // SQLITE_SCHEMA case 11: // SQLITE_CORRUPT case 17: // SQLITE_IOERR case 21: // SQLITE_NOMEM case 22: // SQLITE_MISUSE case 26: // SQLITE_NOTADB case 27: // SQLITE_CANTOPEN addLogEntry("Fatal SQLite error encountered. Error code: " ~ to!string(exception.errorCode), ["info", "notify"]); addLogEntry(); // Must exit here forceExit(); // This line is needed, even though the application technically never gets here .. // - Error: switch case fallthrough - use 'goto default;' if intended goto default; default: addLogEntry("Please restart the application with --resync to potentially fix any local database issues."); // Handle non-fatal errors or continue execution break; } } void insert(const ref Item item) { synchronized(databaseLock) { auto p = db.prepare(insertItemStmt); scope(exit) p.finalise(); // Ensure that the prepared statement is finalised after execution. try { bindItem(item, p); p.exec(); } catch (SqliteException exception) { detailSQLErrorMessage(exception); } } } void update(const ref Item item) { synchronized(databaseLock) { auto p = db.prepare(updateItemStmt); scope(exit) p.finalise(); // Ensure that the prepared statement is finalised after execution. try { bindItem(item, p); p.exec(); } catch (SqliteException exception) { detailSQLErrorMessage(exception); } } } void dump_open_statements() { synchronized(databaseLock) { db.dump_open_statements(); } } int db_checkpoint() { synchronized(databaseLock) { return db.db_checkpoint(); } } void upsert(const ref Item item) { synchronized(databaseLock) { Statement selectStmt = db.prepare("SELECT COUNT(*) FROM item WHERE driveId = ? AND id = ?"); Statement executionStmt = Statement.init; // Initialise executionStmt to avoid uninitialised variable usage scope(exit) { selectStmt.finalise(); executionStmt.finalise(); } try { selectStmt.bind(1, item.driveId); selectStmt.bind(2, item.id); auto result = selectStmt.exec(); size_t count = result.front[0].to!size_t; if (count == 0) { executionStmt = db.prepare(insertItemStmt); } else { executionStmt = db.prepare(updateItemStmt); } bindItem(item, executionStmt); executionStmt.exec(); } catch (SqliteException exception) { // Handle errors appropriately detailSQLErrorMessage(exception); } } } Item[] selectChildren(const(char)[] driveId, const(char)[] id) { synchronized(databaseLock) { Item[] items; auto p = db.prepare(selectItemByParentIdStmt); scope(exit) p.finalise(); // Ensure that the prepared statement is finalised after execution. try { p.bind(1, driveId); p.bind(2, id); auto res = p.exec(); while (!res.empty) { items ~= buildItem(res); res.step(); } return items; } catch (SqliteException exception) { // Handle errors appropriately detailSQLErrorMessage(exception); items = []; return items; // Return an empty array on error } } } bool selectById(const(char)[] driveId, const(char)[] id, out Item item) { synchronized(databaseLock) { auto p = db.prepare(selectItemByIdStmt); scope(exit) p.finalise(); // Ensure that the prepared statement is finalised after execution. try { p.bind(1, driveId); p.bind(2, id); auto r = p.exec(); if (!r.empty) { item = buildItem(r); return true; } } catch (SqliteException exception) { // Handle the error appropriately detailSQLErrorMessage(exception); } return false; } } bool selectByRemoteId(const(char)[] remoteDriveId, const(char)[] remoteId, out Item item) { synchronized(databaseLock) { auto p = db.prepare(selectItemByRemoteIdStmt); scope(exit) p.finalise(); // Ensure that the prepared statement is finalised after execution. try { p.bind(1, remoteDriveId); p.bind(2, remoteId); auto r = p.exec(); if (!r.empty) { item = buildItem(r); return true; } } catch (SqliteException exception) { // Handle the error appropriately detailSQLErrorMessage(exception); } return false; } } // This should return the 'remote' DB entry for a given remote drive id bool selectByRemoteDriveId(const(char)[] remoteDriveId, out Item item) { synchronized(databaseLock) { auto p = db.prepare(selectItemByRemoteDriveIdStmt); scope(exit) p.finalise(); // Ensure that the prepared statement is finalised after execution. try { p.bind(1, remoteDriveId); auto r = p.exec(); if (!r.empty) { item = buildItem(r); return true; } } catch (SqliteException exception) { // Handle the error appropriately detailSQLErrorMessage(exception); } return false; } } // This should return the 'remote' DB entry for the given 'name' bool selectByRemoteEntryByName(const(char)[] entryName, out Item item) { synchronized(databaseLock) { auto p = db.prepare(selectRemoteTypeByNameStmt); scope(exit) p.finalise(); // Ensure that the prepared statement is finalised after execution. try { p.bind(1, entryName); auto r = p.exec(); if (!r.empty) { item = buildItem(r); return true; } } catch (SqliteException exception) { // Handle the error appropriately detailSQLErrorMessage(exception); } return false; } } // This should return the 'remote' DB entry for the given 'remoteDriveId' and 'remoteId' bool selectRemoteTypeByRemoteDriveId(const(char)[] remoteDriveId, const(char)[] remoteId, out Item item) { synchronized(databaseLock) { auto p = db.prepare(selectRemoteTypeByRemoteDriveIdStmt); scope(exit) p.finalise(); // Ensure that the prepared statement is finalised after execution. try { p.bind(1, remoteDriveId); p.bind(2, remoteId); auto r = p.exec(); if (!r.empty) { item = buildItem(r); return true; } } catch (SqliteException exception) { // Handle the error appropriately detailSQLErrorMessage(exception); } return false; } } // returns true if an item id is in the database bool idInLocalDatabase(const(string) driveId, const(string) id) { synchronized(databaseLock) { auto p = db.prepare(selectItemByIdStmt); scope(exit) p.finalise(); // Ensure that the prepared statement is finalised after execution. try { p.bind(1, driveId); p.bind(2, id); auto r = p.exec(); return !r.empty; } catch (SqliteException exception) { // Handle the error appropriately detailSQLErrorMessage(exception); return false; } } } // returns the item with the given path // the path is relative to the sync directory ex: "./Music/file_name.mp3" bool selectByPath(const(char)[] path, string rootDriveId, out Item item) { synchronized(databaseLock) { Item currItem = { driveId: rootDriveId }; // Issue https://github.com/abraunegg/onedrive/issues/578 path = "root/" ~ (startsWith(path, "./") || path == "." ? path.chompPrefix(".") : path); auto s = db.prepare("SELECT * FROM item WHERE name = ?1 AND driveId IS ?2 AND parentId IS ?3"); scope(exit) s.finalise(); // Ensure that the prepared statement is finalised after execution. try { foreach (name; pathSplitter(path)) { s.bind(1, name); s.bind(2, currItem.driveId); s.bind(3, currItem.id); auto r = s.exec(); if (r.empty) return false; currItem = buildItem(r); // If the item is of type remote, substitute it with the child if (currItem.type == ItemType.remote) { if (debugLogging) {addLogEntry("Record is a Remote Object: " ~ to!string(currItem), ["debug"]);} Item child; if (selectById(currItem.remoteDriveId, currItem.remoteId, child)) { assert(child.type != ItemType.remote, "The type of the child cannot be remote"); currItem = child; if (debugLogging) {addLogEntry("Selecting Record that is NOT Remote Object: " ~ to!string(currItem), ["debug"]);} } } } item = currItem; return true; } catch (SqliteException exception) { // Handle the error appropriately detailSQLErrorMessage(exception); return false; } } } // same as selectByPath() but it does not traverse remote folders, returns the remote element if that is what is required bool selectByPathIncludingRemoteItems(const(char)[] path, string rootDriveId, out Item item) { synchronized(databaseLock) { Item currItem = { driveId: rootDriveId }; // Issue https://github.com/abraunegg/onedrive/issues/578 path = "root/" ~ (startsWith(path, "./") || path == "." ? path.chompPrefix(".") : path); auto s = db.prepare("SELECT * FROM item WHERE name IS ?1 AND driveId IS ?2 AND parentId IS ?3"); scope(exit) s.finalise(); // Ensure that the prepared statement is finalised after execution. try { foreach (name; pathSplitter(path)) { s.bind(1, name); s.bind(2, currItem.driveId); s.bind(3, currItem.id); auto r = s.exec(); if (r.empty) return false; currItem = buildItem(r); } if (currItem.type == ItemType.remote) { if (debugLogging) {addLogEntry("Record selected is a Remote Object: " ~ to!string(currItem), ["debug"]);} } item = currItem; return true; } catch (SqliteException exception) { // Handle the error appropriately detailSQLErrorMessage(exception); return false; } } } void deleteById(const(char)[] driveId, const(char)[] id) { synchronized(databaseLock) { auto p = db.prepare(deleteItemByIdStmt); scope(exit) p.finalise(); // Ensure that the prepared statement is finalised after execution. try { p.bind(1, driveId); p.bind(2, id); p.exec(); } catch (SqliteException exception) { // Handle the error appropriately detailSQLErrorMessage(exception); } } } private void bindItem(const ref Item item, ref Statement stmt) { with (stmt) with (item) { bind(1, driveId); bind(2, id); bind(3, name); bind(4, remoteName); // type handling string typeStr = null; final switch (type) with (ItemType) { case file: typeStr = "file"; break; case dir: typeStr = "dir"; break; case remote: typeStr = "remote"; break; case root: typeStr = "root"; break; case unknown: typeStr = "unknown"; break; case none: typeStr = null; break; } bind(5, typeStr); bind(6, eTag); bind(7, cTag); bind(8, mtime.toISOExtString()); bind(9, parentId); bind(10, quickXorHash); bind(11, sha256Hash); bind(12, remoteDriveId); bind(13, remoteParentId); bind(14, remoteId); // remoteType handling string remoteTypeStr = null; final switch (remoteType) with (ItemType) { case file: remoteTypeStr = "file"; break; case dir: remoteTypeStr = "dir"; break; case remote: remoteTypeStr = "remote"; break; case root: remoteTypeStr = "root"; break; case unknown: remoteTypeStr = "unknown"; break; case none: remoteTypeStr = null; break; } bind(15, remoteTypeStr); bind(16, syncStatus); bind(17, size); } } private Item buildItem(Statement.Result result) { assert(!result.empty, "The result must not be empty"); assert(result.front.length == 18, "The result must have 18 columns"); // Check the DB record timestamp entry. Rather than assert(), use forceExit() and exit in a more graceful manner // - empty values // - 2024-11-23T01:16:14\x80Z // - ��Ϣc (#3014) // - ����� (#2876) // - non timestamp formatted strings such as 'CurlEngine curlEngin' (#2813) if (!isValidUTCDateTime(result.front[7].dup)) { addLogEntry(); addLogEntry("FATAL: The DB record mtime entry is not a valid ISO timestamp entry. Please attempt a --resync to fix the local database."); addLogEntry(); // Must force exit here, allow logging to be done forceExit(); } Item item = { // column 0: driveId // column 1: id // column 2: name // column 3: remoteName - only used when there is a difference in the local name & remote shared folder name // column 4: type // column 5: eTag // column 6: cTag // column 7: mtime // column 8: parentId // column 9: quickXorHash // column 10: sha256Hash // column 11: remoteDriveId // column 12: remoteParentId // column 13: remoteId // column 14: remoteType // column 15: deltaLink // column 16: syncStatus // column 17: size driveId: result.front[0].dup, id: result.front[1].dup, name: result.front[2].dup, remoteName: result.front[3].dup, // Column 4 is type - not set here eTag: result.front[5].dup, cTag: result.front[6].dup, mtime: SysTime.fromISOExtString(result.front[7].dup), parentId: result.front[8].dup, quickXorHash: result.front[9].dup, sha256Hash: result.front[10].dup, remoteDriveId: result.front[11].dup, remoteParentId: result.front[12].dup, remoteId: result.front[13].dup, // Column 14 is remoteType - not set here // Column 15 is deltaLink - not set here syncStatus: result.front[16].dup, size: result.front[17].dup }; // Configure item.type switch (result.front[4]) { case "file": item.type = ItemType.file; break; case "dir": item.type = ItemType.dir; break; case "remote": item.type = ItemType.remote; break; case "root": item.type = ItemType.root; break; default: assert(0, "Invalid item type"); } // Configure item.remoteType switch (result.front[14]) { // We only care about 'dir' and 'file' for 'remote' items case "file": item.remoteType = ItemType.file; break; case "dir": item.remoteType = ItemType.dir; break; default: item.remoteType = ItemType.none; break; // Default to ItemType.none } // Return item return item; } // computes the path of the given item id // the path is relative to the sync directory ex: "Music/Turbo Killer.mp3" // the trailing slash is not added even if the item is a directory string computePath(const(char)[] driveId, const(char)[] id) { synchronized(databaseLock) { assert(driveId && id); string path; Item item; auto s = db.prepare("SELECT * FROM item WHERE driveId = ?1 AND id = ?2"); auto s2 = db.prepare("SELECT driveId, id FROM item WHERE remoteDriveId = ?1 AND remoteId = ?2"); scope(exit) { s.finalise(); // Ensure that the prepared statement is finalised after execution. s2.finalise(); // Ensure that the prepared statement is finalised after execution. } try { while (true) { s.bind(1, driveId); s.bind(2, id); auto r = s.exec(); if (!r.empty) { item = buildItem(r); if (item.type == ItemType.remote) { // substitute the last name with the current ptrdiff_t idx = indexOf(path, '/'); path = idx >= 0 ? item.name ~ path[idx .. $] : item.name; } else { if (path) path = item.name ~ "/" ~ path; else path = item.name; } id = item.parentId; } else { if (id == null) { // check for remoteItem s2.bind(1, item.driveId); s2.bind(2, item.id); auto r2 = s2.exec(); if (r2.empty) { // root reached assert(path.length >= 4); // remove "root/" from path string if it exists if (path.length >= 5) { if (canFind(path, "root/")){ path = path[5 .. $]; } } else { path = path[4 .. $]; } // special case of computing the path of the root itself if (path.length == 0) path = "."; break; } else { // remote folder driveId = r2.front[0].dup; id = r2.front[1].dup; } } else { // broken database tree addLogEntry("The following generated a broken database tree query:"); addLogEntry("Drive ID: " ~ to!string(driveId)); addLogEntry("Item ID: " ~ to!string(id)); assert(0); } } } } catch (SqliteException exception) { // Handle the error appropriately detailSQLErrorMessage(exception); } return path; } } Item[] selectRemoteItems() { synchronized(databaseLock) { Item[] items; auto stmt = db.prepare("SELECT * FROM item WHERE remoteDriveId IS NOT NULL"); scope (exit) stmt.finalise(); // Ensure that the prepared statement is finalised after execution. try { auto res = stmt.exec(); while (!res.empty) { items ~= buildItem(res); res.step(); } } catch (SqliteException exception) { // Handle the error appropriately detailSQLErrorMessage(exception); } return items; } } string getDeltaLink(const(char)[] driveId, const(char)[] id) { synchronized(databaseLock) { // Log what we received if (debugLogging) { addLogEntry("DeltaLink Query (driveId): " ~ to!string(driveId), ["debug"]); addLogEntry("DeltaLink Query (id): " ~ to!string(id), ["debug"]); } // assert if these are null assert(driveId && id); auto stmt = db.prepare("SELECT deltaLink FROM item WHERE driveId = ?1 AND id = ?2"); scope(exit) stmt.finalise(); // Ensure that the prepared statement is finalised after execution. try { stmt.bind(1, driveId); stmt.bind(2, id); auto res = stmt.exec(); if (res.empty) return null; return res.front[0].dup; } catch (SqliteException exception) { // Handle the error appropriately detailSQLErrorMessage(exception); return null; } } } void setDeltaLink(const(char)[] driveId, const(char)[] id, const(char)[] deltaLink) { synchronized(databaseLock) { assert(driveId && id); assert(deltaLink); auto stmt = db.prepare("UPDATE item SET deltaLink = ?3 WHERE driveId = ?1 AND id = ?2"); scope(exit) stmt.finalise(); // Ensure that the prepared statement is finalised after execution. try { stmt.bind(1, driveId); stmt.bind(2, id); stmt.bind(3, deltaLink); stmt.exec(); } catch (SqliteException exception) { // Handle the error appropriately detailSQLErrorMessage(exception); } } } // We have 4 different operational modes where 'item.syncStatus' is used to flag if an item is synced or not: // - National Cloud Deployments do not support /delta as a query // - When using --single-directory // - When using --download-only --cleanup-local-files // - Are we scanning a Shared Folder // // As we query /children to get all children from OneDrive, update anything in the database // to be flagged as not-in-sync, thus, we can use that flag to determine what was previously // in-sync, but now deleted on OneDrive void downgradeSyncStatusFlag(const(char)[] driveId, const(char)[] id) { synchronized(databaseLock) { assert(driveId); auto stmt = db.prepare("UPDATE item SET syncStatus = 'N' WHERE driveId = ?1 AND id = ?2"); scope(exit) { stmt.finalise(); // Ensure that the prepared statement is finalised after execution. } try { stmt.bind(1, driveId); stmt.bind(2, id); stmt.exec(); } catch (SqliteException exception) { // Handle the error appropriately detailSQLErrorMessage(exception); } } } // We have 4 different operational modes where 'item.syncStatus' is used to flag if an item is synced or not: // - National Cloud Deployments do not support /delta as a query // - When using --single-directory // - When using --download-only --cleanup-local-files // - Are we scanning a Shared Folder // // Select items that have a out-of-sync flag set Item[] selectOutOfSyncItems(const(char)[] driveId) { synchronized(databaseLock) { assert(driveId); Item[] items; auto stmt = db.prepare("SELECT * FROM item WHERE syncStatus = 'N' AND driveId = ?1"); scope(exit) stmt.finalise(); // Ensure that the prepared statement is finalised after execution. try { stmt.bind(1, driveId); auto res = stmt.exec(); while (!res.empty) { items ~= buildItem(res); res.step(); } } catch (SqliteException exception) { // Handle the error appropriately detailSQLErrorMessage(exception); } return items; } } // OneDrive Business Folders are stored in the database potentially without a root | parentRoot link // Select items associated with the provided driveId Item[] selectByDriveId(const(char)[] driveId) { synchronized(databaseLock) { assert(driveId); Item[] items; auto stmt = db.prepare("SELECT * FROM item WHERE driveId = ?1 AND parentId IS NULL"); scope(exit) stmt.finalise(); // Ensure that the prepared statement is finalised after execution. try { stmt.bind(1, driveId); auto res = stmt.exec(); while (!res.empty) { items ~= buildItem(res); res.step(); } } catch (SqliteException exception) { // Handle the error appropriately detailSQLErrorMessage(exception); } return items; } } // Perform a vacuum on the database, commit WAL / SHM to file void performVacuum() { synchronized(databaseLock) { // Log what we are attempting to do addLogEntry("Attempting to perform a database vacuum to optimise database"); try { // Check the current DB Status - we have to be in a clean state here db.checkStatus(); // Are there any open statements that need to be closed? if (db.count_open_statements() > 0) { // Dump open statements db.dump_open_statements(); // dump open statements so we know what the are // SIGINT (CTRL-C), SIGTERM (kill) handling if (exitHandlerTriggered) { // The SQLITE_INTERRUPT result code indicates that an operation was interrupted - which if we have open statements, most likely a SIGINT scenario throw new SqliteException(9, "Open SQL Statements due to interrupted operations"); } else { // Try and close open statements db.close_open_statements(); } } // Ensure there are no pending operations by performing a checkpoint db.exec("PRAGMA wal_checkpoint(TRUNCATE);"); // Prepare and execute VACUUM statement Statement stmt = db.prepare("VACUUM;"); scope(exit) stmt.finalise(); // Ensure the statement is finalised when we exit stmt.exec(); addLogEntry("Database vacuum is complete"); } catch (SqliteException exception) { addLogEntry(); addLogEntry("ERROR: Unable to perform a database vacuum: " ~ exception.msg); addLogEntry(); } } } // Perform a checkpoint (either TRUNCATE or PASSIVE) by writing the data into to the database from the WAL file void performCheckpoint(string checkpointType) { synchronized(databaseLock) { // Log what we are attempting to do if (debugLogging) {addLogEntry("Attempting to perform a database checkpoint to merge temporary data", ["debug"]);} try { // Check the current DB Status - we have to be in a clean state here db.checkStatus(); // Are there any open statements that need to be closed? if (db.count_open_statements() > 0) { // Dump open statements db.dump_open_statements(); // dump open statements so we know what the are // SIGINT (CTRL-C), SIGTERM (kill) handling if (exitHandlerTriggered) { // The SQLITE_INTERRUPT result code indicates that an operation was interrupted - which if we have open statements, most likely a SIGINT scenario throw new SqliteException(9, "Open SQL Statements due to interrupted operations"); } else { // Try and close open statements db.close_open_statements(); } } // Ensure there are no pending operations by performing a checkpoint string databaseCommand = format("PRAGMA wal_checkpoint(%s);" , checkpointType); db.exec(databaseCommand); if (debugLogging) {addLogEntry("Database checkpoint is complete", ["debug"]);} } catch (SqliteException exception) { addLogEntry(); addLogEntry("ERROR: Unable to perform a database checkpoint: " ~ exception.msg); addLogEntry(); } } } // Select distinct driveId items from database string[] selectDistinctDriveIds() { synchronized(databaseLock) { string[] driveIdArray; auto stmt = db.prepare("SELECT DISTINCT driveId FROM item;"); scope(exit) stmt.finalise(); // Ensure that the prepared statement is finalised after execution. try { auto res = stmt.exec(); if (res.empty) return driveIdArray; while (!res.empty) { driveIdArray ~= res.front[0].dup; res.step(); } } catch (SqliteException exception) { // Handle the error appropriately detailSQLErrorMessage(exception); } return driveIdArray; } } // Function to get the total number of rows in a table int getTotalRowCount() { synchronized(databaseLock) { int rowCount = 0; auto stmt = db.prepare("SELECT COUNT(*) FROM item;"); scope(exit) stmt.finalise(); // Ensure that the prepared statement is finalised after execution. try { auto res = stmt.exec(); if (!res.empty) { rowCount = res.front[0].to!int; } } catch (SqliteException exception) { // Handle the error appropriately detailSQLErrorMessage(exception); } return rowCount; } } }onedrive-2.5.5/src/log.d000066400000000000000000000226101476564400300150650ustar00rootroot00000000000000// What is this module called? module log; // What does this module require to function? import std.stdio; import std.file; import std.datetime; import std.concurrency; import std.typecons; import core.sync.mutex; import core.sync.condition; import core.thread; import std.format; import std.string; import std.conv; // What other modules that we have created do we need to import? import util; version(Notifications) { import dnotify; } // Shared Application Logging Level Variables __gshared bool verboseLogging = false; __gshared bool debugLogging = false; __gshared bool debugHTTPSResponse = false; __gshared string microsoftDataCentre; // Private Shared Module Objects private __gshared LogBuffer logBuffer; // Timer for logging private __gshared MonoTime lastInsertedTime; // Is logging active private __gshared bool isRunning; class LogBuffer { private string[3][] buffer; private Mutex bufferLock; private Condition condReady; private string logFilePath; private bool writeToFile; private bool verboseLogging; private bool debugLogging; private Thread flushThread; private bool environmentVariablesAvailable; private bool sendGUINotification; this(bool verboseLogging, bool debugLogging) { // Initialise the mutex bufferLock = new Mutex(); condReady = new Condition(bufferLock); // Initialise shared items isRunning = true; // Initialise other items this.logFilePath = ""; this.writeToFile = false; this.verboseLogging = verboseLogging; this.debugLogging = debugLogging; this.environmentVariablesAvailable = false; this.sendGUINotification = false; this.flushThread = new Thread(&flushBuffer); this.flushThread.isDaemon(true); this.flushThread.start(); } ~this() { if (!isRunning) { if (exitHandlerTriggered) { bufferLock.unlock(); } } } // Terminate Logging void terminateLogging() { synchronized { // join all threads thread_joinAll(); if (!isRunning) { return; // Prevent multiple shutdowns } // flag that we are no longer running due to shutting down isRunning = false; condReady.notifyAll(); // Wake up all waiting threads } // Wait for the flush thread to finish outside of the synchronized block to avoid deadlocks if (flushThread.isRunning()) { flushThread.join(true); } // Flush any remaining logs flushBuffer(); // Sleep for a while to avoid busy-waiting Thread.sleep(dur!"msecs"(100)); // Adjust the sleep duration as needed // Exit scopes scope(exit) { if (bufferLock !is null) { bufferLock.lock(); } scope(exit) { if (bufferLock !is null) { bufferLock.unlock(); object.destroy(bufferLock); bufferLock = null; } } } scope(failure) { if (bufferLock !is null) { bufferLock.lock(); } scope(exit) { if (bufferLock !is null) { bufferLock.unlock(); object.destroy(bufferLock); bufferLock = null; } } } } // Flush the logging buffer private void flushBuffer() { while (isRunning) { flush(); } stdout.flush(); } // Add the message received to the buffer for logging void logThisMessage(string message, string[] levels = ["info"]) { // Generate the timestamp for this log entry auto timeStamp = leftJustify(Clock.currTime().toString(), 28, '0'); synchronized(bufferLock) { foreach (level; levels) { // Normal application output if (!debugLogging) { if ((level == "info") || ((verboseLogging) && (level == "verbose")) || (level == "logFileOnly") || (level == "consoleOnly") || (level == "consoleOnlyNoNewLine")) { // Add this message to the buffer, with this format buffer ~= [timeStamp, level, format("%s", message)]; } } else { // Debug Logging (--verbose --verbose | -v -v | -vv) output // Add this message, regardless of 'level' to the buffer, with this format buffer ~= [timeStamp, level, format("DEBUG: %s", message)]; // If there are multiple 'levels' configured, ignore this and break as we are doing debug logging break; } // Submit the message to the dbus / notification daemon for display within the GUI being used // Will not send GUI notifications when running in debug mode if ((!debugLogging) && (level == "notify")) { if (sendGUINotification) { notify(message); } } } // Notify thread to wake up condReady.notify(); } } // Send GUI notification if --enable-notifications as been used at compile time void notify(string message) { // Use dnotify's functionality for GUI notifications, if GUI notification support has been compiled in version(Notifications) { try { auto n = new Notification("OneDrive Client for Linux", message, "dialog-information"); n.show(); } catch (NotificationError e) { addLogEntry("Unable to send notification to the D-Bus message bus daemon, disabling GUI notifications: " ~ e.message); sendGUINotification = false; } } } // Flush the logging buffer private void flush() { string[3][] messages; synchronized(bufferLock) { if (isRunning) { while (buffer.empty && isRunning) { // buffer is empty and logging is still active condReady.wait(); } messages = buffer; buffer.length = 0; } } // Are there messages to process? if (messages.length > 0) { // There are messages to process foreach (msg; messages) { // timestamp, logLevel, message // Always write the log line to the console, if level != logFileOnly if (msg[1] != "logFileOnly") { // Console output .. what sort of output if (msg[1] == "consoleOnlyNoNewLine") { // This is used write out a message to the console only, without a new line // This is used in non-verbose mode to indicate something is happening when downloading JSON data from OneDrive or when we need user input from --resync write(msg[2]); } else { // write this to the console with a new line writeln(msg[2]); } } // Was this just console only output? if ((msg[1] != "consoleOnlyNoNewLine") && (msg[1] != "consoleOnly")) { // Write to the logfile only if configured to do so - console only items should not be written out if (writeToFile) { string logFileLine = format("[%s] %s", msg[0], msg[2]); std.file.append(logFilePath, logFileLine ~ "\n"); } } } // Clear Messages messages.length = 0; } } } // Function to initialise the logging system void initialiseLogging(bool verboseLogging = false, bool debugLogging = false) { logBuffer = new LogBuffer(verboseLogging, debugLogging); lastInsertedTime = MonoTime.currTime(); } // Shutdown Logging void shutdownLogging() { if (logBuffer !is null) { // Terminate logging in a safe manner logBuffer.terminateLogging(); logBuffer = null; } } // Function to add a log entry with multiple levels void addLogEntry(string message = "", string[] levels = ["info"]) { // we can only add a log line if we are running ... if (isRunning) { logBuffer.logThisMessage(message, levels); } } // Is logging still active bool loggingActive() { return isRunning; } // Is logging still initialised bool loggingStillInitialised() { return logBuffer !is null; } void addProcessingLogHeaderEntry(string message, long verbosityCount) { if (verbosityCount == 0) { addLogEntry(message, ["logFileOnly"]); // Use the dots to show the application is 'doing something' if verbosityCount == 0 addLogEntry(message ~ " .", ["consoleOnlyNoNewLine"]); } else { // Fallback to normal logging if in verbose or above level addLogEntry(message); } } // Add a processing '.' to indicate activity void addProcessingDotEntry() { if (MonoTime.currTime() - lastInsertedTime < dur!"seconds"(1)) { // Don't flood the log buffer return; } lastInsertedTime = MonoTime.currTime(); addLogEntry(".", ["consoleOnlyNoNewLine"]); } // Finish processing '.' line output void completeProcessingDots() { addLogEntry(" ", ["consoleOnly"]); } // Function to set logFilePath and enable logging to a file void enableLogFileOutput(string configuredLogFilePath) { logBuffer.logFilePath = configuredLogFilePath; logBuffer.writeToFile = true; } // Flag that the environment variables exists so if logging is compiled in, it can be enabled void flagEnvironmentVariablesAvailable(bool variablesAvailable) { logBuffer.environmentVariablesAvailable = variablesAvailable; } // Disable GUI Notifications void disableGUINotifications(bool userConfigDisableNotifications) { logBuffer.sendGUINotification = userConfigDisableNotifications; } // Validate that if GUI Notification support has been compiled in using --enable-notifications, the DBUS Server is actually usable void validateDBUSServerAvailability() { version(Notifications) { if (logBuffer.environmentVariablesAvailable) { auto serverAvailable = dnotify.check_availability(); if (!serverAvailable) { addLogEntry("WARNING: D-Bus message bus daemon is not available; GUI notifications are disabled"); logBuffer.sendGUINotification = false; } else { addLogEntry("D-Bus message bus daemon is available; GUI notifications are now enabled"); if (debugLogging) {addLogEntry("D-Bus message bus daemon server details: " ~ to!string(dnotify.get_server_info()), ["debug"]);} logBuffer.sendGUINotification = true; } } else { addLogEntry("WARNING: The required environment variables to enable GUI Notifications are not available; GUI notifications are disabled"); logBuffer.sendGUINotification = false; } } }onedrive-2.5.5/src/main.d000066400000000000000000002304671476564400300152430ustar00rootroot00000000000000// What is this module called? module main; // What does this module require to function? import core.stdc.stdlib: EXIT_SUCCESS, EXIT_FAILURE, exit; import core.sys.posix.signal; import core.memory; import core.time; import core.thread; import std.stdio; import std.getopt; import std.string; import std.file; import std.process; import std.algorithm; import std.path; import std.concurrency; import std.parallelism; import std.conv; import std.traits; import std.net.curl: CurlException; import std.datetime; // What other modules that we have created do we need to import? import config; import log; import curlEngine; import util; import onedrive; import syncEngine; import itemdb; import clientSideFiltering; import monitor; import webhook; // What other constant variables do we require? const int EXIT_RESYNC_REQUIRED = 126; // Class objects ApplicationConfig appConfig; OneDriveWebhook oneDriveWebhook; SyncEngine syncEngineInstance; ItemDatabase itemDB; ClientSideFiltering selectiveSync; Monitor filesystemMonitor; // Class variables // Flag for performing a synchronised shutdown bool shutdownInProgress = false; // Flag if a --dry-run is being performed, as, on shutdown, once config is destroyed, we have no reference here bool dryRun = false; // Configure the runtime database file path so that it is available to us on shutdown so objects can be destroyed and removed if required // - Typically this will be the default, but in a --dry-run scenario, we use a separate database file string runtimeDatabaseFile = ""; // Flag for if we are performing filesystem monitoring bool performFileSystemMonitoring = false; // Flag for if we perform a database vacuum. This gets set to false if we have not performed a 'no-sync' task bool performDatabaseVacuum = true; // Flag if SIGTERM is used bool sigtermHandlerTriggered = false; int main(string[] cliArgs) { // Application Start Time - used during monitor loop to detail how long it has been running for auto applicationStartTime = Clock.currTime(); // Disable buffering on stdout - this is needed so that when we are using plain write() it will go to the terminal without flushing stdout.setvbuf(0, _IONBF); // Required main function variables string genericHelpMessage = "Please use 'onedrive --help' for further assistance in regards to running this application."; // If the user passes in --confdir we need to store this as a variable string confdirOption = ""; // running as what user? string runtimeUserName = ""; // Are we online? bool online = false; // Does the operating environment have shell environment variables set bool shellEnvSet = false; // What is the runtime synchronisation directory that will be used // Typically this will be '~/OneDrive' .. however tilde expansion is unreliable string runtimeSyncDirectory = ""; // Verbosity Logging Count - this defines if verbose or debug logging is being used long verbosityCount = 0; // Monitor loop failures bool monitorFailures = false; // Help requested bool helpRequested = false; // Did the user specify --sync or --monitor bool syncOrMonitorMissing = false; // Was a no-sync type operation requested bool noSyncTaskOperationRequested = false; // DEVELOPER OPTIONS OUTPUT VARIABLES bool displayMemoryUsage = false; bool displaySyncOptions = false; // Application Version immutable string applicationVersion = "onedrive " ~ strip(import("version")); // Define 'exit' and 'failure' scopes scope(exit) { // Detail what scope was called if (debugLogging) {addLogEntry("Exit scope was called", ["debug"]);} // Perform synchronised exit performSynchronisedExitProcess("exitScope"); // Setup signal handling for the exit scope setupExitScopeSignalHandler(); } scope(failure) { // Detail what scope was called if (debugLogging) {addLogEntry("Failure scope was called", ["debug"]);} // Perform synchronised exit performSynchronisedExitProcess("failureScope"); // Setup signal handling for the exit scope setupExitScopeSignalHandler(); } // Read in application options as passed in try { bool printVersion = false; auto cliOptions = getopt( cliArgs, std.getopt.config.passThrough, std.getopt.config.bundling, std.getopt.config.caseSensitive, "confdir", "Set the directory used to store the configuration files", &confdirOption, "verbose|v+", "Print more details, useful for debugging (repeat for extra debugging)", &verbosityCount, "version", "Print the version and exit", &printVersion ); // Print help and exit if (cliOptions.helpWanted) { cliArgs ~= "--help"; helpRequested = true; } // Print the version and exit if (printVersion) { writeln(applicationVersion); exit(EXIT_SUCCESS); } } catch (GetOptException e) { // Option errors writeln(e.msg); writeln(genericHelpMessage); return EXIT_FAILURE; } catch (Exception e) { // Generic error writeln(e.msg); writeln(genericHelpMessage); return EXIT_FAILURE; } // Determine the application logging verbosity // - As these flags are used to reduce application processing when not required, specifically in a 'debug' scenario, both verboseLogging and debugLogging need to be enabled if (verbosityCount == 1) { verboseLogging = true;} // set __gshared bool verboseLogging in log.d if (verbosityCount >= 2) { verboseLogging = true; debugLogging = true;} // set __gshared bool verboseLogging & debugLogging in log.d // Initialize the application logging class, as we know the application verbosity level // If we need to enable logging to a file, we can only do this once we know the application configuration which is done slightly later on initialiseLogging(verboseLogging, debugLogging); // Log application start time, log line has start time if (debugLogging) {addLogEntry("Application started", ["debug"]);} // Who are we running as? This will print the ProcessID, UID, GID and username the application is running as runtimeUserName = getUserName(); // Print the application version and how this was compiled as soon as possible if (debugLogging) { addLogEntry("Application Version: " ~ applicationVersion, ["debug"]); addLogEntry("Application Compiled With: " ~ compilerDetails(), ["debug"]); // How was this application started - what options were passed in addLogEntry("Passed in 'cliArgs': " ~ to!string(cliArgs), ["debug"]); addLogEntry("Note: --confdir and --verbose are not listed in 'cliArgs' array", ["debug"]); addLogEntry("Passed in --confdir if present: " ~ confdirOption, ["debug"]); addLogEntry("Passed in --verbose count if present: " ~ to!string(verbosityCount), ["debug"]); } // Create a new AppConfig object with default values, appConfig = new ApplicationConfig(); // Update the default application configuration with the verbosity count so this can be used throughout the application as needed appConfig.verbosityCount = verbosityCount; // Initialise the application configuration, utilising --confdir if it was passed in // Otherwise application defaults will be used to configure the application if (!appConfig.initialise(confdirOption, helpRequested)) { // There was an error loading the user specified application configuration // Error message already printed return EXIT_FAILURE; } // Update the current runtime application configuration (default or 'config' file read in options) from any passed in command line arguments appConfig.updateFromArgs(cliArgs); // If --debug-https has been used, set the applicable flag debugHTTPSResponse = appConfig.getValueBool("debug_https"); // set __gshared bool debugHTTPSResponse in log.d now that we have read-in any CLI arguments // Are we doing a --sync or a --monitor operation? Both of these will be false if they are not set if ((!appConfig.getValueBool("synchronize")) && (!appConfig.getValueBool("monitor"))) { syncOrMonitorMissing = true; // --sync or --monitor is missing } // Are we performing some sort of 'no-sync' operation task? noSyncTaskOperationRequested = appConfig.hasNoSyncOperationBeenRequested(); // returns true if we are // If 'syncOrMonitorMissing' is true and 'noSyncTaskOperationRequested' is false (meaning we are not doing some 'no-sync' operation like '--display-sync-status', '--get-sharepoint-drive-id' or '--display-config' // - fail fast here to avoid setting up all the other components, database, initialising the API as this is all pointless if we just fail out later // If we are not using --display-config, perform this check if (!appConfig.getValueBool("display_config")) { if (syncOrMonitorMissing && !noSyncTaskOperationRequested) { // Before failing fast, has the client been authenticated and does the 'refresh_token' contain data if (exists(appConfig.refreshTokenFilePath) && getSize(appConfig.refreshTokenFilePath) > 0) { // fail fast - print error message that --sync or --monitor are missing printMissingOperationalSwitchesError(); // Use exit scopes to shutdown API return EXIT_FAILURE; } } } // If --disable-notifications has not been used, check if everything exists to enable notifications if (!appConfig.getValueBool("disable_notifications")) { // If notifications was compiled in, we need to ensure that these variables are actually available before we enable GUI Notifications flagEnvironmentVariablesAvailable(appConfig.validateGUINotificationEnvironmentVariables()); // If we are not using --display-config attempt to enable GUI notifications if (!appConfig.getValueBool("display_config")) { // Attempt to enable GUI Notifications validateDBUSServerAvailability(); } } // cURL Version Compatibility Test // - Common warning for cURL version issue string distributionWarning = " Please report this to your distribution, requesting an update to a newer cURL version, or consider upgrading it yourself for optimal stability."; // If 'force_http_11' = false, we need to check the curl version being used if (!appConfig.getValueBool("force_http_11")) { // get the curl version string curlVersion = getCurlVersionNumeric(); // Is the version of curl or libcurl being used by the platform a known bad curl version for HTTP/2 support if (isBadCurlVersion(curlVersion)) { // add warning message string curlWarningMessage = format("WARNING: Your cURL/libcurl version (%s) has known HTTP/2 bugs that impact the use of this client.", curlVersion); addLogEntry(); addLogEntry(curlWarningMessage, ["info", "notify"]); addLogEntry(distributionWarning); addLogEntry(" Downgrading all client operations to use HTTP/1.1 to ensure maximum operational stability."); addLogEntry(" Please read https://github.com/abraunegg/onedrive/blob/master/docs/usage.md#compatibility-with-curl for more information."); addLogEntry(); appConfig.setValueBool("force_http_11" , true); } } else { // get the curl version - a bad curl version may still be in use string curlVersion = getCurlVersionNumeric(); // Is the version of curl or libcurl being used by the platform a known bad curl version if (isBadCurlVersion(curlVersion)) { // add warning message string curlWarningMessage = format("WARNING: Your cURL/libcurl version (%s) has known operational bugs that impact the use of this client.", curlVersion); addLogEntry(); addLogEntry(curlWarningMessage); // curl HTTP/1.1 downgrade in place meaning user took steps to remediate, perform standard logging with no GUI notification addLogEntry(distributionWarning); addLogEntry(); } } // OpenSSL Version Compatibility Test // - Example - on CentOS 7.9 (OpenSSL 1.0.2k-fips 26 Jan 2017), access with Microsoft OneDrive causes a segfault in sha1_block_data_order_avx from /lib64/libcrypto.so.10 // - See Discussion #2950 for relevant gdb output checkOpenSSLVersion(); // In a debug scenario, to assist with understanding the run-time configuration, ensure this flag is set if (debugLogging) { appConfig.setValueBool("display_running_config", true); } // Configure dryRun so that this can be used here & during shutdown dryRun = appConfig.getValueBool("dry_run"); // As early as possible, now re-configure the logging class, given that we have read in any applicable 'config' file and updated the application running config from CLI input: // - Enable logging to a file if this is required // - Disable GUI notifications if this has been configured // Configure application logging to a log file only if this has been enabled // This is the earliest point that this can be done, as the client configuration has been read in, and any CLI arguments have been processed. // Either of those ('config' file, CLI arguments) could be enabling logging, thus this is the earliest point at which this can be validated and enabled. // The buffered logging also ensures that all 'output' to this point is also captured and written out to the log file if (appConfig.getValueBool("enable_logging")) { // Calculate the application logging directory string calculatedLogDirPath = appConfig.calculateLogDirectory(); string calculatedLogFilePath; // Initialise using the configured logging directory if (verboseLogging) {addLogEntry("Using the following path to store the runtime application log: " ~ calculatedLogDirPath, ["verbose"]);} // Calculate the logfile name if (calculatedLogDirPath != appConfig.defaultHomePath) { // Log file is not going to the home directory string logfileName = runtimeUserName ~ ".onedrive.log"; calculatedLogFilePath = buildNormalizedPath(buildPath(calculatedLogDirPath, logfileName)); } else { // Log file is going to the users home directory calculatedLogFilePath = buildNormalizedPath(buildPath(calculatedLogDirPath, "onedrive.log")); } // Update the logging class to use 'calculatedLogFilePath' for the application log file now that this has been determined enableLogFileOutput(calculatedLogFilePath); } // Disable GUI Notifications if configured to do so // - This option is reverse action. If 'disable_notifications' is 'true', we need to send 'false' if (appConfig.getValueBool("disable_notifications")) { // disable_notifications is true, ensure GUI notifications is initialised with false so that NO GUI notification is sent disableGUINotifications(false); addLogEntry("Disabling GUI notifications as per user configuration"); } // Perform a deprecated options check now that the config file (if present) and CLI options have all been parsed to advise the user that their option usage might change appConfig.checkDeprecatedOptions(cliArgs); // Configure Client Side Filtering (selective sync) by parsing and getting a usable regex for skip_file, skip_dir and sync_list config components selectiveSync = new ClientSideFiltering(appConfig); if (!selectiveSync.initialise()) { // exit here as something triggered a selective sync configuration failure return EXIT_FAILURE; } // Set runtimeDatabaseFile, this will get updated if we are using --dry-run runtimeDatabaseFile = appConfig.databaseFilePath; // Read in 'sync_dir' from appConfig with '~' if present expanded runtimeSyncDirectory = appConfig.initialiseRuntimeSyncDirectory(); // DEVELOPER OPTIONS OUTPUT // Set to display memory details as early as possible displayMemoryUsage = appConfig.getValueBool("display_memory"); // set to display sync options displaySyncOptions = appConfig.getValueBool("display_sync_options"); // Display the current application configuration (based on all defaults, 'config' file parsing and/or options passed in via the CLI) and exit if --display-config has been used if ((appConfig.getValueBool("display_config")) || (appConfig.getValueBool("display_running_config"))) { // Display the application configuration appConfig.displayApplicationConfiguration(); // Do we exit? We exit only if '--display-config' has been used if (appConfig.getValueBool("display_config")) { return EXIT_SUCCESS; } } // Check for basic application option conflicts - flags that should not be used together and/or flag combinations that conflict with each other, values that should be present and are not if (appConfig.checkForBasicOptionConflicts) { // Any error will have been printed by the function itself, but we need a small delay here to allow the buffered logging to output any error return EXIT_FAILURE; } // Check for --dry-run operation or a 'no-sync' operation where the 'dry-run' DB copy should be used // If this has been requested, we need to ensure that all actions are performed against the dry-run database copy, and, // no actual action takes place - such as deleting files if deleted online, moving files if moved online or local, downloading new & changed files, uploading new & changed files if (dryRun || (noSyncTaskOperationRequested)) { // If --dry-run if (dryRun) { // This is a --dry-run operation addLogEntry("DRY-RUN Configured. Output below shows what 'would' have occurred."); } // Cleanup any existing dry-run elements ... these should never be left hanging around and should be cleaned up first cleanupDatabaseFiles(appConfig.databaseFilePathDryRun); // Make a copy of the original items.sqlite3 for use as the dry run copy if it exists if (exists(appConfig.databaseFilePath)) { // In a --dry-run --resync scenario, we should not copy the existing database file if (!appConfig.getValueBool("resync")) { // Copy the existing DB file to the dry-run copy if (dryRun) { addLogEntry("DRY-RUN: Copying items.sqlite3 to items-dryrun.sqlite3 to use for dry run operations"); } copy(appConfig.databaseFilePath,appConfig.databaseFilePathDryRun); } else { // No database copy due to --resync if (dryRun) { addLogEntry("DRY-RUN: No database copy created for --dry-run due to --resync also being used"); } } } // update runtimeDatabaseFile now that we are using the dry run path runtimeDatabaseFile = appConfig.databaseFilePathDryRun; } else { // Cleanup any existing dry-run elements ... these should never be left hanging around cleanupDatabaseFiles(appConfig.databaseFilePathDryRun); } // Handle --logout as separate item, do not 'resync' on a --logout if (appConfig.getValueBool("logout")) { if (debugLogging) {addLogEntry("--logout requested", ["debug"]);} addLogEntry("Deleting the saved authentication status ..."); if (!dryRun) { safeRemove(appConfig.refreshTokenFilePath); } else { // --dry-run scenario ... technically we should not be making any local file changes ....... addLogEntry("DRY RUN: Not removing the saved authentication status"); } // Exit return EXIT_SUCCESS; } // Handle --reauth to re-authenticate the client if (appConfig.getValueBool("reauth")) { if (debugLogging) {addLogEntry("--reauth requested", ["debug"]);} addLogEntry("Deleting the saved authentication status ... re-authentication requested"); if (!dryRun) { safeRemove(appConfig.refreshTokenFilePath); } else { // --dry-run scenario ... technically we should not be making any local file changes ....... addLogEntry("DRY RUN: Not removing the saved authentication status"); } } // --resync should be considered a 'last resort item' or if the application configuration has changed, where a resync is needed .. the user needs to 'accept' this warning to proceed // If --resync has not been used (bool value is false), check the application configuration for 'changes' that require a --resync to ensure that the data locally reflects the users requested configuration if (appConfig.getValueBool("resync")) { // what is the risk acceptance for --resync? bool resyncRiskAcceptance = appConfig.displayResyncRiskForAcceptance(); if (debugLogging) {addLogEntry("Returned --resync risk acceptance: " ~ to!string(resyncRiskAcceptance), ["debug"]);} // Action based on user response if (!resyncRiskAcceptance){ // --resync risk not accepted return EXIT_FAILURE; } else { if (debugLogging) {addLogEntry("--resync issued and risk accepted", ["debug"]);} // --resync risk accepted, perform a cleanup of items that require a cleanup appConfig.cleanupHashFilesDueToResync(); // Make a backup of the applicable configuration file appConfig.createBackupConfigFile(); // Update hash files and generate a new config backup appConfig.updateHashContentsForConfigFiles(); // Remove the items database processResyncDatabaseRemoval(runtimeDatabaseFile); } } else { // Is the application currently authenticated? If not, it is pointless checking if a --resync is required until the application is authenticated if (exists(appConfig.refreshTokenFilePath)) { // Has any of our application configuration that would require a --resync been changed? if (appConfig.applicationChangeWhereResyncRequired()) { // Application configuration has changed however --resync not issued, fail fast addLogEntry(); addLogEntry("An application configuration change has been detected where a --resync is required", ["info", "notify"]); addLogEntry(); return EXIT_RESYNC_REQUIRED; } else { // No configuration change that requires a --resync to be issued // Special cases need to be checked - if these options were enabled, it creates a false 'Resync Required' flag, so do not create a backup if ((!appConfig.getValueBool("list_business_shared_items"))) { // Make a backup of the applicable configuration file appConfig.createBackupConfigFile(); // Update hash files and generate a new config backup appConfig.updateHashContentsForConfigFiles(); } } } } // Implement https://github.com/abraunegg/onedrive/issues/1129 // Force a synchronisation of a specific folder, only when using --synchronize --single-directory and ignoring all non-default skip_dir and skip_file rules if (appConfig.getValueBool("force_sync")) { // appConfig.checkForBasicOptionConflicts() has already checked for the basic requirements for --force-sync addLogEntry(); addLogEntry("WARNING: Overriding application configuration to use application defaults for skip_dir and skip_file due to --sync --single-directory --force-sync being used"); addLogEntry(); bool forceSyncRiskAcceptance = appConfig.displayForceSyncRiskForAcceptance(); if (debugLogging) {addLogEntry("Returned --force-sync risk acceptance: " ~ forceSyncRiskAcceptance, ["debug"]);} // Action based on user response if (!forceSyncRiskAcceptance){ // --force-sync risk not accepted return EXIT_FAILURE; } else { // --force-sync risk accepted // reset set config using function to use application defaults appConfig.resetSkipToDefaults(); // update sync engine regex with reset defaults selectiveSync.setDirMask(appConfig.getValueString("skip_dir")); selectiveSync.setFileMask(appConfig.getValueString("skip_file")); } } // What IP Protocol are we going to use to access the network with appConfig.displayIPProtocol(); // Test if OneDrive service can be reached, exit if it cant be reached if (debugLogging) {addLogEntry("Testing network to ensure network connectivity to Microsoft OneDrive Service", ["debug"]);} online = testInternetReachability(appConfig); // If we are not 'online' - how do we handle this situation? if (!online) { // We are unable to initialise the OneDrive API as we are not online if (!appConfig.getValueBool("monitor")) { // Running as --synchronize addLogEntry(); addLogEntry("ERROR: Unable to reach Microsoft OneDrive API service, unable to initialise application"); addLogEntry(); return EXIT_FAILURE; } else { // Running as --monitor addLogEntry(); addLogEntry("Unable to reach the Microsoft OneDrive API service at this point in time, re-trying network tests based on applicable intervals"); addLogEntry(); if (!retryInternetConnectivityTest(appConfig)) { return EXIT_FAILURE; } } } // This needs to be a separate 'if' statement, as, if this was an 'if-else' from above, if we were originally offline and using --monitor, we would never get to this point if (online) { // Check Application Version if (verboseLogging) {addLogEntry("Checking Application Version ...", ["verbose"]);} checkApplicationVersion(); // Initialise the OneDrive API if (verboseLogging) {addLogEntry("Attempting to initialise the OneDrive API ...", ["verbose"]);} OneDriveApi oneDriveApiInstance = new OneDriveApi(appConfig); appConfig.apiWasInitialised = oneDriveApiInstance.initialise(); // Did the API initialise successfully? if (appConfig.apiWasInitialised) { if (verboseLogging) {addLogEntry("The OneDrive API was initialised successfully", ["verbose"]);} // Flag that we were able to initialise the API in the application config oneDriveApiInstance.debugOutputConfiguredAPIItems(); oneDriveApiInstance.releaseCurlEngine(); object.destroy(oneDriveApiInstance); oneDriveApiInstance = null; // Need to configure the itemDB and syncEngineInstance for 'sync' and 'non-sync' operations if (verboseLogging) {addLogEntry("Opening the item database ...", ["verbose"]);} // Configure the Item Database itemDB = new ItemDatabase(runtimeDatabaseFile); // Was the database successfully initialised? if (!itemDB.isDatabaseInitialised()) { // no .. destroy class itemDB = null; // exit application return EXIT_FAILURE; } // Initialise the syncEngine syncEngineInstance = new SyncEngine(appConfig, itemDB, selectiveSync); appConfig.syncEngineWasInitialised = syncEngineInstance.initialise(); // Are we not doing a --sync or a --monitor operation? if (syncOrMonitorMissing) { // this is 'true' if --sync or a --monitor were not used // Do not perform a vacuum on exit, pointless performDatabaseVacuum = false; // Are we performing some sort of 'no-sync' task? // - Are we obtaining the Office 365 Drive ID for a given Office 365 SharePoint Shared Library? // - Are we displaying the sync status? // - Are we getting the URL for a file online? // - Are we listing who modified a file last online? // - Are we listing OneDrive Business Shared Items? // - Are we creating a shareable link for an existing file on OneDrive? // - Are we just creating a directory online, without any sync being performed? // - Are we just deleting a directory online, without any sync being performed? // - Are we renaming or moving a directory? // - Are we displaying the quota information? // - Did we just authorise the client? // --get-sharepoint-drive-id - Get the SharePoint Library drive_id if (appConfig.getValueString("sharepoint_library_name") != "") { // Get the SharePoint Library drive_id syncEngineInstance.querySiteCollectionForDriveID(appConfig.getValueString("sharepoint_library_name")); // Exit application // Use exit scopes to shutdown API and cleanup data return EXIT_SUCCESS; } // --display-sync-status - Query the sync status if (appConfig.getValueBool("display_sync_status")) { // path to query variable string pathToQueryStatusOn; // What path do we query? if (!appConfig.getValueString("single_directory").empty) { pathToQueryStatusOn = "/" ~ appConfig.getValueString("single_directory"); } else { pathToQueryStatusOn = "/"; } // Query the sync status syncEngineInstance.queryOneDriveForSyncStatus(pathToQueryStatusOn); // Exit application // Use exit scopes to shutdown API and cleanup data return EXIT_SUCCESS; } // --get-file-link - Get the URL path for a synced file if (appConfig.getValueString("get_file_link") != "") { // Query the OneDrive API for the file link syncEngineInstance.queryOneDriveForFileDetails(appConfig.getValueString("get_file_link"), runtimeSyncDirectory, "URL"); // Exit application // Use exit scopes to shutdown API and cleanup data return EXIT_SUCCESS; } // --modified-by - Get listing the modified-by details of a provided path if (appConfig.getValueString("modified_by") != "") { // Query the OneDrive API for the last modified by details syncEngineInstance.queryOneDriveForFileDetails(appConfig.getValueString("modified_by"), runtimeSyncDirectory, "ModifiedBy"); // Exit application // Use exit scopes to shutdown API and cleanup data return EXIT_SUCCESS; } // --list-shared-items - Get listing OneDrive Business Shared Items if (appConfig.getValueBool("list_business_shared_items")) { // Is this a business account type? if (appConfig.accountType == "business") { // List OneDrive Business Shared Items syncEngineInstance.listBusinessSharedObjects(); } else { addLogEntry("ERROR: Unsupported account type for listing OneDrive Business Shared Items"); } // Exit application // Use exit scopes to shutdown API return EXIT_SUCCESS; } // --create-share-link - Create a shareable link for an existing file, based on the local path if (appConfig.getValueString("create_share_link") != "") { // Query OneDrive for the file, and if valid, create a shareable link for the file // By default, the shareable link will be read-only. // If the user adds: // --with-editing-perms // this will create a writeable link syncEngineInstance.queryOneDriveForFileDetails(appConfig.getValueString("create_share_link"), runtimeSyncDirectory, "ShareableLink"); // Exit application // Use exit scopes to shutdown API return EXIT_SUCCESS; } // --create-directory - Are we just creating a directory online, without any sync being performed? if ((appConfig.getValueString("create_directory") != "")) { // Handle the remote path creation and updating of the local database without performing a sync syncEngineInstance.createDirectoryOnline(appConfig.getValueString("create_directory")); // Exit application // Use exit scopes to shutdown API return EXIT_SUCCESS; } // --remove-directory - Are we just deleting a directory online, without any sync being performed? if ((appConfig.getValueString("remove_directory") != "")) { // Handle the remote path deletion without performing a sync syncEngineInstance.deleteByPathNoSync(appConfig.getValueString("remove_directory")); // Exit application // Use exit scopes to shutdown API return EXIT_SUCCESS; } // Are we renaming or moving a directory online? // onedrive --source-directory 'path/as/source/' --destination-directory 'path/as/destination' if ((appConfig.getValueString("source_directory") != "") && (appConfig.getValueString("destination_directory") != "")) { // We are renaming or moving a directory syncEngineInstance.moveOrRenameDirectoryOnline(appConfig.getValueString("source_directory"), appConfig.getValueString("destination_directory")); // Exit application // Use exit scopes to shutdown API return EXIT_SUCCESS; } // --display-quota - Are we displaying the quota information? if (appConfig.getValueBool("display_quota")) { // Query and respond with the quota details syncEngineInstance.queryOneDriveForQuotaDetails(); // Exit application // Use exit scopes to shutdown API return EXIT_SUCCESS; } // If we get to this point, we have not performed a 'no-sync' task .. // Did we just authorise the client? if (appConfig.applicationAuthorizeResponseUri) { // Authorisation activity if (exists(appConfig.refreshTokenFilePath)) { // OneDrive refresh token exists addLogEntry(); addLogEntry("The application has been successfully authorised, but no extra command options have been specified."); addLogEntry(); addLogEntry(genericHelpMessage); addLogEntry(); // Use exit scopes to shutdown API return EXIT_SUCCESS; } else { // We just authorised, but refresh_token does not exist .. probably an auth error? addLogEntry(); addLogEntry("Your application's authorisation was unsuccessful. Please review your URI response entry, then attempt authorisation again with a new URI response."); addLogEntry(); // Use exit scopes to shutdown API return EXIT_FAILURE; } } else { // No authorisation activity - print error message printMissingOperationalSwitchesError(); // Use exit scopes to shutdown API return EXIT_FAILURE; } } } else { // API could not be initialised addLogEntry("The OneDrive API could not be initialised"); return EXIT_FAILURE; } } // Configure the sync directory based on the runtimeSyncDirectory configured directory if (verboseLogging) {addLogEntry("All application operations will be performed in the configured local 'sync_dir' directory: " ~ runtimeSyncDirectory, ["verbose"]);} // Try and set the 'sync_dir', attempt to create if it does not exist try { if (!exists(runtimeSyncDirectory)) { if (debugLogging) {addLogEntry("runtimeSyncDirectory: Configured 'sync_dir' is missing locally. Creating: " ~ runtimeSyncDirectory, ["debug"]);} // At this point 'sync_dir' is missing and we have requested to create it // However ... 'itemDB' is pointing to a valid database file // If this database has any entries, an empty 'sync_dir' will cause the application to think that all content in 'sync_dir' has been deleted // In this scenario, the application, depending on the options being used, may attempt to delete all files online - which is not desirable // Do a sanity check here to ensure that there are no database entries if (itemDB.getTotalRowCount() == 1) { // Technically an 'empty database' // An empty database will just have 1 row in it, that row being the account 'root' data added when the API is initially initialised above try { // Attempt to create the sync dir we have been configured with mkdirRecurse(runtimeSyncDirectory); // Configure the applicable permissions for the folder if (debugLogging) {addLogEntry("Setting directory permissions for: " ~ runtimeSyncDirectory, ["debug"]);} runtimeSyncDirectory.setAttributes(appConfig.returnRequiredDirectoryPermissions()); } catch (std.file.FileException e) { // Creating the sync directory failed addLogEntry("ERROR: Unable to create the configured local 'sync_dir' directory: " ~ e.msg, ["info", "notify"]); // Use exit scopes to shutdown API return EXIT_FAILURE; } } else { // Not an empty database addLogEntry(); addLogEntry("An application cache state issue has been detected where a --resync is required", ["info", "notify"]); addLogEntry(); return EXIT_RESYNC_REQUIRED; } } } catch (std.file.FileException e) { // Creating the sync directory failed addLogEntry("ERROR: Unable to test for the existence of the configured local 'sync_dir' directory: " ~ e.msg); // Use exit scopes to shutdown API return EXIT_FAILURE; } // Change the working directory to the 'sync_dir' as configured chdir(runtimeSyncDirectory); // Do we need to validate the runtimeSyncDirectory to check for the presence of a '.nosync' file checkForNoMountScenario(); // Set the default thread pool value defaultPoolThreads(to!int(appConfig.getValueLong("threads"))); // Is the sync engine initialised correctly? if (appConfig.syncEngineWasInitialised) { // Configure some initial variables string singleDirectoryPath; string localPath = "."; string remotePath = "/"; // If not performing a --resync , interrupted upload session(s) if (!appConfig.getValueBool("resync")) { // Check if there are interrupted upload session(s) if (syncEngineInstance.checkForInterruptedSessionUploads) { // Need to re-process the session upload files to resume the failed session uploads addLogEntry("There are interrupted session uploads that need to be resumed ..."); // Process the session upload files syncEngineInstance.processForInterruptedSessionUploads(); } } else { // Clean up any upload session files due to --resync being used syncEngineInstance.clearInterruptedSessionUploads(); } // Are we doing a single directory operation (--single-directory) ? if (!appConfig.getValueString("single_directory").empty) { // Ensure that the value stored for appConfig.getValueString("single_directory") does not contain any extra quotation marks string originalSingleDirectoryValue = appConfig.getValueString("single_directory"); // Strip quotation marks from provided path to ensure no issues within a Docker environment when using passed in values string updatedSingleDirectoryValue = strip(originalSingleDirectoryValue, "\""); // Set singleDirectoryPath singleDirectoryPath = updatedSingleDirectoryValue; // Ensure that this is a normalised relative path to runtimeSyncDirectory string normalisedRelativePath = replace(buildNormalizedPath(absolutePath(singleDirectoryPath)), buildNormalizedPath(absolutePath(runtimeSyncDirectory)), "." ); // The user provided a directory to sync within the configured 'sync_dir' path // This also validates if the path being used exists online and/or does not have a 'case-insensitive match' syncEngineInstance.setSingleDirectoryScope(normalisedRelativePath); // Does the directory we want to sync actually exist locally? if (!exists(singleDirectoryPath)) { // The requested path to use with --single-directory does not exist locally within the configured 'sync_dir' addLogEntry("WARNING: The requested path for --single-directory does not exist locally. Creating requested path within " ~ runtimeSyncDirectory, ["info", "notify"]); // Make the required --single-directory path locally mkdirRecurse(singleDirectoryPath); // Configure the applicable permissions for the folder if (debugLogging) {addLogEntry("Setting directory permissions for: " ~ singleDirectoryPath, ["debug"]);} singleDirectoryPath.setAttributes(appConfig.returnRequiredDirectoryPermissions()); } // Update the paths that we use to perform the sync actions localPath = singleDirectoryPath; remotePath = singleDirectoryPath; // Display that we are syncing from a specific path due to --single-directory if (verboseLogging) {addLogEntry("Syncing changes from this selected path: " ~ singleDirectoryPath, ["verbose"]);} } // Handle SIGINT, SIGTERM and SIGSEGV signals setupSignalHandler(); // Are we doing a --sync operation? This includes doing any --single-directory operations if (appConfig.getValueBool("synchronize")) { // We are not using this, so destroy it early object.destroy(filesystemMonitor); filesystemMonitor = null; // Did the user specify --upload-only? if (appConfig.getValueBool("upload_only")) { // Perform the --upload-only sync process performUploadOnlySyncProcess(localPath); } // Did the user specify --download-only? if (appConfig.getValueBool("download_only")) { // Only download data from OneDrive syncEngineInstance.syncOneDriveAccountToLocalDisk(); // Perform the DB consistency check // This will also delete any out-of-sync flagged items if configured to do so syncEngineInstance.performDatabaseConsistencyAndIntegrityCheck(); // Do we cleanup local files? // - Deletes of data from online will already have been performed, but what we are now doing is searching the local filesystem // for any new data locally, that usually would be uploaded to OneDrive, but instead, because of the options being // used, will need to be deleted from the local filesystem if (appConfig.getValueBool("cleanup_local_files")) { // Perform the filesystem walk syncEngineInstance.scanLocalFilesystemPathForNewData(localPath); } } // If no use of --upload-only or --download-only if ((!appConfig.getValueBool("upload_only")) && (!appConfig.getValueBool("download_only"))) { // Perform the standard sync process performStandardSyncProcess(localPath); } // Detail the outcome of the sync process displaySyncOutcome(); } // Are we doing a --monitor operation? if (appConfig.getValueBool("monitor")) { // Update the flag given we are running with --monitor performFileSystemMonitoring = true; // What are the current values for the platform we are running on string maxOpenFiles = strip(getMaxOpenFiles()); // What is the currently configured maximum inotify watches that can be used string maxInotifyWatches = strip(getMaxInotifyWatches()); // Start the monitor process addLogEntry("OneDrive synchronisation interval (seconds): " ~ to!string(appConfig.getValueLong("monitor_interval"))); // If we are in a --download-only method of operation, the output of these is not required if (!appConfig.getValueBool("download_only")) { if (verboseLogging) { addLogEntry("Maximum allowed open files: " ~ maxOpenFiles, ["verbose"]); addLogEntry("Maximum allowed inotify user watches: " ~ maxInotifyWatches, ["verbose"]); } } // Configure the monitor class filesystemMonitor = new Monitor(appConfig, selectiveSync); // Delegated function for when inotify detects a new local directory has been created filesystemMonitor.onDirCreated = delegate(string path) { // Handle .folder creation if skip_dotfiles is enabled if ((appConfig.getValueBool("skip_dotfiles")) && (isDotFile(path))) { if (verboseLogging) {addLogEntry("[M] Skipping watching local path - .folder found & --skip-dot-files enabled: " ~ path, ["verbose"]);} } else { if (verboseLogging) {addLogEntry("[M] Local directory created: " ~ path, ["verbose"]);} try { syncEngineInstance.scanLocalFilesystemPathForNewData(path); } catch (CurlException e) { if (verboseLogging) {addLogEntry("Offline, cannot create remote dir: " ~ path, ["verbose"]);} } catch (Exception e) { addLogEntry("Cannot create remote directory: " ~ e.msg, ["info", "notify"]); } } }; // Delegated function for when inotify detects a local file has been changed filesystemMonitor.onFileChanged = delegate(string[] changedLocalFilesToUploadToOneDrive) { // Handle a potentially locally changed file // Logging for this event moved to handleLocalFileTrigger() due to threading and false triggers from scanLocalFilesystemPathForNewData() above syncEngineInstance.handleLocalFileTrigger(changedLocalFilesToUploadToOneDrive); if (verboseLogging) {addLogEntry("[M] Total number of local file(s) added or changed: " ~ to!string(changedLocalFilesToUploadToOneDrive.length), ["verbose"]);} }; // Delegated function for when inotify detects a delete event filesystemMonitor.onDelete = delegate(string path) { if (verboseLogging) {addLogEntry("[M] Local item deleted: " ~ path, ["verbose"]);} try { // The path has been deleted .. we cannot use isDir or isFile to advise what was deleted. This is the best we can Do addLogEntry("The operating system sent a deletion notification. Trying to delete this item as requested: " ~ path); // perform the delete action syncEngineInstance.deleteByPath(path); } catch (CurlException e) { if (verboseLogging) {addLogEntry("Offline, cannot delete item: " ~ path, ["verbose"]);} } catch (SyncException e) { if (e.msg == "The item to delete is not in the local database") { if (verboseLogging) {addLogEntry("Item cannot be deleted from Microsoft OneDrive because it was not found in the local database", ["verbose"]);} } else { addLogEntry("Cannot delete remote item: " ~ e.msg, ["info", "notify"]); } } catch (Exception e) { addLogEntry("Cannot delete remote item: " ~ e.msg, ["info", "notify"]); } }; // Delegated function for when inotify detects a move event filesystemMonitor.onMove = delegate(string from, string to) { if (verboseLogging) {addLogEntry("[M] Local item moved: " ~ from ~ " -> " ~ to, ["verbose"]);} try { // Handle .folder -> folder if skip_dotfiles is enabled if ((appConfig.getValueBool("skip_dotfiles")) && (isDotFile(from))) { // .folder -> folder handling - has to be handled as a new folder syncEngineInstance.scanLocalFilesystemPathForNewData(to); } else { syncEngineInstance.uploadMoveItem(from, to); } } catch (CurlException e) { if (verboseLogging) {addLogEntry("Offline, cannot move item !", ["verbose"]);} } catch (Exception e) { addLogEntry("Cannot move item: " ~ e.msg, ["info", "notify"]); } }; // Initialise the local filesystem monitor class using inotify to monitor for local filesystem changes // If we are in a --download-only method of operation, we do not enable local filesystem monitoring if (!appConfig.getValueBool("download_only")) { // Not using --download-only try { addLogEntry("Initialising filesystem inotify monitoring ...", ["info", "notify"]); filesystemMonitor.initialise(); addLogEntry("Performing initial synchronisation to ensure consistent local state ..."); } catch (MonitorException e) { // monitor class initialisation failed addLogEntry("ERROR: " ~ e.msg); return EXIT_FAILURE; } } // Filesystem monitor loop variables // Immutables immutable auto checkOnlineInterval = dur!"seconds"(appConfig.getValueLong("monitor_interval")); immutable auto githubCheckInterval = dur!"seconds"(86400); immutable ulong fullScanFrequency = appConfig.getValueLong("monitor_fullscan_frequency"); immutable ulong logOutputSuppressionInterval = appConfig.getValueLong("monitor_log_frequency"); immutable bool webhookEnabled = appConfig.getValueBool("webhook_enabled"); immutable string loopStartOutputMessage = "################################################## NEW LOOP ##################################################"; immutable string loopStopOutputMessage = "################################################ LOOP COMPLETE ###############################################"; // Changeable variables ulong monitorLoopFullCount = 0; ulong fullScanFrequencyLoopCount = 0; ulong monitorLogOutputLoopCount = 0; MonoTime lastCheckTime = MonoTime.currTime(); MonoTime lastGitHubCheckTime = MonoTime.currTime(); while (performFileSystemMonitoring) { // Do we need to validate the runtimeSyncDirectory to check for the presence of a '.nosync' file - the disk may have been ejected .. checkForNoMountScenario(); // If we are in a --download-only method of operation, there is no filesystem monitoring, so no inotify events to check if (!appConfig.getValueBool("download_only")) { // Process any inotify events processInotifyEvents(true); } // Webhook Notification Handling bool notificationReceived = false; // Check for notifications pushed from Microsoft to the webhook if (webhookEnabled) { // Create a subscription on the first run, or renew the subscription // on subsequent runs when it is about to expire. if (oneDriveWebhook is null) { oneDriveWebhook = new OneDriveWebhook(thisTid, appConfig); oneDriveWebhook.serve(); } else { oneDriveWebhook.createOrRenewSubscription(); } } // Get the current time this loop is starting auto currentTime = MonoTime.currTime(); // Do we perform a sync with OneDrive? if ((currentTime - lastCheckTime >= checkOnlineInterval) || (monitorLoopFullCount == 0)) { // Increment relevant counters monitorLoopFullCount++; fullScanFrequencyLoopCount++; monitorLogOutputLoopCount++; // If full scan at a specific frequency enabled? if (fullScanFrequency > 0) { // Full Scan set for some 'frequency' - do we flag to perform a full scan of the online data? if (fullScanFrequencyLoopCount > fullScanFrequency) { // set full scan trigger for true up if (debugLogging) {addLogEntry("Enabling Full Scan True Up (fullScanFrequencyLoopCount > fullScanFrequency), resetting fullScanFrequencyLoopCount = 1", ["debug"]);} fullScanFrequencyLoopCount = 1; appConfig.fullScanTrueUpRequired = true; } else { // unset full scan trigger for true up if (debugLogging) {addLogEntry("Disabling Full Scan True Up", ["debug"]);} appConfig.fullScanTrueUpRequired = false; } } else { // No it is disabled - ensure this is false appConfig.fullScanTrueUpRequired = false; } // Loop Start if (debugLogging) { addLogEntry(loopStartOutputMessage, ["debug"]); addLogEntry("Total Run-Time Loop Number: " ~ to!string(monitorLoopFullCount), ["debug"]); addLogEntry("Full Scan Frequency Loop Number: " ~ to!string(fullScanFrequencyLoopCount), ["debug"]); } SysTime startFunctionProcessingTime = Clock.currTime(); if (debugLogging) {addLogEntry("Start Monitor Loop Time: " ~ to!string(startFunctionProcessingTime), ["debug"]);} // Do we perform any monitor console logging output suppression? // 'monitor_log_frequency' controls how often, in a non-verbose application output mode, how often // the full output of what is occurring is done. This is done to lessen the 'verbosity' of non-verbose // logging, but only when running in --monitor if (monitorLogOutputLoopCount > logOutputSuppressionInterval) { // re-enable the logging output as required monitorLogOutputLoopCount = 1; if (debugLogging) {addLogEntry("Allowing initial sync log output", ["debug"]);} appConfig.suppressLoggingOutput = false; } else { // do we suppress the logging output to absolute minimal if (monitorLoopFullCount == 1) { // application startup with --monitor if (debugLogging) {addLogEntry("Allowing initial sync log output", ["debug"]);} appConfig.suppressLoggingOutput = false; } else { // only suppress if we are not doing --verbose or higher if (appConfig.verbosityCount == 0) { if (debugLogging) {addLogEntry("Suppressing --monitor log output", ["debug"]);} appConfig.suppressLoggingOutput = true; } else { if (debugLogging) {addLogEntry("Allowing log output", ["debug"]);} appConfig.suppressLoggingOutput = false; } } } // How long has the application been running for? auto elapsedTime = Clock.currTime() - applicationStartTime; if (debugLogging) {addLogEntry("Application run-time thus far: " ~ to!string(elapsedTime), ["debug"]);} // Need to re-validate that the client is still online for this loop if (testInternetReachability(appConfig)) { // Starting a sync - we are online addLogEntry("Starting a sync with Microsoft OneDrive"); // Attempt to reset syncFailures from any prior loop syncEngineInstance.resetSyncFailures(); // Update cached quota details from online as this may have changed online in the background outside of this application syncEngineInstance.freshenCachedDriveQuotaDetails(); // Did the user specify --upload-only? if (appConfig.getValueBool("upload_only")) { // Perform the --upload-only sync process performUploadOnlySyncProcess(localPath, filesystemMonitor); } else { // Perform the standard sync process performStandardSyncProcess(localPath, filesystemMonitor); } // Handle any new inotify events processInotifyEvents(true); // Detail the outcome of the sync process displaySyncOutcome(); // Cleanup sync process arrays syncEngineInstance.cleanupArrays(); // Write WAL and SHM data to file for this loop and release memory used by in-memory processing if (debugLogging) {addLogEntry("Merge contents of WAL and SHM files into main database file", ["debug"]);} itemDB.performCheckpoint("TRUNCATE"); } else { // Not online addLogEntry("Microsoft OneDrive service is not reachable at this time. Will re-try on next sync attempt."); } // Output end of loop processing times SysTime endFunctionProcessingTime = Clock.currTime(); if (debugLogging) { addLogEntry("End Monitor Loop Time: " ~ to!string(endFunctionProcessingTime), ["debug"]); addLogEntry("Elapsed Monitor Loop Processing Time: " ~ to!string((endFunctionProcessingTime - startFunctionProcessingTime)), ["debug"]); } // Release all the curl instances used during this loop // New curl instances will be established on next loop if (debugLogging) {addLogEntry("CurlEngine Pool Size PRE Cleanup: " ~ to!string(curlEnginePoolLength()), ["debug"]);} releaseAllCurlInstances(); // Release all CurlEngine instances if (debugLogging) {addLogEntry("CurlEngine Pool Size POST Cleanup: " ~ to!string(curlEnginePoolLength()) , ["debug"]);} // Display memory details before garbage collection if (displayMemoryUsage) displayMemoryUsagePreGC(); // Perform Garbage Collection GC.collect(); // Return free memory to the OS GC.minimize(); // Display memory details after garbage collection if (displayMemoryUsage) displayMemoryUsagePostGC(); // Log that this loop is complete if (debugLogging) {addLogEntry(loopStopOutputMessage, ["debug"]);} // performSync complete, set lastCheckTime to current time lastCheckTime = MonoTime.currTime(); // Developer break via config option if (appConfig.getValueLong("monitor_max_loop") > 0) { // developer set option to limit --monitor loops if (monitorLoopFullCount == (appConfig.getValueLong("monitor_max_loop"))) { performFileSystemMonitoring = false; addLogEntry("Exiting after " ~ to!string(monitorLoopFullCount) ~ " loops due to developer set option"); } } } if (performFileSystemMonitoring) { auto nextCheckTime = lastCheckTime + checkOnlineInterval; currentTime = MonoTime.currTime(); auto sleepTime = nextCheckTime - currentTime; if (debugLogging) {addLogEntry("Sleep for " ~ to!string(sleepTime), ["debug"]);} if(filesystemMonitor.initialised || webhookEnabled) { if(filesystemMonitor.initialised) { // If local monitor is on and is waiting (previous event was not from webhook) // start the worker and wait for event if (!notificationReceived) { filesystemMonitor.send(true); } } if(webhookEnabled) { // if onedrive webhook is enabled // update sleep time based on renew interval Duration nextWebhookCheckDuration = oneDriveWebhook.getNextExpirationCheckDuration(); if (nextWebhookCheckDuration < sleepTime) { sleepTime = nextWebhookCheckDuration; if (debugLogging) {addLogEntry("Update sleeping time to " ~ to!string(sleepTime), ["debug"]);} } // Webhook Notification reset to false for this loop notificationReceived = false; } int res = 1; // Process incoming notifications if any. auto signalExists = receiveTimeout(sleepTime, (int msg) {res = msg;},(ulong _) {notificationReceived = true;}); // Debug values if (debugLogging) { addLogEntry("signalExists = " ~ to!string(signalExists), ["debug"]); addLogEntry("worker status = " ~ to!string(res), ["debug"]); addLogEntry("notificationReceived = " ~ to!string(notificationReceived), ["debug"]); } // Empirical evidence shows that Microsoft often sends multiple // notifications for one single change, so we need a loop to exhaust // all signals that were queued up by the webhook. The notifications // do not contain any actual changes, and we will always rely do the // delta endpoint to sync to latest. Therefore, only one sync run is // good enough to catch up for multiple notifications. if (notificationReceived) { int signalCount = 1; while (true) { signalExists = receiveTimeout(dur!"seconds"(-1), (ulong _) {}); if (signalExists) { signalCount++; } else { addLogEntry("Received " ~ to!string(signalCount) ~ " refresh signals from the webhook"); oneDriveWebhookCallback(); break; } } } if(res == -1) { addLogEntry("ERROR: Monitor worker failed."); monitorFailures = true; performFileSystemMonitoring = false; } } else { // no hooks available, nothing to check Thread.sleep(sleepTime); } } } } } else { // Exit application as the sync engine could not be initialised addLogEntry("Application Sync Engine could not be initialised correctly"); // Use exit scope return EXIT_FAILURE; } // Exit application using exit scope if (!syncEngineInstance.syncFailures && !monitorFailures) { return EXIT_SUCCESS; } else { return EXIT_FAILURE; } } // Retrieves the maximum number of open files allowed by the system string getMaxOpenFiles() { // Predefined Versions // https://dlang.org/spec/version.html#predefined-versions version (linux) { try { // Read max open files from procfs on Linux return strip(readText("/proc/sys/fs/file-max")); } catch (Exception e) { return "Unknown (Error reading /proc/sys/fs/file-max)"; } } else version (FreeBSD) { try { // Read max open files using sysctl on FreeBSD return strip(executeShell("sysctl -n kern.maxfiles").output); } catch (Exception e) { return "Unknown (sysctl error)"; } } else version (OpenBSD) { try { // Read max open files using sysctl on OpenBSD return strip(executeShell("sysctl -n kern.maxfiles").output); } catch (Exception e) { return "Unknown (sysctl error)"; } } else { return "Unsupported platform"; } } // Retrieves the maximum inotify watches allowed by the system string getMaxInotifyWatches() { // Predefined Versions // https://dlang.org/spec/version.html#predefined-versions version (linux) { try { // Read max inotify watches from procfs on Linux return strip(readText("/proc/sys/fs/inotify/max_user_watches")); } catch (Exception e) { return "Unknown (Error reading /proc/sys/fs/inotify/max_user_watches)"; } } else version (FreeBSD) { // FreeBSD uses kqueue instead of inotify, no direct equivalent return "N/A (uses kqueue)"; } else version (OpenBSD) { // OpenBSD uses kqueue instead of inotify, no direct equivalent return "N/A (uses kqueue)"; } else { return "Unsupported platform"; } } // Print error message when --sync or --monitor has not been used and no valid 'no-sync' operation was requested void printMissingOperationalSwitchesError() { // notify the user that --sync or --monitor were missing addLogEntry(); addLogEntry("Your command line input is missing either the '--sync' or '--monitor' switches. Please include one (but not both) of these switches in your command line, or refer to 'onedrive --help' for additional guidance."); addLogEntry(); addLogEntry("It is important to note that you must include one of these two arguments in your command line for the application to perform a synchronisation with Microsoft OneDrive"); addLogEntry(); } // Function used for webhook callbacks to perform specific activities void oneDriveWebhookCallback() { // If we are in a --download-only method of operation, there is no filesystem monitoring, so no inotify events to check if (!appConfig.getValueBool("download_only")) { // Handle inotify events processInotifyEvents(true); } // Download data from OneDrive last syncEngineInstance.syncOneDriveAccountToLocalDisk(); if (appConfig.getValueBool("monitor")) { // Handle inotify events processInotifyEvents(true); } } // Perform only an upload of data when using --upload-only void performUploadOnlySyncProcess(string localPath, Monitor filesystemMonitor = null) { // Perform the local database consistency check, picking up locally modified data and uploading this to OneDrive syncEngineInstance.performDatabaseConsistencyAndIntegrityCheck(); if (appConfig.getValueBool("monitor")) { // Handle any inotify events whilst the DB was being scanned processInotifyEvents(true); } // Scan the configured 'sync_dir' for new data to upload syncEngineInstance.scanLocalFilesystemPathForNewData(localPath); if (appConfig.getValueBool("monitor")) { // Handle any new inotify events whilst the local filesystem was being scanned processInotifyEvents(true); } } // Perform the normal application sync process void performStandardSyncProcess(string localPath, Monitor filesystemMonitor = null) { // If we are performing log suppression, output this message so the user knows what is happening if (appConfig.suppressLoggingOutput) { addLogEntry("Syncing changes from Microsoft OneDrive ..."); } // Zero out these arrays syncEngineInstance.fileDownloadFailures = []; syncEngineInstance.fileUploadFailures = []; // Which way do we sync first? // OneDrive first then local changes (normal operational process that uses OneDrive as the source of truth) // Local First then OneDrive changes (alternate operation process to use local files as source of truth) if (appConfig.getValueBool("local_first")) { // Local data first // Perform the local database consistency check, picking up locally modified data and uploading this to OneDrive syncEngineInstance.performDatabaseConsistencyAndIntegrityCheck(); if (appConfig.getValueBool("monitor")) { // Handle any inotify events whilst the DB was being scanned processInotifyEvents(true); } // Scan the configured 'sync_dir' for new data to upload to OneDrive syncEngineInstance.scanLocalFilesystemPathForNewData(localPath); if (appConfig.getValueBool("monitor")) { // Handle any new inotify events whilst the local filesystem was being scanned processInotifyEvents(true); } // Download data from OneDrive last syncEngineInstance.syncOneDriveAccountToLocalDisk(); if (appConfig.getValueBool("monitor")) { // Cancel out any inotify events from downloading data processInotifyEvents(false); } } else { // Normal sync // Download data from OneDrive first syncEngineInstance.syncOneDriveAccountToLocalDisk(); if (appConfig.getValueBool("monitor")) { // Cancel out any inotify events from downloading data processInotifyEvents(false); } // Perform the local database consistency check, picking up locally modified data and uploading this to OneDrive syncEngineInstance.performDatabaseConsistencyAndIntegrityCheck(); if (appConfig.getValueBool("monitor")) { // Handle any inotify events whilst the DB was being scanned processInotifyEvents(true); } // Is --download-only NOT configured? if (!appConfig.getValueBool("download_only")) { // Scan the configured 'sync_dir' for new data to upload to OneDrive syncEngineInstance.scanLocalFilesystemPathForNewData(localPath); if (appConfig.getValueBool("monitor")) { // Handle any new inotify events whilst the local filesystem was being scanned processInotifyEvents(true); } // If we are not doing a 'force_children_scan' perform a true-up // 'force_children_scan' is used when using /children rather than /delta and it is not efficient to re-run this exact same process twice if (!appConfig.getValueBool("force_children_scan")) { // Perform the final true up scan to ensure we have correctly replicated the current online state locally if (!appConfig.suppressLoggingOutput) { addLogEntry("Performing a last examination of the most recent online data within Microsoft OneDrive to complete the reconciliation process"); } // We pass in the 'appConfig.fullScanTrueUpRequired' value which then flags do we use the configured 'deltaLink' // If 'appConfig.fullScanTrueUpRequired' is true, we do not use the 'deltaLink' if we are in --monitor mode, thus forcing a full scan true up syncEngineInstance.syncOneDriveAccountToLocalDisk(); if (appConfig.getValueBool("monitor")) { // Cancel out any inotify events from downloading data processInotifyEvents(false); } } } } } // PRocess any inotify events void processInotifyEvents(bool updateFlag) { // Attempt to process or cancel inotify events // filesystemMonitor.update will throw this, thus needs to be caught // monitor.MonitorException@src/monitor.d(549): inotify queue overflow: some events may be lost (Interrupted system call) try { // Process any inotify events or cancel events based on flag value // True = process // False = cancel filesystemMonitor.update(updateFlag); } catch (MonitorException e) { // Catch any exceptions thrown by inotify / monitor engine addLogEntry("ERROR: The following inotify error was generated: " ~ e.msg); } } // Display the sync outcome void displaySyncOutcome() { // Detail any download or upload transfer failures syncEngineInstance.displaySyncFailures(); // Sync is either complete or partially complete if (!syncEngineInstance.syncFailures) { // No download or upload issues if (!appConfig.getValueBool("monitor")) addLogEntry(); // Add an additional line break so that this is clear when using --sync addLogEntry("Sync with Microsoft OneDrive is complete"); } else { addLogEntry(); addLogEntry("Sync with Microsoft OneDrive has completed, however there are items that failed to sync."); // Due to how the OneDrive API works 'changes' such as add new files online, rename files online, delete files online are only sent once when using the /delta API call. // That we failed to download it, we need to track that, and then issue a --resync to download any of these failed files .. unfortunate, but there is no easy way here if (!syncEngineInstance.fileDownloadFailures.empty) { addLogEntry("To fix any download failures you may need to perform a --resync to ensure this system is correctly synced with your Microsoft OneDrive Account"); } if (!syncEngineInstance.fileUploadFailures.empty) { addLogEntry("To fix any upload failures you may need to perform a --resync to ensure this system is correctly synced with your Microsoft OneDrive Account"); } // So that from a logging perspective these messages are clear, add a line break in addLogEntry(); } } // Perform database file removal void processResyncDatabaseRemoval(string databaseFilePathToRemove) { // Log what we are doing if (debugLogging) {addLogEntry("Testing if we have exclusive access to local database file", ["debug"]);} // Are we the only running instance? Test that we can open the database file path itemDB = new ItemDatabase(databaseFilePathToRemove); // did we successfully initialise the database class? if (!itemDB.isDatabaseInitialised()) { // no .. destroy class itemDB = null; // exit application - void function, force exit this way exit(EXIT_FAILURE); } // If we have exclusive access we will not have exited // destroy access test itemDB = null; // delete application sync state addLogEntry("Deleting the saved application sync status ..."); if (!dryRun) { safeRemove(databaseFilePathToRemove); } else { // --dry-run scenario ... technically we should not be making any local file changes ....... addLogEntry("DRY RUN: Not removing the saved application sync status"); } } // Clean up the local database files void cleanupDatabaseFiles(string activeDatabaseFileName) { // Temp variables string databaseShmFile = activeDatabaseFileName ~ "-shm"; string databaseWalFile = activeDatabaseFileName ~ "-wal"; // Are we performing a --dry-run? if (dryRun) { // If the dry run database exists, clean this up if (exists(activeDatabaseFileName)) { // remove the dry run database file if (debugLogging) {addLogEntry("DRY-RUN: Removing items-dryrun.sqlite3 as it still exists for some reason", ["debug"]);} safeRemove(activeDatabaseFileName); } } else { // we may have not been using --dry-run, however we may have been running some operations that use a dry-run database, and this needs to be explicitly cleaned up if (exists(appConfig.databaseFilePathDryRun)) { if (debugLogging) {addLogEntry("Removing items-dryrun.sqlite3 as it still exists for some reason post being used for non-dryrun operations", ["debug"]);} safeRemove(appConfig.databaseFilePathDryRun); } } // Silent cleanup of -shm file if it exists if (exists(databaseShmFile)) { // Configure the log message string logMessage = "Removing " ~ baseName(databaseShmFile) ~ " as it still exists for some reason"; // Is this a --dry-run scenario if (dryRun) { logMessage = "DRY-RUN: " ~ logMessage; } // Remove -shm file if (debugLogging) {addLogEntry(logMessage, ["debug"]);} safeRemove(databaseShmFile); } // Silent cleanup of wal files if it exists if (exists(databaseWalFile)) { // Configure the log message string logMessage = "Removing " ~ baseName(databaseWalFile) ~ " as it still exists for some reason"; // Is this a --dry-run scenario if (dryRun) { logMessage = "DRY-RUN: " ~ logMessage; } // Remove -wal file if (debugLogging) {addLogEntry(logMessage, ["debug"]);} safeRemove(databaseWalFile); } } // Perform a check to see if this is a mount point, and if the 'mount' has gone void checkForNoMountScenario() { // If this is a 'mounted' folder, the 'mount point' should have this file to help the application stop any action to preserve data because the drive to mount is not currently mounted if (appConfig.getValueBool("check_nomount")) { // we were asked to check the mount point for the presence of a '.nosync' file if (exists(".nosync")) { addLogEntry("ERROR: .nosync file found in directory mount point. Aborting application startup process to safeguard data.", ["info", "notify"]); // Perform the shutdown process performSynchronisedExitProcess("check_nomount"); // Exit exit(EXIT_FAILURE); } } } // Setup a signal handler for catching SIGINT, SIGTERM and SIGSEGV (CTRL-C and others) during application execution void setupSignalHandler() { sigaction_t action; action.sa_handler = &exitViaSignalHandler; // Direct function pointer assignment sigemptyset(&action.sa_mask); // Initialize the signal set to empty action.sa_flags = 0; sigaction(SIGINT, &action, null); // Interrupt from keyboard sigaction(SIGTERM, &action, null); // Termination signal sigaction(SIGSEGV, &action, null); // Invalid Memory Access signal } // Catch SIGINT (CTRL-C), SIGTERM (kill) and SIGSEGV (invalid memory access), handle rapid repeat CTRL-C presses extern(C) nothrow @nogc @system void exitViaSignalHandler(int signo) { // Update global exitHandlerTriggered flag so that objects that depend on this know we are shutting down exitHandlerTriggered = true; // Catch the generation of SIGSEV post SIGINT or SIGTERM event if (signo == SIGSEGV) { // Was SIGTERM used? if (!sigtermHandlerTriggered) { // No .. so most likely SIGINT (CTRL-C) printf("Due to a termination signal, internal processing stopped abruptly. The application will now exit in a unclean manner.\n"); exit(130); } else { // High probability of being shutdown by systemd, for example: systemctl --user stop onedrive // Exit in a manner that does not trigger an exit failure in systemd exit(0); } } if (signo == SIGTERM) { // systemd will use SIGTERM to terminate a running process sigtermHandlerTriggered = true; } if (shutdownInProgress) { return; // Ignore subsequent presses } else { // Disable logging suppression appConfig.suppressLoggingOutput = false; // Flag we are shutting down shutdownInProgress = true; try { assumeNoGC ( () { // Log that a termination signal was caught addLogEntry("\nReceived termination signal, attempting to cleanly shutdown application"); // Try and shutdown in a safe and synchronised manner performSynchronisedExitProcess("SIGINT-SIGTERM-HANDLER"); })(); } catch (Exception e) { // Any output here will cause a GC allocation // - Error: `@nogc` function `main.exitHandler` cannot call non-@nogc function `std.stdio.writeln!string.writeln` // - Error: cannot use operator `~` in `@nogc` function `main.exitHandler` // writeln("Exception during shutdown: " ~ e.msg); } // Exit the process with the provided exit code exit(signo); } } // Handle application exit void performSynchronisedExitProcess(string scopeCaller = null) { synchronized { // Perform cleanup and shutdown of various services and resources try { // Log who called this function if (debugLogging) {addLogEntry("performSynchronisedExitProcess called by: " ~ scopeCaller, ["debug"]);} // Shutdown the OneDrive Webhook instance shutdownOneDriveWebhook(); // Shutdown any local filesystem monitoring shutdownFilesystemMonitor(); // Shutdown the sync engine if (scopeCaller == "SIGINT-SIGTERM-HANDLER") { // Wait for all parallel jobs that depend on the database being available to complete addLogEntry("Waiting for any existing upload|download process to complete"); } shutdownSyncEngine(); // Release all CurlEngine instances releaseAllCurlInstances(); // Shutdown the client side filtering objects shutdownSelectiveSync(); // Shutdown the database shutdownDatabase(); // Shutdown the application configuration objects - nothing should be active now shutdownAppConfig(); // Shutdown application logging shutdownApplicationLogging(); } catch (Exception e) { addLogEntry("Error during performStandardExitProcess: " ~ e.toString(), ["error"]); } } } void shutdownOneDriveWebhook() { if (oneDriveWebhook !is null) { if (debugLogging) {addLogEntry("Shutting down OneDrive Webhook instance", ["debug"]);} oneDriveWebhook.stop(); object.destroy(oneDriveWebhook); oneDriveWebhook = null; if (debugLogging) {addLogEntry("Shutdown of OneDrive Webhook instance is complete", ["debug"]);} } } void shutdownFilesystemMonitor() { if (filesystemMonitor !is null) { if (debugLogging) {addLogEntry("Shutting down Filesystem Monitoring instance", ["debug"]);} filesystemMonitor.shutdown(); object.destroy(filesystemMonitor); filesystemMonitor = null; if (debugLogging) {addLogEntry("Shutdown of Filesystem Monitoring instance is complete", ["debug"]);} } } void shutdownSelectiveSync() { if (selectiveSync !is null) { if (debugLogging) {addLogEntry("Shutting down Client Side Filtering instance", ["debug"]);} selectiveSync.shutdown(); object.destroy(selectiveSync); selectiveSync = null; if (debugLogging) {addLogEntry("Shutdown of Client Side Filtering instance is complete", ["debug"]);} } } void shutdownSyncEngine() { if (syncEngineInstance !is null) { if (debugLogging) {addLogEntry("Shutting down Sync Engine instance", ["debug"]);} syncEngineInstance.shutdown(); // Make sure any running thread completes first object.destroy(syncEngineInstance); syncEngineInstance = null; if (debugLogging) {addLogEntry("Shutdown Sync Engine instance is complete", ["debug"]);} } } void shutdownDatabase() { if (itemDB !is null && itemDB.isDatabaseInitialised()) { if (debugLogging) {addLogEntry("Shutting down Database instance", ["debug"]);} // Write WAL and SHM data to file if (debugLogging) {addLogEntry("Merge contents of WAL and SHM files into main database file before shutting down database", ["debug"]);} itemDB.performCheckpoint("TRUNCATE"); // Do we perform a database vacuum? if (performDatabaseVacuum) { // Logging to attempt this is denoted from performVacuum() - so no need to confirm here itemDB.performVacuum(); // If this completes, it is denoted from performVacuum() - so no need to confirm here } // Close the DB File Handle itemDB.closeDatabaseFile(); object.destroy(itemDB); cleanupDatabaseFiles(runtimeDatabaseFile); itemDB = null; if (debugLogging) {addLogEntry("Shutdown of Database instance is complete", ["debug"]);} } } void shutdownAppConfig() { if (appConfig !is null) { if (debugLogging) {addLogEntry("Shutting down Application Configuration instance", ["debug"]);} object.destroy(appConfig); appConfig = null; if (debugLogging) {addLogEntry("Shutdown of Application Configuration instance is complete", ["debug"]);} } } void shutdownApplicationLogging() { // Log that we are exiting if (loggingStillInitialised()) { if (loggingActive()) { // join all threads thread_joinAll(); if (debugLogging) {addLogEntry("Application is exiting", ["debug"]);} addLogEntry("#######################################################################################################################################", ["logFileOnly"]); // Destroy the shared logging buffer which flushes any remaining logs if (debugLogging) {addLogEntry("Shutting down Application Logging instance", ["debug"]);} // Allow any logging complete before we exit Thread.sleep(dur!("msecs")(500)); // Shutdown Logging which also sets logBuffer to null shutdownLogging(); } } } string compilerDetails() { version(DigitalMars) enum compiler = "DMD"; else version(LDC) enum compiler = "LDC"; else version(GNU) enum compiler = "GDC"; else enum compiler = "Unknown compiler"; string compilerString = compiler ~ " " ~ to!string(__VERSION__); return compilerString; }onedrive-2.5.5/src/monitor.d000066400000000000000000000606011476564400300157750ustar00rootroot00000000000000// What is this module called? module monitor; // What does this module require to function? import core.stdc.errno; import core.stdc.stdlib; import core.sys.linux.sys.inotify; import core.sys.posix.poll; import core.sys.posix.unistd; import core.sys.posix.sys.select; import core.thread; import core.time; import std.algorithm; import std.concurrency; import std.exception; import std.file; import std.path; import std.process; import std.regex; import std.stdio; import std.string; import std.conv; import core.sync.mutex; // What other modules that we have created do we need to import? import config; import util; import log; import clientSideFiltering; // Relevant inotify events private immutable uint32_t mask = IN_CLOSE_WRITE | IN_CREATE | IN_DELETE | IN_MOVE | IN_IGNORED | IN_Q_OVERFLOW; class MonitorException: ErrnoException { @safe this(string msg, string file = __FILE__, size_t line = __LINE__) { super(msg, file, line); } } class MonitorBackgroundWorker { // inotify file descriptor int fd; Pipe p; bool isAlive; this() { isAlive = true; p = pipe(); } shared void initialise() { fd = inotify_init(); if (fd < 0) throw new MonitorException("inotify_init failed"); } // Add this path to be monitored shared int addInotifyWatch(string pathname) { int wd = inotify_add_watch(fd, toStringz(pathname), mask); if (wd < 0) { if (errno() == ENOSPC) { // Predefined Versions // https://dlang.org/spec/version.html#predefined-versions version (linux) { // Read max inotify watches from procfs on Linux ulong maxInotifyWatches = to!int(strip(readText("/proc/sys/fs/inotify/max_user_watches"))); addLogEntry("The user limit on the total number of inotify watches has been reached."); addLogEntry("Your current limit of inotify watches is: " ~ to!string(maxInotifyWatches)); addLogEntry("It is recommended that you change the max number of inotify watches to at least double your existing value."); addLogEntry("To change the current max number of watches to " ~ to!string((maxInotifyWatches * 2)) ~ " run:"); addLogEntry("EXAMPLE: sudo sysctl fs.inotify.max_user_watches=" ~ to!string((maxInotifyWatches * 2))); } else { // some other platform addLogEntry("The user limit on the total number of inotify watches has been reached."); addLogEntry("Please seek support from your distribution on how to increase the max number of inotify watches to at least double your existing value."); } } if (errno() == 13) { if (verboseLogging) {addLogEntry("WARNING: inotify_add_watch failed - permission denied: " ~ pathname, ["verbose"]);} } // Flag any other errors addLogEntry("ERROR: inotify_add_watch failed: " ~ pathname); return wd; } // Add path to inotify watch - required regardless if a '.folder' or 'folder' if (debugLogging) {addLogEntry("inotify_add_watch successfully added for: " ~ pathname, ["debug"]);} // Do we log that we are monitoring this directory? if (isDir(pathname)) { // Log that this is directory is being monitored if (verboseLogging) {addLogEntry("Monitoring directory: " ~ pathname, ["verbose"]);} } return wd; } shared int removeInotifyWatch(int wd) { assert(fd > 0, "File descriptor 'fd' is invalid."); assert(wd > 0, "Watch descriptor 'wd' is invalid."); // Debug logging of the inotify watch being removed if (debugLogging) {addLogEntry("Attempting to remove inotify watch: fd=" ~ fd.to!string ~ ", wd=" ~ wd.to!string, ["debug"]);} // return the value of performing the action return inotify_rm_watch(fd, wd); } shared void watch(Tid callerTid) { // On failure, send -1 to caller int res; // wait for the caller to be ready receiveOnly!int(); while (isAlive) { fd_set fds; FD_ZERO (&fds); FD_SET(fd, &fds); // Listen for messages from the caller FD_SET((cast()p).readEnd.fileno, &fds); res = select(FD_SETSIZE, &fds, null, null, null); if(res == -1) { if(errno() == EINTR) { // Received an interrupt signal but no events are available // directly watch again } else { // Error occurred, tell caller to terminate. callerTid.send(-1); break; } } else { // Wake up caller callerTid.send(1); // wait for the caller to be ready if (isAlive) isAlive = receiveOnly!bool(); } } } shared void interrupt() { isAlive = false; (cast()p).writeEnd.writeln("done"); (cast()p).writeEnd.flush(); } shared void shutdown() { isAlive = false; if (fd > 0) { close(fd); fd = 0; (cast()p).close(); } } } void startMonitorJob(shared(MonitorBackgroundWorker) worker, Tid callerTid) { try { worker.watch(callerTid); } catch (OwnerTerminated error) { // caller is terminated worker.shutdown(); } } enum ActionType { moved, deleted, changed, createDir } struct Action { ActionType type; bool skipped; string src; string dst; } struct ActionHolder { Action[] actions; size_t[string] srcMap; void append(ActionType type, string src, string dst=null) { size_t[] pendingTargets; switch (type) { case ActionType.changed: if (src in srcMap && actions[srcMap[src]].type == ActionType.changed) { // skip duplicate operations return; } break; case ActionType.createDir: break; case ActionType.deleted: if (src in srcMap) { size_t pendingTarget = srcMap[src]; // Skip operations require reading local file that is gone switch (actions[pendingTarget].type) { case ActionType.changed: case ActionType.createDir: actions[srcMap[src]].skipped = true; srcMap.remove(src); break; default: break; } } break; case ActionType.moved: for(int i = 0; i < actions.length; i++) { // Only match for latest operation if (actions[i].src in srcMap) { switch (actions[i].type) { case ActionType.changed: case ActionType.createDir: // check if the source is the prefix of the target string prefix = src ~ "/"; string target = actions[i].src; if (prefix[0] != '.') prefix = "./" ~ prefix; if (target[0] != '.') target = "./" ~ target; string comm = commonPrefix(prefix, target); if (src == actions[i].src || comm.length == prefix.length) { // Hold operations require reading local file that is moved after the target is moved online pendingTargets ~= i; actions[i].skipped = true; srcMap.remove(actions[i].src); if (comm.length == target.length) actions[i].src = dst; else actions[i].src = dst ~ target[comm.length - 1 .. target.length]; } break; default: break; } } } break; default: break; } actions ~= Action(type, false, src, dst); srcMap[src] = actions.length - 1; foreach (pendingTarget; pendingTargets) { actions ~= actions[pendingTarget]; actions[$-1].skipped = false; srcMap[actions[$-1].src] = actions.length - 1; } } } final class Monitor { // Class variables ApplicationConfig appConfig; ClientSideFiltering selectiveSync; // Are we verbose in logging output bool verbose = false; // skip symbolic links bool skip_symlinks = false; // check for .nosync if enabled bool check_nosync = false; // check if initialised bool initialised = false; // Worker Tid Tid workerTid; // Configure Private Class Variables shared(MonitorBackgroundWorker) worker; // map every inotify watch descriptor to its directory private string[int] wdToDirName; // map the inotify cookies of move_from events to their path private string[int] cookieToPath; // buffer to receive the inotify events private void[] buffer; // Mutex to support thread safe access of inotify watch descriptors private Mutex inotifyMutex; // Configure function delegates void delegate(string path) onDirCreated; void delegate(string[] path) onFileChanged; void delegate(string path) onDelete; void delegate(string from, string to) onMove; // List of paths that were moved, not deleted bool[string] movedNotDeleted; // An array of actions ActionHolder actionHolder; // Configure the class variable to consume the application configuration including selective sync this(ApplicationConfig appConfig, ClientSideFiltering selectiveSync) { this.appConfig = appConfig; this.selectiveSync = selectiveSync; inotifyMutex = new Mutex(); // Define a Mutex for thread-safe access } // The destructor should only clean up resources owned directly by this instance ~this() { object.destroy(worker); } // Initialise the monitor class void initialise() { // Configure the variables skip_symlinks = appConfig.getValueBool("skip_symlinks"); check_nosync = appConfig.getValueBool("check_nosync"); if (appConfig.getValueLong("verbose") > 0) { verbose = true; } assert(onDirCreated && onFileChanged && onDelete && onMove); if (!buffer) buffer = new void[4096]; worker = cast(shared) new MonitorBackgroundWorker; worker.initialise(); // from which point do we start watching for changes? string monitorPath; if (appConfig.getValueString("single_directory") != ""){ // single directory in use, monitor only this path monitorPath = "./" ~ appConfig.getValueString("single_directory"); } else { // default monitorPath = "."; } addRecursive(monitorPath); // Start monitoring workerTid = spawn(&startMonitorJob, worker, thisTid); initialised = true; } // Communication with worker void send(bool isAlive) { workerTid.send(isAlive); } // Shutdown the monitor class void shutdown() { if(!initialised) return; initialised = false; // Release all resources synchronized(inotifyMutex) { // Interrupt the worker to allow removal of inotify watch descriptors worker.interrupt(); // Remove all the inotify watch descriptors removeAll(); // Notify the worker that the monitor has been shutdown worker.interrupt(); send(false); wdToDirName = null; } } // Recursively add this path to be monitored private void addRecursive(string dirname) { // skip non existing/disappeared items if (!exists(dirname)) { if (verboseLogging) {addLogEntry("Not adding non-existing/disappeared directory: " ~ dirname, ["verbose"]);} return; } // Skip the monitoring of any user filtered items if (dirname != ".") { // Is the directory name a match to a skip_dir entry? // The path that needs to be checked needs to include the '/' // This due to if the user has specified in skip_dir an exclusive path: '/path' - that is what must be matched if (isDir(dirname)) { if (selectiveSync.isDirNameExcluded(dirname.strip('.'))) { // dont add a watch for this item if (debugLogging) {addLogEntry("Skipping monitoring due to skip_dir match: " ~ dirname, ["debug"]);} return; } } if (isFile(dirname)) { // Is the filename a match to a skip_file entry? // The path that needs to be checked needs to include the '/' // This due to if the user has specified in skip_file an exclusive path: '/path/file' - that is what must be matched if (selectiveSync.isFileNameExcluded(dirname.strip('.'))) { // dont add a watch for this item if (debugLogging) {addLogEntry("Skipping monitoring due to skip_file match: " ~ dirname, ["debug"]);} return; } } // is the path excluded by sync_list? if (selectiveSync.isPathExcludedViaSyncList(buildNormalizedPath(dirname))) { // dont add a watch for this item if (debugLogging) {addLogEntry("Skipping monitoring due to sync_list match: " ~ dirname, ["debug"]);} return; } } // skip symlinks if configured if (isSymlink(dirname)) { // if config says so we skip all symlinked items if (skip_symlinks) { // dont add a watch for this directory return; } } // Do we need to check for .nosync? Only if check_nosync is true if (check_nosync) { if (exists(buildNormalizedPath(dirname) ~ "/.nosync")) { if (verboseLogging) {addLogEntry("Skipping watching path - .nosync found & --check-for-nosync enabled: " ~ buildNormalizedPath(dirname), ["verbose"]);} return; } } if (isDir(dirname)) { // This is a directory // is the path excluded if skip_dotfiles configured and path is a .folder? if ((selectiveSync.getSkipDotfiles()) && (isDotFile(dirname))) { // dont add a watch for this directory return; } } // passed all potential exclusions // add inotify watch for this path / directory / file if (debugLogging) {addLogEntry("Calling worker.addInotifyWatch() for this dirname: " ~ dirname, ["debug"]);} int wd = worker.addInotifyWatch(dirname); if (wd > 0) { wdToDirName[wd] = buildNormalizedPath(dirname) ~ "/"; } // if this is a directory, recursively add this path if (isDir(dirname)) { // try and get all the directory entities for this path try { auto pathList = dirEntries(dirname, SpanMode.shallow, false); foreach(DirEntry entry; pathList) { if (entry.isDir) { if (debugLogging) {addLogEntry("Calling addRecursive() for this directory: " ~ entry.name, ["debug"]);} addRecursive(entry.name); } } // catch any error which is generated } catch (std.file.FileException e) { // Standard filesystem error displayFileSystemErrorMessage(e.msg, getFunctionName!({})); return; } catch (Exception e) { // Issue #1154 handling // Need to check for: Failed to stat file in error message if (canFind(e.msg, "Failed to stat file")) { // File system access issue addLogEntry("ERROR: The local file system returned an error with the following message:"); addLogEntry(" Error Message: " ~ e.msg); addLogEntry("ACCESS ERROR: Please check your UID and GID access to this file, as the permissions on this file is preventing this application to read it"); addLogEntry("\nFATAL: Forcing exiting application to avoid deleting data due to local file system access issues\n"); // Must force exit here, allow logging to be done forceExit(); } else { // some other error displayFileSystemErrorMessage(e.msg, getFunctionName!({})); return; } } } } // Remove a watch descriptor private void removeAll() { string[int] copy; synchronized(inotifyMutex) { copy = wdToDirName.dup; // Make a thread-safe copy } // Loop through the watch descriptors and remove foreach (wd, path; copy) { remove(wd); } } private void remove(int wd) { assert(wd in wdToDirName); synchronized(inotifyMutex) { int ret = worker.removeInotifyWatch(wd); if (ret < 0) throw new MonitorException("inotify_rm_watch failed"); if (verboseLogging) {addLogEntry("Monitored directory removed: " ~ to!string(wdToDirName[wd]), ["verbose"]);} wdToDirName.remove(wd); } } // Remove the watch descriptors associated to the given path private void remove(const(char)[] path) { path ~= "/"; foreach (wd, dirname; wdToDirName) { if (dirname.startsWith(path)) { int ret = worker.removeInotifyWatch(wd); if (ret < 0) throw new MonitorException("inotify_rm_watch failed"); wdToDirName.remove(wd); if (verboseLogging) {addLogEntry("Monitored directory removed: " ~ dirname, ["verbose"]);} } } } // Return the file path from an inotify event private string getPath(const(inotify_event)* event) { string path = wdToDirName[event.wd]; if (event.len > 0) path ~= fromStringz(event.name.ptr); if (debugLogging) {addLogEntry("inotify path event for: " ~ path, ["debug"]);} return path; } // Update void update(bool useCallbacks = true) { if(!initialised) return; pollfd fds = { fd: worker.fd, events: POLLIN }; while (true) { bool hasNotification = false; int sleep_counter = 0; // Batch events up to 5 seconds while (sleep_counter < 5) { int ret = poll(&fds, 1, 0); if (ret == -1) throw new MonitorException("poll failed"); else if (ret == 0) break; // no events available hasNotification = true; size_t length = read(worker.fd, buffer.ptr, buffer.length); if (length == -1) throw new MonitorException("read failed"); int i = 0; while (i < length) { inotify_event *event = cast(inotify_event*) &buffer[i]; string path; string evalPath; // inotify event debug if (debugLogging) { addLogEntry("inotify event wd: " ~ to!string(event.wd), ["debug"]); addLogEntry("inotify event mask: " ~ to!string(event.mask), ["debug"]); addLogEntry("inotify event cookie: " ~ to!string(event.cookie), ["debug"]); addLogEntry("inotify event len: " ~ to!string(event.len), ["debug"]); addLogEntry("inotify event name: " ~ to!string(event.name), ["debug"]); } // inotify event handling if (debugLogging) { if (event.mask & IN_ACCESS) addLogEntry("inotify event flag: IN_ACCESS", ["debug"]); if (event.mask & IN_MODIFY) addLogEntry("inotify event flag: IN_MODIFY", ["debug"]); if (event.mask & IN_ATTRIB) addLogEntry("inotify event flag: IN_ATTRIB", ["debug"]); if (event.mask & IN_CLOSE_WRITE) addLogEntry("inotify event flag: IN_CLOSE_WRITE", ["debug"]); if (event.mask & IN_CLOSE_NOWRITE) addLogEntry("inotify event flag: IN_CLOSE_NOWRITE", ["debug"]); if (event.mask & IN_MOVED_FROM) addLogEntry("inotify event flag: IN_MOVED_FROM", ["debug"]); if (event.mask & IN_MOVED_TO) addLogEntry("inotify event flag: IN_MOVED_TO", ["debug"]); if (event.mask & IN_CREATE) addLogEntry("inotify event flag: IN_CREATE", ["debug"]); if (event.mask & IN_DELETE) addLogEntry("inotify event flag: IN_DELETE", ["debug"]); if (event.mask & IN_DELETE_SELF) addLogEntry("inotify event flag: IN_DELETE_SELF", ["debug"]); if (event.mask & IN_MOVE_SELF) addLogEntry("inotify event flag: IN_MOVE_SELF", ["debug"]); if (event.mask & IN_UNMOUNT) addLogEntry("inotify event flag: IN_UNMOUNT", ["debug"]); if (event.mask & IN_Q_OVERFLOW) addLogEntry("inotify event flag: IN_Q_OVERFLOW", ["debug"]); if (event.mask & IN_IGNORED) addLogEntry("inotify event flag: IN_IGNORED", ["debug"]); if (event.mask & IN_CLOSE) addLogEntry("inotify event flag: IN_CLOSE", ["debug"]); if (event.mask & IN_MOVE) addLogEntry("inotify event flag: IN_MOVE", ["debug"]); if (event.mask & IN_ONLYDIR) addLogEntry("inotify event flag: IN_ONLYDIR", ["debug"]); if (event.mask & IN_DONT_FOLLOW) addLogEntry("inotify event flag: IN_DONT_FOLLOW", ["debug"]); if (event.mask & IN_EXCL_UNLINK) addLogEntry("inotify event flag: IN_EXCL_UNLINK", ["debug"]); if (event.mask & IN_MASK_ADD) addLogEntry("inotify event flag: IN_MASK_ADD", ["debug"]); if (event.mask & IN_ISDIR) addLogEntry("inotify event flag: IN_ISDIR", ["debug"]); if (event.mask & IN_ONESHOT) addLogEntry("inotify event flag: IN_ONESHOT", ["debug"]); if (event.mask & IN_ALL_EVENTS) addLogEntry("inotify event flag: IN_ALL_EVENTS", ["debug"]); } // skip events that need to be ignored if (event.mask & IN_IGNORED) { // forget the directory associated to the watch descriptor wdToDirName.remove(event.wd); goto skip; } else if (event.mask & IN_Q_OVERFLOW) { throw new MonitorException("inotify queue overflow: some events may be lost"); } // if the event is not to be ignored, obtain path path = getPath(event); // configure the skip_dir & skip skip_file comparison item evalPath = path.strip('.'); // Skip events that should be excluded based on application configuration // We cant use isDir or isFile as this information is missing from the inotify event itself // Thus this causes a segfault when attempting to query this - https://github.com/abraunegg/onedrive/issues/995 // Based on the 'type' of event & object type (directory or file) check that path against the 'right' user exclusions // Directory events should only be compared against skip_dir and file events should only be compared against skip_file if (event.mask & IN_ISDIR) { // The event in question contains IN_ISDIR event mask, thus highly likely this is an event on a directory // This due to if the user has specified in skip_dir an exclusive path: '/path' - that is what must be matched if (selectiveSync.isDirNameExcluded(evalPath)) { // The path to evaluate matches a path that the user has configured to skip goto skip; } } else { // The event in question missing the IN_ISDIR event mask, thus highly likely this is an event on a file // This due to if the user has specified in skip_file an exclusive path: '/path/file' - that is what must be matched if (selectiveSync.isFileNameExcluded(evalPath)) { // The path to evaluate matches a file that the user has configured to skip goto skip; } } // is the path, excluded via sync_list if (selectiveSync.isPathExcludedViaSyncList(path)) { // The path to evaluate matches a directory or file that the user has configured not to include in the sync goto skip; } // handle the inotify events if (event.mask & IN_MOVED_FROM) { if (debugLogging) {addLogEntry("event IN_MOVED_FROM: " ~ path, ["debug"]);} cookieToPath[event.cookie] = path; movedNotDeleted[path] = true; // Mark as moved, not deleted } else if (event.mask & IN_MOVED_TO) { if (debugLogging) {addLogEntry("event IN_MOVED_TO: " ~ path, ["debug"]);} if (event.mask & IN_ISDIR) addRecursive(path); auto from = event.cookie in cookieToPath; if (from) { cookieToPath.remove(event.cookie); if (useCallbacks) actionHolder.append(ActionType.moved, *from, path); movedNotDeleted.remove(*from); // Clear moved status } else { // Handle file moved in from outside if (event.mask & IN_ISDIR) { if (useCallbacks) actionHolder.append(ActionType.createDir, path); } else { if (useCallbacks) actionHolder.append(ActionType.changed, path); } } } else if (event.mask & IN_CREATE) { if (debugLogging) {addLogEntry("event IN_CREATE: " ~ path, ["debug"]);} if (event.mask & IN_ISDIR) { // fix from #2586 auto cookieToPath1 = cookieToPath.dup(); foreach (cookie, path1; cookieToPath1) { if (path1 == path) { cookieToPath.remove(cookie); } } addRecursive(path); if (useCallbacks) actionHolder.append(ActionType.createDir, path); } } else if (event.mask & IN_DELETE) { if (path in movedNotDeleted) { movedNotDeleted.remove(path); // Ignore delete for moved files } else { if (debugLogging) {addLogEntry("event IN_DELETE: " ~ path, ["debug"]);} if (useCallbacks) actionHolder.append(ActionType.deleted, path); } } else if ((event.mask & IN_CLOSE_WRITE) && !(event.mask & IN_ISDIR)) { if (debugLogging) {addLogEntry("event IN_CLOSE_WRITE and not IN_ISDIR: " ~ path, ["debug"]);} // fix from #2586 auto cookieToPath1 = cookieToPath.dup(); foreach (cookie, path1; cookieToPath1) { if (path1 == path) { cookieToPath.remove(cookie); } } if (useCallbacks) actionHolder.append(ActionType.changed, path); } else { addLogEntry("inotify event unhandled: " ~ path); assert(0); } skip: i += inotify_event.sizeof + event.len; } // Sleep for one second to prevent missing fast-changing events. if (poll(&fds, 1, 0) == 0) { sleep_counter += 1; Thread.sleep(dur!"seconds"(1)); } } if (!hasNotification) break; processChanges(); // Assume that the items moved outside the watched directory have been deleted foreach (cookie, path; cookieToPath) { if (debugLogging) {addLogEntry("Deleting cookie|watch (post loop): " ~ path, ["debug"]);} if (useCallbacks) onDelete(path); remove(path); cookieToPath.remove(cookie); } // Debug Log that all inotify events are flushed if (debugLogging) {addLogEntry("inotify events flushed", ["debug"]);} } } private void processChanges() { string[] changes; foreach(action; actionHolder.actions) { if (action.skipped) continue; switch (action.type) { case ActionType.changed: changes ~= action.src; break; case ActionType.deleted: onDelete(action.src); break; case ActionType.createDir: onDirCreated(action.src); break; case ActionType.moved: onMove(action.src, action.dst); break; default: break; } } if (!changes.empty) { onFileChanged(changes); } object.destroy(actionHolder); } } onedrive-2.5.5/src/notifications/000077500000000000000000000000001476564400300170075ustar00rootroot00000000000000onedrive-2.5.5/src/notifications/README000066400000000000000000000006441476564400300176730ustar00rootroot00000000000000The files in this directory have been obtained form the following places: dnotify.d https://github.com/Dav1dde/dnotify/blob/master/dnotify.d License: Creative Commons Zro 1.0 Universal see https://github.com/Dav1dde/dnotify/blob/master/LICENSE notify.d https://github.com/D-Programming-Deimos/libnotify/blob/master/deimos/notify/notify.d License: GNU Lesser General Public License (LGPL) 2.1 or upwards, see file onedrive-2.5.5/src/notifications/dnotify.d000066400000000000000000000213331476564400300206320ustar00rootroot00000000000000module dnotify; import std.path; import std.file; private { import std.string : toStringz; import std.conv : to; import std.traits : isPointer, isArray; import std.variant : Variant; import std.array : appender; import deimos.notify.notify; } public import deimos.notify.notify : NOTIFY_EXPIRES_DEFAULT, NOTIFY_EXPIRES_NEVER, NotifyUrgency; version(NoPragma) { } else { pragma(lib, "notify"); pragma(lib, "gmodule"); pragma(lib, "glib-2.0"); } extern (C) { private void g_free(void* mem); private void g_list_free(GList* glist); } version(NoGdk) { } else { version(NoPragma) { } else { pragma(lib, "gdk_pixbuf"); } private: extern (C) { GdkPixbuf* gdk_pixbuf_new_from_file(const(char)* filename, GError **error); } } class NotificationError : Exception { string message; GError* gerror; this(GError* gerror) { this.message = to!(string)(gerror.message); this.gerror = gerror; super(this.message); } this(string message) { this.message = message; super(message); } } bool check_availability() { // notify_init might return without dbus server actually started // try to check for running dbus server char **ret_name; char **ret_vendor; char **ret_version; char **ret_spec_version; bool ret; try { return notify_get_server_info(ret_name, ret_vendor, ret_version, ret_spec_version); } catch (NotificationError e) { throw new NotificationError("Cannot find dbus server!"); } } void init(in char[] name) { notify_init(name.toStringz()); } alias notify_is_initted is_initted; alias notify_uninit uninit; shared static this() { init(baseName(thisExePath())); } shared static ~this() { uninit(); } string get_app_name() { return to!(string)(notify_get_app_name()); } void set_app_name(in char[] app_name) { notify_set_app_name(app_name.toStringz()); } string[] get_server_caps() { auto result = appender!(string[])(); GList* list = notify_get_server_caps(); if(list !is null) { for(GList* c = list; c !is null; c = c.next) { result.put(to!(string)(cast(char*)c.data)); g_free(c.data); } g_list_free(list); } return result.data; } struct ServerInfo { string name; string vendor; string version_; string spec_version; } ServerInfo get_server_info() { char* name; char* vendor; char* version_; char* spec_version; notify_get_server_info(&name, &vendor, &version_, &spec_version); scope(exit) { g_free(name); g_free(vendor); g_free(version_); g_free(spec_version); } return ServerInfo(to!string(name), to!string(vendor), to!string(version_), to!string(spec_version)); } struct Action { const(char[]) id; const(char[]) label; NotifyActionCallback callback; void* user_ptr; } class Notification { NotifyNotification* notify_notification; const(char)[] summary; const(char)[] body_; const(char)[] icon; bool closed = true; private int _timeout = NOTIFY_EXPIRES_DEFAULT; const(char)[] _category; NotifyUrgency _urgency; GdkPixbuf* _image; Variant[const(char)[]] _hints; const(char)[] _app_name; Action[] _actions; this(in char[] summary, in char[] body_, in char[] icon="") in { assert(is_initted(), "call dnotify.init() before using Notification"); } do { this.summary = summary; this.body_ = body_; this.icon = icon; notify_notification = notify_notification_new(summary.toStringz(), body_.toStringz(), icon.toStringz()); } bool update(in char[] summary, in char[] body_, in char[] icon="") { this.summary = summary; this.body_ = body_; this.icon = icon; return notify_notification_update(notify_notification, summary.toStringz(), body_.toStringz(), icon.toStringz()); } void show() { GError* ge; if(!notify_notification_show(notify_notification, &ge)) { throw new NotificationError(ge); } } @property int timeout() { return _timeout; } @property void timeout(int timeout) { this._timeout = timeout; notify_notification_set_timeout(notify_notification, timeout); } @property const(char[]) category() { return _category; } @property void category(in char[] category) { this._category = category; notify_notification_set_category(notify_notification, category.toStringz()); } @property NotifyUrgency urgency() { return _urgency; } @property void urgency(NotifyUrgency urgency) { this._urgency = urgency; notify_notification_set_urgency(notify_notification, urgency); } void set_image(GdkPixbuf* pixbuf) { notify_notification_set_image_from_pixbuf(notify_notification, pixbuf); //_image = pixbuf; } version(NoGdk) { } else { void set_image(in char[] filename) { GError* ge; // TODO: free pixbuf GdkPixbuf* pixbuf = gdk_pixbuf_new_from_file(filename.toStringz(), &ge); if(pixbuf is null) { if(ge is null) { throw new NotificationError("Unable to load file: " ~ filename.idup); } else { throw new NotificationError(ge); } } assert(notify_notification !is null); notify_notification_set_image_from_pixbuf(notify_notification, pixbuf); // TODO: fix segfault //_image = pixbuf; } } @property GdkPixbuf* image() { return _image; } // using deprecated set_hint_* functions (GVariant is an opaque structure, which needs the glib) void set_hint(T)(in char[] key, T value) { static if(is(T == int)) { notify_notification_set_hint_int32(notify_notification, key, value); } else static if(is(T == uint)) { notify_notification_set_hint_uint32(notify_notification, key, value); } else static if(is(T == double)) { notify_notification_set_hint_double(notify_notification, key, value); } else static if(is(T : const(char)[])) { notify_notification_set_hint_string(notify_notification, key, value.toStringz()); } else static if(is(T == ubyte)) { notify_notification_set_hint_byte(notify_notification, key, value); } else static if(is(T == ubyte[])) { notify_notification_set_hint_byte_array(notify_notification, key, value.ptr, value.length); } else { static assert(false, "unsupported value for Notification.set_hint"); } _hints[key] = Variant(value); } // unset hint? Variant get_hint(in char[] key) { return _hints[key]; } @property const(char)[] app_name() { return _app_name; } @property void app_name(in char[] name) { this._app_name = app_name; notify_notification_set_app_name(notify_notification, app_name.toStringz()); } void add_action(T)(in char[] action, in char[] label, NotifyActionCallback callback, T user_data) { static if(isPointer!T) { void* user_ptr = cast(void*)user_data; } else static if(isArray!T) { void* user_ptr = cast(void*)user_data.ptr; } else { void* user_ptr = cast(void*)&user_data; } notify_notification_add_action(notify_notification, action.toStringz(), label.toStringz(), callback, user_ptr, null); _actions ~= Action(action, label, callback, user_ptr); } void add_action()(Action action) { notify_notification_add_action(notify_notification, action.id.toStringz(), action.label.toStringz(), action.callback, action.user_ptr, null); _actions ~= action; } @property Action[] actions() { return _actions; } void clear_actions() { notify_notification_clear_actions(notify_notification); } void close() { GError* ge; if(!notify_notification_close(notify_notification, &ge)) { throw new NotificationError(ge); } } @property int closed_reason() { return notify_notification_get_closed_reason(notify_notification); } } version(TestMain) { import std.stdio; void main() { writeln(get_app_name()); set_app_name("blargh"); writeln(get_app_name()); writeln(get_server_caps()); writeln(get_server_info()); auto n = new Notification("foo", "bar", "notification-message-im"); n.timeout = 3; n.show(); } } onedrive-2.5.5/src/notifications/notify.d000066400000000000000000000151001476564400300204610ustar00rootroot00000000000000/** * Copyright (C) 2004-2006 Christian Hammond * Copyright (C) 2010 Red Hat, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the * Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. */ module deimos.notify.notify; enum NOTIFY_VERSION_MAJOR = 0; enum NOTIFY_VERSION_MINOR = 7; enum NOTIFY_VERSION_MICRO = 5; template NOTIFY_CHECK_VERSION(int major, int minor, int micro) { enum NOTIFY_CHECK_VERSION = ((NOTIFY_VERSION_MAJOR > major) || (NOTIFY_VERSION_MAJOR == major && NOTIFY_VERSION_MINOR > minor) || (NOTIFY_VERSION_MAJOR == major && NOTIFY_VERSION_MINOR == minor && NOTIFY_VERSION_MICRO >= micro)); } alias ulong GType; alias void function(void*) GFreeFunc; struct GError { uint domain; int code; char* message; } struct GList { void* data; GList* next; GList* prev; } // dummies struct GdkPixbuf {} struct GObject {} struct GObjectClass {} struct GVariant {} GType notify_urgency_get_type(); /** * NOTIFY_EXPIRES_DEFAULT: * * The default expiration time on a notification. */ enum NOTIFY_EXPIRES_DEFAULT = -1; /** * NOTIFY_EXPIRES_NEVER: * * The notification never expires. It stays open until closed by the calling API * or the user. */ enum NOTIFY_EXPIRES_NEVER = 0; // #define NOTIFY_TYPE_NOTIFICATION (notify_notification_get_type ()) // #define NOTIFY_NOTIFICATION(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), NOTIFY_TYPE_NOTIFICATION, NotifyNotification)) // #define NOTIFY_NOTIFICATION_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), NOTIFY_TYPE_NOTIFICATION, NotifyNotificationClass)) // #define NOTIFY_IS_NOTIFICATION(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), NOTIFY_TYPE_NOTIFICATION)) // #define NOTIFY_IS_NOTIFICATION_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), NOTIFY_TYPE_NOTIFICATION)) // #define NOTIFY_NOTIFICATION_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), NOTIFY_TYPE_NOTIFICATION, NotifyNotificationClass)) extern (C) { struct NotifyNotificationPrivate; struct NotifyNotification { /*< private >*/ GObject parent_object; NotifyNotificationPrivate *priv; } struct NotifyNotificationClass { GObjectClass parent_class; /* Signals */ void function(NotifyNotification *notification) closed; } /** * NotifyUrgency: * @NOTIFY_URGENCY_LOW: Low urgency. Used for unimportant notifications. * @NOTIFY_URGENCY_NORMAL: Normal urgency. Used for most standard notifications. * @NOTIFY_URGENCY_CRITICAL: Critical urgency. Used for very important notifications. * * The urgency level of the notification. */ enum NotifyUrgency { NOTIFY_URGENCY_LOW, NOTIFY_URGENCY_NORMAL, NOTIFY_URGENCY_CRITICAL, } /** * NotifyActionCallback: * @notification: * @action: * @user_data: * * An action callback function. */ alias void function(NotifyNotification* notification, char* action, void* user_data) NotifyActionCallback; GType notify_notification_get_type(); NotifyNotification* notify_notification_new(const(char)* summary, const(char)* body_, const(char)* icon); bool notify_notification_update(NotifyNotification* notification, const(char)* summary, const(char)* body_, const(char)* icon); bool notify_notification_show(NotifyNotification* notification, GError** error); void notify_notification_set_timeout(NotifyNotification* notification, int timeout); void notify_notification_set_category(NotifyNotification* notification, const(char)* category); void notify_notification_set_urgency(NotifyNotification* notification, NotifyUrgency urgency); void notify_notification_set_image_from_pixbuf(NotifyNotification* notification, GdkPixbuf* pixbuf); void notify_notification_set_icon_from_pixbuf(NotifyNotification* notification, GdkPixbuf* icon); void notify_notification_set_hint_int32(NotifyNotification* notification, const(char)* key, int value); void notify_notification_set_hint_uint32(NotifyNotification* notification, const(char)* key, uint value); void notify_notification_set_hint_double(NotifyNotification* notification, const(char)* key, double value); void notify_notification_set_hint_string(NotifyNotification* notification, const(char)* key, const(char)* value); void notify_notification_set_hint_byte(NotifyNotification* notification, const(char)* key, ubyte value); void notify_notification_set_hint_byte_array(NotifyNotification* notification, const(char)* key, const(ubyte)* value, ulong len); void notify_notification_set_hint(NotifyNotification* notification, const(char)* key, GVariant* value); void notify_notification_set_app_name(NotifyNotification* notification, const(char)* app_name); void notify_notification_clear_hints(NotifyNotification* notification); void notify_notification_add_action(NotifyNotification* notification, const(char)* action, const(char)* label, NotifyActionCallback callback, void* user_data, GFreeFunc free_func); void notify_notification_clear_actions(NotifyNotification* notification); bool notify_notification_close(NotifyNotification* notification, GError** error); int notify_notification_get_closed_reason(const NotifyNotification* notification); bool notify_init(const(char)* app_name); void notify_uninit(); bool notify_is_initted(); const(char)* notify_get_app_name(); void notify_set_app_name(const(char)* app_name); GList *notify_get_server_caps(); bool notify_get_server_info(char** ret_name, char** ret_vendor, char** ret_version, char** ret_spec_version); } version(MainTest) { import std.string; void main() { notify_init("test".toStringz()); auto n = notify_notification_new("summary".toStringz(), "body".toStringz(), "none".toStringz()); GError* ge; notify_notification_show(n, &ge); scope(success) notify_uninit(); } } onedrive-2.5.5/src/onedrive.d000066400000000000000000002161541476564400300161270ustar00rootroot00000000000000// What is this module called? module onedrive; // What does this module require to function? import core.stdc.stdlib: EXIT_SUCCESS, EXIT_FAILURE, exit; import core.memory; import core.thread; import std.stdio; import std.string; import std.utf; import std.file; import std.exception; import std.regex; import std.json; import std.algorithm; import std.net.curl; import std.datetime; import std.path; import std.conv; import std.math; import std.uri; import std.array; // Required for webhooks import std.uuid; // What other modules that we have created do we need to import? import config; import log; import util; import curlEngine; // Define the 'OneDriveException' class class OneDriveException: Exception { // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/concepts/errors int httpStatusCode; const CurlResponse response; JSONValue error; this(int httpStatusCode, string reason, const CurlResponse response, string file = __FILE__, size_t line = __LINE__) { this.httpStatusCode = httpStatusCode; this.response = response; this.error = response.json(); string msg = format("HTTP request returned status code %d (%s)\n%s", httpStatusCode, reason, toJSON(error, true)); super(msg, file, line); } this(int httpStatusCode, string reason, string file = __FILE__, size_t line = __LINE__) { this.httpStatusCode = httpStatusCode; this.response = null; super(msg, file, line, null); } } // Define the 'OneDriveError' class class OneDriveError: Error { this(string msg) { super(msg); } } // Define the 'OneDriveApi' class class OneDriveApi { // Class variables that use other classes ApplicationConfig appConfig; CurlEngine curlEngine; CurlResponse response; // Class variables string clientId = ""; string companyName = ""; string authUrl = ""; string redirectUrl = ""; string tokenUrl = ""; string driveUrl = ""; string driveByIdUrl = ""; string sharedWithMeUrl = ""; string itemByIdUrl = ""; string itemByPathUrl = ""; string siteSearchUrl = ""; string siteDriveUrl = ""; string subscriptionUrl = ""; string tenantId = ""; string authScope = ""; const(char)[] refreshToken = ""; bool dryRun = false; bool keepAlive = false; this(ApplicationConfig appConfig) { // Configure the class variable to consume the application configuration this.appConfig = appConfig; this.curlEngine = null; this.response = null; // Configure the major API Query URL's, based on using application configuration // These however can be updated by config option 'azure_ad_endpoint', thus handled differently // Drive Queries driveUrl = appConfig.globalGraphEndpoint ~ "/v1.0/me/drive"; driveByIdUrl = appConfig.globalGraphEndpoint ~ "/v1.0/drives/"; // What is 'shared with me' Query sharedWithMeUrl = appConfig.globalGraphEndpoint ~ "/v1.0/me/drive/sharedWithMe"; // Item Queries itemByIdUrl = appConfig.globalGraphEndpoint ~ "/v1.0/me/drive/items/"; itemByPathUrl = appConfig.globalGraphEndpoint ~ "/v1.0/me/drive/root:/"; // Office 365 / SharePoint Queries siteSearchUrl = appConfig.globalGraphEndpoint ~ "/v1.0/sites?search"; siteDriveUrl = appConfig.globalGraphEndpoint ~ "/v1.0/sites/"; // Subscriptions subscriptionUrl = appConfig.globalGraphEndpoint ~ "/v1.0/subscriptions"; } // The destructor should only clean up resources owned directly by this instance ~this() { if (response !is null) { object.destroy(response); // calls class CurlResponse destructor response = null; } if (curlEngine !is null) { object.destroy(curlEngine); // calls class CurlEngine destructor curlEngine = null; } if (appConfig !is null) { appConfig = null; } } // Initialise the OneDrive API class bool initialise(bool keepAlive=true) { // Initialise the curl engine this.keepAlive = keepAlive; if (curlEngine is null) { curlEngine = getCurlInstance(); curlEngine.initialise(appConfig.getValueLong("dns_timeout"), appConfig.getValueLong("connect_timeout"), appConfig.getValueLong("data_timeout"), appConfig.getValueLong("operation_timeout"), appConfig.defaultMaxRedirects, appConfig.getValueBool("debug_https"), appConfig.getValueString("user_agent"), appConfig.getValueBool("force_http_11"), appConfig.getValueLong("rate_limit"), appConfig.getValueLong("ip_protocol_version"), appConfig.getValueLong("max_curl_idle"), keepAlive); } // Authorised value to return bool authorised = false; // Did the user specify --dry-run dryRun = appConfig.getValueBool("dry_run"); // Set clientId to use the configured 'application_id' clientId = appConfig.getValueString("application_id"); if (clientId != appConfig.defaultApplicationId) { // a custom 'application_id' was set companyName = "custom_application"; } // Do we have a custom Azure Tenant ID? if (!appConfig.getValueString("azure_tenant_id").empty) { // Use the value entered by the user tenantId = appConfig.getValueString("azure_tenant_id"); } else { // set to common tenantId = "common"; } // Did the user specify a 'drive_id' ? if (!appConfig.getValueString("drive_id").empty) { // Update base URL's driveUrl = driveByIdUrl ~ appConfig.getValueString("drive_id"); itemByIdUrl = driveUrl ~ "/items"; itemByPathUrl = driveUrl ~ "/root:/"; } // Configure the authentication scope if (appConfig.getValueBool("read_only_auth_scope")) { // read-only authentication scopes has been requested authScope = "&scope=Files.Read%20Files.Read.All%20Sites.Read.All%20offline_access&response_type=code&prompt=login&redirect_uri="; } else { // read-write authentication scopes will be used (default) authScope = "&scope=Files.ReadWrite%20Files.ReadWrite.All%20Sites.ReadWrite.All%20offline_access&response_type=code&prompt=login&redirect_uri="; } // Configure Azure AD endpoints if 'azure_ad_endpoint' is configured string azureConfigValue = appConfig.getValueString("azure_ad_endpoint"); switch(azureConfigValue) { case "": if (tenantId == "common") { if (!appConfig.apiWasInitialised) addLogEntry("Configuring Global Azure AD Endpoints"); } else { if (!appConfig.apiWasInitialised) addLogEntry("Configuring Global Azure AD Endpoints - Single Tenant Application"); } // Authentication authUrl = appConfig.globalAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/authorize"; redirectUrl = appConfig.globalAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/nativeclient"; tokenUrl = appConfig.globalAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/token"; break; case "USL4": if (!appConfig.apiWasInitialised) addLogEntry("Configuring Azure AD for US Government Endpoints"); // Authentication authUrl = appConfig.usl4AuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/authorize"; tokenUrl = appConfig.usl4AuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/token"; if (clientId == appConfig.defaultApplicationId) { // application_id == default if (debugLogging) {addLogEntry("USL4 AD Endpoint but default application_id, redirectUrl needs to be aligned to globalAuthEndpoint", ["debug"]);} redirectUrl = appConfig.globalAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/nativeclient"; } else { // custom application_id redirectUrl = appConfig.usl4AuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/nativeclient"; } // Drive Queries driveUrl = appConfig.usl4GraphEndpoint ~ "/v1.0/me/drive"; driveByIdUrl = appConfig.usl4GraphEndpoint ~ "/v1.0/drives/"; // Item Queries itemByIdUrl = appConfig.usl4GraphEndpoint ~ "/v1.0/me/drive/items/"; itemByPathUrl = appConfig.usl4GraphEndpoint ~ "/v1.0/me/drive/root:/"; // Office 365 / SharePoint Queries siteSearchUrl = appConfig.usl4GraphEndpoint ~ "/v1.0/sites?search"; siteDriveUrl = appConfig.usl4GraphEndpoint ~ "/v1.0/sites/"; // Shared With Me sharedWithMeUrl = appConfig.usl4GraphEndpoint ~ "/v1.0/me/drive/sharedWithMe"; // Subscriptions subscriptionUrl = appConfig.usl4GraphEndpoint ~ "/v1.0/subscriptions"; break; case "USL5": if (!appConfig.apiWasInitialised) addLogEntry("Configuring Azure AD for US Government Endpoints (DOD)"); // Authentication authUrl = appConfig.usl5AuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/authorize"; tokenUrl = appConfig.usl5AuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/token"; if (clientId == appConfig.defaultApplicationId) { // application_id == default if (debugLogging) {addLogEntry("USL5 AD Endpoint but default application_id, redirectUrl needs to be aligned to globalAuthEndpoint", ["debug"]);} redirectUrl = appConfig.globalAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/nativeclient"; } else { // custom application_id redirectUrl = appConfig.usl5AuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/nativeclient"; } // Drive Queries driveUrl = appConfig.usl5GraphEndpoint ~ "/v1.0/me/drive"; driveByIdUrl = appConfig.usl5GraphEndpoint ~ "/v1.0/drives/"; // Item Queries itemByIdUrl = appConfig.usl5GraphEndpoint ~ "/v1.0/me/drive/items/"; itemByPathUrl = appConfig.usl5GraphEndpoint ~ "/v1.0/me/drive/root:/"; // Office 365 / SharePoint Queries siteSearchUrl = appConfig.usl5GraphEndpoint ~ "/v1.0/sites?search"; siteDriveUrl = appConfig.usl5GraphEndpoint ~ "/v1.0/sites/"; // Shared With Me sharedWithMeUrl = appConfig.usl5GraphEndpoint ~ "/v1.0/me/drive/sharedWithMe"; // Subscriptions subscriptionUrl = appConfig.usl5GraphEndpoint ~ "/v1.0/subscriptions"; break; case "DE": if (!appConfig.apiWasInitialised) addLogEntry("Configuring Azure AD Germany"); // Authentication authUrl = appConfig.deAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/authorize"; tokenUrl = appConfig.deAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/token"; if (clientId == appConfig.defaultApplicationId) { // application_id == default if (debugLogging) {addLogEntry("DE AD Endpoint but default application_id, redirectUrl needs to be aligned to globalAuthEndpoint", ["debug"]);} redirectUrl = appConfig.globalAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/nativeclient"; } else { // custom application_id redirectUrl = appConfig.deAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/nativeclient"; } // Drive Queries driveUrl = appConfig.deGraphEndpoint ~ "/v1.0/me/drive"; driveByIdUrl = appConfig.deGraphEndpoint ~ "/v1.0/drives/"; // Item Queries itemByIdUrl = appConfig.deGraphEndpoint ~ "/v1.0/me/drive/items/"; itemByPathUrl = appConfig.deGraphEndpoint ~ "/v1.0/me/drive/root:/"; // Office 365 / SharePoint Queries siteSearchUrl = appConfig.deGraphEndpoint ~ "/v1.0/sites?search"; siteDriveUrl = appConfig.deGraphEndpoint ~ "/v1.0/sites/"; // Shared With Me sharedWithMeUrl = appConfig.deGraphEndpoint ~ "/v1.0/me/drive/sharedWithMe"; // Subscriptions subscriptionUrl = appConfig.deGraphEndpoint ~ "/v1.0/subscriptions"; break; case "CN": if (!appConfig.apiWasInitialised) addLogEntry("Configuring AD China operated by VNET"); // Authentication authUrl = appConfig.cnAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/authorize"; tokenUrl = appConfig.cnAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/token"; if (clientId == appConfig.defaultApplicationId) { // application_id == default if (debugLogging) {addLogEntry("CN AD Endpoint but default application_id, redirectUrl needs to be aligned to globalAuthEndpoint", ["debug"]);} redirectUrl = appConfig.globalAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/nativeclient"; } else { // custom application_id redirectUrl = appConfig.cnAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/nativeclient"; } // Drive Queries driveUrl = appConfig.cnGraphEndpoint ~ "/v1.0/me/drive"; driveByIdUrl = appConfig.cnGraphEndpoint ~ "/v1.0/drives/"; // Item Queries itemByIdUrl = appConfig.cnGraphEndpoint ~ "/v1.0/me/drive/items/"; itemByPathUrl = appConfig.cnGraphEndpoint ~ "/v1.0/me/drive/root:/"; // Office 365 / SharePoint Queries siteSearchUrl = appConfig.cnGraphEndpoint ~ "/v1.0/sites?search"; siteDriveUrl = appConfig.cnGraphEndpoint ~ "/v1.0/sites/"; // Shared With Me sharedWithMeUrl = appConfig.cnGraphEndpoint ~ "/v1.0/me/drive/sharedWithMe"; // Subscriptions subscriptionUrl = appConfig.cnGraphEndpoint ~ "/v1.0/subscriptions"; break; // Default - all other entries default: if (!appConfig.apiWasInitialised) addLogEntry("Unknown Azure AD Endpoint request - using Global Azure AD Endpoints"); } // Has the application been authenticated? if (!exists(appConfig.refreshTokenFilePath)) { if (debugLogging) {addLogEntry("Application has no 'refresh_token' thus needs to be authenticated", ["debug"]);} authorised = authorise(); } else { // Try and read the value from the appConfig if it is set, rather than trying to read the value from disk if (!appConfig.refreshToken.empty) { if (debugLogging) {addLogEntry("Read token from appConfig", ["debug"]);} refreshToken = strip(appConfig.refreshToken); authorised = true; } else { // Try and read the file from disk try { refreshToken = strip(readText(appConfig.refreshTokenFilePath)); // is the refresh_token empty? if (refreshToken.empty) { addLogEntry("RefreshToken exists but is empty: " ~ appConfig.refreshTokenFilePath); authorised = authorise(); } else { // Existing token not empty authorised = true; // update appConfig.refreshToken appConfig.refreshToken = refreshToken; } } catch (FileException exception) { authorised = authorise(); } catch (std.utf.UTFException exception) { // path contains characters which generate a UTF exception addLogEntry("Cannot read refreshToken from: " ~ appConfig.refreshTokenFilePath); addLogEntry(" Error Reason:" ~ exception.msg); authorised = false; } } if (refreshToken.empty) { // PROBLEM ... CODING TO DO ?????????? if (debugLogging) {addLogEntry("DEBUG: refreshToken is empty !!!!!!!!!!", ["debug"]);} } } // Return if we are authorised if (debugLogging) {addLogEntry("Authorised State: " ~ to!string(authorised), ["debug"]);} return authorised; } // If the API has been configured correctly, print the items that been configured void debugOutputConfiguredAPIItems() { // Debug output of configured URL's // Application Identification if (debugLogging) { addLogEntry("Configured clientId " ~ clientId, ["debug"]); addLogEntry("Configured userAgent " ~ appConfig.getValueString("user_agent"), ["debug"]); // Authentication addLogEntry("Configured authScope: " ~ authScope, ["debug"]); addLogEntry("Configured authUrl: " ~ authUrl, ["debug"]); addLogEntry("Configured redirectUrl: " ~ redirectUrl, ["debug"]); addLogEntry("Configured tokenUrl: " ~ tokenUrl, ["debug"]); // Drive Queries addLogEntry("Configured driveUrl: " ~ driveUrl, ["debug"]); addLogEntry("Configured driveByIdUrl: " ~ driveByIdUrl, ["debug"]); // Shared With Me addLogEntry("Configured sharedWithMeUrl: " ~ sharedWithMeUrl, ["debug"]); // Item Queries addLogEntry("Configured itemByIdUrl: " ~ itemByIdUrl, ["debug"]); addLogEntry("Configured itemByPathUrl: " ~ itemByPathUrl, ["debug"]); // SharePoint Queries addLogEntry("Configured siteSearchUrl: " ~ siteSearchUrl, ["debug"]); addLogEntry("Configured siteDriveUrl: " ~ siteDriveUrl, ["debug"]); } } // Release CurlEngine bask to the Curl Engine Pool void releaseCurlEngine() { // Log that this was called if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("OneDrive API releaseCurlEngine() Called", ["debug"]);} // Release curl instance back to the pool if (curlEngine !is null) { curlEngine.releaseEngine(); curlEngine = null; } // Release the response response = null; // Perform Garbage Collection GC.collect(); } // Authenticate this client against Microsoft OneDrive API bool authorise() { char[] response; // What URL should be presented to the user to access string url = authUrl ~ "?client_id=" ~ clientId ~ authScope ~ redirectUrl; // Configure automated authentication if --auth-files authUrl:responseUrl is being used string authFilesString = appConfig.getValueString("auth_files"); string authResponseString = appConfig.getValueString("auth_response"); if (!authResponseString.empty) { // read the response from authResponseString response = cast(char[]) authResponseString; } else if (authFilesString != "") { string[] authFiles = authFilesString.split(":"); string authUrl = authFiles[0]; string responseUrl = authFiles[1]; try { auto authUrlFile = File(authUrl, "w"); authUrlFile.write(url); authUrlFile.close(); } catch (FileException exception) { // There was a file system error // display the error message displayFileSystemErrorMessage(exception.msg, getFunctionName!({})); // Must force exit here, allow logging to be done forceExit(); } catch (ErrnoException exception) { // There was a file system error // display the error message displayFileSystemErrorMessage(exception.msg, getFunctionName!({})); // Must force exit here, allow logging to be done forceExit(); } addLogEntry("Client requires authentication before proceeding. Waiting for --auth-files elements to be available."); while (!exists(responseUrl)) { Thread.sleep(dur!("msecs")(100)); } // read response from provided from OneDrive try { response = cast(char[]) read(responseUrl); } catch (OneDriveException exception) { // exception generated displayOneDriveErrorMessage(exception.msg, getFunctionName!({})); return false; } // try to remove old files try { std.file.remove(authUrl); std.file.remove(responseUrl); } catch (FileException exception) { addLogEntry("Cannot remove files " ~ authUrl ~ " " ~ responseUrl); return false; } } else { // Are we in a --dry-run scenario? if (!appConfig.getValueBool("dry_run")) { // No --dry-run is being used addLogEntry("Authorise this application by visiting:\n", ["consoleOnly"]); addLogEntry(url ~ "\n", ["consoleOnly"]); addLogEntry("Enter the response uri from your browser: ", ["consoleOnlyNoNewLine"]); readln(response); appConfig.applicationAuthorizeResponseUri = true; } else { // The application cannot be authorised when using --dry-run as we have to write out the authentication data, which negates the whole 'dry-run' process addLogEntry(); addLogEntry("The application requires authorisation, which involves saving authentication data on your system. Application authorisation cannot be completed when using the '--dry-run' option."); addLogEntry(); addLogEntry("To authorise the application please use your original command without '--dry-run'."); addLogEntry(); addLogEntry("To exclusively authorise the application without performing any additional actions, do not add '--sync' or '--monitor' to your command line."); addLogEntry(); forceExit(); } } // match the authorization code auto c = matchFirst(response, r"(?:[\?&]code=)([\w\d-.]+)"); if (c.empty) { addLogEntry("An empty or invalid response uri was entered"); return false; } c.popFront(); // skip the whole match redeemToken(c.front); return true; } // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/drive_get JSONValue getDefaultDriveDetails() { string url; url = driveUrl; return get(url); } // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_get JSONValue getDefaultRootDetails() { string url; url = driveUrl ~ "/root"; return get(url); } // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_get JSONValue getDriveIdRoot(string driveId) { string url; url = driveByIdUrl ~ driveId ~ "/root"; return get(url); } // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/drive_get JSONValue getDriveQuota(string driveId) { string url; url = driveByIdUrl ~ driveId ~ "/"; url ~= "?select=quota"; return get(url); } // Return the details of the specified path, by giving the path we wish to query // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_get JSONValue getPathDetails(string path) { string url; if ((path == ".")||(path == "/")) { url = driveUrl ~ "/root/"; } else { url = itemByPathUrl ~ encodeComponent(path) ~ ":/"; } // Add select clause url ~= "?select=id,name,eTag,cTag,deleted,file,folder,root,fileSystemInfo,remoteItem,parentReference,size,createdBy,lastModifiedBy"; return get(url); } // Return the details of the specified item based on its driveID and itemID // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_get JSONValue getPathDetailsById(string driveId, string id) { string url; url = driveByIdUrl ~ driveId ~ "/items/" ~ id; url ~= "?select=id,name,eTag,cTag,deleted,file,folder,root,fileSystemInfo,remoteItem,parentReference,size,createdBy,lastModifiedBy,webUrl,lastModifiedDateTime"; return get(url); } // Return all the items that are shared with the user // https://docs.microsoft.com/en-us/graph/api/drive-sharedwithme JSONValue getSharedWithMe() { return get(sharedWithMeUrl); } // Create a shareable link for an existing file on OneDrive based on the accessScope JSON permissions // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_createlink JSONValue createShareableLink(string driveId, string id, JSONValue accessScope) { string url; url = driveByIdUrl ~ driveId ~ "/items/" ~ id ~ "/createLink"; return post(url, accessScope.toString()); } // Return the requested details of the specified path on the specified drive id and path // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_get JSONValue getPathDetailsByDriveId(string driveId, string path) { string url; // https://learn.microsoft.com/en-us/onedrive/developer/rest-api/concepts/addressing-driveitems?view=odsp-graph-online // Required format: /drives/{drive-id}/root:/{item-path}: url = driveByIdUrl ~ driveId ~ "/root:/" ~ encodeComponent(path) ~ ":"; url ~= "?select=id,name,eTag,cTag,deleted,file,folder,root,fileSystemInfo,remoteItem,parentReference,size,createdBy,lastModifiedBy"; return get(url); } // Track changes for a given driveId // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_delta // Your app begins by calling delta without any parameters. The service starts enumerating the drive's hierarchy, returning pages of items and either an @odata.nextLink or an @odata.deltaLink, as described below. // Your app should continue calling with the @odata.nextLink until you no longer see an @odata.nextLink returned, or you see a response with an empty set of changes. // After you have finished receiving all the changes, you may apply them to your local state. To check for changes in the future, call delta again with the @odata.deltaLink from the previous successful response. JSONValue getChangesByItemId(string driveId, string id, string deltaLink) { string[string] requestHeaders; // From March 1st 2025, this needs to be added to ensure that Shared Folders are sent in the Delta Query Response if (appConfig.accountType == "personal") { // OneDrive Personal Account addIncludeFeatureRequestHeader(&requestHeaders); } else { // Business or SharePoint Library // Only add if configured to do so if (appConfig.getValueBool("sync_business_shared_items")) { // Feature enabled, add headers addIncludeFeatureRequestHeader(&requestHeaders); } } string url; // configure deltaLink to query if (deltaLink.empty) { url = driveByIdUrl ~ driveId ~ "/items/" ~ id ~ "/delta"; // Reduce what we ask for in the response - which reduces the data transferred back to us, and reduces what is held in memory during initial JSON processing url ~= "?select=id,name,eTag,cTag,deleted,file,folder,root,fileSystemInfo,remoteItem,parentReference,size,createdBy,lastModifiedBy"; } else { url = deltaLink; } // get the response return get(url, false, requestHeaders); } // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_list_children JSONValue listChildren(string driveId, string id, string nextLink) { string[string] requestHeaders; // From March 1st 2025, this needs to be added to ensure that Shared Folders are sent in the Delta Query Response if (appConfig.accountType == "personal") { // OneDrive Personal Account addIncludeFeatureRequestHeader(&requestHeaders); } else { // Business or SharePoint Library // Only add if configured to do so if (appConfig.getValueBool("sync_business_shared_items")) { // Feature enabled, add headers addIncludeFeatureRequestHeader(&requestHeaders); } } string url; // configure URL to query if (nextLink.empty) { url = driveByIdUrl ~ driveId ~ "/items/" ~ id ~ "/children"; url ~= "?select=id,name,eTag,cTag,deleted,file,folder,root,fileSystemInfo,remoteItem,parentReference,size,createdBy,lastModifiedBy"; } else { url = nextLink; } return get(url, false, requestHeaders); } // https://learn.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_search JSONValue searchDriveForPath(string driveId, string path) { string url; url = "https://graph.microsoft.com/v1.0/drives/" ~ driveId ~ "/root/search(q='" ~ encodeComponent(path) ~ "')"; return get(url); } // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_update JSONValue updateById(const(char)[] driveId, const(char)[] id, JSONValue data, const(char)[] eTag = null) { string[string] requestHeaders; const(char)[] url = driveByIdUrl ~ driveId ~ "/items/" ~ id; if (eTag) requestHeaders["If-Match"] = to!string(eTag); return patch(url, data.toString(), requestHeaders); } // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_delete void deleteById(const(char)[] driveId, const(char)[] id, const(char)[] eTag = null) { // string[string] requestHeaders; const(char)[] url = driveByIdUrl ~ driveId ~ "/items/" ~ id; //TODO: investigate why this always fail with 412 (Precondition Failed) // if (eTag) requestHeaders["If-Match"] = eTag; performDelete(url); } // https://learn.microsoft.com/en-us/graph/api/driveitem-permanentdelete?view=graph-rest-1.0 void permanentDeleteById(const(char)[] driveId, const(char)[] id, const(char)[] eTag = null) { // string[string] requestHeaders; const(char)[] url = driveByIdUrl ~ driveId ~ "/items/" ~ id ~ "/permanentDelete"; //TODO: investigate why this always fail with 412 (Precondition Failed) // if (eTag) requestHeaders["If-Match"] = eTag; // as per documentation, a permanentDelete needs to be a HTTP POST performPermanentDelete(url); } // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_post_children JSONValue createById(string parentDriveId, string parentId, JSONValue item) { string url = driveByIdUrl ~ parentDriveId ~ "/items/" ~ parentId ~ "/children"; return post(url, item.toString()); } // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_put_content JSONValue simpleUpload(string localPath, string parentDriveId, string parentId, string filename) { string url = driveByIdUrl ~ parentDriveId ~ "/items/" ~ parentId ~ ":/" ~ encodeComponent(filename) ~ ":/content"; return put(url, localPath); } // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_put_content JSONValue simpleUploadReplace(string localPath, string driveId, string id) { string url = driveByIdUrl ~ driveId ~ "/items/" ~ id ~ "/content"; return put(url, localPath); } // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_createuploadsession //JSONValue createUploadSession(string parentDriveId, string parentId, string filename, string eTag = null, JSONValue item = null) { JSONValue createUploadSession(string parentDriveId, string parentId, string filename, const(char)[] eTag = null, JSONValue item = null) { string[string] requestHeaders; string url = driveByIdUrl ~ parentDriveId ~ "/items/" ~ parentId ~ ":/" ~ encodeComponent(filename) ~ ":/createUploadSession"; // eTag If-Match header addition commented out for the moment // At some point, post the creation of this upload session the eTag is being 'updated' by OneDrive, thus when uploadFragment() is used // this generates a 412 Precondition Failed and then a 416 Requested Range Not Satisfiable // This needs to be investigated further as to why this occurs if (eTag) requestHeaders["If-Match"] = to!string(eTag); return post(url, item.toString(), requestHeaders); } // https://dev.onedrive.com/items/upload_large_files.htm JSONValue uploadFragment(string uploadUrl, string filepath, long offset, long offsetSize, long fileSize) { // open file as read-only in binary mode // If we upload a modified file, with the current known online eTag, this gets changed when the session is started - thus, the tail end of uploading // a fragment fails with a 412 Precondition Failed and then a 416 Requested Range Not Satisfiable // For the moment, comment out adding the If-Match header in createUploadSession, which then avoids this issue string contentRange = "bytes " ~ to!string(offset) ~ "-" ~ to!string(offset + offsetSize - 1) ~ "/" ~ to!string(fileSize); if (debugLogging) { addLogEntry("", ["debug"]); // Add an empty newline before log output addLogEntry("contentRange: " ~ contentRange, ["debug"]); } return put(uploadUrl, filepath, true, contentRange, offset, offsetSize); } // https://dev.onedrive.com/items/upload_large_files.htm JSONValue requestUploadStatus(string uploadUrl) { return get(uploadUrl, true); } // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/site_search?view=odsp-graph-online JSONValue o365SiteSearch(string nextLink) { string url; // configure URL to query if (nextLink.empty) { url = siteSearchUrl ~ "=*"; } else { url = nextLink; } return get(url); } // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/drive_list?view=odsp-graph-online JSONValue o365SiteDrives(string site_id, string nextLink){ string url; // configure URL to query if (nextLink.empty) { url = siteDriveUrl ~ site_id ~ "/drives"; } else { url = nextLink; } return get(url); } JSONValue createSubscription(string notificationUrl, SysTime expirationDateTime) { string driveId = appConfig.getValueString("drive_id"); string url = subscriptionUrl; // Create a resource item based on if we have a driveId string resourceItem; if (driveId.length) { resourceItem = "/drives/" ~ driveId ~ "/root"; } else { resourceItem = "/me/drive/root"; } // create JSON request to create webhook subscription const JSONValue request = [ "changeType": "updated", "notificationUrl": notificationUrl, "resource": resourceItem, "expirationDateTime": expirationDateTime.toISOExtString(), "clientState": randomUUID().toString() ]; return post(url, request.toString()); } JSONValue renewSubscription(string subscriptionId, SysTime expirationDateTime) { string url; url = subscriptionUrl ~ "/" ~ subscriptionId; const JSONValue request = [ "expirationDateTime": expirationDateTime.toISOExtString() ]; return patch(url, request.toString()); } void deleteSubscription(string subscriptionId) { string url; url = subscriptionUrl ~ "/" ~ subscriptionId; performDelete(url); } // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_get_content void downloadById(const(char)[] driveId, const(char)[] id, string saveToPath, long fileSize) { scope(failure) { if (exists(saveToPath)) { // try and remove the file, catch error try { remove(saveToPath); } catch (FileException exception) { // display the error message displayFileSystemErrorMessage(exception.msg, getFunctionName!({})); } } } // Create the required local directory string newPath = dirName(saveToPath); // Does the path exist locally? if (!exists(newPath)) { try { if (debugLogging) {addLogEntry("Requested local path does not exist, creating directory structure: " ~ newPath, ["debug"]);} mkdirRecurse(newPath); // Has the user disabled the setting of filesystem permissions? if (!appConfig.getValueBool("disable_permission_set")) { // Configure the applicable permissions for the folder if (debugLogging) {addLogEntry("Setting directory permissions for: " ~ newPath, ["debug"]);} newPath.setAttributes(appConfig.returnRequiredDirectoryPermissions()); } else { // Use inherited permissions if (debugLogging) {addLogEntry("Using inherited filesystem permissions for: " ~ newPath, ["debug"]);} } } catch (FileException exception) { // display the error message displayFileSystemErrorMessage(exception.msg, getFunctionName!({})); } } const(char)[] url = driveByIdUrl ~ driveId ~ "/items/" ~ id ~ "/content?AVOverride=1"; // Download file downloadFile(url, saveToPath, fileSize); // Does path exist? if (exists(saveToPath)) { // Has the user disabled the setting of filesystem permissions? if (!appConfig.getValueBool("disable_permission_set")) { // File was downloaded successfully - configure the applicable permissions for the file if (debugLogging) {addLogEntry("Setting file permissions for: " ~ saveToPath, ["debug"]);} saveToPath.setAttributes(appConfig.returnRequiredFilePermissions()); } else { // Use inherited permissions if (debugLogging) {addLogEntry("Using inherited filesystem permissions for: " ~ newPath, ["debug"]);} } } } // Return the actual siteSearchUrl being used and/or requested when performing 'siteQuery = onedrive.o365SiteSearch(nextLink);' call string getSiteSearchUrl() { return siteSearchUrl; } // Private OneDrive API Functions private void addIncludeFeatureRequestHeader(string[string]* headers) { if (appConfig.accountType == "personal") { // Add logging message for OneDrive Personal Accounts if (debugLogging) {addLogEntry("Adding 'Include-Feature=AddToOneDrive' API request header for OneDrive Personal Account Type", ["debug"]);} } else { // Add logging message for OneDrive Business Accounts if (debugLogging) {addLogEntry("Adding 'Include-Feature=AddToOneDrive' API request header as 'sync_business_shared_items' config option is enabled", ["debug"]);} } // Add feature to request headers (*headers)["Prefer"] = "Include-Feature=AddToOneDrive"; } private void redeemToken(char[] authCode) { char[] postData = "client_id=" ~ clientId ~ "&redirect_uri=" ~ redirectUrl ~ "&code=" ~ authCode ~ "&grant_type=authorization_code"; acquireToken(postData); } private void acquireToken(char[] postData) { JSONValue response; try { response = post(tokenUrl, postData, null, true, "application/x-www-form-urlencoded"); } catch (OneDriveException exception) { // an error was generated if ((exception.httpStatusCode == 400) || (exception.httpStatusCode == 401)) { // Release curl engine releaseCurlEngine(); // Handle an unauthorised client handleClientUnauthorised(exception.httpStatusCode, exception.error); // Must force exit here, allow logging to be done forceExit(); } else { if (exception.httpStatusCode >= 500) { // There was a HTTP 5xx Server Side Error - retry acquireToken(postData); } else { displayOneDriveErrorMessage(exception.msg, getFunctionName!({})); } } } if (response.type() == JSONType.object) { // Has the client been configured to use read_only_auth_scope if (appConfig.getValueBool("read_only_auth_scope")) { // read_only_auth_scope has been configured if ("scope" in response){ string effectiveScopes = response["scope"].str(); // Display the effective authentication scopes addLogEntry(); if (verboseLogging) {addLogEntry("Effective API Authentication Scopes: " ~ effectiveScopes, ["verbose"]);} // if we have any write scopes, we need to tell the user to update an remove online prior authentication and exit application if (canFind(effectiveScopes, "Write")) { // effective scopes contain write scopes .. so not a read-only configuration addLogEntry(); addLogEntry("ERROR: You have authentication scopes that allow write operations. You need to remove your existing application access consent"); addLogEntry(); addLogEntry("Please login to https://account.live.com/consent/Manage and remove your existing application access consent"); addLogEntry(); // force exit releaseCurlEngine(); // Must force exit here, allow logging to be done forceExit(); } } } if ("access_token" in response) { appConfig.accessToken = "bearer " ~ strip(response["access_token"].str); // Do we print the current access token if (appConfig.verbosityCount > 1) { if (appConfig.getValueBool("debug_https")) { if (appConfig.getValueBool("print_token")) { // This needs to be highly restricted in output .... if (debugLogging) {addLogEntry("CAUTION - KEEP THIS SAFE: Current access token: " ~ to!string(appConfig.accessToken), ["debug"]);} } } } refreshToken = strip(response["refresh_token"].str); appConfig.accessTokenExpiration = Clock.currTime() + dur!"seconds"(response["expires_in"].integer()); if (!dryRun) { // Update the refreshToken in appConfig so that we can reuse it if (appConfig.refreshToken.empty) { // The access token is empty if (debugLogging) {addLogEntry("Updating appConfig.refreshToken with new refreshToken as appConfig.refreshToken is empty", ["debug"]);} appConfig.refreshToken = refreshToken; } else { // Is the access token different? if (appConfig.refreshToken != refreshToken) { // Update the memory version if (debugLogging) {addLogEntry("Updating appConfig.refreshToken with updated refreshToken", ["debug"]);} appConfig.refreshToken = refreshToken; } } // try and update the refresh_token file on disk try { if (debugLogging) {addLogEntry("Updating refreshToken on disk", ["debug"]);} std.file.write(appConfig.refreshTokenFilePath, refreshToken); if (debugLogging) {addLogEntry("Setting file permissions for: " ~ appConfig.refreshTokenFilePath, ["debug"]);} appConfig.refreshTokenFilePath.setAttributes(appConfig.returnRequiredFilePermissions()); } catch (FileException exception) { // display the error message displayFileSystemErrorMessage(exception.msg, getFunctionName!({})); } } } else { // Release curl engine releaseCurlEngine(); // Log error message addLogEntry("\nInvalid authentication response from OneDrive. Please check the response uri\n"); // re-authorize authorise(); } } else { // Release curl engine releaseCurlEngine(); addLogEntry("Invalid response from the Microsoft Graph API. Unable to initialise OneDrive API instance."); // Must force exit here, allow logging to be done forceExit(); } } private void newToken() { if (debugLogging) {addLogEntry("Need to generate a new access token for Microsoft OneDrive", ["debug"]);} auto postData = appender!(string)(); postData ~= "client_id=" ~ clientId; postData ~= "&redirect_uri=" ~ redirectUrl; postData ~= "&refresh_token=" ~ to!string(refreshToken); postData ~= "&grant_type=refresh_token"; acquireToken(postData.data.dup); } private void checkAccessTokenExpired() { if (Clock.currTime() >= appConfig.accessTokenExpiration) { if (debugLogging) {addLogEntry("Microsoft OneDrive Access Token has expired. Must generate a new Microsoft OneDrive Access Token", ["debug"]);} newToken(); } else { if (debugLogging) {addLogEntry("Existing Microsoft OneDrive Access Token Expires: " ~ to!string(appConfig.accessTokenExpiration), ["debug"]);} } } private string getAccessToken() { checkAccessTokenExpired(); return to!string(appConfig.accessToken); } private void addAccessTokenHeader(string[string]* requestHeaders) { (*requestHeaders)["Authorization"] = getAccessToken(); } private void connect(HTTP.Method method, const(char)[] url, bool skipToken, CurlResponse response, string[string] requestHeaders=null) { if (debugLogging) {addLogEntry("Request URL = " ~ to!string(url), ["debug"]);} // Check access token first in case the request is overridden if (!skipToken) addAccessTokenHeader(&requestHeaders); curlEngine.setResponseHolder(response); foreach(k, v; requestHeaders) { curlEngine.addRequestHeader(k, v); } curlEngine.connect(method, url); } private void performDelete(const(char)[] url, string[string] requestHeaders=null, string callingFunction=__FUNCTION__, int lineno=__LINE__) { bool validateJSONResponse = false; oneDriveErrorHandlerWrapper((CurlResponse response) { connect(HTTP.Method.del, url, false, response, requestHeaders); return curlEngine.execute(); }, validateJSONResponse, callingFunction, lineno); } private void performPermanentDelete(const(char)[] url, string[string] requestHeaders=null, string callingFunction=__FUNCTION__, int lineno=__LINE__) { bool validateJSONResponse = false; oneDriveErrorHandlerWrapper((CurlResponse response) { connect(HTTP.Method.post, url, false, response, requestHeaders); return curlEngine.execute(); }, validateJSONResponse, callingFunction, lineno); } private void downloadFile(const(char)[] url, string filename, long fileSize, string callingFunction=__FUNCTION__, int lineno=__LINE__) { // Threshold for displaying download bar long thresholdFileSize = 4 * 2^^20; // 4 MiB // To support marking of partially-downloaded files, string originalFilename = filename; string downloadFilename = filename ~ ".partial"; bool validateJSONResponse = false; oneDriveErrorHandlerWrapper((CurlResponse response) { connect(HTTP.Method.get, url, false, response); if (fileSize >= thresholdFileSize){ // Download Progress variables size_t expected_total_segments = 20; ulong start_unix_time = Clock.currTime.toUnixTime(); int h, m, s; string etaString; bool barInit = false; real previousProgressPercent = -1.0; real percentCheck = 5.0; size_t segmentCount = -1; // Setup progress bar to display curlEngine.http.onProgress = delegate int(size_t dltotal, size_t dlnow, size_t ultotal, size_t ulnow) { // For each onProgress, what is the % of dlnow to dltotal // floor - rounds down to nearest whole number real currentDLPercent = floor(double(dlnow)/dltotal*100); string downloadLogEntry = "Downloading: " ~ filename ~ " ... "; // Have we started downloading? if (currentDLPercent > 0){ // We have started downloading if (debugLogging) { addLogEntry("", ["debug"]); // Debug new line only addLogEntry("Data Received = " ~ to!string(dlnow), ["debug"]); addLogEntry("Expected Total = " ~ to!string(dltotal), ["debug"]); addLogEntry("Percent Complete = " ~ to!string(currentDLPercent), ["debug"]); } // Every 5% download we need to increment the download bar // Has the user set a data rate limit? // when using rate_limit, we will get odd download rates, for example: // Percent Complete = 24 // Data Received = 13080163 // Expected Total = 52428800 // Percent Complete = 24 // Data Received = 13685777 // Expected Total = 52428800 // Percent Complete = 26 <---- jumps to 26% missing 25%, thus fmod misses incrementing progress bar // Data Received = 13685777 // Expected Total = 52428800 // Percent Complete = 26 if (appConfig.getValueLong("rate_limit") > 0) { // User configured rate limit // How much data should be in each segment to qualify for 5% ulong dataPerSegment = to!ulong(floor(double(dltotal)/expected_total_segments)); // How much data received do we need to validate against ulong thisSegmentData = dataPerSegment * segmentCount; ulong nextSegmentData = dataPerSegment * (segmentCount + 1); // Has the data that has been received in a 5% window that we need to increment the progress bar at if ((dlnow > thisSegmentData) && (dlnow < nextSegmentData) && (previousProgressPercent != currentDLPercent) || (dlnow == dltotal)) { // Downloaded data equals approx 5% if (debugLogging) {addLogEntry("Incrementing Progress Bar using calculated 5% of data received", ["debug"]);} // 100% check if (currentDLPercent != 100) { // Not 100% yet // Calculate the output segmentCount++; auto eta = calc_eta(segmentCount, expected_total_segments, start_unix_time); dur!"seconds"(eta).split!("hours", "minutes", "seconds")(h, m, s); etaString = format!"| ETA %02d:%02d:%02d"( h, m, s); string percentage = leftJustify(to!string(currentDLPercent) ~ "%", 5, ' '); addLogEntry(downloadLogEntry ~ percentage ~ etaString, ["consoleOnly"]); } else { // 100% done ulong end_unix_time = Clock.currTime.toUnixTime(); auto upload_duration = cast(int)(end_unix_time - start_unix_time); dur!"seconds"(upload_duration).split!("hours", "minutes", "seconds")(h, m, s); etaString = format!"| DONE in %02d:%02d:%02d"( h, m, s); string percentage = leftJustify(to!string(currentDLPercent) ~ "%", 5, ' '); addLogEntry(downloadLogEntry ~ percentage ~ etaString, ["consoleOnly"]); } // update values if (debugLogging) {addLogEntry("Setting previousProgressPercent to " ~ to!string(currentDLPercent), ["debug"]);} previousProgressPercent = currentDLPercent; if (debugLogging) {addLogEntry("Incrementing segmentCount", ["debug"]);} segmentCount++; } } else { // Is currentDLPercent divisible by 5 leaving remainder 0 and does previousProgressPercent not equal currentDLPercent if ((isIdentical(fmod(currentDLPercent, percentCheck), 0.0)) && (previousProgressPercent != currentDLPercent)) { // currentDLPercent matches a new increment if (debugLogging) {addLogEntry("Incrementing Progress Bar using fmod match", ["debug"]);} // 100% check if (currentDLPercent != 100) { // Not 100% yet // Calculate the output segmentCount++; auto eta = calc_eta(segmentCount, expected_total_segments, start_unix_time); dur!"seconds"(eta).split!("hours", "minutes", "seconds")(h, m, s); etaString = format!"| ETA %02d:%02d:%02d"( h, m, s); string percentage = leftJustify(to!string(currentDLPercent) ~ "%", 5, ' '); addLogEntry(downloadLogEntry ~ percentage ~ etaString, ["consoleOnly"]); } else { // 100% done ulong end_unix_time = Clock.currTime.toUnixTime(); auto upload_duration = cast(int)(end_unix_time - start_unix_time); dur!"seconds"(upload_duration).split!("hours", "minutes", "seconds")(h, m, s); etaString = format!"| DONE in %02d:%02d:%02d"( h, m, s); string percentage = leftJustify(to!string(currentDLPercent) ~ "%", 5, ' '); addLogEntry(downloadLogEntry ~ percentage ~ etaString, ["consoleOnly"]); } // update values previousProgressPercent = currentDLPercent; } } } else { if ((currentDLPercent == 0) && (!barInit)) { // Calculate the output segmentCount++; etaString = "| ETA --:--:--"; string percentage = leftJustify(to!string(currentDLPercent) ~ "%", 5, ' '); addLogEntry(downloadLogEntry ~ percentage ~ etaString, ["consoleOnly"]); barInit = true; } } return 0; }; } else { // No progress bar } return curlEngine.download(originalFilename, downloadFilename); }, validateJSONResponse, callingFunction, lineno); } private JSONValue get(string url, bool skipToken = false, string[string] requestHeaders=null, string callingFunction=__FUNCTION__, int lineno=__LINE__) { bool validateJSONResponse = true; return oneDriveErrorHandlerWrapper((CurlResponse response) { connect(HTTP.Method.get, url, skipToken, response, requestHeaders); return curlEngine.execute(); }, validateJSONResponse, callingFunction, lineno); } private JSONValue patch(const(char)[] url, const(char)[] patchData, string[string] requestHeaders=null, const(char)[] contentType = "application/json", string callingFunction=__FUNCTION__, int lineno=__LINE__) { bool validateJSONResponse = true; return oneDriveErrorHandlerWrapper((CurlResponse response) { connect(HTTP.Method.patch, url, false, response, requestHeaders); curlEngine.setContent(contentType, patchData); return curlEngine.execute(); }, validateJSONResponse, callingFunction, lineno); } private JSONValue post(const(char)[] url, const(char)[] postData, string[string] requestHeaders=null, bool skipToken = false, const(char)[] contentType = "application/json", string callingFunction=__FUNCTION__, int lineno=__LINE__) { bool validateJSONResponse = true; return oneDriveErrorHandlerWrapper((CurlResponse response) { connect(HTTP.Method.post, url, skipToken, response, requestHeaders); curlEngine.setContent(contentType, postData); return curlEngine.execute(); }, validateJSONResponse, callingFunction, lineno); } private JSONValue put(const(char)[] url, string filepath, bool skipToken=false, string contentRange=null, ulong offset=0, ulong offsetSize=0, string callingFunction=__FUNCTION__, int lineno=__LINE__) { bool validateJSONResponse = true; return oneDriveErrorHandlerWrapper((CurlResponse response) { connect(HTTP.Method.put, url, skipToken, response); curlEngine.setFile(filepath, contentRange, offset, offsetSize); return curlEngine.execute(); }, validateJSONResponse, callingFunction, lineno); } // Wrapper function for all requests to OneDrive API // - This should throw a OneDriveException so that this exception can be handled appropriately elsewhere in the application private JSONValue oneDriveErrorHandlerWrapper(CurlResponse delegate(CurlResponse response) executer, bool validateJSONResponse, string callingFunction, int lineno) { // Create a new 'curl' response response = new CurlResponse(); // Other wrapper variables int retryAttempts = 0; int baseBackoffInterval = 1; // Base backoff interval in seconds int maxRetryCount = 175200; // Approx 365 days based on maxBackoffInterval + appConfig.defaultDataTimeout //int maxRetryCount = 5; // Temp int maxBackoffInterval = 120; // Maximum backoff interval in seconds int thisBackOffInterval = 0; int timestampAlign = 0; JSONValue result; SysTime currentTime; SysTime retryTime; bool retrySuccess = false; bool transientError = false; while (!retrySuccess) { // Reset thisBackOffInterval thisBackOffInterval = 0; transientError = false; if (retryAttempts >= 1) { // re-try log entry & clock time retryTime = Clock.currTime(); retryTime.fracSecs = Duration.zero; addLogEntry("Retrying the respective Microsoft Graph API call for Internal Thread ID: " ~ to!string(curlEngine.internalThreadId) ~ " (Timestamp: " ~ to!string(retryTime) ~ ") ..."); } try { response.reset(); response = executer(response); // Check for a valid response if (response.hasResponse) { // Process the response result = response.json(); // Print response if 'debugHTTPSResponse' is flagged if (debugHTTPSResponse){ if (debugLogging) {addLogEntry("Microsoft Graph API Response: " ~ response.dumpResponse(), ["debug"]);} } // Check http response code, raise a OneDriveException if the operation was not successfully performed if (checkHttpResponseCode(response.statusLine.code)) { // 'curl' on platforms like Ubuntu does not reliably provide the 'http.statusLine.reason' when using HTTP/2 // This is a curl bug, but because Ubuntu uses old packages and never updates them, we are stuck with working around this bug if (response.statusLine.reason.length == 0) { // No 'reason', fetch what it should have been response.statusLine.reason = getMicrosoftGraphStatusMessage(response.statusLine.code); } // Why are throwing a OneDriveException - do not do this for a 404 error as this is not required as we use a 404 if things are not online, to create them if (response.statusLine.code != 404) { if (debugLogging) { addLogEntry("response.statusLine.code: " ~ to!string(response.statusLine.code), ["debug"]); addLogEntry("response.statusLine.reason: " ~ to!string(response.statusLine.reason), ["debug"]); addLogEntry("actual curl response: " ~ to!string(response), ["debug"]); } } // For every HTTP error status code, including those from 3xx (other Redirection codes excluding 302), 4xx (Client Error), and 5xx (Server Error) series, will trigger the following line of code. throw new OneDriveException(response.statusLine.code, response.statusLine.reason, response); } // Do we need to validate the JSON response? if (validateJSONResponse) { if (result.type() != JSONType.object) { throw new OneDriveException(0, "Caller request a non null JSON response, get null instead", response); } } // If retryAttempts is greater than 1, it means we were re-trying the request if (retryAttempts > 1) { // No error from http.perform() on re-try if (!transientError) { // Log that Internet access has been restored addLogEntry("Internet connectivity to Microsoft OneDrive service has been restored"); } // unset the fresh connect option as this then creates performance issues if left enabled if (debugLogging) {addLogEntry("Unsetting libcurl to use a fresh connection as this causes a performance impact if left enabled", ["debug"]);} curlEngine.http.handle.set(CurlOption.fresh_connect,0); } // On successful processing, break out of the loop break; } else { // Throw a custom 506 error // Whilst this error code is a bit more esoteric and typically involves content negotiation issues that lead to a configuration error on the server, but it could be loosely // interpreted to signal that the response received didn't meet the expected criteria or format. throw new OneDriveException(506, "Received an unexpected response from Microsoft OneDrive", response); } // A 'curl' exception was thrown } catch (CurlException exception) { // Handle 'curl' exception errors // Detail the curl exception, debug output only if (debugLogging) { addLogEntry("Handling a specific Curl exception:", ["debug"]); addLogEntry(to!string(response), ["debug"]); } // Parse and display error message received from OneDrive if (debugLogging) {addLogEntry(callingFunction ~ "() - Generated a OneDrive CurlException", ["debug"]);} auto errorArray = splitLines(exception.msg); string errorMessage = errorArray[0]; // Configure libcurl to perform a fresh connection setFreshConnectOption(); // What is contained in the curl error message? if (canFind(errorMessage, "Couldn't connect to server on handle") || canFind(errorMessage, "Couldn't resolve host name on handle") || canFind(errorMessage, "Timeout was reached on handle")) { // Connectivity to Microsoft OneDrive was lost addLogEntry("Internet connectivity to Microsoft OneDrive service has been interrupted .. re-trying in the background"); // What caused the initial curl exception? if (canFind(errorMessage, "Couldn't resolve host name on handle")) { if (debugLogging) {addLogEntry("Unable to resolve server - DNS access blocked?", ["debug"]);} } if (canFind(errorMessage, "Couldn't connect to server on handle")) { if (debugLogging) {addLogEntry("Unable to connect to server - HTTPS access blocked?", ["debug"]);} } if (canFind(errorMessage, "Timeout was reached on handle")) { // Common cause is libcurl trying IPv6 DNS resolution when there are only IPv4 DNS servers available if (verboseLogging) { addLogEntry("A libcurl timeout has been triggered - data transfer too slow, no DNS resolution response, no server response", ["verbose"]); // There are 3 common causes for this issue: // 1. Usually poor DNS resolution where libcurl flip/flops to use IPv6 and is unable to resolve // 2. A device between the user and Microsoft OneDrive is unable to correctly handle HTTP/2 communication // 3. No Internet access from this system at this point in time addLogEntry(" - IPv6 DNS resolution issues may be causing timeouts. Consider setting 'ip_protocol_version' to IPv4 to potentially avoid this", ["verbose"]); addLogEntry(" - HTTP/2 compatibility issues might also be interfering with your system. Use 'force_http_11' to switch to HTTP/1.1 to potentially avoid this", ["verbose"]); addLogEntry(" - If these options do not resolve this timeout issue, please use --debug-https to diagnose this issue further.", ["verbose"]); } } } else { // Some other 'libcurl' error was returned if (canFind(errorMessage, "Problem with the SSL CA cert (path? access rights?) on handle")) { // error setting certificate verify locations: // CAfile: /etc/pki/tls/certs/ca-bundle.crt // CApath: none // // Tell the Curl Engine to bypass SSL check - essentially SSL is passing back a bad value due to 'stdio' compile time option // Further reading: // https://github.com/curl/curl/issues/6090 // https://github.com/openssl/openssl/issues/7536 // https://stackoverflow.com/questions/45829588/brew-install-fails-curl77-error-setting-certificate-verify // https://forum.dlang.org/post/vwvkbubufexgeuaxhqfl@forum.dlang.org addLogEntry("Problem with reading the local SSL CA cert via libcurl - please repair your system SSL CA Certificates"); throw new OneDriveError("OneDrive operation encountered an issue with libcurl reading the local SSL CA Certificates"); } else { // Was this a curl initialization error? if (canFind(errorMessage, "Failed initialization on handle")) { // initialization error ... prevent a run-away process if we have zero disk space ulong localActualFreeSpace = getAvailableDiskSpace("."); if (localActualFreeSpace == 0) { throw new OneDriveError("Zero disk space detected"); } } else { // Unknown error displayGeneralErrorMessage(exception, callingFunction, lineno); } } } // A OneDrive API exception was thrown } catch (OneDriveException exception) { // https://developer.overdrive.com/docs/reference-guide // https://learn.microsoft.com/en-us/onedrive/developer/rest-api/concepts/errors?view=odsp-graph-online // https://learn.microsoft.com/en-us/graph/errors /** HTTP/1.1 Response handling Errors in the OneDrive API are returned using standard HTTP status codes, as well as a JSON error response object. The following HTTP status codes should be expected. Status code Status message Description 100 Continue Continue 200 OK Request was handled OK 201 Created This means you've made a successful POST to checkout, lock in a format, or place a hold 204 No Content This means you've made a successful DELETE to remove a hold or return a title 400 Bad Request Cannot process the request because it is malformed or incorrect. 401 Unauthorized Required authentication information is either missing or not valid for the resource. 403 Forbidden Access is denied to the requested resource. The user might not have enough permission. 404 Not Found The requested resource doesn’t exist. 405 Method Not Allowed The HTTP method in the request is not allowed on the resource. 406 Not Acceptable This service doesn’t support the format requested in the Accept header. 408 Request Time out CUSTOM ERROR - Not expected from OneDrive, but can be used to handle Internet connection failures the same (fallback and try again) 409 Conflict The current state conflicts with what the request expects. For example, the specified parent folder might not exist. 410 Gone The requested resource is no longer available at the server. 411 Length Required A Content-Length header is required on the request. 412 Precondition Failed A precondition provided in the request (such as an if-match header) does not match the resource's current state. 413 Request Entity Too Large The request size exceeds the maximum limit. 415 Unsupported Media Type The content type of the request is a format that is not supported by the service. 416 Requested Range Not Satisfiable The specified byte range is invalid or unavailable. 422 Unprocessable Entity Cannot process the request because it is semantically incorrect. 423 Locked The file is currently checked out or locked for editing by another user 429 Too Many Requests Client application has been throttled and should not attempt to repeat the request until an amount of time has elapsed. 500 Internal Server Error There was an internal server error while processing the request. 501 Not Implemented The requested feature isn’t implemented. 502 Bad Gateway The service was unreachable 503 Service Unavailable The service is temporarily unavailable. You may repeat the request after a delay. There may be a Retry-After header. 504 Gateway Timeout The server, which is acting as a gateway or proxy, did not receive a timely response from an upstream server it needed to access in order to complete the request 506 Variant Also Negotiates CUSTOM ERROR - Received an unexpected response from Microsoft OneDrive 507 Insufficient Storage The maximum storage quota has been reached. 509 Bandwidth Limit Exceeded Your app has been throttled for exceeding the maximum bandwidth cap. Your app can retry the request again after more time has elapsed. HTTP/2 Response handling 0 OK **/ // Detail the OneDriveAPI exception, debug output only if (debugLogging) { addLogEntry("Handling a OneDrive API exception:", ["debug"]); addLogEntry(to!string(response), ["debug"]); // Parse and display error message received from OneDrive addLogEntry(callingFunction ~ "() - Generated a OneDriveException", ["debug"]); } // Perform action based on the HTTP Status Code switch(exception.httpStatusCode) { // 0 - OK ... HTTP/2 version of 200 OK case 0: break; // 100 - Continue case 100: break; // 408 - Request Time Out // 429 - Too Many Requests, backoff case 408,429: // If OneDrive sends a status code 429 then this function will be used to process the Retry-After response header which contains the value by which we need to wait if (exception.httpStatusCode == 408) { addLogEntry("Handling a Microsoft Graph API HTTP 408 Response Code (Request Time Out) - Internal Thread ID: " ~ to!string(curlEngine.internalThreadId)); } else { addLogEntry("Handling a Microsoft Graph API HTTP 429 Response Code (Too Many Requests) - Internal Thread ID: " ~ to!string(curlEngine.internalThreadId)); } // Read in the Retry-After HTTP header as set and delay as per this value before retrying the request thisBackOffInterval = response.getRetryAfterValue(); if (debugLogging) {addLogEntry("Using Retry-After Value = " ~ to!string(thisBackOffInterval), ["debug"]);} transientError = true; break; // Transient errors // 503 - Service Unavailable // 504 - Gateway Timeout case 503,504: // The server, while acting as a proxy, did not receive a timely response from the upstream server it needed to access in attempting to complete the request auto errorArray = splitLines(exception.msg); addLogEntry(to!string(errorArray[0]) ~ " when attempting to query the Microsoft Graph API Service - retrying applicable request in 30 seconds - Internal Thread ID: " ~ to!string(curlEngine.internalThreadId)); if (debugLogging) {addLogEntry("Thread sleeping for 30 seconds as the server did not receive a timely response from the upstream server it needed to access in attempting to complete the request", ["debug"]);} // Transient error - try again in 30 seconds thisBackOffInterval = 30; transientError = true; break; // Default default: // This exception should be then passed back to the original calling function for handling a OneDriveException throw new OneDriveException(response.statusLine.code, response.statusLine.reason, response); } // A FileSystem exception was thrown } catch (ErrnoException exception) { // There was a file system error // display the error message displayFileSystemErrorMessage(exception.msg, callingFunction); throw new OneDriveException(0, "There was a file system error during OneDrive request: " ~ exception.msg, response); } // Increment re-try counter retryAttempts++; // Configure libcurl to perform a fresh connection on API retry setFreshConnectOption(); // Has maxRetryCount been reached? if (retryAttempts > maxRetryCount) { addLogEntry("ERROR: Unable to reconnect to the Microsoft OneDrive service after " ~ to!string(retryAttempts) ~ " attempts lasting approximately 365 days"); throw new OneDriveException(408, "Request Timeout - HTTP 408 or Internet down?", response); } else { // Was 'thisBackOffInterval' set by a 429 event ? if (thisBackOffInterval == 0) { // Calculate and apply exponential backoff upto a maximum of 120 seconds before the API call is re-tried thisBackOffInterval = calculateBackoff(retryAttempts, baseBackoffInterval, maxBackoffInterval); // If this 'somehow' calculates a negative number, this is not correct .. and this has been seen in testing - unknown cause // // Retry attempt: 31 - Internal Thread ID: ICO4ELBlGXFwyTzh // This attempt timestamp: 2024-Aug-10 10:32:07 // Next retry in approx: -2147483648 seconds // Next retry approx: 1956-Jul-23 07:17:59 // Illegal instruction (core dumped) // // Set to 'maxBackoffInterval' if calculated value is negative if (thisBackOffInterval < 0) { thisBackOffInterval = maxBackoffInterval; } } // set the current time for this thread currentTime = Clock.currTime(); currentTime.fracSecs = Duration.zero; // If verbose logging, detail when we are re-trying the call if (verboseLogging) { auto timeString = currentTime.toString(); addLogEntry("Retry attempt: " ~ to!string(retryAttempts) ~ " - Internal Thread ID: " ~ to!string(curlEngine.internalThreadId), ["verbose"]); addLogEntry(" This attempt timestamp: " ~ timeString, ["verbose"]); // Detail when the next attempt will be tried // Factor in the delay for curl to generate the exception - otherwise the next timestamp appears to be 'out' even though technically correct auto nextRetry = currentTime + dur!"seconds"(thisBackOffInterval) + dur!"seconds"(timestampAlign); addLogEntry(" Next retry in approx: " ~ to!string((thisBackOffInterval + timestampAlign)) ~ " seconds"); addLogEntry(" Next retry approx: " ~ to!string(nextRetry), ["verbose"]); } // Thread sleep Thread.sleep(dur!"seconds"(thisBackOffInterval)); } } // Return the result return result; } // Check the HTTP Response code and determine if a OneDriveException should be thrown private bool checkHttpResponseCode(int httpResponseCode) { bool shouldThrow = false; // // This condition checks if the HTTP response code falls within the acceptable range for both HTTP 1.1 and HTTP 2.0. // // For HTTP 1.1: // - Any 1xx response (Informational responses, ranging from 100 to 199) // - Any 2xx response (Successful responses, ranging from 200 to 299) // - A 302 response (Temporary Redirect) // // For HTTP 2.0: // - Any 1xx response (Informational responses, ranging from 100 to 199) // - Any 2xx response (Successful responses, ranging from 200 to 299) // - A 302 response (Temporary Redirect) // - A 0 response (Interpreted as 200 OK based on empirical evidence) // // If the HTTP response code meets any of these conditions, it is considered acceptable, and no exception will be thrown. // if ((httpResponseCode >= 100 && httpResponseCode < 200) || (httpResponseCode >= 200 && httpResponseCode < 300) || httpResponseCode == 302 || httpResponseCode == 0) { shouldThrow = false; } else { shouldThrow = true; } // return evaluation return shouldThrow; } // Calculates the delay for exponential backoff private int calculateBackoff(int retryAttempts, int baseInterval, int maxInterval) { int backoffTime = min(pow(2, retryAttempts) * baseInterval, maxInterval); return backoffTime; } // Configure libcurl to perform a fresh connection private void setFreshConnectOption() { if (debugLogging) {addLogEntry("Configuring libcurl to use a fresh connection for re-try", ["debug"]);} curlEngine.http.handle.set(CurlOption.fresh_connect,1); } // Generate a HTTP 'reason' based on the HTTP 'code' private string getMicrosoftGraphStatusMessage(ushort code) { string message; switch (code) { case 200: message = "OK"; break; case 201: message = "Created"; break; case 202: message = "Accepted"; break; case 204: message = "No Content"; break; case 301: message = "Moved Permanently"; break; case 302: message = "Found"; break; case 304: message = "Not Modified"; break; case 400: message = "Bad Request"; break; case 401: message = "Unauthorized"; break; case 402: message = "Payment Required"; break; case 403: message = "Forbidden"; break; case 404: message = "Not Found"; break; case 405: message = "Method Not Allowed"; break; case 406: message = "Not Acceptable"; break; case 409: message = "Conflict"; break; case 410: message = "Gone"; break; case 411: message = "Length Required"; break; case 412: message = "Precondition Failed"; break; case 413: message = "Request Entity Too Large"; break; case 415: message = "Unsupported Media Type"; break; case 416: message = "Requested Range Not Satisfiable"; break; case 422: message = "Unprocessable Entity"; break; case 423: message = "Locked"; break; case 429: message = "Too Many Requests"; break; case 500: message = "Internal Server Error"; break; case 501: message = "Not Implemented"; break; case 503: message = "Service Unavailable"; break; case 504: message = "Gateway Timeout"; break; case 507: message = "Insufficient Storage"; break; case 509: message = "Bandwidth Limit Exceeded"; break; default: message = "Unknown Status Code"; break; } return message; } }onedrive-2.5.5/src/qxor.d000066400000000000000000000044071476564400300153010ustar00rootroot00000000000000// What is this module called? module qxor; // What does this module require to function? import std.algorithm; import std.digest; // Implementation of the QuickXorHash algorithm in D // https://github.com/OneDrive/onedrive-api-docs/blob/live/docs/code-snippets/quickxorhash.md struct QuickXor { private enum int widthInBits = 160; private enum size_t lengthInBytes = (widthInBits - 1) / 8 + 1; private enum size_t lengthInQWords = (widthInBits - 1) / 64 + 1; private enum int bitsInLastCell = widthInBits % 64; // 32 private enum int shift = 11; private ulong[lengthInQWords] _data; private ulong _lengthSoFar; private int _shiftSoFar; nothrow @safe void put(scope const(ubyte)[] array...) { int vectorArrayIndex = _shiftSoFar / 64; int vectorOffset = _shiftSoFar % 64; immutable size_t iterations = min(array.length, widthInBits); for (size_t i = 0; i < iterations; i++) { immutable bool isLastCell = vectorArrayIndex == _data.length - 1; immutable int bitsInVectorCell = isLastCell ? bitsInLastCell : 64; if (vectorOffset <= bitsInVectorCell - 8) { for (size_t j = i; j < array.length; j += widthInBits) { _data[vectorArrayIndex] ^= cast(ulong) array[j] << vectorOffset; } } else { int index1 = vectorArrayIndex; int index2 = isLastCell ? 0 : (vectorArrayIndex + 1); ubyte low = cast(ubyte) (bitsInVectorCell - vectorOffset); ubyte xoredByte = 0; for (size_t j = i; j < array.length; j += widthInBits) { xoredByte ^= array[j]; } _data[index1] ^= cast(ulong) xoredByte << vectorOffset; _data[index2] ^= cast(ulong) xoredByte >> low; } vectorOffset += shift; if (vectorOffset >= bitsInVectorCell) { vectorArrayIndex = isLastCell ? 0 : vectorArrayIndex + 1; vectorOffset -= bitsInVectorCell; } } _shiftSoFar = cast(int) (_shiftSoFar + shift * (array.length % widthInBits)) % widthInBits; _lengthSoFar += array.length; } nothrow @safe void start() { _data = _data.init; _shiftSoFar = 0; _lengthSoFar = 0; } nothrow @trusted ubyte[lengthInBytes] finish() { ubyte[lengthInBytes] tmp; tmp[0 .. lengthInBytes] = (cast(ubyte*) _data)[0 .. lengthInBytes]; for (size_t i = 0; i < 8; i++) { tmp[lengthInBytes - 8 + i] ^= (cast(ubyte*) &_lengthSoFar)[i]; } return tmp; } }onedrive-2.5.5/src/sqlite.d000066400000000000000000000211611476564400300156050ustar00rootroot00000000000000// What is this module called? module sqlite; // What does this module require to function? import std.stdio; import etc.c.sqlite3; import std.string: fromStringz, toStringz; import core.stdc.stdlib; import std.conv; import std.format; // What other modules that we have created do we need to import? import log; import util; extern (C) immutable(char)* sqlite3_errstr(int); // missing from the std library // Callback function to check if table exists extern (C) int tableExistsCallback(void* data, int argc, char** argv, char** colNames) { // Set `tableExists` to 1 if at least one row is returned int* tableExists = cast(int*) data; *tableExists = 1; return 0; // Continue processing } static this() { if (sqlite3_libversion_number() < 3006019) { throw new SqliteException(-1, "SQLite 3.6.19 or newer is required"); } } private string ifromStringz(const(char)* cstr) { return fromStringz(cstr).idup; } class SqliteException: Exception { int errorCode; // Add an errorCode member to store the SQLite error code @safe pure nothrow this(int errorCode, string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null) { super(msg, file, line, next); this.errorCode = errorCode; // Set the errorCode } @safe pure nothrow this(int errorCode, string msg, Throwable next, string file = __FILE__, size_t line = __LINE__) { super(msg, file, line, next); this.errorCode = errorCode; // Set the errorCode } } struct Database { private sqlite3* pDb; this(const(char)[] filename) { open(filename); } ~this() { close(); } int db_checkpoint() { return sqlite3_wal_checkpoint(pDb, null); } // Dump open statements void dump_open_statements() { if (debugLogging) {addLogEntry("Dumping open SQL statements:", ["debug"]);} auto p = sqlite3_next_stmt(pDb, null); while (p != null) { if (debugLogging) {addLogEntry(" Still Open: " ~ to!string(ifromStringz(sqlite3_sql(p))), ["debug"]);} p = sqlite3_next_stmt(pDb, p); } } // Close open statements void close_open_statements() { if (debugLogging) {addLogEntry("Closing open SQL statements:", ["debug"]);} auto p = sqlite3_next_stmt(pDb, null); while (p != null) { // The sqlite3_finalize() function is called to delete a prepared statement sqlite3_finalize(p); addLogEntry(" Finalised: " ~ to!string(ifromStringz(sqlite3_sql(p)))); p = sqlite3_next_stmt(pDb, p); } } // Count open statements int count_open_statements() { if (debugLogging) {addLogEntry("Counting open SQL statements", ["debug"]);} int openStatementCount = 0; auto p = sqlite3_next_stmt(pDb, null); while (p != null) { openStatementCount++; p = sqlite3_next_stmt(pDb, p); } return openStatementCount; } // Check DB Status void checkStatus() { int rc = sqlite3_errcode(pDb); if (rc != SQLITE_OK) { throw new SqliteException(rc, getErrorMessage()); } } // Open the database file void open(const(char)[] filename) { // https://www.sqlite.org/c3ref/open.html // Safest multithreaded way to open the database int rc = sqlite3_open_v2( toStringz(filename), /* Database filename (UTF-8) */ &pDb, /* OUT: SQLite db handle */ SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, /* Flags */ null /* Optional: Name of the VFS module to use */ ); if (rc != SQLITE_OK) { string errorMsg; if (rc == SQLITE_CANTOPEN) { // Database cannot be opened errorMsg = "The database cannot be opened. Please check the permissions of " ~ to!string(filename); } else { // Some other error errorMsg = "A database access error occurred: " ~ getErrorMessage(); } // Log why we could not open the database file addLogEntry(); addLogEntry(errorMsg); addLogEntry(); close(); throw new SqliteException(rc, getErrorMessage()); } // Opened database file OK // Flag to always use extended result codes for errors sqlite3_extended_result_codes(pDb, 1); } void exec(const(char)[] sql) { // https://www.sqlite.org/c3ref/exec.html if (pDb !is null) { int rc = sqlite3_exec(pDb, toStringz(sql), null, null, null); if (rc != SQLITE_OK) { // Get error message and print it, then exit string errorMessage = getErrorMessage(); close(); // Throw sqlite error throw new SqliteException(rc, errorMessage); } } } // Check if the table exists before dropping it void dropTableIfExists(const(char)[] tableName) { string checkTableQuery = "SELECT name FROM sqlite_master WHERE type='table' AND name='" ~ to!string(tableName) ~ "';"; int tableExists = 0; // Execute query with callback to check if table exists int rc = sqlite3_exec(pDb, toStringz(checkTableQuery), &tableExistsCallback, &tableExists, null); // Only proceed if the query executed successfully if (rc == SQLITE_OK) { // If the table exists, drop it if (tableExists == 1) { exec("DROP TABLE " ~ tableName); } else { // Optionally log that the table does not exist addLogEntry(format("WARNING: Table '%s' does not exist, skipping table drop.", to!string(tableName))); } } else { // Log or handle the error if `sqlite3_exec` fails addLogEntry(format("ERROR: Failed to execute table existence check for '%s'.", to!string(tableName))); } } // Get DB Version int getVersion() { int userVersion; extern (C) int callback(void* user_version, int count, char** column_text, char** column_name) { import core.stdc.stdlib: atoi; *(cast(int*) user_version) = atoi(*column_text); return 0; } int rc = sqlite3_exec(pDb, "PRAGMA user_version", &callback, &userVersion, null); if (rc != SQLITE_OK) { throw new SqliteException(rc, getErrorMessage()); } return userVersion; } // Get the threadsafe value int getThreadsafeValue() { return sqlite3_threadsafe(); } // Get sqlite error message string getErrorMessage() { return ifromStringz(sqlite3_errmsg(pDb)); } void setVersion(int userVersion) { exec("PRAGMA user_version=" ~ to!string(userVersion)); } Statement prepare(const(char)[] zSql) { Statement s; // https://www.sqlite.org/c3ref/prepare.html if (pDb !is null) { int rc = sqlite3_prepare_v2(pDb, zSql.ptr, cast(int) zSql.length, &s.pStmt, null); if (rc != SQLITE_OK) { throw new SqliteException(rc, getErrorMessage()); } } return s; } void close() { // https://www.sqlite.org/c3ref/close.html if (pDb !is null) { sqlite3_close_v2(pDb); pDb = null; } } } struct Statement { struct Result { private sqlite3_stmt* pStmt; private const(char)[][] row; private this(sqlite3_stmt* pStmt) { this.pStmt = pStmt; step(); // initialize the range } @property bool empty() { return row.length == 0; } @property auto front() { return row; } alias step popFront; void step() { // https://www.sqlite.org/c3ref/step.html int rc = sqlite3_step(pStmt); if (rc == SQLITE_BUSY) { // Database is locked by another onedrive process addLogEntry("The database is currently locked by another process - cannot sync"); return; } if (rc == SQLITE_DONE) { row.length = 0; } else if (rc == SQLITE_ROW) { // https://www.sqlite.org/c3ref/data_count.html int count = 0; count = sqlite3_data_count(pStmt); row = new const(char)[][count]; foreach (size_t i, ref column; row) { // https://www.sqlite.org/c3ref/column_blob.html column = fromStringz(sqlite3_column_text(pStmt, to!int(i))); } } else { string errorMessage = getErrorMessage(); // Must force exit here, allow logging to be done throw new SqliteException(rc, errorMessage); } } string getErrorMessage() { return ifromStringz(sqlite3_errmsg(sqlite3_db_handle(pStmt))); } } private sqlite3_stmt* pStmt; ~this() { // Finalise any prepared statement finalise(); } // https://www.sqlite.org/c3ref/finalize.html void finalise() { if (pStmt !is null) { // The sqlite3_finalize() function is called to delete a prepared statement sqlite3_finalize(pStmt); pStmt = null; } } void bind(int index, const(char)[] value) { reset(); // https://www.sqlite.org/c3ref/bind_blob.html int rc = sqlite3_bind_text(pStmt, index, value.ptr, cast(int) value.length, SQLITE_STATIC); if (rc != SQLITE_OK) { throw new SqliteException(rc, getErrorMessage()); } } Result exec() { reset(); return Result(pStmt); } private void reset() { // https://www.sqlite.org/c3ref/reset.html int rc = sqlite3_reset(pStmt); if (rc != SQLITE_OK) { throw new SqliteException(rc, getErrorMessage()); } } string getErrorMessage() { return ifromStringz(sqlite3_errmsg(sqlite3_db_handle(pStmt))); } }onedrive-2.5.5/src/sync.d000066400000000000000000022553161476564400300152750ustar00rootroot00000000000000// What is this module called? module syncEngine; // What does this module require to function? import core.memory; import core.stdc.stdlib: EXIT_SUCCESS, EXIT_FAILURE, exit; import core.thread; import core.time; import std.algorithm; import std.array; import std.concurrency; import std.container.rbtree; import std.conv; import std.datetime; import std.encoding; import std.exception; import std.file; import std.json; import std.parallelism; import std.path; import std.range; import std.regex; import std.stdio; import std.string; import std.uni; import std.uri; import std.utf; import std.math; import std.typecons; // What other modules that we have created do we need to import? import config; import log; import util; import onedrive; import itemdb; import clientSideFiltering; import xattr; class JsonResponseException: Exception { @safe pure this(string inputMessage) { string msg = format(inputMessage); super(msg); } } class PosixException: Exception { @safe pure this(string localTargetName, string remoteTargetName) { string msg = format("POSIX 'case-insensitive match' between '%s' (local) and '%s' (online) which violates the Microsoft OneDrive API namespace convention", localTargetName, remoteTargetName); super(msg); } } class AccountDetailsException: Exception { @safe pure this() { string msg = format("Unable to query OneDrive API to obtain required account details"); super(msg); } } class SyncException: Exception { @nogc @safe pure nothrow this(string msg, string file = __FILE__, size_t line = __LINE__) { super(msg, file, line); } } struct DriveDetailsCache { // - driveId is the drive for the operations were items need to be stored // - quotaRestricted details a bool value as to if that drive is restricting our ability to understand if there is space available. Some 'Business' and 'SharePoint' restrict, and most (if not all) shared folders it cant be determined if there is free space // - quotaAvailable is a long value that stores the value of what the current free space is available online string driveId; bool quotaRestricted; bool quotaAvailable; long quotaRemaining; } struct DeltaLinkDetails { string driveId; string itemId; string latestDeltaLink; } struct DatabaseItemsToDeleteOnline { Item dbItem; string localFilePath; } class SyncEngine { // Class Variables ApplicationConfig appConfig; ItemDatabase itemDB; ClientSideFiltering selectiveSync; // Array of directory databaseItem.id to skip while applying the changes. // These are the 'parent path' id's that are being excluded, so if the parent id is in here, the child needs to be skipped as well RedBlackTree!string skippedItems = redBlackTree!string(); // Array of databaseItem.id to delete after the changes have been downloaded string[2][] idsToDelete; // Array of JSON items which are files or directories that are not 'root', skipped or to be deleted, that need to be processed JSONValue[] jsonItemsToProcess; // Array of JSON items which are files that are not 'root', skipped or to be deleted, that need to be downloaded JSONValue[] fileJSONItemsToDownload; // Array of paths that failed to download string[] fileDownloadFailures; // Associative array mapping of all OneDrive driveId's that have been seen, mapped with DriveDetailsCache data for reference DriveDetailsCache[string] onlineDriveDetails; // List of items we fake created when using --dry-run string[2][] idsFaked; // List of paths we fake deleted when using --dry-run string[] pathFakeDeletedArray; // Array of database Parent Item ID, Item ID & Local Path where the content has changed and needs to be uploaded string[3][] databaseItemsWhereContentHasChanged; // Array of local file paths that need to be uploaded as new items to OneDrive string[] newLocalFilesToUploadToOneDrive; // Array of local file paths that failed to be uploaded to OneDrive string[] fileUploadFailures; // List of path names changed online, but not changed locally when using --dry-run string[] pathsRenamed; // List of paths that were a POSIX case-insensitive match, thus could not be created online string[] posixViolationPaths; // List of local paths, that, when using the OneDrive Business Shared Folders feature, then disabling it, folder still exists locally and online // This list of local paths need to be skipped string[] businessSharedFoldersOnlineToSkip; // List of interrupted uploads session files that need to be resumed string[] interruptedUploadsSessionFiles; // List of validated interrupted uploads session JSON items to resume JSONValue[] jsonItemsToResumeUpload; // This list of local paths that need to be created online string[] pathsToCreateOnline; // Array of items from the database that have been deleted locally, that needs to be deleted online DatabaseItemsToDeleteOnline[] databaseItemsToDeleteOnline; // Array of parentId's that have been skipped via 'sync_list' string[] syncListSkippedParentIds; // Flag that there were upload or download failures listed bool syncFailures = false; // Is sync_list configured bool syncListConfigured = false; // Was --dry-run used? bool dryRun = false; // Was --upload-only used? bool uploadOnly = false; // Was --remove-source-files used? // Flag to set whether the local file should be deleted once it is successfully uploaded to OneDrive bool localDeleteAfterUpload = false; // Do we configure to disable the download validation routine due to --disable-download-validation // We will always validate our downloads // However, when downloading files from SharePoint, the OneDrive API will not advise the correct file size // which means that the application thinks the file download has failed as the size is different / hash is different // See: https://github.com/abraunegg/onedrive/discussions/1667 bool disableDownloadValidation = false; // Do we configure to disable the upload validation routine due to --disable-upload-validation // We will always validate our uploads // However, when uploading a file that can contain metadata SharePoint will associate some // metadata from the library the file is uploaded to directly in the file which breaks this validation. // See: https://github.com/abraunegg/onedrive/issues/205 // See: https://github.com/OneDrive/onedrive-api-docs/issues/935 bool disableUploadValidation = false; // Do we perform a local cleanup of files that are 'extra' on the local file system, when using --download-only bool cleanupLocalFiles = false; // Are we performing a --single-directory sync ? bool singleDirectoryScope = false; string singleDirectoryScopeDriveId; string singleDirectoryScopeItemId; // Is National Cloud Deployments configured ? bool nationalCloudDeployment = false; // Do we configure not to perform a remote file delete if --upload-only & --no-remote-delete configured bool noRemoteDelete = false; // Is bypass_data_preservation set via config file // Local data loss MAY occur in this scenario bool bypassDataPreservation = false; // Has the user configured to permanently delete files online rather than send to online recycle bin bool permanentDelete = false; // Maximum file size upload // https://support.microsoft.com/en-us/office/invalid-file-names-and-file-types-in-onedrive-and-sharepoint-64883a5d-228e-48f5-b3d2-eb39e07630fa?ui=en-us&rs=en-us&ad=us // July 2020, maximum file size for all accounts is 100GB // January 2021, maximum file size for all accounts is 250GB long maxUploadFileSize = 268435456000; // 250GB // Threshold after which files will be uploaded using an upload session long sessionThresholdFileSize = 4 * 2^^20; // 4 MiB // File size limit for file operations that the user has configured long fileSizeLimit; // Total data to upload long totalDataToUpload; // How many items have been processed for the active operation long processedCount; // Are we creating a simulated /delta response? This is critically important in terms of how we 'update' the database bool generateSimulatedDeltaResponse = false; // Store the latest DeltaLink string latestDeltaLink; // Struct of containing the deltaLink details DeltaLinkDetails deltaLinkCache; // Array of driveId and deltaLink for use when performing the last examination of the most recent online data alias DeltaLinkInfo = string[string]; DeltaLinkInfo deltaLinkInfo; // Flag to denote data cleanup pass when using --download-only --cleanup-local-files bool cleanupDataPass = false; // Create the specific task pool to process items in parallel TaskPool processPool; // Shared Folder Flags for 'sync_list' processing bool sharedFolderDeltaGeneration = false; string currentSharedFolderName = ""; // Directory excluded by 'sync_list flag so that when scanning that directory, if it is excluded, // can be scanned for new data which may be included by other include rule, but parent is excluded bool syncListDirExcluded = false; // Debug Logging Break Lines string debugLogBreakType1 = "-----------------------------------------------------------------------------------------------------------"; string debugLogBreakType2 = "==========================================================================================================="; // Configure this class instance this(ApplicationConfig appConfig, ItemDatabase itemDB, ClientSideFiltering selectiveSync) { // Create the specific task pool to process items in parallel processPool = new TaskPool(to!int(appConfig.getValueLong("threads"))); if (debugLogging) {addLogEntry("Initialised TaskPool worker with threads: " ~ to!string(processPool.size), ["debug"]);} // Configure the class variable to consume the application configuration this.appConfig = appConfig; // Configure the class variable to consume the database configuration this.itemDB = itemDB; // Configure the class variable to consume the selective sync (skip_dir, skip_file and sync_list) configuration this.selectiveSync = selectiveSync; // Configure the dryRun flag to capture if --dry-run was used // Application startup already flagged we are also in a --dry-run state, so no need to output anything else here this.dryRun = appConfig.getValueBool("dry_run"); // Configure file size limit if (appConfig.getValueLong("skip_size") != 0) { fileSizeLimit = appConfig.getValueLong("skip_size") * 2^^20; fileSizeLimit = (fileSizeLimit == 0) ? long.max : fileSizeLimit; } // Is there a sync_list file present? if (exists(appConfig.syncListFilePath)) { // yes there is a file present, but did we load any entries? if (!selectiveSync.validSyncListRules) { // function returned 'false' (array contains valid entries) // flag there are rules to process when we are performing Client Side Filtering if (debugLogging) {addLogEntry("Configuring syncListConfigured flag to TRUE as valid entries were loaded from 'sync_list' file", ["debug"]);} this.syncListConfigured = true; } else { // function returned 'true' meaning there are are zero sync_list rules loaded despite the 'sync_list' file being present // ensure this flag is false so we do not do any extra processing if (debugLogging) {addLogEntry("Configuring syncListConfigured flag to FALSE as no valid entries were loaded from 'sync_list' file", ["debug"]);} this.syncListConfigured = false; } } // Configure the uploadOnly flag to capture if --upload-only was used if (appConfig.getValueBool("upload_only")) { if (debugLogging) {addLogEntry("Configuring uploadOnly flag to TRUE as --upload-only passed in or configured", ["debug"]);} this.uploadOnly = true; } // Configure the localDeleteAfterUpload flag if (appConfig.getValueBool("remove_source_files")) { if (debugLogging) {addLogEntry("Configuring localDeleteAfterUpload flag to TRUE as --remove-source-files passed in or configured", ["debug"]);} this.localDeleteAfterUpload = true; } // Configure the disableDownloadValidation flag if (appConfig.getValueBool("disable_download_validation")) { if (debugLogging) {addLogEntry("Configuring disableDownloadValidation flag to TRUE as --disable-download-validation passed in or configured", ["debug"]);} this.disableDownloadValidation = true; } // Configure the disableUploadValidation flag if (appConfig.getValueBool("disable_upload_validation")) { if (debugLogging) {addLogEntry("Configuring disableUploadValidation flag to TRUE as --disable-upload-validation passed in or configured", ["debug"]);} this.disableUploadValidation = true; } // Do we configure to clean up local files if using --download-only ? if ((appConfig.getValueBool("download_only")) && (appConfig.getValueBool("cleanup_local_files"))) { // --download-only and --cleanup-local-files were passed in addLogEntry(); addLogEntry("WARNING: Application has been configured to cleanup local files that are not present online."); addLogEntry("WARNING: Local data loss MAY occur in this scenario if you are expecting data to remain archived locally."); addLogEntry(); // Set the flag this.cleanupLocalFiles = true; } // Do we configure to NOT perform a remote delete if --upload-only & --no-remote-delete configured ? if ((appConfig.getValueBool("upload_only")) && (appConfig.getValueBool("no_remote_delete"))) { // --upload-only and --no-remote-delete were passed in addLogEntry("WARNING: Application has been configured NOT to cleanup remote files that are deleted locally."); // Set the flag this.noRemoteDelete = true; } // Are we configured to use a National Cloud Deployment? if (appConfig.getValueString("azure_ad_endpoint") != "") { // value is configured, is it a valid value? if ((appConfig.getValueString("azure_ad_endpoint") == "USL4") || (appConfig.getValueString("azure_ad_endpoint") == "USL5") || (appConfig.getValueString("azure_ad_endpoint") == "DE") || (appConfig.getValueString("azure_ad_endpoint") == "CN")) { // valid entries to flag we are using a National Cloud Deployment // National Cloud Deployments do not support /delta as a query // https://docs.microsoft.com/en-us/graph/deployments#supported-features // Flag that we have a valid National Cloud Deployment that cannot use /delta queries this.nationalCloudDeployment = true; // Reverse set 'force_children_scan' for completeness appConfig.setValueBool("force_children_scan", true); } } // Are we forcing to use /children scan instead of /delta to simulate National Cloud Deployment use of /children? if (appConfig.getValueBool("force_children_scan")) { addLogEntry("Forcing client to use /children API call rather than /delta API to retrieve objects from the OneDrive API"); this.nationalCloudDeployment = true; } // Are we forcing the client to bypass any data preservation techniques to NOT rename any local files if there is a conflict? // The enabling of this function could lead to data loss if (appConfig.getValueBool("bypass_data_preservation")) { addLogEntry(); addLogEntry("WARNING: Application has been configured to bypass local data preservation in the event of file conflict."); addLogEntry("WARNING: Local data loss MAY occur in this scenario."); addLogEntry(); this.bypassDataPreservation = true; } // Did the user configure a specific rate limit for the application? if (appConfig.getValueLong("rate_limit") > 0) { // User configured rate limit addLogEntry("User Configured Rate Limit: " ~ to!string(appConfig.getValueLong("rate_limit"))); // If user provided rate limit is < 131072, flag that this is too low, setting to the recommended minimum of 131072 if (appConfig.getValueLong("rate_limit") < 131072) { // user provided limit too low addLogEntry("WARNING: User configured rate limit too low for normal application processing and preventing application timeouts. Overriding to recommended minimum of 131072 (128KB/s)"); appConfig.setValueLong("rate_limit", 131072); } } // Did the user downgrade all HTTP operations to force HTTP 1.1 if (appConfig.getValueBool("force_http_11")) { // User is forcing downgrade to curl to use HTTP 1.1 for all operations if (verboseLogging) {addLogEntry("Downgrading all HTTP operations to HTTP/1.1 due to user configuration", ["verbose"]);} } else { // Use curl defaults if (debugLogging) {addLogEntry("Using Curl defaults for HTTP operational protocol version (potentially HTTP/2)", ["debug"]);} } } // Initialise the Sync Engine class bool initialise() { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Control whether the worker threads are daemon threads. A daemon thread is automatically terminated when all non-daemon threads have terminated. processPool.isDaemon(true); // daemon thread // Flag for 'no-sync' task bool noSyncTask = false; // Create a new instance of the OneDrive API OneDriveApi oneDriveApiInstance; oneDriveApiInstance = new OneDriveApi(appConfig); // Exit scope - release curl engine back to pool scope(exit) { oneDriveApiInstance.releaseCurlEngine(); // Free object and memory oneDriveApiInstance = null; } // Issue #2941 // If the account being used _only_ has access to specific resources, getDefaultDriveDetails() will generate problems and cause // the application to exit, which, is technically the right thing to do (no access to account details) ... but if: // - are we doing a no-sync task ? // - do we have the 'drive_id' via config file ? // Are we not doing a --sync or a --monitor operation? Both of these will be false if they are not set if ((!appConfig.getValueBool("synchronize")) && (!appConfig.getValueBool("monitor"))) { // set flag noSyncTask = true; } // Can the API be initialised successfully? if (oneDriveApiInstance.initialise()) { // Get the relevant default drive details try { getDefaultDriveDetails(); } catch (AccountDetailsException exception) { // was this a no-sync task? if (!noSyncTask) { // details could not be queried addLogEntry(exception.msg); // Must force exit here, allow logging to be done forceExit(); } } // Get the relevant default root details try { getDefaultRootDetails(); } catch (AccountDetailsException exception) { // details could not be queried addLogEntry(exception.msg); // Must force exit here, allow logging to be done forceExit(); } // Display relevant account details try { // we only do this if we are doing --verbose logging if (verboseLogging) { displaySyncEngineDetails(); } } catch (AccountDetailsException exception) { // details could not be queried addLogEntry(exception.msg); // Must force exit here, allow logging to be done forceExit(); } } else { // API could not be initialised addLogEntry("OneDrive API could not be initialised with previously used details"); // Must force exit here, allow logging to be done forceExit(); } // Has the client been configured to permanently delete files online rather than send these to the online recycle bin? if (appConfig.getValueBool("permanent_delete")) { // This can only be set if not using: // - US Government L4 // - US Government L5 (DOD) // - Azure and Office365 operated by VNET in China // // Additionally, this is not supported by OneDrive Personal accounts: // // This is a doc bug. In fact, OneDrive personal accounts do not support the permanentDelete API, it only applies to OneDrive for Business and SharePoint document libraries. // // Reference: https://learn.microsoft.com/en-us/answers/questions/1501170/onedrive-permanently-delete-a-file string azureConfigValue = appConfig.getValueString("azure_ad_endpoint"); // Now that we know the 'accountType' we can configure this correctly if ((appConfig.accountType != "personal") && (azureConfigValue.empty || azureConfigValue == "DE")) { // Only supported for Global Service and DE based on https://learn.microsoft.com/en-us/graph/api/driveitem-permanentdelete?view=graph-rest-1.0 addLogEntry(); addLogEntry("WARNING: Application has been configured to permanently remove files online rather than send to the recycle bin. Permanently deleted items can't be restored."); addLogEntry("WARNING: Online data loss MAY occur in this scenario."); addLogEntry(); this.permanentDelete = true; } else { // what error message do we present if (appConfig.accountType == "personal") { // personal account type - API not supported addLogEntry(); addLogEntry("WARNING: The application is configured to permanently delete files online; however, this action is not supported by Microsoft OneDrive Personal Accounts."); addLogEntry(); } else { // Not a personal account addLogEntry(); addLogEntry("WARNING: The application is configured to permanently delete files online; however, this action is not supported by the National Cloud Deployment in use."); addLogEntry(); } // ensure this is false regardless this.permanentDelete = false; } } // API was initialised if (verboseLogging) {addLogEntry("Sync Engine Initialised with new Onedrive API instance", ["verbose"]);} // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return required value return true; } // Shutdown the sync engine, wait for anything in processPool to complete void shutdown() { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } if (debugLogging) {addLogEntry("SyncEngine: Waiting for all internal threads to complete", ["debug"]);} shutdownProcessPool(); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Shut down all running tasks that are potentially running in parallel void shutdownProcessPool() { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // TaskPool needs specific shutdown based on compiler version otherwise this causes a segfault if (processPool.size > 0) { // TaskPool is still configured for 'thread' size // Normal TaskPool shutdown process if (debugLogging) {addLogEntry("Shutting down processPool in a thread blocking manner", ["debug"]);} // All worker threads are daemon threads which are automatically terminated when all non-daemon threads have terminated. processPool.finish(true); // If blocking argument is true, wait for all worker threads to terminate before returning. } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Get Default Drive Details for this Account void getDefaultDriveDetails() { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Function variables JSONValue defaultOneDriveDriveDetails; bool noSyncTask = false; // Create a new instance of the OneDrive API OneDriveApi getDefaultDriveApiInstance; getDefaultDriveApiInstance = new OneDriveApi(appConfig); getDefaultDriveApiInstance.initialise(); // Are we not doing a --sync or a --monitor operation? Both of these will be false if they are not set if ((!appConfig.getValueBool("synchronize")) && (!appConfig.getValueBool("monitor"))) { // set flag noSyncTask = true; } // Get Default Drive Details for this Account try { if (debugLogging) {addLogEntry("Getting Account Default Drive Details", ["debug"]);} defaultOneDriveDriveDetails = getDefaultDriveApiInstance.getDefaultDriveDetails(); } catch (OneDriveException exception) { if (debugLogging) {addLogEntry("defaultOneDriveDriveDetails = getDefaultDriveApiInstance.getDefaultDriveDetails() generated a OneDriveException", ["debug"]);} if ((exception.httpStatusCode == 400) || (exception.httpStatusCode == 401)) { // Handle the 400 | 401 error handleClientUnauthorised(exception.httpStatusCode, exception.error); } else { // Default operation if not 400,401 errors // - 408,429,503,504 errors are handled as a retry within getDefaultDriveApiInstance // Display what the error is displayOneDriveErrorMessage(exception.msg, thisFunctionName); } } // If the JSON response is a correct JSON object, and has an 'id' we can set these details if ((defaultOneDriveDriveDetails.type() == JSONType.object) && (hasId(defaultOneDriveDriveDetails))) { if (debugLogging) {addLogEntry("OneDrive Account Default Drive Details: " ~ to!string(defaultOneDriveDriveDetails), ["debug"]);} appConfig.accountType = defaultOneDriveDriveDetails["driveType"].str; appConfig.defaultDriveId = defaultOneDriveDriveDetails["id"].str; // Issue #3115 - Validate driveId length // What account type is this? if (appConfig.accountType == "personal") { // Test driveId length and validation // Once checked and validated, we only need to check 'driveId' if it does not match exactly 'appConfig.defaultDriveId' appConfig.defaultDriveId = testProvidedDriveIdForLengthIssue(appConfig.defaultDriveId); } // Make sure that appConfig.defaultDriveId is in our driveIDs array to use when checking if item is in database // Keep the DriveDetailsCache array with unique entries only DriveDetailsCache cachedOnlineDriveData; if (!canFindDriveId(appConfig.defaultDriveId, cachedOnlineDriveData)) { // Add this driveId to the drive cache, which then also sets for the defaultDriveId: // - quotaRestricted; // - quotaAvailable; // - quotaRemaining; // // In some cases OneDrive Business configurations 'restrict' quota details thus is empty / blank / negative value / zero value // When addOrUpdateOneDriveOnlineDetails() is called, messaging is provided if these are zero, negative or missing (thus quota is being restricted) addOrUpdateOneDriveOnlineDetails(appConfig.defaultDriveId); } // Fetch the details from cachedOnlineDriveData for appConfig.defaultDriveId cachedOnlineDriveData = getDriveDetails(appConfig.defaultDriveId); // - cachedOnlineDriveData.quotaRestricted; // - cachedOnlineDriveData.quotaAvailable; // - cachedOnlineDriveData.quotaRemaining; // What did we set based on the data from the JSON and cached drive data if (debugLogging) { addLogEntry("appConfig.accountType = " ~ appConfig.accountType, ["debug"]); addLogEntry("appConfig.defaultDriveId = " ~ appConfig.defaultDriveId, ["debug"]); addLogEntry("cachedOnlineDriveData.quotaRemaining = " ~ to!string(cachedOnlineDriveData.quotaRemaining), ["debug"]); addLogEntry("cachedOnlineDriveData.quotaAvailable = " ~ to!string(cachedOnlineDriveData.quotaAvailable), ["debug"]); addLogEntry("cachedOnlineDriveData.quotaRestricted = " ~ to!string(cachedOnlineDriveData.quotaRestricted), ["debug"]); } // Regardless of this being all set - based on the JSON response, check for 'quota' being present, to check // for the following valid states: normal | nearing | critical | exceeded // // Based on this, then generate an applicable application message to advise the user of their quota status if ((hasQuota(defaultOneDriveDriveDetails)) && (hasQuotaState(defaultOneDriveDriveDetails))) { // get the current state string quotaState = defaultOneDriveDriveDetails["quota"]["state"].str; // quotaState = normal - no message string nearingMessage = "WARNING: Your Microsoft OneDrive storage is nearing capacity, with less than 10% of your available space remaining."; string criticalMessage = "WARNING: Your Microsoft OneDrive storage is critically low, with less than 1% of your available space remaining."; string exceededMessage = "CRITICAL: Your Microsoft OneDrive storage limit has been exceeded. You can no longer upload new content to Microsoft OneDrive."; string actionRequired = " Delete unneeded files or upgrade your storage plan now, as further uploads will not be possible once storage is exceeded"; // switch to display the right message switch(quotaState) { case "nearing": addLogEntry(); addLogEntry(nearingMessage, ["info", "notify"]); addLogEntry(actionRequired); addLogEntry(); break; case "critical": addLogEntry(); addLogEntry(criticalMessage, ["info", "notify"]); addLogEntry(actionRequired); addLogEntry(); break; case "exceeded": addLogEntry(); addLogEntry("******************************************************************************************************************************"); addLogEntry(exceededMessage, ["info", "notify"]); addLogEntry("******************************************************************************************************************************"); addLogEntry(); break; default: // nothing } } } else { // Did the configuration file contain a 'drive_id' entry // If this exists, this will be a 'documentLibrary' if (appConfig.getValueString("drive_id").length) { // Force set these as for whatever reason we could to query these via the getDefaultDriveDetails API call appConfig.accountType = "documentLibrary"; appConfig.defaultDriveId = appConfig.getValueString("drive_id"); } else { // was this a no-sync task? if (!noSyncTask) { // Handle the invalid JSON response by throwing an exception error throw new AccountDetailsException(); } } } // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory getDefaultDriveApiInstance.releaseCurlEngine(); getDefaultDriveApiInstance = null; // Perform Garbage Collection GC.collect(); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Get Default Root Details for this Account void getDefaultRootDetails() { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Function variables JSONValue defaultOneDriveRootDetails; bool noSyncTask = false; // Create a new instance of the OneDrive API OneDriveApi getDefaultRootApiInstance; getDefaultRootApiInstance = new OneDriveApi(appConfig); getDefaultRootApiInstance.initialise(); // Are we not doing a --sync or a --monitor operation? Both of these will be false if they are not set if ((!appConfig.getValueBool("synchronize")) && (!appConfig.getValueBool("monitor"))) { // set flag noSyncTask = true; } // Get Default Root Details for this Account try { if (debugLogging) {addLogEntry("Getting Account Default Root Details", ["debug"]);} defaultOneDriveRootDetails = getDefaultRootApiInstance.getDefaultRootDetails(); } catch (OneDriveException exception) { if (debugLogging) {addLogEntry("defaultOneDriveRootDetails = getDefaultRootApiInstance.getDefaultRootDetails() generated a OneDriveException", ["debug"]);} if ((exception.httpStatusCode == 400) || (exception.httpStatusCode == 401)) { // Handle the 400 | 401 error handleClientUnauthorised(exception.httpStatusCode, exception.error); } else { // Default operation if not 400,401 errors // - 408,429,503,504 errors are handled as a retry within getDefaultRootApiInstance // Display what the error is displayOneDriveErrorMessage(exception.msg, thisFunctionName); } } // If the JSON response is a correct JSON object, and has an 'id' we can set these details if ((defaultOneDriveRootDetails.type() == JSONType.object) && (hasId(defaultOneDriveRootDetails))) { // Read the returned JSON data for the root drive details if (debugLogging) {addLogEntry("OneDrive Account Default Root Details: " ~ to!string(defaultOneDriveRootDetails), ["debug"]);} appConfig.defaultRootId = defaultOneDriveRootDetails["id"].str; if (debugLogging) {addLogEntry("appConfig.defaultRootId = " ~ appConfig.defaultRootId, ["debug"]);} // Save the item to the database, so the account root drive is is always going to be present in the DB saveItem(defaultOneDriveRootDetails); } else { // was this a no-sync task? if (!noSyncTask) { // Handle the invalid JSON response by throwing an exception error throw new AccountDetailsException(); } } // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory getDefaultRootApiInstance.releaseCurlEngine(); getDefaultRootApiInstance = null; // Perform Garbage Collection GC.collect(); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Reset syncFailures to false based on file activity void resetSyncFailures() { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Log initial status and any non-empty arrays string logMessage = "Evaluating reset of syncFailures: "; if (fileDownloadFailures.length > 0) { logMessage ~= "fileDownloadFailures is not empty; "; } if (fileUploadFailures.length > 0) { logMessage ~= "fileUploadFailures is not empty; "; } // Check if both arrays are empty to reset syncFailures if (fileDownloadFailures.length == 0 && fileUploadFailures.length == 0) { if (syncFailures) { syncFailures = false; logMessage ~= "Resetting syncFailures to false."; } else { logMessage ~= "syncFailures already false."; } } else { // Indicate no reset of syncFailures due to non-empty conditions logMessage ~= "Not resetting syncFailures due to non-empty arrays."; } // Log the final decision and conditions if (debugLogging) {addLogEntry(logMessage, ["debug"]);} // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Perform a sync of the OneDrive Account // - Query /delta // - If singleDirectoryScope or nationalCloudDeployment is used we need to generate a /delta like response // - Process changes (add, changes, moves, deletes) // - Process any items to add (download data to local) // - Detail any files that we failed to download // - Process any deletes (remove local data) void syncOneDriveAccountToLocalDisk() { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // performFullScanTrueUp value if (debugLogging) {addLogEntry("Perform a Full Scan True-Up: " ~ to!string(appConfig.fullScanTrueUpRequired), ["debug"]);} // Fetch the API response of /delta to track changes that were performed online fetchOneDriveDeltaAPIResponse(); // Process any download activities or cleanup actions processDownloadActivities(); // If singleDirectoryScope is false, we are not targeting a single directory // but if true, the target 'could' be a shared folder - so dont try and scan it again if (!singleDirectoryScope) { // OneDrive Shared Folder Handling if (appConfig.accountType == "personal") { // Personal Account Type // https://github.com/OneDrive/onedrive-api-docs/issues/764 // Get the Remote Items from the Database Item[] remoteItems = itemDB.selectRemoteItems(); foreach (remoteItem; remoteItems) { // Check if this path is specifically excluded by 'skip_dir', but only if 'skip_dir' is not empty if (appConfig.getValueString("skip_dir") != "") { // The path that needs to be checked needs to include the '/' // This due to if the user has specified in skip_dir an exclusive path: '/path' - that is what must be matched if (selectiveSync.isDirNameExcluded(remoteItem.name)) { // This directory name is excluded if (verboseLogging) {addLogEntry("Skipping path - excluded by skip_dir config: " ~ remoteItem.name, ["verbose"]);} continue; } } // Directory name is not excluded or skip_dir is not populated if (!appConfig.suppressLoggingOutput) { // So that we represent correctly where this shared folder is, calculate the path string sharedFolderLogicalPath = computeItemPath(remoteItem.driveId, remoteItem.id); addLogEntry("Syncing this OneDrive Personal Shared Folder: " ~ ensureStartsWithDotSlash(sharedFolderLogicalPath)); } // Check this OneDrive Personal Shared Folder for changes fetchOneDriveDeltaAPIResponse(remoteItem.remoteDriveId, remoteItem.remoteId, remoteItem.name); // Process any download activities or cleanup actions for this OneDrive Personal Shared Folder processDownloadActivities(); } // Clear the array remoteItems = []; } else { // Is this a Business Account with Sync Business Shared Items enabled? if ((appConfig.accountType == "business") && (appConfig.getValueBool("sync_business_shared_items"))) { // Business Account Shared Items Handling // - OneDrive Business Shared Folder // - OneDrive Business Shared Files // - SharePoint Links // Get the Remote Items from the Database Item[] remoteItems = itemDB.selectRemoteItems(); foreach (remoteItem; remoteItems) { // As all remote items are returned, including files, we only want to process directories here if (remoteItem.remoteType == ItemType.dir) { // Check if this path is specifically excluded by 'skip_dir', but only if 'skip_dir' is not empty if (appConfig.getValueString("skip_dir") != "") { // The path that needs to be checked needs to include the '/' // This due to if the user has specified in skip_dir an exclusive path: '/path' - that is what must be matched if (selectiveSync.isDirNameExcluded(remoteItem.name)) { // This directory name is excluded if (verboseLogging) {addLogEntry("Skipping path - excluded by skip_dir config: " ~ remoteItem.name, ["verbose"]);} continue; } } // Directory name is not excluded or skip_dir is not populated if (!appConfig.suppressLoggingOutput) { // So that we represent correctly where this shared folder is, calculate the path string sharedFolderLogicalPath = computeItemPath(remoteItem.driveId, remoteItem.id); addLogEntry("Syncing this OneDrive Business Shared Folder: " ~ sharedFolderLogicalPath); } // Debug log output if (debugLogging) { addLogEntry("Fetching /delta API response for:", ["debug"]); addLogEntry(" remoteItem.remoteDriveId: " ~ remoteItem.remoteDriveId, ["debug"]); addLogEntry(" remoteItem.remoteId: " ~ remoteItem.remoteId, ["debug"]); } // Check this OneDrive Business Shared Folder for changes fetchOneDriveDeltaAPIResponse(remoteItem.remoteDriveId, remoteItem.remoteId, remoteItem.name); // Process any download activities or cleanup actions for this OneDrive Business Shared Folder processDownloadActivities(); } } // Clear the array remoteItems = []; // OneDrive Business Shared File Handling - but only if this option is enabled if (appConfig.getValueBool("sync_business_shared_files")) { // We need to create a 'new' local folder in the 'sync_dir' where these shared files & associated folder structure will reside // Whilst these files are synced locally, the entire folder structure will need to be excluded from syncing back to OneDrive // But file changes , *if any* , will need to be synced back to the original shared file location // . // ├── Files Shared With Me -> Directory should not be created online | Not Synced // │ └── Display Name (email address) (of Account who shared file) -> Directory should not be created online | Not Synced // │ │ └── shared file.ext -> File synced with original shared file location on remote drive // │ │ └── shared file.ext -> File synced with original shared file location on remote drive // │ │ └── ...... -> File synced with original shared file location on remote drive // │ └── Display Name (email address) ... // │ └── shared file.ext .... -> File synced with original shared file location on remote drive // Does the Local Folder to store the OneDrive Business Shared Files exist? if (!exists(appConfig.configuredBusinessSharedFilesDirectoryName)) { // Folder does not exist locally and needs to be created addLogEntry("Creating the OneDrive Business Shared Files Local Directory: " ~ appConfig.configuredBusinessSharedFilesDirectoryName); // Local folder does not exist, thus needs to be created mkdirRecurse(appConfig.configuredBusinessSharedFilesDirectoryName); // As this will not be created online, generate a response so it can be saved to the database Item sharedFilesPath = makeItem(createFakeResponse(baseName(appConfig.configuredBusinessSharedFilesDirectoryName))); // Add DB record to the local database if (debugLogging) {addLogEntry("Creating|Updating into local database a DB record for storing OneDrive Business Shared Files: " ~ to!string(sharedFilesPath), ["debug"]);} itemDB.upsert(sharedFilesPath); } else { // Folder exists locally, is the folder in the database? // Query DB for this path Item dbRecord; if (!itemDB.selectByPath(baseName(appConfig.configuredBusinessSharedFilesDirectoryName), appConfig.defaultDriveId, dbRecord)) { // As this will not be created online, generate a response so it can be saved to the database Item sharedFilesPath = makeItem(createFakeResponse(baseName(appConfig.configuredBusinessSharedFilesDirectoryName))); // Add DB record to the local database if (debugLogging) {addLogEntry("Creating|Updating into local database a DB record for storing OneDrive Business Shared Files: " ~ to!string(sharedFilesPath), ["debug"]);} itemDB.upsert(sharedFilesPath); } } // Query for OneDrive Business Shared Files if (verboseLogging) {addLogEntry("Checking for any applicable OneDrive Business Shared Files which need to be synced locally", ["verbose"]);} queryBusinessSharedObjects(); // Download any OneDrive Business Shared Files processDownloadActivities(); } } } } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Cleanup arrays when used in --monitor loops void cleanupArrays() { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Debug what we are doing if (debugLogging) {addLogEntry("Cleaning up all internal arrays used when processing data", ["debug"]);} // Multi Dimensional Arrays idsToDelete.length = 0; idsFaked.length = 0; databaseItemsWhereContentHasChanged.length = 0; // JSON Items Arrays jsonItemsToProcess = []; fileJSONItemsToDownload = []; jsonItemsToResumeUpload = []; // String Arrays fileDownloadFailures = []; pathFakeDeletedArray = []; pathsRenamed = []; newLocalFilesToUploadToOneDrive = []; fileUploadFailures = []; posixViolationPaths = []; businessSharedFoldersOnlineToSkip = []; interruptedUploadsSessionFiles = []; pathsToCreateOnline = []; databaseItemsToDeleteOnline = []; // Perform Garbage Collection on this destroyed curl engine GC.collect(); if (debugLogging) {addLogEntry("Cleaning of internal arrays complete", ["debug"]);} // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Configure singleDirectoryScope = true if this function is called // By default, singleDirectoryScope = false void setSingleDirectoryScope(string normalisedSingleDirectoryPath) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Function variables Item searchItem; JSONValue onlinePathData; // Set the main flag singleDirectoryScope = true; // What are we doing? addLogEntry("The OneDrive Client was asked to search for this directory online and create it if it's not located: " ~ normalisedSingleDirectoryPath); // Query the OneDrive API for the specified path online // In a --single-directory scenario, we need to traverse the entire path that we are wanting to sync // and then check the path element does it exist online, if it does, is it a POSIX match, or if it does not, create the path // Once we have searched online, we have the right drive id and item id so that we can downgrade the sync status, then build up // any object items from that location // This is because, in a --single-directory scenario, any folder in the entire path tree could be a 'case-insensitive match' try { onlinePathData = queryOneDriveForSpecificPathAndCreateIfMissing(normalisedSingleDirectoryPath, true); } catch (PosixException e) { displayPosixErrorMessage(e.msg); addLogEntry("ERROR: Requested directory to search for and potentially create has a 'case-insensitive match' to an existing directory on Microsoft OneDrive online."); } // Was a valid JSON response provided? if (onlinePathData.type() == JSONType.object) { // Valid JSON item was returned searchItem = makeItem(onlinePathData); if (debugLogging) {addLogEntry("searchItem: " ~ to!string(searchItem), ["debug"]);} // Is this item a potential Shared Folder? // Is this JSON a remote object if (isItemRemote(onlinePathData)) { // Is this a Personal Account Type or has 'sync_business_shared_items' been enabled? if ((appConfig.accountType == "personal") || (appConfig.getValueBool("sync_business_shared_items"))) { // The path we are seeking is remote to our account drive id searchItem.driveId = onlinePathData["remoteItem"]["parentReference"]["driveId"].str; searchItem.id = onlinePathData["remoteItem"]["id"].str; // Issue #3115 - Validate driveId length // What account type is this? if (appConfig.accountType == "personal") { // Test driveId length and validation if the driveId we are testing is not equal to appConfig.defaultDriveId if (searchItem.driveId != appConfig.defaultDriveId) { searchItem.driveId = testProvidedDriveIdForLengthIssue(searchItem.driveId); } } // Create a 'root' and 'Shared Folder' DB Tie Records for this JSON object in a consistent manner createRequiredSharedFolderDatabaseRecords(onlinePathData); } else { // This is a shared folder location, but we are not a 'personal' account, and 'sync_business_shared_items' has not been enabled addLogEntry(); addLogEntry("ERROR: The requested --single-directory path to sync is a Shared Folder online and 'sync_business_shared_items' is not enabled"); addLogEntry(); forceExit(); } } // Set these items so that these can be used as required singleDirectoryScopeDriveId = searchItem.driveId; singleDirectoryScopeItemId = searchItem.id; } else { addLogEntry(); addLogEntry("ERROR: The requested --single-directory path to sync has generated an error. Please correct this error and try again."); addLogEntry(); forceExit(); } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Query OneDrive API for /delta changes and iterate through items online void fetchOneDriveDeltaAPIResponse(string driveIdToQuery = null, string itemIdToQuery = null, string sharedFolderName = null) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } string deltaLink = null; string currentDeltaLink = null; string databaseDeltaLink; JSONValue deltaChanges; long responseBundleCount; long jsonItemsReceived = 0; // Reset jsonItemsToProcess & processedCount jsonItemsToProcess = []; processedCount = 0; // Reset generateSimulatedDeltaResponse generateSimulatedDeltaResponse = false; // Reset Shared Folder Flags for 'sync_list' processing sharedFolderDeltaGeneration = false; currentSharedFolderName = ""; // Was a driveId provided as an input if (strip(driveIdToQuery).empty) { // No provided driveId to query, use the account default driveIdToQuery = appConfig.defaultDriveId; if (debugLogging) { addLogEntry("driveIdToQuery was empty, setting to appConfig.defaultDriveId", ["debug"]); addLogEntry("driveIdToQuery: " ~ driveIdToQuery, ["debug"]); } } // Was an itemId provided as an input if (strip(itemIdToQuery).empty) { // No provided itemId to query, use the account default itemIdToQuery = appConfig.defaultRootId; if (debugLogging) { addLogEntry("itemIdToQuery was empty, setting to appConfig.defaultRootId", ["debug"]); addLogEntry("itemIdToQuery: " ~ itemIdToQuery, ["debug"]); } } // What OneDrive API query do we use? // - Are we running against a National Cloud Deployments that does not support /delta ? // National Cloud Deployments do not support /delta as a query // https://docs.microsoft.com/en-us/graph/deployments#supported-features // // - Are we performing a --single-directory sync, which will exclude many items online, focusing in on a specific online directory // // - Are we performing a --download-only --cleanup-local-files action? // - If we are, and we use a normal /delta query, we get all the local 'deleted' objects as well. // - If the user deletes a folder online, then replaces it online, we download the deletion events and process the new 'upload' via the web interface .. // the net effect of this, is that the valid local files we want to keep, are actually deleted ...... not desirable if ((singleDirectoryScope) || (nationalCloudDeployment) || (cleanupLocalFiles)) { // Generate a simulated /delta response so that we correctly capture the current online state, less any 'online' delete and replace activity generateSimulatedDeltaResponse = true; } // Shared Folders, by nature of where that path has been shared with us, we cannot use /delta against that path, as this queries the entire 'other persons' drive: // Syncing this OneDrive Business Shared Folder: Sub Folder 2 // Fetching /delta response from the OneDrive API for Drive ID: b!fZgJhK-pU0eTQpylvmoYCkE4YgH_KRNDlxjRx9OWNqmV9Q_E_uWdRJKIB5L_ruPN // Processing API Response Bundle: 1 - Quantity of 'changes|items' in this bundle to process: 18 // Skipping path - excluded by sync_list config: Sub Folder Share/Sub Folder 1/Sub Folder 2 // // When using 'sync_list' potentially nothing is going to match, as, we are getting the 'whole' path from their 'root' , not just the folder shared with us if (!sharedFolderName.empty) { // When using 'sync_list' we need to do this sharedFolderDeltaGeneration = true; currentSharedFolderName = sharedFolderName; generateSimulatedDeltaResponse = true; } // Reset latestDeltaLink & deltaLinkCache latestDeltaLink = null; deltaLinkCache.driveId = null; deltaLinkCache.itemId = null; deltaLinkCache.latestDeltaLink = null; // Perform Garbage Collection GC.collect(); // What /delta query do we use? if (!generateSimulatedDeltaResponse) { // This should be the majority default pathway application use // Do we need to perform a Full Scan True Up? Is 'appConfig.fullScanTrueUpRequired' set to 'true'? if (appConfig.fullScanTrueUpRequired) { addLogEntry("Performing a full scan of online data to ensure consistent local state"); if (debugLogging) {addLogEntry("Setting currentDeltaLink = null", ["debug"]);} currentDeltaLink = null; } else { // Try and get the current Delta Link from the internal cache, this saves a DB I/O call currentDeltaLink = getDeltaLinkFromCache(deltaLinkInfo, driveIdToQuery); // Is currentDeltaLink empty (no cached entry found) ? if (currentDeltaLink.empty) { // Try and get the current delta link from the database for this DriveID and RootID databaseDeltaLink = itemDB.getDeltaLink(driveIdToQuery, itemIdToQuery); if (!databaseDeltaLink.empty) { if (debugLogging) {addLogEntry("Using database stored deltaLink", ["debug"]);} currentDeltaLink = databaseDeltaLink; } else { if (debugLogging) {addLogEntry("Zero deltaLink available for use, we will be performing a full online scan", ["debug"]);} currentDeltaLink = null; } } else { // Log that we are using the deltaLink for cache if (debugLogging) {addLogEntry("Using cached deltaLink", ["debug"]);} } } // Dynamic output for non-verbose and verbose run so that the user knows something is being retrieved from the OneDrive API if (appConfig.verbosityCount == 0) { if (!appConfig.suppressLoggingOutput) { addProcessingLogHeaderEntry("Fetching items from the OneDrive API for Drive ID: " ~ driveIdToQuery, appConfig.verbosityCount); } } else { if (verboseLogging) {addLogEntry("Fetching /delta response from the OneDrive API for Drive ID: " ~ driveIdToQuery, ["verbose"]);} } // Create a new API Instance for querying the actual /delta and initialise it OneDriveApi getDeltaDataOneDriveApiInstance; getDeltaDataOneDriveApiInstance = new OneDriveApi(appConfig); getDeltaDataOneDriveApiInstance.initialise(); // Get the /delta changes via the OneDrive API while (true) { // Check if exitHandlerTriggered is true if (exitHandlerTriggered) { // break out of the 'while (true)' loop break; } // Increment responseBundleCount responseBundleCount++; // Ensure deltaChanges is empty before we query /delta deltaChanges = null; // Perform Garbage Collection GC.collect(); // getDeltaChangesByItemId has the re-try logic for transient errors deltaChanges = getDeltaChangesByItemId(driveIdToQuery, itemIdToQuery, currentDeltaLink, getDeltaDataOneDriveApiInstance); // If the initial deltaChanges response is an invalid JSON object, keep trying until we get a valid response .. if (deltaChanges.type() != JSONType.object) { // While the response is not a JSON Object or the Exit Handler has not been triggered while (deltaChanges.type() != JSONType.object) { // Check if exitHandlerTriggered is true if (exitHandlerTriggered) { // break out of the 'while (true)' loop break; } // Handle the invalid JSON response and retry if (debugLogging) {addLogEntry("ERROR: Query of the OneDrive API via deltaChanges = getDeltaChangesByItemId() returned an invalid JSON response", ["debug"]);} deltaChanges = getDeltaChangesByItemId(driveIdToQuery, itemIdToQuery, currentDeltaLink, getDeltaDataOneDriveApiInstance); } } long nrChanges = count(deltaChanges["value"].array); int changeCount = 0; if (appConfig.verbosityCount == 0) { // Dynamic output for a non-verbose run so that the user knows something is happening if (!appConfig.suppressLoggingOutput) { addProcessingDotEntry(); } } else { if (verboseLogging) {addLogEntry("Processing API Response Bundle: " ~ to!string(responseBundleCount) ~ " - Quantity of 'changes|items' in this bundle to process: " ~ to!string(nrChanges), ["verbose"]);} } // Update the count of items received jsonItemsReceived = jsonItemsReceived + nrChanges; // The 'deltaChanges' response may contain either @odata.nextLink or @odata.deltaLink // Check for @odata.nextLink if ("@odata.nextLink" in deltaChanges) { // @odata.nextLink is the pointer within the API to the next '200+' JSON bundle - this is the checkpoint link for this bundle // This URL changes between JSON bundle sets // Log the action of setting currentDeltaLink to @odata.nextLink if (debugLogging) {addLogEntry("Setting currentDeltaLink to @odata.nextLink: " ~ deltaChanges["@odata.nextLink"].str, ["debug"]);} // Update currentDeltaLink to @odata.nextLink for the next '200+' JSON bundle - this is the checkpoint link for this bundle currentDeltaLink = deltaChanges["@odata.nextLink"].str; } // Check for @odata.deltaLink - usually only in the LAST JSON changeset bundle if ("@odata.deltaLink" in deltaChanges) { // @odata.deltaLink is the pointer that finalises all the online 'changes' for this particular checkpoint // When the API is queried again, this is fetched from the DB as this is the starting point // The API issue here is - the LAST JSON bundle will ONLY ever contain this item, meaning if this is then committed to the database // if there has been any file download failures from within this LAST JSON bundle, the only way to EVER re-try the failed items is for the user to perform a --resync // This is an API capability gap: // // .. // @odata.nextLink: https://graph.microsoft.com/v1.0/drives//items//delta?token= // Processing API Response Bundle: 115 - Quantity of 'changes|items' in this bundle to process: 204 // .. // @odata.nextLink: https://graph.microsoft.com/v1.0/drives//items//delta?token= // Processing API Response Bundle: 127 - Quantity of 'changes|items' in this bundle to process: 204 // @odata.nextLink: https://graph.microsoft.com/v1.0/drives//items//delta?token= // Processing API Response Bundle: 128 - Quantity of 'changes|items' in this bundle to process: 176 // @odata.deltaLink: https://graph.microsoft.com/v1.0/drives//items//delta?token= // Finished processing /delta JSON response from the OneDrive API // Log the action of setting currentDeltaLink to @odata.deltaLink if (debugLogging) {addLogEntry("Setting currentDeltaLink to (@odata.deltaLink): " ~ deltaChanges["@odata.deltaLink"].str, ["debug"]);} // Update currentDeltaLink to @odata.deltaLink as the final checkpoint URL for this entire JSON response set currentDeltaLink = deltaChanges["@odata.deltaLink"].str; // Store this currentDeltaLink as latestDeltaLink latestDeltaLink = deltaChanges["@odata.deltaLink"].str; // Issue #3115 - Validate driveId length // What account type is this? if (appConfig.accountType == "personal") { // Test driveId length and validation if the driveId we are testing is not equal to appConfig.defaultDriveId if (driveIdToQuery != appConfig.defaultDriveId) { driveIdToQuery = testProvidedDriveIdForLengthIssue(driveIdToQuery); } } // Update deltaLinkCache deltaLinkCache.driveId = driveIdToQuery; deltaLinkCache.itemId = itemIdToQuery; deltaLinkCache.latestDeltaLink = currentDeltaLink; } // We have a valid deltaChanges JSON array. This means we have at least 200+ JSON items to process. // The API response however cannot be run in parallel as the OneDrive API sends the JSON items in the order in which they must be processed auto jsonArrayToProcess = deltaChanges["value"].array; // To allow for better debugging, what are all the JSON elements in the array the API responded with in this set? if (count(jsonArrayToProcess) > 0) { if (debugLogging) { string debugLogHeader = format("=============================== jsonArrayToProcess - response bundle %s ===================================", to!string(responseBundleCount)); addLogEntry(debugLogHeader, ["debug"]); addLogEntry(to!string(jsonArrayToProcess), ["debug"]); addLogEntry(debugLogBreakType2, ["debug"]); } } // Process the change set foreach (onedriveJSONItem; jsonArrayToProcess) { // increment change count for this item changeCount++; // Process the received OneDrive object item JSON for this JSON bundle // This will determine its initial applicability and perform some initial processing on the JSON if required processDeltaJSONItem(onedriveJSONItem, nrChanges, changeCount, responseBundleCount, singleDirectoryScope); } // Clear up this data jsonArrayToProcess = null; // Perform Garbage Collection GC.collect(); // Is latestDeltaLink matching deltaChanges["@odata.deltaLink"].str ? if ("@odata.deltaLink" in deltaChanges) { if (latestDeltaLink == deltaChanges["@odata.deltaLink"].str) { // break out of the 'while (true)' loop break; } } // Cleanup deltaChanges as this is no longer needed deltaChanges = null; // Perform Garbage Collection GC.collect(); // Sleep for a while to avoid busy-waiting Thread.sleep(dur!"msecs"(100)); // Adjust the sleep duration as needed } // Terminate getDeltaDataOneDriveApiInstance here getDeltaDataOneDriveApiInstance.releaseCurlEngine(); getDeltaDataOneDriveApiInstance = null; // Perform Garbage Collection on this destroyed curl engine GC.collect(); // To finish off the JSON processing items, this is needed to reflect this in the log if (debugLogging) {addLogEntry(debugLogBreakType1, ["debug"]);} // Log that we have finished querying the /delta API if (appConfig.verbosityCount == 0) { if (!appConfig.suppressLoggingOutput) { // Close out the '....' being printed to the console completeProcessingDots(); } } else { if (verboseLogging) {addLogEntry("Finished processing /delta JSON response from the OneDrive API", ["verbose"]);} } // If this was set, now unset it, as this will have been completed, so that for a true up, we dont do a double full scan if (appConfig.fullScanTrueUpRequired) { if (debugLogging) {addLogEntry("Unsetting fullScanTrueUpRequired as this has been performed", ["debug"]);} appConfig.fullScanTrueUpRequired = false; } // Cleanup deltaChanges as this is no longer needed deltaChanges = null; // Perform Garbage Collection GC.collect(); } else { // Why are we generating a /delta response if (debugLogging) { addLogEntry("Why are we generating a /delta response:", ["debug"]); addLogEntry(" singleDirectoryScope: " ~ to!string(singleDirectoryScope), ["debug"]); addLogEntry(" nationalCloudDeployment: " ~ to!string(nationalCloudDeployment), ["debug"]); addLogEntry(" cleanupLocalFiles: " ~ to!string(cleanupLocalFiles), ["debug"]); addLogEntry(" sharedFolderName: " ~ sharedFolderName, ["debug"]); } // What 'path' are we going to start generating the response for string pathToQuery; // If --single-directory has been called, use the value that has been set if (singleDirectoryScope) { pathToQuery = appConfig.getValueString("single_directory"); } // We could also be syncing a Shared Folder of some description - is this empty? if (!sharedFolderName.empty) { // We need to build 'pathToQuery' to support Shared Folders being anywhere in the directory structure (#2824) // Is the itemIdToQuery in the database? If this is not there, we cannot build the path if (itemDB.idInLocalDatabase(driveIdToQuery, itemIdToQuery)) { // The entries are in our DB, but we need to use our Drive details to compute the actual local path the the point of the 'remote' record and DB Tie Record Item remoteEntryItem; itemDB.selectByRemoteEntryByName(sharedFolderName, remoteEntryItem); // Use the 'remote' item type DB entry to calculate the local path of this item, which then will match the path online for this Shared Folder string computedLocalPathToQuery = computeItemPath(remoteEntryItem.driveId, remoteEntryItem.id); // If we have a computed path, use it, else use 'sharedFolderName' if (!computedLocalPathToQuery.empty) { // computedLocalPathToQuery is not empty pathToQuery = computedLocalPathToQuery; } else { // computedLocalPathToQuery is empty pathToQuery = sharedFolderName; } } else { // shared folder details are not even in the database ... fall back to this pathToQuery = sharedFolderName; } // At this point we have either calculated the shared folder path, or not and can attempt to generate a /delta response from that path entry online } // Generate the simulated /delta response // // The generated /delta response however contains zero deleted JSON items, so the only way that we can track this, is if the object was in sync // we have the object in the database, thus, what we need to do is for every DB object in the tree of items, flag 'syncStatus' as 'N', then when we process // the returned JSON items from the API, we flag the item as back in sync, then we can cleanup any out-of-sync items // // The flagging of the local database items to 'N' is handled within the generateDeltaResponse() function // // When these JSON items are then processed, if the item exists online, and is in the DB, and that the values match, the DB item is flipped back to 'Y' // This then allows the application to look for any remaining 'N' values, and delete these as no longer needed locally deltaChanges = generateDeltaResponse(pathToQuery); // deltaChanges must be a valid JSON object / array of data if (deltaChanges.type() == JSONType.object) { // How many changes were returned? long nrChanges = count(deltaChanges["value"].array); int changeCount = 0; if (debugLogging) {addLogEntry("API Response Bundle: " ~ to!string(responseBundleCount) ~ " - Quantity of 'changes|items' in this bundle to process: " ~ to!string(nrChanges), ["debug"]);} // Update the count of items received jsonItemsReceived = jsonItemsReceived + nrChanges; // The API response however cannot be run in parallel as the OneDrive API sends the JSON items in the order in which they must be processed auto jsonArrayToProcess = deltaChanges["value"].array; foreach (onedriveJSONItem; deltaChanges["value"].array) { // increment change count for this item changeCount++; // Process the received OneDrive object item JSON for this JSON bundle // When we generate a /delta response .. there is no currentDeltaLink value processDeltaJSONItem(onedriveJSONItem, nrChanges, changeCount, responseBundleCount, singleDirectoryScope); } // Clear up this data jsonArrayToProcess = null; // To finish off the JSON processing items, this is needed to reflect this in the log if (debugLogging) {addLogEntry(debugLogBreakType1, ["debug"]);} // Log that we have finished generating our self generated /delta response if (!appConfig.suppressLoggingOutput) { addLogEntry("Finished processing self generated /delta JSON response from the OneDrive API"); } } // Cleanup deltaChanges as this is no longer needed deltaChanges = null; // Perform Garbage Collection GC.collect(); } // Cleanup deltaChanges as this is no longer needed deltaChanges = null; // Perform Garbage Collection GC.collect(); // We have JSON items received from the OneDrive API if (debugLogging) { addLogEntry("Number of JSON Objects received from OneDrive API: " ~ to!string(jsonItemsReceived), ["debug"]); addLogEntry("Number of JSON Objects already processed (root and deleted items): " ~ to!string((jsonItemsReceived - jsonItemsToProcess.length)), ["debug"]); // We should have now at least processed all the JSON items as returned by the /delta call // Additionally, we should have a new array, that now contains all the JSON items we need to process that are non 'root' or deleted items addLogEntry("Number of JSON items submitted for further processing is: " ~ to!string(jsonItemsToProcess.length), ["debug"]); } // Are there items to process? if (jsonItemsToProcess.length > 0) { // Lets deal with the JSON items in a batch process size_t batchSize = 500; long batchCount = (jsonItemsToProcess.length + batchSize - 1) / batchSize; long batchesProcessed = 0; // Dynamic output for a non-verbose run so that the user knows something is happening if (!appConfig.suppressLoggingOutput) { addProcessingLogHeaderEntry("Processing " ~ to!string(jsonItemsToProcess.length) ~ " applicable JSON items received from Microsoft OneDrive", appConfig.verbosityCount); } // For each batch, process the JSON items that need to be now processed. // 'root' and deleted objects have already been handled foreach (batchOfJSONItems; jsonItemsToProcess.chunks(batchSize)) { // Chunk the total items to process into 500 lot items batchesProcessed++; if (appConfig.verbosityCount == 0) { // Dynamic output for a non-verbose run so that the user knows something is happening if (!appConfig.suppressLoggingOutput) { addProcessingDotEntry(); } } else { if (verboseLogging) {addLogEntry("Processing OneDrive JSON item batch [" ~ to!string(batchesProcessed) ~ "/" ~ to!string(batchCount) ~ "] to ensure consistent local state", ["verbose"]);} } // Process the batch processJSONItemsInBatch(batchOfJSONItems, batchesProcessed, batchCount); // To finish off the JSON processing items, this is needed to reflect this in the log if (debugLogging) {addLogEntry(debugLogBreakType1, ["debug"]);} // For this set of items, perform a DB PASSIVE checkpoint itemDB.performCheckpoint("PASSIVE"); } if (appConfig.verbosityCount == 0) { // close off '.' output if (!appConfig.suppressLoggingOutput) { // Close out the '....' being printed to the console completeProcessingDots(); } } // Debug output - what was processed if (debugLogging) { addLogEntry("Number of JSON items to process is: " ~ to!string(jsonItemsToProcess.length), ["debug"]); addLogEntry("Number of JSON items processed was: " ~ to!string(processedCount), ["debug"]); } // Free up memory and items processed as it is pointless now having this data around jsonItemsToProcess = []; // Perform Garbage Collection on this destroyed curl engine GC.collect(); } else { if (!appConfig.suppressLoggingOutput) { addLogEntry("No changes or items that can be applied were discovered while processing the data received from Microsoft OneDrive"); } } // Keep the DriveDetailsCache array with unique entries only DriveDetailsCache cachedOnlineDriveData; if (!canFindDriveId(driveIdToQuery, cachedOnlineDriveData)) { // Add this driveId to the drive cache addOrUpdateOneDriveOnlineDetails(driveIdToQuery); } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Process the /delta API JSON response items void processDeltaJSONItem(JSONValue onedriveJSONItem, long nrChanges, int changeCount, long responseBundleCount, bool singleDirectoryScope) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Variables for this JSON item string thisItemId; bool itemIsRoot = false; bool handleItemAsRootObject = false; bool itemIsDeletedOnline = false; bool itemHasParentReferenceId = false; bool itemHasParentReferencePath = false; bool itemIdMatchesDefaultRootId = false; bool itemNameExplicitMatchRoot = false; bool itemIsRemoteItem = false; string objectParentDriveId; MonoTime jsonProcessingStartTime; // Debugging the processing start of the JSON item if (debugLogging) { addLogEntry(debugLogBreakType1, ["debug"]); jsonProcessingStartTime = MonoTime.currTime(); addLogEntry("Processing OneDrive Item " ~ to!string(changeCount) ~ " of " ~ to!string(nrChanges) ~ " from API Response Bundle " ~ to!string(responseBundleCount), ["debug"]); addLogEntry("Raw JSON OneDrive Item: " ~ sanitiseJSONItem(onedriveJSONItem), ["debug"]); } // What is this item's id thisItemId = onedriveJSONItem["id"].str; // Is this a deleted item - only calculate this once itemIsDeletedOnline = isItemDeleted(onedriveJSONItem); if (!itemIsDeletedOnline) { // This is not a deleted item if (debugLogging) {addLogEntry("This item is not a OneDrive online deletion change", ["debug"]);} // Only calculate this once itemIsRoot = isItemRoot(onedriveJSONItem); itemHasParentReferenceId = hasParentReferenceId(onedriveJSONItem); itemIdMatchesDefaultRootId = (thisItemId == appConfig.defaultRootId); itemNameExplicitMatchRoot = (onedriveJSONItem["name"].str == "root"); objectParentDriveId = onedriveJSONItem["parentReference"]["driveId"].str; itemIsRemoteItem = isItemRemote(onedriveJSONItem); // Test is this is the OneDrive Users Root? // Debug output of change evaluation items if (debugLogging) { addLogEntry("defaultRootId = " ~ appConfig.defaultRootId, ["debug"]); addLogEntry("thisItemName = " ~ onedriveJSONItem["name"].str, ["debug"]); addLogEntry("thisItemId = " ~ thisItemId, ["debug"]); addLogEntry("thisItemId == defaultRootId = " ~ to!string(itemIdMatchesDefaultRootId), ["debug"]); addLogEntry("isItemRoot(onedriveJSONItem) = " ~ to!string(itemIsRoot), ["debug"]); addLogEntry("onedriveJSONItem['name'].str == 'root' = " ~ to!string(itemNameExplicitMatchRoot), ["debug"]); addLogEntry("itemHasParentReferenceId = " ~ to!string(itemHasParentReferenceId), ["debug"]); addLogEntry("itemIsRemoteItem = " ~ to!string(itemIsRemoteItem), ["debug"]); } if ( (itemIdMatchesDefaultRootId || singleDirectoryScope) && itemIsRoot && itemNameExplicitMatchRoot) { // This IS a OneDrive Root item or should be classified as such in the case of 'singleDirectoryScope' if (debugLogging) {addLogEntry("JSON item will flagged as a 'root' item", ["debug"]);} handleItemAsRootObject = true; } } // How do we handle this JSON item from the OneDrive API? // Is this a confirmed 'root' item, has no Parent ID, or is a Deleted Item if (handleItemAsRootObject || !itemHasParentReferenceId || itemIsDeletedOnline){ // Is a root item, has no id in parentReference or is a OneDrive deleted item if (debugLogging) { addLogEntry("objectParentDriveId = " ~ objectParentDriveId, ["debug"]); addLogEntry("handleItemAsRootObject = " ~ to!string(handleItemAsRootObject), ["debug"]); addLogEntry("itemHasParentReferenceId = " ~ to!string(itemHasParentReferenceId), ["debug"]); addLogEntry("itemIsDeletedOnline = " ~ to!string(itemIsDeletedOnline), ["debug"]); addLogEntry("Handling change immediately as 'root item', or has no parent reference id or is a deleted item", ["debug"]); } // OK ... do something with this JSON post here .... processRootAndDeletedJSONItems(onedriveJSONItem, objectParentDriveId, handleItemAsRootObject, itemIsDeletedOnline, itemHasParentReferenceId); } else { // Do we need to update this RAW JSON from OneDrive? if ( (objectParentDriveId != appConfig.defaultDriveId) && (appConfig.accountType == "business") && (appConfig.getValueBool("sync_business_shared_items")) ) { // Potentially need to update this JSON data if (debugLogging) {addLogEntry("Potentially need to update this source JSON .... need to check the database", ["debug"]);} // Check the DB for 'remote' objects, searching 'remoteDriveId' and 'remoteId' items for this remoteItem.driveId and remoteItem.id Item remoteDBItem; itemDB.selectByRemoteId(objectParentDriveId, thisItemId, remoteDBItem); // Is the data that was returned from the database what we are looking for? if ((remoteDBItem.remoteDriveId == objectParentDriveId) && (remoteDBItem.remoteId == thisItemId)) { // Yes, this is the record we are looking for if (debugLogging) {addLogEntry("DB Item response for remoteDBItem: " ~ to!string(remoteDBItem), ["debug"]);} // Must compare remoteDBItem.name with remoteItem.name if (remoteDBItem.name != onedriveJSONItem["name"].str) { // Update JSON Item string actualOnlineName = onedriveJSONItem["name"].str; if (debugLogging) { addLogEntry("Updating source JSON 'name' to that which is the actual local directory", ["debug"]); addLogEntry("onedriveJSONItem['name'] was: " ~ onedriveJSONItem["name"].str, ["debug"]); addLogEntry("Updating onedriveJSONItem['name'] to: " ~ remoteDBItem.name, ["debug"]); } onedriveJSONItem["name"] = remoteDBItem.name; if (debugLogging) {addLogEntry("onedriveJSONItem['name'] now: " ~ onedriveJSONItem["name"].str, ["debug"]);} // Add the original name to the JSON onedriveJSONItem["actualOnlineName"] = actualOnlineName; } } } // Do we discard this JSON item? bool discardDeltaJSONItem = false; // Microsoft OneNote container objects present neither folder or file but has file size if ((!isItemFile(onedriveJSONItem)) && (!isItemFolder(onedriveJSONItem)) && (hasFileSize(onedriveJSONItem))) { // This JSON: // - Is not a file // - Is not a folder // - Has a 'size' element // Online Shared Folder Shortcuts can match the same criteria - we need to ensure this is not a Online Shared Folder Shortcuts if (!itemIsRemoteItem) { // Not a pointer to a remote item, thus high confidence this is not a shared folder link // Log that this was skipped as this was a Microsoft OneNote item and unsupported if (verboseLogging) {addLogEntry("Skipping path - The Microsoft OneNote Notebook '" ~ generatePathFromJSONData(onedriveJSONItem) ~ "' is not supported by this client", ["verbose"]);} discardDeltaJSONItem = true; } } // Microsoft OneDrive OneNote file objects will report as files but have 'application/msonenote' or 'application/octet-stream' as their mime type and will not have any hash entry // Is there a 'file' JSON element and it has a 'mimeType' element? if (isItemFile(onedriveJSONItem) && hasMimeType(onedriveJSONItem)) { // and the mimeType is 'application/msonenote' or 'application/octet-stream' // However there is API inconsistency here between Personal and Business Accounts // Personal OneNote .onetoc2 and .one items all report mimeType as 'application/msonenote' // Business OneNote .onetoc2 and .one items however are different: // .onetoc2 = 'application/octet-stream' mimeType // .one = 'application/msonenote' mimeType if (isMicrosoftOneNoteMimeType1(onedriveJSONItem) || isMicrosoftOneNoteMimeType2(onedriveJSONItem)) { // and must not have any hash // Personal Accounts hashes are 100% missing // Business Accounts hashes exist, but JSON array is empty if ((!hasHashes(onedriveJSONItem)) || (hasZeroHashes(onedriveJSONItem))) { // extreme confidence these are Microsoft OneNote file references which cannot be supported // Log that this was skipped as this was a Microsoft OneNote item and unsupported if (verboseLogging) {addLogEntry("Skipping path - The Microsoft OneNote Notebook File '" ~ generatePathFromJSONData(onedriveJSONItem) ~ "' is not supported by this client", ["verbose"]);} discardDeltaJSONItem = true; } } } // If we are not self-generating a /delta response, check this initial /delta JSON bundle item against the basic checks // of applicability against 'skip_file', 'skip_dir' and 'sync_list' // We only do this if we did not generate a /delta response, as generateDeltaResponse() performs the checkJSONAgainstClientSideFiltering() // against elements as it is building the /delta compatible response // If we blindly just 'check again' all JSON responses then there is potentially double JSON processing going on if we used generateDeltaResponse() if (!generateSimulatedDeltaResponse) { // Did we already exclude? if (!discardDeltaJSONItem) { // Check applicability against 'skip_file', 'skip_dir' and 'sync_list' discardDeltaJSONItem = checkJSONAgainstClientSideFiltering(onedriveJSONItem); } } // Add this JSON item for further processing if this is not being discarded if (!discardDeltaJSONItem) { // Add onedriveJSONItem to jsonItemsToProcess if (debugLogging) { addLogEntry("Adding this Raw JSON OneDrive Item to jsonItemsToProcess array for further processing", ["debug"]); if (itemIsRemoteItem) { addLogEntry("- This JSON record represents a online remote folder, thus needs special handling when being processed further", ["debug"]); } } jsonItemsToProcess ~= onedriveJSONItem; } else { // detail we are discarding the json if (debugLogging) {addLogEntry("Discarding this Raw JSON OneDrive Item as this has been determined to be unwanted", ["debug"]);} } } // How long to initially process this JSON item if (debugLogging) { Duration jsonProcessingElapsedTime = MonoTime.currTime() - jsonProcessingStartTime; addLogEntry("Initial JSON item processing time: " ~ to!string(jsonProcessingElapsedTime), ["debug"]); } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Process 'root' and 'deleted' OneDrive JSON items void processRootAndDeletedJSONItems(JSONValue onedriveJSONItem, string driveId, bool handleItemAsRootObject, bool itemIsDeletedOnline, bool itemHasParentReferenceId) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Use the JSON elements rather can computing a DB struct via makeItem() string thisItemId = onedriveJSONItem["id"].str; string thisItemDriveId = onedriveJSONItem["parentReference"]["driveId"].str; // Check if the item has been seen before Item existingDatabaseItem; bool existingDBEntry = itemDB.selectById(thisItemDriveId, thisItemId, existingDatabaseItem); // Is the item deleted online? if(!itemIsDeletedOnline) { // Is the item a confirmed root object? // The JSON item should be considered a 'root' item if: // 1. Contains a ["root"] element // 2. Has no ["parentReference"]["id"] ... #323 & #324 highlighted that this is false as some 'root' shared objects now can have an 'id' element .. OneDrive API change // 2. Has no ["parentReference"]["path"] // 3. Was detected by an input flag as to be handled as a root item regardless of actual status if ((handleItemAsRootObject) || (!itemHasParentReferenceId)) { if (debugLogging) {addLogEntry("Handing JSON object as OneDrive 'root' object", ["debug"]);} if (!existingDBEntry) { // we have not seen this item before saveItem(onedriveJSONItem); } } } else { // Change is to delete an item if (debugLogging) {addLogEntry("Handing a OneDrive Deleted Item", ["debug"]);} if (existingDBEntry) { // Is the item to delete locally actually in sync with OneDrive currently? // What is the source of this item data? string itemSource = "online"; // Compute this deleted items path based on the database entries string localPathToDelete = computeItemPath(existingDatabaseItem.driveId, existingDatabaseItem.parentId) ~ "/" ~ existingDatabaseItem.name; if (isItemSynced(existingDatabaseItem, localPathToDelete, itemSource)) { // Flag to delete if (debugLogging) {addLogEntry("Flagging to delete item locally: " ~ to!string(onedriveJSONItem), ["debug"]);} idsToDelete ~= [thisItemDriveId, thisItemId]; } else { // If local data protection is configured (bypassDataPreservation = false), safeBackup the local file, passing in if we are performing a --dry-run or not // In case the renamed path is needed string renamedPath; safeBackup(localPathToDelete, dryRun, bypassDataPreservation, renamedPath); } } else { // Flag to ignore if (debugLogging) {addLogEntry("Flagging item to skip: " ~ to!string(onedriveJSONItem), ["debug"]);} skippedItems.insert(thisItemId); } } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Process each of the elements contained in jsonItemsToProcess[] void processJSONItemsInBatch(JSONValue[] array, long batchGroup, long batchCount) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } long batchElementCount = array.length; MonoTime jsonProcessingStartTime; foreach (i, onedriveJSONItem; array.enumerate) { // Use the JSON elements rather can computing a DB struct via makeItem() long elementCount = i +1; jsonProcessingStartTime = MonoTime.currTime(); // To show this is the processing for this particular item, start off with this breaker line if (debugLogging) { addLogEntry(debugLogBreakType1, ["debug"]); addLogEntry("Processing OneDrive JSON item " ~ to!string(elementCount) ~ " of " ~ to!string(batchElementCount) ~ " as part of JSON Item Batch " ~ to!string(batchGroup) ~ " of " ~ to!string(batchCount), ["debug"]); addLogEntry("Raw JSON OneDrive Item (Batched Item): " ~ to!string(onedriveJSONItem), ["debug"]); } // Configure required items from the JSON elements string thisItemId = onedriveJSONItem["id"].str; string thisItemDriveId = onedriveJSONItem["parentReference"]["driveId"].str; string thisItemParentId = onedriveJSONItem["parentReference"]["id"].str; string thisItemName = onedriveJSONItem["name"].str; // Create an empty item struct for an existing DB item Item existingDatabaseItem; // Do we NOT want this item? bool unwanted = false; // meaning by default we will WANT this item // Is this parent is in the database bool parentInDatabase = false; // Is this the 'root' folder of a Shared Folder bool rootSharedFolder = false; // What is the full path of the new item string computedItemPath; string newItemPath; // Configure the remoteItem - so if it is used, it can be utilised later Item remoteItem; // Check the database for an existing entry for this JSON item bool existingDBEntry = itemDB.selectById(thisItemDriveId, thisItemId, existingDatabaseItem); // Calculate if the Parent Item is in the database so that it can be re-used parentInDatabase = itemDB.idInLocalDatabase(thisItemDriveId, thisItemParentId); // Calculate the path of this JSON item, but we can only do this if the parent is in the database if (parentInDatabase) { // Use the original method of calculation for Personal Accounts if (appConfig.accountType == "personal") { // Personal Accounts // Calculate this items path newItemPath = computeItemPath(thisItemDriveId, thisItemParentId) ~ "/" ~ thisItemName; } else { // Business Accounts // Calculate this items path for business accounts computedItemPath = computeItemPath(thisItemDriveId, thisItemParentId); // is 'thisItemParentId' in the DB as a 'root' object? Item databaseItem; // Is this a remote drive? if (thisItemDriveId != appConfig.defaultDriveId) { // query the database for the actual thisItemParentId record itemDB.selectById(thisItemDriveId, thisItemParentId, databaseItem); } // calculate newItemPath if (databaseItem.type == ItemType.root) { // if the record type is now a root record, we dont want to add the name to itself newItemPath = computedItemPath; // set this for later use rootSharedFolder = true; } else { // add the item name to the computed path newItemPath = computedItemPath ~ "/" ~ thisItemName; } } // debug logging of what was calculated if (debugLogging) {addLogEntry("JSON Item calculated full path is: " ~ newItemPath, ["debug"]);} } else { // Parent not in the database // Is the parent a 'folder' from another user? ie - is this a 'shared folder' that has been shared with us? // Lets determine why? if (thisItemDriveId == appConfig.defaultDriveId) { // Parent path does not exist - flagging as unwanted if (debugLogging) {addLogEntry("Flagging as unwanted: thisItemDriveId (" ~ thisItemDriveId ~ "), thisItemParentId (" ~ thisItemParentId ~ ") not in local database", ["debug"]);} // Was this a skipped item? if (thisItemParentId in skippedItems) { // Parent is a skipped item if (debugLogging) {addLogEntry("Reason: thisItemParentId listed within skippedItems", ["debug"]);} } else { // Parent is not in the database, as we are not creating it if (debugLogging) {addLogEntry("Reason: Parent ID is not in the DB .. ", ["debug"]);} } // Flag as unwanted unwanted = true; } else { // Edge case as the parent (from another users OneDrive account) will never be in the database - potentially a shared object? if (debugLogging) { addLogEntry("The reported parentId is not in the database. This potentially is a shared folder as 'remoteItem.driveId' != 'appConfig.defaultDriveId'. Relevant Details: remoteItem.driveId (" ~ remoteItem.driveId ~ "), remoteItem.parentId (" ~ remoteItem.parentId ~ ")", ["debug"]); addLogEntry("Potential Shared Object JSON: " ~ sanitiseJSONItem(onedriveJSONItem), ["debug"]); } // Format the OneDrive change into a consumable object for the database remoteItem = makeItem(onedriveJSONItem); if (appConfig.accountType == "personal") { // Personal Account Handling if (debugLogging) {addLogEntry("Handling a Personal Shared Item JSON object", ["debug"]);} // Does the JSON have a shared element structure if (hasSharedElement(onedriveJSONItem)) { // Has the Shared JSON structure if (debugLogging) {addLogEntry("Personal Shared Item JSON object has the 'shared' JSON structure", ["debug"]);} // Create a 'root' and 'Shared Folder' DB Tie Records for this JSON object in a consistent manner createRequiredSharedFolderDatabaseRecords(onedriveJSONItem); } else { // The Shared JSON structure is missing ..... if (debugLogging) {addLogEntry("Personal Shared Item JSON object is MISSING the 'shared' JSON structure ... API BUG ?", ["debug"]);} } // Ensure that this item has no parent if (debugLogging) {addLogEntry("Setting remoteItem.parentId of Personal Shared Item JSON object to be null", ["debug"]);} remoteItem.parentId = null; // Add this record to the local database if (debugLogging) {addLogEntry("Update/Insert local database with Personal Shared Item JSON object with remoteItem.parentId as null: " ~ to!string(remoteItem), ["debug"]);} itemDB.upsert(remoteItem); // Due to OneDrive API inconsistency, again with European Data Centres, as we have handled this JSON - flag as unwanted as processing is complete for this JSON item unwanted = true; } else { // Business or SharePoint Account Handling if (debugLogging) {addLogEntry("Handling a Business or SharePoint Shared Item JSON object", ["debug"]);} if (appConfig.accountType == "business") { // Create a 'root' and 'Shared Folder' DB Tie Records for this JSON object in a consistent manner createRequiredSharedFolderDatabaseRecords(onedriveJSONItem); // Ensure that this item has no parent if (debugLogging) {addLogEntry("Setting remoteItem.parentId to be null", ["debug"]);} remoteItem.parentId = null; // Check the DB for 'remote' objects, searching 'remoteDriveId' and 'remoteId' items for this remoteItem.driveId and remoteItem.id Item remoteDBItem; itemDB.selectByRemoteId(remoteItem.driveId, remoteItem.id, remoteDBItem); // Must compare remoteDBItem.name with remoteItem.name if ((!remoteDBItem.name.empty) && (remoteDBItem.name != remoteItem.name)) { // Update DB Item if (debugLogging) { addLogEntry("The shared item stored in OneDrive, has a different name to the actual name on the remote drive", ["debug"]); addLogEntry("Updating remoteItem.name JSON data with the actual name being used on account drive and local folder", ["debug"]); addLogEntry("remoteItem.name was: " ~ remoteItem.name, ["debug"]); addLogEntry("Updating remoteItem.name to: " ~ remoteDBItem.name, ["debug"]); } remoteItem.name = remoteDBItem.name; if (debugLogging) {addLogEntry("Setting remoteItem.remoteName to: " ~ onedriveJSONItem["name"].str, ["debug"]);} // Update JSON Item remoteItem.remoteName = onedriveJSONItem["name"].str; if (debugLogging) { addLogEntry("Updating source JSON 'name' to that which is the actual local directory", ["debug"]); addLogEntry("onedriveJSONItem['name'] was: " ~ onedriveJSONItem["name"].str, ["debug"]); addLogEntry("Updating onedriveJSONItem['name'] to: " ~ remoteDBItem.name, ["debug"]); } onedriveJSONItem["name"] = remoteDBItem.name; if (debugLogging) {addLogEntry("onedriveJSONItem['name'] now: " ~ onedriveJSONItem["name"].str, ["debug"]);} // Update newItemPath value newItemPath = computeItemPath(thisItemDriveId, thisItemParentId) ~ "/" ~ remoteDBItem.name; if (debugLogging) {addLogEntry("New Item updated calculated full path is: " ~ newItemPath, ["debug"]);} } // Add this record to the local database if (debugLogging) {addLogEntry("Update/Insert local database with remoteItem details: " ~ to!string(remoteItem), ["debug"]);} itemDB.upsert(remoteItem); } else { // Sharepoint account type addLogEntry("Handling a SharePoint Shared Item JSON object - NOT IMPLEMENTED YET ........ RAISE A BUG PLEASE", ["info"]); } } } } // Check the skippedItems array for the parent id of this JSONItem if this is something we need to skip if (!unwanted) { if (thisItemParentId in skippedItems) { // Flag this JSON item as unwanted if (debugLogging) {addLogEntry("Flagging as unwanted: find(thisItemParentId).length != 0", ["debug"]);} unwanted = true; // Is this item id in the database? if (existingDBEntry) { // item exists in database, most likely moved out of scope for current client configuration if (debugLogging) {addLogEntry("This item was previously synced / seen by the client", ["debug"]);} if (("name" in onedriveJSONItem["parentReference"]) != null) { // How is this out of scope? // is sync_list configured if (syncListConfigured) { // sync_list configured and in use if (selectiveSync.isPathExcludedViaSyncList(onedriveJSONItem["parentReference"]["name"].str)) { // Previously synced item is now out of scope as it has been moved out of what is included in sync_list if (debugLogging) {addLogEntry("This previously synced item is now excluded from being synced due to sync_list exclusion", ["debug"]);} } } // flag to delete local file as it now is no longer in sync with OneDrive if (debugLogging) {addLogEntry("Flagging to delete item locally: ", ["debug"]);} idsToDelete ~= [thisItemDriveId, thisItemId]; } } } } // Check the item type - if it not an item type that we support, we cant process the JSON item if (!unwanted) { if (isItemFile(onedriveJSONItem)) { if (debugLogging) {addLogEntry("The JSON item we are processing is a file", ["debug"]);} } else if (isItemFolder(onedriveJSONItem)) { if (debugLogging) {addLogEntry("The JSON item we are processing is a folder", ["debug"]);} } else if (isItemRemote(onedriveJSONItem)) { if (debugLogging) {addLogEntry("The JSON item we are processing is a remote item", ["debug"]);} } else { // Why was this unwanted? if (newItemPath.empty) { if (debugLogging) {addLogEntry("OOPS: newItemPath is empty ....... need to calculate it", ["debug"]);} // Compute this item path & need the full path for this file newItemPath = computeItemPath(thisItemDriveId, thisItemParentId) ~ "/" ~ thisItemName; if (debugLogging) {addLogEntry("New Item calculated full path is: " ~ newItemPath, ["debug"]);} } // Microsoft OneNote container objects present as neither folder or file but has file size if ((!isItemFile(onedriveJSONItem)) && (!isItemFolder(onedriveJSONItem)) && (hasFileSize(onedriveJSONItem))) { // Log that this was skipped as this was a Microsoft OneNote item and unsupported if (verboseLogging) {addLogEntry("The Microsoft OneNote Notebook '" ~ newItemPath ~ "' is not supported by this client", ["verbose"]);} } else { // Log that this item was skipped as unsupported if (verboseLogging) {addLogEntry("The OneDrive item '" ~ newItemPath ~ "' is not supported by this client", ["verbose"]);} } unwanted = true; if (debugLogging) {addLogEntry("Flagging as unwanted: item type is not supported", ["debug"]);} } } // Check if this is excluded by config option: skip_dir if (!unwanted) { // Only check path if config is != "" if (!appConfig.getValueString("skip_dir").empty) { // Is the item a folder or a remote item? (which itself is a directory, but is missing the 'folder' JSON element we use to determine JSON being a directory or not) if ((isItemFolder(onedriveJSONItem)) || (isRemoteFolderItem(onedriveJSONItem))) { // work out the 'snippet' path where this folder would be created string simplePathToCheck = ""; string complexPathToCheck = ""; string matchDisplay = ""; if (hasParentReference(onedriveJSONItem)) { // we need to workout the FULL path for this item // simple path calculation if (("name" in onedriveJSONItem["parentReference"]) != null) { // how do we build the simplePathToCheck path up ? // did we flag this as the root shared folder object earlier? if (rootSharedFolder) { // just use item name simplePathToCheck = onedriveJSONItem["name"].str; } else { // add parent name to item name simplePathToCheck = onedriveJSONItem["parentReference"]["name"].str ~ "/" ~ onedriveJSONItem["name"].str; } } else { // just use item name simplePathToCheck = onedriveJSONItem["name"].str; } if (debugLogging) {addLogEntry("skip_dir path to check (simple): " ~ simplePathToCheck, ["debug"]);} // complex path calculation if (parentInDatabase) { // build up complexPathToCheck complexPathToCheck = buildNormalizedPath(newItemPath); } else { if (debugLogging) {addLogEntry("Parent details not in database - unable to compute complex path to check", ["debug"]);} } if (!complexPathToCheck.empty) { if (debugLogging) {addLogEntry("skip_dir path to check (complex): " ~ complexPathToCheck, ["debug"]);} } } else { simplePathToCheck = onedriveJSONItem["name"].str; } // If 'simplePathToCheck' or 'complexPathToCheck' is of the following format: root:/folder // then isDirNameExcluded matching will not work if (simplePathToCheck.canFind(":")) { if (debugLogging) {addLogEntry("Updating simplePathToCheck to remove 'root:'", ["debug"]);} simplePathToCheck = processPathToRemoveRootReference(simplePathToCheck); } if (complexPathToCheck.canFind(":")) { if (debugLogging) {addLogEntry("Updating complexPathToCheck to remove 'root:'", ["debug"]);} complexPathToCheck = processPathToRemoveRootReference(complexPathToCheck); } // OK .. what checks are we doing? if ((!simplePathToCheck.empty) && (complexPathToCheck.empty)) { // just a simple check if (debugLogging) {addLogEntry("Performing a simple check only", ["debug"]);} unwanted = selectiveSync.isDirNameExcluded(simplePathToCheck); } else { // simple and complex if (debugLogging) {addLogEntry("Performing a simple then complex path match if required", ["debug"]);} // simple first if (debugLogging) {addLogEntry("Performing a simple check first", ["debug"]);} unwanted = selectiveSync.isDirNameExcluded(simplePathToCheck); matchDisplay = simplePathToCheck; if (!unwanted) { // simple didnt match, perform a complex check if (debugLogging) {addLogEntry("Simple match was false, attempting complex match", ["debug"]);} unwanted = selectiveSync.isDirNameExcluded(complexPathToCheck); matchDisplay = complexPathToCheck; } } // result if (debugLogging) {addLogEntry("skip_dir exclude result (directory based): " ~ to!string(unwanted), ["debug"]);} if (unwanted) { // This path should be skipped if (verboseLogging) {addLogEntry("Skipping path - excluded by skip_dir config: " ~ matchDisplay, ["verbose"]);} } } // Is the item a file? // We need to check to see if this files path is excluded as well if (isItemFile(onedriveJSONItem)) { string pathToCheck; // does the newItemPath start with '/'? if (!startsWith(newItemPath, "/")){ // path does not start with '/', but we need to check skip_dir entries with and without '/' // so always make sure we are checking a path with '/' pathToCheck = '/' ~ dirName(newItemPath); } else { pathToCheck = dirName(newItemPath); } // perform the check unwanted = selectiveSync.isDirNameExcluded(pathToCheck); // result if (debugLogging) {addLogEntry("skip_dir exclude result (file based): " ~ to!string(unwanted), ["debug"]);} if (unwanted) { // this files path should be skipped if (verboseLogging) {addLogEntry("Skipping file - file path is excluded by skip_dir config: " ~ newItemPath, ["verbose"]);} } } } } // Check if this is excluded by config option: skip_file if (!unwanted) { // Is the JSON item a file? if (isItemFile(onedriveJSONItem)) { // skip_file can contain 4 types of entries: // - wildcard - *.txt // - text + wildcard - name*.txt // - full path + combination of any above two - /path/name*.txt // - full path to file - /path/to/file.txt // is the parent id in the database? if (parentInDatabase) { // Compute this item path & need the full path for this file if (newItemPath.empty) { if (debugLogging) {addLogEntry("OOPS: newItemPath is empty ....... need to calculate it", ["debug"]);} newItemPath = computeItemPath(thisItemDriveId, thisItemParentId) ~ "/" ~ thisItemName; if (debugLogging) {addLogEntry("New Item calculated full path is: " ~ newItemPath, ["debug"]);} } // The path that needs to be checked needs to include the '/' // This due to if the user has specified in skip_file an exclusive path: '/path/file' - that is what must be matched // However, as 'path' used throughout, use a temp variable with this modification so that we use the temp variable for exclusion checks string exclusionTestPath = ""; if (!startsWith(newItemPath, "/")){ // Add '/' to the path exclusionTestPath = '/' ~ newItemPath; } if (debugLogging) {addLogEntry("skip_file item to check: " ~ exclusionTestPath, ["debug"]);} unwanted = selectiveSync.isFileNameExcluded(exclusionTestPath); if (debugLogging) {addLogEntry("Result: " ~ to!string(unwanted), ["debug"]);} if (unwanted) { if (verboseLogging) {addLogEntry("Skipping file - excluded by skip_dir config: " ~ thisItemName, ["verbose"]);} } } else { // parent id is not in the database unwanted = true; if (verboseLogging) {addLogEntry("Skipping file - parent path not present in local database", ["verbose"]);} } } } // Check if this is included or excluded by use of sync_list if (!unwanted) { // No need to try and process something against a sync_list if it has been configured if (syncListConfigured) { // Compute the item path if empty - as to check sync_list we need an actual path to check if (newItemPath.empty) { // Calculate this items path if (debugLogging) {addLogEntry("OOPS: newItemPath is empty ....... need to calculate it", ["debug"]);} newItemPath = computeItemPath(thisItemDriveId, thisItemParentId) ~ "/" ~ thisItemName; if (debugLogging) {addLogEntry("New Item calculated full path is: " ~ newItemPath, ["debug"]);} } // What path are we checking? if (debugLogging) {addLogEntry("Path to check against 'sync_list' entries: " ~ newItemPath, ["debug"]);} // Unfortunately there is no avoiding this call to check if the path is excluded|included via sync_list if (selectiveSync.isPathExcludedViaSyncList(newItemPath)) { // selective sync advised to skip, however is this a file and are we configured to upload / download files in the root? if ((isItemFile(onedriveJSONItem)) && (appConfig.getValueBool("sync_root_files")) && (rootName(newItemPath) == "") ) { // This is a file // We are configured to sync all files in the root // This is a file in the logical root unwanted = false; } else { // path is unwanted unwanted = true; if (verboseLogging) {addLogEntry("Skipping path - excluded by sync_list config: " ~ newItemPath, ["verbose"]);} // flagging to skip this item now, but does this exist in the DB thus needs to be removed / deleted? if (existingDBEntry) { // flag to delete if (verboseLogging) {addLogEntry("Flagging item for local delete as item exists in database: " ~ newItemPath, ["verbose"]);} idsToDelete ~= [thisItemDriveId, thisItemId]; } } } } } // Check if the user has configured to skip downloading .files or .folders: skip_dotfiles if (!unwanted) { if (appConfig.getValueBool("skip_dotfiles")) { if (isDotFile(newItemPath)) { if (verboseLogging) {addLogEntry("Skipping item - .file or .folder: " ~ newItemPath, ["verbose"]);} unwanted = true; } } } // Check if this should be skipped due to a --check-for-nosync directive (.nosync)? if (!unwanted) { if (appConfig.getValueBool("check_nosync")) { // need the parent path for this object string parentPath = dirName(newItemPath); // Check for the presence of a .nosync in the parent path if (exists(parentPath ~ "/.nosync")) { if (verboseLogging) {addLogEntry("Skipping downloading item - .nosync found in parent folder & --check-for-nosync is enabled: " ~ newItemPath, ["verbose"]);} unwanted = true; } } } // Check if this is excluded by a user set maximum filesize to download if (!unwanted) { if (isItemFile(onedriveJSONItem)) { if (fileSizeLimit != 0) { if (onedriveJSONItem["size"].integer >= fileSizeLimit) { if (verboseLogging) {addLogEntry("Skipping file - excluded by skip_size config: " ~ thisItemName ~ " (" ~ to!string(onedriveJSONItem["size"].integer/2^^20) ~ " MB)", ["verbose"]);} unwanted = true; } } } } // At this point all the applicable checks on this JSON object from OneDrive are complete: // - skip_file // - skip_dir // - sync_list // - skip_dotfiles // - check_nosync // - skip_size // - We know if this item exists in the DB or not in the DB // We know if this JSON item is unwanted or not if (unwanted) { // This JSON item is NOT wanted - it is excluded if (debugLogging) {addLogEntry("Skipping OneDrive JSON item as this is determined to be unwanted either through Client Side Filtering Rules or prior processing to this point", ["debug"]);} // Add to the skippedItems array, but only if it is a directory ... pointless adding 'files' here, as it is the 'id' we check as the parent path which can only be a directory if (!isItemFile(onedriveJSONItem)) { skippedItems.insert(thisItemId); } } else { // This JSON item is wanted - we need to process this JSON item further if (debugLogging) { addLogEntry("OneDrive JSON item passed all applicable Client Side Filtering Rules and has been determined this is a wanted item", ["debug"]); addLogEntry("Creating newDatabaseItem object using the provided JSON data", ["debug"]); } // Take the JSON item and create a consumable object for eventual database insertion Item newDatabaseItem = makeItem(onedriveJSONItem); if (existingDBEntry) { // The details of this JSON item are already in the DB // Is the item in the DB the same as the JSON data provided - or is the JSON data advising this is an updated file? if (debugLogging) {addLogEntry("OneDrive JSON item is an update to an existing local item", ["debug"]);} // Compute the existing item path // NOTE: // string existingItemPath = computeItemPath(existingDatabaseItem.driveId, existingDatabaseItem.id); // // This will calculate the path as follows: // // existingItemPath: Document.txt // // Whereas above we use the following // // newItemPath = computeItemPath(newDatabaseItem.driveId, newDatabaseItem.parentId) ~ "/" ~ newDatabaseItem.name; // // Which generates the following path: // // changedItemPath: ./Document.txt // // Need to be consistent here with how 'newItemPath' was calculated string queryDriveID; string queryParentID; // Must query with a valid driveid entry if (existingDatabaseItem.driveId.empty) { queryDriveID = thisItemDriveId; } else { queryDriveID = existingDatabaseItem.driveId; } // Must query with a valid parentid entry if (existingDatabaseItem.parentId.empty) { queryParentID = thisItemParentId; } else { queryParentID = existingDatabaseItem.parentId; } // Calculate the existing path string existingItemPath = computeItemPath(queryDriveID, queryParentID) ~ "/" ~ existingDatabaseItem.name; if (debugLogging) {addLogEntry("existingItemPath calculated full path is: " ~ existingItemPath, ["debug"]);} // Attempt to apply this changed item applyPotentiallyChangedItem(existingDatabaseItem, existingItemPath, newDatabaseItem, newItemPath, onedriveJSONItem); } else { // Action this JSON item as a new item as we have no DB record of it // The actual item may actually exist locally already, meaning that just the database is out-of-date or missing the data due to --resync // But we also cannot compute the newItemPath as the parental objects may not exist as well if (debugLogging) {addLogEntry("OneDrive JSON item is potentially a new local item", ["debug"]);} // Attempt to apply this potentially new item applyPotentiallyNewLocalItem(newDatabaseItem, onedriveJSONItem, newItemPath); } } // How long to process this JSON item in batch if (debugLogging) { Duration jsonProcessingElapsedTime = MonoTime.currTime() - jsonProcessingStartTime; addLogEntry("Batched JSON item processing time: " ~ to!string(jsonProcessingElapsedTime), ["debug"]); } // Tracking as to if this item was processed processedCount++; } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Perform the download of any required objects in parallel void processDownloadActivities() { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Are there any items to delete locally? Cleanup space locally first if (!idsToDelete.empty) { // There are elements that potentially need to be deleted locally if (verboseLogging) {addLogEntry("Items to potentially delete locally: " ~ to!string(idsToDelete.length), ["verbose"]);} if (appConfig.getValueBool("download_only")) { // Download only has been configured if (cleanupLocalFiles) { // Process online deleted items if (verboseLogging) {addLogEntry("Processing local deletion activity as --download-only & --cleanup-local-files configured", ["verbose"]);} processDeleteItems(); } else { // Not cleaning up local files if (verboseLogging) {addLogEntry("Skipping local deletion activity as --download-only has been used", ["verbose"]);} // List files and directories we are not deleting locally listDeletedItems(); } } else { // Not using --download-only process normally processDeleteItems(); } // Cleanup array memory idsToDelete = []; } // Are there any items to download post fetching and processing the /delta data? if (!fileJSONItemsToDownload.empty) { // There are elements to download addLogEntry("Number of items to download from Microsoft OneDrive: " ~ to!string(fileJSONItemsToDownload.length)); downloadOneDriveItems(); // Cleanup array memory fileJSONItemsToDownload = []; } // Are there any skipped items still? if (!skippedItems.empty) { // Cleanup array memory skippedItems.clear(); } // If deltaLinkCache.latestDeltaLink is not empty, update the deltaLink in the database for this driveId so that we can reuse this now that jsonItemsToProcess has been fully processed if (!deltaLinkCache.latestDeltaLink.empty) { if (debugLogging) {addLogEntry("Updating completed deltaLink for driveID " ~ deltaLinkCache.driveId ~ " in DB to: " ~ deltaLinkCache.latestDeltaLink, ["debug"]);} itemDB.setDeltaLink(deltaLinkCache.driveId, deltaLinkCache.itemId, deltaLinkCache.latestDeltaLink); // Now that the DB is updated, when we perform the last examination of the most recent online data, cache this so this can be obtained this from memory cacheLatestDeltaLink(deltaLinkInfo, deltaLinkCache.driveId, deltaLinkCache.latestDeltaLink); } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Function to add or update a key pair in the deltaLinkInfo array void cacheLatestDeltaLink(ref DeltaLinkInfo deltaLinkInfo, string driveId, string latestDeltaLink) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } if (driveId !in deltaLinkInfo) { if (debugLogging) {addLogEntry("Added new latestDeltaLink entry: " ~ driveId ~ " -> " ~ latestDeltaLink, ["debug"]);} } else { if (debugLogging) {addLogEntry("Updated latestDeltaLink entry for " ~ driveId ~ " from " ~ deltaLinkInfo[driveId] ~ " to " ~ latestDeltaLink, ["debug"]);} } deltaLinkInfo[driveId] = latestDeltaLink; // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Function to get the latestDeltaLink based on driveId string getDeltaLinkFromCache(ref DeltaLinkInfo deltaLinkInfo, string driveId) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } string cachedDeltaLink; if (driveId in deltaLinkInfo) { cachedDeltaLink = deltaLinkInfo[driveId]; } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return value return cachedDeltaLink; } // If the JSON item is not in the database, it is potentially a new item that we need to action void applyPotentiallyNewLocalItem(Item newDatabaseItem, JSONValue onedriveJSONItem, string newItemPath) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Due to this function, we need to keep the 'return' code as-is, so that this function operates as efficiently as possible. // Whilst this means some extra code / duplication in this function, it cannot be helped // The JSON and Database items being passed in here have passed the following checks: // - skip_file // - skip_dir // - sync_list // - skip_dotfiles // - check_nosync // - skip_size // - Is not currently cached in the local database // As such, we should not be doing any other checks here to determine if the JSON item is wanted .. it is if (exists(newItemPath)) { if (debugLogging) {addLogEntry("Path on local disk already exists", ["debug"]);} // Issue #2209 fix - test if path is a bad symbolic link if (isSymlink(newItemPath)) { if (debugLogging) {addLogEntry("Path on local disk is a symbolic link ........", ["debug"]);} if (!exists(readLink(newItemPath))) { // reading the symbolic link failed if (debugLogging) {addLogEntry("Reading the symbolic link target failed ........ ", ["debug"]);} addLogEntry("Skipping item - invalid symbolic link: " ~ newItemPath, ["info", "notify"]); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return - invalid symbolic link return; } } // Path exists locally, is not a bad symbolic link // Test if this item is actually in-sync // What is the source of this item data? string itemSource = "remote"; if (isItemSynced(newDatabaseItem, newItemPath, itemSource)) { // Issue #3115 - Personal Account Shared Folder // What account type is this? if (appConfig.accountType == "personal") { // Is this a 'remote' DB record if (newDatabaseItem.type == ItemType.remote) { // Issue #3136, #3139 #3143 // Fetch the actual online record for this item // This returns the 'actual' OneDrive Personal driveId value and is 15 character checked string actualOnlineDriveId = testProvidedDriveIdForLengthIssue(fetchRealOnlineDriveIdentifier(newDatabaseItem.remoteDriveId)); newDatabaseItem.remoteDriveId = actualOnlineDriveId; } } // Item details from OneDrive and local item details in database are in-sync if (debugLogging) { addLogEntry("The item to sync is already present on the local filesystem and is in-sync with what is reported online", ["debug"]); addLogEntry("Update/Insert local database with item details: " ~ to!string(newDatabaseItem), ["debug"]); } // Add item to database itemDB.upsert(newDatabaseItem); // With the 'newDatabaseItem' saved to the database, regardless of --dry-run situation - was that new database item a 'remote' item? // If this is this a 'Shared Folder' item - ensure we have created / updated any relevant Database Tie Records // This should be applicable for all account types if (newDatabaseItem.type == ItemType.remote) { // yes this is a remote item type if (debugLogging) {addLogEntry("The 'newDatabaseItem' (applyPotentiallyNewLocalItem) is a remote item type - we need to create all of the associated database tie records for this database entry" , ["debug"]);} // Create a 'root' and 'Shared Folder' DB Tie Records for this JSON object in a consistent manner createRequiredSharedFolderDatabaseRecords(onedriveJSONItem); } // Did the user configure to save xattr data about this file? if (appConfig.getValueBool("write_xattr_data")) { writeXattrData(newItemPath, onedriveJSONItem); } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // all done processing this potential new local item return; } else { // Item details from OneDrive and local item details in database are NOT in-sync if (debugLogging) {addLogEntry("The item to sync exists locally but is potentially not in the local database - otherwise this would be handled as changed item", ["debug"]);} // Which object is newer? The local file or the remote file? SysTime localModifiedTime = timeLastModified(newItemPath).toUTC(); SysTime itemModifiedTime = newDatabaseItem.mtime; // Reduce time resolution to seconds before comparing localModifiedTime.fracSecs = Duration.zero; itemModifiedTime.fracSecs = Duration.zero; // Is the local modified time greater than that from OneDrive? if (localModifiedTime > itemModifiedTime) { // Local file is newer than item on OneDrive based on file modified time // Is this item id in the database? if (itemDB.idInLocalDatabase(newDatabaseItem.driveId, newDatabaseItem.id)) { // item id is in the database // no local rename // no download needed // Fetch the latest DB record - as this could have been updated by the isItemSynced if the date online was being corrected, then the DB updated as a result Item latestDatabaseItem; itemDB.selectById(newDatabaseItem.driveId, newDatabaseItem.id, latestDatabaseItem); if (debugLogging) {addLogEntry("latestDatabaseItem: " ~ to!string(latestDatabaseItem), ["debug"]);} SysTime latestItemModifiedTime = latestDatabaseItem.mtime; // Reduce time resolution to seconds before comparing latestItemModifiedTime.fracSecs = Duration.zero; if (localModifiedTime == latestItemModifiedTime) { // Log action if (verboseLogging) {addLogEntry("Local file modified time matches existing database record - keeping local file", ["verbose"]);} if (debugLogging) {addLogEntry("Skipping OneDrive change as this is determined to be unwanted due to local file modified time matching database data", ["debug"]);} } else { // Log action if (verboseLogging) {addLogEntry("Local file modified time is newer based on UTC time conversion - keeping local file as this exists in the local database", ["verbose"]);} if (debugLogging) {addLogEntry("Skipping OneDrive change as this is determined to be unwanted due to local file modified time being newer than OneDrive file and present in the sqlite database", ["debug"]);} } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // Return as no further action needed return; } else { // item id is not in the database .. maybe a --resync ? // file exists locally but is not in the sqlite database - maybe a failed download? if (verboseLogging) {addLogEntry("Local item does not exist in local database - replacing with file from OneDrive - failed download?", ["verbose"]);} // In a --resync scenario or if items.sqlite3 was deleted before startup we have zero way of knowing IF the local file is meant to be the right file // To this pint we have passed the following checks: // 1. Any client side filtering checks - this determined this is a file that is wanted // 2. A file with the exact name exists locally // 3. The local modified time > remote modified time // 4. The id of the item from OneDrive is not in the database // If local data protection is configured (bypassDataPreservation = false), safeBackup the local file, passing in if we are performing a --dry-run or not // In case the renamed path is needed string renamedPath; safeBackup(newItemPath, dryRun, bypassDataPreservation, renamedPath); } } else { // Is the remote newer? if (localModifiedTime < itemModifiedTime) { // Remote file is newer than the existing local item if (verboseLogging) {addLogEntry("Remote item modified time is newer based on UTC time conversion", ["verbose"]);} // correct message, remote item is newer if (debugLogging) { addLogEntry("localModifiedTime (local file): " ~ to!string(localModifiedTime), ["debug"]); addLogEntry("itemModifiedTime (OneDrive item): " ~ to!string(itemModifiedTime), ["debug"]); } // If local data protection is configured (bypassDataPreservation = false), safeBackup the local file, passing in if we are performing a --dry-run or not // In case the renamed path is needed string renamedPath; safeBackup(newItemPath, dryRun, bypassDataPreservation, renamedPath); } // Are the timestamps equal? if (localModifiedTime == itemModifiedTime) { // yes they are equal if (debugLogging) { addLogEntry("File timestamps are equal, no further action required", ["debug"]); // correct message as timestamps are equal addLogEntry("Update/Insert local database with item details: " ~ to!string(newDatabaseItem), ["debug"]); } // Add item to database itemDB.upsert(newDatabaseItem); // Did the user configure to save xattr data about this file? if (appConfig.getValueBool("write_xattr_data")) { writeXattrData(newItemPath, onedriveJSONItem); } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // everything all OK, DB updated return; } } } } // Path does not exist locally (should not exist locally if renamed file) - this will be a new file download or new folder creation // How to handle this Potentially New Local Item JSON ? final switch (newDatabaseItem.type) { case ItemType.file: // Add to the file to the download array for processing later fileJSONItemsToDownload ~= onedriveJSONItem; goto functionCompletion; case ItemType.dir: // Create the directory immediately as we depend on its entry existing handleLocalDirectoryCreation(newDatabaseItem, newItemPath, onedriveJSONItem); goto functionCompletion; case ItemType.remote: // Add to the directory and relevant details for processing later if (newDatabaseItem.remoteType == ItemType.dir) { handleLocalDirectoryCreation(newDatabaseItem, newItemPath, onedriveJSONItem); } else { // Add to the file to the download array for processing later fileJSONItemsToDownload ~= onedriveJSONItem; } goto functionCompletion; case ItemType.root: case ItemType.unknown: case ItemType.none: // Unknown type - we dont action or sync these items goto functionCompletion; } // To correctly handle a switch|case statement we use goto post the switch|case statement as if 'break' is used, we never get to this point functionCompletion: // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Handle the creation of a new local directory void handleLocalDirectoryCreation(Item newDatabaseItem, string newItemPath, JSONValue onedriveJSONItem) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // To create a path, 'newItemPath' must not be empty if (!newItemPath.empty) { // Update the logging output to be consistent if (verboseLogging) {addLogEntry("Creating local directory: " ~ "./" ~ buildNormalizedPath(newItemPath), ["verbose"]);} if (!dryRun) { try { // Create the new directory if (debugLogging) {addLogEntry("Requested local path does not exist, creating directory structure: " ~ newItemPath, ["debug"]);} mkdirRecurse(newItemPath); // Has the user disabled the setting of filesystem permissions? if (!appConfig.getValueBool("disable_permission_set")) { // Configure the applicable permissions for the folder if (debugLogging) {addLogEntry("Setting directory permissions for: " ~ newItemPath, ["debug"]);} newItemPath.setAttributes(appConfig.returnRequiredDirectoryPermissions()); } else { // Use inherited permissions if (debugLogging) {addLogEntry("Using inherited filesystem permissions for: " ~ newItemPath, ["debug"]);} } // Update the time of the folder to match the last modified time as is provided by OneDrive // If there are any files then downloaded into this folder, the last modified time will get // updated by the local Operating System with the latest timestamp - as this is normal operation // as the directory has been modified // Set the timestamp, logging and error handling done within function setPathTimestamp(dryRun, newItemPath, newDatabaseItem.mtime); // Save the newDatabaseItem to the database saveDatabaseItem(newDatabaseItem); } catch (FileException e) { // display the error message displayFileSystemErrorMessage(e.msg, thisFunctionName); } } else { // we dont create the directory, but we need to track that we 'faked it' idsFaked ~= [newDatabaseItem.driveId, newDatabaseItem.id]; // Save the newDatabaseItem to the database saveDatabaseItem(newDatabaseItem); } // With the 'newDatabaseItem' saved to the database, regardless of --dry-run situation - was that new database item a 'remote' item? // Is this folder that has been created locally a 'Shared Folder' online? // This should be applicable for all account types if (newDatabaseItem.type == ItemType.remote) { // yes this is a remote item type if (debugLogging) {addLogEntry("The 'newDatabaseItem' (handleLocalDirectoryCreation) is a remote item type - we need to create all of the associated database tie records for this database entry" , ["debug"]);} // Create a 'root' and 'Shared Folder' DB Tie Records for this JSON object in a consistent manner createRequiredSharedFolderDatabaseRecords(onedriveJSONItem); } } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Create 'root' DB Tie Record and 'Shared Folder' DB Record in a consistent manner void createRequiredSharedFolderDatabaseRecords(JSONValue onedriveJSONItem) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Due to this function, we need to keep the return code, so that this function operates as efficiently as possible. // Whilst this means some extra code / duplication in this function, it cannot be helped // Detail what we are doing if (debugLogging) {addLogEntry("We have been requested to create 'root' and 'Shared Folder' DB Tie Records in a consistent manner" , ["debug"]);} JSONValue onlineParentData; string parentDriveId; string parentObjectId; OneDriveApi onlineParentOneDriveApiInstance; onlineParentOneDriveApiInstance = new OneDriveApi(appConfig); onlineParentOneDriveApiInstance.initialise(); // Using the onlineParentData JSON data make a DB record for this parent item so that it exists in the database Item sharedFolderDatabaseTie; // What account type is this? This needs to be configured correctly so this can be queried correctly if (appConfig.accountType == "personal") { // OneDrive Personal JSON has this structure that we need to use parentDriveId = onedriveJSONItem["remoteItem"]["parentReference"]["driveId"].str; parentObjectId = onedriveJSONItem["remoteItem"]["id"].str; } else { // OneDrive Business|Sharepoint JSON has this structure that we need to use parentDriveId = onedriveJSONItem["remoteItem"]["parentReference"]["driveId"].str; parentObjectId = onedriveJSONItem["remoteItem"]["id"].str; } // Issue #3115 - Validate driveId length // What account type is this? if (appConfig.accountType == "personal") { // Test driveId length and validation if the driveId we are testing is not equal to appConfig.defaultDriveId if (parentDriveId != appConfig.defaultDriveId) { parentDriveId = testProvidedDriveIdForLengthIssue(parentDriveId); } } // Try and fetch this shared folder parent's details try { if (debugLogging) {addLogEntry(format("Fetching Shared Folder online data for parentDriveId '%s' and parentObjectId '%s'", parentDriveId, parentObjectId), ["debug"]);} onlineParentData = onlineParentOneDriveApiInstance.getPathDetailsById(parentDriveId, parentObjectId); } catch (OneDriveException exception) { // If we get a 404 .. the shared item does not exist online ... perhaps a broken 'Add shortcut to My files' link in the account holders directory? if ((exception.httpStatusCode == 403) || (exception.httpStatusCode == 404)) { // The API call returned a 404 error response if (debugLogging) {addLogEntry("onlineParentData = onlineParentOneDriveApiInstance.getPathDetailsById(parentDriveId, parentObjectId); generated a 404 - shared folder path does not exist online", ["debug"]);} string errorMessage = format("WARNING: The OneDrive Shared Folder link target '%s' cannot be found online using the provided online data.", onedriveJSONItem["name"].str); // detail what this 404 error response means addLogEntry(); addLogEntry(errorMessage); addLogEntry("WARNING: This is potentially a broken online OneDrive Shared Folder link or you no longer have access to it. Please correct this error online."); addLogEntry(); // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory onlineParentOneDriveApiInstance.releaseCurlEngine(); onlineParentOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // we have to return at this point return; } else { // Catch all other errors // Display what the error is // - 408,429,503,504 errors are handled as a retry within uploadFileOneDriveApiInstance displayOneDriveErrorMessage(exception.msg, thisFunctionName); // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory onlineParentOneDriveApiInstance.releaseCurlEngine(); onlineParentOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // If we get an error, we cannot do much else return; } } // Create a 'root' DB Tie Record for a Shared Folder from the parent folder JSON data // - This maps the Shared Folder 'driveId' with the parent folder where the shared folder exists, so we can call the parent folder to query for changes to this Shared Folder createDatabaseRootTieRecordForOnlineSharedFolder(onlineParentData); // Log that we are created the Shared Folder Tie record now if (debugLogging) {addLogEntry("Creating the Shared Folder DB Tie Record that binds the 'root' record to the 'folder'" , ["debug"]);} // Make an item from the online JSON data sharedFolderDatabaseTie = makeItem(onlineParentData); // Ensure we use our online name, as we may have renamed the folder in our location sharedFolderDatabaseTie.name = onedriveJSONItem["name"].str; // use this as the name .. this is the name of the folder online in our OneDrive account // Is sharedFolderDatabaseTie.driveId empty? if (sharedFolderDatabaseTie.driveId.empty) { // This cannot be empty - set to the correct reference for the Shared Folder DB Tie record if (debugLogging) {addLogEntry("The Shared Folder DB Tie record entry for 'driveId' is empty ... correcting it" , ["debug"]);} sharedFolderDatabaseTie.driveId = onlineParentData["parentReference"]["driveId"].str; } // Ensure 'parentId' is not empty, except for Personal Accounts if (appConfig.accountType != "personal") { // Is sharedFolderDatabaseTie.parentId.empty? if (sharedFolderDatabaseTie.parentId.empty) { // This cannot be empty - set to the correct reference for the Shared Folder DB Tie record if (debugLogging) {addLogEntry("The Shared Folder DB Tie record entry for 'parentId' is empty ... correcting it" , ["debug"]);} sharedFolderDatabaseTie.parentId = onlineParentData["id"].str; } } else { // The database Tie Record for Personal Accounts must be empty .. no change, leave 'parentId' empty } // If a user has added the 'whole' SharePoint Document Library, then the DB Shared Folder Tie Record and 'root' record are the 'same' if ((isItemRoot(onlineParentData)) && (onlineParentData["parentReference"]["driveType"].str == "documentLibrary")) { // Yes this is a DocumentLibrary 'root' object if (debugLogging) { addLogEntry("Updating Shared Folder DB Tie record entry with correct values as this is a 'root' object as it is a SharePoint Library Root Object" , ["debug"]); addLogEntry(" sharedFolderDatabaseTie.parentId = null", ["debug"]); addLogEntry(" sharedFolderDatabaseTie.type = ItemType.root", ["debug"]); } sharedFolderDatabaseTie.parentId = null; sharedFolderDatabaseTie.type = ItemType.root; } // Personal Account Shared Folder Handling if (appConfig.accountType == "personal") { // Yes this is a personal account if (debugLogging) { addLogEntry("Updating Shared Folder DB Tie record entry with correct values as this is a 'root' object as it is a Personal Shared Folder Root Object" , ["debug"]); addLogEntry(" sharedFolderDatabaseTie.type = ItemType.root", ["debug"]); } sharedFolderDatabaseTie.type = ItemType.root; } // Issue #3115 - Validate driveId length // What account type is this? if (appConfig.accountType == "personal") { // Test driveId length and validation if the driveId we are testing is not equal to appConfig.defaultDriveId if (sharedFolderDatabaseTie.driveId != appConfig.defaultDriveId) { sharedFolderDatabaseTie.driveId = testProvidedDriveIdForLengthIssue(sharedFolderDatabaseTie.driveId); } } // Log action addLogEntry("Creating|Updating a DB Tie Record for this Shared Folder from the online parental data: " ~ sharedFolderDatabaseTie.name, ["debug"]); addLogEntry("Shared Folder DB Tie Record data: " ~ to!string(sharedFolderDatabaseTie), ["debug"]); // Save item itemDB.upsert(sharedFolderDatabaseTie); // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory onlineParentOneDriveApiInstance.releaseCurlEngine(); onlineParentOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // If the JSON item IS in the database, this will be an update to an existing in-sync item void applyPotentiallyChangedItem(Item existingDatabaseItem, string existingItemPath, Item changedOneDriveItem, string changedItemPath, JSONValue onedriveJSONItem) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // If we are moving the item, we do not need to download it again bool itemWasMoved = false; // Do we need to actually update the database with the details that were provided by the OneDrive API? // Calculate these time items from the provided items SysTime existingItemModifiedTime = existingDatabaseItem.mtime; existingItemModifiedTime.fracSecs = Duration.zero; SysTime changedOneDriveItemModifiedTime = changedOneDriveItem.mtime; changedOneDriveItemModifiedTime.fracSecs = Duration.zero; // Did the eTag change? if (existingDatabaseItem.eTag != changedOneDriveItem.eTag) { // The eTag has changed to what we previously cached if (existingItemPath != changedItemPath) { // Log that we are changing / moving an item to a new name addLogEntry("Moving " ~ existingItemPath ~ " to " ~ changedItemPath); // Is the destination path empty .. or does something exist at that location? if (exists(changedItemPath)) { // Destination we are moving to exists ... Item changedLocalItem; // Query DB for this changed item in specified path that exists and see if it is in-sync if (itemDB.selectByPath(changedItemPath, changedOneDriveItem.driveId, changedLocalItem)) { // The 'changedItemPath' is in the database string itemSource = "database"; if (isItemSynced(changedLocalItem, changedItemPath, itemSource)) { // The destination item is in-sync if (verboseLogging) {addLogEntry("Destination is in sync and will be overwritten", ["verbose"]);} } else { // The destination item is different if (verboseLogging) {addLogEntry("The destination is occupied with a different item, renaming the conflicting file...", ["verbose"]);} // If local data protection is configured (bypassDataPreservation = false), safeBackup the local file, passing in if we are performing a --dry-run or not // In case the renamed path is needed string renamedPath; safeBackup(changedItemPath, dryRun, bypassDataPreservation, renamedPath); } } else { // The to be overwritten item is not already in the itemdb, so it should saved to avoid data loss if (verboseLogging) {addLogEntry("The destination is occupied by an existing un-synced file, renaming the conflicting file...", ["verbose"]);} // If local data protection is configured (bypassDataPreservation = false), safeBackup the local file, passing in if we are performing a --dry-run or not // In case the renamed path is needed string renamedPath; safeBackup(changedItemPath, dryRun, bypassDataPreservation, renamedPath); } } // Try and rename path, catch any exception generated try { // If we are in a --dry-run situation? , the actual rename did not occur - but we need to track like it did if(!dryRun) { // Rename this item, passing in if we are performing a --dry-run or not safeRename(existingItemPath, changedItemPath, dryRun); // Flag that the item was moved | renamed itemWasMoved = true; // If the item is a file, make sure that the local timestamp now is the same as the timestamp online // Otherwise when we do the DB check, the move on the file system, the file technically has a newer timestamp // which is 'correct' .. but we need to report locally the online timestamp here as the move was made online if (changedOneDriveItem.type == ItemType.file) { // Set the timestamp, logging and error handling done within function setPathTimestamp(dryRun, changedItemPath, changedOneDriveItem.mtime); } } else { // --dry-run situation - the actual rename did not occur - but we need to track like it did // Track this as a faked id item idsFaked ~= [changedOneDriveItem.driveId, changedOneDriveItem.id]; // We also need to track that we did not rename this path pathsRenamed ~= [existingItemPath]; } } catch (FileException e) { // display the error message displayFileSystemErrorMessage(e.msg, thisFunctionName); } } // What sort of changed item is this? // Is it a file or remote file, and we did not move it .. if (((changedOneDriveItem.type == ItemType.file) && (!itemWasMoved)) || (((changedOneDriveItem.type == ItemType.remote) && (changedOneDriveItem.remoteType == ItemType.file)) && (!itemWasMoved))) { // The eTag is notorious for being 'changed' online by some backend Microsoft process if (existingDatabaseItem.quickXorHash != changedOneDriveItem.quickXorHash) { // Add to the items to download array for processing - the file hash we previously recorded is not the same as online fileJSONItemsToDownload ~= onedriveJSONItem; } else { // If the timestamp is different, or we are running a client operational mode that does not support /delta queries - we have to update the DB with the details from OneDrive // Unfortunately because of the consequence of National Cloud Deployments not supporting /delta queries, the application uses the local database to flag what is out-of-date / track changes // This means that the constant disk writing to the database fix implemented with https://github.com/abraunegg/onedrive/pull/2004 cannot be utilised when using these operational modes // as all records are touched / updated when performing the OneDrive sync operations. The impacted operational modes are: // - National Cloud Deployments do not support /delta as a query // - When using --single-directory // - When using --download-only --cleanup-local-files // Is the last modified timestamp in the DB the same as the API data or are we running an operational mode where we simulated the /delta response? if ((existingItemModifiedTime != changedOneDriveItemModifiedTime) || (generateSimulatedDeltaResponse)) { // Save this item in the database // Issue #3115 - Personal Account Shared Folder // What account type is this? if (appConfig.accountType == "personal") { // Is this a 'remote' DB record if (changedOneDriveItem.type == ItemType.remote) { // Issue #3136, #3139 #3143 // Fetch the actual online record for this item // This returns the actual OneDrive Personal driveId value and is 15 character checked string actualOnlineDriveId = testProvidedDriveIdForLengthIssue(fetchRealOnlineDriveIdentifier(changedOneDriveItem.remoteDriveId)); changedOneDriveItem.remoteDriveId = actualOnlineDriveId; } } // Add to the local database if (debugLogging) {addLogEntry("Adding changed OneDrive Item to database: " ~ to!string(changedOneDriveItem), ["debug"]);} itemDB.upsert(changedOneDriveItem); } } } else { // Save this item in the database saveItem(onedriveJSONItem); // If the 'Add shortcut to My files' link was the item that was actually renamed .. we have to update our DB records if (changedOneDriveItem.type == ItemType.remote) { // Select remote item data from the database Item existingRemoteDbItem; itemDB.selectById(changedOneDriveItem.remoteDriveId, changedOneDriveItem.remoteId, existingRemoteDbItem); // Update the 'name' in existingRemoteDbItem and save it back to the database // This is the local name stored on disk that was just 'moved' existingRemoteDbItem.name = changedOneDriveItem.name; itemDB.upsert(existingRemoteDbItem); } } } else { // The existingDatabaseItem.eTag == changedOneDriveItem.eTag .. nothing has changed eTag wise // If the timestamp is different, or we are running a client operational mode that does not support /delta queries - we have to update the DB with the details from OneDrive // Unfortunately because of the consequence of National Cloud Deployments not supporting /delta queries, the application uses the local database to flag what is out-of-date / track changes // This means that the constant disk writing to the database fix implemented with https://github.com/abraunegg/onedrive/pull/2004 cannot be utilised when using these operational modes // as all records are touched / updated when performing the OneDrive sync operations. The impacted operational modes are: // - National Cloud Deployments do not support /delta as a query // - When using --single-directory // - When using --download-only --cleanup-local-files // Is the last modified timestamp in the DB the same as the API data or are we running an operational mode where we simulated the /delta response? if ((existingItemModifiedTime != changedOneDriveItemModifiedTime) || (generateSimulatedDeltaResponse)) { // Database update needed for this item because our local record is out-of-date // Issue #3115 - Personal Account Shared Folder // What account type is this? if (appConfig.accountType == "personal") { // Is this a 'remote' DB record if (changedOneDriveItem.type == ItemType.remote) { // Issue #3136, #3139 #3143 // Fetch the actual online record for this item // This returns the actual OneDrive Personal driveId value and is 15 character checked string actualOnlineDriveId = testProvidedDriveIdForLengthIssue(fetchRealOnlineDriveIdentifier(changedOneDriveItem.remoteDriveId)); changedOneDriveItem.remoteDriveId = actualOnlineDriveId; } } // Add to the local database if (debugLogging) {addLogEntry("Adding changed OneDrive Item to database: " ~ to!string(changedOneDriveItem), ["debug"]);} itemDB.upsert(changedOneDriveItem); } } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Download new/changed file items as identified void downloadOneDriveItems() { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Lets deal with all the JSON items that need to be downloaded in a batch process size_t batchSize = to!int(appConfig.getValueLong("threads")); long batchCount = (fileJSONItemsToDownload.length + batchSize - 1) / batchSize; long batchesProcessed = 0; // Transfer order string transferOrder = appConfig.getValueString("transfer_order"); // Has the user configured to specify the transfer order of files? if (transferOrder != "default") { // If we have more than 1 item to download, sort the items if (count(fileJSONItemsToDownload) > 1) { // Perform sorting based on transferOrder if (transferOrder == "size_asc") { fileJSONItemsToDownload.sort!((a, b) => a["size"].integer < b["size"].integer); // sort the array by ascending size } else if (transferOrder == "size_dsc") { fileJSONItemsToDownload.sort!((a, b) => a["size"].integer > b["size"].integer); // sort the array by descending size } else if (transferOrder == "name_asc") { fileJSONItemsToDownload.sort!((a, b) => a["name"].str < b["name"].str); // sort the array by ascending name } else if (transferOrder == "name_dsc") { fileJSONItemsToDownload.sort!((a, b) => a["name"].str > b["name"].str); // sort the array by descending name } } } // Process fileJSONItemsToDownload foreach (chunk; fileJSONItemsToDownload.chunks(batchSize)) { // send an array containing 'appConfig.getValueLong("threads")' JSON items to download downloadOneDriveItemsInParallel(chunk); } // For this set of items, perform a DB PASSIVE checkpoint itemDB.performCheckpoint("PASSIVE"); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Download items in parallel void downloadOneDriveItemsInParallel(JSONValue[] array) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // This function received an array of JSON items to download, the number of elements based on appConfig.getValueLong("threads") foreach (i, onedriveJSONItem; processPool.parallel(array)) { // Take each JSON item and downloadFileItem(onedriveJSONItem); } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Perform the actual download of an object from OneDrive void downloadFileItem(JSONValue onedriveJSONItem, bool ignoreDataPreservationCheck = false) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Function variables bool downloadFailed = false; string OneDriveFileXORHash; string OneDriveFileSHA256Hash; long jsonFileSize = 0; Item databaseItem; bool fileFoundInDB = false; // Capture what time this download started SysTime downloadStartTime = Clock.currTime(); // Download item specifics string downloadItemId = onedriveJSONItem["id"].str; string downloadItemName = onedriveJSONItem["name"].str; string downloadDriveId = onedriveJSONItem["parentReference"]["driveId"].str; string downloadParentId = onedriveJSONItem["parentReference"]["id"].str; // Calculate this items path string newItemPath = computeItemPath(downloadDriveId, downloadParentId) ~ "/" ~ downloadItemName; if (debugLogging) {addLogEntry("JSON Item calculated full path for download is: " ~ newItemPath, ["debug"]);} // Is the item reported as Malware ? if (isMalware(onedriveJSONItem)){ // OneDrive reports that this file is malware addLogEntry("ERROR: MALWARE DETECTED IN FILE - DOWNLOAD SKIPPED: " ~ newItemPath, ["info", "notify"]); downloadFailed = true; } else { // Grab this file's filesize if (hasFileSize(onedriveJSONItem)) { // Use the configured filesize as reported by OneDrive jsonFileSize = onedriveJSONItem["size"].integer; } else { // filesize missing if (debugLogging) {addLogEntry("ERROR: onedriveJSONItem['size'] is missing", ["debug"]);} } // Configure the hashes for comparison post download if (hasHashes(onedriveJSONItem)) { // File details returned hash details // QuickXorHash if (hasQuickXorHash(onedriveJSONItem)) { // Use the provided quickXorHash as reported by OneDrive if (onedriveJSONItem["file"]["hashes"]["quickXorHash"].str != "") { OneDriveFileXORHash = onedriveJSONItem["file"]["hashes"]["quickXorHash"].str; } } else { // Fallback: Check for SHA256Hash if (hasSHA256Hash(onedriveJSONItem)) { // Use the provided sha256Hash as reported by OneDrive if (onedriveJSONItem["file"]["hashes"]["sha256Hash"].str != "") { OneDriveFileSHA256Hash = onedriveJSONItem["file"]["hashes"]["sha256Hash"].str; } } } } else { // file hash data missing if (debugLogging) {addLogEntry("ERROR: onedriveJSONItem['file']['hashes'] is missing - unable to compare file hash after download", ["debug"]);} } // Does the file already exist in the path locally? if (exists(newItemPath)) { // To accommodate forcing the download of a file, post upload to Microsoft OneDrive, we need to ignore the checking of hashes and making a safe backup if (!ignoreDataPreservationCheck) { // file exists locally already foreach (driveId; onlineDriveDetails.keys) { if (itemDB.selectByPath(newItemPath, driveId, databaseItem)) { fileFoundInDB = true; break; } } // Log the DB details if (debugLogging) {addLogEntry("File to download exists locally and this is the DB record: " ~ to!string(databaseItem), ["debug"]);} // Does the DB (what we think is in sync) hash match the existing local file hash? if (!testFileHash(newItemPath, databaseItem)) { // local file is different to what we know to be true addLogEntry("The local file to replace (" ~ newItemPath ~ ") has been modified locally since the last download. Renaming it to avoid potential local data loss."); // If local data protection is configured (bypassDataPreservation = false), safeBackup the local file, passing in if we are performing a --dry-run or not // In case the renamed path is needed string renamedPath; safeBackup(newItemPath, dryRun, bypassDataPreservation, renamedPath); } } } // Is there enough free space locally to download the file // - We can use '.' here as we change the current working directory to the configured 'sync_dir' long localActualFreeSpace = to!long(getAvailableDiskSpace(".")); // So that we are not responsible in making the disk 100% full if we can download the file, compare the current available space against the reservation set and file size // The reservation value is user configurable in the config file, 50MB by default long freeSpaceReservation = appConfig.getValueLong("space_reservation"); // debug output if (debugLogging) { addLogEntry("Local Disk Space Actual: " ~ to!string(localActualFreeSpace), ["debug"]); addLogEntry("Free Space Reservation: " ~ to!string(freeSpaceReservation), ["debug"]); addLogEntry("File Size to Download: " ~ to!string(jsonFileSize), ["debug"]); } // Calculate if we can actually download file - is there enough free space? if ((localActualFreeSpace < freeSpaceReservation) || (jsonFileSize > localActualFreeSpace)) { // localActualFreeSpace is less than freeSpaceReservation .. insufficient free space // jsonFileSize is greater than localActualFreeSpace .. insufficient free space addLogEntry("Downloading file: " ~ newItemPath ~ " ... failed!", ["info", "notify"]); addLogEntry("Insufficient local disk space to download file"); downloadFailed = true; } else { // If we are in a --dry-run situation - if not, actually perform the download if (!dryRun) { // Attempt to download the file as there is enough free space locally OneDriveApi downloadFileOneDriveApiInstance; try { // Initialise API instance downloadFileOneDriveApiInstance = new OneDriveApi(appConfig); downloadFileOneDriveApiInstance.initialise(); // OneDrive Business Shared Files - update the driveId where to get the file from if (isItemRemote(onedriveJSONItem)) { downloadDriveId = onedriveJSONItem["remoteItem"]["parentReference"]["driveId"].str; } // Perform the download downloadFileOneDriveApiInstance.downloadById(downloadDriveId, downloadItemId, newItemPath, jsonFileSize); // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory downloadFileOneDriveApiInstance.releaseCurlEngine(); downloadFileOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); } catch (OneDriveException exception) { if (debugLogging) {addLogEntry("downloadFileOneDriveApiInstance.downloadById(downloadDriveId, downloadItemId, newItemPath, jsonFileSize); generated a OneDriveException", ["debug"]);} // HTTP request returned status code 403 if ((exception.httpStatusCode == 403) && (appConfig.getValueBool("sync_business_shared_files"))) { // We attempted to download a file, that was shared with us, but this was shared with us as read-only and no download permission addLogEntry("Unable to download this file as this was shared as read-only without download permission: " ~ newItemPath); downloadFailed = true; } else { // Default operation if not a 403 error // - 408,429,503,504 errors are handled as a retry within downloadFileOneDriveApiInstance // Display what the error is displayOneDriveErrorMessage(exception.msg, thisFunctionName); } } catch (FileException e) { // There was a file system error // display the error message displayFileSystemErrorMessage(e.msg, thisFunctionName); downloadFailed = true; } catch (ErrnoException e) { // There was a file system error // display the error message displayFileSystemErrorMessage(e.msg, thisFunctionName); downloadFailed = true; } // If we get to this point, something was downloaded .. does it match what we expected? if (exists(newItemPath)) { // When downloading some files from SharePoint, the OneDrive API reports one file size, // but the SharePoint HTTP Server sends a totally different byte count for the same file // we have implemented --disable-download-validation to disable these checks // Regardless of --disable-download-validation we still need to set the file timestamp correctly // Get the mtime from the JSON data SysTime itemModifiedTime; string lastModifiedTimestamp; if (isItemRemote(onedriveJSONItem)) { // remote file item lastModifiedTimestamp = strip(onedriveJSONItem["remoteItem"]["fileSystemInfo"]["lastModifiedDateTime"].str); // is lastModifiedTimestamp valid? if (isValidUTCDateTime(lastModifiedTimestamp)) { // string is a valid timestamp itemModifiedTime = SysTime.fromISOExtString(lastModifiedTimestamp); } else { // invalid timestamp from JSON file addLogEntry("WARNING: Invalid timestamp provided by the Microsoft OneDrive API: " ~ lastModifiedTimestamp); // Set mtime to Clock.currTime(UTC()) given that the time in the JSON should be a UTC timestamp itemModifiedTime = Clock.currTime(UTC()); } } else { // not a remote item lastModifiedTimestamp = strip(onedriveJSONItem["fileSystemInfo"]["lastModifiedDateTime"].str); // is lastModifiedTimestamp valid? if (isValidUTCDateTime(lastModifiedTimestamp)) { // string is a valid timestamp itemModifiedTime = SysTime.fromISOExtString(lastModifiedTimestamp); } else { // invalid timestamp from JSON file addLogEntry("WARNING: Invalid timestamp provided by the Microsoft OneDrive API: " ~ lastModifiedTimestamp); // Set mtime to Clock.currTime(UTC()) given that the time in the JSON should be a UTC timestamp itemModifiedTime = Clock.currTime(UTC()); } } // Did the user configure --disable-download-validation ? if (!disableDownloadValidation) { // A 'file' was downloaded - does what we downloaded = reported jsonFileSize or if there is some sort of funky local disk compression going on // Does the file hash OneDrive reports match what we have locally? string onlineFileHash; string downloadedFileHash; long downloadFileSize = getSize(newItemPath); if (!OneDriveFileXORHash.empty) { onlineFileHash = OneDriveFileXORHash; // Calculate the QuickXOHash for this file downloadedFileHash = computeQuickXorHash(newItemPath); } else { onlineFileHash = OneDriveFileSHA256Hash; // Fallback: Calculate the SHA256 Hash for this file downloadedFileHash = computeSHA256Hash(newItemPath); } if ((downloadFileSize == jsonFileSize) && (downloadedFileHash == onlineFileHash)) { // Downloaded file matches size and hash if (debugLogging) {addLogEntry("Downloaded file matches reported size and reported file hash", ["debug"]);} // Set the timestamp, logging and error handling done within function setPathTimestamp(dryRun, newItemPath, itemModifiedTime); } else { // Downloaded file does not match size or hash .. which is it? bool downloadValueMismatch = false; // Size error? if (downloadFileSize != jsonFileSize) { // downloaded file size does not match downloadValueMismatch = true; if (debugLogging) { addLogEntry("Actual file size on disk: " ~ to!string(downloadFileSize), ["debug"]); addLogEntry("OneDrive API reported size: " ~ to!string(jsonFileSize), ["debug"]); } addLogEntry("ERROR: File download size mismatch. Increase logging verbosity to determine why."); } // Hash Error if (downloadedFileHash != onlineFileHash) { // downloaded file hash does not match downloadValueMismatch = true; if (debugLogging) { addLogEntry("Actual local file hash: " ~ downloadedFileHash, ["debug"]); addLogEntry("OneDrive API reported hash: " ~ onlineFileHash, ["debug"]); } addLogEntry("ERROR: File download hash mismatch. Increase logging verbosity to determine why."); } // .heic data loss check // - https://github.com/abraunegg/onedrive/issues/2471 // - https://github.com/OneDrive/onedrive-api-docs/issues/1532 // - https://github.com/OneDrive/onedrive-api-docs/issues/1723 if (downloadValueMismatch && (toLower(extension(newItemPath)) == ".heic")) { // Need to display a message to the user that they have experienced data loss addLogEntry("DATA-LOSS: File downloaded has experienced data loss due to a Microsoft OneDrive API bug. DO NOT DELETE THIS FILE ONLINE: " ~ newItemPath, ["info", "notify"]); if (verboseLogging) {addLogEntry(" Please read https://github.com/OneDrive/onedrive-api-docs/issues/1723 for more details.", ["verbose"]);} } // Add some workaround messaging for SharePoint if (appConfig.accountType == "documentLibrary"){ // It has been seen where SharePoint / OneDrive API reports one size via the JSON // but the content length and file size written to disk is totally different - example: // From JSON: "size": 17133 // From HTTPS Server: < Content-Length: 19340 // with no logical reason for the difference, except for a 302 redirect before file download addLogEntry("INFO: It is most likely that a SharePoint OneDrive API issue is the root cause. Add --disable-download-validation to work around this issue but downloaded data integrity cannot be guaranteed."); } else { // other account types addLogEntry("INFO: Potentially add --disable-download-validation to work around this issue but downloaded data integrity cannot be guaranteed."); } // If the computed hash does not equal provided online hash, consider this a failed download if (downloadedFileHash != onlineFileHash) { // We do not want this local file to remain on the local file system as it failed the integrity checks addLogEntry("Removing local file " ~ newItemPath ~ " due to failed integrity checks"); if (!dryRun) { safeRemove(newItemPath); } // Was this item previously in-sync with the local system? // We previously searched for the file in the DB, we need to use that record if (fileFoundInDB) { // Purge DB record so that the deleted local file does not cause an online deletion // In a --dry-run scenario, this is being done against a DB copy addLogEntry("Removing DB record due to failed integrity checks"); itemDB.deleteById(databaseItem.driveId, databaseItem.id); } // Flag that the download failed downloadFailed = true; } } } else { // Download validation checks were disabled if (debugLogging) {addLogEntry("Downloaded file validation disabled due to --disable-download-validation", ["debug"]);} if (verboseLogging) {addLogEntry("WARNING: Skipping download integrity check for: " ~ newItemPath, ["verbose"]);} // Whilst the download integrity checks were disabled, we still have to set the correct timestamp on the file // Set the timestamp, logging and error handling done within function setPathTimestamp(dryRun, newItemPath, itemModifiedTime); // Azure Information Protection (AIP) protected files potentially have missing data and/or inconsistent data if (appConfig.accountType != "personal") { // AIP Protected Files cause issues here, as the online size & hash are not what has been downloaded // There is ZERO way to determine if this is an AIP protected file either from the JSON data // Calculate the local file hash and get the local file size string localFileHash = computeQuickXorHash(newItemPath); long downloadFileSize = getSize(newItemPath); if ((OneDriveFileXORHash != localFileHash) && (jsonFileSize != downloadFileSize)) { // High potential to be an AIP protected file given the following scenario // Business | SharePoint Account Type (not a personal account) // --disable-download-validation is being used .. meaning the user has specifically configured this due the Microsoft SharePoint Enrichment Feature (bug) // The file downloaded but the XOR hash and file size locally is not as per the provided JSON - both are different // // Update the 'onedriveJSONItem' JSON data with the local values ..... if (debugLogging) { string aipLogMessage = format("POTENTIAL AIP FILE (Issue 3070) - Changing the source JSON data provided by Graph API to use actual on-disk values (quickXorHash,size): %s", newItemPath); addLogEntry(aipLogMessage, ["debug"]); addLogEntry(" - Online XOR : " ~ to!string(OneDriveFileXORHash), ["debug"]); addLogEntry(" - Online Size : " ~ to!string(jsonFileSize), ["debug"]); addLogEntry(" - Local XOR : " ~ to!string(computeQuickXorHash(newItemPath)), ["debug"]); addLogEntry(" - Local Size : " ~ to!string(getSize(newItemPath)), ["debug"]); } // Make the change in the JSON using local values onedriveJSONItem["file"]["hashes"]["quickXorHash"] = localFileHash; onedriveJSONItem["size"] = downloadFileSize; } } } // end of (!disableDownloadValidation) } else { addLogEntry("ERROR: File failed to download. Increase logging verbosity to determine why."); // Was this item previously in-sync with the local system? // We previously searched for the file in the DB, we need to use that record if (fileFoundInDB) { // Purge DB record so that the deleted local file does not cause an online deletion // In a --dry-run scenario, this is being done against a DB copy addLogEntry("Removing existing DB record due to failed file download."); itemDB.deleteById(databaseItem.driveId, databaseItem.id); } // Flag that the download failed downloadFailed = true; } } } // File should have been downloaded if (!downloadFailed) { // Download did not fail addLogEntry("Downloading file: " ~ newItemPath ~ " ... done", fileTransferNotifications()); // As no download failure, calculate transfer metrics in a consistent manner displayTransferMetrics(newItemPath, jsonFileSize, downloadStartTime, Clock.currTime()); // Save this item into the database saveItem(onedriveJSONItem); // If we are in a --dry-run situation - if we are, we need to track that we faked the download if (dryRun) { // track that we 'faked it' idsFaked ~= [downloadDriveId, downloadItemId]; } // If, the initial download failed, but, during the 'Performing a last examination of the most recent online data within Microsoft OneDrive' Process // the file downloads without issue, check if the path is in 'fileDownloadFailures' and if this is in this array, remove this entry as it is technically no longer valid to be in there if (canFind(fileDownloadFailures, newItemPath)) { // Remove 'newItemPath' from 'fileDownloadFailures' as this is no longer a failed download fileDownloadFailures = fileDownloadFailures.filter!(item => item != newItemPath).array; } // Did the user configure to save xattr data about this file? if (appConfig.getValueBool("write_xattr_data")) { writeXattrData(newItemPath, onedriveJSONItem); } } else { // Output download failed addLogEntry("Downloading file: " ~ newItemPath ~ " ... failed!", ["info", "notify"]); // Add the path to a list of items that failed to download if (!canFind(fileDownloadFailures, newItemPath)) { fileDownloadFailures ~= newItemPath; // Add newItemPath if it's not already present } } } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Write xattr data if configured to do so void writeXattrData(string filePath, JSONValue onedriveJSONItem) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // This function will write the following xattr attributes based on the JSON data received from Microsoft onedrive // - createdBy using the 'displayName' value // - lastModifiedBy using the 'displayName' value string createdBy; string lastModifiedBy; // Configure 'createdBy' from the JSON data if (hasCreatedByUserDisplayName(onedriveJSONItem)) { createdBy = onedriveJSONItem["createdBy"]["user"]["displayName"].str; } else { // required data not in JSON data createdBy = "Unknown"; } // Configure 'lastModifiedBy' from the JSON data if (hasLastModifiedByUserDisplayName(onedriveJSONItem)) { lastModifiedBy = onedriveJSONItem["lastModifiedBy"]["user"]["displayName"].str; } else { // required data not in JSON data lastModifiedBy = "Unknown"; } // Set the xattr values setXAttr(filePath, "user.onedrive.createdBy", createdBy); setXAttr(filePath, "user.onedrive.lastModifiedBy", lastModifiedBy); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Test if the given item is in-sync. Returns true if the given item corresponds to the local one bool isItemSynced(Item item, string path, string itemSource) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Due to this function, we need to keep the return ; code, so that this function operates as efficiently as possible. // It is pointless having the entire code run through and performing additional needless checks where it is not required // Whilst this means some extra code / duplication in this function, it cannot be helped if (!exists(path)) { if (debugLogging) {addLogEntry("Unable to determine the sync state of this file as it does not exist: " ~ path, ["debug"]);} // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } return false; } // Combine common logic for readability and file check into a single block if (item.type == ItemType.file || ((item.type == ItemType.remote) && (item.remoteType == ItemType.file))) { // Can we actually read the local file? if (!readLocalFile(path)) { // Unable to read local file addLogEntry("Unable to determine the sync state of this file as it cannot be read (file permissions or file corruption): " ~ path); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } return false; } // Get time values SysTime localModifiedTime = timeLastModified(path).toUTC(); SysTime itemModifiedTime = item.mtime; // Reduce time resolution to seconds before comparing localModifiedTime.fracSecs = Duration.zero; itemModifiedTime.fracSecs = Duration.zero; if (localModifiedTime == itemModifiedTime) { // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } return true; } else { // The file has a different timestamp ... is the hash the same meaning no file modification? if (verboseLogging) { addLogEntry("Local file time discrepancy detected: " ~ path, ["verbose"]); addLogEntry("This local file has a different modified time " ~ to!string(localModifiedTime) ~ " (UTC) when compared to " ~ itemSource ~ " modified time " ~ to!string(itemModifiedTime) ~ " (UTC)", ["verbose"]); } // The file has a different timestamp ... is the hash the same meaning no file modification? // Test the file hash as the date / time stamp is different // Generating a hash is computationally expensive - we only generate the hash if timestamp was different if (testFileHash(path, item)) { // The hash is the same .. so we need to fix-up the timestamp depending on where it is wrong if (verboseLogging) {addLogEntry("Local item has the same hash value as the item online - correcting the applicable file timestamp", ["verbose"]);} // Correction logic based on the configuration and the comparison of timestamps if (localModifiedTime > itemModifiedTime) { // Local file is newer timestamp wise, but has the same hash .. are we in a --download-only situation? if (!appConfig.getValueBool("download_only") && !dryRun) { // Not --download-only .. but are we in a --resync scenario? if (appConfig.getValueBool("resync")) { // --resync was used // The source of the out-of-date timestamp was the local item and needs to be corrected ... but why is it newer - indexing application potentially changing the timestamp ? if (verboseLogging) {addLogEntry("The source of the incorrect timestamp was the local file - correcting timestamp locally due to --resync", ["verbose"]);} // Fix the timestamp, logging and error handling done within function setPathTimestamp(dryRun, path, item.mtime); } else { // The source of the out-of-date timestamp was OneDrive and this needs to be corrected to avoid always generating a hash test if timestamp is different if (verboseLogging) {addLogEntry("The source of the incorrect timestamp was OneDrive online - correcting timestamp online", ["verbose"]);} // Attempt to update the online date time stamp // We need to use the correct driveId and itemId, especially if we are updating a OneDrive Business Shared File timestamp if (item.type == ItemType.file) { // Not a remote file uploadLastModifiedTime(item, item.driveId, item.id, localModifiedTime, item.eTag); } else { // Remote file, remote values need to be used uploadLastModifiedTime(item, item.remoteDriveId, item.remoteId, localModifiedTime, item.eTag); } } } else if (!dryRun) { // --download-only is being used ... local file needs to be corrected ... but why is it newer - indexing application potentially changing the timestamp ? if (verboseLogging) {addLogEntry("The source of the incorrect timestamp was the local file - correcting timestamp locally due to --download-only", ["verbose"]);} // Fix the timestamp, logging and error handling done within function setPathTimestamp(dryRun, path, item.mtime); } } else if (!dryRun) { // The source of the out-of-date timestamp was the local file and this needs to be corrected to avoid always generating a hash test if timestamp is different if (verboseLogging) {addLogEntry("The source of the incorrect timestamp was the local file - correcting timestamp locally", ["verbose"]);} // Fix the timestamp, logging and error handling done within function setPathTimestamp(dryRun, path, item.mtime); } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } return false; } else { // The hash is different so the content of the file has to be different as to what is stored online if (verboseLogging) {addLogEntry("The local file has a different hash when compared to " ~ itemSource ~ " file hash", ["verbose"]);} // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } return false; } } } else if (item.type == ItemType.dir || ((item.type == ItemType.remote) && (item.remoteType == ItemType.dir))) { // item is a directory // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } return true; } else { // ItemType.unknown or ItemType.none // Logically, we might not want to sync these items, but a more nuanced approach may be needed based on application context // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } return true; } } // Get the /delta data using the provided details JSONValue getDeltaChangesByItemId(string selectedDriveId, string selectedItemId, string providedDeltaLink, OneDriveApi getDeltaQueryOneDriveApiInstance) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Function variables JSONValue deltaChangesBundle; // Get the /delta data for this account | driveId | deltaLink combination if (debugLogging) { addLogEntry(debugLogBreakType1, ["debug"]); addLogEntry("selectedDriveId: " ~ selectedDriveId, ["debug"]); addLogEntry("selectedItemId: " ~ selectedItemId, ["debug"]); addLogEntry("providedDeltaLink: " ~ providedDeltaLink, ["debug"]); addLogEntry(debugLogBreakType1, ["debug"]); } try { deltaChangesBundle = getDeltaQueryOneDriveApiInstance.getChangesByItemId(selectedDriveId, selectedItemId, providedDeltaLink); } catch (OneDriveException exception) { // caught an exception if (debugLogging) {addLogEntry("getDeltaQueryOneDriveApiInstance.getChangesByItemId(selectedDriveId, selectedItemId, providedDeltaLink) generated a OneDriveException", ["debug"]);} // get the error message auto errorArray = splitLines(exception.msg); // Error handling operation if not 408,429,503,504 errors // - 408,429,503,504 errors are handled as a retry within getDeltaQueryOneDriveApiInstance if (exception.httpStatusCode == 410) { addLogEntry(); addLogEntry("WARNING: The OneDrive API responded with an error that indicates the locally stored deltaLink value is invalid"); // Essentially the 'providedDeltaLink' that we have stored is no longer available ... re-try without the stored deltaLink addLogEntry("WARNING: Retrying OneDrive API call without using the locally stored deltaLink value"); // Configure an empty deltaLink if (debugLogging) {addLogEntry("Delta link expired for 'getDeltaQueryOneDriveApiInstance.getChangesByItemId(selectedDriveId, selectedItemId, providedDeltaLink)', setting 'deltaLink = null'", ["debug"]);} string emptyDeltaLink = ""; // retry with empty deltaLink deltaChangesBundle = getDeltaQueryOneDriveApiInstance.getChangesByItemId(selectedDriveId, selectedItemId, emptyDeltaLink); } else { // Display what the error is addLogEntry("CODING TO DO: Hitting this failure error output after getting a httpStatusCode != 410 when the API responded the deltaLink was invalid"); displayOneDriveErrorMessage(exception.msg, thisFunctionName); deltaChangesBundle = null; // Perform Garbage Collection GC.collect(); } } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // Return data return deltaChangesBundle; } // If the JSON response is not correct JSON object, exit void invalidJSONResponseFromOneDriveAPI() { addLogEntry("ERROR: Query of the OneDrive API returned an invalid JSON response"); // Must force exit here, allow logging to be done forceExit(); } // Handle an unhandled API error void defaultUnhandledHTTPErrorCode(OneDriveException exception) { // compute function name string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // display error displayOneDriveErrorMessage(exception.msg, thisFunctionName); // Must force exit here, allow logging to be done forceExit(); } // Display the pertinent details of the sync engine void displaySyncEngineDetails() { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Display accountType, defaultDriveId, defaultRootId & remainingFreeSpace for verbose logging purposes addLogEntry("Application Version: " ~ appConfig.applicationVersion, ["verbose"]); addLogEntry("Account Type: " ~ appConfig.accountType, ["verbose"]); addLogEntry("Default Drive ID: " ~ appConfig.defaultDriveId, ["verbose"]); addLogEntry("Default Root ID: " ~ appConfig.defaultRootId, ["verbose"]); addLogEntry("Microsoft Data Centre: " ~ microsoftDataCentre, ["verbose"]); // Fetch the details from cachedOnlineDriveData DriveDetailsCache cachedOnlineDriveData; cachedOnlineDriveData = getDriveDetails(appConfig.defaultDriveId); // What do we display here for space remaining if (cachedOnlineDriveData.quotaRemaining > 0) { // Display the actual value addLogEntry("Remaining Free Space: " ~ to!string(byteToGibiByte(cachedOnlineDriveData.quotaRemaining)) ~ " GB (" ~ to!string(cachedOnlineDriveData.quotaRemaining) ~ " bytes)", ["verbose"]); } else { // zero or non-zero value or restricted if (!cachedOnlineDriveData.quotaRestricted){ addLogEntry("Remaining Free Space: 0 KB", ["verbose"]); } else { addLogEntry("Remaining Free Space: Not Available", ["verbose"]); } } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Query itemdb.computePath() and catch potential assert when DB consistency issue occurs // This function returns what that local physical path should be on the local disk string computeItemPath(string thisDriveId, string thisItemId) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // static declare this for this function static import core.exception; string calculatedPath; string initialCalculatedPath; string fullCalculatedPath; bool calculateLocalExtension = false; // What driveID and itemID we trying to calculate the path for if (debugLogging) { string initialComputeLogMessage = format("Attempting to calculate initial local filesystem path for '%s' and '%s'", thisDriveId, thisItemId); addLogEntry(initialComputeLogMessage, ["debug"]); } // Perform the original calculation of the path using the values provided try { initialCalculatedPath = itemDB.computePath(thisDriveId, thisItemId); if (debugLogging) {addLogEntry("Initial calculated path = " ~ to!string(initialCalculatedPath), ["debug"]);} } catch (core.exception.AssertError) { // broken tree in the database, we cant compute the path for this item id, exit addLogEntry("ERROR: A database consistency issue has been caught. A --resync is needed to rebuild the database."); // Must force exit here, allow logging to be done forceExit(); } // To support OneDrive Shared Folders being stored anywhere (#2824) if 'thisDriveId' is not our account drive, we need to switch this up and calculate the path to the 'remote' item type DB object // By doing this, we calculate the local path correctly to account for the difference in Shared Folder root path details that start from that Shared Folder online // This then needs to be 'appended' to the shared drive calculation to get the full calculated local path which gest returned // Do we need to perform the local path extension for a shared folder? // - Is this potentially a shared folder that we are trying to compute the path for? This is the only reliable way to determine this ... // Both 'thisDriveId' and 'appConfig.defaultDriveId' have been validated for 15 character OneDrive Personal Account 'driveId' issue if (thisDriveId != appConfig.defaultDriveId) { // The driveId is not our account driveId if (debugLogging) {addLogEntry("The path we are trying to calculate extends to a OneDrive Shared Folder .. need to perform multiple calculations to calculate the full true local path", ["debug"]);} // Use the 'thisDriveId' value to obtain the 'remote' item type record which represents the local path junction point to the shared folder Item remoteEntryItem; string fullLocalPath; string localPathExtension; if (debugLogging) {addLogEntry("Attempting to calculate Shared Folder local filesystem path for " ~ thisDriveId ~ " and " ~ thisItemId, ["debug"]);} // Get the DB entry for this 'remote' item itemDB.selectRemoteTypeByRemoteDriveId(thisDriveId, thisItemId, remoteEntryItem); // What was returned from the Database? if (debugLogging) {addLogEntry("remoteEntryItem: " ~ to!string(remoteEntryItem), ["debug"]);} // What details do we use? if (!remoteEntryItem.driveId.empty) { // use the returned 'remote' DB entry values localPathExtension = itemDB.computePath(remoteEntryItem.driveId, remoteEntryItem.id); } else { // set 'localPathExtension' to initialCalculatedPath localPathExtension = initialCalculatedPath; } // result for localPathExtension if (debugLogging) {addLogEntry(" localPathExtension = " ~ to!string(localPathExtension), ["debug"]);} // what do we use? if (initialCalculatedPath == ".") { // The '.' represents the root shared folder ... fullLocalPath = localPathExtension; } else { // Now to combine the two // - replace remoteEntryItem.name in 'initialCalculatedPath' with 'localPathExtension' fullLocalPath = initialCalculatedPath.replace(remoteEntryItem.name, localPathExtension); } if (debugLogging) {addLogEntry(" fullLocalPath = " ~ to!string(fullLocalPath), ["debug"]);} // Update calculatedPath calculatedPath = fullLocalPath; } else { // not a remote shared folder calculatedPath = initialCalculatedPath; } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return calculated path as string return calculatedPath; } // Try and compute the file hash for the given item bool testFileHash(string path, Item item) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Due to this function, we need to keep the return ; code, so that this function operates as efficiently as possible. // It is pointless having the entire code run through and performing additional needless checks where it is not required // Whilst this means some extra code / duplication in this function, it cannot be helped // Generate QuickXORHash first before attempting to generate any other type of hash if (item.quickXorHash) { if (item.quickXorHash == computeQuickXorHash(path)) { // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } return true; } } else if (item.sha256Hash) { if (item.sha256Hash == computeSHA256Hash(path)) { // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } return true; } } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } return false; } // Process items that need to be removed void processDeleteItems() { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } foreach_reverse (i; idsToDelete) { Item item; string path; if (!itemDB.selectById(i[0], i[1], item)) continue; // check if the item is in the db // Compute this item path path = computeItemPath(i[0], i[1]); // Log the action if the path exists .. it may of already been removed and this is a legacy array item if (exists(path)) { if (item.type == ItemType.file) { addLogEntry("Trying to delete local file: " ~ path); } else { addLogEntry("Trying to delete local directory: " ~ path); } } // Process the database entry removal. In a --dry-run scenario, this is being done against a DB copy itemDB.deleteById(item.driveId, item.id); if (item.remoteDriveId != null) { // delete the linked remote folder itemDB.deleteById(item.remoteDriveId, item.remoteId); } // Add to pathFakeDeletedArray // We dont want to try and upload this item again, so we need to track this objects removal if (dryRun) { // We need to add './' here so that it can be correctly searched to ensure it is not uploaded string pathToAdd = "./" ~ path; pathFakeDeletedArray ~= pathToAdd; } bool needsRemoval = false; if (exists(path)) { // path exists on the local system // make sure that the path refers to the correct item Item pathItem; if (itemDB.selectByPath(path, item.driveId, pathItem)) { if (pathItem.id == item.id) { needsRemoval = true; } else { addLogEntry("Skipped due to id difference!"); } } else { // item has disappeared completely needsRemoval = true; } } if (needsRemoval) { // Log the action if (item.type == ItemType.file) { addLogEntry("Deleting local file: " ~ path, fileTransferNotifications()); } else { addLogEntry("Deleting local directory: " ~ path, fileTransferNotifications()); } // Perform the action if (!dryRun) { if (isFile(path)) { remove(path); } else { try { // Remove any children of this path if they still exist // Resolve 'Directory not empty' error when deleting local files foreach (DirEntry child; dirEntries(path, SpanMode.depth, false)) { attrIsDir(child.linkAttributes) ? rmdir(child.name) : remove(child.name); } // Remove the path now that it is empty of children rmdirRecurse(path); } catch (FileException e) { // display the error message displayFileSystemErrorMessage(e.msg, thisFunctionName); } } } } } if (!dryRun) { // Cleanup array memory idsToDelete = []; } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // List items that were deleted online, but, due to --download-only being used, will not be deleted locally void listDeletedItems() { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // For each id in the idsToDelete array foreach_reverse (i; idsToDelete) { Item item; string path; if (!itemDB.selectById(i[0], i[1], item)) continue; // check if the item is in the db // Compute this item path path = computeItemPath(i[0], i[1]); // Log the action if the path exists .. it may of already been removed and this is a legacy array item if (exists(path)) { if (item.type == ItemType.file) { if (verboseLogging) {addLogEntry("Skipping local deletion for file " ~ path, ["verbose"]);} } else { if (verboseLogging) {addLogEntry("Skipping local deletion for directory " ~ path, ["verbose"]);} } } } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Update the timestamp of an object online void uploadLastModifiedTime(Item originItem, string driveId, string id, SysTime mtime, string eTag) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } string itemModifiedTime; itemModifiedTime = mtime.toISOExtString(); JSONValue data = [ "fileSystemInfo": JSONValue([ "lastModifiedDateTime": itemModifiedTime ]) ]; // What eTag value do we use? string eTagValue; if (appConfig.accountType == "personal") { // Nullify the eTag to avoid 412 errors as much as possible eTagValue = null; } else { eTagValue = eTag; } JSONValue response; OneDriveApi uploadLastModifiedTimeApiInstance; // Try and update the online last modified time try { // Create a new OneDrive API instance uploadLastModifiedTimeApiInstance = new OneDriveApi(appConfig); uploadLastModifiedTimeApiInstance.initialise(); // Use this instance response = uploadLastModifiedTimeApiInstance.updateById(driveId, id, data, eTagValue); // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory uploadLastModifiedTimeApiInstance.releaseCurlEngine(); uploadLastModifiedTimeApiInstance = null; // Perform Garbage Collection GC.collect(); // Do we actually save the response? // Special case here .. if the DB record item (originItem) is a remote object, thus, if we save the 'response' we will have a DB FOREIGN KEY constraint failed problem // Update 'originItem.mtime' with the correct timestamp // Update 'originItem.size' with the correct size from the response // Update 'originItem.eTag' with the correct eTag from the response // Update 'originItem.cTag' with the correct cTag from the response // Update 'originItem.quickXorHash' with the correct quickXorHash from the response // Everything else should remain the same .. and then save this DB record to the DB .. // However, we did this, for the local modified file right before calling this function to update the online timestamp ... so .. do we need to do this again, effectively performing a double DB write for the same data? if ((originItem.type != ItemType.remote) && (originItem.remoteType != ItemType.file)) { // Save the response JSON // Is the response a valid JSON object - validation checking done in saveItem saveItem(response); } } catch (OneDriveException exception) { // Handle a 409 - ETag does not match current item's value // Handle a 412 - A precondition provided in the request (such as an if-match header) does not match the resource's current state. if ((exception.httpStatusCode == 409) || (exception.httpStatusCode == 412)) { // Handle the 409 if (exception.httpStatusCode == 409) { // OneDrive threw a 412 error if (verboseLogging) {addLogEntry("OneDrive returned a 'HTTP 409 - ETag does not match current item's value' when attempting file time stamp update - gracefully handling error", ["verbose"]);} if (debugLogging) { addLogEntry("File Metadata Update Failed - OneDrive eTag / cTag match issue", ["debug"]); addLogEntry("Retrying Function: " ~ thisFunctionName, ["debug"]); } } // Handle the 412 if (exception.httpStatusCode == 412) { // OneDrive threw a 412 error if (verboseLogging) {addLogEntry("OneDrive returned a 'HTTP 412 - Precondition Failed' when attempting file time stamp update - gracefully handling error", ["verbose"]);} if (debugLogging) { addLogEntry("File Metadata Update Failed - OneDrive eTag / cTag match issue", ["debug"]); addLogEntry("Retrying Function: " ~ thisFunctionName, ["debug"]); } } // Retry without eTag uploadLastModifiedTime(originItem, driveId, id, mtime, null); } else { // Any other error that should be handled // - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance // Display what the error is displayOneDriveErrorMessage(exception.msg, thisFunctionName); } // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory uploadLastModifiedTimeApiInstance.releaseCurlEngine(); uploadLastModifiedTimeApiInstance = null; // Perform Garbage Collection GC.collect(); } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Perform a database integrity check - checking all the items that are in-sync at the moment, validating what we know should be on disk, to what is actually on disk void performDatabaseConsistencyAndIntegrityCheck() { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Log what we are doing if (!appConfig.suppressLoggingOutput) { addProcessingLogHeaderEntry("Performing a database consistency and integrity check on locally stored data", appConfig.verbosityCount); } // What driveIDsArray do we use? If we are doing a --single-directory we need to use just the drive id associated with that operation string[] consistencyCheckDriveIdsArray; if (singleDirectoryScope) { consistencyCheckDriveIdsArray ~= singleDirectoryScopeDriveId; } else { // Query the DB for all unique DriveID's consistencyCheckDriveIdsArray = itemDB.selectDistinctDriveIds(); } // Create a new DB blank item Item item; // Use the array we populate, rather than selecting all distinct driveId's from the database foreach (driveId; consistencyCheckDriveIdsArray) { // Make the logging more accurate - we cant update driveId as this then breaks the below queries if (verboseLogging) {addLogEntry("Processing DB entries for this Drive ID: " ~ driveId, ["verbose"]);} // Initialise the array Item[] driveItems = []; // Freshen the cached quota details for this driveID addOrUpdateOneDriveOnlineDetails(driveId); // What OneDrive API query do we use? // - Are we running against a National Cloud Deployments that does not support /delta ? // National Cloud Deployments do not support /delta as a query // https://docs.microsoft.com/en-us/graph/deployments#supported-features // // - Are we performing a --single-directory sync, which will exclude many items online, focusing in on a specific online directory // - Are we performing a --download-only --cleanup-local-files action? // - Are we scanning a Shared Folder // // If we did, we self generated a /delta response, thus need to now process elements that are still flagged as out-of-sync if ((singleDirectoryScope) || (nationalCloudDeployment) || (cleanupLocalFiles) || sharedFolderDeltaGeneration) { // Any entry in the DB than is flagged as out-of-sync needs to be cleaned up locally first before we scan the entire DB // Normally, this is done at the end of processing all /delta queries, however when using --single-directory or a National Cloud Deployments is configured // We cant use /delta to query the OneDrive API as National Cloud Deployments dont support /delta // https://docs.microsoft.com/en-us/graph/deployments#supported-features // We dont use /delta for --single-directory as, in order to sync a single path with /delta, we need to query the entire OneDrive API JSON data to then filter out // objects that we dont want, thus, it is easier to use the same method as National Cloud Deployments, but query just the objects we are after // For each unique OneDrive driveID we know about Item[] outOfSyncItems = itemDB.selectOutOfSyncItems(driveId); foreach (outOfSyncItem; outOfSyncItems) { if (!dryRun) { // clean up idsToDelete idsToDelete.length = 0; assumeSafeAppend(idsToDelete); // flag to delete local file as it now is no longer in sync with OneDrive if (debugLogging) { addLogEntry("Flagging to delete local item as it now is no longer in sync with OneDrive", ["debug"]); addLogEntry("outOfSyncItem: " ~ to!string(outOfSyncItem), ["debug"]); } idsToDelete ~= [outOfSyncItem.driveId, outOfSyncItem.id]; // delete items in idsToDelete if (idsToDelete.length > 0) processDeleteItems(); } } // Clear array outOfSyncItems = []; // Fetch database items associated with this path if (singleDirectoryScope) { // Use the --single-directory items we previously configured // - query database for children objects using those items driveItems = getChildren(singleDirectoryScopeDriveId, singleDirectoryScopeItemId); } else { // Check everything associated with each driveId we know about if (debugLogging) {addLogEntry("Selecting DB items via itemDB.selectByDriveId(driveId)", ["debug"]);} // Query database driveItems = itemDB.selectByDriveId(driveId); } // Log DB items to process if (debugLogging) {addLogEntry("Database items to process for this driveId: " ~ to!string(driveItems.count), ["debug"]);} // Process each database item associated with the driveId foreach(dbItem; driveItems) { // Does it still exist on disk in the location the DB thinks it is checkDatabaseItemForConsistency(dbItem); } } else { // Check everything associated with each driveId we know about if (debugLogging) {addLogEntry("Selecting DB items via itemDB.selectByDriveId(driveId)", ["debug"]);} // Query database driveItems = itemDB.selectByDriveId(driveId); if (debugLogging) {addLogEntry("Database items to process for this driveId: " ~ to!string(driveItems.count), ["debug"]);} // Process each database item associated with the driveId foreach(dbItem; driveItems) { // Does it still exist on disk in the location the DB thinks it is checkDatabaseItemForConsistency(dbItem); } } // Clear the array driveItems = []; } // Close out the '....' being printed to the console if (!appConfig.suppressLoggingOutput) { if (appConfig.verbosityCount == 0) { completeProcessingDots(); } } // Are we doing a --download-only sync? if (!appConfig.getValueBool("download_only")) { // Do we have any known items, where they have been deleted locally, that now need to be deleted online? if (databaseItemsToDeleteOnline.length > 0) { // There are items to delete online addLogEntry("Deleted local items to delete on Microsoft OneDrive: " ~ to!string(databaseItemsToDeleteOnline.length)); foreach(localItemToDeleteOnline; databaseItemsToDeleteOnline) { // Upload to OneDrive the instruction to delete this item. This will handle the 'noRemoteDelete' flag if set uploadDeletedItem(localItemToDeleteOnline.dbItem, localItemToDeleteOnline.localFilePath); } // Cleanup array memory databaseItemsToDeleteOnline = []; } // Do we have any known items, where the content has changed locally, that needs to be uploaded? if (databaseItemsWhereContentHasChanged.length > 0) { // There are changed local files that were in the DB to upload addLogEntry("Changed local items to upload to Microsoft OneDrive: " ~ to!string(databaseItemsWhereContentHasChanged.length)); processChangedLocalItemsToUpload(); // Cleanup array memory databaseItemsWhereContentHasChanged = []; } } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Check this Database Item for its consistency on disk void checkDatabaseItemForConsistency(Item dbItem) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Due to this function, we need to keep the return ; code, so that this function operates as efficiently as possible. // It is pointless having the entire code run through and performing additional needless checks where it is not required // Whilst this means some extra code / duplication in this function, it cannot be helped // What is the local path item string localFilePath; // Do we want to onward process this item? bool unwanted = false; // Remote directory items we can 'skip' if ((dbItem.type == ItemType.remote) && (dbItem.remoteType == ItemType.dir)) { // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return .. nothing to check here, no logging needed return; } // Compute this dbItem path early as we we use this path often localFilePath = buildNormalizedPath(computeItemPath(dbItem.driveId, dbItem.id)); // To improve logging output for this function, what is the 'logical path'? string logOutputPath; if (localFilePath == ".") { // get the configured sync_dir logOutputPath = buildNormalizedPath(appConfig.getValueString("sync_dir")); } else { // Use the path that was computed logOutputPath = localFilePath; } // Log what we are doing if (verboseLogging) {addLogEntry("Processing: " ~ logOutputPath, ["verbose"]);} // Add a processing '.' if (!appConfig.suppressLoggingOutput) { if (appConfig.verbosityCount == 0) { addProcessingDotEntry(); } } // Determine which action to take final switch (dbItem.type) { case ItemType.file: // Logging output result is handled by checkFileDatabaseItemForConsistency checkFileDatabaseItemForConsistency(dbItem, localFilePath); goto functionCompletion; case ItemType.dir, ItemType.root: // Logging output result is handled by checkDirectoryDatabaseItemForConsistency checkDirectoryDatabaseItemForConsistency(dbItem, localFilePath); goto functionCompletion; case ItemType.remote: // DB items that match: dbItem.remoteType == ItemType.dir - these should have been skipped above // This means that anything that hits here should be: dbItem.remoteType == ItemType.file checkFileDatabaseItemForConsistency(dbItem, localFilePath); goto functionCompletion; case ItemType.unknown: case ItemType.none: // Unknown type - we dont action these items goto functionCompletion; } // To correctly handle a switch|case statement we use goto post the switch|case statement as if 'break' is used, we never get to this point functionCompletion: // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Perform the database consistency check on this file item void checkFileDatabaseItemForConsistency(Item dbItem, string localFilePath) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // What is the source of this item data? string itemSource = "database"; // Does this item|file still exist on disk? if (exists(localFilePath)) { // Path exists locally, is this path a file? if (isFile(localFilePath)) { // Can we actually read the local file? if (readLocalFile(localFilePath)){ // File is readable SysTime localModifiedTime = timeLastModified(localFilePath).toUTC(); SysTime itemModifiedTime = dbItem.mtime; // Reduce time resolution to seconds before comparing itemModifiedTime.fracSecs = Duration.zero; localModifiedTime.fracSecs = Duration.zero; if (localModifiedTime != itemModifiedTime) { // The modified dates are different if (verboseLogging) {addLogEntry("Local file time discrepancy detected: " ~ localFilePath, ["verbose"]);} if (debugLogging) {addLogEntry("This local file has a different modified time " ~ to!string(localModifiedTime) ~ " (UTC) when compared to " ~ itemSource ~ " modified time " ~ to!string(itemModifiedTime) ~ " (UTC)", ["debug"]);} // Test the file hash if (!testFileHash(localFilePath, dbItem)) { // Is the local file 'newer' or 'older' (ie was an old file 'restored locally' by a different backup / replacement process?) if (localModifiedTime >= itemModifiedTime) { // Local file is newer if (!appConfig.getValueBool("download_only")) { if (verboseLogging) {addLogEntry("The file content has changed locally and has a newer timestamp, thus needs to be uploaded to OneDrive", ["verbose"]);} // Add to an array of files we need to upload as this file has changed locally in-between doing the /delta check and performing this check databaseItemsWhereContentHasChanged ~= [dbItem.driveId, dbItem.id, localFilePath]; } else { if (verboseLogging) {addLogEntry("The file content has changed locally and has a newer timestamp. The file will remain different to online file due to --download-only being used", ["verbose"]);} } } else { // Local file is older - data recovery process? something else? if (!appConfig.getValueBool("download_only")) { if (verboseLogging) {addLogEntry("The file content has changed locally and file now has a older timestamp. Uploading this file to OneDrive may potentially cause data-loss online", ["verbose"]);} // Add to an array of files we need to upload as this file has changed locally in-between doing the /delta check and performing this check databaseItemsWhereContentHasChanged ~= [dbItem.driveId, dbItem.id, localFilePath]; } else { if (verboseLogging) {addLogEntry("The file content has changed locally and file now has a older timestamp. The file will remain different to online file due to --download-only being used", ["verbose"]);} } } } else { // The file contents have not changed, but the modified timestamp has if (verboseLogging) {addLogEntry("The last modified timestamp has changed however the file content has not changed", ["verbose"]);} // Local file is newer .. are we in a --download-only situation? if (!appConfig.getValueBool("download_only")) { // Not a --download-only scenario if (!dryRun) { // Attempt to update the online date time stamp // We need to use the correct driveId and itemId, especially if we are updating a OneDrive Business Shared File timestamp if (dbItem.type == ItemType.file) { // Not a remote file // Log what is being done if (verboseLogging) {addLogEntry("The local item has the same hash value as the item online - correcting timestamp online", ["verbose"]);} // Correct timestamp uploadLastModifiedTime(dbItem, dbItem.driveId, dbItem.id, localModifiedTime.toUTC(), dbItem.eTag); } else { // Remote file, remote values need to be used, we may not even have permission to change timestamp, update local file if (verboseLogging) {addLogEntry("The local item has the same hash value as the item online, however file is a OneDrive Business Shared File - correcting local timestamp", ["verbose"]);} // Set the timestamp, logging and error handling done within function setPathTimestamp(dryRun, localFilePath, dbItem.mtime); } } } else { // --download-only being used if (verboseLogging) {addLogEntry("The local item has the same hash value as the item online - correcting local timestamp due to --download-only being used to ensure local file matches timestamp online", ["verbose"]);} // Set the timestamp, logging and error handling done within function setPathTimestamp(dryRun, localFilePath, dbItem.mtime); } } } else { // The file has not changed if (verboseLogging) {addLogEntry("The file has not changed", ["verbose"]);} } } else { //The file is not readable - skipped addLogEntry("Skipping processing this file as it cannot be read (file permissions or file corruption): " ~ localFilePath); } } else { // The item was a file but now is a directory if (verboseLogging) {addLogEntry("The item was a file but now is a directory", ["verbose"]);} } } else { // File does not exist locally, but is in our database as a dbItem containing all the data was passed into this function // If we are in a --dry-run situation - this file may never have existed as we never downloaded it if (!dryRun) { // Not --dry-run situation if (verboseLogging) {addLogEntry("The file has been deleted locally", ["verbose"]);} // Add this to the array to handle post checking all database items databaseItemsToDeleteOnline ~= [DatabaseItemsToDeleteOnline(dbItem, localFilePath)]; } else { // We are in a --dry-run situation, file appears to have been deleted locally - this file may never have existed locally as we never downloaded it due to --dry-run // Did we 'fake create it' as part of --dry-run ? bool idsFakedMatch = false; foreach (i; idsFaked) { if (i[1] == dbItem.id) { if (debugLogging) {addLogEntry("Matched faked file which is 'supposed' to exist but not created due to --dry-run use", ["debug"]);} if (verboseLogging) {addLogEntry("The file has not changed", ["verbose"]);} idsFakedMatch = true; } } if (!idsFakedMatch) { // dbItem.id did not match a 'faked' download new file creation - so this in-sync object was actually deleted locally, but we are in a --dry-run situation if (verboseLogging) {addLogEntry("The file has been deleted locally", ["verbose"]);} // Add this to the array to handle post checking all database items databaseItemsToDeleteOnline ~= [DatabaseItemsToDeleteOnline(dbItem, localFilePath)]; } } } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Perform the database consistency check on this directory item void checkDirectoryDatabaseItemForConsistency(Item dbItem, string localFilePath) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // What is the source of this item data? string itemSource = "database"; // Does this item|directory still exist on disk? if (exists(localFilePath)) { // Fix https://github.com/abraunegg/onedrive/issues/1915 try { if (!isDir(localFilePath)) { if (verboseLogging) {addLogEntry("The item was a directory but now it is a file", ["verbose"]);} uploadDeletedItem(dbItem, localFilePath); uploadNewFile(localFilePath); } else { // Directory still exists locally if (verboseLogging) {addLogEntry("The directory has not changed", ["verbose"]);} // When we are using --single-directory, we use the getChildren() call to get all children of a path, meaning all children are already traversed // Thus, if we traverse the path of this directory .. we end up with double processing & log output .. which is not ideal if (!singleDirectoryScope) { // loop through the children Item[] childrenFromDatabase = itemDB.selectChildren(dbItem.driveId, dbItem.id); foreach (Item child; childrenFromDatabase) { checkDatabaseItemForConsistency(child); } // Clear DB response array childrenFromDatabase = []; } } } catch (FileException e) { // display the error message displayFileSystemErrorMessage(e.msg, thisFunctionName); } } else { // Directory does not exist locally, but it is in our database as a dbItem containing all the data was passed into this function // If we are in a --dry-run situation - this directory may never have existed as we never created it if (!dryRun) { // Not --dry-run situation if (!appConfig.getValueBool("monitor")) { // Not in --monitor mode if (verboseLogging) {addLogEntry("The directory has been deleted locally", ["verbose"]);} } else { // Appropriate message as we are in --monitor mode if (verboseLogging) {addLogEntry("The directory appears to have been deleted locally .. but we are running in --monitor mode. This may have been 'moved' on the local filesystem rather than being 'deleted'", ["verbose"]);} if (debugLogging) {addLogEntry("Most likely cause - 'inotify' event was missing for whatever action was taken locally or action taken when application was stopped", ["debug"]);} } // A moved directory will be uploaded as 'new', delete the old directory and database reference // Add this to the array to handle post checking all database items databaseItemsToDeleteOnline ~= [DatabaseItemsToDeleteOnline(dbItem, localFilePath)]; } else { // We are in a --dry-run situation, directory appears to have been deleted locally - this directory may never have existed locally as we never created it due to --dry-run // Did we 'fake create it' as part of --dry-run ? bool idsFakedMatch = false; foreach (i; idsFaked) { if (i[1] == dbItem.id) { if (debugLogging) {addLogEntry("Matched faked dir which is 'supposed' to exist but not created due to --dry-run use", ["debug"]);} if (verboseLogging) {addLogEntry("The directory has not changed", ["verbose"]);} idsFakedMatch = true; } } if (!idsFakedMatch) { // dbItem.id did not match a 'faked' download new directory creation - so this in-sync object was actually deleted locally, but we are in a --dry-run situation if (verboseLogging) {addLogEntry("The directory has been deleted locally", ["verbose"]);} // Add this to the array to handle post checking all database items databaseItemsToDeleteOnline ~= [DatabaseItemsToDeleteOnline(dbItem, localFilePath)]; } else { // When we are using --single-directory, we use a the getChildren() call to get all children of a path, meaning all children are already traversed // Thus, if we traverse the path of this directory .. we end up with double processing & log output .. which is not ideal if (!singleDirectoryScope) { // loop through the children Item[] childrenFromDatabase = itemDB.selectChildren(dbItem.driveId, dbItem.id); foreach (Item child; childrenFromDatabase) { checkDatabaseItemForConsistency(child); } // Clear DB response array childrenFromDatabase = []; } } } } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Does this local path (directory or file) conform with the Microsoft Naming Restrictions? It needs to conform otherwise we cannot create the directory or upload the file. bool checkPathAgainstMicrosoftNamingRestrictions(string localFilePath, string logModifier = "item") { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Check if the given path violates certain Microsoft restrictions and limitations // Return a true|false response bool invalidPath = false; // Check path against Microsoft OneDrive restriction and limitations about Windows naming for files and folders if (!invalidPath) { if (!isValidName(localFilePath)) { // This will return false if this is not a valid name according to the OneDrive API specifications addLogEntry("Skipping " ~ logModifier ~" - invalid name (Microsoft Naming Convention): " ~ localFilePath, ["info", "notify"]); invalidPath = true; } } // Check path for bad whitespace items if (!invalidPath) { if (containsBadWhiteSpace(localFilePath)) { // This will return true if this contains a bad whitespace character addLogEntry("Skipping " ~ logModifier ~" - invalid name (Contains an invalid whitespace character): " ~ localFilePath, ["info", "notify"]); invalidPath = true; } } // Check path for HTML ASCII Codes if (!invalidPath) { if (containsASCIIHTMLCodes(localFilePath)) { // This will return true if this contains HTML ASCII Codes addLogEntry("Skipping " ~ logModifier ~" - invalid name (Contains HTML ASCII Code): " ~ localFilePath, ["info", "notify"]); invalidPath = true; } } // Validate that the path is a valid UTF-16 encoded path if (!invalidPath) { if (!isValidUTF16(localFilePath)) { // This will return true if this is a valid UTF-16 encoded path, so we are checking for 'false' as response addLogEntry("Skipping " ~ logModifier ~" - invalid name (Invalid UTF-16 encoded path): " ~ localFilePath, ["info", "notify"]); invalidPath = true; } } // Check path for ASCII Control Codes if (!invalidPath) { if (containsASCIIControlCodes(localFilePath)) { // This will return true if this contains ASCII Control Codes addLogEntry("Skipping " ~ logModifier ~" - invalid name (Contains ASCII Control Codes): " ~ localFilePath, ["info", "notify"]); invalidPath = true; } } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // Return if this is a valid path return invalidPath; } // Does this local path (directory or file) get excluded from any operation based on any client side filtering rules? bool checkPathAgainstClientSideFiltering(string localFilePath) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Check the path against client side filtering rules // - check_nosync // - skip_dotfiles // - skip_symlinks // - skip_file // - skip_dir // - sync_list // - skip_size // Return a true|false response bool clientSideRuleExcludesPath = false; // Reset global syncListDirExcluded syncListDirExcluded = false; // does the path exist? if (!exists(localFilePath)) { // path does not exist - we cant review any client side rules on something that does not exist locally return clientSideRuleExcludesPath; } // - check_nosync if (!clientSideRuleExcludesPath) { // Do we need to check for .nosync? Only if --check-for-nosync was passed in if (appConfig.getValueBool("check_nosync")) { if (exists(localFilePath ~ "/.nosync")) { if (verboseLogging) {addLogEntry("Skipping item - .nosync found & --check-for-nosync enabled: " ~ localFilePath, ["verbose"]);} clientSideRuleExcludesPath = true; } } } // - skip_dotfiles if (!clientSideRuleExcludesPath) { // Do we need to check skip dot files if configured if (appConfig.getValueBool("skip_dotfiles")) { if (isDotFile(localFilePath)) { if (verboseLogging) {addLogEntry("Skipping item - .file or .folder: " ~ localFilePath, ["verbose"]);} clientSideRuleExcludesPath = true; } } } // - skip_symlinks if (!clientSideRuleExcludesPath) { // Is the path a symbolic link if (isSymlink(localFilePath)) { // if config says so we skip all symlinked items if (appConfig.getValueBool("skip_symlinks")) { if (verboseLogging) {addLogEntry("Skipping item - skip symbolic links configured: " ~ localFilePath, ["verbose"]);} clientSideRuleExcludesPath = true; } // skip unexisting symbolic links else if (!exists(readLink(localFilePath))) { // reading the symbolic link failed - is the link a relative symbolic link // drwxrwxr-x. 2 alex alex 46 May 30 09:16 . // drwxrwxr-x. 3 alex alex 35 May 30 09:14 .. // lrwxrwxrwx. 1 alex alex 61 May 30 09:16 absolute.txt -> /home/alex/OneDrivePersonal/link_tests/intercambio/prueba.txt // lrwxrwxrwx. 1 alex alex 13 May 30 09:16 relative.txt -> ../prueba.txt // // absolute links will be able to be read, but 'relative' links will fail, because they cannot be read based on the current working directory 'sync_dir' string currentSyncDir = getcwd(); string fullLinkPath = buildNormalizedPath(absolutePath(localFilePath)); string fileName = baseName(fullLinkPath); string parentLinkPath = dirName(fullLinkPath); // test if this is a 'relative' symbolic link chdir(parentLinkPath); auto relativeLink = readLink(fileName); auto relativeLinkTest = exists(readLink(fileName)); // reset back to our 'sync_dir' chdir(currentSyncDir); // results if (relativeLinkTest) { if (debugLogging) {addLogEntry("Not skipping item - symbolic link is a 'relative link' to target ('" ~ relativeLink ~ "') which can be supported: " ~ localFilePath, ["debug"]);} } else { addLogEntry("Skipping item - invalid symbolic link: "~ localFilePath, ["info", "notify"]); clientSideRuleExcludesPath = true; } } } } // Is this item excluded by user configuration of skip_dir or skip_file? if (!clientSideRuleExcludesPath) { if (localFilePath != ".") { // skip_dir handling if (isDir(localFilePath)) { if (debugLogging) {addLogEntry("Checking local path: " ~ localFilePath, ["debug"]);} // Only check path if config is != "" if (appConfig.getValueString("skip_dir") != "") { // The path that needs to be checked needs to include the '/' // This due to if the user has specified in skip_dir an exclusive path: '/path' - that is what must be matched if (selectiveSync.isDirNameExcluded(localFilePath.strip('.'))) { if (verboseLogging) {addLogEntry("Skipping path - excluded by skip_dir config: " ~ localFilePath, ["verbose"]);} clientSideRuleExcludesPath = true; } } } // skip_file handling if (isFile(localFilePath)) { if (debugLogging) {addLogEntry("Checking file: " ~ localFilePath, ["debug"]);} // The path that needs to be checked needs to include the '/' // This due to if the user has specified in skip_file an exclusive path: '/path/file' - that is what must be matched if (selectiveSync.isFileNameExcluded(localFilePath.strip('.'))) { if (verboseLogging) {addLogEntry("Skipping file - excluded by skip_dir config: " ~ localFilePath, ["verbose"]);} clientSideRuleExcludesPath = true; } } } } // Is this item excluded by user configuration of sync_list? if (!clientSideRuleExcludesPath) { if (localFilePath != ".") { if (syncListConfigured) { // sync_list configured and in use if (selectiveSync.isPathExcludedViaSyncList(localFilePath)) { if ((isFile(localFilePath)) && (appConfig.getValueBool("sync_root_files")) && (rootName(localFilePath.strip('.').strip('/')) == "")) { if (debugLogging) {addLogEntry("Not skipping path due to sync_root_files inclusion: " ~ localFilePath, ["debug"]);} } else { if (exists(appConfig.syncListFilePath)){ // skipped most likely due to inclusion in sync_list // is this path a file or directory? if (isFile(localFilePath)) { // file if (verboseLogging) {addLogEntry("Skipping file - excluded by sync_list config: " ~ localFilePath, ["verbose"]);} } else { // directory if (verboseLogging) {addLogEntry("Skipping path - excluded by sync_list config: " ~ localFilePath, ["verbose"]);} // update syncListDirExcluded syncListDirExcluded = true; } // flag as excluded clientSideRuleExcludesPath = true; } else { // skipped for some other reason if (verboseLogging) {addLogEntry("Skipping path - excluded by user config: " ~ localFilePath, ["verbose"]);} clientSideRuleExcludesPath = true; } } } } } } // Check if this is excluded by a user set maximum filesize to upload if (!clientSideRuleExcludesPath) { if (isFile(localFilePath)) { if (fileSizeLimit != 0) { // Get the file size long thisFileSize = getSize(localFilePath); if (thisFileSize >= fileSizeLimit) { if (verboseLogging) {addLogEntry("Skipping file - excluded by skip_size config: " ~ localFilePath ~ " (" ~ to!string(thisFileSize/2^^20) ~ " MB)", ["verbose"]);} } } } } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return if path is excluded return clientSideRuleExcludesPath; } // Does this JSON item (as received from OneDrive API) get excluded from any operation based on any client side filtering rules? // This function is used when we are fetching objects from the OneDrive API using a /children query to help speed up what object we query or when checking OneDrive Business Shared Files bool checkJSONAgainstClientSideFiltering(JSONValue onedriveJSONItem) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } bool clientSideRuleExcludesPath = false; // Check the path against client side filtering rules // - check_nosync (MISSING) // - skip_dotfiles (MISSING) // - skip_symlinks (MISSING) // - skip_dir // - skip_file // - sync_list // - skip_size // Return a true|false response // Use the JSON elements rather than computing a DB struct via makeItem() string thisItemId = onedriveJSONItem["id"].str; string thisItemDriveId = onedriveJSONItem["parentReference"]["driveId"].str; string thisItemParentId = onedriveJSONItem["parentReference"]["id"].str; string thisItemName = onedriveJSONItem["name"].str; // Is this parent is in the database bool parentInDatabase = false; // Calculate if the Parent Item is in the database so that it can be re-used parentInDatabase = itemDB.idInLocalDatabase(thisItemDriveId, thisItemParentId); // Check if this is excluded by config option: skip_dir if (!clientSideRuleExcludesPath) { // Is the item a folder? if (isItemFolder(onedriveJSONItem)) { // Only check path if config is != "" if (!appConfig.getValueString("skip_dir").empty) { // work out the 'snippet' path where this folder would be created string simplePathToCheck = ""; string complexPathToCheck = ""; string matchDisplay = ""; if (hasParentReference(onedriveJSONItem)) { // we need to workout the FULL path for this item // simple path if (("name" in onedriveJSONItem["parentReference"]) != null) { simplePathToCheck = onedriveJSONItem["parentReference"]["name"].str ~ "/" ~ onedriveJSONItem["name"].str; } else { simplePathToCheck = onedriveJSONItem["name"].str; } if (debugLogging) {addLogEntry("skip_dir path to check (simple): " ~ simplePathToCheck, ["debug"]);} // complex path calculation if (parentInDatabase) { // build up complexPathToCheck based on database data complexPathToCheck = computeItemPath(thisItemDriveId, thisItemParentId) ~ "/" ~ thisItemName; if (debugLogging) {addLogEntry("skip_dir path to check (computed): " ~ complexPathToCheck, ["debug"]);} } else { if (debugLogging) {addLogEntry("Parent details not in database - unable to compute complex path to check using database data", ["debug"]);} // use onedriveJSONItem["parentReference"]["path"].str string selfBuiltPath = onedriveJSONItem["parentReference"]["path"].str ~ "/" ~ onedriveJSONItem["name"].str; // Check for ':' and split if present auto splitIndex = selfBuiltPath.indexOf(":"); if (splitIndex != -1) { // Keep only the part after ':' selfBuiltPath = selfBuiltPath[splitIndex + 1 .. $]; } // set complexPathToCheck to selfBuiltPath and be compatible with computeItemPath() output complexPathToCheck = "." ~ selfBuiltPath; } // were we able to compute a complexPathToCheck ? if (!complexPathToCheck.empty) { // complexPathToCheck must at least start with './' to ensure logging output consistency but also for pattern matching consistency if (!startsWith(complexPathToCheck, "./")) { complexPathToCheck = "./" ~ complexPathToCheck; } // log the complex path to check if (debugLogging) {addLogEntry("skip_dir path to check (complex): " ~ complexPathToCheck, ["debug"]);} } } else { simplePathToCheck = onedriveJSONItem["name"].str; } // If 'simplePathToCheck' or 'complexPathToCheck' is of the following format: root:/folder // then isDirNameExcluded matching will not work if (simplePathToCheck.canFind(":")) { if (debugLogging) {addLogEntry("Updating simplePathToCheck to remove 'root:'", ["debug"]);} simplePathToCheck = processPathToRemoveRootReference(simplePathToCheck); } if (complexPathToCheck.canFind(":")) { if (debugLogging) {addLogEntry("Updating complexPathToCheck to remove 'root:'", ["debug"]);} complexPathToCheck = processPathToRemoveRootReference(complexPathToCheck); } // OK .. what checks are we doing? if ((!simplePathToCheck.empty) && (complexPathToCheck.empty)) { // just a simple check if (debugLogging) {addLogEntry("Performing a simple check only", ["debug"]);} clientSideRuleExcludesPath = selectiveSync.isDirNameExcluded(simplePathToCheck); } else { // simple and complex if (debugLogging) {addLogEntry("Performing a simple then complex path match if required", ["debug"]);} // simple first if (debugLogging) {addLogEntry("Performing a simple check first", ["debug"]);} clientSideRuleExcludesPath = selectiveSync.isDirNameExcluded(simplePathToCheck); if (!clientSideRuleExcludesPath) { if (debugLogging) {addLogEntry("Simple match was false, attempting complex match", ["debug"]);} // simple didnt match, perform a complex check clientSideRuleExcludesPath = selectiveSync.isDirNameExcluded(complexPathToCheck); } } // End Result if (debugLogging) {addLogEntry("skip_dir exclude result (directory based): " ~ to!string(clientSideRuleExcludesPath), ["debug"]);} if (clientSideRuleExcludesPath) { // what path should be displayed if we are excluding if (!complexPathToCheck.empty) { // try and always use the complex path as it is more complete for application output matchDisplay = complexPathToCheck; } else { matchDisplay = simplePathToCheck; } // This path should be skipped if (verboseLogging) {addLogEntry("Skipping path - excluded by skip_dir config: " ~ matchDisplay, ["verbose"]);} } } } // Is the item a file? // We need to check to see if this files path is excluded as well if (isItemFile(onedriveJSONItem)) { // Only check path if config is != "" if (!appConfig.getValueString("skip_dir").empty) { // variable to check the file path against skip_dir string pathToCheck; if (hasParentReference(onedriveJSONItem)) { // use onedriveJSONItem["parentReference"]["path"].str string selfBuiltPath = onedriveJSONItem["parentReference"]["path"].str; // Check for ':' and split if present auto splitIndex = selfBuiltPath.indexOf(":"); if (splitIndex != -1) { // Keep only the part after ':' selfBuiltPath = selfBuiltPath[splitIndex + 1 .. $]; } // update file path to check against 'skip_dir' pathToCheck = selfBuiltPath; string logItemPath = "." ~ pathToCheck ~ "/" ~ onedriveJSONItem["name"].str; // perform the skip_dir check for file path clientSideRuleExcludesPath = selectiveSync.isDirNameExcluded(pathToCheck); // result if (debugLogging) {addLogEntry("skip_dir exclude result (file based): " ~ to!string(clientSideRuleExcludesPath), ["debug"]);} if (clientSideRuleExcludesPath) { // this files path should be skipped if (verboseLogging) {addLogEntry("Skipping file - file path is excluded by skip_dir config: " ~ logItemPath, ["verbose"]);} } } } } } // Check if this is excluded by config option: skip_file if (!clientSideRuleExcludesPath) { // is the item a file ? if (isFileItem(onedriveJSONItem)) { // JSON item is a file // skip_file can contain 4 types of entries: // - wildcard - *.txt // - text + wildcard - name*.txt // - full path + combination of any above two - /path/name*.txt // - full path to file - /path/to/file.txt string exclusionTestPath = ""; // is the parent id in the database? if (parentInDatabase) { // parent id is in the database, so we can try and calculate the full file path string jsonItemPath = ""; // Compute this item path & need the full path for this file jsonItemPath = computeItemPath(thisItemDriveId, thisItemParentId) ~ "/" ~ thisItemName; // Log the calculation if (debugLogging) {addLogEntry("New Item calculated full path is: " ~ jsonItemPath, ["debug"]);} // The path that needs to be checked needs to include the '/' // This due to if the user has specified in skip_file an exclusive path: '/path/file' - that is what must be matched // However, as 'path' used throughout, use a temp variable with this modification so that we use the temp variable for exclusion checks if (!startsWith(jsonItemPath, "/")){ // Add '/' to the path exclusionTestPath = '/' ~ jsonItemPath; } // what are we checking if (debugLogging) {addLogEntry("skip_file item to check (full calculated path): " ~ exclusionTestPath, ["debug"]);} } else { // parent not in database, we can only check using this JSON item's name if (!startsWith(thisItemName, "/")){ // Add '/' to the path exclusionTestPath = '/' ~ thisItemName; } // what are we checking if (debugLogging) {addLogEntry("skip_file item to check (file name only - parent path not in database): " ~ exclusionTestPath, ["debug"]);} } // Perform the 'skip_file' evaluation clientSideRuleExcludesPath = selectiveSync.isFileNameExcluded(exclusionTestPath); if (debugLogging) {addLogEntry("Result: " ~ to!string(clientSideRuleExcludesPath), ["debug"]);} if (clientSideRuleExcludesPath) { // This path should be skipped if (verboseLogging) {addLogEntry("Skipping file - excluded by skip_dir config: " ~ exclusionTestPath, ["verbose"]);} } } } // Check if this is included or excluded by use of sync_list if (!clientSideRuleExcludesPath) { // No need to try and process something against a sync_list if it has been configured if (syncListConfigured) { // Compute the item path if empty - as to check sync_list we need an actual path to check // What is the path of the new item string newItemPath; // Is the parent in the database? If not, we cannot compute the full path based on the database entries // In a --resync scenario - the database is empty if (parentInDatabase) { // Calculate this items path based on database entries if (debugLogging) {addLogEntry("Parent path details are in DB", ["debug"]);} newItemPath = computeItemPath(thisItemDriveId, thisItemParentId) ~ "/" ~ thisItemName; } else { // parent is not in the database .. we need to compute it .. why ???? if (appConfig.getValueBool("resync")) { if (debugLogging) {addLogEntry("Parent NOT in DB .. we need to manually compute this path due to --resync being used", ["debug"]);} } else { if (debugLogging) {addLogEntry("Parent NOT in DB .. we need to manually compute this path .......", ["debug"]);} } // gather the applicable path details if (("path" in onedriveJSONItem["parentReference"]) != null) { // If there is a parent reference path, try and use it string selfBuiltPath = onedriveJSONItem["parentReference"]["path"].str ~ "/" ~ onedriveJSONItem["name"].str; // Check for ':' and split if present string[] splitPaths; auto splitIndex = selfBuiltPath.indexOf(":"); if (splitIndex != -1) { // Keep only the part after ':' splitPaths = selfBuiltPath.split(":"); selfBuiltPath = splitPaths[1]; } // Issue #2731 // Get the remoteDriveId from JSON record string remoteDriveId = onedriveJSONItem["parentReference"]["driveId"].str; // Is this potentially a shared folder? This is the only reliable way to determine this ... if (remoteDriveId != appConfig.defaultDriveId) { // Yes this JSON is from a Shared Folder // Query the database for the 'remote' folder details from the database if (debugLogging) {addLogEntry("Query database for this 'remoteDriveId' record: " ~ to!string(remoteDriveId), ["debug"]);} Item remoteItem; itemDB.selectByRemoteDriveId(remoteDriveId, remoteItem); if (debugLogging) {addLogEntry("Query returned result (itemDB.selectByRemoteDriveId): " ~ to!string(remoteItem), ["debug"]);} // Update the path that will be used to check 'sync_list' with the 'name' of the remoteDriveId database record selfBuiltPath = remoteItem.name ~ selfBuiltPath; if (debugLogging) {addLogEntry("selfBuiltPath after 'Shared Folder' DB details update = " ~ to!string(selfBuiltPath), ["debug"]);} } // Issue #2740 // If selfBuiltPath is containing any sort of URL encoding, due to special characters (spaces, umlaut, or any other character that is HTML encoded, this specific path now needs to be HTML decoded // Does the path contain HTML encoding? if (containsURLEncodedItems(selfBuiltPath)) { // decode it if (debugLogging) {addLogEntry("selfBuiltPath for sync_list check needs decoding: " ~ selfBuiltPath, ["debug"]);} try { // try and decode selfBuiltPath newItemPath = decodeComponent(selfBuiltPath); } catch (URIException exception) { // why? addLogEntry("ERROR: Unable to URL Decode path: " ~ exception.msg); addLogEntry("ERROR: To resolve, rename this item online: " ~ selfBuiltPath); // have to use as-is due to decode error newItemPath = selfBuiltPath; } } else { // use as-is newItemPath = selfBuiltPath; } // The final format of newItemPath when self building needs to be the same as newItemPath when computed using computeItemPath .. this is handled later below if (debugLogging) {addLogEntry("newItemPath as manually computed by selfBuiltPath process = " ~ to!string(selfBuiltPath), ["debug"]);} } else { // no parent reference path available in provided JSON newItemPath = thisItemName; } } // The 'newItemPath' needs to be updated to ensure it is in the right format // Regardless of built from DB or computed it needs to be in this format: // ./path/path/ etc // This then makes the path output with 'sync_list' consistent, and, more importantly consistent for 'sync_list' evaluations newItemPath = ensureStartsWithDotSlash(newItemPath); // Check for HTML entities (e.g., '%20' for space) in newItemPath if (containsURLEncodedItems(newItemPath)) { addLogEntry("CAUTION: The JSON element transmitted by the Microsoft OneDrive API includes HTML URL encoded items, which may complicate pattern matching and potentially lead to synchronisation problems for this item."); if (verboseLogging) { addLogEntry("WORKAROUND: An alternative solution could be to change the name of this item through the online platform: " ~ newItemPath, ["verbose"]); addLogEntry("See: https://github.com/OneDrive/onedrive-api-docs/issues/1765 for further details", ["verbose"]); } } // What path are we checking against sync_list? if (debugLogging) {addLogEntry("Path to check against 'sync_list' entries: " ~ newItemPath, ["debug"]);} // Unfortunately there is no avoiding this call to check if the path is excluded|included via sync_list if (selectiveSync.isPathExcludedViaSyncList(newItemPath)) { // selective sync advised to skip, however is this a file and are we configured to upload / download files in the root? if ((isItemFile(onedriveJSONItem)) && (appConfig.getValueBool("sync_root_files")) && (rootName(newItemPath) == "") ) { // This is a file // We are configured to sync all files in the root // This is a file in the logical root clientSideRuleExcludesPath = false; } else { // Path is unwanted, flag to exclude clientSideRuleExcludesPath = true; // Has this itemId already been flagged as being skipped? if (!syncListSkippedParentIds.canFind(thisItemId)) { if (isItemFolder(onedriveJSONItem)) { // Detail we are skipping this JSON data from online if (verboseLogging) {addLogEntry("Skipping path - excluded by sync_list config: " ~ newItemPath, ["verbose"]);} // Add this folder id to the elements we have already detailed we are skipping, so we do no output this again syncListSkippedParentIds ~= thisItemId; } } // If this is a 'add shortcut to onedrive' link, we need to actually scan this path, so add this we need to pass this JSON if (isItemRemote(onedriveJSONItem)) { if (verboseLogging) {addLogEntry("Including shared folder shortcut for further analysis: " ~ newItemPath, ["verbose"]);} // reset this flag clientSideRuleExcludesPath = false; } } } else { // Is this a file or directory? if (isItemFile(onedriveJSONItem)) { // File included due to 'sync_list' match if (verboseLogging) {addLogEntry("Including file - included by sync_list config: " ~ newItemPath, ["verbose"]);} // Is the parent item in the database? if (!parentInDatabase) { // Parental database structure needs to be created string newParentalPath = dirName(newItemPath); // Log that this parental structure needs to be created if (verboseLogging) {addLogEntry("Parental Path structure needs to be created to support included file: " ~ newParentalPath, ["verbose"]);} // Recursively, stepping backward from 'thisItemParentId', query online, save entry to DB and create the local path structure createLocalPathStructure(onedriveJSONItem, newParentalPath); // If this is --dry-run if (dryRun) { // we dont create the directory, but we need to track that we 'faked it' idsFaked ~= [onedriveJSONItem["parentReference"]["driveId"].str, onedriveJSONItem["parentReference"]["id"].str]; } } } else { // Directory included due to 'sync_list' match if (verboseLogging) {addLogEntry("Including path - included by sync_list config: " ~ newItemPath, ["verbose"]);} } } } } // Check if this is excluded by a user set maximum filesize to download if (!clientSideRuleExcludesPath) { if (isItemFile(onedriveJSONItem)) { if (fileSizeLimit != 0) { if (onedriveJSONItem["size"].integer >= fileSizeLimit) { if (verboseLogging) {addLogEntry("Skipping file - excluded by skip_size config: " ~ thisItemName ~ " (" ~ to!string(onedriveJSONItem["size"].integer/2^^20) ~ " MB)", ["verbose"]);} clientSideRuleExcludesPath = true; } } } } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return if path is excluded return clientSideRuleExcludesPath; } // Ensure the path passed in, is in the correct format to use when evaluating 'sync_list' rules string ensureStartsWithDotSlash(string inputPath) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Check if the path starts with './' if (inputPath.startsWith("./")) { return inputPath; // No modification needed } // Check if the path starts with '/' or does not start with '.' at all if (inputPath.startsWith("/")) { return "." ~ inputPath; // Prepend '.' to ensure it starts with './' } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // If the path starts with any other character or is missing './', add './' return "./" ~ inputPath; } // When using 'sync_list' if a file is to be included, ensure that the path that the file resides in, is available locally and in the database, and the path exists locally void createLocalPathStructure(JSONValue onedriveJSONItem, string newLocalParentalPath) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Function variables bool parentInDatabase; JSONValue onlinePathData; OneDriveApi onlinePathOneDriveApiInstance; onlinePathOneDriveApiInstance = new OneDriveApi(appConfig); onlinePathOneDriveApiInstance.initialise(); string thisItemDriveId; string thisItemParentId; // Log what we received to analyse if (debugLogging) { addLogEntry("createLocalPathStructure input onedriveJSONItem: " ~ to!string(onedriveJSONItem), ["debug"]); addLogEntry("createLocalPathStructure input newLocalParentalPath: " ~ newLocalParentalPath, ["debug"]); } // Configure these variables based on the JSON input thisItemDriveId = onedriveJSONItem["parentReference"]["driveId"].str; // OneDrive Personal JSON responses are in-consistent with not having 'id' available if (hasParentReferenceId(onedriveJSONItem)) { // Use the parent reference id thisItemParentId = onedriveJSONItem["parentReference"]["id"].str; } // To continue, thisItemDriveId and thisItemParentId must not be empty if ((thisItemDriveId != "") && (thisItemParentId != "")) { // Calculate if the Parent Item is in the database so that it can be re-used parentInDatabase = itemDB.idInLocalDatabase(thisItemDriveId, thisItemParentId); // Is the parent in the database? if (!parentInDatabase) { // Get data from online for this driveId and JSON item parent .. so we have the parent details if (debugLogging) {addLogEntry("createLocalPathStructure parent is not in database, fetching parental details from online", ["debug"]);} try { onlinePathData = onlinePathOneDriveApiInstance.getPathDetailsById(thisItemDriveId, thisItemParentId); } catch (OneDriveException exception) { // Display what the error is // - 408,429,503,504 errors are handled as a retry within uploadFileOneDriveApiInstance displayOneDriveErrorMessage(exception.msg, thisFunctionName); } // Does this JSON match the root name of a shared folder we may be trying to match? if (sharedFolderDeltaGeneration) { if (currentSharedFolderName == onlinePathData["name"].str) { if (debugLogging) {addLogEntry("createLocalPathStructure parent matches the current shared folder name, creating applicable shared folder database records", ["debug"]);} // Create a 'root' and 'Shared Folder' DB Tie Records for this JSON object in a consistent manner createRequiredSharedFolderDatabaseRecords(onlinePathData); } } // Configure the grandparent items string grandparentItemDriveId; string grandparentItemParentId; grandparentItemDriveId = onlinePathData["parentReference"]["driveId"].str; // OneDrive Personal JSON responses are in-consistent with not having 'id' available if (hasParentReferenceId(onlinePathData)) { // Use the parent reference id grandparentItemParentId = onlinePathData["parentReference"]["id"].str; } else { // Testing evidence shows that for Personal accounts, use the 'id' itself grandparentItemParentId = onlinePathData["id"].str; } // Is this item's grandparent data in the database? if (!itemDB.idInLocalDatabase(grandparentItemDriveId, grandparentItemParentId)) { // grandparent needs to be added createLocalPathStructure(onlinePathData, dirName(newLocalParentalPath)); } // If this is --dry-run if (dryRun) { // we dont create the directory, but we need to track that we 'faked it' idsFaked ~= [onlinePathData["parentReference"]["driveId"].str, onlinePathData["parentReference"]["id"].str]; } // Does the parental path exist locally? if (!exists(newLocalParentalPath)) { // the required path does not exist locally - logging is done in handleLocalDirectoryCreation // create a db item record for the online data Item newDatabaseItem = makeItem(onlinePathData); // create the path locally, save the data to the database post path creation handleLocalDirectoryCreation(newDatabaseItem, newLocalParentalPath, onlinePathData); } else { // parent path exists locally, save the data to the database saveItem(onlinePathData); } } else { if (debugLogging) {addLogEntry("createLocalPathStructure parent is in the database", ["debug"]);} } } // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory onlinePathOneDriveApiInstance.releaseCurlEngine(); onlinePathOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Process the list of local changes to upload to OneDrive void processChangedLocalItemsToUpload() { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Each element in this array 'databaseItemsWhereContentHasChanged' is an Database Item ID that has been modified locally size_t batchSize = to!int(appConfig.getValueLong("threads")); long batchCount = (databaseItemsWhereContentHasChanged.length + batchSize - 1) / batchSize; long batchesProcessed = 0; // For each batch of files to upload, upload the changed data to OneDrive foreach (chunk; databaseItemsWhereContentHasChanged.chunks(batchSize)) { processChangedLocalItemsToUploadInParallel(chunk); } // For this set of items, perform a DB PASSIVE checkpoint itemDB.performCheckpoint("PASSIVE"); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Process all the changed local items in parallel void processChangedLocalItemsToUploadInParallel(string[3][] array) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // This function received an array of string items to upload, the number of elements based on appConfig.getValueLong("threads") foreach (i, localItemDetails; processPool.parallel(array)) { if (debugLogging) {addLogEntry("Upload Thread " ~ to!string(i) ~ " Starting: " ~ to!string(Clock.currTime()), ["debug"]);} uploadChangedLocalFileToOneDrive(localItemDetails); if (debugLogging) {addLogEntry("Upload Thread " ~ to!string(i) ~ " Finished: " ~ to!string(Clock.currTime()), ["debug"]);} } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Upload changed local files to OneDrive in parallel void uploadChangedLocalFileToOneDrive(string[3] localItemDetails) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // These are the details of the item we need to upload string changedItemParentId = localItemDetails[0]; string changedItemId = localItemDetails[1]; string localFilePath = localItemDetails[2]; // Log the path that was modified if (debugLogging) {addLogEntry("uploadChangedLocalFileToOneDrive: " ~ localFilePath, ["debug"]);} // How much space is remaining on OneDrive long remainingFreeSpace; // Did the upload fail? bool uploadFailed = false; // Did we skip due to exceeding maximum allowed size? bool skippedMaxSize = false; // Did we skip to an exception error? bool skippedExceptionError = false; // Flag for if space is available online bool spaceAvailableOnline = false; // Capture what time this upload started SysTime uploadStartTime = Clock.currTime(); // When we are uploading OneDrive Business Shared Files, we need to be targeting the right driveId and itemId string targetDriveId; string targetItemId; // Unfortunately, we cant store an array of Item's ... so we have to re-query the DB again - unavoidable extra processing here // This is because the Item[] has no other functions to allow is to parallel process those elements, so we have to use a string array as input to this function Item dbItem; itemDB.selectById(changedItemParentId, changedItemId, dbItem); // Is this a remote target? if ((dbItem.type == ItemType.remote) && (dbItem.remoteType == ItemType.file)) { // This is a remote file targetDriveId = dbItem.remoteDriveId; targetItemId = dbItem.remoteId; // we are going to make the assumption here that as this is a OneDrive Business Shared File, that there is space available spaceAvailableOnline = true; } else { // This is not a remote file targetDriveId = dbItem.driveId; targetItemId = dbItem.id; } // Fetch the details from cachedOnlineDriveData // - cachedOnlineDriveData.quotaRestricted; // - cachedOnlineDriveData.quotaAvailable; // - cachedOnlineDriveData.quotaRemaining; DriveDetailsCache cachedOnlineDriveData; cachedOnlineDriveData = getDriveDetails(targetDriveId); remainingFreeSpace = cachedOnlineDriveData.quotaRemaining; // Get the file size from the actual file long thisFileSizeLocal = getSize(localFilePath); // Get the file size from the DB data long thisFileSizeFromDB; if (!dbItem.size.empty) { thisFileSizeFromDB = to!long(dbItem.size); } else { thisFileSizeFromDB = 0; } // 'remainingFreeSpace' online includes the current file online // We need to remove the online file (add back the existing file size) then take away the new local file size to get a new approximate value long calculatedSpaceOnlinePostUpload = (remainingFreeSpace + thisFileSizeFromDB) - thisFileSizeLocal; // Based on what we know, for this thread - can we safely upload this modified local file? if (debugLogging) { string estimatedMessage = format("This Thread Estimated Free Space Online (%s): ", targetDriveId); addLogEntry(estimatedMessage ~ to!string(remainingFreeSpace), ["debug"]); addLogEntry("This Thread Calculated Free Space Online Post Upload: " ~ to!string(calculatedSpaceOnlinePostUpload), ["debug"]); } // Is there quota available for the given drive where we are uploading to? // If 'personal' accounts, if driveId == defaultDriveId, then we will have quota data - cachedOnlineDriveData.quotaRemaining will be updated so it can be reused // If 'personal' accounts, if driveId != defaultDriveId, then we will not have quota data - cachedOnlineDriveData.quotaRestricted will be set as true // If 'business' accounts, if driveId == defaultDriveId, then we will potentially have quota data - cachedOnlineDriveData.quotaRemaining will be updated so it can be reused // If 'business' accounts, if driveId != defaultDriveId, then we will potentially have quota data, but it most likely will be a 0 value - cachedOnlineDriveData.quotaRestricted will be set as true if (cachedOnlineDriveData.quotaAvailable) { // Our query told us we have free space online .. if we upload this file, will we exceed space online - thus upload will fail during upload? if (calculatedSpaceOnlinePostUpload > 0) { // Based on this thread action, we believe that there is space available online to upload - proceed spaceAvailableOnline = true; } } // Is quota being restricted? if (cachedOnlineDriveData.quotaRestricted) { // Space available online is being restricted - so we have no way to really know if there is space available online spaceAvailableOnline = true; } // Do we have space available or is space available being restricted (so we make the blind assumption that there is space available) JSONValue uploadResponse; if (spaceAvailableOnline) { // Does this file exceed the maximum file size to upload to OneDrive? if (thisFileSizeLocal <= maxUploadFileSize) { // Attempt to upload the modified file // Error handling is in performModifiedFileUpload(), and the JSON that is responded with - will either be null or a valid JSON object containing the upload result uploadResponse = performModifiedFileUpload(dbItem, localFilePath, thisFileSizeLocal); // Evaluate the returned JSON uploadResponse // If there was an error uploading the file, uploadResponse should be empty and invalid if (uploadResponse.type() != JSONType.object) { uploadFailed = true; skippedExceptionError = true; } } else { // Skip file - too large uploadFailed = true; skippedMaxSize = true; } } else { // Cant upload this file - no space available uploadFailed = true; } // Did the upload fail? if (uploadFailed) { // Upload failed .. why? // No space available online if (!spaceAvailableOnline) { addLogEntry("Skipping uploading modified file: " ~ localFilePath ~ " due to insufficient free space available on Microsoft OneDrive", ["info", "notify"]); } // File exceeds max allowed size if (skippedMaxSize) { addLogEntry("Skipping uploading this modified file as it exceeds the maximum size allowed by Microsoft OneDrive: " ~ localFilePath, ["info", "notify"]); } // Generic message if (skippedExceptionError) { // normal failure message if API or exception error generated // If Issue #2626 | Case 2-1 is triggered, the file we tried to upload was renamed, then uploaded as a new name if (exists(localFilePath)) { // Issue #2626 | Case 2-1 was not triggered, file still exists on local filesystem addLogEntry("Uploading modified file: " ~ localFilePath ~ " ... failed!", ["info", "notify"]); } } } else { // Upload was successful addLogEntry("Uploading modified file: " ~ localFilePath ~ " ... done", fileTransferNotifications()); // As no upload failure, calculate transfer metrics in a consistent manner displayTransferMetrics(localFilePath, thisFileSizeLocal, uploadStartTime, Clock.currTime()); // What do we save to the DB? Is this a OneDrive Business Shared File? if ((dbItem.type == ItemType.remote) && (dbItem.remoteType == ItemType.file)) { // We need to 'massage' the old DB record, with data from online, as the DB record was specifically crafted for OneDrive Business Shared Files Item tempItem = makeItem(uploadResponse); dbItem.eTag = tempItem.eTag; dbItem.cTag = tempItem.cTag; dbItem.mtime = tempItem.mtime; dbItem.quickXorHash = tempItem.quickXorHash; dbItem.sha256Hash = tempItem.sha256Hash; dbItem.size = tempItem.size; itemDB.upsert(dbItem); } else { // Save the response JSON item in database as is saveItem(uploadResponse); } // Update the 'cachedOnlineDriveData' record for this 'targetDriveId' so that this is tracked as accurately as possible for other threads updateDriveDetailsCache(targetDriveId, cachedOnlineDriveData.quotaRestricted, cachedOnlineDriveData.quotaAvailable, thisFileSizeLocal); // Check the integrity of the uploaded modified file if not in a --dry-run scenario if (!dryRun) { bool uploadIntegrityPassed; // Check the integrity of the uploaded modified file, if the local file still exists uploadIntegrityPassed = performUploadIntegrityValidationChecks(uploadResponse, localFilePath, thisFileSizeLocal); // Update the date / time of the file online to match the local item // Get the local file last modified time SysTime localModifiedTime = timeLastModified(localFilePath).toUTC(); localModifiedTime.fracSecs = Duration.zero; // Get the latest eTag, and use that string etagFromUploadResponse = uploadResponse["eTag"].str; // Attempt to update the online date time stamp based on our local data if (appConfig.accountType == "personal") { // Business | SharePoint we used a session to upload the data, thus, local timestamps are given when the session is created uploadLastModifiedTime(dbItem, targetDriveId, targetItemId, localModifiedTime, etagFromUploadResponse); } else { // Due to https://github.com/OneDrive/onedrive-api-docs/issues/935 Microsoft modifies all PDF, MS Office & HTML files with added XML content. It is a 'feature' of SharePoint. // This means that the file which was uploaded, is potentially no longer the file we have locally // There are 2 ways to solve this: // 1. Download the modified file immediately after upload as per v2.4.x (default) // 2. Create a new online version of the file, which then contributes to the users 'quota' if (!uploadIntegrityPassed) { // upload integrity check failed if (!appConfig.getValueBool("create_new_file_version")) { // are we in an --upload-only scenario if(!uploadOnly){ // Download the now online modified file addLogEntry("WARNING: Microsoft OneDrive modified your uploaded file via its SharePoint 'enrichment' feature. To keep your local and online versions consistent, the altered file will now be downloaded."); addLogEntry("WARNING: Please refer to https://github.com/OneDrive/onedrive-api-docs/issues/935 for further details."); // Download the file directly using the prior upload JSON response downloadFileItem(uploadResponse, true); } else { // --upload-only being used // we are not downloading a file, warn that file differences will exist addLogEntry("WARNING: The file uploaded to Microsoft OneDrive has been modified through its SharePoint 'enrichment' process and no longer matches your local version."); addLogEntry("WARNING: Please refer to https://github.com/OneDrive/onedrive-api-docs/issues/935 for further details."); } } else { // Create a new online version of the file by updating the metadata, which negates the need to download the file uploadLastModifiedTime(dbItem, targetDriveId, targetItemId, localModifiedTime, etagFromUploadResponse); } } else { // Upload of the modified file passed integrity checks // We need to make sure that the local file on disk has this timestamp from this JSON, otherwise on the next application run: // The last modified timestamp has changed however the file content has not changed // The local item has the same hash value as the item online - correcting timestamp online // This then creates another version online which we do not want to do .. unless configured to do so if (!appConfig.getValueBool("create_new_file_version")) { // create an applicable item Item onlineItem; onlineItem = makeItem(uploadResponse); // Correct the local file timestamp to avoid creating a new version online // Set the timestamp, logging and error handling done within function setPathTimestamp(dryRun, localFilePath, onlineItem.mtime); } else { // Create a new online version of the file by updating the metadata, which negates the need to download the file uploadLastModifiedTime(dbItem, targetDriveId, targetItemId, localModifiedTime, etagFromUploadResponse); } } } } } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Perform the upload of a locally modified file to OneDrive JSONValue performModifiedFileUpload(Item dbItem, string localFilePath, long thisFileSizeLocal) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Function variables JSONValue uploadResponse; OneDriveApi uploadFileOneDriveApiInstance; uploadFileOneDriveApiInstance = new OneDriveApi(appConfig); uploadFileOneDriveApiInstance.initialise(); // Configure JSONValue variables we use for a session upload JSONValue currentOnlineJSONData; Item currentOnlineItemData; JSONValue uploadSessionData; string currentETag; // When we are uploading OneDrive Business Shared Files, we need to be targeting the right driveId and itemId string targetDriveId; string targetParentId; string targetItemId; // Is this a remote target? if ((dbItem.type == ItemType.remote) && (dbItem.remoteType == ItemType.file)) { // This is a remote file targetDriveId = dbItem.remoteDriveId; targetParentId = dbItem.remoteParentId; targetItemId = dbItem.remoteId; } else { // This is not a remote file targetDriveId = dbItem.driveId; targetParentId = dbItem.parentId; targetItemId = dbItem.id; } // Is this a dry-run scenario? if (!dryRun) { // Do we use simpleUpload or create an upload session? bool useSimpleUpload = false; // Try and get the absolute latest object details from online, so we get the latest eTag to try and avoid a 412 eTag error try { currentOnlineJSONData = uploadFileOneDriveApiInstance.getPathDetailsById(targetDriveId, targetItemId); } catch (OneDriveException exception) { // Display what the error is // - 408,429,503,504 errors are handled as a retry within uploadFileOneDriveApiInstance displayOneDriveErrorMessage(exception.msg, thisFunctionName); } // Was a valid JSON response provided? if (currentOnlineJSONData.type() == JSONType.object) { // Does the response contain an eTag? if (hasETag(currentOnlineJSONData)) { // Use the value returned from online as this will attempt to avoid a 412 response if we are creating a session upload currentETag = currentOnlineJSONData["eTag"].str; } else { // Use the database value - greater potential for a 412 error to occur if we are creating a session upload if (debugLogging) {addLogEntry("Online data for file returned zero eTag - using database eTag value", ["debug"]);} currentETag = dbItem.eTag; } // Make a reusable item from this online JSON data currentOnlineItemData = makeItem(currentOnlineJSONData); } else { // no valid JSON response - greater potential for a 412 error to occur if we are creating a session upload if (debugLogging) {addLogEntry("Online data returned was invalid - using database eTag value", ["debug"]);} currentETag = dbItem.eTag; } // What upload method should be used? if (thisFileSizeLocal <= sessionThresholdFileSize) { useSimpleUpload = true; } // If the filesize is greater than zero , and we have valid 'latest' online data is the online file matching what we think is in the database? if ((thisFileSizeLocal > 0) && (currentOnlineJSONData.type() == JSONType.object)) { // Issue #2626 | Case 2-1 // If the 'online' file is newer, this will be overwritten with the file from the local filesystem - potentially constituting online data loss Item onlineFile = makeItem(currentOnlineJSONData); // Which file is technically newer? The local file or the remote file? SysTime localModifiedTime = timeLastModified(localFilePath).toUTC(); SysTime onlineModifiedTime = onlineFile.mtime; // Reduce time resolution to seconds before comparing localModifiedTime.fracSecs = Duration.zero; onlineModifiedTime.fracSecs = Duration.zero; // Which file is newer? If local is newer, it will be uploaded as a modified file in the correct manner if (localModifiedTime < onlineModifiedTime) { // Online File is actually newer than the locally modified file if (debugLogging) { addLogEntry("currentOnlineJSONData: " ~ to!string(currentOnlineJSONData), ["debug"]); addLogEntry("onlineFile: " ~ to!string(onlineFile), ["debug"]); addLogEntry("database item: " ~ to!string(dbItem), ["debug"]); } addLogEntry("Skipping uploading this item as a locally modified file, will upload as a new file (online file already exists and is newer): " ~ localFilePath); // Online is newer, rename local, then upload the renamed file // We need to know the renamed path so we can upload it string renamedPath; // Rename the local path - we WANT this to occur regardless of bypassDataPreservation setting safeBackup(localFilePath, dryRun, false, renamedPath); // Upload renamed local file as a new file uploadNewFile(renamedPath); // Process the database entry removal for the original file. In a --dry-run scenario, this is being done against a DB copy. // This is done so we can download the newer online file itemDB.deleteById(targetDriveId, targetItemId); // This file is now uploaded, return from here, but this will trigger a response that the upload failed (technically for the original filename it did, but we renamed it, then uploaded it return uploadResponse; } } // We can only upload zero size files via simpleFileUpload regardless of account type // Reference: https://github.com/OneDrive/onedrive-api-docs/issues/53 // Additionally, all files where file size is < 4MB should be uploaded by simpleUploadReplace - everything else should use a session to upload the modified file if ((thisFileSizeLocal == 0) || (useSimpleUpload)) { // Must use Simple Upload to replace the file online try { uploadResponse = uploadFileOneDriveApiInstance.simpleUploadReplace(localFilePath, targetDriveId, targetItemId); } catch (OneDriveException exception) { // HTTP request returned status code 403 if ((exception.httpStatusCode == 403) && (appConfig.getValueBool("sync_business_shared_files"))) { // We attempted to upload a file, that was shared with us, but this was shared with us as read-only addLogEntry("Unable to upload this modified file as this was shared as read-only: " ~ localFilePath); } // HTTP request returned status code 423 // Resolve https://github.com/abraunegg/onedrive/issues/36 if (exception.httpStatusCode == 423) { // The file is currently checked out or locked for editing by another user // We cant upload this file at this time addLogEntry("Unable to upload this modified file as this is currently checked out or locked for editing by another user: " ~ localFilePath); } else { // Handle all other HTTP status codes // - 408,429,503,504 errors are handled as a retry within uploadFileOneDriveApiInstance // Display what the error is displayOneDriveErrorMessage(exception.msg, thisFunctionName); } } catch (FileException e) { // filesystem error displayFileSystemErrorMessage(e.msg, thisFunctionName); } } else { // As this is a unique thread, the sessionFilePath for where we save the data needs to be unique // The best way to do this is generate a 10 digit alphanumeric string, and use this as the file extension string threadUploadSessionFilePath = appConfig.uploadSessionFilePath ~ "." ~ generateAlphanumericString(); // Create the upload session using the latest online data 'currentOnlineData' etag try { // create the session uploadSessionData = createSessionForFileUpload(uploadFileOneDriveApiInstance, localFilePath, targetDriveId, targetParentId, baseName(localFilePath), currentOnlineItemData.eTag, threadUploadSessionFilePath); } catch (OneDriveException exception) { // HTTP request returned status code 403 if ((exception.httpStatusCode == 403) && (appConfig.getValueBool("sync_business_shared_files"))) { // We attempted to upload a file, that was shared with us, but this was shared with us as read-only addLogEntry("Unable to upload this modified file as this was shared as read-only: " ~ localFilePath); return uploadResponse; } // HTTP request returned status code 423 // Resolve https://github.com/abraunegg/onedrive/issues/36 if (exception.httpStatusCode == 423) { // The file is currently checked out or locked for editing by another user // We cant upload this file at this time addLogEntry("Unable to upload this modified file as this is currently checked out or locked for editing by another user: " ~ localFilePath); return uploadResponse; } else { // Handle all other HTTP status codes // - 408,429,503,504 errors are handled as a retry within uploadFileOneDriveApiInstance // Display what the error is displayOneDriveErrorMessage(exception.msg, thisFunctionName); } } catch (FileException e) { addLogEntry("DEBUG TO REMOVE: Modified file upload FileException Handling (Create the Upload Session)"); displayFileSystemErrorMessage(e.msg, thisFunctionName); } // Do we have a valid session URL that we can use ? if (uploadSessionData.type() == JSONType.object) { // This is a valid JSON object // Perform the upload using the session that has been created try { // so that we have this data available if we need to re-create the session // - targetDriveId, targetParentId, baseName(localFilePath), currentOnlineItemData.eTag, threadUploadSessionFilePath uploadSessionData["targetDriveId"] = targetDriveId; uploadSessionData["targetParentId"] = targetParentId; uploadSessionData["currentETag"] = currentOnlineItemData.eTag; // attempt the session upload using the session data provided uploadResponse = performSessionFileUpload(uploadFileOneDriveApiInstance, thisFileSizeLocal, uploadSessionData, threadUploadSessionFilePath); } catch (OneDriveException exception) { // Handle all other HTTP status codes // - 408,429,503,504 errors are handled as a retry within uploadFileOneDriveApiInstance // Display what the error is displayOneDriveErrorMessage(exception.msg, thisFunctionName); } catch (FileException e) { addLogEntry("DEBUG TO REMOVE: Modified file upload FileException Handling (Perform the Upload using the session)"); displayFileSystemErrorMessage(e.msg, thisFunctionName); } } else { // Create session Upload URL failed if (debugLogging) {addLogEntry("Unable to upload modified file as the creation of the upload session URL failed", ["debug"]);} } } } else { // We are in a --dry-run scenario uploadResponse = createFakeResponse(localFilePath); } // Debug Log the modified upload response if (debugLogging) {addLogEntry("Modified File Upload Response: " ~ to!string(uploadResponse), ["debug"]);} // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory uploadFileOneDriveApiInstance.releaseCurlEngine(); uploadFileOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // Return JSON return uploadResponse; } // Query the OneDrive API using the provided driveId to get the latest quota details string[3][] getRemainingFreeSpaceOnline(string driveId) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Get the quota details for this driveId // Quota details are ONLY available for the main default driveId, as the OneDrive API does not provide quota details for shared folders JSONValue currentDriveQuota; bool quotaRestricted = false; // Assume quota is not restricted unless "remaining" is missing bool quotaAvailable = false; long quotaRemainingOnline = 0; string[3][] result; OneDriveApi getCurrentDriveQuotaApiInstance; // Ensure that we have a valid driveId to query if (driveId.empty) { // No 'driveId' was provided, use the application default driveId = appConfig.defaultDriveId; } // Try and query the quota for the provided driveId try { // Create a new OneDrive API instance getCurrentDriveQuotaApiInstance = new OneDriveApi(appConfig); getCurrentDriveQuotaApiInstance.initialise(); if (debugLogging) {addLogEntry("Seeking available quota for this drive id: " ~ driveId, ["debug"]);} currentDriveQuota = getCurrentDriveQuotaApiInstance.getDriveQuota(driveId); // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory getCurrentDriveQuotaApiInstance.releaseCurlEngine(); getCurrentDriveQuotaApiInstance = null; // Perform Garbage Collection GC.collect(); } catch (OneDriveException e) { if (debugLogging) {addLogEntry("currentDriveQuota = onedrive.getDriveQuota(driveId) generated a OneDriveException", ["debug"]);} // If an exception occurs, it's unclear if quota is restricted, but quota details are not available quotaRestricted = true; // Considering restricted due to failure to access // Return result result ~= [to!string(quotaRestricted), to!string(quotaAvailable), to!string(quotaRemainingOnline)]; // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory getCurrentDriveQuotaApiInstance.releaseCurlEngine(); getCurrentDriveQuotaApiInstance = null; // Perform Garbage Collection GC.collect(); return result; } // Validate that currentDriveQuota is a JSON value if (currentDriveQuota.type() == JSONType.object && "quota" in currentDriveQuota) { // Response from API contains valid data // If 'personal' accounts, if driveId == defaultDriveId, then we will have data // If 'personal' accounts, if driveId != defaultDriveId, then we will not have quota data // If 'business' accounts, if driveId == defaultDriveId, then we will have data // If 'business' accounts, if driveId != defaultDriveId, then we will have data, but it will be a 0 value if (debugLogging) {addLogEntry("Quota Details: " ~ to!string(currentDriveQuota), ["debug"]);} JSONValue quota = currentDriveQuota["quota"]; if ("remaining" in quota) { // Issue #2806 // If this is a negative value, quota["remaining"].integer can potentially convert to a huge positive number. Convert a different way. string tempQuotaRemainingOnlineString = to!string(quota["remaining"]); long tempQuotaRemainingOnlineValue = to!long(tempQuotaRemainingOnlineString); // Update quotaRemainingOnline to use the converted value quotaRemainingOnline = tempQuotaRemainingOnlineValue; // Set the applicable 'quotaAvailable' value quotaAvailable = quotaRemainingOnline > 0; // If "remaining" is present but its value is <= 0, it's not restricted but exhausted if (quotaRemainingOnline <= 0) { if (appConfig.accountType == "personal") { addLogEntry("ERROR: OneDrive account currently has zero space available. Please free up some space online or purchase additional capacity."); } else { // Assuming 'business' or 'sharedLibrary' if (verboseLogging) {addLogEntry("WARNING: OneDrive quota information is being restricted or providing a zero value. Please fix by speaking to your OneDrive / Office 365 Administrator." , ["verbose"]);} } } } else { // "remaining" not present, indicating restricted quota information quotaRestricted = true; // what sort of account type is this? if (appConfig.accountType == "personal") { if (verboseLogging) {addLogEntry("ERROR: OneDrive quota information is missing. Your OneDrive account potentially has zero space available. Please free up some space online.", ["verbose"]);} } else { // quota details not available if (verboseLogging) {addLogEntry("WARNING: OneDrive quota information is being restricted. Please fix by speaking to your OneDrive / Office 365 Administrator.", ["verbose"]);} } } } else { // When valid quota details are not fetched if (verboseLogging) {addLogEntry("Failed to fetch or query quota details for OneDrive Drive ID: " ~ driveId, ["verbose"]);} quotaRestricted = true; // Considering restricted due to failure to interpret } // What was the determined available quota? if (debugLogging) {addLogEntry("Reported Available Online Quota for driveID '" ~ driveId ~ "': " ~ to!string(quotaRemainingOnline), ["debug"]);} // Return result result ~= [to!string(quotaRestricted), to!string(quotaAvailable), to!string(quotaRemainingOnline)]; // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return new drive array data return result; } // Perform a filesystem walk to uncover new data to upload to OneDrive void scanLocalFilesystemPathForNewData(string path) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Cleanup array memory before we start adding files pathsToCreateOnline = []; newLocalFilesToUploadToOneDrive = []; // Perform a filesystem walk to uncover new data scanLocalFilesystemPathForNewDataToUpload(path); // Create new directories online that has been identified processNewDirectoriesToCreateOnline(); // Upload new data that has been identified processNewLocalItemsToUpload(); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Scan the local filesystem for new data to upload void scanLocalFilesystemPathForNewDataToUpload(string path) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // To improve logging output for this function, what is the 'logical path' we are scanning for file & folder differences? string logPath; if (path == ".") { // get the configured sync_dir logPath = buildNormalizedPath(appConfig.getValueString("sync_dir")); } else { // use what was passed in if (!appConfig.getValueBool("monitor")) { logPath = buildNormalizedPath(appConfig.getValueString("sync_dir")) ~ "/" ~ path; } else { logPath = path; } } // Log the action that we are performing, however only if this is a directory if (exists(path)) { if (isDir(path)) { if (!appConfig.suppressLoggingOutput) { if (!cleanupLocalFiles) { addProcessingLogHeaderEntry("Scanning the local file system '" ~ logPath ~ "' for new data to upload", appConfig.verbosityCount); } else { addProcessingLogHeaderEntry("Scanning the local file system '" ~ logPath ~ "' for data to cleanup", appConfig.verbosityCount); // Set the cleanup flag cleanupDataPass = true; } } } } SysTime startTime; if (debugLogging) { startTime = Clock.currTime(); addLogEntry("Starting Filesystem Walk (Local Time): " ~ to!string(startTime), ["debug"]); } // Add a processing '.' if this is a directory we are scanning if (exists(path)) { if (isDir(path)) { if (!appConfig.suppressLoggingOutput) { if (appConfig.verbosityCount == 0) { addProcessingDotEntry(); } } } } // Perform the filesystem walk of this path, building an array of new items to upload scanPathForNewData(path); // Reset flag cleanupDataPass = false; // Close processing '.' if this is a directory we are scanning if (exists(path)) { if (isDir(path)) { if (appConfig.verbosityCount == 0) { if (!appConfig.suppressLoggingOutput) { // Close out the '....' being printed to the console completeProcessingDots(); } } } } // To finish off the processing items, this is needed to reflect this in the log if (debugLogging) { addLogEntry(debugLogBreakType1, ["debug"]); // finish filesystem walk time SysTime finishTime = Clock.currTime(); addLogEntry("Finished Filesystem Walk (Local Time): " ~ to!string(finishTime), ["debug"]); // duration Duration elapsedTime = finishTime - startTime; addLogEntry("Elapsed Time Filesystem Walk: " ~ to!string(elapsedTime), ["debug"]); } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Create new directories online void processNewDirectoriesToCreateOnline() { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Are there any new local directories to create online? if (!pathsToCreateOnline.empty) { // There are new directories to create online addLogEntry("New directories to create on Microsoft OneDrive: " ~ to!string(pathsToCreateOnline.length) ); foreach(pathToCreateOnline; pathsToCreateOnline) { // Create this directory on OneDrive so that we can upload files to it createDirectoryOnline(pathToCreateOnline); } } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Upload new data that has been identified to Microsoft OneDrive void processNewLocalItemsToUpload() { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Are there any new local items to upload? if (!newLocalFilesToUploadToOneDrive.empty) { // There are elements to upload addLogEntry("New items to upload to Microsoft OneDrive: " ~ to!string(newLocalFilesToUploadToOneDrive.length) ); // Reset totalDataToUpload totalDataToUpload = 0; // How much data do we need to upload? This is important, as, we need to know how much data to determine if all the files can be uploaded foreach (uploadFilePath; newLocalFilesToUploadToOneDrive) { // validate that the path actually exists so that it can be counted if (exists(uploadFilePath)) { totalDataToUpload = totalDataToUpload + getSize(uploadFilePath); } } // How much data is there to upload if (verboseLogging) { if (totalDataToUpload < 1024) { // Display as Bytes to upload addLogEntry("Total New Data to Upload: " ~ to!string(totalDataToUpload) ~ " Bytes", ["verbose"]); } else { if ((totalDataToUpload > 1024) && (totalDataToUpload < 1048576)) { // Display as KB to upload addLogEntry("Total New Data to Upload: " ~ to!string((totalDataToUpload / 1024)) ~ " KB", ["verbose"]); } else { // Display as MB to upload addLogEntry("Total New Data to Upload: " ~ to!string((totalDataToUpload / 1024 / 1024)) ~ " MB", ["verbose"]); } } } // How much space is available // The file, could be uploaded to a shared folder, which, we are not tracking how much free space is available there ... // Iterate through all the drives we have cached thus far, that we know about if (debugLogging) { foreach (driveId, driveDetails; onlineDriveDetails) { // Log how much space is available for each driveId addLogEntry("Current Available Space Online (" ~ driveId ~ "): " ~ to!string((driveDetails.quotaRemaining / 1024 / 1024)) ~ " MB", ["debug"]); } } // Perform the upload uploadNewLocalFileItems(); // Cleanup array memory after uploading all files newLocalFilesToUploadToOneDrive = []; } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Scan this path for new data void scanPathForNewData(string path) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Add a processing '.' if (exists(path)) { if (isDir(path)) { if (!appConfig.suppressLoggingOutput) { if (appConfig.verbosityCount == 0) { addProcessingDotEntry(); } } } } long maxPathLength; long pathWalkLength; // Add this logging break to assist with what was checked for each path if (path != ".") { if (debugLogging) {addLogEntry(debugLogBreakType1, ["debug"]);} } // https://support.microsoft.com/en-us/help/3125202/restrictions-and-limitations-when-you-sync-files-and-folders // If the path is greater than allowed characters, then one drive will return a '400 - Bad Request' // Need to ensure that the URI is encoded before the check is made: // - 400 Character Limit for OneDrive Business / Office 365 // - 430 Character Limit for OneDrive Personal // Configure maxPathLength based on account type if (appConfig.accountType == "personal") { // Personal Account maxPathLength = 430; } else { // Business Account / Office365 / SharePoint maxPathLength = 400; } // OneDrive Business Shared Files Handling - if we make a 'backup' locally of a file shared with us (because we modified it, and then maybe did a --resync), it will be treated as a new file to upload ... // The issue here is - the 'source' was a shared file - we may not even have permission to upload a 'renamed' file to the shared file's parent folder // In this case, we need to skip adding this new local file - we do not upload it (we cant , and we should not) if (appConfig.accountType == "business") { // Check appConfig.configuredBusinessSharedFilesDirectoryName against 'path' if (canFind(path, baseName(appConfig.configuredBusinessSharedFilesDirectoryName))) { // Log why this path is being skipped addLogEntry("Skipping scanning path for new files as this is reserved for OneDrive Business Shared Files: " ~ path, ["info"]); return; } } // A short lived item that has already disappeared will cause an error - is the path still valid? if (!exists(path)) { addLogEntry("Skipping path - path has disappeared: " ~ path); return; } // Calculate the path length by walking the path and catch any UTF-8 sequence errors at the same time // https://github.com/skilion/onedrive/issues/57 // https://github.com/abraunegg/onedrive/issues/487 // https://github.com/abraunegg/onedrive/issues/1192 try { pathWalkLength = path.byGrapheme.walkLength; } catch (std.utf.UTFException e) { // Path contains characters which generate a UTF exception addLogEntry("Skipping item - invalid UTF sequence: " ~ path, ["info", "notify"]); if (debugLogging) {addLogEntry(" Error Reason:" ~ e.msg, ["debug"]);} return; } // Is the path length is less than maxPathLength if (pathWalkLength < maxPathLength) { // Is this path unwanted bool unwanted = false; // First check of this item - if we are in a --dry-run scenario, we may have 'fake deleted' this path // thus, the entries are not in the dry-run DB copy, thus, at this point the client thinks that this is an item to upload // Check this 'path' for an entry in pathFakeDeletedArray - if it is there, this is unwanted if (dryRun) { // Is this path in the array of fake deleted items? If yes, return early, nothing else to do, save processing if (canFind(pathFakeDeletedArray, path)) return; } // Check if item if found in database bool itemFoundInDB = pathFoundInDatabase(path); // If the item is already found in the database, it is redundant to perform these checks if (!itemFoundInDB) { // This not a Client Side Filtering check, nor a Microsoft Check, but is a sanity check that the path provided is UTF encoded correctly // Check the std.encoding of the path against: Unicode 5.0, ASCII, ISO-8859-1, ISO-8859-2, WINDOWS-1250, WINDOWS-1251, WINDOWS-1252 if (!unwanted) { if(!isValid(path)) { // Path is not valid according to https://dlang.org/phobos/std_encoding.html addLogEntry("Skipping item - invalid character encoding sequence: " ~ path, ["info", "notify"]); unwanted = true; } } // Check this path against the Client Side Filtering Rules // - check_nosync // - skip_dotfiles // - skip_symlinks // - skip_file // - skip_dir // - sync_list // - skip_size if (!unwanted) { // If this is not the cleanup data pass when using --download-only --cleanup-local-files we dont want to exclude files we need to delete locally when using 'sync_list' if (!cleanupDataPass) { unwanted = checkPathAgainstClientSideFiltering(path); } } // Check this path against the Microsoft Naming Conventions & Restrictions // - Check path against Microsoft OneDrive restriction and limitations about Windows naming for files and folders // - Check path for bad whitespace items // - Check path for HTML ASCII Codes // - Check path for ASCII Control Codes if (!unwanted) { unwanted = checkPathAgainstMicrosoftNamingRestrictions(path); } } // Before we traverse this 'path', we need to make a last check to see if this was just excluded bool skipFolderTraverse = skipBusinessSharedFolder(path); if (!unwanted) { // At this point, this path, we want to scan for new data as it is not excluded if (isDir(path)) { // Was the path found in the database? if (!itemFoundInDB) { // Path not found in database when searching all drive id's if (!cleanupLocalFiles) { // --download-only --cleanup-local-files not used // Create this directory on OneDrive so that we can upload files to it // Add this path to an array so that the directory online can be created before we upload files pathsToCreateOnline ~= [path]; } else { // we need to clean up this directory addLogEntry("Removing local directory as --download-only & --cleanup-local-files configured"); // Remove any children of this path if they still exist // Resolve 'Directory not empty' error when deleting local files try { auto directoryEntries = dirEntries(path, SpanMode.depth, false); foreach (DirEntry child; directoryEntries) { // what sort of child is this? if (isDir(child.name)) { addLogEntry("Removing local directory: " ~ child.name); } else { addLogEntry("Removing local file: " ~ child.name); } // are we in a --dry-run scenario? if (!dryRun) { // No --dry-run ... process local delete if (exists(child)) { try { attrIsDir(child.linkAttributes) ? rmdir(child.name) : remove(child.name); } catch (FileException e) { // display the error message displayFileSystemErrorMessage(e.msg, thisFunctionName); } } } } // Clear directoryEntries object.destroy(directoryEntries); // Remove the path now that it is empty of children addLogEntry("Removing local directory: " ~ path); // are we in a --dry-run scenario? if (!dryRun) { // No --dry-run ... process local delete if (exists(path)) { try { rmdirRecurse(path); } catch (FileException e) { // display the error message displayFileSystemErrorMessage(e.msg, thisFunctionName); } } } } catch (FileException e) { // display the error message displayFileSystemErrorMessage(e.msg, thisFunctionName); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return as there was an error return; } } } // Do we actually traverse this path? if (!skipFolderTraverse) { // Try and access this directory and any path below if (exists(path)) { try { auto directoryEntries = dirEntries(path, SpanMode.shallow, false); foreach (DirEntry entry; directoryEntries) { string thisPath = entry.name; scanPathForNewData(thisPath); } // Clear directoryEntries object.destroy(directoryEntries); } catch (FileException e) { // display the error message displayFileSystemErrorMessage(e.msg, thisFunctionName); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return as there was an error return; } } } } else { // https://github.com/abraunegg/onedrive/issues/984 // path is not a directory, is it a valid file? // pipes - whilst technically valid files, are not valid for this client // prw-rw-r--. 1 user user 0 Jul 7 05:55 my_pipe if (isFile(path)) { // Is the file a '.nosync' file? if (canFind(path, ".nosync")) { if (debugLogging) {addLogEntry("Skipping .nosync file", ["debug"]);} // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return as there was an error return; } // Was the file found in the database? if (!itemFoundInDB) { // File not found in database when searching all drive id's // Do we upload the file or clean up the file? if (!cleanupLocalFiles) { // --download-only --cleanup-local-files not used // Add this path as a file we need to upload if (debugLogging) {addLogEntry("OneDrive Client flagging to upload this file to Microsoft OneDrive: " ~ path, ["debug"]);} newLocalFilesToUploadToOneDrive ~= path; } else { // we need to clean up this file addLogEntry("Removing local file as --download-only & --cleanup-local-files configured"); // are we in a --dry-run scenario? addLogEntry("Removing local file: " ~ path); if (!dryRun) { // No --dry-run ... process local file delete safeRemove(path); } } } } else { // path is not a valid file addLogEntry("Skipping item - item is not a valid file: " ~ path, ["info", "notify"]); } } } else { // Issue #3126 - https://github.com/abraunegg/onedrive/discussions/3126 // At this point, this path that we want to scan for new data has been excluded .. we may have an include 'sync_list' rule for a subfolder of this excluded parent ... // If the data is created online, this is not usually a problem, but essentially if we create new data locally, in a folder we are expecting to included by an existing configuration, // unless we actually scan the entire tree, including those directories that are excluded, we are not going to detect the new locally added data in a parent that has been excluded, // but the child content has to be included if (isDir(path)) { // Do we actually traverse this path? if (!skipFolderTraverse) { // Not a Business Shared Folder that must not be traversed if 'sync_business_shared_folders' is not enabled // Was this path excluded by the 'sync_list' exclusion process if (syncListDirExcluded) { // yes .. this parent path was excluded by the 'sync_list' ... we need to scan this path for potential new data that may be included if (verboseLogging) {addLogEntry("Bypassing 'sync_list' exclusion to scan directory for potential new data that may be included", ["verbose"]);} // try and go through the excluded directory path try { auto directoryEntries = dirEntries(path, SpanMode.shallow, false); foreach (DirEntry entry; directoryEntries) { string thisPath = entry.name; scanPathForNewData(thisPath); } // Clear directoryEntries object.destroy(directoryEntries); } catch (FileException e) { // display the error message displayFileSystemErrorMessage(e.msg, thisFunctionName); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return as there was an error return; } } } } } } else { // This path was skipped - why? addLogEntry("Skipping item '" ~ path ~ "' due to the full path exceeding " ~ to!string(maxPathLength) ~ " characters (Microsoft OneDrive limitation)", ["info", "notify"]); } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Do we skip this path as it might be an Online Business Shared Folder bool skipBusinessSharedFolder(string path) { // Is this a business account? if (appConfig.accountType == "business") { // search businessSharedFoldersOnlineToSkip for this path if (canFind(businessSharedFoldersOnlineToSkip, path)) { // This path was skipped - why? addLogEntry("Skipping item '" ~ path ~ "' due to this path matching an existing online Business Shared Folder name", ["info", "notify"]); addLogEntry("To sync this Business Shared Folder, consider enabling 'sync_business_shared_folders' within your application configuration.", ["info"]); return true; } } // return value return false; } // Handle a single file inotify trigger when using --monitor void handleLocalFileTrigger(string[] changedLocalFilesToUploadToOneDrive) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Is this path a new file or an existing one? // Normally we would use pathFoundInDatabase() to calculate, but we need 'databaseItem' as well if the item is in the database foreach (localFilePath; changedLocalFilesToUploadToOneDrive) { try { Item databaseItem; bool fileFoundInDB = false; foreach (driveId; onlineDriveDetails.keys) { if (itemDB.selectByPath(localFilePath, driveId, databaseItem)) { fileFoundInDB = true; // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // file found, search no more break; } } // Was the file found in the database? if (!fileFoundInDB) { // This is a new file as it is not in the database // Log that the file has been added locally if (verboseLogging) {addLogEntry("[M] New local file added: " ~ localFilePath, ["verbose"]);} scanLocalFilesystemPathForNewDataToUpload(localFilePath); } else { // This is a potentially modified file, needs to be handled as such. Is the item truly modified? if (!testFileHash(localFilePath, databaseItem)) { // The local file failed the hash comparison test - there is a data difference // Log that the file has changed locally if (verboseLogging) {addLogEntry("[M] Local file changed: " ~ localFilePath, ["verbose"]);} // Add the modified item to the array to upload uploadChangedLocalFileToOneDrive([databaseItem.driveId, databaseItem.id, localFilePath]); } } } catch(Exception e) { addLogEntry("Cannot upload file changes/creation: " ~ e.msg, ["info", "notify"]); } } processNewLocalItemsToUpload(); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Query the database to determine if this path is within the existing database bool pathFoundInDatabase(string searchPath) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Check if this path in the database Item databaseItem; if (debugLogging) {addLogEntry("Search DB for this path: " ~ searchPath, ["debug"]);} foreach (driveId; onlineDriveDetails.keys) { if (itemDB.selectByPath(searchPath, driveId, databaseItem)) { if (debugLogging) {addLogEntry("DB Record for search path: " ~ to!string(databaseItem), ["debug"]);} // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } return true; // Early exit on finding the path in the DB } } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } return false; // Return false if path is not found in any drive } // Create a new directory online on OneDrive // - Test if we can get the parent path details from the database, otherwise we need to search online // for the path flow and create the folder that way void createDirectoryOnline(string thisNewPathToCreate) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Log what we are doing if (verboseLogging) {addLogEntry("OneDrive Client requested to create this directory online: " ~ thisNewPathToCreate, ["verbose"]);} // Function variables Item parentItem; JSONValue onlinePathData; // Special Folder Handling: Do NOT create the folder online if it is being used for OneDrive Business Shared Files // These are local copy files, in a self created directory structure which is not to be replicated online // Check appConfig.configuredBusinessSharedFilesDirectoryName against 'thisNewPathToCreate' if (canFind(thisNewPathToCreate, baseName(appConfig.configuredBusinessSharedFilesDirectoryName))) { // Log why this is being skipped addLogEntry("Skipping creating '" ~ thisNewPathToCreate ~ "' as this path is used for handling OneDrive Business Shared Files", ["info", "notify"]); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return early as skipping return; } // Create a new API Instance for this thread and initialise it OneDriveApi createDirectoryOnlineOneDriveApiInstance; createDirectoryOnlineOneDriveApiInstance = new OneDriveApi(appConfig); createDirectoryOnlineOneDriveApiInstance.initialise(); // What parent path to use? string parentPath = dirName(thisNewPathToCreate); // will be either . or something else // Configure the parentItem by if this is the account 'root' use the root details, or search the database for the parent details if (parentPath == ".") { // Parent path is '.' which is the account root // Use client defaults parentItem.driveId = appConfig.defaultDriveId; // Should give something like 12345abcde1234a1 parentItem.id = appConfig.defaultRootId; // Should give something like 12345ABCDE1234A1!101 } else { // Query the parent path online if (debugLogging) {addLogEntry("Attempting to query Local Database for this parent path: " ~ parentPath, ["debug"]);} // Attempt a 2 step process to work out where to create the directory // Step 1: Query the DB first for the parent path, to try and avoid an API call // Step 2: Query online as last resort // Step 1: Check if this parent path in the database Item databaseItem; bool parentPathFoundInDB = false; foreach (driveId; onlineDriveDetails.keys) { // Issue #3115 - Validate driveId length // What account type is this? if (appConfig.accountType == "personal") { // Test driveId length and validation if the driveId we are testing is not equal to appConfig.defaultDriveId if (driveId != appConfig.defaultDriveId) { driveId = testProvidedDriveIdForLengthIssue(driveId); } } if (debugLogging) {addLogEntry("Query DB with this driveID for the Parent Path: " ~ driveId, ["debug"]);} // Query the database for this parent path using each driveId that we know about if (itemDB.selectByPath(parentPath, driveId, databaseItem)) { parentPathFoundInDB = true; if (debugLogging) { addLogEntry("Parent databaseItem: " ~ to!string(databaseItem), ["debug"]); addLogEntry("parentPathFoundInDB: " ~ to!string(parentPathFoundInDB), ["debug"]); } // Set parentItem to the item returned from the database parentItem = databaseItem; } } // After querying all DB entries for each driveID for the parent path, what are the details in parentItem? if (debugLogging) {addLogEntry("Parent parentItem after DB Query exhausted: " ~ to!string(parentItem), ["debug"]);} // Step 2: Query for the path online if not found in the local database if (!parentPathFoundInDB) { // parent path not found in database try { if (debugLogging) {addLogEntry("Attempting to query OneDrive Online for this parent path as path not found in local database: " ~ parentPath, ["debug"]);} onlinePathData = createDirectoryOnlineOneDriveApiInstance.getPathDetails(parentPath); if (debugLogging) {addLogEntry("Online Parent Path Query Response: " ~ to!string(onlinePathData), ["debug"]);} // Save item to the database saveItem(onlinePathData); parentItem = makeItem(onlinePathData); } catch (OneDriveException exception) { if (exception.httpStatusCode == 404) { // Parent does not exist ... need to create parent if (debugLogging) {addLogEntry("Parent path does not exist online: " ~ parentPath, ["debug"]);} createDirectoryOnline(parentPath); // no return here as we need to continue, but need to re-query the OneDrive API to get the right parental details now that they exist onlinePathData = createDirectoryOnlineOneDriveApiInstance.getPathDetails(parentPath); parentItem = makeItem(onlinePathData); } else { // Default operation if not 408,429,503,504 errors // - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance // Display what the error is displayOneDriveErrorMessage(exception.msg, thisFunctionName); } } } } // Make sure the full path does not exist online, this should generate a 404 response, to which then the folder will be created online try { // Try and query the OneDrive API for the path we need to create if (debugLogging) { addLogEntry("Attempting to query OneDrive API for this path: " ~ thisNewPathToCreate, ["debug"]); addLogEntry("parentItem details: " ~ to!string(parentItem), ["debug"]); } // Depending on the data within parentItem, will depend on what method we are using to search // A Shared Folder will be 'remote' so we need to check the remote parent id, rather than parentItem details Item queryItem; if (parentItem.type == ItemType.remote) { // This folder is a potential shared object if (debugLogging) {addLogEntry("ParentItem is a remote item object", ["debug"]);} // Is this a Personal Account Type or has 'sync_business_shared_items' been enabled? if ((appConfig.accountType == "personal") || (appConfig.getValueBool("sync_business_shared_items"))) { // Need to create the DB Tie for this shared object to ensure this exists in the database createDatabaseTieRecordForOnlineSharedFolder(parentItem); // Update the queryItem values queryItem.driveId = parentItem.remoteDriveId; queryItem.id = parentItem.remoteId; } else { // This is a shared folder location, but we are not a 'personal' account, and 'sync_business_shared_items' has not been enabled addLogEntry("ERROR: Unable to create directory online as 'sync_business_shared_items' is not enabled"); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return as we cannot continue here return; } } else { // Use parent item for the query item if (debugLogging) {addLogEntry("Standard Query, use parentItem", ["debug"]);} queryItem = parentItem; } // Issue #3115 - Validate driveId length // What account type is this? if (appConfig.accountType == "personal") { // Test driveId length and validation if the driveId we are testing is not equal to appConfig.defaultDriveId if (queryItem.driveId != appConfig.defaultDriveId) { queryItem.driveId = testProvidedDriveIdForLengthIssue(queryItem.driveId); } } if (queryItem.driveId == appConfig.defaultDriveId) { // Use getPathDetailsByDriveId if (debugLogging) {addLogEntry("Selecting getPathDetailsByDriveId to query OneDrive API for path data", ["debug"]);} onlinePathData = createDirectoryOnlineOneDriveApiInstance.getPathDetailsByDriveId(queryItem.driveId, thisNewPathToCreate); } else { // Use searchDriveForPath to query OneDrive if (debugLogging) {addLogEntry("Selecting searchDriveForPath to query OneDrive API for path data", ["debug"]);} // If the queryItem.driveId is not our driveId - the path we are looking for will not be at the logical location that getPathDetailsByDriveId // can use - as it will always return a 404 .. even if the path actually exists (which is the whole point of this test) // Search the queryItem.driveId for any folder name match that we are going to create, then compare response JSON items with queryItem.id // If no match, the folder we want to create does not exist at the location we are seeking to create it at, thus generate a 404 onlinePathData = createDirectoryOnlineOneDriveApiInstance.searchDriveForPath(queryItem.driveId, baseName(thisNewPathToCreate)); if (debugLogging) {addLogEntry("onlinePathData: " ~to!string(onlinePathData), ["debug"]);} // Process the response from searching the drive long responseCount = count(onlinePathData["value"].array); if (responseCount > 0) { // Search 'name' matches were found .. need to match these against queryItem.id bool foundDirectoryOnline = false; JSONValue foundDirectoryJSONItem; // Items were returned .. but is one of these what we are looking for? foreach (childJSON; onlinePathData["value"].array) { // Is this item not a file? if (!isFileItem(childJSON)) { Item thisChildItem = makeItem(childJSON); // Direct Match Check if ((queryItem.id == thisChildItem.parentId) && (baseName(thisNewPathToCreate) == thisChildItem.name)) { // High confidence that this child folder is a direct match we are trying to create and it already exists online if (debugLogging) { addLogEntry("Path we are searching for exists online (Direct Match): " ~ baseName(thisNewPathToCreate), ["debug"]); addLogEntry("childJSON: " ~ sanitiseJSONItem(childJSON), ["debug"]); } foundDirectoryOnline = true; foundDirectoryJSONItem = childJSON; break; } // Full Lower Case POSIX Match Check string childAsLower = toLower(childJSON["name"].str); string thisFolderNameAsLower = toLower(baseName(thisNewPathToCreate)); // Child name check if (childAsLower == thisFolderNameAsLower) { // This is a POSIX 'case in-sensitive match' ..... in folder name only // - Local item name has a 'case-insensitive match' to an existing item on OneDrive // The 'parentId' of this JSON object must match the parentId of where the folder was created // - why .. we might have the same folder name, but somewhere totally different if (queryItem.id == thisChildItem.parentId) { // Found the directory in the location, using case in-sensitive matching if (debugLogging) { addLogEntry("Path we are searching for exists online (POSIX 'case in-sensitive match'): " ~ baseName(thisNewPathToCreate), ["debug"]); addLogEntry("childJSON: " ~ sanitiseJSONItem(childJSON), ["debug"]); } foundDirectoryOnline = true; foundDirectoryJSONItem = childJSON; break; } } } } if (foundDirectoryOnline) { // Directory we are seeking was found online ... if (debugLogging) {addLogEntry("The directory we are seeking was found online by using searchDriveForPath ...", ["debug"]);} onlinePathData = foundDirectoryJSONItem; } else { // No 'search item matches found' - raise a 404 so that the exception handling will take over to create the folder throw new OneDriveException(404, "Name not found via search"); } } else { // No 'search item matches found' - raise a 404 so that the exception handling will take over to create the folder throw new OneDriveException(404, "Name not found via search"); } } } catch (OneDriveException exception) { if (exception.httpStatusCode == 404) { // This is a good error - it means that the directory to create 100% does not exist online // The directory was not found on the drive id we queried if (verboseLogging) {addLogEntry("The requested directory to create was not found on OneDrive - creating remote directory: " ~ thisNewPathToCreate, ["verbose"]);} // Build up the online create directory request JSONValue createDirectoryOnlineAPIResponse; JSONValue newDriveItem = [ "name": JSONValue(baseName(thisNewPathToCreate)), "folder": parseJSON("{}") ]; // Submit the creation request // Fix for https://github.com/skilion/onedrive/issues/356 if (!dryRun) { try { // Attempt to create a new folder on the required driveId and parent item id string requiredDriveId; string requiredParentItemId; // Is the item a Remote Object (Shared Folder) ? if (parentItem.type == ItemType.remote) { // Yes .. Shared Folder if (debugLogging) {addLogEntry("parentItem data: " ~ to!string(parentItem), ["debug"]);} requiredDriveId = parentItem.remoteDriveId; requiredParentItemId = parentItem.remoteId; } else { // Not a Shared Folder requiredDriveId = parentItem.driveId; requiredParentItemId = parentItem.id; } // Where are we creating this new folder? if (debugLogging) { addLogEntry("requiredDriveId: " ~ requiredDriveId, ["debug"]); addLogEntry("requiredParentItemId: " ~ requiredParentItemId, ["debug"]); addLogEntry("newDriveItem JSON: " ~ sanitiseJSONItem(newDriveItem), ["debug"]); } // Create the new folder createDirectoryOnlineAPIResponse = createDirectoryOnlineOneDriveApiInstance.createById(requiredDriveId, requiredParentItemId, newDriveItem); // Is the response a valid JSON object - validation checking done in saveItem saveItem(createDirectoryOnlineAPIResponse); // Log that the directory was created addLogEntry("Successfully created the remote directory " ~ thisNewPathToCreate ~ " on Microsoft OneDrive"); } catch (OneDriveException exception) { if (exception.httpStatusCode == 409) { // OneDrive API returned a 404 (above) to say the directory did not exist // but when we attempted to create it, OneDrive responded that it now already exists if (verboseLogging) {addLogEntry("OneDrive reported that " ~ thisNewPathToCreate ~ " already exists .. OneDrive API race condition", ["verbose"]);} // Shutdown this API instance, as we will create API instances as required, when required createDirectoryOnlineOneDriveApiInstance.releaseCurlEngine(); // Free object and memory createDirectoryOnlineOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); } else { // some other error from OneDrive was returned - display what it is addLogEntry("OneDrive generated an error when creating this path: " ~ thisNewPathToCreate); displayOneDriveErrorMessage(exception.msg, thisFunctionName); // Shutdown this API instance, as we will create API instances as required, when required createDirectoryOnlineOneDriveApiInstance.releaseCurlEngine(); // Free object and memory createDirectoryOnlineOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return due to OneDriveException return; } } else { // Simulate a successful 'directory create' & save it to the dryRun database copy addLogEntry("Successfully created the remote directory " ~ thisNewPathToCreate ~ " on Microsoft OneDrive"); // The simulated response has to pass 'makeItem' as part of saveItem auto fakeResponse = createFakeResponse(thisNewPathToCreate); // Save item to the database saveItem(fakeResponse); } // Shutdown this API instance, as we will create API instances as required, when required createDirectoryOnlineOneDriveApiInstance.releaseCurlEngine(); // Free object and memory createDirectoryOnlineOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // shutdown & return return; } else { // Default operation if not 408,429,503,504 errors // - 408,429,503,504 errors are handled as a retry within createDirectoryOnlineOneDriveApiInstance // If we get a 400 error, there is an issue creating this folder on Microsoft OneDrive for some reason // If the error is not 400, re-try, else fail if (exception.httpStatusCode != 400) { // Attempt a re-try createDirectoryOnline(thisNewPathToCreate); } else { // We cant create this directory online if (debugLogging) {addLogEntry("This folder cannot be created online: " ~ buildNormalizedPath(absolutePath(thisNewPathToCreate)), ["debug"]);} } } } // If we get to this point - onlinePathData = createDirectoryOnlineOneDriveApiInstance.getPathDetailsByDriveId(parentItem.driveId, thisNewPathToCreate) generated a 'valid' response .... // This means that the folder potentially exists online .. which is odd .. as it should not have existed if (onlinePathData.type() == JSONType.object) { // A valid object was responded with if (onlinePathData["name"].str == baseName(thisNewPathToCreate)) { // OneDrive 'name' matches local path name if (debugLogging) { addLogEntry("The path to query/search for online was found online", ["debug"]); addLogEntry(" onlinePathData: " ~ to!string(onlinePathData), ["debug"]); } // OneDrive Personal Shared Folder Check if (appConfig.accountType == "personal") { // We are a personal account, this existing online folder, it could be a Shared Online Folder could be a 'Add shortcut to My files' item // Is this a remote folder if (isItemRemote(onlinePathData)) { // The folder is a remote item ... if (debugLogging) {addLogEntry("The existing Remote Online Folder and 'onlinePathData' indicate this is most likely a OneDrive Personal Shared Folder Link added by 'Add shortcut to My files'", ["debug"]);} // It is a 'remote' JSON item denoting a potential shared folder // Create a 'root' and 'Shared Folder' DB Tie Records for this JSON object in a consistent manner createRequiredSharedFolderDatabaseRecords(onlinePathData); } } // OneDrive Business Shared Folder Check if (appConfig.accountType == "business") { // We are a business account, this existing online folder, it could be a Shared Online Folder could be a 'Add shortcut to My files' item // Is this a remote folder if (isItemRemote(onlinePathData)) { // The folder is a remote item ... if (debugLogging) {addLogEntry("The existing Remote Online Folder and 'onlinePathData' indicate this is most likely a OneDrive Shared Business Folder Link added by 'Add shortcut to My files'", ["debug"]);} // Is Shared Business Folder Syncing actually enabled? if (!appConfig.getValueBool("sync_business_shared_items")) { // Shared Business Folder Syncing is NOT enabled if (debugLogging) {addLogEntry("We need to skip this path: " ~ thisNewPathToCreate, ["debug"]);} // Add this path to businessSharedFoldersOnlineToSkip businessSharedFoldersOnlineToSkip ~= [thisNewPathToCreate]; // no save to database, no online create // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory createDirectoryOnlineOneDriveApiInstance.releaseCurlEngine(); createDirectoryOnlineOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return due to skipped path return; } else { // Shared Business Folder Syncing IS enabled // It is a 'remote' JSON item denoting a potential shared folder // Create a 'root' and 'Shared Folder' DB Tie Records for this JSON object in a consistent manner createRequiredSharedFolderDatabaseRecords(onlinePathData); } } } // Path found online if (verboseLogging) {addLogEntry("The requested directory to create was found on OneDrive - skipping creating the directory online: " ~ thisNewPathToCreate, ["verbose"]);} // Is the response a valid JSON object - validation checking done in saveItem saveItem(onlinePathData); // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory createDirectoryOnlineOneDriveApiInstance.releaseCurlEngine(); createDirectoryOnlineOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return due to path found online return; } else { // Normally this would throw an error, however we cant use throw new PosixException() string msg = format("POSIX 'case-insensitive match' between '%s' (local) and '%s' (online) which violates the Microsoft OneDrive API namespace convention", baseName(thisNewPathToCreate), onlinePathData["name"].str); displayPosixErrorMessage(msg); addLogEntry("ERROR: Requested directory to create has a 'case-insensitive match' to an existing directory on Microsoft OneDrive online."); addLogEntry("ERROR: To resolve, rename this local directory: " ~ buildNormalizedPath(absolutePath(thisNewPathToCreate))); addLogEntry("Skipping creating this directory online due to 'case-insensitive match': " ~ thisNewPathToCreate); // Add this path to posixViolationPaths posixViolationPaths ~= [thisNewPathToCreate]; // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory createDirectoryOnlineOneDriveApiInstance.releaseCurlEngine(); createDirectoryOnlineOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // manual POSIX exception return; } } else { // response is not valid JSON, an error was returned from OneDrive addLogEntry("ERROR: There was an error performing this operation on Microsoft OneDrive"); addLogEntry("ERROR: Increase logging verbosity to assist determining why."); addLogEntry("Skipping: " ~ buildNormalizedPath(absolutePath(thisNewPathToCreate))); // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory createDirectoryOnlineOneDriveApiInstance.releaseCurlEngine(); createDirectoryOnlineOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // generic error return; } } // Test that the online name actually matches the requested local name bool performPosixTest(string localNameToCheck, string onlineName) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file // Do not assume case sensitivity. For example, consider the names OSCAR, Oscar, and oscar to be the same, // even though some file systems (such as a POSIX-compliant file system) may consider them as different. // Note that NTFS supports POSIX semantics for case sensitivity but this is not the default behavior. bool posixIssue = false; // Is the name different if (localNameToCheck != onlineName) { // POSIX Error // Local item name has a 'case-insensitive match' to an existing item on OneDrive posixIssue = true; } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return the posix evaluation return posixIssue; } // Upload new file items as identified void uploadNewLocalFileItems() { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Lets deal with the new local items in a batch process size_t batchSize = to!int(appConfig.getValueLong("threads")); long batchCount = (newLocalFilesToUploadToOneDrive.length + batchSize - 1) / batchSize; long batchesProcessed = 0; // Transfer order string transferOrder = appConfig.getValueString("transfer_order"); // Has the user configured to specify the transfer order of files? if (transferOrder != "default") { // If we have more than 1 item to upload, sort the items if (count(newLocalFilesToUploadToOneDrive) > 1) { // Create an array of tuples (file path, file size) auto fileInfo = newLocalFilesToUploadToOneDrive .map!(file => tuple(file, getSize(file))) // Get file size for each file that needs to be uploaded .array; // Perform sorting based on transferOrder if (transferOrder == "size_asc") { fileInfo.sort!((a, b) => a[1] < b[1]); // sort the array by ascending size } else if (transferOrder == "size_dsc") { fileInfo.sort!((a, b) => a[1] > b[1]); // sort the array by descending size } else if (transferOrder == "name_asc") { fileInfo.sort!((a, b) => a[0] < b[0]); // sort the array by ascending name } else if (transferOrder == "name_dsc") { fileInfo.sort!((a, b) => a[0] > b[0]); // sort the array by descending name } // Extract sorted file paths newLocalFilesToUploadToOneDrive = fileInfo.map!(t => t[0]).array; } } // Process newLocalFilesToUploadToOneDrive foreach (chunk; newLocalFilesToUploadToOneDrive.chunks(batchSize)) { // send an array containing 'appConfig.getValueLong("threads")' local files to upload uploadNewLocalFileItemsInParallel(chunk); } // For this set of items, perform a DB PASSIVE checkpoint itemDB.performCheckpoint("PASSIVE"); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Upload the file batches in parallel void uploadNewLocalFileItemsInParallel(string[] array) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // This function received an array of string items to upload, the number of elements based on appConfig.getValueLong("threads") foreach (i, fileToUpload; processPool.parallel(array)) { if (debugLogging) {addLogEntry("Upload Thread " ~ to!string(i) ~ " Starting: " ~ to!string(Clock.currTime()), ["debug"]);} uploadNewFile(fileToUpload); if (debugLogging) {addLogEntry("Upload Thread " ~ to!string(i) ~ " Finished: " ~ to!string(Clock.currTime()), ["debug"]);} } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Upload a new file to OneDrive void uploadNewFile(string fileToUpload) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Debug for the moment if (debugLogging) {addLogEntry("fileToUpload: " ~ fileToUpload, ["debug"]);} // These are the details of the item we need to upload // How much space is remaining on OneDrive long remainingFreeSpaceOnline; // Did the upload fail? bool uploadFailed = false; // Did we skip due to exceeding maximum allowed size? bool skippedMaxSize = false; // Did we skip to an exception error? bool skippedExceptionError = false; // Is the parent path in the item database? bool parentPathFoundInDB = false; // Get this file size long thisFileSize; // Is there space available online bool spaceAvailableOnline = false; DriveDetailsCache cachedOnlineDriveData; long calculatedSpaceOnlinePostUpload; OneDriveApi checkFileOneDriveApiInstance; // Check the database for the parent path of fileToUpload Item parentItem; // What parent path to use? string parentPath = dirName(fileToUpload); // will be either . or something else if (parentPath == "."){ // Assume this is a new file in the users configured sync_dir root // Use client defaults parentItem.id = appConfig.defaultRootId; // Should give something like 12345ABCDE1234A1!101 parentItem.driveId = appConfig.defaultDriveId; // Should give something like 12345abcde1234a1 parentPathFoundInDB = true; } else { // Query the database using each of the driveId's we are using foreach (driveId; onlineDriveDetails.keys) { // Query the database for this parent path using each driveId Item dbResponse; if(itemDB.selectByPath(parentPath, driveId, dbResponse)){ // parent path was found in the database parentItem = dbResponse; parentPathFoundInDB = true; } } } // If the parent path was found in the DB, to ensure we are uploading the right location 'parentItem.driveId' must not be empty if ((parentPathFoundInDB) && (parentItem.driveId.empty)) { // switch to using defaultDriveId if (debugLogging) {addLogEntry("parentItem.driveId is empty - using defaultDriveId for upload API calls", ["debug"]);} parentItem.driveId = appConfig.defaultDriveId; } // Check if the path still exists locally before we try to upload if (exists(fileToUpload)) { // Can we read the file - as a permissions issue or actual file corruption will cause a failure // Resolves: https://github.com/abraunegg/onedrive/issues/113 if (readLocalFile(fileToUpload)) { // The local file can be read - so we can read it to attempt to upload it in this thread // Is the path parent in the DB? if (parentPathFoundInDB) { // Parent path is in the database // Get the new file size // Even if the permissions on the file are: -rw-------. 1 root root 8 Jan 11 09:42 // we can still obtain the file size, however readLocalFile() also tests if the file can be read (permission check) thisFileSize = getSize(fileToUpload); // Does this file exceed the maximum filesize for OneDrive // Resolves: https://github.com/skilion/onedrive/issues/121 , https://github.com/skilion/onedrive/issues/294 , https://github.com/skilion/onedrive/issues/329 if (thisFileSize <= maxUploadFileSize) { // Is there enough free space on OneDrive as compared to when we started this thread, to safely upload the file to OneDrive? // Make sure that parentItem.driveId is in our driveIDs array to use when checking if item is in database // Keep the DriveDetailsCache array with unique entries only if (!canFindDriveId(parentItem.driveId, cachedOnlineDriveData)) { // Add this driveId to the drive cache, which then also sets for the defaultDriveId: // - quotaRestricted; // - quotaAvailable; // - quotaRemaining; addOrUpdateOneDriveOnlineDetails(parentItem.driveId); // Fetch the details from cachedOnlineDriveData cachedOnlineDriveData = getDriveDetails(parentItem.driveId); } // Fetch the details from cachedOnlineDriveData // - cachedOnlineDriveData.quotaRestricted; // - cachedOnlineDriveData.quotaAvailable; // - cachedOnlineDriveData.quotaRemaining; remainingFreeSpaceOnline = cachedOnlineDriveData.quotaRemaining; // When we compare the space online to the total we are trying to upload - is there space online? calculatedSpaceOnlinePostUpload = remainingFreeSpaceOnline - thisFileSize; // Based on what we know, for this thread - can we safely upload this modified local file? if (debugLogging) { string estimatedMessage = format("This Thread Estimated Free Space Online (%s): ", parentItem.driveId); addLogEntry(estimatedMessage ~ to!string(remainingFreeSpaceOnline), ["debug"]); addLogEntry("This Thread Calculated Free Space Online Post Upload: " ~ to!string(calculatedSpaceOnlinePostUpload), ["debug"]); } // If 'personal' accounts, if driveId == defaultDriveId, then we will have data - appConfig.quotaAvailable will be updated // If 'personal' accounts, if driveId != defaultDriveId, then we will not have quota data - appConfig.quotaRestricted will be set as true // If 'business' accounts, if driveId == defaultDriveId, then we will have data // If 'business' accounts, if driveId != defaultDriveId, then we will have data, but it will be a 0 value - appConfig.quotaRestricted will be set as true if (remainingFreeSpaceOnline > totalDataToUpload) { // Space available spaceAvailableOnline = true; } else { // we need to look more granular // What was the latest getRemainingFreeSpace() value? if (cachedOnlineDriveData.quotaAvailable) { // Our query told us we have free space online .. if we upload this file, will we exceed space online - thus upload will fail during upload? if (calculatedSpaceOnlinePostUpload > 0) { // Based on this thread action, we believe that there is space available online to upload - proceed spaceAvailableOnline = true; } } } // Is quota being restricted? if (cachedOnlineDriveData.quotaRestricted) { // If the upload target drive is not our drive id, then it is a shared folder .. we need to print a space warning message if (parentItem.driveId != appConfig.defaultDriveId) { // Different message depending on account type if (appConfig.accountType == "personal") { if (verboseLogging) {addLogEntry("WARNING: Shared Folder OneDrive quota information is being restricted or providing a zero value. Space available online cannot be guaranteed.", ["verbose"]);} } else { if (verboseLogging) {addLogEntry("WARNING: Shared Folder OneDrive quota information is being restricted or providing a zero value. Please fix by speaking to your OneDrive / Office 365 Administrator.", ["verbose"]);} } } else { if (appConfig.accountType == "personal") { if (verboseLogging) {addLogEntry("WARNING: OneDrive quota information is being restricted or providing a zero value. Space available online cannot be guaranteed.", ["verbose"]);} } else { if (verboseLogging) {addLogEntry("WARNING: OneDrive quota information is being restricted or providing a zero value. Please fix by speaking to your OneDrive / Office 365 Administrator.", ["verbose"]);} } } // Space available online is being restricted - so we have no way to really know if there is space available online spaceAvailableOnline = true; } // Do we have space available or is space available being restricted (so we make the blind assumption that there is space available) if (spaceAvailableOnline) { // We need to check that this new local file does not exist on OneDrive JSONValue fileDetailsFromOneDrive; // https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file // Do not assume case sensitivity. For example, consider the names OSCAR, Oscar, and oscar to be the same, // even though some file systems (such as a POSIX-compliant file systems that Linux use) may consider them as different. // Note that NTFS supports POSIX semantics for case sensitivity but this is not the default behavior, OneDrive does not use this. // In order to upload this file - this query HAS to respond with a '404 - Not Found' so that the upload is triggered // Does this 'file' already exist on OneDrive? try { // Create a new API Instance for this thread and initialise it checkFileOneDriveApiInstance = new OneDriveApi(appConfig); checkFileOneDriveApiInstance.initialise(); if (parentItem.driveId == appConfig.defaultDriveId) { // getPathDetailsByDriveId is only reliable when the driveId is our driveId fileDetailsFromOneDrive = checkFileOneDriveApiInstance.getPathDetailsByDriveId(parentItem.driveId, fileToUpload); } else { // We need to curate a response by listing the children of this parentItem.driveId and parentItem.id , without traversing directories // So that IF the file is on a Shared Folder, it can be found, and, if it exists, checked correctly fileDetailsFromOneDrive = searchDriveItemForFile(parentItem.driveId, parentItem.id, fileToUpload); // Was the file found? if (fileDetailsFromOneDrive.type() != JSONType.object) { // No .... throw new OneDriveException(404, "Name not found via searchDriveItemForFile"); } } // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory checkFileOneDriveApiInstance.releaseCurlEngine(); checkFileOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); // Portable Operating System Interface (POSIX) testing of JSON response from OneDrive API if (hasName(fileDetailsFromOneDrive)) { // Perform the POSIX evaluation test against the names if (performPosixTest(baseName(fileToUpload), fileDetailsFromOneDrive["name"].str)) { throw new PosixException(baseName(fileToUpload), fileDetailsFromOneDrive["name"].str); } } else { throw new JsonResponseException("Unable to perform POSIX test as the OneDrive API request generated an invalid JSON response"); } // If we get to this point, the OneDrive API returned a 200 OK with valid JSON data that indicates a 'file' exists at this location already // and that it matches the POSIX filename of the local item we are trying to upload as a new file if (verboseLogging) {addLogEntry("The file we are attempting to upload as a new file already exists on Microsoft OneDrive: " ~ fileToUpload, ["verbose"]);} // No 404 or otherwise was triggered, meaning that the file already exists online and passes the POSIX test ... if (debugLogging) {addLogEntry("fileDetailsFromOneDrive after exist online check: " ~ to!string(fileDetailsFromOneDrive), ["debug"]);} // Does the data from online match our local file that we are attempting to upload as a new file? if (!disableUploadValidation && performUploadIntegrityValidationChecks(fileDetailsFromOneDrive, fileToUpload, thisFileSize)) { // Save online item details to the database saveItem(fileDetailsFromOneDrive); } else { // The local file we are attempting to upload as a new file is different to the existing file online if (debugLogging) {addLogEntry("Triggering newfile upload target already exists edge case, where the online item does not match what we are trying to upload", ["debug"]);} // Issue #2626 | Case 2-2 (resync) // If the 'online' file is newer, this will be overwritten with the file from the local filesystem - potentially constituting online data loss // The file 'version history' online will have to be used to 'recover' the prior online file string changedItemParentDriveId = fileDetailsFromOneDrive["parentReference"]["driveId"].str; string changedItemId = fileDetailsFromOneDrive["id"].str; addLogEntry("Skipping uploading this item as a new file, will upload as a modified file (online file already exists): " ~ fileToUpload); // In order for the processing of the local item as a 'changed' item, unfortunately we need to save the online data of the existing online file to the local DB saveItem(fileDetailsFromOneDrive); // Which file is technically newer? The local file or the remote file? Item onlineFile = makeItem(fileDetailsFromOneDrive); SysTime localModifiedTime = timeLastModified(fileToUpload).toUTC(); SysTime onlineModifiedTime = onlineFile.mtime; // Reduce time resolution to seconds before comparing localModifiedTime.fracSecs = Duration.zero; onlineModifiedTime.fracSecs = Duration.zero; // Which file is newer? if (localModifiedTime >= onlineModifiedTime) { // Upload the locally modified file as-is, as it is newer uploadChangedLocalFileToOneDrive([changedItemParentDriveId, changedItemId, fileToUpload]); } else { // Online is newer, rename local, then upload the renamed file // We need to know the renamed path so we can upload it string renamedPath; // Rename the local path - we WANT this to occur regardless of bypassDataPreservation setting safeBackup(fileToUpload, dryRun, false, renamedPath); // Upload renamed local file as a new file uploadNewFile(renamedPath); // Process the database entry removal for the original file. In a --dry-run scenario, this is being done against a DB copy. // This is done so we can download the newer online file itemDB.deleteById(changedItemParentDriveId, changedItemId); } } } catch (OneDriveException exception) { // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory checkFileOneDriveApiInstance.releaseCurlEngine(); checkFileOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); // If we get a 404 .. the file is not online .. this is what we want .. file does not exist online if (exception.httpStatusCode == 404) { // The file has been checked, client side filtering checked, does not exist online - we need to upload it if (debugLogging) {addLogEntry("fileDetailsFromOneDrive = checkFileOneDriveApiInstance.getPathDetailsByDriveId(parentItem.driveId, fileToUpload); generated a 404 - file does not exist online - must upload it", ["debug"]);} uploadFailed = performNewFileUpload(parentItem, fileToUpload, thisFileSize); } else { // some other error // Default operation if not 408,429,503,504 errors // - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance // Display what the error is displayOneDriveErrorMessage(exception.msg, thisFunctionName); } } catch (PosixException e) { // Display POSIX error message displayPosixErrorMessage(e.msg); addLogEntry("ERROR: Requested file to upload has a 'case-insensitive match' to an existing item on Microsoft OneDrive online."); addLogEntry("ERROR: To resolve, rename this local file: " ~ fileToUpload); addLogEntry("Skipping uploading this new file due to 'case-insensitive match': " ~ fileToUpload); uploadFailed = true; } catch (JsonResponseException e) { // Display JSON error message if (debugLogging) {addLogEntry(e.msg, ["debug"]);} uploadFailed = true; } } else { // skip file upload - insufficient space to upload addLogEntry("Skipping uploading this new file as it exceeds the available free space on Microsoft OneDrive: " ~ fileToUpload); uploadFailed = true; } } else { // Skip file upload - too large addLogEntry("Skipping uploading this new file as it exceeds the maximum size allowed by Microsoft OneDrive: " ~ fileToUpload); uploadFailed = true; } } else { // why was the parent path not in the database? if (canFind(posixViolationPaths, parentPath)) { addLogEntry("ERROR: POSIX 'case-insensitive match' for the parent path which violates the Microsoft OneDrive API namespace convention."); } else { addLogEntry("ERROR: Parent path is not in the database or online."); } addLogEntry("ERROR: Unable to upload this file: " ~ fileToUpload); uploadFailed = true; } } else { // Unable to read local file addLogEntry("Skipping uploading this file as it cannot be read (file permissions or file corruption): " ~ fileToUpload); uploadFailed = true; } } else { // File disappeared before upload addLogEntry("File disappeared locally before upload: " ~ fileToUpload); // dont set uploadFailed = true; as the file disappeared before upload, thus nothing here failed } // Upload success or failure? if (!uploadFailed) { // Update the 'cachedOnlineDriveData' record for this 'dbItem.driveId' so that this is tracked as accurately as possible for other threads updateDriveDetailsCache(parentItem.driveId, cachedOnlineDriveData.quotaRestricted, cachedOnlineDriveData.quotaAvailable, thisFileSize); } else { // Need to add this to fileUploadFailures to capture at the end fileUploadFailures ~= fileToUpload; } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Perform the actual upload to OneDrive bool performNewFileUpload(Item parentItem, string fileToUpload, long thisFileSize) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Assume that by default the upload fails bool uploadFailed = true; // OneDrive API Upload Response JSONValue uploadResponse; // Create the OneDriveAPI Upload Instance OneDriveApi uploadFileOneDriveApiInstance; // Capture what time this upload started SysTime uploadStartTime = Clock.currTime(); // Is this a dry-run scenario? if (!dryRun) { // Not a dry-run situation // Do we use simpleUpload or create an upload session? bool useSimpleUpload = false; if (thisFileSize <= sessionThresholdFileSize) { useSimpleUpload = true; } // We can only upload zero size files via simpleFileUpload regardless of account type // Reference: https://github.com/OneDrive/onedrive-api-docs/issues/53 // Additionally, only where file size is < 4MB should be uploaded by simpleUpload - everything else should use a session to upload if ((thisFileSize == 0) || (useSimpleUpload)) { try { // Initialise API for simple upload uploadFileOneDriveApiInstance = new OneDriveApi(appConfig); uploadFileOneDriveApiInstance.initialise(); // Attempt to upload the zero byte file using simpleUpload for all account types uploadResponse = uploadFileOneDriveApiInstance.simpleUpload(fileToUpload, parentItem.driveId, parentItem.id, baseName(fileToUpload)); uploadFailed = false; addLogEntry("Uploading new file: " ~ fileToUpload ~ " ... done", fileTransferNotifications()); // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory uploadFileOneDriveApiInstance.releaseCurlEngine(); uploadFileOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); } catch (OneDriveException exception) { // An error was responded with - what was it // Default operation if not 408,429,503,504 errors // - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance // Display what the error is addLogEntry("Uploading new file: " ~ fileToUpload ~ " ... failed!", ["info", "notify"]); displayOneDriveErrorMessage(exception.msg, thisFunctionName); // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory uploadFileOneDriveApiInstance.releaseCurlEngine(); uploadFileOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); } catch (FileException e) { // display the error message addLogEntry("Uploading new file: " ~ fileToUpload ~ " ... failed!", ["info", "notify"]); displayFileSystemErrorMessage(e.msg, thisFunctionName); // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory uploadFileOneDriveApiInstance.releaseCurlEngine(); uploadFileOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); } } else { // Initialise API for session upload uploadFileOneDriveApiInstance = new OneDriveApi(appConfig); uploadFileOneDriveApiInstance.initialise(); // Session Upload for this criteria: // - Personal Account and file size > 4MB // - All Business | Office365 | SharePoint files > 0 bytes JSONValue uploadSessionData; // As this is a unique thread, the sessionFilePath for where we save the data needs to be unique // The best way to do this is generate a 10 digit alphanumeric string, and use this as the file extension string threadUploadSessionFilePath = appConfig.uploadSessionFilePath ~ "." ~ generateAlphanumericString(); // Attempt to upload the > 4MB file using an upload session for all account types try { // Create the Upload Session uploadSessionData = createSessionForFileUpload(uploadFileOneDriveApiInstance, fileToUpload, parentItem.driveId, parentItem.id, baseName(fileToUpload), null, threadUploadSessionFilePath); } catch (OneDriveException exception) { // An error was responded with - what was it // Default operation if not 408,429,503,504 errors // - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance // Display what the error is addLogEntry("Uploading new file: " ~ fileToUpload ~ " ... failed!", ["info", "notify"]); displayOneDriveErrorMessage(exception.msg, thisFunctionName); } catch (FileException e) { // display the error message addLogEntry("Uploading new file: " ~ fileToUpload ~ " ... failed!", ["info", "notify"]); displayFileSystemErrorMessage(e.msg, thisFunctionName); } // Do we have a valid session URL that we can use ? if (uploadSessionData.type() == JSONType.object) { // This is a valid JSON object bool sessionDataValid = true; // Validate that we have the following items which we need if (!hasUploadURL(uploadSessionData)) { sessionDataValid = false; if (debugLogging) {addLogEntry("Session data missing 'uploadUrl'", ["debug"]);} } if (!hasNextExpectedRanges(uploadSessionData)) { sessionDataValid = false; if (debugLogging) {addLogEntry("Session data missing 'nextExpectedRanges'", ["debug"]);} } if (!hasLocalPath(uploadSessionData)) { sessionDataValid = false; if (debugLogging) {addLogEntry("Session data missing 'localPath'", ["debug"]);} } if (sessionDataValid) { // We have a valid Upload Session Data we can use try { // Try and perform the upload session uploadResponse = performSessionFileUpload(uploadFileOneDriveApiInstance, thisFileSize, uploadSessionData, threadUploadSessionFilePath); if (uploadResponse.type() == JSONType.object) { uploadFailed = false; addLogEntry("Uploading new file: " ~ fileToUpload ~ " ... done", fileTransferNotifications()); } else { addLogEntry("Uploading new file: " ~ fileToUpload ~ " ... failed!", ["info", "notify"]); uploadFailed = true; } } catch (OneDriveException exception) { // Default operation if not 408,429,503,504 errors // - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance // Display what the error is addLogEntry("Uploading new file: " ~ fileToUpload ~ " ... failed!", ["info", "notify"]); displayOneDriveErrorMessage(exception.msg, thisFunctionName); } } else { // No Upload URL or nextExpectedRanges or localPath .. not a valid JSON we can use if (verboseLogging) {addLogEntry("Session data is missing required elements to perform a session upload.", ["verbose"]);} addLogEntry("Uploading new file: " ~ fileToUpload ~ " ... failed!", ["info", "notify"]); } } else { // Create session Upload URL failed addLogEntry("Uploading new file: " ~ fileToUpload ~ " ... failed!", ["info", "notify"]); } // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory uploadFileOneDriveApiInstance.releaseCurlEngine(); uploadFileOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); } } else { // We are in a --dry-run scenario uploadResponse = createFakeResponse(fileToUpload); uploadFailed = false; addLogEntry("Uploading new file: " ~ fileToUpload ~ " ... done", fileTransferNotifications()); } // If no upload failure, calculate transfer metrics, perform integrity validation if (!uploadFailed) { // Upload did not fail ... // As no upload failure, calculate transfer metrics in a consistent manner displayTransferMetrics(fileToUpload, thisFileSize, uploadStartTime, Clock.currTime()); // OK as the upload did not fail, we need to save the response from OneDrive, but it has to be a valid JSON response if (uploadResponse.type() == JSONType.object) { // check if the path still exists locally before we try to set the file times online - as short lived files, whilst we uploaded it - it may not exist locally already if (exists(fileToUpload)) { // Are we in a --dry-run scenario if (!dryRun) { bool uploadIntegrityPassed; // Check the integrity of the uploaded file, if the local file still exists uploadIntegrityPassed = performUploadIntegrityValidationChecks(uploadResponse, fileToUpload, thisFileSize); // Update the file modified time on OneDrive and save item details to database // Update the item's metadata on OneDrive SysTime mtime = timeLastModified(fileToUpload).toUTC(); mtime.fracSecs = Duration.zero; string newFileId = uploadResponse["id"].str; string newFileETag = uploadResponse["eTag"].str; // Attempt to update the online date time stamp based on our local data if (appConfig.accountType == "personal") { // Business | SharePoint we used a session to upload the data, thus, local timestamps are given when the session is created uploadLastModifiedTime(parentItem, parentItem.driveId, newFileId, mtime, newFileETag); } else { // Due to https://github.com/OneDrive/onedrive-api-docs/issues/935 Microsoft modifies all PDF, MS Office & HTML files with added XML content. It is a 'feature' of SharePoint. // This means that the file which was uploaded, is potentially no longer the file we have locally // There are 2 ways to solve this: // 1. Download the modified file immediately after upload as per v2.4.x (default) // 2. Create a new online version of the file, which then contributes to the users 'quota' if (!uploadIntegrityPassed) { // upload integrity check failed // We do not want to create a new online file version .. unless configured to do so if (!appConfig.getValueBool("create_new_file_version")) { // are we in an --upload-only scenario if(!uploadOnly){ // Download the now online modified file addLogEntry("WARNING: Microsoft OneDrive modified your uploaded file via its SharePoint 'enrichment' feature. To keep your local and online versions consistent, the altered file will now be downloaded."); addLogEntry("WARNING: Please refer to https://github.com/OneDrive/onedrive-api-docs/issues/935 for further details."); // Download the file directly using the prior upload JSON response downloadFileItem(uploadResponse, true); } else { // --upload-only being used // we are not downloading a file, warn that file differences will exist addLogEntry("WARNING: The file uploaded to Microsoft OneDrive has been modified through its SharePoint 'enrichment' process and no longer matches your local version."); addLogEntry("WARNING: The online metadata will now be modified to match your local file which will create a new file version."); addLogEntry("WARNING: Please refer to https://github.com/OneDrive/onedrive-api-docs/issues/935 for further details."); // Create a new online version of the file by updating the metadata - this ensures that the file we uploaded is the file online uploadLastModifiedTime(parentItem, parentItem.driveId, newFileId, mtime, newFileETag); } } else { // Create a new online version of the file by updating the metadata, which negates the need to download the file uploadLastModifiedTime(parentItem, parentItem.driveId, newFileId, mtime, newFileETag); } } else { // integrity checks passed // save the uploadResponse to the database saveItem(uploadResponse); } } } // Are we in an --upload-only & --remove-source-files scenario? // Use actual config values as we are doing an upload session recovery if (localDeleteAfterUpload) { // Log that we are deleting a local item addLogEntry("Removing local file as --upload-only & --remove-source-files configured"); // Are we in a --dry-run scenario? if (!dryRun) { // No --dry-run ... process local file delete if (debugLogging) {addLogEntry("Removing local file: " ~ fileToUpload, ["debug"]);} safeRemove(fileToUpload); } } } else { // will be removed in different event! addLogEntry("File disappeared locally after upload: " ~ fileToUpload); } } else { // Log that an invalid JSON object was returned if (debugLogging) {addLogEntry("uploadFileOneDriveApiInstance.simpleUpload or session.upload call returned an invalid JSON Object from the OneDrive API", ["debug"]);} } } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // Return upload status return uploadFailed; } // Create the OneDrive Upload Session JSONValue createSessionForFileUpload(OneDriveApi activeOneDriveApiInstance, string fileToUpload, string parentDriveId, string parentId, string filename, string eTag, string threadUploadSessionFilePath) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Upload file via a OneDrive API session JSONValue uploadSession; // Calculate modification time SysTime localFileLastModifiedTime = timeLastModified(fileToUpload).toUTC(); localFileLastModifiedTime.fracSecs = Duration.zero; // Construct the fileSystemInfo JSON component needed to create the Upload Session JSONValue fileSystemInfo = [ "item": JSONValue([ "@microsoft.graph.conflictBehavior": JSONValue("replace"), "fileSystemInfo": JSONValue([ "lastModifiedDateTime": localFileLastModifiedTime.toISOExtString() ]) ]) ]; // Try to create the upload session for this file uploadSession = activeOneDriveApiInstance.createUploadSession(parentDriveId, parentId, filename, eTag, fileSystemInfo); if (uploadSession.type() == JSONType.object) { // a valid session object was created if ("uploadUrl" in uploadSession) { // Add the file path we are uploading to this JSON Session Data uploadSession["localPath"] = fileToUpload; // Save this session saveSessionFile(threadUploadSessionFilePath, uploadSession); } } else { // no valid session was created if (verboseLogging) {addLogEntry("Creation of OneDrive API Upload Session failed.", ["verbose"]);} // return upload() will return a JSONValue response, create an empty JSONValue response to return uploadSession = null; } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // Return the JSON return uploadSession; } // Save the session upload data void saveSessionFile(string threadUploadSessionFilePath, JSONValue uploadSessionData) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } try { std.file.write(threadUploadSessionFilePath, uploadSessionData.toString()); } catch (FileException e) { // display the error message displayFileSystemErrorMessage(e.msg, thisFunctionName); } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Perform the upload of file via the Upload Session that was created JSONValue performSessionFileUpload(OneDriveApi activeOneDriveApiInstance, long thisFileSize, JSONValue uploadSessionData, string threadUploadSessionFilePath) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Response for upload JSONValue uploadResponse; // Session JSON needs to contain valid elements // Get the offset details long fragmentSize = 10 * 2^^20; // 10 MiB size_t fragmentCount = 0; long fragSize = 0; long offset = uploadSessionData["nextExpectedRanges"][0].str.splitter('-').front.to!long; size_t expected_total_fragments = cast(size_t) ceil(double(thisFileSize) / double(fragmentSize)); long start_unix_time = Clock.currTime.toUnixTime(); int h, m, s; string etaString; string uploadLogEntry = "Uploading: " ~ uploadSessionData["localPath"].str ~ " ... "; // If we get a 404, create a new upload session and store it here JSONValue newUploadSession; // Start the session upload using the active API instance for this thread while (true) { fragmentCount++; if (debugLogging) {addLogEntry("Fragment: " ~ to!string(fragmentCount) ~ " of " ~ to!string(expected_total_fragments), ["debug"]);} // What ETA string do we use? auto eta = calc_eta((fragmentCount -1), expected_total_fragments, start_unix_time); if (eta == 0) { // Initial calculation ... etaString = format!"| ETA --:--:--"; } else { // we have at least an ETA provided dur!"seconds"(eta).split!("hours", "minutes", "seconds")(h, m, s); etaString = format!"| ETA %02d:%02d:%02d"( h, m, s); } // Calculate this progress output auto ratio = cast(double)(fragmentCount -1) / expected_total_fragments; // Convert the ratio to a percentage and format it to two decimal places string percentage = leftJustify(format("%d%%", cast(int)(ratio * 100)), 5, ' '); addLogEntry(uploadLogEntry ~ percentage ~ etaString, ["consoleOnly"]); // What fragment size will be used? if (debugLogging) {addLogEntry("fragmentSize: " ~ to!string(fragmentSize) ~ " offset: " ~ to!string(offset) ~ " thisFileSize: " ~ to!string(thisFileSize), ["debug"]);} fragSize = fragmentSize < thisFileSize - offset ? fragmentSize : thisFileSize - offset; if (debugLogging) {addLogEntry("Using fragSize: " ~ to!string(fragSize), ["debug"]);} // fragSize must not be a negative value if (fragSize < 0) { // Session upload will fail // not a JSON object - fragment upload failed if (verboseLogging) {addLogEntry("File upload session failed - invalid calculation of fragment size", ["verbose"]);} if (exists(threadUploadSessionFilePath)) { remove(threadUploadSessionFilePath); } // set uploadResponse to null as error uploadResponse = null; return uploadResponse; } // If the resume upload fails, we need to check for a return code here try { uploadResponse = activeOneDriveApiInstance.uploadFragment( uploadSessionData["uploadUrl"].str, uploadSessionData["localPath"].str, offset, fragSize, thisFileSize ); } catch (OneDriveException exception) { // if a 100 uploadResponse is generated, continue if (exception.httpStatusCode == 100) { continue; } // There was an error uploadResponse from OneDrive when uploading the file fragment if (exception.httpStatusCode == 404) { // The upload session was not found .. ?? we just created it .. maybe the backend is still creating it ? if (debugLogging) {addLogEntry("The upload session was not found .... re-create session");} newUploadSession = createSessionForFileUpload(activeOneDriveApiInstance, uploadSessionData["localPath"].str, uploadSessionData["targetDriveId"].str, uploadSessionData["targetParentId"].str, baseName(uploadSessionData["localPath"].str), null, threadUploadSessionFilePath); } // Issue https://github.com/abraunegg/onedrive/issues/2747 // if a 416 uploadResponse is generated, continue if (exception.httpStatusCode == 416) { continue; } // Handle transient errors: // 408 - Request Time Out // 429 - Too Many Requests // 503 - Service Unavailable // 504 - Gateway Timeout // Insert a new line as well, so that the below error is inserted on the console in the right location if (verboseLogging) {addLogEntry("Fragment upload failed - received an exception response from OneDrive API", ["verbose"]);} // display what the error is if (exception.httpStatusCode != 404) { displayOneDriveErrorMessage(exception.msg, thisFunctionName); } // retry fragment upload in case error is transient if (verboseLogging) {addLogEntry("Retrying fragment upload", ["verbose"]);} try { // retry string effectiveRetryUploadURL; string effectiveLocalPath; // If we re-created the session, use the new data on re-try if ("uploadUrl" in newUploadSession) { // get this from 'newUploadSession' effectiveRetryUploadURL = newUploadSession["uploadUrl"].str; effectiveLocalPath = newUploadSession["localPath"].str; } else { // get this from the original input effectiveRetryUploadURL = uploadSessionData["uploadUrl"].str; effectiveLocalPath = uploadSessionData["localPath"].str; } // retry the fragment upload uploadResponse = activeOneDriveApiInstance.uploadFragment( effectiveRetryUploadURL, effectiveLocalPath, offset, fragSize, thisFileSize ); } catch (OneDriveException e) { // OneDrive threw another error on retry if (verboseLogging) {addLogEntry("Retry to upload fragment failed", ["verbose"]);} // display what the error is displayOneDriveErrorMessage(e.msg, thisFunctionName); // set uploadResponse to null as the fragment upload was in error twice uploadResponse = null; } catch (std.exception.ErrnoException e) { // There was a file system error - display the error message displayFileSystemErrorMessage(e.msg, thisFunctionName); return uploadResponse; } } catch (ErrnoException e) { // There was a file system error // display the error message displayFileSystemErrorMessage(e.msg, thisFunctionName); uploadResponse = null; return uploadResponse; } // was the fragment uploaded without issue? if (uploadResponse.type() == JSONType.object){ offset += fragmentSize; if (offset >= thisFileSize) { break; } // update the uploadSessionData details uploadSessionData["expirationDateTime"] = uploadResponse["expirationDateTime"]; uploadSessionData["nextExpectedRanges"] = uploadResponse["nextExpectedRanges"]; saveSessionFile(threadUploadSessionFilePath, uploadSessionData); } else { // not a JSON object - fragment upload failed if (verboseLogging) {addLogEntry("File upload session failed - invalid response from OneDrive API", ["verbose"]);} // cleanup session data if (exists(threadUploadSessionFilePath)) { remove(threadUploadSessionFilePath); } // set uploadResponse to null as error uploadResponse = null; return uploadResponse; } } // upload complete long end_unix_time = Clock.currTime.toUnixTime(); auto upload_duration = cast(int)(end_unix_time - start_unix_time); dur!"seconds"(upload_duration).split!("hours", "minutes", "seconds")(h, m, s); etaString = format!"| DONE in %02d:%02d:%02d"( h, m, s); addLogEntry(uploadLogEntry ~ "100% " ~ etaString, ["consoleOnly"]); // Remove session file if it exists if (exists(threadUploadSessionFilePath)) { remove(threadUploadSessionFilePath); } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // Return the session upload response return uploadResponse; } // Delete an item on OneDrive void uploadDeletedItem(Item itemToDelete, string path) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } OneDriveApi uploadDeletedItemOneDriveApiInstance; // Are we in a situation where we HAVE to keep the data online - do not delete the remote object if (noRemoteDelete) { if ((itemToDelete.type == ItemType.dir)) { // Do not process remote directory delete if (verboseLogging) {addLogEntry("Skipping remote directory delete as --upload-only & --no-remote-delete configured", ["verbose"]);} } else { // Do not process remote file delete if (verboseLogging) {addLogEntry("Skipping remote file delete as --upload-only & --no-remote-delete configured", ["verbose"]);} } } else { // Is this a --download-only operation? if (!appConfig.getValueBool("download_only")) { // Process the delete - delete the object online addLogEntry("Deleting item from Microsoft OneDrive: " ~ path, fileTransferNotifications()); bool flagAsBigDelete = false; Item[] children; long itemsToDelete; if ((itemToDelete.type == ItemType.dir)) { // Query the database - how many objects will this remove? children = getChildren(itemToDelete.driveId, itemToDelete.id); // Count the returned items + the original item (1) itemsToDelete = count(children) + 1; if (debugLogging) {addLogEntry("Number of items online to delete: " ~ to!string(itemsToDelete), ["debug"]);} } else { itemsToDelete = 1; } // Clear array children = []; // A local delete of a file|folder when using --monitor will issue a inotify event, which will trigger the local & remote data immediately be deleted // The user may also be --sync process, so we are checking if something was deleted between application use if (itemsToDelete >= appConfig.getValueLong("classify_as_big_delete")) { // A big delete has been detected flagAsBigDelete = true; if (!appConfig.getValueBool("force")) { addLogEntry("ERROR: An attempt to remove a large volume of data from OneDrive has been detected. Exiting client to preserve your data on Microsoft OneDrive"); addLogEntry("ERROR: The total number of items being deleted is: " ~ to!string(itemsToDelete)); addLogEntry("ERROR: To delete a large volume of data use --force or increase the config value 'classify_as_big_delete' to a larger value"); addLogEntry("ERROR: Optionally, perform a --resync to reset your local synchronisation state"); // Must exit here to preserve data on online , allow logging to be done forceExit(); } } // Are we in a --dry-run scenario? if (!dryRun) { // We are not in a dry run scenario if (debugLogging) { addLogEntry("itemToDelete: " ~ to!string(itemToDelete), ["debug"]); // what item are we trying to delete? addLogEntry("Attempting to delete this single item id: " ~ itemToDelete.id ~ " from drive: " ~ itemToDelete.driveId, ["debug"]); } // Configure these item variables to handle OneDrive Business Shared Folder Deletion Item actualItemToDelete; Item remoteShortcutLinkItem; // OneDrive Shared Folder Link Handling // - If the item to delete is on a remote drive ... technically we do not own this and should not be deleting this online // We should however be deleting the 'link' in our account online, and, remove the DB link entries (root / folder DB Tie records) bool businessSharingEnabled = false; // OneDrive Business Shared Folder Deletion Handling // Is this a Business Account with Sync Business Shared Items enabled? if ((appConfig.accountType == "business") && (appConfig.getValueBool("sync_business_shared_items"))) { // Syncing Business Shared Items is enabled businessSharingEnabled = true; } // Is this a 'personal' account type or is this a Business Account with Sync Business Shared Items enabled? if ((appConfig.accountType == "personal") || businessSharingEnabled) { // Personal account type or syncing Business Shared Items is enabled // Is the 'drive' where this is to be deleted on 'our' drive or is this a remote 'drive' ? if (itemToDelete.driveId != appConfig.defaultDriveId) { // The item to delete is on a remote drive ... this must be handled in a specific way if (itemToDelete.type == ItemType.dir) { // Select the 'remote' database object type using these details // Get the DB entry for this 'remote' item itemDB.selectRemoteTypeByRemoteDriveId(itemToDelete.driveId, itemToDelete.id, remoteShortcutLinkItem); } } // We potentially now have the correct details to delete in our account if (remoteShortcutLinkItem.type == ItemType.remote) { // A valid 'remote' DB entry was returned if (debugLogging) {addLogEntry("remoteShortcutLinkItem: " ~ to!string(remoteShortcutLinkItem), ["debug"]);} // Set actualItemToDelete to this data actualItemToDelete = remoteShortcutLinkItem; // Delete the shortcut reference in the local database if (appConfig.accountType == "personal") { // Personal Shared Folder deletion message if (debugLogging) {addLogEntry("Deleted OneDrive Personal Shared Folder 'Shortcut Link'", ["debug"]);} } else { // Business Shared Folder deletion message if (debugLogging) {addLogEntry("Deleted OneDrive Business Shared Folder 'Shortcut Link'", ["debug"]);} } // Perform action deletion from database itemDB.deleteById(remoteShortcutLinkItem.driveId, remoteShortcutLinkItem.id); } else { // No data was returned, use the original data actualItemToDelete = itemToDelete; } } else { // Set actualItemToDelete to original data actualItemToDelete = itemToDelete; } // Try the online deletion using the 'actualItemToDelete' values try { // Create new OneDrive API Instance uploadDeletedItemOneDriveApiInstance = new OneDriveApi(appConfig); uploadDeletedItemOneDriveApiInstance.initialise(); if (!permanentDelete) { // Perform the delete via the default OneDrive API instance uploadDeletedItemOneDriveApiInstance.deleteById(actualItemToDelete.driveId, actualItemToDelete.id); } else { // Perform the permanent delete via the default OneDrive API instance uploadDeletedItemOneDriveApiInstance.permanentDeleteById(actualItemToDelete.driveId, actualItemToDelete.id); } // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory uploadDeletedItemOneDriveApiInstance.releaseCurlEngine(); uploadDeletedItemOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); } catch (OneDriveException e) { if (e.httpStatusCode == 404) { // item.id, item.eTag could not be found on the specified driveId if (verboseLogging) {addLogEntry("OneDrive reported: The resource could not be found to be deleted.", ["verbose"]);} } // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory uploadDeletedItemOneDriveApiInstance.releaseCurlEngine(); uploadDeletedItemOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); } // Delete the reference in the local database - use the original input itemDB.deleteById(itemToDelete.driveId, itemToDelete.id); // Was the original item a 'Shared Folder' ? if (remoteShortcutLinkItem.type == ItemType.remote) { // Are there any other 'children' for itemToDelete parent ... this parent may have other Shared Folders added to our account that we have not removed .. Item[] remainingChildren; remainingChildren ~= itemDB.selectChildren(itemToDelete.driveId, itemToDelete.parentId); // Only if there are zero children for this parent item, remove the 'root' record if (count(remainingChildren) == 0) { // No more children for this parental object itemDB.deleteById(itemToDelete.driveId, itemToDelete.parentId); } } } else { // log that this is a dry-run activity addLogEntry("dry run - no delete activity"); } } else { // --download-only operation, we are not uploading any delete event to OneDrive if (debugLogging) {addLogEntry("Not pushing local delete to Microsoft OneDrive due to --download-only being used", ["debug"]);} } } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Get the children of an item id from the database Item[] getChildren(string driveId, string id) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } Item[] children; children ~= itemDB.selectChildren(driveId, id); foreach (Item child; children) { if (child.type != ItemType.file) { // recursively get the children of this child children ~= getChildren(child.driveId, child.id); } } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return the database records return children; } // Perform a 'reverse' delete of all child objects on OneDrive void performReverseDeletionOfOneDriveItems(Item[] children, Item itemToDelete) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Log what is happening if (debugLogging) {addLogEntry("Attempting a reverse delete of all child objects from OneDrive", ["debug"]);} // Create a new API Instance for this thread and initialise it OneDriveApi performReverseDeletionOneDriveApiInstance; performReverseDeletionOneDriveApiInstance = new OneDriveApi(appConfig); performReverseDeletionOneDriveApiInstance.initialise(); foreach_reverse (Item child; children) { // Log the action if (debugLogging) {addLogEntry("Attempting to delete this child item id: " ~ child.id ~ " from drive: " ~ child.driveId, ["debug"]);} if (!permanentDelete) { // Perform the delete via the default OneDrive API instance performReverseDeletionOneDriveApiInstance.deleteById(child.driveId, child.id, child.eTag); } else { // Perform the permanent delete via the default OneDrive API instance performReverseDeletionOneDriveApiInstance.permanentDeleteById(child.driveId, child.id, child.eTag); } // delete the child reference in the local database itemDB.deleteById(child.driveId, child.id); } // Log the action if (debugLogging) {addLogEntry("Attempting to delete this parent item id: " ~ itemToDelete.id ~ " from drive: " ~ itemToDelete.driveId, ["debug"]);} if (!permanentDelete) { // Perform the delete via the default OneDrive API instance performReverseDeletionOneDriveApiInstance.deleteById(itemToDelete.driveId, itemToDelete.id, itemToDelete.eTag); } else { // Perform the permanent delete via the default OneDrive API instance performReverseDeletionOneDriveApiInstance.permanentDeleteById(itemToDelete.driveId, itemToDelete.id, itemToDelete.eTag); } // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory performReverseDeletionOneDriveApiInstance.releaseCurlEngine(); performReverseDeletionOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Create a fake OneDrive response suitable for use with saveItem JSONValue createFakeResponse(string path) { import std.digest.sha; // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Generate a simulated JSON response which can be used // At a minimum we need: // 1. eTag // 2. cTag // 3. fileSystemInfo // 4. file or folder. if file, hash of file // 5. id // 6. name // 7. parent reference string fakeDriveId = appConfig.defaultDriveId; string fakeRootId = appConfig.defaultRootId; SysTime mtime = exists(path) ? timeLastModified(path).toUTC() : Clock.currTime(UTC()); auto sha1 = new SHA1Digest(); ubyte[] fakedOneDriveItemValues = sha1.digest(path); JSONValue fakeResponse; string parentPath = dirName(path); if (parentPath != "." && exists(path)) { foreach (searchDriveId; onlineDriveDetails.keys) { Item databaseItem; if (itemDB.selectByPath(parentPath, searchDriveId, databaseItem)) { fakeDriveId = databaseItem.driveId; fakeRootId = databaseItem.id; break; // Exit loop after finding the first match } } } fakeResponse = [ "id": JSONValue(toHexString(fakedOneDriveItemValues)), "cTag": JSONValue(toHexString(fakedOneDriveItemValues)), "eTag": JSONValue(toHexString(fakedOneDriveItemValues)), "fileSystemInfo": JSONValue([ "createdDateTime": mtime.toISOExtString(), "lastModifiedDateTime": mtime.toISOExtString() ]), "name": JSONValue(baseName(path)), "parentReference": JSONValue([ "driveId": JSONValue(fakeDriveId), "driveType": JSONValue(appConfig.accountType), "id": JSONValue(fakeRootId) ]) ]; if (exists(path)) { if (isDir(path)) { fakeResponse["folder"] = JSONValue(""); } else { string quickXorHash = computeQuickXorHash(path); fakeResponse["file"] = JSONValue([ "hashes": JSONValue(["quickXorHash": JSONValue(quickXorHash)]) ]); } } else { // Assume directory if path does not exist fakeResponse["folder"] = JSONValue(""); } if (debugLogging) {addLogEntry("Generated Fake OneDrive Response: " ~ to!string(fakeResponse), ["debug"]);} // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return the generated fake API response return fakeResponse; } // Save JSON item details into the item database void saveItem(JSONValue jsonItem) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // jsonItem has to be a valid object if (jsonItem.type() == JSONType.object) { // Check if the response JSON has an 'id', otherwise makeItem() fails with 'Key not found: id' if (hasId(jsonItem)) { // Are we in a --upload-only & --remove-source-files scenario? // We do not want to add the item to the database in this situation as there is no local reference to the file post file deletion // If the item is a directory, we need to add this to the DB, if this is a file, we dont add this, the parent path is not in DB, thus any new files in this directory are not added if ((uploadOnly) && (localDeleteAfterUpload) && (isItemFile(jsonItem))) { // Log that we skipping adding item to the local DB and the reason why if (debugLogging) {addLogEntry("Skipping adding to database as --upload-only & --remove-source-files configured", ["debug"]);} } else { // What is the JSON item we are trying to create a DB record with? if (debugLogging) {addLogEntry("saveItem - creating DB item from this JSON: " ~ sanitiseJSONItem(jsonItem), ["debug"]);} // Takes a JSON input and formats to an item which can be used by the database Item item = makeItem(jsonItem); // Is this JSON item a 'root' item? if ((isItemRoot(jsonItem)) && (item.name == "root")) { if (debugLogging) { addLogEntry("Updating DB Item object with correct values as this is a 'root' object", ["debug"]); addLogEntry(" item.parentId = null", ["debug"]); addLogEntry(" item.type = ItemType.root", ["debug"]); } item.parentId = null; // ensures that this database entry has no parent item.type = ItemType.root; // Check for parentReference if (hasParentReference(jsonItem)) { // Set the correct item.driveId if (debugLogging) { addLogEntry("The 'root' JSON Item HAS a parentReference .... setting item.driveId = jsonItem['parentReference']['driveId'].str from the provided JSON record", ["debug"]); string logMessage = format(" item.driveId = '%s'", jsonItem["parentReference"]["driveId"].str); addLogEntry(logMessage, ["debug"]); } item.driveId = jsonItem["parentReference"]["driveId"].str; } // We only should be adding our account 'root' to the database, not shared folder 'root' items if (item.driveId != appConfig.defaultDriveId) { // Shared Folder drive 'root' object .. we dont want this item if (debugLogging) {addLogEntry("NOT adding 'remote root' object to database: " ~ to!string(item), ["debug"]);} return; } } // Issue #3115 - Validate driveId length // What account type is this? if (appConfig.accountType == "personal") { // Test driveId length and validation if the driveId we are testing is not equal to appConfig.defaultDriveId if (item.driveId != appConfig.defaultDriveId) { item.driveId = testProvidedDriveIdForLengthIssue(item.driveId); } } // Add to the local database if (debugLogging) {addLogEntry("Saving this DB item record: " ~ to!string(item), ["debug"]);} itemDB.upsert(item); // If we have a remote drive ID, add this to our list of known drive id's if (!item.remoteDriveId.empty) { // Keep the DriveDetailsCache array with unique entries only DriveDetailsCache cachedOnlineDriveData; if (!canFindDriveId(item.remoteDriveId, cachedOnlineDriveData)) { // Add this driveId to the drive cache if (debugLogging) {addLogEntry("Database item is a remote drive object, need to fetch online details for this drive: " ~ to!string(item.remoteDriveId), ["debug"]);} addOrUpdateOneDriveOnlineDetails(item.remoteDriveId); } } } } else { // log error addLogEntry("ERROR: OneDrive response missing required 'id' element"); addLogEntry("ERROR: " ~ sanitiseJSONItem(jsonItem)); } } else { // log error addLogEntry("ERROR: An error was returned from OneDrive and the resulting response is not a valid JSON object that can be processed."); addLogEntry("ERROR: Increase logging verbosity to assist determining why."); } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Save an already created database object into the database void saveDatabaseItem(Item newDatabaseItem) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Issue #3115 - Personal Account Shared Folder // What account type is this? if (appConfig.accountType == "personal") { // Is this a 'remote' DB record if (newDatabaseItem.type == ItemType.remote) { // Test driveId length and validation if the driveId we are testing is not equal to appConfig.defaultDriveId if (newDatabaseItem.remoteDriveId != appConfig.defaultDriveId) { // Issue #3136, #3139 #3143 // Fetch the actual online record for this item // This returns the actual OneDrive Personal driveId value and is 15 character checked string actualOnlineDriveId = testProvidedDriveIdForLengthIssue(fetchRealOnlineDriveIdentifier(newDatabaseItem.remoteDriveId)); newDatabaseItem.remoteDriveId = actualOnlineDriveId; } } } // Add the database record if (debugLogging) {addLogEntry("Creating a new database record for a new local path that has been created: " ~ to!string(newDatabaseItem), ["debug"]);} itemDB.upsert(newDatabaseItem); // If we have a remote drive ID, add this to our list of known drive id's if (!newDatabaseItem.remoteDriveId.empty) { // Keep the DriveDetailsCache array with unique entries only DriveDetailsCache cachedOnlineDriveData; if (!canFindDriveId(newDatabaseItem.remoteDriveId, cachedOnlineDriveData)) { // Add this driveId to the drive cache if (debugLogging) {addLogEntry("New database record is a remote drive object, need to fetch online details for this drive: " ~ to!string(newDatabaseItem.remoteDriveId), ["debug"]);} addOrUpdateOneDriveOnlineDetails(newDatabaseItem.remoteDriveId); } } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Wrapper function for makeDatabaseItem so we can check to ensure that the item has the required hashes Item makeItem(JSONValue onedriveJSONItem) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Make the DB Item from the JSON data provided Item newDatabaseItem = makeDatabaseItem(onedriveJSONItem); // Is this a 'file' item that has not been deleted? Deleted items have no hash if ((newDatabaseItem.type == ItemType.file) && (!isItemDeleted(onedriveJSONItem))) { // Does this item have a file size attribute? if (hasFileSize(onedriveJSONItem)) { // Is the file size greater than 0? if (onedriveJSONItem["size"].integer > 0) { // Does the DB item have any hashes as per the API provided JSON data? if ((newDatabaseItem.quickXorHash.empty) && (newDatabaseItem.sha256Hash.empty)) { // Odd .. there is no hash for this item .. why is that? // Is there a 'file' JSON element? if ("file" in onedriveJSONItem) { // Microsoft OneDrive OneNote objects will report as files but have 'application/msonenote' and 'application/octet-stream' as mime types if ((isMicrosoftOneNoteMimeType1(onedriveJSONItem)) || (isMicrosoftOneNoteMimeType2(onedriveJSONItem))) { // Debug log output that this is a potential OneNote object if (debugLogging) {addLogEntry("This item is potentially an associated Microsoft OneNote Object Item", ["debug"]);} } else { // Not a Microsoft OneNote Mime Type Object .. string apiWarningMessage = "WARNING: OneDrive API inconsistency - this file does not have any hash: "; // This is computationally expensive .. but we are only doing this if there are no hashes provided bool parentInDatabase = itemDB.idInLocalDatabase(newDatabaseItem.driveId, newDatabaseItem.parentId); // Is the parent id in the database? if (parentInDatabase) { // This is again computationally expensive .. calculate this item path to advise the user the actual path of this item that has no hash string newItemPath = computeItemPath(newDatabaseItem.driveId, newDatabaseItem.parentId) ~ "/" ~ newDatabaseItem.name; addLogEntry(apiWarningMessage ~ newItemPath); } else { // Parent is not in the database .. why? // Check if the parent item had been skipped .. if (newDatabaseItem.parentId in skippedItems) { if (debugLogging) {addLogEntry(apiWarningMessage ~ "newDatabaseItem.parentId listed within skippedItems", ["debug"]);} } else { // Use the item ID .. there is no other reference available, parent is not being skipped, so we should have been able to calculate this - but we could not addLogEntry(apiWarningMessage ~ newDatabaseItem.id); } } } } } } else { // zero file size if (debugLogging) {addLogEntry("This item file is zero size - potentially no hash provided by the OneDrive API", ["debug"]);} } } } // OneDrive Personal Account driveId and remoteDriveId length check // Issue #3072 (https://github.com/abraunegg/onedrive/issues/3072) illustrated that the OneDrive API is inconsistent in response when the Drive ID starts with a zero ('0') // - driveId // - remoteDriveId // // Example: // 024470056F5C3E43 (driveId) // 24470056f5c3e43 (remoteDriveId) // If this is a OneDrive Personal Account, ensure this value is 16 characters, padded by leading zero's if eventually required // What account type is this? if (appConfig.accountType == "personal") { // Check the newDatabaseItem.remoteDriveId if (!newDatabaseItem.remoteDriveId.empty) { // Issue #3136, #3139 #3143 // Test searchItem.driveId length and validation // - This check the length, fetch online value and return a 16 character driveId newDatabaseItem.remoteDriveId = testProvidedDriveIdForLengthIssue(fetchRealOnlineDriveIdentifier(newDatabaseItem.remoteDriveId)); } } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // Return the new database item return newDatabaseItem; } // For OneDrive Personal Accounts, the case sensitivity depending on the API call means the 'driveId' can be uppercase or lowercase // For this application use, this causes issues as, in POSIX environments - 024470056F5C3E43 != 024470056f5c3e43 despite on Windows this being treated as the same // This function does NOT do a 15 character driveId validation string fetchRealOnlineDriveIdentifier(string inputDriveId) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // What are we doing if (debugLogging) { string fetchRealValueLogMessage = format("Fetching actual online 'driveId' value for '%s'", inputDriveId); addLogEntry(fetchRealValueLogMessage, ["debug"]); } // variables for this function JSONValue remoteDriveDetails; OneDriveApi fetchDriveDetailsOneDriveApiInstance; string outputDriveId; // Create new OneDrive API Instance fetchDriveDetailsOneDriveApiInstance = new OneDriveApi(appConfig); fetchDriveDetailsOneDriveApiInstance.initialise(); // Get root details for the provided driveId try { remoteDriveDetails = fetchDriveDetailsOneDriveApiInstance.getDriveIdRoot(inputDriveId); } catch (OneDriveException exception) { if (debugLogging) {addLogEntry("remoteDriveDetails = fetchDriveDetailsOneDriveApiInstance.getDriveIdRoot(inputDriveId) generated a OneDriveException", ["debug"]);} // Default operation if not 408,429,503,504 errors // - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance // Display what the error is displayOneDriveErrorMessage(exception.msg, thisFunctionName); } // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory fetchDriveDetailsOneDriveApiInstance.releaseCurlEngine(); fetchDriveDetailsOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); // Do we have details we can use? if (hasParentReferenceDriveId(remoteDriveDetails)) { // We have a [parentReference][driveId] reference driveId to use outputDriveId = remoteDriveDetails["parentReference"]["driveId"].str; } else { // We dont have a value from online we can use // Test existing driveId length and validation outputDriveId = inputDriveId; } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // Return the outputDriveId return outputDriveId; } // Print the fileDownloadFailures and fileUploadFailures arrays if they are not empty void displaySyncFailures() { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } bool logFailures(string[] failures, string operation) { if (failures.empty) return false; addLogEntry(); addLogEntry("Failed items to " ~ operation ~ " to/from Microsoft OneDrive: " ~ to!string(failures.length)); foreach (failedFile; failures) { addLogEntry("Failed to " ~ operation ~ ": " ~ failedFile, ["info", "notify"]); foreach (searchDriveId; onlineDriveDetails.keys) { Item dbItem; if (itemDB.selectByPath(failedFile, searchDriveId, dbItem)) { addLogEntry("ERROR: Failed " ~ operation ~ " path found in database, must delete this item from the database .. it should not be in there if the file failed to " ~ operation); itemDB.deleteById(dbItem.driveId, dbItem.id); if (dbItem.remoteDriveId != null) { itemDB.deleteById(dbItem.remoteDriveId, dbItem.remoteId); } } } } return true; } bool downloadFailuresLogged = logFailures(fileDownloadFailures, "download"); bool uploadFailuresLogged = logFailures(fileUploadFailures, "upload"); syncFailures = downloadFailuresLogged || uploadFailuresLogged; // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Generate a /delta compatible response - for use when we cant actually use /delta // This is required when the application is configured to use National Azure AD deployments as these do not support /delta queries // The same technique can also be used when we are using --single-directory. The parent objects up to the single directory target can be added, // then once the target of the --single-directory request is hit, all of the children of that path can be queried, giving a much more focused // JSON response which can then be processed, negating the need to continuously traverse the tree and 'exclude' items JSONValue generateDeltaResponse(string pathToQuery = null) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // JSON value which will be responded with JSONValue selfGeneratedDeltaResponse; // Function variables bool remotePathObject = false; Item searchItem; JSONValue rootData; JSONValue driveData; JSONValue pathData; JSONValue topLevelChildren; JSONValue[] childrenData; string nextLink; OneDriveApi generateDeltaResponseOneDriveApiInstance; // Was a path to query passed in? if (pathToQuery.empty) { // Will query for the 'root' pathToQuery = "."; } // Create new OneDrive API Instance generateDeltaResponseOneDriveApiInstance = new OneDriveApi(appConfig); generateDeltaResponseOneDriveApiInstance.initialise(); // Is this a --single-directory invocation? if (!singleDirectoryScope) { // In a --resync scenario, there is no DB data to query, so we have to query the OneDrive API here to get relevant details try { // Query the OneDrive API, using the path, which will query 'our' OneDrive Account pathData = generateDeltaResponseOneDriveApiInstance.getPathDetails(pathToQuery); // Is the path on OneDrive local or remote to our account drive id? if (!isItemRemote(pathData)) { // The path we are seeking is local to our account drive id searchItem.driveId = pathData["parentReference"]["driveId"].str; searchItem.id = pathData["id"].str; } else { // The path we are seeking is remote to our account drive id searchItem.driveId = pathData["remoteItem"]["parentReference"]["driveId"].str; searchItem.id = pathData["remoteItem"]["id"].str; remotePathObject = true; // Issue #3115 - Personal Account Shared Folder // What account type is this? if (appConfig.accountType == "personal") { // Issue #3136, #3139 #3143 // Fetch the actual online record for this item // This returns the actual OneDrive Personal driveId value. The check of 'searchItem.driveId' to comply with 16 characters is done below string actualOnlineDriveId = fetchRealOnlineDriveIdentifier(searchItem.driveId); searchItem.driveId = actualOnlineDriveId; } } } catch (OneDriveException exception) { // Display error message displayOneDriveErrorMessage(exception.msg, thisFunctionName); // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory generateDeltaResponseOneDriveApiInstance.releaseCurlEngine(); generateDeltaResponseOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); // Must force exit here, allow logging to be done forceExit(); } } else { // When setSingleDirectoryScope() was called, the following were set to the correct items, even if the path was remote: // - singleDirectoryScopeDriveId // - singleDirectoryScopeItemId // Reuse these prior set values searchItem.driveId = singleDirectoryScopeDriveId; searchItem.id = singleDirectoryScopeItemId; } // Issue #3072 - Validate searchItem.driveId length // What account type is this? if (appConfig.accountType == "personal") { // Test driveId length and validation if the driveId we are testing is not equal to appConfig.defaultDriveId if (searchItem.driveId != appConfig.defaultDriveId) { searchItem.driveId = testProvidedDriveIdForLengthIssue(searchItem.driveId); } } // Before we get any data from the OneDrive API, flag any child object in the database as out-of-sync for this driveId & and object id // Downgrade ONLY files associated with this driveId and idToQuery if (debugLogging) {addLogEntry("Downgrading all children for this searchItem.driveId (" ~ searchItem.driveId ~ ") and searchItem.id (" ~ searchItem.id ~ ") to an out-of-sync state", ["debug"]);} Item[] drivePathChildren = getChildren(searchItem.driveId, searchItem.id); if (count(drivePathChildren) > 0) { // Children to process and flag as out-of-sync foreach (drivePathChild; drivePathChildren) { // Flag any object in the database as out-of-sync for this driveId & and object id if (debugLogging) {addLogEntry("Downgrading item as out-of-sync: " ~ drivePathChild.id, ["debug"]);} itemDB.downgradeSyncStatusFlag(drivePathChild.driveId, drivePathChild.id); } } // Clear DB response array drivePathChildren = []; // Get drive details for the provided driveId try { driveData = generateDeltaResponseOneDriveApiInstance.getPathDetailsById(searchItem.driveId, searchItem.id); } catch (OneDriveException exception) { // An error was generated if (debugLogging) {addLogEntry("driveData = generateDeltaResponseOneDriveApiInstance.getPathDetailsById(searchItem.driveId, searchItem.id) generated a OneDriveException", ["debug"]);} // Was this a 403 or 404 ? if ((exception.httpStatusCode == 403) || (exception.httpStatusCode == 404)) { // The API call returned a 404 error response if (debugLogging) {addLogEntry("onlineParentData = onlineParentOneDriveApiInstance.getPathDetailsById(parentDriveId, parentObjectId); generated a 404 - shared folder path does not exist online", ["debug"]);} string errorMessage = format("WARNING: The OneDrive Shared Folder link target '%s' cannot be found online using the provided online data.", pathToQuery); // detail what this 404 error response means addLogEntry(); addLogEntry(errorMessage); addLogEntry("WARNING: This is potentially a broken online OneDrive Shared Folder link or you no longer have access to it. Please correct this error online."); addLogEntry(); // Release curl engine generateDeltaResponseOneDriveApiInstance.releaseCurlEngine(); // Free object and memory generateDeltaResponseOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); // Return the generated JSON response return selfGeneratedDeltaResponse; } else { // Default operation if not 408,429,503,504 errors // - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance // Display what the error is displayOneDriveErrorMessage(exception.msg, thisFunctionName); } } // Was a valid JSON response for 'driveData' provided? if (driveData.type() == JSONType.object) { // Dynamic output for a non-verbose run so that the user knows something is happening string generatingDeltaResponseMessage = format("Generating a /delta response from the OneDrive API for this Drive ID: %s and Item ID: %s", searchItem.driveId, searchItem.id); if (appConfig.verbosityCount == 0) { if (!appConfig.suppressLoggingOutput) { addProcessingLogHeaderEntry(generatingDeltaResponseMessage, appConfig.verbosityCount); } } else { if (verboseLogging) {addLogEntry(generatingDeltaResponseMessage, ["verbose"]);} } // Process this initial JSON response if (!isItemRoot(driveData)) { // Are we generating a /delta response for a Shared Folder, if not, then we need to add the drive root details first if (!sharedFolderDeltaGeneration) { // Get root details for the provided driveId try { rootData = generateDeltaResponseOneDriveApiInstance.getDriveIdRoot(searchItem.driveId); } catch (OneDriveException exception) { if (debugLogging) {addLogEntry("rootData = onedrive.getDriveIdRoot(searchItem.driveId) generated a OneDriveException", ["debug"]);} // Default operation if not 408,429,503,504 errors // - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance // Display what the error is displayOneDriveErrorMessage(exception.msg, thisFunctionName); } // Add driveData JSON data to array if (verboseLogging) {addLogEntry("Adding OneDrive root details for processing", ["verbose"]);} childrenData ~= rootData; } } // Add driveData JSON data to array if (verboseLogging) {addLogEntry("Adding OneDrive folder details for processing", ["verbose"]);} childrenData ~= driveData; } else { // driveData is an invalid JSON object addLogEntry("CODING TO DO: The query of OneDrive API to getPathDetailsById generated an invalid JSON response - thus we cant build our own /delta simulated response ... how to handle?"); // Release curl engine generateDeltaResponseOneDriveApiInstance.releaseCurlEngine(); // Free object and memory generateDeltaResponseOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); // Must force exit here, allow logging to be done forceExit(); } // For each child object, query the OneDrive API while (true) { // Check if exitHandlerTriggered is true if (exitHandlerTriggered) { // break out of the 'while (true)' loop break; } // query top level children try { topLevelChildren = generateDeltaResponseOneDriveApiInstance.listChildren(searchItem.driveId, searchItem.id, nextLink); } catch (OneDriveException exception) { // OneDrive threw an error if (debugLogging) { addLogEntry(debugLogBreakType1, ["debug"]); addLogEntry("Query Error: topLevelChildren = generateDeltaResponseOneDriveApiInstance.listChildren(searchItem.driveId, searchItem.id, nextLink)", ["debug"]); addLogEntry("driveId: " ~ searchItem.driveId, ["debug"]); addLogEntry("idToQuery: " ~ searchItem.id, ["debug"]); addLogEntry("nextLink: " ~ nextLink, ["debug"]); } // Default operation if not 408,429,503,504 errors // - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance // Display what the error is displayOneDriveErrorMessage(exception.msg, thisFunctionName); } // Process top level children if (!remotePathObject) { // Main account root folder if (verboseLogging) {addLogEntry("Adding " ~ to!string(count(topLevelChildren["value"].array)) ~ " OneDrive items for processing from the OneDrive 'root' Folder", ["verbose"]);} } else { // Shared Folder if (verboseLogging) {addLogEntry("Adding " ~ to!string(count(topLevelChildren["value"].array)) ~ " OneDrive items for processing from the OneDrive Shared Folder", ["verbose"]);} } foreach (child; topLevelChildren["value"].array) { // Check for any Client Side Filtering here ... we should skip querying the OneDrive API for 'folders' that we are going to just process and skip anyway. // This avoids needless calls to the OneDrive API, and potentially speeds up this process. if (!checkJSONAgainstClientSideFiltering(child)) { // add this child to the array of objects childrenData ~= child; // is this child a folder? if (isItemFolder(child)) { // We have to query this folders children if childCount > 0 if (child["folder"]["childCount"].integer > 0){ // This child folder has children string childIdToQuery = child["id"].str; string childDriveToQuery = child["parentReference"]["driveId"].str; auto childParentPath = child["parentReference"]["path"].str.split(":"); string folderPathToScan = childParentPath[1] ~ "/" ~ child["name"].str; string pathForLogging; // Are we in a --single-directory situation? If we are, the path we are using for logging needs to use the input path as a base if (singleDirectoryScope) { pathForLogging = appConfig.getValueString("single_directory") ~ "/" ~ child["name"].str; } else { pathForLogging = child["name"].str; } // Query the children of this item JSONValue[] grandChildrenData = queryForChildren(childDriveToQuery, childIdToQuery, folderPathToScan, pathForLogging); foreach (grandChild; grandChildrenData.array) { // add the grandchild to the array childrenData ~= grandChild; } } } } } // If a collection exceeds the default page size (200 items), the @odata.nextLink property is returned in the response // to indicate more items are available and provide the request URL for the next page of items. if ("@odata.nextLink" in topLevelChildren) { // Update nextLink to next changeSet bundle if (debugLogging) {addLogEntry("Setting nextLink to (@odata.nextLink): " ~ nextLink, ["debug"]);} nextLink = topLevelChildren["@odata.nextLink"].str; } else break; // Sleep for a while to avoid busy-waiting Thread.sleep(dur!"msecs"(100)); // Adjust the sleep duration as needed } if (appConfig.verbosityCount == 0) { // Dynamic output for a non-verbose run so that the user knows something is happening if (!appConfig.suppressLoggingOutput) { // Close out the '....' being printed to the console completeProcessingDots(); } } // Craft response from all returned JSON elements selfGeneratedDeltaResponse = [ "@odata.context": JSONValue("https://graph.microsoft.com/v1.0/$metadata#Collection(driveItem)"), "value": JSONValue(childrenData.array) ]; // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory generateDeltaResponseOneDriveApiInstance.releaseCurlEngine(); generateDeltaResponseOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // Return the generated JSON response return selfGeneratedDeltaResponse; } // Query the OneDrive API for the specified child id for any children objects JSONValue[] queryForChildren(string driveId, string idToQuery, string childParentPath, string pathForLogging) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // function variables JSONValue thisLevelChildren; JSONValue[] thisLevelChildrenData; string nextLink; // Create new OneDrive API Instance OneDriveApi queryChildrenOneDriveApiInstance; queryChildrenOneDriveApiInstance = new OneDriveApi(appConfig); queryChildrenOneDriveApiInstance.initialise(); // Issue #3115 - Validate driveId length // What account type is this? if (appConfig.accountType == "personal") { // Test driveId length and validation if the driveId we are testing is not equal to appConfig.defaultDriveId if (driveId != appConfig.defaultDriveId) { driveId = testProvidedDriveIdForLengthIssue(driveId); } } while (true) { // Check if exitHandlerTriggered is true if (exitHandlerTriggered) { // break out of the 'while (true)' loop break; } // query this level children try { thisLevelChildren = queryThisLevelChildren(driveId, idToQuery, nextLink, queryChildrenOneDriveApiInstance); } catch (OneDriveException exception) { // MAY NEED FUTURE WORK HERE .. YET TO TRIGGER THIS addLogEntry("CODING TO DO: EXCEPTION HANDLING NEEDED: thisLevelChildren = queryThisLevelChildren(driveId, idToQuery, nextLink, queryChildrenOneDriveApiInstance)"); } if (appConfig.verbosityCount == 0) { // Dynamic output for a non-verbose run so that the user knows something is happening if (!appConfig.suppressLoggingOutput) { addProcessingDotEntry(); } } // Was a valid JSON response for 'thisLevelChildren' provided? if (thisLevelChildren.type() == JSONType.object) { // process this level children if (!childParentPath.empty) { // We dont use childParentPath to log, as this poses an information leak risk. // The full parent path of the child, as per the JSON might be: // /Level 1/Level 2/Level 3/Child Shared Folder/some folder/another folder // But 'Child Shared Folder' is what is shared, thus '/Level 1/Level 2/Level 3/' is a potential information leak if logged. // Plus, the application output now shows accurately what is being shared - so that is a good thing. if (verboseLogging) {addLogEntry("Adding " ~ to!string(count(thisLevelChildren["value"].array)) ~ " OneDrive items for processing from " ~ pathForLogging, ["verbose"]);} } foreach (child; thisLevelChildren["value"].array) { // Check for any Client Side Filtering here ... we should skip querying the OneDrive API for 'folders' that we are going to just process and skip anyway. // This avoids needless calls to the OneDrive API, and potentially speeds up this process. if (!checkJSONAgainstClientSideFiltering(child)) { // add this child to the array of objects thisLevelChildrenData ~= child; // is this child a folder? if (isItemFolder(child)){ // We have to query this folders children if childCount > 0 if (child["folder"]["childCount"].integer > 0){ // This child folder has children string childIdToQuery = child["id"].str; string childDriveToQuery = child["parentReference"]["driveId"].str; auto grandchildParentPath = child["parentReference"]["path"].str.split(":"); string folderPathToScan = grandchildParentPath[1] ~ "/" ~ child["name"].str; string newLoggingPath = pathForLogging ~ "/" ~ child["name"].str; JSONValue[] grandChildrenData = queryForChildren(childDriveToQuery, childIdToQuery, folderPathToScan, newLoggingPath); foreach (grandChild; grandChildrenData.array) { // add the grandchild to the array thisLevelChildrenData ~= grandChild; } } } } } // If a collection exceeds the default page size (200 items), the @odata.nextLink property is returned in the response // to indicate more items are available and provide the request URL for the next page of items. if ("@odata.nextLink" in thisLevelChildren) { // Update nextLink to next changeSet bundle nextLink = thisLevelChildren["@odata.nextLink"].str; if (debugLogging) {addLogEntry("Setting nextLink to (@odata.nextLink): " ~ nextLink, ["debug"]);} } else break; } else { // Invalid JSON response when querying this level children if (debugLogging) {addLogEntry("INVALID JSON response when attempting a retry of parent function - queryForChildren(driveId, idToQuery, childParentPath, pathForLogging)", ["debug"]);} // retry thisLevelChildren = queryThisLevelChildren if (debugLogging) {addLogEntry("Thread sleeping for an additional 30 seconds", ["debug"]);} Thread.sleep(dur!"seconds"(30)); if (debugLogging) {addLogEntry("Retry this call thisLevelChildren = queryThisLevelChildren(driveId, idToQuery, nextLink, queryChildrenOneDriveApiInstance)", ["debug"]);} thisLevelChildren = queryThisLevelChildren(driveId, idToQuery, nextLink, queryChildrenOneDriveApiInstance); } // Sleep for a while to avoid busy-waiting Thread.sleep(dur!"msecs"(100)); // Adjust the sleep duration as needed } // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory queryChildrenOneDriveApiInstance.releaseCurlEngine(); queryChildrenOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return response return thisLevelChildrenData; } // Query the OneDrive API for the child objects for this element JSONValue queryThisLevelChildren(string driveId, string idToQuery, string nextLink, OneDriveApi queryChildrenOneDriveApiInstance) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Issue #3115 - Validate driveId length // - The function 'queryForChildren' checks the 'driveId' value and that value is the input to this function. // It is redundant to then check 'driveid' again as this is not changed when this function is called // function variables JSONValue thisLevelChildren; // query children try { // attempt API call if (debugLogging) {addLogEntry("Attempting Query: thisLevelChildren = queryChildrenOneDriveApiInstance.listChildren(driveId, idToQuery, nextLink)", ["debug"]);} thisLevelChildren = queryChildrenOneDriveApiInstance.listChildren(driveId, idToQuery, nextLink); if (debugLogging) {addLogEntry("Query 'thisLevelChildren = queryChildrenOneDriveApiInstance.listChildren(driveId, idToQuery, nextLink)' performed successfully", ["debug"]);} } catch (OneDriveException exception) { // OneDrive threw an error if (debugLogging) { addLogEntry(debugLogBreakType1, ["debug"]); addLogEntry("Query Error: thisLevelChildren = queryChildrenOneDriveApiInstance.listChildren(driveId, idToQuery, nextLink)", ["debug"]); addLogEntry("driveId: " ~ driveId, ["debug"]); addLogEntry("idToQuery: " ~ idToQuery, ["debug"]); addLogEntry("nextLink: " ~ nextLink, ["debug"]); } // Default operation if not 408,429,503,504 errors // - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance // Display what the error is displayOneDriveErrorMessage(exception.msg, thisFunctionName); } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return response return thisLevelChildren; } // Traverses the provided path online, via the OneDrive API, following correct parent driveId and itemId elements across the account // to find if this full path exists. If this path exists online, the last item in the object path will be returned as a full JSON item. // // If the createPathIfMissing = false + no path exists online, a null invalid JSON item will be returned. // If the createPathIfMissing = true + no path exists online, the requested path will be created in the correct location online. The resulting // response to the directory creation will then be returned. // // This function also ensures that each path in the requested path actually matches the requested element to ensure that the OneDrive API response // is not falsely matching a 'case insensitive' match to the actual request which is a POSIX compliance issue. JSONValue queryOneDriveForSpecificPathAndCreateIfMissing(string thisNewPathToSearch, bool createPathIfMissing) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // function variables JSONValue getPathDetailsAPIResponse; string currentPathTree; Item parentDetails; JSONValue topLevelChildren; string nextLink; bool directoryFoundOnline = false; bool posixIssue = false; // Create a new API Instance for this thread and initialise it OneDriveApi queryOneDriveForSpecificPath; queryOneDriveForSpecificPath = new OneDriveApi(appConfig); queryOneDriveForSpecificPath.initialise(); foreach (thisFolderName; pathSplitter(thisNewPathToSearch)) { if (debugLogging) {addLogEntry("Testing for the existence online of this folder path: " ~ thisFolderName, ["debug"]);} directoryFoundOnline = false; // If this is '.' this is the account root if (thisFolderName == ".") { currentPathTree = thisFolderName; } else { currentPathTree = currentPathTree ~ "/" ~ thisFolderName; } // What path are we querying if (debugLogging) {addLogEntry("Attempting to query OneDrive for this path: " ~ currentPathTree, ["debug"]);} // What query do we use? if (thisFolderName == ".") { // Query the root, set the right details try { getPathDetailsAPIResponse = queryOneDriveForSpecificPath.getPathDetails(currentPathTree); parentDetails = makeItem(getPathDetailsAPIResponse); // Save item to the database saveItem(getPathDetailsAPIResponse); directoryFoundOnline = true; } catch (OneDriveException exception) { // Default operation if not 408,429,503,504 errors // - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance // Display what the error is displayOneDriveErrorMessage(exception.msg, thisFunctionName); } } else { // Ensure we have a valid driveId to search here if (parentDetails.driveId.empty) { parentDetails.driveId = appConfig.defaultDriveId; } // If the prior JSON 'getPathDetailsAPIResponse' is on this account driveId .. then continue to use getPathDetails if (parentDetails.driveId == appConfig.defaultDriveId) { try { // Query OneDrive API for this path getPathDetailsAPIResponse = queryOneDriveForSpecificPath.getPathDetails(currentPathTree); // Portable Operating System Interface (POSIX) testing of JSON response from OneDrive API if (hasName(getPathDetailsAPIResponse)) { // Perform the POSIX evaluation test against the names if (performPosixTest(thisFolderName, getPathDetailsAPIResponse["name"].str)) { throw new PosixException(thisFolderName, getPathDetailsAPIResponse["name"].str); } } else { throw new JsonResponseException("Unable to perform POSIX test as the OneDrive API request generated an invalid JSON response"); } // No POSIX issue with requested path element parentDetails = makeItem(getPathDetailsAPIResponse); // Save item to the database saveItem(getPathDetailsAPIResponse); directoryFoundOnline = true; // Is this JSON a remote object if (debugLogging) {addLogEntry("Testing if this is a remote Shared Folder", ["debug"]);} if (isItemRemote(getPathDetailsAPIResponse)) { // Remote Directory .. need a DB Tie Record createDatabaseTieRecordForOnlineSharedFolder(parentDetails); // Temp DB Item to bind the 'remote' path to our parent path Item tempDBItem; // Set the name tempDBItem.name = parentDetails.name; // Set the correct item type tempDBItem.type = ItemType.dir; // Set the right elements using the 'remote' of the parent as the 'actual' for this DB Tie tempDBItem.driveId = parentDetails.remoteDriveId; tempDBItem.id = parentDetails.remoteId; // Set the correct mtime tempDBItem.mtime = parentDetails.mtime; // Update parentDetails to use this temp record parentDetails = tempDBItem; } } catch (OneDriveException exception) { if (exception.httpStatusCode == 404) { directoryFoundOnline = false; } else { // Default operation if not 408,429,503,504 errors // - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance // Display what the error is displayOneDriveErrorMessage(exception.msg, thisFunctionName); } } catch (PosixException e) { // Display POSIX error message displayPosixErrorMessage(e.msg); addLogEntry("ERROR: Requested directory to search for and potentially create has a 'case-insensitive match' to an existing directory on Microsoft OneDrive online."); addLogEntry("ERROR: To resolve, rename this local directory: " ~ currentPathTree); } catch (JsonResponseException e) { if (debugLogging) {addLogEntry(e.msg, ["debug"]);} } } else { // parentDetails.driveId is not the account drive id - thus will be a remote shared item if (debugLogging) {addLogEntry("This parent directory is a remote object this next path will be on a remote drive", ["debug"]);} // For this parentDetails.driveId, parentDetails.id object, query the OneDrive API for it's children while (true) { // Check if exitHandlerTriggered is true if (exitHandlerTriggered) { // break out of the 'while (true)' loop break; } // Query this remote object for its children topLevelChildren = queryOneDriveForSpecificPath.listChildren(parentDetails.driveId, parentDetails.id, nextLink); // Process each child foreach (child; topLevelChildren["value"].array) { // Is this child a folder? if (isItemFolder(child)) { // Is this the child folder we are looking for, and is a POSIX match? if (child["name"].str == thisFolderName) { // EXACT MATCH including case sensitivity: Flag that we found the folder online directoryFoundOnline = true; // Use these details for the next entry path getPathDetailsAPIResponse = child; parentDetails = makeItem(getPathDetailsAPIResponse); // Save item to the database saveItem(getPathDetailsAPIResponse); // No need to continue searching break; } else { string childAsLower = toLower(child["name"].str); string thisFolderNameAsLower = toLower(thisFolderName); try { if (childAsLower == thisFolderNameAsLower) { // This is a POSIX 'case in-sensitive match' ..... // Local item name has a 'case-insensitive match' to an existing item on OneDrive posixIssue = true; throw new PosixException(thisFolderName, child["name"].str); } } catch (PosixException e) { // Display POSIX error message displayPosixErrorMessage(e.msg); addLogEntry("ERROR: Requested directory to search for and potentially create has a 'case-insensitive match' to an existing directory on Microsoft OneDrive online."); addLogEntry("ERROR: To resolve, rename this local directory: " ~ currentPathTree); } } } } if (directoryFoundOnline) { // We found the folder, no need to continue searching nextLink data break; } // If a collection exceeds the default page size (200 items), the @odata.nextLink property is returned in the response // to indicate more items are available and provide the request URL for the next page of items. if ("@odata.nextLink" in topLevelChildren) { // Update nextLink to next changeSet bundle if (debugLogging) {addLogEntry("Setting nextLink to (@odata.nextLink): " ~ nextLink, ["debug"]);} nextLink = topLevelChildren["@odata.nextLink"].str; } else break; // Sleep for a while to avoid busy-waiting Thread.sleep(dur!"msecs"(100)); // Adjust the sleep duration as needed } } } // If we did not find the folder, we need to create this folder if (!directoryFoundOnline) { // Folder not found online // Set any response to be an invalid JSON item getPathDetailsAPIResponse = null; // Was there a POSIX issue? if (!posixIssue) { // No POSIX issue if (createPathIfMissing) { // Create this path as it is missing on OneDrive online and there is no POSIX issue with a 'case-insensitive match' if (debugLogging) { addLogEntry("FOLDER NOT FOUND ONLINE AND WE ARE REQUESTED TO CREATE IT", ["debug"]); addLogEntry("Create folder on this drive: " ~ parentDetails.driveId, ["debug"]); addLogEntry("Create folder as a child on this object: " ~ parentDetails.id, ["debug"]); addLogEntry("Create this folder name: " ~ thisFolderName, ["debug"]); } // Generate the JSON needed to create the folder online JSONValue newDriveItem = [ "name": JSONValue(thisFolderName), "folder": parseJSON("{}") ]; JSONValue createByIdAPIResponse; // Submit the creation request // Fix for https://github.com/skilion/onedrive/issues/356 if (!dryRun) { try { // Attempt to create a new folder on the configured parent driveId & parent id createByIdAPIResponse = queryOneDriveForSpecificPath.createById(parentDetails.driveId, parentDetails.id, newDriveItem); // Is the response a valid JSON object - validation checking done in saveItem saveItem(createByIdAPIResponse); // Set getPathDetailsAPIResponse to createByIdAPIResponse getPathDetailsAPIResponse = createByIdAPIResponse; } catch (OneDriveException e) { // 409 - API Race Condition if (e.httpStatusCode == 409) { // When we attempted to create it, OneDrive responded that it now already exists if (verboseLogging) {addLogEntry("OneDrive reported that " ~ thisFolderName ~ " already exists .. OneDrive API race condition", ["verbose"]);} } else { // some other error from OneDrive was returned - display what it is addLogEntry("OneDrive generated an error when creating this path: " ~ thisFolderName); displayOneDriveErrorMessage(e.msg, thisFunctionName); } } } else { // Simulate a successful 'directory create' & save it to the dryRun database copy // The simulated response has to pass 'makeItem' as part of saveItem auto fakeResponse = createFakeResponse(thisNewPathToSearch); // Save item to the database saveItem(fakeResponse); } } } } } // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory queryOneDriveForSpecificPath.releaseCurlEngine(); queryOneDriveForSpecificPath = null; // Perform Garbage Collection GC.collect(); // Output our search results if (debugLogging) {addLogEntry("queryOneDriveForSpecificPathAndCreateIfMissing.getPathDetailsAPIResponse = " ~ to!string(getPathDetailsAPIResponse), ["debug"]);} // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return JSON result return getPathDetailsAPIResponse; } // Delete an item by it's path // This function is only used in --monitor mode to remove a directory online void deleteByPath(string path) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // function variables Item dbItem; // Need to check all driveid's we know about, not just the defaultDriveId bool itemInDB = false; foreach (searchDriveId; onlineDriveDetails.keys) { if (itemDB.selectByPath(path, searchDriveId, dbItem)) { // item was found in the DB itemInDB = true; break; } } // Was the item found in the database? if (!itemInDB) { // path to delete is not in the local database .. // was this a --remove-directory attempt? if (!appConfig.getValueBool("monitor")) { // --remove-directory deletion attempt addLogEntry("The item to delete is not in the local database - unable to delete online"); return; } else { // normal use .. --monitor being used throw new SyncException("The item to delete is not in the local database"); } } // This needs to be enforced as we have to know the parent id of the object being deleted if (dbItem.parentId == null) { // the item is a remote folder, need to do the operation on the parent enforce(itemDB.selectByPathIncludingRemoteItems(path, appConfig.defaultDriveId, dbItem)); } try { if (noRemoteDelete) { // do not process remote delete if (verboseLogging) {addLogEntry("Skipping remote delete as --upload-only & --no-remote-delete configured", ["verbose"]);} } else { uploadDeletedItem(dbItem, path); } } catch (OneDriveException e) { if (e.httpStatusCode == 404) { addLogEntry(e.msg); } else { // display what the error is displayOneDriveErrorMessage(e.msg, thisFunctionName); } } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Delete an item by it's path // Delete a directory on OneDrive without syncing. This function is only used with --remove-directory void deleteByPathNoSync(string path) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Attempt to delete the requested path within OneDrive without performing a sync addLogEntry("Attempting to delete the requested path within Microsoft OneDrive"); // function variables JSONValue getPathDetailsAPIResponse; OneDriveApi deleteByPathNoSyncAPIInstance; // test if the path we are going to exists on OneDrive try { // Create a new API Instance for this thread and initialise it deleteByPathNoSyncAPIInstance = new OneDriveApi(appConfig); deleteByPathNoSyncAPIInstance.initialise(); getPathDetailsAPIResponse = deleteByPathNoSyncAPIInstance.getPathDetails(path); // If we get here, no error, the path to delete exists online } catch (OneDriveException exception) { // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory deleteByPathNoSyncAPIInstance.releaseCurlEngine(); deleteByPathNoSyncAPIInstance = null; // Perform Garbage Collection GC.collect(); // Log that an error was generated if (debugLogging) {addLogEntry("deleteByPathNoSyncAPIInstance.getPathDetails(path) generated a OneDriveException", ["debug"]);} if (exception.httpStatusCode == 404) { // The directory was not found on OneDrive - no need to delete it addLogEntry("The requested directory to delete was not found on OneDrive - skipping removing the remote directory online as it does not exist"); return; } // Default operation if not 408,429,503,504 errors // - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance // Display what the error is displayOneDriveErrorMessage(exception.msg, thisFunctionName); return; } // Make a DB item from the JSON data that was returned via the API call Item deletionItem = makeItem(getPathDetailsAPIResponse); // Is the item to remove the correct type if (deletionItem.type == ItemType.dir) { // Item is a directory to remove // Log that the path | item was found, is a directory addLogEntry("The requested directory to delete was found on OneDrive - attempting deletion"); // Try the online deletion try { if (!permanentDelete) { // Perform the delete via the default OneDrive API instance deleteByPathNoSyncAPIInstance.deleteById(deletionItem.driveId, deletionItem.id); } else { // Perform the permanent delete via the default OneDrive API instance deleteByPathNoSyncAPIInstance.permanentDeleteById(deletionItem.driveId, deletionItem.id); } // If we get here without error, directory was deleted addLogEntry("The requested directory to delete online has been deleted"); } catch (OneDriveException exception) { // Display what the error is displayOneDriveErrorMessage(exception.msg, thisFunctionName); } } else { // --remove-directory is for removing directories // Log that the path | item was found, is a directory addLogEntry("The requested path to delete is not a directory - aborting deletion attempt"); } // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory deleteByPathNoSyncAPIInstance.releaseCurlEngine(); deleteByPathNoSyncAPIInstance = null; // Perform Garbage Collection GC.collect(); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_move // This function is only called in monitor mode when an move event is coming from // inotify and we try to move the item. void uploadMoveItem(string oldPath, string newPath) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Log that we are doing a move addLogEntry("Moving " ~ oldPath ~ " to " ~ newPath); // Is this move unwanted? bool unwanted = false; // Item variables Item oldItem, newItem, parentItem; // This not a Client Side Filtering check, nor a Microsoft Check, but is a sanity check that the path provided is UTF encoded correctly // Check the std.encoding of the path against: Unicode 5.0, ASCII, ISO-8859-1, ISO-8859-2, WINDOWS-1250, WINDOWS-1251, WINDOWS-1252 if (!unwanted) { if(!isValid(newPath)) { // Path is not valid according to https://dlang.org/phobos/std_encoding.html addLogEntry("Skipping item - invalid character encoding sequence: " ~ newPath, ["info", "notify"]); unwanted = true; } } // Check this path against the Client Side Filtering Rules // - check_nosync // - skip_dotfiles // - skip_symlinks // - skip_file // - skip_dir // - sync_list // - skip_size if (!unwanted) { unwanted = checkPathAgainstClientSideFiltering(newPath); } // Check this path against the Microsoft Naming Conventions & Restrictions // - Check path against Microsoft OneDrive restriction and limitations about Windows naming for files and folders // - Check path for bad whitespace items // - Check path for HTML ASCII Codes // - Check path for ASCII Control Codes if (!unwanted) { unwanted = checkPathAgainstMicrosoftNamingRestrictions(newPath); } // 'newPath' has passed client side filtering validation if (!unwanted) { if (!itemDB.selectByPath(oldPath, appConfig.defaultDriveId, oldItem)) { // The old path|item is not synced with the database, upload as a new file addLogEntry("Moved local item was not in-sync with local database - uploading as new item"); scanLocalFilesystemPathForNewData(newPath); return; } if (oldItem.parentId == null) { // the item is a remote folder, need to do the operation on the parent enforce(itemDB.selectByPathIncludingRemoteItems(oldPath, appConfig.defaultDriveId, oldItem)); } if (itemDB.selectByPath(newPath, appConfig.defaultDriveId, newItem)) { // the destination has been overwritten addLogEntry("Moved local item overwrote an existing item - deleting old online item"); uploadDeletedItem(newItem, newPath); } if (!itemDB.selectByPath(dirName(newPath), appConfig.defaultDriveId, parentItem)) { // the parent item is not in the database throw new SyncException("Can't move an item to an unsynchronised directory"); } if (oldItem.driveId != parentItem.driveId) { // items cannot be moved between drives uploadDeletedItem(oldItem, oldPath); // what sort of move is this? if (isFile(newPath)) { // newPath is a file uploadNewFile(newPath); } else { // newPath is a directory scanLocalFilesystemPathForNewData(newPath); } } else { if (!exists(newPath)) { // is this --monitor use? if (appConfig.getValueBool("monitor")) { if (verboseLogging) {addLogEntry("uploadMoveItem target has disappeared: " ~ newPath, ["verbose"]);} return; } } // Configure the modification JSON item SysTime mtime; if (appConfig.getValueBool("monitor")) { // Use the newPath modified timestamp mtime = timeLastModified(newPath).toUTC(); } else { // Use the current system time mtime = Clock.currTime().toUTC(); } JSONValue data = [ "name": JSONValue(baseName(newPath)), "parentReference": JSONValue([ "id": parentItem.id ]), "fileSystemInfo": JSONValue([ "lastModifiedDateTime": mtime.toISOExtString() ]) ]; // Perform the move operation on OneDrive bool isMoveSuccess = false; JSONValue response; string eTag = oldItem.eTag; // Create a new API Instance for this thread and initialise it OneDriveApi movePathOnlineApiInstance; movePathOnlineApiInstance = new OneDriveApi(appConfig); movePathOnlineApiInstance.initialise(); // Try the online move for (int i = 0; i < 3; i++) { try { response = movePathOnlineApiInstance.updateById(oldItem.driveId, oldItem.id, data, eTag); isMoveSuccess = true; break; } catch (OneDriveException e) { // Handle a 412 - A precondition provided in the request (such as an if-match header) does not match the resource's current state. if (e.httpStatusCode == 412) { // OneDrive threw a 412 error, most likely: ETag does not match current item's value // Retry without eTag if (debugLogging) {addLogEntry("File Move Failed - OneDrive eTag / cTag match issue", ["debug"]);} if (verboseLogging) {addLogEntry("OneDrive returned a 'HTTP 412 - Precondition Failed' when attempting to move the file - gracefully handling error", ["verbose"]);} eTag = null; // Retry to move the file but without the eTag, via the for() loop } else if (e.httpStatusCode == 409) { // Destination item already exists and is a conflict, delete existing item first addLogEntry("Moved local item will overwrite an existing online item - deleting old online item first"); uploadDeletedItem(newItem, newPath); } else break; } } // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory movePathOnlineApiInstance.releaseCurlEngine(); movePathOnlineApiInstance = null; // Perform Garbage Collection GC.collect(); // save the move response from OneDrive in the database // Is the response a valid JSON object - validation checking done in saveItem saveItem(response); } } else { // Moved item is unwanted addLogEntry("Item has been moved to a location that is excluded from sync operations. Removing item from OneDrive"); uploadDeletedItem(oldItem, oldPath); } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Perform integrity validation of the file that was uploaded bool performUploadIntegrityValidationChecks(JSONValue uploadResponse, string localFilePath, long localFileSize) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } bool integrityValid = false; if (!disableUploadValidation) { // Integrity validation has not been disabled (this is the default so we are always integrity checking our uploads) if (uploadResponse.type() == JSONType.object) { // Provided JSON is a valid JSON long uploadFileSize; string uploadFileHash; string localFileHash; // Regardless if valid JSON is responded with, 'size' and 'quickXorHash' must be present if (hasFileSize(uploadResponse) && hasQuickXorHash(uploadResponse)) { uploadFileSize = uploadResponse["size"].integer; uploadFileHash = uploadResponse["file"]["hashes"]["quickXorHash"].str; localFileHash = computeQuickXorHash(localFilePath); } else { addLogEntry("Online file validation unable to be performed: input JSON whilst valid did not contain data which could be validated"); addLogEntry("WARNING: Skipping upload integrity check for: " ~ localFilePath); return integrityValid; } // compare values if ((localFileSize == uploadFileSize) && (localFileHash == uploadFileHash)) { // Uploaded file integrity intact if (debugLogging) {addLogEntry("Uploaded local file matches reported online size and hash values", ["debug"]);} // set to true and return integrityValid = true; return integrityValid; } else { // Upload integrity failure .. what failed? // There are 2 scenarios where this happens: // 1. Failed Transfer // 2. Upload file is going to a SharePoint Site, where Microsoft enriches the file with additional metadata with no way to disable addLogEntry("WARNING: Online file integrity failure for: " ~ localFilePath, ["info", "notify"]); // What integrity failed - size? if (localFileSize != uploadFileSize) { if (verboseLogging) {addLogEntry("WARNING: Online file integrity failure - Size Mismatch", ["verbose"]);} } // What integrity failed - hash? if (localFileHash != uploadFileHash) { if (verboseLogging) {addLogEntry("WARNING: Online file integrity failure - Hash Mismatch", ["verbose"]);} } // What account type is this? if (appConfig.accountType != "personal") { // Not a personal account, thus the integrity failure is most likely due to SharePoint if (verboseLogging) { addLogEntry("CAUTION: When you upload files to Microsoft OneDrive that uses SharePoint as its backend, Microsoft OneDrive will alter your files post upload.", ["verbose"]); addLogEntry("CAUTION: This will lead to technical differences between the version stored online and your local original file, potentially causing issues with the accuracy or consistency of your data.", ["verbose"]); addLogEntry("CAUTION: Please refer to https://github.com/OneDrive/onedrive-api-docs/issues/935 for further details.", ["verbose"]); } } // How can this be disabled? addLogEntry("To disable the integrity checking of uploaded files use --disable-upload-validation"); } } else { addLogEntry("Online file validation unable to be performed: input JSON was invalid"); addLogEntry("WARNING: Skipping upload integrity check for: " ~ localFilePath); } } else { // We are bypassing integrity checks due to --disable-upload-validation if (debugLogging) {addLogEntry("Online file validation disabled due to --disable-upload-validation", ["debug"]);} addLogEntry("WARNING: Skipping upload integrity check for: " ~ localFilePath, ["info", "notify"]); } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // Is the file integrity online valid? return integrityValid; } // Query Office 365 SharePoint Shared Library site name to obtain it's Drive ID void querySiteCollectionForDriveID(string sharepointLibraryNameToQuery) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Steps to get the ID: // 1. Query https://graph.microsoft.com/v1.0/sites?search= with the name entered // 2. Evaluate the response. A valid response will contain the description and the id. If the response comes back with nothing, the site name cannot be found or no access // 3. If valid, use the returned ID and query the site drives // https://graph.microsoft.com/v1.0/sites//drives // 4. Display Shared Library Name & Drive ID string site_id; string drive_id; bool found = false; JSONValue siteQuery; string nextLink; string[] siteSearchResults; // Create a new API Instance for this thread and initialise it OneDriveApi querySharePointLibraryNameApiInstance; querySharePointLibraryNameApiInstance = new OneDriveApi(appConfig); querySharePointLibraryNameApiInstance.initialise(); // The account type must not be a personal account type if (appConfig.accountType == "personal") { addLogEntry("ERROR: A OneDrive Personal Account cannot be used with --get-sharepoint-drive-id. Please re-authenticate your client using a OneDrive Business Account."); return; } // What query are we performing? addLogEntry(); addLogEntry("Office 365 Library Name Query: " ~ sharepointLibraryNameToQuery); while (true) { // Check if exitHandlerTriggered is true if (exitHandlerTriggered) { // break out of the 'while (true)' loop break; } try { siteQuery = querySharePointLibraryNameApiInstance.o365SiteSearch(nextLink); } catch (OneDriveException e) { addLogEntry("ERROR: Query of OneDrive for Office 365 Library Name failed"); // Forbidden - most likely authentication scope needs to be updated if (e.httpStatusCode == 403) { addLogEntry("ERROR: Authentication scope needs to be updated. Use --reauth and re-authenticate client."); // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory querySharePointLibraryNameApiInstance.releaseCurlEngine(); querySharePointLibraryNameApiInstance = null; // Perform Garbage Collection GC.collect(); return; } // Requested resource cannot be found if (e.httpStatusCode == 404) { string siteSearchUrl; if (nextLink.empty) { siteSearchUrl = querySharePointLibraryNameApiInstance.getSiteSearchUrl(); } else { siteSearchUrl = nextLink; } // log the error addLogEntry("ERROR: Your OneDrive Account and Authentication Scope cannot access this OneDrive API: " ~ siteSearchUrl); addLogEntry("ERROR: To resolve, please discuss this issue with whomever supports your OneDrive and SharePoint environment."); // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory querySharePointLibraryNameApiInstance.releaseCurlEngine(); querySharePointLibraryNameApiInstance = null; // Perform Garbage Collection GC.collect(); return; } // Default operation if not 408,429,503,504 errors // - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance // Display what the error is displayOneDriveErrorMessage(e.msg, thisFunctionName); // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory querySharePointLibraryNameApiInstance.releaseCurlEngine(); querySharePointLibraryNameApiInstance = null; // Perform Garbage Collection GC.collect(); return; } // is siteQuery a valid JSON object & contain data we can use? if ((siteQuery.type() == JSONType.object) && ("value" in siteQuery)) { // valid JSON object if (debugLogging) {addLogEntry("O365 Query Response: " ~ to!string(siteQuery), ["debug"]);} foreach (searchResult; siteQuery["value"].array) { // Need an 'exclusive' match here with sharepointLibraryNameToQuery as entered if (debugLogging) {addLogEntry("Found O365 Site: " ~ to!string(searchResult), ["debug"]);} // 'displayName' and 'id' have to be present in the search result record in order to query the site if (("displayName" in searchResult) && ("id" in searchResult)) { if (sharepointLibraryNameToQuery == searchResult["displayName"].str){ // 'displayName' matches search request site_id = searchResult["id"].str; JSONValue siteDriveQuery; string nextLinkDrive; while (true) { try { siteDriveQuery = querySharePointLibraryNameApiInstance.o365SiteDrives(site_id, nextLinkDrive); } catch (OneDriveException e) { addLogEntry("ERROR: Query of OneDrive for Office Site ID failed"); // display what the error is displayOneDriveErrorMessage(e.msg, thisFunctionName); // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory querySharePointLibraryNameApiInstance.releaseCurlEngine(); querySharePointLibraryNameApiInstance = null; // Perform Garbage Collection GC.collect(); return; } // is siteDriveQuery a valid JSON object & contain data we can use? if ((siteDriveQuery.type() == JSONType.object) && ("value" in siteDriveQuery)) { // valid JSON object foreach (driveResult; siteDriveQuery["value"].array) { // Display results found = true; addLogEntry("-----------------------------------------------"); if (debugLogging) {addLogEntry("Site Details: " ~ to!string(driveResult), ["debug"]);} addLogEntry("Site Name: " ~ searchResult["displayName"].str); addLogEntry("Library Name: " ~ driveResult["name"].str); addLogEntry("drive_id: " ~ driveResult["id"].str); addLogEntry("Library URL: " ~ driveResult["webUrl"].str); } // If a collection exceeds the default page size (200 items), the @odata.nextLink property is returned in the response // to indicate more items are available and provide the request URL for the next page of items. if ("@odata.nextLink" in siteDriveQuery) { // Update nextLink to next set of SharePoint library names nextLinkDrive = siteDriveQuery["@odata.nextLink"].str; if (debugLogging) {addLogEntry("Setting nextLinkDrive to (@odata.nextLink): " ~ nextLinkDrive, ["debug"]);} // Sleep for a while to avoid busy-waiting Thread.sleep(dur!"msecs"(100)); // Adjust the sleep duration as needed } else { // closeout addLogEntry("-----------------------------------------------"); break; } } else { // not a valid JSON object addLogEntry("ERROR: There was an error performing this operation on Microsoft OneDrive"); addLogEntry("ERROR: Increase logging verbosity to assist determining why."); // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory querySharePointLibraryNameApiInstance.releaseCurlEngine(); querySharePointLibraryNameApiInstance = null; // Perform Garbage Collection GC.collect(); return; } } } } else { // 'displayName', 'id' or ''webUrl' not present in JSON results for a specific site string siteNameAvailable = "Site 'name' was restricted by OneDrive API permissions"; bool displayNameAvailable = false; bool idAvailable = false; if ("name" in searchResult) siteNameAvailable = searchResult["name"].str; if ("displayName" in searchResult) displayNameAvailable = true; if ("id" in searchResult) idAvailable = true; // Display error details for this site data addLogEntry(); addLogEntry("ERROR: SharePoint Site details not provided for: " ~ siteNameAvailable); addLogEntry("ERROR: The SharePoint Site results returned from OneDrive API do not contain the required items to match. Please check your permissions with your site administrator."); addLogEntry("ERROR: Your site security settings is preventing the following details from being accessed: 'displayName' or 'id'"); if (verboseLogging) { addLogEntry(" - Is 'displayName' available = " ~ to!string(displayNameAvailable), ["verbose"]); addLogEntry(" - Is 'id' available = " ~ to!string(idAvailable), ["verbose"]); } addLogEntry("ERROR: To debug this further, please increase application output verbosity to provide further insight as to what details are actually being returned."); } } if(!found) { // The SharePoint site we are searching for was not found in this bundle set // Add to siteSearchResults so we can display what we did find string siteSearchResultsEntry; foreach (searchResult; siteQuery["value"].array) { // We can only add the displayName if it is available if ("displayName" in searchResult) { // Use the displayName siteSearchResultsEntry = " * " ~ searchResult["displayName"].str; siteSearchResults ~= siteSearchResultsEntry; } else { // Add, but indicate displayName unavailable, use id if ("id" in searchResult) { siteSearchResultsEntry = " * " ~ "Unknown displayName (Data not provided by API), Site ID: " ~ searchResult["id"].str; siteSearchResults ~= siteSearchResultsEntry; } else { // displayName and id unavailable, display in debug log the entry if (debugLogging) {addLogEntry("Bad SharePoint Data for site: " ~ to!string(searchResult), ["debug"]);} } } } } } else { // not a valid JSON object addLogEntry("ERROR: There was an error performing this operation on Microsoft OneDrive"); addLogEntry("ERROR: Increase logging verbosity to assist determining why."); // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory querySharePointLibraryNameApiInstance.releaseCurlEngine(); querySharePointLibraryNameApiInstance = null; // Perform Garbage Collection GC.collect(); return; } // If a collection exceeds the default page size (200 items), the @odata.nextLink property is returned in the response // to indicate more items are available and provide the request URL for the next page of items. if ("@odata.nextLink" in siteQuery) { // Update nextLink to next set of SharePoint library names nextLink = siteQuery["@odata.nextLink"].str; if (debugLogging) {addLogEntry("Setting nextLink to (@odata.nextLink): " ~ nextLink, ["debug"]);} } else break; // Sleep for a while to avoid busy-waiting Thread.sleep(dur!"msecs"(100)); // Adjust the sleep duration as needed } // Was the intended target found? if(!found) { // Was the search a wildcard? if (sharepointLibraryNameToQuery != "*") { // Only print this out if the search was not a wildcard addLogEntry(); addLogEntry("ERROR: The requested SharePoint site could not be found. Please check it's name and your permissions to access the site."); } // List all sites returned to assist user addLogEntry(); addLogEntry("The following SharePoint site names were returned:"); foreach (searchResultEntry; siteSearchResults) { // list the display name that we use to match against the user query addLogEntry(searchResultEntry); } } // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory querySharePointLibraryNameApiInstance.releaseCurlEngine(); querySharePointLibraryNameApiInstance = null; // Perform Garbage Collection GC.collect(); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Query the sync status of the client and the local system void queryOneDriveForSyncStatus(string pathToQueryStatusOn) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Query the account driveId and rootId to get the /delta JSON information // Process that JSON data for relevancy // Function variables long downloadSize = 0; string deltaLink = null; string driveIdToQuery = appConfig.defaultDriveId; string itemIdToQuery = appConfig.defaultRootId; JSONValue deltaChanges; // Array of JSON items JSONValue[] jsonItemsArray; // Query Database for a potential deltaLink starting point deltaLink = itemDB.getDeltaLink(driveIdToQuery, itemIdToQuery); // Log what we are doing addProcessingLogHeaderEntry("Querying the change status of Drive ID: " ~ driveIdToQuery, appConfig.verbosityCount); // Create a new API Instance for querying the actual /delta and initialise it OneDriveApi getDeltaDataOneDriveApiInstance; getDeltaDataOneDriveApiInstance = new OneDriveApi(appConfig); getDeltaDataOneDriveApiInstance.initialise(); while (true) { // Check if exitHandlerTriggered is true if (exitHandlerTriggered) { // break out of the 'while (true)' loop break; } // Add a processing '.' if (appConfig.verbosityCount == 0) { addProcessingDotEntry(); } // Get the /delta changes via the OneDrive API // getDeltaChangesByItemId has the re-try logic for transient errors deltaChanges = getDeltaChangesByItemId(driveIdToQuery, itemIdToQuery, deltaLink, getDeltaDataOneDriveApiInstance); // If the initial deltaChanges response is an invalid JSON object, keep trying until we get a valid response .. if (deltaChanges.type() != JSONType.object) { // While the response is not a JSON Object or the Exit Handler has not been triggered while (deltaChanges.type() != JSONType.object) { // Handle the invalid JSON response and retry if (debugLogging) {addLogEntry("ERROR: Query of the OneDrive API via deltaChanges = getDeltaChangesByItemId() returned an invalid JSON response", ["debug"]);} deltaChanges = getDeltaChangesByItemId(driveIdToQuery, itemIdToQuery, deltaLink, getDeltaDataOneDriveApiInstance); } } // We have a valid deltaChanges JSON array. This means we have at least 200+ JSON items to process. // The API response however cannot be run in parallel as the OneDrive API sends the JSON items in the order in which they must be processed foreach (onedriveJSONItem; deltaChanges["value"].array) { // is the JSON a root object - we dont want to count this if (!isItemRoot(onedriveJSONItem)) { // Files are the only item that we want to calculate if (isItemFile(onedriveJSONItem)) { // JSON item is a file // Is the item filtered out due to client side filtering rules? if (!checkJSONAgainstClientSideFiltering(onedriveJSONItem)) { // Is the path of this JSON item 'in-scope' or 'out-of-scope' ? if (pathToQueryStatusOn != "/") { // We need to check the path of this item against pathToQueryStatusOn string thisItemPath = ""; if (("path" in onedriveJSONItem["parentReference"]) != null) { // If there is a parent reference path, try and use it string selfBuiltPath = onedriveJSONItem["parentReference"]["path"].str ~ "/" ~ onedriveJSONItem["name"].str; // Check for ':' and split if present auto splitIndex = selfBuiltPath.indexOf(":"); if (splitIndex != -1) { // Keep only the part after ':' selfBuiltPath = selfBuiltPath[splitIndex + 1 .. $]; } // Set thisItemPath to the self built path thisItemPath = selfBuiltPath; } else { // no parent reference path available thisItemPath = onedriveJSONItem["name"].str; } // can we find 'pathToQueryStatusOn' in 'thisItemPath' ? if (canFind(thisItemPath, pathToQueryStatusOn)) { // Add this to the array for processing jsonItemsArray ~= onedriveJSONItem; } } else { // We are not doing a --single-directory check // Add this to the array for processing jsonItemsArray ~= onedriveJSONItem; } } } } } // The response may contain either @odata.deltaLink or @odata.nextLink if ("@odata.deltaLink" in deltaChanges) { deltaLink = deltaChanges["@odata.deltaLink"].str; if (debugLogging) {addLogEntry("Setting next deltaLink to (@odata.deltaLink): " ~ deltaLink, ["debug"]);} } // Update deltaLink to next changeSet bundle if ("@odata.nextLink" in deltaChanges) { deltaLink = deltaChanges["@odata.nextLink"].str; if (debugLogging) {addLogEntry("Setting next deltaLink to (@odata.nextLink): " ~ deltaLink, ["debug"]);} } else break; // Sleep for a while to avoid busy-waiting Thread.sleep(dur!"msecs"(100)); // Adjust the sleep duration as needed } // Terminate getDeltaDataOneDriveApiInstance here getDeltaDataOneDriveApiInstance.releaseCurlEngine(); getDeltaDataOneDriveApiInstance = null; // Perform Garbage Collection on this destroyed curl engine GC.collect(); // Needed after printing out '....' when fetching changes from OneDrive API if (appConfig.verbosityCount == 0) { completeProcessingDots(); } // Are there any JSON items to process? if (count(jsonItemsArray) != 0) { // There are items to process foreach (onedriveJSONItem; jsonItemsArray.array) { // variables we need string thisItemParentDriveId; string thisItemId; string thisItemHash; bool existingDBEntry = false; // Is this file a remote item (on a shared folder) ? if (isItemRemote(onedriveJSONItem)) { // remote drive item thisItemParentDriveId = onedriveJSONItem["remoteItem"]["parentReference"]["driveId"].str; thisItemId = onedriveJSONItem["id"].str; } else { // standard drive item thisItemParentDriveId = onedriveJSONItem["parentReference"]["driveId"].str; thisItemId = onedriveJSONItem["id"].str; } // Get the file hash if (hasHashes(onedriveJSONItem)) { // At a minimum we require 'quickXorHash' to exist if (hasQuickXorHash(onedriveJSONItem)) { // JSON item has a hash we can use thisItemHash = onedriveJSONItem["file"]["hashes"]["quickXorHash"].str; } // Check if the item has been seen before Item existingDatabaseItem; existingDBEntry = itemDB.selectById(thisItemParentDriveId, thisItemId, existingDatabaseItem); if (existingDBEntry) { // item exists in database .. do the database details match the JSON record? if (existingDatabaseItem.quickXorHash != thisItemHash) { // file hash is different, this will trigger a download event if (hasFileSize(onedriveJSONItem)) { downloadSize = downloadSize + onedriveJSONItem["size"].integer; } } } else { // item does not exist in the database // this item has already passed client side filtering rules (skip_dir, skip_file, sync_list) // this will trigger a download event if (hasFileSize(onedriveJSONItem)) { downloadSize = downloadSize + onedriveJSONItem["size"].integer; } } } } } // Was anything detected that would constitute a download? if (downloadSize > 0) { // we have something to download if (pathToQueryStatusOn != "/") { addLogEntry("The selected local directory via --single-directory is out of sync with Microsoft OneDrive"); } else { addLogEntry("The configured local 'sync_dir' directory is out of sync with Microsoft OneDrive"); } addLogEntry("Approximate data to download from Microsoft OneDrive: " ~ to!string(downloadSize/1024) ~ " KB"); } else { // No changes were returned addLogEntry("There are no pending changes from Microsoft OneDrive; your local directory matches the data online."); } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Query OneDrive for file details of a given path, returning either the 'webURL' or 'lastModifiedBy' JSON facet void queryOneDriveForFileDetails(string inputFilePath, string runtimePath, string outputType) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } OneDriveApi queryOneDriveForFileDetailsApiInstance; // Calculate the full local file path string fullLocalFilePath = buildNormalizedPath(buildPath(runtimePath, inputFilePath)); // Query if file is valid locally if (exists(fullLocalFilePath)) { // search drive_id list string[] distinctDriveIds = itemDB.selectDistinctDriveIds(); bool pathInDB = false; Item dbItem; foreach (searchDriveId; distinctDriveIds) { // Does this path exist in the database, use the 'inputFilePath' if (itemDB.selectByPath(inputFilePath, searchDriveId, dbItem)) { // item is in the database pathInDB = true; JSONValue fileDetailsFromOneDrive; // Create a new API Instance for this thread and initialise it queryOneDriveForFileDetailsApiInstance = new OneDriveApi(appConfig); queryOneDriveForFileDetailsApiInstance.initialise(); try { fileDetailsFromOneDrive = queryOneDriveForFileDetailsApiInstance.getPathDetailsById(dbItem.driveId, dbItem.id); // Dont cleanup here as if we are creating a shareable file link (below) it is still needed } catch (OneDriveException exception) { // display what the error is displayOneDriveErrorMessage(exception.msg, thisFunctionName); // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory queryOneDriveForFileDetailsApiInstance.releaseCurlEngine(); queryOneDriveForFileDetailsApiInstance = null; // Perform Garbage Collection GC.collect(); return; } // Is the API response a valid JSON file? if (fileDetailsFromOneDrive.type() == JSONType.object) { // debug output of response if (debugLogging) {addLogEntry("API Response: " ~ to!string(fileDetailsFromOneDrive), ["debug"]);} // What sort of response to we generate // --get-file-link response if (outputType == "URL") { if ((fileDetailsFromOneDrive.type() == JSONType.object) && ("webUrl" in fileDetailsFromOneDrive)) { // Valid JSON object addLogEntry(); writeln("WebURL: ", fileDetailsFromOneDrive["webUrl"].str); } } // --modified-by response if (outputType == "ModifiedBy") { if ((fileDetailsFromOneDrive.type() == JSONType.object) && ("lastModifiedBy" in fileDetailsFromOneDrive)) { // Valid JSON object writeln(); writeln("Last modified: ", fileDetailsFromOneDrive["lastModifiedDateTime"].str); writeln("Last modified by: ", fileDetailsFromOneDrive["lastModifiedBy"]["user"]["displayName"].str); // if 'email' provided, add this to the output if ("email" in fileDetailsFromOneDrive["lastModifiedBy"]["user"]) { writeln("Email Address: ", fileDetailsFromOneDrive["lastModifiedBy"]["user"]["email"].str); } } } // --create-share-link response if (outputType == "ShareableLink") { JSONValue accessScope; JSONValue createShareableLinkResponse; string thisDriveId = fileDetailsFromOneDrive["parentReference"]["driveId"].str; string thisItemId = fileDetailsFromOneDrive["id"].str; string fileShareLink; bool writeablePermissions = appConfig.getValueBool("with_editing_perms"); // What sort of shareable link is required? if (writeablePermissions) { // configure the read-write access scope accessScope = [ "type": "edit", "scope": "anonymous" ]; } else { // configure the read-only access scope (default) accessScope = [ "type": "view", "scope": "anonymous" ]; } // If a share-password was passed use it when creating the link if (strip(appConfig.getValueString("share_password")) != "") { accessScope["password"] = appConfig.getValueString("share_password"); } // Try and create the shareable file link try { createShareableLinkResponse = queryOneDriveForFileDetailsApiInstance.createShareableLink(thisDriveId, thisItemId, accessScope); } catch (OneDriveException exception) { // display what the error is displayOneDriveErrorMessage(exception.msg, thisFunctionName); return; } // Is the API response a valid JSON file? if ((createShareableLinkResponse.type() == JSONType.object) && ("link" in createShareableLinkResponse)) { // Extract the file share link from the JSON response fileShareLink = createShareableLinkResponse["link"]["webUrl"].str; writeln("File Shareable Link: ", fileShareLink); if (writeablePermissions) { writeln("Shareable Link has read-write permissions - use and provide with caution"); } } } } // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory queryOneDriveForFileDetailsApiInstance.releaseCurlEngine(); queryOneDriveForFileDetailsApiInstance = null; // Perform Garbage Collection GC.collect(); } } // was path found? if (!pathInDB) { // File has not been synced with OneDrive addLogEntry("Selected path has not been synced with Microsoft OneDrive: " ~ inputFilePath); } } else { // File does not exist locally addLogEntry("Selected path not found on local system: " ~ inputFilePath); } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Query OneDrive for the quota details void queryOneDriveForQuotaDetails() { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // This function is similar to getRemainingFreeSpace() but is different in data being analysed and output method JSONValue currentDriveQuota; string driveId; OneDriveApi getCurrentDriveQuotaApiInstance; if (appConfig.getValueString("drive_id").length) { driveId = appConfig.getValueString("drive_id"); } else { driveId = appConfig.defaultDriveId; } try { // Create a new OneDrive API instance getCurrentDriveQuotaApiInstance = new OneDriveApi(appConfig); getCurrentDriveQuotaApiInstance.initialise(); if (debugLogging) {addLogEntry("Seeking available quota for this drive id: " ~ driveId, ["debug"]);} currentDriveQuota = getCurrentDriveQuotaApiInstance.getDriveQuota(driveId); // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory getCurrentDriveQuotaApiInstance.releaseCurlEngine(); getCurrentDriveQuotaApiInstance = null; // Perform Garbage Collection GC.collect(); } catch (OneDriveException e) { if (debugLogging) {addLogEntry("currentDriveQuota = onedrive.getDriveQuota(driveId) generated a OneDriveException", ["debug"]);} // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory getCurrentDriveQuotaApiInstance.releaseCurlEngine(); getCurrentDriveQuotaApiInstance = null; // Perform Garbage Collection GC.collect(); } // validate that currentDriveQuota is a JSON value if (currentDriveQuota.type() == JSONType.object) { // was 'quota' in response? if ("quota" in currentDriveQuota) { // debug output of response if (debugLogging) {addLogEntry("currentDriveQuota: " ~ to!string(currentDriveQuota), ["debug"]);} // human readable output of response string deletedValue = "Not Provided"; string remainingValue = "Not Provided"; string stateValue = "Not Provided"; string totalValue = "Not Provided"; string usedValue = "Not Provided"; // Update values if ("deleted" in currentDriveQuota["quota"]) { deletedValue = byteToGibiByte(currentDriveQuota["quota"]["deleted"].integer); } if ("remaining" in currentDriveQuota["quota"]) { remainingValue = byteToGibiByte(currentDriveQuota["quota"]["remaining"].integer); } if ("state" in currentDriveQuota["quota"]) { stateValue = currentDriveQuota["quota"]["state"].str; } if ("total" in currentDriveQuota["quota"]) { totalValue = byteToGibiByte(currentDriveQuota["quota"]["total"].integer); } if ("used" in currentDriveQuota["quota"]) { usedValue = byteToGibiByte(currentDriveQuota["quota"]["used"].integer); } writeln("Microsoft OneDrive quota information as reported for this Drive ID: ", driveId); writeln(); writeln("Deleted: ", deletedValue, " GB (", currentDriveQuota["quota"]["deleted"].integer, " bytes)"); writeln("Remaining: ", remainingValue, " GB (", currentDriveQuota["quota"]["remaining"].integer, " bytes)"); writeln("State: ", stateValue); writeln("Total: ", totalValue, " GB (", currentDriveQuota["quota"]["total"].integer, " bytes)"); writeln("Used: ", usedValue, " GB (", currentDriveQuota["quota"]["used"].integer, " bytes)"); writeln(); } else { writeln("Microsoft OneDrive quota information is being restricted for this Drive ID: ", driveId); } } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Query the system for session_upload.* files bool checkForInterruptedSessionUploads() { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } bool interruptedUploads = false; long interruptedUploadsCount; // Scan the filesystem for the files we are interested in, build up interruptedUploadsSessionFiles array foreach (sessionFile; dirEntries(appConfig.configDirName, "session_upload.*", SpanMode.shallow)) { // calculate the full path string tempPath = buildNormalizedPath(buildPath(appConfig.configDirName, sessionFile)); // add to array interruptedUploadsSessionFiles ~= [tempPath]; } // Count all 'session_upload' files in appConfig.configDirName //interruptedUploadsCount = count(dirEntries(appConfig.configDirName, "session_upload.*", SpanMode.shallow)); interruptedUploadsCount = count(interruptedUploadsSessionFiles); if (interruptedUploadsCount != 0) { interruptedUploads = true; } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return if there are interrupted uploads to process return interruptedUploads; } // Clear any session_upload.* files void clearInterruptedSessionUploads() { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Scan the filesystem for the files we are interested in, build up interruptedUploadsSessionFiles array foreach (sessionFile; dirEntries(appConfig.configDirName, "session_upload.*", SpanMode.shallow)) { // calculate the full path string tempPath = buildNormalizedPath(buildPath(appConfig.configDirName, sessionFile)); JSONValue sessionFileData = readText(tempPath).parseJSON(); addLogEntry("Removing interrupted session upload file due to --resync for: " ~ sessionFileData["localPath"].str, ["info"]); // Process removal if (!dryRun) { safeRemove(tempPath); } } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Process interrupted 'session_upload' files void processForInterruptedSessionUploads() { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // For each upload_session file that has been found, process the data to ensure it is still valid foreach (sessionFilePath; interruptedUploadsSessionFiles) { // What session data are we trying to restore if (verboseLogging) {addLogEntry("Attempting to restore file upload session using this session data file: " ~ sessionFilePath, ["verbose"]);} // Does this pass validation? if (!validateUploadSessionFileData(sessionFilePath)) { // Remove upload_session file as it is invalid // upload_session file contains an error - cant resume this session if (verboseLogging) {addLogEntry("Restore file upload session failed - cleaning up resumable session data file: " ~ sessionFilePath, ["verbose"]);} // cleanup session path if (exists(sessionFilePath)) { if (!dryRun) { remove(sessionFilePath); } } } } // At this point we should have an array of JSON items to resume uploading if (count(jsonItemsToResumeUpload) > 0) { // there are valid items to resume upload // Lets deal with all the JSON items that need to be resumed for upload in a batch process size_t batchSize = to!int(appConfig.getValueLong("threads")); long batchCount = (jsonItemsToResumeUpload.length + batchSize - 1) / batchSize; long batchesProcessed = 0; foreach (chunk; jsonItemsToResumeUpload.chunks(batchSize)) { // send an array containing 'appConfig.getValueLong("threads")' JSON items to resume upload resumeSessionUploadsInParallel(chunk); } // For this set of items, perform a DB PASSIVE checkpoint itemDB.performCheckpoint("PASSIVE"); } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // A resume session upload file need to be valid to be used // This function validates this data bool validateUploadSessionFileData(string sessionFilePath) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Due to this function, we need to keep the return ; code, so that this function operates as efficiently as possible. // It is pointless having the entire code run through and performing additional needless checks where it is not required // Whilst this means some extra code / duplication in this function, it cannot be helped JSONValue sessionFileData; OneDriveApi validateUploadSessionFileDataApiInstance; // Try and read the text from the session file as a JSON array try { if (getSize(sessionFilePath) > 0) { // There is data to read in sessionFileData = readText(sessionFilePath).parseJSON(); } else { // No data to read in - invalid file if (debugLogging) {addLogEntry("SESSION-RESUME: Invalid JSON file: " ~ sessionFilePath, ["debug"]);} // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return session file is invalid return false; } } catch (JSONException e) { if (debugLogging) {addLogEntry("SESSION-RESUME: Invalid JSON data in: " ~ sessionFilePath, ["debug"]);} // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return session file is invalid return false; } // Does the file we wish to resume uploading exist locally still? if ("localPath" in sessionFileData) { string sessionLocalFilePath = sessionFileData["localPath"].str; if (debugLogging) {addLogEntry("SESSION-RESUME: sessionLocalFilePath: " ~ sessionLocalFilePath, ["debug"]);} // Does the file exist? if (!exists(sessionLocalFilePath)) { if (verboseLogging) {addLogEntry("The local file to upload does not exist locally anymore", ["verbose"]);} // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return session file is invalid return false; } // Can we read the file? if (!readLocalFile(sessionLocalFilePath)) { // filesystem error already returned if unable to read // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return session file is invalid return false; } } else { if (debugLogging) {addLogEntry("SESSION-RESUME: No localPath data in: " ~ sessionFilePath, ["debug"]);} // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return session file is invalid return false; } // Check the session data for expirationDateTime if ("expirationDateTime" in sessionFileData) { SysTime expiration; string expirationTimestamp; expirationTimestamp = strip(sessionFileData["expirationDateTime"].str); // is expirationTimestamp valid? if (isValidUTCDateTime(expirationTimestamp)) { // string is a valid timestamp expiration = SysTime.fromISOExtString(expirationTimestamp); } else { // invalid timestamp from JSON file addLogEntry("WARNING: Invalid timestamp provided by the Microsoft OneDrive API: " ~ expirationTimestamp); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return session file is invalid return false; } // valid timestamp if (expiration < Clock.currTime()) { if (verboseLogging) {addLogEntry("The upload session has expired for: " ~ sessionFilePath, ["verbose"]);} // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return session file is invalid return false; } } else { if (debugLogging) {addLogEntry("SESSION-RESUME: No expirationDateTime data in: " ~ sessionFilePath, ["debug"]);} // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return session file is invalid return false; } // Check the online upload status, using the uloadURL in sessionFileData if ("uploadUrl" in sessionFileData) { JSONValue response; try { // Create a new OneDrive API instance validateUploadSessionFileDataApiInstance = new OneDriveApi(appConfig); validateUploadSessionFileDataApiInstance.initialise(); // Request upload status response = validateUploadSessionFileDataApiInstance.requestUploadStatus(sessionFileData["uploadUrl"].str); // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory validateUploadSessionFileDataApiInstance.releaseCurlEngine(); validateUploadSessionFileDataApiInstance = null; // Perform Garbage Collection GC.collect(); // no error .. potentially all still valid } catch (OneDriveException e) { // handle any onedrive error response as invalid if (debugLogging) {addLogEntry("SESSION-RESUME: Invalid response when using uploadUrl in: " ~ sessionFilePath, ["debug"]);} // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory validateUploadSessionFileDataApiInstance.releaseCurlEngine(); validateUploadSessionFileDataApiInstance = null; // Perform Garbage Collection GC.collect(); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return session file is invalid return false; } // Do we have a valid response from OneDrive? if (response.type() == JSONType.object) { // Valid JSON object was returned if (("expirationDateTime" in response) && ("nextExpectedRanges" in response)) { // The 'uploadUrl' is valid, and the response contains elements we need sessionFileData["expirationDateTime"] = response["expirationDateTime"]; sessionFileData["nextExpectedRanges"] = response["nextExpectedRanges"]; if (sessionFileData["nextExpectedRanges"].array.length == 0) { if (verboseLogging) {addLogEntry("The upload session was already completed", ["verbose"]);} // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return session file is invalid return false; } } else { if (debugLogging) {addLogEntry("SESSION-RESUME: No expirationDateTime & nextExpectedRanges data in Microsoft OneDrive API response: " ~ to!string(response), ["debug"]);} // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return session file is invalid return false; } } else { // not a JSON object if (verboseLogging) {addLogEntry("Restore file upload session failed - invalid response from Microsoft OneDrive", ["verbose"]);} // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return session file is invalid return false; } } else { if (debugLogging) {addLogEntry("SESSION-RESUME: No uploadUrl data in: " ~ sessionFilePath, ["debug"]);} // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return session file is invalid return false; } // Add 'sessionFilePath' to 'sessionFileData' so that it can be used when we reuse the JSON data to resume the upload sessionFileData["sessionFilePath"] = sessionFilePath; // Add sessionFileData to jsonItemsToResumeUpload as it is now valid jsonItemsToResumeUpload ~= sessionFileData; // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return session file is invalid return true; } // Resume all resumable session uploads in parallel void resumeSessionUploadsInParallel(JSONValue[] array) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // This function received an array of JSON items to resume upload, the number of elements based on appConfig.getValueLong("threads") foreach (i, jsonItemToResume; processPool.parallel(array)) { // Take each JSON item and resume upload using the JSON data JSONValue uploadResponse; OneDriveApi uploadFileOneDriveApiInstance; // Create a new API instance uploadFileOneDriveApiInstance = new OneDriveApi(appConfig); uploadFileOneDriveApiInstance.initialise(); // Pull out data from this JSON element string threadUploadSessionFilePath = jsonItemToResume["sessionFilePath"].str; long thisFileSizeLocal = getSize(jsonItemToResume["localPath"].str); // Try to resume the session upload using the provided data try { uploadResponse = performSessionFileUpload(uploadFileOneDriveApiInstance, thisFileSizeLocal, jsonItemToResume, threadUploadSessionFilePath); } catch (OneDriveException exception) { writeln("CODING TO DO: Handle an exception when performing a resume session upload"); } // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory uploadFileOneDriveApiInstance.releaseCurlEngine(); uploadFileOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); // Was the response from the OneDrive API a valid JSON item? if (uploadResponse.type() == JSONType.object) { // A valid JSON object was returned - session resumption upload successful // Are we in an --upload-only & --remove-source-files scenario? // Use actual config values as we are doing an upload session recovery if (localDeleteAfterUpload) { // Log that we are deleting a local item addLogEntry("Removing local file as --upload-only & --remove-source-files configured"); // are we in a --dry-run scenario? if (!dryRun) { // No --dry-run ... process local file delete // Only perform the delete if we have a valid file path if (exists(jsonItemToResume["localPath"].str)) { // file exists if (debugLogging) {addLogEntry("Removing local file: " ~ jsonItemToResume["localPath"].str, ["debug"]);} safeRemove(jsonItemToResume["localPath"].str); } } // as file is removed, we have nothing to add to the local database if (debugLogging) {addLogEntry("Skipping adding to database as --upload-only & --remove-source-files configured", ["debug"]);} } else { // Save JSON item in database saveItem(uploadResponse); } } else { // No valid response was returned addLogEntry("CODING TO DO: what to do when session upload resumption JSON data is not valid ... nothing ? error message ?"); } } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Function to process the path by removing prefix up to ':' - remove '/drive/root:' from a path string string processPathToRemoveRootReference(ref string pathToCheck) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } size_t colonIndex = pathToCheck.indexOf(":"); if (colonIndex != -1) { if (debugLogging) {addLogEntry("Updating " ~ pathToCheck ~ " to remove prefix up to ':'", ["debug"]);} pathToCheck = pathToCheck[colonIndex + 1 .. $]; if (debugLogging) {addLogEntry("Updated path: " ~ pathToCheck, ["debug"]);} } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return updated path return pathToCheck; } // Generate path from JSON data string generatePathFromJSONData(JSONValue onedriveJSONItem) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } string itemName = onedriveJSONItem["name"].str; string parentPath = onedriveJSONItem["parentReference"]["path"].str; string combinedPath = buildNormalizedPath(buildPath(parentPath, itemName)); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } return processPathToRemoveRootReference(combinedPath); } // Function to find a given DriveId in the onlineDriveDetails associative array that maps driveId to DriveDetailsCache // If 'true' will return 'driveDetails' containing the struct data 'DriveDetailsCache' bool canFindDriveId(string driveId, out DriveDetailsCache driveDetails) { // Not adding performance metrics to this function auto ptr = driveId in onlineDriveDetails; if (ptr !is null) { driveDetails = *ptr; // Dereference the pointer to get the value return true; } else { return false; } } // Add this driveId plus relevant details for future reference and use void addOrUpdateOneDriveOnlineDetails(string driveId) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } bool quotaRestricted; bool quotaAvailable; long quotaRemaining; // Get the data from online auto onlineDriveData = getRemainingFreeSpaceOnline(driveId); quotaRestricted = to!bool(onlineDriveData[0][0]); quotaAvailable = to!bool(onlineDriveData[0][1]); quotaRemaining = to!long(onlineDriveData[0][2]); onlineDriveDetails[driveId] = DriveDetailsCache(driveId, quotaRestricted, quotaAvailable, quotaRemaining); // Debug log what the cached array now contains if (debugLogging) {addLogEntry("onlineDriveDetails: " ~ to!string(onlineDriveDetails), ["debug"]);} // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Return a specific 'driveId' details from 'onlineDriveDetails' DriveDetailsCache getDriveDetails(string driveId) { // Not adding performance metrics to this function auto ptr = driveId in onlineDriveDetails; if (ptr !is null) { return *ptr; // Dereference the pointer to get the value } else { // Return a default DriveDetailsCache or handle the case where the driveId is not found return DriveDetailsCache.init; // Return default-initialised struct } } // Search a given Drive ID, Item ID and filename to see if this exists in the location specified JSONValue searchDriveItemForFile(string parentItemDriveId, string parentItemId, string fileToUpload) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } JSONValue onedriveJSONItem; string searchName = baseName(fileToUpload); JSONValue thisLevelChildren; string nextLink; // Create a new API Instance for this thread and initialise it OneDriveApi checkFileOneDriveApiInstance; checkFileOneDriveApiInstance = new OneDriveApi(appConfig); checkFileOneDriveApiInstance.initialise(); while (true) { // Check if exitHandlerTriggered is true if (exitHandlerTriggered) { // break out of the 'while (true)' loop break; } // query top level children try { thisLevelChildren = checkFileOneDriveApiInstance.listChildren(parentItemDriveId, parentItemId, nextLink); } catch (OneDriveException exception) { // OneDrive threw an error if (debugLogging) { addLogEntry(debugLogBreakType1, ["debug"]); addLogEntry("Query Error: thisLevelChildren = checkFileOneDriveApiInstance.listChildren(parentItemDriveId, parentItemId, nextLink)", ["debug"]); addLogEntry("driveId: " ~ parentItemDriveId, ["debug"]); addLogEntry("idToQuery: " ~ parentItemId, ["debug"]); addLogEntry("nextLink: " ~ nextLink, ["debug"]); } // Default operation if not 408,429,503,504 errors // - 408,429,503,504 errors are handled as a retry within oneDriveApiInstance // Display what the error is displayOneDriveErrorMessage(exception.msg, thisFunctionName); } // process thisLevelChildren response foreach (child; thisLevelChildren["value"].array) { // Only looking at files if ((child["name"].str == searchName) && (("file" in child) != null)) { // Found the matching file, return its JSON representation // Operations in this thread are done / complete // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory checkFileOneDriveApiInstance.releaseCurlEngine(); checkFileOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // Return child as found item return child; } } // If a collection exceeds the default page size (200 items), the @odata.nextLink property is returned in the response // to indicate more items are available and provide the request URL for the next page of items. if ("@odata.nextLink" in thisLevelChildren) { // Update nextLink to next changeSet bundle if (debugLogging) {addLogEntry("Setting nextLink to (@odata.nextLink): " ~ nextLink, ["debug"]);} nextLink = thisLevelChildren["@odata.nextLink"].str; } else break; // Sleep for a while to avoid busy-waiting Thread.sleep(dur!"msecs"(100)); // Adjust the sleep duration as needed } // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory checkFileOneDriveApiInstance.releaseCurlEngine(); checkFileOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // return an empty JSON item, as search item was not found return onedriveJSONItem; } // Update 'onlineDriveDetails' with the latest data about this drive void updateDriveDetailsCache(string driveId, bool quotaRestricted, bool quotaAvailable, long localFileSize) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // As each thread is running differently, what is the current 'quotaRemaining' for 'driveId' ? long quotaRemaining; DriveDetailsCache cachedOnlineDriveData; cachedOnlineDriveData = getDriveDetails(driveId); quotaRemaining = cachedOnlineDriveData.quotaRemaining; // Update 'quotaRemaining' quotaRemaining = quotaRemaining - localFileSize; // Do the flags get updated? if (quotaRemaining <= 0) { if (appConfig.accountType == "personal"){ if (driveId == appConfig.defaultDriveId) { // zero space available on our drive addLogEntry("ERROR: OneDrive account currently has zero space available. Please free up some space online or purchase additional capacity."); quotaRemaining = 0; quotaAvailable = false; } } else { // zero space available is being reported, maybe being restricted? if (verboseLogging) {addLogEntry("WARNING: OneDrive quota information is being restricted or providing a zero value. Please fix by speaking to your OneDrive / Office 365 Administrator.", ["verbose"]);} quotaRemaining = 0; quotaRestricted = true; } } // Updated the details onlineDriveDetails[driveId] = DriveDetailsCache(driveId, quotaRestricted, quotaAvailable, quotaRemaining); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Update all of the known cached driveId quota details void freshenCachedDriveQuotaDetails() { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } foreach (driveId; onlineDriveDetails.keys) { // Update this driveid quota details if (debugLogging) {addLogEntry("Freshen Quota Details for this driveId: " ~ driveId, ["debug"]);} addOrUpdateOneDriveOnlineDetails(driveId); } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Create a 'root' DB Tie Record for a Shared Folder from the JSON data void createDatabaseRootTieRecordForOnlineSharedFolder(JSONValue onedriveJSONItem) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Creating|Updating a DB Tie if (debugLogging) { addLogEntry("Creating|Updating a 'root' DB Tie Record for this Shared Folder: " ~ onedriveJSONItem["name"].str, ["debug"]); addLogEntry("Raw JSON for 'root' DB Tie Record: " ~ to!string(onedriveJSONItem), ["debug"]); } // New DB Tie Item to detail the 'root' of the Shared Folder Item tieDBItem; string lastModifiedTimestamp; tieDBItem.name = "root"; // Get the right parentReference details if (isItemRemote(onedriveJSONItem)) { tieDBItem.driveId = onedriveJSONItem["remoteItem"]["parentReference"]["driveId"].str; tieDBItem.id = onedriveJSONItem["remoteItem"]["id"].str; } else { if (onedriveJSONItem["name"].str != "root") { tieDBItem.driveId = onedriveJSONItem["parentReference"]["driveId"].str; // OneDrive Personal JSON responses are in-consistent with not having 'id' available if (hasParentReferenceId(onedriveJSONItem)) { // Use the parent reference id tieDBItem.id = onedriveJSONItem["parentReference"]["id"].str; } else { // Testing evidence shows that for Personal accounts, use the 'id' itself tieDBItem.id = onedriveJSONItem["id"].str; } } else { tieDBItem.driveId = onedriveJSONItem["parentReference"]["driveId"].str; tieDBItem.id = onedriveJSONItem["id"].str; } } // set the item type tieDBItem.type = ItemType.root; // get the lastModifiedDateTime lastModifiedTimestamp = strip(onedriveJSONItem["fileSystemInfo"]["lastModifiedDateTime"].str); // is lastModifiedTimestamp valid? if (isValidUTCDateTime(lastModifiedTimestamp)) { // string is a valid timestamp tieDBItem.mtime = SysTime.fromISOExtString(lastModifiedTimestamp); } else { // invalid timestamp from JSON file addLogEntry("WARNING: Invalid timestamp provided by the Microsoft OneDrive API: " ~ lastModifiedTimestamp); // Set mtime to SysTime(0) tieDBItem.mtime = SysTime(0); } // ensure there is no parentId tieDBItem.parentId = null; // Issue #3115 - Validate driveId length // What account type is this? if (appConfig.accountType == "personal") { // Test driveId length and validation if the driveId we are testing is not equal to appConfig.defaultDriveId if (tieDBItem.driveId != appConfig.defaultDriveId) { tieDBItem.driveId = testProvidedDriveIdForLengthIssue(tieDBItem.driveId); } } // Add this DB Tie parent record to the local database if (debugLogging) {addLogEntry("Creating|Updating into local database a 'root' DB Tie record for a OneDrive Shared Folder online: " ~ to!string(tieDBItem), ["debug"]);} itemDB.upsert(tieDBItem); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Create a DB Tie Record for a Shared Folder void createDatabaseTieRecordForOnlineSharedFolder(Item parentItem) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Creating|Updating a DB Tie if (debugLogging) { //addLogEntry("Creating|Updating a DB Tie Record for this Shared Folder: " ~ parentItem.name, ["debug"]); addLogEntry("Creating|Updating a DB Tie Record for this Shared Folder from the provided parental data: " ~ parentItem.name, ["debug"]); addLogEntry("Parent Item Record: " ~ to!string(parentItem), ["debug"]); } // New DB Tie Item to bind the 'remote' path to our parent path in the database Item tieDBItem; tieDBItem.name = parentItem.name; tieDBItem.id = parentItem.remoteId; tieDBItem.type = ItemType.dir; tieDBItem.mtime = parentItem.mtime; // Initially set this tieDBItem.driveId = parentItem.remoteDriveId; // What account type is this as this determines what 'tieDBItem.parentId' should be set to // There is a difference in the JSON responses between 'personal' and 'business' account types for Shared Folders // Essentially an API inconsistency if (appConfig.accountType == "personal") { // Set tieDBItem.parentId to null tieDBItem.parentId = null; tieDBItem.type = ItemType.root; // Issue #3136, #3139 #3143 // Fetch the actual online record for this item // This returns the actual OneDrive Personal driveId value and is 15 character checked string actualOnlineDriveId = testProvidedDriveIdForLengthIssue(fetchRealOnlineDriveIdentifier(tieDBItem.driveId)); tieDBItem.driveId = actualOnlineDriveId; } else { // The tieDBItem.parentId needs to be the correct driveId id reference // Query the DB Item[] rootDriveItems; Item dbRecord; rootDriveItems = itemDB.selectByDriveId(parentItem.remoteDriveId); // Fix Issue #2883 if (rootDriveItems.length > 0) { // Use the first record returned dbRecord = rootDriveItems[0]; tieDBItem.parentId = dbRecord.id; } else { // Business Account ... but itemDB.selectByDriveId returned no entries ... need to query for this item online to get the correct details given they are not in the database if (debugLogging) {addLogEntry("itemDB.selectByDriveId(parentItem.remoteDriveId) returned zero database entries for this remoteDriveId: " ~ to!string(parentItem.remoteDriveId), ["debug"]);} // Create a new API Instance for this query and initialise it OneDriveApi getPathDetailsApiInstance; JSONValue latestOnlineDetails; getPathDetailsApiInstance = new OneDriveApi(appConfig); getPathDetailsApiInstance.initialise(); try { // Get the latest online details latestOnlineDetails = getPathDetailsApiInstance.getPathDetailsById(parentItem.remoteDriveId, parentItem.remoteId); if (debugLogging) {addLogEntry("Parent JSON details from Online Query: " ~ to!string(latestOnlineDetails), ["debug"]);} // Convert JSON to a database compatible item Item tempOnlineRecord = makeItem(latestOnlineDetails); // Configure tieDBItem.parentId to use tempOnlineRecord.id tieDBItem.parentId = tempOnlineRecord.id; // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory getPathDetailsApiInstance.releaseCurlEngine(); getPathDetailsApiInstance = null; // Perform Garbage Collection GC.collect(); } catch (OneDriveException e) { // Display error message displayOneDriveErrorMessage(e.msg, thisFunctionName); // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory getPathDetailsApiInstance.releaseCurlEngine(); getPathDetailsApiInstance = null; // Perform Garbage Collection GC.collect(); return; } } // Free the array memory rootDriveItems = []; } // Add tie DB record to the local database if (debugLogging) {addLogEntry("Creating|Updating into local database a DB Tie record: " ~ to!string(tieDBItem), ["debug"]);} itemDB.upsert(tieDBItem); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // List all the OneDrive Business Shared Items for the user to see void listBusinessSharedObjects() { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } JSONValue sharedWithMeItems; // Create a new API Instance for this thread and initialise it OneDriveApi sharedWithMeOneDriveApiInstance; sharedWithMeOneDriveApiInstance = new OneDriveApi(appConfig); sharedWithMeOneDriveApiInstance.initialise(); try { sharedWithMeItems = sharedWithMeOneDriveApiInstance.getSharedWithMe(); // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory sharedWithMeOneDriveApiInstance.releaseCurlEngine(); sharedWithMeOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); } catch (OneDriveException e) { // Display error message displayOneDriveErrorMessage(e.msg, thisFunctionName); // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory sharedWithMeOneDriveApiInstance.releaseCurlEngine(); sharedWithMeOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); return; } if (sharedWithMeItems.type() == JSONType.object) { if (count(sharedWithMeItems["value"].array) > 0) { // No shared items addLogEntry(); addLogEntry("Listing available OneDrive Business Shared Items:"); addLogEntry(); // Iterate through the array foreach (searchResult; sharedWithMeItems["value"].array) { // loop variables for each item string sharedByName; string sharedByEmail; // Debug response output if (debugLogging) {addLogEntry("shared folder entry: " ~ to!string(searchResult), ["debug"]);} // Configure 'who' this was shared by if ("sharedBy" in searchResult["remoteItem"]["shared"]) { // we have shared by details we can use if ("displayName" in searchResult["remoteItem"]["shared"]["sharedBy"]["user"]) { sharedByName = searchResult["remoteItem"]["shared"]["sharedBy"]["user"]["displayName"].str; } if ("email" in searchResult["remoteItem"]["shared"]["sharedBy"]["user"]) { sharedByEmail = searchResult["remoteItem"]["shared"]["sharedBy"]["user"]["email"].str; } } // Output query result addLogEntry(debugLogBreakType1); if (isItemFile(searchResult)) { addLogEntry("Shared File: " ~ to!string(searchResult["name"].str)); } else { addLogEntry("Shared Folder: " ~ to!string(searchResult["name"].str)); } // Detail 'who' shared this if ((sharedByName != "") && (sharedByEmail != "")) { addLogEntry("Shared By: " ~ sharedByName ~ " (" ~ sharedByEmail ~ ")"); } else { if (sharedByName != "") { addLogEntry("Shared By: " ~ sharedByName); } } // More detail if --verbose is being used if (verboseLogging) { addLogEntry("Item Id: " ~ searchResult["remoteItem"]["id"].str, ["verbose"]); addLogEntry("Parent Drive Id: " ~ searchResult["remoteItem"]["parentReference"]["driveId"].str, ["verbose"]); if ("id" in searchResult["remoteItem"]["parentReference"]) { addLogEntry("Parent Item Id: " ~ searchResult["remoteItem"]["parentReference"]["id"].str, ["verbose"]); } } } // Close out the loop addLogEntry(debugLogBreakType1); addLogEntry(); } else { // No shared items addLogEntry(); addLogEntry("No OneDrive Business Shared Folders were returned"); addLogEntry(); } } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Query all the OneDrive Business Shared Objects to sync only Shared Files void queryBusinessSharedObjects() { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } JSONValue sharedWithMeItems; Item sharedFilesRootDirectoryDatabaseRecord; // Create a new API Instance for this thread and initialise it OneDriveApi sharedWithMeOneDriveApiInstance; sharedWithMeOneDriveApiInstance = new OneDriveApi(appConfig); sharedWithMeOneDriveApiInstance.initialise(); try { sharedWithMeItems = sharedWithMeOneDriveApiInstance.getSharedWithMe(); // We cant shutdown the API instance here, as we reuse it below } catch (OneDriveException e) { // Display error message displayOneDriveErrorMessage(e.msg, thisFunctionName); // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory sharedWithMeOneDriveApiInstance.releaseCurlEngine(); sharedWithMeOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); return; } // Valid JSON response if (sharedWithMeItems.type() == JSONType.object) { // Get the configuredBusinessSharedFilesDirectoryName DB item // We need this as we need to 'fake' create all the folders for the shared files // Then fake create the file entries for the database with the correct parent folder that is the local folder itemDB.selectByPath(baseName(appConfig.configuredBusinessSharedFilesDirectoryName), appConfig.defaultDriveId, sharedFilesRootDirectoryDatabaseRecord); // For each item returned, if a file, process it foreach (searchResult; sharedWithMeItems["value"].array) { // Shared Business Folders are added to the account using 'Add shortcut to My files' // We only care here about any remaining 'files' that are shared with the user if (isItemFile(searchResult)) { // Debug response output if (debugLogging) {addLogEntry("getSharedWithMe Response Shared File JSON: " ~ sanitiseJSONItem(searchResult), ["debug"]);} // Make a DB item from this JSON Item sharedFileOriginalData = makeItem(searchResult); // Variables for each item string sharedByName; string sharedByEmail; string sharedByFolderName; string newLocalSharedFilePath; string newItemPath; Item sharedFilesPath; JSONValue fileToDownload; JSONValue detailsToUpdate; JSONValue latestOnlineDetails; // Configure 'who' this was shared by if ("sharedBy" in searchResult["remoteItem"]["shared"]) { // we have shared by details we can use if ("displayName" in searchResult["remoteItem"]["shared"]["sharedBy"]["user"]) { sharedByName = searchResult["remoteItem"]["shared"]["sharedBy"]["user"]["displayName"].str; } if ("email" in searchResult["remoteItem"]["shared"]["sharedBy"]["user"]) { sharedByEmail = searchResult["remoteItem"]["shared"]["sharedBy"]["user"]["email"].str; } } // Configure 'who' shared this, so that we can create the directory for that users shared files with us if ((sharedByName != "") && (sharedByEmail != "")) { sharedByFolderName = sharedByName ~ " (" ~ sharedByEmail ~ ")"; } else { if (sharedByName != "") { sharedByFolderName = sharedByName; } } // Create the local path to store this users shared files with us newLocalSharedFilePath = buildNormalizedPath(buildPath(appConfig.configuredBusinessSharedFilesDirectoryName, sharedByFolderName)); // Does the Shared File Users Local Directory to store the shared file(s) exist? if (!exists(newLocalSharedFilePath)) { // Folder does not exist locally and needs to be created addLogEntry("Creating the OneDrive Business Shared File Users Local Directory: " ~ newLocalSharedFilePath); // Local folder does not exist, thus needs to be created mkdirRecurse(newLocalSharedFilePath); // As this will not be created online, generate a response so it can be saved to the database sharedFilesPath = makeItem(createFakeResponse(baseName(newLocalSharedFilePath))); // Update sharedFilesPath parent items to that of sharedFilesRootDirectoryDatabaseRecord sharedFilesPath.parentId = sharedFilesRootDirectoryDatabaseRecord.id; // Add DB record to the local database if (debugLogging) {addLogEntry("Creating|Updating into local database a DB record for storing OneDrive Business Shared Files: " ~ to!string(sharedFilesPath), ["debug"]);} itemDB.upsert(sharedFilesPath); } else { // Folder exists locally, is the folder in the database? // Query DB for this path Item dbRecord; if (!itemDB.selectByPath(baseName(newLocalSharedFilePath), appConfig.defaultDriveId, dbRecord)) { // As this will not be created online, generate a response so it can be saved to the database sharedFilesPath = makeItem(createFakeResponse(baseName(newLocalSharedFilePath))); // Update sharedFilesPath parent items to that of sharedFilesRootDirectoryDatabaseRecord sharedFilesPath.parentId = sharedFilesRootDirectoryDatabaseRecord.id; // Add DB record to the local database if (debugLogging) {addLogEntry("Creating|Updating into local database a DB record for storing OneDrive Business Shared Files: " ~ to!string(sharedFilesPath), ["debug"]);} itemDB.upsert(sharedFilesPath); } } // The file to download JSON details fileToDownload = searchResult; // Get the latest online details latestOnlineDetails = sharedWithMeOneDriveApiInstance.getPathDetailsById(sharedFileOriginalData.remoteDriveId, sharedFileOriginalData.remoteId); Item tempOnlineRecord = makeItem(latestOnlineDetails); // With the local folders created, now update 'fileToDownload' to download the file to our location: // "parentReference": { // "driveId": "", // "driveType": "business", // "id": "", // }, // The getSharedWithMe() JSON response also contains an API bug where the 'hash' of the file is not provided // Use the 'latestOnlineDetails' response to obtain the hash // "file": { // "hashes": { // "quickXorHash": "" // } // }, // // The getSharedWithMe() JSON response also contains an API bug where the 'size' of the file is not the actual size of the file // The getSharedWithMe() JSON response also contains an API bug where the 'eTag' of the file is not present // The getSharedWithMe() JSON response also contains an API bug where the 'lastModifiedDateTime' of the file is date when the file was shared, not the actual date last modified detailsToUpdate = [ "parentReference": JSONValue([ "driveId": JSONValue(appConfig.defaultDriveId), "driveType": JSONValue("business"), "id": JSONValue(sharedFilesPath.id) ]), "file": JSONValue([ "hashes":JSONValue([ "quickXorHash": JSONValue(tempOnlineRecord.quickXorHash) ]) ]), "eTag": JSONValue(tempOnlineRecord.eTag) ]; foreach (string key, JSONValue value; detailsToUpdate.object) { fileToDownload[key] = value; } // Update specific items // Update 'size' fileToDownload["size"] = to!int(tempOnlineRecord.size); fileToDownload["remoteItem"]["size"] = to!int(tempOnlineRecord.size); // Update 'lastModifiedDateTime' fileToDownload["lastModifiedDateTime"] = latestOnlineDetails["fileSystemInfo"]["lastModifiedDateTime"].str; fileToDownload["fileSystemInfo"]["lastModifiedDateTime"] = latestOnlineDetails["fileSystemInfo"]["lastModifiedDateTime"].str; fileToDownload["remoteItem"]["lastModifiedDateTime"] = latestOnlineDetails["fileSystemInfo"]["lastModifiedDateTime"].str; fileToDownload["remoteItem"]["fileSystemInfo"]["lastModifiedDateTime"] = latestOnlineDetails["fileSystemInfo"]["lastModifiedDateTime"].str; // Final JSON that will be used to download the file if (debugLogging) {addLogEntry("Final fileToDownload: " ~ to!string(fileToDownload), ["debug"]);} // Make the new DB item from the consolidated JSON item Item downloadSharedFileDbItem = makeItem(fileToDownload); // Calculate the full local path for this shared file newItemPath = computeItemPath(downloadSharedFileDbItem.driveId, downloadSharedFileDbItem.parentId) ~ "/" ~ downloadSharedFileDbItem.name; // Does this potential file exists on disk? if (!exists(newItemPath)) { // The shared file does not exists locally // Is this something we actually want? Check the JSON against Client Side Filtering Rules bool unwanted = checkJSONAgainstClientSideFiltering(fileToDownload); if (!unwanted) { // File has not been excluded via Client Side Filtering // Submit this shared file to be processed further for downloading applyPotentiallyNewLocalItem(downloadSharedFileDbItem, fileToDownload, newItemPath); } } else { // A file, in the desired local location already exists with the same name // Is this local file in sync? string itemSource = "remote"; if (!isItemSynced(downloadSharedFileDbItem, newItemPath, itemSource)) { // Not in sync .... Item existingDatabaseItem; bool existingDBEntry = itemDB.selectById(downloadSharedFileDbItem.driveId, downloadSharedFileDbItem.id, existingDatabaseItem); // Is there a DB entry? if (existingDBEntry) { // Existing DB entry // Need to be consistent here with how 'newItemPath' was calculated string existingItemPath = computeItemPath(existingDatabaseItem.driveId, existingDatabaseItem.parentId) ~ "/" ~ existingDatabaseItem.name; // Attempt to apply this changed item applyPotentiallyChangedItem(existingDatabaseItem, existingItemPath, downloadSharedFileDbItem, newItemPath, fileToDownload); } else { // File exists locally, it is not in sync, there is no record in the DB of this file // In case the renamed path is needed string renamedPath; // If local data protection is configured (bypassDataPreservation = false), safeBackup the local file, passing in if we are performing a --dry-run or not safeBackup(newItemPath, dryRun, bypassDataPreservation, renamedPath); // Submit this shared file to be processed further for downloading applyPotentiallyNewLocalItem(downloadSharedFileDbItem, fileToDownload, newItemPath); } } else { // Item is in sync, ensure the DB record is the same itemDB.upsert(downloadSharedFileDbItem); } } } } } // OneDrive API Instance Cleanup - Shutdown API, free curl object and memory sharedWithMeOneDriveApiInstance.releaseCurlEngine(); sharedWithMeOneDriveApiInstance = null; // Perform Garbage Collection GC.collect(); // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Renaming or moving a directory online manually using --source-directory 'path/as/source/' --destination-directory 'path/as/destination' void moveOrRenameDirectoryOnline(string sourcePath, string destinationPath) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Function Variables bool sourcePathExists = false; bool destinationPathExists = false; bool invalidDestination = false; JSONValue sourcePathData; JSONValue destinationPathData; JSONValue parentPathData; Item sourceItem; Item parentItem; // Log that we are doing a move addLogEntry("Moving " ~ sourcePath ~ " to " ~ destinationPath); // Create a new API Instance for this thread and initialise it OneDriveApi onlineMoveApiInstance; onlineMoveApiInstance = new OneDriveApi(appConfig); onlineMoveApiInstance.initialise(); // In order to move, the 'source' needs to exist online, so this is the first check try { sourcePathData = onlineMoveApiInstance.getPathDetails(sourcePath); sourceItem = makeItem(sourcePathData); sourcePathExists = true; } catch (OneDriveException exception) { if (exception.httpStatusCode == 404) { // The item to search was not found. If it does not exist, how can we move it? addLogEntry("The source path to move does not exist online - unable to move|rename a path that does not already exist online"); forceExit(); } else { // An error, regardless of what it is ... not good // Display what the error is // - 408,429,503,504 errors are handled as a retry within uploadFileOneDriveApiInstance displayOneDriveErrorMessage(exception.msg, thisFunctionName); forceExit(); } } // The second check needs to be that the destination does not already exist try { destinationPathData = onlineMoveApiInstance.getPathDetails(destinationPath); destinationPathExists = true; addLogEntry("The destination path to move to exists online - unable to move|rename to a path that already exists online"); forceExit(); } catch (OneDriveException exception) { if (exception.httpStatusCode == 404) { // The item to search was not found. This is good as the destination path is empty } else { // An error, regardless of what it is ... not good // Display what the error is // - 408,429,503,504 errors are handled as a retry within uploadFileOneDriveApiInstance displayOneDriveErrorMessage(exception.msg, thisFunctionName); forceExit(); } } // Can we move? if ((sourcePathExists) && (!destinationPathExists)) { // Make an item we can use Item onlineItem = makeItem(sourcePathData); // The directory to move MUST be a directory if (onlineItem.type == ItemType.dir) { // Validate that the 'destination' is valid // This not a Client Side Filtering check, nor a Microsoft Check, but is a sanity check that the path provided is UTF encoded correctly // Check the std.encoding of the path against: Unicode 5.0, ASCII, ISO-8859-1, ISO-8859-2, WINDOWS-1250, WINDOWS-1251, WINDOWS-1252 if (!invalidDestination) { if(!isValid(destinationPath)) { // Path is not valid according to https://dlang.org/phobos/std_encoding.html addLogEntry("Skipping move - invalid character encoding sequence: " ~ destinationPath, ["info", "notify"]); invalidDestination = true; } } // We do not check this path against the Client Side Filtering Rules as this is 100% an online move only // Check this path against the Microsoft Naming Conventions & Restrictions // - Check path against Microsoft OneDrive restriction and limitations about Windows naming for files and folders // - Check path for bad whitespace items // - Check path for HTML ASCII Codes // - Check path for ASCII Control Codes if (!invalidDestination) { invalidDestination = checkPathAgainstMicrosoftNamingRestrictions(destinationPath, "move"); } // Is the destination location invalid? if (!invalidDestination) { // We can perform the online move // We need to query for the parent information of the destination path string parentPath = dirName(destinationPath); // Configure the parentItem by if this is the account 'root' use the root details, or query online for the parent details if (parentPath == ".") { // Parent path is '.' which is the account root - use client defaults parentItem.driveId = appConfig.defaultDriveId; // Should give something like 12345abcde1234a1 parentItem.id = appConfig.defaultRootId; // Should give something like 12345ABCDE1234A1!101 } else { // Need to query to obtain the details try { if (debugLogging) {addLogEntry("Attempting to query OneDrive Online for this parent path: " ~ parentPath, ["debug"]);} parentPathData = onlineMoveApiInstance.getPathDetails(parentPath); if (debugLogging) {addLogEntry("Online Parent Path Query Response: " ~ to!string(parentPathData), ["debug"]);} parentItem = makeItem(parentPathData); } catch (OneDriveException exception) { if (exception.httpStatusCode == 404) { // The item to search was not found. If it does not exist, how can we move it? addLogEntry("The parent path to move to does not exist online - unable to move|rename a path to a parent that does exist online"); forceExit(); } else { // Display what the error is // - 408,429,503,504 errors are handled as a retry within uploadFileOneDriveApiInstance displayOneDriveErrorMessage(exception.msg, thisFunctionName); forceExit(); } } } // Configure the modification JSON item SysTime mtime; // Use the current system time mtime = Clock.currTime().toUTC(); JSONValue data = [ "name": JSONValue(baseName(destinationPath)), "parentReference": JSONValue([ "id": parentItem.id ]), "fileSystemInfo": JSONValue([ "lastModifiedDateTime": mtime.toISOExtString() ]) ]; // Try the online move try { onlineMoveApiInstance.updateById(sourceItem.driveId, sourceItem.id, data, sourceItem.eTag); // Log that it was successful addLogEntry("Successfully moved " ~ sourcePath ~ " to " ~ destinationPath); } catch (OneDriveException exception) { // Display what the error is // - 408,429,503,504 errors are handled as a retry within uploadFileOneDriveApiInstance displayOneDriveErrorMessage(exception.msg, thisFunctionName); forceExit(); } } } else { // The source item is not a directory addLogEntry("ERROR: The source path to move is not a directory"); forceExit(); } } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } // Return an array of the notification parameters when this is called. This implements FR #2760 string[] fileTransferNotifications() { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Based on the configuration option, send the file transfer actions to the GUI notifications if configured // GUI notifications are already sent for files that meet this criteria: // - Skipping a particular item due to an invalid name // - Skipping a particular item due to an invalid symbolic link // - Skipping a particular item due to an invalid UTF sequence // - Skipping a particular item due to an invalid character encoding sequence // - Files that fail to upload // - Files that fail to download // // This is about notifying on: // - Successful file download // - Successful file upload // - Successful deletion locally // - Successful deletion online string[] loggingOptions; if (appConfig.getValueBool("notify_file_actions")) { // Add the 'notify' to enable GUI notifications loggingOptions = ["info", "notify"]; } else { // Logging to console and/or logfile only loggingOptions = ["info"]; } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } return loggingOptions; } // OneDrive Personal driveId or parentReference driveId must be 16 characters in length string testProvidedDriveIdForLengthIssue(string objectParentDriveId) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Due to this function, we need to keep the return ; code, so that this function operates as efficiently as possible. // Whilst this means some extra code / duplication in this function, it cannot be helped // OneDrive Personal Account driveId and remoteDriveId length check // Issue #3072 (https://github.com/abraunegg/onedrive/issues/3072) illustrated that the OneDrive API is inconsistent in response when the Drive ID starts with a zero ('0') // - driveId // - remoteDriveId // // Example: // 024470056F5C3E43 (driveId) // 24470056f5c3e43 (remoteDriveId) // // If this is a OneDrive Personal Account, ensure this value is 16 characters, padded by leading zero's if eventually required string oldEntry; string newEntry; // Check the provided objectParentDriveId if (!objectParentDriveId.empty) { // Ensure objectParentDriveId is 16 characters long by padding with leading zeros if required if (debugLogging) { string validationMessage = format("Validating that the provided OneDrive Personal 'driveId' value '%s' is 16 characters", objectParentDriveId); addLogEntry(validationMessage, ["debug"]); } // Is this less than 16 characters if (objectParentDriveId.length < 16) { // Debug logging if (debugLogging) {addLogEntry("ONEDRIVE PERSONAL API BUG (Issue #3072): The provided 'driveId' is not 16 characters in length - fetching the correct value from Microsoft Graph API via getDriveIdRoot call", ["debug"]);} // Generate the change oldEntry = objectParentDriveId; string onlineDriveValue; // Fetch the actual online record for this item // This returns the actual OneDrive Personal driveId value based on the input value. // The function 'fetchRealOnlineDriveIdentifier' does not check for length issue, this is done below onlineDriveValue = fetchRealOnlineDriveIdentifier(oldEntry); // Check the onlineDriveValue value for 15 character issue if (!onlineDriveValue.empty) { // Ensure remoteDriveId is 16 characters long by padding with leading zeros if required if (onlineDriveValue.length < 16) { // online value is not 16 characters in length // Debug logging if (debugLogging) {addLogEntry("ONEDRIVE PERSONAL API BUG (Issue #3072): The provided online ['parentReference']['driveId'] value is not 16 Characters in length - padding with leading zero's", ["debug"]);} // Generate the change newEntry = to!string(onlineDriveValue.padLeft('0', 16)); // Explicitly use padLeft for leading zero padding, leave case as-is } else { // Online value is 16 characters in length, use as-is newEntry = onlineDriveValue; } } // Debug Logging of result if (debugLogging) { addLogEntry(" - old 'driveId' value = " ~ oldEntry, ["debug"]); addLogEntry(" - new 'driveId' value = " ~ newEntry, ["debug"]); } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // Return the new calculated value return newEntry; } else { // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // Return input value as-is return objectParentDriveId; } } else { // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // Return input value as-is return objectParentDriveId; } } // Calculate the transfer metrics for the file to aid in performance discussions when they are raised void displayTransferMetrics(string fileTransferred, long transferredBytes, SysTime transferStartTime, SysTime transferEndTime) { // We only calculate this if 'display_transfer_metrics' is enabled or we are doing debug logging if (appConfig.getValueBool("display_transfer_metrics") || debugLogging) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Calculations must be done on files > 0 transferredBytes if (transferredBytes > 0) { // Calculate transfer metrics auto transferDuration = transferEndTime - transferStartTime; double transferDurationAsSeconds = (transferDuration.total!"msecs"/1e3); // msec --> seconds double transferSpeedAsMbps = ((transferredBytes / transferDurationAsSeconds) / 1024 / 1024); // bytes --> Mbps // Output the transfer metrics string transferMetrics = format("File: %s | Size: %d Bytes | Duration: %.2f Seconds | Speed: %.2f Mbps (approx)", fileTransferred, transferredBytes, transferDurationAsSeconds, transferSpeedAsMbps); addLogEntry("Transfer Metrics - " ~ transferMetrics); } else { // Zero bytes - not applicable addLogEntry("Transfer Metrics - N/A (Zero Byte File)"); } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } } } // Recursively validate JSONValue for UTF-8 compliance bool validateUTF8JSON(in JSONValue json) { switch (json.type) { case JSONType.string: return isValidUTF8(json.str); case JSONType.array: foreach (ref item; json.array) { if (!validateUTF8JSON(item)) return false; } break; case JSONType.object: foreach (key, ref value; json.object) { if (!isValidUTF8(key) || !validateUTF8JSON(value)) return false; } break; default: break; // Other types (null, bool, int, float) don't need UTF-8 validation } return true; } // Sanitise the provided onedriveJSONItem into a string that can actually be printed without error or issue string sanitiseJSONItem(JSONValue onedriveJSONItem) { // Function Start Time SysTime functionStartTime; string logKey; string thisFunctionName = format("%s.%s", strip(__MODULE__) , strip(getFunctionName!({}))); // Only set this if we are generating performance processing times if (appConfig.getValueBool("display_processing_time") && debugLogging) { functionStartTime = Clock.currTime(); logKey = generateAlphanumericString(); displayFunctionProcessingStart(thisFunctionName, logKey); } // Eventual output variable string sanitisedJSONString; // Validate UTF-8 before serialisation if (!validateUTF8JSON(onedriveJSONItem)) { return "JSON Validation Failed: JSON data from OneDrive API contains invalid UTF-8 characters"; } // Try and serialise the JSON into a string try { auto app = appender!string(); toJSON(app, onedriveJSONItem); sanitisedJSONString = app.data; } catch (Exception e) { sanitisedJSONString = "JSON Serialisation Failed: " ~ e.msg; } // Display function processing time if configured to do so if (appConfig.getValueBool("display_processing_time") && debugLogging) { // Combine module name & running Function displayFunctionProcessingTime(thisFunctionName, functionStartTime, Clock.currTime(), logKey); } // Return sanitised JSON string for logging output return sanitisedJSONString; } } onedrive-2.5.5/src/util.d000066400000000000000000001637421476564400300152750ustar00rootroot00000000000000// What is this module called? module util; // What does this module require to function? import core.stdc.stdlib: EXIT_SUCCESS, EXIT_FAILURE, exit; import std.base64; import std.conv; import std.digest.crc; import std.digest.sha; import std.net.curl; import std.datetime; import std.file; import std.path; import std.regex; import std.socket; import std.stdio; import std.string; import std.algorithm; import std.uri; import std.json; import std.traits; import std.utf; import core.stdc.stdlib; import core.thread; import core.memory; import std.math; import std.format; import std.random; import std.array; import std.ascii; import std.range; import std.exception; import core.sys.posix.pwd; import core.sys.posix.unistd; import core.stdc.string; import core.sys.posix.signal; import etc.c.curl; import std.process; // What other modules that we have created do we need to import? import log; import config; import qxor; import curlEngine; // Global variable for the device name __gshared string deviceName; // Global flag for SIGINT (CTRL-C) and SIGTERM (kill) state __gshared bool exitHandlerTriggered = false; // util module variable ulong previousRSS; shared static this() { deviceName = Socket.hostName; } // Creates a safe backup of the given item, and only performs the function if not in a --dry-run scenario void safeBackup(const(char)[] path, bool dryRun, bool bypassDataPreservation, out string renamedPath) { // Do we actually perform a safe backup? // Has the user configured to IGNORE local data protection rules? if (bypassDataPreservation) { // The user has configured to ignore data safety checks and overwrite local data rather than preserve & safeBackup addLogEntry("WARNING: Local Data Protection has been disabled - not renaming local file. You may experience data loss on this file: " ~ to!string(path), ["info", "notify"]); } else { // configure variables auto ext = extension(path); auto newPath = path.chomp(ext) ~ "-" ~ deviceName ~ "-safeBackup-"; int n = 1; // Limit to 1000 iterations .. 1000 file backups while (exists(newPath ~ format("%04d", n) ~ ext) && n < 1000) { n++; } // Check if unique file name was found if (exists(newPath ~ format("%04d", n) ~ ext)) { // On the 1000th backup of this file, this should be triggered addLogEntry("Failed to backup " ~ to!string(path) ~ ": Unique file name could not be found after 1000 attempts", ["error"]); return; // Exit function as a unique file name could not be found } // Configure the new name with zero-padded counter newPath ~= format("%04d", n) ~ ext; // Log that we are performing the backup by renaming the file if (verboseLogging) { addLogEntry("The local item is out-of-sync with OneDrive, renaming to preserve existing file and prevent local data loss: " ~ to!string(path) ~ " -> " ~ to!string(newPath), ["verbose"]); } if (!dryRun) { // Not a --dry-run scenario - do the file rename // // There are 2 options to rename a file // rename() - https://dlang.org/library/std/file/rename.html // std.file.copy() - https://dlang.org/library/std/file/copy.html // // rename: // It is not possible to rename a file across different mount points or drives. On POSIX, the operation is atomic. That means, if to already exists there will be no time period during the operation where to is missing. // // std.file.copy // Copy file from to file to. File timestamps are preserved. File attributes are preserved, if preserve equals Yes.preserveAttributes // // Use rename() as Linux is POSIX compliant, we have an atomic operation where at no point in time the 'to' is missing. try { rename(path, newPath); renamedPath = to!string(newPath); } catch (Exception e) { // Handle exceptions, e.g., log error addLogEntry("Renaming of local file failed for " ~ to!string(path) ~ ": " ~ e.msg, ["error"]); } } else { if (debugLogging) { addLogEntry("DRY-RUN: Skipping renaming local file to preserve existing file and prevent data loss: " ~ to!string(path) ~ " -> " ~ to!string(newPath), ["debug"]); } } } } // Rename the given item, and only performs the function if not in a --dry-run scenario void safeRename(const(char)[] oldPath, const(char)[] newPath, bool dryRun) { // Perform the rename if (!dryRun) { if (debugLogging) {addLogEntry("Calling rename(oldPath, newPath)", ["debug"]);} // Use rename() as Linux is POSIX compliant, we have an atomic operation where at no point in time the 'to' is missing. rename(oldPath, newPath); } else { if (debugLogging) {addLogEntry("DRY-RUN: Skipping local file rename", ["debug"]);} } } // Deletes the specified file without throwing an exception if it does not exists void safeRemove(const(char)[] path) { if (exists(path)) remove(path); } // Returns the SHA1 hash hex string of a file string computeSha1Hash(string path) { SHA1 sha; auto file = File(path, "rb"); scope(exit) file.close(); // Ensure file is closed post read foreach (ubyte[] data; chunks(file, 4096)) { sha.put(data); } // Store the hash in a local variable before converting to string auto hashResult = sha.finish(); return toHexString(hashResult).idup; // Convert the hash to a hex string } // Returns the quickXorHash base64 string of a file string computeQuickXorHash(string path) { QuickXor qxor; auto file = File(path, "rb"); scope(exit) file.close(); // Ensure file is closed post read foreach (ubyte[] data; chunks(file, 4096)) { qxor.put(data); } // Store the hash in a local variable before converting to string auto hashResult = qxor.finish(); return Base64.encode(hashResult).idup; // Convert the hash to a base64 string } // Returns the SHA256 hex string of a file string computeSHA256Hash(string path) { SHA256 sha256; auto file = File(path, "rb"); scope(exit) file.close(); // Ensure file is closed post read foreach (ubyte[] data; chunks(file, 4096)) { sha256.put(data); } // Store the hash in a local variable before converting to string auto hashResult = sha256.finish(); return toHexString(hashResult).idup; // Convert the hash to a hex string } // Converts wildcards (*, ?) to regex // The changes here need to be 100% regression tested before full release Regex!char wild2regex(const(char)[] pattern) { string str; str.reserve(pattern.length + 2); str ~= "^"; foreach (c; pattern) { switch (c) { case '*': str ~= ".*"; // Changed to match any character. Was: str ~= "[^/]*"; break; case '.': str ~= "\\."; break; case '?': str ~= "."; // Changed to match any single character. Was: str ~= "[^/]"; break; case '|': str ~= "$|^"; break; case '+': str ~= "\\+"; break; case ' ': str ~= "\\s"; // Changed to match exactly one whitespace. Was: str ~= "\\s+"; break; case '/': str ~= "\\/"; break; case '(': str ~= "\\("; break; case ')': str ~= "\\)"; break; default: str ~= c; break; } } str ~= "$"; return regex(str, "i"); } // Test Internet access to Microsoft OneDrive using a simple HTTP HEAD request bool testInternetReachability(ApplicationConfig appConfig) { HTTP http = HTTP(); http.url = "https://login.microsoftonline.com"; // Configure timeouts based on application configuration http.dnsTimeout = dur!"seconds"(appConfig.getValueLong("dns_timeout")); http.connectTimeout = dur!"seconds"(appConfig.getValueLong("connect_timeout")); http.dataTimeout = dur!"seconds"(appConfig.getValueLong("data_timeout")); http.operationTimeout = dur!"seconds"(appConfig.getValueLong("operation_timeout")); // Set IP protocol version http.handle.set(CurlOption.ipresolve, appConfig.getValueLong("ip_protocol_version")); // Set HTTP method to HEAD for minimal data transfer http.method = HTTP.Method.head; // Exit scope to ensure cleanup scope(exit) { // Shut http down and destroy http.shutdown(); object.destroy(http); // Perform Garbage Collection GC.collect(); // Return free memory to the OS GC.minimize(); } // Execute the request and handle exceptions try { addLogEntry("Attempting to contact Microsoft OneDrive Login Service"); http.perform(); // Check response for HTTP status code if (http.statusLine.code >= 200 && http.statusLine.code < 400) { addLogEntry("Successfully reached Microsoft OneDrive Login Service"); return true; } else { addLogEntry("Failed to reach Microsoft OneDrive Login Service. HTTP status code: " ~ to!string(http.statusLine.code)); return false; } } catch (SocketException e) { addLogEntry("Cannot connect to Microsoft OneDrive Service - Socket Issue: " ~ e.msg); displayOneDriveErrorMessage(e.msg, getFunctionName!({})); return false; } catch (CurlException e) { addLogEntry("Cannot connect to Microsoft OneDrive Service - Network Connection Issue: " ~ e.msg); displayOneDriveErrorMessage(e.msg, getFunctionName!({})); return false; } catch (Exception e) { addLogEntry("Unexpected error occurred: " ~ e.toString()); displayOneDriveErrorMessage(e.toString(), getFunctionName!({})); return false; } } // Retry Internet access test to Microsoft OneDrive bool retryInternetConnectivityTest(ApplicationConfig appConfig) { int retryAttempts = 0; int backoffInterval = 1; // initial backoff interval in seconds int maxBackoffInterval = 3600; // maximum backoff interval in seconds int maxRetryCount = 100; // max retry attempts, reduced for practicality bool isOnline = false; while (retryAttempts < maxRetryCount && !isOnline) { if (backoffInterval < maxBackoffInterval) { backoffInterval = min(backoffInterval * 2, maxBackoffInterval); // exponential increase } if (debugLogging) { addLogEntry(" Retry Attempt: " ~ to!string(retryAttempts + 1), ["debug"]); addLogEntry(" Retry In (seconds): " ~ to!string(backoffInterval), ["debug"]); } Thread.sleep(dur!"seconds"(backoffInterval)); isOnline = testInternetReachability(appConfig); // assuming this function is defined elsewhere if (isOnline) { addLogEntry("Internet connectivity to Microsoft OneDrive service has been restored"); } retryAttempts++; } if (!isOnline) { addLogEntry("ERROR: Was unable to reconnect to the Microsoft OneDrive service after " ~ to!string(maxRetryCount) ~ " attempts!"); } // Return state return isOnline; } // Can we read the local file - as a permissions issue or file corruption will cause a failure // https://github.com/abraunegg/onedrive/issues/113 // returns true if file can be accessed bool readLocalFile(string path) { // What is the file size if (getSize(path) != 0) { try { // Attempt to read up to the first 1 byte of the file auto data = read(path, 1); // Check if the read operation was successful if (data.length != 1) { // Read operation not successful addLogEntry("Failed to read the required amount from the file: " ~ path); return false; } } catch (std.file.FileException e) { // Unable to read the file, log the error message displayFileSystemErrorMessage(e.msg, getFunctionName!({})); return false; } return true; } else { // zero byte files cannot be read, return true return true; } } // Calls globMatch for each string in pattern separated by '|' bool multiGlobMatch(const(char)[] path, const(char)[] pattern) { if (path.length == 0 || pattern.length == 0) { return false; } if (!pattern.canFind('|')) { return globMatch!(std.path.CaseSensitive.yes)(path, pattern); } foreach (glob; pattern.split('|')) { if (globMatch!(std.path.CaseSensitive.yes)(path, glob)) { return true; } } return false; } // Does the path pass the Microsoft restriction and limitations about naming files and folders bool isValidName(string path) { // Restriction and limitations about windows naming files and folders // https://msdn.microsoft.com/en-us/library/aa365247 // https://support.microsoft.com/en-us/help/3125202/restrictions-and-limitations-when-you-sync-files-and-folders if (path == ".") { return true; } string itemName = baseName(path).toLower(); // Ensure case-insensitivity // Check for explicitly disallowed names // https://support.microsoft.com/en-us/office/restrictions-and-limitations-in-onedrive-and-sharepoint-64883a5d-228e-48f5-b3d2-eb39e07630fa?ui=en-us&rs=en-us&ad=us#invalidfilefoldernames string[] disallowedNames = [ ".lock", "desktop.ini", "CON", "PRN", "AUX", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" ]; // Creating an associative array for faster lookup bool[string] disallowedSet; foreach (name; disallowedNames) { disallowedSet[name.toLower()] = true; // Normalise to lowercase } if (disallowedSet.get(itemName, false) || itemName.startsWith("~$") || canFind(itemName, "_vti_")) { return false; } // Regular expression for invalid patterns // https://support.microsoft.com/en-us/office/restrictions-and-limitations-in-onedrive-and-sharepoint-64883a5d-228e-48f5-b3d2-eb39e07630fa?ui=en-us&rs=en-us&ad=us#invalidcharacters // Leading whitespace and trailing whitespace // Invalid characters // Trailing dot '.' (not documented above) , however see issue https://github.com/abraunegg/onedrive/issues/2678 //auto invalidNameReg = ctRegex!(`^\s.*|^.*[\s\.]$|.*[<>:"\|\?*/\\].*`); - original to remove at some point auto invalidNameReg = ctRegex!(`^\s+|\s$|\.$|[<>:"\|\?*/\\]`); // revised 25/3/2024 // - ^\s+ matches one or more whitespace characters at the start of the string. The + ensures we match one or more whitespaces, making it more efficient than .* for detecting leading whitespaces. // - \s$ matches a whitespace character at the end of the string. This is more precise than [\s\.]$ because we'll handle the dot separately. // - \.$ specifically matches a dot character at the end of the string, addressing the requirement to catch trailing dots as invalid. // - [<>:"\|\?*/\\] matches any single instance of the specified invalid characters: ", *, :, <, >, ?, /, \, | auto matchResult = match(itemName, invalidNameReg); if (!matchResult.empty) { return false; } // Determine if the path is at the root level, if yes, check that 'forms' is not the first folder auto segments = pathSplitter(path).array; if (segments.length <= 2 && segments.back.toLower() == "forms") { // Check only the last segment, convert to lower as OneDrive is not POSIX compliant, easier to compare return false; } return true; } // Does the path contain any bad whitespace characters bool containsBadWhiteSpace(string path) { // Check for null or empty string if (path.length == 0) { return false; } // Check for root item if (path == ".") { return false; } // https://github.com/abraunegg/onedrive/issues/35 // Issue #35 presented an interesting issue where the filename contained a newline item // 'State-of-the-art, challenges, and open issues in the integration of Internet of'$'\n''Things and Cloud Computing.pdf' // When the check to see if this file was present the GET request queries as follows: // /v1.0/me/drive/root:/.%2FState-of-the-art%2C%20challenges%2C%20and%20open%20issues%20in%20the%20integration%20of%20Internet%20of%0AThings%20and%20Cloud%20Computing.pdf // The '$'\n'' is translated to %0A which causes the OneDrive query to fail // Check for the presence of '%0A' via regex string itemName = encodeComponent(baseName(path)); // Check for encoded newline character return itemName.indexOf("%0A") != -1; } // Does the path contain any ASCII HTML Codes bool containsASCIIHTMLCodes(string path) { // Check for null or empty string if (path.length == 0) { return false; } // Check for root item if (path == ".") { return false; } // https://github.com/abraunegg/onedrive/issues/151 // If a filename contains ASCII HTML codes, it generates an error when attempting to upload this to Microsoft OneDrive // Check if the filename contains an ASCII HTML code sequence // Check for the pattern &# followed by 1 to 4 digits and a semicolon auto invalidASCIICode = ctRegex!(`&#[0-9]{1,4};`); // Use match to search for ASCII HTML codes in the path auto matchResult = match(path, invalidASCIICode); // Return true if ASCII HTML codes are found return !matchResult.empty; } // Does the path contain any ASCII Control Codes bool containsASCIIControlCodes(string path) { // Check for null or empty string if (path.length == 0) { return false; } // Check for root item if (path == ".") { return false; } // https://github.com/abraunegg/onedrive/discussions/2553#discussioncomment-7995254 // Define a ctRegex pattern for ASCII control codes and specific non-ASCII control characters // This pattern includes the ASCII control range and common non-ASCII control characters // Adjust the pattern as needed to include specific characters of concern auto controlCodePattern = ctRegex!(`[\x00-\x1F\x7F]|\p{Cc}`); // Blocks ƒ†¯~‰ (#2553) , allows α (#2598) // Use match to search for ASCII control codes in the path auto matchResult = match(path, controlCodePattern); // Return true if matchResult is not empty (indicating a control code was found) return !matchResult.empty; } // Is the string a valid UTF-8 timestamp string? bool isValidUTF8Timestamp(string input) { try { // Validate the entire string for UTF-8 correctness validate(input); // Throws UTFException if invalid UTF-8 is found // Validate the input against UTF-8 test cases if (!isValidUTF8(input)) { // error message already printed return false; } // Additional edge-case handling because the input format is known and controlled: // Ensure input length is within the expected range for a UTC datetime if (input.length < 20 || input.length > 30) { // not the correct length addLogEntry("UTF-8 validation failed: Input '" ~ input ~ "' is not within the expected length range for UTC datetime strings (20-30 characters)."); return false; } return true; } catch (UTFException) { addLogEntry("UTF-8 validation failed: Input '" ~ input ~ "' contains invalid UTF-8 characters."); return false; } } // Is the string a valid UTF-8 string? bool isValidUTF8(string input) { try { // Validate the entire string for UTF-8 correctness validate(input); // Throws UTFException if invalid UTF-8 is found // Iterate through each character using byUTF to ensure proper UTF-8 decoding auto it = input.byUTF!(char); foreach (_; it) { // Iterating over the range ensures every UTF-8 sequence in the string is decoded into valid `dchar`s. // Throws a UTFException if an invalid UTF-8 sequence is encountered during decoding. } // Check for replacement characters if (input.count!((dchar c) => c == '\uFFFD') > 0) { // contains replacement character addLogEntry("UTF-8 validation failed: Input contains replacement characters (�)."); return false; } // is the string empty? if (input.empty) { // input is empty addLogEntry("UTF-8 validation failed: Input is empty."); return false; } // return true return true; } catch (UTFException) { addLogEntry("UTF-8 validation failed: Input '" ~ input ~ "' contains invalid UTF-8 characters."); return false; } } // Is the path a valid UTF-16 encoded path? bool isValidUTF16(string path) { // Check for null or empty string if (path.length == 0) { return true; } // Check for root item if (path == ".") { return true; } auto wpath = toUTF16(path); // Convert to UTF-16 encoding auto it = wpath.byCodeUnit; while (!it.empty) { ushort current = it.front; // Check for valid single unit if (current <= 0xD7FF || (current >= 0xE000 && current <= 0xFFFF)) { it.popFront(); } // Check for valid surrogate pair else if (current >= 0xD800 && current <= 0xDBFF) { it.popFront(); if (it.empty || it.front < 0xDC00 || it.front > 0xDFFF) { return false; // Invalid surrogate pair } it.popFront(); } else { return false; // Invalid code unit } } return true; } // Validate that the provided string is a valid date time stamp in UTC format bool isValidUTCDateTime(string dateTimeString) { // Regular expression for validating the string against UTC datetime format // Allows for an optional fractional second part (e.g., .123 or .123456789) auto pattern = regex(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$"); // Validate for UTF-8 first if (!isValidUTF8Timestamp(dateTimeString)) { if (dateTimeString.empty) { // empty string addLogEntry("BAD TIMESTAMP (UTF-8 FAIL): empty string"); } else { // log string that caused UTF-8 failure addLogEntry("BAD TIMESTAMP (UTF-8 FAIL): " ~ dateTimeString); } return false; } // First, check if the string matches the pattern if (!match(dateTimeString, pattern)) { addLogEntry("BAD TIMESTAMP (REGEX FAIL): " ~ dateTimeString); return false; } // Attempt to parse the string into a DateTime object try { auto dt = SysTime.fromISOExtString(dateTimeString); return true; } catch (TimeException) { addLogEntry("BAD TIMESTAMP (CONVERSION FAIL): " ~ dateTimeString); return false; } } // Does the path contain any HTML URL encoded items (e.g., '%20' for space) bool containsURLEncodedItems(string path) { // Check for null or empty string if (path.length == 0) { return false; } // Pattern for percent encoding: % followed by two hexadecimal digits auto urlEncodedPattern = ctRegex!(`%[0-9a-fA-F]{2}`); // Search for URL encoded items in the string auto matchResult = match(path, urlEncodedPattern); // Return true if URL encoded items are found return !matchResult.empty; } // Parse and display error message received from OneDrive void displayOneDriveErrorMessage(string message, string callingFunction) { addLogEntry(); addLogEntry("ERROR: Microsoft OneDrive API returned an error with the following message:"); auto errorArray = splitLines(message); addLogEntry(" Error Message: " ~ to!string(errorArray[0])); // Extract 'message' as the reason JSONValue errorMessage = parseJSON(replace(message, errorArray[0], "")); // What is the reason for the error if (errorMessage.type() == JSONType.object) { // configure the error reason string errorReason; string errorCode; string requestDate; string requestId; string localizedMessage; // set the reason for the error try { // Use error_description as reason errorReason = errorMessage["error_description"].str; } catch (JSONException e) { // we dont want to do anything here } // set the reason for the error try { // Use ["error"]["message"] as reason errorReason = errorMessage["error"]["message"].str; } catch (JSONException e) { // we dont want to do anything here } // Microsoft has started adding 'localizedMessage' to error JSON responses. If this is available, use this try { // Use ["error"]["localizedMessage"] as localised reason localizedMessage = errorMessage["error"]["localizedMessage"].str; } catch (JSONException e) { // we dont want to do anything here if not available } // Display the error reason if (errorReason.startsWith(" 0 ? to!string(errorArray[0]) : "No error message available"; addLogEntry(" Error Message: " ~ errorMessage); // Log the calling function addLogEntry(" Calling Function: " ~ callingFunction); try { // Safely check for disk space ulong localActualFreeSpace = to!ulong(getAvailableDiskSpace(".")); if (localActualFreeSpace == 0) { // Must force exit here, allow logging to be done forceExit(); } } catch (Exception e) { // Handle exceptions from disk space check or type conversion addLogEntry(" Exception in disk space check: " ~ e.msg); } } // Display the POSIX Error Message void displayPosixErrorMessage(string message) { addLogEntry(); // used rather than writeln addLogEntry("ERROR: Microsoft OneDrive API returned data that highlights a POSIX compliance issue:"); addLogEntry(" Error Message: " ~ message); } // Display the Error Message void displayGeneralErrorMessage(Exception e, string callingFunction=__FUNCTION__, int lineno=__LINE__) { addLogEntry(); // used rather than writeln addLogEntry("ERROR: Encountered a " ~ e.classinfo.name ~ ":"); addLogEntry(" Error Message: " ~ e.msg); addLogEntry(" Calling Function: " ~ callingFunction); addLogEntry(" Line number: " ~ to!string(lineno)); } // Get the function name that is being called to assist with identifying where an error is being generated string getFunctionName(alias func)() { return __traits(identifier, __traits(parent, func)) ~ "()\n"; } JSONValue fetchOnlineURLContent(string url) { // Function variables char[] content; JSONValue onlineContent; // Setup HTTP request HTTP http = HTTP(); // Exit scope to ensure cleanup scope(exit) { // Shut http down and destroy http.shutdown(); object.destroy(http); // Perform Garbage Collection GC.collect(); // Return free memory to the OS GC.minimize(); } // Configure the URL to access http.url = url; // HTTP the connection method http.method = HTTP.Method.get; // Data receive handler http.onReceive = (ubyte[] data) { content ~= data; // Append data as it's received return data.length; }; // Perform HTTP request http.perform(); // Parse Content onlineContent = parseJSON(to!string(content)); // Return onlineResponse return onlineContent; } // Get the latest release version from GitHub JSONValue getLatestReleaseDetails() { JSONValue githubLatest; JSONValue versionDetails; string latestTag; string publishedDate; // Query GitHub for the 'latest' release details try { githubLatest = fetchOnlineURLContent("https://api.github.com/repos/abraunegg/onedrive/releases/latest"); } catch (CurlException e) { if (debugLogging) {addLogEntry("CurlException: Unable to query GitHub for latest release - " ~ e.msg, ["debug"]);} } catch (JSONException e) { if (debugLogging) {addLogEntry("JSONException: Unable to parse GitHub JSON response - " ~ e.msg, ["debug"]);} } // githubLatest has to be a valid JSON object if (githubLatest.type() == JSONType.object){ // use the returned tag_name if ("tag_name" in githubLatest) { // use the provided tag // "tag_name": "vA.B.CC" and strip 'v' latestTag = strip(githubLatest["tag_name"].str, "v"); } else { // set to latestTag zeros if (debugLogging) {addLogEntry("'tag_name' unavailable in JSON response. Setting GitHub 'tag_name' release version to 0.0.0", ["debug"]);} latestTag = "0.0.0"; } // use the returned published_at date if ("published_at" in githubLatest) { // use the provided value publishedDate = githubLatest["published_at"].str; } else { // set to v2.0.0 release date if (debugLogging) {addLogEntry("'published_at' unavailable in JSON response. Setting GitHub 'published_at' date to 2018-07-18T18:00:00Z", ["debug"]);} publishedDate = "2018-07-18T18:00:00Z"; } } else { // JSONValue is not an object if (debugLogging) {addLogEntry("Invalid JSON Object response from GitHub. Setting GitHub 'tag_name' release version to 0.0.0", ["debug"]);} latestTag = "0.0.0"; if (debugLogging) {addLogEntry("Invalid JSON Object. Setting GitHub 'published_at' date to 2018-07-18T18:00:00Z", ["debug"]);} publishedDate = "2018-07-18T18:00:00Z"; } // return the latest github version and published date as our own JSON versionDetails = [ "latestTag": JSONValue(latestTag), "publishedDate": JSONValue(publishedDate) ]; // return JSON return versionDetails; } // Get the release details from the 'current' running version JSONValue getCurrentVersionDetails(string thisVersion) { JSONValue githubDetails; JSONValue versionDetails; string versionTag = "v" ~ thisVersion; string publishedDate; // Query GitHub for the release details to match the running version try { githubDetails = fetchOnlineURLContent("https://api.github.com/repos/abraunegg/onedrive/releases"); } catch (CurlException e) { if (debugLogging) {addLogEntry("CurlException: Unable to query GitHub for release details - " ~ e.msg, ["debug"]);} return parseJSON(`{"Error": "CurlException", "message": "` ~ e.msg ~ `"}`); } catch (JSONException e) { if (debugLogging) {addLogEntry("JSONException: Unable to parse GitHub JSON response - " ~ e.msg, ["debug"]);} return parseJSON(`{"Error": "JSONException", "message": "` ~ e.msg ~ `"}`); } // githubDetails has to be a valid JSON array if (githubDetails.type() == JSONType.array){ foreach (searchResult; githubDetails.array) { // searchResult["tag_name"].str; if (searchResult["tag_name"].str == versionTag) { if (debugLogging) { addLogEntry("MATCHED version", ["debug"]); addLogEntry("tag_name: " ~ searchResult["tag_name"].str, ["debug"]); addLogEntry("published_at: " ~ searchResult["published_at"].str, ["debug"]); } publishedDate = searchResult["published_at"].str; } } if (publishedDate.empty) { // empty .. no version match ? // set to v2.0.0 release date if (debugLogging) {addLogEntry("'published_at' unavailable in JSON response. Setting GitHub 'published_at' date to 2018-07-18T18:00:00Z", ["debug"]);} publishedDate = "2018-07-18T18:00:00Z"; } } else { // JSONValue is not an Array if (debugLogging) {addLogEntry("Invalid JSON Array. Setting GitHub 'published_at' date to 2018-07-18T18:00:00Z", ["debug"]);} publishedDate = "2018-07-18T18:00:00Z"; } // return the latest github version and published date as our own JSON versionDetails = [ "versionTag": JSONValue(thisVersion), "publishedDate": JSONValue(publishedDate) ]; // return JSON return versionDetails; } // Check the application version versus GitHub latestTag void checkApplicationVersion() { // Get the latest details from GitHub JSONValue latestVersionDetails = getLatestReleaseDetails(); string latestVersion = latestVersionDetails["latestTag"].str; SysTime publishedDate = SysTime.fromISOExtString(latestVersionDetails["publishedDate"].str).toUTC(); SysTime releaseGracePeriod = publishedDate; SysTime currentTime = Clock.currTime().toUTC(); // drop fraction seconds publishedDate.fracSecs = Duration.zero; currentTime.fracSecs = Duration.zero; releaseGracePeriod.fracSecs = Duration.zero; // roll the grace period forward to allow distributions to catch up based on their release cycles releaseGracePeriod = releaseGracePeriod.add!"months"(1); // what is this clients version? auto currentVersionArray = strip(strip(import("version"), "v")).split("-"); string applicationVersion = currentVersionArray[0]; // debug output if (debugLogging) { addLogEntry("applicationVersion: " ~ applicationVersion, ["debug"]); addLogEntry("latestVersion: " ~ latestVersion, ["debug"]); addLogEntry("publishedDate: " ~ to!string(publishedDate), ["debug"]); addLogEntry("currentTime: " ~ to!string(currentTime), ["debug"]); addLogEntry("releaseGracePeriod: " ~ to!string(releaseGracePeriod), ["debug"]); } // display details if not current // is application version is older than available on GitHub if (applicationVersion != latestVersion) { // application version is different bool displayObsolete = false; // what warning do we present? if (applicationVersion < latestVersion) { // go get this running version details JSONValue thisVersionDetails = getCurrentVersionDetails(applicationVersion); SysTime thisVersionPublishedDate = SysTime.fromISOExtString(thisVersionDetails["publishedDate"].str).toUTC(); thisVersionPublishedDate.fracSecs = Duration.zero; if (debugLogging) {addLogEntry("thisVersionPublishedDate: " ~ to!string(thisVersionPublishedDate), ["debug"]);} // the running version grace period is its release date + 1 month SysTime thisVersionReleaseGracePeriod = thisVersionPublishedDate; thisVersionReleaseGracePeriod = thisVersionReleaseGracePeriod.add!"months"(1); if (debugLogging) {addLogEntry("thisVersionReleaseGracePeriod: " ~ to!string(thisVersionReleaseGracePeriod), ["debug"]);} // Is this running version obsolete ? if (!displayObsolete) { // if releaseGracePeriod > currentTime // display an information warning that there is a new release available if (releaseGracePeriod.toUnixTime() > currentTime.toUnixTime()) { // inside release grace period ... set flag to false displayObsolete = false; } else { // outside grace period displayObsolete = true; } } // display version response addLogEntry(); if (!displayObsolete) { // display the new version is available message addLogEntry("INFO: A new onedrive client version is available. Please upgrade your client version when possible.", ["info", "notify"]); } else { // display the obsolete message addLogEntry("WARNING: Your onedrive client version is now obsolete and unsupported. Please upgrade your client version.", ["info", "notify"]); } addLogEntry("Current Application Version: " ~ applicationVersion); addLogEntry("Version Available: " ~ latestVersion); addLogEntry(); } } } bool hasId(JSONValue item) { return ("id" in item) != null; } bool hasMimeType(const ref JSONValue item) { return ("mimeType" in item["file"]) != null; } bool hasQuota(JSONValue item) { return ("quota" in item) != null; } bool hasQuotaState(JSONValue item) { return ("state" in item["quota"]) != null; } bool isItemDeleted(JSONValue item) { return ("deleted" in item) != null; } bool isItemRoot(JSONValue item) { return ("root" in item) != null; } bool hasParentReference(const ref JSONValue item) { return ("parentReference" in item) != null; } bool hasParentReferenceDriveId(JSONValue item) { return ("driveId" in item["parentReference"]) != null; } bool hasParentReferenceId(JSONValue item) { return ("id" in item["parentReference"]) != null; } bool hasParentReferencePath(JSONValue item) { return ("path" in item["parentReference"]) != null; } bool isFolderItem(const ref JSONValue item) { return ("folder" in item) != null; } bool isRemoteFolderItem(const ref JSONValue item) { if (isItemRemote(item)) { return ("folder" in item["remoteItem"]) != null; } else { return false; } } bool isFileItem(const ref JSONValue item) { return ("file" in item) != null; } bool isItemRemote(const ref JSONValue item) { return ("remoteItem" in item) != null; } bool isItemFile(const ref JSONValue item) { return ("file" in item) != null; } bool isItemFolder(const ref JSONValue item) { return ("folder" in item) != null; } bool hasFileSize(const ref JSONValue item) { return ("size" in item) != null; } // Function to determine if the final component of the provided path is a .file or .folder bool isDotFile(const(string) path) { // Check for null or empty path if (path is null || path.length == 0) { return false; } // Special case for root if (path == ".") { return false; } // Extract the last component of the path auto paths = pathSplitter(buildNormalizedPath(path)); // Optimised way to fetch the last component string lastComponent = paths.empty ? "" : paths.back; // Check if the last component starts with a dot return startsWith(lastComponent, "."); } bool isMalware(const ref JSONValue item) { return ("malware" in item) != null; } bool hasHashes(const ref JSONValue item) { return ("hashes" in item["file"]) != null; } bool hasZeroHashes(const ref JSONValue item) { // Check if "hashes" exists under "file" and is empty if ("hashes" in item["file"]) { auto hashes = item["file"]["hashes"]; if (hashes.type == JSONType.object && hashes.object.keys.length == 0) { return true; } } return false; } bool hasQuickXorHash(const ref JSONValue item) { return ("quickXorHash" in item["file"]["hashes"]) != null; } bool hasSHA256Hash(const ref JSONValue item) { return ("sha256Hash" in item["file"]["hashes"]) != null; } bool isMicrosoftOneNoteMimeType1(const ref JSONValue item) { return (item["file"]["mimeType"].str) == "application/msonenote"; } bool isMicrosoftOneNoteMimeType2(const ref JSONValue item) { return (item["file"]["mimeType"].str) == "application/octet-stream"; } bool hasUploadURL(const ref JSONValue item) { return ("uploadUrl" in item) != null; } bool hasNextExpectedRanges(const ref JSONValue item) { return ("nextExpectedRanges" in item) != null; } bool hasLocalPath(const ref JSONValue item) { return ("localPath" in item) != null; } bool hasETag(const ref JSONValue item) { return ("eTag" in item) != null; } bool hasSharedElement(const ref JSONValue item) { return ("shared" in item) != null; } bool hasName(const ref JSONValue item) { return ("name" in item) != null; } bool hasCreatedBy(const ref JSONValue item) { return ("createdBy" in item) != null; } bool hasCreatedByUser(const ref JSONValue item) { return ("user" in item["createdBy"]) != null; } bool hasCreatedByUserDisplayName(const ref JSONValue item) { if (hasCreatedBy(item)) { if (hasCreatedByUser(item)) { return ("displayName" in item["createdBy"]["user"]) != null; } else { return false; } } else { return false; } } bool hasLastModifiedBy(const ref JSONValue item) { return ("lastModifiedBy" in item) != null; } bool hasLastModifiedByUser(const ref JSONValue item) { return ("user" in item["lastModifiedBy"]) != null; } bool hasLastModifiedByUserDisplayName(const ref JSONValue item) { if (hasLastModifiedBy(item)) { if (hasLastModifiedByUser(item)) { return ("displayName" in item["lastModifiedBy"]["user"]) != null; } else { return false; } } else { return false; } } // Convert bytes to GB string byteToGibiByte(ulong bytes) { if (bytes == 0) { return "0.00"; // or handle the zero case as needed } double gib = bytes / 1073741824.0; // 1024^3 for direct conversion return format("%.2f", gib); // Format to ensure two decimal places } // Test if entrypoint.sh exists on the root filesystem bool entrypointExists(string basePath = "/") { try { // Build the path to the entrypoint.sh file string entrypointPath = buildNormalizedPath(buildPath(basePath, "entrypoint.sh")); // Check if the path exists and return the result return exists(entrypointPath); } catch (Exception e) { // Handle any exceptions (e.g., permission issues, invalid path) addLogEntry("An error occurred: " ~ e.msg); return false; } } // Generate a random alphanumeric string with specified length string generateAlphanumericString(size_t length = 16) { // Ensure length is not zero if (length == 0) { throw new Exception("Length must be greater than 0"); } auto asciiLetters = to!(dchar[])(letters); auto asciiDigits = to!(dchar[])(digits); dchar[] randomString; randomString.length = length; // Create a random number generator auto rndGen = Random(unpredictableSeed); // Fill the string with random alphanumeric characters fill(randomString[], randomCover(chain(asciiLetters, asciiDigits), rndGen)); return to!string(randomString); } // Display internal memory stats pre garbage collection void displayMemoryUsagePreGC() { // Display memory usage addLogEntry(); addLogEntry("Memory Usage PRE Garbage Collection (KB)"); addLogEntry("-----------------------------------------------------"); writeMemoryStats(); addLogEntry(); } // Display internal memory stats post garbage collection + RSS (actual memory being used) void displayMemoryUsagePostGC() { // Display memory usage title addLogEntry("Memory Usage POST Garbage Collection (KB)"); addLogEntry("-----------------------------------------------------"); writeMemoryStats(); // Assuming this function logs memory stats correctly // Query the actual Resident Set Size (RSS) for the PID pid_t pid = getCurrentPID(); ulong rss = getRSS(pid); // Check and log the previous RSS value if (previousRSS != 0) { addLogEntry("previous Resident Set Size (RSS) = " ~ to!string(previousRSS) ~ " KB"); // Calculate and log the difference in RSS long difference = rss - previousRSS; // 'difference' can be negative, use 'long' to handle it string sign = difference > 0 ? "+" : (difference < 0 ? "" : ""); // Determine the sign for display, no sign for zero addLogEntry("difference in Resident Set Size (RSS) = " ~ sign ~ to!string(difference) ~ " KB"); } // Update previous RSS with the new value previousRSS = rss; // Closeout addLogEntry(); } // Write internal memory stats void writeMemoryStats() { addLogEntry("current memory usedSize = " ~ to!string((GC.stats.usedSize/1024))); // number of used bytes on the GC heap (might only get updated after a collection) addLogEntry("current memory freeSize = " ~ to!string((GC.stats.freeSize/1024))); // number of free bytes on the GC heap (might only get updated after a collection) addLogEntry("current memory allocatedInCurrentThread = " ~ to!string((GC.stats.allocatedInCurrentThread/1024))); // number of bytes allocated for current thread since program start // Query the actual Resident Set Size (RSS) for the PID pid_t pid = getCurrentPID(); ulong rss = getRSS(pid); // The RSS includes all memory that is currently marked as occupied by the process. // Over time, the heap can become fragmented. Even after garbage collection, fragmented memory blocks may not be contiguous enough to be returned to the OS, leading to an increase in the reported memory usage despite having free space. // This includes memory that might not be actively used but has not been returned to the system. // The GC.minimize() function can sometimes cause an increase in RSS due to how memory pages are managed and freed. addLogEntry("current Resident Set Size (RSS) = " ~ to!string(rss) ~ " KB"); // actual memory in RAM used by the process at this point in time } // Return the username of the UID running the 'onedrive' process string getUserName() { // Retrieve the UID of the current user auto uid = getuid(); // Retrieve password file entry for the user auto pw = getpwuid(uid); enforce(pw !is null, "Failed to retrieve user information for UID: " ~ to!string(uid)); // Extract username and convert to immutable string string userName = to!string(fromStringz(pw.pw_name)); // Log User identifiers from process if (debugLogging) { addLogEntry("Process ID: " ~ to!string(pw), ["debug"]); addLogEntry("User UID: " ~ to!string(pw.pw_uid), ["debug"]); addLogEntry("User GID: " ~ to!string(pw.pw_gid), ["debug"]); } // Check if username is valid if (!userName.empty) { if (debugLogging) {addLogEntry("User Name: " ~ userName, ["debug"]);} return userName; } else { // Log and return unknown user if (debugLogging) {addLogEntry("User Name: unknown", ["debug"]);} return "unknown"; } } // Calculate the ETA for when a 'large file' will be completed (upload & download operations) int calc_eta(size_t counter, size_t iterations, ulong start_time) { if (counter == 0) { return 0; // Avoid division by zero } double ratio = cast(double) counter / iterations; auto current_time = Clock.currTime.toUnixTime(); ulong duration = (current_time - start_time); // Segments left to download auto segments_remaining = (iterations > counter) ? (iterations - counter) : 0; // Calculate the average time per iteration so far double avg_time_per_iteration = cast(double) duration / counter; // Debug output for the ETA calculation if (debugLogging) { addLogEntry("counter: " ~ to!string(counter), ["debug"]); addLogEntry("iterations: " ~ to!string(iterations), ["debug"]); addLogEntry("segments_remaining: " ~ to!string(segments_remaining), ["debug"]); addLogEntry("ratio: " ~ format("%.2f", ratio), ["debug"]); addLogEntry("start_time: " ~ to!string(start_time), ["debug"]); addLogEntry("current_time: " ~ to!string(current_time), ["debug"]); addLogEntry("duration: " ~ to!string(duration), ["debug"]); addLogEntry("avg_time_per_iteration: " ~ format("%.2f", avg_time_per_iteration), ["debug"]); } // Return the ETA or duration if (counter != iterations) { auto eta_sec = avg_time_per_iteration * segments_remaining; // ETA Debug if (debugLogging) { addLogEntry("eta_sec: " ~ to!string(eta_sec), ["debug"]); addLogEntry("estimated_total_time: " ~ to!string(avg_time_per_iteration * iterations), ["debug"]); } // Return ETA return eta_sec > 0 ? cast(int) ceil(eta_sec) : 0; } else { // Return the average time per iteration for the last iteration return cast(int) ceil(avg_time_per_iteration); } } // Force Exit due to failure void forceExit() { // Allow any logging complete before we force exit Thread.sleep(dur!("msecs")(500)); // Shutdown logging, which also flushes all logging buffers shutdownLogging(); // Setup signal handling for the exit scope setupExitScopeSignalHandler(); // Force Exit exit(EXIT_FAILURE); } // Get the current PID of the application pid_t getCurrentPID() { // The '/proc/self' is a symlink to the current process's proc directory string path = "/proc/self/stat"; // Read the content of the stat file string content; try { content = readText(path); } catch (Exception e) { writeln("Failed to read stat file: ", e.msg); return 0; } // The first value in the stat file is the PID auto parts = split(content); return to!pid_t(parts[0]); // Convert the first part to pid_t } // Access the Resident Set Size (RSS) based on the PID of the running application ulong getRSS(pid_t pid) { // Construct the path to the statm file for the given PID string path = format("/proc/%s/statm", to!string(pid)); // Read the content of the file string content; try { content = readText(path); } catch (Exception e) { writeln("Failed to read statm file: ", e.msg); return 0; } // Split the content and get the RSS (second value) auto stats = split(content); if (stats.length < 2) { writeln("Unexpected format in statm file."); return 0; } // RSS is in pages, convert it to kilobytes ulong rssPages = to!ulong(stats[1]); ulong rssKilobytes = rssPages * sysconf(_SC_PAGESIZE) / 1024; return rssKilobytes; } // Getting around the @nogc problem // https://p0nce.github.io/d-idioms/#Bypassing-@nogc auto assumeNoGC(T) (T t) if (isFunctionPointer!T || isDelegate!T) { enum attrs = functionAttributes!T | FunctionAttribute.nogc; return cast(SetFunctionAttributes!(T, functionLinkage!T, attrs)) t; } // When using exit scopes, set up this to catch any undesirable signal void setupExitScopeSignalHandler() { sigaction_t action; action.sa_handler = &exitScopeSignalHandler; // Direct function pointer assignment sigemptyset(&action.sa_mask); // Initialize the signal set to empty action.sa_flags = 0; sigaction(SIGSEGV, &action, null); // Invalid Memory Access signal } // Catch any SIGSEV generated by the exit scopes extern(C) nothrow @nogc @system void exitScopeSignalHandler(int signo) { if (signo == SIGSEGV) { assumeNoGC ( () { // Caught a SIGSEGV but everything was shutdown cleanly ..... //printf("Caught a SIGSEGV but everything was shutdown cleanly .....\n"); exit(0); })(); } } // Return the compiler details string compilerDetails() { version(DigitalMars) enum compiler = "DMD"; else version(LDC) enum compiler = "LDC"; else version(GNU) enum compiler = "GDC"; else enum compiler = "Unknown compiler"; string compilerString = compiler ~ " " ~ to!string(__VERSION__); return compilerString; } // Return the curl version details string getCurlVersionString() { // Get curl version auto versionInfo = curl_version(); return to!string(versionInfo); } // Function to return the decoded curl version as a string string getCurlVersionNumeric() { // Get curl version info using curl_version_info auto curlVersionDetails = curl_version_info(CURLVERSION_NOW); // Extract the major, minor, and patch numbers from version_num uint versionNum = curlVersionDetails.version_num; // The version number is in the format 0xXXYYZZ uint major = (versionNum >> 16) & 0xFF; // Extract XX (major version) uint minor = (versionNum >> 8) & 0xFF; // Extract YY (minor version) uint patch = versionNum & 0xFF; // Extract ZZ (patch version) // Return the version in the format "major.minor.patch" return major.to!string ~ "." ~ minor.to!string ~ "." ~ patch.to!string; } // Test the curl version against known curl versions with HTTP/2 issues bool isBadCurlVersion(string curlVersion) { // List of known curl versions with HTTP/2 issues string[] supportedVersions = [ "7.68.0", // Ubuntu 20.x "7.74.0", // Debian 11 "7.81.0", // Ubuntu 22.x "7.88.1", // Debian 12 "8.2.1", // Ubuntu 23.10 "8.5.0", // Ubuntu 24.x "8.10.0" // Various - HTTP/2 bug which was fixed in 8.10.1 ]; // Check if the current version matches one of the supported versions return canFind(supportedVersions, curlVersion); } string getOpenSSLVersion() { try { // Execute 'openssl version' and capture the output auto result = executeShell("openssl version"); // Strip any extraneous whitespace from the output return result.output.strip(); } catch (Exception e) { // Handle any exceptions, possibly returning an error message return "Error fetching OpenSSL version: " ~ e.msg; } } void checkOpenSSLVersion() { // Get OpenSSL version string auto versionString = getOpenSSLVersion(); if (versionString.startsWith("Error")) { addLogEntry(versionString); // Must force exit here, allow logging to be done forceExit(); } // Define regex to extract version parts auto versionRegex = regex(r"OpenSSL\s(\d+)\.(\d+)\.(\d+)([a-z]?)"); auto matches = versionString.match(versionRegex); if (matches.empty) { if (!versionString.empty) { if (debugLogging) {addLogEntry("Unable to parse provided OpenSSL version: " ~ versionString, ["debug"]);} } } else { // Extract major, minor, patch, and optional letter parts uint major = matches.captures[1].to!uint; uint minor = matches.captures[2].to!uint; uint patch = matches.captures[3].to!uint; string letter = matches.captures[4]; // Empty if version is 3.x.x or higher string distributionWarning = " Please report this to your distribution, requesting an update to a newer OpenSSL version, or consider upgrading it yourself for optimal stability."; // Compare versions if (major < 1 || (major == 1 && minor < 1) || (major == 1 && minor == 1 && patch < 1) || (major == 1 && minor == 1 && patch == 1 && (letter.empty || letter[0] < 'a'))) { addLogEntry(); addLogEntry(format("WARNING: Your OpenSSL version (%d.%d.%d%s) is below the minimum required version of 1.1.1a. Significant operational issues are likely when using this client.", major, minor, patch, letter), ["info", "notify"]); addLogEntry(distributionWarning); addLogEntry(); } else if (major == 1 && minor == 1 && patch == 1 && !letter.empty && letter[0] >= 'a' && letter[0] <= 'w') { addLogEntry(); addLogEntry(format("WARNING: Your OpenSSL version (%d.%d.%d%s) may cause stability issues with this client.", major, minor, patch, letter), ["info", "notify"]); addLogEntry(distributionWarning); addLogEntry(); } else if (major >= 3) { // Do nothing for version >= 3.0.0 } } } // Set the timestamp of the provided path to ensure this is done in a consistent manner void setPathTimestamp(bool dryRun, string inputPath, SysTime newTimeStamp) { // Try and set the local path timestamp, catch filesystem error try { // Set the correct time on the requested inputPath if (!dryRun) { if (debugLogging) { addLogEntry("Setting 'lastAccessTime' and 'lastModificationTime' properties for: " ~ inputPath ~ " to " ~ to!string(newTimeStamp), ["debug"]); } // Make the timestamp change for the path provided try { // Function detailed here: https://dlang.org/library/std/file/set_times.html // setTimes(path, accessTime, modificationTime) // We use the provided 'newTimeStamp' to set both: // accessTime Time the file/folder was last accessed. // modificationTime Time the file/folder was last modified. if (debugLogging) {addLogEntry("Calling setTimes() for the given path", ["debug"]);} setTimes(inputPath, newTimeStamp, newTimeStamp); if (debugLogging) {addLogEntry("Timestamp updated for this path: " ~ inputPath, ["debug"]);} } catch (FileException e) { // display the error message displayFileSystemErrorMessage(e.msg, getFunctionName!({})); } } } catch (FileException e) { // display the error message displayFileSystemErrorMessage(e.msg, getFunctionName!({})); } } // Generate the initial function processing time log entry void displayFunctionProcessingStart(string functionName, string logKey) { // Output the function processing header addLogEntry(format("[%s] Application Function '%s' Started", strip(logKey), strip(functionName))); } // Calculate the time taken to perform the application Function void displayFunctionProcessingTime(string functionName, SysTime functionStartTime, SysTime functionEndTime, string logKey) { // Calculate processing time auto functionDuration = functionEndTime - functionStartTime; double functionDurationAsSeconds = (functionDuration.total!"msecs"/1e3); // msec --> seconds // Output the function processing time string processingTime = format("[%s] Application Function '%s' Processing Time = %.4f Seconds", strip(logKey), strip(functionName), functionDurationAsSeconds); addLogEntry(processingTime); }onedrive-2.5.5/src/webhook.d000066400000000000000000000212771476564400300157520ustar00rootroot00000000000000module webhook; // What does this module require to function? import core.atomic : atomicOp; import std.datetime; import std.concurrency; import std.json; // What other modules that we have created do we need to import? import arsd.cgi; import config; import onedrive; import log; import util; class OneDriveWebhook { private RequestServer server; private string host; private ushort port; private Tid parentTid; private bool started; private ApplicationConfig appConfig; private OneDriveApi oneDriveApiInstance; string subscriptionId = ""; SysTime subscriptionExpiration, subscriptionLastErrorAt; Duration subscriptionExpirationInterval, subscriptionRenewalInterval, subscriptionRetryInterval; string notificationUrl = ""; private uint count; this(Tid parentTid, ApplicationConfig appConfig) { this.host = appConfig.getValueString("webhook_listening_host"); this.port = to!ushort(appConfig.getValueLong("webhook_listening_port")); this.parentTid = parentTid; this.appConfig = appConfig; subscriptionExpiration = Clock.currTime(UTC()); subscriptionLastErrorAt = SysTime.fromUnixTime(0); subscriptionExpirationInterval = dur!"seconds"(appConfig.getValueLong("webhook_expiration_interval")); subscriptionRenewalInterval = dur!"seconds"(appConfig.getValueLong("webhook_renewal_interval")); subscriptionRetryInterval = dur!"seconds"(appConfig.getValueLong("webhook_retry_interval")); notificationUrl = appConfig.getValueString("webhook_public_url"); } // The static serve() is necessary because spawn() does not like instance methods void serve() { if (this.started) { return; } this.started = true; this.count = 0; server.listeningHost = this.host; server.listeningPort = this.port; spawn(&serveImpl, cast(shared) this); addLogEntry("Started OneDrive API Webhook server"); // Subscriptions oneDriveApiInstance = new OneDriveApi(this.appConfig); oneDriveApiInstance.initialise(); createOrRenewSubscription(); } void stop() { if (!this.started) return; server.stop(); this.started = false; addLogEntry("Stopped OneDrive API Webhook server"); object.destroy(server); // Delete subscription if there exists any try { deleteSubscription(); } catch (OneDriveException e) { logSubscriptionError(e); } // Release API instance back to the pool oneDriveApiInstance.releaseCurlEngine(); object.destroy(oneDriveApiInstance); oneDriveApiInstance = null; } private static void handle(shared OneDriveWebhook _this, Cgi cgi) { if (debugHTTPSResponse) { addLogEntry("Webhook request: " ~ to!string(cgi.requestMethod) ~ " " ~ to!string(cgi.requestUri)); if (!cgi.postBody.empty) { addLogEntry("Webhook post body: " ~ to!string(cgi.postBody)); } } cgi.setResponseContentType("text/plain"); if ("validationToken" in cgi.get) { // For validation requests, respond with the validation token passed in the query string // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/concepts/webhook-receiver-validation-request cgi.write(cgi.get["validationToken"]); addLogEntry("OneDrive API Webhook: handled validation request"); } else { // Notifications don't include any information about the changes that triggered them. // Put a refresh signal in the queue and let the main monitor loop process it. // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/concepts/using-webhooks _this.count.atomicOp!"+="(1); send(cast()_this.parentTid, to!ulong(_this.count)); cgi.write("OK"); addLogEntry("OneDrive API Webhook: sent refresh signal #" ~ to!string(_this.count)); } } private static void serveImpl(shared OneDriveWebhook _this) { _this.server.serveEmbeddedHttp!(handle, OneDriveWebhook)(_this); } // Create a new subscription or renew the existing subscription void createOrRenewSubscription() { auto elapsed = Clock.currTime(UTC()) - subscriptionLastErrorAt; if (elapsed < subscriptionRetryInterval) { return; } try { if (!hasValidSubscription()) { createSubscription(); } else if (isSubscriptionUpForRenewal()) { renewSubscription(); } } catch (OneDriveException e) { logSubscriptionError(e); subscriptionLastErrorAt = Clock.currTime(UTC()); addLogEntry("Will retry creating or renewing subscription in " ~ to!string(subscriptionRetryInterval)); } catch (JSONException e) { addLogEntry("ERROR: Unexpected JSON error when attempting to validate subscription: " ~ e.msg); subscriptionLastErrorAt = Clock.currTime(UTC()); addLogEntry("Will retry creating or renewing subscription in " ~ to!string(subscriptionRetryInterval)); } } // Return the duration to next subscriptionExpiration check Duration getNextExpirationCheckDuration() { SysTime now = Clock.currTime(UTC()); if (hasValidSubscription()) { Duration elapsed = Clock.currTime(UTC()) - subscriptionLastErrorAt; // Check if we are waiting for the next retry if (elapsed < subscriptionRetryInterval) return subscriptionRetryInterval - elapsed; else return subscriptionExpiration - now - subscriptionRenewalInterval; } else return subscriptionRetryInterval; } private bool hasValidSubscription() { return !subscriptionId.empty && subscriptionExpiration > Clock.currTime(UTC()); } private bool isSubscriptionUpForRenewal() { return subscriptionExpiration < Clock.currTime(UTC()) + subscriptionRenewalInterval; } private void createSubscription() { addLogEntry("Initialising webhook subscription for updates ..."); auto expirationDateTime = Clock.currTime(UTC()) + subscriptionExpirationInterval; try { JSONValue response = oneDriveApiInstance.createSubscription(notificationUrl, expirationDateTime); // Save important subscription metadata including id and expiration subscriptionId = response["id"].str; subscriptionExpiration = SysTime.fromISOExtString(response["expirationDateTime"].str); addLogEntry("Created new subscription " ~ subscriptionId ~ " with expiration: " ~ to!string(subscriptionExpiration.toISOExtString())); } catch (OneDriveException e) { if (e.httpStatusCode == 409) { // Take over an existing subscription on HTTP 409. // // Sample 409 error: // { // "error": { // "code": "ObjectIdentifierInUse", // "innerError": { // "client-request-id": "615af209-467a-4ab7-8eff-27c1d1efbc2d", // "date": "2023-09-26T09:27:45", // "request-id": "615af209-467a-4ab7-8eff-27c1d1efbc2d" // }, // "message": "Subscription Id c0bba80e-57a3-43a7-bac2-e6f525a76e7c already exists for the requested combination" // } // } // Make sure the error code is "ObjectIdentifierInUse" try { if (e.error["error"]["code"].str != "ObjectIdentifierInUse") { throw e; } } catch (JSONException jsonEx) { throw e; } // Extract the existing subscription id from the error message import std.regex; auto idReg = ctRegex!(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", "i"); auto m = matchFirst(e.error["error"]["message"].str, idReg); if (!m) { throw e; } // Save the subscription id and renew it immediately since we don't know the expiration timestamp subscriptionId = m[0]; addLogEntry("Found existing webhook subscription " ~ subscriptionId); renewSubscription(); } else { throw e; } } } private void renewSubscription() { addLogEntry("Renewing webhook subscription for updates ..."); auto expirationDateTime = Clock.currTime(UTC()) + subscriptionExpirationInterval; try { JSONValue response = oneDriveApiInstance.renewSubscription(subscriptionId, expirationDateTime); // Update subscription expiration from the response subscriptionExpiration = SysTime.fromISOExtString(response["expirationDateTime"].str); addLogEntry("Renewed webhook subscription " ~ subscriptionId ~ " with expiration: " ~ to!string(subscriptionExpiration.toISOExtString())); } catch (OneDriveException e) { if (e.httpStatusCode == 404) { addLogEntry("The subscription is not found on the server. Recreating subscription ..."); subscriptionId = null; subscriptionExpiration = Clock.currTime(UTC()); createSubscription(); } else { throw e; } } } private void deleteSubscription() { if (!hasValidSubscription()) { return; } oneDriveApiInstance.deleteSubscription(subscriptionId); addLogEntry("Deleted subscription"); } private void logSubscriptionError(OneDriveException e) { // Log a message to the GUI only addLogEntry("ERROR: An issue has occurred with webhook subscriptions: " ~ e.error["error"]["message"].str, ["notify"]); // Use the standard OneDrive API logging method displayOneDriveErrorMessage(e.msg, getFunctionName!({})); } }onedrive-2.5.5/src/xattr.d000066400000000000000000000034621476564400300154520ustar00rootroot00000000000000module xattr; import core.sys.posix.sys.types; import core.stdc.errno; import core.stdc.stdlib; import core.stdc.string; import core.stdc.stdio; import std.string; import std.conv; extern (C) { int setxattr(const(char)* path, const(char)* name, const(void)* value, size_t size, int flags); ssize_t getxattr(const(char)* path, const(char)* name, void* value, size_t size); } class XAttrException : Exception { this(string message) { super(message); } } // Sets an extended attribute for a given file. // Throws `XAttrException` on failure. void setXAttr(string filePath, string attrName, string attrValue) { int result = setxattr(filePath.toStringz(), attrName.toStringz(), cast(const(void)*)attrValue.ptr, attrValue.length, 0); if (result != 0) { throw new XAttrException("Failed to set xattr '" ~ attrName ~ "' on '" ~ filePath ~ "': " ~ to!string(strerror(errno))); } } // Retrieves an extended attribute value from a file. // Returns the attribute value as a string. // Throws `XAttrException` if the attribute cannot be read. string getXAttr(string filePath, string attrName) { // First, determine the required buffer size ssize_t size = getxattr(filePath.toStringz(), attrName.toStringz(), null, 0); if (size == -1) { throw new XAttrException("Failed to determine xattr size for '" ~ attrName ~ "' on '" ~ filePath ~ "': " ~ to!string(strerror(errno))); } // Allocate buffer char[] buffer = new char[size]; // Read the attribute value ssize_t result = getxattr(filePath.toStringz(), attrName.toStringz(), cast(void*)buffer.ptr, buffer.length); if (result == -1) { throw new XAttrException("Failed to read xattr '" ~ attrName ~ "' from '" ~ filePath ~ "': " ~ to!string(strerror(errno))); } return buffer[0 .. result].idup; } onedrive-2.5.5/tests/000077500000000000000000000000001476564400300145115ustar00rootroot00000000000000onedrive-2.5.5/tests/bad-file-name.tar.xz000066400000000000000000017202501476564400300202510ustar00rootroot000000000000007zXZִF!t/B](KHg-}~EI>CJ!"8*uBP`L¤Wlkmzm]-"k-{eRwZyQ68Ҁ.|:(]pAB׭]·凂 taVD҂6A|-$’`'smvWځQWp΁{ 2,˦S`,| ]`ssSδZ$iF~ݗ"PE#]GrTlKv}-QJZVqV7 gלŢ Vji 5x}H}Q '㼰3ā,R%{ΐ8e%3OcŶƼfldXwt~䊘(>X6ЉV70c\Q;3P6mK|{y#uP"f@3y?F€ITHw֗P.q/f4Wp|SP6O#X'h(4fG9g!L=a` PFcŒp#cD5xy J'݉DoW86J# ]TmgK8唱hƻW޹8VBRqۓD) lCm\ؽ Tt 'PÎٵmj%H .* tG[Aq}'sbEp7*^4T&6͡cwӿ4V%Ų1#WT~"59O^a^:5{6W.@>l/['Ȣ{]2-\uHcMO#/]FᄊPcΧj9mUm;[rZ px'/ 4#@@N?'ž[SG/z犀b^$s拓BwGHyLLY ͍9Moa֯/F-)ZѬɜ8Y_2m0Lk=H(i䞩a'Fc4=vԗcyޭE PTc 5r {YJo~O$q^&./*G|<A.ɸGfĐ`} z3(T,WB=G M{źhyGulto׶?>mkXZ1YpH8~bsUH\acge ۊM: OF!E(8!/ w"GuM"Qnoߩr=}AB;;ZMgiY\+7,[f{ $g((U͍}J"K (:+aS=18+e}+1݁;0lU d[WS`m =Rp|_9v4h(U9$[w'7}bBC%;9A _ ˩|.oȲtvAokݟ:! S5f':qR=2'r"|`ToArauJ,{c}(!h]vCVI׋ĢV:%mXYe?%y+ƎlCmsK".ZS"v7'c> #[Mlx` a}(^D!tN 2¾Cƒ#zC$wf^̐Fkt<Ň$D\t|@i&kgg_wp\a:Z΢gs08"Z)KJy>ȴk퇰 0N7.S}T7rh'egu5o^oT%1G>{e!x5M) ^+| fk!, HX#eKGKr7`R "ęmJ~B2 {ΟF~ImG岬6e !KAA.WFj"*4 D"4~|Kipr'h,5B*ᶩcd*iTޏB.aX[1.w6 iHFE3%U p99)tm[qa~pٔLJG 8䏓MKs!e$OP=X/xQX[P hJ^{eyy2`ԟ]kNn.U`;;K2ēd_jIz 0H>/UGu  Md!-;?9LtVPwU \>X ]f `%@QB͢QFR}/`iZLB'FÖj1Z:8&mɷL:ڗPP%9U{V{HCe'K,$(%ݛ_/n_]ڃRC{^/)B-z+3\ᯪYH_6/%=.ؠ0WcHJpBS8(%(Z!EAa PB9kW1d3jY+vD'5nXCM-l|C;DzĴahyxyqqxeg' tcx{:oH\q]I& ݗ(# aFMlͧnthj;ZQ.p%Xqd{j<v17T Lbg2Ūd:UkXQ4;ŴV.<;wJ@;})):A2a6%uR]h4qX x jNK2G2U3yq/ܴyX }pѱ ck=K9) ]dp8_oH_"{"TnC` =S0ug *.6_*ajB6;x6#APn-:~6poCp?úh=8O 2qNO3iF&Y=2 S39ݸnh|L$L*ڡpϣ^p)>705u\YYHGz]Ya P"(O"p\(f2-J4/\.<, uKMs7h¹t)޳~*64:#?Ԣ8l&I w}ɢD_g4'K!QVQߘ2VUK0+;2vΒ|'mnqMd$]2\.ÝO;CkV<ײP.m[2nCS\֯[l3l M'hQm؅ZYiJ_,+9_8ڹvDrtv)@P~wlA0jTLdsjgt.&'oT9SM:tI *3Cǁo l>K{=:V~KAP/9ą xk?ɵ?83Y؊>"]@ܠJ AjQ|S\@(m E0*)gq" \7w/]y._0z{3~C;4+$@7fկ5OH1|5f{ tj v(E\1֦r @"~=, >Fl)Pc L<|AZ4QJf aj՞`BtFe/^_8DkVV"8|WNIʄ&oa`9Q;&.ٲV=H6?H7Uvv$9+bQOLÜ̼FA؎kCn98ei$*5Jc3[vjxڬR#$;ͣxMw;ُW{'ǴbY ,QQH1TmG)uH>&1NnV^9\zNJ4ZuM$ſdPfL3,8$)cf2F@J60 6}@Ll¡J&^s>SJi"ܷez.۝JqqL\uXvGCNas^Ob]s=Hݨ ,K ~<%Km1FbyM@"BSb104DvpIҋ+1\ҐEl⳰}istzznѹT^bCN}S] CC$RϷn%X/"vg;p)ث, GHf0t$ /RI4'0,IQSj+ PhDS hFP!E.PGl37]9-@Ҟ?gM Or7s<xgGKp\̅%9Q "V|~ZSrtYPH3`𢃹1 rаF2&TtcQގF"]Γ7ى3߸ ;Us=cs5H&˸tpX| hqobC6i@Hh5AҨն-Ec~iExy1lyᠣr˗N)1>V;&ݺKk#ӖsNMiX")i89y{; !"OɀJ͂s:#_LmNԍn I +~e?7b f$ɓwk!8!te!^hhd*F);J'NAdc̓:ѧ&?v |JVa7OդTӪVjAcr"Y0`Kis@(#yS^,! =&kpbM+a7T2#1$&t)AI=S~9 ͉CJO:>Eʀ貕i$'"z)4=L *04r I F".TPwT:Czª" L*=l<~Bs~!&E6{f9k<^ ˚o /Ns2ćZByRjDmN:m[.+.. hF9NDTNr5! #}9Qkt)I) VBAa/nfaՔQz9;rł1I mwlFiܰ2h qz}}oK(k/YhwV:Sg m+qڜHyYۃ3{%@prI&|gtw~ְL)?0MuMv<gk^˕}}[d¹FG`CKayplU*u诛 mVÄ(J kCq2geCMP}s?g{zXNSRVjsR"|ɏ w U|T%a֟ȳ+B36UVa%Tre]:p'r'Ƚ0r06fXVBoO&V4`%/Nh2SkQaceGqԪE@SV!sF%sDG91a+kxq=nHbGk&8''hnW Lؗ1ȕXS5`~@|1I#IB@}{ gɑn^Z"ϭə.=,v\*g"@QO!:كšňCLm&j?UQo LjbIsxRj722칼c0i&'rUe?XYoxʉӲ?pXY݁߈&gelȹ3xLKn! ZA\oGW /m_ӍL S@ϵdiV TGߓ.gKvV@z&50,k6vyB7yocH2}Տ9 =U_!rs녶LYTਨ.$4 2 8J}p:%硰z> |QlZSo/ \ao^\DKNˤMr[xEP~?@?|׼ui5zg]ʳ,RExJ 0'֗rX1`\mKJforwy9۞MV[SIÃ00nIөO࡙kg}w?c/N|q~#CcAՌ("E5XlqAd*UІDH{kYQLL8R\F66)Ϯ,Y_ᖘZ.8%JPMfjS;ќ\hh`ƭk|&*h8J. Nxsy"$rhzs/:{c kؚ|:` 1wBx&0_X Gd/, Qd7f(d)I4h5g!7dF; ]@BlJr1G<{` `fCU`zш/cO =^oI)Q=QӰ ( M'َ_s~c,Ia)dH_8A/32i~A8Ic\ip$Jh%ND#Dާ"9F4B}oVBtsJMUL%̩(\Xk;O'`(W:1r U sq4p;(8<Fà5LGdsyTlGx #0-Z0H?\ɭRj|DwuR}?SCAz_[ %QȅL<҅:~OY'hcwO'7yԝw~A~0]"Ѩg LiOC$Ƞ;c'%trRޫǒ 8҇K]gtRT[q^|zaLOq6p^~:"٣ / $rѦ(r1?-Kգn}WX4B<\䚣MYxs8ZY'őHZGɻcm/N]h:RDz664ԄS7m&/cST_7FZŕZڙԻLvK%JjHKgh3 cQèM] % nIV&',VB/3~Ԍ:SeA3Fx766>>$mC.mLZwd|Լ5E,ynv4@gȨca8C~FW*.B߹^*9W֤2yMg_{zT&VLۻ+|.k ` Dm o Lx?2-[_L2ɛ7pMA8׶o{L턎#`eܬ7Zg n=ENl)PAkSK_ SǻˡxFGAo] upiVlKBЁgvͮevrkr&)@:tb+T" $ 䌩_*+ؖFd };jrnS].`;>IP8{Gyȿ[`3Y;$a/ [uOGi؉ 3g 9]R ù@)\=Yt *}I(jbWrӳoѻUo:N>VM } c;gxu2kDv,1g4Z܏yGaleP*8!؇ȐPo6]%Yob< 5^WCX*@L0G7)=uOBp FDK|G)7"$z؇D% JC1&"PLh4_[ mvǤ֙;&=Nt+Jnc`0'Wi,T!5)VQ7)DoTED\JnV|ssGLm> IP64]F~Gxih+\ĭt^N#QoX<@GCoV)1 }5*bNI]h&f*Yї,aSHOf叹ݶ0^JYנ`H=KhRYv1ObAܣ XahqL:dr ۧMc@~A}ߚSFT܎g cD_5%0~yI$yopɸw8p"(S@tLwM] Gs HzS"vG1C=FIė1\λ :+H]YL4 (Jej(Ud&7xJ0qh&t[{ʀq5h9bxA쐔q4vnvn@xzYՅA2~16giT>$~Ԯo!O&W{ n,R #L8~hk[$L&m/(ʘWx'4>#9[23n'|-~ S{W3Q1M )lnE5Ϣ[W^>岌)f` N 0a-K[l 0S_>V$G!+״%vxn\,{:F۾LIU%!ncz3+t8Oڽ%1\l;o1rVLCvBNk%(lQ׷d:mKF;cwB.:eUt,` LvT`n.͇ $VvRrK} $ߋϨI32ٗ (owt0YX8?rCI [U4کMr]U;Ju~\󥃂?!EZ9v:f/8XUwJP+糇HaTH.VCB+=/F!5nŷ؀^,+(O>ajUsG ®%1Y.lS9=rރ c*+>fKۨ-tؓ B,ϗ*C;aAQPַ $}j.vEtpNX2U'Ґ*ۍ,ء)h5|j}!}tm a{;k1p4QXe!^J䀦AOn/r%&[PHΎ}]BtgU1I` /mV} Bxʅoq#Tbp)i^ A3nTʞ!Cϒ9ҢAgСM,/Q0s06=Z#}a]8%* Pq; ~վ.Ђ[MRs5êMLŭ?%`9/X[cV7zҏtqYPPާJ /~`)7Og Z)2t)n+Q, =I&t3nа+t둴x-n9T/׸(5DplȌ%>ԋz8A>ڔr8j_tsNcT\ &{">t<RwrY'RRn&LZd-j:fTvRub<L/xI}>!ZU6>ou5CH)9? {V]"5, :3ʢ ypP0@;Y98>#=f$iǾC5P ^+$7lk7ix4 8(Oy.ґd=NI4/ÎβvBbG}6f^Bqf¨*',h-fE8NUe\K,y-;݋ U6̓CՌ \׺>3pr91-qS\O83-ҟ@_KUS,?-+$xӉOƂ[[5!DReZ2^=RҸ H5,*m"GB4 謿m=u2X=߼?ήN㼳J)9iI䗍N/wD,3i22I# Xei^zK #!y 6z9H6DR K.9eLy'+\28`t"Okdlp׷tl1eeuBzxbW|2]5b3>2"\?B U~2%vKA;_㽰Ne8nz<ƅ 0SY2sSLnHHh Z/Og"Ԫ ]ozIޡ_.j_Tx:xx!@竏_.2ScN=bU^MAHHeJ]dnNMZn U'?pj+,EߗCS9)ߴ䰬GCӒMl<]xNLⰦ qc~ٕ? ; Wl<ִM؋31%Xy[–aSv ^npa2`Ӯ[w xӜI7sTq\w@ő[Źcq% +`A{ /YJРd"`f3;!x|o32;U1˗ :tQ{Fe@8c^kqlmLkirehE$ on=L\&BepA:eDiN6`}-CQ΂y*=B't+L4i#l~@i|LⱔhV'a,.^Ra94KfsU\Am t cJkC.aTUE47(  ;\]b3i qJɨJ{dy[e”ph4_a0^S1-n4&\QI9XuU'|0^+˱L5Wq<*)nwx/B.J17$F7-P,%q?{S}r6*uml`>xUxnț/ _Ta/;f\mqq$\* Pjw=z;7dQ<*$ p/oSEzFMoL`\-rt# io6mL(V Oal.mtdgJZ'Z$tS `{mYmPj!.!cW1s <@ ydaP\ sx÷ԭ&tjdBma€!c"+ō:dloˀH90+@_HNctM0n.t0U:bX(Qi,ݶ(N.s~ 6Frc'W#SsoO,F} ս%'*ͿNJ-V]+R HXE)Lo}o8Cb;!$Ze ͳ7mU^'s0k]o> )gd>.7S|n:s֓_ۿgM# jfY I8E+7,;9. :qZY9G3;Ieiub9CB,=&~,!rT%QS KR?E=QkJ:5puhɵ ɉ˄e6}̺q v&_W`2i+ZO˩EjA$ wH8 f{X4ʖ։=Mcj\\,YRPY:"I3*l#" 2Yu ,BUw>*5NhLLmj}?9CQ~+HYDp0-ʨ zvY 5ЂS$PPu hÜ ^)42{Y?f؄;KDBԢQ71M.RG$Gg=I/V9eu]8)nBibJYHwn\]ڌF3 PhPmݙNXP/Jn<0=0Z^>7ho"n?)~SC\ُ+<11M/<+iyNf.(Sهܝ,S+J&$%N_GNi<7}*Oه`RW+Zx~EPA9_L_͓j}iX=Kgޕ,m[o !h:3Y ) \^00[+7Sz˔Rk_yڋi 8⿪J |#]h]fƒWP2s T12xn VXe~]B؀8\D: U`l0A,h? PC2Iʍ#vǺ8{&#zͬbfv .mm8xNE#K b6)oaRf̛sx ׈%ߤ 0dw+*92􂳽i}\\f /)Pt%x)Gn/Z@k6[5}~-څ~+mHgLx͑(-=44_s^8 tǪ/IOhٲG {MV+M_Av;4M&_rϟ-%6V^Ǜ|0(&1qB~NZ_Y]37"J*aGe ^k54uIŐPH1H$; ẓn?A][jkB֦Ú%ȡQ]{Ie%,Z k4fuu{Pq26Ga !ـIx9Q֨H~鳼4Ɓ*")a:c@ew5#txCv?9Q-͉y~*0,z.ldb/6cG_d/ckXF&(^oǛJ3q/ܝ݊SGFSTPc@t*rIU1тJgz'GHO nDJd:=&,G*@Yi,# 3{ 4=)Uuь 6eH|ײn:h/<72ͱߡO/!4zË,2i?g IvPa))hsLH Sxԟ%dkں@bC̸!ED|kl dthi-~!rA&Q[FbVq I>xs # E vi#@ˣxr\5|v,9 bq`O[Zdr<(S5%NR-;@=^=YQ|L^8ohuOrr {{FSX$x#4`ta 5t| { .!c+z?Nd)h3m%u֓ ^;Ƙӑ7 DT0!͹}0m-w\ߏ>J9lt kKJCвe>$ajA:cc n^#/h$/Pcj8<eQGW@2>4EX7hn 0imVnfrs1;C\g1/ "0Fbg4~^ԵPlm;b-wIr9}Yjs'7Hm=ړd`Qe_1>qqrך|6\RQQՓG2,88[ͮ<_ 'dh5hF5[}͛l_N fX rWaNڗ(=>f/3Uގ^E"(Rӏo5K_:'ya"IQ¯k7;JWc4 2`62qhH]{վ6!"vT!sO1+O` B9sKӐҬ8IwfDEp] }oܖG" d$ ѷY\y~knbmٳ㙽p8 W+M,-isy=!+)E N2i *|y u0nj:w evv?{.=!}_N4XNg!Սb6k[9ͦƻm;bk7?嚮d߀P.ŸUR USb n9nCGQ#J3 tTZm}G*:rh6J*TGOcl9%qP.2"ok-em#.?gN%|Hv0(g#DdqU#W>[˒'B+A ߚY ]be"F<}L6BLA(_ea5T~g43"5^RVChqbbU4xn7޾`Ђ^Sb;Թ)`3"24Ufpy8Q"|rlK8%KhX)J#D3pTFGޤn!+ٿߠ;9ӭC%zfhKxk/w2¾oۯubY]1Z? |A0C}˰\_`*>;{v&][;G= (A!.559z>Vz5?cv-_D1H8K^_&KX9eE-! NE\Ke/#J8:;{!)*:aW8cH*ץ{"cM@%N8Im81brD9~u0F+ =9 J5;ͺAc]guE)?),rTdiP7~8z d-w6o;{QD{ÃSmu!а!dF2r{zv[d 8\y^@s/kWm|G؅u7=_< goy`dф^s9S\v&P߻alܼ ۈ@O4 ȸeɏL÷Ҁv` 8qpш" ]ж il2p,Ulʕ͗"Brdc&.- MlΕq}XHS;z0 &HUF~x#H&"@0b#hXPLJY/-2H3ɊӴJ62!+~# U4`Vfr!DvI$|` P~|42gKѥ.ѡii؜n2-S#'˗Avsi{SՌZ1r2 S8F+h? Z8⧢_cEL.&}5UIV}h^n[E{nsyqvZ(ml n%xaFcN5nJ ÙAd?1u3+UC{= /o?o&mLɄ7vU,}_mʇZ0d?ul1Z6&J4Efk' op#=M+oǡǡvw&E}j"#ȼвte= ޲[Jw#`:zmߜHC/H=HLqN棞JLk40>,{F{[Nk;|qVw. y*@iRET _ \l޴-is4c; 1Pm֌Cʫ+gRv\:"HOTKuS9fB L^B1wh/R4)x?yӄr=p 2 !T$_yޖj Iot_4]C$IUptuQmCxmu; sjAL1۫/RHLw |P99qG:cج8:(3UQ ^͆O}_pS) ջc@MxJUƶڀ<;ԅF#:ZS(8-Jf*ZN/TjgTu6nC+o-$L8kr"DTvWRG!&28QOuW(/td\;UX;+bׅj'NqofE$RA 0tH['ҔjPs|rU?[dŢj0#yGAx|n<җv5T#ٿ["LQrWu0xrl./9Z[6RT[PJ3o Y#FlCDM %lt֗J-"Hz{.F+2wx^ .:RB`] |;ev (Iˏ `^71QdV]K@ĿdyCA/ z\\9}>2Ӌ&Ȫ"=ʛuN]͡_"vc,bEOn/FrYH Bf!%v:]۞ xXRo"5ߍ+W,t@R [nOgH{!Y8(<Эao}_| k/MxߌbBk T7dXj,ausQY)FXz42UMSqƄJK'ʺTvw9}R&ܬ/Y;;dVD6W{#j8"ĹV~LݔZ9NpV#۝**̎&ĺβOÓʒ#@ڽ,mTpt7tk'OjӮ 6 UNEJ֊ *PkCEi#,HTPN qK 3 qu²NՍ> n/O!o]rC@fCuȈikZѝ.Qa[/0HΈZ|QO4Ň}|k%v݀3f~txV;_\dEf3blX:Vm;)TM_!05Q?FODNf-K6;/sF,9VrU~<'mp |en!6U o=DZ8y'o8TE$y6KmJA{y7@!,{m#ꘛ7R/Z>Z.5"i^2Ce8/ͼE<.ĠF$< ~s7ZfYLzxmY내flHW88!.4VNT;ؠ€)GRN2eS #*F=Lj姦W4 K۵c4,h^Aj TX J 7SvR8q~OI`K^tؗ6Jp$0@9IOm'/Q=![LGg|69DUR5>܉E&G^gGj@pWާ_g~&/6m,ZbVQNj3:@ˬaWJf D7ZFʵLǶ㔥1DX(t姐DfQ8D cFE96`4MW_^7pI)(:İcY FlZdK_lr ƑDƳaL Vwqs)V Nޓg0֟QH[O@gBSng+K%q;(HJLm}nভ\,X<)5VwzT[U}mޭB8!g=@Y'X iB_9}5 + ae՘:mj!@(F5<iR&ƅ.Y'>ױǰ'#dx7bz;ibMG`7oZ#S-(wo\5 *$#D݋mv`u ? ֏Ƴth@-7x^NGa8z0:0.z MN:`):B31qI-OJGw|Q@xvR0ƾI".\~e_Y {~UX)EA v%PgWR;k9J X2qpA]o镘J]-r̞MVPWw#Or7iUjs.R'~3rF908*Ӡąomq:z3jIiɇٷ`0y4X"\,Nx4`1koM>5Rԣv=)Q3V#Y i4w1iv-uB_8!]Q&)04BX%oŽS{eNj .b/_[f+"w#@c'?,qfda!*aT F E ?'m;IxIskx: 1 \-8CdSj=%#2,#f#{knKo|j h/i,K{pͱV 'EN{L]pТM>;,0T1qdz3u,F;~on'ƞr[Uj\cUu7OA l;na2#T&)c&Hj&[ %X4}۾e1 Cl bI.sۙoQXZul{^ܱ88KR)+3hݕ^> |E,\F[+Z2]#/0pxUYS(PZY.lR` x\D 1g̏; юLpT>hPySl6bڶL0 $=Xq؎"}N㣢::Ϗj7,7jLw'yB?C aT8Kgwq2 SZj) rhtrkga:{&VUVR&~Qţ=ndQLZȾB#yW˥+9wO)Ė{b\h%8\Cڐ^ی(!> =@< ҳFP84Jo9̪Al6wgv1V3 3%w O"ld 5@zXHI(m04OwǨF0oWx煒֩{M2 ~S? ݵEk_0+7X -*[/j,*xRP}&OPq~y N38IQu\BKrJ8" 0viwOq2%Z㜒 S"< ]cV""6]@c> 3`7w }9՘/ovia~hV^dZtjU>.lxU.7|J%ƒnўZŀ|B];eѹkQSAdpu.> ]w*8\mQj=3U{K8iRK 'LGwV2myytVFaqIe\V x_P#C_:NK:+{ݹܠjJqxȌS}0Mwm;YU%I6n¹ǫ5JqʫmɳZ͢Td)H!%,z8[/@kV7~$jmԫDZMw^;ޡ__FO, Kw: QHdĤ}2Ӽ-أ5rSf^B>k{RPᆹch)U1qtؼ*!IIz]hlh.jl [^JMzBL4­nؤߴZ52K%g{GvvQqH1,:'K%ZQ+THg}`vLaNš}?'jR5DunNjěN" 'H$ղ<;=#6z[$i4Yw((4/:`E|Y4)OB{iYۓ5qtֱ7tyHGpoۂ3W0ѩ*'X7i'tMON3s 8LS_<ɗf 1jR.^lزAI*˃@vqIHeYzR+"*RUN{Bb ',xRAҍBO NFW 8S2 9eV]@g\دKpI ;\am%FDfk@cx/.m}0*`Oy*'?4Fu>:Usdu0+7>ZQ,lmtQ(,Kh߬n(0q8u +PJu@*bTYk~?ax l%Z Q=f26T(<>h|7m>`Vki(2u'/ծ+ޯQ]}OQ:ct~00H8e1E B49}PPcXR&m;Hjؽ.?Q`>+@!M Inhf_Y=yZqJ l";IV3SnNRzlVc+]cQ%߲,HG3'hVWQ -֒VBSnQ` e1Qp8>j\|c;+H/p8#]HPzDK;J;;=p8:?_ZE%XCy"CDJ%zztg.2ZYxf`X~ ^Ÿc>POԍx9C ?^O"tKA|-Bt|jl?[1#YB/$N0Udw㊌NbeuXXJA%erGzsUqNK6ϝrH; Y7Us#,Q [])alA3nCtMj9, VeJu{%J| &ΰJҬymm,[63}=Ҥ¾DȓUokkyHS!;,B CC>-=27 YT-dY4v{Oi%:#x2K75o(b'U E.4v1ƝGsͧkx utPo"'Zf+;2HYM`X;\K~NRL=mY1#QMGD?Г :BDJ2 z2pcw1P)(!+y'gUFҔkbZQ4no0B% "\-A)<}Ϗu9sJ"R :MЯ6b(yEо=3@#ѱF+;6-]!_*{+룭g.ܩMQ27R!fAwLSm'aIhuq MwJ|͍ Tm[څ[9~A.wYdSY &u}r,4vEB. * Ym,fj/j&+n6ۭ#ݥX"67K<_*֟}+kH~gIvK:Aa#5y8'V˨q¡ ls8SlkٷUj}!!,a0 |[YTu%,;|NĤ~3哂5C h`Uk]NoAd@*@=^l:AN#Y}¨dpEKV.=JV`ZsV80ɺO՝7y`gq[ͣs⣋;$D8kWym#`~ ˥+x`.C*LHRQ@}ļ\يbxDBbQgx_J8ɟϥՈ*a V/m+S׺uap}h׾Ǹߏ&1~'u*-+&exL,oAqcT3A af"Pl(ёRn\6:Jm76@F~&֧UM]@`evTMyW҆զ{_# |)0,*6^,Lf1 zY`oUKiߜr=ŮX}t}g`s.w+hs$q:'5wқZ:ue|NAsWbsPR8wDFmfw,Tq:uw.?sQNHwȊSi݀Lun7:T1[)Pz>^9\+Gwյ04,1-}+'uyMP߂W)AB?ɚ ƾZT(ǎ6ƊϷ=smscO{ /QjrXJۑyw54^i70iл,lp&3xazg4f$7=itpukrq#e(D!?&< Lo:00* f*Y|k*ժ?HtL ~Ky ݸh&䕈 !oa5"S_ 8g{ ySv"1I;hcA7`hb= wMEOE7M% CǦίy-ⒿDrX)|Wl&Ta< ;qG RE=ٜ%g5=%G֨PNopshLPJ"F G+?rlum?X>:>EiBb{,"n^Ň0GУL<Yۓ.R˱fz?GA<+qՄE ;=&X Ȓ~RIvI{ޭ mNbnLqcGJ98z߿ ˆQ&XؘRdz ߭9"-6C2 nP:$ R,^{k=JM: zKa?Y>EnCTm +kADlrKcógp:'iZ:PY_7`,KAOLmQĬM5<rYdҟ+[hgp3W"V 6f--aA<@tlUQg_%σ!a!ZciFp=FÝ\=m#v fǔx?mHt~oCFq[HD' \b㓕X!i }n._ B@Z׌pXdb|~,ְ`o:ڬ>ÙIC~j Z:<'P>͎맳 4 ݿ6ӛzMt:ZݶxR9f< RyrtC/)S'9KAq/sTfc.VX BIP`|F,ڲ[z:HsipOg<G:R&p@ם/hḇk V8c gVs(FPU&攑Thq,$Vo+vNi)jpKeрRYG7k'饤z+C*4^:%RSq;5I R'5N[rw>SzlO(5֍G`n*9 BU #FMكΊD57:Exhy?=LZĚ{_Ԗ/x{0%Y%21ࠕ.Yڧ۝76C3kaRv tCH4k0 [a =+ NrNGCC*'Q8@uaQ6ohԧlzy*lĢ=-sM4S@"f!aR*=Z@;9p@%  %q;9)mc4 krwXhZH= !=vc֭гm0pz /.0?M)dͥZG `! Fӛ|vWȁ*N  "_DO95ȓ^C jrEfUvRoSۊնǥ^9X&D J:UT/.q@fd<8r4c(`Nٽ9I95 =+-6z8㙒%4YROB[>AcB8L˅K[s$gKqOeYYdȬtPW`Z4uJ5{R4(ƬQ\D n@._ؼ%4۶}ޕ= $#⦯Wo ~rA+){ˑw}Ҥ\Ȯڇs4Qӭv"3Ph6 Rv;7\( 'N_Y-#7+P {ʭ9<-V31Fll3kL9ʀdX#[vi."(T!- #LV&pJ#/ڲ?*ZŘh9.zKlsbyW#8+7H\VǾlx)|IC)pY@` F.qg9[]v FMa lsU#^5;qsYR!ߺ|RY*EcM>9ъDPLУE\-ԥsiÕpȲG*YMg*9OdA1@<׏VzBt+EၗJm1 <3U4Qt\`}xEtT|גihEtny\@z5d-QPʽ[$czOt~ :Eӽ Ɛo$8d_:Ju8Ѹ$5} Տ𪋜ȡݏ2Jv`uh ` Vvc5 [ݮ)عTCi~Ws^9dC5W3Lrҟe9kSG!ǐ!==*JVAݾ.CLئrKX/j(|+0v@XѼZP! )8uǹ{ir'5GCV\ʆ!`mRwս]O^a?xΙ!@<6^J,!`;]!LG"tAg={G,TZF4HI88ű[tBpu?X)dxq])S.Ȉ1Eyn !ص{Oxqf/L.nDmZRّrem%9A7U# '+yZ|N[ L~՜U<& Q,Ӷ` YțS^0t;"15󃄞@ICU.]d:?NM6̱]'ՏDfd"WLYau?OS2=*Djt=kvNpH_P|iX()R%{GaBuq˭ʔ|_#X=!wUv1p!9ky36]˝ /t%/ki' 'ȇG2Yh!H $ |"ږt?c,*bA-=G S+ o{ZWpBW8=i{/qmn2p.IvuW2.Dr $'J =ff卜S/P{t/q?oLVVy0PEm.mCwނ]m3:|E3rEQ;0.eNک_鯃_4c8dpe?jE"I - !&\iAUh໤YL7|2!TIa; V(nwscj#nz/PэZPw8< :T:=DZ{{hh {y ™ŻjGTRu̖17=|92)Le-4غFG s{ӨFؕfR+Cu'*/oi t[ J$58xUE2ϫ_$au=V!\AMH1Tw1iNjYt85CRzhđϐqe&%+JIZzVN U@\dg %n#aDZ&d31iis:1XϾ@szI~ݠaSrnTS]>Ve(LS%zXd1seL D{Pŭf['L *l4`2T ct'nˉsJR~wƞ[܉-Lᮚ(o~N[y_Ё8=\N#ݦ1)sN'WIv? @zHO+/WY;R vjD7}#<&En|20H`hVHS/u( e$ C2ҥɻeP,%GZEQ\)fLB(nxmd6ҴG5h T:'%a"rVas14ݯ_]{/J o45rM}7 v|D IP]Hov\ie;NFP) F,?687.̺|fv]B.A]6<]ߧ+e^.҉{ &Įކұ!UBlpOqȧb9] )EA=l2]X*%ܐo?Dֳ7烺)%2 'Tt"[Xp(X?aR{JbVg>BW }q҇Otbױ Xr)Z!<w݀%̫RYctAB^@$4uzqB/#@+)ه݀ D/efnN讔eL7EƟ55^wF8 *h#s>t<~5g3Y41 Yzuܕ`rwӺDunD^ QsF4$/ S4"/\?ڷu="ltg|G^sv/GUAnḷ'*|8#z1y6^58*B5ǫ̑_[rK/dGl%YiD {aibdKj;YsGa32뱱WMD-UTƉ@O䆐~D^!%gUs# D^_< kae&򉓰>ÿk@Ȟ| |>I,:ɧ\giS:XZ{BXx☢~2ˋM˥:o8V|8%Bh_{O񌉒b{/YcˊHIvgV̮xU؜,|B\Jʌ.g]J,5z.;qN? A勩;ێ6Wv IbmK8.dE$eJ;Z(˚=CbX<_CG[=x6, fcy<@^NC2 m~Z˽`&fX,PZ dfab|YoRs=,bkKao!o:-|EXGEV޽$y \afw7=m>9&Kl=@CX7K(j2F j-`ͅo!1wGO\tI"xh\F@R>[*@f u&s-^FKFdXu>kr,8Ǩh%e:CbgRJKe`Ե68_Wb)XL\3kyGyBO+L3ҿ͔Ok>-v^|ܹ+Hy'D$l46@M5w3S 9`ymx\O;OZ:?&JωFRpT7\Gξ,SK~J=tM /솶DÛhHk;q5QEaYDWQQMLIw!°7wK팷rњ9iu;݁bOrY=v$E9c}nvB \_\A?_,z܆OI9yjʃ0k˂jrEve?gU`?mȐ J:|[ QٝߘWقbnF_?"]iMiȴehFB}Ld0\zX@@q$ u%^ L+mZFy7&Ii Կf}Рtr&_7Nqw27/˴SFOM~6B"Oxķf.ZZ:=avn2H)fpȉrT=od3ZW}ב4RuC.Hr_oB0L#(\Lh9/1X.dfB*s$^+'6ޏ~ib5vT[֢ͅo\kc1wK [r潃XKmX2dIF1O 0B g|-axsI&>~i\Bh$Ū={ɂ\,gnLX=2q\Uho{zsSb'9) *␁O۹2UMeBh [/l1V<e k1'vsLd1t`$(b ;mg4&z>"Q˂o _KmV!>(yI9yQ2+<7W*P綳@kWhޢҎ}ua?Wqv4 [BW"a894/!Mzqc/f' s^bNc*ou_t2-4W塂QBKnMll3RL0i>0]` ]X72l2Lq'n/#u3_Tm#l*- 9*nx)K;kimD0zYqMe!|JC͇~(ܳBs9?nƘ/ ܺN&։|8^SzPm[ Z0rbw$@lܬM4' eBS~AcS:.EdT!}n }7y{"V&Ѓ)nhfjfI`.U}OUU 4kOqn+>g!U/o 9So%Z*n~(jZz(iaH-;iMɭ3^>2fUI^qv=M==VE\) r eu>3v~08J#u۷t"AKZQi;BbTLˬ<̐Q neuʈŕAitb#L RNS>Wص:T=Je;0ÒJIʎ⫋>~x 2*gžN< 5;YяzXXYiFTO)Ӗp}~q:MRSHGTwZؽvbjԧн N1ԒI@/Zv &⛣f %yNp]v:a|wO/ɇjC?9*FRcDER v#j2ԋîeDH"+TשFW Fۆ2Eǯ9 n$gtV%D4-MsZ%T N`4eK \&/叇-xL\{}%a I,_k8LBKޘ%>ַzyKсdf1 }fzs)Eᑾ/.b J铮_>h\28MV'|i|cXGhyL0&8;R;QLʣ=@JȪw6?,~& h5yf:͈wXwkqsn*%[/]G_9)w#;a=-MڟB`P)Mzi \G|xQ7OA-+n' ״G7GOA:cB+R^4[=[ݕj2#huzF^uy=TA%NQ3'_cME3\f$Wg"΍AFtZǜz0p`֜kJRf*e,d1lH 16 \W붃|RYq Lt.e r%4Vl kf2y7Er >̮z j2E* /nd+{p7o@s<}L!"lo+A-NZ!aL6l;?SaUe?N8MBLe[aP-֋`B%ː*-Ct!BSfrd %H&̣Ů+EۇH/sևܞS3\3^py~dv7$ "(8͓KpGB79Q977d[]kZQiDfn釄DA@Aؓwg57do~bx,Y0F$&&$YxM$!通q>0tWh7WZXgS`_Mōlt^wE,-eo. [)ɨJ I,&=qb6+L 1:'ܓn rSM? 7'uvgX+iIgLEVcv%slZrXM5_m-Ŀ?}DEftXMƸZV3F֪E,[EtkvLć =,LVnǽN X)LV!)"ŴǼ&dЮ:x$ŕzPŏ d5Gŕ>dBb&J S*74|v+ %dO_ѳoҟzW8vf7,DVfIPz$=4 āLK[Ѹw]`elk3u.8tp1 Yͨ OGppå9>\=yL`^n_9;ךZPv)PGE혢3ga(N ^-lNTҎ*\|Q%jPq[gX) -HVꮷL+C`Yoq&h2_"EJ#>.hYc|{̫K͡pLX ~jNYERp7 TvϤ^l>ĤKl@jo{h%QHDD->d+ѵ!/#, 1])LrC˿t==~  N򝅂@H}Pi@\C /)t9 HFݴo?&B`Q 3#KGݶB}Dd$̐O5y]S?^mA؄V}2Wۿ^-UT3xG@ס7+t*|ܤ ڋ˳8 ^>@ }_mΈOb+cWwͳܮ|ڇ2Ko~/H>O[xWUTaQS]>E2)n~gdEj0hJڬN|kozO'?1u6cK?<4(c^OTf@?n.RMOw@7iB!!r>5VCcioW,{,4c( ` 驫'vU!(fi;?ǥvj'3,=U,V y'Pk,=v Zn/߯Y = 6 7vƮ0e58J=~̄9N-5mыaVUvi>u/$}xr&~$OǏ=$4dvf3,M}ˍ"196 `l}WVw:$@!JX\x f.X_ĪA-X a@D8@hi'9 01ߕ1P[ \ghR2q1W*:S_ty(+z o, ixD|*8%?ArHFڇ~2E59*4yծ(&S%8@^Пd-&Ciۙk (/fhZ֫'c#Yz@Кyڵj+xzEBVqT$~E;lԸޔ*MT>GEXV(rx\fĄ?/HC[S,;=d4-'cu;?,N7ę:o8k畕?@xE :8`'C庸~)|D]ήf0K|BcGWs7 C쁌-~!3f]{@%;rPHIrGqi&:dhԷyR_.c{k)bc||bҘ UoT!nƔGg| W@ 9 RIWe2WI$|Kgצ6b"B ]و:?j\ zNa'dQb4iwY}Ex9$xbB4Th, ٜdj yr៭FRY㘷R)Q7gkUuJQvX$Grex8ٿzownHW*joIS'0v 4!BA|ln5]ݑ\m-\BE !vTS VFx|QW!>SN"?696+@*){|zFco#)P/޴N[ ,f|(dԓOLb>ȣDÙl Prk]Zjzⓢi4xz[Z{& 6?cЃqp~3ys&quVVE.:apyY8J1Ʃhet?^JjY` ѧ),\H*L>LDAa{ O-Zb;6́h{t|aZ(W om,2q2\{NHӇJFZ'X`}U`.2K9Zl{ |.Xm4mqtdž7 S*ى7_A@ma9HHЭ>wni65Ud;G`ik/Q=>>ږ'ٔ/asb@`qaTr l_q$Z) ˨Ԯ*$(I o_%L7*=-V|H}UqtiIb."Ph%í}0ڃMMP|,N>i{(뺻~( LS`%ʁgD|۽[$/ p_.fvHlM*EQ'RrX:UһnҼy^LmUE}ND|u _"ͅG*O}_]8q܉ѕ+;?A%6Vk+x:]:w&w4|k)]l3#Ae`X\xa=sM_8#>*ryx^qۇ%R׽]22y7x oj]]r|pa^[|6~Meؾ%u*L-[gh{^th1ԯٵE{1Nꍳn_B^iJD yOFRIF' JffMPsrp K Gzi0vordi`D ?G*WMWsͳ 9#8rN&zM4Yt=< =锫8O6rreXPezJf#a AM2_XFRZ]{ r6q9H!#%Iu TFX7w%XS8bw:l:BLi3ny'`VKWKãL4^gqIhbY {,n=q QTnаVFs{f\^eJf-SRRy/  L`KĞ&@ !|WNh[U%a-"X^O-ɴ(.dBǂetMLL9Tĭ1W6qg3YdG|vW'7\6sl؈q'~`@~]ٜ~[gqy#gmæFL>1ѮӫWS5PJ1N(3ErZkK/ȝgۡ^ !BP >7Ó奜'JA3< )idFcnDKũ8 58|  6 iPBD`?.1R.*^^X;k6Jąȴe[Y8MUIQN!d\n|WxgkAf--0Aݵ@m}a趑o/^u*} hbsTTL[4 '6̎2?dcݪ[lJ1F/GveVw"?]&]ad57tUs!v0?;vQq9ceޅ 1څ+O"}A0VYy \V)[XGktb[[CⅤ3T:x$q|a5hR +HxD6A4 ?4'9=U+Fz\&'n=zڭ&',v͐jj\,K3mj/{ 'xelWfIK?A=hT_%x"XzZأ6ggzڑb2"St5foWiz,wSbMaiɢC0;>RSv[*b Ok 7н5lyUxUk4nd悪Z3$(+a!!Y̆b\Ա\29'2J,s^rߤW<M\Ս!8{ |]97D[4r} ǠI[*?x֣|KܠU7J L"5,^h@HKAe@ 5KQ:sc[:yYL]+$ :7ADʍ ?,c%%ԜOV+$D,nz`[SlfxJtU(}=.sc|2j]f$(~PjXFе^Th c|( 84L2=щ"F׫6J **n?I Ok) 7WzN2?muJK\M{MϤuߋcM` rQ[>Fr`?vK#ԍrCB6ڙyX]DzO n:X]9KrS}}H ޺Ҙlh>KCAt4"!aʹrab +kjq靄B(D7W@,YkiGd-6w-NwC)`Gh o&B]pm(4I(u8Bu 2GK syċ:&EhrWoĿ޽ummMHӪe:Q;7 G$=]^gu,Pt%G}.IAՊC@p/{R^MbkkXwf0UZF)bF;1 ^|d"k2c`Ht'H?gcuGZV7r`讉CWMoUZB\gkVb*'Dt>78JZJP$]A+G%S&I( vUj6}CqbeCng?+4b%/EkasǁÌ="􄡿/@DSAg0Vb#пGʟ"C}g!dg\џKZto;A>>fܡ8NtpSZhߋ8omP.oST(j^1C&lV=e9aBUKЪ(7w6$B,U2~ɶ=hdj4T]LZq?+!PUw)Z_5&ʧO':=Hʭ>j[9R1 hx2dұw"")H7`c`s>Rż8قm($1PСυÆڵVHz z0@rk}0ʩHfWN.~62RH_/xeC;'L$/ulΘL)@d;بF{Fe{̕װOO"޵+Ch`\pdЙNdQHYrʅ& k*@ӜۢtE7DM_➱Tz |cc/}$Ru g#mT]i:quia*r+ƒ{dOZ"Tc5I,=q5»; D̦\fw2в3Izij&9|[cj6 „lGSl RK<'pBc _J6U;d^0Ci1XF9|.ҥWp_^pw,O]] dS Oᄢŭ^3h`W_\f*/(I[לd>`_؏#V;6Z зU`1=ʢ8 e6i:T#Ȱ~tF K@뙚2ZeЫKBlé. U:x3$+x%Wjmq=#zLxzQ*Mbp Tn@F wu͌g=p:"ELbEs9 mmPIGh^hh3_3eT":B/hU :l逛4Y?%'{0K̘>s|BinoCiڌ5eKx:x'n+ 1vF,G~]걲5i"31եu7Ɣ̟咻X.(/&Ke*LYZT 48ZڅJ.2%h~"؛Fܯ<^S(fQ\ҥm;Бwz3a7Ѷ$=צ& ]Aw}$VYϖru%yX\s%ʨUX+I7&ٜL.o ![F{+/3Ifv,QMqy+>N{<-z.'#P;UsMf7^Nx J=4LFb5$(t4cOd9W=/0"Les(t?<*XFc+C&\PZa4W$T+#m<*&hpuRQ<ȸ)- I68LVmfF; >t-dW`Wn;pJB0O)[̱Δ>ya\Hb>Ϲ[OkդQh]0hW0ѯ ·z4KOC$F(|tDw`omJ]oaurhqHrBގYҩT~o*U.X 4xd)PNuPw.;{[P85 +1(E~/-x\E20.+<#csZTk02G6 6^nΙ3Y|-!w˛|9Lk󊟎Dd0@s+ M+My5?kbIU/VR% J J%Dν0FGn_ Dr(z,#6C9$lB•~ĽNK>bcby%^MDCa9t-`5,*cG1*;og)-VTs9hT,+=Hś8ZvM3jNtiz)q@PiZ!ɐ'*KCy (s(axic[Ԛ[Adid߫}A!"[λqT5N>u2s Y'Z0/p"{ Hr `g#Ԅ[[ V(V;V08ÒZ}71?hi?ȳ .IzbhK W>sZտ i?$`MtD|=_kayh03]^ ocUI:d9i wΓ_BbU0`l 윢(Cl=Gb^Huh("0Ҧ>榳HYRF(U%,$;jQw)WSa<$@4@do!J 4aYsJH攍Ҵzwa 0JA+VG{&U^[vltŘ`MZu3/v>@iނlB^U-p_H<1#4(0yPq{?hܯZU`Hx swbWjLK&u=SL*11NL_5qL;R:G]z[D/(~"x@78;1ǰ_nvpF8{h{lRX!2!0amE=-̌׺7Q^raқ0-^bE1/G&B9@?U:|WYבlaqz&k;K 4 `6z`<+F7K޽Q|XU(1u8 uq&ܿOB%? %;eNED@Gٻq0KԷSʭƑRL>ɥINol4gV<)of@$$Pcҁ0V:z֛;bk}T@SDyv%1{Ո6fB9Og8F,č >VcGn4_d`j;iĄ-;kKBNf8;%N4rMu͑8bh@8l[e~bZ\o-(T-!ɜFn Q̴ QRYБ[SPeGaJSmRCpg+m <+I-e77GJPl ՞/8_sP)`>5up%]<˂l<;wUI /TM{zW8X33D(]ZB\Ω[blYbx ib<9!b DZzobn&+4KقD/vbҾecI"t'b!4ZvW6!0pJ J3 FkHCY'deM|^l^~af×?zlvNB wmDݐ)"Ԅ A/e)ĒFd ~E* M0[@?95ٮӊ|Dm;>uS=1|EGmqzphꁘ'Vacmvع=-G[jxBc6[a`-%xӚuV1Wll۶>;~d$@VہVMAh44|nQ , { R2Ǽ's‰[K E+G]9KBP*nW2dᎽ*Ž2tìkhd4!hlY.CR@DibR<8 P9cI J3Bb*A)@111q^>tp:|sf1ș>Xh86Cw~<ÙgrCSV{ ^gVF+.qU`2\6xo:LHg55xd">~,(Υ +qrI5˶%;UE`vAyQ$84#44wa6KwqmϋIV0}%.ޏn,P G-( 3S,wGuh0uhx JAƠPւ!1RTlm=T{ńMN*([6A@ WpݠG\jwqcvqTtc ww|2HΰpTb2IlPʇ(?bp݆^A-S1 u.Yq G{ FGd;W@_IuǙ"ARR@a g P'}[jD?$8ό _4a#*7j_!'F=jQo] Ea=X8|6.^A}3])dQkDW>RΡg"azd ?j,pC[^Gig,=:(~gP1GpLyhJ`pS6y3:eU|~>MNР|-ShV\}K'pvVK]`sK XU|C>;6'HvVuZ{=f}+$-' !Y63-dbglG҆C&o OUȧGkWlEe%6YX8Ү3X+h7ձzΓ̷p0(\QC禱gX㠊M4/Dytƒnvr~ؗ]yۧ/L=uAhueOH}dgM0+^g}DjShlKڢ˗ӝ_ ^u(Ozvdtyymgod ɤQm(ߓ|7ã}FbVxlj@ '|+p쾭OGm_ۯM*_yJkto5(7]*** F-[o/gh[JYWo0k:Y3}yL{!T D"?@hIeCa c1mD&S{"Mz WfDzq𡪶B~EmPUlA^؍暈_*Z+2%Lgᑿe 5G#؍U7-Had{87m-T&=N^RҲ -¼Dц5!}q O N&9·j) ^XOoER$ IU/ь}fgaP:,aڀ`6!nZhtFRWsn([97Tm\˥tI,64ꓼ#@ Ɇ7YS"f2d}cR'3>}^S8AP0c8/t I^lHzV7Jq&4s>eSXJ 豨 .gPLUM깝iQOSDnVa^kD驡JcTrjκ匽ҹO$ $]>B_ IۇV%b*#mTN/:6>0u/1RmnŇ}?e魛(T ûPOJyڽSuKV0)$-,DB,DK![fnRMIY@cѩa[ sm? RQIOos;ZM|EG y2Y&3+-iMƳJwO?!R) n|@t HP죳D0cR]37,Oe73A8Kk$41'qgT2#AǃT>u "^!p~_Zu[dY敹ٯD9?jIXݪ` F?5LN޶޽ĐcfOKtzx̀-a?{p鳨lO XV[SoV=ey tzq~"3z'zmuC]-g/ ΕSfFT 7S0&یq,cwbt3 Sf?#rο ͥMrf{\1ʭCiRmA`*f~I?B9I'H0lIpX묑 NQ"^2}C-qUv3AJFQ?ۀbKa-U_[J͈%á)YeEo7rtf# l΀#ߍ9BbZzY^+xjJd'|;o)̬q𒤽PL{G)@EiӷL 8D0{k0=%?LSykXߔ(2V^($􆧛4ur?^du_R6Y n%c&粏F#jɰ7rm 4CH'h$)ūA׫{qgD,`u Owή{=$?!-}xtXW0lʭ^֣lL=.>>B/jl̜#1a{qqSedۜXH^-Wk "2ݧ ؜k~sٟL /P Oe\,f$2*q_qu:#w8rٟ9'X, 1~dڨQ&>@78lNj\co$:Vt581ےqg`'!(8VYx0{M%q;fuJKbHV &SSN "ң}&chU)[j`ͤNLs5N\v'&]v6Fc|FY/)c%g9o*uXxy4tu 9p z1&[9c.3!-t//Y;`<z~a6OoACx͖KW}E\>u&*}1x8#y2UH6V΅rmД;_K8pv3?/ /#Gn1ꄹ/W;m 7Vݑ`I}=uS/^PpqsQd^Nturi=`3_pcP oY6kg\$2 #_tnǨƖe y[#QIpMg“K!C)l<)]t|/ p&-fOXjY׌`Qq1Um{{C,_&FcH;uS~kUtq$Uʅ[Y(<[m<8ҟ:*>cP1e%5 alHa˕[G<Ǿ7<33cl E%w& D"ݻ^8ޝZ\E@^JD\ mWFO{٫}X.TQ)n[Τ_gdFT:PS6VHϸ*╇$ʃ3I<@/ 6iP;Zp=z}N9&}rh{eeՂH-iiƍlt;j(ܗ@3CreP:&DFP@vFL~ES*ܐ.ch0~wB$[a7(RlX{vR;;HR:\,u]֪01J$O7C9%k/ȢN3/yԟ8 >3x\@7R,sGƙO%bBlA5;) !rOT+ъdWKQZ Ъ Vp›;LZ j/!;-)l^GY,xcQ*u<7nyn[d׉hr#K\NCAַw[חtX+0c tfBG_wN˕1iR=MP zA{2D;x*unr3]T$&f:]̸qpi|\nvgX{[uBTba폟`<\q!D z'b%ׁ-3s,u2yM:W*c2BdƜ*qAe2;B]~:i 7wX$یdcfφY((x qhjh ~ˮK5آ+9rV0)\nhK?\?1# u}!6B 2Y><՞ݐnK'pZiݵ4J`2"Z/@HlA(7#}ؙnH*`B1:jTZ6ΔƎXLMtAS@L(VVqoٍ(tZ|.z9j;orFZhL(Vlh0De,s}*ȻW%v2Ȕ'D?~ޱ$}l_R)MGSi6^&so89!E` 6GYO=_P(?N*Lr) aRtMV|:SrgkR3n* 3IS5>6Y 4% 49l$ZA|ݾߑkM*j1gVkJ^K"]a5@`* h @Aү97s,qV'klG(sgI_|sL^Q"Pt⓶-@Ά/g=hFgMhWweynw#z x#2em!ϰ#}}|aٶ PJLxaa~TfPsc'\qbrt{cez) ƀW?kB YAU|jL,SԁB+%܋PxKn\'w):֜m}R?4ɱ Y(wVr2I:sW=f$mb`F :UrH8Z- .u&gOMJ?i{~vzȡ4H:fD0YmtN,R~Wwy<ckm߼+3&g rEeUdrnxb j-^AYit"BLI ܺ|d _XM(3 boha]@zMcΨoiygUTGxdHS-uLOq*h$c0vQsC֪!JԼi@5G&qhm]ݧ 3@ \^ X}M k;g8Avgy ]^6asR C~C]D?zlⶾӅGpfh?'깇 x8PpNʗis:iۺfRV4ndyޭ ȂF/&mfMu!%`A]Io=|( 4\olz Dw%1 ,fuᩲIEE)S0S91xQ_jNE c|j0blw00[}<;K( \ۊ_Yhnȑv[Wq$o~ LpQ3S5u@ )+F3:S>l-FT%7ϋy3\xr1pN\Z9#X5g&B^X%gPFVi zPф\*W;n<` M8")Ў݀69 yͨgcՍ-.[hZ=0͎g)t>" 'ڕ`-qA1Wf%7BFE'Q(HOva9xe5O%;Q!c$ӘfظWؔF#3~00efTx+4|hkGye ybo92-w@Ci^yN>M+bNB9k. h_g!<\wVW)My~QlŐڈ?!'pI^@Ļ%%ƶvfZ?j1MȥVg`1핍-rڀ{0q^EWw]ހbZ kjunHPJ/ȏޅo>2LHuSqspDsCs1+ՠNvة$vsVe=Ƽ_czP]':wϑ*(`~$qlZ[Eaqd,ɃT(8 tuB"@(~A=(mףt{dKz!)B{@{q;M'mcZ&&=;Hbn|2tL'cft4lRt8-ݝ@E.+3,Eɡ麥Hvm+WPè/' 4. u#_ tĝN,V_9Bb@Sc)b3w[ tJ-V㊏yV!n߯{:]  D[1 zͷ ij_P5{D^;Xf _BQ>h٥T(<$/̗U՝yL/6l@vr׸-ir.m͆{34dЊƤJU|7É 8/˞]-M#źP56ύQ+KϮiV`+ 44j6W Ob?1R,ne\ƨ-lQ|&2|IŌ+>B-5mP9iI\'`zLFtS|ZW])мøDr~4nLb jEkX, iʆ۝uގ`(Ÿz_]&{H%Vu}Zh.CmV8mŀk Ym]syFw^CL{OM5"'"{ZCN_9,vJ=n1גk" ]Fƥȯg1Ca`E,Ȫcxl9$|<|ϧRo],EK–z5m͵{ -y/+ƴa\uhW>JrW7ʁܕ,E5-|K*1tS\saBehmH;/W̘ ʣk9N5Q/UrSۂ"N2%Sz?qzCL6sr6Pd';Y=r׳WIO'{7_=PUI?;2A߇*znIŷ}Mr'Y3dwk пN{ႜDdRwЬUS>4\v3gdE~`nR ˨gom M\S/k bs<½>6#sq)j9v n&gc9-B p=]N'J>'rݒ$kK6'σe:33=fHenBsMt},r Z=#!e+&yTNvu-((!fȒ6,M,,m n餆G6툕&U_2b{}Z KROs`Дnj_pwF'P^- WI t@*JCx41<^>I&xUiN\f.si]{}J8#=AZq3L'mOIZU\iQccB]$9'mOX|?ɦ .-% ?%xǵlZ U˞GA*PK7 >ҨP'"k])Dȉ3IB@t+ʏcg12-$WI+)0DH8<;Nt}>0]# !WMH> L~^(SE[Rv 8@{Pm=iaN6zpy:BHɈedIGuXBf2S|>z_CoiyC>-!78DR%NarG=<Z7584JIh,쯫{M&[+s<hHYʊY.o-$"-wIL4NfI 1^OhL9/J Ep(؝ 4ҩ`J1zRz U.z~Tf3',B;-m oJװ[PC%$F8GqcTP˿u.x^7gw G5ۭS,H9 ϊX6F9j9Dpa&#[S'ˊb"+`51z^~KAofcmz2>FE4I^oafխWgbiЬW8hPx%̛<]+pIe*g-]^fI3KH\>`Fu$]VQg^TY];_#-t?-lnDDA7 E|!5x}E{̄f "+#"W4ﷇEҜr{*Ilh ;xa? Dqjw$ZMKPqŁP~2eZ8@ً*4V_qh"20䈶mu`ףQ5P*Vcځ $kk & MԽ0ѧ ת**-La4z1Un3p&ʺ`K#W=d*fjG*0qϹm #tic:[B`bZQ;g,TXGP~IJ92ZpNB+"V#qB$y=Oz;6ܾw 2>!F5l?ރ;!A*ww?b@ts",uX}+Px$;; n n~EΚ5T.+Pr:dC{u"sy/c옟o0sej gy خ1;5S`3 D[$0*l6Iܿ~E7˒TZOZ3 2+vXL5Kjxq$wB? Ȳx| !.6[ltsIͪlơ0ch3b0'ikFSh`T5wZZ\X&)c(7LF ¢N&+kXR2c@ 9(u)FiA48b|27i3+mr`^*v 75K^"{pL*b:$K}օžn-v+j,z.@:6m/78?N*(I޴@dKKdg6 VabXͭ !Ç(cl ;@\ndp Rƒ?cs9y {7 DEۘFgo%O[O'R'={y-s I`QbtoaٝOJ*tѤyӊ890O4Ox.h:%l%"&d\{h`8YnKDє!WMO}umkxTf-(#qSdV|/f[a]N"Z+&*_ޮ[R իr$h<2 FT;wAZ&.i~MMe,?}x +QIc*;qh$"V9h/6^ $b^'ÈDLp˹JpQ2ܥwRBwBeGJ u{VY'kR;Ιph7x"Efi=ؾ ;LT , /UPS| 7gê@^h,@] B#rA38h͡sXvh^GEaU]:egI*Q2Aǧam!53'b`G+:Y #WA&mbe4?K[4#u Bz-SLx'u}"n50Z$|ϤZ|\7m%GrO2δ2 玫_,O~Iː:"F:cWccۨގ QBߒukd቟'㛦,QC t'N5l#M&|+Yd5ŷe;iGKrG@<'38̝tbt]kh6ڴc2 {X:lkwGJ2}v|Tke*dz%[:sL?c۪C!xFtxnUEi]6i*U?I]i{$qaF9qp3oƜQEASśODkIr&sF.>oؿ@5t!7t|fGndZ㹽ϤFK;g5+FEu_9pHS0]^lvM B.OQ OQJC/n@t@{>f BxaUfn/t,>At TA'ozF`nOR !f-̹YƮ}&?r cLTbK&v`97WC)^SGGx͚#娻T]|⸋?~&NM4œҧ') }Hƙn7Lo]HW~m! &"ETŸpaA\L5>Z+[t%;/ MzxWA?KS1X:!ڛA`ɡ@]׬ ~*T M% OeBVH ɭ9EZmz~3e;^]#b@5D/"7L1' [l&YR{ m@qqע^0i. ew D;oP^=N IK&)^(gZfŚ?bQH"76cy2z.^| F(83N ^80] [0RR UV{.,BmawڇHiYEWfN 6H$n*B/Kdu?gKm*ѳ "aV.V36扁oJNl";} ye4")F*#c;歡fSPsJ>m:p),nd8I[W'ho؂6pbTG65g|\*rP;->𦘆W:'GOH#̿qQ=܀yQ(JR٬_6<4M M3doLŻx/+âhiډ7z+D﹟LJZ ͚#3 k P:d/`QmZ;wS Oq’!cH)ðx (~*Pqsh[">H+S XJVg:QKX{:q(HH.x`haSLD>Ҕo;SN/[vY,Bx 8']9H&il$CBM&9qI|QoNҊB:Q{S S.%z~Q?kzr ݛ]VES`0TdxxSE8΅] &-g=()qAkU䜿Q,cD2t UiL;QIpq&5: =Oj7AUVZ)z6~VD>/|g|'11Y[D@1T_Qܕv TTJewˎQ&aB4ˏWGN>Ix O+0`X pV) ~Ժ_( E+DuQ3NƁRcIU3VFh{lx 8؈.b9_Om Me+6(0RCM̲2J4 D^K[QD_/i"ތ)qdK~! uxm?6iD]HL9 jm#տ56ӧ,V~6hAպtsv^z;ZC~vED_:]Ϫ'EfGsu%Q% H+0 b߫C1>p$¿6U]Ȳ\VbuFz.a Ӟacvcl'@e4% N^{[0(vZIߛuSYgUFۃjЬǃ/[Qv`ɢXh;/ȅ B9f}Q`wJPPI(AK9C55yW针=z;A4!S|۬Ug,c YԀ`˶;x~##R3QbĎ}*u9}4" iDDI,l sV.?Ĭ) C1"Kœ/<^1"R/+d+Y0]ZxCEȝA5:?c B]zTخ|݊ObIk6c^Fr\ăows4U5g%I}&>R|%Lv/v.zXzYlT +zleK# ]q:_BȆt i,!;J,'wъҋ 0;܂JuS`1V0t͏[Җ.hHɖ,},^ YT'/RDLқrGTF( B_pg!JӲj!mNr`V=`/8.7rN47f9!Wet>=&0s@#YjlW2XĕxGx[hme͙ͅV>pϝTu[\:IJ i~1lStd餻1u,jnG=Ho!Qmn|Wx`d _Kv>Raޜp( Xa d*,kTO?,-7հj$'`cZ a=%Rw/бh;kGZtmg.f龛@8&Y*A+G>Jg^%A,'4J0됛P%q4`Z^oj{stqk~\ّrf5$E=Љ#CUA+9~krq* HM{;ޕl"Zΐ!'SBBO7Qmߏ} UFٍw !Mv!؋~!(Ii+ፓ*% RCO)`yudf'Ԫd4Z\:K"&.Xh:KSn1$_ )~L<"–%2Uhe HXPL&"8獝֫%1T`wюm]6Qi(ƔL8@c=d@gk3Pj&kVlodx)~PC}$Jr4K~/Sw/F7 '3ӱuХ ήqĊ)f;]I~z6%sh3A.A6*?qt*~_kYFW 9}(#WvAWxNY )8PfЗ6?M(eG>N:sMzU2۴zۍ?@gSR(}Gބv0^  mDg ĺNgGlIi#rS33c5dTbIlTBHx4UϪٞb>KWCZ48or<\s6i)$QH'-NJgmpw B5sǞDN-EGANJ3_$R3"L),[Dk=.)bkNz9; kmzPm-n0:ū3bc ǂ% G?r7`5MY'@kQĺUK.htAFX5ˆ+rqݭt0z ai _ȹH؂Nnli]`-r;p}/^! N3z%lDm`\MB?/(9z؊h>xȑCu_}dp \br` ͮWA%#6aB8bܵ3ӞC͹$yvzX7N ϊYrF "HݒU|QDڢsuZ$48>=C 5g^6а"|R6#LU"U/!˜|nS*g:tR84RJa1jiDiH=jЇ:}[4"A>/!_gfwGPG*iVTJUivBpo@1cs ¨ ">aLj/9LOވ})N A.hn W nZyq}N#fx x_{o~²<'avµ<E#8Ƹctg,C+S.?i 2ÌS3v4H$/ŬWv\i4Wb(2ݳ<3kWR\!=^c!޽'Xr=)nx53jy>QFrl}ςl*Sp1Bbg(.)vIAE?-uu6J#0o-ckFa srJp(+h HCFWd$ -A d&.'*ib^?XXkWBȭ Wu&uM;w[4vHlSLh(~o̝^%ӶV8_ZԜ I j7zOiwm~4rU$0 &nMąY֋_1^>F43ҹ/<aZIzS阹ǝFS>15m'd%mJ_UOȊ|F}$q*rjuyWKl(o$G2LT؄##[Irn81t|6B"E_4aÓ065)lqflV( |JU#>ks'˅JRL*E*er}K5薒Ba!̲D&V3Uwi/QiDE?G*)t.?>^#M=[s"#֢^$򎻌b ;P#A$CJ%7.L.\LD-(\/_u81*J龳[Zh h#ǍSS61p݆jJ ^8~jEo<8b Udy9e+Ǐ綝pj Iŏq?/.MwW9 @/y}>)E99A kOI˚EgAʋoeE[/@RtɄ }nT[xf둖58dP%Mo@)OHYQ^p7+Nn <oUyl2s@.@Bp\`CO*ja{CX6cH?ÿ4@Ǿ&WLaYGE%.Edq䷇<% N,C݊k:lR jNV|Nxbpp)2yNZ;z8-鶌~;6UI9f j(ISm;_]&`c)4NGG :}wFT"SA;?'F[u@ݨ1Yy[6a.kI: YVO}gXwce霕*6ϊn}ݨHPI{a%6:ARM\n0xbWp=Rb>Qj\S*}U<ҼླSU`*v-<V!Y6SWXb+chh^S({Ku3ͦ"Y,!;V.ŭ)B`^/Ԗ[GWGͥ0(_3IhQM>[dm/(Fx7c;;`6½sSmR:,J4$ܭya|wN84)4Nuy >R_ a[RL)i- }q 6QVxW"\;%u6u?ԫa1ѳj;튣p3 H%d?JH Tfmd2,Tw+˻p}: iP}~ٛt[;j`\USp! H r7}܇zղ8 qac~+CX+(AṞsX(촼Y!ڣbĤ#-us\m-,p13Xkx"҉2 ^UEk ȳ"U /(py=n'\U9hktwp0e-W/F k:*->y<-wbf(wvjoDuDE;ؐ#dɣl& 4mHצ F^);)~t.'V&%b!|c=*)1wLp,6 \1Kqg#f%# G;`~>[[BJi=S2j0g=k2϶p\&"2Xz{8%\-$_;SLt<=BYؚ#g>vvQx@GF2ϡczYi\eDdC&B7}걻-Z9-wiS7Vϑޫ*v6OTZY!.ۧuTaoJ&tWxPCj+ժ`v;b,ӧCyZ̄xv=oU>/eIiۄׄ'.T 3էr?kRP̡K%ir WVN:Ϭ7^|`[`^0,R`f7`VN.6lxʤm 8(֦WC`Tsf{)2u\gUA~OwGS7{]k탱w3"9X(e9Ϩ7N,/iQe_ hn" Sd&f\Nyy;uf|#z 0IfZY^\+nmR' ֗#`~,$ع<-ra+NqscMTnFnk2ذtv'fLT5 vwp֎xƉz=A.0m(&ض8ыXp زRǗI8a>22 l)ކLp**_F0SI#<3R1֡kZ4,!I0 6W?S,pٞ@Nh;%#yDTi~ 7r.ԑ.z,A3q%( 88n3 {F.hHs#~#A@p2.G*M؝YTx|T1!pQrp K[S`@w)4جj68>Fҷh`p=80I-$;%|kqp>l2!o7,3`[&1>M/Phjf|3!0y[2 Xj%lԊCv1O=t7MS>i-ٺ&RquUCkM%/SLlTUu G"RϏ읲lf>=ҷG`"ymó7})*w $PM҂X Nz-k1rUhE*=;b2޸α"֟~l9O>KsQNT՚w/+2^=As`A9nw ֞_?u`0XWsVg<#WT'*x>K؍е@I/@a1ޔlYvxMo_kE[w̛.}C8 %~˽p{ M]šcn 'RAȷ쇄s;tkW/8yh]?х԰+d IH//N7D&8,7>2-*sie6'?ʹf $ j((q}CU bX~g5L"fƎQCy2[Z< 3Q r17\lǥxzjXzˣ(cc4D` :vD;_WTp N~E0)E+`i_<쬍GhV9K#yJȳk9}mjLpPrX|LW_w_wiH9%4Ja_Ҷ[RTNUG?e(z̈́2*[;@ JwCś['ERe#>ŖD@J֓.S܅WD԰ ;g,iWHCZ~ptZ"Tn Y9ʀ+8'>(>^(`C#o>\Ib˂l |bBw`da:kw&3+=vjfaH0ױI)~R+*zO`cʧ.iHo}y# L [CD˛i1w<݁8:P rf[hM<3W)J`BɎQ3Ë5x67fehk,6J}27E)&pG̦EJ$ժ@8\FڼR)ң֢y '=AѦ/lO[?)Uҗ.LU&*"z%:KSڼ5 B5_*풜-< =c1oG%!u3m$ɓ+JWS\MzBH&y^iDL#>=ZL!/1dI%!,iji7F],Gl;ylrcP|BPQڪ#;OwP[Іb(\3)Ch]ڠ`afj`jƤt܃FkI~Ռ ~r4bZ N; uªqƺ)٨`n'o^(d@iڢrރdʽ DAevU&v{Nf^Ge!K^Q_Dk3Տ7V+.z+6WoB528?_v &ϸ*!;'`#Pùx9aa$^ {*0R.&/~qG&GGCэ\U"IfE7|:{n/EP+^<9{hvIVrӆTFlRYb])[^V;~߿Y$}@f"Y@\Pjy?9ɹ5zq{Rk(~3+%[G}'ā(a#ℙOzDlghF%\ۧ 1śR1ה+fÎLkҮKcpFҗ1P;ײ#-p.8 `أf ڍaP6 `ʱߦ̞蔡ylY6[[AUwLY|〣2-;@up%bT[BWeemvMD3J*Im6WOܵ=aR;Dio:0i9m~$# +Ύaaߞ}& @cUoZ&E'sAR# FLQa/`߯ #6&m_>/KASkL5tTwo تZ vmi5SHֺ0d"r38lD謄3۴_8d~,OSzQwc|HE\c聓xѪ0yޔ;+oouﵙwt=x ZS1Uj'󫴈cOZ2J> ?ii8)@d~Z_Ov̝ştD;qHcl{|1Pڙ fWeGu΅@!jQO-VBL\ ԭ&#Q Zޗװ +5+|"$JDӐ}ߪAQn#E+akCt?  !0Pi@9WO&X"x`•:n{a44OP,XDw!n2+ߐ܃WUAңeEdam(/q;"\1ޣPA~1iмc Qw`շ6KT.ǹMl6M:}mh GIƍL5ُ2ml-k;B +i&Ia `vF?:`)[R i0ֵל1TwTJA nI)^)mXyy70c)6Ah@IoƦ[n Bf:Mh ve+9Nl~зߵ!N '7P<%To7lV5O~94p6Nwɪ!u0[P=8vd-j?g-jr޳jo*8=}x0bk C"13M l[.$ZCWNp27a`; rǤkNo&ref,ayYDD!7lo8vx6av9Y:͎YOЪTOnMGP[05 )OZVtg]hs% 4wt.LW_̲L n^W^p̿A A[_gk? yG-ͩ> m]*"8`UP߲P);;%^:ԶhM}`щ/ޑn7> I@v[jpl] GEW.p+?9nugלĻEyxVP]Hk,c>?x7ctnX˜lj_CgZ7!ͣml-ѕu1uw N/CAdѺ@qKZ}]]96/?JZZM6.KFtR= ׻]3>B>B!xYa9 ;]ž!DZ@@T$Y:1u‹_$O/c?dkvOJcA P s}uon)1t-QI7YΌٸxdoRss ynF-3ll(WCc}I7 n4YζRzPGp_٠; l( Je3|7/]Sh NQFLX_.YpRg:VRnC˥k^cAK$E+hdWgEV{@JhvT!w[p'Vl&-@;[o*=RNgIl\50wBDم<؞ѫG4|񹣤'nj4LmʞL&8˱f s$lؼ#j(wN1[^Ihx@av'_[o 撏t5^9?wYπ#~چĩ 59!DUKYĭ ]j쪰56sfCqDyHZRB|S]Y g]OQRA8(7jWn^k/g1ZօN_|kHo Ry7"w>값U;KQ>w lK+$h7:utc@_4|[n$F0 D=eC|Rd 5<&@2`Ж8;w>cTc1k75{v^9Fr6UP&.}>?LqLӇ'WžZɪ\N}gDral:Š&l.a&^]1{L/1Ĭ΍W\ x+bEfMK>}lZ:-O!jCK-T.h pi?M>b8Hm`oĞtzz閚}o?_u*.5ģޯԜ4NJL*,e_?[5׾)75dLO{->o3|!k]d&tx2^V;"GU(I.@ q;M F[r}&IPsY?Ej8FϝqGXkB/$HT:A:\iI*83\;&Ux!rcL ]TR̊eGg1ʕlCj"# 48CnQzT~d &B6˦DFiq/ ^̙S Ѳb;ֺ |/ĤG4dph/[.^R5dC) ?rA,uM<OBk'f?hL2 yŷڱ5Bl>`&FkOQ5LqEўT 5(Δ薕=Si ΏeB|*)Nx? c3zۈTc P[ŠQ(ㆦ 2qr>Fn0_gEnIRB>T>S tp (( Bz폁j]13=H.Zp@ U@Cz "Vv#=29g'9KO{8c$@iFdb)Q@߹|:Yl,mxkH&uߤLJidbBJo:Bn4cYҶ"TO5iXE'=%E_Ep(O7wp<k&Ԍg4< QjļASu@a}iwc1x$Q)u4ɾo\1~.vOa ܏ xg`K獏@0{Ms]D +TR.N?S+-iLE1&=M6@~/ 1BNk3JZŤo}bqz$4 'Hh$x%pkrtoI4Nm\ouEuVn!3@`m)VF5gxU$=; Xń5,rL*l&NR{d)D;3m 2vy utDcbflMψ,=NZz%] ]\8|WS' /0Ϳ\xw'~S+R{@tf5id49ρ-}7#,j=EVxk*-PL+^oS7LXсVqoZvIcC[|83+TN>B 9e8x m=O#Վj`LТks;ﵢ&*]gS'/FAArSؠ[ۓ;٨n5=d$D~UmyI/XSQg4:+ EKNI#%S,۷pFJ'Pҧ2S|NSIbp0+^XL)͕K_$G:s252{Ֆ AFAV/k'C1}C-S) wjo#)0fZ_~<%`fUz)qĥM߳;Ěwѿe}>? 7x⸗ih^ *y}``%?+-}-2M#pHB~BU {%P\^NSFo(-ّb֯H%BK|Xnۻ}*ji ϲ䲚ּ<,d4ᆭ%s YC A 3@q>=l6H}͉V\. Yxk„cFS5=5%!?%s e0gԏG`:Vbt+ 0lE ;RԓHFUL)h%@I=-~_hrm=bágY&qWs׀RatW*rX #h "L/Aæ~C"45fʆ1Gp7qsU52tKw֤>zH݃HX1X Wo4}f;ΘSKۚYX<ι0 ڈ 6Iq!Nw}KoyH Akg,t}j=@9,4$A`6N`i}vs[;1/bơhZ硘Fj*Yt[O*&\#{`HB: v75TUR%#~=4]E&bp+?!b-wDl1P1} ':34DeC(Xz\@4/sѪJvt^itOR pRJu݋eW]흥͕pvhe-!7R.'l@B]:ZKr=xzDU-V3|J%Ҁ $|Z񊷇nB_'pB̓c% #XLQxW{WVM#nJOEG*#K>.f8,AҘ6(*Խ7Ak@=!-6HuU:Y’4zT?מ-+WQזNmg fex&'Æ;&L-'&gE]b¸)UfށDAJ# @ɍ^r!"}D469v+ߴ0*;d6sӤøID$TclU/x1U;am+5,"MC_O(p_.KЯ'{߷s7ʲtMQ0QoSf#JR`ݚ.1 )!m3㩏;׊)4lcgV趵,Au~ y`k1/]K Oċ~×(BFg~ET L ,0!R+"|2"0u( l wnÙ$|[/HO`to=v<Iصn~r;76i.;ؿMO% zo,Nzǰ&ʹh+Q=-жzamF^a;_=+hsv/U[f?s}㔾aLékItG"̵1FBLxr9mv|Tgu NsC{NES Gڨ驐t`Sƕ-Y%DyCkjJDkl>&>|zqI2|yk-ɟdVXl"ה#8xP@ӧ阤xlD PҀ\j}ݷur ϘHz"|#n,K% ^&׭La c=NgVǣic5n8I (DQyi(zdī鼒|{Hyi<B_Vبd.\>5Ԉ7xZJi#bb7"&Gn$$΁5~2ykӔgQtdp^@B"KN;.9/b BfC   ]l5**a˓[1 .:þ{`~f ]RsUhig e9JTPҔydys1f%X_S3Qu,͵%r18`aPx׶Z0ALx:ïvMSԫepfToh`$bT\ -r3Nq/o3ף"ck2cLRtr0C,X/ӉFOzϤZZ+j~YzQ _;r,чV۲B$bU2?_kf< m#.ڨup­ tS"y&n\W"Qãald?c^|QQ/s̻nQe5Y P]rY;n\HԙT:Y {7wr*!<] jWS9lt>nw/̦;ۘ 4&zD]_q:8;W-upi޾k}jbb!f:%STF!yKIB^K1y>k)|{O=QN_ٍʵҀhb520i+B5XNun$*;;=xבM8pmjKZ/'qry EӝK;:n+)+IaЫ_&Fw`qڪu YYWv㈨b\[7@];[9Ex3+[6\49K[Z'QNtsDJ%#(2e LcPꈝ^ z ](=ٸ $4c{,ϭ;o?7-yքAC{Ppk#zXn. LHӐ p[ >g\S~EH%Yh\*mq脌4E[%ǽfEKv0X D? e!5E j+u'Kxվ7POtOiߩ؃($IEmexWם9M]P7KĜ:h]H_{*ZqiaI~^~hW1]b$5_~)&-__C e;.KUq2>k46j|e: u~Xa O,} "@^@O'm!tVq;v5Ӗ~s !, O]K6DDhtL5/n`W:,qG<5Tg/KH=ʌӳk14f[@3p¨#dta,M<&~/wpe%zYh\`&%x L볭Q}[3u|>akU*=b4]n.w#澍ܪN9^ |TagxTuޱi9XNJt͵+;ȴ$ot͈ ]x؇h}+lGFX!!#Y~v #{!K^Lz%C2?0Y4(\ǂ=XD gvD س)oEW{̆'[6S9 J&xѩ!arR 2oVQ@s7ڇ]nLpz:2v-=S&Cڿ⮚BA>>r3 ,b~uOHe⪿GbFj+9B/XvNؒ7 8\ujJerOJb4B.(Xmxg3 W]1W `So`$trrXDF\aB ?^ySsc6N_ܒZh t%/bW+:xH/҉"4Z(mk`UpSM{MgQ1_{3X8@,&jhEftnQDt*1S Z .bƊ߸p66k/xh:ۭ0P< vhm8m}v5EsS[zY8c3NOMQ{]dwX}qAB+pV{Z]l|NXkjz݋>8QT;#$߮%n[;T7YF~iiƖ\ 1ݺ'a,bZ/ `@7ql' `wS I5$I*jo PGN}hn*0R`{J^ ~';~?aG/ ka~2~l~1,qv}iVn]X ypZP9MWpӾ;9Ԡ"JowA6=@(!ӽ8npbmi u̫+e}3}fX-e[;gUr+Z'_HGSUs>i9_'cGM+yĔ"fXCwvަ-jݞɌdCs7M"jTtej&wt/jOY@3/ewJuJ Wg4u[*o=C}S0s)w:Aw@2h"\QpĤ$W]f'LoPdR7ȽvbtmqE,bIJA@x·cgYPFB՜g hRar)ؚ;.桴nb&Z>/WMil_>]0_,@|T"!*VdK|NV$C#na#6Sif sv<7 aB(7Z2&n#Y}Ly֛%IdӖu&N [ ofɖ<yp.)dCZlgNF=YgFE{JY>xb[+3 -Gy׃?dBH!2lMIv߳PBARu%[<)2*'! !jE*Sw\JE)Ncp~ jc \3eُ:URl]CYr;Bg#g0!7.$c8hN}`/C g>ͅf Y ⫔]Wpm}ZW7OZwzD[Ś,&N^[`$2MZOE6crmHnFĒ@"h2RW!hۤ< 63Z9cuIK(ֺnámj9 039>.ρ W,%Yp|`p;"82JdS&MP}hbX9M|[L0CLӺq~ j6ZO_u)>q0nפ_O͌"Sưt~1PZ4ǽ C=4} z9gC~t̿BC8h$Ir~fX#PgUIksSVv!R\=wd(="o"}vG ^Dž{ 1T~5]tpMr/kTW=e Z]TUcšB8KkvW^|XvX.3]"2%3LHUI@ߨKCzYti̹SKlm̤ZZ!,G?L>~gqVխJuTy;CA36~.}ǪO-U4w)2)Zq‹r'sr٧i$s!ː3؆?7) E<e煹u\nˌA;YwЈG|ǚax.&VMj/O_670ئ[ GP%'7k: &bKds7\´gT a2>v' ụ SEr'ኲ=r("۹zd\ 5 2$R-+/Gb݃y=9BPw |tYiKD%GiLy|Q2eV `bX'FD,g~쪍fc1 (]| O8עF,4־,GEqaAdt.S$/x>A]HÍxLe\#Hv܊V">e?.rɲσF)ʺ+\[{ܾ)$\:HB T@5CgRߣ7fĝ1НeIfXG?!~\ȡJsDJBlA6O ^| #ɣO2<݈]6)*vn`sTj <ӪĥdfnihR%p?F}r1˾R7M=)C]#L;[C+WAGݵUCs{0KH,PqnQXϖJ4k5 Yzc@ReƎZcF1^'90)n4l# GjWuQ7 &/pUy-gbB\gm{]JA>0$y;R ,X+}LNAS ="85D@r LQFoIsSrHAA}'ykة 3,0:*™,أlVس$$*b yW8#/JLaN `[qФJ~YGk_ۻSgnxvD\#"^ՇDr2G47@HRe&:, كo +92hzRɀO*05,73/KM9 ^;,:SSӘa!,!bSC8k!8π%~ESғ9q2_Y^z2 D9l;nވ}ňx};옺Kt(UOw8=VK9LJB;<'G"]BI1p+ ՈnҬi oq6tqEo@V615S cSmFȣ9[Z39;g!re&z4<#C*tm,fm섍Eߡ$&$6^q̼t/0+ėմ_i̓|V`M*BZ[ ݋|'@$^xPRav`id;E䗣/n+d*DYZZ"?`Sޜn.E T%wxrұ yV7W kuVT"IӉœ:jnCzP@,98,u˛Nߢ-@dTR7U bzRȳIS{: gGjrC*0=mr"0*,MzQi~9̀zK?J_ZNyӳpF% |^_ 2PRF;q4Ns mnJ q' Hab9aGV\Y 7EI1 y#O 4s&1xF*=U k7!T^Zfr;"9  -]*AAHhO=\@1@F+8#3WVȫ='^ZaJNH9 r4um- Tå (x/)wZI4Jg߹L͡"(%zxw\}SL;_bKid#]Zs5AsϏgq34C}|IWL&)5^9j5ƭ@bJڤ?Eݾ8joCz(]y˙ӅvU!֠l#t{k6`PXRf1vrJzBя=pr<{UhnD[V`z?5=?IߎP!d^ ~:`gd\28yQ(04br^Q$C $xuƭ$o`_R-Wit u6W "+]XX=$j. H݈7cUd@9Ha(xbdƘSM00< VɢLp Mq8!L$כ<H T2\UQ@_OY+VߒO4z8+@*h6<|FY@^ vd"a5OeU,iVeplO@zflGWC00oA~,||?n̴IavTr:EjtɂN$j'[1AGrZhgd%Yj IZŦGV0ji 2$2I]6 CIp׆WGYJKS8(A _jĮ .E}ǖLߜӠآ03tO4WbQP"c g߬cŽ'0 FL$ 㪄\09i!LXUL?mG$,%8DFrm|Jz=Od~!Ef&2 E2ɉvUZSM88 97؍j:=R<Td`D [??]\?v;Fd,zʱ`^=^kJ(xjPH/~ @Ļe5aYQz>թE[qr_rQb[0p>-|c-B尦'};"Rf͗-zY |(^RQ$**žpz-w?s)]f= o0z#]%l4@Y~W|;i!z*{W"yk^@Q_Y{L@Q^}z#A~e:gj3W$t0o}'s5)S䝷d5r* !"ޞÉс/M_p C QZW\]1>&l-ʉVK:"BjLXMwu9>jT:vX?ˈga- ,T}ŋЊzu*SW>Ykc x )EH`*fG7o%؅^'k cUZ)kf`A"lX7YH]d,y/q+ hnI8<ηRXRzpg,Ɠא]LWARp }?`uhLhRǰd+o|ﻔE pX"CCi|6STƩ&o{:F'nj 8\-&CYz(OgON\We.߄r)DwLau29 ,ɾ&O֑W" 5vѕ<6ǁ (~\@:x}5v5&'(-xI rV4Z ~kwckWʃ;0)-A^_kkO [N 6 3 F^X]bFآ[lv멚T&ep Ux5_HI3;ԸRϓ Yv#Z?aTq6bۃ"vxzڎLƇQ_r!g= B%4ChrBQ9]BU%ijP߫1ʟIX}VS8\6x_Z# Wtqx[I Muct3ٙވa׍ֺ%.'jYu}f2M%^et Y&6!u&UP'&m>S# {ڱHk_^9D\J`Ye.3[ \ ЦS hUl!m9wʉzd9u@f %6vy:1mh|bB5m`̃opNr0mp1Y._A FE&!vu}C@\ͺI_%]04hG.9Y 7 |WFyZcmʗSkݠ~H?ȼ.Sٳ\t-'d1PeL;z$9N|z OB=86v5_K1„,M/U=Q'Ru]ZG.TY֣r,UPk  Uŝ;q}y#*GV^ @hܥ|5u:L! '^=l=wAlGm-0Q_t̥!g >0FWWݻK ;N BCFY`8CEP0]Vba{ue3q9kALɄbU.1ОC{6gM VE DAqq2ht9B|&cD6˥!mRP-VL>McpF"V'9lk!J@+Ę* -ќ<*NOr#΀rkGD cd1`j_fv7biݶ=9W,^ XʓPK"̒77gd~!E{-%leK1]E3;ɠ!umYwhv%sA$3i\&a]Ʀ!"sԌP" ʫ^e6\@RehFDa+q@D Z͈I0 *l",J-T;U.Wv&TP$ '6< ת72lz5ͩǃa>- _G.m?f<RHФsNU¼=4ᦞ5D7\dRuY%qa8+bIZ"QZBP-,4`8[q(jE1TV;0c;KIGL[cnϪ덿6[Vr~ 1G $EU$ZS:QtuAn:(g #ȐlK4?L-3PF"@0 i2,֖쫷~w+܁PT^j>cyhb?=J:ba*|< ƁTXj)Y ׆QpC/c7)ϖ|ÿZChUϫďNB]#$?; T7Q+Ђ -yfS)sX1Nc/Ad `B޵Z !bKB 'tNbb֭'; ]W^M@Ytu-nxkpn1a`M9gN-{iaVQJgO@] E/m* y^v3i~uAd 4O[uD\"6ij2*a{\jydKn~U3$2="- \13끬dN\!B-"loBH2}f ؊etӾY&pVs\Jk0vFdeOX;rWP>fDj_'Z6hbGuW|A}/M1ΣM}݆b3-oX>#+.a< iSfX!B=+ 'G.2f~oe讳F!QVWԞA]n*C1cK@Χ-JEv!~Na$aTz.--%|-f>Íu:45*7DҏaUO;휭ShЩ_Ò h]LsA]m݂y\͠T[ )a ِN. "+N@E5I:@0l @YY8"j6lC5mQ:8uV&#l薋>$QzW:@"¹EgJlհo|px-ք(w0-UzK;ǀIt !ܒ'RpRuD[ʎ̰S Xa4|௚aNä́B\Ӎw4tl+pT MHe%'u hAlD_K_|BxfsLge&ϭ|?cL_)z#\^YsK|6*4 o8]W졶S:vage/BFd[ZQcyAm"]:gP}G CZHb/l.jNB`T*LifC;ѐ$^Dú~pQfcn;k}J*WՉMOK{іF1ɳ߿kd/^Ed$WvBϼZ4VZIH+$p19ČIeY$.T¨PQ߷L5D^A"l'^Ep%_}3(| Dy;c&g7sEFRw?]6RHb*ZS7,\6R--D>w4N5p~M_!KD-]T@frg04[6%pR()҈eXyCAToyO# BݑdaHlh|g-::}Bb [(ڗ7${~;J*lҿSNK-P ؛?`&v;t,E#D5M >}pL281s=B(o̲U0JB0ǜEf+t9j?/;ٕwn_H&>v14a*m7gK,\Ê"W/Rv3<5tq_~“ja6C_l H'Pm7,|Z2D/$QӤAX8r7aWH|.SzѶ c6$O.Cz&鴀KWyۮaV0}Azm*KI|f_Z=zq|RƑQ4U ΓA&F0B2ǂՎxMEtqvN1"9` G&Ђ]sznh}5f  ukpZ%jGkt@}(V@#2V4JSً;$Z26|d [KҐ\!(N矀?N]5܆xfMCmyKQ ܄k׹0ݟ]e_P}p8гԪm|ESZ[;YtǷX#s̟X,츺J}9R`~eQFRMȌMeAh tj6.qKcvo: q>lbt3FT%#*SPiC ;Ag|R_6FZNOZ8BB(<BE<#+W|k;5fLN mZE<)&@=oll^*enF 7F'moH1.n޳[ͯdT?J^"?i[XmZ=ᎿПۍ,zlT> :%eH#FZC9% ,AJ ܈ ud3}_?6!%g#atyo-娟=Ê{Ѿ=CBU[W;1w.kߦϮV@qW>?O<ͻIJwwšրf?j//&P*C6oOԭT|D6{K7'[ `@^,?F%+(“B 5r˵Qt\]I%jm#"Z2 (1|Yޑ T.)mjq?֮A%W?ϥB4%Uck`!3$%~0#q,?o1ܵhߡ 9N`~tLdE׽63pتr1#mf]3Oq7H}:o[^ff(3x%-4aR7oZ~Ҹ pElG䐼ă!j @,Ҽŏ#`E/(tЭyr`ֽY~aGDxaj"3S[O J9h;u3j;R{+ťA=-5D;.N>DBbcrK`$p(@ Y{up3^QL"#qP|WM.7")-ܽ|}>D`X(X< BSByLv%GP(Pϗ%! Dn[~!RLW)\vzGUGK5d_j쁳5 86(P@0H2L|G?!)r.?}T3?0a X(.3wiĆϵ-^S+H#<2-ga;KAkkcԔ.?8hx|Z20_X?Q[lvl%UչXrZMG3j`@P<$I -)8FGolfuy=ujbRF4*EAJd|/#_q'OK+ڹYc) j5JYWR~nMĂ DU޾z2 CN=Qkw#V}2k Y;-6 ([qc̱sQm5h .:px l(UY\ `GWGױ?ΏXDk0M0rh[LQ3,ܠW~=ˡۉz6l`NjGVˎ < +l  Lxp4\CQ ɐMEM@/}繐h'UȲތ]ָc0J}P4>xȏo6tfs;Z'xpoK cPv sHi?'.`bՈOM坞$RA댹aH. dr;ڪT 7;h3uw a}dq1놣PXn:>'MW|zIó6" (dgIXT>hѝ s7_5we~oqip2 @~ʹE+g}TB+Y"%[yfz㑊MEvQE= 5g,daN:} ʼfmA?eĵOQoS{gT=Hj[FşZŜQ}&(R& 䦿P\ThjATeWRk.0=xDC~W%z.XWR^A8wc=$3[:(B&@]O[|{}8 fOm;XIM׶!Wb. ]E#BVT:dsƪHࢆ- HpYJXxM<OCɥ(Y BL_C$&qպ$&/뉓Fmx]V18hCx'e!~C7N޻>T_k,% A/h5{gHyc `ff̡uGUsd!:`7?`~]-_h|r ` 7(OmAlj%.q>}ޣB:c^m %!ŪL+P9mNgү A"I||pXAq^Ac|@z V3?&t-̉eU֕ݦgÿ].ͦ {X%P vrq'#x%Y b(i4Y.4x #Vl6#'}X@qˣrsKjavm'/\o!9Nm>4N>j4Ϙ'(A/@yKr'`H`N>3)Cc0XC^FM1IJ*rE,V: eHERCrKFED\9wFl~n7W!&4Xs6wT83xWJB^9{w촸tM$mt=5STvaPDonf6ZW4.=.ѣ(Hוxh T,{XQX0hlgqEF q[I<+ Ca2j$wuկ..9M]V;j؂可K>$Vّa#1] Bޠ&RKDNѓչ=╚(oR=2rlM0K@Ag9̾N&A+mgQPO~z`SxPsݵo_P$%1`8v-&ÛaG:Q^Φ2aNg˷Wr.@([L: Os&y'Riuac锷ơAnη.]Tf4ﷲ~SKZLD;xܒX !>{Gmw)3j{xC|m@:蓈gCߦC: vdQd9}b6Ag?;u}?G|x:.zA:W᰻P@?~+{ւjZb>=sX*0I=Ks[?_g ӄߋO)|Cݿy7l6 fB\sR4ɣg%YZGdP>yu1M A:hdGߤˍW PbƫBUMSySis> w2,dli&I!P , AL8|DiT .d͵~&e]&V! r9Dat&&6.يٵ"y"-8ӧbpQJH<-梟Gq 3N2[' M{v+J47ē P/̅4}{|ān߹yof8Xs`.ӊ%@`Vo|Z_q3CI^Lλsb 6cl'm=s(KZM:?1>gw/RiVKe$Kr dQnZx.#'lI=i}75Y $*sARQ6%+ٷyjzwW2)]4<M-8)ni`"-w eD\)>G'%yЂ䙋%=)\4TW;+pӭʩljoC` YЦ78*G \gA8CT\,K ^ QK{M+Xz m]ugA&} Ӧ55PFl`?>![<$ɼT,5~+`5E9Yc(9ˌ`~]-J:_]AncQa'./öw\etx*8XuV mJ,"Ѹ Ng jҐͯ)'稰1 \yKvLh.}m8#VrOj썋u܇X4`4"DVCܔ]v W蟘?dٞm#DM.kуD\wy:^}z\= bA?D_ TC8(/Ζ_ʊCi&&%a6vK oz-6!{3` ~ϗTjanXBְ^#8sZ"/Ն-`<#ߴcm"71tQ/hњ,w.5?~;8}oϴtWtv/ӛ=U$0c''D\Oábm$07:vN>3@DYE9xz"CAyIu&b?/tȓf0`V8J뺧XA6=\kVc5>p8+l?XL$@7q伡:)WC@>3nwaQ`ݞ_ 8AaUCDTɹ80NƐAM5f>. : פYR9Yi4#[ȤKB~ńsZԘAAe7X/6ff ).ۗ NRZbnF\jZEG'y$M=-TV:3V<%p OjCqMFTb-3c6B쁈4L>t5hiXS@cx끕a'Ea@R*|Xqڏ\ +{ IUtVf P D8xsgbBOtx=UX@u+6ʂ{lIsE01ī2~JDl!\tuz"G"(k(V`%tuȟ m3e]>)FTkmwX(-55e袧m .H,QWxRmV4m OQ]CdNםe3R1:f8ܧMb>%/&߾œy~VyƪPG-0wW<bIHKPaCc4膯7y}Т(aRFsL(a膾 UmJ%WWHwxܖ:t3<]iA.8;Dre=D ,yƻ8}LU*)8CdzC?sKsykQ(O[Tʩq6cԓZn/Ɇ1q՞51Deh"ĐPV{GyQ{UI{GLr*X_x7<F Mj?O򄥮8,TwnNP2YWL 4C46 oxc+oD"?/%%4B^ub],~#\KXҢg@~ks%n5yE:ח~Ԭ1]IXgX`+H7sy<4|bBgAAǔjE*"Z!5Easpw 4QyVsKH#'^d1#IGp;]nbu#"ŞhdU ^tfe|l+UͭBg~5 )u>-W/[iCG54Gsv 皆<∲BMvIhؚ7>~ІCMI",HxդS1N_ӣFku+33 هOo(4dco6e JD_{h٢x6T?#dW7]JwC* <$bա#XeǤ`R[#3] ^xĽ t0%-Y)1][q{P5Oo"$aDc};Ty43]W:Ds%'sgE9F޺; R\Jye_8x6Ŵ~Bwy܎ 7ON%N29 u]x@EjVl\wxr/5"y渡Uhi/ҼF#$SkS'|cg.Sn>T]eXG=!.Gw6.maqek'J{ל[I^NtO$lm$0Q|qMZ;oix妸RmBs"_hq[˱r`to漼?%8=Y^Wf)۶|"#C o\ժa$^ uQ"|Ǘ$,ȼ *= vpV3.0ܪ5֛9v)5n,}r LO&2AL ߲s,]ŊR)M侼]kgIiLdT:Wp3jqB|Nalje =* pTigFO>@TYmg2-G,;t?-)댓v),[}'{{egY!`jfKJVvcqy2{@Rb^@GV&o2qk*HxDpd6.F0"s~eoK7쀄G#bzN*u)0F7i:eӔ3s~޼ ؂_{OpX %Z#C[Ӌ рP R ! W~+Jyٷ5l]FEPXAb_lh DT$f&Jcq_toǚxc/H-\z,1sOev c6T[j]\Rl_QQ2!sM6xZ&+r}n ^L]O"g/O vL -8v ~̞o[~䥕|_g犖\5cKT/33OhVkqϓ\7CFMXWb;k@:tSz&X47aaEj<@\ Cs*_$,+TKҟ9H0 ?t'y!j ׾bn06X#O4AgQOa@xUKv|,!5Awp&3'ʮh)l1^Ml|y%g'$tIH8;bmCgHbeFZ{FC%LC12;5yᣭq2jĿJ9.=)]0i#]f 6|^$yHV Y X|hr*2r=qlj9+ְ{BHލjQ"|m_MQ^͡_( QːՅB" (2ɬFImdur]ysoË]0skQ=YξM'rjPY3wҐ7IҢV$c@gmߏEw: yԠѬ Rd|8 ^1&e?)NtCTU 9n1p!t1I!ZY&{be]׍1AyJ_*ԭ=Tuq#<aY> $v#RʃSĸn%j 'M2w j-FAF# ոDzV==_-gx{*oή  Je v7lğ!Uǹ@^a=ӕK.H¤1㤎Gm-SUltoG2>kQ9&X!;:SA7Ww =pªugS50`+ҹZ08OC";X-,ix$8'~0 f~65fL0Ofm@XiwZhd=E,EJ\, [,wP ,*i6qpPS ,ưYeBLNvٲ̭!6Ͱ+ eNLC=/@͍WIOo[!/oPf!˳i`jIlwDgZؾYq6)s_ -wNШ%-2)YCq K"N=+\k=?קu573y=ףÉ&п:(Hjӆ*e/;c6#cMUMڍrufծa j(7jQ\ [s'^q8.D@.#xIVFQ\l WxILKNծ!/9Z' 8^N<=u6}8KY{2yy֥RaOTuq`MC7y$SW?.Aގ)'@R_G& :~fE{CN;'˜B~>rDGJE?c~B>u\刑ZeEu`srO[Ԭ|d1r@WTۇfg7B;'NV}=ZMp=~H5k 'PS +fZ;yar%y@j1\,7 (z $%E8] +ƹn?^α:Ղ^K*7$-:U@Ijw?)+ YHnaAq]m^ZUg,,lAI0 9S~|G/CnhM}_nmvlʚ81##78xx $$r1))Pq* L <^N~6aDܐeB*Ө=A X/DTM3*IxUo-!%ߣd mB輓ɫ~Ñv[JG¼mf\(DUR?]s3*cETPDu?PMmE~4LqTKR9$1Td:>c'nwrՙG]P_'!EzxYdB$y4PDڇy\x-! ^#pG\6ې9_ IWr !Ib_@/h+:9?G8"0;S`Y8Cv4$( Zz&M;hqyumh8.i?Wl߀8)W"Q bKF^*KmEZ}`(9E^cWk NY lv+))㣩?j,ao)6ׅB) oJ(qZzb="NF04H0 'nVRHTR UI>3ŤvUfj5ktR%wc+Z^:1, 0{ k6tw18QJ󕀖Ɲ5`e UD>IZeBvOiey:L(i^Ӓ̌[מJ\7l$eV{l 7*u Lfu(B+J@W)L̪^H Tg$.=Wid>DN`b'7Q42BrHËlXsO*+Uik5 zbVel\I}pDVuC#i0 WftNDy 79E£שWM[{)QDw5 k0s}w8WBqзo wTa?'/$~}!K@ q-ŀ[b}}efqM#w y`) UpD|YS ERZuxh dBv~,VqtXNxUr4"BXD;auu*z&nF*1) r 9>X u8+Ab9 S+Mh6-*.-ƕb_0F|\rfjBMȒCKSCD\O]ʫTYV!?kPU|f'B$Jʌfm5<!>r9%G/[3j=Pbߔ #{/NRBx[S=F3S9EbyQ_qEI80q!SJC{hz,4^k8wy$gbA3etGW` s"W4Lȍz g 1'6]"ʩ?-khH{9}"¿'EN9>_0B'!{GҨSU7 77Fqtx (;*i ?q I&.Ll0H-9uKQ CŖ.nh)WOjjԳw[iio YGg^ "?;%? N@YX1UeP0${\X+Kk<\=W^}N_*ҊcU"1sg)uے #Db"3O ]xf;q#HAvV}!}?`Fm[=t>JUVMR˳VsK,=踥=̎˨dQdUm1dnBWne[9uYez:"]nrU VldN҇UÐVAbO 尾p`GBHzCjO3Zw\ah73R?Yn[!@o.3k A!03toӮ9[Z/1>>9+!Yk% c:(񥽜c:˅BoDyIξ E#FU123/}U;Gq/3@S!} 88>+D`Z$0~38g86hT|oJJ/9^މ^g Ŝu *k,\1J$eYPU`G{h\L۔1A؏ ѫߑqֵRp(aDcdRj:4$/g>pSC4ώ,Dݚrm=Lownp| HڣFUy!O,\o7([ȫjW0&>;p/rh̸!ɎP'D{e&|m7nB<ْO@Z.ELXח7$G|TY"v 5xt_{B`5J,8+k`+>BcNTwTy:W1Nvˌ1}ho$;mu ݵa1C4s4P~m& N.:(jxkr$O /Bއ}ou$B@&jw&k-C~v$ >wssOF*8NoS׸iX'_o:ڂa/ʦIDw(V֒UVO^3֦Q>r"m3L b+~q8lkZ핉wJf7'G=czGIAp'W!ۭ [fhwg+߽ )V!70EEBnV{y%:r#2a9Mn0vꐡ(˄mZ#lau3oK{n.ueb8Ixy: '|sqcS˯ m>9/Ƞ5tǐυ*1̓#k!=C5%Hep$I9"3I]hVԥs1Y5~ ABߦAz=e}7UtntrĺIM(U(|"ʟGp#px7~!Ҳ״;nqȭ6\*"tvڴ780=v"Rn4 0[=oï&o |>Om;8|1 b:F+Msv{|Y8YOS~hn)ű'|jQ?b=M 56A=_a.lM -SaED  @ T8޲d`a0[kqiQK )Pg=zK 3Xr^͝/!M蒇j0f$r9No 2MOu+<f^;AbF ,d'V+ۢtv󍨶!¨3iP Yߢ=i%[k՚ yLhl芷hRin'g#\>@SgV 6*;iVZի}-[zbg8U}+ W?!*[TcI1+$V<тG-&k2zx\co4)=D|?$+J|(cbxs{ M>yc8^p7w #ޓ6v9i'~Ab[b=:Lh4ޖ#ͷ3z|t`A[aE(y1r8S njT\& Wi2zz߶=Caؤ`ZGlJ?ꚲu\Zv=հ- Valbie#SE-󌙘Vl>!2Rh.\9klR"z ɁYvx:bQ%Nf{viD`,9Ԏ{ e >RoO%_a2{e< L݀prG;t؈Y>D20,⽧Nv;R(3b ͢bɈK$*L؈=R8/ݯ4bXԭXIG4o[8CqO.50U&}<}cݴL-duMxE;tiW<Wh>CV1)" }+B&ᦏMzÑN~<Ŀ+7WE5Nѩ_ŷ:{bod_k\] UU[スz LroƞT?jjZs >UJV}206!=seE'Sd.#xjj,8=!JA'ȩ/ڎ^b1_@{LtsB3=y88k RԥriL lH⨢{u/\+^搁bưl x ?t:ǔ#k ty9C`+i)qJXPy6LY'k)zոZe|yopGĸ*rY@:ӻI#{$ȋwl|Mc2('b!bu)83I( w8Ꚇ blAZkGG -=Q|rH~6w&] ϣbi7&3 q1b_NAЌ`2p)ƑBo{s Sht$Aj8ʏYƸ1fKj^eUO# /+hBGWY{/RE83mJ=x;Re/X]Lܘ+썸C!|4 @$U'Z?!l} Ds:}dD}Й$P{ҿ0ZJvaG]Vv%VO>+J2Ia3h2N ^X?@`(Iptڴ|k.r}+tVm4Cvxx25nhXQӡio *byC/k|JGeFZ/\Q& O/aޠX㷐fAGQ V[#oaRC C"8\v z -:cMR:n'-L25/ChúKԨ4,̘zD ʔ~_n,a2se۶:JѦ j `*L7?*%@L{%sMd*.݁7IL6S};&< %&eyDAHBF 8)WhעOs2irmW4~8EF=纓u;d9U8ݤ#lH_LSm4 hΏh?`Ӗe/悱~c(:􀺯=҂[ZqWK6LRws"KZSu +P7l6]x+xeKC<Ǽ}cg^4~{ ᐁFI-Z2^bI!# %}^ký9exs|Nz3RFΪeA&z3,Ŏ7(v%̓oS&>|&Q[W&Aޔ7'q/6k`\x0elM?|^AZOQ;OU&=z0|t]T:MjP=,"nwSVVt'K01I(]m+7 {n&ɬ,wen a.olOp^«6X~ ņw0j .1#0.*ϠձFSnh;|"槾>=9GcƀO}sS ,6 5*W .B>Fuj, o#S(FH&-,J*3mmy}eR3 38o/rOT%{I k5-<ƷDKwSwKTouN8jI-y^0x=&QmYbI*vg c`&bc@aӹ̚} ʟ>dz 5 ouW2O-HϷF7J 6t|34}JǣþNj-xCF Q-E++{UfZb6M1Vi`Z q AM Z~;(\0 å:$kV,-.8 @׏P(&Q w 1r,BNVA5wJ*vnrօQYalR WE|G Q:St?ђ foʎڟ띳a&,zF IOd?(=S9O+jnY.;' Kkq 03H9!OOLg+9!YfHwPYjSI*u}"VHV@!Dhm:96q)-"_?$߄ hCz7^*V[ՎL!&4 y6@L2| "LXdžϣokon V>J~[r؜  /8xܵ!퓺&*\V_M.{{wTTޏs&lBQLq~E3׸` Wގ.NO\Pj\R8uo)\Leڙk}S4q= %TFucRVG?29e/NTeD"cr?~퓽#D;KUuc'rȩ2\}"䠹PTv=3Ho*}"Dt$%a5?w:-e1eeeS(/x[)8L+1Hv96Slj uYgYt IKoB:-fԁ` .::C2E3ǿtip[5Y e*@~%z"^2Exfͱ#.u%ϫũd{ $fg@^0g.Xc;p+tL qU<ɼ\e4 L!:8H6~BšׯS)xER`qjX:Ub7/`qC\Ő4ÒĒ?G61i' U  ' ЊDIHHd!6gZS)k}wG%8a0]p&<z2q ` 5_O,{h:UdEQ#fĿQ %ϼ.qeG賰 P}^CGZ+B>˘#h _C$vmK'͗dq5H|!TLؐc(" _e,Us ^܌ab8~[Aٮ=RX d$ck.z4BM%+ʠ H?XZ|n-0]A,ŞF*+-H9mҴT6Xp==fäj}Ye- ([!i _lSLޠ*g׷xRcae9| {LE2qTdoDHK*7>d& Ka)!јbR4ƈN4XI \&ܾeS!SUyP),R&R- mGi{!_ŗ+K1P-Ń,F<@kEQM?xЧ q|&]1-(U]1ҹN^9 j?nǢ|ֽ; T,^<a,ܿ2I؟"|(rߥ{&$C.CUn#K3`|HAOIP`p/Le9S\XKO}`r% ;̈] PuQ #0ңlZ%P-B5OS nL1ͭ&U[ чuӣ>{9婿`Wo`CwiO:3bQ%h$B!ڋ'5V8wV^}y,x}v_Ii 5I ~z ޚc[f_DyfgD%)Y]X\PlPR99paf4%p4Ȱ$~=wwt@*+gA|+čDsC ruؑٷ% _11}Qe[Ь~% sOZS:cm ]x*t!v+ lVCJdes9g5XyhGm\-GBn!uq]L66,V b+:t6Q~K^`(96 Q_4 ESr5bnwog5:j8E^6+ 6o!%q)f_YM?+ɸg~]l7!n ;..Uuܰy q:R|4(&hoGhP z*W$s -S ,.Y78uYkKeZszM[O$Ζ>ͫ]Lc4 $t'dR<O=8RdE }ص@ϲI(bqi8\:aP'va3{ʞ_uxg^8mcnWBQZkB/2Hy@jv-#}K.'B$$rӉOjV bZ_{mRB5Hz[( R' &jP;W9(AI: 9[ʯ7 6 S( D_6,kFl !"ʼ\s &pA`ۂ>)JrPFhi"4$8V$YewR/ڃ]e,آ`1E9ޯ4/Qk[ LϬ)G FrWGgR}e3zvL ɸK"FߠƑFs$1?YJGa9'0kF ʧ{$JbXފV`f% TA`29"m"NNQ9Z*Ygֽy m_dTL:z,7S/S]p!] \ZRB!huٵ"}\.vۼ'}7LMʊ xZc"0_mmkR{"aBSPM-Kz$eP/w˫T#~ T;0'a87yG`.!!LK %K90GaMud6}펳"ԌXV0$Ɗ\v.TNTՔYHyT4 3Q݄%LUp@mdNE}~ uIn |NDykiPv#@[CebM*='=# 'ܷ@.No8{Rx9S=x,Ac1G`zw'r3ΘG*pK&idcRGӒ#$L}IuUa;@&<^ks~YDُ_Xט>M,sCj7H&mM'BJŔA (纻awJx!ދo#B?^~މQx=*̊W-Mܙ+s;h\f$)bYLcAuϧ1^<4@ғГw\W3uN<~}ҸR!i,;T8 bYA pEI6!_IԋPMĜ!,+@Lw9u9ss MDg!QكyGF:֔4Q *B},^Ne\y<EVS `Rآal^3\vsVl14(ySNT0~ܿq{x Eh~7ԭ""5a#I/\!8!{M4~¬nC5wY8=8z|ư0/3RNYuq)#juR췴kf.ڞ#|>.^) &3N9NP&50t/} | q$ bWR/ nWb+QcF=L &y><7G$5SכY+( p=_+-ofx j—`i*X5*G7(+xI lR N%]ZrzEOxӪdQbHM 1("rcydG@k* q KkӊI"PJvQnM?͹TVgs:VN3q*Lj!IR[{z;͐,:m%?H^,clѶ zՋ=}ju ۘ׸ ̎2oi;FytcxHPD/V%9{ "2ioʚ>(F7y]0N ?QtyM-V(M;r*eϓoU4vBt;Ia=jS`EGA1 hӦ߻ZU᯷Nco ]h3 Gh$;\HeE9ܝa9vz~vXǾ F3%>U|1*꯭^e)oz˗Qz]R256'u+v:X^vܤm70PuT]r“̐378s!o7"7"#BY >D_GB.x:>Y& 'yX-92ʇ`|#cIZtf}#w.6cP@?!{ ܾuRhKMHՀ/c b?dWɬ{/vai{vԵ[Qd[AXВ gx a) z1TDa=iiqu^c"p2sy.𼦾揻˂& g(*j{oWuS9"V?&+4֋: 5"W1Ծ+OaCq6á\,װI?dQXOxUw+FK^)6i ,ߖP9בޞ;7ZŴـکହ\{J|M%MRmo.gTzۇ;S)XI3"G RȞ+yr?Er$Kx!t\5dYYJ^/׶~j@r-k1:@wC_ڜq)!?~d3k=-M/?U E҄,Y޳+xNVv}Pl:`Hp9KW*[C_-"JhӋۘ ffT'ΰh6\۟SJy@ X# LGVItTSI;7#R/*rV03Ӗ_ G)h2͑fѸޞ=(nbl/SD`럍ޑ.?ʹdZ?~gh.idv0>D,ov"Ψd d#\&fTV ̩ }Q!83$m{%wqY/ \E/a\r`kG=5a!*鵎+W[9̩:sVzvDMVG_13HZnli$&ԌYc))VM0 p$??GT `3,P.b[d*ĪNZY3ZLMu,do4|?2pn372as&BڹHA)Wr!)}`;8;KW#w 4r9Q=ߔzp"r^ըV09Do)Ojc-fz` [SaԦtwxˁĕIUJcTvq4!oQw^\M`Xos=1-LAsѬe 448mLǑEiƅ6 K_[o$V2QYe˞Ʀ8.] Bt|)/ܜV,oO]}(T'xMpG" 5;<ȡ'$CEhK2,M'nv]%Ks:bw) \R eFye;b.)QfukLk G-ziK.?̮'m~BShE7peELmA*.ae-?E^[sM\ligCF06 [:-1CB)VKg"dTXywݧYn?u(/g_^f$ %C@6hD*g4_qM[2PdT!E"$*y|5sB-gq`:p 3PSwP$ZKD3|\oH+}63zQGi+;3<ɣ*(zN|W2,,a㺍D#-[pV~ޅ*8cmo-?1rPÚJ7ju#m%o;O4!]yb\+kMљ ނYfQd|,4N=)U?#+߷th^f\h yv&Fس3Emd8:O~X;/!rj 5MKi!%_aʌ'"KUܓ?eJLdhՕjD ;oS%ݶL32nIS gZFcm`BȒ9WXhTԶl>$jQ5p+1@óNd5?B{8XG13ωw{N=k$)PDꉏ <$bj?hf$:;u9+ޯPȳkr'֋%\^)/?2hL$|qқqHd| %=\ż+/nS‰ ) >'8hi-E(] v5_N˓W\ 8/Ehg :6*W% ':dU`?x>/HY /@jv*%]Ն֩иYrXeZmi F.M) !wf0q$S\"M"4z6@.j`[l]|5U<.gap}^ۑلgtB\] 1b G?z=&p:i=B6fQ>o;NI2"oNW2| .trj:QÉEŀZGH%@4xۆBmewv΄Y 0:@8=F/r_DCGw6KLw?[l\PJopIuSBB Fn>T'~ 0hki G-u˥z&ڶ+H2Gs=n\M9Qkq1>HfC^ G]psz<,=1țʆ1پ?U@BeT`k$R:)25~M@c =%**- J"klSF7N6uF5_y'QܿfD?4`>7D3gmGVMR 'GX"ɏ>–1NQ{4}b-[еC}ujEqܚhJ] <9(p;xQ4<}w{ť"f8BI1`V%#pMb$Egn”,dԘK^~'zߪ]gp م e*, r !C%/9FS% cHkEi&SXڣ+n=4U@nʽBi;b H$@==%n_uߺb1]]|q wM#9Z/MyfXUhFٯZY[95qr? +8aEmd5l2hP vW~ݟW;FFfj%Ul*>Kt)Qv)u#E]y\b̳-}{ 5lFuvate;{<|rkCɏPA[GO[W4@o4n9اS0 Hh{k`k2@[؜;L}&ZGa\J%>Ity焂=;S/@BRDsR<;,Ur5QV LHXuQlHoj2~wO 7(9P=df;/5b[~b6e0m$SAvb͊QvR Nlfgxt0J'fGp{pZ`%)s)-_jvE}x?<૧vCb'8FA9cM‡TgXޜ,8D(Zi%d N(GaT6^!ӷ1"-ft"}~8ٔK#RYSl\~nKpe!RO@}s5bc2[YDδ1R͌~\*ܠ _户o{;l ŸA W0b &goű G&$@#UpHɝ>Y|DUV(ԤyM=#lpNm:p<Uto}×)م"9*:g @4 *80ݐȷsYh^rgiZkPFaD!(iNRj@çjHN~C Z%?˘JޛK!_p Kt}Njϛ\ؖGgXET Y֕ Z%!]Nul\E!.ruF v/[;XjP?1DWŴaWKU@>+9N~ϻ<9~ʹZSyx$. 26TYYB?mWK@ SOjT$y%Á -Cli% 2RL@~CVW(dG@vEjչ CLRJng&#HOK1PY{X[u@1?nU# _ʠˏ;^v~2;g={͎U’WGiΛo?V$e?WHAa5qzSύr\"2v狡n+]CK-k>ElW:jgQ̓ ^jIb۫Byl|#ppZѪͯr cSWdqu烎U&(13 䫫'n)*+jΪy| j NʷrS[_BexDjQB.uy۝{Y?t$ۍ۠ {'ʜ7l< O,S-坃zu5O]fZgam@YHCW\,+국1iUWՀ{7CJwfd}wpgקDU5O,$%~_ #hhw_x\~EiAs2DizaVu26gU|HN?`` 搩1B?> 3ى}΍yOl/}߾V?Uo@$5O3!e0S)aZ`+s{K43NP?eꨭp_I\ƖbF Q#{)%`@~+5! E|ƯA3wsu`zan, n*]6^^mAy4?RQL!G/Á1r&= 8lD]&7HfB欯{"HrTY K Wm-`P pU+!ެU|icvgu/ݠESE$JpԿE3U;]@)E%D7}TwB#q2u1;șc-sӽȅ'F\s2 2D' "{la>Gy.RȰJ< r^3UWWnNt D~Ga8j2z2uk;zG[A*uϓIβX\_dߺg妁I!ڀ"uDMU2e)ʺ} $(R5ړ9Iq(k$ldĤ7jZ2˃A+%)1W鬣d3My]Ee\F*KRY)xjrvCu{b*Ha= PYwFT9FJdL/c([DDƵDZqxqXE*z^Ԇ`ė')%NCAҬ=&iHfս}=i+MEBbH7K),&؜*kg]0̗Ș9rCFK}ѕ) Q0E'vgFT$UQIKE!Ҏep8cHS0}tscy{)Ɨ">jՆrx6#s`{& WtB ]ne2MH#NHWeڌ~淞 y54AA@NҀ)_Φ),YS;*iK8' /WN}upڴ< qٙv*bX/&yoIɞ:3=FAʈSc\kZ$5E)0O;3|[ #c jme>]v_߉3Zet90V5;Vrebw K_'t4~{~uyO'CVQFUlS km1LHx3! ÿٰlkB܁^ecGWìxQa/$i~l;I~Lx’έDþU3QkaW lepSD/i_{8\[j: Y\ޙMXh,1E2pL68s鉲]+,:`fL}EY*($U3)+5?NUQb* ǁĵ w0d00<M|kj* )bi 㝃z5Olz(%RNhhi=8'eA Hq}mOT~F!H4Ye8:m@cC y[ Ϥ Zpq:ӑ}N &mwunaXeE16/oTi?Ğ;33:*7OUUJަ#iP WaEl+Tv5B 'h`AkUz@ YLhE_wpv1ryAp˴*з ,Ied+|&`62Q;侹Zv_N+euCz2tcp *-%;&V)FUR&&<QMОHO zrg%5|89du "Ngڇy0\/* LkN3u"O6|=ШՌV͚ n|;Yۇ1RnҜ\]FëqW{{vB`*LV?XC>1'*8DS.Mb}%O~ϼr: (]t %\ #dzewUm)Ur40:7ih*:ׄ'9ps\v`\G%|6sIi8Jr;;S FbS|JZƪIwYzΦ vP@2"kۀt:8 P0Eø ~?B9)0ۄ3ARgs'9%A NUR Q()-)׍8jVex#4Ĥ!2k5Ʌ5@ky߸2i̘ףF>s5V6XjM۵`Lxr #dc<YO$+Tw7 I?lXB/[/an~zA$] [8Xl9eƋ垷ZfN㕝Tܐ3`5MUQ'VA _ArdK$vag=TM<=*`'?,cX t(سL/['eVWԂJUO(9af+,uo8]\D<_Le;=SHAzWqCjjB=e4Rݕ,f 1Np6I l")#r PĈXYvSm.AZdlivn'{ PeZľsۀ.7l4fE6mu`y[nn;9C9/vE!՛ + ̿1näՇdTkΦ'PUFεŠ0.7 \T֚^DW{QDdOׅ\@e5l!O3 aZxa}pG F3PSƶ{1ҊG3ӇM fQ9Tc0A؇Le覓>"5*Xk#[" ^gz]0hi/J g?_q8F: %Yɺ;sMq{ՙe4TU0u6M c-0уj޼!#ٓg^⤟N+1՘7W^x -X0'BOav3vs*hA8:a-.kX{ :[#> >?rN17䱥,\-==Kq+ϛf$ 5t1&r`G!C{!r}bHESUMX\#xt Pqsޠ ޡ5&7~peru \n%W_\V  3}7 jwloCckU&C2YZl|q'`';K`._ʯwqM彌1߻+~(\}DM?9" ~$TY)[' E`O|Dv(v)n/W9 }1oTe&{qSNQƤXMR"uM TC]mʴ=g wMd.s+-6PkYRl o%Znd, <M5P8z][x2ĦdN}.K+])]I`&xט+,Z` xԇtCC}k$رK4O}l(VDch)QAd!62#wa:/E5\1U֞q#}UaD:J:D|Lu!6DYw|BkZČ36! f C K,=(5LB4 &`! ^%Zm&f=mj'yYP^5nd~C+ SeTe2rkJY./ ݾ$~lĊnG*`IIN Yp naDԏ")x>5$aNJE1o @.>>d j efdKd`5.v>xZz< +ő]DD|0bT](9V .5Ք筵o?%kdn`қLdpTԥ\}4\mnI@6cCb)۷,Bb6Y\ 9)Ч BT٣g0;F?w<-2A x$r iТo3#eaXŠ|) Sly!Y-`GZ#zϜ5#/N?o]wi<Q`to 5\nrܯ< ?}Up3*05Y%-f4M)Nǂ2\P~ Ω[c&Zt_bՈ巑t$h)) `@YC_zTD|Kge6Sg0uN4tIӊ- -й~X>_# E W?;6pWp n|_.slK@J7Rg#R˕ː:y=p &CxIՉsvigȊjlR";d-oEKy 2z K5BPyN6k? ڇKf͉<(6c[<Һ')-2!1Eh4sAGxh۵e4cp`!QI^KI?*2*\{ ?RKoINȃRz& PψkU)SpmUfe\a/B[\MDJ4L@Wu bۡT(PJ2ہD'jǥBǞ6kt m 'kNI]D-<5N{HKxuܢnXmHzCʴ*}j.=Mqvy(hق͔@HZl[LWoϘJ8 1C؋:W+Gg'ە7dm9xYWŜA/ @\X/VmFGrvY&kʾùD0:\`}+:]k|E(欃d|~ّ B{9vbUvKl#T{O,–H) ٭L/|{"zFiEiweϣ"߉($FF&9yפbo[ey{"Nಯ\>09._kWR{xڄJ(~-|]<943Q`n޲x*AS򽠑}x\#>WůB=sXT2nM{)$;ž@N<0w:N')&-Nw8LYMI( fY4Hẃ n7sQTrZ/| 􊷹=##z;^֜lIJ UCS8cV{FOR+ h;<;gI7y\v:R[^)= cl.yxoқ^k)PSdmL6aʹQ1 85L|*R !7yιGL:k̜'W[uSzq1Li]Yn?l诧_ +^ԭU?W|~!2g5@C `O4=a:*/X _uIj*hݧOġnkgݣs9iЛv F.y]q_p}IQ\+5cD2؜7Uwea&?4^>alꨘ &8G ixlTZJpGVv4&c6ddܻ)D{=1Q?t!A+>` Xho6Y [Fg hh½nó+Php(Ү‚Z bi[e-+qj ,+Tr.ugt:Tѥҩ;{4V>̦.dv55T?-lM"c'AD]R@0ѻo`B-"K%ғ-h݂N)[+jOY޽6vR +P*n1n7/Dr<xIrq~`$ai{`c,IˆmH9/ H nz@ d ^@0׫|"k6y^m3{yqΞù Kn$e.xr[AYh;8HE!P8tuu&vߩ^ٚ+3qKxޚ&v@8z*{~|s L]}CK<~U7[iA?:mcz$*΢Hhz/!)Wt{BYrWTrZѱt=V^i2A_wJ䤛4]M5IUlLPz?DEd[/_iAg4ؘ744?EJ5'+Ι6_'4=sue<~c x9 F7CŅpb ƍb4 `rIx̝Ey2Z|~,x5QO;VE7m- e S/Mw\#8mK!imj![o`0;:LJTN'W$|uoH4%g]S;fa1ygV}(rz@n>m3>Ԅn#ɍpq..O qߵb%bv]3 .NӉeVD^Iu-.X/\ !_Hqcu'v" z;~ \L$Hĝ]O{]. / K1?z޲XPQ; WԺ)ؚtyLo/'P,U.Ȱ`{lq >C|q.[&,2}sxF|æRb(5,wAtmja_\Wjj27f~@vY=u8}{R` 1hbT2 |XtMsu[L$c+{~O-?fQ]n?}b?{^ɣ0ydxj;CE`dΫHk=IX$`2}Q#Hw?D zK\K(,VFmhi WUxjpGSO$ɫ@y"݈@il<=A{C,)y k R+h[R؈5. a+T1ZEʴ͉լU鋯8x:=ʌLbryLDA~܇|ɔU&>b #+.'O< vefh*H,lwxnD=հs?0ɋk,C"fe͒ozo{^ݽoojj;#\Qm`#*ܮU4|}6! .kzu*x&[W|f!qKVen7N>E8.񻚡.5o@nR ;$M k;лZH,.I|_#6AV경Q+0\7 :}շ̵Ⱥܥl9Li6k(ޥ˽PM&bY͋i⨸H{b=h^1mQ]fbo~L}VV`W͊D 1jíqn柍 |ŒSGtey 0&=Iܕ:;YfdHwAiJ0m ;l|Բ*x;GeB%輯YT`WJG~^k5ib^gnjSKYA<r\jugj%8vSP;Pb$3RwNԃ t{D&4RQpG)>;"Lx0ǚe6򙪄wǟí9YP#txD>kזaƮ3oK#XبTss/Dggۃí<f xP2meu">I'?4Ty]5y & }|} P;TY wnoIxo`T7B< T\ _N(]K.;3H %(Hx76]'!VQl *T&bKؽ|:]u3-a4F2G> l*~t r2Ez+p6p;z4Hm%T.]3kjӟbQ~)L׫5m_ rܿm8</~7{Zjk+S+1]?oCejȸiX0)8O;~^aav[g\"v η;-س(~wFUD\M&w$rV/$08N<},3}QQ -&|9U}oBB4w#qdv״e|DS"<퇕ríO$G(#ٱWCىv:Ȏ[49SBrqF{p~CJ5z c>W1m~kjF9;ki?wCڅTq&jK*˟Q1Hqw{`Bfw=,O/u V{ zϕ&_&=+pz$OtmS 0}* s Aڬ&=hsɯ~F ȰutB3sNKx@eOXBn)9arSS&pOG{6=Uy{J:NrԾB̧J &kyv0bE\6ܮWj4 }h9(o|OV24_b^1 [j1z)rBAomMrlD6@͔(ݜAqI %YQS*[jV[U.˕ )c;1 d]/WgYأ2tޖL`VSVOfntK6d`>]i\T5zdd8!z JM2'FywnٱA8Cj_APmsm5<:78`Ù ]l: Զ K}r"gG:4`8kd>/ WV coJ!MR#=[y\Ž݊w,Ћ +ĩMQ[Hi9ѥķ!kCmsԊW:=d E')+ }\Rp:`N,+q+Sɸ40smz1zݓE/ؗT[c8 ڳ~v5*xo*\){:oN<:Q#b58j>8Io #/ pWTܒ5~DR x+Wm-!ቍ/`xq56$b^ɕJ^cm@ w.qt60(jJ_ ֐"&bZGvc_esD]`=njt,h]C$@?"JSxڬďʺ~ylŗtă ͮ*Gt w )pgG^;FFs2dz[汓L2.z,n{^(\p/ώso ){ VNaT"N$'/cI|>4l~dߐ( 4c%ӥ CIHҴY)fHoȌǑr^{K}iV#:m->Hڻ7~Cs;d is8U5-ְ qO3%ۇCB/}'y@;ueBc vMTM(ym@! ˅ s#Ny3P #q|כV8L#jh?+y`EؼJ{kM+d?/d8dj^Sآv*m:i#^՚fF@(\\sʉՓ8h+nh~NBSF45 i t,}+'%1pw{ "|(}K`Hh5?D;@ㅋFn"7k%wɂeA -uNpg-BڊFXh{84zj3%<)Ub[{n@غF=W`|ނ`; P\#?z4!Y}xPIG#њiDg/ne^Yfz+n{IEE$'d?{n  q,PMۉ^bSs0RqXi9Sʵ{/;zETKQEQ$w덥 8f>lxPv V}6D\v! \$=k e{Q;W:"+I} ]lb]Z"F{4}vI %|4잱EIYeaddvqn ,} V)kg·TQi~不⻖E oK\(h2.@_ZC Nq *"Md3ӹ at;Eqr-(rS[J!z9E]YDNU]%P:^FsVk!ϞUw)dϽ{Qyjc#\DaQ 'LIvR؇Ot`ٌsաw]%b*T+[0Hns joSDn()^Z%i 1~']4 ^apmn`Vm1_7i})Ul,&imzA8ff?iS0!'QspW?!C}39[z Ş3uP؛t&Լkaw0w?vR!IUA|ʋ?lä1ڒ71J4iĊጛLFXQktCo^^QjQ+E{u11.;]\Ooze(lU=\iyӯO/`ΈnJlY4FM*B n],/*NR(!A|0z3Cl w wH'˚Y&kT% 䠓*!a[Ȥ/W*[~>j,ӨZb`]텪z|0arPbPS :1a NO\-Tק޴sD_wFxGo @ӂͬb[Oqrq~Vxq*1wOS ݱYBnj{v,ꤢ~݈[.)<P& ,yM/Qu~r_ӽ!vX[(HȠZ}C8;UOs"b-=m,-!9tSػ|,s2+=V 21t!t`^3e4L$f)S8EC~Ȅ?Y3H~(eÓ6~si{aK mUO<+z0XS$9@u,yK7n&[@|OՕkᕾͱu+$z.>嗞@hۗ~2rW7{بѴ+1CzGݾi(pU".[A]ƉLaZ qh'ĺ! a.YFt>Dj~]{^^~*p=dBُW\%NzJ ypL^}۞A;OVqv <.cukO)%wB0rfU,mn&>axKc$$cژY<{&{>PJ䃮[üDtHbڧEOaNH5UԵ}8]!\s[rtNv xy6E s'#PW7$ŭdcYi$X& ]/iy6 C(" ay^KM7ۺ&t=S!އ8ϭXuW=7}'QJ7HI@`5YcT``#eRĈjJ_/$ެF=?  p=@hk9|نƟ($ٮ$ʰt%>g0*-H_6t"<3gh{+1aq,(RMJ_>] ͔5" \:Kxɱ  gwcC;{@](D휻*ՓHg<Έ٭g|/% advc.Wdlon*Ky4QBf&09iș]p ~O[ Ψre6=3 -:q9?վ' C'O⅜dK68Goe ޿Ņ?e6 \UUs(] >e'䥰piG&9a@~sFc-q_se {/AJjMLyoa7&<$:~c ՗2ګw«j Kխַj'q|ãdf*m' ̌kc+O5{* \ՙyiY{lM~x%P{qbF@.q t/]*L'QYhrՓax!* 0jʔWwzSt;>BNQ٫vY̜"Qkxxڍx^wOPǵ`.D "}4 xSU,:N(r0-! !C\؍a~" 1B&W2Χ. x:k S}޷.OQM_&gt=,+VP'Ю50! ] KE _hF舱*OnؖˎU4䝇h|Kl`~pqj햃>*'bJLOO'W@4MzH1>&I)7Yg kWyTbb鑍~i2S7df|u5@O'MnMvVoX41j L9ļmA.<'e3j'M6$Փ%B2xغ,~TB,Gẖ?O{dy3#66 \cgǃZ$$*Wܺvᵘ6Jpfլ4Pqb$?_B9'~YwS,40l*1ٔyQ"#Ό|%F#hO~yZM`uAf(cnb*bvVx4b24G'0k]:0viS#ХȣF8R]uɢΘJ,=)[Kx%ت yJ4G.hk ,%cLMKl͂Cvuq[ZS3A@*YAKdlϟ?YFM$GJG- Qd/ŏ.=wbQKj$+ٝo0EW5O8qJ8hCyYyW_A#K tO"6gE&r/@W~wJِ]s@:F#-3Y}`0ġ4OVZPIsvO DdC e^y=;Yh SpNlsAe exZ^{u#4c~ e,c/~?z{*W|!zrXj/tUZoijKrxS->q tKp{B3pUrqL+L.|Tҋu?^lUSe/񾸞V㮽G~ƒv7;\t{X%3988O'2 R>=^d/MxI2W&c+1P`#Mg֥Ol-lLk8Ih9,e%Ak@[:2܏hMۋkڍW}L?fޡX#Ȃ_\X8PQ1WjL[zڕ"j߭†9 > EH#z&#jQdA(ɼIKܵ"O @$904s-8IF/-R?08I❑` vƪ(ntYN)j; \kp7e p5{2#vH,tùz*/QsC⧨nneOhxbM:HͲU4ҷtQѣTo?Rr=J7 TUz t{OJ?s Kj37i?;e0F?V 汌ܧ'HUUV(r{ms(/+Y1%)_66U{O-2ؒW@a-#GB 7,Wڰn"p[,ޅ,юIfDΥj칅t`<% #8ZmU9 -hhՓÀh9kSL(n'3 N[D7@6G;z'RUfJ,gLΖ2<9/4c͖a9Fx5Kw7[9!Au++(?wT;2{i(fKqfbX\/P7^||4"Q@ 9/k $uw$c=i`,ʮ( GRPR(HR?)^*1f ~ +AV>m Orb I&\5hmZ}l_ܟ7ʵ bB48ۓS`*x#J(e$b 7,UF /TkTVD>%(/i HSzЩtۆS"iN81f\9yxMY["GNUf@~ AQ wk|e@9Vh=J42xR_hڤ0 dvAl:6>ݯ4ۄ,,(QMLƘϔ4a v5(a=P=`nt~9%ךG$W*CU6g T_/Ч{4]x]Ib*geU!ZI=H0ά@:]4`أ㬣h޿PL %{ "-G36 Q B6)IJ p"L\z J5SEI vOT#Db1d>G)ِ[~wp/xsRLj 8h(3i$J_5v/ Ӫ|v2:o$6Ľz t kJ"t EYlz ҕ&Z2lo KY g13$,tP7MG$N4!zI{ V$YN3>_N߼ȟ-p!CN"<*=W%$oE=!eIӼ:?kc$7=X8v:#N`։RR8eۭ;ᝳ , Mulmzߖ$Vg#3  82pH|۱9, ͱ9Whp)1pHx]CZVTe⵶D( A \|.{1RJ0hd^'}F:)֞3̭"(^%_ȹL'DAH6z*w!#UA%%Z3@:t,ùg} x[:-Ee_o.Z/cl'>INE%TqtXV! C>1PhPCCq9?N]-gyP[I\(7+pܡ#Bjh2`CYK9Ā' NtA~@\PaWx Д)ĩI8= sKה`֔AlvA7#n Gݜ e A}ԾǦ"NL&ы ;9c*:,-.aq>b`[O#bE r*#2lysUԞLRe/(!RD)8runyL)f[$+S',b,IJf6H.~M.bv'b.Ŀ`JjP +_/o1ჟ\mTf)m_8l~MMxjPkpba2^4X Iͼ9+rP^uxrRi5^B)97EUs)Vi5J,-xLp:q}AFb='NpM໹ҭ?KVe9b]6?"ЄYv́QLx0?S®wK#5KT $z֏_X?ho3;CJ迓홆S_.v*&;A=RnHT%7/> _C$$Ɣt\nT#ΑU A>Am>AEdkơ8զBPTߴj 5ı]eJLOdʡ2{t9-'8֌B7 *Qj6dmC|]0i/!BEW ڀwMU 0!acN[uYc./tV U+%삱 />̩wU f<+6of>1^XBlΡNh21]?IPw-z|ҵ}et/$a RDMtpc쿂1t'$pЅs{^/X&\EЇ,RW?#`X:?EY̺I]k7as뾄Kn8f*#CZzXy{Y(xI=K _xRz-jYOL\Qю_/`Rv 䃬0"Պ`R dR(p~e:he$rfqkD/} {PsT[ d?Rn>'[~/?t<[ƧǾ8&"ule /e]od.mu;>':d&>Ь6e닝# R{'+"_V6Ƃc`9*7'׋ !û5/LJy4o>怞({!kIoJ!kZ|'G[P{/!O) ht"HAM dxUOGύ'P<>alXKƄҌ^R. 3pBrRfiUcp N&_d5@v<1t1M7 tmpqI_JPTm$o2CKg]Z &BUY~O{󬩅ocNu %-sq&PrTx)ca2`1̅1[5Fa,rMeAJ@;2%ﱤO0 5Ba$_ELXi]On[6lƏ"ꫀv'o}p*/P[Pc*R5~h G'r H8H5RV#]TL =&ӅO BAgc{}7#Ҋ؆h1 <)fV{L^mmSI*=?lqvAA*mnǔsrv:o6HL[K71L3׬[Q#*D8.ꑩ3"E̜~kū<$ұgr "&I=}c>H$NWm1c*qr*#+9Bh>)c "c xlLjFfq<>>. >cߍ7s=5S1D䓟3wl!KZg5=S88u*p6O\Ef~Yor*s:*<֎wp^aڄš Pef_luz!qDz]B.΃0 _-ѽL 6I< I A8$u}3 ӠiKz"r]7bF=>+XPZYR?Ns ƢI,}L-L<͚↞CkWXA2@Xyf+Ct3lmT\OMZ>=ͤjWD!6" *[gQ32Oҙ:*;4既HO**IƂ2oۗ {rD:#nZsf4i~ MW/;蚴5}>(k@zfqK8*xx I҂.@CAP<5 I~""bҽXT"_;ݺԱL tnJˌ)vp0M|BGCilr`W|ya=ka?ZELJ$"0̃4(y}rۭU?_IN?;.3Dl֒ϵ[v4'Ժ<\@pWdc^'EV<ʨ!a#IJB!SV1]ۄ3@%>+gֺ ^>.{4r FgQ]}%3gppK5ڿ"sm}k'_{S We&`&k]ɐ,[(-mXf(~*-D`dUGylM˗o 6_́;[A`kK$D hZWfQ(|&0RMԒI|ܟg`*qA 'L~Nm,7dx֟BnF߽mà"\ UڪegREWU+u_p s9t,RSؔQ6 g_F厓K݈IޖІTŹf-q2Xi NVDž: ɅȅD=c>` FXm)Y]p<ɼ5t @UUvN`fƨb& eɹ$G6i#Z0ۏs3ؤydcȽXEue!wuIWlsvx}u[:V ˧D:ȼP N :6ޕ5hbwF9t  '(}V-N bzx;ʼN#F6-؟w-iƾp(@n{rEik}5k(EL c2RXjvmlkyd?H]Wn//)e;RP/|KAvGLȜ>غR̅;gpkF]wGwl#{g [:!'qILfTMe#:Ju`A}^JYid% )p.;jw~9Cb #XG+?r* r}L)6ҒpL%PSVgЊcH=O <7QFzK}os}"ܙwj7iyA("$jA+Ȃ`'/~c>=oȕySY 7 K2y8֠o 1Yк=țIvhB=a&FNkbH꘏"lcsH]qVvY*mAތ I]˓7N0w»7DJ4RjyiK1k  f TD@tOeE'FYw("M>]ITc=S#)TTgA:R"Xa&1ީ7/2ڕ ymMEƽ&Ԯ%zdJJvǢfgaCu=jl.ї1^nb/k; 0uc-X@V9ۘ xE)ANZvQ&-F~W(Sl3:SZ-q[^ ݍr_M@=wp'e۷=^XҜ+|䡑2w6OB/YcV#W:ȀeĝH$sݷX~g5Yk2mD64-x\+5,Gy3m z!?Ș["sk얁o"0ʦr#nO sHAQiА4?_ l_+sRz (9E1<3¿z6*R?Mp ~ 8Zn%IՐyF_̌30R½:cYqri6ˈPNzT?EdӋ\7瘰֒g-\gZ^2 B/&ᾳF}w{Ǥ_ڏศw ;>V$ǥjĬQBް91"&۩!K|%-2C jBFx'dX]yuCj)k\}t̐j`my}1Ue_!?[,}Y۳kKd [n]^ m:`@il_oHj@Kxvf)u>zX5dlu ^ 1'cYp%#|G,7dHG( %: #b/MlVNEwx pեׇ [hy"~Wȵ^WO?Lí.$IÕV+LPOe1+N6rBcCEX,]`@dYFi|u~'pI .V_ #ǵjg TD?!- ^wS e$kn'&SIdf x{ QmS גhڪ)¨g&IԮbjc,)rnd.gD ~Lxr`-%/%2)NUV|u >W2YtknvZ7?Hi0`#7e2̓M^H_/cˁ#[xQ!uM,R~) <<fv٩эdbWUtE̺tE E\g6\vUdn=÷Z2'e"` +TPa,]e [zvX!l󣹪^ *긜]˭uN` X&/7!| @`ŒݽA` 'cWcGN>[BnƷ8UrknkزQfݦeuuh|}u4)hOt)‚tyao*|+{:،x8>>j-TE")X谾iL|PZ"7g7t$4iX;q!KQWI;a1!im+)hm^gi0UDi}|w,'fTx[A`Rl|?Յ5nN:g )byՀ1@6Q]b\rJ uA4@͉-T\@`w{3jvkE*&#aQH$CHjN*VSr C;B{!ʼnh٘Z2{DR#:uMEeel>ea!hM->Ǘ!yw^K#à`Dg~:s b7N莖W}錉lׄ~~.i[y_o&+$QDzC`Vg.%ޱt2pf';z5g,>IW!T/Iʸb-_%`ֶ(hw=}(} G(P5Z71Aq. 5IT3|mtw:BapHM=|<}kG fwv"񮋈C%?Z)}Aga_Tښ`=yyGTͦ@y*5 bYoI? %v(iS^6|qDPe_ @0D_.%5hF6T} fe,6{in BlGS9J# ܓ+hK8ar^iO`+CnqWrGK5~j;;>-(7w?rX-L6RAgL\2R;V|q[) ?${;p/<؇Ş4RH׹UEA!V4_ gmZ {pՈ -zN1(C&e GG4y0{T 7dTy)a Fl3'%h)~p\*xyBr;x LGӦb:P ךR|KyUnֱnDD7RwT92z5VfKV|6اyPXu@_FqaeFͼCbɚTĖPtvkO, 9r֖x^s/'#[r ;S;EdkZȁ#7F#ա@v'16ەy^5 d0><%/|f?MUKzxqxJO%~d0W n7Z#RGLDIc%8r:P;q^f׸pG@* rŸ iC@ǂB!S p7Whʠԗ @5]Ԡ3m Lfq"y3۩ _c!ܛyRߥ6:]oD~zOx={'pFlͥ BN%ʳO񝕣1Se!= wJL5s\r NX|+.4'*)jfgKtږ6sпF &ϢQ/1+x\XKExL7Ҿ~z?] lHIrٗ.zթ $]uMA^k{K/:A0N{wI"~s'2yr@r 6GӌPDpJ0xH#Scc@@d- {bUzaryVN>w* EM+OgaG`K&HvZB< qYΘ+RC*P Mf`0Л6ڸaю,]0p[6op¬0{!}΢*>PWU{_-@) n'^ΪC* %DAgp%WwSYCp$Aq6_NMZz ,Qa،OyxBqN+t١,eCpX@THD㰄뒎Wp&u B5 MH= j| ,©i˪OH; [5ax085% m35tۂY,5(`ų|Wտ#10KZo I071[gXs)PXCqfv?$[Iu1ZoglO|^ 1ΞpO 5Ř3 m{Oθܾ>ì1yF{e}A>]rqd'dh0T/`P:{$ѽ?} }I>oV yMcSC_uri+QT 'Dp5YRyLqsl2>&4NeGe )A$G/KF K 8),~n*AeM[!J| Sd;ONp"=|P.˦6⦅:Jy`U1O0t[@rPR!}#F;OzY R}Vp|:E±bJoH>:So-&t eT|CN)ҧWf RLdI!sx{LwKZpNSL:;gO؋# ܋Ev+{jByʢYjwr|T̽s57䇭~+aMiKϚTW순VZZ~ch]{/ZG䑮:J|7{[#%P\fΫBq= SOܜ.h3<3ml6D"d]}5CO@/$9!⺜&  _gՆpƷ4:j DF} E潘 leX b;[e]cý bI5A ЫGRijD= 2s,6>Muq94x;[ +4j)w[CRrodD?1+A5Z'qIDOu@ XePs&:,$^lp9-*;23za uw ]"Ӛ;^{QAz嵺๜&Q.ZI0LZk/w/PG?\Z)||Q|R@ZX{WFֽ_|xQbҀxi\$veCcƃ AdF3j[ܔf ^ o$UP'kVg -( RL yt)/ {*L՛,;B8d4/ R zu՗6%J[=X1ƬNs:t,<';(^{xGzg4ȺmS~~jdg|0Ӊx!R3N[#P@ l- 1HM[ZWW@)3sLt|:"T:Nۏ;"JGۑ} h[ДK֠\= ?#Y)Z~NMQn>׳跥q{{Q(U}̯``>y60P;jS{Ι8U̜ V=C F(*Naa2_q^GoP*1hk|qĺ#pu'I{3{'s~Ι~«߿}TԤ:ZA _p1U!6&45Xp0+).8eh`Uitӳ D UX.,B(6ciS8~u^{iXL}Y0 JyJ0Sp^@';c,['g& v~_0AۚHtq]SB͝iL52 @\-kj~~USse=φX-Lq!˶iѓUmkHwLhتZ4~ViRkWNZvWm纟O6_ίCB8>9I}1J̰3D`d1^WŎ v0Փz3ӨO@8 <ӈȟyH⤌r7Jn vf~NOSf y&WtmGI[@BWy c^#496v֔xZEz雖1lIcZe -usQdF*_UuMIxiFf@^]V[gL5NQ\P?u~PtO1&63<#2#tiaur Jv#$89} 2;!&Y۲P}c%/NS)q=%tfS~3P܋-UGO?lSItfIݭࣽd4z }`-.v!63liJ C5,k'tڵhĠz.EՕޕO?zS'&.r;bEԵ}^X(O5uzpeL+ %=ۨr[߶s&Ck<8:j cdy-;]ED|@E)Sfl2" yW"98ZLtipr9<6c;jSV;*wnY9+dpV \: dyЛ]6~L4SFojqᏑzԘS8/}J b89)q @LLc䰑3)kO73>PPEkiOLB:H3?0 lw5*MbQ:H=1D3Du'DQmzݝ u>l3ucK'CBc0^WA-慠Mb#6D:;:Rdtt,2*.TnxWfFx8sRc1)Ýdc_#e]RԵAV2H:JpӜ=)_AnR.h5砭MJ儹'~sqH=mNRZ,>Wjm3F!cibγ] |M5Nr'>ncXm -(2uC+mæ}eOp=\BD_ "1kU}2y>7}#e~jSs7RtC?+f\%Qgƚ C6@I,i~Eʗiv94{C%]|J66W !3Xߛ)A]يeYLBnx"m V._v=a/gG% [l J} 6PWVYÀUu>hݙȀ//-[u2bL&@/w,YHrJ ZPb~oeϳ,P |p]u2ʽiRmRkkAsmtQYMpM7%1$!hKuhX2/5[uٷg\6B 箪/(1 d??ȯyZ=vf!JysQF37QbSo-mibwNI{[2:E:^ˣO>kwj(1ܩh ' xhں9`i>v>`{X7QrH23WXZ˻8žCuVӡcAPyp$4 +su߿̲vyپBΑڏȌpʏi[2cY*%$Pu b@̙B슈I$;14A,ԫ KyQ5=0mNwEs}iTem3 xrNVDb ӯ%i ڼn M">zOk"ۅYY:K E1WP:bGRHٲ܊֬?޵"[-{- '{ PK]ptZ{- bXZ ѓoJOqAOhmay+=ebMbhv>K_?efŇ HrBͽϤ?\ K? º%xVF#*͛ T%9RD֦/SfsճOWH_FjM 2BO*Yq/j'u!7ۘt+8|ys$ΰݟ(w|lb>ܮ(EB|)#c"IJf R;=3'=M {6-;[!!Q F)l `2ϩ.:7eSP;ثde n[WN:rĽCcAY* B]G3&SUx88.6*2uҌrpQ<[f[1 +)m.CE6ZP/ %UHXIY@b(cr݆'ۅaýi(ᷜ3  N}+5_Ğ:!:xʩ>im 6P._q$X#<[lU>5׻  &QO6b#pXn1݀l!j()'>ɫᙩ0ե}50wv,?@St8Q4sG{tSW 6ܡ#lDhT!ꙆtS.L]9[_tK%:7|}򑫯Ӓ٠ .g`Egg߰a}= =M/23CD, Wlj?'rkмdEֈ׿}S:Q*9Dէp4bKR@R"'zyl(.RRh!BjU3^߁,M9N'Úw+g>j2 $2tiGZE C\%u ؔ]gR&1$+w+$MBC)e!dl-m%qNz*5bۜ0@LjWu!PDY VbbT.?X6=Rב<;凑[ A1]}m3*x&^{uw@Adt7fpεGx4:1R;Pkb/CN "MCU7|R\:P#Ҍ[IYkk$Ñ]v'08q޻ȒH#_UJ*ZOf:Ga˙2m*ƮyI;J*k&y]~骲Mk{$ L;1- GA)agu*6Z Y6邔qۇddsCڳ8#}+;S7܇|Ҽh\73dzʧTh[ N׆;D9k :DfՎnfNg6䁂É@Fir !M"t5;\XpM[F#:g_ qłd%5J^}D#Z8N( 7WFW # Mm} ]," m-lJ{eTB}1R91+I8a)J@APŤ?#@Baffbr߲ZΚoPNu `pRy{.Dno pQ"\tfπ6O4ݰ.Mđ':#&9 ,lD:N28  DM~p/W\O4{qx@ US,X30'uQA\( x5qӵE;^'UpL]ڊD$ԧ-D۵xp/DHRaRvPLqA*w\Y[g&_%NB<=o}l$՗fF7tӨ]Ey9-GtCH5}|cgNed(z$2w(UƥoHJ`#C5*} ~}6C-p\p;~}QyFNXqb6>YHl5A$k1Zf΅7زg^vqFd6!CUeΦKӌ8'Jٔye` ~)YG zwF.OQlZp"Qq2u(Ж';BՠdFWHVΡkF}ZЏ'MUѭ5i[2v{&[jw*+J y>{.͌XKyݎ̓Չ8oLI,Mþ i3\m_Kr~QW޳' ܃}m_XѥAQڳǶ;z4}=fE}S0T. ٲ Ah<~(a/DGd ~1$.:HE7 Iɗ4h ۚPoϻKtjNCFVmc KYWPp }QR$qKCݕ%sҶ[zؐ3WfAo;^]xu(XiC{.=A&yz 1{h(;0n9OVd<`a-9%Y?h!ОdxMFZvwԘЗ ʓ DZGPf'Ax/7*ZͻreͦD#P:Vd>"ʺBsIq}(چ.;Xm=7Mm؃9 j1hUBPPRZ &#SkoJT)<)}-a_]%P$v`z*BU)$ps";-L6wIA&;!@ak<]T޸ x٩ޜ;,orE,0P"d9 ᦔuMCs{qR_!\%&Og_v9St0C3ujD 6{&2VDRd{\8i2G7O9{c"# t4kuBV=\i"l!Oݐz4>&O90WB0o6{]?brV[F۱v9d܍z(dJM$̬ yGm Gާ\(G>[D&:iF*^`.2d4l8uO8YDv |g1Y}sEL}/9X?דObtqfx0|ٗw=e11\093In{Ū =`ZtV4L Bf,7#>`/q }vV͹b:| %@trm11i+ĉhZ$<*MS JQ3KWtW?Y1#-a$/%wwDa(8޺ r>Mw:WO8NU3Dy7lJ"Aёiw,@@+$ ")rPV{M"߈tJe$QbK%uJx1b_&܂ E`(^PO'^գ`j0&sл@*\ɧ1=L 8Bg!Ý ڛm)=Ԝ&o  >]vl[4 MmSAB!զ] w҅kJጔK!V0@;wE#&x)w<@0=H^q"N6Ӊ]͗glg˩vbewFK"A |69uIGaOҩk=M֖$WӇյ벷B̯B42jov*:k%l ]6m_9#+|P=QxESaMOF(E\o0:mH5W>E!-ѿoep04x"e;Fe5Q7 )2Bu4W95i~J@ko|cI2 A&Bo[c8Ak's9 KsW?_`=D”.kk =f dՄ1+:ì &6ʉ'(DiK\d1<0C,MP= n.^ᶗ,# ;lg&ZW.yO7L(ҷ.żS4\P (r6ykS0dadf;pA9QqD̗ C`kB7~Ј #Bt G c\:֣4bƒ{AR\/ my q3 z˵6S)es1D6o a2X_SGv[1l1s YmxdUsfkz?I`!V=d= avtL~Orl/ؒ^E:Q[AEeMg\Y a)xOHH_ ըX__\QJeJ v8 ^|0.IK)(@&: tC:m%4Mx'z_<](&QwlYK^oN,U QEn 4WJݻ`\ FrM=-n;PжgI󃂤_G`=PE~V*S-sRlN2uT:P$f1rm_fS@E'>L 삿 ;[t.A%7XfZ6:g?B$9LJe*!,9IW-`ED Y|; CiU&lJ~tccr<;5?\6 <>G =qO-Z/`YRU%#H3Vؔ@jF̍!4U0t<79+5 PYLؾViR,)h)QRe:Q; "k1 `xJW`X]ە(V1=گ:L.,o ҂QxGjV\:2l O\픙PnaI짧 r2K%IgWpѐɻ~} 1W>JM9M e8葜\+ ?2Ka|<tìTg|f0~t6>q~"=R45ehrzGav52UƮ<<0/"\Pmu۰nh- g3qΥu8{<35~ipl|;/E[88'\O\A8{)9 ʼn KmL1n($G>ijV#;"p{A}!Ad2z֬;GD/oiO[Q%$ 5>S3/WP霋/0m 7C_\ Lud=M l0E lp;ADW{t$aT?6:{F߲&o4 Eē1dž@-h.NƳʧS7cYMZpnSɑwHҨ? 憹%-Ίˤ=:Hh:a%{jyC %BQ`op]T来3FF!y2[QQxd+~G2=O-P [wfU$T\L"c]gbpO tSˤ -z[CJ*nkBNh4qXV ߉jCq6I1&%pI?«ŴC@R4aZZxYUBY-Gq,D"j淦 _Bml$Ŋ2ae38]ρF*@Nޱ`p% kZd=V=W͢.T;>0&$ T;XePXm2E |+]l}~A]o cZSXtZP8b0ćⵜ]z;Zfq~Sü<5kX0-HvmSK T|ovzA oYt  şNI06 xg\xdIn:]ؔJdp)sxgU!OZ\LI̴ 9')d^wؖ<*&BֈsW1Ur71Pk{e v}Z-iú6BN*IC?&b k:cu޿E"ezUvj'n[+Brƪ[Oѐ#jKɷB,7|6]B%dOuVj7nPn )|AE@cd.F%ݶp'+-M\cxeS& d݊ѹ`)>D!We(t7OHIA@Uk\c/f v IA5w:'UUͷEYEQ#4JN| %= {6v;C柮sM";7YmДsvm+=uܵE.Nc f #2sdaIiJtOT\8]=H'RA5G:KAS&]Sކ1em( 2zLtv͝Lg_^]?{S;usΊid.TG>&ƴ|aC_(W[86;ehMxf߼ !{,G(74'(RGMglD~鴀hJU|WեA\=WGB⒒+Het )7Af-%j]HX+8*?$=wW] +=@v}z $f;l*D6O1q:q#.ã8y9(oVurКVCV|XL)WjO6Sy3 n#Zݚo - ')%cT,fn˄qĘE4U{tOqD Դ{/%DBl ya"&84﫜m}8j Bf"Q$Q꼮`4lJuOC#W߹u7ߖY]А| rn,w{RS,usC**A=9԰kJ 0FJ?+kL뒸8ft#8۔I\kgj|Lݴ%dz{,Dh:$-gxp6qn.NE)G1`_5.S^E  oQ4&F_ךb@ F{i[IJ]%AӼ&^=\K/|}] ޻@U[gZjtz?HD8?el9o5/TWȭfLĻɵ|EFCfTW ixk*Ni;T ϠT~$mס66 O[8ގ,P_0zLl:ksZBOm Xms |Z>6XAo2zsTRvyS~^9ƻ^'U5DC!ں> Y m 7.-ףtp̢$ IQ{IVM=;N.\${2VT'<.^8m.|Ϸ23+@\H7"L8Txl_[ye`Izæ3v:mk <̠ELd3jHvM~hs n(_ >7T {)PTBԮ>,\} X0"V.?\ ᩞ  %¥CDŽ 5&(\«HwLIWke?~aK,nHpJO& $WܳJ>ܲco[QVf֖Eޖ}Dri(moʲiwa6J(鲮R'!B\xujKy*;N(~RνO vOG>[ o\Hٴ\`ɜ#'VOzÜ/Z.JӦGC~=չ֑P ڸSe )N""M J 9֬%?kܤ5dY?oXa\Jr_ݫ0}X,=[LMpvΕ^wFj (n$aӑJnylYM恒~M\#t#QYe? )96)LN9%ٹ* s['2"dQ e@,%E nǬyNmZY2QHTtL @ipQqgϵ+G`X&)ՆG.̀&"f"UA^ IeH)VC.o!W؞ [H,Hp(b90Wv z 2)ɕ8`l,!1@ Uw, uBRTJA{f:], 7r(G NjM Yw~RgXBt=c4zsݔ$ +dGDmv֘.ɼmQ ^<]D!OKyfp{.ƕ9@Tު.BY y۹bU]Ə|~, Y~;hzbd "E/Vbdo3ZQMrRh+bS\Ð 4H)ϵ9;OSDRW$n)ա{$QoSyIDVee+1bKkv3IW_YJTg%a\7! * 7Cr'a.ZϠhM ?W@ M?be[7/ٮ戊Y_%%_ShB z>[ R!)WA;kANMendsq򫛴|414tX5@<4$rC2A1BH⺎[/rתlw Kvo-}gBºT3?abd,F낰&d&v4h?+9>KX DS mJX-СKط'`f~#uH?,"};un$8TV m]ȏ t"}mv9=|ӗG̘$% _~Q>7M]6'&+ u{y% ?d+vػ* p[n dR(,:`S$B38tZ}-Wз#$J cӐbkLSя"Yy2J-90u)=$T?u]٥R߁V5da>tYl̖ ߿(lÀ8\(RBl}ztb3%$F8S;ʮu ǡL9N`WV]ܢe>C84s vȴ`{:Dq@f]ՐA- MJWS@heCc$>lIg֊xF]xb^Dx G5/֙OJeX7Ц}i4_Q)&\?ҺGB#>w+]3r8݉*M}tg:5}LwC?sg<0z(ڜJ3Aݥ c`yfrFB릶?!GQ[q G*7˭]Nc5ܹ{}V<=NQ0RC=J 0-הwZ("\_k#K >Gv=W-OBd̙{N2_ch>4dtqPv/:dZZ$PEoT ֲnoSM \u o_w֋$SuC71[MwoYS1j\Vb׻f!}#s =QT@l;زK",;˨y ST-;8GMj\v:#2X &)QkT5'*!%~5/H1p^uS0Hej ׻s sLcIT=lkiT'{KK&Y"+wB58NyZ p(rqZ 6)E]'b/?Fuq"ïźgS9,4xD82?@6kXKj2icnx((5yD,Y:P[ Bzcibi>~9ؚ('%\'")WR Նri>^OYۉ}S@ӌKB»'eט%+x} ~&oJoƒ=cψ:bYB%{t":2%F@oՐ2Yd1;>ad[Zi]H0k4vv9tWe %ڧbMOۃ.{iz:9]"F{VT)c%Z6:wk|<hsYAn\V'o8.Pf\U)[p \WS7R7iJxd~4O5$F FJ{t ^zڟ{8(CgM5.60]FLb}o,1 h=tbFO0jAa& / Pr2pjC{#ZrxU&Ûa('hr_gy;E+^>dDؠ@ס1NMPthݜpwP$q}h)綦c$Y ƟRNNyL/:)/0TdhV*cê2gy$ 8ׄi+G-=l Ҝ]^Iؘy ^00-'cz(X/1! Eb 9d8yz3'7=V9DѺ:~AbNR]+ؾjL$dY vˈ#@of~?cU|'/Uekgdw`+f#+>O/M9r*wjZ8^ xqQ p?۸W[*~= 1Ky*G'(YVez#%e_Jݓd KUӚ:R5j]Y)P' f\  dx*-E w| /) B?nva`%q;>6A)E>B?hD3!KOq ->H0EwMZӟs3 =n/ĸ W4 "ς|ujR ="69E "@@q٢XZP~s}Y1b~FnUn$ yo'y/1_R?YU82 #MtmY’]9c sJ_fr ^ÓBI>3yctQ.ށg7'-oED7(gOc"F%EH2N| 좱QF0U֞mjKJX} .a g<7)s, Yr؀o %HAmdM0]Eeq-Ŵ~S;}Y$v 'iN)Qrai9^9a0箲Rf?ɑ pU+&T^Ef+=fؘpW ߣ%")* ,3|e&B Mu=PX?T7 Ry_% *W? UWs+; qkҫgw?dP/-}=_0'x 3%T}Z)5S1yUڡVGfNce+7gh{8Ї`r,Ӯ1uCӹ?UV6.jdk w8:B͞vfO3qYJ|ͪSHcPt3vIQ_(jmHjx 0cL妸M7c"jLC V+9M^ 2 7#,PhgڸWԧŒ1BGe$mGϜQ?(bv)`:lD=h_Pj)QK`0'Q$nn82z2 `DhwN3X <|YP]rCrb I_Ql{}䟙=UB[RJq_;Wߑ.8LAK(n)Оo A\W9k]Č2G `; "SyUsDݗBB,1.<ݚCeE?=ބ~yօq":vV8Ix- =}=u5yu8A]nv=t 'ŦfO[U !٤2G9,7H5rDHF*(E$zwРXP)K Id!F׆.#Ս- +::ߥAdn22N6 Kr$wmD}7ҺhL|i kJ=̹kJwؗ5u>& L#wsA\ۋx xԮZq凕sH3ci+w4֒\"Wrk]5%u0iۿ}8C,p>\~qv_Y)|ei9vhݚU!lT*am,GeW*5YƋTUϪ#9MK}~\ )TGX ?3ۅ`&U`)}e!jy4?*7D* hً vU"_2&F\V@ELŋD&vf.jS /WzģuVg\ J%D MfNDc\޴Fo]1`40օZI0![Da7ץul%p_ηPrǑKkmA2%ލ!Nhu4C{YtR64v_T0|P# L)ݪ;H[Zɪ" P6e$EMmd >v;gt-Qi3;&8jJzĒ^" 8Q=4Ky&B6~"Vײ'}/LѣB@8.)~fOr"R0Y 'QK=H]N,!xdԨ=h %B3ժ KƹOZH@v5^CDQ{L yn'ZM0 eK8ex5Q]p  Ro/Pz-:gw]7/NРfKb.-g9C$:K{-@0g '0=~$Q̳ƻewnܤCXL*' bՎJ޽C^c@pG+/YbEZ9Tw[CO#A`-2KLV9DD0@A֚piL$+ݛ# ,TЌc/ިoN3X8)*t ޝέU }:-п]{I dX쥐SOG[{&!4co H@vC$cu3>&XbˆI[x~4Z_VLhIh(Ш7U0v Hѫ2 8^P@&4iw@=LE2A [MT7L& hZU#ĉD`CEH`q||,{ɎnZ@?!(w.?}:ƔK\s/Tir)mXy|qea!8r\3IjVTWX a3&0ez+tyWT@dG fB *(dxx@ M^t߼p8lު1_'g7D\x-v?P.#]R~2z[Q\P춷k5L=DWCrb ~ӖÕ[:(J81ʺX#WWHsv5 QJGts66X0ӐT$op.eU+IF[䜹 \8M áٓlHIX贞Eڋ0UM!I.SVr4wjQԃZy=ښ[Tڳa%nwk+. 2px "%뙼amOD&V.?0HP=$놻VM߿oF`y!* h6yVu۱kb5ҽȕ7g }BcdsPxl$ APZ{]m-G 6v#3FC7C LZT^>n5}>@Xr?6\Bά y"OA?~֕=2 [#:v8hU)YWb*td7h!C  *:,UսQdE)ΜG=u~GUfj/߭ۡZ>Z%Dw QHfE\|0U'Wȁe \䆾aJJqtV =c%zBy!|f&JJ x<5Cz5-&\cAN~MYKNz`I;6 F]Q48aONg뎒ԙ@Pbjc]D "-FR$!7\XXc,gu<f1T^҅Y.+ƔSFh}"/ -c{%V*B<>wk,T@ &7\T[Omxf+{?#Ƥ9 6';E($f=\6,@$ phbN[sPMJd%:t#yKxɿOح0VX.xWp^w {{k<~`[]wM(+J[x%ܙ Zbi.B~;eGrM.͏b+@S\IìNRd%;R#3 *1?L玱 >m y FE5J-%7Z` C|G_4Rr %Aַ#-$" ĬV:=.X[˲kyA}98vBPdmK03~C{BDzM.{qCS~6Q^JZ`@?%wm~L,JtltlR  ` + nOusdN,nQr 2:,Sjo-gIILؠOРivx]LpHSfrC 3-,/ DNLOcUS++n/׏? l蚡PAŰח`A<@τᅡ^Ei2Lj]Q蝯~2FvCK(_W|OMq Uĵr V6I/"m2^Wq @fXk8 ;\%˕H֧f@N z,vݑsN}-x`1R3H)wC~ON|tk)F/7Ln=, ['8>"f~e#]OwaF?1 OFZ1ƨ"1+se"B?<S.RG?^nG;Q[t]K֓FM#V1vF| ']g^ipx0t>וnPYh& u42;|v8ޮbdjmٙ $gh@F"1h42ʏtUD8d-RD:ꍆfP٫-t2^O(0 ZG:p8/^W5sKVOڸ]3t3xpNjM;%>!rFη)Npi.aG֊x-&(?0 bqoq[5FUv&#jK .NG X[llc;{Ռ3\Z-w^_LxCyrzGxxT> _#.RW+ͽ9D<M(JyÀזln5R# aVQN7K(1uX *J\vo…ׁwlĀjTSH( -DB{?Y=s:FJ@-BTΒv^4515 -ƛSIl*l]+Tte9dE-k(%ӑ8ߟ [6t9BI@JC+yHk(7 +5H&+v#KL*8ÏeT_|̟y=p5UMɩ*O `Prh"F79h OS|(EaC`oN;@v{J<{*cP/(q$*L[${x=P!gߺnI֬BLenw#۴K DV O,)>+)Rs.!KdH%{zcE кzDdײYk#3m5\4)^vvfǯk5SDFPO]٥سE,jҫv'RÕYĥ lEex G#8CE~2HU5]\|+ZdySoBǾ0,hLdYUk<%.A@y9oBhqK//ckKly!N& ;q8b͹0YSS?CK `3W$!ZqPa/-~Eyn$7=MIKiw*Ԑ NLɲUװ|[|O7h]˨':d"5"דBh&ƒQ!a7ʢe=bb>AG.HHkO9(Sq:DurSJ@q ^2ZE4o<8w3#eʼx* {?ݱ8^_o&E6 H_!u> D!~=bޑ_t5#8n'y>qT8/|u5ܱ"8@cVVQBGrMñQyKR;Y mif0/jM!FË+9րjl=TX)J},|OAti>ӽZY8ϼqܿ[*4"08|B3aH]蝭Ha[_wU[ҕVaJY.`˲kJĜ\4nĶm~3Nl# )N$/G5!'ZI " o\p"A֎} 34#1^rBXlӦn?\} 4 LQf`{8i4'[)NM+625y.ثaV=[lT Cɒ mX fpEql]:&1)be -m~闁lެdhNbR@&)#(buiSPZ( vlȈA,̕P ~σ{䬿5uu̔-^J3p"QZ+?ŽI6B'qw>ƺ–jsɋ,܉ǎlh|¿7t Sb|SZ IBڢI` "[>T7_"$ C1DTxPV^%PB'Ɇ~^Ӗ!l 2%AgSSkNvT,Z fP:.\[PiZ)m|}CȑJyۣ¡u-҇64W]A?J.^R})g@wpT[_X~}ñ?Ґ`]`9<(y kU@[ }mȇ-Z{"')!K};WtaQ&?Lh"1ׅjCV0Ha*n a7nџIҴmI{: &^IPo{.}J-Թ~JOZYᖰ]ιGAVT3RDi׽Hl;΀'ñr_g t$;"t-bgZ0oax|70 ѮPD}hK7tyv;8I'*Tp?[<>?/K XΫ*ʕxB LDyh~ ۀ1G[]ReMDiTgt-$jBڷ]lΚj] 'q #7R /H׸ +X/7J:`"[<=BF{<׽dZ3_Eu=bf(r o ;O({:_ e)@1xp9{aЮ3zM2zC ;y"򋙡&d"G{/t- ]79"UK%纈j>m-oi.ח pm Giiooy7d F!0г}DB[priuA ߳Lje "GH'`D5\X{ZrN RU&!vكrԶ.+K-f}Ue;~%^;3P/")ch}`nm 5 ݿCZ#fޯv Cjѩ-h&-=+D 3s45K=eP؆8{FApƸJ2=ڭ\*2.6.vyJQϾ 9yltWBfG)IKN `Kt@ ,hN 6t^WHےhl5BD0_ḇb?%Wrz%m3wj@vV GzhT"bIbG&CZ5ձ@KY4_C'j02:.9:\`m4NA ]w njzLblcbEFt~xGW̒()_wQ{k#)q\EN},22uP:vY.2[[2\&ɽ.fX7T^ a.Dthh?l,(%7ZY.A97Cwܳ_o!$?pRʓ4\d~AV >=*<`12c$0._yشkpq9 ^dn`^w\m8@S?LDq`M8=A S פ=s:ONOo݅s|UX1W?i}t'4`xmfZ?*2aP,aTWhtBG^rzI)8*ߌHB#:+P9闖^5Aꐠ0;c1raBW>P hb @jI. [<;~v nGk~hHo%z Ur"_ :YbyI՚nG1`H ShkY'"5߼!?~cYp%V{(ϒ ,Gf?~}<.4@^8&b:PMϳAaTq Ԑ@mrbE/qKhR)t)C6P1:hӗ%.5eNHr[2ƛ^a:R\,0+b*Hy- dc$0e2K\B6r'2Z ~bSuY/$_p(@m2sBQ!]}/q۱ 2w֛i *WOQ0H{,pxJ 'WfeW|!>K ݸw_YWn XW)~&:P7^a=laޒTbJZ.z^/GYeit Rz[*/`=w smCg&&Iad,>̚K״3dWFkϬt 4ݘ6J `}5ᧀ ?]p ǡ..TO`I_z7O?d(Q̆~J' G[>$ICL⥴OlP% `VWbߩfz"/#.Fw8Ay Ω*w@P܁յ,QƽuN+yjK%+8N-~2ʠGnCL+W u̙L tA9( {L=;d> T7Y9B~z҄S[<4!HMIzZ/J|xh!!doU^osfԑCLe$2RQ,Ns^?ߧ;~j\ =ƭFtqr7wQul3gLu>(8p:X tR41&i˴dn(J-vSy8ʣ;ErxWۗ{DOv|}!I r{^yqLbHXrb9 Gx"73Nαa B 0*Z9$k0n '<\VҘbZJOO`_.X@rm_G̹dSB YfpV W ޕ`55ϡQ,c8/}ϷCZftho;>@,$ޓG#e.襈sxO-V}J^²xQEsfy_4Oߒ4؋. E)q3ϵz~wx_v.7DQmM*_6(VHd_:}g\ue宵@_,ȣٺg3f̐aw,p3`>}TT_.P;x!182x9w( 67Bn/j> vSD"}r)HO $ f8Q'sID싖*"g/'Fm8h(a(g6WՉ^ ²O#azkIakߔAg6޼FO{7jMޖ?,l觿 kwC^!KzL/$[<Y`R$0֙}[Aʐ!m9szrD13UMp݊:y?hA.ocD19ŕ\$+bÊ)kBU(\\;|зRC-DoJ[ꋎ?>&Ǭrx{R% s\y/B637i|{'%'`;fXVe-2B9ypk;ʽ@߰G|SRL?G^#uFe'`|+uکrC f& [ i8 1 D+=&kU/;u0o0o+$}UV_f軵Z un w+F@<@773eVK"|c롵v97{q")XK,/) ,0"CjE0x!NfO,ЀE+x>_a ~]Co>Yh!>Ӗ4#㣐K?y>Ѭ} /tXƒޔcjC 8FժH|-jj}W8E:&GgD-\obG\55KQ$T \p-cN Z;HSE窀q/$\m>}ypr&KCCoDP>V'yS5t3'hJ!2#$oS҆zrG˒H\Uȍ,E B5տAgt f~ D8ѳrkvbV{*B=a"4T,Pٰjl'Pm3D^dsiyI;[]{%!s9W,O"J{@fJyϥá].,G2V~8LMz;q/` rJ-F|#2B潝6tn?l?*4T~z* „=,Ǩ |3֏;9gfb{w_;`Q!i!qg]lӧ+!%TMJz_+jB1 ⢃hkyNnɜww n|X>zʈD}ƚ@1k|zXJZ|f:^0$QjMUjL\ X_H:;6 I@Ok`6s@%enb3<Œuv;A>VʳʍLB!4@ܜ0v.I)ٺa?gI2rh]9]vgy|z0 |$aQ'J%Ms?T\kGt jbJ/s 06Q4^@-1d+m)`P=$')Jɕ@9:/ia `S\,ÉUU(+ߋq[".5pͽ*n.nZq,v(6A̕nr:uEy_YREAR-Te ;|r<$wr=k@ chR7v`zqa1ɜȜNa\C-B z"zx&[{zOD="tTtSNx`׉a. XY 6u,Yܷ_%xgGrF7ԘR$[ROlƨ`5|FcɭtJth W})yNj͌/~/iuZIz^&0ekiNĨ|u!\2cb h8meTB01Vw%І<.=Q/t熂"5x؎ pjN_L$V Y2_t9 d l2GUchm6GutniyܨGۢNŭ{uZs38oa9Ay鯒'IRǡ\3>kfH6@wKLnpaAۦ/ `x>&6d# 쳗AJ@Ӱ!\Yy| տGe g^S;Y p'8|S0^i6g^}8×[Y lX,1z;<8Vk)0_a$$~} >#G.m1Z? Cy4QR#̤ug(:wɧ~p&Gbts]f)BdjLڮXi-&n3 @`Z5L͔wi\׀~?J\=jC3U%\,Uߛ#|5ʂ'fG:!{xJ. <@/@u~+}(NޛIR2mpzPHm"5p )R|)]+K U咅&Y3 DMvJNbdzWmc%mlJ.,:+;ӞW$=y;jeGT*mCB} \6Gjo֙]-V !]ʜᅡlfL9idt3lV (va_wf$Xf/wp. 1QC'Cɸӓj- =? IR/'$wK9Y(E["S:Ì(gbCPUm@|»H|\woP0jQaX佥\5,AUWd'kWCPî}#UJ?~ #uUC6{vp(}=|R9OnؒA*߮|\޳Z5|Fzd{sj.&;&dh`-%30Ɂ;6 #=;MFA#G !!YL*Q.WES{Ia _ݢb OK<9䇡z D^e+-SkINW0bgԟ yA+QT7%xLn[(3P2gEc$w0t?oֹp ~fS889e땿y_'ulI.M{ꏔ"6a2zu^&ъi͠=+!޹{9X6F;ʌ~g^ Eò!C߲cgԷ !vrȦ1 pMT;>V`2ֹ`~*_?vٜekW?@['v,;{YhL3㲁7);) 7Mx6gUˤ] 83c&+,]bVehFslz>Һװ0onx.)Fk3]D4sa@V { d-ߘ"yI'2AQDATk% ^r6wpMR0 '4C랍n0#*]9;6k OFBQ:j2:YR ~ܰ &1GBj_H q@vkwg2_tC6'<:%h*&cIk|UePDKhh6e}?A>/'wcA@k3-'pfٵx"66K[N.p.b*[N~3`حZr.m/B?ƯXл?Kgp496IA-(Zb5-ajgj!4j#E2kLNw٩<D?:_w=Xe%۩:< ^FĒ6ƺgT }n{(4[~ q ,?`å_Ҝ10er>Jz!,GOE":x)48su!f 7b) ۔DKssNzpAuJX0ſ2Y AUk_itDP^pg2۟bؐ`$~ÐtTws<0]qS+JPx8ye$wNRY2W!dM@3S$:\` i:-$ϫvZfh K,P>Mpd.s#%UYT\8 <$7B+&HnhħX޶|hzN0e wWB7QBFU;vS<7ZEu*FVli552+5ڪ>SMYL;tU/Dd}% / Sr; E2<;&tX*}%!: ,HOÕx@vțk5@&X/CJMe^,K ؈d/?t%crHu_ yJT,ޯ7c *+GfMr;CbH∔0?*|ȬkOy2Sep=HV1v%^-Tc>E=u#K4eĔn7xcUdLe}<RtOX.Iu 6Ujtwy9r-=}RLε!l;tZx}/@51ȧ ?y.ԭx;(E.xRnH=?q` y<3jb^vO#x&r&Ui՚@|N`e.:6 ] Q dxj:1vwApQk7˴8r!@EUvk<#և"C/)#RUulPqdV/ceo7IZ=jKI~EQTkfeۢ笽('m QrVs@ .,`K~u'U mD6m/T㱉=0ax<`w:m."X5BWF@ލkE3>0*W@7J/%vVߐ-~zw-JKiJ:z/E1PRomZvCl3,^ʜh=$(?j8V40[:^>*YP@/.nu>Sˆeǵ<9!l-P,&wd'afί|;#ODEw~ ɓ.bgɽ"1UN>n|ԋdvpw B1$;@p۰r[p"*#|# , g܊nfr%z6,QAZ,UiR?NuvR3_ \u"! $jg$5Gt1ϳwؚ[5o")O|TeQW)7**-nCH ݝA0#=jF"GڐH?k;ϫRH'Ow"&x{(Nk;21dsnJuBZx,wA,|t ~!h B HbcIHܴpU uaf0<!UPHmڣ}XU7_n]#Kc%@פ%A$ڹp:hN/W%u4Ol;B_]TI6;Oum GN 7/dLނ|bCAG :h px]NBr/%<(\#*XTDݹV#tqc\A`zPZO4F Q!~e"iJO\x]Ƿ0j'T$\Ѹ5e0y[)dQ7j+EI 7Hef!Olp|ؔustm.ŻO9bXJ%Yɘ.:P.hxXnTqɀSr:iVT'K䭠bRxP+*{e972=؍hnfp|@aV*'-Tyύ*͘r/u=74i#xDH1ӊG粺{p},$C^3X,+ߠ΍ShM~&Qj2>dHH71stvbr*!J'抄Y_$T-f ÛՠN +4Z߁^$X%,wVw440I'ѢgQxcټ>RIrbUE5+?1 wsP̼)^NMgt߯|竗ih9*z%-a JkGB`=.qB,-X>E~FEėȷ)<l+/Ӕ VMOÐ J ?dr0Ef묘M0ƩK{;&~xӑ5@9UHp)D}J c^]#5ȗl.(FC,`QvsC-W0N[_DA2+o^Z_3.b.xԒ Ħ- :E>_1b٧ahz% _6e ۾tZh]3Vb\|>;$_vWANe}6e3j4i|ĸ;px;LgȖIkg-G޺%z+ y8>I` H 2{HJ ieyn!C-%s8rxVyS٩p\Ȯfb-ŔuVķkkzUBZIM69{$7hf*14#I/ɅhO~[1ҠTdz%:)06JGԜKТV}w^dҜ|i8O | FћR@6>lY6_ꩮx6%l )wӰ֤[ #A&hrpA"VIQ߰;%EfnRܚ"r c%-JĤ\?飦yuꜵΛ0Hfm#LEs7.Q@@DJއKx`F7nxL_]\6$qe%1i}NNTl!Dct#LmR'a&'ƘCbq)p3@{CW~PZh \Mzö[R+yi8<=-#9%182{EWǵ0G)m=_N(WKRŃ1ӟb.dr҅^`UcڛUWTkJrlX ﶖ”]aƅuWk{݈{%1ߕAG\Lw2I̒QH  9ոUܜBЃ6{0G1sUrw!UUGHVT<AʷQuv/ޗySm{A3GVQZ^maL2|Fŀ=3p-h1/죤1aT%pX}"@[g{DnYFR5K6le[԰N؟%:2_Ip!F s{)^>T8FilOw !8 T cŀ j W~߅ZV`aJ; l?^eA2Yn[U[P)%~ &`{J@9 `w >g+W aAϞwY,6oO<0̸9dRr2w9Vh\ *nW2 k1H3fyZW1uLD.1b=VQ.LL*6|&q=ER> sR Ҋ*T(1>&6?c9 |ſ0:- Zɡ} TDmՍ:HM]R⦳nͤ  䐷+tvP O0Yz(]Z ։Fz -Y]y{K p$'="uס~m(<FGMi-tG \ʕ Oݦ>n'4(?2+R}|@iWg$3"`Ei3bT}v97e?j]]4&'l+˵,`b" 9ݒA. ]Ow.p% )upC-j[J#vKiM"@RS*77q-D6tVLJP IzO6 d;F⿐;v:?tw 0Fꗺ5Rhol:yO"V.&JhPa[TGL#'wI l+Iԙ/i%Mp<㛪uJu{4(-ӷ5ahitì q 7xOg;4KD/Q+)KJ.^\nz\1a}Q0(.E3F|pOHd])RiuG^M˫%b5o'`RDifwpH,LZ6Ă M n?ALΜ.uwLD A /?Qu3.~&C Qn)?S_>od6&~j-E03^X7ZDvue1q4cHUF^ 0׫Ka*ͽCzk8ry`";kdpŐ[9!Gz1 2CEUiܱlFI<_œU/M $ECC{>.(L#U-,| %'vU1u +e~,7 dudkhA/C~6tf A4gqgVV۔="]vhlm\ Tq'O{O&]0f{ܒ. !|; dN{`s{ yG3whU BZ™oYg?qہ%*YȫVqϟ 9gB}|6 ӖId6K#N@dvUpfN^)ʖWi`8̠7 yedLj*D#.XZ1XiCA:P1Z!b_27;NӏSsq\v"LTQR (՝cS gƾvuHkw,\X~`[6c="7ĩ?ФxMQu̓]=؃(3OօP|O:qF2{b _VBKG}eV*kK(%#Xy=n˳8[Z>% W!tN=\7ۃDZG3ZXgWxgnw\6z>T[6&4QE^?ڜIPu {;S<(`+ب3jTDmPʸx3Iܝ!Y ^wmnV}G"ْIqd9i0^qSؗ{gFN-A_0&@I5:JXn 9v2S{O%y)Pn~'.$oK o=l n g{-Tv:Dd5CcB?5蘬mm֯<5Rsf"2-jpn!.mL E^uUo0nl=U'^ >[HȮRi-txR:΁8LP8iCz K0lo``u8"Ά!Lbkt_渒ɉx/wP$t>l[68o$jO#w)(\3&i/L8uy6d|ٔįGLnSPwBSMT'X%G2 $#| `n#e`FE",2 /e& (Տ7o;V-.횄^MT6>zwTR2G g[,9H7˝Ȭw$OFR1d8=Lso^ij>taV6ZIijJЗL67e*eg_4IBEp<v[u`΢C35d$f]|BZ!y,@ U:%խ3xYnJyI-1m"&ܛ\y y v+{c&&X'j@b,l*YhZtܧ*i됝+| pϞr )nUBc_olxdrz qd=؀忿y"\LhiX>aؚ3mYta a%ݮJΨ1Tx`1W zX"1TҿK@^]a^a̺`~]z5I ,7,h 2S4$s 5 63 J^og:+ӊ@␗œ^ҧ%{wÜ^NLGZ~~4@sN\Gan86/HNzLS H| i5,K ICsEfMI(%UZ DpdZhI.X8kD84S()=g%k{snz{X2Dut?ֈnB^R[3R0uI囅e:mlef4ªaϞΕ,e1އkSs(B)͹v-Fq :&P+,3spqل9YH>5k=}&<5[,BnzY*,LK&͎ൠhMDl\ɷ\pދ$ JzQ3ٳGpB^иǩڏMrz|: ^&*<&bmF+?fw w:hrۘ"qObܺ.=;H:qNO9}?jL;hϗH5`Ǹ1]!5p}k(#15' .'hBtPn#<83'Q7R.4^'a6d۷ǧ`(S裁[x皓݁0dOFp]L1R(ٻd$>-{J%p̀H݌!jg89FN5\ϱKEeƼVRj6]+-2&p"𙅐F8[ {P%`0"rx`v]eu[>ZKZc $-;+닚I0lۯƦLےiMD. q;`S3E#xݦ ma!Fnz)8u\ۓmllL Mx(IUbZD_epNr2"w*:"tE:^1xΛ ?_>SSfyHF{ \QLdv:Sܷ&~mC?5ľq]BRC|c,ϋIߘn;AvP?LC$.#f%<&4ѹEv(ial*,$wcRq2gh >%[9&skP-`SQ\ cPtZn ph6ޘ\VcَZYˬ+&]#{G0͕BXWoK7|U\\3z^S&H#]ۥƫU=qn 3'iOu0$GC];B40Jrܴ jίXf:o QqtA6#H{7EHX@O^E2 7Zp٣^]Z5}$,C( xQ&Wk?nÎOۑv9TD_DGl2Tޱ̓ee!MWҞ¬G"mF4_umcwE \"NCɌT #.9WE9p̘ NqtT-ãN^.aM&}BV' U:gd\TY*ye[6TO{ޟB7Ŏ.L,P"+_S }[C'ȅҘsܚ ca>cã2Yݽ%9C~Xf`h g' `o ħęl,PMnQr94b7_cIh˜zBb^26wUq}V[ZЎWxսK^.kSyߐj4ę+O!B?BB h v%v| eJ. ft!S#ޞfiƎ;˻R#P2׫+.w ֗L=N%p4W}5^PW1ixx?z[^E=~ÓeVz^ \Ļ])t5B5P7#=]h-Ke֧8t4‹nO.KWy(F $W02 o]Hio. $D}6c*n **%726+tҍtMEneh$ybzwu, x'cEzv춽e&^,usVYb'B(4'B\ͦgP@;Sx(1ɾ=)[RCD@g^{ sNroIˆrx|%H fE`Vd!G<0ˡ>yk΍p]v*TX|)gp(.๓qqW JC9ظenH؛m*'t޼GLS%>QjO4[ҖF~Md~y "fG_G*WB8ήF,!8!(6 qYd"0\X;Zpq!xP&qj|#T48IE}(ЧdEǝEw?Y$ ߕԖi#@0/LF;6rK!߰wS aI:{c+vf]Рu å ].Kh~\u'{woVqF 14\?e~ߎxraKaLS"͠][Ic2"h0Nrv qEwJ]zu4dlp&{JBS0Pfz8t3{.>'{j5@n0 oR:tP~K*UvZP`|TcT3Q RU7WD5n4H5.'#o&W,Mȉr;4v@3hf-䧭qlDBf\X_&$6}:7KA v6t y7=M1 =՛HNqF>jcb1@z!6#7P{Hn80<$RJ#Nܬ-ω~:xqkV~TʀdLU\1I4MUЪH/Ljg|A.yF~BFB%f3CPC7eղ k-1{ c%:VՉ/c͸\6-t)/yH p.+ r+aȸ9OUqeɍK6[_<44JBjkx&`c'\k.bAJM/]AfLUdZD{÷!փ^4s% -@bu&cki`|H{~7p4Jt͸:~g0o`D 9`S {]5"DU#SNBg)Uf!}ɼDZ&$8+8x%r>[O"kUTQ xTnB?h3$2 -R:v ȭ>,*?'Xn8ki /WtZˆ@E>C+-Q>ړՍ`|78`j= &1^3W'U9l PU.)-Zq iP =#1,Vk%/+W gsd.݆dM JU.+ |U_6]'vpG t9so/R7wZn2:J]9`P膭t_|>._tb]$8/Zl?C'A%x#lŠX/H'E]Dlsa5有-I*%G>VrO:Bi\Kh׽R6TCc:B$%JIitǼEƛ6x2);d4qp:>=0!ovba2hLܚs J6Ac%* XyCgN巿-ӽXxuѝb7M77I~ 03-33џX9_/镱^dNBn#@YNʹ)UBȜHz7*{5>oq1tp `/ _[ń[?Oxj$_GUQ Ѵl).uTeA?bRbi\?L4r|eV%b`UY={ dRXMqz6ŃDke:3mMک &7j;4XlfB Bzz )sK%~*{l:L8hScźFn ; q`gKZ7o$M$*\j\d k[daAC]+3j -!#zYQ=}SU[Xh4|OH*E?ֲ 3p.ѣ~:eIfzA  O*$s=Ԥ@R4ȣ}WF]K/X -\,ATGHc7=qzO|i_u0*ȏn!Q";wOˍ<  ?*@KX7% +]3Td `d\nY31H^vfA@o+h6 ! gAM0Ua(4?DVXR)phު#3;p!_Oh?K8_~I^y 5 Gq}aVqkM)SqUU/0ֈv;xETȢ %l?vtc-Y< 9I-$l 4LߥV&I9fX/'Qoi\[ hs>N} K&kj=M!lG&2-Tf-D?XcӢtLɿ}P[[j:Ow(8ݖ A\|Ǜ6[Ww#1k҇?ϋM+t%f]2Qض²['SON&JϸtcjY)5D2RnkR53{)62+AP*fłBBgV/CcY[K@RE塍Q_7]fGz ;9ZvvhNuRX`%#81|os{DyqdWW=Vdںщ/:\JǏqjuZErf%[wzM#!yXîXrv @= 1)aut>{S)\s/dQ[cMnL|CRybs t'sa3"^La[B(JM5=iRґ {Th2n祿^kPBC8E41GrK<xG :=↝ 1{wD@3Lo{i7H{4/lP%{Z)Ueb/tkz7.T` [4IS52l%Y ~0 |`5w y%:uGSG;4-p^ +='ylZ % )r.YqnWGzR b @XMDv~a9xo4D"<쀟^d\RW^2{n5UZejd&Ű\72C㌧K-;x5~MT:`4붉H s;~DU6,UE ʦ7o6okx U<`%TK XTL vI$empmQ!tƺ'B,vA~|no,^HQ-F(Bgq33,DG tu SCnR0uasCa.E +;&bL_|Yph=l1LHj= td%1XDy^U|-]Spw|<6H$+cFq Yα.? C]SF S*G)X}ԐWlkMbh*|?o= 9xns(NM\8JBȠm?eߖ*gO  ^ EGi]=;ɹ3[!HEn &ڠ\.v:&>'%蠏l`֨8dY7"0m< 1yjگ4lygw"ڨ`hq" *Qz4߇uAm'tY,I6O( ,Z l w͝b\)ΖxNVFmrIz"Ć2sS,{g 8LsIc , l/LMM𚹘L~z']MThoݓWvӅQ _8d?JHﱦٶ{++R ٣6sBkZ1?@[^ /(Uݷ]5 U}؅O3|U٧ Q 2a]k/{xT!(^?ˬbێ|u.5f9u'y6~F 51xaC#IE?ZR2qe] apR 46 [Hۓ ryV@n2MGr0sv.5CNJ| Qs'k7x[-˓&x(@]r9zgSvq}1v YAlmtbVm}_&>NO64X^a-(+#t̪2ic@4GQElղ-vL7;3rq_s0l+msgAQdcP6v` ߻m5\vRM_o|=ooթ5"֜b»yϕ4HeIrB_#&*{r+ÆEoV2ߊnʭ.1v ;!jc}SQs4 sGw,8G2l3 f܇gt6vz]Au&bVzh=k#m-a釞n!7Dž^X>N4\*,w\IY>tJKAF@|ţ>Fpjf*do.J6Xe!4blKŮɩ_>ך&6 9d8THӯvҞ$^t\]keau7@1[e,RUZBu0t(qI*fBX0@:C\d| {}=ڜx#G6y|sb6C*%=a?0HU|"=#6ԔMBJTxH -+!sqfߧ"8cR׌A;#ka=Z>l'"Oq|eI2#~Br irDsɆY(ULK*`1h+Ǐue{ g%,I5BgH麉!ix8 Oțݍ)yr^kdҳNe"IB3$- NÎ)TW9g\/z8{`8d:8 RFwfҰE(S˾S͗z7(PSA}^`nMu(>ʎ!0cPȝB:Wz)|z;G vn&A=aE[Hޚ/k8 (lMk9oo*{#RjY*h K nڌ|+\Z-nxd]Uނ:Y6>b.8 "0~W-Hb C|TMnKDA2S|:Gdrㄿ<!kCݶQYpK%(t(?rMrA[hA0j!h%)/`ce6$Zʊ@*6 I9Օ c'61!d-FP'W4f+/oXyD\t(U%FH>Iw͐? /AmH>{)~ 2[p[ !] .L x+`6r dUkӂ+@s3Qx=n4:RɰUo 3xEdC(mcG6}ˤ=cr I/[e@`b=2c~8 =~_)RAZw9rT;P-nC nVVFaC>6mI ce&t<|>Ԋ9Lm0(at8 tj柑a}$ od QqD'3y݋6ToLN栩p}S O'݄^ĕgkl[⸏C*OQ%/-5XF(o9kZ>,E7mJ5M)oTMwxχq[%Q d$ 3Tb0h'P-o9J_(Ջǫp?%,J>K{m>B=G_|dH4"ڹwV3 rj68[b7љOgE>tUͯ`ْ)w+FĮ\ ߜ`;9J:ثY<2V0 .8sPw 7 <Ėw۱;^F>7gd>0㞤hIVW/2£ڢPWC0>Olr ݁:e 3 J19|-p'S5!-\LU^bZС:F8!;/y2,fS5~N5(HyV-sx-.emW *v$ޑRDQUے9<$H(:ܗL, p@L+y?1UZw+eR"@RƷa7o|T>N)hZ̃)rgZ;J(6 0Q7QKs@Юy&)mnV%jq5ZE/-L*@W -PyW8̾$ D+ԓz,_eG}=G\uC!,BLxa6bSDh#܍levNH_]DfPֽZÁ}0؈' fyG(ˇ%]OW )]R '璋/o9J 7ZG݅œr:AV;GHVe}qAn*ZP9ty3s>Owy {1/OoD?Ƒ*{:]!YKShrW'_6s17.]Y1&zJtP Sd!i?e YU_%D&>;5&NQG-8DC^gΓ'<$d+_L+6oNM8A|'w^ߔG~q,-Dύ[}kkV+2iӁqY_TOM^nKHRG<_ *U9ʒٖs$BwX+Bǒ7&| k#61+*E*$J܇`v\ኙl,dߠ3?a;xO_r.9zڝvh݂У:` l A{5kO['z^3p"vIdt>6V!6bM 6u,Rb8]x 䡪0,z7gitW %.=I៲%i{#ިP6m-9Ty rKǚ6y#;*ˬkl#{"^^#'ʛk;!Br]B6I*m;"qvj $d'P<0`!R/%!92G\%F[]xxy6 gk[8xW.Ne ڷ -!8ϹHR UCg/ec3~e >:73[c (Q9 C=AQlY8nB ,֜i)䄷ȓ8j--ez+wg/,ϭ;HfKNu쇉CmdW.8..4변~B"^@WZ:+j ~̙z;d)'+G#jD_Nv+ud57 [2jZ*UIܓ/6@U鼒]'[ h/RS-?"y #4ٱM<ɗV)Rvꅖl-9)7qȘ֟&ei#,Qkvh[MɫXi HunT_J&2bUe6"pR1x F4ҞAS'pO 5䖚i5%loFnԞZ6#l X|E@l 1:bt[O?eN3<і%CUm͔Ro]~T{% >,e1AtZ&ڤQMa@4w>htl6O&[ͩM"=Y%K { 65-吘d_"c[datv I|ĩ^Ozyp%_|]GFge9:b0&]4#a!8z1t:vdƽ"r,|n3S.zJ p:ȷǐ>/IкYGvڄU9Aڊ1L^pt|eg):uC3Y gWH1. M3%7gĞcM/j? D0I1ݍiLu$/J.=s8}BIn(g4g}T+!z}vh(/U %oU?ɒnag'p p.7t ",G5]w]S8$L˞6Re%[UPt|5Uz+wKD+`G\C8 Usԍ 4c=JT}(jQB<jֆ(WO9lԗ-j|A UL* }B2.i-mdڜ56{ϞK XRjԌ^kLQ΂?Gm8M}C3:|Z&0YvV+T{ȃd^s %lJ?;aw2w/`!u44h@gPWC5LϲrNdB}lMK/3g̀l]w;/%tQS:83ĬQnnSd[JhlRȄ)A>9c4 1],&bO=uK0r%̹8`d<{ +d+UxHkC"fjE}'v+YToL]/W P,:'Bo\ ~' PI{W@N#Fx42ByZ>yW|ݼ8X6ӽ.@ MH^cl<{z֗PBz;r"20žZs!%h&Ҍi**N2XPssLOYK I24d<&Noys#<r*~~WkOt7:'z{Q|qjj'W ' sxCNݝN9G\`[ I?搄NFGgW"a(\j$J@ћqa4~81^dGt==|Sxxh/Cϯ3t FniU evѫFmo+B-WaH> .V=~OCcGv>}hy@ˣjӰ߻=¸)ce1Z[o]>Bޔ X#5@Ԍ! 2h,L[7r|Z<vӫ[%y-ha3D_{ @{8mn <47lVQ O[ijmhSDX,/9*; }k'ïrܓN-j  #yHz=q,T Q'㟏 hUUl]a_`pm(1l&BQ$jf;DBTXdQ-e+Wt𱯭*Ms/+ݩnoإ~/i ~3RD3ajN?~7S-Յ!lS$p1u`)~t{qI'LYZ &AG +>Mrp%N!b}BhL˹8͒/Zad_E4UTyFueOyHTү3GE:Ty$Otb,HsV}%"}jٲ|tGf955Q*}ݗy?k8lcLHeۉ# @+>wuD˞,ߖ ~a fEnT]NTYMwmDx}Rۋz{ z0ĘlTļTj6H& *`BL+?Y` cʳ"ڈH"ojSy?/'?)+:]:oN?QFNW NFԓg<8ɩ~ eY~.Jg9"+zW?qҤ5zK3a9Rp-rBg\syF iCبA&~B!f*D.o5.l:Yxh }f` :#8:Ējp]d1Cx6^ӏZ#&] ͔}(+TD?Lx焝5>[`fM6{8%ۡ [o5#dwp,53kM̎n(c>~$ w=J A]~}4\T5w6Ƙ]D eaq"]pτ 6+4~acu$]fO'~&C/)(vPͤV>MXźWQߔ&@[ޤ^XRzo=quǧ3KgiY)׋H < j^ڛg2r WVSEQO]g""EW" r0^C1XA{muqb.[Bkb"H[_:)a{v*9_rAGfDN9/IroAy/c쌭?$׵y; V 'cN3aG" \Hַr%al^dWqT_ MɇِI#*q9Z'!`3D}eLVEKA~$Qc<'"mgX@x,:`ٮ}cȃjʋO*)QDfy@k.&ug8%B2==ަLmG?#co F16(9ݢQ;B$Ru婡Ro[ӖF,aG_+ċN8}Q R@l ,0gzli um~s;3rO Ov Q gHQ o?v%4,Jg-8T=t :V -{'< S4#{&RLO1oAOtFڀ>tNoz2db!UUEjU PnWn9O:$ P"S\)l_Pƽ"})}5܍5effn@ YdvfG:4>3,vPNM]"ku{77NyqnBUDY0ˊ8s˸7d\6&QdOlVh }?"JLmi38FEUs sʮ(g6~l4W 'O~&1 }*;DN3+J^ۄ+$r4ްiDP^$ڳxN'|X%J_6 NqX 8>.,ţq-_̓*#ǩuqH+Y42@m{>SB;lͲMJk`1f>fg 6$fنo0c*N03do&&g%b[h0]P$v<#|Kc]zXg\ܜZ0!nҨ=Pz;.ʏ2?B)iHwưyB5q!:;! *NYu(o_+@þ㨏gOu(0e))E B:N0bHSڮgE2}j #8p*D)\k0}>eOUH3_J~Ur3 :åU~ y@y(ffZlyZm13(052R=6uюIjz']B˶Ϗ)ySGV^tu}h0{=O~"_o02DHS= 1l[|9Fmdս0; ܾ(@7Z%xnTA5ͱEL}1zE&.xx7})ߥW3hN P뽭}-"/讘SxY}/%0H+a~L/1ÕsuSbXTÅ dVeOC 44*S[g05$;}ҭ<dݖю1(_<ˍ" XPqtߙ=n$x޴@,5It"m+9GƱb&jS\5%b!|$L~ɫb1(&4 ?1]+=+~yoi ~ E! kڷn iij47½kx^2:ULh<fBm)W"m$!60)~B0F9>WWmw[ pޛJ"yD=9Qv |bYoxϱ4S=<o~.yǯ$Gk<]>\Bp B "gkv*CNGJXD8Bw q  sG:LZiHY\S"y~YJfWz_ ;_Yw I'\\*L}9Dݥ?j6*a,esχzƉ| P 5.Pur\_ٝ{2[.+[D yu u*^9 I7x7h2H oDeP.| l=!OԞ;_|>+~LY֙kI=9~]N5) =uqAj?qgCŬ fp]iԏl4HR$EM [óY3^~@ڲ_I+܌rXZs|@C+q.aJCjIq7RHf^YlšB#/o"mx00?z< ?3bOjz#I%Q-n3\"F-ž.!zupto %oF{jH Z̃ˉCZm(/,!|h/D]ߓhTCKBes[_i]<4)xpbc: kZO;i<(g޵=u1r~sHMI۷s6g<Ma͝hٸ:$7zSI۔r"R{#Wc˖~\*J~wkA0T`'@5k$Jz`lsNYZ9 `Lzf wKm>>5<@7BXVL9mS8Q#Ie_vyL̺.mŸ7LOX}^R ,M-rkg+/s^ŘޜjbAf,Dž-/:\t:&}aD(z P>`<Ԥ3FvMp'iZ `FJ.' P:^qނTS`Xr>Jc DLg~)Ĉ8b_1.0#XUzSXO1H^c+)4~ڔ`6`$ OvU8NTfD2._Vj1ĶSӄ1ʲEO$mŸls;nެEM S;H)qť #7EOF%^ WRBJ zFcj]ޙ/*68 U#G~ѯQLu:CE,X=J埛Kh޴{@c n*VwT4FF+B^SL6l|DD<'Z$ ϯ7l]>߬ѸV*Z"6K (s.|)Ն2x*xnh W PL [ZФ_Q -6gY89t8' Wg\F*,bC@1)=$lќ6ב)QBU ι']!Kԅx׶&{:'zqdܔtK|-3+UgmsnY˽xd+&} u|8qr{m,muT1:^J3}@K_VW~E=yHXRz %ڵE &#<6` OVdisFWNpv[:3k"Zw(8مF;fiDT2LD v}XMQpGhuVK MJ6N>tAokhotD 13Gh/7Ƣ'݄ &Ek5iFeѿy^|I:,~A WPw:y{e@r^oG3׊lDQENZ"@{Z?HאVJ? uDz! tA-MmҤ~ ^$lh*;A0GKzh ,ċf2WIcUL([R=VtmюE9:K_Mnvk'>><8lk;y6.V#5g)# FDQ LݺŪmgYȸ츲w]al:a%ؿ@tv a<Z_ |%M؋K|8 FUӹ+#Аc KG8#MP ^"ÜMP jQò[7%a]6뢢3 Z0njOC̥gsVE0} /)ɿR-0֞bVss ޫAmnavmppѴk}Xƪq6G%!֥l',ZecS4KOJ nsֶ(Zh7tᕹaT6:>Z-{%ni~k 볍 ,|-8->[!-9þEqhƨլMz\$A^* [f>m+ID(wqtΑ83YtDT>9X$frq|Fic5V8NO纔F}_l!X6SEpdlFwzANpXܧ77m%NRoX̨ڍKV|_M+%iT`^ͣxY 1._PKY{Y4 helgMA7\&,B=1?\ *'p)Gay)]SLsb>DuGMXϫU'd Mt{nlԯ>SwE(E8le Qh.#վ6)XfDtD 3Tt_ۄ8R/, v/HuL38 VD0;-zթSa>)A-xA[m, & VsheA;́4U]͉t]Qc ̑qf 4'ɡ<-X@3vN,'n&v;xThWj ݠ tZ|cu&.2?ɛď' *M CTU< u N$#,%5 I^1оv[HT',M)r#ٻ N<yHXy/ #f.=Uy"Xn . <=yݔ:y[y&w9-Y Pk|uvlp1H j{FҠ(J v5GjP8@5ȅ"4"jVQjʼnR0'ozhCV8%jh>g3"\ "z3%)75ɫ\)i $gȟʼn⽲y=kN˦)G nz2loDS3t')N@Iru]W{=,W(8hlUCq/e2F=UڃDNͥ툙H* > 1*,~,X5Iė;G)APxm|Ca'C@"sMZi ꐺD%ZA^WD1Ӓ\ %#ð/EB`ȩB&Q^ |qO!XzH r-ڢ\ߩ(2Cvԉ)Q"wGbIۚ{&h,g*#4&8 g9=_1;2ftw90+kp)-RH6FHfz`d ҩ3AA;{9Zx%b"4Ing Tf24VS(~Uz{8pF(^/ "ӱ-&jgE.QД-i:@i2tƦd] ]yG]22lЕ]ǩ ߓh׼>\p#^!9[O̳gN"̑B]sZ EpCdFў!g():7\[W3GCV{c'wf&. eɖ 5(tͽ"*ރb3[ȲI ` s E:Jt5Ot8Y#D;@r%等.1i ^zVNt㡇  _.Bɤnk ۙbFYj.SQ0 5~RBֹ58e1$P Er )D89ȸ+ l,6W\a#aIﻁYlw^'z~P^ІpYahfO<|Gj~~ dJ{(I?Glt;( otIFBբ ̾Xm3`:u1gVHU`6^CXVٱjk%z'fӡ-?ͮ^-&DǪ)DLj>Ӡ XQY# rG)c1<[xN)zMYǴe,UY0dQNjHpfh#R*0AO)_cVNaѽ7dQӒ-8M=E#a{ '~lCGyBEl1W} t䬶 oKP4]3u/F`Tnim[yF#R{xKk8vx2QBhnweF)Eƒ4LJ>VP#lm[KlD=G?90[ޘ0D|9QHDWL˧v?z#E^~ۉY\:NsETKmaPd# Q2xo`loUĠ!˧+dcC183MOHIؿh !?jr#bC*eF5e˚Dŧ?"kb L+8&=JM&l6Da͚lJ">Q,.qFh&;Ɲ2z}FF`揼N|!mS}Ȗ_ݞ: TjƑe0 1W,<}6z+?gpHZdY?]  'ZA_cʤJw ib &lZ })AП*-jb:ag8 /E8?U$G-I[{0R\ZX,SLS> ڐ(I|FE^Øe =e;w<5hDr .|Vi{:¡}לp5OB `x&g0}a_> ,x' H'Ei.4}]VR"Xg^ ܶ]c#knOCz\<f(ڡ}{Úl bzKK[[%5G%\ RoT a%+?3qIYxJ:/ _Вү/a5w#3\7ȍs)1̙X!wa"R[{1Ilɴil4*+yE<_5Oۮ%3awF~^ext8L-;bT"ϕ.EB_7sHҍc%зa?`ObnKi j@vew w+hub9p'Ã?ݼ9?~k 0hHxfyzS_JVѨ];g@P/"Hr=W`CMh>M@{Z͇>wr±8y;jN,Kr@L#u,OA npE>fVV&^jcgfmjX o}]GŐ)[e"(!"xhe.w6};8U}SSV:v/6Z./].qNSѷJn3|b^}1;Zpc=m%TyIW z1~F||;:2 k~(iً$,X@d3ツզ-+c V"`#$ ;y.3;GlJEg5Qe7sP td ?`dBމmkcrݰ+1c|tAq,9d:>&"ݩ~5Pz6LB會qT9ӫ]6 rH1o,Ee}-E٪9]A%=Fk}8gS .:Ǜز@l<<@$]U<IEvMj!:a6cؗY_׺,evatVk$ ]-_ i"}CT,+T}s4|!!-q"Pe-$Y xSd7]ԢQtG,M6cK~!wH<a`põY'1넇)῀S3헅a.n#x'0Y5oU{my+bG*>a ?r2"&I9DKS`EAoy 9†3A F:ޢkӶr(J]N\ANLHܩ톞i2 z?_I!񥇸Пݚu/ /9kg0`ysD(ι 7)<]:czE_ӑv9E_6tӚX#BŶœ!{EU߉ PM [^Wjl2ݔS?x~cW+t~#8q7ɇWM Wis)%l->$'~v!]?XKMYxW,We$!CHԪJ6ʲsj'Ɛ$QjI5stDDPftj>6P7 D31pQ]T̂!:"E=*/F.{5`uF!O'Зo:.b>.s;ynq&T[597m,Bl?ֆX2Y/c2F.bu&LҽgwX%La%B")=(3{JQɇlK \ͩyȊN_݇/ui {UYl- #DhUh&RN>7}w{#Y QDZpM Phu d ϼug&b~b+`7,+d)k nw6]+!^2:5<* ԥ~gא&԰Q.\VvoC%ϩO!t'U,Rw RGV;ۀĵ!Jqm9|JX.nVe`}dܴ9SsXU~kZ@3HXB]y|?ҪpgdZQX-%%cv=Ss^wSs>!? TtbH68<9KӎRxOK:i!ef3#Z 1uO 2ׂqbj"DPw|_3P'(Dm},L37hE3s`ޢpoѝ"~}4A}1o ]3xN2c7)U%JIkKO'сfh1±iHh$RCFgpW+sX7r` ?LRKb(M0B%aU`',jI'3֔ך䷫oA>O G1 4NRz a"f紖 Y+SeW'5έdl 5Er3DvʀF>U{2"'yopIϠY[RTw7!b8Dƍl˲͊tZ8#bV/ZAiڢ@M&7aM"J(\rH4{=- thVXu. s S *Kd5 !e-.?:m\0#Rt鳌 ɕQ"#" i ##Da _r^ HC~hq/N'rK"gnﭵh:)IM : Lxh$VX-0><~B 溦5Gvk\b:} \9OøkrmGܺJqnǷi,mHAZGkMj*'eo~_Gt*Ѝ`@v3Za2D;쎳؛A^dR7 ~1! 䈞Q?QUH< Z;q]޴L2\ 5[(_pnCJ,J{uJrWsD4 P>_11 (_ 5s.*8\S|ᐳʮ- ,ŝAǁqe>AHpDڳy<:%Wq~/'\P|t8:AͿk2yV%ՙK nċ&"~ cLOM&82QRN}ti~t $}?}/m<$zKo*Qlۡr=OrNa'flJʼIن([ ^LeVX&W>̴JRFqG>ؼU7x+B*d'ժbRq65GԢpIs&ЛPsAC=lB " 2c2X(q77]YpߗDD߅.s') !=d.b#_2ٿ}ÌJ mEr8T-vk'!W-_;3V^Foۣ:#xqr[d23N f.stn4ghV/sMcr(J8P'ٙz7Y'(nJhIy)>CwCYY֐uzl^GKLI.iKS N[w+1D= [+JX#eXmk3`kX%6 ,@Qׅ) Xa.i<1Al%+Kh7,1>n%rF},1C+>8`ALx%CXI%> NDZVZbJr%6>\4#6oEpp,=&6vuv![:W ]D;X~Xґ6#IaA鴯gߎZ`)F~Aدq~PZ< P/&*.}rt[DR\_&Բvi@3HFr aC 0G=WrI"`u[yB^?`WחQc{ |l0ȏأu;H6 䮜^ DcFM䈝Ԩ 56eΝmmz4ەRp 3c;{e&ˮ\D5Hf[o,:J<@?Ŀm'sjs%+PpWX?Sa׾\QȦ0 /~E/r!~n F $[ l#"(6٢:gqY}V+8 :fW׀lhO'ԁl]c?24qg2 x*5?يT 7Ǵo4x*`%ô^% =*^ ܦHRl-*'u2׈ĩƋجbgLkWkC+";1k ~1 *9'..:$ @! oVӼ>Lh=m~enKw4zn9aܟp\5 >d]t؅|#r4w݅7_=䶲@rHN&Ď n)Kaz )LGvv! I| 5 QL ֻ`EXt&- gNK S-y};AHeׁax=>a`7@Mj鷈A$D1CS.Tk`}z+R$ĹSYi.,&g6{E<(14GjäwěRBBjcUS-؈\gka޺;TtdLpiBwZ@n`; C rIY9\Kg6c5HVspld{ǿ0@W<:h\&:YB]ŕ+FT[F0R-@(%яb&Yatä_T5V5.l8 gqΒx?V ȹ 7>WK{VXc'+}(vR?*<}_,"_6$Cl Ngx8LSE氬\*"lXQuQy$Gs;񈨷/Cp\;mgU5ד^NΆn :1csEI`ik3!-9 }}0ylI4bƢ"1 vt ( OS@k %*_JP;ʹHw, Q1x:865m*Qxe %2y[zf(3qD\H<4Φ t$S:`2x 1j&)LaӰ4@E\If܎Kh]Sdt[ڴ769Yzj6,LP+f%KZu{0^?ȷfd̸BagF7B5th۲0r4Lk&R{Yճ˅ N8zRep)q xߎ.,택n'0 *=٭l}zW*biUcUF\&7vz34ZTL?Mx L+UfQ_DQM8J}BAҰBd,A?p澎'$n䢝 gjjjb?:HP,_  !^4c*};q>xșl(;Wtxו.V͖rV&LxksuNNJb^)X*%z@=z9 ([F"S@u 3ܰA%mL[ s)@s.WfUO${FmZ8\)G/ xo\s6(h0z|t]}#i YqL*>0$[gǖ?tC^8<%zW{aN#qEEF'USk0r\CIs IUӜ^TfÇ9xXF>6aȡmE@5<#%"я➌8yBH@yZΒJ6$ Tm͹m[|Rvlrȉ:9Bmu"s]d2.;S @`p)Jѷyr;PKv)zuͥH+"yzBzEA:XCv4;+&pɼtMՇ#0OT%@1fM&L#_24O]AqUe]gGUҿ4өV&ӷ&?P Z NY*= _=;޺NܑbHbh||-C%C?^Q׺D^#EtL11qŔ ~lNYrC\bbcm-zp"}uh%Z^9zaLkLl/٪1E2'nl W[x ^FtLV]U' _4 Zg{L)KN)R䁲k=^e5 L3S.E! Mr]}<%)leXryDP*V \Pũ 7 ~ck .Ap; ~n5vP8HɶrvDNAcya'd^!kA WL݋ۦ|te-lUnk{(39q8|.6\˅Lt>Zjs75="6G6v)_oW~<}<{&#'@]x0KLlQ[Y{zl\uxt+%XT4>HZF@5ዾP}`#pE8p@'N'wmu%p.$ {.^o9?\T(鍀fakGԤoP#Կ(:tw]=-k}!bd}+s#b %838B>G?""LUGLT0OG溛ZQ o9V`ta\Dޒ~u +J׿Z Q28NT~2H@j<9-ZKf;V$55Mly%|Oevh~w7ݘRûk|C&0nCKX;} C`;z8qwuچIvPp2%htvo""_IqTb DE݉i 6;3\Ӧ=DSٰz5`P:l/67"OJwg_ |G_cV=ؼp&ةu k޿b2;@5B ,j%j-,g^J]hTC{}5+ӃYCr@n3d5F\7 H d+J|6?H֗],(MUn*CugTӔYk5i-UHSҞ<6AA ׶ CPG^'oϻ{HHno(\@I54%I8 {qm`'h0]:eZzI`rT|5!509Zhj|#S"XA6$"XOݜ *IȽb4 :@.b- 6&mB(o.vf}?~PQ-r PQw>:9$((B6a9vHTx"᫓KE^gP5NS+yU=uB.t0OOĉ}*rǒcӜS&Axw94l3`S3dTw $2Haɉ+˂_Mk4}ܫ?"0Z=X{gdO`DHKS_ٜVURfMvB‹Ɔ%hȁ5;4"BlrճdoP=I$9+߭ L5ftZ,LW/tBcB6];@ёu OjAF{d5 ZpZ*h^$L}N ;802vvYD_8uLU7@׋,]7ڇ#Y-`üYb),lC&b= []kxfK{V gK@%kg*OvT60kYC;,086Pͼ±/߯w^7~ZMS3owaXg,]V|bH>?-i|OXggVa!:}ods nul~H MU%{,*1|x~Gs)I wwfW1OE,42Uji~<'lHs8V>5MDE.cfz`p#/2@<+2U!QF 2؅T{~7Pe29٥_iok=<ɫ5!G15Cw~%t5Ād?4S!@0tҥPw&Y"EN o1k%;7Y[B~~鹹^SG ÕGLpXsbq86+̒ %e#3[8+EFK4>A8Rl3p^g˽_j QEG Oۛy?֠Ԙjdy0qgaZO;YуV_ I> Hg2+(1J(30#:ϭOUh^d9LF࢒Y7r!j z~P9WC*3#徆 غa%"c Ћ%$#L)ʓgJA;h;]٨,:Z pPgU#^6Z呹n$o*f0q%:tm/gO1{0:.Jq 48 t{Hzesϥ6+Fҕ}F"Ahb&@%].̰ /ޫnQtUTfhW{.SB)nE8S2@?bt<%ͻi *|\CV  # =N)J dhC U1RCuOAm\ᥴ̦Tqkt 7|A7K\\Umi( @MȬ^d̄a{CzT':کe叆|T`Yė~iy6W'ԍ@R=<4 Oοd*L;I,]E׫JXqTXO}>Ad%"8ywD[LX@cCH6`N];2I-cƘaml"&Լ\ V^ic1ЧDJ|4-Zjx>.ueR\QAF&(=#C9QŞ:z@wQjqmak 9ϧ (yI#>S'T[ iˋmO;.w?/[8'b G*<6V&}KcG V,Tq2U! >5I%oiv|ж_$8 DB JX=7usmN+DځΦޜ$XoTD4"ѯ|/_% ~ !BOgU]=8F=*E&[ ʞifGbcPR<6S %)6481P2Y5\`M7aN^2}l3K}DbZ%W#w\[$;\B,x=Lb A|ۧzV847& Hwfdkxwu'c^6 :?:5<4^t_!EU񪿺t,zmĠS`MtTy/`~A=!ߎXY nmGJNhAױΜN7&~m. 8JD >3#'SL]%q'R/ ՗4x,ܱ.szSXL.@Nǫqa5^Rr)Lr s{4D*4P/=*pq_U$;nεjp(zF&m^\# [A lR(K;10+JKh ɺt}~IX" ,JGlhJ%l,yn^͡d76s#@xl+e^ T3Q.}FAh0 ^^фX(2Zo͂G saJ9Ɔ 쨭n i 0M!5z `}%a.ڕvŊ+f(ɿL̷yK \Lu rY?SҧbH-d3r 5*:ͤr)YHNRCw!mUڂfjEbp[h4=avv޵R*}Y_}NLazϚ9=|]4xB_- =8FQw 簀ae3'ѿx3H2i^ƀ"B "i g)` Tj.Abz//- MBK|`Aتb4r%K' )U+! I*>2S%QϒHRg^[+\ȳ.8Ă4=ww1h,:,c #ԝg(a'n2[1*SR\}-ŭh-;'4rUd25UNwglDt& k,nݽxSSD)2 =[N8cM'tnٓeݚ淽x͵5vu0 6.&lK7pUz^nX:ܓvι2H-Wypș?ƀv$ʅ(:ԄɛM} /UNeYmD:uQT5z F ɺ;l5t2 vdKpn{p3? Qԍ^ P [ 71RȾ.sH($)3%ӊd0(@:a azt?e]N o|0?gU@stO(< BFG0aA\ 9El ּJĘjkNȆіGʍS.rC߀إ%&sJ"u"glA4t~ `ܼyv~9<97H=sߕ D*nQz:Q$PM4M9bmuF=_vN u955kJ[ޛւi1&n1Tet@R.Wm8t 4BKcNa;x'J~}vρX7o6">db([S{c* ߨ%㆘xc,1"ZuVZBJD{Uy.>SCxˁѷݚHS\0K#F_nUD 4P`BEBV<ͨ=p b" PFҊs>ܳ.?wNS(JgxIf3Ul( @%WKx|VӜo9dcnr]fvY1J#hl Np[bI |x^"-5.h =^YrLMYT>eTn_P-c6Uy.W_(8kmuX>ͲyR?CFh)?ÄoDKlW@ZҞesr o*}!}A9f(hs2 "c٣1] ĩ0f,.1 ޸r4r;mv *e21JA{ Ƙ\ugEB9u>a^I0FfWGW ȥp)Rg7dTXxN!ZjWB$@sJadmN[Bּ2}8/}8Cd pCxqςAyX Y[#tѨ|ڕp!pjk&N_y35%)+RI%%Dwh5J|bLbLƭVz4H۝~:"7;/;e8+'m{Bk)o)6௉X  ӫ*T gE+d v~8~%p"Vd_a,ˁ^ݩz^UVkWO0X>dmѤ컶j(s\mChWa9t;ַ`&덑pht]<𽫚W%U+H+^iy@qZc?LyFvLHD99rr+̓'OV15QԌ||RvoW& (N]_ֲbOdƴX\˞!1q 4kaJ޷Sp=5VFlTS!,iyȅ@"0Hk7$6n3|ܪg$貗Z }_QEu>Xs<|iޝF1J4s'ũe^jf+w@ (wCtQ,gf*TOXogb^qZ\diKU{vg@-{r"PAoNރP=zWF'ȲׇѬi`DhZB.W!5ߟS?,nRngN?WYrv L3oCV"9m^syWSc_L{ `8R7S(~3'˭TE.R Û`NH #6]kkS`^\H*T)6s;t*T{k!&QL <Ӄ\4uv#BZ܍{>OU*C#"椹ofK-wbc^oQ+yAK*9ƩƈVusCq#u.F z㣿}"gYNbOiNw0l#u> q^uu'}}KUd1DD$^nî吴2BF<Eg nI,+@aq̭@t,%mAF k.2'IMe<&+ps^!?׼GC9;8, >pC@Avel:!Âc6Xh2 7kt7Uk`6@ Wϐ4ZH\zg5]<^;>G%` ?+*g:A2YӅtyr[{iLyDZŒj0ϭS]=v(Gadu" Hutc{ýN>WOU'QUw`8!2egj tpWO]aYKG`*:jwR~)Q4}ՊlPܧWa$UV1*e σ!Y%W?3U)sv|8CݩؤPN`/g@L /_֑ t]]L-FM*8y.ޤۯ.Ahdgգ-)hOE XޝmYǟR%} PZ0nFmC4X4\7MpM!z[lhEv<}5 ` l3g ڃϋcx:0pMj(oF5z7/v-* boW%.lǐdhPS +Du%Ut߳ZZl_hƽP7t;`>W,g Rֳ05t$Wf,EGxD!,"7m)!c2u_Nа^6f.[h1/{>TE~Dz:GG l0q$s}$uP;pFID#;M_X_Dr[#n/Je6(M'Lo~dM-a{DVR+Ew1Sk-_98>= Ғ1VmHHF\ (p 7vm9VG2KYCj ԼT*=Ģ jŏgu撪Vو-]Лaqlzޛg?Bc {PAh*XY'UOh U/}'R(~23wnUwB ˔m1Y>DZ@hȄWBfmx45]J dp(Aeз/tfk{HӚҭRwߗ>T|!m7>l< D $Ѷg1p^ W\6N~_(e^Z2!HlD\N %W.r솦B(7+ut]Jy;r6Z3TKjBMSkzUMFu'9i\Z)e@Iu('#ӾLBՌ*s?\l4jc|z ^olxo6v;c);|P87;oph%Ux`nei}ėXcI[~␑ 钐iJ) $Y z |w|}n$ W)Pʐ"qk$[{G+:еkI}:VFkՑ'AC BͨYIF" 5ӥ<ioceJEt{r0jll~Jl 2'/+cF/Y܂*ݏ4Lo:.߷$w^m˹Ϣ`N9F)RK2  ( P rDX*у@HQMK711/۠2{<.+ , ]DCs?ڷ/sѶ^]+XZ0 YO$wn݃ nE_!;QӒt$|J/F>#dL>U赠Dyۿ05Ls#d~ogҋQ eWWo2ig>7K O\Iʷ}`_Bvl²!ע9"2BV%FXŭU8ƾ8ҙ+i/ Sc᤿9-bu% h]_B\<ژt? K>$߷ ҧJ2,M󆲌ȔwYcU;v|8ٞtXQmOrBpr])rdt׃)܍BK<*Ъ0]'p@A#[mWq?7+ bpj @4o%'W/eL@Rv7^  auB.cŢ4U_ܛCAA%gfP/ ʳvSntr3tӴn`C}KfEfa,ÎD+;oN3Us$AF1gGyDE#'g|/,EZ4|ٰlyCRG;;u߱Oʙ(k/4= ۴yēeX+VV:771FZn[\6?bDV$NJdmD=XUe sH^%C_v6"X5#@(|oD68arGg:)һs iTKͧFɱCQFΙW&Vyqf?emy E.r;8kɵ z oend*;u xo>Wn9F /."DBݒVZ6ȝ<+F̲3%wX OQs] ~$NJ<!Feː鴘0\`/)yf$l[o+܌yx:yqyviR>B]ts98݂1bU ^  ]}E9|NC9c1ĀҞhZ).tNIOme/PvA9Eq)q0 -m\8.V7\ (+!@vSu+4>eVHInr,j 9CSXgd?y`9fo/5ϸ=éUEXh4$wZ3j(HhZVQz.An)=rQS`>(\bg [\ z{bo =p5Ϙ/'&ikikLJvw׏4 ~v~M9W~0Gm'oJ`85ү 7sCz/ _qOg`ܝͱo/jEx&7\܈%!+a}ݱX +99'"0 X.Xe⫻ե'݄_g[7I  1> ir_1 L)R~L/= RP>neaɣĜ\"&Da#2)j7-)[굙vr]K̴k q 3ʻߺ ]~uԨZ-k@Ӕ7\q*6?tlSHE PՈ#\0R\eykJ lk8A'TTB$X?--ܞ鎤¢7'5p-h\&/A#OW bja3P"O*^e dAEcoz 8ԴbS6cBAŀl) aGe|r|Cuj?ƣznJ?n$Jk-]`n&ՉTA%JBs;ʴ,ivȋ[Ke)a@e*%4q݉S|tpT*9#.R{OfiWP.%UJNkS戦l0NNG&Y #k(RM{[]yqꍬ`018DޅNy⼏)PZ 1g$j1P[T*\¾&bfȱ1fQJ@/AH )`؏ vD1_&1 IQFH1<{1+4dV|'_&dzͻhFtVvxt֛4 Ъ/w?ޗl;l)WSD;sti?(ܙQDNWJOisvs;;ƘOs7y]oOdT%Xg5%~H^k)&Ncy} Y.#v[_rbM2&^~ , 4u CpI ̶d -sd_,Z6T'I$MȬq3 HF7e4xHT0W0YagaӴ EC1͓ʜ>jlTcm5l<&] 5^1D "i󸁴 S&e5Jv`X Ft>KVOI|Ky<]ijp;ѾDZًeѻቷ%]Hgʪ,G,M\yto<~͓eN߰7$J~B!驨|:tv/QV-(_C61F֜Ҋi,>{5S$B':銊'o:5/Zm+FQ'@t&cTlwTͽ.o(կw^ռo'ӠA)xw&' |@P8MQ΄b agm룽ȤJpƝw/%ÍyϝWo|jZ{UG7$ kE$k/HHX\d:k;%/$1~86Ig U'zBۤ@ؚ%5ُ1sY~΅#5nMf0r.}(0pP{" *m3K[Xz?1u?0 Ť8f??&FKц̀5F#ځR v%B Fijp- nb)ѧ2T"Mxi.{#+HBQu>`3 {Ol]4(Ta?Ǭ:LJ'2vb$[ePf)J0v֯4ְÛN+9^>rbc8/vŸ?pI#ʹ7&_(’w/sXp+ӥE*_+M OS=6yroSVho ۃ/G*9vHɜ3>$á^zQvD$'݂7yXL5rsUMg[ll$$NvYd|j;³o)V޲(!S˸BTlԩQ]HV+ה tf3),XZE=#:du|cO: QHymBh_\6h~hz'U(|6Hk_HWH̅ yؖf~QdWB$^E\(Y nw,O)Iуqz6HH8x c `;s80|4\-*RJ34& *С-"ek# yfl7ea$YUF΋2 -!+ԕ >GvZs`g0t\u/U+BDJwoWI.I@;І ~zҥF΄{ώ6 F p 0ʕ- dSՏ0X0#PЖq>2 i0X">2Z~cj#=H?=/\'!hl1EpDhme>sH+O$ OnfQ#Y/mtӑ߲SANcR05>ּ13?4 ޕ ODV94Z~'aMNTyTJh|Pbf7A" PZV:ђ [ bB}<_c+Y('. /" bnX'\,OM1;iC󆫽j>k {NẠe]hM(`ɃEza,"]Y3 F;֞y/*^VtzpQ=gK͙Gcql]̩W֒97PP[;60R@)?lz-qJ} j^VKm}$K䮼馂~z[Bu4]tZr5BpvoitR?NkXJ7h[JP|5jz E.5M]U]ȝ.ꥳ5v+Az|44a$2O׋+'\gx03_ɫo[ Lchf 1ZygHnШ셍~פ%oWN2ODcAB d8E4d"_pBBi3'! \"HA5j5l&3eP*O''Kg&ygcRXB*C_#k&K^'yMfF`qZfe2\oak}CN~7:BBwWd6}+*̪yR2Tg#u hGaŊ`SOsNA,rRVĂ[OQ?ot>dOഽ6&E/ ])O^g2Kjr_ hA`!7._D3?ZU*җ͘03iN}w+tC5\ӗ]~Z| :A\Y*(y Ztet\?ZZKԩ lx5gY]#n4->īv?~>lRP[f@S'vvnd!Ȝ)fp5X]i8 0Y؜P?h\r5IX4s:u!8\26oNNJW)e'6yԖ 2bE2Ӿ<(PD/ElީtNɰ+٠$ӈy!bnMG!PB&wUDr 1eX\ffGL=Qx2ۙ iaL.GCƏ6Ű9ނ>9fYeNz|@'f46 >/8K 'IJ;i3eN艚Oaz}.˜oyYSb"'s5d v֞ȞR*F,k%ɩج \  xgv aF ?abM17ǥ3DӯraR)i%>?y/ǻs j-cj5*@,Inz!m:Ga*)%Fq=YAodt59FA_8FB .VFrJygz4oݒ !LO"m@ xz&fj:^\gl I8!R|f8GE GWxD"2UJ}<ղ+aaoUb)&`/2Ǒ\R*x__`d1p:݂Pcg =v#6\|Kex;.D9 ^&.4cTr?5::^8a!Kh|0xJhOp`Xv;WfuLDg!肉.Q&tw B|iw0ʱۃdB`ϟ+6xTC$iv[UXó4rÝc\.:ip,#;H'=,/bdBޝJh,_T0EvˇjpHD,*Xqjb؃JR$ʴt{J].$}|BxHWF!XWPILg {7}?zo9m]sO?tynF>zsBCU!VH͖ʗGV91sjFsfJAA S+C#XJzMwh2xtfk3(2LvVi;qd1i}\4 cojYʕ1<*MO>7:.M"c!1TFNTzW?=WsB ZN*{?5$s:IC)Y`LYtdp0- `t"]y%s[ cah6R>?Lt2m\;ÇD-gקJݴSGu[`Sצ97Xd*=LݍdG ԋ,in*^Avoy)w cɉ5-<%ٷۺJ4Jie;I.%#Sv:ӵ*gOR+YsUڕZ gBhkջnglm536#/F̆㥩B_Xڲ+ G *.NW߃^+2ԲMC&קGƢéMm, +ƶ`;VB|OwOкI^d>ߴ[sf^L6BG﹄B(Ffkn6u] Fen\lV L-FZƂ)h ?-p!Ral /ڮF3*.Mi~D܂6,Ud%>+TH 'rI'W' HTBHfg2UP*2v!$Uxƪ$EV Hg}^I;rWz"o5Cl(x|̫G橂~3;@F a \w ՎՈKr۶$.o|*RUw\#ql]8V0{:8c3Ko3w&zn 6GyKVORAY`>xjN򽞮}0~! Q! %eU}xԄ~Py]aqm Hd'WFfRWi}BV5Znf; /9_wkķfR8Ik5?9ڙC$[`N>3>z.ZR(msMYTaB]kpc ԠǦaFOO-$vAJnHF@N L{{~3eKM1(C03 4QkkھpM*HOZ'Pۼ`TOΊ5Uc`Qv!+0R!'fڭ8] _Ri aH3G>5ϥ8H[YԊZezW|7{? ²eKs)j>w4#BO[\*swt%X`ٵE'ЭuU#Z,GǦ`{x+SC QIl)"56@Q~z3[ S$쪒 ~b)+-Ld35qp,R }Pņ(U] +c&vB]榓b,_`,D=;I;S.qQQmo6gM3uD5*NN,ZS>~u|Ia~-e$} uzf'tE:D1p?0L)t"ֱ}fcڝqg|52b+!m&FEb$O R]vDfZoˡ:H {[  H0Ny˭ JN+"Dcڧ}[ro6tP)?@[)G|A~'ΖRE@O d ʚLJF@֡L80s[d0A+:SxtJ~=-NxR rd~4 ϟMib8y$"$~m/׬=i7[XkL˘V^wٱ76(PiMY4m$ˮ5P4BLDp~BE_&V2j/F~h2Ec(+Cˢ^1EE:F/Mn6XU0+89("?aBQ ?HchM䮥>z,ܟ|-$['*T550?ͷyQWʐ ˇBw|Q(oT]LjP~ǑF1z~ĀA<"(}XA($&A3qmV0H9"7~n限-( f|EUrgYƑhĆlN+}j5GçQ /R="QIC2Bo* k &K1K͏lʦ! }! Q2_9y˙V@$#yMb]t˜?xioOvm@=kI=js䱓N!0֨."GհtZ'P[JaE] 2q b+chZ;4Qgo;2o|0U`(D?GV₸}YH9 =*s6OڐRcSQg\xCzJDגQ}jDSoWaĪ6a݈܂>PKנ1/rU?Cݹe[V4T6|}J%tX$RB)*Ĵa+g([qՉyv0r:Iq됄7Ħj1Zh3:w" $Z,PXQ};"&P?R㴒Z܁U3cVQ8@k\^MŅQT^D!Aú|' LMo4f$O;a5˿HGT@D'$-!Z0 0ŸH4 {J~շfPos*٩x]P9[0 %&`.>DZT7zE uhrNU| s;Jp"VL7%uHlӕYq,3jsփ\wj\JQ 4Xt ~;5_Xm̏a$nO{G^jpJ;|O,y77YȜ+už`aX~ d%`LH{x-Wgd*7d^, [H/_jMa~܁ZH4$B i/㵶Ri.+N &;WʸEJE Z@B?Dz3פ ߝ<4MX9.a/8rAgY&d[ ]j]v3θ 7JGqFGޓ )o#s1$YYRa Y{h(FAž?9q OȆY޻,co4_o+XL%l$',o0F/ 6Co{Q2M@̺ͽڮX>ѕ(5lMQhָGV>9+?\f0e׳-@ 2B79HqA"jw8ڪY3[jL#}uWGW&?5O)SZEWNP?Iglsf|Y9-< ImEļfs4{oMV!z~u&e#`%JAUsqJD>[PJaSY7{d HhX ɫ̜ Q W8<ʷ327#N[BzCݜkHjln эk6!yU3/c]&"SlD5x}-=ĺ&>8C\${Ł+:05!@+Oq|Jn[XJkG4 zU~w+qLNkdc~یO\dR|稃yM(6D6QWDywnBX#i|agN DSf:%-))|.,dujI`$DZl "Jkظ;nlΩGHN.-x&c-*3T4W%42fOG~Er@>)7@}dK.LbRx-hFm@_P( Y@~l4[1A)ؤ9W,sqLi,8i{pB2ג Ûf[,'FXOBϼx 3>$|XfhEhK=??]O-˟RnIDSˬ $C']kCQB ! :⍃)ã~hPHt!0k!A(5EOW A|܊V!p߿B9E/jb%3Y?#(2S`8k߃X$^[Z#ό=Gy~M#$=yPwr^".%XR;%'*De'}m{o{ 0u endstream endobj 526 0 obj << /Length1 2005 /Length2 14361 /Length3 0 /Length 15571 /Filter /FlateDecode >> stream xڍPJ pw'Hpww]H[pw=@p ].{|սEY )(ub`ad krY((T-1#P-A q9D@igk ? ^)@ :"P,-޷j B6@K#[}G#k RPY8921282hNe#h ` (U@fNF@hx"% P,O=?g`ado+ߋLL@6vF3Kk @A\͉`dkW#}{ʍBJS#_%2lME@66@['GZ:Mޏݝ韛z,mM*َI(%w9 n&LWud^`^ prpz{ `ji0["n`a~=_?^ [k?/$?', rx23X9,l.NNhdJٚ<}?Qpw.y{\V{+[_Al,޴N zghA֦'d>B=DKGqK7?]*-zT ,>X&V{CRd׀ߨ;}Mn0^7 }rrw_ $L$_b0!Vb0I!NzA wP/qXLJ}??b0л?ENF]zWf_래Lڀwqf]j~"\ w;OĻuZ ߅Z ߕAwJ0J2G/|W/|W/|WYU UMt ,L>} 긯"pe؛⟣Ha\qt~DM tJC[Jy-A Nyfay{{P qw5c`9da:/$ya`f֋K 7?ivoU1ʄuB PY"SCO:f>YʝlX#MTqvG  =_6jԂ'#FmxʈmO6о:1CG/޻2L,_ܵ %/sguHXe[( [ƴ"5'J#!DjSYZX8,/Yanwyі4ݕe4pWV̼bH̫LL` [׀֣n;RXZGeW ʳ4by,34DL*]T(s6I=kRݹ:1~'v& ",oǩkgNJ8}8p:@# ,voAK_ZΣׯ<_Jn9>9XSfK,p jd~]ʒEKnl U@2Iojmm4W/-b""cI,+ GdĚ^gΎ CC.r`.I~`1z%X{Wne+zH/9'GzFMlN}X#"]&2Y$!gn;+% JyMM*x)lŕ%x ao7mX|] h2rgŏxVJt.j+Ի #8HrNDn!qAGw(RGUUPw03Ve"w [wn7G C(:3nC. 2?\C"y)is}-Mo?CՒUAVW,F zNʧQAv'݇R,y7?#t-&|Jpvrk"WPb=zn9${A**_N[o8VY+GS"u] ˭Z&DOLg+9 |pĦ^ktRG  N*&t:-0JsbVDTeXP]g^enҮ.NlB殠0hg߲ na?x6c<;O3BIIrhCS<|(<)E[#DfOtMȬ Y$2ODZI{w;"W^Եu6{۟* soaBx-%/{aJް4܏z|q_7Ws2P樎bhѦKTD=}:M3`d@\VL-IW ',j"#ArV39Gz/X<?vXEҢ-/UX@G Vہ=zf!5Y`fX;'m*DUb ug40S;9c냕|.u?G\b&ZOЧoz"$7 |?y_m_aZMFϚXƋZPdkhGYUn2K8ӽZCQtܲ~Ʉ j}0>ʛ\pȭQc(W63SdJċ2hmH Fa gME@nW" Xܨr+af W (:.d@*) k-mXϒkݒa鷱}OFFEs\}_ܝ'!“x*43u EygBqv#c|wBȸzֳc2*x~QK4d韢0wr0a"Ykeu%K"]߈D3z>dhuk͂,62rJznaEY<Ү7=&ϱo W'E_\FDH+1(7BuT\ JM(N;RhnH-.>V@_)kr ,R+6zR.{G~|>?:͆"(yS $o4B7lN0f'y0 *`qeb)) @M8G 3{Bj^?HV.w}fe~)/Ci/KnAyd̕I"u:? yY^)ZQhS a7,J⾙gd<î)u8Qd%C|:̱=GzAzH#Mӥn;-Us {s,j ̶YvڤS7 dWan&+*nmI&8%,4%r Bk_&:o+<[ŕQ3| D[qHJ_3 g}oi*gxP#xE–B1. gre ^Vpr [Dfקa}b]XWp_(l<O,sJp=р>t"; pZ4_"x.h_c,=?6yVUi_vWa"Bs穢aids7 (u.?ߑ c}ˁmn`b` BN Q4 s~f.vxys*CIpf(]4|%|$]w*jjglh5qm0 "(Άtq|%Eӈ\|9Ɯ}c`]pS~MBΏM9 XHqmDNcR2 8INE i8-<]` 3~UDI.gy Mu< FZMt,f "06nqAX"  B_}@rkzW3f53)[OLݰlOl[ Sd<[Gȼ1XeLC;OCzHDrAkz =@ɻ0ҤJ3fti} Ϻc…a.IiA2:%߂zyܬIf5A.R'{+mw\9bn g_pp%`{/4QIvr6/tW닠 r"P2wjnp\ǜZ%'LϢFWDa=*+eje~/Wp" k,t!T(^$:XOCt1Ft}9eAtXPP &4>(XhvA2Dx3 r8'!~K䐿 ~f[>ɼB7$#R٨w,i`j|u]6#(=J #â^_i&fr3#0_bu{i\uzG;䗾nTfQzBCxjdH> Q9g nQU߱b+ nOHjeI }NOAv((O4AXӰB1ByBAāx0a oGF=xfc29-22hKp/RbǞLNŨ2\!_eu,3oOV4؂yfy1Ţ\UA[+憣[D&I6ܠcx W?I~TSP2bދ YUF AphwcWq(4_b?7 ^; r}VՅ24s'_[ڙOz.,# [澰_i:GHbY_eԝ9Q pr*Ah{T1ruP0Z Q|Q͉ۑa>gdY{8( ֖Xt!ב7њ ~;Za”6ԧ](xzLI@ҩEa2% I=m鬐D&PrX| Ot!CSȈAƋa?ϕ>uK^}^f׻`}vi/JYFfgaY&ːE"9a2Cm{Λ'mo0ԇ`uV gHVկރ`~%3# l<$ibHO d9N 4.c5u`iOfl˺Ťk4P~l׈&C5.6Ӳ=.i݀*U^{-3oҰl/!<91+^%^c̭C!o7$†X'mlՃ5U]̠2$?;<ߌDJ=sUVPAّ""k}k?~IVw*Ms؞hPp^Mc[X]r,2VLª߈ԩg,6bPIki/D\'Њ0@I&POxۏخt5ҮQTU^UKDdc5z)ϾyVI{F B_+!Iy;PyTwqDd DR"[dg6/$q6gG2 }Xℾ/icWrU? "7e \aVeLI6ZK%0 dd]wm}L<\%RPd"R6X}- K]yHAsawNQc=9ڨ3S%y9HfTm`yﵕ -8'tiEA]v|?p ]BN/:d<i\|? iD ̍`e@lf~bHګŕge&xntdl-=yT V7kr&hzmY۳z&,ǧϨ|Nu ~u7W۪ӊ!uyBs$VBRFzJČd1KjyT!8Zn;K!Y /moffy5x )AxVHM5/ wۼtLt+P:Bkg2|# f I;1|ݍNQEy!.{qe5)_\ATwN]`Mf7jo62S5iG`C##8 _V) COӝOhy~!_\lJX6- h&\g'y^H p_Ix)M>'M.ƗT{Tqɴ̀_ݜqٹdeQJAM}~,KQV pO*u @ O۩I. )8I)C #2F1&q+tRBK/N 6 v&yYZKotfӱRDteʹ&ykM=48RoT1?La,T-j )8;?#JcۖYjw*UE30;$sZ$ᆾAB0et য়?qƇqm~uehUNd,k~A$W@囼 h4<˚h c 1&J"sij7 |~%aq[X-JO/;xgx .,Ft֑naQ] \:ľ֋G9(_cmmn"8:0 hY:/cDw/oLR邖.vՖ06^C Q7Ǚvc#~B{_ _0,l&8SI1>^ 1]XAy C1cr\Jyaif륥e`z$,{o*S=& 㟤>6Zψ:6PNms\ `m6D|N[W;iDzWnb̾ͫ@&BG0#'q^>Xj-9ך˾XfQzIZ0{[NV*RouZpj8;fMiSrV3'8T34 V{ &S"{ؑr5@*؂ۚ*ggl'Lő,9~xaό`i^Y8B97)&Q3IOm 35lfr_j%։f RF;Y$A'6WH)rt^`4nxۺTHpa5%dޚj|X[a~2{z>H {͵]hbKzn6T5a#'G#oEeY ÙlWtxS$ln2$A U}җTg-j =*viH& O MoR"PuL M++?$ِTup=ڷM% D= _sb*G]N*o!/]kh=O>ɗBKeT+ N3 tTڊ (`~tJ^Uz#bp qPpO09 p"Z1WC&Eem((&~|徟xP"glk/ѿ۩5sY'}RMqL\>KUk܄1KE=tuj#_`f#vqƗ$;#fmkd5( vEc$\`M5JcC㍕xzjdǫM$mʥ }B8Ŋ(d{mMX<? :VHt,;Pu>?K^}|b/`E@1 >jDv3~\\C`i\YE%9HC74MUW6PkF2FXh7 F% *!.8Zޑ[E $}-i' Kŗ_#3Ԩ83[m])^&bˮHUCz.xı ÿbX,.0*iW I8+Fg_Ж ɽyݕ ۸ C$鲺BW`9@"9/Ǫ'_3/#p\XUIvi a`oM<.؜KRY6Adl& /`OD;K `>.ԩ\?̌FDa-pUo!k4P!+-@h YDA*"`1P~Tܓf1&l(⾄QMBZik'آ>:ߟg#1ka؍Au2{9t,^ц5h;E*q{QL;<ۗNTÓ (3ҕm*m𳿆PPݵ:޲3%^g(L%,~KX/ u&"Acύ>fkmo$J!Pv*>USķNr .Zˬ(,&M{iANo$85o#$ s`x?e!+dLdֱJ`KMcTxE5ժ?e~DXQX߻Zզ:%u(sܗ8W3xB),O0ksӄkHRTNN|l)Uyy狋֓ЕַPw Mq򴏘 "I p$Q/Rdf{6 8>5/)lHr!o 'u|Yڨ G%GK0c6[ByTXfiDb LOtW^|j` "6ѷ2uY^mR k[Q}K>Lij( +=7Oև)lXCEYrsNeEʩ A OtP״>jr M 8a8 MwAAH[S}vP([%To4h٠ t"Vh|r_x;%9 gW 8a]'|d  fHbV2ì=sy X{Eiz-I2"̈́~-y4:Mvۀ:L^ƸcH[5nݜU,.%o!7ٔ8xt*C/-%aiqPD[=8X"Zϟ[Av cB=A=L*}9włK^cډt&,|]9!Tmg $N!lm52`[U^E?E>=G.^_Cc SP Im-A\㠒z)(g6WϪ8P|^/~o5PN%wGY_{l@ާr3Cve< ڹ&yἠpV\<~ ԛGYTNƢ//>Ll02= į*YΏfq;^2CZI'U!ʖ'56ՏHvQ)TɜEO2Q{0y8 q -\ yv&he.._-[LF)_?R,dWs$\ȟyoonb F.0&Q5W۩+ Qh|,8= J$ı2j^dDqS)60bOd,}0(] r]6Z2gyuFjJiK5h1 me<:2~~XmlDE0 gg1Nj@$}Է$^/Ǣh5sUZ+I`>׷cr0-'#dL ֽ?=T),4kb3y"m/:ocoGkoD<'nwg05 OҜݯ6i`7sm2\3jXOd$ n)d3oO5oc9R:S|gw0c"JC0}oщza1͋H"Sص-[=GchphMֳ)1h!I ;hP,ej Cb|zGm%ҲUb>+Jxj> Ah@>r-cNK;=KNo =,3W~Me[Xu_~םq9gFۙ[qm~`_9NTY|$fSxj+: w6GϷfIYPO~,R>5C |:3粤7Z6q ̕Dꠐ0Weu0}/蜬-WסP j˟AdZ(QBIszBf@EYBafb-EC.%EjlpuLq.4f(jfݩ+X3{曏-6Fn#=)vxvK,˟p~n 09}@YqHVlg&Z˫A@0QV9Bry5<ob>Zޯ_:\fnZڿQ& k~&7HD/BGsywsJ:_bTiuR`⼶T*ws2[+> g.~H`w08ʄfQr1l`uѽmfԃK]n\vA04zëvqչq#Jr(iS,⸥s%ͱDJ,l]һVj#sp=oD).>E[h~3~[>Ջ)aS*2)i4"pF5ZAAvT㪴fGTٵ!q&43jLKcŵm?-~qr`A[9@ `YL.&`Z=@m95X}hrN'r;9{%ZU$~B*C]LJ$oc~(I=Mףߣkbo{͖VVeHXCjS"XH9 ZJ 8_%&=5_qx >C/s$.jz$HYuorv];Q6 pjUDN2E׍ X$-qeҪ킄3袰l):**|Dk`gՖUNSRӚ7(SR4nP,K |3V )CK26"N>nVE!g ؄|_&Cf IRD)xʲlx6JվS49گ O#`wD"zgcv+ WH\VR|7|U rf }g4vե8n~ן1-AwH B&.eyv}wSc'krqN^gB#oKG'#Aߙl `'2"Z6!%*Ѭ"*Uf@|9407q$9Yc"|/خq4l~WLЏ3m{A'Ň|\5t#!ؚ^sk92H~@ĭU/Dʑ}2%Ogz^fAaκWFqY7&lbD4*=eOEJ_I{d96A .;v VU̔=<ʲ"҇$1Zt5%Z}fp n*b3ӹ}ѿl0"0Xdnp[)lx{0Y*k顑ȓ,(ƫ @8DʄOEn2̥. \(%0Q"akn`~q{\bclM]$ucW?ЇbݎdYNRƧ fԞV=1?}BO_Z endstream endobj 528 0 obj << /Length1 1387 /Length2 6001 /Length3 0 /Length 6952 /Filter /FlateDecode >> stream xڍtT.eHЍtwJ 33 Hw*H -)*tHwsw]5k}߽<"H>A~@IGGC BDll0  C@ B} $4\A1)Aq)  J (a?@z)!<`H1zpq%%y\0;B:B]'ځ\F;2H?Փ !PO750@ 3?oG<f{3}8HCk @_f*N!\@p?`sTH^x" oF~w*@isCz{\~( U%+$՟2jv??u#| {bk P As"@ qa 9 *oFOpأ?O7?-"AAf08P?6z0_%=AohzApW@\ϟSTD|B@4]E$藠VvO}M/8 b4k ( C?;_U_$T\\~90&-ZAV ywT B A&3?P BaH;?7%5e~xy'D {l(ZNCnҝ#Bm@}3 G )A{ѯ5 < _^T@ce7 #@I;U@U* d  ;UF-z9VuCP铮;Z?S>&M&p)XZ=yFY"TG'>CgN#6@4Ūt?~,~ME/ill^vM [}%(էbgVCkMе[*="dE;/n@:KQպǎ;i:v RIMHb5Q+8yxBDֺc?o#k"gc^9RoMbCԮ9]n%?)ί;y|+6=7v4 ctlLv̩S65O~z{;,T 53:/#nVfJz>ՄsO9VEGu(b]OzcGH{_+԰NOJyz:08K)~~ޏFƕQL)T/6VB-YӇOEqǖD[.5^մ: S?8goPXS`w?PT:Vӛ8JyW-/2(nAfՓW 5/ʲ ryި&X%`Z&|X mBmNryɴƃϏN>(f%OKQ}UsI!"vt1>>%')( 3ޒU6 >c̏:r+6=-~8 d*jxYUdG.8Mj}N1wGon%q6?ԫl,o^r*ϩQ+kCq4zC訢R7+ b#ó9Dˎ>EZ{֎xaLUj+WD(*R5ܱotlJN,*y@my3mہ~OR9dٹ{ʖm5ӚGb] u ^{/~2@P%U32wF_Uq%b8P3<+3GbȆl׭wZX2CF_b+_iT㨥g럴1Moڽ8Z|$*V f[RiyIj]{}(8%~=hooV>ttN@`F1>&< K$JuɲQ*vhz j၍I"&}fNjoNb*wj s[ӮhVcnt\AN´y8 k ֥FlD<lV}v|rw94躑VOmNԄ|ɢu<=Ѱd$dL?YHAܧi[\=[uZ&Q~9ƾ$#op0k^e;p_ W|>uWf^lxh 1U$X8HB]%erGFE}e \($u ϰj0v9=Gf5X/ũ^5iqpnf0Td5~'2鑈+KbYsfzي0.?٧n)܈Ω}'0&!sqaU$9\n!+Y{Z1w'W&&)'nrJZU5Q˵gqQX ilYABS!zwC6d8ӑd8\m[.)hy*)J-2HO'Yb& cyLwN_1)ऍpEJcvrjwy ;_|]YpB ٍrqWZCuG@ȇt Ϗl#KsLjvX2ϓ k {j_^(&2M {@~fdm«g |v<BBN$Ԓ|Tc*j{P|䭡Rފ$(_k(r"LU}_xzÉ";w8źQzp{gSQGB`j k\ ReEkpUsPxӝ~V&%L>gj/4$\kIK2bTKjTg-^ֽ9uy8WfnYOyaٹ36hcSdV.~?C繗پS\Yb﹮h%7qAo#MT͉hꏫJ-YZb8৛kѢϩ;fiywظѵ;_8*9?ԱQ-ۼde`TTu(\[,cNC}>^OٜUfІ+r6 YL\whifasR"ei*j:s,(>RCfբW+dF2 s^/^8.v̮kh7tn׹U=uDvQ(sD+^?l 6몎\ڼjƎB⬞52 FGL as" d*hlQoJ6eS*V:2߼Z*R~;`HarWH؋rhRά05ج}ڮ;eX᳓H,$u_5c7旅mU0M n1r<.9Å=,Wu~ )Q)Ts;M=*aCZCjZ}Ł.xdQV\̎jԠpLw(V-b#&E/^Tc^Ƽ[{jA\\Iʢyj]}a/bSr=u]|ceMT\%y'/ȄP_XP \"uru߭K{$9X!.qx`i~31<)ýZ >ʮ=bU"3* :R]XjX, L \!7̥Zg[ac}}|^$@˄,@xl!}?!Z7ǵq,W+3{`JР%Afj~P = sBTQ Ef_r̩Km_\zc3*3I, m`}l*_ָbAO ~!AXw븎<+bLe:{b.i|OƄBG2醧_^eoK0z}`K+=$W "15a_*J>CS1Y!ݧy|SR@a Td<=,yJS>"gRGr?c#:q5DMF"E'%G;ψrB"J1yюSnr$oZ('7Wbqw j-OrRweٿXV R澙EE}< gj\ "?G`?M8xx&3j; ,pj!PDe bUig/$g'54opȆkoKPҽ;{gTAZ=)jLcGT7ĥZqE{x}|Q(ٙ~ė [als`obx7T@wCk#]è=n*8=ۨ5)ĝ ]`JƞeaoY1@H:G%@ EJbLf6]*, E֐M njح,<ͰH>a ]PJQ*B^xD0wL|%\3Ɋ5%^g;B_Mz,cOڒς辂 G1&GoR'厛J4x4Wx!aJ|@$!J-HÌdi0mrsxqBh,4`-q^ 7DFI@U*VFHW%f<_SV8N/sN[>u& F&;+=:n-hڃ Qi )bYq endstream endobj 530 0 obj << /Length1 2721 /Length2 23992 /Length3 0 /Length 25517 /Filter /FlateDecode >> stream xڌPX;ww'xp%7U[TAmgm;&'W21X;13rDd,掖@{sk? D G5࣓%`fd=71@O.bcfonj:TF&..ڿBV@{s#k t%@Tf ...V6ԴsG3"h 4J k`;3zxrr%G{ $47Z;<%Ii-oc hDo2`bn ȉI;: X:؀ - A17 ) @ ~UYX hs{n w?WN *vN@De@G### h1 f KK `Jenp0p흀^*FC5 1o j+@4{L_?~o )-)Awꄅm\t:f6Fӯ!}oyh+imb-LcPԀƒM-@{ȵ@?_.&W!Ĝ,-RSQX[cZ'G؀^ZJ:A2;*(o`ZЁZt2:@# ڜ)jmdckŘn& @׿@omr(;Ao`8 "'o`q0~#&o `X +o"HF .2o"/q@\~#E7qQ@\T~#E7qQ@\4E\ ._40o=(obnho` zLY[*@El`F6 c0 Á@?) (wcKK߆@";42ÏDhel`#&!(_ [ :Kۜ헹y0 L~scffk$3eP@P>A-,c#UPGfZ6=6ɀm%c[ f zȭ3*LH;( ֶwYAtr#>Hb{@sqZ7 ?~Jjl ' z8i`5=0 f?TG?@1&9A\0orH@%mdj_(" h`chQv_%B;7KLMd&2V(auu[Fp㨹MHKBO'XV)#~B8:e=g;OUOɳ8s1]]kKVƂv*٥Jf"U>kΑf㼃q#}~2ws;5J18Cs9a}L١ W}lC oEͼYHIti֍cKL; @JFL=wrXz,&km&I\39$ #ߦnuvk#twI-Í#^j.LEʹWWK!Ԁ OAxL[нclٞQ^sh,]u^e_ ;qXDӃrDr֣)xjs% ݈/ X%{wU kY G}W PrKz= gHz,Lb=ɅD9eL!o⇂^*_A찛F_v{*0tԵ(HRFՋ7-T 7ƠIߴ0bOK)ץ h0_3-3*k1oxJ46>{bOȚqQ,O 1$_Im^mVf*v5g uXXmyJ̡ E y@:j4J͗)!(K}5"'>:QzpİWÓNNu/H_E0 }3uy'%B)̳Fwb" aZ߻76啷ꬮa1}}|=;p*M`W_#Ao,Z+Uiq^l=^ʉgy*vcy׭‹ݙvK3VޝdKnYmQa, Ʋhg / ,w,|w_2g4Wp~ SwӾ0l'"t[B ecUM`'#yXDvS h8>q?+5a{1Z'o2>u9" NruNmI}6}nXN!P5wFȥ=~,XV[)-bYvaR@ٕӎDxb{I0` c\Fݠ/y_y0:6% gIQaS1LȰưCnw]B1@ik6Kb٪Lq"8|Cq;TcmOfu ;rd&Higy+,h]CdX 6[3.%CO=zӗ`rӛaN./ʜM*D$,Ͱ[}ʆQ")qaS1څ@Eڲm-6,,Cq&O-Վ9Ћ l Al;ێ+oivnc&1QC7"Ҕm#oSkZVe-|`-KJ|B- G xR@* )c7aEBgKCMC}ӚPW E{;xu],z}Y 1lw¾ Oc{T[4y΂'!o<펫2W59YDdg02Zf7cd ί\@Ug1mcK_riLmyj7+O `g4g#9.$Q0Fڼ$T^h+4IŚlX* 1#0V.{V;]cK^wMb|  Ko9Q/e#)*һ+~sZirxʍlW!d!\t5&1ӹ UldM2t;RwoB[n ؔ"z{a02,& r>cAһ"NO!sRUٳd%1YX乾qkF:pSo!/=p~)8rH,cKD^l鎰G6ǬvQ4p,eNkȸZ{1t5n%Bh+$/Y5Wƾ<Е++ʶHqDW(,k= ichڪ  "5B'UDs^>JtͣH|T@\t$yML*x6@=Ԡ[1iPTHaֳN(K(_=: _^/K!%Vr)p7teAJxsS(֙j^.k5S2CfեVs4LCAXXFj> gD{o6 mL䓍覵o+'(ym;ЛP,1=8hL&݊Wd_3 8ϵ@jҫnG‰;ocDcW NCMI"( "Fx,81Ù1\?i膁Vɽ+DP67jOu<+D XٖKh-ՂgJH91b*g*|Uy`Ju]슥@ȯU9/MHƥÎIhVAm^<|hsV!ݧ(K(l`yZM#Mjct, FTk9J=OOu/G(|Jvԛ`$6 Q%|4.JQWz`:䞶d餑#ֱuVL13y"NCف} ݫĕM_,;lcJs}f2 ."y(R"6p8B}ZZ J(>3т`U<2ey&*O&o⎓Szm/ .[LNĔ;MA69 鬒“-)AY1cK Ty&l=/ɀK5(L1 >Qk,$]6eai LXB)iYM[s3+3r%F2o@oH~Fiڶ#b9*eU]/ [7|#}1 m9xc69ev\P UPPLn `N;E{@fP6yʉbҔ.pVS NѦ@Ǹ{lY;Bo1~&ϟn)})Ǖ{>-Y=pc"j,Ԗ[6T;鉧_bőf9&kX5Su^04{&%\fdE:g23R6 Gq36|e1R?,3/|׫eLMʮuCT2{dJKb}b[:igR"ӵa]qX #ߋd MhV:3mE.O_07؉q˞NPt[k*)x|1dz*άZ$:2U{NhSrrxW7L="a RT.C"/LWߟ$կLӧ!^'`,}U=.LDk+J]Dg+H3KBiJB;[aїnesv؝k5HWp*^Y' O&)oz|K/c;`o24B ߂}G){x$*sP(rDOM:Y䷠U3ߑi ;ygRK:t}!g伸/^h#I8ͱ*?O%Sf( 6E\+qX79~}?#zNh׃S Ou]kfӁ3%ƃJ+KQDa^]--y)17~=E%}"(vv_ፒzc5ou٦_;I؇FZej]%i: kgZPM&Q); HEh`{9Kی6|âˋw6bziݑ*q!(* )Oêz?!rdž~籈{Ƣ>p1#n9t#Q4B-ʉRB]qn>e7ǽilAZ\jn Y0^ R#z]T^.@3+OzN #פdުG,z\ 7vAָq1cj-6Q9%2'̨c@оK8VEt GZ)bOp˺t4:u^nun(#[jUa%6ZȒw }鵦ʇmk;j0+T5]`?obʒؗjSO2g󂧳kZ^=KV+1^!T"NЇݷvjfLyqJBm%щƎ0]0L7!ᰭ,2t0 fr^-=H3DۇcU>y˃_m] rǏxXz+\m`+m d lK K:ūx/P*C:DӤ֍Մ@:B4!!Lj8O- UB4%4#ֆucls`a \QEzk~wv L [ ? < xXFJAZ2tdA8R4ckag2 [^DmA|:}Kj0BΟ!4kߏeN:ŨrAMl}&[RpAo۶ƭCP*ך3#b} 5֒twY<&,l{ԬE< !+ܲݲV 6hT&o}y7tbnokhqLdrZNӴxN@&n 1]T1VuLLibGJVU@dxmmZjj&^/}]L#YCỶ%M[;9~O Fm>LKa˞deDL!qwH=f[>.a|6YWғy.U 3 DxTgJ褪r*oP"ha7"rƑ w /!=t}v)2 5`kƇMs Qg۟~^ͥY09-ćR914m9(bY^W҅3VrqkW>7X 35@C#9i#IyIu+Nb`nA]pQF2)ո4oNF:&|0J̨hz~Sz,xf -.7ߺTB@MbCԎCFTkB.7F, Q8{/ \& ~|W6uzZz,Kjqm?8%Fg%Xz6[s:I $Gfo4}o%3ȾL-S};YZfg|f5E+]za[Dv?Bv/.gVG'T)2>ze?1YZR^=eγM KmkD۹ҋdz[ 8ݺaq»뛈׾ e0vxJCJo}'4,?~yޞҞY$4i~#.ն.F5]ycYA "Jr2υ Wx1R N%>ۥN8980XZlB"C޸j툑5K#\up.ƕZl{h ѫp*uJkdtL#l4R73B&p{SM.뷞pbޏ60ƣb(TY4ޕ1pH,ϙu >Y׸3WhҚ;Ϲ%$t{cFxIr3͡ʊKDBx#?'W[c󙡍;SfUMxh:)[`>B ǏG%A5I ~jiFbO+$_6W[=u~Ʈ-ͻI~$n4R3J,6uE/酴ѿǾ`sA/pz#1Q}i m1&S5 N~q8TuF:)pu6Evg|0'̍#F+,ւ>/[s-퐼]RDuqi)[[];#2TRu?Dn܂S~] kn-ϳ@Ƀ1;0>c>z鴨JF``7A+j$$ɢkv$5)|8 mq3ȢOGBD)WmRT4F?poLح]/Hkzm|QO].zc “FȌBp]e$tKW˩,Fc,Jnc 5ʐ!])-Vܓ=>Uu&-0+EH .Y7}=͝U`Xէe<_>dҖiҊCrEϸIF[ ȍAp<8`¼u>W6_6j (%TRTTÔku"}̆iVQځlv4 ͤ]8#k4ݐ<,L2CEdVzlw_ BZU_l  pwڗɾgdQ-w+8λ2e 4oұFv$̤d DSVOU7|'mv:E%LosrX]n} N^ OO{н8 tX\<Y2/3gP b}1ݰ3/V"c#*!bԾ nؼgL|#]#+fR֦#YiS84a.܀f5oiTg>k`XI/>FV$(D8pSr|8% !)/VSĹZ^ Jo"> ѧxm; } =f, z-9ZZmJCVP=rhp v細 U>hʊl^ӑg9)2pƤ?lp1S _$Ԅwa߬UL`/Q %j@`h Za܉(^r!/Uȓ UBwq !*NTu2Kh:ptg-7]"`:#*Fxǝ6 .YB6&jHvqi, ?SS3=n/ІG#_VMOCnVs{HY0.hbAn͸ՙ"avj5jJAj7?ݒ~~@dE(GxWǍֹ|U! XFp`y,i-6{ ,r35%8J0ŘE[Ei`cRG0ڵSt=آ~Ow ojol>lꄨS?E %]KlPI}A8id|iB4r! j%9^ߕh$m6x:3 Dժghb|zn kRbjnpvw)> R=&Mtb^;,Ψ?r$)iI_.Z,_L^cL9Ob.BT<~!"BDZs$ ~ϞpOɽS CGpg8*)uksO"ߕA Ay>ÆӧEB^c4)[{zf3Q䳏&Bu-Ō㐷Fԍo1'/0#š"GDRdK QQi-O!ռgA~Z:5F?>ޝ' $GJ EaÓ_wo$ܧKeޜ"CQ=K䛥rTݑW8V8K.'c$>s( pNJx+(븽Azή=U(JF/a$/MFYXb4ӂM|ئG<ĻvYa w\C;yKȆUHItE>79O[({uB fz ky!"sɱOyE3`wJ?9,(w5cGb!vjߣeO89.Zn7b{K(1ʂekWa|-R4W~ Ӽ _ ְ㤾޺w'aW.͎  T"fErDWd*IMh<װδʅI?c jZ]0uɔ3˭x-85&1IM|t-[j˪7gQ޼5|k2=b6%) _f!lQ(KfG9L8>M'uBӾ0mNy mg@ 2|cp<*ZLZ Ulgtr@g|lvgLDv8NKW uLf [|ֵj_{O^2;mI}OHĞ f -,Ea'r zkun$a ܦ+ŠQ}!(|~]AFF1nF8eGҜO TE|»Y(4(3c8!}6[]]dAfp Y{{^ 62O&<,q70}]̳){ItT?.)8 ޢYc Ofj۪[R5/1ޟZ %?1%#yҸQ޸4{v ![Nkf&".7 !>Ї'b}L=t.ԉ$ˑsX [5;yEsxָ^Jxc];|喷pr&;tUY zdj#1Sls!]I1~3qC/r|tmE޸lעeլIώWڛ.bqSgӣ ڸ6׸ 3&?wFY^)և'kբ{(jy/y?[T4cG7bI1/T*IF!&ۃP{ʦ@&8} D2ƴ(^¦sNEDgR9,B"4ai/z!V 4DH DV/k,J4 F*I(j^rcb;]' ҜnRԚhC{iᘌ'";Ӹr2fbpSђة~#yyo/?J$>N+ N UIc$aBZ'RD=&7`*W{򋭁ŲT|zXa 8ӎ&ąu1.3q k^hcReOx /* T(2-q 9vRZaQmd?&&#?#ERP#+zTB}dŠHQx.|taSA2Sd|& <]@iN}`tś6oǶw~؂a } vfmM%j X!uc{kq0F=1N=¨ +!lG59[ &\*\7 E96*]hQAD$ܥqAZ'Eli##Jξv&4r>STrB |Q&Z˪7<`}QFE%#pLcJ!`O]M°b k;UcY9 'ĈY?f2h1FۇE{*X\A;Q_ d{܁wgWhE_'i3Q/Q"NL:Ec?.+wE<|, `;6P [ZC/I8BZ*jd?F*.$^?DjFj؍a2lEP#a^>SEV\ĠbP ,+UѱsYF￈=ObaKjC A^1r5 @nm*zf7p1FNT fsG<+XWO:Dkfegb6/ _HLY*Eb/OqŊwtƄ)ewo]91\ۆXi-nM[2% īY?2@>s}oVEFdfY {LPf2fUw/:Sj_WیEr)_ )b|%#@u\I0T뚃2%Je_,ĥՄ?LT2&YªpHĩ6-WfMbq{j\g5qq5Ŋ֜sc7>L}Jlq+5z?L*`hJ0xՋihnט):¤x7hw`W,F7}B< <%|;NB=A<+&(q˭#"0 #ߌ{f%#Ψpy{ltQD&ti|ѥ+vX>;O=$RbhN%y@1\wkWӰ|C}h +ğ2zl-\N46spY$38a}=Rù'Ճf nR~g,R4+ۅj A@M[j$XdX xF^ڴ&!*# zŌH@BI}ahn,1<1~9UN]e% +|aʹ lmjW>C-ȱبi#tS:,0qH%BE6AvYWѷajE dB @5YYRߺP4hJ{O*Fpn/f8kGՓbLQM SloQ׮ x`r|d1<2ne:FD '4~捩k7*ψ#hgs7,7=2D{靵r$/_.b3|5^|9WGlyG a\n! ~ׂJTdX]h^-c'R ƪŤZB xX m_DSݩ*7_ ] tm Tmd>P#/N j{)p&Nvy1.%ј%>(y˭ٵj&2vwjxPņZ%`ЕE>;O+ ,'ʹ5Ò$o3JJ/ݱY]IpSSX.wqЊNw>[ eY%MA(Ѕt2녅t6yQ~%QV79O!3w\_ht]bZ#=޼* d'z!U΄ K v 8.2(qA"X:lC6j.YT.+@n S%㆗6JyEH_]W[X8+,8)w53q璉L% [TA ´Gi3?vr/|.Q_̤͠躬ajȋē\;+y?eewUG 7pVNl>uGRrTLH!Lp9]Ӄ-rc7"О$-ĖPeHwLJ@0wap+5lR.,`l7fp˼)klfmOE.q p,o{8!mz+bCo }3-"2߉M*%yANK/ըfW U[b=̡_k)n*/3OJBBt)d!Rc  Bb_+yOKԠЕ';5r3k=,B^蟏%ԦCKf|"Xǖ$vPFj>Nh~ˋHG2ȹx [ }JY:=:H'f! /s\$N>>;O}rҠZa'Iy0;7ui H޼`~Oͧ7L~)$EŠGGr NRJѝcPDbOPfGʦ@O-dz(fN(0A B]? - ͧx7b=og]z fHbd j4_Vab {)& |[s_h̪*D$U{TQxWYŖG0fEP JfZ_귶ơN86KLij5dTD7y"+M ڴ2F@TRۅ3ѧ/aa0ƿo (S"x6d(ޟɸ[p/bX 5,zj/q$Y,?ä1|V?ɺ.+>5/&?|in 67e:p -׊倓PV$hpֲ A!T-7c`L UTEED<uM[[/?b-JEc0SvIjFḙ_͞5+7 .Y;9/)vz7BVzRpb1H"Nei@ڐGrikw9lL}y;}v`.e|/g tGQe1km|xif-OǮЋ.abٲZ71z*ivlNF { ,O l'Q/cEnؽaocD I]^|1\2J҆AZ"lC}&%Kc֘OȜv<3k}W(XL26(_vZ/xU34Z#+݆jnuzm0-qGm4-AU#<=W y["Cw+Q&ygǬFn2$|n=J; sK&,أ@u+Ԟ{Cc^z<+ 4H 6R9 "D5]qoV<>Y%(|Xg&M/uz!&$G{ [ݸ"ӯcaMwk܉KY7qQ ۸da{;g4wve|};w?=Lo,_!"أ)P-'t4SFSfa 1Bo[ 7g^'kەh QE .Vtj밳;80Ww.nи!|^=^E(2 JޯxT-E,!9zp~ݓgc# $d_e(=y}hz9ΠGtt7L m4븯,WڟBjj?wYUk'):F EVw3vGf;YdUPlZg駑ҧqBOt=ǹnQRl_7N5k;h67J/+gisT[&FSm} f]f= 0>8|j06ޮ-_Vйȷb8JVhTEjP1>Qd/"KQO8.{vMhZ=oH硱t1C*.?quE X0-B V<1 F(PZq2gmZ-DŤ(sG#J陼B\j NW:#E wz(FSNṴV9eH/?*Ӯ;|lG5aw ʯ`S:%}$bvseB~ulF 0b]ޞN30%=r. %1wFŢ,! OђU*Nvh&6Dd T^rFu\0tRV 2&`j$btcd됓Hv\uTkgc q|H. P4|\k|G1Ce]",`ͷÅ>6CpL3q۬`w1,o?[|km?SqS!O%[]D*u?6Y9Ѻia9GԄE{9z'BO3>ܽ Hd >d?~x/hFdr]g(LN_jrs.)63hMJQa>#,L-_~;/f2jVd ~VR[S~Hگ- \%Au,󍒛A4Yanء),D_ѣTM{G08W } u) whi4/)T̺mȂ.wwy@ YVEr]ˠ(D@i}uC*}! \Dʻq]p1僑f?E%˹ͻlw *A_cҍDܯ+S7B%t`zIݮjGi>С3Üb>GYY%' C1blB6g TVx܃dL nBnͧdŘaЇBxeLg Ӷ3èy"^AU rmx<"qم/27랛4b!MH+!G9Cr¼B$n1%?  {sJvdiP%:r#MyMYOS'[]1@®Q9{OGퟹeJiR10@E+mE早׍8p:P'!eiva\XHZMI7rxV6ީc%v~yȥm[\p%ݵ&WU+[ tJۺp+{߈tT!%l{ni; ghj-9fo4TVG̚يwo{3TE'wI`֔PG‡Huߐ)W<ԘOML+rnN,Lt@Mgv {&.jPôž{.,HyJ<& u':*}T`EmZPh 4`c(p\w֩$!b7 T!0Lq<_/dz͉#a>/7W$;CLTɶH)<ġ5)Fs5“L0X.Jґ[ 阮O旽",7v_bW_7x!.B)1pN }iXO'tRx]{S+l#M tXkty2,*"$-%e*#KdiGw+'(pFr/}d O+&'!zɯNMŦiOEBb͑@PñMykWs2z,-@>ۅbmӿQw4zq$x#q ?n //0(@|yDO*^dk~m7q_5<='WٚVp+17|`XN-*ܰXQ `djܚpIdGM[dsJݱ$A5K;_+Fȶ `eBn (]fe^nŚYUR(/ 1f h\ѮB@|h_pD!nΞ6ס}|D.=|!&7J0\DyH="MbbΞ)n<GmkZUKN$&0H KQ %_z;$Qigx| _H`.+U+o7~QΔ׽06W+%XZQ>{%+f4PS`̽(q3el쵵3Ca=*k ŊWivhgK-V7A]BB>Dj@$0v]~1Yo P>վT w7>_'Z6W R!X^w$4r(*v=7\73vftm]xQ+!s#q=_uП7;ryG|Sat9LpEHZ*n0jN*׽ RҝpUޭߑ.EV/_Z@N =!K( eqNNf8[t4.C ;{";m 6"6w۴2epdFcii-> J˃Kی^sҹ}n=s[:>#4jJ| m~#9Ձ/Ͻ V52!'>YN=Ӯ&Ӂqb:rٛD;)c 'TMA Hzw8yӆ2g3+ioX锖rGI4yH4+x,h5kǣFVGq/Wȶ;W_y,D1 jO8З4xV~c(w &b7+iGvf<Tt EEU9ɗ74"ՕvsnYcsh,1$I;Dh.Xnoݎ 9FZ{7LGlD$(_Zӱ%)PQio$qg}#M1x#XZ6qDk޼J_]c.Ѝ }[`Js@d3Ȫ(u\VN|p~* }YE1q 5|U1f_hEOmF@߯~9P`j~dv+,7(BHK37k8{8| ޒT)!.iD_N|E[ R.P-zyNgCrqe4ç(Xgb1G=ʩz'QIi9V+Fi _bM?I^Y]"eE+;s!Dvb) @l˞wWr.<-d7ă`$Uc=KLe&(J]@!Zc YƮzBQBDQN]ԑLl>69yW.x 2S_BکA8CCM#S C_#}R+D&)|Dbef8 %L 若~`G[N4qa0]20ۨȲvd8U[7YQP5@xꜧsLN䢏V>5Qc<R]ug^7.m'PBc :u+|di*lB*?EHMnؿ [ ZnɾۨS9kbE(DEVZ {negFhY\CƉ%V@ig3bZ $v/*vWxMbg |y2vrq%3:ǂ5ܕ3,0]z͊#7[LIf:lG_  o;^-2M9 1ŧƂr3ó[ _5\ƨC}DϢK'|N*KcV~8|UFl/H4=TIRT_z-/AWGb&|󡗲M]MY-DoSV endstream endobj 532 0 obj << /Length1 1694 /Length2 9743 /Length3 0 /Length 10831 /Filter /FlateDecode >> stream xڍT.\[qw=@ w8/nS(Jq tf̜kݻV=~T`t48,I%uN;;+;;' &jKB tv9e 4>ˤ̠vJ`= +'dg @ d Pb(8.(4Og 9 >?@gd:ei uA]h|lN XVtiAN@yLE(ȬP;;;7zXذ -~X=7YP]܀+ߊF(K`Q,Z ?s3,1_6--E}M]?;['!fpp|<j \V?}> Rv|&-@ y-8Gq qCM6s{eYW3:?wV h ru_& ;(^ !h<8ht):c޸z-VE/dъ6 (3Ϟ!B!1z`N_]O?Q($0~*_猽Zt&&'$OQ .)ZsMy9~kGYܘf\ O>*zM.ѡ2;tYaC܅Y%bWϣ*sS﵏H_n\O #ɩ/ر!X5XYkl0&kߴΪs8XѤ^>S w;b;tst 2Iil ޯiKΑBe"zUMz-JO:9+#QuĨn!z&p LI2? Y1.6lc AU G c.N X؅Xˬ5$0єPW<ʋ!1h{,~C%0ɻ7_6+ͼFS,H-qQJZ#n񻃐"Oa+h˭|W"Oː=r ߸Aݫef6}s 9 p܅X  @ "% ?fҘ D*U}1KHuEC%NǑpa,V+UA0T/l$K %nS68_츬 v:~enEyJJ;ZAwGm 9~z`}FT>nخXqqF^+Q2I1 aA}y֓yʷYT-U&7l~4Cc!qZƼ<BT.{HK-劑A $`ugӉǺ 2S+W<dO h='z{փ;~Zw^eFta΢D(n|g'/e{Ѱv~^k-9 !#WeiE ?{\Cޟhl9ǥdh+KaY,M?{0PoP.[Sk -ޣXk( Nk\%V>$»&w pEHk)x|ƛvJTX(wg@_%X8KUd1V1\b?n#w&sa[N<9eƦ} ` i\y60m^ūK^`Ѻtxw >1} FR%Rl#q?ԛEӠ=q=?O$`00iCbHv Ŗ–"L 4nk̛ʉgm>;ozosm034LFcw($퍍F}X&րObs}"ݕ",T?#VI.cK*hNU_#lT90Ո?,C3Ƹ [iF R@f@I[ OOg f]ҁkH8)(1?٥,5T"eG1V$+n4(~=+:Ab9dJe`_8I*l?łrbw_s6+`KEQ#'f%Bh B/7c^iمm1hnhpD\-ydϗAƾ\b? o6RJQRSz}¥ /] 7eS;FM&R<,=ҕ(\၌heC.  XYnE>OguѶ zTz0C$KjtXx" Lֶ6P\EЅOZtJwk'vV) oҍ7;*Q>\=L [ո3 z`]ְjn' #ܧ-ߙϖu 2&W*ѧ4.m6gU;cMf^8gNڀrqbxOFD:yhTS$f!03߅g42"tm hTKY'Si CS}mx r}rE*͈tED(O*=^ h~@aioThl䩻YBiT5,6_zsb%.IJyAn:y8kzZ&)Payؕ;-`qԟ66gB(14 m"ĘC\JLc1\~Y]䶆xHvʧ>֥/+Z.ekxA;vp>r!6qڑgI[B+eȰᵤ=aJ](ԖRXh}I =iws 6~ Dٌ NS{<1sD 4bwTk`3FvpB[IW<:c }Oߑ,B |s.gcF'EtgQtQîS4uw',Hkr9?~v2{>xOIn8EM2 a c-5]%}#i'3 mAmr< 0XƗ(*nl? _ CD$7REyV,DC:KY]ZĬ~U+g>̍&?p3 @'B g  )_EXLp/#7ZVK5^WU| NolQB#3r(Oc@X}u</KnUF.``XG#`ܢSX}B׺wt+za®R.z*Ԍ.z=Ұ<氺]O@su۞S?gT!*|l¹<8mpK{4zX+7_$rɮf֖ph$RcӇ36Iۛx0?m|O?%:%uIҶ5%c;PKE z~Z#'v}IǮ"r[1e#V}+A5Ay|4cE|Q_kÆyf;!Tolj_U"5P7l{Q:鮼N Y`[*c\&O@jf?]Ɋ{> rRL+U~xwtymfU_%%*}˪# o1r^ԪEF5v~#J,02)<'B X` 2Jg9U哩9ڰ-UmŸVqbjX(5Kg̍ŽqG s>."#P;#(Lt;?PA*$(L#,"{[u *}L.u_ 'عÐ1<z44QRU+kMO y^ޏtYh'L(_~A3zFrW\{O\5/ r(|-+e*U4Hoqasr+_ EGЄc MɁj|unWb{ F=O6!= Rٙ_:z՟g6/#o*Ka}?%)Ïf_u8_8dvYH C {IW2X nXH y"!vA)VhbKo|Ɋb"VkLu'7An [;4uVF4q*Ѐwq;[Vvy8"ó9!Mb&u[O:x{% odDb#Jd{<1%5<4`n ng mQ"ao7g868@t{ЦR\iqP0k|Bi0S.ԤqgUŋo7uB sԹZd,M%$Pb|3SQ4Ս\pSml>Jw,$hoxw|sz\ j;1'53G\Z.IREm%7TVD0n@K` dCu<\T ̀p韃Hl58OT.4}&\ӽu"5k.$0+Av}1H Yy6së:7ðͨpyj880RӇn\RmD2LCrbJUkliTB[E0Qeu&DaӳöOo?^\I(퉴qzAI.J3Ms+`aF%KOֿYvp~/yWKT&mŖ2MīEŜrzuc0.,h\'A@8d-\XJ,.Ӻk{Sy@]Bx-iq>ׄvA ÍJGPVd,%fo̎<eB-PU-FgXaQ_]2%u;qN& #M2W'y_Hȶ>QGPᙌbq?%„9#V Mb懁EK+ 2{ gc E,>WJμȖض1?ݫ9m14$S³w;6LP={5aoUq!+ ' N5Å\g_d^hd 0 bvF&MugQ+Az'ZuǭۧPb ΂g5d;հ^ lHi${M 5Y HmPVm;M1+)A฽s'BDbC[pjQ_PvTs?dʄ4AJϧgeEWzbpߵ+[C~$Ӳ| h1o֍}Fatĕh}Fc $uKk*\|58!Wn[MdaR߃gO,cЧ>+:C~.;KH*}\ |ato㧙J*=J<eP)Ţ4 81%بeTLs-l, Mj;=.ojIgݔbt^*cWn ﺹl`J5"10?AƜyvF{Ҩ%b@ 3M{xGO1cPT ]uWVZb".ӪfRUx3_:N?:y2 {YͲ`k*h.y|Q&U+(IDוڒTE |8A[▲5LEFsOU%Ⱥv~sO6'S?(q>:]ooxCw.&|LFChflvc{@˗Q7b;hC $Z(]}0J?i,~M)A,7+!9Teٳ_SŦޅe{ w[5EBo}ܨSѕR}a9Q6iA #[i^G)E:ހpƽ%tzyd+%a;dAUh 6yv t#ZCw!{J/2q8?f3Sťᩐ^Yvx%@V 538mDcȅF[GRܤBے\q^G6)Lmێ2%ڪ E 6KVNhij)d>pxPJm&M:Ͱ0<1eŋDF>+$u*)teR~L<'p(UC5h ?ӵبIhaDWeN|UIɕ.n+p>yŤG2v*ۅ7#rq56-X}F<92z)K̒NMtɼL S#Bs|5 ^Q"WaWn߾:!5nzD= è#+>,`b!^Ib̠SĤ{RaK&JK{uSӋ<*͚rк\9Tr蛽ͳ^c&k%Yk isڏ Wkg[ V i^W._rWJ/6a`DlBRu&¥wv J6~JS)c]X!?:6t:Mq#}e!y=6F$5vCڻ@Ntbv&L(vNgۆz ooXM߈|XpnjAyQ'H)]NF/E5)`K@BىoxSZbB`iRSmԀH54tݸIMضKapǺ*©rq`8;J% 0/ZK7q]Ƽʟ*"z_Uǯ&fykyqo^1w)˳CZAz"Ԃh72{'l(2lpj4߆p|hɏ1vA$4dԸTG5_Msd-3nQ]2fa*7jv]0#sYƭBiC呫MXM$:er9O7ԣ $ Yu^!]vF]?*Db--~%/ʐS%yW+dZ(fZڪbaxِ8VTΊ^k^Baݴh%'NjCR=|*Knm?;?=U55sV9;$:Nz=x{ #3wY@(sa69C , \Y8a}?Aj/%=W fm4٢v+X7^FWw-.ZI]:NA _Ep:`xJ>} bP]&ҝK-qdZ;{Ja`3VRʪd&Oz;/^9cwJ}(ȃ{a4YlWJթ/?BΩC1L; $m5'%;ͨ1<\<\)k.svTˣ+ᙘa]Vd?,N(X$dz"314,5YTamS9i")9A=XyK A6l Ɨ]GԆM4>:SqGy cΟqK6)9&Nhovg⵳_D k 8+ ˉrEilbo=dy}9cQH.Ogp c**gK,R 1 )"%])!m6Jdzz+mWxgr+~}k[ "p:"Oa7^_i Y ;CDVoW3KunX35e|x 5pD2H7+&o\|?8ϢfcK|Έ!x!kdQܥy?.SnyTqo=J <݊êoc3$IX0PZ;`ITjFΰ Lmu2PJ"j 4T^b0)o!7DD 0~D endstream endobj 534 0 obj << /Length1 2382 /Length2 18460 /Length3 0 /Length 19848 /Filter /FlateDecode >> stream xڌP۶C`q%44,w'8@pz{-{c9$URe67JہXy*ff6FffVDJJ5K _@'gK{;EF &ooqX8yYx^3p @K3 +rrx߄04vfb;Yt`?Oz22ɋ hJ]L""/f+3xV\΢ddob< v`;=xb?l<M_Y/HmZxWx'ju\偦.* 2a;s4Yhd 2רk]fciTwB7|i8o|hv&.VN"xbCS `bC|fN''I'IqD7IC\&?`C&?`C&?"ZXkQC`-!n?֢ZTXkQC`-Eh!?֢kqFgq&!vi`2[.T3KDewqG`G?5 X?\?\?\_ML2sC;Z?be N<i;˿WO`KOopqqG~?ܚ4܇?blcl[h߉L '?v\.@p7]AVp?nqL@n'p@kq h`olUq_+Lư;f*-ײS#*|mMfӭp>_47B+$/^ǭ۟ Tfv 2 y8zkXCBvP:p*c޻ H7 .*p"=2DGQg>b3F"GsV'ׇyJ5V< ϸD73T^"28^EkCKE$9()k h5*vu],;I`j!̝f,'25:6k A )cMvq.LhJX(>gL9«_WqKlܳ<տ,|ÍT l/mړ`rm!,qs?@XfӤ%Ԥ#v,C꯳R|%BUhi;t vE;CmNoo3#߳ k Vݥ"-+h)40|Y?= K-zc $].KޯM4SQ3_ϡPRP!8WGA/yV${k\V¨0Ɇ>B.))3K{cw}hXcUXZ;vLL+_61qMT>I3 eB]{Mi2,Kb胼~-AWZIrdK/PڛwƜe1IBwl f5^~U@)źӵyKu+ 1/h!H;vˋy>{7멒= !c_ ( S ” W7}j☰xe1;?Q̍LFbCȕ}Ś995)S1 Al3&n~/QCuJd0ƔwwqG#-+mt-nn`lH lr_ljf,P42Z7Ɯ[: *"|M${SI4Tԍh}[@VJÏ,y,v࠙6@n`[ 5Q!JF~LUkf$o{.甊k+w3;ȖDjul%RgmC~.r94Ddv«~?.n{W]/Da%FOXxldD@\W ,SFLⰃX _ GkN.zrCASC|~)>`rWuc{EEuX'mT趆O !78ETeٞ[\1J,"pw=߭}pnr[ɪRe:#{)GJ_t{ԪU毚Qh~:N d҆/Q+7Q \YJdNUh& +Mb3Xyci* :E/ UquFW(>ħr7նB-pF  $vQuD}0Nv}/GNh!\!ilq Ԩ4$RlKU[M:%@-뙔]Lwy)[[aqOxhl|L+7 O<`閣m%`Q+L8xd&V4 b=W|AY $"}Hi0o%CVhLiCQ9\INfF6 K ǓZd>Ctj/la yٽǺhR,)eZemFi]tgиdH-nOo_A]uۣV&O]3]3^HHբ"O|^:Zx$^ W.0 nssT;$U 6<§e}yiȏĐ.K_^ ZS8=R&$o>:f]gG0~ns*x4-=tCPgɐwpãE>'e<Z-&'w 'Woʵ]aCy4v={k\])/pCI$װ:8M*"y&(-,Bw#8Yv_M~bP }RñO6X`(Tk–}n ri,dț dRG@GN&wrIߠmlkjN{)CD@P>[#/q ?0rjpS nrSo%mOu!\]w~ЊD ҬrU\G]dFEu'm.2|LAj@w6"77ɩ({`4}`jT&hp٣JiMӋI#2,{ -V5+zqHɂZpt/ (D!4SqRTqÆZ zӦ&ƆD9l#{^P-B>i+\g@mY?FҕmWaԄK;x0^WgWKȕ~NWlht!$2 ۄJLqyVaC߭x#bB,%MbUN}OL3^2T0 e'AR^zo7NWTl-P\zcnۀ,Oc$xg?&<ȢEZuY;auK9uƮ͸gϠN Գk9d׏rنϖf&Ē/ٱ|lၣ9R_:}xvSxBdKTTy\b7*Y<ڗ]b- `J'Syѷ?퀚N :{bhZYs8YՀ;-j^fӧRQ9)}-T`9չwt'S0/24! Q۴=APXEk-^&!6\.@Q96'ډѾ?^~ಢ'fg{!8Z~I*eQ|2ӧqH(I u׍ᰡfvtfCq瀗ᢻbaоuն.+'JГǔUmXSdw{wX2ǟC]3P̅+M>"aVٽSbl$fJGKS' 佴j/x F^t8}{dgF`jܓM~kRf* İ%~u%uS!<\x]I'Èv~cB}MĜQࢠ-=+UKk&UIx}M`-<yy?Cm V0(mcx(!ƇX‹_9] 7Ll]ZBxR]h;9Hoܣ(ߥz})!0Ifc{bv*=S} x.ty燦Y#72`SK)k]"N]\4H\J@6"ҳԶ!i~Q g;5eu>[bXs .tw𥤫VڪО9G"Ê:&UNSoLWBGXo'H"2bcǤ{fb$ck[2'C[1/ӴrgRW vw |G\NU%DztS&'Q㜍a^a}.jTdYͨzn?‘+G<[{>“Q,u|H@2iuS,n<xXr'^Y1Mҏ4ly uQ3Nj*}eQ*|R A>yŚɞ|.;[2uHK^]Y[ᑉ4"bYӪ+!ġ+xI7ɇT,ԵI~BȮ Q^-S?u/#ͥLɃ,\<_cg%8qFE1WQœ{Ys0ճ^iK@97 V^.{7'$6T+FU<[r'$$S n)"RFμH|KJ5kRq{D"RG.^+g*'\hdf! stN.'#6\̇x虙$!|J n i~>Нj}n(HB?!q {E͏ 6Q?@t5NlXPE: {$hN!?*yO1NƄU#]-^A6޹5頻VN'UMA.*SU['/wXC<Sr8y }CwaP尛PلBF֪eJ4b_ԞBW~PiHԦvT LL_2$z Tf D >8`WsXr^VD.Qf_HRwa != tͥRY{AΟi# sO$$;ڧ쇏n@Xߗ \+PB ) :+EzS#6(-4K8mme7O:ּZ&qN~>K)|>Q wa?c ۥks~i>Gl: JSFzv:DwW>AmEΛC/~NЮ[Ͱ}0o'"'b,$$˧4(ס'@_)z[nTZxG8n3EI~R]+FkQ+#_ܭb)򋺎,FO~XJ,VlsaTPɢNy]ˈ6"^j8j9y54ٹ;)Ɲ{b 9'l>fv_pI޵. '<`ݺ$f{ &v-\;*Pn@~1V)V/LXňLNxA 9X0ytle&Q7{198Ǔ0$:/"d="Hb JBV8]&CՋȡvQ׾w<ΫT`Ja.[ms%u; +`%AaBsCkȾPx:fh߉K!Ey/^`e-$(͡d9/K@l,%=MɿEw-]cݾIY]wee*%^8A~tO<_qXWbaS19쩐~G<\ȋV]+s&Lpw t( ~}ÂNR0󁍿w]D~a}p]^/޼ˠDۢ|Q6aUqXV0k4-AA# $)`ef%&po{.+g2JqWjC=&?ixZ.Heod<'g-uZCR' f"-|'~ɤTg'f˼4߳h|hd:=4Kggˁ}Tz2Wynulqq7LJ7+V'o]?(3';eIJ- Uש %FS|v+|֑zrjƮ2kWY8נPfֆ1: E|^I12rk%GAqH΅;eE #5[c \OF9eM3b\%y}U]"a(itKB-=,dw6|B#@/8(؁g'E6Ll/y(:FE$5=֒X%tyoh>-˾#S+g5 let>5ǭIӄM$6RJ7ҹB%phnMԥۦbBCr5ir7ڣݩnۆlayH\WTwJZi1Bi7請Y)%Oqg hc[ a95c sMfVBOn9HLtT.M85wPjW!L)KTN۬R-W{L Xg\xdN{, 佲И*N{^Ppj3BKfAܛjM+_"ha˾3V`@+z95m /H߰nlxڋGpJC7>E;1Xj'TzgG#Q-ͺUkk{sON{>05a J匠.3+@gdwlā⊡eEe˶߾J^S3 CҜ$|u-ğcE(WC߁XAb?) K *ÐQwf+?O(oɦcE8^ἑq*g󩹔wd g؉_x"i1Wj1{,ty,ډs>U^(wAįt&XC$U#./K$@ uj`@:_E~PuU<|>>gH/94†GgAwc2tS8IM%~waT'|[иCP-#3Xcʪaol FO:st:gXVk:"11.Ay7qy8|X0o{jD*gl|EԚ4եBY+^|ZHtƆ I~rL'?DNHH8*J睄#r/RJt dLp&|EV[ ai:1Odtxʽ|YQ.`uIM0p誈̪`_yއJ@7z: 5TcʥָqOѲyCvTd}26=La/Tq͐BHWr.#lԗXx(/ёe͇’,4{f]AiJ S3x=ےW 㷾ѰdN}AxײTI2_i*x` OtvijF.8b!7B`Ha.LDnb|U͛Ӭ3#Kxi1=:#}dɭj%G9Jف'D#S1g3 /w,KH܎ Q : {"]aFJ]*aWR `"9^E c4zTS>*OlJoZe?x0:BĘzGߩThcki\} Lq̾?zi&ES}S#ZׇH'xT5Ru >#aJ^xteLpfa|*"?􉙽Ҍk[WQgkd}NyA.㲑 *cO<)]=&B9beg<]S 4Rφ~S0 ˜=}AwhkP/Z/ӥD'!MSE26lRLV5TGK vWb1RhUd)> 9l&sFBRtL3Ig)4|k6sT`L'yIFr| Jpҙqt>Bl't˻h])% B4AA>QuQxѶ.[*E4[89 _Yo (kaJSN?#6g`1q"t1,;׬&gfGL> Cj1`kR`dyEQ0[M65mAܱB+UB.1˜zc )K3c[?bB`VꏽRD30$Iq2E d>3'oxA*Q*Au%D-ǩ%EA %U(2ZRN﨓)V ?oN 1d}7b |?g%sҨv]4yP\OVѽN3 zV2(ɹ]~¶2mY /x5c5aDS9whlT-IcjYM;|j0¹EAّbz42g3# 8-J~hJ;sj sAuqL5ƞ_)CAlӮā4NVrXPBwaKVٗt(+xC )ds.!Cg{*sz r|5hcsdvEcoRfqy/i"wcyLɍ3a?l0-'FI ^A(>U8gAGPJ8Tk0Xy FzNBi+7g $*d&Mh3s%IƐ|~0z6 GIo}'wď),t2Y]BRz/^C/4_I}7d78H/e|J$|m76ӊ4->޴y=Iu!Ub;X&ce 3?{1)dLVBB.^@1nZxjJ<l$|(,)x>ih}|x0so 9e:L\9FAhKܭo|Yƣjl0fL\Mq,x/*e!;)L jHb3|7D grp5]\ojͪ~~'RuhJxt~eK=yKM"vg*d9GG;P1FڈfmT9rxb>//MK4Z39RpŇZ, Zw=Zf1FcZ}хqE&+eۥ?oND0|@`A3mJ_P{7u2ڶ<'D')7e3?_KG&o\l S9O8sD*-&X |rؕ.WҐ[\sm>` =ѐ-*SeU('mK @$׶Zerr4i4Q$'ҩkE핢#ԋ-@ rEl^:΀Wt%-Ď_jE+۪|tDG'cۘWx'kzy e '^;QI>}Sv]Dm}sPrIɳvd-ȿKoS6}%658ŨΠv]uGEz`i\M2C+7i!֒uG !?rp.irf)dԞHM@~iNXu+RLֽZ=\ѬcVKkA[uZD_D 0Tiu*m"ެegmTů?PHfB+bG0RHuHVܞ5R8EDWʽ brVE5 Sdrf&>Ub-_wngpt!^O2SaQ6QwqC"YUe)VbP._J,~9NKx't =pM KJ9Ic'J(sw#Vngr,)e#X#7^r|=fx܊8.Z09,jHN9^vm9nJE,JUO^Ⱦ INـ *$$)V rtOIj)j< 0y1}zd^Ejv',gVVe=a1o)˴p#v7mT+&7"w( *FES-I@Y>Zd}GNGX"2sE w|nLKuH7v9/YM|I|q[<v?]n`kǟo]_\.%Z; Dfe*75񒇉oΪ# Uk pת*QKҺK kNDmg5ВA0yINy MP4=*@}zqhI[d@7o Wʪ%DT}Zs.[}DdpIHW-T$7t}2?s=1T 5t9sBoh{P\e^ۆ\GX,^QМ2~12z@!ICeSֽq@Kؗ1X3.hQVjW.z.U[z3@tmPXKr _c}&0_,ȡ9os=b-xS Ս;xJ M@\Ʃ-ٴG{E{*S3A1p!a шyGys^. iTNBAN\s$!ȧZ(%(jXT[ކtYNؽLUiLq>#A J^d _ap0y箙]k{sIAc'Ϸwفè꬟ElLE[¯ݛE ,f0B_M :#/Y%.6 ݅<2g? "Suc(R0 )NOUE*_cv./h2ĝ) A8ڲ X&G~oF8܉;kD -[ZSW8]IZk[wn50)p.6f enQqaGL¬ ]JHV>kٯʮ=jI8E9sE(X fxBx]_*o?>%L[]AOo) 6dlhh~^3PgoL 4s,s.o\wdn xazG2߃O8\e2& e&=qn|OZb{ohQ*ZQi(̜B~utn`}tM[(T jUpR ⥕Ądh| )/ߞv0V#>_)Zc$TՅ6L8 A=3$N01KtQ׊80 %'%$-IK P`7kr gc}~֍p).͆ck92 鄝D5jxoC Sj( O'Ԛׂ@r)/l$t*^kD|'.hw4q6օr7`'Kj ۤC-HY ఝՅ* FEv;gv&œ}e-sl,- >Hr͗ǽ۱aW,5OsWBBF:\& tr8hv$N6KIM`}\>Z,CE"|lB#yhkl7I |Rc/FlQ15dOT֭; w0.M=Թr^yel.S*FyZ?+@CgI5̎O1]G sO)P [77N>vD1r*ßgkiDM|ڠ;'?&Mv:SP_(7VSQJ/cZ]~u51  IFSbp}ٜ=D^D58 ^D #^f)x'0.gmGCpE XJ{cf5G)/i2#MnjRo4dd 4:!ǘ)S5LR_ҽضuivV|-Mn..8"D!%7d=K:1U4.2BR,y[MdJ Nr+-Nzj(GuЁm=M3u1ݭ>5?@D/t 0Jw n̢zk4Y({Z,²>o{u0 h ҏrA@'}gJˆnJ4\\"]}l8 f7Ciex;IޤzܻM>rRQdͷg=o^[&8ʠQ|;UT<"WOPStKksAǢh'jM_Ґ=u%`j 2=0L&Ocd@YFVvM;%ڥǭDhV/BKuDqv|6kәq;`Y4w:yRgJ!Jl`OfͮLœ\mK11rDsBOIB3vSy;t[9xZ(>hѕ 4Ț|>nH{9Ěnw] -fsՖL۠>N}ϡ'yqciI!qpa^5!2ts뗜Y|ɬw|3R !m77JPcuԵ cP@3;}xkⰶO:#@bjcv6l=53tBb>՛;B(R(k$gm &FI#ɕc{-> stream xڍVTS[F@JEr(= UPzJ !@$B DTQK&Et)JU鼨{kZ9{73{fF'AU}@AfH'o 1hb01h@JPi9C r`5t0h_Ek p@"04ù!P0OG"pH!p `(1 UAPGp~ Pߍ37)" G}hg j^g?"_W@ŠNWDH`AyЁH+DzbC;ry`0?Dp]8 P6`Ez||:JCdu*Bq>_!8.H˯}h/B[/  ,@xWr@/oL?\- B. !?w (pFq`F&E7!Aȯ+;1hkke*C**/*.KA(T\!,B ߱h X-`/02&-D '@4WYoT?P N/}} AtD>OT#h@KDh FHU Fa|>((*CoA?TG1ο%.% XX 0bN C 2tFf0Ccp^~SJ`| $/Ko&41_Jeѥ~a% AQrlx.qqGD(FvsθuA!% 6sAO}ZlXHv]|ėH|j% [LB^ftdړ[^R+ǐݜ[12ƚ<-OdT_= {]^~j 6ޤ$[ZH0 1N0P@6edi[H7LI9YDǾ.]}>(hjhabwCզOĔWD?퉆_IF+ZF' f'fje")ёqYmJx'*MO1W0?;dpp4':a!j#UeX;^x5dhԒ3 |gsyDV-w_¬qJa/uc~UAS2? Zerw*LI Fl]L}

    /(J&:}5ʗCc3kK06/ie@(J솅' wEBϊ'<4;?}ӬD:41֞ёp]6ꪑ{ʤ&2B$ktTV05c.ֲGca2۸Yv_N b(ܤ՜ (薒}&U !?yL29wR28e!RfS.IEWZDٷ1/S>qK^TKg>D&}PƎ$Ws%2.z*.u*_"r{KXګ^}<-pj]H톆Y1YM0W$e< |C}Bn]YLBZ}'?T7ە!y. if=Nt6'C@x(}!yӡ~B_)juJ/)~^ZC(㳲+r4>F3LPl DL2y[uX*,l,]>F4%,fNt_| sH^kV\,m7͐>ܴy*}320XO]Q<r1 =N'6&Q"T_kLysV5Gw1[GJ;! ncqW7+Mʡ"e^J˒Rzn#~v()!~ZmKuk[RK7帅5Uv^jЃoEdҕqJ{9)a27LZdFs߃sP!<2+5ٺoZXeo᚞kk[bunĽ{WS8tD>ިwKmW[/=jhcY"/%r@yvn jKl9kEl]eSW=[@vRiTr]"uXJ_n'V`zx~%%fPz|ʁf6hJPCHhOo1`y?n&}h^2bz'zJJ<2yV7 ]l#i#AlM8>BlўOpbDb;sϨhn-B{b樂tSsɾ>PƬƞhpЍMMԋ /ݭ"ڝ9{lJ&8n78)&P{;j%%LM?zߍsHشptNgA+:^|B oV1MC76#;-eYgN=i@kBǜD5]޸5,#XTIAoI ܝ':]Űpgc}e:/Xݺ2zg?xh^vbYW]"s[eh2uӝHk#P4ʚ gYC:_dq$0Ĩgv+}B"`1)`uz|UF>=3RH̽l^BiԤ/W.KC/:>#;i}~Ӿo.p5Nq!0.9 ]fA?gδ YL>;"bX(-Ui|SM|)@Tm9V,=7-Z'Zb>q__+ϊF5\L.r%E}7c.W$n-Y=;F4x JtΣtVQ{sHT7} += qP#J)ȒOGd^Nc-gNVql-U+'`ޣK:Gw, bf[-#GolEQdnK<|sOJ8Gm\)UYN֚OI[әx;nX+|SXct_f,mS9FM♋W̞@ [Ո̴ظ4NJ]=i{YZ|aU:X]_IA7xJu,2sJ U{iۓT}Pu9k݃"Žyo. o7+z+Z &-FLTbꘪ \4}ϣ43t%9r>@ps lF+[tS,3 %͔~3KSY$f𢨥Y|i|gۘ`OgɼhWt urfF_s wLKYD͍f МĨ_~|:k(5 Z|Ʒ)yp Ow^!` |mv ꊍmU55Uk;gWHwcJi'εimݢпѨrސZLz1^/[§>͟t__c8ZzGM{q̱^)*N E)!"΄ VJydh7ޖSP{°aYc0# Gxh 6?^ >M^t3K]@9O+r=B$|7E(c3Jy*hQ;Y}藥eA7^è~MD-p3$9 x<2֐LN\݌~yQQ .>D6V)d k7J!rkXJ}ClE9^7~IS%cL)e-ܟbLPRvY|Թ2XQK5 k.c2bWlFnkVZA=$'[ufen1U1;6އR iZ9~JKmͷ5[u"\<֗ה~wqQIJ/x&6In288ʋNQ |U%|8R "Fc_"C[E[o'E.FLC[O87Ih4x.爠:RΙˬ,͎1iaD~f]JM$o# L|V*뮺 s9Ɨ^RT%sBL3*A4_8 f}|Ҿ"J7L^< yQ},t^®z]Ѽmi(ҕ`5gff6ߎ(ǝ8A9Y*0Pքf|QXFs[TN[*J|e5||Ymˉ\@I2c7/TbaWVgR +&J|VXo_(U b|sM}E N *÷dϑnP4VcQOYkS =nv׬]ULHuޜL snT]cλK1$kh`]-YTo]>"Jf q7.mkO='VG].:?G?]hx{X)V5pN)h%ʫYM" ,߇z:ZZAf% cWeq3$~f)0)q"(/IUqkwMC^_N"QB!e^$]vrâ\ r\iĸy:#YKb} Rlq=,k.$nϔ n qb^v M x8VET/F@:{S$Em1SI[,(*LJULrSnzi=Nl- Gb76:+w)w 'gm-Z?* g>֣%s{JKřL[,ᬃ@Cwb$>D_]|pc˹J`izmnQDVvr,`I\ay'S-a}lV{օX=A 'z '%o=*6i=wNJ9Z7;.8=A箿Ty]:#~=XCO4H^EpTZ(X{ͻA%C'J_bcE}#XH$f7q_),&3"^f݀'sqe"͕;6ݡ2du=7}ڨ@S! Lw}R%9/=?꽈͗1^AJTs;?ERTTu 8( endstream endobj 538 0 obj << /Length1 2672 /Length2 20878 /Length3 0 /Length 22399 /Filter /FlateDecode >> stream xڌP\[-ӸK,xpk5`A[pvU[]սǴ5\c4,掦 G7 +@RI΁DCafۊDrqvt_dIÔ v '#`gpO@ Ppt"H::yX[ZWGW:@bmfP2qكW43;Yܼ'+3 rx(؃jaeY@ws 6@]^r;XfF)߅J613sw2qvXXہ*2n^n߁&v|k;Sp_M2o&ӝ+~Y\ ]<o$7>  `cupt,]~'7Mo`xlMIb6?&q .sQ\ 0?E\ 0?Es\ 0?Esysy& 8 ,,J&Nng`^&nL p{.&f [_Q>~8 oxU܅5 djL0?/2@0MM\#?\_7@nϚ?.Z `/ XVNV EmnܔͿ x8 oS>{wC_np3NbN෸  3kQ9"x5gwGE;{5> xT Nr[ǀ<5DnpW:]KgY[/1utW x`fR 8_\_M4!͏W"h=;iB7@)8byvzU, ݭyT2mi % )#ֹV+&$8_u2 vBjB]Rk Z_ sEk.)3x-#v>;jw.4cl81krI(2ϗּ/xL]U_UxЌ/-'&weԗay0{(Dsv:,5@z^> (%O˔tCʦ:U1o$NNt)o. 0O )OL{ȷZ[] V %1ѽ>Ыmy'320ɭnCt2ePF (5 љ^."av%43_G5>.?oJ&2|rͩ^a'{AJq]D#|:׃Yr'B4fA@/&hIuL؋BqC8'lk,cR[ }v'u 2Săɽz6wLbuF!()[ҫCFo"D`EŔN$d[uQPPU/7w,I&X靺ett]b4$p>80a]5&*Dbž1ئl]Ng!,ZzOi(Fqf ;_IW]EQZ?4 UٴO-H86M=Ƨ8_YR}j>S2Wў ϤkQRܴJ:jz.zZ]Ʌ:ѓS>]dN;{Љm,EP WWV5#G97._iNdP !>$OgK[|a[kViތ~ 5&v5-!RD~ꓷQj" = ȿPAz BT:(Ecq2lYC@N? G|8n n[iR|rc)ybckLdAh*$8@TYqLdo<5^;fhEm5=>1^j9.zPjrS%~+pa7:ْV+)7̠!%u\Aౌ0c!ҀNc|u]ą'_g] :hus;;zg7RW=i\nK^u@oQ:x>ahv52cCws *Os|}.nWXfEnw|HW H $:&iBpJ&Af< nvSՎh YX-t81` :xkz7;Bs*dƢhJ1v7])J"v#+y3xpFn } WY_?ٺKxWQ1TMLvR~^"kW0ÜpOr q+OO\8d~V@.+ûDqCLu;HڤL80]21'`ڹp6ȿjȭ'ȑ'Aܤyd&LzC O*VwhQw$4mhvm[?T٢F53N[ک.QKvV~kH q d3SHh3[ٖۄFH$mo`7Z~k/z_v1G+n'K>aHK#9P[uN.u$~AZ4 Z{I} һڊTYByTWVwq!nbOi֦_7.71WAmI;Iz* 'O!hM\nω՚RcvRj7la%(Ҳ֑,K$15;%=^x$$eV8wD- vGav ))TsΩ-w \Sia 6nH5o(6}h5bP2i~L 0|Q>Shy;#4G>,1 P<Q):Л;OXٝ试˔p1b\ { ̫{՗Qse`b&$NԟT'Kt '>4фG]B+j&7>ظ_ 1Q,"Ȏ6@'A}y4ݳ ެ_݁+ʼnl=-)5 xkYiIO>;lN#Zܡ$5M#&~Ab5eH^C\]u^I7yߌ{ m6\ճ.IԄey&ۖ׫) #,Z"j20a`W> TeUma#lQ3ㇸ wLhm$W+z OߴTw.Nn2HF%Ҋ2pL]"lR[o݂DɅ7y`,{CJC 'lޤ3=62?BJ'•>T9& %2NH2Vjnc-vD}^I AԳ=5zkA%1;p{S;Ib Pn]bRü~x!]+ _LWKe<*[OMnnNTB>+i\r;63l6EAՈ$loY n>eJ̮W1>@,lYj%v 54:L ` Sn`Sz.@]zfvSfO~ Ag!G-4,39E7\'+> {7Ѓ>VmmWSϙ ޷YkgqM8E5ڿvBP%yQXcn|6JxO mm~jF|KbWm/_=0o e%;e}w8d+Tf%VTwo |6^TK3ջե bx0RA' npm^Dz3AdXF1诳$*8&WF(Ec45; l =_ WbFZl'-4 eqTYL|c:nn E ;7cbC{'sDo>Av\P9;nd 0֚Zj9!(T!ڒIL#~E|4Ӱlt'qGTx*]ա.pT cli6+h*Q{ѕTz1Sv{/bdNY=M#H__iFjCcs^G+:1]JNq fQ 7aβ줭A6G)ʼn{~즭/GQsx.-=. 245UimJԝ^QÅ{@H=ِV/Zd@/OEt(O>2)Q]c!Vn_{*=2Tډڎs:wL碔ȶ܌]^yW_yˋESX)_}ɛ eZIc3 .`Q UzF+< l̤R+*) %Zn-a)_=>B%$SW!J@'T!~_BIw zڒm{N Xaėj/E3G6t&E_n8Ql99K,t^qau|Q5i^ΚmUͣy|lcUsӒ qk;?P@~Ve(xnjqǨƯNO 1TC7bp{y&jK4 O#[۽.Qම: x~")T),O=+NjWmܗ5(':|d՟;cΤoi)﹟`˜>պ1vpH[MXz!>ưQs0rWt~+r)cqo5b A)Rd>ꦤ>HJB<@/*L+䱺 f)7g] >bS|ucG!ndE1l:*Ş 6mkoQb`w'N6o@^ !2|H؂ٌz>UVD9޻aV.ӹ3Sru&5z % JrOTty^ߔO&O8lD .D`}xl~Q^/6\ߋpYgqGn%YK %躡)I4c8PgH,Jx"dqFl)4,CjfW(0h/AJcn4lV)l;LVq*oߝiI>xFiw`zZ,4 1 0rdg *omkEWKk3Z.R{Lm8{BR,'fWoʴarOA%BK\[/kzߚ/`~%'jjhh4,1BTZltpڅlvO 3 ~Q"Ͳ0jQmzQf]kbp@b 5仇[FOaE#LvҎr)~n[]½ډFr[Wc*j Rlz?վ/rctzz\kg$ #cs49p )xY.[umWD~NyQje[a Թظ92no35Ee %ffCX"+}`9붚v/3Qt {U*D3C i$bZ-Ĥv&9gmd s]A!l%ާ5~'|c?>؛K9( JW;ᣵW%!?*Jr|$ٳ̆gXM,g_5hH c9;YQzNG9ihkC\Te=pG>eC:4Gek4^>yyY[ _v`y6 ~L@l/yښdu8(0S}ERxq X,"obNA:<0yɔ3[s*<2*rř@pJм" g`ښqS_MT&HY pc%;CD?/olbБׁNg=}j|ٛkF;~m酪B:6]6612]ß>POώOJ xFClN5 ftS }Wrimojv<)_C[wx}*)"IDY37mQ?jjޜ+k!H. \er-iũ㖀l|1cBwk#RQs?lt_- 0x%T>;l+|{<b2%kԆh2ZO״۬V<G63p F;|USR[lũJyHw:Ka-%йWN| +R 7&_ :fLa?gpE"R. w@'6)~9&r2!"gZF[* a@̰" #_1O;&)3 Yn$#~Mb4G18_ٝ l]j5H 7rV#2!dXS͊MknjFt-ǁk&GXYS/f =p# F4s Av+@<1٩D [ Q>u} (R'Й#-3{QeJ}s ޲*%S-R39"~113/bu~ ԴՉ2c<ހ,nS f/~qul m7׉O+]|oe+!; +-A0kCEyc΢rF|Umv OL@zD`^8?'i)`pMsK S!/8 R#Zs>i܌f3O蘝I戝@?Q͚ė7ԍ"|\`$Mɠa\ptfl^9tJt!i濢.2s6>KHjV )|I(wuZߢ)q3=,Yg>6KAfԭ!W2;Ѭ}CndRQm0mq QXZGv% b-YHdq B%޿zF'{ٖQ| bGЈ2/ {K9ID.ZFk6Or%56T]=ѐm^4M nͶR̅n+" K-+<ۿzP:f])V4`(4\}~V Wl L1=##;8Зp>2:4wmLɦN,Qs+mYFn,>У1<ӹSa #Ha}o4Z !';j\_d3$ ٖO(U ?%y%W={^ .AhVݝ{ubq%9Ub©p5H^#_>ISZ(j3(iW .pRJ6]1)#T8uMˊE4)kWT%99w}˿E0(1"e13Q:ulfge<(`zĈflw;Ѷi kQI4N#O)f+ֶxd/J-*y8|LdehNrи$ "Sӡ*ބ1D816atm`lI ʐ ~[(_պo!&}qSm>F\FbkJLya|Jc"9Vv-#ALOg)r̓7Kw'r]$H (g.?عȧ^` 1-PpI 9nM?/BҢ!TB]%ccU [k:躇muLs74 KR+>hV6{몦fQr!md{o_b{Y[;u?=[kbZqzȬ8o+??ߋ:b]D 5sb}٥֦ GɾrI#Qc{[(}AY'!&S[,͏RFw1st:reƋŜ-6CF/,Рgw&/啲!8Op"CXF+%努h 5?C4UlS11·%vtR.:R,_p%s7N8"-qg"~ #G Z.%+IWszΗKkRpɅmISz\,\eMkQKlJ##\o/}i7\{+!r \е~ ^o( /B3EMry2#[}Q9 s[uɷiPagn?/Qp$ŻRDF|_ >E3}‚5Q;sJ>q1ݭ4=;v9fD2lH۾2dDSALE {L#JjdhkOeY#+JȩQ/0(SW!3By~HB GV²>Fb 0xB3|;O~?GlKP'V-=' U ANu^SQ{|҆R,W V $ƟE}ms-;UVB %bim@t`%goԃmHg֏#:SH,=wEMtM>8%2%OT{2T6[)%S&z$D0nN[lD3#_n(g"78]FvLʣx#Mg措>=%ۓ2P Pާru@#ɳ>TaDa@F;]fJVnI}aW=>ɧUG6Is:bC_zM"ltPo׷Y/;i&z|i$ձ$NM4x̆oȩ˥KOﯵ㐉b"K~^x+:4;! ^i)fM90~!1`&\l?\m^rS*Zlp1akv@ZqcBQp~&`0t$%g C 9Ye% |$:m%RKf{a5eλI+/U\OIBv($nt@Yc#}H7C$(wsQ|SN'g=H_Μx,nB8${F Uz3ܓbRaLSY 3M7!'1ĥ#nya} NG G ]:vX BgFǃ*h5S"X҂$NuFBi'c ckWTbxF.wU](JuDexX^~qdOGzFbV(7WofYhĬr]`)Tҟ>5/_^ "֕O}A te) y_+m𰽲"XL sZ}+D@P MXEl7iۙ(X2e6OO7L ם֗w Q<>j~6NRP nVMt̞o Q,ۜz~"q2.czOݎ_vbՋ$0>mCQL#ݲ#3-4jʋ]X w=5) E%GJBRTՈ]rz0l,̮zD.xtWcPaJE4X&5'p"~եҧ SHEz*hk+;iPFPBZ8a"pmvs+M1ɘL>=9D\jt|&\^~V%XWiCRol$f+Y=4plJ5vPge/BS sr6(+K f:^/zȅtDųgw.(gㄖ災s+tӬF(2?3Utk"Tk&B7U$7ehBTţ"9lVCpoOkX4/l9crqhN}п*([MzH6hHdk̂!@ b y7߯(.#F~/QZ){@^ƣT!)/kH" k"}qsA)fuC)u%]P}Ӂ/:bXcsE>Qz5s2g]5^<֛C͈q%ԎYMwM/|9؊3#L R&i@܅e=+[ZxXRڻȏwPzPҽMb.v%Ps{gxE*Nq*.n"{"tU$ s3v2ˏԏo2Qb4[>bmHPYi~akU9}Fkr%)73KEa}aeun5?p"{EeB9j֚ ?Q2練k :;p-ofpPPMBU~wsvm?kp) m{50yNLEz-ܭqVL0 >x zBac27Z QWs3 6iR%M=ĆF|(IzAB/M A'cۻh* kn.vvp6(i3eEEq5{ngZN6ާNMPuDCl!llOM@2%4 `,e_+jQlh\g*9 ߏޕg$ pE"m$3J@(ʿGٵHzlmݴDss''t_ȭiDAn秹1N { s]wfvBϳhb?д*No/ȓ񳄜xHT+D^v{ Şq\gr6K-խ6dYbd%֑ګߧDRp8S?)z 4m08wrDHeSjZyd$Ć~_k+s SYjeg;Yv_[R3m" JZxwQ 1%c^u #Ah/O8%igZn5\zeqnnAf,1ly@w~;K ٺ_l*mp&ܮ:5Y+k#+e ~6LCy駁+sJ'|79[kPɎha:Rȸ" 3 {/>Hfm&ws~T@P#O6z89kMsl JmpJQL1p5Ҟ:ĊgH@il"Tj/+A;J*ӭ*&'!w ;[D ѼSߜv{V<7Ώ+G{G_u(SѴQM&"1!X19H=TbB傰e*(Kiۼ7P׹e,ʹtAk7k'iY R-m  Gyu^{YUz_i=Yl6T;*=Z v%#X$՗v詰>kؽ1xk0Gj.;ֲd=%u|]$=f}aa>c7Z @eZ?l i|sFpHsD[ޒpX]epmfscpY4vXؿEU?)9;O詚8c~9* ?1=Y㠎o܁| .c;PBd~SO{/9Q' wI~@vb 5ϘKeK[HX0{!4􏽑Μo&ӦWbrdwY)IIrLP]_{6Gs7GuEjH:awmNPt:P[yg{^@$.<ﮦusn!$E7vf]gJ%wU~%+iIB5 痵%d_֑OemrN oӊH}̬~ca~@K1v"}5famR4M4+4weC(@ Q>D=֘LlVŠi ܊oS8YShN QLʓĺw;z dC+!YWҊW|"峝Hڞ^m\1O̞FЃN#V>RQQAm}v\͊iK'  IѷE\GЉR~ɱ ҟ8{F~"e6]Tm4Gyo3 4)U$[ۺFfp0n.n83Vfkn 6շ(%`[F5>݁,vֳʆ32:XŠ% FopP3iۋ^;%R#jhcϢ1G9\'טcq~\TӇ"Ùi`EiZ}ؙp_R Q!_]P9(!-(ҍp(8v}+myHꂓq9eT@~ᦘ(P4h]$z/Gh<9V4tc&+/`2adjWh-/=E U2PP YlC`U9}v ?`K P.Bp:PG)i6}(d=| bhe.ݩ~eGem3%vqw%3I&H]d*Y=P8Q晧`5;R资$du+Le&S J6ϭk ߢH~%Ysж֒_, R(|EP\۫BI( $.{Zb?FS: >v{*c֒#Ǭdku )ĩ@6e.Gf Z\l1l=1ךi1͒0~)0JFB?ɖ!ΊVs]`7{bFLu!.A25id {U8<_ j<a̼=طGgͬ,-'~DIz軄La,b0 AV",Lz q!N(.C#,u(KB {F˷yNbbBuPȼI>Pwx;3z!E$/p0jǣҤδ'%Q@"ˑ1;w ~|Q&ɼx>нgK LU˚hin~`מTv~M(Ur*/ɦAo|;9DZmWOpK2Re*c\N4W[W_c($Yn_ZoߴZ>*6A,3>wMmB¡t-%`j˸؍$Ů/ nfBꇘVN&;̄#ѩE}i*cđBp7wCqgJGhb쯡jmcq]o{$&4U]TNB0S4܅k:yKiAaȉ JBTEx7(_i_9|#׵2Ҡ.MżbM!֤ tpD9gJ){ w7ϑ%q 1s,:~CM#QNGl JGvs*IyGT2 }1ث%fbKtҫ?yۊ4oCj5`|-7ϫ2PticI\ >Ģ1p1ncJybS_:=@P~1EQ,"DXXҍ]%b]hgHqpŚfF&kru`dl;tOoRE[x('`*80YwyBckf~ޑ4*1x2&8PT}mak|;h1ģw;,B`zYd(oŅ\񅃋˕x#<"j,폡+cyUdC̏!/ƸZS4k'">84 /e7_ݰun:[f70wp뢕Nnp}^VG>Z/Qo"'uN?ĂbUwd4+V-m3&!<+|Q#8cD/4me핐9: =KSHLf) J W>(ti=]GVg6̷s b]'GQ}U99|X Y\> q> stream xڍxTT6"̀t "90 % H()] JZ߷f=繮=6;1#Re]cK! M(o;> EGm*(4PhyB"!1)!q)  J DxHT콠]F+#}=N(to\ n8p tQ`7tE= `A(qFܥݐ'9n>7 0#^`Gzn? Lȿ @`PDxtu@ gsBBJ'W"(w=psBN|P|{/= @{Cahj{C<( 54mV;*#pW*P0ツ@᎐_c8z ¡<*0hmN`@()&&*?}@΂ ;3#@(G{(Op:8BA( wv k> ;k4p῏XPLr*)!|’~I1 @HHH .. g{>#VA$jO\ g.=`׿( /BgWݑ' ۻAahz*Eo9/nD٣ՠwB3__v(R v4@//p AGC ~ 5Ϻp؄EFDBhU:}~ (G!nP'Ahߚ} 9H:exe"71յ$hQ3glGJv54#O *"y/} Y(@ao(};}frD9M? =J6d$¶L5z @r(='aɠG+wa>da[C [>$I 44|MPꈣa5܁"'Eڽb5~Z,#)ɹZ-H %s$VH,;3EEyT++Ŧb4t-ԝA_X`.5>1_Iӱhb鱸yZe ?n}1u`;dIMn=Gjƣ*מGtr''cR~ 0ɚh&B\hB:owR*B1xR3Vt`[*$w {ݶIr8Ƴ.zlWǩmKV9[)PadK^a${׭ ņ 磌2_Ovroh4]c; K Eە? PƘZ tyBϾY]H qn;r^HI@F̹!)Q!MmBU~)Tx. i߄k/QY=i%mRw?>e@^ 5* Ue [_EDw-kG*m8іWN#[I,gG, Tun7lִU 4}i)v9;ðһN%|qQ)=5 ,Kf+?ۇ) OS{ҘreGlUu=d֜M=etpH9};PhF/j$ӕ*RԼ4l^&/us]|Ob 765lkW!",k; $NX}_`ja/TL%Y1Lz><7lZ+ְ'3.E4O-l P'NU(K9I1 iFs7Vg>OE W'(J1{N~z 񏚴!Uq~&Y䟕>;xz`1) T>]Y|1B$ZFv}W}YU s0'+ԟ]1e=²Zbٿj_؞yxj3ĚTقl#nÝb/؞spqa*ӘP:q;8_Q$KLIt eWX?1uQAn-3=50P0!Jtd?Zh8_IWq̎Vsh_+ Tm9>m_m}-?Gyp*f:%ԏ-1mL2`_yD!vFAv+/iUF4 Յ@(wR(ܺC<+{hC7v}ƘXH66︇:T-l8rV~ok&x+!H`Mm$5]_xib:6GG]|̯yAcJ rn+4pn9r''+PCĎ-(ɹ,".\̛,_-l+6d6,p-N/1o_ač3o+~j /#HM\b&;}T\ԗJxr+Ew`o^朑juΎ\4Pn;bU $('<-ȷn*TNzUǵf6e5V& 7[(=Yy$BPMӛ^yD'yoZAx@[-րͱtZOעޮ.*nD5n 0LB|E1m5GeNôɧG ۳oI~$Zy%H=?3vd܀ĬBK9Ka>K^_z5s`*:GDB. ሳNNIU0%Q\xH轧Q_ ʕrl?9LFCmG z.=s/*^1c=w)j#b0_*aQRP񜯳)GMOHvFE(ܵXLVo03m7A3望ɡVQ~=#tHٺ!NccIբv$Y'<۵Ģ"%jN3 󔲶7s˫:8!f^}2u 7)8iPݝS9 <ˬ?HT=;Bg}x@`aѴr]jcYiPY7[<#8[8}1F\ OA znO?<`y20"2:m}'Εz6e'}nIyg@&J/&UQ:Z8ٸOˌEx]h<NV,G9M`25Hx暣e(@fuEAJ4QMpLc_N}LN;mfaMRƣۙrDc]"rZ~*z:,X%xt \"c/1Oc46zU. :(P>Y]"`4[rCY߫>AZ jI^)cg(~; 3}j9+aRV3G]jSض?fIS{pi'sIvㄸlXAG'r䡺ydlr ~s=ēv]ڨc_')c0dr W} [JUճOTWPSKN ٮ^R} ϕuTqXTe2bj0eK+;>_˼=I['DrSFJG @29/֕fscpȵe&-⮟j)iێNhvByFd2} /^ME铧j_M/ X_iGFVuL[vU\xPGzlFi'W\7d\Iq0_f)>+NiH5xf ڴJcп;lr*Z 3:y\ߩm+jB{p յ 5ZbkF+PLClݑHew*4q.q(R`u=aWH)SK\$/dkm,H{?WwfED~>1B94sRImuJ)wb, 4 XLw[TX֛/Rd,bܽIm|󕭍qPzlC؉W/q6__zxvP0ĠVh!Bc{Ik;_:gnfizo9r|Uq|V+5V1S:{mÙp -LՔ^ABzmeZL@`H(qHEo5iIKukFSdh߭O}&qʢN8}$lQO8cP\KXXi˅U 2Qrc=no9jNLF$đ΋%f$efo;NKjA9żO6,^*th3Ҿp9)x[CJͲFzv }a!V35],|ˋIpý[V9~)l`eYѧFv\ I}序7宒k|>l3]JWCzq, ^cIl?P(߱{ss!Fg̠ NJexގ6/hg[箄I{}n1Gy+9M&w߼a8Igj>Tfd7ݣUEGHz&w8e|񹏝1{[hreu%+p'~*8EoYk-h×tgu0s@--Jcvrmz[T|`3iRY]j97hc4TܐPTVnCga7_}oҪ,9W/,L'Yd5_=SkŦ _fZ#x^7$~5˚0M}dv[0U;zQyAշ̧Cc[޳?|-ӎwg\(\#mbf+i'0C/t7G1mvRa4ud'y3"\V{\,Tnƾ CFo}f0¸"k d#*j{oVUK5M,KR|3);"мYO0> stream xڍvT6)%2@FJw`&lmAENETix{=;g~s_TTriXQ0$P7@@H u5SZ8 )hAx> r%`iy<Djo8蠐0 *bepdD~04 C0`rðPtb=|||BA@?)xfp)Ax; #P/0zz"wN7W"8w0 qp jX cPx7q~wh( qB= kD_i𷬎՟ s_;Nfݐ(߃3 u5C iM) :Jov{<!`pgh/XvDp',GR'; ss/ ؀@>~t{b憖f&OE I*.3 B(ܟf_6e“mAR 'ConH[pCpw_^X ]0zYW鄂4FCp <ĥ`0D(,>1BSZ@ ǸIN^h4^gyoQ`0'iB轺жZe6DH),߻W=, Ohա Oǧ؄vrQY]C}ej-q+8F c t^MfH9_"v,>T?0dHaQMBj4~hq2I̗D|kzJv΋DŽ<L* VǚK]B>uZLcK@<xnxrٞZ֡yؔ i-k)i6u' kwi#ڟHh/)huVg0lhlWR [ ]/~-%iū ,ۂ"RϾ|DXL7D!j"$Y&Rֹ㾱F l:K2)&*R\t4 z1zRïS!g tk+>+ 0};EH09%&/(ӡ5;&Hh^f.7ZdNj˦%p*]awUy\e';, 5SG6 l]. 49gڡ2aV["[B= t`S%vN 骧+v[2{=ߖCms&[j~ơyO:#G~)q3p/R!%3iVcET-7?|6;ZP.Рvf#K*'#VEKH+|8'qBKEpQp+d*WQx&";>a֗ot3yz5t[s\-bVhxBri=tfEtmDxoGDJ'+1YuҊLE1Bַ 8ˇ(*p[ mjE|kYVxa6%:OK]'#DCb GQ36OU#C鯡m썾7חWe?\f_߯-_ʧ[hQ ?5QO ,JBY{1C~lNB@tA3SW[Ꝉ ezn%LXQ(!}dBTN͠ށTMƏzKjnjh &5M%}Yjl۴liW-kcfx7?̧K {Px"G'E =Vӥn,~`LvWy@kT}Zp7Wo<@(AwVe@p̤*8Xyc~RH{βdLNpi`IA7YʴiW o8|iGZ |QD>5&áܽ 嫇P\}my3} sglbhpNH-V3]*Ejkۥ, 0k3Q}L$5pu|x1 :fYA@1>$&=RhgO7FM. *\]қl:8뵸fq&WQ)Nശn?={̲}铿5 =4(&uîє[!M%#O9u+ 䎵|\)_ dJ`/K~+ܝ8<%^oBV3o }(뼾H|8ГeUPtDHk" `.QbE,7_G ,GjF2ϣPQn;Ie@ϊ@-"{DsՎE@̏ku48v=x{(^s w|pަjv`L3~WTsztzNe s.L֋ :"8ԤV]Wy7[N8kJϼfi&4@.(#'d0ȝa[TX-IjA5Uk?`%;ߑC n }x_Ͳ&$ZR24q{2=^Ƭ[jzZpJOn^Uqx{ JHҍ9HYk/ zӭ*u)ޢ&%<3jMjA+DW9ڌ)x׵. R[Oqf|9A7Nq1UDEP'-L['=5Ԩ~\ۈ OHB6O NMkS+΁Aҳ@愱c4AJzs* R&GXo]-,֚/p'r%XAs& ;H,펥-\R^fqԘr?&l*Ȃ!dLgε8/C{B_2ҝW MDJ⼯mk/ujyoTOt{k/.tE aؒR.JNz?[DbS\YH/ ]7aKI(?˞8ⷛI.}dc!z|s[~w'2mv:7v/ͽt!ӾTx1VHt{*ԓi:Yjwb[L}溦IS+K0}Gf3+!?@P󯛸lںo!{;Oi:uۖ) ?Oz .}(-ܓ?ERqVݟqU'Jh_qѝ63뭜zjH۫T` d^=5[ű(sW{}Z},Io`b+xд&evhVFj% #MnhС#\f@SA"/ ڣj  s?LGU(/?„K1 Q mt;ʇKZ[W}i)I )-G &!{eviZYZ\~Lyk;'UQD]{Jo 8:!%voy=Ѯݠ37y(?g37,S[9!I |l1'@m4j4 aţoH Y߈YU20OdNT(Ny62>s^'oe)Bȴkg=<%VȐ7 [ԂMx)Vv2O`$B/HB+yo] -_ w+$Y]T|J<~*lrx\y t/m`ϡ\1ʿkQ#P(귥?bxL& TX~-:!֘:&4y&AvMGtJ82+@itUXzSm;)#L 'USPg&I36Pm >|=;ӝ}0:2eD[7,L1X> WR7Ēg#Ji_hM?i OBijs_%P}vij~bk=vOF>p3{GF61u$idqG;?vպ8)0; 3<&4D1z_dyK$JkvX4ī?X+2zVcH'0{s/-^ #:L1);e_ .wSC7UZCo^c F6`u"?{[̖p4G$J?tt$,OnZ6LD@-ۣpk1͸Pщ%m23?3ζE|Rpyvǫ$c-zKEc#>ĵ9u'/$ ztQ@W%6)"Зr5 : j.&b[ cV(>z<9sJN,[MTKقJsL}@:Ƙ>0L`)Xfj+1۾lt`i曔/䛘@&4ΰhrDaiL+W i=2b[m/:z;|9K{9U,Tb-O%qwAN[:[]V^认#(yj}fFS:.' LL{zU.?.;v8wS!5{1.Ȗ`f+mKˬn<|?V8ʲF4MIU&G˦gtmQ=%t%Ο4ù,_ܐrc[aUM|Ttm}ka!,I/7Ƴm3rsyi-Jԟ9,2];o8we%ϩolĹQܭ 1 6©!zo M{t%l`{cMIIˏ*&Ql4uH}mk3_Ȃަ^h30hgR;uiQs 7nC]NR> TqڧttHjYx>(Ճ Lm >|h QPZ),dp9q-%$ 9q-6ZV d>QfK 1\2VL()ЪK?$ |Rj_mN44G&K ޯ$"UJbD?<n5 endstream endobj 544 0 obj << /Length1 1378 /Length2 6059 /Length3 0 /Length 7008 /Filter /FlateDecode >> stream xڍtT.(1( !!!ݍH C0tw#!%%*(% -({׺wZ|Ϯw?k'g*#(>~$@AD  @D0t! Qh"i!uw'@@TR@L@"EPGnD o$.W !!vw8@ApeuFW G .){E;#v20=@Ez@mh&' (O0 @`( "5M pnN7W"w0A80(kP`/ {aN`k4w`xn$s5"W-+mP8ʍW0$vo:p߿[6.Gp;TM/m"$DEPW b)ˌEBDn`(to?ODDɎ6CmG¼f 4_=Aw~F r&O^I1qA?a'V nH}M/\4k$Tc,/wCNN\ s @-CCV jswo Mf>a~;Mх (Ԝ`p. ۂˇpC ?*!_:H7M'AZ6PL:`@ZvC ssDo H$ZhyoUC^P8 ̡*{'bstIgsⅯ&sWy+rto"yzKSCa(u.8deA0 G/'1߶21xgX۵«fuSm3-/ۼkƾ51'K@e=Š%7_x/%ddwu h'ZAIOΖۗMO^)/r*5}ss՘{B=P'LFVihbCzV#0UsA6KzךUjV'R=&91t}m\ ysƱu.c?e&4HQ62֬"~?^3WJAApxm:f>,w0,/UE_B(\rMkY fWj6{_ĩT,f̯-3_^jf !ev孛*}V_)aVPuHR=?1F K1ܭf k(qiq]fOZT2U@k:S } p,OQ,l RVx{[GY7%\IG/.;9n3ȡ(V Ѯ y>eFܪS3d[L&$;|zY&IVw)B0D>Ēʭ)oE9xg!=ސӏɀuv/-4_?_9qE/֐ @"9ԡHꊫ]IuȩکvaF]#3. X-4Ms,~AnaQoΒPL,?q"n,+[D@n˫i^9$+9/FA>'O3/f¼g*s fGw.!8sw3o< ]6S{-7a&* #U ? uܔH6Fj=Vq='ky6@p,`{r14'YJ|WdGL@Si⥁buJRP_?K@Rΰ\{Qx)ժ"Sxߊt&(9$Ss}b HqA0Wi棏θ_ynj;^? >%J⣊ud^\5IVTo+OQwq{#:%s8[%?{ǽͦ=x;n:.f%4AfGK۸=Mԭ6gNfOa6ݲŁLorq#ҍV]&<*7m5RڤAOˣ.n)z!("Դ_Or~*4ȃ1,97;BM G掍t=~+sz(*`-UuI swVhR?y&<;?>d(} ]Rya(bU}H_+UfnmJdmHf-}5"$cc4!p-GʔbL#fON ag~az@Xφ$oDglA,-#:){=/XNv1w[DwA^OpCxɃ< >2oh=0]LTaȪ.Sfz8π_Ds{1ۺ0x v֬q%|ǭK{Dx9*c9oľ!T]8eWuԸiZxv|f;|wSFmbenQQt|_w+y +HpQ@DFD msmp~sj_ӗɎ9JI?(b qq K Owܴ ŷyJLqLsx}ϒ'zg̕~DR!]u-Oiuo_[t37VrZ\ nDFh;ùMeDm<3u>ǵ.0Q%4 Û'tkiFV#V[ڿ:$3|9q%nL/1\c tVUp1YO,MԔx'7&o=:+SPV,Zq!au`nlYױs3˅tq9|7뼌#6s xR<y#DxD_aKlL$%B̲ jȹ8iܔ7!0=8}qrwDMNnw_ 0 3  |!&ӂSA߇^k}a j|alyEV6\UH5\J!MG-S9oSW/rq*5buGpLwq3դ/֫>+?Ŏcμ_#>n⛘#~IA}8e\s x#\͸Ǘ1uݹ~3`@%B,I.#VmoBK7 :"uqL̍Y:~7vG,x+Ol &=N0;l;{0j.յYP3dZt9CT_f`m`ÿ=tztg[c{ 4# ϔk|r<[jw{=p@S _] hI|"dYudw>%NIMiw=E5SPsjeM֮7bCGqS e}3S ~O_PUn C%D~&=0Ircb\vZ#c4F|Ux_ظR VmŴ[ FMD4b'ii;H&W*LJ@)FZ&ŰWۼ=mj^ߖStٱ"Յ M^g e6=sEF84VlaXUQVب}4@9w SI`CN͞4uBP 0le'c>ڇcZ¢'ࡥb1ͱ?opcXf$ST-;YU{0|wz]z~ yꨏq2F5a˛G:HU/QD.et(BW2j]6M`ӅVYp~ZzM.a`t~7h-yZ<*Tm;$=AzG< @utA@& On/ߢ zFBÎ:Vyy:{6=eReı[ăl8d vaniO O3.pvQ~bz&x !V&Ӂ@[zXS˼6-G[= zzkMN\`P ` poÙ#U:8O1$-bÞIiF!T%mj}::>Ow]F~щY,]:tK}f6g^,>)4_khZLNanKEvGiA;QӼ Iq-< oBo xN)^$7ge4^ l6Ig 9?榅F!JM^f87H'ȉ2yHy!|n!"jU嶓;}}^Y2I({51V-Kw {҃Ń 6smGVZ/ tCM7q)[c/&(Q鷊eӢ`3`w~1a_kiç}ߞ:VcUi+ep|rAO[ӨRYy9)e$5'PcGGAy )"Sqdy @=ȲX)㷨ӦC3s &zKNb߂6/B4D|as7~tYKiqzֶ;6 䶂&! {jZ\l%@PY-ADm=[;.K8N:2OÛ5L$y,Feٟ$in]^튰Qe!W:RMkj6#S)dR{G<πպk;+V - ''.BPUZܰ6ǻ?><"~mUΉ;wj95f|i" =W;g(v0J*3~(,mw`yбw2 2Gq]3^A<Cz+tFY|*jCZ+լd XjH(ps Y902F)LP 6V~?$>WH;lSvL759="zYYݒughԗi~'NU]-ocPS]K cm wŏC?jcxp{RI9_?ZIgkΪ\ >$jE;jD?[׌qiyki0rrē+w (Hb.UV P\Ϯwiٟ:|I)f1+i舠㷰S3񻕼)b^V={޿|j =!N9چwyQzV~.xY8 ?]ǚaz!ŷP>7d-Avv+۩.T_WI/&> 1RI_O8Qs{BΤ\:r_r(>"b*f "˼.y-IZEyN])н+G*>K5Y /:hm&"JrvT>+mg-e hn-Hwz)lOplK~q? 'ѩ(Roc endstream endobj 546 0 obj << /Length1 1989 /Length2 15310 /Length3 0 /Length 16538 /Filter /FlateDecode >> stream xڍP[ Iwwww='C?9{UTͼOw?-ku5SCIE(aaf`ʫI33XX))լ)5NV X:21c𻡼= b `f0s0s01Xkh3v23dA@gDJQ{'+ K{>MiܜNV 1hjoj{/|`#3 = l P:\fJ(S"%@ U{s.2). 3=:@UZX,#+пƦv +Pc mƮV&J ! 0~?9:Y9l/7,2Έ'f4}_wl _dn23 3Fu PZ?6"Ŀe@0 tM- /{ >^2>VD/gcW #Dff)`!] 47~'+w.{1zϓ{كl=63ʪHQػ3Y>ۏWdnwߔ]`޹@ߍdEym6Yzs]S o> k ͬ\Vl> o;L lj\y읭:aYLLG>d6杻{kK|Wdjoװs=ߧ f02{>s{'Ŀ6(߈(70d0JX#.{STFlFA<{t7}+acKbg7d4d02{ZVRۻ8nb `` ]f?{Ave`e{v}Q ?ja~Oc-Xn ?{~nCwg^'ջ.NN/׭MMyk?}ٟdaC'p=T{f͋\;|4)$JRgSOSgV98)όT-Ø 5gC}fO ׃ca 5ȮrdЪO߾Z6ڶ>'LkfK#z:cXi4TR-mƅf[0z͸y|RTc)}PyWXG`jWmi-ERݍUebb,exS#xrVR|I#Z߼vݼoܷ]}5p &We~m(ijM%̑ɒ}\y*MK)v](gd#ICM1y͐rc$[>Is'#%<-b e,4xτ<V aS8옟$[?3- =Sn&4]q7?0˓F7׬n9atkx&MP?z$V>9A8+و?~zx{q4--(t1ԯ:&M"n:LDpce(E t vEXq61 POYn\dg1,_ 2z7Z˶Wy_$&}$a+djFW.*FY3 dx]|Ϻf$X=iզU(Fm#8* vcK,$'wͼS%iQqgi=w 0}.,FKM~O} t饜.պIo2Qj7􁎂JJ׮l<.C:n@s]w `{̺:z=8m|9~mT/X7V`{|C!?hT%r@]?gK&a>2k1 Fh~EY9$4*&\%فp $v8i c_DIP&6ͽ}j+Way2 ^9nS0̨1oV] K*rSI9P<`Cm%mK ^՝!Z~5M&˂hM!EERSOnsYj'7_|9+oN8U}VN^dL4:lt'jyH= <]mC=ӭ&O:b,P8ne!UQz|O.Ȃ|Q-V\29&"h/(i=Ǚ4>fZR۩/b!pu-=hsjMd.o)Ù^4o[o x:ƲT,J|۠ٚD[~ї%-t*/8 1$ ƳxD-J jAa~\dIkk@,-C# ԸT oˏ- ̏T(*CL0ϧدݛGtv'NI %`Gh9lӀ96}q%;~pkn hƣJSP<%,n{/~TQz%H0di̘K7ҫ-Y{۵·]T%VnYFM bn&ﻠ]fRB?}&v^8mtLY.Z줬Ù}*zs* Uھc,4<= Ei':5m.>:ldsuA]pgnti4O3\4IX!<1 P腮 {æ=PZ`]C'#0N xc񆢜!*`~aryyޤ9E%̣4>ޭw=Vxx*d?e(,Rp_֧CĚ/|Khę[$e|i_)c٧Kb?Y:4Ǎ2ss 倘 #X2Dڢ=cD[!|&VnqmqAzX}s6jr Mg N R}}k^ZҟNZz*>A:pbl33 T6=9<#a (I!B4\^Iv7*NśN//XBHM(7 Y0|H3f&wdB"".e"unDo$F+_hMŧ] : hwqٍ=ɞ'$j%3g: 7oDo~X;ME1e˩?%mL'ϼ3TZA# c [Q Qxp p sqٟ1=V税*# F$eoCk8ۺlLABqxuRz],4Nm5FꓕpF:lŰOrQ\m@f=2@让xM5s繿9l{s[g6lk ]ϱ_gg>0Iqa[HzΪZ Z`0[}AYuplԉ'pz&sxQhވ[?TԲA:#^M'vZh8|sU>+Mk O!O Z,scݤn)> P EWv(q]W XedP6)RG"~ȩ*JoЄ/rAG&,j?*T.(C5cݮg' t ֯W^ nkgoL{_ο5X vȄ-.蟺Yɍm7d$HIW?V>h_@0cI]PJM'b"oC.qOdQo>Ql Ǫ}@ aw!U"6h9_2f (sW~jC `|l@na;tM+s=uس_Pbw4mQw,yzv%pXџ.@OX|'Ly f#%]j+ϖv6{r0T(1Eꑥwr6x6W8A4pL=cL{^oRý[>i\-Y)uߎ[Xsa+5r\s)7ZG=ϾگՈ^Ǿ'EJ|dq 7TaAX0 5'8`O&H`@XuElI ;rJvU_1?:=z?-N8A +5dj?!@ݨ_XG;m+!Xk}s)? R d4o %c%ݤSiVnID^~'7Ȣ&ޑӝHMm5ɜX6=I{oWd<5ƒĪaᵤ0V3;hX(Al8~EOUv*zV8Bګ{t3vF`lP_«FQ &A)_uzѦ=[ b1'T1ٮڮN?& Wt!ͻIrIiKbv }' ܊R Yl`J Z]肋K#֞NrbO s[H Osu@w1NDJq:lÚ~˩I:\\&}ZJrK+4n`2TӜmNXa_nfμܪeaLr"ͧn%Hh >vK*QEji3:\aXNyMH|Vh)q^g`r=jVD0V0)enV :'dئE0_-IPA8NDVS h 0Cai ABjP ෱.:*2EC9R$e]/^Q*>})#w!N]5&AErmB8X[14̒I(˛RW2Dv&}ɍBFAM㭺_A5yeEedY s1*^+Ό2t 6X9)6Ăd͖g@Kn[$+rY<',@=PDͥO\%M.21BFP344bTAkT/Q!$ԖyuM#7/.޻PUužhPNqyxfv$\&(ZXd.Wtc a7jQҥMH1[OiK )7h=|0Q,ԙ~ק @b  Qn;nZWA@NI's[sr.j'J*V. uvN̫+ ^DiɱsnPc2~mT&0GRovZswنm{.Dx`]Hz)oinM^>p>o[mssǎ7coj@AyӀc|kd@3hxɪyЭ,p*jl:Ge֠f9w&.8s*(s&Xw2$/Ŧ)[>x :foNf֟1E#U_9!7G=\:q!k)0(EBHcU{w|ތbTc鯧 ٘1N4[0_'=#f'QB~~?ɛEc#WlqO?T 0HЮ, MRwcuE(>}AKy5 AB:Lg$ͶQQoӓkr>ƲTLK$IsS.:}ƭwU| YJ~6ۼL:A@' qm"֮u 8u~d[֮ƗD+[SDXJҲbYP5xiQ2 *&R  uRFǚ)'ܮ~8⌝F-H&>fK-Tʈ!IN_=Lu{c]gCy cFKiV>$-/ 0/[*+#֯҇B2u.N豆׏Zy6#>$s5w9gIf+zKӣWl1F Tԫ}X8,ZSI'MESstH&xaV񝉧eo%Cn F::W UOm?8WGv[^РuU+ex9M-(~8 " b\e`ΒH@1/~'JIFzg[,qrM6WHnȻ;b"i{:4jI^|tnBXs3\ߍ|(..pSuFfyJ`:glmƂ,~i5 ڣDMvs(}r40E6Zwܻ>+?:]RoLIX_jI; ,O9# b1-1]̊.ďm6ҩz!iO+F4$ tBRv\=-{+.-ra-}3jK4n81&;!iq5WW?nFA+uW}ߍdPX'y'rW y%p÷>6\醔רQk HRl>Q7&crpYê;v6mIZ]Kӹ#E";= 4"' 56-G'Qawd帇 }wؗu9oh%Ρ(x!Uqf!l<}w#ЋKe;d-^EdRwz8l׫֔:pK:IYOGj=4C@F|{N]02:mѦGPӽfv@&G~kerV=$tqijDGɁQU{QNw9CCFn}ag/%$Fouhy &(Adc9КG|"y .Mgo]񵦴oLޮgefZd^gs۪;zI/@~#I.g"4lj۝>Sr{:(7!fV1.̣8Ha³_1z Ƶ{ uQAF'] n%bΦ;&4N.--f˒-ٜӞ -xS|m+ ^8#Jz%mG@PőaT>,v8Ä.rt+ z(Ae\<}7aDJn WD]^Xn_?}.<93<Q2#g&5`~U V*m&On卒1ج5zǒ$?2t8mı&&Yo?'VD -WsW~%7N`=Kq(fOa#~qvKڊ$!E1 |_T$G'ԇ_ ߆c<#qʌվLi( 1-$G bXc[=El jtʻQk:r `q6( #kTHSOkyK%e6X׬XS/O ,SqV[@HGVv J4Ƥ%AwM xۮ08\}Xam s@+ʅ`7BAuNOԛG|7Ŷ"\rf\6Ɂϔ$5<X-ԛp*I=yyЫDlc~]byQgͯE 4o$t=%KQ!Wߢ X>!#ɊFEW.EMAs֡F֑OZ#OMw)z"vmv 2 u/ȵIhy{c|\){gu7%7S < g1l? SFLD FG@#޴dciSc0%|!ɡXl _0ϏVGr4@eɺ}|RkVlUv*U1Q<1RxELzM3P蠄s|؂uz (+""UK?p$>bI@ Q<P}1^Tza}#|T|]8=; nc:tږF~aAYFsTmF72^ĴqBMrR}PsOm[if&FX?<ӇU6?k^č' ;.g/Syqm(s==t~"Myr֠V *?X<ts'" Wwͫ^ =R=Üf#i "*Swe)DŽ O Fr`icߧ_|V'>ZX6$RF~c^e0́T0W.Ӽ6Jw]1uͯQ9LZZH#C}@MX^xo6}+1)UQfJ'N zoE;9;ͪ\(` gW|Ke>,+~ {+uB0V%b4y!ż;~i~(QC=%& +):6F\=[䰣ݒgm?q薌at%|@/a[̾+\tp9j ʜ3Ulx0E9FQ5ޫ;}  2"Gݬ&-/½*O3(EЕɖer!Q7ySJ:nlpJh&ߚ'+pyVT [r;“C8ۗ~[ 5D>UX 'WgSL ,RfuJ"-锢[#cvrw'O;e '~LcTT9CM˞CqIhyD e&>Sh+Wf/$/ZI-؄Xۼ(f1AC>o7 ,>_δ (:?_k³~E nGŽꞵ´I3da X'kXcFI0i1W:^'O&w/2Hx `T䵈>F Q)ѷ)3a' t5<{ 9Z_o,% u{c$0 ]/ݙ)ZLL Pv鳱tM@Qm3}qַo)Dϼfݹ/ft+|rsgF AnJlaNށX*~B1/vɌu"5oFSCS¤5I`@#$mFxh_d O *WB_O orGN<.֔$ ;-c<*EyYcv P)/CѿzoaiY=%c ܣ=֊?lWlpWO,^qcs@JNZ_TU!,ϕLa4(aYOw{Kڶ% 1^OϺ* G.  oh(S&56' w^TvSGĐڰueIJ]ef<"( ; ?_xAhM rZ<8@gHyd {-ӫ*vӑi5m$J1+]|͐!ztM^18IV|uYQaSL'r\=6.bv0z/EER* $Zڳ/Cv  9+JHSZ:]\Q_tH6k.%F4Z! 5VtW5H&26E"q_w#PeM.MԷGf&:נy!;i>MB(>e[3E}7kԥ WT^gWc 9,i_4> djYچ5R,7HMȶU^ySЦ;S_ yl:Q-HxyHCqPxT<34QN%Pꜳ%`c/*}͢*&d^^hdmLܦk%Uk7r"o=G #7KNXH=?V0F5k(9GYDTa!k?F^Wñ{ib*Q_jl Ix}$JF#)#&o?r+- dcf6̚m[5?nP;K/D5F6CY1qt|3ɦyLvަ`%=~A6bly+-lSPK<.ZY쥞!r I[ W1DNA]ݓ^IpO\|(ʆ$_HFΐ2%A]ߢp8 /Fem?df -;p#mf.$= >};G. @Աx<`na5Ԉ.@_5&~nؑO$fnA`%9X d8Hq)&PBA֩Iz/B#iv݆F/;m;mNOM ;RVnޤMqinǟ|u?vD5זޏ& , ¬Ӫj*a18/scՏhH=tDzxޭ̽z9?n-2Q2̜GUb)IR@_`Ѥ/1ҭiS:!bY^rh81-YGvc +9 HڅqNa)*u7vx3NJ#iR00֟! (ixb"/k(EQ.u\ ]%~g@ȴ%TO#+ɴ{ 7̵c}BOoPmgR t !O*l+[v@3w`xcQ7Ψf⑊$5aH(< 5fBtyҒ`Če7Lf6oA@}Z!Lje.Yf\F. C$}sT*_?. ʱ%s;6$(Ѷ{ 5 ~}*|JM}LuP*{:m1 cdB'b 6ԙwpSԄښm!X7a `p pv*$h\r{afޔ`m,[)9$Q ~s\י|D]gz00Fz2, Қ?df)uG4T}N@Iwc4w4{ .ebsN/; l~R4h(*M_-Z'Cov`y\|'9fdυ<>ncmTyEL6KGL1{ O227$b{|K}cQ_).6abv^y=/zu6,S<#>aE_5Β.,MjK\I*~CZK0Y+,iu "X7LD\hǿq~bfoi Y7FTZz-0EY>|u1@-=CaG:=Qz5{KgSa\y7ܩb2#c"bBgrmS4RFm* [v|ӝV]0R1,] 4mJ%ыrd ;)7k,c_mBRsA%F l*KQBFٱ}޻W D1yUayh#G>lWx>&bӦ4s.*mNb#]Mz]G-z0K "% ":iҶR}!B.V 63+}h:/ཱ F Q+ok}39y,Zvˤ.qPpNfn!sB(nȍgTCUb.5 BMzA҆L/dz/ǀjaRA:_aW/pP tf/~ݎ+,JwG6LN Y[g2Xkz>>-୩'$%h"T+ mM"7\$MVChf9UvҪ'q1j5޺|Ύס|c%J+gl!T=\c^SwF<§5H?w=`V?+k1DR5}&1EYMtu3=0r&~9oaycM*JM6_#JA{B\bV}Zi S]iw+Ti޵j̆ *O*4)!{s 2w#ΕMlr*j]6|_,bSZjQE|y L0/Ҍ $2/-\P"˛ T)4INn0D.=* hodݞ:ȫEmq0I`ZUv>cBmXGOA>E᪇aTvi"9QюS. mHJ򓘵D0z#; 32c'b}g<~pji"<)ctIpUZ3ݚLMB>Dv CfJzz7=yx O88b l @d>\tƮȥ'd.K)hjKpL>4Yit|GH1CS|2O Y T; ) gV Ō+݇Q5'L ՂI2"& FN45I^R).(,!X endstream endobj 548 0 obj << /Length1 2037 /Length2 14664 /Length3 0 /Length 15906 /Filter /FlateDecode >> stream xڍP۲-,4ݝ}pw 5[>dUU1G+  G:&zFn'L o3,*d!{M=Ndt0ع8̌\8d ,PQ8hڛd ̀'X@F@G1stf`pqq7vٛQ\@3WYkߕÒ+L] 쁀w=hx?$! ,w-w`gݿ"2W`bnȉJ;: l 4r8[K@T@`^s07utw0DhoYXdm qtK= "lTl휀y7ljd-_Nxy؂l&EM`= G{'?`FC w3o|{sW1ϗxl Ңr4W Acfc01q18?F2Jؘ\}QrPK>@!fdc4z+=_w sC:q{['ὗg;> .$ v|]Xwd5FNo4@+ved%Т.Fׅ`a&Fm_H_%?|Yg0y唹RRw35cps穃Tw>)e&BƱyKc%4E=v)i~֯J@?JCq%Ehzx̕s~L6E\i/ I]^%3":61ugƋy~J.ύL=^an_]CCAKjH/lDIllmɧ P ,pTҜdNiG@ I8tb]^Rܽ/meэ6Y&/u%xTzP8J;aXEn g ف` nیEJO&Q &-Bk\eI4rgωJ 7ˇE;)g07VY$˴(M84Tv5E:A{4j" ׼hM@/v@ I&Y/B槸qD¨3'[OD dzC!\:sj5f+rYܐ6Yn[N4vX bڝu*'QE~ ;.J(KOe$X>edR_9>$)RqZ =%6\ ` X89Fm"k!bJ"Ct 7%W |i YVv6l('8࢟= <Gț7{kH,֯p3pY R"NURF*5wz.,uIbb]^Dؘ}"uӪ z$sG\ΘJH<| ;[" ;ګ$S%0 e~/EO5219咗&é%191z:~wPV8=#Tj*9_fů.+.IOԳ{ @$U\gqnA ¿)HybD5HkA1tWy8'Dq |n[+[4)\B4E櫵[Lsm; H#Tֈ̱YvrJfR5^ͬEn+LYU )^Q 0>I􆹻,#rQҕK?$4fue647\i`SYm}xPG'2nH8_XAd*]4LC(qK@qh"{t񫟍O@i8H-itɤpA=Jܚc5T~T=gՓ7]ԗgN98klBnaQCQŢU*Nh9X6̱DBǮͳRqh`*нn][E@qHBS=[a@=BV9pXFjyn4v{}.yjZ|MBƊq(9w5Y{20]SOY:AX[G`cvZz %gU.؅)]9G$sY2_`ELD埧eY$S5H3 ٮ{_mYMMK`,q~A 짂J7AM~Dmy 0E0iFӑyBY CR cQ3rAu\]`edg>WeYcMp"BJCZcw{-q **tE_oE"%0ORm[L' T!0Ѱ롛v)LFͥLO!@H@IWwh*{QnG]K(F1>@$,.ޭɢ$(Mz!.wk‡̅#E3}–!D;Xv2s|0%`؀ke<-#z _J)ZJD`fa8Xւ}0tmRLl_/V{*0H@.lNLZt= aK4qE׼v|<=g԰,o \ae oF" ,.=A Vȥ0d\Rf$쑲2:!0OZ :ҜRSVXCC-{R?^W&8$׃cB|]?6@-G}ֳ4\[HYޞ]'$ 1G9Gz+tHQvPUà)іA ,ij`Gy>59ӑ;n&Lc ՂŒ6ˈً@H!|3?`7wlS3SWcr#^a].YK5h4%= Hy㧠o -^ D$W9vkD[Q)}|Oe/ U)EZjB47bRTE*KMYu' lws=51uk&݊:,}Uhߑd)ETTQ󐒓AȰɜ:"rizb~K!LM7!Ak'k!4,?Rp~j چG-'iSA:}y@$!̧-^!MKضwkƨF/F1'W٩_8~"||ܨgSPuOGJ㍳TSNQ1X AUƅSN aXoL!T9PA?1c_(,9\]^+0yW^{{y&)'1 $s@Ѻa(B4K%B爞Y+!en'G#qXA΂Du_$Njԯq%(5 wobnw"IN]=}mxXPn/p3`սʸZt?A$)SاMnLFy೴QJ:*Y; b NY Ǘ;d/ho!zec!&^BƎ?|-[T#@Ё(8g|ЈI94COȀbL .pa3UV>RXRI$1kyo.Ӛ$K8wnŴо-KU2pq{HPyq> v5"V ؍SEq5|dOmB^ZM)b bȞ 'b߈[CζT7`1Fižk-9\"].ʔMMA-=6'"HdfToo%d}:z@Nb̳Pu?9=b HqfV`8R&3D.o0ID$e[wlɿ,ڲ& h?`),s{/Q o?S׺ CboB?nI*wbBraEG)B+l:s3qS;|lҜo$e`8axy~:pT>18ȔWejG'l@=A+'!;Ȕ^6xaPvɄXғ-L* +5v2H!޷<5FUB@>ګ6+d" @Kfs; C!J¾5+BbdvǩՐ"5pbhg'~C6Ͻ5(L0L]:#T( F fdK=#}zb=2O6beLjo r:UÒ mЖ? Ugrpt̰WH•|n߾Ii#?ZleƨOQs8N =|mGA "yDDsf(8 f{* ka`HE}_9 XyMcH$ze,5e%_¸kES@5l?){?xfsHW›C#ex!)&<ۨX'bT-. ;7QPp,d0_K_'Gb7g7&7rBBdn8K1B=5/PVc(;'%U\7 !{(m7p:` opo~5\.}*z2u}ZЗ]9m.-giA|kC̺W^(ƚOA)6P좁EjWRŦw" P`7ݺyJѱaRMJHRuL9EFO'|(Ʈш"IkT?V<~nEXf ՉcC,kU$"ͽՐha<7ZÊ('75"45O6G qU0ŧ|gv#kIn&3f֣PF05:?l'+d@(J`^|߻`Io`#NtO,ۉ%%~]i6W?uas  MaJ15"v"uIY+tsh%lAᄼR':ODQ p"4 0K|fݵWe*i55*(eƑC‚AI^؝}* 4 7qwr|+8_lJ`UuTf&n3E)d^F$_RZȲf :RH.ۉE*\bB3 RŀwVwU0&LTs~~ZET."C oTi,:o&/C wPߖ4iQ*Ѹ"={KG휋lg񩔷g$oXUX\[<˝ .GQʱ!$ht,Nv /,3"MEAs]9odScXe Io_v7%m K7=5L\%x>PYEϑUH,ތ|GIĹ}+lbiSbTŷDa2+p ˜9fr(~cAǃ*EMDQ|Mce0r @j/Xi. "6N}IC'k7[U?ђp pC$Gf'Qq{ 31k.%v_S%`xMAW4*|ZiiiT4o Vm23(U. ϻfԥxJk9Jprh"t .GmBF*R22 sHaHh$V+輓 :xN2o;0jy6gBьOWEIPHpNt)q5qfk*98q2zGhr4ؗy$c}\=OBm''FlN?#$[lӂ5\94vDʕ8G p]ʙ-ZcLWt N[[r1 |~a͏lĐt:%ghoMTҖoKPY[$JL{B,(H'L"F0

    ˧Ï66`5{|΢PB+JQpsl]x_?ٺf֨aKcwDkkTq! "oOG$;Y.:8 az:Xgc?L3b\cNZFpےش.bg`Dh^Y"%Ս絬v BD ËLU'&1xlLAu SUf8g`/?ėjjP{&wuqn %<҅?p!]|GbfeEm&:3f{(K#<l63փaW^le P! ڂ?<@I57W,>{w9D0\ d/mS958T[DqQ9jY.xP^0\k&Dmw@pFP/yRUsO7M%ySIɾ 7ŊXd߶xy1ɖn^Ɋ6jnVZ EъNleI֫X:II(u=XwVuP~[-.dl˚@ˠ`ٕ< Y ?LȧQwicZ0Rf.8> s^ܡB+dCcYźh*q_N(f:znUIm yaI^MXAhpg8x0\uMܾp^ h-IZv\N `ӷI xJ!y CnOSbi˟QH$Uq^m88k!tHK;p;2d~N.}6ႍ`jͰ*.Ig?~:%*y3HfLSXRN%kq46Ưn3Oټ+{1JpNJj B60émR3ŴN*ǩS$:@/"1 ͞7|܁-Pw qvwbbsPvA2{l[~෫QxT+o,tW痧6Vu2P>0f#Vp@yc=^M+ %TL֑Ὠ|p :y՟upTDƓ{n>D6=Y]DF򊫢j啍z@AQ=(EbgfP'qM6FXǐG=B:Bǂ2uXij3Dp;niRm1aPAivo^w7O1fex?SN̉X,2,K0&K:~~5@K2H% C#K{='¸&æ}[v}@ZP  Se`( sXY*.(ƀKx^[b+XJܙ.P",acHY諌B65ˠ̔bC3BL ݛM7`ʀ@D*W<1rp6x391K2 ~,Kx7jQY$1FH3քRFn`{2CSeH?(eL>f h У#㍸t&/ϊ0O?Ï;9.R+摈a84٭C5o9C6|)iiZ])MuyS$3ːwnA`#@(ƴZ?'a EY$Fw ,^)l0:S"k5;-0} 72$ Vv 4sYZ%vE.ӱ7עfzFGn@-ѩS+ӺڕUNOVCkrKT{tKIQ^)fJ6ZAwQp JhX59-OGSK3s=e5^f2\Yp}#|kfIMPIҠBE_+e=n/HXUcmHLG١$RqrDLƖ^X@8Jp>vD%sT}'&mvg/9X!PT-V}}vw"K#&Xh$~ڮvBRG8H~v,7'?[o}ZSN'ZK5Jct9ě%8;k|P!3{d^1*DY-V\ID2m~W&e fRhctPnhg ӗX>R(>@W,ZTP".Li0 ֈUYkj ݷ>FZ߰(oKD1 D3#^"rQ5)B}V'a>; PghA8aW9,ˑww/g@giQbe0QK2𛉆$Gjs =56<"zBU.$$I_'WO2g&mKtS봨 ۚXi/O~A+@nje2G[p78 W,=q.]SvBu]o;~}jֲ8]nגO!Or_ ۧ!ڒևMD 5g[[52xD$jP+l UufISzD:)@jPrWoA|}3aʓmʕ[Ht2 ,BY0H^!#WiGDC[UUuZ/#ۣB-dOD,201}|3/ w1h cc~< hq:6a"8{q*DSM8@|Vm!QC ^cUKqǜg bB7 '!<si_-.ˈ|Wm62$F/p[XURs2 \Oi9EQd֓ =ϊXj[ZLJonư -- ȻD ~,.TZ.d gt"Bk~ΨjY^%틥⡻N=ܼYv8(U!N`DOֵ`Z\hǧ4=UqrQ:qa5aR&ێmy;BḬmliCPf}ѯU+oSHݶn}ӫON\χeninCB$HUUu$9OA^@{5fa-X d_fE<,%#epݲ߆wzlIlQImUrwq&Dp ե#.ԇx2aaMSkEimQ95ÎYm{~' IV,qF@.:):U<4U}Ѩ7K萻jWs&7.CGeˈRӖ}9UncLůڡ߱phۢ544%zwwSbcgM^-^-pKC^X˔)h,C;l/*{*=i—wB!F-',+%N%v?sQ .(_L)guz`0_]yD6E@B?Ý?U=ErU`6Fr=]Z' x*VLpA_=-̝S KyJ~&Va>} r9'^a9F @ik?=@jO,=-2S7|x1 :~0kޙDÜP6an _bV!&U6^Ɓڡ3i_Wha:D݀"j L~VyZM:(cСpcm̵SLxI!A0}@v)OyAܱ~["JvHSNO2˓r’--;jBg=MwG2VLUCX5aʔ5^쵼ͨOIqRZm4AH3I Rg'E\XQs6l!PK 9!B-o+Xb4A'%BFҘ8vܘCR͂Q2Lj]zދUH*8JhWp ʹNKvuE\Ȉ7B:Lf'[Y5, qJEv%o߂-ek[(Z~v1 J 2smA^ȶJk6]obE\S'$8 &Bx;07O&.jv'NX@#ӫn *~L&(ZU7&RPG\z.{iGIƥB] H f|gy3vtk75v~`]-̥^J+GfH0YqŁT_Q6#/L'X[q.6L~l (Q' O;3E\ChH5'Kl{u $X-hmaE*RVT̉! #n.D+me R endstream endobj 550 0 obj << /Length1 2171 /Length2 8362 /Length3 0 /Length 9640 /Filter /FlateDecode >> stream xڍuTlۿH(fctHNaNFn$DZIT>wYtydlV0E$ ȩ@0/Oʪww-&`5Hr(-ԑ@b@ !%zmU$F*tAin sr8Ppk(u93ZCHk8_!%]xnHns._JO1? ,n CA=aw,Oſ[`vph1/>|` D9zl'ΗOGND믎"~<~0?@P@ $* w-("=UH_IO?߫w, $zfa#nZ@AQ=~5zd=㯎D/M a:Zw(z dvNwS{l/s#`ZH7;K+kGᆞ_*zmQa_B( !Lj_PB/ xHw ]"yB>_HS>H#Q?H=:o$@ABhRgSX:kH~g`up$w@otݶ]u=S@6ghf8 t# ]C3_jt3.`.GMo)_ vA/.O s-kK rב 3GXr-Ϧ.1R5;S>N!I'SdMLC ؃NO}iĎyv[{_'[E"i|VbŊF347/;ŷǔB -zT F̐:>p,"jUw72uZ!Y_FNzn_>(*~R +QO2CcaWjcN4"J=lG|:^U Kr/4pNõ_# ƇֲXU0S+V=M''lj{UԇJp*1 .78!W% qJV@H^I!pcHiL@dt̵:A\hCGMBL?_~R.9r+>.>c yQ^.ݧn|4wS 9I"!-!ʡJ%I7^3QhB*Չ_/Z9ѐHWyĵq4,°޳3YN^z4\#Ԭks2'١ceLz0m*2/>b![y4o4p2Yxۃ>{Yt0ZiCƷPA!{ݣOԘ\*v8>NVg'2XUō|5Pe)'ۙƞ"%y%ĽE|Ndye Mׄyl=Cf圱4+ ux[d#K=Ċ hup/%2Tk ;[ k8’0+g_?A}U'=_>tK|sWБnOcPwX)N#c"3d*[{Ha"B:!C#g,)4 Up D"A|mw)RE<V \hdWצ.fЦi^F88iX @+,=2magcK+;[Zio.? (+4strĝ3-&DFOd&nuA؂%c} P#jgJp4"]"L*SL,p4 [&)Oȅ9;" 7_8I[}m^zƅa2ճM"M"ι̎qv7S$vYP+>g P9O͆;\|ZJ鈛Z<j6䨍JA Pf.2v'}tQ+m=d{fMHQJ%u5\2T?+1SܔKo?PɆzLubTpA/Ei+~]n=?_os/)h܍LtZG휓z}ǓMopBRwQNkᡲ@xh#bNQ⛅EiDMi-"5ieql7p\[X+>evaa^.F6,MH rp}JLuWӂw'MK>f^[y0UTjt}fE(SKˀV+`RK U.g|ě@R(YI(mEq;T(a`q|xJ!0.B[B'>𶯀$9YţqORT#)K(U\^TCfi/UƥMziYX __j=QRyg"hanyjVd&ӧ*iׁ oRh@A׍O-3 CkNc8S?%1^Ҟl 23E˛2Joyw p+TcSnherk`,:<^EZ?Bx8 i'1sB'->y2dWYQ_.\M{}y^7虐喝|KgHAd봞'0H?6ҍdzOԈ؏cJy;2]$/ _}L0p$Xt5b=~:6cUYn:?sEo>eq]0^Βlcbosxhɏ]EA<6voqOmhZ&?fR7i( w]ìi|y> |qwfYECIB "|bH,X5gN]"ȸn5Յo2c;Ɲod!. j7=O76V56e1PYQɂswog_wBxsZ۟qB`]kb'`SWZݬx.y/4jv3(qkbZPe 2`%)Є*5=9'jZ]IjA|Fr{g|ͩ"rECݯo3XӘ/>yb &. oكD pf!;~vP\ک@BIf[支YbG60 )ga4ߪ+b&"_Dҿ|~i)._KL}[ي@}圓Wsz &g ~a)$F{)*n٨,Lpu|D)wxi_DDbِKj[UN嵴?5!>3:?3VkPv{I.lA|9<=O1@wL(03p9DG}@ԧ{7Y}P9ߙTA:`gkLlѹ~a@x>K1Lk,hTƑ\)O% v tj\t0R|3 xtne|xOdu`Lk..KTD{8zg%Y3 g49yI1SZ]?޿KQGYNv(z7]@;o6,q# 2%1Bgg[^Ü:SHd /k_CTL r~ -5mK'j\SmNdClp: [܀\ZiX"Mm;.%oI֕vS)*hs] LT[hcqj^ڐ_(=)6AfszAhpt]]7hQR߿R\R 1T9o)9bȌ1[K :5 -FL٧='k64" #rC=8T yvxiQFx5  ~۞\r%پ͏Q4nʊ,*7hBr4̶>+{#΀i,E n\ 9*fDMErIƽYu=ސDRi͉^vvoٖE.f U b=o(l n69z|^V;=aCA{K!%>?J#v;{Oi4;ۄ^?Mr͕pM/U]kݦ ]/|'yd U6~(3\Xwe;MކEig"kV}(匵;{އl"mIez6%!>qMCW\~5A_uxoK` ơU%Tz}s~G^[<=_^i8oMymә ;DGtFVi A1|el@GUi>3ԹQ! x3h~u;:Һ:/߅1=)K73[}C1jHsfn\ }mu0+wtdK#I"nAgRtw^` BxAMdH5p&vB$|ϼDJ7d'H;N xL MF;"~-4М!!l &l鋱Z̳+{I2wծu'&na`M)hyD׫E&'<$[oNeC_>xY"͛Ɨ;y5*zhW.L1WUk&;B2?6]8sҲgbR9753pL{ 6G3(=eB& ɜ~}ܕ =1U,'3m<l~RL@"ǓȗwBcb[@6Nr9 esfظ~"+C T4I6*O lV6pi,?<7لB"Krm_n"~?tyIt ܘܚbm"%6^P%{yXvOG3G ]݃(]ǩ$AwTݟ9+M܄o@Hoxv+m+Mk؞<>bɔmQrft)0Y-\5X+{s}x Yen2|bfX=% !e+ }mDMT*BN $}5B=: &lb&AGVyҹI(M7 "elwLDusYn-+A_S—e[NIνMfO^3{S3<ղQ%~pvƵO$x̄n9]b{uG6>X#$=#r)ngvgryߜc nE' ϐ||j[-W}=MTc̊DLaf )eCr"첨e[ :?aZو%v4xyN|5N+H_vΒ/( j#b?{B_P'8HA'M 7ՏS83|yD]pӸ ҭ^e=Ɣ8bX5@%DiWhlp9|bSQ>)ԡ-13ZfdRީѿ}WUݷ[Drt$G~5`Ǒo@M%W/#1@{7 }ʻCՊ,±?.-I^k] ELUOBs1^g *$XBg];h*smRɤAyNReJ}ICPhT9'};$HNzL%d6W @|4+F`%`z1<6 +SU蛥$asLz4J$`PyU^7@E 7 El۽\3KqWFJm%*,a c{ޒt tRSq+hz "; r;aW+a kyGy,SO+_Rti#=ۨ䛋%L#J"6*fIG)R 1_$! ]WNMMF,WX&4u 4:,WBHH}:JT;aЧjp0D'HSoĮQF0έRO6^$??;۸gIPD8olKnu3|f)e}н`&W'++CZ'j_4,r.ampr΅7kir7LB&}eCB ~&z !#s;ɤb3ZpB99ijvT=wqVfl" Sp+P}(~l6Cqh5I-g)^ R:& 01dV [^\^mddMK)D~ᰞ\U5ʀ(6e"7sÔ 1Ŋ 0^SوK l:D ">"x`~#Z؆'=e`Pc_/NEPk3?gmёiuڶ$Iyu`q U3«&ֿS[DWG P qWjU 7ҞWT>,RG^RZvHZ*V&.$cp?hCFJ iK`ͣIR#$~ K@VYT2Gd _qxC:3@[`LH8 Mp/2AV܍mn{XG%d&7 װ>E}Z KGdB2MM^(;jƂDsYv#c(NA[P.4gO?*{]C&%4-ȟaGcڵRa=q@qGDg-yI&،Z8Ht[ATL9'cSֶ =k0%x-{ap.ah,4 OH1\/|L9صasC,p/OןH/," fRo>]v5ʧkrqQN!J$|UvCzgsNxS4ΦB=9|s\YDb+* UZ |T"?Lmq-rrm|o0٪cf ,ǁ SkҟzwŽ[&ugJ/{&̽# 8 ﰯ=,[3]Yϳ76JVaF~5VGڟqRf^X֭9si J5h(6*E}f7'o*t{`~54d,KgneDTaxhRD̔|ork cl=uW$׻kvL.:aP_@U!e@qӅ/2?O\ v.{lZLL0Jj W Rm7RLJhȊ½}Ȧ$:egЭyuD"h&K:ңZ9E7Z! ~>)%$7iY5 Au\A}޽k&iT'Qژc 7e%0yezֶ񛛳=PC XSLT_@!jt]I;G;}M;Z>Ák^T4Jҩ?UO bi*@V #P"򀦛{}Y㧎Gm~EhÎN7ب p&hA2ٌ}?YxR uuIo}5;{_Ǽxlj/Yn ۩CDx݀PdiM)% XU2S{vM&hE~L֧~ dSwn-/^5[1&FW4z>9SxYj'|0<+G*p'/]~6祡l2%w3kol^clgz-u6c+/dhmŔšiPqcz-qSu6"-.|N36~Dmқ8NIՠŝ%ڛeӓ!sqY\4)i=?cxmkdSmKҌJ[o#/0݈AXLTGcZ{a^9JD7IjvXg0!BWU:+}'En'ZIҞbU,D:Чi@}J,5@H6w;q߁ V*_9#d[H$b`$/6zpI~~DRyGv0ItY" na~൰)P)sӶ:%/󕺚k5ok,6b@T)#w1%o8Ӻ]Fw6 #Zuz>0ʹFnXN~Z$}DяͻG045c.Ӳ\e0ddȫUZaۄY:$3}U< ?tB4Cw2yvɹG]s(HcS {YZ"#2CpwS`1 8$Sl!XE(Z``}}a]_ȍsTo`uMDd_VDQ;NyO"jԿɏQ?!y^ l@db$NB0C3݀j^lvdGʢ gyNo9Y\:`9}n6akv6HE<Mss<C_|.+i~QbvAj1& RP1C!tP:v~YbtXSTJhP#՘ҢWtRfu ]|@뷘4A)N/]\TʪY_ަgKןW#SЄW'Ioză0,aE2.[0Uy4:wpd~SnrƋXjȭښAnힼ>?(MZzs8tu0^fˬ?x $iʊA_euT\[NDެXWV!iAH$'.7r<{Eh={ر$CiUhHB-A3V-n ת7ӟlP0,TZۮ^dSHX̽ߤ& !*0 "; تrt*Od!!zn{DZ'ĥ ;10͊e}uz2iš1ͣugkvo9k3xe^ eGC -Kʆc>ɱiv)Gr ~/AX5hb <`Cav*J]i2Ͳ3C! |;BT)@6I*8|vQ0`k!X4`z.\|H[sF.LH JPa!Ʀ1{v{Q?֔?nz@t$EXIP/k_L ҹxBA8П!U[ZZE>Bkzu~jQiMtOfߔ>Ep-OVuV`M @rLMId b֕kWg`:Ո92Ѻ!ha eg<7Ϭ69>"r)ϴlpkoI*φ Ae-[?oTKNٟBfqL0 Ns͈VC$+UMqYnZ? aXUAO Ft@93` d;ˌ&`|y)CILqfC?%kkR/T 0WKsnzܼ)+&%. o;d$w=>Л@⨴9VO.th>na(@Qx-e߃QvaKJ۹Xu 'k>a9xufdRtU %~&($h*8?ANȭ/jIsoɿ P&jvtL[}ݡ&|[7A*uLߧ.Ϧkzjþp2!|j=6C0Z%p!8F$9|JYNgi܍Iߙv}"%kB~ ڎbaP\ [SDDizdh_nlby63.] cʡkIzk('?;4A8 t/2[KC. Yŭ( 7DшW~l;TtTaR:a'ǐ:9Feq+=kZp | !3plX`p0'Jt U h+~St{,a丏Q PVh^" әW'Q8oUH5. \{iFՐ_oh9SvGN0ۻCZ^ jhoo#IE7opҹ 9[:)W+e; BF؂`|XU m5őASGC%Jh]f١ksGD)~Sލָu ߙu3Yym.0o5Oii>U9lKnBЏq󡾷!ID>PCʓixSg%A(P&≡j FK/C#ߨqJ!Sw)Oޮ ' wyy kqv!ujboD8dW)A4IxFWi9y ``!p={xlfGr}M wqwb (9ZS[a5|ˈT!Kjs }}厳ߎxBz=9XvWvYn(㬅6ک,23OQ =AFedrՁiPr +P'nû唼#yr&u!`(%эe1߈:)=C|Zv("+rݗ5:tg'!JYsXhr6Z} ŝ[$3 qZ]"uNQNoo@,^(pBp}0p8U3G/R̗}iEp2e@Y]=DoO]V6_Qk=fk+eK`6*0aUlpYc\̄g.?%ẏ+#yBy$Dc)n.)ms\O#BZuLA? GR?U /,>xz&l@ΕtM> 0N FB𤠥3A|x¬-FÀݮ>[ rZC,* څĎ ojtfR ]ORo+7àGA-#xE5": ߷q+N֠| ?BVnAG,#_̸Dn$ǺP. 1f^'kj4&6Ym#ǫ&8) "WcTB; hpM7QrBsxL\nG$ `s&)igO-L E@p#)v?J1 sc3bZe" w*%nS }'0U1r`(FxJ'.v3nP &,="!2-gMMT |%rm8yhIVKfTρv(d}xuۡzU$4Y){H)E2o=%M *)ʋTn9Lr P/PւXo;b/leF1.?@)w)$m1 ni>gHOe籞S71GZ$' 4r@3Z YLL|ȸda R1*`fc+D6j+ %9 3^3@8"w rr y*}B1ㅭ\As4‹yА$Ĥ[5[#i5{KӑPdt%#FB%6|h(J+hiThz!fLV HZIa{ %βT4|aIpq7 HG g6{1@䱗Ky ByS1…W}|۲H?h>$quD,p8^̃\xnb‹SWEgYZonedrive-2.5.5/tests/makefiles.sh000066400000000000000000000034511476564400300170100ustar00rootroot00000000000000#!/bin/bash ONEDRIVEALT=~/OneDriveALT if [ ! -d ${ONEDRIVEALT} ]; then mkdir -p ${ONEDRIVEALT} else rm -rf ${ONEDRIVEALT}/* fi BADFILES=${ONEDRIVEALT}/bad_files TESTFILES=${ONEDRIVEALT}/test_files mkdir -p ${BADFILES} mkdir -p ${TESTFILES} dd if=/dev/urandom of=${TESTFILES}/large_file1.txt count=15 bs=1572864 dd if=/dev/urandom of=${TESTFILES}/large_file2.txt count=20 bs=1572864 # Create bad files that should be skipped touch "${BADFILES}/ leading_white_space" touch "${BADFILES}/trailing_white_space " touch "${BADFILES}/trailing_dot." touch "${BADFILES}/includes < in the filename" touch "${BADFILES}/includes > in the filename" touch "${BADFILES}/includes : in the filename" touch "${BADFILES}/includes \" in the filename" touch "${BADFILES}/includes | in the filename" touch "${BADFILES}/includes ? in the filename" touch "${BADFILES}/includes * in the filename" touch "${BADFILES}/includes \\ in the filename" touch "${BADFILES}/includes \\\\ in the filename" touch "${BADFILES}/CON" touch "${BADFILES}/CON.text" touch "${BADFILES}/PRN" touch "${BADFILES}/AUX" touch "${BADFILES}/NUL" touch "${BADFILES}/COM0" touch "${BADFILES}/COM1" touch "${BADFILES}/COM2" touch "${BADFILES}/COM3" touch "${BADFILES}/COM4" touch "${BADFILES}/COM5" touch "${BADFILES}/COM6" touch "${BADFILES}/COM7" touch "${BADFILES}/COM8" touch "${BADFILES}/COM9" touch "${BADFILES}/LPT0" touch "${BADFILES}/LPT1" touch "${BADFILES}/LPT2" touch "${BADFILES}/LPT3" touch "${BADFILES}/LPT4" touch "${BADFILES}/LPT5" touch "${BADFILES}/LPT6" touch "${BADFILES}/LPT7" touch "${BADFILES}/LPT8" touch "${BADFILES}/LPT9" # Test files from cases # File contains invalid whitespace characters tar xf ./bad-file-name.tar.xz -C ${BADFILES}/ # HelloCOM2.rar should be allowed dd if=/dev/urandom of=${TESTFILES}/HelloCOM2.rar count=5 bs=1572864